first version of reminder web application
This commit is contained in:
parent
e43cba4769
commit
0c4fed2a2b
7 changed files with 316 additions and 1 deletions
|
@ -1 +1,2 @@
|
|||
# reminder
|
||||
A simple python Flask application which adds reminders to every event in a given ICS file.
|
||||
|
|
44
apache_sample_config.conf
Normal file
44
apache_sample_config.conf
Normal 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
57
index.py
Normal 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
153
static/css/style.css
Normal 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
6
static/js/reminder.js
Normal 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
21
templates/base.html
Normal 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
33
templates/index.html
Normal 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 %}
|
Loading…
Reference in a new issue