From 9f3a7995a1a9777fbe982cf346e3813f2e387a8d Mon Sep 17 00:00:00 2001 From: Edward Betts <edward@4angle.com> 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/<int:event_id>") @@ -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/<int:person_id>/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 %} +<style> +.image-container { + width: 200px; /* Adjust this to your desired square size */ + height: 240px; /* Same as width for a square */ + display: inline-flex; /* Use inline-flex to display containers horizontally */ + margin-right: 10px; /* Add some spacing between images (adjust as needed) */ + justify-content: center; /* Horizontally center the content */ + align-items: center; /* Vertically center the content */ + overflow: hidden; /* Hide overflowing image parts */ +} + +.image-container img { + max-width: 100%; + max-height: 100%; + object-fit: cover; /* Crop and scale the image to fit the container */ + object-position: center; /* Center the cropping horizontally */ +} +</style> +{% endblock %} + +{% set show_images = True %} + {% block title %}{{ item.title }}{% endblock %} {% block content %} @@ -8,36 +31,84 @@ <h1>{{ item.title }}</h1> <p><a href="{{ url_for("index") }}">home</a></p> - <ul> - <li>start: {{ item.start }}</li> - <li>end: {{ item.end }}</li> + <div> + <div>series: {{ item.series.name }} + {% if item.series.wikidata_qid %} + <a href="https://www.wikidata.org/wiki/{{ item.series.wikidata_qid }}">Wikidata</a> + {% endif %} + </div> + <div>start: {{ item.start }}</div> + <div>end: {{ item.end }}</div> {% if days %} - <li>days: {{ item.days }}</li> + <div>days: {{ item.days }}</div> {% endif %} - <li>short name: {{ item.short_name }}</li> - <li>country: {{ item.country or "n/a" }}</li> - </ul> + {# <div>short name: {{ item.short_name }}</div> #} + {% if item.venue %} + {% set country = item.venue.city.country %} + <div> + venue: {{ item.venue.name }} + {% if item.venue.wikidata_qid %} + <a href="https://www.wikidata.org/wiki/{{ item.venue.wikidata_qid }}">Wikidata</a> + {% endif %} + </div> + <div> + city: {{ item.venue.city.name }} + {% if item.venue.city.wikidata_qid %} + <a href="https://www.wikidata.org/wiki/{{ item.venue.city.wikidata_qid }}">Wikidata</a> + {% endif %} + </div> + <div>country: {{ country.name }} {{ country.flag }}</div> + {% endif %} + {% if item.wikidata_qid %} + <div>wikidata: <a href="https://www.wikidata.org/wiki/{{ item.wikidata_qid }}">{{ item.wikidata_qid }}</a></div> + {% endif %} + </div> + + {% if show_images %} + <div> + {% for person in item.people %} + {% if person.wikidata_photo %} + <span class="image-container"> + <a href="{{ url_for("person", person_id=person.id) }}"> + <img src="{{ url_for("static", filename=person_image_filename(person.id)) }}" alt="{{ person.name}}" title="{{ person.name}}"> + </a> + </span> + {% endif %} + {% endfor %} + </div> + {% endif %} <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"> + <div> + <div> + <p> + 🎤 + <a href="{{ url_for("event_page", event_id=event.id) }}">{{ event.title }}</a><br> + + Speakers: + {% for p in event.people %} + 👤 + <a href="{{ url_for("person", person_id=p.id) }}">{{ p.name }}</a> + {% endfor %}<br> + {% 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 %} - </h6> - <p class="card-text"> + + <a class="event-detail-toggle" href="#">show details</a><br> + + + + </p> + <div class="event-detail" id="event_{{event.id }}" style="display:none"> {% if event.url %} - <a href="{{ event.url }}">talk on conference website</a> + <p><a href="{{ event.url }}">talk on conference website</a></p> {% endif %} {% if event.abstract %} @@ -60,12 +131,7 @@ </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> </div> @@ -74,3 +140,32 @@ </div> </div> {% endblock %} + +{% block script %} +<script> + // Get all elements with the class "event-detail-toggle" + var toggleLinks = document.querySelectorAll(".event-detail-toggle"); + + // Loop through each toggle link and attach a click event handler + toggleLinks.forEach(function(link) { + link.addEventListener("click", function(e) { + e.preventDefault(); // Prevent the default link behavior + + // Find the parent div of the clicked link + var parentDiv = this.closest("div"); + + // Find the element with class "event-detail" inside the parent div + var detailElement = parentDiv.querySelector(".event-detail"); + + // Toggle the display of the detail element + if (detailElement.style.display === "none" || detailElement.style.display === "") { + detailElement.style.display = "block"; + this.textContent = "hide detail"; // Change the link text + } else { + detailElement.style.display = "none"; + this.textContent = "show detail"; // Change the link text + } + }); + }); +</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 %} <p class="card-text"> - {% if "<" in item.description %} + {% if "<" in item.description %} {{ item.description | safe }} {% else %} {{ item.description }} {% endif %} </p> + {% endif %} <p class="card-text"> 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 @@ <button type="submit" class="btn btn-primary">Search</button> </form> - <div style="margin-bottom:1rem"> + 👥 {{ "{:,d}".format(count.conference) }} conferences<br/> + 🌍 + {{ "{:,d}".format(count.country) }} countries<br/> + 📍 + {{ "{:,d}".format(count.venue) }} venues<br/> + 🎤 {{ "{:,d}".format(count.event) }} talks - <a href="{{ url_for("events_page") }}">most common titles</a><br/> + 👤 {{ "{:,d}".format(count.person) }} speakers <a href="{{ url_for("top_speakers_page") }}">top speakers</a><br/> </div> + <h2>Conferences</h2> + {% for item in items %} + {% if loop.first or item.start.year != loop.previtem.start.year %} + <h3>{{ item.start.year }}</h3> + {% endif %} + <div style="margin-bottom:1.5rem"> + 👥 <a href="{{ url_for("conference_page", short_name=item.short_name) }}">{{ item.title }}</a> + 📅 {{ item.start.strftime("%d %b %Y") }} <br/> {% if item.venue %} + 📍 {{ item.venue.name }} – {{ item.venue.city.name }}, {{ item.venue.city.country.name }} + {{ item.venue.city.country.flag }} <br/> {% endif %} + {# + {% if item.series %} + 📃 Series: {{ item.series.name }} + <br/> + {% 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 @@ <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 %} @@ -33,6 +32,10 @@ <a href="https://www.wikidata.org/wiki/{{ item.wikidata_qid }}">{{ item.wikidata_qid }} on Wikidata</a> {% endif %} </label> + <input class="form-check-input" type="radio" name="name" value="{{ item.id }}" id="name{{ item.id }}"> + <label class="form-check-label" for="name{{ item.id }}">use this name</label><br> + + {% for conf in item.conferences %} 👥{{ conf.title }}{% endfor %} </div> {% 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 @@ <div class="container"> <div class="row"> <h1>{{ item.name }}</h1> - <p><a href="{{ url_for("index") }}">home</a></p> + + <p> + 👥 {{ plural(item.conference_count, "conference") }}<br/> + 🎤 {{ plural(item.event_count, "talk") }}<br/> + {% set start, end = item.active_years() %} + 📅 Years active: {{ start.year }} to {{end.year }} + {% if item.wikidata_qid %} + <br/> + 📊 Wikidata: <a href="https://www.wikidata.org/wiki/{{ item.wikidata_qid }}">{{ item.wikidata_qid }}</a> + {% endif %} + </p> - <h3>Conferences</h3> - {% for apperance in item.conferences_association %} - {% set conf = apperance.conference %} + {% if item.wikidata_photo %} + <img src="{{ url_for("static", filename="wikidata_photo/thumb/" + item.wikidata_photo.0) }}"> + {% endif %} - <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" %} + {% 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"> @@ -39,60 +38,111 @@ <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 %} + <form method="POST" action="{{ url_for("delete_person", person_id=item.id) }}"> + <button type="submit" class="btn btn-primary">delete</button> + </form> - </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> + {% for apperance in item.conference_by_time() %} + {% set conf = apperance.conference %} + + <div> + <h3>👥 {{ conf.title }} + <small>📅 {{ conf.start.strftime("%d %b %Y") }}</small> + + </h3> + {% if apperance.bio %}<p>Biography: {{ apperance.bio | safe }}</p>{% endif %} + </div> + + {% for event in apperance.events %} + <div> + <h4> + 🎤 + <a href="{{ url_for("event_page", event_id=event.id) }}">{{ event.title }}</a> + <small> + {% if event.event_date %} + {{ event.event_date.strftime("%d %b %Y") }} + {% else %} + event date missing {% endif %} - {% endfor %} - </p> - </div> -</div> + + <a class="event-detail-toggle" href="#">show details</a> + </small> + </h4> + <div class="event-detail" id="event_{{event.id }}" style="display:none"> + + <p> + {% if event.url %} + <a href="{{ event.url }}">talk on conference website</a> + {% endif %} + <p> + + {% if event.abstract %} + <div> + {% if "<" in event.abstract %} + {{ event.abstract | safe }} + {% else %} + {{ event.abstract }} + {% endif %} + </div> + {% endif %} + + {% if event.description %} + <div> + {% if "<" in event.description %} + {{ event.description | safe }} + {% else %} + {{ event.description }} + {% endif %} + </div> + {% endif %} + <div> + {% 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 %} + </div> + </div> + </div> + + {% endfor %} + + {% endfor %} + </div> </div> {% endblock %} + +{% block script %} +<script> + // Get all elements with the class "event-detail-toggle" + var toggleLinks = document.querySelectorAll(".event-detail-toggle"); + + // Loop through each toggle link and attach a click event handler + toggleLinks.forEach(function(link) { + link.addEventListener("click", function(e) { + e.preventDefault(); // Prevent the default link behavior + + // Find the parent div of the clicked link + var parentDiv = this.closest("div"); + + // Find the element with class "event-detail" inside the parent div + var detailElement = parentDiv.querySelector(".event-detail"); + + // Toggle the display of the detail element + if (detailElement.style.display === "none" || detailElement.style.display === "") { + detailElement.style.display = "block"; + this.textContent = "hide detail"; // Change the link text + } else { + detailElement.style.display = "none"; + this.textContent = "show detail"; // Change the link text + } + }); + }); +</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 %} + <div class="container"> + <div class="row"> + <h1>Conference archive</h1> + <p><a href="{{ url_for("index") }}">home</a></p> + + <form action="{{ url_for("search_everything") }}"> + <div class="mb-3"> + <input type="text" class="form-control" name="q" id="q" value="{{ search_for }}"> + </div> + <button type="submit" class="btn btn-primary">Search</button> + </form> + + {% if search_for %} + + <h3>Talks</h3> + + <p>Found {{ events.count() }} events matching '{{ search_for }}'</p> + + {% for event in events %} + <div> + <p> + 🎤 + <a href="{{ url_for("event_page", event_id=event.id) }}">{{ event.title }}</a><br> + Speakers: + {% for p in event.people %} + 👤 + <a href="{{ url_for("person", person_id=p.id) }}">{{ p.name }}</a> + {% endfor %}<br> + + 👥 <a href="{{ url_for("conference_page", short_name=event.conference.short_name) }}">{{ event.conference.title }}</a><br> + + {% if event.event_date %} + 📅 {{ event.event_date.strftime("%a, %d %b %Y at %H:%M") }} + {% else %} + event date missing + {% endif %} + </p> + + {% if event.abstract %} + <p class="card-text"> + {% if "<" in event.abstract %} + {{ event.abstract | safe }} + {% else %} + {% for line in event.abstract.splitlines() %} + {{ line }}<br> + {% endfor %} + {% endif %} + </p> + {% endif %} + + {% if event.description and event.description != event.abstract %} + <p class="card-text"> + {% if "<" in event.description %} + {{ event.description | safe }} + {% else %} + {% for line in event.description.splitlines() %} + {{ line }}<br> + {% endfor %} + {% endif %} + </p> + {% endif %} + </div> + + {% endfor %} + + + <h3>People</h3> + <p> + Found {{ people.count() }} people matching '{{ search_for }}' + + </p> + + <ul> + + {% for item in people %} + <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> + + {% endif %} + + </div> + </div> +{% 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 %} - <div class="container"> - <div class="row"> <h1>Conference archive</h1> <form action="{{ url_for("search_people") }}"> @@ -15,23 +13,31 @@ <button type="submit" class="btn btn-primary">Search</button> </form> + <h3>Speaker/conference frequency distribution</h3> + + <p>Distribution of speakers by conference count.</p> + {% for conf_count, speaker_count in speaker_counts %} + <div> + {{ plural(conf_count, "conference") }}: + {{ plural(speaker_count, "speaker") }} + </div> + {% endfor %} + <h3>Top speakers</h3> <ul> {% for person, count in top_speakers %} - <li> + <div> + 👤 <a href="{{ url_for("person", person_id=person.id) }}">{{ person.name }}</a> - ({{ count }}) + ({{ count }} conferences, {{ person.event_count }} talks) + {% if person.wikidata_photo %}📷{% endif %} {% if person.wikidata_qid %} - — - <a href="https://www.wikidata.org/wiki/{{ person.wikidata_qid }}">{{ person.wikidata_qid }} on Wikidata</a> + <a href="https://www.wikidata.org/wiki/{{ person.wikidata_qid }}">Wikidata</a> {% endif %} - </li> + </div> {% endfor %} - </ul> - </div> - </div> {% endblock %}