diff --git a/Gruntfile.js b/Gruntfile.js index 98d0b6ad63..6184ea2896 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -39,6 +39,9 @@ module.exports = function(grunt) { }, dist_progress: { src: [ + 'public/javascripts/Admin/src/*.js', + 'public/javascripts/SVValidate/src/util/*.js', + 'public/javascripts/common/Panomarker.js', 'public/javascripts/Progress/src/*.js', 'public/javascripts/common/Utilities.js', 'public/javascripts/common/UtilitiesSidewalk.js', diff --git a/app/controllers/AdminController.scala b/app/controllers/AdminController.scala index 8c77f3d416..1acc03dd09 100644 --- a/app/controllers/AdminController.scala +++ b/app/controllers/AdminController.scala @@ -14,10 +14,13 @@ import formats.json.LabelFormat import formats.json.TaskFormats._ import formats.json.AdminUpdateSubmissionFormats._ import formats.json.LabelFormat._ +import formats.json.OrganizationFormats._ +import formats.json.UserFormats._ import javassist.NotFoundException import models.attribute.{GlobalAttribute, GlobalAttributeTable} import models.audit.{AuditTaskInteractionTable, AuditTaskTable, AuditedStreetWithTimestamp, InteractionWithLabel} import models.daos.slick.DBTableDefinitions.UserTable +import models.daos.slick._ import models.gsv.{GSVDataSlim, GSVDataTable} import models.label.LabelTable.{AdminValidationData, LabelMetadata} import models.label.{LabelLocationWithSeverity, LabelPointTable, LabelTable, LabelTypeTable, LabelValidationTable} @@ -275,7 +278,9 @@ class AdminController @Inject() (implicit val env: Environment[User, SessionAuth "audit_task_id" -> label.auditTaskId, "label_id" -> label.labelId, "gsv_panorama_id" -> label.gsvPanoramaId, - "label_type" -> label.labelType + "label_type" -> label.labelType, + "correct" -> label.correct, + "has_validations" -> label.hasValidations ) Json.obj("type" -> "Feature", "geometry" -> point, "properties" -> properties) } @@ -570,17 +575,16 @@ class AdminController @Inject() (implicit val env: Environment[User, SessionAuth val newOrgId: Int = submission.orgId if (isAdmin(request.identity)) { - // Remove any previous org and add the new org. Will add the ability to be in multiple orgs in the future. - val currentOrg: Option[Int] = UserOrgTable.getAllOrgs(userId).headOption + val currentOrg: Option[Int] = UserOrgTable.getOrg(userId) if (currentOrg.nonEmpty) { UserOrgTable.remove(userId, currentOrg.get) } val rowsUpdated: Int = UserOrgTable.save(userId, newOrgId) - if (rowsUpdated > 0) { - Future.successful(Ok(Json.obj("user_id" -> userId, "org_id" -> newOrgId))) + if (rowsUpdated == -1 && currentOrg.isEmpty) { + Future.successful(BadRequest("Update failed")) } else { - Future.successful(BadRequest("Error saving org")) + Future.successful(Ok(Json.obj("user_id" -> userId, "org_id" -> newOrgId))) } } else { Future.failed(new AuthenticationException("User is not an administrator")) @@ -740,4 +744,19 @@ class AdminController @Inject() (implicit val env: Environment[User, SessionAuth ) Future.successful(Ok(data)) } + + /** + * Get the stats for the users table in the admin page. + */ + def getUserStats = UserAwareAction.async { implicit request => + if (isAdmin(request.identity)) { + val data = Json.obj( + "user_stats" -> Json.toJson(UserDAOSlick.getUserStatsForAdminPage), + "organizations" -> Json.toJson(OrganizationTable.getAllOrganizations) + ) + Future.successful(Ok(data)) + } else { + Future.failed(new AuthenticationException("User is not an administrator")) + } + } } diff --git a/app/controllers/CredentialsAuthController.scala b/app/controllers/CredentialsAuthController.scala index 00a09ca8f9..d55806678f 100644 --- a/app/controllers/CredentialsAuthController.scala +++ b/app/controllers/CredentialsAuthController.scala @@ -92,6 +92,8 @@ class CredentialsAuthController @Inject() ( val expirationDate = authenticator.expirationDate.minusSeconds(defaultExpiry).plusSeconds(rememberMeExpiry) val updatedAuthenticator = authenticator.copy(expirationDate=expirationDate, idleTimeout = Some(2592000)) + // Add to user_stat or user_current_region if user hasn't logged in in this city before. + UserStatTable.addUserStatIfNew(user.userId) if (!UserCurrentRegionTable.isAssigned(user.userId)) { UserCurrentRegionTable.assignRegion(user.userId) } diff --git a/app/controllers/SignUpController.scala b/app/controllers/SignUpController.scala index 1fbe67a84b..5ad66eaa06 100644 --- a/app/controllers/SignUpController.scala +++ b/app/controllers/SignUpController.scala @@ -75,14 +75,12 @@ class SignUpController @Inject() ( UserTable.find(data.username) match { case Some(user) => WebpageActivityTable.save(WebpageActivity(0, oldUserId, ipAddress, "Duplicate_Username_Error", timestamp)) - // Future.successful(Redirect(routes.UserController.signUp()).flashing("error" -> Messages("authenticate.error.username.exists"))) Future.successful(Status(409)(Messages("authenticate.error.username.exists"))) case None => // Check presence of user by email. UserTable.findEmail(data.email.toLowerCase) match { case Some(user) => WebpageActivityTable.save(WebpageActivity(0, oldUserId, ipAddress, "Duplicate_Email_Error", timestamp)) - // Future.successful(Redirect(routes.UserController.signUp()).flashing("error" -> Messages("authenticate.error.email.exists"))) Future.successful(Status(409)(Messages("authenticate.error.email.exists"))) case None => // Check if passwords match and are at least 6 characters. @@ -107,8 +105,8 @@ class SignUpController @Inject() ( } yield { // Set the user role, assign the neighborhood to audit, and add to the user_stat table. UserRoleTable.setRole(user.userId, "Registered", Some(serviceHoursUser)) - UserCurrentRegionTable.assignRegion(user.userId) UserStatTable.addUserStatIfNew(user.userId) + UserCurrentRegionTable.assignRegion(user.userId) // Log the sign up/in. val timestamp: Timestamp = new Timestamp(Instant.now.toEpochMilli) @@ -124,9 +122,9 @@ class SignUpController @Inject() ( // If someone was already authenticated (i.e., they were signed into an anon user account), Play // doesn't let us sign one account out and the other back in in one response header. So we start // by redirecting to the "/finishSignUp" endpoint, discarding the old authenticator info and - // sending the new authenticator info in a temp element in the session cookie. The "/finishSignUp" - // endpoint will then move authenticator we put in "temp-authenticator" over to "authenticator" - // where it belongs, finally completing the sign up. + // sending the new authenticator info in a temp element in the session cookie. The + // "/finishSignUp" endpoint will then move authenticator we put in "temp-authenticator" over to + // "authenticator" where it belongs, finally completing the sign up. val redirectURL: String = nextUrl match { case Some(u) => "/finishSignUp?url=" + u case None => "/finishSignUp" @@ -236,6 +234,7 @@ class SignUpController @Inject() ( // Set the user role and add to the user_stat table. UserRoleTable.setRole(user.userId, "Anonymous", Some(false)) UserStatTable.addUserStatIfNew(user.userId) + UserCurrentRegionTable.assignRegion(user.userId) // Add Timestamp val timestamp: Timestamp = new Timestamp(Instant.now.toEpochMilli) @@ -303,8 +302,8 @@ class SignUpController @Inject() ( } yield { // Set the user role, assign the neighborhood to audit, and add to the user_stat table. UserRoleTable.setRole(user.userId, "Turker", Some(false)) - UserCurrentRegionTable.assignRegion(user.userId) UserStatTable.addUserStatIfNew(user.userId) + UserCurrentRegionTable.assignRegion(user.userId) // Log the sign up/in. val timestamp: Timestamp = new Timestamp(Instant.now.toEpochMilli) diff --git a/app/controllers/TaskController.scala b/app/controllers/TaskController.scala index 68d5778dfe..3ee09c6b05 100644 --- a/app/controllers/TaskController.scala +++ b/app/controllers/TaskController.scala @@ -21,12 +21,13 @@ import models.region._ import models.route.{AuditTaskUserRouteTable, UserRouteTable} import models.street.StreetEdgePriorityTable.streetPrioritiesFromIds import models.street.{StreetEdgeIssue, StreetEdgeIssueTable, StreetEdgePriority, StreetEdgePriorityTable} -import models.user.{User, UserCurrentRegionTable} +import models.user.{User, UserCurrentRegionTable, UserStatTable} import models.utils.CommonUtils.ordered import play.api.Play.current import play.api.{Logger, Play} import play.api.libs.json._ import play.api.mvc._ + import scala.collection.mutable.ListBuffer //import scala.concurrent.{ExecutionContext, Future} import scala.concurrent.Future @@ -229,18 +230,13 @@ class TaskController @Inject() (implicit val env: Environment[User, SessionAuthe if (!AuditTaskTable.userHasAuditedStreet(streetEdgeId, user.userId)) { data.auditTask.completed.map { completed => if (completed) { - StreetEdgePriorityTable.partiallyUpdatePriority(streetEdgeId, Some(user.userId.toString)) + StreetEdgePriorityTable.partiallyUpdatePriority(streetEdgeId, user.userId) } } } case None => // Update the street's priority for anonymous user. Logger.warn("User without user_id audited a street, but every user should have a user_id.") - data.auditTask.completed.map { completed => - if (completed) { - StreetEdgePriorityTable.partiallyUpdatePriority(streetEdgeId, None) - } - } } // If street priority went from 1 to < 1 due to this audit, update the region_completion table accordingly. val priorityAfter: StreetEdgePriority = streetPrioritiesFromIds(List(streetEdgeId)).head @@ -315,8 +311,9 @@ class TaskController @Inject() (implicit val env: Environment[User, SessionAuthe _streetId <- LabelTable.getStreetEdgeIdClosestToLatLng(_lat, _lng) } yield _streetId).getOrElse(streetEdgeId) - // Add the new entry to the label table. + // Add the new entry to the label table. Make sure there's also an entry in the user_stat table. val u: String = userOption.map(_.userId.toString).getOrElse(UserTable.find("anonymous").get.userId) + UserStatTable.addUserStatIfNew(UUID.fromString(u)) val newLabelId: Int = LabelTable.save(Label(0, auditTaskId, missionId, u, label.gsvPanoramaId, labelTypeId, label.deleted, label.temporaryLabelId, timeCreated, label.tutorial, calculatedStreetEdgeId, 0, 0, 0, None, label.severity, label.temporary, label.description, label.tagIds.distinct.flatMap(t => TagTable.selectAllTags.filter(_.tagId == t).map(_.tag).headOption).toList)) diff --git a/app/controllers/UserProfileController.scala b/app/controllers/UserProfileController.scala index c37b5961f8..5fc788b510 100644 --- a/app/controllers/UserProfileController.scala +++ b/app/controllers/UserProfileController.scala @@ -19,6 +19,8 @@ import play.api.libs.json.{JsObject, JsValue, Json} import play.extras.geojson import play.api.i18n.Messages import scala.concurrent.Future +import play.api.mvc._ +import models.user.OrganizationTable /** * Holds the HTTP requests associated with the user dashboard. @@ -111,7 +113,9 @@ class UserProfileController @Inject() (implicit val env: Environment[User, Sessi "audit_task_id" -> label.auditTaskId, "label_id" -> label.labelId, "gsv_panorama_id" -> label.gsvPanoramaId, - "label_type" -> label.labelType + "label_type" -> label.labelType, + "correct" -> label.correct, + "has_validations" -> label.hasValidations ) Json.obj("type" -> "Feature", "geometry" -> point, "properties" -> properties) } @@ -183,11 +187,11 @@ class UserProfileController @Inject() (implicit val env: Environment[User, Sessi case Some(user) => val userId: UUID = user.userId if (user.role.getOrElse("") != "Anonymous") { - val allUserOrgs: List[Int] = UserOrgTable.getAllOrgs(userId) - if (allUserOrgs.headOption.isEmpty) { + val userOrg: Option[Int] = UserOrgTable.getOrg(userId) + if (userOrg.isEmpty) { UserOrgTable.save(userId, orgId) - } else if (allUserOrgs.head != orgId) { - UserOrgTable.remove(userId, allUserOrgs.head) + } else if (userOrg.get != orgId) { + UserOrgTable.remove(userId, userOrg.get) UserOrgTable.save(userId, orgId) } } @@ -197,6 +201,23 @@ class UserProfileController @Inject() (implicit val env: Environment[User, Sessi } } + /** + * Creates a team and puts them in the organization table. + */ + def createTeam() = Action(parse.json) { request => + val orgName: String = (request.body \ "name").as[String] + val orgDescription: String = (request.body \ "description").as[String] + + // Inserting into the database and capturing the generated orgId. + val orgId: Int = OrganizationTable.insert(orgName, orgDescription) + + Ok(Json.obj( + "message" -> "Organization created successfully!", + "org_id" -> orgId + )) +} + + /** * Gets some basic stats about the logged in user that we show across the site: distance, label count, and accuracy. */ diff --git a/app/controllers/ValidationTaskController.scala b/app/controllers/ValidationTaskController.scala index 005213b3ee..56f465bd78 100644 --- a/app/controllers/ValidationTaskController.scala +++ b/app/controllers/ValidationTaskController.scala @@ -212,7 +212,7 @@ class ValidationTaskController @Inject() (implicit val env: Environment[User, Se MissionTable.resumeOrCreateNewValidationMission(userId, 0.0D, 0.0D, "labelmapValidation", labelTypeId).get // Check if user already has a validation for this label. - if(LabelValidationTable.countValidationsFromUserAndLabel(userId, submission.labelId) != 0) { + if (LabelValidationTable.countValidationsFromUserAndLabel(userId, submission.labelId) != 0) { // Delete the user's old label. LabelValidationTable.deleteLabelValidation(submission.labelId, userId.toString) } diff --git a/app/formats/json/OrganizationFormats.scala b/app/formats/json/OrganizationFormats.scala new file mode 100644 index 0000000000..6e10120f25 --- /dev/null +++ b/app/formats/json/OrganizationFormats.scala @@ -0,0 +1,14 @@ +package formats.json + +import models.user._ +import play.api.libs.json._ +import play.api.libs.functional.syntax._ + +// https://github.com/datalek/silhouette-rest-seed/blob/master/app/formatters/json/UserFormats.scala +object OrganizationFormats { + implicit val organizationWrites: Writes[Organization] = ( + (JsPath \ "orgId").write[Int] and + (JsPath \ "orgName").write[String] and + (JsPath \ "orgDescription").write[String] + )(unlift(Organization.unapply _)) +} diff --git a/app/formats/json/UserFormats.scala b/app/formats/json/UserFormats.scala index da3ca4ef31..df973e7372 100644 --- a/app/formats/json/UserFormats.scala +++ b/app/formats/json/UserFormats.scala @@ -1,7 +1,9 @@ package formats.json +import java.sql.Timestamp import java.util.UUID +import models.daos.slick.UserStatsForAdminPage import models.user._ import play.api.libs.json._ import play.api.libs.functional.syntax._ @@ -31,4 +33,21 @@ object UserFormats { (__ \ "validated_incorrect").write[Int] and (__ \ "not_validated").write[Int] )(unlift(LabelTypeStat.unapply _)) + + implicit val userStatsWrites: Writes[UserStatsForAdminPage] = ( + (__ \ "userId").write[String] and + (__ \ "username").write[String] and + (__ \ "email").write[String] and + (__ \ "role").write[String] and + (__ \ "org").writeNullable[String] and + (__ \ "signUpTime").writeNullable[Timestamp] and + (__ \ "lastSignInTime").writeNullable[Timestamp] and + (__ \ "signInCount").write[Int] and + (__ \ "labels").write[Int] and + (__ \ "ownValidated").write[Int] and + (__ \ "ownValidatedAgreedPct").write[Double] and + (__ \ "othersValidated").write[Int] and + (__ \ "othersValidatedAgreedPct").write[Double] and + (__ \ "highQuality").write[Boolean] + )(unlift(UserStatsForAdminPage.unapply _)) } diff --git a/app/models/daos/slick/UserDAOSlick.scala b/app/models/daos/slick/UserDAOSlick.scala index a7e7a666ac..0036101e08 100644 --- a/app/models/daos/slick/UserDAOSlick.scala +++ b/app/models/daos/slick/UserDAOSlick.scala @@ -13,7 +13,6 @@ import models.user.OrganizationTable.organizations import models.user.UserOrgTable.userOrgs import models.user.{RoleTable, UserStatTable, WebpageActivityTable} import models.user.{User, UserRoleTable} -import models.label.{LabelValidation, LabelValidationTable} import play.api.db.slick._ import play.api.db.slick.Config.driver.simple._ import play.api.Play.current @@ -503,12 +502,12 @@ object UserDAOSlick { // Map(user_id: String -> (role: String, total: Int, agreed: Int, disagreed: Int, unsure: Int)). val validatedCounts = LabelValidationTable.getValidationCountsPerUser.map { valCount => - (valCount._1, (valCount._2, valCount._3, valCount._4, valCount._5, valCount._6)) + (valCount._1, (valCount._2, valCount._3, valCount._4)) }.toMap // Map(user_id: String -> (count: Int, agreed: Int, disagreed: Int)). val othersValidatedCounts = LabelValidationTable.getValidatedCountsPerUser.map { valCount => - (valCount._1, (valCount._2, valCount._3, valCount._4)) + (valCount._1, (valCount._2, valCount._3)) }.toMap val userHighQuality = @@ -516,23 +515,21 @@ object UserDAOSlick { // Now left join them all together and put into UserStatsForAdminPage objects. usersMinusAnonUsersWithNoLabelsAndNoValidations.list.map { u => - val ownValidatedCounts = validatedCounts.getOrElse(u.userId, ("", 0, 0, 0, 0)) + val ownValidatedCounts = validatedCounts.getOrElse(u.userId, ("", 0, 0)) val ownValidatedTotal = ownValidatedCounts._2 val ownValidatedAgreed = ownValidatedCounts._3 - val ownValidatedDisagreed = ownValidatedCounts._4 - val otherValidatedCounts = othersValidatedCounts.getOrElse(u.userId, (0, 0, 0)) + val otherValidatedCounts = othersValidatedCounts.getOrElse(u.userId, (0, 0)) val otherValidatedTotal = otherValidatedCounts._1 val otherValidatedAgreed = otherValidatedCounts._2 - val otherValidatedDisagreed = otherValidatedCounts._3 val ownValidatedAgreedPct = if (ownValidatedTotal == 0) 0f - else ownValidatedAgreed * 1.0 / (ownValidatedAgreed + ownValidatedDisagreed) + else ownValidatedAgreed * 1.0 / ownValidatedTotal val otherValidatedAgreedPct = if (otherValidatedTotal == 0) 0f - else otherValidatedAgreed * 1.0 / (otherValidatedAgreed + otherValidatedDisagreed) + else otherValidatedAgreed * 1.0 / otherValidatedTotal UserStatsForAdminPage( u.userId, u.username, u.email, diff --git a/app/models/label/LabelTable.scala b/app/models/label/LabelTable.scala index e4466c7fb4..286fb3fcf5 100644 --- a/app/models/label/LabelTable.scala +++ b/app/models/label/LabelTable.scala @@ -40,7 +40,7 @@ case class POV(heading: Double, pitch: Double, zoom: Int) case class Dimensions(width: Int, height: Int) case class LocationXY(x: Int, y: Int) -case class LabelLocation(labelId: Int, auditTaskId: Int, gsvPanoramaId: String, labelType: String, lat: Float, lng: Float) +case class LabelLocation(labelId: Int, auditTaskId: Int, gsvPanoramaId: String, labelType: String, lat: Float, lng: Float, correct: Option[Boolean], hasValidations: Boolean) case class LabelLocationWithSeverity(labelId: Int, auditTaskId: Int, labelType: String, lat: Float, lng: Float, correct: Option[Boolean], hasValidations: Boolean, expired: Boolean, @@ -250,7 +250,7 @@ object LabelTable { )) implicit val labelLocationConverter = GetResult[LabelLocation](r => - LabelLocation(r.nextInt, r.nextInt, r.nextString, r.nextString, r.nextFloat, r.nextFloat)) + LabelLocation(r.nextInt, r.nextInt, r.nextString, r.nextString, r.nextFloat, r.nextFloat, r.nextBooleanOption, r.nextBoolean)) implicit val labelSeverityConverter = GetResult[LabelLocationWithSeverity](r => LabelLocationWithSeverity(r.nextInt, r.nextInt, r.nextString, r.nextFloat, r.nextFloat, r.nextBooleanOption, @@ -601,6 +601,9 @@ object LabelTable { val userIdString: String = userId.toString val labelsValidatedByUser = labelValidations.filter(_.userId === userIdString) + // Make sure there is a user_stat entry for the given user. + UserStatTable.addUserStatIfNew(userId) + // Get labels the given user has not placed that have non-expired GSV imagery. val labelsToValidate = for { _lb <- labels @@ -1038,8 +1041,12 @@ object LabelTable { if _l.userId === userId.toString if regionId.isEmpty.asColumnOf[Boolean] || _ser.regionId === regionId.getOrElse(-1) if _lp.lat.isDefined && _lp.lng.isDefined - } yield (_l.labelId, _l.auditTaskId, _l.gsvPanoramaId, _lt.labelType, _lp.lat.get, _lp.lng.get) - _labels.list.map(LabelLocation.tupled) + } yield (_l.labelId, _l.auditTaskId, _l.gsvPanoramaId, _lt.labelType, _lp.lat, _lp.lng, _l.correct, _l.agreeCount > 0 || _l.disagreeCount > 0 || _l.unsureCount > 0) + + // For some reason we couldn't use both `_l.agreeCount > 0` and `_lPoint.lat.get` in the yield without a runtime + // error, which is why we couldn't use `.tupled` here. This was the error message: + // SlickException: Expected an option type, found Float/REAL + _labels.list.map(l => LabelLocation(l._1, l._2, l._3, l._4, l._5.get, l._6.get, l._7, l._8)) } /** diff --git a/app/models/label/LabelValidationTable.scala b/app/models/label/LabelValidationTable.scala index 69950e7b8c..1ec6b6c91c 100644 --- a/app/models/label/LabelValidationTable.scala +++ b/app/models/label/LabelValidationTable.scala @@ -148,6 +148,7 @@ object LabelValidationTable { * @return The label_validation_id of the inserted/updated validation. */ def insert(labelVal: LabelValidation): Int = db.withTransaction { implicit session => + UserStatTable.addUserStatIfNew(UUID.fromString(labelVal.userId)) val isExcludedUser: Boolean = UserStatTable.userStats.filter(_.userId === labelVal.userId).map(_.excluded).first val userThatAppliedLabel: String = labelsUnfiltered.filter(_.labelId === labelVal.labelId).map(_.userId).list.head @@ -250,25 +251,23 @@ object LabelValidationTable { /** * Select validation counts per user. * - * @return list of tuples (labeler_id, labeler_role, labels_validated, agreed_count, disagreed_count, unsure_count) + * @return list of tuples (labeler_id, labeler_role, labels_validated, agreed_count) */ - def getValidationCountsPerUser: List[(String, String, Int, Int, Int, Int)] = db.withSession { implicit session => + def getValidationCountsPerUser: List[(String, String, Int, Int)] = db.withSession { implicit session => val _labels = for { _label <- LabelTable.labelsWithExcludedUsers - _user <- users if _user.username =!= "anonymous" && _user.userId === _label.userId // User who placed the label + _user <- users if _user.username =!= "anonymous" && _user.userId === _label.userId // User who placed the label. _userRole <- userRoles if _user.userId === _userRole.userId _role <- roleTable if _userRole.roleId === _role.roleId - if _label.agreeCount > 0 || _label.disagreeCount > 0 || _label.unsureCount > 0 // Filter for labels w/ validation + if _label.correct.isDefined // Filter for labels marked as either correct or incorrect. } yield (_user.userId, _role.role, _label.correct) - // Count the number of correct/incorrect/unsure labels for each user. + // Count the number of correct labels and total number marked as either correct or incorrect for each user. _labels.groupBy(l => (l._1, l._2)).map { case ((userId, role), group) => ( userId, role, - group.length, - group.map(l => Case.If(l._3.getOrElse(false) === true).Then(1).Else(0)).sum.getOrElse(0), // # correct labels - group.map(l => Case.If(l._3.getOrElse(true) === false).Then(1).Else(0)).sum.getOrElse(0), // # incorrect labels - group.map(l => Case.If(l._3.isEmpty).Then(1).Else(0)).sum.getOrElse(0) // # unsure labels + group.length, // # Correct or incorrect. + group.map(l => Case.If(l._3.getOrElse(false) === true).Then(1).Else(0)).sum.getOrElse(0) // # Correct labels. )}.list } @@ -277,27 +276,21 @@ object LabelValidationTable { * * @return list of tuples of (labeler_id, validation_count, validation_agreed_count, validation_disagreed_count) */ - def getValidatedCountsPerUser: List[(String, Int, Int, Int)] = db.withSession { implicit session => + def getValidatedCountsPerUser: List[(String, Int, Int)] = db.withSession { implicit session => val validations = for { _validation <- validationLabels _validationUser <- users if _validationUser.userId === _validation.userId _userRole <- userRoles if _validationUser.userId === _userRole.userId if _validationUser.username =!= "anonymous" + if _validation.labelValidationId =!= 3 // Exclude "unsure" validations. } yield (_validationUser.userId, _validation.validationResult) // Counts the number of labels for each user by grouping by user_id and role. validations.groupBy(l => l._1).map { case (uId, group) => { - // Sum up the agreed/disagreed results - val agreed = group.map { r => - Case.If(r._2 === 1).Then(1).Else(0) // Only count it if the result was "agree" - }.sum.getOrElse(0) - val disagreed = group.map { r => - Case.If(r._2 === 2).Then(1).Else(0) // Only count it if the result was "disagree" - }.sum.getOrElse(0) - - // group.length is the total # of validations - (uId, group.length, agreed, disagreed) + // Sum up the agreed validations and total validations (just agreed + disagreed). + val agreed = group.map { r => Case.If(r._2 === 1).Then(1).Else(0) }.sum.getOrElse(0) + (uId, group.length, agreed) } }.list } diff --git a/app/models/street/StreetEdgePriorityTable.scala b/app/models/street/StreetEdgePriorityTable.scala index aa94de496f..fa3fc15128 100644 --- a/app/models/street/StreetEdgePriorityTable.scala +++ b/app/models/street/StreetEdgePriorityTable.scala @@ -6,6 +6,8 @@ import models.utils.MyPostgresDriver.simple._ import play.api.Play.current import play.api.cache.Cache import play.api.libs.json._ + +import java.util.UUID import scala.concurrent.duration.DurationInt import scala.slick.lifted.ForeignKeyQuery import scala.slick.jdbc.GetResult @@ -253,11 +255,10 @@ object StreetEdgePriorityTable { * @param streetEdgeId * @return success boolean */ - def partiallyUpdatePriority(streetEdgeId: Int, userId: Option[String]): Boolean = db.withTransaction { implicit session => - // Check if the user that audited is high quality. If we don't have their user_id, assume high quality for now. - val userHighQuality: Boolean = userId.flatMap { - u => UserStatTable.userStats.filter(_.userId === u).map(_.highQuality).list.headOption - }.getOrElse(true) + def partiallyUpdatePriority(streetEdgeId: Int, userId: UUID): Boolean = db.withTransaction { implicit session => + // Check if the user that audited is high quality. Make sure they have an entry in user_stat table first. + UserStatTable.addUserStatIfNew(userId) + val userHighQuality: Boolean = UserStatTable.userStats.filter(_.userId === userId.toString).map(_.highQuality).first val priorityQuery = for { edge <- streetEdgePriorities if edge.streetEdgeId === streetEdgeId } yield edge.priority val rowsWereUpdated: Option[Boolean] = priorityQuery.run.headOption.map { currPriority => diff --git a/app/models/user/OrganizationTable.scala b/app/models/user/OrganizationTable.scala index 8bf9123cb8..e9af3841af 100644 --- a/app/models/user/OrganizationTable.scala +++ b/app/models/user/OrganizationTable.scala @@ -58,4 +58,16 @@ object OrganizationTable { def getOrganizationDescription(orgId: Int): Option[String] = db.withTransaction { implicit session => organizations.filter(_.orgId === orgId).map(_.orgDescription).firstOption } + + /** + * Inserts a new organization into the database. + * + * @param orgName The name of the organization to be created. + * @param orgDescription A brief description of the organization. + * @return The auto-generated ID of the newly created organization. + */ + def insert(orgName: String, orgDescription: String): Int = db.withSession { implicit session => + val newOrganization = Organization(0, orgName, orgDescription) // orgId is auto-generated. + (organizations returning organizations.map(_.orgId)) += newOrganization + } } diff --git a/app/models/user/UserOrgTable.scala b/app/models/user/UserOrgTable.scala index fafc5b584e..147dcdae0f 100644 --- a/app/models/user/UserOrgTable.scala +++ b/app/models/user/UserOrgTable.scala @@ -22,13 +22,13 @@ object UserOrgTable { val userOrgs = TableQuery[UserOrgTable] /** - * Gets all organizations the given user is affiliated with. + * Gets the organization the given user is affiliated with. * * @param userId The id of the user. - * @return A list of all organizations the given user is affiliated with. + * @return The organization the given user is affiliated with. */ - def getAllOrgs(userId: UUID): List[Int] = db.withSession { implicit session => - userOrgs.filter(_.userId === userId.toString).map(_.orgId).list + def getOrg(userId: UUID): Option[Int] = db.withSession { implicit session => + userOrgs.filter(_.userId === userId.toString).map(_.orgId).firstOption } /** diff --git a/app/views/admin/index.scala.html b/app/views/admin/index.scala.html index bb875795ec..d12ad26609 100644 --- a/app/views/admin/index.scala.html +++ b/app/views/admin/index.scala.html @@ -5,25 +5,16 @@ @import models.audit.AuditTaskCommentTable @import models.utils.DataFormatter -@import models.user.OrganizationTable @(title: String, user: Option[User] = None)(implicit lang: Lang) @main(title) { @navbar(user, Some("/admin")) - -
-
+
+
+

@Messages("loading")

+ +
+
+ diff --git a/app/views/forgotPassword.scala.html b/app/views/forgotPassword.scala.html index 4e5cdd83bd..0a58dd45cc 100644 --- a/app/views/forgotPassword.scala.html +++ b/app/views/forgotPassword.scala.html @@ -22,7 +22,6 @@
@Messages("reset.pw.forgot.title") @helper.form(action = routes.ForgotPasswordController.submit()) { -

@Messages("reset.pw.forgot.suggest")

@Messages("reset.pw.forgot.submit.email")

@text(forgotPasswordForm("emailForgotPassword"), Messages("authenticate.email"), icon = "at")
diff --git a/app/views/labelMap.scala.html b/app/views/labelMap.scala.html index 79b3cc0164..e17942977c 100644 --- a/app/views/labelMap.scala.html +++ b/app/views/labelMap.scala.html @@ -226,7 +226,7 @@ } toggleLabelLayer(checkbox.id.split('-')[0], checkbox, slider, map, mapData); } else if (checkbox.getAttribute('data-filter-type') === 'label-validations') { - filterLabelLayers(checkbox, map, mapData); + filterLabelLayers(checkbox, map, mapData, true); } else if (checkbox.getAttribute('data-filter-type') === 'misc') { toggleMiscLayers(checkbox, map, mapData); } else { diff --git a/app/views/leaderboard.scala.html b/app/views/leaderboard.scala.html index ec97e1ffd3..7c9627600f 100644 --- a/app/views/leaderboard.scala.html +++ b/app/views/leaderboard.scala.html @@ -8,8 +8,10 @@ @leaderboardStats = @{UserStatTable.getLeaderboardStats(10)} @leaderboardStatsThisWeek = @{UserStatTable.getLeaderboardStats(10, "weekly")} @leaderboardStatsByOrg = @{UserStatTable.getLeaderboardStats(10, "overall", true)} -@leaderboardStatsOrg = @{UserStatTable.getLeaderboardStats(10, "overall", false, UserOrgTable.getAllOrgs(user.get.userId).headOption)} +@leaderboardStatsOrg = @{UserStatTable.getLeaderboardStats(10, "overall", false, UserOrgTable.getOrg(user.get.userId))} @currentCountryId =@{Configs.getCurrentCountryId()} +@userOrg = @{UserOrgTable.getOrg(user.get.userId)} +@orgName = @{userOrg.flatMap(orgId => OrganizationTable.getOrganizationName(orgId)).getOrElse("Team Name Not Found")}
@@ -239,10 +241,10 @@

@Messages("leaderboard.weekly.title

} - @if(user && user.get.role.getOrElse("") != "Anonymous" && !UserOrgTable.getAllOrgs(user.get.userId).isEmpty) { + @if(user && user.get.role.getOrElse("") != "Anonymous" && !userOrg.isEmpty) {
-

@Messages("leaderboard.org.title", OrganizationTable.getOrganizationName(UserOrgTable.getAllOrgs(user.get.userId).head).getOrElse(""))

-
@Messages("leaderboard.org.detail", OrganizationTable.getOrganizationName(UserOrgTable.getAllOrgs(user.get.userId).head).getOrElse(""))
+

@Messages("leaderboard.org.title", orgName)

+
@Messages("leaderboard.org.detail", orgName)
@@ -325,7 +327,7 @@

@if(user && user.get.role.getOrElse("") == "Anonymous") { @Html(Messages("leaderboard.encouragement.no.user")) } else { - @if(UserOrgTable.getAllOrgs(user.get.userId).isEmpty) { + @if(userOrg.isEmpty) { @if(currentCountryId == "taiwan") { @Html(Messages("leaderboard.encouragement.no.org", routes.Assets.at("documents/labeling-guide-Taiwan.pdf").url)) } else { diff --git a/app/views/main.scala.html b/app/views/main.scala.html index 494c128bca..6c0c10befe 100644 --- a/app/views/main.scala.html +++ b/app/views/main.scala.html @@ -165,20 +165,9 @@ }