Start Rietveld report taken from code.google.com/p/rietveld with the revision a2d5c409a0ee

This commit is contained in:
Bernat Brunet Torruella 2013-04-26 19:15:43 +02:00
commit 813b72b1b8
115 changed files with 23016 additions and 0 deletions

12
.hgignore Normal file
View File

@ -0,0 +1,12 @@
.*\.py[co]$
.*\.pyc-2.4$
.*~$
.*\.orig$
.*\#.*$
.*@.*$
index\.yaml$
REVISION$
.coverage$
htmlcov$
.DS_Store$
workspace.xml$

202
COPYING Normal file
View File

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
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.

71
Makefile Normal file
View File

@ -0,0 +1,71 @@
# Makefile to simplify some common AppEngine actions.
# Use 'make help' for a list of commands.
APPID?= `cat app.yaml | sed -n 's/^application: *//p'`
SDK_PATH ?=
DEV_APPSERVER?= $(if $(SDK_PATH), $(SDK_PATH)/,)dev_appserver.py
DEV_APPSERVER_FLAGS?=
APPCFG?= $(if $(SDK_PATH), $(SDK_PATH)/,)appcfg.py
APPCFG_FLAGS?=
# Set dirty suffix depending on output of "hg status".
dirty=
ifneq ($(shell hg status),)
dirty="-tainted"
endif
VERSION_TAG= `hg parents --template='{rev}:{node|short}'`$(dirty)
# AppEngine version cannot use ':' in its name so use a '-' instead.
VERSION?= `hg parents --template='{rev}-{node|short}'`$(dirty)
PYTHON?= python2.7
COVERAGE?= coverage
default: help
help:
@echo "Available commands:"
@sed -n '/^[a-zA-Z0-9_.]*:/s/:.*//p' <Makefile | sort
run: serve
serve: update_revision
@echo "---[Starting SDK AppEngine Server]---"
$(DEV_APPSERVER) $(DEV_APPSERVER_FLAGS) .
serve_remote: update_revision
$(DEV_APPSERVER) $(DEV_APPSERVER_FLAGS) --address 0.0.0.0 .
serve_email: update_revision
$(DEV_APPSERVER) $(DEV_APPSERVER_FLAGS) --enable_sendmail .
serve_remote_email: update_revision
$(DEV_APPSERVER) $(DEV_APPSERVER_FLAGS) --enable_sendmail --address 0.0.0.0 .
update_revision:
@echo "---[Updating REVISION]---"
@echo "$(VERSION_TAG)" >REVISION
update: update_revision
@echo "---[Updating $(APPID)]---"
$(APPCFG) $(APPCFG_FLAGS) update . --application $(APPID) --version $(VERSION)
upload: update
deploy: update
update_indexes:
$(APPCFG) $(APPCFG_FLAGS) update_indexes .
vacuum_indexes:
$(APPCFG) $(APPCFG_FLAGS) vacuum_indexes .
test:
$(PYTHON) tests/run_tests.py $(SDK_PATH)
coverage:
$(COVERAGE) run --branch tests/run_tests.py $(SDK_PATH)
$(COVERAGE) html --include="codereview/*"

81
README Normal file
View File

