Improvements

This commit is contained in:
Edward Betts 2023-09-15 23:34:41 +05:30
parent 4e5ee195dd
commit a0df624f16
14 changed files with 1021 additions and 59 deletions

View file

@ -3,6 +3,7 @@
import sqlalchemy import sqlalchemy
import sqlalchemy.orm.decl_api import sqlalchemy.orm.decl_api
from sqlalchemy import func from sqlalchemy import func
from sqlalchemy.dialects import postgresql
from sqlalchemy.ext.associationproxy import association_proxy from sqlalchemy.ext.associationproxy import association_proxy
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.ext.orderinglist import ordering_list from sqlalchemy.ext.orderinglist import ordering_list
@ -39,6 +40,32 @@ class Conference(TimeStampedModel):
acronym = Column(String) acronym = Column(String)
url = Column(String) url = Column(String)
schedule_xml_url = Column(String) schedule_xml_url = Column(String)
short_name = Column(String, unique=True)
people_detail = relationship(
"ConferencePerson", lazy="dynamic", back_populates="conference"
)
people = association_proxy("people_detail", "person")
events = relationship(
"Event",
order_by="Event.event_date",
back_populates="conference",
lazy="dynamic",
)
class ConferencePerson(Base):
__tablename__ = "conference_person"
conference_id = Column(Integer, ForeignKey("conference.id"), primary_key=True)
person_id = Column(Integer, ForeignKey("person.id"), primary_key=True)
named_as = Column(String)
bio = Column(String)
slug = Column(String)
url = Column(String)
person = relationship("Person", back_populates="conferences_association")
conference = relationship("Conference", back_populates="people_detail")
class Event(TimeStampedModel): class Event(TimeStampedModel):
@ -48,7 +75,7 @@ class Event(TimeStampedModel):
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
conference_id = Column(Integer, ForeignKey("conference.id"), nullable=False) conference_id = Column(Integer, ForeignKey("conference.id"), nullable=False)
event_date = Column(DateTime) event_date = Column(DateTime)
day = Column(Integer) # day = Column(Integer)
guid = Column(String) guid = Column(String)
start = Column(String) start = Column(String)
duration = Column(String) duration = Column(String)
@ -56,22 +83,23 @@ class Event(TimeStampedModel):
track = Column(String) track = Column(String)
slug = Column(String) slug = Column(String)
title = Column(String, nullable=False) title = Column(String, nullable=False)
abstract = Column(String)
description = Column(String) description = Column(String)
event_type = Column(String) event_type = Column(String)
url = Column(String) url = Column(String)
conference = relationship("Conference", backref="events") conference = relationship("Conference", back_populates="events")
people_association = relationship( people_detail = relationship(
"EventPerson", "EventPerson",
order_by="EventPerson.position", order_by="EventPerson.position",
back_populates="event", back_populates="event",
collection_class=ordering_list("position"), collection_class=ordering_list("position"),
) )
people = association_proxy( people = association_proxy(
"people_association", "people_detail",
"person", "person",
creator=lambda person: EventPerson(person=person), creator=lambda i: EventPerson(person=i[0], named_as=i[1]),
) )
@ -83,9 +111,39 @@ class Person(TimeStampedModel):
name = Column(String) name = Column(String)
wikidata_qid = Column(String) wikidata_qid = Column(String)
gender = Column(String) gender = Column(String)
wikidata_photo = Column(postgresql.ARRAY(String))
events_association = relationship("EventPerson", back_populates="person") events_association = relationship(
events = association_proxy("event_association", "event") "EventPerson",
back_populates="person",
lazy="dynamic",
)
events = association_proxy("events_association", "event")
conferences_association = relationship("ConferencePerson", back_populates="person")
conferences = association_proxy("conference_association", "conference")
# photos = relationship("PersonPhoto", back_populates="person")
def events_by_time(self):
q = (
session.query(Event)
.join(EventPerson)
.filter(EventPerson.person == self)
.order_by(Event.event_date.desc())
)
return q
# class PersonPhoto(TimeStampedModel):
# """Person photo."""
#
# __tablename__ = "person_photo"
# person_id = Column(Integer, ForeignKey("person.id"), primary_key=True)
# source_filename = Column(String)
#
# person = relationship("Person", back_populates="photos")
class EventPerson(Base): class EventPerson(Base):
@ -95,7 +153,6 @@ class EventPerson(Base):
event_id = Column(Integer, ForeignKey("event.id"), primary_key=True) event_id = Column(Integer, ForeignKey("event.id"), primary_key=True)
person_id = Column(Integer, ForeignKey("person.id"), primary_key=True) person_id = Column(Integer, ForeignKey("person.id"), primary_key=True)
position = Column(Integer, nullable=False) position = Column(Integer, nullable=False)
named_as = Column(String)
person = relationship("Person", back_populates="events_association") person = relationship("Person", back_populates="events_association")
event = relationship("Event", back_populates="people_association") event = relationship("Event", back_populates="people_detail")

