From 9f3a7995a1a9777fbe982cf346e3813f2e387a8d Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Thu, 21 Sep 2023 04:59:17 +0100 Subject: [PATCH] Improvements --- confarchive/model.py | 69 +++++++++++- main.py | 100 ++++++++++++++++-- templates/conference.html | 141 +++++++++++++++++++++---- templates/event.html | 4 +- templates/index.html | 24 ++++- templates/merge_people.html | 5 +- templates/person.html | 176 ++++++++++++++++++++----------- templates/search_everything.html | 97 +++++++++++++++++ templates/top_speakers.html | 26 +++-- 9 files changed, 532 insertions(+), 110 deletions(-) create mode 100644 templates/search_everything.html diff --git a/confarchive/model.py b/confarchive/model.py index b05531c..2795491 100644 --- a/confarchive/model.py +++ b/confarchive/model.py @@ -9,7 +9,7 @@ from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.orderinglist import ordering_list from sqlalchemy.orm import relationship from sqlalchemy.schema import Column, ForeignKey -from sqlalchemy.types import Date, DateTime, Integer, String +from sqlalchemy.types import Boolean, Date, DateTime, Integer, String from .database import session @@ -25,6 +25,18 @@ class TimeStampedModel(Base): modified = Column(DateTime, default=func.now(), onupdate=func.now()) +class Series(TimeStampedModel): + """Conference series.""" + + __tablename__ = "series" + id = Column(Integer, primary_key=True) + name = Column(String, nullable=False) + slug = Column(String, unique=True) + wikidata_qid = Column(String, unique=True) + + conferences = relationship("Conference", back_populates="series") + + class Conference(TimeStampedModel): """Conference.""" @@ -42,6 +54,9 @@ class Conference(TimeStampedModel): schedule_xml_url = Column(String) short_name = Column(String, unique=True) venue_id = Column(Integer, ForeignKey("venue.id")) + online = Column(Boolean) + wikidata_qid = Column(String, unique=True) + series_id = Column(Integer, ForeignKey("series.id")) people_detail = relationship( "ConferencePerson", lazy="dynamic", back_populates="conference" @@ -56,6 +71,7 @@ class Conference(TimeStampedModel): ) venue = relationship("Venue", back_populates="conferences") + series = relationship("Series", back_populates="conferences") class City(TimeStampedModel): @@ -95,6 +111,17 @@ class Country(TimeStampedModel): cities = relationship("City", back_populates="country") + @property + def flag(self) -> str: + a = ord("A") + flag_a = 0x1F1E6 + char1, char2 = ( + flag_a + ord(self.alpha2[0]) - a, + flag_a + ord(self.alpha2[1]) - a, + ) + + return chr(char1) + chr(char2) + class ConferencePerson(Base): __tablename__ = "conference_person" @@ -108,6 +135,16 @@ class ConferencePerson(Base): person = relationship("Person", back_populates="conferences_association") conference = relationship("Conference", back_populates="people_detail") + @property + def events(self): + return ( + Event.query.join(EventPerson) + .filter( + Event.conference == self.conference, EventPerson.person == self.person + ) + .order_by(Event.event_date.desc()) + ) + class Event(TimeStampedModel): """Event.""" @@ -140,7 +177,7 @@ class Event(TimeStampedModel): people = association_proxy( "people_detail", "person", - creator=lambda i: EventPerson(person=i[0], named_as=i[1]), + creator=lambda i: EventPerson(person=i), ) @@ -162,7 +199,23 @@ class Person(TimeStampedModel): events = association_proxy("events_association", "event") conferences_association = relationship("ConferencePerson", back_populates="person") - conferences = association_proxy("conference_association", "conference") + conferences = association_proxy("conferences_association", "conference") + + @property + def conference_count(self): + return ConferencePerson.query.filter_by(person_id=self.id).count() + + @property + def event_count(self): + return EventPerson.query.filter_by(person_id=self.id).count() + + def active_years(self): + q = ( + session.query(func.min(Event.event_date), func.max(Event.event_date)) + .join(EventPerson) + .filter_by(person_id=self.id) + ) + return q.one() # photos = relationship("PersonPhoto", back_populates="person") @@ -176,6 +229,16 @@ class Person(TimeStampedModel): return q + def conference_by_time(self): + q = ( + session.query(ConferencePerson) + .join(Conference) + .filter(ConferencePerson.person == self) + .order_by(Conference.start.desc()) + ) + + return q + # class PersonPhoto(TimeStampedModel): # """Person photo.""" diff --git a/main.py b/main.py index 27ce2f3..7037f7c 100755 --- a/main.py +++ b/main.py @@ -10,7 +10,7 @@ from typing import cast import flask import requests import sqlalchemy -from sqlalchemy import func, update +from sqlalchemy import func, or_, update from werkzeug.wrappers import Response from confarchive import database, model @@ -50,7 +50,7 @@ def wikidata_search(q: str) -> list[dict[str, typing.Any]]: data = r.json() time.sleep(1) - return cast(dict[str, typing.Any], data["query"]["search"]) + return cast(list[dict[str, typing.Any]], data["query"]["search"]) def wikidata_get_item(qid: str) -> typing.Any: @@ -58,6 +58,7 @@ def wikidata_get_item(qid: str) -> typing.Any: if os.path.exists(cache_filename): item = json.load(open(cache_filename)) else: + print(qid) params: dict[str, str | int] = { "action": "wbgetentities", "ids": qid, @@ -78,7 +79,7 @@ def top_speakers() -> sqlalchemy.orm.query.Query: .join(model.ConferencePerson) .group_by(model.Person) .order_by(func.count().desc()) - .having(func.count() > 5) + .having(func.count() > 3) ) return q @@ -118,7 +119,9 @@ def person(person_id: int) -> str | Response: flask.url_for(flask.request.endpoint, person_id=person_id) ) - return flask.render_template("person.html", item=item, Event=model.Event) + return flask.render_template( + "person.html", item=item, Event=model.Event, plural=plural + ) @app.route("/event/") @@ -132,7 +135,9 @@ 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) + return flask.render_template( + "conference.html", item=item, person_image_filename=person_image_filename + ) @app.route("/people") @@ -157,9 +162,16 @@ def merge() -> str | Response: merge_to_id = min(item_ids) other_ids = [i for i in item_ids if i != merge_to_id] + name_from_person_id = flask.request.form["name"] + print(other_ids, "->", merge_to_id) with database.session.begin(): + if merge_to_id != name_from_person_id: + merge_to = model.Person.query.get(merge_to_id) + name_from_person = model.Person.query.get(name_from_person_id) + merge_to.name = name_from_person.name + print("update ConferencePerson") database.session.execute( update(model.ConferencePerson) @@ -224,15 +236,37 @@ def index() -> str: "conference": model.Conference.query.count(), "event": model.Event.query.count(), "person": model.Person.query.count(), + "country": model.Country.query.count(), + "venue": model.Venue.query.count(), } return flask.render_template("index.html", items=q, count=count) +def plural(num: int, label: str) -> str: + return f'{num:,d} {label}{"s" if num != 1 else ""}' + + +def speaker_counts(): + sql = """ +select num, count(*) +from (select person_id, count(*) as num from conference_person group by person_id) a +group by num +order by num +""" + + return database.session.execute(sql) + + @app.route("/speakers") def top_speakers_page() -> str: """Top speakers page.""" - return flask.render_template("top_speakers.html", top_speakers=top_speakers()) + return flask.render_template( + "top_speakers.html", + top_speakers=top_speakers(), + speaker_counts=speaker_counts(), + plural=plural, + ) @app.route("/country") @@ -264,11 +298,11 @@ def link_to_wikidata() -> str: for person, num in top_speakers2(): if person.wikidata_qid: continue - search_hits = wikidata_search(f'"{person.name}"') + search_hits = wikidata_search(person.name) if not search_hits: continue - if len(search_hits) > 10: + if len(search_hits) > 14: continue hits = [] @@ -299,5 +333,55 @@ def link_to_wikidata() -> str: return flask.render_template("wikidata.html", items=items) +@app.route("/search") +def search_everything() -> str: + search_for = flask.request.args["q"] + if not search_for: + return flask.render_template("search_everything.html") + + search_for = search_for.strip() + like = f"%{search_for}%" + + people = model.Person.query.filter(model.Person.name.ilike(like)).order_by( + model.Person.name + ) + + events = model.Event.query.filter( + or_(model.Event.abstract.ilike(like), model.Event.description.ilike(like)) + ).order_by(model.Event.event_date) + + return flask.render_template( + "search_everything.html", people=people, events=events, search_for=search_for + ) + + +@app.route("/person//delete", methods=["POST"]) +def delete_person(person_id: int) -> str | Response: + item = model.Person.query.get(person_id) + + for cp in item.conferences_association: + database.session.delete(cp) + + for ep in item.events_association: + database.session.delete(ep) + database.session.delete(item) + + database.session.commit() + + return flask.redirect(flask.url_for("index")) + + +def person_image_filename(person_id): + person = model.Person.query.get(person_id) + return os.path.join("wikidata_photo", "thumb", person.wikidata_photo[0]) + for filename in person.wikidata_photo: + face_crop = "face_1_" + filename + full = os.path.join("static", "wikidata_photo", "face_cropped", face_crop) + if os.path.exists(full): + return os.path.join("wikidata_photo", "face_cropped", face_crop) + + return os.path.join("wikidata_photo", "thumb", person.wikidata_photo[0]) + + if __name__ == "__main__": app.run(host="0.0.0.0", port=5002) diff --git a/templates/conference.html b/templates/conference.html index 8b355fc..fd5064c 100644 --- a/templates/conference.html +++ b/templates/conference.html @@ -1,5 +1,28 @@ {% extends "base.html" %} +{% block style %} + +{% endblock %} + +{% set show_images = True %} + {% block title %}{{ item.title }}{% endblock %} {% block content %} @@ -8,36 +31,84 @@