@ -0,0 +1,81 @@
Welcome to Rietveld
-------------------
This project shows how to create a somewhat substantial web
application using Django on Google App Engine. It requires Python 2.7
and Django version 1.3 (although a previous version using Python 2.5
and Django 1.2 can still be found in the py25 branch in the repository).
In addition, I hope it will serve as a practical tool for the Python
developer community, and hopefully for other open source communities.
As I've learned over the last two years at Google, where I developed a
similar tool named Mondrian, proper code review habits can really
improve the quality of a code base, and good tools for code review
will improve developers' life.
Some code in this project was derived from Mondrian, but this is not
the full Mondrian tool.
--Guido van Rossum, Python creator and Google employee
Links
-----
Mondrian video: http://www.youtube.com/watch?v=sMql3Di4Kgc
Google App Engine: http://code.google.com/appengine/
Live app: http://codereview.appspot.com
About code review: http://en.wikipedia.org/wiki/Code_review
Django: http://djangoproject.com
Python: http://python.org
License
-------
The license is Apache 2.0. See the file COPYING.
Running
-------
To run the app locally (e.g. for testing), download the Google App
Engine SDK from http://code.google.com/appengine/downloads.html. You
can then run the server using
make serve
(assuming you're on Linux or Mac OS X). On Windows just use Google
App Engine Launcher.
Rietveld uses Django 1.2 libraries. They are included in App Engine
SDK version 1.4.2 and above.
The server is only accessible on http://localhost:8080. The server in
the Google App Engine SDK is not designed for serving real traffic.
The App Engine FAQ at http://code.google.com/appengine/kb/general.html
says about this: "You can override this using the -a <hostname> flag
when running it, but doing so is not recommended because the SDK has
not been hardened for security and may contain vulnerabilities."
To deploy your own instance of the app to Google App Engine:
1. Register your own application ID on the App Engine admin site.
2. Edit app.yaml to use this app ID instead of 'codereview-hr'.
3. Upload using
make update VERSION=123
*** Don't forget step 2! If you forget to change the application ID,
you'll get a error message from "appcfg.py update" (called by "make
update") complaining you don't have the right to administer this app.
*** Don't update the Python runtime in app.yaml to Python 2.7!
Currently Rietveld doesn't support Python 2.7 on App Engine due to
some thread-safety issues (see
http://code.google.com/p/rietveld/issues/detail?id=348).
The VERSION=xxx argument sets the version; the version from the
app.yaml is not used. This is to support a convention used for the
main Rietveld instance (codereview.appspot.com) whereby we never
deploy to the same version twice; the version must be manually picked
by the developer doing the deployment. If you don't like this, just
edit the Makefile to remove "--version $(VERSION)" and edit app.yaml
to hardcode a version number.

69
TODO Normal file
View File

@ -0,0 +1,69 @@
Bugs
----
If a user never logs in, someone else can grab their nickname
See the issue tracker at code.google.com/p/rietveld for more bugs
Data Cleaning
-------------
Email addresses should be lowercased before comparing
Nicknames too???
Nicknames should not be allowed to contain multiple internal spaces
nor internal whitespace other than space
Issues
------
Archive issues (that's per user rather than per issue)
Patch Sets
----------
Edit patch set message
Delete patch sets
Repositories and Branches
-------------------------
Get rid of the repository feature in favor of always using upload.py?
Edit/Delete Repository?
Automatically add Repository entries when a new project uploads stuff?
Searching, Organizing
---------------------
View all open issues by base, or by repository
View all closed issues (issue 15 in the tracker)
Commenting
----------
Add stars
Add non-inline comments per file, per patchset
Diffs and Patches
-----------------
Deprecate the upload form (and repositories) and do it all with upload.py
Improve UI for selecting patch set deltas; handle missing files better
Make delta calculation a background task when uploading patch sets.
Syntax colorization
Record revision and show in UI; indicate action (add/edit/delete)
Handle binary files?! (Progress: fewer crashes related to binary files)
Add line length option
Support more diff formats (Better: let upload.py normlize these)
Need a more powerful way to specify the URL for finding a revision (?)
User Experience
---------------
Make Edit Issue show the form inline instead of opening a new page?
Fields "SVN Base" and "Base" have hard-to-guess names
Better UI for adding branches? (Low priority now we have upload.py)
Right-justify the "Id" and "Drafts (mine)" headings in the Issues list table
Software Engineering
--------------------
Unittests

0
__init__.py Normal file
View File

48
app.yaml Normal file
View File

@ -0,0 +1,48 @@
application: codereview-hr
version: use-version-arg
runtime: python27
api_version: 1
threadsafe: false
default_expiration: 7d # This is good for images, which never change
handlers:
- url: /(robots.txt|favicon.ico)
static_files: static/\1
upload: static/(robots.txt|favicon.ico)
- url: /google7db36eb2cc527940.html
static_files: static/robots.txt
upload: static/robots.txt
- url: /static/upload.py
script: main.application
- url: /static/(script.js|styles.css)
static_files: static/\1
upload: static/(script.js|styles.css)
expiration: 1h # Shorter expiration, these change often
- url: /static
static_dir: static
- url: /tasks/migrate_entities
script: main.application
login: admin
- url: /.*
script: main.application
inbound_services:
- mail
- xmpp_message
- warmup
builtins:
- appstats: on
- remote_api: on
libraries:
- name: django
version: 1.3

46
appengine_config.py Normal file
View File

@ -0,0 +1,46 @@
"""Configuration."""
import logging
import os
import re
from google.appengine.ext.appstats import recording
logging.info('Loading %s from %s', __name__, __file__)
# Custom webapp middleware to add Appstats.
def webapp_add_wsgi_middleware(app):
app = recording.appstats_wsgi_middleware(app)
return app
# Custom Appstats path normalization.
def appstats_normalize_path(path):
if path.startswith('/user/'):
return '/user/X'
if path.startswith('/user_popup/'):
return '/user_popup/X'
if '/diff/' in path:
return '/X/diff/...'
if '/diff2/' in path:
return '/X/diff2/...'
if '/patch/' in path:
return '/X/patch/...'
if path.startswith('/rss/'):
i = path.find('/', 5)
if i > 0:
return path[:i] + '/X'
return re.sub(r'\d+', 'X', path)
# Segregate Appstats by runtime (python vs. python27).
appstats_KEY_NAMESPACE = '__appstats_%s__' % os.getenv('APPENGINE_RUNTIME')
# Enable Interactive Playground.
appstats_SHELL_OK = True
# Enable RPC cost calculation.
appstats_CALC_RPC_COSTS = True
# Django 1.2+ requires DJANGO_SETTINGS_MODULE environment variable to be set
# http://code.google.com/appengine/docs/python/tools/libraries.html#Django
os.environ['DJANGO_SETTINGS_MODULE'] = 'settings'
# NOTE: All "main" scripts must import webapp.template before django.

0
codereview/__init__.py Normal file
View File

702
codereview/engine.py Normal file
View File

@ -0,0 +1,702 @@
# 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.
"""Diff rendering in HTML for Rietveld."""
import cgi
import difflib
import re
from google.appengine.api import users
from django.conf import settings
from django.template import loader, RequestContext
from codereview import intra_region_diff
from codereview import models
from codereview import patching
from codereview import utils
# NOTE: The SplitPatch function is duplicated in upload.py, keep them in sync.
def SplitPatch(data):
"""Splits a patch into separate pieces for each file.
Args:
data: A string containing the output of svn diff.
Returns:
A list of 2-tuple (filename, text) where text is the svn diff output
pertaining to filename.
"""
patches = []
filename = None
diff = []
for line in data.splitlines(True):
new_filename = None
if line.startswith('Index:'):
_, new_filename = line.split(':', 1)
new_filename = new_filename.strip()
elif line.startswith('Property changes on:'):
_, temp_filename = line.split(':', 1)
# When a file is modified, paths use '/' between directories, however
# when a property is modified '\' is used on Windows. Make them the same
# otherwise the file shows up twice.
temp_filename = temp_filename.strip().replace('\\', '/')
if temp_filename != filename:
# File has property changes but no modifications, create a new diff.
new_filename = temp_filename
if new_filename:
if filename and diff:
patches.append((filename, ''.join(diff)))
filename = new_filename
diff = [line]
continue
if diff is not None:
diff.append(line)
if filename and diff:
patches.append((filename, ''.join(diff)))
return patches
def ParsePatchSet(patchset):
"""Patch a patch set into individual patches.
Args:
patchset: a models.PatchSet instance.
Returns:
A list of models.Patch instances.
"""
patches = []
for filename, text in SplitPatch(patchset.data):
patches.append(models.Patch(patchset=patchset, text=utils.to_dbtext(text),
filename=filename, parent=patchset))
return patches
def RenderDiffTableRows(request, old_lines, chunks, patch,
colwidth=settings.DEFAULT_COLUMN_WIDTH, debug=False,
context=settings.DEFAULT_CONTEXT):
"""Render the HTML table rows for a side-by-side diff for a patch.
Args:
request: Django Request object.
old_lines: List of lines representing the original file.
chunks: List of chunks as returned by patching.ParsePatchToChunks().
patch: A models.Patch instance.
colwidth: Optional column width (default 80).
debug: Optional debugging flag (default False).
context: Maximum number of rows surrounding a change (default CONTEXT).
Yields:
Strings, each of which represents the text rendering one complete
pair of lines of the side-by-side diff, possibly including comments.
Each yielded string may consist of several <tr> elements.
"""
rows = _RenderDiffTableRows(request, old_lines, chunks, patch,
colwidth, debug)
return _CleanupTableRowsGenerator(rows, context)
def RenderDiff2TableRows(request, old_lines, old_patch, new_lines, new_patch,
colwidth=settings.DEFAULT_COLUMN_WIDTH, debug=False,
context=settings.DEFAULT_CONTEXT):
"""Render the HTML table rows for a side-by-side diff between two patches.
Args:
request: Django Request object.
old_lines: List of lines representing the patched file on the left.
old_patch: The models.Patch instance corresponding to old_lines.
new_lines: List of lines representing the patched file on the right.
new_patch: The models.Patch instance corresponding to new_lines.
colwidth: Optional column width (default 80).
debug: Optional debugging flag (default False).
context: Maximum number of visible context lines (default
settings.DEFAULT_CONTEXT).
Yields:
Strings, each of which represents the text rendering one complete
pair of lines of the side-by-side diff, possibly including comments.
Each yielded string may consist of several <tr> elements.
"""
rows = _RenderDiff2TableRows(request, old_lines, old_patch,
new_lines, new_patch, colwidth, debug)
return _CleanupTableRowsGenerator(rows, context)
def _CleanupTableRowsGenerator(rows, context):
"""Cleanup rows returned by _TableRowGenerator for output.
Args:
rows: List of tuples (tag, text)
context: Maximum number of visible context lines.
Yields:
Rows marked as 'equal' are possibly contracted using _ShortenBuffer().
Stops on rows marked as 'error'.
"""
buffer = []
for tag, text in rows:
if tag == 'equal':
buffer.append(text)
continue
else:
for t in _ShortenBuffer(buffer, context):
yield t
buffer = []
yield text
if tag == 'error':
yield None
break
if buffer:
for t in _ShortenBuffer(buffer, context):
yield t
def _ShortenBuffer(buffer, context):
"""Render a possibly contracted series of HTML table rows.
Args:
buffer: a list of strings representing HTML table rows.
context: Maximum number of visible context lines. If None all lines are
returned.
Yields:
If the buffer has fewer than 3 times context items, yield all
the items. Otherwise, yield the first context items, a single
table row representing the contraction, and the last context
items.
"""
if context is None or len(buffer) < 3*context:
for t in buffer:
yield t
else:
last_id = None
for t in buffer[:context]:
m = re.match('^<tr( name="hook")? id="pair-(?P<rowcount>\d+)">', t)
if m:
last_id = int(m.groupdict().get("rowcount"))
yield t
skip = len(buffer) - 2*context
expand_link = []
if skip > 3*context:
expand_link.append(('<a href="javascript:M_expandSkipped(%(before)d, '
'%(after)d, \'t\', %(skip)d)">'
'Expand %(context)d before'
'</a> | '))
expand_link.append(('<a href="javascript:M_expandSkipped(%(before)d, '
'%(after)d, \'a\', %(skip)d)">Expand all</a>'))
if skip > 3*context:
expand_link.append((' | '
'<a href="javascript:M_expandSkipped(%(before)d, '
'%(after)d, \'b\', %(skip)d)">'
'Expand %(context)d after'
'</a>'))
expand_link = ''.join(expand_link) % {'before': last_id+1,
'after': last_id+skip,
'skip': last_id,
'context': max(context, None)}
yield ('<tr id="skip-%d"><td colspan="2" align="center" '
'style="background:lightblue">'
'(...skipping <span id="skipcount-%d">%d</span> matching lines...) '
'<span id="skiplinks-%d">%s</span> '
'<span id="skiploading-%d" style="visibility:hidden;">Loading...'
'</span>'
'</td></tr>\n' % (last_id, last_id, skip,
last_id, expand_link, last_id))
for t in buffer[-context:]:
yield t
def _RenderDiff2TableRows(request, old_lines, old_patch, new_lines, new_patch,
colwidth=settings.DEFAULT_COLUMN_WIDTH, debug=False):
"""Internal version of RenderDiff2TableRows().
Args:
The same as for RenderDiff2TableRows.
Yields:
Tuples (tag, row) where tag is an indication of the row type.
"""
old_dict = {}
new_dict = {}
for patch, dct in [(old_patch, old_dict), (new_patch, new_dict)]:
# XXX GQL doesn't support OR yet... Otherwise we'd be using that.
for comment in models.Comment.gql(
'WHERE patch = :1 AND left = FALSE ORDER BY date', patch):
if comment.draft and comment.author != request.user:
continue # Only show your own drafts
comment.complete()
lst = dct.setdefault(comment.lineno, [])
lst.append(comment)
return _TableRowGenerator(old_patch, old_dict, len(old_lines)+1, 'new',
new_patch, new_dict, len(new_lines)+1, 'new',
_GenerateTriples(old_lines, new_lines),
colwidth, debug, request)
def _GenerateTriples(old_lines, new_lines):
"""Helper for _RenderDiff2TableRows yielding input for _TableRowGenerator.
Args:
old_lines: List of lines representing the patched file on the left.
new_lines: List of lines representing the patched file on the right.
Yields:
Tuples (tag, old_slice, new_slice) where tag is a tag as returned by
difflib.SequenceMatchser.get_opcodes(), and old_slice and new_slice
are lists of lines taken from old_lines and new_lines.
"""
sm = difflib.SequenceMatcher(None, old_lines, new_lines)
for tag, i1, i2, j1, j2 in sm.get_opcodes():
yield tag, old_lines[i1:i2], new_lines[j1:j2]
def _GetComments(request):
"""Helper that returns comments for a patch.
Args:
request: Django Request object.
Returns:
A 2-tuple of (old, new) where old/new are dictionaries that holds comments
for that file, mapping from line number to a Comment entity.
"""
old_dict = {}
new_dict = {}
# XXX GQL doesn't support OR yet... Otherwise we'd be using
# .gql('WHERE patch = :1 AND (draft = FALSE OR author = :2) ORDER BY data',
# patch, request.user)
for comment in models.Comment.gql('WHERE patch = :1 ORDER BY date',
request.patch):
if comment.draft and comment.author != request.user:
continue # Only show your own drafts
comment.complete()
if comment.left:
dct = old_dict
else:
dct = new_dict
dct.setdefault(comment.lineno, []).append(comment)
return old_dict, new_dict
def _RenderDiffTableRows(request, old_lines, chunks, patch,
colwidth=settings.DEFAULT_COLUMN_WIDTH, debug=False):
"""Internal version of RenderDiffTableRows().
Args:
The same as for RenderDiffTableRows.
Yields:
Tuples (tag, row) where tag is an indication of the row type.
"""
old_dict = {}
new_dict = {}
if patch:
old_dict, new_dict = _GetComments(request)
old_max, new_max = _ComputeLineCounts(old_lines, chunks)
return _TableRowGenerator(patch, old_dict, old_max, 'old',
patch, new_dict, new_max, 'new',
patching.PatchChunks(old_lines, chunks),
colwidth, debug, request)
def _TableRowGenerator(old_patch, old_dict, old_max, old_snapshot,
new_patch, new_dict, new_max, new_snapshot,
triple_iterator, colwidth=settings.DEFAULT_COLUMN_WIDTH,
debug=False, request=None):
"""Helper function to render side-by-side table rows.
Args:
old_patch: First models.Patch instance.
old_dict: Dictionary with line numbers as keys and comments as values (left)
old_max: Line count of the patch on the left.
old_snapshot: A tag used in the comments form.
new_patch: Second models.Patch instance.
new_dict: Same as old_dict, but for the right side.
new_max: Line count of the patch on the right.
new_snapshot: A tag used in the comments form.
triple_iterator: Iterator that yields (tag, old, new) triples.
colwidth: Optional column width (default 80).
debug: Optional debugging flag (default False).
Yields:
Tuples (tag, row) where tag is an indication of the row type and
row is an HTML fragment representing one or more <td> elements.
"""
diff_params = intra_region_diff.GetDiffParams(dbg=debug)
ndigits = 1 + max(len(str(old_max)), len(str(new_max)))
indent = 1 + ndigits
old_offset = new_offset = 0
row_count = 0
# Render a row with a message if a side is empty or both sides are equal.
if old_patch == new_patch and (old_max == 0 or new_max == 0):
if old_max == 0:
msg_old = '(Empty)'
else:
msg_old = ''
if new_max == 0:
msg_new = '(Empty)'
else:
msg_new = ''
yield '', ('<tr><td class="info">%s</td>'
'<td class="info">%s</td></tr>' % (msg_old, msg_new))
elif old_patch is None or new_patch is None:
msg_old = msg_new = ''
if old_patch is None:
msg_old = '(no file at all)'
if new_patch is None:
msg_new = '(no file at all)'
yield '', ('<tr><td class="info">%s</td>'
'<td class="info">%s</td></tr>' % (msg_old, msg_new))
elif old_patch != new_patch and old_patch.lines == new_patch.lines:
yield '', ('<tr><td class="info" colspan="2">'
'(Both sides are equal)</td></tr>')
for tag, old, new in triple_iterator:
if tag.startswith('error'):
yield 'error', '<tr><td><h3>%s</h3></td></tr>\n' % cgi.escape(tag)
return
old1 = old_offset
old_offset = old2 = old1 + len(old)
new1 = new_offset
new_offset = new2 = new1 + len(new)
old_buff = []
new_buff = []
frag_list = []
do_ir_diff = tag == 'replace' and intra_region_diff.CanDoIRDiff(old, new)
for i in xrange(max(len(old), len(new))):
row_count += 1
old_lineno = old1 + i + 1
new_lineno = new1 + i + 1
old_valid = old1+i < old2
new_valid = new1+i < new2
# Start rendering the first row
frags = []
if i == 0 and tag != 'equal':
# Mark the first row of each non-equal chunk as a 'hook'.
frags.append('<tr name="hook"')
else:
frags.append('<tr')
frags.append(' id="pair-%d">' % row_count)
old_intra_diff = ''
new_intra_diff = ''
if old_valid:
old_intra_diff = old[i]
if new_valid:
new_intra_diff = new[i]
frag_list.append(frags)
if do_ir_diff:
# Don't render yet. Keep saving state necessary to render the whole
# region until we have encountered all the lines in the region.
old_buff.append([old_valid, old_lineno, old_intra_diff])
new_buff.append([new_valid, new_lineno, new_intra_diff])
else:
# We render line by line as usual if do_ir_diff is false
old_intra_diff = intra_region_diff.Break(
old_intra_diff, 0, colwidth, "\n" + " "*indent)
new_intra_diff = intra_region_diff.Break(
new_intra_diff, 0, colwidth, "\n" + " "*indent)
old_buff_out = [[old_valid, old_lineno,
(old_intra_diff, True, None)]]
new_buff_out = [[new_valid, new_lineno,
(new_intra_diff, True, None)]]
for tg, frag in _RenderDiffInternal(old_buff_out, new_buff_out,
ndigits, tag, frag_list,
do_ir_diff,
old_dict, new_dict,
old_patch, new_patch,
old_snapshot, new_snapshot,
debug, request):
yield tg, frag
frag_list = []
if do_ir_diff:
# So this was a replace block which means that the whole region still
# needs to be rendered.
old_lines = [b[2] for b in old_buff]
new_lines = [b[2] for b in new_buff]
ret = intra_region_diff.IntraRegionDiff(old_lines, new_lines,
diff_params)
old_chunks, new_chunks, ratio = ret
old_tag = 'old'
new_tag = 'new'
old_diff_out = intra_region_diff.RenderIntraRegionDiff(
old_lines, old_chunks, old_tag, ratio,
limit=colwidth, indent=indent, mark_tabs=True,
dbg=debug)
new_diff_out = intra_region_diff.RenderIntraRegionDiff(
new_lines, new_chunks, new_tag, ratio,
limit=colwidth, indent=indent, mark_tabs=True,
dbg=debug)
for (i, b) in enumerate(old_buff):
b[2] = old_diff_out[i]
for (i, b) in enumerate(new_buff):
b[2] = new_diff_out[i]
for tg, frag in _RenderDiffInternal(old_buff, new_buff,
ndigits, tag, frag_list,
do_ir_diff,
old_dict, new_dict,
old_patch, new_patch,
old_snapshot, new_snapshot,
debug, request):
yield tg, frag
old_buff = []
new_buff = []
def _RenderDiffInternal(old_buff, new_buff, ndigits, tag, frag_list,
do_ir_diff, old_dict, new_dict,
old_patch, new_patch,
old_snapshot, new_snapshot,
debug, request):
"""Helper for _TableRowGenerator()."""
obegin = (intra_region_diff.BEGIN_TAG %
intra_region_diff.COLOR_SCHEME['old']['match'])
nbegin = (intra_region_diff.BEGIN_TAG %
intra_region_diff.COLOR_SCHEME['new']['match'])
oend = intra_region_diff.END_TAG
nend = oend
user = users.get_current_user()
for i in xrange(len(old_buff)):
tg = tag
old_valid, old_lineno, old_out = old_buff[i]
new_valid, new_lineno, new_out = new_buff[i]
old_intra_diff, old_has_newline, old_debug_info = old_out
new_intra_diff, new_has_newline, new_debug_info = new_out
frags = frag_list[i]
# Render left text column
frags.append(_RenderDiffColumn(old_valid, tag, ndigits,
old_lineno, obegin, oend, old_intra_diff,
do_ir_diff, old_has_newline, 'old'))
# Render right text column
frags.append(_RenderDiffColumn(new_valid, tag, ndigits,
new_lineno, nbegin, nend, new_intra_diff,
do_ir_diff, new_has_newline, 'new'))
# End rendering the first row
frags.append('</tr>\n')
if debug:
frags.append('<tr>')
if old_debug_info:
frags.append('<td class="debug-info">%s</td>' %
old_debug_info.replace('\n', '<br>'))
else:
frags.append('<td></td>')
if new_debug_info:
frags.append('<td class="debug-info">%s</td>' %
new_debug_info.replace('\n', '<br>'))
else:
frags.append('<td></td>')
frags.append('</tr>\n')
if old_patch or new_patch:
# Start rendering the second row
if ((old_valid and old_lineno in old_dict) or
(new_valid and new_lineno in new_dict)):
tg += '_comment'
frags.append('<tr class="inline-comments" name="hook">')
else:
frags.append('<tr class="inline-comments">')
# Render left inline comments
frags.append(_RenderInlineComments(old_valid, old_lineno, old_dict,
user, old_patch, old_snapshot, 'old',
request))
# Render right inline comments
frags.append(_RenderInlineComments(new_valid, new_lineno, new_dict,
user, new_patch, new_snapshot, 'new',
request))
# End rendering the second row
frags.append('</tr>\n')
# Yield the combined fragments
yield tg, ''.join(frags)
def _RenderDiffColumn(line_valid, tag, ndigits, lineno, begin, end,
intra_diff, do_ir_diff, has_newline, prefix):
"""Helper function for _RenderDiffInternal().
Returns:
A rendered column.
"""
if line_valid:
cls_attr = '%s%s' % (prefix, tag)
if tag == 'equal':
lno = '%*d' % (ndigits, lineno)
else:
lno = _MarkupNumber(ndigits, lineno, 'u')
if tag == 'replace':
col_content = ('%s%s %s%s' % (begin, lno, end, intra_diff))
# If IR diff has been turned off or there is no matching new line at
# the end then switch to dark background CSS style.
if not do_ir_diff or not has_newline:
cls_attr = cls_attr + '1'
else:
col_content = '%s %s' % (lno, intra_diff)
return '<td class="%s" id="%scode%d">%s</td>' % (cls_attr, prefix,
lineno, col_content)
else:
return '<td class="%sblank"></td>' % prefix
def _RenderInlineComments(line_valid, lineno, data, user,
patch, snapshot, prefix, request):
"""Helper function for _RenderDiffInternal().
Returns:
Rendered comments.
"""
comments = []
if line_valid:
comments.append('<td id="%s-line-%s">' % (prefix, lineno))
if lineno in data:
comments.append(
_ExpandTemplate('inline_comment.html',
request,
user=user,
patch=patch,
patchset=patch.patchset,
issue=patch.patchset.issue,
snapshot=snapshot,
side='a' if prefix == 'old' else 'b',
comments=data[lineno],
lineno=lineno,
))
comments.append('</td>')
else:
comments.append('<td></td>')
return ''.join(comments)
def RenderUnifiedTableRows(request, parsed_lines):
"""Render the HTML table rows for a unified diff for a patch.
Args:
request: Django Request object.
parsed_lines: List of tuples for each line that contain the line number,
if they exist, for the old and new file.
Returns:
A list of html table rows.
"""
old_dict, new_dict = _GetComments(request)
rows = []
for old_line_no, new_line_no, line_text in parsed_lines:
row1_id = row2_id = ''
# When a line is unchanged (i.e. both old_line_no and new_line_no aren't 0)
# pick the old column line numbers when adding a comment.
if old_line_no:
row1_id = 'id="oldcode%d"' % old_line_no
row2_id = 'id="old-line-%d"' % old_line_no
elif new_line_no:
row1_id = 'id="newcode%d"' % new_line_no
row2_id = 'id="new-line-%d"' % new_line_no
if line_text[0] == '+':
style = 'udiffadd'
elif line_text[0] == '-':
style = 'udiffremove'
else:
style = ''
rows.append('<tr><td class="udiff %s" %s>%s</td></tr>' %
(style, row1_id, cgi.escape(line_text)))
frags = []
if old_line_no in old_dict or new_line_no in new_dict:
frags.append('<tr class="inline-comments" name="hook">')
if old_line_no in old_dict:
dct = old_dict
line_no = old_line_no
snapshot = 'old'
else:
dct = new_dict
line_no = new_line_no
snapshot = 'new'
frags.append(_RenderInlineComments(True, line_no, dct, request.user,
request.patch, snapshot, snapshot, request))
else:
frags.append('<tr class="inline-comments">')
frags.append('<td ' + row2_id +'></td>')
frags.append('</tr>')
rows.append(''.join(frags))
return rows
def _ComputeLineCounts(old_lines, chunks):
"""Compute the length of the old and new sides of a diff.
Args:
old_lines: List of lines representing the original file.
chunks: List of chunks as returned by patching.ParsePatchToChunks().
Returns:
A tuple (old_len, new_len) representing len(old_lines) and
len(new_lines), where new_lines is the list representing the
result of applying the patch chunks to old_lines, however, without
actually computing new_lines.
"""
old_len = len(old_lines)
new_len = old_len
if chunks:
(_, old_b), (_, new_b), old_lines, _ = chunks[-1]
new_len += new_b - old_b
return old_len, new_len
def _MarkupNumber(ndigits, number, tag):
"""Format a number in HTML in a given width with extra markup.
Args:
ndigits: the total width available for formatting
number: the number to be formatted
tag: HTML tag name, e.g. 'u'
Returns:
An HTML string that displays as ndigits wide, with the
number right-aligned and surrounded by an HTML tag; for example,
_MarkupNumber(42, 4, 'u') returns ' <u>42</u>'.
"""
formatted_number = str(number)
space_prefix = ' ' * (ndigits - len(formatted_number))
return '%s<%s>%s</%s>' % (space_prefix, tag, formatted_number, tag)
def _ExpandTemplate(name, request, **params):
"""Wrapper around django.template.loader.render_to_string().
For convenience, this takes keyword arguments instead of a dict.
"""
rslt = loader.render_to_string(name, params,
context_instance=RequestContext(request))
return rslt.encode('utf-8')

23
codereview/exceptions.py Normal file
View File

@ -0,0 +1,23 @@
# Copyright 2011 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.
"""Exception classes."""
class RietveldError(Exception):
"""Base class for all exceptions in this application."""
class FetchError(RietveldError):
"""Exception raised when fetching of remote files fails."""

163
codereview/feeds.py Normal file
View File

@ -0,0 +1,163 @@
# 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.
import md5
from django.contrib.syndication.feeds import Feed
from django.core.exceptions import ObjectDoesNotExist
from django.core.urlresolvers import reverse
from django.utils.feedgenerator import Atom1Feed
from codereview import library
from codereview import models
class BaseFeed(Feed):
title = 'Code Review'
description = 'Rietveld: Code Review Tool hosted on Google App Engine'
feed_type = Atom1Feed
def link(self):
return reverse('codereview.views.index')
def author_name(self):
return 'rietveld'
def item_guid(self, item):
return 'urn:md5:%s' % (md5.new(str(item.key())).hexdigest())
def item_link(self, item):
if isinstance(item, models.PatchSet):
if item.data is not None:
return reverse('codereview.views.download',
args=[item.issue.key().id(),item.key().id()])
else:
# Patch set is too large, only the splitted diffs are available.
return reverse('codereview.views.show', args=[item.parent_key().id()])
if isinstance(item, models.Message):
return '%s#msg-%s' % (reverse('codereview.views.show',
args=[item.issue.key().id()]),
item.key())
return reverse('codereview.views.show', args=[item.key().id()])
def item_title(self, item):
return 'the title'
def item_author_name(self, item):
if isinstance(item, models.Issue):
return library.get_nickname(item.owner, True)
if isinstance(item, models.PatchSet):
return library.get_nickname(item.issue.owner, True)
if isinstance(item, models.Message):
return library.get_nickname(item.sender, True)
return 'Rietveld'
def item_pubdate(self, item):
if isinstance(item, models.Issue):
return item.modified
if isinstance(item, models.PatchSet):
# Use created, not modified, so that commenting on
# a patch set does not bump its place in the RSS feed.
return item.created
if isinstance(item, models.Message):
return item.date
return None
class BaseUserFeed(BaseFeed):
def get_object(self, bits):
"""Returns the account for the requested user feed.
bits is a list of URL path elements. The first element of this list
should be the user's nickname. A 404 is raised if the list is empty or
has more than one element or if the a user with that nickname
doesn't exist.
"""
if len(bits) != 1:
raise ObjectDoesNotExist
obj = bits[0]
account = models.Account.get_account_for_nickname('%s' % obj)
if account is None:
raise ObjectDoesNotExist
return account
class ReviewsFeed(BaseUserFeed):
title = 'Code Review - All issues I have to review'
def items(self, obj):
return _rss_helper(obj.email, 'closed = FALSE AND reviewers = :1',
use_email=True)
class ClosedFeed(BaseUserFeed):
title = "Code Review - Reviews closed by me"
def items(self, obj):
return _rss_helper(obj.email, 'closed = TRUE AND owner = :1')
class MineFeed(BaseUserFeed):
title = 'Code Review - My issues'
def items(self, obj):
return _rss_helper(obj.email, 'closed = FALSE AND owner = :1')
class AllFeed(BaseFeed):
title = 'Code Review - All issues'
def items(self):
query = models.Issue.gql('WHERE closed = FALSE AND private = FALSE '
'ORDER BY modified DESC')
return query.fetch(RSS_LIMIT)
class OneIssueFeed(BaseFeed):
def link(self):
return reverse('codereview.views.index')
def get_object(self, bits):
if len(bits) != 1:
raise ObjectDoesNotExist
obj = models.Issue.get_by_id(int(bits[0]))
if obj:
return obj
raise ObjectDoesNotExist
def title(self, obj):
return 'Code review - Issue %d: %s' % (obj.key().id(), obj.subject)
def items(self, obj):
all = list(obj.patchset_set) + list(obj.message_set)
all.sort(key=self.item_pubdate)
return all
### RSS feeds ###
# Maximum number of issues reported by RSS feeds
RSS_LIMIT = 20
def _rss_helper(email, query_string, use_email=False):
account = models.Account.get_account_for_email(email)
if account is None:
issues = []
else:
query = models.Issue.gql('WHERE %s AND private = FALSE '
'ORDER BY modified DESC' % query_string,
use_email and account.email or account.user)
issues = query.fetch(RSS_LIMIT)
return issues

View File

@ -0,0 +1,696 @@
# 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.
"""Intra-region diff utilities.
Intra-region diff highlights the blocks of code which have been changed or
deleted within a region. So instead of highlighting the whole region marked as
changed, the user can see what exactly was changed within that region.
Terminology:
'region' is a list of consecutive code lines.
'word' is the unit of intra-region diff. Its definition is arbitrary based on
what we think as to be a good unit of difference between two regions.
'block' is a small section of code within a region. It can span multiple
lines. There can be multiple non overlapping blocks within a region. A block
can potentially span the whole region.
The blocks have two representations. One is of the format (offset1, offset2,
size) which is returned by the SequenceMatcher to indicate a match of
length 'size' starting at offset1 in the first/old line and starting at offset2
in the second/new line. We convert this representation to a pair of tuples i.e.
(offset1, size) and (offset2, size) for rendering each side of the diff
separately. This latter representation is also more efficient for doing
compaction of adjacent blocks which reduces the size of the HTML markup. See
CompactBlocks for more details.
SequenceMatcher always returns one special matching block at the end with
contents (len(line1), len(line2), 0). We retain this special block as it
simplifies for loops in rendering the last non-matching block. All functions
which deal with the sequence of blocks assume presence of the special block at
the end of the sequence and retain it.
"""
import cgi
import difflib
import re
# Tag to begin a diff chunk.
BEGIN_TAG = "<span class=\"%s\">"
# Tag to end a diff block.
END_TAG = "</span>"
# Tag used for visual tab indication.
TAB_TAG = "<span class=\"visualtab\">&raquo;</span>"
# Color scheme to govern the display properties of diff blocks and matching
# blocks. Each value e.g. 'oldlight' corresponds to a CSS style.
COLOR_SCHEME = {
'old': {
'match': 'oldlight',
'diff': 'olddark',
'bckgrnd': 'oldlight',
},
'new': {
'match': 'newlight',
'diff': 'newdark',
'bckgrnd': 'newlight',
},
'oldmove': {
'match': 'movelight',
'diff': 'oldmovedark',
'bckgrnd': 'movelight'
},
'newmove': {
'match': 'newlight',
'diff': 'newdark',
'bckgrnd': 'newlight'
},
}
# Regular expressions to tokenize lines. Default is 'd'.
EXPRS = {
'a': r'(\w+|[^\w\s]+|\s+)',
'b': r'([A-Za-z0-9]+|[^A-Za-z0-9])',
'c': r'([A-Za-z0-9_]+|[^A-Za-z0-9_])',
'd': r'([^\W_]+|[\W_])',
}
# Maximum total characters in old and new lines for doing intra-region diffs.
# Intra-region diff for larger regions is hard to comprehend and wastes CPU
# time.
MAX_TOTAL_LEN = 10000
def _ExpandTabs(text, column, tabsize, mark_tabs=False):
"""Expand tab characters in a string into spaces.
Args:
text: a string containing tab characters.
column: the initial column for the first character in text
tabsize: tab stops occur at columns that are multiples of tabsize
mark_tabs: if true, leave a tab character as the first character
of the expansion, so that the caller can find where
the tabs were.
Note that calling _ExpandTabs with mark_tabs=True is not idempotent.
"""
expanded = ""
while True:
tabpos = text.find("\t")
if tabpos < 0:
break
fillwidth = tabsize - (tabpos + column) % tabsize
column += tabpos + fillwidth
if mark_tabs:
fill = "\t" + " " * (fillwidth - 1)
else:
fill = " " * fillwidth
expanded += text[0:tabpos] + fill
text = text[tabpos+1:]
return expanded + text
def Break(text, offset=0, limit=80, brk="\n ", tabsize=8, mark_tabs=False):
"""Break text into lines.
Break text, which begins at column offset, each time it reaches
column limit.
To break the text, insert brk, which does not count toward
the column count of the next line and is assumed to be valid HTML.
During the text breaking process, replaces tabs with spaces up
to the next column that is a multiple of tabsize.
If mark_tabs is true, replace the first space of each expanded
tab with TAB_TAG.
Input and output are assumed to be in UTF-8; the computation is done
in Unicode. (Still not good enough if zero-width characters are
present.) If the input is not valid UTF-8, then the encoding is
passed through, potentially breaking up multi-byte characters.
We pass the line through cgi.escape before returning it.
A trailing newline is always stripped from the input first.
"""
assert tabsize > 0, tabsize
if text.endswith("\n"):
text = text[:-1]
try:
text = unicode(text, "utf-8")
except:
pass
# Expand all tabs.
# If mark_tabs is true, we retain one \t character as a marker during
# expansion so that we later replace it with an HTML snippet.
text = _ExpandTabs(text, offset, tabsize, mark_tabs)
# Perform wrapping.
if len(text) > limit - offset:
parts, text = [text[0:limit-offset]], text[limit-offset:]
while len(text) > limit:
parts.append(text[0:limit])
text = text[limit:]
parts.append(text)
text = brk.join([cgi.escape(p) for p in parts])
else:
text = cgi.escape(text)
# Colorize tab markers
text = text.replace("\t", TAB_TAG)
if isinstance(text, unicode):
return text.encode("utf-8", "replace")
return text
def CompactBlocks(blocks):
"""Compacts adjacent code blocks.
In many cases 2 adjacent blocks can be merged into one. This allows
to do some further processing on those blocks.
Args:
blocks: [(offset1, size), ...]
Returns:
A list with the same structure as the input with adjacent blocks
merged. However, the last block (which is always assumed to have
a zero size) is never merged. For example, the input
[(0, 2), (2, 8), (10, 5), (15, 0)]
will produce the output [(0, 15), (15, 0)].
"""
if len(blocks) == 1:
return blocks
result = [blocks[0]]
for block in blocks[1:-1]:
last_start, last_len = result[-1]
curr_start, curr_len = block
if last_start + last_len == curr_start:
result[-1] = last_start, last_len + curr_len
else:
result.append(block)
result.append(blocks[-1])
return result
def FilterBlocks(blocks, filter_func):
"""Gets rid of any blocks if filter_func evaluates false for them.
Args:
blocks: [(offset1, offset2, size), ...]; must have at least 1 entry
filter_func: a boolean function taking a single argument of the form
(offset1, offset2, size)
Returns:
A list with the same structure with entries for which filter_func()
returns false removed. However, the last block is always included.
"""
# We retain the 'special' block at the end.
res = [b for b in blocks[:-1] if filter_func(b)]
res.append(blocks[-1])
return res
def GetDiffParams(expr='d', min_match_ratio=0.6, min_match_size=2, dbg=False):
"""Returns a tuple of various parameters which affect intra region diffs.
Args:
expr: regular expression id to use to identify 'words' in the intra region
diff
min_match_ratio: minimum similarity between regions to qualify for intra
region diff
min_match_size: the smallest matching block size to use. Blocks smaller
than this are ignored.
dbg: to turn on generation of debugging information for the diff
Returns:
4 tuple (expr, min_match_ratio, min_match_size, dbg) that can be used to
customize diff. It can be passed to functions like WordDiff and
IntraLineDiff.
"""
assert expr in EXPRS
assert min_match_size in xrange(1, 5)
assert min_match_ratio > 0.0 and min_match_ratio < 1.0
return (expr, min_match_ratio, min_match_size, dbg)
def CanDoIRDiff(old_lines, new_lines):
"""Tells if it would be worth computing the intra region diff.
Calculating IR diff is costly and is usually helpful only for small regions.
We use a heuristic that if the total number of characters is more than a
certain threshold then we assume it is not worth computing the IR diff.
Args:
old_lines: an array of strings containing old text
new_lines: an array of strings containing new text
Returns:
True if we think it is worth computing IR diff for the region defined
by old_lines and new_lines, False otherwise.
TODO: Let GetDiffParams handle MAX_TOTAL_LEN param also.
"""
total_chars = (sum(len(line) for line in old_lines) +
sum(len(line) for line in new_lines))
return total_chars <= MAX_TOTAL_LEN
def WordDiff(line1, line2, diff_params):
"""Returns blocks with positions indiciating word level diffs.
Args:
line1: string representing the left part of the diff
line2: string representing the right part of the diff
diff_params: return value of GetDiffParams
Returns:
A tuple (blocks, ratio) where:
blocks: [(offset1, offset2, size), ...] such that
line1[offset1:offset1+size] == line2[offset2:offset2+size]
and the last block is always (len(line1), len(line2), 0)
ratio: a float giving the diff ratio computed by SequenceMatcher.
"""
match_expr, min_match_ratio, min_match_size, _ = diff_params
exp = EXPRS[match_expr]
# Strings may have been left undecoded up to now. Assume UTF-8.
try:
line1 = unicode(line1, "utf8")
except:
pass
try:
line2 = unicode(line2, "utf8")
except:
pass
a = re.findall(exp, line1, re.U)
b = re.findall(exp, line2, re.U)
s = difflib.SequenceMatcher(None, a, b)
matching_blocks = s.get_matching_blocks()
ratio = s.ratio()
# Don't show intra region diffs if both lines are too different and there is
# more than one block of difference. If there is only one change then we
# still show the intra region diff regardless of how different the blocks
# are.
# Note: We compare len(matching_blocks) with 3 because one block of change
# results in 2 matching blocks. We add the one special block and we get 3
# matching blocks per one block of change.
if ratio < min_match_ratio and len(matching_blocks) > 3:
return ([(0, 0, 0)], ratio)
# For now convert to character level blocks because we already have
# the code to deal with folding across lines for character blocks.
# Create arrays lena an lenb which have cumulative word lengths
# corresponding to word positions in a and b
lena = []
last = 0
for w in a:
lena.append(last)
last += len(w)
lenb = []
last = 0
for w in b:
lenb.append(last)
last += len(w)
lena.append(len(line1))
lenb.append(len(line2))
# Convert to character blocks
blocks = []
for s1, s2, blen in matching_blocks[:-1]:
apos = lena[s1]
bpos = lenb[s2]
block_len = lena[s1+blen] - apos
blocks.append((apos, bpos, block_len))
# Recreate the special block.
blocks.append((len(line1), len(line2), 0))
# Filter any matching blocks which are smaller than the desired threshold.
# We don't remove matching blocks with only a newline character as doing so
# results in showing the matching newline character as non matching which
# doesn't look good.
blocks = FilterBlocks(blocks, lambda b: (b[2] >= min_match_size or
line1[b[0]:b[0]+b[2]] == '\n'))
return (blocks, ratio)
def IntraLineDiff(line1, line2, diff_params, diff_func=WordDiff):
"""Computes intraline diff blocks.
Args:
line1: string representing the left part of the diff
line2: string representing the right part of the diff
diff_params: return value of GetDiffParams
diff_func: a function whose signature matches that of WordDiff() above
Returns:
A tuple of (blocks1, blocks2) corresponding to line1 and line2.
Each element of the tuple is an array of (start_pos, length)
tuples denoting a diff block.
"""
blocks, ratio = diff_func(line1, line2, diff_params)
blocks1 = [(start1, length) for (start1, start2, length) in blocks]
blocks2 = [(start2, length) for (start1, start2, length) in blocks]
return (blocks1, blocks2, ratio)
def DumpDiff(blocks, line1, line2):
"""Helper function to debug diff related problems.
Args:
blocks: [(offset1, offset2, size), ...]
line1: string representing the left part of the diff
line2: string representing the right part of the diff
"""
for offset1, offset2, size in blocks:
print offset1, offset2, size
print offset1, size, ": ", line1[offset1:offset1+size]
print offset2, size, ": ", line2[offset2:offset2+size]
def RenderIntraLineDiff(blocks, line, tag, dbg_info=None, limit=80, indent=5,
tabsize=8, mark_tabs=False):
"""Renders the diff blocks returned by IntraLineDiff function.
Args:
blocks: [(start_pos, size), ...]
line: line of code on which the blocks are to be rendered.
tag: 'new' or 'old' to control the color scheme.
dbg_info: a string that holds debugging informaion header. Debug
information is rendered only if dbg_info is not None.
limit: folding limit to be passed to the Break function.
indent: indentation size to be passed to the Break function.
tabsize: tab stops occur at columns that are multiples of tabsize
mark_tabs: if True, mark the first character of each expanded tab visually
Returns:
A tuple of two elements. First element is the rendered version of
the input 'line'. Second element tells if the line has a matching
newline character.
"""
res = ""
prev_start, prev_len = 0, 0
has_newline = False
debug_info = dbg_info
if dbg_info:
debug_info += "\nBlock Count: %d\nBlocks: " % (len(blocks) - 1)
for curr_start, curr_len in blocks:
if dbg_info and curr_len > 0:
debug_info += Break(
"\n(%d, %d):|%s|" %
(curr_start, curr_len, line[curr_start:curr_start+curr_len]),
limit, indent, tabsize, mark_tabs)
res += FoldBlock(line, prev_start + prev_len, curr_start, limit, indent,
tag, 'diff', tabsize, mark_tabs)
res += FoldBlock(line, curr_start, curr_start + curr_len, limit, indent,
tag, 'match', tabsize, mark_tabs)
# TODO: This test should be out of loop rather than inside. Once we
# filter out some junk from blocks (e.g. some empty blocks) we should do
# this test only on the last matching block.
if line[curr_start:curr_start+curr_len].endswith('\n'):
has_newline = True
prev_start, prev_len = curr_start, curr_len
return (res, has_newline, debug_info)
def FoldBlock(src, start, end, limit, indent, tag, btype, tabsize=8,
mark_tabs=False):
"""Folds and renders a block.
Args:
src: line of code
start: starting position of the block within 'src'.
end: ending position of the block within 'src'.
limit: folding limit
indent: indentation to use for folding.
tag: 'new' or 'old' to control the color scheme.
btype: block type i.e. 'match' or 'diff' to control the color schme.
tabsize: tab stops occur at columns that are multiples of tabsize
mark_tabs: if True, mark the first character of each expanded tab visually
Returns:
A string representing the rendered block.
"""
text = src[start:end]
# We ignore newlines because we do newline management ourselves.
# Any other new lines with at the end will be stripped off by the Break
# method.
if start >= end or text == '\n':
return ""
fbegin, lend, nl_plus_indent = GetTags(tag, btype, indent)
# 'bol' is beginning of line.
# The text we care about begins at byte offset start
# but if there are tabs it will have a larger column
# offset. Use len(_ExpandTabs()) to find out how many
# columns the starting prefix occupies.
offset_from_bol = len(_ExpandTabs(src[0:start], 0, tabsize)) % limit
brk = lend + nl_plus_indent + fbegin
text = Break(text, offset_from_bol, limit, brk, tabsize, mark_tabs)
if text:
text = fbegin + text + lend
# If this is the first block of the line and this is not the first line then
# insert newline + indent.
if offset_from_bol == 0 and not start == 0:
text = nl_plus_indent + text
return text
def GetTags(tag, btype, indent):
"""Returns various tags for rendering diff blocks.
Args:
tag: a key from COLOR_SCHEME
btype: 'match' or 'diff'
indent: indentation to use
Returns
A 3 tuple (begin_tag, end_tag, formatted_indent_block)
"""
assert tag in COLOR_SCHEME
assert btype in ['match', 'diff']
fbegin = BEGIN_TAG % COLOR_SCHEME[tag][btype]
bbegin = BEGIN_TAG % COLOR_SCHEME[tag]['bckgrnd']
lend = END_TAG
nl_plus_indent = '\n'
if indent > 0:
nl_plus_indent += bbegin + cgi.escape(" "*indent) + lend
return fbegin, lend, nl_plus_indent
def ConvertToSingleLine(lines):
"""Transforms a sequence of strings into a single line.
Returns the state that can be used to reconstruct the original lines with
the newline separators placed at the original place.
Args:
lines: sequence of strings
Returns:
Returns (single_line, state) tuple. 'state' shouldn't be modified by the
caller. It is only used to pass to other functions which will do certain
operations on this state.
'state' is an array containing a dictionary for each item in lines. Each
dictionary has two elements 'pos' and 'blocks'. 'pos' is the end position
of each line in the final converted string. 'blocks' is an array of blocks
for each line of code. These blocks are added using MarkBlock function.
"""
state = []
total_length = 0
for l in lines:
total_length += len(l)
# TODO: Use a tuple instead.
state.append({'pos': total_length, # the line split point
'blocks': [], # blocks which belong to this line
})
result = "".join(lines)
assert len(state) == len(lines)
return (result, state)
def MarkBlock(state, begin, end):
"""Marks a block on a region such that it doesn't cross line boundaries.
It is an operation that can be performed on the single line which was
returned by the ConvertToSingleLine function. This operation marks arbitrary
block [begin,end) on the text. It also ensures that if [begin,end) crosses
line boundaries in the original region then it splits the section up in 2 or
more blocks such that no block crosses the boundaries.
Args:
state: the state returned by ConvertToSingleLine function. The state
contained is modified by this function.
begin: Beginning of the block.
end: End of the block (exclusive).
Returns:
None.
"""
# TODO: Make sure already existing blocks don't overlap
if begin == end:
return
last_pos = 0
for entry in state:
pos = entry['pos']
if begin >= last_pos and begin < pos:
if end < pos:
# block doesn't cross any line boundary
entry['blocks'].append((begin, end))
else:
# block crosses the line boundary
entry['blocks'].append((begin, pos))
MarkBlock(state, pos, end)
break
last_pos = pos
def GetBlocks(state):
"""Returns all the blocks corresponding to the lines in the region.
Args:
state: the state returned by ConvertToSingleLine().
Returns:
An array of [(start_pos, length), ..] with an entry for each line in the
region.
"""
result = []
last_pos = 0
for entry in state:
pos = entry['pos']
# Calculate block start points from the beginning of individual lines.
blocks = [(s[0]-last_pos, s[1]-s[0]) for s in entry['blocks']]
# Add one end marker block.
blocks.append((pos-last_pos, 0))
result.append(blocks)
last_pos = pos
return result
def IntraRegionDiff(old_lines, new_lines, diff_params):
"""Computes intra region diff.
Args:
old_lines: array of strings
new_lines: array of strings
diff_params: return value of GetDiffParams
Returns:
A tuple (old_blocks, new_blocks) containing matching blocks for old and new
lines.
"""
old_line, old_state = ConvertToSingleLine(old_lines)
new_line, new_state = ConvertToSingleLine(new_lines)
old_blocks, new_blocks, ratio = IntraLineDiff(old_line, new_line, diff_params)
for begin, length in old_blocks:
MarkBlock(old_state, begin, begin+length)
old_blocks = GetBlocks(old_state)
for begin, length in new_blocks:
MarkBlock(new_state, begin, begin+length)
new_blocks = GetBlocks(new_state)
return (old_blocks, new_blocks, ratio)
def NormalizeBlocks(blocks, line):
"""Normalizes block representation of an intra line diff.
One diff can have multiple representations. Some times the diff returned by
the difflib for similar text sections is different even within same region.
For example if 2 already indented lines were indented with one additional
space character, the difflib may return the non matching space character to
be any of the already existing spaces. So one line may show non matching
space character as the first space character and the second line may show it
to be the last space character. This is sometimes confusing. This is the
side effect of the new regular expression we are using in WordDiff for
identifying indvidual words. This regular expression ('b') treats a sequence
of punctuation and whitespace characters as individual characters. It has
some visual advantages for showing a character level punctuation change as
one character change rather than a group of character change.
Making the normalization too generic can have performance implications. So
this implementation of normalize blocks intends to handle only one case.
Let's say S represents the space character and () marks a matching block.
Then the normalize operation will do following:
SSSS(SS)(ABCD) => SSSS(SS)(ABCD)
(SS)SSSS(ABCD) => SSSS(SS)(ABCD)
(SSSS)SS(ABCD) => SS(SSSS)(ABCD)
and so on..
Args:
blocks: An array of (offset, len) tuples defined on 'line'. These blocks
mark the matching areas. Anything between these matching blocks is
considered non-matching.
line: The text string on which the blocks are defined.
Returns:
An array of (offset, len) tuples representing the same diff but in
normalized form.
"""
result = []
prev_start, prev_len = blocks[0]
for curr_start, curr_len in blocks[1:]:
# Note: nm_ is a prefix for non matching and m_ is a prefix for matching.
m_len, nm_len = prev_len, curr_start - (prev_start+prev_len)
# This if condition checks if matching and non matching parts are greater
# than zero length and are comprised of spaces ONLY. The last condition
# deals with most of the observed cases of strange diffs.
# Note: curr_start - prev_start == m_l + nm_l
# So line[prev_start:curr_start] == matching_part + non_matching_part.
text = line[prev_start:curr_start]
if m_len > 0 and nm_len > 0 and text == ' ' * len(text):
# Move the matching block towards the end i.e. normalize.
result.append((prev_start + nm_len, m_len))
else:
# Keep the existing matching block.
result.append((prev_start, prev_len))
prev_start, prev_len = curr_start, curr_len
result.append(blocks[-1])
assert len(result) == len(blocks)
return result
def RenderIntraRegionDiff(lines, diff_blocks, tag, ratio, limit=80, indent=5,
tabsize=8, mark_tabs=False, dbg=False):
"""Renders intra region diff for one side.
Args:
lines: list of strings representing source code in the region
diff_blocks: blocks that were returned for this region by IntraRegionDiff()
tag: 'new' or 'old'
ratio: similarity ratio returned by the diff computing function
limit: folding limit
indent: indentation size
tabsize: tab stops occur at columns that are multiples of tabsize
mark_tabs: if True, mark the first character of each expanded tab visually
dbg: indicates if debug information should be rendered
Returns:
A list of strings representing the rendered version of each item in input
'lines'.
"""
result = []
dbg_info = None
if dbg:
dbg_info = 'Ratio: %.1f' % ratio
for line, blocks in zip(lines, diff_blocks):
blocks = NormalizeBlocks(blocks, line)
blocks = CompactBlocks(blocks)
diff = RenderIntraLineDiff(blocks,
line,
tag,
dbg_info=dbg_info,
limit=limit,
indent=indent,
tabsize=tabsize,
mark_tabs=mark_tabs)
result.append(diff)
assert len(result) == len(lines)
return result

261
codereview/library.py Normal file
View File

@ -0,0 +1,261 @@
# 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.
"""Django template library for Rietveld."""
import cgi
from google.appengine.api import memcache
from google.appengine.api import users
import django.template
import django.utils.safestring
from django.core.urlresolvers import reverse
from codereview import models
register = django.template.Library()
user_cache = {}
def get_links_for_users(user_emails):
"""Return a dictionary of email->link to user page and fill caches."""
link_dict = {}
remaining_emails = set(user_emails)
# initialize with email usernames
for email in remaining_emails:
nick = email.split('@', 1)[0]
link_dict[email] = cgi.escape(nick)
# look in the local cache
for email in remaining_emails:
if email in user_cache:
link_dict[email] = user_cache[email]
remaining_emails = remaining_emails - set(user_cache)
if not remaining_emails:
return link_dict
# then look in memcache
memcache_results = memcache.get_multi(remaining_emails,
key_prefix="show_user:")
for email in memcache_results:
link_dict[email] = memcache_results[email]
user_cache[email] = memcache_results[email]
remaining_emails = remaining_emails - set(memcache_results)
if not remaining_emails:
return link_dict
# and finally hit the datastore
accounts = models.Account.get_accounts_for_emails(remaining_emails)
for account in accounts:
if account and account.user_has_selected_nickname:
ret = ('<a href="%s" onMouseOver="M_showUserInfoPopup(this)">%s</a>' %
(reverse('codereview.views.show_user', args=[account.nickname]),
cgi.escape(account.nickname)))
link_dict[account.email] = ret
datastore_results = dict((e, link_dict[e]) for e in remaining_emails)
memcache.set_multi(datastore_results, 300, key_prefix='show_user:')
user_cache.update(datastore_results)
return link_dict
def get_link_for_user(email):
"""Get a link to a user's profile page."""
links = get_links_for_users([email])
return links[email]
@register.filter
def show_user(email, arg=None, _autoescape=None, _memcache_results=None):
"""Render a link to the user's dashboard, with text being the nickname."""
if isinstance(email, users.User):
email = email.email()
if not arg:
user = users.get_current_user()
if user is not None and email == user.email():
return 'me'
ret = get_link_for_user(email)
return django.utils.safestring.mark_safe(ret)
@register.filter
def show_users(email_list, arg=None):
"""Render list of links to each user's dashboard."""
new_email_list = []
for email in email_list:
if isinstance(email, users.User):
email = email.email()
new_email_list.append(email)
links = get_links_for_users(new_email_list)
if not arg:
user = users.get_current_user()
if user is not None:
links[user.email()] = 'me'
return django.utils.safestring.mark_safe(', '.join(
links[email] for email in email_list))
class UrlAppendViewSettingsNode(django.template.Node):
"""Django template tag that appends context and column_width parameter.
This tag should be used after any URL that requires view settings.
Example:
<a href='{%url /foo%}{%urlappend_view_settings%}'>
The tag tries to get the current column width and context from the
template context and if they're present it returns '?param1&param2'
otherwise it returns an empty string.
"""
def __init__(self):
super(UrlAppendViewSettingsNode, self).__init__()
self.view_context = django.template.Variable('context')
self.view_colwidth = django.template.Variable('column_width')
def render(self, context):
"""Returns a HTML fragment."""
url_params = []
current_context = -1
try:
current_context = self.view_context.resolve(context)
except django.template.VariableDoesNotExist:
pass
if current_context is None:
url_params.append('context=')
elif isinstance(current_context, int) and current_context > 0:
url_params.append('context=%d' % current_context)
current_colwidth = None
try:
current_colwidth = self.view_colwidth.resolve(context)
except django.template.VariableDoesNotExist:
pass
if current_colwidth is not None:
url_params.append('column_width=%d' % current_colwidth)
if url_params:
return '?%s' % '&'.join(url_params)
return ''
@register.tag
def urlappend_view_settings(_parser, _token):
"""The actual template tag."""
return UrlAppendViewSettingsNode()
def get_nickname(email, never_me=False, request=None):
"""Return a nickname for an email address.
If 'never_me' is True, 'me' is not returned if 'email' belongs to the
current logged in user. If 'request' is a HttpRequest, it is used to
cache the nickname returned by models.Account.get_nickname_for_email().
"""
if isinstance(email, users.User):
email = email.email()
if not never_me:
if request is not None:
user = request.user
else:
user = users.get_current_user()
if user is not None and email == user.email():
return 'me'
if request is None:
return models.Account.get_nickname_for_email(email)
# _nicknames is injected into request as a cache.
# TODO(maruel): Use memcache instead.
# Access to a protected member _nicknames of a client class
# pylint: disable=W0212
if getattr(request, '_nicknames', None) is None:
request._nicknames = {}
if email in request._nicknames:
return request._nicknames[email]
result = models.Account.get_nickname_for_email(email)
request._nicknames[email] = result
return result
class NicknameNode(django.template.Node):
"""Renders a nickname for a given email address.
The return value is cached if a HttpRequest is available in a
'request' template variable.
The template tag accepts one or two arguments. The first argument is
the template variable for the email address. If the optional second
argument evaluates to True, 'me' as nickname is never rendered.
Example usage:
{% cached_nickname msg.sender %}
{% cached_nickname msg.sender True %}
"""
def __init__(self, email_address, never_me=''):
"""Constructor.
'email_address' is the name of the template variable that holds an
email address. If 'never_me' evaluates to True, 'me' won't be returned.
"""
super(NicknameNode, self).__init__()
self.email_address = django.template.Variable(email_address)
self.never_me = bool(never_me.strip())
self.is_multi = False
def render(self, context):
try:
email = self.email_address.resolve(context)
except django.template.VariableDoesNotExist:
return ''
request = context.get('request')
if self.is_multi:
return ', '.join(get_nickname(e, self.never_me, request) for e in email)
return get_nickname(email, self.never_me, request)
@register.tag
def nickname(_parser, token):
"""Almost the same as nickname filter but the result is cached."""
try:
_, email_address, never_me = token.split_contents()
except ValueError:
try:
_, email_address = token.split_contents()
never_me = ''
except ValueError:
raise django.template.TemplateSyntaxError(
"%r requires exactly one or two arguments" % token.contents.split()[0])
return NicknameNode(email_address, never_me)
@register.tag
def nicknames(parser, token):
"""Wrapper for nickname tag with is_multi flag enabled."""
node = nickname(parser, token)
node.is_multi = True
return node

104
codereview/middleware.py Normal file
View File

@ -0,0 +1,104 @@
# 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.
"""Custom middleware. Some of this may be generally useful."""
import logging
from google.appengine.api import users
from google.appengine.runtime import apiproxy_errors
from google.appengine.runtime import DeadlineExceededError
from django.conf import settings
from django.http import Http404, HttpResponse, HttpResponsePermanentRedirect
from django.template import Context, loader
from codereview import models
class AddHSTSHeaderMiddleware(object):
"""Add HTTP Strict Transport Security header."""
def process_response(self, request, response):
if request.is_secure():
response['Strict-Transport-Security'] = (
'max-age=%d' % settings.HSTS_MAX_AGE)
return response
class AddUserToRequestMiddleware(object):
"""Add a user object and a user_is_admin flag to each request."""
def process_request(self, request):
request.user = users.get_current_user()
request.user_is_admin = users.is_current_user_admin()
# Update the cached value of the current user's Account
account = None
if request.user is not None:
account = models.Account.get_account_for_user(request.user)
models.Account.current_user_account = account
class PropagateExceptionMiddleware(object):
"""Catch exceptions, log them and return a friendly error message.
Disables itself in DEBUG mode.
"""
def _text_requested(self, request):
"""Returns True if a text/plain response is requested."""
# We could use a better heuristics that takes multiple
# media_ranges and quality factors into account. For now we return
# True iff 'text/plain' is the only media range the request
# accepts.
media_ranges = request.META.get('HTTP_ACCEPT', '').split(',')
return len(media_ranges) == 1 and media_ranges[0] == 'text/plain'
def process_exception(self, request, exception):
if settings.DEBUG or isinstance(exception, Http404):
return None
if isinstance(exception, apiproxy_errors.CapabilityDisabledError):
msg = ('Rietveld: App Engine is undergoing maintenance. '
'Please try again in a while.')
status = 503
elif isinstance(exception, (DeadlineExceededError, MemoryError)):
msg = ('Rietveld is too hungry at the moment.'
'Please try again in a while.')
status = 503
else:
msg = 'Unhandled exception.'
status = 500
logging.exception('%s: ' % exception.__class__.__name__)
technical = '%s [%s]' % (exception, exception.__class__.__name__)
if self._text_requested(request):
content = '%s\n\n%s\n' % (msg, technical)
content_type = 'text/plain'
else:
tpl = loader.get_template('exception.html')
ctx = Context({'msg': msg, 'technical': technical})
content = tpl.render(ctx)
content_type = 'text/html'
return HttpResponse(content, status=status, content_type=content_type)
class RedirectToHTTPSMiddleware(object):
"""Redirect HTTP requests to the equivalent HTTPS resource."""
def process_request(self, request):
if settings.DEBUG or request.method == 'POST':
return
if not request.is_secure():
host = request.get_host().split(':')[0]
return HttpResponsePermanentRedirect(
'https://%s%s' % (host, request.get_full_path()))

919
codereview/models.py Normal file
View File

@ -0,0 +1,919 @@
# 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.
"""App Engine data model (schema) definition for Rietveld."""
import logging
import md5
import os
import re
import time
from google.appengine.api import memcache
from google.appengine.api import urlfetch
from google.appengine.api import users
from google.appengine.ext import db
from django.conf import settings
from codereview import patching
from codereview import utils
from codereview.exceptions import FetchError
CONTEXT_CHOICES = (3, 10, 25, 50, 75, 100)
### GQL query cache ###
_query_cache = {}
def gql(cls, clause, *args, **kwds):
"""Return a query object, from the cache if possible.
Args:
cls: a db.Model subclass.
clause: a query clause, e.g. 'WHERE draft = TRUE'.
*args, **kwds: positional and keyword arguments to be bound to the query.
Returns:
A db.GqlQuery instance corresponding to the query with *args and
**kwds bound to the query.
"""
query_string = 'SELECT * FROM %s %s' % (cls.kind(), clause)
query = _query_cache.get(query_string)
if query is None:
_query_cache[query_string] = query = db.GqlQuery(query_string)
query.bind(*args, **kwds)
return query
### Issues, PatchSets, Patches, Contents, Comments, Messages ###
class Issue(db.Model):
"""The major top-level entity.
It has one or more PatchSets as its descendants.
"""
subject = db.StringProperty(required=True)
description = db.TextProperty()
#: in Subversion - repository path (URL) for files in patch set
base = db.StringProperty()
#: if True then base files for patches were uploaded with upload.py
#: (if False - then Rietveld attempts to download them from server)
local_base = db.BooleanProperty(default=False)
repo_guid = db.StringProperty()
owner = db.UserProperty(auto_current_user_add=True, required=True)
created = db.DateTimeProperty(auto_now_add=True)
modified = db.DateTimeProperty(auto_now=True)
reviewers = db.ListProperty(db.Email)
cc = db.ListProperty(db.Email)
closed = db.BooleanProperty(default=False)
private = db.BooleanProperty(default=False)
n_comments = db.IntegerProperty()
_is_starred = None
@property
def is_starred(self):
"""Whether the current user has this issue starred."""
if self._is_starred is not None:
return self._is_starred
account = Account.current_user_account
self._is_starred = account is not None and self.key().id() in account.stars
return self._is_starred
def user_can_edit(self, user):
"""Return true if the given user has permission to edit this issue."""
return user and (user == self.owner or self.is_collaborator(user)
or users.is_current_user_admin())
@property
def edit_allowed(self):
"""Whether the current user can edit this issue."""
account = Account.current_user_account
if account is None:
return False
return self.user_can_edit(account.user)
def update_comment_count(self, n):
"""Increment the n_comments property by n.
If n_comments in None, compute the count through a query. (This
is a transitional strategy while the database contains Issues
created using a previous version of the schema.)
"""
if self.n_comments is None:
self.n_comments = self._get_num_comments()
self.n_comments += n
@property
def num_comments(self):
"""The number of non-draft comments for this issue.
This is almost an alias for self.n_comments, except that if
n_comments is None, it is computed through a query, and stored,
using n_comments as a cache.
"""
if self.n_comments is None:
self.n_comments = self._get_num_comments()
return self.n_comments
def _get_num_comments(self):
"""Helper to compute the number of comments through a query."""
return gql(Comment,
'WHERE ANCESTOR IS :1 AND draft = FALSE',
self).count()
_num_drafts = None
@property
def num_drafts(self):
"""The number of draft comments on this issue for the current user.
The value is expensive to compute, so it is cached.
"""
if self._num_drafts is None:
account = Account.current_user_account
if account is None:
self._num_drafts = 0
else:
query = gql(Comment,
'WHERE ANCESTOR IS :1 AND author = :2 AND draft = TRUE',
self, account.user)
self._num_drafts = query.count()
return self._num_drafts
@staticmethod
def _collaborator_emails_from_description(description):
"""Parses a description, returning collaborator email addresses.
Broken out for unit testing.
"""
collaborators = []
for line in description.splitlines():
m = re.match(
r'\s*COLLABORATOR\s*='
r'\s*([a-zA-Z0-9._]+@[a-zA-Z0-9_]+\.[a-zA-Z0-9._]+)\s*',
line)
if m:
collaborators.append(m.group(1))
return collaborators
def collaborator_emails(self):
"""Returns a possibly empty list of emails specified in
COLLABORATOR= lines.
Note that one COLLABORATOR= lines is required per address.
"""
if not self.description:
return []
return Issue._collaborator_emails_from_description(self.description)
def is_collaborator(self, user):
"""Returns true if the given user is a collaborator on this issue.
This is determined by checking if the user's email is listed as a
collaborator email.
"""
if not user:
return False
return user.email() in self.collaborator_emails()
class PatchSet(db.Model):
"""A set of patchset uploaded together.
This is a descendant of an Issue and has Patches as descendants.
"""
issue = db.ReferenceProperty(Issue) # == parent
message = db.StringProperty()
data = db.BlobProperty()
url = db.LinkProperty()
created = db.DateTimeProperty(auto_now_add=True)
modified = db.DateTimeProperty(auto_now=True)
n_comments = db.IntegerProperty(default=0)
def update_comment_count(self, n):
"""Increment the n_comments property by n."""
self.n_comments = self.num_comments + n
@property
def num_comments(self):
"""The number of non-draft comments for this issue.
This is almost an alias for self.n_comments, except that if
n_comments is None, 0 is returned.
"""
# For older patchsets n_comments is None.
return self.n_comments or 0
class Message(db.Model):
"""A copy of a message sent out in email.
This is a descendant of an Issue.
"""
issue = db.ReferenceProperty(Issue) # == parent
subject = db.StringProperty()
sender = db.EmailProperty()
recipients = db.ListProperty(db.Email)
date = db.DateTimeProperty(auto_now_add=True)
text = db.TextProperty()
draft = db.BooleanProperty(default=False)
in_reply_to = db.SelfReferenceProperty()
issue_was_closed = db.BooleanProperty(default=False)
_approval = None
_disapproval = None
def find(self, text):
"""Returns True when the message says text and is not written by the issue owner."""
# Must not be issue owner.
# Must contain text in a line that doesn't start with '>'.
return self.issue.owner.email() != self.sender and any(
True for line in self.text.lower().splitlines()
if not line.strip().startswith('>') and text in line)
@property
def approval(self):
"""Is True when the message represents an approval of the review."""
if self._approval is None:
self._approval = self.find('lgtm') and not self.find('not lgtm')
return self._approval
@property
def disapproval(self):
"""Is True when the message represents a disapproval of the review."""
if self._disapproval is None:
self._disapproval = self.find('not lgtm')
return self._disapproval
class Content(db.Model):
"""The content of a text file.
This is a descendant of a Patch.
"""
# parent => Patch
text = db.TextProperty()
data = db.BlobProperty()
# Checksum over text or data depending on the type of this content.
checksum = db.TextProperty()
is_uploaded = db.BooleanProperty(default=False)
is_bad = db.BooleanProperty(default=False)
file_too_large = db.BooleanProperty(default=False)
@property
def lines(self):
"""The text split into lines, retaining line endings."""
if not self.text:
return []
return self.text.splitlines(True)
class Patch(db.Model):
"""A single patch, i.e. a set of changes to a single file.
This is a descendant of a PatchSet.
"""
patchset = db.ReferenceProperty(PatchSet) # == parent
filename = db.StringProperty()
status = db.StringProperty() # 'A', 'A +', 'M', 'D' etc
text = db.TextProperty()
content = db.ReferenceProperty(Content)
patched_content = db.ReferenceProperty(Content, collection_name='patch2_set')
is_binary = db.BooleanProperty(default=False)
# Ids of patchsets that have a different version of this file.
delta = db.ListProperty(int)
delta_calculated = db.BooleanProperty(default=False)
_lines = None
@property
def lines(self):
"""The patch split into lines, retaining line endings.
The value is cached.
"""
if self._lines is not None:
return self._lines
if not self.text:
lines = []
else:
lines = self.text.splitlines(True)
self._lines = lines
return lines
_property_changes = None
@property
def property_changes(self):
"""The property changes split into lines.
The value is cached.
"""
if self._property_changes != None:
return self._property_changes
self._property_changes = []
match = re.search('^Property changes on.*\n'+'_'*67+'$', self.text,
re.MULTILINE)
if match:
self._property_changes = self.text[match.end():].splitlines()
return self._property_changes
_num_added = None
@property
def num_added(self):
"""The number of line additions in this patch.
The value is cached.
"""
if self._num_added is None:
self._num_added = self.count_startswith('+') - 1
return self._num_added
_num_removed = None
@property
def num_removed(self):
"""The number of line removals in this patch.
The value is cached.
"""
if self._num_removed is None:
self._num_removed = self.count_startswith('-') - 1
return self._num_removed
_num_chunks = None
@property
def num_chunks(self):
"""The number of 'chunks' in this patch.
A chunk is a block of lines starting with '@@'.
The value is cached.
"""
if self._num_chunks is None:
self._num_chunks = self.count_startswith('@@')
return self._num_chunks
_num_comments = None
@property
def num_comments(self):
"""The number of non-draft comments for this patch.
The value is cached.
"""
if self._num_comments is None:
self._num_comments = gql(Comment,
'WHERE patch = :1 AND draft = FALSE',
self).count()
return self._num_comments
_num_my_comments = None
def num_my_comments(self):
"""The number of non-draft comments for this patch by the logged in user.
The value is cached.
"""
if self._num_my_comments is None:
account = Account.current_user_account
if account is None:
self._num_my_comments = 0
else:
query = gql(Comment,
'WHERE patch = :1 AND draft = FALSE AND author = :2',
self, account.user)
self._num_my_comments = query.count()
return self._num_my_comments
_num_drafts = None
@property
def num_drafts(self):
"""The number of draft comments on this patch for the current user.
The value is expensive to compute, so it is cached.
"""
if self._num_drafts is None:
account = Account.current_user_account
if account is None:
self._num_drafts = 0
else:
query = gql(Comment,
'WHERE patch = :1 AND draft = TRUE AND author = :2',
self, account.user)
self._num_drafts = query.count()
return self._num_drafts
def count_startswith(self, prefix):
"""Returns the number of lines with the specified prefix."""
return len([l for l in self.lines if l.startswith(prefix)])
def get_content(self):
"""Get self.content, or fetch it if necessary.
This is the content of the file to which this patch is relative.
Returns:
a Content instance.
Raises:
FetchError: If there was a problem fetching it.
"""
try:
if self.content is not None:
if self.content.is_bad:
msg = 'Bad content. Try to upload again.'
logging.warn('Patch.get_content: %s', msg)
raise FetchError(msg)
if self.content.is_uploaded and self.content.text == None:
msg = 'Upload in progress.'
logging.warn('Patch.get_content: %s', msg)
raise FetchError(msg)
else:
return self.content
except db.Error:
# This may happen when a Content entity was deleted behind our back.
self.content = None
content = self.fetch_base()
content.put()
self.content = content
self.put()
return content
def get_patched_content(self):
"""Get self.patched_content, computing it if necessary.
This is the content of the file after applying this patch.
Returns:
a Content instance.
Raises:
FetchError: If there was a problem fetching the old content.
"""
try:
if self.patched_content is not None:
return self.patched_content
except db.Error:
# This may happen when a Content entity was deleted behind our back.
self.patched_content = None
old_lines = self.get_content().text.splitlines(True)
logging.info('Creating patched_content for %s', self.filename)
chunks = patching.ParsePatchToChunks(self.lines, self.filename)
new_lines = []
for _, _, new in patching.PatchChunks(old_lines, chunks):
new_lines.extend(new)
text = db.Text(''.join(new_lines))
patched_content = Content(text=text, parent=self)
patched_content.put()
self.patched_content = patched_content
self.put()
return patched_content
@property
def no_base_file(self):
"""Returns True iff the base file is not available."""
return self.content and self.content.file_too_large
def fetch_base(self):
"""Fetch base file for the patch.
Returns:
A models.Content instance.
Raises:
FetchError: For any kind of problem fetching the content.
"""
rev = patching.ParseRevision(self.lines)
if rev is not None:
if rev == 0:
# rev=0 means it's a new file.
return Content(text=db.Text(u''), parent=self)
# AppEngine can only fetch URLs that db.Link() thinks are OK,
# so try converting to a db.Link() here.
try:
base = db.Link(self.patchset.issue.base)
except db.BadValueError:
msg = 'Invalid base URL for fetching: %s' % self.patchset.issue.base
logging.warn(msg)
raise FetchError(msg)
url = utils.make_url(base, self.filename, rev)
logging.info('Fetching %s', url)
try:
result = urlfetch.fetch(url)
except urlfetch.Error, err:
msg = 'Error fetching %s: %s: %s' % (url, err.__class__.__name__, err)
logging.warn('FetchBase: %s', msg)
raise FetchError(msg)
if result.status_code != 200:
msg = 'Error fetching %s: HTTP status %s' % (url, result.status_code)
logging.warn('FetchBase: %s', msg)
raise FetchError(msg)
return Content(text=utils.to_dbtext(utils.unify_linebreaks(result.content)),
parent=self)
class Comment(db.Model):
"""A Comment for a specific line of a specific file.
This is a descendant of a Patch.
"""
patch = db.ReferenceProperty(Patch) # == parent
message_id = db.StringProperty() # == key_name
author = db.UserProperty(auto_current_user_add=True)
date = db.DateTimeProperty(auto_now=True)
lineno = db.IntegerProperty()
text = db.TextProperty()
left = db.BooleanProperty()
draft = db.BooleanProperty(required=True, default=True)
buckets = None
shorttext = None
def complete(self):
"""Set the shorttext and buckets attributes."""
# TODO(guido): Turn these into caching proprties instead.
# The strategy for buckets is that we want groups of lines that
# start with > to be quoted (and not displayed by
# default). Whitespace-only lines are not considered either quoted
# or not quoted. Same goes for lines that go like "On ... user
# wrote:".
cur_bucket = []
quoted = None
self.buckets = []
def _Append():
if cur_bucket:
self.buckets.append(Bucket(text="\n".join(cur_bucket),
quoted=bool(quoted)))
lines = self.text.splitlines()
for line in lines:
if line.startswith("On ") and line.endswith(":"):
pass
elif line.startswith(">"):
if quoted is False:
_Append()
cur_bucket = []
quoted = True
elif line.strip():
if quoted is True:
_Append()
cur_bucket = []
quoted = False
cur_bucket.append(line)
_Append()
self.shorttext = self.text.lstrip()[:50].rstrip()
# Grab the first 50 chars from the first non-quoted bucket
for bucket in self.buckets:
if not bucket.quoted:
self.shorttext = bucket.text.lstrip()[:50].rstrip()
break
class Bucket(db.Model):
"""A 'Bucket' of text.
A comment may consist of multiple text buckets, some of which may be
collapsed by default (when they represent quoted text).
NOTE: This entity is never written to the database. See Comment.complete().
"""
# TODO(guido): Flesh this out.
text = db.TextProperty()
quoted = db.BooleanProperty()
### Repositories and Branches ###
class Repository(db.Model):
"""A specific Subversion repository."""
name = db.StringProperty(required=True)
url = db.LinkProperty(required=True)
owner = db.UserProperty(auto_current_user_add=True)
guid = db.StringProperty() # global unique repository id
def __str__(self):
return self.name
class Branch(db.Model):
"""A trunk, branch, or a tag in a specific Subversion repository."""
repo = db.ReferenceProperty(Repository, required=True)
# Cache repo.name as repo_name, to speed up set_branch_choices()
# in views.IssueBaseForm.
repo_name = db.StringProperty()
category = db.StringProperty(required=True,
choices=('*trunk*', 'branch', 'tag'))
name = db.StringProperty(required=True)
url = db.LinkProperty(required=True)
owner = db.UserProperty(auto_current_user_add=True)
### Accounts ###
class Account(db.Model):
"""Maps a user or email address to a user-selected nickname, and more.
Nicknames do not have to be unique.
The default nickname is generated from the email address by
stripping the first '@' sign and everything after it. The email
should not be empty nor should it start with '@' (AssertionError
error is raised if either of these happens).
This also holds a list of ids of starred issues. The expectation
that you won't have more than a dozen or so starred issues (a few
hundred in extreme cases) and the memory used up by a list of
integers of that size is very modest, so this is an efficient
solution. (If someone found a use case for having thousands of
starred issues we'd have to think of a different approach.)
"""
user = db.UserProperty(auto_current_user_add=True, required=True)
email = db.EmailProperty(required=True) # key == <email>
nickname = db.StringProperty(required=True)
default_context = db.IntegerProperty(default=settings.DEFAULT_CONTEXT,
choices=CONTEXT_CHOICES)
default_column_width = db.IntegerProperty(
default=settings.DEFAULT_COLUMN_WIDTH)
created = db.DateTimeProperty(auto_now_add=True)
modified = db.DateTimeProperty(auto_now=True)
stars = db.ListProperty(int) # Issue ids of all starred issues
fresh = db.BooleanProperty()
uploadpy_hint = db.BooleanProperty(default=True)
notify_by_email = db.BooleanProperty(default=True)
notify_by_chat = db.BooleanProperty(default=False)
# Spammer; only blocks sending messages, not uploading issues.
blocked = db.BooleanProperty(default=False)
# Current user's Account. Updated by middleware.AddUserToRequestMiddleware.
current_user_account = None
lower_email = db.StringProperty()
lower_nickname = db.StringProperty()
xsrf_secret = db.BlobProperty()
# Note that this doesn't get called when doing multi-entity puts.
def put(self):
self.lower_email = str(self.email).lower()
self.lower_nickname = self.nickname.lower()
super(Account, self).put()
@classmethod
def get_account_for_user(cls, user):
"""Get the Account for a user, creating a default one if needed."""
email = user.email()
assert email
key = '<%s>' % email
# Since usually the account already exists, first try getting it
# without the transaction implied by get_or_insert().
account = cls.get_by_key_name(key)
if account is not None:
return account
nickname = cls.create_nickname_for_user(user)
return cls.get_or_insert(key, user=user, email=email, nickname=nickname,
fresh=True)
@classmethod
def create_nickname_for_user(cls, user):
"""Returns a unique nickname for a user."""
name = nickname = user.email().split('@', 1)[0]
next_char = chr(ord(nickname[0].lower())+1)
existing_nicks = [account.lower_nickname
for account in cls.gql(('WHERE lower_nickname >= :1 AND '
'lower_nickname < :2'),
nickname.lower(), next_char)]
suffix = 0
while nickname.lower() in existing_nicks:
suffix += 1
nickname = '%s%d' % (name, suffix)
return nickname
@classmethod
def get_nickname_for_user(cls, user):
"""Get the nickname for a user."""
return cls.get_account_for_user(user).nickname
@classmethod
def get_account_for_email(cls, email):
"""Get the Account for an email address, or return None."""
assert email
key = '<%s>' % email
return cls.get_by_key_name(key)
@classmethod
def get_accounts_for_emails(cls, emails):
"""Get the Accounts for each of a list of email addresses."""
return cls.get_by_key_name(['<%s>' % email for email in emails])
@classmethod
def get_by_key_name(cls, key, **kwds):
"""Override db.Model.get_by_key_name() to use cached value if possible."""
if not kwds and cls.current_user_account is not None:
if key == cls.current_user_account.key().name():
return cls.current_user_account
return super(Account, cls).get_by_key_name(key, **kwds)
@classmethod
def get_multiple_accounts_by_email(cls, emails):
"""Get multiple accounts. Returns a dict by email."""
results = {}
keys = []
for email in emails:
if cls.current_user_account and email == cls.current_user_account.email:
results[email] = cls.current_user_account
else:
keys.append('<%s>' % email)
if keys:
accounts = cls.get_by_key_name(keys)
for account in accounts:
if account is not None:
results[account.email] = account
return results
@classmethod
def get_nickname_for_email(cls, email, default=None):
"""Get the nickname for an email address, possibly a default.
If default is None a generic nickname is computed from the email
address.
Args:
email: email address.
default: If given and no account is found, returned as the default value.
Returns:
Nickname for given email.
"""
account = cls.get_account_for_email(email)
if account is not None and account.nickname:
return account.nickname
if default is not None:
return default
return email.replace('@', '_')
@classmethod
def get_account_for_nickname(cls, nickname):
"""Get the list of Accounts that have this nickname."""
assert nickname
assert '@' not in nickname
return cls.all().filter('lower_nickname =', nickname.lower()).get()
@classmethod
def get_email_for_nickname(cls, nickname):
"""Turn a nickname into an email address.
If the nickname is not unique or does not exist, this returns None.
"""
account = cls.get_account_for_nickname(nickname)
if account is None:
return None
return account.email
def user_has_selected_nickname(self):
"""Return True if the user picked the nickname.
Normally this returns 'not self.fresh', but if that property is
None, we assume that if the created and modified timestamp are
within 2 seconds, the account is fresh (i.e. the user hasn't
selected a nickname yet). We then also update self.fresh, so it
is used as a cache and may even be written back if we're lucky.
"""
if self.fresh is None:
delta = self.created - self.modified
# Simulate delta = abs(delta)
if delta.days < 0:
delta = -delta
self.fresh = (delta.days == 0 and delta.seconds < 2)
return not self.fresh
_drafts = None
@property
def drafts(self):
"""A list of issue ids that have drafts by this user.
This is cached in memcache.
"""
if self._drafts is None:
if self._initialize_drafts():
self._save_drafts()
return self._drafts
def update_drafts(self, issue, have_drafts=None):
"""Update the user's draft status for this issue.
Args:
issue: an Issue instance.
have_drafts: optional bool forcing the draft status. By default,
issue.num_drafts is inspected (which may query the datastore).
The Account is written to the datastore if necessary.
"""
dirty = False
if self._drafts is None:
dirty = self._initialize_drafts()
id = issue.key().id()
if have_drafts is None:
have_drafts = bool(issue.num_drafts) # Beware, this may do a query.
if have_drafts:
if id not in self._drafts:
self._drafts.append(id)
dirty = True
else:
if id in self._drafts:
self._drafts.remove(id)
dirty = True
if dirty:
self._save_drafts()
def _initialize_drafts(self):
"""Initialize self._drafts from scratch.
This mostly exists as a schema conversion utility.
Returns:
True if the user should call self._save_drafts(), False if not.
"""
drafts = memcache.get('user_drafts:' + self.email)
if drafts is not None:
self._drafts = drafts
##logging.info('HIT: %s -> %s', self.email, self._drafts)
return False
# We're looking for the Issue key id. The ancestry of comments goes:
# Issue -> PatchSet -> Patch -> Comment.
issue_ids = set(comment.key().parent().parent().parent().id()
for comment in gql(Comment,
'WHERE author = :1 AND draft = TRUE',
self.user))
self._drafts = list(issue_ids)
##logging.info('INITIALIZED: %s -> %s', self.email, self._drafts)
return True
def _save_drafts(self):
"""Save self._drafts to memcache."""
##logging.info('SAVING: %s -> %s', self.email, self._drafts)
memcache.set('user_drafts:' + self.email, self._drafts, 3600)
def get_xsrf_token(self, offset=0):
"""Return an XSRF token for the current user."""
# This code assumes that
# self.user.email() == users.get_current_user().email()
current_user = users.get_current_user()
if self.user.user_id() != current_user.user_id():
# Mainly for Google Account plus conversion.
logging.info('Updating user_id for %s from %s to %s' % (
self.user.email(), self.user.user_id(), current_user.user_id()))
self.user = current_user
self.put()
if not self.xsrf_secret:
self.xsrf_secret = os.urandom(8)
self.put()
m = md5.new(self.xsrf_secret)
email_str = self.lower_email
if isinstance(email_str, unicode):
email_str = email_str.encode('utf-8')
m.update(self.lower_email)
when = int(time.time()) // 3600 + offset
m.update(str(when))
return m.hexdigest()

260
codereview/patching.py Normal file
View File

@ -0,0 +1,260 @@
# 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.
"""Utility to read and apply a unified diff without forking patch(1).
For a discussion of the unified diff format, see my blog on Artima:
http://www.artima.com/weblogs/viewpost.jsp?thread=164293
"""
import difflib
import logging
import patiencediff
import re
import sys
_CHUNK_RE = re.compile(r"""
@@
\s+
-
(?: (\d+) (?: , (\d+) )?)
\s+
\+
(?: (\d+) (?: , (\d+) )?)
\s+
@@
""", re.VERBOSE)
def PatchLines(old_lines, patch_lines, name="<patch>"):
"""Patches the old_lines with patches read from patch_lines.
This only reads unified diffs. The header lines are ignored.
Yields (tag, old, new) tuples where old and new are lists of lines.
The tag can either start with "error" or be a tag from difflib: "equal",
"insert", "delete", "replace". After "error" is yielded, no more
tuples are yielded. It is possible that consecutive "equal" tuples
are yielded.
"""
chunks = ParsePatchToChunks(patch_lines, name)
if chunks is None:
return iter([("error: ParsePatchToChunks failed", [], [])])
return PatchChunks(old_lines, chunks)
def PatchChunks(old_lines, chunks):
"""Patche old_lines with chunks.
Yields (tag, old, new) tuples where old and new are lists of lines.
The tag can either start with "error" or be a tag from difflib: "equal",
"insert", "delete", "replace". After "error" is yielded, no more
tuples are yielded. It is possible that consecutive "equal" tuples
are yielded.
"""
if not chunks:
# The patch is a no-op
yield ("equal", old_lines, old_lines)
return
old_pos = 0
for (old_i, old_j), _, old_chunk, new_chunk in chunks:
eq = old_lines[old_pos:old_i]
if eq:
yield "equal", eq, eq
old_pos = old_i
# Check that the patch matches the target file
if old_lines[old_i:old_j] != old_chunk:
logging.warn("mismatch:%s.%s.", old_lines[old_i:old_j], old_chunk)
yield ("error: old chunk mismatch", old_lines[old_i:old_j], old_chunk)
return
# TODO(guido): ParsePatch knows the diff details, but throws the info away
sm = patiencediff.PseudoPatienceSequenceMatcher(None, old_chunk, new_chunk)
for tag, i1, i2, j1, j2 in sm.get_opcodes():
yield tag, old_chunk[i1:i2], new_chunk[j1:j2]
old_pos = old_j
# Copy the final matching chunk if any.
eq = old_lines[old_pos:]
if eq:
yield ("equal", eq, eq)
def ParseRevision(lines):
"""Parse the revision number out of the raw lines of the patch.
Returns 0 (new file) if no revision number was found.
"""
for line in lines[:10]:
if line.startswith('@'):
break
m = re.match(r'---\s.*\(.*\s(\d+)\)\s*$', line)
if m:
return int(m.group(1))
return 0
_NO_NEWLINE_MESSAGE = "\\ No newline at end of file"
def ParsePatchToChunks(lines, name="<patch>"):
"""Parses a patch from a list of lines.
Return a list of chunks, where each chunk is a tuple:
old_range, new_range, old_lines, new_lines
Returns a list of chunks (possibly empty); or None if there's a problem.
"""
lineno = 0
raw_chunk = []
chunks = []
old_range = new_range = None
old_last = new_last = 0
in_prelude = True
for line in lines:
lineno += 1
if in_prelude:
# Skip leading lines until after we've seen one starting with '+++'
if line.startswith("+++"):
in_prelude = False
continue
match = _CHUNK_RE.match(line)
if match:
if raw_chunk:
# Process the lines in the previous chunk
old_chunk = []
new_chunk = []
for tag, rest in raw_chunk:
if tag in (" ", "-"):
old_chunk.append(rest)
if tag in (" ", "+"):
new_chunk.append(rest)
# Check consistency
old_i, old_j = old_range
new_i, new_j = new_range
if len(old_chunk) != old_j - old_i or len(new_chunk) != new_j - new_i:
logging.warn("%s:%s: previous chunk has incorrect length",
name, lineno)
return None
chunks.append((old_range, new_range, old_chunk, new_chunk))
raw_chunk = []
# Parse the @@ header
old_ln, old_n, new_ln, new_n = match.groups()
old_ln, old_n, new_ln, new_n = map(long,
(old_ln, old_n or 1,
new_ln, new_n or 1))
# Convert the numbers to list indices we can use
if old_n == 0:
old_i = old_ln
else:
old_i = old_ln - 1
old_j = old_i + old_n
old_range = old_i, old_j
if new_n == 0:
new_i = new_ln
else:
new_i = new_ln - 1
new_j = new_i + new_n
new_range = new_i, new_j
# Check header consistency with previous header
if old_i < old_last or new_i < new_last:
logging.warn("%s:%s: chunk header out of order: %r",
name, lineno, line)
return None
if old_i - old_last != new_i - new_last:
logging.warn("%s:%s: inconsistent chunk header: %r",
name, lineno, line)
return None
old_last = old_j
new_last = new_j
else:
tag, rest = line[0], line[1:]
if tag in (" ", "-", "+"):
raw_chunk.append((tag, rest))
elif line.startswith(_NO_NEWLINE_MESSAGE):
# TODO(guido): need to check that no more lines follow for this file
if raw_chunk:
last_tag, last_rest = raw_chunk[-1]
if last_rest.endswith("\n"):
raw_chunk[-1] = (last_tag, last_rest[:-1])
else:
# Only log if it's a non-blank line. Blank lines we see a lot.
if line and line.strip():
logging.warn("%s:%d: indecypherable input: %r", name, lineno, line)
if chunks or raw_chunk:
break # Trailing garbage isn't so bad
return None
if raw_chunk:
# Process the lines in the last chunk
old_chunk = []
new_chunk = []
for tag, rest in raw_chunk:
if tag in (" ", "-"):
old_chunk.append(rest)
if tag in (" ", "+"):
new_chunk.append(rest)
# Check consistency
old_i, old_j = old_range
new_i, new_j = new_range
if len(old_chunk) != old_j - old_i or len(new_chunk) != new_j - new_i:
print >> sys.stderr, ("%s:%s: last chunk has incorrect length" %
(name, lineno))
return None
chunks.append((old_range, new_range, old_chunk, new_chunk))
raw_chunk = []
return chunks
def ParsePatchToLines(lines):
"""Parses a patch from a list of lines.
Returns None on error, otherwise a list of 3-tuples:
(old_line_no, new_line_no, line)
A line number can be 0 if it doesn't exist in the old/new file.
"""
# TODO: can we share some of this code with ParsePatchToChunks?
result = []
in_prelude = True
for line in lines:
if in_prelude:
result.append((0, 0, line))
# Skip leading lines until after we've seen one starting with '+++'
if line.startswith("+++"):
in_prelude = False
elif line.startswith("@"):
result.append((0, 0, line))
match = _CHUNK_RE.match(line)
if not match:
logging.warn("ParsePatchToLines match failed on %s", line)
return None
old_ln = int(match.groups()[0])
new_ln = int(match.groups()[2])
else:
if line[0] == "-":
result.append((old_ln, 0, line))
old_ln += 1
elif line[0] == "+":
result.append((0, new_ln, line))
new_ln += 1
elif line[0] == " ":
result.append((old_ln, new_ln, line))
old_ln += 1
new_ln += 1
elif line.startswith(_NO_NEWLINE_MESSAGE):
continue
else: # Something else, could be property changes etc.
result.append((0, 0, line))
return result

View File

@ -0,0 +1,85 @@
# Copyright (C) 2012 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.
import difflib
class PseudoPatienceSequenceMatcher(difflib.SequenceMatcher):
"""Provides a SequenceMatcher that prefers longer "first" matches to longer
"second" matches.
"""
def get_matching_blocks(self):
"""Returns list of triples describing matching subsequences.
Each triple is of the form (i, j, n), and means that a[i:i+n] == b[j:j+n].
The triples are monotonically increasing in i and j.
The last triple is a dummy, and has the value (len(a), len(b), 0). It is the
only triple with n == 0. If (i, j, n) and (i', j', n') are adjacent triples
in the list, and the second is not the last triple in the list, then
i+n != i' or j+n != j'; in other words, adjacent triples always describe
non-adjacent equal blocks.
"""
matches = difflib.SequenceMatcher.get_matching_blocks(self)
# Make sure all elements are of type difflib.Match.
for index in xrange(len(matches)):
if not isinstance(matches[index], difflib.Match):
matches[index] = difflib.Match(matches[index][0],
matches[index][1],
matches[index][2])
# Check if there's a match at the beginning of the current region, and
# insert a new Match object at the beginning of |matches| if necessary.
if matches[0].a != matches[0].b:
match_length = 0
is_a = matches[0].a < matches[0].b
index = matches[0].a if is_a else matches[0].b
while (index + match_length < len(self.a) and
index + match_length < len(self.b) and
self.a[index + match_length] == self.b[index + match_length]):
match_length += 1
if match_length:
matches[0] = difflib.Match(
(index if is_a else matches[0].a) + match_length,
(matches[0].b if is_a else index) + match_length,
matches[0].size - match_length)
if matches[0].size == 0:
matches[0] = difflib.Match(index, index, match_length)
else:
matches.insert(0, difflib.Match(index, index, match_length))
if len(matches) < 2:
return matches
# For all pairs of Match objects, prefer a longer |first| Match if the end
# of the first match is the same as the beginning of the second match.
for index in xrange(len(matches) - 2):
first = matches[index]
second = matches[index + 1]
while True:
if (first.a + first.size < len(self.a) and
first.b + first.size < len(self.b) and
second.a < len(self.a) and second.b < len(self.b) and
self.a[first.a + first.size] == self.b[first.b + first.size] and
self.a[second.a] == self.b[second.b]):
first = difflib.Match(first.a, first.b, first.size + 1)
second = difflib.Match(second.a + 1, second.b + 1, second.size - 1)
else:
break
matches[index] = first
matches[index + 1] = second
return matches

102
codereview/urls.py Normal file
View File

@ -0,0 +1,102 @@
# 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.
"""URL mappings for the codereview package."""
# NOTE: Must import *, since Django looks for things here, e.g. handler500.
from django.conf.urls.defaults import *
import django.views.defaults
from codereview import feeds
urlpatterns = patterns(
'codereview.views',
(r'^$', 'index'),
(r'^all$', 'all'),
(r'^mine$', 'mine'),
(r'^starred$', 'starred'),
(r'^new$', 'new'),
(r'^upload$', 'upload'),
(r'^(\d+)$', 'show', {}, 'show_bare_issue_number'),
(r'^(\d+)/(show)?$', 'show'),
(r'^(\d+)/add$', 'add'),
(r'^(\d+)/edit$', 'edit'),
(r'^(\d+)/delete$', 'delete'),
(r'^(\d+)/close$', 'close'),
(r'^(\d+)/mail$', 'mailissue'),
(r'^(\d+)/publish$', 'publish'),
(r'^download/issue(\d+)_(\d+)\.diff', 'download'),
(r'^download/issue(\d+)_(\d+)_(\d+)\.diff', 'download_patch'),
(r'^(\d+)/patch/(\d+)/(\d+)$', 'patch'),
(r'^(\d+)/image/(\d+)/(\d+)/(\d+)$', 'image'),
(r'^(\d+)/diff/(\d+)/(.+)$', 'diff'),
(r'^(\d+)/diff2/(\d+):(\d+)/(.+)$', 'diff2'),
(r'^(\d+)/diff_skipped_lines/(\d+)/(\d+)/(\d+)/(\d+)/([tba])/(\d+)$',
'diff_skipped_lines'),
(r'^(\d+)/diff_skipped_lines/(\d+)/(\d+)/$',
django.views.defaults.page_not_found, {}, 'diff_skipped_lines_prefix'),
(r'^(\d+)/diff2_skipped_lines/(\d+):(\d+)/(\d+)/(\d+)/(\d+)/([tba])/(\d+)$',
'diff2_skipped_lines'),
(r'^(\d+)/diff2_skipped_lines/(\d+):(\d+)/(\d+)/$',
django.views.defaults.page_not_found, {}, 'diff2_skipped_lines_prefix'),
(r'^(\d+)/upload_content/(\d+)/(\d+)$', 'upload_content'),
(r'^(\d+)/upload_patch/(\d+)$', 'upload_patch'),
(r'^(\d+)/upload_complete/(\d+)?$', 'upload_complete'),
(r'^(\d+)/description$', 'description'),
(r'^(\d+)/fields', 'fields'),
(r'^(\d+)/star$', 'star'),
(r'^(\d+)/unstar$', 'unstar'),
(r'^(\d+)/draft_message$', 'draft_message'),
(r'^api/(\d+)/?$', 'api_issue'),
(r'^api/(\d+)/(\d+)/?$', 'api_patchset'),
(r'^user/([^/]+)$', 'show_user'),
(r'^user/([^/]+)/block$', 'block_user'),
(r'^inline_draft$', 'inline_draft'),
(r'^repos$', 'repos'),
(r'^repo_new$', 'repo_new'),
(r'^repo_init$', 'repo_init'),
(r'^branch_new/(\d+)$', 'branch_new'),
(r'^branch_edit/(\d+)$', 'branch_edit'),
(r'^branch_delete/(\d+)$', 'branch_delete'),
(r'^settings$', 'settings'),
(r'^account_delete$', 'account_delete'),
(r'^migrate_entities$', 'migrate_entities'),
(r'^user_popup/(.+)$', 'user_popup'),
(r'^(\d+)/patchset/(\d+)$', 'patchset'),
(r'^(\d+)/patchset/(\d+)/delete$', 'delete_patchset'),
(r'^account$', 'account'),
(r'^use_uploadpy$', 'use_uploadpy'),
(r'^_ah/xmpp/message/chat/', 'incoming_chat'),
(r'^_ah/mail/(.*)', 'incoming_mail'),
(r'^xsrf_token$', 'xsrf_token'),
# patching upload.py on the fly
(r'^static/upload.py$', 'customized_upload_py'),
(r'^search$', 'search'),
(r'^tasks/calculate_delta$', 'calculate_delta'),
(r'^tasks/migrate_entities$', 'task_migrate_entities'),
)
feed_dict = {
'reviews': feeds.ReviewsFeed,
'closed': feeds.ClosedFeed,
'mine' : feeds.MineFeed,
'all': feeds.AllFeed,
'issue' : feeds.OneIssueFeed,
}
urlpatterns += patterns(
'',
(r'^rss/(?P<url>.*)$', 'django.contrib.syndication.views.feed',
{'feed_dict': feed_dict}),
)

96
codereview/utils.py Normal file
View File

@ -0,0 +1,96 @@
# Copyright 2011 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.
"""Collection of helper functions."""
import urlparse
from google.appengine.ext import db
from codereview.exceptions import FetchError
def make_url(base, filename, rev):
"""Helper to construct the URL to fetch.
Args:
base: The base property of the Issue to which the Patch belongs.
filename: The filename property of the Patch instance.
rev: Revision number, or None for head revision.
Returns:
A URL referring to the given revision of the file.
"""
scheme, netloc, path, _, _, _ = urlparse.urlparse(base)
if netloc.endswith(".googlecode.com"):
# Handle Google code repositories
if rev is None:
raise FetchError("Can't access googlecode.com without a revision")
if not path.startswith("/svn/"):
raise FetchError( "Malformed googlecode.com URL (%s)" % base)
path = path[5:] # Strip "/svn/"
url = "%s://%s/svn-history/r%d/%s/%s" % (scheme, netloc, rev,
path, filename)
return url
elif netloc.endswith("sourceforge.net") and rev is not None:
if path.strip().endswith("/"):
path = path.strip()[:-1]
else:
path = path.strip()
splitted_path = path.split("/")
url = "%s://%s/%s/!svn/bc/%d/%s/%s" % (scheme, netloc,
"/".join(splitted_path[1:3]), rev,
"/".join(splitted_path[3:]),
filename)
return url
# Default for viewvc-based URLs (svn.python.org)
url = base
if not url.endswith('/'):
url += '/'
url += filename
if rev is not None:
url += '?rev=%s' % rev
return url
def to_dbtext(text):
"""Helper to turn a string into a db.Text instance.
Args:
text: a string.
Returns:
A db.Text instance.
"""
if isinstance(text, unicode):
# A TypeError is raised if text is unicode and an encoding is given.
return db.Text(text)
else:
try:
return db.Text(text, encoding='utf-8')
except UnicodeDecodeError:
return db.Text(text, encoding='latin-1')
def unify_linebreaks(text):
"""Helper to return a string with all line breaks converted to LF.
Args:
text: a string.
Returns:
A string with all line breaks converted to LF.
"""
return text.replace('\r\n', '\n').replace('\r', '\n')

4140
codereview/views.py Normal file

File diff suppressed because it is too large Load Diff

101
main.py Normal file
View File

@ -0,0 +1,101 @@
# 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.
"""Main program for Rietveld.
This is also a template for running a Django app under Google App
Engine, especially when using a newer version of Django than provided
in the App Engine standard library.
The site-specific code is all in other files: urls.py, models.py,
views.py, settings.py.
"""
# Standard Python imports.
import os
import sys
import logging
# Log a message each time this module get loaded.
logging.info('Loading %s, app version = %s',
__name__, os.getenv('CURRENT_VERSION_ID'))
import appengine_config
# AppEngine imports.
from google.appengine.ext.webapp import util
# Import webapp.template. This makes most Django setup issues go away.
from google.appengine.ext.webapp import template
# Import various parts of Django.
import django.core.handlers.wsgi
import django.core.signals
import django.db
import django.dispatch.dispatcher
import django.forms
def log_exception(*args, **kwds):
"""Django signal handler to log an exception."""
cls, err = sys.exc_info()[:2]
logging.exception('Exception in request: %s: %s', cls.__name__, err)
# Log all exceptions detected by Django.
django.core.signals.got_request_exception.connect(log_exception)
# Unregister Django's default rollback event handler.
django.core.signals.got_request_exception.disconnect(
django.db._rollback_on_exception)
# Create a Django application for WSGI.
application = django.core.handlers.wsgi.WSGIHandler()
def real_main():
"""Main program."""
# Run the WSGI CGI handler with that application.
util.run_wsgi_app(application)
def profile_main():
"""Main program for profiling."""
import cProfile
import pstats
import StringIO
prof = cProfile.Profile()
prof = prof.runctx('real_main()', globals(), locals())
stream = StringIO.StringIO()
stats = pstats.Stats(prof, stream=stream)
# stats.strip_dirs() # Don't; too many modules are named __init__.py.
stats.sort_stats('time') # 'time', 'cumulative' or 'calls'
stats.print_stats() # Optional arg: how many to print
# The rest is optional.
# stats.print_callees()
# stats.print_callers()
print '\n<hr>'
print '<h1>Profile</h1>'
print '<pre>'
print stream.getvalue()[:1000000]
print '</pre>'
# Set this to profile_main to enable profiling.
main = real_main
if __name__ == '__main__':
main()

283
pylintrc Normal file
View File

@ -0,0 +1,283 @@
[MASTER]
# Specify a configuration file.
#rcfile=
# Python code to execute, usually for sys.path manipulation such as
# pygtk.require().
#init-hook=
# Profiled execution.
profile=no
# Add files or directories to the blacklist. They should be base names, not
# paths.
ignore=CVS
# Pickle collected data for later comparisons.
persistent=yes
# List of plugins (as comma separated values of python modules names) to load,
# usually to register additional checkers.
load-plugins=
[MESSAGES CONTROL]
# Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option
# multiple time.
#enable=
# Disable the message, report, category or checker with the given id(s). You
# can either give multiple identifier separated by comma (,) or put this option
# multiple time (only on the command line, not in the configuration file where
# it should appear only once).
#
# These warnings are annoying, disable them:
# C0111: Missing docstring
# C0302: Too many lines in module (NN)
# I0011: Locally disabling W0000
# R0201: Method could be a function
# R0801: Similar lines in N files
# R0902: Too many instance attributes (N/7)
# R0903: Too few public methods (N/2)
# R0904: Too many public methods (N/20)
# R0911: Too many return statements (N/6)
# R0912: Too many branches (NN/12)
# R0913: Too many arguments (NN/5)
# R0914: Too many local variables (NN/15)
# R0915: Too many statements (NN/50)
# W0511: TODO
# W0401: Wildcard import FOO
# W0141: Used builtin function 'map'
# W0142: Used * or ** magic
# W0232: Class has no __init__ method
# W0603: Using the global statement
# W0614: Unused import FOO from wildcard import
# W0703: Catch "Exception"
# W1201: Specify string format arguments as logging function parameters
#
# These warnings are genuine, we should add them back, by potentially only
# disabling at the lines generating a false positive:
# C0103: Invalid name "FOO" (should match [a-z_][a-z0-9_]{2,30}$)
# E1103: Instance of 'FOO' has no 'BAR' member (but some types could not be inferred)
# W0621: Redefining name 'FOO' from outer scope (line NN)
# W0622: Redefining built-in 'FOO'
# W0702: No exception type(s) specified
#
disable=C0103,C0111,C0302,E1103,I0011,R0201,R0801,R0902,R0903,R0904,R0911,R0912,R0913,R0914,R0915,W0141,W0142,W0232,W0401,W0511,W0603,W0614,W0621,W0622,W0702,W0703,W1201
[REPORTS]
# Set the output format. Available formats are text, parseable, colorized, msvs
# (visual studio) and html
output-format=text
# Include message's id in output
include-ids=yes
# Put messages in a separate file for each module / package specified on the
# command line instead of printing them on stdout. Reports (if any) will be
# written in a file name "pylint_global.[txt|html]".
files-output=no
# Tells whether to display a full report or only the messages
reports=no
# Python expression which should return a note less than 10 (10 is the highest
# note). You have access to the variables errors warning, statement which
# respectively contain the number of errors / warnings messages and the total
# number of statements analyzed. This is used by the global evaluation report
# (RP0004).
evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
# Add a comment according to your evaluation note. This is used by the global
# evaluation report (RP0004).
comment=no
[VARIABLES]
# Tells whether we should check for unused import in __init__ files.
init-import=no
# A regular expression matching the beginning of the name of dummy variables
# (i.e. not used).
dummy-variables-rgx=_|dummy
# List of additional names supposed to be defined in builtins. Remember that
# you should avoid to define new builtins when possible.
additional-builtins=
[TYPECHECK]
# Tells whether missing members accessed in mixin class should be ignored. A
# mixin class is detected if its name ends with "mixin" (case insensitive).
ignore-mixin-members=yes
# List of classes names for which member attributes should not be checked
# (useful for classes with attributes dynamically set).
ignored-classes=SQLObject,google.appengine.api.memcache
# When zope mode is activated, add a predefined set of Zope acquired attributes
# to generated-members.
zope=no
# List of members which are set dynamically and missed by pylint inference
# system, and so shouldn't trigger E0201 when accessed. Python regular
# expressions are accepted.
generated-members=REQUEST,acl_users,aq_parent
[MISCELLANEOUS]
# List of note tags to take in consideration, separated by a comma.
notes=FIXME,XXX,TODO
[SIMILARITIES]
# Minimum lines number of a similarity.
min-similarity-lines=4
# Ignore comments when computing similarities.
ignore-comments=yes
# Ignore docstrings when computing similarities.
ignore-docstrings=yes
[FORMAT]
# Maximum number of characters on a single line.
max-line-length=80
# Maximum number of lines in a module
max-module-lines=1000
# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
# tab).
# In rietveld, 2 spaces indents are used.
indent-string=' '
[BASIC]
# Required attributes for module, separated by a comma
required-attributes=
# List of builtins function names that should not be used, separated by a comma
bad-functions=map,filter,apply,input
# Regular expression which should only match correct module names
module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
# Regular expression which should only match correct module level names
const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$
# Regular expression which should only match correct class names
class-rgx=[A-Z_][a-zA-Z0-9]+$
# Regular expression which should only match correct function names
function-rgx=[a-z_][a-z0-9_]{2,30}$
# Regular expression which should only match correct method names
method-rgx=[a-z_][a-z0-9_]{2,30}$
# Regular expression which should only match correct instance attribute names
attr-rgx=[a-z_][a-z0-9_]{2,30}$
# Regular expression which should only match correct argument names
argument-rgx=[a-z_][a-z0-9_]{2,30}$
# Regular expression which should only match correct variable names
variable-rgx=[a-z_][a-z0-9_]{2,30}$
# Regular expression which should only match correct list comprehension /
# generator expression variable names
inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$
# Good variable names which should always be accepted, separated by a comma
good-names=i,j,k,ex,Run,_
# Bad variable names which should always be refused, separated by a comma
bad-names=foo,bar,baz,toto,tutu,tata
# Regular expression which should only match functions or classes name which do
# not require a docstring
no-docstring-rgx=__.*__
[DESIGN]
# Maximum number of arguments for function / method
max-args=5
# Argument names that match this expression will be ignored. Default to name
# with leading underscore
ignored-argument-names=_.*
# Maximum number of locals for function / method body
max-locals=15
# Maximum number of return / yield for function / method body
max-returns=6
# Maximum number of branch for function / method body
max-branchs=12
# Maximum number of statements in function / method body
max-statements=50
# Maximum number of parents for a class (see R0901).
max-parents=7
# Maximum number of attributes for a class (see R0902).
max-attributes=7
# Minimum number of public methods for a class (see R0903).
min-public-methods=2
# Maximum number of public methods for a class (see R0904).
max-public-methods=20
[CLASSES]
# List of interface methods to ignore, separated by a comma. This is used for
# instance to not check methods defines in Zope's Interface base class.
ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by
# List of method names used to declare (i.e. assign) instance attributes.
defining-attr-methods=__init__,__new__,setUp
# List of valid names for the first argument in a class method.
valid-classmethod-first-arg=cls
[IMPORTS]
# Deprecated modules which should not be used, separated by a comma
deprecated-modules=regsub,string,TERMIOS,Bastion,rexec
# Create a graph of every (i.e. internal and external) dependencies in the
# given file (report RP0402 must not be disabled)
import-graph=
# Create a graph of external dependencies in the given file (report RP0402 must
# not be disabled)
ext-import-graph=
# Create a graph of internal dependencies in the given file (report RP0402 must
# not be disabled)
int-import-graph=
[EXCEPTIONS]
# Exceptions that will emit a warning when being caught. Defaults to
# "Exception"
overgeneral-exceptions=Exception

6
queue.yaml Normal file
View File

@ -0,0 +1,6 @@
queue:
- name: deltacalculation
rate: 5/s
retry_parameters:
task_retry_limit: 5
task_age_limit: 1d

80
settings.py Normal file
View File

@ -0,0 +1,80 @@
# 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.
"""Minimal Django settings."""
import os
from google.appengine.api import app_identity
# Banner for e.g. planned downtime announcements
## SPECIAL_BANNER = """\
## Rietveld will be down for maintenance on
## Thursday November 17
## from
## <a href="http://www.timeanddate.com/worldclock/fixedtime.html?iso=20111117T17&ah=6">
## 17:00 - 23:00 UTC
## </a>
## """
APPEND_SLASH = False
DEBUG = os.environ['SERVER_SOFTWARE'].startswith('Dev')
INSTALLED_APPS = (
'codereview',
)
HSTS_MAX_AGE = 60*60*24*365 # 1 year in seconds.
MIDDLEWARE_CLASSES = (
'django.middleware.common.CommonMiddleware',
'django.middleware.http.ConditionalGetMiddleware',
'codereview.middleware.RedirectToHTTPSMiddleware',
'codereview.middleware.AddHSTSHeaderMiddleware',
'codereview.middleware.AddUserToRequestMiddleware',
'codereview.middleware.PropagateExceptionMiddleware',
)
ROOT_URLCONF = 'urls'
TEMPLATE_CONTEXT_PROCESSORS = (
'django.core.context_processors.request',
)
TEMPLATE_DEBUG = DEBUG
TEMPLATE_DIRS = (
os.path.join(os.path.dirname(__file__), 'templates'),
)
TEMPLATE_LOADERS = (
'django.template.loaders.filesystem.load_template_source',
)
FILE_UPLOAD_HANDLERS = (
'django.core.files.uploadhandler.MemoryFileUploadHandler',
)
FILE_UPLOAD_MAX_MEMORY_SIZE = 1048576 # 1 MB
MEDIA_URL = '/static/'
appid = app_identity.get_application_id()
RIETVELD_INCOMING_MAIL_ADDRESS = ('reply@%s.appspotmail.com' % appid)
RIETVELD_INCOMING_MAIL_MAX_SIZE = 500 * 1024 # 500K
RIETVELD_REVISION = '<unknown>'
try:
RIETVELD_REVISION = open(
os.path.join(os.path.dirname(__file__), 'REVISION')
).read()
except:
pass
UPLOAD_PY_SOURCE = os.path.join(os.path.dirname(__file__), 'upload.py')
# Default values for patch rendering
DEFAULT_CONTEXT = 10
DEFAULT_COLUMN_WIDTH = 80
MIN_COLUMN_WIDTH = 3
MAX_COLUMN_WIDTH = 2000

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,4 @@
jQuery Autocomplete plugin
http://bassistance.de/jquery-plugins/jquery-plugin-autocomplete/
Version: 1.0.2
License: MIT/GPL

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,48 @@
.ac_results {
padding: 0px;
border: 1px solid black;
background-color: white;
overflow: hidden;
z-index: 99999;
}
.ac_results ul {
width: 100%;
list-style-position: outside;
list-style: none;
padding: 0;
margin: 0;
}
.ac_results li {
margin: 0px;
padding: 2px 5px;
cursor: default;
display: block;
/*
if width will be 100% horizontal scrollbar will apear
when scroll mode will be used
*/
/*width: 100%;*/
font: menu;
font-size: 12px;
/*
it is very important, if line-height not setted or setted
in relative units scroll will be broken in firefox
*/
line-height: 16px;
overflow: hidden;
}
.ac_loading {
background: white url('indicator.gif') right center no-repeat;
}
.ac_odd {
background-color: #FFFFFF;
}
.ac_over {
background-color: #4797F2;
color: white;
}

View File

@ -0,0 +1,759 @@
/*
* Autocomplete - jQuery plugin 1.0.2
*
* Copyright (c) 2007 Dylan Verheul, Dan G. Switzer, Anjesh Tuladhar, Jörn Zaefferer
*
* Dual licensed under the MIT and GPL licenses:
* http://www.opensource.org/licenses/mit-license.php
* http://www.gnu.org/licenses/gpl.html
*
* Revision: $Id: jquery.autocomplete.js 5747 2008-06-25 18:30:55Z joern.zaefferer $
*
*/
;(function($) {
$.fn.extend({
autocomplete: function(urlOrData, options) {
var isUrl = typeof urlOrData == "string";
options = $.extend({}, $.Autocompleter.defaults, {
url: isUrl ? urlOrData : null,
data: isUrl ? null : urlOrData,
delay: isUrl ? $.Autocompleter.defaults.delay : 10,
max: options && !options.scroll ? 10 : 150
}, options);
// if highlight is set to false, replace it with a do-nothing function
options.highlight = options.highlight || function(value) { return value; };
// if the formatMatch option is not specified, then use formatItem for backwards compatibility
options.formatMatch = options.formatMatch || options.formatItem;
return this.each(function() {
new $.Autocompleter(this, options);
});
},
result: function(handler) {
return this.bind("result", handler);
},
search: function(handler) {
return this.trigger("search", [handler]);
},
flushCache: function() {
return this.trigger("flushCache");
},
setOptions: function(options){
return this.trigger("setOptions", [options]);
},
unautocomplete: function() {
return this.trigger("unautocomplete");
}
});
$.Autocompleter = function(input, options) {
var KEY = {
UP: 38,
DOWN: 40,
DEL: 46,
TAB: 9,
RETURN: 13,
ESC: 27,
COMMA: 188,
PAGEUP: 33,
PAGEDOWN: 34,
BACKSPACE: 8
};
// Create $ object for input element
var $input = $(input).attr("autocomplete", "off").addClass(options.inputClass);
var timeout;
var previousValue = "";
var cache = $.Autocompleter.Cache(options);
var hasFocus = 0;
var lastKeyPressCode;
var config = {
mouseDownOnSelect: false
};
var select = $.Autocompleter.Select(options, input, selectCurrent, config);
var blockSubmit;
// prevent form submit in opera when selecting with return key
$.browser.opera && $(input.form).bind("submit.autocomplete", function() {
if (blockSubmit) {
blockSubmit = false;
return false;
}
});
// only opera doesn't trigger keydown multiple times while pressed, others don't work with keypress at all
$input.bind(($.browser.opera ? "keypress" : "keydown") + ".autocomplete", function(event) {
// track last key pressed
lastKeyPressCode = event.keyCode;
switch(event.keyCode) {
case KEY.UP:
event.preventDefault();
if ( select.visible() ) {
select.prev();
} else {
onChange(0, true);
}
break;
case KEY.DOWN:
event.preventDefault();
if ( select.visible() ) {
select.next();
} else {
onChange(0, true);
}
break;
case KEY.PAGEUP:
event.preventDefault();
if ( select.visible() ) {
select.pageUp();
} else {
onChange(0, true);
}
break;
case KEY.PAGEDOWN:
event.preventDefault();
if ( select.visible() ) {
select.pageDown();
} else {
onChange(0, true);
}
break;
// matches also semicolon
case options.multiple && $.trim(options.multipleSeparator) == "," && KEY.COMMA:
case KEY.TAB:
case KEY.RETURN:
if( selectCurrent() ) {
// stop default to prevent a form submit, Opera needs special handling
event.preventDefault();
blockSubmit = true;
return false;
}
break;
case KEY.ESC:
select.hide();
break;
default:
clearTimeout(timeout);
timeout = setTimeout(onChange, options.delay);
break;
}
}).focus(function(){
// track whether the field has focus, we shouldn't process any
// results if the field no longer has focus
hasFocus++;
}).blur(function() {
hasFocus = 0;
if (!config.mouseDownOnSelect) {
hideResults();
}
}).click(function() {
// show select when clicking in a focused field
if ( hasFocus++ > 1 && !select.visible() ) {
onChange(0, true);
}
}).bind("search", function() {
// TODO why not just specifying both arguments?
var fn = (arguments.length > 1) ? arguments[1] : null;
function findValueCallback(q, data) {
var result;
if( data && data.length ) {
for (var i=0; i < data.length; i++) {
if( data[i].result.toLowerCase() == q.toLowerCase() ) {
result = data[i];
break;
}
}
}
if( typeof fn == "function" ) fn(result);
else $input.trigger("result", result && [result.data, result.value]);
}
$.each(trimWords($input.val()), function(i, value) {
request(value, findValueCallback, findValueCallback);
});
}).bind("flushCache", function() {
cache.flush();
}).bind("setOptions", function() {
$.extend(options, arguments[1]);
// if we've updated the data, repopulate
if ( "data" in arguments[1] )
cache.populate();
}).bind("unautocomplete", function() {
select.unbind();
$input.unbind();
$(input.form).unbind(".autocomplete");
});
function selectCurrent() {
var selected = select.selected();
if( !selected )
return false;
var v = selected.result;
previousValue = v;
if ( options.multiple ) {
var words = trimWords($input.val());
if ( words.length > 1 ) {
v = words.slice(0, words.length - 1).join( options.multipleSeparator ) + options.multipleSeparator + v;
}
v += options.multipleSeparator;
}
$input.val(v);
hideResultsNow();
$input.trigger("result", [selected.data, selected.value]);
return true;
}
function onChange(crap, skipPrevCheck) {
if( lastKeyPressCode == KEY.DEL ) {
select.hide();
return;
}
var currentValue = $input.val();
if ( !skipPrevCheck && currentValue == previousValue )
return;
previousValue = currentValue;
currentValue = lastWord(currentValue);
if ( currentValue.length >= options.minChars) {
$input.addClass(options.loadingClass);
if (!options.matchCase)
currentValue = currentValue.toLowerCase();
request(currentValue, receiveData, hideResultsNow);
} else {
stopLoading();
select.hide();
}
};
function trimWords(value) {
if ( !value ) {
return [""];
}
var words = value.split( options.multipleSeparator );
var result = [];
$.each(words, function(i, value) {
if ( $.trim(value) )
result[i] = $.trim(value);
});
return result;
}
function lastWord(value) {
if ( !options.multiple )
return value;
var words = trimWords(value);
return words[words.length - 1];
}
// fills in the input box w/the first match (assumed to be the best match)
// q: the term entered
// sValue: the first matching result
function autoFill(q, sValue){
// autofill in the complete box w/the first match as long as the user hasn't entered in more data
// if the last user key pressed was backspace, don't autofill
if( options.autoFill && (lastWord($input.val()).toLowerCase() == q.toLowerCase()) && lastKeyPressCode != KEY.BACKSPACE ) {
// fill in the value (keep the case the user has typed)
$input.val($input.val() + sValue.substring(lastWord(previousValue).length));
// select the portion of the value not typed by the user (so the next character will erase)
$.Autocompleter.Selection(input, previousValue.length, previousValue.length + sValue.length);
}
};
function hideResults() {
clearTimeout(timeout);
timeout = setTimeout(hideResultsNow, 200);
};
function hideResultsNow() {
var wasVisible = select.visible();
select.hide();
clearTimeout(timeout);
stopLoading();
if (options.mustMatch) {
// call search and run callback
$input.search(
function (result){
// if no value found, clear the input box
if( !result ) {
if (options.multiple) {
var words = trimWords($input.val()).slice(0, -1);
$input.val( words.join(options.multipleSeparator) + (words.length ? options.multipleSeparator : "") );
}
else
$input.val( "" );
}
}
);
}
if (wasVisible)
// position cursor at end of input field
$.Autocompleter.Selection(input, input.value.length, input.value.length);
};
function receiveData(q, data) {
if ( data && data.length && hasFocus ) {
stopLoading();
select.display(data, q);
autoFill(q, data[0].value);
select.show();
} else {
hideResultsNow();
}
};
function request(term, success, failure) {
if (!options.matchCase)
term = term.toLowerCase();
var data = cache.load(term);
// recieve the cached data
if (data && data.length) {
success(term, data);
// if an AJAX url has been supplied, try loading the data now
} else if( (typeof options.url == "string") && (options.url.length > 0) ){
var extraParams = {
timestamp: +new Date()
};
$.each(options.extraParams, function(key, param) {
extraParams[key] = typeof param == "function" ? param() : param;
});
$.ajax({
// try to leverage ajaxQueue plugin to abort previous requests
mode: "abort",
// limit abortion to this input
port: "autocomplete" + input.name,
dataType: options.dataType,
url: options.url,
data: $.extend({
q: lastWord(term),
limit: options.max
}, extraParams),
success: function(data) {
var parsed = options.parse && options.parse(data) || parse(data);
cache.add(term, parsed);
success(term, parsed);
}
});
} else {
// if we have a failure, we need to empty the list -- this prevents the the [TAB] key from selecting the last successful match
select.emptyList();
failure(term);
}
};
function parse(data) {
var parsed = [];
var rows = data.split("\n");
for (var i=0; i < rows.length; i++) {
var row = $.trim(rows[i]);
if (row) {
row = row.split("|");
parsed[parsed.length] = {
data: row,
value: row[0],
result: options.formatResult && options.formatResult(row, row[0]) || row[0]
};
}
}
return parsed;
};
function stopLoading() {
$input.removeClass(options.loadingClass);
};
};
$.Autocompleter.defaults = {
inputClass: "ac_input",
resultsClass: "ac_results",
loadingClass: "ac_loading",
minChars: 1,
delay: 400,
matchCase: false,
matchSubset: true,
matchContains: false,
cacheLength: 10,
max: 100,
mustMatch: false,
extraParams: {},
selectFirst: true,
formatItem: function(row) { return row[0]; },
formatMatch: null,
autoFill: false,
width: 0,
multiple: false,
multipleSeparator: ", ",
highlight: function(value, term) {
return value.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + term.replace(/([\^\$\(\)\[\]\{\}\*\.\+\?\|\\])/gi, "\\$1") + ")(?![^<>]*>)(?![^&;]+;)", "gi"), "<strong>$1</strong>");
},
scroll: true,
scrollHeight: 180
};
$.Autocompleter.Cache = function(options) {
var data = {};
var length = 0;
function matchSubset(s, sub) {
if (!options.matchCase)
s = s.toLowerCase();
var i = s.indexOf(sub);
if (i == -1) return false;
return i == 0 || options.matchContains;
};
function add(q, value) {
if (length > options.cacheLength){
flush();
}
if (!data[q]){
length++;
}
data[q] = value;
}
function populate(){
if( !options.data ) return false;
// track the matches
var stMatchSets = {},
nullData = 0;
// no url was specified, we need to adjust the cache length to make sure it fits the local data store
if( !options.url ) options.cacheLength = 1;
// track all options for minChars = 0
stMatchSets[""] = [];
// loop through the array and create a lookup structure
for ( var i = 0, ol = options.data.length; i < ol; i++ ) {
var rawValue = options.data[i];
// if rawValue is a string, make an array otherwise just reference the array
rawValue = (typeof rawValue == "string") ? [rawValue] : rawValue;
var value = options.formatMatch(rawValue, i+1, options.data.length);
if ( value === false )
continue;
var firstChar = value.charAt(0).toLowerCase();
// if no lookup array for this character exists, look it up now
if( !stMatchSets[firstChar] )
stMatchSets[firstChar] = [];
// if the match is a string
var row = {
value: value,
data: rawValue,
result: options.formatResult && options.formatResult(rawValue) || value
};
// push the current match into the set list
stMatchSets[firstChar].push(row);
// keep track of minChars zero items
if ( nullData++ < options.max ) {
stMatchSets[""].push(row);
}
};
// add the data items to the cache
$.each(stMatchSets, function(i, value) {
// increase the cache size
options.cacheLength++;
// add to the cache
add(i, value);
});
}
// populate any existing data
setTimeout(populate, 25);
function flush(){
data = {};
length = 0;
}
return {
flush: flush,
add: add,
populate: populate,
load: function(q) {
if (!options.cacheLength || !length)
return null;
/*
* if dealing w/local data and matchContains than we must make sure
* to loop through all the data collections looking for matches
*/
if( !options.url && options.matchContains ){
// track all matches
var csub = [];
// loop through all the data grids for matches
for( var k in data ){
// don't search through the stMatchSets[""] (minChars: 0) cache
// this prevents duplicates
if( k.length > 0 ){
var c = data[k];
$.each(c, function(i, x) {
// if we've got a match, add it to the array
if (matchSubset(x.value, q)) {
csub.push(x);
}
});
}
}
return csub;
} else
// if the exact item exists, use it
if (data[q]){
return data[q];
} else
if (options.matchSubset) {
for (var i = q.length - 1; i >= options.minChars; i--) {
var c = data[q.substr(0, i)];
if (c) {
var csub = [];
$.each(c, function(i, x) {
if (matchSubset(x.value, q)) {
csub[csub.length] = x;
}
});
return csub;
}
}
}
return null;
}
};
};
$.Autocompleter.Select = function (options, input, select, config) {
var CLASSES = {
ACTIVE: "ac_over"
};
var listItems,
active = -1,
data,
term = "",
needsInit = true,
element,
list;
// Create results
function init() {
if (!needsInit)
return;
element = $("<div/>")
.hide()
.addClass(options.resultsClass)
.css("position", "absolute")
.appendTo(document.body);
list = $("<ul/>").appendTo(element).mouseover( function(event) {
if(target(event).nodeName && target(event).nodeName.toUpperCase() == 'LI') {
active = $("li", list).removeClass(CLASSES.ACTIVE).index(target(event));
$(target(event)).addClass(CLASSES.ACTIVE);
}
}).click(function(event) {
$(target(event)).addClass(CLASSES.ACTIVE);
select();
// TODO provide option to avoid setting focus again after selection? useful for cleanup-on-focus
input.focus();
return false;
}).mousedown(function() {
config.mouseDownOnSelect = true;
}).mouseup(function() {
config.mouseDownOnSelect = false;
});
if( options.width > 0 )
element.css("width", options.width);
needsInit = false;
}
function target(event) {
var element = event.target;
while(element && element.tagName != "LI")
element = element.parentNode;
// more fun with IE, sometimes event.target is empty, just ignore it then
if(!element)
return [];
return element;
}
function moveSelect(step) {
listItems.slice(active, active + 1).removeClass(CLASSES.ACTIVE);
movePosition(step);
var activeItem = listItems.slice(active, active + 1).addClass(CLASSES.ACTIVE);
if(options.scroll) {
var offset = 0;
listItems.slice(0, active).each(function() {
offset += this.offsetHeight;
});
if((offset + activeItem[0].offsetHeight - list.scrollTop()) > list[0].clientHeight) {
list.scrollTop(offset + activeItem[0].offsetHeight - list.innerHeight());
} else if(offset < list.scrollTop()) {
list.scrollTop(offset);
}
}
};
function movePosition(step) {
active += step;
if (active < 0) {
active = listItems.size() - 1;
} else if (active >= listItems.size()) {
active = 0;
}
}
function limitNumberOfItems(available) {
return options.max && options.max < available
? options.max
: available;
}
function fillList() {
list.empty();
var max = limitNumberOfItems(data.length);
for (var i=0; i < max; i++) {
if (!data[i])
continue;
var formatted = options.formatItem(data[i].data, i+1, max, data[i].value, term);
if ( formatted === false )
continue;
var li = $("<li/>").html( options.highlight(formatted, term) ).addClass(i%2 == 0 ? "ac_even" : "ac_odd").appendTo(list)[0];
$.data(li, "ac_data", data[i]);
}
listItems = list.find("li");
if ( options.selectFirst ) {
listItems.slice(0, 1).addClass(CLASSES.ACTIVE);
active = 0;
}
// apply bgiframe if available
if ( $.fn.bgiframe )
list.bgiframe();
}
return {
display: function(d, q) {
init();
data = d;
term = q;
fillList();
},
next: function() {
moveSelect(1);
},
prev: function() {
moveSelect(-1);
},
pageUp: function() {
if (active != 0 && active - 8 < 0) {
moveSelect( -active );
} else {
moveSelect(-8);
}
},
pageDown: function() {
if (active != listItems.size() - 1 && active + 8 > listItems.size()) {
moveSelect( listItems.size() - 1 - active );
} else {
moveSelect(8);
}
},
hide: function() {
element && element.hide();
listItems && listItems.removeClass(CLASSES.ACTIVE);
active = -1;
},
visible : function() {
return element && element.is(":visible");
},
current: function() {
return this.visible() && (listItems.filter("." + CLASSES.ACTIVE)[0] || options.selectFirst && listItems[0]);
},
show: function() {
var offset = $(input).offset();
element.css({
width: typeof options.width == "string" || options.width > 0 ? options.width : $(input).width(),
top: offset.top + input.offsetHeight,
left: offset.left
}).show();
if(options.scroll) {
list.scrollTop(0);
list.css({
maxHeight: options.scrollHeight,
overflow: 'auto'
});
if($.browser.msie && typeof document.body.style.maxHeight === "undefined") {
var listHeight = 0;
listItems.each(function() {
listHeight += this.offsetHeight;
});
var scrollbarsVisible = listHeight > options.scrollHeight;
list.css('height', scrollbarsVisible ? options.scrollHeight : listHeight );
if (!scrollbarsVisible) {
// IE doesn't recalculate width when scrollbar disappears
listItems.width( list.width() - parseInt(listItems.css("padding-left")) - parseInt(listItems.css("padding-right")) );
}
}
}
},
selected: function() {
var selected = listItems && listItems.filter("." + CLASSES.ACTIVE).removeClass(CLASSES.ACTIVE);
return selected && selected.length && $.data(selected[0], "ac_data");
},
emptyList: function (){
list && list.empty();
},
unbind: function() {
element && element.remove();
}
};
};
$.Autocompleter.Selection = function(field, start, end) {
if( field.createTextRange ){
var selRange = field.createTextRange();
selRange.collapse(true);
selRange.moveStart("character", start);
selRange.moveEnd("character", end);
selRange.select();
} else if( field.setSelectionRange ){
field.setSelectionRange(start, end);
} else {
if( field.selectionStart ){
field.selectionStart = start;
field.selectionEnd = end;
}
}
field.focus();
};
})(jQuery);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,116 @@
/**
* Ajax Queue Plugin
*
* Homepage: http://jquery.com/plugins/project/ajaxqueue
* Documentation: http://docs.jquery.com/AjaxQueue
*/
/**
<script>
$(function(){
jQuery.ajaxQueue({
url: "test.php",
success: function(html){ jQuery("ul").append(html); }
});
jQuery.ajaxQueue({
url: "test.php",
success: function(html){ jQuery("ul").append(html); }
});
jQuery.ajaxSync({
url: "test.php",
success: function(html){ jQuery("ul").append("<b>"+html+"</b>"); }
});
jQuery.ajaxSync({
url: "test.php",
success: function(html){ jQuery("ul").append("<b>"+html+"</b>"); }
});
});
</script>
<ul style="position: absolute; top: 5px; right: 5px;"></ul>
*/
/*
* Queued Ajax requests.
* A new Ajax request won't be started until the previous queued
* request has finished.
*/
/*
* Synced Ajax requests.
* The Ajax request will happen as soon as you call this method, but
* the callbacks (success/error/complete) won't fire until all previous
* synced requests have been completed.
*/
(function($) {
var ajax = $.ajax;
var pendingRequests = {};
var synced = [];
var syncedData = [];
$.ajax = function(settings) {
// create settings for compatibility with ajaxSetup
settings = jQuery.extend(settings, jQuery.extend({}, jQuery.ajaxSettings, settings));
var port = settings.port;
switch(settings.mode) {
case "abort":
if ( pendingRequests[port] ) {
pendingRequests[port].abort();
}
return pendingRequests[port] = ajax.apply(this, arguments);
case "queue":
var _old = settings.complete;
settings.complete = function(){
if ( _old )
_old.apply( this, arguments );
jQuery([ajax]).dequeue("ajax" + port );;
};
jQuery([ ajax ]).queue("ajax" + port, function(){
ajax( settings );
});
return;
case "sync":
var pos = synced.length;
synced[ pos ] = {
error: settings.error,
success: settings.success,
complete: settings.complete,
done: false
};
syncedData[ pos ] = {
error: [],
success: [],
complete: []
};
settings.error = function(){ syncedData[ pos ].error = arguments; };
settings.success = function(){ syncedData[ pos ].success = arguments; };
settings.complete = function(){
syncedData[ pos ].complete = arguments;
synced[ pos ].done = true;
if ( pos == 0 || !synced[ pos-1 ] )
for ( var i = pos; i < synced.length && synced[i].done; i++ ) {
if ( synced[i].error ) synced[i].error.apply( jQuery, syncedData[i].error );
if ( synced[i].success ) synced[i].success.apply( jQuery, syncedData[i].success );
if ( synced[i].complete ) synced[i].complete.apply( jQuery, syncedData[i].complete );
synced[i] = null;
syncedData[i] = null;
}
};
}
return ajax.apply(this, arguments);
};
})(jQuery);

View File

@ -0,0 +1,10 @@
/* Copyright (c) 2006 Brandon Aaron (http://brandonaaron.net)
* Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php)
* and GPL (http://www.opensource.org/licenses/gpl-license.php) licenses.
*
* $LastChangedDate: 2007-07-22 01:45:56 +0200 (Son, 22 Jul 2007) $
* $Rev: 2447 $
*
* Version 2.1.1
*/
(function($){$.fn.bgIframe=$.fn.bgiframe=function(s){if($.browser.msie&&/6.0/.test(navigator.userAgent)){s=$.extend({top:'auto',left:'auto',width:'auto',height:'auto',opacity:true,src:'javascript:false;'},s||{});var prop=function(n){return n&&n.constructor==Number?n+'px':n;},html='<iframe class="bgiframe"frameborder="0"tabindex="-1"src="'+s.src+'"'+'style="display:block;position:absolute;z-index:-1;'+(s.opacity!==false?'filter:Alpha(Opacity=\'0\');':'')+'top:'+(s.top=='auto'?'expression(((parseInt(this.parentNode.currentStyle.borderTopWidth)||0)*-1)+\'px\')':prop(s.top))+';'+'left:'+(s.left=='auto'?'expression(((parseInt(this.parentNode.currentStyle.borderLeftWidth)||0)*-1)+\'px\')':prop(s.left))+';'+'width:'+(s.width=='auto'?'expression(this.parentNode.offsetWidth+\'px\')':prop(s.width))+';'+'height:'+(s.height=='auto'?'expression(this.parentNode.offsetHeight+\'px\')':prop(s.height))+';'+'"/>';return this.each(function(){if($('> iframe.bgiframe',this).length==0)this.insertBefore(document.createElement(html),this.firstChild);});}return this;};})(jQuery);

3558
static/autocomplete/lib/jquery.js vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,163 @@
/* ----------------------------------------------------------------------------------------------------------------*/
/* ---------->>> global settings needed for thickbox <<<-----------------------------------------------------------*/
/* ----------------------------------------------------------------------------------------------------------------*/
*{padding: 0; margin: 0;}
/* ----------------------------------------------------------------------------------------------------------------*/
/* ---------->>> thickbox specific link and font settings <<<------------------------------------------------------*/
/* ----------------------------------------------------------------------------------------------------------------*/
#TB_window {
font: 12px Arial, Helvetica, sans-serif;
color: #333333;
}
#TB_secondLine {
font: 10px Arial, Helvetica, sans-serif;
color:#666666;
}
#TB_window a:link {color: #666666;}
#TB_window a:visited {color: #666666;}
#TB_window a:hover {color: #000;}
#TB_window a:active {color: #666666;}
#TB_window a:focus{color: #666666;}
/* ----------------------------------------------------------------------------------------------------------------*/
/* ---------->>> thickbox settings <<<-----------------------------------------------------------------------------*/
/* ----------------------------------------------------------------------------------------------------------------*/
#TB_overlay {
position: fixed;
z-index:100;
top: 0px;
left: 0px;
height:100%;
width:100%;
}
.TB_overlayMacFFBGHack {background: url(macFFBgHack.png) repeat;}
.TB_overlayBG {
background-color:#000;
filter:alpha(opacity=75);
-moz-opacity: 0.75;
opacity: 0.75;
}
* html #TB_overlay { /* ie6 hack */
position: absolute;
height: expression(document.body.scrollHeight > document.body.offsetHeight ? document.body.scrollHeight : document.body.offsetHeight + 'px');
}
#TB_window {
position: fixed;
background: #ffffff;
z-index: 102;
color:#000000;
display:none;
border: 4px solid #525252;
text-align:left;
top:50%;
left:50%;
}
* html #TB_window { /* ie6 hack */
position: absolute;
margin-top: expression(0 - parseInt(this.offsetHeight / 2) + (TBWindowMargin = document.documentElement && document.documentElement.scrollTop || document.body.scrollTop) + 'px');
}
#TB_window img#TB_Image {
display:block;
margin: 15px 0 0 15px;
border-right: 1px solid #ccc;
border-bottom: 1px solid #ccc;
border-top: 1px solid #666;
border-left: 1px solid #666;
}
#TB_caption{
height:25px;
padding:7px 30px 10px 25px;
float:left;
}
#TB_closeWindow{
height:25px;
padding:11px 25px 10px 0;
float:right;
}
#TB_closeAjaxWindow{
padding:7px 10px 5px 0;
margin-bottom:1px;
text-align:right;
float:right;
}
#TB_ajaxWindowTitle{
float:left;
padding:7px 0 5px 10px;
margin-bottom:1px;
}
#TB_title{
background-color:#e8e8e8;
height:27px;
}
#TB_ajaxContent{
clear:both;
padding:2px 15px 15px 15px;
overflow:auto;
text-align:left;
line-height:1.4em;
}
#TB_ajaxContent.TB_modal{
padding:15px;
}
#TB_ajaxContent p{
padding:5px 0px 5px 0px;
}
#TB_load{
position: fixed;
display:none;
height:13px;
width:208px;
z-index:103;
top: 50%;
left: 50%;
margin: -6px 0 0 -104px; /* -height/2 0 0 -width/2 */
}
* html #TB_load { /* ie6 hack */
position: absolute;
margin-top: expression(0 - parseInt(this.offsetHeight / 2) + (TBWindowMargin = document.documentElement && document.documentElement.scrollTop || document.body.scrollTop) + 'px');
}
#TB_HideSelect{
z-index:99;
position:fixed;
top: 0;
left: 0;
background-color:#fff;
border:none;
filter:alpha(opacity=0);
-moz-opacity: 0;
opacity: 0;
height:100%;
width:100%;
}
* html #TB_HideSelect { /* ie6 hack */
position: absolute;
height: expression(document.body.scrollHeight > document.body.offsetHeight ? document.body.scrollHeight : document.body.offsetHeight + 'px');
}
#TB_iframeContent{
clear:both;
border:none;
margin-bottom:-1px;
margin-top:1px;
_margin-bottom:1px;
}

