0002-agendas-add-unicity-constraint-on-exception-source-s.patch
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 |
- |