Assignment-1 :Docker
This commit is contained in:
parent
292fe08c84
commit
5d26cf4329
BIN
z1/CT-Assignment1 - Documentation.pdf
Normal file
BIN
z1/CT-Assignment1 - Documentation.pdf
Normal file
Binary file not shown.
25
z1/Dockerfile
Normal file
25
z1/Dockerfile
Normal file
@ -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"]
|
||||
0
z1/diary/__init__.py
Normal file
0
z1/diary/__init__.py
Normal file
8
z1/diary/admin.py
Normal file
8
z1/diary/admin.py
Normal file
@ -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')
|
||||
27
z1/diary/models.py
Normal file
27
z1/diary/models.py
Normal file
@ -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')})"
|
||||
11
z1/diary/urls.py
Normal file
11
z1/diary/urls.py
Normal file
@ -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/<int:pk>/', views.entry_detail, name='entry_detail'),
|
||||
path('entry/<int:pk>/edit/', views.entry_edit, name='entry_edit'),
|
||||
path('entry/<int:pk>/del/', views.entry_delete, name='entry_delete'),
|
||||
path('register/', views.register, name='register'),
|
||||
]
|
||||
86
z1/diary/views.py
Normal file
86
z1/diary/views.py
Normal file
@ -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})
|
||||
0
z1/diary_app/__init__.py
Normal file
0
z1/diary_app/__init__.py
Normal file
72
z1/diary_app/settings.py
Normal file
72
z1/diary_app/settings.py
Normal file
@ -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/'
|
||||
4
z1/diary_app/settings_build.py
Normal file
4
z1/diary_app/settings_build.py
Normal file
@ -0,0 +1,4 @@
|
||||
from diary_app.settings import *
|
||||
|
||||
DATABASES = {}
|
||||
STATICFILES_DIRS = [BASE_DIR / 'static']
|
||||
193
z1/diary_app/static/diary/css/style.css
Normal file
193
z1/diary_app/static/diary/css/style.css
Normal file
@ -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; }
|
||||
33
z1/diary_app/templates/diary/base.html
Normal file
33
z1/diary_app/templates/diary/base.html
Normal file
@ -0,0 +1,33 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}My Diary{% endblock %}</title>
|
||||
{% load static %}
|
||||
<link rel="stylesheet" href="{% static 'diary/css/style.css' %}">
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar">
|
||||
<a href="{% url 'entry_list' %}" class="nav-brand">📓 My Diary</a>
|
||||
<div class="nav-links">
|
||||
{% if user.is_authenticated %}
|
||||
<span class="nav-user">Hello, <strong>{{ user.username }}</strong></span>
|
||||
<a href="{% url 'entry_create' %}" class="btn btn-primary btn-sm">+ New Entry</a>
|
||||
<a href="{% url 'logout' %}" class="btn btn-ghost btn-sm">Logout</a>
|
||||
{% else %}
|
||||
<a href="{% url 'login' %}" class="btn btn-ghost btn-sm">Login</a>
|
||||
<a href="{% url 'register' %}" class="btn btn-primary btn-sm">Register</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</nav>
|
||||
<main class="container">
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<div class="alert alert-{{ message.tags }}">{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
15
z1/diary_app/templates/diary/entry_confirm_delete.html
Normal file
15
z1/diary_app/templates/diary/entry_confirm_delete.html
Normal file
@ -0,0 +1,15 @@
|
||||
{% extends 'diary/base.html' %}
|
||||
{% block title %}Delete Entry{% endblock %}
|
||||
{% block content %}
|
||||
<div class="form-page">
|
||||
<h1>Delete Entry</h1>
|
||||
<p>Are you sure you want to delete <strong>"{{ entry.title }}"</strong>? This cannot be undone.</p>
|
||||
<form method="post" class="diary-form">
|
||||
{% csrf_token %}
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-danger">Yes, delete it</button>
|
||||
<a href="{% url 'entry_detail' entry.pk %}" class="btn btn-ghost">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
22
z1/diary_app/templates/diary/entry_detail.html
Normal file
22
z1/diary_app/templates/diary/entry_detail.html
Normal file
@ -0,0 +1,22 @@
|
||||
{% extends 'diary/base.html' %}
|
||||
{% block title %}{{ entry.title }}{% endblock %}
|
||||
{% block content %}
|
||||
<div class="detail-header">
|
||||
<a href="{% url 'entry_list' %}" class="back-link">← Back</a>
|
||||
<div class="detail-actions">
|
||||
<a href="{% url 'entry_edit' entry.pk %}" class="btn btn-primary btn-sm">Edit</a>
|
||||
<a href="{% url 'entry_delete' entry.pk %}" class="btn btn-danger btn-sm">Delete</a>
|
||||
</div>
|
||||
</div>
|
||||
<article class="entry-detail">
|
||||
<div class="entry-meta">
|
||||
<span class="mood-badge mood-large">{{ entry.get_mood_display }}</span>
|
||||
<span class="entry-date">{{ entry.created_at|date:"F j, Y — H:i" }}</span>
|
||||
{% if entry.updated_at != entry.created_at %}
|
||||
<span class="entry-date">(edited {{ entry.updated_at|date:"M d" }})</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h1 class="detail-title">{{ entry.title }}</h1>
|
||||
<div class="entry-content">{{ entry.content|linebreaks }}</div>
|
||||
</article>
|
||||
{% endblock %}
|
||||
39
z1/diary_app/templates/diary/entry_form.html
Normal file
39
z1/diary_app/templates/diary/entry_form.html
Normal file
@ -0,0 +1,39 @@
|
||||
{% extends 'diary/base.html' %}
|
||||
{% block title %}{{ action }} Entry{% endblock %}
|
||||
{% block content %}
|
||||
<div class="form-page">
|
||||
<h1>{{ action }} Entry</h1>
|
||||
<form method="post" class="diary-form">
|
||||
{% csrf_token %}
|
||||
<div class="form-group">
|
||||
<label for="title">Title</label>
|
||||
<input type="text" id="title" name="title"
|
||||
value="{{ entry.title|default:'' }}"
|
||||
placeholder="Give your entry a title…"
|
||||
class="input" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="mood">Mood</label>
|
||||
<select id="mood" name="mood" class="input">
|
||||
{% for val, label in mood_choices %}
|
||||
<option value="{{ val }}"
|
||||
{% if entry.mood == val or not entry and val == 'calm' %}selected{% endif %}>
|
||||
{{ label }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="content">Write your thoughts…</label>
|
||||
<textarea id="content" name="content" rows="12"
|
||||
placeholder="What's on your mind today?"
|
||||
class="input textarea" required>{{ entry.content|default:'' }}</textarea>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">{{ action }} Entry</button>
|
||||
<a href="{% if entry %}{% url 'entry_detail' entry.pk %}{% else %}{% url 'entry_list' %}{% endif %}"
|
||||
class="btn btn-ghost">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
42
z1/diary_app/templates/diary/entry_list.html
Normal file
42
z1/diary_app/templates/diary/entry_list.html
Normal file
@ -0,0 +1,42 @@
|
||||
{% extends 'diary/base.html' %}
|
||||
{% block title %}My Diary — All Entries{% endblock %}
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1>My Diary</h1>
|
||||
<p class="subtitle">{{ entries.count }} entr{{ entries.count|pluralize:"y,ies" }}</p>
|
||||
</div>
|
||||
<div class="search-bar">
|
||||
<form method="get" class="search-form">
|
||||
<input type="text" name="q" value="{{ query }}" placeholder="Search entries…" class="input">
|
||||
<select name="mood" class="input select-input">
|
||||
<option value="">All moods</option>
|
||||
{% for val, label in mood_choices %}
|
||||
<option value="{{ val }}" {% if mood_filter == val %}selected{% endif %}>{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button type="submit" class="btn btn-primary">Search</button>
|
||||
{% if query or mood_filter %}
|
||||
<a href="{% url 'entry_list' %}" class="btn btn-ghost">Clear</a>
|
||||
{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
{% if entries %}
|
||||
<div class="entries-grid">
|
||||
{% for entry in entries %}
|
||||
<a href="{% url 'entry_detail' entry.pk %}" class="entry-card">
|
||||
<div class="entry-card-header">
|
||||
<span class="mood-badge">{{ entry.get_mood_display }}</span>
|
||||
<span class="entry-date">{{ entry.created_at|date:"M d, Y" }}</span>
|
||||
</div>
|
||||
<h2 class="entry-title">{{ entry.title }}</h2>
|
||||
<p class="entry-excerpt">{{ entry.content|truncatechars:120 }}</p>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty-state">
|
||||
<p class="empty-icon">📓</p>
|
||||
<p>No entries yet. <a href="{% url 'entry_create' %}">Write your first one!</a></p>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
27
z1/diary_app/templates/diary/login.html
Normal file
27
z1/diary_app/templates/diary/login.html
Normal file
@ -0,0 +1,27 @@
|
||||
{% extends 'diary/base.html' %}
|
||||
{% block title %}Login{% endblock %}
|
||||
{% block content %}
|
||||
<div class="auth-page">
|
||||
<div class="auth-card">
|
||||
<h1>Welcome back 📓</h1>
|
||||
<form method="post" class="diary-form">
|
||||
{% csrf_token %}
|
||||
{% for field in form %}
|
||||
<div class="form-group">
|
||||
<label for="{{ field.id_for_label }}">{{ field.label }}</label>
|
||||
<input type="{{ field.field.widget.input_type }}"
|
||||
id="{{ field.id_for_label }}"
|
||||
name="{{ field.html_name }}"
|
||||
class="input"
|
||||
{% if field.field.required %}required{% endif %}>
|
||||
{% if field.errors %}
|
||||
<span class="field-error">{{ field.errors|join:', ' }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
<button type="submit" class="btn btn-primary btn-full">Login</button>
|
||||
</form>
|
||||
<p class="auth-footer">No account? <a href="{% url 'register' %}">Register here</a></p>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
30
z1/diary_app/templates/diary/register.html
Normal file
30
z1/diary_app/templates/diary/register.html
Normal file
@ -0,0 +1,30 @@
|
||||
{% extends 'diary/base.html' %}
|
||||
{% block title %}Register{% endblock %}
|
||||
{% block content %}
|
||||
<div class="auth-page">
|
||||
<div class="auth-card">
|
||||
<h1>Create an account 📓</h1>
|
||||
<form method="post" class="diary-form">
|
||||
{% csrf_token %}
|
||||
{% for field in form %}
|
||||
<div class="form-group">
|
||||
<label for="{{ field.id_for_label }}">{{ field.label }}</label>
|
||||
<input type="{{ field.field.widget.input_type }}"
|
||||
id="{{ field.id_for_label }}"
|
||||
name="{{ field.html_name }}"
|
||||
class="input"
|
||||
{% if field.field.required %}required{% endif %}>
|
||||
{% if field.help_text %}
|
||||
<span class="field-hint">{{ field.help_text }}</span>
|
||||
{% endif %}
|
||||
{% if field.errors %}
|
||||
<span class="field-error">{{ field.errors|join:', ' }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
<button type="submit" class="btn btn-primary btn-full">Register</button>
|
||||
</form>
|
||||
<p class="auth-footer">Already have an account? <a href="{% url 'login' %}">Login</a></p>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
10
z1/diary_app/urls.py
Normal file
10
z1/diary_app/urls.py
Normal file
@ -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')),
|
||||
]
|
||||
5
z1/diary_app/wsgi.py
Normal file
5
z1/diary_app/wsgi.py
Normal file
@ -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()
|
||||
63
z1/docker-compose.yaml
Normal file
63
z1/docker-compose.yaml
Normal file
@ -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
|
||||
30
z1/entrypoint.sh
Normal file
30
z1/entrypoint.sh
Normal file
@ -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
|
||||
13
z1/manage.py
Normal file
13
z1/manage.py
Normal file
@ -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)
|
||||
21
z1/nginx/nginx.conf
Normal file
21
z1/nginx/nginx.conf
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
23
z1/prepare-app.sh
Normal file
23
z1/prepare-app.sh
Normal file
@ -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."
|
||||
17
z1/remove-app.sh
Normal file
17
z1/remove-app.sh
Normal file
@ -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."
|
||||
3
z1/requirements.txt
Normal file
3
z1/requirements.txt
Normal file
@ -0,0 +1,3 @@
|
||||
Django==4.2.11
|
||||
psycopg2-binary==2.9.9
|
||||
gunicorn==21.2.0
|
||||
49
z1/start-app.sh
Normal file
49
z1/start-app.sh
Normal file
@ -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."
|
||||
16
z1/stop-app.sh
Normal file
16
z1/stop-app.sh
Normal file
@ -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."
|
||||
Loading…
Reference in New Issue
Block a user