Skip to content

Commit a223f34

Browse files
committed
Working now with inline 'add another'. Max length now has a default of 255 so is an optional param. Javascript refactor.
1 parent 36d1209 commit a223f34

File tree

6 files changed

+378
-102
lines changed

6 files changed

+378
-102
lines changed

README.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,4 @@ Installation
1515
* from browse_and_upload_field.fields import FileBrowseAndUploadField
1616
* Use in the same way as the existing FileBrowseField and UploadField
1717

18-
my_image = FileBrowseAndUploadField(max_length=512, null=True, blank=True, format="image", directory='images', upload_to='images')
18+
my_image = FileBrowseAndUploadField(null=True, blank=True, format="image", directory='images', upload_to='images')

browse_and_upload_field/fields.py

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,13 @@ class Media:
3131
js = (
3232
'filebrowser/js/AddFileBrowser.js',
3333
'filebrowser/js/fileuploader.js',
34+
'filebrowser/js/insQ.js',
35+
'filebrowser/js/custom_upload_field.js',
3436
)
3537
css = {
36-
'all': (os.path.join('/static/filebrowser/css/uploadfield.css'),)
38+
'all': (
39+
'filebrowser/css/uploadfield.css',
40+
)
3741
}
3842

3943
def __init__(self, attrs=None):
@@ -59,18 +63,19 @@ def render(self, name, value, attrs=None):
5963
final_attrs = self.build_attrs(attrs, type=self.input_type, name=name)
6064
final_attrs['search_icon'] = static('filebrowser/img/filebrowser_icon_show.gif')
6165
final_attrs['url'] = url
62-
final_attrs['directory'] = self.directory
66+
final_attrs['directory'] = self.directory or self.upload_to
6367
final_attrs['filebrowser_directory'] = self.site.directory
6468
final_attrs['extensions'] = self.extensions
6569
final_attrs['format'] = self.format
6670
final_attrs['upload_to'] = self.upload_to
6771
final_attrs['temp_upload_dir'] = UPLOAD_TEMPDIR
6872
final_attrs['ADMIN_THUMBNAIL'] = ADMIN_THUMBNAIL
6973
filebrowser_site = self.site
70-
if value != "":
74+
if value:
75+
# If we have an image then filebrowser should open to that directory rather than the default
7176
try:
7277
final_attrs['directory'] = os.path.split(value.original.path_relative_directory)[0]
73-
except:
78+
except Exception:
7479
pass
7580
return render_to_string("filebrowser/custom_browse_and_upload_field.html", locals())
7681

@@ -114,6 +119,7 @@ def __init__(self, *args, **kwargs):
114119
self.format = kwargs.pop('format', '')
115120
self.upload_to = kwargs.pop('upload_to', '')
116121
self.temp_upload_dir = kwargs.pop('temp_upload_dir', '') or UPLOAD_TEMPDIR
122+
kwargs['max_length'] = kwargs.get('max_length', False) or 255
117123
return super(FileBrowseAndUploadField, self).__init__(*args, **kwargs)
118124

119125
def to_python(self, value):
@@ -158,19 +164,15 @@ def formfield(self, **kwargs):
158164
return super(FileBrowseAndUploadField, self).formfield(**defaults)
159165

160166
def upload_callback(self, sender, instance, created, using, **kwargs):
161-
167+
162168
opts = instance._meta
163169
fields = [f.name for f in opts.fields]
164170

165-
if "image" in fields and instance.image:
166-
167-
try:
168-
filename = os.path.basename(smart_text(instance.image)).split("__")[1]
169-
except:
170-
filename = os.path.basename(smart_text(instance.image))
171+
if "image" in fields and instance.image: # TODO this hardcodes 'image' as the field name
171172

173+
filename = os.path.basename(smart_text(instance.image))
172174
upload_to = opts.get_field("image").upload_to
173-
filebrowser_directory = opts.get_field("image").directory or self.site.directory
175+
filebrowser_directory = self.site.directory
174176

