Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
201 changes: 201 additions & 0 deletions backend/api/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -894,6 +894,25 @@ components:
required:
- lead_nuid
type: object
CreatePreferenceListCommentInputBody:
additionalProperties: false
properties:
$schema:
description: A URL to the JSON Schema for this object.
examples:
- https://example.com/schemas/CreatePreferenceListCommentInputBody.json
format: uri
readOnly: true
type: string
application_id:
description: Application ID
type: string
body:
minLength: 1
type: string
required:
- body
type: object
CreatePreferenceListInputBody:
additionalProperties: false
properties:
Expand Down Expand Up @@ -2087,6 +2106,42 @@ components:
- created_at
- updated_at
type: object
PreferenceListCommentDetail:
additionalProperties: false
properties:
$schema:
description: A URL to the JSON Schema for this object.
examples:
- https://example.com/schemas/PreferenceListCommentDetail.json
format: uri
readOnly: true
type: string
application_id:
type: string
author_name:
type: string
author_nuid:
type: string
body:
type: string
created_at:
format: date-time
type: string
id:
type: string
preference_list_id:
type: string
updated_at:
format: date-time
type: string
required:
- id
- preference_list_id
- author_nuid
- body
- created_at
- updated_at
type: object
PreferenceListDeadline:
additionalProperties: false
properties:
Expand Down Expand Up @@ -2130,6 +2185,12 @@ components:
format: uri
readOnly: true
type: string
comments:
items:
$ref: "#/components/schemas/PreferenceListCommentDetail"
type:
- array
- "null"
created_at:
format: date-time
type: string
Expand Down Expand Up @@ -2173,6 +2234,7 @@ components:
- members
- entries
- personal_entries
- comments
- id
- cycle_id
- name
Expand Down Expand Up @@ -3106,6 +3168,22 @@ components:
- challenge_tracks
- post_interview_checklist
type: object
UpdatePreferenceListCommentInputBody:
additionalProperties: false
properties:
$schema:
description: A URL to the JSON Schema for this object.
examples:
- https://example.com/schemas/UpdatePreferenceListCommentInputBody.json
format: uri
readOnly: true
type: string
body:
minLength: 1
type: string
required:
- body
type: object
UpdatePreferenceListInputBody:
additionalProperties: false
properties:
Expand Down Expand Up @@ -7972,6 +8050,129 @@ paths:
summary: Rename a preference list or toggle its submitted status
tags:
- Preference lists
/preference-lists/{id}/comments:
post:
description: Any group member (or chief/admin) may post. Omit application_id for a comment on the group as a whole; set it to comment on one applicant in the shared list. Never deadline-gated.
operationId: create-preference-list-comment
parameters:
- description: Preference list ID
in: path
name: id
required: true
schema:
description: Preference list ID
type: string
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/CreatePreferenceListCommentInputBody"
required: true
responses:
"201":
content:
application/json:
schema:
$ref: "#/components/schemas/PreferenceListCommentDetail"
description: Created
"401":
content:
application/problem+json:
schema:
$ref: "#/components/schemas/ErrorModel"
description: Unauthorized
"403":
content:
application/problem+json:
schema:
$ref: "#/components/schemas/ErrorModel"
description: Forbidden
"404":
content:
application/problem+json:
schema:
$ref: "#/components/schemas/ErrorModel"
description: Not Found
"422":
content:
application/problem+json:
schema:
$ref: "#/components/schemas/ErrorModel"
description: Unprocessable Entity
"500":
content:
application/problem+json:
schema:
$ref: "#/components/schemas/ErrorModel"
description: Internal Server Error
summary: Add a comment on a preference list group or one of its applicants
tags:
- Preference lists
/preference-lists/{id}/comments/{commentId}:
put:
description: Only the comment's own author may edit it.
operationId: update-preference-list-comment
parameters:
- description: Preference list ID
in: path
name: id
required: true
schema:
description: Preference list ID
type: string
- description: Comment ID
in: path
name: commentId
required: true
schema:
description: Comment ID
type: string
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/UpdatePreferenceListCommentInputBody"
required: true
responses:
"200":
content:
application/json:
schema:
$ref: "#/components/schemas/PreferenceListCommentDetail"
description: OK
"401":
content:
application/problem+json:
schema:
$ref: "#/components/schemas/ErrorModel"
description: Unauthorized
"403":
content:
application/problem+json:
schema:
$ref: "#/components/schemas/ErrorModel"
description: Forbidden
"404":
content:
application/problem+json:
schema:
$ref: "#/components/schemas/ErrorModel"
description: Not Found
"422":
content:
application/problem+json:
schema:
$ref: "#/components/schemas/ErrorModel"
description: Unprocessable Entity
"500":
content:
application/problem+json:
schema:
$ref: "#/components/schemas/ErrorModel"
description: Internal Server Error
summary: Edit a preference list comment
tags:
- Preference lists
/preference-lists/{id}/entries/{applicationId}:
delete:
operationId: delete-preference-list-entry
Expand Down
72 changes: 72 additions & 0 deletions backend/internal/handlers/preference_list_comments.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package handlers

import (
"context"

"github.com/danielgtaylor/huma/v2"

"github.com/GenerateNU/apportal/backend/internal/models"
)

type PreferenceListCommentOutput struct {
Body models.PreferenceListCommentDetail
}

