diff --git a/resources/db/migration/V1.2.5.2__add_report_statistics.sql b/resources/db/migration/V1.2.5.2__add_report_statistics.sql new file mode 100644 index 0000000..c4112aa --- /dev/null +++ b/resources/db/migration/V1.2.5.2__add_report_statistics.sql @@ -0,0 +1,6 @@ +create table report_statistics +( + subject varchar(128) not null, + count integer not null, + constraint report_statistics_pk primary key (subject) +) \ No newline at end of file diff --git a/src/api/centers.go b/src/api/centers.go index be7f880..add3e4f 100644 --- a/src/api/centers.go +++ b/src/api/centers.go @@ -62,6 +62,9 @@ type Centers struct { func NewCentersAPI(centersService services.Centers, centersRepository repositories.Centers, bugReportsService services.BugReports, operatorsService services.Operators, geocoder geocoding.Geocoder, auth *jwtauth.JWTAuth) *Centers { + validate := validator.New() + validate.RegisterTagNameFunc(util.JsonTagNameFunc) + centers := &Centers{ Router: chi.NewRouter(), centersService: centersService, @@ -69,7 +72,7 @@ func NewCentersAPI(centersService services.Centers, centersRepository repositori operatorsService: operatorsService, geocoder: geocoder, bugReportsService: bugReportsService, - validate: validator.New(), + validate: validate, } // public endpoints @@ -84,6 +87,7 @@ func NewCentersAPI(centersService services.Centers, centersRepository repositori r.Get("/all", api.Handle(centers.getAllCenters)) r.Post("/csv", api.Handle(centers.prepareCSVImport)) r.Post("/", api.Handle(centers.importCenters)) + r.Put("/{uuid}", api.Handle(centers.updateCenter)) // get centers r.Get("/reference/{reference}", api.Handle(centers.getCenterByReferenceLegacy)) @@ -245,7 +249,7 @@ func (c *Centers) importCenters(_ http.ResponseWriter, r *http.Request) (interfa centers := make([]domain.Center, len(importData.Centers)) for i, center := range importData.Centers { - centers[i] = center.MapToDomain() + centers[i] = *center.MapToDomain() } result, err := c.centersService.ImportCenters(r.Context(), centers, importData.DeleteAll) @@ -255,16 +259,46 @@ func (c *Centers) importCenters(_ http.ResponseWriter, r *http.Request) (interfa return model.MapToCenterDTOs(result), nil } +func (c *Centers) updateCenter(_ http.ResponseWriter, r *http.Request) (interface{}, error) { + centerUUID := chi.URLParam(r, "uuid") + logrus.WithField("uuid", centerUUID).Trace("updateCenter") + + operator, err := c.operatorsService.GetCurrentOperator(r.Context()) + if err != nil { + return nil, err + } + + center, err := c.centersRepository.FindByUUID(r.Context(), centerUUID) + if err != nil { + return nil, err + } + + if center.OperatorUUID != operator.UUID && !security.HasRole(r.Context(), security.RoleAdmin) { + return nil, gorm.ErrRecordNotFound + } + + var editCenterDTO model.EditCenterDTO + if err := api.ParseRequestBody(r, c.validate, &editCenterDTO); err != nil { + return nil, err + } + + editCenterDTO.CopyToDomain(¢er) + if err = c.centersService.Save(r.Context(), ¢er, true); err != nil { + return nil, err + } + return model.CenterDTO{}.MapFromDomain(¢er), nil +} + // getCenterByUUID returns the center with the given uuid. // If the center does not belong to the currently authenticated operator, this method will return an error func (c *Centers) getCenterByUUID(_ http.ResponseWriter, r *http.Request) (interface{}, error) { - uuid := chi.URLParam(r, "uuid") + centerUUID := chi.URLParam(r, "uuid") operator, err := c.operatorsService.GetCurrentOperator(r.Context()) if err != nil { return nil, err } - center, err := c.centersRepository.FindByUUID(r.Context(), uuid) + center, err := c.centersRepository.FindByUUID(r.Context(), centerUUID) if err != nil { return nil, err } diff --git a/src/api/model/centers.go b/src/api/model/centers.go index 4251c29..ccd197b 100644 --- a/src/api/model/centers.go +++ b/src/api/model/centers.go @@ -136,35 +136,35 @@ type EditCenterDTO struct { Note *string `json:"note"` } -func (c EditCenterDTO) MapToDomain() domain.Center { - var enterDate *time.Time +func (c EditCenterDTO) CopyToDomain(dst *domain.Center) *domain.Center { if c.EnterDate != nil { if date, err := time.Parse("02.01.2006", *c.EnterDate); err == nil { - enterDate = &date + dst.EnterDate = &date } } - var leaveDate *time.Time if c.LeaveDate != nil { if date, err := time.Parse("02.01.2006", *c.LeaveDate); err == nil { - leaveDate = &date + dst.LeaveDate = &date } } - return domain.Center{ - UserReference: c.UserReference, - Name: c.Name, - Website: c.Website, - Address: c.Address, - AddressNote: c.AddressNote, - OpeningHours: c.OpeningHours, - Appointment: (*domain.AppointmentType)(c.Appointment), - TestKinds: c.TestKinds, - DCC: c.DCC, - EnterDate: enterDate, - LeaveDate: leaveDate, - Email: c.Email, - } + dst.UserReference = c.UserReference + dst.Name = c.Name + dst.Website = c.Website + dst.Address = c.Address + dst.AddressNote = c.AddressNote + dst.OpeningHours = c.OpeningHours + dst.Appointment = (*domain.AppointmentType)(c.Appointment) + dst.TestKinds = c.TestKinds + dst.DCC = c.DCC + dst.Email = c.Email + + return dst +} + +func (c EditCenterDTO) MapToDomain() *domain.Center { + return c.CopyToDomain(&domain.Center{}) } func (EditCenterDTO) MapFromDomain(center domain.Center) EditCenterDTO { diff --git a/src/api/model/operators.go b/src/api/model/operators.go index d55dbf8..af4b385 100644 --- a/src/api/model/operators.go +++ b/src/api/model/operators.go @@ -5,10 +5,11 @@ import "com.t-systems-mms.cwa/domain" type OperatorDTO struct { UUID string `json:"uuid"` OperatorNumber *string `json:"operatorNumber"` - Name string `json:"name"` - Email *string `json:"email"` + Name string `json:"name" validate:"required"` + Email *string `json:"email" validate:"email"` Logo *string `json:"logo"` MarkerIcon *string `json:"markerIcon"` + ReportReceiver *string `json:"reportReceiver" validate:"oneof=operator center"` } func MapToOperatorDTO(operator *domain.Operator) *OperatorDTO { @@ -35,5 +36,6 @@ func MapToOperatorDTO(operator *domain.Operator) *OperatorDTO { Logo: logo, MarkerIcon: markerIcon, Email: operator.Email, + ReportReceiver: operator.BugReportsReceiver, } } diff --git a/src/api/model/statistics.go b/src/api/model/statistics.go new file mode 100644 index 0000000..ed5e76f --- /dev/null +++ b/src/api/model/statistics.go @@ -0,0 +1,15 @@ +package model + +import "com.t-systems-mms.cwa/repositories" + +type ReportStatisticsDTO struct { + Subject string `json:"subject"` + ReportCount uint `json:"report_count"` +} + +func (dto ReportStatisticsDTO) FromModel(model repositories.ReportStatistics) *ReportStatisticsDTO { + return &ReportStatisticsDTO{ + Subject: model.Subject, + ReportCount: model.Count, + } +} diff --git a/src/api/operators.go b/src/api/operators.go index 458adfa..465097e 100644 --- a/src/api/operators.go +++ b/src/api/operators.go @@ -4,15 +4,15 @@ import ( "bytes" "com.t-systems-mms.cwa/api/model" "com.t-systems-mms.cwa/core/api" + "com.t-systems-mms.cwa/core/util" "com.t-systems-mms.cwa/repositories" "com.t-systems-mms.cwa/services" - "encoding/json" "github.com/go-chi/chi" "github.com/go-chi/jwtauth" + "github.com/go-playground/validator" "github.com/sirupsen/logrus" "github.com/vincent-petithory/dataurl" "image" - "io" "net/http" ) @@ -20,13 +20,18 @@ type Operators struct { chi.Router operatorsRepository repositories.Operators operatorsService services.Operators + validate *validator.Validate } func NewOperatorsAPI(operatorsRepository repositories.Operators, operatorsService services.Operators, auth *jwtauth.JWTAuth) *Operators { + validate := validator.New() + validate.RegisterTagNameFunc(util.JsonTagNameFunc) + operators := &Operators{ Router: chi.NewRouter(), operatorsService: operatorsService, operatorsRepository: operatorsRepository, + validate: validate, } operators.Get("/{operator}/logo", operators.GetOperatorLogo) @@ -48,16 +53,15 @@ func (c *Operators) SaveCurrentOperator(w http.ResponseWriter, r *http.Request) return nil, err } - body, err := io.ReadAll(r.Body) - if err != nil { - return nil, err - } request := model.OperatorDTO{} - if err := json.Unmarshal(body, &request); err != nil { + if err := api.ParseRequestBody(r, c.validate, &request); err != nil { return nil, err } operator.Name = request.Name + operator.Email = request.Email + operator.BugReportsReceiver = request.ReportReceiver + if request.Logo != nil { data, err := dataurl.DecodeString(*request.Logo) if err != nil { diff --git a/src/api/statistics.go b/src/api/statistics.go new file mode 100644 index 0000000..da3f40d --- /dev/null +++ b/src/api/statistics.go @@ -0,0 +1,45 @@ +package api + +import ( + "com.t-systems-mms.cwa/api/model" + "com.t-systems-mms.cwa/core/api" + "com.t-systems-mms.cwa/core/security" + "com.t-systems-mms.cwa/repositories" + "github.com/go-chi/chi" + "github.com/go-chi/jwtauth" + "net/http" +) + +type Statistics struct { + chi.Router + reportsRepository repositories.BugReports +} + +func NewStatisticsAPI(reportsRepository repositories.BugReports, auth *jwtauth.JWTAuth) *Statistics { + statistics := &Statistics{ + Router: chi.NewRouter(), + reportsRepository: reportsRepository, + } + + statistics.Group(func(r chi.Router) { + r.Use(jwtauth.Verifier(auth)) + r.Use(jwtauth.Authenticator) + r.Use(api.RequireRole(security.RoleAdmin)) + + r.Get("/reports", api.Handle(statistics.getReportStatistics)) + }) + return statistics +} + +func (c *Statistics) getReportStatistics(_ http.ResponseWriter, r *http.Request) (interface{}, error) { + stats, err := c.reportsRepository.GetStatistics(r.Context()) + if err != nil { + return nil, err + } + + result := make([]model.ReportStatisticsDTO, len(stats)) + for i, stat := range stats { + result[i] = *model.ReportStatisticsDTO{}.FromModel(stat) + } + return result, nil +} diff --git a/src/cmd/backend/main.go b/src/cmd/backend/main.go index 3f5cc38..69b5112 100644 --- a/src/cmd/backend/main.go +++ b/src/cmd/backend/main.go @@ -96,6 +96,7 @@ func main() { router := chi.NewRouter() router.Use(middleware.DefaultLogger) router.Handle("/metrics", initMetricsHandler(centersRepository, operatorsRepository)) + router.Mount("/api/statistics", api.NewStatisticsAPI(bugReportsRepository, tokenAuth)) router.Mount("/api/centers", api.NewCentersAPI(centersService, centersRepository, bugReportsService, operatorsService, geocoder, tokenAuth)) router.Mount("/api/operators", api.NewOperatorsAPI(operatorsRepository, operatorsService, tokenAuth)) diff --git a/src/core/util/validation.go b/src/core/util/validation.go new file mode 100644 index 0000000..abc0327 --- /dev/null +++ b/src/core/util/validation.go @@ -0,0 +1,14 @@ +package util + +import ( + "reflect" + "strings" +) + +func JsonTagNameFunc(fld reflect.StructField) string { + name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0] + if name == "-" { + return "" + } + return name +} diff --git a/src/repositories/bugreports.go b/src/repositories/bugreports.go index f6c6603..a10fc19 100644 --- a/src/repositories/bugreports.go +++ b/src/repositories/bugreports.go @@ -9,6 +9,11 @@ import ( "time" ) +type ReportStatistics struct { + Subject string + Count uint +} + type BugReports interface { Repository Save(ctx context.Context, center *domain.BugReport) error @@ -18,6 +23,9 @@ type BugReports interface { UpdateLeaderForAll(ctx context.Context, leader string) error FindAllByLeader(ctx context.Context, leader string) ([]domain.BugReport, error) ResetLeader(ctx context.Context, leader string) error + + IncrementReportCount(ctx context.Context, subject string) error + GetStatistics(ctx context.Context) ([]ReportStatistics, error) } type bugReportsRepository struct { @@ -72,3 +80,19 @@ func (b *bugReportsRepository) FindAll(ctx context.Context) ([]domain.BugReport, err := b.GetTX(ctx).Find(&reports).Error return reports, err } + +func (b *bugReportsRepository) IncrementReportCount(ctx context.Context, subject string) error { + return b.GetTX(ctx).Exec("insert into report_statistics (subject, count)"+ + "VALUES (?, 1)"+ + "on conflict (subject) "+ + "do update set count = report_statistics.count + 1", subject).Error +} + +func (b *bugReportsRepository) GetStatistics(ctx context.Context) ([]ReportStatistics, error) { + var statistics []ReportStatistics + err := b.GetTX(ctx). + Raw("select * from report_statistics"). + Scan(&statistics).Error + + return statistics, err +} diff --git a/src/services/bugreports.go b/src/services/bugreports.go index 3fd4a79..c7caa28 100644 --- a/src/services/bugreports.go +++ b/src/services/bugreports.go @@ -106,6 +106,14 @@ func (s *bugReportsService) CreateBugReport(ctx context.Context, centerUUID, sub } err = s.bugReportsRepository.Save(ctx, &report) + if err != nil { + logrus.WithError(err).Error("Error creating bug report") + } + + if err := s.bugReportsRepository.IncrementReportCount(ctx, report.Subject); err != nil { + logrus.WithError(err).Error("Error updating report statistics") + } + createdBugReportsCount.Inc() return report, err } diff --git a/src/services/centers.go b/src/services/centers.go index d993ce5..eefadf1 100644 --- a/src/services/centers.go +++ b/src/services/centers.go @@ -41,12 +41,15 @@ type centersService struct { } func NewCentersService(centersRepository repositories.Centers, operators repositories.Operators, operatorsService Operators, geocoder geocoding.Geocoder) Centers { + validate := validator.New() + validate.RegisterTagNameFunc(util.JsonTagNameFunc) + return ¢ersService{ centersRepository: centersRepository, operators: operators, operatorsService: operatorsService, geocoder: geocoder, - validate: validator.New(), + validate: validate, } }