-
Notifications
You must be signed in to change notification settings - Fork 84
Village long description #1863
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Village long description #1863
Changes from all commits
f51feb3
362ee57
2ef44b9
fdef7e0
4930ec4
a16d97f
bb2be5b
2a1227f
911877f
955379d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 | ||
|
|
@@ -41,7 +46,7 @@ def register() -> ResponseValue: | |
| db.session.commit() | ||
|
|
||
| flash("Your village registration has been received, thanks! You can edit it below.") | ||
| return redirect(url_for(".edit", year=event_year(), village_id=village.id)) | ||
| return redirect(url_for(".view", year=event_year(), village_id=village.id)) | ||
|
|
||
| return render_template("villages/register.html", form=form) | ||
|
|
||
|
|
@@ -65,6 +70,81 @@ 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) | ||
| show_edit = ( | ||
| current_user.village | ||
| and current_user.village.id == village_id | ||
| and current_user.village_membership.admin | ||
| ) | ||
|
|
||
| return render_template( | ||
| "villages/view.html", | ||
| village=village, | ||
| show_edit=show_edit, | ||
| village_long_description_html=( | ||
| render_markdown(village.long_description) if village.long_description else None | ||
| ), | ||
| ) | ||
|
|
||
|
|
||
| def render_markdown(markdown_text: str) -> Markup: | ||
| """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"}), | ||
| link_rel="noopener nofollow", # default includes noreferrer but not nofollow | ||
| ) | ||
| 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>' | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't really like having onload here; is it possible to move attaching the message handler elsewhere? There are a few things which are a bit weird about doing this in onload anyway:
You'll probably need to do some iterating over the available |
||
| 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) | ||
| rendered_long_description = ( | ||
| render_markdown2(village.long_description) if village.long_description else None | ||
| ) | ||
|
|
||
| return render_template( | ||
| "villages/view2.html", | ||
| village=village, | ||
| village_long_description_html=rendered_long_description, | ||
| ) | ||
|
|
||
|
|
||
| def render_markdown2(markdown_text: str) -> Markup: | ||
| """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"}), | ||
| link_rel="noopener nofollow", # default includes noreferrer but not nofollow | ||
| ) | ||
| innerHtml = f""" | ||
| <link rel="stylesheet" href="/static/css/main.css"> | ||
| <div id="emf-container" style="min-height: 100%;"> | ||
| <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: | ||
|
|
@@ -84,13 +164,13 @@ def edit(year: int, village_id: int) -> ResponseValue: | |
| else: | ||
| # All good, update DB | ||
| for venue in village.venues: | ||
| if venue.name == village.name: | ||
| if venue.name == village.name and form.name.data is not None: | ||
| # Rename a village venue if it exists and has the old name. | ||
| venue.name = form.name.data | ||
|
|
||
| form.populate_obj(village) | ||
| db.session.commit() | ||
| flash("Your village registration has been updated.") | ||
| return redirect(url_for(".edit", year=year, village_id=village_id)) | ||
| return redirect(url_for(".view", year=year, village_id=village_id)) | ||
|
|
||
| return render_template("villages/edit.html", form=form, village=village) | ||
| 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; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,6 +20,7 @@ | |
| @use "./_tickets.scss"; | ||
| @use "./_responsive_table.scss"; | ||
| @use "./_sponsorship.scss"; | ||
| @use "./_village.scss"; | ||
| @use "./volunteer_schedule.scss"; | ||
|
|
||
| @font-face { | ||
|
|
@@ -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"); | ||
| } | ||
| } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: please leave the ending newline in.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. /me glares at the .editorconfig |
||
| 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; | ||
| // } | ||
|
Comment on lines
+11
to
+15
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, I think we just ignore the origin here anyway - in any case, as per the other comment we probably want to check on evt.source. |
||
|
|
||
| if (evt.data.type === "frame-resized") { | ||
| iFrameEle.style.height = evt.data.value + "px"; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| window.listenForFrameResizedMessages = listenForFrameResizedMessages; | ||
| window.sendFrameResizedMessage = sendFrameResizedMessage; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: trailing newline please
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You might also want to bind a |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| """Add long_description to village | ||
|
|
||
| Revision ID: 8c17cee05585 | ||
| Revises: 53220373bfde | ||
| Create Date: 2025-10-25 09:40:24.997019 | ||
|
|
||
| """ | ||
|
|
||
| # revision identifiers, used by Alembic. | ||
| revision = '8c17cee05585' | ||
| down_revision = '53220373bfde' | ||
|
|
||
| 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 ### |
| 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;"> | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We probably want some other mechanism here (other than onload). Possibly split the resize JS into two (one for the parent, one for the child)? |
||
| {% block document %} | ||
| <div id="emf-container" style="min-height: 100%;"> | ||
| <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> | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
probably
inner_htmlto keep with the snake-case standard?