Skip to content

Appendix

Some of Pinmok's built-in features are self-contained and not yet substantial enough to warrant their own chapters, yet they remain essential in day-to-day development. This appendix collects them in one place. As the project evolves, some of these features will grow into dedicated chapters, and the appendix will be updated accordingly.

Admin Context Extension

Admin pages often need to share global context data such as the current user's menu, site configuration, or application state. If each view handles this separately, the same logic becomes scattered across the project and is difficult to maintain. To address this, Pinmok overrides Django Admin’s each_context() method and introduces a dedicated signal mechanism.

The extend_admin_context Signal

This signal is sent by PinmokAdminSite each time each_context() is called. The sender is the PinmokAdminSite class itself. Receivers inject data by modifying the context dictionary directly — no return value is needed.

Signal parameters:

Parameter Type Description
sender type The signal sender, i.e. the PinmokAdminSite class
request HttpRequest The current request object
context dict The shared admin context dictionary; call update() to inject data

Example:

from django.dispatch import receiver
from pinmok.core.signals import extend_admin_context


@receiver(extend_admin_context)
def inject_my_context(sender, request, context, **kwargs):
    context.update({
        'my_key': 'my_value',
    })

The receiver signature must include **kwargs. This is a Django signal requirement that ensures compatibility if additional parameters are introduced in the future.


Email Service

Pinmok provides a built-in EmailService class that wraps Django's email sending logic. Its primary purpose is to move SMTP configuration out of settings.py and into the admin site settings, so that end users can manage mail configuration through the backend without touching the codebase. Developers call EmailService directly; the actual connection parameters are configured by the user in the admin.

Import path:

from pinmok.padmin.service.email import EmailService

The current version of EmailService covers the core sending interface, supporting both plain email delivery and template variable substitution, which is sufficient for most common use cases.

Configuration Priority

EmailService automatically selects which SMTP configuration to use during initialization:

Database config settings config In effect
Present Present Database config takes priority
Present Absent Database config
Absent Present settings config
Absent Absent Django defaults (sending may fail)

Sending a Plain Email

send(to, subject, content)

from pinmok.padmin.service.email import EmailService

service = EmailService()
service.send(
    to='user@example.com',
    subject='Hello',
    content='<p>This is a test email.</p>',
)

Sends an HTML email to one or more recipients. The sender address is read from the admin site configuration and does not need to be passed in. The email body supports HTML; Django sets content_subtype to html so the recipient's mail client renders it accordingly.

Parameters:

Parameter Type Description
to str \| list[str] Recipient address or list of addresses
subject str Email subject line; required — pass '' to send without a subject
content str Email body; HTML is supported

Return value: int — the number of messages successfully sent.


Sending a Template Email

send_with_template(to, subject, content, template_params=None)

service.send_with_template(
    to='user@example.com',
    subject='Hello ${username}, you have a new message',
    content='<p>Your verification code is <strong>${verify_code}</strong>. It expires in 10 minutes.</p>',
    template_params={
        'username': 'Tomas',
        'verify_code': '123456',
    }
)

Extends plain email sending with variable substitution. Before sending, all ${var_name} placeholders in subject and content are replaced with the corresponding values from template_params. Placeholders with no matching key are left as-is. The template content is supplied by the caller — EmailService only handles substitution and delivery.

If template_params is omitted or empty, subject and content are sent as-is, making the behavior identical to send().

Parameters:

Parameter Type Description
to str \| list[str] Recipient address or list of addresses
subject str Email subject line; supports placeholders; required
content str Email body; supports HTML and placeholders
template_params dict[str, str] \| None Variable substitution map; keys are variable names, values are substitution strings; defaults to None

Return value: int — the number of messages successfully sent.

Exceptions: Sending errors propagate as standard exceptions. It is good practice to wrap calls in a try/except block:

from pinmok.padmin.service.email import EmailService, EmailValueError

service = EmailService()
try:
    service.send_with_template(
        to='user@example.com',
        subject='Hello ${username}',
        content='<p>Code: ${verify_code}</p>',
        template_params={'username': 'Tomas', 'verify_code': '123456'},
    )
except EmailValueError as e:
    # Handle validation errors
    print(e)
except Exception as e:
    # Handle other sending errors
    print(e)

Admin URL Registration

Django admin views are not ordinary views — every admin URL must pass through login verification, permission checks, and CSRF protection, all of which are handled by AdminSite.admin_view(). Registering an admin view directly in the project's URL configuration, bypassing this wrapper, can cause functional errors at best and security vulnerabilities at worst.

Pinmok overrides AdminSite.get_urls() and establishes a convention: declare an admin_urlpatterns variable in your app's urls.py, and Pinmok will scan it at startup, wrap each URL with admin_view(), and register it into the admin URL configuration. Developers focus on the view logic itself; the framework handles the rest.

You do not need admin_urlpatterns in these cases:

  • The URL belongs to the frontend and has nothing to do with the admin — register it in the project's URL configuration as usual.
  • You have your own admin permission handling and do not need Pinmok to manage it.

Declaring Admin URLs

Declare a module-level variable named admin_urlpatterns in your app's urls.py, with a standard list of URL patterns as its value:

# myapp/urls.py
from django.urls import path
from . import views

admin_urlpatterns = [
    path('dashboard/', views.dashboard, name='myapp-dashboard'),
    path('export/', views.export, name='myapp-export'),
]

Pinmok scans all installed apps for this variable at startup and registers the patterns into the admin URL configuration. The URLs are mounted under the app's namespace:

/admin/myapp/dashboard/
/admin/myapp/export/

Automatic Permission Wrapping

For every URLPattern entry in the list, Pinmok automatically wraps the view function with admin_view(), enforcing login verification and permission checks on every request. No manual handling is required.

Using include()

If an entry is a URLResolver — that is, a URL group introduced via include() — Pinmok registers it as-is without applying admin_view(). This is an intentional escape hatch: when you have custom permission handling, use include() to bring in your own URL group and manage permissions yourself.

# myapp/urls.py
from django.urls import path, include
from . import views, api_urls

admin_urlpatterns = [
    # Plain view: Pinmok wraps this with admin_view() automatically
    path('dashboard/', views.dashboard, name='myapp-dashboard'),

    # Custom permission handling: Pinmok does not intervene
    path('api/', include(api_urls)),
]

Warning

When using include(), make sure the included URL group contains proper permission checks. Without them, the views will be accessible to all users.


PinmokPermissionMixin

PinmokPermissionMixin is a class-based view mixin for enforcing permission checks on views that cannot be covered by admin_view(). The most common use case is AJAX endpoints.

Usage

Inherit from PinmokPermissionMixin and declare a permission attribute on the class:

from pinmok.core.mixins import PinmokPermissionMixin
from django.views import View


class MyAjaxView(PinmokPermissionMixin, View):
    permission = 'myapp.can_do_something'

permission is optional. Setting it to None performs a login check only, without checking any specific permission.

Behavior

Permission checks run at the dispatch stage. If the check fails:

  • AJAX requests (identified by the X-Requested-With: XMLHttpRequest header) receive a JSON 401 response.
  • Regular requests are redirected to the login page.

Note

PinmokPermissionMixin uses Pinmok's own permission_checker, which is independent of Django's admin_view() mechanism. Do not mix the two. permission_checker supports custom replacement; see the Permissions chapter for details.