Skip to content

Commit

Permalink
Merge pull request #3510 from ProjectSidewalk/3496-admin-validate
Browse files Browse the repository at this point in the history
Adds an admin version of Validate
  • Loading branch information
misaugstad authored Mar 5, 2024
2 parents 6b79288 + 9c6f215 commit 7e52e5c
Show file tree
Hide file tree
Showing 26 changed files with 390 additions and 123 deletions.
13 changes: 2 additions & 11 deletions app/controllers/AdminController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import com.mohiva.play.silhouette.api.{Environment, Silhouette}
import com.mohiva.play.silhouette.impl.authenticators.SessionAuthenticator
import com.vividsolutions.jts.geom.Coordinate
import controllers.headers.ProvidesHeader
import controllers.helper.ControllerUtils.parseIntegerList
import controllers.helper.ControllerUtils.{isAdmin, parseIntegerList}
import formats.json.LabelFormat
import formats.json.TaskFormats._
import formats.json.AdminUpdateSubmissionFormats._
Expand Down Expand Up @@ -43,15 +43,6 @@ import scala.concurrent.Future
class AdminController @Inject() (implicit val env: Environment[User, SessionAuthenticator])
extends Silhouette[User, SessionAuthenticator] with ProvidesHeader {

/**
* Checks if the given user is an Administrator.
*/
def isAdmin(user: Option[User]): Boolean = user match {
case Some(user) =>
if (user.role.getOrElse("") == "Administrator" || user.role.getOrElse("") == "Owner") true else false
case _ => false
}

/**
* Loads the admin page.
*/
Expand Down Expand Up @@ -170,7 +161,7 @@ class AdminController @Inject() (implicit val env: Environment[User, SessionAuth
val point = geojson.Point(geojson.LatLng(attribute.lat.toDouble, attribute.lng.toDouble))
val properties = Json.obj(
"attribute_id" -> attribute.globalAttributeId,
"label_type" -> LabelTypeTable.labelTypeIdToLabelType(attribute.labelTypeId),
"label_type" -> LabelTypeTable.labelTypeIdToLabelType(attribute.labelTypeId).get,
"severity" -> attribute.severity
)
Json.obj("type" -> "Feature", "geometry" -> point, "properties" -> properties)
Expand Down
4 changes: 2 additions & 2 deletions app/controllers/AttributeController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ class AttributeController @Inject() (implicit val env: Environment[User, Session
UserAttribute(0,
userSessionId,
thresholds(cluster.labelType),
LabelTypeTable.labelTypeToId(cluster.labelType),
LabelTypeTable.labelTypeToId(cluster.labelType).get,
RegionTable.selectRegionIdOfClosestNeighborhood(cluster.lng, cluster.lat),
cluster.lat,
cluster.lng,
Expand Down Expand Up @@ -203,7 +203,7 @@ class AttributeController @Inject() (implicit val env: Environment[User, Session
GlobalAttribute(0,
globalSessionId,
thresholds(cluster.labelType),
LabelTypeTable.labelTypeToId(cluster.labelType),
LabelTypeTable.labelTypeToId(cluster.labelType).get,
LabelTable.getStreetEdgeIdClosestToLatLng(cluster.lat, cluster.lng).get,
RegionTable.selectRegionIdOfClosestNeighborhood(cluster.lng, cluster.lat),
cluster.lat,
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/LabelController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ class LabelController @Inject() (implicit val env: Environment[User, SessionAuth
val tags: List[Tag] = TagTable.selectAllTags().filter( tag => !excludedTags.contains(tag.tag))
Future.successful(Ok(JsArray(tags.map { tag => Json.obj(
"tag_id" -> tag.tagId,
"label_type" -> LabelTypeTable.labelTypeIdToLabelType(tag.labelTypeId),
"label_type" -> LabelTypeTable.labelTypeIdToLabelType(tag.labelTypeId).get,
"tag" -> tag.tag
)})))
}
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/TaskController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@ class TaskController @Inject() (implicit val env: Environment[User, SessionAuthe

// Insert labels.
for (label: LabelSubmission <- data.labels) {
val labelTypeId: Int = LabelTypeTable.labelTypeToId(label.labelType)
val labelTypeId: Int = LabelTypeTable.labelTypeToId(label.labelType).get

val existingLabel: Option[Label] = if (userOption.isDefined) {
LabelTable.find(label.temporaryLabelId, userOption.get.userId)
Expand Down
109 changes: 88 additions & 21 deletions app/controllers/ValidationController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,24 @@ import javax.inject.Inject
import com.mohiva.play.silhouette.api.{Environment, Silhouette}
import com.mohiva.play.silhouette.impl.authenticators.SessionAuthenticator
import controllers.headers.ProvidesHeader
import controllers.helper.ControllerUtils
import controllers.helper.ControllerUtils.{isAdmin, isMobile}
import controllers.helper.ValidateHelper.AdminValidateParams
import formats.json.CommentSubmissionFormats._
import formats.json.LabelFormat
import models.amt.AMTAssignmentTable
import models.daos.slick.DBTableDefinitions.{DBUser, UserTable}
import models.label.LabelTable
import models.label.LabelTable.LabelValidationMetadata
import models.label.LabelValidationTable
import models.label.{LabelTable, LabelTypeTable, LabelValidationTable}
import models.label.LabelTable.{AdminValidationData, LabelValidationMetadata}
import models.mission.{Mission, MissionSetProgress, MissionTable}
import models.region.{Region, RegionTable}
import models.validation._
import models.user._
import play.api.libs.json._
import play.api.Logger
import play.api.mvc._
import javax.naming.AuthenticationException
import scala.concurrent.Future
import scala.util.Try

/**
* Holds the HTTP requests associated with the validation page.
Expand All @@ -37,14 +40,14 @@ class ValidationController @Inject() (implicit val env: Environment[User, Sessio
*/
def validate = UserAwareAction.async { implicit request =>
val ipAddress: String = request.remoteAddress

request.identity match {
case Some(user) =>
val validationData = getDataForValidationPages(user, ipAddress, labelCount = 10, "Visit_Validate")
val adminParams = AdminValidateParams(adminVersion = false)
val validationData = getDataForValidationPages(user, ipAddress, labelCount = 10, "Visit_Validate", adminParams)
if (validationData._4.missionType != "validation") {
Future.successful(Redirect("/explore"))
} else {
Future.successful(Ok(views.html.validation("Project Sidewalk - Validate", Some(user), validationData._1, validationData._2, validationData._3, validationData._4.numComplete, validationData._5, validationData._6)))
Future.successful(Ok(views.html.validation("Sidewalk - Validate", Some(user), adminParams, validationData._1, validationData._2, validationData._3, validationData._4.numComplete, validationData._5, validationData._6)))
}
case None =>
Future.successful(Redirect(s"/anonSignUp?url=/validate"));
Expand All @@ -59,23 +62,76 @@ class ValidationController @Inject() (implicit val env: Environment[User, Sessio

request.identity match {
case Some(user) =>
val validationData = getDataForValidationPages(user, ipAddress, labelCount = 10, "Visit_MobileValidate")
if (validationData._4.missionType != "validation" || user.role.getOrElse("") == "Turker" || !ControllerUtils.isMobile(request)) {
val adminParams = AdminValidateParams(adminVersion = false)
val validationData = getDataForValidationPages(user, ipAddress, labelCount = 10, "Visit_MobileValidate", adminParams)
if (validationData._4.missionType != "validation" || user.role.getOrElse("") == "Turker" || !isMobile(request)) {
Future.successful(Redirect("/explore"))
} else {
Future.successful(Ok(views.html.mobileValidate("Project Sidewalk - Validate", Some(user), validationData._1, validationData._2, validationData._3, validationData._4.numComplete, validationData._5, validationData._6)))
Future.successful(Ok(views.html.mobileValidate("Sidewalk - Validate", Some(user), validationData._1, validationData._2, validationData._3, validationData._4.numComplete, validationData._5, validationData._6)))
}
case None =>
Future.successful(Redirect(s"/anonSignUp?url=/mobile"));
}
}

/**
* Returns an admin version of the validation page.
* @param labelType Label type or label type ID to validate.
* @param users Comma-separated list of usernames or user IDs to validate (could be mixed).
* @param neighborhoods Comma-separated list of neighborhood names or region IDs to validate (could be mixed).
*/
def adminValidate(labelType: Option[String], users: Option[String], neighborhoods: Option[String]) = UserAwareAction.async { implicit request =>
val ipAddress: String = request.remoteAddress
if (isAdmin(request.identity)) {
// If any inputs are invalid, send back error message. For each input, we check if the input is an integer
// representing a valid ID (label_type_id, user_id, or region_id) or a String representing a valid name for that
// parameter (label_type, username, or region_name).
val possibleLabTypeIds: List[Int] = LabelTable.valLabelTypeIds
val parsedLabelTypeId: Option[Option[Int]] = labelType.map { lType =>
val parsedId: Try[Int] = Try(lType.toInt)
val lTypeIdFromName: Option[Int] = LabelTypeTable.labelTypeToId(lType)
if (parsedId.isSuccess && possibleLabTypeIds.contains(parsedId.get)) parsedId.toOption
else if (lTypeIdFromName.isDefined) lTypeIdFromName
else None
}
val userIdsList: Option[List[Option[String]]] = users.map(_.split(',').map(_.trim).map { userStr =>
val parsedUserId: Option[UUID] = Try(UUID.fromString(userStr)).toOption
val user: Option[DBUser] = parsedUserId.flatMap(u => UserTable.findById(u))
val userId: Option[String] = UserTable.find(userStr).map(_.userId)
if (user.isDefined) Some(userStr) else if (userId.isDefined) Some(userId.get) else None
}.toList)
val neighborhoodIdList: Option[List[Option[Int]]] = neighborhoods.map(_.split(",").map { regionStr =>
val parsedRegionId: Try[Int] = Try(regionStr.toInt)
val regionFromName: Option[Region] = RegionTable.getRegionByName(regionStr)
if (parsedRegionId.isSuccess && RegionTable.getRegion(parsedRegionId.get).isDefined) parsedRegionId.toOption
else if (regionFromName.isDefined) regionFromName.map(_.regionId)
else None
}.toList)

// If any inputs are invalid (even any item in the list of users/regions), send back error message.
if (parsedLabelTypeId.isDefined && parsedLabelTypeId.get.isEmpty) {
Future.successful(BadRequest(s"Invalid label type provided: ${labelType.get}. Valid label types are: ${LabelTypeTable.getAllLabelTypes.filter(l => possibleLabTypeIds.contains(l.labelTypeId)).map(_.labelType).toList.reverse.mkString(", ")}. Or you can use their IDs: ${possibleLabTypeIds.mkString(", ")}."))
} else if (userIdsList.isDefined && userIdsList.get.length != userIdsList.get.flatten.length) {
Future.successful(BadRequest(s"One or more of the users provided were not found; please double check your list of users! You can use either their usernames or user IDs. You provided: ${users.get}"))
} else if (neighborhoodIdList.isDefined && neighborhoodIdList.get.length != neighborhoodIdList.get.flatten.length) {
Future.successful(BadRequest(s"One or more of the neighborhoods provided were not found; please double check your list of neighborhoods! You can use either their names or IDs. You provided: ${neighborhoods.get}"))
} else {
// If all went well, load the data for Admin Validate with the specified filters.
val adminParams: AdminValidateParams = AdminValidateParams(adminVersion = true, parsedLabelTypeId.flatten, userIdsList.map(_.flatten), neighborhoodIdList.map(_.flatten))
val validationData = getDataForValidationPages(request.identity.get, ipAddress, labelCount=10, "Visit_AdminValidate", adminParams)
Future.successful(Ok(views.html.validation("Sidewalk - Admin Validate", request.identity, adminParams, validationData._1, validationData._2, validationData._3, validationData._4.numComplete, validationData._5, validationData._6)))
}
} else {
Future.failed(new AuthenticationException("User is not an administrator"))
}
}

/**
* Get the data needed by the /validate or /mobileValidate endpoints.
*
* @return (mission, labelList, missionProgress, missionSetProgress, hasNextMission, completedValidations)
*/
def getDataForValidationPages(user: User, ipAddress: String, labelCount: Int, visitTypeStr: String): (Option[JsObject], Option[JsValue], Option[JsObject], MissionSetProgress, Boolean, Int) = {
def getDataForValidationPages(user: User, ipAddress: String, labelCount: Int, visitTypeStr: String, adminParams: AdminValidateParams): (Option[JsObject], Option[JsValue], Option[JsObject], MissionSetProgress, Boolean, Int) = {
val timestamp: Timestamp = new Timestamp(Instant.now.toEpochMilli)

WebpageActivityTable.save(WebpageActivity(0, user.userId.toString, ipAddress, visitTypeStr, timestamp))
Expand All @@ -85,12 +141,13 @@ class ValidationController @Inject() (implicit val env: Environment[User, Sessio
else MissionTable.defaultValidationMissionSetProgress

val possibleLabTypeIds: List[Int] = LabelTable.retrievePossibleLabelTypeIds(user.userId, labelCount, None)
.filter(labTypeId => adminParams.labelTypeId.isEmpty || adminParams.labelTypeId.get == labTypeId)
val hasWork: Boolean = possibleLabTypeIds.nonEmpty

val completedValidations: Int = LabelValidationTable.countValidations(user.userId)
// Checks if there are still labels in the database for the user to validate.
if (hasWork && missionSetProgress.missionType == "validation") {
// possibleLabTypeIds can contain [1, 2, 3, 4, 7]. Select ids 1, 2, 3, 4 if possible, o/w choose 7.
// possibleLabTypeIds can contain [1, 2, 3, 4, 7, 9, 10]. Select ids 1, 2, 3, 4, 9, 10 if possible, o/w choose 7.
val possibleIds: List[Int] =
if (possibleLabTypeIds.size > 1) possibleLabTypeIds.filter(_ != 7)
else possibleLabTypeIds
Expand All @@ -99,11 +156,12 @@ class ValidationController @Inject() (implicit val env: Environment[User, Sessio
val mission: Mission = MissionTable.resumeOrCreateNewValidationMission(user.userId,
AMTAssignmentTable.TURKER_PAY_PER_LABEL_VALIDATION, 0.0, validationMissionStr, labelTypeId).get

val labelList: JsValue = getLabelListForValidation(user.userId, labelTypeId, mission)
val labelList: JsValue = getLabelListForValidation(user.userId, labelTypeId, mission, adminParams)
val missionJsObject: JsObject = mission.toJSON
val progressJsObject: JsObject = LabelValidationTable.getValidationProgress(mission.missionId)
val hasDataForMission: Boolean = labelList.toString != "[]"

(Some(missionJsObject), Some(labelList), Some(progressJsObject), missionSetProgress, true, completedValidations)
(Some(missionJsObject), Some(labelList), Some(progressJsObject), missionSetProgress, hasDataForMission, completedValidations)
} else {
// TODO When fixing the mission sequence infrastructure (#1916), this should update that table since there are
// no validation missions that can be done.
Expand All @@ -113,18 +171,27 @@ class ValidationController @Inject() (implicit val env: Environment[User, Sessio

/**
* This gets a random list of labels to validate for this mission.
* @param userId User ID for current user.
* @param labelType Label type id of labels to retrieve.
* @param mission Mission object for the current mission
* @return JsValue containing a list of labels.
* @param userId User ID for current user.
* @param labelType Label type id of labels to retrieve.
* @param mission Mission object for the current mission
* @param adminParams Parameters related to the admin version of the validate page.
* @return JsValue containing a list of labels.
*/
def getLabelListForValidation(userId: UUID, labelType: Int, mission: Mission): JsValue = {
def getLabelListForValidation(userId: UUID, labelType: Int, mission: Mission, adminParams: AdminValidateParams): JsValue = {
val labelsProgress: Int = mission.labelsProgress.get
val labelsToValidate: Int = MissionTable.validationMissionLabelsToRetrieve
val labelsToRetrieve: Int = labelsToValidate - labelsProgress

val labelMetadata: Seq[LabelValidationMetadata] = LabelTable.retrieveLabelListForValidation(userId, labelsToRetrieve, labelType, skippedLabelId = None)
val labelMetadataJsonSeq: Seq[JsObject] = labelMetadata.map(label => LabelFormat.validationLabelMetadataToJson(label))
// Get list of labels and their metadata for Validate page. Get extra metadata if it's for Admin Validate.
val labelMetadata: Seq[LabelValidationMetadata] = LabelTable.retrieveLabelListForValidation(userId, labelsToRetrieve, labelType, adminParams.userIds, adminParams.neighborhoodIds)
val labelMetadataJsonSeq: Seq[JsObject] = if (adminParams.adminVersion) {
val adminData: List[AdminValidationData] = LabelTable.getExtraAdminValidateData(labelMetadata.map(_.labelId).toList)
labelMetadata.sortBy(_.labelId).zip(adminData.sortBy(_.labelId))
.map(label => LabelFormat.validationLabelMetadataToJson(label._1, Some(label._2)))
} else {
labelMetadata.map(l => LabelFormat.validationLabelMetadataToJson(l))
}

val labelMetadataJson : JsValue = Json.toJson(labelMetadataJsonSeq)
labelMetadataJson
}
Expand Down
Loading

0 comments on commit 7e52e5c

Please sign in to comment.