Skip to content

Commit

Permalink
Merge branch 'feature/1301-multiple-program-types' of github.com:torc…
Browse files Browse the repository at this point in the history
…hbox/rca-wagtail-2019 into dev
Patrick Gan committed Jan 19, 2025
2 parents b4df74a + 52baff0 commit e12b856
Showing 18 changed files with 313 additions and 45 deletions.
12 changes: 10 additions & 2 deletions rca/enquire_to_study/tests/test_forms.py
Original file line number Diff line number Diff line change
@@ -10,13 +10,21 @@
EnquiryFormSubmission,
EnquiryFormSubmissionProgrammesOrderable,
)
from rca.programmes.factories import ProgrammePageFactory
from rca.programmes.factories import (
ProgrammePageFactory,
ProgrammePageProgrammeTypeFactory,
ProgrammeTypeFactory,
)


class TestEnquireToStudyForm(TestCase):
def setUp(self):
self.start_date = StartDateFactory(qs_code="test-code")
self.enquiry_reason = EnquiryReasonFactory()
page = ProgrammePageFactory(qs_code=1)
ProgrammePageProgrammeTypeFactory(
page=page, programme_type=ProgrammeTypeFactory()
)
self.form_data = {
"first_name": "Monty",
"last_name": "python",
@@ -25,7 +33,7 @@ def setUp(self):
"country_of_residence": "GB",
"city": "Bristol",
"country_of_citizenship": "GB",
"programmes": [ProgrammePageFactory(qs_code=1, programme_type__pk=2).pk],
"programmes": [page.pk],
"start_date": self.start_date.pk,
"enquiry_reason": self.enquiry_reason.pk,
"enquiry_questions": "What is your name?",
12 changes: 10 additions & 2 deletions rca/enquire_to_study/tests/test_views.py
Original file line number Diff line number Diff line change
@@ -22,7 +22,11 @@
from rca.enquire_to_study.models import EnquireToStudySettings, EnquiryFormSubmission
from rca.enquire_to_study.views import EnquireToStudyFormView
from rca.enquire_to_study.wagtail_hooks import EnquiryFormSubmissionAdmin
from rca.programmes.factories import ProgrammePageFactory
from rca.programmes.factories import (
ProgrammePageFactory,
ProgrammePageProgrammeTypeFactory,
ProgrammeTypeFactory,
)


class EnquireToStudyFormViewTest(TestCase):
@@ -54,6 +58,10 @@ def setUp(self):
email_content="Test email content",
site_id=Site.objects.get().pk,
)
page = ProgrammePageFactory(qs_code=1)
ProgrammePageProgrammeTypeFactory(
page=page, programme_type=ProgrammeTypeFactory()
)

self.form_data = {
"first_name": "Monty",
@@ -63,7 +71,7 @@ def setUp(self):
"country_of_residence": "GB",
"city": "Bristol",
"country_of_citizenship": "GB",
"programmes": [ProgrammePageFactory(qs_code=1, programme_type__pk=2).pk],
"programmes": [page.pk],
"start_date": StartDateFactory(qs_code="test-code").pk,
"enquiry_reason": EnquiryReasonFactory().pk,
"enquiry_questions": "What is your name?",
15 changes: 13 additions & 2 deletions rca/programmes/factories.py
Original file line number Diff line number Diff line change
@@ -2,7 +2,12 @@
import wagtail_factories
from faker import Factory as FakerFactory

from .models import DegreeLevel, ProgrammePage, ProgrammeType
from .models import (
DegreeLevel,
ProgrammePage,
ProgrammePageProgrammeType,
ProgrammeType,
)

faker = FakerFactory.create()

@@ -31,4 +36,10 @@ class Meta:
scholarships_information = factory.Faker("text", max_nb_chars=100)
search_description = factory.Faker("text", max_nb_chars=25)
degree_level = factory.SubFactory(DegreeLevelFactory)
programme_type = factory.SubFactory(ProgrammeTypeFactory)


class ProgrammePageProgrammeTypeFactory(factory.django.DjangoModelFactory):
class Meta:
model = ProgrammePageProgrammeType

page = factory.SubFactory(ProgrammePageFactory)
44 changes: 44 additions & 0 deletions rca/programmes/migrations/0093_programmepageprogrammetype.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Generated by Django 4.2.16 on 2025-01-14 14:23

from django.db import migrations, models
import django.db.models.deletion
import modelcluster.fields


class Migration(migrations.Migration):

dependencies = [
("programmes", "0092_link_block_url_label"),
]

operations = [
migrations.CreateModel(
name="ProgrammePageProgrammeType",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"page",
modelcluster.fields.ParentalKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="programme_types",
to="programmes.programmepage",
),
),
(
"programme_type",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="programmes.programmetype",
),
),
],
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Generated by Django 4.2.16 on 2025-01-14 14:25