BIN
static/blank.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 631 B

BIN
static/close.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 B

BIN
static/closedtriangle.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 B

BIN
static/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 B

BIN
static/opentriangle.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 B

17
static/robots.txt Normal file
View File

@ -0,0 +1,17 @@
# Directions for web crawlers.
# See http://www.robotstxt.org/wc/norobots.html.
User-agent: HTTrack
User-agent: puf
User-agent: MSIECrawler
User-agent: Nutch
Disallow: /
User-agent: *
Disallow: /*/diff/
Disallow: /*/diff2/
Disallow: /*/patch/
Disallow: /*/publish
Disallow: /download/
Disallow: /user/
Disallow: /rss/

BIN
static/rss.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

3315
static/script.js Normal file

File diff suppressed because it is too large Load Diff

BIN
static/star-dark.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 B

BIN
static/star-lite.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 B

886
static/styles.css Normal file
View File

@ -0,0 +1,886 @@
/*
* 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.
*/
body {
font-family: Arial, Helvetica, sans-serif;
font-size: 83%;
background-color: #fff;
}
.home {
font-size: 200%;
font-weight: bold;
color: #000;
/* text-decoration: none; */
}
tt, code, pre {
font-size: 12px;
}
/* Looks like a no-op, but inputs don't inherit font-size otherwise. */
input {
font-size: 100%;
}
form {
margin: 0px;
}
ul {
margin-top: 0px;
margin-bottom: 0px;
}
.gaia {
text-align: right;
padding: 2px;
white-space: nowrap;
}
.search {
text-align: left;
padding: 2px;
white-space: nowrap;
}
.bluebg {
background-color: #ccf;
}
a {
color: #4182fa;
}
a.novisit {
color: #2a55a3;
}
a.redlink {
color: red;
font-weight: bold;
}
.anchor {
visibility: hidden;
}
div.header:hover a.anchor,
h3:hover .anchor {
visibility: visible;
}
h3:hover .anchor {
color: #2a55a3;
text-decoration: underline;
}
a.noul, a.noulv {
color: #4182fa; /* #93b7fa; */
text-decoration: none;
}
a:hover.noul, a:hover.noulv {
text-decoration: underline;
}
a:visited.noul, a:visited.noulv {
color: #a32a91; /* #2a55a3; */
}
.filegone, .filegone a, .filegone a:visited {
color: gray;
}
/* Old/new styles for replace, delete, insert, equal, blank (no text) */
.olddark, .newdark, .oldreplace, .olddelete, .oldinsert, .oldequal, .oldblank,
.oldlight, .newlight, .oldreplace1, .newreplace1,
.newreplace, .newdelete, .newinsert, .newequal, .newblank,
.oldmove, .oldchangemove, .oldchangemove1, .oldmove_out, .oldchangemove_out,
.newmove, .newchangemove, .newchangemove1,
.udiffadd, .udiffremove, .udiff, .debug-info {
white-space: pre;
font-family: monospace;
font-size: 12px;
vertical-align: top;
}
.oldlight {
background-color: #fee;
font-size: 100%;
}
.newlight {
background-color: #dfd;
font-size: 100%;
}
.olddark {
background-color: #faa;
font-size: 100%;
}
.newdark {
background-color: #9f9;
font-size: 100%;
}
.oldblank, .newblank {
background-color: #eee;
}
.oldreplace1 {
background-color: #faa;
}
.newreplace1 {
background-color: #9f9;
}
.newchangemove1 {
background-color: #9f9;
}
.oldreplace {
background-color: #fee;
}
.olddelete {
background-color: #faa;
}
.newreplace {
background-color: #dfd;
}
.newchangemove {
background-color: #dfd;
}
.newinsert {
background-color: #9f9;
}
.oldinsert, newdelete {
background-color: #ddd;
}
.oldequal, .newequal, .newmove {
background-color: #fff;
}
.oldmove, .oldchangemove, .oldchangemove1, .moved_header, .moved_lno {
background-color: #ff9;
}
.oldmove_out, .oldchangemove_out, .moved_out_header {
background-color: #fc8;
}
.movelight {
background-color: #ff9;
font-size: 100%;
}
.oldmovedark {
background-color: #faa;
font-size: 100%;
}
.newmovedark {
background-color: #9f9;
font-size: 100%;
}
/* Message above a side-by-side view */
.info {
width: 250px;
text-align: center;
padding-bottom: 0.6em;
color: #777;
}
/* Header sections for moved regions */
.moved_header {
white-space: nowrap;
font-size: 80%;
color: #777;
}
.moved_out_header {
white-space: nowrap;
font-size: 80%;
cursor: pointer;
color: #777;
}
.moved_lno {
color: #777;
}
.udiffadd {
color: blue;
}
.udiffremove {
color: red;
}
/* CL summary formatting styles */
.users {
white-space: nowrap;
overflow: hidden;
}
.subject {
white-space: nowrap;
overflow: hidden;
}
.extra {
color: #777;
}
.extra-note {
color: #777;
font-style:italic;
}
.date {
white-space: nowrap;
}
span.dump {
margin-left: 4px;
padding-left: 1em;
border-left: 1px solid silver;
}
/* Subheaders have standard Google light blue bg. */
h2, h3 {
margin-top: 0.5em;
margin-bottom: 0.5em;
}
/* Make h1, h2 headers lighter */
h1, h2 {
font-weight: normal;
}
/* Make h1 header smaller */
h1 {
margin-top: 0px;
font-size: 180%;
}
/* Comment title, make it seem clickable, single line, hidden overflow */
div.comment_title {
overflow: hidden;
white-space: nowrap;
border-top: thin solid;
text-overflow: ellipsis; /* An IE-only feature */
cursor: pointer;
}
div.comment-draft-title {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis; /* An IE-only feature */
cursor: pointer;
}
/* Equivalent of pre */
div.comment {
font-family: monospace;
font-size: 12px;
}
div.comment-text {
white-space: pre;
}
div.comment-text-quoted {
white-space: pre;
color: #777;
}
a.comment-hide-link {
font-size: small;
text-decoration: none;
}
tr.inline-comments {
background-color: #e5ecf9;
}
div.linter {
background-color: #f9f9c0;
}
div.bugbot {
background-color: #f9f9c0;
}
div.comment-border {
border-top: thin solid;
border-left: thin solid;
border-right: thin solid;
padding: 2px;
}
div.inline-comment-title {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis; /* An IE-only feature */
cursor: pointer;
}
/* Equivalent of pre */
div.inline-comment {
font-family: monospace;
font-size: 12px;
}
#hook-sel {
position: absolute;
background: #06c;
width: 1px;
height: 2px;
font-size: 0; /* Work-around for an IE bug */
}
span.moved_body {
white-space: pre;
font-family: monospace;
font-size: 12px;
vertical-align: top;
background-color: #fa6;
}
/* CL# color-coding */
.cl-unreviewed {
background: #ff7; /* Yellow */
}
.cl-overdue {
background: #faa; /* Red */
}
.cl-approved {
background: #bfb; /* Green */
}
.cl-aged {
background: #ddd; /* Gray */
}
/* Debugging information */
.debug-info {
background: lightgrey;
}
.toggled-section {
padding-left: 15px;
background-image: url('closedtriangle.gif');
background-position: center left;
background-repeat: no-repeat;
background-align: left;
text-decoration: none;
color: black;
}
.opentriangle {
background-image: url('opentriangle.gif');
}
.fat-button {
height: 2em;
}
.error {
color: red;
border: 1px solid red;
margin: 1em;
margin-top: 0px;
padding: 0.5em;
font-size: 110%;
}
div.fileresult {
margin-left: 1em;
}
div.result {
margin-left: 2em;
}
div.errorbox-bad {
border: 2px solid #990000;
padding: 2px;
}
div.errorbox-bad-msg {
text-align: center;
color: red;
font-weight: bold;
}
.vivid-msg, .calm-msg {
margin: 0 2em 0.5em 2em;
}
.vivid-msg {
font-size:125%;
background: #ffeac0;
border: 1px solid #ff9900;
}
.leftdiffbg {
background: #d9d9d9;
}
.rightdiffbg {
background: #ffff63;
}
/* Styles for the history table */
.header-right {
float: right;
padding-top: 0.2em;
margin-right: 2px;
}
table.history {
border-width: 0px;
border-spacing: 0px;
width: 100%;
border-collapse: collapse;
}
table.history td, th {
padding: 3px;
}
table.history td.triangle {
width: 11px;
}
table.history div.triangle-open {
padding-left: 15px;
width: 11px;
height: 11px;
/*background: url('softopentriangle.gif') no-repeat scroll center;*/
}
table.history div.triangle-closed {
padding-left: 15px;
width: 11px;
height: 11px;
/*background: url('softclosedtriangle.gif') no-repeat scroll center;*/
}
/* Make the diff background colors slightly lighter for the table.*/
table.history .leftdiffbg {
background: #e8e8e8;
}
table.history .leftdiffbg-light {
background: #f2f2f2;
}
table.history .rightdiffbg-light {
background: #ffffaa;
}
table.history .sep {
border-top: 1px solid #f2f2f2;
}
table.property_changes {
color: #333333;
background-color: #eeeeec;
border: 1px solid lightgray;
border-radius: 5px;
-moz-border-radius: 5px;
-webkit-border-radius: 5px;
padding: 5px;
margin-bottom: 1em;
}
table#queues, table#clfiles {
border-collapse: collapse;
width: 100%;
}
table#queues tr, table#clfiles tr {
border-bottom: thin solid lightgray;
}
table#queues td, table#clfiles td {
padding: 2px;
}
div#help {
position: fixed;
right: 4%;
left: 4%;
top: 5%;
opacity: 0.85;
-moz-opacity: 0.85;
-khtml-opacity: 0.85;
filter: alpha(opacity=85);
border-radius: 10px;
-webkit-border-radius: 10px;
-moz-border-radius: 10px;
background: black;
color: white;
font-weight: bold;
padding: 1em;
z-index: 1;
overflow-x: hidden;
overflow-y: auto;
}
div#help th {
color: yellow;
text-align: left;
}
div#help td {
font-weight: normal;
}
td.shortcut {
text-align: right;
}
span.letter {
color: #88dd00;
font-weight: bold;
font-family: monospace;
font-size: medium;
}
/* Visual tab indication. */
span.visualtab {
color: red;
font-weight: bold;
}
/* Disabled text. */
.disabled {
color: gray;
}
/* Generation counter. */
.counter {
float: right;
font-size: 80%;
color: gray;
}
/* Issue description. */
.description {
background-color: #e5ecf9;
margin-top: 0;
padding: 3px;
overflow: auto;
border-radius: 9px;
-webkit-border-radius: 9px;
-moz-border-radius: 9px;
}
/* Main menu */
.mainmenu {
margin-top: 1em;
}
.mainmenu a {
margin-right: .2em;
background-color: #f3f3f3;
border-bottom: none;
border-radius: 5px 5px 0px 0px;
-webkit-border-radius: 5px 5px 0px 0px;
-moz-border-radius: 5px 5px 0px 0px;
padding-left: .8em;
padding-right: .8em;
padding-top: 3px;
text-decoration: none;
color: #666666;
font-weight: bold;
}
.mainmenu a.active {
background-color: #e9f2df;
border-bottom: none;
color: black;
}
.mainmenu2 {
background-color: #e9f2df;
padding: 5px;
padding-left: .8em;
margin-bottom: 1.3em;
border-radius: 0px 5px 5px 5px;
-webkit-border-radius: 0px 5px 5px 5px;
-moz-border-radius: 0px 5px 5px 5px;
}
.mainmenu2 a {
color: black;
}
.mainmenu2 a.active {
text-decoration: none;
color: black;
font-weight: bold;
}
/* Issue lists */
.issue-list {
background-color: #E5ECF9;
border: 1px solid #93b7fa;
border-bottom: 2px solid #93b7fa;
padding: 3px;
border-radius: 5px 5px 0px 0px;
-webkit-border-radius: 5px 5px 0px 0px;
-moz-border-radius: 5px 5px 0px 0px;
}
.issue-list .header {
background-color: #E5ECF9;
}
.issue-list .header h3 {
font-size: 1.1em;
margin: 0px;
padding: 3px;
padding-left: 0px;
}
.issue-list .pagination {
text-align: right;
padding: 3px;
}
.issue-list table{
background-color: white;
}
.issue-list table th {
background-color: #eeeeec;
border-right: 1px solid lightgray;
border-top: 1px solid lightgray;
}
.issue-list table td.last {
border-right: 1px solid lightgray;
}
.issue-list table .first {
border-left: 1px solid lightgray;
}
.issue-list form {
display: inline;
}
.issue-list input.link-to {
color: #4182fa;
border: 0px;
margin: 0px;
padding: 0px;
width: auto;
overflow: visible;
font-family: Arial, Helvetica, sans-serif;
background-color: transparent;
text-decoration: underline;
cursor: pointer;
display: inline-block;
}
.issue_details_sidebar div {
margin-top: .8em;
}
/* Issue details */
.issue-header .h2 {
font-size: 1.2em;
font-weight: bold;
}
.issue-details .meta {
border-right: 2px solid #b7d993;
padding: 5px;
}
/* Messages */
.message .header {
border: 1px solid lightgray;
border-bottom: none;
border-radius: 8px 8px 0px 0px;
-webkit-border-radius: 8px 8px 0px 0px;
-moz-border-radius: 8px 8px 0px 0px;
cursor: pointer;
}
.message-body {
border-left: 1px solid lightgray;
border-right: 1px solid lightgray;
}
.message-body span, .message-body pre {
margin: 0px;
margin-left: 5px;
margin-right: 5px;
}
.message-actions {
background-color: #dddddd;
font-size: .8em;
padding: 3px;
padding-left: 5px;
border-radius: 0px 0px 8px 8px;
-webkit-border-radius: 0px 0px 8px 8px;
-moz-border-radius: 0px 0px 8px 8px;
margin-bottom: .8em;
}
/* Approval messages have light green header background and dark green border */
.message.approval .header {
background-color: #7FFF7F;
border-color: green;
}
.message.approval .message-body,
.message.approval .message-actions {
border-color: green;
}
/* Disapproval messages have light red header background and red border */
.message.disapproval .header {
background-color: #FFAAAA;
border-color: red;
}
.message.disapproval .message-body,
.message.disapproval .message-actions {
border-color: red;
}
.message.referenced .header {
background-color: #FFFF7F;
border-color: yellow;
}
.message.referenced .message-body,
.message.referenced .message-actions {
border-color: yellow;
}
/* Messages sent while issue was closed have a light blue header background
and a blue border */
.message.issue_was_closed .header {
background-color: #AAAAFF;
border-color: blue;
}
.message.issue_was_closed .message-body,
.message.issue_was_closed .message-actions {
border-color: blue;
}
.popup {
visibility: hidden;
background-color: #cdf2a5;
padding: 8px;
border: 1px solid #2e3436;
border-radius: 5px;
-webkit-border-radius: 5px;
-moz-border-radius: 5px;
opacity: 0.95;
filter: alpha(opacity=95);
position: absolute;
}
.code {
background-color: #eeeeec;
padding: 3px;
border: 1px solid lightgray;
margin-top: .8em;
}
#table-top {
background-color: white;
border-radius: 5px;
-webkit-border-radius: 5px;
-moz-border-radius: 5px;
border: 1px solid lightgray;
}
.codenav {
text-align:center;
padding: 3px;
}
.help {
font-size: .9em;
}
ul.errorlist {
list-style-type: none;
padding: 0;
margin: 0;
}
ul.errorlist li {
color: red;
font-weight: bold;
}
#reviewmsgdlg {
background-color: #E9F2DF;
border: 1px solid lightgray;
border-right: 1px solid darkgray;
border-bottom: 1px solid darkgray;
position: fixed;
left: 10px;
top: 10px;
padding: 5px;
opacity: 0.95;
-moz-opacity: 0.95;
-khtml-opacity: 0.95;
filter: alpha(opacity=95);
border-radius: 10px;
-webkit-border-radius: 10px;
-moz-border-radius: 10px;
}
#reviewmsgdlg textarea {
border: none;
width: 350px;
height: 350px;
opacity: 1;
-moz-opacity: 1;
-khtml-opacity: 1;
filter: alpha(opacity=100);
}
#repoview div.header {
margin-top: 1.5em;
}
#repoview div.header span {
font-size: 120%;
font-weight: bold;
}
#repoview table {
margin-top: 0.5em;
}

