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

Delta Between Two Patch Sets: rietveld/views.py

Issue 776: Allow reviewers to change the reviewers list (Closed) SVN Base: http://rietveld.googlecode.com/svn/trunk/
Left Patch Set: Revert index.yaml Created 3 months, 4 weeks ago
Right Patch Set: Stop emailing the current user if she remove herself from the reviewers list Created 3 months, 4 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:
Left: Side by side diff | Download
Right: Side by side diff | Download
LEFTRIGHT
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 88 matching lines...) Show 10 above Show 10 below
139 url = forms.URLField(required=False, 139 url = forms.URLField(required=False,
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()
GvR 2008/05/11 23:27:41 The order of the fields should be the same as in P
190 reviewers = forms.CharField(required=False, 189 reviewers = forms.CharField(required=False,
191 max_length=1000, 190 max_length=1000,
192 widget=forms.TextInput(attrs={'size': 60})) 191 widget=forms.TextInput(attrs={'size': 60}))
192 send_mail = forms.BooleanField()
193 message = forms.CharField(required=False, 193 message = forms.CharField(required=False,
194 max_length=10000, 194 max_length=10000,
195 widget=forms.Textarea(attrs={'cols': 60})) 195 widget=forms.Textarea(attrs={'cols': 60}))
196 196
197 197
198 class SettingsForm(forms.Form): 198 class SettingsForm(forms.Form):
199 199
200 nickname = forms.CharField(max_length=30) 200 nickname = forms.CharField(max_length=30)
201 201
202 202
203 ### Helper functions ### 203 ### Helper functions ###
204 204
205 205
206 # Counter displayed (by respond()) below) on every page showing how 206 # Counter displayed (by respond()) below) on every page showing how
207 # many requests the current incarnation has handled, not counting 207 # many requests the current incarnation has handled, not counting
208 # redirects. Rendered by templates/base.html. 208 # redirects. Rendered by templates/base.html.
209 counter = 0 209 counter = 0
210 210
211 def respond(request, template, params=None): 211 def respond(request, template, params=None):
212 """Helper to render a response, passing standard stuff to the response. 212 """Helper to render a response, passing standard stuff to the response.
213 213
214 Args: 214 Args:
215 request: The request object. 215 request: The request object.
216 template: The template name; '.html' is appended automatically. 216 template: The template name; '.html' is appended automatically.
217 params: A dict giving the template parameters; modified in-place. 217 params: A dict giving the template parameters; modified in-place.
218 218
219 Returns: 219 Returns:
220 Whatever render_to_response(template, params) returns. 220 Whatever render_to_response(template, params) returns.
221 221
222 Raises: 222 Raises:
223 Whatever render_to_response(template, params) raises. 223 Whatever render_to_response(template, params) raises.
224 """ 224 """
225 global counter 225 global counter
226 counter += 1 226 counter += 1
227 if params is None: 227 if params is None:
228 params = {} 228 params = {}
229 must_choose_nickname = False 229 must_choose_nickname = False
230 if request.user is not None: 230 if request.user is not None:
231 account = models.Account.get_account_for_user(request.user) 231 account = models.Account.get_account_for_user(request.user)
232 delta = account.created - account.modified 232 delta = account.created - account.modified
233 if delta.days < 0: 233 if delta.days < 0:
234 delta = -delta 234 delta = -delta
235 must_choose_nickname = delta.days == 0 and delta.seconds < 2 235 must_choose_nickname = delta.days == 0 and delta.seconds < 2
236 params['request'] = request 236 params['request'] = request
237 params['counter'] = counter 237 params['counter'] = counter
238 params['user'] = request.user 238 params['user'] = request.user
239 params['is_admin'] = request.user_is_admin 239 params['is_admin'] = request.user_is_admin
240 params['is_dev'] = IS_DEV 240 params['is_dev'] = IS_DEV
241 params['sign_in'] = users.create_login_url(request.path) 241 params['sign_in'] = users.create_login_url(request.path)
242 params['sign_out'] = users.create_logout_url(request.path) 242 params['sign_out'] = users.create_logout_url(request.path)
(...skipping 720 matching lines...) Show 10 above Show 10 below
963 tbd = [] # List of things to put() after all is said and done 963 tbd = [] # List of things to put() after all is said and done
964 if request.user == issue.owner: 964 if request.user == issue.owner:
965 subject = form.cleaned_data['subject'] 965 subject = form.cleaned_data['subject']
966 issue.subject = subject 966 issue.subject = subject
967 issue.reviewers = reviewers 967 issue.reviewers = reviewers
968 else: 968 else:
969 subject = issue.subject 969 subject = issue.subject
970 issue.reviewers = reviewers 970 issue.reviewers = reviewers
971 tbd.append(issue) # To update the last modified time 971 tbd.append(issue) # To update the last modified time
972 message = form.cleaned_data['message'].replace('\r\n', '\n') 972 message = form.cleaned_data['message'].replace('\r\n', '\n')
973 send_mail = form.cleaned_data['send_mail'] 973 send_mail = form.cleaned_data['send_mail']
974 comments = [] 974 comments = []
975 975
976 # XXX Should request all drafts for this issue once, now we can. 976 # XXX Should request all drafts for this issue once, now we can.
977 for patchset in issue.patchset_set.order('created'): 977 for patchset in issue.patchset_set.order('created'):
978 ## ps_comments = list(models.Comment.gql( 978 ## ps_comments = list(models.Comment.gql(
979 ## 'WHERE ANCESTOR IS :1 AND author = :2 AND draft = TRUE', 979 ## 'WHERE ANCESTOR IS :1 AND author = :2 AND draft = TRUE',
980 ## patchset, request.user)) 980 ## patchset, request.user))
981 # XXX Somehow the index broke, do without it 981 # XXX Somehow the index broke, do without it
982 ps_comments = [c for c in 982 ps_comments = [c for c in
983 models.Comment.gql('WHERE ANCESTOR IS :1', patchset) 983 models.Comment.gql('WHERE ANCESTOR IS :1', patchset)
984 if c.draft and c.author == request.user] 984 if c.draft and c.author == request.user]
985 # XXX End 985 # XXX End
986 if ps_comments: 986 if ps_comments:
987 patches = dict((p.key(), p) for p in patchset.patch_set) 987 patches = dict((p.key(), p) for p in patchset.patch_set)
988 for p in patches.itervalues(): 988 for p in patches.itervalues():
989 p.patchset = patchset 989 p.patchset = patchset
990 for c in ps_comments: 990 for c in ps_comments:
991 c.draft = False 991 c.draft = False
992 # XXX Using internal knowledge about db package: the key for 992 # XXX Using internal knowledge about db package: the key for
993 # reference property foo is stored as _foo. 993 # reference property foo is stored as _foo.
994 pkey = getattr(c, '_patch', None) 994 pkey = getattr(c, '_patch', None)
995 if pkey in patches: 995 if pkey in patches:
996 patch = patches[pkey] 996 patch = patches[pkey]
997 c.patch = patch 997 c.patch = patch
998 tbd.append(ps_comments) 998 tbd.append(ps_comments)
999 ps_comments.sort(key=lambda c: (c.patch.filename, not c.left, 999 ps_comments.sort(key=lambda c: (c.patch.filename, not c.left,
1000 c.lineno, c.date)) 1000 c.lineno, c.date))
1001 comments += ps_comments 1001 comments += ps_comments
1002 1002
1003 if comments: 1003 if comments:
1004 logging.warn('Publishing %d comments', len(comments)) 1004 logging.warn('Publishing %d comments', len(comments))
1005 # Decide who should receive mail 1005 # Decide who should receive mail
1006 my_email = db.Email(request.user.email()) 1006 my_email = db.Email(request.user.email())
1007 addressees = [db.Email(issue.owner.email())] + issue.reviewers 1007 addressees = [db.Email(issue.owner.email())] + issue.reviewers
1008 if my_email in addressees: 1008 if my_email in addressees:
1009 everyone = addressees[:] 1009 everyone = addressees[:]
1010 if len(addressees) > 1: # Keep it if sending only to yourself 1010 if len(addressees) > 1: # Keep it if sending only to yourself
1011 addressees.remove(my_email) 1011 addressees.remove(my_email)
1012 else: 1012 else:
1013 everyone = addressees + [my_email] 1013 everyone = addressees
GvR 2008/05/11 23:27:41 What would you think of *not* adding my_email in t
jiayao 2008/05/12 09:36:35 Make sense. It would be confusing if this person r
1014 details = _get_draft_details(request, comments) 1014 details = _get_draft_details(request, comments)
1015 text = ((message.strip() + '\n\n' + details.strip())).strip() 1015 text = ((message.strip() + '\n\n' + details.strip())).strip()
1016 msg = models.Message(issue=issue, 1016 msg = models.Message(issue=issue,
1017 subject=issue.subject, 1017 subject=issue.subject,
1018 sender=my_email, 1018 sender=my_email,
1019 recipients=everyone, 1019 recipients=everyone,
1020 text=db.Text(text), 1020 text=db.Text(text),
1021 parent=issue) 1021 parent=issue)
1022 tbd.append(msg) 1022 tbd.append(msg)
1023 1023
1024 if send_mail: 1024 if send_mail:
1025 url = request.build_absolute_uri('/%s' % issue.key().id()) 1025 url = request.build_absolute_uri('/%s' % issue.key().id())
1026 addressees_nicknames = ", ".join(library.nickname(addressee, True) 1026 addressees_nicknames = ", ".join(library.nickname(addressee, True)
1027 for addressee in addressees) 1027 for addressee in addressees)
1028 my_nickname = library.nickname(request.user, True) 1028 my_nickname = library.nickname(request.user, True)
1029 addressees = ', '.join(addressees) 1029 addressees = ', '.join(addressees)
1030 description = (issue.description or '').replace('\r\n', '\n') 1030 description = (issue.description or '').replace('\r\n', '\n')
1031 home = request.build_absolute_uri('/') 1031 home = request.build_absolute_uri('/')
1032 body = PUBLISH_MAIL_TEMPLATE % (addressees_nicknames, my_nickname, 1032 body = PUBLISH_MAIL_TEMPLATE % (addressees_nicknames, my_nickname,
1033 url, message, 1033 url, message,
1034 details, description, home) 1034 details, description, home)
1035 logging.warn('Mail: to=%s; cc=%s', addressees, my_email) 1035 logging.warn('Mail: to=%s; cc=%s', addressees, my_email)
1036 mail.send_mail(sender=SENDER, 1036 mail.send_mail(sender=SENDER,
1037 to=_encode_safely(addressees), 1037 to=_encode_safely(addressees),
1038 subject=_encode_safely('Re: ' + subject), 1038 subject=_encode_safely('Re: ' + subject),
1039 body=_encode_safely(body), 1039 body=_encode_safely(body),
1040 cc=_encode_safely(my_email), 1040 cc=_encode_safely(my_email),
1041 reply_to=_encode_safely(', '.join(everyone))) 1041 reply_to=_encode_safely(', '.join(everyone)))
1042 1042
1043 for obj in tbd: 1043 for obj in tbd:
1044 db.put(obj) 1044 db.put(obj)
1045 return HttpResponseRedirect('/%s' % issue.key().id()) 1045 return HttpResponseRedirect('/%s' % issue.key().id())
1046 1046
1047 1047
1048 def _encode_safely(s): 1048 def _encode_safely(s):
1049 """Helper to turn a unicode string into 8-bit bytes.""" 1049 """Helper to turn a unicode string into 8-bit bytes."""
1050 if isinstance(s, unicode): 1050 if isinstance(s, unicode):
1051 s = s.encode('utf-8') 1051 s = s.encode('utf-8')
1052 return s 1052 return s
1053 1053
1054 1054
1055 def _get_draft_details(request, comments): 1055 def _get_draft_details(request, comments):
1056 """Helper to display comments with context in the email message.""" 1056 """Helper to display comments with context in the email message."""
1057 last_key = None 1057 last_key = None
1058 output = [] 1058 output = []
1059 linecache = {} # Maps (c.patch.filename, c.left) to list of lines 1059 linecache = {} # Maps (c.patch.filename, c.left) to list of lines
1060 modified_patches = [] 1060 modified_patches = []
1061 for c in comments: 1061 for c in comments:
1062 if (c.patch.filename, c.left) != last_key: 1062 if (c.patch.filename, c.left) != last_key:
1063 url = request.build_absolute_uri('/%d/diff/%d/%d' % 1063 url = request.build_absolute_uri('/%d/diff/%d/%d' %
(...skipping 140 matching lines...) Show 10 above Show 10 below
1204 return respond(request, 'branch_edit.html', 1204 return respond(request, 'branch_edit.html',
1205 {'branch': branch, 'form': form}) 1205 {'branch': branch, 'form': form})
1206 branch.put() 1206 branch.put()
1207 return HttpResponseRedirect('/repos') 1207 return HttpResponseRedirect('/repos')
1208 1208
1209 1209
1210 @login_required 1210 @login_required
1211 def branch_delete(request, branch_id): 1211 def branch_delete(request, branch_id):
1212 """/branch_delete/<branch> - Delete a Branch record.""" 1212 """/branch_delete/<branch> - Delete a Branch record."""
1213 branch = models.Branch.get_by_id(int(branch_id)) 1213 branch = models.Branch.get_by_id(int(branch_id))
1214 if branch.owner != request.user: 1214 if branch.owner != request.user:
1215 return HttpResponseForbidden('You do not own this branch') 1215 return HttpResponseForbidden('You do not own this branch')
1216 repo = branch.repo 1216 repo = branch.repo
1217 branch.delete() 1217 branch.delete()
1218 num_branches = models.Branch.gql('WHERE repo = :1', repo).count() 1218 num_branches = models.Branch.gql('WHERE repo = :1', repo).count()
1219 if not num_branches: 1219 if not num_branches:
1220 # Even if we don't own the repository? Yes, I think so! Empty 1220 # Even if we don't own the repository? Yes, I think so! Empty
1221 # repositories have no representation on screen. 1221 # repositories have no representation on screen.
1222 repo.delete() 1222 repo.delete()
1223 return HttpResponseRedirect('/repos') 1223 return HttpResponseRedirect('/repos')
1224 1224
1225 1225
1226 ### User Profiles ### 1226 ### User Profiles ###
1227 1227
1228 @login_required 1228 @login_required
1229 def settings(request): 1229 def settings(request):
1230 account = models.Account.get_account_for_user(request.user) 1230 account = models.Account.get_account_for_user(request.user)
1231 if request.method != 'POST': 1231 if request.method != 'POST':
1232 nickname = account.nickname 1232 nickname = account.nickname
1233 form = SettingsForm(initial={'nickname': nickname}) 1233 form = SettingsForm(initial={'nickname': nickname})
1234 return respond(request, 'settings.html', {'form': form}) 1234 return respond(request, 'settings.html', {'form': form})
1235 form = SettingsForm(request.POST) 1235 form = SettingsForm(request.POST)
1236 if form.is_valid(): 1236 if form.is_valid():
1237 nickname = form.cleaned_data['nickname'].strip() 1237 nickname = form.cleaned_data['nickname'].strip()
1238 if not nickname: 1238 if not nickname:
1239 form.errors['nickname'] = ['Your nickname cannot be empty.'] 1239 form.errors['nickname'] = ['Your nickname cannot be empty.']
1240 elif '@' in nickname: 1240 elif '@' in nickname:
1241 form.errors['nickname'] = ['Your nickname cannot contain "@".'] 1241 form.errors['nickname'] = ['Your nickname cannot contain "@".']
1242 elif ',' in nickname: 1242 elif ',' in nickname:
1243 form.errors['nickname'] = ['Your nickname cannot contain ",".'] 1243 form.errors['nickname'] = ['Your nickname cannot contain ",".']
1244 else: 1244 else:
1245 accounts = models.Account.get_accounts_for_nickname(nickname) 1245 accounts = models.Account.get_accounts_for_nickname(nickname)
1246 if nickname != account.nickname and accounts: 1246 if nickname != account.nickname and accounts:
1247 form.errors['nickname'] = ['This nickname is already in use.'] 1247 form.errors['nickname'] = ['This nickname is already in use.']
1248 else: 1248 else:
1249 account.nickname = nickname 1249 account.nickname = nickname
1250 account.put() 1250 account.put()
1251 if not form.is_valid(): 1251 if not form.is_valid():
1252 return respond(request, 'settings.html', {'form': form}) 1252 return respond(request, 'settings.html', {'form': form})
1253 return HttpResponseRedirect('/settings') 1253 return HttpResponseRedirect('/settings')
LEFTRIGHT

Powered by Google App Engine
This is Rietveld r305