Skip to content
Closed
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
2 changes: 2 additions & 0 deletions docs/references/signals.md
Original file line number Diff line number Diff line change
@@ -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.

7 changes: 7 additions & 0 deletions extensions/django_hooks/README.md
Original file line number Diff line number Diff line change
@@ -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.
Empty file.
89 changes: 89 additions & 0 deletions extensions/django_hooks/docs/templatehook.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
TemplateHook
============

Adding a hook-point in ``main_app``'s template::

# my_main_app/templates/_base.html

{% load hooks_tags %}

<!DOCTYPE html>
<html>
<head>
#...

{% hook 'within_head' %}

#...
</head>
</html>

.. 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'<link rel="stylesheet" href="%s/app_hook/styles.css">' % settings.STATIC_URL)


# Example 2
def user_about_info(context, *args, **kwargs):
user = context['request'].user
return format_html(
"<b>{name}</b> {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/
139 changes: 139 additions & 0 deletions extensions/django_hooks/templatehook.py
Original file line number Diff line number Diff line change
@@ -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()
Empty file.
62 changes: 62 additions & 0 deletions extensions/django_hooks/templatetags/hooks_tags.py
Original file line number Diff line number Diff line change
@@ -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)),
)
Empty file.
28 changes: 28 additions & 0 deletions extensions/ots/user_newsletter_signup/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
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
Original file line number Diff line number Diff line change
@@ -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
)
]
Empty file.
6 changes: 6 additions & 0 deletions extensions/ots/user_newsletter_signup/models.py
Original file line number Diff line number Diff line change
@@ -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))
24 changes: 24 additions & 0 deletions extensions/ots/user_newsletter_signup/signals.py
Original file line number Diff line number Diff line change
@@ -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!")
Loading