Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Send survey utility & default num_tries #32

Open
wants to merge 12 commits into
base: develop
Choose a base branch
from
5 changes: 5 additions & 0 deletions decisiontree/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@ class SessionAdmin(admin.ModelAdmin):
list_display = ('id', 'connection', 'tree', 'canceled')


class TranscriptMessageAdmin(admin.ModelAdmin):
pass


admin.site.register(models.Tree, TreeAdmin)
admin.site.register(models.Question, QuestionAdmin)
admin.site.register(models.Answer, AnswerAdmin)
Expand All @@ -78,3 +82,4 @@ class SessionAdmin(admin.ModelAdmin):
admin.site.register(models.TagNotification, TagNotificationAdmin)
admin.site.register(models.Entry, EntryAdmin)
admin.site.register(models.Session, SessionAdmin)
admin.site.register(models.TranscriptMessage, TranscriptMessageAdmin)
41 changes: 30 additions & 11 deletions decisiontree/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from rapidsms.models import Connection

from . import conf
from .models import Entry, Session, TagNotification, Transition
from .models import Entry, Session, TagNotification, Transition, TranscriptMessage
from .signals import session_end_signal
from .utils import get_survey

Expand All @@ -21,6 +21,21 @@ class App(AppBase):
registered_functions = {}
session_listeners = {}

def record_message(self, msg, session):
TranscriptMessage.objects.create(
session=session,
direction=TranscriptMessage.INCOMING,
message=msg.text,
)

def send_response(self, msg, session, response):
TranscriptMessage.objects.create(
session=session,
direction=TranscriptMessage.OUTGOING,
message=response,
)
msg.respond(response)

def handle(self, msg):
sessions = msg.connection.session_set.open().select_related('state')

Expand All @@ -38,13 +53,14 @@ def handle(self, msg):
# the caller is part-way though a question
# tree, so check their answer and respond
session = sessions[0]
self.record_message(msg, session)
state = session.state
logger.debug(state)

end_trigger = conf.SESSION_END_TRIGGER
if end_trigger is not None and msg.text == end_trigger:
response = _("Your session with '%s' has ended")
msg.respond(response % session.tree.trigger)
self.send_response(msg, session, response % session.tree.trigger)
self._end_session(session, True, message=msg)
return True

Expand All @@ -63,7 +79,7 @@ def handle(self, msg):
if not found_transition:
if transitions.count() == 0:
logger.error('No questions found!')
msg.respond(_("No questions found"))
self.send_response(msg, session, _("No questions found"))
self._end_session(session, message=msg)
else:
# update the number of times the user has tried
Expand All @@ -74,17 +90,19 @@ def handle(self, msg):
if state.num_retries is not None:
if session.num_tries >= state.num_retries:
session.state = None
msg.respond("Sorry, invalid answer %d times. "
"Your session will now end. Please try again "
"later." % session.num_tries)
self.send_response(
msg, session,
"Sorry, invalid answer %d times. "
"Your session will now end. Please try again "
"later." % session.num_tries)
# send them some hints about how to respond
elif state.question.error_response:
msg.respond(state.question.error_response)
self.send_response(msg, session, state.question.error_response)
else:
answers = [t.answer.helper_text() for t in transitions]
answers = " or ".join(answers)
response = '"%s" is not a valid answer. You must enter ' + answers
msg.respond(response % msg.text)
self.send_response(msg, session, response % msg.text)

session.save()
return True
Expand Down Expand Up @@ -128,7 +146,7 @@ def handle(self, msg):
# completion text and if so send it
if not session.state:
if session.tree.completion_text:
msg.respond(session.tree.completion_text)
self.send_response(msg, session, session.tree.completion_text)

# end the connection so the caller can start a new session
self._end_session(session, message=msg)
Expand All @@ -155,12 +173,13 @@ def tick(self, session):
self.router.incoming(msg)
msg.flush_responses() # make sure response goes out

def start_tree(self, tree, connection, msg=None):
def start_tree(self, tree, connection, msg):
"""Initiates a new tree sequence, terminating any active sessions"""
self.end_sessions(connection)
session = Session(connection=connection,
tree=tree, state=tree.root_state, num_tries=0)
session.save()
self.record_message(msg, session)
logger.debug("new session %s saved", session)