175177
if getattr(upload_to, '__call__', None):
176178
upload_to = os.path.split(upload_to(instance, filename))[0]
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
(function($){
2+
$(document).ready(function() {
3+
4+
function initUploader($el) {
5+
6+
var action = $el.data('action');
7+
var csrf_token = $el.data('title');
8+
var dir = $el.data('directory');
9+
var folder = $el.data('folder');
10+
var format = $el.data('format');
11+
var id_prefix = $el.data('input-id');
12+
var title = $el.data('title');
13+
var url = $el.data('url') + '?pop=1';
14+
15+
if (dir !== undefined) {
16+
url += '&dir=' + dir;
17+
}
18+
if (format !== undefined) {
19+
url += '&type=' + format;
20+
}
21+
22+
$('#' + id_prefix + '-fb_show').click(function() {
23+
24+
FileBrowser.show(
25+
id_prefix,
26+
url,
27+
function(){
28+
// Set the text label when the filebrowser window is closed
29+
var pathParts = $('#' + id_prefix).val().split('/');
30+
var filename = pathParts[pathParts.length -1 ];
31+
$('#' + id_prefix + '-textLabel').text(filename);
32+
});
33+
});
34+
var uploader = new qq.FileUploader({
35+
36+
element: $el[0],
37+
action: action,
38+
39+
template: '<div class="fb-uploader">' +
40+
'<div class="fb-upload-list"></div>' +
41+
'<a href="javascript://" class="fb-upload-button" title="' + title + '">&nbsp;</a>' +
42+
'<div class="fb-upload-drop-area"><span>Drop files here to upload</span></div>' +
43+
'</div>',
44+
45+
// Template for one item in file list
46+
fileTemplate: '<div class="fb-upload-item">' +
47+
'<span class="fb-upload-file"></span>' +
48+
'<span class="fb-upload-spinner">&nbsp;</span>' +
49+
'<span class="fb-upload-size"></span>' +
50+
'<a class="fb-upload-cancel" href="#"></a>' +
51+
'<span class="fb-upload-failed-text"></span>' +
52+
'<div class="progress-bar">' +
53+
'<div class="content"></div>' +
54+
'</div>' +
55+
'</div>',
56+
57+
classes: {
58+
// Used to get elements from templates
59+
button: 'fb-upload-button',
60+
drop: 'fb-upload-drop-area',
61+
dropActive: 'fb-upload-drop-area-active',
62+
list: 'fb-upload-list',
63+
64+
file: 'fb-upload-file',
65+
spinner: 'fb-upload-spinner',
66+
size: 'fb-upload-size',
67+
cancel: 'fb-upload-cancel',
68+
69+
// Added to list item when upload completes
70+
// Used in css to hide progress spinner
71+
success: 'fb-upload-success',
72+
fail: 'fb-upload-fail'
73+
},
74+
75+
params: {
76+
'csrf_token': csrf_token,
77+
'csrf_name': 'csrfmiddlewaretoken',
78+
'csrf_xname': 'X-CSRFToken',
79+
'temporary': 'true',
80+
'folder': folder
81+
},
82+
83+
minSizeLimit: 0,
84+
debug: false,
85+
86+
getItem: function(id) {
87+
var items = $(this.element).find('.fb-upload-file').get();
88+
var item = items.pop();
89+
90+
while (typeof item != "undefined") {
91+
if (item.qqFileId == id) {
92+
return $(item);
93+
}
94+
item = items.pop();
95+
}
96+
},
97+
98+
onProgress: function(id, fileName, loaded, total){
99+
var bar = $(this.element).find('.progress-bar .content');
100+
var new_width = '' + (loaded/total * 100) + '%';
101+
bar.css('width', new_width);
102+
},
103+
104+
onComplete: function(id, fileName, resp){
105+
if (resp.success) {
106+
$(this.element).find('.progress-bar').fadeOut();
107+
$('#' + id_prefix).val(resp.temp_filename);
108+
$('#' + id_prefix + '-textLabel').text(resp.filename);
109+
var previewContainer = $el.parent().parent().find('p.preview');
110+
previewContainer.find('a').attr('href', resp.url);
111+
previewContainer.find('img').attr('src', resp.admin_thumbnail_url);
112+
previewContainer.show();
113+
}
114+
},
115+
116+
showMessage: function(message){
117+
alert(message);
118+
}
119+
120+
});
121+
122+
// Fix error in django.formset.js caused by the hidden uploader input with no id
123+
var container = $el.closest('.fb-uploadfield');
124+
var id = container.find('.vFileBrowseField').get(-1).id;
125+
container.find('.fb-upload-button input').attr('id', id + '-uploadInput');
126+
127+
}
128+
129+
// See https://github.com/naugtur/insertionQuery
130+
// Hook to run code after 'add another' has been used on inlines
131+
// Hooking into the add-row click was unreliable and the only other options were timeout polling
132+
// For Django 1.9+ we can use https://code.djangoproject.com/ticket/15760 instead
133+
insertionQ('.fb-uploadfield').every(function(element){
134+
135+
var $el = $(element);
136+
var container = $el.find('.fb-uploader-container');
137+
var inlineRowCount = container.closest('.inline-group').find('.form-row').not('.empty-form').length - 1;
138+
var inputId = container.data('input-id');
139+
container.data('input-id', inputId.replace('__prefix__', inlineRowCount));
140+
initUploader(container);
141+
142+
});
143+
144+
$('.fb-uploader-container').each(function(index) {
145+
var $el = $(this);
146+
initUploader($el);
147+
});
148+
149+
});
150+
151+
})(django.jQuery);
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
var insertionQ = (function () {
2+
"use strict";
3+
4+
var sequence = 100,
5+
isAnimationSupported = false,
6+
animationstring = 'animationName',
7+
keyframeprefix = '',
8+
domPrefixes = 'Webkit Moz O ms Khtml'.split(' '),
9+
pfx = '',
10+
elm = document.createElement('div'),
11+
options = {
12+
strictlyNew: true,
13+
timeout: 20
14+
};
15+
16+
if (elm.style.animationName) {
17+
isAnimationSupported = true;
18+
}
19+
20+
if (isAnimationSupported === false) {
21+
for (var i = 0; i < domPrefixes.length; i++) {
22+
if (elm.style[domPrefixes[i] + 'AnimationName'] !== undefined) {
23+
pfx = domPrefixes[i];
24+
animationstring = pfx + 'AnimationName';
25+
keyframeprefix = '-' + pfx.toLowerCase() + '-';
26+
isAnimationSupported = true;
27+
break;
28+
}
29+
}
30+
}
31+
32+
33+
function listen(selector, callback) {
34+
var styleAnimation, animationName = 'insQ_' + (sequence++);
35+
36+
var eventHandler = function (event) {
37+
if (event.animationName === animationName || event[animationstring] === animationName) {
38+
if (!isTagged(event.target)) {
39+
callback(event.target);
40+
}
41+
}
42+
};
43+
44+
styleAnimation = document.createElement('style');
45+
styleAnimation.innerHTML = '@' + keyframeprefix + 'keyframes ' + animationName + ' { from { outline: 1px solid transparent } to { outline: 0px solid transparent } }' +
46+
"\n" + selector + ' { animation-duration: 0.001s; animation-name: ' + animationName + '; ' +
47+
keyframeprefix + 'animation-duration: 0.001s; ' + keyframeprefix + 'animation-name: ' + animationName + '; ' +
48+
' } ';
49+
50+
document.head.appendChild(styleAnimation);
51+
52+
var bindAnimationLater = setTimeout(function () {
53+
document.addEventListener('animationstart', eventHandler, false);
54+
document.addEventListener('MSAnimationStart', eventHandler, false);
55+
document.addEventListener('webkitAnimationStart', eventHandler, false);
56+
//event support is not consistent with DOM prefixes
57+
}, options.timeout); //starts listening later to skip elements found on startup. this might need tweaking
58+
59+
return {
60+
destroy: function () {
61+
clearTimeout(bindAnimationLater);
62+
if (styleAnimation) {
63+
document.head.removeChild(styleAnimation);
64+
styleAnimation = null;
65+
}
66+
document.removeEventListener('animationstart', eventHandler);
67+
document.removeEventListener('MSAnimationStart', eventHandler);
68+
document.removeEventListener('webkitAnimationStart', eventHandler);
69+
}
70+
};
71+
}
72+
73+
74+
function tag(el) {
75+
el.QinsQ = true; //bug in V8 causes memory leaks when weird characters are used as field names. I don't want to risk leaking DOM trees so the key is not '-+-' anymore
76+
}
77+
78+
function isTagged(el) {
79+
return (options.strictlyNew && (el.QinsQ === true));
80+
}
81+
82+
function topmostUntaggedParent(el) {
83+
if (isTagged(el.parentNode)) {
84+
return el;
85+
} else {
86+
return topmostUntaggedParent(el.parentNode);
87+
}
88+
}
89+
90+
function tagAll(e) {
91+
tag(e);
92+
e = e.firstChild;
93+
for (; e; e = e.nextSibling) {
94+
if (e !== undefined && e.nodeType === 1) {
95+
tagAll(e);
96+
}
97+
}
98+
}
99+
100+
//aggregates multiple insertion events into a common parent
101+
function catchInsertions(selector, callback) {
102+
var insertions = [];
103+
//throttle summary
104+
var sumUp = (function () {
105+
var to;
106+
return function () {
107+
clearTimeout(to);
108+
to = setTimeout(function () {
109+
insertions.forEach(tagAll);
110+
callback(insertions);
111+
insertions = [];
112+
}, 10);
113+
};
114+
})();
115+
116+
return listen(selector, function (el) {
117+
if (isTagged(el)) {
118+
return;
119+
}
120+
tag(el);
121+
var myparent = topmostUntaggedParent(el);
122+
if (insertions.indexOf(myparent) < 0) {
123+
insertions.push(myparent);
124+
}
125+
sumUp();
126+
});
127+
}
128+
129+
//insQ function
130+
var exports = function (selector) {
131+
if (isAnimationSupported && selector.match(/[^{}]/)) {
132+
133+
if (options.strictlyNew) {
134+
tagAll(document.body); //prevents from catching things on show
135+
}
136+
return {
137+
every: function (callback) {
138+
return listen(selector, callback);
139+
},
140+
summary: function (callback) {
141+
return catchInsertions(selector, callback);
142+
}
143+
};
144+
} else {
145+
return false;
146+
}
147+
};
148+
149+
//allows overriding defaults
150+
exports.config = function (opt) {
151+
for (var o in opt) {
152+
if (opt.hasOwnProperty(o)) {
153+
options[o] = opt[o];
154+
}
155+
}
156+
};
157+
158+
return exports;
159+
})();
160+
161+
if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
162+
module.exports = insertionQ;
163+
}

browse_and_upload_field/static/filebrowser/js/insQ.min.js

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)