View file

@ -1,12 +1,15 @@
#!/usr/bin/python3 #!/usr/bin/python3
import datetime import datetime
import os
import sys
import lxml.etree import lxml.etree
from confarchive import database, model from confarchive import database, model
DB_URL = "postgresql:///confarchive" DB_URL = "postgresql:///confarchive"
schedules_loc = "/home/edward/src/2022/conference-gender-mix/schedules"
database.init_db(DB_URL) database.init_db(DB_URL)
@ -38,7 +41,7 @@ def read_field(root: Element, field: str) -> str | None:
def read_date_field(root: Element, field: str) -> datetime.date | None: def read_date_field(root: Element, field: str) -> datetime.date | None:
"""Read date from a field.""" """Read date from a field."""
value = read_field(root, field) value = read_field(root, field)
return parse_isodate(value) if value is not None else None return parse_isodate(value) if value else None
def read_required_field(root: Element, field: str) -> str: def read_required_field(root: Element, field: str) -> str:
@ -58,55 +61,99 @@ def conference_obj(root: Element) -> model.Conference:
e = root.find(".//conference") e = root.find(".//conference")
assert e is not None assert e is not None
start = read_date_field(e, "start") or read_date_field(e, "start_date")
days_str = read_field(e, "days")
days = int(days_str) if days_str else None
assert start
end: datetime.date | None
if days:
end = start + datetime.timedelta(days=days - 1)
else:
end = read_date_field(e, "end") or read_date_field(e, "end_date")
assert end
if not start:
print(lxml.etree.tostring(e, encoding=str))
sys.exit(1)
assert start and end and end >= start
return model.Conference( return model.Conference(
title=read_required_field(e, "title"), title=read_required_field(e, "title"),
start=read_date_field(e, "start"), start=start,
end=read_date_field(e, "end"), end=end,
timezone=read_field(e, "time_zone_name"), timezone=read_field(e, "time_zone_name"),
) )
def build_event_object( def build_event_object(
e: Element, person_lookup: dict[int, model.Person] e: Element, person_lookup: dict[str, model.Person]
) -> model.Event | None: ) -> model.Event | None:
"""Build an event object.""" """Build an event object."""
title = read_required_field(e, "title") title = read_field(e, "title")
if not_a_talk(title): guid = e.get("guid")
return None
room = read_field(e, "room") room = read_field(e, "room")
slug = read_field(e, "slug") slug = read_field(e, "slug")
description = read_field(e, "description") description = read_field(e, "description")
event_type = read_field(e, "type") event_type = read_field(e, "type")
url = read_field(e, "url") url = read_field(e, "url")
persons_element = e.find(".//persons") if title is None:
if persons_element is None: print("no title")
assert description is None and event_type is None
return None
persons = e.find(".//persons")
if persons is None or len(persons) == 0:
persons = e.findall(".//person")
if persons is None or len(persons) == 0:
return None return None
people = [] people = []
for p in persons_element: seen_person = set()
id_str = p.get("id") print("persons:", len(persons))
assert id_str is not None for p in persons:
people.append(person_lookup[int(id_str)]) name = p.text
print("peron:", name)
if name is None:
print("no name")
if name in seen_person:
print("seen already:", name)
if name is None or name in seen_person:
continue
seen_person.add(name)
people.append(model.EventPerson(person=person_lookup[name]))
print(title, people) if not people:
print("no people")
return None
return model.Event( return model.Event(
guid=guid,
title=title, title=title,
room=room, room=room,
slug=slug, slug=slug,
description=description, description=description,
event_type=event_type, event_type=event_type,
url=url, url=url,
people=people, people_detail=people,
) )
def schedule_has_person_ids(root: Element) -> bool:
"""People listed in schedule have ids."""
person = root.find(".//person")
assert person is not None
person_id = person.get("id")
return person_id is not None
def get_all_people(root: Element) -> list[tuple[int, str]]: def get_all_people(root: Element) -> list[tuple[int, str]]:
people: dict[int, str] = {} people: dict[int, str] = {}
for person in root.findall(".//person"): for person in root.findall(".//person"):
assert person.text assert person.text
person_id_str = person.get("id") person_id_str = person.get("id")
if not person_id_str:
print(lxml.etree.tostring(person, encoding=str))
assert person_id_str assert person_id_str
person_id = int(person_id_str) person_id = int(person_id_str)
existing = people.get(person_id) existing = people.get(person_id)
@ -118,36 +165,111 @@ def get_all_people(root: Element) -> list[tuple[int, str]]:
return sorted(people.items()) return sorted(people.items())
def load(filename: str) -> None: def get_people_names(root: Element) -> set[str]:
return {
normalize_name(person.text)
for person in root.findall(".//person")
if person.text
}
def normalize_name(n: str) -> str:
"""Normalize name."""
return " ".join(n.split()).strip()
def find_existing_person(name: str) -> model.Person | None:
# print("searching for:", name)
person = model.Person.query.filter(model.Person.name.ilike(name)).one_or_none()
assert person is None or isinstance(person, model.Person)
if person:
return person
person = model.ConferencePerson.query.filter(
model.ConferencePerson.named_as.ilike(name)
).one_or_none()
assert person is None or isinstance(person, model.Person)
if person:
return person
def load(filename: str, short_name: str) -> None:
"""Load conference schedule.""" """Load conference schedule."""
start = open(filename).read(15)
if start == "BEGIN:VCALENDAR" or start.startswith("{"):
return None
root = lxml.etree.parse(filename).getroot() root = lxml.etree.parse(filename).getroot()
conf = model.Conference.query.filter_by(short_name=short_name).one_or_none()
if conf:
assert conf.events.count() == 0
else:
conf = conference_obj(root) conf = conference_obj(root)
assert model.Conference.query.filter_by(title=conf.title).count() == 0
database.session.add(conf) database.session.add(conf)
print((conf.short_name, conf.title))
event_count = 0 event_count = 0
people = get_all_people(root) people_names = get_people_names(root)
person_lookup = {} person_lookup = {}
for person_id, name in people: for name in people_names:
person = model.Person.query.filter_by(name=name).first() cp = model.ConferencePerson.query.filter_by(
conference=conf, named_as=name
).one_or_none()
if cp and cp.person.events_association.count() == 0:
person = cp.person
for cp2 in person.conferences_association:
database.session.delete(cp2)
database.session.delete(cp.person)
database.session.commit()
person = find_existing_person(name)
if not person: if not person:
person = model.Person(name=name) person = model.Person(name=name)
database.session.add(person) database.session.add(person)
person_lookup[person_id] = person person_lookup[name] = person
for name, person in person_lookup.items():
if model.ConferencePerson.query.filter_by(
conference=conf, person=person
).one_or_none():
continue
conf_person = model.ConferencePerson(
conference=conf, person=person, named_as=name
)
database.session.add(conf_person)
for day in root.findall(".//day"): for day in root.findall(".//day"):
for room in root.findall(".//room"): day_index_str = day.get("index")
for event_element in root.findall(".//event"): # assert day_index_str is not None
# day_index = int(day_index_str)
print("day", day_index_str)
for event_element in day.findall(".//event"):
title = read_field(event_element, "title")
event = build_event_object(event_element, person_lookup) event = build_event_object(event_element, person_lookup)
if not event: if not event:
print(f"skip event: {title}")
continue continue
event.conference = conf event.conference = conf
print() # event.day = day_index
database.session.add(event) database.session.add(event)
event_count += 1 event_count += 1
if event_count > 10:
return None
load("/home/edward/src/2022/conference-gender-mix/schedules/debconf22")
if event_count > 1:
database.session.commit() database.session.commit()
for f in os.scandir(schedules_loc):
if f.is_dir():
continue
if f.name in {"datenspuren_2019"}:
continue
if not f.name.startswith("capitole_du_libre"):
continue
print(f.name)
load(f.path, f.name)

