Skip to content

Commit

Permalink
Merge pull request #3738 from ProjectSidewalk/develop
Browse files Browse the repository at this point in the history
v8.0.1
  • Loading branch information
misaugstad authored Nov 14, 2024
2 parents d6e8392 + 58a9fa1 commit d8010c4
Show file tree
Hide file tree
Showing 67 changed files with 799 additions and 314 deletions.
3 changes: 3 additions & 0 deletions Gruntfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
31 changes: 25 additions & 6 deletions app/controllers/AdminController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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"))
Expand Down Expand Up @@ -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"))
}
}
}
2 changes: 2 additions & 0 deletions app/controllers/CredentialsAuthController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
13 changes: 6 additions & 7 deletions app/controllers/SignUpController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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)
Expand All @@ -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"
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
13 changes: 5 additions & 8 deletions app/controllers/TaskController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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))
Expand Down
31 changes: 26 additions & 5 deletions app/controllers/UserProfileController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
}
}
Expand All @@ -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.
*/
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/ValidationTaskController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
14 changes: 14 additions & 0 deletions app/formats/json/OrganizationFormats.scala
Original file line number Diff line number Diff line change
@@ -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 _))
}
19 changes: 19 additions & 0 deletions app/formats/json/UserFormats.scala
Original file line number Diff line number Diff line change
@@ -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._
Expand Down Expand Up @@ -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 _))
}
15 changes: 6 additions & 9 deletions app/models/daos/slick/UserDAOSlick.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -503,36 +502,34 @@ 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 =
UserStatTable.userStats.map { x => (x.userId, x.highQuality) }.list.toMap

// 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,
Expand Down
Loading

0 comments on commit d8010c4

Please sign in to comment.