Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

2280 add quick summary of num missions, distance, labels, validations, and accuracy to dashboard #2289

Merged
merged 30 commits into from
Oct 15, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
4afbe0a
made initial duplicate admin user table
jonfroehlich Sep 23, 2020
8634162
added initial stats for num completed missions, total distance, num l…
jonfroehlich Sep 25, 2020
7acbd32
removed daily contribution graphs and history table
jonfroehlich Sep 25, 2020
0a1d08a
added initial version of user dashboard with distance, labels, accura…
jonfroehlich Sep 25, 2020
7c1b89d
keeps the distance, missions, and total reward earned table but only …
jonfroehlich Sep 25, 2020
eb795b7
added some additional new files for badges
jonfroehlich Sep 26, 2020
18f055a
added getUserAccuracy to LabelValidationTable
jonfroehlich Sep 26, 2020
f1a9dd8
Updates getUserAccuracy calculation to fix type errors
jonfroehlich Sep 26, 2020
16d3a1e
Fixed the accuracy display
jonfroehlich Sep 27, 2020
2ea92ae
added validation count in summary stats
jonfroehlich Sep 28, 2020
6b3b5ec
Merge branch 'develop' of https://github.com/ProjectSidewalk/Sidewalk…
misaugstad Sep 28, 2020
3f06870
added validations to quick summary dash
jonfroehlich Sep 28, 2020
fda5c02
added adaptive km vs mi based on @Messages("measurement.system")
jonfroehlich Oct 1, 2020
2eb9800
Switched to using message table with spanish translations
jonfroehlich Oct 1, 2020
ab35f1b
removed commented out old dashboard code
jonfroehlich Oct 1, 2020
db5dd26
removed more old commented code
jonfroehlich Oct 1, 2020
701e20b
removed outdated JavaScript code from Progress.js
jonfroehlich Oct 1, 2020
b2b4c6e
Removed unnecessary JavaScript includes
jonfroehlich Oct 1, 2020
5dd4746
Merge branch 'develop' of https://github.com/ProjectSidewalk/Sidewalk…
misaugstad Oct 13, 2020
49f7518
Merge branch 'develop' of https://github.com/ProjectSidewalk/Sidewalk…
misaugstad Oct 14, 2020
63b2f86
fixes some code style
misaugstad Oct 14, 2020
3d6ef5d
moved unit conversion logic from model to controller
misaugstad Oct 14, 2020
31ec1d5
removes redundant info on turker user dashboard
misaugstad Oct 14, 2020
328531c
removes unused translations
misaugstad Oct 14, 2020
988fb2c
removes unused icons
misaugstad Oct 14, 2020
4ff212e
removing code that we no longer use
misaugstad Oct 14, 2020
4069d68
removed/renamed some CSS
misaugstad Oct 14, 2020
7fcf77c
adds alt text to images on user dashboard
misaugstad Oct 14, 2020
1b96b0f
removes uncropped icons that have been moved to branding repo
misaugstad Oct 15, 2020
37e50f7
adds newlines to end of dashboard.json files
misaugstad Oct 15, 2020
File filter

Filter by extension

Filter by extension

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make sure to change your settings to use spaces instead of tabs, as per style guide.

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