I have a Django app that uses an Abstract Base Class ('Answer') and creates different Answers depending on the answer_type required by the Question objects. (This project started life as the Polls tutorial). Question is now:
class Question(models.Model):
ANSWER_TYPE_CHOICES = (
('CH', 'Choice'),
('SA', 'Short Answer'),
('LA', 'Long Answer'),
('E3', 'Expert Judgement of Probabilities'),
('E4', 'Expert Judgment of Values'),
('BS', 'Brainstorms'),
('FB', 'Feedback'),
)
answer_type = models.CharField(max_length=2,
choices=ANSWER_TYPE_CHOICES,
default='SA')
question_text = models.CharField(max_length=200, default="enter a question here")
And Answer is:
class Answer(models.Model):
"""
Answer is an abstract base class which ensures that question and user are
always defined for every answer
"""
question = models.ForeignKey(Question, on_delete=models.CASCADE)
user = models.ForeignKey(User, on_delete=models.CASCADE, default=1)
class Meta:
abstract = True
ordering = ['user']
At the moment, I have a single method in Answer (overwriting get_or_update_answer()) with type-specific instructions to look in the right table and collect or create the right type of object.
#classmethod
def get_or_update_answer(self, user, question, submitted_value={}, pk_ans=None):
"""
this replaces get_or_update_answer with appropriate handling for all
different Answer types. This allows the views answer and page_view to get
or create answer objects for every question type calling this function.
"""
if question.answer_type == 'CH':
if not submitted_value:
# by default, select the top of a set of radio buttons
selected_choice = question.choice_set.first()
answer, _created = Vote.objects.get_or_create(
user=user,
question=question,
defaults={'choice': selected_choice})
else:
selected_choice = question.choice_set.get(pk=submitted_value)
answer = Vote.objects.get(user=user, question=question)
answer.choice = selected_choice
elif question.answer_type == 'SA':
if not submitted_value:
submitted_value = ""
answer, _created = Short_Answer.objects.get_or_create(
user=user,
question=question,
defaults={'short_answer': submitted_value})
else:
answer = Short_Answer.objects.get(
user=user,
question=question)
answer.short_answer = hashtag_cleaner(submitted_value['short_answer'])
etc... etc... (similar handling for five more types)
By putting all this logic in 'models.py', I can load user answers for a page_view for any number of questions with:
for question in page_question_list:
answers[question] = Answer.get_or_update_answer(user, question, submitted_value, pk_ans)
I believe there is a more Pythonic way to design this code - something that I haven't learned to use, but I'm not sure what. Something like interfaces, so that each object type can implement its own version of Answer.get_or_update_answer(), and Python will use the version appropriate for the object. This would make extending 'models.py' a lot neater.
I've revisited this problem recently, replaced one or two hundred lines of code with five or ten, and thought it might one day be useful to someone to find what I did here.
There are several elements to the problem I had - first, many answer types to be created, saved and retrieved when required; second, the GET vs POST dichotomy (and my idiosyncratic solution of always creating an answer, sending it to a form); third, some of the types have different logic (the Brainstorm can have multiple answers per user, the FeedBack does not even need a response - if it is created for a user, it has been presented.) These elements probably obscured some opportunity to remove repetition, which make the visitor pattern quite appropriate.
Solution for elements 1 & 2
A dictionary of question.answer_type codes that map to the relevant Answer sub-class, is created in views.py (because its hard to place it in models.py and resolve dependencies):
# views.py:
ANSWER_CLASS_DICT = {
'CH': Vote,
'SA': Short_Answer,
'LA': Long_Answer,
'E3': EJ_three_field,
'E4': EJ_four_field,
'BS': Brainstorm,
'FB': FB,}
Then I can get the class of Answer that I want 'get_or_created' for any question with:
ANSWER_CLASS_DICT[question.answer_type]
I pass it as a parameter to the class method:
# models.py:
def get_or_update_answer(self, user, question, Cls, submitted_value=None, pk_ans=None):
if not submitted_value:
answer, _created = Cls.objects.get_or_create(user=user, question=question)
elif isinstance(submitted_value, dict):
answer, _created = Cls.objects.get_or_create(user=user, question=question)
for key, value in submitted_value.items():
setattr(answer, key, value)
else:
pass
So the same six lines of code handles get_or_creating any Answer when submitted_value=None (GET) or not (submitted_value).
Solution for element 3
The solution for element three has been to extend the model to separate at least three types of handling for users revisiting the same question:
'S' - single, which allows them to record only one answer, revisit and amend the answer, but never to give two different answers.
'T' - tracked, which allows them to update their answer every time, but makes the history of what their answer was available (e.g. to researchers.)
'M' - multiple, which allows many answers to be submitted to a question.
Still bug-fixing after all these changes, so I won't post code.
Next feature: compound questions and question templates, so people can use the admin to screen to make their own answer types.
Based on what you've shown, you're most of the way to reimplementing the Visitor pattern, which is a pretty standard way of handling this sort of situation (you have a bunch of related subclasses, each needing its own handling logic, and want to iterate over instances of them and do something with each).
I'd suggest taking a look at how that pattern works, and perhaps implementing it more explicitly.
Related
Lets say I have a Form model:
class Form(models.Model):
name = models.TextField()
date = models.DateField()
and various "child" models
class FormA(models.Model):
form = models.OneToOneField(Form, on_delete=models.CASCADE)
property_a = models.TextField()
class FormB(models.Model):
form = models.OneToOneField(Form, on_delete=models.CASCADE)
property_b = models.IntegerField()
class FormC(models.Model):
form = models.OneToOneField(Form, on_delete=models.CASCADE)
property_c = models.BooleanField()
a Form can be one AND ONLY ONE of 3 types of forms (FormA, FormB, FormC). Given a Query Set of Form, is there any way I can recover what types of Form (A, B or C) they are?
I would need to get a better understanding of your actual use case to know whether this is a good option for you or not, but in these situations, I would first suggest using model inheritance instead of a one to one field. The code you have there is basically doing what multi-table inheritance already does.
Take a read through the inheritance docs real quick first and make sure that multi-table inheritance makes sense for you as compared to the other options provided by django. If you do wish to continue with multi-table inheritance, I would suggest taking a look at InheritanceManager from django-module-utils.
At this point (if using InheritanceManager), you would be able to use isinstance.
for form in Form.objects.select_subclasses():
if isinstance(form, FormA):
..... do stuff ......
This might sound like a lot of extra effort but IMO it would reduce the moving parts (and custom code) and make things easier to deal with while still handling the functionality you need.
You can check it by name or isinstance.
a = FormA()
print(a.__class__)
print(a.__class__.__name__)
print(isinstance(a, Forma))
outputs:
<class __main__.FormA at 0xsomeaddress>
'FormA'
True
------------------- EDIT -----------------
Ok based on your comment, you just want to know which instance is assigned to your main Form.
So you can do something like this:
if hasattr(form, 'forma'):
# do something
elif hasattr(form, 'formb'):
# do something else
elif hasattr(form, 'formb'):
# do something else
After investigating a bit I came up with this
for form in forms:
#reduces fields to those of OneToOne types
one_to_ones = [field for field in form._meta.get_fields() if field.one_to_one]
for o in one_to_ones:
if hasattr(form,o.name):
#do something
Might have some drawbacks (maybe bad runtime) but serves its purpose for now.
Ideas to improve this are appreciated
Suppose I have the following models, where Questions and Choices have a many-to-many relationship. (To understand it better, consider a poll where each Question can have multiple Choices and each Choice can be associated to multiple Questions.)
class Question(models.Model):
question_text = models.CharField(max_length=200)
choices = models.ManyToManyField('Choice')
class Choice(models.Model):
choice_text = models.CharField(max_length=200)
Now suppose I have a QuerySet consisting of Choice objects, call it universe_choices. I want to filter all Question objects, to get only those Questions whose choices have at least one element in common with universe_choices. In other words, if at least one of a Question's choices is also in universe_choices, include that Question in the QuerySet returned from my filter.
Ideally, I would do this with something equivalent to:
Question.objects.filter(choices__intersection__exists=universe_choices)
or
Question.objects.filter(choices.intersection(universe_choices).exists())
But obviously neither the intersection() nor exists() methods exist in lookup-form, and you can't use them as-is in a filtering query.
Is there a way to do this?
The inefficient work-around of course is to loop through all Question objects, and check whether there is an intersection between each iteration's Question.choices object and universe_choices.
You should be able to get it very simply with this:
Question.objects.filter(choices__in=universe_choices)
See https://docs.djangoproject.com/en/2.2/ref/models/querysets/#in (The documentation doesn't seem to describe or give examples of doing it when the thing on the left hand side of the __in is multi-valued, but I've definitely used it for an identical use case and had it work as desired.)
I have a model in Django like follows:
class A(models.Model):
STATUS_DEFAULT = "default"
STATUS_ACCEPTED = "accepted"
STATUS_REJECTED = "rejected"
STATUS_CHOICES = (
(STATUS_DEFAULT, 'Just Asked'),
(STATUS_ACCEPTED, 'Accepted'),
(STATUS_REJECTED, 'Rejected'),
)
status = models.CharField(choices=STATUS_CHOICES, max_length=20, default=STATUS_DEFAULT)
question = models.ForeignKey(Question)
Notice that Question is another model in my project. I have a constraint on the A model. Between rows with the same question only one of them can has status=STATUS_ACCEPTED and at the first all of them have status=STATUS_DEFAULT. I want to write a function that does the following :
def accept(self):
self.status = STATUS_ACCEPTED
self.save()
A.objects.filter(question=self.question).update(status=STATUS_REJECTED)
But if two instances of A with same question call this function maybe a race condition will happen. So the one who calls this function sooner should lock other instances with same question to prevent race condition.
How should I do this?
Assuming you are using a DB backend that supports locks, you can lock the question using select_for_update
You code could then look like:
#transaction.atomic
def accept(self):
# Lock related question so no other instance will run the following code at the same time.
Question.objects.filter(pk=self.question.pk).select_for_update()
# now we have the lock, reload to make sure we have not been updated meanwhile
self.refresh_from_db()
if self.status != STATUS_REJECTED:
A.objects.filter(question=self.question).exclude(pk=self.pk).update()
self.status = STATUS_ACCEPTED
self.save()
else:
raise Exception('An answer has already been accepted !')
With that code, only one instance at a time will be able to run the code after select_for_update (for a given question).
Note the refresh_from_db call as while waiting to acquire the lock, another instance may have accepted another answer...
As I understand it, you want to make sure that two instances of A which share a Question cannot both simultaneously have the 'accepted' status. A objects are initiated at the default status.
Perhaps you should rethink your approach:
Let the question itself tell you which A has the accepted status.
Solution:
add the following to your Question model:
accepted_a = models.OneToOneField(A, null = true, default = null)
since you seem to want the accept method to be part of the A class, you can write your accept the way you have it laid out in your question. I disagree though, I think the behaviour of the Question is that the Question accepts the A, so the method should be defined in Question class.
def accept(self,A):
self.accepted_a = A
now, in your views, when you want the A to get accepted, you would write:
q = Question.objects.get(Question_id)
a = A.objects.get(A_id)
q.accept(A)
q.save()
How this works:
Django (and databases in general) provides a mechanism by which a relationship can specify One-to-One relationships. By using that in the Question model, we specify that each question can have exactly one accepted A. This does not override or alter the behaviour of the Many-to-One relationship the Question has with A.
Our accept is a bit naive though, it doesn't look to see if the question is a foreign key to A. We chan change that (or any other logic you wish):
Edit: With information provided in comments, we need to ensure that the first Ask (A) To accept the question locks it out. To that end, we will check if the question already has an acceptor Ask. Since a question defaults to null, we can simply test if it is null.
def accept(self, A):
if (A.question == self) and (self.accepted_a==null):
self.accepted_a = A
return True
else:
return False
I'm implementing likes on profiles for my website and I'm not sure which would be the best practice, a ManyToManyField like so:
class MyUser(AbstractBaseUser):
...
likes = models.ManyToManyField('self', symmetrical = False, null = True)
...
or just creating a class Like, like so:
class Like(models.Model):
liker = models.ForeignKey(MyUser, related_name='liker')
liked = models.ForeignKey(MyUser, related_name='liked')
Is one of them a better choice than the other? If so, why?
thanks
The first option should be preffered. If you need some additional fields to describe the likes, you can still use through="Likes" in your ManyToManyField and define the model Likes.
Manipulating the data entries would be also somewhat more pythonic:
# returns an object collection
likes_for_me = MyUser.objects.filter(pk=1).likes
instead of:
me = MyUser.objects.filter(pk=1)
likes_for_me = Like.objects.filter(liked=me)
The second option is basically what is done internally: a new table is created, which is used to create the links between the entities.
For the first option, you let django do the job for you.
The choice is certainly more about how you want to do the requests. On the second options, you would have to query the Like models that match you model, while on the first one, you only have to request the MyUser, from which you can access the connections.
Second option is more flexible and extensible. For example, you'll probably want to track when like was created (just add Like.date_created field). Also you'll probably want to send notification to content author when content was liked. But at first like only (add Like.cancelled boolead field and wrap it with some logic...).
So I'll go with separate model.
I think the one you choose totally depends on the one you find easier to implement or better. I tend to always use the first approach, as it is more straightforward and logical, at least to me. I also disagree with Igor on that it's not flexible and extensible, you can also initiate notifications when it happens. If you are going to use the Django rest framework, then I totally suggest using the first method, as the second could be a pain.
class Post(models.Model):
like = models.ManyToManyField(settings.AUTH_USER_MODEL, blank=True, related_name='post_like')
Then in your view, you just do this.
#api_view(['GET'])
#permission_classes([IsAuthenticated])
def like(request, id):
signed_in = request.user
post = Post.objects.get(id=id)
if signed_in and post:
post.like.add(signed_in)
# For unlike, remove instead of add
return Response("Successful")
else:
return Response("Unsuccessful", status.HTTP_404_NOT_FOUND)
Then you can use the response however you like on the front end.
I have the following models:
class Question(models.Model):
question = models.CharField(max_length=100)
detail = models.TextField(null=True, blank=True)
class PossibleAnswers(models.Model):
question = models.ForeignKey(Question)
value = models.CharField(max_length=200)
class Answer(models.Model):
question = models.ForeignKey(Question)
value = models.CharField(max_length=200)
Each Question has PossibleAnswers defined defined by the User. For Example: Question - What is the best fruit? Possible Answers - Apple, Orange, Grapes. Now other user's can Answer the question with their responses restricted to PossibleAnswers.
The problem I'm having is getting a count of each Answer. How many people responded by selecting Apple vs Orange vs Grapes?
Question.answer_set.filter(value="Grapes").count() returns a count of all grape answers, but what if you don't know what the filter criteria (grapes in this case) will be? Since the user defines the answer options, and defines how many different options there are, how would you get a response count for each answer option?
First I would change your models. Your schema is not normalized. That means you keep the same information (the text of an answer) in multiple places. That is considered a bad thing by itself, but also makes designing the right query much harder. That is how I think your models should look like:
class Question(models.Model):
question = models.CharField(max_length=100)
detail = models.TextField(null=True, blank=True)
class PossibleAnswer(models.Model):
question = models.ForeignKey(Question)
value = models.CharField(max_length=200)
class Answer(models.Model):
possible_answer = models.ForeignKey(PossibleAnswers)
Every time a user vote for a possible answer you add a new Answer model referencing that possible answer. You can also add additional fields to the Answer model, like a Foreign Key referencing the user voting.
Then you would use the following code to get the information (i.e. how many actual answers there are for every possible answer) for a question foo:
PossibleAnswer.objects.filter(question=foo).annotate(Count('answer'))
You need an aggregation query:
from django.db.models import Count
Question.objects.annotate(Count('answer'))
Does this help you out?
q = Question.objects.get(pk=1)
[{v: q.answer_set.filter(value=v).count()}
for v in [pa.value for
pa in q.possibleanswers_set.all()]]
It will return a dictionary of {'possible_answer': 'count'} for each possible answer that exists for Question object q.
This is however not the most efficient way of doing things if you intend to use it on a larger scale (say for all Question objects), in which case you'd be much better off using aggregation (read about it here).