BIN
static/zippyplus.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 B

1
templates/404.html Normal file
View File

@ -0,0 +1 @@
<h1>404 Not Found</h1>

1
templates/500.html Normal file
View File

@ -0,0 +1 @@
<h1>500 Server Error</h1>

7
templates/all.html Normal file
View File

@ -0,0 +1,7 @@
{%extends "issue_pagination.html"%}
{%block subtitle%}
Recent
{%if closed|lower == 'false' %} Open {%endif%}
{%if closed|lower == 'true' %} Closed {%endif%}
Issues
{%endblock%}

273
templates/base.html Normal file
View File

@ -0,0 +1,273 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>
{%if is_dev%}
(DEV)
{%endif%}
{%block title1%}
{%if patch%}{{patch.filename}} -{%endif%}
{%endblock%}
{%block title2%}
{%if issue%}Issue {{issue.key.id}}: {{issue.subject}} -{%endif%}
{%endblock%}
Code Review
</title>
<link rel="icon" href="{{media_url}}favicon.ico" />
<link type="text/css" rel="stylesheet" href="{{media_url}}styles.css" />
<script type="text/javascript" src="{{media_url}}script.js"></script>
<link rel="alternate" type="application/atom+xml"
title="Recent Issues"
href="{%url django.contrib.syndication.views.feed url="all"%}" />
{%if user%}
<link rel="alternate" type="application/atom+xml"
title="Issues created by me"
href="{%url django.contrib.syndication.views.feed url="mine"%}/{%nickname user True%}" />
<link rel="alternate" type="application/atom+xml"
title="Issues reviewable by me"
href="{%url django.contrib.syndication.views.feed url="reviews"%}/{%nickname user True%}" />
<link rel="alternate" type="application/atom+xml"
title="Issues closed by me"
href="{%url django.contrib.syndication.views.feed url="closed"%}/{%nickname user True%}" />
{%endif%}
{%if issue%}
<link rel="alternate" type="application/atom+xml"
title="Issue {{issue.key.id}}"
href="{%url django.contrib.syndication.views.feed url="issue"%}/{{issue.key.id}}" />
{%endif%}
<!-- head block to insert js/css for forms processing -->
{%block head%}{%endblock%}
<!-- /head -->
</head>
<body onunload="M_unloadPage();">
<!-- Begin help window -->
<script type="text/javascript"><!--
var xsrfToken = '{{xsrf_token}}';
var helpDisplayed = false;
document.onclick = M_clickCommon;
var media_url = "{{media_url}}";
var base_url = "{%url codereview.views.index%}";
{%if issue%}
var publish_link = "{%url codereview.views.publish issue.key.id%}";
{%endif%}
// -->
</script>
<div id="help" style="display: none;">
{%block help%}{%endblock%}
<div style="font-size: medium; text-align: center;">Keyboard Shortcuts</div>
<hr />
<table width="100%">
<tr valign="top">
<td>
<table width="100%">
<tr>
<td></td><th>File</th>
</tr>
<tr>
<td class="shortcut"><span class="letter">u</span> <b>:</b></td><td>up to issue</td>
</tr>
<tr>
<td class="shortcut"><span class="letter">m</span> <b>:</b></td><td>publish + mail comments</td>
</tr>
<tr>
<td class="shortcut"><span class="letter">M</span> <b>:</b></td><td>edit review message</td>
</tr>
<tr>
<td class="shortcut"><span class="letter">j</span> / <span class="letter">k</span> <b>:</b></td><td>jump to file after / before current file</td>
</tr>
<tr>
<td class="shortcut"><span class="letter">J</span> / <span class="letter">K</span> <b>:</b></td><td>jump to next file with a comment after / before current file</td>
</tr>
<tr>
<td></td><th>Side-by-side diff</th>
</tr>
<tr>
<td class="shortcut"><span class="letter">i</span> <b>:</b></td><td>toggle intra-line diffs</td>
</tr>
<tr>
<td class="shortcut"><span class="letter">e</span> <b>:</b></td><td>expand all comments</td>
</tr>
<tr>
<td class="shortcut"><span class="letter">c</span> <b>:</b></td><td>collapse all comments</td>
</tr>
<tr>
<td class="shortcut"><span class="letter">s</span> <b>:</b></td><td>toggle showing all comments</td>
</tr>
<tr>
<td class="shortcut"><span class="letter">n</span> / <span class="letter">p</span> <b>:</b></td><td>next / previous diff chunk or comment</td>
</tr>
<tr>
<td class="shortcut"><span class="letter">N</span> / <span class="letter">P</span> <b>:</b></td><td>next / previous comment</td>
</tr>
<tr>
<td class="shortcut"><span class="letter">&lt;Up&gt;</span> / <span class="letter">&lt;Down&gt;</span> <b>:</b></td><td>next / previous line</td>
</tr>
<tr>
<td class="shortcut"><span class="letter">&lt;Enter&gt;</span> <b>:</b></td><td>respond to / edit current comment</td>
</tr>
<tr>
<td class="shortcut"><span class="letter">d</span> <b>:</b></td><td>mark current comment as done</td>
</tr>
</table>
</td>
<td>
<table width="100%">
<tr>
<td></td><th>Issue</th>
</tr>
<tr>
<td class="shortcut"><span class="letter">u</span> <b>:</b></td><td>up to list of issues</td>
</tr>
<tr>
<td class="shortcut"><span class="letter">m</span> <b>:</b></td><td>publish + mail comments</td>
</tr>
<tr>
<td class="shortcut"><span class="letter">j</span> / <span class="letter">k</span> <b>:</b></td><td>jump to patch after / before current patch</td>
</tr>
<tr>
<td class="shortcut"><span class="letter">o</span> / <span class="letter">&lt;Enter&gt;</span> <b>:</b></td><td>open current patch in side-by-side view</td>
</tr>
<tr>
<td class="shortcut"><span class="letter">i</span> <b>:</b></td><td>open current patch in unified diff view</td>
</tr>
<tr><td>&nbsp;</td></tr>
<tr><td></td><th>Issue List</th></tr>
<tr>
<td class="shortcut"><span class="letter">j</span> / <span class="letter">k</span> <b>:</b></td><td>jump to issue after / before current issue</td>
</tr>
<tr>
<td class="shortcut"><span class="letter">o</span> / <span class="letter">&lt;Enter&gt;</span> <b>:</b></td><td>open current issue</td>
</tr>
<tr>
<td class="shortcut"><span class="letter">#</span> <b>:</b></td>
<td>close issue</td>
</tr>
<tr><td>&nbsp;</td></tr>
<tr>
<td></td><th>Comment/message editing</th>
</tr>
<tr>
<td class="shortcut"><span class="letter">&lt;Ctrl&gt;</span> + <span class="letter">s</span> <b>:</b></td><td>save comment</td>
</tr>
<tr>
<td class="shortcut"><span class="letter">&lt;Esc&gt;</span> <b>:</b></td><td>cancel edit</td>
</tr>
</table>
</td>
</tr>
</table>
</div>
<!-- End help window -->
<div align="right">
{%if is_dev%}
<div style="float:left; color:green; font-weight:bold">DEVELOPMENT SERVER</div>
{%else%}
<div style="float:left; font-weight:bold"><i>Rietveld</i> Code Review Tool</div>
{%endif%}
{%if special_banner%}
<div style="float:left;
border:solid; border-width:1px; border-color: black;
font-weight:bold; font-size:111%; color:red;
background-color:yellow;
margin-left:5px; padding:2px">
{{special_banner|safe}}
</div>
{%endif%}
{%if user%}
<b>{{user.email}} ({%nickname user True%})</b>
|
{%if must_choose_nickname%}
<span style="color:red">Please choose your nickname with</span>{%endif%}
<a class="novisit" href="{%url codereview.views.settings%}">Settings</a>
|
{%endif%}
{%if is_dev%}
<a class="novisit" target="_blank" href="/_ah/admin">Admin</a>
|
{%endif%}
<a class="novisit" target="_blank"
href="http://code.google.com/p/rietveld/wiki/CodeReviewHelp">Help</a>
|
<a class="novisit" target="_blank"
href="http://code.google.com/p/rietveld/issues/list">Bug tracker</a>
|
<a class="novisit" target="_blank"
href="http://groups.google.com/group/codereview-discuss">Discussion group</a>
|
<a class="novisit" target="_blank"
href="http://code.google.com/p/rietveld">Source code</a>
|
{%if user%}
<a class="novisit" href="{{sign_out}}">Sign out</a>
{% else %}
<a class="novisit" href="{{sign_in}}">Sign in</a>
{%endif%}
</div>
<div class="counter">({{counter}})</div>
<div class="mainmenu">
{%block mainmenu%}
<a href="{%url codereview.views.index %}">Issues</a>
<a href="{%url codereview.views.repos %}">Repositories</a>
<a href="{%url codereview.views.search%}">Search</a>
{%endblock%}
</div>
<div class="mainmenu2">
{%block mainmenu2%}{%endblock%}
</div>
<div>
{%block body%}BODY GOES HERE{%endblock%}
</div>
{%block popup%}{%endblock%}
<p></p>
<div style="float: left;">
<a target="_blank" href="http://code.google.com/appengine/"><img border="0"
src="{{media_url}}appengine-noborder-120x30.gif"
alt="Powered by Google App Engine" /></a>
</div>
<div class="extra" style="font-size: 9pt; float: right; text-align: right;">
<div style="height:14px;">
<img src="{{media_url}}rss.gif" alt="RSS Feeds" width="14" height="14"
align="top" />
<a href="{%url django.contrib.syndication.views.feed url="all"%}">Recent Issues</a>
{%if user%}
|
<a href="{%url django.contrib.syndication.views.feed url="mine"%}/{%nickname user True%}">My Issues</a>
|
<a href="{%url django.contrib.syndication.views.feed url="reviews"%}/{%nickname user True%}">My Reviews</a>
|
<a href="{%url django.contrib.syndication.views.feed url="closed"%}/{%nickname user True%}">My Closed</a>
{%endif%}
{%if issue%}
|
<a href="{%url django.contrib.syndication.views.feed url="issue"%}/{{issue.key.id}}">This issue</a>
{%endif%}
</div>
<div style="margin-top: .3em;">This is Rietveld <a href='http://code.google.com/p/rietveld/source/list'>{{rietveld_revision}}</a></div>
</div>
{%if not is_dev%}
<script type="text/javascript">
var gaJsHost = (("https:" == document.location.protocol) ? "https://ssl." : "http://www.");
document.write(unescape("%3Cscript src='" + gaJsHost + "google-analytics.com/ga.js' type='text/javascript'%3E%3C/script%3E"));
</script>
<script type="text/javascript">
var pageTracker = _gat._getTracker("UA-4803694-4");
pageTracker._initData();
pageTracker._trackPageview();
</script>
{%endif%}
</body>
</html>

