feat: Enhance hackathon submission and participant management
- Updated HackathonSubmissionsSchema to use Option types for team_id, project_name, description, technologies, submission_status, and submitted_at. - Modified seed_hackathons and seed_test_submission scripts to accommodate new optional fields. - Added routes for participant registration and listing in hackathon_controller. - Implemented register_participant and list_participants functions in hackathon_controller. - Introduced HackathonParticipantSchema and corresponding DTOs for participant management. - Enhanced HackathonRepository with CRUD operations for hackathon participants. - Updated HackathonService to include methods for participant registration and listing. - Refactored TeamsService to allow admin-level updates and invitations, bypassing leader-only restrictions. - Added validation for member emails in TeamsCreateRequestDto and TeamInviteRequestDto.
This commit is contained in:
@@ -648,4 +648,39 @@ pub fn hackathon_routes() -> Router {
|
||||
.route("/submissions/{id}", put(update_hackathon_submission))
|
||||
.route("/submissions/{id}/submit", post(submit_hackathon_submission))
|
||||
.route("/submissions/{id}", delete(delete_hackathon_submission))
|
||||
// Participants
|
||||
.route("/{id}/participants", post(register_participant))
|
||||
.route("/{id}/participants", get(list_participants))
|
||||
}
|
||||
|
||||
use super::hackathon_dto::RegisterParticipantRequestDto;
|
||||
|
||||
// Register a participant for a hackathon (persistent)
|
||||
pub async fn register_participant(
|
||||
Extension(state): Extension<AppState>,
|
||||
Path(hackathon_id): Path<String>,
|
||||
Json(payload): Json<RegisterParticipantRequestDto>,
|
||||
) -> impl IntoResponse {
|
||||
match HackathonService::register_participant(hackathon_id, payload, &state).await {
|
||||
Ok(response) => {
|
||||
let body = serde_json::json!({ "message": "Participant registered", "data": response.data });
|
||||
(axum::http::StatusCode::OK, Json(body)).into_response()
|
||||
}
|
||||
Err(error) => (StatusCode::from_u16(error.status).unwrap(), Json(error)).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
// List participants for a hackathon
|
||||
pub async fn list_participants(
|
||||
Extension(state): Extension<AppState>,
|
||||
Path(hackathon_id): Path<String>,
|
||||
Query(meta): Query<imphnen_libs::MetaRequestDto>,
|
||||
) -> impl IntoResponse {
|
||||
match HackathonService::list_participants(meta, hackathon_id, &state).await {
|
||||
Ok(response) => {
|
||||
let body = serde_json::json!({ "message": "Success", "data": response.data, "meta": response.meta });
|
||||
(axum::http::StatusCode::OK, Json(body)).into_response()
|
||||
}
|
||||
Err(error) => (StatusCode::from_u16(error.status).unwrap(), Json(error)).into_response(),
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ use crate::v1::hackathon::hackathon_schema::{
|
||||
HackathonEventType, HackathonEventsSchema, HackathonPhase, HackathonSchema,
|
||||
HackathonStatus, HackathonSubmissionsSchema, HackathonTimelineSchema,
|
||||
SubmissionStatus,
|
||||
HackathonParticipantSchema,
|
||||
};
|
||||
|
||||
// Hackathon DTOs
|
||||
@@ -410,16 +411,46 @@ impl From<HackathonSubmissionsSchema> for HackathonSubmissionDto {
|
||||
Self {
|
||||
id: schema.id.id.to_raw(),
|
||||
hackathon_id: schema.hackathon_id.id.to_raw(),
|
||||
team_id: schema.team_id.id.to_raw(),
|
||||
project_name: schema.project_name,
|
||||
description: schema.description,
|
||||
team_id: schema.team_id.map(|t| t.id.to_raw()).unwrap_or_default(),
|
||||
project_name: schema.project_name.unwrap_or_default(),
|
||||
description: schema.description.unwrap_or_default(),
|
||||
repository_url: schema.repository_url,
|
||||
demo_url: schema.demo_url,
|
||||
slides_url: schema.slides_url,
|
||||
technologies: schema.technologies,
|
||||
submission_status: schema.submission_status,
|
||||
technologies: schema.technologies.unwrap_or_default(),
|
||||
submission_status: schema.submission_status.unwrap_or(super::hackathon_schema::SubmissionStatus::Draft),
|
||||
judge_feedback: schema.judge_feedback,
|
||||
submitted_at: schema.submitted_at,
|
||||
submitted_at: schema.submitted_at.unwrap_or(chrono::Utc::now()),
|
||||
is_deleted: schema.is_deleted,
|
||||
created_at: schema.created_at,
|
||||
updated_at: schema.updated_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Hackathon Participant DTOs
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||
pub struct RegisterParticipantRequestDto {
|
||||
#[validate(length(min = 1, message = "user_id cannot be empty"))]
|
||||
pub user_id: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct HackathonParticipantDto {
|
||||
pub id: String,
|
||||
pub hackathon_id: String,
|
||||
pub user_id: String,
|
||||
pub is_deleted: bool,
|
||||
pub created_at: Option<String>,
|
||||
pub updated_at: Option<String>,
|
||||
}
|
||||
|
||||
impl From<HackathonParticipantSchema> for HackathonParticipantDto {
|
||||
fn from(schema: HackathonParticipantSchema) -> Self {
|
||||
Self {
|
||||
id: schema.id.id.to_raw(),
|
||||
hackathon_id: schema.hackathon_id.id.to_raw(),
|
||||
user_id: schema.user_id,
|
||||
is_deleted: schema.is_deleted,
|
||||
created_at: schema.created_at,
|
||||
updated_at: schema.updated_at,
|
||||
|
||||
@@ -524,16 +524,16 @@ impl<'a> HackathonRepository<'a> {
|
||||
let schema = HackathonSubmissionsSchema {
|
||||
id: Thing::from((table.clone(), id.clone())),
|
||||
hackathon_id: Thing::from(("app_hackathons".to_string(), normalized_hackathon_id)),
|
||||
team_id: Thing::from(("app_teams".to_string(), normalized_team_id)),
|
||||
project_name: submission.project_name,
|
||||
description: submission.description,
|
||||
team_id: Some(Thing::from(("app_teams".to_string(), normalized_team_id))),
|
||||
project_name: Some(submission.project_name),
|
||||
description: Some(submission.description),
|
||||
repository_url: submission.repository_url,
|
||||
demo_url: submission.demo_url,
|
||||
slides_url: submission.slides_url,
|
||||
technologies: submission.technologies,
|
||||
submission_status: super::hackathon_schema::SubmissionStatus::Draft,
|
||||
technologies: Some(submission.technologies),
|
||||
submission_status: Some(super::hackathon_schema::SubmissionStatus::Draft),
|
||||
judge_feedback: None,
|
||||
submitted_at: chrono::Utc::now(),
|
||||
submitted_at: Some(chrono::Utc::now()),
|
||||
is_deleted: false,
|
||||
created_at: Some(get_iso_date()),
|
||||
updated_at: Some(get_iso_date()),
|
||||
@@ -560,6 +560,12 @@ impl<'a> HackathonRepository<'a> {
|
||||
|
||||
let builder = QueryListBuilder::new(&self.state.surrealdb_ws, &table, &meta)
|
||||
.with_condition("is_deleted = false")
|
||||
// Some stray records (from earlier bugs) may lack team_id; ensure we only fetch proper submissions
|
||||
.with_condition("team_id IS NOT NULL")
|
||||
// Ensure required string fields exist to prevent deserialization errors
|
||||
.with_condition("project_name IS NOT NULL")
|
||||
.with_condition("description IS NOT NULL")
|
||||
.with_condition("technologies IS NOT NULL")
|
||||
.with_condition(&format!("hackathon_id = type::thing('app_hackathons', '{}')", normalized_hackathon_id))
|
||||
.search_field("project_name")
|
||||
.select_fields(vec!["*"]);
|
||||
@@ -578,6 +584,12 @@ impl<'a> HackathonRepository<'a> {
|
||||
|
||||
let builder = QueryListBuilder::new(&self.state.surrealdb_ws, &table, &meta)
|
||||
.with_condition("is_deleted = false")
|
||||
// Ensure we don't deserialize records without a team_id
|
||||
.with_condition("team_id IS NOT NULL")
|
||||
// Ensure required string fields exist to prevent deserialization errors
|
||||
.with_condition("project_name IS NOT NULL")
|
||||
.with_condition("description IS NOT NULL")
|
||||
.with_condition("technologies IS NOT NULL")
|
||||
.with_condition(&format!("team_id = type::thing('app_teams', '{}')", normalized_team_id))
|
||||
.search_field("project_name")
|
||||
.select_fields(vec!["*"]);
|
||||
@@ -598,7 +610,7 @@ impl<'a> HackathonRepository<'a> {
|
||||
bail!("Submission not found");
|
||||
}
|
||||
|
||||
existing.submission_status = status;
|
||||
existing.submission_status = Some(status);
|
||||
existing.judge_feedback = feedback;
|
||||
existing.updated_at = Some(get_iso_date());
|
||||
|
||||
@@ -627,10 +639,10 @@ impl<'a> HackathonRepository<'a> {
|
||||
|
||||
// Apply updates
|
||||
if let Some(project_name) = updates.project_name {
|
||||
existing.project_name = project_name;
|
||||
existing.project_name = Some(project_name);
|
||||
}
|
||||
if let Some(description) = updates.description {
|
||||
existing.description = description;
|
||||
existing.description = Some(description);
|
||||
}
|
||||
if let Some(repository_url) = updates.repository_url {
|
||||
existing.repository_url = Some(repository_url);
|
||||
@@ -642,7 +654,7 @@ impl<'a> HackathonRepository<'a> {
|
||||
existing.slides_url = Some(slides_url);
|
||||
}
|
||||
if let Some(technologies) = updates.technologies {
|
||||
existing.technologies = technologies;
|
||||
existing.technologies = Some(technologies);
|
||||
}
|
||||
|
||||
existing.updated_at = Some(get_iso_date());
|
||||
@@ -693,8 +705,8 @@ impl<'a> HackathonRepository<'a> {
|
||||
bail!("Submission not found");
|
||||
}
|
||||
|
||||
existing.submission_status = super::hackathon_schema::SubmissionStatus::Submitted;
|
||||
existing.submitted_at = chrono::Utc::now();
|
||||
existing.submission_status = Some(super::hackathon_schema::SubmissionStatus::Submitted);
|
||||
existing.submitted_at = Some(chrono::Utc::now());
|
||||
existing.updated_at = Some(get_iso_date());
|
||||
|
||||
info!(query = %format!("UPDATE {} SET submission_status = 'Submitted' WHERE id = '{}'", table, id), "Executing SurrealDB query");
|
||||
@@ -748,4 +760,54 @@ impl<'a> HackathonRepository<'a> {
|
||||
|
||||
Ok(timeline)
|
||||
}
|
||||
}
|
||||
|
||||
// Hackathon Participants CRUD operations
|
||||
impl<'a> HackathonRepository<'a> {
|
||||
#[instrument(skip(self, hackathon_id, user_id), err)]
|
||||
pub async fn create_hackathon_participant(&self, hackathon_id: String, user_id: String) -> Result<super::hackathon_schema::HackathonParticipantSchema> {
|
||||
// Use the dedicated participants table to avoid polluting submissions
|
||||
let table = "app_hackathon_participants".to_string();
|
||||
let id = surrealdb::Uuid::new_v4().to_string();
|
||||
|
||||
let normalized_hackathon_id = self.normalize_id("app_hackathons", &hackathon_id);
|
||||
|
||||
let schema = super::hackathon_schema::HackathonParticipantSchema {
|
||||
id: Thing::from((table.clone(), id.clone())),
|
||||
hackathon_id: Thing::from(("app_hackathons".to_string(), normalized_hackathon_id)),
|
||||
user_id,
|
||||
is_deleted: false,
|
||||
created_at: Some(get_iso_date()),
|
||||
updated_at: Some(get_iso_date()),
|
||||
};
|
||||
|
||||
info!(query = %format!("CREATE {}:{}", table, id), "Executing SurrealDB query");
|
||||
let record: Option<super::hackathon_schema::HackathonParticipantSchema> = self
|
||||
.state
|
||||
.surrealdb_ws
|
||||
.create((table, id))
|
||||
.content(schema.clone())
|
||||
.await?;
|
||||
|
||||
match record {
|
||||
Some(p) => Ok(p),
|
||||
None => bail!("Failed to create participant"),
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(skip(self, meta, hackathon_id), err)]
|
||||
pub async fn list_hackathon_participants(&self, meta: imphnen_libs::MetaRequestDto, hackathon_id: String) -> Result<imphnen_libs::ResponseListSuccessDto<Vec<super::hackathon_schema::HackathonParticipantSchema>>> {
|
||||
let table = "app_hackathon_participants".to_string();
|
||||
let normalized_hackathon_id = self.normalize_id("app_hackathons", &hackathon_id);
|
||||
|
||||
let builder = QueryListBuilder::new(&self.state.surrealdb_ws, &table, &meta)
|
||||
.with_condition("is_deleted = false")
|
||||
.with_condition(&format!("hackathon_id = type::thing('app_hackathons', '{}')", normalized_hackathon_id))
|
||||
.select_fields(vec!["*"]);
|
||||
|
||||
let mut result = builder.build().await?;
|
||||
// sort by created_at for deterministic results
|
||||
result.data.sort_by_key(|s: &super::hackathon_schema::HackathonParticipantSchema| s.created_at.clone());
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
@@ -66,16 +66,16 @@ pub struct HackathonTimelineSchema {
|
||||
pub struct HackathonSubmissionsSchema {
|
||||
pub id: Thing,
|
||||
pub hackathon_id: Thing,
|
||||
pub team_id: Thing,
|
||||
pub project_name: String,
|
||||
pub description: String,
|
||||
pub team_id: Option<Thing>,
|
||||
pub project_name: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub repository_url: Option<String>,
|
||||
pub demo_url: Option<String>,
|
||||
pub slides_url: Option<String>,
|
||||
pub technologies: Vec<String>,
|
||||
pub submission_status: SubmissionStatus,
|
||||
pub technologies: Option<Vec<String>>,
|
||||
pub submission_status: Option<SubmissionStatus>,
|
||||
pub judge_feedback: Option<String>,
|
||||
pub submitted_at: DateTime<Utc>,
|
||||
pub submitted_at: Option<DateTime<Utc>>,
|
||||
pub is_deleted: bool,
|
||||
pub created_at: Option<String>,
|
||||
pub updated_at: Option<String>,
|
||||
@@ -186,6 +186,32 @@ pub enum SubmissionStatus {
|
||||
Rejected,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct HackathonParticipantSchema {
|
||||
pub id: Thing,
|
||||
pub hackathon_id: Thing,
|
||||
pub user_id: String,
|
||||
pub is_deleted: bool,
|
||||
pub created_at: Option<String>,
|
||||
pub updated_at: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for HackathonParticipantSchema {
|
||||
fn default() -> Self {
|
||||
HackathonParticipantSchema {
|
||||
id: make_thing(
|
||||
&"app_hackathon_participants".to_string(),
|
||||
&surrealdb::Uuid::new_v4().to_string(),
|
||||
),
|
||||
hackathon_id: Thing::from(("app_hackathons".to_string(), surrealdb::sql::Id::rand())),
|
||||
user_id: String::new(),
|
||||
is_deleted: false,
|
||||
created_at: Some(get_iso_date()),
|
||||
updated_at: Some(get_iso_date()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for HackathonSchema {
|
||||
fn default() -> Self {
|
||||
HackathonSchema {
|
||||
@@ -266,16 +292,16 @@ impl Default for HackathonSubmissionsSchema {
|
||||
&surrealdb::Uuid::new_v4().to_string(),
|
||||
),
|
||||
hackathon_id: Thing::from(("app_hackathons".to_string(), surrealdb::sql::Id::rand())),
|
||||
team_id: Thing::from(("app_teams".to_string(), surrealdb::sql::Id::rand())),
|
||||
project_name: String::new(),
|
||||
description: String::new(),
|
||||
team_id: Some(Thing::from(("app_teams".to_string(), surrealdb::sql::Id::rand()))),
|
||||
project_name: Some(String::new()),
|
||||
description: Some(String::new()),
|
||||
repository_url: None,
|
||||
demo_url: None,
|
||||
slides_url: None,
|
||||
technologies: vec![],
|
||||
submission_status: SubmissionStatus::Draft,
|
||||
technologies: Some(vec![]),
|
||||
submission_status: Some(SubmissionStatus::Draft),
|
||||
judge_feedback: None,
|
||||
submitted_at: Utc::now(),
|
||||
submitted_at: Some(Utc::now()),
|
||||
is_deleted: false,
|
||||
created_at: Some(get_iso_date()),
|
||||
updated_at: Some(get_iso_date()),
|
||||
|
||||
@@ -123,6 +123,19 @@ pub trait HackathonServiceTrait: Send + Sync + 'static {
|
||||
id: String,
|
||||
state: &AppState,
|
||||
) -> Pin<Box<dyn Future<Output = Result<ResponseSuccessDto<String>, ErrorDto>> + Send>>;
|
||||
|
||||
// Participants
|
||||
fn register_participant(
|
||||
hackathon_id: String,
|
||||
payload: super::hackathon_dto::RegisterParticipantRequestDto,
|
||||
state: &AppState,
|
||||
) -> Pin<Box<dyn Future<Output = Result<ResponseSuccessDto<super::hackathon_dto::HackathonParticipantDto>, ErrorDto>> + Send>>;
|
||||
|
||||
fn list_participants(
|
||||
meta: MetaRequestDto,
|
||||
hackathon_id: String,
|
||||
state: &AppState,
|
||||
) -> ListServiceFut<super::hackathon_dto::HackathonParticipantDto>;
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -981,4 +994,58 @@ impl HackathonServiceTrait for HackathonService {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn register_participant(
|
||||
hackathon_id: String,
|
||||
payload: super::hackathon_dto::RegisterParticipantRequestDto,
|
||||
state: &AppState,
|
||||
) -> Pin<Box<dyn Future<Output = Result<ResponseSuccessDto<super::hackathon_dto::HackathonParticipantDto>, ErrorDto>> + Send>> {
|
||||
let state = state.to_owned();
|
||||
Box::pin(async move {
|
||||
// Validate
|
||||
if let Err((_, errors)) = imphnen_utils::validator::validate_request(&payload) {
|
||||
return Err(ErrorDto { status: StatusCode::BAD_REQUEST.as_u16(), message: "Validation failed".to_string(), details: Some(serde_json::json!({ "validation_errors": errors })) });
|
||||
}
|
||||
|
||||
let repo = HackathonRepository::new(&state);
|
||||
|
||||
// ensure hackathon exists
|
||||
if repo.get_hackathon_by_id(hackathon_id.clone()).await.is_err() {
|
||||
return Err(ErrorDto { status: StatusCode::NOT_FOUND.as_u16(), message: "Hackathon not found".to_string(), details: None });
|
||||
}
|
||||
|
||||
match repo.create_hackathon_participant(hackathon_id, payload.user_id).await {
|
||||
Ok(schema) => {
|
||||
let dto = super::hackathon_dto::HackathonParticipantDto::from(schema);
|
||||
Ok(ResponseSuccessDto { data: dto })
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to register participant: {}", e);
|
||||
Err(ErrorDto { status: StatusCode::INTERNAL_SERVER_ERROR.as_u16(), message: "Failed to register participant".to_string(), details: None })
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn list_participants(
|
||||
meta: MetaRequestDto,
|
||||
hackathon_id: String,
|
||||
state: &AppState,
|
||||
) -> ListServiceFut<super::hackathon_dto::HackathonParticipantDto> {
|
||||
let state = state.to_owned();
|
||||
Box::pin(async move {
|
||||
let repo = HackathonRepository::new(&state);
|
||||
|
||||
match repo.list_hackathon_participants(meta, hackathon_id).await {
|
||||
Ok(result) => {
|
||||
let dtos: Vec<super::hackathon_dto::HackathonParticipantDto> = result.data.into_iter().map(super::hackathon_dto::HackathonParticipantDto::from).collect();
|
||||
Ok(ResponseListSuccessDto { data: dtos, meta: result.meta })
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to list participants: {}", e);
|
||||
Err(ErrorDto { status: StatusCode::INTERNAL_SERVER_ERROR.as_u16(), message: "Failed to list participants".to_string(), details: None })
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user