Add sortable columns to the dashboard and review request lists
Updated 1 year, 6 months ago
| Christian Hammond | Reviewers | ||
| trunk | reviewboard | ||
| None | Review Board SVN | ||
Added support for sorting review requests in the dashboard and various lists (submitters, groups, all). The columns are now clickable and will cause the list to sort. Up to three columns can be taken into consideration for sorting at any given point. We update the columns when clicked as follows: * If the column is not in the existing sort list, it becomes the primary sort column and we display a large down arrow. * If the column is in the existing sort list and is the primary sort column, we reverse the order of the column and display a large up arrow. * If the column is in the sort list but is not the primary sort column, we make it the primary sort column, and we don't reverse the order. We save a cookie every time the order changes and use that next time unless a ?sort= parameter is set.
Sorted various columns and messed with each case above. Appears to work correctly.
Diff revision 5 (Latest)
- /trunk/reviewboard/urls.py: 10 changes [ 1 2 3 4 5 6 7 8 9 10 ]
- /trunk/reviewboard/accounts/models.py: 1 change [ 1 ]
- /trunk/reviewboard/htdocs/css/common.css: 2 changes [ 1 2 ]
- /trunk/reviewboard/reviews/views.py: 22 changes [ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 ]
- /trunk/reviewboard/templates/reviews/dashboard.html: 1 change [ 1 ]
- /trunk/reviewboard/templates/reviews/review_list.html: 1 change [ 1 ]
- /trunk/reviewboard/utils/views.py: 1 change [ new content ]
- /trunk/reviewboard/utils/templatetags/htmlutils.py: 5 changes [ 1 2 3 4 5 ]
| /trunk/reviewboard/urls.py | |||
|---|---|---|---|
| Revision 768 | New Change | ||
| ... | 26 lines hidden [Expand] | ||
| 27 | 27 | ||
| 28 | (r'^$', 'django.views.generic.simple.redirect_to', |
28 | (r'^$', 'django.views.generic.simple.redirect_to', |
| 29 | {'url': '/dashboard/'}), |
29 | {'url': '/dashboard/'}), |
| 30 | 30 | ||
| 31 | # Review request browsing |
31 | # Review request browsing |
| 32 | (r'^dashboard/$', 'reviewboard.reviews.views.dashboard', |
32 | (r'^dashboard/$', 'reviewboard.reviews.views.dashboard'), |
| 33 | {'template_name': 'reviews/dashboard.html'}), |
||
| 34 | 33 | ||
| 35 | (r'^r/$', 'reviewboard.reviews.views.all_review_requests', |
34 | (r'^r/$', 'reviewboard.reviews.views.all_review_requests'), |
| 36 | {'template_name': 'reviews/review_list.html'}), |
||
| 37 | 35 | ||
| 38 | # Review request creation |
36 | # Review request creation |
| 39 | (r'^r/new/$', 'reviewboard.reviews.views.new_review_request'), |
37 | (r'^r/new/$', 'reviewboard.reviews.views.new_review_request'), |
| 40 | 38 | ||
| 41 | # Review request detail |
39 | # Review request detail |
| ... | 53 lines hidden [Expand] | ||
| 95 | 'reviewboard.reviews.views.preview_review_email'), |
93 | 'reviewboard.reviews.views.preview_review_email'), |
| 96 | (r'^r/(?P<review_request_id>[0-9]+)/reviews/(?P<review_id>[0-9]+)/replies/(?P<reply_id>[0-9]+)/preview-email/$', |
94 | (r'^r/(?P<review_request_id>[0-9]+)/reviews/(?P<review_id>[0-9]+)/replies/(?P<reply_id>[0-9]+)/preview-email/$', |
| 97 | 'reviewboard.reviews.views.preview_reply_email'), |
95 | 'reviewboard.reviews.views.preview_reply_email'), |
| 98 | 96 | ||
| 99 | # Users |
97 | # Users |
| 100 | (r'^users/$', 'reviewboard.reviews.views.submitter_list', |
98 | (r'^users/$', 'reviewboard.reviews.views.submitter_list'), |
| 101 | {'template_name': 'reviews/submitter_list.html'}), |
||
| 102 | |||
| 103 | (r'^users/(?P<username>[A-Za-z0-9_-]+)/$', |
99 | (r'^users/(?P<username>[A-Za-z0-9_-]+)/$', |
| 104 | 'reviewboard.reviews.views.submitter', |
100 | 'reviewboard.reviews.views.submitter'), |
| 105 | {'template_name': 'reviews/review_list.html'}), |
||
| 106 | 101 | ||
| 107 | # Groups |
102 | # Groups |
| 108 | (r'^groups/$', 'reviewboard.reviews.views.group_list', |
103 | (r'^groups/$', 'reviewboard.reviews.views.group_list'), |
| 109 | {'template_name': 'reviews/group_list.html'}), |
104 | (r'^groups/(?P<name>[A-Za-z0-9_-]+)/$', 'reviewboard.reviews.views.group'), |
| 110 | |||
| 111 | (r'^groups/(?P<name>[A-Za-z0-9_-]+)/$', |
||
| 112 | 'reviewboard.reviews.views.group', |
||
| 113 | {'template_name': 'reviews/review_list.html'}), |
||
| 114 | 105 | ||
| 115 | # Feeds |
106 | # Feeds |
| 116 | (r'^feeds/rss/(?P<url>.*)/$', 'django.contrib.syndication.views.feed', |
107 | (r'^feeds/rss/(?P<url>.*)/$', 'django.contrib.syndication.views.feed', |
| 117 | {'feed_dict': rss_feeds}), |
108 | {'feed_dict': rss_feeds}), |
| 118 | (r'^feeds/atom/(?P<url>.*)/$', 'django.contrib.syndication.views.feed', |
109 | (r'^feeds/atom/(?P<url>.*)/$', 'django.contrib.syndication.views.feed', |
| ... | 41 lines hidden [Expand] | ||
| /trunk/reviewboard/accounts/models.py | |||
|---|---|---|---|
| Revision 768 | New Change | ||
| ... | 13 lines hidden [Expand] | ||
| 14 | 14 | ||
| 15 | collapsed_diffs = models.BooleanField(default=True) |
15 | collapsed_diffs = models.BooleanField(default=True) |
| 16 | wordwrapped_diffs = models.BooleanField(default=True) |
16 | wordwrapped_diffs = models.BooleanField(default=True) |
| 17 | syntax_highlighting = models.BooleanField(default=True) |
17 | syntax_highlighting = models.BooleanField(default=True) |
| 18 | 18 | ||
| 19 | sort_columns = models.CharField(maxlength=128, blank=True) |
||
| 20 | columns = models.CharField(maxlength=128, blank=True) |
||
| 21 | |||
| 19 | starred_review_requests = models.ManyToManyField(ReviewRequest, core=False, |
22 | starred_review_requests = models.ManyToManyField(ReviewRequest, core=False, |
| 20 | blank=True) |
23 | blank=True) |
| 21 | 24 | ||
| 22 | def __str__(self): |
25 | def __str__(self): |
| 23 | return self.user.username |
26 | return self.user.username |
| 24 | 27 | ||
| 25 | class Admin: |
28 | class Admin: |
| 26 | list_display = ('__str__', 'first_time_setup_done') |
29 | list_display = ('__str__', 'first_time_setup_done') |
| /trunk/reviewboard/htdocs/css/common.css | |||
|---|---|---|---|
| Revision 768 | New Change | ||
| ... | 89 lines hidden [Expand] | ||
| 90 | background: url("/images/ui/header_bg.png") repeat-x bottom left; |
90 | background: url("/images/ui/header_bg.png") repeat-x bottom left; |
| 91 | border-top: 1px #999999 solid; |
91 | border-top: 1px #999999 solid; |
| 92 | border-bottom: 1px #999999 solid; |
92 | border-bottom: 1px #999999 solid; |
| 93 | border-left: 1px #CCCCCC solid; |
93 | border-left: 1px #CCCCCC solid; |
| 94 | border-right: 1px #CCCCCC solid; |
94 | border-right: 1px #CCCCCC solid; |
| 95 | color: #444444; |
||
| 96 | cursor: pointer; |
||
| 97 | } |
||
| 98 | |||
| 99 | table.list tr.headers th a { |
||
| 100 | color: black; |
||
| 101 | text-decoration: none; |
||
| 102 | } |
||
| 103 | |||
| 104 | table.list tr.headers th a.unsort { |
||
| 105 | color: #444444; |
||
| 106 | } |
||
| 107 | |||
| 108 | table.list tr.headers th a:hover { |
||
| 109 | text-decoration: underline; |
||
| 95 | } |
110 | } |
| 96 | 111 | ||
| 97 | table.list tr:hover.headers { |
112 | table.list tr:hover.headers { |
| 98 | background-color: transparent; |
113 | background-color: transparent; |
| 99 | } |
114 | } |
| 100 | 115 | ||
| 116 | table.list tr.headers th:hover { |
||
| 117 | background: url("/images/ui/header_bg_primary.png") repeat-x bottom left; |
||
| 118 | } |
||
| 119 | |||
| 120 | table.list tr.headers th:hover a { |
||
| 121 | text-decoration: underline; |
||
| 122 | } |
||
| 123 | |||
| 101 | table.list tr:hover .age1 { |
124 | table.list tr:hover .age1 { |
| 102 | background: #abd5a9; |
125 | background: #abd5a9; |
| 103 | } |
126 | } |
| 104 | 127 | ||
| 105 | table.list tr:hover .age2 { |
128 | table.list tr:hover .age2 { |
| ... | 328 lines hidden [Expand] | ||
| /trunk/reviewboard/reviews/views.py | |||
|---|---|---|---|
| Revision 768 | New Change | ||
| 1 |
|
1 |
|
| 2 | 2 | ||
| 3 | from django.conf import settings |
3 | from django.conf import settings |
| 4 | from django.contrib.auth import REDIRECT_FIELD_NAME |
4 | from django.contrib.auth import REDIRECT_FIELD_NAME |
| 5 | from django.contrib.auth.models import User |
5 | from django.contrib.auth.models import User |
| 6 | from django.contrib.sites.models import Site |
6 | from django.contrib.sites.models import Site |
| 7 | from django.db.models import Q |
7 | from django.db.models import Q |
| 8 | from django.http import HttpResponse, HttpResponseRedirect, Http404, \ |
8 | from django.http import HttpResponse, HttpResponseRedirect, Http404, \ |
| 9 | HttpResponseForbidden |
9 | HttpResponseForbidden |
| 10 | from django.shortcuts import get_object_or_404, render_to_response |
10 | from django.shortcuts import get_object_or_404, render_to_response |
| 11 | from django.template.context import RequestContext |
11 | from django.template.context import RequestContext |
| 12 | from django.template.loader import render_to_string |
12 | from django.template.loader import render_to_string |
| 13 | from django.utils import simplejson |
13 | from django.utils import simplejson |
| 14 | from django.views.generic.list_detail import object_list |
||
| 15 | from djblets.auth.util import login_required |
14 | from djblets.auth.util import login_required |
| 16 | from djblets.util.decorators import simple_decorator |
15 | from djblets.util.decorators import simple_decorator |
| 17 | from djblets.util.misc import get_object_or_none |
16 | from djblets.util.misc import get_object_or_none |
| 18 | 17 | ||
| 19 | from reviewboard.accounts.models import Profile |
18 | from reviewboard.accounts.models import Profile |
| ... | 7 lines hidden [Expand] | ||
| 27 | UploadScreenshotForm, \ |
26 | UploadScreenshotForm, \ |
| 28 | OwnershipError |
27 | OwnershipError |
| 29 | from reviewboard.reviews.email import mail_review_request, \ |
28 | from reviewboard.reviews.email import mail_review_request, \ |
| 30 | mail_diff_update |
29 | mail_diff_update |
| 31 | from reviewboard.scmtools.models import Repository |
30 | from reviewboard.scmtools.models import Repository |
| 31 | from reviewboard.utils.views import sortable_object_list |
||
| 32 | import reviewboard.reviews.db as reviews_db |
32 | import reviewboard.reviews.db as reviews_db |
| 33 | 33 | ||
| 34 | 34 | ||
| 35 | @simple_decorator |
35 | @simple_decorator |
| 36 | def valid_prefs_required(view_func): |
36 | def valid_prefs_required(view_func): |
| ... | 66 lines hidden [Expand] | ||
| 103 | 'request': request, |
103 | 'request': request, |
| 104 | })) |
104 | })) |
| 105 | 105 | ||
| 106 | 106 | ||
| 107 | def review_list(request, queryset, template_name, extra_context={}): |
107 | def review_list(request, queryset, template_name, extra_context={}): |
| 108 | return object_list(request, |
108 | return sortable_object_list(request, |
| 109 | queryset=queryset.filter(Q(status='P') | |
109 | queryset=queryset.filter(Q(status='P') | |
| 110 | Q(status='S')).order_by('-last_updated'), |
110 | Q(status='S')).order_by('-last_updated'), |
| 111 | paginate_by=50, |
||
| 112 | allow_empty=True, |
||
| 113 | template_name=template_name, |
111 | template_name=template_name, |
| 114 | extra_context=dict( |
112 | extra_context=extra_context) |
| 115 | {'app_path': request.path}, |
||
| 116 | **extra_context |
||
| 117 | )) |
||
| 118 | 113 | ||
| 119 | 114 | ||
| 120 | @login_required |
115 | @login_required |
| 121 | def all_review_requests(request, template_name): |
116 | def all_review_requests(request, template_name='reviews/review_list.html'): |
| 122 | return review_list(request, |
117 | return review_list(request, |
| 123 | queryset=reviews_db.get_all_review_requests(request.user, status=None), |
118 | queryset=reviews_db.get_all_review_requests(request.user, status=None), |
| 124 | template_name=template_name) |
119 | template_name=template_name) |
| 125 | 120 | ||
| 126 | 121 | ||
| 127 | @login_required |
122 | @login_required |
| 128 | def submitter_list(request, template_name): |
123 | def submitter_list(request, template_name='reviews/submitter_list.html'): |
| 129 | return object_list(request, |
124 | return sortable_object_list(request, |
| 130 | queryset=User.objects.filter(), |
125 | queryset=User.objects.filter(), |
| 131 | template_name=template_name, |
126 | template_name=template_name) |
| 132 | paginate_by=50, |
||
| 133 | allow_empty=True, |
||
| 134 | extra_context={ |
||
| 135 | 'app_path': request.path, |
||
| 136 | }) |
||
| 137 | 127 | ||
| 138 | 128 | ||
| 139 | @login_required |
129 | @login_required |
| 140 | def group_list(request, template_name): |
130 | def group_list(request, template_name='reviews/group_list.html'): |
| 141 | return object_list(request, |
131 | return sortable_object_list(request, |
| 142 | queryset=Group.objects.all(), |
132 | queryset=Group.objects.all(), |
| 143 | template_name=template_name, |
133 | template_name=template_name) |
| 144 | paginate_by=50, |
||
| 145 | allow_empty=True, |
||
| 146 | extra_context={ |
||
| 147 | 'app_path': request.path, |
||
| 148 | }) |
||
| 149 | 134 | ||
| 150 | 135 | ||
| 151 | @login_required |
136 | @login_required |
| 152 | @valid_prefs_required |
137 | @valid_prefs_required |
| 153 | def dashboard(request, template_name='reviews/dashboard.html'): |
138 | def dashboard(request, template_name='reviews/dashboard.html'): |
| 154 | view = request.GET.get('view', 'incoming') |
139 | view = request.GET.get('view', 'incoming') |
| 155 | group = request.GET.get('group', "") |
140 | group = request.GET.get('group', "") |
| 156 | 141 | ||
| 157 | if view == 'outgoing': |
142 | if view == 'outgoing': |
| 158 | review_requests = \ |
143 | review_requests = \ |
| 159 | reviews_db.get_review_requests_from_user(request.user.username, |
144 | reviews_db.get_review_requests_from_user(request.user.username, |
| 160 | request.user) |
145 | request.user) |
| 161 | title = "All Outgoing Review Requests"; |
146 | title = "All Outgoing Review Requests" |
| 162 | elif view == 'to-me': |
147 | elif view == 'to-me': |
| 163 | review_requests = reviews_db.get_review_requests_to_user_directly( |
148 | review_requests = reviews_db.get_review_requests_to_user_directly( |
| 164 | request.user.username, request.user) |
149 | request.user.username, request.user) |
| 165 | title = "Incoming Review Requests to Me"; |
150 | title = "Incoming Review Requests to Me" |
| 166 | elif view == 'to-group': |
151 | elif view == 'to-group': |
| 167 | if group != "": |
152 | if group != "": |
| 168 | review_requests = reviews_db.get_review_requests_to_group( |
153 | review_requests = reviews_db.get_review_requests_to_group( |
| 169 | group, request.user) |
154 | group, request.user) |
| 170 | title = "Incoming Review Requests to %s" % group; |
155 | title = "Incoming Review Requests to %s" % group |
| 171 | else: |
156 | else: |
| 172 | review_requests = reviews_db.get_review_requests_to_user_groups( |
157 | review_requests = reviews_db.get_review_requests_to_user_groups( |
| 173 | request.user.username, request.user) |
158 | request.user.username, request.user) |
| 174 | title = "All Incoming Review Requests to My Groups" |
159 | title = "All Incoming Review Requests to My Groups" |
| 175 | else: # "incoming" or invalid |
160 | else: # "incoming" or invalid |
| ... | 7 lines hidden [Expand] | ||
| 183 | list is a QuerySet. |
168 | list is a QuerySet. |
| 184 | """ |
169 | """ |
| 185 | def __init__(self, list): |
170 | def __init__(self, list): |
| 186 | self.list = list |
171 | self.list = list |
| 187 | 172 | ||
| 173 | def order_by(self, *field_names): |
||
| 174 | return BogusQuerySet(sorted(self.list, |
||
| 175 | lambda a,b: self._sort_func(a, b, field_names))) |
||
| 176 | |||
| 177 | def _sort_func(self, a, b, field_list): |
||
| 178 | for field in field_list: |
||
| 179 | if field[0] == "-": |
||
| 180 | reverse = True |
||
| 181 | field = field[1:] |
||
| 182 | else: |
||
| 183 | reverse = False |
||
| 184 | |||
| 185 | try: |
||
| 186 | a_value = str(getattr(a, field)) |
||
| 187 | b_value = str(getattr(b, field)) |
||
| 188 | |||
| 189 | if reverse: |
||
| 190 | i = cmp(b_value, a_value) |
||
| 191 | else: |
||
| 192 | i = cmp(a_value, b_value) |
||
| 193 | |||
| 194 | if i != 0: |
||
| 195 | return i |
||
| 196 | except AttributeError: |
||
| 197 | # The field doesn't exist, so just ignore it. |
||
| 198 | pass |
||
| 199 | |||
| 200 | # They're equal, so compare the objects themselves to "sort" it out. |
||
| 201 | return cmp(a, b) |
||
| 202 | |||
| 188 | def _clone(self): |
203 | def _clone(self): |
| 189 | return self.list |
204 | return self.list |
| 190 | 205 | ||
| 191 | return object_list(request, |
206 | if isinstance(review_requests, list): |
| 192 | queryset=BogusQuerySet(review_requests), |
207 | queryset = BogusQuerySet(review_requests) |
| 208 | else: |
||
| 209 | queryset = review_requests |
||
| 210 | |||
| 211 | return sortable_object_list(request, |
||
| 212 | queryset=queryset, |
||
| 193 | template_name=template_name, |
213 | template_name=template_name, |
| 194 | paginate_by=50, |
||
| 195 | allow_empty=True, |
||
| 196 | template_object_name='review_request', |
214 | template_object_name='review_request', |
| 197 | extra_context={ |
215 | extra_context={ |
| 198 | 'title': title, |
216 | 'title': title, |
| 199 | 'view': view, |
217 | 'view': view, |
| 200 | 'group': group, |
218 | 'group': group, |
| 201 | }) |
219 | }) |
| 202 | 220 | ||
| 203 | 221 | ||
| 204 | @login_required |
222 | @login_required |
| 205 | def group(request, name, template_name): |
223 | def group(request, name, template_name='reviews/review_list.html'): |
| 206 | return review_list(request, |
224 | return review_list(request, |
| 207 | queryset=reviews_db.get_review_requests_to_group(name, status=None), |
225 | queryset=reviews_db.get_review_requests_to_group(name, status=None), |
| 208 | template_name=template_name, |
226 | template_name=template_name, |
| 209 | extra_context={ |
227 | extra_context={ |
| 210 | 'source': name, |
228 | 'source': name, |
| 211 | }) |
229 | }) |
| 212 | 230 | ||
| 213 | 231 | ||
| 214 | @login_required |
232 | @login_required |
| 215 | def submitter(request, username, template_name): |
233 | def submitter(request, username, template_name='reviews/review_list.html'): |
| 216 | return review_list(request, |
234 | return review_list(request, |
| 217 | queryset=reviews_db.get_review_requests_from_user(username, |
235 | queryset=reviews_db.get_review_requests_from_user(username, |
| 218 | status=None), |
236 | status=None), |
| 219 | template_name=template_name, |
237 | template_name=template_name, |
| 220 | extra_context={ |
238 | extra_context={ |
| ... | 295 lines hidden [Expand] | ||
| /trunk/reviewboard/templates/reviews/dashboard.html | |||
|---|---|---|---|
| Revision 768 | New Change | ||
| ... | 41 lines hidden [Expand] | ||
| 42 | <col class="posted" width="0%" /> |
42 | <col class="posted" width="0%" /> |
| 43 | <col class="updated" width="0%" /> |
43 | <col class="updated" width="0%" /> |
| 44 | </colgroup> |
44 | </colgroup> |
| 45 | <thead> |
45 | <thead> |
| 46 | <tr class="headers"> |
46 | <tr class="headers"> |
| 47 | <th>Summary</th> |
47 | {% column_header "summary" "Summary" %} |
| 48 | <th>Submitter</th> |
48 | {% column_header "submitter" "Submitter" %} |
| 49 | <th>Posted</th> |
49 | {% column_header "time_added" "Posted" %} |
| 50 | <th>Last Updated</th> |
50 | {% column_header "last_updated" "Last Updated" %} |
| 51 | </tr> |
51 | </tr> |
| 52 | </thead> |
52 | </thead> |
| 53 | <tbody> |
53 | <tbody> |
| 54 | {% for review_request in review_request_list %} |
54 | {% for review_request in review_request_list %} |
| 55 | <tr> |
55 | <tr> |
| ... | 34 lines hidden [Expand] | ||
| /trunk/reviewboard/templates/reviews/review_list.html | |||
|---|---|---|---|
| Revision 768 | New Change | ||
| ... | 18 lines hidden [Expand] | ||
| 19 | <col class="submitter" width="0%" /> |
19 | <col class="submitter" width="0%" /> |
| 20 | <col class="updated" width="0%" /> |
20 | <col class="updated" width="0%" /> |
| 21 | </colgroup> |
21 | </colgroup> |
| 22 | <thead> |
22 | <thead> |
| 23 | <tr class="headers"> |
23 | <tr class="headers"> |
| 24 | <th>Summary</th> |
24 | {% column_header "summary" "Summary" %} |
| 25 | <th>Submitter</th> |
25 | {% column_header "submitter" "Submitter" %} |
| 26 | <th>Posted</th> |
26 | {% column_header "time_added" "Posted" %} |
| 27 | <th>Last Updated</th> |
27 | {% column_header "last_updated" "Last Updated" %} |
| 28 | </tr> |
28 | </tr> |
| 29 | </thead> |
29 | </thead> |
| 30 | <tbody> |
30 | <tbody> |
| 31 | {% for review in object_list %} |
31 | {% for review in object_list %} |
| 32 | <tr> |
32 | <tr> |
| ... | 18 lines hidden [Expand] | ||