Skip to content

Commit f12e139

Browse files
authored
Merge pull request #289 from kw-coms/feature/club-event-contests
Add activity events and contest voting
2 parents cdc1091 + 2c03e9a commit f12e139

26 files changed

Lines changed: 3028 additions & 19 deletions

backend/openapi.json

Lines changed: 493 additions & 17 deletions
Large diffs are not rendered by default.

backend/src/main/java/com/coms/backend/config/SecurityConfig.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,11 @@ public SecurityFilterChain filterChain(HttpSecurity http,
7777
auth.requestMatchers(HttpMethod.POST, "/api/club-activities/**").hasRole("ADMIN");
7878
auth.requestMatchers(HttpMethod.PATCH, "/api/club-activities/**").hasRole("ADMIN");
7979
auth.requestMatchers(HttpMethod.DELETE, "/api/club-activities/**").hasRole("ADMIN");
80+
auth.requestMatchers(HttpMethod.GET, "/api/club-events", "/api/club-events/**").authenticated();
81+
auth.requestMatchers(HttpMethod.POST, "/api/club-events/*/entries/*/vote").authenticated();
82+
auth.requestMatchers(HttpMethod.POST, "/api/club-events", "/api/club-events/*/entries").hasRole("ADMIN");
83+
auth.requestMatchers(HttpMethod.PATCH, "/api/club-events/**").hasRole("ADMIN");
84+
auth.requestMatchers(HttpMethod.DELETE, "/api/club-events/**").hasRole("ADMIN");
8085
// Club projects showcase is public (the /apps route is public); admin CRUD
8186
// lives under /api/admin/** and is guarded above.
8287
auth.requestMatchers(HttpMethod.GET, "/api/club-projects", "/api/club-projects/**").permitAll();
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
package com.coms.backend.controller;
2+
3+
import com.coms.backend.domain.ClubEventEntry;
4+
import com.coms.backend.dto.ClubEventRequest;
5+
import com.coms.backend.dto.ClubEventResponse;
6+
import com.coms.backend.dto.ClubEventVoteRequest;
7+
import com.coms.backend.service.ClubEventService;
8+
import jakarta.validation.Valid;
9+
import org.springframework.core.io.Resource;
10+
import org.springframework.http.ContentDisposition;
11+
import org.springframework.http.HttpHeaders;
12+
import org.springframework.http.InvalidMediaTypeException;
13+
import org.springframework.http.MediaType;
14+
import org.springframework.http.ResponseEntity;
15+
import org.springframework.security.core.Authentication;
16+
import org.springframework.web.bind.annotation.DeleteMapping;
17+
import org.springframework.web.bind.annotation.GetMapping;
18+
import org.springframework.web.bind.annotation.PatchMapping;
19+
import org.springframework.web.bind.annotation.PathVariable;
20+
import org.springframework.web.bind.annotation.PostMapping;
21+
import org.springframework.web.bind.annotation.RequestBody;
22+
import org.springframework.web.bind.annotation.RequestMapping;
23+
import org.springframework.web.bind.annotation.RequestParam;
24+
import org.springframework.web.bind.annotation.RestController;
25+
import org.springframework.web.multipart.MultipartFile;
26+
27+
import java.nio.charset.StandardCharsets;
28+
import java.util.List;
29+
30+
@RestController
31+
@RequestMapping("/api/club-events")
32+
public class ClubEventController {
33+
34+
private final ClubEventService clubEventService;
35+
36+
public ClubEventController(ClubEventService clubEventService) {
37+
this.clubEventService = clubEventService;
38+
}
39+
40+
@GetMapping
41+
public ResponseEntity<List<ClubEventResponse>> list(Authentication authentication) {
42+
return ResponseEntity.ok(clubEventService.list(authentication.getName()));
43+
}
44+
45+
@GetMapping("/{id}")
46+
public ResponseEntity<ClubEventResponse> get(@PathVariable Long id, Authentication authentication) {
47+
return ResponseEntity.ok(clubEventService.get(id, authentication.getName()));
48+
}
49+
50+
@PostMapping
51+
public ResponseEntity<ClubEventResponse> create(@Valid @RequestBody ClubEventRequest request,
52+
Authentication authentication) {
53+
return ResponseEntity.ok(clubEventService.createEvent(
54+
request.title(), request.description(), request.startsAt(), request.endsAt(), authentication.getName()));
55+
}
56+
57+
@PatchMapping("/{id}")
58+
public ResponseEntity<ClubEventResponse> update(@PathVariable Long id,
59+
@Valid @RequestBody ClubEventRequest request,
60+
Authentication authentication) {
61+
return ResponseEntity.ok(clubEventService.updateEvent(
62+
id, request.title(), request.description(), request.startsAt(), request.endsAt(), authentication.getName()));
63+
}
64+
65+
@PostMapping(path = "/{id}/entries", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
66+
public ResponseEntity<ClubEventResponse.Entry> addEntry(@PathVariable Long id,
67+
@RequestParam("title") String title,
68+
@RequestParam(value = "authorName", required = false) String authorName,
69+
@RequestParam(value = "description", required = false) String description,
70+
@RequestParam("file") MultipartFile file,
71+
Authentication authentication) {
72+
return ResponseEntity.ok(clubEventService.addEntry(id, title, authorName, description, file, authentication.getName()));
73+
}
74+
75+
@PostMapping("/{id}/entries/{entryId}/vote")
76+
public ResponseEntity<ClubEventResponse> vote(@PathVariable Long id,
77+
@PathVariable Long entryId,
78+
@Valid @RequestBody(required = false) ClubEventVoteRequest request,
79+
Authentication authentication) {
80+
Long requestedEntryId = request == null || request.entryId() == null ? entryId : request.entryId();
81+
return ResponseEntity.ok(clubEventService.vote(id, requestedEntryId, authentication.getName()));
82+
}
83+
84+
@GetMapping("/{id}/entries/{entryId}/download")
85+
public ResponseEntity<Resource> downloadEntry(@PathVariable Long id, @PathVariable Long entryId) {
86+
ClubEventEntry meta = clubEventService.loadEntryMeta(id, entryId);
87+
Resource resource = clubEventService.loadEntryResource(id, entryId);
88+
return ResponseEntity.ok()
89+
.contentType(mediaType(meta.getMimeType()))
90+
.header(HttpHeaders.CONTENT_DISPOSITION, ContentDisposition.attachment()
91+
.filename(meta.getOriginalName(), StandardCharsets.UTF_8).build().toString())
92+
.body(resource);
93+
}
94+
95+
@DeleteMapping("/{id}/entries/{entryId}")
96+
public ResponseEntity<Void> deleteEntry(@PathVariable Long id, @PathVariable Long entryId) {
97+
clubEventService.deleteEntry(id, entryId);
98+
return ResponseEntity.noContent().build();
99+
}
100+
101+
@DeleteMapping("/{id}")
102+
public ResponseEntity<Void> deleteEvent(@PathVariable Long id) {
103+
clubEventService.deleteEvent(id);
104+
return ResponseEntity.noContent().build();
105+
}
106+
107+
private MediaType mediaType(String mimeType) {
108+
if (mimeType == null || mimeType.isBlank()) {
109+
return MediaType.APPLICATION_OCTET_STREAM;
110+
}
111+
try {
112+
return MediaType.parseMediaType(mimeType);
113+
} catch (InvalidMediaTypeException e) {
114+
return MediaType.APPLICATION_OCTET_STREAM;
115+
}
116+
}
117+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package com.coms.backend.domain;
2+
3+
import jakarta.persistence.*;
4+
5+
import java.time.LocalDateTime;
6+
7+
@Entity
8+
@Table(name = "club_events")
9+
public class ClubEvent {
10+
11+
@Id
12+
@GeneratedValue(strategy = GenerationType.IDENTITY)
13+
private Long id;
14+
15+
@Column(nullable = false, length = 160)
16+
private String title;
17+
18+
@Column(columnDefinition = "TEXT")
19+
private String description;
20+
21+
@Column(name = "starts_at", nullable = false)
22+
private LocalDateTime startsAt;
23+
24+
@Column(name = "ends_at", nullable = false)
25+
private LocalDateTime endsAt;
26+
27+
@Column(name = "created_by", nullable = false, length = 50)
28+
private String createdBy;
29+
30+
@Column(name = "created_by_name", nullable = false, length = 100)
31+
private String createdByName;
32+
33+
@Column(name = "created_at", nullable = false)
34+
private LocalDateTime createdAt = LocalDateTime.now();
35+
36+
@Column(name = "updated_at", nullable = false)
37+
private LocalDateTime updatedAt = LocalDateTime.now();
38+
39+
public Long getId() { return id; }
40+
public String getTitle() { return title; }
41+
public void setTitle(String title) { this.title = title; }
42+
public String getDescription() { return description; }
43+
public void setDescription(String description) { this.description = description; }
44+
public LocalDateTime getStartsAt() { return startsAt; }
45+
public void setStartsAt(LocalDateTime startsAt) { this.startsAt = startsAt; }
46+
public LocalDateTime getEndsAt() { return endsAt; }
47+
public void setEndsAt(LocalDateTime endsAt) { this.endsAt = endsAt; }
48+
public String getCreatedBy() { return createdBy; }
49+
public void setCreatedBy(String createdBy) { this.createdBy = createdBy; }
50+
public String getCreatedByName() { return createdByName; }
51+
public void setCreatedByName(String createdByName) { this.createdByName = createdByName; }
52+
public LocalDateTime getCreatedAt() { return createdAt; }
53+
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
54+
public LocalDateTime getUpdatedAt() { return updatedAt; }
55+
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
56+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package com.coms.backend.domain;
2+
3+
import jakarta.persistence.*;
4+
5+
import java.time.LocalDateTime;
6+
7+
@Entity
8+
@Table(name = "club_event_entries")
9+
public class ClubEventEntry {
10+
11+
@Id
12+
@GeneratedValue(strategy = GenerationType.IDENTITY)
13+
private Long id;
14+
15+
@Column(name = "club_event_id", nullable = false)
16+
private Long clubEventId;
17+
18+
@Column(nullable = false, length = 160)
19+
private String title;
20+
21+
@Column(name = "author_name", length = 100)
22+
private String authorName;
23+
24+
@Column(columnDefinition = "TEXT")
25+
private String description;
26+
27+
@Column(name = "stored_name", nullable = false, length = 255)
28+
private String storedName;
29+
30+
@Column(name = "original_name", nullable = false, length = 255)
31+
private String originalName;
32+
33+
@Column(name = "mime_type", nullable = false, length = 100)
34+
private String mimeType;
35+
36+
@Column(name = "file_size", nullable = false)
37+
private long fileSize;
38+
39+
@Column(nullable = false)
40+
private int position;
41+
42+
@Column(name = "created_at", nullable = false)
43+
private LocalDateTime createdAt = LocalDateTime.now();
44+
45+
public Long getId() { return id; }
46+
public Long getClubEventId() { return clubEventId; }
47+
public void setClubEventId(Long clubEventId) { this.clubEventId = clubEventId; }
48+
public String getTitle() { return title; }
49+
public void setTitle(String title) { this.title = title; }
50+
public String getAuthorName() { return authorName; }
51+
public void setAuthorName(String authorName) { this.authorName = authorName; }
52+
public String getDescription() { return description; }
53+
public void setDescription(String description) { this.description = description; }
54+
public String getStoredName() { return storedName; }
55+
public void setStoredName(String storedName) { this.storedName = storedName; }
56+
public String getOriginalName() { return originalName; }
57+
public void setOriginalName(String originalName) { this.originalName = originalName; }
58+
public String getMimeType() { return mimeType; }
59+
public void setMimeType(String mimeType) { this.mimeType = mimeType; }
60+
public long getFileSize() { return fileSize; }
61+
public void setFileSize(long fileSize) { this.fileSize = fileSize; }
62+
public int getPosition() { return position; }
63+
public void setPosition(int position) { this.position = position; }
64+
public LocalDateTime getCreatedAt() { return createdAt; }
65+
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
66+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package com.coms.backend.domain;
2+
3+
import jakarta.persistence.*;
4+
5+
import java.time.LocalDateTime;
6+
7+
@Entity
8+
@Table(name = "club_event_votes",
9+
uniqueConstraints = @UniqueConstraint(name = "uk_club_event_votes_event_student", columnNames = {"club_event_id", "student_id"}))
10+
public class ClubEventVote {
11+
12+
@Id
13+
@GeneratedValue(strategy = GenerationType.IDENTITY)
14+
private Long id;
15+
16+
@Column(name = "club_event_id", nullable = false)
17+
private Long clubEventId;
18+
19+
@Column(name = "entry_id", nullable = false)
20+
private Long entryId;
21+
22+
@Column(name = "student_id", nullable = false, length = 50)
23+
private String studentId;
24+
25+
@Column(name = "created_at", nullable = false)
26+
private LocalDateTime createdAt = LocalDateTime.now();
27+
28+
@Column(name = "updated_at", nullable = false)
29+
private LocalDateTime updatedAt = LocalDateTime.now();
30+
31+
public Long getId() { return id; }
32+
public Long getClubEventId() { return clubEventId; }
33+
public void setClubEventId(Long clubEventId) { this.clubEventId = clubEventId; }
34+
public Long getEntryId() { return entryId; }
35+
public void setEntryId(Long entryId) { this.entryId = entryId; }
36+
public String getStudentId() { return studentId; }
37+
public void setStudentId(String studentId) { this.studentId = studentId; }
38+
public LocalDateTime getCreatedAt() { return createdAt; }
39+
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
40+
public LocalDateTime getUpdatedAt() { return updatedAt; }
41+
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
42+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.coms.backend.dto;
2+
3+
import jakarta.validation.constraints.NotBlank;
4+
5+
public record ClubEventEntryRequest(
6+
@NotBlank String title,
7+
String authorName,
8+
String description
9+
) {}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.coms.backend.dto;
2+
3+
import jakarta.validation.constraints.NotBlank;
4+
import jakarta.validation.constraints.NotNull;
5+
6+
import java.time.LocalDateTime;
7+
8+
public record ClubEventRequest(
9+
@NotBlank String title,
10+
String description,
11+
@NotNull LocalDateTime startsAt,
12+
@NotNull LocalDateTime endsAt
13+
) {}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package com.coms.backend.dto;
2+
3+
import java.time.LocalDateTime;
4+
import java.util.List;
5+
6+
public record ClubEventResponse(
7+
Long id,
8+
String title,
9+
String description,
10+
LocalDateTime startsAt,
11+
LocalDateTime endsAt,
12+
boolean votingOpen,
13+
long totalVotes,
14+
Long myEntryId,
15+
int entryCount,
16+
List<Entry> entries,
17+
String createdByName,
18+
LocalDateTime createdAt,
19+
LocalDateTime updatedAt
20+
) {
21+
public record Entry(
22+
Long id,
23+
String title,
24+
String authorName,
25+
String description,
26+
String downloadUrl,
27+
String originalName,
28+
String mimeType,
29+
long fileSize,
30+
long voteCount,
31+
boolean myVote,
32+
int rank,
33+
LocalDateTime createdAt
34+
) {}
35+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package com.coms.backend.dto;
2+
3+
import jakarta.validation.constraints.NotNull;
4+
5+
public record ClubEventVoteRequest(@NotNull Long entryId) {}

0 commit comments

Comments
 (0)