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: bridgeNginx 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.dbProduction 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 htmlDeployment 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/adminUpdate 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 webDatabase 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.mdREADME.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- Create virtual environment:
python -m venv venvsource venv/bin/activate # On Windows: venv\Scripts\activate- Install dependencies:
pip install -r requirements.txt- Configure environment:
cp .env.example .env# Edit .env with your settings- Run migrations:
python manage.py migrate- Create superuser:
python manage.py createsuperuser- Run development server:
python manage.py runserverDocker Deployment
- Clone repository:
git clone https://github.com/yourusername/news-website.gitcd news-website- Configure environment:
cp .env.example .env# Edit .env with production values- Build and start:
docker-compose up -d --build- Run migrations:
docker-compose exec web python manage.py migrate- Create superuser:
docker-compose exec web python manage.py createsuperuser- Access application:
- Website: http://localhost
- Admin: http://localhost/admin
Usage
Creating Articles
- Login to your account
- Click "Create Article" in user menu
- Fill in article details
- Select category and tags
- Upload featured image
- Choose status (draft/published)
- 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 testWith coverage:
coverage run manage.py testcoverage reportDeployment
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 featuresExercise 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. DocumentationTài Liệu Tham Khảo
- Django Documentation
- Django Best Practices
- Docker Documentation
- PostgreSQL Documentation
- Nginx Documentation
Previous: Bài 34.3: Templates and Admin | Next: Bài 35: Practice Project - E-commerce