From 09243ad749e3008eb5310a967d365ca1c945fe58 Mon Sep 17 00:00:00 2001 From: Frank Duncan Date: Mon, 13 May 2024 08:12:09 -0500 Subject: [PATCH 1/7] Add top level infrastructure for new extensions work This creates a place to put extensions django applications so that base.py doesn't run into extra changes for people who want extensions. Extension based configuration will also go here. --- docs/references/signals.md | 2 ++ hypha/settings/base.py | 5 +++++ hypha/settings/extensions.py | 4 ++++ 3 files changed, 11 insertions(+) create mode 100644 docs/references/signals.md create mode 100644 hypha/settings/extensions.py diff --git a/docs/references/signals.md b/docs/references/signals.md new file mode 100644 index 0000000000..28bf5434e4 --- /dev/null +++ b/docs/references/signals.md @@ -0,0 +1,2 @@ +Hypha uses django signals to enable extensions to modify behavior, as well as execute their own code when certain things happen in hypha. This page servers as a reference to implemented signals in hypha. + diff --git a/hypha/settings/base.py b/hypha/settings/base.py index 1bb31dbd7a..2ec698a6e1 100644 --- a/hypha/settings/base.py +++ b/hypha/settings/base.py @@ -622,3 +622,8 @@ debug=SENTRY_DEBUG, integrations=[DjangoIntegration()], ) + +# We import so we can override settings in base.py +from .extensions import * # noqa + +INSTALLED_APPS.extend(EXTENSION_APPS) diff --git a/hypha/settings/extensions.py b/hypha/settings/extensions.py new file mode 100644 index 0000000000..98700289d2 --- /dev/null +++ b/hypha/settings/extensions.py @@ -0,0 +1,4 @@ +# Add enabled extensions here +EXTENSIONS_APPS = [] + +# Add configuration for extensions below here From 00bc1fcc4ff26a1f2cdee36edad7d235902aa6b5 Mon Sep 17 00:00:00 2001 From: Frank Duncan Date: Fri, 17 May 2024 09:20:39 -0500 Subject: [PATCH 2/7] Add django_hooks for template hooks This is pulled from https://github.com/nitely/django-hooks with everything except the template hooks removed. --- extensions/django_hooks/README.md | 7 + extensions/django_hooks/__init__.py | 0 extensions/django_hooks/docs/templatehook.rst | 89 +++++++++++ extensions/django_hooks/templatehook.py | 139 ++++++++++++++++++ .../django_hooks/templatetags/__init__.py | 0 .../django_hooks/templatetags/hooks_tags.py | 62 ++++++++ hypha/settings/django.py | 1 + 7 files changed, 298 insertions(+) create mode 100644 extensions/django_hooks/README.md create mode 100644 extensions/django_hooks/__init__.py create mode 100644 extensions/django_hooks/docs/templatehook.rst create mode 100644 extensions/django_hooks/templatehook.py create mode 100644 extensions/django_hooks/templatetags/__init__.py create mode 100644 extensions/django_hooks/templatetags/hooks_tags.py diff --git a/extensions/django_hooks/README.md b/extensions/django_hooks/README.md new file mode 100644 index 0000000000..a1248c912c --- /dev/null +++ b/extensions/django_hooks/README.md @@ -0,0 +1,7 @@ +# django_hooks + +Adds the ability to have hooks injected into templates, via the `hooks_tags.hook` template tag. + +Originally from https://github.com/nitely/django-hooks but after culling everything except the template hook. The documentation on that template hook at [readthedocs](https://django-hooks.readthedocs.io/en/latest/hooks.html#templatehook) is [replicated locally](docs/templatehook.rst) + +The reason this was forked, rather than used as a module, is that the last commit from the other project was made in 2015, and can no longer be imported into a modern django project. The template hook continues to work correctly, but the other hooks most likely do not, and so were removed. diff --git a/extensions/django_hooks/__init__.py b/extensions/django_hooks/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/extensions/django_hooks/docs/templatehook.rst b/extensions/django_hooks/docs/templatehook.rst new file mode 100644 index 0000000000..c8e25005e2 --- /dev/null +++ b/extensions/django_hooks/docs/templatehook.rst @@ -0,0 +1,89 @@ +TemplateHook +============ + +Adding a hook-point in ``main_app``'s template:: + + # my_main_app/templates/_base.html + + {% load hooks_tags %} + + + + + #... + + {% hook 'within_head' %} + + #... + + + +.. Tip:: Here we are adding a *hook-point* called ``within_head`` where *third-party* + apps will be able to insert their code. + +Creating a hook listener in a ``third_party_app``:: + + # third_party_app/template_hooks.py + + from django.template.loader import render_to_string + from django.utils.html import mark_safe, format_html + + + # Example 1 + def css_resources(context, *args, **kwargs): + return mark_safe(u'' % settings.STATIC_URL) + + + # Example 2 + def user_about_info(context, *args, **kwargs): + user = context['request'].user + return format_html( + "{name} {last_name}: {about}", + name=user.first_name, + last_name=user.last_name, + about=mark_safe(user.profile.about_html_field) # Some safe (sanitized) html data. + ) + + + # Example 3 + def a_more_complex_hook(context, *args, **kwargs): + # If you are doing this a lot, make sure to keep your templates in memory (google: django.template.loaders.cached.Loader) + return render_to_string( + template_name='templates/app_hook/head_resources.html', + context_instance=context + ) + + + # Example 4 + def an_even_more_complex_hook(context, *args, **kwargs): + articles = Article.objects.all() + return render_to_string( + template_name='templates/app_hook/my_articles.html', + dictionary={'articles': articles, }, + context_instance=context + ) + +Registering a hook listener in a ``third_party_app``:: + + # third_party_app/apps.py + + from django.apps import AppConfig + + + class MyAppConfig(AppConfig): + + name = 'myapp' + verbose_name = 'My App' + + def ready(self): + from hooks.templatehook import hook + from third_party_app.template_hooks import css_resources + + hook.register("within_head", css_resources) + +.. Tip:: Where to register your hooks: + + Use ``AppConfig.ready()``: docs_ and example_ + +.. _docs: https://docs.djangoproject.com/en/1.8/ref/applications/#django.apps.AppConfig.ready +.. _example: http://chriskief.com/2014/02/28/django-1-7-signals-appconfig/ diff --git a/extensions/django_hooks/templatehook.py b/extensions/django_hooks/templatehook.py new file mode 100644 index 0000000000..1e8cf1a768 --- /dev/null +++ b/extensions/django_hooks/templatehook.py @@ -0,0 +1,139 @@ +class TemplateHook(object): + """ + A hook for templates. This can be used directly or\ + through the :py:class:`Hook` dispatcher + + :param list providing_args: A list of the arguments\ + this hook can pass along in a :py:func:`.__call__` + """ + + def __init__(self, providing_args=None): + self.providing_args = providing_args or [] + self._registry = [] + + def __call__(self, *args, **kwargs): + """ + Collect all callbacks responses for this template hook + + :return: Responses by registered callbacks,\ + this is usually a list of HTML strings + :rtype: list + """ + return [func(*args, **kwargs) for func in self._registry] + + def register(self, func): + """ + Register a new callback + + :param callable func: A function reference used as a callback + """ + assert callable(func), "Callback func must be a callable" + + self._registry.append(func) + + def unregister(self, func): + """ + Remove a previously registered callback + + :param callable func: A function reference\ + that was registered previously + """ + try: + self._registry.remove(func) + except ValueError: + pass + + def unregister_all(self): + """ + Remove all callbacks + """ + del self._registry[:] + + +class Hook(object): + """ + Dynamic dispatcher (proxy) for :py:class:`TemplateHook` + """ + + def __init__(self): + self._registry = {} + + def __call__(self, name, *args, **kwargs): + """ + Collect all callbacks responses for this template hook.\ + The hook (name) does not need to be pre-created,\ + it may not exist at call time + + :param str name: Hook name, it must be unique,\ + prefixing it with the app label is a good idea + :return: Responses by registered callbacks + :rtype: list + """ + try: + templatehook = self._registry[name] + except KeyError: + return [] + + return templatehook(*args, **kwargs) + + def _register(self, name): + """ + @Api private + Add new :py:class:`TemplateHook` into the registry + + :param str name: Hook name + :return: Instance of :py:class:`TemplateHook` + :rtype: :py:class:`TemplateHook` + """ + templatehook = TemplateHook() + self._registry[name] = templatehook + return templatehook + + def register(self, name, func): + """ + Register a new callback.\ + When the name/id is not found\ + a new hook is created under its name,\ + meaning the hook is usually created by\ + the first registered callback + + :param str name: Hook name + :param callable func: A func reference (callback) + """ + try: + templatehook = self._registry[name] + except KeyError: + templatehook = self._register(name) + + templatehook.register(func) + + def unregister(self, name, func): + """ + Remove a previously registered callback + + :param str name: Hook name + :param callable func: A function reference\ + that was registered previously + """ + try: + templatehook = self._registry[name] + except KeyError: + return + + templatehook.unregister(func) + + def unregister_all(self, name): + """ + Remove all callbacks + + :param str name: Hook name + """ + try: + templatehook = self._registry[name] + except KeyError: + return + + templatehook.unregister_all() + + +hook = Hook() diff --git a/extensions/django_hooks/templatetags/__init__.py b/extensions/django_hooks/templatetags/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/extensions/django_hooks/templatetags/hooks_tags.py b/extensions/django_hooks/templatetags/hooks_tags.py new file mode 100644 index 0000000000..e9b98d8e8c --- /dev/null +++ b/extensions/django_hooks/templatetags/hooks_tags.py @@ -0,0 +1,62 @@ +from django import template +from django.utils.html import format_html_join + +from extensions.django_hooks.templatehook import hook + +register = template.Library() + + +@register.simple_tag(name="hook", takes_context=True) +def hook_tag(context, name, *args, **kwargs): + r""" + Hook tag to call within templates + + :param dict context: This is automatically passed,\ + contains the template state/variables + :param str name: The hook which will be dispatched + :param \*args: Positional arguments, will be passed to hook callbacks + :param \*\*kwargs: Keyword arguments, will be passed to hook callbacks + :return: A concatenation of all callbacks\ + responses marked as safe (conditionally) + :rtype: str + """ + return format_html_join( + sep="\n", + format_string="{}", + args_generator=( + (response,) for response in hook(name, context, *args, **kwargs) + ), + ) + + +def template_hook_collect(module, hook_name, *args, **kwargs): + r""" + Helper to include in your own templatetag, for static TemplateHooks + + Example:: + + import myhooks + from hooks.templatetags import template_hook_collect + + @register.simple_tag(takes_context=True) + def hook(context, name, *args, **kwargs): + return template_hook_collect(myhooks, name, context, *args, **kwargs) + + :param module module: Module containing the template hook definitions + :param str hook_name: The hook name to be dispatched + :param \*args: Positional arguments, will be passed to hook callbacks + :param \*\*kwargs: Keyword arguments, will be passed to hook callbacks + :return: A concatenation of all callbacks\ + responses marked as safe (conditionally) + :rtype: str + """ + try: + templatehook = getattr(module, hook_name) + except AttributeError: + return "" + + return format_html_join( + sep="\n", + format_string="{}", + args_generator=((response,) for response in templatehook(*args, **kwargs)), + ) diff --git a/hypha/settings/django.py b/hypha/settings/django.py index 8682287c4d..2e48c9840f 100644 --- a/hypha/settings/django.py +++ b/hypha/settings/django.py @@ -7,6 +7,7 @@ # Application definition INSTALLED_APPS = [ "scout_apm.django", + "extensions.django_hooks", "hypha.cookieconsent", "hypha.images", "hypha.core.apps.CoreAppConfig", From 00ba65d055ce76c6e091132c62e0be358168b17b Mon Sep 17 00:00:00 2001 From: Frank Duncan Date: Tue, 21 May 2024 13:44:25 -0500 Subject: [PATCH 3/7] Make migration resistent to model changes This migration loads up Users to update their groups. However, if the User class has been changed in some way, and the migration runs from a clean database, it will fail with the error that you cannot select the new field from the database (as it hasn't been created!) This change makes it so that only the groups will be loaded, which is the only part of the User model that is needed for the migration. --- hypha/apply/users/migrations/0012_set_applicant_group.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/hypha/apply/users/migrations/0012_set_applicant_group.py b/hypha/apply/users/migrations/0012_set_applicant_group.py index 0358424960..c39463b539 100644 --- a/hypha/apply/users/migrations/0012_set_applicant_group.py +++ b/hypha/apply/users/migrations/0012_set_applicant_group.py @@ -11,7 +11,7 @@ def set_group(apps, schema_editor): User = get_user_model() applicant_group = Group.objects.get(name=APPLICANT_GROUP_NAME) - applicants = User.objects.exclude(applicationsubmission=None) + applicants = User.objects.exclude(applicationsubmission=None).only("groups") for user in applicants: if not user.is_apply_staff: user.groups.add(applicant_group) @@ -21,7 +21,9 @@ def set_group(apps, schema_editor): def unset_group(apps, schema_editor): User = get_user_model() applicant_group = Group.objects.get(name=APPLICANT_GROUP_NAME) - applicants = User.objects.filter(groups__name=APPLICANT_GROUP_NAME).all() + applicants = ( + User.objects.filter(groups__name=APPLICANT_GROUP_NAME).all().only("groups") + ) for user in applicants: user.groups.remove(applicant_group) user.save() From f9526397729ac29367a2ba18e812d701495a69bf Mon Sep 17 00:00:00 2001 From: Frank Duncan Date: Fri, 17 May 2024 09:21:51 -0500 Subject: [PATCH 4/7] Add a template hook for extensions to add to This gives any extension the ability to add css and javascript to every page in hypha. --- hypha/templates/base.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/hypha/templates/base.html b/hypha/templates/base.html index 189de7ad51..e069f526eb 100644 --- a/hypha/templates/base.html +++ b/hypha/templates/base.html @@ -1,4 +1,4 @@ -{% load i18n static wagtailcore_tags wagtailimages_tags hijack cookieconsent_tags heroicons %} +{% load i18n static wagtailcore_tags wagtailimages_tags hijack cookieconsent_tags heroicons hooks_tags %} {% wagtail_site as current_site %} {% get_current_language as LANGUAGE_CODE %} {% get_current_language_bidi as LANGUAGE_BIDI %} @@ -73,6 +73,7 @@ + {% hook 'hypha_extension_head' %} {% include "includes/head_end.html" %} From f970bf1a1341e8a876d128c3aab30e494e6e2909 Mon Sep 17 00:00:00 2001 From: Frank Duncan Date: Fri, 17 May 2024 09:22:27 -0500 Subject: [PATCH 5/7] Add template hooks for wagtail user creation/editing Allows extensions that want to add new fields to users to have those fields present in the wagtail admin. --- hypha/apply/users/templates/wagtailusers/users/create.html | 3 +++ hypha/apply/users/templates/wagtailusers/users/edit.html | 2 ++ 2 files changed, 5 insertions(+) diff --git a/hypha/apply/users/templates/wagtailusers/users/create.html b/hypha/apply/users/templates/wagtailusers/users/create.html index c0647508b3..043bba431c 100644 --- a/hypha/apply/users/templates/wagtailusers/users/create.html +++ b/hypha/apply/users/templates/wagtailusers/users/create.html @@ -1,5 +1,6 @@ {% extends "wagtailusers/users/create.html" %} {% load static %} +{% load hooks_tags %} {% block fields %} {% if form.separate_username_field %} @@ -8,6 +9,8 @@ {% include "wagtailadmin/shared/field_as_li.html" with field=form.email %} {% include "wagtailadmin/shared/field_as_li.html" with field=form.full_name %} + {% hook 'wagtail_user_edit' %} + {% comment %} First/last name hidden input with dummy values because.. Wagtail admin See hypha.apply.users.forms.CustomUserCreationForm diff --git a/hypha/apply/users/templates/wagtailusers/users/edit.html b/hypha/apply/users/templates/wagtailusers/users/edit.html index 75a4cea040..7e23c65562 100644 --- a/hypha/apply/users/templates/wagtailusers/users/edit.html +++ b/hypha/apply/users/templates/wagtailusers/users/edit.html @@ -3,6 +3,7 @@ {% load wagtailimages_tags %} {% load users_tags %} {% load static %} +{% load hooks_tags %} {% load i18n %} {% block content %} @@ -45,6 +46,7 @@ {% endif %} {% include "wagtailadmin/shared/field_as_li.html" with field=form.email %} {% include "wagtailadmin/shared/field_as_li.html" with field=form.full_name %} + {% hook 'wagtail_user_edit' %} {% block extra_fields %}{% endblock extra_fields %} {% comment %} From 904aceaf4d5c74c857629b6eca967847540e8b93 Mon Sep 17 00:00:00 2001 From: Frank Duncan Date: Thu, 16 May 2024 10:45:06 -0500 Subject: [PATCH 6/7] Add extension to add newsletter_signup to users --- .../ots/user_newsletter_signup/__init__.py | 0 extensions/ots/user_newsletter_signup/apps.py | 27 +++++++++++++++ .../migrations/0024_user_newsletter_signup.py | 34 +++++++++++++++++++ .../migrations/__init__.py | 0 .../ots/user_newsletter_signup/models.py | 6 ++++ .../ots/user_newsletter_signup/signals.py | 24 +++++++++++++ .../static/css/user_newsletter_signup.css | 3 ++ .../static/js/user_newsletter_signup.js | 9 +++++ .../user_newsletter_signup/template_hooks.py | 15 ++++++++ .../templates/hypha_extension_head.html | 3 ++ .../templates/wagtail_user_edit.html | 1 + .../user_newsletter_signup/tests/__init__.py | 1 + .../tests/test_newsletter.py | 22 ++++++++++++ hypha/settings/extensions.py | 3 +- pyproject.toml | 3 +- 15 files changed, 149 insertions(+), 2 deletions(-) create mode 100644 extensions/ots/user_newsletter_signup/__init__.py create mode 100644 extensions/ots/user_newsletter_signup/apps.py create mode 100644 extensions/ots/user_newsletter_signup/migrations/0024_user_newsletter_signup.py create mode 100644 extensions/ots/user_newsletter_signup/migrations/__init__.py create mode 100644 extensions/ots/user_newsletter_signup/models.py create mode 100644 extensions/ots/user_newsletter_signup/signals.py create mode 100644 extensions/ots/user_newsletter_signup/static/css/user_newsletter_signup.css create mode 100644 extensions/ots/user_newsletter_signup/static/js/user_newsletter_signup.js create mode 100644 extensions/ots/user_newsletter_signup/template_hooks.py create mode 100644 extensions/ots/user_newsletter_signup/templates/hypha_extension_head.html create mode 100644 extensions/ots/user_newsletter_signup/templates/wagtail_user_edit.html create mode 100644 extensions/ots/user_newsletter_signup/tests/__init__.py create mode 100644 extensions/ots/user_newsletter_signup/tests/test_newsletter.py diff --git a/extensions/ots/user_newsletter_signup/__init__.py b/extensions/ots/user_newsletter_signup/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/extensions/ots/user_newsletter_signup/apps.py b/extensions/ots/user_newsletter_signup/apps.py new file mode 100644 index 0000000000..67b3ca3a25 --- /dev/null +++ b/extensions/ots/user_newsletter_signup/apps.py @@ -0,0 +1,27 @@ +from django.apps import AppConfig +from django.conf import settings + + +class UserNewsletterSignupConfig(AppConfig): + name = "extensions.ots.user_newsletter_signup" + label = "extension_user_newsletter_signup" + + def ready(self): + from extensions.django_hooks.templatehook import hook + + from .template_hooks import hypha_extension_head, wagtail_user_edit + + hook.register("wagtail_user_edit", wagtail_user_edit) + hook.register("hypha_extension_head", hypha_extension_head) + + from django.forms.fields import BooleanField + + from hypha.apply.users.forms import ProfileForm + + ProfileForm.Meta.fields.append("newsletter_signup") + field = BooleanField( + required=False, + label="Yes, I'd like to receive occasional emails from %s about their mission and programs." % settings.ORG_SHORT_NAME, + ) + field.widget.attrs.update({"class": "profile_newsletter_signup"}) + ProfileForm.base_fields["newsletter_signup"] = field diff --git a/extensions/ots/user_newsletter_signup/migrations/0024_user_newsletter_signup.py b/extensions/ots/user_newsletter_signup/migrations/0024_user_newsletter_signup.py new file mode 100644 index 0000000000..580f123d96 --- /dev/null +++ b/extensions/ots/user_newsletter_signup/migrations/0024_user_newsletter_signup.py @@ -0,0 +1,34 @@ +# Generated by Django 4.2.11 on 2024-05-16 11:49 + +from django.db import migrations, models +from hypha.apply.users.models import User + + +def add_newsletter_signup_field(apps, schema_editor): + field = models.BooleanField(default=True) + field.column = "newsletter_signup" + schema_editor.add_field( + User, + field, + ) + + +def remove_newsletter_signup_field(apps, schema_editor): + field = models.BooleanField(default=True) + field.column = "newsletter_signup" + schema_editor.remove_field( + User, + field, + ) + + +class Migration(migrations.Migration): + dependencies = [ + ("users", "0023_merge_0021_groupdesc_0022_confirmaccesstoken"), + ] + + operations = [ + migrations.RunPython( + add_newsletter_signup_field, remove_newsletter_signup_field + ) + ] diff --git a/extensions/ots/user_newsletter_signup/migrations/__init__.py b/extensions/ots/user_newsletter_signup/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/extensions/ots/user_newsletter_signup/models.py b/extensions/ots/user_newsletter_signup/models.py new file mode 100644 index 0000000000..b04c54709e --- /dev/null +++ b/extensions/ots/user_newsletter_signup/models.py @@ -0,0 +1,6 @@ +from django.db import models + +# from hypha.apply.users.forms import ProfileForm +from hypha.apply.users.models import User + +User.add_to_class("newsletter_signup", models.BooleanField(default=True)) diff --git a/extensions/ots/user_newsletter_signup/signals.py b/extensions/ots/user_newsletter_signup/signals.py new file mode 100644 index 0000000000..2458003d4f --- /dev/null +++ b/extensions/ots/user_newsletter_signup/signals.py @@ -0,0 +1,24 @@ +from django.db.models.signals import post_save, pre_save +from django.dispatch import receiver + +from hypha.apply.users.models import User + + +@receiver(post_save, sender=User) +def user_post_save(sender, instance, created, update_fields, **kwargs): + if created: + if instance.newsletter_signup: + print("User added with newsletter being signed up!") + + +@receiver(pre_save, sender=User) +def user_pre_save(sender, instance, update_fields, **kwargs): + if ( + instance.id + and User.objects.get(id=instance.id).newsletter_signup + != instance.newsletter_signup + ): + if instance.newsletter_signup: + print("Someone is opting in to the newsletter signup") + else: + print("Someone is opting out of the newsletter signup!") diff --git a/extensions/ots/user_newsletter_signup/static/css/user_newsletter_signup.css b/extensions/ots/user_newsletter_signup/static/css/user_newsletter_signup.css new file mode 100644 index 0000000000..ebbd5fa274 --- /dev/null +++ b/extensions/ots/user_newsletter_signup/static/css/user_newsletter_signup.css @@ -0,0 +1,3 @@ +input.profile_newsletter_signup { + border: 5px solid green; +} diff --git a/extensions/ots/user_newsletter_signup/static/js/user_newsletter_signup.js b/extensions/ots/user_newsletter_signup/static/js/user_newsletter_signup.js new file mode 100644 index 0000000000..87bf811c8f --- /dev/null +++ b/extensions/ots/user_newsletter_signup/static/js/user_newsletter_signup.js @@ -0,0 +1,9 @@ +"use strict"; + +(function ($) { + $(function () { + $(".profile_newsletter_signup").click(function () { + window.alert("You clicked me!"); + }); + }); +})(jQuery); diff --git a/extensions/ots/user_newsletter_signup/template_hooks.py b/extensions/ots/user_newsletter_signup/template_hooks.py new file mode 100644 index 0000000000..5252e5e971 --- /dev/null +++ b/extensions/ots/user_newsletter_signup/template_hooks.py @@ -0,0 +1,15 @@ +from django.template.loader import render_to_string + + +def wagtail_user_edit(context, *args, **kwargs): + return render_to_string( + "wagtail_user_edit.html", + context.flatten(), + ) + + +def hypha_extension_head(context, *args, **kwargs): + return render_to_string( + "hypha_extension_head.html", + context.flatten(), + ) diff --git a/extensions/ots/user_newsletter_signup/templates/hypha_extension_head.html b/extensions/ots/user_newsletter_signup/templates/hypha_extension_head.html new file mode 100644 index 0000000000..901215dded --- /dev/null +++ b/extensions/ots/user_newsletter_signup/templates/hypha_extension_head.html @@ -0,0 +1,3 @@ +{% load static %} + + diff --git a/extensions/ots/user_newsletter_signup/templates/wagtail_user_edit.html b/extensions/ots/user_newsletter_signup/templates/wagtail_user_edit.html new file mode 100644 index 0000000000..04138a67aa --- /dev/null +++ b/extensions/ots/user_newsletter_signup/templates/wagtail_user_edit.html @@ -0,0 +1 @@ +{% include "wagtailadmin/shared/field_as_li.html" with field=form.newsletter_signup %} diff --git a/extensions/ots/user_newsletter_signup/tests/__init__.py b/extensions/ots/user_newsletter_signup/tests/__init__.py new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/extensions/ots/user_newsletter_signup/tests/__init__.py @@ -0,0 +1 @@ + diff --git a/extensions/ots/user_newsletter_signup/tests/test_newsletter.py b/extensions/ots/user_newsletter_signup/tests/test_newsletter.py new file mode 100644 index 0000000000..ff70181b57 --- /dev/null +++ b/extensions/ots/user_newsletter_signup/tests/test_newsletter.py @@ -0,0 +1,22 @@ +from django.conf import settings +from django.core import mail +from django.test import TestCase +from django.urls import reverse + +from hypha.apply.utils.testing.tests import BaseViewTestCase + +from hypha.apply.users.tests.factories import UserFactory, SuperUserFactory + + +class TestProfileViewNewsletter(TestCase): + def test_newsletter_on_profile_page(self): + user = UserFactory() + self.client.force_login(user) + response = self.client.get(reverse("users:account"), follow=True) + self.assertContains(response, "like to receive occasional emails") + + def test_newsletter_on_user_add_page(self): + user = SuperUserFactory() + self.client.force_login(user) + response = self.client.get(reverse("wagtailusers_users:add"), follow=True) + self.assertContains(response, "newsletter") diff --git a/hypha/settings/extensions.py b/hypha/settings/extensions.py index 98700289d2..7518a1fa3d 100644 --- a/hypha/settings/extensions.py +++ b/hypha/settings/extensions.py @@ -1,4 +1,5 @@ # Add enabled extensions here -EXTENSIONS_APPS = [] +EXTENSION_APPS = ["extensions.ots.user_newsletter_signup"] # Add configuration for extensions below here +WAGTAIL_USER_CUSTOM_FIELDS = ["full_name", "newsletter_signup"] diff --git a/pyproject.toml b/pyproject.toml index 942ba0d70e..a1dbcb5142 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,8 @@ python_files = [ '*_tests.py', ] testpaths = [ - "hypha" + "hypha", + "extensions/ots/user_newsletter_signup", ] filterwarnings = [ 'ignore::DeprecationWarning', From 0001e0087ce4524602447042be88cf28f18136a6 Mon Sep 17 00:00:00 2001 From: Frank Duncan Date: Thu, 26 Sep 2024 21:05:33 -0500 Subject: [PATCH 7/7] Running `make fmt` --- extensions/ots/user_newsletter_signup/apps.py | 3 ++- .../ots/user_newsletter_signup/tests/test_newsletter.py | 6 +----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/extensions/ots/user_newsletter_signup/apps.py b/extensions/ots/user_newsletter_signup/apps.py index 67b3ca3a25..b7acdf8d35 100644 --- a/extensions/ots/user_newsletter_signup/apps.py +++ b/extensions/ots/user_newsletter_signup/apps.py @@ -21,7 +21,8 @@ def ready(self): ProfileForm.Meta.fields.append("newsletter_signup") field = BooleanField( required=False, - label="Yes, I'd like to receive occasional emails from %s about their mission and programs." % settings.ORG_SHORT_NAME, + label="Yes, I'd like to receive occasional emails from %s about their mission and programs." + % settings.ORG_SHORT_NAME, ) field.widget.attrs.update({"class": "profile_newsletter_signup"}) ProfileForm.base_fields["newsletter_signup"] = field diff --git a/extensions/ots/user_newsletter_signup/tests/test_newsletter.py b/extensions/ots/user_newsletter_signup/tests/test_newsletter.py index ff70181b57..b000a20c2c 100644 --- a/extensions/ots/user_newsletter_signup/tests/test_newsletter.py +++ b/extensions/ots/user_newsletter_signup/tests/test_newsletter.py @@ -1,11 +1,7 @@ -from django.conf import settings -from django.core import mail from django.test import TestCase from django.urls import reverse -from hypha.apply.utils.testing.tests import BaseViewTestCase - -from hypha.apply.users.tests.factories import UserFactory, SuperUserFactory +from hypha.apply.users.tests.factories import SuperUserFactory, UserFactory class TestProfileViewNewsletter(TestCase):