267
main.py
View file

@ -1,8 +1,19 @@
#!/usr/bin/python3 #!/usr/bin/python3
import flask import hashlib
import json
import os
import time
import typing
from typing import cast
from confarchive import database import flask
import requests
import sqlalchemy
from sqlalchemy import func, update
from werkzeug.wrappers import Response
from confarchive import database, model
app = flask.Flask(__name__) app = flask.Flask(__name__)
app.debug = True app.debug = True
@ -10,12 +21,260 @@ app.debug = True
app.config.from_object("config.default") app.config.from_object("config.default")
database.init_app(app) database.init_app(app)
wikidata_api = "https://www.wikidata.org/w/api.php"
def md5sum(s: str) -> str:
return hashlib.md5(s.encode("utf-8")).hexdigest()
def wikidata_search(q: str) -> list[dict[str, typing.Any]]:
q += " haswbstatement:P31=Q5"
q_md5 = md5sum(q)
cache_filename = os.path.join("cache", q_md5 + ".json")
if os.path.exists(cache_filename):
data = json.load(open(cache_filename))
else:
params: dict[str, str | int] = {
"action": "query",
"list": "search",
"format": "json",
"formatversion": 2,
"srsearch": q,
"srlimit": "max",
}
r = requests.get(wikidata_api, params=params)
open(cache_filename, "w").write(r.text)
data = r.json()
time.sleep(1)
return cast(dict[str, typing.Any], data["query"]["search"])
def wikidata_get_item(qid: str) -> typing.Any:
cache_filename = os.path.join("items", qid + ".json")
if os.path.exists(cache_filename):
item = json.load(open(cache_filename))
else:
params: dict[str, str | int] = {
"action": "wbgetentities",
"ids": qid,
"format": "json",
"formatversion": 2,
}
r = requests.get(wikidata_api, params=params)
item = r.json()["entities"][qid]
with open(cache_filename, "w") as f:
json.dump(item, f, indent=2)
time.sleep(0.1)
return item
def top_speakers() -> sqlalchemy.orm.query.Query:
q = (
database.session.query(model.Person, func.count())
.join(model.ConferencePerson)
.group_by(model.Person)
.order_by(func.count().desc())
.having(func.count() > 5)
)
return q
def top_speakers2() -> sqlalchemy.orm.query.Query:
q = (
database.session.query(model.Person, func.count())
.join(model.ConferencePerson)
.filter(model.Person.name.like("% %"))
.group_by(model.Person)
.order_by(func.count().desc())
.having(func.count() > 2)
)
# .order_by(func.length(model.Person.name).desc())
return q
def top_events() -> sqlalchemy.orm.query.Query:
q = (
database.session.query(model.Event.title, func.count())
.group_by(model.Event.title)
.order_by(func.count().desc())
.having(func.count() > 5)
)
return q
@app.route("/person/<int:person_id>", methods=["GET", "POST"])
def person(person_id: int) -> str | Response:
item = model.Person.query.get(person_id)
if flask.request.method == "POST":
item.wikidata_qid = flask.request.form["wikidata_qid"] or None
item.name = flask.request.form["name"]
database.session.commit()
assert flask.request.endpoint
return flask.redirect(
flask.url_for(flask.request.endpoint, person_id=person_id)
)
return flask.render_template("person.html", item=item, Event=model.Event)
@app.route("/event/<int:event_id>")
def event_page(event_id: int) -> str:
item = model.Event.query.get(event_id)
return flask.render_template("event.html", item=item)
@app.route("/conference/<short_name>")
def conference_page(short_name: str) -> str:
item = model.Conference.query.filter_by(short_name=short_name).one_or_none()
if item is None:
flask.abort(404)
return flask.render_template("conference.html", item=item)
@app.route("/people")
def search_people() -> str:
search_for = flask.request.args["q"]
assert search_for
search_for = search_for.strip()
q = model.Person.query.filter(model.Person.name.ilike(f"%{search_for}%")).order_by(
model.Person.name
)
return flask.render_template("search_people.html", q=q, search_for=search_for)
@app.route("/merge", methods=["GET", "POST"])
def merge() -> str | Response:
if flask.request.method == "POST":
search_for = flask.request.form["q"]
item_ids_str = flask.request.form.getlist("person_id")
item_ids: list[int] = [int(i) for i in item_ids_str]
merge_to_id = min(item_ids)
other_ids = [i for i in item_ids if i != merge_to_id]
print(other_ids, "->", merge_to_id)
with database.session.begin():
print("update ConferencePerson")
database.session.execute(
update(model.ConferencePerson)
.where(model.ConferencePerson.person_id.in_(other_ids))
.values(person_id=merge_to_id)
)
print("update EventPerson")
database.session.execute(
update(model.EventPerson)
.where(model.EventPerson.person_id.in_(other_ids))
.values(person_id=merge_to_id)
)
print("delete people")
for person_id in other_ids:
item = model.Person.query.get(person_id)
database.session.delete(item)
endpoint = flask.request.endpoint
assert endpoint
return flask.redirect(flask.url_for(endpoint, q=search_for))
else:
search_for = flask.request.args["q"]
assert search_for
search_for = search_for.strip()
q = model.Person.query.filter(model.Person.name.ilike(f"%{search_for}%")).order_by(
model.Person.name
)
return flask.render_template("merge_people.html", q=q, search_for=search_for)
@app.route("/events")
def events_page() -> str:
search_for = flask.request.args.get("q")
if not search_for:
return flask.render_template("top_events.html", top_events=top_events())
q = model.Event.query.filter(model.Event.title.ilike(f"%{search_for}%")).order_by(
model.Event.title
)
return flask.render_template("search_events.html", q=q, search_for=search_for)
@app.route("/") @app.route("/")
def index() -> str: def index() -> str:
"""Start page.""" """Start page."""
return flask.render_template("index.html") if False:
q = (
model.Conference.query.order_by(model.Conference.start.desc())
.add_columns(
func.count(model.Event.id), func.count(model.ConferencePerson.person_id)
)
.group_by(model.Conference)
)
q = model.Conference.query.order_by(model.Conference.start.desc())
count = {
"conference": model.Conference.query.count(),
"event": model.Event.query.count(),
"person": model.Person.query.count(),
}
return flask.render_template("index.html", items=q, count=count)
@app.route("/speakers")
def top_speakers_page() -> str:
"""Top speakers page."""
return flask.render_template("top_speakers.html", top_speakers=top_speakers())
@app.route("/wikidata")
def link_to_wikidata() -> str:
items = []
for person, num in top_speakers2():
if person.wikidata_qid:
continue
search_hits = wikidata_search(f'"{person.name}"')
if not search_hits:
continue
if len(search_hits) > 10:
continue
hits = []
for search_hit in search_hits:
qid = search_hit["title"]
item = wikidata_get_item(qid)
if "en" in item["labels"]:
label = item["labels"]["en"]["value"]
else:
label = "[no english label]"
if "en" in item["descriptions"]:
description = item["descriptions"]["en"]["value"]
else:
description = "[no english description]"
hits.append(
{
"qid": qid,
"label": label,
"description": description,
}
)
items.append((person, num, hits))
return flask.render_template("wikidata.html", items=items)
if __name__ == "__main__": if __name__ == "__main__":
app.run(host="0.0.0.0") app.run(host="0.0.0.0", port=5002)

