diff --git a/chrono/agendas/management/__init__.py b/chrono/agendas/management/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/chrono/agendas/management/commands/__init__.py b/chrono/agendas/management/commands/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/chrono/agendas/management/commands/sync_desks_timeperiod_exceptions.py b/chrono/agendas/management/commands/sync_desks_timeperiod_exceptions.py
new file mode 100644
index 00000000..0e9950a1
--- /dev/null
+++ b/chrono/agendas/management/commands/sync_desks_timeperiod_exceptions.py
@@ -0,0 +1,32 @@
+# chrono - agendas system
+# Copyright (C) 2016-2017 Entr'ouvert
+#
+# This program is free software: you can redistribute it and/or modify it
+# under the terms of the GNU Affero General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+import six
+import sys
+
+from chrono.agendas.models import Desk, ICSError
+from django.core.management.base import BaseCommand, CommandError
+
+
+class Command(BaseCommand):
+ help = 'Synchronize time period exceptions from desks remote ics'
+
+ def handle(self, **options):
+ for desk in Desk.objects.exclude(timeperiod_exceptions_remote_url=''):
+ try:
+ desk.create_timeperiod_exceptions_from_remote_ics(desk.timeperiod_exceptions_remote_url)
+ except ICSError as e:
+ print >> sys.stderr, u'unable to create timeperiod exceptions for "%s": %s' % (desk, e)
diff --git a/chrono/agendas/migrations/0020_auto_20171102_1021.py b/chrono/agendas/migrations/0020_auto_20171102_1021.py
new file mode 100644
index 00000000..40bb940f
--- /dev/null
+++ b/chrono/agendas/migrations/0020_auto_20171102_1021.py
@@ -0,0 +1,32 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import datetime
+from django.utils.timezone import utc
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('agendas', '0019_timeperiodexception'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='desk',
+ name='timeperiod_exceptions_remote_url',
+ field=models.URLField(verbose_name='URL to fetch time period exceptions from', blank=True),
+ ),
+ migrations.AddField(
+ model_name='timeperiodexception',
+ name='external_id',
+ field=models.CharField(max_length=256, verbose_name='External ID', blank=True),
+ ),
+ migrations.AddField(
+ model_name='timeperiodexception',
+ name='update_datetime',
+ field=models.DateTimeField(default=datetime.datetime(2017, 11, 2, 10, 21, 1, 826837, tzinfo=utc), auto_now=True),
+ preserve_default=False,
+ ),
+ ]
diff --git a/chrono/agendas/models.py b/chrono/agendas/models.py
index 3a1c1a0f..cb545e7f 100644
--- a/chrono/agendas/models.py
+++ b/chrono/agendas/models.py
@@ -16,6 +16,7 @@
# along with this program. If not, see .
import datetime
+import requests
import vobject
from django.contrib.auth.models import Group
@@ -28,7 +29,7 @@ from django.utils.dates import WEEKDAYS
from django.utils.encoding import force_text
from django.utils.formats import date_format, get_format
from django.utils.text import slugify
-from django.utils.timezone import localtime, now, make_aware, make_naive
+from django.utils.timezone import localtime, now, make_aware, make_naive, is_aware
from django.utils.translation import ugettext_lazy as _
from jsonfield import JSONField
@@ -358,6 +359,8 @@ class Desk(models.Model):
agenda = models.ForeignKey(Agenda)
label = models.CharField(_('Label'), max_length=150)
slug = models.SlugField(_('Identifier'), max_length=150)
+ timeperiod_exceptions_remote_url = models.URLField(_('URL to fetch time period exceptions from'),
+ blank=True)
def __unicode__(self):
return self.label
@@ -417,7 +420,21 @@ class Desk(models.Model):
in_two_weeks = self.get_exceptions_within_two_weeks()
return self.timeperiodexception_set.count() == len(in_two_weeks)
- def create_timeperiod_exceptions_from_ics(self, data):
+ def create_timeperiod_exceptions_from_remote_ics(self, url):
+ try:
+ response = requests.get(url)
+ response.raise_for_status()
+ except requests.HTTPError as e:
+ raise ICSError(_('Failed to retrieve remote calendar (HTTP error %s).') % e.response.status_code)
+ except requests.RequestException as e:
+ raise ICSError(_('Failed to retrieve remote calendar (%s).') % e)
+
+ return self.create_timeperiod_exceptions_from_ics(response.text, keep_synced_by_uid=True)
+
+ def remove_timeperiod_exceptions_from_remote_ics(self):
+ TimePeriodException.objects.filter(desk=self).exclude(external_id='').delete()
+
+ def create_timeperiod_exceptions_from_ics(self, data, keep_synced_by_uid=False):
try:
parsed = vobject.readOne(data)
except vobject.base.ParseError:
@@ -425,12 +442,14 @@ class Desk(models.Model):
total_created = 0
- if not parsed.contents.get('vevent'):
+ if not parsed.contents.get('vevent') and not keep_synced_by_uid:
raise ICSError(_('The file doesn\'t contain any events.'))
with transaction.atomic():
- for vevent in parsed.contents['vevent']:
+ update_datetime = now()
+ for vevent in parsed.contents.get('vevent', []):
event = {}
+
summary = vevent.contents['summary'][0].value
if not isinstance(summary, unicode):
summary = unicode(summary, 'utf-8')
@@ -441,7 +460,10 @@ class Desk(models.Model):
if not isinstance(start_dt, datetime.datetime):
start_dt = datetime.datetime.combine(start_dt,
datetime.datetime.min.time())
- event['start_datetime'] = start_dt
+ if not is_aware(start_dt):
+ event['start_datetime'] = make_aware(start_dt)
+ else:
+ event['start_datetime'] = start_dt
except AttributeError:
raise ICSError(_('Event "%s" has no start date.') % summary)
try:
@@ -452,21 +474,36 @@ class Desk(models.Model):
except AttributeError:
# events without end date are considered as ending the same day
end_dt = datetime.datetime.combine(start_dt, datetime.datetime.max.time())
- event['end_datetime'] = end_dt
-
- obj, created = TimePeriodException.objects.get_or_create(desk=self, label=summary,
- **event)
+ if not is_aware(end_dt):
+ event['end_datetime'] = make_aware(end_dt)
+ else:
+ event['end_datetime'] = end_dt
+ if keep_synced_by_uid:
+ external_id = vevent.contents['uid'][0].value
+ event['label'] = summary
+ obj, created = TimePeriodException.objects.update_or_create(desk=self, external_id=external_id,
+ defaults=event)
+ else:
+ obj, created = TimePeriodException.objects.update_or_create(desk=self, label=summary, defaults=event)
+ # return total_created
if created:
total_created += 1
+ if keep_synced_by_uid:
+ # delete all outdated exceptions from remote calendar
+ TimePeriodException.objects.filter(update_datetime__lt=update_datetime,
+ desk=self).exclude(external_id='').delete()
+
return total_created
class TimePeriodException(models.Model):
desk = models.ForeignKey(Desk)
+ external_id = models.CharField(_('External ID'), max_length=256, blank=True)
label = models.CharField(_('Optional Label'), max_length=150, blank=True, null=True)
start_datetime = models.DateTimeField(_('Exception start time'))
end_datetime = models.DateTimeField(_('Exception end time'))
+ update_datetime = models.DateTimeField(auto_now=True)
class Meta:
ordering = ['start_datetime']
diff --git a/chrono/manager/forms.py b/chrono/manager/forms.py
index cc6e37c4..9388d72e 100644
--- a/chrono/manager/forms.py
+++ b/chrono/manager/forms.py
@@ -83,7 +83,7 @@ class NewDeskForm(forms.ModelForm):
widgets = {
'agenda': forms.HiddenInput(),
}
- exclude = ['slug']
+ exclude = ['slug', 'timeperiod_exceptions_remote_url']
class DeskForm(forms.ModelForm):
@@ -92,7 +92,7 @@ class DeskForm(forms.ModelForm):
widgets = {
'agenda': forms.HiddenInput(),
}
- exclude = []
+ exclude = ['timeperiod_exceptions_remote_url']
class TimePeriodExceptionForm(forms.ModelForm):
@@ -170,5 +170,7 @@ class ExceptionsImportForm(forms.ModelForm):
model = Desk
fields = []
- ics_file = forms.FileField(label=_('ICS File'),
+ ics_file = forms.FileField(label=_('ICS File'), required=False,
help_text=_('ICS file containing events which will be considered as exceptions'))
+ ics_url = forms.URLField(label=_('URL'), required=False,
+ help_text=_('URL to remote calendar which will be synchronised hourly'))
diff --git a/chrono/manager/templates/chrono/manager_import_exceptions.html b/chrono/manager/templates/chrono/manager_import_exceptions.html
index d3eacc5c..1b119d80 100644
--- a/chrono/manager/templates/chrono/manager_import_exceptions.html
+++ b/chrono/manager/templates/chrono/manager_import_exceptions.html
@@ -13,6 +13,7 @@
{% block content %}