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
42 changes: 42 additions & 0 deletions src/backend/src/main/java/team/projectpulse/rubric/Criterion.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package team.projectpulse.rubric;

import jakarta.persistence.*;

@Entity
@Table(name = "criteria")
public class Criterion {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "criterion_id")
private Long criterionId;

@Column(nullable = false)
private String criterion;

@Column(columnDefinition = "TEXT")
private String description;

@Column(name = "max_score", nullable = false)
private int maxScore = 10;

@Column(name = "course_id")
private Long courseId;

// ── Getters / Setters ────────────────────────────────────────────────────

public Long getCriterionId() { return criterionId; }
public void setCriterionId(Long v) { this.criterionId = v; }

public String getCriterion() { return criterion; }
public void setCriterion(String v) { this.criterion = v; }

public String getDescription() { return description; }
public void setDescription(String v) { this.description = v; }

public int getMaxScore() { return maxScore; }
public void setMaxScore(int v) { this.maxScore = v; }

public Long getCourseId() { return courseId; }
public void setCourseId(Long v) { this.courseId = v; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package team.projectpulse.rubric;

import org.springframework.web.bind.annotation.*;
import team.projectpulse.common.Result;

import java.util.List;
import java.util.Map;

@RestController
@RequestMapping("${api.endpoint.base-url}/criteria")
public class CriterionController {

private final RubricService rubricService;

public CriterionController(RubricService rubricService) {
this.rubricService = rubricService;
}

/** POST /criteria/search */
@PostMapping("/search")
public Result<Map<String, Object>> search(@RequestBody CriterionSearchRequest body) {
List<Criterion> list = rubricService.searchCriteria(body.criterionId(), body.criterion());
return Result.success(Map.of("content", list));
}

/** GET /criteria/{id} */
@GetMapping("/{id}")
public Result<Criterion> findById(@PathVariable Long id) {
return Result.success(rubricService.findCriterionById(id));
}

/** POST /criteria */
@PostMapping
public Result<Criterion> create(@RequestBody Criterion criterion) {
return Result.success("Criterion created.", rubricService.createCriterion(criterion));
}

/** PUT /criteria/{id} */
@PutMapping("/{id}")
public Result<Criterion> update(@PathVariable Long id, @RequestBody Criterion criterion) {
return Result.success("Criterion updated.", rubricService.updateCriterion(id, criterion));
}

record CriterionSearchRequest(Long criterionId, String criterion) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package team.projectpulse.rubric;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.util.List;

public interface CriterionRepository extends JpaRepository<Criterion, Long> {

@Query("""
SELECT c FROM Criterion c
WHERE (:criterionId IS NULL OR c.criterionId = :criterionId)
AND (:criterion IS NULL OR LOWER(c.criterion) LIKE LOWER(CONCAT('%', :criterion, '%')))
ORDER BY c.criterion ASC
""")
List<Criterion> searchByCriteria(
@Param("criterionId") Long criterionId,
@Param("criterion") String criterion
);
}
44 changes: 44 additions & 0 deletions src/backend/src/main/java/team/projectpulse/rubric/Rubric.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package team.projectpulse.rubric;

import jakarta.persistence.*;

import java.util.ArrayList;
import java.util.List;

@Entity
@Table(name = "rubrics")
public class Rubric {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "rubric_id")
private Long rubricId;

@Column(name = "rubric_name", nullable = false)
private String rubricName;

@Column(name = "course_id")
private Long courseId;

@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(
name = "rubric_criteria",
joinColumns = @JoinColumn(name = "rubric_id"),
inverseJoinColumns = @JoinColumn(name = "criterion_id")
)
private List<Criterion> criteria = new ArrayList<>();

// ── Getters / Setters ────────────────────────────────────────────────────

public Long getRubricId() { return rubricId; }
public void setRubricId(Long v) { this.rubricId = v; }

public String getRubricName() { return rubricName; }
public void setRubricName(String v) { this.rubricName = v; }

public Long getCourseId() { return courseId; }
public void setCourseId(Long v) { this.courseId = v; }

public List<Criterion> getCriteria() { return criteria; }
public void setCriteria(List<Criterion> v) { this.criteria = v; }
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,57 @@
package team.projectpulse.rubric;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
* Handles Rubric and Criteria management endpoints.
*
* Use Cases:
* UC-1: Admin creates a rubric
*/
import org.springframework.web.bind.annotation.*;
import team.projectpulse.common.Result;

import java.util.List;
import java.util.Map;

@RestController
@RequestMapping("${api.endpoint.base-url}/rubrics")
public class RubricController {
// TODO: Implement UC-1

private final RubricService rubricService;

public RubricController(RubricService rubricService) {
this.rubricService = rubricService;
}

/** POST /rubrics/search */
@PostMapping("/search")
public Result<Map<String, Object>> search(@RequestBody RubricSearchRequest body) {
List<Rubric> list = rubricService.searchRubrics(body.rubricId(), body.rubricName());
return Result.success(Map.of("content", list));
}

/** GET /rubrics/{id} */
@GetMapping("/{id}")
public Result<Rubric> findById(@PathVariable Long id) {
return Result.success(rubricService.findRubricById(id));
}

/** POST /rubrics */
@PostMapping
public Result<Rubric> create(@RequestBody Rubric rubric) {
return Result.success("Rubric created.", rubricService.createRubric(rubric));
}

/** PUT /rubrics/{id} */
@PutMapping("/{id}")
public Result<Rubric> update(@PathVariable Long id, @RequestBody Rubric rubric) {
return Result.success("Rubric updated.", rubricService.updateRubric(id, rubric));
}

/** PUT /rubrics/{rubricId}/criteria/{criterionId} */
@PutMapping("/{rubricId}/criteria/{criterionId}")
public Result<Rubric> assignCriterion(@PathVariable Long rubricId, @PathVariable Long criterionId) {
return Result.success("Criterion assigned.", rubricService.assignCriterion(rubricId, criterionId));
}

/** DELETE /rubrics/{rubricId}/criteria/{criterionId} */
@DeleteMapping("/{rubricId}/criteria/{criterionId}")
public Result<Rubric> unassignCriterion(@PathVariable Long rubricId, @PathVariable Long criterionId) {
return Result.success("Criterion removed.", rubricService.unassignCriterion(rubricId, criterionId));
}

record RubricSearchRequest(Long rubricId, String rubricName) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package team.projectpulse.rubric;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import team.projectpulse.common.Result;

@RestControllerAdvice
public class RubricExceptionHandler {

@ExceptionHandler(RubricNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
Result<Void> handleNotFound(RubricNotFoundException ex) {
return Result.notFound(ex.getMessage());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package team.projectpulse.rubric;

public class RubricNotFoundException extends RuntimeException {
public RubricNotFoundException(String type, Long id) {
super("Could not find " + type + " with id: " + id);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package team.projectpulse.rubric;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.util.List;

public interface RubricRepository extends JpaRepository<Rubric, Long> {

@Query("""
SELECT r FROM Rubric r
WHERE (:rubricId IS NULL OR r.rubricId = :rubricId)
AND (:rubricName IS NULL OR LOWER(r.rubricName) LIKE LOWER(CONCAT('%', :rubricName, '%')))
ORDER BY r.rubricName ASC
""")
List<Rubric> searchByCriteria(
@Param("rubricId") Long rubricId,
@Param("rubricName") String rubricName
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package team.projectpulse.rubric;

import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class RubricService {

private final RubricRepository rubricRepository;
private final CriterionRepository criterionRepository;

public RubricService(RubricRepository rubricRepository, CriterionRepository criterionRepository) {
this.rubricRepository = rubricRepository;
this.criterionRepository = criterionRepository;
}

// ── Criteria ─────────────────────────────────────────────────────────────

public List<Criterion> searchCriteria(Long criterionId, String criterion) {
String name = (criterion == null || criterion.isBlank()) ? null : criterion.trim();
return criterionRepository.searchByCriteria(criterionId, name);
}

public Criterion findCriterionById(Long id) {
return criterionRepository.findById(id)
.orElseThrow(() -> new RubricNotFoundException("criterion", id));
}

public Criterion createCriterion(Criterion c) {
c.setCriterionId(null);
return criterionRepository.save(c);
}

public Criterion updateCriterion(Long id, Criterion c) {
Criterion existing = findCriterionById(id);
existing.setCriterion(c.getCriterion());
existing.setDescription(c.getDescription());
existing.setMaxScore(c.getMaxScore());
existing.setCourseId(c.getCourseId());
return criterionRepository.save(existing);
}

// ── Rubrics ───────────────────────────────────────────────────────────────

public List<Rubric> searchRubrics(Long rubricId, String rubricName) {
String name = (rubricName == null || rubricName.isBlank()) ? null : rubricName.trim();
return rubricRepository.searchByCriteria(rubricId, name);
}

public Rubric findRubricById(Long id) {
return rubricRepository.findById(id)
.orElseThrow(() -> new RubricNotFoundException("rubric", id));
}

public Rubric createRubric(Rubric r) {
r.setRubricId(null);
r.setCriteria(List.of());
return rubricRepository.save(r);
}

public Rubric updateRubric(Long id, Rubric r) {
Rubric existing = findRubricById(id);
existing.setRubricName(r.getRubricName());
existing.setCourseId(r.getCourseId());
return rubricRepository.save(existing);
}

public Rubric assignCriterion(Long rubricId, Long criterionId) {
Rubric rubric = findRubricById(rubricId);
Criterion criterion = findCriterionById(criterionId);
if (!rubric.getCriteria().contains(criterion)) {
rubric.getCriteria().add(criterion);
rubricRepository.save(rubric);
}
return rubric;
}

public Rubric unassignCriterion(Long rubricId, Long criterionId) {
Rubric rubric = findRubricById(rubricId);
rubric.getCriteria().removeIf(c -> c.getCriterionId().equals(criterionId));
return rubricRepository.save(rubric);
}
}
41 changes: 41 additions & 0 deletions src/backend/src/main/resources/db/migration/V3__rubrics.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
-- V3: Rubrics and Criteria schema

CREATE TABLE IF NOT EXISTS criteria (
criterion_id BIGINT AUTO_INCREMENT PRIMARY KEY,
criterion VARCHAR(255) NOT NULL,
description TEXT,
max_score INT NOT NULL DEFAULT 10,
course_id BIGINT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE IF NOT EXISTS rubrics (
rubric_id BIGINT AUTO_INCREMENT PRIMARY KEY,
rubric_name VARCHAR(255) NOT NULL,
course_id BIGINT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE IF NOT EXISTS rubric_criteria (
rubric_id BIGINT NOT NULL,
criterion_id BIGINT NOT NULL,
PRIMARY KEY (rubric_id, criterion_id),
FOREIGN KEY (rubric_id) REFERENCES rubrics(rubric_id) ON DELETE CASCADE,
FOREIGN KEY (criterion_id) REFERENCES criteria(criterion_id) ON DELETE CASCADE
);

-- Seed criteria
INSERT INTO criteria (criterion, description, max_score) VALUES
('Participation', 'Active engagement in team meetings and project activities.', 10),
('Code Quality', 'Adherence to coding standards, readability, and documentation.', 10),
('Communication', 'Clarity and frequency of communication with teammates.', 10),
('Deliverables', 'Timely completion of assigned tasks and milestones.', 10),
('Problem Solving', 'Ability to identify and resolve technical or team challenges.', 10);

-- Seed a default rubric with all criteria assigned
INSERT INTO rubrics (rubric_name) VALUES ('Default Peer Evaluation Rubric');

INSERT INTO rubric_criteria (rubric_id, criterion_id)
SELECT r.rubric_id, c.criterion_id
FROM rubrics r, criteria c
WHERE r.rubric_name = 'Default Peer Evaluation Rubric';
Loading
Loading