Review Board

beta

Add support for starring groups and review requests

Updated 1 year, 4 months ago

Christian Hammond Reviewers
trunk reviewboard
200
None Review Board SVN
Added support for starring groups and review requests. Starring a group puts it on your dashboard under a "Watched Groups" list, which is useful when you want to keep track of a group but don't want the group's review requests appearing under your Incoming Reviews list or sending you e-mails.

Starring a review request puts it in your Starred Reviews list and sends any e-mails your way. This is useful if you're not a reviewer of a review request but want to keep track of what's going on with it without having to get involved.
Tested that starring and unstarring works in each area, and that they appear on the dashboard.
/trunk/reviewboard/accounts/models.py
Revision 916 New Change
1
from datetime import datetime
1
from datetime import datetime
2
2
3
from django.db import models
3
from django.db import models
4
from django.contrib.auth.models import User
4
from django.contrib.auth.models import User
5
5
6
from reviewboard.reviews.models import ReviewRequest
6
from reviewboard.reviews.models import Group, ReviewRequest
7
7
8
8
9
class ReviewRequestVisit(models.Model):
9
class ReviewRequestVisit(models.Model):
10
    """A review request visit."""
10
    """A review request visit."""
11
    user = models.ForeignKey(User, related_name="review_request_visits")
11
    user = models.ForeignKey(User, related_name="review_request_visits")
36
36
37
    #review_request_columns = models.CharField(maxlength=128, blank=True)
37
    #review_request_columns = models.CharField(maxlength=128, blank=True)
38
    #submitter_columns = models.CharField(maxlength=128, blank=True)
38
    #submitter_columns = models.CharField(maxlength=128, blank=True)
39
    #group_columns = models.CharField(maxlength=128, blank=True)
39
    #group_columns = models.CharField(maxlength=128, blank=True)
40
40
41
    starred_review_requests = models.ManyToManyField(ReviewRequest, core=False,
41
    starred_review_requests = models.ManyToManyField(
42
                                                     blank=True)
42
        ReviewRequest, core=False, blank=True,
43
        filter_interface=models.HORIZONTAL,
44
        related_name="starred_by")
45
    starred_groups = models.ManyToManyField(
46
        Group, core=False, blank=True, filter_interface=models.HORIZONTAL,
47
        related_name="starred_by")
43
48
44
    def __unicode__(self):
49
    def __unicode__(self):
45
        return self.user.username
50
        return self.user.username
46
51
47
    class Admin:
52
    class Admin:
48
        list_display = ('__unicode__', 'first_time_setup_done')
53
        list_display = ('__unicode__', 'first_time_setup_done')
/trunk/reviewboard/htdocs/css/common.css
Revision 916 New Change
272
  padding: 0;
272
  padding: 0;
273
  position: relative;
273
  position: relative;
274
  top: -23px;
274
  top: -23px;
275
}
275
}
276
276
277
.box .star {
278
  cursor: pointer;
279
}
280
277
.box h1.title,
281
.box h1.title,
278
.box .titlebox {
282
.box .titlebox {
279
  background: #a2bedc url('/images/title_box_top_bg.png') repeat-x top left;
283
  background: #a2bedc url('/images/title_box_top_bg.png') repeat-x top left;
280
  border-bottom: 1px #728eac solid;
284
  border-bottom: 1px #728eac solid;
281
  margin: 0;
285
  margin: 0;
282
  padding: 5px 10px;
286
  padding: 5px 10px 5px 5px;
283
}
287
}
284
288
285
.box h1.title {
289
.box h1.title {
286
  font-size: 120%;
290
  font-size: 120%;
287
}
291
}
/trunk/reviewboard/htdocs/css/dashboard.css
Revision 916 New Change
36
36
37
#dashboard-navbar .main-item {
37
#dashboard-navbar .main-item {
38
  font-weight: bold;
38
  font-weight: bold;
39
}
39
}
40
40
41
#dashboard-navbar .header {
42
  font-weight: bold;
43
  padding-top: 15px;
44
}
45
41
#dashboard-navbar .sub-item {
46
#dashboard-navbar .sub-item {
42
  padding-left: 25px;
47
  padding-left: 25px;
43
}
48
}
44
49
45
#dashboard-navbar .sub-sub-item {
50
#dashboard-navbar .star-item {
46
  padding-left: 45px;
51
  background: url("/images/star_on.png") no-repeat 5px 4px;
52
  padding-left: 25px;
47
}
53
}
48
54
49
#dashboard-navbar tr,
55
#dashboard-navbar tr,
50
#dashboard-wrapper {
56
#dashboard-wrapper {
51
  background-color: #E9E9E9;
57
  background-color: #E9E9E9;
