feat: add travel and concert streak patches
This commit is contained in:
+86
-27
@@ -89,6 +89,10 @@ BADGE_DEFINITIONS = (
|
||||
("tour_veteran", "25 Gigs", "⚡", 25, "25 besuchte Konzerte", "attendance"),
|
||||
("fifty_gigs", "50 Gigs", "💀", 50, "50 besuchte Konzerte", "attendance"),
|
||||
("hundred_gigs", "100 Gigs", "👑", 100, "100 besuchte Konzerte", "attendance"),
|
||||
("border_breaker", "BORDER BREAKER", "🛂", 1, "Erstes Konzert im Ausland", "travel"),
|
||||
("globe_banger", "GLOBE BANGER", "🌍", 5, "Konzerte in 5 Ländern", "travel"),
|
||||
("double_trouble", "DOUBLE TROUBLE", "⚡", 2, "Zwei Konzerte an zwei aufeinanderfolgenden Tagen", "streak"),
|
||||
("iron_weekend", "IRON WEEKEND", "🤘", 3, "Drei Konzerte innerhalb eines Wochenendes", "streak"),
|
||||
)
|
||||
ATTENDANCE_BADGE_CODES = tuple(
|
||||
badge_code
|
||||
@@ -104,6 +108,14 @@ BADGE_BY_CODE = {
|
||||
badge_code: (name, icon, threshold, description, category)
|
||||
for badge_code, name, icon, threshold, description, category in BADGE_DEFINITIONS
|
||||
}
|
||||
BADGE_CATEGORY_LABELS = {
|
||||
"special": "⚔️ Spezial-Patches",
|
||||
"beta": "🧪 Community",
|
||||
"attendance": "🎸 Konzert-Meilensteine",
|
||||
"venue": "🤘 Stammorte",
|
||||
"travel": "🌍 Unterwegs",
|
||||
"streak": "⚡ Tourmodus",
|
||||
}
|
||||
|
||||
|
||||
def get_db_connection():
|
||||
@@ -1129,41 +1141,65 @@ def remove_uploaded_file(path: str | None, destination_dir: str, url_prefix: str
|
||||
return True
|
||||
|
||||
|
||||
def attended_concert_stats(user_id: int) -> tuple[int, int]:
|
||||
def normalize_country(value: str) -> str:
|
||||
country = (value or "").strip().casefold()
|
||||
aliases = {"de": "deutschland", "deu": "deutschland", "germany": "deutschland"}
|
||||
return aliases.get(country, country)
|
||||
|
||||
|
||||
def attended_concert_stats(user_id: int) -> dict:
|
||||
with get_db_connection() as connection:
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(
|
||||
"""
|
||||
WITH attended AS (
|
||||
SELECT concerts.venue_id
|
||||
, concerts.event_type
|
||||
FROM concert_attendance
|
||||
JOIN concerts ON concerts.id = concert_attendance.concert_id
|
||||
WHERE concert_attendance.user_id = %s
|
||||
AND concert_attendance.status = 'attending'
|
||||
AND COALESCE(concerts.end_datetime, concerts.start_datetime) < CURRENT_TIMESTAMP
|
||||
), venue_counts AS (
|
||||
SELECT venue_id, COUNT(*) AS visit_count
|
||||
FROM attended
|
||||
WHERE venue_id IS NOT NULL AND event_type <> 'other'
|
||||
GROUP BY venue_id
|
||||
)
|
||||
SELECT
|
||||
(SELECT COUNT(*) FROM attended),
|
||||
COALESCE((SELECT MAX(visit_count) FROM venue_counts), 0)
|
||||
SELECT concerts.venue_id, concerts.event_type,
|
||||
concerts.start_datetime::date, COALESCE(venues.country, '')
|
||||
FROM concert_attendance
|
||||
JOIN concerts ON concerts.id = concert_attendance.concert_id
|
||||
LEFT JOIN venues ON venues.id = concerts.venue_id
|
||||
WHERE concert_attendance.user_id = %s
|
||||
AND concert_attendance.status = 'attending'
|
||||
AND COALESCE(concerts.end_datetime, concerts.start_datetime) < CURRENT_TIMESTAMP
|
||||
""",
|
||||
(user_id,),
|
||||
)
|
||||
total, max_same_venue = cursor.fetchone()
|
||||
return total, max_same_venue
|
||||
rows = cursor.fetchall()
|
||||
|
||||
venue_counts = {}
|
||||
countries = set()
|
||||
dates = []
|
||||
weekend_counts = {}
|
||||
for venue_id, event_type, concert_date, country_value in rows:
|
||||
if venue_id is not None and event_type != "other":
|
||||
venue_counts[venue_id] = venue_counts.get(venue_id, 0) + 1
|
||||
country = normalize_country(country_value)
|
||||
if country:
|
||||
countries.add(country)
|
||||
dates.append(concert_date)
|
||||
if concert_date.isoweekday() >= 5:
|
||||
weekend_key = concert_date.isocalendar()[:2]
|
||||
weekend_counts[weekend_key] = weekend_counts.get(weekend_key, 0) + 1
|
||||
|
||||
distinct_dates = sorted(set(dates))
|
||||
return {
|
||||
"total": len(rows),
|
||||
"max_same_venue": max(venue_counts.values(), default=0),
|
||||
"country_count": len(countries),
|
||||
"has_foreign_concert": any(country != "deutschland" for country in countries),
|
||||
"has_consecutive_days": any(
|
||||
later - earlier == timedelta(days=1)
|
||||
for earlier, later in zip(distinct_dates, distinct_dates[1:])
|
||||
),
|
||||
"has_iron_weekend": max(weekend_counts.values(), default=0) >= 3,
|
||||
}
|
||||
|
||||
|
||||
def grant_earned_badges(user_id: int, attended_count: int, max_same_venue, registered_at):
|
||||
def grant_earned_badges(user_id: int, stats: dict, registered_at):
|
||||
highest_attendance_badge = None
|
||||
venue_badge = "regular" if max_same_venue >= 5 else None
|
||||
venue_badge = "regular" if stats["max_same_venue"] >= 5 else None
|
||||
|
||||
for badge_code, _name, _icon, threshold, _description, category in BADGE_DEFINITIONS:
|
||||
qualifies = category == "attendance" and threshold is not None and attended_count >= threshold
|
||||
qualifies = category == "attendance" and threshold is not None and stats["total"] >= threshold
|
||||
if qualifies:
|
||||
highest_attendance_badge = badge_code
|
||||
|
||||
@@ -1199,6 +1235,20 @@ def grant_earned_badges(user_id: int, attended_count: int, max_same_venue, regis
|
||||
"INSERT INTO user_badges (user_id, badge_code) VALUES (%s, %s) ON CONFLICT DO NOTHING",
|
||||
(user_id, venue_badge),
|
||||
)
|
||||
achievement_codes = []
|
||||
if stats["has_foreign_concert"]:
|
||||
achievement_codes.append("border_breaker")
|
||||
if stats["country_count"] >= 5:
|
||||
achievement_codes.append("globe_banger")
|
||||
if stats["has_consecutive_days"]:
|
||||
achievement_codes.append("double_trouble")
|
||||
if stats["has_iron_weekend"]:
|
||||
achievement_codes.append("iron_weekend")
|
||||
for badge_code in achievement_codes:
|
||||
cursor.execute(
|
||||
"INSERT INTO user_badges (user_id, badge_code) VALUES (%s, %s) ON CONFLICT DO NOTHING",
|
||||
(user_id, badge_code),
|
||||
)
|
||||
connection.commit()
|
||||
|
||||
|
||||
@@ -2337,11 +2387,10 @@ def render_profile(
|
||||
{"username": row[0], "display_name": row[1], "avatar_path": row[2]}
|
||||
for row in cursor.fetchall()
|
||||
]
|
||||
attended_count, max_same_venue = attended_concert_stats(profile["id"])
|
||||
attendance_stats = attended_concert_stats(profile["id"])
|
||||
grant_earned_badges(
|
||||
profile["id"],
|
||||
attended_count,
|
||||
max_same_venue,
|
||||
attendance_stats,
|
||||
profile["registered_at"],
|
||||
)
|
||||
profile = load_profile(username)
|
||||
@@ -2361,14 +2410,24 @@ def render_profile(
|
||||
for code, name, icon, threshold, description, category in BADGE_DEFINITIONS
|
||||
]
|
||||
badges.sort(key=lambda badge: badge["sort_key"])
|
||||
badge_groups = [
|
||||
{
|
||||
"code": category,
|
||||
"label": label,
|
||||
"badges": [badge for badge in badges if badge["earned"] and badge["category"] == category],
|
||||
}
|
||||
for category, label in BADGE_CATEGORY_LABELS.items()
|
||||
if any(badge["earned"] and badge["category"] == category for badge in badges)
|
||||
]
|
||||
|
||||
template = templates.get_template("profile.html")
|
||||
return HTMLResponse(
|
||||
template.render(
|
||||
user=viewer,
|
||||
profile=profile,
|
||||
attended_count=attended_count,
|
||||
attended_count=attendance_stats["total"],
|
||||
badges=badges,
|
||||
badge_groups=badge_groups,
|
||||
is_own_profile=force_own or is_own_profile,
|
||||
can_view_details=can_view_details,
|
||||
friendship=friendship,
|
||||
|
||||
Reference in New Issue
Block a user