This commit is contained in:
Andy McKay 2011-04-19 10:19:30 -07:00
Родитель 338a431052
Коммит 10a3768422
9 изменённых файлов: 303 добавлений и 285 удалений

Просмотреть файл

@ -8,8 +8,9 @@ from django.conf import settings
from django.utils.datastructures import SortedDict
from django.utils.encoding import smart_unicode
import jinja2
import commonware.log
from jingo import register
from jingo import register, env
from tower import ugettext as _
from amo.utils import memoize
@ -32,6 +33,22 @@ def file_viewer_class(value, selected):
return ' '.join(result)
@register.function
def file_tree(files, selected):
depth = 0
output = ['<ul class="root">']
t = env.get_template('files/node.html')
for k, v in files.items():
if v['depth'] > depth:
output.append('<ul class="hidden">')
elif v['depth'] < depth:
output.extend(['</ul>' for x in range(v['depth'], depth) ])
output.append(t.render(value=v, selected=selected))
depth = v['depth']
output.extend(['</ul>' for x in range(depth, -1, -1) ])
return jinja2.Markup('\n'.join(output))
class FileViewer:
def __init__(self, file_obj):
@ -123,12 +140,18 @@ class FileViewer:
@memoize(prefix='file-viewer')
def _get_files(self):
all_files, res = [], SortedDict()
for root, dirs, files in os.walk(self.dest):
for filename in dirs + files:
all_files.append([os.path.join(root, filename), filename])
# Not using os.path.walk so we get just the right order.
for path, filename in sorted(all_files):
filename = smart_unicode(filename)
def iterate(node):
for filename in os.listdir(node):
full = os.path.join(node, filename)
all_files.append(full)
if os.path.isdir(full):
iterate(full)
iterate(self.dest)
for path in all_files:
filename = smart_unicode(os.path.basename(path))
short = smart_unicode(path[len(self.dest) + 1:])
mime, encoding = mimetypes.guess_type(filename)
directory = os.path.isdir(path)
@ -144,8 +167,8 @@ class FileViewer:
'short': short,
'truncated': self.truncate(filename),
'url': reverse('files.list', args=args),
'url_serve': reverse('files.redirect', args=args),
'parent': '/'.join(short.split(os.sep)[:-1])}
'url_serve': reverse('files.redirect', args=args)}
return res

Просмотреть файл

@ -0,0 +1,5 @@
<li>
<a class="{{ file_viewer_class(value, selected) }}"
href="{{ value['url'] }}"
title="{{ value['filename'] }}">{{ value['truncated'] }}</a>
</li>

Просмотреть файл