52
}
58
}
53
59
54
#dashboard-navbar tr.selected {
60
#dashboard-navbar tr.selected {
55
  background: #b2ceec;
61
  background: #b2ceec;
56
}
62
}
63
64
#dashboard-navbar td img {
65
  vertical-align: bottom;
66
}
67
68
#dashboard-navbar tbody {
69
}
/trunk/reviewboard/htdocs/css/reviews.css
Revision 916 New Change
109
109
110
.review-request .header {
110
.review-request .header {
111
  margin: 5px;
111
  margin: 5px;
112
}
112
}
113
113
114
.review-request .star {
115
  float: left;
116
  margin-right: 10px;
117
}
118
114
.review-request #details {
119
.review-request #details {
115
  border-bottom: 1px #c2c1b0 solid;
120
  border-bottom: 1px #c2c1b0 solid;
116
  border-spacing: 8px;
121
  border-spacing: 8px;
117
  border-top: 1px #c2c1b0 solid;
122
  border-top: 1px #c2c1b0 solid;
118
  margin-top: 5px;
123
  margin-top: 5px;
158
.review-request #summary_wrapper {
163
.review-request #summary_wrapper {
159
  margin-right: 10em;
164
  margin-right: 10em;
160
  padding: 0px;
165
  padding: 0px;
161
}
166
}
162
167
168
.review-request #summary_wrapper label {
169
  font-size: 130%;
170
}
171
163
.review-request #topcontrols {
172
.review-request #topcontrols {
164
  float: right;
173
  float: right;
165
  list-style: none;
174
  list-style: none;
166
  margin: -1px -1px 0 0;
175
  margin: -1px -1px 0 0;
167
  padding: 0px;
176
  padding: 0px;
/trunk/reviewboard/htdocs/images/star.png
Revision UNKNOWN New Change
This is a binary file. The content cannot be displayed.
/trunk/reviewboard/htdocs/images/star_off.png
Revision UNKNOWN New Change
This is a binary file. The content cannot be displayed.
/trunk/reviewboard/htdocs/images/star_on.png
Revision UNKNOWN New Change
This is a binary file. The content cannot be displayed.
/trunk/reviewboard/htdocs/scripts/rb/common.js
Revision 916 New Change
1
RB = {utils: {}}
1
RB = {utils: {}}
2
2
3
4
// Constants
5
var STAR_ON_IMG = "/images/star_on.png";
6
var STAR_OFF_IMG = "/images/star_off.png";
7
8
3
RB.utils.String = function() {};
9
RB.utils.String = function() {};
4
RB.utils.String.prototype = {
10
RB.utils.String.prototype = {
5
	strip: function() {
11
	strip: function() {
6
		return this.replace(/^\s+/, '').replace(/\s+$/, '');
12
		return this.replace(/^\s+/, '').replace(/\s+$/, '');
7
	},
13
	},
126
                callbacks.failure(res.statusText);
132
                callbacks.failure(res.statusText);
127
            }
133
            }
128
        }
134
        }
129
    }, postData || "dummy");
135
    }, postData || "dummy");
130
};
136
};
137
138
139
/*
140
 * Toggles whether an object is starred. Right now, we support
141
 * "reviewrequests" and "groups" types.
142
 *
143
 * @param {HTMLElement} el     The star img element.
144
 * @param {string}      type   The type used for constructing the path.
145
 * @param {string}      objid  The object ID to star/unstar.
146
 */
147
function toggleStar(el, type, objid) {
148
  var isStarred = (el.src.indexOf(STAR_ON_IMG) != -1);
149
  var url = "/api/json/" + type + "/" + objid + "/";
150
151
  if (isStarred) {
152
    url += "unstar/";
153
  } else {
154
    url += "star/";
155
  }
156
157
  asyncJsonRequest("GET", url, {
158
    success: function(rsp) {
159
      if (isStarred) {
160
        el.src = STAR_OFF_IMG;
161
      } else {
162
        el.src = STAR_ON_IMG;
163
      }
164
    },
165
    failure: function(errmsg) {
166
      alert(errmsg);
167
    }
168
  });
169
}
/trunk/reviewboard/reviews/email.py
Revision 916 New Change
66
66
67
    for group in review_request.target_groups.all():
67
    for group in review_request.target_groups.all():
68
        for address in get_email_addresses_for_group(group):
68
        for address in get_email_addresses_for_group(group):
69
            recipient_table[address] = 1
69
            recipient_table[address] = 1
70
70
71
    for profile in review_request.starred_by.all():
72
        recipient_table[get_email_address_for_user(profile.user)] = 1
73
71
    if extra_recipients:
74
    if extra_recipients:
72
        for recipient in extra_recipients:
