Skip to content

Commit 74b74b7

Browse files
authored
Merge pull request #23 from fsbraun/main
chore: Merge changes for versions 0.03 to 0.2 into original repo
2 parents 51a3a7b + 23be0c8 commit 74b74b7

File tree

12 files changed

+349
-22
lines changed

12 files changed

+349
-22
lines changed

.github/workflows/test.yml

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
name: migration test
2+
3+
on: [push, pull_request]
4+
5+
concurrency:
6+
group: ${{ github.workflow }}-${{ github.ref }}
7+
cancel-in-progress: true
8+
9+
jobs:
10+
migrate-project:
11+
runs-on: ${{ matrix.os }}
12+
strategy:
13+
fail-fast: false
14+
matrix:
15+
django-version: [
16+
'4.2',
17+
]
18+
python-version: ['3.11']
19+
os: [
20+
ubuntu-latest,
21+
]
22+
23+
steps:
24+
- uses: actions/checkout@v4
25+
- name: Set up Python ${{ matrix.python-version }}
26+
uses: actions/setup-python@v4
27+
with:
28+
python-version: ${{ matrix.python-version }}
29+
- name: Create dummy project
30+
run: |
31+
python -m venv .venv
32+
source ./.venv/bin/activate
33+
python -m pip install --upgrade pip
34+
pip install "djangocms-frontend[cms-3]<2" Django~=${{ matrix.django-version }}
35+
django-admin startproject --template https://github.com/django-cms/cms-template/archive/migration.zip cmsproject
36+
pip install -e .
37+
cd cmsproject
38+
python -m manage migrate
39+
python -m manage cms check
40+
python -m djangocms_4_migration.test
41+
- name: Upgrade to django CMS 4
42+
run: |
43+
source ./.venv/bin/activate
44+
cd cmsproject
45+
pip install -U "django-cms<5" djangocms-versioning djangocms-alias
46+
DJANGO_SETTINGS_MODULE="cmsproject.settings4" python -m manage cms4_migration
47+
- name: Run tests
48+
run: |
49+
source ./.venv/bin/activate
50+
cd cmsproject
51+
python -m djangocms_4_migration.test

CHANGELOG.md

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,28 @@
11
# Changelog
22

3-
## (unreleased)
3+
## 0.2.0 (unreleased)
44

5+
* feat: Migrate permissions by @fsbraun in https://github.com/fsbraun/djangocms-4-migration/pull/7
6+
* feat: ensure page urls are unique by @fsbraun in https://github.com/fsbraun/djangocms-4-migration/pull/8
7+
8+
## 0.1.0 (2025-03-14)
9+
10+
* feature: Update foreign key relations to page objects
11+
* feature: Update soft relation of djangocms-link 5+
12+
* feature: Update soft relation of djangocms-frontend objects
13+
* fix: Customer model username retrieval during migration
14+
* fix: Crashed when running w/o `CMS_MIGRATION_USER_ID` setting
15+
16+
## 0.0.3
17+
18+
* feature: Update foreign key relations to page objects
19+
* feature: Update soft relation of djangocms-link 5+
20+
* feature: Update soft relation of djangocms-frontend objects
21+
* fix: Customer model username retrieval during migration
522
* feature: Add support for custom user models
623

724
## 0.0.2 (2023-07-11)
825
* fix: Added the Github project url to the setup.py file for PyPi linking
926

1027
## 0.0.1 (2021-01-12)
11-
* Initial commit containing basic django CMS 3 page and content migration with django-cms-alias and djangocms-versioning integration.
28+
* Initial commit containing basic django CMS 3 page and content migration with django-cms-alias and djangocms-versioning integration.

README.md

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@
66
This package is designed to migrate a django CMS 3.5+ project to django CMS 4.0.
77

88
## What does this package do?
9-
- Keeps any draft and published content, ensuring that any new draft changes are kept as a new draft version in djangocms_versioning.
9+
- Keeps any draft and published content, ensuring that any new draft changes are kept as a new draft version in djangocms_versioning.
10+
- Creates aliases for static placeholder
11+
- Migrates alias plugins
12+
- Runs django CMS' migrations
1013

