Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ repos:
- id: ruff-format
- id: ruff-check

- repo: git@github.com:pre-commit/pre-commit-hooks
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.4.0
hooks:
- id: no-commit-to-branch
Expand Down
3 changes: 3 additions & 0 deletions apps/villages/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
class VillageForm(Form):
name = StringField("Village Name", [Length(2, 25)])
description = TextAreaField("Description", [Optional()])
long_description = TextAreaField("Long Description", [Optional()])
url = StringField("URL", [URL(), Optional()])

num_attendees = IntegerField("Number of People", [Optional()])
Expand Down Expand Up @@ -46,6 +47,7 @@ class VillageForm(Form):
def populate(self, village: Village) -> None:
self.name.data = village.name
self.description.data = village.description
self.long_description.data = village.long_description
self.url.data = village.url

requirements = village.requirements
Expand All @@ -60,6 +62,7 @@ def populate_obj(self, village: Village) -> None:
assert self.name.data is not None
village.name = self.name.data
village.description = self.description.data
village.long_description = self.long_description.data
village.url = self.url.data

if village.requirements is None:
Expand Down
65 changes: 65 additions & 0 deletions apps/villages/views.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import html

import markdown
import nh3
from flask import abort, flash, redirect, render_template, request, url_for
from flask.typing import ResponseValue
from flask_login import current_user, login_required
from markupsafe import Markup
from sqlalchemy import exists, select

from main import db
Expand Down Expand Up @@ -65,6 +70,66 @@ def main(year: int) -> ResponseValue:
)


@villages.route("/<int:year>/<int:village_id>")
def view(year: int, village_id: int) -> ResponseValue:
village = load_village(year, village_id)

return render_template(
"villages/view.html",
village=village,
village_long_description_html=render_markdown(village.long_description),
)


def render_markdown(markdown_text):
"""Render untrusted markdown

This doesn't have access to any templating unlike email markdown
which is from a trusted user so is pre-processed with jinja.
"""
extensions = ["markdown.extensions.nl2br", "markdown.extensions.smarty", "tables"]
contentHtml = nh3.clean(
markdown.markdown(markdown_text, extensions=extensions), tags=(nh3.ALLOWED_TAGS - {"img"})
)
innerHtml = render_template("sandboxed-iframe.html", body=Markup(contentHtml))
iFrameHtml = f'<iframe sandbox="allow-scripts" class="embedded-content" srcdoc="{html.escape(innerHtml, True)}" onload="javascript:window.listenForFrameResizedMessages(this);"></iframe>'
return Markup(iFrameHtml)


@villages.route("/<int:year>/<int:village_id>/view2")
def view2(year: int, village_id: int) -> ResponseValue:
village = load_village(year, village_id)

return render_template(
"villages/view2.html",
village=village,
village_long_description_html=render_markdown2(village.long_description),
)


def render_markdown2(markdown_text):
"""Render untrusted markdown

This doesn't have access to any templating unlike email markdown
which is from a trusted user so is pre-processed with jinja.
"""
extensions = ["markdown.extensions.nl2br", "markdown.extensions.smarty", "tables"]
contentHtml = nh3.clean(
markdown.markdown(markdown_text, extensions=extensions), tags=(nh3.ALLOWED_TAGS - {"img"})
)
innerHtml = f"""
<link rel="stylesheet" href="/static/css/main.css">
<div id="emf-container" >
<div class="emf-row">
<div class="emf-col" role="main">
{Markup(contentHtml)}
</div>
</div>
</div>"""
iFrameHtml = f'<iframe sandbox class="embedded-content" srcdoc="{html.escape(innerHtml, True)}"></iframe>'
return Markup(iFrameHtml)


@villages.route("/<int:year>/<int:village_id>/edit", methods=["GET", "POST"])
@login_required
def edit(year: int, village_id: int) -> ResponseValue:
Expand Down
16 changes: 16 additions & 0 deletions css/_village.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
.village-form {
textarea#description {
height: 100px;
}

textarea#long_description {
height: 200px;
}
}

.embedded-content {
width: 100%;
height: 450px; // default height, recalculated by javascript.
border: none;
overflow: hidden;
}
3 changes: 2 additions & 1 deletion css/main.scss
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
@use "./_tickets.scss";
@use "./_responsive_table.scss";
@use "./_sponsorship.scss";
@use "./_village.scss";
@use "./volunteer_schedule.scss";

@font-face {
Expand All @@ -40,4 +41,4 @@
src: local(""),
url("../static/fonts/raleway-v22-latin-ext_latin-700.woff2") format("woff2"),
url("../static/fonts/raleway-v22-latin-ext_latin-700.woff") format("woff");
}
}
24 changes: 24 additions & 0 deletions js/sandboxed-iframe.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
function sendFrameResizedMessage() {
//postMessage to set iframe height
window.parent.postMessage({ "type": "frame-resized", "value": document.body.parentElement.scrollHeight }, '*');
}

