Skip to content

Commit

Permalink
Add slug and locale to orderedcontentset
Browse files Browse the repository at this point in the history
  • Loading branch information
Gerrit Vermeulen committed Nov 21, 2024
1 parent c3b8b9f commit f0e0920
Show file tree
Hide file tree
Showing 19 changed files with 453 additions and 93 deletions.
6 changes: 6 additions & 0 deletions home/export_ordered_sets.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ class ExportRow:
unit: str | None
before_or_after: str | None
contact_field: str | None
slug: str | None
locale: str | None

@classmethod
def headings(cls) -> list[str]:
Expand Down Expand Up @@ -57,6 +59,8 @@ def perform_export(self) -> Iterable[ExportRow]:
unit=item.unit,
before_or_after=str(item.before_or_after),
contact_field=item.contact_field,
slug=item.slug,
locale=item.locale.language_code,
)


Expand Down Expand Up @@ -100,6 +104,8 @@ def _set_xlsx_styles(wb: Workbook, sheet: Worksheet) -> None:
"unit": 110,
"before_or_after": 120,
"contact_field": 100,
"slug": 100,
"locale": 100,
}
for column in sheet.iter_cols(max_row=1):
[cell] = column
Expand Down
35 changes: 35 additions & 0 deletions home/import_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
StructValue, # type: ignore
)
from wagtail.blocks.list_block import ListValue # type: ignore
from wagtail.coreutils import get_content_languages # type: ignore
from wagtail.models import Locale # type: ignore
from wagtail.rich_text import RichText # type: ignore
from wagtail.test.utils.form_data import nested_form_data, streamfield # type: ignore
Expand All @@ -38,6 +39,40 @@ def __init__(
super().__init__()


class LocaleHelper:
"""
Helper class to map language names to locales
"""

def __init__(self):
self.locale_map: dict[str, Locale] = {}

def locale_from_display_name(self, langname: str) -> Locale:
"""
Get a locale from its display name.
If the locale for the given language name does not exist, an exception is raised.
:param langname: The name of the language.
:return: The locale for the given language.
:raises ImportException: If the language name is not found or has multiple codes.
"""

if langname not in self.locale_map:
codes = []
for lang_code, lang_dn in get_content_languages().items():
if lang_dn == langname:
codes.append(lang_code)
if not codes:
raise ImportException(f"Language not found: {langname}")
if len(codes) > 1:
raise ImportException(
f"Multiple codes for language: {langname} -> {codes}"
)
self.locale_map[langname] = Locale.objects.get(language_code=codes[0])
return self.locale_map[langname]


def wagtail_to_formdata(val: Any) -> Any:
"""
Convert a model dict field that may be a nested streamfield (or associated
Expand Down
22 changes: 16 additions & 6 deletions home/import_ordered_content_sets.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from django.core.files.base import File # type: ignore
from openpyxl import load_workbook

from home.import_helpers import ImportException
from home.import_helpers import ImportException, LocaleHelper
from home.models import ContentPage, OrderedContentSet

logger = getLogger(__name__)
Expand Down Expand Up @@ -40,20 +40,25 @@ def __init__(
self.file = file
self.filetype = file_type
self.progress_queue = progress_queue
self.locale_helper = LocaleHelper()

def _get_or_init_ordered_content_set(
self, row: dict[str, str], set_name: str
self, row: dict[str, str], set_slug: str, set_locale: str
) -> OrderedContentSet:
"""
Get or initialize an instance of OrderedContentSet from a row of a CSV file.
:param row: The row of the CSV file, as a dict.
:param set_name: The name of the ordered content set.
:param set_slug: The slug of the ordered content set.
:param set_locale: The locale of the ordered content set.
:return: An instance of OrderedContentSet.
"""
ordered_set = OrderedContentSet.objects.filter(name=set_name).first()
locale = self.locale_helper.locale_from_display_name(set_locale)
ordered_set = OrderedContentSet.objects.filter(
slug=set_slug, locale=locale
).first()
if not ordered_set:
ordered_set = OrderedContentSet(name=set_name)
ordered_set = OrderedContentSet(slug=set_slug, locale=locale)

return ordered_set

Expand Down Expand Up @@ -155,7 +160,10 @@ def _create_ordered_set_from_row(
:return: An instance of OrderedContentSet.
:raises ImportException: If time, units, before_or_afters, page_slugs and contact_fields are not all equal length.
"""
ordered_set = self._get_or_init_ordered_content_set(row, row["Name"])
ordered_set = self._get_or_init_ordered_content_set(
row, row["Slug"], row["Locale"]
)
ordered_set.name = row["Name"]
self._add_profile_fields(ordered_set, row)

pages = self._extract_ordered_content_set_pages(row, index)
Expand Down Expand Up @@ -187,6 +195,8 @@ def _get_xlsx_rows(self, file: File) -> list[dict[str, str]]:
"Unit": str(row[4]),
"Before Or After": str(row[5]),
"Contact Field": str(row[6]),
"Slug": str(row[7]),
"Locale": str(row[8]),
}
lines.append(row_dict)
return lines
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Generated by Django 4.2.11 on 2024-11-19 13:19