19
templates/block_user.html Normal file
View File

@ -0,0 +1,19 @@
{%extends "base.html"%}
{%block mainmenu%}
<a href="{%url codereview.views.index%}" class="active">Issues</a>
<a href="{%url codereview.views.repos%}">Repositories</a>
<a href="{%url codereview.views.search%}">Search</a>
{%endblock%}
{%block title1%}Block a user -{%endblock%}
{%block head%}{{form.media}}{%endblock%}
{%block body%}
<h2>Blocking {{account.email}} ({{account.nickname}})</h2>
<form action="{%url codereview.views.block_user account.email%}" method="post">
<table>
{{form}}
<tr><td><input type="submit" value="Update"></td></tr>
</table>
</form>
{%endblock%}

View File

@ -0,0 +1,23 @@
{%extends "repos_base.html"%}
{%block title1%}Edit Branch -{%endblock%}
{%block body%}
<h2>Edit Branch '{{branch.name}}' in Repository '{{branch.repo.name}}'</h2>
<form action="{%url codereview.views.branch_edit branch.key.id%}" method="post">
<input type="hidden" name="xsrf_token" value="{{xsrf_token}}">
<table>
{{form}}
<tr><td><input type="submit" value="Update Branch"></td></tr>
</table>
</form>
<hr>
<form action="{%url codereview.views.branch_delete branch.key.id%}" method="post">
<input type="hidden" name="xsrf_token" value="{{xsrf_token}}">
<table>
<tr><td><input type="submit" value="Delete This Branch"></td></tr>
</table>
</form>
{%endblock%}

