Projet

Général

Profil

0001-agendas-add-global-exceptions-sources-18904.patch

Valentin Deniaud, 20 février 2020 14:51

Télécharger (13,5 ko)

Voir les différences:

Subject: [PATCH] agendas: add global exceptions sources (#18904)

 ...sks_timeperiod_exceptions_from_settings.py | 27 +++++++++
 .../migrations/0037_auto_20200220_1146.py     | 33 +++++++++++
 chrono/agendas/models.py                      | 59 ++++++++++++++++++-
 chrono/manager/static/css/style.scss          |  6 ++
 .../chrono/manager_confirm_source_delete.html |  4 ++
 .../chrono/manager_import_exceptions.html     |  8 ++-
 chrono/manager/urls.py                        |  5 ++
 chrono/manager/views.py                       | 17 ++++++
 chrono/settings.py                            |  6 ++
 setup.py                                      |  1 +
 10 files changed, 163 insertions(+), 3 deletions(-)
 create mode 100644 chrono/agendas/management/commands/sync_desks_timeperiod_exceptions_from_settings.py
 create mode 100644 chrono/agendas/migrations/0037_auto_20200220_1146.py
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
-