Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/webapp/polls/models.py
diff options
context:
space:
mode:
Diffstat (limited to 'webapp/polls/models.py')
-rw-r--r--webapp/polls/models.py633
1 files changed, 551 insertions, 82 deletions
diff --git a/webapp/polls/models.py b/webapp/polls/models.py
index f2b9119..19a8d3c 100644
--- a/webapp/polls/models.py
+++ b/webapp/polls/models.py
@@ -1,7 +1,21 @@
# -*- encoding: utf-8 -*-
import time
+import json
+import os
+import shutil
+import re
+import Image
+import base64
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
WIDGET_TYPES = (
@@ -9,40 +23,251 @@ WIDGET_TYPES = (
('MultipleCheckBox', 'Respuesta con checks (multiple selección)'),
('RadioButton', 'Respuesta con radios (Solo una selección)'),
('DropDownList', 'Respuesta con lista de opciones'),
+ ('ImageCheckBox', 'Respuesta con imagenes tipo checks'),
+ ('ImageRadioButton', 'Respuesta con imagenes tipo radios'),
)
-class AbastractObject(object):
+WITH_OPTIONS = [
+ "MultipleCheckBox",
+ "DropDownList",
+ "RadioButton",
+ "ImageCheckBox",
+ "ImageRadioButton",
+]
+
+
+WITH_IMAGES = ["ImageCheckBox", "ImageRadioButton"]
+
+
+class ComponentStructure(ObjectId):
+
+ def __init__(self, poll=None, *args, **kwargs):
+ super(ComponentStructure, self).__init__(*args, **kwargs)
+ self.poll = poll
+
+
+class AbstracErrorObject(object):
ValidationError = ValidationError
errors = []
+ dict_errors = {}
+
+
+class Poll(AbstracErrorObject):
+
+ collection_name = 'polls'
+ UniqueNameError = UniqueNameError
+
+ OPEN = "Abierta"
+ CLOSED = "Cerrada"
+
+ def __init__(self, data={}, *args, **kwargs):
+ super(Poll, self).__init__(*args, **kwargs)
+ self.id = None
+
+ _id = data.get('id', None) or data.get('_id', None)
+ if _id and (isinstance(_id, str) or isinstance(_id, unicode)):
+ self.id = ObjectId(_id)
+ elif _id and isinstance(_id, ObjectId):
+ self.id = _id
+
+ self.name = data.get('name', None)
+ self.status = data.get('status', Poll.OPEN)
+
+ @property
+ def structure(self):
+ structure_data = get_db().structures.find_one(
+ {'poll.$id': self.id})
+ structure_data = structure_data if structure_data else {}
+
+ return Structure(data=structure_data, poll=self)
+
+ @staticmethod
+ def status_choices():
+ return (
+ (Poll.OPEN, 'Abierta'),
+ (Poll.CLOSED, 'Cerrada'),
+ )
+
+ def is_open(self):
+ return self.status == Poll.OPEN
+
+ def to_dict(self):
+ _dict = {}
+
+ if self.id:
+ _dict.update({'_id': self.id})
+
+ if self.name:
+ _dict.update({'name': self.name})
+
+ if self.status:
+ _dict.update({'status': self.status})
+
+ return _dict
+
+ def validate(self):
+ self.errors = []
+
+ if not self.name:
+ msg = "Necesita ingresar un nombre de encuesta."
+ self.errors.append(msg)
+ else:
+ # Check unique name key, Important !!!
+ existing = get_db().polls.find_one(
+ {'name': re.compile("^%s$" % self.name, re.IGNORECASE)})
+ if existing and existing.get("_id", None) != self.id:
+ msg = u"Poll name '%s' already in use." % self.name
+ self.errors.append(msg)
+ raise Poll.ValidationError(msg)
+
+ if len(self.errors):
+ raise Poll.ValidationError(str(self.errors))
+
+ def save(self):
+ self.validate()
+
+ poll_id = None
+
+ poll_id = get_db().polls.save(self.to_dict())
+
+ return poll_id
+
+ @staticmethod
+ def get(id=None):
+ poll = None
+
+ objects = get_db().polls.find({'_id': ObjectId(id)})
+ if objects.count():
+ obj = objects[0]
+
+ poll = Poll(obj)
+
+ return poll
+
+ # TODO: Test
+ @staticmethod
+ def all(*args, **kwargs):
+ _all = []
+ for poll_data in get_db().polls.find(**kwargs):
+ _all.append(Poll(poll_data))
+
+ return _all
+
+ def to_json(self):
+ structure_data = get_db().structures.find_one(
+ {'poll.$id': self.id}, fields={'poll': False})
+ structure = Structure(structure_data, poll=self)
+
+ _json = json.dumps(
+ structure.to_python(with_errors=False, img_serialize=True),
+ sort_keys=True,
+ indent=4,
+ separators=(',', ': '),
+ ensure_ascii=False
+ )
+
+ return _json
+
+
+class AbstractObject(AbstracErrorObject):
@staticmethod
def get_offset_id():
return int(time.time() * 1000)
-class Option(AbastractObject):
+class Option(AbstractObject, ComponentStructure):
- def __init__(self, id, text):
- super(Option, self).__init__()
+ def __init__(self, data={}, *args, **kwargs):
+ super(Option, self).__init__(*args, **kwargs)
- self.id = id
- self.text = text
+ self.id = data.get('id', None)
+ self.text = data.get('text', None)
+ self.img = data.get('img', None)
+ self.img_name = data.get('img_name', None)
- @staticmethod
- def from_dict(data):
- option = Option(
- id=data.get('id', None),
- text=data.get('text', None),
+ weight = data.get('weight', None)
+ self.weight = int(weight) if weight else weight
+
+ if not self.img_name and isinstance(self.img, InMemoryUploadedFile):
+ fileExtension = os.path.splitext(self.img.name)[1]
+ self.img_name = '%s%s' % (self.id, fileExtension)
+
+ def get_absolute_path(self):
+ return "%s/%s/%s" % (
+ settings.IMAGE_OPTIONS_ROOT, str(self.poll.id), self.img_name
)
- return option
+ def validate(self):
+ self.dict_errors = {}
+ self.errors = []
+
+ if self.img and isinstance(self.img, InMemoryUploadedFile):
+ try:
+ img = ImageField().to_python(self.img)
+ except DjangoValidationError, e:
+ self.dict_errors.update(
+ {'img': '%s: %s' % (self.img.name, e.messages[0])})
+ else:
+ width, height = Image.open(img).size
+ if width > 250 or height > 250:
+ msg = u"Se necesita una imagen menor a 250x250."
+ self.dict_errors.update(
+ {'img': '%s: %s' % (self.id, msg)})
+
+ if 'img' in self.dict_errors.keys():
+ self.img_name = None
+ self.img = None
+ else:
+ self.img.seek(0)
+
+ if self.weight is None or self.weight == '':
+ msg = u"opcion %s: ponderación requerida." % self.id
+ self.dict_errors.update({'weight': msg})
+
+ self.errors = self.dict_errors.values()
+ if len(self.errors):
+ raise Option.ValidationError(str(self.errors))
+
+ def to_python(self, with_errors=False, img_serialize=False):
+
+ data = {'%s' % self.id: {}}
+
+ if self.text:
+ data[self.id].update({'text': self.text})
+
+ if self.img_name:
+ if img_serialize:
+ img_path = self.get_absolute_path()
+
+ img_file = open(img_path, 'rb')
+ image_string = base64.b64encode(img_file.read())
+ img_file.close()
+
+ data[self.id].update({'img': image_string})
+ else:
+ data[self.id].update({'img_name': self.img_name})
+
+ if self.weight is not None and self.weight != '':
+ data[self.id].update({'weight': self.weight})
+
+ return data
-class Field(AbastractObject):
+class Field(AbstractObject, ComponentStructure):
- rules = {
+ TextInput = 'TextInput'
+
+ MultipleCheckBox = 'MultipleCheckBox'
+ RadioButton = 'RadioButton'
+ DropDownList = 'DropDownList'
+
+ ImageCheckBox = 'ImageCheckBox'
+ ImageRadioButton = 'ImageRadioButton'
+
+ VALIDATION_RULES = {
'MultipleCheckBox': (
lambda f: f.options and len(f.options) > 0,
"Respuesta con checks (multiple selección): necesita "
@@ -58,70 +283,65 @@ 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, ""),
}
- def __init__(self, name, widget_type, key=None):
- super(Field, self).__init__()
+ def __init__(self, data={}, *args, **kwargs):
+ super(Field, self).__init__(*args, **kwargs)
- self.name = name
- self.key = key
- self.options = None
- self.dependence = None
+ order = data.get('order', None)
+ self.order = int(order) if order else order
+ self.name = data.get('name', None)
+ self.dependence = data.get('dependence', None)
+ self.options = []
+ widget_type = data.get('widget_type', None)
if widget_type and widget_type not in dict(WIDGET_TYPES).keys():
raise AttributeError(
- 'valid widget types are TextInput, MultipleCheckBox, \
- RadioButton, DropDownList.'
+ 'valid widget types are %s' % WIDGET_TYPES.keys().join(', ')
)
self.widget_type = widget_type
- @staticmethod
- def from_dict(data):
- name = data.get('name', None)
- key = data.get('key', None)
- widget_type = data.get('widget_type', None)
- dependence = data.get('dependence', None)
-
- field = Field(
- key=key,
- name=name,
- widget_type=widget_type,
- )
-
- if dependence:
- field.add_dependence(dependence)
-
- return field
-
def add_options(self, data):
for id, info in data.iteritems():
opt_data = {'id': id}
opt_data.update(info)
- opt = Option.from_dict(opt_data)
+ opt = Option(opt_data, poll=self.poll)
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)
- def add_dependence(self, dependence):
- self.dependence = dependence
+ 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 validate(self, options=[]):
self.errors = []
- rule, msg = Field.rules.get(self.widget_type)
+ rule, msg = Field.VALIDATION_RULES.get(self.widget_type)
if not rule(self):
self.errors.append(msg)
- options_id = [opt.id for opt in options]
-
- # TODO: Test
- # TODO: Comprobacion que exista en el mismo grupo
- # TODO: Que la dependencia no sea de una opcion de este campo
- if self.dependence and self.dependence not in options_id:
- msg = "Dependencia no valida"
- self.errors.append(msg)
+ # Validate option of current field
+ for opt in self.options:
+ # TODO: Refactoring this.
+ # HORRIBLE path to avoid validation for TextInput options.
+ if self.widget_type != Field.TextInput:
+ try:
+ opt.validate()
+ except Option.ValidationError:
+ self.errors += opt.errors
# TODO: Test
if not self.name:
@@ -131,87 +351,336 @@ class Field(AbastractObject):
if len(self.errors):
raise Field.ValidationError(str(self.errors))
- def need_options(self):
- return self.widget_type in (
- 'MultipleCheckBox', 'RadioButton', 'DropDownList')
+ def to_python(self, with_errors=False, img_serialize=False):
+
+ data = {}
+ data.update({
+ 'name': self.name,
+ 'widget_type': self.widget_type,
+ 'options': {}
+ })
+ if self.dependence:
+ data.update({'dependence': self.dependence})
+
+ if with_errors:
+ data.update({'errors': self.errors})
+
+ options = self.options if self.options else []
+ for option in options:
+ data['options'].update(option.to_python(
+ with_errors=with_errors, img_serialize=img_serialize)
+ )
+ return {'%d' % self.order: data}
-class Group(AbastractObject):
- def __init__(self, name, fields=[]):
- super(Group, self).__init__()
- self.name = name
- self.fields = fields
+class Group(AbstractObject, ComponentStructure):
+
+ def __init__(self, data={}, *args, **kwargs):
+ super(Group, self).__init__(*args, **kwargs)
+
+ order = data.get('order', None)
+ self.order = int(order) if order else order
+ self.name = data.get('name', None)
+ self.fields = data.get('fields', [])
def add_field(self, field, order):
order = int(order)
+ field.order = order
fields_pre = self.fields[:order]
fields_post = self.fields[order:]
self.fields = fields_pre + [field] + fields_post
return field
+ def validate(self):
+ self.errors = []
+
+ if not self.name:
+ msg = "Necesita ingresar un nombre para el grupo."
+ self.errors.append(msg)
+
+ if not self.fields:
+ msg = "Necesita al menos una pregunta para un grupo."
+ self.errors.append(msg)
+
+ if len(self.errors):
+ raise Group.ValidationError(str(self.errors))
+
+ def to_python(self, with_errors=False, img_serialize=False):
+ data = {'name': self.name, 'fields': {}}
+
+ if with_errors:
+ data.update({'errors': self.errors})
+
+ for field_obj in self.fields:
+ field_data = field_obj.to_python(
+ with_errors=with_errors, img_serialize=img_serialize
+ )
+ data['fields'].update(field_data)
+
+ return {'%d' % self.order: data}
-class Structure(object):
+
+class Structure(AbstractObject, ComponentStructure):
"""
{
'groups': {
'0': {
+ 'name': 'group name',
'fields': {
- '0': {'widget_type': 'MultipleCheckBox', 'name': 'sagas'},
- '1': {u'widget_type': 'TextInput', u'name': 'otro'}
+ '0': {
+ 'widget_type': ...,
+ 'name': ...,
+ 'options': ...,
+ 'dependence': ...,
+ },
+ '1' ...
}
},
- '1': {
- 'name': 'nombre'
- 'fields': {
- '0': {'widget_type': 'RadioButton', 'name': 'dsadas'},
- '1': {'widget_type': 'TextInput', 'name': 'asfa'}
- }
- }
+ '1' ...
+ ...
}
}
"""
- def __init__(self, data=None):
- super(Structure, self).__init__()
+ def __init__(self, data=None, poll=None, *args, **kwargs):
+ super(Structure, self).__init__(poll, *args, **kwargs)
self.data = data
self.groups = []
-
+ self.poll = poll
+ self.id = None
+
+ # Getting parent poll id
+ self._poll_id = getattr(poll, 'id', None)
+ if self.data and self._poll_id is None:
+ poll_dbref = data.get('poll', None)
+ poll_id = poll_dbref.id if poll_dbref else None
+ self._poll_id = str(poll_id) if poll_id else self._poll_id
+ self._poll_id = self.data.get('poll_id', self._poll_id)
+
+ # Build model Structure obj based in dict data
if self.data:
+ # Getting id
+ _id = data.get('id', None) or data.get('_id', None)
+ if _id and (isinstance(_id, str) or isinstance(_id, unicode)):
+ self.id = ObjectId(_id)
+ elif _id and isinstance(_id, ObjectId):
+ self.id = _id
+
groups_info = data['groups']
for group_order, group_data in groups_info.iteritems():
- group = Group(name=group_data['name'])
- fields_info = group_data['fields']
+ group = Group({
+ 'order': group_order,
+ 'name': group_data['name']
+ }, poll=self.poll)
+
+ fields_info = group_data.get('fields', {})
for field_order, field_data in fields_info.iteritems():
- field = Field.from_dict(field_data)
+ field_data.update({'order': field_order})
+ field = Field(field_data, poll=self.poll)
field.add_options(field_data.get('options', {}))
- field.add_dependence(field_data.get('dependence', None))
group.add_field(field, field_order)
+
self.add_group(group, group_order)
+ # Require: parent poll id !!!
+ if not self._poll_id:
+ raise Exception('INTERNAL ERROR: A structure need a poll id!')
+
+ @property
+ def poll_id(self):
+ return self._poll_id
+
def add_group(self, group, order):
order = int(order)
+ group.order = order
groups_pre = self.groups[:order]
groups_post = self.groups[order:]
self.groups = groups_pre + [group] + groups_post
return group
- def is_valid(self):
- valid = True
-
- # TODO: corregir este pasaje de opciones forzado
+ def get_options(self):
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], [])
+ return options
+
+ def get_image_options(self):
+ options = self.get_options()
+ return filter(lambda opt: opt.img_name is not None, options)
+
+ def validate(self):
+ self.errors = []
+
+ if not self.data.get('groups', {}):
+ msg = "Necesita al menos un grupo con preguntas."
+ self.errors.append(msg)
+
+ if len(self.errors):
+ raise Group.ValidationError(str(self.errors))
+
+ def is_valid(self):
+ valid = True
+
+ options = self.get_options()
+
for group in self.groups:
+ try:
+ group.validate()
+ except Group.ValidationError:
+ valid = False
+
for field in group.fields:
try:
field.validate(options)
except Field.ValidationError:
valid = False
+
+ try:
+ self.validate()
+ except Structure.ValidationError:
+ valid = False
+
return valid
+
+ def to_python(self, with_errors=False, img_serialize=False):
+ data = {'groups': {}}
+
+ for group_obj in self.groups:
+ data['groups'].update(
+ group_obj.to_python(
+ with_errors=with_errors, img_serialize=img_serialize
+ )
+ )
+
+ return data
+
+ def save(self):
+ structure_id = None
+
+ self.validate()
+
+ _dict = self.to_python()
+
+ # Prepare dbref to poll object
+ if not self.poll:
+ raise ValidationError("Need a parent poll.")
+ else:
+ dbref = DBRef(Poll.collection_name, ObjectId(self.poll.id))
+ _dict.update({'poll': dbref})
+
+ # Prepare id if is a existing Structure object
+ if self.id:
+ _dict.update({'_id': ObjectId(self.id)})
+
+ # Save process -> Update if it have id, else insert
+ structure_id = get_db().structures.save(_dict)
+
+ # Removing older img options files
+ current_options = self.get_image_options()
+ current_opts_file_name = [opt.img_name for opt in current_options]
+ path = self.get_image_options_path()
+ for file in os.listdir(path):
+ if file not in current_opts_file_name:
+ try:
+ os.remove("%s/%s" % (path, file))
+ except:
+ pass
+
+ return structure_id
+
+ @staticmethod
+ def get(id=None):
+ structure = None
+
+ objects = get_db().structures.find({'_id': ObjectId(id)})
+ if objects.count():
+ obj = objects[0]
+ poll_id = obj['poll'].id
+
+ structure = Structure(obj)
+ structure.poll = Poll.get(poll_id)
+
+ return structure
+
+ def get_image_options_tmp_path(self):
+ path = settings.IMAGE_OPTIONS_ROOT + '/%s/tmp' % self.poll_id
+
+ try:
+ os.makedirs(path)
+ except OSError:
+ pass
+
+ return path
+
+ def get_image_options_path(self):
+ path = settings.IMAGE_OPTIONS_ROOT + '/%s' % self.poll_id
+
+ try:
+ os.makedirs(path)
+ except OSError:
+ pass
+
+ return path
+
+ def get_image_options_tmp_media_url(self):
+ media_url = None
+
+ media_url = settings.IMAGE_OPTIONS_MEDIA_URL + '/%s/tmp' % self.poll_id
+
+ return media_url
+
+ def rollback(self):
+ tmp_path = self.get_image_options_tmp_path()
+ path = self.get_image_options_path()
+ for file in os.listdir(path):
+ if file != "tmp":
+ src_file = os.path.join(path, file)
+ dst_file = os.path.join(tmp_path, file)
+ shutil.move(src_file, dst_file)
+
+ def storing_image_options(self, path, options):
+ for img_opt in options:
+ if isinstance(img_opt.img, InMemoryUploadedFile):
+ with open(path + '/%s' % img_opt.img_name, 'wb+') as dst:
+ for chunk in img_opt.img.chunks():
+ dst.write(chunk)
+ dst.close()
+
+ def save_image_options(self, tmp=False):
+
+ options = self.get_options()
+ valid_img_options = filter(
+ lambda opt: not 'img' in opt.dict_errors.keys(), options)
+
+ tmp_path = self.get_image_options_tmp_path()
+
+ if len(valid_img_options):
+ if tmp:
+ self.storing_image_options(tmp_path, valid_img_options)
+ else:
+ path = self.get_image_options_path()
+ self.storing_image_options(path, valid_img_options)
+
+ # Moving tmp images options to the final place for store them
+ for img_opt in valid_img_options:
+ src = '%s/%s' % (tmp_path, img_opt.img_name)
+ dst = '%s/%s' % (path, img_opt.img_name)
+ try:
+ shutil.move(src, dst)
+ except Exception:
+ # TODO: LOG orphan img options
+ pass
+
+ try:
+ os.removedirs(tmp_path)
+ except:
+ # TODO: tmp must be empty,
+ # if raise exception here is someting bad
+ # TODO: LOG!
+ pass