Changeset 75

Show
Ignore:
Timestamp:
01/02/07 17:49:59 (2 years ago)
Author:
rgrp
Message:

Add support for retrieving list of annotations in atom format (format needed for marginalia) and integrate with marginalia.

  • sandbox/annotater/model.py: add list annotations method onto model.
    • list_annotations_html: crude method return query results as html.
    • list_annotations_atom: return annoations in a stripped-down version of the marginlia atom format for annotations (as defined in marginalia/annotation-db.php).
  • sandbox/annotater/model_test.py: add tests for new methods.
  • sandbox/annotater/annotater.py: index method now returns list of annotations in format determined by format parameter in query string.
  • sandbox/annotater/annotater_test.py: modify index test as a result of changes.
  • sandbox/annotater/marginalia/rest-annotate.js: small hack to get it working.
    • modify the post update request to put all info in url string as well as body because paste does not support application/x-www-form-urlencoded.
  • sandbox/annotater/marginalia/annotation.js: small hack to get retrieval of existing annotations working.
    • comment out test that quote in returned annotation matches current quote as we are not storing quotes yet.
  • sandbox/annotater/marginalia/index.html: change http://geof.net/ style urls to http://localhost:8080/.
Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • sandbox/annotater/annotater.py

    Revision 71 Revision 75
    1""" 1""" 
    2Annotation of a web resource. 2Annotation of a web resource. 
    3 3 
    4@copyright: (c) 2006 Open Knowledge Foundation 4@copyright: (c) 2006 Open Knowledge Foundation 
    5@author: Rufus Pollock (Open Knowledge Foundation) 5@author: Rufus Pollock (Open Knowledge Foundation) 
    6@license: MIT License <http://www.opensource.org/licenses/mit-license.php> 6@license: MIT License <http://www.opensource.org/licenses/mit-license.php> 
    7""" 7""" 
    8import os 8import os 
    9 9 
    10import wsgiref.simple_server 10import wsgiref.simple_server 
    11import paste.request 11import paste.request 
    12# import genshi.template 12# import genshi.template 
    13# import genshi.output 13# import genshi.output 
    14 14 
    15from routes import * 15from routes import * 
    16 16 
    17# annotater stuff 17# annotater stuff 
    18import model 18import model 
    19 19 
    20# absolute url to annotation service 20# absolute url to annotation service 
    21# this should go 21# this should go 
    22service_path = '/annotation' 22service_path = '/annotation' 
    23 23 
    24map = Mapper() 24map = Mapper() 
    25map.connect('annotation/delete/:id', controller='annotation', action='delete', 25map.connect('annotation/delete/:id', controller='annotation', action='delete', 
    26        conditions=dict(method=['GET'])) 26        conditions=dict(method=['GET'])) 
    27map.connect('annotation/edit/:id', controller='annotation', action='edit', 27map.connect('annotation/edit/:id', controller='annotation', action='edit', 
    28        conditions=dict(method=['GET'])) 28        conditions=dict(method=['GET'])) 
    29 29 
    30map.resource('annotation') 30map.resource('annotation') 
    31 31 
    32# map.resource assumes PUT for update but marginalias uses POST 32# map.resource assumes PUT for update but marginalias uses POST 
    33# the exacting mappings for REST seems a hotly contested matter see e.g. 33# the exacting mappings for REST seems a hotly contested matter see e.g. 
    34# http://www.megginson.com/blogs/quoderat/archives/2005/04/03/post-in-rest-create-update-or-action/ 34# http://www.megginson.com/blogs/quoderat/archives/2005/04/03/post-in-rest-create-update-or-action/ 
    35# must have this *after* map.resource as otherwise overrides the create action 35# must have this *after* map.resource as otherwise overrides the create action 
    36map.connect('annotation/:id', controller='annotation', action='update', 36map.connect('annotation/:id', controller='annotation', action='update', 
    37    conditions=dict(method=['POST'])) 37    conditions=dict(method=['POST'])) 
    38 38 
    39# misc config 39# misc config 
    40marginalia_path = os.path.abspath('./marginalia') 40marginalia_path = os.path.abspath('./marginalia') 
    41html_doc_path = os.path.join(marginalia_path, 'index.html') 41html_doc_path = os.path.join(marginalia_path, 'index.html') 
    42 42 
    43 43 
    44import logging 44import logging 
    45def setup_logging(): 45def setup_logging(): 
    46    level = logging.DEBUG 46    level = logging.DEBUG 
    47    logger = logging.getLogger('annotater') 47    logger = logging.getLogger('annotater') 
    48    logger.setLevel(level) 48    logger.setLevel(level) 
    49    log_file_path = 'debug.log' 49    log_file_path = 'debug.log' 
    50    fh = logging.FileHandler(log_file_path, 'w') 50    fh = logging.FileHandler(log_file_path, 'w') 
    51    fh.setLevel(level) 51    fh.setLevel(level) 
    52    logger.addHandler(fh) 52    logger.addHandler(fh) 
    53    logger.info('START LOGGING') 53    logger.info('START LOGGING') 
    54    return logger 54    return logger 
    55 55 
    56logger = setup_logging() 56logger = setup_logging() 
    57 57 
    58class AnnotaterApp(object): 58class AnnotaterApp(object): 
    59 59 
    60    def __init__(self): 60    def __init__(self): 
    61        pass 61        pass 
    62     62     
    63    def __call__(self, environ, start_response): 63    def __call__(self, environ, start_response): 
    64        self.environ = environ 64        self.environ = environ 
    65        self.map = map 65        self.map = map 
    66        self.map.environ = environ 66        self.map.environ = environ 
    67        self.start_response = start_response 67        self.start_response = start_response 
    68        self.path = environ['PATH_INFO'] 68        self.path = environ['PATH_INFO'] 
    69        logger.debug(self.path) 69        logger.debug(self.path) 
    70        # special test cases 70        # special test cases 
    71        if self.path.startswith('/debug'): 71        if self.path.startswith('/debug'): 
    72            return wsgiref.simple_server.demo_app(environ, start_response) 72            return wsgiref.simple_server.demo_app(environ, start_response) 
    73        elif self.path.startswith('/_js/'): 73        elif self.path.startswith('/_js/'): 
    74            status = '200 OK' 74            status = '200 OK' 
    75            response_headers = [('Content-type','text/plain')] 75            response_headers = [('Content-type','text/plain')] 
    76            start_response(status, response_headers) 76            start_response(status, response_headers) 
    77            jspath = os.path.join(marginalia_path, self.path[5:]) 77            jspath = os.path.join(marginalia_path, self.path[5:]) 
    78            jsfile = file(jspath).read() 78            jsfile = file(jspath).read() 
    79            return [jsfile] 79            return [jsfile] 
    80        elif self.path.endswith('.js') or self.path.endswith('.css'): 80        elif self.path.endswith('.js') or self.path.endswith('.css'): 
    81            status = '200 OK' 81            status = '200 OK' 
    82            if self.path.endswith('.js'): filetype = 'text/javascript' 82            if self.path.endswith('.js'): filetype = 'text/javascript' 
    83            else: filetype = 'text/css' 83            else: filetype = 'text/css' 
    84            response_headers = [('Content-type', filetype)] 84            response_headers = [('Content-type', filetype)] 
    85            start_response(status, response_headers) 85            start_response(status, response_headers) 
    86            jspath = os.path.join(marginalia_path, self.path[1:]) 86            jspath = os.path.join(marginalia_path, self.path[1:]) 
    87            jsfile = file(jspath).read() 87            jsfile = file(jspath).read() 
    88            return [jsfile] 88            return [jsfile] 
    89        elif self.path.startswith('/example-annotations.xml'): 89        elif self.path.startswith('/example-annotations.xml'): 
    90            status = '200 OK' 90            status = '200 OK' 
    91            filetype = 'text/xml' 91            filetype = 'text/xml' 
    92            response_headers = [('Content-type', filetype)] 92            response_headers = [('Content-type', filetype)] 
    93            start_response(status, response_headers) 93            start_response(status, response_headers) 
    94            jspath = os.path.join(marginalia_path, self.path[1:]) 94            jspath = os.path.join(marginalia_path, self.path[1:]) 
    95            jsfile = file(jspath).read() 95            jsfile = file(jspath).read() 
    96            return [jsfile] 96            return [jsfile] 
    97        elif self.path.startswith(service_path): 97        elif self.path.startswith(service_path): 
    98            return self.annotate() 98            return self.annotate() 
    99        else: 99        else: 
    100            logger.info('Call to base url /') 100            logger.info('Call to base url /') 
    101            status = '200 OK' 101            status = '200 OK' 
    102            response_headers = [('Content-type','text/html')] 102            response_headers = [('Content-type','text/html')] 
    103            start_response(status, response_headers) 103            start_response(status, response_headers) 
    104            out = file(html_doc_path).read() 104            out = file(html_doc_path).read() 
    105            return [out] 105            return [out] 
    106 106 
    107    def _make_annotate_form(self, form_name, action_url, form_defaults): 107    def _make_annotate_form(self, form_name, action_url, form_defaults): 
    108        from formencode import htmlfill 108        from formencode import htmlfill 
    109        keys = [ 'url' , 'range', 'note' ] 109        keys = [ 'url' , 'range', 'note' ] 
    110        vals = {} 110        vals = {} 
    111        for key in keys: 111        for key in keys: 
    112            vals[key] = form_defaults.get(key, '') 112            vals[key] = form_defaults.get(key, '') 
    113        formfields = '' 113        formfields = '' 
    114        for key in keys: 114        for key in keys: 
    115            formfields += \ 115            formfields += \ 
    116'''            <label for="%s">%s:</label><input name="%s" id="%s" /><br /> 116'''            <label for="%s">%s:</label><input name="%s" id="%s" /><br /> 
    117''' % (key, key, key, key) 117''' % (key, key, key, key) 
    118             118             
    119 119 
    120        form = \ 120        form = \ 
    121'''<html> 121'''<html> 
    122    <head></head> 122    <head></head> 
    123    <body> 123    <body> 
    124        <form name="%s" action="%s" method="POST"> 124        <form name="%s" action="%s" method="POST"> 
    125           %s 125           %s 
    126           <input type="submit" name="submission" value="send the form" /> 126           <input type="submit" name="submission" value="send the form" /> 
    127       </form> 127       </form> 
    128    </body> 128    </body> 
    129</html>''' % (form_name, action_url, formfields) 129</html>''' % (form_name, action_url, formfields) 
    130         130         
    131        form = htmlfill.render(form, vals) 131        form = htmlfill.render(form, vals) 
    132        return form 132        return form 
    133          133          
    134 134 
    135    def annotate(self): 135    def annotate(self): 
    136        query_vals = paste.request.parse_formvars(self.environ) 136        query_vals = paste.request.parse_formvars(self.environ) 
    137        request_method = self.environ['REQUEST_METHOD'] 137        request_method = self.environ['REQUEST_METHOD'] 
      138        mapdict = self.map.match(self.path) 
      139        format = query_vals.get('format', 'html') 
    138        logger.debug('CALL TO ANNOTATE') 140        logger.debug('CALL TO ANNOTATE') 
    139        logger.debug(self.path) 141        logger.debug(self.path) 
    140        logger.debug(query_vals) 142        logger.debug(query_vals) 
    141        logger.debug(self.environ['QUERY_STRING']) 143        logger.debug(self.environ['QUERY_STRING']) 
    142        logger.debug(request_method) 144        logger.debug(request_method) 
    143        mapdict = self.map.match(self.path)   
    144        logger.debug('mapdict: %s' % mapdict) 145        logger.debug('mapdict: %s' % mapdict) 
    145        action = mapdict['action'] 146        action = mapdict['action'] 
    146        anno_schema = model.AnnotationSchema() 147        anno_schema = model.AnnotationSchema() 
    147 148 
    148        if action == 'index': 149        if action == 'index': 
    149            status = '200 OK' 150            status = '200 OK' 
    150            response_headers = [  151             response_headers = [ ('Content-type', 'text/html') ] 
    151                    ('Content-type', 'text/plain'),  152             result = '' 
    152                    ]  153             if format == 'html': 
    153            items = list(model.Annotation.select())  154                 result = model.Annotation.list_annotations_html() 
    154            out = ''  155                 result = \ 
    155            for item in items:  156 '''<html> 
    156                out += str(item) + '\n\n'  157     <head> 
    157            self.start_response(status, response_headers)  158         <title>Annotations</title> 
    158            return [out]  159     </head> 
       160     <body> 
       161         %s 
       162     </body> 
       163 </html>''' % (result) 
       164             elif format == 'atom': 
       165                 response_headers = [ ('Content-type', 'application/xml') ] 
       166                 result = model.Annotation.list_annotations_atom() 
       167             else: 
       168                 status = '500 Internal server error' 
       169                 result = 'Unknown format: %s' % format 
       170             self.start_response(status, response_headers) 
       171             return [result] 
    159        elif action == 'new': 172        elif action == 'new': 
    160            status = '200 OK' 173            status = '200 OK' 
    161            response_headers = [ 174            response_headers = [ 
    162                    ('Content-type', 'text/html'), 175                    ('Content-type', 'text/html'), 
    163                    ] 176                    ] 
    164            posturl = self.map.generate(controller='annotation', action='create') 177            posturl = self.map.generate(controller='annotation', action='create') 
    165            form = \ 178            form = \ 
    166'''<html> 179'''<html> 
    167    <head></head> 180    <head></head> 
    168    <body> 181    <body> 
    169        <form name='annotation_create' action="%s" method="POST"> 182        <form name='annotation_create' action="%s" method="POST"> 
    170           <label>url:</label> <input name="url" id="url" /><br /> 183           <label>url:</label> <input name="url" id="url" /><br /> 
    171           <label>range:</label><input name="range" id="range" /><br /> 184           <label>range:</label><input name="range" id="range" /><br /> 
    172           <label>note:</label><input name="note" id="note" /><br /> 185           <label>note:</label><input name="note" id="note" /><br /> 
    173           <input type="submit" name="submission" value="send the form" /> 186           <input type="submit" name="submission" value="send the form" /> 
    174       </form> 187       </form> 
    175    </body> 188    </body> 
    176</html>''' % posturl 189</html>''' % posturl 
    177            self.start_response(status, response_headers) 190            self.start_response(status, response_headers) 
    178            return [ form ] 191            return [ form ] 
    179        elif action == 'create': 192        elif action == 'create': 
    180            url = query_vals.get('url') 193            url = query_vals.get('url') 
    181            range = query_vals.get('range', 'NO RANGE') 194            range = query_vals.get('range', 'NO RANGE') 
    182            note = query_vals.get('note', 'NO NOTE') 195            note = query_vals.get('note', 'NO NOTE') 
    183            anno = model.Annotation( 196            anno = model.Annotation( 
    184                    url=url, 197                    url=url, 
    185                    range=range, 198                    range=range, 
    186                    note=note) 199                    note=note) 
    187            status = '201 Created' 200            status = '201 Created' 
    188            location = '/annotation/%s' % anno.id 201            location = '/annotation/%s' % anno.id 
    189            response_headers = [ 202            response_headers = [ 
    190                    ('Content-type', 'text/html'), 203                    ('Content-type', 'text/html'), 
    191                    ('Location', location) 204                    ('Location', location) 
    192                    ] 205                    ] 
    193            self.start_response(status, response_headers) 206            self.start_response(status, response_headers) 
    194            return [''] 207            return [''] 
    195        elif action == 'edit': 208        elif action == 'edit': 
    196            id = mapdict['id'] 209            id = mapdict['id'] 
    197            try: 210            try: 
    198                id = int(id) 211                id = int(id) 
    199            except: 212            except: 
    200                status = '400 Bad Request' 213                status = '400 Bad Request' 
    201                response_headers = [ 214                response_headers = [ 
    202                    ('Content-type', 'text/html'), 215                    ('Content-type', 'text/html'), 
    203                    ] 216                    ] 
    204                self.start_response(status, response_headers) 217                self.start_response(status, response_headers) 
    205                msg = '<h1>400 Bad Request</h1><p>No such annotation #%s</p>' % id 218                msg = '<h1>400 Bad Request</h1><p>No such annotation #%s</p>' % id 
    206                return [msg] 219                return [msg] 
    207            anno = model.Annotation.get(id) 220            anno = model.Annotation.get(id) 
    208            posturl = self.map.generate(controller='annotation', 221            posturl = self.map.generate(controller='annotation', 
    209                    action='update', id=anno.id, method='POST') 222                    action='update', id=anno.id, method='POST') 
    210            print 'Post url:', posturl 223            print 'Post url:', posturl 
    211            form_defaults = anno_schema.from_python(anno) 224            form_defaults = anno_schema.from_python(anno) 
    212            form = self._make_annotate_form('annotate_edit', posturl, 225            form = self._make_annotate_form('annotate_edit', posturl, 
    213                    form_defaults) 226                    form_defaults) 
    214            status = '200 OK' 227            status = '200 OK' 
    215            response_headers = [ 228            response_headers = [ 
    216                    ('Content-type', 'text/html'), 229                    ('Content-type', 'text/html'), 
    217                    ] 230                    ] 
    218            self.start_response(status, response_headers) 231            self.start_response(status, response_headers) 
    219            return [ form ] 232            return [ form ] 
    220 233 
    221        elif action == 'update': 234        elif action == 'update': 
    222            id = mapdict['id'] 235            id = mapdict['id'] 
    223            try: 236            try: 
    224                id = int(id) 237                id = int(id) 
    225            except: 238            except: 
    226                status = '400 Bad Request' 239                status = '400 Bad Request' 
    227                response_headers = [ 240                response_headers = [ 
    228                    ('Content-type', 'text/html'), 241                    ('Content-type', 'text/html'), 
    229                    ] 242                    ] 
    230                self.start_response(status, response_headers) 243                self.start_response(status, response_headers) 
    231                msg = '<h1>400 Bad Request</h1><p>No such annotation #%s</p>' % id 244                msg = '<h1>400 Bad Request</h1><p>No such annotation #%s</p>' % id 
    232                return [msg] 245                return [msg] 
    233            new_values = dict(query_vals) 246            new_values = dict(query_vals) 
    234            new_values['id'] = id 247            new_values['id'] = id 
    235            del new_values['submission']  248             # if this comes from a form POST have to remove submission field 
       249             if new_values.has_key('submission'): 
       250                 del new_values['submission'] 
    236            anno_edited = anno_schema.to_python(new_values) 251            anno_edited = anno_schema.to_python(new_values) 
    237            status = '204 Updated' 252            status = '204 Updated' 
    238            response_headers = [] 253            response_headers = [] 
    239            self.start_response(status, response_headers) 254            self.start_response(status, response_headers) 
    240            return [''] 255            return [''] 
    241 256 
    242        elif action == 'delete': 257        elif action == 'delete': 
    243            id = mapdict['id'] 258            id = mapdict['id'] 
    244            if id is None: 259            if id is None: 
    245                status = '400 Bad Request' 260                status = '400 Bad Request' 
    246                response_headers = [ 261                response_headers = [ 
    247                    ('Content-type', 'text/html'), 262                    ('Content-type', 'text/html'), 
    248                    ] 263                    ] 
    249                self.start_response(status, response_headers) 264                self.start_response(status, response_headers) 
    250                return ['<h1>400 Bad Request</h1><p>Bad ID</p>'] 265                return ['<h1>400 Bad Request</h1><p>Bad ID</p>'] 
    251            else: 266            else: 
    252                status = '204 Deleted' 267                status = '204 Deleted' 
    253                response_headers = [] 268                response_headers = [] 
    254                try: 269                try: 
    255                    id = int(id) 270                    id = int(id) 
    256                    model.Annotation.delete(id) 271                    model.Annotation.delete(id) 
    257                    self.start_response(status, response_headers) 272                    self.start_response(status, response_headers) 
    258                    return [''] 273                    return [''] 
    259                except: 274                except: 
    260                    status = '500 Internal server error' 275                    status = '500 Internal server error' 
    261                    self.start_response(status, response_headers) 276                    self.start_response(status, response_headers) 
    262                    return ['<h1>500 Internal Server Error</h1>Delete failed'] 277                    return ['<h1>500 Internal Server Error</h1>Delete failed'] 
    263        else: 278        else: 
    264            status = '404 Not Found' 279            status = '404 Not Found' 
    265            response_headers = [ 280            response_headers = [ 
    266                    ('Content-type', 'text/plain'), 281                    ('Content-type', 'text/plain'), 
    267                    ] 282                    ] 
    268            self.start_response(status, response_headers) 283            self.start_response(status, response_headers) 
    269            return ['Not found or method not allowed'] 284            return ['Not found or method not allowed'] 
    270     285     
    271 286 
    272if __name__ == '__main__':  287if __name__ == '__main__':  
    273    app = AnnotaterApp() 288    app = AnnotaterApp() 
    274    import paste.httpserver 289    import paste.httpserver 
    275    paste.httpserver.serve(app) 290    paste.httpserver.serve(app) 
  • sandbox/annotater/annotater_test.py

    Revision 71 Revision 75
    1import os 1import os 
    2import shutil 2import shutil 
    3import tempfile 3import tempfile 
    4import commands 4import commands 
    5from StringIO import StringIO 5from StringIO import StringIO 
    6 6 
    7import twill 7import twill 
    8from twill import commands as web 8from twill import commands as web 
    9 9 
    10import annotater 10import annotater 
    11import model 11import model 
    12 12 
    13class TestMapper: 13class TestMapper: 
    14 14 
    15    def test_match_new(self): 15    def test_match_new(self): 
    16        annotater.map.environ = { 'REQUEST_METHOD' : 'GET' } 16        annotater.map.environ = { 'REQUEST_METHOD' : 'GET' } 
    17        out = annotater.map.match('/annotation/new') 17        out = annotater.map.match('/annotation/new') 
    18        assert out['action'] == 'new' 18        assert out['action'] == 'new' 
    19 19 
    20    def test_match_index(self): 20    def test_match_index(self): 
    21        annotater.map.environ = { 'REQUEST_METHOD' : 'GET' } 21        annotater.map.environ = { 'REQUEST_METHOD' : 'GET' } 
    22        out = annotater.map.match('/annotation') 22        out = annotater.map.match('/annotation') 
    23        assert out['action'] == 'index' 23        assert out['action'] == 'index' 
    24 24 
    25    def test_match_create(self): 25    def test_match_create(self): 
    26        annotater.map.environ = { 'REQUEST_METHOD' : 'POST' } 26        annotater.map.environ = { 'REQUEST_METHOD' : 'POST' } 
    27        out = annotater.map.match('/annotation') 27        out = annotater.map.match('/annotation') 
    28        assert out['action'] == 'create' 28        assert out['action'] == 'create' 
    29 29 
    30    def test_match_delete(self): 30    def test_match_delete(self): 
    31        annotater.map.environ = { 'REQUEST_METHOD' : 'GET' } 31        annotater.map.environ = { 'REQUEST_METHOD' : 'GET' } 
    32        out = annotater.map.match('/annotation/delete/1') 32        out = annotater.map.match('/annotation/delete/1') 
    33        assert out['action'] == 'delete' 33        assert out['action'] == 'delete' 
    34        assert out['id'] == '1' 34        assert out['id'] == '1' 
    35        annotater.map.environ = { 'REQUEST_METHOD' : 'DELETE' } 35        annotater.map.environ = { 'REQUEST_METHOD' : 'DELETE' } 
    36        out = annotater.map.match('/annotation/1') 36        out = annotater.map.match('/annotation/1') 
    37        assert out['action'] == 'delete' 37        assert out['action'] == 'delete' 
    38        assert out['id'] == '1' 38        assert out['id'] == '1' 
    39        out = annotater.map.match('/annotation/') 39        out = annotater.map.match('/annotation/') 
    40        assert out['id'] == None 40        assert out['id'] == None 
    41 41 
    42    def test_match_delete(self): 42    def test_match_delete(self): 
    43        annotater.map.environ = { 'REQUEST_METHOD' : 'GET' } 43        annotater.map.environ = { 'REQUEST_METHOD' : 'GET' } 
    44        out = annotater.map.match('/annotation/edit/1') 44        out = annotater.map.match('/annotation/edit/1') 
    45        assert out['action'] == 'edit' 45        assert out['action'] == 'edit' 
    46        assert out['id'] == '1' 46        assert out['id'] == '1' 
    47        annotater.map.environ = { 'REQUEST_METHOD' : 'PUT' } 47        annotater.map.environ = { 'REQUEST_METHOD' : 'PUT' } 
    48        out = annotater.map.match('/annotation/1') 48        out = annotater.map.match('/annotation/1') 
    49        assert out['action'] == 'update' 49        assert out['action'] == 'update' 
    50        assert out['id'] == '1' 50        assert out['id'] == '1' 
    51        annotater.map.environ = { 'REQUEST_METHOD' : 'POST' } 51        annotater.map.environ = { 'REQUEST_METHOD' : 'POST' } 
    52        out = annotater.map.match('/annotation/1') 52        out = annotater.map.match('/annotation/1') 
    53        assert out['action'] == 'update' 53        assert out['action'] == 'update' 
    54 54 
    55    def test_url_for_new(self): 55    def test_url_for_new(self): 
    56        offset = annotater.map.generate(controller='annotation', action='new') 56        offset = annotater.map.generate(controller='annotation', action='new') 
    57        exp = '/annotation/new' 57        exp = '/annotation/new' 
    58        assert offset == exp 58        assert offset == exp 
    59 59 
    60    def test_url_for_create(self): 60    def test_url_for_create(self): 
    61        offset = annotater.map.generate(controller='annotation', action='create', 61        offset = annotater.map.generate(controller='annotation', action='create', 
    62                method='POST' ) 62                method='POST' ) 
    63        exp = '/annotation' 63        exp = '/annotation' 
    64        assert offset == exp 64        assert offset == exp 
    65 65 
    66    def test_url_for_delete(self): 66    def test_url_for_delete(self): 
    67        offset = annotater.map.generate(controller='annotation', 67        offset = annotater.map.generate(controller='annotation', 
    68                action='delete', id=1, method='GET' ) 68                action='delete', id=1, method='GET' ) 
    69        exp = '/annotation/delete/1' 69        exp = '/annotation/delete/1' 
    70        assert offset == exp 70        assert offset == exp 
    71        offset = annotater.map.generate(controller='annotation', 71        offset = annotater.map.generate(controller='annotation', 
    72                action='delete', id=1, method='DELETE' ) 72                action='delete', id=1, method='DELETE' ) 
    73        exp = '/annotation/1' 73        exp = '/annotation/1' 
    74        assert offset == exp 74        assert offset == exp 
    75 75 
    76    def test_url_for_edit(self): 76    def test_url_for_edit(self): 
    77        offset = annotater.map.generate(controller='annotation', 77        offset = annotater.map.generate(controller='annotation', 
    78                action='edit', id=1, method='GET') 78                action='edit', id=1, method='GET') 
    79        exp = '/annotation/edit/1' 79        exp = '/annotation/edit/1' 
    80        assert offset == exp 80        assert offset == exp 
    81 81 
    82class TestStatic: 82class TestStatic: 
    83     83     
    84    def test__make_annotate_form(self): 84    def test__make_annotate_form(self): 
    85        app = annotater.AnnotaterApp() 85        app = annotater.AnnotaterApp() 
    86        defaults = { 'url' : 'http://www.blackandwhite.com' } 86        defaults = { 'url' : 'http://www.blackandwhite.com' } 
    87        out = app._make_annotate_form(form_name='formname', action_url='.', 87        out = app._make_annotate_form(form_name='formname', action_url='.', 
    88                form_defaults=defaults) 88                form_defaults=defaults) 
    89        exp1 = '<label for="url">url:</label><input name="url" id="url" \ 89        exp1 = '<label for="url">url:</label><input name="url" id="url" \ 
    90value="%s" /><br />' % defaults['url'] 90value="%s" /><br />' % defaults['url'] 
    91        assert exp1 in out 91        assert exp1 in out 
    92 92 
    93 93 
    94class TestWsgi: 94class TestWsgi: 
    95 95 
    96    # disabled = True 96    # disabled = True 
    97 97 
    98    def setup_method(self, name=''): 98    def setup_method(self, name=''): 
    99        wsgi_app = annotater.AnnotaterApp() 99        wsgi_app = annotater.AnnotaterApp() 
    100        twill.add_wsgi_intercept('localhost', 8080, lambda : wsgi_app) 100        twill.add_wsgi_intercept('localhost', 8080, lambda : wsgi_app) 
    101        self.outp = StringIO() 101        self.outp = StringIO() 
    102        twill.set_output(self.outp) 102        twill.set_output(self.outp) 
    103        self.siteurl = 'http://localhost:8080/' 103        self.siteurl = 'http://localhost:8080/' 
    104 104 
    105    def teardown_method(self, name=''): 105    def teardown_method(self, name=''): 
    106        # remove intercept. 106        # remove intercept. 
    107        twill.remove_wsgi_intercept('localhost', 8080) 107        twill.remove_wsgi_intercept('localhost', 8080) 
    108 108 
    109    def test_js(self): 109    def test_js(self): 
    110        filename = 'domutil.js' 110        filename = 'domutil.js' 
    111        url = self.siteurl + '_js/' + filename 111        url = self.siteurl + '_js/' + filename 
    112        web.go(url) 112        web.go(url) 
    113        web.code(200) 113        web.code(200) 
    114        web.find('ELEMENT_NODE = 1;') 114        web.find('ELEMENT_NODE = 1;') 
    115 115 
    116    def test_js_2(self): 116    def test_js_2(self): 
    117        filename = 'domutil.js' 117        filename = 'domutil.js' 
    118        url = self.siteurl + filename 118        url = self.siteurl + filename 
    119        web.go(url) 119        web.go(url) 
    120        web.code(200) 120        web.code(200) 
    121        web.find('ELEMENT_NODE = 1;') 121        web.find('ELEMENT_NODE = 1;') 
    122 122 
    123    def test_show_root(self): 123    def test_show_root(self): 
    124        web.go(self.siteurl) 124        web.go(self.siteurl) 
    125        web.code(200) 125        web.code(200) 
    126        web.find('This is a demonstration of') 126        web.find('This is a demonstration of') 
    127 127 
    128    def test_annotate_get(self): 128    def test_annotate_get(self): 
    129        anno = model.Annotation( 129        anno = self._create_annotation() 
    130                url='http://xyz.com',   
    131                range='blah range',   
    132                note='blah note',   
    133                )   
    134        offset = annotater.map.generate(controller='annotation', action='index') 130        offset = annotater.map.generate(controller='annotation', action='index') 
    135        url = self.siteurl + offset[1:] 131        url = self.siteurl + offset[1:] 
    136        web.go(url) 132        web.go(url) 
    137        web.code(200) 133        web.code(200) 
    138        web.find(anno.url) 134        web.find(anno.url) 
    139        web.find(anno.range) 135        web.find(anno.range) 
      136 
      137    def test_annotate_get_atom(self): 
      138        anno = self._create_annotation() 
      139        offset = annotater.map.generate(controller='annotation', action='index') 
      140        url = self.siteurl + offset[1:] + '?format=atom' 
      141        web.go(url) 
      142        web.code(200) 
      143        web.find(anno.note) 
      144        web.find(anno.range) 
      145        out = web.show() 
      146        exp1 = '<feed xmlns:ptr="http://www.geof.net/code/annotation/"' 
      147        assert exp1 in out 
    140 148 
    141    def test_annotate_new(self): 149    def test_annotate_new(self): 
    142        # exercises both create and new 150        # exercises both create and new 
    143        import model 151        import model 
    144        model.rebuilddb() 152        model.rebuilddb() 
    145        offset = annotater.map.generate(controller='annotation', action='new', 153        offset = annotater.map.generate(controller='annotation', action='new', 
    146                method='GET') 154                method='GET') 
    147        url = self.siteurl + offset[1:] 155        url = self.siteurl + offset[1:] 
    148        web.go(url) 156        web.go(url) 
    149        web.code(200) 157        web.code(200) 
    150        note = 'any old thing' 158        note = 'any old thing' 
    151        web.fv('', 'url', 'http://localhost/') 159        web.fv('', 'url', 'http://localhost/') 
    152        web.fv('', 'note', note) 160        web.fv('', 'note', note) 
    153        web.submit() 161        web.submit() 
    154        web.code(201) 162        web.code(201) 
    155        # TODO make this test more selective 163        # TODO make this test more selective 
    156        items = model.Annotation.select() 164        items = model.Annotation.select() 
    157        items = list(items) 165        items = list(items) 
    158        assert len(items) == 1 166        assert len(items) == 1 
    159        assert items[0].note == note 167        assert items[0].note == note 
    160 168 
    161    def test_annotate_delete(self): 169    def test_annotate_delete(self): 
    162        anno = model.Annotation( 170        anno = self._create_annotation() 
    163                url='http://xyz.com',   
    164                range='blah range',   
    165                note='blah note',   
    166                )   
    167        offset = annotater.map.generate(controller='annotation', action='delete', 171        offset = annotater.map.generate(controller='annotation', action='delete', 
    168                id=anno.id) 172                id=anno.id) 
    169        url = self.siteurl + offset[1:] 173        url = self.siteurl + offset[1:] 
    170        web.go(url) 174        web.go(url) 
    171        web.code(204) 175        web.code(204) 
    172        tmp = model.Annotation.select(model.Annotation.q.id == anno.id) 176        tmp = model.Annotation.select(model.Annotation.q.id == anno.id) 
    173        num = len(list(tmp)) 177        num = len(list(tmp)) 
    174        assert num == 0 178        assert num == 0 
    175     179     
    176    def _create_annotation(self): 180    def _create_annotation(self): 
    177        anno = model.Annotation( 181        anno = model.Annotation( 
    178                url='http://xyz.com', 182                url='http://xyz.com', 
    179                range='blah range', 183                range='1.0 2.0', 
    180                note='blah note', 184                note='blah note', 
    181                ) 185                ) 
    182        return anno 186        return anno 
    183 187 
    184    def test_annotate_edit(self): 188    def test_annotate_edit(self): 
    185        anno = self._create_annotation() 189        anno = self._create_annotation() 
    186        offset = annotater.map.generate(controller='annotation', action='edit', 190        offset = annotater.map.generate(controller='annotation', action='edit', 
    187                id=anno.id, method='GET') 191                id=anno.id, method='GET') 
    188        url = self.siteurl + offset[1:] 192        url = self.siteurl + offset[1:] 
    189        web.go(url) 193        web.go(url) 
    190        web.code(200) 194        web.code(200) 
    191        newnote = u'This is a NEW note, a NEW note I say.' 195        newnote = u'This is a NEW note, a NEW note I say.' 
    192        web.fv('', 'note', newnote) 196        web.fv('', 'note', newnote) 
    193        web.submit() 197        web.submit() 
    194        web.code(204) 198        web.code(204) 
    195        assert anno.note == newnote 199        assert anno.note == newnote 
    196     200     
    197    def test_not_found(self): 201    def test_not_found(self): 
    198        offset = annotater.map.generate(controller='annotation') 202        offset = annotater.map.generate(controller='annotation') 
    199        url = self.siteurl + offset[1:] + '/blah' 203        url = self.siteurl + offset[1:] + '/blah' 
    200        web.go(url) 204        web.go(url) 
    201        web.code(404) 205        web.code(404) 
    202 206 
    203    def test_bad_request(self): 207    def test_bad_request(self): 
    204        offset = annotater.map.generate(controller='annotation', action='edit', 208        offset = annotater.map.generate(controller='annotation', action='edit', 
    205                method='GET') 209                method='GET') 
    206        url = self.siteurl + offset[1:] 210        url = self.siteurl + offset[1:] 
    207        web.go(url) 211        web.go(url) 
    208        web.code(400) 212        web.code(400) 
    209         213         
    210 214 
  • sandbox/annotater/marginalia/annotation.js

    Revision 70 Revision 75
    1/* 1/* 
    2 * annotation.js 2 * annotation.js 
    3 * 3 * 
    4 * Web Annotation is being developed for Moodle with funding from BC Campus  4 * Web Annotation is being developed for Moodle with funding from BC Campus  
    5 * and support from Simon Fraser University and SFU's Applied Communication 5 * and support from Simon Fraser University and SFU's Applied Communication 
    6 * Technologies Group and the e-Learning Innovation Centre of the 6 * Technologies Group and the e-Learning Innovation Centre of the 
    7 * Learning Instructional Development Centre at SFU 7 * Learning Instructional Development Centre at SFU 
    8 * Copyright (C) 2005 Geoffrey Glass www.geof.net 8 * Copyright (C) 2005 Geoffrey Glass www.geof.net 
    9 *  9 *  
    10 * This program is free software; you can redistribute it and/or 10 * This program is free software; you can redistribute it and/or 
    11 * modify it under the terms of the GNU General Public License 11 * modify it under the terms of the GNU General Public License 
    12 * as published by the Free Software Foundation; either version 2 12 * as published by the Free Software Foundation; either version 2 
    13 * of the License, or (at your option) any later version. 13 * of the License, or (at your option) any later version. 
    14 * 14 * 
    15 * This program is distributed in the hope that it will be useful, 15 * This program is distributed in the hope that it will be useful, 
    16 * but WITHOUT ANY WARRANTY; without even the implied warranty of 16 * but WITHOUT ANY WARRANTY; without even the implied warranty of 
    17 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 17 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
    18 * GNU General Public License for more details. 18 * GNU General Public License for more details. 
    19 * 19 * 
    20 * You should have received a copy of the GNU General Public License 20 * You should have received a copy of the GNU General Public License 
    21 * along with this program; if not, write to the Free Software 21 * along with this program; if not, write to the Free Software 
    22 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA. 22 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA. 
    23 */ 23 */ 
    24 24 
    25// namespaces 25// namespaces 
    26NS_PTR = 'http://www.geof.net/code/annotation/'; 26NS_PTR = 'http://www.geof.net/code/annotation/'; 
    27NS_ATOM = 'http://www.w3.org/2005/Atom'; 27NS_ATOM = 'http://www.w3.org/2005/Atom'; 
    28 28 
    29// The names of HTML/CSS classes used by the annotation code. 29// The names of HTML/CSS classes used by the annotation code. 
    30AN_NOTES_CLASS = 'notes';                       // the notes portion of a fragment 30AN_NOTES_CLASS = 'notes';                       // the notes portion of a fragment 
    31AN_HIGHLIGHT_CLASS = 'annotation';// class given to em nodes for highlighting 31AN_HIGHLIGHT_CLASS = 'annotation';// class given to em nodes for highlighting 
    32AN_HOVER_CLASS = 'hover';                       // assigned to highlights and notes when the mouse is over the other 32AN_HOVER_CLASS = 'hover';                       // assigned to highlights and notes when the mouse is over the other 
    33AN_ANNOTATED_CLASS = 'annotated';       // class added to fragment when annotation is on 33AN_ANNOTATED_CLASS = 'annotated';       // class added to fragment when annotation is on 
    34AN_SELFANNOTATED_CLASS = 'self-annotated';  // annotations are by the current user (and therefore editable) 34AN_SELFANNOTATED_CLASS = 'self-annotated';  // annotations are by the current user (and therefore editable) 
    35AN_DUMMY_CLASS = 'dummy';                       // used for dummy item in note list 35AN_DUMMY_CLASS = 'dummy';                       // used for dummy item in note list 
    36AN_RANGEMISMATCH_ERROR_CLASS = 'annotation-range-mismatch';     // one or more annotations don't match the current state of the document 36AN_RANGEMISMATCH_ERROR_CLASS = 'annotation-range-mismatch';     // one or more annotations don't match the current state of the document 
    37AN_ID_PREFIX = 'a';                                     // prefix for annotation IDs in element classes and IDs 37AN_ID_PREFIX = 'a';                                     // prefix for annotation IDs in element classes and IDs 
    38AN_SUN_SYMBOL = '\u25cb'; //'\u263c'; 38AN_SUN_SYMBOL = '\u25cb'; //'\u263c'; 
    39AN_MOON_SYMBOL = '\u25c6'; //'\u2641'; 39AN_MOON_SYMBOL = '\u25c6'; //'\u2641'; 
    40 40 
    41// Length limits 41// Length limits 
    42MAX_QUOTE_LENGTH = 1000; 42MAX_QUOTE_LENGTH = 1000; 
    43MAX_NOTE_LENGTH = 250; 43MAX_NOTE_LENGTH = 250; 
    44<