diff options
author | Rogelio Mita <rogeliomita@activitycentral.com> | 2013-03-21 00:23:01 (GMT) |
---|---|---|
committer | Rogelio Mita <rogeliomita@activitycentral.com> | 2013-03-25 21:51:48 (GMT) |
commit | 09ae6b31b5bc865046911b5c8db1ae64ad96958e (patch) | |
tree | 8644ae08595f3de98d45bb7060606eb09cc60715 | |
parent | d92a0318216818fd65286ccaefdf581327221c83 (diff) |
Keeping img options temporarily while the structure form is filling up
-rw-r--r-- | .gitignore | 3 | ||||
-rw-r--r-- | webapp/README | 3 | ||||
-rw-r--r-- | webapp/polls/models.py | 84 | ||||
-rw-r--r-- | webapp/polls/templates/mustache/option_image_thumbnail.html | 7 | ||||
-rw-r--r-- | webapp/polls/templates/mustache/option_image_upload.html (renamed from webapp/polls/templates/mustache/option_image.html) | 4 | ||||
-rw-r--r-- | webapp/polls/templates/poll-structure-form.html | 2 | ||||
-rw-r--r-- | webapp/polls/templates/tags/structure.html | 7 | ||||
-rw-r--r-- | webapp/polls/templatetags/poll_tags.py | 2 | ||||
-rw-r--r-- | webapp/polls/tests.py | 37 | ||||
-rw-r--r-- | webapp/polls/views.py | 12 | ||||
-rw-r--r-- | webapp/requirements | 3 | ||||
-rw-r--r-- | webapp/utils/data_structure.py | 7 | ||||
-rw-r--r-- | webapp/utils/test.py | 32 | ||||
-rw-r--r-- | webapp/webapp/media/image_options/empty | 0 | ||||
-rw-r--r-- | webapp/webapp/settings.py | 2 | ||||
-rw-r--r-- | webapp/webapp/static/js/dynamic_structure.js | 48 |
16 files changed, 231 insertions, 22 deletions
@@ -1,6 +1,7 @@ *.pyc *.pyo *.bak -webapp/webapp/media/output/*.json +webapp/webapp/media/image_options/* +!webapp/webapp/media/image_options/empty webapp/webapp/env_settings.py database.db
\ No newline at end of file diff --git a/webapp/README b/webapp/README index 2a75dd5..4121de7 100644 --- a/webapp/README +++ b/webapp/README @@ -1,5 +1,8 @@ === dev env === +system requirements: +- libjpeg + python version: 2.7.3 1 - create virtualenv (example: mkvirtualenv --no-site-packages polls) diff --git a/webapp/polls/models.py b/webapp/polls/models.py index 609b38e..8b090c3 100644 --- a/webapp/polls/models.py +++ b/webapp/polls/models.py @@ -1,10 +1,16 @@ # -*- encoding: utf-8 -*- import time import json +import os from exceptions import * from bson import ObjectId, DBRef +from django.conf import settings +from django.forms.fields import ImageField +from django.core.exceptions import ValidationError as DjangoValidationError +from django.core.files.uploadedfile import InMemoryUploadedFile + from utils.mongo_connection import get_db @@ -127,21 +133,38 @@ class AbastractObject(AbstracErrorObject): class Option(AbastractObject): - def __init__(self, id, text): + def __init__(self, id, text=None, img=None, img_name=None): super(Option, self).__init__() self.id = id self.text = text + self.img = img + self.img_name = img_name @staticmethod def from_dict(data): option = Option( id=data.get('id', None), text=data.get('text', None), + img=data.get('img', None), + img_name=data.get('img_name', None) ) return option + def validate(self): + self.errors = [] + + if self.img and isinstance(self.img, InMemoryUploadedFile): + try: + ImageField().to_python(self.img) + except DjangoValidationError, e: + img_name = self.img.name + self.errors.append('%s: %s' % (img_name, e.messages[0])) + + if len(self.errors): + raise Option.ValidationError(str(self.errors)) + class Field(AbastractObject): @@ -161,6 +184,16 @@ class Field(AbastractObject): "Respuesta con lista de opciones: necesita " "al menos una opción." ), + 'ImageCheckBox': ( + lambda f: f.options and len(f.options) > 0, + "Respuesta con checks (multiple selección): necesita " + "al menos una imagen de opción." + ), + 'ImageRadioButton': ( + lambda f: f.options and len(f.options) > 1, + "Respuesta con radios (Solo una selección): necesita " + "al menos dos imagenes de opciones." + ), 'TextInput': (lambda f: True, ""), } @@ -169,7 +202,7 @@ class Field(AbastractObject): self.name = name self.key = key - self.options = None + self.options = [] self.dependence = None if widget_type and widget_type not in dict(WIDGET_TYPES).keys(): @@ -205,7 +238,11 @@ class Field(AbastractObject): opt = Option.from_dict(opt_data) self.options = self.options if self.options is not None else [] - if opt_data['text'] and opt not in self.options: + if opt_data.get('text', None) and opt not in self.options: + self.options.append(opt) + + img = opt_data.get('img', None) or opt_data.get('img_name', None) + if img and opt not in self.options: self.options.append(opt) def add_dependence(self, dependence): @@ -217,6 +254,13 @@ class Field(AbastractObject): if not rule(self): self.errors.append(msg) + # Validate option of current field + for opt in self.options: + try: + opt.validate() + except Option.ValidationError: + self.errors += opt.errors + options_id = [opt.id for opt in options] # TODO: Test @@ -389,9 +433,15 @@ class Structure(AbastractObject): options = field_obj.options if field_obj.options else [] for option in options: _dic_field = _dict[group_order]['fields'][field_order] + + # TODO: Check when opt_data = {} + opt_data = {'text': option.text} + if option.img_name: + opt_data = {'img_name': option.img_name} _dic_field['options'].update( - {'%s' % option.id: {'text': option.text}} + {'%s' % option.id: opt_data} ) + return {'groups': _dict} def save(self): @@ -430,3 +480,29 @@ class Structure(AbastractObject): structure.poll = Poll.get(poll_id) return structure + + def save_image_options(self, tmp=False): + fields = reduce( + lambda x, y: x + y, [g.fields for g in self.groups], []) + options = reduce( + lambda x, y: x + y, [f.options or [] for f in fields], []) + + valid_img_options = filter(lambda opt: not len(opt.errors), options) + + if tmp and self.id: + path = settings.IMAGE_OPTIONS_ROOT + '/%s/tmp' % self.id + try: + os.makedirs(path) + except OSError: + pass + + # Storing image + for img_opt in valid_img_options: + if isinstance(img_opt.img, InMemoryUploadedFile): + fileExtension = os.path.splitext(img_opt.img.name)[1] + opt_img_name = '%s%s' % (img_opt.id, fileExtension) + with open(path + '/%s' % opt_img_name, 'wb+') as dst: + for chunk in img_opt.img.chunks(): + dst.write(chunk) + dst.close() + img_opt.img_name = opt_img_name diff --git a/webapp/polls/templates/mustache/option_image_thumbnail.html b/webapp/polls/templates/mustache/option_image_thumbnail.html new file mode 100644 index 0000000..f0ff65a --- /dev/null +++ b/webapp/polls/templates/mustache/option_image_thumbnail.html @@ -0,0 +1,7 @@ +<!-- Template: Field Image option thumbnail --> +<script type="text/x-mustache-template" name="field_option_image_thumbnail"> + <input type="hidden" name="groups.[[ group_order ]].fields.[[ field_order ]].options.[[ id ]].img_name" value="[[ img_name ]]" /> + <div class="thumbnail" style="width: 200px; height: 150px;"> + <img src="/media/image_options/[[ STRUCTURE_ID ]]/tmp/[[ img_name ]]" style="width: 200px; height: 150px;" /> + </div> +</script>
\ No newline at end of file diff --git a/webapp/polls/templates/mustache/option_image.html b/webapp/polls/templates/mustache/option_image_upload.html index b8cb19b..e81f797 100644 --- a/webapp/polls/templates/mustache/option_image.html +++ b/webapp/polls/templates/mustache/option_image_upload.html @@ -1,5 +1,5 @@ -<!-- Template: Field --> -<script type="text/x-mustache-template" name="field_option_image"> +<!-- Template: Field Image option upload --> +<script type="text/x-mustache-template" name="field_option_image_upload"> <div class="fileupload fileupload-new" data-provides="fileupload"> <div class="fileupload-new thumbnail" style="width: 200px; height: 150px;"> diff --git a/webapp/polls/templates/poll-structure-form.html b/webapp/polls/templates/poll-structure-form.html index 9303558..7724946 100644 --- a/webapp/polls/templates/poll-structure-form.html +++ b/webapp/polls/templates/poll-structure-form.html @@ -34,7 +34,7 @@ </div> {% endif %} - <form class="form-inline" method="post" action=""> + <form class="form-inline" method="post" action="" enctype="multipart/form-data"> {% if structure.id %} <input type="hidden" name="id" value="{{ structure.id }}" /> diff --git a/webapp/polls/templates/tags/structure.html b/webapp/polls/templates/tags/structure.html index 427cd81..c02abc0 100644 --- a/webapp/polls/templates/tags/structure.html +++ b/webapp/polls/templates/tags/structure.html @@ -17,7 +17,9 @@ {% include "mustache/option_default.html" %} -{% include "mustache/option_image.html" %} +{% include "mustache/option_image_upload.html" %} + +{% include "mustache/option_image_thumbnail.html" %} <!-- Global variables for dynamic_structure.js --> <script type="text/javascript"> @@ -26,7 +28,8 @@ WIDGET_TYPES = {{ WIDGET_TYPES }}, WITH_OPTIONS = {{ WITH_OPTIONS }}, WITH_IMAGES = {{ WITH_IMAGES }}, - OFFSET_OPTION_ID = {{ OFFSET_OPTION_ID }}; + OFFSET_OPTION_ID = {{ OFFSET_OPTION_ID }}, + STRUCTURE_ID = "{{ STRUCTURE_ID }}"; </script> diff --git a/webapp/polls/templatetags/poll_tags.py b/webapp/polls/templatetags/poll_tags.py index bde1962..f903155 100644 --- a/webapp/polls/templatetags/poll_tags.py +++ b/webapp/polls/templatetags/poll_tags.py @@ -12,6 +12,7 @@ register = template.Library() @register.simple_tag(takes_context=True) def render_structure(context, structure): + STRUCTURE_ID = structure.id structure = structure.to_dict(with_errors=True) groups = SafeUnicode(json.dumps(structure.get("groups", {}))) widget_types = SafeUnicode(json.dumps( @@ -25,6 +26,7 @@ def render_structure(context, structure): "WITH_OPTIONS": SafeUnicode(json.dumps(WITH_OPTIONS)), "WITH_IMAGES": SafeUnicode(json.dumps(WITH_IMAGES)), "OFFSET_OPTION_ID": SafeUnicode(json.dumps(Field.get_offset_id())), + "STRUCTURE_ID": str(STRUCTURE_ID) if STRUCTURE_ID else '', "groups": groups } ) diff --git a/webapp/polls/tests.py b/webapp/polls/tests.py index 51247ca..e472e25 100644 --- a/webapp/polls/tests.py +++ b/webapp/polls/tests.py @@ -7,7 +7,7 @@ from polls.models import ( Poll, Field, Group, Structure, Option, WIDGET_TYPES) from polls.forms import PollAddForm -from utils.test import MongoTestCase +from utils.test import MongoTestCase, mock_in_memory_image, mock_text_file class PollTests(MongoTestCase): @@ -153,6 +153,22 @@ class FieldsTests(TestCase): self.assertEqual(1, len(field.options)) + def test_add_image_options(self): + data = { + 'widget_type': 'ImageCheckBox', + 'name': 'field_0', + } + field = Field.from_dict(data) + + img_file = mock_in_memory_image() + + options_data = { + '1': {'img': img_file}, + } + field.add_options(options_data) + + self.assertEqual(1, len(field.options)) + def test_validate(self): self.field.widget_type = "MultipleCheckBox" @@ -186,6 +202,25 @@ class OptionTests(TestCase): self.assertEqual('1', option.id) self.assertEqual('si', option.text) + def test_image_option(self): + + img_name = 'image.jpg' + img_file = mock_in_memory_image(img_name) + + data = {'id': '1', 'img': img_file} + option = Option.from_dict(data) + + self.assertEqual('1', option.id) + self.assertEqual(img_file, option.img) + + def test_validate(self): + invalid_img_file = mock_text_file() + + data = {'id': '1', 'img': invalid_img_file} + option = Option.from_dict(data) + + self.assertRaises(Option.ValidationError, option.validate) + class GroupTests(TestCase): diff --git a/webapp/polls/views.py b/webapp/polls/views.py index 18b2eeb..2a26390 100644 --- a/webapp/polls/views.py +++ b/webapp/polls/views.py @@ -9,9 +9,11 @@ from django.http import HttpResponse, HttpResponseRedirect, Http404 from django.core.urlresolvers import reverse from django.views.generic.edit import FormView from django.contrib import messages +from django.core.files.uploadedfile import InMemoryUploadedFile from utils.forms import BadFormValidation from utils.mongo_connection import get_db +from utils.data_structure import dict_merge from polls.models import WIDGET_TYPES, Structure, Poll from polls.forms import PollAddForm @@ -36,7 +38,8 @@ def clean_data(value): value = [clean_data(value_) for value_ in value] return value - value = value.strip(' ') + if not isinstance(value, InMemoryUploadedFile): + value = value.strip(' ') # TODO: Organizar esto con Flavio. #if value == '': @@ -131,8 +134,12 @@ class StructureFormView(TemplateView): def post(self, request, *args, **kwargs): context = self.get_context_data() + data = {} - data = DotExpandedDict(dict(request.POST.lists())) + data_post = DotExpandedDict(dict(request.POST.lists())) + data_files = DotExpandedDict(dict(request.FILES.lists())) + + data = dict_merge(data_post, data_files) for key, value in data.items(): data[key] = clean_data(value) @@ -156,6 +163,7 @@ class StructureFormView(TemplateView): reverse('polls:success', kwargs={"poll_id": str(self.poll.id)}) ) else: + structure.save_image_options(tmp=True) context.update({'errors': structure.errors}) context.update({'structure': structure}) diff --git a/webapp/requirements b/webapp/requirements index 8bbc37b..f59a2e8 100644 --- a/webapp/requirements +++ b/webapp/requirements @@ -5,4 +5,5 @@ nose==1.2.1 django-nose==1.1 fabric==1.5.3 pymongo==2.4.2 -django-jasmine==0.3.2
\ No newline at end of file +django-jasmine==0.3.2 +PIL==1.1.7
\ No newline at end of file diff --git a/webapp/utils/data_structure.py b/webapp/utils/data_structure.py new file mode 100644 index 0000000..730ba7e --- /dev/null +++ b/webapp/utils/data_structure.py @@ -0,0 +1,7 @@ +def dict_merge(d1, d2): + for k1, v1 in d1.iteritems(): + if not k1 in d2: + d2[k1] = v1 + elif isinstance(v1, dict): + dict_merge(v1, d2[k1]) + return d2 diff --git a/webapp/utils/test.py b/webapp/utils/test.py index c481dd7..d9cbfde 100644 --- a/webapp/utils/test.py +++ b/webapp/utils/test.py @@ -1,5 +1,9 @@ +import StringIO +import Image + from django.conf import settings from django.test import TestCase +from django.core.files.uploadedfile import InMemoryUploadedFile from utils.mongo_connection import connect @@ -23,3 +27,31 @@ class MongoTestCase(TestCase): self.db.drop_collection(collection_name) super(MongoTestCase, self)._post_teardown() + + +def mock_text_file(): + io = StringIO.StringIO() + io.write('foo') + text_file = InMemoryUploadedFile(io, None, 'foo.txt', 'text', io.len, None) + text_file.seek(0) + return text_file + + +def mock_image_file(format="jpeg"): + io = StringIO.StringIO() + size = (200, 200) + color = (255, 0, 0, 0) + image = Image.new("RGBA", size, color) + image.save(io, format=format.upper()) + io.seek(0) + return io + + +def mock_in_memory_image(name='foo.jpg', format="jpeg"): + io = mock_image_file(format) + content_type = "Image/%s" % format.lower() + image_in_memory = InMemoryUploadedFile( + io, None, name, content_type, io.len, None + ) + image_in_memory.seek(0) + return image_in_memory diff --git a/webapp/webapp/media/image_options/empty b/webapp/webapp/media/image_options/empty new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/webapp/webapp/media/image_options/empty diff --git a/webapp/webapp/settings.py b/webapp/webapp/settings.py index 5d3748c..fdae3de 100644 --- a/webapp/webapp/settings.py +++ b/webapp/webapp/settings.py @@ -194,6 +194,8 @@ LOGIN_REDIRECT_URL = '/' JASMINE_TEST_DIRECTORY = PROJECT_ROOT + '/../js_tests/' +IMAGE_OPTIONS_ROOT = MEDIA_ROOT + 'image_options' + try: from env_settings import * except ImportError: diff --git a/webapp/webapp/static/js/dynamic_structure.js b/webapp/webapp/static/js/dynamic_structure.js index 9bc87af..9ecf781 100644 --- a/webapp/webapp/static/js/dynamic_structure.js +++ b/webapp/webapp/static/js/dynamic_structure.js @@ -27,10 +27,10 @@ var factoryOptionDefault = function(group_order, field_widget, id, default_value }; -var factoryImageOption = function(id, value, container, group_order, field_order) { +var factoryImageOptionUpload = function(id, value, container, group_order, field_order) { var option = $( - Mustache.render(TEMPLATES['field_option_image'], { + Mustache.render(TEMPLATES['field_option_image_upload'], { "id": id, "group_order": group_order, "field_order": field_order @@ -44,6 +44,25 @@ var factoryImageOption = function(id, value, container, group_order, field_order }; +var factoryImageOptionThumbnail = function(id, value, container, group_order, field_order) { + + var option = $( + Mustache.render(TEMPLATES['field_option_image_thumbnail'], { + "id": id, + "STRUCTURE_ID": STRUCTURE_ID, + "img_name": value['img_name'], + "group_order": group_order, + "field_order": field_order + }) + ); + + // Append new option + row_fluid = $('<div class="row-fluid"></div>'); + row_fluid.append(option); + container.append(row_fluid); +}; + + var factoryOption = function(id, value, container, group_order, field_order) { var option = $( @@ -144,7 +163,7 @@ var bindFieldAddOptionButton = function(add_option_button) { if ($.inArray(current_widget_type, WITH_IMAGES) == -1){ factoryOption(OFFSET_OPTION_ID, empty_option_widget, options_container, group_order, field_order ); } else { - factoryImageOption(OFFSET_OPTION_ID, empty_option_widget, options_container, group_order, field_order ); + factoryImageOptionUpload(OFFSET_OPTION_ID, empty_option_widget, options_container, group_order, field_order ); } }); } @@ -241,12 +260,25 @@ var factoryField = function(order, value) { bindFieldAddOptionButton(add_option_button); } - // Show options + // Render options if ($.inArray(widget_type, WITH_OPTIONS) != -1){ - $.each(value['options'] || [], function(id, opt_value){ - options_container = field_widget.find('.WFieldOptions_container'); - factoryOption(id, opt_value, options_container, group_order, order); - }); + if ($.inArray(widget_type, WITH_IMAGES) == -1){ + // Basic option + $.each(value['options'] || [], function(id, opt_value){ + options_container = field_widget.find('.WFieldOptions_container'); + factoryOption(id, opt_value, options_container, group_order, order); + }); + } else { + // Image option + $.each(value['options'] || [], function(id, opt_value){ + options_container = field_widget.find('.WFieldOptions_container'); + if (opt_value.hasOwnProperty('img_name')){ + factoryImageOptionThumbnail(id, opt_value, options_container, group_order, order); + } else { + factoryImageOptionUpload(id, opt_value, options_container, group_order, order); + } + }); + } } else { if (value['options'] && Object.keys(value['options']).length){ $.each(value['options'] || [], function(id, opt_value){ |