Skip to content

Commit

Permalink
Merge pull request #9 from nyaruka/multimedia
Browse files Browse the repository at this point in the history
Merge multimedia field submissions via ODK.
  • Loading branch information
nicpottier committed Sep 14, 2011
2 parents cca45f6 + 72f3d7c commit efaf4c5
Show file tree
Hide file tree
Showing 18 changed files with 561 additions and 55 deletions.
4 changes: 4 additions & 0 deletions CHANGES.rst
Expand Up @@ -4,6 +4,10 @@ Version History

Many thanks to everyone who submits pull requests. We'll merge in most changes that are unit tested and well thought out.

0.3.6
-----
- nicpottier: added ability to submit photos, sounds and videos from ODK clients

0.3.5
-----
- nicpottier: fix performance issue when listing submissions either via CSV or through web interface (thanks jaredalexander for bug)
Expand Down
1 change: 1 addition & 0 deletions MANIFEST.in
@@ -1,3 +1,4 @@
include README.rst
recursive-include rapidsms_xforms/static *
recursive-include rapidsms_xforms/templates *
prune test-runner
2 changes: 1 addition & 1 deletion rapidsms_xforms/__init__.py
@@ -1 +1 @@
__version__ = '0.3.5'
__version__ = '0.3.6'
71 changes: 64 additions & 7 deletions rapidsms_xforms/models.py
Expand Up @@ -16,6 +16,7 @@
from django.contrib.sites.managers import CurrentSiteManager
from rapidsms.models import ExtensibleModelBase
from eav.fields import EavSlugField
from django.core.files.base import ContentFile

class XForm(models.Model):
"""
Expand Down Expand Up @@ -168,8 +169,17 @@ def update_submission_from_dict(self, submission, values):
# we have a new value, update it
field = XFormField.objects.get(pk=value.attribute.pk)
if field.command in values:
value.value = values[field.command]
value.save()
new_val = values[field.command]

# for binary fields, a value of None means leave the current value
if field.xform_type() == 'binary' and new_val is None:
# clean up values that have null values
if value.value is None:
value.delete()
else:
value.value = new_val
value.save()

del values[field.command]

# no new value, we need to remove this one
Expand All @@ -180,7 +190,9 @@ def update_submission_from_dict(self, submission, values):
for key, value in values.items():
# look up the field by key
field = XFormField.objects.get(xform=self, command=key)
sub_value = submission.values.create(attribute=field, value=value, entity=submission)

if field.xform_type() != 'binary' or not value is None:
sub_value = submission.values.create(attribute=field, value=value, entity=submission)

# clear out our error flag if there were some
if submission.has_errors:
Expand All @@ -190,7 +202,7 @@ def update_submission_from_dict(self, submission, values):
# trigger our signal for anybody interested in form submissions
xform_received.send(sender=self, xform=self, submission=submission)

def process_odk_submission(self, xml, values):
def process_odk_submission(self, xml, values, binaries):
"""
Given the raw XML content and a map of values, processes a new ODK submission, returning the newly
created submission.
Expand All @@ -200,7 +212,15 @@ def process_odk_submission(self, xml, values):
for field in self.fields.filter(datatype=XFormField.TYPE_OBJECT):
if field.command in values:
typedef = XFormField.lookup_type(field.field_type)
values[field.command] = typedef['parser'](field.command, values[field.command])

# binary fields (image, audio, video) will have their value set in the XML to the name
# of the key that contains their binary content
if typedef['xforms_type'] == 'binary':
binary_key = values[field.command]
values[field.command] = typedef['parser'](field.command, binaries[binary_key], filename=binary_key)

else:
values[field.command] = typedef['parser'](field.command, values[field.command])

