Skip to content
Open
2 changes: 1 addition & 1 deletion app/controllers/AdminController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -752,7 +752,7 @@ class AdminController @Inject() (implicit val env: Environment[User, SessionAuth
if (isAdmin(request.identity)) {
val data = Json.obj(
"user_stats" -> Json.toJson(UserDAOSlick.getUserStatsForAdminPage),
"organizations" -> Json.toJson(OrganizationTable.getAllOrganizations)
"organizations" -> Json.toJson(OrganizationTable.getAllTeams)
)
Future.successful(Ok(data))
} else {
Expand Down
102 changes: 102 additions & 0 deletions app/controllers/LabelController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,108 @@ import play.api.Logger
class LabelController @Inject() (implicit val env: Environment[User, SessionAuthenticator])
extends Silhouette[User, SessionAuthenticator] with ProvidesHeader {

/**
* Fetches a single label by its ID.
*
* @param labelId The ID of the label to find.
* @return The label data as JSON if found, otherwise a 404 response.
*/
def getLabelById(labelId: Int) = UserAwareAction.async { implicit request =>
Logger.info(s"Attempting to fetch label with ID: $labelId")

// Check if the user is authenticated
request.identity match {
case Some(user) =>
LabelTable.getLabelById(labelId) match {
case Some(label) =>
// Map the Label object to a JSON response
val jsLabel = Json.obj(
"labelId" -> label.labelId,
"description" -> label.description,
"severity" -> label.severity,
"tags" -> label.tags
// Add other fields as necessary
)
// Return the label data as JSON
Future.successful(Ok(jsLabel))
case None =>
// If label is not found, return a 404 Not Found response
Future.successful(NotFound(Json.obj("error" -> "Label not found")))
}
case None =>
// If user is not authenticated, redirect to login
Future.successful(Redirect("/login"))
}
}

/**
* Delete a label by its ID.
*/
def deleteLabelById(labelId: Int) = UserAwareAction.async { implicit request =>
Logger.info(s"Attempting to delete label with ID: $labelId")

// Check if the user is authenticated
request.identity match {
case Some(user) =>
// Try to delete the label
LabelTable.deleteLabelById(labelId) match {
case affectedRows if affectedRows > 0 =>
// If at least one row was affected, label is deleted
Future.successful(Ok(Json.obj("message" -> s"Label with ID $labelId has been deleted.")))
case _ =>
// If no rows were affected, return a 404 response
Future.successful(NotFound(Json.obj("error" -> "Label not found")))
}
case None =>
// If the user is not authenticated, redirect to login
Future.successful(Redirect("/login"))
}
}

/**
* Updates a label's properties (severity, description, tags).
*
* @param labelId The ID of the label to update.
* @return A JSON response indicating success or failure.
*/
def updateFromUserDashboard(labelId: Int) = UserAwareAction.async(parse.json) { implicit request =>
// Log the labelId for debugging purposes
Logger.info(s"Attempting to update label with ID: $labelId")

// Extract the JSON data from the request body
val updatedData = request.body.as[JsObject]

// Extract severity, description, and tags from the request body
val severity = (updatedData \ "severity").asOpt[Int]
val description = (updatedData \ "description").asOpt[String]
val tags = (updatedData \ "tags").asOpt[List[String]].getOrElse(List())

// Check if the user is authenticated
request.identity match {
case Some(user) =>
// Check if the label exists
LabelTable.getLabelById(labelId) match {
case Some(labelToUpdate) =>
// Proceed with updating the label
val updateResult = LabelTable.updateFromUserDashboard(labelId, severity, description, tags)

// If update was successful, return the updated label details
if (updateResult > 0) {
Future.successful(Ok(Json.obj("message" -> s"Label with ID $labelId updated successfully.")))
} else {
// If no rows were affected, return Not Found
Future.successful(NotFound(Json.obj("error" -> "Label update failed or no changes made.")))
}
case None =>
// If the label was not found, return Not Found
Future.successful(NotFound(Json.obj("error" -> "Label not found")))
}
case None =>
// If the user is not authenticated, redirect to login
Future.successful(Redirect("/login"))
}
}

/**
* Fetches the labels that a user has added in the current region they are working in.
*
Expand Down
71 changes: 62 additions & 9 deletions app/controllers/UserProfileController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import play.api.i18n.Messages
import scala.concurrent.Future
import play.api.mvc._
import models.user.OrganizationTable
import models.user.Organization

/**
* Holds the HTTP requests associated with the user dashboard.
Expand Down Expand Up @@ -205,17 +206,43 @@ 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]
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)
// 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
))
}
Ok(Json.obj(
"message" -> "Organization created successfully!",
"org_id" -> orgId
))
}

/**
* Grabs a list of all the teams in the tables,
* regardless of open or closed status.
*/
def getTeams() = UserAwareAction.async { implicit request =>
val teams: List[Organization] = OrganizationTable.getAllTeams()

// Convert the list of organizations to JSON
val teamJson = Json.toJson(teams)

// Return the JSON response
Future.successful(Ok(teamJson))
}

/**
* Grabs a list of all "open" teams in the tables.
*/
def getAllOpenTeams() = UserAwareAction.async { implicit request =>

val OpenTeams: List[Organization] = OrganizationTable.getAllOpenTeams()

val teamJson = Json.toJson(OpenTeams)

Future.successful(Ok(teamJson))
}


/**
Expand All @@ -239,4 +266,30 @@ class UserProfileController @Inject() (implicit val env: Environment[User, Sessi
Future.successful(Ok(Json.obj("error" -> "0", "message" -> "Your user id could not be found.")))
}
}

/**
* Updates the open status of the specified organization.
*
* @param orgId The ID of the organization to update.
*/
def updateStatus(orgId: Int) = Action(parse.json) { request =>
val isOpen = (request.body \ "isOpen").as[Boolean]

OrganizationTable.updateStatus(orgId, isOpen)

Ok(Json.obj("status" -> "success", "org_id" -> orgId, "isOpen" -> isOpen))
}

/**
* Updates the visibility status of the specified organization.
*
* @param orgId The ID of the organization to update.
*/
def updateVisibility(orgId: Int) = Action(parse.json) { request =>
val isVisible = (request.body \ "isVisible").as[Boolean]

OrganizationTable.updateVisibility(orgId, isVisible)

Ok(Json.obj("status" -> "success", "org_id" -> orgId, "isVisible" -> isVisible))
}
}
4 changes: 3 additions & 1 deletion app/formats/json/OrganizationFormats.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ object OrganizationFormats {
implicit val organizationWrites: Writes[Organization] = (
(JsPath \ "orgId").write[Int] and
(JsPath \ "orgName").write[String] and
(JsPath \ "orgDescription").write[String]
(JsPath \ "orgDescription").write[String] and
(JsPath \ "isOpen").write[Boolean] and
(JsPath \ "isVisible").write[Boolean]
)(unlift(Organization.unapply _))
}
45 changes: 45 additions & 0 deletions app/models/label/LabelTable.scala
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,51 @@ object LabelTable {
labelsWithExcludedUsers.filter(_.userId === userId.toString).size.run
}

/**
* Find a Label by its ID.
*
* @param labelId The ID of the label to find.
* @return An Option[Label] - Some(Label) if found, None if not found.
*/
def getLabelById(labelId: Int): Option[Label] = db.withSession { implicit session =>
val query = labelsUnfiltered.filter(_.labelId === labelId).take(1)

query.firstOption
}

/**
* Marks a label as deleted by setting its `deleted` field to true.
*
* @param labelId The ID of the label to delete.
* @return The number of affected rows (should be 1 if the label exists and is successfully updated).
*/
def deleteLabelById(labelId: Int): Int = db.withTransaction { implicit session =>
val labelToDeleteQuery = labelsUnfiltered.filter(_.labelId === labelId)

labelToDeleteQuery.map(_.deleted).update(true)
}

def updateFromUserDashboard(labelId: Int, severity: Option[Int], description: Option[String], tags: List[String]): Int = db.withTransaction { implicit session =>
val labelToUpdateQuery = labelsUnfiltered.filter(_.labelId === labelId)
val labelToUpdate: Label = labelToUpdateQuery.first
val cleanedTags: List[String] = TagTable.cleanTagList(tags, labelToUpdate.labelTypeId)
// If the severity or tags have been changed, we need to update the label_history table as well.
if (labelToUpdate.severity != severity || labelToUpdate.tags.toSet != cleanedTags.toSet) {
// If there are multiple entries in the label_history table, then the label has been edited before and we need to
// add an entirely new entry to the table. Otherwise we can just update the existing entry.
val labelHistoryCount: Int = LabelHistoryTable.labelHistory.filter(_.labelId === labelId).length.run
if (labelHistoryCount > 1) {
LabelHistoryTable.save(LabelHistory(0, labelId, severity, cleanedTags, labelToUpdate.userId, new Timestamp(Instant.now.toEpochMilli), "User Dashboard Edit", None))
} else {
LabelHistoryTable.labelHistory.filter(_.labelId === labelId).map(l => (l.severity, l.tags)).update((severity, cleanedTags))
}
}
// Update the label table here.
labelToUpdateQuery
.map(l => (l.severity, l.description, l.tags))
.update((severity, description, tags.distinct))
}

/**
* Update the metadata that users might change on the Explore page after initially placing the label.
*
Expand Down
77 changes: 65 additions & 12 deletions app/models/user/OrganizationTable.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,23 @@ package models.user

import models.utils.MyPostgresDriver.simple._
import play.api.Play.current
import play.api.libs.json.{Json, OFormat}

case class Organization(orgId: Int, orgName: String, orgDescription: String)

case class Organization(orgId: Int, orgName: String, orgDescription: String, isOpen: Boolean, isVisible: Boolean)

object Organization {
implicit val format: OFormat[Organization] = Json.format[Organization]
}

class OrganizationTable(tag: slick.lifted.Tag) extends Table[Organization](tag, "organization") {
def orgId = column[Int]("org_id", O.PrimaryKey, O.AutoInc)
def orgName = column[String]("org_name", O.NotNull)
def orgDescription = column[String]("org_description", O.NotNull)
def isOpen = column[Boolean]("is_open", O.NotNull, O.Default(true))
def isVisible = column[Boolean]("is_visible", O.NotNull, O.Default(true))

def * = (orgId, orgName, orgDescription) <> ((Organization.apply _).tupled, Organization.unapply)
def * = (orgId, orgName, orgDescription, isOpen, isVisible) <> ((Organization.apply _).tupled, Organization.unapply)
}

/**
Expand All @@ -20,15 +28,6 @@ object OrganizationTable {
val db = play.api.db.slick.DB
val organizations = TableQuery[OrganizationTable]

/**
* Gets a list of all organizations.
*
* @return A list of all organizations.
*/
def getAllOrganizations: List[Organization] = db.withSession { implicit session =>
organizations.list
}

/**
* Checks if the organization with the given id exists.
*
Expand Down Expand Up @@ -67,7 +66,61 @@ object OrganizationTable {
* @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.
val newOrganization = Organization(0, orgName, orgDescription, true, true) // orgId is auto-generated.
(organizations returning organizations.map(_.orgId)) += newOrganization
}

/**
* Gets a list of all teams, regardless of status.
*
* @return A list of all teams.
*/
def getAllTeams(): List[Organization] = db.withSession { implicit session =>
organizations.list
}

/**
* Gets a list of all "open" teams.
*
* @return A list of all open teams.
*/
def getAllOpenTeams(): List[Organization] = db.withSession { implicit session =>
organizations.filter(_.isOpen === true).list
}

/**
* Updates the visibility of an organization.
*
* @param orgId: The ID of the organization to update.
* @param isVisible: The new visibility status.
*/
def updateVisibility(orgId: Int, isVisible: Boolean): Int = db.withSession { implicit session =>
val query = for {
org <- organizations if org.orgId === orgId
} yield (org.isVisible)
query.update((isVisible))
}

/**
* Updates the status of an organization.
*
* @param orgId: The ID of the organization to update.
* @param isOpen: The new status of the organization.
*/
def updateStatus(orgId: Int, isOpen: Boolean): Int = db.withSession { implicit session =>
val query = for {
org <- organizations if org.orgId === orgId
} yield (org.isOpen)
query.update((isOpen))
}

/**
* Gets the organization by the given organization id.
*
* @param orgId The id of the organization.
* @return An Option containing the organization, or None if not found.
*/
def getOrganization(orgId: Int): Option[Organization] = db.withTransaction { implicit session =>
organizations.filter(_.orgId === orgId).firstOption
}
}
14 changes: 9 additions & 5 deletions app/models/user/UserStatTable.scala
Original file line number Diff line number Diff line change
Expand Up @@ -306,11 +306,15 @@ object UserStatTable {
""
}
val orgFilter: String = orgId match {
case Some(id) => "AND user_org.org_id = " + id
case None =>
// Temporarily filtering out previous course sections from the leaderboard. Need to remove soon.
if (byOrg) "AND organization.org_name NOT LIKE 'DHD206 % 2021' AND organization.org_name NOT LIKE 'DHD206 % 2022'"
else ""
case Some(id) =>
// If orgId is provided, filter by orgId and also ensure the organization is visible
s"AND user_org.org_id = $id AND organization.is_visible = TRUE"
case None =>
if (byOrg) {
"AND organization.is_visible = TRUE"
} else {
""
}
}
// There are quite a few changes to make to the query when grouping by team/org instead of user. All of those below.
val groupingCol: String = if (byOrg) "user_org.org_id" else "sidewalk_user.user_id"
Expand Down
Loading