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

v8.0.1 #3738

Merged
merged 53 commits into from
Nov 14, 2024
Merged

v8.0.1 #3738

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
f40b154
Added label validation filter and popup to user profile map
Oct 24, 2024
e7d37ed
Finally fixed dev environment. Reset original push to fix change #331…
aslassi777 Oct 28, 2024
0055cdb
fixing small typo for language string
aslassi777 Oct 28, 2024
27b0f93
Fixed all changes requested by mikey including: removing ability to j…
aslassi777 Oct 29, 2024
579dd07
Merge remote-tracking branch 'origin/develop' into 3313-create-teams-…
aslassi777 Oct 29, 2024
2feb002
Making small refactoring changes to code, but most importantly making…
aslassi777 Oct 31, 2024
1d76092
Merge branch 'develop' into 3313-create-teams-final
aslassi777 Oct 31, 2024
6ea8ba4
Merge branch 'develop' into 3313-create-teams-final
aslassi777 Oct 31, 2024
f1a7a6f
Merge branch 'develop' into 3313-create-teams-final
aslassi777 Oct 31, 2024
918d026
Added label validation filter and popup to user profile map
Oct 24, 2024
65bc4cf
Merge remote-tracking branch 'origin/465-add-label-filter-to-user-pro…
Nov 5, 2024
fc35eeb
Implemented PR feedback, added label filter to admin user dashboard view
Nov 8, 2024
96aaa38
adds configs for Knox County, Kaohsiung, and Taichung
misaugstad Nov 9, 2024
3ed84aa
removes references to old Google Analytics
misaugstad Nov 9, 2024
331818d
updates dev env and new city setup with unified login
misaugstad Nov 9, 2024
e219d1c
Merge pull request #3730 from ProjectSidewalk/3722-new-city-configs-a…
misaugstad Nov 9, 2024
e65c5ef
Making final changes requested by Mikey, including fixing spacing iss…
aslassi777 Nov 10, 2024
ebc7239
Merge branch 'develop' into 3313-create-teams-final
aslassi777 Nov 10, 2024
3c165e2
fixes missing user_stat table entry when logging into a new city
misaugstad Nov 11, 2024
9d68d7c
adds further checks that user_stat entry exists
misaugstad Nov 11, 2024
5720f5e
Merge pull request #3732 from ProjectSidewalk/3728-fix-unified-login-…
misaugstad Nov 11, 2024
f5934b1
removes text on forgot pw page post unified login
misaugstad Nov 11, 2024
ccb3839
Merge pull request #3733 from ProjectSidewalk/3717-update-forgot-pw-p…
misaugstad Nov 11, 2024
76b21c3
authentication cookie should now be shared across subdomains
misaugstad Nov 12, 2024
94141d9
Merge pull request #3734 from ProjectSidewalk/3714-unified-login-shar…
misaugstad Nov 12, 2024
dcaabf7
small code cleanup
misaugstad Nov 12, 2024
2a5fd4b
Merge pull request #3710 from ProjectSidewalk/3313-create-teams-final
misaugstad Nov 12, 2024
e0bf554
Merge branch 'develop' of https://github.com/ProjectSidewalk/Sidewalk…
misaugstad Nov 12, 2024
07037e7
final code cleanup
misaugstad Nov 12, 2024
8dd1bcc
Merge pull request #3708 from ProjectSidewalk/465-add-label-filter-to…
misaugstad Nov 12, 2024
fabc6eb
adds new tags for cycle lanes, utility cabinet, etc
misaugstad Nov 13, 2024
b208226
only show cycle lane tags in Chicago
misaugstad Nov 13, 2024
11fb121
cycle lane: with/without prot from traffic now mutually exclusive
misaugstad Nov 13, 2024
95d4f9d
Merge pull request #3736 from ProjectSidewalk/3729-add-tags-for-cycle…
misaugstad Nov 13, 2024
8e76db8
fixes cookies attempting to be shared between test and prod
misaugstad Nov 14, 2024
d0f99de
fixes servers not connecting to db
misaugstad Nov 14, 2024
b662a2a
fixes typo
misaugstad Nov 14, 2024
27e7e17
Populate users table with data from new API endpoint, moving out of u…
srihariKrishnaswamy Nov 14, 2024
d1d3022
test-specific config file now used in dev envs
misaugstad Nov 14, 2024
571f028
Merge branch 'develop' of https://github.com/ProjectSidewalk/Sidewalk…
misaugstad Nov 14, 2024
c0b1640
adds missing formats file
misaugstad Nov 14, 2024
1e2dc1b
fixes NaN values when returning user stats on admin page
misaugstad Nov 14, 2024
4a80e74
removes unused HTML after moving Admin users query to an api call
misaugstad Nov 14, 2024
9c8422a
fixes Admin Users table functionality
misaugstad Nov 14, 2024
e9a8218
fixes issue where psql17 was being installed in db image
misaugstad Nov 14, 2024
5c51f09
Merge branch 'develop' of https://github.com/ProjectSidewalk/Sidewalk…
misaugstad Nov 14, 2024
2acf463
fixes timestamp localization on Admin User tab
misaugstad Nov 14, 2024
2d86293
fixes setting org/role in Admin Users table
misaugstad Nov 14, 2024
59352a5
adds loading spinner specifically for Admin Users tab
misaugstad Nov 14, 2024
e10eadc
removes unused import
misaugstad Nov 14, 2024
9e15206
Merge pull request #3737 from ProjectSidewalk/380-admin-page-loads-sl…
misaugstad Nov 14, 2024
5563f62
actually removes the import statement
misaugstad Nov 14, 2024
58a9fa1
8.0.0 -> 8.0.1
misaugstad Nov 14, 2024
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
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