Skip to content

Commit

Permalink
Restore the stats endpoint (mostly!)
Browse files Browse the repository at this point in the history
This largely reverts commit cdddc39.

Reading from a CSV is something of a one-time hack that's meant to buy
me some time while we build a real API endpoint over in the geardb.

This "temporary" disabling nearly made it 2 years, hah.
  • Loading branch information
DavidCain committed Apr 17, 2024
1 parent 6175a0d commit 6bfe7b9
Show file tree
Hide file tree
Showing 3 changed files with 64 additions and 26 deletions.
17 changes: 3 additions & 14 deletions ws/templates/stats/membership.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
{{ block.super }}
{# For now, use the Vega charting library for fast-and-easy charts #}
{# I may substitute this later for raw D3 #}
<script src="https://cdn.jsdelivr.net/npm/vega@4.3.0"></script>
<script src="https://cdn.jsdelivr.net/npm/vega-embed@3"></script>
<script src="https://cdn.jsdelivr.net/npm/vega@5.28.0"></script>
<script src="https://cdn.jsdelivr.net/npm/vega-embed@6.25.0"></script>
<script>
var possibleAffiliations = [
"MIT undergrad",
Expand All @@ -17,12 +17,10 @@
"MIT alum",
"MIT affiliate",
"Non-affiliate",
"Student",
"Unknown",
];

var barChartTemplate = {
"$schema": "https://vega.github.io/schema/vega/v4.json",
"$schema": "https://vega.github.io/schema/vega/v5.json",
"width": 900,
"height": 300,
"padding": 5,
Expand Down Expand Up @@ -215,15 +213,6 @@
{{ block.super }}

<h1>Membership Statistics</h1>
<div class="alert alert-danger">
<strong>July 14, 2022</strong>
<p>
I've temporarily taken down this page to support mitoc-gear modernizations.
I anticipate we'll have the stats back some time in late July. Need data sooner?
<a href="mailto:[email protected]">Contact me</a>.
</p>
</div>

<p class="lead">
These visualizations are meant to give quick insights. Feel free to
download the <a href="{% url 'json-membership_stats' %}">raw data</a> to
Expand Down
8 changes: 0 additions & 8 deletions ws/tests/utils/test_geardb.py
Original file line number Diff line number Diff line change
Expand Up @@ -277,11 +277,3 @@ def test_success(self):
waiver_expires=date(2023, 5, 4),
),
)


class MembershipStatsTest(TestCase):
"""Small stub class, to be completed once a full API integration returns."""

def test_currently_empty(self):
"""We can at least *call* this method, it just returns nothing for now."""
self.assertEqual(geardb.membership_information(), {})
65 changes: 61 additions & 4 deletions ws/utils/geardb.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,19 @@
via machine-to-machine API endpoints.
"""

import csv
import logging
from collections.abc import Iterable, Iterator
from datetime import date, datetime, timedelta
from pathlib import Path
from typing import Any, NamedTuple
from urllib.parse import urljoin

import requests
from allauth.account.models import EmailAddress
from django.contrib.auth.models import User
from django.db.models import Case, Count, IntegerField, Sum, When
from django.db.models.functions import Lower

from ws import models, settings
from ws.utils import api as api_util
Expand Down Expand Up @@ -258,6 +261,42 @@ def update_affiliation(participant: models.Participant) -> requests.Response | N
)


def _stats_only_all_active_members() -> Iterator[tuple[str, MembershipInformation]]:
"""Yield emails and rental activity for all members with current dues."""

# Populated with a psql command:
# COPY (
# SELECT p.id AS person_id,
# coalesce(p.affiliation, 'Unknown') AS last_known_affiliation,
# lower(p.email) AS email,
# lower(pe.alternate_email) AS alternate_email,
# count(r.id) AS num_rentals
# FROM people p
# join people_memberships pm on p.id = pm.person_id
# left join geardb_peopleemails pe on p.id = pe.person_id
# left join rentals r on p.id = r.person_id
# where pm.expires > now()
# group by p.id, p.affiliation, p.email, pe.alternate_email
# ) To '/tmp/members.csv' With CSV DELIMITER ',' HEADER;
members_path = Path(__file__).parent.parent / "members.csv"

with open(members_path, encoding="utf-8") as member_file:
reader = csv.DictReader(member_file)
for row in reader:
known_emails = (e for e in (row["email"], row["alternate_email"]) if e)
for email in known_emails:
info = MembershipInformation(
# NOTE: Anyone with >1 alternate email will have multiple rows!
# (We'll use multiple emails to look up). Ensure we enforce uniqueness
person_id=int(row["person_id"]),
last_known_affiliation=row["last_known_affiliation"],
num_rentals=int(row["num_rentals"]),
# We don't have any trips information here.
trips_information=None,
)
yield email, info


def trips_information() -> dict[int, TripsInformation]:
"""Give important counts, indexed by user IDs.
Expand Down Expand Up @@ -310,9 +349,27 @@ def membership_information() -> dict[int, MembershipInformation]:
- have rented gear
- make use MITOC discounts
"""
info_by_user_id = ( # pylint: disable=unused-variable # noqa: F841
trips_information()
info_by_user_id = trips_information()

# Bridge from a lowercase email address to a Trips user ID
email_to_user_id: dict[str, int] = dict(
EmailAddress.objects.filter(verified=True)
.annotate(lower_email=Lower("email"))
.values_list("lower_email", "user_id")
)

# This method should soon be replaced by an API call to mitoc-gear
return {}
def trips_info_for(email: str) -> TripsInformation | None:
try:
user_id = email_to_user_id[email]
except KeyError: # No Trips account
return None

try:
return info_by_user_id[user_id]
except KeyError: # User, but no corresponding Participant
return None

return {
info.person_id: info._replace(trips_information=trips_info_for(email))
for email, info in _stats_only_all_active_members()
}

0 comments on commit 6bfe7b9

Please sign in to comment.