Skip to main content

Overview

Event Organizers submit proposals to request permission to host events. The proposal system supports multipart file uploads, approval workflows, and resubmission after rejection.

Multipart Upload

Attach supporting documents to proposals

Approval Workflow

Admin review and approval process

Resubmission

Update and resubmit rejected proposals

Status Tracking

Track proposal through pending, approved, rejected states

Proposal Lifecycle

1

Submission

Organizer creates a proposal with event details and optional document attachments
2

Pending Review

Proposal enters the queue for admin review
3

Admin Decision

Admin approves or rejects the proposal with comments
4

Approved

Event is created and becomes visible to students
5

Rejected (Optional)

Organizer can update and resubmit with changes

Create Proposal

Endpoint

POST /api/proposals
Content-Type: multipart/form-data

Authorization

Only users with ORGANIZER role can create proposals.
ProposalController.java:48-49
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@PreAuthorize("hasRole('ORGANIZER')")

Request Structure

The request must be sent as multipart/form-data with two parts:
proposal
string
required
JSON string containing the proposal details
files
MultipartFile[]
Array of document files (optional)

Proposal JSON Structure

title
string
required
Event title
description
string
Detailed event description
proposedDate
LocalDate
required
Proposed event date (must be in the future)
startTime
LocalTime
required
Event start time
endTime
LocalTime
required
Event end time
venue
string
required
Event location
capacity
integer
required
Maximum number of participants (minimum 1)
organizationType
enum
required
One of: ACADEMIC, CULTURAL, SPORTS, SOCIAL, CAREER, VOLUNTEER

Implementation

The controller handles multipart requests by deserializing JSON and processing files:
ProposalController.java:50-72
public ResponseEntity<Response<ProposalDTO>> createProposal(
        @RequestPart("proposal") String proposalJson,
        @RequestPart(value = "files", required = false) MultipartFile[] files,
        @AuthenticationPrincipal CustomUserDetails userDetails) throws Exception {

    ProposalDTO proposalDTO = objectMapper.readValue(proposalJson, ProposalDTO.class);

    var violations = validator.validate(proposalDTO);
    if (!violations.isEmpty()) {
        throw new ConstraintViolationException(violations);
    }

    Long organizerID = userDetails.getUser().getUserID();
    ProposalDTO createdProposal = proposalService.createProposal(proposalDTO, files, organizerID);

    Response<ProposalDTO> response = Response.<ProposalDTO>builder()
            .statusCode(HttpStatus.CREATED.value())
            .message("Proposal submitted successfully and is pending approval")
            .data(createdProposal)
            .build();

    return ResponseEntity.status(HttpStatus.CREATED).body(response);
}
The proposalJson must be valid JSON. Use JSON.stringify() when sending from JavaScript.

Example Request

curl -X POST https://api.ems.edu/api/proposals \
  -H "Authorization: Bearer YOUR_JWT_TOKEN" \
  -F 'proposal={"title":"Tech Workshop","description":"Learn React","proposedDate":"2024-04-15","startTime":"14:00:00","endTime":"16:00:00","venue":"Room 301","capacity":50,"organizationType":"ACADEMIC"}' \
  -F '[email protected]' \
  -F '[email protected]'

Response Example

{
  "statusCode": 201,
  "message": "Proposal submitted successfully and is pending approval",
  "data": {
    "proposalID": 123,
    "title": "Tech Workshop",
    "description": "Learn React",
    "proposedDate": "2024-04-15",
    "startTime": "14:00:00",
    "endTime": "16:00:00",
    "venue": "Room 301",
    "capacity": 50,
    "organizationType": "ACADEMIC",
    "status": "PENDING",
    "submittedAt": "2024-03-15T10:30:00",
    "organizerID": 456,
    "organizerName": "Computer Science Club",
    "attachmentsJson": "[\"proposal-doc.pdf\",\"budget.xlsx\"]"
  },
  "timestamp": "2024-03-15T10:30:00"
}

Get My Proposals

Endpoint