from django.db import migrations, models
from django.db.models import Count
import django.db.models.deletion


def rename_duplicate_slugs(OrderedContentSet):
duplicate_slugs = (
OrderedContentSet.objects.values("slug")
.annotate(count=Count("slug"))
.order_by("-count")
.filter(count__gt=1)
.values_list("slug", flat=True)
)
for slug in duplicate_slugs:
pages = OrderedContentSet.objects.filter(slug=slug)
while pages.count() > 1:
page = pages.first()

suffix = 1
candidate_slug = slug
while OrderedContentSet.objects.filter(slug=candidate_slug).exists():
suffix += 1
candidate_slug = f"{slug}-{suffix}"

page.slug = candidate_slug
page.save(update_fields=["slug"])


def set_locale_from_instance(OrderedContentSet, Site):
for ocs in OrderedContentSet.objects.all():
if ocs.pages:
# Get the first page's data
first_page_data = ocs.pages[0]
contentpage = first_page_data.value.get("contentpage")
ocs.locale = contentpage.locale
else:
site = Site.objects.get(is_default_site=True)
ocs.locale = site.root_page.locale
ocs.save(update_fields=["locale"])


def run_migration(apps, schema_editor):
OrderedContentSet = apps.get_model("home", "OrderedContentSet")
Site = apps.get_model("wagtailcore", "Site")
rename_duplicate_slugs(OrderedContentSet)
set_locale_from_instance(OrderedContentSet, Site)


class Migration(migrations.Migration):

dependencies = [
("wagtailcore", "0089_log_entry_data_json_null_to_object"),
("home", "0084_alter_contentpage_whatsapp_body"),
]

