summaryrefslogtreecommitdiff
path: root/app/backend
diff options
context:
space:
mode:
authoremkael <emkael@tlen.pl>2016-06-07 15:16:59 +0200
committeremkael <emkael@tlen.pl>2016-06-10 11:46:41 +0200
commitdf401552aac363655ab8f056a6c910a7611954d6 (patch)
treeccd02b63b8f915f02959b7890d71507e44679917 /app/backend
parentfaa340586f78beacff2e32df65d2f37c08f0df2b (diff)
* renaming python directory
Diffstat (limited to 'app/backend')
-rw-r--r--app/backend/.gitignore1
l---------app/backend/config1
-rw-r--r--app/backend/fetch_cals.py91
-rw-r--r--app/backend/generate_cal_urls.py17
-rw-r--r--app/backend/import_cals.py132
-rw-r--r--app/backend/init_db.py16
-rw-r--r--app/backend/rcal/__init__.py0
-rw-r--r--app/backend/rcal/db.py34
-rw-r--r--app/backend/rcal/model.py153
-rw-r--r--app/backend/weekdays.py32
10 files changed, 477 insertions, 0 deletions
diff --git a/app/backend/.gitignore b/app/backend/.gitignore
new file mode 100644
index 0000000..df04015
--- /dev/null
+++ b/app/backend/.gitignore
@@ -0,0 +1 @@
+rcal/*.pyc
diff --git a/app/backend/config b/app/backend/config
new file mode 120000
index 0000000..899f698
--- /dev/null
+++ b/app/backend/config
@@ -0,0 +1 @@
+../../config \ No newline at end of file
diff --git a/app/backend/fetch_cals.py b/app/backend/fetch_cals.py
new file mode 100644
index 0000000..8b53254
--- /dev/null
+++ b/app/backend/fetch_cals.py
@@ -0,0 +1,91 @@
+import datetime
+
+import dateutil.parser as dateparser
+import ics
+import pytz
+import requests
+from rcal.db import Session
+from rcal.model import Calendar, Entry
+
+
+def update_event_data(db_event, ical_event):
+ db_event.name = ical_event.name
+ db_event.location = ical_event.location
+ db_event.begin_date = ical_event.begin.datetime
+ db_event.end_date = ical_event.end.datetime
+ db_event.all_day = (
+ (db_event.end_date - db_event.begin_date).seconds % 86400 == 0
+ ) and (
+ db_event.begin_date.time() == datetime.time.min)
+ db_event.last_modified = get_last_modification_time(ical_event)
+ return db_event
+
+
+def update_event(db_event, ical_event):
+ update_event_data(db_event, ical_event)
+
+
+def add_event(event, calendar, session):
+ entry = Entry()
+ entry.uid = event.uid
+ entry.calendar = calendar
+ entry = update_event_data(entry, event)
+ session.add(entry)
+
+
+def remove_event(event, session):
+ session.delete(event)
+
+
+def get_last_modification_time(event):
+ for unused in event.__dict__['_unused']:
+ if unused.name == 'LAST-MODIFIED':
+ return dateparser.parse(unused.value)
+ return None
+
+
+def fetch_calendar(calendar, session):
+ cal_data = requests.get(calendar.url)
+ cal_object = ics.Calendar(cal_data.content.decode(cal_data.encoding))
+ cal_events = {e.uid: e for e in cal_object.events}
+ db_events = {e.uid: e for e in calendar.entries}
+ new_events = [e for u, e in cal_events.iteritems()
+ if u not in db_events.keys()]
+ old_events = [e for u, e in db_events.iteritems()
+ if u not in cal_events.keys()]
+ mod_events = [{'ics': cal_events[u], 'db': e}
+ for u, e in db_events.iteritems() if u in cal_events.keys()]
+ changes_present = False
+ for event in mod_events:
+ modified_date = get_last_modification_time(event['ics'])
+ if not modified_date or \
+ not event['db'].last_modified or \
+ modified_date > event['db'].last_modified.replace(tzinfo=pytz.UTC):
+ print 'Updating event %s' % event['db'].uid
+ update_event(event['db'], event['ics'])
+ changes_present = True
+ for event in new_events:
+ print 'Adding event %s' % event.uid
+ add_event(event, calendar, session)
+ changes_present = True
+ for event in old_events:
+ print 'Removing event %s' % event.uid
+ remove_event(event, session)
+ changes_present = True
+ if changes_present:
+ calendar.last_updated = datetime.datetime.now()
+
+
+def main():
+ session = Session.create()
+
+ calendars = session.query(Calendar).all()
+ for calendar in calendars:
+ # print 'Fetching %s' % calendar.url
+ fetch_calendar(calendar, session)
+
+ session.commit()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/app/backend/generate_cal_urls.py b/app/backend/generate_cal_urls.py
new file mode 100644
index 0000000..a79f9d0
--- /dev/null
+++ b/app/backend/generate_cal_urls.py
@@ -0,0 +1,17 @@
+from slugify import slugify
+
+from rcal.db import Session
+from rcal.model import Calendar
+
+
+def main():
+ session = Session.create()
+ for calendar in session.query(Calendar).all():
+ calendar.custom_url = slugify(
+ calendar.custom_name
+ if calendar.custom_name
+ else calendar.name)
+ session.commit()
+
+if __name__ == '__main__':
+ main()
diff --git a/app/backend/import_cals.py b/app/backend/import_cals.py
new file mode 100644
index 0000000..f0b5262
--- /dev/null
+++ b/app/backend/import_cals.py
@@ -0,0 +1,132 @@
+import json
+import os
+import re
+import time
+import urllib2
+import urlparse
+
+from sqlalchemy import inspect
+
+from rcal.db import Session
+from rcal.model import Calendar, Category
+
+BASEPATH = os.path.join(
+ os.environ['PYTHONPATH'],
+ '..',
+ '..')
+
+CONFIG = json.load(open(
+ os.path.join(
+ os.environ['PYTHONPATH'],
+ 'config',
+ 'reddit-import.json'),
+ 'r'))
+
+
+def get_cal_list():
+ cache_path = os.path.join(
+ BASEPATH,
+ CONFIG['cache'])
+ if not os.path.exists(cache_path) or \
+ int(time.time()) - int(os.path.getmtime(cache_path)) > \
+ CONFIG['cache_time']:
+ opener = urllib2.build_opener()
+ opener.addheaders = [('User-Agent', CONFIG['user_agent'])]
+ cal_list = json.loads(opener.open(CONFIG['reddit_url']).read())
+ cal_list = cal_list['data']['content_md']
+ with open(cache_path, 'w') as cache_file:
+ cache_file.write(cal_list)
+ cache_file.close()
+ else:
+ cal_list = open(cache_path, 'r').read()
+ return cal_list
+
+
+def update_calendar(cal, session):
+ db_cal = Calendar.fetch(cal['uid'], session)
+
+ if inspect(db_cal).pending:
+ print 'Adding calendar %s (%s)' % (cal['name'], cal['uid'])
+ db_cal.name = cal['name']
+ db_cal.url = cal['url']
+ db_cal.website = cal['website']
+ db_cal.category = Category.fetch(cal['category'], session)
+
+ if db_cal.name != cal['name']:
+ print 'Updating calendar name: %s -> %s (%s)' % (
+ db_cal.name, cal['name'], db_cal.uid)
+ db_cal.name = cal['name']
+ if db_cal.url != cal['url']:
+ print 'Updating calendar url: %s -> %s (%s)' % (
+ db_cal.url, cal['url'], db_cal.uid)
+ db_cal.url = cal['url']
+ if db_cal.website != cal['website']:
+ print 'Updating calendar website: %s -> %s (%s)' % (
+ db_cal.website, cal['website'], db_cal.uid)
+ db_cal.website = cal['website']
+
+ if 'category_mapping' in CONFIG and \
+ cal['category'] in CONFIG['category_mapping']:
+ cal['category'] = CONFIG['category_mapping'][cal['category']]
+
+ # informational only
+ if db_cal.category.name != cal['category']:
+ print 'Calendar category changed: %s -> %s (%s, %s)' % (
+ db_cal.category.name, cal['category'], db_cal.name, db_cal.uid)
+
+
+def get_imported_calendars(cells, ical_markdown):
+ imported_calendars = []
+ for row in cells:
+ row = [r for r in row if r.strip()]
+ if len(row) == 5:
+ markdown_match = re.match(ical_markdown, row[2])
+ if markdown_match:
+ ical_url = urlparse.urlparse(markdown_match.group(1))
+ if ical_url.netloc == 'calendar.google.com':
+ ical_path = re.sub(
+ '^/?calendar/ical/', '', ical_url.path).split('/')
+ if len(ical_path) == 3:
+ imported_calendars.append({
+ 'uid': ical_path[0],
+ 'url': ical_url.geturl(),
+ 'name': row[0],
+ 'website': row[4].split()[0],
+ 'category': row[1]
+ })
+ else:
+ print 'Unknown iCal URL format: %s' % (
+ ical_url.geturl())
+ else:
+ print 'Unknown iCal URL format: %s' % (
+ ical_url.geturl())
+ return imported_calendars
+
+
+def main():
+ session = Session.create()
+
+ cal_list = get_cal_list()
+
+ ical_markdown = re.compile(r'^\[iCal\]\((.*)\)$')
+ cells = [row.split('|') for row in cal_list.split('\n')]
+
+ imported_calendars = get_imported_calendars(cells, ical_markdown)
+ imported_calendar_uids = [c['uid'] for c in imported_calendars]
+
+ db_only_calendars = session.query(Calendar).filter(
+ ~Calendar.uid.in_(imported_calendar_uids)).all()
+
+ if len(db_only_calendars):
+ print 'Local calendars not in remote source:'
+ for cal in db_only_calendars:
+ print '%s (%s)' % (cal.name, cal.uid)
+ print
+
+ for cal in imported_calendars:
+ update_calendar(cal, session)
+
+ session.commit()
+
+if __name__ == '__main__':
+ main()
diff --git a/app/backend/init_db.py b/app/backend/init_db.py
new file mode 100644
index 0000000..7d48e80
--- /dev/null
+++ b/app/backend/init_db.py
@@ -0,0 +1,16 @@
+import sys
+
+from rcal.db import Session
+from rcal.model import BASE
+
+
+def main():
+ session = Session.create()
+
+ if len(sys.argv) > 1 and sys.argv[1] == 'force':
+ BASE.metadata.drop_all(session.get_bind())
+
+ BASE.metadata.create_all(session.get_bind())
+
+if __name__ == '__main__':
+ main()
diff --git a/app/backend/rcal/__init__.py b/app/backend/rcal/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/app/backend/rcal/__init__.py
diff --git a/app/backend/rcal/db.py b/app/backend/rcal/db.py
new file mode 100644
index 0000000..7e892a1
--- /dev/null
+++ b/app/backend/rcal/db.py
@@ -0,0 +1,34 @@
+import json
+from os import path
+
+import sqlalchemy.engine.url as url
+from sqlalchemy import create_engine
+from sqlalchemy.orm import sessionmaker
+
+
+class Session(object):
+
+ engine = None
+
+ def __init__(self):
+ config = json.load(
+ open(path.join(path.dirname(
+ path.realpath(__file__)), '..', 'config', 'db.json')))
+ db_str = url.URL(
+ drivername=config['type'],
+ host=config['host'],
+ username=config['user'],
+ password=config['pass'],
+ database=config['name'],
+ query={'charset': config['cset']}
+ )
+ self.engine = create_engine(db_str, encoding=config['cset'])
+
+ def get_maker(self):
+ return sessionmaker(bind=self.engine)
+
+ @staticmethod
+ def create():
+ session = Session()
+ maker = session.get_maker()
+ return maker()
diff --git a/app/backend/rcal/model.py b/app/backend/rcal/model.py
new file mode 100644
index 0000000..512c75e
--- /dev/null
+++ b/app/backend/rcal/model.py
@@ -0,0 +1,153 @@
+# pylint: disable=too-few-public-methods, invalid-name
+from sqlalchemy import Column, ForeignKey, Table
+from sqlalchemy.ext.declarative import declarative_base
+from sqlalchemy.orm import relationship
+from sqlalchemy.types import (TIMESTAMP, Boolean, DateTime, Integer, String,
+ TypeDecorator)
+
+from dateutil.tz import tzutc
+
+
+# pylint: disable=abstract-method, unused-argument
+class UTCDateTime(TypeDecorator):
+ impl = DateTime
+
+ def process_bind_param(self, value, engine):
+ if value is not None:
+ return value.astimezone(tzutc())
+
+ def process_result_value(self, value, engine):
+ if value is not None:
+ return value.replace(tzinfo=tzutc())
+
+
+BASE = declarative_base()
+
+
+class Calendar(BASE):
+ __tablename__ = 'calendars'
+
+ uid = Column(String(255), primary_key=True)
+ url = Column(String(255))
+ name = Column(String(255), index=True)
+ website = Column(String(255))
+ visible = Column(Boolean, index=True)
+ custom_name = Column(String(255))
+ custom_image = Column(String(255))
+ custom_url = Column(String(255), unique=True, nullable=False, index=True)
+ last_updated = Column(TIMESTAMP)
+
+ _category = Column(
+ Integer,
+ ForeignKey(
+ 'categories.id',
+ onupdate='CASCADE',
+ ondelete='SET NULL'))
+ category = relationship(
+ 'Category',
+ back_populates='calendars',
+ order_by='Calendar.name')
+
+ entries = relationship(
+ 'Entry',
+ back_populates='calendar',
+ cascade="all",
+ passive_deletes=True,
+ order_by='Entry.begin_date')
+
+ @staticmethod
+ def fetch(uid, session, name=None, url=None):
+ calendar = session.query(Calendar).filter(Calendar.uid == uid).first()
+ if not calendar:
+ calendar = Calendar()
+ calendar.uid = uid
+ session.add(calendar)
+ if name:
+ calendar.name = name
+ if url:
+ calendar.url = url
+ return calendar
+
+
+class Entry(BASE):
+ __tablename__ = 'entries'
+
+ id = Column(Integer, primary_key=True)
+ uid = Column(String(255), index=True, unique=True, nullable=False)
+ begin_date = Column(UTCDateTime, index=True)
+ end_date = Column(UTCDateTime)
+ all_day = Column(Boolean)
+ name = Column(String(255))
+ location = Column(String(255))
+ last_modified = Column(UTCDateTime)
+
+ _calendar = Column(
+ String(255),
+ ForeignKey(
+ 'calendars.uid',
+ onupdate='CASCADE',
+ ondelete='CASCADE'))
+ calendar = relationship(
+ 'Calendar',
+ back_populates='entries',
+ order_by='Entry.begin_date')
+
+ @staticmethod
+ def fetch(uid, session):
+ entry = session.query(Entry).filter(Entry.uid == uid).first()
+ if not entry:
+ entry = Entry()
+ session.add(entry)
+ return entry
+
+
+class Category(BASE):
+ __tablename__ = 'categories'
+
+ id = Column(Integer, primary_key=True)
+ name = Column(String(255), index=True)
+ priority = Column(Integer, index=True)
+
+ calendars = relationship(
+ 'Calendar',
+ back_populates='category',
+ cascade="all",
+ passive_deletes=True,
+ order_by='Calendar.name')
+
+ @staticmethod
+ def fetch(name, session):
+ category = session.query(Category).filter(
+ Category.name == name).first()
+ if not category:
+ category = Category()
+ category.name = name
+ session.add(category)
+ return category
+
+
+user_selections = Table(
+ 'user_selections', BASE.metadata,
+ Column('_user', Integer,
+ ForeignKey('users.id', onupdate='CASCADE', ondelete='CASCADE'),
+ primary_key=True),
+ Column('_calendar', String(255),
+ ForeignKey('calendars.uid', onupdate='CASCADE', ondelete='CASCADE'),
+ primary_key=True)
+)
+
+
+class User(BASE):
+ __tablename__ = 'users'
+
+ id = Column(Integer, primary_key=True)
+ login = Column(String(255), unique=True, index=True)
+ password = Column(String(255))
+ is_admin = Column(Boolean)
+ timezone = Column(String(255))
+ last_login = Column(UTCDateTime)
+
+ calendars = relationship('Calendar',
+ secondary=user_selections)
+
+__all__ = ('Calendar', 'Entry', 'Category', 'User')
diff --git a/app/backend/weekdays.py b/app/backend/weekdays.py
new file mode 100644
index 0000000..547f6b2
--- /dev/null
+++ b/app/backend/weekdays.py
@@ -0,0 +1,32 @@
+import json
+import os
+import urllib
+
+from lxml import etree
+
+
+def main():
+ week_day_config = {}
+
+ supplemental_data = etree.fromstring(
+ urllib.urlopen(
+ 'http://unicode.org/repos/cldr/trunk/common/supplemental/' +
+ 'supplementalData.xml'
+ ).read())
+ for first_day in supplemental_data.xpath('weekData/firstDay[not(@alt)]'):
+ day = first_day.get('day')
+ territories = first_day.get('territories').split()
+ for territory in territories:
+ week_day_config[territory] = day
+
+ json.dump(week_day_config,
+ file(os.path.join(
+ os.environ['PYTHONPATH'],
+ 'config',
+ 'weekdays.json'), 'w'),
+ sort_keys=True,
+ indent=4,
+ separators=(',', ': '))
+
+if __name__ == '__main__':
+ main()