# also notify any session listeners of this
Expand All @@ -177,7 +196,7 @@ def _send_question(self, session, msg=None):
response = state.question.text
logger.info("Sending: %s", response)
if msg:
msg.respond(response)
self.send_response(msg, session, response)
else:
# we need to get the real backend from the router
# to properly send it
Expand Down
20 changes: 20 additions & 0 deletions decisiontree/migrations/0005_num_tries_default.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django.db import models, migrations


class Migration(migrations.Migration):

dependencies = [
('decisiontree', '0004_answer_color'),
]

operations = [
migrations.AlterField(
model_name='session',
name='num_tries',
field=models.PositiveIntegerField(default=0, help_text=b'The number of times the user has tried to answer the current question.'),
preserve_default=True,
),
]
28 changes: 28 additions & 0 deletions decisiontree/migrations/0006_transcriptmessage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django.db import models, migrations


class Migration(migrations.Migration):

dependencies = [
('decisiontree', '0005_num_tries_default'),
]

operations = [
migrations.CreateModel(
name='TranscriptMessage',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('direction', models.CharField(max_length=1, choices=[(b'I', b'Incoming'), (b'O', b'Outgoing')])),
('message', models.CharField(max_length=255)),
('created', models.DateTimeField(auto_now_add=True)),
('session', models.ForeignKey(to='decisiontree.Session')),
],
options={
'ordering': ['created'],
},
bases=(models.Model,),
),
]
22 changes: 22 additions & 0 deletions decisiontree/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ class Session(models.Model):
TreeState, blank=True, null=True,
help_text="None if the session is complete.")
num_tries = models.PositiveIntegerField(
default=0,
help_text="The number of times the user has tried to answer the "
"current question.")
# this flag stores the difference between completed
Expand Down Expand Up @@ -329,3 +330,24 @@ def save(self, **kwargs):
if not self.pk:
self.date_added = datetime.datetime.now()
super(TagNotification, self).save(**kwargs)


@python_2_unicode_compatible
class TranscriptMessage(models.Model):
INCOMING = 'I'
OUTGOING = 'O'
DIRECTION_CHOICES = (
(INCOMING, 'Incoming'),
(OUTGOING, 'Outgoing'),
)

session = models.ForeignKey('Session')
direction = models.CharField(max_length=1, choices=DIRECTION_CHOICES)
message = models.CharField(max_length=255)
created = models.DateTimeField(auto_now_add=True)

class Meta(object):
ordering = ['created']

def __str__(self):
return self.message
2 changes: 1 addition & 1 deletion decisiontree/multitenancy/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,5 @@ def save(self, *args, **kwargs):
obj = super(TenancyModelForm, self).save(*args, **kwargs)
if utils.multitenancy_enabled():
TenantLink = utils.get_link_class_from_model(obj._meta.model)
TenantLink.all_tenants.get_or_create(tenant=self.tenant, linked=obj)
TenantLink.objects.get_or_create(tenant=self.tenant, linked=obj)
return obj
29 changes: 29 additions & 0 deletions decisiontree/multitenancy/migrations/0002_transcriptmessagelink.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django.db import models, migrations
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('decisiontree', '0006_transcriptmessage'),
('multitenancy', '0003_auto_20141115_1029'),
('decisiontree_multitenancy', '0001_initial'),
]

operations = [
migrations.CreateModel(
name='TranscriptMessageLink',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('linked', models.OneToOneField(related_name='tenantlink', to='decisiontree.TranscriptMessage')),
('tenant', models.ForeignKey(on_delete=django.db.models.deletion.SET_NULL, default=None, to='multitenancy.Tenant', null=True)),
],
options={
'abstract': False,
},
bases=(models.Model,),
),
]
11 changes: 8 additions & 3 deletions decisiontree/multitenancy/models.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
from multitenancy.models import TenantEnabled

from django.db import models
from django.utils.encoding import python_2_unicode_compatible


@python_2_unicode_compatible
class TenantLink(TenantEnabled):
class TenantLink(models.Model):
tenant = models.ForeignKey(
'multitenancy.Tenant', null=True, default=None, on_delete=models.SET_NULL)
# Whether the linked object's relationship to a tenant is direct or
# derived. Derived relationships are handled by the signals in
# decisiontree/multitenancy/signals.py.
Expand Down Expand Up @@ -45,6 +45,11 @@ class TagNotificationLink(TenantLink):
linked = models.OneToOneField('decisiontree.TagNotification', related_name='tenantlink')


