Improvements
This commit is contained in:
parent
4e5ee195dd
commit
a0df624f16
|
@ -3,6 +3,7 @@
|
|||
import sqlalchemy
|
||||
import sqlalchemy.orm.decl_api
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy.dialects import postgresql
|
||||
from sqlalchemy.ext.associationproxy import association_proxy
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.ext.orderinglist import ordering_list
|
||||
|
@ -39,6 +40,32 @@ class Conference(TimeStampedModel):
|
|||
acronym = Column(String)
|
||||
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):
|
||||
|
@ -48,7 +75,7 @@ class Event(TimeStampedModel):
|
|||
id = Column(Integer, primary_key=True)
|
||||
conference_id = Column(Integer, ForeignKey("conference.id"), nullable=False)
|
||||
event_date = Column(DateTime)
|
||||
day = Column(Integer)
|
||||
# day = Column(Integer)
|
||||
guid = Column(String)
|
||||
start = Column(String)
|
||||
duration = Column(String)
|
||||
|
@ -56,22 +83,23 @@ class Event(TimeStampedModel):
|
|||
track = Column(String)
|
||||
slug = Column(String)
|
||||
title = Column(String, nullable=False)
|
||||
abstract = Column(String)
|
||||
description = Column(String)
|
||||
event_type = Column(String)
|
||||
url = Column(String)
|
||||
|
||||
conference = relationship("Conference", backref="events")
|
||||
conference = relationship("Conference", back_populates="events")
|
||||
|
||||
people_association = relationship(
|
||||
people_detail = relationship(
|
||||
"EventPerson",
|
||||
order_by="EventPerson.position",
|
||||
back_populates="event",
|
||||
collection_class=ordering_list("position"),
|
||||
)
|
||||
people = association_proxy(
|
||||
"people_association",
|
||||
"people_detail",
|
||||
"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)
|
||||
wikidata_qid = Column(String)
|
||||
gender = Column(String)
|
||||
wikidata_photo = Column(postgresql.ARRAY(String))
|
||||
|
||||
events_association = relationship("EventPerson", back_populates="person")
|
||||
events = association_proxy("event_association", "event")
|
||||
events_association = relationship(
|
||||
"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):
|
||||
|
@ -95,7 +153,6 @@ class EventPerson(Base):
|
|||
event_id = Column(Integer, ForeignKey("event.id"), primary_key=True)
|
||||
person_id = Column(Integer, ForeignKey("person.id"), primary_key=True)
|
||||
position = Column(Integer, nullable=False)
|
||||
named_as = Column(String)
|
||||
|
||||
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
|
||||
|
||||
import datetime
|
||||
import os
|
||||
import sys
|
||||
|
||||
import lxml.etree
|
||||
|
||||
from confarchive import database, model
|
||||
|
||||
DB_URL = "postgresql:///confarchive"
|
||||
schedules_loc = "/home/edward/src/2022/conference-gender-mix/schedules"
|
||||
|
||||
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:
|
||||
"""Read date from a 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:
|
||||
|
@ -58,55 +61,99 @@ def conference_obj(root: Element) -> model.Conference:
|
|||
e = root.find(".//conference")
|
||||
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(
|
||||
title=read_required_field(e, "title"),
|
||||
start=read_date_field(e, "start"),
|
||||
end=read_date_field(e, "end"),
|
||||
start=start,
|
||||
end=end,
|
||||
timezone=read_field(e, "time_zone_name"),
|
||||
)
|
||||
|
||||
|
||||
def build_event_object(
|
||||
e: Element, person_lookup: dict[int, model.Person]
|
||||
e: Element, person_lookup: dict[str, model.Person]
|
||||
) -> model.Event | None:
|
||||
"""Build an event object."""
|
||||
title = read_required_field(e, "title")
|
||||
if not_a_talk(title):
|
||||
return None
|
||||
|
||||
title = read_field(e, "title")
|
||||
guid = e.get("guid")
|
||||
room = read_field(e, "room")
|
||||
slug = read_field(e, "slug")
|
||||
description = read_field(e, "description")
|
||||
event_type = read_field(e, "type")
|
||||
url = read_field(e, "url")
|
||||
|
||||
persons_element = e.find(".//persons")
|
||||
if persons_element is None:
|
||||
if title 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
|
||||
people = []
|
||||
for p in persons_element:
|
||||
id_str = p.get("id")
|
||||
assert id_str is not None
|
||||
people.append(person_lookup[int(id_str)])
|
||||
seen_person = set()
|
||||
print("persons:", len(persons))
|
||||
for p in persons:
|
||||
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(
|
||||
guid=guid,
|
||||
title=title,
|
||||
room=room,
|
||||
slug=slug,
|
||||
description=description,
|
||||
event_type=event_type,
|
||||
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]]:
|
||||
people: dict[int, str] = {}
|
||||
for person in root.findall(".//person"):
|
||||
assert person.text
|
||||
person_id_str = person.get("id")
|
||||
if not person_id_str:
|
||||
print(lxml.etree.tostring(person, encoding=str))
|
||||
assert person_id_str
|
||||
person_id = int(person_id_str)
|
||||
existing = people.get(person_id)
|
||||
|
@ -118,36 +165,111 @@ def get_all_people(root: Element) -> list[tuple[int, str]]:
|
|||
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."""
|
||||
start = open(filename).read(15)
|
||||
if start == "BEGIN:VCALENDAR" or start.startswith("{"):
|
||||
return None
|
||||
|
||||
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)
|
||||
assert model.Conference.query.filter_by(title=conf.title).count() == 0
|
||||
database.session.add(conf)
|
||||
|
||||
print((conf.short_name, conf.title))
|
||||
|
||||
event_count = 0
|
||||
people = get_all_people(root)
|
||||
people_names = get_people_names(root)
|
||||
person_lookup = {}
|
||||
for person_id, name in people:
|
||||
person = model.Person.query.filter_by(name=name).first()
|
||||
for name in people_names:
|
||||
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:
|
||||
person = model.Person(name=name)
|
||||
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 room in root.findall(".//room"):
|
||||
for event_element in root.findall(".//event"):
|
||||
day_index_str = day.get("index")
|
||||
# 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)
|
||||
if not event:
|
||||
print(f"skip event: {title}")
|
||||
continue
|
||||
event.conference = conf
|
||||
print()
|
||||
# event.day = day_index
|
||||
database.session.add(event)
|
||||
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()
|
||||
|
||||
|
||||
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
|
||||
|
||||
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.debug = True
|
||||
|
@ -10,12 +21,260 @@ app.debug = True
|
|||
app.config.from_object("config.default")
|
||||
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("/")
|
||||
def index() -> str:
|
||||
"""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__":
|
||||
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>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title></title>
|
||||
</head>
|
||||
{% 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>
|
||||
|
||||
|
||||
<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