splent-feature-events 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- splent_feature_events-0.1.0/MANIFEST.in +8 -0
- splent_feature_events-0.1.0/PKG-INFO +5 -0
- splent_feature_events-0.1.0/pyproject.toml +52 -0
- splent_feature_events-0.1.0/setup.cfg +4 -0
- splent_feature_events-0.1.0/src/splent_feature_events.egg-info/PKG-INFO +5 -0
- splent_feature_events-0.1.0/src/splent_feature_events.egg-info/SOURCES.txt +28 -0
- splent_feature_events-0.1.0/src/splent_feature_events.egg-info/dependency_links.txt +1 -0
- splent_feature_events-0.1.0/src/splent_feature_events.egg-info/top_level.txt +1 -0
- splent_feature_events-0.1.0/src/splent_io/splent_feature_events/__init__.py +25 -0
- splent_feature_events-0.1.0/src/splent_io/splent_feature_events/assets/dist/splent_feature_events.bundle.js +20 -0
- splent_feature_events-0.1.0/src/splent_io/splent_feature_events/assets/dist/splent_feature_events.bundle.js.map +1 -0
- splent_feature_events-0.1.0/src/splent_io/splent_feature_events/commands.py +21 -0
- splent_feature_events-0.1.0/src/splent_io/splent_feature_events/config.py +19 -0
- splent_feature_events-0.1.0/src/splent_io/splent_feature_events/forms.py +6 -0
- splent_feature_events-0.1.0/src/splent_io/splent_feature_events/hooks.py +23 -0
- splent_feature_events-0.1.0/src/splent_io/splent_feature_events/migrations/alembic.ini +36 -0
- splent_feature_events-0.1.0/src/splent_io/splent_feature_events/migrations/env.py +9 -0
- splent_feature_events-0.1.0/src/splent_io/splent_feature_events/migrations/script.py.mako +24 -0
- splent_feature_events-0.1.0/src/splent_io/splent_feature_events/migrations/versions/b6efdeb9b074_splent_feature_events.py +48 -0
- splent_feature_events-0.1.0/src/splent_io/splent_feature_events/models.py +29 -0
- splent_feature_events-0.1.0/src/splent_io/splent_feature_events/repositories.py +19 -0
- splent_feature_events-0.1.0/src/splent_io/splent_feature_events/routes.py +184 -0
- splent_feature_events-0.1.0/src/splent_io/splent_feature_events/seeders.py +54 -0
- splent_feature_events-0.1.0/src/splent_io/splent_feature_events/services.py +13 -0
- splent_feature_events-0.1.0/src/splent_io/splent_feature_events/signals.py +19 -0
- splent_feature_events-0.1.0/src/splent_io/splent_feature_events/templates/events/admin/form.html +73 -0
- splent_feature_events-0.1.0/src/splent_io/splent_feature_events/templates/events/admin/list.html +68 -0
- splent_feature_events-0.1.0/src/splent_io/splent_feature_events/templates/events/detail.html +55 -0
- splent_feature_events-0.1.0/src/splent_io/splent_feature_events/templates/events/index.html +10 -0
- splent_feature_events-0.1.0/src/splent_io/splent_feature_events/templates/events/list.html +28 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Compiled frontend bundles
|
|
2
|
+
recursive-include src/splent_io/splent_feature_events/assets/dist *.js *.css *.map
|
|
3
|
+
|
|
4
|
+
# Jinja templates (feature views + hook fragments)
|
|
5
|
+
recursive-include src/splent_io/splent_feature_events/templates *.html
|
|
6
|
+
|
|
7
|
+
# Alembic migration config and scripts
|
|
8
|
+
recursive-include src/splent_io/splent_feature_events/migrations *.py *.ini *.mako
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=80.3.1", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "splent_feature_events"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
requires-python = ">=3.13"
|
|
10
|
+
|
|
11
|
+
dependencies = [
|
|
12
|
+
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[tool.setuptools]
|
|
16
|
+
package-dir = { "" = "src" }
|
|
17
|
+
include-package-data = true
|
|
18
|
+
|
|
19
|
+
[tool.setuptools.packages.find]
|
|
20
|
+
where = ["src"]
|
|
21
|
+
exclude = ["*.tests", "*.tests.*"]
|
|
22
|
+
|
|
23
|
+
[tool.splent]
|
|
24
|
+
cli_version = "1.11.0"
|
|
25
|
+
|
|
26
|
+
# ── Feature Contract (auto-generated) ────────────────────────────────────────
|
|
27
|
+
# Do not edit manually — re-run `splent feature:contract --write` to refresh.
|
|
28
|
+
[tool.splent.contract]
|
|
29
|
+
description = "Events — talks, workshops, competitions and ceremonies (public list/detail + admin CRUD)"
|
|
30
|
+
|
|
31
|
+
[tool.splent.contract.provides]
|
|
32
|
+
routes = ["/admin/events", "/admin/events/<int:event_id>/delete", "/admin/events/<int:event_id>/edit", "/admin/events/<int:event_id>/move", "/admin/events/new", "/events", "/events/<slug>"]
|
|
33
|
+
blueprints = []
|
|
34
|
+
models = ["Event"]
|
|
35
|
+
commands = ["hello"]
|
|
36
|
+
hooks = ["layout.authenticated_sidebar"]
|
|
37
|
+
services = ["EventsService"]
|
|
38
|
+
signals = []
|
|
39
|
+
translations = ["en", "es"]
|
|
40
|
+
docker = []
|
|
41
|
+
|
|
42
|
+
[tool.splent.contract.requires]
|
|
43
|
+
features = []
|
|
44
|
+
env_vars = []
|
|
45
|
+
signals = []
|
|
46
|
+
|
|
47
|
+
[tool.splent.contract.extensible]
|
|
48
|
+
services = ["EventsService"]
|
|
49
|
+
templates = ["events/admin/form.html", "events/admin/list.html", "events/detail.html", "events/index.html", "events/list.html"]
|
|
50
|
+
models = ["Event"]
|
|
51
|
+
hooks = ["layout.authenticated_sidebar"]
|
|
52
|
+
routes = false
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
MANIFEST.in
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/splent_feature_events.egg-info/PKG-INFO
|
|
4
|
+
src/splent_feature_events.egg-info/SOURCES.txt
|
|
5
|
+
src/splent_feature_events.egg-info/dependency_links.txt
|
|
6
|
+
src/splent_feature_events.egg-info/top_level.txt
|
|
7
|
+
src/splent_io/splent_feature_events/__init__.py
|
|
8
|
+
src/splent_io/splent_feature_events/commands.py
|
|
9
|
+
src/splent_io/splent_feature_events/config.py
|
|
10
|
+
src/splent_io/splent_feature_events/forms.py
|
|
11
|
+
src/splent_io/splent_feature_events/hooks.py
|
|
12
|
+
src/splent_io/splent_feature_events/models.py
|
|
13
|
+
src/splent_io/splent_feature_events/repositories.py
|
|
14
|
+
src/splent_io/splent_feature_events/routes.py
|
|
15
|
+
src/splent_io/splent_feature_events/seeders.py
|
|
16
|
+
src/splent_io/splent_feature_events/services.py
|
|
17
|
+
src/splent_io/splent_feature_events/signals.py
|
|
18
|
+
src/splent_io/splent_feature_events/assets/dist/splent_feature_events.bundle.js
|
|
19
|
+
src/splent_io/splent_feature_events/assets/dist/splent_feature_events.bundle.js.map
|
|
20
|
+
src/splent_io/splent_feature_events/migrations/alembic.ini
|
|
21
|
+
src/splent_io/splent_feature_events/migrations/env.py
|
|
22
|
+
src/splent_io/splent_feature_events/migrations/script.py.mako
|
|
23
|
+
src/splent_io/splent_feature_events/migrations/versions/b6efdeb9b074_splent_feature_events.py
|
|
24
|
+
src/splent_io/splent_feature_events/templates/events/detail.html
|
|
25
|
+
src/splent_io/splent_feature_events/templates/events/index.html
|
|
26
|
+
src/splent_io/splent_feature_events/templates/events/list.html
|
|
27
|
+
src/splent_io/splent_feature_events/templates/events/admin/form.html
|
|
28
|
+
src/splent_io/splent_feature_events/templates/events/admin/list.html
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
splent_io
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from splent_framework.blueprints.base_blueprint import create_blueprint
|
|
2
|
+
from splent_framework.nav.nav_registry import register_nav_item
|
|
3
|
+
from splent_framework.services.service_locator import register_service
|
|
4
|
+
|
|
5
|
+
from splent_io.splent_feature_events.services import EventsService
|
|
6
|
+
|
|
7
|
+
events_bp = create_blueprint(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def init_feature(app):
|
|
11
|
+
from splent_framework.assets.asset_registry import register_asset
|
|
12
|
+
|
|
13
|
+
# Events are managed through their OWN custom admin screens (see routes.py
|
|
14
|
+
# and hooks.py) — the WordPress-plugin pattern — instead of the generic
|
|
15
|
+
# admin resource, so it does not call register_admin_resource.
|
|
16
|
+
register_service(app, "EventsService", EventsService)
|
|
17
|
+
register_nav_item(key="events", label="Events", href="/events", order=10)
|
|
18
|
+
# Public event-detail stylesheet (token-driven; styled by the active skin).
|
|
19
|
+
register_asset(
|
|
20
|
+
"css", "events.assets", order=100, subfolder="css", filename="events.css"
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def inject_context_vars(app):
|
|
25
|
+
return {}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/******/ (() => { // webpackBootstrap
|
|
2
|
+
/*!*****************************************************************************************!*\
|
|
3
|
+
!*** ../splent_feature_events/src/splent_io/splent_feature_events/assets/js/scripts.js ***!
|
|
4
|
+
\*****************************************************************************************/
|
|
5
|
+
// Entry point for splent_feature_events frontend assets.
|
|
6
|
+
// Add your JavaScript here. Webpack compiles this into assets/dist/splent_feature_events.bundle.js
|
|
7
|
+
//
|
|
8
|
+
// To load the compiled bundle in the product layout, register it in hooks.py:
|
|
9
|
+
//
|
|
10
|
+
// from splent_framework.hooks.template_hooks import register_template_hook
|
|
11
|
+
// from flask import url_for
|
|
12
|
+
//
|
|
13
|
+
// def events_scripts():
|
|
14
|
+
// return '<script src="' + url_for("events.assets", subfolder="dist", filename="splent_feature_events.bundle.js") + '"></script>'
|
|
15
|
+
//
|
|
16
|
+
// register_template_hook("layout.scripts", events_scripts)
|
|
17
|
+
|
|
18
|
+
/******/ })()
|
|
19
|
+
;
|
|
20
|
+
//# sourceMappingURL=splent_feature_events.bundle.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"splent_feature_events.bundle.js","mappings":";;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","sources":["webpack://innosoft_app/../splent_feature_events/src/splent_io/splent_feature_events/assets/js/scripts.js"],"sourcesContent":["// Entry point for splent_feature_events frontend assets.\n// Add your JavaScript here. Webpack compiles this into assets/dist/splent_feature_events.bundle.js\n//\n// To load the compiled bundle in the product layout, register it in hooks.py:\n//\n// from splent_framework.hooks.template_hooks import register_template_hook\n// from flask import url_for\n//\n// def events_scripts():\n// return '<script src=\"' + url_for(\"events.assets\", subfolder=\"dist\", filename=\"splent_feature_events.bundle.js\") + '\"></script>'\n//\n// register_template_hook(\"layout.scripts\", events_scripts)\n"],"names":[],"sourceRoot":""}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CLI commands contributed by splent_feature_events.
|
|
3
|
+
|
|
4
|
+
These commands are auto-discovered by the framework and exposed in the
|
|
5
|
+
SPLENT CLI under the ``feature:events`` group.
|
|
6
|
+
|
|
7
|
+
Usage::
|
|
8
|
+
|
|
9
|
+
splent feature:events hello
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import click
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@click.command("hello")
|
|
16
|
+
def hello():
|
|
17
|
+
"""Example command — replace with your own."""
|
|
18
|
+
click.echo(" Hello from splent_feature_events!")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
cli_commands = [hello]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""
|
|
2
|
+
events feature configuration.
|
|
3
|
+
|
|
4
|
+
Injects environment variables into Flask app.config.
|
|
5
|
+
Add your feature's env vars here so the framework can track them.
|
|
6
|
+
|
|
7
|
+
To regenerate from source code: splent feature:inject-config splent_feature_events
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import os # noqa: F401 — used when adding env vars below
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def inject_config(app):
|
|
14
|
+
app.config.update(
|
|
15
|
+
{
|
|
16
|
+
# Add your feature's env vars here, e.g.:
|
|
17
|
+
# "MY_VAR": os.getenv("MY_VAR", "default_value"),
|
|
18
|
+
}
|
|
19
|
+
)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from flask import request, url_for
|
|
2
|
+
|
|
3
|
+
from splent_framework.hooks.template_hooks import register_template_hook
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def events_admin_link():
|
|
7
|
+
"""Sidebar entry for the Events management screen (the WP-plugin pattern)."""
|
|
8
|
+
active = (
|
|
9
|
+
"active"
|
|
10
|
+
if request.endpoint and request.endpoint.startswith("events.admin")
|
|
11
|
+
else ""
|
|
12
|
+
)
|
|
13
|
+
return (
|
|
14
|
+
f'<li class="sidebar-item {active}">'
|
|
15
|
+
f'<a class="sidebar-link" href="{url_for("events.admin_index")}">'
|
|
16
|
+
'<i class="align-middle" data-feather="calendar"></i> '
|
|
17
|
+
'<span class="align-middle">Events</span>'
|
|
18
|
+
"</a>"
|
|
19
|
+
"</li>"
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
register_template_hook("layout.authenticated_sidebar", events_admin_link)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
[alembic]
|
|
2
|
+
script_location = migrations
|
|
3
|
+
|
|
4
|
+
[loggers]
|
|
5
|
+
keys = root,sqlalchemy,alembic
|
|
6
|
+
|
|
7
|
+
[handlers]
|
|
8
|
+
keys = console
|
|
9
|
+
|
|
10
|
+
[formatters]
|
|
11
|
+
keys = generic
|
|
12
|
+
|
|
13
|
+
[logger_root]
|
|
14
|
+
level = WARN
|
|
15
|
+
handlers = console
|
|
16
|
+
qualname =
|
|
17
|
+
|
|
18
|
+
[logger_sqlalchemy]
|
|
19
|
+
level = WARN
|
|
20
|
+
handlers =
|
|
21
|
+
qualname = sqlalchemy.engine
|
|
22
|
+
|
|
23
|
+
[logger_alembic]
|
|
24
|
+
level = INFO
|
|
25
|
+
handlers =
|
|
26
|
+
qualname = alembic
|
|
27
|
+
|
|
28
|
+
[handler_console]
|
|
29
|
+
class = StreamHandler
|
|
30
|
+
args = (sys.stderr,)
|
|
31
|
+
level = NOTSET
|
|
32
|
+
formatter = generic
|
|
33
|
+
|
|
34
|
+
[formatter_generic]
|
|
35
|
+
format = %(levelname)-5.5s [%(name)s] %(message)s
|
|
36
|
+
datefmt = %H:%M:%S
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Alembic migration environment for splent_feature_events."""
|
|
2
|
+
|
|
3
|
+
from splent_io.splent_feature_events import models # noqa
|
|
4
|
+
from splent_framework.migrations.feature_env import run_feature_migrations
|
|
5
|
+
|
|
6
|
+
FEATURE_NAME = "splent_feature_events"
|
|
7
|
+
FEATURE_TABLES = set()
|
|
8
|
+
|
|
9
|
+
run_feature_migrations(FEATURE_NAME, FEATURE_TABLES)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""${message}
|
|
2
|
+
|
|
3
|
+
Revision ID: ${up_revision}
|
|
4
|
+
Revises: ${down_revision | comma,n}
|
|
5
|
+
Create Date: ${create_date}
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
from alembic import op
|
|
9
|
+
import sqlalchemy as sa
|
|
10
|
+
${imports if imports else ""}
|
|
11
|
+
|
|
12
|
+
# revision identifiers, used by Alembic.
|
|
13
|
+
revision = ${repr(up_revision)}
|
|
14
|
+
down_revision = ${repr(down_revision)}
|
|
15
|
+
branch_labels = ${repr(branch_labels)}
|
|
16
|
+
depends_on = ${repr(depends_on)}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def upgrade():
|
|
20
|
+
${upgrades if upgrades else "pass"}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def downgrade():
|
|
24
|
+
${downgrades if downgrades else "pass"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""splent_feature_events
|
|
2
|
+
|
|
3
|
+
Revision ID: b6efdeb9b074
|
|
4
|
+
Revises:
|
|
5
|
+
Create Date: 2026-06-27 20:03:40.536213
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from alembic import op
|
|
10
|
+
import sqlalchemy as sa
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# revision identifiers, used by Alembic.
|
|
14
|
+
revision = "b6efdeb9b074"
|
|
15
|
+
down_revision = None
|
|
16
|
+
branch_labels = None
|
|
17
|
+
depends_on = None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def upgrade():
|
|
21
|
+
# ### commands auto generated by Alembic - please adjust! ###
|
|
22
|
+
op.create_table(
|
|
23
|
+
"event",
|
|
24
|
+
sa.Column("id", sa.Integer(), nullable=False),
|
|
25
|
+
sa.Column("title", sa.String(length=255), nullable=False),
|
|
26
|
+
sa.Column("slug", sa.String(length=255), nullable=False),
|
|
27
|
+
sa.Column("kind", sa.String(length=64), nullable=True),
|
|
28
|
+
sa.Column("summary", sa.Text(), nullable=True),
|
|
29
|
+
sa.Column("description", sa.Text(), nullable=True),
|
|
30
|
+
sa.Column("speaker", sa.String(length=255), nullable=True),
|
|
31
|
+
sa.Column("room", sa.String(length=128), nullable=True),
|
|
32
|
+
sa.Column("starts_at", sa.DateTime(), nullable=True),
|
|
33
|
+
sa.Column("ends_at", sa.DateTime(), nullable=True),
|
|
34
|
+
sa.Column("image", sa.String(length=512), nullable=True),
|
|
35
|
+
sa.Column("link", sa.String(length=512), nullable=True),
|
|
36
|
+
sa.Column("published", sa.Boolean(), nullable=True),
|
|
37
|
+
sa.Column("order", sa.Integer(), nullable=True),
|
|
38
|
+
sa.PrimaryKeyConstraint("id"),
|
|
39
|
+
)
|
|
40
|
+
op.create_index(op.f("ix_event_slug"), "event", ["slug"], unique=True)
|
|
41
|
+
# ### end Alembic commands ###
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def downgrade():
|
|
45
|
+
# ### commands auto generated by Alembic - please adjust! ###
|
|
46
|
+
op.drop_index(op.f("ix_event_slug"), table_name="event")
|
|
47
|
+
op.drop_table("event")
|
|
48
|
+
# ### end Alembic commands ###
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from datetime import datetime # noqa: F401 — used by seeders/migrations
|
|
2
|
+
|
|
3
|
+
from splent_framework.db import db
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Event(db.Model):
|
|
7
|
+
"""An event/activity: a talk, workshop, competition or ceremony."""
|
|
8
|
+
|
|
9
|
+
__tablename__ = "event"
|
|
10
|
+
|
|
11
|
+
id = db.Column(db.Integer, primary_key=True)
|
|
12
|
+
title = db.Column(db.String(255), nullable=False)
|
|
13
|
+
slug = db.Column(db.String(255), nullable=False, unique=True, index=True)
|
|
14
|
+
kind = db.Column(
|
|
15
|
+
db.String(64), default="talk"
|
|
16
|
+
) # talk|workshop|competition|ceremony
|
|
17
|
+
summary = db.Column(db.Text, default="")
|
|
18
|
+
description = db.Column(db.Text, default="") # rich text / HTML
|
|
19
|
+
speaker = db.Column(db.String(255), default="")
|
|
20
|
+
room = db.Column(db.String(128), default="")
|
|
21
|
+
starts_at = db.Column(db.DateTime)
|
|
22
|
+
ends_at = db.Column(db.DateTime)
|
|
23
|
+
image = db.Column(db.String(512), default="")
|
|
24
|
+
link = db.Column(db.String(512), default="")
|
|
25
|
+
published = db.Column(db.Boolean, default=True)
|
|
26
|
+
order = db.Column(db.Integer, default=0)
|
|
27
|
+
|
|
28
|
+
def __repr__(self):
|
|
29
|
+
return f"Event<{self.slug}>"
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from splent_io.splent_feature_events.models import Event
|
|
4
|
+
from splent_framework.repositories.BaseRepository import BaseRepository
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class EventsRepository(BaseRepository):
|
|
8
|
+
def __init__(self):
|
|
9
|
+
super().__init__(Event)
|
|
10
|
+
|
|
11
|
+
def list_published(self) -> list[Event]:
|
|
12
|
+
return (
|
|
13
|
+
Event.query.filter_by(published=True)
|
|
14
|
+
.order_by(Event.order.asc(), Event.starts_at.asc())
|
|
15
|
+
.all()
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
def get_by_slug(self, slug: str) -> Event | None:
|
|
19
|
+
return Event.query.filter_by(slug=slug).first()
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
from flask import (
|
|
4
|
+
abort,
|
|
5
|
+
flash,
|
|
6
|
+
redirect,
|
|
7
|
+
render_template,
|
|
8
|
+
request,
|
|
9
|
+
url_for,
|
|
10
|
+
)
|
|
11
|
+
from flask_login import login_required
|
|
12
|
+
|
|
13
|
+
from splent_io.splent_feature_events import events_bp
|
|
14
|
+
from splent_io.splent_feature_events.models import Event
|
|
15
|
+
from splent_framework.db import db
|
|
16
|
+
from splent_framework.services.service_locator import service_proxy
|
|
17
|
+
|
|
18
|
+
events_service = service_proxy("EventsService")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# =====================================================================
|
|
22
|
+
# PUBLIC
|
|
23
|
+
# =====================================================================
|
|
24
|
+
@events_bp.route("/events", methods=["GET"])
|
|
25
|
+
def index():
|
|
26
|
+
events = events_service.list_published()
|
|
27
|
+
return render_template("events/list.html", events=events)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@events_bp.route("/events/<slug>", methods=["GET"])
|
|
31
|
+
def detail(slug):
|
|
32
|
+
event = events_service.get_by_slug(slug)
|
|
33
|
+
if event is None:
|
|
34
|
+
abort(404)
|
|
35
|
+
return render_template("events/detail.html", event=event)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# =====================================================================
|
|
39
|
+
# ADMIN — domain-specific management (the "plugin" screen)
|
|
40
|
+
# =====================================================================
|
|
41
|
+
# Known kinds shown first in the grouped list and offered in the form.
|
|
42
|
+
KNOWN_KINDS = ["talk", "workshop", "competition", "ceremony"]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _slugify(value):
|
|
46
|
+
base = re.sub(r"[^a-z0-9]+", "-", (value or "").lower()).strip("-")
|
|
47
|
+
return base or "event"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _unique_slug(title, exclude_id=None):
|
|
51
|
+
base = _slugify(title)
|
|
52
|
+
slug, i = base, 2
|
|
53
|
+
while True:
|
|
54
|
+
q = Event.query.filter_by(slug=slug)
|
|
55
|
+
if exclude_id:
|
|
56
|
+
q = q.filter(Event.id != exclude_id)
|
|
57
|
+
if not q.first():
|
|
58
|
+
return slug
|
|
59
|
+
slug, i = f"{base}-{i}", i + 1
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _ordered_groups():
|
|
63
|
+
"""All events (incl. drafts) grouped by kind; known kinds first, extras after."""
|
|
64
|
+
grouped = {}
|
|
65
|
+
for e in Event.query.order_by(
|
|
66
|
+
Event.order.asc(), Event.starts_at.asc(), Event.title.asc()
|
|
67
|
+
).all():
|
|
68
|
+
grouped.setdefault(e.kind or "talk", []).append(e)
|
|
69
|
+
ordered = {g: grouped.pop(g) for g in KNOWN_KINDS if g in grouped}
|
|
70
|
+
ordered.update(grouped)
|
|
71
|
+
return ordered
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _known_groups():
|
|
75
|
+
existing = [g[0] for g in db.session.query(Event.kind).distinct().all() if g[0]]
|
|
76
|
+
seen, out = set(), []
|
|
77
|
+
for g in KNOWN_KINDS + existing:
|
|
78
|
+
if g and g not in seen:
|
|
79
|
+
seen.add(g)
|
|
80
|
+
out.append(g)
|
|
81
|
+
return out
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _form_to_data(form):
|
|
85
|
+
return {
|
|
86
|
+
"title": (form.get("title") or "").strip(),
|
|
87
|
+
"kind": (form.get("kind") or "talk").strip() or "talk",
|
|
88
|
+
"summary": (form.get("summary") or "").strip(),
|
|
89
|
+
"description": (form.get("description") or "").strip(),
|
|
90
|
+
"speaker": (form.get("speaker") or "").strip(),
|
|
91
|
+
"room": (form.get("room") or "").strip(),
|
|
92
|
+
"starts_at": _parse_dt(form.get("starts_at")),
|
|
93
|
+
"ends_at": _parse_dt(form.get("ends_at")),
|
|
94
|
+
"image": (form.get("image") or "").strip(),
|
|
95
|
+
"link": (form.get("link") or "").strip(),
|
|
96
|
+
"order": int(form.get("order") or 0),
|
|
97
|
+
"published": bool(form.get("published")),
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _parse_dt(value):
|
|
102
|
+
value = (value or "").strip()
|
|
103
|
+
if not value:
|
|
104
|
+
return None
|
|
105
|
+
from datetime import datetime
|
|
106
|
+
|
|
107
|
+
for fmt in ("%Y-%m-%dT%H:%M", "%Y-%m-%dT%H:%M:%S", "%Y-%m-%d %H:%M"):
|
|
108
|
+
try:
|
|
109
|
+
return datetime.strptime(value, fmt)
|
|
110
|
+
except ValueError:
|
|
111
|
+
continue
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@events_bp.route("/admin/events", methods=["GET"])
|
|
116
|
+
@login_required
|
|
117
|
+
def admin_index():
|
|
118
|
+
return render_template(
|
|
119
|
+
"events/admin/list.html",
|
|
120
|
+
groups=_ordered_groups(),
|
|
121
|
+
known_groups=_known_groups(),
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@events_bp.route("/admin/events/new", methods=["GET", "POST"])
|
|
126
|
+
@login_required
|
|
127
|
+
def admin_new():
|
|
128
|
+
if request.method == "POST":
|
|
129
|
+
data = _form_to_data(request.form)
|
|
130
|
+
if not data["title"]:
|
|
131
|
+
flash("Title is required.", "danger")
|
|
132
|
+
return redirect(url_for("events.admin_new"))
|
|
133
|
+
data["slug"] = _unique_slug(data["title"])
|
|
134
|
+
db.session.add(Event(**data))
|
|
135
|
+
db.session.commit()
|
|
136
|
+
flash(f"Added {data['title']}.", "success")
|
|
137
|
+
return redirect(url_for("events.admin_index"))
|
|
138
|
+
return render_template(
|
|
139
|
+
"events/admin/form.html", event=None, known_groups=_known_groups()
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@events_bp.route("/admin/events/<int:event_id>/edit", methods=["GET", "POST"])
|
|
144
|
+
@login_required
|
|
145
|
+
def admin_edit(event_id):
|
|
146
|
+
event = Event.query.get_or_404(event_id)
|
|
147
|
+
if request.method == "POST":
|
|
148
|
+
data = _form_to_data(request.form)
|
|
149
|
+
if not data["title"]:
|
|
150
|
+
flash("Title is required.", "danger")
|
|
151
|
+
return redirect(url_for("events.admin_edit", event_id=event_id))
|
|
152
|
+
if data["title"] != event.title:
|
|
153
|
+
data["slug"] = _unique_slug(data["title"], exclude_id=event.id)
|
|
154
|
+
for key, value in data.items():
|
|
155
|
+
setattr(event, key, value)
|
|
156
|
+
db.session.commit()
|
|
157
|
+
flash(f"Updated {event.title}.", "success")
|
|
158
|
+
return redirect(url_for("events.admin_index"))
|
|
159
|
+
return render_template(
|
|
160
|
+
"events/admin/form.html", event=event, known_groups=_known_groups()
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@events_bp.route("/admin/events/<int:event_id>/move", methods=["POST"])
|
|
165
|
+
@login_required
|
|
166
|
+
def admin_move(event_id):
|
|
167
|
+
event = Event.query.get_or_404(event_id)
|
|
168
|
+
new_group = (request.form.get("group") or "").strip()
|
|
169
|
+
if new_group and new_group != event.kind:
|
|
170
|
+
event.kind = new_group
|
|
171
|
+
db.session.commit()
|
|
172
|
+
flash(f"Moved {event.title} to {new_group}.", "success")
|
|
173
|
+
return redirect(url_for("events.admin_index"))
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
@events_bp.route("/admin/events/<int:event_id>/delete", methods=["POST"])
|
|
177
|
+
@login_required
|
|
178
|
+
def admin_delete(event_id):
|
|
179
|
+
event = Event.query.get_or_404(event_id)
|
|
180
|
+
title = event.title
|
|
181
|
+
db.session.delete(event)
|
|
182
|
+
db.session.commit()
|
|
183
|
+
flash(f"Removed {title}.", "success")
|
|
184
|
+
return redirect(url_for("events.admin_index"))
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
|
|
3
|
+
from splent_framework.seeders.BaseSeeder import BaseSeeder
|
|
4
|
+
|
|
5
|
+
from splent_io.splent_feature_events.models import Event
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class EventsSeeder(BaseSeeder):
|
|
9
|
+
def run(self):
|
|
10
|
+
self.seed(
|
|
11
|
+
[
|
|
12
|
+
Event(
|
|
13
|
+
slug="opening-ceremony",
|
|
14
|
+
title="Opening Ceremony",
|
|
15
|
+
kind="ceremony",
|
|
16
|
+
room="Salón de Actos",
|
|
17
|
+
order=1,
|
|
18
|
+
starts_at=datetime(2025, 11, 4, 9, 0),
|
|
19
|
+
summary="Kick-off of InnoSoft Days XIII at the ETSII.",
|
|
20
|
+
description="<p>Welcome to three days of talks, workshops, "
|
|
21
|
+
"competitions and fun.</p>",
|
|
22
|
+
),
|
|
23
|
+
Event(
|
|
24
|
+
slug="ai-testing",
|
|
25
|
+
title="New Ways to Test Software using AI",
|
|
26
|
+
kind="talk",
|
|
27
|
+
room="Aula 0.1",
|
|
28
|
+
order=2,
|
|
29
|
+
starts_at=datetime(2025, 11, 4, 10, 0),
|
|
30
|
+
speaker="Andreas Zeller",
|
|
31
|
+
link="https://andreas-zeller.info",
|
|
32
|
+
summary="Keynote on AI-assisted software testing.",
|
|
33
|
+
description="<p>How AI is changing the way we test software.</p>",
|
|
34
|
+
),
|
|
35
|
+
Event(
|
|
36
|
+
slug="escape-room-grace",
|
|
37
|
+
title="Escape Room — Grace's Enigma",
|
|
38
|
+
kind="competition",
|
|
39
|
+
room="Lab 2",
|
|
40
|
+
order=3,
|
|
41
|
+
starts_at=datetime(2025, 11, 5, 16, 0),
|
|
42
|
+
summary="A software-themed escape room.",
|
|
43
|
+
),
|
|
44
|
+
Event(
|
|
45
|
+
slug="closing-ceremony",
|
|
46
|
+
title="Closing Ceremony",
|
|
47
|
+
kind="ceremony",
|
|
48
|
+
room="Salón de Actos",
|
|
49
|
+
order=99,
|
|
50
|
+
starts_at=datetime(2025, 11, 6, 18, 0),
|
|
51
|
+
summary="Awards and farewell.",
|
|
52
|
+
),
|
|
53
|
+
]
|
|
54
|
+
)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from splent_io.splent_feature_events.repositories import EventsRepository
|
|
2
|
+
from splent_framework.services.BaseService import BaseService
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class EventsService(BaseService):
|
|
6
|
+
def __init__(self):
|
|
7
|
+
super().__init__(EventsRepository())
|
|
8
|
+
|
|
9
|
+
def list_published(self):
|
|
10
|
+
return self.repository.list_published()
|
|
11
|
+
|
|
12
|
+
def get_by_slug(self, slug: str):
|
|
13
|
+
return self.repository.get_by_slug(slug)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Signals for splent_feature_events.
|
|
3
|
+
|
|
4
|
+
Use ``define_signal`` to declare signals this feature emits.
|
|
5
|
+
Use ``connect_signal`` to listen to signals from other features.
|
|
6
|
+
|
|
7
|
+
Examples::
|
|
8
|
+
|
|
9
|
+
# Define a signal (this feature emits it):
|
|
10
|
+
from splent_framework.signals.signal_utils import define_signal
|
|
11
|
+
item_created = define_signal("item-created", "splent_feature_events")
|
|
12
|
+
|
|
13
|
+
# Connect to a signal from another feature:
|
|
14
|
+
from splent_framework.signals.signal_utils import connect_signal
|
|
15
|
+
|
|
16
|
+
@connect_signal("user-registered", "splent_feature_events")
|
|
17
|
+
def on_user_registered(sender, user, **kwargs):
|
|
18
|
+
pass
|
|
19
|
+
"""
|
splent_feature_events-0.1.0/src/splent_io/splent_feature_events/templates/events/admin/form.html
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
{% extends "base_template.html" %}
|
|
2
|
+
|
|
3
|
+
{% block title %}{{ 'Edit event' if event else 'Add event' }}{% endblock %}
|
|
4
|
+
|
|
5
|
+
{% block content %}
|
|
6
|
+
<div class="d-flex flex-column align-items-start gap-2 mb-4">
|
|
7
|
+
<h1 class="h3 mb-0">{{ 'Edit event' if event else 'Add event' }}</h1>
|
|
8
|
+
<a href="{{ url_for('events.admin_index') }}" class="btn btn-outline-secondary">← Back to events</a>
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
<form method="POST" class="card">
|
|
12
|
+
<div class="card-body">
|
|
13
|
+
<div class="row g-3">
|
|
14
|
+
<div class="col-md-8">
|
|
15
|
+
<label class="form-label">Title *</label>
|
|
16
|
+
<input name="title" class="form-control" required value="{{ event.title if event else '' }}">
|
|
17
|
+
</div>
|
|
18
|
+
<div class="col-md-4">
|
|
19
|
+
<label class="form-label">Kind</label>
|
|
20
|
+
<input name="kind" class="form-control" list="known-groups" value="{{ event.kind if event else 'talk' }}" placeholder="Pick an existing kind or type a new one">
|
|
21
|
+
<datalist id="known-groups">{% for g in known_groups %}<option value="{{ g }}">{% endfor %}</datalist>
|
|
22
|
+
<div class="form-text">Type a new name to create a new kind.</div>
|
|
23
|
+
</div>
|
|
24
|
+
<div class="col-md-6">
|
|
25
|
+
<label class="form-label">Speaker</label>
|
|
26
|
+
<input name="speaker" class="form-control" value="{{ event.speaker if event else '' }}">
|
|
27
|
+
</div>
|
|
28
|
+
<div class="col-md-6">
|
|
29
|
+
<label class="form-label">Room</label>
|
|
30
|
+
<input name="room" class="form-control" value="{{ event.room if event else '' }}">
|
|
31
|
+
</div>
|
|
32
|
+
<div class="col-md-6">
|
|
33
|
+
<label class="form-label">Starts at</label>
|
|
34
|
+
<input name="starts_at" type="datetime-local" class="form-control" value="{{ event.starts_at.strftime('%Y-%m-%dT%H:%M') if event and event.starts_at else '' }}">
|
|
35
|
+
</div>
|
|
36
|
+
<div class="col-md-6">
|
|
37
|
+
<label class="form-label">Ends at</label>
|
|
38
|
+
<input name="ends_at" type="datetime-local" class="form-control" value="{{ event.ends_at.strftime('%Y-%m-%dT%H:%M') if event and event.ends_at else '' }}">
|
|
39
|
+
</div>
|
|
40
|
+
<div class="col-12">
|
|
41
|
+
<label class="form-label">Summary</label>
|
|
42
|
+
<textarea name="summary" class="form-control" rows="2">{{ event.summary if event else '' }}</textarea>
|
|
43
|
+
</div>
|
|
44
|
+
<div class="col-12">
|
|
45
|
+
<label class="form-label">Description</label>
|
|
46
|
+
<textarea name="description" class="form-control" rows="5">{{ event.description if event else '' }}</textarea>
|
|
47
|
+
</div>
|
|
48
|
+
<div class="col-md-6">
|
|
49
|
+
<label class="form-label">Image URL</label>
|
|
50
|
+
<input name="image" class="form-control" value="{{ event.image if event else '' }}">
|
|
51
|
+
</div>
|
|
52
|
+
<div class="col-md-6">
|
|
53
|
+
<label class="form-label">Link URL</label>
|
|
54
|
+
<input name="link" type="url" class="form-control" value="{{ event.link if event else '' }}">
|
|
55
|
+
</div>
|
|
56
|
+
<div class="col-md-3">
|
|
57
|
+
<label class="form-label">Order</label>
|
|
58
|
+
<input name="order" type="number" class="form-control" value="{{ event.order if event else 0 }}">
|
|
59
|
+
</div>
|
|
60
|
+
<div class="col-md-3 d-flex align-items-end">
|
|
61
|
+
<div class="form-check">
|
|
62
|
+
<input class="form-check-input" type="checkbox" name="published" id="published" {{ 'checked' if (event.published if event else True) else '' }}>
|
|
63
|
+
<label class="form-check-label" for="published">Published</label>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
<div class="card-footer text-end">
|
|
69
|
+
<a href="{{ url_for('events.admin_index') }}" class="btn btn-link text-muted">Cancel</a>
|
|
70
|
+
<button class="btn btn-primary" type="submit">{{ 'Save changes' if event else 'Add event' }}</button>
|
|
71
|
+
</div>
|
|
72
|
+
</form>
|
|
73
|
+
{% endblock %}
|
splent_feature_events-0.1.0/src/splent_io/splent_feature_events/templates/events/admin/list.html
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{% extends "base_template.html" %}
|
|
2
|
+
|
|
3
|
+
{% block title %}Events{% endblock %}
|
|
4
|
+
|
|
5
|
+
{% block content %}
|
|
6
|
+
<div class="d-flex flex-column align-items-start gap-2 mb-4">
|
|
7
|
+
<div>
|
|
8
|
+
<h1 class="h3 mb-0">Events</h1>
|
|
9
|
+
<p class="text-muted mb-0">Manage events and their kinds.</p>
|
|
10
|
+
</div>
|
|
11
|
+
<a href="{{ url_for('events.admin_new') }}" class="btn btn-primary">+ Add event</a>
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
{% with messages = get_flashed_messages(with_categories=true) %}
|
|
15
|
+
{% for category, message in messages %}
|
|
16
|
+
<div class="alert alert-{{ 'danger' if category == 'danger' else 'success' }} py-2">{{ message }}</div>
|
|
17
|
+
{% endfor %}
|
|
18
|
+
{% endwith %}
|
|
19
|
+
|
|
20
|
+
{% for group, events in groups.items() %}
|
|
21
|
+
<div class="card mb-3">
|
|
22
|
+
<div class="card-header d-flex justify-content-between align-items-center">
|
|
23
|
+
<strong>{{ group }}</strong>
|
|
24
|
+
<span class="badge bg-secondary">{{ events | length }}</span>
|
|
25
|
+
</div>
|
|
26
|
+
<div class="table-responsive">
|
|
27
|
+
<table class="table table-hover align-middle mb-0">
|
|
28
|
+
<tbody>
|
|
29
|
+
{% for e in events %}
|
|
30
|
+
<tr>
|
|
31
|
+
<td style="width:52px">
|
|
32
|
+
{% if e.image %}
|
|
33
|
+
<img src="{{ e.image }}" alt="" style="width:38px;height:38px;border-radius:6px;object-fit:cover">
|
|
34
|
+
{% else %}
|
|
35
|
+
<span class="d-inline-flex align-items-center justify-content-center bg-light text-muted" style="width:38px;height:38px;border-radius:6px">{{ e.title[:1] | upper }}</span>
|
|
36
|
+
{% endif %}
|
|
37
|
+
</td>
|
|
38
|
+
<td>
|
|
39
|
+
<strong>{{ e.title }}</strong>
|
|
40
|
+
{% if not e.published %}<span class="badge bg-warning text-dark ms-1">draft</span>{% endif %}
|
|
41
|
+
<div class="text-muted small">
|
|
42
|
+
{% if e.starts_at %}{{ e.starts_at.strftime('%Y-%m-%d %H:%M') }}{% endif %}
|
|
43
|
+
{% if e.room %} · {{ e.room }}{% endif %}
|
|
44
|
+
{% if e.speaker %} · {{ e.speaker }}{% endif %}
|
|
45
|
+
</div>
|
|
46
|
+
</td>
|
|
47
|
+
<td style="width:240px">
|
|
48
|
+
<form method="POST" action="{{ url_for('events.admin_move', event_id=e.id) }}" class="d-flex gap-1">
|
|
49
|
+
<select name="group" class="form-select form-select-sm">
|
|
50
|
+
{% for g in known_groups %}<option value="{{ g }}" {{ 'selected' if g == e.kind else '' }}>{{ g }}</option>{% endfor %}
|
|
51
|
+
</select>
|
|
52
|
+
<button class="btn btn-sm btn-outline-secondary" type="submit" title="Move to kind">Move</button>
|
|
53
|
+
</form>
|
|
54
|
+
</td>
|
|
55
|
+
<td style="width:150px" class="text-end">
|
|
56
|
+
<a href="{{ url_for('events.admin_edit', event_id=e.id) }}" class="btn btn-sm btn-outline-primary">Edit</a>
|
|
57
|
+
<form method="POST" action="{{ url_for('events.admin_delete', event_id=e.id) }}" class="d-inline" onsubmit="return confirm('Remove {{ e.title }}?')">
|
|
58
|
+
<button class="btn btn-sm btn-outline-danger" type="submit" title="Remove"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg></button>
|
|
59
|
+
</form>
|
|
60
|
+
</td>
|
|
61
|
+
</tr>
|
|
62
|
+
{% endfor %}
|
|
63
|
+
</tbody>
|
|
64
|
+
</table>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
{% endfor %}
|
|
68
|
+
{% endblock %}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{% extends "public_base.html" %}
|
|
2
|
+
|
|
3
|
+
{% block title %}{{ event.title }} — {{ SPLENT_APP }}{% endblock %}
|
|
4
|
+
|
|
5
|
+
{% block hero %}
|
|
6
|
+
{{ render_block('hero', eyebrow=event.kind, title=event.title, subtitle=event.summary) }}
|
|
7
|
+
{% endblock %}
|
|
8
|
+
|
|
9
|
+
{% block content %}
|
|
10
|
+
<article class="container section event-detail">
|
|
11
|
+
{% if event.room or event.starts_at or event.speaker %}
|
|
12
|
+
<ul class="event-meta">
|
|
13
|
+
{% if event.room %}
|
|
14
|
+
<li class="event-meta__item">
|
|
15
|
+
<svg class="event-meta__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
16
|
+
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/>
|
|
17
|
+
</svg>
|
|
18
|
+
<span class="event-meta__label">{{ _('Room') }}</span>
|
|
19
|
+
<span class="event-meta__value">{{ event.room }}</span>
|
|
20
|
+
</li>
|
|
21
|
+
{% endif %}
|
|
22
|
+
{% if event.starts_at %}
|
|
23
|
+
<li class="event-meta__item">
|
|
24
|
+
<svg class="event-meta__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
25
|
+
<circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/>
|
|
26
|
+
</svg>
|
|
27
|
+
<span class="event-meta__label">{{ _('When') }}</span>
|
|
28
|
+
<span class="event-meta__value">{{ event.starts_at.strftime('%A %d %B, %H:%M') }}</span>
|
|
29
|
+
</li>
|
|
30
|
+
{% endif %}
|
|
31
|
+
{% if event.speaker %}
|
|
32
|
+
<li class="event-meta__item">
|
|
33
|
+
<svg class="event-meta__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
34
|
+
<path d="M12 2a3 3 0 0 0-3 3v6a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3z"/><path d="M19 10v1a7 7 0 0 1-14 0v-1"/><path d="M12 18v4"/>
|
|
35
|
+
</svg>
|
|
36
|
+
<span class="event-meta__label">{{ _('Speaker') }}</span>
|
|
37
|
+
<span class="event-meta__value">{{ event.speaker }}</span>
|
|
38
|
+
</li>
|
|
39
|
+
{% endif %}
|
|
40
|
+
</ul>
|
|
41
|
+
{% endif %}
|
|
42
|
+
|
|
43
|
+
{% if event.image %}
|
|
44
|
+
<img class="event-detail__image" src="{{ event.image }}" alt="">
|
|
45
|
+
{% endif %}
|
|
46
|
+
|
|
47
|
+
<div class="prose">{{ event.description | safe }}</div>
|
|
48
|
+
|
|
49
|
+
{% if event.link %}
|
|
50
|
+
<p class="event-detail__actions"><a class="btn btn-primary" href="{{ event.link }}">{{ _('More info') }}</a></p>
|
|
51
|
+
{% endif %}
|
|
52
|
+
|
|
53
|
+
<p><a class="event-detail__back" href="{{ url_for('events.index') }}">← {{ _('Back to events') }}</a></p>
|
|
54
|
+
</article>
|
|
55
|
+
{% endblock %}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{% extends "public_base.html" %}
|
|
2
|
+
|
|
3
|
+
{% block title %}Events — {{ SPLENT_APP }}{% endblock %}
|
|
4
|
+
|
|
5
|
+
{% block hero %}
|
|
6
|
+
{{ render_block('hero', eyebrow=_('Programme'), title=_('Events'),
|
|
7
|
+
subtitle=_('Talks, workshops and competitions.')) }}
|
|
8
|
+
{% endblock %}
|
|
9
|
+
|
|
10
|
+
{% block content %}
|
|
11
|
+
<div class="container section">
|
|
12
|
+
<div class="card-grid">
|
|
13
|
+
{% for e in events %}
|
|
14
|
+
<a class="card" href="{{ url_for('events.detail', slug=e.slug) }}">
|
|
15
|
+
{% if e.image %}<img class="card__img" src="{{ e.image }}" alt="">{% endif %}
|
|
16
|
+
<span class="badge">{{ e.kind }}</span>
|
|
17
|
+
<h3 class="card__title">{{ e.title }}</h3>
|
|
18
|
+
{% if e.summary %}<p class="card__text">{{ e.summary }}</p>{% endif %}
|
|
19
|
+
{% if e.room or e.starts_at %}
|
|
20
|
+
<p class="card__meta">{{ e.room }}{% if e.starts_at %} · {{ e.starts_at.strftime('%d %b %H:%M') }}{% endif %}</p>
|
|
21
|
+
{% endif %}
|
|
22
|
+
</a>
|
|
23
|
+
{% else %}
|
|
24
|
+
<p class="card__text">{{ _('No events yet.') }}</p>
|
|
25
|
+
{% endfor %}
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
{% endblock %}
|