first version of reminder web application

This commit is contained in:
Finn Christiansen 2018-12-29 18:31:10 +01:00
parent e43cba4769
commit 0c4fed2a2b
7 changed files with 316 additions and 1 deletions

View file

@ -1 +1,2 @@
# reminder
# reminder
A simple python Flask application which adds reminders to every event in a given ICS file.

44
apache_sample_config.conf Normal file
View file

@ -0,0 +1,44 @@
<VirtualHost *:80>
ServerAdmin postmaster@finnchristiansen.de
DocumentRoot /var/www/vhosts/reminder.pimux.de
ServerName reminder.pimux.de
ErrorLog ${APACHE_LOG_DIR}/reminder.pimux.de_error.log
CustomLog ${APACHE_LOG_DIR}/reminder.pimux.de_access.log combined
RewriteEngine On
RewriteCond %{HTTPS} off
RewriteCond %{REQUEST_URI} !^/\.well-known/acme-challenge/.*
RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI}
RewriteCond %{SERVER_NAME} =reminder.pimux.de
RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [END,NE,R=permanent]
</VirtualHost>
<VirtualHost *:443>
ServerAdmin postmaster@finnchristiansen.de
ServerName reminder.pimux.de
DocumentRoot /var/www/vhosts/reminder.pimux.de/
ErrorLog ${APACHE_LOG_DIR}/reminder.pimux.de_error.log
CustomLog ${APACHE_LOG_DIR}/reminder.pimux.de_access.log combined
WSGIDaemonProcess reminder user=www-data group=www-data threads=5
WSGIScriptAlias / /var/www/vhosts/reminder.pimux.de/index.py
WSGIScriptReloading On
WSGIPassAuthorization On
Alias /static/ /var/www/vhosts/reminder.pimux.de/static/
<Directory /var/www/vhosts/reminder.pimux.de/>
WSGIProcessGroup reminder
WSGIApplicationGroup %{GLOBAL}
Order deny,allow
Allow from all
</Directory>
SSLEngine on
SSLCACertificateFile /etc/letsencrypt/live/reminder.pimux.de/chain.pem
SSLCertificateFile /etc/letsencrypt/live/reminder.pimux.de/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/reminder.pimux.de/privkey.pem
Include /etc/letsencrypt/options-ssl-apache.conf
</VirtualHost>

57
index.py Normal file
View file

@ -0,0 +1,57 @@
from flask import Flask, render_template, request, send_file
from icalendar import Calendar, Alarm
from flask_wtf import FlaskForm
#from flask_wtf.file import FileField, FileRequired
from wtforms import FileField, IntegerField, SubmitField
from wtforms.validators import InputRequired
from wtforms.widgets.html5 import NumberInput
import tempfile
application = Flask(__name__)
application.config['SECRET_KEY'] = 'secret'
@application.route("/", methods=['GET', 'POST'])
def addreminder():
form = IcsForm()
content = render_template('index.html', form=form)
file = form.icsfile.data
if not file:
return content
hours = form.hours.data
minutes = form.minutes.data
try:
calendar = Calendar.from_ical(file.read())
except ValueError:
error = 'can\'t read file'
return render_template('index.html', form=form, error=error)
for component in calendar.walk('VEVENT'):
valarm_found = False
for k, v in component.property_items():
if k == 'BEGIN' and v == 'VALARM':
valarm_found = True
if not valarm_found:
alarm = Alarm()
alarm.add('ACTION', 'DISPLAY')
alarm.add('DESCRIPTION', component.get('SUMMARY'))
alarm.add('TRIGGER;VALUE=DURATION', '-PT%dH%dM' % (hours, minutes))
component.add_component(alarm)
new_ics = tempfile.TemporaryFile()
new_ics.write(calendar.to_ical())
new_ics.seek(0)
new_filename = file.filename.rstrip('.ics')+'_with_reminders'+'.ics'
return send_file(new_ics, as_attachment=True,
attachment_filename=new_filename)
class IcsForm(FlaskForm):
icsfile = FileField('ICS File', validators=[InputRequired()])
hours = IntegerField('Hours', default=0,
widget=NumberInput(step=1, min=0, max=24))
minutes = IntegerField('Minutes', default=0,
widget=NumberInput(step=5, min=0, max=55))
submit = SubmitField('Submit')

