├── 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 |
2 | gravatar for <%= author %> 3 | <%= author %> @ <%= formatted_date %> 4 | • 5 | #<%= id %> 6 | <% if (can_delete) { %> 7 | Delete 8 | <% } %> 9 | 10 |
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 |

33 | 34 | 35 | 36 | Cancel 37 |
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 | ![Inline comment screenshot](https://github.com/Automattic/trac-code-comments-plugin/raw/master/screenshots/0.png) 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 |
33 | 39 | 45 | 46 |
47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 76 | 77 | 78 | 79 |
ID AuthorDatePathTextTicketsActions
$comment.id$comment.author${comment.formatted_date()}${comment.path_link_tag()}$comment.html${comment.get_ticket_links()} 71 |
72 | 73 | 74 |
75 |
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 | --------------------------------------------------------------------------------