75
        for recipient in extra_recipients:
73
            recipient_table[get_email_address_for_user(recipient)] = 1
76
            recipient_table[get_email_address_for_user(recipient)] = 1
74
77
75
    recipient_list = recipient_table.keys()
78
    recipient_list = recipient_table.keys()
/trunk/reviewboard/reviews/json.py
Revision 916 New Change
13
from django.template.defaultfilters import timesince
13
from django.template.defaultfilters import timesince
14
from django.utils import simplejson
14
from django.utils import simplejson
15
from django.views.decorators.http import require_POST
15
from django.views.decorators.http import require_POST
16
16
17
from djblets.util.decorators import simple_decorator
17
from djblets.util.decorators import simple_decorator
18
from reviewboard.accounts.models import Profile
18
from reviewboard.diffviewer.forms import UploadDiffForm, EmptyDiffError
19
from reviewboard.diffviewer.forms import UploadDiffForm, EmptyDiffError
19
from reviewboard.diffviewer.models import FileDiff, DiffSet
20
from reviewboard.diffviewer.models import FileDiff, DiffSet
20
from reviewboard.reviews.email import mail_review, mail_review_request, \
21
from reviewboard.reviews.email import mail_review, mail_review_request, \
21
                                      mail_reply, mail_diff_update
22
                                      mail_reply, mail_diff_update
22
from reviewboard.reviews.forms import UploadScreenshotForm
23
from reviewboard.reviews.forms import UploadScreenshotForm
324
        'repositories': Repository.objects.all(),
325
        'repositories': Repository.objects.all(),
325
    })
326
    })
326
327
327
328
328
@json_login_required
329
@json_login_required
330
def group_star(request, group_name):
331
    try:
332
        group = Group.objects.get(name=group_name)
333
    except Group.DoesNotExist:
334
        return JsonResponseError(request, DOES_NOT_EXIST)
335
336
    profile, profile_is_new = Profile.objects.get_or_create(user=request.user)
337
    profile.starred_groups.add(group)
338
    profile.save()
339
340
    return JsonResponse(request)
341
342
343
@json_login_required
344
def group_unstar(request, group_name):
345
    try:
346
        group = Group.objects.get(name=group_name)
347
    except Group.DoesNotExist:
348
        return JsonResponseError(request, DOES_NOT_EXIST)
349
350
    profile, profile_is_new = Profile.objects.get_or_create(user=request.user)
351
352
    if not profile_is_new:
353
        profile.starred_groups.remove(group)
354
        profile.save()
355
356
    return JsonResponse(request)
357
358
359
@json_login_required
329
@require_POST
360
@require_POST
330
def new_review_request(request):
361
def new_review_request(request):
331
    try:
362
    try:
332
        repository_path = request.POST.get('repository_path',
363
        repository_path = request.POST.get('repository_path',
333
                                           settings.DEFAULT_REPOSITORY_PATH)
364
                                           settings.DEFAULT_REPOSITORY_PATH)
378
        return JsonResponse(request, {'review_request': review_request})
409
        return JsonResponse(request, {'review_request': review_request})
379
    except ReviewRequest.DoesNotExist:
410
    except ReviewRequest.DoesNotExist:
380
        return JsonResponseError(request, INVALID_CHANGE_NUMBER)
411
        return JsonResponseError(request, INVALID_CHANGE_NUMBER)
381
412
382
413
414
@json_login_required
415
def review_request_star(request, review_request_id):
416
    try:
417
        review_request = ReviewRequest.objects.get(pk=review_request_id)
418
    except ReviewRequest.DoesNotExist:
419
        return JsonResponseError(request, DOES_NOT_EXIST)
420
421
    profile, profile_is_new = Profile.objects.get_or_create(user=request.user)
422
    profile.starred_review_requests.add(review_request)
423
    profile.save()
424
425
    return JsonResponse(request)
426
427
428
@json_login_required
429
def review_request_unstar(request, review_request_id):
430
    try:
431
        review_request = ReviewRequest.objects.get(pk=review_request_id)
432
    except ReviewRequest.DoesNotExist:
433
        return JsonResponseError(request, DOES_NOT_EXIST)
434
435
    profile, profile_is_new = Profile.objects.get_or_create(user=request.user)
436
437
    if not profile_is_new:
438
        profile.starred_review_requests.remove(review_request)
439
        profile.save()
440
441
    return JsonResponse(request)
442
443
383
@json_permission_required('reviews.delete_reviewrequest')
444
@json_permission_required('reviews.delete_reviewrequest')
384
def review_request_delete(request, review_request_id):
445
def review_request_delete(request, review_request_id):
385
    try:
446
    try:
386
        review_request = ReviewRequest.objects.get(pk=review_request_id)
