Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds an admin version of Validate #3510

Merged
merged 13 commits into from
Mar 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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