Skip to content

Commit

Permalink
Add schedule
Browse files Browse the repository at this point in the history
  • Loading branch information
liZe committed Sep 15, 2024
1 parent b8dfbd1 commit 24df311
Show file tree
Hide file tree
Showing 24 changed files with 809 additions and 370 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,6 @@ build
__pycache__
*.css
*.css.map

# See schedule.py
token.key
5 changes: 4 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
.venv:
python3 -m venv .venv
.venv/bin/pip install setuptools frozen-flask flask libsass markdown2 beautifulsoup4 icalendar python-slugify
.venv/bin/pip install setuptools frozen-flask flask libsass markdown2 icalendar python-slugify babel

install: .venv

Expand All @@ -15,6 +15,9 @@ serve-static: .venv
@echo -e "\nHome page available at \033[0;33mhttp://localhost:8000/index.html\033[0m\n"
.venv/bin/python -m http.server 8000 -d build

schedule: .venv
.venv/bin/python schedule.py

deploy: static
rsync -vazh --delete build/2024/ [email protected]:/var/www/pycon.fr/2024/

Expand Down
127 changes: 48 additions & 79 deletions pyconfr.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
from urllib.request import urlopen
from urllib.error import HTTPError
from xml.etree import ElementTree
import json
from pathlib import Path

from bs4 import BeautifulSoup
from flask import Flask, Response, render_template, url_for
from babel.dates import format_date, format_time, format_timedelta
from datetime import date, time, timedelta
from flask import Flask, Response, render_template
from flask_frozen import Freezer
from icalendar import Calendar
from markdown2 import Markdown
Expand All @@ -17,105 +17,74 @@
'css_path': 'static/css',
'wsgi_path': '/2024/static/css',
'strip_extension': True}})
with (Path(app.root_path) / 'schedule.json').open() as fd:
SCHEDULE = json.load(fd)


@app.template_filter()
def slug(string):
return slugify(string, max_length=30)


TALK_CATEGORIES = {
slug(talk['submission_type']['en']): talk['submission_type']
for dates in
tuple(SCHEDULE['schedule'].values()) + tuple(SCHEDULE['sprints'].values())
for hours in dates.values()
for talk in hours.values()
}


@app.template_filter()
def format_duration(minutes):
return format_timedelta(
timedelta(seconds=minutes*60), threshold=10, format='short')


@app.template_filter()
def format_day(day, lang):
day_date = date.fromisoformat(day)
return format_date(day_date, format='full', locale=lang)


@app.template_filter()
def format_minutes(minutes, lang):
hour_time = time(int(minutes) // 60, int(minutes) % 60)
return format_time(hour_time, format='short', locale=lang)


@app.template_filter()
def markdown(string):
return Markdown().convert(string)


@app.route('/')
@app.route('/2024/')
@app.route('/2024/<lang>/<name>.html')
def page(name='index', lang='fr'):
return render_template(
f'{lang}/{name}.jinja2.html', page_name=name, lang=lang)
f'{lang}/{name}.jinja2.html', page_name=name, lang=lang,
schedule=SCHEDULE)


@app.route('/2024/<lang>/talks/<category>.html')
def talks(lang, category):
try:
with urlopen('https://cfp-2024.pycon.fr/schedule/xml/') as fd:
tree = ElementTree.fromstring(fd.read().decode('utf-8'))
except HTTPError:
return []
talks = []
for day in tree.findall('.//day'):
for event in day.findall('.//event'):
talk = {child.tag: child.text for child in event}
talk['person'] = ', '.join(
person.text for person in event.findall('.//person'))
talk['id'] = event.attrib['id']
talk['day'] = day.attrib['date']
if talk['type'] != category:
continue
if 'description' in talk:
talk['description'] = Markdown().convert(talk['description'])
talks.append(talk)
return render_template(
f'{lang}/talks.jinja2.html', category=category, talks=talks, lang=lang)
f'{lang}/talks.jinja2.html', lang=lang, page_name='talks',
category=category, title=TALK_CATEGORIES[category][lang],
schedule=SCHEDULE, categories=TALK_CATEGORIES)


