diff --git a/combo/apps/search/models.py b/combo/apps/search/models.py index 37e8f7af..9d147f2e 100644 --- a/combo/apps/search/models.py +++ b/combo/apps/search/models.py @@ -168,8 +168,7 @@ class SearchCell(CellBase): return cell_data for options in cell_data['fields']['_search_services']['options'].values(): if options.get('target_page'): - page_slug = options['target_page'].strip('/').split('/')[-1] - options['target_page'] = Page.get_page_ids_by_slugs().get(page_slug) or '' + options['target_page'] = Page.get_page_ids_by_uuids().get(options['target_page']) return cell_data def modify_global_context(self, context, request): diff --git a/combo/apps/wcs/models.py b/combo/apps/wcs/models.py index 308fb9ed..0a0682c4 100644 --- a/combo/apps/wcs/models.py +++ b/combo/apps/wcs/models.py @@ -947,8 +947,7 @@ class WcsCardCell(CardMixin, CellBase): custom_schema = cell_data['fields']['custom_schema'] for cell in custom_schema.get('cells') or []: if cell.get('page'): - page_slug = cell['page'].strip('/').split('/')[-1] - cell['page'] = Page.get_page_ids_by_slugs().get(page_slug) or '' + cell['page'] = Page.get_page_ids_by_uuids().get(cell['page']) or '' return cell_data diff --git a/combo/data/migrations/0060_page_uuid.py b/combo/data/migrations/0060_page_uuid.py new file mode 100644 index 00000000..eff9842e --- /dev/null +++ b/combo/data/migrations/0060_page_uuid.py @@ -0,0 +1,16 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('data', '0059_textcell_title'), + ] + + operations = [ + migrations.AddField( + model_name='page', + name='uuid', + field=models.UUIDField(editable=False, null=True), + ), + ] diff --git a/combo/data/migrations/0061_page_uuid.py b/combo/data/migrations/0061_page_uuid.py new file mode 100644 index 00000000..6a42a8dc --- /dev/null +++ b/combo/data/migrations/0061_page_uuid.py @@ -0,0 +1,54 @@ +import hashlib +import uuid + +from django.db import migrations + + +def natural_key(page): + def get_parents_and_self(page): + pages = [page] + _page = page + while _page.parent_id: + _page = _page.parent + pages.append(_page) + return list(reversed(pages)) + + def get_online_url(page): + parts = [x.slug for x in get_parents_and_self(page)] + if parts[0] == 'index': + parts = parts[1:] + if not parts: + return '/' + return '/' + '/'.join(parts) + '/' + + return get_online_url(page).strip('/') + + +def forward(apps, schema_editor): + Page = apps.get_model('data', 'Page') + known_uuids = set(Page.objects.filter(uuid__isnull=False).values_list('uuid', flat=True)) + for page in Page.objects.filter(uuid__isnull=True): + if page.snapshot is not None: + page.uuid = uuid.uuid4() + page.save() + known_uuids.add(page.uuid) + continue + slug = natural_key(page) or 'index' + slug_hash = hashlib.sha256(slug.encode('utf-8')) + page.uuid = uuid.UUID(slug_hash.hexdigest()[:32]) + if page.uuid in known_uuids: + # uuid unicity ! + page.uuid = uuid.uuid4() + known_uuids.add(page.uuid) + page.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('data', '0060_page_uuid'), + ] + + operations = [ + migrations.RunPython(forward, reverse_code=migrations.RunPython.noop), + ] diff --git a/combo/data/migrations/0062_page_uuid.py b/combo/data/migrations/0062_page_uuid.py new file mode 100644 index 00000000..77534f11 --- /dev/null +++ b/combo/data/migrations/0062_page_uuid.py @@ -0,0 +1,18 @@ +import uuid + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('data', '0061_page_uuid'), + ] + + operations = [ + migrations.AlterField( + model_name='page', + name='uuid', + field=models.UUIDField(default=uuid.uuid4, editable=False, unique=True), + ), + ] diff --git a/combo/data/migrations/0063_old_card_cells.py b/combo/data/migrations/0063_old_card_cells.py new file mode 100644 index 00000000..00706a6d --- /dev/null +++ b/combo/data/migrations/0063_old_card_cells.py @@ -0,0 +1,36 @@ +from django.db import migrations + + +def forward(apps, schema_editor): + PageSnapshot = apps.get_model('data', 'PageSnapshot') + for snapshot in PageSnapshot.objects.all(): + changed = False + for cell in snapshot.serialization.get('cells') or []: + if cell.get('model') not in ['wcs.wcscardinfoscell', 'wcs.wcscardscell']: + continue + if cell['model'] == 'wcs.wcscardinfoscell': + cell['model'] = 'wcs.wcscardcell' + elif cell['model'] == 'wcs.wcscardscell': + cell['model'] = 'wcs.wcscardcell' + cell['fields']['card_ids'] = '' + cell['fields']['related_card_path'] = '__all__' + cell['fields']['display_mode'] = 'table' + cell['fields']['title_type'] = 'auto' + cell['fields']['custom_schema'] = {} + if cell['fields'].get('custom_title'): + cell['fields']['title_type'] = 'manual' + changed = True + if changed: + snapshot.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('data', '0062_page_uuid'), + ('wcs', '0053_new_card_cell'), + ] + + operations = [ + migrations.RunPython(forward, reverse_code=migrations.RunPython.noop), + ] diff --git a/combo/data/migrations/0064_snapshot_uuids.py b/combo/data/migrations/0064_snapshot_uuids.py new file mode 100644 index 00000000..e729bded --- /dev/null +++ b/combo/data/migrations/0064_snapshot_uuids.py @@ -0,0 +1,102 @@ +import uuid + +from django.db import migrations + + +def get_page_uuid(page_uuids_by_slugs, slug, default_index=False): + try: + uuid.UUID(slug) + except (ValueError, AttributeError): + pass + else: + # it's a uuid, don't change it + if slug in page_uuids_by_slugs.values(): + return slug + + slug = str(slug).strip('/').rsplit('/', maxsplit=1)[-1] + if not slug and default_index: + slug = 'index' + return page_uuids_by_slugs.get(slug) + + +def forward(apps, schema_editor): + PageSnapshot = apps.get_model('data', 'PageSnapshot') + Page = apps.get_model('data', 'Page') + page_uuids_by_slugs = {page.slug: str(page.uuid) for page in Page.objects.only('uuid', 'slug')} + for snapshot in PageSnapshot.objects.all(): + changed = False + + if (snapshot.serialization.get('fields') or {}).get('parent'): + new_uuid = get_page_uuid( + page_uuids_by_slugs, snapshot.serialization['fields']['parent'][0], default_index=True + ) + if new_uuid: + new_uuid = [new_uuid] + snapshot.serialization['fields']['parent'] = new_uuid + changed = True + + for cell in snapshot.serialization.get('cells') or []: + if cell.get('model') not in [ + 'data.linkcell', + 'data.linklistcell', + 'search.searchcell', + 'wcs.wcscardcell', + ]: + continue + + if cell['model'] == 'data.linkcell': + if not cell['fields'].get('link_page'): + continue + new_uuid = get_page_uuid(page_uuids_by_slugs, cell['fields']['link_page'][0]) + if new_uuid: + new_uuid = [new_uuid] + cell['fields']['link_page'] = new_uuid + changed = True + + elif cell['model'] == 'data.linklistcell': + for link in cell['fields'].get('links') or []: + if link.get('model') != 'data.linkcell': + continue + if not link['fields'].get('link_page'): + continue + new_uuid = get_page_uuid(page_uuids_by_slugs, link['fields']['link_page'][0]) + if new_uuid: + new_uuid = [new_uuid] + link['fields']['link_page'] = new_uuid + changed = True + + elif cell['model'] == 'search.searchcell': + if not cell['fields'].get('_search_services'): + continue + if not cell['fields']['_search_services'].get('options'): + continue + for option in cell['fields']['_search_services']['options'].values(): + if not option: + continue + if not option.get('target_page'): + continue + option['target_page'] = get_page_uuid(page_uuids_by_slugs, option['target_page']) + changed = True + + elif cell['model'] == 'wcs.wcscardcell': + if not cell['fields'].get('custom_schema'): + continue + for custom_cell in cell['fields']['custom_schema'].get('cells') or []: + if not custom_cell.get('page'): + continue + custom_cell['page'] = get_page_uuid(page_uuids_by_slugs, custom_cell['page']) + changed = True + + if changed: + snapshot.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('data', '0063_old_card_cells'), + ] + + operations = [ + migrations.RunPython(forward, reverse_code=migrations.RunPython.noop), + ] diff --git a/combo/data/models.py b/combo/data/models.py index 209c068d..d77cda43 100644 --- a/combo/data/models.py +++ b/combo/data/models.py @@ -24,6 +24,7 @@ import os import re import subprocess import urllib.parse +import uuid import feedparser import requests @@ -179,9 +180,8 @@ class PageManager(models.Manager): self.snapshots = kwargs.pop('snapshots', False) super().__init__(*args, **kwargs) - def get_by_natural_key(self, path): - parts = [x for x in path.strip('/').split('/') if x] or ['index'] - return self.get(slug=parts[-1]) + def get_by_natural_key(self, uuid): + return self.get(uuid=uuid) def get_queryset(self): queryset = super().get_queryset() @@ -195,6 +195,7 @@ class Page(models.Model): objects = PageManager() snapshots = PageManager(snapshots=True) + uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True) title = models.CharField(_('Title'), max_length=150) slug = models.SlugField(_('Slug')) sub_slug = models.CharField( @@ -262,12 +263,12 @@ class Page(models.Model): return str(self.title) def natural_key(self): - return (self.get_online_url(follow_redirection=False).strip('/'),) + return (str(self.uuid),) @classmethod @utils.cache_during_request - def get_page_ids_by_slugs(cls): - return {page.slug: page.pk for page in cls.objects.only('pk', 'slug')} + def get_page_ids_by_uuids(cls): + return {str(page.uuid): page.pk for page in cls.objects.only('pk', 'uuid')} def picture_extension(self): if not self.picture: @@ -584,13 +585,13 @@ class Page(models.Model): if page is None: qs_kwargs = {} if snapshot: - qs_kwargs = {'snapshot': snapshot} - elif json_page['fields'].get('parent'): - qs_kwargs = {'parent__slug': json_page['fields']['parent'][0].split('/')[-1] or 'index'} - page, created = Page.objects.get_or_create(slug=json_page['fields']['slug'], **qs_kwargs) + qs_kwargs = {'snapshot': snapshot} # don't take uuid from snapshot: it has to be unique ! + else: + qs_kwargs = {'uuid': json_page['fields']['uuid']} + page, created = Page.objects.get_or_create(**qs_kwargs) json_page['pk'] = page.id - parent_slug = json_page['fields'].get('parent') or [] - if parent_slug and not Page.objects.filter(slug=parent_slug[0].split('/')[-1] or 'index').exists(): + parent_uuid = json_page['fields'].get('parent') or [] + if parent_uuid and not Page.objects.filter(uuid=parent_uuid[0]).exists(): # parent not found, remove it and exclude page from navigation json_page['fields'].pop('parent') json_page['fields']['exclude_from_navigation'] = True @@ -602,8 +603,12 @@ class Page(models.Model): ) % json_page['fields']['title'], ) + page_uuid = page.uuid page = next(serializers.deserialize('json', json.dumps([json_page]), ignorenonexistent=True)) page.object.snapshot = snapshot + if snapshot: + # keep the generated uuid + page.object.uuid = page_uuid page.save() for cell in json_page.get('cells'): cell['fields']['groups'] = [[x] for x in cell['fields']['groups'] if isinstance(x, str)] @@ -622,16 +627,6 @@ class Page(models.Model): def load_serialized_cells(cls, cells): # load new cells for cell_data in cells: - # legacy card cells, for snapshots - if cell_data['model'] == 'wcs.wcscardinfoscell': - cell_data['model'] = 'wcs.wcscardcell' - elif cell_data['model'] == 'wcs.wcscardscell': - cell_data['model'] = 'wcs.wcscardcell' - cell_data['fields']['related_card_path'] = '__all__' - cell_data['fields']['display_mode'] = 'table' - cell_data['fields']['title_type'] = 'auto' - if cell_data['fields'].get('custom_title'): - cell_data['fields']['title_type'] = 'manual' model = apps.get_model(cell_data['model']) cell_data = model.prepare_serialized_data(cell_data) cell = list(serializers.deserialize('json', json.dumps([cell_data]), ignorenonexistent=True))[0] @@ -651,13 +646,7 @@ class Page(models.Model): for json_page in json_site: # pre-create pages - parent = None - if json_page['fields'].get('parent'): - parent = json_page['fields']['parent'][0].split('/')[-1] or 'index' - - page, created = Page.objects.get_or_create( - slug=json_page['fields']['slug'], parent__slug=parent - ) + page, created = Page.objects.get_or_create(uuid=json_page['fields']['uuid']) to_load.append((page, created, json_page)) # delete cells of already existing pages @@ -720,6 +709,8 @@ class Page(models.Model): new_page.title = title or _('Copy of %s') % self.title # reset slug new_page.slug = None + # reset uuid + new_page.uuid = uuid.uuid4() # reset snapshot new_page.snapshot = None # set order @@ -776,10 +767,12 @@ class PageSnapshot(models.Model): def restore(self): json_page = self.serialization + # keep current page uuid + json_page['fields']['uuid'] = str(self.page.uuid) # keep current page order json_page['fields']['order'] = self.page.order # and current parent - json_page['fields']['parent'] = [self.page.parent.slug] if self.page.parent else None + json_page['fields']['parent'] = self.page.parent.natural_key() if self.page.parent else None # and current exclude_from_navigation value json_page['fields']['exclude_from_navigation'] = self.page.exclude_from_navigation # restore snapshot @@ -1717,8 +1710,7 @@ class LinkCell(CellBase): @classmethod def prepare_serialized_data(cls, cell_data): if cell_data['fields'].get('link_page'): - link_page_slug = cell_data['fields']['link_page'][0].strip('/').split('/')[-1] - if link_page_slug not in Page.get_page_ids_by_slugs(): + if cell_data['fields']['link_page'][0] not in Page.get_page_ids_by_uuids(): del cell_data['fields']['link_page'] return cell_data diff --git a/combo/manager/views.py b/combo/manager/views.py index 0231b6f0..33761241 100644 --- a/combo/manager/views.py +++ b/combo/manager/views.py @@ -659,7 +659,7 @@ def snapshot_export(request, *args, **kwargs): page = snapshot.get_page() response = HttpResponse(content_type='application/json') response['Content-Disposition'] = 'attachment; filename="export_page_{}_{}.json"'.format( - snapshot.get_page().slug, snapshot.timestamp.strftime('%Y%m%d') + page.slug, snapshot.timestamp.strftime('%Y%m%d') ) json.dump(page.get_serialized_page(), response, indent=2) return response diff --git a/tests/test_import_export.py b/tests/test_import_export.py index 7d6b8271..0f08acf3 100644 --- a/tests/test_import_export.py +++ b/tests/test_import_export.py @@ -5,6 +5,7 @@ import shutil import sys import tarfile import tempfile +import uuid from io import BytesIO, StringIO import pytest @@ -97,7 +98,7 @@ def test_import_export(app, some_data): def test_import_export_with_parent(app, some_data): output = get_output_of_command('export_site') payload = json.loads(output) - payload['pages'][1]['fields']['parent'] = ['one'] + payload['pages'][1]['fields']['parent'] = [str(Page.objects.get(slug='one').uuid)] Page.objects.all().delete() import_site(data=payload) @@ -111,7 +112,7 @@ def test_import_export_with_unknown_parent(app, some_data): output = get_output_of_command('export_site') payload = json.loads(output) payload['pages'][0]['fields']['exclude_from_navigation'] = False - payload['pages'][0]['fields']['parent'] = ['unknown-parent'] + payload['pages'][0]['fields']['parent'] = [str(uuid.uuid4())] Page.objects.all().delete() import_site(data=payload) diff --git a/tests/test_manager.py b/tests/test_manager.py index 5e92d871..54449ba8 100644 --- a/tests/test_manager.py +++ b/tests/test_manager.py @@ -5,6 +5,7 @@ import os import re import shutil import urllib.parse +import uuid from io import BytesIO from unittest import mock @@ -925,12 +926,16 @@ def test_export_page_snapshot(freezer, app, admin_user): history = app.get('/manage/pages/%s/history' % page.id, status=200) resp = history.click('export', index=0, verbose=True) + snapshot_page = Page.snapshots.latest('pk') + assert snapshot_page.uuid != page.uuid assert resp.headers['content-type'] == 'application/json' assert resp.headers['content-disposition'] == 'attachment; filename="export_page_one_20200717.json"' assert resp.json['fields']['title'] == 'Updated Title' resp = history.click('export', index=1, verbose=True) + snapshot_page = Page.snapshots.latest('pk') + assert snapshot_page.uuid != page.uuid assert resp.headers['content-type'] == 'application/json' assert resp.headers['content-disposition'] == 'attachment; filename="export_page_one_20200716.json"' @@ -1001,7 +1006,7 @@ def test_export_page_with_redirection(app, admin_user): resp = app.get('/manage/pages/%s/' % page1.pk) resp = resp.click('Export') assert resp.json['pages'][0]['cells'][0]['fields']['link_page'] == [ - 'two' + str(page2.uuid) ] # and not http://www.example.net @@ -1041,7 +1046,7 @@ def test_site_export_import_json(app, admin_user): resp.form['site_file'] = Upload('site-export.json', site_export, 'application/json') with CaptureQueriesContext(connection) as ctx: resp = resp.form.submit() - assert len(ctx.captured_queries) in [304, 305] + assert len(ctx.captured_queries) in [295, 296] assert Page.objects.count() == 4 assert PageSnapshot.objects.all().count() == 4 @@ -1052,7 +1057,7 @@ def test_site_export_import_json(app, admin_user): resp.form['site_file'] = Upload('site-export.json', site_export, 'application/json') with CaptureQueriesContext(connection) as ctx: resp = resp.form.submit() - assert len(ctx.captured_queries) == 274 + assert len(ctx.captured_queries) == 276 assert set(Page.objects.get(slug='one').related_cells['cell_types']) == {'data_textcell', 'data_linkcell'} assert Page.objects.count() == 4 assert LinkCell.objects.count() == 2 @@ -1172,7 +1177,7 @@ def test_site_export_import_unknown_parent(app, admin_user): resp = resp.form.submit() payload = json.loads(force_str(resp.body)) payload['pages'][0]['fields']['exclude_from_navigation'] = False - payload['pages'][0]['fields']['parent'] = ['unknown-parent'] + payload['pages'][0]['fields']['parent'] = [str(uuid.uuid4())] resp = app.get('/manage/') resp = resp.click('Import Site') @@ -1193,13 +1198,15 @@ def test_site_export_import_unknown_page(app, admin_user): resp = app.get('/manage/site-export') resp = resp.form.submit() payload = json.loads(force_str(resp.body)) - payload['pages'][0]['cells'][0]['fields']['root_page'] = ['unknown-parent'] + payload['pages'][0]['cells'][0]['fields']['root_page'] = [str(uuid.uuid4())] Page.objects.all().delete() resp = app.get('/manage/site-import') resp.form['site_file'] = Upload('site-export.json', force_bytes(json.dumps(payload)), 'application/json') resp = resp.form.submit() - assert resp.context['form'].errors['site_file'] == ['Unknown page "unknown-parent".'] + assert resp.context['form'].errors['site_file'] == [ + 'Unknown page "%s".' % payload['pages'][0]['cells'][0]['fields']['root_page'][0] + ] assert Page.objects.count() == 0 resp = app.get('/manage/site-import') @@ -1307,6 +1314,7 @@ def test_duplicate_page(app, admin_user): in resp.text ) assert new_page.exclude_from_navigation is True + assert new_page.uuid != page.uuid page.exclude_from_navigation = True page.save() @@ -2470,9 +2478,6 @@ def test_page_discover_placeholder_with_error_cells(app, admin_user): def test_page_versionning(app, admin_user): - Page.objects.all().delete() - PageSnapshot.objects.all() - page = Page(title='One', slug='one') page.save() @@ -2517,8 +2522,10 @@ def test_page_versionning(app, admin_user): with CaptureQueriesContext(connection) as ctx: resp2 = resp.click('view', index=1) - assert len(ctx.captured_queries) == 70 + assert len(ctx.captured_queries) == 72 assert Page.snapshots.latest('pk').related_cells == {'cell_types': ['data_textcell']} + snapshot_page = Page.snapshots.latest('pk') + assert snapshot_page.uuid != page.uuid assert resp2.text.index('Hello world') < resp2.text.index('Foobar3') resp2 = resp.click('view', index=0) @@ -2616,6 +2623,8 @@ def test_restore_page_attributes(app): snapshot = PageSnapshot.objects.latest('pk') # move page2, remove page 1 + new_uuid = uuid.uuid4() + page2.uuid = new_uuid page2.parent = page4 page2.order = 3 page2.save() @@ -2635,6 +2644,7 @@ def test_restore_page_attributes(app): # check some attributes assert page2.order == 3 assert page2.parent == page4 + assert page2.uuid == new_uuid assert page2.exclude_from_navigation is False PageSnapshot.take(page2) diff --git a/tests/test_migrations.py b/tests/test_migrations.py new file mode 100644 index 00000000..061a1544 --- /dev/null +++ b/tests/test_migrations.py @@ -0,0 +1,504 @@ +import uuid + +from django.db import connection +from django.db.migrations.executor import MigrationExecutor + + +def test_page_snapshot_with_old_card_cells_migration(transactional_db): + migrate_from = [('data', '0062_page_uuid')] + migrate_to = [('data', '0063_old_card_cells')] + + executor = MigrationExecutor(connection) + old_apps = executor.loader.project_state(migrate_from).apps + executor.migrate(migrate_from) + + pagesnapshot_class = old_apps.get_model('data', 'PageSnapshot') + + snapshot = pagesnapshot_class.objects.create( + serialization={ + 'cells': [ + { + 'model': 'wcs.wcscardinfoscell', + 'fields': { + 'card_ids': '42,35', + 'title_type': 'manual', + 'custom_title': 'my-title', + 'display_mode': 'card', + 'without_user': True, + 'custom_schema': {}, + 'only_for_user': True, + 'carddef_reference': 'default:card_model_1', + 'related_card_path': '', + 'limit': 42, + 'slug': 'my-card', + 'order': 1, + 'groups': [], + 'public': True, + 'condition': 'my-condition', + 'placeholder': 'content', + 'template_name': None, + 'extra_css_class': '', + 'last_update_timestamp': '2022-08-11T13:57:43.362Z', + 'restricted_to_unlogged': False, + }, + }, + { + 'model': 'wcs.wcscardscell', + 'fields': { + 'custom_title': 'my-other-title', + 'without_user': False, + 'only_for_user': False, + 'carddef_reference': 'default:card_model_1', + 'limit': 35, + 'slug': 'my-other-card', + 'order': 2, + 'groups': [], + 'public': True, + 'condition': '', + 'placeholder': 'content', + 'template_name': None, + 'extra_css_class': '', + 'last_update_timestamp': '2022-08-12T07:19:18.541Z', + 'restricted_to_unlogged': False, + }, + }, + ], + } + ) + + executor = MigrationExecutor(connection) + executor.migrate(migrate_to) + executor.loader.build_graph() + + apps = executor.loader.project_state(migrate_to).apps + pagesnapshot_class = apps.get_model('data', 'PageSnapshot') + + snapshot = pagesnapshot_class.objects.get() + assert snapshot.serialization['cells'][0] == { + 'model': 'wcs.wcscardcell', + 'fields': { + 'card_ids': '42,35', + 'title_type': 'manual', + 'custom_title': 'my-title', + 'display_mode': 'card', + 'without_user': True, + 'custom_schema': {}, + 'only_for_user': True, + 'carddef_reference': 'default:card_model_1', + 'related_card_path': '', + 'limit': 42, + 'slug': 'my-card', + 'order': 1, + 'groups': [], + 'public': True, + 'condition': 'my-condition', + 'placeholder': 'content', + 'template_name': None, + 'extra_css_class': '', + 'last_update_timestamp': '2022-08-11T13:57:43.362Z', + 'restricted_to_unlogged': False, + }, + } + assert snapshot.serialization['cells'][1] == { + 'model': 'wcs.wcscardcell', + 'fields': { + 'card_ids': '', + 'title_type': 'manual', + 'custom_title': 'my-other-title', + 'display_mode': 'table', + 'without_user': False, + 'custom_schema': {}, + 'only_for_user': False, + 'carddef_reference': 'default:card_model_1', + 'related_card_path': '__all__', + 'limit': 35, + 'slug': 'my-other-card', + 'order': 2, + 'groups': [], + 'public': True, + 'condition': '', + 'placeholder': 'content', + 'template_name': None, + 'extra_css_class': '', + 'last_update_timestamp': '2022-08-12T07:19:18.541Z', + 'restricted_to_unlogged': False, + }, + } + + +def test_page_snapshot_uuids_migration(transactional_db): + migrate_from = [('data', '0063_old_card_cells')] + migrate_to = [('data', '0064_snapshot_uuids')] + + executor = MigrationExecutor(connection) + old_apps = executor.loader.project_state(migrate_from).apps + executor.migrate(migrate_from) + + page_class = old_apps.get_model('data', 'Page') + pagesnapshot_class = old_apps.get_model('data', 'PageSnapshot') + + root = page_class.objects.create(order=0, slug='index') + page = page_class.objects.create(order=1, slug='slug') + old_uuid = uuid.uuid4() + link_cells = [ + { + 'model': 'data.linkcell', + 'fields': { + 'link_page': None, + }, + }, + { + 'model': 'data.linkcell', + 'fields': { + 'link_page': [str(old_uuid)], + }, + }, + { + 'model': 'data.linkcell', + 'fields': { + 'link_page': [str(page.uuid)], # will not change + }, + }, + { + 'model': 'data.linkcell', + 'fields': { + 'link_page': ['unknown'], + }, + }, + { + 'model': 'data.linkcell', + 'fields': { + 'link_page': ['slug'], + }, + }, + { + 'model': 'data.linkcell', + 'fields': { + 'link_page': ['foo/bar/slug'], + }, + }, + ] + snapshot1 = pagesnapshot_class.objects.create( + serialization={ + 'fields': { + 'parent': None, + }, + 'cells': link_cells + + [ + { + 'model': 'data.linklistcell', + 'fields': { + 'links': link_cells, + }, + }, + { + 'model': 'search.searchcell', + 'fields': { + '_search_services': { + 'options': { + 'foobar': { + 'target_page': None, + } + } + } + }, + }, + { + 'model': 'search.searchcell', + 'fields': { + '_search_services': { + 'options': { + 'foobar': { + 'target_page': str(old_uuid), + } + } + } + }, + }, + { + 'model': 'search.searchcell', + 'fields': { + '_search_services': { + 'options': { + 'foobar': { + 'target_page': str(page.uuid), # will not change + } + } + } + }, + }, + { + 'model': 'search.searchcell', + 'fields': { + '_search_services': { + 'options': { + 'foobar': { + 'target_page': 'unknown', + } + } + } + }, + }, + { + 'model': 'search.searchcell', + 'fields': { + '_search_services': { + 'options': { + 'foobar': { + 'target_page': 'slug', + } + } + } + }, + }, + { + 'model': 'search.searchcell', + 'fields': { + '_search_services': { + 'options': { + 'foobar': { + 'target_page': 'foo/bar/slug', + } + } + } + }, + }, + { + 'model': 'wcs.wcscardcell', + 'fields': { + 'custom_schema': { + 'cells': [ + { + 'page': None, + }, + { + 'page': str(old_uuid), + }, + { + 'page': str(page.uuid), # will not change + }, + { + 'page': 'unknown', + }, + { + 'page': 'slug', + }, + { + 'page': 'foo/bar/slug', + }, + ], + }, + }, + }, + ], + } + ) + snapshot2 = pagesnapshot_class.objects.create( + serialization={ + 'fields': { + 'parent': [str(old_uuid)], + } + } + ) + snapshot3 = pagesnapshot_class.objects.create( + serialization={ + 'fields': { + 'parent': [str(page.uuid)], # will not change + } + } + ) + snapshot4 = pagesnapshot_class.objects.create( + serialization={ + 'fields': { + 'parent': ['unknown'], + } + } + ) + snapshot5 = pagesnapshot_class.objects.create( + serialization={ + 'fields': { + 'parent': ['slug'], + } + } + ) + snapshot6 = pagesnapshot_class.objects.create( + serialization={ + 'fields': { + 'parent': ['foo/bar/slug'], + } + } + ) + snapshot7 = pagesnapshot_class.objects.create( + serialization={ + 'fields': { + 'parent': [''], # index ! + } + } + ) + + executor = MigrationExecutor(connection) + executor.migrate(migrate_to) + executor.loader.build_graph() + + apps = executor.loader.project_state(migrate_to).apps + pagesnapshot_class = apps.get_model('data', 'PageSnapshot') + + snapshot1 = pagesnapshot_class.objects.get(pk=snapshot1.pk) + assert snapshot1.serialization['fields']['parent'] is None + new_link_cells = [ + { + 'model': 'data.linkcell', + 'fields': { + 'link_page': None, + }, + }, + { + 'model': 'data.linkcell', + 'fields': { + 'link_page': None, + }, + }, + { + 'model': 'data.linkcell', + 'fields': { + 'link_page': [str(page.uuid)], + }, + }, + { + 'model': 'data.linkcell', + 'fields': { + 'link_page': None, + }, + }, + { + 'model': 'data.linkcell', + 'fields': { + 'link_page': [str(page.uuid)], + }, + }, + { + 'model': 'data.linkcell', + 'fields': { + 'link_page': [str(page.uuid)], + }, + }, + ] + assert snapshot1.serialization['cells'] == new_link_cells + [ + { + 'model': 'data.linklistcell', + 'fields': { + 'links': new_link_cells, + }, + }, + { + 'model': 'search.searchcell', + 'fields': { + '_search_services': { + 'options': { + 'foobar': { + 'target_page': None, + } + } + } + }, + }, + { + 'model': 'search.searchcell', + 'fields': { + '_search_services': { + 'options': { + 'foobar': { + 'target_page': None, + } + } + } + }, + }, + { + 'model': 'search.searchcell', + 'fields': { + '_search_services': { + 'options': { + 'foobar': { + 'target_page': str(page.uuid), + } + } + } + }, + }, + { + 'model': 'search.searchcell', + 'fields': { + '_search_services': { + 'options': { + 'foobar': { + 'target_page': None, + } + } + } + }, + }, + { + 'model': 'search.searchcell', + 'fields': { + '_search_services': { + 'options': { + 'foobar': { + 'target_page': str(page.uuid), + } + } + } + }, + }, + { + 'model': 'search.searchcell', + 'fields': { + '_search_services': { + 'options': { + 'foobar': { + 'target_page': str(page.uuid), + } + } + } + }, + }, + { + 'model': 'wcs.wcscardcell', + 'fields': { + 'custom_schema': { + 'cells': [ + { + 'page': None, + }, + { + 'page': None, + }, + { + 'page': str(page.uuid), + }, + { + 'page': None, + }, + { + 'page': str(page.uuid), + }, + { + 'page': str(page.uuid), + }, + ], + }, + }, + }, + ] + snapshot2 = pagesnapshot_class.objects.get(pk=snapshot2.pk) + assert snapshot2.serialization['fields']['parent'] is None + snapshot3 = pagesnapshot_class.objects.get(pk=snapshot3.pk) + assert snapshot3.serialization['fields']['parent'] == [str(page.uuid)] + snapshot4 = pagesnapshot_class.objects.get(pk=snapshot4.pk) + assert snapshot4.serialization['fields']['parent'] is None + snapshot5 = pagesnapshot_class.objects.get(pk=snapshot5.pk) + assert snapshot5.serialization['fields']['parent'] == [str(page.uuid)] + snapshot6 = pagesnapshot_class.objects.get(pk=snapshot6.pk) + assert snapshot6.serialization['fields']['parent'] == [str(page.uuid)] + snapshot7 = pagesnapshot_class.objects.get(pk=snapshot7.pk) + assert snapshot7.serialization['fields']['parent'] == [str(root.uuid)] diff --git a/tests/test_search.py b/tests/test_search.py index 80847f56..d6f911a1 100644 --- a/tests/test_search.py +++ b/tests/test_search.py @@ -2,6 +2,7 @@ import datetime import json import os import sys +import uuid from io import StringIO from unittest import mock @@ -1689,12 +1690,9 @@ def test_test_export_import_search_cell_with_target_page(): site_export = get_output_of_command('export_site') site_data = json.loads(site_export) assert len(site_data['pages']) == 3 - assert ( - site_data['pages'][-1]['cells'][0]['fields']['_search_services']['options'][ - 'cards:c21f969b:card-bar' - ]['target_page'] - == 'root/card' - ) + assert site_data['pages'][-1]['cells'][0]['fields']['_search_services']['options'][ + 'cards:c21f969b:card-bar' + ]['target_page'] == str(card_page.uuid) import_site(data={}, clean=True) assert Page.objects.all().count() == 0 @@ -1717,8 +1715,8 @@ def test_test_export_import_search_cell_with_target_page(): site_data['pages'][-1]['cells'][0]['fields']['_search_services']['options']['cards:c21f969b:card-bar'][ 'target_page' - ] = 'unknown' + ] = str(uuid.uuid4()) import_site(data=site_data, clean=True) new_card_page = Page.objects.get(slug='card') new_cell = SearchCell.objects.get() - assert new_cell._search_services['options']['cards:c21f969b:card-bar']['target_page'] == '' + assert new_cell._search_services['options']['cards:c21f969b:card-bar']['target_page'] is None diff --git a/tests/wcs/test_card.py b/tests/wcs/test_card.py index 87966307..19041f47 100644 --- a/tests/wcs/test_card.py +++ b/tests/wcs/test_card.py @@ -17,7 +17,7 @@ from requests.models import Response from combo.apps.wcs.forms import WcsCardCellDisplayForm from combo.apps.wcs.models import WcsCardCell -from combo.data.models import Page, PageSnapshot, TextCell, ValidityInfo +from combo.data.models import Page, TextCell, ValidityInfo from combo.data.utils import import_site from tests.test_manager import login from tests.utils import manager_submit_cell @@ -3264,90 +3264,6 @@ def test_card_cell_assets(mock_send, settings, app, admin_user): assert 'Logo — %s' % cell2.get_label_for_asset() in resp.text -def test_page_snapshot_with_old_card_cells(): - page = Page.objects.create(title='xxx', slug='test_snapshots', template_name='standard') - PageSnapshot.take(page) - snapshot = page.pagesnapshot_set.first() - snapshot.serialization['cells'].append( - { - 'model': 'wcs.wcscardinfoscell', - 'fields': { - 'slug': 'my-card', - 'limit': 42, - 'order': 1, - 'groups': [], - 'public': True, - 'card_ids': '42,35', - 'condition': 'my-condition', - 'title_type': 'manual', - 'placeholder': 'content', - 'custom_title': 'my-title', - 'display_mode': 'card', - 'without_user': True, - 'custom_schema': {}, - 'only_for_user': True, - 'template_name': None, - 'extra_css_class': '', - 'carddef_reference': 'default:card_model_1', - 'related_card_path': '', - 'last_update_timestamp': '2022-08-11T13:57:43.362Z', - 'restricted_to_unlogged': False, - 'page': page.pk, - }, - } - ) - snapshot.serialization['cells'].append( - { - 'model': 'wcs.wcscardscell', - 'fields': { - 'slug': 'my-other-card', - 'limit': 35, - 'order': 2, - 'groups': [], - 'public': True, - 'condition': '', - 'placeholder': 'content', - 'custom_title': 'my-other-title', - 'without_user': False, - 'only_for_user': False, - 'template_name': None, - 'extra_css_class': '', - 'carddef_reference': 'default:card_model_1', - 'last_update_timestamp': '2022-08-12T07:19:18.541Z', - 'restricted_to_unlogged': False, - 'page': page.pk, - }, - } - ) - old_page = snapshot.get_page() - cell1 = old_page.get_cells()[0] - assert isinstance(cell1, WcsCardCell) - assert cell1.slug == 'my-card' - assert cell1.limit == 42 - assert cell1.card_ids == '42,35' - assert cell1.title_type == 'manual' - assert cell1.custom_title == 'my-title' - assert cell1.display_mode == 'card' - assert cell1.without_user is True - assert cell1.custom_schema == {} - assert cell1.only_for_user is True - assert cell1.carddef_reference == 'default:card_model_1' - assert cell1.related_card_path == '' - cell2 = old_page.get_cells()[1] - assert isinstance(cell1, WcsCardCell) - assert cell2.slug == 'my-other-card' - assert cell2.limit == 35 - assert cell2.card_ids == '' - assert cell2.title_type == 'manual' - assert cell2.custom_title == 'my-other-title' - assert cell2.display_mode == 'table' - assert cell2.without_user is False - assert cell2.custom_schema == {} - assert cell2.only_for_user is False - assert cell2.carddef_reference == 'default:card_model_1' - assert cell2.related_card_path == '__all__' - - def get_output_of_command(command, *args, **kwargs): old_stdout = sys.stdout output = sys.stdout = StringIO() @@ -3387,7 +3303,9 @@ def test_export_import_card_cell_with_page_link(): site_export = get_output_of_command('export_site') site_data = json.loads(site_export) assert len(site_data['pages']) == 3 - assert site_data['pages'][-1]['cells'][0]['fields']['custom_schema']['cells'][0]['page'] == 'root/card' + assert site_data['pages'][-1]['cells'][0]['fields']['custom_schema']['cells'][0]['page'] == str( + card_page.uuid + ) import_site(data={}, clean=True) assert Page.objects.all().count() == 0