447
        review_request = ReviewRequest.objects.get(pk=review_request_id)
387
        review_request.delete()
448
        review_request.delete()
/trunk/reviewboard/reviews/views.py
Revision 916 New Change
211
@login_required
211
@login_required
212
@valid_prefs_required
212
@valid_prefs_required
213
def dashboard(request, template_name='reviews/dashboard.html'):
213
def dashboard(request, template_name='reviews/dashboard.html'):
214
    view = request.GET.get('view', 'incoming')
214
    view = request.GET.get('view', 'incoming')
215
    group = request.GET.get('group', "")
215
    group = request.GET.get('group', "")
216
    user = request.user
216
217
217
    if view == 'outgoing':
218
    if view == 'outgoing':
218
        review_requests = \
219
        review_requests = ReviewRequest.objects.from_user(user.username, user)
219
            ReviewRequest.objects.from_user(request.user.username,
220
                                            request.user)
221
        title = "All Outgoing Review Requests"
220
        title = "All Outgoing Review Requests"
222
    elif view == 'to-me':
221
    elif view == 'to-me':
223
        review_requests = ReviewRequest.objects.to_user_directly(
222
        review_requests = \
224
            request.user.username, request.user)
223
            ReviewRequest.objects.to_user_directly(user.username, user)
225
        title = "Incoming Review Requests to Me"
224
        title = "Incoming Review Requests to Me"
226
    elif view == 'to-group':
225
    elif view == 'to-group':
227
        if group != "":
226
        if group != "":
228
            review_requests = ReviewRequest.objects.to_group(group,
227
            review_requests = ReviewRequest.objects.to_group(group, user)
229
                                                             request.user)
230
            title = "Incoming Review Requests to %s" % group
228
            title = "Incoming Review Requests to %s" % group
231
        else:
229
        else:
232
            review_requests = ReviewRequest.objects.to_user_groups(
230
            review_requests = \
233
                request.user.username, request.user)
231
                ReviewRequest.objects.to_user_groups(user.username, user)
234
            title = "All Incoming Review Requests to My Groups"
232
            title = "All Incoming Review Requests to My Groups"
233
    elif view == 'starred':
234
        review_requests = \
235
            user.get_profile().starred_review_requests.public(user)
236
        title = "Starred Review Requests"
235
    else: # "incoming" or invalid
237
    else: # "incoming" or invalid
236
        review_requests = ReviewRequest.objects.to_user(request.user.username,
238
        review_requests = ReviewRequest.objects.to_user(user.username, user)
237
                                                        request.user)
238
        title = "All Incoming Review Requests"
239
        title = "All Incoming Review Requests"
239
240
240
    class BogusQuerySet:
241
    class BogusQuerySet:
241
        """
242
        """
242
        Simple class to fool the object_list generic view into thinking a
243
        Simple class to fool the object_list generic view into thinking a
301
    return review_list(request,
302
    return review_list(request,
302
        queryset=ReviewRequest.objects.to_group(name, status=None),
303
        queryset=ReviewRequest.objects.to_group(name, status=None),
303
        template_name=template_name,
304
        template_name=template_name,
304
        extra_context={
305
        extra_context={
305
            'source': name,
306
            'source': name,
307
            'group': get_object_or_none(Group, name=name),
306
        })
308
        })
307
309
308
310
309
@check_login_required
311
@check_login_required
310
def submitter(request, username, template_name='reviews/review_list.html'):
312
def submitter(request, username, template_name='reviews/review_list.html'):
/trunk/reviewboard/reviews/templatetags/reviewtags.py
Revision 916 New Change
1
from django import template
1
from django import template
2
from django.db.models import Q
2
from django.db.models import Q
3
from django.db.models.query import QuerySet
3
from django.db.models.query import QuerySet
4
from django.template import resolve_variable
4
from django.template import resolve_variable
5
from django.template import NodeList, TemplateSyntaxError, VariableDoesNotExist
5
from django.template import NodeList, TemplateSyntaxError, VariableDoesNotExist
6
from django.template.loader import render_to_string
6
from django.template.loader import render_to_string
7
from django.utils import simplejson
7
from django.utils import simplejson
8
9
from djblets.util.decorators import blocktag
8
from djblets.util.decorators import blocktag
10
from reviewboard.accounts.models import ReviewRequest, ReviewRequestVisit
9
from djblets.util.misc import get_object_or_none
11
from reviewboard.reviews.models import ReviewRequestDraft, ScreenshotComment
10
11
from reviewboard.accounts.models import Profile, ReviewRequestVisit
12
from reviewboard.reviews.models import Group, ReviewRequest, \
13
                                       ReviewRequestDraft, ScreenshotComment
12