| # Copyright 2008 Google Inc. |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| |
| """Views for Gerrit. |
| |
| This requires Django 0.97.pre. |
| """ |
| |
| |
| ### Imports ### |
| |
| |
| # Python imports |
| import os |
| import cgi |
| import random |
| import re |
| import logging |
| import binascii |
| import datetime |
| import hashlib |
| import zlib |
| from xml.etree import ElementTree |
| from cStringIO import StringIO |
| |
| # AppEngine imports |
| from google.appengine.api import mail |
| from google.appengine.api import memcache |
| from google.appengine.api import users |
| from google.appengine.ext import db |
| from google.appengine.ext.db import djangoforms |
| |
| # Django imports |
| # TODO(guido): Don't import classes/functions directly. |
| from django import forms |
| from django import http |
| from django.http import HttpResponse, HttpResponseRedirect |
| from django.http import HttpResponseForbidden, HttpResponseNotFound |
| from django.shortcuts import render_to_response |
| import django.template |
| from django.utils import simplejson |
| from django.forms import formsets |
| |
| # Local imports |
| from memcache import Key as MemCacheKey |
| import models |
| import email |
| import engine |
| import library |
| import patching |
| import fields |
| import project |
| import git_models |
| from view_util import * |
| |
| |
| ### Constants ### |
| |
| MAX_ROWS = 1000 |
| |
| |
| ### Helper functions ### |
| |
| |
| def _random_bytes(n): |
| """Helper returning a string of random bytes of given length.""" |
| return ''.join(map(chr, (random.randrange(256) for i in xrange(n)))) |
| |
| |
| ### Request handlers ### |
| |
| |
| def index(request): |
| """/ - Show a list of patches.""" |
| if request.user is None: |
| return all(request) |
| else: |
| return mine(request) |
| |
| |
| DEFAULT_LIMIT = 10 |
| |
| def all(request): |
| """/all - Show a list of up to DEFAULT_LIMIT recent change.""" |
| offset = request.GET.get('offset') |
| if offset: |
| try: |
| offset = int(offset) |
| except: |
| offset = 0 |
| else: |
| offset = max(0, offset) |
| else: |
| offset = 0 |
| limit = request.GET.get('limit') |
| if limit: |
| try: |
| limit = int(limit) |
| except: |
| limit = DEFAULT_LIMIT |
| else: |
| limit = max(1, min(limit, 100)) |
| else: |
| limit = DEFAULT_LIMIT |
| query = db.GqlQuery('SELECT * FROM Change ' |
| 'WHERE closed = FALSE ORDER BY modified DESC') |
| # Fetch one more to see if there should be a 'next' link |
| changes = query.fetch(limit+1, offset) |
| more = bool(changes[limit:]) |
| if more: |
| del changes[limit:] |
| if more: |
| next = '/all?offset=%d&limit=%d' % (offset+limit, limit) |
| else: |
| next = '' |
| if offset > 0: |
| prev = '/all?offset=%d&limit=%d' % (max(0, offset-limit), limit) |
| else: |
| prev = '' |
| newest = '' |
| if offset > limit: |
| newest = '/all?limit=%d' % limit |
| |
| _optimize_draft_counts(changes) |
| _prefetch_names(changes) |
| return respond(request, 'all.html', |
| {'changes': changes, 'limit': limit, |
| 'newest': newest, 'prev': prev, 'next': next, |
| 'first': offset+1, |
| 'last': len(changes) > 1 and offset+len(changes) or None}) |
| |
| |
| def _optimize_draft_counts(changes): |
| """Force _num_drafts to zero for changes that are known to have no drafts. |
| |
| Args: |
| changes: list of model.Change instances. |
| |
| This inspects the drafts attribute of the current user's Account |
| instance, and forces the draft count to zero of those changes in the |
| list that aren't mentioned there. |
| |
| If there is no current user, all draft counts are forced to 0. |
| """ |
| account = models.Account.current_user_account |
| if account is None: |
| change_ids = None |
| else: |
| change_ids = account.drafts |
| for change in changes: |
| if change_ids is None or change.key().id() not in change_ids: |
| change._num_drafts = 0 |
| |
| def _prefetch_names(changes): |
| for c in changes: |
| library.prefetch_names([c.owner]) |
| library.prefetch_names(c.reviewers) |
| library.prefetch_names(c.cc) |
| |
| |
| @login_required |
| def mine(request): |
| """/mine - Show a list of changes created by the current user.""" |
| request.user_to_show = request.user |
| return _show_user(request) |
| |
| def unclaimed_project_memcache_key(user): |
| return "user_unclaimed_projects:%s" % user.email() |
| |
| def all_unclaimed(request): |
| flat_changes = models.gql(models.Change, |
| ' WHERE closed = FALSE' |
| ' AND claimed = FALSE' |
| ' ORDER BY dest_project DESC,' |
| ' modified DESC').fetch(1000) |
| changes = [] |
| c_list = [] |
| last_project_key = None |
| for c in flat_changes: |
| logging.info("c=" + str(c)) |
| k = c.dest_project.key() |
| if k != last_project_key: |
| if c_list: |
| _optimize_draft_counts(c_list) |
| _prefetch_names(c_list) |
| changes.append({ |
| 'name': c.dest_project.name, |
| 'changes': c_list, |
| }) |
| last_project_key = k |
| c_list = [] |
| c_list.append(c) |
| if c_list: |
| _optimize_draft_counts(c_list) |
| _prefetch_names(c_list) |
| changes.append({ |
| 'name': c.dest_project.name, |
| 'changes': c_list, |
| }) |
| vars = { |
| 'projects': changes, |
| } |
| |
| return respond(request, 'unclaimed.html', vars) |
| |
| @login_required |
| def unclaimed(request): |
| """/unclaimed - Show changes with no reviewer listed for user's selected |
| projects.""" |
| def _get_unclaimed_projects(user): |
| memcache_key = unclaimed_project_memcache_key(user) |
| keys = memcache.get(memcache_key) |
| if keys is None: |
| account = models.Account.get_account_for_user(user) |
| keys = account.unclaimed_changes_projects |
| err = memcache.set(memcache_key, keys) |
| result = models.Project.get(keys) |
| if not result: |
| result = [] |
| return result |
| |
| user = request.user |
| changes = [] |
| |
| projects = _get_unclaimed_projects(request.user) |
| for project in projects: |
| c = models.gql(models.Change, |
| ' WHERE closed = FALSE' |
| ' AND claimed = FALSE' |
| ' AND dest_project = :dest_project' |
| ' ORDER BY modified DESC', |
| dest_project=project.key()).fetch(1000) |
| if c: |
| _optimize_draft_counts(c) |
| _prefetch_names(c) |
| changes.append({ |
| 'name': project.name, |
| 'changes': c, |
| }) |
| |
| vars = { |
| 'projects': changes, |
| } |
| |
| return respond(request, 'unclaimed.html', vars) |
| |
| |
| @login_required |
| def starred(request): |
| """/starred - Show a list of changes starred by the current user.""" |
| stars = models.Account.current_user_account.stars |
| if not stars: |
| changes = [] |
| else: |
| changes = [change for change in models.Change.get_by_id(stars) |
| if change is not None] |
| _optimize_draft_counts(changes) |
| _prefetch_names(changes) |
| return respond(request, 'starred.html', {'changes': changes}) |
| |
| |
| @user_key_required |
| def show_user(request): |
| """/user - Show the user's dashboard""" |
| return _show_user(request) |
| |
| @user_key_required |
| def ajax_user_mine(request, offset_str): |
| m = _user_mine(request, request.user_to_show, int(offset_str)) |
| return _respond_paged(request,'change_pagedrow.html', |
| 'change', 'mine', m) |
| |
| @user_key_required |
| def ajax_user_review(request, offset_str): |
| m = _user_review(request, request.user_to_show, int(offset_str)) |
| return _respond_paged(request,'change_pagedrow.html', |
| 'change', 'review', m) |
| |
| @user_key_required |
| def ajax_user_closed(request, offset_str): |
| m = _user_closed(request, request.user_to_show, int(offset_str)) |
| return _respond_paged(request,'change_pagedrow.html', |
| 'change', 'closed', m) |
| |
| def _respond_paged(request, template, new_pfx, old_pfx, vars): |
| return respond(request, template, { |
| new_pfx + '_list': vars[old_pfx + '_list'], |
| new_pfx + '_opos': vars[old_pfx + '_opos'], |
| new_pfx + '_oend': vars[old_pfx + '_oend'], |
| new_pfx + '_prev': vars[old_pfx + '_prev'], |
| new_pfx + '_next': vars[old_pfx + '_next'] |
| }) |
| |
| def _paginate(prefix, n, offset, q): |
| list = q.fetch(n + 1, offset - 1) |
| have = len(list) |
| |
| if have == n + 1: |
| list = list[0:-1] |
| have = n |
| next = offset + n |
| if next >= 1000: |
| next = None |
| else: |
| next = None |
| |
| if offset == 0: |
| prev = None |
| else: |
| prev = offset - n |
| if prev < 0: |
| prev = 0 |
| |
| if next: |
| last = next - 1 |
| else: |
| last = offset + have - 1 |
| |
| for i in list: |
| i.paginate_row_type = prefix |
| |
| return {prefix + '_list': list, |
| prefix + '_opos': offset, |
| prefix + '_oend': last, |
| prefix + '_prev': prev, |
| prefix + '_next': next} |
| |
| |
| def _user_mine(request, user, offset): |
| r = _paginate('mine', 10, offset, |
| models.gql(models.Change, |
| 'WHERE closed = FALSE AND owner = :1' |
| ' ORDER BY modified DESC', |
| user)) |
| _optimize_draft_counts(r['mine_list']) |
| _prefetch_names(r['mine_list']) |
| return r |
| |
| def _user_review(request, user, offset): |
| r = _paginate('review', 10, offset, |
| models.gql(models.Change, |
| 'WHERE closed = FALSE AND reviewers = :1' |
| ' ORDER BY modified DESC', |
| user.email())) |
| _optimize_draft_counts(r['review_list']) |
| _prefetch_names(r['review_list']) |
| return r |
| |
| def _user_closed(request, user, offset): |
| r = _paginate('closed', 10, offset, |
| models.gql(models.Change, |
| 'WHERE closed = TRUE AND owner = :1 AND modified > :2' |
| ' ORDER BY modified DESC', |
| user, |
| datetime.datetime.now() - datetime.timedelta(days=7) |
| )) |
| _optimize_draft_counts(r['closed_list']) |
| _prefetch_names(r['closed_list']) |
| return r |
| |
| def _show_user(request): |
| user = request.user_to_show |
| |
| mine = _user_mine(request, user, 1) |
| review = _user_review(request, user, 1) |
| closed = _user_closed(request, user, 1) |
| |
| vars = {'email': user.email()} |
| vars.update(mine) |
| vars.update(review) |
| vars.update(closed) |
| |
| return respond(request, 'user.html', vars) |
| |
| |
| def _get_emails(form, label): |
| """Helper to return the list of reviewers, or None for error.""" |
| emails = [] |
| raw_emails = form.cleaned_data.get(label) |
| if raw_emails: |
| for email in raw_emails.split(','): |
| email = email.strip().lower() |
| if email and email not in emails: |
| try: |
| email = db.Email(email) |
| if email.count('@') != 1: |
| raise db.BadValueError('Invalid email address: %s' % email) |
| head, tail = email.split('@') |
| if '.' not in tail: |
| raise db.BadValueError('Invalid email address: %s' % email) |
| except db.BadValueError, err: |
| form.errors[label] = [unicode(err)] |
| continue |
| emails.append(email) |
| return emails |
| |
| def _prepare_show_patchset(user, patchset): |
| if user: |
| drafts = list(models.gql(models.Comment, |
| 'WHERE ANCESTOR IS :1' |
| ' AND draft = TRUE' |
| ' AND author = :2', |
| patchset, user)) |
| else: |
| drafts = [] |
| |
| max_rows = 100 |
| if len(patchset.filenames) < max_rows: |
| files = models.gql(models.Patch, |
| 'WHERE patchset = :1 ORDER BY filename', |
| patchset).fetch(max_rows) |
| patchset.n_comments = 0 |
| patchset.n_drafts = len(drafts) |
| patchset.patches = files |
| |
| if drafts: |
| p_bykey = dict() |
| for p in patchset.patches: |
| p_bykey[p.key()] = p |
| p._num_drafts = 0 |
| patchset.n_comments += p.num_comments |
| |
| for d in drafts: |
| if d.parent_key() in p_bykey: |
| p = p_bykey[d.parent_key()] |
| p._num_drafts += 1 |
| else: |
| for p in patchset.patches: |
| p._num_drafts = 0 |
| else: |
| patchset.freaking_huge = True |
| patchset.patches = [] |
| |
| |
| def _restrict_lgtm(lgtm, can_approve): |
| if not can_approve: |
| if lgtm == 'lgtm': |
| return 'yes' |
| elif lgtm == 'reject': |
| return 'no' |
| return lgtm |
| |
| |
| def _restrict_verified(verified, can_verify): |
| if can_verify: |
| return verified |
| else: |
| return False |
| |
| def _map_status(rs, real_approvers, real_deniers, real_verifiers): |
| email = rs.user.email() |
| lgtm = _restrict_lgtm(rs.lgtm, |
| (email in real_approvers) or (email in real_deniers)) |
| verified = _restrict_verified(rs.verified, email in real_verifiers) |
| return { |
| 'user': rs.user, |
| 'lgtm': lgtm, |
| 'verified': verified, |
| } |
| |
| @change_required |
| def show(request, form=None): |
| """/<change> - Show a change.""" |
| change = request.change |
| |
| messages = list(change.message_set.order('date')) |
| patchsets = list(change.patchset_set.order('id')) |
| if not patchsets: |
| return HttpResponse('No patchset available.') |
| |
| last_patchset = patchsets[-1] |
| _prepare_show_patchset(request.user, last_patchset) |
| |
| if last_patchset.patches: |
| first_patch = last_patchset.patches[0] |
| else: |
| first_patch = None |
| |
| depends_on = [ r.patchset.change |
| for r in last_patchset.revision.get_ancestors() |
| if r.patchset ] |
| needed_by = [ r.patchset.change |
| for r in last_patchset.revision.get_children() |
| if r.patchset ] |
| |
| # approvals |
| review_status = change.get_review_status() |
| reviewer_status = models.Change.get_reviewer_status(review_status) |
| ready_to_submit = project.ready_to_submit( |
| change.dest_branch, |
| change.owner, |
| reviewer_status, |
| last_patchset.filenames) |
| |
| # if the owner can lgtm or verify, show her too |
| author_status = { |
| 'lgtm': 'lgtm' if ready_to_submit['owner_auto_lgtm'] else 'abstain', |
| 'verified': ready_to_submit['owner_auto_verify'] |
| } |
| |
| show_dependencies = len(needed_by) > 0 |
| for c in depends_on: |
| if not c.closed: |
| show_dependencies = True |
| if change.closed: |
| show_dependencies = False |
| |
| can_submit = ready_to_submit['can_submit'] |
| if not last_patchset.complete: |
| can_submit = False |
| |
| real_approvers = ready_to_submit['real_approvers'] |
| real_deniers = ready_to_submit['real_deniers'] |
| real_verifiers = ready_to_submit['real_verifiers'] |
| |
| real_review_status = [ |
| _map_status(rs, real_approvers, real_deniers, real_verifiers) |
| for rs in review_status] |
| |
| # If the change isn't ready to submit, don't bother with this because |
| # they can't submit it anyway. |
| if can_submit: |
| user_can_submit = models.AccountGroup.is_user_submitter(request.user) |
| else: |
| user_can_submit = False |
| |
| show_submit_button = ((not change.is_submitted) |
| and ready_to_submit |
| and user_can_submit) |
| |
| _prefetch_names([change]) |
| _prefetch_names(depends_on) |
| _prefetch_names(needed_by) |
| library.prefetch_names(map(lambda s: s.user, review_status)) |
| return respond(request, 'change.html', { |
| 'change': change, |
| 'ready_to_submit': can_submit, |
| 'user_can_submit': user_can_submit, |
| 'show_submit_button': show_submit_button, |
| 'is_approved': ready_to_submit['approved'], |
| 'is_rejected': ready_to_submit['denied'], |
| 'is_verified': ready_to_submit['verified'], |
| 'author_status': author_status, |
| 'review_status': real_review_status, |
| 'depends_on': depends_on, |
| 'needed_by': needed_by, |
| 'show_dependencies': show_dependencies, |
| 'patchsets': patchsets, |
| 'messages': messages, |
| 'last_patchset': last_patchset, |
| 'first_patch': first_patch, |
| 'reply_url': '/%s/publish' % change.key().id(), |
| 'merge_url': '/%s/merge/%s' % (change.key().id(), |
| last_patchset.key().id()), |
| }) |
| |
| |
| @patchset_required |
| def ajax_patchset(request): |
| """/<change>/ajax_patchset/<ps> - Format one patchset.""" |
| change = request.change |
| patchset = request.patchset |
| |
| _prepare_show_patchset(request.user, patchset) |
| return respond(request, 'patchset.html', |
| {'change' : request.change, |
| 'patchset' : request.patchset |
| }) |
| |
| |
| def revision_redirect(request, hash): |
| """/r/<hash> - Redirect to a Change for this git revision, if we can.""" |
| hash = hash.lower() |
| if len(hash) < 40: |
| q = models.gql(models.RevisionId, |
| "WHERE id > :1 AND id < :2", |
| hash, hash.ljust(40, 'z')) |
| else: |
| q = models.gql(models.RevisionId, "WHERE id=:1", hash) |
| revs = q.fetch(2) |
| count = len(revs) |
| |
| if count == 1: |
| rev_id = revs[0] |
| if rev_id.patchset: |
| return HttpResponseRedirect('/%s' % rev_id.patchset.change.key().id()) |
| else: |
| return respond(request, 'change_revision_unknown.html', { 'hash': hash }) |
| |
| if count > 0: |
| return http.HttpResponseServerError("error 500: multiple matches for hash") |
| |
| if len(hash) < 40: |
| q = models.gql(git_models.ReceivedBundle, |
| "WHERE contained_objects > :1" |
| " AND contained_objects < :2", |
| hash, hash.ljust(40, 'z')) |
| else: |
| q = models.gql(git_models.ReceivedBundle, |
| "WHERE contained_objects=:1", |
| hash) |
| rb = q.get() |
| if rb: |
| return respond(request, 'change_revision_uploading.html', |
| { 'hash': hash }) |
| else: |
| return respond(request, 'change_revision_unknown.html', |
| { 'hash': hash }, |
| status = 404) |
| |
| |
| class EditChangeForm(BaseForm): |
| _template = 'edit.html' |
| |
| reviewers = forms.CharField(required=False, |
| max_length=1000, |
| widget=forms.TextInput(attrs={'size': 60})) |
| cc = forms.CharField(required=False, |
| max_length=1000, |
| label = 'CC', |
| widget=forms.TextInput(attrs={'size': 60})) |
| closed = forms.BooleanField(required=False) |
| |
| @classmethod |
| def _init(cls, change): |
| return {'initial': {'reviewers': ', '.join(change.reviewers), |
| 'cc': ', '.join(change.cc), |
| 'closed': change.closed}} |
| |
| def _save(self, cd, change): |
| change.closed = cd['closed'] |
| change.set_reviewers(_get_emails(self, 'reviewers')) |
| change.cc = _get_emails(self, 'cc') |
| change.put() |
| |
| @change_owner_required |
| def edit(request): |
| """/<change>/edit - Edit a change.""" |
| change = request.change |
| id = change.key().id() |
| def done(): |
| return HttpResponseRedirect('/%s' % id) |
| return process_form(request, EditChangeForm, change, done, |
| {'change': change, |
| 'del_url': '/%s/delete' % id}) |
| |
| |
| @change_owner_required |
| @xsrf_required |
| def delete(request): |
| """/<change>/delete - Delete a change. There is no way back.""" |
| change = request.change |
| |
| for ps in models.PatchSet.gql('WHERE ANCESTOR IS :1', change): |
| for rev in models.RevisionId.get_for_patchset(ps): |
| rev.patchset = None |
| rev.put() |
| |
| tbd = [] |
| for cls in [models.Patch, |
| models.PatchSet, |
| models.PatchSetFilenames, |
| models.Comment, |
| models.Message, |
| models.ReviewStatus]: |
| tbd += cls.gql('WHERE ANCESTOR IS :1', change).fetch(50) |
| if len(tbd) > 100: |
| db.delete(tbd) |
| return respond(request, 'delete_loop.html', |
| {'change': change, |
| 'del_url': '/%s/delete' % id}) |
| |
| tbd += [change] |
| db.delete(tbd) |
| return HttpResponseRedirect('/mine') |
| |
| |
| @xsrf_required |
| @patchset_required |
| def merge(request): |
| """/<change>/merge/<patchset> - Submit a change for merge.""" |
| change = request.change |
| patchset = request.patchset |
| patchset.patches = list(patchset.patch_set.order('filename')) |
| |
| if not models.AccountGroup.is_user_submitter(request.user): |
| # The button shouldn't exist if they can't do it, if they somehow |
| # managed to get here, just send them back to the change |
| # (which ought to have the button gone this time). |
| return HttpResponseRedirect('/%d' % change.key().id()) |
| |
| # approvals |
| reviewer_status = models.Change.get_reviewer_status( |
| change.get_review_status()) |
| ready_to_submit = project.ready_to_submit( |
| change.dest_branch, |
| change.owner, |
| reviewer_status, |
| patchset.filenames) |
| |
| if not ready_to_submit['can_submit']: |
| # Again, the button shouldn't have been there in this case. |
| # Just send 'em back to the change page. |
| return HttpResponseRedirect('/%d' % change.key().id()) |
| |
| try: |
| change.submit_merge(patchset) |
| change.put() |
| except models.InvalidSubmitMergeException, why: |
| return HttpResponseForbidden(str(why)) |
| return HttpResponseRedirect('/mine') |
| |
| |
| @patch_required |
| def patch(request): |
| """/<change>/patch/<patchset>/<patch> - View a raw patch.""" |
| return patch_helper(request) |
| |
| |
| def patch_helper(request, nav_type='patch'): |
| """Returns a unified diff. |
| |
| Args: |
| request: Django Request object. |
| nav_type: the navigation used in the url (i.e. patch/diff/diff2). Normally |
| the user looks at either unified or side-by-side diffs at one time, going |
| through all the files in the same mode. However, if side-by-side is not |
| available for some files, we temporarly switch them to unified view, then |
| switch them back when we can. This way they don't miss any files. |
| |
| Returns: |
| Whatever respond() returns. |
| """ |
| _add_next_prev(request.patchset, request.patch) |
| request.patch.nav_type = nav_type |
| parsed_lines = patching.ParsePatchToLines(request.patch.patch_lines) |
| if parsed_lines is None: |
| return HttpResponseNotFound('Can\'t parse the patch') |
| rows = engine.RenderUnifiedTableRows(request, parsed_lines) |
| return respond(request, 'patch.html', |
| {'patch': request.patch, |
| 'patchset': request.patchset, |
| 'rows': rows, |
| 'change': request.change}) |
| |
| |
| @patch_required |
| def download_patch(request): |
| """/download/change<change>_<patchset>_<patch>.diff - Download patch.""" |
| return HttpResponse(request.patch.patch_text, content_type='text/plain') |
| |
| |
| def _get_context_for_user(request): |
| """Returns the context setting for a user. |
| |
| The value is validated against models.CONTEXT_CHOICES. |
| If an invalid value is found, the value is overwritten with |
| models.DEFAULT_CONTEXT. |
| """ |
| if request.user: |
| account = models.Account.current_user_account |
| default_context = account.default_context |
| else: |
| default_context = models.DEFAULT_CONTEXT |
| try: |
| context = int(request.GET.get("context", default_context)) |
| except ValueError: |
| context = default_context |
| if context not in models.CONTEXT_CHOICES: |
| context = models.DEFAULT_CONTEXT |
| return context |
| |
| |
| @patch_required |
| def diff(request): |
| """/<change>/diff/<patchset>/<patch> - View a patch as a side-by-side diff""" |
| patchset = request.patchset |
| patch = request.patch |
| |
| context = _get_context_for_user(request) |
| rows = _get_diff_table_rows(request, patch, context) |
| if isinstance(rows, HttpResponseNotFound): |
| return rows |
| |
| _add_next_prev(patchset, patch) |
| return respond(request, 'diff.html', |
| {'change': request.change, 'patchset': patchset, |
| 'patch': patch, 'rows': rows, |
| 'context': context, 'context_values': models.CONTEXT_CHOICES}) |
| |
| |
| def _get_diff_table_rows(request, patch, context): |
| """Helper function that returns rendered rows for a patch""" |
| chunks = patching.ParsePatchToChunks(patch.patch_lines, |
| patch.filename) |
| if chunks is None: |
| return HttpResponseNotFound('Can\'t parse the patch') |
| |
| return list(engine.RenderDiffTableRows(request, patch.old_lines, |
| chunks, patch, |
| context=context)) |
| |
| |
| @patch_required |
| def diff_skipped_lines(request, id_before, id_after, where): |
| """/<change>/diff/<patchset>/<patch> - Returns a fragment of skipped lines""" |
| patchset = request.patchset |
| patch = request.patch |
| |
| # TODO: allow context = None? |
| rows = _get_diff_table_rows(request, patch, 10000) |
| if isinstance(rows, HttpResponseNotFound): |
| return rows |
| return _get_skipped_lines_response(rows, id_before, id_after, where) |
| |
| |
| def _get_skipped_lines_response(rows, id_before, id_after, where): |
| """Helper function that creates a Response object for skipped lines""" |
| response_rows = [] |
| id_before = int(id_before) |
| id_after = int(id_after) |
| |
| if where == "b": |
| rows.reverse() |
| |
| for row in rows: |
| m = re.match('^<tr( name="hook")? id="pair-(?P<rowcount>\d+)">', row) |
| if m: |
| curr_id = int(m.groupdict().get("rowcount")) |
| if curr_id < id_before or curr_id > id_after: |
| continue |
| if where == "b" and curr_id <= id_after: |
| response_rows.append(row) |
| elif where == "t" and curr_id >= id_before: |
| response_rows.append(row) |
| if len(response_rows) >= 10: |
| break |
| |
| # Create a usable structure for the JS part |
| response = [] |
| dom = ElementTree.parse(StringIO('<div>%s</div>' % "".join(response_rows))) |
| for node in dom.getroot().getchildren(): |
| content = "\n".join([ElementTree.tostring(x) for x in node.getchildren()]) |
| response.append([node.items(), content]) |
| return HttpResponse(simplejson.dumps(response)) |
| |
| |
| def _get_diff2_data(request, ps_left_id, ps_right_id, patch_id, context): |
| """Helper function that returns objects for diff2 views""" |
| ps_left = models.PatchSet.get_by_id(int(ps_left_id), parent=request.change) |
| if ps_left is None: |
| return HttpResponseNotFound('No patch set exists with that id (%s)' % |
| ps_left_id) |
| ps_left.change = request.change |
| ps_right = models.PatchSet.get_by_id(int(ps_right_id), parent=request.change) |
| if ps_right is None: |
| return HttpResponseNotFound('No patch set exists with that id (%s)' % |
| ps_right_id) |
| ps_right.change = request.change |
| patch_right = models.Patch.get_patch(ps_right, patch_id) |
| if patch_right is None: |
| return HttpResponseNotFound('No patch exists with that id (%s/%s)' % |
| (ps_right_id, patch_id)) |
| patch_right.patchset = ps_right |
| # Now find the corresponding patch in ps_left |
| patch_left = models.Patch.gql('WHERE patchset = :1 AND filename = :2', |
| ps_left, patch_right.filename).get() |
| if patch_left is None: |
| return HttpResponseNotFound( |
| "Patch set %s doesn't have a patch with filename %s" % |
| (ps_left_id, patch_right.filename)) |
| |
| rows = engine.RenderDiff2TableRows(request, |
| patch_left.new_lines, patch_left, |
| patch_right.new_lines, patch_right, |
| context=context) |
| rows = list(rows) |
| if rows and rows[-1] is None: |
| del rows[-1] |
| |
| return dict(patch_left=patch_left, ps_left=ps_left, |
| patch_right=patch_right, ps_right=ps_right, |
| rows=rows) |
| |
| |
| @change_required |
| def diff2(request, ps_left_id, ps_right_id, patch_id): |
| """/<change>/diff2/... - View the delta between two different patch sets.""" |
| context = _get_context_for_user(request) |
| data = _get_diff2_data(request, ps_left_id, ps_right_id, patch_id, context) |
| if isinstance(data, HttpResponseNotFound): |
| return data |
| |
| _add_next_prev(data["ps_right"], data["patch_right"]) |
| return respond(request, 'diff2.html', |
| {'change': request.change, |
| 'ps_left': data["ps_left"], |
| 'patch_left': data["patch_left"], |
| 'ps_right': data["ps_right"], |
| 'patch_right': data["patch_right"], |
| 'rows': data["rows"], |
| 'patch_id': patch_id, |
| 'context': context, |
| 'context_values': models.CONTEXT_CHOICES, |
| }) |
| |
| |
| @change_required |
| def diff2_skipped_lines(request, ps_left_id, ps_right_id, patch_id, |
| id_before, id_after, where): |
| """/<change>/diff2/... - Returns a fragment of skipped lines""" |
| data = _get_diff2_data(request, ps_left_id, ps_right_id, patch_id, 10000) |
| if isinstance(data, HttpResponseNotFound): |
| return data |
| return _get_skipped_lines_response(data["rows"], id_before, id_after, where) |
| |
| |
| def _add_next_prev(patchset, patch): |
| """Helper to add .next and .prev attributes to a patch object.""" |
| patch.prev = patch.next = None |
| patches = list(models.Patch.gql("WHERE patchset = :1 ORDER BY filename", |
| patchset)) |
| patchset.patches = patches # Required to render the jump to select. |
| last = None |
| for p in patches: |
| if last is not None: |
| if p.filename == patch.filename: |
| patch.prev = last |
| elif last.filename == patch.filename: |
| patch.next = p |
| break |
| last = p |
| |
| |
| def inline_draft(request): |
| """/inline_draft - Ajax handler to submit an in-line draft comment. |
| |
| This wraps _inline_draft(); all exceptions are logged and cause an |
| abbreviated response indicating something went wrong. |
| """ |
| if request.method != 'POST': |
| return HttpResponse("POST request required.", status=405) |
| try: |
| return _inline_draft(request) |
| except Exception, err: |
| s = '' |
| for k,v in request.POST.iteritems(): |
| s += '\n%s=%s' % (k, v) |
| logging.exception('Exception in inline_draft processing:%s' % s) |
| return HttpResponse('<font color="red">' |
| 'Please report error "%s".' |
| '</font>' |
| % err.__class__.__name__) |
| |
| |
| def _inline_draft(request): |
| """Helper to submit an in-line draft comment. |
| """ |
| # Don't use @login_required; JS doesn't understand redirects. |
| if not request.user: |
| return HttpResponse('<font color="red">Not logged in</font>') |
| |
| if not is_xsrf_ok(request): |
| return HttpResponse('<font color="red">' |
| 'Stale xsrf signature.<br />' |
| 'Please reload the page and try again.' |
| '</font>') |
| |
| snapshot = request.POST.get('snapshot') |
| assert snapshot in ('old', 'new'), repr(snapshot) |
| left = (snapshot == 'old') |
| side = request.POST.get('side') |
| assert side in ('a', 'b'), repr(side) # Display left (a) or right (b) |
| change_id = int(request.POST['change']) |
| change = models.Change.get_by_id(change_id) |
| assert change # XXX |
| patchset_id = request.POST.get('patchset') or request.POST[side == 'a' and 'ps_left' or 'ps_right'] |
| patchset = models.PatchSet.get_by_id(int(patchset_id), parent=change) |
| assert patchset # XXX |
| patch_id = request.POST.get('patch') or request.POST[side == 'a' and 'patch_left' or 'patch_right'] |
| patch = models.Patch.get_patch(patchset, patch_id) |
| assert patch # XXX |
| text = request.POST.get('text') |
| lineno = int(request.POST['lineno']) |
| message_id = request.POST.get('message_id') |
| comment = None |
| if message_id: |
| comment = models.Comment.get_by_key_name(message_id, parent=patch) |
| if comment is None or not comment.draft or comment.author != request.user: |
| comment = None |
| message_id = None |
| if not message_id: |
| # Prefix with 'z' to avoid key names starting with digits. |
| message_id = 'z' + binascii.hexlify(_random_bytes(16)) |
| |
| if not text.rstrip(): |
| if comment is not None: |
| assert comment.draft and comment.author == request.user |
| comment.delete() # Deletion |
| comment = None |
| # Re-query the comment count. |
| models.Account.current_user_account.update_drafts(change) |
| else: |
| if comment is None: |
| comment = models.Comment( |
| patch=patch, |
| key_name=message_id, |
| parent=patch) |
| comment.lineno = lineno |
| comment.left = left |
| comment.author = request.user |
| comment.text = db.Text(text) |
| comment.message_id = message_id |
| comment.put() |
| # The actual count doesn't matter, just that there's at least one. |
| models.Account.current_user_account.update_drafts(change, 1) |
| |
| query = models.Comment.gql( |
| 'WHERE patch = :patch AND lineno = :lineno AND left = :left ' |
| 'ORDER BY date', |
| patch=patch, lineno=lineno, left=left) |
| comments = list(c for c in query if not c.draft or c.author == request.user) |
| if comment is not None and comment.author is None: |
| # Show anonymous draft even though we don't save it |
| comments.append(comment) |
| if not comments: |
| return HttpResponse(' ') |
| for c in comments: |
| c.complete(patch) |
| return render_to_response('inline_comment.html', |
| {'inline_draft_url': '/inline_draft', |
| 'user': request.user, |
| 'patch': patch, |
| 'patchset': patchset, |
| 'change': change, |
| 'comments': comments, |
| 'lineno': lineno, |
| 'snapshot': snapshot, |
| 'side': side}) |
| |
| |
| class PublishCommentsForm(BaseForm): |
| _template = 'publish.html' |
| |
| reviewers = forms.CharField(required=False, |
| max_length=1000, |
| widget=forms.TextInput(attrs={'size': 60})) |
| cc = forms.CharField(required=False, |
| max_length=1000, |
| label = 'CC', |
| widget=forms.TextInput(attrs={'size': 60})) |
| send_mail = forms.BooleanField(required=False) |
| message = forms.CharField(required=False, |
| max_length=10000, |
| widget=forms.Textarea(attrs={'cols': 60})) |
| |
| lgtm = forms.CharField(label='Code review') |
| verified = forms.BooleanField(required=False, |
| label='Verified') |
| |
| |
| |
| def __init__(self, *args, **kwargs): |
| is_initial = kwargs.pop('is_initial', False) |
| self.user_can_approve = kwargs.pop('user_can_approve', False) |
| self.user_can_verify = kwargs.pop('user_can_verify', False) |
| self.user_is_owner = kwargs.pop('user_is_owner', False) |
| |
| BaseForm.__init__(self, *args, **kwargs) |
| |
| if is_initial: |
| # only show the available lgtm options |
| lgtm_field = self.fields.get("lgtm") |
| if self.user_can_approve and not self.user_is_owner: |
| lgtm_field.widget = forms.RadioSelect(choices=models.LGTM_CHOICES) |
| else: |
| lgtm_field.widget = forms.RadioSelect( |
| choices=models.LIMITED_LGTM_CHOICES) |
| |
| # only show verified if the user can do it |
| if not self.user_can_verify or self.user_is_owner: |
| del self.fields['verified'] |
| |
| @classmethod |
| def _init(cls, state): |
| request, change = state |
| user = request.user |
| |
| reviewers = list(change.reviewers) |
| cc = list(change.cc) |
| |
| if user != change.owner and user.email() not in reviewers: |
| reviewers.append(user.email()) |
| if user.email() in cc: |
| cc.remove(user.email()) |
| |
| (user_can_approve,user_can_verify) = project.user_can_approve( |
| request.user, change) |
| |
| # Pick the proper review / verify status |
| review = change.get_review_status(user) |
| if review: |
| lgtm = _restrict_lgtm(review.lgtm, user_can_approve) |
| verified = review.verified |
| else: |
| lgtm = 'abstain' |
| verified = False |
| |
| return {'initial': { |
| 'reviewers': ', '.join(reviewers), |
| 'cc': ', '.join(cc), |
| 'send_mail': True, |
| 'lgtm': lgtm, |
| 'verified': verified |
| }, |
| 'user_can_approve': user_can_approve, |
| 'user_can_verify': user_can_verify, |
| 'user_is_owner': user == change.owner, |
| 'is_initial': True, |
| } |
| |
| def _save(self, cd, state): |
| request, change = state |
| user = request.user |
| |
| tbd, comments = _get_draft_comments(request, change) |
| reviewers = _get_emails(self, 'reviewers') |
| cc = _get_emails(self, 'cc') |
| (self.user_can_approve,self.user_can_verify) = project.user_can_approve( |
| request.user, change) |
| lgtm = _restrict_lgtm(cd.get('lgtm', ''), self.user_can_approve) |
| verified = _restrict_verified(cd.get('verified', False), |
| self.user_can_verify) |
| |
| if user != change.owner: |
| # Owners shouldn't have their own review status as it |
| # is implied that 'lgtm' and verified by them. |
| # |
| review_status = _update_review_status(change, |
| user, |
| lgtm, |
| verified) |
| tbd.append(review_status) |
| |
| change.set_reviewers(reviewers) |
| change.cc = cc |
| change.update_comment_count(len(comments)) |
| |
| msg = _make_comment_message(request, change, lgtm, verified, |
| cd['message'], |
| comments, |
| cd['send_mail']) |
| tbd.append(msg) |
| tbd.append(change) |
| |
| while tbd: |
| db.put(tbd[:50]) |
| tbd = tbd[50:] |
| models.Account.current_user_account.update_drafts(change, 0) |
| |
| @change_required |
| @login_required |
| def publish(request): |
| """ /<change>/publish - Publish draft comments and send mail.""" |
| change = request.change |
| |
| tbd, comments = _get_draft_comments(request, change, True) |
| preview = _get_draft_details(request, comments) |
| |
| def done(): |
| return HttpResponseRedirect('/%s' % change.key().id()) |
| return process_form(request, PublishCommentsForm, (request, change), done, |
| {'change': change, |
| 'preview' : preview}) |
| |
| |
| def _update_review_status(change, user, lgtm, verified): |
| """Creates / updates the ReviewStatus for a user, returns that object.""" |
| review = change.set_review_status(user) |
| review.lgtm = lgtm |
| review.verified = verified |
| return review |
| |
| |
| |
| def _encode_safely(s): |
| """Helper to turn a unicode string into 8-bit bytes.""" |
| if isinstance(s, unicode): |
| s = s.encode('utf-8') |
| return s |
| |
| |
| def _get_draft_comments(request, change, preview=False): |
| """Helper to return objects to put() and a list of draft comments. |
| |
| If preview is True, the list of objects to put() is empty to avoid changes |
| to the datastore. |
| |
| Args: |
| request: Django Request object. |
| change: Change instance. |
| preview: Preview flag (default: False). |
| |
| Returns: |
| 2-tuple (put_objects, comments). |
| """ |
| comments = [] |
| tbd = [] |
| dps = dict() |
| |
| # XXX Should request all drafts for this change once, now we can. |
| for patchset in change.patchset_set.order('id'): |
| ps_comments = list(models.Comment.gql( |
| 'WHERE ANCESTOR IS :1 AND author = :2 AND draft = TRUE', |
| patchset, request.user)) |
| if ps_comments: |
| patches = dict((p.key(), p) for p in patchset.patch_set) |
| for p in patches.itervalues(): |
| p.patchset = patchset |
| for c in ps_comments: |
| c.draft = False |
| # XXX Using internal knowledge about db package: the key for |
| # reference property foo is stored as _foo. |
| pkey = getattr(c, '_patch', None) |
| if pkey in patches: |
| patch = patches[pkey] |
| c.patch = patch |
| if pkey not in dps: |
| dps[pkey] = c.patch |
| c.patch.update_comment_count(1) |
| if not preview: |
| tbd += ps_comments |
| ps_comments.sort(key=lambda c: (c.patch.filename, not c.left, |
| c.lineno, c.date)) |
| comments += ps_comments |
| if not preview: |
| tbd += dps.values() |
| return tbd, comments |
| |
| FILE_LINE = '======================================================================' |
| COMMENT_LINE = '------------------------------' |
| |
| def _get_draft_details(request, comments): |
| """Helper to display comments with context in the email message.""" |
| last_key = None |
| output = [] |
| linecache = {} # Maps (c.patch.filename, c.left) to list of lines |
| for c in comments: |
| if (c.patch.filename, c.left) != last_key: |
| if not last_key is None: |
| output.append('%s\n' % FILE_LINE) |
| url = request.build_absolute_uri('/%d/diff/%d/%s' % |
| (request.change.key().id(), |
| c.patch.patchset.key().id(), |
| c.patch.id)) |
| output.append('\n%s\n%s\nFile %s:' % (FILE_LINE, url, c.patch.filename)) |
| last_key = (c.patch.filename, c.left) |
| patch = c.patch |
| if c.left: |
| linecache[last_key] = patch.old_lines |
| else: |
| linecache[last_key] = patch.new_lines |
| file_lines = linecache.get(last_key, ()) |
| context = '' |
| if 1 <= c.lineno <= len(file_lines): |
| context = file_lines[c.lineno - 1].strip() |
| url = request.build_absolute_uri('/%d/diff/%d/%s#%scode%d' % |
| (request.change.key().id(), |
| c.patch.patchset.key().id(), |
| c.patch.id, |
| c.left and "old" or "new", |
| c.lineno)) |
| output.append('%s\nLine %d: %s\n%s' % (COMMENT_LINE, c.lineno, |
| context, c.text.rstrip())) |
| if not last_key is None: |
| output.append('%s\n' % FILE_LINE) |
| return '\n'.join(output) |
| |
| def _make_comment_message(request, change, lgtm, verified, message, |
| comments=None, send_mail=False): |
| """Helper to create a Message instance and optionally send an email.""" |
| # Decide who should receive mail |
| my_email = db.Email(request.user.email()) |
| to = [db.Email(change.owner.email())] + change.reviewers |
| cc = change.cc[:] |
| reply_to = to + cc |
| if my_email in to and len(to) > 1: # send_mail() wants a non-empty to list |
| to.remove(my_email) |
| if my_email in cc: |
| cc.remove(my_email) |
| subject = email.make_change_subject(change) |
| if comments: |
| details = _get_draft_details(request, comments) |
| else: |
| details = '' |
| prefix = '' |
| if lgtm: |
| prefix = prefix + [y for (x,y) in models.LGTM_CHOICES |
| if x == lgtm][0] + '\n' |
| if verified: |
| prefix = prefix + 'Verified.\n' |
| if prefix: |
| prefix = prefix + '\n' |
| message = message.replace('\r\n', '\n') |
| message = prefix + message |
| text = ((message.strip() + '\n\n' + details.strip())).strip() |
| msg = models.Message(change=change, |
| subject=subject, |
| sender=my_email, |
| recipients=reply_to, |
| text=db.Text(text), |
| parent=change) |
| |
| if send_mail: |
| to_users = set([change.owner] + change.reviewers + cc) |
| template_args = { |
| 'message': message, |
| 'details': details, |
| } |
| email.send_change_message(request, change, |
| 'mails/comment.txt', template_args) |
| |
| return msg |
| |
| |
| @xsrf_required |
| @posted_change_required |
| def star(request): |
| account = models.Account.current_user_account |
| if account.stars is None: |
| account.stars = [] |
| id = request.change.key().id() |
| if id not in account.stars: |
| account.stars.append(id) |
| account.put() |
| return respond(request, 'change_star.html', {'change': request.change}) |
| |
| |
| @xsrf_required |
| @posted_change_required |
| def unstar(request): |
| account = models.Account.current_user_account |
| if account.stars is None: |
| account.stars = [] |
| id = request.change.key().id() |
| if id in account.stars: |
| account.stars[:] = [i for i in account.stars if i != id] |
| account.put() |
| return respond(request, 'change_star.html', {'change': request.change}) |
| |
| |
| @gae_admin_required |
| def download_bundle(request, bundle_id, segment_id): |
| """/download/bundle(\d+)_(\d+) - get a bundle segment""" |
| rb = git_models.ReceivedBundle.get_by_id(int(bundle_id)) |
| if not rb: |
| return HttpResponseNotFound('No bundle exists with that id (%s)' % bundle_id) |
| seg = rb.get_segment(int(segment_id)) |
| if not seg: |
| return HttpResponseNotFound('No segment %s in bundle %s' % (segment_id,bundle_id)) |
| return HttpResponse(seg.bundle_data, |
| content_type='application/x-git-bundle-segment') |
| |
| |
| ### Administration ### |
| |
| @project_owner_or_admin_required |
| def admin(request): |
| """/admin - user & other settings""" |
| return respond(request, 'admin.html', {}) |
| |
| @gae_admin_required |
| def admin_settings(request): |
| settings = models.Settings.get_settings() |
| return respond(request, 'admin_settings.html', { |
| 'settings': settings, |
| 'from_email_test_xsrf': xsrf_for('/admin/settings/from_email_test') |
| }) |
| |
| class AdminSettingsAnalyticsForm(BaseForm): |
| _template = 'admin_settings_analytics.html' |
| |
| analytics = forms.CharField(required=False, max_length=20, |
| widget=forms.TextInput(attrs={'size': 20})) |
| |
| @classmethod |
| def _init(cls, state): |
| settings = models.Settings.get_settings() |
| return {'initial': { |
| 'analytics': settings.analytics, |
| } |
| } |
| |
| def _save(self, cd, change): |
| settings = models.Settings.get_settings() |
| settings.analytics = cd['analytics'] |
| settings.put() |
| |
| @gae_admin_required |
| def admin_settings_analytics(request): |
| def done(): |
| return HttpResponseRedirect('/admin/settings') |
| return process_form(request, AdminSettingsAnalyticsForm, None, done) |
| |
| class AdminSettingsFromEmailForm(BaseForm): |
| _template = 'admin_settings_from_email.html' |
| |
| from_email = forms.CharField(required=False, |
| max_length=60, |
| widget=forms.TextInput(attrs={'size': 60})) |
| |
| @classmethod |
| def _init(cls, state): |
| settings = models.Settings.get_settings() |
| return {'initial': { |
| 'from_email': settings.from_email, |
| } |
| } |
| |
| def _save(self, cd, change): |
| settings = models.Settings.get_settings() |
| settings.from_email = cd['from_email'] |
| settings.put() |
| |
| @gae_admin_required |
| def admin_settings_from_email(request): |
| def done(): |
| return HttpResponseRedirect('/admin/settings') |
| return process_form(request, AdminSettingsFromEmailForm, None, done) |
| |
| @gae_admin_required |
| def admin_settings_from_email_test(request): |
| if request.method == 'POST': |
| if is_xsrf_ok(request, xsrf=request.POST.get('xsrf')): |
| address = email.get_default_sender() |
| try: |
| mail.check_email_valid(address, 'blah') |
| email.send(None, [models.Account.current_user_account], 'test', |
| 'mails/from_email_test.txt', None) |
| status = 'email sent' |
| except mail.InvalidEmailError: |
| status = 'invalid email address' |
| return respond(request, 'admin_settings_from_email_test.html', { |
| 'status': status, |
| 'address': address |
| }) |
| return HttpResponse('<font color="red">Ich don\'t think so!</font>') |
| |
| |
| |
| ### User Profiles ### |
| |
| def validate_real_name(real_name): |
| """Returns None if real_name is fine and an error message otherwise.""" |
| if not real_name: |
| return 'Name cannot be empty.' |
| elif real_name == 'me': |
| return 'Of course, you are what you are. But \'me\' is for everyone.' |
| else: |
| return None |
| |
| |
| @user_key_required |
| def user_popup(request): |
| """/user_popup - Pop up to show the user info.""" |
| try: |
| user = request.user_to_show |
| def gen(): |
| account = models.Account.get_account_for_user(user) |
| return render_to_response('user_popup.html', {'account': account}) |
| return MemCacheKey('user_popup:' + user.email(), 300).get(gen) |
| except Exception, err: |
| logging.exception('Exception in user_popup processing:') |
| return HttpResponse( |
| '<font color="red">Error: %s; please report!</font>' |
| % err.__class__.__name__) |
| |
| |
| class AdminDataStoreDeleteForm(BaseForm): |
| _template = 'admin_datastore_delete.html' |
| |
| really = forms.CharField(required=True) |
| |
| def _save(self, cd, request): |
| if cd['really'] != 'DELETE EVERYTHING': |
| return self |
| |
| max_cnt = 50 |
| rm = [] |
| specials = [] |
| for cls in [models.ApprovalRight, |
| models.Project, |
| models.Branch, |
| models.RevisionId, |
| models.BuildAttempt, |
| models.Change, |
| models.PatchSetFilenames, |
| models.PatchSet, |
| models.Message, |
| models.DeltaContent, |
| models.Patch, |
| models.Comment, |
| models.Bucket, |
| models.ReviewStatus, |
| models.Account, |
| models.AccountGroup, |
| git_models.ReceivedBundleSegment, |
| git_models.ReceivedBundle, |
| ]: |
| all = cls.all().fetch(max_cnt) |
| if cls == models.Account: |
| for a in all: |
| if a.key() == request.account.key(): |
| specials.append(a) |
| else: |
| rm.append(a) |
| elif cls == models.AccountGroup: |
| for a in all: |
| if a.is_auto_group: |
| specials.append(a) |
| else: |
| rm.append(a) |
| else: |
| rm.extend(all) |
| if len(rm) >= max_cnt: |
| break |
| |
| if rm: |
| # Delete this block of data and continue via |
| # a JavaScript reload in the browser. |
| # |
| db.delete(rm) |
| return self |
| |
| # We are done with the bulk of the content, but we need |
| # to clean up a few special objects. |
| # |
| library._user_cache.clear() |
| models.Settings._Key.clear() |
| |
| specials.extend(models.Settings.all().fetch(100)) |
| db.delete(specials) |
| return None |
| |
| @gae_admin_required |
| def admin_datastore_delete(request): |
| def done(): |
| return HttpResponseRedirect('/') |
| return process_form(request, AdminDataStoreDeleteForm, request, done) |
| |
| |
| class AdminDataStoreUpgradeForm(BaseForm): |
| _template = 'admin_datastore_upgrade.html' |
| |
| really = forms.CharField(required=True) |
| |
| def _save(self, cd, request): |
| if cd['really'] != 'UPGRADE': |
| return self |
| return None |
| |
| @gae_admin_required |
| def admin_datastore_upgrade(request): |
| def done(): |
| return HttpResponseRedirect('/') |
| return process_form(request, AdminDataStoreUpgradeForm, request, done) |