14
templates/branch_new.html Normal file
View File

@ -0,0 +1,14 @@
{%extends "repos_base.html"%}
{%block title1%}Create Branch -{%endblock%}
{%block body%}
<h2>Create New Branch in Repository '{{repo.name}}'</h2>
<form action="{%url codereview.views.branch_new repo.key.id%}" method="post">
<input type="hidden" name="xsrf_token" value="{{xsrf_token}}">
<table>
{{form}}
<tr><td><input type="submit" value="Create Branch"></td></tr>
</table>
</form>
{%endblock%}

197
templates/diff.html Normal file
View File

@ -0,0 +1,197 @@
{%extends "issue_base.html"%}
{%block body%}
<script language="JavaScript" type="text/javascript"><!--
document.onkeydown = M_keyDown;
{%if user%}
logged_in = true;
{%else%}
logged_in = false;
login_warned = false;
{%endif%}
// -->
</script>
{%if user%}
<!-- Form used by in-line comment JS; XXX filled in by JS code -->
<form id="dainlineform" style="display: none;"
action="{%url codereview.views.inline_draft%}" method="post">
<div class="comment-border">
<input type="hidden" name="snapshot" value="XXX">
<input type="hidden" name="lineno" value="XXX">
<input type="hidden" name="side" value="XXX">
<input type="hidden" name="issue" value="{{issue.key.id}}">
<input type="hidden" name="patchset" value="{{patchset.key.id}}">
<input type="hidden" name="patch" value="{{patch.key.id}}">
<textarea name="text" cols="60" rows="5"></textarea><br>
<input type="submit" name="save" value="Save"
onclick="return M_submitInlineComment(this.form);">
<input type="reset" name="cancel" value="Cancel"
onclick="M_removeTempInlineComment(this.form)">
</div>
<div class="comment-border" style="padding: 0pt;"></div>
</form>
<a id="resizer" style="display:none;cursor:pointer"><img src="{{media_url}}zippyplus.gif"></a>
{%endif%}
<div style="float: left;">
<h2 style="margin-bottom: 0em; margin-top: 0em;">Side by Side Diff: {{patch.filename}}</h2>
<div style="margin-top: .2em;">{%include "issue_star.html"%}
<b>Issue <a href="{%url codereview.views.show issue.key.id%}" onmouseover="M_showPopUp(this, 'popup-issue');" id="upCL">{{issue.key.id}}</a>:</b>
{{issue.subject}} {%if issue.closed %} (Closed) {%endif%}
{%if issue.base%}<span class="extra">Base URL: {{issue.base}}</span>{%endif%}</div>
<div style="margin-top: .4em;">
<b>Patch Set: {%if patchset.message%}{{patchset.message}}{%endif%}</b>
<span class="extra">
Created {{patchset.created|timesince}} ago
{%if patchset.url%},
Downloaded from: <a href="{{patchset.url}}">{{patchset.url}}</a>
{%endif%}
</span>
</div>
<div style="margin-top: .4em">
<table>
<tr>
<td>Left:</td>
<td>
<select name="left" id="left">
<option value="-1">Base</option>
{%for p in patchsets%}
<option value="{{p.key.id}}">Patch Set {{forloop.counter}}: {{p.message}}</option>
{%endfor%}
</select>
</td>
<td rowspan="2"><input type="button" value="Go" onclick="M_navigateDiff({{issue.key.id}}, '{{patch.filename}}')"></td>
</tr>
<tr>
<td>Right:</td>
<td>
<select name="right" id="right">
{%for p in patchsets%}
<option value="{{p.key.id}}" {%ifequal patchset.key.id p.key.id%}selected="selected"{%endifequal%}>Patch Set {{forloop.counter}}: {{p.message}}</option>
{%endfor%}
</select>
</td>
</tr>
</table>
</div>
<div style="margin-top: .4em;" class="help">
Use n/p to move between diff chunks;
N/P to move between comments.
{%if user%}
Double-click a line to add a draft in-line comment.
<br><span style="color:red">Draft comments are only viewable by you;</span>
use <a href="{%url codereview.views.publish issue.key.id%}" class="novisit">Publish+Mail Comments</a> ('m') to let others view them.
{%else%}
Please Sign in to add in-line comments.
{%endif%}
</div>
</div>
<div style="float: right; color: #333333; background-color: #eeeeec; border: 1px solid lightgray; -moz-border-radius: 5px 5px 5px 5px; padding: 5px;">
<div>{%include "view_details_select.html"%}</div>
<div style="margin-top: 5px;">
Jump to: <select onchange="M_jumpToPatch(this, {{issue.key.id}}, {{patchset.key.id}});">
{% for jump_patch in patchset.patches %}
<option value="{{jump_patch.filename}}"
{%ifequal jump_patch.key.id patch.key.id%} selected="selected"{%endifequal%}>{{jump_patch.filename}}</option>
{% endfor %}
</select>
</div>
{%if patch%}
<div style="margin-top: 5px;">
<a href="{%url codereview.views.patch issue.key.id,patchset.key.id,patch.key.id%}{%urlappend_view_settings%}">
View unified diff</a>
|
<a href="{%url codereview.views.download_patch issue.key.id,patchset.key.id,patch.key.id%}"
title="Download patch for {{patch.filename}}">
Download patch
</a>
</div>
{%endif%}
{%if user%}
<div style="margin-top: 5px;">
<a class="novisit" href="{%url codereview.views.publish issue.key.id%}">Publish+Mail
Comments</a> ('m')
|
<a class="novisit" href="javascript:draftMessage.dialog_show()">Edit draft message</a> ('M')
</div>
{%endif%}
</div>
<div style="clear: both;"></div>
<div class="code" style="margin-top: 1.3em; display: table; margin-left: auto; margin-right: auto;">
{%include "diff_navigation.html"%}
<div style="position:relative;" id="table-top">
<span id="hook-sel" style="display:none;"></span>
{%if patch.property_changes %}
<table border="0" cellpadding="0" cellspacing="0" width="100%">
<tr align="center"><td>
<table class="property_changes">
<tr><th>Property Changes:</th></tr>
{%for row in patch.property_changes%}<tr><td>{{row|safe}}</td></tr>{%endfor%}
</table></td></tr>
</table>
{%endif%}
<table border="0" cellpadding="0" cellspacing="0" id="thecode"
ondblclick="M_handleTableDblClick(event)" width="100%"
ontouchstart="M_handleTableTouchStart(event)"
ontouchend="M_handleTableTouchEnd(event)">
<tr id="codeTop"><th>OLD</th><th>NEW</th></tr>
{%if patch.is_binary %}
<tr>
<td style="width:50%" align="center">
<img src="{%url codereview.views.image issue.key.id,patchset.key.id,patch.key.id,0%}" />
</td>
<td style="width:50%" align="center">
<img src="{%url codereview.views.image issue.key.id,patchset.key.id,patch.key.id,1%}" />
</td>
</tr>
{%else%}
{%for row in rows%}{{row|safe}}{%endfor%}
{%endif%}
<tr id="codeBottom"><th>OLD</th><th>NEW</th></tr>
</table>
</div>
<div class="codenav">{%include "file_navigation.html"%}</div>
</div>
<script language="JavaScript" type="text/javascript"><!--
var old_snapshot = "old";
var new_snapshot = "new";
var intraLineDiff = new M_IntraLineDiff();
var hookState = new M_HookState(window);
hookState.updateHooks();
var skipped_lines_url = ('{%url diff_skipped_lines_prefix issue.key.id,patchset.key.id,patch.key.id%}');
;
// -->
</script>
{%include "draft_message.html"%}
{%endblock%}
{%block help%}
<div style="font-size: medium; text-align: center;">Side by Side Diff</div>
<hr/>
<div style="text-align: center; margin-bottom: .8em;">
Use n/p to move between diff chunks;
N/P to move between comments.
{%if user%}
Double-click a line to add a draft in-line comment.
<br><span style="color:red">Draft comments are only viewable by you;</span>
use <b>Publish+Mail Comments</b> ('m') to let others view them.
{%else%}
Please <a href="{{sign_in}}">Sign in</a> to add in-line comments.
{%endif%}
</div>
{%endblock%}

274
templates/diff2.html Normal file
View File

