class FormGroup(db.EmbeddedDocument): '''The :class:`core.documents.FormGroup` model provides storage for form groups in a :class:`core.documents.Form` and are the organizational structure for form fields. Besides the :attr:`fields` attribute for storing form fields, there's also a :attr:`name` attribute for storing the name.''' name = db.StringField(required=True) slug = db.StringField(required=True) fields = db.ListField(db.EmbeddedDocumentField('FormField'))
class Deployment(db.Document): name = db.StringField(required=True) hostnames = db.ListField(db.StringField()) administrative_divisions_graph = db.StringField() participant_extra_fields = db.ListField( db.EmbeddedDocumentField(CustomDataField)) allow_observer_submission_edit = db.BooleanField( default=True, verbose_name='Allow editing of Participant submissions?') logo = db.StringField() include_rejected_in_votes = db.BooleanField(default=False) is_initialized = db.BooleanField(default=False) dashboard_full_locations = db.BooleanField( default=True, verbose_name='Show all locations for dashboard stats?') meta = {'indexes': [['hostnames']]} def __unicode__(self): return self.name or u''
class Form(db.Document): '''Primary storage for Checklist/Incident Forms. Defines the following attributes: :attr:`events` a list of references to :class:`core.documents.Event` objects defining which events this form is to be used in. :attr:`groups` storage for the form groups in the form. :attr:`form_type` for specifying the type of the form as described by :attr:`FORM_TYPES`. :attr:`prefix` determines the prefix for the form. This prefix is used in identifying which form is to be used in parsing incoming submissions. :attr:`name` is the name for this form. :attr:`party_mappings` uses field names as keys, party identifiers as values. :attr:`calculate_moe` is true if Margin of Error calculations are going to be computed on results for this form.''' FORM_TYPES = (('CHECKLIST', _('Checklist Form')), ('INCIDENT', _('Incident Form'))) name = db.StringField(required=True) prefix = db.StringField() form_type = db.StringField(choices=FORM_TYPES) require_exclamation = db.BooleanField( default=True, verbose_name= _('Require exclamation (!) mark in text message? (Does not apply to Checklist Forms)' )) groups = db.ListField(db.EmbeddedDocumentField('FormGroup')) version_identifier = db.StringField() events = db.ListField(db.ReferenceField(Event, reverse_delete_rule=db.PULL)) deployment = db.ReferenceField(Deployment) quality_checks = db.ListField(db.DictField()) party_mappings = db.DictField() calculate_moe = db.BooleanField(default=False) accredited_voters_tag = db.StringField(verbose_name=_("Accredited Voters")) verifiable = db.BooleanField(default=False, verbose_name=_("Quality Assurance")) invalid_votes_tag = db.StringField(verbose_name=_("Invalid Votes")) registered_voters_tag = db.StringField(verbose_name=_("Registered Voters")) blank_votes_tag = db.StringField(verbose_name=_("Blank Votes")) permitted_roles = db.ListField(db.ReferenceField( Role, reverse_delete_rule=db.PULL), verbose_name=_("Permitted Roles")) meta = { 'indexes': [['prefix'], ['events'], ['events', 'prefix'], ['events', 'form_type'], ['deployment'], ['deployment', 'events']] } def __unicode__(self): return self.name or u'' @property def tags(self): if not hasattr(self, '_field_cache'): self._field_cache = { f.name: f for g in self.groups for f in g.fields } return sorted(self._field_cache.keys()) # added so we don't always have to iterate over everything # in the (admittedly rare) cases we need a specific field def get_field_by_tag(self, tag): if not hasattr(self, '_field_cache'): self._field_cache = { f.name: f for g in self.groups for f in g.fields } return self._field_cache.get(tag) # see comment on get_field_by_tag def get_group_by_name(self, name): if not hasattr(self, '_group_cache'): self._group_cache = {g.name: g for g in self.groups} return self._group_cache.get(name) def clean(self): '''Ensures all :class: `core.documents.FormGroup` instances for this document have their slug set.''' for group in self.groups: if not group.slug: group.slug = slugify_unicode(group.name).lower() return super(Form, self).clean() def save(self, **kwargs): # overwrite version identifier self.version_identifier = uuid4().hex super(Form, self).save(**kwargs) # create permissions for roles Need.objects.filter(action='view_forms', items=self, deployment=self.deployment).delete() Need.objects.create(action='view_forms', items=[self], entities=self.permitted_roles, deployment=self.deployment) def update(self, **kwargs): # overwrite version identifier kwargs2 = kwargs.copy() kwargs2.update(set__version_identifier=uuid4().hex) return super(Form, self).update(**kwargs2) def hash(self): xform_data = etree.tostring(self.to_xml(), encoding='UTF-8', xml_declaration=True) m = hashlib.md5() m.update(xform_data) return "md5:%s" % m.hexdigest() def to_xml(self): root = HTML_E.html() head = HTML_E.head(HTML_E.title(self.name)) data = E.data(id='-1') # will be replaced with actual submission ID model = E.model(E.instance(data)) body = HTML_E.body() model.append(E.bind(nodeset='/data/form_id', readonly='true()')) model.append(E.bind(nodeset='/data/version_id', readonly='true()')) form_id = etree.Element('form_id') form_id.text = unicode(self.id) version_id = etree.Element('version_id') version_id.text = self.version_identifier data.append(form_id) data.append(version_id) # set up identifiers data.append(E.device_id()) data.append(E.subscriber_id()) data.append(E.phone_number()) device_id_bind = E.bind(nodeset='/data/device_id') device_id_bind.attrib['{%s}preload' % NSMAP['jr']] = 'property' device_id_bind.attrib['{%s}preloadParams' % NSMAP['jr']] = 'deviceid' subscriber_id_bind = E.bind(nodeset='/data/subscriber_id') subscriber_id_bind.attrib['{%s}preload' % NSMAP['jr']] = 'property' subscriber_id_bind.attrib['{%s}preloadParams' % NSMAP['jr']] = 'subscriberid' phone_number_bind = E.bind(nodeset='/data/phone_number') phone_number_bind.attrib['{%s}preload' % NSMAP['jr']] = 'property' phone_number_bind.attrib['{%s}preloadParams' % NSMAP['jr']] = 'phonenumber' model.append(device_id_bind) model.append(subscriber_id_bind) model.append(phone_number_bind) for group in self.groups: grp_element = E.group(E.label(group.name)) for field in group.fields: data.append(etree.Element(field.name)) path = '/data/{}'.format(field.name) # fields that carry options may be single- or multiselect if field.options: # sort options by value sorted_options = sorted(field.options.iteritems(), key=itemgetter(1)) if field.allows_multiple_values: elem_fac = E.select model.append(E.bind(nodeset=path, type='select')) else: elem_fac = E.select1 model.append(E.bind(nodeset=path, type='select1')) field_element = elem_fac(E.label(field.description), ref=field.name) for key, value in sorted_options: field_element.append( E.item(E.label(key), E.value(unicode(value)))) else: if field.represents_boolean: field_element = E.select1( E.label(field.description), E.item(E.label('True'), E.value('1')), E.item(E.label('False'), E.value('0')), ref=field.name) model.append(E.bind(nodeset=path, type='select1')) elif field.is_comment_field: field_element = E.input(E.label(field.description), ref=field.name) model.append(E.bind(nodeset=path, type='string')) else: field_element = E.input(E.label(field.description), ref=field.name) model.append( E.bind(nodeset=path, type='integer', constraint='. >= {} and . <= {}'.format( field.min_value, field.max_value))) grp_element.append(field_element) body.append(grp_element) head.append(model) root.append(head) root.append(body) return root
class Participant(db.DynamicDocument): '''Storage for participant contact information''' GENDER = (('F', _('Female')), ('M', _('Male')), ('', _('Unspecified'))) participant_id = db.StringField() name = db.StringField() role = db.ReferenceField('ParticipantRole') partner = db.ReferenceField('ParticipantPartner') location = db.ReferenceField('Location') location_name_path = db.DictField() supervisor = db.ReferenceField('Participant') gender = db.StringField(choices=GENDER, default='') groups = db.ListField( db.ReferenceField(ParticipantGroup, reverse_delete_rule=db.PULL)) email = db.EmailField() phones = db.ListField(db.EmbeddedDocumentField(PhoneContact)) message_count = db.IntField(default=0) accurate_message_count = db.IntField(default=0) event = db.ReferenceField(Event) deployment = db.ReferenceField(Deployment) completion_rating = db.FloatField(default=1) device_id = db.StringField() password = db.StringField() meta = { 'indexes': [['participant_id'], ['device_id'], ['location'], ['phones.number'], ['event'], ['name'], ['role'], ['partner'], ['groups'], ['deployment'], ['deployment', 'event']], 'queryset_class': ParticipantQuerySet } def __unicode__(self): return self.name or u'' def clean(self): # unlike for submissions, this always gets called, because # participants are 'mobile' - they can be moved from one location # to another. we want this to reflect that. self.location_name_path = compute_location_path(self.location) if self.gender not in map(lambda op: op[0], self.GENDER): self.gender = '' def get_phone(self): if self.phones: return self.phones[0].number else: return None def set_phone(self, value): # TODO: blind overwrite is silly. find a way to ensure the number # doesn't already exist if not self.phones: self.phones.append(PhoneContact(number=value, verified=True)) else: self.phones[0].number = value self.phones[0].verified = True self.save() self.reload() phone = property(get_phone, set_phone) @property def last_seen_phone(self): if self.phones: phones = sorted(self.phones, key=lambda p: p.last_seen if p.last_seen else datetime.fromtimestamp(0)) phones.reverse() return phones[0].number else: return None