153
static/css/style.css Normal file
View file

@ -0,0 +1,153 @@
body {
background-color: #EEEEEE;
font-family: sans-serif;
text-align: center;
margin: 0px;
}
main {
padding: 12px;
}
h1 {
font-size: 1.25em;
}
label {
text-align: left;
}
ol {
text-align: left;
}
a:link,
a:visited {
text-decoration: none;
color: #0066cc;
}
a:hover,
a:active {
text-decoration: underline;
color: #0066cc;
}
div.form {
width: 420px;
margin: 0 auto;
}
div.form-group {
display: flex;
justify-content: space-between;
margin-bottom: 24px;
}
div.form label {
font-size: 18px;
}
div.form input[type="number"] {
width: 80px;
}
footer {
width: 100vw;
position: absolute;
bottom: 0px;
padding: 10px 0px;
}
input[type="submit"] {
width: 150px;
color: white;
background-color: #0066cc;
padding: 14px 20px;
margin: 18px;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: bold;
font-size: 18px;
}
input[type="submit"]:hover {
background-color: #0044aa;
}
.file {
position: relative;
display: inline-block;
cursor: pointer;
height: 2.5rem;
width: 100%;
margin-bottom: 16px;
}
.file input {
min-width: 14rem;
margin: 0;
filter: alpha(opacity=0);
opacity: 0;
}
.file-custom::before {
position: absolute;
top: -.075rem;
right: -.075rem;
bottom: -.075rem;
z-index: 6;
display: block;
content: "Browse";
height: 2.5rem;
padding: .5rem 1rem;
line-height: 1.5;
color: #555;
background-color: #eee;
border: .075rem solid #ddd;
border-radius: 0 .25rem .25rem 0;
}
.file-custom {
position: absolute;
top: 0;
right: 0;
left: 0;
z-index: 5;
height: 2.5rem;
padding: .5rem 1rem;
line-height: 1.5;
color: #555;
background-color: #fff;
border: .075rem solid #ddd;
border-radius: .25rem;
box-shadow: inset 0 .2rem .4rem rgba(0,0,0,.05);
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
*, ::before, ::after {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.file-custom::after {
content: attr(data-content);
}
div.error {
color: #721c24;
background-color: #f8d7da;
padding: .75rem 1.25rem;
border: 1px solid #f5c6cb;
border-radius: .25rem;
}
@media all and (max-width: 500px) {
div.form {
width: 100%;
margin: 0 auto;
}
}

6
static/js/reminder.js Normal file
View file

@ -0,0 +1,6 @@
document.addEventListener("DOMContentLoaded", function(event) {
document.getElementById('icsfile').addEventListener("change", function(e){
var filename = this.value.replace(/^.*[\\\/]/, '');
document.getElementById('filename').setAttribute('data-content', filename);
});
});

21
templates/base.html Normal file
View file

@ -0,0 +1,21 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
<title>Add reminders to ICS files</title>
<link rel="stylesheet" href="/static/css/style.css">
<script type="text/javascript" src="/static/js/reminder.js"></script>
</head>
<body>
<main>
{% block body %}
{% endblock %}
</main>
<footer>
<a href="https://github.com/Finn10111/caesar-chiffre">source code on github</a>
</footer>
</body>
</html>

33
templates/index.html Normal file
View file

@ -0,0 +1,33 @@
{% extends 'base.html' %}
{% block body %}
<h1>Add reminders to ICS files</h1>
<form method="POST" action="/" enctype=multipart/form-data>
<div class="form">
<ol>
<li>Select ICS file</li>
<li>Set reminder time</li>
<li>Submit and download your ICS file with reminders added</li>
</ol>
<div>
<label class="file">
{{ form.icsfile }}
<span id="filename" class="file-custom" data-content="Choose file..."></span>
</label>
</div>
<div class="form-group">
{{ form.hours.label }}
{{ form.hours() }}
</div>
<div class="form-group">
{{ form.minutes.label }}
{{ form.minutes() }}
</div>
<div>
{{ form.submit() }}
</div>
{% if error %}
<div class="error">{{ error }}</div>
{% endif %}
</div>
</form>
{% endblock %}