0001-agendas-add-global-exceptions-sources-18904.patch
chrono/agendas/management/commands/sync_desks_timeperiod_exceptions_from_settings.py | ||
---|---|---|
1 |
# chrono - agendas system |
|
2 |
# Copyright (C) 2020 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
17 |
from django.core.management.base import BaseCommand |
|
18 | ||
19 |
from chrono.agendas.models import TimePeriodExceptionSource |
|
20 | ||
21 | ||
22 |
class Command(BaseCommand): |
|
23 |
help = 'Synchronize time period exceptions from settings' |
|
24 | ||
25 |
def handle(self, **options): |
|
26 |
for source in TimePeriodExceptionSource.objects.filter(settings_slug__isnull=False): |
|
27 |
source.desk.import_timeperiod_exceptions_from_settings() |
chrono/agendas/migrations/0037_auto_20200220_1146.py | ||
---|---|---|
1 |
# -*- coding: utf-8 -*- |
|
2 |
# Generated by Django 1.11.18 on 2020-02-20 10:46 |
|
3 |
from __future__ import unicode_literals |
|
4 | ||
5 |
from django.db import migrations, models |
|
6 | ||
7 | ||
8 |
class Migration(migrations.Migration): |
|
9 | ||
10 |
dependencies = [ |
|
11 |
('agendas', '0036_auto_20191223_1758'), |
|
12 |
] |
|
13 | ||
14 |
operations = [ |
|
15 |
migrations.AddField( |
|
16 |
model_name='timeperiodexceptionsource', name='enabled', field=models.BooleanField(default=True), |
|
17 |
), |
|
18 |
migrations.AddField( |
|
19 |
model_name='timeperiodexceptionsource', |
|
20 |
name='last_update', |
|
21 |
field=models.DateTimeField(auto_now=True, null=True), |
|
22 |
), |
|
23 |
migrations.AddField( |
|
24 |
model_name='timeperiodexceptionsource', |
|
25 |
name='settings_label', |
|
26 |
field=models.CharField(max_length=150, null=True), |
|
27 |
), |
|
28 |
migrations.AddField( |
|
29 |
model_name='timeperiodexceptionsource', |
|
30 |
name='settings_slug', |
|
31 |
field=models.CharField(max_length=150, null=True), |
|
32 |
), |
|
33 |
] |
chrono/agendas/models.py | ||
---|---|---|
31 | 31 |
from django.utils.dates import WEEKDAYS |
32 | 32 |
from django.utils.encoding import force_text |
33 | 33 |
from django.utils.formats import date_format |
34 |
from django.utils.module_loading import import_string |
|
34 | 35 |
from django.utils.text import slugify |
35 | 36 |
from django.utils.timezone import localtime, now, make_aware, make_naive, is_aware |
36 |
from django.utils.translation import ugettext_lazy as _ |
|
37 |
from django.utils.translation import ugettext, ugettext_lazy as _
|
|
37 | 38 | |
38 | 39 |
from jsonfield import JSONField |
39 | 40 | |
... | ... | |
515 | 516 |
unique_together = ['agenda', 'slug'] |
516 | 517 | |
517 | 518 |
def save(self, *args, **kwargs): |
519 |
first_created = not self.pk |
|
518 | 520 |
if not self.slug: |
519 | 521 |
self.slug = generate_slug(self, agenda=self.agenda) |
520 | 522 |
super(Desk, self).save(*args, **kwargs) |
523 |
if first_created: |
|
524 |
self.import_timeperiod_exceptions_from_settings(enable=True) |
|
521 | 525 | |
522 | 526 |
@classmethod |
523 | 527 |
def import_json(cls, data): |
... | ... | |
691 | 695 | |
692 | 696 |
return openslots.search(aware_date, aware_next_date) |
693 | 697 | |
698 |
def import_timeperiod_exceptions_from_settings(self, enable=False): |
|
699 |
start_update = now() |
|
700 |
for slug, source_info in settings.EXCEPTIONS_SOURCES.items(): |
|
701 |
label = source_info['label'] |
|
702 |
source, created = TimePeriodExceptionSource.objects.update_or_create( |
|
703 |
desk=self, settings_slug=slug, defaults={'settings_label': _(label)} |
|
704 |
) |
|
705 |
if enable: |
|
706 |
source.enable() |
|
707 |
TimePeriodExceptionSource.objects.filter( |
|
708 |
desk=self, settings_slug=slug, last_update__lt=start_update |
|
709 |
).delete() # source was not in settings anymore |
|
710 | ||
694 | 711 | |
695 | 712 |
def ics_directory_path(instance, filename): |
696 | 713 |
return 'ics/{0}/{1}'.format(str(uuid.uuid4()), filename) |
... | ... | |
701 | 718 |
ics_filename = models.CharField(null=True, max_length=256) |
702 | 719 |
ics_file = models.FileField(upload_to=ics_directory_path, blank=True, null=True) |
703 | 720 |
ics_url = models.URLField(null=True, max_length=500) |
721 |
settings_slug = models.CharField(null=True, max_length=150) |
|
722 |
settings_label = models.CharField(null=True, max_length=150) |
|
723 |
last_update = models.DateTimeField(auto_now=True, null=True) |
|
724 |
enabled = models.BooleanField(default=True) |
|
704 | 725 | |
705 | 726 |
def __str__(self): |
706 | 727 |
if self.ics_filename is not None: |
707 | 728 |
return self.ics_filename |
729 |
if self.settings_label is not None: |
|
730 |
return ugettext(self.settings_label) |
|
708 | 731 |
return self.ics_url |
709 | 732 | |
733 |
def enable(self): |
|
734 |
source_info = settings.EXCEPTIONS_SOURCES.get(self.settings_slug) |
|
735 |
if not source_info: |
|
736 |
return |
|
737 |
self.timeperiodexception_set.all().delete() |
|
738 |
source_class = import_string(source_info['class']) |
|
739 |
calendar = source_class() |
|
740 |
current_year = now().year |
|
741 |
days = [day for year in range(current_year, current_year + 3) for day in calendar.holidays(year)] |
|
742 |
with transaction.atomic(): |
|
743 |
for day, label in days: |
|
744 |
start_datetime = make_aware(datetime.datetime.combine(day, datetime.datetime.min.time())) |
|
745 |
end_datetime = start_datetime + datetime.timedelta(days=1) |
|
746 |
TimePeriodException.objects.create( |
|
747 |
desk=self.desk, |
|
748 |
source=self, |
|
749 |
label=_(label), |
|
750 |
start_datetime=start_datetime, |
|
751 |
end_datetime=end_datetime, |
|
752 |
) |
|
753 |
self.enabled = True |
|
754 |
self.save() |
|
755 | ||
756 |
def disable(self): |
|
757 |
TimePeriodException.objects.filter(desk=self.desk, source=self).delete() |
|
758 |
self.enabled = False |
|
759 |
self.save() |
|
760 | ||
761 |
def delete(self, *args, **kwargs): |
|
762 |
if self.settings_slug is not None: |
|
763 |
self.disable() |
|
764 |
return (0, {}) |
|
765 |
return super().delete(*args, **kwargs) |
|
766 | ||
710 | 767 | |
711 | 768 |
class TimePeriodException(models.Model): |
712 | 769 |
desk = models.ForeignKey(Desk, on_delete=models.CASCADE) |
chrono/manager/static/css/style.scss | ||
---|---|---|
274 | 274 |
} |
275 | 275 |
} |
276 | 276 | |
277 |
ul.objects-list.single-links li a.link-action-icon.show { |
|
278 |
&::before { |
|
279 |
content: "\f06e"; /* eye */ |
|
280 |
} |
|
281 |
} |
|
282 | ||
277 | 283 |
div.ui-dialog form p span.datetime input { |
278 | 284 |
width: auto; |
279 | 285 |
} |
chrono/manager/templates/chrono/manager_confirm_source_delete.html | ||
---|---|---|
9 | 9 |
<form method="post"> |
10 | 10 |
{% csrf_token %} |
11 | 11 |
<p> |
12 |
{% if not object.settings_slug %} |
|
12 | 13 |
{% blocktrans %}Are you sure you want to delete this exception source?{% endblocktrans %} |
14 |
{% else %} |
|
15 |
{% blocktrans %}Are you sure you want to disable this exception source?{% endblocktrans %} |
|
16 |
{% endif %} |
|
13 | 17 |
</p> |
14 | 18 |
<div class="buttons"> |
15 | 19 |
<button class="delete-button">{% trans 'Delete' %}</button> |
chrono/manager/templates/chrono/manager_import_exceptions.html | ||
---|---|---|
17 | 17 |
<ul class="objects-list single-links"> |
18 | 18 |
{% for object in exception_sources %} |
19 | 19 |
<li> |
20 |
<a title="{{ object }}" {% if not object.ics_filename %}href="{{ object }}"{% endif %}>{% if object.ics_filename %}{{ object|truncatechars:50 }}{% else %}{{ object|truncatechars:50 }}{% endif %}</a>
|
|
20 |
<a {% if not object.enabled %}class="disabled"{% endif %} title="{{ object }}" {% if object.ics_url %}href="{{ object }}"{% endif %}>{% if object.ics_filename %}{{ object|truncatechars:50 }}{% else %}{{ object|truncatechars:50 }}{% endif %}</a>
|
|
21 | 21 |
{% if object.ics_filename %} |
22 | 22 |
<a rel="popup" class="link-action-icon refresh" href="{% url 'chrono-manager-time-period-exception-source-replace' object.pk %}">{% trans "replace" %}</a> |
23 |
{% else %}
|
|
23 |
{% elif object.ics_url %}
|
|
24 | 24 |
<a class="link-action-icon refresh" href="{% url 'chrono-manager-time-period-exception-source-refresh' object.pk %}">{% trans "refresh" %}</a> |
25 | 25 |
{% endif %} |
26 |
{% if object.enabled %} |
|
26 | 27 |
<a rel="popup" class="delete" href="{% url 'chrono-manager-time-period-exception-source-delete' object.pk %}">{% trans "remove" %}</a> |
28 |
{% else %} |
|
29 |
<a class="link-action-icon show" href="{% url 'chrono-manager-time-period-exception-source-enable' object.pk %}">{% trans "enable" %}</a> |
|
30 |
{% endif %} |
|
27 | 31 |
</li> |
28 | 32 |
{% endfor %} |
29 | 33 |
</ul> |
chrono/manager/urls.py | ||
---|---|---|
123 | 123 |
views.time_period_exception_source_refresh, |
124 | 124 |
name='chrono-manager-time-period-exception-source-refresh', |
125 | 125 |
), |
126 |
url( |
|
127 |
r'^time-period-exceptions-source/(?P<pk>\d+)/enable$', |
|
128 |
views.time_period_exception_source_enable, |
|
129 |
name='chrono-manager-time-period-exception-source-enable', |
|
130 |
), |
|
126 | 131 |
url( |
127 | 132 |
r'^time-period-exceptions-source/(?P<pk>\d+)/replace$', |
128 | 133 |
views.time_period_exception_source_replace, |
chrono/manager/views.py | ||
---|---|---|
1049 | 1049 |
time_period_exception_source_refresh = TimePeriodExceptionSourceRefreshView.as_view() |
1050 | 1050 | |
1051 | 1051 | |
1052 |
class TimePeriodExceptionSourceEnableView(ManagedDeskSubobjectMixin, DetailView): |
|
1053 |
model = TimePeriodExceptionSource |
|
1054 | ||
1055 |
def get(self, request, *args, **kwargs): |
|
1056 |
source = self.get_object() |
|
1057 |
source.enable() |
|
1058 |
messages.info( |
|
1059 |
self.request, _('Exception source %s has been enabled on desk %s.' % (source, source.desk)) |
|
1060 |
) |
|
1061 |
return HttpResponseRedirect( |
|
1062 |
reverse('chrono-manager-agenda-settings', kwargs={'pk': source.desk.agenda_id}) |
|
1063 |
) |
|
1064 | ||
1065 | ||
1066 |
time_period_exception_source_enable = TimePeriodExceptionSourceEnableView.as_view() |
|
1067 | ||
1068 | ||
1052 | 1069 |
def menu_json(request): |
1053 | 1070 |
label = _('Agendas') |
1054 | 1071 |
json_str = json.dumps( |
chrono/settings.py | ||
---|---|---|
26 | 26 |
import os |
27 | 27 |
from django.conf.global_settings import STATICFILES_FINDERS |
28 | 28 | |
29 |
_ = lambda s: s |
|
30 | ||
29 | 31 |
# Build paths inside the project like this: os.path.join(BASE_DIR, ...) |
30 | 32 |
BASE_DIR = os.path.dirname(os.path.dirname(__file__)) |
31 | 33 | |
... | ... | |
161 | 163 |
# (see http://docs.python-requests.org/en/master/user/advanced/#proxies) |
162 | 164 |
REQUESTS_PROXIES = None |
163 | 165 | |
166 |
EXCEPTIONS_SOURCES = { |
|
167 |
'holidays': {'class': 'workalendar.europe.France', 'label': _('Holidays')}, |
|
168 |
} |
|
169 | ||
164 | 170 |
local_settings_file = os.environ.get( |
165 | 171 |
'CHRONO_SETTINGS_FILE', os.path.join(os.path.dirname(__file__), 'local_settings.py') |
166 | 172 |
) |
setup.py | ||
---|---|---|
168 | 168 |
'vobject', |
169 | 169 |
'python-dateutil', |
170 | 170 |
'requests', |
171 |
'workalendar', |
|
171 | 172 |
], |
172 | 173 |
zip_safe=False, |
173 | 174 |
cmdclass={ |
174 |
- |