diff --git a/README.md b/README.md index 29040ee..31b4055 100644 --- a/README.md +++ b/README.md @@ -1 +1,2 @@ -# reminder \ No newline at end of file +# reminder +A simple python Flask application which adds reminders to every event in a given ICS file. diff --git a/apache_sample_config.conf b/apache_sample_config.conf new file mode 100644 index 0000000..9a275dc --- /dev/null +++ b/apache_sample_config.conf @@ -0,0 +1,44 @@ + + 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] + + + + 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/ + + + WSGIProcessGroup reminder + WSGIApplicationGroup %{GLOBAL} + Order deny,allow + Allow from all + + + 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 + + diff --git a/index.py b/index.py new file mode 100644 index 0000000..b1d2883 --- /dev/null +++ b/index.py @@ -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') diff --git a/static/css/style.css b/static/css/style.css new file mode 100644 index 0000000..6a13954 --- /dev/null +++ b/static/css/style.css @@ -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; + } +} diff --git a/static/js/reminder.js b/static/js/reminder.js new file mode 100644 index 0000000..9034e19 --- /dev/null +++ b/static/js/reminder.js @@ -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); + }); +}); diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..037e7d2 --- /dev/null +++ b/templates/base.html @@ -0,0 +1,21 @@ + + + + + + Add reminders to ICS files + + + + + +
+{% block body %} +{% endblock %} +
+ + + + diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..722c408 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,33 @@ +{% extends 'base.html' %} +{% block body %} +

Add reminders to ICS files

+
+
+
    +
  1. Select ICS file
  2. +
  3. Set reminder time
  4. +
  5. Submit and download your ICS file with reminders added
  6. +
+
+ +
+
+ {{ form.hours.label }} + {{ form.hours() }} +
+
+ {{ form.minutes.label }} + {{ form.minutes() }} +
+
+ {{ form.submit() }} +
+ {% if error %} +
{{ error }}
+ {% endif %} +
+
+{% endblock %}