Add RESTful HTML interface.
authorW. Trevor King <wking@drexel.edu>
Tue, 20 Jul 2010 16:16:21 +0000 (12:16 -0400)
committerW. Trevor King <wking@drexel.edu>
Tue, 20 Jul 2010 16:16:21 +0000 (12:16 -0400)
Since I was having problems getting the XUL interface working on my
server.  It works on my development box though...  Strange.

Anyhow, the RESTful interface is less like a desktop application, but
it is less annoying and more robust than XUL+JavaScript.

dirtag/__init__.py
dirtag/web.py
template/dir.html [new file with mode: 0644]
template/dirtag.html [new file with mode: 0644]
template/file.html [new file with mode: 0644]

index 7ad96456b65aacd4cbd749c06b1d29c1522bbfbf..e572446d17997f273996b84ec30b8e73a2b283cd 100644 (file)
@@ -75,15 +75,18 @@ elif sys.version_info < (2, 6, 0, 'alpha', 0):  # Workarounds for 2.5
 
 
 class Node (list):
-    def __init__(self, root, type, tags, *args, **kwargs):
+    def __init__(self, tree, root, *args, **kwargs):
         list.__init__(self, *args, **kwargs)
+        self.tree = tree
         self.root = root
-        self.type = type
-        self.tags = tags
+        if isinstance(tree, Tree):
+            self.type = 'dir'
+        else:
+            self.type = 'file'
 
     def pre_extend(self, a):
         """
-        >>> n = Node(None, None, None, ['a', 'b', 'c'])
+        >>> n = Node(None, None, ['a', 'b', 'c'])
         >>> n.pre_extend([1, 2, 3])
         >>> print n
         [1, 2, 3, 'a', 'b', 'c']
@@ -98,16 +101,31 @@ class Tree (list):
     >>> t.append(Tree('1'))
     >>> t.append(Tree('2'))
     >>> t[0].append(Tree('A'))
+    >>> t[0].append('x')
     >>> print '\\n'.join(['|'.join(x) for x in t.traverse()])
     a
     a|1
     a|1|A
+    a|1|x
     a|2
     >>> print '\\n'.join(['|'.join(x) for x in t.traverse(prefix=['z'])])
     z|a
     z|a|1
     z|a|1|A
+    z|a|1|x
     z|a|2
+    >>> print '\\n'.join([x for x in t.hook_out(
+    ...    on_open_dir=lambda node, data : ' '*len(node) + 'opening %s' % '/'.join(node),
+    ...    on_close_dir=lambda node, data : ' '*len(node) + 'closing %s' % '/'.join(node),
+    ...    on_empty_dir=lambda node, data : ' '*len(node) + 'empty %s' % '/'.join(node),
+    ...    on_file=lambda node, data : ' '*len(node) + 'file %s' % '/'.join(node),)])
+     opening a
+      opening a/1
+       empty a/1/A
+       file a/1/x
+      closing a/1
+      empty a/2
+     closing a
     """
     def __init__(self, data=None, root=None, *args, **kwargs):
         self.data = data
@@ -117,18 +135,64 @@ class Tree (list):
     def traverse(self, prefix=[], depth=0, type='both', dirtag=None):
         p = prefix + [self.data]
         if depth < len(p) and type in ['both', 'dirs']:
-            yield Node(self.root, 'dir', [], p[depth:])
+            n = Node(self, self.root, p[depth:])
+            n.tags = []
+            yield n
         for child in self:
             if hasattr(child, 'traverse'):
                 for grandchild in child.traverse(
                     p, depth=depth, type=type, dirtag=dirtag):
                     yield grandchild
             elif depth <= len(p) and type in ['both', 'files']:
-                n = Node(self.root, 'file', [], (p + [child])[depth:])
+                n = Node(None, self.root, (p + [child])[depth:])
                 if dirtag != None:
-                    n.tags = dirtag.node_tags(n)
+                    n.tags = list(dirtag.node_tags(n))
                 yield n
 
