Skip to content

Commit

Permalink
Add ui prefs, and a topic-filter-first forum buttons layout, and
Browse files Browse the repository at this point in the history
... and also a timeout-idle-session-after-minutes setting.
  • Loading branch information
kajmagnus committed Feb 20, 2019
1 parent 732b47c commit fe08383
Show file tree
Hide file tree
Showing 37 changed files with 499 additions and 146 deletions.
2 changes: 1 addition & 1 deletion app/controllers/CustomFormController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ class CustomFormController @Inject()(cc: ControllerComponents, edContext: EdCont
// (A bit weird, here we authz with Authz.maySubmitCustomForm(), but later in
// PostsDao.insertReply via Authz.mayPostReply() — but works okay.)
throwNoUnless(Authz.maySubmitCustomForm(
request.userAndLevels, dao.getGroupIds(request.user),
request.userAndLevels, dao.getGroupIdsOwnFirst(request.user),
pageMeta, inCategoriesRootLast = categoriesRootLast,
permissions = dao.getPermsOnPages(categoriesRootLast)),
"EdE2TE4A0")
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/ForumController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ class ForumController @Inject()(cc: ControllerComponents, edContext: EdContext)
(editedCategory, permissions)
}

val callersGroupIds = request.authzContext.groupIds
val callersGroupIds = request.authzContext.groupIdsUserIdFirst
val callersNewPerms = permsWithIds.filter(callersGroupIds contains _.forPeopleId)
val mkJson = dao.jsonMaker.makeCategoriesJson _

