mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 04:14:39 +03:00
Fix performer validation (#4248)
* Fix performer validation * Add tests * Rename QueryCount argument * Minor refactoring * Add duplicate alias validation * Make UI alias validation also case-insensitive
This commit is contained in:
@@ -1,39 +1,260 @@
|
||||
package performer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
)
|
||||
|
||||
func ValidateDeathDate(performer *models.Performer, birthdate *string, deathDate *string) error {
|
||||
// don't validate existing values
|
||||
if birthdate == nil && deathDate == nil {
|
||||
return nil
|
||||
var (
|
||||
ErrNameMissing = errors.New("performer name must not be blank")
|
||||
)
|
||||
|
||||
type NotFoundError struct {
|
||||
id int
|
||||
}
|
||||
|
||||
func (e *NotFoundError) Error() string {
|
||||
return fmt.Sprintf("performer with id %d not found", e.id)
|
||||
}
|
||||
|
||||
type NameExistsError struct {
|
||||
Name string
|
||||
Disambiguation string
|
||||
}
|
||||
|
||||
func (e *NameExistsError) Error() string {
|
||||
if e.Disambiguation != "" {
|
||||
return fmt.Sprintf("performer with name '%s' and disambiguation '%s' already exists", e.Name, e.Disambiguation)
|
||||
}
|
||||
return fmt.Sprintf("performer with name '%s' already exists", e.Name)
|
||||
}
|
||||
|
||||
type DuplicateAliasError struct {
|
||||
Alias string
|
||||
}
|
||||
|
||||
func (e *DuplicateAliasError) Error() string {
|
||||
return fmt.Sprintf("performer contains duplicate alias '%s'", e.Alias)
|
||||
}
|
||||
|
||||
type DeathDateError struct {
|
||||
Birthdate models.Date
|
||||
DeathDate models.Date
|
||||
}
|
||||
|
||||
func (e *DeathDateError) Error() string {
|
||||
return fmt.Sprintf("death date %s should be after birthdate %s", e.DeathDate, e.Birthdate)
|
||||
}
|
||||
|
||||
func ValidateCreate(ctx context.Context, performer models.Performer, qb models.PerformerReader) error {
|
||||
if err := ValidateName(ctx, performer.Name, performer.Disambiguation, qb); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if performer != nil {
|
||||
if birthdate == nil && performer.Birthdate != nil {
|
||||
s := performer.Birthdate.String()
|
||||
birthdate = &s
|
||||
}
|
||||
if deathDate == nil && performer.DeathDate != nil {
|
||||
s := performer.DeathDate.String()
|
||||
deathDate = &s
|
||||
}
|
||||
if err := ValidateAliases(performer.Name, performer.Aliases); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if birthdate == nil || deathDate == nil || *birthdate == "" || *deathDate == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
f, _ := utils.ParseDateStringAsTime(*birthdate)
|
||||
t, _ := utils.ParseDateStringAsTime(*deathDate)
|
||||
|
||||
if f.After(t) {
|
||||
return errors.New("the date of death should be higher than the date of birth")
|
||||
if err := ValidateDeathDate(performer.Birthdate, performer.DeathDate); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ValidateUpdate(ctx context.Context, id int, partial models.PerformerPartial, qb models.PerformerReader) error {
|
||||
existing, err := qb.Find(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if existing == nil {
|
||||
return &NotFoundError{id}
|
||||
}
|
||||
|
||||
if err := ValidateUpdateName(ctx, *existing, partial.Name, partial.Disambiguation, qb); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := existing.LoadAliases(ctx, qb); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ValidateUpdateAliases(*existing, partial.Name, partial.Aliases); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := ValidateUpdateDeathDate(*existing, partial.Birthdate, partial.DeathDate); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateName(ctx context.Context, name string, disambig string, existingID *int, qb models.PerformerQueryer) error {
|
||||
performerFilter := models.PerformerFilterType{
|
||||
Name: &models.StringCriterionInput{
|
||||
Value: name,
|
||||
Modifier: models.CriterionModifierEquals,
|
||||
},
|
||||
}
|
||||
|
||||
if disambig != "" {
|
||||
performerFilter.Disambiguation = &models.StringCriterionInput{
|
||||
Value: disambig,
|
||||
Modifier: models.CriterionModifierEquals,
|
||||
}
|
||||
}
|
||||
|
||||
if existingID == nil {
|
||||
// creating: error if any existing performer matches
|
||||
|
||||
pp := 1
|
||||
findFilter := models.FindFilterType{
|
||||
PerPage: &pp,
|
||||
}
|
||||
|
||||
count, err := qb.QueryCount(ctx, &performerFilter, &findFilter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if count > 0 {
|
||||
return &NameExistsError{
|
||||
Name: name,
|
||||
Disambiguation: disambig,
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
} else {
|
||||
// updating: check for matches, but ignore self
|
||||
|
||||
pp := 2
|
||||
findFilter := models.FindFilterType{
|
||||
PerPage: &pp,
|
||||
}
|
||||
|
||||
conflicts, _, err := qb.Query(ctx, &performerFilter, &findFilter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(conflicts) > 0 {
|
||||
// valid if the only conflict is the existing performer
|
||||
if len(conflicts) > 1 || conflicts[0].ID != *existingID {
|
||||
return &NameExistsError{
|
||||
Name: name,
|
||||
Disambiguation: disambig,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateName returns an error if the performer name and disambiguation provided is used by another performer.
|
||||
func ValidateName(ctx context.Context, name string, disambig string, qb models.PerformerQueryer) error {
|
||||
if name == "" {
|
||||
return ErrNameMissing
|
||||
}
|
||||
|
||||
return validateName(ctx, name, disambig, nil, qb)
|
||||
}
|
||||
|
||||
// ValidateUpdateName performs the same check as ValidateName, but is used when modifying an existing performer.
|
||||
func ValidateUpdateName(ctx context.Context, existing models.Performer, name models.OptionalString, disambig models.OptionalString, qb models.PerformerQueryer) error {
|
||||
// if neither name nor disambig is set, don't check anything
|
||||
if !name.Set && !disambig.Set {
|
||||
return nil
|
||||
}
|
||||
|
||||
newName := existing.Name
|
||||
if name.Set {
|
||||
newName = name.Value
|
||||
}
|
||||
|
||||
if newName == "" {
|
||||
return ErrNameMissing
|
||||
}
|
||||
|
||||
newDisambig := existing.Disambiguation
|
||||
if disambig.Set {
|
||||
newDisambig = disambig.Value
|
||||
}
|
||||
|
||||
return validateName(ctx, newName, newDisambig, &existing.ID, qb)
|
||||
}
|
||||
|
||||
func ValidateAliases(name string, aliases models.RelatedStrings) error {
|
||||
if !aliases.Loaded() {
|
||||
return nil
|
||||
}
|
||||
|
||||
m := make(map[string]bool)
|
||||
nameL := strings.ToLower(name)
|
||||
m[nameL] = true
|
||||
|
||||
for _, alias := range aliases.List() {
|
||||
aliasL := strings.ToLower(alias)
|
||||
if m[aliasL] {
|
||||
return &DuplicateAliasError{alias}
|
||||
}
|
||||
m[aliasL] = true
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ValidateUpdateAliases(existing models.Performer, name models.OptionalString, aliases *models.UpdateStrings) error {
|
||||
// if neither name nor aliases is set, don't check anything
|
||||
if !name.Set && aliases == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
newName := existing.Name
|
||||
if name.Set {
|
||||
newName = name.Value
|
||||
}
|
||||
|
||||
newAliases := aliases.Apply(existing.Aliases.List())
|
||||
|
||||
return ValidateAliases(newName, models.NewRelatedStrings(newAliases))
|
||||
}
|
||||
|
||||
// ValidateDeathDate returns an error if the birthdate is after the death date.
|
||||
func ValidateDeathDate(birthdate *models.Date, deathDate *models.Date) error {
|
||||
if birthdate == nil || deathDate == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if birthdate.After(*deathDate) {
|
||||
return &DeathDateError{Birthdate: *birthdate, DeathDate: *deathDate}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateUpdateDeathDate performs the same check as ValidateDeathDate, but is used when modifying an existing performer.
|
||||
func ValidateUpdateDeathDate(existing models.Performer, birthdate models.OptionalDate, deathDate models.OptionalDate) error {
|
||||
// if neither birthdate nor deathDate is set, don't check anything
|
||||
if !birthdate.Set && !deathDate.Set {
|
||||
return nil
|
||||
}
|
||||
|
||||
newBirthdate := existing.Birthdate
|
||||
if birthdate.Set {
|
||||
newBirthdate = birthdate.Ptr()
|
||||
}
|
||||
|
||||
newDeathDate := existing.DeathDate
|
||||
if deathDate.Set {
|
||||
newDeathDate = deathDate.Ptr()
|
||||
}
|
||||
|
||||
return ValidateDeathDate(newBirthdate, newDeathDate)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user