1 # Copyright (C) 2005-2012 Aaron Bentley <abentley@panoramicfeedback.com>
2 # Chris Ball <cjb@laptop.org>
3 # Gianluca Montecchi <gian@grys.it>
4 # Marien Zwart <marien.zwart@gmail.com>
5 # Mathieu Clabaut <mathieu.clabaut@gmail.com>
6 # Thomas Gerigk <tgerigk@gmx.de>
7 # W. Trevor King <wking@tremily.us>
9 # This file is part of Bugs Everywhere.
11 # Bugs Everywhere is free software: you can redistribute it and/or modify it
12 # under the terms of the GNU General Public License as published by the Free
13 # Software Foundation, either version 2 of the License, or (at your option) any
16 # Bugs Everywhere is distributed in the hope that it will be useful, but
17 # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
18 # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
21 # You should have received a copy of the GNU General Public License along with
22 # Bugs Everywhere. If not, see <http://www.gnu.org/licenses/>.
33 import xml.sax.saxutils
35 from jinja2 import Environment, FileSystemLoader, DictLoader, ChoiceLoader
39 import libbe.command.depend
40 import libbe.command.target
41 import libbe.command.util
43 import libbe.util.encoding
45 import libbe.util.wsgi
49 class ServerApp (libbe.util.wsgi.WSGI_AppObject,
50 libbe.util.wsgi.WSGI_DataObject):
51 """WSGI server for a BE Storage instance over HTML.
53 Serve browsable HTML for public consumption. Currently everything
56 server_version = 'BE-html-server/' + libbe.version.version()
58 def __init__(self, bugdirs={}, template_dir=None, title='Site Title',
59 header='Header', index_file='', min_id_length=-1,
60 strip_email=False, generation_time=None, **kwargs):
61 super(ServerApp, self).__init__(
63 (r'^{}$'.format(index_file), self.index),
64 (r'^style.css$', self.style),
65 (r'^([^/]+)/([^/]+)/{}'.format(index_file), self.bug),
68 self.bugdirs = bugdirs
71 self._index_file = index_file
72 self.min_id_length = min_id_length
73 self.strip_email = strip_email
74 self.generation_time = generation_time
76 self.http_user_error = 418
77 self._load_templates(template_dir=template_dir)
79 'active': lambda bug: bug.active and bug.severity != 'target',
80 'inactive': lambda bug: not bug.active and bug.severity !='target',
81 'target': lambda bug: bug.severity == 'target'
85 def style(self, environ, start_response):
86 template = self.template.get_template('style.css')
87 content = template.render()
88 return self.ok_response(
89 environ, start_response, content, content_type='text/css')
91 def index(self, environ, start_response):
92 data = self.query_data(environ)
94 bug_type = self.data_get_string(
95 data, 'type', default='active', source=source)
96 assert bug_type in ['active', 'inactive', 'target'], bug_type
98 filter_ = self._filters.get(bug_type, self._filters['active'])
99 bugs = list(itertools.chain(*list(
100 [bug for bug in bugdir if filter_(bug)]
101 for bugdir in self.bugdirs.values())))
105 self.log_level, 'generate {} index file for {} bugs'.format(
106 bug_type, len(bugs)))
110 'stylesheet': 'style.css',
111 'header': self.header,
112 'active_class': 'tab nsel',
113 'inactive_class': 'tab nsel',
114 'target_class': 'tab nsel',
116 'bug_entry': self.template.get_template('index_bug_entry.html'),
117 'bug_dir': self.bug_dir,
118 'index_file': self._index_file,
119 'generation_time': self._generation_time(),
121 template_info['{}_class'.format(bug_type)] = 'tab sel'
122 if bug_type == 'target':
123 template = self.template.get_template('target_index.html')
124 template_info['targets'] = [
125 (target, sorted(libbe.command.depend.get_blocked_by(
126 self.bugdirs, target)))
129 template = self.template.get_template('standard_index.html')
130 content = template.render(template_info)+'\n'
131 return self.ok_response(
132 environ, start_response, content, content_type='text/html')
134 def bug(self, environ, start_response):
136 bugdir_id,bug_id = environ['be-server.url_args']
138 raise libbe.util.wsgi.HandlerError(404, 'Not Found')
139 user_id = '{}/{}'.format(bugdir_id, bug_id)
140 bugdir,bug,comment = (
141 libbe.command.util.bugdir_bug_comment_from_user_id(
142 self.bugdirs, user_id))
145 self.log_level, 'generate bug file for {}/{}'.format(
146 bugdir.uuid, bug.uuid))
147 if bug.severity == 'target':
148 index_type = 'target'
150 index_type = 'active'
152 index_type = 'inactive'
153 target = libbe.command.target.bug_target(self.bugdirs, bug)
154 if target == bug: # e.g. when bug.severity == 'target'
156 up_link = '../../{}?type={}'.format(self._index_file, index_type)
157 bug.load_comments(load_full=True)
158 bug.comment_root.sort(cmp=libbe.comment.cmp_time, reverse=True)
162 'stylesheet': '../../style.css',
163 'header': self.header,
164 'backlinks': self.template.get_template('bug_backlinks.html'),
166 'index_type': index_type.capitalize(),
167 'index_file': self._index_file,
170 'comment_entry': self.template.get_template(
171 'bug_comment_entry.html'),
172 'comments': [(depth,comment) for depth,comment
173 in bug.comment_root.thread(flatten=False)],
174 'bug_dir': self.bug_dir,
175 'comment_dir': self._truncated_comment_id,
176 'format_body': self._format_comment_body,
177 'div_close': _DivCloser(),
178 'strip_email': self._strip_email,
179 'generation_time': self._generation_time(),
181 template = self.template.get_template('bug.html')
182 content = template.render(template_info)
183 return self.ok_response(
184 environ, start_response, content, content_type='text/html')
188 if time.time() > self._refresh:
190 self.logger.log(self.log_level, 'refresh bugdirs')
191 for bugdir in self.bugdirs.values():
192 bugdir.load_all_bugs()
193 self._refresh = time.time() + 60
195 def _truncated_bugdir_id(self, bugdir):
196 return libbe.util.id._truncate(
197 bugdir.uuid, self.bugdirs.keys(),
198 min_length=self.min_id_length)
200 def _truncated_bug_id(self, bug):
201 return libbe.util.id._truncate(
202 bug.uuid, bug.sibling_uuids(),
203 min_length=self.min_id_length)
205 def _truncated_comment_id(self, comment):
206 return libbe.util.id._truncate(
207 comment.uuid, comment.sibling_uuids(),
208 min_length=self.min_id_length)
210 def bug_dir(self, bug):
211 return '{}/{}'.format(
212 self._truncated_bugdir_id(bug.bugdir),
213 self._truncated_bug_id(bug))
215 def _long_to_linked_user(self, text):
217 >>> import libbe.bugdir
218 >>> bugdir = libbe.bugdir.SimpleBugDir(memory=False)
219 >>> a = ServerApp(bugdirs={bugdir.uuid: bugdir})
220 >>> a._long_to_linked_user('A link #abc123/a#, and a non-link #x#y#.')
221 'A link <a href="./a/">abc/a</a>, and a non-link #x#y#.'
224 replacer = libbe.util.id.IDreplacer(
225 self.bugdirs, self._long_to_linked_user_replacer, wrap=False)
227 libbe.util.id.REGEXP, replacer, text)
229 def _long_to_linked_user_replacer(self, bugdirs, long_id):
231 >>> import libbe.bugdir
232 >>> import libbe.util.id
233 >>> bugdir = libbe.bugdir.SimpleBugDir(memory=False)
234 >>> bugdirs = {bugdir.uuid: bugdir}
235 >>> a = bugdir.bug_from_uuid('a')
236 >>> uuid_gen = libbe.util.id.uuid_gen
237 >>> libbe.util.id.uuid_gen = lambda : '0123'
238 >>> c = a.new_comment('comment for link testing')
239 >>> libbe.util.id.uuid_gen = uuid_gen
242 >>> a = ServerApp(bugdirs=bugdirs)
243 >>> a._long_to_linked_user_replacer(bugdirs, 'abc123')
245 >>> a._long_to_linked_user_replacer(bugdirs, 'abc123/a')
246 '<a href="./a/">abc/a</a>'
247 >>> a._long_to_linked_user_replacer(bugdirs, 'abc123/a/0123')
248 '<a href="./a/#0123">abc/a/012</a>'
249 >>> a._long_to_linked_user_replacer(bugdirs, 'x')
251 >>> a._long_to_linked_user_replacer(bugdirs, '')
256 p = libbe.util.id.parse_user(bugdirs, long_id)
257 except (libbe.util.id.MultipleIDMatches,
258 libbe.util.id.NoIDMatches,
259 libbe.util.id.InvalidIDStructure), e:
260 return '#%s#' % long_id # re-wrap failures
261 if p['type'] == 'bugdir':
262 return '#%s#' % long_id
263 elif p['type'] == 'bug':
264 bugdir,bug,comment = (
265 libbe.command.util.bugdir_bug_comment_from_user_id(
267 return '<a href="./%s/">%s</a>' \
268 % (self._truncated_bug_id(bug), bug.id.user())
269 elif p['type'] == 'comment':
270 bugdir,bug,comment = (
271 libbe.command.util.bugdir_bug_comment_from_user_id(
273 return '<a href="./%s/#%s">%s</a>' \
274 % (self._truncated_bug_id(bug),
275 self._truncated_comment_id(comment),
277 raise Exception('Invalid id type %s for "%s"'
278 % (p['type'], long_id))
280 def _format_comment_body(self, bug, comment):
281 link_long_ids = False
284 if comment.content_type == 'text/html':
286 elif comment.content_type.startswith('text/'):
287 value = '<pre>\n'+self._escape(value)+'\n</pre>'
289 elif comment.content_type.startswith('image/'):
291 value = '<img src="./%s/%s" />' % (
292 self._truncated_bug_id(bug),
293 self._truncated_comment_id(comment))
296 value = '<a href="./%s/%s">Link to %s file</a>.' % (
297 self._truncated_bug_id(bug),
298 self._truncated_comment_id(comment),
299 comment.content_type)
300 if link_long_ids == True:
301 value = self._long_to_linked_user(value)
302 if save_body == True:
303 per_bug_dir = os.path.join(self.out_dir_bugs, bug.uuid)
304 if not os.path.exists(per_bug_dir):
305 os.mkdir(per_bug_dir)
306 comment_path = os.path.join(per_bug_dir, comment.uuid)
308 '<Files %s>\n ForceType %s\n</Files>' \
309 % (comment.uuid, comment.content_type),
310 [per_bug_dir, '.htaccess'], mode='a')
311 self._write_file(comment.body,
312 [per_bug_dir, comment.uuid], mode='wb')
315 def _generation_time(self):
316 if self.generation_time:
317 return self.generation_time
320 def _escape(self, string):
323 return xml.sax.saxutils.escape(string)
325 def _strip_email(self, string):
327 name,address = email.utils.parseaddr(string)
333 def _load_templates(self, template_dir=None):
334 if template_dir is not None:
335 template_dir = os.path.abspath(os.path.expanduser(template_dir))
337 self.template_dict = {
341 font-family: "lucida grande", "sans serif";
353 background-color: #fcfcfc;
376 padding-bottom: 10px;
385 border-color: #305275;
386 background-color: #305275;
389 border-top-left-radius: 8px;
390 border-top-right-radius: 8px;
398 border-spacing: 0px 0px;
409 border-style: dotted;
417 border-color: #c3d9ff;
418 border-collapse: collapse;
426 border-color: #c3d9ff;
427 border-collapse: collapse;
433 img { border-style: none; }
436 list-style-type: none;
449 text-decoration: none;
452 a { color: #553d41; }
453 a:hover { color: #003d41; }
454 a:visited { color: #305275; }
455 .footer a { color: #508d91; }
457 /* bug index pages */
465 background-color: #c3d9ff ;
466 border: 1px solid #c3d9ff;
468 border-top-left-radius: 15px;
469 border-top-right-radius: 15px;
473 border: 1px solid #c3d9ff;
475 border-top-left-radius: 5px;
476 border-top-right-radius: 5px;
482 border-color: #c3d9ff;
485 border: 1px solid #c3d9ff;
494 table.target_list.td {
498 tr.wishlist { background-color: #DCFAFF;}
499 tr.wishlist:hover { background-color: #C2DCE1; }
501 tr.minor { background-color: #FFFFA6; }
502 tr.minor:hover { background-color: #E6E696; }
504 tr.serious { background-color: #FF9077;}
505 tr.serious:hover { background-color: #E6826B; }
507 tr.critical { background-color: #FF752A; }
508 tr.critical:hover { background-color: #D63905;}
510 tr.fatal { background-color: #FF3300;}
511 tr.fatal:hover { background-color: #D60000;}
513 td.uuid { width: 5%; border-style: dotted;}
514 td.status { width: 5%; border-style: dotted;}
515 td.severity { width: 5%; border-style: dotted;}
516 td.summary { border-style: dotted;}
517 td.date { width: 25%; border-style: dotted;}
519 /* bug detail pages */
521 td.bug_detail_label { text-align: right; border: none;}
522 td.bug_detail { border: none;}
523 td.bug_comment_label { text-align: right; vertical-align: top; }
535 /* padding-top: 0px; */
536 padding-bottom: 20px;
541 """<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
542 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
543 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
545 <title>{{ title }}</title>
546 <meta http-equiv="Content-Type" content="text/html; charset={{ charset }}" />
547 <link rel="stylesheet" href="{{ stylesheet }}" type="text/css" />
550 <div class="header">{{ header }}</div>
552 {% block content %}{% endblock %}
555 <p>Generated by <a href="http://www.bugseverywhere.org/">
556 Bugs Everywhere</a> on {{ generation_time }}</p>
558 <a href="http://validator.w3.org/check?uri=referer">
559 Validate XHTML</a> |
560 <a href="http://jigsaw.w3.org/css-validator/check?uri=referer">
569 """{% extends "base.html" %}
573 {% block bugdir_table %}{% endblock %}
581 """{% extends "base.html" %}
587 <td class="{{ active_class }}"><a href="{% if index_file %}{{ index_file }}{% else %}.{% endif %}">Active Bugs</a></td>
588 <td class="{{ inactive_class }}"><a href="{{ index_file }}?type=inactive">Inactive Bugs</a></td>
589 <td class="{{ target_class }}"><a href="{{ index_file }}?type=target">Divided by target</a></td>
594 {% block bug_table %}{% endblock %}
601 'standard_index.html':
602 """{% extends "index.html" %}
604 {% block bug_table %}
605 <table class="bug_list">
616 {% for bug in bugs %}
617 {{ bug_entry.render({'bug':bug, 'dir':bug_dir(bug), 'index_file':index_file}) }}
625 """{% extends "index.html" %}
627 {% block bug_table %}
628 {% for target,bugs in targets %}
629 <table class="target_list">
632 <th class="target_name" colspan="5">
633 Target: {{ target.summary|e }} ({{ target.status|e }})
645 {% for bug in bugs %}
646 {{ bug_entry.render({'bug':bug, 'dir':bug_dir(bug), 'index_file':index_file}) }}
654 'index_bug_entry.html':
655 """<tr class="{{ bug.severity }}">
656 <td class="uuid"><a href="{{ dir }}/{{ index_file }}">{{ bug.id.user()|e }}</a></td>
657 <td class="status"><a href="{{ dir }}/{{ index_file }}">{{ bug.status|e }}</a></td>
658 <td class="severity"><a href="{{ dir }}/{{ index_file }}">{{ bug.severity|e }}</a></td>
659 <td class="summary"><a href="{{ dir }}/{{ index_file }}">{{ bug.summary|e }}</a></td>
660 <td class="date"><a href="{{ dir }}/{{ index_file }}">{{ (bug.time_string or '')|e }}</a></td>
665 """{% extends "base.html" %}
668 {{ backlinks.render({'up_link': up_link, 'index_type':index_type, 'index_file':index_file}) }}
669 <h1>Bug: {{ bug.id.user()|e }}</h1>
673 <tr><td class="bug_detail_label">ID :</td>
674 <td class="bug_detail">{{ bug.uuid|e }}</td></tr>
675 <tr><td class="bug_detail_label">Short name :</td>
676 <td class="bug_detail">{{ bug.id.user()|e }}</td></tr>
677 <tr><td class="bug_detail_label">Status :</td>
678 <td class="bug_detail">{{ bug.status|e }}</td></tr>
679 <tr><td class="bug_detail_label">Severity :</td>
680 <td class="bug_detail">{{ bug.severity|e }}</td></tr>
681 <tr><td class="bug_detail_label">Assigned :</td>
682 <td class="bug_detail">{{ strip_email(bug.assigned or '')|e }}</td></tr>
683 <tr><td class="bug_detail_label">Reporter :</td>
684 <td class="bug_detail">{{ strip_email(bug.reporter or '')|e }}</td></tr>
685 <tr><td class="bug_detail_label">Creator :</td>
686 <td class="bug_detail">{{ strip_email(bug.creator or '')|e }}</td></tr>
687 <tr><td class="bug_detail_label">Created :</td>
688 <td class="bug_detail">{{ (bug.time_string or '')|e }}</td></tr>
690 <tr><td class="bug_detail_label">Target :</td>
691 <td class="bug_detail"><a href="../../{{ bug_dir(target) }}/{{ index_file }}">{{ target.summary }}</a></td></tr>
693 <tr><td class="bug_detail_label">Summary :</td>
694 <td class="bug_detail">{{ bug.summary|e }}</td></tr>
701 {% for depth,comment in comments %}
703 <div class="comment root" id="C{{ comment_dir(comment) }}">
705 <div class="comment" id="C{{ comment_dir(comment) }}">
707 {{ comment_entry.render({
708 'depth':depth, 'bug': bug, 'comment':comment, 'comment_dir':comment_dir,
709 'format_body': format_body, 'div_close': div_close,
710 'strip_email': strip_email}) }}
711 {{ div_close(depth) }}
713 {% if comments[-1][0] > 0 %}
719 {{ backlinks.render({'up_link': up_link, 'index_type': index_type}) }}
723 'bug_backlinks.html':
724 """<p class="backlink"><a href="{{ up_link }}">Back to {{ index_type }} Index</a></p>
725 <p class="backlink"><a href="../../{{ index_file }}?type=target">Back to Target Index</a></p>
728 'bug_comment_entry.html':
732 <td class="bug_comment_label">Comment:</td>
733 <td class="bug_comment">
734 --------- Comment ---------<br/>
735 ID: {{ comment.uuid }}<br/>
736 Short name: {{ comment.id.user() }}<br/>
737 From: {{ strip_email(comment.author or '')|e }}<br/>
738 Date: {{ (comment.date or '')|e }}<br/>
740 {{ format_body(bug, comment) }}
748 loader = DictLoader(self.template_dict)
751 file_system_loader = FileSystemLoader(template_dir)
752 loader = ChoiceLoader([file_system_loader, loader])
753 self.template = Environment(loader=loader)
756 class HTML (libbe.util.wsgi.ServerCommand):
757 """Serve or dump browsable HTML for the current repository
760 >>> import libbe.bugdir
761 >>> bugdir = libbe.bugdir.SimpleBugDir(memory=False)
762 >>> io = libbe.command.StringInputOutput()
763 >>> io.stdout = sys.stdout
764 >>> ui = libbe.command.UserInterface(io=io)
765 >>> ui.storage_callbacks.set_storage(bugdir.storage)
766 >>> cmd = HTML(ui=ui)
768 >>> export_path = os.path.join(bugdir.storage.repo, 'html_export')
769 >>> ret = ui.run(cmd, {'output': export_path, 'export-html': True})
770 >>> os.path.exists(export_path)
772 >>> os.path.exists(os.path.join(export_path, 'index.html'))
774 >>> os.path.exists(os.path.join(export_path, 'index_inactive.html'))
776 >>> os.path.exists(os.path.join(export_path, bugdir.uuid))
778 >>> for bug in sorted(bugdir):
779 ... if os.path.exists(os.path.join(
780 ... export_path, bugdir.uuid, bug.uuid, 'index.html')):
781 ... print('got {}'.format(bug.uuid))
783 ... print('missing {}'.format(bug.uuid))
792 def __init__(self, *args, **kwargs):
793 super(HTML, self).__init__(*args, **kwargs)
794 # ServerApp cannot write, so drop some security options
796 option for option in self.options
797 if option.name not in [
803 self.options.extend([
804 libbe.command.Option(name='template-dir', short_name='t',
805 help=('Use different templates. Defaults to internal '
807 arg=libbe.command.Argument(
808 name='template-dir', metavar='DIR',
809 completion_callback=libbe.command.util.complete_path)),
810 libbe.command.Option(name='title',
811 help='Set the bug repository title',
812 arg=libbe.command.Argument(
813 name='title', metavar='STRING',
814 default='Bugs Everywhere Issue Tracker')),
815 libbe.command.Option(name='index-header',
816 help='Set the index page headers',
817 arg=libbe.command.Argument(
818 name='index-header', metavar='STRING',
819 default='Bugs Everywhere Bug List')),
820 libbe.command.Option(name='min-id-length', short_name='l',
821 help=('Attempt to truncate bug and comment IDs to this '
822 'length. Set to -1 for non-truncated IDs'),
823 arg=libbe.command.Argument(
824 name='min-id-length', metavar='INT',
825 default=-1, type='int')),
826 libbe.command.Option(name='strip-email',
827 help='Strip email addresses from person fields.'),
828 libbe.command.Option(name='export-html', short_name='e',
829 help='Export all HTML pages and exit.'),
830 libbe.command.Option(name='output', short_name='o',
831 help='Set the output path for HTML export',
832 arg=libbe.command.Argument(
833 name='output', metavar='DIR', default='./html_export',
834 completion_callback=libbe.command.util.complete_path)),
835 libbe.command.Option(name='export-template', short_name='E',
836 help='Export the default template and exit.'),
837 libbe.command.Option(name='export-template-dir', short_name='d',
838 help='Set the directory for the template export',
839 arg=libbe.command.Argument(
840 name='export-template-dir', metavar='DIR',
841 default='./default-templates/',
842 completion_callback=libbe.command.util.complete_path)),
845 def _run(self, **params):
846 if True in [params['export-template'], params['export-html']]:
848 logger=None, storage=None, index_file='index.html',
849 generation_time=time.ctime(), **params)
850 if params['export-template']:
851 self._write_default_template(
852 template_dict=app.template_dict,
853 out_dir=params['export-template-dir'])
854 elif params['export-html']:
855 self._write_static_pages(app=app, out_dir=params['output'])
857 # provide defaults for the dropped options
858 params['read-only'] = True
859 params['notify'] = None
860 params['auth'] = None
861 return super(HTML, self)._run(**params)
863 def _get_app(self, logger, storage, index_file='', generation_time=None,
866 logger=logger, bugdirs=self._get_bugdirs(),
867 template_dir=kwargs['template-dir'],
868 title=kwargs['title'],
869 header=kwargs['index-header'],
870 index_file=index_file,
871 min_id_length=kwargs['min-id-length'],
872 strip_email=kwargs['strip-email'],
873 generation_time=generation_time)
875 def _long_help(self):
881 Then point your browser at ``http://localhost:8000/``.
883 If either ``--export-html`` or ``export-template`` is set, the command
884 will exit after the dump without serving anything over the wire.
887 def _write_default_template(self, template_dict, out_dir):
888 out_dir = self._make_dir(out_dir)
889 for filename,text in template_dict.iteritems():
890 self._write_file(text, [out_dir, filename])
892 def _write_static_pages(self, app, out_dir):
894 ('index.html?type=active', 'index.html'),
895 ('index.html?type=inactive', 'index_inactive.html'),
896 ('index.html?type=target', 'index_by_target.html'),
898 out_dir = self._make_dir(out_dir)
899 caller = libbe.util.wsgi.WSGICaller()
901 content=self._get_content(caller, app, 'style.css'),
902 path_array=[out_dir, 'style.css'])
903 for url,data_dict,path in [
904 ('index.html', {'type': 'active'}, 'index.html'),
905 ('index.html', {'type': 'inactive'}, 'index_inactive.html'),
906 ('index.html', {'type': 'target'}, 'index_by_target.html'),
908 content = self._get_content(caller, app, url, data_dict)
909 for url_,path_ in url_mappings:
910 content = content.replace(url_, path_)
911 self._write_file(content=content, path_array=[out_dir, path])
912 for bugdir in app.bugdirs.values():
914 bug_dir_url = app.bug_dir(bug=bug)
915 segments = bug_dir_url.split('/')
916 path_array = [out_dir]
917 path_array.extend(segments)
918 bug_dir_path = os.path.join(*path_array)
919 path_array.append(app._index_file)
920 url = '{}/{}'.format(bug_dir_url, app._index_file)
921 content = self._get_content(caller, app, url)
922 for url_,path_ in url_mappings:
923 content = content.replace(url_, path_)
924 if not os.path.isdir(bug_dir_path):
925 self._make_dir(bug_dir_path)
926 self._write_file(content=content, path_array=path_array)
928 def _get_content(self, caller, app, path, data_dict=None):
930 return caller.getURL(app=app, path=path, data_dict=data_dict)
931 except libbe.util.wsgi.HandlerError:
933 'error retrieving {} with {}\n'.format(path, data_dict))
936 def _make_dir(self, dir_path):
937 dir_path = os.path.abspath(os.path.expanduser(dir_path))
938 if not os.path.exists(dir_path):
940 os.makedirs(dir_path)
942 raise libbe.command.UserError(
943 'Cannot create output directory "{}".'.format(dir_path))
946 def _write_file(self, content, path_array, mode='w'):
947 if not hasattr(self, 'encoding'):
948 self.encoding = libbe.util.encoding.get_text_file_encoding()
949 return libbe.util.encoding.set_file_contents(
950 os.path.join(*path_array), content, mode, self.encoding)
953 Html = HTML # alias for libbe.command.base.get_command_class()
956 class _DivCloser (object):
957 def __init__(self, depth=0):
960 def __call__(self, depth):
962 while self.depth >= depth:
966 return '\n'.join(ret)