wip: planning

This commit is contained in:
Michael Costa 2023-04-09 06:17:34 +11:00
parent 27a6205595
commit a76b8e034a
30 changed files with 1078 additions and 22 deletions

126
build/lib/thsf/__init__.py Normal file
View File

@ -0,0 +1,126 @@
import sys
import json
import logging
from logging import config
import yaml
from flask import Flask, render_template, redirect, request, url_for
from thsf.backend import Backend
from thsf.schedule import Schedule
# ------------------------------------------------------------------------------
# -- Configuration
# ------------------------------------------------------------------------------
class AppConfig:
""" Flask application config """
CONFIG_FILENAME = "config.yml"
# ------------------------------------------------------------------------------
# -- Application
# ------------------------------------------------------------------------------
logger = logging.getLogger('wsgi')
app = Flask(__name__)
# ------------------------------------------------------------------------------
# -- Local configuration
# ------------------------------------------------------------------------------
app.config.from_object(__name__ + '.AppConfig')
try:
with open(app.config["CONFIG_FILENAME"], mode="r", encoding="utf-8") as local_config_file:
app.local_config = yaml.load(local_config_file, Loader=yaml.SafeLoader)
app.config["SECRET_KEY"] = app.local_config["app"]["secret_key"]
app.config["LANGUAGES"] = app.local_config["app"]["languages"]
config.dictConfig(app.local_config["log"])
backend = Backend(url=app.local_config["pretalx"]["url"],
apiprefix=app.local_config["pretalx"]["apiprefix"],
apikey=app.local_config["pretalx"]["apikey"])
schedule = Schedule()
except Exception as err:
logger.critical("[{}] {}".format(err.__class__, str(err)))
sys.exit(1)
# ------------------------------------------------------------------------------
# -- Tools
# ------------------------------------------------------------------------------
@app.errorhandler(404)
def page_not_found(err):
return redirect(url_for('index'))
# ------------------------------------------------------------------------------
# -- Routes
# ------------------------------------------------------------------------------
@app.route('/favicon.ico', methods=['GET'])
def favicon():
return redirect(url_for('static', filename='images/favicon.png'))
@app.route('/', methods=['GET'])
def index():
return render_template("index.html")
@app.route('/planning', methods=['GET'])
def planning():
slots = backend.get(endpoint=f"events/{app.local_config['pretalx']['event']}/schedules/{app.local_config['pretalx']['schedule']}").json()
# schedule.set_slots(slots["slots"])
return render_template("planning.html", slots=slots["slots"])
# return "toto"
# for slot in sched["slots"]:
# schedule.add_slot(slot)
# return schedule.df.to_html()
@app.route('/concerts', methods=['GET'])
def concerts():
return "concerts"
@app.route('/djsets', methods=['GET'])
def djsets():
return "djsets"
@app.route('/exhibitions', methods=['GET'])
def exhibitions():
return "exhibitions"
@app.route('/lighttalks', methods=['GET'])
def lighttalks():
return "lighttalks"
@app.route('/paneldiscussions', methods=['GET'])
def paneldiscussions():
return "paneldiscussions"
@app.route('/screenings', methods=['GET'])
def screenings():
return "screenings"
@app.route('/talks', methods=['GET'])
def talks():
return "talks"
@app.route('/workshops', methods=['GET'])
def workshops():
return "workshops"
# ------------------------------------------------------------------------------
# -- Main
# ------------------------------------------------------------------------------
if __name__ == '__main__':
app.run(host='127.0.0.1', port=5000, debug=True)

View File

@ -0,0 +1,15 @@
import requests
import logging
class Backend:
def __init__(self, url, apiprefix, apikey):
self.url = url
self.apiprefix = apiprefix
self.apikey = apikey
self.session = requests.Session()
def get(self, endpoint, params=None):
url = f"{self.url}/{self.apiprefix}/{endpoint}"
headers = {"Authorization": f"Token {self.apikey}",
"Accept": "application/json"}
return self.session.get(url, params=params, headers=headers)

View File

@ -0,0 +1,6 @@
class Schedule:
def __init__(self):
self.slots = list()
def set_slots(self, slots):
self.slots = slots

View File