{{ item.title }}

home

-
    -
  • start: {{ item.start }}
  • -
  • end: {{ item.end }}
  • +
    +
    series: {{ item.series.name }} + {% if item.series.wikidata_qid %} + Wikidata + {% endif %} +
    +
    start: {{ item.start }}
    +
    end: {{ item.end }}
    {% if days %} -
  • days: {{ item.days }}
  • +
    days: {{ item.days }}
    {% endif %} -
  • short name: {{ item.short_name }}
  • -
  • country: {{ item.country or "n/a" }}
  • -
+ {#
short name: {{ item.short_name }}
#} + {% if item.venue %} + {% set country = item.venue.city.country %} +
+ venue: {{ item.venue.name }} + {% if item.venue.wikidata_qid %} + Wikidata + {% endif %} +
+
+ city: {{ item.venue.city.name }} + {% if item.venue.city.wikidata_qid %} + Wikidata + {% endif %} +
+
country: {{ country.name }} {{ country.flag }}
+ {% endif %} + {% if item.wikidata_qid %} +
wikidata: {{ item.wikidata_qid }}
+ {% endif %} + + + {% if show_images %} +
+ {% for person in item.people %} + {% if person.wikidata_photo %} + + + {{ person.name}} + + + {% endif %} + {% endfor %} +
+ {% endif %}

Talks

{{ item.events.count() }} talks

{% for event in item.events %} -
-
-
- {{ event.title }} -
-
+
+
+

+ 🎤 + {{ event.title }}
+ + Speakers: + {% for p in event.people %} + 👤 + {{ p.name }} + {% endfor %}
+ {% if event.event_date %} - {{ event.event_date.strftime("%d %b %Y at %H:%M") }} + 📅 {{ event.event_date.strftime("%a, %d %b %Y at %H:%M") }} {% else %} event date missing {% endif %} -

-

+ + show details
+ + + +

+
@@ -74,3 +140,32 @@ {% endblock %} + +{% block script %} + +{% endblock %} diff --git a/templates/event.html b/templates/event.html index 1aa828e..7c3cbda 100644 --- a/templates/event.html +++ b/templates/event.html @@ -33,13 +33,15 @@ {% endif %} + {% if item.description %}

- {% if "<" in item.description %} + {% if "<" in item.description %} {{ item.description | safe }} {% else %} {{ item.description }} {% endif %}

+ {% endif %}

Speakers: {% for p in item.people %} diff --git a/templates/index.html b/templates/index.html index d0f2051..8e063c1 100644 --- a/templates/index.html +++ b/templates/index.html @@ -15,27 +15,49 @@ -

+ 👥 {{ "{:,d}".format(count.conference) }} conferences
+ 🌍 + {{ "{:,d}".format(count.country) }} countries
+ 📍 + {{ "{:,d}".format(count.venue) }} venues
+ 🎤 {{ "{:,d}".format(count.event) }} talks - most common titles
+ 👤 {{ "{:,d}".format(count.person) }} speakers top speakers
+

Conferences

+ {% for item in items %} + {% if loop.first or item.start.year != loop.previtem.start.year %} +

{{ item.start.year }}

+ {% endif %} +
+ 👥 {{ item.title }} + 📅 {{ item.start.strftime("%d %b %Y") }}
{% if item.venue %} + 📍 {{ item.venue.name }} – {{ item.venue.city.name }}, {{ item.venue.city.country.name }} + {{ item.venue.city.country.flag }}
{% endif %} + {# + {% if item.series %} + 📃 Series: {{ item.series.name }} +
+ {% endif %} + #} {{ (item.end - item.start).days + 1 }} days, {{ item.events.count() }} talks, diff --git a/templates/merge_people.html b/templates/merge_people.html index 54e2685..2f04c89 100644 --- a/templates/merge_people.html +++ b/templates/merge_people.html @@ -25,7 +25,6 @@
+ +
+ + {% for conf in item.conferences %} 👥{{ conf.title }}{% endfor %}
{% endfor %} diff --git a/templates/person.html b/templates/person.html index 50c6452..b23897e 100644 --- a/templates/person.html +++ b/templates/person.html @@ -6,25 +6,24 @@

{{ item.name }}

-

home

+ +

+ 👥 {{ plural(item.conference_count, "conference") }}
+ 🎤 {{ plural(item.event_count, "talk") }}
+ {% set start, end = item.active_years() %} + 📅 Years active: {{ start.year }} to {{end.year }} + {% if item.wikidata_qid %} +
+ 📊 Wikidata: {{ item.wikidata_qid }} + {% endif %} +

-

Conferences

- {% for apperance in item.conferences_association %} - {% set conf = apperance.conference %} + {% if item.wikidata_photo %} + + {% endif %} -
-
-
{{ conf.id }}: {{ conf.title }}
-

- {% if apperance.bio %}{{ apperance.bio | safe }}{% else %}No speaker biography.{% endif %} -

-
-
- {% endfor %} - - - {% set search_for = '"' + item.name + '" ' + " haswbstatement:P31=Q5" %} + {% set search_for = item.name + ' ' + " haswbstatement:P31=Q5" %}

Search for {{ item.name }} on Wikidata

@@ -39,60 +38,111 @@
-

Talks

-

Has {{ item.events_association.count() }} events

- {% for event in item.events_by_time() %} -
-
-
- {{ event.title }} -
-
- {{ event.conference.title }} - — - {% if event.event_date %} - {{ event.event_date.strftime("%d %b %Y") }} - {% else %} - event date missing - {% endif %} +
+ +
-
-

- {% if event.url %} - {{ event.title }} on conference website - {% endif %} - {% if event.abstract %} -

- {% if "<" in event.abstract %} - {{ event.abstract | safe }} - {% else %} - {{ event.abstract }} - {% endif %} -

- {% endif %} - {% if event.description %} -

- {% if "<" in event.description %} - {{ event.description | safe }} - {% else %} - {{ event.description }} - {% endif %} -

- {% endif %} -

- {% for p in event.people %} - {% if p.id != item.id %} - {{ p.name }} + {% for apperance in item.conference_by_time() %} + {% set conf = apperance.conference %} + +

+

👥 {{ conf.title }} + 📅 {{ conf.start.strftime("%d %b %Y") }} + +

+ {% if apperance.bio %}

Biography: {{ apperance.bio | safe }}

{% endif %} +
+ + {% for event in apperance.events %} +
+

+ 🎤 + {{ event.title }} + + {% if event.event_date %} + {{ event.event_date.strftime("%d %b %Y") }} + {% else %} + event date missing {% endif %} - {% endfor %} -

-

-
+ + show details + + + +
+ + {% endfor %} + + {% endfor %} +
{% endblock %} + +{% block script %} + +{% endblock %} diff --git a/templates/search_everything.html b/templates/search_everything.html new file mode 100644 index 0000000..4ba27ba --- /dev/null +++ b/templates/search_everything.html @@ -0,0 +1,97 @@ +{% extends "base.html" %} + +{% block title %}Conference archive{% endblock %} + + {% block content %} +
+
+

Conference archive

+

home

+ +
+
+ +
+ +
+ + {% if search_for %} + +

Talks

+ +

Found {{ events.count() }} events matching '{{ search_for }}'

+ + {% for event in events %} +
+

+ 🎤 + {{ event.title }}
+ Speakers: + {% for p in event.people %} + 👤 + {{ p.name }} + {% endfor %}
+ + 👥 {{ event.conference.title }}
+ + {% if event.event_date %} + 📅 {{ event.event_date.strftime("%a, %d %b %Y at %H:%M") }} + {% else %} + event date missing + {% endif %} +

+ + {% if event.abstract %} +

+ {% if "<" in event.abstract %} + {{ event.abstract | safe }} + {% else %} + {% for line in event.abstract.splitlines() %} + {{ line }}
+ {% endfor %} + {% endif %} +

+ {% endif %} + + {% if event.description and event.description != event.abstract %} +

+ {% if "<" in event.description %} + {{ event.description | safe }} + {% else %} + {% for line in event.description.splitlines() %} + {{ line }}
+ {% endfor %} + {% endif %} +

+ {% endif %} +
+ + {% endfor %} + + +

People

+

+ Found {{ people.count() }} people matching '{{ search_for }}' + +

+ + + + {% endif %} + +
+
+{% endblock %} + + diff --git a/templates/top_speakers.html b/templates/top_speakers.html index 8b5a71b..8b7beb1 100644 --- a/templates/top_speakers.html +++ b/templates/top_speakers.html @@ -3,8 +3,6 @@ {% block title %}Conference archive{% endblock %} {% block content %} -
-

Conference archive

@@ -15,23 +13,31 @@
+

Speaker/conference frequency distribution

+ +

Distribution of speakers by conference count.

+ {% for conf_count, speaker_count in speaker_counts %} +
+ {{ plural(conf_count, "conference") }}: + {{ plural(speaker_count, "speaker") }} +
+ {% endfor %} +

Top speakers

{% endfor %} - -
-
{% endblock %}