Fix rendering new widgets in admin dashboard
Review Request #10165 — Created Sept. 22, 2018 and submitted
The dashboard on /admin/ crashed when a brand new widget was added
to the page. The expected output is that the new widget, whether
it's a primary or secondary widget, should appear on the dashboard
with the other widgets.This was because there was a check to find the new widgets position,
before it actually had been given one. This raised aKeyError
,
which crashed the page.I added some .get() checks, so that if the widget position is not in
the dictionary, a None value is returned.I also refactored code on how new widgets are rendered in the
dashboard, so that thedashboard()
function inadmin/views.py
is much easier to read.
I ran all tests using
./tests/runtests.py
and no errors resulted.I also used the JS-tests on /js-tests/, and there were no issues.
I created a Unit Test class named WidgetTests, that can be run
with./tests/runtests.py reviewboard.admin.tests:WidgetTests
.
This test checks that adding a new primary widget and a new secondary
widget on the dashboard does not raise a KeyError, and gives the two
widgets a position.
Description | From | Last Updated |
---|---|---|
Can you rewrite your summary in the imperitive mood, i.e., as if it were a command or order? The following … |
brennie | |
This is a good opportunity to get comfortable with unit tests. You'll need to write a unit test that sets … |
chipx86 | |
Please run all tests, e.g., ./tests/runtests.py. They take a few minutes, but sometimes things cascade into other suites and break … |
brennie | |
In your description: `KeyError` over "KeyError" |
brennie | |
dict.get accepts a default argument if the key is not present so this can be rewritten as: lambda y: primary_widget_positions.get(y.widget_id, … |
brennie | |
Same as above. |
brennie | |
E127 continuation line over-indented for visual indent |
reviewbot | |
E127 continuation line over-indented for visual indent |
reviewbot | |
E127 continuation line over-indented for visual indent |
reviewbot | |
E127 continuation line over-indented for visual indent |
reviewbot | |
E211 whitespace before '[' |
reviewbot | |
E211 whitespace before '[' |
reviewbot | |
E251 unexpected spaces around keyword / parameter equals |
reviewbot | |
E251 unexpected spaces around keyword / parameter equals |
reviewbot | |
E251 unexpected spaces around keyword / parameter equals |
reviewbot | |
E251 unexpected spaces around keyword / parameter equals |
reviewbot | |
E128 continuation line under-indented for visual indent |
reviewbot | |
E128 continuation line under-indented for visual indent |
reviewbot | |
E128 continuation line under-indented for visual indent |
reviewbot | |
E128 continuation line under-indented for visual indent |
reviewbot | |
E211 whitespace before '[' |
reviewbot | |
E211 whitespace before '[' |
reviewbot | |
We generally avoid \\ where we can: total_initial_widgets = ( len(response.context['selected_secondary_widgets']) + len(response.context['selected_primary_widgets'])) |
brennie | |
You can wrap this without the \\: profile = Profile.objects.get_or_create( user=response.context['user'])[0] |
brennie | |
blank line between these. |
brennie | |
You are registering widgets outside of the try..finally. Lets do it this way. try: register_admin_widget(TestPrimaryWidget) register_admin_widget(TestSecondaryWIdget) # lines 298 thru … |
brennie | |
blank line between these. |
brennie | |
We can pull out the selections into a intermediate variables to make things easier to read. primary_selections = profile.extra_data['primary_widget_selections'] secondary_selections … |
brennie | |
Again, we can wrap this without \ as above. |
brennie | |
Blank line between expression and block statement. |
brennie | |
This should actually be: self.assertNotIn(TestPrimaryWidget, response.context['selected_primary_widgets']) Otherwise the error message we would get when running tests (True is not False) … |
brennie | |
Blank line between block statements. |
brennie | |
As per my comment above, this should actually be: self.assertNotIn(TestSecondaryWidget, response.context['selected_secondary_widgets']) |
brennie | |
While you're here, blank line between these. |
brennie | |
Since we're doing the same operations essentially twice with different variables, maybe we should extract this into two functions so … |
brennie | |
Lets use widget over p here. |
brennie | |
While you're here, blank line between these. |
brennie | |
Lets use widget over s here. |
brennie | |
While you're here, blank line between these. |
brennie | |
This shouldn't need the parentheses around the lambda, should it? |
brennie | |
While you're here, blank line between these. |
brennie | |
This shouldn't need the parentheses around the lambda, should it? |
brennie | |
E303 too many blank lines (2) |
reviewbot | |
E501 line too long (81 > 79 characters) |
reviewbot | |
E501 line too long (82 > 79 characters) |
reviewbot | |
E128 continuation line under-indented for visual indent |
reviewbot | |
E128 continuation line under-indented for visual indent |
reviewbot | |
E128 continuation line under-indented for visual indent |
reviewbot | |
E128 continuation line under-indented for visual indent |
reviewbot | |
Our imports should be alphabetically sorted so this (a) should go after the reviewboard.admin.validation imports and (b) needs its import( … |
brennie | |
Comments should be full sentences and end in periods. |
brennie | |
In this case just use \ to wrap to the next line since it saves a vertical line. |
brennie | |
In this case just use \ to wrap to the next line since it saves a vertical line. |
brennie | |
Missing Returns: |
brennie | |
How about: A dictionary mapping widgets (:py:class:`~reviewboard.admin.widgets. Widget`) to whether or not they are selected (as a :py:class:`unicode`). |
brennie | |
The summary should fit entirely on the first line. How about Return widgets sorted based on their positions. |
brennie | |
How about: A dictionary mapping widget IDs to their ordinals. |
brennie | |
Missing Returns: |
brennie | |
Uppercase before lowercase, so this would be the first import. |
brennie | |
Single quotes. |
brennie | |
Single quotes. |
brennie | |
Single quotes. |
brennie | |
Single quotes. |
brennie | |
Can you add a comment here explaining why we're doing this? We both know why we wrap these in try..except … |
brennie | |
Should be "admin/views.py" |
david | |
This would be a little easier as: profile = response.context['user'].get_profile() |
david | |
This line can go away (the definition of a try block is to wrap something in case it fails). |
david | |
This is a little bit too verbose (I can read the words "try", "pass", and "KeyError" in the code). How … |
david | |
Add a blank line here, please. |
david | |
We usually write the type as tuple, like this: ``` Returns: tuple of list: A 2-tuple containing a list of … |
david |
Change Summary:
Reduced line columns to less than 72
Description: |
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Testing Done: |
|
-
-
Can you rewrite your summary in the imperitive mood, i.e., as if it were a command or order?
The following sentence should make sense if you substitute in your summary:
"This patch will <summary>"
-
reviewboard/admin/views.py (Diff revision 1) dict.get
accepts a default argument if the key is not present so this can be rewritten as:lambda y: primary_widget_positions.get(y.widget_id, len(primary_widget_positions)
Also can we rename
y
to something likewidget
?y
is not a good variable name. -
-
-
This is a good opportunity to get comfortable with unit tests.
You'll need to write a unit test that sets up some state in the profile for the widget positions, performs a HTTP GET to the admin UI dashboard, and then checks the resulting template context to see if your fixes did what you'd expect them to do.
This will be done with Django's
TestClient
, which works like:def test_blah(self): ... response = self.client.get(reverse('admin-dashboard'), ...) self.assertEqual(response.context['key'], expected_value)
Change Summary:
Add unit test to check rendering new widget doesn't raise KeyError
Testing Done: |
|
||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Commit: |
|
||||||||||||||||||||||||||||||||||||||||||||||||
Diff: |
Revision 2 (+100 -6) |
Checks run (1 failed, 1 succeeded)
flake8
-
-
-
-
-
-
-
reviewboard/admin/views.py (Diff revision 2) E251 unexpected spaces around keyword / parameter equals
-
reviewboard/admin/views.py (Diff revision 2) E251 unexpected spaces around keyword / parameter equals
-
reviewboard/admin/views.py (Diff revision 2) E251 unexpected spaces around keyword / parameter equals
-
reviewboard/admin/views.py (Diff revision 2) E251 unexpected spaces around keyword / parameter equals
Commit: |
|
||||
---|---|---|---|---|---|
Diff: |
Revision 3 (+100 -6) |
Checks run (1 failed, 1 succeeded)
flake8
-
reviewboard/admin/tests.py (Diff revision 3) E128 continuation line under-indented for visual indent
-
reviewboard/admin/tests.py (Diff revision 3) E128 continuation line under-indented for visual indent
-
reviewboard/admin/tests.py (Diff revision 3) E128 continuation line under-indented for visual indent
-
reviewboard/admin/tests.py (Diff revision 3) E128 continuation line under-indented for visual indent
-
-
Change Summary:
Fix Review Bot issues
Commit: |
|
||||
---|---|---|---|---|---|
Diff: |
Revision 4 (+100 -6) |
Checks run (2 succeeded)
-
-
Please run all tests, e.g.,
./tests/runtests.py
. They take a few minutes, but sometimes things cascade into other suites and break things. -
-
reviewboard/admin/tests.py (Diff revision 4) We generally avoid
\\
where we can:total_initial_widgets = ( len(response.context['selected_secondary_widgets']) + len(response.context['selected_primary_widgets']))
-
reviewboard/admin/tests.py (Diff revision 4) You can wrap this without the
\\
:profile = Profile.objects.get_or_create( user=response.context['user'])[0]
-
-
reviewboard/admin/tests.py (Diff revision 4) You are registering widgets outside of the
try..finally
.Lets do it this way.
try: register_admin_widget(TestPrimaryWidget) register_admin_widget(TestSecondaryWIdget) # lines 298 thru 321 without the try/finally. finally: try: unregister_admin_widget(TestPrimaryWidget) except KeyError: pass try: unregister_admin_widget(TestSecondaryWidget) except KeyError: pass
We need the nested
try
in thefinally
clause because soemthing could go wrong with registering either of the widgets and we would fall into thefinally
clause. -
-
reviewboard/admin/tests.py (Diff revision 4) We can pull out the selections into a intermediate variables to make things easier to read.
primary_selections = profile.extra_data['primary_widget_selections'] secondary_selections = profile.extra_data['secondaary_widget_selections'] primary_selections[TestPrimaryWidget.widget_id] = '1' secondary_selections[TestSecondaryWidget.widget_id] = '1'
-
-
-
reviewboard/admin/tests.py (Diff revision 4) This should actually be:
self.assertNotIn(TestPrimaryWidget, response.context['selected_primary_widgets'])
Otherwise the error message we would get when running tests (
True is not False
) is not helpful. This way, it willrepr()
print the contents ofresponse.context['selected_primary_widgets']
to aid in debugging. -
-
reviewboard/admin/tests.py (Diff revision 4) As per my comment above, this should actually be:
self.assertNotIn(TestSecondaryWidget, response.context['selected_secondary_widgets'])
-
-
reviewboard/admin/views.py (Diff revision 4) Since we're doing the same operations essentially twice with different variables, maybe we should extract this into two functions so that we could do:
selected_primary_widgets, unselected_primary_widgets = _get_widget_selections( primary, profile_data.get('primary_widget_selections')) sorted_primary_widgets = _sort_widgets( selected_primary_widgets, profile_data.get('primary_widget_positions')) selected_secondary_widgets, unselected_secondary_widgets = _get_widget_selections( secondary_widgets, profile_data.get('secondary_widget_selections')) sorted_secondary_widgets = _sort_widgets( selected_secondary_widgets, profile_data.get('secondary_widget_positions'))
where
_get_widget_selections
and_sort_widgets
are new functions that encapsulate the current behaviour. This refactor would go a long way towards shortening the function bodies, encapsulating repeated logic, and making this code more readable. -
-
-
-
-
reviewboard/admin/views.py (Diff revision 4) This shouldn't need the parentheses around the lambda, should it?
-
-
reviewboard/admin/views.py (Diff revision 4) This shouldn't need the parentheses around the lambda, should it?
Change Summary:
Refactor admin dashboard widget code, and update code style
Description: |
|
||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Testing Done: |
|
||||||||||||||||||||||||||||||||||||||||||||||||
Commit: |
|
||||||||||||||||||||||||||||||||||||||||||||||||
Diff: |
Revision 5 (+168 -44) |
Checks run (1 failed, 1 succeeded)
flake8
-
-
-
-
reviewboard/admin/tests.py (Diff revision 5) E128 continuation line under-indented for visual indent
-
reviewboard/admin/tests.py (Diff revision 5) E128 continuation line under-indented for visual indent
-
reviewboard/admin/views.py (Diff revision 5) E128 continuation line under-indented for visual indent
-
reviewboard/admin/views.py (Diff revision 5) E128 continuation line under-indented for visual indent
Change Summary:
Fix Review Bot issues
Commit: |
|
||||
---|---|---|---|---|---|
Diff: |
Revision 6 (+167 -44) |
Checks run (2 succeeded)
-
-
reviewboard/admin/tests.py (Diff revision 6) Our imports should be alphabetically sorted so this (a) should go after the
reviewboard.admin.validation
imports and (b) needs itsimport( ... )
list to be sorted. -
-
reviewboard/admin/views.py (Diff revision 6) In this case just use
\
to wrap to the next line since it saves a vertical line. -
reviewboard/admin/views.py (Diff revision 6) In this case just use
\
to wrap to the next line since it saves a vertical line. -
-
reviewboard/admin/views.py (Diff revision 6) How about:
A dictionary mapping widgets (:py:class:`~reviewboard.admin.widgets. Widget`) to whether or not they are selected (as a :py:class:`unicode`).
-
reviewboard/admin/views.py (Diff revision 6) The summary should fit entirely on the first line.
How about
Return widgets sorted based on their positions.
-
reviewboard/admin/views.py (Diff revision 6) How about:
A dictionary mapping widget IDs to their ordinals.
-
Change Summary:
Update documentation and style, based on Barret's suggestions.
Commit: |
|
||||
---|---|---|---|---|---|
Diff: |
Revision 7 (+173 -44) |
Checks run (2 succeeded)
-
After these tiny nits, I'm happy with this landing!
-
reviewboard/admin/tests.py (Diff revision 7) Uppercase before lowercase, so this would be the first import.
-
-
-
-
-
reviewboard/admin/tests.py (Diff revision 7) Can you add a comment here explaining why we're doing this? We both know why we wrap these in
try..except
and ignore the error, but future readers of this file might not grok it immediately.
Change Summary:
Fix documentation issues based on Barret's suggestions.
Commit: |
|
||||
---|---|---|---|---|---|
Diff: |
Revision 8 (+176 -44) |
Checks run (2 succeeded)
-
-
-
reviewboard/admin/tests.py (Diff revision 8) This would be a little easier as:
profile = response.context['user'].get_profile()
-
reviewboard/admin/tests.py (Diff revision 8) This line can go away (the definition of a try block is to wrap something in case it fails).
-
reviewboard/admin/tests.py (Diff revision 8) This is a little bit too verbose (I can read the words "try", "pass", and "KeyError" in the code).
How about "If an error was encountered above, the widgets will not be registered. Ignore any errors here in that case"?
-
-
reviewboard/admin/views.py (Diff revision 8) We usually write the type as tuple, like this:
```
Returns:
tuple of list:
A 2-tuple containing a list of selected widgets (to display
on the dashboard) and a list of the unselected widgets.
"""
Change Summary:
Based on David's suggestions, updated some comments and changed how we get the user profile, in
test_new_widget_render
Commit: |
|
||||
---|---|---|---|---|---|
Diff: |
Revision 9 (+173 -44) |