Skip to content

Commit

Permalink
Merge pull request #386 from praekeltfoundation/add-slugs-locale-orde…
Browse files Browse the repository at this point in the history
…redset

Add slug and locale to orderedcontentset
  • Loading branch information
HawkiesZA authored Nov 26, 2024
2 parents 0966897 + 84c05c9 commit 5afc97b
Show file tree
Hide file tree
Showing 23 changed files with 556 additions and 118 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Filter ordered content sets by profile field values.
- Add list_button_title to Whatsapp messages.
- Add ability to specify actions to Whatsapp list item messages.
- Add slug and locale to OrderedContentSets.
### Removed
- Removed word embeddings search (`s` query parameter in API)
Expand Down
46 changes: 26 additions & 20 deletions home/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from wagtail.api.v2.views import BaseAPIViewSet, PagesAPIViewSet
from wagtail.documents.api.v2.views import DocumentsAPIViewSet
from wagtail.images.api.v2.views import ImagesAPIViewSet
from wagtail.models import Locale
from wagtailmedia.api.views import MediaAPIViewSet

from .models import Assessment, AssessmentTag, OrderedContentSet
Expand Down Expand Up @@ -134,9 +135,11 @@ class OrderedContentSetViewSet(BaseAPIViewSet):
"name",
"profile_fields",
"pages",
"locale",
"slug",
]
known_query_parameters = BaseAPIViewSet.known_query_parameters.union(
["page", "qa", "gender", "age", "relationship"]
["page", "qa", "gender", "age", "relationship", "slug"]
)
pagination_class = PageNumberPagination
search_fields = ["name", "profile_fields"]
Expand Down Expand Up @@ -202,38 +205,41 @@ def get_queryset(self):
gender = self.request.query_params.get("gender", "")
age = self.request.query_params.get("age", "")
relationship = self.request.query_params.get("relationship", "")
slug = self.request.query_params.get("slug", "")
locale = self.request.query_params.get("locale", "")

if qa:
# return the latest revision for each OrderedContentSet
queryset = OrderedContentSet.objects.all().order_by("latest_revision_id")

for ocs in queryset:
latest_revision = ocs.revisions.order_by("-created_at").first()
if latest_revision:
latest_revision = latest_revision.as_object()
ocs.name = latest_revision.name
ocs.pages = latest_revision.pages
ocs.profile_fields = latest_revision.profile_fields
ocs.locale = latest_revision.locale
ocs.slug = latest_revision.slug

else:
if gender or age or relationship:
# it looks like you can't use advanced queries to filter on StreamFields
# so we have to do it like this.``
queryset = OrderedContentSet.objects.filter(
live=True,
)
filter_ids = [
self._filter_queryset_by_profile_fields(
x, gender, age, relationship
)
for x in queryset
]
queryset = OrderedContentSet.objects.filter(id__in=filter_ids).order_by(
"last_published_at"
)
else:
queryset = OrderedContentSet.objects.filter(
live=True,
).order_by("last_published_at")
queryset = OrderedContentSet.objects.filter(
live=True,
).order_by("last_published_at")

if gender or age or relationship:
# it looks like you can't use advanced queries to filter on StreamFields
# so we have to do it like this.``
filter_ids = [
self._filter_queryset_by_profile_fields(x, gender, age, relationship)
for x in queryset
]
queryset = queryset.filter(id__in=filter_ids).order_by("last_published_at")
if slug:
queryset = queryset.filter(slug=slug)
if locale:
locale = Locale.objects.get(language_code=locale)
queryset = queryset.filter(locale=locale)
return queryset


Expand Down
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
25 changes: 18 additions & 7 deletions home/import_ordered_content_sets.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from django.core.files.base import File # type: ignore
from openpyxl import load_workbook
from wagtail.models import Locale # type: ignore

from home.import_helpers import ImportException
from home.models import ContentPage, OrderedContentSet
Expand Down Expand Up @@ -42,18 +43,22 @@ def __init__(
self.progress_queue = progress_queue

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 = Locale.objects.get(language_code=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 @@ -117,6 +122,7 @@ def _add_pages(
self,
ordered_set: OrderedContentSet,
pages: list[OrderedContentSetPage],
index: int,
) -> None:
"""
Given the extracted values from a row of the file, create the corresponding ordered content set pages.
Expand All @@ -141,7 +147,7 @@ def _add_pages(
ordered_set.pages.append(("pages", os_page))
else:
raise ImportException(
f"Content page not found for slug '{page.page_slug}'"
f"Content page not found for slug '{page.page_slug}'", index
)

def _create_ordered_set_from_row(
Expand All @@ -155,12 +161,15 @@ 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"].lower(), row["Locale"]
)
ordered_set.name = row["Name"]
self._add_profile_fields(ordered_set, row)

pages = self._extract_ordered_content_set_pages(row, index)

self._add_pages(ordered_set, pages)
self._add_pages(ordered_set, pages, index)

ordered_set.save()
return ordered_set
Expand All @@ -187,6 +196,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,85 @@
# Generated by Django 4.2.11 on 2024-11-25 12:05

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", "locale")
.annotate(count=Count("slug"))
.order_by("-count")
.filter(count__gt=1)
.values_list("slug", "locale")
)
for slug, locale in duplicate_slugs:
pages = OrderedContentSet.objects.filter(slug=slug, locale=locale)
while pages.count() > 1:
page = pages.first()

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

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


def set_locale_from_instance(OrderedContentSet, Site):
site = Site.objects.get(is_default_site=True)
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")
if contentpage:
ocs.locale = contentpage.locale
else:
ocs.locale = site.root_page.locale
else:
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
),
]
33 changes: 25 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 @@ -940,7 +940,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 @@ -1016,13 +1016,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 @@ -1209,12 +1216,16 @@ def status(self) -> str:
return status

panels = [
FieldPanel("slug"),
FieldPanel("locale"),
FieldPanel("name"),
FieldPanel("profile_fields"),
FieldPanel("pages"),
]

api_fields = [
APIField("slug"),
APIField("locale"),
APIField("name"),
APIField("profile_fields"),
APIField("pages"),
Expand All @@ -1228,6 +1239,12 @@ class Meta: # noqa
verbose_name = "Ordered Content Set"
verbose_name_plural = "Ordered Content Sets"

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

def language_code(self):
return self.locale.language_code


class ContentPageRating(models.Model):
timestamp = models.DateTimeField(auto_now_add=True)
Expand Down
13 changes: 13 additions & 0 deletions home/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -555,8 +555,21 @@ def to_representation(self, instance):
return text


class OrderedLocaleField(serializers.Field):
"""
Serializes the "locale" field.
"""

def get_attribute(self, instance):
return instance

def to_representation(self, instance):
return instance.locale.language_code


class OrderedContentSetSerializer(BaseSerializer):
name = NameField(read_only=True)
locale = OrderedLocaleField(read_only=True)
pages = OrderedPagesField(read_only=True)
profile_fields = ProfileFieldsField(read_only=True)

Expand Down
Loading

0 comments on commit 5afc97b

Please sign in to comment.