1114
## Limitations
1215
Due to the nature of the changes between django CMS 3.5+ and 4.0 the package will fail to function if an incompatible package is installed.
@@ -27,36 +30,48 @@ Requires knowledge of django CMS Versioning
2730
### Install the following packages
2831
The following packages are not yet officially released, they need to be installed directly from the repository. We need your help to make packages v4.0 compatible and to provide documentation for the wider community!
2932

30-
django CMS 4.0
33+
django CMS 4.0+
3134
```
32-
pip install http://github.com/divio/django-cms/tarball/release/4.0.x#egg=django-cms
35+
pip install django-cms
3336
```
3437

3538
djangocms-text-ckeditor
3639
```
37-
pip install https://github.com/divio/djangocms-text-ckeditor/tarball/support/4.0.x#egg=djangocms-text-ckeditor
40+
pip install djangocms-text-ckeditor
3841
```
3942

4043
djangocms-versioning
4144
```
42-
pip install https://github.com/divio/djangocms-versioning/tarball/master#egg=djangocms-versioning
45+
pip install djangocms-versioning
4346
```
4447

4548
djangocms-alias
4649
```
47-
pip install https://github.com/divio/djangocms-alias/tarball/master#egg=djangocms-alias
50+
pip install djangocms-alias
4851
```
4952

5053
## Installation
5154
**Warning**: This package can leave your DB in a corrupted state if used incorrectly, be sure to backup any databases prior to running any commands listed here!
5255

5356
First install this package in your project
5457
```
55-
pip install djangocms-4-migration
58+
pip install git+https://github.com/fsbraun/djangocms-4-migration
5659
```
5760

5861
## Configuration
5962

63+
Add the migration tool to `INSTALLED_APPS` temporarily. In your `settings.py` make sure that it is listed. You can remove it after the migration process.
64+
```
65+
INSTALLED_APPS = [
66+
...,
67+
"djangocms_4_migration",
68+
"djangocms_versioning",
69+
"djangocms_alias",
70+
...,
71+
]
72+
CMS_CONFIRM_VERSION4 = True
73+
```
74+
6075
If you have a custom user model, you should designate a "migration user" by specifying the user ID in your settings like so:
6176

6277
```
@@ -70,6 +85,13 @@ Simply run the following command to run the data migration.
7085
python manage.py cms4_migration
7186
```
7287

88+
You can ignore warnings of the form
89+
```
90+
UserWarning: No user has been supplied when creating a new AliasContent object.
91+
No version could be created. Make sure that the creating code also creates a
92+
Version objects or use AliasContent.objects.with_user(user).create(...)
93+
```
94+
7395
## Common solutions for django CMS 4.0 compatibility
7496

7597
Import PageContent in a backwards compatible way (Title).

djangocms_4_migration/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "0.0.2"
1+
__version__ = "0.2.0"