@ -0,0 +1,50 @@
#schedule {
display: flex;
flex-direction: column;
justify-content: start;
align-items: flex-start;
align-content: flex-start;
margin-bottom: 5em;
}
.slot {
display: flex;
flex-direction: row;
align-items: flex-start;
align-content: flex-start;
margin: 1em;
border-radius: 5px;
border-color: var(--alt-color);
background-color: var(--main-color);
border-style: solid;
border-width: 1px;
font-size: 2em;
width: 25em;
}
.metadata {
display: flex;
flex-direction: column;
justify-content: start;
align-items: flex-start;
align-content: flex-start;
}
.data {
display: flex;
flex-direction: column;
justify-content: start;
align-items: flex-start;
align-content: flex-start;
margin-left: 2em;
}
.speaker_avatar {
height: 50px;
width: 50px;
}
.data_img {
height: 50px;
width: 50px;
}

View File

@ -0,0 +1,156 @@
@font-face {
font-family: pfdintextcomppromedium;
src: url(../fonts/PFDinTextCompPro-Medium.ttf);
}
@font-face {
font-family: pfdintextcompprothin;
src: url(../fonts/PFDinTextCompPro-Thin.ttf);
}
:root {
--main-bg-color: #e6007e;
--main-color: #ffffff;
--alt-color: #1A000D;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background-color: var(--main-bg-color);
font-family: pfdintextcomppromedium;
}
.white {
color: var(--main-color);
}
.black {
color: var(--alt-color);
}
.thin {
font-family: pfdintextcompprothin;
}
.bold {
font-family: pfdintextcomppromedium;
}
.button {
font-size: 2.5em;
transition-property: color;
transition-duration: 1s;
}
.button:hover {
color: var(--main-color);
cursor: pointer;
}
.logo {
width: inherit;
}
.cursor {
width: 0;
height: 0;
border-left: 0.75em solid transparent;
border-right: 0.75em solid transparent;
border-bottom: 0.75em solid var(--alt-color);
}
a {
font-family: pfdintextcomppromedium;
font-weight: 250;
color: var(--alt-color);
transition-property: color;
transition-duration: 1s;
text-decoration: wavy;
}
a:hover {
color: var(--main-color);
cursor: pointer;
}
#main_wrapper {
display: flex;
flex-direction: row;
justify-content: center;
align-items: flex-start;
align-content: flex-start;
margin-bottom: 5em;
}
#center_wrapper, #header_wrapper {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
}
#header {
display: flex;
flex-direction: row;
justify-content: center;
gap: 0;
text-align: center;
font-size: 9.75em;
font-weight: bold;
padding: 0;
}
#header > span {
margin: 0;
}
#subheader {
margin: -1em 0 0 0;
font-size: 3.47em;
}
#place {
margin: 0;
font-size: 2.145em;
}
#logo_wrapper {
margin-top: 1em;
width: 40em;
}
#navbar_wrapper {
position: fixed;
bottom: 0;
padding: 1em 0;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
gap: 2em;
background-color: var(--main-bg-color);
width: 100vw;
}
#cursorbar {
margin-top: 1em;
}
#blah {
margin: 2em 0 2em;
font-size: 2em;
font-family: pfdintextcompprothin;
color: var(--main-color);
width: 20em;
text-align:justify;
text-justify:inter-word;
}
#blah > p, blah.h2 {
margin-top: 1em;
}

View File

