Kutten Test
This commit is contained in:
+78
-17
@@ -42,9 +42,17 @@ AVATAR_DIR = os.path.join(
|
|||||||
"avatars"
|
"avatars"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
PATCH_DIR = os.path.join(
|
||||||
|
BASE_DIR,
|
||||||
|
"static",
|
||||||
|
"uploads",
|
||||||
|
"patches"
|
||||||
|
)
|
||||||
|
|
||||||
os.makedirs(UPLOAD_DIR, exist_ok=True)
|
os.makedirs(UPLOAD_DIR, exist_ok=True)
|
||||||
os.makedirs(PHOTO_DIR, exist_ok=True)
|
os.makedirs(PHOTO_DIR, exist_ok=True)
|
||||||
os.makedirs(AVATAR_DIR, exist_ok=True)
|
os.makedirs(AVATAR_DIR, exist_ok=True)
|
||||||
|
os.makedirs(PATCH_DIR, exist_ok=True)
|
||||||
|
|
||||||
SESSION_COOKIE = "pingu_session"
|
SESSION_COOKIE = "pingu_session"
|
||||||
SESSION_DAYS = 30
|
SESSION_DAYS = 30
|
||||||
@@ -55,11 +63,22 @@ INITIAL_ADMIN_PASSWORD = os.environ.get("INITIAL_ADMIN_PASSWORD")
|
|||||||
INITIAL_ADMIN_EMAIL = os.environ.get("INITIAL_ADMIN_EMAIL")
|
INITIAL_ADMIN_EMAIL = os.environ.get("INITIAL_ADMIN_EMAIL")
|
||||||
|
|
||||||
BADGE_DEFINITIONS = (
|
BADGE_DEFINITIONS = (
|
||||||
("first_gig", "Erster Gig", "🎸", 1, "Dein erstes besuchtes Konzert"),
|
("beta_tester", "Beta Tester", "🧪", None, "In der Beta dabei", "beta"),
|
||||||
("regular", "Stammgast", "🤘", 5, "5 Konzerte besucht"),
|
("first_gig", "Erster Gig", "🎸", 1, "Dein erstes besuchtes Konzert", "attendance"),
|
||||||
("ten_gigs", "Zehnerrunde", "🔥", 10, "10 Konzerte besucht"),
|
("regular", "Stammgast", "🤘", 5, "5 Konzerte besucht", "attendance"),
|
||||||
("tour_veteran", "Tourveteran", "⚡", 25, "25 Konzerte besucht"),
|
("ten_gigs", "Zehnerrunde", "🔥", 10, "10 Konzerte besucht", "attendance"),
|
||||||
|
("tour_veteran", "Tourveteran", "⚡", 25, "25 Konzerte besucht", "attendance"),
|
||||||
)
|
)
|
||||||
|
ATTENDANCE_BADGE_CODES = tuple(
|
||||||
|
badge_code
|
||||||
|
for badge_code, _name, _icon, _threshold, _description, category in BADGE_DEFINITIONS
|
||||||
|
if category == "attendance"
|
||||||
|
)
|
||||||
|
BETA_REGISTRATION_DEADLINE = datetime(2026, 9, 16)
|
||||||
|
BADGE_BY_CODE = {
|
||||||
|
badge_code: (name, icon, threshold, description, category)
|
||||||
|
for badge_code, name, icon, threshold, description, category in BADGE_DEFINITIONS
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def get_db_connection():
|
def get_db_connection():
|
||||||
@@ -147,6 +166,14 @@ def ensure_schema():
|
|||||||
PRIMARY KEY (user_id, badge_code)
|
PRIMARY KEY (user_id, badge_code)
|
||||||
)
|
)
|
||||||
""",
|
""",
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS badge_assets (
|
||||||
|
badge_code VARCHAR(50) PRIMARY KEY,
|
||||||
|
path TEXT NOT NULL,
|
||||||
|
updated_by INTEGER REFERENCES users(id) ON DELETE SET NULL,
|
||||||
|
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
""",
|
||||||
]
|
]
|
||||||
|
|
||||||
with get_db_connection() as connection:
|
with get_db_connection() as connection:
|
||||||
@@ -496,22 +523,48 @@ def attended_concert_count(user_id: int) -> int:
|
|||||||
return cursor.fetchone()[0]
|
return cursor.fetchone()[0]
|
||||||
|
|
||||||
|
|
||||||
def grant_earned_badges(user_id: int, attended_count: int):
|
def grant_earned_badges(user_id: int, attended_count: int, registered_at):
|
||||||
|
highest_attendance_badge = None
|
||||||
|
|
||||||
|
for badge_code, _name, _icon, threshold, _description, category in BADGE_DEFINITIONS:
|
||||||
|
if category == "attendance" and attended_count >= threshold:
|
||||||
|
highest_attendance_badge = badge_code
|
||||||
|
|
||||||
with get_db_connection() as connection:
|
with get_db_connection() as connection:
|
||||||
with connection.cursor() as cursor:
|
with connection.cursor() as cursor:
|
||||||
for badge_code, _name, _icon, threshold, _description in BADGE_DEFINITIONS:
|
if registered_at < BETA_REGISTRATION_DEADLINE:
|
||||||
if attended_count >= threshold:
|
cursor.execute(
|
||||||
cursor.execute(
|
"""
|
||||||
"""
|
INSERT INTO user_badges (user_id, badge_code)
|
||||||
INSERT INTO user_badges (user_id, badge_code)
|
VALUES (%s, 'beta_tester')
|
||||||
VALUES (%s, %s)
|
ON CONFLICT (user_id, badge_code) DO NOTHING
|
||||||
ON CONFLICT (user_id, badge_code) DO NOTHING
|
""",
|
||||||
""",
|
(user_id,),
|
||||||
(user_id, badge_code),
|
)
|
||||||
)
|
|
||||||
|
if highest_attendance_badge:
|
||||||
|
cursor.execute(
|
||||||
|
"DELETE FROM user_badges WHERE user_id = %s AND badge_code = ANY(%s)",
|
||||||
|
(user_id, list(ATTENDANCE_BADGE_CODES)),
|
||||||
|
)
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO user_badges (user_id, badge_code)
|
||||||
|
VALUES (%s, %s)
|
||||||
|
ON CONFLICT (user_id, badge_code) DO NOTHING
|
||||||
|
""",
|
||||||
|
(user_id, highest_attendance_badge),
|
||||||
|
)
|
||||||
connection.commit()
|
connection.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def load_badge_assets():
|
||||||
|
with get_db_connection() as connection:
|
||||||
|
with connection.cursor() as cursor:
|
||||||
|
cursor.execute("SELECT badge_code, path FROM badge_assets")
|
||||||
|
return {row[0]: row[1] for row in cursor.fetchall()}
|
||||||
|
|
||||||
|
|
||||||
def load_profile(username: str):
|
def load_profile(username: str):
|
||||||
with get_db_connection() as connection:
|
with get_db_connection() as connection:
|
||||||
with connection.cursor() as cursor:
|
with connection.cursor() as cursor:
|
||||||
@@ -540,6 +593,7 @@ def load_profile(username: str):
|
|||||||
"display_name": row[2] or row[1],
|
"display_name": row[2] or row[1],
|
||||||
"avatar_path": row[3],
|
"avatar_path": row[3],
|
||||||
"created_at": row[4].strftime("%d.%m.%Y"),
|
"created_at": row[4].strftime("%d.%m.%Y"),
|
||||||
|
"registered_at": row[4],
|
||||||
"earned_codes": earned_codes,
|
"earned_codes": earned_codes,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1134,8 +1188,13 @@ def render_profile(request: Request, username: str):
|
|||||||
return HTMLResponse("<h1>Benutzer nicht gefunden</h1>", status_code=404)
|
return HTMLResponse("<h1>Benutzer nicht gefunden</h1>", status_code=404)
|
||||||
|
|
||||||
attended_count = attended_concert_count(profile["id"])
|
attended_count = attended_concert_count(profile["id"])
|
||||||
grant_earned_badges(profile["id"], attended_count)
|
grant_earned_badges(
|
||||||
|
profile["id"],
|
||||||
|
attended_count,
|
||||||
|
profile["registered_at"],
|
||||||
|
)
|
||||||
profile = load_profile(username)
|
profile = load_profile(username)
|
||||||
|
badge_assets = load_badge_assets()
|
||||||
badges = [
|
badges = [
|
||||||
{
|
{
|
||||||
"code": code,
|
"code": code,
|
||||||
@@ -1143,9 +1202,11 @@ def render_profile(request: Request, username: str):
|
|||||||
"icon": icon,
|
"icon": icon,
|
||||||
"threshold": threshold,
|
"threshold": threshold,
|
||||||
"description": description,
|
"description": description,
|
||||||
|
"category": category,
|
||||||
"earned": code in profile["earned_codes"],
|
"earned": code in profile["earned_codes"],
|
||||||
|
"image_path": badge_assets.get(code),
|
||||||
}
|
}
|
||||||
for code, name, icon, threshold, description in BADGE_DEFINITIONS
|
for code, name, icon, threshold, description, category in BADGE_DEFINITIONS
|
||||||
]
|
]
|
||||||
|
|
||||||
template = templates.get_template("profile.html")
|
template = templates.get_template("profile.html")
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
.patch-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(105px, 1fr)); gap: 14px; }
|
.patch-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(105px, 1fr)); gap: 14px; }
|
||||||
.patch { min-height: 94px; padding: 10px 7px; display: grid; place-items: center; align-content: center; gap: 4px; text-align: center; background: #242424; border: 2px dashed #78716c; border-radius: 9px; color: #f8fafc; }
|
.patch { min-height: 94px; padding: 10px 7px; display: grid; place-items: center; align-content: center; gap: 4px; text-align: center; background: #242424; border: 2px dashed #78716c; border-radius: 9px; color: #f8fafc; }
|
||||||
.patch-icon { font-size: 1.6rem; }
|
.patch-icon { font-size: 1.6rem; }
|
||||||
|
.patch-image { width: 58px; height: 58px; object-fit: contain; }
|
||||||
.patch small { color: #d1d5db; }
|
.patch small { color: #d1d5db; }
|
||||||
.patch.locked { opacity: .38; filter: grayscale(1); }
|
.patch.locked { opacity: .38; filter: grayscale(1); }
|
||||||
.patch.earned { border-style: solid; border-color: #a78bfa; box-shadow: 0 0 12px rgba(167, 139, 250, .35); }
|
.patch.earned { border-style: solid; border-color: #a78bfa; box-shadow: 0 0 12px rgba(167, 139, 250, .35); }
|
||||||
@@ -57,16 +58,26 @@
|
|||||||
</section>
|
</section>
|
||||||
<section class="badges-card">
|
<section class="badges-card">
|
||||||
<h2>🥋 Virtuelle Kutte</h2>
|
<h2>🥋 Virtuelle Kutte</h2>
|
||||||
<p>Für besuchte Konzerte schaltest du neue Patches frei.</p>
|
<p>Deine freigeschalteten Patches. Besuchs-Patches werden jeweils durch die höchste Stufe ersetzt.</p>
|
||||||
<div class="kutte">
|
<div class="kutte">
|
||||||
<div class="patch-grid">
|
<div class="patch-grid">
|
||||||
{% for badge in badges %}
|
{% for badge in badges %}
|
||||||
<div class="patch {% if badge.earned %}earned{% else %}locked{% endif %}" title="{{ badge.description }}">
|
{% if badge.earned %}
|
||||||
<span class="patch-icon">{{ badge.icon }}</span>
|
<div class="patch earned" title="{{ badge.description }}">
|
||||||
<strong>{{ badge.name }}</strong>
|
{% if badge.image_path %}
|
||||||
<small>{% if badge.earned %}Freigeschaltet{% else %}ab {{ badge.threshold }} Konzerten{% endif %}</small>
|
<img class="patch-image" src="{{ badge.image_path }}" alt="Patch {{ badge.name }}">
|
||||||
</div>
|
{% else %}
|
||||||
|
<span class="patch-icon">{{ badge.icon }}</span>
|
||||||
|
{% endif %}
|
||||||
|
<strong>{{ badge.name }}</strong>
|
||||||
|
<small>Freigeschaltet</small>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
|
{% if not profile.earned_codes %}
|
||||||
|
<p>Noch keine Patches – dein erstes Konzert wartet!</p>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
Reference in New Issue
Block a user