Files
paul a3546b4c01 Security hardening: env-based config, CSRF protection, secure code generation, plus presenter management and email communications system
- Move all credentials from hardcoded to .env with python-dotenv (add .env.example template and .gitignore)
- Add CSRF token generation and validation on all state-changing requests
- Replace random.choices with secrets.choice for all secure code generation
- Add session cookie security headers (HttpOnly, Secure, SameSite)
- Add presenter management: assign/remove presenters to breakout sessions, presenter QR scanning
- Add email communications system: templates per event, custom email sending, sent email tracking
- Add staff/organizer profile pages with staff code display
- New DB tables: event_email_templates, sent_emails, user_communications
- New DB columns: staff.reminder_count/reminder_dates, breakout_session_organizers.presenter_code
- Expand English translation strings across all new features

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-26 20:34:48 +00:00

95 lines
4.3 KiB
HTML

{% extends "base.html" %}
{% block title %}{{ 'badges'|t }} - {{ event.name }}{% endblock %}
{% block content %}
<div class="badges-page">
<div class="badges-header">
<h1>{{ event.name }}</h1>
<p>{{ 'attendee_badges'|t }}</p>
<div class="header-actions">
<a href="{{ url_for('event_scan', event_id=event.id) }}" class="btn btn-secondary"><i data-lucide="scan-line"></i> {{ 'scan_qr_codes'|t }}</a>
<a href="{{ url_for('download_rectangular_badges', event_id=event.id) }}" class="btn btn-secondary"><i data-lucide="tag"></i> Rectangular Labels (80x50mm)</a>
<a href="{{ url_for('download_attendees_excel', event_id=event.id) }}" class="btn btn-secondary"><i data-lucide="download"></i> Download Excel</a>
<button onclick="window.print()" class="btn btn-primary"><i data-lucide="printer"></i> {{ 'print_badges'|t }}</button>
</div>
</div>
<div class="badges-grid">
{% for attendee in attendees %}
<div class="badge-card" data-first="{{ attendee.first_name }}" data-last="{{ attendee.last_name }}">
<div class="badge-header">
<h3 class="badge-name">
<span class="first-name">{{ attendee.first_name }}</span>
<span class="last-name">{{ attendee.last_name }}</span>
</h3>
</div>
<div class="badge-qr">
<img src="{{ attendee.qr_code }}" alt="QR Code" width="100" height="100">
</div>
<div class="badge-body">
<p class="badge-org">{{ attendee.organisation if attendee.organisation else '' }}</p>
<p class="badge-role">{{ attendee.role if attendee.role else '' }}</p>
{% if attendee.introduction %}
<p class="badge-intro">{{ attendee.introduction[:80] }}{% if attendee.introduction|length > 80 %}...{% endif %}</p>
{% endif %}
</div>
<div class="badge-footer">
<span class="badge-id">#{{ attendee.id }}</span>
<span class="badge-check {% if attendee.checked_in %}checked-in{% endif %}">
{% if attendee.checked_in %}<i data-lucide="check"></i> {{ 'checked_in'|t }}{% else %}{{ 'pending'|t }}{% endif %}
</span>
</div>
<div class="badge-event-bar">
{{ event.name }} — {{ event.start_time|localized_date if event.start_time else 'TBD' }}
</div>
</div>
{% endfor %}
</div>
<script>
(function() {
// Create offscreen canvas for text measurement
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
ctx.font = '30pt sans-serif';
// Container width limit (badge width)
const BADGE_WIDTH_LIMIT = 280;
function measureText(text, fontSize, fontFamily) {
ctx.font = fontSize + 'px ' + (fontFamily || 'sans-serif');
return ctx.measureText(text).width;
}
function formatBadgeNames() {
const cards = document.querySelectorAll('.badge-card');
cards.forEach(card => {
const firstName = card.dataset.first || '';
const lastName = card.dataset.last || '';
const firstSpan = card.querySelector('.first-name');
const lastSpan = card.querySelector('.last-name');
// Measure first + last name at 20pt
const combinedWidth = measureText(firstName + ' ' + lastName, 30);
// If combined width exceeds limit, insert br before last name
if (combinedWidth > BADGE_WIDTH_LIMIT) {
if (!lastSpan.previousElementSibling || lastSpan.previousElementSibling.tagName !== 'BR') {
const br = document.createElement('br');
lastSpan.parentNode.insertBefore(br, lastSpan);
}
}
});
}
// Run after DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', formatBadgeNames);
} else {
formatBadgeNames();
}
})();
</script>
</div>
{% endblock %}