type CreatePreferenceListCommentInput struct {
ID string `path:"id" doc:"Preference list ID"`
Body struct {
// Omit for a comment on the group as a whole; set to comment on one
// applicant already in the shared list's cycle.
ApplicationID *string `json:"application_id,omitempty" doc:"Application ID"`
Body string `json:"body" minLength:"1"`
}
}

func (h *preferenceListHandler) createComment(ctx context.Context, in *CreatePreferenceListCommentInput) (*PreferenceListCommentOutput, error) {
if err := requireReviewer(ctx); err != nil {
return nil, err
}
if err := h.requireAccess(ctx, in.ID); err != nil {
return nil, err
}
if in.Body.ApplicationID != nil {
list, err := h.store.GetPreferenceList(ctx, in.ID)
if err != nil {
return nil, storeErr(err)
}
app, err := h.store.GetApplication(ctx, *in.Body.ApplicationID)
if err != nil {
return nil, storeErr(err)
}
if app.CycleID != list.CycleID {
return nil, huma.Error422UnprocessableEntity("application is not in this list's cycle")
}
}
comment, err := h.store.CreatePreferenceListComment(ctx, in.ID, in.Body.ApplicationID, currentActor(ctx).NUID, in.Body.Body)
if err != nil {
return nil, storeErr(err)
}
return &PreferenceListCommentOutput{Body: comment}, nil
}

type UpdatePreferenceListCommentInput struct {
ID string `path:"id" doc:"Preference list ID"`
CommentID string `path:"commentId" doc:"Comment ID"`
Body struct {
Body string `json:"body" minLength:"1"`
}
}

func (h *preferenceListHandler) updateComment(ctx context.Context, in *UpdatePreferenceListCommentInput) (*PreferenceListCommentOutput, error) {
if err := requireReviewer(ctx); err != nil {
return nil, err
}
if err := h.requireAccess(ctx, in.ID); err != nil {
return nil, err
}
comment, err := h.store.UpdatePreferenceListComment(ctx, in.CommentID, currentActor(ctx).NUID, in.Body.Body)
if err != nil {
return nil, storeErr(err)
}
return &PreferenceListCommentOutput{Body: comment}, nil
}
21 changes: 21 additions & 0 deletions backend/internal/handlers/preference_lists.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,27 @@ func (h *preferenceListHandler) register(api huma.API) {
Errors: []int{http.StatusUnauthorized, http.StatusForbidden, http.StatusNotFound, http.StatusUnprocessableEntity},
}, h.reorderPersonalEntries)

huma.Register(api, huma.Operation{
OperationID: "create-preference-list-comment",
Method: http.MethodPost,
Path: "/preference-lists/{id}/comments",
Summary: "Add a comment on a preference list group or one of its applicants",
Description: "Any group member (or chief/admin) may post. Omit application_id for a comment on the group as a whole; set it to comment on one applicant in the shared list. Never deadline-gated.",
Tags: []string{"Preference lists"},
DefaultStatus: http.StatusCreated,
Errors: []int{http.StatusUnauthorized, http.StatusForbidden, http.StatusNotFound, http.StatusUnprocessableEntity},
}, h.createComment)

huma.Register(api, huma.Operation{
OperationID: "update-preference-list-comment",
Method: http.MethodPut,
Path: "/preference-lists/{id}/comments/{commentId}",
Summary: "Edit a preference list comment",
Description: "Only the comment's own author may edit it.",
Tags: []string{"Preference lists"},
Errors: []int{http.StatusUnauthorized, http.StatusForbidden, http.StatusNotFound, http.StatusUnprocessableEntity},
}, h.updateComment)

huma.Register(api, huma.Operation{
OperationID: "get-preference-list-deadline",
Method: http.MethodGet,
Expand Down
32 changes: 28 additions & 4 deletions backend/internal/models/preference_lists.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,15 +114,39 @@ type PreferenceListPersonalEntryDetail struct {
ApplicationRole Role `json:"application_role"`
}

// PreferenceListDetail bundles a list with its members, shared entries, and
// every member's personal entries for a single detail-page fetch, like
// WrittenReviewDetail bundling a review with its answers — personal entries
// for every member are fetched in bulk here rather than once per member.
// PreferenceListComment is an open comment within a group's shared list —
// mirrors InterviewComment's shape (any member may post, edit only their
// own). ApplicationID nil means a comment on the group as a whole; set means
// a comment on that one applicant/entry. Scoped to the shared list only, not
// personal lists.
type PreferenceListComment struct {
ID string `json:"id"`
PreferenceListID string `json:"preference_list_id"`
ApplicationID *string `json:"application_id,omitempty"`
AuthorNUID string `json:"author_nuid"`
Body string `json:"body"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}

// PreferenceListCommentDetail bundles a comment with the author's resolved
// display name (not a table column).
type PreferenceListCommentDetail struct {
PreferenceListComment
AuthorName string `json:"author_name,omitempty"`
}

// PreferenceListDetail bundles a list with its members, shared entries,
// every member's personal entries, and every comment (group-level and
// per-entry) for a single detail-page fetch, like WrittenReviewDetail
// bundling a review with its answers — everything is fetched in bulk here
// rather than once per member/entry.
type PreferenceListDetail struct {
PreferenceList
Members []PreferenceListMember `json:"members"`
Entries []PreferenceListEntryDetail `json:"entries"`
PersonalEntries []PreferenceListPersonalEntryDetail `json:"personal_entries"`
Comments []PreferenceListCommentDetail `json:"comments"`
}

// PreferenceListDeadline is a per-(cycle, role) settings row — not a column
Expand Down
Loading
Loading