+    def hook_out(self, on_open_dir=None, on_close_dir=None,
+                 on_empty_dir=None, on_file=None, data=None,
+                 **kwargs):
+        stack = []
+        for node in self.traverse(**kwargs):
+            while (len(stack) > 0 and
+                   not self._list_startswith(stack[-1], node[:-1])):
+                if on_close_dir != None:
+                    yield on_close_dir(stack[-1], data)
+                stack.pop()
+            assert len(node) == len(stack)+1, (
+                'Floating %s -> %s (%d -> %d)'
+                % (stack[-1], node, len(stack), len(node)))
+            stack.append(node)
+            if node.type == 'file':
+                if on_file != None:
+                    yield on_file(node, data)
+                stack.pop()
+            elif node.type == 'dir' and len(node.tree) == 0:
+                if on_empty_dir != None:
+                    yield on_empty_dir(node, data)
+                stack.pop()
+            else:
+                if on_open_dir != None:
+                    yield on_open_dir(node, data)
+        while len(stack) > 0:
+            if on_close_dir != None:
+                yield on_close_dir(stack[-1], data)
+            stack.pop()
+
+    def _list_startswith(self, a, b):
+        """
+        >>> t = Tree('a')
+        >>> t._list_startswith([1, 2, 3], [1, 2, 3, 4])
+        True
+        >>> t._list_startswith([1, 2, 3], [1, 2, 3])
+        True
+        >>> t._list_startswith([1, 2, 3], [1, 2])
+        False
+        >>> t._list_startswith([1, 2, 3], [2, 2, 3])
+        False
+        """
+        return len([x for x,y in zip(a,b) if x==y]) == len(a)
+
 
 def dir_tree(root_dir):
     """Generate a directory tree.
@@ -191,12 +255,15 @@ class Dirtag (object):
     x|y|b1.html
     x|a2.html
     x|b3.html
+    >>> print '\\n'.join(['|'.join(x) for x in d.tags()])
+    1
+    x
+    x|y
     >>> print '\\n'.join(['|'.join(x) for x in d.tags(['b', 'b2.html'])])
     <BLANKLINE>
     >>> print '\\n'.join(['|'.join(x) for x in d.tags(['b', 'b1.html'])])
     1
     x|y
-
     >>> print d.tag_path(['a', 'a3.html'], ['x', 'y'])
     test/tag/x/y/a3.html
     >>> os.listdir('test/tag/x/y')
@@ -226,12 +293,16 @@ class Dirtag (object):
             node.pre_extend(tag)
             yield node
 
-    def tags(self, target):
-        for node in dir_tree(self.tag_dir).traverse(depth=1):
-            p = os.path.join(*([self.tag_dir]+node))
-            t = os.path.abspath(os.path.join(*([self.raw_dir] + target)))
-            if (os.path.islink(p) and os.path.realpath(p) == t):
-                yield os.path.join(node[:-1])
+    def tags(self, target=None):
+        if target == None:
+            for node in dir_tree(self.tag_dir).traverse(depth=1, type='dirs'):
+                yield os.path.join(node)
+        else:
+            for node in dir_tree(self.tag_dir).traverse(depth=1):
+                p = os.path.join(*([self.tag_dir]+node))
+                t = os.path.abspath(os.path.join(*([self.raw_dir] + target)))
+                if (os.path.islink(p) and os.path.realpath(p) == t):
+                    yield os.path.join(node[:-1])
 
     def tag_path(self, target, tag):
         # TODO: assumes unique basenames.  Check?
index 3826022493ff72deaf56ab077dd6d323fd5cae2a..e676e044d7b4f3affbb2b02a7c19bc658b601e82 100755 (executable)
@@ -14,15 +14,164 @@ from dirtag import Dirtag, Tree, dir_tree
 class WebInterface:
     """The web interface to Dirtag.
     """
-    def __init__(self, dirtag, template_dir, repository_name='Dirtag'):
+    def __init__(self, dirtag, template_dir='template',
+                 repository_name='Dirtag'):
         """Initialize the bug repository for this web interface."""
         self.dirtag = dirtag
         self.env = Environment(loader=FileSystemLoader(template_dir))
         self.repository_name = repository_name
         self.rdf_root = 'http://dirtag.com/'
 
+    # RESTful HTML interface
+
     @cherrypy.expose
     def index(self):
+        template = self.env.get_template('dirtag.html')
+        return template.render(
+            repository_name=self.repository_name,
+            raw_html=self._dir_html(self.dirtag.raw_dir),
+            tag_html=self._dir_html(self.dirtag.tag_dir),
+            )
+
+    def _dir_html(self, root_dir):
+        """
+        >>> x = WebInterface(None)
+        >>> print x._dir_html('test/tag')
+        <ul>
+          <li><a href="">1</a></li>
+        <BLANKLINE>
+          <li>x
+            <ul>
+              <li><a href="">x/y</a></li>
+        <BLANKLINE>
+            </ul>
+          </li>
+        </ul>
+        """
+        lines = ['<ul>']
+        lines.extend([x for x in dir_tree(root_dir).hook_out(
+                    depth=1, type='dirs',
+                    on_open_dir=self._dir_html_on_open_dir,
+                    on_close_dir=self._dir_html_on_close_dir,
+                    on_empty_dir=self._dir_html_on_empty_dir,
+                    )])
+        lines.append('</ul>')
+        return '\n'.join(lines)
+
+    def _dir_html_on_open_dir(self, node, data):
+        if len([x for x in node.tree if isinstance(x, Tree)]) > 0:
+            return ('%s<li>%s\n%s<ul>'
+                    % (' '*(4*len(node)-2),
+                       self._dir_html_link(node, data),
+                       ' '*(4*len(node))))
+        return self._dir_html_on_empty_dir(node, data)
+        
+    def _dir_html_on_close_dir(self, node, data):
+        if len([x for x in node.tree if isinstance(x, Tree)]) > 0:
+            return ('%s</ul>\n%s</li>'
+                    % (' '*(4*len(node)), ' '*(4*len(node)-2)))
+        return ''
+
+    def _dir_html_on_empty_dir(self, node, data):
+        return ('%s<li>%s</li>'
+                % (' '*(4*len(node)-2), self._dir_html_link(node, data)))
+
+    def _dir_html_link(self, node, data):
+        return ('<a href="%s">%s</a>'
+                % ('dir?%s' % urlencode({
+                        'root':node.root,
+                        'selected':'/'.join(node),
+                        }),
+                   '/'.join(node)))
+
+    @cherrypy.expose
+    def dir(self, root, selected):
+        s = self._selected_dir(root, selected)
+        template = self.env.get_template('dir.html')
+
+        return template.render(
+            repository_name=self.repository_name,
+            files=[(f[-1],
+                    '/'.join(f),
+                    self._dir_tags(f),
+                    'file?%s' % urlencode({'selected':'/'.join(
+                                self.dirtag.raw_node(f))}))
+                   for f in s.tree.traverse(prefix=[s.root]+s[:-1],
+                                            depth=1,
+                                            type='files',
+                                            dirtag=self.dirtag)
+                   if len(f) == len(s)+1])
+
+    def _dir_tags(self, node):
+        if len(node.tags) > 0:
+            return ','.join(['/'.join(t) for t in node.tags])
+        else:
+            return '-'
+
+    @cherrypy.expose
+    def file(self, selected):
+        # Disable form value caching in Firefox.  See
+        #   http://www.mozilla.org/projects/netlib/http/http-caching-faq.html
+        cherrypy.response.headers['Cache-Control'] = 'no-store'
+        s = self._selected_file(dir_tree(self.dirtag.raw_dir), selected)
+        template = self.env.get_template('file.html')
+        return template.render(
+            repository_name=self.repository_name,
+            selected_path=selected,
+            selected_url='static/raw/'+selected,
+            tags=[('/'.join(t), t in s.tags) for t in self.dirtag.tags()],
+            )
+
+    def _selected_dir(self, root, selected):
+        if root == self.dirtag.raw_dir:
+            tree = dir_tree(self.dirtag.raw_dir)
+        else:
+            assert root == self.dirtag.tag_dir, root
+            tree = dir_tree(self.dirtag.tag_dir)
+        s = None
+        for node in tree.traverse(depth=1, type='dirs'):
+            if '/'.join(node) == selected:
+                s = node
+                break
+        assert s != None, selected
+        assert s.tree != None, selected
+        return s
+
+    def _selected_file(self, tree, selected):
+        s = None
+        for node in tree.traverse(depth=1, type='files', dirtag=self.dirtag):
+            if '/'.join(node) == selected:
+                s = node
+                break
+        assert s != None, selected
+        assert s.tree == None, selecte
+        return s
+
+    # HTML user input methods
+
+    @cherrypy.expose
+    def set_tags(self, path, tags=[], selected=None):
+        s = self._selected_file(dir_tree(self.dirtag.raw_dir), path)
+        old_tags = list(s.tags)
+        path = path.split('/')
+        selected_tags = [tag.split('/') for tag in tags]
+        for tag in self.dirtag.tags():
+            if tag in selected_tags:
+                if tag not in old_tags:
+                    self.dirtag.add_tag(path, tag)
+            else:
+                if tag in old_tags:
+                    self.dirtag.remove_tag(path, tag)
+        if selected != None:
+            raise cherrypy.HTTPRedirect(
+                'file?%s' % urlencode({'selected':selected}),
+                status=302)
+        return '<p>Removed tag %s from %s</p>' % (tag, path)
+
+    # XUL interface
+
+    @cherrypy.expose
+    def dirtag_xul(self):
         template = self.env.get_template('dirtag.xul')
         cherrypy.response.headers['Content-Type'] = \
             'application/vnd.mozilla.xul+xml'
@@ -63,37 +212,20 @@ class WebInterface:
 
         lines.append('  <RDF:Seq RDF:about="%s%s/files">'
                      % (self.rdf_root, name))
-        stack = []
-        for node in tree.traverse(depth=1,):
-            while (len(stack) > 0 and
-                   not self.list_startswith(stack[-1], node[:-1])):
-                lines.extend([
-                        ' '*(4*len(stack)+2) + '</RDF:Seq>',
-                        ' '*(4*len(stack)) + '</RDF:li>',
-                        ])
-                stack.pop()
-            assert len(node) == len(stack)+1, (
-                'Floating %s -> %s (%d -> %d)'
-                % (stack[-1], node, len(stack), len(node)))
-            stack.append(node)
-            if node.type == 'file':
-                raw_node = self.dirtag.raw_node(node)
-                lines.append(' '*(4*len(stack))
-                             + '<RDF:li RDF:resource="%s%s"/>'
-                             % (self.rdf_root, '/'.join(['raw']+raw_node)))
-                stack.pop()
-            else:
-                lines.extend([
-                        ' '*(4*len(stack)) + '<RDF:li>',
-                        ' '*(4*len(stack)+2) + '<RDF:Seq RDF:about="%s%s">'
-                        % (self.rdf_root, '/'.join([name]+node)),
-                        ])
-        while len(stack) > 0:
-            lines.extend([
-                    ' '*(4*len(stack)+2) + '</RDF:Seq>',
-                    ' '*(4*len(stack)) + '</RDF:li>',
-                    ])
-            stack.pop()
+        lines.extend([x for x in tree.hook_out(
+                    data=self, depth=1,
+                    on_open_dir=lambda node,data: (
+                        '%s<RDF:li>\n%s<RDF:Seq RDF:about="%s%s">'
+                        % (' '*(4*len(node)), ' '*(4*len(node)+2),
+                           data.rdf_root, '/'.join([name]+node))),
+                    on_close_dir=lambda node,data: (
+                        '%s</RDF:Seq>\n%s</RDF:li>'
+                        % (' '*(4*len(node)+2), ' '*(4*len(node)))),
+                    on_file=lambda node,data: (
+                        '%s<RDF:li RDF:resource="%s%s"/>'
+                        % (' '*(4*len(node)), data.rdf_root,
+                           '/'.join(['raw']+data.dirtag.raw_node(node)))),
+                    )])
         lines.extend([
                 '  </RDF:Seq>',
                 '',
@@ -102,19 +234,7 @@ class WebInterface:
                 ])
         return '\n'.join(lines)
 
-    def list_startswith(self, a, b):
-        """
-        >>> x = WebInterface(None, 'template')
-        >>> x.list_startswith([1, 2, 3], [1, 2, 3, 4])
-        True
-        >>> x.list_startswith([1, 2, 3], [1, 2, 3])
-        True
-        >>> x.list_startswith([1, 2, 3], [1, 2])
-        False
-        >>> x.list_startswith([1, 2, 3], [2, 2, 3])
-        False
-        """
-        return len([x for x,y in zip(a,b) if x==y]) == len(a)
+    # XUL user input methods
 
     @cherrypy.expose
     def new_tag(self, tag):
diff --git a/template/dir.html b/template/dir.html
new file mode 100644 (file)
index 0000000..aa727ae
--- /dev/null
@@ -0,0 +1,21 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
+<head>
+  <title>{{ repository_name }}</title>
+</head>
+<body>
+<h1>{{ dir_name }}</h1>
+<table>
+  <tr>
+    <th>File</th>
+    <th>Tags</th>
+  </tr>
+  {% for file,path,tags,tag_url in files %}
+  <tr>
+    <td><a href="static/raw/{{ path }}">{{ file }}</></td>
+    <td><a href="{{ tag_url }}">{{ tags  }}</a></td>
+  </tr>
+  {% endfor %}
+</table>
+</body>
diff --git a/template/dirtag.html b/template/dirtag.html
new file mode 100644 (file)
index 0000000..5001a17
--- /dev/null
@@ -0,0 +1,12 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
+<head>
+  <title>{{ repository_name }}</title>
+</head>
+<body>
+<h1 id="raw">Raw</h1>
+{{ raw_html }}
+<h1 id="tag">Tagged</h1>
+{{ tag_html }}
+</body>
diff --git a/template/file.html b/template/file.html
new file mode 100644 (file)
index 0000000..b083ef9
--- /dev/null
@@ -0,0 +1,21 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
+<head>
+  <title>{{ repository_name }}</title>
+</head>
+<body>
+<h1>{{ selected }}</h1>
+<form action="set_tags" method="get">
+  <input type="hidden" name="path" value="{{ selected_path }}"/>
+  <input type="hidden" name="selected" value="{{ selected_path }}"/>
+  <ul>
+  {% for tag,checked in tags %}
+    <li><input type="checkbox" name="tags" value="{{ tag }}"{% if checked %} checked="checked"{% endif %}/>
+      {{ tag }}</li>
+  {% endfor %}
+  </ul>
+  <input type="submit" value="Set tags" />
+</form>
+<iframe style="width:100%; height:500px" src="{{ selected_url }}"/>
+</body>