function listenForFrameResizedMessages(iFrameEle) {
window.addEventListener('message', receiveMessage, false);

function receiveMessage(evt) {
console.log("Got message: " + JSON.stringify(evt.data) + " from origin: " + evt.origin);
// Do we trust the sender of this message?
// origin of sandboxed iframes is null but is this a useful check?
// if (evt.origin !== null) {
// return;
// }

if (evt.data.type === "frame-resized") {
iFrameEle.style.height = evt.data.value + "px";
}
}
}

window.listenForFrameResizedMessages = listenForFrameResizedMessages;
window.sendFrameResizedMessage = sendFrameResizedMessage;
2 changes: 2 additions & 0 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,8 @@ def send_security_headers(response):
"'unsafe-hashes'",
"'sha256-2rvfFrggTCtyF5WOiTri1gDS8Boibj4Njn0e+VCBmDI='", # return false;
"'sha256-gC0PN/M+TSxp9oNdolzpqpAA+ZRrv9qe1EnAbUuDmk8='", # return modelActions.execute('notify');
"'sha256-GtgSCbDPm83G2B75TQfbv5TR/nqHF4WnrnN+njVmQFU='", # javascript:window.listenForFrameResizedMessages(this);
"'sha256-dW+ze6eYWjNQB4tjHLKsdbtI4AqFRK/FpdEu/ZCxmLc='", # javascript:window.sendFrameResizedMessage()
]

if app.config.get("DEBUG_TB_ENABLED"):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""Add long_description to village

Revision ID: 6646de329427
Revises: 5062a9a72efc
Create Date: 2025-10-02 20:34:55.746599