# create our submission now
submission = self.submissions.create(type='odk-www', raw=xml)
Expand Down Expand Up @@ -637,7 +657,9 @@ class XFormField(Attribute):
TYPE_TEXT = Attribute.TYPE_TEXT
TYPE_OBJECT = Attribute.TYPE_OBJECT
TYPE_GEOPOINT = 'geopoint'
TYPE_PHOTO = 'photo'
TYPE_IMAGE = 'image'
TYPE_AUDIO = 'audio'
TYPE_VIDEO = 'video'

# These are the choices of types available for XFormFields.
#
Expand All @@ -654,7 +676,6 @@ class XFormField(Attribute):
TYPE_INT: dict( label='Integer', type=TYPE_INT, db_type=TYPE_INT, xforms_type='integer', parser=None, puller=None, xform_only=False),
TYPE_FLOAT: dict( label='Decimal', type=TYPE_FLOAT, db_type=TYPE_FLOAT, xforms_type='decimal', parser=None, puller=None, xform_only=False),
TYPE_TEXT: dict( label='String', type=TYPE_TEXT, db_type=TYPE_TEXT, xforms_type='string', parser=None, puller=None, xform_only=False),
TYPE_PHOTO: dict( label="Photo", type=TYPE_PHOTO, db_type=TYPE_OBJECT, xforms_type='photo', parser=None, puller=None, xform_only=True),
}

xform = models.ForeignKey(XForm, related_name='fields')
Expand Down Expand Up @@ -1008,11 +1029,47 @@ def __unicode__(self): # pragma: no cover
return "%s=%s" % (self.attribute, self.value)


class BinaryValue(models.Model):
"""
Simple holder for values that are submitted and which represent binary files.
"""
binary = models.FileField(upload_to='binary')

def url(self):
return self.binary.url

def __unicode__(self):
name = self.binary.name
if name.find('/') != -1:
name = name[name.rfind("/")+1:]
return name

# Signal triggered whenever an xform is received. The caller can derive from the submission
# whether it was successfully parsed or not and do what they like with it.

xform_received = django.dispatch.Signal(providing_args=["xform", "submission"])

def create_binary(command, value, filename="binary"):
"""
Save the image to our filesystem, associating a new object to hold it's contents etc..
"""
binary = BinaryValue.objects.create()

# TODO: this seems kind of odd, but I can't figure out how Django really wants you to save
# these files on the initial create
binary.binary.save(filename, ContentFile(value))
binary.save()
return binary

XFormField.register_field_type(XFormField.TYPE_IMAGE, 'Image', create_binary,
xforms_type='binary', db_type=XFormField.TYPE_OBJECT, xform_only=True)

XFormField.register_field_type(XFormField.TYPE_AUDIO, 'Audio', create_binary,
xforms_type='binary', db_type=XFormField.TYPE_OBJECT, xform_only=True)

XFormField.register_field_type(XFormField.TYPE_VIDEO, 'Video', create_binary,
xforms_type='binary', db_type=XFormField.TYPE_OBJECT, xform_only=True)

