Skip to main content

splent_feature_legal

The legal feature of a SPLENT product line.

A SPLENT feature is a Python package that a product composes with others: its models, routes, services, templates and hooks are registered when the product starts. What this feature provides and requires is listed in the [tool.splent.contract] section of pyproject.toml, which splent feature:contract --write keeps in sync with the source.

What it does

The legal documents of the site: the privacy notice, the terms of use, the cookie policy, the legal notice and any other. Each one is served at /legal/<slug> in the version in force, and gets a link in the footer through the layout.footer hook, privacy notice first, then terms, cookies, legal notice and the rest. A document with no version in force answers 404 and has no footer link.

Versions

A document's words live in versions. A version is a draft until it is published; publishing gives it the next number (1, 2, 3...) and the date it comes into force, which is the day the panel planned for it or the moment it is published, never earlier. From then on it is final. The models check what the database holds when the session flushes, so nothing that goes through the ORM edits or deletes a published version or its texts, or adds a text to it: not the panel, a seeder, the admin feature's generic CRUD or code in a shell, and a bulk UPDATE, DELETE or INSERT through the ORM is refused for versions and texts altogether. SQL sent to the database past the ORM is beyond the models' reach. A typo fixed is a new version, so what a page said on a given day, and what each account accepted, can always be shown again.

The numbers follow the days the versions come into force. A version cannot come into force before one already scheduled, which it would replace for good: the panel refuses it and asks for that day or a later one. A version published for the same day as a scheduled one takes its place, which is how a scheduled version is corrected; the one it replaced is marked as never in force.

  • /legal/<slug> shows the version in force, and says when a later one is scheduled.
  • /legal/<slug>/versions lists every published version, newest first.
  • /legal/<slug>/versions/<number> shows one of them as it was, marked as replaced when a later one is in force.

Each version holds one text per language of the product (BABEL_SUPPORTED_LOCALES). A reader gets the text in their language, else in the product's default, else in any the version has. A text is stored HTML, drawn through the framework's rich_text filter, which keeps the formatting and drops scripts, event handlers and javascript: links.

Publishing sends legal-document-published with document, version and requires_reacceptance, for a feature that tells the accounts about the change.

Acceptance

A document can be asked at sign-up (its "Asked at sign-up" setting). Then:

  • Sign-up. legal adds a box to signup's form (register_signup_field, signup 0.3.0), last, after the password: "I have read: Privacy notice (version 3). I accept: Terms of use (version 2).", with a link to each version. Without the box ticked, or when the versions changed while the form was open, no account is created. The acceptance is recorded on auth's user-registered.
  • Afterwards. A signed-in account that never accepted a required document, or that accepted an earlier version when a later one in force asks for acceptance again ("Ask the accounts that accepted an earlier version to accept this one"), is sent to /legal/accept before any other page, and comes back where it was going once it accepts. A request for JSON gets 403 with accept_url. The legal pages, logout and the static files stay open, and so does any page a feature keeps open with register_gate_exemption("profile.delete") (registry.py, looked up by name like the other two registries) or the product lists in LEGAL_GATE_OPEN_ENDPOINTS: an account that does not accept still deletes itself, exports its data or changes the language it reads in. LEGAL_ACCEPTANCE_GATE = false turns this off, for a product that asks only at sign-up. A version that does not ask again (a typo fixed) asks nobody.
  • The record. Each acceptance names the account, the version, the moment and the channel: signup, prompt (the acceptance page, for a document the account had never accepted), reaccept (the same page, after a version asked again), or the name another feature passes. An account sees its own at /legal/accepted, and the panel counts them per version. They go with the account, on auth's user-deleting.

A feature that opens accounts outside signup's form (an identity provider's "complete your sign-up" step) asks LegalService through the service locator, without importing legal: acceptance_box() draws the same box as HTML, acceptance_error(posted) says why what was posted does not count (or None), and accept_token(user_id, posted, "orcid") records it once the account exists. See the docstring of services.py.

Third parties and cookies

Each feature declares, from its init_feature, the outside services it sends personal data to and the cookies it sets:

register_third_party(
    "zenodo",
    "Zenodo (CERN)",
    purpose=_l("Publishes the datasets you deposit and gives them a DOI."),
    data=[_l("Names, affiliations and ORCID iDs of the authors")],
    country="Switzerland",
    legal_basis=_l("Performance of the deposit you ask for"),
    privacy_url="https://about.zenodo.org/privacy-policy/",
    hosts=["zenodo.org"],
)
register_cookie(
    "remember_token",
    category="necessary",  # necessary, preferences or analytics
    purpose=_l("Keeps you signed in when you ask the site to remember you."),
    duration=_l("1 year"),
)

The privacy notice (a document of kind privacy) lists the declared third parties under its own text, and the cookie policy (kind cookies) the declared cookies by category, so nobody keeps a copy by hand that drifts from the code. The lists are part of the version: publishing keeps them as the features declare them at that moment, in every language of the product, and each version shows its own, so an earlier version reads as it did and the one in force changes only with a new version. When the features declare something else (a deployment adds a service or a cookie), the panel marks the document "List out of date" and says so on its page; a new version, with the same text if nothing else changes, brings the list up to date, and asks the accounts again when it is told to. An empty list is not drawn (it would claim there are none). LegalService.third_party_hosts() gives every declared host, for a content security policy or a test of the hosts a page contacts. legal declares the framework's session cookie itself, under the name and lifetime the product gives it.

