Skip to content

Commit

Permalink
Merge pull request #2289 from ProjectSidewalk/1680-give-users-more-fe…
Browse files Browse the repository at this point in the history
…edback-on-dashboard

2280 add quick summary of num missions, distance, labels, validations, and accuracy to dashboard
  • Loading branch information
misaugstad authored Oct 15, 2020
2 parents a1afe12 + 37e50f7 commit f844de2
Show file tree
Hide file tree
Showing 25 changed files with 156 additions and 405 deletions.
43 changes: 6 additions & 37 deletions app/controllers/UserProfileController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ import com.mohiva.play.silhouette.impl.authenticators.SessionAuthenticator
import com.vividsolutions.jts.geom.Coordinate
import controllers.headers.ProvidesHeader
import formats.json.TaskFormats._
import formats.json.MissionFormat._
import models.audit.{AuditTaskInteractionTable, AuditTaskTable, InteractionWithLabel}
import models.mission.MissionTable
import models.label.{LabelTable, LabelValidationTable}
import models.user.User
import play.api.libs.json.{JsArray, JsObject, Json}
import play.extras.geojson
import play.api.i18n.Messages


import scala.concurrent.Future
Expand All @@ -31,7 +31,11 @@ class UserProfileController @Inject() (implicit val env: Environment[User, Sessi
request.identity match {
case Some(user) =>
val username: String = user.username
Future.successful(Ok(views.html.userProfile(s"Project Sidewalk - $username", Some(user))))
// Get distance audited by the user. If using metric units, convert from miles to kilometers.
val auditedDistance: Float =
if (Messages("measurement.system") == "metric") MissionTable.getDistanceAudited(user.userId) * 1.60934.toFloat
else MissionTable.getDistanceAudited(user.userId)
Future.successful(Ok(views.html.userProfile(s"Project Sidewalk - $username", Some(user), auditedDistance)))
case None => Future.successful(Redirect(s"/anonSignUp?url=/contribution/$username"))
}
}
Expand Down Expand Up @@ -105,22 +109,6 @@ class UserProfileController @Inject() (implicit val env: Environment[User, Sessi
}
}

/**
*
* @return
*/
def getMissions = UserAwareAction.async { implicit request =>
request.identity match {
case Some(user) =>
val tasksWithLabels = MissionTable.selectMissions(user.userId).map(x => Json.toJson(x))
Future.successful(Ok(JsArray(tasksWithLabels)))
case None => Future.successful(Ok(Json.obj(
"error" -> "0",
"message" -> "Your user id could not be found."
)))
}
}

/**
* Get a list of labels submitted by the user
* @return
Expand Down Expand Up @@ -215,25 +203,6 @@ class UserProfileController @Inject() (implicit val env: Environment[User, Sessi
}
}

/**
*
* @return
*/
def getAuditCounts = UserAwareAction.async { implicit request =>
request.identity match {
case Some(user) =>
val auditCounts = AuditTaskTable.selectAuditCountsPerDayByUserId(user.userId)
val json = Json.arr(auditCounts.map(x => Json.obj(
"date" -> x.date, "count" -> x.count
)))
Future.successful(Ok(json))
case None => Future.successful(Ok(Json.obj(
"error" -> "0",
"message" -> "We could not find your username."
)))
}
}

def getAllAuditCounts = UserAwareAction.async { implicit request =>
val auditCounts = AuditTaskTable.auditCounts
val json = Json.arr(auditCounts.map(x => Json.obj(
Expand Down
24 changes: 0 additions & 24 deletions app/formats/json/MissionFormat.scala

This file was deleted.

22 changes: 0 additions & 22 deletions app/models/audit/AuditTaskTable.scala
Original file line number Diff line number Diff line change
Expand Up @@ -374,28 +374,6 @@ object AuditTaskTable {
_streetEdges.list.groupBy(_.streetEdgeId).map(_._2.head).toList
}


/**
* Return audit counts for the last 31 days.
*
* @param userId User id
*/
def selectAuditCountsPerDayByUserId(userId: UUID): List[AuditCountPerDay] = db.withSession { implicit session =>
val selectAuditCountQuery = Q.query[String, (String, Int)](
"""SELECT calendar_date::date, COUNT(audit_task_id)
|FROM
|(
| SELECT current_date - (n || ' day')::INTERVAL AS calendar_date
| FROM generate_series(0, 30) n
|) AS calendar
|LEFT JOIN sidewalk.audit_task ON audit_task.task_start::date = calendar_date::date
| AND audit_task.user_id = ?
|GROUP BY calendar_date
|ORDER BY calendar_date""".stripMargin
)
selectAuditCountQuery(userId.toString).list.map(x => AuditCountPerDay.tupled(x))
}

/**
*
* @param userId
Expand Down
54 changes: 54 additions & 0 deletions app/models/label/LabelValidationTable.scala
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package models.label

import java.util.UUID

import models.utils.MyPostgresDriver.simple._
import models.audit.AuditTaskTable
import models.daos.slick.DBTableDefinitions.{DBUser, UserTable}
Expand Down Expand Up @@ -135,6 +137,48 @@ object LabelValidationTable {
}
}

/**
* Calculates and returns the user accuracy for the supplied userId. The accuracy calculation is performed if and only
* if the users' labels have been validated 10 or more times. The simplest way to think about the accuracy calculation
* is something like:
*
* number of labels validated correct / (number of labels validated - number of labels marked as unsure)
*
* Which does not penalize users for labels that they supplied but were rated as unsure by other users.
*
* However, this calculation does not take into account that multiple users can validate a single label. So, a
* slightly more complicated version of this uses majority vote where a label is counted as correct if and only if the
* number of agreement ratings > number of disagreement ratings. If the num of agreement ratings - num of disagreement
* ratings = 0, then it counts as unsure
*
* This is the version implemented below.
*
* @param userId
* @return
*/
def getUserAccuracy(userId: UUID): Option[Float] = db.withSession { implicit session =>
val accuracyQuery = Q.query[String, Option[Float]](
"""SELECT CASE WHEN validated_count > 9 THEN accuracy ELSE NULL END AS accuracy
FROM (
SELECT user_id,
CAST (COUNT(CASE WHEN n_agree > n_disagree THEN 1 END) AS FLOAT) / NULLIF(COUNT(CASE WHEN n_agree > n_disagree THEN 1 END) + COUNT(CASE WHEN n_disagree > n_agree THEN 1 END), 0) AS accuracy,
COUNT(CASE WHEN n_agree > n_disagree THEN 1 END) + COUNT(CASE WHEN n_disagree > n_agree THEN 1 END) AS validated_count
FROM (
SELECT mission.user_id, label.label_id,
COUNT(CASE WHEN validation_result = 1 THEN 1 END) AS n_agree,
COUNT(CASE WHEN validation_result = 2 THEN 1 END) AS n_disagree
FROM mission
INNER JOIN label ON mission.mission_id = label.mission_id
INNER JOIN label_validation ON label.label_id = label_validation.label_id
WHERE mission.user_id = ?
GROUP BY mission.user_id, label.label_id
) agree_count
GROUP BY user_id
) "accuracy";""".stripMargin
)
accuracyQuery(userId.toString).list.headOption.flatten
}

/**
* Select validation counts per user.
*
Expand Down Expand Up @@ -238,6 +282,16 @@ object LabelValidationTable {
.size.run
}

/**
* Counts the number of validations performed by this user (given the supplied userId).
*
* @param userId
* @returns the number of validations performed by this user
*/
def countValidationsByUserId(userId: UUID): Int = db.withSession { implicit session =>
validationLabels.filter(_.userId === userId.toString).size.run
}

/**
* @return total number of validations
*/
Expand Down
44 changes: 1 addition & 43 deletions app/models/mission/MissionTable.scala
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,6 @@ import scala.slick.jdbc.GetResult
case class RegionalMission(missionId: Int, missionType: String, regionId: Option[Int], regionName: Option[String],
distanceMeters: Option[Float], labelsValidated: Option[Int])

case class AuditMission(userId: String, username: String, missionId: Int, completed: Boolean, missionStart: Timestamp,
missionEnd: Timestamp, neighborhood: Option[String], labelId: Option[Int], labelType: Option[String])

case class MissionSetProgress(missionType: String, numComplete: Int)

case class Mission(missionId: Int, missionTypeId: Int, userId: String, missionStart: Timestamp, missionEnd: Timestamp,
Expand Down Expand Up @@ -108,10 +105,6 @@ object MissionTable {
val userRoles = TableQuery[UserRoleTable]
val roles = TableQuery[RoleTable]

val labels = TableQuery[LabelTable]
val labelTypes = TableQuery[LabelTypeTable]
val regionProperties = TableQuery[RegionPropertyTable]

// Distances for first few missions: 500 ft, 500 ft, 750 ft, then 1,000 ft for all remaining.
val distancesForFirstAuditMissions: List[Float] = List(152.4F, 152.4F, 228.6F)
val distanceForLaterMissions: Float = 304.8F // 1,000 ft
Expand Down Expand Up @@ -436,41 +429,6 @@ object MissionTable {
regionalMissions.sortBy(rm => (rm.regionId, rm.missionId))
}

/**
* Return a list of missions for a specific user
*
* @param userId User id
* @return
*/
def selectMissions(userId: UUID): List[AuditMission] = db.withSession { implicit session =>
// gets all the missions that correspond to the user
val userMissions = for {
_users <- users if _users.userId === userId.toString
_missions <- missions if _missions.skipped === false && _missions.userId === _users.userId
_missionTypes <- missionTypes if _missions.missionTypeId === _missionTypes.missionTypeId &&
(_missionTypes.missionType === "audit" ||
_missionTypes.missionType === "auditOnboarding")
} yield (_users.userId, _users.username, _missions.missionId, _missions.completed, _missions.missionStart, _missions.missionEnd, _missions.regionId)

// gets all the labels for all the missions but maintains missions that have no labels
val userMissionLabels = for {
(_userMissions, _labels) <- userMissions.leftJoin(labels).on(_._3 === _.missionId)
} yield (_userMissions._1, _userMissions._2, _userMissions._3, _userMissions._4, _userMissions._5, _userMissions._6, _userMissions._7, _labels.labelId.?, _labels.labelTypeId.?)

// changes the id of each label to a string representing its label type
val missionsWithLabels = for {
(_userMissionLabels, _labelTypes) <- userMissionLabels.leftJoin(labelTypes).on(_._9 === _.labelTypeId)
} yield (_userMissionLabels._1, _userMissionLabels._2, _userMissionLabels._3, _userMissionLabels._4, _userMissionLabels._5, _userMissionLabels._6, _userMissionLabels._7, _userMissionLabels._8, _labelTypes.labelType.?)

// changes the region id to the name of the neighborhood
val missionsWithNeighborhoods = for {
(_missionsWithLabels, _regionProperties) <- missionsWithLabels.leftJoin(regionProperties).on(_._7 === _.regionId)
} yield (_missionsWithLabels._1, _missionsWithLabels._2, _missionsWithLabels._3, _missionsWithLabels._4, _missionsWithLabels._5, _missionsWithLabels._6, _regionProperties.value.?, _missionsWithLabels._8, _missionsWithLabels._9)

// formats the finalized JSON object using the format in the MissionFormat class
missionsWithNeighborhoods.list.map(x => AuditMission.tupled(x))
}

/**
* Returns all the missions.
*
Expand Down Expand Up @@ -512,7 +470,7 @@ object MissionTable {
/**
* Gets total distance audited by a user in miles.
*
* @param userId
* @param userId the UUID of the user
* @return
*/
def getDistanceAudited(userId: UUID): Float = db.withSession { implicit session =>
Expand Down
22 changes: 10 additions & 12 deletions app/views/admin/index.scala.html
Original file line number Diff line number Diff line change
Expand Up @@ -148,9 +148,9 @@ <h1>Activities</h1>
</tr>
<tr>
<th>Audited Distance</th>
<th>@("%.1f".format(convertDistance(StreetEdgeTable.auditedStreetDistance(1)))) @Messages("admin.overview.distance")</td>
<th>@("%.1f".format(convertDistance(StreetEdgeTable.auditedStreetDistanceToday()))) @Messages("admin.overview.distance")</td>
<th>@("%.1f".format(convertDistance(StreetEdgeTable.auditedStreetDistanceYesterday()))) @Messages("admin.overview.distance")</td>
<th>@("%.1f".format(convertDistance(StreetEdgeTable.auditedStreetDistance(1)))) @Messages("dist.metric.abbr")</td>
<th>@("%.1f".format(convertDistance(StreetEdgeTable.auditedStreetDistanceToday()))) @Messages("dist.metric.abbr")</td>
<th>@("%.1f".format(convertDistance(StreetEdgeTable.auditedStreetDistanceYesterday()))) @Messages("dist.metric.abbr")</td>
</tr>
<tr>
<th>Total Validation Users</th>
Expand Down Expand Up @@ -257,31 +257,31 @@ <h1>Coverage</h1>
</tr>
<tr>
<th>Distance</th>
<td>@("%.1f".format(convertDistance(StreetEdgeTable.auditedStreetDistance(1)))) @Messages("admin.overview.distance")</td>
<td>@("%.1f".format(convertDistance(StreetEdgeTable.totalStreetDistance()))) @Messages("admin.overview.distance")</td>
<td>@("%.1f".format(convertDistance(StreetEdgeTable.auditedStreetDistance(1)))) @Messages("dist.metric.abbr")</td>
<td>@("%.1f".format(convertDistance(StreetEdgeTable.totalStreetDistance()))) @Messages("dist.metric.abbr")</td>
<td>@("%.0f".format(StreetEdgeTable.streetDistanceCompletionRate(1) * 100))%</td>
</tr>
<tr style="font-size: 90%;">
<td style="padding-left: 30px">Registered</td>
<td>@("%.1f".format(convertDistance(StreetEdgeTable.auditedStreetDistance(1, "Registered")))) @Messages("admin.overview.distance")</td>
<td>@("%.1f".format(convertDistance(StreetEdgeTable.auditedStreetDistance(1, "Registered")))) @Messages("dist.metric.abbr")</td>
<td></td>
<td>@("%.0f".format(StreetEdgeTable.streetDistanceCompletionRate(1, "Registered") * 100))%</td>
</tr>
<tr style="font-size: 90%;">
<td style="padding-left: 30px">Anonymous</td>
<td>@("%.1f".format(convertDistance(StreetEdgeTable.auditedStreetDistance(1, "Anonymous")))) @Messages("admin.overview.distance")</td>
<td>@("%.1f".format(convertDistance(StreetEdgeTable.auditedStreetDistance(1, "Anonymous")))) @Messages("dist.metric.abbr")</td>
<td></td>
<td>@("%.0f".format(StreetEdgeTable.streetDistanceCompletionRate(1, "Anonymous") * 100))%</td>
</tr>
<tr style="font-size: 90%;">
<td style="padding-left: 30px">Turker</td>
<td>@("%.1f".format(convertDistance(StreetEdgeTable.auditedStreetDistance(1, "Turker")))) @Messages("admin.overview.distance")</td>
<td>@("%.1f".format(convertDistance(StreetEdgeTable.auditedStreetDistance(1, "Turker")))) @Messages("dist.metric.abbr")</td>
<td></td>
<td>@("%.0f".format(StreetEdgeTable.streetDistanceCompletionRate(1, "Turker") * 100))%</td>
</tr>
<tr style="font-size: 90%;">
<td style="padding-left: 30px">Researcher</td>
<td>@("%.1f".format(convertDistance(StreetEdgeTable.auditedStreetDistance(1, "Researcher")))) @Messages("admin.overview.distance")</td>
<td>@("%.1f".format(convertDistance(StreetEdgeTable.auditedStreetDistance(1, "Researcher")))) @Messages("dist.metric.abbr")</td>
<td></td>
<td>@("%.0f".format(StreetEdgeTable.streetDistanceCompletionRate(1, "Researcher") * 100))%</td>
</tr>
Expand Down Expand Up @@ -791,7 +791,6 @@ <h1>Label Search</h1>


</div>
<link href='@routes.Assets.at("stylesheets/c3.min.css")' rel="stylesheet"/>
<link href='@routes.Assets.at("stylesheets/bootstrap.min.css")' rel="stylesheet"/>
<link href='@routes.Assets.at("stylesheets/dataTables.bootstrap.min.css")' rel="stylesheet"/>
<link href='@routes.Assets.at("stylesheets/ekko-lightbox.css")' rel="stylesheet"/>
Expand All @@ -802,7 +801,6 @@ <h1>Label Search</h1>
<script type="text/javascript" src='@routes.Assets.at("javascripts/lib/moment/moment.js")'></script>
<script type="text/javascript" src='@routes.Assets.at("javascripts/lib/moment/es.js")'></script>
<script type="text/javascript" src='@routes.Assets.at("javascripts/lib/d3.v3.js")'></script>
<script type="text/javascript" src='@routes.Assets.at("javascripts/lib/c3.min.js")'></script>
<script type="text/javascript" src='@routes.Assets.at("javascripts/Admin/build/Admin.js")'></script>
<script type="text/javascript" src='@routes.Assets.at("javascripts/lib/turf.min.js")'></script>
<script type="text/javascript" src='@routes.Assets.at("javascripts/lib/jquery.dataTables.min.js")'></script>
Expand Down Expand Up @@ -886,7 +884,7 @@ <h1>Label Search</h1>
debug: false
}, function(err, t) {
var difficultRegionIds = @Json.toJson(RegionTable.difficultRegionIds);
window.admin = Admin(_, $, c3, turf, difficultRegionIds);
window.admin = Admin(_, $, turf, difficultRegionIds);
$('#commentsTable').dataTable();
$('#labelTable').dataTable();
$('#userTable').dataTable();
Expand Down
2 changes: 0 additions & 2 deletions app/views/admin/task.scala.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,8 @@
</div>
<script type="text/javascript" src='@routes.Assets.at("javascripts/lib/turf.min.js")'></script>
<link href='@routes.Assets.at("stylesheets/previousAudit.css")' rel="stylesheet"/>
<link href='@routes.Assets.at("stylesheets/c3.min.css")' rel="stylesheet"/>
<link href='@routes.Assets.at("stylesheets/admin.css")' rel="stylesheet"/>
<script type="text/javascript" src='@routes.Assets.at("javascripts/lib/d3.v3.js")'></script>
<script type="text/javascript" src='@routes.Assets.at("javascripts/lib/c3.min.js")'></script>
<script type="text/javascript" src='@routes.Assets.at("javascripts/lib/underscore-min.js")'></script>
<script type="text/javascript" src='@routes.Assets.at("javascripts/Admin/build/Admin.js")'></script>
<script type="text/javascript" src='@routes.Assets.at("javascripts/SVLabel/src/SVLabel/util/UtilitiesSidewalk.js")'></script>
Expand Down
Loading

0 comments on commit f844de2

Please sign in to comment.