From d8f1a928d380408e0506cf9f5cf0fcf7e613638f Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Tue, 20 Jul 2010 12:16:21 -0400 Subject: [PATCH] Add RESTful HTML interface. 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 | 99 +++++++++++++++++--- dirtag/web.py | 210 +++++++++++++++++++++++++++++++++---------- template/dir.html | 21 +++++ template/dirtag.html | 12 +++ template/file.html | 21 +++++ 5 files changed, 304 insertions(+), 59 deletions(-) create mode 100644 template/dir.html create mode 100644 template/dirtag.html create mode 100644 template/file.html diff --git a/dirtag/__init__.py b/dirtag/__init__.py index 7ad9645..e572446 100644 --- a/dirtag/__init__.py +++ b/dirtag/__init__.py @@ -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'])]) >>> 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? diff --git a/dirtag/web.py b/dirtag/web.py index 3826022..e676e04 100755 --- a/dirtag/web.py +++ b/dirtag/web.py @@ -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') + + """ + lines = ['') + 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
  • %s\n%s
      ' + % (' '*(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
    \n%s
  • ' + % (' '*(4*len(node)), ' '*(4*len(node)-2))) + return '' + + def _dir_html_on_empty_dir(self, node, data): + return ('%s
  • %s
  • ' + % (' '*(4*len(node)-2), self._dir_html_link(node, data))) + + def _dir_html_link(self, node, data): + return ('%s' + % ('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 '

    Removed tag %s from %s

    ' % (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(' ' % (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) + '', - ' '*(4*len(stack)) + '', - ]) - 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)) - + '' - % (self.rdf_root, '/'.join(['raw']+raw_node))) - stack.pop() - else: - lines.extend([ - ' '*(4*len(stack)) + '', - ' '*(4*len(stack)+2) + '' - % (self.rdf_root, '/'.join([name]+node)), - ]) - while len(stack) > 0: - lines.extend([ - ' '*(4*len(stack)+2) + '', - ' '*(4*len(stack)) + '', - ]) - stack.pop() + lines.extend([x for x in tree.hook_out( + data=self, depth=1, + on_open_dir=lambda node,data: ( + '%s\n%s' + % (' '*(4*len(node)), ' '*(4*len(node)+2), + data.rdf_root, '/'.join([name]+node))), + on_close_dir=lambda node,data: ( + '%s\n%s' + % (' '*(4*len(node)+2), ' '*(4*len(node)))), + on_file=lambda node,data: ( + '%s' + % (' '*(4*len(node)), data.rdf_root, + '/'.join(['raw']+data.dirtag.raw_node(node)))), + )]) lines.extend([ ' ', '', @@ -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 index 0000000..aa727ae --- /dev/null +++ b/template/dir.html @@ -0,0 +1,21 @@ + + + + {{ repository_name }} + + +

    {{ dir_name }}

    + + + + + + {% for file,path,tags,tag_url in files %} + + + + + {% endfor %} +
    FileTags
    {{ file }}{{ tags }}
    + diff --git a/template/dirtag.html b/template/dirtag.html new file mode 100644 index 0000000..5001a17 --- /dev/null +++ b/template/dirtag.html @@ -0,0 +1,12 @@ + + + + {{ repository_name }} + + +

    Raw

    +{{ raw_html }} +

    Tagged

    +{{ tag_html }} + diff --git a/template/file.html b/template/file.html new file mode 100644 index 0000000..b083ef9 --- /dev/null +++ b/template/file.html @@ -0,0 +1,21 @@ + + + + {{ repository_name }} + + +

    {{ selected }}

    +
    + + +
      + {% for tag,checked in tags %} +
    • + {{ tag }}
    • + {% endfor %} +
    + +
    +