legal is optional in the product line, so a feature that declares does not import registry.py with an import statement: it looks it up by name and does nothing when the product has no legal (the docstring of registry.py has the five lines). The same goes for register_gate_exemption.

Seeding a product's texts

legal seeds a neutral privacy notice and cookie policy that name no organisation, so a new site starts with the pages its footer links to. A product with its own wording takes that seeder over from src/<product>/seeders.py and seeds its documents with seeding.seed_documents:

from splent_framework.seeders.BaseSeeder import BaseSeeder
from splent_io.splent_feature_legal.seeding import seed_documents

DOCUMENTS = [
    {
        "slug": "privacy",
        "kind": "privacy",
        "title": "Privacy notice",
        "acceptance_required": True,
        "versions": [
            {"texts": {"en": ("Privacy notice", "<p>...</p>"),
                       "es": ("Aviso de privacidad", "<p>...</p>")}},
        ],
    },
]


class ProductLegalSeeder(BaseSeeder):
    replaces = ("splent_io.splent_feature_legal.seeders",)

    def run(self):
        seed_documents(DOCUMENTS)

Import the function, never a seeder class: db:seed runs every seeder class it finds in the module. A document whose slug is there already is left as it is. A version with "draft": True is seeded as a draft, for a text someone has to review (the data protection officer, say) before it is published from the panel.

Upgrading from 0.2

The migration turns each legal page into a document: a published page becomes its version 1, in force since it was last updated, and an unpublished one a draft. Its text is carried over as it is and, being published, is final from then on, so a page that should not stay in force (a text another organisation wrote, left in the database) is deleted from legal_page before upgrading, and the product's seeder writes the new one. The kind comes from the slug: the privacy-policy page the catalogue seeded becomes the privacy notice, and so do politica-de-privacidad and the other usual names in English and Spanish (terms, cookies, legal notice); any other page is "other", for the panel to set. A migrated version lists nothing, as before, and the panel marks it until a new version brings the lists. No document is asked at sign-up until the panel says so.

  • LegalPage is gone. A product seeder that built LegalPage rows seeds seeding.seed_documents instead (above), with the kind of each document, and replaces splent_io.splent_feature_legal.seeders.
  • A product that overrides legal/page.html gets page (the LegalDocument), version, text (the LegalDocumentText in the reader's language), in_force, scheduled, never_in_force, third_parties and cookie_groups. page.content, page.updated_at and page.published no longer exist.

The downgrade puts legal_page back with the text of each version in force, and keeps the four versioned tables, renamed with an _archive suffix: legal_page cannot hold the history, the other languages, the lists or the acceptances, and those are the record of what the site said and who agreed. Nothing is deleted; drop the archive by hand once it is not needed.

Access

The panel manages the documents at /admin/legal: their settings, their drafts, and publishing. A document that published a version is never deleted. The screens under /admin/legal open for the accounts whose role is admin or staff, the same pair the admin dashboard lets in. Any other signed-in account gets 403 and does not see the Legal pages link in the sidebar or the menu, and an anonymous visitor is sent to log in. The published versions under /legal and their footer links stay open to everyone, signed in or not, and a document with no version in force answers 404 to everyone. /legal/accept and /legal/accepted open for any signed-in account.

There is no setting for these roles, and a refinement cannot replace the views that check them, because it may add routes to this feature but never replace one of its own. Another feature can decide for these screens with an access policy registered for the prefix legal.admin (register_access_policy, splent_framework 1.20.0). The framework asks the policy before the roles. It answers True to let an account in, False to refuse it, or None to leave the decision to the roles, and the sidebar link follows the same answer. That is the way to let a group of accounts manage only this section, for example the people who keep the legal texts, without giving them a panel role. See Access control.

Without such a feature, give the person's account the staff role. Staff opens every panel screen that asks for PANEL_ROLES, not only this one, so a person who should manage nothing but the legal pages needs a policy instead.

splent feature:auth set-role someone@example.com staff

Use it in a product

splent feature:add splent_feature_legal
Setting Default What it does
LEGAL_ACCEPTANCE_GATE true Sends a signed-in account with a required text to accept to /legal/accept first. false asks only at sign-up.
LEGAL_GATE_OPEN_ENDPOINTS empty Endpoints, comma separated, that such an account still opens, besides legal's own pages and the ones the features keep open with register_gate_exemption.

Develop

splent feature:test splent_feature_legal
splent feature:contract splent_feature_legal

Release

splent feature:release splent_feature_legal --dry-run --bump patch
splent feature:release splent_feature_legal

The first command rehearses the release and changes nothing. The second publishes it to the git forge and to PyPI, and a PyPI version is permanent.

Metadata

Release files for splent-feature-legal 0.4.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for splent-feature-legal 0.4.1
File Size Uploaded Provenance
splent_feature_legal-0.4.1.tar.gz 69.7 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for splent-feature-legal 0.4.1
File Interpreter ABI Platform
splent_feature_legal-0.4.1-py3-none-any.whl Python 3 none any Details

Total release size: 160.2 kB

Security for splent-feature-legal 0.4.1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page