@ -7,91 +7,80 @@
<a href="{{ file_url }}">{{ addon.name }} {{ version }}</a>
{% if file.platform.id != amo.PLATFORM_ALL.id %}({{ file.platform }}){% endif %}
</h3>
<div id="file-viewer">
{% if not status %}
<p class="waiting" id="extracting" data-url="{{ poll_url }}">
{{ _('Add-on file being processed, please wait.') }}
</p>
{% endif %}
<div id="files">
<ul>
{% if files %}
{% for key, value in files.items() %}
<li data-parent="{{ value['parent'] }}"
data-short="{{ value['short'] }}"
style="padding-left: {{ value['depth'] }}em"
class="{% if value['depth'] %}hidden{% endif %}">
<a class="{{ file_viewer_class(value, selected) }}"
href="{{ value['url'] }}"
title="{{ value['filename'] }}">{{ value['truncated'] }}</a>
</li>
{% endfor %}
{% endif %}
</ul>
{% if files %}
<p>
<a href="#" id="files-prev">{{ _('Prev') }}</a> |
<a href="#" id="files-next">{{ _('Next') }}</a><br />
<a href="#" id="files-expand-all">{{ _('Expand All') }}</a><br />
<a href="#" id="files-unwrap">{{ _('Unwrap text')}}</a>
<a href="#" id="files-wrap" class="hidden">{{ _('Wrap text')}}</a><br />
</p>
{% elif not files and status %}
<p>{{ _('No files in the uploaded file.') }}</p>
{% endif %}
</div>
<div id="thinking" class="hidden">
<p class="waiting">
{{ _('Fetching file.') }}
</p>
</div>
<div id="content-wrapper">
{% if msg %}
<p>{{ msg }}</p>
{% endif %}
<div>
{% if viewer %}
{% if selected['binary'] and not selected['directory'] %}
{% include "files/file.html" %}
{% else %}
{% if selected and content %}
<pre id="content" class="wrapped">{{ content }}</pre>
{% elif content == '' %}
<p>{{ _('No content.') }}</p>
{% endif %}
{% endif %}
{% endif %}
{% if diff and not diff.is_binary() %}
{% if text_one and text_two %}
<pre id="diff" class="wrapped"></pre>
{% endif %}
{% endif %}
{% if diff and diff.is_binary() %}
<p>
{% if diff.is_different() %}
{{ _('Files are different.') }}<br/>
{% else %}
{{ _('Files are the same.') }}<br/>
{% endif %}
{% with selected = diff.one, file = diff.file_one.file %}
{% include "files/file.html" %}
{% endwith %}
{% with selected = diff.two, file = diff.file_two.file %}
{% include "files/file.html" %}
{% endwith %}
<ol class="breadcrumbs">
<li>{{ addon.name }}</li>
<li>{{ version }}</li>
<li id="breadcrumb">{{ selected['filename'] }}</li>
</ol>
<div id="file-viewer" class="featured">
<div class="featured-inner">
{% if not status %}
<p class="waiting" id="extracting" data-url="{{ poll_url }}">
{{ _('Add-on file being processed, please wait.') }}
</p>
{% endif %}
{% if diff and not diff.is_binary() %}
<div class="hidden">
<pre id="file-one">{{ text_one }}</pre>
<pre id="file-two">{{ text_two }}</pre>
<div id="files">
{% if files %}
{{ file_tree(files, selected) }}
{% endif %}
{% if files %}
<p id="commands">
<code>j</code> <a href="#" id="files-prev">{{ _('Previous file') }}</a><br/>
<code>k</code> <a href="#" id="files-next">{{ _('Next file') }}</a><br/>
<code>e</code> <a href="#" id="files-expand-all">{{ _('Expand all') }}</a><br/>
<code>h</code> <a href="#" id="files-hide">{{ _('Hide or unhide tree') }}</a><br/>
<code>w</code> <a href="#" id="files-wrap">{{ _('Wrap or unwrap text') }}</a>
</p>
{% elif not files and status %}
<p>{{ _('No files in the uploaded file.') }}</p>
{% endif %}
</div>
<div id="thinking" class="hidden">
<p class="waiting">
{{ _('Fetching file.') }}
</p>
</div>
<div id="content-wrapper">
{% if msg %}
<p>{{ msg }}</p>
{% endif %}
<div>
{% if viewer %}
{% if selected['binary'] and not selected['directory'] %}
{% include "files/file.html" %}
{% else %}
{% if selected and content %}
<pre id="content" class="wrapped">{{ content }}</pre>
{% elif content == '' %}
<p>{{ _('No content.') }}</p>
{% endif %}
{% endif %}
{% endif %}
{% if diff and diff.is_binary() %}
<p>
{% if diff.is_different() %}
{{ _('Files are different.') }}<br/>
{% else %}
{{ _('Files are the same.') }}<br/>
{% endif %}
{% with selected = diff.one, file = diff.file_one.file %}
{% include "files/file.html" %}
{% endwith %}
{% with selected = diff.two, file = diff.file_two.file %}
{% include "files/file.html" %}
{% endwith %}
</p>
{% endif %}
{% if diff and not diff.is_binary() %}
{% if text_one and text_two %}
<pre id="diff" class="wrapped hidden"></pre>
<pre class="left hidden">{{ text_one }}</pre>
<pre class="right hidden">{{ text_two }}</pre>
{% endif %}
{% endif %}
</div>
{% endif %}
</div>
</div>
<p class="help">
{{ _('Shortcuts: prev j, next k') }}
</p>
</div>
{% endblock %}

Просмотреть файл

