Improvements
This commit is contained in:
parent
4e5ee195dd
commit
a0df624f16
|
@ -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")
|
||||||
|
|
|
@ -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
267
main.py
|
@ -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
25
templates/base.html
Normal 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
76
templates/conference.html
Normal 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
55
templates/event.html
Normal 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 %}
|
|
@ -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>
|
|
46
templates/merge_people.html
Normal file
46
templates/merge_people.html
Normal 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 %}
|
||||||
|
—
|
||||||
|
<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
98
templates/person.html
Normal 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 }}
|
||||||
|
—
|
||||||
|
{% 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 %}
|
37
templates/search_events.html
Normal file
37
templates/search_events.html
Normal 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>
|
||||||
|
—
|
||||||
|
{{ item.conference.title }}
|
||||||
|
—
|
||||||
|
{% for p in item.people %}
|
||||||
|
<a href="{{ url_for("person", person_id=p.id) }}">{{ p.name }}</a>
|
||||||
|
{% endfor %}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
|
41
templates/search_people.html
Normal file
41
templates/search_people.html
Normal 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 %}
|
||||||
|
—
|
||||||
|
<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
36
templates/top_events.html
Normal 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 %}
|
||||||
|
|
||||||
|
|
37
templates/top_speakers.html
Normal file
37
templates/top_speakers.html
Normal 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 %}
|
||||||
|
—
|
||||||
|
<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
33
templates/wikidata.html
Normal 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 }} — {{ hit.description }}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block style %}
|
||||||
|
<style>
|
||||||
|
.searchmatch { background: lightgreen }
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
Loading…
Reference in a new issue