@ -0,0 +1,36 @@
.tooltip {
position: relative;
display: inline-block;
}
.tooltip .tooltiptext {
visibility: hidden;
background-color: var(--main-color);
color: var(--alt-color);
text-align: center;
border-radius: 6px;
padding: 5px 0;
position: absolute;
z-index: 1;
bottom: 125%;
left: 50%;
margin-left: -60px;
opacity: 0;
transition: opacity 1s;
}
.tooltip .tooltiptext::after {
content: "";
position: absolute;
top: 100%;
left: 50%;
margin-left: -5px;
border-width: 5px;
}
.tooltip:hover .tooltiptext {
visibility: visible;
opacity: 1;
font-size: 0.7em;
padding: 0.2em;
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

View File

@ -0,0 +1,77 @@
<!DOCTYPE html>
<html lang='zxx'>
<head>
<title>THSF 2023: S/Extraire</title>
<meta name="viewport" content="initial-scale=1.0" />
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<link rel="stylesheet"
href="{{ url_for('static', filename='css/style.css') }}">
<link rel="stylesheet"
href="{{ url_for('static', filename='css/tooltip.css') }}">
<link rel="icon"
type="image/png"
href="{{ url_for('static', filename='images/favicon.png') }}" />
<link rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
{% block headers %}
{% endblock %}
</head>
<body>
{% block content %}
{% endblock %}
<div id="navbar_wrapper">
<i class="button tooltip black fa-regular fa-calendar"
title="Programme"
alt="Programme"
onclick="document.location='/planning'">
<span class="tooltiptext thin">Programme</span>
</i>
<i class="button tooltip black fa-solid fa-map-location-dot"
title="Le Lieu"
alt="Le Lieu">
<span class="tooltiptext thin">Le lieu</span>
</i>
<i class="button tooltip black fa-solid fa-burger"
title="Restauration"
alt="Restauration">
<span class="tooltiptext thin">Restauration</span>
</i>
<i class="button tooltip black fa-solid fa-shirt"
title="T-shirt et goodies"
alt="T-shirt et goodies">
<span class="tooltiptext thin">Goodies</span>
</i>
<i class="button tooltip black fa-solid fa-guitar"
title="Concerts et DJ sets"
alt="Concerts et DJ sets">
<span class="tooltiptext thin">Concerts et DJ sets</span>
</i>
<i class="button tooltip black fa-solid fa-screwdriver-wrench"
title="Ateliers"
alt="Ateliers">
<span class="tooltiptext thin">Ateliers</span>
</i>
<i class="button tooltip black fa-solid fa-film"
title="Projections"
alt="Projections">
<span class="tooltiptext thin">Projections</span>
</i>
<i class="button tooltip black fa-solid fa-users-line"
title="Tables rondes"
alt="Tables rondes">
<span class="tooltiptext thin">Tables rondes</span>
</i>
<i class="button tooltip black fa-solid fa-palette"
title="Expositions"
alt="Expositions">
<span class="tooltiptext thin">Expositions</span>
</i>
<i class="button tooltip black fa-solid fa-person-chalkboard"
title="Conférences"
alt="Conférences">
<span class="tooltiptext thin">Conférences</span>
</i>
</div>
</body>
</html>

View File

@ -0,0 +1,36 @@
{% extends "base.html" %}
{% block content %}
<div id="main_wrapper">
<div id="center_wrapper">
<div id="header_wrapper">
<div id="header">
<span class="black bold">THSF</span>
<span class="white thin">2023</span>
</div>
<div id="subheader">
<span class="white thin">Toulouse Hacker Space Factory</span>
</div>
<div id="place">
<span class="black thin">26 28 mai 2023 - </span>
<span class="white bold">CINÉMA UTOPIA BORDEROUGE</span>
</div>
</div>
<div id="logo_wrapper">
<img class="logo"
src="{{ url_for('static', filename='images/logo.svg') }}"
alt="THSF 2023 - S/Extraire"
title="THSF 2023 - S/Extraire"/>
</div>
<div id="blah">
<h2>Le T.H.S.F est enfin de retour !</h2>
<p>Nous vous invitons à passer un week-end de 3 jours à <a href="https://www.cinemas-utopia.org/toulouse/">Utopia Borderouge Toulouse</a> pour partager avec vous nos projets, nos réflexions, nos performances, nos poésies et nos doutes sur la technologie.</p>
<p>Après avoir été soutenu et accueilli pendant 10 ans par <a href="https://vive.mixart-myrys.org/">Mix'Art Myrys</a>, le <strong>Toulouse HackerSpace Factory (T.H.S.F)</strong> se tient désormais dans un autre lieu où l'utopie nécessaire est inscrite programme.</p>
<p>Cette année nous mettrons en avant des réflexions sur <a href="https://fr.wikipedia.org/wiki/Extractivisme" target="_new">l'extractivisme des ressources</a> planétaires, des données, de la valeur travail.</p>
<p>Comme toujours, notre objectif est de créer un festival qui poétise les bifurcations de nos idées communes et qui réinvente le sens de certains schémas imposés par notre époque. Rejoignez-nous pour une expérience enrichissante et pleine de surprises !</p>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,44 @@
{% extends "base.html" %}
{% block headers %}
<link rel="stylesheet"
href="{{ url_for('static', filename='css/planning.css') }}">
{% endblock %}
{% block content %}
<div id="main_wrapper">
<div id="#schedule">
{% for slot in slots %}
<div class="slot">
<div class="metadata">
<div class="title">{{slot["title"]}}</div>
<div class="speakers">
{% for speaker in slot["speakers"] %}
<div class="speaker">
<div class="name">{{speaker["name"]}}</div>
<img class="speaker_avatar" src="{{speaker['avatar']}}"/>
<div class="speaker_biography">{{speaker["biography"]}}</div>
</div>
{% endfor %}
</div>
<div class="type">{{slot["submission_type"]["fr"]}}</div>
<div class="start">{{slot["slot"]["start"]}}</div>
<div class="duration">{{slot["duration"]}}</div>
<div class="room">{{slot["slot"]["room"]["fr"]}}</div>
<div class="locale">{{slot["content_locale"]}}</div>
</div>
<div class="data">
<div class="abstract">{{slot["abstract"]}}</div>
<div class="description">{{slot["description"]}}</div>
<img class="data_img" src="{{slot['image']}}"/>
<div class="resources">
{% for resource in slot["resources"] %}
<div class="resources">
<a href="{{resource['resource']}}">{{resource["description"]}}</a>
</div>
{% endfor %}
</div>
</div>
</div>
{% endfor %}
</div>
</div>
{% endblock %}

View File

@ -23,6 +23,7 @@ install_requires =
gunicorn
pyYAML
requests
pandas
[options.packages.find]
where = src

View File

@ -0,0 +1,54 @@
Metadata-Version: 2.1
Name: thsf
Version: 0.0.1
Summary: "THSF website"
Home-page: https://git.tetalab.org/tetalab/thsf.net
Author: Doug Le Tough
Author-email: doug@redatomik.org
License: UNKNOWN
Platform: UNKNOWN
Classifier: Programming Language :: Python :: 3
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Requires-Python: >=3.7
Description-Content-Type: text/markdown
Provides-Extra: dev
# THSF.NET
Le site du THSF
## Modifier le site
Il n'est **pas possible** de pousser directement des modifications sur la branche `master`.
Pour modifier le site, il est nécessaire de créer une branche spécifique et d'y pousser vos modifications.
## Publication du site
Lorsque vous êtes satisfaits de vos modifications, vous pouvez créer [une demande d'ajout](https://git.tetalab.org/tetalab/thsf.net/pulls) de votre branche sur la branche `master`.
Lorsque la demande de fusion sera acceptée (vous pouvez auto-accepter vos demandes de fusion), vos modifications seront automatiquement publiées sur [le site du THSF](https://www.thsf.net).
### Personnalisation de la publication
Afin de rendre le processus plus souple, il est possible de personnaliser la livraison en plaçant **à la racine du dépôt** un fichier `Makefile` contenant une cible `all` qui sera systématiquement exécutée.
C'est dans cette cible `all` que vous pourrez mettre toutes vos commandes personnalisées, typiquement l'installation de modules `python`, etc.
Le processus de publication est le suivant:
1. Le site actuellement en production est **supprimé**
2. La branche `master` du présent dépôt est cloné sur le serveur hébergeant le site du **THSF**
3. Si un fichier `Makefile` se trouve **à la racine du dépôt**, la cible `all` (i.e: `make all`) est automatiquement exécutée.
## Contrôle de qualité et tests
Aucun contrôle de qualité ou de tests n'est mis en place. Vous êtes seuls sur le coup.
Soyez responsable et **testez vos modifications sur votre machine locale avant de fusionner votre branche** sur la branche `master`.

View File

@ -0,0 +1,28 @@
MANIFEST.in
README.md
pyproject.toml
setup.cfg
setup.py
src/thsf/__init__.py
src/thsf.egg-info/PKG-INFO
src/thsf.egg-info/SOURCES.txt
src/thsf.egg-info/dependency_links.txt
src/thsf.egg-info/requires.txt
src/thsf.egg-info/top_level.txt
src/thsf/backend/__init__.py
src/thsf/schedule/__init__.py
src/thsf/static/css/planning.css
src/thsf/static/css/style.css
src/thsf/static/css/tooltip.css
src/thsf/static/fonts/PFDinTextCompPro-Medium.ttf
src/thsf/static/fonts/PFDinTextCompPro-Thin.ttf
src/thsf/static/fonts/Uni Sans Bold.otf
src/thsf/static/fonts/Uni Sans Book.otf
src/thsf/static/images/affiche_v1.png
src/thsf/static/images/bg.png
src/thsf/static/images/favicon.png
src/thsf/static/images/logo.svg
src/thsf/static/images/pretalx-header.png
src/thsf/templates/base.html
src/thsf/templates/index.html
src/thsf/templates/planning.html

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,18 @@
flask
gunicorn
pyYAML
requests
pandas
[dev]
twine
build
wheel>=0.37.0
flake8>=4.0.1
flake8-breakpoint>=1.1.0
flake8-builtins>=1.5.3
flake8-print>=4.0.0
flake8-return>=1.1.3
pep8-naming>=0.8.2
setuptools>=60.9.2
pylint>=2.12.2

View File

@ -0,0 +1 @@
thsf

View File

@ -1,9 +1,11 @@
import sys
import json
import logging
from logging import config
import yaml
from flask import Flask, render_template, redirect, request, url_for
# from thsf.backend import Backend
from thsf.backend import Backend
from thsf.schedule import Schedule
# ------------------------------------------------------------------------------
@ -26,14 +28,15 @@ app = Flask(__name__)
# ------------------------------------------------------------------------------
app.config.from_object(__name__ + '.AppConfig')
try:
with open(app.config["CONFIG_FILENAME"], "r") as local_config_file:
with open(app.config["CONFIG_FILENAME"], mode="r", encoding="utf-8") as local_config_file:
app.local_config = yaml.load(local_config_file, Loader=yaml.SafeLoader)
app.config["SECRET_KEY"] = app.local_config["app"]["secret_key"]
app.config["LANGUAGES"] = app.local_config["app"]["languages"]
config.dictConfig(app.local_config["log"])
# backend = Backend(url=app.local_config["pretalx"]["url"],
# apiprefix=app.local_config["pretalx"]["apiprefix"],
# apikey=app.local_config["pretalx"]["apikey"])
backend = Backend(url=app.local_config["pretalx"]["url"],
apiprefix=app.local_config["pretalx"]["apiprefix"],
apikey=app.local_config["pretalx"]["apikey"])
schedule = Schedule()
except Exception as err:
logger.critical("[{}] {}".format(err.__class__, str(err)))
sys.exit(1)
@ -54,43 +57,63 @@ def page_not_found(err):
def favicon():
return redirect(url_for('static', filename='images/favicon.png'))
@app.route('/', methods=['GET'])
def index():
return render_template("index.html")
@app.route('/planning', methods=['GET'])
def planning():
backend.get(endpoint=f"events/{app.local_config['pretalx']['event']}/schedules/{app.local_config['pretalx']['schedule']}")
return "planning"
slots = backend.get(endpoint=f"events/{app.local_config['pretalx']['event']}/schedules/{app.local_config['pretalx']['schedule']}").json()
# schedule.set_slots(slots["slots"])
return render_template("planning.html", slots=slots["slots"])
# return "toto"
# for slot in sched["slots"]:
# schedule.add_slot(slot)
# return schedule.df.to_html()
@app.route('/concerts', methods=['GET'])
def concerts():
return "concerts"
@app.route('/djsets', methods=['GET'])
def djsets():
return "djsets"
@app.route('/exhibitions', methods=['GET'])
def exhibitions():
return "exhibitions"
@app.route('/lighttalks', methods=['GET'])
def lighttalks():
return "lighttalks"
@app.route('/paneldiscussions', methods=['GET'])
def paneldiscussions():
return "paneldiscussions"
@app.route('/screenings', methods=['GET'])
def screenings():
return "screenings"
@app.route('/talks', methods=['GET'])
def talks():
return "talks"
@app.route('/workshops', methods=['GET'])
def workshops():
return "workshops"

View File

@ -1,7 +1,7 @@
import requests
import logging
class Backend(url, apiprefix, apikey):
class Backend:
def __init__(self, url, apiprefix, apikey):
self.url = url
self.apiprefix = apiprefix
@ -10,6 +10,6 @@ class Backend(url, apiprefix, apikey):
def get(self, endpoint, params=None):
url = f"{self.url}/{self.apiprefix}/{endpoint}"
headers = {f"Authorization: Token {self.apikey}"}
return self.session.get(url, **{"params": params, "headers": headers})
headers = {"Authorization": f"Token {self.apikey}",
"Accept": "application/json"}
return self.session.get(url, params=params, headers=headers)

View File

@ -0,0 +1,6 @@
class Schedule:
def __init__(self):
self.slots = list()
def set_slots(self, slots):
self.slots = slots

View File

@ -0,0 +1,50 @@
#schedule {
display: flex;
flex-direction: column;
justify-content: start;
align-items: flex-start;
align-content: flex-start;
margin-bottom: 5em;
}
.slot {
display: flex;
flex-direction: row;
align-items: flex-start;
align-content: flex-start;
margin: 1em;
border-radius: 5px;
border-color: var(--alt-color);
background-color: var(--main-color);
border-style: solid;
border-width: 1px;
font-size: 2em;
width: 25em;
}
.metadata {
display: flex;
flex-direction: column;
justify-content: start;
align-items: flex-start;
align-content: flex-start;
}
.data {
display: flex;
flex-direction: column;
justify-content: start;
align-items: flex-start;
align-content: flex-start;
margin-left: 2em;
}
.speaker_avatar {
height: 50px;
width: 50px;
}
.data_img {
height: 50px;
width: 50px;
}

View File

@ -1,18 +1,44 @@
{% extends "base.html" %}
{% block headers %}
<script type="text/javascript" src="https://23.thsf.net/thsf-2023/schedule/widget/v2.en.js"></script>
<link rel="stylesheet"
href="{{ url_for('static', filename='css/planning.css') }}">
{% endblock %}
{% block content %}
<div id="thsf-schedule-wrapper">
<pretalx-schedule event-url="https://23.thsf.net/thsf-2023/" locale="fr" format="list" style="--pretalx-clr-primary: #91AE2C"></pretalx-schedule>
<noscript>
<div class="pretalx-widget">
<div class="pretalx-widget-info-message">
JavaScript is disabled in your browser. To access our schedule without JavaScript,
please <a target="_blank" href="https://23.thsf.net/thsf-2023/schedule/">click here</a>.
<div id="main_wrapper">
<div id="#schedule">
{% for slot in slots %}
<div class="slot">
<div class="metadata">
<div class="title">{{slot["title"]}}</div>
<div class="speakers">
{% for speaker in slot["speakers"] %}
<div class="speaker">
<div class="name">{{speaker["name"]}}</div>
<img class="speaker_avatar" src="{{speaker['avatar']}}"/>
<div class="speaker_biography">{{speaker["biography"]}}</div>
</div>
{% endfor %}
</div>
<div class="type">{{slot["submission_type"]["fr"]}}</div>
<div class="start">{{slot["slot"]["start"]}}</div>
<div class="duration">{{slot["duration"]}}</div>
<div class="room">{{slot["slot"]["room"]["fr"]}}</div>
<div class="locale">{{slot["content_locale"]}}</div>
</div>
</noscript>
<div class="data">
<div class="abstract">{{slot["abstract"]}}</div>
<div class="description">{{slot["description"]}}</div>
<img class="data_img" src="{{slot['image']}}"/>
<div class="resources">
{% for resource in slot["resources"] %}
<div class="resources">
<a href="{{resource['resource']}}">{{resource["description"]}}</a>
</div>
{% endfor %}
</div>
</div>
</div>
{% endfor %}
</div>
</div>
{% endblock %}

1
thsf.pid Normal file
View File

@ -0,0 +1 @@
3629