Bài 34: Comprehensive Project - News/Blog Website (Part 4)

Docker Setup

Dockerfile

# DockerfileFROM python:3.11-slim ENV PYTHONDONTWRITEBYTECODE=1ENV PYTHONUNBUFFERED=1 # Install system dependenciesRUN apt-get update && apt-get install -y \    postgresql-client \    && rm -rf /var/lib/apt/lists/* # Create app userRUN groupadd -r app && useradd -r -g app app WORKDIR /app # Install Python dependenciesCOPY requirements.txt .RUN pip install --no-cache-dir -r requirements.txt # Copy project filesCOPY --chown=app:app . . # Create necessary directoriesRUN mkdir -p media/articles media/profiles staticfiles # Collect static filesRUN python manage.py collectstatic --noinput USER app EXPOSE 8000 CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "3", "news_website.wsgi:application"]

docker-compose.yml

# docker-compose.ymlversion: '3.8' services:  db:    image: postgres:15-alpine    container_name: news_db    volumes:      - postgres_data:/var/lib/postgresql/data    environment:      POSTGRES_DB: news_db      POSTGRES_USER: news_user      POSTGRES_PASSWORD: news_password    restart: unless-stopped    healthcheck:      test: ["CMD-SHELL", "pg_isready -U news_user"]      interval: 10s      timeout: 5s      retries: 5    networks:      - backend   web:    build: .    container_name: news_web    command: gunicorn --bind 0.0.0.0:8000 --workers 3 news_website.wsgi:application    volumes:      - static_volume:/app/staticfiles      - media_volume:/app/media    environment:      DEBUG: "False"      SECRET_KEY: "your-secret-key-here-change-in-production"      DATABASE_URL: postgresql://news_user:news_password@db:5432/news_db      ALLOWED_HOSTS: "localhost,127.0.0.1"    depends_on:      db:        condition: service_healthy    restart: unless-stopped    networks:      - backend      - frontend   nginx:    image: nginx:alpine    container_name: news_nginx    ports:      - "80:80"    volumes:      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro      - static_volume:/app/staticfiles:ro      - media_volume:/app/media:ro    depends_on:      - web    restart: unless-stopped    networks:      - frontend volumes:  postgres_data:  static_volume:  media_volume: networks:  frontend:    driver: bridge  backend:    driver: bridge

Nginx Configuration

# nginx/nginx.confupstream django {    server web:8000;} server {    listen 80;    server_name localhost;        client_max_body_size 10M;        location / {        proxy_pass http://django;        proxy_set_header Host $host;        proxy_set_header X-Real-IP $remote_addr;        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;        proxy_set_header X-Forwarded-Proto $scheme;        proxy_redirect off;    }        location /static/ {        alias /app/staticfiles/;        expires 30d;        add_header Cache-Control "public, immutable";    }        location /media/ {        alias /app/media/;        expires 30d;    }}

.env Configuration

# .env.exampleDEBUG=FalseSECRET_KEY=your-secret-key-here-change-in-productionALLOWED_HOSTS=localhost,127.0.0.1,yourdomain.com # DatabaseDB_NAME=news_dbDB_USER=news_userDB_PASSWORD=news_passwordDB_HOST=dbDB_PORT=5432 # or use DATABASE_URLDATABASE_URL=postgresql://news_user:news_password@db:5432/news_db # Email (optional)EMAIL_HOST=smtp.gmail.comEMAIL_PORT=587[email protected]EMAIL_HOST_PASSWORD=your-app-passwordEMAIL_USE_TLS=True

.dockerignore

# .dockerignore*.pyc__pycache__/*.pyo*.pyd.Pythonenv/venv/.venv/ENV/db.sqlite3.env.git/.gitignore.vscode/.idea/*.logstaticfiles/media/*.md!README.md.DS_StoreThumbs.db

Production Settings

Settings Configuration

