-
Notifications
You must be signed in to change notification settings - Fork 0
/
models.py
236 lines (171 loc) · 7.22 KB
/
models.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
#!/usr/bin/env python
# vim: ai ts=4 sts=4 et sw=4
import re
from django.db import models
# TODO FIXME
#from rapidsms.contrib.locations.models import Location
from rapidsms.models import Connection, Contact
class Section(models.Model):
title = models.CharField(max_length=100)
code = models.CharField(max_length=30)
pattern = models.CharField(max_length=100, blank=True,
help_text="Any incoming message prefixed by a string matching this regexp " +
"is assumed to be reporting on this section. The CODE field is " +
"automatically prepended to this pattern.")
def __unicode__(self):
return self.title
@property
def prefix(self):
if self.pattern:
return("%s|(?:%s)" %
(self.code, self.pattern))
# no pattern was specified, so all
# we need to look for is the code
return self.code
class Question(models.Model):
QUESTION_TYPES = (
("F", "Free text"),
("B", "Boolean"),
("N", "Numeric"),
("M", "Multiple choice"))
type = models.CharField(max_length=1, choices=QUESTION_TYPES,
help_text="<br>".join([
"<b>Free Text</b> questions will accept any text, but can use Options to coerce expected answers into data.",
"<b>Boolean</b> questions will accept TRUE or FALSE, in a variety of guises. Options are ignored.",
"<b>Numeric</b> questions will accept any numeric (integer or decimal) value. Options are ignored.",
"<b>Multiple Choice</b> questions will only accept a valid Option.",
]))
section = models.ForeignKey(Section, related_name="questions")
number = models.IntegerField()
text = models.TextField()
class Meta:
ordering = ["section", "number"]
def __unicode__(self):
return "%s Q%d" % (
self.section,
self.number)
@property
def num_answers(self):
return self.answers.count()
@property
def answer_percentage(self):
sect_sub = self.section.submissions.count()
if not sect_sub:
return "0"
return "%2d" % ((float(self.num_answers) / sect_sub) * 100)
@property
def last_answer(self):
"""Returns the latest Answer to this Question, via the linked Sumbmission
object's _submission_ field,, or returns None is no Answers are linked."""
try:
return self.answers.all().order_by("-submission__submitted")[0]
# if there were no Answer objects, accessing [0]
# will raise an indexerror, so wrap and return
# None, to indicate something like NO ANSWERS
except IndexError:
return None
class Option(models.Model):
question = models.ForeignKey(Question, related_name="options")
text = models.CharField(max_length=100)
letters = models.CharField(max_length=10, blank=True,
help_text="Any answer containing ONLY a letter from this field " +\
"is assumed to be referring to this option. Intended " +\
"for shortcuts like <b>1</b> or <b>a</b>.")
words = models.TextField(blank=True,
help_text="Enter one word per line. Any answer containing " +\
"a word is assumed to be referring to this option.")
pattern = models.CharField(max_length=100, blank=True,
help_text="Any answer matched by this pattern is " +\
"assumed to be referring to this option.")
class Meta:
ordering = ["id"]
def __unicode__(self):
return "Option to %s: %s" % (
self.question,
self.text)
def match(self, text):
if self.letters:
if re.match(r"^(" + "|".join(self.letters) + r")\.?$", text, re.IGNORECASE):
return True
elif self.words:
if re.match(r"^.*" + ("|".join(self.words.split("\n"))) + r".*$", text, re.IGNORECASE):
return True
elif self.pattern:
if re.match(self.pattern, text, re.IGNORECASE):
return True
# if we haven't returned yet,
# this option does not match
return False
class Submission(models.Model):
reporter = models.ForeignKey(Contact, null=True, related_name="submissions")
connection = models.ForeignKey(Connection, null=True, related_name="submissions")
# TODO FIXME
# location = models.ForeignKey(Location, null=True, related_name="submissions")
section = models.ForeignKey(Section, related_name="submissions")
submitted = models.DateTimeField(auto_now_add=True)
raw_text = models.TextField()
class Meta:
ordering = ["-submitted"]
def __unicode__(self):
return "Submission by %s on %s" % (
(self.reporter or self.connection),
self.section)
@property
def reported_by(self):
for x in [self.reporter, self.connection]:
if x is not None:
return x
# this won't happen if the submission was
# created by app.py, but it could have
# been created in the admin... (?)
return None
@property
def num_answers(self):
return self.answers.count()
class Answer(models.Model):
TRUE = re.compile(r"^(?:[YT].*|1)$", re.IGNORECASE)
FALSE = re.compile(r"^(?:[NF].*|0)$", re.IGNORECASE)
submission = models.ForeignKey(Submission, related_name="answers")
question = models.ForeignKey(Question, related_name="answers")
raw_text = models.TextField()
def __unicode__(self):
return "Answer to %s: %s" % (
self.question,
self.raw_text)
@property
def normalized(self):
text = self.raw_text.strip()
type = self.question.type
# free text
if type == "F":
# attempt to match an option
# but it's no big deal if none do
for opt in self.question.options.all():
if opt.match(text):
return opt.text
return text
# boolean
elif type == "B":
if self.TRUE.match(text): return True
elif self.FALSE.match(text): return False
else: return None
# numeric
elif type == "N":
# attempt to cast the text to a
for func in [int, float]:
try: return func(text)
except: pass
# the answer couldn't be cast to a
# float or int, so return None (unknown)
return None
# multiple-choice
elif type == "M":
# match the text to an option or return
# None (unknown) - it's a strict version of
# "free text", when ambiguity is undesirable
for opt in self.question.options.all():
if opt.match(text):
return opt.text
return None
# nothing else is supported yet!
else: return None