class TranscriptMessageLink(TenantLink):
direct = False
linked = models.OneToOneField('decisiontree.TranscriptMessage', related_name='tenantlink')


class TransitionLink(TenantLink):
linked = models.OneToOneField('decisiontree.Transition', related_name='tenantlink')

Expand Down
22 changes: 10 additions & 12 deletions decisiontree/multitenancy/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,27 +15,25 @@
def create_session_tenant_link(sender, instance, **kwargs):
"""Infer a tenant link from the associated connection."""
tenant_id = instance.connection.backend.tenantlink.tenant_id
link_class = utils.get_link_class_from_model(sender)
tenant_link, _ = link_class.all_tenants.get_or_create(linked=instance)
tenant_link.tenant_id = tenant_id
tenant_link.save()
utils.create_tenant_link(instance, tenant_id)


@receiver(post_save, sender=tree_models.Entry)
def create_entry_tenant_link(sender, instance, **kwargs):
"""Infer a tenant link from the associated session."""
tenant_id = instance.session.tenantlink.tenant_id
link_class = utils.get_link_class_from_model(sender)
tenant_link, _ = link_class.all_tenants.get_or_create(linked=instance)
tenant_link.tenant_id = tenant_id
tenant_link.save()
utils.create_tenant_link(instance, tenant_id)


@receiver(post_save, sender=tree_models.TagNotification)
def create_tag_notification_tenant_link(sender, instance, **kwargs):
"""Infer a tenant link from the associated tag."""
tenant_id = instance.tag.tenantlink.tenant_id
link_class = utils.get_link_class_from_model(sender)
tenant_link, _ = link_class.all_tenants.get_or_create(linked=instance)
tenant_link.tenant_id = tenant_id
tenant_link.save()
utils.create_tenant_link(instance, tenant_id)


@receiver(post_save, sender=tree_models.TranscriptMessage)
def create_transcript_message_tenant_link(sender, instance, **kwargs):
"""Infer a tenant link from the associated session."""
tenant_id = instance.session.tenantlink.tenant_id
utils.create_tenant_link(instance, tenant_id)
12 changes: 10 additions & 2 deletions decisiontree/multitenancy/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ def multitenancy_enabled():
return "decisiontree.multitenancy" in settings.INSTALLED_APPS


def create_tenant_link(instance, tenant_id):
"""Link the instance to a tenant."""
link_class = get_link_class_from_model(instance)
tenant_link, _ = link_class.objects.get_or_create(linked=instance)
tenant_link.tenant_id = tenant_id
tenant_link.save()


def get_tenants_for_user(user):
"""Return all tenants that the user can manage."""
from multitenancy.models import Tenant
Expand All @@ -27,8 +35,8 @@ def get_link_class_from_model(model):
related = getattr(link_field, 'related', None)
if related:
link_model = getattr(related, 'model', None)
from multitenancy.models import TenantEnabled
if link_model and issubclass(link_model, TenantEnabled):
from decisiontree.multitenancy.models import TenantLink
if link_model and issubclass(link_model, TenantLink):
return link_model
raise TypeError("This method should only be used on tenant-enabled models.")

Expand Down
18 changes: 18 additions & 0 deletions decisiontree/templates/tree/answers/list.html
Original file line number Diff line number Diff line change
@@ -1,12 +1,24 @@
{% extends "tree/cbv/list.html" %}

{% block extra_stylesheets %}
<style>
.color {
border: 1px solid black;
border-radius: 5px;
display: inline-block;
width: 30px;
}
</style>
{% endblock extra_stylesheets %}

{% load tree_tags %}

{% block list_table %}
<table class="table table-bordered table-condensed table-hover">
<thead>
<tr>
<th>ID</th>
<th>Color</th>
<th>Name</th>
<th>Type</th>
<th>Answer</th>
Expand All @@ -19,6 +31,12 @@
{% for answer in object_list %}
<tr>
<td>{{ answer.pk }}</td>
<td>
<span class="color" style="background-color: {{ answer.color }};">
&nbsp;
</span>
{{ answer.color }}
</td>
<td>{{ answer.name }}</td>
<td>{{ answer.get_type_display }}</td>
<td>{{ answer.answer }}</td>
Expand Down
Loading