Compare commits

..

10 Commits

Author SHA1 Message Date
c3dcccb479 reloads page after successful form transmition in routes 2020-07-25 23:44:49 +02:00
5ec5ba488d adds remove button to all calendars 2020-07-25 19:45:37 +02:00
9ecaf0211f timezone fix to always use calendar timezone for event description 2020-07-25 19:28:30 +02:00
1f48689a32 Merge branch 'master' of git.maenle.net:raphael/calendarwatch_frontend 2020-07-25 18:10:04 +02:00
752c7c5577 adds ical input to calendar html templates and implements data handling
- the icalHandler in the backend is used in two instances. First, when
  the user adds a new ical calendar into the database, routes.py passes
  the information (name and url) to the ical Handler. The calendars are
  saved in the database as type 'ical'.

  Then, when a connected device pulls current events, all the calendars
  which are of type 'ical' are pulled and parsed for current events.
  See the backend commit for details on what happens there.

- All google Calendar related functions now have an additional check,
  to make sure that the calendar they are working on is of type 'Google'.
2020-07-25 18:05:55 +02:00
04c5410c41 migration scripts to newest database 2020-07-25 11:41:12 +02:00
524d2f1e1d adds calendar_type to calendar model and to calendar handling 2020-07-25 11:28:59 +02:00
2add28fa00 adds ical form and form visualization in calendar.html 2020-07-25 10:50:02 +02:00
38e16f92e8 device receives a timestamp at creation
no new devices are ever without a lastConnection timestamp
2020-07-17 10:42:27 +02:00
0284eb2fa8 adds cleanup functionality for old devices
- device now last field 'last connection' which gets updated
  at every server connection from the respective device
- manual cleanup script in database deletes all devices which
  have never been updated, or have a timestamp older than 30 days
2020-07-17 10:39:16 +02:00
9 changed files with 154 additions and 43 deletions

Submodule backend updated: 45cd71cc4b...f939127a0c

14
database/__init__.py Normal file
View File

@ -0,0 +1,14 @@
from server import db
import time
from database.models import Device
def cleanDevices():
allDevs = db.session.query(Device)
devices = allDevs.filter(Device.lastConnection <= int(round(time.time())) - 60*60*24*30).all()
devices += allDevs.filter(Device.lastConnection == None)
for device in devices:
print(device.deviceName)
db.session.delete(device)
db.session.commit()

View File

