class FormTemplateFont(Base): '''Represents a font specification for visual form templates.''' __tablename__ = "form_template_font" id = id_column(__tablename__) place = Column(UnicodeText, nullable=False) name = Column(UnicodeText, nullable=False) size = Column(Integer, nullable=False) bold = Column(Boolean, default=False) italic = Column(Boolean, default=False) template_id = Column(Integer, ForeignKey('form_template.id')) template = relationship(FormTemplate, backref=backref('fonts', cascade='all')) __table_args__ = (UniqueConstraint( 'template_id', 'place', name='form_template_font_template_id_place_key'), {}) def __unicode__(self): style = "" style += " bold " if self.bold else "" style += " italic " if self.italic else "" return "{0} {1} {2}".format(self.name, self.size, style) def __repr__(self): return "FormTemplateFont: {0} = {1}" \ .format(self.place, self.__unicode__())
class Entry(Base): '''Represents a form entry. *created* is the moment the entry was created *form_id* points to the corresponding form. *entry_number* is the number of an entry. ''' __tablename__ = "entry" id = id_column(__tablename__) created = now_column() # when was this record created entry_number = Column(Integer) new = Column(Boolean, default=True) form_id = Column(Integer, ForeignKey('form.id'), index=True) form = relationship(Form, backref=backref('entries', order_by=(desc(created)), cascade='all')) collector_id = Column(Integer, ForeignKey('collector.id'), index=True) collector = relationship(Collector, backref=backref('entries', order_by=id)) def fields_data(self, field_idx="FIELD_ID", fields=[], request=None): url = request.application_url if request else '' if fields == []: # Get all text data if field_idx == "FIELD_ID": fields_data_list = [{ 'id': f.id, 'data': f.value(self).format(url=url) } for f in self.form.fields] elif field_idx == "FIELD_LABEL": fields_data_list = { 'form_title': self.form.name, 'entry_id': self.id, 'entry_number': self.entry_number, 'fields': [{ 'position': f.position + 1, 'label': f.label, 'data': f.value(self).format(url=url), 'type': f.typ.name } for f in self.form.fields] } return fields_data_list def delete_entry(self): sas.delete(self) sas.flush() return () def to_dict(self): return { 'entry_id': self.id, 'entry_created': unicode(self.created)[:16], 'entry_number': self.entry_number, 'entry_new': self.new }
class FieldOption(Base): '''Represents a field option.''' __tablename__ = "field_option" id = id_column(__tablename__) option = Column(UnicodeText) value = Column(UnicodeText) field_id = Column(Integer, ForeignKey('field.id'), nullable=False) field = relationship(Field, backref=backref('options', cascade='all, delete-orphan', single_parent=True)) def __repr__(self): return "[{}. {} = {}]".format(self.id, self.option, self.value) def copy(self): field_option_copy = FieldOption() # field option instance copy for attr in ('option', 'value'): field_option_copy.__setattr__(attr, self.__getattribute__(attr)) sas.add(field_option_copy) return field_option_copy
class TextData(Base): __tablename__ = "text_data" id = id_column(__tablename__) value = Column(UnicodeText, nullable=True) field_id = Column(Integer, ForeignKey('field.id')) field = relationship(Field, backref=backref('text_data', cascade='all')) entry_id = Column(Integer, ForeignKey('entry.id')) entry = relationship(Entry, backref=backref('text_data', cascade='all'))
class NumberData(Base): __tablename__ = "number_data" id = id_column(__tablename__) value = Column(Float, nullable=True) field_id = Column(Integer, ForeignKey('field.id')) field = relationship(Field, backref=backref('number_data', cascade='all')) entry_id = Column(Integer, ForeignKey('entry.id')) entry = relationship(Entry, backref=backref('number_data', cascade='all'))
class ListData(Base): __tablename__ = "list_data" id = id_column(__tablename__) value = Column(Integer, ForeignKey('list_option.id'), index=True) list_option = relationship(ListOption, backref='list_data') entry_id = Column(Integer, ForeignKey('entry.id'), index=True) entry = relationship(Entry, backref=backref('list_data')) field_id = Column(Integer, ForeignKey('field.id'), index=True) field = relationship(Field, backref=backref('list_data', cascade='all'))
class FieldType(Base): '''Represents a kind of field that is possible in a form. *name* is the type's name. *description* is a brief explanation of the type. ''' __tablename__ = "field_type" id = id_column(__tablename__) name = Column(UnicodeText, nullable=False) description = Column(UnicodeText, nullable=True) def __repr__(self): return 'FieldType(id={0}, name="{1}")'.format(self.id, self.name)
class FormTemplate(Base): '''Represents a visual template of a form.''' __tablename__ = "form_template" id = id_column(__tablename__) # System templates have this not null. system_template_id = Column(Integer, unique=True, default=None) system_template_name = Column(UnicodeText(32)) @property def system(self): '''Returns True if this template is a *system* template.''' return True if self.system_template_id else False def __repr__(self): return "FormTemplate (id = {0})".format(self.id) def __unicode__(self): return self.__repr__() def to_dict(self): colors = {} fonts = {} for c in self.colors: colors[c.place] = c.hexcode for f in self.fonts: fonts[f.place] = dict(name=f.name, size=f.size, bold=f.bold, italic=f.italic) return { 'formtemplate_id': self.id, 'system_template_id': self.system_template_id, 'system_template_name': self.system_template_name, 'colors': colors, 'fonts': fonts } def css_template_dicts(self): fonts = dict() for ftf in self.fonts: fonts[ftf.place] = dict() for attr in ('name', 'size', 'bold', 'italic'): fonts[ftf.place][attr] = ftf.__getattribute__(attr) colors = dict() for ftc in self.colors: colors[ftc.place] = ftc.hexcode return fonts, colors
class ListOption(Base): __tablename__ = "list_option" id = id_column(__tablename__) label = Column(UnicodeText()) value = Column(UnicodeText()) opt_default = Column(Boolean(), default=False) position = Column(Integer, default=0) status = Column( Enum('Approved', 'Rejected', 'Awaiting moderation', 'Form owner', name='list_option_status')) field_id = Column(Integer, ForeignKey('field.id'), index=True) field = relationship(Field, backref=backref('list_options', cascade='all'))
class FileUploadTempStore(Base): __tablename__ = "file_upload_temp_store" id = id_column(__tablename__) created = now_column() uid = Column(Unicode(10), nullable=False) mimetype = Column(Unicode(255)) #http://tools.ietf.org/html/rfc4288#section-4.2 filename = Column(UnicodeText) size = Column(Integer) path = Column(UnicodeText) thumbnail_path = Column(UnicodeText) def to_dict(self): d = {k: getattr(self, k) for k in ('created', 'uid', 'mimetype', 'filename', 'size', 'path', 'thumbnail_path')} return d
class FileData(Base): __tablename__ = "file_data" id = id_column(__tablename__) created = now_column() mimetype = Column( Unicode(255)) #http://tools.ietf.org/html/rfc4288#section-4.2 filename = Column(UnicodeText) size = Column(Integer) path = Column(UnicodeText) thumbnail_path = Column(UnicodeText) field_id = Column(Integer, ForeignKey('field.id')) field = relationship(Field, backref=backref('file_data', cascade='all')) entry_id = Column(Integer, ForeignKey('entry.id')) entry = relationship(Entry, backref=backref('file_data', cascade='all'))
class FormTemplateColor(Base): '''Represents a color specification for visual form templates.''' __tablename__ = "form_template_color" id = id_column(__tablename__) place = Column(UnicodeText, nullable=False) hexcode = Column(UnicodeText, nullable=False) template_id = Column(Integer, ForeignKey('form_template.id')) template = relationship(FormTemplate, backref=backref('colors', cascade='all')) __table_args__ = (UniqueConstraint( 'template_id', 'place', name='form_template_color_template_id_place_key'), {}) def __unicode__(self): return self.hexcode def __repr__(self): return "FormTemplateColor: {0} = {1}" \ .format(self.place, self.__unicode__())
class Field(Base): '''Represents a field of a form. *label* is the text that appears next to the field, identifying it. *description* is a brief explanation. *rich* may contain a rich text alternative to both *label* and *description* together. *use_rich* is a flag that determines which alternative is to be used. *help_text* is a long explanation. *title* is short content for a tooltip (HTML "title" attribute). *position* is an integer for ordering fields inside the form. *required* states whether filling in this field is mandatory. *form_id* points to the form that owns this field. ''' __tablename__ = "field" id = id_column(__tablename__) label = Column(UnicodeText, nullable=False) description = Column(UnicodeText, nullable=True) rich = Column(UnicodeText, nullable=False, default='') use_rich = Column(Boolean, default=False) help_text = Column(UnicodeText, nullable=True) title = Column(UnicodeText, nullable=True) position = Column(Integer) required = Column(Boolean) typ_id = Column(ForeignKey('field_type.id')) # TODO: index? typ = relationship(FieldType) form_id = Column(Integer, ForeignKey('form.id'), index=True) form = relationship(Form, backref=backref('fields', order_by=position, cascade='all')) def __repr__(self): return '{} #{} "{}"{}'.format(self.typ.name, self.id, self.label, '*' if self.required else '') @reify def fieldtype(self): return fields_dict[self.typ.name](self) def to_dict(self, to_export=False): d = self.fieldtype.to_dict(to_export=to_export) return d def save_option(self, option, value): return self.fieldtype.save_option(option, value) def save_options(self, options_dict): return self.fieldtype.save_options(options_dict) def validate_and_save(self, props): return self.fieldtype.validate_and_save(props) def get_option(self, option): opt = sas.query(FieldOption)\ .filter(FieldOption.field_id == self.id) \ .filter(FieldOption.option == option).first() if opt: return opt.value else: return self.fieldtype.defaultValue[option] def value(self, entry): return self.fieldtype.value(entry) copy_props = 'label description rich use_rich help_text title position ' \ 'required typ'.split() def copy(self): field_copy = Field() # field instance copy for attr in self.copy_props: field_copy.__setattr__(attr, self.__getattribute__(attr)) # field options copy for o in self.options: field_copy.options.append(o.copy()) # field specific options copy fieldtype = fields_dict[self.typ.name](field_copy) if getattr(fieldtype, 'copy', None): fieldtype.copy(self) sas.add(field_copy) return field_copy
class Form(Base): '''Represents a form as created by a user. *name* is the text that appears at the top of the form, identifying it. *description* is a brief explanation. *rich* may contain a rich text alternative to both *name* and *description* together. *use_rich* is a flag that determines which alternative is to be used. ''' __tablename__ = "form" id = id_column(__tablename__) created = now_column() # when was this record created modified = now_column() # when was this form saved name = Column(UnicodeText(255), nullable=False) description = Column(UnicodeText) rich = Column(UnicodeText, nullable=False, default='') use_rich = Column(Boolean, default=False) submit_label = Column(UnicodeText(255)) # Incremented number of the last entry. Serves as a counter of the entries last_entry_number = Column(Integer, default=0) # How many new entries since last visit new_entries = Column(Integer, default=0) # TODO: Create an index here when categories make their triumphant comeback category_id = Column(Integer, ForeignKey('form_category.id')) category = relationship(FormCategory, backref=backref('forms', order_by=name)) # TODO: Create an index here if we ever query from the parent template_id = Column(Integer, ForeignKey('form_template.id')) template = relationship(FormTemplate, backref=backref('forms', cascade='all')) user_id = Column(Integer, ForeignKey('user.id'), index=True) user = relationship(User, backref=backref('forms', order_by=name, cascade='all')) STATUS_EDITION = _("edition") STATUS_PENDING = _("pending") STATUS_PUBLISHED = _("published") STATUS_CLOSED = _("closed") @property def status(self): from mootiro_form.models.collector import Collector collectors_status = (Collector.STATUS_DURING, Collector.STATUS_BEFORE, Collector.STATUS_AFTER) classifier = {} for cs in collectors_status: classifier[cs] = [] for c in self.collectors: classifier[c.status].append(c) if classifier[Collector.STATUS_DURING]: status = self.STATUS_PUBLISHED num_colls_in_status = len(classifier[Collector.STATUS_DURING]) elif classifier[Collector.STATUS_BEFORE]: status = self.STATUS_PENDING num_colls_in_status = len(classifier[Collector.STATUS_BEFORE]) elif classifier[Collector.STATUS_AFTER]: status = self.STATUS_CLOSED num_colls_in_status = len(classifier[Collector.STATUS_AFTER]) else: status = self.STATUS_EDITION num_colls_in_status = 0 return (status, num_colls_in_status) def __unicode__(self): return self.name def __repr__(self): return '{0}. {1}'.format(self.id, self.name) @property def num_entries(self): num_entries = sas.query(Entry).filter(Entry.form_id == self.id).count() return num_entries def to_dict(self): return {'form_id': self.id, 'form_name': self.name or _('Untitled form'), 'form_entries': self.num_entries, 'form_new_entries': self.new_entries, 'form_description': self.description, 'form_rich': self.rich, 'form_use_rich': self.use_rich, 'form_created': unicode(self.created)[:16], 'form_modified': unicode(self.modified)[:16], 'form_status': self.status[0], 'form_status_num': self.status[1], 'form_questions': sas.query(Field) \ .filter(Field.form_id == self.id).count(), } def export_json(self): form_dict = dict(form_name=self.name or _('Untitled form'), form_description=self.description, fields=[f.to_dict(to_export=True) \ for f in self.fields]) for field in form_dict['fields']: field.pop('field_id') return json.dumps(form_dict, indent=4) copy_props = 'user category rich use_rich name template description ' \ 'submit_label'.split() def copy(self): form_copy = Form() # form instance copy for attr in self.copy_props: setattr(form_copy, attr, getattr(self, attr)) # fields copy for f in self.fields: form_copy.fields.append(f.copy()) sas.add(form_copy) return form_copy
class Collector(Base): '''Represents a collector to collect form entries.''' __tablename__ = "collector" id = id_column(__tablename__) # Inheritance configuration typ = Column('type', UnicodeText(50)) __mapper_args__ = {'polymorphic_on': typ} name = Column(UnicodeText(255), nullable=False) # When an entry is received, we can either display a thanks message, # or redirect to some URL. 3 columns are needed for this: thanks_message = Column(UnicodeText) thanks_url = Column(UnicodeText(2000)) # We define on_completion as a property to validate its possible values: ON_COMPLETION_VALUES = ('msg', 'url') _on_completion = Column('on_completion', Unicode(3)) @hybrid_property def on_completion(self): return self._on_completion @on_completion.setter def on_completion(self, val): if val not in self.ON_COMPLETION_VALUES: raise ValueError \ ('Invalid value for on_completion: "{0}"'.format(val)) self._on_completion = val email_each_entry = Column(Boolean, default=False) limit_by_date = Column(Boolean, default=False) start_date = Column(DateTime) end_date = Column(DateTime) message_after_end = Column(UnicodeText) message_before_start = Column(UnicodeText) # When an instance is persisted, it automatically gets a slug, slug = Column( UnicodeText(10), nullable=False, # a part of the URL. index=True, default=lambda: random_word(10)) form_id = Column(Integer, ForeignKey('form.id'), index=True) form = relationship(Form, backref=backref('collectors', order_by=id, cascade='all')) def __unicode__(self): return self.name def __repr__(self): return 'Collector(id={0}, name="{1}")'.format(self.id, self.name) def to_dict(self, translator=None): d = { k: getattr(self, k) for k in ('id', 'name', 'thanks_message', 'thanks_url', 'on_completion', 'message_before_start', 'message_after_end', 'email_each_entry', 'limit_by_date', 'slug', 'status') } d['start_date'] = unicode(self.start_date)[:16] \ if self.start_date else '' d['end_date'] = unicode(self.end_date)[:16] if self.end_date else '' d['type'] = self.typ d['display_type'] = self.typ.replace("_", " ").capitalize() d['translated_status'] = \ translator(d['status']) if translator else d['status'] return d STATUS_BEFORE = _('pending') # before start date STATUS_DURING = _('published') # entries may be created STATUS_AFTER = _('closed') # after end date @property def status(self): '''Returns a status code.''' if (self.start_date and datetime.utcnow() < self.start_date and self.limit_by_date): return self.STATUS_BEFORE if (self.end_date and datetime.utcnow() > self.end_date and self.limit_by_date): return self.STATUS_AFTER return self.STATUS_DURING