Projet

Général

Profil

0002-agendas-add-unicity-constraint-on-exception-source-s.patch

Valentin Deniaud, 21 octobre 2020 18:08

Télécharger (8,05 ko)

Voir les différences:

Subject: [PATCH 2/2] agendas: add unicity constraint on exception source slug
 (#47916)

 ...iodexceptionsource_unique_settings_slug.py | 47 +++++++++++
 .../migrations/0067_auto_20201021_1746.py     | 18 ++++
 chrono/agendas/models.py                      |  3 +
 tests/test_misc.py                            | 84 +++++++++++++++++++
 4 files changed, 152 insertions(+)
 create mode 100644 chrono/agendas/migrations/0066_timeperiodexceptionsource_unique_settings_slug.py
 create mode 100644 chrono/agendas/migrations/0067_auto_20201021_1746.py
chrono/agendas/migrations/0066_timeperiodexceptionsource_unique_settings_slug.py
1
# -*- coding: utf-8 -*-
2
# Generated by Django 1.11.18 on 2020-10-21 11:56
3
from __future__ import unicode_literals
4

  
5
from django.db import migrations, models
6
from django.db.models import Count, F
7

  
8

  
9
def remove_broken_exceptions(apps, schema_editor):
10
    TimePeriodException = apps.get_model('agendas', 'TimePeriodException')
11
    # an exception is broken if its desk in not the same at the desk of its source
12
    qs = TimePeriodException.objects.filter(source__isnull=False)
13
    qs.exclude(source__desk=F('desk')).delete()
14

  
15

  
16
def remove_duplicate_sources(apps, schema_editor):
17
    Desk = apps.get_model('agendas', 'Desk')
18
    TimePeriodExceptionSource = apps.get_model('agendas', 'TimePeriodExceptionSource')
19
    for desk in Desk.objects.all():
20
        duplicate_source_slugs = (
21
            desk.timeperiodexceptionsource_set.values('settings_slug')
22
            .annotate(count=Count('settings_slug'))
23
            .order_by()
24
            .filter(count__gt=1)
25
        )
26
        if not duplicate_source_slugs:
27
            continue
28
        for source in duplicate_source_slugs:
29
            settings_slug = source['settings_slug']
30
            duplicate_sources = desk.timeperiodexceptionsource_set.filter(settings_slug=settings_slug)
31
            # remove duplicates, keeping the one that has related time period exceptions, if any
32
            source_to_keep = duplicate_sources.filter(timeperiodexception__isnull=False).first()
33
            if not source_to_keep:
34
                source_to_keep = duplicate_sources.first()
35
            duplicate_sources.exclude(pk=source_to_keep.pk).delete()
36

  
37

  
38
class Migration(migrations.Migration):
39

  
40
    dependencies = [
41
        ('agendas', '0065_unavailability_calendar'),
42
    ]
43

  
44
    operations = [
45
        migrations.RunPython(remove_broken_exceptions, migrations.RunPython.noop),
46
        migrations.RunPython(remove_duplicate_sources, migrations.RunPython.noop),
47
    ]
chrono/agendas/migrations/0067_auto_20201021_1746.py
1
# -*- coding: utf-8 -*-
2
# Generated by Django 1.11.18 on 2020-10-21 15:46
3
from __future__ import unicode_literals
4

  
5
from django.db import migrations
6

  
7

  
8
class Migration(migrations.Migration):
9

  
10
    dependencies = [
11
        ('agendas', '0066_timeperiodexceptionsource_unique_settings_slug'),
12
    ]
13

  
14
    operations = [
15
        migrations.AlterUniqueTogether(
16
            name='timeperiodexceptionsource', unique_together=set([('desk', 'settings_slug')]),
17
        ),
18
    ]
chrono/agendas/models.py
1433 1433
    last_update = models.DateTimeField(auto_now=True, null=True)
1434 1434
    enabled = models.BooleanField(default=True)
1435 1435

  
1436
    class Meta:
1437
        unique_together = ['desk', 'settings_slug']
1438

  
1436 1439
    def __str__(self):
1437 1440
        if self.ics_filename is not None:
1438 1441
            return self.ics_filename
tests/test_misc.py
4 4
from django.db import ProgrammingError
5 5
from django.db import connection
6 6
from django.db import transaction
7
from django.db.migrations.executor import MigrationExecutor
7 8
from django.utils.timezone import now
8 9

  
9 10

  
......
209 210
    Event.objects.create(
210 211
        start_datetime=event1.start_datetime, meeting_type=meeting_type1, places=10, agenda=agenda, desk=desk1
211 212
    )
213

  
214

  
215
def test_clean_time_period_exceptions(transactional_db):
216
    app = 'agendas'
217

  
218
    migrate_from = [(app, '0065_unavailability_calendar')]
219
    migrate_to = [(app, '0066_timeperiodexceptionsource_unique_settings_slug')]
220
    executor = MigrationExecutor(connection)
221
    old_apps = executor.loader.project_state(migrate_from).apps
222
    executor.migrate(migrate_from)
223

  
224
    Agenda = old_apps.get_model(app, 'Agenda')
225
    Desk = old_apps.get_model(app, 'Desk')
226
    TimePeriodException = old_apps.get_model(app, 'TimePeriodException')
227
    TimePeriodExceptionSource = old_apps.get_model(app, 'TimePeriodExceptionSource')
228

  
229
    agenda = Agenda.objects.create(label='Agenda')
230
    desk = Desk.objects.create(label='Desk', slug='desk', agenda=agenda)
231

  
232
    # add normal time period exception to Desk
233
    source_desk = TimePeriodExceptionSource.objects.create(desk=desk, settings_slug='holidays', enabled=True)
234
    start_datetime = datetime.datetime(year=2020, month=1, day=2)
235
    end_datetime = datetime.datetime(year=2020, month=1, day=3)
236
    for i in range(5):
237
        TimePeriodException.objects.create(
238
            desk=desk,
239
            source=source_desk,
240
            external=True,
241
            start_datetime=start_datetime,
242
            end_datetime=end_datetime,
243
        )
244

  
245
    # now simulate broken state (desk duplication)
246
    new_desk = Desk.objects.create(label='New Desk', slug='new-desk', agenda=agenda)
247

  
248
    # normal source and exceptions
249
    source_new_desk = TimePeriodExceptionSource.objects.create(
250
        desk=new_desk, settings_slug='holidays', enabled=True
251
    )
252
    for i in range(5):
253
        TimePeriodException.objects.create(
254
            desk=new_desk,
255
            source=source_new_desk,
256
            external=True,
257
            start_datetime=start_datetime,
258
            end_datetime=end_datetime,
259
        )
260

  
261
    # wrong duplicate of source
262
    TimePeriodExceptionSource.objects.create(desk=new_desk, settings_slug='holidays', enabled=True)
263

  
264
    # wrong duplicate of exceptions, referencing original desk source
265
    for i in range(5):
266
        TimePeriodException.objects.create(
267
            desk=new_desk,
268
            source=source_desk,
269
            external=True,
270
            start_datetime=start_datetime,
271
            end_datetime=end_datetime,
272
        )
273

  
274
    # ensure migration fixes state
275
    executor = MigrationExecutor(connection)
276
    executor.migrate(migrate_to)
277
    executor.loader.build_graph()
278

  
279
    apps = executor.loader.project_state(migrate_to).apps
280
    Desk = apps.get_model(app, 'Desk')
281
    TimePeriodException = apps.get_model(app, 'TimePeriodException')
282
    TimePeriodExceptionSource = apps.get_model(app, 'TimePeriodExceptionSource')
283

  
284
    # original desk hasn't been touched
285
    desk = Desk.objects.get(pk=desk.pk)
286
    assert desk.timeperiodexception_set.count() == 5
287
    assert desk.timeperiodexceptionsource_set.filter(settings_slug='holidays').count() == 1
288

  
289
    # duplicated desk has correct exceptions
290
    new_desk = Desk.objects.get(pk=new_desk.pk)
291
    assert new_desk.timeperiodexception_set.count() == 5
292
    assert new_desk.timeperiodexceptionsource_set.filter(settings_slug='holidays').count() == 1
293

  
294
    exc = new_desk.timeperiodexception_set.first()
295
    assert exc.source == new_desk.timeperiodexceptionsource_set.get(settings_slug='holidays')
212
-