from django.db import migrations

def migrate_programme_type_to_programme_types(apps, schema_editor):
# Get the models
ProgrammePage = apps.get_model("programmes", "ProgrammePage")
ProgrammePageProgrammeType = apps.get_model("programmes", "ProgrammePageProgrammeType")

for page in ProgrammePage.objects.all():
if programme_type := page.programme_type:
ProgrammePageProgrammeType.objects.create(
page_id=page.id,
programme_type=programme_type,
)


class Migration(migrations.Migration):

dependencies = [
("programmes", "0093_programmepageprogrammetype"),
]

operations = [
migrations.RunPython(migrate_programme_type_to_programme_types, reverse_code=migrations.RunPython.noop),
]
17 changes: 17 additions & 0 deletions rca/programmes/migrations/0095_remove_programme_type_field.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Generated by Django 4.2.16 on 2025-01-17 08:54

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
("programmes", "0094_migrate_programme_type_to_programme_types"),
]

operations = [
migrations.RemoveField(
model_name="programmepage",
name="programme_type",
),
]
37 changes: 23 additions & 14 deletions rca/programmes/models.py
Original file line number Diff line number Diff line change
@@ -113,6 +113,18 @@ def get_fake_slug(self):
return slugify(self.display_name)


class ProgrammePageProgrammeType(models.Model):
page = ParentalKey("programmes.ProgrammePage", related_name="programme_types")
programme_type = models.ForeignKey(
"programmes.ProgrammeType",
on_delete=models.CASCADE,
)
panels = [FieldPanel("programme_type")]

def __str__(self):
return self.programme_type.title


