diff --git a/z1/CT-Assignment1 - Documentation.pdf b/z1/CT-Assignment1 - Documentation.pdf new file mode 100644 index 0000000..55f9a79 Binary files /dev/null and b/z1/CT-Assignment1 - Documentation.pdf differ diff --git a/z1/Dockerfile b/z1/Dockerfile new file mode 100644 index 0000000..2637cdc --- /dev/null +++ b/z1/Dockerfile @@ -0,0 +1,25 @@ +FROM python:3.11-slim + +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 + +WORKDIR /app + +RUN apt-get update && apt-get install -y --no-install-recommends \ + gcc \ + libpq-dev \ + && rm -rf /var/lib/apt/lists/* + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +RUN DJANGO_SETTINGS_MODULE=diary_app.settings_build python manage.py collectstatic --noinput + +COPY entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +EXPOSE 8000 + +ENTRYPOINT ["/entrypoint.sh"] diff --git a/z1/diary/__init__.py b/z1/diary/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/z1/diary/admin.py b/z1/diary/admin.py new file mode 100644 index 0000000..ae25540 --- /dev/null +++ b/z1/diary/admin.py @@ -0,0 +1,8 @@ +from django.contrib import admin +from .models import Entry + +@admin.register(Entry) +class EntryAdmin(admin.ModelAdmin): + list_display = ('title', 'user', 'mood', 'created_at') + list_filter = ('mood', 'user') + search_fields = ('title', 'content') diff --git a/z1/diary/models.py b/z1/diary/models.py new file mode 100644 index 0000000..b4fbe86 --- /dev/null +++ b/z1/diary/models.py @@ -0,0 +1,27 @@ +from django.db import models +from django.contrib.auth.models import User + +MOOD_CHOICES = [ + ('happy', '😊 Happy'), + ('sad', '😒 Sad'), + ('anxious', '😰 Anxious'), + ('calm', '😌 Calm'), + ('excited', '🀩 Excited'), + ('angry', '😠 Angry'), + ('grateful', 'πŸ™ Grateful'), + ('tired', '😴 Tired'), +] + +class Entry(models.Model): + user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='entries') + title = models.CharField(max_length=200) + content = models.TextField() + mood = models.CharField(max_length=20, choices=MOOD_CHOICES, default='calm') + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + ordering = ['-created_at'] + + def __str__(self): + return f"{self.title} ({self.created_at.strftime('%Y-%m-%d')})" diff --git a/z1/diary/urls.py b/z1/diary/urls.py new file mode 100644 index 0000000..08e76fa --- /dev/null +++ b/z1/diary/urls.py @@ -0,0 +1,11 @@ +from django.urls import path +from . import views + +urlpatterns = [ + path('', views.entry_list, name='entry_list'), + path('entry/new/', views.entry_create, name='entry_create'), + path('entry//', views.entry_detail, name='entry_detail'), + path('entry//edit/', views.entry_edit, name='entry_edit'), + path('entry//del/', views.entry_delete, name='entry_delete'), + path('register/', views.register, name='register'), +] diff --git a/z1/diary/views.py b/z1/diary/views.py new file mode 100644 index 0000000..c2158d9 --- /dev/null +++ b/z1/diary/views.py @@ -0,0 +1,86 @@ +from django.shortcuts import render, get_object_or_404, redirect +from django.contrib.auth.decorators import login_required +from django.contrib.auth import login +from django.contrib.auth.forms import UserCreationForm +from django.contrib import messages +from django.db.models import Q +from .models import Entry + +@login_required +def entry_list(request): + query = request.GET.get('q', '') + mood = request.GET.get('mood', '') + entries = Entry.objects.filter(user=request.user) + if query: + entries = entries.filter(Q(title__icontains=query) | Q(content__icontains=query)) + if mood: + entries = entries.filter(mood=mood) + return render(request, 'diary/entry_list.html', { + 'entries': entries, + 'query': query, + 'mood_filter': mood, + 'mood_choices': Entry.MOOD_CHOICES, + }) + +@login_required +def entry_detail(request, pk): + entry = get_object_or_404(Entry, pk=pk, user=request.user) + return render(request, 'diary/entry_detail.html', {'entry': entry}) + +@login_required +def entry_create(request): + if request.method == 'POST': + title = request.POST.get('title', '').strip() + content = request.POST.get('content', '').strip() + mood = request.POST.get('mood', 'calm') + if title and content: + Entry.objects.create(user=request.user, title=title, content=content, mood=mood) + messages.success(request, 'Entry created!') + return redirect('entry_list') + messages.error(request, 'Title and content are required.') + return render(request, 'diary/entry_form.html', { + 'mood_choices': Entry.MOOD_CHOICES, + 'action': 'Create', + }) + +@login_required +def entry_edit(request, pk): + entry = get_object_or_404(Entry, pk=pk, user=request.user) + if request.method == 'POST': + title = request.POST.get('title', '').strip() + content = request.POST.get('content', '').strip() + mood = request.POST.get('mood', 'calm') + if title and content: + entry.title = title + entry.content = content + entry.mood = mood + entry.save() + messages.success(request, 'Entry updated!') + return redirect('entry_detail', pk=pk) + messages.error(request, 'Title and content are required.') + return render(request, 'diary/entry_form.html', { + 'entry': entry, + 'mood_choices': Entry.MOOD_CHOICES, + 'action': 'Edit', + }) + +@login_required +def entry_delete(request, pk): + entry = get_object_or_404(Entry, pk=pk, user=request.user) + if request.method == 'POST': + entry.delete() + messages.success(request, 'Entry deleted.') + return redirect('entry_list') + return render(request, 'diary/entry_confirm_delete.html', {'entry': entry}) + +def register(request): + if request.method == 'POST': + form = UserCreationForm(request.POST) + if form.is_valid(): + user = form.save() + login(request, user) + messages.success(request, f'Welcome, {user.username}!') + return redirect('entry_list') + else: + form = UserCreationForm() + return render(request, 'diary/register.html', {'form': form}) diff --git a/z1/diary_app/__init__.py b/z1/diary_app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/z1/diary_app/settings.py b/z1/diary_app/settings.py new file mode 100644 index 0000000..3bd1ee5 --- /dev/null +++ b/z1/diary_app/settings.py @@ -0,0 +1,72 @@ +import os +from pathlib import Path + +BASE_DIR = Path(__file__).resolve().parent + +SECRET_KEY = os.environ.get('SECRET_KEY', 'dev-secret-key-change-in-production') +DEBUG = os.environ.get('DEBUG', 'True') == 'True' +ALLOWED_HOSTS = ['*'] + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'diary', +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + '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', +] + +ROOT_URLCONF = 'diary_app.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [BASE_DIR / 'templates'], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql', + 'NAME': os.environ.get('POSTGRES_DB', 'diarydb'), + 'USER': os.environ.get('POSTGRES_USER', 'diaryuser'), + 'PASSWORD': os.environ.get('POSTGRES_PASSWORD', 'diarypass'), + 'HOST': os.environ.get('DB_HOST', 'db'), + 'PORT': os.environ.get('DB_PORT', '5432'), + } +} + +LANGUAGE_CODE = 'en-us' +TIME_ZONE = 'UTC' +USE_I18N = True +USE_TZ = True + +STATIC_URL = '/static/' +STATIC_ROOT = BASE_DIR / 'staticfiles' +STATICFILES_DIRS = [BASE_DIR / 'static'] + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +LOGIN_URL = '/login/' +LOGIN_REDIRECT_URL = '/' +LOGOUT_REDIRECT_URL = '/login/' diff --git a/z1/diary_app/settings_build.py b/z1/diary_app/settings_build.py new file mode 100644 index 0000000..cb8ed61 --- /dev/null +++ b/z1/diary_app/settings_build.py @@ -0,0 +1,4 @@ +from diary_app.settings import * + +DATABASES = {} +STATICFILES_DIRS = [BASE_DIR / 'static'] diff --git a/z1/diary_app/static/diary/css/style.css b/z1/diary_app/static/diary/css/style.css new file mode 100644 index 0000000..15a65f8 --- /dev/null +++ b/z1/diary_app/static/diary/css/style.css @@ -0,0 +1,193 @@ +@import url('https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=DM+Sans:wght@300;400;500&display=swap'); + +*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } + +:root { + --bg: #f5f0e8; + --surface: #fdfaf4; + --border: #ddd5c0; + --text: #1a1814; + --muted: #8c8070; + --accent: #c84b2f; + --accent-h: #a83a20; + --danger: #c0392b; + --radius: 6px; + --shadow: 0 1px 6px rgba(0,0,0,.08); +} + +body { + font-family: 'DM Sans', sans-serif; + background: var(--bg); + color: var(--text); + line-height: 1.65; + font-size: 16px; + font-weight: 300; +} + +h1, h2, h3 { + font-family: 'Instrument Serif', serif; + font-weight: 400; +} + +a { color: var(--accent); text-decoration: none; } +a:hover { text-decoration: underline; } + +/* ---- Navbar ---- */ +.navbar { + background: var(--surface); + border-bottom: 1px solid var(--border); + padding: 0 2.5rem; + height: 58px; + display: flex; + align-items: center; + justify-content: space-between; + position: sticky; + top: 0; + z-index: 100; +} +.nav-brand { + font-family: 'Instrument Serif', serif; + font-size: 1.35rem; + color: var(--text); + letter-spacing: 0.01em; +} +.nav-brand:hover { text-decoration: none; } +.nav-links { display: flex; align-items: center; gap: .75rem; } +.nav-user { font-size: .85rem; color: var(--muted); font-weight: 300; } + +/* ---- Container ---- */ +.container { max-width: 880px; margin: 0 auto; padding: 2.5rem 2rem; } + +/* ---- Buttons ---- */ +.btn { + display: inline-flex; align-items: center; justify-content: center; + padding: .5rem 1.25rem; + border-radius: var(--radius); + font-family: 'DM Sans', sans-serif; + font-size: .875rem; + font-weight: 400; + cursor: pointer; + border: 1px solid transparent; + transition: all .15s; + text-decoration: none; + letter-spacing: 0.02em; +} +.btn-primary { background: var(--accent); color: #fff; border-color: var(--accent); } +.btn-primary:hover { background: var(--accent-h); border-color: var(--accent-h); text-decoration: none; } +.btn-ghost { background: transparent; color: var(--text); border-color: var(--border); } +.btn-ghost:hover { background: var(--bg); text-decoration: none; } +.btn-danger { background: transparent; color: var(--danger); border-color: var(--danger); } +.btn-danger:hover { background: var(--danger); color: #fff; text-decoration: none; } +.btn-sm { padding: .35rem .9rem; font-size: .82rem; } +.btn-full { width: 100%; } + +/* ---- Alerts ---- */ +.alert { padding: .75rem 1.1rem; border-radius: var(--radius); margin-bottom: 1.25rem; font-size: .9rem; border-left: 3px solid; } +.alert-success { background: #f0f7f0; border-color: #5a9e6f; color: #2d6a40; } +.alert-error { background: #fdf0ee; border-color: var(--accent); color: var(--accent-h); } + +/* ---- Page header ---- */ +.page-header { margin-bottom: 2rem; border-bottom: 1px solid var(--border); padding-bottom: 1.25rem; } +.page-header h1 { font-size: 2.2rem; line-height: 1.2; } +.subtitle { color: var(--muted); font-size: .88rem; margin-top: .35rem; font-weight: 300; } + +/* ---- Search bar ---- */ +.search-form { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: 2rem; } +.input { + padding: .55rem .9rem; + border: 1px solid var(--border); + border-radius: var(--radius); + font-size: .9rem; + color: var(--text); + background: var(--surface); + outline: none; + transition: border-color .15s; + font-family: 'DM Sans', sans-serif; + font-weight: 300; +} +.input:focus { border-color: var(--accent); } +.search-form .input:first-child { flex: 1; min-width: 180px; } +.select-input { cursor: pointer; } + +/* ---- Entries grid ---- */ +.entries-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(290px, 1fr)); gap: 1.25rem; } +.entry-card { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 1.5rem; + display: block; + transition: box-shadow .15s, transform .15s; + color: var(--text); +} +.entry-card:hover { + box-shadow: var(--shadow); + transform: translateY(-2px); + text-decoration: none; +} +.entry-card-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: .75rem; } +.mood-badge { + font-size: .75rem; + padding: .2rem .65rem; + background: var(--bg); + border: 1px solid var(--border); + border-radius: 20px; + color: var(--muted); + font-weight: 400; + letter-spacing: 0.03em; +} +.mood-large { font-size: .88rem; padding: .3rem .85rem; } +.entry-date { font-size: .75rem; color: var(--muted); font-weight: 300; } +.entry-title { + font-family: 'Instrument Serif', serif; + font-size: 1.2rem; + font-weight: 400; + margin-bottom: .5rem; + line-height: 1.3; +} +.entry-excerpt { font-size: .875rem; color: var(--muted); line-height: 1.6; font-weight: 300; } + +/* ---- Empty state ---- */ +.empty-state { text-align: center; padding: 5rem 2rem; color: var(--muted); } +.empty-icon { font-size: 2.5rem; margin-bottom: 1rem; } + +/* ---- Entry detail ---- */ +.detail-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem; } +.back-link { color: var(--muted); font-size: .88rem; font-weight: 300; } +.back-link:hover { color: var(--text); } +.detail-actions { display: flex; gap: .5rem; } +.entry-detail { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 2.5rem; +} +.entry-meta { display: flex; align-items: center; gap: .85rem; margin-bottom: 1.25rem; } +.detail-title { font-size: 2.4rem; line-height: 1.2; margin-bottom: 2rem; } +.entry-content { font-size: 1.05rem; line-height: 1.85; color: var(--text); font-weight: 300; } +.entry-content p { margin-bottom: 1.1rem; } + +/* ---- Forms ---- */ +.form-page { max-width: 660px; } +.form-page h1 { font-size: 2rem; margin-bottom: 1.75rem; } +.diary-form { display: flex; flex-direction: column; gap: 1.35rem; } +.form-group { display: flex; flex-direction: column; gap: .45rem; } +.form-group label { font-size: .85rem; font-weight: 500; color: var(--muted); text-transform: uppercase; letter-spacing: 0.06em; } +.form-group .input { width: 100%; } +.textarea { resize: vertical; min-height: 240px; line-height: 1.7; } +.form-actions { display: flex; gap: .75rem; padding-top: .5rem; } +.field-error { font-size: .82rem; color: var(--danger); } +.field-hint { font-size: .82rem; color: var(--muted); font-weight: 300; } + +/* ---- Auth ---- */ +.auth-page { min-height: calc(100vh - 58px); display: flex; align-items: center; justify-content: center; padding: 2rem; } +.auth-card { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 2.75rem; + width: 100%; + max-width: 420px; +} +.auth-card h1 { font-size: 1.9rem; margin-bottom: 1.75rem; } +.auth-footer { margin-top: 1.25rem; font-size: .875rem; text-align: center; color: var(--muted); font-weight: 300; } diff --git a/z1/diary_app/templates/diary/base.html b/z1/diary_app/templates/diary/base.html new file mode 100644 index 0000000..322b8d5 --- /dev/null +++ b/z1/diary_app/templates/diary/base.html @@ -0,0 +1,33 @@ + + + + + + {% block title %}My Diary{% endblock %} + {% load static %} + + + + +
+ {% if messages %} + {% for message in messages %} +
{{ message }}
+ {% endfor %} + {% endif %} + {% block content %}{% endblock %} +
+ + diff --git a/z1/diary_app/templates/diary/entry_confirm_delete.html b/z1/diary_app/templates/diary/entry_confirm_delete.html new file mode 100644 index 0000000..2b218d1 --- /dev/null +++ b/z1/diary_app/templates/diary/entry_confirm_delete.html @@ -0,0 +1,15 @@ +{% extends 'diary/base.html' %} +{% block title %}Delete Entry{% endblock %} +{% block content %} +
+

Delete Entry

+

Are you sure you want to delete "{{ entry.title }}"? This cannot be undone.

+
+ {% csrf_token %} +
+ + Cancel +
+
+
+{% endblock %} diff --git a/z1/diary_app/templates/diary/entry_detail.html b/z1/diary_app/templates/diary/entry_detail.html new file mode 100644 index 0000000..4d37f57 --- /dev/null +++ b/z1/diary_app/templates/diary/entry_detail.html @@ -0,0 +1,22 @@ +{% extends 'diary/base.html' %} +{% block title %}{{ entry.title }}{% endblock %} +{% block content %} +
+ ← Back +
+ Edit + Delete +
+
+
+ +

{{ entry.title }}

+
{{ entry.content|linebreaks }}
+
+{% endblock %} diff --git a/z1/diary_app/templates/diary/entry_form.html b/z1/diary_app/templates/diary/entry_form.html new file mode 100644 index 0000000..315afb7 --- /dev/null +++ b/z1/diary_app/templates/diary/entry_form.html @@ -0,0 +1,39 @@ +{% extends 'diary/base.html' %} +{% block title %}{{ action }} Entry{% endblock %} +{% block content %} +
+

{{ action }} Entry

+
+ {% csrf_token %} +
+ + +
+
+ + +
+
+ + +
+
+ + Cancel +
+
+
+{% endblock %} diff --git a/z1/diary_app/templates/diary/entry_list.html b/z1/diary_app/templates/diary/entry_list.html new file mode 100644 index 0000000..a86d601 --- /dev/null +++ b/z1/diary_app/templates/diary/entry_list.html @@ -0,0 +1,42 @@ +{% extends 'diary/base.html' %} +{% block title %}My Diary β€” All Entries{% endblock %} +{% block content %} + + +{% if entries %} + +{% else %} +
+

πŸ““

+

No entries yet. Write your first one!

+
+{% endif %} +{% endblock %} diff --git a/z1/diary_app/templates/diary/login.html b/z1/diary_app/templates/diary/login.html new file mode 100644 index 0000000..85b4caa --- /dev/null +++ b/z1/diary_app/templates/diary/login.html @@ -0,0 +1,27 @@ +{% extends 'diary/base.html' %} +{% block title %}Login{% endblock %} +{% block content %} +
+
+

Welcome back πŸ““

+
+ {% csrf_token %} + {% for field in form %} +
+ + + {% if field.errors %} + {{ field.errors|join:', ' }} + {% endif %} +
+ {% endfor %} + +
+ +
+
+{% endblock %} diff --git a/z1/diary_app/templates/diary/register.html b/z1/diary_app/templates/diary/register.html new file mode 100644 index 0000000..47c9309 --- /dev/null +++ b/z1/diary_app/templates/diary/register.html @@ -0,0 +1,30 @@ +{% extends 'diary/base.html' %} +{% block title %}Register{% endblock %} +{% block content %} +
+
+

Create an account πŸ““

+
+ {% csrf_token %} + {% for field in form %} +
+ + + {% if field.help_text %} + {{ field.help_text }} + {% endif %} + {% if field.errors %} + {{ field.errors|join:', ' }} + {% endif %} +
+ {% endfor %} + +
+ +
+
+{% endblock %} diff --git a/z1/diary_app/urls.py b/z1/diary_app/urls.py new file mode 100644 index 0000000..eb1f20f --- /dev/null +++ b/z1/diary_app/urls.py @@ -0,0 +1,10 @@ +from django.contrib import admin +from django.urls import path, include +from django.contrib.auth import views as auth_views + +urlpatterns = [ + path('admin/', admin.site.urls), + path('login/', auth_views.LoginView.as_view(template_name='diary/login.html'), name='login'), + path('logout/', auth_views.LogoutView.as_view(), name='logout'), + path('', include('diary.urls')), +] diff --git a/z1/diary_app/wsgi.py b/z1/diary_app/wsgi.py new file mode 100644 index 0000000..f8e8f27 --- /dev/null +++ b/z1/diary_app/wsgi.py @@ -0,0 +1,5 @@ +import os +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'diary_app.settings') +application = get_wsgi_application() diff --git a/z1/docker-compose.yaml b/z1/docker-compose.yaml new file mode 100644 index 0000000..78ac98d --- /dev/null +++ b/z1/docker-compose.yaml @@ -0,0 +1,63 @@ +services: + + db: + image: postgres:15-alpine + container_name: diary-db + restart: unless-stopped + environment: + POSTGRES_DB: diarydb + POSTGRES_USER: diaryuser + POSTGRES_PASSWORD: diarypass + volumes: + - postgres_data:/var/lib/postgresql/data + networks: + - diary_network + healthcheck: + test: ["CMD-SHELL", "pg_isready -U diaryuser -d diarydb"] + interval: 5s + timeout: 5s + retries: 10 + + app: + build: . + image: diary-app:latest + container_name: diary-app + restart: unless-stopped + environment: + DJANGO_SETTINGS_MODULE: diary_app.settings + POSTGRES_DB: diarydb + POSTGRES_USER: diaryuser + POSTGRES_PASSWORD: diarypass + DB_HOST: db + DB_PORT: "5432" + SECRET_KEY: change-me-in-production-use-a-long-random-string + DEBUG: "False" + volumes: + - diary_static:/app/staticfiles + networks: + - diary_network + depends_on: + db: + condition: service_healthy + + nginx: + image: nginx:alpine + container_name: diary-nginx + restart: unless-stopped + ports: + - "80:80" + volumes: + - ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro + - diary_static:/app/staticfiles:ro + networks: + - diary_network + depends_on: + - app + +volumes: + postgres_data: + diary_static: + +networks: + diary_network: + driver: bridge diff --git a/z1/entrypoint.sh b/z1/entrypoint.sh new file mode 100644 index 0000000..df67521 --- /dev/null +++ b/z1/entrypoint.sh @@ -0,0 +1,30 @@ +#!/bin/sh +set -e + +echo "Waiting for PostgreSQL to be ready..." +until python -c " +import psycopg2, os, sys +try: + psycopg2.connect( + dbname=os.environ.get('POSTGRES_DB','diarydb'), + user=os.environ.get('POSTGRES_USER','diaryuser'), + password=os.environ.get('POSTGRES_PASSWORD','diarypass'), + host=os.environ.get('DB_HOST','db'), + port=os.environ.get('DB_PORT','5432') + ) +except Exception: + sys.exit(1) +"; do + echo " DB not ready yet, retrying in 2s..." + sleep 2 +done + +echo "PostgreSQL is ready." +echo "Running migrations..." +python manage.py migrate --noinput + +echo "Starting Gunicorn..." +exec gunicorn diary_app.wsgi:application \ + --bind 0.0.0.0:8000 \ + --workers 2 \ + --timeout 120 diff --git a/z1/manage.py b/z1/manage.py new file mode 100644 index 0000000..f677fb9 --- /dev/null +++ b/z1/manage.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python +import os +import sys + +if __name__ == '__main__': + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'diary_app.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed?" + ) from exc + execute_from_command_line(sys.argv) diff --git a/z1/nginx/nginx.conf b/z1/nginx/nginx.conf new file mode 100644 index 0000000..91f9ccd --- /dev/null +++ b/z1/nginx/nginx.conf @@ -0,0 +1,21 @@ +upstream django { + server app:8000; +} + +server { + listen 80; + server_name localhost; + + location /static/ { + alias /app/staticfiles/; + } + + 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; + } +} diff --git a/z1/prepare-app.sh b/z1/prepare-app.sh new file mode 100644 index 0000000..c6a3446 --- /dev/null +++ b/z1/prepare-app.sh @@ -0,0 +1,23 @@ +#!/bin/bash +set -e + +echo "Preparing app..." + +if docker compose version > /dev/null 2>&1; then + echo " Using docker compose to build image..." + docker compose build +else + echo " Using plain docker to build image..." + docker build -t diary-app:latest . + docker network create diary_network 2>/dev/null || echo " Network diary_network already exists, skipping." + docker volume create postgres_data 2>/dev/null || echo " Volume postgres_data already exists, skipping." + docker volume create diary_static 2>/dev/null || echo " Volume diary_static already exists, skipping." +fi + +echo "" +echo "Preparation complete." +echo " Image : diary-app:latest" +echo " Network: diary_network" +echo " Volumes: postgres_data, diary_static" +echo "" +echo "Run ./start-app.sh to start the application." diff --git a/z1/remove-app.sh b/z1/remove-app.sh new file mode 100644 index 0000000..3539470 --- /dev/null +++ b/z1/remove-app.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +echo "Removing app..." + +if docker compose version > /dev/null 2>&1; then + docker compose down --volumes --rmi all +else + docker stop diary-nginx diary-app diary-db 2>/dev/null || true + docker rm diary-nginx diary-app diary-db 2>/dev/null || true + docker rmi diary-app:latest 2>/dev/null && echo " Removed image: diary-app:latest" || echo " Image not found, skipping." + docker volume rm postgres_data 2>/dev/null && echo " Removed volume: postgres_data" || echo " Volume not found, skipping." + docker volume rm diary_static 2>/dev/null && echo " Removed volume: diary_static" || echo " Volume not found, skipping." + docker network rm diary_network 2>/dev/null && echo " Removed network: diary_network" || echo " Network not found, skipping." +fi + +echo "" +echo "Removed app." diff --git a/z1/requirements.txt b/z1/requirements.txt new file mode 100644 index 0000000..15f4a4d --- /dev/null +++ b/z1/requirements.txt @@ -0,0 +1,3 @@ +Django==4.2.11 +psycopg2-binary==2.9.9 +gunicorn==21.2.0 diff --git a/z1/start-app.sh b/z1/start-app.sh new file mode 100644 index 0000000..e0392fa --- /dev/null +++ b/z1/start-app.sh @@ -0,0 +1,49 @@ +#!/bin/bash +set -e + +echo "Running app..." + +if docker compose version > /dev/null 2>&1; then + docker compose up -d +else + docker run -d \ + --name diary-db \ + --network diary_network \ + --restart unless-stopped \ + -e POSTGRES_DB=diarydb \ + -e POSTGRES_USER=diaryuser \ + -e POSTGRES_PASSWORD=diarypass \ + -v postgres_data:/var/lib/postgresql/data \ + postgres:15-alpine + + docker run -d \ + --name diary-app \ + --network diary_network \ + --restart unless-stopped \ + -e DJANGO_SETTINGS_MODULE=diary_app.settings \ + -e POSTGRES_DB=diarydb \ + -e POSTGRES_USER=diaryuser \ + -e POSTGRES_PASSWORD=diarypass \ + -e DB_HOST=diary-db \ + -e DB_PORT=5432 \ + -e SECRET_KEY=change-me-in-production-use-a-long-random-string \ + -e DEBUG=False \ + -v diary_static:/app/staticfiles \ + diary-app:latest + + docker run -d \ + --name diary-nginx \ + --network diary_network \ + --restart unless-stopped \ + -p 80:80 \ + -v "$(pwd)/nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro" \ + -v diary_static:/app/staticfiles:ro \ + nginx:alpine +fi + +echo "" +echo "All containers started." +echo "The app is available at http://localhost" +echo "" +echo "Note: First startup may take 10-15 seconds while the" +echo " database initialises and migrations run." diff --git a/z1/stop-app.sh b/z1/stop-app.sh new file mode 100644 index 0000000..a0f292a --- /dev/null +++ b/z1/stop-app.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +echo "Stopping app..." + +if docker compose version > /dev/null 2>&1; then + docker compose down +else + docker stop diary-nginx 2>/dev/null && echo " Stopped diary-nginx" || echo " diary-nginx was not running" + docker stop diary-app 2>/dev/null && echo " Stopped diary-app" || echo " diary-app was not running" + docker stop diary-db 2>/dev/null && echo " Stopped diary-db" || echo " diary-db was not running" + docker rm diary-nginx diary-app diary-db 2>/dev/null || true +fi + +echo "" +echo "App stopped. All data is preserved in the postgres_data volume." +echo "Run ./start-app.sh to start it again."