Dramatically improve the experience for admins with other users' drafts.
Review Request #14020 — Created July 10, 2024 and submitted
For a very long time, we've had an annoying situation with privileged
users and drafts. Users who are admins or have the
can_edit_reviewrequest
privilege have permission to create or edit
drafts on other people's review requests, but we had some
inconsistencies that made the experience less than desirable:
- The vast majority of the time, users with privileges really just want
to operate as if they were a normal user doing reviews. Seeing the
draft data was annoying in this case. - Loading the diff attached to a draft would fail. We'd happily send the
user the diff context, but the diff fragment view was not properly
loading the draft, and would return 404s. - It was not clear at all that the admin user was seeing draft data.
Even as someone who has been using Review Board for all of its
existence, I frequently got confused.
This change makes a huge improvement to the user experience here. When
loading a review request, privileged users will see the published data
rather than the draft, but the unified banner will have a note that
there's an unpublished draft, with a link to reload the page with a new
?view-draft=1
parameter. When viewing the draft, that note tells them
that they're viewing a draft on a review request owned by another user,
and gives them a link to go back to the public data.
In cases where there is no public data to go back to (for example,
viewing a review request which has never been published, or a file
attachment which only exists on the draft), the notice is shown to the
user but there's no link in the banner.
This ends up adding three new pieces of data to the reviewable page
context:
user_draft_exists
will beTrue
if the review request is owned by
another user but there exists a draft which is accessible by the
person viewing the page.viewing_user_draft
will beTrue
if the person viewing the page is
currently viewing data which is contained in the draft. This will
happen either if the current data is only available in the draft (such
as an unpublished review request) or if they explicitly wanted to see
it by including?view-draft=1
in the URL.force_view_user_draft
will beTrue
if the current page is only
available in draft form, and we want to suppress the link to go back
to published data.
While adding those to the context, I've also moved more things into the
make_review_request_context
method in order to be more consistent
about how stuff ends up in the ReviewRequestContext
dict, since we
were repeating ourselves a fair bit in each of the views that calls it.
There's one major piece of implementation left to do. This does not yet
change anything about the admin user actually making changes to the
review request/draft data. Now that the admin user will see non-draft
data by default, we will need some logic about what to do when they
start making changes, especially in the case where there's already a
draft present. This will be done in a separate change because this one
is already way too big.
There are also a couple of bugs I've discovered while implementing this,
which will be fixed in their own changes.
- Ran Python unit tests.
- Ran JS unit tests.
- Created review requests with various draft states and checked access
from the owner user, an admin user, and a regular user. These draft
states include unpublished review requests that include both diffs and
file attachments, and draft updates to review requests that include
new revisions of diffs, new file attachments, or new revisions of
public file attachments.
Summary | ID |
---|---|
7068b1886f96b2e8cb850c810be9b3dbc49fc3d8 |
Description | From | Last Updated |
---|---|---|
I feel like a different banner colour here would be nicer, make it stand out more. Maybe ink-p-accent-info-bg or ink-p-accent-highlight-bg? |
![]() |
|
Can you update this bottom border colour to match the new banner colour (--ink-p-accent-info-border-color) |
![]() |
|
continuation line unaligned for hanging indent Column: 5 Error code: E131 |
![]() |
|
continuation line unaligned for hanging indent Column: 5 Error code: E131 |
![]() |
|
continuation line unaligned for hanging indent Column: 5 Error code: E131 |
![]() |
|
line too long (80 > 79 characters) Column: 80 Error code: E501 |
![]() |
|
line too long (80 > 79 characters) Column: 80 Error code: E501 |
![]() |
|
line too long (83 > 79 characters) Column: 80 Error code: E501 |
![]() |
|
continuation line missing indentation or outdented Column: 13 Error code: E122 |
![]() |
|
continuation line missing indentation or outdented Column: 13 Error code: E122 |
![]() |
|
continuation line missing indentation or outdented Column: 13 Error code: E122 |
![]() |
|
continuation line missing indentation or outdented Column: 13 Error code: E122 |
![]() |
|
continuation line missing indentation or outdented Column: 13 Error code: E122 |
![]() |
|
continuation line missing indentation or outdented Column: 13 Error code: E122 |
![]() |
|
continuation line missing indentation or outdented Column: 13 Error code: E122 |
![]() |
|
continuation line missing indentation or outdented Column: 13 Error code: E122 |
![]() |
|
continuation line missing indentation or outdented Column: 13 Error code: E122 |
![]() |
|
continuation line missing indentation or outdented Column: 13 Error code: E122 |
![]() |
|
continuation line missing indentation or outdented Column: 13 Error code: E122 |
![]() |
|
continuation line missing indentation or outdented Column: 13 Error code: E122 |
![]() |
|
continuation line missing indentation or outdented Column: 13 Error code: E122 |
![]() |
|
continuation line missing indentation or outdented Column: 13 Error code: E122 |
![]() |
|
continuation line missing indentation or outdented Column: 13 Error code: E122 |
![]() |
|
continuation line missing indentation or outdented Column: 13 Error code: E122 |
![]() |
|
continuation line missing indentation or outdented Column: 13 Error code: E122 |
![]() |
|
continuation line missing indentation or outdented Column: 13 Error code: E122 |
![]() |
|
continuation line missing indentation or outdented Column: 13 Error code: E122 |
![]() |
|
continuation line missing indentation or outdented Column: 13 Error code: E122 |
![]() |
|
continuation line missing indentation or outdented Column: 13 Error code: E122 |
![]() |
|
continuation line missing indentation or outdented Column: 13 Error code: E122 |
![]() |
|
continuation line missing indentation or outdented Column: 13 Error code: E122 |
![]() |
|
continuation line missing indentation or outdented Column: 13 Error code: E122 |
![]() |
|
line too long (86 > 79 characters) Column: 80 Error code: E501 |
![]() |
|
line too long (80 > 79 characters) Column: 80 Error code: E501 |
![]() |
|
missing whitespace around operator Column: 30 Error code: E225 |
![]() |
|
undefined name 'Diffset' Column: 33 Error code: F821 |
![]() |
|
Should we also do an admin check here? I know that we do checks later so that when a regular … |
![]() |
|
To reduce the number of lines and code repetition, you could turn this into a function that takes context, draft, … |
![]() |
|
For TypedDicts elsewhere, we've been making sure new fields have the Version Added directly. |
|
|
I don't see anything replacing this variable. |
|
|
These need to be explicitly typed as bool or they can end up as Literal[False]. |
|
|
Can we use keyword arguments for this? |
|
|
Can we make these keyword-only? |
|
|
Just a suggestion, but maybe move all of these inside an if draft, so that there's only one False code … |
|
|
We check this in a couple of places. It might be nice to have a function somewhere that just wraps … |
|
|
Version Added? |
|
|
Version Added? |
|
|
To keep tests speedy, let's fetch these at once. We can do: users = User.objects.in_bulk( ['doc', 'grumpy', 'admin', 'dopey'], field_name='username') … |
|
|
Since this is a private method, it should go after the public methods. |
|
|
""" should be on the next line. Same with the other tests below. |
|
|
Rather than the cast every time, maybe the util function should just take a dictionary and then check for the … |
|
|
Needs a Version Added. |
|
|
Needs Version Added. |
|
|
Needs Version Added. |
|
|
Needs a Version Added. |
|
|
We have model locally, so we can use that. |
|
|
line too long (81 > 79 characters) Column: 80 Error code: E501 |
![]() |
- Commits:
-
Summary ID 355e48fae8a2c941e5812d2f1d1c786b9e340a81 b316920b0740112d1aef2b697cd63c0105bbcfa1 - Diff:
-
Revision 2 (+3808 -368)
Checks run (1 failed, 1 succeeded)
flake8
- Commits:
-
Summary ID b316920b0740112d1aef2b697cd63c0105bbcfa1 f10cd90108c460a8877bdb79683084e881f76f76 - Diff:
-
Revision 3 (+3812 -368)
Checks run (2 succeeded)
![maubin](https://media-reviews-reviewboard-org.s3.amazonaws.com/uploaded/avatars/m/ma/maubin__28892f51-5581-4db1-b65d-322d3b6117de.jpg?AWSAccessKeyId=AKIAQF2RGG6MLO3L2GME&Signature=utI8cfrx%2BboM7Tu0xfpZkQhf3kY%3D&Expires=1729223559)
-
-
I feel like a different banner colour here would be nicer, make it stand out more. Maybe
ink-p-accent-info-bg
orink-p-accent-highlight-bg
? -
Should we also do an admin check here? I know that we do checks later so that when a regular user actually tries to view someone's draft, they can't. But if a caller were to use this function and assume these were the only checks to be done, it would lead to the problem where regular users can see everyone's drafts. Either we do an admin check here too, or say in the docs that this does not check for whether the user has admin permissions.
-
To reduce the number of lines and code repetition, you could turn this into a function that takes
context
,draft
,mutable_by_user
,force_view_user_draft
, and etc as arguments. Can default all of the booleans toFalse
and then for each test pass inTrue
when needed.I've done this in the past and I know its a bit tedious to switch everything over :p but makes for much cleaner code, and if you ever have to add any new assertions, you only need to do it in one spot instead of having to repeat it for each unit test.
- Commits:
-
Summary ID f10cd90108c460a8877bdb79683084e881f76f76 3c487ff7f6af3f514a4ef2b6866eea1119fcc75f - Diff:
-
Revision 4 (+3812 -368)
Checks run (2 succeeded)
- Commits:
-
Summary ID 3c487ff7f6af3f514a4ef2b6866eea1119fcc75f 04149ed84208e48038ad2b8ad49291a667ca6bd8 - Diff:
-
Revision 5 (+3938 -370)
Checks run (2 succeeded)
![maubin](https://media-reviews-reviewboard-org.s3.amazonaws.com/uploaded/avatars/m/ma/maubin__28892f51-5581-4db1-b65d-322d3b6117de.jpg?AWSAccessKeyId=AKIAQF2RGG6MLO3L2GME&Signature=utI8cfrx%2BboM7Tu0xfpZkQhf3kY%3D&Expires=1729223559)
-
I still wish that we could fix the bottom border of the banner to match the new colour somehow, but other than that looks good.
Maybe in a separate change we could add some flag to the unified banner whenever this unpublished draft banner is present, and use that to change the bottom border colour?
-
-
-
-
-
-
-
Just a suggestion, but maybe move all of these inside an
if draft
, so that there's only oneFalse
code path? -
We check this in a couple of places. It might be nice to have a function somewhere that just wraps this, so the logic never differs anywhere.
-
-
-
To keep tests speedy, let's fetch these at once.
We can do:
users = User.objects.in_bulk( ['doc', 'grumpy', 'admin', 'dopey'], field_name='username')
And then either just use that or we can assign each via a lookup in the dictionary.
-
-
-
Rather than the cast every time, maybe the util function should just take a dictionary and then check for the appropriate keys.
-
-
-
-
-
- Commits:
-
Summary ID 04149ed84208e48038ad2b8ad49291a667ca6bd8 64d2e05d92ee127610054c95cee76fd839671f52 - Diff:
-
Revision 6 (+4148 -376)
- Commits:
-
Summary ID 64d2e05d92ee127610054c95cee76fd839671f52 7068b1886f96b2e8cb850c810be9b3dbc49fc3d8 - Diff:
-
Revision 7 (+4150 -376)