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

Refactor Backend: Merge UserSQL and User #2913

Merged
merged 16 commits into from
Jul 24, 2018
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
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
18 changes: 9 additions & 9 deletions app/controllers/AnnotationController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import models.binary.{DataSetDAO, DataSetSQLDAO}
import models.binary.DataSetDAO
import models.task.TaskSQLDAO
import models.user.time._
import models.user.{User, UserDAO}
import oxalis.security.WebknossosSilhouette.{SecuredAction, UserAwareAction, SecuredRequest}
import models.user.{UserSQL, UserSQLDAO}
import oxalis.security.WebknossosSilhouette.{SecuredAction, SecuredRequest, UserAwareAction}
import play.api.i18n.{Messages, MessagesApi}
import play.api.libs.json.{JsArray, _}
import utils.ObjectId
Expand Down Expand Up @@ -112,15 +112,14 @@ class AnnotationController @Inject()(val messagesApi: MessagesApi)
}

def reopen(typ: String, id: String) = SecuredAction.async { implicit request =>
def isReopenAllowed(user: User, annotation: AnnotationSQL) = for {
teamIdBson <- annotation._team.toBSONObjectId.toFox
isAdminOrTeamManager <- user.isTeamManagerOrAdminOf(teamIdBson)
def isReopenAllowed(user: UserSQL, annotation: AnnotationSQL) = for {
isAdminOrTeamManager <- user.isTeamManagerOrAdminOf(annotation._team)
} yield (annotation._user == user._id || isAdminOrTeamManager)

for {
annotation <- provideAnnotation(typ, id)(securedRequestToUserAwareRequest)
isAllowed <- isReopenAllowed(request.identity, annotation)
_ <- isAllowed ?~> "reopen.notAllowed"
_ <- isAllowed.toFox ?~> "reopen.notAllowed"
_ <- annotation.muta.reopen ?~> "annotation.invalid"
updatedAnnotation <- provideAnnotation(typ, id)(securedRequestToUserAwareRequest)
json <- updatedAnnotation.publicWrites(Some(request.identity))
Expand All @@ -145,7 +144,7 @@ class AnnotationController @Inject()(val messagesApi: MessagesApi)
}
}

private def finishAnnotation(typ: String, id: String, user: User)(implicit request: SecuredRequest[_]): Fox[(AnnotationSQL, String)] = {
private def finishAnnotation(typ: String, id: String, user: UserSQL)(implicit request: SecuredRequest[_]): Fox[(AnnotationSQL, String)] = {
for {
annotation <- provideAnnotation(typ, id)(securedRequestToUserAwareRequest)
restrictions <- restrictionsFor(typ, id)(securedRequestToUserAwareRequest)
Expand Down Expand Up @@ -231,7 +230,8 @@ class AnnotationController @Inject()(val messagesApi: MessagesApi)
restrictions <- restrictionsFor(typ, id)(securedRequestToUserAwareRequest)
_ <- restrictions.allowFinish(request.identity) ?~> Messages("notAllowed")
newUserId <- (request.body \ "userId").asOpt[String].toFox
newUser <- UserDAO.findOneById(newUserId) ?~> Messages("user.notFound")
newUserIdValidated <- ObjectId.parse(newUserId)
newUser <- UserSQLDAO.findOne(newUserIdValidated) ?~> Messages("user.notFound")
_ <- annotation.muta.transferToUser(newUser)
updated <- provideAnnotation(typ, id)(securedRequestToUserAwareRequest)
json <- updated.publicWrites(Some(request.identity), Some(restrictions))
Expand All @@ -251,7 +251,7 @@ class AnnotationController @Inject()(val messagesApi: MessagesApi)
}
}

private def duplicateAnnotation(annotation: AnnotationSQL, user: User)(implicit ctx: DBAccessContext): Fox[AnnotationSQL] = {
private def duplicateAnnotation(annotation: AnnotationSQL, user: UserSQL)(implicit ctx: DBAccessContext): Fox[AnnotationSQL] = {
for {
dataSet <- DataSetDAO.findOneById(annotation._dataSet).toFox ?~> Messages("dataSet.notFound", annotation._dataSet)
oldTracingReference = annotation.tracing
Expand Down
11 changes: 5 additions & 6 deletions app/controllers/AnnotationIOController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ class AnnotationIOController @Inject()(val messagesApi: MessagesApi)
} yield result
}

def downloadExplorational(annotationId: String, typ: String, user: User)(implicit request: UserAwareRequest[_]) = {
def downloadExplorational(annotationId: String, typ: String, user: UserSQL)(implicit request: UserAwareRequest[_]) = {

def skeletonToDownloadStream(dataSet: DataSet, annotation: AnnotationSQL, name: String) = {
for {
Expand Down Expand Up @@ -170,20 +170,19 @@ class AnnotationIOController @Inject()(val messagesApi: MessagesApi)
}
}

def downloadProject(projectId: String, user: User)(implicit ctx: DBAccessContext) = {
def downloadProject(projectId: String, user: UserSQL)(implicit ctx: DBAccessContext) = {
for {
projectIdValidated <- ObjectId.parse(projectId)
project <- ProjectSQLDAO.findOne(projectIdValidated) ?~> Messages("project.notFound", projectId)
teamIdBson <- project._team.toBSONObjectId.toFox
_ <- user.assertTeamManagerOrAdminOf(teamIdBson)
_ <- Fox.assertTrue(user.isTeamManagerOrAdminOf(project._team))
annotations <- AnnotationSQLDAO.findAllFinishedForProject(projectIdValidated)
zip <- AnnotationService.zipAnnotations(annotations, project.name + "_nmls.zip")
} yield {
Ok.sendFile(zip.file)
}
}

def downloadTask(taskId: String, user: User)(implicit ctx: DBAccessContext) = {
def downloadTask(taskId: String, user: UserSQL)(implicit ctx: DBAccessContext) = {
def createTaskZip(task: TaskSQL): Fox[TemporaryFile] = task.annotations.flatMap { annotations =>
val finished = annotations.filter(_.state == Finished)
AnnotationService.zipAnnotations(finished, task._id.toString + "_nmls.zip")
Expand All @@ -197,7 +196,7 @@ class AnnotationIOController @Inject()(val messagesApi: MessagesApi)
} yield Ok.sendFile(zip.file)
}

def downloadTaskType(taskTypeId: String, user: User)(implicit ctx: DBAccessContext) = {
def downloadTaskType(taskTypeId: String, user: UserSQL)(implicit ctx: DBAccessContext) = {
def createTaskTypeZip(taskType: TaskTypeSQL) =
for {
tasks <- TaskSQLDAO.findAllByTaskType(taskType._id)
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/Application.scala
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ class Application @Inject()(val messagesApi: MessagesApi) extends Controller {
AnalyticsSQLDAO.insertOne(
AnalyticsEntrySQL(
ObjectId.generate,
request.identity.map(user => ObjectId.fromBsonId(user._id)),
request.identity.map(_._id),
namespace,
request.body))
Ok
Expand Down
16 changes: 8 additions & 8 deletions app/controllers/Authentication.scala
Original file line number Diff line number Diff line change
Expand Up @@ -163,9 +163,9 @@ class Authentication @Inject()(
} else {
for {
organization <- OrganizationSQLDAO.findOneByName(signUpData.organization)(GlobalAccessContext)
user <- UserService.insert(organization.name, email, firstName, lastName, signUpData.password, automaticUserActivation, roleOnRegistration,
user <- UserService.insert(organization._id, email, firstName, lastName, signUpData.password, automaticUserActivation, roleOnRegistration,
loginInfo, passwordHasher.hash(signUpData.password))
brainDBResult <- BrainTracing.register(user).toFox
brainDBResult <- BrainTracing.registerIfNeeded(user).toFox
} yield {
Mailer ! Send(DefaultMails.registerMail(user.name, user.email, brainDBResult))
Mailer ! Send(DefaultMails.registerAdminNotifyerMail(user, user.email, brainDBResult, organization))
Expand All @@ -188,7 +188,7 @@ class Authentication @Inject()(
UserService.retrieve(loginInfo).flatMap {
case None =>
Future.successful(BadRequest(Messages("error.noUser")))
case Some(user) if (user.isActive) => for {
case Some(user) if (!user.isDeactivated) => for {
authenticator <- env.authenticatorService.create(loginInfo)
value <- env.authenticatorService.init(authenticator)
result <- env.authenticatorService.embed(value, Ok)
Expand All @@ -213,7 +213,7 @@ class Authentication @Inject()(
}

def switchTo(email: String) = SecuredAction.async { implicit request =>
if (request.identity._isSuperUser.openOr(false)) {
if (request.identity.isSuperUser) {
val loginInfo = LoginInfo(CredentialsProvider.ID, email)
for {
_ <- findOneByEmail(email) ?~> Messages("user.notFound")
Expand Down Expand Up @@ -254,7 +254,7 @@ class Authentication @Inject()(
bearerTokenAuthenticatorService.userForToken(passwords.token.trim)(GlobalAccessContext).futureBox.flatMap {
case Full(user) =>
for {
_ <- UserDAO.changePasswordInfo(user._id, passwordHasher.hash(passwords.password1))(GlobalAccessContext)
_ <- UserSQLDAO.updatePasswordInfo(user._id, passwordHasher.hash(passwords.password1))(GlobalAccessContext)
_ <- bearerTokenAuthenticatorService.remove(passwords.token.trim)
} yield Ok
case _ =>
Expand Down Expand Up @@ -346,7 +346,7 @@ class Authentication @Inject()(
val returnPayload =
s"nonce=$nonce&" +
s"email=${URLEncoder.encode(user.email, "UTF-8")}&" +
s"external_id=${URLEncoder.encode(user.id, "UTF-8")}&" +
s"external_id=${URLEncoder.encode(user._id.toString, "UTF-8")}&" +
s"username=${URLEncoder.encode(user.abreviatedName, "UTF-8")}&" +
s"name=${URLEncoder.encode(user.name, "UTF-8")}"
val encodedReturnPayload = Base64.encodeBase64String(returnPayload.getBytes("UTF-8"))
Expand Down Expand Up @@ -390,7 +390,7 @@ class Authentication @Inject()(
} else {
for {
organization <- createOrganization(signUpData.organization) ?~> Messages("organization.create.failed")
user <- UserService.insert(organization.name, email, firstName, lastName, signUpData.password, isActive = true, teamRole = true,
user <- UserService.insert(organization._id, email, firstName, lastName, signUpData.password, isActive = true, teamRole = true,
loginInfo, passwordHasher.hash(signUpData.password), isAdmin = true)
_ <- createOrganizationFolder(organization.name, loginInfo)
} yield Ok
Expand All @@ -403,7 +403,7 @@ class Authentication @Inject()(
)
}

private def creatingOrganizationsIsAllowed(requestingUser: Option[User]) = {
private def creatingOrganizationsIsAllowed(requestingUser: Option[UserSQL]) = {
val noOrganizationPresent = InitialDataService.assertNoOrganizationsPresent
val configurationFlagSet = Play.configuration.getBoolean("application.allowOrganzationCreation").getOrElse(false) ?~> "allowOrganzationCreation.notEnabled"
val userIsSuperUser = requestingUser.exists(_.isSuperUser).toFox
Expand Down
28 changes: 14 additions & 14 deletions app/controllers/ConfigurationController.scala
Original file line number Diff line number Diff line change
@@ -1,27 +1,25 @@
package controllers

import com.scalableminds.util.tools.Fox
import javax.inject.Inject

import models.binary.{DataSet, DataSetDAO, DataSetSQLDAO}
import models.configuration.{DataSetConfiguration, UserConfiguration}
import models.user.UserService
import oxalis.security.WebknossosSilhouette.{SecuredAction, SecuredRequest, UserAwareAction, UserAwareRequest}
import models.team.OrganizationSQLDAO
import models.user.{UserDataSetConfigurationSQLDAO, UserService}
import oxalis.security.WebknossosSilhouette.{SecuredAction, UserAwareAction}
import play.api.i18n.{Messages, MessagesApi}
import play.api.libs.concurrent.Execution.Implicits._
import play.api.libs.json.JsObject
import play.api.libs.json.{JsObject, JsValue}
import play.api.libs.json.Json._
import play.api.mvc.Action
import play.libs.Json

/**
* Controller that handles the CRUD api for configurations (mostly settings for the tracing view)
*/

class ConfigurationController @Inject()(val messagesApi: MessagesApi) extends Controller {

def read = UserAwareAction.async { implicit request =>
request.identity.toFox.flatMap { user =>
UserService.findOneById(user.id, useCache = false)
.map(_.userConfiguration.configurationOrDefaults)
for {
Copy link
Contributor

Choose a reason for hiding this comment

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

just out of curiosity: why did you switch here to the for syntax?

Copy link
Member Author

Choose a reason for hiding this comment

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

userConfigurationStructured now returns a Fox instead of the content directly (this is a common change in this PR). I tend to get confused with nested flatMaps, which would have now been necessary here.

Copy link
Contributor

Choose a reason for hiding this comment

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

got it, thanks 👍

userConfig <- user.userConfigurationStructured
} yield userConfig.configurationOrDefaults
}
.getOrElse(UserConfiguration.default.configuration)
.map(configuration => Ok(toJson(configuration)))
Expand All @@ -39,8 +37,9 @@ class ConfigurationController @Inject()(val messagesApi: MessagesApi) extends Co

def readDataSet(dataSetName: String) = UserAwareAction.async { implicit request =>
request.identity.toFox.flatMap { user =>
UserService.findOneById(user.id, useCache = false)
.flatMap(_.dataSetConfigurations.get(dataSetName))
for {
Copy link
Contributor

Choose a reason for hiding this comment

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

This seems to be more than a refactoring. If I understand it correctly, this used to be cached in the UserCache, not sure if it was used from there.

And for my understanding: findOneForUserAndDataset comes from slick now, right?

Copy link
Member Author

Choose a reason for hiding this comment

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

Correct observation, I did not consider that this is now bypassing the Cache. However, I believe that it is not problematic. This configuration is requested via a separate route (not part of the user json) and that route is requested far less often. Also, the UserCache’s cache invalidation should provide that there is not actually a change in behavior (albeit maybe a tiny one in performance)

Copy link
Contributor

Choose a reason for hiding this comment

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

Fine for me 👍

configurationJson: JsValue <- UserDataSetConfigurationSQLDAO.findOneForUserAndDataset(user._id, dataSetName)
} yield DataSetConfiguration(configurationJson.validate[Map[String, JsValue]].getOrElse(Map.empty))
}
.orElse(DataSetDAO.findOneBySourceName(dataSetName).flatMap(_.defaultConfiguration))
.getOrElse(DataSetConfiguration.constructInitialDefault(List()))
Expand Down Expand Up @@ -68,7 +67,8 @@ class ConfigurationController @Inject()(val messagesApi: MessagesApi) extends Co
def updateDataSetDefault(dataSetName: String) = SecuredAction.async(parse.json(maxLength = 20480)) { implicit request =>
for {
dataset <- DataSetDAO.findOneBySourceName(dataSetName) ?~> Messages("dataset.notFound")
_ <- (request.identity.isAdminOf(dataset.owningOrganization) || request.identity.isTeamManagerInOrg(dataset.owningOrganization)) ?~> Messages("notAllowed")
organization <- OrganizationSQLDAO.findOneByName(dataset.owningOrganization)
_ <- Fox.assertTrue(request.identity.isTeamManagerOrAdminOfOrg(organization._id)) ?~> Messages("notAllowed")
jsConfiguration <- request.body.asOpt[JsObject] ?~> Messages("user.configuration.dataset.invalid")
conf = jsConfiguration.fields.toMap
_ <- DataSetSQLDAO.updateDefaultConfigurationByName(dataSetName, DataSetConfiguration(conf))
Expand Down
30 changes: 12 additions & 18 deletions app/controllers/Controller.scala
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
package controllers

import com.scalableminds.util.accesscontext.DBAccessContext
import com.scalableminds.webknossos.datastore.controllers.ValidationHelpers
import com.scalableminds.util.mvc.ExtendedController
import com.scalableminds.util.tools.{Converter, Fox}
import com.typesafe.scalalogging.LazyLogging
import models.basics.Implicits
import models.binary.DataSet
import models.user.User
import models.user.UserSQL
import net.liftweb.common.{Box, Failure, Full, ParamFailure}
import oxalis.security._
import oxalis.view.ProvidesSessionData
Expand All @@ -16,7 +17,6 @@ import play.api.libs.json._
import play.api.mvc.{Request, Result, Controller => PlayController}
import play.twirl.api.Html
import oxalis.security.WebknossosSilhouette.{SecuredAction, SecuredRequest, UserAwareAction, UserAwareRequest}
import reactivemongo.bson.BSONObjectID
import utils.ObjectId


Expand All @@ -31,31 +31,25 @@ trait Controller extends PlayController
implicit def AuthenticatedRequest2Request[T](r: SecuredRequest[T]): Request[T] =
r.request

def ensureTeamAdministration(user: User, teamId: ObjectId): Fox[Unit] =
for {
teamIdBson <- teamId.toBSONObjectId.toFox
_ <- ensureTeamAdministration(user, teamIdBson)
} yield ()
def ensureTeamAdministration(user: UserSQL, teamId: ObjectId): Fox[Unit] =
Fox.assertTrue(user.isTeamManagerOrAdminOf(teamId)) ?~> Messages("team.admin.notAllowed")

def ensureTeamAdministration(user: User, team: BSONObjectID): Fox[Unit] =
user.assertTeamManagerOrAdminOf(team) ?~> Messages("team.admin.notAllowed")

def allowedToAdministrate(admin: User, dataSet: DataSet) =
def allowedToAdministrate(admin: UserSQL, dataSet: DataSet)(implicit ctx: DBAccessContext) =
dataSet.isEditableBy(Some(admin)) ?~> Messages("notAllowed")

case class Filter[A, T](name: String, predicate: (A, T) => Boolean, default: Option[String] = None)(implicit converter: Converter[String, A]) {
def applyOn(list: List[T])(implicit request: Request[_]): List[T] = {
case class Filter[A, T](name: String, predicate: (A, T) => Fox[Boolean], default: Option[String] = None)(implicit converter: Converter[String, A]) {
def applyOn(list: List[T])(implicit request: Request[_]): Fox[List[T]] = {
request.getQueryString(name).orElse(default).flatMap(converter.convert) match {
case Some(attr) => list.filter(predicate(attr, _))
case _ => list
case Some(attr) => Fox.filter(list)(predicate(attr, _))
case _ => Fox.successful(list)
}
}
}

case class FilterColl[T](filters: Seq[Filter[_, T]]) {
def applyOn(list: List[T])(implicit request: Request[_]): List[T] = {
filters.foldLeft(list) {
case (l, filter) => filter.applyOn(l)
def applyOn(list: List[T])(implicit request: Request[_]): Fox[List[T]] = {
filters.foldLeft(Fox.successful(list)) {
case (l, filter) => l.flatMap(filter.applyOn(_))
}
}
}
Expand Down
18 changes: 10 additions & 8 deletions app/controllers/DataSetController.scala
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
package controllers

import javax.inject.Inject

import com.scalableminds.util.geometry.Point3D
import com.scalableminds.util.accesscontext.GlobalAccessContext
import com.scalableminds.util.tools.DefaultConverters._
import com.scalableminds.util.tools.{Fox, JsonHelper}
import models.binary._
import models.team.TeamDAO
import models.user.{User, UserService}
import models.user.UserService
import oxalis.ndstore.{ND2WK, NDServerConnection}
import oxalis.security.URLSharing
import oxalis.security.WebknossosSilhouette.{SecuredAction, SecuredRequest, UserAwareAction}
Expand All @@ -20,6 +19,7 @@ import play.api.libs.json._
import reactivemongo.bson.BSONObjectID
import reactivemongo.play.json.BSONFormats._
import com.scalableminds.util.tools.Math
import utils.ObjectId

import scala.concurrent.ExecutionContext.Implicits._
import scala.concurrent.Future
Expand Down Expand Up @@ -78,14 +78,15 @@ class DataSetController @Inject()(val messagesApi: MessagesApi) extends Controll
def list = UserAwareAction.async { implicit request =>
UsingFilters(
Filter("isEditable", (value: Boolean, el: DataSet) =>
el.isEditableBy(request.identity) && value || !el.isEditableBy(request.identity) && !value),
for {isEditable <- el.isEditableBy(request.identity)} yield {isEditable && value || !isEditable && !value}),
Copy link
Contributor

Choose a reason for hiding this comment

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

also here, why the for syntax?

Copy link
Member Author

Choose a reason for hiding this comment

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

isEditableBy is now also returning a Fox, so the alternative to the for-yield would have been a map of some kind, and I find the for easier to read

Filter("isActive", (value: Boolean, el: DataSet) =>
el.isActive == value)
Fox.successful(el.isActive == value))
) { filter =>
DataSetDAO.findAll.flatMap {
dataSets =>
for {
js <- Fox.serialCombined(filter.applyOn(dataSets))(d => DataSet.dataSetPublicWrites(d, request.identity))
filtered <- filter.applyOn(dataSets)
Copy link
Contributor

Choose a reason for hiding this comment

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

❤️

Copy link
Member Author

Choose a reason for hiding this comment

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

filter.applyOn is now also asynchronous so the result has to be unpacked, which is what this line does :)

js <- Fox.serialCombined(filtered)(d => DataSet.dataSetPublicWrites(d, request.identity))
} yield {
Ok(Json.toJson(js))
}
Expand All @@ -96,9 +97,10 @@ class DataSetController @Inject()(val messagesApi: MessagesApi) extends Controll
def accessList(dataSetName: String) = SecuredAction.async { implicit request =>
for {
dataSet <- DataSetDAO.findOneBySourceName(dataSetName) ?~> Messages("dataSet.notFound", dataSetName)
users <- UserService.findByTeams(dataSet.allowedTeams)
users <- UserService.findByTeams(dataSet.allowedTeams.map(ObjectId.fromBsonId(_)))
usersJs <- Fox.serialCombined(users.distinct)(_.compactWrites)
} yield {
Ok(Writes.list(User.userCompactWrites).writes(users.distinct))
Ok(Json.toJson(usersJs))
}
}

Expand Down Expand Up @@ -176,7 +178,7 @@ class DataSetController @Inject()(val messagesApi: MessagesApi) extends Controll
case (server, name, token, team) =>
for {
_ <- DataSetService.checkIfNewDataSetName(name) ?~> Messages("dataSet.name.alreadyTaken")
_ <- ensureTeamAdministration(request.identity, team)
_ <- ensureTeamAdministration(request.identity, ObjectId.fromBsonId(team)) ?~> Messages("team.admin.notAllowed")
ndProject <- NDServerConnection.requestProjectInformationFromNDStore(server, name, token)
dataSet <- ND2WK.dataSetFromNDProject(ndProject, team)
_ <- DataSetDAO.insert(dataSet)(GlobalAccessContext)
Expand Down
Loading