Event Details Pages and Attached Files

This document describes how an event's details page is rendered, how to replace it with a custom page, and how to list the files attached to an event (for example PDFs) on that page.


How the details page is chosen

Every event is served at its permanent URL /events/<title-slug>-e<id> (for example /events/general-assembly-e2). The URL is the same regardless of which page renders it; replacing the page never changes the URL.

When an event is requested the platform:

  1. Returns 404 if the event does not exist or is deleted, or if it is not yet published (PostTime in the future) or has expired (ExpirationTime passed). Admins can preview with ?preview=true.
  2. Evaluates the property EventDetailsPageId.
    • Empty: the built-in view (platform resource Event.Details.View, which renders the partial view Event.Details) is used.
    • A custom page ID: that custom page is rendered instead.

The built-in view shows attached files

The built-in Event.Details partial view renders title, description, date and time, location, link, attached files, ticket price and cover image. The attached files appear as a "Vedhæftede filer" row with one link per file in FileRelations, and the row is omitted when the event has none. There is no setting that turns it on or off. The only per-resource configuration the view reads is the cover image size (cover_image_width, cover_image_height, cover_image_stretchmode).

A page that renders Event.Details itself must pass an event loaded with enough depth to include FileRelations (see below); otherwise the row is not shown.

Use a custom details page when the markup or layout of the attachments must differ from the built-in row.


Setting up a custom details page

  1. Create a custom page for the event details. Do not give it required page parameters; the event id arrives automatically (see below).
  2. Put the event markup, including the attachments block below, in the page content. The content is Piranya Script embedded in HTML, see Piranya Script.
  3. Open the Events module's customize view (the one that contains the event features) and set EventDetailsPageId to the numeric ID of the page. The value is evaluated as a Piranya Script expression, so a plain number works.
  4. To go back to the built-in view, clear the property.

Optionally set EventBrowsePageId the same way to replace the event overview page.

How the page gets the current event

The router puts the event id in the page's global parameters under the key event_id. It is available as a variable in the page script (the aliases eId and event_id are both accepted in URLs). Load the event with the query provider, using a depth that includes the file relations:

{%
var event = QueryProvider.Single type="Event" id=event_id depth=6 cache=true;
if (event == null) {
    respond code=404;
}
%}

This is the same call the built-in view uses. Without a sufficient depth the FileRelations list is not populated.


Where the attached files are

Event.FileRelations is a list of EntityFile objects, one per file attached to the event in the admin ("Vedhæftede filer").

Expression Meaning
event.FileRelations The attached files. Empty when nothing is attached.
rel.FileId ID of the file. Use it to build the download URL.
rel.File A FileItem describing the file.
rel.File.FileContentType MIME type, for example application/pdf.
rel.File.Metadata["title"] File name as shown in the file archive.
rel.File.Metadata["description"] Description entered on the file (may be empty).
rel.File.Metadata["extension"] File extension.
rel.File.Metadata["size"] Size in bytes.
rel.File.Metadata["last_modified"] Last modified time.
rel.File.Metadata["alternative_text"] Alternative text (mainly for images).
rel.File.FileGuid GUID of the file, usable in the guid file URL.

Because the list is read when the page is rendered, files that are added to or removed from the event in the admin appear and disappear without changing the page. Event and page queries are cached, so a change can take a few minutes to show on the public site.

Download URL

Use the Url tag with the File route:

{%Url File id=rel.FileId filename=rel.File.Metadata['title']%}

This produces /File/<id>/<filename>. Add download=true to force a download instead of opening the file in the browser:

{%Url File id=rel.FileId filename=rel.File.Metadata['title'] download=true%}

Use AbsoluteUrl instead of Url when a full URL including the domain is needed.


Complete example: attachments block

Place this in a custom details page where the files should appear. The built-in view already contains an equivalent row. It renders nothing when the event has no attachments.

{%
var event = QueryProvider.Single type="Event" id=event_id depth=6 cache=true;
if (event == null) {
    respond code=404;
}
%}

<h1>{%binding event.Title%}</h1>
<div class="description">{%binding event.Description%}</div>

{%if event.FileRelations.Count%}
<div class="attached-files">
    <h2>Documents</h2>
    <ul>
        {%foreach rel in event.FileRelations%}
        <li>
            <a href="{%Url File id=rel.FileId filename=rel.File.Metadata['title']%}" target="_blank" rel="noopener">
                {%binding rel.File.Metadata["description"] || rel.File.Metadata["title"] || "Attached file"%}
            </a>
            <span class="meta">{%binding rel.File.Metadata["extension"]%}</span>
        </li>
        {%endforeach%}
    </ul>
</div>
{%endif%}

The same pattern is used for attachments on blog posts (BlogPostDetails.aspx in the Blogs module).


File visibility and access

Whether a visitor without login can open an attached file is decided by the file itself, not by the event. Attaching a file to a published event does not make it public, and hiding the link does not protect it: anyone who has the URL gets the same answer as any other visitor.

For each request the file service checks, in this order:

  1. Admins can always open the file.
  2. IsPublic on the file ("Offentlig" in the file archive). When it is set, true allows everyone and false restricts the file.
  3. IsPublic on the folder. When the file has no value of its own, the nearest ancestor folder with a value decides.
  4. No value anywhere means the file is accessible by default. Files in the root folder are also accessible by default.
  5. Files owned by an account (account-scoped files) are only open to that account's users when they are not public.
  6. IsGuidRequiredForLookup on the file means the file can only be opened with the guid URL form (/File/<guid>/<filename>), not the numeric id URL.

A visitor who is not allowed to open a restricted file is redirected to the protected file route, which requires login. A logged-in user who is not allowed gets 403.

To make one attached file open for anonymous visitors without making other files public:

To check a file, open its URL in a private browser window. Responses to the file URL are cached for ten minutes, so a changed setting can take a short while to apply.


Access to Platform → Ressourcer

The Platform → Ressourcer list (platform resources such as Event.Details and Event.Details.View) requires the Unrestricted access level, which is the super-user level. A 403 there is expected for other users. The resources are embedded in the modules and cannot be created or deleted there; only their configuration and metadata can be edited. The custom page approach above does not need access to them.