Expand Down
1 change: 1 addition & 0 deletions app/controllers/ImportExportController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,7 @@ class ImportExportController @Inject()(cc: ControllerComponents, edContext: EdCo
tinyAvatar = None, // [readlater]
smallAvatar = None, // [readlater]
mediumAvatar = None, // [readlater]
uiPrefs = None, // [readlater]
isOwner = readOptBool(jsObj, "isOwner") getOrElse false,
isAdmin = readOptBool(jsObj, "isAdmin") getOrElse false,
isModerator = readOptBool(jsObj, "isModerator") getOrElse false,
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/ReplyController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ class ReplyController @Inject()(cc: ControllerComponents, edContext: EdContext)
val pageRole = PageRole.EmbeddedComments

throwNoUnless(Authz.mayCreatePage(
request.theUserAndLevels, dao.getGroupIds(requester),
request.theUserAndLevels, dao.getGroupIdsOwnFirst(requester),
pageRole, PostType.Normal, pinWhere = None, anySlug = slug, anyFolder = folder,
inCategoriesRootLast = categoriesRootLast,
permissions = dao.getPermsOnPages(categories = categoriesRootLast)),
Expand Down
24 changes: 20 additions & 4 deletions app/controllers/UserController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ class UserController @Inject()(cc: ControllerComponents, edContext: EdContext)
callerIsAdmin: Boolean, callerIsStaff: Boolean = false, callerIsUserHerself: Boolean = false,
anyStats: Option[UserStats] = None)
: JsObject = {
var userJson = Json.obj( // MemberInclDetails
var userJson = Json.obj( // MemberInclDetails [B28JG4]
"id" -> user.id,
"externalId" -> JsStringOrNull(user.externalId),
"createdAtEpoch" -> JsNumber(user.createdAt.getTime),
Expand Down Expand Up @@ -246,6 +246,7 @@ class UserController @Inject()(cc: ControllerComponents, edContext: EdContext)
userJson += "summaryEmailIfActiveOwn" -> JsBooleanOrNull(user.summaryEmailIfActive)
userJson += "summaryEmailIfActive" ->
JsBooleanOrNull(user.effectiveSummaryEmailIfActive(groups))
userJson += "uiPrefs" -> user.uiPrefs.getOrElse(JsNull)
userJson += "isApproved" -> JsBooleanOrNull(user.isApproved)
userJson += "approvedAtMs" -> JsDateMsOrNull(user.approvedAt)
userJson += "approvedById" -> JsNumberOrNull(user.approvedById)
Expand Down Expand Up @@ -275,7 +276,7 @@ class UserController @Inject()(cc: ControllerComponents, edContext: EdContext)

private def jsonForGroupInclDetails(group: Group, callerIsAdmin: Boolean,
callerIsStaff: Boolean = false): JsObject = {
var json = Json.obj(
var json = Json.obj( // [B28JG4]
"id" -> group.id,
"isGroup" -> JsTrue,
//"createdAtEpoch" -> JsWhen(group.createdAt),
Expand All @@ -284,6 +285,7 @@ class UserController @Inject()(cc: ControllerComponents, edContext: EdContext)
if (callerIsStaff) {
json += "summaryEmailIntervalMins" -> JsNumberOrNull(group.summaryEmailIntervalMins)
json += "summaryEmailIfActive" -> JsBooleanOrNull(group.summaryEmailIfActive)
json += "uiPrefs" -> group.uiPrefs.getOrElse(JsNull)
}
json
}
Expand Down Expand Up @@ -1279,7 +1281,7 @@ class UserController @Inject()(cc: ControllerComponents, edContext: EdContext)

SECURITY // Later: shouldn't list authors of hidden / deleted / whisper posts.
throwNoUnless(Authz.maySeePage(
pageMeta, request.user, dao.getGroupIds(request.user),
pageMeta, request.user, dao.getGroupIdsOwnFirst(request.user),
dao.getAnyPrivateGroupTalkMembers(pageMeta), categoriesRootLast,
permissions = dao.getPermsOnPages(categoriesRootLast)), "EdEZBXKSM2")

Expand Down Expand Up @@ -1307,7 +1309,7 @@ class UserController @Inject()(cc: ControllerComponents, edContext: EdContext)
}


def saveGroupPreferences: Action[JsValue] = PostJsonAction(RateLimits.ConfigUser,
def saveAboutGroupPreferences: Action[JsValue] = PostJsonAction(RateLimits.ConfigUser,
maxBytes = 3000) { request =>
import request.{dao, theRequester => requester}
val prefs = aboutGroupPrefsFromJson(request.body)
Expand All @@ -1318,6 +1320,20 @@ class UserController @Inject()(cc: ControllerComponents, edContext: EdContext)
}


def saveUiPreferences: Action[JsValue] = PostJsonAction(RateLimits.ConfigUser,
maxBytes = 3000) { request =>
import request.{body, dao, theRequester => requester}
val memberId = (body \ "memberId").as[UserId]
val prefs = (body \ "prefs").as[JsObject]
throwForbiddenIf(prefs.fields.length > 10, "TyE7ABKSG2", "Too many UI prefs fields")
anyWeirdJsObjField(prefs, maxLength = 10) foreach { problemMessage =>
throwBadRequest("TyE5AKBR024", s"Weird UI prefs: $problemMessage")
}
dao.saveUiPrefs(memberId, prefs, request.who)
Ok
}


def loadMembersCatsTagsSiteNotfPrefs(memberId: Int): Action[Unit] = GetAction { request =>
loadMembersCatsTagsSiteNotfPrefsImpl(memberId, request)
}
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/VoteController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ class VoteController @Inject()(cc: ControllerComponents, edContext: EdContext)

throwNoUnless(Authz.maySeePage(
pageMeta, requester,
dao.getGroupIds(requester),
dao.getGroupIdsOwnFirst(requester),
dao.getAnyPrivateGroupTalkMembers(pageMeta),
categoriesRootLast,
permissions = dao.getPermsOnPages(categoriesRootLast)),
Expand Down
119 changes: 70 additions & 49 deletions app/debiki/ReactJson.scala
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,8 @@ class JsonMaker(dao: SiteDao) {
"myDataByPageId" -> JsObject(Nil),
"marksByPostId" -> JsObject(Nil),
"closedHelpMessages" -> JsObject(Nil),
"tourTipsSeen" -> JsArray(),
"uiPrefsOwnFirst" -> JsArray(),
"permsOnPages" -> permsOnPagesToJson(everyonesPerms, excludeEveryone = false))
}

Expand All @@ -570,71 +572,82 @@ class JsonMaker(dao: SiteDao) {
def userDataJson(pageRequest: PageRequest[_], unapprovedPostAuthorIds: Set[UserId])
: Option[JsObject] = {
require(pageRequest.dao == dao, "TyE4GKVRY3")
val user = pageRequest.user getOrElse {
val requester = pageRequest.user getOrElse {
return None
}

val permissions = pageRequest.authzContext.permissions

var watchbar: BareWatchbar = dao.getOrCreateWatchbar(user.id)
var watchbar: BareWatchbar = dao.getOrCreateWatchbar(requester.id)
if (pageRequest.pageExists) {
// (See comment above about ought-to-rename this whole function / stuff.)
RACE // if the user opens a page, and someone adds her to a chat at the same time.
watchbar.tryAddRecentTopicMarkSeen(pageRequest.thePageMeta) match {
case None => // watchbar wasn't modified
case Some(modifiedWatchbar) =>
watchbar = modifiedWatchbar
dao.saveWatchbar(user.id, watchbar)
dao.pubSub.userWatchesPages(pageRequest.siteId, user.id, watchbar.watchedPageIds) ;RACE
dao.saveWatchbar(requester.id, watchbar)
dao.pubSub.userWatchesPages(pageRequest.siteId, requester.id, watchbar.watchedPageIds) ;RACE
}
}
val watchbarWithTitles = dao.fillInWatchbarTitlesEtc(watchbar)
val (restrCategories, restrTopics, users) = listRestrictedCategoriesAndTopics(pageRequest)
dao.readOnlyTransaction { transaction =>
Some(userDataJsonImpl(user, pageRequest.pageId, watchbarWithTitles, restrCategories,
restrictedTopics = restrTopics, restrictedTopicsUsers = users, permissions, unapprovedPostAuthorIds,
transaction))
val (restrCategories, restrTopics, restrTopicUsers) = listRestrictedCategoriesAndTopics(pageRequest)
val myGroupsEveryoneLast: Seq[Group] =
pageRequest.authzContext.groupIdsEveryoneLast map dao.getTheGroup

dao.readOnlyTransaction { tx =>
Some(requestersJsonImpl(requester, pageRequest.pageId, watchbarWithTitles, restrCategories,
restrictedTopics = restrTopics, restrictedTopicsUsers = restrTopicUsers, permissions,
unapprovedPostAuthorIds, myGroupsEveryoneLast, tx))
}
}


def userNoPageToJson(request: DebikiRequest[_]): JsValue = {
import request.authzContext
require(request.dao == dao, "TyE4JK5WS2")
val user = request.user getOrElse {
val requester = request.user getOrElse {
return JsNull
}
val permissions = authzContext.permissions
val watchbar = dao.getOrCreateWatchbar(user.id)
val watchbar = dao.getOrCreateWatchbar(requester.id)
val watchbarWithTitles = dao.fillInWatchbarTitlesEtc(watchbar)
val restrictedCategories = JsArray()
request.dao.readOnlyTransaction(userDataJsonImpl(user, anyPageId = None, watchbarWithTitles,
restrictedCategories, restrictedTopics = Nil, restrictedTopicsUsers = Nil,
permissions, unapprovedPostAuthorIds = Set.empty, _))
val myGroupsEveryoneLast: Seq[Group] =
request.authzContext.groupIdsEveryoneLast map dao.getTheGroup

dao.readOnlyTransaction { tx =>
requestersJsonImpl(requester, anyPageId = None, watchbarWithTitles,
restrictedCategories, restrictedTopics = Nil, restrictedTopicsUsers = Nil,
permissions, unapprovedPostAuthorIds = Set.empty, myGroupsEveryoneLast, tx)
}
}


private def userDataJsonImpl(user: Participant, anyPageId: Option[PageId],
private def requestersJsonImpl(requester: Participant, anyPageId: Option[PageId],
watchbar: WatchbarWithTitles, restrictedCategories: JsArray,
restrictedTopics: Seq[JsValue], restrictedTopicsUsers: Seq[JsObject],
permissions: Seq[PermsOnPages], unapprovedPostAuthorIds: Set[UserId],
tx: SiteTransaction): JsObject = {
myGroupsEveryoneLast: Seq[Group], tx: SiteTransaction): JsObject = {

// Bug: If !isAdmin, might count [review tasks one cannot see on the review page]. [5FSLW20]
val reviewTasksAndCounts =
if (user.isStaff) tx.loadReviewTaskCounts(user.isAdmin)
if (requester.isStaff) tx.loadReviewTaskCounts(requester.isAdmin)
else ReviewTaskCounts(0, 0)

// dupl line [8AKBR0]
val notfsAndCounts = loadNotifications(user.id, tx, unseenFirst = true, limit = 20)
val notfsAndCounts = loadNotifications(requester.id, tx, unseenFirst = true, limit = 20)

val ownIdAndGroupIds = tx.loadGroupIdsMemberIdFirst(user)
// Hmm not needed? Group ids already incl in myGroupsEveryoneLast above —
// if user = requester.
COULD_OPTIMIZE // use: requester.id +: myGroupsEveryoneLast.map(_.id) instead of db request.
val ownIdAndGroupIds = tx.loadGroupIdsMemberIdFirst(requester)

COULD_OPTIMIZE // could cache this, unless on the user's profile page (then want up-to-date info)?
// Related code: [6RBRQ204]
val ownCatsTagsSiteNotfPrefs = tx.loadNotfPrefsForMemberAboutCatsTagsSite(ownIdAndGroupIds)
val myCatsTagsSiteNotfPrefs = ownCatsTagsSiteNotfPrefs.filter(_.peopleId == user.id)
val groupsCatsTagsSiteNotfPrefs = ownCatsTagsSiteNotfPrefs.filter(_.peopleId != user.id)
val myCatsTagsSiteNotfPrefs = ownCatsTagsSiteNotfPrefs.filter(_.peopleId == requester.id)
val groupsCatsTagsSiteNotfPrefs = ownCatsTagsSiteNotfPrefs.filter(_.peopleId != requester.id)

val (pageNotfPrefs: Seq[PageNotfPref], votes, unapprovedPosts, unapprovedAuthors) =
anyPageId map { pageId =>
Expand All @@ -643,31 +656,38 @@ class JsonMaker(dao: SiteDao) {
SECURITY // minor: filter out prefs for cats one may not access... [7RKBGW02]
SECURITY // Ensure done when generating notfs.

val votes = votesJson(user.id, pageId, tx)
val votes = votesJson(requester.id, pageId, tx)
// + flags, interesting for staff, & so people won't attempt to flag twice [7KW20WY1]
val (postsJson, postAuthorsJson) =
unapprovedPostsAndAuthorsJson(user, pageId, unapprovedPostAuthorIds, tx)
unapprovedPostsAndAuthorsJson(requester, pageId, unapprovedPostAuthorIds, tx)

(pageNotfPrefs, votes, postsJson, postAuthorsJson)
} getOrElse (
Nil, JsEmptyObj, JsEmptyObj, JsArray())

val threatLevel = user match {
case member: User => member.threatLevel
val (threatLevel, tourTipsSeenJson, uiPrefsOwnFirstJsonSeq) = requester match {
case member: User =>
COULD_OPTIMIZE // load stats together with other user fields, in the same db request
val (requesterInclDetails, anyStats) = tx.loadTheUserInclDetailsAndStatsById(requester.id)

val tourTipsSeenJson: Seq[JsString] = anyStats flatMap { stats: UserStats =>
stats.tourTipsSeen.map((theTourTipsSeen: TourTipsSeen) =>
theTourTipsSeen.map(JsString): Seq[JsString])
} getOrElse Nil

val groupsUiPrefsJson: Seq[JsValue] = myGroupsEveryoneLast.flatMap(_.uiPrefs)
val ownUiPrefsJson = requesterInclDetails.uiPrefs getOrElse JsEmptyObj
val uiPrefsOwnFirstJsonSeq: Seq[JsValue] = ownUiPrefsJson +: groupsUiPrefsJson
(member.threatLevel,
tourTipsSeenJson,
uiPrefsOwnFirstJsonSeq)
case _ =>
COULD // load or get-from-cache IP bans ("blocks") for this guest and derive the
// correct threat level. However, for now, since this is for the browser only, this'll do:
ThreatLevel.HopefullySafe
(ThreatLevel.HopefullySafe, Nil, Nil)
}

COULD_OPTIMIZE // load stats together with other user fields, in the same db request
val anyStats = tx.loadUserStats(user.id)
val tourTipsSeenJson: Seq[JsString] = anyStats flatMap { stats: UserStats =>
stats.tourTipsSeen.map((theTourTipsSeen: TourTipsSeen) =>
theTourTipsSeen.map(JsString): Seq[JsString])
} getOrElse Nil

val anyReadingProgress = anyPageId.flatMap(tx.loadReadProgress(user.id, _))
val anyReadingProgress = anyPageId.flatMap(tx.loadReadProgress(requester.id, _))
val anyReadingProgressJson = anyReadingProgress.map(makeReadingProgressJson).getOrElse(JsNull)

val ownDataByPageId = anyPageId match {
Expand All @@ -676,8 +696,8 @@ class JsonMaker(dao: SiteDao) {
Json.obj(pageId ->
Json.obj( // MyPageData
"pageId" -> pageId,
"myPageNotfPref" -> pageNotfPrefs.find(_.peopleId == user.id).map(JsPageNotfPref),
"groupsPageNotfPrefs" -> pageNotfPrefs.filter(_.peopleId != user.id).map(JsPageNotfPref),
"myPageNotfPref" -> pageNotfPrefs.find(_.peopleId == requester.id).map(JsPageNotfPref),
"groupsPageNotfPrefs" -> pageNotfPrefs.filter(_.peopleId != requester.id).map(JsPageNotfPref),
"readingProgress" -> anyReadingProgressJson,
"votes" -> votes,
// later: "flags" -> JsArray(...) [7KW20WY1]
Expand All @@ -691,19 +711,19 @@ class JsonMaker(dao: SiteDao) {
// Somewhat dupl code, (2WB4G7) and [B28JG4].
var json = Json.obj(
"dbgSrc" -> "4JKW7A0",
"id" -> JsNumber(user.id),
"userId" -> JsNumber(user.id), // try to remove, use 'id' instead
"username" -> JsStringOrNull(user.anyUsername),
"fullName" -> JsStringOrNull(user.anyName),
"id" -> JsNumber(requester.id),
"userId" -> JsNumber(requester.id), // try to remove, use 'id' instead
"username" -> JsStringOrNull(requester.anyUsername),
"fullName" -> JsStringOrNull(requester.anyName),
"isLoggedIn" -> JsBoolean(true),
"isAdmin" -> JsBoolean(user.isAdmin),
"isModerator" -> JsBoolean(user.isModerator),
"isDeactivated" -> JsBoolean(user.isDeactivated),
"isDeleted" -> JsBoolean(user.isDeleted),
"avatarSmallHashPath" -> JsStringOrNull(user.smallAvatar.map(_.hashPath)),
"isEmailKnown" -> JsBoolean(user.email.nonEmpty),
"isAuthenticated" -> JsBoolean(user.isAuthenticated),
"trustLevel" -> JsNumber(user.effectiveTrustLevel.toInt),
"isAdmin" -> JsBoolean(requester.isAdmin),
"isModerator" -> JsBoolean(requester.isModerator),
"isDeactivated" -> JsBoolean(requester.isDeactivated),
"isDeleted" -> JsBoolean(requester.isDeleted),
"avatarSmallHashPath" -> JsStringOrNull(requester.smallAvatar.map(_.hashPath)),
"isEmailKnown" -> JsBoolean(requester.email.nonEmpty),
"isAuthenticated" -> JsBoolean(requester.isAuthenticated),
"trustLevel" -> JsNumber(requester.effectiveTrustLevel.toInt),
"threatLevel" -> JsNumber(threatLevel.toInt),

"numUrgentReviewTasks" -> reviewTasksAndCounts.numUrgent,
Expand All @@ -727,12 +747,13 @@ class JsonMaker(dao: SiteDao) {
"restrictedCategories" -> restrictedCategories,
"closedHelpMessages" -> JsObject(Nil),
"tourTipsSeen" -> tourTipsSeenJson,
"uiPrefsOwnFirst" -> JsArray(uiPrefsOwnFirstJsonSeq),
"myCatsTagsSiteNotfPrefs" -> JsArray(myCatsTagsSiteNotfPrefs.map(JsPageNotfPref)),
"groupsCatsTagsSiteNotfPrefs" -> JsArray(groupsCatsTagsSiteNotfPrefs.map(JsPageNotfPref)),
"myDataByPageId" -> ownDataByPageId,
"marksByPostId" -> JsObject(Nil))

if (user.isAdmin) {
if (requester.isAdmin) {
val siteSettings = tx.loadSiteSettings()
json += "isEmbeddedCommentsSite" -> JsBoolean(siteSettings.exists(_.allowEmbeddingFrom.nonEmpty))
}
Expand Down
2 changes: 1 addition & 1 deletion app/debiki/dao/CategoriesDao.scala
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ trait CategoriesDao {
val may = ed.server.auth.Authz.maySeePage(
page.meta,
user = authzCtx.requester,
groupIds = authzCtx.groupIds,
groupIds = authzCtx.groupIdsUserIdFirst,
pageMembers = getAnyPrivateGroupTalkMembers(page.meta),
categoriesRootLast = categories,
permissions = authzCtx.permissions,
Expand Down
Loading

0 comments on commit fe08383

Please sign in to comment.