@ -114,13 +114,23 @@ class TestFileHelper(test_utils.TestCase):
self.viewer.extract()
files = self.viewer.get_files()
eq_(files['dictionaries/license.txt']['depth'], 1)
eq_(files['dictionaries/license.txt']['parent'], 'dictionaries')
def test_bom(self):
dest = tempfile.mkstemp()[1]
open(dest, 'w').write('foo'.encode('utf-16'))
eq_(self.viewer.read_file({'full': dest}), (u'foo', ''))
def test_file_order(self):
self.viewer.extract()
dest = self.viewer.dest
open(os.path.join(dest, 'chrome.manifest'), 'w')
subdir = os.path.join(dest, 'chrome')
os.mkdir(subdir)
open(os.path.join(subdir, 'foo'), 'w')
cache.clear()
eq_(self.viewer.get_files().keys()[8:11],
[u'chrome', u'chrome/foo', u'chrome.manifest'])
class TestDiffHelper(test_utils.TestCase):

Просмотреть файл

@ -331,8 +331,7 @@ class TestDiffViewer(FilesBase, test_utils.TestCase):
self.file_viewer.extract()
res = self.client.get(self.file_url(not_binary))
doc = pq(res.content)
eq_(len(doc('#file-one')), 1)
eq_(len(doc('#file-two')), 1)
eq_(len(doc('pre')), 3)
def test_binary_serve_links(self):
self.file_viewer.extract()

Просмотреть файл

