Exemple #1
0
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''
Exemple #3
0
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
Exemple #4
0
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