Skip to content

Commit da54838

Browse files
authored
Remove deleted challenges from uniqueness validation for challenge names (#1204)
* remove deleted challenges from uniqueness validation for challenge names * run formatting * remove incorrect down
1 parent b5c6fa7 commit da54838

3 files changed

Lines changed: 53 additions & 1 deletion

File tree

app/org/maproulette/models/dal/ChallengeDAL.scala

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -527,6 +527,24 @@ class ChallengeDAL @Inject() (
527527
}
528528

529529
this.permission.hasObjectWriteAccess(challenge, user)
530+
531+
// Check for existing non-deleted challenge with same name in same project
532+
this.withMRConnection { implicit c =>
533+
val existingChallenge = SQL"""
534+
SELECT id FROM challenges
535+
WHERE parent_id = ${challenge.general.parent}
536+
AND LOWER(name) = LOWER(${challenge.name})
537+
AND (deleted = false OR deleted IS NULL)
538+
LIMIT 1
539+
""".as(SqlParser.long("id").singleOpt)
540+
541+
if (existingChallenge.isDefined) {
542+
throw new UniqueViolationException(
543+
s"Challenge with name ${challenge.name} already exists in the database"
544+
)
545+
}
546+
}
547+
530548
this.cacheManager.withOptionCaching { () =>
531549
val insertedChallenge =
532550
this.withMRTransaction { implicit c =>
@@ -553,7 +571,7 @@ class ChallengeDAL @Inject() (
553571
${challenge.extra.limitReviewTags}, ${challenge.extra.taskStyles}, ${challenge.general.requiresLocal}, ${challenge.extra.isArchived},
554572
${challenge.extra.reviewSetting}, ${challenge.extra.datasetUrl}, ${challenge.requireConfirmation}, ${challenge.requireRejectReason},
555573
${asJson(challenge.extra.taskWidgetLayout.getOrElse(Json.parse("{}")))}
556-
) ON CONFLICT(parent_id, LOWER(name)) DO NOTHING RETURNING #${this.retrieveColumns}"""
574+
) RETURNING #${this.retrieveColumns}"""
557575
.as(this.parser.*)
558576
.headOption
559577
}
@@ -656,6 +674,25 @@ class ChallengeDAL @Inject() (
656674
val name = (updates \ "name").asOpt[String].getOrElse(cachedItem.name)
657675
val ownerId = (updates \ "ownerId").asOpt[Long].getOrElse(cachedItem.general.owner)
658676
val parentId = (updates \ "parentId").asOpt[Long].getOrElse(cachedItem.general.parent)
677+
678+
// Check if name or parent changed and if so, validate uniqueness
679+
if (name != cachedItem.name || parentId != cachedItem.general.parent) {
680+
val existingChallenge = SQL"""
681+
SELECT id FROM challenges
682+
WHERE parent_id = $parentId
683+
AND LOWER(name) = LOWER($name)
684+
AND (deleted = false OR deleted IS NULL)
685+
AND id != $id
686+
LIMIT 1
687+
""".as(SqlParser.long("id").singleOpt)
688+
689+
if (existingChallenge.isDefined) {
690+
throw new UniqueViolationException(
691+
s"Challenge with name $name already exists in the database"
692+
)
693+
}
694+
}
695+
659696
val difficulty =
660697
(updates \ "difficulty").asOpt[Int].getOrElse(cachedItem.general.difficulty)
661698
val description =

app/org/maproulette/models/dal/ParentDAL.scala

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ trait ParentDAL[Key, T <: BaseObject[Key], C <: BaseObject[Key]]
118118
val query =
119119
s"""SELECT ${this.childColumns} FROM ${this.childTable}
120120
WHERE parent_id = {id} ${this.enabled(onlyEnabled)}
121+
AND (deleted = false OR deleted IS NULL)
121122
${this.searchField("name")}
122123
${this.order(Some(orderColumn), orderDirection)}
123124
LIMIT ${this.sqlLimit(limit)} OFFSET {offset}"""
@@ -155,6 +156,7 @@ trait ParentDAL[Key, T <: BaseObject[Key], C <: BaseObject[Key]]
155156
s"""SELECT ${this.childColumns} FROM ${this.childTable} c
156157
INNER JOIN ${this.tableName} p ON p.id = c.parent_id
157158
WHERE p.name = LOWER({name}) ${this.enabled(onlyEnabled, "p")}
159+
AND (c.deleted = false OR c.deleted IS NULL)
158160
${this.searchField("c.name")}
159161
${this.order(Some(orderColumn), orderDirection, "c")}
160162
LIMIT ${this.sqlLimit(limit)} OFFSET {offset}"""
@@ -183,6 +185,7 @@ trait ParentDAL[Key, T <: BaseObject[Key], C <: BaseObject[Key]]
183185
val query =
184186
s"""SELECT COUNT(*) as TotalChildren FROM ${this.childTable}
185187
|WHERE parent_id = {id} ${this.searchField("name")}
188+
|AND (deleted = false OR deleted IS NULL)
186189
|${this.enabled(onlyEnabled)}""".stripMargin
187190
SQL(query)
188191
.on(

conf/evolutions/default/105.sql

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# --- MapRoulette Scheme
2+
3+
# --- !Ups
4+
-- Modify unique constraint on (parent_id, name) to exclude deleted challenges
5+
-- This allows challenge names to be reused after deletion
6+
DROP INDEX IF EXISTS idx_challenges_parent_id_name;;
7+
8+
CREATE UNIQUE INDEX idx_challenges_parent_id_name ON challenges (parent_id, lower(name))
9+
WHERE (deleted = false OR deleted IS NULL);;
10+
11+
# --- !Downs
12+
DROP INDEX IF EXISTS idx_challenges_parent_id_name;;

0 commit comments

Comments
 (0)