Project Overview
Throughout this week, we've explored Django's powerful features for building web applications and APIs. Now it's time to put everything together in a comprehensive weekend project that will solidify your understanding and give you a complete application to showcase in your portfolio.
In this project, you'll build a Knowledge Sharing Platform called "DevExchange" where developers can share resources, ask questions, and collaborate. You'll create both a traditional web interface using Django templates and a modern REST API using Django REST Framework.
The Architectural Blueprint Analogy
Think of this weekend project as building a house. You're not just hammering nails randomly – you need a clear blueprint and a systematic approach:
- The foundation is your data models and database design
- The framing is your URL patterns and views
- The electrical and plumbing systems are your business logic
- The exterior walls are your traditional web interface
- The modern interior is your REST API
- The quality inspections are your tests
Just as no one would build a house without a plan, we'll approach this project systematically using George Polya's problem-solving framework.
George Polya's Problem-Solving Procedure
George Polya, a renowned mathematician, developed a four-step approach to problem-solving that's applicable to any complex task, including software development. We'll use this framework to guide our weekend project:
- Understand the Problem: Define project requirements, clarify the problem domain, and identify constraints
- Devise a Plan: Design the solution architecture, create models, and plan the implementation
- Execute the Plan: Implement the solution in stages, testing as you go
- Look Back and Reflect: Review the solution, optimize, and consider extensions
This structured approach will help ensure we build a robust, well-designed application. Let's apply this framework to our DevExchange project.
Step 1: Understand the Problem
Before writing any code, we need to clearly understand what we're building. Let's define the requirements and constraints for our DevExchange platform.
Core Functionality Requirements
- User Management: Registration, login, profile management
- Resource Sharing: Users can post links to articles, tutorials, etc.
- Q&A System: Users can ask questions and post answers
- Tagging: Resources and questions can be tagged and filtered
- Voting: Users can upvote or downvote content
- Comments: Users can comment on resources and answers
- Search: Users can search for resources and questions
Interface Requirements
- Web Interface: Traditional Django template-based views
- REST API: Complete API for all functionality
- API Documentation: Browsable API with clear documentation
Technical Constraints
- Must use Django and Django REST Framework
- Must follow RESTful design principles
- Must implement proper authentication and permissions
- Must validate input data thoroughly
- Must handle errors gracefully
- Must be appropriately tested
Key Questions to Consider
Polya emphasizes asking questions to understand the problem deeply. For our project:
- Who are the users and what are their goals?
- What types of resources will be shared?
- How will we organize the data?
- How will permissions work?
- What are the most important features for MVP?
Step 2: Devise a Plan
Now that we understand the requirements, let's design our solution. This includes data modeling, URL planning, and creating a development roadmap.
Data Model Design
URL Structure Planning
Let's plan our URL structure for both the web interface and the API:
| Web URL | API Endpoint | Description |
|---|---|---|
| /accounts/... | /api/auth/... | User authentication URLs |
| /resources/ | /api/resources/ | List and create resources |
| /resources/<id>/ | /api/resources/<id>/ | Resource detail view |
| /questions/ | /api/questions/ | List and create questions |
| /questions/<id>/ | /api/questions/<id>/ | Question detail view |
| /tags/<slug>/ | /api/tags/<slug>/ | Filter by tag |
| /users/<username>/ | /api/users/<username>/ | User profile |
| /search/ | /api/search/ | Search functionality |
Implementation Roadmap
Breaking down the project into manageable steps:
- Set up project structure and environment
- Create data models and migrations
- Implement user authentication
- Build resource sharing functionality
- Build Q&A functionality
- Implement tagging system
- Add voting and commenting
- Implement search functionality
- Create API serializers and views
- Add API authentication and permissions
- Implement documentation and testing
- Final testing and deployment
Technical Approach
For this project, we'll use:
- Django's built-in authentication system
- Class-based views for the web interface
- ViewSets and Routers for the API
- ModelSerializers for data transformation
- Django's generic relations for votes and comments
- Django's forms and DRF's serializers for validation
Step 3: Execute the Plan
Now we'll implement our solution in stages, following the roadmap. Let's break this down into practical implementation steps.
Project Setup
# Create project and environment
mkdir devexchange
cd devexchange
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
# Install dependencies
pip install django djangorestframework django-filter markdown pygments
# Start project
django-admin startproject devexchange .
django-admin startapp core
django-admin startapp resources
django-admin startapp questions
django-admin startapp api
# Update settings.py
# Add to INSTALLED_APPS:
# 'rest_framework',
# 'django_filters',
# 'core',
# 'resources',
# 'questions',
# 'api',
Core Models Implementation
# core/models.py
from django.db import models
from django.contrib.auth.models import User
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.utils.text import slugify
class Profile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
bio = models.TextField(blank=True)
avatar = models.ImageField(upload_to='avatars/', blank=True)
website = models.URLField(blank=True)
github_username = models.CharField(max_length=50, blank=True)
def __str__(self):
return f"{self.user.username}'s profile"
class Tag(models.Model):
name = models.CharField(max_length=50, unique=True)
slug = models.SlugField(max_length=50, unique=True)
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.name)
super().save(*args, **kwargs)
def __str__(self):
return self.name
class Vote(models.Model):
VOTE_CHOICES = (
(1, 'Upvote'),
(-1, 'Downvote'),
)
user = models.ForeignKey(User, on_delete=models.CASCADE)
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey('content_type', 'object_id')
value = models.SmallIntegerField(choices=VOTE_CHOICES)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
unique_together = ('user', 'content_type', 'object_id')
def __str__(self):
return f"{self.user.username}'s {self.get_value_display()} on {self.content_object}"
class Comment(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey('content_type', 'object_id')
body = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f"Comment by {self.user.username} on {self.content_object}"
Resource Sharing Implementation
# resources/models.py
from django.db import models
from django.contrib.auth.models import User
from django.contrib.contenttypes.fields import GenericRelation
from core.models import Tag, Vote, Comment
class Resource(models.Model):
title = models.CharField(max_length=200)
url = models.URLField()
description = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='resources')
tags = models.ManyToManyField(Tag, related_name='resources')
votes = GenericRelation(Vote)
comments = GenericRelation(Comment)
def __str__(self):
return self.title
@property
def vote_score(self):
return self.votes.aggregate(models.Sum('value'))['value__sum'] or 0
Q&A Implementation
# questions/models.py
from django.db import models
from django.contrib.auth.models import User
from django.contrib.contenttypes.fields import GenericRelation
from core.models import Tag, Vote, Comment
class Question(models.Model):
title = models.CharField(max_length=300)
body = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='questions')
tags = models.ManyToManyField(Tag, related_name='questions')
votes = GenericRelation(Vote)
comments = GenericRelation(Comment)
def __str__(self):
return self.title
@property
def vote_score(self):
return self.votes.aggregate(models.Sum('value'))['value__sum'] or 0
class Answer(models.Model):
question = models.ForeignKey(Question, on_delete=models.CASCADE, related_name='answers')
body = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='answers')
is_accepted = models.BooleanField(default=False)
votes = GenericRelation(Vote)
comments = GenericRelation(Comment)
def __str__(self):
return f"Answer to '{self.question.title}' by {self.author.username}"
@property
def vote_score(self):
return self.votes.aggregate(models.Sum('value'))['value__sum'] or 0
API Serializers
# api/serializers.py
from rest_framework import serializers
from django.contrib.auth.models import User
from core.models import Profile, Tag, Vote, Comment
from resources.models import Resource
from questions.models import Question, Answer
from django.contrib.contenttypes.models import ContentType
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['id', 'username', 'email', 'date_joined']
class ProfileSerializer(serializers.ModelSerializer):
username = serializers.CharField(source='user.username', read_only=True)
class Meta:
model = Profile
fields = ['id', 'username', 'bio', 'avatar', 'website', 'github_username']
class TagSerializer(serializers.ModelSerializer):
class Meta:
model = Tag
fields = ['id', 'name', 'slug']
class CommentSerializer(serializers.ModelSerializer):
author = UserSerializer(read_only=True)
class Meta:
model = Comment
fields = ['id', 'author', 'body', 'created_at']
read_only_fields = ['created_at']
class VoteSerializer(serializers.ModelSerializer):
class Meta:
model = Vote
fields = ['id', 'value', 'created_at']
read_only_fields = ['created_at']
class ResourceSerializer(serializers.ModelSerializer):
author = UserSerializer(read_only=True)
tags = TagSerializer(many=True, read_only=True)
tag_ids = serializers.PrimaryKeyRelatedField(
queryset=Tag.objects.all(),
write_only=True,
many=True,
source='tags',
required=False
)
vote_score = serializers.IntegerField(read_only=True)
comments_count = serializers.SerializerMethodField()
class Meta:
model = Resource
fields = [
'id', 'title', 'url', 'description',
'created_at', 'updated_at', 'author',
'tags', 'tag_ids', 'vote_score', 'comments_count'
]
read_only_fields = ['created_at', 'updated_at']
def get_comments_count(self, obj):
return obj.comments.count()
def create(self, validated_data):
tags_data = validated_data.pop('tags', [])
resource = Resource.objects.create(**validated_data)
for tag in tags_data:
resource.tags.add(tag)
return resource
class AnswerSerializer(serializers.ModelSerializer):
author = UserSerializer(read_only=True)
vote_score = serializers.IntegerField(read_only=True)
comments = CommentSerializer(many=True, read_only=True)
class Meta:
model = Answer
fields = [
'id', 'question', 'body', 'created_at',
'updated_at', 'author', 'is_accepted',
'vote_score', 'comments'
]
read_only_fields = ['created_at', 'updated_at', 'is_accepted']
class QuestionSerializer(serializers.ModelSerializer):
author = UserSerializer(read_only=True)
tags = TagSerializer(many=True, read_only=True)
tag_ids = serializers.PrimaryKeyRelatedField(
queryset=Tag.objects.all(),
write_only=True,
many=True,
source='tags',
required=False
)
answers = AnswerSerializer(many=True, read_only=True)
vote_score = serializers.IntegerField(read_only=True)
comments = CommentSerializer(many=True, read_only=True)
class Meta:
model = Question
fields = [
'id', 'title', 'body', 'created_at',
'updated_at', 'author', 'tags', 'tag_ids',
'vote_score', 'answers', 'comments'
]
read_only_fields = ['created_at', 'updated_at']
def create(self, validated_data):
tags_data = validated_data.pop('tags', [])
question = Question.objects.create(**validated_data)
for tag in tags_data:
question.tags.add(tag)
return question
API ViewSets
# api/views.py
from rest_framework import viewsets, permissions, filters, status
from rest_framework.decorators import action
from rest_framework.response import Response
from django_filters.rest_framework import DjangoFilterBackend
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from core.models import Profile, Tag, Vote, Comment
from resources.models import Resource
from questions.models import Question, Answer
from .serializers import (
UserSerializer, ProfileSerializer, TagSerializer,
ResourceSerializer, QuestionSerializer, AnswerSerializer,
CommentSerializer, VoteSerializer
)
from .permissions import IsAuthorOrReadOnly
class UserViewSet(viewsets.ReadOnlyModelViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer
lookup_field = 'username'
class ProfileViewSet(viewsets.ModelViewSet):
queryset = Profile.objects.all()
serializer_class = ProfileSerializer
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
user = self.request.user
return Profile.objects.filter(user=user)
class TagViewSet(viewsets.ReadOnlyModelViewSet):
queryset = Tag.objects.all()
serializer_class = TagSerializer
lookup_field = 'slug'
class ResourceViewSet(viewsets.ModelViewSet):
queryset = Resource.objects.all()
serializer_class = ResourceSerializer
permission_classes = [permissions.IsAuthenticatedOrReadOnly, IsAuthorOrReadOnly]
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
filterset_fields = ['author__username', 'tags__slug']
search_fields = ['title', 'description']
ordering_fields = ['created_at', 'title']
ordering = ['-created_at']
def perform_create(self, serializer):
serializer.save(author=self.request.user)
@action(detail=True, methods=['post'])
def vote(self, request, pk=None):
resource = self.get_object()
user = request.user
# Check if user has already voted
content_type = ContentType.objects.get_for_model(Resource)
vote = Vote.objects.filter(
user=user,
content_type=content_type,
object_id=resource.id
).first()
value = request.data.get('value')
if value not in [1, -1]:
return Response(
{'detail': 'Invalid vote value. Must be 1 or -1.'},
status=status.HTTP_400_BAD_REQUEST
)
if vote:
# Update existing vote
vote.value = value
vote.save()
else:
# Create new vote
vote = Vote.objects.create(
user=user,
content_type=content_type,
object_id=resource.id,
value=value
)
serializer = VoteSerializer(vote)
return Response(serializer.data)
@action(detail=True, methods=['post'])
def comment(self, request, pk=None):
resource = self.get_object()
user = request.user
serializer = CommentSerializer(data=request.data)
if serializer.is_valid():
comment = Comment.objects.create(
user=user,
content_type=ContentType.objects.get_for_model(Resource),
object_id=resource.id,
body=serializer.validated_data['body']
)
return Response(CommentSerializer(comment).data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class QuestionViewSet(viewsets.ModelViewSet):
queryset = Question.objects.all()
serializer_class = QuestionSerializer
permission_classes = [permissions.IsAuthenticatedOrReadOnly, IsAuthorOrReadOnly]
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
filterset_fields = ['author__username', 'tags__slug']
search_fields = ['title', 'body']
ordering_fields = ['created_at', 'title']
ordering = ['-created_at']
def perform_create(self, serializer):
serializer.save(author=self.request.user)
@action(detail=True, methods=['post'])
def vote(self, request, pk=None):
question = self.get_object()
user = request.user
# Check if user has already voted
content_type = ContentType.objects.get_for_model(Question)
vote = Vote.objects.filter(
user=user,
content_type=content_type,
object_id=question.id
).first()
value = request.data.get('value')
if value not in [1, -1]:
return Response(
{'detail': 'Invalid vote value. Must be 1 or -1.'},
status=status.HTTP_400_BAD_REQUEST
)
if vote:
# Update existing vote
vote.value = value
vote.save()
else:
# Create new vote
vote = Vote.objects.create(
user=user,
content_type=content_type,
object_id=question.id,
value=value
)
serializer = VoteSerializer(vote)
return Response(serializer.data)
@action(detail=True, methods=['post'])
def comment(self, request, pk=None):
question = self.get_object()
user = request.user
serializer = CommentSerializer(data=request.data)
if serializer.is_valid():
comment = Comment.objects.create(
user=user,
content_type=ContentType.objects.get_for_model(Question),
object_id=question.id,
body=serializer.validated_data['body']
)
return Response(CommentSerializer(comment).data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@action(detail=True, methods=['post'])
def answer(self, request, pk=None):
question = self.get_object()
user = request.user
serializer = AnswerSerializer(data=request.data)
if serializer.is_valid():
answer = Answer.objects.create(
question=question,
author=user,
body=serializer.validated_data['body']
)
return Response(AnswerSerializer(answer).data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class AnswerViewSet(viewsets.ModelViewSet):
queryset = Answer.objects.all()
serializer_class = AnswerSerializer
permission_classes = [permissions.IsAuthenticatedOrReadOnly, IsAuthorOrReadOnly]
def perform_create(self, serializer):
serializer.save(author=self.request.user)
@action(detail=True, methods=['post'])
def vote(self, request, pk=None):
answer = self.get_object()
user = request.user
# Check if user has already voted
content_type = ContentType.objects.get_for_model(Answer)
vote = Vote.objects.filter(
user=user,
content_type=content_type,
object_id=answer.id
).first()
value = request.data.get('value')
if value not in [1, -1]:
return Response(
{'detail': 'Invalid vote value. Must be 1 or -1.'},
status=status.HTTP_400_BAD_REQUEST
)
if vote:
# Update existing vote
vote.value = value
vote.save()
else:
# Create new vote
vote = Vote.objects.create(
user=user,
content_type=content_type,
object_id=answer.id,
value=value
)
serializer = VoteSerializer(vote)
return Response(serializer.data)
@action(detail=True, methods=['post'])
def comment(self, request, pk=None):
answer = self.get_object()
user = request.user
serializer = CommentSerializer(data=request.data)
if serializer.is_valid():
comment = Comment.objects.create(
user=user,
content_type=ContentType.objects.get_for_model(Answer),
object_id=answer.id,
body=serializer.validated_data['body']
)
return Response(CommentSerializer(comment).data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@action(detail=True, methods=['post'], permission_classes=[permissions.IsAuthenticated])
def accept(self, request, pk=None):
answer = self.get_object()
question = answer.question
# Only the question author can accept an answer
if request.user != question.author:
return Response(
{'detail': 'Only the question author can accept an answer.'},
status=status.HTTP_403_FORBIDDEN
)
# Clear any previously accepted answers
question.answers.filter(is_accepted=True).update(is_accepted=False)
# Accept this answer
answer.is_accepted = True
answer.save()
serializer = self.get_serializer(answer)
return Response(serializer.data)
API Routes
# api/urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from . import views
router = DefaultRouter()
router.register(r'users', views.UserViewSet)
router.register(r'profiles', views.ProfileViewSet)
router.register(r'tags', views.TagViewSet)
router.register(r'resources', views.ResourceViewSet)
router.register(r'questions', views.QuestionViewSet)
router.register(r'answers', views.AnswerViewSet)
urlpatterns = [
path('', include(router.urls)),
path('auth/', include('rest_framework.urls')),
]
# main urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include('api.urls')),
# Web interface URLs...
]
Web Interface Views (Sample)
# resources/views.py
from django.views.generic import ListView, DetailView, CreateView, UpdateView, DeleteView
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.urls import reverse_lazy
from .models import Resource
from .forms import ResourceForm
class ResourceListView(ListView):
model = Resource
template_name = 'resources/resource_list.html'
context_object_name = 'resources'
paginate_by = 10
def get_queryset(self):
queryset = super().get_queryset()
tag = self.request.GET.get('tag')
if tag:
queryset = queryset.filter(tags__slug=tag)
return queryset
class ResourceDetailView(DetailView):
model = Resource
template_name = 'resources/resource_detail.html'
class ResourceCreateView(LoginRequiredMixin, CreateView):
model = Resource
form_class = ResourceForm
template_name = 'resources/resource_form.html'
success_url = reverse_lazy('resource-list')
def form_valid(self, form):
form.instance.author = self.request.user
return super().form_valid(form)
class ResourceUpdateView(LoginRequiredMixin, UserPassesTestMixin, UpdateView):
model = Resource
form_class = ResourceForm
template_name = 'resources/resource_form.html'
def test_func(self):
resource = self.get_object()
return self.request.user == resource.author
def get_success_url(self):
return reverse_lazy('resource-detail', kwargs={'pk': self.object.pk})
class ResourceDeleteView(LoginRequiredMixin, UserPassesTestMixin, DeleteView):
model = Resource
template_name = 'resources/resource_confirm_delete.html'
success_url = reverse_lazy('resource-list')
def test_func(self):
resource = self.get_object()
return self.request.user == resource.author
Templates (Sample)
{% extends 'base.html' %}
{% block content %}
Dev Resources
{% if request.user.is_authenticated %}
Share a Resource
{% endif %}
{% for resource in resources %}
{{ resource.title }}
{{ resource.url }}
{{ resource.description|truncatewords:50 }}
{% for tag in resource.tags.all %}
{{ tag.name }}
{% endfor %}
Shared by {{ resource.author.username }} on {{ resource.created_at|date:"M d, Y" }}
Vote score: {{ resource.vote_score }}
Comments: {{ resource.comments.count }}
{% empty %}
No resources found.
{% endfor %}
{% if is_paginated %}
{% endif %}
Popular Tags
{% for tag in popular_tags %}
{{ tag.name }} ({{ tag.count }})
{% endfor %}
{% endblock %}
Integration and Testing
As we implement each component, we should test it thoroughly. Here's a sample test file for our API:
# api/tests.py
from django.test import TestCase
from django.urls import reverse
from rest_framework.test import APIClient
from rest_framework import status
from django.contrib.auth.models import User
from resources.models import Resource
from core.models import Tag
class ResourceAPITest(TestCase):
def setUp(self):
self.client = APIClient()
# Create users
self.user1 = User.objects.create_user(
username='testuser1',
email='test1@example.com',
password='password123'
)
self.user2 = User.objects.create_user(
username='testuser2',
email='test2@example.com',
password='password123'
)
# Create tags
self.tag1 = Tag.objects.create(name='Python', slug='python')
self.tag2 = Tag.objects.create(name='Django', slug='django')
# Create resources
self.resource1 = Resource.objects.create(
title='Test Resource 1',
url='https://example.com/resource1',
description='This is test resource 1',
author=self.user1
)
self.resource1.tags.add(self.tag1)
self.resource2 = Resource.objects.create(
title='Test Resource 2',
url='https://example.com/resource2',
description='This is test resource 2',
author=self.user2
)
self.resource2.tags.add(self.tag2)
def test_get_resources_list(self):
"""Test retrieving a list of resources"""
url = reverse('resource-list')
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data['results']), 2)
def test_get_resource_detail(self):
"""Test retrieving a specific resource"""
url = reverse('resource-detail', args=[self.resource1.id])
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['title'], 'Test Resource 1')
def test_create_resource_unauthenticated(self):
"""Test creating a resource without authentication"""
url = reverse('resource-list')
data = {
'title': 'New Resource',
'url': 'https://example.com/new',
'description': 'This is a new resource',
'tag_ids': [self.tag1.id]
}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
def test_create_resource_authenticated(self):
"""Test creating a resource with authentication"""
self.client.login(username='testuser1', password='password123')
url = reverse('resource-list')
data = {
'title': 'New Resource',
'url': 'https://example.com/new',
'description': 'This is a new resource',
'tag_ids': [self.tag1.id]
}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(Resource.objects.count(), 3)
def test_update_own_resource(self):
"""Test updating own resource"""
self.client.login(username='testuser1', password='password123')
url = reverse('resource-detail', args=[self.resource1.id])
data = {
'title': 'Updated Resource',
'url': 'https://example.com/updated',
'description': 'This is an updated resource',
'tag_ids': [self.tag2.id]
}
response = self.client.put(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.resource1.refresh_from_db()
self.assertEqual(self.resource1.title, 'Updated Resource')
def test_update_others_resource(self):
"""Test updating someone else's resource"""
self.client.login(username='testuser1', password='password123')
url = reverse('resource-detail', args=[self.resource2.id])
data = {
'title': 'Updated Resource',
'url': 'https://example.com/updated',
'description': 'This is an updated resource',
'tag_ids': [self.tag2.id]
}
response = self.client.put(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
def test_filter_by_tag(self):
"""Test filtering resources by tag"""
url = reverse('resource-list') + f'?tags__slug={self.tag1.slug}'
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data['results']), 1)
self.assertEqual(response.data['results'][0]['title'], 'Test Resource 1')
Step 4: Look Back and Reflect
After implementing the project, it's important to review our work, identify improvements, and consider extensions. This step aligns with Polya's final phase of problem-solving.
Code Review Checklist
- Are the models properly structured?
- Is the API RESTful and consistent?
- Are permissions properly implemented?
- Is validation thorough?
- Is the code DRY (Don't Repeat Yourself)?
- Are the UI templates responsive and user-friendly?
- Is error handling comprehensive?
- Are the tests thorough and do they pass?
Performance Optimization
Once the basic functionality is working, consider these optimizations:
- Add caching for frequently accessed data
- Optimize database queries using select_related and prefetch_related
- Implement pagination for all list views
- Use Django Debug Toolbar to identify bottlenecks
Potential Extensions
To further enhance the project, consider these extensions:
- Social login with OAuth
- Email notifications for answers to questions
- Markdown support for content
- User reputation system based on votes
- Tag-based recommendation system
- Admin dashboard with analytics
- Full-text search using Elasticsearch
- WebSockets for real-time updates
Deployment Considerations
For deployment, consider:
- Setting up a production-ready database (PostgreSQL)
- Configuring static and media file serving
- Setting proper security settings (HTTPS, CSRF, etc.)
- Implementing a CI/CD pipeline
- Setting up monitoring and logging
Project Submission
Your completed project should include:
- Full source code with all implemented features
- Documentation for the API (can be the browsable API or a separate document)
- Test suite with good coverage
- README.md with setup instructions and project overview
- A reflection document discussing challenges faced and solutions implemented
By following George Polya's four-step approach, you've built a comprehensive web application that demonstrates your understanding of Django and Django REST Framework. This project showcases your ability to:
- Design and implement complex data models
- Create RESTful APIs with proper resources and relationships
- Implement authentication and authorization
- Build user-friendly web interfaces
- Test and document your code
- Approach problems systematically
These skills are highly valuable in real-world software development, and this project will serve as an excellent addition to your portfolio.
Challenge Extensions
For an extra challenge, try implementing some of these features:
- API Versioning: Implement versioning for your API to allow for future changes without breaking existing clients.
- Rate Limiting: Add rate limiting to protect your API from abuse.
- JWT Authentication: Implement JSON Web Token authentication for the API.
- API Documentation: Add Swagger/OpenAPI documentation for better API discoverability.
- Frontend Integration: Create a simple React or Vue.js frontend that consumes your API.