The site
Table of contents
- Introduction
- Naming the site
- Telling the time
- Building links for mails and documents
- Sending the reader back safely
- Linking to a page that may not be there
- Using another feature when it is there
- Choosing who manages a screen
- Reference
- See also
Introduction
Every feature sooner or later asks something about the site it runs in. What the site is called, what time it is there, which address a link in a mail should use, or whether another feature is installed. These helpers answer, so you never read the config for it yourself.
from splent_framework.site import local_today, site_name
from splent_framework.urls import external_url_for
subject = _("A reminder from %(site)s", site=site_name())
link = external_url_for("notes.show", note_id=note.id)
overdue = note.due_on < local_today()
Naming the site
from splent_framework.site import site_name
site_name() # "Example Lab"
site_name() is never empty. It looks in this order and takes the first answer.
- The name stored under
site_namein the site’s settings, which an editor in the panel can change. - The
SITE_NAMEconfig key. - The product’s name,
SPLENT_APP. - The app’s package.
Telling the time
from splent_framework.site import local_now, local_today, site_timezone
local_now() # now, on the site's clock, with its zone
local_now(naive=True) # the same, without the zone, for a column that keeps local time
local_today() # the site's calendar day
site_timezone() # ZoneInfo("Europe/Madrid")
Use these instead of datetime.now(), which is the server’s clock, or a zone written by hand. The zone is the product’s TIMEZONE, Europe/Madrid unless the product says otherwise. A name the system does not know falls back to that default.
The date, datetime and time_tag template filters show times in the same zone, see Internationalization.
Those filters read a datetime without a zone as UTC. A column you fill with
local_now(naive=True)shows shifted through them, so keep UTC in the columns a page shows that way.
Building links for mails and documents
from splent_framework.urls import external_url_for
link = external_url_for("notes.show", note_id=note.id)
# "https://notes.example.org/notes/12"
A link that leaves the site, in a mail, a feed or a PDF, must point at the site’s real address. url_for(..., _external=True) takes the host from the request’s Host header, which the client writes, so a reset link built that way can point anywhere.
external_url_for builds on the address the deployment names in SERVER_NAME instead. It is a template global too.
<a href="{{ external_url_for('notes.show', note_id=note.id) }}">{{ note.title }}</a>
Set the address in the deployment’s environment.
SERVER_NAME=notes.example.org
PREFERRED_URL_SCHEME=https
https is the default scheme. Behind a path prefix, name it too, SERVER_NAME=https://example.org/notes, and the prefix appears once in every link.
Without SERVER_NAME, in development, links use the address the request reached. Outside a request, in a command or a task, there is no address to use, so external_url_for raises RuntimeError. Set SERVER_NAME wherever tasks send mail.
site_root() gives the address alone, and absolute_url("/terms") puts a path on it.
Sending the reader back safely
from splent_framework.urls import safe_next_url
target = safe_next_url(request.args.get("next")) or url_for("notes.index")
return redirect(target)
safe_next_url answers the next only when it is a path on this site. A full URL, //elsewhere.example, its backslash spelling, a scheme such as javascript: or a control character gives None, so your form is never an open redirect.
Linking to a page that may not be there
from splent_framework.urls import has_endpoint
if has_endpoint("reviews.index"):
links.append(url_for("reviews.index"))
has_endpoint(name) says whether this product serves the endpoint. It is a template global too.
{% if has_endpoint("reviews.index") %}
<a href="{{ url_for('reviews.index') }}">{{ _("Reviews") }}</a>
{% endif %}
The contract scan reads such a link as an optional dependency.
Using another feature when it is there
from splent_framework.optional import optional_attr, optional_module
reviews = optional_module("splent_io.splent_feature_reviews.services")
if reviews is not None:
reviews.ReviewsService().count_for(item)
register_badge = optional_attr("splent_io.splent_feature_reviews.badges", "register_badge")
optional_module answers the module, or None when its feature is not installed. optional_attr answers one name of it, or the default you pass, which also covers an older release that lacks the name.
A module that is installed but fails while importing still raises, as a broken feature should.
The contract scan does not see these calls. List the feature under
requires.features_optionalin yourpyproject.tomlyourself, and the scan keeps the entry.
Choosing who manages a screen
from splent_framework.decorators.decorators import role_required
from splent_framework.roles import PANEL_ROLES
@notes_bp.route("/admin/notes")
@role_required(*PANEL_ROLES)
def admin_index():
...
| Constant | Roles | For |
|---|---|---|
PANEL_ROLES |
("admin", "staff") |
The site’s content, which the people who run it edit day to day |
ADMIN_ROLES |
("admin",) |
The site itself, its appearance, accounts, keys and settings |
Access control explains gates, links and policies.
Reference
| Call | Answers |
|---|---|
site_name(app=None) |
The site’s name, never empty |
site_timezone(app=None) |
The product’s ZoneInfo |
local_now(naive=False), local_today() |
Now and today on the site’s clock |
site_root() |
The site’s address, without a trailing slash |
absolute_url(path) |
path on that address |
external_url_for(endpoint, **values) |
url_for on that address |
safe_next_url(candidate) |
The path, or None |
has_endpoint(name) |
Whether the product serves name |
optional_module(name), optional_attr(name, attr, default=None) |
The module or name, or None |
| Setting | Default | Read from |
|---|---|---|
SITE_NAME |
(none) | The app’s config |
TIMEZONE |
Europe/Madrid |
The product’s config or the environment |
SERVER_NAME |
(none) | The environment only, so it never binds Flask’s routing to a host |
PREFERRED_URL_SCHEME |
https |
The environment |
SPLENT_SITE_SERVER_NAME |
(none) | The app’s config, when the environment has no SERVER_NAME |
See also
- Content helpers, slugs, previews, pages and the sitemap.
- Access control