GET /api/proposals

Authorization

ProposalController.java:31-32
@GetMapping
@PreAuthorize("hasRole('ORGANIZER')")

Implementation

ProposalController.java:33-46
public ResponseEntity<Response<List<ProposalDTO>>> getMyProposals(
        @AuthenticationPrincipal CustomUserDetails userDetails) {

    Long organizerID = userDetails.getUser().getUserID();
    List<ProposalDTO> proposals = proposalService.getProposalsByOrganizer(organizerID);

    Response<List<ProposalDTO>> response = Response.<List<ProposalDTO>>builder()
            .statusCode(HttpStatus.OK.value())
            .message("Proposals fetched successfully")
            .data(proposals)
            .build();

    return ResponseEntity.ok(response);
}
This endpoint automatically filters proposals to only show those created by the authenticated organizer.

Response Example

{
  "statusCode": 200,
  "message": "Proposals fetched successfully",
  "data": [
    {
      "proposalID": 123,
      "title": "Tech Workshop",
      "status": "PENDING",
      "submittedAt": "2024-03-15T10:30:00"
    },
    {
      "proposalID": 122,
      "title": "Career Fair",
      "status": "APPROVED",
      "submittedAt": "2024-03-10T09:00:00",
      "reviewedAt": "2024-03-12T14:30:00"
    },
    {
      "proposalID": 121,
      "title": "Networking Event",
      "status": "REJECTED",
      "submittedAt": "2024-03-05T11:00:00",
      "reviewedAt": "2024-03-07T16:00:00",
      "rejectionReason": "Venue not available on proposed date"
    }
  ],
  "timestamp": "2024-03-15T10:35:00"
}

Resubmit Proposal

Endpoint

PUT /api/proposals/{proposalID}/resubmit
Content-Type: multipart/form-data

Authorization

ProposalController.java:74-75
@PutMapping(value = "/{proposalID}/resubmit", consumes = {"multipart/form-data"})
@PreAuthorize("hasRole('ORGANIZER')")

Use Case

After a proposal is rejected, the organizer can update the details and resubmit:
1

Review Rejection Reason

Read the rejectionReason from the proposal
2

Update Proposal

Modify the proposal details to address the concerns
3

Resubmit

Send the updated proposal with new or additional files

Implementation

ProposalController.java:76-93
public ResponseEntity<Response<ProposalDTO>> resubmit(
        @PathVariable Long proposalID,
        @RequestPart("proposal") String proposalJson,
        @RequestPart(value = "files", required = false) MultipartFile[] files,
        @AuthenticationPrincipal CustomUserDetails userDetails) throws Exception {

    Long organizerID = userDetails.getUser().getUserID();
    ProposalDTO dto = objectMapper.readValue(proposalJson, ProposalDTO.class);
    ProposalDTO updated = proposalService.updateAndResubmit(proposalID, dto, files, organizerID);

    Response<ProposalDTO> response = Response.<ProposalDTO>builder()
            .statusCode(HttpStatus.OK.value())
            .message("Proposal resubmitted successfully")
            .data(updated)
            .build();

    return ResponseEntity.ok(response);
}

Example Request

curl -X PUT https://api.ems.edu/api/proposals/123/resubmit \
  -H "Authorization: Bearer YOUR_JWT_TOKEN" \
  -F 'proposal={"title":"Tech Workshop - Updated","proposedDate":"2024-05-15","venue":"Room 401"}' \
  -F '[email protected]'

Response Example

{
  "statusCode": 200,
  "message": "Proposal resubmitted successfully",
  "data": {
    "proposalID": 123,
    "title": "Tech Workshop - Updated",
    "status": "PENDING",
    "submittedAt": "2024-03-15T14:00:00",
    "rejectionReason": null
  },
  "timestamp": "2024-03-15T14:00:00"
}
Resubmitting a proposal resets its status to PENDING and clears the previous rejectionReason.

Proposal Statuses

Description: Proposal is awaiting admin reviewActions Available:
  • Admin can approve or reject
  • Organizer can view details