# news_website/settings.pyfrom pathlib import Pathfrom decouple import config, Csvimport dj_database_url BASE_DIR = Path(__file__).resolve().parent.parent # SecuritySECRET_KEY = config('SECRET_KEY')DEBUG = config('DEBUG', default=False, cast=bool)ALLOWED_HOSTS = config('ALLOWED_HOSTS', cast=Csv()) # Installed AppsINSTALLED_APPS = [    'django.contrib.admin',    'django.contrib.auth',    'django.contrib.contenttypes',    'django.contrib.sessions',    'django.contrib.messages',    'django.contrib.staticfiles',        # Third-party    'ckeditor',    'ckeditor_uploader',        # Local apps    'articles',    'accounts',    'pages',] MIDDLEWARE = [    'django.middleware.security.SecurityMiddleware',    'whitenoise.middleware.WhiteNoiseMiddleware',  # WhiteNoise for static files    'django.contrib.sessions.middleware.SessionMiddleware',    'django.middleware.common.CommonMiddleware',    'django.middleware.csrf.CsrfViewMiddleware',    'django.contrib.auth.middleware.AuthenticationMiddleware',    'django.contrib.messages.middleware.MessageMiddleware',    'django.middleware.clickjacking.XFrameOptionsMiddleware',] # DatabaseDATABASES = {    'default': dj_database_url.config(        default=config('DATABASE_URL'),        conn_max_age=600    )} # Static filesSTATIC_URL = '/static/'STATIC_ROOT = BASE_DIR / 'staticfiles'STATICFILES_DIRS = [BASE_DIR / 'static']STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage' # Media filesMEDIA_URL = '/media/'MEDIA_ROOT = BASE_DIR / 'media' # CKEditorCKEDITOR_UPLOAD_PATH = 'uploads/'CKEDITOR_CONFIGS = {    'default': {        'toolbar': 'full',        'height': 300,        'width': '100%',    },} # Security settings for productionif not DEBUG:    SECURE_SSL_REDIRECT = True    SESSION_COOKIE_SECURE = True    CSRF_COOKIE_SECURE = True    SECURE_HSTS_SECONDS = 31536000    SECURE_HSTS_INCLUDE_SUBDOMAINS = True    SECURE_HSTS_PRELOAD = True # AuthenticationLOGIN_URL = 'accounts:login'LOGIN_REDIRECT_URL = 'articles:article_list'LOGOUT_REDIRECT_URL = 'articles:article_list' # Messagesfrom django.contrib.messages import constants as messagesMESSAGE_TAGS = {    messages.DEBUG: 'debug',    messages.INFO: 'info',    messages.SUCCESS: 'success',    messages.WARNING: 'warning',    messages.ERROR: 'danger',} # Email configurationEMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'EMAIL_HOST = config('EMAIL_HOST', default='smtp.gmail.com')EMAIL_PORT = config('EMAIL_PORT', default=587, cast=int)EMAIL_USE_TLS = config('EMAIL_USE_TLS', default=True, cast=bool)EMAIL_HOST_USER = config('EMAIL_HOST_USER', default='')EMAIL_HOST_PASSWORD = config('EMAIL_HOST_PASSWORD', default='')

Testing

Model Tests

# articles/tests.pyfrom django.test import TestCasefrom django.contrib.auth.models import Userfrom .models import Category, Tag, Article, Comment class CategoryModelTest(TestCase):    def setUp(self):        self.category = Category.objects.create(name='Technology')        def test_category_creation(self):        self.assertEqual(self.category.name, 'Technology')        self.assertEqual(self.category.slug, 'technology')        def test_category_str(self):        self.assertEqual(str(self.category), 'Technology')        def test_category_absolute_url(self):        url = self.category.get_absolute_url()        self.assertEqual(url, f'/category/{self.category.slug}/') class ArticleModelTest(TestCase):    def setUp(self):        self.user = User.objects.create_user(            username='testuser',            password='testpass123'        )        self.category = Category.objects.create(name='Tech')        self.article = Article.objects.create(            title='Test Article',            author=self.user,            category=self.category,            excerpt='Test excerpt',            content='Test content',            status='published'        )        def test_article_creation(self):        self.assertEqual(self.article.title, 'Test Article')        self.assertEqual(self.article.author, self.user)        self.assertEqual(self.article.status, 'published')        def test_article_slug_generation(self):        self.assertEqual(self.article.slug, 'test-article')        def test_article_str(self):        self.assertEqual(str(self.article), 'Test Article')        def test_article_increment_views(self):        initial_views = self.article.views        self.article.increment_views()        self.assertEqual(self.article.views, initial_views + 1)        def test_article_comment_count(self):        Comment.objects.create(            article=self.article,            user=self.user,            content='Test comment'        )        self.assertEqual(self.article.comment_count, 1)

View Tests