@ -0,0 +1,274 @@
{%extends "issue_base.html"%}
{%block title1%}{{patch_right.filename}} -{%endblock%}
{%block body%}
<script language="JavaScript" type="text/javascript"><!--
document.onkeydown = M_keyDown;
{%if user%}
logged_in = true;
{%else%}
logged_in = false;
login_warned = false;
{%endif%}
// -->
</script>
{%if user%}
<!-- Form used by in-line comment JS; XXX filled in by JS code -->
<form id="dainlineform" style="display: none;"
action="{%url codereview.views.inline_draft%}" method="post">
<div class="comment-border">
<input type="hidden" name="snapshot" value="XXX">
<input type="hidden" name="lineno" value="XXX">
<input type="hidden" name="side" value="XXX">
<input type="hidden" name="issue" value="{{issue.key.id}}">
{%if patch_left%}
<input type="hidden" name="ps_left" value="{{ps_left.key.id}}">
<input type="hidden" name="patch_left" value="{{patch_left.key.id}}">
<input type="hidden" name="ps_right" value="{{ps_right.key.id}}">
<input type="hidden" name="patch_right" value="{{patch_right.key.id}}">
{%else%}
<input type="hidden" name="patchset" value="{{ps_right.key.id}}">
<input type="hidden" name="patch" value="{{patch_right.key.id}}">
{%endif%}
<textarea name="text" cols="60" rows="5"></textarea><br>
<input type="submit" name="save" value="Save"
onclick="return M_submitInlineComment(this.form);">
<input type="reset" name="cancel" value="Cancel"
onclick="M_removeTempInlineComment(this.form)">
</div>
<div class="comment-border" style="padding: 0pt;"></div>
</form>
<a id="resizer" style="display:none;cursor:pointer"><img src="{{media_url}}zippyplus.gif"></a>
{%endif%}
<div style="float: left;">
<h2 style="margin-bottom: 0em; margin-top: 0em;">Delta Between Two Patch Sets: {{patch_right.filename}}</h2>
<div style="margin-top: .2em;">
{%include "issue_star.html"%}
<b>Issue <a href="{%url codereview.views.show issue.key.id%}" onmouseover="M_showPopUp(this, 'popup-issue');" id="upCL">{{issue.key.id}}</a>:</b>
{{issue.subject}} {%if issue.closed %} (Closed) {%endif%}
{%if issue.base%}<span class="extra">Base URL: {{issue.base}}</span>{%endif%}</div>
<div style="margin-top: .4em;">
<b>Left Patch Set: {%if ps_left.message%}{{ps_left.message}}{%endif%}</b>
<span class="extra">
Created {{ps_left.created|timesince}} ago
{%if ps_left.url%},
Downloaded from: <a href="{{ps_left.url}}">{{ps_left.url}}</a>
{%endif%}
</span>
</div>
<div style="margin-top: .4em;">
<b>Right Patch Set: {%if ps_right.message%}{{ps_right.message}}{%endif%}</b>
<span class="extra">
Created {{ps_right.created|timesince}} ago
{%if ps_right.url%},
Downloaded from: <a href="{{ps_right.url}}">{{ps_right.url}}</a>
{%endif%}
</span>
</div>
<div style="margin-top: .4em">
<table>
<tr>
<td>Left:</td>
<td>
<select name="left" id="left">
<option value="-1">Base</option>
{%for p in patchsets%}
<option value="{{p.key.id}}" {%ifequal ps_left.key.id p.key.id%}selected="selected"{%endifequal%}>Patch Set {{forloop.counter}}: {{p.message}}</option>
{%endfor%}
</select>
</td>
<td rowspan="2"><input type="button" value="Go" onclick="M_navigateDiff({{issue.key.id}}, '{{filename|escapejs}}')"></td>
</tr>
<tr>
<td>Right:</td>
<td>
<select name="right" id="right">
{%for p in patchsets%}
<option value="{{p.key.id}}" {%ifequal ps_right.key.id p.key.id%}selected="selected"{%endifequal%}>Patch Set {{forloop.counter}}: {{p.message}}</option>
{%endfor%}
</select>
</td>
</tr>
</table>
</div>
<div style="margin-top: .4em;" class="help">
Use n/p to move between diff chunks;
N/P to move between comments.
{%if user%}
Double-click a line to add a draft in-line comment.
<br><span style="color:red">Draft comments are only viewable by you;</span>
use <a href="{%url codereview.views.publish issue.key.id%}" class="novisit">Publish+Mail Comments</a> ('m') to let others view them.
{%else%}
Please Sign in to add in-line comments.
{%endif%}
</div>
</div>
<div style="float: right; color: #333333; background-color: #eeeeec; border: 1px solid lightgray; -moz-border-radius: 5px 5px 5px 5px; padding: 5px;">
<div>{%include "view_details_select.html"%}</div>
<div>
Jump to: <select onchange="M_jumpToPatch(this, {{issue.key.id}}, '{{ps_left.key.id}}:{{ps_right.key.id}}', false, 'diff2');">
{% for jump_patch in ps_right.patches %}
<option value="{{jump_patch.filename}}"
{%ifequal jump_patch.key.id patch_right.key.id%} selected="selected"{%endifequal%}>{{jump_patch.filename}}</option>
{% endfor %}
</select>
</div>
<div>
{%if patch_left%}
Left: <a href="{%url codereview.views.diff issue.key.id,ps_left.key.id,patch_left.filename%}{%urlappend_view_settings%}"
title="View regular side by side diff">Side by side diff</a>
|
<a href="{%url codereview.views.download_patch issue.key.id,ps_left.key.id,patch_left.key.id%}"
title="Download patch for {{patch_left.filename}}">Download</a>
<br/>
{%endif%}
{%if patch_right%}
Right: <a href="{%url codereview.views.diff issue.key.id,ps_right.key.id,patch_right.filename%}{%urlappend_view_settings%}"
title="View regular side by side diff">Side by side diff</a>
|
<a href="{%url codereview.views.download_patch issue.key.id,ps_right.key.id,patch_right.key.id%}"
title="Download patch for {{patch_right.filename}}">Download</a>
{%endif%}
{%if user%}
</div>
<div style="margin-top: 5px;">
<a class="novisit" href="{%url codereview.views.publish issue.key.id%}">Publish+Mail
Comments</a> ('m')
|
<a class="novisit" href="javascript:draftMessage.dialog_show()">Edit draft message</a> ('M')
{%endif%}
</div>
</div>
<div style="clear: both;"></div>
<div class="code" style="margin-top: 1.3em; display: table; margin-left: auto; margin-right: auto;">
<div class="codenav">
{%comment%}
For some reason,
{%url codereview.views.diff issue.key.id,patchset.key.id,patch.prev.filename%}
doesn't work. Go figure. Bleah. So use absolute URLs.
{%endcomment%}
{%if patch_right.prev_with_comment%}
<a id="prevFileWithComment"
href="{%url codereview.views.diff2 issue.key.id,ps_left.key.id,ps_right.key.id,patch_right.prev_with_comment.filename%}{%urlappend_view_settings%}">
&laquo; {{patch_right.prev_with_comment.filename}}</a> ('K'){%else%}
<span class="disabled">&laquo; no previous file with change/comment</span>{%endif%}
|
{%if patch_right.prev%}
<a id="prevFile"
href="{%url codereview.views.diff2 issue.key.id,ps_left.key.id,ps_right.key.id,patch_right.prev.filename%}{%urlappend_view_settings%}">
&laquo; {{patch_right.prev.filename}}</a> ('k'){%else%}
<span class="disabled">&laquo; no previous file</span>{%endif%}
|
{%if patch_right.next%}
<link rel="prerender"
href="{%url codereview.views.diff2 issue.key.id,ps_left.key.id,ps_right.key.id,patch_right.next.filename%}{%urlappend_view_settings%}"></link>
<a id="nextFile"
href="{%url codereview.views.diff2 issue.key.id,ps_left.key.id,ps_right.key.id,patch_right.next.filename%}{%urlappend_view_settings%}">
{{patch_right.next.filename}} &raquo;</a> ('j'){%else%}
<span class="disabled">no next file &raquo;</span>{%endif%}
|
{%if patch_right.next_with_comment%}
<a id="nextFileWithComment"
href="{%url codereview.views.diff2 issue.key.id,ps_left.key.id,ps_right.key.id,patch_right.next_with_comment.filename%}{%urlappend_view_settings%}">
{{patch_right.next_with_comment.filename}} &raquo;</a> ('J'){%else%}
<span class="disabled">no next file with change/comment &raquo;</span>{%endif%}
<br/>
<a href="javascript:if (intraLineDiff) intraLineDiff.toggle()">
Toggle Intra-line Diffs</a> ('i')
|
<a href="javascript:M_expandAllInlineComments()">Expand Comments</a> ('e')
|
<a href="javascript:M_collapseAllInlineComments()">Collapse Comments</a> ('c')
|
<a name="show-all-inline"
style="display:none"
href="javascript:M_showAllInlineComments()">Show Comments</a>
<a name="hide-all-inline"
href="javascript:M_hideAllInlineComments()">Hide Comments</a> ('s')
</div>
<div style="position:relative" id="table-top">
<span id="hook-sel" style="display:none;"></span>
<table border="0" cellpadding="0" cellspacing="0" id="thecode"
ondblclick="M_handleTableDblClick(event)"
ontouchstart="M_handleTableTouchStart(event)"
ontouchend="M_handleTableTouchEnd(event)"
width="100%">
<tr><th>LEFT</th><th>RIGHT</th></tr>
{%if patch_right.is_binary %}
<tr>
<td style="width:50%" align="center">
{%if patch_left%}
<img src="{%url codereview.views.image issue.key.id,ps_left.key.id,patch_left.key.id,1%}" />
{%else%}
<img src="{%url codereview.views.image issue.key.id,ps_right.key.id,patch_right.key.id,0%}" />
{%endif%}
</td>
<td style="width:50%" align="center">
<img src="{%url codereview.views.image issue.key.id,ps_right.key.id,patch_right.key.id,1%}" />
</td>
</tr>
{%else%}
{%for row in rows%}{{row|safe}}{%endfor%}
{%endif%}
<tr><th>LEFT</th><th>RIGHT</th></tr>
</table>
</div>
<div class="codenav">
{%if patch_right.prev%}
<a id="prevFile"
href="{%url codereview.views.diff2 issue.key.id,ps_left.key.id,ps_right.key.id,patch_right.prev.filename%}{%urlappend_view_settings%}">
&laquo; {{patch_right.prev.filename}}</a> ('k'){%else%}
<span class="disabled">&laquo; no previous file</span>{%endif%}
|
{%if patch_right.next%}
<link rel="prerender"
href="{%url codereview.views.diff2 issue.key.id,ps_left.key.id,ps_right.key.id,patch_right.next.filename%}{%urlappend_view_settings%}"></link>
<a id="nextFile"
href="{%url codereview.views.diff2 issue.key.id,ps_left.key.id,ps_right.key.id,patch_right.next.filename%}{%urlappend_view_settings%}">
{{patch_right.next.filename}} &raquo;</a> ('j'){%else%}
<span class="disabled">no next file &raquo;</span>{%endif%}
|
<a href="javascript:if (intraLineDiff) intraLineDiff.toggle()">
Toggle Intra-line Diffs</a> ('i')
|
<a href="javascript:M_expandAllInlineComments()">Expand Comments</a> ('e')
|
<a href="javascript:M_collapseAllInlineComments()">Collapse Comments</a> ('c')
|
<a href="javascript:M_toggleAllInlineComments()">
Toggle Comments</a> ('s')
</div>
</div>
<script language="JavaScript" type="text/javascript"><!--
{%if patch_left%}
var old_snapshot = "new";
{%else%}
var old_snapshot = "old";
{%endif%}
var new_snapshot = "new";
var intraLineDiff = new M_IntraLineDiff();
var hookState = new M_HookState(window);
hookState.updateHooks();
{%if patch_right%}
var skipped_lines_url = ('{%url diff2_skipped_lines_prefix issue.key.id,ps_left.key.id,ps_right.key.id,patch_id%}');
{%endif%}
// -->
</script>
{%include "draft_message.html"%}
{%endblock%}

View File

@ -0,0 +1,64 @@
{%extends "issue_base.html"%}
{%block body%}
<script language="JavaScript" type="text/javascript"><!--
document.onkeydown = M_keyDown;
{%if user%}
logged_in = true;
{%else%}
logged_in = false;
login_warned = false;
{%endif%}
// -->
</script>
<div style="float: left;">
<h2 style="margin-bottom: 0em; margin-top: 0em;">Side by Side diff: {{ filename }}</h2>
<div style="margin-top: .2em;">{%include "issue_star.html"%}
<b>Issue <a href="{%url codereview.views.show issue.key.id%}" onmouseover="M_showPopUp(this, 'popup-issue');" id="upCL">{{issue.key.id}}</a>:</b>
{{issue.subject}} {%if issue.closed %} (Closed) {%endif%}
{%if issue.base%}<span class="extra">Base URL: {{issue.base}}</span>{%endif%}</div>
<div style="margin-top: .4em;">
<b>Patch Set: {%if patchset.message%}{{patchset.message}}{%endif%}</b>
<span class="extra">
Created {{patchset.created|timesince}} ago
{%if patchset.url%},
Downloaded from: <a href="{{patchset.url}}">{{patchset.url}}</a>
{%endif%}
</span>
</div>
<div style="margin-top: .4em">
<table>
<tr>
<td>Left:</td>
<td>
<select name="left" id="left">
<option value="-1">Base</option>
{%for p in patchsets%}
<option value="{{p.key.id}}">Patch Set {{forloop.counter}}: {{p.message}}</option>
{%endfor%}
</select>
</td>
<td rowspan="2"><input type="button" value="Go" onclick="M_navigateDiff({{issue.key.id}}, '{{filename|escapejs}}')"></td>
</tr>
<tr>
<td>Right:</td>
<td>
<select name="right" id="right">
{%for p in patchsets%}
<option value="{{p.key.id}}" {%ifequal patchset.key.id p.key.id%}selected="selected"{%endifequal%}>Patch Set {{forloop.counter}}: {{p.message}}</option>
{%endfor%}
</select>
</td>
</tr>
</table>
</div>
</div>
<div style="clear: both;"></div>
<div class="error">
<p>The selected patch doesn't exist in the selected patchset.</p>
<p>Use the patchset chooser above or go back to the
<a href="{%url codereview.views.show issue.key.id%}">issue page</a>.
</p>
</div>
{%endblock%}

View File

@ -0,0 +1,15 @@
<div class="codenav">
{%include "file_navigation.html"%} <br/>
<a href="javascript:if (intraLineDiff) intraLineDiff.toggle()">
Toggle Intra-line Diffs</a> ('i')
|
<a href="javascript:M_expandAllInlineComments()">Expand Comments</a> ('e')
|
<a href="javascript:M_collapseAllInlineComments()">Collapse Comments</a> ('c')
|
<a name="show-all-inline"
style="display:none"
href="javascript:M_showAllInlineComments()">Show Comments</a>
<a name="hide-all-inline"
href="javascript:M_hideAllInlineComments()">Hide Comments</a> ('s')
</div>

View File

@ -0,0 +1,22 @@
{%if issue and user%}
<form>
<div id="reviewmsgdlg" style="display:none;">
<div class="title">Edit Message</div>
<textarea id="reviewmsg"></textarea>
<input type="hidden" id="reviewmsgorig" />
<div class="button">
<input type="button" value="Save" name="save"
onclick="return draftMessage.dialog_save();" />
<input type="button" value="Discard"
onclick="return draftMessage.dialog_discard();" />
<input type="button" value="Close"
onclick="return draftMessage.dialog_hide(true);" />
<span id="reviewmsgstatus"></span>
</div>
</div>
</form>
<script language="JavaScript" type="text/javascript"><!--
draftMessage = new M_draftMessage({{issue.key.id}});
// -->
</script>
{%endif%}

43
templates/edit.html Normal file
View File

@ -0,0 +1,43 @@
{%extends "issue_base.html"%}
{%block title1%}Edit Issue -{%endblock%}
{%block head%}{{form.media}}{%endblock%}
{%block issue_body%}
<form action="{%url codereview.views.edit issue.key.id%}"
method="post" id="edit-form">
<input type="hidden" name="xsrf_token" value="{{xsrf_token}}">
<table>
{{form}}
{%if issue.local_base%}
<tr>
<th><label>Base files:</label></hd>
<td>Use <i>upload.py</i> to upload base files.</td>
</tr>
{%endif%}
<tr><td><input type="submit" value="Update Issue"></td></tr>
</table>
</form>
<a id="resizer" style="display:none;cursor:pointer">
<img src="{{media_url}}zippyplus.gif"></a>
<script language="JavaScript" type="text/javascript"><!--
M_addTextResizer_(document.getElementById("edit-form"));
--></script>
<p>
<ul>
<li>Owner: {{issue.owner|show_user}}
<li>Created: {{issue.created|date:"Y/m/d H:i:s"}}
<li>Last updated: {{issue.modified|date:"Y/m/d H:i:s"}}
</ul>
</p>
{%ifequal issue.owner user%}
<h2>Delete This Issue</h2>
<form action="{%url codereview.views.delete issue.key.id%}" method="post">
<input type="hidden" name="xsrf_token" value="{{xsrf_token}}">
<input type="submit" value="Delete Issue"> There is no undo!
</form>
{%endifequal%}
{%endblock%}

27
templates/exception.html Normal file
View File

@ -0,0 +1,27 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<title>An Error Occurred - Code Review</title>
<style>
body {
font-family: Arial, Helvetica, sans-serif;
background-color: #fff;
}
.technical {
color: #666;
}
</style>
</head>
<body>
<div style="font-weight:bold; font-size: 83%;">
<i>Rietveld</i> Code Review Tool
</div>
<div style="margin: 20px;">
<h1>Sorry, an error occurred...</h1>
<p>{{msg}}</p>
<p class="technical">Details: {{technical}}</p>
<p>Please post a message to the <a href="http://groups.google.com/group/codereview-discuss">discussion group</a> if you have any questions.</p>
</div>
</body>
</html>

View File

@ -0,0 +1 @@
{%extends "feeds/template_description.html"%}

View File

@ -0,0 +1 @@
{%extends "feeds/template_title.html"%}

View File

@ -0,0 +1 @@
{%extends "feeds/template_description.html"%}

View File

@ -0,0 +1 @@
{%extends "feeds/template_title.html"%}

View File

@ -0,0 +1,7 @@
{%if obj.owner%}
<a href="{%url codereview.views.download obj.issue.key.id,obj.key.id%}">
Download raw patch set
</a>
{%else%}
{{obj.text}}
{%endif%}

View File

@ -0,0 +1,5 @@
{%if obj.owner%}
PatchSet : {{obj.message}}
{%else%}
Message from {%if obj.sender%}{{obj.sender}}{%else%}unknown{%endif%}
{%endif%}

View File

@ -0,0 +1 @@
{%extends "feeds/template_description.html"%}

View File

@ -0,0 +1 @@
{%extends "feeds/template_title.html"%}

View File

@ -0,0 +1 @@
{%extends "feeds/template_description.html"%}

View File

@ -0,0 +1 @@
{%extends "feeds/template_title.html"%}

View File

@ -0,0 +1,5 @@
{%if obj.description%}
{{obj.description}}
{%else%}
(No description)
{%endif%}

View File

@ -0,0 +1 @@
{{obj.subject}}

View File

@ -0,0 +1,25 @@
{%if patch.prev_with_comment%}
<a id="prevFileWithComment"
href="{%ifequal view_style 'patch'%}{%url codereview.views.patch issue.key.id,patchset.key.id,patch.prev_with_comment.key.id%}{%else%}{%url codereview.views.diff issue.key.id,patchset.key.id,patch.prev_with_comment.filename%}{%endifequal%}{%urlappend_view_settings%}">
&laquo; {{patch.prev_with_comment.filename}}</a> ('K'){%else%}
<span class="disabled">&laquo; no previous file with comments</span>{%endif%}
|
{%if patch.prev%}
<a id="prevFile"
href="{%ifequal view_style 'patch'%}{%url codereview.views.patch issue.key.id,patchset.key.id,patch.prev.key.id%}{%else%}{%url codereview.views.diff issue.key.id,patchset.key.id,patch.prev.filename%}{%endifequal%}{%urlappend_view_settings%}">
&laquo; {{patch.prev.filename}}</a> ('k'){%else%}
<span class="disabled">&laquo; no previous file</span>{%endif%}
|
{%if patch.next%}
<link rel="prerender"
href="{%ifequal view_style 'patch'%}{%url codereview.views.patch issue.key.id,patchset.key.id,patch.next.key.id%}{%else%}{%url codereview.views.diff issue.key.id,patchset.key.id,patch.next.filename%}{%endifequal%}{%urlappend_view_settings%}"></link>
<a id="nextFile"
href="{%ifequal view_style 'patch'%}{%url codereview.views.patch issue.key.id,patchset.key.id,patch.next.key.id%}{%else%}{%url codereview.views.diff issue.key.id,patchset.key.id,patch.next.filename%}{%endifequal%}{%urlappend_view_settings%}">
{{patch.next.filename}} &raquo;</a> ('j'){%else%}
<span class="disabled">no next file &raquo;</span>{%endif%}
|
{%if patch.next_with_comment%}
<a id="nextFileWithComment"
href="{%ifequal view_style 'patch'%}{%url codereview.views.patch issue.key.id,patchset.key.id,patch.next_with_comment.key.id%}{%else%}{%url codereview.views.diff issue.key.id,patchset.key.id,patch.next_with_comment.filename%}{%endifequal%}{%urlappend_view_settings%}">
{{patch.next_with_comment.filename}} &raquo;</a> ('J'){%else%}
<span class="disabled">no next file with comments &raquo;</span>{%endif%}

View File

@ -0,0 +1,67 @@
{%for c in comments%}
<div class="comment-border{%if c.confidence%} conf{{c.confidence}}{%endif%} {{c.backend}}" name="comment-border">
<div class="inline-comment-title" onclick="M_switchInlineComment({{forloop.counter0}}, {{lineno}}, '{{side}}')">
{%if c.draft%}<b>(Draft)</b>{%else%}<b>{%nickname c.author%}</b>{%endif%}
{%if c.confidence_text%} <b>(confidence {{c.confidence_text}})</b>{%endif%}
{{c.date|date:"Y/m/d H:i:s"}}
<span id="inline-preview-{{forloop.counter0}}-{{lineno}}-{{side}}"
class="extra" name="inline-preview"
{%if c.draft%}style="display: none"{%endif%}>{{c.shorttext}}</span>
</div>
<div id="inline-comment-{{forloop.counter0}}-{{lineno}}-{{side}}"
class="inline-comment"
name="inline-comment"
{%if c.draft%}ondblclick="M_editInlineComment({{forloop.counter0}}, {{lineno}}, '{{side}}'); M_stopBubble(window, event);"{%endif%}
{%if not c.draft%}style="display: none"{%endif%}>
{%for bucket in c.buckets%}
{%if bucket.quoted%}
<div name="comment-hide-{{forloop.parentloop.counter0}}-{{lineno}}-{{side}}"><a class="comment-hide-link" id="comment-hide-link-{{forloop.parentloop.counter0}}-{{lineno}}-{{side}}-{{forloop.counter0}}" href="javascript:M_switchQuotedText({{forloop.parentloop.counter0}}, {{forloop.counter0}}, {{lineno}}, '{{side}}')">Show quoted text</a></div>
{%endif%}
<div name="comment-text-{{forloop.parentloop.counter0}}-{{lineno}}-{{side}}"
id="comment-text-{{forloop.parentloop.counter0}}-{{lineno}}-{{side}}-{{forloop.counter0}}"
class="{%if bucket.quoted%}comment-text-quoted{%else%}comment-text{%endif%}"
{%if bucket.quoted%}style="display: none"{%endif%}
>{{bucket.text|wordwrap:"80"|escape|urlizetrunc:80}}
</div>
{%endfor%}
{%if c.draft%}
<a name="comment-reply"
id="edit-link-{{forloop.counter0}}-{{lineno}}-{{side}}"
href="javascript:M_editInlineComment({{forloop.counter0}}, {{lineno}}, '{{side}}')"><b>Edit</b></a>
<a name="comment-reply"
id="undo-link-{{forloop.counter0}}-{{lineno}}-{{side}}"
style="display:none"
href="javascript:M_restoreEditInlineComment({{forloop.counter0}}, {{lineno}}, '{{side}}')"><b>Undo cancel</b></a>
<form id="comment-form-{{forloop.counter0}}-{{lineno}}-{{side}}"
name="comment-form-{{forloop.counter0}}-{{lineno}}-{{side}}"
style="display:none" action="{%url codereview.views.inline_draft%}" method = "POST">
<div>
<input type="hidden" name="issue" value="{{issue.key.id}}">
<input type="hidden" name="patchset" value="{{patchset.key.id}}">
<input type="hidden" name="patch" value="{{patch.key.id}}">
<input type="hidden" name="snapshot" value="{{snapshot}}">
<input type="hidden" name="side" value="{{side}}">
<input type="hidden" name="file" value="{{file.depot_path|escape}}">
<input type="hidden" name="lineno" value="{{lineno}}">
<input type="hidden" name="oldtext" value="{{c.text}}">
{%if c.message_id%}<input type="hidden" name="message_id" value="{{c.message_id}}">{%endif%}
<textarea name="text" cols="60" rows="5">{{c.text}}</textarea><br>
<input type="submit" name="save" value="Save" onclick="return M_submitInlineComment(this.form, {{forloop.counter0}}, {{lineno}}, '{{side}}')">
<input type="reset" name="cancel" value="Cancel" onclick="M_resetAndHideInlineComment(this.form, {{forloop.counter0}}, {{lineno}}, '{{side}}')">
<input type="submit" name="discard" value="Discard" onclick="return M_removeInlineComment(this.form, {{forloop.counter0}}, {{lineno}}, '{{side}}')">
</div>
</form>
{%else%}
{%if user%}
<a name="comment-reply"
href="javascript:M_replyToInlineComment('{%nickname c.author True%}', '{{c.date|date:"Y/m/d H:i:s"}}', {{forloop.counter0}}, {{lineno}}, '{{side}}')"
><b>Reply</b></a>
{%if c.backend%}<a href="javascript:M_replyToInlineComment('{%nickname c.author True%}', '{{c.date|date:"Y/m/d H:i:s"}}', {{forloop.counter0}}, {{lineno}}, '{{side}}', 'Please fix.', true)"><b>Please fix</b></a>
{%else%}<a name="comment-done" href="javascript:M_replyToInlineComment('{%nickname c.author True%}', '{{c.date|date:"Y/m/d H:i:s"}}', {{forloop.counter0}}, {{lineno}}, '{{side}}', 'Done.', true)"><b>Done</b></a>{%endif%}
{%ifequal c.backend "bugbot"%} &nbsp; <a target="_blank" href="http://bugbotexp{{file.depot_path|escape}}"><b>&raquo; Suppress</b></a>{%endifequal%}
{%endif%}
{%endif%}
</div>
</div>
{%endfor%}
<div class="comment-border" style="padding: 0"></div>

224
templates/issue.html Normal file
View File

@ -0,0 +1,224 @@
{%extends "issue_base.html"%}
{%block head%}{{form.media}}{%endblock%}
{%block issue_body%}
{%if issue.draft_count or has_draft_message%}
<div class="error">
You have {%if issue.draft_count%}<b>{{issue.draft_count}} draft</b>
comment{{issue.draft_count|pluralize}}{%endif%}
{%if has_draft_message%}{%if issue.draft_count%}and {%endif%}a draft
message{%endif%}.
Drafts are not viewable by others;
use <a class="novisit"
href="{%url codereview.views.publish issue.key.id%}">
Publish+Mail Comments</a> ('m') to let others view them.
</div>
{%endif%}
{%if issue.description%}
<h3><a id="issue-description-pointer"
href="javascript:M_toggleSection('issue-description')"
class="toggled-section opentriangle">
Description</a></h3>
<div id="issue-description" style="margin-left:15px;">
<pre>{{issue.description|wordwrap:80|urlizetrunc:80}}</pre>
</div>
{%endif%}
{%for patchset in patchsets%}
<h3>
<a id="ps-{{patchset.key.id}}-pointer"
href="{%url codereview.views.show issue.key.id%}#ps{{patchset.key.id}}"
onclick="M_toggleSectionForPS('{{issue.key.id}}', '{{patchset.key.id}}')"
class="toggled-section {%if forloop.last%}opentriangle{%endif%}">
Patch Set {{forloop.counter}}
{%if patchset.message%}: {{patchset.message}}{%endif%}
<span class="anchor">#</span>
</a>
</h3>
{%if patchset.num_comments or patchset.n_drafts%}
<div>
<i>Total comments:</i> {{patchset.num_comments}}
{%if patchset.n_drafts%}
<span style="color:red">
<b>+ {{patchset.n_drafts}} draft{{patchset.n_drafts|pluralize}}</b>
</span>
{%endif%}
</div>
{%endif%}
<div id="ps-{{patchset.key.id}}"
{%if forloop.last%}
style="">
{%include "patchset.html"%}
{%else%}
style="display:none">
{%endif%}
</div>
{%if forloop.last%}
<script language="JavaScript" type="text/javascript">
<!--
var lastPSId = {{patchset.key.id}};
// -->
</script>
{%endif%}
{%endfor%}
{%ifequal user issue.owner%}
{%if not issue.local_base%}
<h3>
<a id="add-pointer"
href="javascript:M_toggleSection('add')"
class="toggled-section">
Add Another Patch Set
</a>
</h3>
<form id="add" style="{%if not form.errors%}display:none{%endif%}"
action="{%url codereview.views.add issue.key.id%}" method="post"
enctype="multipart/form-data">
<input type="hidden" name="xsrf_token" value="{{xsrf_token}}">
<table>
{{form}}
<tr>
<td><input type="submit" value="Add Patch Set" /></td>
<td>
You can also add a patch set to this issue using
<code>upload.py -i {{issue.key.id}}</code>
</td>
</tr>
</table>
</form>
{%endif%}
{%endifequal%}
{%if messages%}
<h3>
<a id="messages-pointer"
href="javascript:M_toggleSection('messages')"
class="toggled-section opentriangle">
Messages
</a>
</h3>
{%if messages%}<div><i>Total messages: {{messages|length}}</i></div>{%endif%}
<div id="messages">
<div style="margin-bottom: .5em;">
<a href="javascript:M_showAllComments('cl', {{messages|length}})">
Expand All Messages</a>
|
<a href="javascript:M_hideAllComments('cl', {{messages|length}})">
Collapse All Messages</a>
</div>
{%for message in messages%}
<div class="message {%if message.issue_was_closed%}issue_was_closed{%endif%} {%if message.approval%}approval{%endif%} {%if message.disapproval%}disapproval{%endif%}"
id="msg{{forloop.counter}}"
name="{{forloop.counter0}}">
<div class="header">
<table border="0" width="100%" cellspacing="0" cellpadding="0">
<tr class="comment_title"
onclick="M_switchChangelistComment({{forloop.counter0}})">
<td style="padding-left: 5px; white-space: nowrap;">
<b>{%nickname message.sender%}</b>
</td>
<td width="100%" style="overflow:hidden;">
<table style="table-layout:fixed; white-space: nowrap;"
width="100%">
<tr>
<td>
<span style="white-space: nowrap; overflow: hidden;{%if forloop.last%} display: none;{%endif%}"
class="extra"
id="cl-preview-{{forloop.counter0}}">
{{message.text|truncatewords:15}}
</span>
</td>
</tr>
</table>
</td>
<td align="right"
style="white-space: nowrap; padding-right: 5px; padding-left: 3px;">
{{message.date|timesince}} ago
<a href="#msg{{forloop.counter}}">#{{forloop.counter}}</a>
</td>
</tr>
</table>
</div>
<div id="cl-comment-{{forloop.counter0}}"
{%if forloop.last%}{%else%}style="display: none;"{%endif%}>
<div class="message-body">
{%if message.issue_was_closed%}
<span class="extra-note">
Message was sent while issue was closed.
</span>
{%endif%}
<pre name="cl-message-{{forloop.counter0}}"
>{{message.text|wordwrap:80|urlizetrunc:80}}</pre>
</div>
<div class="message-actions">
{%if user%}
<a href="javascript:M_replyToMessage('{{forloop.counter0}}', '{{message.date|date:"Y/m/d H:i:s"}}', '{%nickname message.sender True%}', '{{message.key}}')"
id="message-reply-href-{{forloop.counter0}}">Reply</a>
<textarea rows="7" cols="70" name="message" style="display:none"></textarea>
<div id="message-reply-{{forloop.counter0}}"
style="display:none;"></div>
{%else%}
<a href="{{sign_in}}">Sign in</a> to reply to this message.
{%endif%}
</div>
</div>
</div>
{%endfor%}
<div>
<a href="javascript:M_showAllComments('cl', {{messages|length}})">
Expand All Messages</a>
|
<a href="javascript:M_hideAllComments('cl', {{messages|length}})">
Collapse All Messages</a>
</div>
</div>
{%endif%}
<script language="JavaScript" type="text/javascript">
<!--
document.onkeydown = M_changelistKeyDown;
var dashboardState = new M_DashboardState(window, 'patch', 'M_CLPatchMarker');
var issueId = {{issue.key.id}};
M_toggleIssueOverviewByAnchor();
// -->
</script>
{%if user%}
<div style="display:none;">
<form method="POST" action="{%url codereview.views.publish issue.key.id%}"
id="message-reply-form">
<input type="hidden" name="xsrf_token" value="{{xsrf_token}}">
<div></div>
<input type="hidden" name="in_reply_to" value="" />
<input type="hidden" name="subject" value="{{issue.subject}}" />
<input type="hidden" name="message_only" value="1" />
<input type="submit" value="Send Message" />
<input type="button" value="Discard" name="discard" />
<input type="checkbox" name="send_mail" value="1"
id="message-reply-send-mail" checked="checked" />
<label>Send mail to reviewers</label>
</form>
</div>
<a id="resizer" class="resizer" style="display:none;cursor:pointer">
<img src="{{media_url}}zippyplus.gif">
</a>
{%endif%}
{%endblock%}

147
templates/issue_base.html Normal file
View File

