mirror of
https://gitlab.dit.htwk-leipzig.de/htwk-software/htwkalender.git
synced 2026-01-17 12:02:26 +01:00
feat: introduce feed management for individual and professor modules across frontend and backend services.
This commit is contained in:
@@ -31,6 +31,9 @@ type Feed struct {
|
||||
Created string `protobuf:"bytes,4,opt,name=created,proto3" json:"created,omitempty"`
|
||||
Updated string `protobuf:"bytes,5,opt,name=updated,proto3" json:"updated,omitempty"`
|
||||
Deleted bool `protobuf:"varint,6,opt,name=deleted,proto3" json:"deleted,omitempty"`
|
||||
Type string `protobuf:"bytes,7,opt,name=type,proto3" json:"type,omitempty"` // "prof" or "student"
|
||||
User string `protobuf:"bytes,8,opt,name=user,proto3" json:"user,omitempty"` // User ID relation
|
||||
UserEmail string `protobuf:"bytes,9,opt,name=user_email,json=userEmail,proto3" json:"user_email,omitempty"` // User Email for filtering
|
||||
}
|
||||
|
||||
func (x *Feed) Reset() {
|
||||
@@ -107,6 +110,27 @@ func (x *Feed) GetDeleted() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *Feed) GetType() string {
|
||||
if x != nil {
|
||||
return x.Type
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Feed) GetUser() string {
|
||||
if x != nil {
|
||||
return x.User
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Feed) GetUserEmail() string {
|
||||
if x != nil {
|
||||
return x.UserEmail
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type GetFeedRequest struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
@@ -204,7 +228,7 @@ func (x *GetFeedResponse) GetFeed() *Feed {
|
||||
var File_feeds_proto protoreflect.FileDescriptor
|
||||
|
||||
var file_feeds_proto_rawDesc = []byte{
|
||||
0x0a, 0x0b, 0x66, 0x65, 0x65, 0x64, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x9c, 0x01,
|
||||
0x0a, 0x0b, 0x66, 0x65, 0x65, 0x64, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xe3, 0x01,
|
||||
0x0a, 0x04, 0x46, 0x65, 0x65, 0x64, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01,
|
||||
0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65,
|
||||
0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x73,
|
||||
@@ -214,19 +238,23 @@ var file_feeds_proto_rawDesc = []byte{
|
||||
0x07, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x75, 0x70, 0x64, 0x61,
|
||||
0x74, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x75, 0x70, 0x64, 0x61, 0x74,
|
||||
0x65, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x18, 0x06, 0x20,
|
||||
0x01, 0x28, 0x08, 0x52, 0x07, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x22, 0x20, 0x0a, 0x0e,
|
||||
0x47, 0x65, 0x74, 0x46, 0x65, 0x65, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e,
|
||||
0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x22, 0x2c,
|
||||
0x0a, 0x0f, 0x47, 0x65, 0x74, 0x46, 0x65, 0x65, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
|
||||
0x65, 0x12, 0x19, 0x0a, 0x04, 0x66, 0x65, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32,
|
||||
0x05, 0x2e, 0x46, 0x65, 0x65, 0x64, 0x52, 0x04, 0x66, 0x65, 0x65, 0x64, 0x32, 0x3d, 0x0a, 0x0b,
|
||||
0x46, 0x65, 0x65, 0x64, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x2e, 0x0a, 0x07, 0x47,
|
||||
0x65, 0x74, 0x46, 0x65, 0x65, 0x64, 0x12, 0x0f, 0x2e, 0x47, 0x65, 0x74, 0x46, 0x65, 0x65, 0x64,
|
||||
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x10, 0x2e, 0x47, 0x65, 0x74, 0x46, 0x65, 0x65,
|
||||
0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x1c, 0x5a, 0x1a, 0x68,
|
||||
0x74, 0x77, 0x6b, 0x61, 0x6c, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x6f,
|
||||
0x6e, 0x2f, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f,
|
||||
0x33,
|
||||
0x01, 0x28, 0x08, 0x52, 0x07, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x12, 0x12, 0x0a, 0x04,
|
||||
0x74, 0x79, 0x70, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65,
|
||||
0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04,
|
||||
0x75, 0x73, 0x65, 0x72, 0x12, 0x1d, 0x0a, 0x0a, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x65, 0x6d, 0x61,
|
||||
0x69, 0x6c, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x75, 0x73, 0x65, 0x72, 0x45, 0x6d,
|
||||
0x61, 0x69, 0x6c, 0x22, 0x20, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x46, 0x65, 0x65, 0x64, 0x52, 0x65,
|
||||
0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28,
|
||||
0x09, 0x52, 0x02, 0x69, 0x64, 0x22, 0x2c, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x46, 0x65, 0x65, 0x64,
|
||||
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x19, 0x0a, 0x04, 0x66, 0x65, 0x65, 0x64,
|
||||
0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x05, 0x2e, 0x46, 0x65, 0x65, 0x64, 0x52, 0x04, 0x66,
|
||||
0x65, 0x65, 0x64, 0x32, 0x3d, 0x0a, 0x0b, 0x46, 0x65, 0x65, 0x64, 0x53, 0x65, 0x72, 0x76, 0x69,
|
||||
0x63, 0x65, 0x12, 0x2e, 0x0a, 0x07, 0x47, 0x65, 0x74, 0x46, 0x65, 0x65, 0x64, 0x12, 0x0f, 0x2e,
|
||||
0x47, 0x65, 0x74, 0x46, 0x65, 0x65, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x10,
|
||||
0x2e, 0x47, 0x65, 0x74, 0x46, 0x65, 0x65, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
|
||||
0x22, 0x00, 0x42, 0x1c, 0x5a, 0x1a, 0x68, 0x74, 0x77, 0x6b, 0x61, 0x6c, 0x65, 0x6e, 0x64, 0x65,
|
||||
0x72, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2f, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x73,
|
||||
0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
}
|
||||
|
||||
var (
|
||||
|
||||
175
services/common/professor/matching.go
Normal file
175
services/common/professor/matching.go
Normal file
@@ -0,0 +1,175 @@
|
||||
package professor
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ExtractNameFromEmail extracts the first and last name from a professor's email.
|
||||
// Expected format: firstname.lastname@htwk-leipzig.de
|
||||
func ExtractNameFromEmail(email string) (firstName, lastName string, err error) {
|
||||
parts := strings.Split(email, "@")
|
||||
if len(parts) != 2 {
|
||||
return "", "", fmt.Errorf("invalid email format")
|
||||
}
|
||||
|
||||
nameParts := strings.Split(parts[0], ".")
|
||||
if len(nameParts) < 2 {
|
||||
return "", "", fmt.Errorf("email does not contain dot separator")
|
||||
}
|
||||
|
||||
// Extract first and last name
|
||||
firstName = nameParts[0]
|
||||
lastName = nameParts[len(nameParts)-1]
|
||||
|
||||
// Capitalize first letter
|
||||
if len(firstName) > 0 {
|
||||
firstName = strings.ToUpper(firstName[:1]) + firstName[1:]
|
||||
}
|
||||
if len(lastName) > 0 {
|
||||
lastName = strings.ToUpper(lastName[:1]) + lastName[1:]
|
||||
}
|
||||
|
||||
return firstName, lastName, nil
|
||||
}
|
||||
|
||||
// CalculateConfidenceScore returns a score from 0.0 to 1.0 indicating how confident we are
|
||||
// that this professor string matches the given first and last name
|
||||
// 1.0 = perfect match (both first and last name exact)
|
||||
// 0.7-0.9 = good match (last name exact, first name fuzzy or present)
|
||||
// 0.4-0.6 = possible match (last name fuzzy or partial)
|
||||
// 0.1-0.3 = weak match (last name substring)
|
||||
// 0.0 = no match
|
||||
func CalculateConfidenceScore(profString, firstName, lastName string) float64 {
|
||||
// Normalize the professor string: remove common titles and split into words
|
||||
profString = strings.ToLower(profString)
|
||||
|
||||
// Remove common titles
|
||||
titles := []string{"prof.", "dr.", "arch.", "ing.", "dipl.", "m.sc.", "b.sc.", "ph.d."}
|
||||
for _, title := range titles {
|
||||
profString = strings.ReplaceAll(profString, title, "")
|
||||
}
|
||||
|
||||
// Split by spaces, hyphens, and other separators
|
||||
words := strings.FieldsFunc(profString, func(r rune) bool {
|
||||
return r == ' ' || r == '-' || r == ',' || r == '.'
|
||||
})
|
||||
|
||||
// Normalize firstName and lastName
|
||||
firstNameLower := strings.ToLower(firstName)
|
||||
lastNameLower := strings.ToLower(lastName)
|
||||
|
||||
lastNameExact := false
|
||||
lastNameFuzzy := false
|
||||
lastNameSubstring := false
|
||||
firstNameExact := false
|
||||
firstNameFuzzy := false
|
||||
|
||||
for _, word := range words {
|
||||
word = strings.TrimSpace(word)
|
||||
if word == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check last name
|
||||
if word == lastNameLower {
|
||||
lastNameExact = true
|
||||
} else if levenshteinDistance(word, lastNameLower) <= 1 && len(lastNameLower) > 3 {
|
||||
lastNameFuzzy = true
|
||||
} else if strings.Contains(word, lastNameLower) || strings.Contains(lastNameLower, word) {
|
||||
lastNameSubstring = true
|
||||
}
|
||||
|
||||
// Check first name
|
||||
if word == firstNameLower {
|
||||
firstNameExact = true
|
||||
} else if levenshteinDistance(word, firstNameLower) <= 1 && len(firstNameLower) > 3 {
|
||||
firstNameFuzzy = true
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate confidence score based on matches
|
||||
score := 0.0
|
||||
|
||||
if lastNameExact {
|
||||
if firstNameExact {
|
||||
score = 1.0 // Perfect match
|
||||
} else if firstNameFuzzy {
|
||||
score = 0.9 // Excellent match
|
||||
} else {
|
||||
score = 0.8 // Good match (last name exact, no first name match)
|
||||
}
|
||||
} else if lastNameFuzzy {
|
||||
if firstNameExact || firstNameFuzzy {
|
||||
score = 0.6 // Decent match (fuzzy last name but first name matches)
|
||||
} else {
|
||||
score = 0.5 // Medium match (fuzzy last name, no first name)
|
||||
}
|
||||
} else if lastNameSubstring {
|
||||
score = 0.2 // Weak match (substring only)
|
||||
}
|
||||
|
||||
return score
|
||||
}
|
||||
|
||||
// MatchesProfessor checks if the professor string matches the given last name (and optional first name)
|
||||
// It uses a simplified check suitable for filtering events where we want high recall but reasonable precision.
|
||||
// It returns true if the confidence score is > 0.
|
||||
func MatchesProfessor(profString, firstName, lastName string) bool {
|
||||
return CalculateConfidenceScore(profString, firstName, lastName) > 0
|
||||
}
|
||||
|
||||
// levenshteinDistance calculates the Levenshtein distance between two strings
|
||||
func levenshteinDistance(s1, s2 string) int {
|
||||
if len(s1) == 0 {
|
||||
return len(s2)
|
||||
}
|
||||
if len(s2) == 0 {
|
||||
return len(s1)
|
||||
}
|
||||
|
||||
// Create a 2D array for dynamic programming
|
||||
d := make([][]int, len(s1)+1)
|
||||
for i := range d {
|
||||
d[i] = make([]int, len(s2)+1)
|
||||
}
|
||||
|
||||
// Initialize first column and row
|
||||
for i := 0; i <= len(s1); i++ {
|
||||
d[i][0] = i
|
||||
}
|
||||
for j := 0; j <= len(s2); j++ {
|
||||
d[0][j] = j
|
||||
}
|
||||
|
||||
// Fill the matrix
|
||||
for i := 1; i <= len(s1); i++ {
|
||||
for j := 1; j <= len(s2); j++ {
|
||||
cost := 0
|
||||
if s1[i-1] != s2[j-1] {
|
||||
cost = 1
|
||||
}
|
||||
|
||||
d[i][j] = min(
|
||||
d[i-1][j]+1, // deletion
|
||||
d[i][j-1]+1, // insertion
|
||||
d[i-1][j-1]+cost, // substitution
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return d[len(s1)][len(s2)]
|
||||
}
|
||||
|
||||
func min(a, b, c int) int {
|
||||
if a < b {
|
||||
if a < c {
|
||||
return a
|
||||
}
|
||||
return c
|
||||
}
|
||||
if b < c {
|
||||
return b
|
||||
}
|
||||
return c
|
||||
}
|
||||
Reference in New Issue
Block a user