class ProgrammePageRelatedSchoolsAndResearchPages(RelatedPage):
source_page = ParentalKey(
"ProgrammePage", related_name="related_schools_and_research_pages"
@@ -330,13 +342,6 @@ class ProgrammePage(TapMixin, ContactFieldsMixin, BasePage):
degree_level = models.ForeignKey(
DegreeLevel, on_delete=models.SET_NULL, blank=False, null=True, related_name="+"
)
programme_type = models.ForeignKey(
ProgrammeType,
on_delete=models.SET_NULL,
blank=False,
null=True,
related_name="+",
)
hero_image = models.ForeignKey(
"images.CustomImage",
null=True,
@@ -615,10 +620,7 @@ class ProgrammePage(TapMixin, ContactFieldsMixin, BasePage):
# Taxonomy, relationships etc
FieldPanel("degree_level"),
InlinePanel("subjects", label="Subjects"),
FieldPanel(
"programme_type",
help_text="Used to show content related to this programme page",
),
InlinePanel("programme_types", label="Programme Types"),
MultiFieldPanel(
[
FieldPanel("hero_image"),
@@ -844,7 +846,14 @@ class ProgrammePage(TapMixin, ContactFieldsMixin, BasePage):
index.SearchField("scholarship_accordion_items"),
index.SearchField("scholarship_information_blocks"),
index.SearchField("more_information_blocks", boost=2),
index.RelatedFields("programme_type", [index.SearchField("display_name")]),
index.RelatedFields(
"programme_types",
[
index.RelatedFields(
"programme_type", [index.SearchField("display_name")]
)
],
),
index.RelatedFields(
"programme_locations",
[index.RelatedFields("programme_location", [index.SearchField("title")])],
@@ -876,7 +885,7 @@ class ProgrammePage(TapMixin, ContactFieldsMixin, BasePage):
api_fields = [
# Fields for filtering and display, shared with shortcourses.ShortCoursePage.
APIField("subjects"),
APIField("programme_type"),
APIField("programme_types"),
APIField("related_schools_and_research_pages"),
APIField(
"summary",
@@ -1169,7 +1178,7 @@ def get_context(self, request, *args, **kwargs):

filters = [
{"id": "subjects", "title": "Subject", "items": subjects},
{"id": "programme_type", "title": "Type", "items": programme_types},
{"id": "programme_types", "title": "Type", "items": programme_types},
{
"id": "related_schools_and_research_pages",
"title": "Schools & centres",
Original file line number Diff line number Diff line change
@@ -26,7 +26,17 @@ <h3 class="related-content__title heading heading--five" property="schema:name">
<svg width="12" height="8" class="related-content__icon" aria-hidden="true"><use xlink:href="#arrow"></use></svg>
</a>
</h3>
<p class="related-content__degree body body--one" property="schema:EducationalCredentialAwarded">{% firstof related_item.degree_level related_item.programme_type %}</p>
{% with programme_types=related_item.programme_types.all %}
{% if related_item.degree_levels %}
<p class="related-content__degree body body--one" property="schema:EducationalCredentialAwarded">{{ related_item.degree_level }}</p>
{% else %}
<p class="related-content__degree body body--one" property="schema:EducationalCredentialAwarded">
{% for item in programme_types %}
{{ item.programme_type.display_name }}{% if not forloop.last %}, {% endif %}
{% endfor %}
</p>
{% endif %}
{% endwith %}
</div>
<p class="related-content__copy body body--one" property="schema:description">
{% firstof related_item.programme_description_subtitle related_item.introduction related_item.listing_summary %}
Original file line number Diff line number Diff line change
@@ -39,15 +39,25 @@ <h3 class="related-content__title body body--one" property="schema:name">
<svg width="12" height="8" class="related-content__icon" aria-hidden="true"><use xlink:href="#arrow"></use></svg>
</a>
</h3>
{% if related_item.degree_level %}
<p class="related-content__degree body body--two" property="schema:EducationalCredentialAwarded">{{ related_item.degree_level }}</p>
{% elif related_item.programme_type %}
<p class="related-content__degree body body--two">{{ related_item.booking_summary|default:related_item.programme_type }}</p>
{% elif related_item.meta %}
<p class="related-content__degree body body--two">{{ related_item.meta }}</p>
{% elif related_item.get_verbose_name == 'Guide page' %}
<p class="related-content__degree body body--two">Guide</p>
{% endif %}
{% with programme_types=related_item.programme_types.all %}
{% if related_item.degree_level %}
<p class="related-content__degree body body--two" property="schema:EducationalCredentialAwarded">{{ related_item.degree_level }}</p>
{% elif programme_types %}
{% if related_item.booking_summary %}
<p class="related-content__degree body body--two">{{ related_item.booking_summary }}</p>
{% else %}
<p class="related-content__degree body body--two">
{% for item in programme_types %}
{{ item.programme_type.display_name }}{% if not forloop.last %}, {% endif %}
{% endfor %}
</p>
{% endif %}
{% elif related_item.meta %}
<p class="related-content__degree body body--two">{{ related_item.meta }}</p>
{% elif related_item.get_verbose_name == 'Guide page' %}
<p class="related-content__degree body body--two">Guide</p>
{% endif %}
{% endwith %}
</div>
<p class="related-content__copy body body--two" property="schema:description">
{% firstof related_item.programme_description_subtitle related_item.introduction|default:''|striptags related_item.listing_summary %}
2 changes: 1 addition & 1 deletion rca/schools/models.py
Original file line number Diff line number Diff line change
@@ -557,7 +557,7 @@ def get_short_courses_index_link(self):
).first()
if short_course_type:
return (
f"{self.get_programme_index_link()}?category=programme_type&"
f"{self.get_programme_index_link()}?category=programme_types&"
f"value={short_course_type.id}-{slugify(short_course_type.display_name)}"
)

11 changes: 9 additions & 2 deletions rca/shortcourses/factories.py
Original file line number Diff line number Diff line change
@@ -3,13 +3,20 @@

from rca.programmes.factories import ProgrammeTypeFactory

from .models import ShortCoursePage
from .models import ShortCoursePage, ShortCourseProgrammeType


class ShortCoursePageFactory(wagtail_factories.PageFactory):
class Meta:
model = ShortCoursePage

title = factory.Faker("text", max_nb_chars=25)
programme_type = factory.SubFactory(ProgrammeTypeFactory)
show_register_link = False


class ShortCourseProgrammeTypeFactory(factory.django.DjangoModelFactory):
class Meta:
model = ShortCourseProgrammeType

page = factory.SubFactory(ShortCoursePageFactory)
programme_type = factory.SubFactory(ProgrammeTypeFactory)
46 changes: 46 additions & 0 deletions rca/shortcourses/migrations/0039_shortcourseprogrammetype.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Generated by Django 4.2.16 on 2025-01-14 14:45

from django.db import migrations, models
import django.db.models.deletion
import modelcluster.fields


class Migration(migrations.Migration):

dependencies = [
("programmes", "0094_migrate_programme_type_to_programme_types"),
("shortcourses", "0038_shortcoursepage_dates"),
]

operations = [
migrations.CreateModel(
name="ShortCourseProgrammeType",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"page",
modelcluster.fields.ParentalKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="programme_types",
to="shortcourses.shortcoursepage",
),
),
(
"programme_type",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="short_course",
to="programmes.programmetype",
),
),
],
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Generated by Django 4.2.16 on 2025-01-14 14:45

from django.db import migrations


def migrate_programme_type_to_programme_types(apps, schema_editor):
# Get the models
ShortCoursePage = apps.get_model("shortcourses", "ShortCoursePage")
ShortCourseProgrammeType = apps.get_model("shortcourses", "ShortCourseProgrammeType")

for page in ShortCoursePage.objects.all():
if programme_type := page.programme_type:
ShortCourseProgrammeType.objects.create(
page_id=page.id,
programme_type=programme_type,
)

class Migration(migrations.Migration):

dependencies = [
("shortcourses", "0039_shortcourseprogrammetype"),
]

operations = [
migrations.RunPython(migrate_programme_type_to_programme_types, reverse_code=migrations.RunPython.noop),
]
17 changes: 17 additions & 0 deletions rca/shortcourses/migrations/0041_remove_programme_type_field.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Generated by Django 4.2.16 on 2025-01-17 08:54

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
("shortcourses", "0040_migrate_programme_type_to_programme_types"),
]

operations = [
migrations.RemoveField(
model_name="shortcoursepage",
name="programme_type",
),
]
31 changes: 20 additions & 11 deletions rca/shortcourses/models.py
Original file line number Diff line number Diff line change
@@ -23,7 +23,6 @@
from wagtail.models import Orderable
from wagtail.search import index

from rca.programmes.models import ProgrammeType
from rca.utils.blocks import (
AccordionBlockWithTitle,
GalleryBlock,
@@ -81,6 +80,16 @@ class ShortCourseSubjectPlacement(models.Model):
panels = [FieldPanel("subject")]


class ShortCourseProgrammeType(models.Model):
page = ParentalKey("ShortCoursePage", related_name="programme_types")
programme_type = models.ForeignKey(
"programmes.ProgrammeType",
on_delete=models.CASCADE,
related_name="short_course",
)
panels = [FieldPanel("programme_type")]


class ShortCourseManualDate(Orderable):
start_date = models.DateField(null=True)
end_date = models.DateField(null=True)
@@ -172,13 +181,6 @@ class ShortCoursePage(ContactFieldsMixin, BasePage):
help_text="If selected, an automatic 'Register your interest' link "
"will be visible in the key details section",
)
programme_type = models.ForeignKey(
ProgrammeType,
on_delete=models.SET_NULL,
blank=False,
null=True,
related_name="+",
)
dates = RichTextField(blank=True, features=["link"])
location = RichTextField(blank=True, features=["link"])
introduction = models.CharField(max_length=500, blank=True)
@@ -247,7 +249,7 @@ class ShortCoursePage(ContactFieldsMixin, BasePage):
heading=_("Course Introduction"),
),
FieldPanel("about"),
FieldPanel("programme_type"),
InlinePanel("programme_types", label="Programme types"),
FieldPanel("quote_carousel"),
MultiFieldPanel(
[FieldPanel("staff_title"), InlinePanel("related_staff", label="Staff")],
@@ -308,7 +310,14 @@ class ShortCoursePage(ContactFieldsMixin, BasePage):
index.SearchField("body"),
index.SearchField("about"),
index.SearchField("location"),
index.RelatedFields("programme_type", [index.SearchField("display_name")]),
index.RelatedFields(
"programme_types",
[
index.RelatedFields(
"programme_type", [index.SearchField("display_name")]
)
],
),
index.RelatedFields(
"subjects",
[index.RelatedFields("subject", [index.SearchField("title")])],
@@ -330,7 +339,7 @@ class ShortCoursePage(ContactFieldsMixin, BasePage):
api_fields = [
# Fields for filtering and display, shared with programmes.ProgrammePage.
APIField("subjects"),
APIField("programme_type"),
APIField("programme_types"),
APIField("related_schools_and_research_pages"),
APIField("summary", serializer=CharFieldSerializer(source="introduction")),
APIField(
1 change: 0 additions & 1 deletion rca/shortcourses/tests/test_models.py
Original file line number Diff line number Diff line change
@@ -44,7 +44,6 @@ def setUp(self):
self.short_course_page = ShortCoursePage(
title="Short course title",
slug="short-course",
programme_type_id=1,
contact_model_url="https://rca.ac.uk",
contact_model_text="Read more",
show_register_link=0,
20 changes: 20 additions & 0 deletions rca/wagtailapi/filters.py
Original file line number Diff line number Diff line change
@@ -43,6 +43,26 @@ def filter_queryset(self, request, queryset, view):
return queryset


class ProgrammeTypesFilter(filters.BaseFilterBackend):
def filter_queryset(self, request, queryset, view):
try:
queryset.model._meta.get_field("programme_types")
programme_type_ids = [
int(id) for id in request.GET.getlist("programme_types", [])
]
if programme_type_ids:
queryset = (
queryset.model.objects.filter(
programme_types__programme_type_id__in=programme_type_ids
)
.order_by("title")
.live()
)
return queryset
except FieldDoesNotExist:
return queryset


class RelatedSchoolsFilter(filters.BaseFilterBackend):
def filter_queryset(self, request, queryset, view):
try:
1 change: 1 addition & 0 deletions rca/wagtailapi/views.py
Original file line number Diff line number Diff line change
@@ -20,6 +20,7 @@ class PagesAPIViewSet(views.PagesAPIViewSet):
DescendantOfFilter,
filters.RelatedSchoolsFilter,
filters.SubjectsFilter,
filters.ProgrammeTypesFilter,
filters.StudyModeFilter,
filters.DistinctFilter,
filters.SearchFilter,

0 comments on commit e12b856

Please sign in to comment.