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:
-
~ 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. ~ 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 a "KeyError", which ~ crashed the page. ~ before it actually had been given one. This raised a "KeyError", ~ 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 added some .get() checks, so that if the widget position is not in
~ the dictionary, a None value is returned. - Testing Done:
-
~ I ran the admin/tests.py file using ./tests/runtests.py reviewboard.admin.tests
~ I ran the admin/tests.py file using
+ ./tests/runtests.py reviewboard.admin.tests
~ I also used the JS-tests on /js-tests/, and there were no (new) issues.
~ I also used the JS-tests on /js-tests/, and there were no
+ (new) issues. ~ I did a thorough test by adding my patch onto a branch off release-4.0.x
~ that created a new widget. Ensuring I initially loaded the dashboard with ~ release-4.0.x, and then reloaded the dashboard after applying my patch on ~ the new widget branch, the page was able to sucessfully load the new widget. ~ I did a thorough test by adding my patch onto a branch off
~ release-4.0.x that created a new widget. Ensuring I initially ~ loaded the dashboard with release-4.0.x, and then reloaded the ~ dashboard after applying my patch on the new widget branch, the + page was able to sucessfully load the new widget.
-
-
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>"
-
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:
-
I ran the admin/tests.py file using
./tests/runtests.py reviewboard.admin.tests
~ I also used the JS-tests on /js-tests/, and there were no
~ I also used the JS-tests on /js-tests/, and there were no issues.
- (new) issues. ~ I did a thorough test by adding my patch onto a branch off
~ release-4.0.x that created a new widget. Ensuring I initially ~ loaded the dashboard with release-4.0.x, and then reloaded the ~ dashboard after applying my patch on the new widget branch, the ~ 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 widget on the dashboard ~ does not raise a KeyError, and gives the widget a position. - page was able to sucessfully load the new widget. - Commit:
-
4610e71104cbf7970e4ae6438169faeb76f130a7dabda4a3a6bae541c3fb00db9066d47b86a244db
Checks run (1 failed, 1 succeeded)
flake8
- Commit:
-
dabda4a3a6bae541c3fb00db9066d47b86a244db165d9058b2dbea04b15a06a0946288b67a359eb7
Checks run (1 failed, 1 succeeded)
flake8
- Change Summary:
-
Fix Review Bot issues
- Commit:
-
165d9058b2dbea04b15a06a0946288b67a359eb73c53d2d3b63322d66ca4ebba09a19513f9457f03
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. -
-
We generally avoid
\\
where we can:total_initial_widgets = ( len(response.context['selected_secondary_widgets']) + len(response.context['selected_primary_widgets']))
-
You can wrap this without the
\\
:profile = Profile.objects.get_or_create( user=response.context['user'])[0]
-
-
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. -
-
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'
-
-
-
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. -
-
As per my comment above, this should actually be:
self.assertNotIn(TestSecondaryWidget, response.context['selected_secondary_widgets'])
-
-
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. -
-
-
-
-
-
-
- Change Summary:
-
Refactor admin dashboard widget code, and update code style
- Description:
-
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 a "KeyError", ~ before it actually had been given one. This raised a KeyError
,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 the dashboard()
function inadmin/views.py
+ is much easier to read. - Testing Done:
-
~ I ran the admin/tests.py file using
~ ./tests/runtests.py reviewboard.admin.tests
~ 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 widget on the dashboard ~ does not raise a KeyError, and gives the widget a position. ~ 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. - Commit:
-
3c53d2d3b63322d66ca4ebba09a19513f9457f0355b376e3b2314f590a047846c77e759a6463e9ad
- Diff:
-
Revision 5 (+168 -44)
Checks run (1 failed, 1 succeeded)
flake8
- Change Summary:
-
Fix Review Bot issues
- Commit:
-
55b376e3b2314f590a047846c77e759a6463e9ad36217cc5b8103a2bd3c2865f4e9c950b213d6234
- Diff:
-
Revision 6 (+167 -44)
Checks run (2 succeeded)
-
-
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. -
-
-
-
-
How about:
A dictionary mapping widgets (:py:class:`~reviewboard.admin.widgets. Widget`) to whether or not they are selected (as a :py:class:`unicode`).
-
The summary should fit entirely on the first line.
How about
Return widgets sorted based on their positions.
-
-
- Change Summary:
-
Update documentation and style, based on Barret's suggestions.
- Commit:
-
36217cc5b8103a2bd3c2865f4e9c950b213d623491fbdbee9a4bd7fd4b62c700087d85d82047dc71
- Diff:
-
Revision 7 (+173 -44)
Checks run (2 succeeded)
- Change Summary:
-
Fix documentation issues based on Barret's suggestions.
- Commit:
-
91fbdbee9a4bd7fd4b62c700087d85d82047dc717b5cf3181ed1839079a697e243e7def9cc074f26
- Diff:
-
Revision 8 (+176 -44)
Checks run (2 succeeded)
-
-
-
-
-
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"?
-
-
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:
-
7b5cf3181ed1839079a697e243e7def9cc074f2661483f7edfe1b4cf90a95f6d77fd2dad2f58a6f9
- Diff:
-
Revision 9 (+173 -44)