"""

# revision identifiers, used by Alembic.
revision = '6646de329427'
down_revision = '5062a9a72efc'

from alembic import op
import sqlalchemy as sa


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('village', schema=None) as batch_op:
batch_op.add_column(sa.Column('long_description', sa.String(), nullable=True))

with op.batch_alter_table('village_version', schema=None) as batch_op:
batch_op.add_column(sa.Column('long_description', sa.String(), autoincrement=False, nullable=True))

# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('village_version', schema=None) as batch_op:
batch_op.drop_column('long_description')

with op.batch_alter_table('village', schema=None) as batch_op:
batch_op.drop_column('long_description')

# ### end Alembic commands ###
2 changes: 2 additions & 0 deletions models/village.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class Village(BaseModel):

name: Mapped[str] = mapped_column(unique=True)
description: Mapped[str | None]
long_description: Mapped[str | None]
url: Mapped[str | None]
location: Mapped[WKBElement | None] = mapped_column(Geometry("POINT", srid=4326, spatial_index=False))

Expand Down Expand Up @@ -98,6 +99,7 @@ def get_export_data(cls):
v.id: {
"name": v.name,
"description": v.description,
"long_description": v.long_description,
"url": v.url,
"location": v.latlon,
}
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ dependencies = [
"logging_tree~=1.9",
"lxml>=6.0.0",
"markdown~=3.1",
"nh3>=0.3.1",
"pendulum~=3.1",
"pillow<12.0",
"playwright>=1.43.0,<2",
Expand Down
1 change: 1 addition & 0 deletions rsbuild.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export default defineConfig({
"volunteer_schedule.js": './js/volunteer-schedule.js',
"event_tickets.js": './js/event-tickets.js',
"arrivals.js": './js/arrivals.js',
"sandboxed-iframe.js": './js/sandboxed-iframe.js',

"admin.scss": './css/admin.scss',
"arrivals.scss": './css/arrivals.scss',
Expand Down
2 changes: 1 addition & 1 deletion templates/about/villages.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
title: Villages
---
# Villages
A village is a group of friends, like-minded people, colleagues or even families camping together at EMF. Maybe your village is a collection of sub villages, or maybe it’s just your hackerspace. A village can be based around themes, shared passions and existing communities - great places in the past have been the Maths village, the HAB Village and groups like Milliways and the Scottish Consulate. You can find the list of currently registered villages for EMF 2024 at [emfcamp.org/villages/{{ event_year }}](/villages/{{ event_year }}).
A village is a group of friends, like-minded people, colleagues or even families camping together at EMF. Maybe your village is a collection of sub villages, or maybe it’s just your hackerspace. A village can be based around themes, shared passions and existing communities - great places in the past have been the Maths village, the HAB Village and groups like Milliways and the Scottish Consulate. You can find the list of currently registered villages for EMF {{ event_year }} at [emfcamp.org/villages/{{ event_year }}](/villages/{{ event_year }}).

If there is a village based around something you are interested in, then you should definitely visit that village during the event to see what's going on. Or you could even join that village to help make more things go on. If there's not a village based around your interests, then you can start your own village and others will probably join you.

Expand Down
27 changes: 27 additions & 0 deletions templates/sandboxed-iframe.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{# The source of an iFrame used to sandbox user-controlled HTML #}
{# Once loaded, this emits an event with it's content's size which the parent page can listen for to adapt the iFrame's height #}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
{% block css -%}
<link rel="stylesheet" href="{{ static_url_for('static', filename="css/main.css") }}">
{% endblock -%}
{% block head -%}{% endblock -%}
</head>
<body itemscope itemtype="http://schema.org/WebPage" {% block body_class %}{% endblock %} onload="javascript:window.sendFrameResizedMessage()" style="overflow: hidden;">
{% block document %}
<div id="emf-container">
<div class="emf-row">
<div class="emf-col {{ main_class }}" role="main" {% if self.content_scope() -%}
itemscope itemtype="{% block content_scope %}{% endblock %}"
{%- endif %}>
{% block body -%} {{ body }} {% endblock -%}
</div>
</div>
</div>
{% endblock %}
<script src="{{static_url_for('static', filename="js/sandboxed-iframe.js")}}"></script>
{% block foot -%}{% endblock -%}
</body>
</html>
6 changes: 4 additions & 2 deletions templates/villages/_form.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<div class="well">
<form method="post" class="form-horizontal" role="form">
<form method="post" class="form-horizontal village-form" role="form">
{{ form.hidden_tag() }}
<fieldset>
<legend>Basic Details</legend>
Expand All @@ -8,7 +8,9 @@
{{ render_field(form.name, horizontal=9, placeholder="My Village") }}
{{ render_field(form.url, horizontal=9, placeholder="") }}
{{ render_field(form.description, horizontal=9,
placeholder="A description of your village. Who are you, what cool things will you be doing?") }}
placeholder="A short description of your village. Who are you, what cool things will you be doing?") }}
{{ render_field(form.long_description, horizontal=9,
placeholder="A longer description of your village using Markdown. Project details etc.") }}
</fieldset>
<fieldset>
<legend>Requirements</legend>
Expand Down
1 change: 1 addition & 0 deletions templates/villages/admin/info.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ <h3>Village Info: {{village.name}}</h3>

{{ render_field(form.name) }}
{{ render_field(form.description) }}
{{ render_field(form.long_description) }}
{{ render_field(form.url) }}
{{ render_field(form.lat) }}
{{ render_field(form.lon) }}
Expand Down
25 changes: 25 additions & 0 deletions templates/villages/view.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{% extends "base.html" %}
{% block title %}Village: {{village.name}}{% endblock %}
{% block foot %}<script src="{{static_url_for('static', filename="js/sandboxed-iframe.js")}}"></script>{% endblock%}
{% block body %}
{% if village.url %}
<h2><a href="{{ village.url }}" rel="nofollow" target="_blank">{{village.name}}</a></h2>
{% else %}
<h2>{{village.name}}</h2>
{% endif %}

{% if village.description %}
<p>{{village.description}}</p>
{% endif %}

{% if village_long_description_html %}
<p>{{village_long_description_html}}</p>
{% endif %}

{# TODO: embed a map somehow? #}
{% if village.map_link %}
<a href="{{ village.map_link }}">📍&nbsp;Map</a>
{% endif %}

{# TODO: Can we show what's on at the village venues here? #}
{% endblock %}
24 changes: 24 additions & 0 deletions templates/villages/view2.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{% extends "base.html" %}
{% block title %}Village: {{village.name}}{% endblock %}
{% block body %}
{% if village.url %}
<h2><a href="{{ village.url }}" rel="nofollow" target="_blank">{{village.name}}</a></h2>
{% else %}
<h2>{{village.name}}</h2>
{% endif %}

{% if village.description %}
<p>{{village.description}}</p>
{% endif %}

{% if village_long_description_html %}
<p>{{village_long_description_html}}</p>
{% endif %}

{# TODO: embed a map somehow? #}
{% if village.map_link %}
<a href="{{ village.map_link }}">📍&nbsp;Map</a>
{% endif %}

{# TODO: Can we show what's on at the village venues here? #}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was going to say that I'm not sure whether there's a link between Venue and Village, but on closer look Venue has a village_id field :)

@SamLR is this planned to still be the case?

{% endblock %}
6 changes: 1 addition & 5 deletions templates/villages/villages.html
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,7 @@ <h3>List of Villages</h3>
<tbody>
{% for village in villages %}
<tr>
{% if village.url %}
<td><a href="{{ village.url }}" rel="nofollow" target="_blank">{{village.name}}</a></td>
{% else %}
<td>{{village.name}}</td>
{% endif %}
<td><a href="{{ url_for('villages.view', year=event_year, village_id=village.id) }}">{{village.name}}</a></td>
<td style="overflow-wrap: anywhere">{{village.description}}</td>
{% if any_village_located %}
<td style="width: 1px; white-space: nowrap;">
Expand Down
Loading
Loading