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
- 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>
375 lines
9.1 KiB
Go
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)
|
|
}
|