25
templates/base.html Normal file
View file

@ -0,0 +1,25 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}Xanadu{% endblock %}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-4bw+/aepP/YC94hEpVNVgiZdgIC5+VKNBQNGCHeKRQN+PtmoHDEXuppvnDJzQIu9" crossorigin="anonymous">
{% block style %}
{% endblock %}
</head>
<body>
{% block content %}
{% endblock %}
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.1/dist/js/bootstrap.bundle.min.js" integrity="sha384-HwwvtgBNo3bZJJLYd8oVXjrBZt8cqVSpeBNS5n7C8IVInixGAoxmnlMuBnhbgrkm" crossorigin="anonymous"></script>
{% block scripts %}
{% endblock %}
</body>
</html>

76
templates/conference.html Normal file
View file

@ -0,0 +1,76 @@
{% extends "base.html" %}
{% block title %}{{ item.title }}{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<h1>{{ item.title }}</h1>
<p><a href="{{ url_for("index") }}">home</a></p>
<ul>
<li>start: {{ item.start }}</li>
<li>end: {{ item.end }}</li>
{% if days %}
<li>days: {{ item.days }}</li>
{% endif %}
<li>short name: {{ item.short_name }}</li>
<li>country: {{ item.country or "n/a" }}</li>
</ul>
<h3>Talks</h3>
<p>{{ item.events.count() }} talks</p>
{% for event in item.events %}
<div class="card my-2">
<div class="card-body">
<h5 class="card-title">
<a href="{{ url_for("event_page", event_id=event.id) }}">{{ event.title }}</a>
</h5>
<h6 class="card-subtitle mb-2 text-body-secondary">
{% if event.event_date %}
{{ event.event_date.strftime("%d %b %Y at %H:%M") }}
{% else %}
event date missing
{% endif %}
</h6>
<p class="card-text">
{% if event.url %}
<a href="{{ event.url }}">talk on conference website</a>
{% endif %}
{% if event.abstract %}
<p class="card-text">
{% if "<" in event.abstract %}
{{ event.abstract | safe }}
{% else %}
{{ event.abstract }}
{% endif %}
</p>
{% endif %}
{% if event.description %}
<p class="card-text">
{% if "<" in event.description %}
{{ event.description | safe }}
{% else %}
{{ event.description }}
{% endif %}
</p>
{% endif %}
<p class="card-text">
Speakers:
{% for p in event.people %}
<a href="{{ url_for("person", person_id=p.id) }}">{{ p.name }}</a>
{% endfor %}
</p>
</div>
</div>
{% endfor %}
</div>
</div>
{% endblock %}

55
templates/event.html Normal file
View file

@ -0,0 +1,55 @@
{% extends "base.html" %}
{% block title %}{{ item.name }}{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<h1>{{ item.title }}</h1>
<p><a href="{{ url_for("index") }}">home</a></p>
<div class="card my-2">
<div class="card-body">
<h5 class="card-title">
{% if item.url %}
<a href="{{ item.url }}">{{ item.title }}</a>
{% else %}
{{ item.title }}
{% endif %}
</h5>
<h6 class="card-subtitle mb-2 text-body-secondary">
<a href="{{ url_for("conference_page", short_name=item.conference.short_name) }}">{{ item.conference.title }}</a>
</h6>
{% if item.abstract %}
<p class="card-text">
{% if "<" in item.abstract %}
{{ item.abstract | safe }}
{% else %}
{{ item.abstract }}
{% endif %}
</p>
{% endif %}
<p class="card-text">
{% if "<" in item.description %}
{{ item.description | safe }}
{% else %}
{{ item.description }}
{% endif %}
</p>
<p class="card-text">
Speakers:
{% for p in item.people %}
<a href="{{ url_for("person", person_id=p.id) }}">{{ p.name }}</a>
{% endfor %}
</p>
</div>
</div>
</div>
</div>
{% endblock %}

View file

@ -1,10 +1,50 @@
<!doctype html> {% extends "base.html" %}
<html lang="en">
<head> {% block title %}Conference archive{% endblock %}
<meta charset="utf-8">
<title></title> {% block content %}
</head> <div class="container">
<div class="row">
<h1>Conference archive</h1>
<form action="{{ url_for("search_people") }}">
<div class="mb-3">
<label for="q" class="form-label">speaker name</label>
<input type="text" class="form-control" name="q" id="q">
</div>
<button type="submit" class="btn btn-primary">Search</button>
</form>
<ul>
<li>{{ "{:,d}".format(count.conference) }} conferences</li>
<li>{{ "{:,d}".format(count.event) }} talks -
<a href="{{ url_for("events_page") }}">most common titles</a>
</li>
<li>
{{ "{:,d}".format(count.person) }} speakers
<a href="{{ url_for("top_speakers_page") }}">top speakers</a>
</li>
</ul>
<table class="table w-auto">
<tbody>
{% for item in items %}
<tr>
<td>
<a href="{{ url_for("conference_page", short_name=item.short_name) }}">{{ item.title }}</a>
</td>
<td class="text-end">{{ item.start.strftime("%d %b %Y") }}</td>
<td class="text-end">{{ (item.end - item.start).days + 1 }} days</td>
<td class="text-end">{{ item.events.count() }} talks</td>
<td class="text-end">{{ item.people_detail.count() }} speakers</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}
<body>
</body>
</html>

View file

@ -0,0 +1,46 @@
{% extends "base.html" %}
{% block title %}Conference archive{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<h1>Conference archive</h1>
<form action="{{ url_for("merge") }}">
<div class="mb-3">
<label for="q" class="form-label">speaker name</label>
<input type="text" class="form-control" name="q" id="q" value="{{ search_for }}">
</div>
<button type="submit" class="btn btn-primary">Search</button>
</form>
<p>Found {{ q.count() }} people matching '{{ search_for }}'</p>
<form method="POST">
<input type="hidden" name="q" value="{{search_for }}"/>
{% for item in q %}
<div class="form-check">
<input class="form-check-input" type="checkbox" name="person_id" value="{{ item.id }}" id="person{{ item.id }}">
<label class="form-check-label" for="person{{ item.id }}">
{{ item.id }}
<a href="{{ url_for("person", person_id=item.id) }}">{{ item.name }}</a>
{% if item.wikidata_qid %}
&mdash;
<a href="https://www.wikidata.org/wiki/{{ item.wikidata_qid }}">{{ item.wikidata_qid }} on Wikidata</a>
{% endif %}
</label>
</div>
{% endfor %}
<button type="submit" class="btn btn-primary">Merge</button>
</form>
</div>
</div>
{% endblock %}

98
templates/person.html Normal file
View file

@ -0,0 +1,98 @@
{% extends "base.html" %}
{% block title %}{{ item.name }}{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<h1>{{ item.name }}</h1>
<p><a href="{{ url_for("index") }}">home</a></p>
<h3>Conferences</h3>
{% for apperance in item.conferences_association %}
{% set conf = apperance.conference %}
<div class="card my-2">
<div class="card-body">
<h5 class="card-title">{{ conf.id }}: {{ conf.title }}</h5>
<p class="card-text">
{% if apperance.bio %}{{ apperance.bio | safe }}{% else %}No speaker biography.{% endif %}
</p>
</div>
</div>
{% endfor %}
{% set search_for = '"' + item.name + '" ' + " haswbstatement:P31=Q5" %}
<p><a href="https://www.wikidata.org/w/index.php?search={{ search_for | urlencode }}&title=Special%3ASearch&ns0=1&ns120=1">Search for {{ item.name }} on Wikidata</a></p>
<form method="POST">
<div class="mb-3">
<label for="name" class="form-label">Name</label>
<input type="text" class="form-control" name="name" id="name" value="{{ item.name }}">
</div>
<div class="mb-3">
<label for="wikidata_qid" class="form-label">Wikidata QID</label>
<input type="text" class="form-control" name="wikidata_qid" id="wikidata_qid" value="{{ item.wikidata_qid or "" }}">
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
<h3>Talks</h3>
<p>Has {{ item.events_association.count() }} events</p>
{% for event in item.events_by_time() %}
<div class="card my-2">
<div class="card-body">
<h5 class="card-title">
<a href="{{ url_for("event_page", event_id=event.id) }}">{{ event.title }}</a>
</h5>
<h6 class="card-subtitle mb-2 text-body-secondary">
{{ event.conference.title }}
&mdash;
{% if event.event_date %}
{{ event.event_date.strftime("%d %b %Y") }}
{% else %}
event date missing
{% endif %}
</h6>
<p class="card-text">
{% if event.url %}
<a href="{{ event.url }}">{{ event.title }} on conference website</a>
{% endif %}
{% if event.abstract %}
<p class="card-text">
{% if "<" in event.abstract %}
{{ event.abstract | safe }}
{% else %}
{{ event.abstract }}
{% endif %}
</p>
{% endif %}
{% if event.description %}
<p class="card-text">
{% if "<" in event.description %}
{{ event.description | safe }}
{% else %}
{{ event.description }}
{% endif %}
</p>
{% endif %}
<p class="card-text">
{% for p in event.people %}
{% if p.id != item.id %}
<a href="{{ url_for(request.endpoint, person_id=p.id) }}">{{ p.name }}</a>
{% endif %}
{% endfor %}
</p>
</div>
</div>
{% endfor %}
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,37 @@
{% extends "base.html" %}
{% block title %}Conference archive{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<h1>Conference archive</h1>
<form action="{{ url_for("events_page") }}">
<div class="mb-3">
<label for="q" class="form-label">event</label>
<input type="text" class="form-control" name="q" id="q" value="{{ search_for }}">
</div>
<button type="submit" class="btn btn-primary">Search</button>
</form>
<p>Found {{ q.count() }} events matching '{{ search_for }}'</p>
<ul>
{% for item in q %}
<li>
<a href="{{ url_for("event_page", event_id=item.id) }}">{{ item.title }}</a>
&mdash;
{{ item.conference.title }}
&mdash;
{% for p in item.people %}
<a href="{{ url_for("person", person_id=p.id) }}">{{ p.name }}</a>
{% endfor %}
</li>
{% endfor %}
</ul>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,41 @@
{% extends "base.html" %}
{% block title %}Conference archive{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<h1>Conference archive</h1>
<p><a href="{{ url_for("index") }}">home</a></p>
<form action="{{ url_for("search_people") }}">
<div class="mb-3">
<label for="q" class="form-label">speaker name</label>
<input type="text" class="form-control" name="q" id="q" value="{{ search_for }}">
</div>
<button type="submit" class="btn btn-primary">Search</button>
</form>
<p>
Found {{ q.count() }} people matching '{{ search_for }}'
<a href="{{ url_for("merge", q=search_for) }}">merge</a>
</p>
<ul>
{% for item in q %}
<li>
<a href="{{ url_for("person", person_id=item.id) }}">{{ item.name }}</a>
{% if item.wikidata_qid %}
&mdash;
<a href="https://www.wikidata.org/wiki/{{ item.wikidata_qid }}">{{ item.wikidata_qid }} on Wikidata</a>
{% endif %}
</li>
{% endfor %}
</ul>
</div>
</div>
{% endblock %}

36
templates/top_events.html Normal file
View file

@ -0,0 +1,36 @@
{% extends "base.html" %}
{% block title %}Conference archive{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<h1>Conference archive</h1>
<p><a href="{{ url_for("index") }}">home</a></p>
<h3>Top events</h3>
<form action="{{ url_for("events_page") }}">
<div class="mb-3">
<label for="q" class="form-label">event</label>
<input type="text" class="form-control" name="q" id="q">
</div>
<button type="submit" class="btn btn-primary">Search</button>
</form>
<ul>
{% for event_title, count in top_events %}
<li>
<a href="{{ url_for("events_page", q=event_title) }}">{{ event_title }}</a>
({{ count }})
</li>
{% endfor %}
</ul>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,37 @@
{% extends "base.html" %}
{% block title %}Conference archive{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<h1>Conference archive</h1>
<form action="{{ url_for("search_people") }}">
<div class="mb-3">
<label for="q" class="form-label">speaker name</label>
<input type="text" class="form-control" name="q" id="q">
</div>
<button type="submit" class="btn btn-primary">Search</button>
</form>
<h3>Top speakers</h3>
<ul>
{% for person, count in top_speakers %}
<li>
<a href="{{ url_for("person", person_id=person.id) }}">{{ person.name }}</a>
({{ count }})
{% if person.wikidata_qid %}
&mdash;
<a href="https://www.wikidata.org/wiki/{{ person.wikidata_qid }}">{{ person.wikidata_qid }} on Wikidata</a>
{% endif %}
</li>
{% endfor %}
</ul>
</div>
</div>
{% endblock %}

33
templates/wikidata.html Normal file
View file

@ -0,0 +1,33 @@
{% extends "base.html" %}
{% block title %}Conference archive{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<h1>Conference archive</h1>
{% for person, count, wikidata_hits in items %}
<div>
<h4><a href="{{ url_for("person", person_id=person.id) }}">{{ person.name }}</a> ({{ count }})</h4>
<ul>
{% for hit in wikidata_hits %}
<li>
<a href="https://www.wikidata.org/wiki/{{ hit.qid }}">{{ hit.qid }}</a>
{{ hit.label }} &mdash; {{ hit.description }}
</li>
{% endfor %}
</ul>
</div>
{% endfor %}
</div>
</div>
{% endblock %}
{% block style %}
<style>
.searchmatch { background: lightgreen }
</style>
{% endblock %}