diff options
-rw-r--r-- | requirements.txt | 4 | ||||
-rw-r--r-- | sublab_project/calendarium/admin.py | 20 | ||||
-rw-r--r-- | sublab_project/calendarium/calendarium.py | 77 | ||||
-rw-r--r-- | sublab_project/calendarium/fixtures/testing.json | 20 | ||||
-rw-r--r-- | sublab_project/calendarium/migrations/0001_initial.py | 41 | ||||
-rw-r--r-- | sublab_project/calendarium/migrations/__init__.py | 0 | ||||
-rw-r--r-- | sublab_project/calendarium/models.py | 30 | ||||
-rw-r--r-- | sublab_project/calendarium/tasks.py | 82 | ||||
-rw-r--r-- | sublab_project/calendarium/tests.py | 55 | ||||
-rw-r--r-- | sublab_project/settings.py | 9 |
10 files changed, 251 insertions, 87 deletions
diff --git a/requirements.txt b/requirements.txt index 7d55e8a..05683b0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,7 @@ creole==1.2 PIL==1.1.7 django-auth-ldap==1.0.14 python-ldap==2.4.6 +icalendar==2.2 +python-dateutil==1.5 +django-celery==2.4.2 +redis==2.4.10 diff --git a/sublab_project/calendarium/admin.py b/sublab_project/calendarium/admin.py new file mode 100644 index 0000000..f94a34d --- /dev/null +++ b/sublab_project/calendarium/admin.py @@ -0,0 +1,20 @@ +from django.contrib import admin + +from models import Event + + +class EventAdmin(admin.ModelAdmin): + list_display = ('name', 'start', 'end', 'source') + list_filter = ('source',) + search_fields = ['name', 'description'] + date_hierachy = 'start' + + def save_model(self, request, obj, form, change): + """Sets the source to "admin". + """ + if not obj.id: + obj.source = Event.SOURCE_ADMIN + obj.save() + + +admin.site.register(Event, EventAdmin) diff --git a/sublab_project/calendarium/calendarium.py b/sublab_project/calendarium/calendarium.py deleted file mode 100644 index 9935ace..0000000 --- a/sublab_project/calendarium/calendarium.py +++ /dev/null @@ -1,77 +0,0 @@ -import icalendar -from dateutil.rrule import rrulestr - - -class Event(object): - def __init__(self, name, description, start, end): - self.name = name - self.description = description - self.start = start - self.end = end - - def __repr__(self): - return '<%s.%s (name=%s, start=%s, end=%s)>' % ( - __name__, self.__class__.__name__, - repr(self.name), repr(self.start), repr(self.end)) - - -class Calendarium(object): - def __init__(self, string): - """ - Loads a calendar from the string and provides a nice API to it. - """ - self.calendar = icalendar.Calendar.from_string(string) - - def get_events(self, after, before): - """ - This functions yields a list of events in the calendar. - - Only events after (including) after will be shown. Recurring - events til before (inclusively) will be shown. - """ - - for event in self.calendar.walk('vevent'): - event_fields = [ - # dest , src, default - ('name', 'summary', 'unknown Event'), - ('description', 'description', ''), - ] - - event_info = {} - for fieldinfo in event_fields: - try: - event_info[fieldinfo[0]] = str(event[fieldinfo[1]]) - except KeyError: - event_info[fieldinfo[0]] = fieldinfo[2] - - start = icalendar.vDatetime.from_ical(event['dtstart'].ical()) - end = icalendar.vDatetime.from_ical(event['dtend'].ical()) - - if 'rrule' in event: - rrule = rrulestr(event['rrule'].ical(), dtstart=start) - duration = end - start - - for occurence in rrule.between(after, before, True): - yield Event( - start=occurence, - end=occurence + duration, - **event_info) - else: - if start >= after: - yield Event(start=start, end=end, **event_info) - -if __name__ == '__main__': - # simple example - - from dateutil.relativedelta import relativedelta - from datetime import datetime - import urllib2 - - response = urllib2.urlopen('https://sublab.org:5232/calendars/events') - calendar = Calendarium(response.read()) - now = datetime.now() - events = list(calendar.get_events(now-relativedelta(days=1), now+relativedelta(months=+2))) - events.sort(key=lambda x:x.start) - - for event in events: - print event diff --git a/sublab_project/calendarium/fixtures/testing.json b/sublab_project/calendarium/fixtures/testing.json new file mode 100644 index 0000000..0af16c0 --- /dev/null +++ b/sublab_project/calendarium/fixtures/testing.json @@ -0,0 +1,20 @@ +[ + { + "pk": 1, + "model": "auth.user", + "fields": { + "username": "admin", + "first_name": "", + "last_name": "", + "is_active": true, + "is_superuser": true, + "is_staff": true, + "last_login": "2012-01-04 13:02:11", + "groups": [], + "user_permissions": [], + "password": "sha1$b4c7a$379f348659ad7aaf21b8d04fa891d81c3a607f69", + "email": "admin@example.com", + "date_joined": "2011-12-29 03:18:29" + } + } +] diff --git a/sublab_project/calendarium/migrations/0001_initial.py b/sublab_project/calendarium/migrations/0001_initial.py new file mode 100644 index 0000000..2a1f0a0 --- /dev/null +++ b/sublab_project/calendarium/migrations/0001_initial.py @@ -0,0 +1,41 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + +class Migration(SchemaMigration): + + def forwards(self, orm): + + # Adding model 'Event' + db.create_table('calendarium_event', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('name', self.gf('django.db.models.fields.CharField')(max_length=255)), + ('description', self.gf('django.db.models.fields.TextField')(blank=True)), + ('start', self.gf('django.db.models.fields.DateTimeField')()), + ('end', self.gf('django.db.models.fields.DateTimeField')()), + ('source', self.gf('django.db.models.fields.CharField')(default='import', max_length=10)), + )) + db.send_create_signal('calendarium', ['Event']) + + + def backwards(self, orm): + + # Deleting model 'Event' + db.delete_table('calendarium_event') + + + models = { + 'calendarium.event': { + 'Meta': {'ordering': "['-start']", 'object_name': 'Event'}, + 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'end': ('django.db.models.fields.DateTimeField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'source': ('django.db.models.fields.CharField', [], {'default': "'import'", 'max_length': '10'}), + 'start': ('django.db.models.fields.DateTimeField', [], {}) + } + } + + complete_apps = ['calendarium'] diff --git a/sublab_project/calendarium/migrations/__init__.py b/sublab_project/calendarium/migrations/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/sublab_project/calendarium/migrations/__init__.py diff --git a/sublab_project/calendarium/models.py b/sublab_project/calendarium/models.py index 71a8362..1cd0665 100644 --- a/sublab_project/calendarium/models.py +++ b/sublab_project/calendarium/models.py @@ -1,3 +1,31 @@ from django.db import models -# Create your models here. + +class Event(models.Model): + """A calendar event. + """ + SOURCE_ADMIN = 'admin' + SOURCE_IMPORT = 'import' + name = models.CharField('Name', max_length=255) + description = models.TextField('Beschreibung', blank=True) + start = models.DateTimeField('Beginn') + end = models.DateTimeField('Ende') + source = models.CharField('Quelle', max_length=10, editable=False, + default=SOURCE_IMPORT) + + class Meta: + verbose_name = 'Termin' + verbose_name_plural = 'Termine' + ordering = ['start', 'end', 'name'] + + def __unicode__(self): + return '%s (%s - %s)' % (self.name, + self.start.strftime('%d.%m.%Y %T'), + self.end.strftime('%d.%m.%Y %T')) + + def clean(self): + """Checks if the start date is lower than the end date. + """ + from django.core.exceptions import ValidationError + if self.start > self.end: + raise ValidationError('Der Beginn darf nicht vor dem Ende liegen.') diff --git a/sublab_project/calendarium/tasks.py b/sublab_project/calendarium/tasks.py new file mode 100644 index 0000000..9174105 --- /dev/null +++ b/sublab_project/calendarium/tasks.py @@ -0,0 +1,82 @@ +"""Celery tasks. +""" +from datetime import datetime, timedelta +from dateutil.relativedelta import relativedelta +import urllib2 + +from celery.task import PeriodicTask +from django.conf import settings +from dateutil.rrule import rrulestr +import icalendar + +from models import Event + + +class CalendariumImport(PeriodicTask): + """Periodic task for importing calendar events. + """ + run_every = timedelta(hours=1) + ignore_result = True + + def flush(self): + """Flushes all previously imported events. + """ + Event.objects.filter(source=Event.SOURCE_IMPORT).delete() + self.logger.info('Flushed all previously imported events.') + + def fetch_calendar(self): + """Fetches the calendar events and returns a icalendar.Calendar instance. + """ + response = urllib2.urlopen(settings.CALENDARIUM_IMPORT_URL) + self.logger.info('Fetched calendar from %s' % settings.CALENDARIUM_IMPORT_URL) + return icalendar.Calendar.from_string(response.read()) + + def get_events(self, calendar, after, before): + """Yields all events in calendar as dictionary. + + Only events after (including) after will be shown. Recurring events + til before (inclusively) will be shown. + """ + for event in calendar.walk('vevent'): + event_fields = [ + # dest, src, default + ('name', 'summary', 'unknown Event'), + ('description', 'description', ''), + ] + event_info = {} + for fieldinfo in event_fields: + try: + event_info[fieldinfo[0]] = str(event[fieldinfo[1]]) + except KeyError: + event_info[fieldinfo[0]] = fieldinfo[2] + start = icalendar.vDatetime.from_ical(event['dtstart'].ical()) + end = icalendar.vDatetime.from_ical(event['dtend'].ical()) + if 'rrule' in event: + rrule = rrulestr(event['rrule'].ical(), dtstart=start) + duration = end - start + for occurence in rrule.between(after, before, True): + event_info['start'] = occurence + event_info['end'] = occurence + duration + yield event_info + else: + if start >= after: + event_info['start'] = start + event_info['end'] = end + yield event_info + + def run(self, **kwargs): + """Imports all events. + """ + self.logger = self.get_logger(**kwargs) + now = datetime.now() + after = now - relativedelta(days=1) + before = now + relativedelta(months=+2) + self.logger.info('Importing events from %s to %s' % (after, before)) + try: + calendar = self.fetch_calendar() + self.flush() + for event_info in self.get_events(calendar, after, before): + event = Event.objects.create(**event_info) + self.logger.info('Added event "%s".' % event) + except urllib2.URLError, error: + self.logger.error(error) diff --git a/sublab_project/calendarium/tests.py b/sublab_project/calendarium/tests.py index 501deb7..0ad8c06 100644 --- a/sublab_project/calendarium/tests.py +++ b/sublab_project/calendarium/tests.py @@ -1,16 +1,53 @@ +"""Tests for Event model and EventAdmin. """ -This file demonstrates writing tests using the unittest module. These will pass -when you run "manage.py test". - -Replace this with more appropriate tests for your application. -""" +from datetime import datetime, timedelta +from django.core.exceptions import ValidationError from django.test import TestCase +from models import Event + + +class EventTest(TestCase): + """Event model tests. + """ + def test_source_default(self): + """Tests if the source field default is set to "import". + """ + event = Event(name='Test', start=datetime.now(), end=datetime.now()) + self.assertEqual(event.source, Event.SOURCE_IMPORT) + + def test_start_end(self): + """Tests if an ValidationError is raised if start is lower than end. + """ + start = datetime.now() + end = start - timedelta(1) + event = Event(name='Test', start=start, end=end) + self.assertRaises(ValidationError, event.full_clean) + -class SimpleTest(TestCase): - def test_basic_addition(self): +class EventAdminTest(TestCase): + """EventAdmin tests. + """ + fixtures = ['testing.json'] + + def setUp(self): + """Performs the login with the client. """ - Tests that 1 + 1 always equals 2. + result = self.client.login(username='admin', password='admin') + self.assertTrue(result, 'Login failed.') + + def test_source(self): + """Tests if the source field is set to "admin" when saved with the admin. """ - self.assertEqual(1 + 1, 2) + name = 'Test source admin' + data = { + 'name': name, + 'start_0': '2012-01-01', + 'start_1': '12:00:00', + 'end_0': '2012-01-01', + 'end_1': '13:00:00', + } + self.client.post('/admin/calendarium/event/add/', data=data) + event = Event.objects.get(name=name) + self.assertEqual(event.source, Event.SOURCE_ADMIN) diff --git a/sublab_project/settings.py b/sublab_project/settings.py index d35e5c7..e52008a 100644 --- a/sublab_project/settings.py +++ b/sublab_project/settings.py @@ -133,6 +133,8 @@ INSTALLED_APPS = ( 'gunicorn', 'news', 'projects', + 'djcelery', + 'calendarium', ) # A sample logging configuration. The only tangible logging @@ -189,6 +191,13 @@ AUTHENTICATION_BACKENDS = ( 'django.contrib.auth.backends.ModelBackend', ) +import djcelery +djcelery.setup_loader() + +BROKER_URL = 'redis://localhost:6379/0' + +CALENDARIUM_IMPORT_URL = 'https://sublab.org:5232/calendars/events' + try: from local_settings import * except ImportError: |