<commit_before># 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 Rietveld.

This requires Django 0.97.pre.
"""


### Imports ###


# Python imports
import os
import cgi
import random
import re
import logging
import binascii
import datetime
import urllib
import md5
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.api import urlfetch
from google.appengine.ext import db
from google.appengine.ext.db import djangoforms

# DeadlineExceededError can live in two different places
# TODO(guido): simplify once this is fixed.
try:
  # When deployed
  from google.appengine.runtime import DeadlineExceededError
except ImportError:
  # In the development server
  from google.appengine.runtime.apiproxy_errors import DeadlineExceededError

# Django imports
# TODO(guido): Don't import classes/functions directly.
from django import forms
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

# Local imports
import models
import engine
import library
import patching

# Add our own template library.
_library_name = __name__.rsplit('.', 1)[0] + '.library'
if not django.template.libraries.get(_library_name, None):
  django.template.add_to_builtins(_library_name)


### Constants ###


IS_DEV = os.environ['SERVER_SOFTWARE'].startswith('Dev')  # Development server


### Form classes ###


class IssueBaseForm(forms.Form):

  subject = forms.CharField(max_length=100,
                            widget=forms.TextInput(attrs={'size': 60}))
  description = forms.CharField(required=False,
                                max_length=10000,
                                widget=forms.Textarea(attrs={'cols': 60}))
  branch = forms.ChoiceField(required=False, label='SVN base')
  base = forms.CharField(required=False,
                         max_length=1000,
                         widget=forms.TextInput(attrs={'size': 60}))
  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}))

  def set_branch_choices(self, base=None):
    branches = models.Branch.gql('ORDER BY repo, category, name')
    bound_field = self['branch']
    choices = [('', '[See Base]')]
    default = None
    for b in branches:
      pair = (b.key(), '%s - %s - %s' % (b.repo.name, b.category, b.name))
      choices.append(pair)
      if default is None and (base is None or b.url == base):
        default = b.key()
    bound_field.field.choices = choices
    if default is not None:
      self.initial['branch'] = default

  def get_base(self):
    base = self.cleaned_data.get('base')
    if not base:
      key = self.cleaned_data['branch']
      if key:
        branch = models.Branch.get(key)
        if branch is not None:
          base = branch.url
    else:
      try:
        db.Link(base)
      except db.Error:
        self.errors['base'] = ['Invalid base: %s (must be a URL or empty)' %
                               base]
        return None
    if not base:
      self.errors['base'] = ['You must specify a base']
    return base or None


class NewForm(IssueBaseForm):

  data = forms.FileField(required=False)
  url = forms.URLField(required=False,
                       max_length=2083,
                       widget=forms.TextInput(attrs={'size': 60}))
  send_mail = forms.BooleanField(required=False, initial=True)


class AddForm(forms.Form):

  message = forms.CharField(max_length=100,
                            widget=forms.TextInput(attrs={'size': 60}))
  data = forms.FileField(required=False)
  url = forms.URLField(required=False,
                       max_length=2083,
                       widget=forms.TextInput(attrs={'size': 60}))
  reviewers = forms.CharField(max_length=1000, required=False,
                              widget=forms.TextInput(attrs={'size': 60}))
  send_mail = forms.BooleanField(required=False, initial=True)


class UploadForm(forms.Form):

  subject = forms.CharField(max_length=100)
  description = forms.CharField(max_length=10000, required=False)
  content_upload = forms.BooleanField(required=False)
  separate_patches = forms.BooleanField(required=False)
  base = forms.CharField(max_length=2000, required=False)
  data = forms.FileField(required=False)
  issue = forms.IntegerField(required=False)
  description = forms.CharField(max_length=10000, required=False)
  reviewers = forms.CharField(max_length=1000, required=False)
  cc = forms.CharField(max_length=1000, required=False)
  send_mail = forms.BooleanField(required=False)

  def clean_base(self):
    base = self.cleaned_data.get('base')
    if not base and not self.cleaned_data.get('content_upload', False):
      raise forms.ValidationError, 'SVN base is required.'
    elif base:
      try:
        db.Link(base)
      except db.BadValueError:
        raise forms.ValidationError, 'Invalid URL'
    return self.cleaned_data.get('base')

  def get_base(self):
    return self.cleaned_data.get('base')


class UploadContentForm(forms.Form):
  filename = forms.CharField(max_length=255)
  status = forms.CharField(required=False, max_length=20)
  checksum = forms.CharField(max_length=32)
  no_base_file = forms.BooleanField(required=False)

  def clean(self):
    # Check presence of 'data'. We cannot use FileField because
    # it disallows empty files.
    super(UploadContentForm, self).clean()
    if not self.files and not self.files.has_key('data'):
      raise forms.ValidationError, 'No content uploaded.'
    return self.cleaned_data

  def get_uploaded_content(self):
    return self.files['data'].read()


class UploadPatchForm(forms.Form):
  filename = forms.CharField(max_length=255)
  content_upload = forms.BooleanField(required=False)

  def get_uploaded_patch(self):
    return self.files['data'].read()


class EditForm(IssueBaseForm):

  closed = forms.BooleanField(required=False)


class EditLocalBaseForm(forms.Form):
  subject = forms.CharField(max_length=100,
                            widget=forms.TextInput(attrs={'size': 60}))
  description = forms.CharField(required=False,
                                max_length=10000,
                                widget=forms.Textarea(attrs={'cols': 60}))
  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)

  def get_base(self):
    return None


class RepoForm(djangoforms.ModelForm):

  class Meta:
    model = models.Repository
    exclude = ['owner']


class BranchForm(djangoforms.ModelForm):

  class Meta:
    model = models.Branch
    exclude = ['owner']


class PublishForm(forms.Form):

  subject = forms.CharField(max_length=100,
                            widget=forms.TextInput(attrs={'size': 60}))
  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}))
  message_only = forms.BooleanField(required=False,
                                    widget=forms.HiddenInput())


class MiniPublishForm(forms.Form):

  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}))
  message_only = forms.BooleanField(required=False,
                                    widget=forms.HiddenInput())


FORM_CONTEXT_VALUES = [(x, "%d lines" % x) for x in models.CONTEXT_CHOICES]


class SettingsForm(forms.Form):

  nickname = forms.CharField(max_length=30)
  context = forms.IntegerField(widget=forms.Select(choices=FORM_CONTEXT_VALUES),
                               label='Context')


### Helper functions ###


# Counter displayed (by respond()) below) on every page showing how
# many requests the current incarnation has handled, not counting
# redirects.  Rendered by templates/base.html.
counter = 0

def respond(request, template, params=None):
  """Helper to render a response, passing standard stuff to the response.

  Args:
    request: The request object.
    template: The template name; '.html' is appended automatically.
    params: A dict giving the template parameters; modified in-place.

  Returns:
    Whatever render_to_response(template, params) returns.

  Raises:
    Whatever render_to_response(template, params) raises.
  """
  global counter
  counter += 1
  if params is None:
    params = {}
  must_choose_nickname = False
  if request.user is not None:
    account = models.Account.current_user_account
    must_choose_nickname = not account.user_has_selected_nickname()
  params['request'] = request
  params['counter'] = counter
  params['user'] = request.user
  params['is_admin'] = request.user_is_admin
  params['is_dev'] = IS_DEV
  if request.user is None:
    params['sign_in'] = users.create_login_url(request.get_full_path())
  else:
    params['sign_out'] = users.create_logout_url(request.get_full_path())
  params['must_choose_nickname'] = must_choose_nickname
  try:
    return render_to_response(template, params)
  except DeadlineExceededError:
    logging.exception('DeadlineExceededError')
    return HttpResponse('DeadlineExceededError')
  except MemoryError:
    logging.exception('MemoryError')
    return HttpResponse('MemoryError')
  except AssertionError:
    logging.exception('AssertionError')
    return HttpResponse('AssertionError')


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))))


### Decorators for request handlers ###


def login_required(func):
  """Decorator that redirects to the login page if you're not logged in."""
  def login_wrapper(request, *args, **kwds):
    if request.user is None:
      return HttpResponseRedirect(
          users.create_login_url(request.get_full_path()))
    return func(request, *args, **kwds)
  return login_wrapper


