Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,15 @@ Versioning](https://semver.org/spec/v2.0.0.html).

### Added

- Add `offer` and `certificate_offer` search filters on course runs
- Add main content placeholder to categories list view
- Withdrawing feature for certificate and credential orders
- Upgrade to node 24.21.0

### Fixed

- Match facet keys on the filter name prefix so a filter does not collect the
facet counts of another filter whose name ends with its own name
- Pin webpack version in dependent front build
- Handle user data form the authentication method (especially for mail)
- Handle response detail for withdrawal request in specific usecases
Expand Down
5 changes: 5 additions & 0 deletions UPGRADE.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ $ make migrate

## Unreleased

- Two new search filters, `offer` and `certificate_offer`, are added to the default
`RICHIE_FILTERS_CONFIGURATION` (nested under `course_runs`) and appended to the default
`RICHIE_FILTERS_PRESENTATION`. If you override either setting, add them to your own
configuration to display them, or leave them out to keep your search page unchanged.

## 2.34.0 to v3.0.0-beta.0

- We upgrade our frontend to React 19, you have to update your dependencies
Expand Down
36 changes: 36 additions & 0 deletions docs/filters-customization.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,42 @@ FILTERS_CONFIGURATION = {
}
```

Richie ships with two other nested filters on course runs, based on the `offer` and
`certificate_offer` fields of course runs: `OfferFilterDefinition` lets users filter courses
that can be followed for free, partially for free, with a subscription or by paying, and
`CertificateOfferFilterDefinition` does the same for the certificate. They only need a
`human_name` to be configured:

```
FILTERS_CONFIGURATION = {
...
"course_runs": {
"class": "richie.apps.search.filter_definitions.NestingWrapper",
"params": {
"filters": {
"offer": {
"class": "richie.apps.search.filter_definitions.OfferFilterDefinition",
"params": {
"human_name": _("Offer"),
"min_doc_count": 0,
"sorting": "conf",
},
},
"certificate_offer": {
"class": "richie.apps.search.filter_definitions.CertificateOfferFilterDefinition",
"params": {
"human_name": _("Certificate offer"),
"min_doc_count": 0,
"sorting": "conf",
},
},
}
},
},
...
}
```

## Filters presentation

Which filters are displayed in the left side bar of the search page and in which order is defined
Expand Down
21 changes: 21 additions & 0 deletions src/richie/apps/search/defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,25 @@
"sorting": "conf",
},
},
"offer": {
"class": "richie.apps.search.filter_definitions.OfferFilterDefinition",
"params": {
"human_name": _("Offer"),
"min_doc_count": 0,
"sorting": "conf",
},
},
"certificate_offer": {
"class": (
"richie.apps.search.filter_definitions."
"CertificateOfferFilterDefinition"
),
"params": {
"human_name": _("Certificate offer"),
"min_doc_count": 0,
"sorting": "conf",
},
},
"languages": {
"class": "richie.apps.search.filter_definitions.LanguagesFilterDefinition",
"params": {
Expand Down Expand Up @@ -172,5 +191,7 @@
"persons",
"licences",
"pace",
"offer",
"certificate_offer",
],
)
2 changes: 2 additions & 0 deletions src/richie/apps/search/filter_definitions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@
from .base import BaseFilterDefinition, NestingWrapper # noqa
from .courses import ( # noqa
AvailabilityFilterDefinition,
CertificateOfferFilterDefinition,
IndexableFilterDefinition,
IndexableHierarchicalFilterDefinition,
LanguagesFilterDefinition,
OfferFilterDefinition,
StaticChoicesFilterDefinition,
)

Expand Down
2 changes: 1 addition & 1 deletion src/richie/apps/search/filter_definitions/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ def get_facet_info(self, facets, data, *args, **kwargs):
facet_counts = [
(key.split("@")[1], facet["doc_count"])
for key, facet in facets.items()
if f"{self.name:s}@" in key # 6 times faster than startswith
if key.startswith(f"{self.name:s}@")
]

# Detect the applicable facet counts limit depending on the request
Expand Down
52 changes: 51 additions & 1 deletion src/richie/apps/search/filter_definitions/courses.py
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,7 @@ def get_facet_info(self, facets, data, *args, **kwargs):
{
key.split("@")[1]: facet["doc_count"]
for key, facet in facets.items()
if f"{self.name:s}@" in key # 6 times faster than startswith
if key.startswith(f"{self.name:s}@")
}
)

Expand Down Expand Up @@ -475,6 +475,56 @@ def get_fragment_map(self):
}


class OfferFilterDefinition(
NestedChoicesAggsMixin, TermsQueryMixin, BaseChoicesFilterDefinition
):
"""
Filter definition to allow filtering by the offer of course runs ie whether the course
can be followed for free, partially for free, with a subscription or by paying for it.

Hardcoded choices match the `offer` field of the course run model and are applied on the
`course_runs` nested field. Selected values are combined in a single terms query so that
choosing several offers matches course runs having any of them.
"""

FREE, PARTIALLY_FREE, SUBSCRIPTION, PAID = (
"free",
"partially_free",
"subscription",
"paid",
)

def get_values(self):
"""Return the hardcoded values with internationalized human names."""
return {
self.FREE: _("Free"),
self.PARTIALLY_FREE: _("Partially free"),
self.SUBSCRIPTION: _("Subscription"),
self.PAID: _("Paid"),
}

def get_fragment_map(self):
"""Return a terms query on the nested field for each of the possible values."""
return {value: [{"terms": {self.term: [value]}}] for value in self.get_values()}


class CertificateOfferFilterDefinition(OfferFilterDefinition):
"""
Filter definition to allow filtering by the certificate offer of course runs ie whether
the certificate can be obtained for free, with a subscription or by paying for it.

Hardcoded choices match the `certificate_offer` field of the course run model.
"""

def get_values(self):
"""Return the hardcoded values with internationalized human names."""
return {
self.FREE: _("Free"),
self.SUBSCRIPTION: _("Subscription"),
self.PAID: _("Paid"),
}


class LanguagesFilterDefinition(
NestedChoicesAggsMixin, TermsQueryMixin, BaseChoicesFilterDefinition
):
Expand Down
6 changes: 6 additions & 0 deletions tests/apps/search/test_forms_search_courses.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ def test_forms_courses_empty_querystring(self, *_):
form.cleaned_data,
{
"availability": [],
"certificate_offer": [],
"facet_sorting": "",
"languages": [],
"levels": [],
Expand All @@ -41,6 +42,7 @@ def test_forms_courses_empty_querystring(self, *_):
"licences_children_aggs": "",
"limit": None,
"new": [],
"offer": [],
"offset": None,
"organizations": [],
"organizations_aggs": [],
Expand Down Expand Up @@ -154,6 +156,7 @@ def test_forms_courses_single_values_in_querystring(self, *_):
form.cleaned_data,
{
"availability": ["coming_soon"],
"certificate_offer": [],
"facet_sorting": "count",
"languages": ["fr"],
"levels": ["1"],
Expand All @@ -164,6 +167,7 @@ def test_forms_courses_single_values_in_querystring(self, *_):
"licences_children_aggs": "",
"limit": 9,
"new": ["new"],
"offer": [],
"offset": 3,
"organizations": ["10"],
"organizations_aggs": [],
Expand Down Expand Up @@ -222,6 +226,7 @@ def test_forms_courses_multi_values_in_querystring(self, *_):
form.cleaned_data,
{
"availability": ["coming_soon", "ongoing"],
"certificate_offer": [],
"facet_sorting": "name",
"languages": ["fr", "en"],
"levels": ["1", "2"],
Expand All @@ -232,6 +237,7 @@ def test_forms_courses_multi_values_in_querystring(self, *_):
"licences_children_aggs": "",
"limit": 9,
"new": ["new"],
"offer": [],
"offset": 3,
"organizations": ["10", "11"],
"organizations_aggs": ["43", "44"],
Expand Down
Loading