# articles/tests.py (continued)from django.test import TestCase, Clientfrom django.urls import reverse class ArticleViewTest(TestCase):    def setUp(self):        self.client = Client()        self.user = User.objects.create_user(            username='testuser',            password='testpass123'        )        self.category = Category.objects.create(name='Tech')        self.article = Article.objects.create(            title='Test Article',            author=self.user,            category=self.category,            excerpt='Test excerpt',            content='Test content',            status='published'        )        def test_article_list_view(self):        response = self.client.get(reverse('articles:article_list'))        self.assertEqual(response.status_code, 200)        self.assertContains(response, 'Test Article')        self.assertTemplateUsed(response, 'articles/article_list.html')        def test_article_detail_view(self):        response = self.client.get(            reverse('articles:article_detail', kwargs={'slug': self.article.slug})        )        self.assertEqual(response.status_code, 200)        self.assertContains(response, 'Test Article')        self.assertTemplateUsed(response, 'articles/article_detail.html')        def test_article_create_view_login_required(self):        response = self.client.get(reverse('articles:article_create'))        self.assertEqual(response.status_code, 302)  # Redirect to login        def test_article_create_view_authenticated(self):        self.client.login(username='testuser', password='testpass123')        response = self.client.get(reverse('articles:article_create'))        self.assertEqual(response.status_code, 200)        self.assertTemplateUsed(response, 'articles/article_form.html')        def test_article_search(self):        response = self.client.get(reverse('articles:article_search') + '?q=Test')        self.assertEqual(response.status_code, 200)        self.assertContains(response, 'Test Article')

Run Tests

# Run all testspython manage.py test # Run specific app testspython manage.py test articles # Run with coveragepip install coveragecoverage run manage.py testcoverage reportcoverage html

Deployment Workflow

Initial Setup

# 1. Clone repository (or create project)git clone https://github.com/yourusername/news-website.gitcd news-website # 2. Create .env filecp .env.example .env# Edit .env with production values # 3. Build and start containersdocker-compose up -d --build # 4. Run migrationsdocker-compose exec web python manage.py migrate # 5. Create superuserdocker-compose exec web python manage.py createsuperuser # 6. Create default categoriesdocker-compose exec web python manage.py shell>>> from articles.models import Category>>> Category.objects.create(name='Technology')>>> Category.objects.create(name='Business')>>> Category.objects.create(name='Sports')>>> Category.objects.create(name='Entertainment')>>> exit() # 7. Access application# http://localhost# Admin: http://localhost/admin

Update Deployment

# 1. Pull latest codegit pull origin main # 2. Rebuild and restartdocker-compose up -d --build # 3. Run migrationsdocker-compose exec web python manage.py migrate # 4. Collect static filesdocker-compose exec web python manage.py collectstatic --noinput # 5. Check logsdocker-compose logs -f web

Database Backup

# Backup databasedocker-compose exec db pg_dump -U news_user news_db > backup_$(date +%Y%m%d).sql # Restore databasedocker-compose exec -T db psql -U news_user news_db < backup_20240101.sql # Automated backup script#!/bin/bash# backup.shBACKUP_DIR="./backups"mkdir -p $BACKUP_DIRDATE=$(date +%Y%m%d_%H%M%S)docker-compose exec -T db pg_dump -U news_user news_db > $BACKUP_DIR/backup_$DATE.sqlfind $BACKUP_DIR -name "backup_*.sql" -mtime +7 -deleteecho "Backup completed: $BACKUP_DIR/backup_$DATE.sql"

Complete Project Structure

news_website/├── news_website/│   ├── __init__.py│   ├── settings.py│   ├── urls.py│   └── wsgi.py├── articles/│   ├── migrations/│   ├── templates/│   │   └── articles/│   │       ├── article_list.html│   │       ├── article_detail.html│   │       ├── article_form.html│   │       ├── article_confirm_delete.html│   │       ├── category_detail.html│   │       ├── tag_detail.html│   │       ├── article_search.html│   │       └── user_articles.html│   ├── __init__.py│   ├── admin.py│   ├── apps.py│   ├── forms.py│   ├── models.py│   ├── tests.py│   ├── urls.py│   └── views.py├── accounts/│   ├── migrations/│   ├── templates/│   │   └── accounts/│   │       ├── login.html│   │       ├── register.html│   │       ├── profile.html│   │       ├── profile_edit.html│   │       ├── password_change.html│   │       └── password_change_done.html│   ├── __init__.py│   ├── admin.py│   ├── apps.py│   ├── forms.py│   ├── models.py│   ├── urls.py│   └── views.py├── pages/│   ├── templates/│   │   └── pages/│   │       ├── about.html│   │       └── contact.html│   ├── __init__.py│   ├── admin.py│   ├── apps.py│   ├── forms.py│   ├── models.py│   ├── urls.py│   └── views.py├── templates/│   ├── base.html│   └── includes/│       ├── navbar.html│       ├── footer.html│       └── sidebar.html├── static/│   ├── css/│   │   └── style.css│   ├── js/│   │   └── main.js│   └── images/│       └── logo.png├── media/│   ├── articles/│   └── profiles/├── nginx/│   └── nginx.conf├── manage.py├── requirements.txt├── Dockerfile├── docker-compose.yml├── .dockerignore├── .env.example├── .gitignore└── README.md

