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

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: Revert index.yaml 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 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() 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,
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 285 matching lines...) Show 10 above Show 10 below
525 return None 528 return None
526 if fetch_result.status_code != 200: 529 if fetch_result.status_code != 200:
527 form.errors['url'] = ['HTTP status code %s' % fetch_result.status_code] 530 form.errors['url'] = ['HTTP status code %s' % fetch_result.status_code]
528 return None 531 return None
529 data = db.Blob(fetch_result.content) 532 data = db.Blob(fetch_result.content)
530 533
531 return data, url 534 return data, url
532 535
533 536
534 @issue_owner_required 537 @issue_owner_required
535 def add(request): 538 def add(request):
536 """/<issue>/add - Add a new PatchSet to an existing Issue.""" 539 """/<issue>/add - Add a new PatchSet to an existing Issue."""
537 issue = request.issue 540 issue = request.issue
538 form = AddForm(request.POST, request.FILES) 541 form = AddForm(request.POST, request.FILES)
539 if not add_patchset_from_form(request, issue, form): 542 if not add_patchset_from_form(request, issue, form):
540 return show(request, issue.key().id(), form) 543 return show(request, issue.key().id(), form)
541 return HttpResponseRedirect('/%s' % issue.key().id()) 544 return HttpResponseRedirect('/%s' % issue.key().id())
542 545
543 546
544 def add_patchset_from_form(request, issue, form, message_key='message'): 547 def add_patchset_from_form(request, issue, form, message_key='message'):
545 """Helper for add() and upload().""" 548 """Helper for add() and upload()."""
546 # TODO(guido): use a transaction like in _make_new(); may be share more code? 549 # TODO(guido): use a transaction like in _make_new(); may be share more code?
547 if form.is_valid(): 550 if form.is_valid():
548 data_url = _get_data_url(form) 551 data_url = _get_data_url(form)
549 if not form.is_valid(): 552 if not form.is_valid():
550 return False 553 return False
551 data, url = data_url 554 data, url = data_url
552 message = form.cleaned_data[message_key] 555 message = form.cleaned_data[message_key]
553 patchset = models.PatchSet(issue=issue, message=message, data=data, url=url, 556 patchset = models.PatchSet(issue=issue, message=message, data=data, url=url,
554 base=issue.base, owner=request.user, parent=issue) 557 base=issue.base, owner=request.user, parent=issue)
555 patchset.put() 558 patchset.put()
556 559
557 patches = engine.ParsePatchSet(patchset) 560 patches = engine.ParsePatchSet(patchset)
558 if not patches: 561 if not patches:
559 patchset.delete() 562 patchset.delete()
560 errkey = url and 'url' or 'data' 563 errkey = url and 'url' or 'data'
561 form.errors[errkey] = ['Patch set contains no recognizable patches'] 564 form.errors[errkey] = ['Patch set contains no recognizable patches']
562 return False 565 return False
563 db.put(patches) 566 db.put(patches)
564 issue.put() # To update last modified time 567 issue.put() # To update last modified time
565 return True 568 return True
566 569
567 570
568 def _get_reviewers(form): 571 def _get_reviewers(form):
569 """Helper to return the list of reviewers, or None for error.""" 572 """Helper to return the list of reviewers, or None for error."""
570 reviewers = [] 573 reviewers = []
571 raw_reviewers = form.cleaned_data.get('reviewers') 574 raw_reviewers = form.cleaned_data.get('reviewers')
572 if raw_reviewers: 575 if raw_reviewers:
573 for reviewer in raw_reviewers.split(','): 576 for reviewer in raw_reviewers.split(','):
574 reviewer = reviewer.strip() 577 reviewer = reviewer.strip()
575 if reviewer: 578 if reviewer and reviewer not in reviewers:
576 try: 579 try:
577 reviewer = db.Email(reviewer) 580 reviewer = db.Email(reviewer)
578 if reviewer.count('@') != 1: 581 if reviewer.count('@') != 1:
579 raise db.BadValueError('Invalid email address: %s' % reviewer) 582 raise db.BadValueError('Invalid email address: %s' % reviewer)
580 head, tail = reviewer.split('@') 583 head, tail = reviewer.split('@')
581 if '.' not in tail: 584 if '.' not in tail:
582 raise db.BadValueError('Invalid email address: %s' % reviewer) 585 raise db.BadValueError('Invalid email address: %s' % reviewer)
583 except db.BadValueError, err: 586 except db.BadValueError, err:
584 form.errors['reviewers'] = [unicode(err)] 587 form.errors['reviewers'] = [unicode(err)]
585 return None 588 return None
586 reviewers.append(reviewer) 589 reviewers.append(reviewer)
587 return reviewers 590 return reviewers
588 591
589 592
590 593
591 @issue_required 594 @issue_required
592 def show(request, form=AddForm()): 595 def show(request, form=AddForm()):
593 """/<issue> - Show an issue.""" 596 """/<issue> - Show an issue."""
594 issue = request.issue 597 issue = request.issue
595 patchsets = list(issue.patchset_set.order('created')) 598 patchsets = list(issue.patchset_set.order('created'))
596 issue.draft_count = 0 599 issue.draft_count = 0
597 issue.comment_count = 0 600 issue.comment_count = 0
598 for patchset in patchsets: 601 for patchset in patchsets:
599 patchset.patches = list(patchset.patch_set.order('filename')) 602 patchset.patches = list(patchset.patch_set.order('filename'))
600 patchset.n_comments = 0 603 patchset.n_comments = 0
601 for patch in patchset.patches: 604 for patch in patchset.patches:
602 patchset.n_comments += patch.num_comments 605 patchset.n_comments += patch.num_comments
603 issue.comment_count += patchset.n_comments 606 issue.comment_count += patchset.n_comments
604 patchset.n_drafts = 0 607 patchset.n_drafts = 0
605 if request.user: 608 if request.user:
606 for patch in patchset.patches: 609 for patch in patchset.patches:
607 patchset.n_drafts += patch.num_drafts 610 patchset.n_drafts += patch.num_drafts
608 issue.draft_count += patchset.n_drafts 611 issue.draft_count += patchset.n_drafts
609 last_patchset = first_patch = None 612 last_patchset = first_patch = None
610 if patchsets: 613 if patchsets:
611 last_patchset = patchsets[-1] 614 last_patchset = patchsets[-1]
612 if last_patchset.patches: 615 if last_patchset.patches:
613 first_patch = last_patchset.patches[0] 616 first_patch = last_patchset.patches[0]
614 messages = list(issue.message_set.order('date')) 617 messages = list(issue.message_set.order('date'))
615 return respond(request, 'issue.html', 618 return respond(request, 'issue.html',
616 {'issue': issue, 'patchsets': patchsets, 619 {'issue': issue, 'patchsets': patchsets,
617 'messages': messages, 'form': form, 620 'messages': messages, 'form': form,
618 'last_patchset': last_patchset, 621 'last_patchset': last_patchset,
619 'first_patch': first_patch}) 622 'first_patch': first_patch})
620 623
621 624
622 @issue_owner_required 625 @issue_owner_required
623 def edit(request): 626 def edit(request):
624 """/<issue>/edit - Edit an issue.""" 627 """/<issue>/edit - Edit an issue."""
625 issue = request.issue 628 issue = request.issue
(...skipping 269 matching lines...) Show 10 above Show 10 below
895 'ORDER BY date', 898 'ORDER BY date',
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':
948 reviewers = issue.reviewers[:]
949 if request.user != issue.owner and (request.user.email()
950 not in issue.reviewers):
951 reviewers.append(request.user.email())
945 form = form_class(initial={'subject': issue.subject, 952 form = form_class(initial={'subject': issue.subject,
946 'reviewers': ', '.join(issue.reviewers), 953 'reviewers': ', '.join(reviewers),
947 'send_mail': True, 954 'send_mail': True,
948 }) 955 })
949 return respond(request, 'publish.html', {'form': form, 'issue': issue}) 956 return respond(request, 'publish.html', {'form': form, 'issue': issue})
950 957
951 form = form_class(request.POST) 958 form = form_class(request.POST)
952 if form.is_valid(): 959 if form.is_valid():
953 reviewers = _get_reviewers(form) 960 reviewers = _get_reviewers(form)
954 if not form.is_valid(): 961 if not form.is_valid():
955 return respond(request, 'publish.html', {'form': form, 'issue': issue}) 962 return respond(request, 'publish.html', {'form': form, 'issue': issue})
956 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
957 if request.user == issue.owner: 964 if request.user == issue.owner:
958 subject = form.cleaned_data['subject'] 965 subject = form.cleaned_data['subject']
959 issue.subject = subject 966 issue.subject = subject
960 issue.reviewers = reviewers 967 issue.reviewers = reviewers
961 else: 968 else:
962 subject = issue.subject 969 subject = issue.subject
970 issue.reviewers = reviewers
963 tbd.append(issue) # To update the last modified time 971 tbd.append(issue) # To update the last modified time
964 message = form.cleaned_data['message'].replace('\r\n', '\n') 972 message = form.cleaned_data['message'].replace('\r\n', '\n')
965 send_mail = form.cleaned_data['send_mail'] 973 send_mail = form.cleaned_data['send_mail']
966 comments = [] 974 comments = []
967 975
968 # 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.
969 for patchset in issue.patchset_set.order('created'): 977 for patchset in issue.patchset_set.order('created'):
970 ## ps_comments = list(models.Comment.gql( 978 ## ps_comments = list(models.Comment.gql(
971 ## 'WHERE ANCESTOR IS :1 AND author = :2 AND draft = TRUE', 979 ## 'WHERE ANCESTOR IS :1 AND author = :2 AND draft = TRUE',
972 ## patchset, request.user)) 980 ## patchset, request.user))
973 # XXX Somehow the index broke, do without it 981 # XXX Somehow the index broke, do without it
974 ps_comments = [c for c in 982 ps_comments = [c for c in
975 models.Comment.gql('WHERE ANCESTOR IS :1', patchset) 983 models.Comment.gql('WHERE ANCESTOR IS :1', patchset)
976 if c.draft and c.author == request.user] 984 if c.draft and c.author == request.user]
977 # XXX End 985 # XXX End
978 if ps_comments: 986 if ps_comments:
979 patches = dict((p.key(), p) for p in patchset.patch_set) 987 patches = dict((p.key(), p) for p in patchset.patch_set)
980 for p in patches.itervalues(): 988 for p in patches.itervalues():
981 p.patchset = patchset 989 p.patchset = patchset
982 for c in ps_comments: 990 for c in ps_comments:
983 c.draft = False 991 c.draft = False
984 # XXX Using internal knowledge about db package: the key for 992 # XXX Using internal knowledge about db package: the key for
985 # reference property foo is stored as _foo. 993 # reference property foo is stored as _foo.
986 pkey = getattr(c, '_patch', None) 994 pkey = getattr(c, '_patch', None)
987 if pkey in patches: 995 if pkey in patches:
988 patch = patches[pkey] 996 patch = patches[pkey]
989 c.patch = patch 997 c.patch = patch
990 tbd.append(ps_comments) 998 tbd.append(ps_comments)
991 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,
992 c.lineno, c.date)) 1000 c.lineno, c.date))
993 comments += ps_comments 1001 comments += ps_comments
994 1002
995 if comments: 1003 if comments:
996 logging.warn('Publishing %d comments', len(comments)) 1004 logging.warn('Publishing %d comments', len(comments))
997 # Decide who should receive mail 1005 # Decide who should receive mail
998 my_email = db.Email(request.user.email()) 1006 my_email = db.Email(request.user.email())
999 addressees = [db.Email(issue.owner.email())] + issue.reviewers 1007 addressees = [db.Email(issue.owner.email())] + issue.reviewers
1000 if my_email in addressees: 1008 if my_email in addressees:
1001 everyone = addressees[:] 1009 everyone = addressees[:]
1002 if len(addressees) > 1: # Keep it if sending only to yourself 1010 if len(addressees) > 1: # Keep it if sending only to yourself
1003 addressees.remove(my_email) 1011 addressees.remove(my_email)
1004 else: 1012 else:
1005 everyone = addressees + [my_email] 1013 everyone = addressees + [my_email]
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
1006 details = _get_draft_details(request, comments) 1014 details = _get_draft_details(request, comments)
1007 text = ((message.strip() + '\n\n' + details.strip())).strip() 1015 text = ((message.strip() + '\n\n' + details.strip())).strip()
1008 msg = models.Message(issue=issue, 1016 msg = models.Message(issue=issue,
1009 subject=issue.subject, 1017 subject=issue.subject,
1010 sender=my_email, 1018 sender=my_email,
1011 recipients=everyone, 1019 recipients=everyone,
1012 text=db.Text(text), 1020 text=db.Text(text),
1013 parent=issue) 1021 parent=issue)
1014 tbd.append(msg) 1022 tbd.append(msg)
1015 1023
1016 if send_mail: 1024 if send_mail:
1017 url = request.build_absolute_uri('/%s' % issue.key().id()) 1025 url = request.build_absolute_uri('/%s' % issue.key().id())
1018 addressees_nicknames = ", ".join(library.nickname(addressee, True) 1026 addressees_nicknames = ", ".join(library.nickname(addressee, True)
1019 for addressee in addressees) 1027 for addressee in addressees)
1020 my_nickname = library.nickname(request.user, True) 1028 my_nickname = library.nickname(request.user, True)
1021 addressees = ', '.join(addressees) 1029 addressees = ', '.join(addressees)
1022 description = (issue.description or '').replace('\r\n', '\n') 1030 description = (issue.description or '').replace('\r\n', '\n')
1023 home = request.build_absolute_uri('/') 1031 home = request.build_absolute_uri('/')
1024 body = PUBLISH_MAIL_TEMPLATE % (addressees_nicknames, my_nickname, 1032 body = PUBLISH_MAIL_TEMPLATE % (addressees_nicknames, my_nickname,
1025 url, message, 1033 url, message,
1026 details, description, home) 1034 details, description, home)
1027 logging.warn('Mail: to=%s; cc=%s', addressees, my_email) 1035 logging.warn('Mail: to=%s; cc=%s', addressees, my_email)
1028 mail.send_mail(sender=SENDER, 1036 mail.send_mail(sender=SENDER,
1029 to=_encode_safely(addressees), 1037 to=_encode_safely(addressees),
1030 subject=_encode_safely('Re: ' + subject), 1038 subject=_encode_safely('Re: ' + subject),
1031 body=_encode_safely(body), 1039 body=_encode_safely(body),
1032 cc=_encode_safely(my_email), 1040 cc=_encode_safely(my_email),
1033 reply_to=_encode_safely(', '.join(everyone))) 1041 reply_to=_encode_safely(', '.join(everyone)))
1034 1042
1035 for obj in tbd: 1043 for obj in tbd:
1036 db.put(obj) 1044 db.put(obj)
1037 return HttpResponseRedirect('/%s' % issue.key().id()) 1045 return HttpResponseRedirect('/%s' % issue.key().id())
1038 1046
1039 1047
1040 def _encode_safely(s): 1048 def _encode_safely(s):
1041 """Helper to turn a unicode string into 8-bit bytes.""" 1049 """Helper to turn a unicode string into 8-bit bytes."""
1042 if isinstance(s, unicode): 1050 if isinstance(s, unicode):
1043 s = s.encode('utf-8') 1051 s = s.encode('utf-8')
1044 return s 1052 return s
1045 1053
1046 1054
1047 def _get_draft_details(request, comments): 1055 def _get_draft_details(request, comments):
1048 """Helper to display comments with context in the email message.""" 1056 """Helper to display comments with context in the email message."""
1049 last_key = None 1057 last_key = None
1050 output = [] 1058 output = []
1051 linecache = {} # Maps (c.patch.filename, c.left) to list of lines 1059 linecache = {} # Maps (c.patch.filename, c.left) to list of lines
1052 modified_patches = [] 1060 modified_patches = []
1053 for c in comments: 1061 for c in comments:
1054 if (c.patch.filename, c.left) != last_key: 1062 if (c.patch.filename, c.left) != last_key:
1055 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
1196 return respond(request, 'branch_edit.html', 1204 return respond(request, 'branch_edit.html',
1197 {'branch': branch, 'form': form}) 1205 {'branch': branch, 'form': form})
1198 branch.put() 1206 branch.put()
1199 return HttpResponseRedirect('/repos') 1207 return HttpResponseRedirect('/repos')
1200 1208
1201 1209
1202 @login_required 1210 @login_required
1203 def branch_delete(request, branch_id): 1211 def branch_delete(request, branch_id):
1204 """/branch_delete/<branch> - Delete a Branch record.""" 1212 """/branch_delete/<branch> - Delete a Branch record."""
1205 branch = models.Branch.get_by_id(int(branch_id)) 1213 branch = models.Branch.get_by_id(int(branch_id))
1206 if branch.owner != request.user: 1214 if branch.owner != request.user:
1207 return HttpResponseForbidden('You do not own this branch') 1215 return HttpResponseForbidden('You do not own this branch')
1208 repo = branch.repo 1216 repo = branch.repo
1209 branch.delete() 1217 branch.delete()