diff --git a/mongodbforms/documents.py b/mongodbforms/documents.py index cecabd0d..6cbe1c6a 100644 --- a/mongodbforms/documents.py +++ b/mongodbforms/documents.py @@ -116,7 +116,21 @@ def construct_instance(form, instance, fields=None, exclude=None): elif isinstance(f, ListField): list_field = getattr(instance, f.name) uploads = cleaned_data[f.name] + idx_to_pop = [] for i, uploaded_file in enumerate(uploads): + if isinstance(uploaded_file, list): # ListOfFilesWidget + uploaded_file, to_delete = uploaded_file + if to_delete: + try: + list_field[i].delete() + idx_to_pop.append(i) + except IndexError: + # someone checked the delete box of the last + # form item, which obviously doesnt exist + # on the list + pass + continue + if uploaded_file is None: continue try: @@ -129,6 +143,10 @@ def construct_instance(form, instance, fields=None, exclude=None): list_field[i] = file_obj except IndexError: list_field.append(file_obj) + + for idx in reversed(idx_to_pop): + del list_field[idx] + setattr(instance, f.name, list_field) else: field = getattr(instance, f.name) diff --git a/mongodbforms/fields.py b/mongodbforms/fields.py index 6f4817d4..4776548d 100644 --- a/mongodbforms/fields.py +++ b/mongodbforms/fields.py @@ -32,7 +32,7 @@ except ImportError: from pymongo.errors import InvalidId -from mongodbforms.widgets import ListWidget, MapWidget, HiddenMapWidget +from mongodbforms.widgets import ListWidget, MapWidget, HiddenMapWidget, ListOfFilesWidget class MongoChoiceIterator(object): @@ -398,3 +398,38 @@ def _has_changed(self, initial, data): if self.contained_field._has_changed(init_val, v): return True return False + + +class ListOfFilesField(ListField): + widget = ListOfFilesWidget + + def clean(self, value): + """ + We clean every subwidget. + """ + clean_data = [] + errors = ErrorList() + is_empty = not value or not [v for v in value if v not in self.empty_values] + if is_empty and not self.required: + return [] + + if is_empty and self.required: + raise ValidationError(self.error_messages['required']) + + if value and not isinstance(value, (list, tuple)): + raise ValidationError(self.error_messages['invalid']) + + for field_value, checkbox_value in value: + try: + clean_data.append([self.contained_field.clean(field_value), checkbox_value]) + except ValidationError as e: + errors.extend(e.messages) + # FIXME: copy paste from above + if self.contained_field.required: + self.contained_field.required = False + if errors: + raise ValidationError(errors) + + self.validate(clean_data) + self.run_validators(clean_data) + return clean_data diff --git a/mongodbforms/static/mongodbforms/list_of_files_widget.js b/mongodbforms/static/mongodbforms/list_of_files_widget.js new file mode 100644 index 00000000..2f160a2c --- /dev/null +++ b/mongodbforms/static/mongodbforms/list_of_files_widget.js @@ -0,0 +1,48 @@ +$(document).ready(function() { + var itemTemplate, + lastItemId, newItemId, + count = $(".js-widget-item").length, + fieldNameMatch = $(".js-widget-last-item input:first").attr("id").match(/id_([a-z_]+)_\d+_\d+/), + fieldName = fieldNameMatch ? fieldNameMatch[1] : null; + + if (!fieldName) { + throw new Error("Missing field name."); + } + + lastItemId = fieldName + "_" + (count - 1) + "_"; + newItemId = fieldName + "_{count}_"; + itemTemplate = $(".js-widget-last-item").prop("outerHTML"); + itemTemplate = itemTemplate.replace("id_" + lastItemId + "0", "id_" + newItemId + "0"); + itemTemplate = itemTemplate.replace("id_" + lastItemId + "1", "id_" + newItemId + "1"); + itemTemplate = itemTemplate.replace(lastItemId + "0", newItemId + "0"); + itemTemplate = itemTemplate.replace(lastItemId + "1", newItemId + "1"); + + var $lastItem; + + function addNewItem () { + var $item = $(itemTemplate.replace(/\{count\}/g, count)); + count += 1; + $item.insertAfter($lastItem); + $lastItem.removeClass("js-widget-last-item"); + stopListening($lastItem); + $lastItem = $item; + startListening($lastItem); + } + + function startListening ($item) { + $("input:first", $item).on("change.listWidget", _onLastItemChange); + } + + function stopListening ($item) { + $("input:first", $item).off("change.listWidget", "**", _onLastItemChange); + } + + function _onLastItemChange () { + if ($(this).val()) { + addNewItem(); + } + } + + $lastItem = $(".js-widget-last-item"); + startListening($lastItem); +}); diff --git a/mongodbforms/templates/mongodbforms/list_of_files_widget.html b/mongodbforms/templates/mongodbforms/list_of_files_widget.html new file mode 100644 index 00000000..4834ac57 --- /dev/null +++ b/mongodbforms/templates/mongodbforms/list_of_files_widget.html @@ -0,0 +1,9 @@ + diff --git a/mongodbforms/templates/mongodbforms/list_widget.html b/mongodbforms/templates/mongodbforms/list_widget.html new file mode 100644 index 00000000..668fe5a2 --- /dev/null +++ b/mongodbforms/templates/mongodbforms/list_widget.html @@ -0,0 +1,5 @@ + diff --git a/mongodbforms/widgets.py b/mongodbforms/widgets.py index 77acbc12..9ede400d 100644 --- a/mongodbforms/widgets.py +++ b/mongodbforms/widgets.py @@ -1,8 +1,9 @@ import copy -from django.forms.widgets import (Widget, Media, TextInput, +from django.forms.widgets import (Widget, Media, TextInput, FileInput, SplitDateTimeWidget, DateInput, TimeInput, - MultiWidget, HiddenInput) + MultiWidget, HiddenInput, CheckboxInput) +from django.template.loader import render_to_string from django.utils.safestring import mark_safe from django.core.validators import EMPTY_VALUES from django.forms.util import flatatt @@ -59,6 +60,8 @@ def __deepcopy__(self, memo): class ListWidget(BaseContainerWidget): + template = "mongodbforms/list_widget.html" + def render(self, name, value, attrs=None): if value is not None and not isinstance(value, (list, tuple)): raise TypeError( @@ -78,6 +81,16 @@ def render(self, name, value, attrs=None): ) return mark_safe(self.format_output(output)) + def format_output(self, rendered_widgets): + """ + Given a list of rendered widgets (as strings), returns a Unicode string + representing the HTML for the whole lot. + + This hook allows you to format the HTML design of the widgets, if + needed. + """ + return render_to_string(self.template, {"widgets": rendered_widgets}) + def value_from_datadict(self, data, files, name): widget = self.data_widget i = 0 @@ -176,3 +189,47 @@ def __init__(self, attrs=None): data_widget = HiddenInput() super(MapWidget, self).__init__(data_widget, attrs) self.key_widget = HiddenInput() + + +class DeletableFileWidget(MultiWidget): + + default_delete_label = "Delete this file." + + def __init__(self, file_widget=FileInput, attrs=None, delete_label=None): + widgets = [file_widget, CheckboxInput] + self.delete_label = delete_label or self.default_delete_label + super(DeletableFileWidget, self).__init__(widgets, attrs) + + def decompress(self, value): + return [value, False] + + def value_from_datadict(self, data, files, name): + filename = name + '_0' + if filename not in data and filename not in files: + return None + return super(DeletableFileWidget, self).value_from_datadict(data, files, name) + + def format_output(self, rendered_widgets): + label = "" % self.delete_label + return super(DeletableFileWidget, self).format_output(rendered_widgets) + label + + +class ListOfFilesWidget(ListWidget): + template = "mongodbforms/list_of_files_widget.html" + + class Media: + js = ('mongodbforms/list_of_files_widget.js', ) + + def __init__(self, contained_widget=None, attrs=None, delete_label=None): + super(ListOfFilesWidget, self).__init__(DeletableFileWidget(contained_widget, attrs, delete_label), attrs) + + def value_from_datadict(self, data, files, name): + widget = self.data_widget + i = 0 + ret = [] + value = widget.value_from_datadict(data, files, name + '_%s' % i) + while value is not None: + ret.append(value) + i = i + 1 + value = widget.value_from_datadict(data, files, name + '_%s' % i) + return ret diff --git a/setup.py b/setup.py index c5bf5ae4..12a3fb85 100755 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ #!/usr/bin/env python -from setuptools import setup +from setuptools import setup, find_packages from subprocess import call def convert_readme(): @@ -17,7 +17,7 @@ def convert_readme(): author='Jan Schrewe', author_email='jan@schafproductions.com', url='http://www.schafproductions.com/projects/django-mongodb-forms/', - packages=['mongodbforms',], + packages=find_packages(), classifiers=[ 'Development Status :: 3 - Alpha', 'Environment :: Web Environment', @@ -30,6 +30,9 @@ def convert_readme(): license='New BSD License', long_description=convert_readme(), include_package_data=True, + package_data={ + "mongodbforms": ['templates/mongodbforms/*', 'static/mongodbforms/*'] + }, zip_safe=False, install_requires=['setuptools', 'django>=1.4', 'mongoengine>=0.8.3',], )