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 = conference_obj(root)
 | 
			
		||||
    database.session.add(conf)
 | 
			
		||||
 | 
			
		||||
    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"):
 | 
			
		||||
                event = build_event_object(event_element, person_lookup)
 | 
			
		||||
                if not event:
 | 
			
		||||
                    continue
 | 
			
		||||
                event.conference = conf
 | 
			
		||||
                print()
 | 
			
		||||
                database.session.add(event)
 | 
			
		||||
                event_count += 1
 | 
			
		||||
                if event_count > 10:
 | 
			
		||||
                    return None
 | 
			
		||||
        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
 | 
			
		||||
            # event.day = day_index
 | 
			
		||||
            database.session.add(event)
 | 
			
		||||
            event_count += 1
 | 
			
		||||
 | 
			
		||||
    if event_count > 1:
 | 
			
		||||
        database.session.commit()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
load("/home/edward/src/2022/conference-gender-mix/schedules/debconf22")
 | 
			
		||||
 | 
			
		||||
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