def admin_required(func):
  def admin_wrapper(request, *args, **kwds):
    """Decorator that insists that you're logged in as administratior."""
    if request.user is None:
      return HttpResponseRedirect(
          users.create_login_url(request.get_full_path()))
    if not request.user_is_admin:
      return HttpResponseForbidden('You must be admin in for this function')
    return func(request, *args, **kwds)
  return admin_wrapper


def issue_required(func):
  """Decorator that processes the issue_id handler argument."""
  def issue_wrapper(request, issue_id, *args, **kwds):
    issue = models.Issue.get_by_id(int(issue_id))
    if issue is None:
      return HttpResponseNotFound('No issue exists with that id (%s)' %
                                  issue_id)
    request.issue = issue
    return func(request, *args, **kwds)
  return issue_wrapper


def user_key_required(func):
  """Decorator that processes the user handler argument."""
  def user_key_wrapper(request, user_key, *args, **kwds):
    user_key = urllib.unquote(user_key)
    if '@' in user_key:
      request.user_to_show = users.User(user_key)
    else:
      accounts = models.Account.get_accounts_for_nickname(user_key)
      if not accounts:
        logging.info("account not found for nickname %s" % user_key)
        return HttpResponseNotFound('No user found with that key (%s)' %
                                    user_key)
      request.user_to_show = accounts[0].user
    return func(request, *args, **kwds)
  return user_key_wrapper


