├── screenshots
└── 0.png
├── code_comments
├── templates
│ ├── js
│ │ ├── top-comments-block.html
│ │ ├── comments-for-a-line.html
│ │ ├── add-comment-dialog.html
│ │ ├── line-comment.html
│ │ └── comment.html
│ ├── delete.html
│ └── comments.html
├── htdocs
│ ├── jquery-ui
│ │ ├── images
│ │ │ ├── ui-icons_4b954f_256x240.png
│ │ │ ├── ui-icons_505050_256x240.png
│ │ │ ├── ui-icons_9b081d_256x240.png
│ │ │ ├── ui-icons_b00000_256x240.png
│ │ │ ├── ui-icons_d7d7d7_256x240.png
│ │ │ ├── ui-icons_ffffff_256x240.png
│ │ │ ├── ui-bg_flat_0_000000_40x100.png
│ │ │ ├── ui-bg_flat_0_333333_40x100.png
│ │ │ ├── ui-bg_flat_00_ffffff_40x100.png
│ │ │ ├── ui-bg_glass_55_c0f0c0_1x400.png
│ │ │ ├── ui-bg_highlight-hard_0_b00000_1x100.png
│ │ │ ├── ui-bg_highlight-hard_0_e0e0e0_1x100.png
│ │ │ ├── ui-bg_highlight-soft_30_ffffdd_1x100.png
│ │ │ ├── ui-bg_diagonals-thick_20_666666_40x40.png
│ │ │ └── ui-bg_diagonals-thick_75_ffddcc_40x40.png
│ │ └── trac-theme.css
│ ├── jquery.ba-throttle-debounce.min.js
│ ├── code-comments.css
│ ├── code-comments.js
│ ├── underscore-min.js
│ ├── backbone-min.js
│ ├── json2.js
│ └── backbone.js
├── __init__.py
├── comment_macro.py
├── ticket_event_listener.py
├── db.py
├── comments.py
├── comment.py
└── web.py
├── CHANGELOG
├── setup.py
├── .gitignore
├── HACKING
└── README.md
/screenshots/0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sagemath/trac-code-comments-plugin/master/screenshots/0.png
--------------------------------------------------------------------------------
/code_comments/templates/js/top-comments-block.html:
--------------------------------------------------------------------------------
1 |
Comments
2 |
3 |
--------------------------------------------------------------------------------
/code_comments/templates/js/comments-for-a-line.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | |
4 |
5 |
6 |
7 | |
--------------------------------------------------------------------------------
/code_comments/htdocs/jquery-ui/images/ui-icons_4b954f_256x240.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sagemath/trac-code-comments-plugin/master/code_comments/htdocs/jquery-ui/images/ui-icons_4b954f_256x240.png
--------------------------------------------------------------------------------
/code_comments/htdocs/jquery-ui/images/ui-icons_505050_256x240.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sagemath/trac-code-comments-plugin/master/code_comments/htdocs/jquery-ui/images/ui-icons_505050_256x240.png
--------------------------------------------------------------------------------
/code_comments/htdocs/jquery-ui/images/ui-icons_9b081d_256x240.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sagemath/trac-code-comments-plugin/master/code_comments/htdocs/jquery-ui/images/ui-icons_9b081d_256x240.png
--------------------------------------------------------------------------------
/code_comments/htdocs/jquery-ui/images/ui-icons_b00000_256x240.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sagemath/trac-code-comments-plugin/master/code_comments/htdocs/jquery-ui/images/ui-icons_b00000_256x240.png
--------------------------------------------------------------------------------
/code_comments/htdocs/jquery-ui/images/ui-icons_d7d7d7_256x240.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sagemath/trac-code-comments-plugin/master/code_comments/htdocs/jquery-ui/images/ui-icons_d7d7d7_256x240.png
--------------------------------------------------------------------------------
/code_comments/htdocs/jquery-ui/images/ui-icons_ffffff_256x240.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sagemath/trac-code-comments-plugin/master/code_comments/htdocs/jquery-ui/images/ui-icons_ffffff_256x240.png
--------------------------------------------------------------------------------
/code_comments/htdocs/jquery-ui/images/ui-bg_flat_0_000000_40x100.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sagemath/trac-code-comments-plugin/master/code_comments/htdocs/jquery-ui/images/ui-bg_flat_0_000000_40x100.png
--------------------------------------------------------------------------------
/code_comments/htdocs/jquery-ui/images/ui-bg_flat_0_333333_40x100.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sagemath/trac-code-comments-plugin/master/code_comments/htdocs/jquery-ui/images/ui-bg_flat_0_333333_40x100.png
--------------------------------------------------------------------------------
/code_comments/htdocs/jquery-ui/images/ui-bg_flat_00_ffffff_40x100.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sagemath/trac-code-comments-plugin/master/code_comments/htdocs/jquery-ui/images/ui-bg_flat_00_ffffff_40x100.png
--------------------------------------------------------------------------------
/code_comments/htdocs/jquery-ui/images/ui-bg_glass_55_c0f0c0_1x400.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sagemath/trac-code-comments-plugin/master/code_comments/htdocs/jquery-ui/images/ui-bg_glass_55_c0f0c0_1x400.png
--------------------------------------------------------------------------------
/code_comments/htdocs/jquery-ui/images/ui-bg_highlight-hard_0_b00000_1x100.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sagemath/trac-code-comments-plugin/master/code_comments/htdocs/jquery-ui/images/ui-bg_highlight-hard_0_b00000_1x100.png
--------------------------------------------------------------------------------
/code_comments/htdocs/jquery-ui/images/ui-bg_highlight-hard_0_e0e0e0_1x100.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sagemath/trac-code-comments-plugin/master/code_comments/htdocs/jquery-ui/images/ui-bg_highlight-hard_0_e0e0e0_1x100.png
--------------------------------------------------------------------------------
/code_comments/htdocs/jquery-ui/images/ui-bg_highlight-soft_30_ffffdd_1x100.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sagemath/trac-code-comments-plugin/master/code_comments/htdocs/jquery-ui/images/ui-bg_highlight-soft_30_ffffdd_1x100.png
--------------------------------------------------------------------------------
/code_comments/htdocs/jquery-ui/images/ui-bg_diagonals-thick_20_666666_40x40.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sagemath/trac-code-comments-plugin/master/code_comments/htdocs/jquery-ui/images/ui-bg_diagonals-thick_20_666666_40x40.png
--------------------------------------------------------------------------------
/code_comments/htdocs/jquery-ui/images/ui-bg_diagonals-thick_75_ffddcc_40x40.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sagemath/trac-code-comments-plugin/master/code_comments/htdocs/jquery-ui/images/ui-bg_diagonals-thick_75_ffddcc_40x40.png
--------------------------------------------------------------------------------
/CHANGELOG:
--------------------------------------------------------------------------------
1 | CHANGELOG
2 |
3 | 1.0.1 2012-02-13
4 | * Fix AJAX calls not working in sub-folder installs
5 | * Closed issues: http://git.io/zimpow
6 | * Props: https://github.com/mumpitzstuff
7 |
8 | 1.0.0 2012-01-31
9 | * Initial release
--------------------------------------------------------------------------------
/code_comments/__init__.py:
--------------------------------------------------------------------------------
1 | from code_comments import comment
2 | from code_comments import comments
3 | from code_comments import db
4 | from code_comments import web
5 | from code_comments import comment_macro
6 | from code_comments import ticket_event_listener
--------------------------------------------------------------------------------
/code_comments/templates/js/add-comment-dialog.html:
--------------------------------------------------------------------------------
1 |
2 | Preview
3 |
4 |
5 |
6 |
7 | Wiki Formatting
8 |
9 |
--------------------------------------------------------------------------------
/code_comments/templates/js/line-comment.html:
--------------------------------------------------------------------------------
1 |
2 | <%= html %>
3 |
4 | by <%= author %> @ <%= formatted_date %>
5 | •
6 | ∞
7 | <% if (can_delete) { %>
8 | •
9 | Delete
10 | <% } %>
11 |
12 | |
13 |
--------------------------------------------------------------------------------
/code_comments/templates/js/comment.html:
--------------------------------------------------------------------------------
1 |
11 |
12 | <%= html %>
13 |
14 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import find_packages, setup
2 |
3 | setup(
4 | name='TracCodeComments', version='1.0.1',
5 | author='Nikolay Bachiyski, Thorsten Ott',
6 | author_email='nikolay@automattic.com, tott@automattic.com',
7 | description='Tool for leaving inline code comments',
8 | packages=find_packages(exclude=['*.tests*']),
9 | entry_points = {
10 | 'trac.plugins': [
11 | 'code_comments = code_comments',
12 | ],
13 | },
14 | package_data = {'code_comments': ['templates/*.html', 'templates/js/*.html', 'htdocs/*.*','htdocs/jquery-ui/*.*', 'htdocs/jquery-ui/images/*.*',]},
15 | )
16 |
--------------------------------------------------------------------------------
/code_comments/htdocs/jquery.ba-throttle-debounce.min.js:
--------------------------------------------------------------------------------
1 | /*
2 | * jQuery throttle / debounce - v1.1 - 3/7/2010
3 | * http://benalman.com/projects/jquery-throttle-debounce-plugin/
4 | *
5 | * Copyright (c) 2010 "Cowboy" Ben Alman
6 | * Dual licensed under the MIT and GPL licenses.
7 | * http://benalman.com/about/license/
8 | */
9 | (function(b,c){var $=b.jQuery||b.Cowboy||(b.Cowboy={}),a;$.throttle=a=function(e,f,j,i){var h,d=0;if(typeof f!=="boolean"){i=j;j=f;f=c}function g(){var o=this,m=+new Date()-d,n=arguments;function l(){d=+new Date();j.apply(o,n)}function k(){h=c}if(i&&!h){l()}h&&clearTimeout(h);if(i===c&&m>e){l()}else{if(f!==true){h=setTimeout(i?k:l,i===c?e-m:e)}}}if($.guid){g.guid=j.guid=j.guid||$.guid++}return g};$.debounce=function(d,e,f){return f===c?a(d,e,false):a(d,f,e!==false)}})(this);
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | tracd.sh
2 |
3 | ################
4 | # Python ignores
5 | ################
6 | # stolen from https://github.com/github/gitignore/blob/master/Python.gitignore
7 | *.py[co]
8 |
9 | # Packages
10 | *.egg
11 | *.egg-info
12 | dist
13 | build
14 | eggs
15 | parts
16 | bin
17 | var
18 | sdist
19 | develop-eggs
20 | .installed.cfg
21 |
22 | # Installer logs
23 | pip-log.txt
24 |
25 | # Unit test / coverage reports
26 | .coverage
27 | .tox
28 |
29 | #Translations
30 | *.mo
31 |
32 | #Mr Developer
33 | .mr.developer.cfg
34 |
35 | #############
36 | # OSX Ignores
37 | #############
38 | # stolen from https://github.com/github/gitignore/blob/master/Global/OSX.gitignore
39 |
40 | .DS_Store
41 |
42 | # Thumbnails
43 | ._*
44 |
45 | # Files that might appear on external disk
46 | .Spotlight-V100
47 | .Trashes
48 |
--------------------------------------------------------------------------------
/code_comments/comment_macro.py:
--------------------------------------------------------------------------------
1 | from code_comments.comments import Comments
2 | from genshi.builder import tag
3 | from trac.wiki.macros import WikiMacroBase
4 |
5 | class CodeCommentLinkMacro(WikiMacroBase):
6 | """CodeCommentLink macro.
7 | This macro is used to embed a comment link in a ticket or wiki page:
8 | [[CodeCommentLink(5)]]
9 | where the number in the parentheses is the comment ID.
10 | """
11 |
12 | revision = "$Rev$"
13 | url = "$URL$"
14 | re = r'\[\[CodeCommentLink\((\d+)\)\]\]'
15 |
16 | def expand_macro(self, formatter, name, text, args):
17 | try:
18 | comment = Comments(formatter.req, formatter.env).by_id(text)
19 | return tag.a(comment.link_text(), href=comment.href())
20 | except:
21 | return ''
--------------------------------------------------------------------------------
/code_comments/templates/delete.html:
--------------------------------------------------------------------------------
1 |
4 |
7 |
8 |
9 | Code Comments - Delete Comment
10 |
11 |
12 |
13 |
14 |
Delete Comment
15 |
Do you want to delete this comment:
16 |
17 | - ID
18 | - $comment.id
19 |
20 | - Author
21 | - $comment.author
22 |
23 | - Time
24 | - ${comment.formatted_date()}
25 |
26 | - Text
27 | - $comment.html
28 |
29 | - Path
30 | - ${comment.path_link_tag()}
31 |
32 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/code_comments/htdocs/code-comments.css:
--------------------------------------------------------------------------------
1 | table.code-comments td.check {
2 | width: 2em;
3 | }
4 | #add-comment-dialog textarea {
5 | width: 400px;
6 | height: 180px;
7 | padding: 6px;
8 | }
9 | #add-comment-dialog button {
10 | float: left;
11 | }
12 | #add-comment-dialog a.formatting {
13 | float: right;
14 | }
15 | #add-comment-dialog h3 {
16 | display: none;
17 | }
18 | #add-comment-dialog div.preview {
19 | margin-bottom: 12px;
20 | }
21 |
22 | .ui-dialog {
23 | width: auto !important;
24 | }
25 | ul.comments .active {
26 | background-color: #FFFFCD;
27 | }
28 | ul.comments {
29 | margin: 0;
30 | padding: 2px 4px 0 0;
31 | font-size: 11px;
32 | font-family: Verdana, sans-serif;
33 | }
34 | ul.comments>li {
35 | border-radius: 4px;
36 | border: 1px solid #dfdfdf;
37 | margin-bottom: 4px;
38 | list-style-type: none;
39 | }
40 | ul.comments>li .meta {
41 | padding: 6px;
42 | background-color: #dfdfdf;
43 | }
44 | ul.comments>li .time {
45 | font-size: 10px;
46 | }
47 | ul.comments>li .meta img.gravatar {
48 | vertical-align: middle;
49 | padding: 1px;
50 | border: 1px solid #888;
51 | }
52 | ul.comments>li .meta .author {
53 | font-weight: bold;
54 | }
55 | ul.comments>li .meta .delete {
56 | float: right;
57 | margin-right: 1em;
58 | }
59 |
60 | ul.comments>li .text {
61 | padding: 1px 8px;
62 | border: 0;
63 | margin: 0;
64 | }
65 | #top-comments button {
66 | margin-bottom: 16px;
67 | }
68 | tr.with-comments {
69 | background-color: #FFFFE2;
70 | border-bottom: 1px solid #998;
71 | }
72 | tr.comments {
73 | border-bottom: 1px solid #998;
74 | }
75 | table.code tr {
76 | height: 17px;
77 | }
78 | table.code tr:hover td {
79 | background-color: #FFFFCA;
80 | }
--------------------------------------------------------------------------------
/HACKING:
--------------------------------------------------------------------------------
1 | YOU WANT TO CONTRIBUTE BUT YOU DON'T KNOW HOW? NO MORE SUCH EXCUSES
2 |
3 | 0. Install Trac
4 |
5 | On Mac OS X this is as easy as:
6 |
7 | # easy_install Trac
8 |
9 | 1. Create a test Trac install somewhere:
10 |
11 | $ trac-admin initenv
12 |
13 | The default offered values are good.
14 |
15 | This will create a directory with the Trac install in the
16 | current directory.
17 |
18 | 2. Create a local SVN repo:
19 |
20 | $ svnadmin create
21 |
22 | 3. Checkout the repo and add a dummy file:
23 |
24 | $ svn co file:// # including leading slash, e.g. file:///home/username/svn-repo/
25 |
26 | 4. Add the repo in the trac.ini:
27 |
28 | $ $EDITOR /conf/trac.ini
29 |
30 | Change:
31 |
32 | repository_dir =
33 | repository_type = svn
34 |
35 | 5. Create htpasswd file for Trac auth:
36 |
37 | $ htpasswd -c
38 |
39 | Remember the file location.
40 |
41 | 6. Add yourself as admin:
42 |
43 | $ $ trac-admin permission add TRAC_ADMIN
44 |
45 | 6. Run trac server:
46 |
47 | $ tracd -s -r --port 8000 --basic-auth=',,' # see http://trac.edgewall.org/wiki/TracStandalone
48 |
49 | You should now have a working vanilla Trac at http://localhost:8000/, where you can log in and be an admin.
50 |
51 | 7. Checkout the plugin in R/W mode
52 |
53 | $ git clone git@github.com:Automattic/vip-trac-code-comments.git
54 |
55 | 8. Deploy the plugin in development mode
56 |
57 | $ python setup.py develop -mxd /plugins
58 |
59 | 9. Go to the Admin section in Trac, then Plugins and enable all the components of the TracCodeComments plugin and also the CodeComments macro
--------------------------------------------------------------------------------
/code_comments/ticket_event_listener.py:
--------------------------------------------------------------------------------
1 | from trac.core import *
2 | from trac.ticket.api import ITicketChangeListener
3 |
4 | import re
5 |
6 | from code_comments.comment_macro import CodeCommentLinkMacro
7 |
8 | class UpdateTicketCodeComments(Component):
9 | """Automatically stores relations to CodeComments whenever a ticket is saved or created
10 | Note: This does not catch edits on replies right away but on the next change of the ticket or when adding a new reply
11 | """
12 |
13 | implements(ITicketChangeListener)
14 |
15 | def ticket_created(self, ticket):
16 | self.update_relations(ticket)
17 |
18 | def ticket_changed(self, ticket, comment, author, old_values):
19 | self.update_relations(ticket)
20 |
21 | def ticket_deleted(self, ticket):
22 | self.update_relations(ticket)
23 |
24 | def update_relations(self, ticket):
25 | comment_ids = []
26 | # (time, author, field, oldvalue, newvalue, permanent)
27 | changes = ticket.get_changelog()
28 | description = ticket['description']
29 |
30 | comment_ids += re.findall(CodeCommentLinkMacro.re, description)
31 | if changes:
32 | for change in changes:
33 | if change[2] == 'comment':
34 | comment_ids += re.findall(CodeCommentLinkMacro.re, change[4])
35 |
36 | comment_ids = set(comment_ids)
37 | comment_ids_csv = ','.join(comment_ids)
38 |
39 | existing_comments_query = 'SELECT * FROM ticket_custom WHERE ticket = %s AND name = "code_comment_relation"'
40 | existing_comments = self.fetch(existing_comments_query, [ticket.id])
41 |
42 | if existing_comments:
43 | self.query('UPDATE ticket_custom SET value=%s WHERE ticket=%s AND name="code_comment_relation"', [comment_ids_csv, ticket.id])
44 | else:
45 | self.query('INSERT INTO ticket_custom (ticket, name, value) VALUES (%s, "code_comment_relation", %s)', [ticket.id, comment_ids_csv])
46 |
47 | def query(self, query, args = [], result_callback=None):
48 | if result_callback is None:
49 | result_callback = lambda db, cursor: True
50 | result = {}
51 | @self.env.with_transaction()
52 | def insert_comment(db):
53 | cursor = db.cursor()
54 | cursor.execute(query, args)
55 | result['result'] = result_callback(db, cursor)
56 | return result['result']
57 |
58 | def fetch(self, query, args = []):
59 | return self.query(query, args, lambda db, cursor: cursor.fetchall())
--------------------------------------------------------------------------------
/code_comments/db.py:
--------------------------------------------------------------------------------
1 | from trac.core import *
2 | from trac.db.schema import Table, Column, Index
3 | from trac.env import IEnvironmentSetupParticipant
4 | from trac.db.api import DatabaseManager
5 |
6 | # Database version identifier for upgrades.
7 | db_version = 1
8 |
9 | # Database schema
10 | schema = {
11 | 'code_comments': Table('code_comments', key=('id', 'version'))[
12 | Column('id', auto_increment=True),
13 | Column('version', type='int'),
14 | Column('text'),
15 | Column('path'),
16 | Column('revision', type='int'),
17 | Column('line', type='int'),
18 | Column('author'),
19 | Column('time', type='int'),
20 | Index(['path']),
21 | Index(['author']),
22 | ],
23 | }
24 |
25 | def to_sql(env, table):
26 | """ Convenience function to get the to_sql for the active connector."""
27 | dc = DatabaseManager(env)._get_connector()[0]
28 | return dc.to_sql(table)
29 |
30 | def create_tables(env, db):
31 | cursor = db.cursor()
32 | for table_name in schema:
33 | for stmt in to_sql(env, schema[table_name]):
34 | cursor.execute(stmt)
35 | cursor.execute("INSERT into system values ('code_comments_schema_version', %s)",
36 | str(db_version))
37 | # Upgrades
38 | def upgrade_from_1_to_2(env, db):
39 | pass
40 |
41 | upgrade_map = {
42 | 2: upgrade_from_1_to_2
43 | }
44 |
45 |
46 | class CodeCommentsSetup(Component):
47 | """Component that deals with database setup and upgrades."""
48 |
49 | implements(IEnvironmentSetupParticipant)
50 |
51 | def environment_created(self):
52 | """Called when a new Trac environment is created."""
53 | pass
54 |
55 | def environment_needs_upgrade(self, db):
56 | """Called when Trac checks whether the environment needs to be upgraded.
57 | Returns `True` if upgrade is needed, `False` otherwise."""
58 | return self._get_version(db) != db_version
59 |
60 | def upgrade_environment(self, db):
61 | """Actually perform an environment upgrade, but don't commit as
62 | that is done by the common upgrade procedure when all plugins are done."""
63 | current_ver = self._get_version(db)
64 | if current_ver == 0:
65 | create_tables(self.env, db)
66 | else:
67 | while current_ver+1 <= db_version:
68 | upgrade_map[current_ver+1](self.env, db)
69 | current_ver += 1
70 | cursor = db.cursor()
71 | cursor.execute("UPDATE system SET value=%s WHERE name='code_comments_schema_version'",
72 | str(db_version))
73 |
74 | def _get_version(self, db):
75 | cursor = db.cursor()
76 | try:
77 | sql = "SELECT value FROM system WHERE name='code_comments_schema_version'"
78 | cursor.execute(sql)
79 | for row in cursor:
80 | return int(row[0])
81 | return 0
82 | except:
83 | return 0
84 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Code Comments, an enhancement for Trac
2 | =====================================
3 |
4 | The problem is two-fold. When reviewing code, it's difficult to
5 | associate your comments with their appropriate context. Then,
6 | collecting all of these new issues into actionable tickets requires
7 | a lot of manual effort.
8 |
9 | This plugin allows you to leave comments on top of files, changesets, and
10 | attachments. Once you've added all of your comments, you can send them to
11 | tickets. These include links to these comments and their description.
12 |
13 | It's Github, in your Trac.
14 |
15 | Installation
16 | ------------
17 |
18 | Pick an `.egg` file from the Downloads section and place it in the `plugins/`
19 | directory of your Trac install.
20 |
21 | Trac Code Comments plugin requres at least python 2.4 and runs on Trac 0.12.
22 |
23 | Features
24 | --------
25 |
26 | * Comments on files – you can comment on every file in the repository.
27 |
28 | * Inline comments on files – comment on a specific line. The comments appears
29 | in context, below the line in question.
30 |
31 | * Comments on changesets – useful when doing code reviews of incoming commits.
32 |
33 | * Comments on attachment pages – useful when reviewing patches.
34 |
35 | * Wiki Markup – you can use the standard Trac wiki markup inside your
36 | comments.
37 |
38 | * Instant preview – to make sure you get the formatting right.
39 |
40 | * Sending comments to tickets – you can select arbitrary number of comments
41 | and create a new ticket out of them. The text of the ticket defaults to links
42 | to the comments and their text, but you can edit these before saving the
43 | ticket.
44 |
45 | * Comments/ticket cross-reference – to remember which comments are already in
46 | tickets and which are not.
47 |
48 | Screenshots
49 | -----------
50 |
51 | 
52 |
53 | Contributing
54 | ------------
55 |
56 | We'd love your help!
57 |
58 | If you are a developer, feel free to fork the project here, on github and
59 | submit a pull request with your changes.
60 |
61 | If you are a designer and have UI suggestions, [open an issue](https://github.com/Automattic/trac-code-comments-plugin/issues), and we'll make sure to address your concerns.
62 |
63 | If you want to help with copy, or just wanna say how great or sucky we are
64 | [creating an issue](https://github.com/Automattic/trac-code-comments-plugin/issues) is the way to go.
65 |
66 | You can find help with setting up a local development environment in the [`HACKING`](https://github.com/Automattic/trac-code-comments-plugin/blob/master/HACKING) file in this repostitory.
67 |
68 | Roadmap
69 | -------
70 |
71 | Nobody can predict the future, but here are some features on the roadmap:
72 |
73 | * Line-level comments for changesets and diff atatchments, too
74 | * E-mail notifictaions
75 |
76 | License
77 | -------
78 | Copyright (C) 2011-2012, Automattic Inc.
79 |
80 | This plugin is distributed under the GPLv2 or later license.
--------------------------------------------------------------------------------
/code_comments/templates/comments.html:
--------------------------------------------------------------------------------
1 |
4 |
7 |
8 |
9 | Code Comments
10 |
11 |
24 |
25 |
26 |
27 |
28 |
29 |
Code Comments
30 |
31 | Filter comments:
32 |
47 |
80 |
81 |
82 |
83 |
--------------------------------------------------------------------------------
/code_comments/comments.py:
--------------------------------------------------------------------------------
1 | import os.path
2 | from time import time
3 | from code_comments.comment import Comment
4 |
5 | FILTER_MAX_PATH_DEPTH = 2
6 |
7 | class Comments:
8 | def __init__(self, req, env):
9 | self.req, self.env = req, env
10 |
11 | def comment_from_row(self, row):
12 | return Comment(self.req, self.env, row)
13 |
14 | def get_filter_values(self):
15 | comments = self.all()
16 | return {
17 | 'paths': self.get_all_paths(comments),
18 | 'authors': self.get_all_comment_authors(comments),
19 | }
20 |
21 | def get_all_paths(self, comments):
22 | get_directory = lambda path: '/'.join(os.path.split(path)[0].split('/')[:FILTER_MAX_PATH_DEPTH])
23 | return sorted(set([get_directory(comment.path) for comment in comments if get_directory(comment.path)]))
24 |
25 | def get_all_comment_authors(self, comments):
26 | return sorted(list(set([comment.author for comment in comments])))
27 |
28 | def select(self, *query):
29 | result = {}
30 | @self.env.with_transaction()
31 | def get_comments(db):
32 | cursor = db.cursor()
33 | cursor.execute(*query)
34 | result['comments'] = cursor.fetchall()
35 | return [self.comment_from_row(row) for row in result['comments']]
36 |
37 | def count(self, args = {}):
38 | conditions_str, values = self.condition_str_and_corresponding_values(args)
39 | where = ''
40 | if conditions_str:
41 | where = 'WHERE '+conditions_str
42 | query = 'SELECT COUNT(*) FROM code_comments ' + where
43 | result = {}
44 | @self.env.with_transaction()
45 | def get_comment_count(db):
46 | cursor = db.cursor()
47 | cursor.execute(query, values)
48 | result['count'] = cursor.fetchone()[0]
49 | return result['count']
50 |
51 | def all(self):
52 | return self.search({}, order='DESC')
53 |
54 | def by_id(self, id):
55 | return self.select("SELECT * FROM code_comments WHERE id=%s", [id])[0]
56 |
57 | def assert_name(self, name):
58 | if not name in Comment.columns:
59 | raise ValueError("Column '%s' doesn't exist." % name)
60 |
61 | def search(self, args, order = 'ASC', per_page = None, page = 1):
62 | conditions_str, values = self.condition_str_and_corresponding_values(args)
63 | where = ''
64 | limit = ''
65 | if conditions_str:
66 | where = 'WHERE '+conditions_str
67 | if order != 'ASC':
68 | order = 'DESC'
69 | if per_page:
70 | limit = ' LIMIT %d OFFSET %d' % (per_page, (page - 1)*per_page)
71 | return self.select('SELECT * FROM code_comments ' + where + ' ORDER BY time ' + order + limit, values)
72 |
73 | def condition_str_and_corresponding_values(self, args):
74 | conditions = []
75 | values = []
76 | for name in args:
77 | if not name.endswith('__in') and not name.endswith('__prefix'):
78 | values.append(args[name])
79 | if name.endswith('__gt'):
80 | name = name.replace('__gt', '')
81 | conditions.append(name + ' > %s')
82 | elif name.endswith('__lt'):
83 | name = name.replace('__lt', '')
84 | conditions.append(name + ' < %s')
85 | elif name.endswith('__prefix'):
86 | values.append(args[name].replace('%', '\\%').replace('_', '\\_') + '%')
87 | name = name.replace('__prefix', '')
88 | conditions.append(name + ' LIKE %s')
89 | elif name.endswith('__in'):
90 | items = [item.strip() for item in args[name].split(',')]
91 | name = name.replace('__in', '')
92 | for item in items:
93 | values.append(item)
94 | conditions.append(name + ' IN (' + ','.join(['%s']*len(items)) + ')')
95 | else:
96 | conditions.append(name + ' = %s')
97 | # don't let SQL injections in - make sure the name is an existing comment column
98 | self.assert_name(name)
99 | conditions_str = ' AND '.join(conditions)
100 | return conditions_str, values
101 |
102 | def create(self, args):
103 | comment = Comment(self.req, self.env, args)
104 | comment.validate()
105 | comment.time = int(time())
106 | values = [getattr(comment, column_name) for column_name in comment.columns if column_name != 'id']
107 | comment_id = [None]
108 | @self.env.with_transaction()
109 | def insert_comment(db):
110 | cursor = db.cursor()
111 | sql = "INSERT INTO code_comments values(DEFAULT, %s)" % ', '.join(['%s'] * len(values))
112 | cursor.execute(sql, values)
113 | comment_id[0] = db.get_last_id(cursor, 'code_comments')
114 | return comment_id[0]
115 |
--------------------------------------------------------------------------------
/code_comments/comment.py:
--------------------------------------------------------------------------------
1 | import re
2 |
3 | import trac.wiki.formatter
4 | from trac.mimeview.api import Context
5 | from time import gmtime, strftime
6 | from code_comments import db
7 | from trac.util import Markup
8 |
9 | try:
10 | import json
11 | except ImportError:
12 | import simplejson as json
13 |
14 | try:
15 | import hashlib
16 | md5_hexdigest = lambda s: hashlib.md5(s).hexdigest()
17 | except ImportError:
18 | import md5
19 | md5_hexdigest = lambda s: md5.new(s).hexdigest()
20 |
21 |
22 | VERSION = 1
23 |
24 | class Comment:
25 | columns = [column.name for column in db.schema['code_comments'].columns]
26 |
27 | required = 'text', 'author'
28 |
29 | _email_map = None
30 |
31 | def __init__(self, req, env, data):
32 | if isinstance(data, dict):
33 | self.__dict__ = data
34 | else:
35 | self.__dict__ = dict(zip(self.columns, data))
36 | self.env = env
37 | self.req = req
38 | if self._empty('version'):
39 | self.version = VERSION
40 | self.html = format_to_html(self.req, self.env, self.text)
41 | email = self.email_map().get(self.author, 'baba@baba.net')
42 | self.email_md5 = md5_hexdigest(email)
43 | attachment_info = self.attachment_info()
44 | self.is_comment_to_attachment = attachment_info['is']
45 | self.attachment_ticket = attachment_info['ticket']
46 | self.attachment_filename = attachment_info['filename']
47 | self.is_comment_to_changeset = self.revision and not self.path
48 | self.is_comment_to_file = self.revision and self.path
49 |
50 | def _empty(self, column_name):
51 | return not hasattr(self, column_name) or not getattr(self, column_name)
52 |
53 | def email_map(self):
54 | if Comment._email_map is None:
55 | Comment._email_map = {}
56 | for username, name, email in self.env.get_known_users():
57 | if email:
58 | Comment._email_map[username] = email
59 | return Comment._email_map
60 |
61 | def validate(self):
62 | missing = [column_name for column_name in self.required if self._empty(column_name)]
63 | if missing:
64 | raise ValueError("Comment column(s) missing: %s" % ', '.join(missing))
65 |
66 | def href(self):
67 | if self.is_comment_to_file:
68 | href = self.req.href.browser(self.path, rev=self.revision, codecomment=self.id)
69 | elif self.is_comment_to_changeset:
70 | href = self.req.href.changeset(self.revision, codecomment=self.id)
71 | elif self.is_comment_to_attachment:
72 | href = self.req.href('/attachment/ticket/%d/%s' % (self.attachment_ticket, self.attachment_filename), codecomment=self.id)
73 | if self.line:
74 | href += '#L' + str(self.line)
75 | return href
76 |
77 | def link_text(self):
78 | if self.revision and not self.path:
79 | return '[%s]' % self.revision
80 | if self.path.startswith('attachment:'):
81 | return self.attachment_link_text()
82 |
83 | # except the two specials cases of changesets (revision-only)
84 | # and arrachments (path-only), we must always have them both
85 | assert self.path and self.revision
86 |
87 | link_text = self.path + '@' + str(self.revision)
88 | if self.line:
89 | link_text += '#L' + str(self.line)
90 | return link_text
91 |
92 | def attachment_link_text(self):
93 | return '#%s: %s' % (self.attachment_ticket, self.attachment_filename)
94 |
95 | def trac_link(self):
96 | if self.is_comment_to_attachment:
97 | return '[%s %s]' % (self.req.href())
98 | return 'source:' + self.link_text()
99 |
100 | def attachment_info(self):
101 | info = {'is': False, 'ticket': None, 'filename': None}
102 | info['is'] = self.path.startswith('attachment:')
103 | if not info['is']:
104 | return info
105 | match = re.match(r'attachment:/ticket/(\d+)/(.*)', self.path)
106 | if not match:
107 | return info
108 | info['ticket'] = int(match.group(1))
109 | info['filename'] = match.group(2)
110 | return info
111 |
112 | def path_link_tag(self):
113 | return Markup('%s' % (self.href(), self.link_text()))
114 |
115 | def formatted_date(self):
116 | return strftime('%d %b %Y, %H:%M', gmtime(self.time))
117 |
118 | def get_ticket_relations(self):
119 | relations = set()
120 | db = self.env.get_db_cnx()
121 | cursor = db.cursor()
122 | query = """SELECT ticket FROM ticket_custom WHERE name = 'code_comment_relation' AND
123 | (value LIKE '%(comment_id)d' OR
124 | value LIKE '%(comment_id)d,%%' OR
125 | value LIKE '%%,%(comment_id)d' OR value LIKE '%%,%(comment_id)d,%%')""" % {'comment_id': self.id}
126 | result = {}
127 | @self.env.with_transaction()
128 | def get_ticket_ids(db):
129 | cursor = db.cursor()
130 | cursor.execute(query)
131 | result['tickets'] = cursor.fetchall()
132 | return set([int(row[0]) for row in result['tickets']])
133 |
134 | def get_ticket_links(self):
135 | relations = self.get_ticket_relations()
136 | links = ['[[ticket:%s]]' % relation for relation in relations]
137 | return format_to_html(self.req, self.env, ', '.join(links))
138 |
139 | def delete(self):
140 | @self.env.with_transaction()
141 | def delete_comment(db):
142 | cursor = db.cursor()
143 | cursor.execute("DELETE FROM code_comments WHERE id=%s", [self.id])
144 |
145 | class CommentJSONEncoder(json.JSONEncoder):
146 | def default(self, o):
147 | if isinstance(o, Comment):
148 | for_json = dict([(name, getattr(o, name)) for name in o.__dict__ if isinstance(getattr(o, name), (basestring, int, list, dict))])
149 | for_json['formatted_date'] = o.formatted_date()
150 | for_json['permalink'] = o.href()
151 | return for_json
152 | else:
153 | return json.JSONEncoder.default(self, o)
154 |
155 | def format_to_html(req, env, text):
156 | context = Context.from_request(req)
157 | return trac.wiki.formatter.format_to_html(env, context, text)
--------------------------------------------------------------------------------
/code_comments/htdocs/code-comments.js:
--------------------------------------------------------------------------------
1 | jQuery(function($) {
2 | var _ = window.underscore;
3 | $(document).ajaxError( function(e, xhr, options){
4 | var errorText = xhr.statusText;
5 | if (-1 == xhr.responseText.indexOf('');
188 | $('a.bubble').click(function(e) {
189 | e.preventDefault();
190 | AddCommentDialog.open(LineComments, line);
191 | })
192 | .css({width: $th.width(), height: $th.height(), 'text-align': 'center'})
193 | .find('span').css('margin-left', ($th.width() - 16) / 2);
194 | },
195 | function() {
196 | var $th = $('th', this);
197 | $('a.bubble', $th).remove();
198 | $('a', $th).show();
199 | }
200 | );
201 | }
202 | });
203 |
204 | window.TopComments = new CommentsList;
205 | window.LineComments = new CommentsList;
206 | window.TopCommentsBlock = new TopCommentsView;
207 | window.LineCommentsBlock = new LineCommentsView;
208 | window.AddCommentDialog = new AddCommentDialogView;
209 | window.LineCommentBubbles = new LineCommentBubblesView({el: $('table.code')});
210 |
211 | $(CodeComments.selectorToInsertBefore).before(TopCommentsBlock.render().el);
212 | LineCommentsBlock.render();
213 | AddCommentDialog.render();
214 | LineCommentBubbles.render();
215 | });
--------------------------------------------------------------------------------
/code_comments/web.py:
--------------------------------------------------------------------------------
1 | import re
2 | from trac.core import *
3 | from trac.web.chrome import INavigationContributor, ITemplateProvider, add_script, add_script_data, add_stylesheet, add_notice, add_link
4 | from trac.web.main import IRequestHandler, IRequestFilter
5 | from trac.util import Markup
6 | from trac.util.text import to_unicode
7 | from trac.util.presentation import Paginator
8 | from trac.versioncontrol.api import RepositoryManager
9 | from code_comments.comments import Comments
10 | from code_comments.comment import CommentJSONEncoder, format_to_html
11 |
12 | try:
13 | import json
14 | except ImportError:
15 | import simplejson as json
16 |
17 | class CodeComments(Component):
18 | implements(ITemplateProvider, IRequestFilter)
19 |
20 | href = 'code-comments'
21 |
22 | # ITemplateProvider methods
23 | def get_templates_dirs(self):
24 | return [self.get_template_dir()]
25 |
26 | def get_template_dir(self):
27 | from pkg_resources import resource_filename
28 | return resource_filename(__name__, 'templates')
29 |
30 | def get_htdocs_dirs(self):
31 | from pkg_resources import resource_filename
32 | return [('code-comments', resource_filename(__name__, 'htdocs'))]
33 |
34 | # IRequestFilter methods
35 | def pre_process_request(self, req, handler):
36 | return handler
37 |
38 | def post_process_request(self, req, template, data, content_type):
39 | add_stylesheet(req, 'code-comments/code-comments.css')
40 | return template, data, content_type
41 |
42 | class MainNavigation(CodeComments):
43 | implements(INavigationContributor)
44 |
45 | # INavigationContributor methods
46 | def get_active_navigation_item(self, req):
47 | return self.href
48 |
49 | def get_navigation_items(self, req):
50 | if 'TRAC_ADMIN' in req.perm:
51 | yield 'mainnav', 'code-comments', Markup('Code Comments' % (
52 | req.href(self.href) ) )
53 |
54 | class JSDataForRequests(CodeComments):
55 | implements(IRequestFilter)
56 |
57 | js_templates = ['top-comments-block', 'comment', 'add-comment-dialog', 'line-comment', 'comments-for-a-line',]
58 |
59 | # IRequestFilter methods
60 | def pre_process_request(self, req, handler):
61 | return handler
62 |
63 | def post_process_request(self, req, template, data, content_type):
64 | if data is None:
65 | return
66 |
67 | js_data = {
68 | 'comments_rest_url': req.href(CommentsREST.href),
69 | 'formatting_help_url': req.href.wiki('WikiFormatting'),
70 | 'delete_url': req.href(DeleteCommentForm.href),
71 | 'preview_url': req.href(WikiPreview.href),
72 | 'templates': self.templates_js_data(),
73 | 'active_comment_id': req.args.get('codecomment'),
74 | 'username': req.authname,
75 | 'is_admin': 'TRAC_ADMIN' in req.perm,
76 | }
77 |
78 | original_return_value = template, data, content_type
79 | if req.path_info.startswith('/changeset/'):
80 | js_data.update(self.changeset_js_data(req, data))
81 | elif req.path_info.startswith('/browser'):
82 | js_data.update(self.browser_js_data(req, data))
83 | elif re.match(r'/attachment/ticket/\d+/.*', req.path_info):
84 | js_data.update(self.attachment_js_data(req, data))
85 | else:
86 | return original_return_value
87 |
88 | add_script(req, 'code-comments/json2.js')
89 | add_script(req, 'code-comments/underscore-min.js')
90 | add_script(req, 'code-comments/backbone-min.js')
91 | # jQuery UI includes: UI Core, Interactions, Button & Dialog Widgets, Core Effects, custom theme
92 | add_script(req, 'code-comments/jquery-ui/jquery-ui.js')
93 | add_stylesheet(req, 'code-comments/jquery-ui/trac-theme.css')
94 | add_script(req, 'code-comments/jquery.ba-throttle-debounce.min.js')
95 | add_script(req, 'code-comments/code-comments.js')
96 | add_script_data(req, {'CodeComments': js_data})
97 | return original_return_value
98 |
99 | def templates_js_data(self):
100 | data = {}
101 | for name in self.js_templates:
102 | # we want to use the name as JS identifier and we can't have dashes there
103 | data[name.replace('-', '_')] = self.template_js_data(name)
104 | return data
105 |
106 | def changeset_js_data(self, req, data):
107 | return {'page': 'changeset', 'revision': data['new_rev'], 'path': '', 'selectorToInsertBefore': 'div.diff:first'}
108 |
109 | def browser_js_data(self, req, data):
110 | return {'page': 'browser', 'revision': data['rev'], 'path': data['path'], 'selectorToInsertBefore': 'table#info'}
111 |
112 | def attachment_js_data(self, req, data):
113 | path = req.path_info.replace('/attachment/', 'attachment:/')
114 | return {'page': 'attachment', 'revision': 0, 'path': path, 'selectorToInsertBefore': 'table#info'}
115 |
116 | def template_js_data(self, name):
117 | file_name = name + '.html'
118 | return to_unicode(open(self.get_template_dir() + '/js/' + file_name).read())
119 |
120 |
121 |
122 | class ListComments(CodeComments):
123 | implements(IRequestHandler)
124 |
125 | COMMENTS_PER_PAGE = 20
126 |
127 | # IRequestHandler methods
128 | def match_request(self, req):
129 | return req.path_info == '/' + self.href
130 |
131 | def process_request(self, req):
132 | req.perm.require('TRAC_ADMIN')
133 |
134 | self.data = {}
135 | self.args = {}
136 | self.req = req
137 |
138 | self.per_page = int(req.args.get('per_page', self.COMMENTS_PER_PAGE))
139 | self.page = int(req.args.get('page', 1))
140 |
141 | self.add_path_and_author_filters()
142 |
143 | self.data['comments'] = Comments(req, self.env).search(self.args, 'DESC', self.per_page, self.page)
144 | self.data['reponame'], repos, path = RepositoryManager(self.env).get_repository_by_path('/')
145 | self.data['can_delete'] = 'TRAC_ADMIN' in req.perm
146 | self.data['paginator'] = self.get_paginator()
147 |
148 | self.data.update(Comments(req, self.env).get_filter_values())
149 |
150 | return 'comments.html', self.data, None
151 |
152 | def add_path_and_author_filters(self):
153 | self.data['current_path_selection'] = '';
154 | self.data['current_author_selection'] = '';
155 |
156 | if self.req.args.get('filter-by-path'):
157 | self.args['path__prefix'] = self.req.args['filter-by-path'];
158 | self.data['current_path_selection'] = self.req.args['filter-by-path']
159 | if self.req.args.get('filter-by-author'):
160 | self.args['author'] = self.req.args['filter-by-author']
161 | self.data['current_author_selection'] = self.req.args['filter-by-author']
162 |
163 |
164 | def get_paginator(self):
165 | href_with_page = lambda page: self.req.href(self.href, page=page)
166 | paginator = Paginator(self.data['comments'], self.page-1, self.per_page, Comments(self.req, self.env).count(self.args))
167 | if paginator.has_next_page:
168 | add_link(self.req, 'next', href_with_page(self.page + 1), 'Next Page')
169 | if paginator.has_previous_page:
170 | add_link(self.req, 'prev', href_with_page(self.page - 1), 'Previous Page')
171 | shown_pages = paginator.get_shown_pages(page_index_count = 11)
172 | links = [{'href': href_with_page(page), 'class': None, 'string': str(page), 'title': 'Page %d' % page}
173 | for page in shown_pages]
174 | paginator.shown_pages = links
175 | paginator.current_page = {'href': None, 'class': 'current', 'string': str(paginator.page + 1), 'title': None}
176 | return paginator
177 |
178 | class DeleteCommentForm(CodeComments):
179 | implements(IRequestHandler)
180 |
181 | href = CodeComments.href + '/delete'
182 |
183 | # IRequestHandler methods
184 | def match_request(self, req):
185 | return req.path_info == '/' + self.href
186 |
187 | def process_request(self, req):
188 | req.perm.require('TRAC_ADMIN')
189 | if 'GET' == req.method:
190 | return self.form(req)
191 | else:
192 | return self.delete(req)
193 |
194 | def form(self, req):
195 | data = {}
196 | referrer = req.get_header('Referer')
197 | data['comment'] = Comments(req, self.env).by_id(req.args['id'])
198 | data['return_to'] = referrer
199 | return 'delete.html', data, None
200 |
201 | def delete(self, req):
202 | comment = Comments(req, self.env).by_id(req.args['id'])
203 | comment.delete()
204 | add_notice(req, 'Comment deleted.')
205 | req.redirect(req.args['return_to'] or req.href())
206 |
207 | class BundleCommentsRedirect(CodeComments):
208 | implements(IRequestHandler)
209 |
210 | href = CodeComments.href + '/create-ticket'
211 |
212 | # IRequestHandler methods
213 | def match_request(self, req):
214 | return req.path_info == '/' + self.href
215 |
216 | def process_request(self, req):
217 | text = ''
218 | for id in req.args['ids'].split(','):
219 | comment = Comments(req, self.env).by_id(id)
220 | text += """
221 | [[CodeCommentLink(%(id)s)]]
222 | %(comment_text)s
223 |
224 | """.lstrip() % {'id': id, 'comment_text': comment.text}
225 | req.redirect(req.href.newticket(description=text))
226 |
227 | class CommentsREST(CodeComments):
228 | implements(IRequestHandler)
229 |
230 | href = CodeComments.href + '/comments'
231 |
232 | # IRequestHandler methods
233 | def match_request(self, req):
234 | return req.path_info.startswith('/' + self.href)
235 |
236 | def return_json(self, req, data, code=200):
237 | req.send(json.dumps(data, cls=CommentJSONEncoder), 'application/json')
238 |
239 | def process_request(self, req):
240 | #TODO: catch errors
241 | if '/' + self.href == req.path_info:
242 | if 'GET' == req.method:
243 | self.return_json(req, Comments(req, self.env).search(req.args))
244 | if 'POST' == req.method:
245 | comments = Comments(req, self.env)
246 | id = comments.create(json.loads(req.read()))
247 | self.return_json(req, comments.by_id(id))
248 |
249 | class WikiPreview(CodeComments):
250 | implements(IRequestHandler)
251 |
252 | href = CodeComments.href + '/preview'
253 |
254 | # IRequestHandler methods
255 | def match_request(self, req):
256 | return req.path_info.startswith('/' + self.href)
257 |
258 | def process_request(self, req):
259 | req.send(format_to_html(req, self.env, req.args.get('text', '')).encode('utf-8'))
--------------------------------------------------------------------------------
/code_comments/htdocs/underscore-min.js:
--------------------------------------------------------------------------------
1 | // Underscore.js 1.2.2
2 | // (c) 2011 Jeremy Ashkenas, DocumentCloud Inc.
3 | // Underscore is freely distributable under the MIT license.
4 | // Portions of Underscore are inspired or borrowed from Prototype,
5 | // Oliver Steele's Functional, and John Resig's Micro-Templating.
6 | // For all details and documentation:
7 | // http://documentcloud.github.com/underscore
8 | (function(){function r(a,c,d){if(a===c)return a!==0||1/a==1/c;if(a==null||c==null)return a===c;if(a._chain)a=a._wrapped;if(c._chain)c=c._wrapped;if(b.isFunction(a.isEqual))return a.isEqual(c);if(b.isFunction(c.isEqual))return c.isEqual(a);var e=l.call(a);if(e!=l.call(c))return false;switch(e){case "[object String]":return String(a)==String(c);case "[object Number]":return a=+a,c=+c,a!=a?c!=c:a==0?1/a==1/c:a==c;case "[object Date]":case "[object Boolean]":return+a==+c;case "[object RegExp]":return a.source==
9 | c.source&&a.global==c.global&&a.multiline==c.multiline&&a.ignoreCase==c.ignoreCase}if(typeof a!="object"||typeof c!="object")return false;for(var f=d.length;f--;)if(d[f]==a)return true;d.push(a);var f=0,g=true;if(e=="[object Array]"){if(f=a.length,g=f==c.length)for(;f--;)if(!(g=f in a==f in c&&r(a[f],c[f],d)))break}else{if("constructor"in a!="constructor"in c||a.constructor!=c.constructor)return false;for(var h in a)if(m.call(a,h)&&(f++,!(g=m.call(c,h)&&r(a[h],c[h],d))))break;if(g){for(h in c)if(m.call(c,
10 | h)&&!f--)break;g=!f}}d.pop();return g}var s=this,F=s._,o={},k=Array.prototype,p=Object.prototype,i=k.slice,G=k.unshift,l=p.toString,m=p.hasOwnProperty,v=k.forEach,w=k.map,x=k.reduce,y=k.reduceRight,z=k.filter,A=k.every,B=k.some,q=k.indexOf,C=k.lastIndexOf,p=Array.isArray,H=Object.keys,t=Function.prototype.bind,b=function(a){return new n(a)};if(typeof exports!=="undefined"){if(typeof module!=="undefined"&&module.exports)exports=module.exports=b;exports._=b}else typeof define==="function"&&define.amd?
11 | define("underscore",function(){return b}):s._=b;b.VERSION="1.2.2";var j=b.each=b.forEach=function(a,c,b){if(a!=null)if(v&&a.forEach===v)a.forEach(c,b);else if(a.length===+a.length)for(var e=0,f=a.length;e=e.computed&&(e={value:a,computed:b})});return e.value};b.min=function(a,c,d){if(!c&&b.isArray(a))return Math.min.apply(Math,a);if(!c&&b.isEmpty(a))return Infinity;var e={computed:Infinity};j(a,function(a,b,h){b=c?c.call(d,a,b,h):a;bd?1:0}),"value")};b.groupBy=function(a,c){var d={},e=b.isFunction(c)?c:function(a){return a[c]};j(a,function(a,c){var b=e(a,c);(d[b]||(d[b]=[])).push(a)});return d};b.sortedIndex=function(a,c,d){d||(d=b.identity);for(var e=0,f=a.length;e<
17 | f;){var g=e+f>>1;d(a[g])=0})})};b.difference=function(a,c){return b.filter(a,function(a){return!b.include(c,a)})};b.zip=function(){for(var a=i.call(arguments),c=b.max(b.pluck(a,"length")),d=Array(c),e=0;e=0;d--)b=[a[d].apply(this,b)];return b[0]}};b.after=function(a,b){return a<=0?b():function(){if(--a<1)return b.apply(this,arguments)}};b.keys=H||function(a){if(a!==
24 | Object(a))throw new TypeError("Invalid object");var b=[],d;for(d in a)m.call(a,d)&&(b[b.length]=d);return b};b.values=function(a){return b.map(a,b.identity)};b.functions=b.methods=function(a){var c=[],d;for(d in a)b.isFunction(a[d])&&c.push(d);return c.sort()};b.extend=function(a){j(i.call(arguments,1),function(b){for(var d in b)b[d]!==void 0&&(a[d]=b[d])});return a};b.defaults=function(a){j(i.call(arguments,1),function(b){for(var d in b)a[d]==null&&(a[d]=b[d])});return a};b.clone=function(a){return!b.isObject(a)?
25 | a:b.isArray(a)?a.slice():b.extend({},a)};b.tap=function(a,b){b(a);return a};b.isEqual=function(a,b){return r(a,b,[])};b.isEmpty=function(a){if(b.isArray(a)||b.isString(a))return a.length===0;for(var c in a)if(m.call(a,c))return false;return true};b.isElement=function(a){return!!(a&&a.nodeType==1)};b.isArray=p||function(a){return l.call(a)=="[object Array]"};b.isObject=function(a){return a===Object(a)};b.isArguments=l.call(arguments)=="[object Arguments]"?function(a){return l.call(a)=="[object Arguments]"}:
26 | function(a){return!(!a||!m.call(a,"callee"))};b.isFunction=function(a){return l.call(a)=="[object Function]"};b.isString=function(a){return l.call(a)=="[object String]"};b.isNumber=function(a){return l.call(a)=="[object Number]"};b.isNaN=function(a){return a!==a};b.isBoolean=function(a){return a===true||a===false||l.call(a)=="[object Boolean]"};b.isDate=function(a){return l.call(a)=="[object Date]"};b.isRegExp=function(a){return l.call(a)=="[object RegExp]"};b.isNull=function(a){return a===null};
27 | b.isUndefined=function(a){return a===void 0};b.noConflict=function(){s._=F;return this};b.identity=function(a){return a};b.times=function(a,b,d){for(var e=0;e/g,">").replace(/"/g,""").replace(/'/g,"'").replace(/\//g,"/")};b.mixin=function(a){j(b.functions(a),function(c){I(c,b[c]=a[c])})};var J=0;b.uniqueId=function(a){var b=J++;return a?a+b:b};b.templateSettings={evaluate:/<%([\s\S]+?)%>/g,
28 | interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};b.template=function(a,c){var d=b.templateSettings,d="var __p=[],print=function(){__p.push.apply(__p,arguments);};with(obj||{}){__p.push('"+a.replace(/\\/g,"\\\\").replace(/'/g,"\\'").replace(d.escape,function(a,b){return"',_.escape("+b.replace(/\\'/g,"'")+"),'"}).replace(d.interpolate,function(a,b){return"',"+b.replace(/\\'/g,"'")+",'"}).replace(d.evaluate||null,function(a,b){return"');"+b.replace(/\\'/g,"'").replace(/[\r\n\t]/g," ")+";__p.push('"}).replace(/\r/g,
29 | "\\r").replace(/\n/g,"\\n").replace(/\t/g,"\\t")+"');}return __p.join('');",e=new Function("obj","_",d);return c?e(c,b):function(a){return e(a,b)}};var n=function(a){this._wrapped=a};b.prototype=n.prototype;var u=function(a,c){return c?b(a).chain():a},I=function(a,c){n.prototype[a]=function(){var a=i.call(arguments);G.call(a,this._wrapped);return u(c.apply(b,a),this._chain)}};b.mixin(b);j("pop,push,reverse,shift,sort,splice,unshift".split(","),function(a){var b=k[a];n.prototype[a]=function(){b.apply(this._wrapped,
30 | arguments);return u(this._wrapped,this._chain)}});j(["concat","join","slice"],function(a){var b=k[a];n.prototype[a]=function(){return u(b.apply(this._wrapped,arguments),this._chain)}});n.prototype.chain=function(){this._chain=true;return this};n.prototype.value=function(){return this._wrapped}}).call(this);
31 | this.underscore = _.noConflict();
32 |
--------------------------------------------------------------------------------
/code_comments/htdocs/backbone-min.js:
--------------------------------------------------------------------------------
1 | // Backbone.js 0.5.3
2 | // (c) 2010 Jeremy Ashkenas, DocumentCloud Inc.
3 | // Backbone may be freely distributed under the MIT license.
4 | // For all details and documentation:
5 | // http://documentcloud.github.com/backbone
6 |
7 | // Set the global _ to the underscore library instance
8 | // because backbone doesn't support dependency injection
9 | // in the underscore library
10 | var _old_underscore = this._;
11 | this._ = this.underscore;
12 |
13 | (function(){var h=this,p=h.Backbone,e;e=typeof exports!=="undefined"?exports:h.Backbone={};e.VERSION="0.5.3";var f=h._;if(!f&&typeof require!=="undefined")f=require("underscore")._;var g=h.jQuery||h.Zepto;e.noConflict=function(){h.Backbone=p;return this};e.emulateHTTP=!1;e.emulateJSON=!1;e.Events={bind:function(a,b,c){var d=this._callbacks||(this._callbacks={});(d[a]||(d[a]=[])).push([b,c]);return this},unbind:function(a,b){var c;if(a){if(c=this._callbacks)if(b){c=c[a];if(!c)return this;for(var d=
14 | 0,e=c.length;d/g,">").replace(/"/g,""").replace(/'/g,"'").replace(/\//g,"/")},has:function(a){return this.attributes[a]!=null},set:function(a,b){b||(b={});if(!a)return this;if(a.attributes)a=a.attributes;var c=this.attributes,d=this._escapedAttributes;if(!b.silent&&this.validate&&!this._performValidation(a,b))return!1;if(this.idAttribute in a)this.id=a[this.idAttribute];
17 | var e=this._changing;this._changing=!0;for(var g in a){var h=a[g];if(!f.isEqual(c[g],h))c[g]=h,delete d[g],this._changed=!0,b.silent||this.trigger("change:"+g,this,h,b)}!e&&!b.silent&&this._changed&&this.change(b);this._changing=!1;return this},unset:function(a,b){if(!(a in this.attributes))return this;b||(b={});var c={};c[a]=void 0;if(!b.silent&&this.validate&&!this._performValidation(c,b))return!1;delete this.attributes[a];delete this._escapedAttributes[a];a==this.idAttribute&&delete this.id;this._changed=
18 | !0;b.silent||(this.trigger("change:"+a,this,void 0,b),this.change(b));return this},clear:function(a){a||(a={});var b,c=this.attributes,d={};for(b in c)d[b]=void 0;if(!a.silent&&this.validate&&!this._performValidation(d,a))return!1;this.attributes={};this._escapedAttributes={};this._changed=!0;if(!a.silent){for(b in c)this.trigger("change:"+b,this,void 0,a);this.change(a)}return this},fetch:function(a){a||(a={});var b=this,c=a.success;a.success=function(d,e,f){if(!b.set(b.parse(d,f),a))return!1;c&&
19 | c(b,d)};a.error=i(a.error,b,a);return(this.sync||e.sync).call(this,"read",this,a)},save:function(a,b){b||(b={});if(a&&!this.set(a,b))return!1;var c=this,d=b.success;b.success=function(a,e,f){if(!c.set(c.parse(a,f),b))return!1;d&&d(c,a,f)};b.error=i(b.error,c,b);var f=this.isNew()?"create":"update";return(this.sync||e.sync).call(this,f,this,b)},destroy:function(a){a||(a={});if(this.isNew())return this.trigger("destroy",this,this.collection,a);var b=this,c=a.success;a.success=function(d){b.trigger("destroy",
20 | b,b.collection,a);c&&c(b,d)};a.error=i(a.error,b,a);return(this.sync||e.sync).call(this,"delete",this,a)},url:function(){var a=k(this.collection)||this.urlRoot||l();if(this.isNew())return a;return a+(a.charAt(a.length-1)=="/"?"":"/")+encodeURIComponent(this.id)},parse:function(a){return a},clone:function(){return new this.constructor(this)},isNew:function(){return this.id==null},change:function(a){this.trigger("change",this,a);this._previousAttributes=f.clone(this.attributes);this._changed=!1},hasChanged:function(a){if(a)return this._previousAttributes[a]!=
21 | this.attributes[a];return this._changed},changedAttributes:function(a){a||(a=this.attributes);var b=this._previousAttributes,c=!1,d;for(d in a)f.isEqual(b[d],a[d])||(c=c||{},c[d]=a[d]);return c},previous:function(a){if(!a||!this._previousAttributes)return null;return this._previousAttributes[a]},previousAttributes:function(){return f.clone(this._previousAttributes)},_performValidation:function(a,b){var c=this.validate(a);if(c)return b.error?b.error(this,c,b):this.trigger("error",this,c,b),!1;return!0}});
22 | e.Collection=function(a,b){b||(b={});if(b.comparator)this.comparator=b.comparator;f.bindAll(this,"_onModelEvent","_removeReference");this._reset();a&&this.reset(a,{silent:!0});this.initialize.apply(this,arguments)};f.extend(e.Collection.prototype,e.Events,{model:e.Model,initialize:function(){},toJSON:function(){return this.map(function(a){return a.toJSON()})},add:function(a,b){if(f.isArray(a))for(var c=0,d=a.length;c').hide().appendTo("body")[0].contentWindow,this.navigate(a);
32 | this._hasPushState?g(window).bind("popstate",this.checkUrl):"onhashchange"in window&&!b?g(window).bind("hashchange",this.checkUrl):setInterval(this.checkUrl,this.interval);this.fragment=a;m=!0;a=window.location;b=a.pathname==this.options.root;if(this._wantsPushState&&!this._hasPushState&&!b)return this.fragment=this.getFragment(null,!0),window.location.replace(this.options.root+"#"+this.fragment),!0;else if(this._wantsPushState&&this._hasPushState&&b&&a.hash)this.fragment=a.hash.replace(j,""),window.history.replaceState({},
33 | document.title,a.protocol+"//"+a.host+this.options.root+this.fragment);if(!this.options.silent)return this.loadUrl()},route:function(a,b){this.handlers.unshift({route:a,callback:b})},checkUrl:function(){var a=this.getFragment();a==this.fragment&&this.iframe&&(a=this.getFragment(this.iframe.location.hash));if(a==this.fragment||a==decodeURIComponent(this.fragment))return!1;this.iframe&&this.navigate(a);this.loadUrl()||this.loadUrl(window.location.hash)},loadUrl:function(a){var b=this.fragment=this.getFragment(a);
34 | return f.any(this.handlers,function(a){if(a.route.test(b))return a.callback(b),!0})},navigate:function(a,b){var c=(a||"").replace(j,"");if(!(this.fragment==c||this.fragment==decodeURIComponent(c))){if(this._hasPushState){var d=window.location;c.indexOf(this.options.root)!=0&&(c=this.options.root+c);this.fragment=c;window.history.pushState({},document.title,d.protocol+"//"+d.host+c)}else if(window.location.hash=this.fragment=c,this.iframe&&c!=this.getFragment(this.iframe.location.hash))this.iframe.document.open().close(),
35 | this.iframe.location.hash=c;b&&this.loadUrl(a)}}});e.View=function(a){this.cid=f.uniqueId("view");this._configure(a||{});this._ensureElement();this.delegateEvents();this.initialize.apply(this,arguments)};var u=/^(\S+)\s*(.*)$/,n=["model","collection","el","id","attributes","className","tagName"];f.extend(e.View.prototype,e.Events,{tagName:"div",$:function(a){return g(a,this.el)},initialize:function(){},render:function(){return this},remove:function(){g(this.el).remove();return this},make:function(a,
36 | b,c){a=document.createElement(a);b&&g(a).attr(b);c&&g(a).html(c);return a},delegateEvents:function(a){if(a||(a=this.events))for(var b in f.isFunction(a)&&(a=a.call(this)),g(this.el).unbind(".delegateEvents"+this.cid),a){var c=this[a[b]];if(!c)throw Error('Event "'+a[b]+'" does not exist');var d=b.match(u),e=d[1];d=d[2];c=f.bind(c,this);e+=".delegateEvents"+this.cid;d===""?g(this.el).bind(e,c):g(this.el).delegate(d,e,c)}},_configure:function(a){this.options&&(a=f.extend({},this.options,a));for(var b=
37 | 0,c=n.length;b').hide().appendTo('body')[0].contentWindow;
788 | this.navigate(fragment);
789 | }
790 |
791 | // Depending on whether we're using pushState or hashes, and whether
792 | // 'onhashchange' is supported, determine how we check the URL state.
793 | if (this._hasPushState) {
794 | $(window).bind('popstate', this.checkUrl);
795 | } else if ('onhashchange' in window && !oldIE) {
796 | $(window).bind('hashchange', this.checkUrl);
797 | } else {
798 | setInterval(this.checkUrl, this.interval);
799 | }
800 |
801 | // Determine if we need to change the base url, for a pushState link
802 | // opened by a non-pushState browser.
803 | this.fragment = fragment;
804 | historyStarted = true;
805 | var loc = window.location;
806 | var atRoot = loc.pathname == this.options.root;
807 | if (this._wantsPushState && !this._hasPushState && !atRoot) {
808 | this.fragment = this.getFragment(null, true);
809 | window.location.replace(this.options.root + '#' + this.fragment);
810 | // Return immediately as browser will do redirect to new url
811 | return true;
812 | } else if (this._wantsPushState && this._hasPushState && atRoot && loc.hash) {
813 | this.fragment = loc.hash.replace(hashStrip, '');
814 | window.history.replaceState({}, document.title, loc.protocol + '//' + loc.host + this.options.root + this.fragment);
815 | }
816 |
817 | if (!this.options.silent) {
818 | return this.loadUrl();
819 | }
820 | },
821 |
822 | // Add a route to be tested when the fragment changes. Routes added later may
823 | // override previous routes.
824 | route : function(route, callback) {
825 | this.handlers.unshift({route : route, callback : callback});
826 | },
827 |
828 | // Checks the current URL to see if it has changed, and if it has,
829 | // calls `loadUrl`, normalizing across the hidden iframe.
830 | checkUrl : function(e) {
831 | var current = this.getFragment();
832 | if (current == this.fragment && this.iframe) current = this.getFragment(this.iframe.location.hash);
833 | if (current == this.fragment || current == decodeURIComponent(this.fragment)) return false;
834 | if (this.iframe) this.navigate(current);
835 | this.loadUrl() || this.loadUrl(window.location.hash);
836 | },
837 |
838 | // Attempt to load the current URL fragment. If a route succeeds with a
839 | // match, returns `true`. If no defined routes matches the fragment,
840 | // returns `false`.
841 | loadUrl : function(fragmentOverride) {
842 | var fragment = this.fragment = this.getFragment(fragmentOverride);
843 | var matched = _.any(this.handlers, function(handler) {
844 | if (handler.route.test(fragment)) {
845 | handler.callback(fragment);
846 | return true;
847 | }
848 | });
849 | return matched;
850 | },
851 |
852 | // Save a fragment into the hash history. You are responsible for properly
853 | // URL-encoding the fragment in advance. This does not trigger
854 | // a `hashchange` event.
855 | navigate : function(fragment, triggerRoute) {
856 | var frag = (fragment || '').replace(hashStrip, '');
857 | if (this.fragment == frag || this.fragment == decodeURIComponent(frag)) return;
858 | if (this._hasPushState) {
859 | var loc = window.location;
860 | if (frag.indexOf(this.options.root) != 0) frag = this.options.root + frag;
861 | this.fragment = frag;
862 | window.history.pushState({}, document.title, loc.protocol + '//' + loc.host + frag);
863 | } else {
864 | window.location.hash = this.fragment = frag;
865 | if (this.iframe && (frag != this.getFragment(this.iframe.location.hash))) {
866 | this.iframe.document.open().close();
867 | this.iframe.location.hash = frag;
868 | }
869 | }
870 | if (triggerRoute) this.loadUrl(fragment);
871 | }
872 |
873 | });
874 |
875 | // Backbone.View
876 | // -------------
877 |
878 | // Creating a Backbone.View creates its initial element outside of the DOM,
879 | // if an existing element is not provided...
880 | Backbone.View = function(options) {
881 | this.cid = _.uniqueId('view');
882 | this._configure(options || {});
883 | this._ensureElement();
884 | this.delegateEvents();
885 | this.initialize.apply(this, arguments);
886 | };
887 |
888 | // Element lookup, scoped to DOM elements within the current view.
889 | // This should be prefered to global lookups, if you're dealing with
890 | // a specific view.
891 | var selectorDelegate = function(selector) {
892 | return $(selector, this.el);
893 | };
894 |
895 | // Cached regex to split keys for `delegate`.
896 | var eventSplitter = /^(\S+)\s*(.*)$/;
897 |
898 | // List of view options to be merged as properties.
899 | var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName'];
900 |
901 | // Set up all inheritable **Backbone.View** properties and methods.
902 | _.extend(Backbone.View.prototype, Backbone.Events, {
903 |
904 | // The default `tagName` of a View's element is `"div"`.
905 | tagName : 'div',
906 |
907 | // Attach the `selectorDelegate` function as the `$` property.
908 | $ : selectorDelegate,
909 |
910 | // Initialize is an empty function by default. Override it with your own
911 | // initialization logic.
912 | initialize : function(){},
913 |
914 | // **render** is the core function that your view should override, in order
915 | // to populate its element (`this.el`), with the appropriate HTML. The
916 | // convention is for **render** to always return `this`.
917 | render : function() {
918 | return this;
919 | },
920 |
921 | // Remove this view from the DOM. Note that the view isn't present in the
922 | // DOM by default, so calling this method may be a no-op.
923 | remove : function() {
924 | $(this.el).remove();
925 | return this;
926 | },
927 |
928 | // For small amounts of DOM Elements, where a full-blown template isn't
929 | // needed, use **make** to manufacture elements, one at a time.
930 | //
931 | // var el = this.make('li', {'class': 'row'}, this.model.escape('title'));
932 | //
933 | make : function(tagName, attributes, content) {
934 | var el = document.createElement(tagName);
935 | if (attributes) $(el).attr(attributes);
936 | if (content) $(el).html(content);
937 | return el;
938 | },
939 |
940 | // Set callbacks, where `this.callbacks` is a hash of
941 | //
942 | // *{"event selector": "callback"}*
943 | //
944 | // {
945 | // 'mousedown .title': 'edit',
946 | // 'click .button': 'save'
947 | // }
948 | //
949 | // pairs. Callbacks will be bound to the view, with `this` set properly.
950 | // Uses event delegation for efficiency.
951 | // Omitting the selector binds the event to `this.el`.
952 | // This only works for delegate-able events: not `focus`, `blur`, and
953 | // not `change`, `submit`, and `reset` in Internet Explorer.
954 | delegateEvents : function(events) {
955 | if (!(events || (events = this.events))) return;
956 | if (_.isFunction(events)) events = events.call(this);
957 | $(this.el).unbind('.delegateEvents' + this.cid);
958 | for (var key in events) {
959 | var method = this[events[key]];
960 | if (!method) throw new Error('Event "' + events[key] + '" does not exist');
961 | var match = key.match(eventSplitter);
962 | var eventName = match[1], selector = match[2];
963 | method = _.bind(method, this);
964 | eventName += '.delegateEvents' + this.cid;
965 | if (selector === '') {
966 | $(this.el).bind(eventName, method);
967 | } else {
968 | $(this.el).delegate(selector, eventName, method);
969 | }
970 | }
971 | },
972 |
973 | // Performs the initial configuration of a View with a set of options.
974 | // Keys with special meaning *(model, collection, id, className)*, are
975 | // attached directly to the view.
976 | _configure : function(options) {
977 | if (this.options) options = _.extend({}, this.options, options);
978 | for (var i = 0, l = viewOptions.length; i < l; i++) {
979 | var attr = viewOptions[i];
980 | if (options[attr]) this[attr] = options[attr];
981 | }
982 | this.options = options;
983 | },
984 |
985 | // Ensure that the View has a DOM element to render into.
986 | // If `this.el` is a string, pass it through `$()`, take the first
987 | // matching element, and re-assign it to `el`. Otherwise, create
988 | // an element from the `id`, `className` and `tagName` proeprties.
989 | _ensureElement : function() {
990 | if (!this.el) {
991 | var attrs = this.attributes || {};
992 | if (this.id) attrs.id = this.id;
993 | if (this.className) attrs['class'] = this.className;
994 | this.el = this.make(this.tagName, attrs);
995 | } else if (_.isString(this.el)) {
996 | this.el = $(this.el).get(0);
997 | }
998 | }
999 |
1000 | });
1001 |
1002 | // The self-propagating extend function that Backbone classes use.
1003 | var extend = function (protoProps, classProps) {
1004 | var child = inherits(this, protoProps, classProps);
1005 | child.extend = this.extend;
1006 | return child;
1007 | };
1008 |
1009 | // Set up inheritance for the model, collection, and view.
1010 | Backbone.Model.extend = Backbone.Collection.extend =
1011 | Backbone.Router.extend = Backbone.View.extend = extend;
1012 |
1013 | // Map from CRUD to HTTP for our default `Backbone.sync` implementation.
1014 | var methodMap = {
1015 | 'create': 'POST',
1016 | 'update': 'PUT',
1017 | 'delete': 'DELETE',
1018 | 'read' : 'GET'
1019 | };
1020 |
1021 | // Backbone.sync
1022 | // -------------
1023 |
1024 | // Override this function to change the manner in which Backbone persists
1025 | // models to the server. You will be passed the type of request, and the
1026 | // model in question. By default, uses makes a RESTful Ajax request
1027 | // to the model's `url()`. Some possible customizations could be:
1028 | //
1029 | // * Use `setTimeout` to batch rapid-fire updates into a single request.
1030 | // * Send up the models as XML instead of JSON.
1031 | // * Persist models via WebSockets instead of Ajax.
1032 | //
1033 | // Turn on `Backbone.emulateHTTP` in order to send `PUT` and `DELETE` requests
1034 | // as `POST`, with a `_method` parameter containing the true HTTP method,
1035 | // as well as all requests with the body as `application/x-www-form-urlencoded` instead of
1036 | // `application/json` with the model in a param named `model`.
1037 | // Useful when interfacing with server-side languages like **PHP** that make
1038 | // it difficult to read the body of `PUT` requests.
1039 | Backbone.sync = function(method, model, options) {
1040 | var type = methodMap[method];
1041 |
1042 | // Default JSON-request options.
1043 | var params = _.extend({
1044 | type: type,
1045 | dataType: 'json'
1046 | }, options);
1047 |
1048 | // Ensure that we have a URL.
1049 | if (!params.url) {
1050 | params.url = getUrl(model) || urlError();
1051 | }
1052 |
1053 | // Ensure that we have the appropriate request data.
1054 | if (!params.data && model && (method == 'create' || method == 'update')) {
1055 | params.contentType = 'application/json';
1056 | params.data = JSON.stringify(model.toJSON());
1057 | }
1058 |
1059 | // For older servers, emulate JSON by encoding the request into an HTML-form.
1060 | if (Backbone.emulateJSON) {
1061 | params.contentType = 'application/x-www-form-urlencoded';
1062 | params.data = params.data ? {model : params.data} : {};
1063 | }
1064 |
1065 | // For older servers, emulate HTTP by mimicking the HTTP method with `_method`
1066 | // And an `X-HTTP-Method-Override` header.
1067 | if (Backbone.emulateHTTP) {
1068 | if (type === 'PUT' || type === 'DELETE') {
1069 | if (Backbone.emulateJSON) params.data._method = type;
1070 | params.type = 'POST';
1071 | params.beforeSend = function(xhr) {
1072 | xhr.setRequestHeader('X-HTTP-Method-Override', type);
1073 | };
1074 | }
1075 | }
1076 |
1077 | // Don't process data on a non-GET request.
1078 | if (params.type !== 'GET' && !Backbone.emulateJSON) {
1079 | params.processData = false;
1080 | }
1081 |
1082 | // Make the request.
1083 | return $.ajax(params);
1084 | };
1085 |
1086 | // Helpers
1087 | // -------
1088 |
1089 | // Shared empty constructor function to aid in prototype-chain creation.
1090 | var ctor = function(){};
1091 |
1092 | // Helper function to correctly set up the prototype chain, for subclasses.
1093 | // Similar to `goog.inherits`, but uses a hash of prototype properties and
1094 | // class properties to be extended.
1095 | var inherits = function(parent, protoProps, staticProps) {
1096 | var child;
1097 |
1098 | // The constructor function for the new subclass is either defined by you
1099 | // (the "constructor" property in your `extend` definition), or defaulted
1100 | // by us to simply call `super()`.
1101 | if (protoProps && protoProps.hasOwnProperty('constructor')) {
1102 | child = protoProps.constructor;
1103 | } else {
1104 | child = function(){ return parent.apply(this, arguments); };
1105 | }
1106 |
1107 | // Inherit class (static) properties from parent.
1108 | _.extend(child, parent);
1109 |
1110 | // Set the prototype chain to inherit from `parent`, without calling
1111 | // `parent`'s constructor function.
1112 | ctor.prototype = parent.prototype;
1113 | child.prototype = new ctor();
1114 |
1115 | // Add prototype properties (instance properties) to the subclass,
1116 | // if supplied.
1117 | if (protoProps) _.extend(child.prototype, protoProps);
1118 |
1119 | // Add static properties to the constructor function, if supplied.
1120 | if (staticProps) _.extend(child, staticProps);
1121 |
1122 | // Correctly set child's `prototype.constructor`.
1123 | child.prototype.constructor = child;
1124 |
1125 | // Set a convenience property in case the parent's prototype is needed later.
1126 | child.__super__ = parent.prototype;
1127 |
1128 | return child;
1129 | };
1130 |
1131 | // Helper function to get a URL from a Model or Collection as a property
1132 | // or as a function.
1133 | var getUrl = function(object) {
1134 | if (!(object && object.url)) return null;
1135 | return _.isFunction(object.url) ? object.url() : object.url;
1136 | };
1137 |
1138 | // Throw an error when a URL is needed, and none is supplied.
1139 | var urlError = function() {
1140 | throw new Error('A "url" property or function must be specified');
1141 | };
1142 |
1143 | // Wrap an optional error callback with a fallback error event.
1144 | var wrapError = function(onError, model, options) {
1145 | return function(resp) {
1146 | if (onError) {
1147 | onError(model, resp, options);
1148 | } else {
1149 | model.trigger('error', model, resp, options);
1150 | }
1151 | };
1152 | };
1153 |
1154 | // Helper function to escape a string for HTML rendering.
1155 | var escapeHTML = function(string) {
1156 | return string.replace(/&(?!\w+;|#\d+;|#x[\da-f]+;)/gi, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, ''').replace(/\//g,'/');
1157 | };
1158 |
1159 | }).call(this);
1160 |
--------------------------------------------------------------------------------