diff options
Diffstat (limited to 'webapp/polls/models.py')
-rw-r--r-- | webapp/polls/models.py | 633 |
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 |