Skip to content

Commit 7e922f1

Browse files
Add event visibility validation and rebase fix (#45)
1 parent 41d25f4 commit 7e922f1

2 files changed

Lines changed: 263 additions & 13 deletions

File tree

pkg/handlers/event.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,15 @@ func CreateEvent(c *gin.Context) {
8585
return
8686
}
8787

88+
event.ApplyDefaults()
89+
90+
if err := event.Validate(); err != nil {
91+
c.JSON(http.StatusBadRequest, gin.H{
92+
"error": err.Error(),
93+
})
94+
return
95+
}
96+
8897
createdEvent, err := db.CreateEvent(event)
8998
if err != nil {
9099
c.JSON(http.StatusInternalServerError, gin.H{
@@ -170,6 +179,32 @@ func UpdateEventByID(c *gin.Context) {
170179
return
171180
}
172181

182+
// Validate patch fields for visibility/status support
183+
updatedEvent := existingEvent
184+
if err := updatedEvent.ApplyPatch(fields); err != nil {
185+
c.JSON(http.StatusBadRequest, gin.H{
186+
"error": err.Error(),
187+
})
188+
return
189+
}
190+
191+
// Validate the merged event state after applying PATCH fields
192+
if err := updatedEvent.Validate(); err != nil {
193+
c.JSON(http.StatusBadRequest, gin.H{
194+
"error": err.Error(),
195+
})
196+
return
197+
}
198+
199+
if _, ok := fields["visibility"]; ok {
200+
fields["visibility"] = updatedEvent.Visibility
201+
fields["minimum_visible_role"] = updatedEvent.MinimumVisibleRole
202+
}
203+
204+
if _, ok := fields["minimum_visible_role"]; ok {
205+
fields["minimum_visible_role"] = updatedEvent.MinimumVisibleRole
206+
}
207+
173208
err = db.UpdateEventByID(id, fields)
174209
if err != nil {
175210
if err == mongo.ErrNoDocuments {

pkg/models/event.go

Lines changed: 228 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,26 @@
11
package models
22

3-
import "strings"
3+
import (
4+
"fmt"
5+
"strings"
6+
)
7+
8+
const (
9+
StatusDraft = "draft"
10+
StatusPublished = "published"
11+
StatusClosed = "closed"
12+
)
13+
14+
const (
15+
VisibilityPublic = "public"
16+
VisibilityPrivate = "private"
17+
)
18+
19+
const (
20+
RoleMember = "member"
21+
RoleOfficer = "officer"
22+
RoleAdmin = "admin"
23+
)
424

525
type AnswerDetails struct {
626
MaxChars int `bson:"max_chars,omitempty" json:"max_chars,omitempty"`
@@ -20,18 +40,20 @@ type FormQuestion struct {
2040
}
2141

2242
type Event struct {
23-
ID string `bson:"_id" json:"id"`
24-
Name string `bson:"name" json:"name"`
25-
Date string `bson:"date" json:"date"`
26-
EndDate string `bson:"end_date,omitempty" json:"end_date,omitempty"`
27-
Time string `bson:"time" json:"time"`
28-
Location string `bson:"location" json:"location"`
29-
Description string `bson:"description" json:"description"`
30-
Admins []string `bson:"admins" json:"admins"`
31-
RegistrationForm []FormQuestion `bson:"registration_form" json:"registration_form"`
32-
MaxAttendees int `bson:"max_attendees" json:"max_attendees"`
33-
CreatedAt string `bson:"created_at" json:"created_at"`
34-
Status string `bson:"status" json:"status"` // draft, published, closed
43+
ID string `bson:"_id" json:"id"`
44+
Name string `bson:"name" json:"name"`
45+
Date string `bson:"date" json:"date"`
46+
EndDate string `bson:"end_date,omitempty" json:"end_date,omitempty"`
47+
Time string `bson:"time" json:"time"`
48+
Location string `bson:"location" json:"location"`
49+
Description string `bson:"description" json:"description"`
50+
Admins []string `bson:"admins" json:"admins"`
51+
RegistrationForm []FormQuestion `bson:"registration_form" json:"registration_form"`
52+
MaxAttendees int `bson:"max_attendees" json:"max_attendees"`
53+
CreatedAt string `bson:"created_at" json:"created_at"`
54+
Status string `bson:"status" json:"status"`
55+
Visibility string `bson:"visibility" json:"visibility"`
56+
MinimumVisibleRole string `bson:"minimum_visible_role,omitempty" json:"minimum_visible_role,omitempty"`
3557
}
3658

3759
type RegistrationFormValidationError struct {
@@ -43,6 +65,113 @@ func (e *RegistrationFormValidationError) Error() string {
4365
return e.Message
4466
}
4567

68+
// ApplyDefaults sets safe defaults for newly created events.
69+
func (e *Event) ApplyDefaults() {
70+
if strings.TrimSpace(e.Status) == "" {
71+
e.Status = StatusDraft
72+
}
73+
74+
if strings.TrimSpace(e.Visibility) == "" {
75+
e.Visibility = VisibilityPublic
76+
}
77+
78+
e.normalize()
79+
}
80+
81+
// normalize enforces internal consistency for dependent fields.
82+
func (e *Event) normalize() {
83+
e.Status = strings.TrimSpace(e.Status)
84+
e.Visibility = strings.TrimSpace(e.Visibility)
85+
e.MinimumVisibleRole = strings.TrimSpace(e.MinimumVisibleRole)
86+
87+
if e.Visibility == VisibilityPublic {
88+
e.MinimumVisibleRole = ""
89+
}
90+
}
91+
92+
// Validate validates the event as a whole.
93+
// Use this for creates and for PATCH after applying incoming fields onto an existing event.
94+
func (e *Event) Validate() error {
95+
e.normalize()
96+
97+
if strings.TrimSpace(e.Name) == "" {
98+
return fmt.Errorf("name is required")
99+
}
100+
if strings.TrimSpace(e.Date) == "" {
101+
return fmt.Errorf("date is required")
102+
}
103+
if strings.TrimSpace(e.Time) == "" {
104+
return fmt.Errorf("time is required")
105+
}
106+
if strings.TrimSpace(e.Location) == "" {
107+
return fmt.Errorf("location is required")
108+
}
109+
110+
if err := ValidateStatus(e.Status); err != nil {
111+
return err
112+
}
113+
if err := ValidateVisibility(e.Visibility); err != nil {
114+
return err
115+
}
116+
if err := ValidateMinimumVisibleRole(e.Visibility, e.MinimumVisibleRole); err != nil {
117+
return err
118+
}
119+
if e.MaxAttendees < 0 {
120+
return fmt.Errorf("max_attendees cannot be negative")
121+
}
122+
123+
return nil
124+
}
125+
126+
func ValidateStatus(status string) error {
127+
switch strings.TrimSpace(status) {
128+
case StatusDraft, StatusPublished, StatusClosed:
129+
return nil
130+
default:
131+
return fmt.Errorf("invalid status: must be one of draft, published, closed")
132+
}
133+
}
134+
135+
func ValidateVisibility(visibility string) error {
136+
switch strings.TrimSpace(visibility) {
137+
case VisibilityPublic, VisibilityPrivate:
138+
return nil
139+
default:
140+
return fmt.Errorf("invalid visibility: must be one of public, private")
141+
}
142+
}
143+
144+
func ValidateRole(role string) error {
145+
switch strings.TrimSpace(role) {
146+
case RoleMember, RoleOfficer, RoleAdmin:
147+
return nil
148+
default:
149+
return fmt.Errorf("invalid minimum_visible_role: must be one of member, officer, admin")
150+
}
151+
}
152+
153+
func ValidateMinimumVisibleRole(visibility string, role string) error {
154+
visibility = strings.TrimSpace(visibility)
155+
role = strings.TrimSpace(role)
156+
157+
switch visibility {
158+
case VisibilityPublic:
159+
if role != "" {
160+
return fmt.Errorf("minimum_visible_role must be empty when visibility is public")
161+
}
162+
return nil
163+
164+
case VisibilityPrivate:
165+
if role == "" {
166+
return fmt.Errorf("minimum_visible_role is required when visibility is private")
167+
}
168+
return ValidateRole(role)
169+
170+
default:
171+
return fmt.Errorf("invalid visibility: must be one of public, private")
172+
}
173+
}
174+
46175
// IsAdmin checks if the given userID is in the event's Admins list.
47176
func (e *Event) IsAdmin(userID string) bool {
48177
for _, admin := range e.Admins {
@@ -94,3 +223,89 @@ func SanitizeUpdateFields(fields map[string]interface{}) {
94223
delete(fields, "_id")
95224
delete(fields, "created_at")
96225
}
226+
227+
// ApplyPatch applies supported PATCH fields onto the event.
228+
// It performs type validation for incoming map values.
229+
// After ApplyPatch, call Validate() on the event.
230+
func (e *Event) ApplyPatch(fields map[string]interface{}) error {
231+
for key, value := range fields {
232+
switch key {
233+
case "name":
234+
s, ok := value.(string)
235+
if !ok {
236+
return fmt.Errorf("name must be a string")
237+
}
238+
e.Name = s
239+
240+
case "date":
241+
s, ok := value.(string)
242+
if !ok {
243+
return fmt.Errorf("date must be a string")
244+
}
245+
e.Date = s
246+
247+
case "end_date":
248+
s, ok := value.(string)
249+
if !ok {
250+
return fmt.Errorf("end_date must be a string")
251+
}
252+
e.EndDate = s
253+
254+
case "time":
255+
s, ok := value.(string)
256+
if !ok {
257+
return fmt.Errorf("time must be a string")
258+
}
259+
e.Time = s
260+
261+
case "location":
262+
s, ok := value.(string)
263+
if !ok {
264+
return fmt.Errorf("location must be a string")
265+
}
266+
e.Location = s
267+
268+
case "description":
269+
s, ok := value.(string)
270+
if !ok {
271+
return fmt.Errorf("description must be a string")
272+
}
273+
e.Description = s
274+
275+
case "status":
276+
s, ok := value.(string)
277+
if !ok {
278+
return fmt.Errorf("status must be a string")
279+
}
280+
e.Status = s
281+
282+
case "visibility":
283+
s, ok := value.(string)
284+
if !ok {
285+
return fmt.Errorf("visibility must be a string")
286+
}
287+
e.Visibility = s
288+
289+
case "minimum_visible_role":
290+
s, ok := value.(string)
291+
if !ok {
292+
return fmt.Errorf("minimum_visible_role must be a string")
293+
}
294+
e.MinimumVisibleRole = s
295+
296+
case "max_attendees":
297+
n, ok := value.(float64)
298+
if !ok {
299+
return fmt.Errorf("max_attendees must be a number")
300+
}
301+
e.MaxAttendees = int(n)
302+
303+
case "admins", "registration_form":
304+
// supported by persistence model, but not yet patchable here
305+
return fmt.Errorf("%s cannot be updated through this endpoint yet", key)
306+
}
307+
}
308+
309+
e.normalize()
310+
return nil
311+
}

0 commit comments

Comments
 (0)