def issue_owner_required(func):
  """Decorator that processes the issue_id argument and insists you own it."""
  @issue_required
  @login_required
  def issue_owner_wrapper(request, *args, **kwds):
    if request.issue.owner != request.user:
      return HttpResponseForbidden('You do not own this issue')
    return func(request, *args, **kwds)
  return issue_owner_wrapper


def patchset_required(func):
  """Decorator that processes the patchset_id argument."""
  @issue_required
  def patchset_wrapper(request, patchset_id, *args, **kwds):
    patchset = models.PatchSet.get_by_id(int(patchset_id), parent=request.issue)
    if patchset is None:
      return HttpResponseNotFound('No patch set exists with that id (%s)' %
                                  patchset_id)
    patchset.issue = request.issue
    request.patchset = patchset
    return func(request, *args, **kwds)
  return patchset_wrapper


def patch_required(func):
  """Decorator that processes the patch_id argument."""
  @patchset_required
  def patch_wrapper(request, patch_id, *args, **kwds):
    patch = models.Patch.get_by_id(int(patch_id), parent=request.patchset)
    if patch is None:
      return HttpResponseNotFound('No patch exists with that id (%s/%s)' %
                                  (request.patchset.key().id(), patch_id))
    patch.patchset = request.patchset
    request.patch = patch
    return func(request, *args, **kwds)
  return patch_wrapper


