Fix two-column badge layout and descriptions
This commit is contained in:
@@ -412,6 +412,22 @@
|
|||||||
"Pro Tagebucheintrag sind maximal drei Bilder möglich.": "Each diary entry can have a maximum of three images.",
|
"Pro Tagebucheintrag sind maximal drei Bilder möglich.": "Each diary entry can have a maximum of three images.",
|
||||||
"Ein Mitglied": "A member",
|
"Ein Mitglied": "A member",
|
||||||
"Von Anfang an dabei und MetalCircle aufgebaut": "Here from the start and helped build MetalCircle",
|
"Von Anfang an dabei und MetalCircle aufgebaut": "Here from the start and helped build MetalCircle",
|
||||||
|
"Dieser Patch würdigt dich als Teil der ersten Crew, die MetalCircle möglich gemacht hat. Du hast die Community von Anfang an begleitet und beim Aufbau mitgewirkt.": "This patch recognizes you as part of the first crew that made MetalCircle possible. You have been with the community from the beginning and helped build it.",
|
||||||
|
"Dieser Patch steht für die Verantwortung, MetalCircle zu verwalten und die Community im Alltag zu unterstützen.": "This patch represents the responsibility of managing MetalCircle and supporting the community in its day-to-day work.",
|
||||||
|
"Ein besonderer Patch für die treue Gefährtin des Captains – als sichtbares Zeichen für Vertrauen und ihren festen Platz in der Crew.": "A special patch for the Captain's trusted companion, recognizing the trust and the valued place she holds in the crew.",
|
||||||
|
"Du hast MetalCircle bereits in der Alpha begleitet. Mit deinen frühen Erfahrungen und Rückmeldungen hast du geholfen, die Plattform vor der Beta weiterzuentwickeln.": "You joined MetalCircle during the alpha. Your early experience and feedback helped shape the platform before the beta.",
|
||||||
|
"Du bist während der geschlossenen Beta Teil der Crew geworden und hast MetalCircle in einer frühen Phase begleitet.": "You joined the crew during the closed beta and supported MetalCircle in an early stage of its journey.",
|
||||||
|
"Du bist früh zur MetalCircle-Community gestoßen und gehörst zu den ersten, die gemeinsam den Weg der Plattform mitgehen.": "You joined the MetalCircle community early and are among the first members to help the platform grow.",
|
||||||
|
"Der erste Schritt zählt: Dieser Patch markiert das erste Konzert, das du in MetalCircle als besucht festgehalten hast.": "Every journey starts somewhere. This patch marks the first concert you recorded as attended in MetalCircle.",
|
||||||
|
"Fünf Besuche am selben Veranstaltungsort machen diesen Ort zu mehr als einem Eintrag im Kalender. Dieser Patch würdigt deine wiederkehrenden Abende dort.": "Five visits turn a venue into more than a name on the calendar. This patch celebrates the many nights you have returned there for live music.",
|
||||||
|
"Zehn besuchte Konzerte sind ein erster kräftiger Meilenstein deiner Konzertgeschichte. Jeder Eintrag steht für einen Abend, den du live erlebt hast.": "Ten attended concerts are a strong first milestone in your concert history. Each one marks a night of live music you experienced in person.",
|
||||||
|
"Mit 25 besuchten Konzerten wächst aus einzelnen Abenden eine echte Konzertgeschichte. Dieser Patch hält fest, wie oft du schon mitten im Publikum standest.": "With 25 attended concerts, a collection of nights out becomes a real concert history. This patch marks all the times you have been there in the crowd.",
|
||||||
|
"50 besuchte Konzerte sind ein großer Meilenstein. Dieser Patch würdigt viele Abende mit lauter Musik, besonderen Erinnerungen und gemeinsamen Erlebnissen.": "Fifty attended concerts are a major milestone. This patch celebrates years of loud music, lasting memories, and nights shared with others.",
|
||||||
|
"100 besuchte Konzerte: Dieser Patch steht für eine beeindruckende Konzertgeschichte und unzählige Stunden voller Live-Musik.": "One hundred attended concerts make for an impressive history and countless hours spent enjoying live music.",
|
||||||
|
"Dein Konzertbesuch hat dich zum ersten Mal über die Landesgrenze geführt. Dieser Patch erinnert an den Auftakt deiner internationalen Konzertreise.": "Your first concert abroad took you across a national border. This patch marks the beginning of your international concert journey.",
|
||||||
|
"Du hast Konzerte in fünf unterschiedlichen Ländern erlebt. Dieser Patch steht für eine Konzertreise, die weit über den eigenen Heimatraum hinausgeht.": "You have attended concerts in five different countries. This patch celebrates a concert journey that has taken you well beyond home.",
|
||||||
|
"An zwei aufeinanderfolgenden Tagen ein Konzert zu besuchen, zeigt echte Ausdauer. Dieser Patch markiert genau diese Konzertserie.": "Attending concerts on two consecutive days takes dedication. This patch marks that back-to-back run of live music.",
|
||||||
|
"Drei Konzerte an einem Wochenende: Dieser Patch würdigt ein besonders intensives Wochenende voller Live-Musik.": "Three concerts in one weekend make for an unforgettable run. This patch celebrates a weekend packed with live music.",
|
||||||
"In der Beta dabei": "Joined during the beta",
|
"In der Beta dabei": "Joined during the beta",
|
||||||
"Erster Gig": "First gig",
|
"Erster Gig": "First gig",
|
||||||
"Stammgast": "Regular",
|
"Stammgast": "Regular",
|
||||||
|
|||||||
+19
@@ -116,6 +116,24 @@ BADGE_DEFINITIONS = (
|
|||||||
("double_trouble", "DOUBLE TROUBLE", "⚡", 2, "Zwei Konzerte an zwei aufeinanderfolgenden Tagen", "streak"),
|
("double_trouble", "DOUBLE TROUBLE", "⚡", 2, "Zwei Konzerte an zwei aufeinanderfolgenden Tagen", "streak"),
|
||||||
("iron_weekend", "IRON WEEKEND", "🤘", 3, "Drei Konzerte innerhalb eines Wochenendes", "streak"),
|
("iron_weekend", "IRON WEEKEND", "🤘", 3, "Drei Konzerte innerhalb eines Wochenendes", "streak"),
|
||||||
)
|
)
|
||||||
|
BADGE_LONG_DESCRIPTIONS = {
|
||||||
|
"founder": "Dieser Patch würdigt dich als Teil der ersten Crew, die MetalCircle möglich gemacht hat. Du hast die Community von Anfang an begleitet und beim Aufbau mitgewirkt.",
|
||||||
|
"admin": "Dieser Patch steht für die Verantwortung, MetalCircle zu verwalten und die Community im Alltag zu unterstützen.",
|
||||||
|
"captns_mate": "Ein besonderer Patch für die treue Gefährtin des Captains – als sichtbares Zeichen für Vertrauen und ihren festen Platz in der Crew.",
|
||||||
|
"alpha_tester": "Du hast MetalCircle bereits in der Alpha begleitet. Mit deinen frühen Erfahrungen und Rückmeldungen hast du geholfen, die Plattform vor der Beta weiterzuentwickeln.",
|
||||||
|
"beta_tester": "Du bist während der geschlossenen Beta Teil der Crew geworden und hast MetalCircle in einer frühen Phase begleitet.",
|
||||||
|
"early_bird": "Du bist früh zur MetalCircle-Community gestoßen und gehörst zu den ersten, die gemeinsam den Weg der Plattform mitgehen.",
|
||||||
|
"first_gig": "Der erste Schritt zählt: Dieser Patch markiert das erste Konzert, das du in MetalCircle als besucht festgehalten hast.",
|
||||||
|
"regular": "Fünf Besuche am selben Veranstaltungsort machen diesen Ort zu mehr als einem Eintrag im Kalender. Dieser Patch würdigt deine wiederkehrenden Abende dort.",
|
||||||
|
"ten_gigs": "Zehn besuchte Konzerte sind ein erster kräftiger Meilenstein deiner Konzertgeschichte. Jeder Eintrag steht für einen Abend, den du live erlebt hast.",
|
||||||
|
"tour_veteran": "Mit 25 besuchten Konzerten wächst aus einzelnen Abenden eine echte Konzertgeschichte. Dieser Patch hält fest, wie oft du schon mitten im Publikum standest.",
|
||||||
|
"fifty_gigs": "50 besuchte Konzerte sind ein großer Meilenstein. Dieser Patch würdigt viele Abende mit lauter Musik, besonderen Erinnerungen und gemeinsamen Erlebnissen.",
|
||||||
|
"hundred_gigs": "100 besuchte Konzerte: Dieser Patch steht für eine beeindruckende Konzertgeschichte und unzählige Stunden voller Live-Musik.",
|
||||||
|
"border_breaker": "Dein Konzertbesuch hat dich zum ersten Mal über die Landesgrenze geführt. Dieser Patch erinnert an den Auftakt deiner internationalen Konzertreise.",
|
||||||
|
"globe_banger": "Du hast Konzerte in fünf unterschiedlichen Ländern erlebt. Dieser Patch steht für eine Konzertreise, die weit über den eigenen Heimatraum hinausgeht.",
|
||||||
|
"double_trouble": "An zwei aufeinanderfolgenden Tagen ein Konzert zu besuchen, zeigt echte Ausdauer. Dieser Patch markiert genau diese Konzertserie.",
|
||||||
|
"iron_weekend": "Drei Konzerte an einem Wochenende: Dieser Patch würdigt ein besonders intensives Wochenende voller Live-Musik.",
|
||||||
|
}
|
||||||
ATTENDANCE_BADGE_CODES = tuple(
|
ATTENDANCE_BADGE_CODES = tuple(
|
||||||
badge_code
|
badge_code
|
||||||
for badge_code, _name, _icon, _threshold, _description, category in BADGE_DEFINITIONS
|
for badge_code, _name, _icon, _threshold, _description, category in BADGE_DEFINITIONS
|
||||||
@@ -2736,6 +2754,7 @@ def render_profile(
|
|||||||
"icon": icon,
|
"icon": icon,
|
||||||
"threshold": threshold,
|
"threshold": threshold,
|
||||||
"description": description,
|
"description": description,
|
||||||
|
"long_description": BADGE_LONG_DESCRIPTIONS[code],
|
||||||
"category": category,
|
"category": category,
|
||||||
"earned": code in profile["earned_codes"] and (category != "attendance" or code == highest_visible_attendance),
|
"earned": code in profile["earned_codes"] and (category != "attendance" or code == highest_visible_attendance),
|
||||||
"image_path": badge_assets.get(code),
|
"image_path": badge_assets.get(code),
|
||||||
|
|||||||
@@ -26,17 +26,20 @@
|
|||||||
.kutte::before, .kutte::after { content: ""; position: absolute; top: 0; width: 30%; height: 62px; background: #111; border-bottom: 3px solid #404040; }
|
.kutte::before, .kutte::after { content: ""; position: absolute; top: 0; width: 30%; height: 62px; background: #111; border-bottom: 3px solid #404040; }
|
||||||
.kutte::before { left: 0; border-radius: 24px 0 45% 0; }
|
.kutte::before { left: 0; border-radius: 24px 0 45% 0; }
|
||||||
.kutte::after { right: 0; border-radius: 0 24px 0 45%; }
|
.kutte::after { right: 0; border-radius: 0 24px 0 45%; }
|
||||||
.patch-grid { display: flex; flex-wrap: wrap; gap: 14px; justify-content: flex-start; align-items: flex-start; }
|
.patch-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); grid-auto-flow: row; gap: 14px; align-items: start; }
|
||||||
|
.patch-grid > p { grid-column: 1 / -1; }
|
||||||
|
.patch-button { display: flex; width: 100%; min-width: 0; justify-content: center; padding:0; border:0; background:transparent; color:inherit; cursor:pointer; }
|
||||||
.patch { padding: 5px; display: grid; place-items: center; text-align: center; background: #242424; border: 2px dashed #78716c; border-radius: 9px; color: #f8fafc; }
|
.patch { padding: 5px; display: grid; place-items: center; text-align: center; background: #242424; border: 2px dashed #78716c; border-radius: 9px; color: #f8fafc; }
|
||||||
.patch-image-frame { width: fit-content; height: fit-content; }
|
.patch-image-frame { width: 100%; height: 140px; }
|
||||||
.patch-icon-frame { width: 105px; height: 105px; }
|
.patch-icon-frame { width: 105px; height: 105px; }
|
||||||
.patch-icon { font-size: 2.5rem; }
|
.patch-icon { font-size: 2.5rem; }
|
||||||
.patch-image { width: auto; height: auto; max-width: 140px; max-height: 140px; object-fit: contain; display: block; }
|
.patch-image { width: 100%; height: 100%; max-width: 140px; max-height: 140px; object-fit: contain; display: block; }
|
||||||
.patch.locked { opacity: .38; filter: grayscale(1); }
|
.patch.locked { opacity: .38; filter: grayscale(1); }
|
||||||
.patch.earned { border: 0; background: transparent; box-shadow: 2px 3px 7px rgba(0,0,0,.7); }
|
.patch.earned { border: 0; background: transparent; box-shadow: 2px 3px 7px rgba(0,0,0,.7); }
|
||||||
.patch-button { padding:0; border:0; background:transparent; color:inherit; cursor:pointer; }
|
|
||||||
.patch-dialog { width:min(92vw,460px); padding:24px; color:#f8fafc; background:#171717; border:1px solid var(--border); border-radius:14px; box-shadow:0 20px 60px #000; }
|
.patch-dialog { width:min(92vw,460px); padding:24px; color:#f8fafc; background:#171717; border:1px solid var(--border); border-radius:14px; box-shadow:0 20px 60px #000; }
|
||||||
.patch-dialog::backdrop { background:rgba(0,0,0,.78); }
|
.patch-dialog::backdrop { background:rgba(0,0,0,.78); }
|
||||||
|
.patch-dialog-description { line-height:1.55; }
|
||||||
|
.patch-dialog-reason { padding-top:10px; border-top:1px solid var(--border); color:var(--muted); }
|
||||||
.patch-dialog-preview { max-width:150px; max-height:150px; object-fit:contain; }
|
.patch-dialog-preview { max-width:150px; max-height:150px; object-fit:contain; }
|
||||||
.patch-dialog-close { float:right; margin:0; }
|
.patch-dialog-close { float:right; margin:0; }
|
||||||
.profile-edit { margin-top: 24px; }
|
.profile-edit { margin-top: 24px; }
|
||||||
@@ -150,9 +153,9 @@
|
|||||||
<dialog class="patch-dialog" id="patch-{{ badge.code }}">
|
<dialog class="patch-dialog" id="patch-{{ badge.code }}">
|
||||||
<form method="dialog"><button class="button button-secondary patch-dialog-close" aria-label="{{ _('Schließen') }}">×</button></form>
|
<form method="dialog"><button class="button button-secondary patch-dialog-close" aria-label="{{ _('Schließen') }}">×</button></form>
|
||||||
{% if badge.image_path %}<img class="patch-dialog-preview" src="{{ badge.image_path }}" alt="Patch {{ badge.name | t }}">{% else %}<div class="patch-icon">{{ badge.icon }}</div>{% endif %}
|
{% if badge.image_path %}<img class="patch-dialog-preview" src="{{ badge.image_path }}" alt="Patch {{ badge.name | t }}">{% else %}<div class="patch-icon">{{ badge.icon }}</div>{% endif %}
|
||||||
<h3>{{ badge.name | t }}</h3><p>{{ badge.description | t }}</p>
|
<h3>{{ badge.name | t }}</h3><p class="patch-dialog-description">{{ badge.long_description | t }}</p>
|
||||||
<p><strong>{{ _('Erhalten am:') }}</strong> {{ badge.awarded_at }}</p>
|
<p><strong>{{ _('Erhalten am:') }}</strong> {{ badge.awarded_at }}</p>
|
||||||
{% if badge.trigger_concert %}<p><strong>{{ _('Ausgelöst durch:') }}</strong> <a href="/concerts/{{ badge.trigger_concert.id }}">{{ badge.trigger_concert.artist }} · {{ badge.trigger_concert.date }}</a></p>{% else %}<p><strong>{{ _('Grund:') }}</strong> {{ badge.description | t }}</p>{% endif %}
|
{% if badge.trigger_concert %}<p class="patch-dialog-reason"><strong>{{ _('Ausgelöst durch:') }}</strong> <a href="/concerts/{{ badge.trigger_concert.id }}">{{ badge.trigger_concert.artist }} · {{ badge.trigger_concert.date }}</a></p>{% else %}<p class="patch-dialog-reason"><strong>{{ _('Grund:') }}</strong> {{ badge.description | t }}</p>{% endif %}
|
||||||
</dialog>
|
</dialog>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
+23
-3
@@ -108,17 +108,32 @@ class TranslationTests(unittest.TestCase):
|
|||||||
self.assertIn('My profile', page)
|
self.assertIn('My profile', page)
|
||||||
self.assertIn('new message(s) or request(s)', page)
|
self.assertIn('new message(s) or request(s)', page)
|
||||||
|
|
||||||
|
def test_every_patch_has_a_long_description_with_an_english_translation(self):
|
||||||
|
codes = {definition[0] for definition in main.BADGE_DEFINITIONS}
|
||||||
|
self.assertEqual(set(main.BADGE_LONG_DESCRIPTIONS), codes)
|
||||||
|
current_language.set('en')
|
||||||
|
for description in main.BADGE_LONG_DESCRIPTIONS.values():
|
||||||
|
with self.subTest(description=description):
|
||||||
|
self.assertNotEqual(main._(description), description)
|
||||||
|
|
||||||
def test_every_page_renders_with_populated_content_in_both_languages(self):
|
def test_every_page_renders_with_populated_content_in_both_languages(self):
|
||||||
from jinja2 import ChainableUndefined
|
from jinja2 import ChainableUndefined
|
||||||
import os
|
import os
|
||||||
env = main.templates.overlay(undefined=ChainableUndefined)
|
env = main.templates.overlay(undefined=ChainableUndefined)
|
||||||
member = dict(id=1, username='sample', display_name='Nachrichten <test>', email='sample@example.invalid', is_admin=True, notification_count=2, created_at='14.09.2026')
|
member = dict(id=1, username='sample', display_name='Nachrichten <test>', email='sample@example.invalid', is_admin=True, notification_count=2, created_at='14.09.2026')
|
||||||
event = dict(id=1, concert_id=1, artist='Nachrichten <test>', title='Nachrichten <test>', venue='Venue', city='Hagen', country='Deutschland', date='14.09.2026', time='20:00', event_type='concert', event_type_label='Konzert', visibility='friends', is_past=True, is_verified=True, concert_count=2, name='Venue', rating=4, favorite_song='Nachrichten <test>', notes='Nachrichten <test>', photos=[dict(id=1, path='/photo.jpg')], bands=[])
|
event = dict(id=1, concert_id=1, artist='Nachrichten <test>', title='Nachrichten <test>', venue='Venue', city='Hagen', country='Deutschland', date='14.09.2026', time='20:00', event_type='concert', event_type_label='Konzert', visibility='friends', is_past=True, is_verified=True, concert_count=2, name='Venue', rating=4, favorite_song='Nachrichten <test>', notes='Nachrichten <test>', photos=[dict(id=1, path='/photo.jpg')], bands=[])
|
||||||
badge = dict(code='founder', name='Gründer', description='Von Anfang an dabei und MetalCircle aufgebaut', earned=True, icon='⚔️', image_path='/patch.jpg')
|
badge = dict(code='founder', name='Gründer', description='Von Anfang an dabei und MetalCircle aufgebaut', long_description=main.BADGE_LONG_DESCRIPTIONS['founder'], earned=True, icon='⚔️', image_path='/patch.jpg')
|
||||||
|
profile_badges = [badge]
|
||||||
|
for code in ('alpha_tester', 'beta_tester', 'early_bird'):
|
||||||
|
name, icon, _threshold, reason, _category = main.BADGE_BY_CODE[code]
|
||||||
|
profile_badges.append(dict(code=code, name=name, description=reason,
|
||||||
|
long_description=main.BADGE_LONG_DESCRIPTIONS[code],
|
||||||
|
earned=True, icon=icon, image_path='/patch.jpg',
|
||||||
|
awarded_at='14.09.2026', trigger_concert=None))
|
||||||
event['badges'] = [badge]
|
event['badges'] = [badge]
|
||||||
event['linked_events'] = []
|
event['linked_events'] = []
|
||||||
profile = dict(member, attended_concerts=1)
|
profile = dict(member, attended_concerts=1, earned_codes={badge['code'] for badge in profile_badges})
|
||||||
context = dict(user=member, users=[member], managed_user=member, concert=event, profile=profile, filters={}, stats={}, connections=dict(friends=[member], incoming=[member], outgoing=[member], blocked=[member]), badges=[badge], patch_groups=[dict(label='⚔️ Spezial-Patches', patches=[badge])], venues=[event, dict(event, id=2)], entries=[event], upcoming_concerts=[event], past_concerts=[event], events=[event], followed_bands=[dict(name='Nachrichten <test>', key='test')], followed_venues=[event], linkable_events=[event], invitable_users=[member], invited_user_ids=[1], attending_users=[member], maybe_users=[member], ticket_seekers=[member], ticket_offers=[member], attending_count=2, maybe_count=1, ticket_seeker_count=1, ticket_offer_count=1, comments=[dict(member, body='Nachrichten <test>')], is_own_profile=True, can_view_details=True, can_edit_title=True, can_edit_details=True, can_manage_access=True, can_delete=True, can_edit=True, can_manage=True, user_results=[member], friend_requests=[member], event_invitations=[dict(event, invited_by='Nachrichten <test>')], conversations=[dict(member, unread_count=1, last_message='Nachrichten <test>')], messages=[dict(body='Nachrichten <test>')])
|
context = dict(user=member, users=[member], managed_user=member, concert=event, profile=profile, filters={}, stats={}, connections=dict(friends=[member], incoming=[member], outgoing=[member], blocked=[member]), badges=profile_badges, patch_groups=[dict(label='⚔️ Spezial-Patches', patches=[badge])], venues=[event, dict(event, id=2)], entries=[event], upcoming_concerts=[event], past_concerts=[event], events=[event], followed_bands=[dict(name='Nachrichten <test>', key='test')], followed_venues=[event], linkable_events=[event], invitable_users=[member], invited_user_ids=[1], attending_users=[member], maybe_users=[member], ticket_seekers=[member], ticket_offers=[member], attending_count=2, maybe_count=1, ticket_seeker_count=1, ticket_offer_count=1, comments=[dict(member, body='Nachrichten <test>')], is_own_profile=True, can_view_details=True, can_edit_title=True, can_edit_details=True, can_manage_access=True, can_delete=True, can_edit=True, can_manage=True, user_results=[member], friend_requests=[member], event_invitations=[dict(event, invited_by='Nachrichten <test>')], conversations=[dict(member, unread_count=1, last_message='Nachrichten <test>')], messages=[dict(body='Nachrichten <test>')])
|
||||||
for language in ('de', 'en'):
|
for language in ('de', 'en'):
|
||||||
current_language.set(language)
|
current_language.set(language)
|
||||||
for path in Path(main.BASE_DIR, 'templates').glob('*.html'):
|
for path in Path(main.BASE_DIR, 'templates').glob('*.html'):
|
||||||
@@ -128,6 +143,11 @@ class TranslationTests(unittest.TestCase):
|
|||||||
if not path.name.startswith('_'):
|
if not path.name.startswith('_'):
|
||||||
self.assertIn(f'<html lang="{language}">', page)
|
self.assertIn(f'<html lang="{language}">', page)
|
||||||
self.assertIn('class="language-switch"', page)
|
self.assertIn('class="language-switch"', page)
|
||||||
|
if path.name == 'profile.html':
|
||||||
|
self.assertIn('grid-template-columns: repeat(2, minmax(0, 1fr))', page)
|
||||||
|
self.assertIn('This patch recognizes you as part of the first crew' if language == 'en' else 'Dieser Patch würdigt dich als Teil der ersten Crew', page)
|
||||||
|
self.assertIn('Reason:' if language == 'en' else 'Grund:', page)
|
||||||
|
self.assertEqual(page.count('class="patch-button"'), len(profile_badges))
|
||||||
if os.environ.get('I18N_RENDER_DIR'):
|
if os.environ.get('I18N_RENDER_DIR'):
|
||||||
target = Path(os.environ['I18N_RENDER_DIR'], language)
|
target = Path(os.environ['I18N_RENDER_DIR'], language)
|
||||||
target.mkdir(parents=True, exist_ok=True)
|
target.mkdir(parents=True, exist_ok=True)
|
||||||
|
|||||||
Reference in New Issue
Block a user