Files
breakpilot-lehrer/edu-search-service/internal/api/handlers/staff_handlers.go
Benjamin Boenisch 414e0f5ec0
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-school (push) Successful in 28s
CI / test-go-edu-search (push) Successful in 27s
CI / test-python-klausur (push) Successful in 1m45s
CI / test-python-agent-core (push) Successful in 16s
CI / test-nodejs-website (push) Successful in 21s
feat: edu-search-service migriert, voice-service/geo-service entfernt
- edu-search-service von breakpilot-pwa nach breakpilot-lehrer kopiert (ohne vendor)
- opensearch + edu-search-service in docker-compose.yml hinzugefuegt
- voice-service aus docker-compose.yml entfernt (jetzt in breakpilot-core)
- geo-service aus docker-compose.yml entfernt (nicht mehr benoetigt)
- CI/CD: edu-search-service zu Gitea Actions und Woodpecker hinzugefuegt
  (Go lint, test mit go mod download, build, SBOM)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 18:36:38 +01:00

375 lines
9.1 KiB
Go

package handlers
import (
"fmt"
"net/http"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/breakpilot/edu-search-service/internal/database"
"github.com/breakpilot/edu-search-service/internal/publications"
"github.com/breakpilot/edu-search-service/internal/staff"
)
// StaffHandlers handles staff-related API endpoints
type StaffHandlers struct {
repo *database.Repository
crawler *staff.StaffCrawler
pubCrawler *publications.PublicationCrawler
}
// NewStaffHandlers creates new staff handlers
func NewStaffHandlers(repo *database.Repository, email string) *StaffHandlers {
return &StaffHandlers{
repo: repo,
crawler: staff.NewStaffCrawler(repo),
pubCrawler: publications.NewPublicationCrawler(repo, email),
}
}
// SearchStaff searches for university staff
// GET /api/v1/staff/search?q=...&university_id=...&state=...&position_type=...&is_professor=...
func (h *StaffHandlers) SearchStaff(c *gin.Context) {
params := database.StaffSearchParams{
Query: c.Query("q"),
Limit: parseIntDefault(c.Query("limit"), 20),
Offset: parseIntDefault(c.Query("offset"), 0),
}
// Optional filters
if uniID := c.Query("university_id"); uniID != "" {
id, err := uuid.Parse(uniID)
if err == nil {
params.UniversityID = &id
}
}
if deptID := c.Query("department_id"); deptID != "" {
id, err := uuid.Parse(deptID)
if err == nil {
params.DepartmentID = &id
}
}
if state := c.Query("state"); state != "" {
params.State = &state
}
if uniType := c.Query("uni_type"); uniType != "" {
params.UniType = &uniType
}
if posType := c.Query("position_type"); posType != "" {
params.PositionType = &posType
}
if isProfStr := c.Query("is_professor"); isProfStr != "" {
isProf := isProfStr == "true" || isProfStr == "1"
params.IsProfessor = &isProf
}
result, err := h.repo.SearchStaff(c.Request.Context(), params)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, result)
}
// GetStaff gets a single staff member by ID
// GET /api/v1/staff/:id
func (h *StaffHandlers) GetStaff(c *gin.Context) {
idStr := c.Param("id")
id, err := uuid.Parse(idStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid staff ID"})
return
}
staff, err := h.repo.GetStaff(c.Request.Context(), id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Staff not found"})
return
}
c.JSON(http.StatusOK, staff)
}
// GetStaffPublications gets publications for a staff member
// GET /api/v1/staff/:id/publications
func (h *StaffHandlers) GetStaffPublications(c *gin.Context) {
idStr := c.Param("id")
id, err := uuid.Parse(idStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid staff ID"})
return
}
pubs, err := h.repo.GetStaffPublications(c.Request.Context(), id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"publications": pubs,
"total": len(pubs),
"staff_id": id,
})
}
// SearchPublications searches for publications
// GET /api/v1/publications/search?q=...&year=...&pub_type=...
func (h *StaffHandlers) SearchPublications(c *gin.Context) {
params := database.PublicationSearchParams{
Query: c.Query("q"),
Limit: parseIntDefault(c.Query("limit"), 20),
Offset: parseIntDefault(c.Query("offset"), 0),
}
if staffID := c.Query("staff_id"); staffID != "" {
id, err := uuid.Parse(staffID)
if err == nil {
params.StaffID = &id
}
}
if year := c.Query("year"); year != "" {
y := parseIntDefault(year, 0)
if y > 0 {
params.Year = &y
}
}
if yearFrom := c.Query("year_from"); yearFrom != "" {
y := parseIntDefault(yearFrom, 0)
if y > 0 {
params.YearFrom = &y
}
}
if yearTo := c.Query("year_to"); yearTo != "" {
y := parseIntDefault(yearTo, 0)
if y > 0 {
params.YearTo = &y
}
}
if pubType := c.Query("pub_type"); pubType != "" {
params.PubType = &pubType
}
result, err := h.repo.SearchPublications(c.Request.Context(), params)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, result)
}
// GetStaffStats gets statistics about staff data
// GET /api/v1/staff/stats
func (h *StaffHandlers) GetStaffStats(c *gin.Context) {
stats, err := h.repo.GetStaffStats(c.Request.Context())
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, stats)
}
// ListUniversities lists all universities
// GET /api/v1/universities
func (h *StaffHandlers) ListUniversities(c *gin.Context) {
universities, err := h.repo.ListUniversities(c.Request.Context())
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"universities": universities,
"total": len(universities),
})
}
// StartStaffCrawl starts a staff crawl for a university
// POST /api/v1/admin/crawl/staff
func (h *StaffHandlers) StartStaffCrawl(c *gin.Context) {
var req struct {
UniversityID string `json:"university_id"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
return
}
uniID, err := uuid.Parse(req.UniversityID)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid university ID"})
return
}
uni, err := h.repo.GetUniversity(c.Request.Context(), uniID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "University not found"})
return
}
// Start crawl in background
go func() {
result, err := h.crawler.CrawlUniversity(c.Request.Context(), uni)
if err != nil {
// Log error
return
}
_ = result
}()
c.JSON(http.StatusAccepted, gin.H{
"status": "started",
"university_id": uniID,
"message": "Staff crawl started in background",
})
}
// StartPublicationCrawl starts a publication crawl for a university
// POST /api/v1/admin/crawl/publications
func (h *StaffHandlers) StartPublicationCrawl(c *gin.Context) {
var req struct {
UniversityID string `json:"university_id"`
Limit int `json:"limit"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
return
}
uniID, err := uuid.Parse(req.UniversityID)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid university ID"})
return
}
limit := req.Limit
if limit <= 0 {
limit = 50
}
// Start crawl in background
go func() {
status, err := h.pubCrawler.CrawlForUniversity(c.Request.Context(), uniID, limit)
if err != nil {
// Log error
return
}
_ = status
}()
c.JSON(http.StatusAccepted, gin.H{
"status": "started",
"university_id": uniID,
"message": "Publication crawl started in background",
})
}
// ResolveDOI resolves a DOI and saves the publication
// POST /api/v1/publications/resolve-doi
func (h *StaffHandlers) ResolveDOI(c *gin.Context) {
var req struct {
DOI string `json:"doi"`
StaffID string `json:"staff_id,omitempty"`
}
if err := c.ShouldBindJSON(&req); err != nil || req.DOI == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "DOI is required"})
return
}
pub, err := h.pubCrawler.ResolveDOI(c.Request.Context(), req.DOI)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Link to staff if provided
if req.StaffID != "" {
staffID, err := uuid.Parse(req.StaffID)
if err == nil {
link := &database.StaffPublication{
StaffID: staffID,
PublicationID: pub.ID,
}
h.repo.LinkStaffPublication(c.Request.Context(), link)
}
}
c.JSON(http.StatusOK, pub)
}
// GetCrawlStatus gets crawl status for a university
// GET /api/v1/admin/crawl/status/:university_id
func (h *StaffHandlers) GetCrawlStatus(c *gin.Context) {
idStr := c.Param("university_id")
id, err := uuid.Parse(idStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid university ID"})
return
}
status, err := h.repo.GetCrawlStatus(c.Request.Context(), id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if status == nil {
c.JSON(http.StatusOK, gin.H{
"university_id": id,
"staff_crawl_status": "never",
"pub_crawl_status": "never",
})
return
}
c.JSON(http.StatusOK, status)
}
// Helper to parse int with default
func parseIntDefault(s string, def int) int {
if s == "" {
return def
}
var n int
_, err := fmt.Sscanf(s, "%d", &n)
if err != nil {
return def
}
return n
}
// RegisterStaffRoutes registers staff-related routes
func (h *StaffHandlers) RegisterRoutes(r *gin.RouterGroup) {
// Public endpoints
r.GET("/staff/search", h.SearchStaff)
r.GET("/staff/stats", h.GetStaffStats)
r.GET("/staff/:id", h.GetStaff)
r.GET("/staff/:id/publications", h.GetStaffPublications)
r.GET("/publications/search", h.SearchPublications)
r.POST("/publications/resolve-doi", h.ResolveDOI)
r.GET("/universities", h.ListUniversities)
// Admin endpoints
r.POST("/admin/crawl/staff", h.StartStaffCrawl)
r.POST("/admin/crawl/publications", h.StartPublicationCrawl)
r.GET("/admin/crawl/status/:university_id", h.GetCrawlStatus)
}