@ -0,0 +1,147 @@
{%extends "base.html"%}
{%block mainmenu%}
<a href="{%url codereview.views.index%}" class="active">Issues</a>
<a href="{%url codereview.views.repos%}">Repositories</a>
<a href="{%url codereview.views.search%}">Search</a>
{%endblock%}
{%block mainmenu2%}
{%if user%}
{%if uploadpy_hint%}
<a href="{%url codereview.views.use_uploadpy%}">Create Issue</a>
{%else%}
<a href="{%url codereview.views.new%}">Create Issue</a>
{%endif%}
&nbsp;&nbsp;&nbsp;
<a href="{%url codereview.views.mine%}">My Issues</a>
|
<a href="{%url codereview.views.starred%}">Starred</a>
&nbsp;&nbsp;&nbsp;
<a href="{%url codereview.views.all%}?closed=0">Open</a>
|
<a href="{%url codereview.views.all%}?closed=1">Closed</a>
|
<a href="{%url codereview.views.all%}">All</a>
{%else%}
<a class="novisit" href="{%url codereview.views.index%}?closed=0">Open Issues</a>
|
<a class="novisit" href="{%url codereview.views.index%}?closed=1">Closed Issues</a>
|
<a class="novisit" href="{%url codereview.views.all%}">All Issues</a>
|
<a class="novisit" href="{{sign_in}}">Sign in</a>
with your <a href="https://www.google.com/accounts/NewAccount">Google
Account</a> to create issues and add comments
{%endif%}
{%endblock%}
{%block body%}
<h2>
{%include "issue_star.html"%}
{%if issue.edit_allowed and not issue.closed%}
<span class="issue-close" id="issue-close-{{issue.key.id}}">
<a href="javascript:M_closeIssue({{issue.key.id}})">
<img src="{{media_url}}close.gif" title="Close This Issue" width="15"
height="15" border="0"></a>
</span>
{%endif%}
Issue <a href="{%url codereview.views.show issue.key.id%}"
onmouseover="M_showPopUp(this, 'popup-issue');">
{{issue.key.id}}</a>:
{{issue.subject}} {%if issue.closed %} (Closed) {%endif%}
</h2>
<table class="issue-details" border="0" width="100%">
<tr valign="top">
<td class="meta" width="20%">
{%block issue_actions%}
<div>
{%if issue.edit_allowed%}
<a class="novisit"
href="{%url codereview.views.edit issue.key.id%}">
Edit Issue
</a>
{%else%}
<span class="disabled">Can't Edit</span>
{%endif%}
<br/>
{%if user%}
<a class="novisit"
href="{%url codereview.views.publish issue.key.id%}">
Publish+Mail Comments
</a> ('m')
{%else%}
<span class="disabled">Can't Publish+Mail</span>
{%endif%}
{%if last_patchset and first_patch%}
<br/>
<a class="novisit"
href="{%url codereview.views.diff issue.key.id,last_patchset.key.id,first_patch.filename%}">
<b>Start Review</b>
</a>
{%endif%}
</div>
{%endblock%}
<div class="issue_details_sidebar">
<div><b>Created:</b><br/>
{{issue.created|timesince}} ago by {{issue.owner|show_user}}
</div>
<div><b>Modified:</b><br/>
{{issue.modified|timesince}} ago
</div>
<div><b>Reviewers:</b><br/>
{{issue.reviewers|show_users}}
</div>
{%if issue.cc%}
<div><b>CC:</b><br/>
{%nicknames issue.cc%}
</div>
{%endif%}
{%if issue.base%}
<div><b>Base URL:</b><br/>
{{issue.base}}
</div>
{%endif%}
<div><b>Visibility:</b><br/>
{%if issue.private%}
Private. Only viewable by reviewers and CCs.
{% else %}
Public.
{%endif%}
</div>
{%if issue.repo_guid%}
<div><a title="Find reviews for the same repository ID - {{issue.repo_guid}}"
href="{%url codereview.views.search%}?repo_guid={{issue.repo_guid}}">
<b>More Reviews</b></a>
</div>
{%endif%}
</div>
</td>
<td style="padding-left: .8em; padding-right: .8em;" width="80%">
{%block issue_body%}BODY GOES HERE{%endblock%}
</td>
</tr>
</table>
{%endblock%}
{%block popup%}
{%if issue%}
<div class="popup" id="popup-issue">
<b>Issue {{issue.key.id}}: {{issue.subject}}
{%if issue.closed %} (Closed) {%endif%}</b><br/>
Created {{issue.created|timesince}} ago by {%nickname issue.owner%}<br/>
Modified {{issue.modified|timesince}} ago<br/>
Reviewers: {%nicknames issue.reviewers%}<br/>
Base URL: {{issue.base}}<br/>
Comments: {{issue.num_comments}}
{%if issue.num_drafts%} <span style="color: red;">+
{{issue.num_drafts}} drafts</span>{%endif%}
</div>
{%endif%}
{%endblock%}

View File

@ -0,0 +1,9 @@
<tr align="left">
<th class="first" colspan="3" align="right">Id</th>
<th>Subject</th>
<th>Owner</th>
<th>Reviewers</th>
<th align="center">Comments</th>
<th align="center">Drafts</th>
<th>Last updated</th>
</tr>

View File

@ -0,0 +1,51 @@
{%extends "issue_base.html"%}
{%block body%}
<script language="JavaScript" type="text/javascript"><!--
document.onkeydown = M_dashboardKeyDown;
-->
</script>
<h2>{%block subtitle%}Issues{%endblock%}</h2>
<div class="issue-list">
<div class="pagination">
{%if newest%}
<a class="novisit" href="{{newest}}">&laquo; Newest</a>
{%endif%}
{%if prev%}
<a class="novisit" href="{{prev}}">&lsaquo; Newer</a>
{%endif%}
<b>{{first}}{%if last%} - {{last}}{%endif%}</b>
{%if next%}<a class="novisit" href="{{next}}">{{nexttext}} &rsaquo;</a>
{%else%}<span style="color:gray">{{nexttext}} &rsaquo;</span>{%endif%}
</div>
<table id="queues">
{%if not issues%}
<tr><td colspan="9"><span class="disabled">(None)</span></td></tr>
{%else%}
{%include "issue_heading.html"%}
{%for issue in issues%}
{%include "issue_row.html"%}
{%endfor%}
{%endif%}
</table>
<div class="pagination">
{%if newest%}
<a class="novisit" href="{{newest}}">&laquo; Newest</a>
{%endif%}
{%if prev%}
<a class="novisit" href="{{prev}}">&lsaquo; Newer</a>
{%endif%}
<b>{{first}}{%if last%} - {{last}}{%endif%}</b>
{%if next%}<a class="novisit" href="{{next}}">{{nexttext}} &rsaquo;</a>
{%else%}<span style="color:gray">{{nexttext}} &rsaquo;</span>{%endif%}
</div>
</div>
<script language="JavaScript" type="text/javascript"><!--
var dashboardState = new M_DashboardState(window,'issue');
-->
</script>
{%endblock%}

28
templates/issue_row.html Normal file
View File

@ -0,0 +1,28 @@
<tr {%if issue.num_drafts%}style="color:red"{%endif%} name="issue">
<td class="first" width="14"><img src="{{media_url}}closedtriangle.gif"
style="visibility: hidden;" width="12" height="9" /></td>
<td width="34" align="left" style="white-space: nowrap">{%include "issue_star.html"%}
{%if issue.edit_allowed and not issue.closed%}
<span class="issue-close" id="issue-close-{{issue.key.id}}">
<a href="javascript:M_closeIssue({{issue.key.id}})">
<img src="{{media_url}}close.gif" title="Close This Issue" width="15"
height="15" border="0"></a>
</span>
{%endif%}</td>
<td align="right"><div class="subject"><a class="noul"
href="{%url codereview.views.show issue.key.id%}">{{issue.key.id}}</a>
</div>
</td>
<td>
<div class="subject">
<a class="noul" href="{%url codereview.views.show issue.key.id%}"
id="issue-title-{{issue.key.id}}">{{issue.subject}}</a>
{%if issue.closed and not closed_issues %} (Closed) {%endif%}
</div>
</td>
<td><div class="users">{{issue.owner|show_user}}</div></td>
<td><div class="users">{{issue.reviewers|show_users}}</div></td>
<td align="center">{%firstof issue.num_comments%}</td>
<td align="center"><b>{%firstof issue.num_drafts%}</b></td>
<td class="last"><div class="date">{{issue.modified|timesince}}</div></td>
</tr>

11
templates/issue_star.html Normal file
View File

@ -0,0 +1,11 @@
<span id="issue-star-{{issue.key.id}}">
{%if issue.is_starred%}
<a href="javascript:M_removeIssueStar({{issue.key.id}})">
<img src="{{media_url}}star-lite.gif" width="15" height="15" border="0"></a>
{%else%}
{%if request.user%}
<a href="javascript:M_addIssueStar({{issue.key.id}})">
<img src="{{media_url}}star-dark.gif" width="15" height="15" border="0"></a>
{%endif%}
{%endif%}
</span>

View File

@ -0,0 +1,5 @@
{%autoescape off%}{%if message%}{{message|wordwrap:"72"}}
{%endif%}{%if details%}{{details|wordwrap:"72"}}
{%endif%}{{url}}{%endautoescape%}

View File

@ -0,0 +1,18 @@
{%autoescape off%}Reviewers: {{reviewer_nicknames}},
{%if message%}Message:
{{message|wordwrap:"72"}}
{%endif%}{%if details%}{{details|wordwrap:"72"}}
{%endif%}{%if description%}Description:
{{description|wordwrap:"72"}}{%endif%}
Please review this at {{url}}{%if files%}
Affected files:
{% for file in files %} {{file}}
{% endfor %}
{%endif%}{%if patch%}
{{patch}}
{%endif%}{%endautoescape%}

View File

@ -0,0 +1,23 @@
{%extends "settings_base.html"%}
{%block title1%}Settings -{%endblock%}
{%block body%}
<h2>Migrate issues and repositories</h2>
{%if msg%}
<p>{{msg}}</p>
{%else%}
<p>
In case you've changed your email address you're using for login,
enter your previous email address below to migrate issues,
repositories and branches you own to your current account.
</p>
<form action="{%url codereview.views.migrate_entities%}" method="post">
<input type="hidden" name="xsrf_token" value="{{xsrf_token}}">
<table>
{{form}}
<tr><td><input type="submit" value="Start migration" name="migrate"></td></tr>
</table>
</form>
{%endif%}
<p>Back to your <a href="{%url codereview.views.settings%}">settings</a>.</p>
{%endblock%}

30
templates/new.html Normal file
View File

@ -0,0 +1,30 @@
{%extends "issue_base.html"%}
{%block title1%}Create Issue -{%endblock%}
{%block head%}{{form.media}}{%endblock%}
{%block body%}
{%if form.errors%}
<h2>There Were Errors!</h2>
{%else%}
<h2>Create New Issue</h2>
{%endif%}
<p>Download <a href="{%url codereview.views.customized_upload_py%}">upload.py</a>, a simple tool for
uploading diffs from a version control system to the codereview app.</p>
<form action="{%url codereview.views.new%}"
method="post" enctype="multipart/form-data" id="create-form">
<input type="hidden" name="xsrf_token" value="{{xsrf_token}}">
<table>
{{form}}
<tr><td><input type="submit" value="Create Issue"></td></tr>
</table>
</form>
<a id="resizer" style="display:none;cursor:pointer">
<img src="{{media_url}}zippyplus.gif"></a>
<script language="JavaScript" type="text/javascript"><!--
M_addTextResizer_(document.getElementById("create-form"));
--></script>
{%endblock%}

152
templates/patch.html Normal file
View File

@ -0,0 +1,152 @@
{%extends "issue_base.html"%}
{%block body%}
<script language="JavaScript" type="text/javascript"><!--
document.onkeydown = M_keyDown;
{%if user%}
logged_in = true;
{%else%}
logged_in = false;
login_warned = false;
{%endif%}
// -->
</script>
{%if user%}
<!-- Form used by in-line comment JS; XXX filled in by JS code -->
<form id="dainlineform" style="display: none;"
action="{%url codereview.views.inline_draft%}" method="post">
<div class="comment-border">
<input type="hidden" name="snapshot" value="XXX">
<input type="hidden" name="lineno" value="XXX">
<input type="hidden" name="side" value="XXX">
<input type="hidden" name="issue" value="{{issue.key.id}}">
<input type="hidden" name="patchset" value="{{patchset.key.id}}">
<input type="hidden" name="patch" value="{{patch.key.id}}">
<textarea name="text" cols="60" rows="5"></textarea><br>
<input type="submit" name="save" value="Save"
onclick="return M_submitInlineComment(this.form);">
<input type="reset" name="cancel" value="Cancel"
onclick="M_removeTempInlineComment(this.form)">
</div>
<div class="comment-border" style="padding: 0pt;"></div>
</form>
<a id="resizer" style="display:none;cursor:pointer"><img src="{{media_url}}zippyplus.gif"></a>
{%endif%}
<div style="float: left;">
<h2 style="margin-bottom: 0em; margin-top: 0em;">Unified Diff: {{patch.filename}}</h2>
{%ifnotequal patch.nav_type "patch"%}
<span style="color:red">Side-by-side diff isn't available for this file because of its large size.</span>
{%endifnotequal%}
<div style="margin-top: .2em;">{%include "issue_star.html"%}
<b>Issue <a href="{%url codereview.views.show issue.key.id%}" onmouseover="M_showPopUp(this, 'popup-issue');" id="upCL">{{issue.key.id}}</a>:</b>
{{issue.subject}} {%if issue.closed %} (Closed) {%endif%}
{%if issue.base%}<span class="extra">Base URL: {{issue.base}}</span>{%endif%}</div>
<div style="margin-top: .4em;">
<b>Patch Set: {%if patchset.message%}{{patchset.message}}{%endif%}</b>
<span class="extra">
Created {{patchset.created|timesince}} ago
{%if patchset.url%},
Downloaded from: <a href="{{patchset.url}}">{{patchset.url}}</a>
{%endif%}
</span>
</div>
<div style="margin-top: .4em;" class="help">
Use n/p to move between diff chunks;
N/P to move between comments.
{%if user%}
Double-click a line to add a draft in-line comment.
<br><span style="color:red">Draft comments are only viewable by you;</span>
use <a href="{%url codereview.views.publish issue.key.id%}" class="novisit">Publish+Mail Comments</a> ('m') to let others view them.
{%else%}
Please Sign in to add in-line comments.
{%endif%}
</div>
</div>
{%if column_width and context%}
<input type="hidden" id="id_context" value="{{context}}" />
<input type="hidden" id="id_column_width" value="{{column_width}}" />
{%endif%}
<div style="float: right; color: #333333; background-color: #eeeeec; border: 1px solid lightgray; -moz-border-radius: 5px 5px 5px 5px; padding: 5px;">
<div>
Jump to: <select onchange="M_jumpToPatch(this, {{issue.key.id}}, {{patchset.key.id}}, true);">
{% for jump_patch in patchset.patch_set %}
<option value="{{jump_patch.key.id}}"
{%ifequal jump_patch.key.id patch.key.id%} selected="selected"{%endifequal%}>{{jump_patch.filename}}</option>
{% endfor %}
</select>
</div>
{%if not patch.no_base_file%}
<div style="margin-top: 5px;">
<a href="{%url codereview.views.diff issue.key.id,patchset.key.id,patch.filename%}{%urlappend_view_settings%}">
View side-by-side diff with in-line comments</a>
</div>
{%endif%}
<div style="margin-top: 5px;">
<a href="{%url codereview.views.download_patch issue.key.id,patchset.key.id,patch.key.id%}"
title="Download patch for {{patch.filename}}">
Download patch
</a>
</div>
{%if user%}
<div style="margin-top: 5px;">
<a class="novisit" href="{%url codereview.views.publish issue.key.id%}">Publish+Mail
Comments</a> ('m')
|
<a class="novisit" href="javascript:draftMessage.dialog_show()">Edit draft message</a> ('M')
</div>
{%endif%}
</div>
<div style="clear: both;"></div>
<div class="code" style="margin-top: 1.3em; display: table; margin-left: auto; margin-right: auto;">
<div class="codenav">
{%include "file_navigation.html"%} <br/>
<a href="javascript:M_expandAllInlineComments()">Expand Comments</a> ('e')
|
<a href="javascript:M_collapseAllInlineComments()">Collapse Comments</a> ('c')
|
<a name="show-all-inline"
style="display:none"
href="javascript:M_showAllInlineComments()">Show Comments</a>
<a name="hide-all-inline"
href="javascript:M_hideAllInlineComments()">Hide Comments</a> ('s')
</div>
<div style="position:relative" id="table-top">
{%if patch.is_binary%}
<img src="{%url codereview.views.image issue.key.id,patchset.key.id,patch.key.id,1%}" />
{%else%}
<span id="hook-sel" style="display:none;"></span>
<table style="padding: 5px;" cellpadding="0" cellspacing="0" id="thecode"
ondblclick="M_handleTableDblClick(event)"
ontouchstart="M_handleTableTouchStart(event)"
ontouchend="M_handleTableTouchEnd(event)">
{%for row in rows%}{{row|safe}}{%endfor%}
</table>
{%endif%}
</div>
<div class="codenav">
{%include "file_navigation.html"%}
</div>
</div>
<script language="JavaScript" type="text/javascript"><!--
var old_snapshot = "old";
var new_snapshot = "new";
var intraLineDiff = new M_IntraLineDiff();
var hookState = new M_HookState(window);
hookState.updateHooks();
// -->
</script>
{%include "draft_message.html"%}
{%endblock%}

91
templates/patchset.html Normal file
View File

@ -0,0 +1,91 @@
{%if patchset.url%}
<div>
Downloaded from: <a href="{{patchset.url}}">{{patchset.url}}</a>
</div>
{%endif%}
<div class="issue-list">
<div class="pagination">
<div style="float: left;">
<i>Created:</i> {{patchset.created|timesince}} ago
</div>
<div style="float: right;">
{%if patchset.data%}
<a href="{%url codereview.views.download issue.key.id,patchset.key.id%}">
Download raw patch set</a>
{% else %}
<span class="disabled">(Patch set is too large to download)</span>
{% endif %}
{%ifnotequal 1 num_patchsets%}
{%if is_editor %}
<form method="post" action="{%url codereview.views.delete_patchset issue.key.id patchset.key.id %}">
<input type="hidden" name="xsrf_token" value="{{xsrf_token}}">
<input type="hidden" name="_method" value="delete" />
|<input onclick="return confirm('Are you sure?');"
type="submit" value="Delete patch set" class="link-to"/>
</form>
{%endif%}
{%endifnotequal%}
</div>
<div style="clear:both;"></div>
</div>
<table id="queues" style="clear:both;">
<tr align="left">
<th colspan="2"></th>
<th>Unified diffs</th>
<th>Side-by-side diffs</th>
<th>Delta from patch set</th>
<th colspan="3">Stats</th>
<th>Patch</th>
</tr>
{%for patch in patchset.patches%}
<tr name="patch">
<td class="first" width="14"><img src="{{media_url}}closedtriangle.gif"
style="visibility: hidden;" width="12" height="9" /></td>
<td style="white-space: nowrap">{%if patch.status%}{{patch.status}}{%endif%}</td>
<td>
<a class="noul"
href="{%url codereview.views.patch issue.key.id,patchset.key.id,patch.key.id%}">
{{patch.filename}}
</a>
</td>
<td>
<a class="noul"
href="{%url codereview.views.diff issue.key.id,patchset.key.id,patch.filename%}">
View
</a>
</td>
<td style="white-space: nowrap">
{%for delta in patch.parsed_deltas%}
<a href="{%url codereview.views.diff2 issue.key.id,delta.1,patchset.key.id,patch.filename%}"
title="Delta from patch set {{delta.0}}">{{delta.0}}</a>
{%endfor%}
</td>
<td style="white-space: nowrap">{{patch.num_chunks}} chunk{{patch.num_chunks|pluralize}}</td>
<td style="white-space: nowrap">+{{patch.num_added}} line{{patch.num_added|pluralize}}, -{{patch.num_removed}} line{{patch.num_removed|pluralize}}</td>
<td style="white-space: nowrap">
{%if patch.num_comments or patch.num_drafts%}<b>{%endif%}
{{patch.num_comments}} comment{{patch.num_comments|pluralize}}
{%if patch.num_my_comments%}
({{patch.num_my_comments}} by me)
{%endif%}
{%if patch.num_drafts%}
<span style="color:red">+
{{patch.num_drafts}} draft{{patch.num_drafts|pluralize}}
</span>
{%endif%}
{%if patch.num_comments or patch.num_drafts%}</b>{%endif%}
</td>
<td>
<a href="{%url codereview.views.download_patch issue.key.id,patchset.key.id,patch.key.id%}"
title="Download patch for {{patch.filename}}">
Download
</a>
</td>
</tr>
{%endfor%}
</table>
</div>

59
templates/publish.html Normal file
View File

@ -0,0 +1,59 @@
{%extends "issue_base.html"%}
{%block title1%}Publish+Mail -{%endblock%}
{%block head%}{{form.media}}{%endblock%}
{%block issue_body%}
<script><!--
function discard(btn) {
draftMessage.discard();
btn.disabled = "disabled";
document.getElementById("id_message").value = "";
return false;
}
draftMessage = new M_draftMessage({{issue.key.id}}, true);
// -->
</script>
<h2>Publish + Mail Draft Comments</h2>
<div>
<form action="{%url codereview.views.publish issue.key.id%}"
method="post" id="publish-form">
<input type="hidden" name="xsrf_token" value="{{xsrf_token}}">
<table>
{%ifnotequal user issue.owner%}
<tr><th>Subject:</th><td>{{issue.subject}}</td></tr>
{%endifnotequal%}
{{form}}
<tr>
<td></td>
<td><i>The message will be included in the email sent (if any).</i></td>
</tr>
<tr>
<td></td>
<td>
<input type="submit" value="Publish All My Drafts" />
{%if draft_message%}
<input type="button" onclick="return discard(this);"
value="Discard Message" />
{%endif%}
</td>
</tr>
</table>
</form>
<a id="resizer" style="display:none;cursor:pointer">
<img src="{{media_url}}zippyplus.gif"></a>
<script language="JavaScript" type="text/javascript"><!--
M_addTextResizer_(document.getElementById("publish-form"));
document.getElementById("id_message").focus();
--></script>
</div>
{%if preview%}
<div style="margin-top: 3em;">
<h3>Unpublished Drafts:</h3>
<pre class="description">{{preview|wordwrap:"80"|urlizetrunc:80}}</pre>
</div>
{%endif%}
<div style="clear:both"></div>
{%endblock%}

14
templates/repo_new.html Normal file
View File

@ -0,0 +1,14 @@
{%extends "repos_base.html"%}
{%block title1%}Add Repository -{%endblock%}
{%block body%}
<h2>Create New Repository</h2>
<form action="{%url codereview.views.repo_new%}" method="post">
<input type="hidden" name="xsrf_token" value="{{xsrf_token}}">
<table>
{{form}}
<tr><td><input type="submit" value="Create Repository"></td></tr>
</table>
</form>
{%endblock%}

31
templates/repos.html Normal file
View File

@ -0,0 +1,31 @@
{%extends "repos_base.html"%}
{%block title1%}Repositories -{%endblock%}
{%block body%}
<h2>Repositories and Branches</h2>
<div id="repoview">
{%regroup branches by repository as repo_list%}
{%for repo in repo_list%}
<div class="header"><span>{{repo.grouper.name}}</span>
{%if user%}
(<a href="{%url codereview.views.branch_new repo.grouper.key.id%}">add branch</a>)
{%endif%}
<a name="{{repo.grouper.name|slugify}}" class="anchor" href="#{{repo.grouper.name|slugify}}">&para;</a>
</div>
<table>
{%for br in repo.list%}
<tr><td style="{{br.category|slugify}}">{{br.category}}</td><td>{{br.name}}</td><td>{{br.url}}</td>
<td>
{%ifequal br.owner user%}
(<a href="{%url codereview.views.branch_edit br.key.id%}">edit</a>)
{%endifequal%}
</td>
<td>(<a title="search for issues"
href="{%url codereview.views.search%}?base={{br.url}}">search</a>)</td>
</tr>
{%endfor%}
</table>
{%endfor%}
</div>
{%endblock%}

24
templates/repos_base.html Normal file
View File

@ -0,0 +1,24 @@
{%extends "base.html"%}
{%block mainmenu%}
<a href="{%url codereview.views.index%}">Issues</a>
<a href="{%url codereview.views.repos%}" class="active">Repositories</a>
<a href="{%url codereview.views.search%}">Search</a>
{%endblock%}
{%block mainmenu2%}
<a href="{%url codereview.views.repos%}">Repositories and Branches</a>
|
{%if user%}
<a href="{%url codereview.views.repo_new%}">Add Repository</a>
{%else%}
<a class="novisit" href="{{sign_in}}">Sign in</a>
with your <a href="https://www.google.com/accounts/NewAccount">Google
Account</a> to add repositories.
{%endif%}
{%if is_admin%}
|
<a href="{%url codereview.views.repo_init%}">Initialize Repositories</a>
{%endif%}
{%endblock%}
{%block body%}BODY GOES HERE{%endblock%}

19
templates/search.html Normal file
View File

@ -0,0 +1,19 @@
{%extends "base.html"%}
{%block mainmenu%}
<a href="{%url codereview.views.index%}">Issues</a>
<a href="{%url codereview.views.repos%}">Repositories</a>
<a href="{%url codereview.views.search%}" class="active">Search</a>
{%endblock%}
{%block title1%}Search -{%endblock%}
{%block head%}{{form.media}}{%endblock%}
{%block body%}
<h2>Search</h2>
<form action="{%url codereview.views.search%}" method="get">
<table>
{{form}}
<tr><td><input type="submit" value="Search"></td></tr>
</table>
</form>
{%endblock%}

View File

@ -0,0 +1,7 @@
{%extends "issue_pagination.html"%}
{%block mainmenu%}
<a href="{%url codereview.views.index%}">Issues</a>
<a href="{%url codereview.views.repos%}">Repositories</a>
<a href="{%url codereview.views.search%}" class="active">Search</a>
{%endblock%}
{%block subtitle%}Results{%endblock%}

30
templates/settings.html Normal file
View File

@ -0,0 +1,30 @@
{%extends "settings_base.html"%}
{%block title1%}Settings -{%endblock%}
{%block body%}
<h2>Settings for {%nickname user True%}</h2>
{%if chat_status%}
<p>Chat status: {{chat_status}}</p>
{%endif%}
<p>You can change your nickname. This affects how your name is
displayed to other users.</p>
<form action="{%url codereview.views.settings%}" method="post">
<input type="hidden" name="xsrf_token" value="{{xsrf_token}}">
<table>
{{form}}
<tr><td><input type="submit" value="Update Settings"></td></tr>
</table>
</form>
<p>&nbsp;</p>
<h2>Delete This Account</h2>
<form action="{%url codereview.views.account_delete%}" method="post">
<input type="hidden" name="xsrf_token" value="{{xsrf_token}}">
<input type="submit" value="Delete Account"
onClick="return confirm('Are you sure you want to delete this account?')">
There is no undo!
</form>
{%endblock%}

View File

@ -0,0 +1,7 @@
{%extends "base.html"%}
{%block mainmenu2%}
<a href="{%url codereview.views.settings%}">Settings</a>
|
<a href="{%url codereview.views.migrate_entities%}">Migrate issues and repositories</a>
{%endblock%}

27
templates/starred.html Normal file
View File

@ -0,0 +1,27 @@
{%extends "issue_base.html"%}
{%block title1%}Starred Issues -{%endblock%}
{%block body%}
<script language="JavaScript" type="text/javascript"><!--
document.onkeydown = M_dashboardKeyDown;
-->
</script>
<h2>Starred Issues</h2>
<div class="issue-list">
<table id="queues">
{%if not issues%}
<tr><td colspan="9"><span class="disabled">(None)</span></td></tr>
{%else%}
{%include "issue_heading.html"%}
{%for issue in issues%}
{%include "issue_row.html"%}
{%endfor%}
{%endif%}
</table>
</div>
<script language="JavaScript" type="text/javascript"><!--
var dashboardState = new M_DashboardState(window,'issue');
-->
</script>
{%endblock%}

View File

@ -0,0 +1,33 @@
{%extends "issue_base.html"%}
{%block body%}
<h1>Tired of uploading files through the form?</h1>
<p>Download <a href="{%url codereview.views.customized_upload_py%}">upload.py</a>, a simple tool for
uploading diffs from a version control system to the codereview app.</p>
<p><strong>Usage summary:</strong>
<pre>upload.py [options] [-- diff_options]</pre></p>
<p>Diff options are passed to the diff command of the underlying system.</p>
<p><strong>Supported version control systems:</strong></p>
<ul>
<li>Subversion</li>
<li>Git</li>
<li>Mercurial</li>
</ul>
<p><a href="http://code.google.com/p/rietveld/wiki/UploadPyUsage"
target="_blank">Read more</a> about this script.</p>
<div style="margin-top: 1.5em;">
<form method="POST" action="{%url codereview.views.use_uploadpy%}">
<p>
<input type="hidden" name="xsrf_token" value="{{xsrf_token}}">
<input name="disable_msg" value="1" id="disable_msg" type="checkbox" />
<label for="disable_msg">Don't show this message again</label></p>
<input type="submit" name="download" value="Download upload.py" />
<input type="submit" name="create" value="Go to the Create Issue Form" />
</form>
</div>
{%endblock%}

105
templates/user.html Normal file
View File

@ -0,0 +1,105 @@
{%extends "issue_base.html"%}
{%block title1%}Issues for {{account.nickname}} -{%endblock%}
{%block body%}
<script language="JavaScript" type="text/javascript"><!--
document.onkeydown = M_dashboardKeyDown;
-->
</script>
<h2 style="display:inline">Issues for {{account.nickname}}</h2>
{%if show_block and account.blocked %}<a href="{%url codereview.views.block_user account.email%}">Unblock</a>{%endif%}
{%if show_block and not account.blocked %}<a href="{%url codereview.views.block_user account.email%}">Block</a>{%endif%}
<p>
<div class="issue-list">
<table id="queues">
{%if draft_issues%}
<tr>
<td colspan="9" class="header">
<h3>Issues with drafts by me</h3>
</td>
</tr>
{%include "issue_heading.html"%}
{%for issue in draft_issues%}
{%include "issue_row.html"%}
{%endfor%}
{%endif%}
<tr>
<td colspan="9" class="header">
<h3>Created by {%nickname email%} ({{my_issues|length}})</h3>
</td>
</tr>
{%if not my_issues%}
<tr>
<td colspan="9" class="first last">
<span class="disabled">(None)</span>
</td>
</tr>
{%else%}
{%include "issue_heading.html"%}
{%for issue in my_issues%}
{%include "issue_row.html"%}
{%endfor%}
{%endif%}
<tr>
<td colspan="9" class="header">
<h3>Reviewable by {%nickname email%} ({{review_issues|length}})</h3>
</td>
</tr>
{%if not review_issues%}
<tr>
<td colspan="9" class="first last">
<span class="disabled">(None)</span>
</td>
</tr>
{%else%}
{%include "issue_heading.html"%}
{%for issue in review_issues%}
{%include "issue_row.html"%}
{%endfor%}
{%endif%}
<tr>
<td colspan="9" class="header">
<h3>Issues CCd to {%nickname email%} ({{cc_issues|length}})</h3>
</td>
</tr>
{%if not cc_issues%}
<tr>
<td colspan="9" class="first last">
<span class="disabled">(None)</span>
</td>
</tr>
{%else%}
{%include "issue_heading.html"%}
{%for issue in cc_issues%}
{%include "issue_row.html"%}
{%endfor%}
{%endif%}
<tr>
<td colspan="9" class="header">
<h3>Closed Recently ({{closed_issues|length}})</h3>
</td>
</tr>
{%if not closed_issues%}
<tr>
<td colspan="9" class="first last">
<span class="disabled">(None)</span>
</td>
</tr>
{%else%}
{%include "issue_heading.html"%}
{%for issue in closed_issues%}
{%include "issue_row.html"%}
{%endfor%}
{%endif%}
</table>
</div>
<script language="JavaScript" type="text/javascript"><!--
var dashboardState = new M_DashboardState(window,'issue', 'M_myDashboardIssueMarker');
-->
</script>
{%endblock%}

Some files were not shown because too many files have changed in this diff Show More