@app.route('/2024/<lang>/full-schedule.html')
def schedule(lang):
try:
with urlopen('https://cfp-2024.pycon.fr/schedule/html/') as fd:
html = fd.read().decode('utf-8')
except HTTPError:
html = ""

if lang == 'fr':
html = (
html
.replace('Room', 'Salle')
.replace('Saturday 18 February', 'Samedi 18 février')
.replace('Sunday 19 February', 'Dimanche 19 février'))
else:
for minute in (0, 30):
html = html.replace(f'12:{minute:02}', f'12:{minute:02} PM')
for hour in range(9, 12):
html = html.replace(
f'{hour:02}:{minute:02}', f'{hour:02}:{minute:02} AM')
for hour in range(13, 19):
html = html.replace(
f'{hour:02}:{minute:02}',
f'{hour-12:02}:{minute:02} PM')

# Insert links in the table
soup = BeautifulSoup(html, 'html.parser')
conf_colors = {
'#fe6f61': '1h',
'#f6b36a': '30m',
'#378899': 'workshop',
'#fce16b': 'plenary',
}
for color, kind in conf_colors.items():
for td in soup.find_all('td', attrs={'bgcolor': color}):
title = next(td.children)
anchor = slug(title)
href = url_for('talks', lang=lang, category=kind, _anchor=anchor)
link = soup.new_tag('a', href=href, target='_parent')
title.wrap(link)

for tag in soup.find_all(lambda tag: 'style' in tag.attrs):
del tag.attrs['style']

return render_template('schedule.jinja2.html', lang=lang, data=soup)
return render_template(
'schedule.jinja2.html', page_name='full-schedule', lang=lang,
schedule=SCHEDULE)


@app.route('/2024/pyconfr-2024.ics')
def calendar():
try:
with urlopen('https://cfp-2024.pycon.fr/schedule/ics/') as fd:
calendar = Calendar.from_ical(fd.read())
except HTTPError:
calendar = Calendar()

# Delete sprints
calendar.subcomponents = [
event for event in calendar.subcomponents
if event['DTSTART'].dt.day != 16]

calendar = Calendar.from_ical('TODO')
return Response(calendar.to_ical(), mimetype='text/calendar')


Expand Down
1 change: 1 addition & 0 deletions schedule.json

Large diffs are not rendered by default.

151 changes: 151 additions & 0 deletions schedule.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import json
from datetime import date, datetime, time
from pathlib import Path
from sys import exit
from urllib.request import Request, urlopen

# Constants, should be changed each year.

URL_BASE = "https://cfp.pycon.fr/api/events/pyconfr-2024/"
TALKS_URL = f"{URL_BASE}submissions/?limit=1000&questions=all"
ROOMS_URL = f"{URL_BASE}rooms/?limit=1000"
TOKEN_FILE = Path("token.key")
OUTPUT = Path("schedule.json")
if TOKEN_FILE.is_file():
TOKEN = TOKEN_FILE.read_text()
else:
print(f"ERROR: Please put your Pretalx token in the {TOKEN_FILE} file.")
exit()

SPRINT_DAYS = (
date(year=2024, month=10, day=31),
date(year=2024, month=11, day=1),
)
CONFERENCE_DAYS = (
date(year=2024, month=11, day=2),
date(year=2024, month=11, day=3),
)
DAY_START_TIME = time(hour=8, minute=30)
DAY_STOP_TIME = time(hour=18, minute=0)
SLOT_MINUTES = 30

EXTRA = {
"2024-11-02": {
"510": {
"id": "saturday-breakfast",
"title": {
"en": "Breakfast",
"fr": "Petit-déjeuner",
}
},
"750": {
"id": "saturday-lunch",
"title": {
"en": "Lunch",
"fr": "Déjeuner",
}
},
"960": {
"id": "saturday-snack",
"title": {
"en": "Snack Time",
"fr": "Goûter",
}
},
},
"2024-11-03": {
"510": {
"id": "sunday-breakfast",
"title": {
"en": "Breakfast",
"fr": "Petit-déjeuner",
}
},
"750": {
"id": "sunday-lunch",
"title": {
"en": "Lunch",
"fr": "Déjeuner",
}
},
},
}


