Table of Contents
For every project I add to Grok My Code, I provide extra information on design decisions, code snippets, technologies used or anything of interest related to the development.
Tech used on this project:
Who Knows is a Django based question and answer site in the same vein as Stack Overflow or Quora.
The site provides the following functionality:
- Asking and answering questions.
- Commenting on questions and answers.
- Up voting questions, answers and comments.
- Accepting answers as being correct.
- Tagging questions along with filtering questions by tag.
- Filtering for questions with no answers, accepted answers etc.
- Text based searching.
- User profiles including statistics based on votes received for questions, answers and comments.
Source code and live demo:
If you're testing the functionality of the demo site, I suggest you signup as a user since only signed in users have access to most of the functionality. Alternatively, simply sign in using the following credentials:
2. Generic Relations Between Models
Typically foreign keys only point to one other model. Although this is the most common use case, there are situations where creating model relationships that are more generic (or polymorphic) is useful.
A good use case for this comes in the form of voting and commenting on the Who Knows site. We need the ability to comment on either questions or answers. Additionally, we need the ability to vote for questions, answers or comments.
Essentially, what we're looking for is to create Vote and Comment models that can be used generically to vote or comment on any other model that we specify. Django Generic Relations gives us exactly this behavior.
Generic Relations is a part of the content types framework which keeps track of all the models installed in a Django project and provides a generic interface for working with those models.
There are 3 required fields when creating a generic relation, namely:
- content_type: This is a ForeignKey to ContentType which is used to reference the related model id as specified by the content types framework. For example, since we can comment on questions or answers, this field will contain the content type id for the Question or Answer model.
- object_id: This stores the primary key of the model instance (ie: database record) you're commenting / voting on. In other words, it references the specific question or answer being commented / voted on.
- content_object: This creates a GenericForeignKey which accepts the above fields as arguments. They can either be passed in explicitly or left out if the above fields have been named 'content_type' and 'object_id' respectively.
Creating a Comment model in the form of a generic relation can be done as follows (note the 3 fields at the bottom):
Now that we have a generic Comment Model, we need to add it as a GenericRelation to whichever model we want to allow commenting on. Here's how you would add it to the Answer Model:
We've specified a comments as well as a votes GenericRelation along with their respective related_query_name fields for doing reverse lookups.
Adding the ability to comment or vote on any other models can be done by simply adding a GenericRelation to that model. For example, we could add commenting or voting to user profiles if we wanted to.
Generic relations can also be added to models which are themselves used as generic relations. For example, you can see from the first code snippet that we also added a votes generic relation to the Comment model. This allows us to vote on comments (or on any other model we add the votes generic relation to).
Generic relations come with most of the API functionality related to standard foreign keys as well as to the aggregation API. This allows for useful and intuitive querying, for example:
- Retrieve all the comments from the first question:
question = Question.objects.first() question.comments.all()
- Count how many votes the latest comment on the first answer has:
answer = Answer.objects.first() latest_comment = answer.comments.last() latest_comment.votes.count()
3. Combining OuterRef and Exists
A typical use case when implementing generic voting functionality is to limit a user to only having one vote. For example, in the Who Knows site, a user should only be able up vote a question, answer or comment once.
Using the Question model as an example, we need to retrieve a specific question from the database and add a flag to the queryset indicating whether the current user has already voted for the question.
To do this, we can combine the use of Django's OuterRef and Exists, both of which were added in version 1.11.
This is used when a queryset in a sub query needs to refer to a field from the outer query.
This uses an SQL EXISTS statement which means this part of the sub query stops evaluating as soon as a match is found.
- First we filter for questions which the current user has voted for. In the filter clause, we set up the OuterRef by specifying which field it refers to.
Next, we create a query that retrieves the associated user for a given question using the select_related clause. This ensures that only one resulting SQL query is generated.
We annotate this query with a voted flag which checks for the existence of the first query result.
- Lastly, we use the above query to obtain a specific question based on it's slug.
As always, thanks to Django's lazy queryset evaluation, only the final queryset is executed against the database and thanks to the use of selected_related combined with the sub query (ie: Exists and OuterRef), everything is done in one query.
The following code snippet demonstrates how to flag a specific question as having been voted for by the current user or not.
4. Django Tests and Initial Data
A TDD (Test Driven Development) approach was used for the most part while developing Who Knows. This meant that there was a need for test data. There are several options when it comes to creating test data, each of which is appropriate in different situations:
- Fixtures: Test data is created once for the whole test case (before SetUpTestData is run).
- Manual: Create test data as and when it is needed within each test.
- SetUp(self): Test data is created before each test is run.
- SetUpTestData(cls): Test data is created once for the whole test case.
Without going into the pros and cons of each option, I used a combination of the last 3.
The benefit of using SetUpTestData(cls) wherever possible is that there is a significant performance boost in terms of speed thanks to only creating test data once for each test case. SetUp(self) on the other hand re-creates the initial data for every test.
Since the initial test data I required was largely similar for all the apps within Who Knows, I created a mixin which could be used anywhere it was needed.
SetUpTestData is defined as a class method which then has access to create_test_data() since BaseTestMixins was added as a parent.