def create_geopoint(command, value):
"""
Used by our arbitrary field saving / lookup. This takes the command and the string value representing
Expand Down
14 changes: 14 additions & 0 deletions rapidsms_xforms/static/stylesheets/style.css
Expand Up @@ -365,3 +365,17 @@ button.positive, .buttons a.positive{
color:#fff;
}

input[type="checkbox"] + label {
display: inline;
}

.paginator {
margin-top: 5px;
margin-left: 2px;
margin-right: 2px;
color: #aaa;
}

.paginator_pager {
float: right;
}
6 changes: 1 addition & 5 deletions rapidsms_xforms/templates/xforms/form_index.html
Expand Up @@ -55,16 +55,12 @@
<img border="0" src="/static/rapidsms/icons/silk/add.png" alt=""/>Add New Form
</a>
</div>
<br class="clear"/>

<br/><br/>

</div>


<form name="form" method="post" action="/xforms/">
{% csrf_token %}
</form>

</div>

{% endblock %}
13 changes: 13 additions & 0 deletions rapidsms_xforms/templates/xforms/odk_get_form.xml
Expand Up @@ -28,11 +28,24 @@
</model>
</h:head>
<h:body>
{# first output all the normal fields, we'll do media next #}
{% for field in xform.fields.all %}
{% if field.xform_type != 'binary' %}
<input ref="{{ field.command }}">
<label ref="jr:itext('/data/{{ field.command }}:label')"/>
<hint ref="jr:itext('/data/{{ field.command }}:hint')"/>
</input>
{% endif %}
{% endfor %}

{# now all the upload fields, these are all binary (photos and the like) #}
{% for field in xform.fields.all %}
{% if field.xform_type == 'binary' %}
<upload ref="{{ field.command }}" mediatype="{{ field.field_type }}/*">
<label ref="jr:itext('/data/{{ field.command }}:label')"/>
<hint ref="jr:itext('/data/{{ field.command }}:hint')"/>
</upload>
{% endif %}
{% endfor %}
</h:body>
</h:html>
2 changes: 1 addition & 1 deletion rapidsms_xforms/templates/xforms/submission_edit.html
Expand Up @@ -16,7 +16,7 @@
<h3>Values</h3>

{% load uni_form_tags %}
<form action="/xforms/submissions/{{submission.pk}}/edit/" method="post" class="uniForm" id="subForm">
<form action="/xforms/submissions/{{submission.pk}}/edit/" method="post" class="uniForm" id="subForm" enctype="multipart/form-data">
{% csrf_token %}
<fieldset>
{{ form|as_uni_form }}
Expand Down
47 changes: 37 additions & 10 deletions rapidsms_xforms/templates/xforms/submissions.html
Expand Up @@ -4,13 +4,13 @@
{% block content %}

<div class="details">
<div class="name">{{ xform.name }} Submissions</div>
<div class="buttons">
<a href="/xforms/{{ xform.pk }}/submissions.csv">
<img src="{{ MEDIA_URL }}rapidsms_xforms/icons/silk/table_save.png" alt=""/> Export
</a>
<br/>
</div>
<div class="name">{{ xform.name }} Submissions</div>
<div class="buttons">
<a href="/xforms/{{ xform.pk }}/submissions.csv">
<img src="{{ MEDIA_URL }}rapidsms_xforms/icons/silk/table_save.png" alt=""/> Export
</a>
<br/>
</div>
</div>

<table width="100%" class="form_table">
Expand All @@ -25,8 +25,8 @@
</tr>
</thead>

{% if submissions %}
{% for submission in submissions %}
{% if submissions.object_list %}
{% for submission in submissions.object_list %}
<tr class="form_table_row">
<td class="center">
{% ifequal submission.type "odk-www" %}
Expand All @@ -49,7 +49,11 @@
<td>
{% for value in submission.submission_values %}
{% ifequal field.pk value.attribute.pk %}
{% if field.xform_type == 'binary' %}
<a href="{{ value.value.url }}">{{ value.value }}</a>
{% else %}
{{ value.value }}
{% endif %}
{% endifequal %}
{% endfor %}
</td>
Expand All @@ -71,8 +75,31 @@
</tr>
{% endif %}
</table>
<br/>

<div class="paginator">
<div class="paginator_pager">
<span class="paginator_prev">
{% if submissions.has_previous %}
<a href="?page={{ submissions.previous_page_number }}">&laquo; prev</a>
{% endif %}
</span>

<span class="paginator_curr">
Page {{ submissions.number }} of {{ submissions.paginator.num_pages }}
</span>

<span class="paginator_next">
{% if submissions.has_next %}
<a href="?page={{ submissions.next_page_number }}">next &raquo;</a>
{% endif %}
</span>
</div>

<div class="paginator_blurb">
Submissions {{ submissions.start_index }}-{{ submissions.end_index }} of {{ submissions.paginator.count }}
</div>
</div>

<br/>

{% endblock %}

0 comments on commit efaf4c5

Please sign in to comment.