Next States:
  • APPROVED (admin approves)
  • REJECTED (admin rejects)

Document Attachments

File Handling

Proposals support multiple file attachments:
@RequestPart(value = "files", required = false) MultipartFile[] files

Optional Files

Files are optional - proposals can be submitted without attachments

Multiple Files

Multiple files can be attached in a single request

Storage

File paths are stored as JSON in the attachmentsJson field

Retrieval

Files can be retrieved through the file service

Supported File Types

Always validate file types and sizes on the server to prevent security vulnerabilities.
Common file types for proposals:
  • PDF documents (.pdf)
  • Word documents (.doc, .docx)
  • Excel spreadsheets (.xls, .xlsx)
  • Images (.jpg, .png)
  • Presentations (.ppt, .pptx)

Validation Rules

Field Validation

ProposalDTO.java:26-49
@NotBlank(message = "Title is required")
private String title;

@NotNull(message = "Proposed date is required")
@Future(message = "Proposed date must be in the future")
private LocalDate proposedDate;

@NotNull(message = "Start time is required")
private LocalTime startTime;

@NotNull(message = "End time is required")
private LocalTime endTime;

@NotBlank(message = "Venue is required")
private String venue;

@NotNull(message = "Capacity is required")
@Min(value = 1, message = "Capacity must be at least 1")
private Integer capacity;

@NotNull(message = "Organization type is required")
private OrganizationType organizationType;

Manual Validation

The controller performs manual validation before processing:
ProposalController.java:57-60
var violations = validator.validate(proposalDTO);
if (!violations.isEmpty()) {
    throw new ConstraintViolationException(violations);
}
Validation errors are automatically caught by the global exception handler and returned as structured error responses.

The Wrapper Rule

All API responses follow the Wrapper Rule: ResponseEntity<Response<T>>

Response Structure

Response.java:11-33
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Response<T> {
    private int statusCode;
    private String message;
    private T data;

    @Builder.Default
    private LocalDateTime timestamp = LocalDateTime.now();

    private List<String> errors;
}

Example Responses

{
  "statusCode": 201,
  "message": "Proposal submitted successfully",
  "data": { /* ProposalDTO */ },
  "timestamp": "2024-03-15T10:30:00",
  "errors": null
}
{
  "statusCode": 400,
  "message": "Validation failed",
  "data": null,
  "timestamp": "2024-03-15T10:30:00",
  "errors": [
    "Title is required",
    "Proposed date must be in the future"
  ]
}
{
  "statusCode": 403,
  "message": "Access Denied",
  "data": null,
  "timestamp": "2024-03-15T10:30:00",
  "errors": null
}

Admin Review Process

For admin-side endpoints (approve/reject), see the Admin Operations documentation.
Admins can:
  • View all pending proposals
  • Filter by department
  • Approve proposals (creates events)
  • Reject proposals with reasons
  • View approval history

Best Practices

1

Validate Before Upload

Validate proposal data on the client side before sending files to reduce server load.
2

Handle File Size Limits

Implement client-side file size checks to prevent upload failures.
3

Provide Clear Rejection Reasons

When rejecting proposals, provide specific, actionable feedback.
4

Check Status Before Actions

Verify proposal status before attempting to resubmit or modify.

Error Handling

{
  "statusCode": 404,
  "message": "Proposal not found with ID: 123",
  "timestamp": "2024-03-15T10:30:00"
}
{
  "statusCode": 403,
  "message": "You can only access your own proposals",
  "timestamp": "2024-03-15T10:30:00"
}
{
  "statusCode": 400,
  "message": "Cannot resubmit an approved proposal",
  "timestamp": "2024-03-15T10:30:00"
}
{
  "statusCode": 500,
  "message": "Failed to upload file: document.pdf",
  "timestamp": "2024-03-15T10:30:00"
}

Events

Learn about event management after approval

Roles & Permissions

Understand organizer permissions

Build docs developers (and LLMs) love