Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
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
10 changes: 5 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Binary file added public/fonts/Geist-Variable.woff2
Binary file not shown.
Binary file added public/fonts/GeistMono-Variable.woff2
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -1,24 +1,23 @@
package de.tum.cit.memo.controller;

import de.tum.cit.memo.dto.CreateCompetencyRelationshipRequest;
import de.tum.cit.memo.entity.CompetencyRelationship;
import de.tum.cit.memo.service.CompetencyRelationshipService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

/**
* Controller for competency relationship CRUD.
* For creating relationships and voting, use {@link SchedulingController}.
*/
@RestController
@RequestMapping("/api/competency-relationships")
@RequiredArgsConstructor
Expand All @@ -27,13 +26,6 @@ public class CompetencyRelationshipController {

private final CompetencyRelationshipService relationshipService;

@PostMapping
@Operation(summary = "Create a new competency relationship")
public ResponseEntity<CompetencyRelationship> createRelationship(@Valid @RequestBody CreateCompetencyRelationshipRequest request) {
CompetencyRelationship relationship = relationshipService.createRelationship(request);
return ResponseEntity.status(HttpStatus.CREATED).body(relationship);
}

@GetMapping("/{id}")
@Operation(summary = "Get competency relationship by ID")
public ResponseEntity<CompetencyRelationship> getRelationshipById(@PathVariable String id) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package de.tum.cit.memo.controller;

import de.tum.cit.memo.dto.RelationshipTaskResponse;
import de.tum.cit.memo.dto.VoteRequest;
import de.tum.cit.memo.dto.VoteResponse;
import de.tum.cit.memo.service.SchedulingService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/scheduling")
@RequiredArgsConstructor
@Tag(name = "Scheduling", description = "Endpoints for competency mapping scheduling")
public class SchedulingController {

private final SchedulingService schedulingService;

@GetMapping("/next-relationship")
@Operation(summary = "Get next relationship to vote on", description = "Returns a competency pair for the user to map. Uses coverage (70%) and consensus (30%) pipelines. Returns 204 if no tasks remain.")
public ResponseEntity<RelationshipTaskResponse> getNextRelationship(
@RequestHeader("X-User-Id") String userId) {
return schedulingService.getNextTask(userId)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.noContent().build());
}

@PostMapping("/vote")
@Operation(summary = "Submit a vote on a relationship", description = "Records the user's vote on a competency relationship. MATCHES votes are bidirectional.")
Comment thread
markstockhausen marked this conversation as resolved.
Outdated
public ResponseEntity<VoteResponse> submitVote(
@RequestHeader("X-User-Id") String userId,
@Valid @RequestBody VoteRequest request) {
VoteResponse response = schedulingService.submitVote(userId, request);
return ResponseEntity.ok(response);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package de.tum.cit.memo.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class RelationshipTaskResponse {

private String relationshipId;
private CompetencyInfo origin;
private CompetencyInfo destination;
private String pipeline;
private VoteCounts currentVotes;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class CompetencyInfo {
private String id;
private String title;
private String description;
}
}
18 changes: 18 additions & 0 deletions server/src/main/java/de/tum/cit/memo/dto/VoteCounts.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package de.tum.cit.memo.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class VoteCounts {

private int assumes;
private int extendsRelation;
private int matches;
private int unrelated;
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,11 @@
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CreateCompetencyRelationshipRequest {
public class VoteRequest {

@NotBlank(message = "Relationship ID is required")
private String relationshipId;

@NotNull(message = "Relationship type is required")
private RelationshipType relationshipType;

@NotBlank(message = "Origin ID is required")
private String originId;

@NotBlank(message = "Destination ID is required")
private String destinationId;

@NotBlank(message = "User ID is required")
private String userId;
}
17 changes: 17 additions & 0 deletions server/src/main/java/de/tum/cit/memo/dto/VoteResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package de.tum.cit.memo.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class VoteResponse {

private boolean success;
private VoteCounts updatedVotes;
private double newEntropy;
}
3 changes: 3 additions & 0 deletions server/src/main/java/de/tum/cit/memo/entity/Competency.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,7 @@ public class Competency {
@CreationTimestamp
@Column(nullable = false, updatable = false)
private Instant createdAt;

@Column(nullable = false)
private int degree;
}
Original file line number Diff line number Diff line change
@@ -1,24 +1,22 @@
package de.tum.cit.memo.entity;

import de.tum.cit.memo.enums.RelationshipType;
import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.FetchType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.OnDelete;
import org.hibernate.annotations.OnDeleteAction;
import org.hibernate.annotations.UpdateTimestamp;

import java.time.Instant;

Expand All @@ -34,11 +32,6 @@ public class CompetencyRelationship {
@Column(length = 30)
private String id;

@Enumerated(EnumType.STRING)
@Column(name = "relationship_type", nullable = false)
@NotNull
private RelationshipType relationshipType;

@NotBlank
@Column(name = "origin_id", nullable = false, length = 30)
private String originId;
Expand All @@ -47,25 +40,66 @@ public class CompetencyRelationship {
@Column(name = "destination_id", nullable = false, length = 30)
private String destinationId;

@NotBlank
@Column(name = "user_id", nullable = false, length = 30)
private String userId;
// Aggregated vote counters
@Column(name = "vote_assumes", nullable = false)
private int voteAssumes;

@Column(name = "vote_extends", nullable = false)
private int voteExtends;

@Column(name = "vote_matches", nullable = false)
private int voteMatches;

@Column(name = "vote_unrelated", nullable = false)
private int voteUnrelated;

@Column(name = "entropy", nullable = false)
private double entropy;

@Column(name = "total_votes", nullable = false)
private int totalVotes;

@JsonIgnore
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "origin_id", insertable = false, updatable = false)
@OnDelete(action = OnDeleteAction.CASCADE)
private Competency origin;

@JsonIgnore
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "destination_id", insertable = false, updatable = false)
@OnDelete(action = OnDeleteAction.CASCADE)
private Competency destination;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", insertable = false, updatable = false)
private User user;

@CreationTimestamp
@Column(nullable = false, updatable = false)
private Instant createdAt;

@UpdateTimestamp
@Column(nullable = false)
private Instant updatedAt;

/**
* Recalculates entropy based on current vote distribution.
* Entropy ranges from 0 (unanimous) to ~2.0 (max disagreement with 4 types).
*/
public void recalculateEntropy() {
int total = voteAssumes + voteExtends + voteMatches + voteUnrelated;
this.totalVotes = total;

if (total == 0) {
this.entropy = 0.0;
return;
}

double ent = 0.0;
int[] counts = { voteAssumes, voteExtends, voteMatches, voteUnrelated };
for (int count : counts) {
if (count > 0) {
double p = (double) count / total;
ent -= p * (Math.log(p) / Math.log(2));
}
}
this.entropy = ent;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package de.tum.cit.memo.entity;

import de.tum.cit.memo.enums.RelationshipType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.FetchType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.OnDelete;
import org.hibernate.annotations.OnDeleteAction;

import java.time.Instant;

@Entity
@Table(name = "competency_relationships_votes")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CompetencyRelationshipVote {

@Id
@Column(length = 30)
private String id;

@NotBlank
@Column(name = "relationship_id", nullable = false, length = 30)
private String relationshipId;

@NotBlank
@Column(name = "user_id", nullable = false, length = 30)
private String userId;

@Enumerated(EnumType.STRING)
@Column(name = "relationship_type", nullable = false)
@NotNull
private RelationshipType relationshipType;

// Relationships
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "relationship_id", insertable = false, updatable = false)
@OnDelete(action = OnDeleteAction.CASCADE)
private CompetencyRelationship relationship;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", insertable = false, updatable = false)
private User user;

@CreationTimestamp
@Column(nullable = false, updatable = false)
private Instant createdAt;
}
Loading
Loading