Start Rietveld report taken from code.google.com/p/rietveld with the revision a2d5c409a0ee
|
@ -0,0 +1,12 @@
|
|||
.*\.py[co]$
|
||||
.*\.pyc-2.4$
|
||||
.*~$
|
||||
.*\.orig$
|
||||
.*\#.*$
|
||||
.*@.*$
|
||||
index\.yaml$
|
||||
REVISION$
|
||||
.coverage$
|
||||
htmlcov$
|
||||
.DS_Store$
|
||||
workspace.xml$
|
|
@ -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.
|
|
@ -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/*"
|
|
@ -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.
|
|
@ -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,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
|
|
@ -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,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')
|
||||
|
||||
|
|
@ -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."""
|
|
@ -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
|
|
@ -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\">»</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
|
|
@ -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¶m2'
|
||||
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
|
|
@ -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()))
|
|
@ -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()
|
|
@ -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
|
|
@ -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
|
|
@ -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}),
|
||||
)
|
|
@ -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')
|
||||
|
|
@ -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()
|
|
@ -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
|
|
@ -0,0 +1,6 @@
|
|||
queue:
|
||||
- name: deltacalculation
|
||||
rate: 5/s
|
||||
retry_parameters:
|
||||
task_retry_limit: 5
|
||||
task_age_limit: 1d
|
|
@ -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
|
After Width: | Height: | Size: 1.3 KiB |
|
@ -0,0 +1,4 @@
|
|||
jQuery Autocomplete plugin
|
||||
http://bassistance.de/jquery-plugins/jquery-plugin-autocomplete/
|
||||
Version: 1.0.2
|
||||
License: MIT/GPL
|
After Width: | Height: | Size: 1.5 KiB |
|
@ -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;
|
||||
}
|
|
@ -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);
|
|
@ -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);
|
|
@ -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);
|
|
@ -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;
|
||||
}
|
After Width: | Height: | Size: 631 B |
After Width: | Height: | Size: 173 B |
After Width: | Height: | Size: 78 B |
After Width: | Height: | Size: 232 B |
After Width: | Height: | Size: 56 B |
|
@ -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/
|
After Width: | Height: | Size: 1.0 KiB |
After Width: | Height: | Size: 179 B |
After Width: | Height: | Size: 171 B |
|
@ -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;
|
||||
}
|
After Width: | Height: | Size: 200 B |
|
@ -0,0 +1 @@
|
|||
<h1>404 Not Found</h1>
|
|
@ -0,0 +1 @@
|
|||
<h1>500 Server Error</h1>
|
|
@ -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%}
|
|
@ -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"><Up></span> / <span class="letter"><Down></span> <b>:</b></td><td>next / previous line</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="shortcut"><span class="letter"><Enter></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"><Enter></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> </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"><Enter></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> </td></tr>
|
||||
<tr>
|
||||
<td></td><th>Comment/message editing</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="shortcut"><span class="letter"><Ctrl></span> + <span class="letter">s</span> <b>:</b></td><td>save comment</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="shortcut"><span class="letter"><Esc></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>
|
|
@ -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%}
|
|
@ -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%}
|
|
@ -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%}
|
|
@ -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%}
|
|
@ -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%}">
|
||||
« {{patch_right.prev_with_comment.filename}}</a> ('K'){%else%}
|
||||
<span class="disabled">« 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%}">
|
||||
« {{patch_right.prev.filename}}</a> ('k'){%else%}
|
||||
<span class="disabled">« 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}} »</a> ('j'){%else%}
|
||||
<span class="disabled">no next file »</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}} »</a> ('J'){%else%}
|
||||
<span class="disabled">no next file with change/comment »</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%}">
|
||||
« {{patch_right.prev.filename}}</a> ('k'){%else%}
|
||||
<span class="disabled">« 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}} »</a> ('j'){%else%}
|
||||
<span class="disabled">no next file »</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%}
|
|
@ -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%}
|
|
@ -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>
|
|
@ -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%}
|
|
@ -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%}
|
|
@ -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>
|
|
@ -0,0 +1 @@
|
|||
{%extends "feeds/template_description.html"%}
|
|
@ -0,0 +1 @@
|
|||
{%extends "feeds/template_title.html"%}
|
|
@ -0,0 +1 @@
|
|||
{%extends "feeds/template_description.html"%}
|
|
@ -0,0 +1 @@
|
|||
{%extends "feeds/template_title.html"%}
|
|
@ -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%}
|
|
@ -0,0 +1,5 @@
|
|||
{%if obj.owner%}
|
||||
PatchSet : {{obj.message}}
|
||||
{%else%}
|
||||
Message from {%if obj.sender%}{{obj.sender}}{%else%}unknown{%endif%}
|
||||
{%endif%}
|
|
@ -0,0 +1 @@
|
|||
{%extends "feeds/template_description.html"%}
|
|
@ -0,0 +1 @@
|
|||
{%extends "feeds/template_title.html"%}
|
|
@ -0,0 +1 @@
|
|||
{%extends "feeds/template_description.html"%}
|
|
@ -0,0 +1 @@
|
|||
{%extends "feeds/template_title.html"%}
|
|
@ -0,0 +1,5 @@
|
|||
{%if obj.description%}
|
||||
{{obj.description}}
|
||||
{%else%}
|
||||
(No description)
|
||||
{%endif%}
|
|
@ -0,0 +1 @@
|
|||
{{obj.subject}}
|
|
@ -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%}">
|
||||
« {{patch.prev_with_comment.filename}}</a> ('K'){%else%}
|
||||
<span class="disabled">« 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%}">
|
||||
« {{patch.prev.filename}}</a> ('k'){%else%}
|
||||
<span class="disabled">« 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}} »</a> ('j'){%else%}
|
||||
<span class="disabled">no next file »</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}} »</a> ('J'){%else%}
|
||||
<span class="disabled">no next file with comments »</span>{%endif%}
|
|
@ -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"%} <a target="_blank" href="http://bugbotexp{{file.depot_path|escape}}"><b>» Suppress</b></a>{%endifequal%}
|
||||
{%endif%}
|
||||
{%endif%}
|
||||
</div>
|
||||
</div>
|
||||
{%endfor%}
|
||||
<div class="comment-border" style="padding: 0"></div>
|
|
@ -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%}
|
|
@ -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%}
|
||||
|
||||
<a href="{%url codereview.views.mine%}">My Issues</a>
|
||||
|
|
||||
<a href="{%url codereview.views.starred%}">Starred</a>
|
||||
|
||||
<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%}
|
|
@ -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>
|
|
@ -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}}">« Newest</a>
|
||||
{%endif%}
|
||||
{%if prev%}
|
||||
<a class="novisit" href="{{prev}}">‹ Newer</a>
|
||||
{%endif%}
|
||||
<b>{{first}}{%if last%} - {{last}}{%endif%}</b>
|
||||
{%if next%}<a class="novisit" href="{{next}}">{{nexttext}} ›</a>
|
||||
{%else%}<span style="color:gray">{{nexttext}} ›</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}}">« Newest</a>
|
||||
{%endif%}
|
||||
{%if prev%}
|
||||
<a class="novisit" href="{{prev}}">‹ Newer</a>
|
||||
{%endif%}
|
||||
<b>{{first}}{%if last%} - {{last}}{%endif%}</b>
|
||||
{%if next%}<a class="novisit" href="{{next}}">{{nexttext}} ›</a>
|
||||
{%else%}<span style="color:gray">{{nexttext}} ›</span>{%endif%}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script language="JavaScript" type="text/javascript"><!--
|
||||
var dashboardState = new M_DashboardState(window,'issue');
|
||||
-->
|
||||
</script>
|
||||
{%endblock%}
|
|
@ -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>
|
|
@ -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>
|
|
@ -0,0 +1,5 @@
|
|||
{%autoescape off%}{%if message%}{{message|wordwrap:"72"}}
|
||||
|
||||
{%endif%}{%if details%}{{details|wordwrap:"72"}}
|
||||
|
||||
{%endif%}{{url}}{%endautoescape%}
|
|
@ -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%}
|
|
@ -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%}
|
|
@ -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%}
|
|
@ -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%}
|
|
@ -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>
|
|
@ -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%}
|
|
@ -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%}
|
|
@ -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}}">¶</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%}
|
|
@ -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%}
|
|
@ -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%}
|
|
@ -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%}
|
|
@ -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> </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%}
|
|
@ -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%}
|
|
@ -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%}
|
|
@ -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%}
|
|
@ -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%}
|