README.md

# News/Blog Website A full-featured news and blog website built with Django. ## Features - User registration and authentication- Article CRUD with rich text editor- Categories and tags- Comment system with nested replies- Search and filtering- Responsive design- Docker deployment- Admin dashboard ## Tech Stack - Django 5.0- PostgreSQL 15- Bootstrap 5- Docker & Docker Compose- Nginx- Gunicorn ## Installation ### Local Development 1. Clone repository:```bashgit clone https://github.com/yourusername/news-website.gitcd news-website
  1. Create virtual environment:
python -m venv venvsource venv/bin/activate  # On Windows: venv\Scripts\activate
  1. Install dependencies:
pip install -r requirements.txt
  1. Configure environment:
cp .env.example .env# Edit .env with your settings
  1. Run migrations:
python manage.py migrate
  1. Create superuser:
python manage.py createsuperuser
  1. Run development server:
python manage.py runserver

Docker Deployment

  1. Clone repository:
git clone https://github.com/yourusername/news-website.gitcd news-website
  1. Configure environment:
cp .env.example .env# Edit .env with production values
  1. Build and start:
docker-compose up -d --build
  1. Run migrations:
docker-compose exec web python manage.py migrate
  1. Create superuser:
docker-compose exec web python manage.py createsuperuser
  1. Access application:

Usage

Creating Articles

  1. Login to your account
  2. Click "Create Article" in user menu
  3. Fill in article details
  4. Select category and tags
  5. Upload featured image
  6. Choose status (draft/published)
  7. Click "Save"

Managing Content

Access admin panel at /admin to:

  • Manage users and profiles
  • Moderate comments
  • Manage categories and tags
  • View contact messages

Testing

Run tests:

python manage.py test

With coverage:

coverage run manage.py testcoverage report

Deployment

See DEPLOYMENT.md for production deployment guide.

License

MIT License

Contributing

Pull requests are welcome!

 ## Final Checklist ### Development Checklist

✅ Project Setup
✅ Create Django project
✅ Create apps (articles, accounts, pages)
✅ Install dependencies
✅ Configure settings

✅ Models
✅ UserProfile model
✅ Category model
✅ Tag model
✅ Article model
✅ Comment model
✅ ContactMessage model

✅ Views
✅ Article CRUD views
✅ Category/Tag views
✅ Search view
✅ Comment views
✅ Account views
✅ Profile views

✅ Templates
✅ Base template
✅ Article templates
✅ Account templates
✅ Page templates
✅ Responsive design

✅ Forms
✅ Article form
✅ Comment form
✅ Registration form
✅ Profile form
✅ Contact form

✅ Admin
✅ Custom admin for all models
✅ Inline editing
✅ Custom actions
✅ Search and filters

✅ Features
✅ User authentication
✅ Rich text editor
✅ Image upload
✅ Search functionality
✅ Pagination
✅ Comments with replies
✅ View counter
✅ SEO-friendly URLs

✅ Testing
✅ Model tests
✅ View tests
✅ Form tests
✅ Coverage report

✅ Docker
✅ Dockerfile
✅ docker-compose.yml
✅ Nginx configuration
✅ Production settings

✅ Documentation
✅ README.md
✅ .env.example
✅ Deployment guide

 ## Bài Tập ### Exercise 1: Complete Project **Task:** Build complete news/blog website: ```python# Requirements:# 1. All models implemented# 2. All views with proper permissions# 3. All templates with Bootstrap# 4. Admin customization# 5. Docker setup# 6. Testing coverage > 80%# 7. Documentation# 8. Deploy locally with Docker# 9. Create sample content# 10. Test all features

Exercise 2: Advanced Features

Task: Add advanced features to project:

# Requirements:# 1. Article bookmarking# 2. User following system# 3. Email notifications# 4. Social sharing buttons# 5. Reading list# 6. Article recommendations# 7. Advanced search filters# 8. Export articles to PDF# 9. RSS feed# 10. API endpoints (optional)

Exercise 3: Deploy to Production

Task: Deploy to real hosting:

# Requirements:# 1. Choose hosting platform (DigitalOcean/AWS/Railway)# 2. Setup domain and SSL# 3. Configure production database# 4. Setup email service# 5. Configure media storage# 6. Setup backups# 7. Configure monitoring# 8. Load testing# 9. Security audit# 10. Documentation

Tài Liệu Tham Khảo


Previous: Bài 34.3: Templates and Admin | Next: Bài 35: Practice Project - E-commerce