djangocms_4_migration/helpers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ def get_or_create_migration_user(user_model=get_user_model()):
1414
This is the user that is used to automatically attach to new items created as
1515
part of the cms migration.
1616
"""
17-
if getattr(settings, "CMS_MIGRATION_USER_ID"):
17+
if getattr(settings, "CMS_MIGRATION_USER_ID", None):
1818
return user_model.objects.get(id=settings.CMS_MIGRATION_USER_ID), False
1919
return user_model.objects.get_or_create(
2020
username='djangocms_4_migration_user',

djangocms_4_migration/management/commands/migrate_alias_plugins.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,6 @@ def process_old_alias_sources(site, language, site_plugin_queryset):
210210
# Create version
211211
changed_by = User.objects.get(**{User.USERNAME_FIELD: old_plugin.placeholder.source.changed_by})
212212
version = Version.objects.create(content=alias_content, created_by=changed_by)
213-
version.save()
214213
version.publish(changed_by)
215214

216215
# create csm4 alias plugins for cms3 alias references
@@ -254,8 +253,7 @@ class Command(BaseCommand):
254253
def handle(self, *args, **options):
255254
with transaction.atomic():
256255
# Alias source plugin list
257-
cms3_alias_ref_ids = AliasPluginModel.objects.values('plugin_id').order_by('plugin_id').distinct('plugin_id')
258-
plugin_id_list = [cms3_plugin['plugin_id'] for cms3_plugin in cms3_alias_ref_ids if cms3_plugin['plugin_id']]
256+
plugin_id_list = list(AliasPluginModel.objects.values_list('plugin_id', flat=True).order_by('plugin_id'))
259257
alias_source_total = len(plugin_id_list)
260258
# Alias references list count
261259
alias_reference_total = AliasPluginModel.objects.count()

djangocms_4_migration/management/commands/migrate_static_placeholders.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ def _get_or_create_alias(category, static_code, site):
124124

125125

126126
def _create_alias_content(alias, name, language, user, state=PUBLISHED):
127-
alias_content = AliasContent.objects.create(
127+
alias_content = AliasContent.objects.with_user(user).create(
128128
alias=alias,
129129
name=name,
130130
language=language,

djangocms_4_migration/management/commands/migration_cleanup.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import logging
2+
from packaging.version import Version as PgkVersion
23

4+
from django.conf import settings
35
from django.core.management.base import BaseCommand
46
from django.contrib.contenttypes.models import ContentType
57
from django.db import connection
@@ -17,6 +19,84 @@
1719
logger = logging.getLogger(__name__)
1820

1921

22+
def _fix_link_plugins(page):
23+
"""djangocms-link with version above 5.0.0 stores only "soft" references to pages in JSON fields which will not be
24+
updated by `_fix_page_refernces`"""
25+
if "djangocms_link" in settings.INSTALLED_APPS:
26+
from djangocms_link import __version__
27+
from djangocms_link.models import Link
28+
29+
if PgkVersion(__version__) >= PgkVersion("5.0.0"):
30+
for link in Link.objects.all():
31+
if "internal_link" in link.link and link.link["internal_link"].startswith("cms.page:"):
32+
_, linked_page_id = link.link["internal_link"].split(":")
33+
if linked_page_id == str(page.pk):
34+
replacement_page = Page.objects.filter(node_id=page.node_id).exclude(id=page.id).get()
35+
logger.info("Fixing link reference from Page %s to %s", page.id, replacement_page.id)
36+
link.link["internal_link"] = f"cms.page:{replacement_page.pk}"
37+
link.save()
38+
39+
def _fix_frontend_refernces(page):
40+
"""djangocms-frontend stores only "soft" references to pages in JSON fields which will not be
41+
updated by `_fix_page_refernces`"""
42+
def search(json, reference, pk):
43+
changed = False
44+
for key, value in json.items():
45+
if key == "internal_link" and value.startswith(reference + ":"):
46+
# Update link field
47+
_, linked_page_id = value.split(":")
48+
if linked_page_id == str(pk):
49+
replacement_page = Page.objects.filter(node_id=page.node_id).exclude(id=page.id).get()
50+
json[key] = f"{reference}:{replacement_page.pk}"
51+
changed = True
52+
elif isinstance(value, dict) and "model" in value and value["model"] == reference and "pk" in value:
53+
# Update reference
54+
if value["pk"] == pk:
55+
replacement_page = Page.objects.filter(node_id=page.node_id).exclude(id=page.id).get()
56+
value["pk"] = replacement_page.pk
57+
changed = True
58+
elif isinstance(value, dict):
59+
# search recursively
60+
changed = changed or search(value, reference, pk)
61+
return changed
62+
63+
if "djangocms_frontend" in settings.INSTALLED_APPS:
64+
from djangocms_frontend.models import FrontendUIItem
65+
66+
for frontend_component in FrontendUIItem.objects.all():
67+
if search(frontend_component.config, "cms.page", page.pk):
68+
frontend_component.save()
69+
70+
71+
def _fix_page_references(page):
72+
relations = [
73+
f
74+
for f in Page._meta.get_fields()
75+
if (f.one_to_many or f.one_to_one or f.many_to_many)
76+
and f.auto_created
77+
and not f.concrete
78+
]
79+
80+
replacement_page = Page.objects.filter(node_id=page.node_id).exclude(id=page.id).get()
81+
logger.info("Fixing reference from Page %s to %s", page.id, replacement_page.id)
82+
83+
for rel in relations:
84+
model = rel.related_model
85+
if rel.one_to_one:
86+
# One to one relationships should not be duplicated, so just delete object
87+
model.objects.filter(**{rel.field.name: page}).delete()
88+
elif rel.many_to_many:
89+
m2m_objs = model.objects.filter(**{rel.field.name: page})
90+
for m2m_obj in m2m_objs:
91+
m2m_rel = getattr(m2m_obj, rel.field.name)
92+
m2m_rel.remove(page)
93+
m2m_rel.add(replacement_page)
94+
else:
95+
model.objects.filter(**{rel.field.name: page}).update(
96+
**{rel.field.name: replacement_page}
97+
)
98+
99+
20100
def _delete_page(page):
21101
try:
22102
logger.info("Deleting Page %s" % page.id)
@@ -76,13 +156,17 @@ def handle(self, *args, **options):
76156
page_content_list = _get_page_contents(page)
77157

78158
if not page_content_list.exists():
159+
_fix_page_references(page)
160+
_fix_link_plugins(page)
161+
_fix_frontend_refernces(page)
79162
_delete_page(page)
80163
stats['page_deleted'] = stats['page_deleted'] + 1
81164
continue
82165

83166
stats['pagecontents_count'] = stats['pagecontents_count'] + page_content_list.count()
84167

85168
# Find if each PageContents has versions attached.
169+
languages = []
86170
for page_content in page_content_list:
87171
# If there are no versions for the pagecontents clean them out as they are not required
88172
if not Version.objects.filter(
@@ -92,5 +176,12 @@ def handle(self, *args, **options):
92176
_delete_page_content_placeholders(page_content_contenttype, page_content)
93177
_delete_page_content(page_content)
94178
stats['pagecontents_deleted'] = stats['pagecontents_deleted'] + 1
179+
else:
180+
languages.append(page_content.language)
181+
182+
for language in languages:
183+
# Delete redundant page urls (the first is the published one - keep it)
184+
for url in page.urls.filter(language=language).order_by("pk")[1:]:
185+
url.delete()
95186

96187
logger.info("Stats: %s", str(stats))

djangocms_4_migration/migrations/0003_page_version_integration_data_migration.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import logging
2-
import os
32

43
from django.conf import settings
4+
from django.contrib.auth import get_user_model
55
from django.db import migrations
66
from djangocms_versioning.constants import ARCHIVED, DRAFT, PUBLISHED
77

@@ -15,6 +15,8 @@
1515
# Page was marked published, but some of page parents are not.
1616
CMS_3_PUBLISHER_STATE_PENDING = 4
1717

18+
USERNAME_FIELD = getattr(get_user_model(), "USERNAME_FIELD", "username")
19+
1820

1921
def forwards(apps, schema_editor):
2022
db_alias = schema_editor.connection.alias
@@ -64,7 +66,7 @@ def _create_version(page_content, state=DRAFT, number=1):
6466
# Find the user
6567
try:
6668
created_by = User.objects.using(db_alias).get(
67-
**{User.USERNAME_FIELD: page_content.page.created_by}
69+
**{USERNAME_FIELD: page_content.page.created_by}
6870
)
6971
except:
7072
# Use the first super user as the author as a fall back
@@ -88,7 +90,7 @@ def _create_version(page_content, state=DRAFT, number=1):
8890
for existing_title in PageData.objects.using(db_alias).all():
8991
"""
9092
If Title was published keep it and create a version
91-
If Title was not published
93+
If Title was not published
9294
"""
9395
logger.info("Existing title: {}".format(str(existing_title.title_id)))
9496

@@ -99,7 +101,6 @@ def _create_version(page_content, state=DRAFT, number=1):
99101
# If this is the draft page
100102
if existing_title.publisher_is_draft:
101103
_handle_draft_page(existing_title)
102-
103104
# Otherwise this is the published page
104105
else:
105106
_handle_public_page(existing_title)

0 commit comments

Comments
 (0)