### 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 issues."""
  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 Issue '
                      'WHERE closed = FALSE ORDER BY modified DESC')
  # Fetch one more to see if there should be a 'next' link
  issues = query.fetch(limit+1, offset)
  more = bool(issues[limit:])
  if more:
    del issues[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(issues)
  return respond(request, 'all.html',
                 {'issues': issues, 'limit': limit,
                  'newest': newest, 'prev': prev, 'next': next,
                  'first': offset+1,
                  'last': len(issues) > 1 and offset+len(issues) or None})


def _optimize_draft_counts(issues):
  """Force _num_drafts to zero for issues that are known to have no drafts.

  Args:
    issues: list of model.Issue instances.

  This inspects the drafts attribute of the current user's Account
  instance, and forces the draft count to zero of those issues 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:
    issue_ids = None
  else:
    issue_ids = account.drafts
  for issue in issues:
    if issue_ids is None or issue.key().id() not in issue_ids:
      issue._num_drafts = 0


@login_required
def mine(request):
  """/mine - Show a list of issues created by the current user."""
  request.user_to_show = request.user
  return _show_user(request)


@login_required
def starred(request):
  """/starred - Show a list of issues starred by the current user."""
  stars = models.Account.current_user_account.stars
  if not stars:
    issues = []
  else:
    issues = [issue for issue in models.Issue.get_by_id(stars)
                    if issue is not None]
    _optimize_draft_counts(issues)
  return respond(request, 'starred.html', {'issues': issues})


@user_key_required
def show_user(request):
  """/user - Show the user's dashboard"""
  return _show_user(request)


def _show_user(request):
  user = request.user_to_show
  my_issues = list(db.GqlQuery(
      'SELECT * FROM Issue '
      'WHERE closed = FALSE AND owner = :1 ORDER BY modified DESC',
      user))
  review_issues = [issue for issue in db.GqlQuery(
      'SELECT * FROM Issue '
      'WHERE closed = FALSE AND reviewers = :1 ORDER BY modified DESC',
      user.email()) if issue.owner != user]
  closed_issues = list(db.GqlQuery(
      'SELECT * FROM Issue '
      'WHERE closed = TRUE AND modified > :1 AND owner = :2 '
      'ORDER BY modified DESC',
      datetime.datetime.now() - datetime.timedelta(days=7), user))
  _optimize_draft_counts(my_issues + review_issues + closed_issues)
  return respond(request, 'user.html',
                 {'email':user.email(),
                  'my_issues': my_issues,
                  'review_issues': review_issues,
                  'closed_issues': closed_issues})


@login_required
def new(request):
  """/new - Upload a new patch set.

  GET shows a blank form, POST processes it.
  """
  if request.method != 'POST':
    form = NewForm()
    form.set_branch_choices()
    return respond(request, 'new.html', {'form': form})

  form = NewForm(request.POST, request.FILES)
  form.set_branch_choices()
  issue = _make_new(request, form)
  if issue is None:
    return respond(request, 'new.html', {'form': form})
  else:
    return HttpResponseRedirect('/%s' % issue.key().id())


def upload(request):
  """/upload - Like new() or add(), but from the upload.py script.

  This generates a text/plain response.
  """
  if request.user is None:
    if IS_DEV:
      request.user = users.User(request.POST.get('user', '<EMAIL>'))
    else:
      return HttpResponse('Login required', status=401)
  form = UploadForm(request.POST, request.FILES)
  issue = None
  patchset = None
  if form.is_valid():
    issue_id = form.cleaned_data['issue']<commit_msg>Check against old upload.py usage.<commit_after># 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 Rietveld.

This requires Django 0.97.pre.
"""


### Imports ###


# Python imports
import os
import cgi
import random
import re
import logging
import binascii
import datetime
import urllib
import md5
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.api import urlfetch
from google.appengine.ext import db
from google.appengine.ext.db import djangoforms

# DeadlineExceededError can live in two different places
# TODO(guido): simplify once this is fixed.
try:
  # When deployed
  from google.appengine.runtime import DeadlineExceededError
except ImportError:
  # In the development server
  from google.appengine.runtime.apiproxy_errors import DeadlineExceededError

# Django imports
# TODO(guido): Don't import classes/functions directly.
from django import forms
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

# Local imports
import models
import engine
import library
import patching

# Add our own template library.
_library_name = __name__.rsplit('.', 1)[0] + '.library'
if not django.template.libraries.get(_library_name, None):
  django.template.add_to_builtins(_library_name)


### Constants ###


IS_DEV = os.environ['SERVER_SOFTWARE'].startswith('Dev')  # Development server


### Form classes ###


class IssueBaseForm(forms.Form):

  subject = forms.CharField(max_length=100,
                            widget=forms.TextInput(attrs={'size': 60}))
  description = forms.CharField(required=False,
                                max_length=10000,
                                widget=forms.Textarea(attrs={'cols': 60}))
  branch = forms.ChoiceField(required=False, label='SVN base')
  base = forms.CharField(required=False,
                         max_length=1000,
                         widget=forms.TextInput(attrs={'size': 60}))
  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}))

  def set_branch_choices(self, base=None):
    branches = models.Branch.gql('ORDER BY repo, category, name')
    bound_field = self['branch']
    choices = [('', '[See Base]')]
    default = None
    for b in branches:
      pair = (b.key(), '%s - %s - %s' % (b.repo.name, b.category, b.name))
      choices.append(pair)
      if default is None and (base is None or b.url == base):
        default = b.key()
    bound_field.field.choices = choices
    if default is not None:
      self.initial['branch'] = default

  def get_base(self):
    base = self.cleaned_data.get('base')
    if not base:
      key = self.cleaned_data['branch']
      if key:
        branch = models.Branch.get(key)
        if branch is not None:
          base = branch.url
    else:
      try:
        db.Link(base)
      except db.Error:
        self.errors['base'] = ['Invalid base: %s (must be a URL or empty)' %
                               base]
        return None
    if not base:
      self.errors['base'] = ['You must specify a base']
    return base or None


class NewForm(IssueBaseForm):

  data = forms.FileField(required=False)
  url = forms.URLField(required=False,
                       max_length=2083,
                       widget=forms.TextInput(attrs={'size': 60}))
  send_mail = forms.BooleanField(required=False, initial=True)


class AddForm(forms.Form):

  message = forms.CharField(max_length=100,
                            widget=forms.TextInput(attrs={'size': 60}))
  data = forms.FileField(required=False)
  url = forms.URLField(required=False,
                       max_length=2083,
                       widget=forms.TextInput(attrs={'size': 60}))
  reviewers = forms.CharField(max_length=1000, required=False,
                              widget=forms.TextInput(attrs={'size': 60}))
  send_mail = forms.BooleanField(required=False, initial=True)


class UploadForm(forms.Form):

  subject = forms.CharField(max_length=100)
  description = forms.CharField(max_length=10000, required=False)
  content_upload = forms.BooleanField(required=False)
  separate_patches = forms.BooleanField(required=False)
  base = forms.CharField(max_length=2000, required=False)
  data = forms.FileField(required=False)
  issue = forms.IntegerField(required=False)
  description = forms.CharField(max_length=10000, required=False)
  reviewers = forms.CharField(max_length=1000, required=False)
  cc = forms.CharField(max_length=1000, required=False)
  send_mail = forms.BooleanField(required=False)

  def clean_base(self):
    base = self.cleaned_data.get('base')
    if not base and not self.cleaned_data.get('content_upload', False):
      raise forms.ValidationError, 'SVN base is required.'
    elif base:
      try:
        db.Link(base)
      except db.BadValueError:
        raise forms.ValidationError, 'Invalid URL'
    return self.cleaned_data.get('base')

  def get_base(self):
    return self.cleaned_data.get('base')


class UploadContentForm(forms.Form):
  filename = forms.CharField(max_length=255)
  status = forms.CharField(required=False, max_length=20)
  checksum = forms.CharField(max_length=32)
  no_base_file = forms.BooleanField(required=False)

  def clean(self):
    # Check presence of 'data'. We cannot use FileField because
    # it disallows empty files.
    super(UploadContentForm, self).clean()
    if not self.files and not self.files.has_key('data'):
      raise forms.ValidationError, 'No content uploaded.'
    return self.cleaned_data

  def get_uploaded_content(self):
    return self.files['data'].read()


class UploadPatchForm(forms.Form):
  filename = forms.CharField(max_length=255)
  content_upload = forms.BooleanField(required=False)

  def get_uploaded_patch(self):
    return self.files['data'].read()


class EditForm(IssueBaseForm):

  closed = forms.BooleanField(required=False)


class EditLocalBaseForm(forms.Form):
  subject = forms.CharField(max_length=100,
                            widget=forms.TextInput(attrs={'size': 60}))
  description = forms.CharField(required=False,
                                max_length=10000,
                                widget=forms.Textarea(attrs={'cols': 60}))
  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)

  def get_base(self):
    return None


class RepoForm(djangoforms.ModelForm):

  class Meta:
    model = models.Repository
    exclude = ['owner']


class BranchForm(djangoforms.ModelForm):

  class Meta:
    model = models.Branch
    exclude = ['owner']


class PublishForm(forms.Form):

  subject = forms.CharField(max_length=100,
                            widget=forms.TextInput(attrs={'size': 60}))
  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}))
  message_only = forms.BooleanField(required=False,
                                    widget=forms.HiddenInput())


class MiniPublishForm(forms.Form):

  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}))
  message_only = forms.BooleanField(required=False,
                                    widget=forms.HiddenInput())


FORM_CONTEXT_VALUES = [(x, "%d lines" % x) for x in models.CONTEXT_CHOICES]


class SettingsForm(forms.Form):

  nickname = forms.CharField(max_length=30)
  context = forms.IntegerField(widget=forms.Select(choices=FORM_CONTEXT_VALUES),
                               label='Context')


### Helper functions ###


# Counter displayed (by respond()) below) on every page showing how
# many requests the current incarnation has handled, not counting
# redirects.  Rendered by templates/base.html.
counter = 0

def respond(request, template, params=None):
  """Helper to render a response, passing standard stuff to the response.

  Args:
    request: The request object.
    template: The template name; '.html' is appended automatically.
    params: A dict giving the template parameters; modified in-place.

  Returns:
    Whatever render_to_response(template, params) returns.

  Raises:
    Whatever render_to_response(template, params) raises.
  """
  global counter
  counter += 1
  if params is None:
    params = {}
  must_choose_nickname = False
  if request.user is not None:
    account = models.Account.current_user_account
    must_choose_nickname = not account.user_has_selected_nickname()
  params['request'] = request
  params['counter'] = counter
  params['user'] = request.user
  params['is_admin'] = request.user_is_admin
  params['is_dev'] = IS_DEV
  if request.user is None:
    params['sign_in'] = users.create_login_url(request.get_full_path())
  else:
    params['sign_out'] = users.create_logout_url(request.get_full_path())
  params['must_choose_nickname'] = must_choose_nickname
  try:
    return render_to_response(template, params)
  except DeadlineExceededError:
    logging.exception('DeadlineExceededError')
    return HttpResponse('DeadlineExceededError')
  except MemoryError:
    logging.exception('MemoryError')
    return HttpResponse('MemoryError')
  except AssertionError:
    logging.exception('AssertionError')
    return HttpResponse('AssertionError')


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))))


### Decorators for request handlers ###


def login_required(func):
  """Decorator that redirects to the login page if you're not logged in."""
  def login_wrapper(request, *args, **kwds):
    if request.user is None:
      return HttpResponseRedirect(
          users.create_login_url(request.get_full_path()))
    return func(request, *args, **kwds)
  return login_wrapper


def admin_required(func):
  def admin_wrapper(request, *args, **kwds):
    """Decorator that insists that you're logged in as administratior."""
    if request.user is None:
      return HttpResponseRedirect(
          users.create_login_url(request.get_full_path()))
    if not request.user_is_admin:
      return HttpResponseForbidden('You must be admin in for this function')
    return func(request, *args, **kwds)
  return admin_wrapper


def issue_required(func):
  """Decorator that processes the issue_id handler argument."""
  def issue_wrapper(request, issue_id, *args, **kwds):
    issue = models.Issue.get_by_id(int(issue_id))
    if issue is None:
      return HttpResponseNotFound('No issue exists with that id (%s)' %
                                  issue_id)
    request.issue = issue
    return func(request, *args, **kwds)
  return issue_wrapper


def user_key_required(func):
  """Decorator that processes the user handler argument."""
  def user_key_wrapper(request, user_key, *args, **kwds):
    user_key = urllib.unquote(user_key)
    if '@' in user_key:
      request.user_to_show = users.User(user_key)
    else:
      accounts = models.Account.get_accounts_for_nickname(user_key)
      if not accounts:
        logging.info("account not found for nickname %s" % user_key)
        return HttpResponseNotFound('No user found with that key (%s)' %
                                    user_key)
      request.user_to_show = accounts[0].user
    return func(request, *args, **kwds)
  return user_key_wrapper


def issue_owner_required(func):
  """Decorator that processes the issue_id argument and insists you own it."""
  @issue_required
  @login_required
  def issue_owner_wrapper(request, *args, **kwds):
    if request.issue.owner != request.user:
      return HttpResponseForbidden('You do not own this issue')
    return func(request, *args, **kwds)
  return issue_owner_wrapper


def patchset_required(func):
  """Decorator that processes the patchset_id argument."""
  @issue_required
  def patchset_wrapper(request, patchset_id, *args, **kwds):
    patchset = models.PatchSet.get_by_id(int(patchset_id), parent=request.issue)
    if patchset is None:
      return HttpResponseNotFound('No patch set exists with that id (%s)' %
                                  patchset_id)
    patchset.issue = request.issue
    request.patchset = patchset
    return func(request, *args, **kwds)
  return patchset_wrapper


def patch_required(func):
  """Decorator that processes the patch_id argument."""
  @patchset_required
  def patch_wrapper(request, patch_id, *args, **kwds):
    patch = models.Patch.get_by_id(int(patch_id), parent=request.patchset)
    if patch is None:
      return HttpResponseNotFound('No patch exists with that id (%s/%s)' %
                                  (request.patchset.key().id(), patch_id))
    patch.patchset = request.patchset
    request.patch = patch
    return func(request, *args, **kwds)
  return patch_wrapper


### 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 issues."""
  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 Issue '
                      'WHERE closed = FALSE ORDER BY modified DESC')
  # Fetch one more to see if there should be a 'next' link
  issues = query.fetch(limit+1, offset)
  more = bool(issues[limit:])
  if more:
    del issues[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(issues)
  return respond(request, 'all.html',
                 {'issues': issues, 'limit': limit,
                  'newest': newest, 'prev': prev, 'next': next,
                  'first': offset+1,
                  'last': len(issues) > 1 and offset+len(issues) or None})


def _optimize_draft_counts(issues):
  """Force _num_drafts to zero for issues that are known to have no drafts.

  Args:
    issues: list of model.Issue instances.

  This inspects the drafts attribute of the current user's Account
  instance, and forces the draft count to zero of those issues 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:
    issue_ids = None
  else:
    issue_ids = account.drafts
  for issue in issues:
    if issue_ids is None or issue.key().id() not in issue_ids:
      issue._num_drafts = 0


@login_required
def mine(request):
  """/mine - Show a list of issues created by the current user."""
  request.user_to_show = request.user
  return _show_user(request)


@login_required
def starred(request):
  """/starred - Show a list of issues starred by the current user."""
  stars = models.Account.current_user_account.stars
  if not stars:
    issues = []
  else:
    issues = [issue for issue in models.Issue.get_by_id(stars)
                    if issue is not None]
    _optimize_draft_counts(issues)
  return respond(request, 'starred.html', {'issues': issues})


@user_key_required
def show_user(request):
  """/user - Show the user's dashboard"""
  return _show_user(request)


def _show_user(request):
  user = request.user_to_show
  my_issues = list(db.GqlQuery(
      'SELECT * FROM Issue '
      'WHERE closed = FALSE AND owner = :1 ORDER BY modified DESC',
      user))
  review_issues = [issue for issue in db.GqlQuery(
      'SELECT * FROM Issue '
      'WHERE closed = FALSE AND reviewers = :1 ORDER BY modified DESC',
      user.email()) if issue.owner != user]
  closed_issues = list(db.GqlQuery(
      'SELECT * FROM Issue '
      'WHERE closed = TRUE AND modified > :1 AND owner = :2 '
      'ORDER BY modified DESC',
      datetime.datetime.now() - datetime.timedelta(days=7), user))
  _optimize_draft_counts(my_issues + review_issues + closed_issues)
  return respond(request, 'user.html',
                 {'email':user.email(),
                  'my_issues': my_issues,
                  'review_issues': review_issues,
                  'closed_issues': closed_issues})


@login_required
def new(request):
  """/new - Upload a new patch set.

  GET shows a blank form, POST processes it.
  """
  if request.method != 'POST':
    form = NewForm()
    form.set_branch_choices()
    return respond(request, 'new.html', {'form': form})

  form = NewForm(request.POST, request.FILES)
  form.set_branch_choices()
  issue = _make_new(request, form)
  if issue is None:
    return respond(request, 'new.html', {'form': form})
  else:
    return HttpResponseRedirect('/%s' % issue.key().id())


def upload(request):
  """/upload - Like new() or add(), but from the upload.py script.

  This generates a text/plain response.
  """
  if request.user is None:
    if IS_DEV:
      request.user = users.User(request.POST.get('user', '<EMAIL>'))
    else:
      return HttpResponse('Login required', status=401)
  # Check against old upload.py usage.
  if request.POST.get('num_parts') > 1:
    return HttpResponse('Upload.py is too old, get the latest version.',
                        content_type='text/plain')
  form = UploadForm(request.POST, request.FILES)
  issue = None
  patchset = None
  if form.is_valid():
    issue_id = form.cleaned_data['issue']
{'fraction_non_alphanumeric': 0.1085527978567457, 'fraction_numerical': 0.01018551281403225, 'mean_word_length': 3.0145089285714284, 'pattern_counts': {'":': 0, '<': 5, '<?xml version=': 0, '>': 14, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 2}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 14, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3905178', 'n_tokens_mistral': 11982, 'n_tokens_neox': 11449, 'n_words': 3103}