I have a website where users are directed to go through a sequence of pages to perform a sequence of work tasks (transcribe a paragraph, answer a survey, interact with another user, etc). For short, let’s call these tasks A, B, C, etc. I’m using Django, and each task is currently implemented as a view function (or method), e.g. def A(request)
. Currently, each view function has the next step hardcoded in an HTTP redirect, e.g. HttpResponseRedirect(reverse(B))
.
My challenge is that I will soon have a requirement to create various permutations of the sequence of tasks. Sometimes I will want users to do A, B, C; for others, B, C, D; for others, A, B, D, F. How can I architect this so that I can define these permutations elegantly and concisely in my code? Intuitively, I feel like I need to genericize the HTTP redirect calls with a variable, like: HttpResponseRedirect(reverse(next_view_function))
, but I struggle to figure out where next_view_function
should be defined, and how the definition of the sequence (e.g. A, B, D) should be persisted across requests.
Can anyone lead me down the right path with a suggestion or idea? I’m happy to provide more details.
Edit in response to Bart’s questions:
- The order of tasks is predetermined. Once we have defined it, we send the users a URL that takes them to the first page. They should then be routed through the tasks. One group of users might get a URL that takes them through steps A,B,C, while another group of users gets a URL that takes them through steps A,B,D. We decide in advance which users should get which URL.
- Since the order is predetermined, the next step does not depend on the results of the current task. The only requirement is for them to complete the task (we of course validate the POST data they submit as a precondition to the HTTP redirect).
Edit 2, my solution so far:
A central concept in my code architecture is that of a “Treatment” (defined in my models.py). A Treatment is a specification of the experience a user has when they use my site, such as what UI is presented to them. I split my users into groups and assign each group to a different treatment by emailing them a URL that contains the PK of their treatment.
(There are also different subclasses of Treatment, since some Treatments are totally different from others. SoccerTreatment and HockeyTreatment have little in common, so it makes sense to subclass them.)
So, the class definition for a Treatment seems like the sensible place to store the sequence of views as a Python list, as follows:
# in soccer/models.py
class SoccerTreatment(BaseTreatment):
world_cup = models.BooleanField()
....
def views(self, request):
seq = [soccer.views.ViewClassA]
if self.world_cup:
seq.append(soccer.views.ViewClassWorldCup)
return seq
# in shared/models.py
class BaseTreatment(models.Model):
def next_view(self, request):
seq = self.views() # views() method implemented by descendant classes
if request.session.get('current_view_index'):
request.session['current_view_index'] += 1
else:
request.session['current_view_index'] = 0
# [handle boundary cases, code snipped...]
# ...
return seq[request.session['current_view_index']]
# ...
# in views.py, at the end of a view function
# ...
HttpResponseRedirect(reverse(user.treatment.next_view()))
However, the design above wouldn’t work, since then models.py would have to import views.py, creating a circular import. So the list elements would have to be strings (either URLs, or a the name of the view class as a string), which could be brittle since renaming my classes or URLs won’t automatically rename these strings.
Maybe I am being too picky, but I wonder if there is a better design.
I’ve only skimmed the requirements, but I’d be thinking something simple like this:
- Give each task a letter.
- Generate a task sequence string (eg, EBDFA) for a user.
- Look at the first letter to decide what task to do next.
- Carry out that task.
- Strip the first letter off, pass the rest in the URL.
- Go to step 3.
Keep it simple. Don’t create models you don’t need, don’t store local state you don’t need.
2
You’ll have to group your users according to the flow assignments. For each usergroup you have sequence of pages. The data model will then be something like
User --> UserGroup <-- Sequences
+id +id +user_group_id
+name +current_step
+user_group_id +next_step
On each single page you know the user’s id and the current step, so you can easily retrieve the next step for the current situation.
There are a few questions that will affect the design here:
- Do you already have information that will determine the set/order of tasks, or will the order effectively be random? In the latter case, you could generate the task list when you create a new session and pass it along in the session data, along with an indicator where in the list the user is.
- If you already have information on the set/order of tasks, in how far does this information consist of the answers/result of the current task? If there is a strong influence of the current task in selecting the next, you might be better off with a
next_view_function
per task. Otherwise, you might have one is a base class that all task controllers inherit from.
Update
In response to the update: You can either encode the sequence in the URL you give out (base64 encoded if you don’t want it to be too obvious), or you can store the information in the session, like with the random order, except that the initial URL drives the sequence generation, instead it being random.
2
I came across a situation like this in an Application I helped create.
I called it the User Sanity Test, because I was testing the User model and the possible states it could be in with regards to its relations to other models.
Things I would test were, Had they set an email address, Did they have valid billing details, have they created their first Thing(tm), etc.
I approached this by creating a Middleware that performed these tests on authenticated users, which took the UserSanityTest class and feeds in the current User.
If a test fails, then it returns an url that the middleware will redirect the user to.
The Sanity check is in effect on all urls except those in the SanityCheck.
If the user is visiting a SanityCheck Url that they’ve already satisfied then it either sends them to the users dashboard or checks for other SanityChecks and processes them.
Normal URLS described in your UrlConf can only be visited when all SanityChecks are completed.