From 9bf1bd8ee3b6e632cb58575d7d077e25d4e793a5 Mon Sep 17 00:00:00 2001 From: Mikey Saugstad Date: Thu, 29 Feb 2024 14:25:42 -0800 Subject: [PATCH 01/12] adds an Admin Validate page with extra info when validating --- app/controllers/AdminController.scala | 11 +--- app/controllers/ValidationController.scala | 51 ++++++++++++++----- .../ValidationTaskController.scala | 24 ++++++--- app/controllers/helper/ControllerUtils.scala | 8 +++ app/formats/json/LabelFormat.scala | 13 +++-- .../ValidationTaskSubmissionFormats.scala | 3 +- app/models/label/LabelTable.scala | 24 +++++++++ app/views/navbar.scala.html | 8 +++ app/views/validation.scala.html | 12 ++++- conf/routes | 1 + .../javascripts/SVValidate/css/svv-status.css | 8 ++- public/javascripts/SVValidate/src/Main.js | 1 + .../javascripts/SVValidate/src/data/Form.js | 5 +- .../SVValidate/src/keyboard/Keyboard.js | 2 +- .../javascripts/SVValidate/src/label/Label.js | 32 ++++++++++-- .../src/panorama/PanoramaContainer.js | 19 ++++++- 16 files changed, 178 insertions(+), 44 deletions(-) diff --git a/app/controllers/AdminController.scala b/app/controllers/AdminController.scala index e44a2545f7..8aa4efd60f 100644 --- a/app/controllers/AdminController.scala +++ b/app/controllers/AdminController.scala @@ -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._ @@ -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. */ diff --git a/app/controllers/ValidationController.scala b/app/controllers/ValidationController.scala index b4f46af3af..79117e6a05 100644 --- a/app/controllers/ValidationController.scala +++ b/app/controllers/ValidationController.scala @@ -8,12 +8,13 @@ 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 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.LabelTable.{AdminValidationData, LabelValidationMetadata} import models.label.LabelValidationTable import models.mission.{Mission, MissionSetProgress, MissionTable} import models.validation._ @@ -21,6 +22,7 @@ import models.user._ import play.api.libs.json._ import play.api.Logger import play.api.mvc._ +import javax.naming.AuthenticationException import scala.concurrent.Future /** @@ -37,14 +39,13 @@ 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 validationData = getDataForValidationPages(user, ipAddress, labelCount = 10, "Visit_Validate", adminVersion=false) 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), adminVersion=false, validationData._1, validationData._2, validationData._3, validationData._4.numComplete, validationData._5, validationData._6))) } case None => Future.successful(Redirect(s"/anonSignUp?url=/validate")); @@ -59,23 +60,36 @@ class ValidationController @Inject() (implicit val env: Environment[User, Sessio request.identity match { case Some(user) => - val validationData = getDataForValidationPages(user, ipAddress, labelCount = 10, "Visit_MobileValidate") + val validationData = getDataForValidationPages(user, ipAddress, labelCount = 10, "Visit_MobileValidate", adminVersion=false) if (validationData._4.missionType != "validation" || user.role.getOrElse("") == "Turker" || !ControllerUtils.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. + */ + def adminValidate = UserAwareAction.async { implicit request => + val ipAddress: String = request.remoteAddress + if (isAdmin(request.identity)) { + val validationData = getDataForValidationPages(request.identity.get, ipAddress, labelCount = 10, "Visit_AdminValidate", adminVersion=true) + Future.successful(Ok(views.html.validation("Sidewalk - Admin Validate", request.identity, adminVersion=true, 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, adminVersion: Boolean): (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)) @@ -99,7 +113,7 @@ 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, adminVersion) val missionJsObject: JsObject = mission.toJSON val progressJsObject: JsObject = LabelValidationTable.getValidationProgress(mission.missionId) @@ -113,18 +127,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 adminVersion Whether to include data only shown on admin version of the site. + * @return JsValue containing a list of labels. */ - def getLabelListForValidation(userId: UUID, labelType: Int, mission: Mission): JsValue = { + def getLabelListForValidation(userId: UUID, labelType: Int, mission: Mission, adminVersion: Boolean): JsValue = { val labelsProgress: Int = mission.labelsProgress.get val labelsToValidate: Int = MissionTable.validationMissionLabelsToRetrieve val labelsToRetrieve: Int = labelsToValidate - labelsProgress + // Get list of labels and their metadata for Validate page. Get extra data if it's for Admin Validate. val labelMetadata: Seq[LabelValidationMetadata] = LabelTable.retrieveLabelListForValidation(userId, labelsToRetrieve, labelType, skippedLabelId = None) - val labelMetadataJsonSeq: Seq[JsObject] = labelMetadata.map(label => LabelFormat.validationLabelMetadataToJson(label)) + val labelMetadataJsonSeq: Seq[JsObject] = if (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 } diff --git a/app/controllers/ValidationTaskController.scala b/app/controllers/ValidationTaskController.scala index 849fc7b26d..d656c01101 100644 --- a/app/controllers/ValidationTaskController.scala +++ b/app/controllers/ValidationTaskController.scala @@ -6,11 +6,11 @@ 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.sendSciStarterContributions +import controllers.helper.ControllerUtils.{isAdmin, sendSciStarterContributions} import formats.json.ValidationTaskSubmissionFormats._ import models.amt.AMTAssignmentTable import models.label._ -import models.label.LabelTable.LabelValidationMetadata +import models.label.LabelTable.{AdminValidationData, LabelValidationMetadata} import models.mission.{Mission, MissionTable} import models.user.{User, UserStatTable} import models.validation._ @@ -39,6 +39,7 @@ class ValidationTaskController @Inject() (implicit val env: Environment[User, Se */ def processValidationTaskSubmissions(data: ValidationTaskSubmission, remoteAddress: String, identity: Option[User]) = { val userOption = identity + val adminVersion: Boolean = data.adminVersion && isAdmin(userOption) val currTime = new Timestamp(data.timestamp) ValidationTaskInteractionTable.saveMultiple(data.interactions.map { interaction => ValidationTaskInteraction(0, interaction.missionId, interaction.action, interaction.gsvPanoramaId, @@ -81,7 +82,7 @@ class ValidationTaskController @Inject() (implicit val env: Environment[User, Se // Load new mission, generate label list for validation. case Some (nextMissionLabelTypeId) => val possibleNewMission: Option[Mission] = updateMissionTable(userOption, missionProgress, Some(nextMissionLabelTypeId)) - val labelList: Option[JsValue] = getLabelList(userOption, missionProgress, nextMissionLabelTypeId) + val labelList: Option[JsValue] = getLabelList(userOption, missionProgress, nextMissionLabelTypeId, adminVersion) val progress: Option[JsObject] = Some(LabelValidationTable.getValidationProgress(possibleNewMission.get.missionId)) ValidationTaskPostReturnValue(Some (true), possibleNewMission, labelList, progress) case None => @@ -246,12 +247,13 @@ class ValidationTaskController @Inject() (implicit val env: Environment[User, Se * * @param user * @param missionProgress Metadata for this mission + * @param adminVersion Whether to include data only shown on admin version of the site. * @return List of label metadata (if this mission is complete). */ - def getLabelList(user: Option[User], missionProgress: ValidationMissionProgress, labelTypeId: Int): Option[JsValue] = { + def getLabelList(user: Option[User], missionProgress: ValidationMissionProgress, labelTypeId: Int, adminVersion: Boolean): Option[JsValue] = { val userId: UUID = user.get.userId if (missionProgress.completed) { - Some(getLabelListForValidation(userId, MissionTable.validationMissionLabelsToRetrieve, labelTypeId)) + Some(getLabelListForValidation(userId, MissionTable.validationMissionLabelsToRetrieve, labelTypeId, adminVersion)) } else { None } @@ -263,11 +265,19 @@ class ValidationTaskController @Inject() (implicit val env: Environment[User, Se * @param userId User ID of the current user. * @param n Number of labels to retrieve for this list. * @param labelTypeId Label Type to retrieve + * @param adminVersion Whether to include data only shown on admin version of the site. * @return JsValue containing a list of labels. */ - def getLabelListForValidation(userId: UUID, n: Int, labelTypeId: Int): JsValue = { + def getLabelListForValidation(userId: UUID, n: Int, labelTypeId: Int, adminVersion: Boolean): JsValue = { + // Get list of labels and their metadata for Validate page. Get extra data if it's for Admin Validate. val labelMetadata: Seq[LabelValidationMetadata] = LabelTable.retrieveLabelListForValidation(userId, n, labelTypeId, skippedLabelId = None) - val labelMetadataJsonSeq: Seq[JsObject] = labelMetadata.map(LabelFormat.validationLabelMetadataToJson) + val labelMetadataJsonSeq: Seq[JsObject] = if (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 } diff --git a/app/controllers/helper/ControllerUtils.scala b/app/controllers/helper/ControllerUtils.scala index 2ad7a0f314..69d9623ee2 100644 --- a/app/controllers/helper/ControllerUtils.scala +++ b/app/controllers/helper/ControllerUtils.scala @@ -1,5 +1,6 @@ package controllers.helper +import models.user.User import play.api.{Logger, Play} import play.api.Play.current import play.api.mvc.Request @@ -30,6 +31,13 @@ object ControllerUtils { }) } + /** + * Checks if the given user is an Administrator. + */ + def isAdmin(user: Option[User]): Boolean = { + user.map(u => u.role.getOrElse("") == "Administrator" || u.role.getOrElse("") == "Owner").getOrElse(false) + } + def sha256Hash(text: String) : String = String.format("%064x", new java.math.BigInteger(1, java.security.MessageDigest.getInstance("SHA-256").digest(text.getBytes("UTF-8")))) /** diff --git a/app/formats/json/LabelFormat.scala b/app/formats/json/LabelFormat.scala index 2925f1a992..0552b47ac3 100644 --- a/app/formats/json/LabelFormat.scala +++ b/app/formats/json/LabelFormat.scala @@ -2,7 +2,7 @@ package formats.json import controllers.helper.GoogleMapsHelper import models.gsv.GSVDataSlim -import models.label.LabelTable.{LabelMetadata, LabelMetadataUserDash, LabelValidationMetadata} +import models.label.LabelTable.{AdminValidationData, LabelMetadata, LabelMetadataUserDash, LabelValidationMetadata} import java.sql.Timestamp import models.label._ import play.api.libs.json._ @@ -61,7 +61,7 @@ object LabelFormat { (__ \ "camera_pitch").writeNullable[Float] )(unlift(GSVDataSlim.unapply)) - def validationLabelMetadataToJson(labelMetadata: LabelValidationMetadata): JsObject = { + def validationLabelMetadataToJson(labelMetadata: LabelValidationMetadata, adminData: Option[AdminValidationData] = None): JsObject = { Json.obj( "label_id" -> labelMetadata.labelId, "label_type" -> labelMetadata.labelType, @@ -83,7 +83,14 @@ object LabelFormat { "disagree_count" -> labelMetadata.disagreeCount, "notsure_count" -> labelMetadata.notsureCount, "user_validation" -> labelMetadata.userValidation.map(LabelValidationTable.validationOptions.get), - "tags" -> labelMetadata.tags + "tags" -> labelMetadata.tags, + "admin_data" -> adminData.map(ad => Json.obj( + "username" -> ad.username, + "previous_validations" -> ad.previousValidations.map(prevVal => Json.obj( + "username" -> prevVal._1, + "validation" -> LabelValidationTable.validationOptions.get(prevVal._2) + )) + )) ) } diff --git a/app/formats/json/ValidationTaskSubmissionFormats.scala b/app/formats/json/ValidationTaskSubmissionFormats.scala index 82dbb2bcd7..9a6079f530 100644 --- a/app/formats/json/ValidationTaskSubmissionFormats.scala +++ b/app/formats/json/ValidationTaskSubmissionFormats.scala @@ -12,7 +12,7 @@ object ValidationTaskSubmissionFormats { case class LabelValidationSubmission(labelId: Int, missionId: Int, validationResult: Int, canvasX: Option[Int], canvasY: Option[Int], heading: Float, pitch: Float, zoom: Float, canvasHeight: Int, canvasWidth: Int, startTimestamp: Long, endTimestamp: Long, source: String) case class SkipLabelSubmission(labels: Seq[LabelValidationSubmission]) case class ValidationMissionProgress(missionId: Int, missionType: String, labelsProgress: Int, labelTypeId: Int, completed: Boolean, skipped: Boolean) - case class ValidationTaskSubmission(interactions: Seq[InteractionSubmission], environment: EnvironmentSubmission, labels: Seq[LabelValidationSubmission], missionProgress: Option[ValidationMissionProgress], timestamp: Long) + case class ValidationTaskSubmission(interactions: Seq[InteractionSubmission], environment: EnvironmentSubmission, labels: Seq[LabelValidationSubmission], missionProgress: Option[ValidationMissionProgress], adminVersion: Boolean, timestamp: Long) case class LabelMapValidationSubmission(labelId: Int, labelType: String, validationResult: Int, canvasX: Option[Int], canvasY: Option[Int], heading: Float, pitch: Float, zoom: Float, canvasHeight: Int, canvasWidth: Int, startTimestamp: Long, endTimestamp: Long, source: String) implicit val environmentSubmissionReads: Reads[EnvironmentSubmission] = ( @@ -74,6 +74,7 @@ object ValidationTaskSubmissionFormats { (JsPath \ "environment").read[EnvironmentSubmission] and (JsPath \ "labels").read[Seq[LabelValidationSubmission]] and (JsPath \ "missionProgress").readNullable[ValidationMissionProgress] and + (JsPath \ "adminVersion").read[Boolean] and (JsPath \ "timestamp").read[Long] )(ValidationTaskSubmission.apply _) diff --git a/app/models/label/LabelTable.scala b/app/models/label/LabelTable.scala index b2608e954a..d3577584d9 100644 --- a/app/models/label/LabelTable.scala +++ b/app/models/label/LabelTable.scala @@ -172,6 +172,9 @@ object LabelTable { agreeCount: Int, disagreeCount: Int, notsureCount: Int, userValidation: Option[Int]) extends BasicLabelMetadata + // Extra data to include with validations for Admin Validate. Includes usernames and previous validators. + case class AdminValidationData(labelId: Int, username: String, previousValidations: List[(String, Int)]) + case class ResumeLabelMetadata(labelData: Label, labelType: String, pointData: LabelPoint, panoLat: Option[Float], panoLng: Option[Float], cameraHeading: Option[Float], cameraPitch: Option[Float], panoWidth: Int, panoHeight: Int, tagIds: List[Int]) @@ -633,6 +636,27 @@ object LabelTable { selectedLabels } + /** + * Get additional info about a label for use by admins on Admin Validate. + * @param labelIds + * @return + */ + def getExtraAdminValidateData(labelIds: List[Int]): List[AdminValidationData] = db.withSession { implicit session => + labels.filter(_.labelId inSet labelIds) + // Inner join label -> mission -> sidewalk_user to get username of person who placed the label. + .innerJoin(missions).on(_.missionId === _.missionId) + .innerJoin(users).on(_._2.userId === _.userId) + // Left join label -> label_validation -> sidewalk_user to get username & validation result of ppl who validated. + .leftJoin(labelValidations).on(_._1._1.labelId === _.labelId) + .leftJoin(users).on(_._2.userId === _.userId) + .map(x => (x._1._1._1._1.labelId, x._1._1._2.username, x._2.username.?, x._1._2.validationResult.?)).list + // Turn the left joined validators into lists of tuples. + .groupBy(l => (l._1, l._2)) // Group by label_id and username from the placed label. + .map(x => (x._1._1, x._1._2, x._2.map(y => (y._3, y._4)))).toList + .map(y => (y._1, y._2, y._3.collect({ case (Some(a), Some(b)) => (a, b) }))) + .map(AdminValidationData.tupled) + } + /** * Retrieves n labels of specified label type, severities, and tags. If no label type supplied, split across types. * diff --git a/app/views/navbar.scala.html b/app/views/navbar.scala.html index 19f0df80e1..8d84c3d472 100644 --- a/app/views/navbar.scala.html +++ b/app/views/navbar.scala.html @@ -192,6 +192,11 @@ @Messages("navbar.admin") +
  • + + @Messages("navbar.admin") @Messages("navbar.validate") + +
  • }
  • @Messages("navbar.signout") @@ -310,6 +315,9 @@ $("#navbar-admin-btn").on('click', function() { logWebpageActivity("Click_module=ToAdmin"); }); + $("#navbar-admin-validate-btn").on('click', function() { + logWebpageActivity("Click_module=ToAdminValidate"); + }); $("#navbar-dashboard-btn").on('click', function() { logWebpageActivity("Click_module=ToDashboard") }); diff --git a/app/views/validation.scala.html b/app/views/validation.scala.html index 4cc707631d..590fe7e80b 100644 --- a/app/views/validation.scala.html +++ b/app/views/validation.scala.html @@ -2,7 +2,7 @@ @import models.amt.AMTAssignmentTable @import play.api.libs.json.JsValue -@(title: String, user: Option[User] = None, mission: Option[JsValue], labelList: Option[JsValue], progress: Option[JsValue], missionSetProgress: Int, hasNextMission: Boolean, completedValidations: Int)(implicit lang: Lang) +@(title: String, user: Option[User] = None, adminVersion: Boolean, mission: Option[JsValue], labelList: Option[JsValue], progress: Option[JsValue], missionSetProgress: Int, hasNextMission: Boolean, completedValidations: Int)(implicit lang: Lang) @main(title, Some("/validate")) { @navbar(user, Some("/validate")) @@ -191,6 +191,7 @@

    + @if(!adminVersion) {

    @Html(Messages("validate.right.ui.correct.examples")) @@ -213,6 +214,14 @@

    + } else { +
    +

    Admin Info

    +

    Username

    +

    +

    Previous Validations

    +
    + }
    @@ -269,6 +278,7 @@

    param.canvasHeight = 440; param.canvasWidth = 720; param.language = "@lang.code"; + param.adminVersion = @adminVersion; param.modalText = { 1: "@Messages("labeling.guide.curb.ramp.summary")", 2: "@Messages("labeling.guide.curb.ramp.summary")", diff --git a/conf/routes b/conf/routes index e3b10cc17b..9137c5bd17 100644 --- a/conf/routes +++ b/conf/routes @@ -102,6 +102,7 @@ GET /audit/street/:id @controllers.AuditC # Label validation tasks GET /validate @controllers.ValidationController.validate +GET /adminValidate @controllers.ValidationController.adminValidate POST /validate/comment @controllers.ValidationController.postComment # Task API. diff --git a/public/javascripts/SVValidate/css/svv-status.css b/public/javascripts/SVValidate/css/svv-status.css index 68e9464a67..3ae3e01160 100644 --- a/public/javascripts/SVValidate/css/svv-status.css +++ b/public/javascripts/SVValidate/css/svv-status.css @@ -70,7 +70,7 @@ } .status-box h1 { - font-size: 10pt; + font-size: 11.5pt; margin: 0 0 4px 0; } @@ -80,6 +80,12 @@ margin: 1px 0 5px 1px; } +.status-box p { + font-size: 7pt; + font-weight: normal; + margin: 1px 0 5px 1px; +} + .status-column-half { white-space: nowrap; padding: 5px 5px 5px 5px; diff --git a/public/javascripts/SVValidate/src/Main.js b/public/javascripts/SVValidate/src/Main.js index 2180c8e4e4..37f9ae3eb5 100644 --- a/public/javascripts/SVValidate/src/Main.js +++ b/public/javascripts/SVValidate/src/Main.js @@ -8,6 +8,7 @@ var svv = svv || {}; * @constructor */ function Main (param) { + svv.adminVersion = param.adminVersion; svv.canvasHeight = param.canvasHeight; svv.canvasWidth = param.canvasWidth; svv.missionsCompleted = param.missionSetProgress; diff --git a/public/javascripts/SVValidate/src/data/Form.js b/public/javascripts/SVValidate/src/data/Form.js index e8404b59cb..d891df9805 100644 --- a/public/javascripts/SVValidate/src/data/Form.js +++ b/public/javascripts/SVValidate/src/data/Form.js @@ -10,7 +10,10 @@ function Form(url, beaconUrl) { * @param {boolean} missionComplete Whether or not the mission is complete. To ensure we only send once per mission. */ function compileSubmissionData(missionComplete) { - let data = { timestamp: new Date().getTime() }; + let data = { + adminVersion: svv.adminVersion, + timestamp: new Date().getTime() + }; let missionContainer = svv.missionContainer; let mission = missionContainer ? missionContainer.getCurrentMission() : null; diff --git a/public/javascripts/SVValidate/src/keyboard/Keyboard.js b/public/javascripts/SVValidate/src/keyboard/Keyboard.js index e42bf9f341..16f349c40c 100644 --- a/public/javascripts/SVValidate/src/keyboard/Keyboard.js +++ b/public/javascripts/SVValidate/src/keyboard/Keyboard.js @@ -27,7 +27,7 @@ function Keyboard(menuUI) { * @param button jQuery element for the button clicked. * @param action {String} Validation action. Must be either agree, disagree, or not sure. */ - function validateLabel (button, action, comment) { + function validateLabel(button, action, comment) { // Want at least 800ms in-between to allow GSV Panorama to load. (Value determined // experimentally). diff --git a/public/javascripts/SVValidate/src/label/Label.js b/public/javascripts/SVValidate/src/label/Label.js index 81382ef93c..890f125c8a 100644 --- a/public/javascripts/SVValidate/src/label/Label.js +++ b/public/javascripts/SVValidate/src/label/Label.js @@ -41,6 +41,11 @@ function Label(params) { isMobile: undefined }; + let adminProperties = { + username: null, + previousValidations: null + } + let icons = { CurbRamp : '/assets/images/icons/AdminTool_CurbRamp.png', NoCurbRamp : '/assets/images/icons/AdminTool_NoCurbRamp.png', @@ -77,7 +82,7 @@ function Label(params) { let self = this; /** - * Initializes a label from metadata (if parameters are passed in) + * Initializes a label from metadata (if parameters are passed in). * @private */ function _init() { @@ -98,6 +103,16 @@ function Label(params) { if ("street_edge_id" in params) setAuditProperty("streetEdgeId", params.street_edge_id); if ("region_id" in params) setAuditProperty("regionId", params.region_id); if ("tags" in params) setAuditProperty("tags", params.tags); + // Properties only used on the Admin version of Validate. + if ("admin_data" in params && params.admin_data !== null) { + if ("username" in params.admin_data) adminProperties.username = params.admin_data.username; + if ("previous_validations" in params.admin_data) { + adminProperties.previousValidations = [] + for (let prevVal of params.admin_data.previous_validations) { + adminProperties.previousValidations.push(prevVal); + } + } + } setAuditProperty("isMobile", isMobile()); } } @@ -115,10 +130,19 @@ function Label(params) { * @param key Name of property. * @returns Value associated with this key. */ - function getAuditProperty (key) { + function getAuditProperty(key) { return key in auditProperties ? auditProperties[key] : null; } + /** + * Returns a specific adminProperty of this label. + * @param key Name of property. + * @returns {*|null} Value associated with this key. + */ + function getAdminProperty(key) { + return key in adminProperties ? adminProperties[key] : null; + } + /** * Gets the position of this label from the POV from which it was originally placed. * @returns {heading: number, pitch: number} @@ -219,8 +243,7 @@ function Label(params) { * @param comment An optional comment submitted with the validation. */ function validate(validationResult, comment) { - // This is the POV of the PanoMarker, where the PanoMarker would be loaded at the center - // of the viewport. + // This is the POV of the PanoMarker, where the PanoMarker would be loaded at the center of the viewport. let pos = getPosition(); let panomarkerPov = { heading: pos.heading, @@ -297,6 +320,7 @@ function Label(params) { _init(); self.getAuditProperty = getAuditProperty; + self.getAdminProperty = getAdminProperty; self.getIconUrl = getIconUrl; self.getProperty = getProperty; self.getProperties = getProperties; diff --git a/public/javascripts/SVValidate/src/panorama/PanoramaContainer.js b/public/javascripts/SVValidate/src/panorama/PanoramaContainer.js index 3497d26341..b5584f34e2 100644 --- a/public/javascripts/SVValidate/src/panorama/PanoramaContainer.js +++ b/public/javascripts/SVValidate/src/panorama/PanoramaContainer.js @@ -80,6 +80,23 @@ function PanoramaContainer (labelList) { if (svv.zoomControl) { svv.zoomControl.updateZoomAvailability(); } + + // Update the status area with extra info if on Admin Validate. + if (svv.adminVersion) { + $('#curr-label-username').text(svv.panorama.getCurrentLabel().getAdminProperty('username')); + + // Remove prior set of previous validations and add the new set. + document.querySelectorAll('.prev-val').forEach(e => e.remove()); + const prevVals = svv.panorama.getCurrentLabel().getAdminProperty('previousValidations'); + if (prevVals.length === 0) { + $(`

    None

    `).insertAfter('#curr-label-prev-validations'); + } else { + for (const prevVal of svv.panorama.getCurrentLabel().getAdminProperty('previousValidations')) { + $(`

    ${prevVal.username}: ${prevVal.validation}

    `).insertAfter('#curr-label-prev-validations'); + // $('#curr-label-prev-validations').append(`${prevVal.username}: ${prevVal.validation}`); + } + } + } } function getCurrentLabel() { @@ -121,7 +138,7 @@ function PanoramaContainer (labelList) { /** * Validates the label. */ - function validateLabel (action, timestamp, comment) { + function validateLabel(action, timestamp, comment) { svv.panorama.getCurrentLabel().validate(action, comment); svv.panorama.setProperty('validationTimestamp', timestamp); } From aad787a026e29e0e2355bee42c97ea5fca76cb23 Mon Sep 17 00:00:00 2001 From: Mikey Saugstad Date: Thu, 29 Feb 2024 14:35:53 -0800 Subject: [PATCH 02/12] admin info now shows on first label on Admin Validate --- .../src/panorama/PanoramaContainer.js | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/public/javascripts/SVValidate/src/panorama/PanoramaContainer.js b/public/javascripts/SVValidate/src/panorama/PanoramaContainer.js index b5584f34e2..27ea1bb382 100644 --- a/public/javascripts/SVValidate/src/panorama/PanoramaContainer.js +++ b/public/javascripts/SVValidate/src/panorama/PanoramaContainer.js @@ -24,6 +24,7 @@ function PanoramaContainer (labelList) { // Set the HTML svv.statusField.updateLabelText(labelList[0].getAuditProperty('labelType')); svv.statusExample.updateLabelImage(labelList[0].getAuditProperty('labelType')); + if (svv.adminVersion) updateAdminHTML(); } /** @@ -81,8 +82,19 @@ function PanoramaContainer (labelList) { svv.zoomControl.updateZoomAvailability(); } - // Update the status area with extra info if on Admin Validate. + if (svv.adminVersion) updateAdminHTML(); + } + + function getCurrentLabel() { + return labels[getProperty('progress') - 1]; + } + + /** + * Updates the admin HTML with extra information about the label being validated. Only call if on Admin Validate! + */ + function updateAdminHTML() { if (svv.adminVersion) { + // Update the status area with extra info if on Admin Validate. $('#curr-label-username').text(svv.panorama.getCurrentLabel().getAdminProperty('username')); // Remove prior set of previous validations and add the new set. @@ -93,16 +105,11 @@ function PanoramaContainer (labelList) { } else { for (const prevVal of svv.panorama.getCurrentLabel().getAdminProperty('previousValidations')) { $(`

    ${prevVal.username}: ${prevVal.validation}

    `).insertAfter('#curr-label-prev-validations'); - // $('#curr-label-prev-validations').append(`${prevVal.username}: ${prevVal.validation}`); } } } } - function getCurrentLabel() { - return labels[getProperty('progress') - 1]; - } - /** * Resets the validation interface for a new mission. Loads a new set of label onto the panoramas. */ From ce48fbca8d6c5acdd7edb4f078b618020b7a7614 Mon Sep 17 00:00:00 2001 From: Mikey Saugstad Date: Thu, 29 Feb 2024 14:36:11 -0800 Subject: [PATCH 03/12] fixes bug in Dutch translations on Validate --- public/locales/nl/validate.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/locales/nl/validate.json b/public/locales/nl/validate.json index 84806341a7..a0e8f66986 100644 --- a/public/locales/nl/validate.json +++ b/public/locales/nl/validate.json @@ -16,7 +16,7 @@ }, "right-ui": { "current-mission": { - "validate-labels": "Valideer {{n}} {{labelType}} labels" + "validate-labels": "Valideer {{n}} labels" }, "correct": { "curb-ramp": { From 66e09d1df3eb080e5327da326ee20c2994b9f79b Mon Sep 17 00:00:00 2001 From: Mikey Saugstad Date: Thu, 29 Feb 2024 14:53:26 -0800 Subject: [PATCH 04/12] hyperlinks usernames on Admin Validate to admin user dashboards --- public/javascripts/SVValidate/src/Main.js | 4 ++ .../src/panorama/PanoramaContainer.js | 25 +--------- .../SVValidate/src/status/StatusField.js | 46 +++++++++++++++---- 3 files changed, 42 insertions(+), 33 deletions(-) diff --git a/public/javascripts/SVValidate/src/Main.js b/public/javascripts/SVValidate/src/Main.js index 37f9ae3eb5..435cb55d5d 100644 --- a/public/javascripts/SVValidate/src/Main.js +++ b/public/javascripts/SVValidate/src/Main.js @@ -121,6 +121,10 @@ function Main (param) { svv.ui.status.examples.popupPointer = $("#example-image-popup-pointer"); svv.ui.status.examples.popupTitle = $("#example-image-popup-title"); + svv.ui.status.admin = {}; + svv.ui.status.admin.username = $('#curr-label-username'); + svv.ui.status.admin.prevValidations = $('#curr-label-prev-validations'); + svv.ui.dateHolder = $("#svv-panorama-date-holder"); } diff --git a/public/javascripts/SVValidate/src/panorama/PanoramaContainer.js b/public/javascripts/SVValidate/src/panorama/PanoramaContainer.js index 27ea1bb382..aa733f35ee 100644 --- a/public/javascripts/SVValidate/src/panorama/PanoramaContainer.js +++ b/public/javascripts/SVValidate/src/panorama/PanoramaContainer.js @@ -24,7 +24,7 @@ function PanoramaContainer (labelList) { // Set the HTML svv.statusField.updateLabelText(labelList[0].getAuditProperty('labelType')); svv.statusExample.updateLabelImage(labelList[0].getAuditProperty('labelType')); - if (svv.adminVersion) updateAdminHTML(); + if (svv.adminVersion) svv.statusField.updateAdminInfo(); } /** @@ -82,34 +82,13 @@ function PanoramaContainer (labelList) { svv.zoomControl.updateZoomAvailability(); } - if (svv.adminVersion) updateAdminHTML(); + if (svv.adminVersion) svv.statusField.updateAdminInfo(); } function getCurrentLabel() { return labels[getProperty('progress') - 1]; } - /** - * Updates the admin HTML with extra information about the label being validated. Only call if on Admin Validate! - */ - function updateAdminHTML() { - if (svv.adminVersion) { - // Update the status area with extra info if on Admin Validate. - $('#curr-label-username').text(svv.panorama.getCurrentLabel().getAdminProperty('username')); - - // Remove prior set of previous validations and add the new set. - document.querySelectorAll('.prev-val').forEach(e => e.remove()); - const prevVals = svv.panorama.getCurrentLabel().getAdminProperty('previousValidations'); - if (prevVals.length === 0) { - $(`

    None

    `).insertAfter('#curr-label-prev-validations'); - } else { - for (const prevVal of svv.panorama.getCurrentLabel().getAdminProperty('previousValidations')) { - $(`

    ${prevVal.username}: ${prevVal.validation}

    `).insertAfter('#curr-label-prev-validations'); - } - } - } - } - /** * Resets the validation interface for a new mission. Loads a new set of label onto the panoramas. */ diff --git a/public/javascripts/SVValidate/src/status/StatusField.js b/public/javascripts/SVValidate/src/status/StatusField.js index 0325aa4da0..a6759f5145 100644 --- a/public/javascripts/SVValidate/src/status/StatusField.js +++ b/public/javascripts/SVValidate/src/status/StatusField.js @@ -9,6 +9,7 @@ function StatusField(param) { let containerWidth = 730; let self = this; let completedValidations = param.completedValidations; + let statusUI = svv.ui.status; /** * Resets the status field whenever a new mission is introduced. * @param currentMission Mission object for the current mission. @@ -35,7 +36,7 @@ function StatusField(param) { * Refreshes the number count displayed. */ function refreshLabelCountsDisplay(){ - svv.ui.status.labelCount.html(completedValidations); + statusUI.labelCount.html(completedValidations); } /** @@ -44,12 +45,12 @@ function StatusField(param) { */ function updateLabelText(labelType) { // Centers and updates title top of the validation interface. - svv.ui.status.upperMenuTitle.html(i18next.t(`top-ui.title.${util.camelToKebab(labelType)}`)); - let offset = svv.ui.status.zoomInButton.outerWidth() - + svv.ui.status.zoomOutButton.outerWidth() - + svv.ui.status.labelVisibilityControlButton.outerWidth(); - let width = ((svv.canvasWidth - offset) / 2) - (svv.ui.status.upperMenuTitle.outerWidth() / 2); - svv.ui.status.upperMenuTitle.css("left", width + "px"); + statusUI.upperMenuTitle.html(i18next.t(`top-ui.title.${util.camelToKebab(labelType)}`)); + let offset = statusUI.zoomInButton.outerWidth() + + statusUI.zoomOutButton.outerWidth() + + statusUI.labelVisibilityControlButton.outerWidth(); + let width = ((svv.canvasWidth - offset) / 2) - (statusUI.upperMenuTitle.outerWidth() / 2); + statusUI.upperMenuTitle.css("left", width + "px"); } /** @@ -57,7 +58,7 @@ function StatusField(param) { * @param count {Number} Number of labels to validate this mission. */ function updateMissionDescription(count) { - svv.ui.status.missionDescription.html(i18next.t('right-ui.current-mission.validate-labels', { n: count })); + statusUI.missionDescription.html(i18next.t('right-ui.current-mission.validate-labels', { n: count })); } /** @@ -74,7 +75,7 @@ function StatusField(param) { completionRate = completionRate + "%"; // Update blue portion of progress bar - svv.ui.status.progressFiller.css({ + statusUI.progressFiller.css({ background: color, width: completionRate }); @@ -90,7 +91,7 @@ function StatusField(param) { if (completionRate > 100) completionRate = 100; completionRate = completionRate.toFixed(0, 10); completionRate = completionRate + "% " + i18next.t('common:complete'); - svv.ui.status.progressText.html(completionRate); + statusUI.progressText.html(completionRate); } /** @@ -100,6 +101,30 @@ function StatusField(param) { return completedValidations; } + /** + * Updates the admin HTML with extra information about the label being validated. Only call if on Admin Validate! + */ + function updateAdminInfo() { + if (svv.adminVersion) { + // Update the status area with extra info if on Admin Validate. + const user = svv.panorama.getCurrentLabel().getAdminProperty('username'); + statusUI.admin.username.html(`${user}`); + + // Remove prior set of previous validations and add the new set. + document.querySelectorAll('.prev-val').forEach(e => e.remove()); + const prevVals = svv.panorama.getCurrentLabel().getAdminProperty('previousValidations'); + if (prevVals.length === 0) { + // TODO statusUI.admin.prevValidations + $(`

    None

    `).insertAfter('#curr-label-prev-validations'); + } else { + for (const prevVal of svv.panorama.getCurrentLabel().getAdminProperty('previousValidations')) { + $(`

    ${prevVal.username}: ${prevVal.validation}

    `) + .insertAfter('#curr-label-prev-validations'); + } + } + } + } + self.setProgressBar = setProgressBar; self.setProgressText = setProgressText; self.updateLabelText = updateLabelText; @@ -108,6 +133,7 @@ function StatusField(param) { self.incrementLabelCounts = incrementLabelCounts; self.reset = reset; self.getCompletedValidations = getCompletedValidations; + self.updateAdminInfo = updateAdminInfo; return this; } From 206674848e42102b62bb1e89515b7a158578e58a Mon Sep 17 00:00:00 2001 From: Mikey Saugstad Date: Thu, 29 Feb 2024 15:49:38 -0800 Subject: [PATCH 05/12] Admin Validate can show given label type through query param --- app/controllers/ValidationController.scala | 39 +++++++++++-------- .../ValidationTaskController.scala | 27 +++++++------ app/controllers/helper/ValidateHelper.scala | 22 +++++++++++ .../ValidationTaskSubmissionFormats.scala | 13 +++++-- app/views/validation.scala.html | 9 +++-- conf/routes | 2 +- public/javascripts/SVValidate/src/Main.js | 1 + .../javascripts/SVValidate/src/data/Form.js | 12 +++--- 8 files changed, 84 insertions(+), 41 deletions(-) create mode 100644 app/controllers/helper/ValidateHelper.scala diff --git a/app/controllers/ValidationController.scala b/app/controllers/ValidationController.scala index 79117e6a05..e03956126e 100644 --- a/app/controllers/ValidationController.scala +++ b/app/controllers/ValidationController.scala @@ -7,8 +7,9 @@ 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, ValidateHelper} import controllers.helper.ControllerUtils.isAdmin +import controllers.helper.ValidateHelper.AdminValidateParams import formats.json.CommentSubmissionFormats._ import formats.json.LabelFormat import models.amt.AMTAssignmentTable @@ -41,11 +42,12 @@ class ValidationController @Inject() (implicit val env: Environment[User, Sessio val ipAddress: String = request.remoteAddress request.identity match { case Some(user) => - val validationData = getDataForValidationPages(user, ipAddress, labelCount = 10, "Visit_Validate", adminVersion=false) + 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("Sidewalk - Validate", Some(user), adminVersion=false, 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")); @@ -60,7 +62,8 @@ class ValidationController @Inject() (implicit val env: Environment[User, Sessio request.identity match { case Some(user) => - val validationData = getDataForValidationPages(user, ipAddress, labelCount = 10, "Visit_MobileValidate", adminVersion=false) + val adminParams = AdminValidateParams(adminVersion = false) + val validationData = getDataForValidationPages(user, ipAddress, labelCount = 10, "Visit_MobileValidate", adminParams) if (validationData._4.missionType != "validation" || user.role.getOrElse("") == "Turker" || !ControllerUtils.isMobile(request)) { Future.successful(Redirect("/explore")) } else { @@ -74,11 +77,12 @@ class ValidationController @Inject() (implicit val env: Environment[User, Sessio /** * Returns an admin version of the validation page. */ - def adminValidate = UserAwareAction.async { implicit request => + def adminValidate(labelTypeId: Option[Int]) = UserAwareAction.async { implicit request => val ipAddress: String = request.remoteAddress if (isAdmin(request.identity)) { - val validationData = getDataForValidationPages(request.identity.get, ipAddress, labelCount = 10, "Visit_AdminValidate", adminVersion=true) - Future.successful(Ok(views.html.validation("Sidewalk - Admin Validate", request.identity, adminVersion=true, validationData._1, validationData._2, validationData._3, validationData._4.numComplete, validationData._5, validationData._6))) + val adminParams = AdminValidateParams(adminVersion = true, labelTypeId) + 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")) } @@ -89,7 +93,7 @@ class ValidationController @Inject() (implicit val env: Environment[User, Sessio * * @return (mission, labelList, missionProgress, missionSetProgress, hasNextMission, completedValidations) */ - def getDataForValidationPages(user: User, ipAddress: String, labelCount: Int, visitTypeStr: String, adminVersion: Boolean): (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)) @@ -99,12 +103,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 @@ -113,7 +118,7 @@ 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, adminVersion) + val labelList: JsValue = getLabelListForValidation(user.userId, labelTypeId, mission, adminParams) val missionJsObject: JsObject = mission.toJSON val progressJsObject: JsObject = LabelValidationTable.getValidationProgress(mission.missionId) @@ -127,20 +132,20 @@ 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 - * @param adminVersion Whether to include data only shown on admin version of the site. - * @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, adminVersion: Boolean): 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 // Get list of labels and their metadata for Validate page. Get extra data if it's for Admin Validate. val labelMetadata: Seq[LabelValidationMetadata] = LabelTable.retrieveLabelListForValidation(userId, labelsToRetrieve, labelType, skippedLabelId = None) - val labelMetadataJsonSeq: Seq[JsObject] = if (adminVersion) { + 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))) diff --git a/app/controllers/ValidationTaskController.scala b/app/controllers/ValidationTaskController.scala index d656c01101..b7586129fa 100644 --- a/app/controllers/ValidationTaskController.scala +++ b/app/controllers/ValidationTaskController.scala @@ -7,6 +7,7 @@ import com.mohiva.play.silhouette.api.{Environment, Silhouette} import com.mohiva.play.silhouette.impl.authenticators.SessionAuthenticator import controllers.headers.ProvidesHeader import controllers.helper.ControllerUtils.{isAdmin, sendSciStarterContributions} +import controllers.helper.ValidateHelper.AdminValidateParams import formats.json.ValidationTaskSubmissionFormats._ import models.amt.AMTAssignmentTable import models.label._ @@ -39,7 +40,9 @@ class ValidationTaskController @Inject() (implicit val env: Environment[User, Se */ def processValidationTaskSubmissions(data: ValidationTaskSubmission, remoteAddress: String, identity: Option[User]) = { val userOption = identity - val adminVersion: Boolean = data.adminVersion && isAdmin(userOption) + val adminParams: AdminValidateParams = + if (data.adminParams.adminVersion && isAdmin(userOption)) data.adminParams + else AdminValidateParams(adminVersion = false) val currTime = new Timestamp(data.timestamp) ValidationTaskInteractionTable.saveMultiple(data.interactions.map { interaction => ValidationTaskInteraction(0, interaction.missionId, interaction.action, interaction.gsvPanoramaId, @@ -77,12 +80,12 @@ class ValidationTaskController @Inject() (implicit val env: Environment[User, Se case Some(_) => val missionProgress: ValidationMissionProgress = data.missionProgress.get val currentMissionLabelTypeId: Int = missionProgress.labelTypeId - val nextMissionLabelTypeId: Option[Int] = getLabelTypeId(userOption, missionProgress, Some(currentMissionLabelTypeId)) + val nextMissionLabelTypeId: Option[Int] = getLabelTypeId(userOption, missionProgress, Some(currentMissionLabelTypeId), adminParams) nextMissionLabelTypeId match { // Load new mission, generate label list for validation. case Some (nextMissionLabelTypeId) => val possibleNewMission: Option[Mission] = updateMissionTable(userOption, missionProgress, Some(nextMissionLabelTypeId)) - val labelList: Option[JsValue] = getLabelList(userOption, missionProgress, nextMissionLabelTypeId, adminVersion) + val labelList: Option[JsValue] = getLabelList(userOption, missionProgress, nextMissionLabelTypeId, adminParams) val progress: Option[JsObject] = Some(LabelValidationTable.getValidationProgress(possibleNewMission.get.missionId)) ValidationTaskPostReturnValue(Some (true), possibleNewMission, labelList, progress) case None => @@ -222,16 +225,18 @@ class ValidationTaskController @Inject() (implicit val env: Environment[User, Se * @param user UserId of the current user. * @param missionProgress Progress of the current validation mission. * @param currentLabelTypeId Label Type ID of the current mission + * @param adminParams Parameters related to the admin version of the validate page. */ - def getLabelTypeId(user: Option[User], missionProgress: ValidationMissionProgress, currentLabelTypeId: Option[Int]): Option[Int] = { + def getLabelTypeId(user: Option[User], missionProgress: ValidationMissionProgress, currentLabelTypeId: Option[Int], adminParams: AdminValidateParams): Option[Int] = { val userId: UUID = user.get.userId if (missionProgress.completed) { val labelsToRetrieve: Int = MissionTable.validationMissionLabelsToRetrieve val possibleLabelTypeIds: List[Int] = LabelTable.retrievePossibleLabelTypeIds(userId, labelsToRetrieve, currentLabelTypeId) + .filter(labTypeId => adminParams.labelTypeId.isEmpty || adminParams.labelTypeId.get == labTypeId) val hasNextMission: Boolean = possibleLabelTypeIds.nonEmpty if (hasNextMission) { - // 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 1, 2, 3, 4, 9, 10 if possible, o/w choose 7. val possibleIds: List[Int] = if (possibleLabelTypeIds.size > 1) possibleLabelTypeIds.filter(_ != 7) else possibleLabelTypeIds @@ -247,13 +252,13 @@ class ValidationTaskController @Inject() (implicit val env: Environment[User, Se * * @param user * @param missionProgress Metadata for this mission - * @param adminVersion Whether to include data only shown on admin version of the site. + * @param adminParams Parameters related to the admin version of the validate page. * @return List of label metadata (if this mission is complete). */ - def getLabelList(user: Option[User], missionProgress: ValidationMissionProgress, labelTypeId: Int, adminVersion: Boolean): Option[JsValue] = { + def getLabelList(user: Option[User], missionProgress: ValidationMissionProgress, labelTypeId: Int, adminParams: AdminValidateParams): Option[JsValue] = { val userId: UUID = user.get.userId if (missionProgress.completed) { - Some(getLabelListForValidation(userId, MissionTable.validationMissionLabelsToRetrieve, labelTypeId, adminVersion)) + Some(getLabelListForValidation(userId, MissionTable.validationMissionLabelsToRetrieve, labelTypeId, adminParams)) } else { None } @@ -265,13 +270,13 @@ class ValidationTaskController @Inject() (implicit val env: Environment[User, Se * @param userId User ID of the current user. * @param n Number of labels to retrieve for this list. * @param labelTypeId Label Type to retrieve - * @param adminVersion Whether to include data only shown on admin version of the site. + * @param adminParams Parameters related to the admin version of the validate page. * @return JsValue containing a list of labels. */ - def getLabelListForValidation(userId: UUID, n: Int, labelTypeId: Int, adminVersion: Boolean): JsValue = { + def getLabelListForValidation(userId: UUID, n: Int, labelTypeId: Int, adminParams: AdminValidateParams): JsValue = { // Get list of labels and their metadata for Validate page. Get extra data if it's for Admin Validate. val labelMetadata: Seq[LabelValidationMetadata] = LabelTable.retrieveLabelListForValidation(userId, n, labelTypeId, skippedLabelId = None) - val labelMetadataJsonSeq: Seq[JsObject] = if (adminVersion) { + 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))) diff --git a/app/controllers/helper/ValidateHelper.scala b/app/controllers/helper/ValidateHelper.scala new file mode 100644 index 0000000000..29ed75612c --- /dev/null +++ b/app/controllers/helper/ValidateHelper.scala @@ -0,0 +1,22 @@ +package controllers.helper + +import models.user.User +import play.api.{Logger, Play} +import play.api.Play.current +import play.api.mvc.Request +import scala.concurrent.{ExecutionContext, Future} +import scala.util.matching.Regex +import org.apache.http.NameValuePair +import org.apache.http.client.entity.UrlEncodedFormEntity +import org.apache.http.client.methods.HttpPost +import org.apache.http.impl.client.DefaultHttpClient +import org.apache.http.message.BasicNameValuePair +import java.io.InputStream +import java.util +import scala.util.Try + +object ValidateHelper { + case class AdminValidateParams(adminVersion: Boolean, labelTypeId: Option[Int] = None) { + require(labelTypeId.isEmpty || adminVersion, "labelTypeId can only be set if adminVersion is true") + } +} \ No newline at end of file diff --git a/app/formats/json/ValidationTaskSubmissionFormats.scala b/app/formats/json/ValidationTaskSubmissionFormats.scala index 9a6079f530..787f65d8da 100644 --- a/app/formats/json/ValidationTaskSubmissionFormats.scala +++ b/app/formats/json/ValidationTaskSubmissionFormats.scala @@ -1,8 +1,8 @@ package formats.json +import controllers.helper.ValidateHelper.AdminValidateParams import java.sql.Timestamp import play.api.libs.json.{JsBoolean, JsPath, Reads} - import scala.collection.immutable.Seq import play.api.libs.functional.syntax._ @@ -12,7 +12,7 @@ object ValidationTaskSubmissionFormats { case class LabelValidationSubmission(labelId: Int, missionId: Int, validationResult: Int, canvasX: Option[Int], canvasY: Option[Int], heading: Float, pitch: Float, zoom: Float, canvasHeight: Int, canvasWidth: Int, startTimestamp: Long, endTimestamp: Long, source: String) case class SkipLabelSubmission(labels: Seq[LabelValidationSubmission]) case class ValidationMissionProgress(missionId: Int, missionType: String, labelsProgress: Int, labelTypeId: Int, completed: Boolean, skipped: Boolean) - case class ValidationTaskSubmission(interactions: Seq[InteractionSubmission], environment: EnvironmentSubmission, labels: Seq[LabelValidationSubmission], missionProgress: Option[ValidationMissionProgress], adminVersion: Boolean, timestamp: Long) + case class ValidationTaskSubmission(interactions: Seq[InteractionSubmission], environment: EnvironmentSubmission, labels: Seq[LabelValidationSubmission], missionProgress: Option[ValidationMissionProgress], adminParams: AdminValidateParams, timestamp: Long) case class LabelMapValidationSubmission(labelId: Int, labelType: String, validationResult: Int, canvasX: Option[Int], canvasY: Option[Int], heading: Float, pitch: Float, zoom: Float, canvasHeight: Int, canvasWidth: Int, startTimestamp: Long, endTimestamp: Long, source: String) implicit val environmentSubmissionReads: Reads[EnvironmentSubmission] = ( @@ -69,12 +69,17 @@ object ValidationTaskSubmissionFormats { (JsPath \ "skipped").read[Boolean] )(ValidationMissionProgress.apply _) + implicit val adminValidateParamsReads: Reads[AdminValidateParams] = ( + (JsPath \ "admin_version").read[Boolean] and + (JsPath \ "label_type_id").readNullable[Int] + )(AdminValidateParams.apply _) + implicit val validationTaskSubmissionReads: Reads[ValidationTaskSubmission] = ( (JsPath \ "interactions").read[Seq[InteractionSubmission]] and (JsPath \ "environment").read[EnvironmentSubmission] and (JsPath \ "labels").read[Seq[LabelValidationSubmission]] and - (JsPath \ "missionProgress").readNullable[ValidationMissionProgress] and - (JsPath \ "adminVersion").read[Boolean] and + (JsPath \ "mission_progress").readNullable[ValidationMissionProgress] and + (JsPath \ "admin_params").read[AdminValidateParams] and (JsPath \ "timestamp").read[Long] )(ValidationTaskSubmission.apply _) diff --git a/app/views/validation.scala.html b/app/views/validation.scala.html index 590fe7e80b..9bdf58250f 100644 --- a/app/views/validation.scala.html +++ b/app/views/validation.scala.html @@ -2,7 +2,8 @@ @import models.amt.AMTAssignmentTable @import play.api.libs.json.JsValue -@(title: String, user: Option[User] = None, adminVersion: Boolean, mission: Option[JsValue], labelList: Option[JsValue], progress: Option[JsValue], missionSetProgress: Int, hasNextMission: Boolean, completedValidations: Int)(implicit lang: Lang) +@import controllers.helper.ValidateHelper.AdminValidateParams +@(title: String, user: Option[User] = None, adminParams: AdminValidateParams, mission: Option[JsValue], labelList: Option[JsValue], progress: Option[JsValue], missionSetProgress: Int, hasNextMission: Boolean, completedValidations: Int)(implicit lang: Lang) @main(title, Some("/validate")) { @navbar(user, Some("/validate")) @@ -191,7 +192,7 @@

    - @if(!adminVersion) { + @if(!adminParams.adminVersion) {

    @Html(Messages("validate.right.ui.correct.examples")) @@ -278,7 +279,8 @@

    Previous Validations

    param.canvasHeight = 440; param.canvasWidth = 720; param.language = "@lang.code"; - param.adminVersion = @adminVersion; + param.adminVersion = @adminParams.adminVersion; + param.adminLabelTypeId = @Html(adminParams.labelTypeId.map(_.toString).getOrElse("null")); param.modalText = { 1: "@Messages("labeling.guide.curb.ramp.summary")", 2: "@Messages("labeling.guide.curb.ramp.summary")", @@ -301,6 +303,7 @@

    Previous Validations

    param.labelList[key] = new Label(param.labelList[key]); }); } + console.log(param); svv.main = new Main(param); } diff --git a/conf/routes b/conf/routes index 9137c5bd17..ccf58d11f4 100644 --- a/conf/routes +++ b/conf/routes @@ -102,7 +102,7 @@ GET /audit/street/:id @controllers.AuditC # Label validation tasks GET /validate @controllers.ValidationController.validate -GET /adminValidate @controllers.ValidationController.adminValidate +GET /adminValidate @controllers.ValidationController.adminValidate(labelTypeId: Option[Int] ?= None) POST /validate/comment @controllers.ValidationController.postComment # Task API. diff --git a/public/javascripts/SVValidate/src/Main.js b/public/javascripts/SVValidate/src/Main.js index 435cb55d5d..a18cc6e7ff 100644 --- a/public/javascripts/SVValidate/src/Main.js +++ b/public/javascripts/SVValidate/src/Main.js @@ -9,6 +9,7 @@ var svv = svv || {}; */ function Main (param) { svv.adminVersion = param.adminVersion; + svv.adminLabelTypeId = param.adminLabelTypeId; svv.canvasHeight = param.canvasHeight; svv.canvasWidth = param.canvasWidth; svv.missionsCompleted = param.missionSetProgress; diff --git a/public/javascripts/SVValidate/src/data/Form.js b/public/javascripts/SVValidate/src/data/Form.js index d891df9805..5089821241 100644 --- a/public/javascripts/SVValidate/src/data/Form.js +++ b/public/javascripts/SVValidate/src/data/Form.js @@ -10,10 +10,7 @@ function Form(url, beaconUrl) { * @param {boolean} missionComplete Whether or not the mission is complete. To ensure we only send once per mission. */ function compileSubmissionData(missionComplete) { - let data = { - adminVersion: svv.adminVersion, - timestamp: new Date().getTime() - }; + let data = { timestamp: new Date().getTime() }; let missionContainer = svv.missionContainer; let mission = missionContainer ? missionContainer.getCurrentMission() : null; @@ -23,7 +20,7 @@ function Form(url, beaconUrl) { // Only submit mission progress if there is a mission when we're compiling submission data. if (mission) { // Add the current mission - data.missionProgress = { + data.mission_progress = { mission_id: mission.getProperty("missionId"), mission_type: mission.getProperty("missionType"), labels_progress: mission.getProperty("labelsProgress"), @@ -56,6 +53,11 @@ function Form(url, beaconUrl) { css_zoom: svv.cssZoom ? svv.cssZoom : 100 }; + data.admin_params = { + admin_version: svv.adminVersion, + label_type_id: svv.adminLabelTypeId + } + data.interactions = svv.tracker.getActions(); svv.tracker.refresh(); return data; From 26c11f990b995d20c00a03d914db0aacd86364af Mon Sep 17 00:00:00 2001 From: Mikey Saugstad Date: Mon, 4 Mar 2024 12:58:03 -0800 Subject: [PATCH 06/12] adds Admin Validate URL params for regions & users filters --- app/controllers/ValidationController.scala | 27 +++++++++++++------ .../ValidationTaskController.scala | 4 +-- app/controllers/helper/ValidateHelper.scala | 19 +++---------- .../ValidationTaskSubmissionFormats.scala | 4 ++- app/models/label/LabelTable.scala | 22 +++++++++------ app/views/validation.scala.html | 2 ++ conf/routes | 2 +- public/javascripts/SVValidate/src/Main.js | 2 ++ .../javascripts/SVValidate/src/data/Form.js | 4 ++- 9 files changed, 49 insertions(+), 37 deletions(-) diff --git a/app/controllers/ValidationController.scala b/app/controllers/ValidationController.scala index e03956126e..11a87729de 100644 --- a/app/controllers/ValidationController.scala +++ b/app/controllers/ValidationController.scala @@ -7,8 +7,7 @@ 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, ValidateHelper} -import controllers.helper.ControllerUtils.isAdmin +import controllers.helper.ControllerUtils.{isAdmin, parseIntegerList, isMobile} import controllers.helper.ValidateHelper.AdminValidateParams import formats.json.CommentSubmissionFormats._ import formats.json.LabelFormat @@ -18,6 +17,7 @@ import models.label.LabelTable import models.label.LabelTable.{AdminValidationData, LabelValidationMetadata} import models.label.LabelValidationTable import models.mission.{Mission, MissionSetProgress, MissionTable} +import models.region.RegionTable import models.validation._ import models.user._ import play.api.libs.json._ @@ -64,7 +64,7 @@ class ValidationController @Inject() (implicit val env: Environment[User, Sessio case Some(user) => val adminParams = AdminValidateParams(adminVersion = false) val validationData = getDataForValidationPages(user, ipAddress, labelCount = 10, "Visit_MobileValidate", adminParams) - if (validationData._4.missionType != "validation" || user.role.getOrElse("") == "Turker" || !ControllerUtils.isMobile(request)) { + if (validationData._4.missionType != "validation" || user.role.getOrElse("") == "Turker" || !isMobile(request)) { Future.successful(Redirect("/explore")) } else { Future.successful(Ok(views.html.mobileValidate("Sidewalk - Validate", Some(user), validationData._1, validationData._2, validationData._3, validationData._4.numComplete, validationData._5, validationData._6))) @@ -77,12 +77,23 @@ class ValidationController @Inject() (implicit val env: Environment[User, Sessio /** * Returns an admin version of the validation page. */ - def adminValidate(labelTypeId: Option[Int]) = UserAwareAction.async { implicit request => + def adminValidate(labelTypeId: Option[Int], userIds: Option[String], neighborhoods: Option[String]) = UserAwareAction.async { implicit request => val ipAddress: String = request.remoteAddress if (isAdmin(request.identity)) { - val adminParams = AdminValidateParams(adminVersion = true, labelTypeId) - val validationData = getDataForValidationPages(request.identity.get, ipAddress, labelCount = 10, "Visit_AdminValidate", adminParams) + // If any inputs are invalid, send back error to the user. + val userIdsList: Option[List[String]] = userIds.map(_.split(',').map(_.trim).toList) + val neighborhoodIdList: Option[List[Int]] = neighborhoods.map(parseIntegerList) + if (labelTypeId.isDefined && !LabelTable.valLabelTypeIds.contains(labelTypeId.get)) { + Future.successful(BadRequest(s"Invalid label type ID: ${labelTypeId.get}. Valid label type IDs are: ${LabelTable.valLabelTypeIds.mkString(", ")}.")) + } else if (userIdsList.isDefined && userIdsList.get.exists(u => UserTable.findById(UUID.fromString(u)).isEmpty)) { // UserTable.find() works for usernames + Future.successful(BadRequest(s"User not found with given ID: ${userIds.get}.")) + } else if (neighborhoodIdList.isDefined && neighborhoodIdList.get.exists(n => RegionTable.getRegion(n).isEmpty)) { + Future.successful(BadRequest(s"No neighborhood found with given ID: TODO.")) + } else { + val adminParams = AdminValidateParams(adminVersion = true, labelTypeId, userIdsList, neighborhoodIdList) + 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")) } @@ -143,8 +154,8 @@ class ValidationController @Inject() (implicit val env: Environment[User, Sessio val labelsToValidate: Int = MissionTable.validationMissionLabelsToRetrieve val labelsToRetrieve: Int = labelsToValidate - labelsProgress - // Get list of labels and their metadata for Validate page. Get extra data if it's for Admin Validate. - val labelMetadata: Seq[LabelValidationMetadata] = LabelTable.retrieveLabelListForValidation(userId, labelsToRetrieve, labelType, skippedLabelId = None) + // 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)) diff --git a/app/controllers/ValidationTaskController.scala b/app/controllers/ValidationTaskController.scala index b7586129fa..f417884fae 100644 --- a/app/controllers/ValidationTaskController.scala +++ b/app/controllers/ValidationTaskController.scala @@ -275,7 +275,7 @@ class ValidationTaskController @Inject() (implicit val env: Environment[User, Se */ def getLabelListForValidation(userId: UUID, n: Int, labelTypeId: Int, adminParams: AdminValidateParams): JsValue = { // Get list of labels and their metadata for Validate page. Get extra data if it's for Admin Validate. - val labelMetadata: Seq[LabelValidationMetadata] = LabelTable.retrieveLabelListForValidation(userId, n, labelTypeId, skippedLabelId = None) + val labelMetadata: Seq[LabelValidationMetadata] = LabelTable.retrieveLabelListForValidation(userId, n, labelTypeId, 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)) @@ -310,7 +310,7 @@ class ValidationTaskController @Inject() (implicit val env: Environment[User, Se } val userId: UUID = request.identity.get.userId - val labelMetadata: LabelValidationMetadata = LabelTable.retrieveLabelListForValidation(userId, n = 1, labelTypeId, Some(skippedLabelId)).head + val labelMetadata: LabelValidationMetadata = LabelTable.retrieveLabelListForValidation(userId, n = 1, labelTypeId=labelTypeId, skippedLabelId=Some(skippedLabelId)).head LabelFormat.validationLabelMetadataToJson(labelMetadata) } Future.successful(Ok(labelMetadataJson.head)) diff --git a/app/controllers/helper/ValidateHelper.scala b/app/controllers/helper/ValidateHelper.scala index 29ed75612c..52aa35a322 100644 --- a/app/controllers/helper/ValidateHelper.scala +++ b/app/controllers/helper/ValidateHelper.scala @@ -1,22 +1,9 @@ package controllers.helper -import models.user.User -import play.api.{Logger, Play} -import play.api.Play.current -import play.api.mvc.Request -import scala.concurrent.{ExecutionContext, Future} -import scala.util.matching.Regex -import org.apache.http.NameValuePair -import org.apache.http.client.entity.UrlEncodedFormEntity -import org.apache.http.client.methods.HttpPost -import org.apache.http.impl.client.DefaultHttpClient -import org.apache.http.message.BasicNameValuePair -import java.io.InputStream -import java.util -import scala.util.Try - object ValidateHelper { - case class AdminValidateParams(adminVersion: Boolean, labelTypeId: Option[Int] = None) { + case class AdminValidateParams(adminVersion: Boolean, labelTypeId: Option[Int]=None, userIds: Option[List[String]]=None, neighborhoodIds: Option[List[Int]]=None) { require(labelTypeId.isEmpty || adminVersion, "labelTypeId can only be set if adminVersion is true") + require(userIds.isEmpty || adminVersion, "userIds can only be set if adminVersion is true") + require(neighborhoodIds.isEmpty || adminVersion, "neighborhoodIds can only be set if adminVersion is true") } } \ No newline at end of file diff --git a/app/formats/json/ValidationTaskSubmissionFormats.scala b/app/formats/json/ValidationTaskSubmissionFormats.scala index 787f65d8da..cb9bf24c64 100644 --- a/app/formats/json/ValidationTaskSubmissionFormats.scala +++ b/app/formats/json/ValidationTaskSubmissionFormats.scala @@ -71,7 +71,9 @@ object ValidationTaskSubmissionFormats { implicit val adminValidateParamsReads: Reads[AdminValidateParams] = ( (JsPath \ "admin_version").read[Boolean] and - (JsPath \ "label_type_id").readNullable[Int] + (JsPath \ "label_type_id").readNullable[Int] and + (JsPath \ "user_ids").readNullable[List[String]] and + (JsPath \ "neighborhood_ids").readNullable[List[Int]] )(AdminValidateParams.apply _) implicit val validationTaskSubmissionReads: Reads[ValidationTaskSubmission] = ( diff --git a/app/models/label/LabelTable.scala b/app/models/label/LabelTable.scala index d3577584d9..00d0d11831 100644 --- a/app/models/label/LabelTable.scala +++ b/app/models/label/LabelTable.scala @@ -542,15 +542,18 @@ object LabelTable { * @param userId User ID for the current user. * @param n Number of labels we need to query. * @param labelTypeId Label Type ID of labels requested. + * @param userIds Optional list of user IDs to filter by. + * @param regionIds Optional list of region IDs to filter by. * @param skippedLabelId Label ID of the label that was just skipped (if applicable). * @return Seq[LabelValidationMetadata] */ - def retrieveLabelListForValidation(userId: UUID, n: Int, labelTypeId: Int, skippedLabelId: Option[Int]): Seq[LabelValidationMetadata] = db.withSession { implicit session => - var selectedLabels: ListBuffer[LabelValidationMetadata] = new ListBuffer[LabelValidationMetadata]() + def retrieveLabelListForValidation(userId: UUID, n: Int, labelTypeId: Int, userIds: Option[List[String]]=None, regionIds: Option[List[Int]]=None, skippedLabelId: Option[Int]=None): Seq[LabelValidationMetadata] = db.withSession { implicit session => + val selectedLabels: ListBuffer[LabelValidationMetadata] = new ListBuffer[LabelValidationMetadata]() var potentialLabels: List[LabelValidationMetadata] = List() - val userIdStr = userId.toString + val checkedLabelIds: ListBuffer[Int] = ListBuffer[Int]() + val userIdStr: String = userId.toString - while (selectedLabels.length < n) { + do { val selectRandomLabelsQuery = Q.queryNA[LabelValidationMetadata] ( s"""SELECT label.label_id, label_type.label_type, label.gsv_panorama_id, gsv_data.capture_date, | label.time_created, label_point.heading, label_point.pitch, label_point.zoom, label_point.canvas_x, @@ -597,12 +600,15 @@ object LabelTable { | AND label.street_edge_id <> $tutorialStreetId | AND audit_task.street_edge_id <> $tutorialStreetId | AND gsv_data.expired = FALSE + | AND ${regionIds.map(ids => s"street_edge_region.region_id IN (${ids.mkString(",")})").getOrElse("TRUE")} + | AND ${userIds.map(ids => s"mission.user_id IN ('${ids.mkString("','")}')").getOrElse("TRUE")} | AND mission.user_id <> '$userIdStr' | AND label.label_id NOT IN ( | SELECT label_id | FROM label_validation | WHERE user_id = '$userIdStr' | ) + | AND ${if (checkedLabelIds.isEmpty) "TRUE" else s"label.label_id NOT IN (${checkedLabelIds.mkString(",")})"} |-- Generate a priority value for each label that we sort by, between 0 and 276. A label gets 100 points if |-- the labeler has < 50 of their labels validated. Another 50 points if the labeler was marked as high |-- quality. And up to 100 more points (100 / (1 + validation_count)) depending on the number of previous @@ -625,14 +631,14 @@ object LabelTable { // Remove label that was just skipped (if one was skipped). potentialLabels = potentialLabels.filter(_.labelId != skippedLabelId.getOrElse(-1)) - // Randomize those n * 5 high priority labels to prevent repeated and similar labels in a mission. - // https://github.com/ProjectSidewalk/SidewalkWebpage/issues/1874 - // https://github.com/ProjectSidewalk/SidewalkWebpage/issues/1823 + // Randomize those n * 5 high priority labels to prevent similar labels in a mission. potentialLabels = scala.util.Random.shuffle(potentialLabels) // Take the first `n` labels with non-expired GSV imagery. selectedLabels ++= checkForGsvImagery(potentialLabels, n) - } + + checkedLabelIds ++= potentialLabels.map(_.labelId) + } while (selectedLabels.length < n && potentialLabels.length == n * 5) // Stop if we have enough or we run out. selectedLabels } diff --git a/app/views/validation.scala.html b/app/views/validation.scala.html index 76e86b0d19..7511befcb1 100644 --- a/app/views/validation.scala.html +++ b/app/views/validation.scala.html @@ -283,6 +283,8 @@

    Previous Validations

    param.language = "@lang.code"; param.adminVersion = @adminParams.adminVersion; param.adminLabelTypeId = @Html(adminParams.labelTypeId.map(_.toString).getOrElse("null")); + param.adminUserIds = @Html(adminParams.userIds.map(_.mkString("['", "','", "']")).getOrElse("null")); + param.adminNeighborhoodIds = @Html(adminParams.neighborhoodIds.map(_.mkString("[", ",", "]")).getOrElse("null")); param.modalText = { 1: "@Messages("labeling.guide.curb.ramp.summary")", 2: "@Messages("labeling.guide.curb.ramp.summary")", diff --git a/conf/routes b/conf/routes index ccf58d11f4..33880836f6 100644 --- a/conf/routes +++ b/conf/routes @@ -102,7 +102,7 @@ GET /audit/street/:id @controllers.AuditC # Label validation tasks GET /validate @controllers.ValidationController.validate -GET /adminValidate @controllers.ValidationController.adminValidate(labelTypeId: Option[Int] ?= None) +GET /adminValidate @controllers.ValidationController.adminValidate(labelTypeId: Option[Int] ?= None, userIds: Option[String] ?= None, neighborhoods: Option[String] ?= None) POST /validate/comment @controllers.ValidationController.postComment # Task API. diff --git a/public/javascripts/SVValidate/src/Main.js b/public/javascripts/SVValidate/src/Main.js index 50beb851cb..77f6ab1a38 100644 --- a/public/javascripts/SVValidate/src/Main.js +++ b/public/javascripts/SVValidate/src/Main.js @@ -10,6 +10,8 @@ var svv = svv || {}; function Main (param) { svv.adminVersion = param.adminVersion; svv.adminLabelTypeId = param.adminLabelTypeId; + svv.adminUserIds = param.adminUserIds; + svv.adminNeighborhoodIds = param.adminNeighborhoodIds; svv.canvasHeight = param.canvasHeight; svv.canvasWidth = param.canvasWidth; svv.missionsCompleted = param.missionSetProgress; diff --git a/public/javascripts/SVValidate/src/data/Form.js b/public/javascripts/SVValidate/src/data/Form.js index 5089821241..e10d19943c 100644 --- a/public/javascripts/SVValidate/src/data/Form.js +++ b/public/javascripts/SVValidate/src/data/Form.js @@ -55,7 +55,9 @@ function Form(url, beaconUrl) { data.admin_params = { admin_version: svv.adminVersion, - label_type_id: svv.adminLabelTypeId + label_type_id: svv.adminLabelTypeId, + user_ids: svv.adminUserIds, + neighborhood_ids: svv.adminNeighborhoodIds } data.interactions = svv.tracker.getActions(); From 7ca81e014aa4304de73fa703af5985b63ec173ec Mon Sep 17 00:00:00 2001 From: Mikey Saugstad Date: Mon, 4 Mar 2024 13:38:24 -0800 Subject: [PATCH 07/12] Admin Validate more gracefully handles running out of labels --- app/controllers/ValidationController.scala | 3 +- .../ValidationTaskController.scala | 3 +- .../SVValidate/src/modal/ModalNoNewMission.js | 2 +- .../src/panorama/PanoramaContainer.js | 29 +++++++++++-------- 4 files changed, 22 insertions(+), 15 deletions(-) diff --git a/app/controllers/ValidationController.scala b/app/controllers/ValidationController.scala index 11a87729de..413553a552 100644 --- a/app/controllers/ValidationController.scala +++ b/app/controllers/ValidationController.scala @@ -132,8 +132,9 @@ class ValidationController @Inject() (implicit val env: Environment[User, Sessio 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. diff --git a/app/controllers/ValidationTaskController.scala b/app/controllers/ValidationTaskController.scala index f417884fae..f7a223068e 100644 --- a/app/controllers/ValidationTaskController.scala +++ b/app/controllers/ValidationTaskController.scala @@ -87,7 +87,8 @@ class ValidationTaskController @Inject() (implicit val env: Environment[User, Se val possibleNewMission: Option[Mission] = updateMissionTable(userOption, missionProgress, Some(nextMissionLabelTypeId)) val labelList: Option[JsValue] = getLabelList(userOption, missionProgress, nextMissionLabelTypeId, adminParams) val progress: Option[JsObject] = Some(LabelValidationTable.getValidationProgress(possibleNewMission.get.missionId)) - ValidationTaskPostReturnValue(Some (true), possibleNewMission, labelList, progress) + val hasDataForMission: Boolean = labelList.toString != "[]" + ValidationTaskPostReturnValue(Some(hasDataForMission), possibleNewMission, labelList, progress) case None => updateMissionTable(userOption, missionProgress, None) // No more validation missions available. diff --git a/public/javascripts/SVValidate/src/modal/ModalNoNewMission.js b/public/javascripts/SVValidate/src/modal/ModalNoNewMission.js index e7a4e0fc29..fde895bb8b 100644 --- a/public/javascripts/SVValidate/src/modal/ModalNoNewMission.js +++ b/public/javascripts/SVValidate/src/modal/ModalNoNewMission.js @@ -26,7 +26,7 @@ function ModalNoNewMission (uiModalMission) { } } - function show () { + function show() { if (svv.keyboard) { svv.keyboard.disableKeyboard(); } diff --git a/public/javascripts/SVValidate/src/panorama/PanoramaContainer.js b/public/javascripts/SVValidate/src/panorama/PanoramaContainer.js index aa733f35ee..8b6348283f 100644 --- a/public/javascripts/SVValidate/src/panorama/PanoramaContainer.js +++ b/public/javascripts/SVValidate/src/panorama/PanoramaContainer.js @@ -70,19 +70,24 @@ function PanoramaContainer (labelList) { /** * Loads a new label onto a panorama after the user validates a label. */ - function loadNewLabelOntoPanorama () { - svv.panorama.setLabel(labels[getProperty('progress')]); - setProperty('progress', getProperty('progress') + 1); - if (svv.labelVisibilityControl && !svv.labelVisibilityControl.isVisible()) { - svv.labelVisibilityControl.unhideLabel(true); - } + function loadNewLabelOntoPanorama() { + // If no more labels are left, show no more validations modal (should on happen on Admin Validate). + if (labels[getProperty('progress')] === undefined) { + svv.modalNoNewMission.show(); + } else { + svv.panorama.setLabel(labels[getProperty('progress')]); + setProperty('progress', getProperty('progress') + 1); + if (svv.labelVisibilityControl && !svv.labelVisibilityControl.isVisible()) { + svv.labelVisibilityControl.unhideLabel(true); + } - // Update zoom availability on desktop. - if (svv.zoomControl) { - svv.zoomControl.updateZoomAvailability(); - } + // Update zoom availability on desktop. + if (svv.zoomControl) { + svv.zoomControl.updateZoomAvailability(); + } - if (svv.adminVersion) svv.statusField.updateAdminInfo(); + if (svv.adminVersion) svv.statusField.updateAdminInfo(); + } } function getCurrentLabel() { @@ -92,7 +97,7 @@ function PanoramaContainer (labelList) { /** * Resets the validation interface for a new mission. Loads a new set of label onto the panoramas. */ - function reset () { + function reset() { setProperty('progress', 0); loadNewLabelOntoPanorama(); } From bed784e1c0f727db6ffd7c8fc8e42e23196cd060 Mon Sep 17 00:00:00 2001 From: Mikey Saugstad Date: Mon, 4 Mar 2024 14:01:49 -0800 Subject: [PATCH 08/12] skipping label now works correctly for Admin Validate: --- .../ValidationTaskController.scala | 23 +++++++++++-------- .../ValidationTaskSubmissionFormats.scala | 7 +++--- .../javascripts/SVValidate/src/data/Form.js | 2 +- .../src/panorama/PanoramaContainer.js | 20 ++++++++-------- 4 files changed, 30 insertions(+), 22 deletions(-) diff --git a/app/controllers/ValidationTaskController.scala b/app/controllers/ValidationTaskController.scala index f7a223068e..4f69621cdf 100644 --- a/app/controllers/ValidationTaskController.scala +++ b/app/controllers/ValidationTaskController.scala @@ -297,7 +297,7 @@ class ValidationTaskController @Inject() (implicit val env: Environment[User, Se * @return Label metadata containing GSV metadata and label type */ def getRandomLabelData(labelTypeId: Int, skippedLabelId: Int) = UserAwareAction.async(BodyParsers.parse.json) { implicit request => - var submission = request.body.validate[Seq[SkipLabelSubmission]] + var submission = request.body.validate[SkipLabelSubmission] submission.fold( errors => { Future.successful(BadRequest(Json.obj("status" -> "Error", "message" -> JsError.toFlatJson(errors)))) @@ -305,16 +305,21 @@ class ValidationTaskController @Inject() (implicit val env: Environment[User, Se submission => { var labelIdList = new ListBuffer[Int]() - val labelMetadataJson: Seq[JsObject] = for (data <- submission) yield { - for (label: LabelValidationSubmission <- data.labels) { - labelIdList += label.labelId - } - - val userId: UUID = request.identity.get.userId - val labelMetadata: LabelValidationMetadata = LabelTable.retrieveLabelListForValidation(userId, n = 1, labelTypeId=labelTypeId, skippedLabelId=Some(skippedLabelId)).head + for (label: LabelValidationSubmission <- submission.labels) { + labelIdList += label.labelId + } + val adminParams: AdminValidateParams = + if (submission.adminParams.adminVersion && isAdmin(request.identity)) submission.adminParams + else AdminValidateParams(adminVersion = false) + val userId: UUID = request.identity.get.userId + val labelMetadata: LabelValidationMetadata = LabelTable.retrieveLabelListForValidation(userId, n=1, labelTypeId, adminParams.userIds, adminParams.neighborhoodIds, skippedLabelId=Some(skippedLabelId)).head + val labelMetadataJson: JsObject = if (adminParams.adminVersion) { + val adminData: AdminValidationData = LabelTable.getExtraAdminValidateData(List(labelMetadata.labelId)).head + LabelFormat.validationLabelMetadataToJson(labelMetadata, Some(adminData)) + } else { LabelFormat.validationLabelMetadataToJson(labelMetadata) } - Future.successful(Ok(labelMetadataJson.head)) + Future.successful(Ok(labelMetadataJson)) } ) } diff --git a/app/formats/json/ValidationTaskSubmissionFormats.scala b/app/formats/json/ValidationTaskSubmissionFormats.scala index cb9bf24c64..9ae4200f16 100644 --- a/app/formats/json/ValidationTaskSubmissionFormats.scala +++ b/app/formats/json/ValidationTaskSubmissionFormats.scala @@ -10,7 +10,7 @@ object ValidationTaskSubmissionFormats { case class EnvironmentSubmission(missionId: Option[Int], browser: Option[String], browserVersion: Option[String], browserWidth: Option[Int], browserHeight: Option[Int], availWidth: Option[Int], availHeight: Option[Int], screenWidth: Option[Int], screenHeight: Option[Int], operatingSystem: Option[String], language: String, cssZoom: Int) case class InteractionSubmission(action: String, missionId: Option[Int], gsvPanoramaId: Option[String], lat: Option[Float], lng: Option[Float], heading: Option[Float], pitch: Option[Float], zoom: Option[Float], note: Option[String], timestamp: Long, isMobile: Boolean) case class LabelValidationSubmission(labelId: Int, missionId: Int, validationResult: Int, canvasX: Option[Int], canvasY: Option[Int], heading: Float, pitch: Float, zoom: Float, canvasHeight: Int, canvasWidth: Int, startTimestamp: Long, endTimestamp: Long, source: String) - case class SkipLabelSubmission(labels: Seq[LabelValidationSubmission]) + case class SkipLabelSubmission(labels: Seq[LabelValidationSubmission], adminParams: AdminValidateParams) case class ValidationMissionProgress(missionId: Int, missionType: String, labelsProgress: Int, labelTypeId: Int, completed: Boolean, skipped: Boolean) case class ValidationTaskSubmission(interactions: Seq[InteractionSubmission], environment: EnvironmentSubmission, labels: Seq[LabelValidationSubmission], missionProgress: Option[ValidationMissionProgress], adminParams: AdminValidateParams, timestamp: Long) case class LabelMapValidationSubmission(labelId: Int, labelType: String, validationResult: Int, canvasX: Option[Int], canvasY: Option[Int], heading: Float, pitch: Float, zoom: Float, canvasHeight: Int, canvasWidth: Int, startTimestamp: Long, endTimestamp: Long, source: String) @@ -102,6 +102,7 @@ object ValidationTaskSubmissionFormats { )(LabelMapValidationSubmission.apply _) implicit val skipLabelReads: Reads[SkipLabelSubmission] = ( - (JsPath \ "labels").read[Seq[LabelValidationSubmission]] - ).map(SkipLabelSubmission(_)) + (JsPath \ "labels").read[Seq[LabelValidationSubmission]] and + (JsPath \ "admin_params").read[AdminValidateParams] + )(SkipLabelSubmission.apply _) } diff --git a/public/javascripts/SVValidate/src/data/Form.js b/public/javascripts/SVValidate/src/data/Form.js index e10d19943c..ca940bbc36 100644 --- a/public/javascripts/SVValidate/src/data/Form.js +++ b/public/javascripts/SVValidate/src/data/Form.js @@ -58,7 +58,7 @@ function Form(url, beaconUrl) { label_type_id: svv.adminLabelTypeId, user_ids: svv.adminUserIds, neighborhood_ids: svv.adminNeighborhoodIds - } + }; data.interactions = svv.tracker.getActions(); svv.tracker.refresh(); diff --git a/public/javascripts/SVValidate/src/panorama/PanoramaContainer.js b/public/javascripts/SVValidate/src/panorama/PanoramaContainer.js index 8b6348283f..11b2762ac7 100644 --- a/public/javascripts/SVValidate/src/panorama/PanoramaContainer.js +++ b/public/javascripts/SVValidate/src/panorama/PanoramaContainer.js @@ -17,7 +17,7 @@ function PanoramaContainer (labelList) { * Initializes panorama(s) on the validate page. * @private */ - function _init () { + function _init() { svv.panorama = new Panorama(labelList[getProperty("progress")]); setProperty("progress", getProperty("progress") + 1); @@ -32,16 +32,18 @@ function PanoramaContainer (labelList) { * because missions fetch exactly the number of labels that are needed to complete the mission. * @param skippedLabelId the ID of the label that we are skipping */ - function fetchNewLabel (skippedLabelId) { + function fetchNewLabel(skippedLabelId) { let labelTypeId = svv.missionContainer.getCurrentMission().getProperty('labelTypeId'); let labelUrl = '/label/geo/random/' + labelTypeId + '/' + skippedLabelId; let data = {}; data.labels = svv.labelContainer.getCurrentLabels(); - - if (data.constructor !== Array) { - data = [data]; - } + data.admin_params = { + admin_version: svv.adminVersion, + label_type_id: svv.adminLabelTypeId, + user_ids: svv.adminUserIds, + neighborhood_ids: svv.adminNeighborhoodIds + }; $.ajax({ async: false, @@ -63,7 +65,7 @@ function PanoramaContainer (labelList) { * @param key Property name. * @returns Value associated with this property or null. */ - function getProperty (key) { + function getProperty(key) { return key in properties ? properties[key] : null; } @@ -107,7 +109,7 @@ function PanoramaContainer (labelList) { * Called when a new mission is loaded onto the screen. * @param labelList Object containing key-value pairings of {index: labelMetadata} */ - function setLabelList (labelList) { + function setLabelList(labelList) { Object.keys(labelList).map(function(key, index) { labelList[key] = new Label(labelList[key]); }); @@ -121,7 +123,7 @@ function PanoramaContainer (labelList) { * @param value Value of property. * @returns {setProperty} */ - function setProperty (key, value) { + function setProperty(key, value) { properties[key] = value; return this; } From 40ec8cafc5409d8c006ec8decc50cd2f7cf0d4ac Mon Sep 17 00:00:00 2001 From: Mikey Saugstad Date: Mon, 4 Mar 2024 14:29:18 -0800 Subject: [PATCH 09/12] Admin Validate changes Username header to say Labeler --- app/views/validation.scala.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/validation.scala.html b/app/views/validation.scala.html index 7511befcb1..938d3599fc 100644 --- a/app/views/validation.scala.html +++ b/app/views/validation.scala.html @@ -220,7 +220,7 @@

    } else {

    Admin Info

    -

    Username

    +

    Labeler

    Previous Validations

    From 120cb41a350d7b05f4b52534a4938edc469efec1 Mon Sep 17 00:00:00 2001 From: Mikey Saugstad Date: Mon, 4 Mar 2024 15:51:28 -0800 Subject: [PATCH 10/12] Admin Validate now accepts params as ids OR with their names --- app/controllers/AdminController.scala | 2 +- app/controllers/AttributeController.scala | 4 +- app/controllers/LabelController.scala | 2 +- app/controllers/TaskController.scala | 2 +- app/controllers/ValidationController.scala | 59 ++++++++++++++----- .../ValidationTaskController.scala | 4 +- .../attribute/GlobalAttributeTable.scala | 2 +- app/models/label/LabelTypeTable.scala | 16 +++-- app/models/region/RegionTable.scala | 4 ++ conf/routes | 2 +- 10 files changed, 66 insertions(+), 31 deletions(-) diff --git a/app/controllers/AdminController.scala b/app/controllers/AdminController.scala index 8aa4efd60f..5dc243283e 100644 --- a/app/controllers/AdminController.scala +++ b/app/controllers/AdminController.scala @@ -161,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) diff --git a/app/controllers/AttributeController.scala b/app/controllers/AttributeController.scala index 66115033b7..bc7854bfd2 100644 --- a/app/controllers/AttributeController.scala +++ b/app/controllers/AttributeController.scala @@ -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, @@ -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, diff --git a/app/controllers/LabelController.scala b/app/controllers/LabelController.scala index e615124e34..c8a8b0c0c2 100644 --- a/app/controllers/LabelController.scala +++ b/app/controllers/LabelController.scala @@ -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 )}))) } diff --git a/app/controllers/TaskController.scala b/app/controllers/TaskController.scala index 5de72d40df..3037d2b46a 100644 --- a/app/controllers/TaskController.scala +++ b/app/controllers/TaskController.scala @@ -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) diff --git a/app/controllers/ValidationController.scala b/app/controllers/ValidationController.scala index 413553a552..f73e3708c2 100644 --- a/app/controllers/ValidationController.scala +++ b/app/controllers/ValidationController.scala @@ -7,17 +7,16 @@ 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.{isAdmin, parseIntegerList, isMobile} +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, LabelTypeTable, LabelValidationTable} import models.label.LabelTable.{AdminValidationData, LabelValidationMetadata} -import models.label.LabelValidationTable import models.mission.{Mission, MissionSetProgress, MissionTable} -import models.region.RegionTable +import models.region.{Region, RegionTable} import models.validation._ import models.user._ import play.api.libs.json._ @@ -25,6 +24,7 @@ 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. @@ -76,22 +76,49 @@ class ValidationController @Inject() (implicit val env: Environment[User, Sessio /** * 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(labelTypeId: Option[Int], userIds: Option[String], neighborhoods: Option[String]) = UserAwareAction.async { implicit request => + 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 to the user. - val userIdsList: Option[List[String]] = userIds.map(_.split(',').map(_.trim).toList) - val neighborhoodIdList: Option[List[Int]] = neighborhoods.map(parseIntegerList) - if (labelTypeId.isDefined && !LabelTable.valLabelTypeIds.contains(labelTypeId.get)) { - Future.successful(BadRequest(s"Invalid label type ID: ${labelTypeId.get}. Valid label type IDs are: ${LabelTable.valLabelTypeIds.mkString(", ")}.")) - } else if (userIdsList.isDefined && userIdsList.get.exists(u => UserTable.findById(UUID.fromString(u)).isEmpty)) { // UserTable.find() works for usernames - Future.successful(BadRequest(s"User not found with given ID: ${userIds.get}.")) - } else if (neighborhoodIdList.isDefined && neighborhoodIdList.get.exists(n => RegionTable.getRegion(n).isEmpty)) { - Future.successful(BadRequest(s"No neighborhood found with given ID: TODO.")) + // 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 { - val adminParams = AdminValidateParams(adminVersion = true, labelTypeId, userIdsList, neighborhoodIdList) - val validationData = getDataForValidationPages(request.identity.get, ipAddress, labelCount = 10, "Visit_AdminValidate", adminParams) + // 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 { diff --git a/app/controllers/ValidationTaskController.scala b/app/controllers/ValidationTaskController.scala index 4f69621cdf..b71c5a1a5f 100644 --- a/app/controllers/ValidationTaskController.scala +++ b/app/controllers/ValidationTaskController.scala @@ -172,7 +172,7 @@ class ValidationTaskController @Inject() (implicit val env: Environment[User, Se }, submission => { // Get the (or create a) mission_id for this user_id and label_type_id. - val labelTypeId: Int = LabelTypeTable.labelTypeToId(submission.labelType) + val labelTypeId: Int = LabelTypeTable.labelTypeToId(submission.labelType).get val mission: Mission = MissionTable.resumeOrCreateNewValidationMission(userId, 0.0D, 0.0D, "labelmapValidation", labelTypeId).get @@ -203,7 +203,7 @@ class ValidationTaskController @Inject() (implicit val env: Environment[User, Se val userId: UUID = request.identity.get.userId // Get the (or create a) mission_id for this user_id and label_type_id. - val labelTypeId: Int = LabelTypeTable.labelTypeToId(submission.labelType) + val labelTypeId: Int = LabelTypeTable.labelTypeToId(submission.labelType).get val mission: Mission = MissionTable.resumeOrCreateNewValidationMission(userId, 0.0D, 0.0D, "labelmapValidation", labelTypeId).get diff --git a/app/models/attribute/GlobalAttributeTable.scala b/app/models/attribute/GlobalAttributeTable.scala index 4a3485336e..6caeb29ab4 100644 --- a/app/models/attribute/GlobalAttributeTable.scala +++ b/app/models/attribute/GlobalAttributeTable.scala @@ -367,7 +367,7 @@ object GlobalAttributeTable { globalAttributes .filter(_.labelTypeId inSet List(2, 3, 4, 7)) .groupBy(a => (a.regionId, a.labelTypeId)).map { case ((rId, typeId), group) => (rId, typeId, group.length) } - .list.map{ case (rId, typeId, count) => (rId, LabelTypeTable.labelTypeIdToLabelType(typeId), count) } + .list.map{ case (rId, typeId, count) => (rId, LabelTypeTable.labelTypeIdToLabelType(typeId).get, count) } } def countGlobalAttributes: Int = db.withTransaction { implicit session => diff --git a/app/models/label/LabelTypeTable.scala b/app/models/label/LabelTypeTable.scala index c36c7c3a35..998d2fc3a1 100644 --- a/app/models/label/LabelTypeTable.scala +++ b/app/models/label/LabelTypeTable.scala @@ -24,31 +24,35 @@ object LabelTypeTable { def validLabelTypes: Set[String] = Set("CurbRamp", "NoCurbRamp", "Obstacle", "SurfaceProblem", "Other", "Occlusion", "NoSidewalk", "Crosswalk", "Signal") def primaryLabelTypes: Set[String] = Set("CurbRamp", "NoCurbRamp", "Obstacle", "SurfaceProblem", "NoSidewalk") + def getAllLabelTypes: Set[LabelType] = db.withSession { implicit session => + labelTypes.list.toSet + } + /** * Set of valid label type ids for the above valid label types. */ - def validLabelTypeIds: Set[Int] = db.withTransaction { implicit session => + def validLabelTypeIds: Set[Int] = db.withSession { implicit session => labelTypes.filter(_.labelType inSet validLabelTypes).map(_.labelTypeId).list.toSet } /** * Set of primary label type ids for the above valid label types. */ - def primaryLabelTypeIds: Set[Int] = db.withTransaction { implicit session => + def primaryLabelTypeIds: Set[Int] = db.withSession { implicit session => labelTypes.filter(_.labelType inSet primaryLabelTypes).map(_.labelTypeId).list.toSet } /** * Gets the label type id from the label type name. */ - def labelTypeToId(labelType: String): Int = db.withTransaction { implicit session => - labelTypes.filter(_.labelType === labelType).map(_.labelTypeId).first + def labelTypeToId(labelType: String): Option[Int] = db.withSession { implicit session => + labelTypes.filter(_.labelType === labelType).map(_.labelTypeId).firstOption } /** * Gets the label type name from the label type id. */ - def labelTypeIdToLabelType(labelTypeId: Int): String = db.withTransaction { implicit session => - labelTypes.filter(_.labelTypeId === labelTypeId).map(_.labelType).first + def labelTypeIdToLabelType(labelTypeId: Int): Option[String] = db.withSession { implicit session => + labelTypes.filter(_.labelTypeId === labelTypeId).map(_.labelType).firstOption } } diff --git a/app/models/region/RegionTable.scala b/app/models/region/RegionTable.scala index cbc76f2eaf..0a7befc6fa 100644 --- a/app/models/region/RegionTable.scala +++ b/app/models/region/RegionTable.scala @@ -111,6 +111,10 @@ object RegionTable { regionsWithoutDeleted.filter(_.regionId === regionId).firstOption } + def getRegionByName(regionName: String): Option[Region] = db.withSession { implicit session => + regionsWithoutDeleted.filter(_.name === regionName).firstOption + } + /** * Get the neighborhood that is currently assigned to the user. */ diff --git a/conf/routes b/conf/routes index 33880836f6..47816c2e0a 100644 --- a/conf/routes +++ b/conf/routes @@ -102,7 +102,7 @@ GET /audit/street/:id @controllers.AuditC # Label validation tasks GET /validate @controllers.ValidationController.validate -GET /adminValidate @controllers.ValidationController.adminValidate(labelTypeId: Option[Int] ?= None, userIds: Option[String] ?= None, neighborhoods: Option[String] ?= None) +GET /adminValidate @controllers.ValidationController.adminValidate(labelType: Option[String] ?= None, users: Option[String] ?= None, neighborhoods: Option[String] ?= None) POST /validate/comment @controllers.ValidationController.postComment # Task API. From de39e922b6c72616484b8a86b3f4efa6d80f8ddd Mon Sep 17 00:00:00 2001 From: Mikey Saugstad Date: Mon, 4 Mar 2024 16:02:46 -0800 Subject: [PATCH 11/12] Admin Validate is now using translations we already had --- public/javascripts/SVValidate/src/status/StatusField.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/javascripts/SVValidate/src/status/StatusField.js b/public/javascripts/SVValidate/src/status/StatusField.js index a6759f5145..29c22a49f7 100644 --- a/public/javascripts/SVValidate/src/status/StatusField.js +++ b/public/javascripts/SVValidate/src/status/StatusField.js @@ -118,7 +118,7 @@ function StatusField(param) { $(`

    None

    `).insertAfter('#curr-label-prev-validations'); } else { for (const prevVal of svv.panorama.getCurrentLabel().getAdminProperty('previousValidations')) { - $(`

    ${prevVal.username}: ${prevVal.validation}

    `) + $(`

    ${prevVal.username}: ${i18next.t(`common:${util.camelToKebab(prevVal.validation)}`)}

    `) .insertAfter('#curr-label-prev-validations'); } } From 9c6f215d8cc566a3b3d3011fa0a21f12e9e03a0f Mon Sep 17 00:00:00 2001 From: Mikey Saugstad Date: Mon, 4 Mar 2024 16:25:37 -0800 Subject: [PATCH 12/12] adds instructions for Admin Validate URL params --- app/views/validation.scala.html | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/app/views/validation.scala.html b/app/views/validation.scala.html index 938d3599fc..5a2348bc7b 100644 --- a/app/views/validation.scala.html +++ b/app/views/validation.scala.html @@ -220,6 +220,18 @@

    } else {

    Admin Info

    +

    + You can choose to validate a specific label type, user, or neighborhood by editing the URL and + pressing enter. For example: +
    ?labelType=CurbRamp +
    ?users=User1 +
    ?neighborhoods=Neighborhood1 +
    You can also combine the filters by using the & symbol. For example: +
    ?labelType=CurbRamp&users=User1 +
    You can also filter for multiple users or neighborhoods by separating them with commas. + For example: +
    ?users=User1,User2 +

    Labeler

    Previous Validations