@ -1,123 +1,82 @@
#file-viewer {
background: white;
padding-top: 1em
#file-viewer div.featured-inner {
padding: 1em;
}
#files {
width: 20%;
display: block;
float: left;
}
#files li a {
display: inline-block;
margin-left: 20px;
padding: 2px 5px 2px 15px;
}
#content a {
padding: 0em;
margin: 0em;
#files ul.root {
overflow-x: scroll;
}
#content-wrapper, #thinking {
width: 50em;
float: left;
margin-left: 1em;
padding-left: 3em;
}
#content div.number, #diff div.number {
display: inline;
float: left;
vertical-align: top;
}
#content div.code, #diff div.code {
width: 50em;
display: block;
position: relative;
left: 3em;
#files ul {
padding-left: 1em;
}
.wrapped {
#thinking {
padding-left: 25%;
}
span.number {
width: 4em;
display: inline-block;
text-align: right;
}
pre {
overflow-x: scroll;
}
pre.wrapped {
white-space: pre-wrap;
word-wrap: break-word;
overflow-x: hidden;
}
pre div, pre ins, pre del {
display: inline-block;
padding: 0;
margin: 0;
}
pre div:hover, pre ins:hover, pre del:hover {
pre div.line:hover {
background-color: #ffffcc;
}
pre ins {
background-color: #e6ffe6;
text-decoration: none;
}
pre del {
background-color: #ffe6e6;
text-decoration: none;
}
#content, #diff {
background: white;
float: left;
}
.add {
background-color: #ddf8dd;
width: 100%;
background-color: #e6ffe6;
}
.delete {
background-color: #ffdddd;
width: 100%;
background-color: #ffe6e6;
}
#file-viewer span {
margin: 0;
padding: 0;
line-height: 0;
}
#file-viewer li a {
margin-left: 20px;
padding: 2px 5px 2px 15px;
width: 100%;
}
#file-viewer a.selected {
a.selected {
background-color: #ddf8dd;
}
#file-viewer a.closed {
a.closed {
background: url('/media/img/icons/plus.gif') 0 no-repeat;
}
#file-viewer a.open {
a.open {
background: url('/media/img/icons/minus.gif') 0 no-repeat;
}
#file-viewer .waiting {
.waiting {
background-image: url(../../img/zamboni/loading-white.gif);
background-repeat: no-repeat;
background-position: left top;
padding-left: 2em;
}
#file-viewer p.help {
clear: both;
}
dl dt {
padding: 0;
margin: 0;
}
dl dd {
padding-left: 1em;
code {
font-weight: bold;
color: white;
background-color: darkgrey;
padding: 2px 6px 2px 6px;
margin: 2px;
}

Просмотреть файл

@ -19,121 +19,115 @@ if (typeof diff_match_patch !== 'undefined') {
for (var t = 0; t < lines.length; t++) {
switch (op) {
case DIFF_INSERT:
html.push(format('<div><div class="number"><a href="#L{0}" name="L{0}">{0}</a>' +
'</div><div class="code add">{1}</div></div>', k, lines[t]));
// TODO (andym): templates might work here as suggested by cvan
html.push(format('<div class="line add"><span class="number"><a href="#L{0}" name="L{0}">{0}</a> +' +
'</span><span class="code">{1}</span></div>', k, lines[t]));
k++;
break;
case DIFF_DELETE:
html.push(format('<div><div class="number"></div>' +
'<div class="code delete">{0}</div></div>', lines[t]));
html.push(format('<div class="line delete"><span class="number"> -</span>' +
'<span class="code">{0}</span></div>', lines[t]));
break;
case DIFF_EQUAL:
html.push(format('<div><div class="number"><a href="#L{0}" name="L{0}">{0}</a>' +
'</div><div class="code">{1}</div></div>', k, lines[t]));
html.push(format('<div class="line"><span class="number"><a href="#L{0}" name="L{0}">{0}</a>&nbsp;&nbsp;' +
'</span><span class="code">{1}</span></div>', k, lines[t]));
k++;
break;
}
}
}
return html.join('\n');
return html.join('');
};
}
function bind_viewer() {
function bind_viewer(nodes) {
function Viewer() {
this.$tree = $('#files ul');
this.compute = function() {
var node = $('#content');
if (node.length) {
var splitted = node.text().split('\n'),
this.nodes = nodes;
this.wrapped = true;
this.hidden = false;
this.compute = function(node) {
var $content = node.find('#content'),
$diff = node.find('#diff');
if ($content.length) {
var splitted = $content.text().split('\n'),
length = splitted.length,
html = [];
if (splitted.splice(length-1, length) == '') {
if (splitted.slice(length-1) == '') {
length = length - 1;
}
for (var k = 0; k < splitted.length; k++) {
var text = splitted[k].replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
html.push(format('<div><div class="number"><a href="#L{0}" name="L{0}">{0}</a></div>' +
'<div class="code">{1}</div></div>', k+1, text));
for (var k = 0; k < length; k++) {
if (splitted[k] !== undefined) {
var text = splitted[k].replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
html.push(format('<div class="line"><span class="number"><a href="#L{0}" name="L{0}">{0}</a></span>' +
'<span class="code"> {1}</span></div>', k+1, text));
}
}
node.html(html.join('\n'));
$content.html(html.join('')).removeClass('hidden').show();
}
if ($('#diff').length) {
if ($diff.length) {
var dmp = new diff_match_patch();
var diff = dmp.diff_main($('#file-one').text(), $('#file-two').text());
$('#diff').html(dmp.diff_prettyHtml(diff));
var diff = dmp.diff_main($diff.siblings('.left').text(), $diff.siblings('.right').text());
$diff.html(dmp.diff_prettyHtml(diff)).removeClass('hidden').show();
}
if (window.location.hash) {
window.location = window.location;
}
this.$tree.show();
};
this.show_leaf = function(names) {
/* Exposes the leaves for a given set of nodes. */
this.$tree.find('li').each(function() {
var $this = $(this),
parent = $this.attr('data-parent'),
shrt = $this.attr('data-short'),
a = $this.find('a');
if (parent && (names.indexOf(parent) > -1) &&
$this.hasClass('hidden')) {
$this.removeClass('hidden').show();
}
else if (names.length === 1 &&
(shrt.length > names[0].length) &&
(shrt.indexOf(names[0]) === 0)) {
$this.addClass('hidden').hide();
if (a.hasClass('open')) {
a.removeClass('open').addClass('closed');
}
}
if (names.indexOf($this.attr('data-short')) > -1) {
if (a.hasClass('closed')) {
a.removeClass('closed').addClass('open');
}
}
});
this.toggle_leaf = function($leaf) {
if ($leaf.hasClass('open')) {
this.hide_leaf($leaf);
} else {
this.show_leaf($leaf);
}
};
this.hide_leaf = function($leaf) {
$leaf.removeClass('open').addClass('closed')
.closest('li').next('ul')
.addClass('hidden').hide();
};
this.show_leaf = function($leaf) {
/* Exposes the leaves for a given set of node. */
$leaf.removeClass('closed').addClass('open')
.closest('li').next('ul')
.removeClass('hidden').show();
};
this.selected = function($link) {
/* Updates the tree, showing the leaves relevant to node */
var $curr = $link.closest('li'),
leaf = $curr.attr('data-parent').split('/'),
names = [];
$curr.removeClass('hidden').show();
if (leaf.length && (leaf[0])) {
for (var k = 0; k <= leaf.length; k += 1) {
names.push(leaf.slice(0, k).join('/'));
}
this.show_leaf(names);
}
/* Exposes all the leaves to an element */
$link.parentsUntil('ul.root').filter('ul')
.removeClass('hidden').show()
.each(function() {
$(this).prev('li').find('a:first')
.removeClass('closed').addClass('open');
});
};
this.load = function($link) {
/* Accepts a jQuery wrapped node, which is part of the tree.
Hides content, shows spinner, gets the content and then
shows it all. */
shows it all. Then alters the title. */
var self = this,
$thinking = $('#thinking'),
$wrapper = $('#content-wrapper');
$wrapper.hide();
$thinking.removeClass('hidden').show();
$old_wrapper = $('#content-wrapper');
$old_wrapper.hide();
this.nodes.$thinking.removeClass('hidden').show();
if (history.pushState !== undefined) {
history.pushState({ path: $link.text() }, '', $link.attr('href'));
}
$('#content-wrapper').load($link.attr('href') + ' #content-wrapper', function() {
$old_wrapper.load($link.attr('href') + ' #content-wrapper', function() {
$(this).children().unwrap();
self.compute();
$thinking.hide();
$wrapper.slideDown();
var $new_wrapper = $('#content-wrapper');
self.compute($new_wrapper);
self.nodes.$thinking.hide();
$new_wrapper.slideDown();
if (self.hidden) {
self.toggle_files('hide');
}
});
this.nodes.$title.text($link.closest('li').attr('data-short'));
};
this.select = function($link) {
/* Given a node, alters the tree and then loads the content. */
this.$tree.find('a.selected').each(function() {
this.nodes.$files.find('a.selected').each(function() {
$(this).removeClass('selected');
});
$link.addClass('selected');
@ -141,72 +135,94 @@ function bind_viewer() {
this.load($link);
};
this.get_selected = function() {
return this.$tree.find('a.selected');
var k = 0;
$.each(this.nodes.$files.find('a.file'), function(i, el) {
if ($(el).hasClass("selected")) {
k = i;
}
});
return k;
};
this.toggle_wrap = function(state) {
this.wrapped = (state == 'wrap' || !this.wrapped);
$('pre').toggleClass('wrapped', this.wrapped);
};
this.toggle_files = function(state) {
this.hidden = (state == 'hide' || !this.hidden);
if (this.hidden) {
this.nodes.$files.hide();
this.nodes.$commands.detach().appendTo('#content-wrapper');
} else {
this.nodes.$files.show();
this.nodes.$commands.detach().appendTo('#files');
}
};
}
var viewer = new Viewer();
viewer.$tree.find('.directory').click(_pd(function() {
viewer.show_leaf([$(this).closest('li').attr('data-short')]);
viewer.nodes.$files.find('.directory').click(_pd(function() {
viewer.toggle_leaf($(this));
}));
$('#files-prev').click(_pd(function() {
var $curr = viewer.get_selected().closest('li'),
choices = $curr.prevUntil('ul').find('a.file');
if (choices.length) { viewer.select($(choices[0])); }
viewer.select(viewer.nodes.$files.find('a.file').eq(viewer.get_selected() - 1));
}));
$('#files-next').click(_pd(function() {
var $curr = viewer.get_selected().closest('li'),
choices = $curr.nextUntil('ul').find('a.file');
if (choices.length) { viewer.select($(choices[0])); }
viewer.select(viewer.nodes.$files.find('a.file').eq(viewer.get_selected() + 1));
}));
$('#files-wrap').click(_pd(function() {
$('pre').addClass('wrapped');
$('#files-wrap').hide();
$('#files-unwrap').removeClass('hidden').show();
viewer.toggle_wrap();
}));
$('#files-unwrap').click(_pd(function() {
$('pre').removeClass('wrapped');
$('#files-wrap').removeClass('hidden').show();
$('#files-unwrap').hide();
$('#files-hide').click(_pd(function() {
viewer.toggle_files();
}));
$('#files-expand-all').click(_pd(function() {
viewer.$tree.find('li.hidden').removeClass('hidden').show();
viewer.$tree.find('a.directory').removeClass('closed').addClass('open');
viewer.nodes.$files.find('a.closed').each(function() {
viewer.show_leaf($(this));
});
}));
viewer.$tree.find('.file').click(_pd(function() {
viewer.nodes.$files.find('.file').click(_pd(function() {
viewer.select($(this));
$('#files-unwrap').removeClass('hidden').show();
$('#files-wrap').hide();
viewer.toggle_wrap('wrap');
}));
$(document).bind('keyup', _pd(function(e) {
if (e.keyCode === 75) {
if (e.keyCode == 72) {
$('#files-hide').trigger('click');
} else if (e.keyCode == 75) {
$('#files-next').trigger('click');
} else if (e.keyCode === 74) {
} else if (e.keyCode == 74) {
$('#files-prev').trigger('click');
} else if (e.keyCode == 87) {
$('#files-wrap').trigger('click');
} else if (e.keyCode == 69) {
$('#files-expand-all').trigger('click');
}
}));
return viewer;
}
$(document).ready(function() {
var viewer = null;
var nodes = {
$files: $('#files'),
$thinking: $('#thinking'),
$title: $('#breadcrumb'),
$commands: $('#commands')
};
function poll_file_extraction() {
$.getJSON($('#extracting').attr('data-url'), function(json) {
if (json && json.status) {
$('#file-viewer').load(window.location.pathname + ' #file-viewer', function() {
viewer = bind_viewer();
viewer = bind_viewer(nodes);
viewer.selected(viewer.$tree.find('a.selected'));
viewer.compute();
viewer.compute($('#content-wrapper'));
});
} else {
setTimeout(poll_file_extraction, 2000);
@ -217,8 +233,8 @@ $(document).ready(function() {
if ($('#extracting').length) {
poll_file_extraction();
} else if ($('#file-viewer').length) {
viewer = bind_viewer();
viewer.selected(viewer.$tree.find('a.selected'));
viewer.compute();
viewer = bind_viewer(nodes);
viewer.selected(viewer.nodes.$files.find('a.selected'));
viewer.compute($('#content-wrapper'));
}
});

Просмотреть файл

@ -1,12 +1,25 @@
$(document).ready(function(){
var fileViewer = {
setup: function() {
this.sandbox = tests.createSandbox('#files-wrapper');
},
teardown: function() {
this.sandbox.remove();
}
};
module('File viewer');
module('File viewer', fileViewer);
test('Show leaf', function() {
var viewer = bind_viewer();
viewer.show_leaf(['foo']);
equal($($('#files li a')[1]).hasClass('open'), true);
equal($($('#files li')[2]).hasClass('hidden'), false);
var nodes = {
$files: this.sandbox.find($('#files'))
};
var viewer = bind_viewer(nodes);
viewer.toggle_leaf(this.sandbox.find('a.directory'));
equal(this.sandbox.find('a.directory').hasClass('open'), true);
equal(this.sandbox.find('ul').hasClass('hidden'), false);
viewer.toggle_leaf(this.sandbox.find('a.directory'));
equal(this.sandbox.find('a.directory').hasClass('open'), false);
equal(this.sandbox.find('ul').hasClass('hidden'), true);
});
});

Просмотреть файл

@ -255,18 +255,22 @@
</div>
</div>
</div>
<div id="files">
<div id="files-wrapper">
<div id="files>
<ul>
<li class="" style="padding-left: 0em;" data-parent="" data-short="someurl">
<li>
<a class="file" href="">someurl</a>
</li>
<li style="padding-left: 1em; display: list-item;" data-parent="" data-short="foo">
<li>
<a class="directory closed" href="">foo</a>
</li>
<li class="hidden" style="padding-left: 1em; display: list-item;" data-parent="foo" data-short="foo/bar.txt">
<a class="file" href="someurl">foo/bar.txt</a>
</li>
<ul class="hidden">
<li>
<a class="file" href="someurl">foo/bar.txt</a>
</li>
</ul>
</ul>
</div>
</div>
<div id="paypal">
<div class="contribute">