# -*- encoding: utf-8 -*- import time import json import os import shutil import re import Image import base64 import copy import warnings import glob import errno from distutils.dir_util import copy_tree from bson import ObjectId, DBRef from datetime import datetime from django.conf import settings from django.forms.fields import ImageField from django.core.exceptions import ValidationError as DjangoValidationError from utils.mongo_connection import get_db from utils.mongo_document import Document from utils.strings import multiple_replace from pollster.models import Pollster from polls.exceptions import ReadOnly, ValidationError def is_image_file(value): return hasattr(value, "name") WIDGET_TYPES = ( ('TextInput', 'Texto'), ('MultipleCheckBox', 'Checklist (multiple opción)'), ('RadioButton', 'Checklist (única respuesta)'), ('DropDownList', 'Despliegue de lista (única respuesta)'), ('ImageCheckBox', 'Checklist con imágenes (multiple opción)'), ('ImageRadioButton', 'Checklist con imágenes (única respuesta)'), ) 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(Document, AbstracErrorObject): collection_name = 'polls' OPEN = "Abierta" CLOSED = "Cerrada" def __init__(self, data={}, *args, **kwargs): super(Poll, self).__init__(data=data, *args, **kwargs) self.results_path = settings.RESULT_BCK_ROOT self.name = data.get('name', None) self.status = data.get('status', Poll.OPEN) self.pollsters = data.get('pollsters', []) self.creation_date = data.get('creation_date', None) self.modification_date = data.get('modification_date', None) @staticmethod def clean_question(question_name): new_string = question_name.encode('ascii', 'ignore') to_replace = [ (" ", ''), ("#", ''), (".", ''), (":", ''), ("?", ''), ("!", ''), ("@", ''), ("$", ''), ("%", ''), ("^", ''), ("&", ''), ("(", ''), (")", ''), ("+", ''), ("=", ''), ("-", ''), ("/", ''), ("\\", ''), ("-", ''), (",", ''), ('"', ''), ("'", '') ] return multiple_replace(new_string, to_replace) def clone(self): clone_data = self.to_python() if clone_data.get('_id', None): del clone_data['_id'] poll_clone = Poll(data=clone_data) poll_clone.name = "clon-%s" % poll_clone.name poll_clone.status = Poll.OPEN poll_clone_id = poll_clone.save() poll_clone = Poll.get(poll_clone_id) clone_structure_data = self.structure.to_python() if clone_structure_data.get('_id', None): del clone_structure_data['_id'] clone_structure = Structure( data=clone_structure_data, poll=poll_clone) clone_structure.save() path = self.structure.get_image_options_path() clone_path = poll_clone.structure.get_image_options_path() copy_tree(path, clone_path) path = self.structure.get_image_path() clone_path = poll_clone.structure.get_image_path() copy_tree(path, clone_path) return poll_clone def delete(self): structure = self.structure if self.structure else None structure_id = structure.id if structure.id else None if structure_id: get_db().structures.remove(ObjectId(structure_id)) # removing image options files if structure: path = structure.get_image_options_path() if os.path.exists(path): shutil.rmtree(path) if self.id: get_db().polls.remove(ObjectId(self.id)) @staticmethod def pollster_assignment(pollster_id, poll_ids): dbref = DBRef(Pollster.collection_name, ObjectId(pollster_id)) ids = [ObjectId(id) for id in poll_ids] get_db().polls.update( {"_id": {"$in": ids}}, {"$push": {"pollsters": dbref}}, False, multi=True ) def get_pollsters(self): pollsters = [] ids = [] for object in self.pollsters: pollster_id = None if isinstance(object, Pollster) or isinstance(object, DBRef): pollster_id = object.id else: pollster_id = object ids.append(pollster_id) pollsters_data = get_db().pollsters.find({"_id": {"$in": ids}}) for data in pollsters_data: pollster = Pollster(data=data) pollsters.append(pollster) return pollsters @property def structure(self): structure_data = get_db().structures.find_one( {'poll.$id': self.id}) structure_id = structure_data['_id'] if structure_data else None return Structure.get(structure_id) if structure_id else 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_python(self, with_dates=False): _dict = {} if with_dates: _dict.update({'creation_date': self.creation_date}) _dict.update({'modification_date': self.modification_date}) if self.id: _dict.update({'_id': self.id}) if self.name: _dict.update({'name': self.name}) if self.status: _dict.update({'status': self.status}) # Ensure pollsters to dbref pollsters = [] for object in self.pollsters: pollster_id = None if isinstance(object, Pollster) or isinstance(object, DBRef): pollster_id = object.id else: pollster_id = object dbref = DBRef(Pollster.collection_name, ObjectId(pollster_id)) pollsters.append(dbref) _dict.update({'pollsters': pollsters}) 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 now = datetime.now() if self.id is None: self.creation_date = now self.modification_date = now else: self.modification_date = now poll_id = get_db().polls.save(self.to_python(with_dates=True)) 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(data=obj) return poll # TODO: Test @staticmethod def all(*args, **kwargs): _all = [] for poll_data in get_db().polls.find(**kwargs): _all.append(Poll(data=poll_data)) return _all @staticmethod def assigned_to_pollster(pollster, *args, **kwargs): _all = [] _find = get_db().polls.find({'pollsters.$id': pollster.id}, **kwargs) for pollster_data in _find: _all.append(Poll(data=pollster_data)) return _all def get_template(self, pollster=None): structure_data = get_db().structures.find_one( {'poll.$id': self.id}, fields={'poll': False}) structure_data = structure_data if structure_data else {} structure = Structure(data=structure_data, poll=self) # some cleans for output template data = structure.to_python(with_errors=False, img_serialize=True) for index, group in data["groups"].iteritems(): for index, field in group['fields'].iteritems(): # Hack for Desktop application. # Desktop app don't know process questions without options. widget_type = field['widget_type'] if widget_type == Field.TextInput: options = field.get('options', {}) if not len(options): field['options'].update({ '%s' % Option.get_offset_id(): { 'text': "", 'order': "0" } }) # Filter dependences. Clean when None values if field.get('dependence', False): dependence_values = field.get( 'dependence', {}).get('values', []) field['dependence']['values'] = filter( lambda x: x is not None, dependence_values) if not any(field['dependence']['values']): del field['dependence'] pollsters = self.get_pollsters() if pollster and pollster not in pollsters: message = "Pollster given not belongs to this poll" warnings.warn(message, Warning, stacklevel=2) data.update({ 'poll_id': str(self.id), 'pollster_id': str(pollster.id) if pollster else None, 'poll_name': self.name, 'pollster_username': pollster.username if pollster else None }) # Removing _id key if data.get('_id', None): del data['_id'] _json = json.dumps( data, sort_keys=True, indent=4, separators=(',', ': '), ensure_ascii=False ) return _json @staticmethod def unassigned_to(pollster_id): _all = [] _find = get_db().polls.find({ "pollsters": { "$not": { '$elemMatch': { "$id": ObjectId(pollster_id)} } } }) for poll_data in _find: _all.append(Poll(data=poll_data)) return _all def add_result(self, poll_result_id): dbref = DBRef("poll_results", ObjectId(poll_result_id)) get_db().polls.update({"_id": self.id}, {"$set": {"result": dbref}}) def get_result(self): poll_result_files = self.get_result_files() datas = map(lambda prf: prf.get_data(), poll_result_files) return PollResult(data=datas) def get_questions(self): questions = [] for group in self.structure.groups: for field in group.fields: questions.append(field.name) return questions def get_csv_header(self, with_order=False): MANY_ANSWERS = [ "MultipleCheckBox", "ImageCheckBox", ] ONLY_ANSWER = [ "DropDownList", "RadioButton", "TextInput", "ImageRadioButton" ] structure = self.structure.to_python() header = [] groups = structure['groups'] sorted_groups = sorted(map(lambda x: int(x), groups.keys())) for group_id in sorted_groups: fields = groups[str(group_id)]['fields'] sorted_fields = sorted(map(lambda x: int(x), fields.keys())) for field_id in sorted_fields: field = fields[str(field_id)] order = "%s_%s " % (group_id, field_id) if field['widget_type'] in ONLY_ANSWER: col_name = field['name'] if with_order: col_name = order + col_name header.append(col_name) elif field['widget_type'] in MANY_ANSWERS: cmp_ = lambda x, y: cmp( int(x[1]['order']), int(y[1]['order'])) sorted_options = sorted(field['options'].items(), cmp_) for option_id, option in sorted_options: col_name = option.get("text", option_id) if with_order: col_name = order + col_name header.append(col_name) return ";".join(header) def get_resutls_path(self): results_path = self._results_path poll_id = self.id if poll_id is None: raise Exception("Please, save poll first!!") return os.path.join(results_path, str(poll_id)) def set_results_path(self, path): self._results_path = path results_path = property(get_resutls_path, set_results_path) def get_result_files(self, as_instance_of=None): result_file = PollResultFile if not as_instance_of else as_instance_of results_path = self.results_path result_files = [] for file_path in glob.glob(results_path + '/*'): result_files.append(result_file(file_path)) return result_files def add_result_files(self, path_and_name): results_path = self.results_path cp = lambda src, dst: shutil.copyfile(src, dst) for path, name in path_and_name: src = path file_name = name dst = os.path.join(results_path, file_name) try: cp(src, dst) except IOError as e: if e.errno == errno.ENOENT: os.mkdir(results_path) cp(src, dst) else: raise e def has_result(self, as_instance_of=None): result_file = PollResultFile if not as_instance_of else as_instance_of results = self.get_result_files(as_instance_of=result_file) return len(results) > 0 def remove_results(self): results = self.get_result_files() for r in results: r.delete() class AbstractObject(AbstracErrorObject): @staticmethod def get_offset_id(): return int(time.time() * 1000) class Option(AbstractObject, ComponentStructure): def __init__(self, data={}, *args, **kwargs): super(Option, self).__init__(*args, **kwargs) self.type = data.get('type', None) 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) self.order = data.get('order', None) self.none = data.get('none', None) weight = data.get('weight', None) self.weight = int(weight) if weight else weight if not self.img_name and is_image_file(self.img): fileExtension = os.path.splitext(self.img.name)[1] self.img_name = '%s%s' % (self.id, fileExtension) @staticmethod def get_img(poll_id, option_id): path = "%s/%s" % ( settings.IMAGE_OPTIONS_ROOT, str(poll_id) ) for file in os.listdir(path): file_name = os.path.splitext(os.path.split(file)[1])[0] if file_name == option_id: return "%s/%s" % (path, file), settings.MEDIA_URL return None, None def get_absolute_path(self): return "%s/%s/%s" % ( settings.IMAGE_OPTIONS_ROOT, str(self.poll.id), self.img_name ) def validate(self, img_validator=None): self.dict_errors = {} self.errors = [] if self.img and is_image_file(self.img): validator = ImageField().to_python if img_validator: validator = img_validator try: img = validator(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}) if self.text and ";" in self.text: msg = u"Las opciones no pueden contener ';'" self.dict_errors.update({'semicolon_error': 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.type: data[self.id].update({'type': self.type}) if self.text: data[self.id].update({'text': self.text}) if self.order is not None: data[self.id].update({'order': self.order}) 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}) elif self.type == "img_input": data[self.id].update({'img': None}) if self.weight is not None and self.weight != '': data[self.id].update({'weight': self.weight}) if self.none: data[self.id].update({'none': self.none}) return data class Dependence(AbstracErrorObject): def __init__(self, form=4, values=None): super(Dependence, self).__init__() self.form = int(form) self.values = values def __str__(self): values = list(self.values) if not len(values) or values is None: return "" if len(values) == 1 or len(values) == 2: __str = values[0] return __str.strip(' ') if len(values) % 2 == 0: values = values[:len(values) - 1] if self.form == 4: __str = ' '.join(values) return __str.strip(' ') elif self.form == 3 or len(values) == 3: __str = "( %s ) %s" % ( ' '.join(values[0:3]), ' '.join(values[3:len(values)]), ) return __str.strip(' ') elif self.form == 2: values.reverse() __str = "( %s ) %s" % ( ' '.join(values[0:5]), ' '.join(values[5:len(values)]), ) return __str.strip(' ') elif self.form == 1: values.reverse() __str = "( %s ) %s ( %s )" % ( ' '.join(values[0:3]), values[3], ' '.join(values[4:len(values)]), ) return __str.strip(' ') def to_python(self): return { 'form': self.form, 'values': self.values } def validate(self, options=[]): self.errors = [] if not any(self.values): return operators = [] opt_ids = [] for pos, value in enumerate(self.values): if value is not None: if value in ('OR', 'AND', ''): operators.append((pos, value)) else: opt_ids.append((pos, value)) if (len(opt_ids) - 1) != len(operators): self.errors.append("Bad logic operators.") values_dict = dict(operators + opt_ids) cleaned_data = [None for x in range(0, 7)] for i in range(0, 7): cleaned_data[i] = values_dict.get(i, '') self.values = cleaned_data if len(self.errors): raise Dependence.ValidationError(str(self.errors)) class Field(AbstractObject, ComponentStructure): 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 " "al menos una opción." ), 'RadioButton': ( lambda f: f.options and len(f.options) > 1, "Respuesta con radios (Solo una selección): necesita " "al menos dos opciones." ), 'DropDownList': ( lambda f: f.options and len(f.options) > 0, "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, data={}, *args, **kwargs): super(Field, self).__init__(*args, **kwargs) self.poll_id = None order = data.get('order', None) self.order = int(order) if order else order self.name = data.get('name', None) self.img = data.get('img', None) self.uploaded_file = data.get("uploaded_file", None) value = data.get('dependence', None) self.dependence = value if isinstance(value, Dependence): self.dependence = value.to_python() elif isinstance(value, dict): form = value.get('form', 4) values = value.get('values', []) self.dependence = Dependence(form=form, values=values).to_python() elif isinstance(value, list): value = value if isinstance(value, list) else [value] self.dependence = Dependence(values=value).to_python() 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 %s' % WIDGET_TYPES.keys().join(', ') ) self.widget_type = widget_type def get_field_offset(self, structure): stop = False field_counts = {} for group in structure.groups: field_counts.update({"%s" % group.order: len(group.fields)}) group_order = 0 for group in structure.groups: for field in group.fields: if field.name == self.name: group_order = group.order break if stop: break offset = 0 for index in range(0, group_order): offset += int(field_counts[str(index)]) return offset + self.order def show_dependence(self): form = self.dependence.get('form', 4) values = self.dependence.get('values', []) return str(Dependence(form=form, values=values)) def add_options(self, data): for id, info in data.iteritems(): opt_data = {'id': id} opt_data.update(info) opt = Option(opt_data, poll=self.poll) self.options = self.options if self.options is not None else [] 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 img_validate(self, img_file): try: ImageField().to_python(img_file) except DjangoValidationError, e: self.img = None raise Field.ValidationError(e.messages[0]) def validate(self, options=[], new_data=None): self.errors = [] rule, msg = Field.VALIDATION_RULES.get(self.widget_type) if not rule(self): self.errors.append(msg) # Validate option of current field for opt in self.options: try: opt.validate() except Option.ValidationError: if self.widget_type != Field.TextInput: self.errors += opt.errors elif "semicolon_error" in opt.dict_errors.keys(): self.errors.append( "El texto default no puede contener ';'") # Validate dependences temporary_structure = new_data if new_data else self.poll.structure has_dependence = self.dependence and any( self.dependence.get('values', {})) if len(options) and has_dependence: prev_options = [] field_offset = self.get_field_offset(temporary_structure) for i in range(0, field_offset): prev_options.extend(options.get(str(i), [])) prev_options_ids = [opt.id for opt in prev_options] values = self.dependence.get('values', []) msg = [] if len(values): for value in values: # TODO: Replace ('OR', 'AND', '') for CONSTANT valid_value = value and value not in ('OR', 'AND', '') if valid_value and value not in prev_options_ids: try: msg.append(str(value)) except: msg.append( "'%s'" % value.encode('ascii', 'ignore')) if len(msg): self.errors += [ 'Dependencias: %s no validas.' % str(', '.join(msg))] # TODO: Test if self.dependence: form = self.dependence.get('form', 4) values = self.dependence.get('values', []) dependence = Dependence(form=form, values=values) try: dependence.validate() except Dependence.ValidationError: msg = u'Error en la formación de la dependencia.' self.errors += [msg] self.dependence['values'] = dependence.values if not self.name: msg = "Necesita ingresar una pregunta" self.errors.append(msg) elif ";" in self.name: msg = "Las preguntas no pueden contener ';'" self.errors.append(msg) if self.uploaded_file: try: self.img_validate(self.uploaded_file) except Field.ValidationError: self.errors.append(u"Pregunta con imagen no válida.") if len(self.errors): raise Field.ValidationError(str(self.errors)) def storing_image(self, path, img_file): img_path = path + '/%s' % img_file.name if os.path.exists(img_path): os.remove(img_path) with open(img_path, 'wb+') as dst: for chunk in img_file.chunks(): dst.write(chunk) def store_image(self, path): if self.uploaded_file: self.storing_image(path, self.uploaded_file) def get_img_name(self): return getattr(self.uploaded_file, "name", self.img) def get_img_absolute_path(self, poll_id): path = settings.IMAGES_ROOT + '/%s' % poll_id return path + "/%s" % self.get_img_name() # TODO: Need refactoring for receive poll_id in other way def to_python(self, with_errors=False, img_serialize=False, poll_id=None): data = {} data.update({ 'name': self.name, 'widget_type': self.widget_type, 'options': {} }) if self.dependence: data.update({'dependence': self.dependence}) img_name = self.get_img_name() if img_name: if img_serialize: img_path = self.get_img_absolute_path(poll_id) img_file = open(img_path, 'rb') image_string = base64.b64encode(img_file.read()) img_file.close() data.update({'img': image_string}) else: data.update({'img': img_name}) 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(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, poll_id=None): 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, poll_id=poll_id ) data['fields'].update(field_data) return {'%d' % self.order: data} class Structure(AbstractObject, ComponentStructure): """Class that handles the structure of a poll. Example of data_structure: { 'groups': { '0': { 'name': 'group name', 'fields': { '0': { 'widget_type': ..., 'name': ..., 'options': ..., 'dependence': ..., }, '1' ... } }, '1' ... ... } } """ READ_ONLY_MSG = ('No puede modificar la estructura' ' de una encuesta con resultados.') def __init__(self, data={}, poll=None, *args, **kwargs): super(Structure, self).__init__(poll, *args, **kwargs) self.data = data self.groups = [] self.poll = poll # Getting parent poll id from {poll obj | poll dbref | data['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) # REQUIRE: parent poll id !!! if not self._poll_id: raise Exception('INTERNAL ERROR: A structure need a poll id!') # TODO: Test for refatoring => inherit Document class # Getting id 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 # Build model Structure obj based in dict data groups_info = data.get('groups', {}) for group_order, group_data in groups_info.iteritems(): 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_data.update({'order': field_order}) field = Field(field_data, poll=self.poll) field.add_options(field_data.get('options', {})) group.add_field(field, field_order) self.add_group(group, group_order) @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 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_enumerate_options(self, order=None): options = {} for group in self.groups: for field in group.fields: field_offset = field.get_field_offset(self) options.update({"%s" % field_offset: field.options}) return options def get_image_media_url(self): return settings.IMAGES_MEDIA_URL + '/%s' % self.poll_id def get_image_options(self): options = self.get_options() return filter(lambda opt: opt.img_name is not None, options) def get_images_fields(self): fields = [] for group in self.groups: for field in group.fields: fields.append(field) fields = filter(lambda field: field.get_img_name() is not None, fields) return [field.get_img_name() for field in fields] 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, new_data=None): valid = True options = self.get_enumerate_options() temporary_structure = Structure(data=new_data) if new_data else None for group in self.groups: try: group.validate() except Group.ValidationError: valid = False for field in group.fields: try: field.validate(options, new_data=temporary_structure) 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, poll_id=self.poll_id ) ) if self.id: data.update({'_id': ObjectId(self.id)}) return data def save(self): """Save structure to database. Returns structure's ID.""" structure_id = None self.validate() _dict = self.to_python() # Prepare dbref to poll object if not self.poll: raise ValidationError("Need a parent poll.") elif self.is_read_only(): raise ReadOnly(Structure.READ_ONLY_MSG) 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 has an 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 # Removing older img files current_img_fields = self.get_images_fields() path = self.get_image_path() for file in os.listdir(path): if file not in current_img_fields: 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 image_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: with open(path + '/%s' % img_opt.img_name, 'wb+') as dst: for chunk in img_opt.img.chunks(): dst.write(chunk) def get_image_path(self): path = settings.IMAGES_ROOT + '/%s' % self.poll_id try: os.makedirs(path) except OSError: pass return path def save_image_fields(self): for group in self.groups: for field in group.fields: field.store_image(path=self.get_image_path()) def save_image_options(self, tmp=False): options = self.get_image_options() valid_img_options = filter( lambda opt: not 'img' in opt.dict_errors.keys(), options) imgs_to_store = filter( lambda opt: opt.img is not None, valid_img_options) tmp_path = self.get_image_options_tmp_path() path = self.get_image_options_path() if len(imgs_to_store): if tmp: self.__storing_image_options(tmp_path, imgs_to_store) else: self.__storing_image_options(path, imgs_to_store) if not tmp: # Moving tmp images options to the final place for store them for img_opt in valid_img_options: #for img_opt in imgs_to_store: src = '%s/%s' % (tmp_path, img_opt.img_name) dst = '%s/%s' % (path, img_opt.img_name) if os.path.exists(src): shutil.move(src, dst) try: os.removedirs(tmp_path) except: # TODO: tmp must be empty, # if raise exception here is someting bad # TODO: LOG! pass def is_read_only(self): """Tells if the structure can be modified.""" poll = self.poll return not poll.is_open() or poll.has_result() class NodePollResult(object): def __init__(self, id_, answers): self.id = id_ self.answers = answers def sorted(self): answers = self.answers['answers'] sorted_groups = sorted(map(lambda x: int(x), answers.keys())) for group_id in sorted_groups: fields = answers[str(group_id)]['fields'] sorted_fields = sorted(map(lambda x: int(x), fields.keys())) for field_id in sorted_fields: yield fields[str(field_id)] class PollResult(object): def __init__(self, data=None): super(PollResult, self).__init__() _merge = [] _data = {} if isinstance(data, list): _data = copy.deepcopy(data[0]) if len(data) else {} if _data.get('result', None): del _data['result'] # Merge results new_id = 0 for partial_res in data: for id, value in partial_res['result'].iteritems(): if len(value.get('answers', {})): _merge.append((str(new_id), value)) new_id += 1 _data['result'] = dict(_merge) else: _data = data self._data = copy.deepcopy(_data) del _data self._poll_id = self._data.get('poll_id', None) self.id = self._data.get('_id', None) def get_poll(self): return Poll.get(self._poll_id) def to_python(self): return self._data def get_csv_header(self, with_order=False): poll = self.get_poll() try: poll_type = self._data['poll_type'] except: raise Exception("Can't export to csv format") else: poll_record = [] if poll_type == 'general': poll_record = [ 'RUEE', 'DEPARTAMENTO', 'NUM_ESC', 'GRADO', 'GRUPO', 'TIPO_GRUPO' ] elif poll_type == 'monitoreo': poll_record = [ 'RUEE', 'DEPARTAMENTO', 'NUM_ESC', 'GRADO', 'GRUPO', 'TIPO_GRUPO', 'CI', 'NOMBRE' ] poll_header = poll.get_csv_header(with_order=with_order) header = poll_record + poll_header.split(";") return ";".join(header) def to_csv(self): poll = self.get_poll() poll_csv_header = self.get_csv_header(with_order=True) header_order = poll_csv_header.split(";") MANY_ANSWERS = [ "MultipleCheckBox", "ImageCheckBox", ] ONLY_ANSWER = [ "DropDownList", "RadioButton", "TextInput", "ImageRadioButton" ] results = [] for index_record, respuesta in self._data["result"].iteritems(): poll_col_names = poll.get_csv_header(with_order=True).split(";") aux = dict([(x, "") for x in poll_col_names]) for key, value in respuesta['polled'].iteritems(): if key in header_order: aux[key] = value for index_group, data_group in respuesta['answers'].iteritems(): for index_preg, data_preg in data_group['fields'].iteritems(): order = u"%s_%s " % (index_group, index_preg) widget_type = data_preg['widget_type'] name = data_preg['name'] name = unicode(name, "utf-8") if isinstance( name, str) else name name = order + name answer = data_preg['answer'] if len(answer) and widget_type in ONLY_ANSWER: if widget_type == "TextInput": aux[name] = answer.values()[0].get("text", "-") else: aux[name] = str( answer.values()[0].get("weight", "-")) elif len(answer) and widget_type in MANY_ANSWERS: for opt_id, opt_value in answer.iteritems(): text = opt_value.get('text', None) col_name = text if text else opt_id col_name = order + col_name aux[col_name] = str(opt_value.get("weight", "-")) record = [] for key in header_order: record.append(aux[key]) record = ";".join(record) + ";" results.append(record) poll_csv_header = self.get_csv_header() + ";\n" return poll_csv_header + "\n".join(results) def sorted(self): data = self._data['result'] sorted_keys = sorted(map(lambda x: int(x), data.keys())) for result_id in sorted_keys: yield NodePollResult(str(result_id), answers=data[str(result_id)]) class PollResultFile(object): def __init__(self, file_path): self.file_path = file_path self.name = os.path.basename(self.file_path) with open(self.file_path) as f: f.seek(0) self.json_str = f.read() self.data = json.loads(self.json_str, 'utf-8') def get_ruees(self): results = self.data['result'] all_ruees = [] for i in results: ruee = results[i]['polled']['RUEE'] all_ruees.append(ruee) return set(all_ruees) def get_pollster_username(self): return self.data['pollster_username'] def get_polled_count(self): polled_count = len(self.data['result'].keys()) return polled_count def get_file_name(self): return self.name def get_upload_timestamp(self): time_string = self.data.get('upload_timestamp', None) return time_string def set_upload_timestamp(self, time_string): data = self.data data['upload_timestamp'] = time_string self._save() def _save(self): data = self.data with open(self.file_path, 'w') as file_: j = json.dumps(data, sort_keys=True, indent=4, separators=(',', ': ')) file_.seek(0) file_.write(j) def save(self): name = self.get_file_name() file_path = self.file_path poll_id = self.data['poll_id'] poll = Poll.get(poll_id) poll.add_result_files([(file_path, name)]) def get_data(self): return self.data def get_absolute_url(self): poll_id = str(self.data['poll_id']) return os.path.join( settings.RESULT_BCK_URL, poll_id, self.get_file_name()) def delete(self): file_path = self.file_path os.remove(file_path)