diff --git a/nau_openedx_extensions/custom_registration_form/context_extender.py b/nau_openedx_extensions/custom_registration_form/context_extender.py index 5b61005..149faa0 100644 --- a/nau_openedx_extensions/custom_registration_form/context_extender.py +++ b/nau_openedx_extensions/custom_registration_form/context_extender.py @@ -27,7 +27,7 @@ def get_fields(custom_model_instance): for field in custom_fields: if field.name not in allowed_fields: continue - if isinstance(field, (models.CharField, models.TextField)): + if isinstance(field, (models.CharField, models.TextField, models.BooleanField)): yield field @@ -44,14 +44,21 @@ def update_account_view(context, user, **kwargs): custom_model_instance = NauUserExtendedModel() finally: for field in get_fields(custom_model_instance): - extended_profile_fields.append( - { - "field_name": _(field.name), # pylint: disable=translation-of-non-string - "field_label": _(field.verbose_name), # pylint: disable=translation-of-non-string - "field_type": "TextField" if not field.choices else "ListField", - "field_options": [] if not field.choices else field.choices, - } - ) + extended_profile_field = { + "field_name": _(field.name), # pylint: disable=translation-of-non-string + "field_label": _(field.verbose_name), # pylint: disable=translation-of-non-string + } + if isinstance(field, models.BooleanField): + extended_profile_field["field_type"] = "CheckboxField" + extended_profile_field["field_options"] = [] + elif field.choices: + extended_profile_field["field_type"] = "ListField" + extended_profile_field["field_options"] = field.choices + else: + extended_profile_field["field_type"] = "TextField" + extended_profile_field["field_options"] = [] + + extended_profile_fields.append(extended_profile_field) context["extended_profile_fields"].extend(extended_profile_fields) diff --git a/nau_openedx_extensions/filters/pipeline.py b/nau_openedx_extensions/filters/pipeline.py index 99dfd60..36cf371 100644 --- a/nau_openedx_extensions/filters/pipeline.py +++ b/nau_openedx_extensions/filters/pipeline.py @@ -5,6 +5,7 @@ from fnmatch import fnmatch from django.conf import settings +from django.db.models.query import QuerySet from django.utils.translation import gettext as _ from openedx_filters import PipelineStep from openedx_filters.learning.filters import CourseEnrollmentStarted @@ -77,3 +78,43 @@ def _is_user_email_allowed(user, domains_allowed): if user_domain == domain or fnmatch(user_domain, f"*.{domain}"): return True return False + + +class FilterUsersWithAllowedNewsletter(PipelineStep): + """ + Filter the Schedules QuerySet to only keep those whose associated user has + the `allow_newsletter` field set to `True`. If the user does not have the + `allow_newsletter` field set to `True`, or if the field does not exist, the + Schedule will be filtered out. + + The Schedules QuerySet is used to send recurring nudges emails to users. + This filter allows excluding users who have opted out of receiving these + emails. + + Example usage: + + Add the following configurations to your configuration file: + + ``` + OPEN_EDX_FILTERS_CONFIG = { + "org.openedx.learning.schedule.queryset.requested.v1": { + "fail_silently": False, + "pipeline": [ + "nau_openedx_extensions.filters.pipeline.FilterUsersWithAllowedNewsletter", + ], + }, + } + ``` + """ + + def run_filter(self, schedules: QuerySet) -> dict: # pylint: disable=arguments-differ + """ + Execute filter that filters users with allowed newsletter. + + Arguments: + schedules (QuerySet): Queryset of schedules to be sent. + + Returns: + dict: Dictionary with the filtered schedules. + """ + return {"schedules": schedules.filter(enrollment__user__nauuserextendedmodel__allow_newsletter=True)} diff --git a/nau_openedx_extensions/locale/en/LC_MESSAGES/django.mo b/nau_openedx_extensions/locale/en/LC_MESSAGES/django.mo index 46d442a..ca6659e 100644 Binary files a/nau_openedx_extensions/locale/en/LC_MESSAGES/django.mo and b/nau_openedx_extensions/locale/en/LC_MESSAGES/django.mo differ diff --git a/nau_openedx_extensions/locale/en/LC_MESSAGES/django.po b/nau_openedx_extensions/locale/en/LC_MESSAGES/django.po index 007c63a..186bc96 100644 --- a/nau_openedx_extensions/locale/en/LC_MESSAGES/django.po +++ b/nau_openedx_extensions/locale/en/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: equipa@nau.edu.pt\n" -"POT-Creation-Date: 2024-10-02 13:56+0100\n" +"POT-Creation-Date: 2024-12-12 12:58-0500\n" "PO-Revision-Date: 2021-02-15 15:56+0000\n" "Last-Translator: FULL NAME \n" "Language: en\n" @@ -108,18 +108,18 @@ msgstr "" "href='https://www.nau.edu.pt/legal/consentimento-para-o-envio-de-" "newsletters/'>consent to the sending of newsletters" -#: nau_openedx_extensions/filters/pipeline.py:50 +#: nau_openedx_extensions/filters/pipeline.py:51 msgid "" "You need to activate your account before you can enroll in the course. " "Check your {email} inbox for an account activation link from " "{platform_name}." msgstr "" -#: nau_openedx_extensions/filters/pipeline.py:62 +#: nau_openedx_extensions/filters/pipeline.py:63 msgid "If you think this is an error, contact the course support." msgstr "" -#: nau_openedx_extensions/filters/pipeline.py:63 +#: nau_openedx_extensions/filters/pipeline.py:64 #, python-format msgid "" "You can't enroll on this course because your email domain is not allowed." diff --git a/nau_openedx_extensions/locale/pt_PT/LC_MESSAGES/django.mo b/nau_openedx_extensions/locale/pt_PT/LC_MESSAGES/django.mo index b2f3646..97e5ba8 100644 Binary files a/nau_openedx_extensions/locale/pt_PT/LC_MESSAGES/django.mo and b/nau_openedx_extensions/locale/pt_PT/LC_MESSAGES/django.mo differ diff --git a/nau_openedx_extensions/locale/pt_PT/LC_MESSAGES/django.po b/nau_openedx_extensions/locale/pt_PT/LC_MESSAGES/django.po index 415ac13..8eab565 100644 --- a/nau_openedx_extensions/locale/pt_PT/LC_MESSAGES/django.po +++ b/nau_openedx_extensions/locale/pt_PT/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: equipa@nau.edu.pt\n" -"POT-Creation-Date: 2024-10-02 13:56+0100\n" +"POT-Creation-Date: 2024-12-12 12:58-0500\n" "PO-Revision-Date: 2021-02-15 15:56+0000\n" "Last-Translator: Ivo Branco \n" "Language: pt_PT\n" @@ -110,7 +110,7 @@ msgstr "" "href='https://www.nau.edu.pt/legal/consentimento-para-o-envio-de-" "newsletters/'>consentimento para o envio de newsletters" -#: nau_openedx_extensions/filters/pipeline.py:50 +#: nau_openedx_extensions/filters/pipeline.py:51 msgid "" "You need to activate your account before you can enroll in the course. " "Check your {email} inbox for an account activation link from " @@ -120,11 +120,11 @@ msgstr "" "Verifique no seu e-mail {email} o link de ativação da conta " "{platform_name}." -#: nau_openedx_extensions/filters/pipeline.py:62 +#: nau_openedx_extensions/filters/pipeline.py:63 msgid "If you think this is an error, contact the course support." msgstr "Se achar que se trata de um erro, contacte o suporte." -#: nau_openedx_extensions/filters/pipeline.py:63 +#: nau_openedx_extensions/filters/pipeline.py:64 #, python-format msgid "" "You can't enroll on this course because your email domain is not allowed." diff --git a/nau_openedx_extensions/settings/common.py b/nau_openedx_extensions/settings/common.py index 36e4995..02c7890 100644 --- a/nau_openedx_extensions/settings/common.py +++ b/nau_openedx_extensions/settings/common.py @@ -77,7 +77,7 @@ def plugin_settings(settings): settings.NAU_COURSE_MESSAGE_BATCH_SIZE = 50 settings.NAU_COURSE_MESSAGE_RECIPIENT_FIELDS = ["profile__name", "email"] settings.NAU_CC_ALLOWED_SLUG = "cccmd:" - settings.NAU_ACCOUNTS_CC_VISIBLE_FIELDS = ["employment_situation", "nif"] + settings.NAU_ACCOUNTS_CC_VISIBLE_FIELDS = ["employment_situation", "nif", "allow_newsletter"] settings.SCORMXBLOCK_ASYNC_THRESHOLD = 500 settings.NAU_SITE_CONFIGURATION_HELPERS_MODULE = ( "nau_openedx_extensions.edxapp_wrapper.backends.site_configuration_helpers_l_v1" diff --git a/nau_openedx_extensions/tests/test_pipeline.py b/nau_openedx_extensions/tests/test_pipeline.py index 43b4689..cb47ecc 100644 --- a/nau_openedx_extensions/tests/test_pipeline.py +++ b/nau_openedx_extensions/tests/test_pipeline.py @@ -6,10 +6,11 @@ from django.test import TestCase from django.test.utils import override_settings +from django_mock_queries.query import MockModel, MockSet from opaque_keys.edx.keys import CourseKey from openedx_filters.learning.filters import CourseEnrollmentStarted -from nau_openedx_extensions.filters.pipeline import FilterEnrollmentByDomain +from nau_openedx_extensions.filters.pipeline import FilterEnrollmentByDomain, FilterUsersWithAllowedNewsletter class FilterEnrollmentByDomainTest(TestCase): @@ -246,3 +247,38 @@ def test_inactive_user_with_email_not_in_allowed_domains(self): "You need to activate your account before you can enroll in the course. " "Check your example@example.com inbox for an account activation link from NAU." )) + + +class TestFilterUsersWithAllowedNewsletter(TestCase): + """ + Test the FilterUsersWithAllowedNewsletter class that filters users who have allowed newsletters. + """ + + def test_run_filter(self): + """ + Test that the filter returns only schedules for users who have allowed newsletters. + + Expected result: + - The filter returns a dictionary with the key schedules and a queryset of schedules. + - The schedules queryset has only one schedule that has a user with allow_newsletter=True. + - The other schedules that have a user with allow_newsletter=False or without allow_newsletter + are not in the queryset. + """ + mock_schedules = MockSet( + MockModel( + mock_name="allow_newsletter_true", + enrollment=MockModel(user=MockModel(nauuserextendedmodel=MockModel(allow_newsletter=True))), + ), + MockModel( + mock_name="allow_newsletter_false", + enrollment=MockModel(user=MockModel(nauuserextendedmodel=MockModel(allow_newsletter=False))), + ), + MockModel(mock_name="without_allow_newsletter", enrollment=MockModel(user=MockModel())), + ) + + result = FilterUsersWithAllowedNewsletter.run_filter(self, mock_schedules) + + self.assertIsInstance(result, dict) + self.assertIn("schedules", result) + self.assertEqual(len(result["schedules"]), 1) + self.assertEqual(result["schedules"][0].mock_name, "allow_newsletter_true") diff --git a/nau_openedx_extensions/verify_student/id_verification.py b/nau_openedx_extensions/verify_student/id_verification.py index 20ea909..d51ff4c 100644 --- a/nau_openedx_extensions/verify_student/id_verification.py +++ b/nau_openedx_extensions/verify_student/id_verification.py @@ -40,7 +40,7 @@ def verification_active_predicate(verification): if verification_active: log.info("User %d already has an ID verification", user_id) else: - expiration_date = now + timedelta(days=36500) # 100 years + expiration_date = now + timedelta(days=365 * 100 + 100/4) # 365 days * 100 years + leap year days log.info("Create user ID Verification for %d", user_id) create_user_id_verification( user_id, diff --git a/requirements/base.in b/requirements/base.in index b1fbc13..96505e1 100644 --- a/requirements/base.in +++ b/requirements/base.in @@ -4,5 +4,5 @@ edx-opaque-keys[django] six future; python_version < "3.0" web-fragments -openedx-filters==1.8.1 -openedx-events==9.10.0 +openedx-filters==1.12.0 +openedx-events==9.15.0 diff --git a/requirements/base.txt b/requirements/base.txt index d0320b2..67e7649 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -4,19 +4,19 @@ # # make upgrade # -amqp==2.6.1 # via kombu -billiard==3.6.4.0 # via celery -celery==4.4.7 # via -c requirements/constraints.txt, -r requirements/base.in +amqp==5.2.0 # via kombu +billiard==4.2.0 # via celery +celery==5.4.0 # via -c requirements/constraints.txt, -r requirements/base.in django==2.2.25 # via -c requirements/constraints.txt, edx-opaque-keys, openedx-filters edx-opaque-keys[django]==2.2.0 # via -c requirements/constraints.txt, -r requirements/base.in -kombu==4.6.11 # via celery -openedx-filters==1.8.1 # via -c requirements/constraints.txt, -r requirements/base.in -openedx-events==9.10.0 +kombu==5.3.4 # via celery +openedx-filters==1.12.0 # via -c requirements/constraints.txt, -r requirements/base.in +openedx-events==9.15.0 pbr==5.10.0 # via stevedore pymongo==4.2.0 # via edx-opaque-keys pytz==2022.2.1 # via celery, django six==1.16.0 # via -r requirements/base.in sqlparse==0.4.2 # via django stevedore==4.0.0 # via edx-opaque-keys -vine==1.3.0 # via amqp, celery +vine==5.1.0 # via amqp, celery web-fragments==2.0.0 # via -r requirements/base.in diff --git a/requirements/constraints.txt b/requirements/constraints.txt index 628d734..22c2b03 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -2,7 +2,7 @@ celery<5.0 Django==2.2.25 edx-opaque-keys[django]==2.2.0 -openedx-filters==1.8.1 -openedx-events==9.10.0 +openedx-filters==1.12.0 +openedx-events==9.15.0 pip-tools<5.4 click==7.1.2 diff --git a/requirements/django.txt b/requirements/django.txt index f6c201b..652a7c2 100644 --- a/requirements/django.txt +++ b/requirements/django.txt @@ -1,2 +1,2 @@ -celery==4.4.7 # via -c requirements/constraints.txt, -r requirements/base.txt +celery==5.4.0 # via -c requirements/constraints.txt, -r requirements/base.txt django==2.2.25 # via -c requirements/constraints.txt, -r requirements/base.txt, edx-opaque-keys, openedx-filters diff --git a/requirements/test.in b/requirements/test.in index d774883..ecb481a 100644 --- a/requirements/test.in +++ b/requirements/test.in @@ -8,3 +8,4 @@ pycodestyle pylint pytest coverage +django_mock_queries diff --git a/requirements/test.txt b/requirements/test.txt index 258393d..b24fb05 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -4,10 +4,10 @@ # # make upgrade # -amqp==2.6.1 # via -r requirements/base.txt, kombu +amqp==5.2.0 # via -r requirements/base.txt, kombu astroid==2.12.9 # via pylint, pylint-celery attrs==22.1.0 # via pytest -billiard==3.6.4.0 # via -r requirements/base.txt, celery +billiard==4.2.0 # via -r requirements/base.txt, celery click-log==0.4.0 # via edx-lint click==7.1.2 # via -c requirements/constraints.txt, click-log, code-annotations, edx-lint code-annotations==1.3.0 # via edx-lint @@ -18,12 +18,12 @@ edx-opaque-keys[django]==2.2.0 # via -c requirements/constraints.txt, -r requir iniconfig==1.1.1 # via pytest isort==5.10.1 # via pylint jinja2==3.1.2 # via code-annotations -kombu==4.6.11 # via -r requirements/base.txt, celery +kombu==5.3.4 # via -r requirements/base.txt, celery lazy-object-proxy==1.7.1 # via astroid markupsafe==2.1.1 # via jinja2 mccabe==0.7.0 # via pylint -openedx-filters==1.8.1 # via -c requirements/constraints.txt, -r requirements/base.txt -openedx-events==9.10.0 +openedx-filters==1.12.0 # via -c requirements/constraints.txt, -r requirements/base.txt +openedx-events==9.15.0 packaging==21.3 # via pytest pbr==5.10.0 # via -r requirements/base.txt, stevedore platformdirs==2.5.2 # via pylint @@ -47,6 +47,8 @@ text-unidecode==1.3 # via python-slugify tomli==2.0.1 # via pylint, pytest tomlkit==0.11.4 # via pylint typing-extensions==4.3.0 # via astroid, pylint -vine==1.3.0 # via -r requirements/base.txt, amqp, celery +vine==5.1.0 # via -r requirements/base.txt, amqp, celery web-fragments==2.0.0 # via -r requirements/base.txt wrapt==1.14.1 # via astroid +django_mock_queries==2.3.0 # via -r requirements/test.in +model-bakery==1.5.0 # via django_mock_queries