Weekend Project: Build a Complete Django Web Application with REST API

Module 18: Python Backend - Django

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:

graph TD A[1. Understand the Problem] --> B[2. Devise a Plan] B --> C[3. Execute the Plan] C --> D[4. Look Back and Reflect] D --> A
  1. Understand the Problem: Define project requirements, clarify the problem domain, and identify constraints
  2. Devise a Plan: Design the solution architecture, create models, and plan the implementation
  3. Execute the Plan: Implement the solution in stages, testing as you go
  4. 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

Interface Requirements

Technical Constraints

Key Questions to Consider

Polya emphasizes asking questions to understand the problem deeply. For our project:

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

classDiagram User "1" -- "*" Profile User "1" -- "*" Resource User "1" -- "*" Question User "1" -- "*" Answer User "1" -- "*" Comment User "1" -- "*" Vote Resource "*" -- "*" Tag Question "*" -- "*" Tag Resource "1" -- "*" Comment Question "1" -- "*" Answer Answer "1" -- "*" Comment Resource "1" -- "*" Vote Question "1" -- "*" Vote Answer "1" -- "*" Vote class User { username email password } class Profile { user bio avatar website github_username } class Resource { title url description created_at author tags } class Question { title body created_at updated_at author tags } class Answer { question body created_at updated_at author is_accepted } class Comment { content_type object_id body created_at author } class Vote { content_type object_id user value } class Tag { name slug }

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:

  1. Set up project structure and environment
  2. Create data models and migrations
  3. Implement user authentication
  4. Build resource sharing functionality
  5. Build Q&A functionality
  6. Implement tagging system
  7. Add voting and commenting
  8. Implement search functionality
  9. Create API serializers and views
  10. Add API authentication and permissions
  11. Implement documentation and testing
  12. Final testing and deployment

Technical Approach

For this project, we'll use:

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

Performance Optimization

Once the basic functionality is working, consider these optimizations:

Potential Extensions

To further enhance the project, consider these extensions:

Deployment Considerations

For deployment, consider:

Project Submission

Your completed project should include:

  1. Full source code with all implemented features
  2. Documentation for the API (can be the browsable API or a separate document)
  3. Test suite with good coverage
  4. README.md with setup instructions and project overview
  5. 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:

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:

  1. API Versioning: Implement versioning for your API to allow for future changes without breaking existing clients.
  2. Rate Limiting: Add rate limiting to protect your API from abuse.
  3. JWT Authentication: Implement JSON Web Token authentication for the API.
  4. API Documentation: Add Swagger/OpenAPI documentation for better API discoverability.
  5. Frontend Integration: Create a simple React or Vue.js frontend that consumes your API.