operations = [
migrations.AddField(
model_name="orderedcontentset",
name="locale",
field=models.ForeignKey(
default="",
on_delete=django.db.models.deletion.CASCADE,
to="wagtailcore.locale",
),
),
migrations.AddField(
model_name="orderedcontentset",
name="slug",
field=models.SlugField(
default="",
help_text="A unique identifier for this ordered content set",
max_length=255,
),
),
migrations.RunPython(
code=run_migration, reverse_code=migrations.RunPython.noop
),
]
26 changes: 18 additions & 8 deletions home/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,27 +63,27 @@ class UniqueSlugMixin:
Ensures that slugs are unique per locale
"""

def is_slug_available(self, slug):
pages = Page.objects.filter(
def is_slug_available(self, slug, PO=Page):
pages = PO.objects.filter(
locale=self.locale_id or self.get_default_locale(), slug=slug
)
if self.pk is not None:
pages = pages.exclude(pk=self.pk)
return not pages.exists()

def get_unique_slug(self, slug):
def get_unique_slug(self, slug, PO):
suffix = 1
candidate_slug = slug
while not self.is_slug_available(candidate_slug):
while not self.is_slug_available(candidate_slug, PO):
suffix += 1
candidate_slug = f"{slug}-{suffix}"
return candidate_slug

def clean(self):
def clean(self, PO=Page):
super().clean()

if not self.is_slug_available(self.slug):
page = Page.objects.get(locale=self.locale, slug=self.slug)
if not self.is_slug_available(self.slug, PO):
page = PO.objects.get(locale=self.locale, slug=self.slug)
raise ValidationError(
{
"slug": ValidationError(
Expand Down Expand Up @@ -931,7 +931,7 @@ def save_revision(
return revision

def clean(self):
result = super().clean()
result = super().clean(Page)
errors = {}

# The WA title is needed for all templates to generate a name for the template
Expand Down Expand Up @@ -1007,13 +1007,20 @@ def clean(self):


class OrderedContentSet(
UniqueSlugMixin,
WorkflowMixin,
DraftStateMixin,
LockableMixin,
RevisionMixin,
index.Indexed,
models.Model,
):
slug = models.SlugField(
max_length=255,
help_text="A unique identifier for this ordered content set",
default="",
)
locale = models.ForeignKey(to=Locale, on_delete=models.CASCADE, default="")
revisions = GenericRelation(
"wagtailcore.Revision", related_query_name="orderedcontentset"
)
Expand Down Expand Up @@ -1219,6 +1226,9 @@ class Meta: # noqa
verbose_name = "Ordered Content Set"
verbose_name_plural = "Ordered Content Sets"

def clean(self):
return super().clean(OrderedContentSet)


class ContentPageRating(models.Model):
timestamp = models.DateTimeField(auto_now_add=True)
Expand Down
4 changes: 2 additions & 2 deletions home/tests/import-export-data/ordered_content.csv
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
Name,Profile Fields,Page Slugs,Time,Unit,Before Or After,Contact Field
Test Set,"gender:male, relationship:in_a_relationship","first_time_user, first_time_user, first_time_user","2, 3, 4","days, months, years","before, before, after",edd
Name,Profile Fields,Page Slugs,Time,Unit,Before Or After,Contact Field,Slug,Locale
Test Set,"gender:male, relationship:in_a_relationship","first_time_user, first_time_user, first_time_user","2, 3, 4","days, months, years","before, before, after",edd,test_set,English
Binary file modified home/tests/import-export-data/ordered_content.xlsx
Binary file not shown.
4 changes: 2 additions & 2 deletions home/tests/import-export-data/ordered_content_broken.csv
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
Name,Profile Fields,Page Slugs,Time,Unit,Before Or After,Contact Field
Test Set,"gender:male, relationship:in_a_relationship","first_time_user, first_time_user, first_time_user","2, 4","days, years","before, before, after","edd, name, age"
Name,Profile Fields,Page Slugs,Time,Unit,Before Or After,Contact Field,Slug,Locale
Test Set,"gender:male, relationship:in_a_relationship","first_time_user, first_time_user, first_time_user","2, 4","days, years","before, before, after","edd, name, age",test_set,English
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
Name,Profile Fields,Page Slugs,Time,Unit,Before Or After,Contact Field
Test Set,"gender:male, relationship:in_a_relationship","first_time_user, first_time_user, first_time_user","2, 3, 4","days, months, years","before, before, after","edd, name, age"
Name,Profile Fields,Page Slugs,Time,Unit,Before Or After,Contact Field,Slug,Locale
Test Set,"gender:male, relationship:in_a_relationship","first_time_user, first_time_user, first_time_user","2, 3, 4","days, months, years","before, before, after","edd, name, age",test_set,English
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
Name,Profile Fields,Page Slugs,Time,Unit,Before Or After,Contact Field
Test Set,,first_time_user,2,days,before,edd
Name,Profile Fields,Page Slugs,Time,Unit,Before Or After,Contact Field,Slug,Locale
Test Set,,first_time_user,2,days,before,edd,test_set,English
Loading

0 comments on commit f0e0920

Please sign in to comment.