-
Notifications
You must be signed in to change notification settings - Fork 24
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
Conversation
…nossos into refactor-backend-user
for { | ||
projectIdValidated <- ObjectId.parse(projectId) | ||
project <- ProjectSQLDAO.findOne(projectIdValidated) ?~> Messages("project.notFound", projectId) | ||
teamIdBson <- project._team.toBSONObjectId.toFox | ||
_ <- user.assertTeamManagerOrAdminOf(teamIdBson) | ||
_ <- Fox.assertBoolean(user.isTeamManagerOrAdminOf(project._team)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This purely waits for the future and checks that it is not None
, right? Seems to be the correct refactoring, still wondering what this line does.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is about functions that return Fox[Boolean]
. At several locations, we want to check such booleans which we can only get packed in a Fox. One previous method to deal with that by creating helper functions for each, such as assertTeamManagerOrAdminOf
, which unpacks the Fox returned by isTeamManagerOrAdminOf
, and then checks the content
of that boolean, by casting it to a new Fox via bool2fox
. In this PR the number of these helper functions would have increased, so I refactored this into the helper function Fox.assertBoolean
. It returns Fox[Unit]
, which is successful
only when the original function returns `Fox.successful(true)´.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, ok. How about renaming it to Fox.assertTrue
?
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 { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
got it, thanks 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice 👍
I just reviewed the first files (in the github order), until DataSetController.scala
. Let's clarify those things first :-)
@@ -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 { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fine for me 👍
app/controllers/Controller.scala
Outdated
_ <- ensureTeamAdministration(user, teamIdBson) | ||
} yield () | ||
def ensureTeamAdministration(user: UserSQL, teamId: ObjectId): Fox[Unit] = | ||
Fox.assertBoolean(user.isTeamManagerOrAdminOf(teamId)) ?~> Messages("team.admin.notAllowed") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does assertBoolean
actually assert it's true? It looks like it should, but it doesn't sound like it does.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I sure hope it does (see above)
@@ -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.assertBoolean(request.identity.isTeamManagerOrAdminOfOrg(organization._id)) ?~> Messages("notAllowed") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we just use ensureTeamAdministration
instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, will do.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
or not, since this is not team-specific (kind of a special case, all teammanagers are allowed to do this, no matter which team they are in). But I did this replacement in another instance.
@@ -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}), |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 => | ||
DataSetDAO.findAll.flatMap { | ||
dataSets => | ||
for { | ||
js <- Fox.serialCombined(filter.applyOn(dataSets))(d => DataSet.dataSetPublicWrites(d, request.identity)) | ||
filtered <- filter.applyOn(dataSets) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
❤️
There was a problem hiding this comment.
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 :)
@@ -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) | |||
_ <- Fox.assertBoolean(request.identity.isTeamManagerOrAdminOf(ObjectId.fromBsonId(team))) ?~> Messages("team.admin.notAllowed") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not stay with the new ensureTeamAdministration
? Seems to be equivalent.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, will do.
(the explanation is that I first wanted to remove the ensureTeamAdministration
entirely because I found it a little superfluos and the name a little confusing, but then I saw that it was used in so many places that I stopped removing it. Also, I guess does help reduce clutter a little bit)
def current = SecuredAction { implicit request => | ||
Ok(Json.toJson(request.identity)(User.userPublicWrites(request.identity))) | ||
def current = SecuredAction.async { implicit request => | ||
for { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we have this pattern quite often, should we extract this at some point? (Not part of this PR, though.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
implicit request =>
for {
…
js <- something.publicWrites(request.identity)
} yield Ok(userJs)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
see #2942
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Still not done with the review, but have some more questions and proposals.
@@ -61,8 +63,9 @@ class UserController @Inject()(val messagesApi: MessagesApi) | |||
|
|||
def userLoggedTime(userId: String) = SecuredAction.async { implicit request => | |||
for { | |||
user <- UserDAO.findOneById(userId) ?~> Messages("user.notFound") | |||
_ <- user.isEditableBy(request.identity) ?~> Messages("notAllowed") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How was isEditableBy
actually checked? Did this happen via the ?~>
? I thought that was only checking that the option is defined, but not the packed boolean.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good catch, needs Fox.assertTrue
. The ?~> just adds the failure message
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is now done via the assertBoolean
, right? We had the same thing in multiple places, so probably users were able to do actually to much. So we might now forbid things that were working before (wrongly). Let's maybe discuss that later in person.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
previously, isEditableBy just returned a boolean (which could be checked without the assertBoolean) – but now the signature has changed, so the way we check it needs to change too, in order to preserve the behavior
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wasn't aware of the implicit conversion, see #2941.
Let's double check all the changes where user.isEditableBy
was used before, if they have assertBoolean
/assertTrue
.
) | ||
} | ||
) | ||
def usersLoggedTime = SecuredAction.async(validateJson[TimeSpanRequest]) { implicit request => |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 for extracting validateJson
@@ -149,15 +147,15 @@ class UserController @Inject()(val messagesApi: MessagesApi) | |||
// REST API | |||
def list = SecuredAction.async { implicit request => | |||
UsingFilters( | |||
Filter("includeAnonymous", (value: Boolean, el: User) => value || !el.isAnonymous, default = Some("false")), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is the includeAnonymous
Filter removed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the backend does not support anonymous users anymore (was for amazon mturk users).
@@ -190,20 +192,23 @@ class UserController @Inject()(val messagesApi: MessagesApi) | |||
withJsonBodyUsing(userUpdateReader) { | |||
case (firstName, lastName, email, isActive, isAdmin, assignedMemberships, experiences) => | |||
for { | |||
user <- UserDAO.findOneById(userId) ?~> Messages("user.notFound") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not keep findOneById
? I think we use it quite often.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also unpacking with the Messages("user.notFound")
should be possible in an abstract findOneById
, maybe having a match
expression switching the message-identifier.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure I understand you here. UserSQLDAO replaces UserDAO here. The findOne methods everywhere expect an ObjectId, which is why there is no explicit “byId” in the name. The “user.notFound” failure message is added here since the SQLDAOs have more specific database-related failure messages
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 , see #2940
private def selectSuitableTeam(user: UserSQL, dataSet: DataSet)(implicit ctx: DBAccessContext): Fox[ObjectId] = { | ||
(for { | ||
userTeamIds <- user.teamIds | ||
} yield { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What exactly is flatten
ed here? Could we use async
to circumvent those (for {} yield { for {} yield x }).flatten
patterns?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the outer for-yield returns a Fox, with the content taken from the yield block.
that content, in this case, is itself a Fox.
Hence, the Fox[Fox[ObjectId]] is flattened to Fox[ObjectId].
I haven’t yet tried scala-async, and I’m a bit sceptical of it, especially in combination with Foxes, but maybe that is something we can explore in the future
app/models/team/Team.scala
Outdated
user.organization == organization && user.isAdmin | ||
def assertCouldBeAdministratedBy(user: UserSQL) = | ||
for { | ||
asBoolean <- couldBeAdministratedBy(user) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This syntax is not very intuitive to me. Having something like the following would be awesome (in my opinion, of course):
async def assertCouldBeAdministratedBy(user: UserSQL) =
if (await couldBeAdministratedBy(user))
Success()
else {
// not sure with the syntax here, but something like
throw Messages("notAllowed")
Failure()
}
a) What is your opinion about this, @fm3 ?
b) Is something similar possible?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(having the bool2fox conversion explicitely would help to make it clearer, but I still find an if
statement appropriate)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should be Fox.assertTrue(team.couldBeAdministratedBy(user)) ?~> Messages("notAllowed")
. will change.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
app/models/user/UserService.scala
Outdated
result | ||
} | ||
for { | ||
_ <- UserSQLDAO.updateValues(user._id, firstName, lastName, email, isAdmin, !activated) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why now !activated
? Probably the attribute name would help here.
@@ -96,57 +97,59 @@ object BrainTracing extends LazyLogging with FoxImplicits { | |||
a.id)) | |||
} | |||
|
|||
def logTime(user: User, time: Long, annotation: Option[AnnotationSQL])(implicit ctx: DBAccessContext): Future[Boolean] = { | |||
def logTimeIfNeeded(user: UserSQL, time: Long, annotation: Option[AnnotationSQL])(implicit ctx: DBAccessContext): Fox[Boolean] = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess you forgot to exchange all calls to logTime
to logTimeIfNeeded
. This would also be fine as an inner function, I guess.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
actually, there are no calls to logTime or to logTimeIfNeeded. We don’t log time to BrainTracing anymore, haven’t been doing that for severall months (instead they fetch time via our API now). I’m removing that functionality in #2939
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
brainTracingRequest.onFailure{ | ||
case e: Exception => | ||
logger.error(s"Failed to register user '${user.email}' in brain tracing db. Exception: ${e.getMessage}") | ||
def registerIfNeeded(user: UserSQL): Fox[String] = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@fm3 Did you double check to change all register
calls to registerIfNeeded
? Also here, maybe just use an inner function, if the new register
should actually not be called.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I made register private, forbidding external calls to it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
True, didn't notice this!
app/models/user/User.scala
Outdated
} | ||
|
||
def isAdminOf(otherUser: UserSQL): Boolean = | ||
this._organization == otherUser._organization && this.isAdmin |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
isAdminOf(otherUser._organization)
would be the same and more precise
app/models/user/User.scala
Outdated
def isTeamManagerOrAdminOfOrg(_organization: ObjectId): Fox[Boolean] = | ||
for { | ||
teamManagerTeamIds <- teamManagerTeamIds | ||
} yield teamManagerTeamIds.nonEmpty || this.isAdmin && this._organization == _organization |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yield teamManagerTeamIds.nonEmpty || isAdminOf(_organization)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, use isTeamManagerInOrg(_organization)
should be used, so I guess it's just
isTeamManagerInOrg(_organization) || isAdminOf(_organization)
app/models/user/User.scala
Outdated
organization <- OrganizationSQLDAO.findOneByName(user.organization) | ||
team <- TeamSQLDAO.findOne(_team)(GlobalAccessContext) | ||
teamManagerTeamIds <- teamManagerTeamIds | ||
} yield (teamManagerTeamIds.contains(_team) || this.isAdmin && this._organization == team._organization) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
… || isAdminOf(team._organization)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awesome PR! I went through it now once. I list the requested changes here just to have an overview:
-
Fox.assertBoolean
->Fox.assertTrue
- use
ensureTeamAdministration
when possible - double check old
user.isEditableBy
usages forassertBoolean
/assertTrue
- use
Fox.assertTrue(team.couldBeAdministratedBy(user)) ?~> Messages("notAllowed")
around line 152 ofTeam.scala
- merge in removal of
logTimeIfNeeded
/logTime
from organization-specific newUserMailingList and overTimeMailingList #2939 - refactor
isManager
/… inUser.scala
The only open question from my side is the comment
-
Why now !activated? Probably the attribute name would help here.
👓
Thanks a lot! I will incorporate your feedback! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🚢
URL of deployed dev instance (used for testing):
Steps to test:
Issues:
[ ] Updated changelog[ ] Updated migration guide if applicable