Skip to content
This repository was archived by the owner on Apr 24, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions mongodbforms/documents.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = []

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Docstring/comments explaining what we're trying to achieve and how? As discussed the enumerate is a very important component and is slightly deviously hidden.

for i, uploaded_file in enumerate(uploads):
if isinstance(uploaded_file, list): # ListOfFilesWidget

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you test instead on isinstance(f, ListOfFilesWidget) as is done above? Not a huge fan of this, seems like dark magic, and in reality makes the to_delete aspect a bit more complicated than it perhaps needs to be?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes indeed

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:
Expand All @@ -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)
Expand Down
37 changes: 36 additions & 1 deletion mongodbforms/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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):

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Docstring? Complex method, worth summarizing the things it'll check.

"""
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'])

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be simplified to reduce indent levels with something like

if not value and not self.required:
    return []

if value and not isinstance(value, (list, tuple)):
    raise ValidationError(self.error_messages['invalid'])

etc. Simply making sure that the approach is to return a value as quickly as possible by eliminating cases rather than drilling down exhaustively.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes I agree, it's copy-paste from above :(


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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is worth a comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, at which point do we check that we have all contained_field requirements fulfilled?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also copy-paste :(

if errors:
raise ValidationError(errors)

self.validate(clean_data)
self.run_validators(clean_data)
return clean_data
48 changes: 48 additions & 0 deletions mongodbforms/static/mongodbforms/list_of_files_widget.js
Original file line number Diff line number Diff line change
@@ -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;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Race condition? Should you increase count first, perhaps with

var $item = $(itemTemplate.replace(/\{count\}/g, ++count);

Doesn't matter that much, dunno. (actually that's not better, and you'd have to do count++, and maintain an internal count that's one less than currently).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how can it be a race condition?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you click twice really fast to add a new item

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there is no + button

$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);
});
9 changes: 9 additions & 0 deletions mongodbforms/templates/mongodbforms/list_of_files_widget.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<ul>
{% for widget in widgets %}
{% if forloop.last %}
<li class="js-widget-item js-widget-last-item">{{ widget|safe }}</li>
{% else %}
<li class="js-widget-item">{{ widget|safe }}</li>
{% endif %}
{% endfor %}
</ul>
5 changes: 5 additions & 0 deletions mongodbforms/templates/mongodbforms/list_widget.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<ul>
{% for widget in widgets %}
<li>{{ widget|safe }}</li>
{% endfor %}
</ul>
61 changes: 59 additions & 2 deletions mongodbforms/widgets.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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(
Expand All @@ -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
Expand Down Expand Up @@ -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 = "<label>%s</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
7 changes: 5 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
@@ -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():
Expand All @@ -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',
Expand All @@ -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',],
)