@ -0,0 +1,30 @@
"""empty message
Revision ID: 9882522aafa9
Revises: e5ef5e4a807b
Create Date: 2020-07-25 09:34:07.987380
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '9882522aafa9'
down_revision = 'e5ef5e4a807b'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('calendar', sa.Column('calendar_type', sa.String(length=32), nullable=True))
op.create_index(op.f('ix_calendar_calendar_type'), 'calendar', ['calendar_type'], unique=False)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f('ix_calendar_calendar_type'), table_name='calendar')
op.drop_column('calendar', 'calendar_type')
# ### end Alembic commands ###

View File

@ -61,11 +61,13 @@ class Device(db.Model):
user_id = db.Column(db.Integer, db.ForeignKey('user.id')) user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
deviceName = db.Column(db.String(64), unique=True) deviceName = db.Column(db.String(64), unique=True)
connection = db.Column(db.Boolean) connection = db.Column(db.Boolean)
lastConnection = db.Column(db.BigInteger)
class Calendar(db.Model): class Calendar(db.Model):
id = db.Column(db.Integer, primary_key=True, autoincrement=True) id = db.Column(db.Integer, primary_key=True, autoincrement=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), index=True, nullable=False) user_id = db.Column(db.Integer, db.ForeignKey('user.id'), index=True, nullable=False)
calendar_id = db.Column(db.String(256), primary_key=True) calendar_id = db.Column(db.String(256), primary_key=True)
calendar_type = db.Column(db.String(32), index=True)
name = db.Column(db.String(256), index=True) name = db.Column(db.String(256), index=True)
toggle = db.Column(db.String(8)) toggle = db.Column(db.String(8))
color = db.Column(db.String(16)) color = db.Column(db.String(16))

View File

@ -2,9 +2,10 @@ FROM python:3.8-slim-buster
RUN apt-get update && apt-get upgrade RUN apt-get update && apt-get upgrade
RUN pip3 install flask Flask-SQLAlchemy flask_migrate flask_login flask_wtf python-dotenv RUN pip3 install flask Flask-SQLAlchemy flask_migrate flask_login flask_wtf python-dotenv
RUN apt-get install gcc libpcre3 libpcre3-dev libmariadbclient-dev -y RUN apt-get install gcc libpcre3 libpcre3-dev libmariadbclient-dev -y
RUN pip3 install uwsgi email-validator RandomWords RUN pip3 install uwsgi email-validator RandomWords ics
RUN pip3 install google google-oauth google-auth-oauthlib google-api-python-client mysqlclient RUN pip3 install google google-oauth google-auth-oauthlib google-api-python-client mysqlclient
COPY docker-entrypoint.sh /usr/local/bin/ COPY docker-entrypoint.sh /usr/local/bin/
EXPOSE 8084 EXPOSE 8084
EXPOSE 3001 EXPOSE 3001
ENTRYPOINT ["docker-entrypoint.sh"] ENTRYPOINT ["docker-entrypoint.sh"]
# CMD tail -f /dev/null

View File

@ -46,3 +46,13 @@ class DeviceForm(FlaskForm):
device = Device.query.filter_by(deviceName=deviceName.data).first() device = Device.query.filter_by(deviceName=deviceName.data).first()
if device is None: if device is None:
raise ValidationError('Device not Found') raise ValidationError('Device not Found')
class CalendarForm(FlaskForm):
iCalURL = StringField('New ical URL', validators=[DataRequired()])
calName = StringField('Calendar Name', validators=[DataRequired()])
submit = SubmitField('Add URL')
def validate_iCalURL (self, iCalURL):
return None

View File

@ -122,18 +122,22 @@ def deleteAccount(user):
def fetchCalendarEvents(user, calendars, startDate, endDate): def fetchCalendarEvents(user, calendars, startDate, endDate):
client_token = GC.build_credentials(user.google_token.token, service = None
user.google_token.refresh_token) if user.google_token is not None:
credentials = google.oauth2.credentials.Credentials(**client_token) client_token = GC.build_credentials(user.google_token.token,
user.google_token.refresh_token)
credentials = google.oauth2.credentials.Credentials(**client_token)
service = build(GC.API_SERVICE_NAME, GC.API_VERSION, credentials=credentials) service = build(GC.API_SERVICE_NAME, GC.API_VERSION, credentials=credentials)
all_events = [] all_events = []
for calendar in calendars: for calendar in calendars:
if calendar.toggle == "True": if (calendar.toggle == "True" and
calendar.calendar_type == "Google" and
service != None):
event_result = service.events().list(calendarId=calendar.calendar_id, event_result = service.events().list(calendarId=calendar.calendar_id,
timeMin=startDate, timeMin=startDate.isoformat(),
timeMax=endDate, timeMax=endDate.isoformat(),
maxResults=10, maxResults=10,
singleEvents=True, singleEvents=True,
orderBy='startTime').execute() orderBy='startTime').execute()
@ -156,7 +160,10 @@ def fetchCalendarEvents(user, calendars, startDate, endDate):
all_events.append(newEvent) all_events.append(newEvent)
colors = service.colors().get().execute() if service != None:
colors = service.colors().get().execute()
else:
colors = None
return all_events, colors return all_events, colors
@ -178,6 +185,7 @@ def fetchCalendars():
calendar_list = service.calendarList().list(pageToken=page_token).execute() calendar_list = service.calendarList().list(pageToken=page_token).execute()
for calendar in calendar_list['items']: for calendar in calendar_list['items']:
calendars.append(Calendar(name=calendar['summary'], calendars.append(Calendar(name=calendar['summary'],
calType="Google",
calendarId=calendar['id'], calendarId=calendar['id'],
color=calendar['colorId'])) color=calendar['colorId']))
page_token = calendar_list.get('nextPageToken') page_token = calendar_list.get('nextPageToken')

View File

@ -1,6 +1,7 @@
# Python standard libraries # Python standard libraries
import json import json
import os import os
import time
# Third-party libraries # Third-party libraries
import flask import flask
@ -16,10 +17,11 @@ from flask_login import (
from random_words import RandomWords from random_words import RandomWords
import requests import requests
import backend.icalHandler as ical
import server.googleHandler as google import server.googleHandler as google
from server import login_manager, app, db from server import login_manager, app, db
from server.forms import LoginForm, RegistrationForm, DeviceForm from server.forms import LoginForm, RegistrationForm, DeviceForm, CalendarForm
import backend import backend
from database.models import User, Calendar, Device, GoogleToken from database.models import User, Calendar, Device, GoogleToken
@ -93,11 +95,58 @@ def devices():
form=form) form=form)
@app.route("/calendar") @app.route("/calendar", methods=['GET', 'POST', 'DELETE'])
@login_required
def calendar(): def calendar():
if not current_user.is_authenticated:
return flask.render_template('login.html')
calendars = backend.calendarsFromDb(current_user) calendars = backend.calendarsFromDb(current_user)
return flask.render_template('calendar.html', calendars=calendars)
form = CalendarForm()
if request.method == 'POST':
if request.form.get("submit") == "Remove":
calendar = db.session.query(Calendar).filter(Calendar.calendar_id==request.form.get("calendar")).first()
db.session.delete(calendar)
db.session.commit()
return flask.redirect(url_for('calendar'))
elif form.validate_on_submit():
ical.icalToCalendarDb(form.iCalURL.data, form.calName.data, current_user)
return flask.redirect(url_for('calendar'))
# otherwise it is a javascript POST
else:
try:
calId = request.json.get('calendar_id')
color = request.json.get('color', None)
toggle = request.json.get('toggle', None)
except:
return flask.render_template('calendar.html', calendars=calendars, form=form)
if color != None:
current_user.updateCalendar(calId, color=color)
if toggle != None:
current_user.updateCalendar(calId, toggle=toggle)
# toggle specific calendar of user
try:
calId = request.json.get('calendar_id')
color = request.json.get('color', None)
toggle = request.json.get('toggle', None)
except:
return flask.render_template('calendar.html', calendars=calendars, form=form)
if color != None:
current_user.updateCalendar(calId, color=color)
if toggle != None:
current_user.updateCalendar(calId, toggle=toggle)
# toggle specific calendar of user
return flask.render_template('calendar.html', calendars=calendars, form=form)
# POST
@app.route('/login/email', methods=['GET', 'POST']) @app.route('/login/email', methods=['GET', 'POST'])
def emaillogin(): def emaillogin():
@ -204,6 +253,7 @@ def downloader(device):
if request_device.user_id == None: if request_device.user_id == None:
return jsonify(kind="unregistered") return jsonify(kind="unregistered")
request_device.lastConnection=int(round(time.time()))
request_device.connection=True request_device.connection=True
db.session.commit() db.session.commit()
request_user = db.session.query(User).filter(User.id==request_device.user_id).first() request_user = db.session.query(User).filter(User.id==request_device.user_id).first()
@ -213,6 +263,7 @@ def downloader(device):
# TODO only pass along google calendars form user # TODO only pass along google calendars form user
startDate, endDate = backend.getTimeStamps() startDate, endDate = backend.getTimeStamps()
events, colors = google.fetchCalendarEvents(request_user, request_user.calendars, startDate, endDate) events, colors = google.fetchCalendarEvents(request_user, request_user.calendars, startDate, endDate)
events.extend(ical.fetchCalendarEvents(request_user.calendars, startDate, endDate))
calendarjson = backend.generateJsonFromCalendarEntries(events, colors) calendarjson = backend.generateJsonFromCalendarEntries(events, colors)
return jsonify(calendarjson) return jsonify(calendarjson)
@ -233,6 +284,7 @@ def generateDeviceFingerprint():
if not db.session.query(Device).filter(Device.deviceName==fingerprint).first(): if not db.session.query(Device).filter(Device.deviceName==fingerprint).first():
# Save as new Device # Save as new Device
device = Device(deviceName=fingerprint, connection=False) device = Device(deviceName=fingerprint, connection=False)
device.lastConnection = int(round(time.time()))
db.session.add(device) db.session.add(device)
db.session.commit() db.session.commit()
break; break;
@ -240,27 +292,3 @@ def generateDeviceFingerprint():
# Send to Device # Send to Device
return jsonify(deviceName=fingerprint) return jsonify(deviceName=fingerprint)
# POST
@app.route('/calendar', methods = ['POST', 'DELETE'])
@login_required
def user():
if request.method == 'POST':
calId = request.json.get('calendar_id')
color = request.json.get('color', None)
toggle = request.json.get('toggle', None)
if color != None:
current_user.updateCalendar(calId, color=color)
if toggle != None:
current_user.updateCalendar(calId, toggle=toggle)
# toggle specific calendar of user
elif request.method == 'DELETE':
# do nothing
return 'NONE'
else:
# POST Error 405
print("405")
return 'OK'

View File

@ -9,6 +9,13 @@
{% for item in calendars %} {% for item in calendars %}
<div class="container"> <div class="container">
<!--action button-->
<div style="width: 4rem; margin: 1rem;">
<form action="" method="post">
<input type="hidden" name="calendar" value={{ item.calendarId }}>
<input type="submit" name="submit" value="Remove">
</form>
</div>
<!--Name--> <!--Name-->
<div style="width: 15rem; margin: 1rem;">{{ item.name }}</div> <div style="width: 15rem; margin: 1rem;">{{ item.name }}</div>
@ -32,12 +39,23 @@
</div> </div>
{% endfor %} {% endfor %}
<div id=calendars class="container"> <div id=calendars class="container">
<a class="button" href="login/google">Google Calendar</a>
<a class="button" href="#" >Nextcloud Calendar</a>
</div> </div>
<div class="container"> <form action="" method="post">
<div class="container grey" style="margin-top: 3rem;">
<div>{{ form.hidden_tag() }}</div>
<div style="margin: 1rem">{{ form.calName.label }}</div>
<div style="margin: 1rem">{{ form.calName(size=24) }}</div>
<div style="margin: 1rem">{{ form.iCalURL.label }}</div>
<div style="margin: 1rem">{{ form.iCalURL(size=24) }}</div>
<div style="with: 8rem; margin: 1rem">{{ form.submit() }}</div>
{% for error in form.iCalURL.errors %}
<span style="color: red;">[{{ error }}]</span>
{% endfor %}
</div>
</form>
<!--div class="container">
<a class="button addcalendar" href="/login/google" style="width: auto; margin: 4rem">Add Calendar</a> <a class="button addcalendar" href="/login/google" style="width: auto; margin: 4rem">Add Calendar</a>
</div> </div-->
<script type="text/javascript"> <script type="text/javascript">