# Define some util functions.

def to_minutes(time):
"""Get number of minutes in datetime.time."""
return time.hour * 60 + time.minute

def to_time(minutes):
"""Generate datetime.time containing given minutes."""
return time(hour=minutes//60, minute=minutes%60)

def clean_talk(talk):
"""Remove non-public data from talks"""
for speaker in talk["speakers"]:
del speaker["email"]
for key in ("do_not_record", "notes", "internal_notes"):
del talk[key]


# Check constants consistency.

assert DAY_START_TIME < DAY_STOP_TIME
assert to_minutes(DAY_START_TIME) % SLOT_MINUTES == 0
assert to_minutes(DAY_STOP_TIME) % SLOT_MINUTES == 0


# Download talks and rooms from API.

print("Downloading talks")
request = Request(TALKS_URL, headers={"Authorization": f"Token {TOKEN}"})
response = urlopen(request)
talks = tuple(talk for talk in json.loads(response.read())["results"] if talk["slot"])

print("Downloading rooms")
request = Request(ROOMS_URL, headers={"Authorization": f"Token {TOKEN}"})
response = urlopen(request)
rooms = sorted(json.loads(response.read())["results"], key=lambda room: room["position"])
rooms_dict = {room["id"]: room for room in rooms}


# Build table for conferences.

print("Generating schedule")
slots = range(to_minutes(DAY_START_TIME), to_minutes(DAY_STOP_TIME), SLOT_MINUTES)
hours = [to_time(minutes) for minutes in slots]
schedule = {day.isoformat(): {to_minutes(hour): {} for hour in hours} for day in CONFERENCE_DAYS}
sprints = {day.isoformat(): {to_minutes(hour): {} for hour in hours} for day in SPRINT_DAYS}
for talk in talks:
slot = talk["slot"]
# We assume that talks and schedule share the same timezone.
start = datetime.fromisoformat(slot["start"])
end = datetime.fromisoformat(slot["end"])
clean_talk(talk)
slot_start_minutes = to_minutes(start) // SLOT_MINUTES * SLOT_MINUTES
slot_start = to_time(slot_start_minutes)
if start.date() in CONFERENCE_DAYS:
schedule[start.date().isoformat()][to_minutes(slot_start)][slot["room_id"]] = talk
rooms_dict[slot["room_id"]]["in_conferences"] = True
elif start.date() in SPRINT_DAYS:
sprints[start.date().isoformat()][to_minutes(slot_start)][slot["room_id"]] = talk
rooms_dict[slot["room_id"]]["in_sprints"] = True
else:
print("Wrong date for talk:", talk)

print(f"Writing schedule to {OUTPUT}")
OUTPUT.write_text(json.dumps({
"schedule": schedule,
"sprints": sprints,
"rooms": rooms,
"extra": EXTRA,
"speakers": {
speaker["code"]: speaker
for hours in schedule.values()
for rooms in hours.values()
for room in rooms.values()
for speaker in room["speakers"]
},
}))
Binary file removed static/fonts/MonaSans-Bold.ttf
Binary file not shown.
Binary file removed static/fonts/MonaSans-Regular.ttf
Binary file not shown.
Binary file removed static/fonts/MonaSansCondensed-ExtraBold.ttf
Binary file not shown.
Binary file added static/fonts/MonaSans[slnt,wdth,wght].woff2
Binary file not shown.
Binary file added static/images/breakfast.webp
Binary file not shown.
Binary file added static/images/lunch.webp
Binary file not shown.
Binary file added static/images/snack.webp
Binary file not shown.
Loading

0 comments on commit 24df311

Please sign in to comment.