457 lines
12 KiB
Rust
457 lines
12 KiB
Rust
use rocket::serde::{Deserialize, Serialize};
|
|
use serde_json::Value;
|
|
use sqlx::FromRow;
|
|
use std::collections::HashMap;
|
|
use std::collections::HashSet;
|
|
|
|
pub const ATTENDANCE_ID_FIELD_ID: &str = "attendance-id";
|
|
pub const ATTENDANCE_ID_FIELD_LABEL: &str = "Deltagar-ID";
|
|
pub const ATTENDANCE_ID_FIELD_PLACEHOLDER: &str = "Ange ditt deltagar-ID från närvarolistan";
|
|
|
|
#[derive(Debug, FromRow, Clone)]
|
|
pub struct TournamentInfo {
|
|
pub id: i32,
|
|
pub title: String,
|
|
pub game: String,
|
|
pub slug: String,
|
|
pub tagline: Option<String>,
|
|
pub start_at: Option<chrono::DateTime<chrono::Utc>>,
|
|
pub location: Option<String>,
|
|
pub description: Option<String>,
|
|
pub contact: Option<String>,
|
|
pub signup_mode: String,
|
|
pub team_size_min: i32,
|
|
pub team_size_max: i32,
|
|
pub created_at: chrono::DateTime<chrono::Utc>,
|
|
pub updated_at: chrono::DateTime<chrono::Utc>,
|
|
}
|
|
|
|
#[derive(Debug, FromRow, Clone)]
|
|
pub struct TournamentSectionRecord {
|
|
pub id: i32,
|
|
pub tournament_id: i32,
|
|
pub position: i32,
|
|
pub title: String,
|
|
pub body: String,
|
|
}
|
|
|
|
#[derive(Debug, FromRow, Clone)]
|
|
pub struct TournamentSignupFieldRecord {
|
|
pub id: i32,
|
|
pub tournament_id: i32,
|
|
pub field_key: String,
|
|
pub scope: String,
|
|
pub label: String,
|
|
pub field_type: String,
|
|
pub required: bool,
|
|
pub placeholder: Option<String>,
|
|
pub position: i32,
|
|
#[sqlx(rename = "unique_field")]
|
|
pub unique: bool,
|
|
}
|
|
|
|
#[derive(Debug, FromRow, Clone)]
|
|
pub struct TournamentRegistrationRow {
|
|
pub id: i32,
|
|
pub tournament_id: i32,
|
|
pub entry_label: Option<String>,
|
|
pub created_at: chrono::DateTime<chrono::Utc>,
|
|
}
|
|
|
|
#[derive(Debug, FromRow, Clone)]
|
|
pub struct TournamentRegistrationValueRow {
|
|
pub registration_id: i32,
|
|
pub signup_field_id: i32,
|
|
pub value: String,
|
|
}
|
|
|
|
#[derive(Debug, FromRow, Clone)]
|
|
pub struct TournamentParticipantRow {
|
|
pub id: i32,
|
|
pub registration_id: i32,
|
|
pub position: i32,
|
|
}
|
|
|
|
#[derive(Debug, FromRow, Clone)]
|
|
pub struct TournamentParticipantValueRow {
|
|
pub participant_id: i32,
|
|
pub signup_field_id: i32,
|
|
pub value: String,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
|
|
#[serde(rename_all = "snake_case", crate = "rocket::serde")]
|
|
pub enum TournamentFieldType {
|
|
Text,
|
|
Email,
|
|
Tel,
|
|
Discord,
|
|
}
|
|
|
|
impl Default for TournamentFieldType {
|
|
fn default() -> Self {
|
|
TournamentFieldType::Text
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
#[serde(crate = "rocket::serde")]
|
|
pub struct TournamentSignupField {
|
|
pub id: String,
|
|
pub label: String,
|
|
#[serde(default)]
|
|
pub field_type: TournamentFieldType,
|
|
#[serde(default)]
|
|
pub required: bool,
|
|
#[serde(default)]
|
|
pub placeholder: Option<String>,
|
|
#[serde(default)]
|
|
pub unique: bool,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
#[serde(crate = "rocket::serde")]
|
|
pub struct TournamentTeamSize {
|
|
pub min: i32,
|
|
pub max: i32,
|
|
}
|
|
|
|
impl Default for TournamentTeamSize {
|
|
fn default() -> Self {
|
|
TournamentTeamSize { min: 1, max: 1 }
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
#[serde(crate = "rocket::serde")]
|
|
pub struct TournamentSignupConfig {
|
|
#[serde(default = "TournamentSignupConfig::default_mode")]
|
|
pub mode: String,
|
|
#[serde(default = "TournamentSignupConfig::default_team_size")]
|
|
pub team_size: TournamentTeamSize,
|
|
#[serde(default)]
|
|
pub entry_fields: Vec<TournamentSignupField>,
|
|
#[serde(default)]
|
|
pub participant_fields: Vec<TournamentSignupField>,
|
|
}
|
|
|
|
impl Default for TournamentSignupConfig {
|
|
fn default() -> Self {
|
|
TournamentSignupConfig {
|
|
mode: Self::default_mode(),
|
|
team_size: Self::default_team_size(),
|
|
entry_fields: Vec::new(),
|
|
participant_fields: Vec::new(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl TournamentSignupConfig {
|
|
fn default_mode() -> String {
|
|
"solo".to_string()
|
|
}
|
|
|
|
fn default_team_size() -> TournamentTeamSize {
|
|
TournamentTeamSize::default()
|
|
}
|
|
|
|
pub fn normalized(mut self) -> Self {
|
|
self.mode = match self.mode.as_str() {
|
|
"team" => "team".to_string(),
|
|
_ => "solo".to_string(),
|
|
};
|
|
|
|
if self.mode == "solo" {
|
|
self.team_size.min = 1;
|
|
self.team_size.max = 1;
|
|
} else {
|
|
if self.team_size.min < 1 {
|
|
self.team_size.min = 1;
|
|
}
|
|
if self.team_size.max < self.team_size.min {
|
|
self.team_size.max = self.team_size.min;
|
|
}
|
|
if self.team_size.max > 64 {
|
|
self.team_size.max = 64;
|
|
}
|
|
}
|
|
|
|
self.entry_fields = normalize_signup_fields(self.entry_fields);
|
|
self.participant_fields = normalize_signup_fields(self.participant_fields);
|
|
ensure_attendance_field_for_mode(&mut self);
|
|
|
|
self
|
|
}
|
|
}
|
|
|
|
fn normalize_signup_fields(mut fields: Vec<TournamentSignupField>) -> Vec<TournamentSignupField> {
|
|
let mut seen = HashSet::new();
|
|
|
|
for field in fields.iter_mut() {
|
|
let base = if field.id.trim().is_empty() {
|
|
normalize_field_id(&field.label)
|
|
} else {
|
|
normalize_field_id(&field.id)
|
|
};
|
|
|
|
let mut candidate = base.clone();
|
|
let mut counter = 1;
|
|
while seen.contains(&candidate) {
|
|
counter += 1;
|
|
candidate = format!("{base}-{counter}");
|
|
}
|
|
seen.insert(candidate.clone());
|
|
field.id = candidate;
|
|
|
|
field.label = field.label.trim().to_string();
|
|
if field.label.is_empty() {
|
|
field.label = "Fält".to_string();
|
|
}
|
|
|
|
field.placeholder = field
|
|
.placeholder
|
|
.as_ref()
|
|
.map(|value| value.trim().to_string())
|
|
.filter(|value| !value.is_empty());
|
|
}
|
|
|
|
fields
|
|
}
|
|
|
|
fn remove_attendance_id_field(
|
|
fields: &mut Vec<TournamentSignupField>,
|
|
) -> Option<TournamentSignupField> {
|
|
let mut attendance_index = None;
|
|
for (index, field) in fields.iter().enumerate() {
|
|
if field.id == ATTENDANCE_ID_FIELD_ID {
|
|
attendance_index = Some(index);
|
|
break;
|
|
}
|
|
}
|
|
|
|
attendance_index.map(|index| {
|
|
let mut field = fields.remove(index);
|
|
sanitize_attendance_id_field(&mut field);
|
|
field
|
|
})
|
|
}
|
|
|
|
fn insert_attendance_field_front(
|
|
fields: &mut Vec<TournamentSignupField>,
|
|
mut field: TournamentSignupField,
|
|
) {
|
|
sanitize_attendance_id_field(&mut field);
|
|
fields.insert(0, field);
|
|
}
|
|
|
|
fn ensure_attendance_field_for_mode(config: &mut TournamentSignupConfig) {
|
|
let mut attendance = remove_attendance_id_field(&mut config.entry_fields)
|
|
.or_else(|| remove_attendance_id_field(&mut config.participant_fields))
|
|
.unwrap_or_else(default_attendance_id_field);
|
|
|
|
if config.mode == "team" {
|
|
config
|
|
.entry_fields
|
|
.retain(|field| field.id != ATTENDANCE_ID_FIELD_ID);
|
|
config
|
|
.participant_fields
|
|
.retain(|field| field.id != ATTENDANCE_ID_FIELD_ID);
|
|
insert_attendance_field_front(&mut config.participant_fields, attendance);
|
|
} else {
|
|
config
|
|
.entry_fields
|
|
.retain(|field| field.id != ATTENDANCE_ID_FIELD_ID);
|
|
config
|
|
.participant_fields
|
|
.retain(|field| field.id != ATTENDANCE_ID_FIELD_ID);
|
|
insert_attendance_field_front(&mut config.entry_fields, attendance);
|
|
}
|
|
}
|
|
|
|
fn sanitize_attendance_id_field(field: &mut TournamentSignupField) {
|
|
field.id = ATTENDANCE_ID_FIELD_ID.to_string();
|
|
field.label = sanitize_attendance_label(&field.label);
|
|
field.field_type = TournamentFieldType::Text;
|
|
field.required = true;
|
|
field.unique = true;
|
|
field.placeholder = Some(
|
|
field
|
|
.placeholder
|
|
.as_ref()
|
|
.map(|value| value.trim().to_string())
|
|
.filter(|value| !value.is_empty())
|
|
.unwrap_or_else(|| ATTENDANCE_ID_FIELD_PLACEHOLDER.to_string()),
|
|
);
|
|
}
|
|
|
|
fn sanitize_attendance_label(label: &str) -> String {
|
|
let trimmed = label.trim();
|
|
if trimmed.is_empty() {
|
|
ATTENDANCE_ID_FIELD_LABEL.to_string()
|
|
} else {
|
|
trimmed.to_string()
|
|
}
|
|
}
|
|
|
|
fn default_attendance_id_field() -> TournamentSignupField {
|
|
TournamentSignupField {
|
|
id: ATTENDANCE_ID_FIELD_ID.to_string(),
|
|
label: ATTENDANCE_ID_FIELD_LABEL.to_string(),
|
|
field_type: TournamentFieldType::Text,
|
|
required: true,
|
|
placeholder: Some(ATTENDANCE_ID_FIELD_PLACEHOLDER.to_string()),
|
|
unique: true,
|
|
}
|
|
}
|
|
|
|
fn normalize_field_id(input: &str) -> String {
|
|
let mut slug = input
|
|
.trim()
|
|
.to_lowercase()
|
|
.chars()
|
|
.map(|ch| match ch {
|
|
'a'..='z' | '0'..='9' => ch,
|
|
_ => '-',
|
|
})
|
|
.collect::<String>();
|
|
|
|
while slug.contains("--") {
|
|
slug = slug.replace("--", "-");
|
|
}
|
|
|
|
slug = slug.trim_matches('-').to_string();
|
|
if slug.is_empty() {
|
|
"field".to_string()
|
|
} else {
|
|
slug
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
#[serde(crate = "rocket::serde")]
|
|
pub struct TournamentSection {
|
|
pub title: String,
|
|
pub body: String,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Clone)]
|
|
#[serde(crate = "rocket::serde")]
|
|
pub struct TournamentInfoData {
|
|
pub id: i32,
|
|
pub title: String,
|
|
pub game: String,
|
|
pub slug: String,
|
|
pub tagline: Option<String>,
|
|
pub start_at: Option<chrono::DateTime<chrono::Utc>>,
|
|
pub location: Option<String>,
|
|
pub description: Option<String>,
|
|
pub contact: Option<String>,
|
|
pub registration_url: String,
|
|
#[serde(default)]
|
|
pub sections: Vec<TournamentSection>,
|
|
#[serde(default)]
|
|
pub signup_config: TournamentSignupConfig,
|
|
#[serde(default)]
|
|
pub total_registrations: i32,
|
|
#[serde(default)]
|
|
pub total_participants: i32,
|
|
pub updated_at: chrono::DateTime<chrono::Utc>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Clone)]
|
|
#[serde(crate = "rocket::serde")]
|
|
pub struct TournamentListResponse {
|
|
pub tournaments: Vec<TournamentInfoData>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Clone)]
|
|
#[serde(crate = "rocket::serde")]
|
|
pub struct TournamentItemResponse {
|
|
pub tournament: TournamentInfoData,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
#[serde(crate = "rocket::serde")]
|
|
pub struct CreateTournamentRequest {
|
|
pub title: String,
|
|
pub game: String,
|
|
pub slug: String,
|
|
#[serde(default)]
|
|
pub tagline: Option<String>,
|
|
#[serde(default)]
|
|
pub start_at: Option<chrono::DateTime<chrono::Utc>>,
|
|
#[serde(default)]
|
|
pub location: Option<String>,
|
|
#[serde(default)]
|
|
pub description: Option<String>,
|
|
#[serde(default)]
|
|
pub registration_url: Option<String>,
|
|
#[serde(default)]
|
|
pub contact: Option<String>,
|
|
#[serde(default)]
|
|
pub sections: Vec<TournamentSection>,
|
|
#[serde(default)]
|
|
pub signup_config: TournamentSignupConfig,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
#[serde(crate = "rocket::serde")]
|
|
pub struct UpdateTournamentRequest {
|
|
pub title: String,
|
|
pub game: String,
|
|
pub slug: String,
|
|
#[serde(default)]
|
|
pub tagline: Option<String>,
|
|
#[serde(default)]
|
|
pub start_at: Option<chrono::DateTime<chrono::Utc>>,
|
|
#[serde(default)]
|
|
pub location: Option<String>,
|
|
#[serde(default)]
|
|
pub description: Option<String>,
|
|
#[serde(default)]
|
|
pub registration_url: Option<String>,
|
|
#[serde(default)]
|
|
pub contact: Option<String>,
|
|
#[serde(default)]
|
|
pub sections: Vec<TournamentSection>,
|
|
#[serde(default)]
|
|
pub signup_config: TournamentSignupConfig,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Clone)]
|
|
#[serde(crate = "rocket::serde")]
|
|
pub struct TournamentRegistrationResponse {
|
|
pub registration_id: i32,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize, Clone)]
|
|
#[serde(crate = "rocket::serde")]
|
|
pub struct TournamentSignupSubmission {
|
|
#[serde(default)]
|
|
pub entry: HashMap<String, String>,
|
|
#[serde(default)]
|
|
pub participants: Vec<HashMap<String, String>>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Clone)]
|
|
#[serde(crate = "rocket::serde")]
|
|
pub struct TournamentRegistrationItem {
|
|
pub id: i32,
|
|
pub created_at: chrono::DateTime<chrono::Utc>,
|
|
pub entry: Value,
|
|
pub participants: Value,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Clone)]
|
|
#[serde(crate = "rocket::serde")]
|
|
pub struct TournamentRegistrationListResponse {
|
|
pub tournament: TournamentInfoData,
|
|
pub registrations: Vec<TournamentRegistrationItem>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Clone)]
|
|
#[serde(crate = "rocket::serde")]
|
|
pub struct TournamentRegistrationDetailResponse {
|
|
pub tournament: TournamentInfoData,
|
|
pub registration: TournamentRegistrationItem,
|
|
}
|