Rietveld Code Review Tool
Help | Bug tracker | Discussion group | Source code | Sign in
(92)

Side by Side Diff: rietveld/views.py

Issue 776: Allow reviewers to change the reviewers list (Closed) SVN Base: http://rietveld.googlecode.com/svn/trunk/
Patch Set: Created 3 months, 2 weeks ago
Use n/p to move between diff chunks; N/P to move between comments. Please Sign in to add in-line comments.
Jump to:
View unified diff | Download patch
OLDNEW
1 # Copyright 2008 Google Inc. 1 # Copyright 2008 Google Inc.
2 # 2 #
3 # Licensed under the Apache License, Version 2.0 (the "License"); 3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License. 4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at 5 # You may obtain a copy of the License at
6 # 6 #
7 # http://www.apache.org/licenses/LICENSE-2.0 7 # http://www.apache.org/licenses/LICENSE-2.0
8 # 8 #
9 # Unless required by applicable law or agreed to in writing, software 9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS, 10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and 12 # See the License for the specific language governing permissions and
13 # limitations under the License. 13 # limitations under the License.
14 14
15 """Views for Rietveld. 15 """Views for Rietveld.
16 16
17 This requires Django 0.97.pre. 17 This requires Django 0.97.pre.
18 """ 18 """
19 19
20 20
21 ### Imports ### 21 ### Imports ###
22 22
23 23
24 # Python imports 24 # Python imports
25 import os 25 import os
26 import cgi 26 import cgi
27 import random 27 import random
28 import logging 28 import logging
29 import binascii 29 import binascii
30 30
31 # AppEngine imports 31 # AppEngine imports
32 from google.appengine.api import mail 32 from google.appengine.api import mail
33 from google.appengine.api import users 33 from google.appengine.api import users
34 from google.appengine.api import urlfetch 34 from google.appengine.api import urlfetch
35 from google.appengine.ext import db 35 from google.appengine.ext import db
36 from google.appengine.ext.db import djangoforms 36 from google.appengine.ext.db import djangoforms
37 37
38 # DeadlineExceededError can live in two different places 38 # DeadlineExceededError can live in two different places
39 # TODO(guido): simplify once this is fixed. 39 # TODO(guido): simplify once this is fixed.
40 try: 40 try:
41 # When deployed 41 # When deployed
42 from google.appengine.runtime import DeadlineExceededError 42 from google.appengine.runtime import DeadlineExceededError
43 except ImportError: 43 except ImportError:
44 # In the development server 44 # In the development server
45 from google.appengine.runtime.apiproxy_errors import DeadlineExceededError 45 from google.appengine.runtime.apiproxy_errors import DeadlineExceededError
46 46
47 # Django imports 47 # Django imports
48 # TODO(guido): Don't import classes/functions directly. 48 # TODO(guido): Don't import classes/functions directly.
49 from django import newforms as forms 49 from django import newforms as forms
50 from django.http import HttpResponse, HttpResponseRedirect 50 from django.http import HttpResponse, HttpResponseRedirect
(...skipping 89 matching lines...) Show 10 above Show 10 below
140 max_length=2083, 140 max_length=2083,
141 widget=forms.TextInput(attrs={'size': 60})) 141 widget=forms.TextInput(attrs={'size': 60}))
142 142
143 143
144 class UploadForm(forms.Form): 144 class UploadForm(forms.Form):
145 145
146 subject = forms.CharField(max_length=100) 146 subject = forms.CharField(max_length=100)
147 description = forms.CharField(max_length=10000, required=False) 147 description = forms.CharField(max_length=10000, required=False)
148 base = forms.CharField(max_length=2000) 148 base = forms.CharField(max_length=2000)
149 data = forms.FileField() 149 data = forms.FileField()
150 issue = forms.IntegerField(required=False) 150 issue = forms.IntegerField(required=False)
151 151
152 def get_base(self): 152 def get_base(self):
153 return self.cleaned_data.get('base') 153 return self.cleaned_data.get('base')
154 154
155 155
156 class EditForm(IssueBaseForm): 156 class EditForm(IssueBaseForm):
157 pass 157 pass
158 158
159 159
160 class RepoForm(djangoforms.ModelForm): 160 class RepoForm(djangoforms.ModelForm):
161 161
162 class Meta: 162 class Meta:
163 model = models.Repository 163 model = models.Repository
164 exclude = ['owner'] 164 exclude = ['owner']
165 165
166 166
167 class BranchForm(djangoforms.ModelForm): 167 class BranchForm(djangoforms.ModelForm):
168 168
169 class Meta: 169 class Meta:
170 model = models.Branch 170 model = models.Branch
171 exclude = ['owner'] 171 exclude = ['owner']
172 172
173 173
174 class PublishForm(forms.Form): 174 class PublishForm(forms.Form):
175 175
176 subject = forms.CharField(max_length=100, 176 subject = forms.CharField(max_length=100,
177 widget=forms.TextInput(attrs={'size': 60})) 177 widget=forms.TextInput(attrs={'size': 60}))
178 reviewers = forms.CharField(required=False, 178 reviewers = forms.CharField(required=False,
179 max_length=1000, 179 max_length=1000,
180 widget=forms.TextInput(attrs={'size': 60})) 180 widget=forms.TextInput(attrs={'size': 60}))
181 send_mail = forms.BooleanField() 181 send_mail = forms.BooleanField()
182 message = forms.CharField(required=False, 182 message = forms.CharField(required=False,
183 max_length=10000, 183 max_length=10000,
184 widget=forms.Textarea(attrs={'cols': 60})) 184 widget=forms.Textarea(attrs={'cols': 60}))
185 185
186 186
187 class MiniPublishForm(forms.Form): 187 class MiniPublishForm(forms.Form):
188 188
189 send_mail = forms.BooleanField() 189 send_mail = forms.BooleanField()
190 reviewers = forms.CharField(required=False,
191 max_length=1000,
192 widget=forms.TextInput(attrs={'size': 60}))
190 message = forms.CharField(required=False, 193 message = forms.CharField(required=False,
191 max_length=10000, 194 max_length=10000,
192 widget=forms.Textarea(attrs={'cols': 60})) 195 widget=forms.Textarea(attrs={'cols': 60}))
193 196
194 197
195 class SettingsForm(forms.Form): 198 class SettingsForm(forms.Form):
196 199
197 nickname = forms.CharField(max_length=30) 200 nickname = forms.CharField(max_length=30)
198 201
199 202
200 ### Helper functions ### 203 ### Helper functions ###
201 204
202 205
203 # Counter displayed (by respond()) below) on every page showing how 206 # Counter displayed (by respond()) below) on every page showing how
204 # many requests the current incarnation has handled, not counting 207 # many requests the current incarnation has handled, not counting
205 # redirects. Rendered by templates/base.html. 208 # redirects. Rendered by templates/base.html.
206 counter = 0 209 counter = 0
207 210
208 def respond(request, template, params=None): 211 def respond(request, template, params=None):
209 """Helper to render a response, passing standard stuff to the response. 212 """Helper to render a response, passing standard stuff to the response.
210 213
211 Args: 214 Args:
212 request: The request object. 215 request: The request object.
213 template: The template name; '.html' is appended automatically. 216 template: The template name; '.html' is appended automatically.
214 params: A dict giving the template parameters; modified in-place. 217 params: A dict giving the template parameters; modified in-place.
215 218
216 Returns: 219 Returns:
217 Whatever render_to_response(template, params) returns. 220 Whatever render_to_response(template, params) returns.
218 221
219 Raises: 222 Raises:
220 Whatever render_to_response(template, params) raises. 223 Whatever render_to_response(template, params) raises.
221 """ 224 """
222 global counter 225 global counter
223 counter += 1 226 counter += 1
224 if params is None: 227 if params is None:
225 params = {} 228 params = {}
226 must_choose_nickname = False 229 must_choose_nickname = False
227 if request.user is not None: 230 if request.user is not None:
228 account = models.Account.get_account_for_user(request.user) 231 account = models.Account.get_account_for_user(request.user)
229 delta = account.created - account.modified 232 delta = account.created - account.modified
230 if delta.days < 0: 233 if delta.days < 0:
231 delta = -delta 234 delta = -delta
232 must_choose_nickname = delta.days == 0 and delta.seconds < 2 235 must_choose_nickname = delta.days == 0 and delta.seconds < 2
233 params['request'] = request 236 params['request'] = request
234 params['counter'] = counter 237 params['counter'] = counter
235 params['user'] = request.user 238 params['user'] = request.user
236 params['is_admin'] = request.user_is_admin 239 params['is_admin'] = request.user_is_admin
237 params['is_dev'] = IS_DEV 240 params['is_dev'] = IS_DEV
238 params['sign_in'] = users.create_login_url(request.path) 241 params['sign_in'] = users.create_login_url(request.path)
239 params['sign_out'] = users.create_logout_url(request.path) 242 params['sign_out'] = users.create_logout_url(request.path)
(...skipping 656 matching lines...) Show 10 above Show 10 below
896 patch=patch, lineno=lineno, left=left) 899 patch=patch, lineno=lineno, left=left)
897 comments = list(c for c in query if not c.draft or c.author == request.user) 900 comments = list(c for c in query if not c.draft or c.author == request.user)
898 if comment is not None and comment.author is None: 901 if comment is not None and comment.author is None:
899 # Show anonymous draft even though we don't save it 902 # Show anonymous draft even though we don't save it
900 comments.append(comment) 903 comments.append(comment)
901 if not comments: 904 if not comments:
902 return HttpResponse(' ') 905 return HttpResponse(' ')
903 for c in comments: 906 for c in comments:
904 c.complete(patch) 907 c.complete(patch)
905 return render_to_response('inline_comment.html', 908 return render_to_response('inline_comment.html',
906 {'user': request.user, 909 {'user': request.user,
907 'patch': patch, 910 'patch': patch,
908 'patchset': patchset, 911 'patchset': patchset,
909 'issue': issue, 912 'issue': issue,
910 'comments': comments, 913 'comments': comments,
911 'lineno': lineno, 914 'lineno': lineno,
912 'snapshot': snapshot, 915 'snapshot': snapshot,
913 'side': side}) 916 'side': side})
914 917
915 918
916 PUBLISH_MAIL_TEMPLATE = """Dear %s, 919 PUBLISH_MAIL_TEMPLATE = """Dear %s,
917 920
918 New code review comments by %s have been published. 921 New code review comments by %s have been published.
919 Please go to %s to read them. 922 Please go to %s to read them.
920 923
921 Message: 924 Message:
922 %s 925 %s
923 926
924 Details: 927 Details:
925 %s 928 %s
926 929
927 Issue Description: 930 Issue Description:
928 %s 931 %s
929 932
930 Sincerely, 933 Sincerely,
931 934
932 Your friendly code review daemon (%s). 935 Your friendly code review daemon (%s).
933 """ 936 """
934 937
935 @issue_required 938 @issue_required
936 @login_required 939 @login_required
937 def publish(request): 940 def publish(request):
938 """ /<issue>/publish - Publish draft comments and send mail.""" 941 """ /<issue>/publish - Publish draft comments and send mail."""
939 issue = request.issue 942 issue = request.issue
940 if request.user == issue.owner: 943 if request.user == issue.owner:
941 form_class = PublishForm 944 form_class = PublishForm
942 else: 945 else:
943 form_class = MiniPublishForm 946 form_class = MiniPublishForm
944 if request.method != 'POST': 947 if request.method != 'POST':
945 form = form_class(initial={'subject': issue.subject, 948 form = form_class(initial={'subject': issue.subject,
946 'reviewers': ', '.join(issue.reviewers), 949 'reviewers': ', '.join(issue.reviewers),
GvR 2008/05/10 15:44:24 I would suggest one more addition: add the current
jiayao 2008/05/11 11:33:27 Good suggestion, I've added that. Just another tho
947 'send_mail': True, 950 'send_mail': True,
948 }) 951 })
949 return respond(request, 'publish.html', {'form': form, 'issue': issue}) 952 return respond(request, 'publish.html', {'form': form, 'issue': issue})
950 953
951 form = form_class(request.POST) 954 form = form_class(request.POST)
952 if form.is_valid(): 955 if form.is_valid():
953 reviewers = _get_reviewers(form) 956 reviewers = _get_reviewers(form)
954 if not form.is_valid(): 957 if not form.is_valid():
955 return respond(request, 'publish.html', {'form': form, 'issue': issue}) 958 return respond(request, 'publish.html', {'form': form, 'issue': issue})
956 tbd = [] # List of things to put() after all is said and done 959 tbd = [] # List of things to put() after all is said and done
957 if request.user == issue.owner: 960 if request.user == issue.owner:
958 subject = form.cleaned_data['subject'] 961 subject = form.cleaned_data['subject']
959 issue.subject = subject 962 issue.subject = subject
960 issue.reviewers = reviewers 963 issue.reviewers = reviewers
961 else: 964 else:
965 issue.reviewers = reviewers
GvR 2008/05/10 15:44:24 I'd move this one line downfor symmetry with the '
jiayao 2008/05/11 11:33:27 On 2008/05/10 15:44:24, GvR wrote: > I'd move this
962 subject = issue.subject 966 subject = issue.subject
963 tbd.append(issue) # To update the last modified time 967 tbd.append(issue) # To update the last modified time
964 message = form.cleaned_data['message'].replace('\r\n', '\n') 968 message = form.cleaned_data['message'].replace('\r\n', '\n')
965 send_mail = form.cleaned_data['send_mail'] 969 send_mail = form.cleaned_data['send_mail']
966 comments = [] 970 comments = []
967 971
968 # XXX Should request all drafts for this issue once, now we can. 972 # XXX Should request all drafts for this issue once, now we can.
969 for patchset in issue.patchset_set.order('created'): 973 for patchset in issue.patchset_set.order('created'):
970 ## ps_comments = list(models.Comment.gql( 974 ## ps_comments = list(models.Comment.gql(
971 ## 'WHERE ANCESTOR IS :1 AND author = :2 AND draft = TRUE', 975 ## 'WHERE ANCESTOR IS :1 AND author = :2 AND draft = TRUE',
972 ## patchset, request.user)) 976 ## patchset, request.user))
973 # XXX Somehow the index broke, do without it 977 # XXX Somehow the index broke, do without it
974 ps_comments = [c for c in 978 ps_comments = [c for c in
975 models.Comment.gql('WHERE ANCESTOR IS :1', patchset) 979 models.Comment.gql('WHERE ANCESTOR IS :1', patchset)
976 if c.draft and c.author == request.user] 980 if c.draft and c.author == request.user]
977 # XXX End 981 # XXX End
978 if ps_comments: 982 if ps_comments:
979 patches = dict((p.key(), p) for p in patchset.patch_set) 983 patches = dict((p.key(), p) for p in patchset.patch_set)
980 for p in patches.itervalues(): 984 for p in patches.itervalues():
981 p.patchset = patchset 985 p.patchset = patchset
982 for c in ps_comments: 986 for c in ps_comments:
983 c.draft = False 987 c.draft = False
984 # XXX Using internal knowledge about db package: the key for 988 # XXX Using internal knowledge about db package: the key for
985 # reference property foo is stored as _foo. 989 # reference property foo is stored as _foo.
986 pkey = getattr(c, '_patch', None) 990 pkey = getattr(c, '_patch', None)
987 if pkey in patches: 991 if pkey in patches:
988 patch = patches[pkey] 992 patch = patches[pkey]
989 c.patch = patch 993 c.patch = patch
990 tbd.append(ps_comments) 994 tbd.append(ps_comments)
991 ps_comments.sort(key=lambda c: (c.patch.filename, not c.left, 995 ps_comments.sort(key=lambda c: (c.patch.filename, not c.left,
992 c.lineno, c.date)) 996 c.lineno, c.date))
993 comments += ps_comments 997 comments += ps_comments
994 998
995 if comments: 999 if comments:
996 logging.warn('Publishing %d comments', len(comments)) 1000 logging.warn('Publishing %d comments', len(comments))
997 # Decide who should receive mail 1001 # Decide who should receive mail
998 my_email = db.Email(request.user.email()) 1002 my_email = db.Email(request.user.email())
999 addressees = [db.Email(issue.owner.email())] + issue.reviewers 1003 addressees = [db.Email(issue.owner.email())] + issue.reviewers
1000 if my_email in addressees: 1004 if my_email in addressees:
1001 everyone = addressees[:] 1005 everyone = addressees[:]
1002 if len(addressees) > 1: # Keep it if sending only to yourself 1006 if len(addressees) > 1: # Keep it if sending only to yourself
1003 addressees.remove(my_email) 1007 addressees.remove(my_email)
1004 else: 1008 else:
1005 everyone = addressees + [my_email] 1009 everyone = addressees + [my_email]
1006 details = _get_draft_details(request, comments) 1010 details = _get_draft_details(request, comments)
1007 text = ((message.strip() + '\n\n' + details.strip())).strip() 1011 text = ((message.strip() + '\n\n' + details.strip())).strip()
1008 msg = models.Message(issue=issue, 1012 msg = models.Message(issue=issue,
1009 subject=issue.subject, 1013 subject=issue.subject,
1010 sender=my_email, 1014 sender=my_email,
1011 recipients=everyone, 1015 recipients=everyone,
(...skipping 184 matching lines...) Show 10 above Show 10 below
1196 return respond(request, 'branch_edit.html', 1200 return respond(request, 'branch_edit.html',
1197 {'branch': branch, 'form': form}) 1201 {'branch': branch, 'form': form})
1198 branch.put() 1202 branch.put()
1199 return HttpResponseRedirect('/repos') 1203 return HttpResponseRedirect('/repos')
1200 1204
1201 1205
1202 @login_required 1206 @login_required
1203 def branch_delete(request, branch_id): 1207 def branch_delete(request, branch_id):
1204 """/branch_delete/<branch> - Delete a Branch record.""" 1208 """/branch_delete/<branch> - Delete a Branch record."""
1205 branch = models.Branch.get_by_id(int(branch_id)) 1209 branch = models.Branch.get_by_id(int(branch_id))
1206 if branch.owner != request.user: 1210 if branch.owner != request.user:
1207 return HttpResponseForbidden('You do not own this branch') 1211 return HttpResponseForbidden('You do not own this branch')
1208 repo = branch.repo 1212 repo = branch.repo
1209 branch.delete() 1213 branch.delete()
1210 num_branches = models.Branch.gql('WHERE repo = :1', repo).count() 1214 num_branches = models.Branch.gql('WHERE repo = :1', repo).count()
1211 if not num_branches: 1215 if not num_branches:
1212 # Even if we don't own the repository? Yes, I think so! Empty 1216 # Even if we don't own the repository? Yes, I think so! Empty
1213 # repositories have no representation on screen. 1217 # repositories have no representation on screen.
1214 repo.delete() 1218 repo.delete()
1215 return HttpResponseRedirect('/repos') 1219 return HttpResponseRedirect('/repos')
1216 1220
1217 1221
1218 ### User Profiles ### 1222 ### User Profiles ###
1219 1223
1220 @login_required 1224 @login_required
1221 def settings(request): 1225 def settings(request):
1222 account = models.Account.get_account_for_user(request.user) 1226 account = models.Account.get_account_for_user(request.user)
1223 if request.method != 'POST': 1227 if request.method != 'POST':
1224 nickname = account.nickname 1228 nickname = account.nickname
1225 form = SettingsForm(initial={'nickname': nickname}) 1229 form = SettingsForm(initial={'nickname': nickname})
1226 return respond(request, 'settings.html', {'form': form}) 1230 return respond(request, 'settings.html', {'form': form})
1227 form = SettingsForm(request.POST) 1231 form = SettingsForm(request.POST)
1228 if form.is_valid(): 1232 if form.is_valid():
1229 nickname = form.cleaned_data['nickname'].strip() 1233 nickname = form.cleaned_data['nickname'].strip()
1230 if not nickname: 1234 if not nickname:
1231 form.errors['nickname'] = ['Your nickname cannot be empty.'] 1235 form.errors['nickname'] = ['Your nickname cannot be empty.']
1232 elif '@' in nickname: 1236 elif '@' in nickname:
1233 form.errors['nickname'] = ['Your nickname cannot contain "@".'] 1237 form.errors['nickname'] = ['Your nickname cannot contain "@".']
1234 elif ',' in nickname: 1238 elif ',' in nickname:
1235 form.errors['nickname'] = ['Your nickname cannot contain ",".'] 1239 form.errors['nickname'] = ['Your nickname cannot contain ",".']
1236 else: 1240 else:
1237 accounts = models.Account.get_accounts_for_nickname(nickname) 1241 accounts = models.Account.get_accounts_for_nickname(nickname)
1238 if nickname != account.nickname and accounts: 1242 if nickname != account.nickname and accounts:
1239 form.errors['nickname'] = ['This nickname is already in use.'] 1243 form.errors['nickname'] = ['This nickname is already in use.']
1240 else: 1244 else:
1241 account.nickname = nickname 1245 account.nickname = nickname
1242 account.put() 1246 account.put()
1243 if not form.is_valid(): 1247 if not form.is_valid():
1244 return respond(request, 'settings.html', {'form': form}) 1248 return respond(request, 'settings.html', {'form': form})
1245 return HttpResponseRedirect('/settings') 1249 return HttpResponseRedirect('/settings')
1250
GvR 2008/05/10 15:44:24 Don't add an extra blank line at the end.
jiayao 2008/05/11 11:33:27 On 2008/05/10 15:44:24, GvR wrote: > Don't add an
OLDNEW

Powered by Google App Engine
This is Rietveld r292