From e3a1de39c29856ed0df9abbe574f9ebdf476d7ef Mon Sep 17 00:00:00 2001 From: lcgeneralprojects Date: Sun, 17 Dec 2023 01:06:50 +0500 Subject: [PATCH] First commit. Basic blog functionality implemented. --- .gitignore | 4 + .idea/.gitignore | 3 + .idea/django-posgresql-study.iml | 8 + .../inspectionProfiles/profiles_settings.xml | 6 + .idea/misc.xml | 7 + .idea/modules.xml | 8 + .idea/vcs.xml | 6 + Pipfile | 12 + Pipfile.lock | 62 +++ server/django_postgresql_study/__init__.py | 0 server/django_postgresql_study/asgi.py | 16 + server/django_postgresql_study/settings.py | 147 +++++++ server/django_postgresql_study/urls.py | 25 ++ server/django_postgresql_study/wsgi.py | 16 + server/docker-compose.yml | 9 + server/dockerfile | 14 + server/manage.py | 22 ++ server/my_app/__init__.py | 0 server/my_app/admin.py | 5 + server/my_app/apps.py | 6 + server/my_app/forms.py | 22 ++ server/my_app/migrations/0001_initial.py | 28 ++ server/my_app/migrations/__init__.py | 0 server/my_app/models.py | 16 + server/my_app/tests.py | 3 + server/my_app/urls.py | 16 + server/my_app/views.py | 169 ++++++++ server/requirements.txt | 10 + server/static/css/style.css | 369 ++++++++++++++++++ server/templates/base.html | 48 +++ server/templates/hello.html | 10 + server/templates/home.html | 24 ++ server/templates/login.html | 27 ++ server/templates/post_confirm_delete.html | 14 + server/templates/post_form.html | 12 + server/templates/register.html | 33 ++ 36 files changed, 1177 insertions(+) create mode 100644 .idea/.gitignore create mode 100644 .idea/django-posgresql-study.iml create mode 100644 .idea/inspectionProfiles/profiles_settings.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml create mode 100644 Pipfile create mode 100644 Pipfile.lock create mode 100644 server/django_postgresql_study/__init__.py create mode 100644 server/django_postgresql_study/asgi.py create mode 100644 server/django_postgresql_study/settings.py create mode 100644 server/django_postgresql_study/urls.py create mode 100644 server/django_postgresql_study/wsgi.py create mode 100644 server/docker-compose.yml create mode 100644 server/dockerfile create mode 100644 server/manage.py create mode 100644 server/my_app/__init__.py create mode 100644 server/my_app/admin.py create mode 100644 server/my_app/apps.py create mode 100644 server/my_app/forms.py create mode 100644 server/my_app/migrations/0001_initial.py create mode 100644 server/my_app/migrations/__init__.py create mode 100644 server/my_app/models.py create mode 100644 server/my_app/tests.py create mode 100644 server/my_app/urls.py create mode 100644 server/my_app/views.py create mode 100644 server/requirements.txt create mode 100644 server/static/css/style.css create mode 100644 server/templates/base.html create mode 100644 server/templates/hello.html create mode 100644 server/templates/home.html create mode 100644 server/templates/login.html create mode 100644 server/templates/post_confirm_delete.html create mode 100644 server/templates/post_form.html create mode 100644 server/templates/register.html diff --git a/.gitignore b/.gitignore index 68bc17f..a92693c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# VSCode stuff +.vscode/ + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] @@ -120,6 +123,7 @@ celerybeat.pid *.sage.py # Environments +*.env .env .venv env/ diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/django-posgresql-study.iml b/.idea/django-posgresql-study.iml new file mode 100644 index 0000000..1fbc8a2 --- /dev/null +++ b/.idea/django-posgresql-study.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..2446bb2 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..ba7c338 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..9049469 --- /dev/null +++ b/Pipfile @@ -0,0 +1,12 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +django-debug-toolbar = "*" + +[dev-packages] + +[requires] +python_version = "3.12" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..b0c7770 --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,62 @@ +{ + "_meta": { + "hash": { + "sha256": "d9fd30eec2179f683fe29c84c46b80cd8c9d221205daab88c3827fe22822441b" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.12" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "asgiref": { + "hashes": [ + "sha256:89b2ef2247e3b562a16eef663bc0e2e703ec6468e2fa8a5cd61cd449786d4f6e", + "sha256:9e0ce3aa93a819ba5b45120216b23878cf6e8525eb3848653452b4192b92afed" + ], + "markers": "python_version >= '3.7'", + "version": "==3.7.2" + }, + "django": { + "hashes": [ + "sha256:8e0f1c2c2786b5c0e39fe1afce24c926040fad47c8ea8ad30aaf1188df29fc41", + "sha256:e1d37c51ad26186de355cbcec16613ebdabfa9689bbade9c538835205a8abbe9" + ], + "markers": "python_version >= '3.8'", + "version": "==4.2.7" + }, + "django-debug-toolbar": { + "hashes": [ + "sha256:af99128c06e8e794479e65ab62cc6c7d1e74e1c19beb44dcbf9bad7a9c017327", + "sha256:bc7fdaafafcdedefcc67a4a5ad9dac96efd6e41db15bc74d402a54a2ba4854dc" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==4.2.0" + }, + "sqlparse": { + "hashes": [ + "sha256:5430a4fe2ac7d0f93e66f1efc6e1338a41884b7ddf2a350cedd20ccc4d9d28f3", + "sha256:d446183e84b8349fa3061f0fe7f06ca94ba65b426946ffebe6e3e8295332420c" + ], + "markers": "python_version >= '3.5'", + "version": "==0.4.4" + }, + "tzdata": { + "hashes": [ + "sha256:11ef1e08e54acb0d4f95bdb1be05da659673de4acbd21bf9c69e94cc5e907a3a", + "sha256:7e65763eef3120314099b6939b5546db7adce1e7d6f2e179e3df563c70511eda" + ], + "markers": "sys_platform == 'win32'", + "version": "==2023.3" + } + }, + "develop": {} +} diff --git a/server/django_postgresql_study/__init__.py b/server/django_postgresql_study/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/server/django_postgresql_study/asgi.py b/server/django_postgresql_study/asgi.py new file mode 100644 index 0000000..47e6dd0 --- /dev/null +++ b/server/django_postgresql_study/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for django_postgresql_study project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_postgresql_study.settings') + +application = get_asgi_application() diff --git a/server/django_postgresql_study/settings.py b/server/django_postgresql_study/settings.py new file mode 100644 index 0000000..143f272 --- /dev/null +++ b/server/django_postgresql_study/settings.py @@ -0,0 +1,147 @@ +""" +Django settings for django_postgresql_study project. + +Generated by 'django-admin startproject' using Django 4.2.7. + +For more information on this file, see +https://docs.djangoproject.com/en/4.2/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/4.2/ref/settings/ +""" + +from pathlib import Path +import environ +env = environ.Env() +environ.Env.read_env() + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = env("SECRET_KEY") + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + +# Login redirection +LOGIN_URL = 'login' + + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'my_app', + "debug_toolbar", +] + +MIDDLEWARE = [ + "debug_toolbar.middleware.DebugToolbarMiddleware", + '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', +] + +INTERNAL_IPS = [ + "127.0.0.1", +] + +ROOT_URLCONF = 'django_postgresql_study.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', + ], + }, + }, +] + +WSGI_APPLICATION = 'django_postgresql_study.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/4.2/ref/settings/#databases + +DATABASES = { + # 'default': { + # 'ENGINE': 'django.db.backends.sqlite3', + # 'NAME': BASE_DIR / 'db.sqlite3', + # } + 'default': { + 'ENGINE': 'django.db.backends.postgresql_psycopg2', + 'NAME': env("DB_NAME"), + 'USER': env("DB_USER"), + 'PASSWORD': env("DB_PASSWORD"), + 'HOST': env("DB_HOST"), + 'PORT': env("DB_PORT") + } +} + + +# Password validation +# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/4.2/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/4.2/howto/static-files/ + +STATIC_URL = 'static/' +STATICFILES_DIRS = [BASE_DIR / 'static'] + +# Default primary key field type +# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' diff --git a/server/django_postgresql_study/urls.py b/server/django_postgresql_study/urls.py new file mode 100644 index 0000000..0edbcba --- /dev/null +++ b/server/django_postgresql_study/urls.py @@ -0,0 +1,25 @@ +""" +URL configuration for django_postgresql_study project. + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/4.2/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.contrib import admin +from django.urls import path, include + +urlpatterns = [ + path('admin/', admin.site.urls), + path('my_app/', include('my_app.urls')), + path('', include('my_app.urls')), + path("__debug__/", include("debug_toolbar.urls")), +] diff --git a/server/django_postgresql_study/wsgi.py b/server/django_postgresql_study/wsgi.py new file mode 100644 index 0000000..773e862 --- /dev/null +++ b/server/django_postgresql_study/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for django_postgresql_study project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_postgresql_study.settings') + +application = get_wsgi_application() diff --git a/server/docker-compose.yml b/server/docker-compose.yml new file mode 100644 index 0000000..2f3d6c6 --- /dev/null +++ b/server/docker-compose.yml @@ -0,0 +1,9 @@ +version: '3.12' + +services: + django: + image: django_postgresql_study_image + command: python manage.py runserver 0.0.0.0:8000 + build: . + ports: + - "8000:8000" \ No newline at end of file diff --git a/server/dockerfile b/server/dockerfile new file mode 100644 index 0000000..6fa716d --- /dev/null +++ b/server/dockerfile @@ -0,0 +1,14 @@ +FROM python:3.12.0 + +WORKDIR /app + +COPY ./ . +COPY requirements.txt . + +ENV PYTHONUNBUFFERED=1 + +RUN pip install -r requirements.txt + +EXPOSE 8000 + +CMD ["python", "./manage.py", "runserver"] \ No newline at end of file diff --git a/server/manage.py b/server/manage.py new file mode 100644 index 0000000..703acee --- /dev/null +++ b/server/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_postgresql_study.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 and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/server/my_app/__init__.py b/server/my_app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/server/my_app/admin.py b/server/my_app/admin.py new file mode 100644 index 0000000..ffea664 --- /dev/null +++ b/server/my_app/admin.py @@ -0,0 +1,5 @@ +from django.contrib import admin +from .models import Post + +# Register your models here. +admin.site.register(Post) \ No newline at end of file diff --git a/server/my_app/apps.py b/server/my_app/apps.py new file mode 100644 index 0000000..e360eca --- /dev/null +++ b/server/my_app/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class MyAppConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'my_app' diff --git a/server/my_app/forms.py b/server/my_app/forms.py new file mode 100644 index 0000000..be77452 --- /dev/null +++ b/server/my_app/forms.py @@ -0,0 +1,22 @@ +from django import forms +from django.contrib.auth.models import User +from django.contrib.auth.forms import UserCreationForm +from .models import Post + + +class LoginForm(forms.Form): + username = forms.CharField(label='username', max_length=100) + password = forms.CharField(label='password', max_length=100, widget=forms.PasswordInput) + + +class RegisterForm(UserCreationForm): + class Meta: + model=User + fields = ['username', 'password1', 'password2'] + + +class PostForm(forms.ModelForm): + + class Meta: + model = Post + fields = ['title', 'content'] \ No newline at end of file diff --git a/server/my_app/migrations/0001_initial.py b/server/my_app/migrations/0001_initial.py new file mode 100644 index 0000000..86b8a6d --- /dev/null +++ b/server/my_app/migrations/0001_initial.py @@ -0,0 +1,28 @@ +# Generated by Django 4.2.7 on 2023-12-13 22:47 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Post', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=120)), + ('content', models.TextField()), + ('published_at', models.DateTimeField(default=django.utils.timezone.now)), + ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/server/my_app/migrations/__init__.py b/server/my_app/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/server/my_app/models.py b/server/my_app/models.py new file mode 100644 index 0000000..eca68d7 --- /dev/null +++ b/server/my_app/models.py @@ -0,0 +1,16 @@ +from django.db import models +from django.utils import timezone +from django.contrib.auth.models import User + +class Post(models.Model): + title = models.CharField(max_length=120) + content = models.TextField() + published_at = models.DateTimeField(default=timezone.now) + author = models.ForeignKey(User, on_delete=models.CASCADE) + + def __str__(self): + return self.title + + class META: + # db_table = 'posts' + ordering = ['-published_at'] \ No newline at end of file diff --git a/server/my_app/tests.py b/server/my_app/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/server/my_app/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/server/my_app/urls.py b/server/my_app/urls.py new file mode 100644 index 0000000..0eab88a --- /dev/null +++ b/server/my_app/urls.py @@ -0,0 +1,16 @@ +from django.urls import path +from . import views + +# URLConf +urlpatterns = [ + path(r'hello/', views.say_hello), + # path(r'insert/', views.dummy_insert), + path(r'register/', views.sign_up, name='register'), + path(r'login/', views.sign_in, name='login'), + path(r'logout/', views.sign_out, name='logout'), + path(r'home/', views.home, name='home'), + path(r'home/create', views.create_post, name='post_create'), + path(r'home/edit//', views.edit_post, name='post_edit'), + path(r'home/delete//', views.delete_post, name='post_delete'), + path(r'', views.default_view, name='default') +] \ No newline at end of file diff --git a/server/my_app/views.py b/server/my_app/views.py new file mode 100644 index 0000000..6f30710 --- /dev/null +++ b/server/my_app/views.py @@ -0,0 +1,169 @@ +from django.shortcuts import render, redirect, get_object_or_404 +from django.http import HttpResponse +from django.contrib import messages +from django.contrib.auth import login, authenticate, logout +from django.contrib.auth.decorators import login_required +from .models import Post +from .forms import LoginForm, RegisterForm, PostForm +import psycopg2 +import environ + +# Environ stuff +env = environ.Env() +environ.Env.read_env() + +# Create your views here. +def say_hello(request): + # return HttpResponse('Hello, World!') + x = 1 + y = 2 + return render(request, 'hello.html', {'name': 'Test'}) + +# def dummy_insert(request): +# # return HttpResponse(request.GET.get('input')) +# columns = ['first_name', 'last_name', 'gender', 'date_of_birth', 'email'] +# values = [f'\'{value}\'' for value in request.GET.get('input').split(', ')] +# values[3] = f'DATE{values[3]}' +# table = 'person' + +# # Begin PostgreSQL stuff +# conn = psycopg2.connect(host=env("DB_HOST"), dbname=env("DB_NAME"), user=env("DB_USER"), password=env("DB_PASSWORD"), port=env("DB_PORT")) +# cur = conn.cursor() + +# cur.execute(f""" +# INSERT INTO {table} ({', '.join(column for column in columns)}) +# VALUES ({', '.join(value for value in values)}) +# """) + +# conn.commit() +# cur.close() +# conn.close() +# # End PostgreSQL stuff + +# return HttpResponse(f'inserted ({ columns }) into person values ({ values })') + +def default_view(request): + if request.method == 'GET': + return redirect('home') + +def sign_up(request): + if request.method == 'GET': + form = RegisterForm() + return render(request, 'register.html', {'form': form}) + + elif request.method == 'POST': + form = RegisterForm(request.POST) + if form.is_valid(): + user = form.save(commit=False) + user.username = user.username.lower() + user.save() + messages.success(request, 'Registration successful') + login(request, user) + return redirect('home') + else: + return render(request, 'register.html', {'form': form}) + +def sign_in(request): + if request.method == 'GET': + if request.user.is_authenticated: + return redirect('home') + + form = LoginForm() + return render(request, 'login.html', {'form': form}) + + elif request.method == 'POST': + form = LoginForm(request.POST) + + if form.is_valid(): + username = form.cleaned_data['username'] + password = form.cleaned_data['password'] + user = authenticate(request, username=username, password=password) + if user: + login(request, user) + messages.success(request, f'Hi, {username.title()}, welcome back!') + return redirect('home') + + messages.error(request, 'Invalid username or password') + return render(request, 'login.html', {'form': form}) + +def sign_out(request): + if request.method == 'GET': + logout(request) + messages.success(request, 'You have been logged out.') + return redirect('login') + + +@login_required +def create_post(request): + if request.method == 'GET': + context = {'form': PostForm()} + return render(request, 'post_form.html', context) + + elif request.method == 'POST': + form = PostForm(request.POST) + if form.is_valid(): + user = form.save(commit=False) + user.author = request.user + form.save() + messages.success(request, 'The post has been created successfully.') + return redirect('home') + else: + messages.error(request, 'Please correct the following errors:') + return render(request, 'post_form.html', {'form': form}) + + +@login_required +def edit_post(request, id): + post = get_object_or_404(Post, id=id) + + if request.method == 'GET': + context = {'form': PostForm(instance=post), 'id': id} + return render(request, 'post_form.html', context) + + elif request.method == 'POST': + form = PostForm(request.POST, instance=post) + if form.is_valid(): + form.save() + messages.success(request, 'The post has been updated successfully') + return redirect('home') + else: + messages.error(request, 'Please correct the following error:') + return render(request, 'post_form.html', {'form': form}) + + +@login_required +def delete_post(request, id): + post = get_object_or_404(Post, id=id) + context = {'post': post} + + if request.method == 'GET': + return render(request, 'post_confirm_delete.html', context) + + elif request.method == 'POST': + post.delete() + messages.success(request, 'The post has been deleted successfully.') + return redirect('home') + + +def home(request): + posts = Post.objects.all().order_by('-published_at') + context = {'posts': posts, 'form': PostForm()} + + if request.method == 'GET': + return render(request, 'home.html', context) + + elif request.method == 'POST': + form = PostForm(request.POST) + if form.is_valid(): + user = form.save(commit=False) + user.author = request.user + form.save() + messages.success(request, 'The post has been created successfully.') + return render(request, 'home.html', context) + else: + messages.error(request, 'Please correct the following errors:') + return render(request, 'home.html', context) + + +def about(request): + return render(request, 'about.html') \ No newline at end of file diff --git a/server/requirements.txt b/server/requirements.txt new file mode 100644 index 0000000..dee837f --- /dev/null +++ b/server/requirements.txt @@ -0,0 +1,10 @@ +asgiref==3.7.2 +Django==4.2.7 +django-debug-toolbar==4.2.0 +django-environ==0.11.2 +psycopg2==2.9.9 +setuptools==57.0.0 +sqlparse==0.4.4 +typing_extensions==4.8.0 +tzdata==2023.3 +wheel==0.36.2 diff --git a/server/static/css/style.css b/server/static/css/style.css new file mode 100644 index 0000000..5b8f2fb --- /dev/null +++ b/server/static/css/style.css @@ -0,0 +1,369 @@ +@import url('https://fonts.googleapis.com/css2?family=Dosis:wght@200;300;400;500;700;800&display=swap'); +@import url('https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.2/font/bootstrap-icons.css'); + +:root { + --color-primary: #ee394c; + --color-secondary: #86b7fe; + --color-light: #f2f2f2; + --color-gray: #ced4da; + --color-dark: #212121; + --color-white: #ffffff; + --color-error: #e2442f; + --color-warning: #ffc900; + --color-info: #02a2b9; + --color-success: #1ba345; + --color-input: #00d1b2; + --border-radius: 5px; +} +* { + margin: 0; + padding: 0; + box-sizing: border-box; + text-rendering: optimizeLegibility; + color: white; +} + +html, +body { + height: 100%; +} + +img { + max-width: 100%; +} +input, +button, +textarea { + font: inherit; +} + +input[type='text']:focus, +input[type='email']:focus, +input[type='password']:focus, +textarea:focus { + outline: 0; + box-shadow: 0 0 0 0.125rem rgb(0 209 178 / 25%); + border: solid 1px var(--color-input); +} + +.btn:focus { + outline: 0; + box-shadow: 0 0 0 0.125rem rgb(238, 57, 76, 25%); +} +body { + font-family: 'Dosis', sans-serif; + font-size: 1rem; + line-height: 1.7; + display: flex; + flex-direction: column; +} +main { + margin: 0 auto; +} + +h1 { + font-size: 2rem; + line-height: 1.5; +} +h2 { + font-size: 1.4rem; +} + +a { + color: var(--color-primary); + text-decoration: none; +} +a:hover { + text-decoration: underline; +} + +.container { + max-width: 960px; + margin: 0 auto; + padding: 0.5rem; +} + +.header { + border-bottom: solid 1px var(--color-light); +} + +.header .container { + display: flex; + align-items: center; + gap:1rem; + +} + +.nav { + display:flex; + gap:1rem; + align-items: center; + justify-content: center; + flex-basis: 100%; +} + + + +.nav a:nth-last-child(2) { + margin-left: auto; + font-weight: bold; +} + + +.logo { + padding: 0.5rem; + background-color: var(--color-primary); + text-decoration: none; + color: var(--color-white); + font-weight: bold; + text-transform: uppercase; +} +.logo:hover { + text-decoration: none; +} +/* feature */ +.feature { + display: flex; + justify-content: space-between; +} +.feature-content { + width: 50%; + display: flex; + flex-direction: column; + justify-content: center; +} +.feature-content .cta { + margin-top: 1rem; + align-self: flex-start; +} +.feature-image { + width: 50%; + height: auto; +} + +/* form */ + +.card { + border: solid 1px var(--color-light); + padding: 1rem; + border-radius: var(--border-radius); + box-shadow: rgb(11 43 158 / 15%) 0px 6px 20px -6px; + min-width: 400px; +} + +/* form */ +form > * { + display: block; + margin-bottom: 0.5rem; +} + +form > input[type='submit'], +form > button { + margin: 1rem 0; + background-color: darkgrey; + color: black +} + +.form-buttons { + display: flex; + justify-content: space-between; + align-items: center; + gap: 0.75rem; +} + +hr { + height: 1px; + border: none; + background-color: var(--color-light); +} + +input[type='text'], +input[type='email'], +input[type='password'], +textarea { + border-radius: var(--border-radius); + border: solid 1px var(--color-gray); + width: 100%; + padding: 0.25rem 0.75rem; + color: black; +} + +textarea { + max-height: 7rem; +} + +/* Task list */ + +.tasks { + list-style: none; + width: 100%; + max-width: 450px; +} + +.tasks li { + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + border-bottom: solid 1px var(--color-dark); + padding: 0.5rem 0; +} +.tasks li:last-of-type { + border-bottom: none; +} + +.task-controls { + display: flex; + justify-content: space-between; + gap: 1rem; +} + +.task-controls a { + color: var(--color-dark); +} + +/* task detail */ + +.task { + margin-bottom: auto; +} + +.badge { + font-size: 0.5rem; + padding: 0.25rem 1rem; + color: var(--color-white); + border-radius: 2rem; + align-self: flex-start; +} + +.badge-completed { + background-color: var(--color-success); +} + +.badge-pending { + background-color: var(--color-warning); +} + +.completed { + text-decoration: line-through; +} + +.completed:hover { + text-decoration: line-through; +} +/* footer */ +.footer p { + text-align: center; +} + +.footer { + margin-top: auto; + padding: 0.5rem 0; + border-top: solid 1px var(--color-light); +} + +.btn { + padding: 0.25rem 0.75rem; + border-radius: var(--border-radius); + background-color: var(--color-white); + border: solid 1px var(--color-white); + color: var(--color-dark); + cursor: pointer; + text-decoration: none; + font-size: 1rem; + min-width: 6rem; + text-align: center; +} + +.btn:hover { + text-decoration: none; +} +.btn-outline { + border: solid 1px var(--color-primary); +} + +.btn-primary { + border: solid 1px var(--color-primary); + background-color: var(--color-primary); + color: var(--color-white); +} + +/* alert */ +.alert { + padding: 0.25rem 0.75rem; + border-radius: 0.5rem; +} +.alert-error { + background-color: var(--color-error); + color: var(--color-light); +} + +.alert-warning { + background-color: var(--color-warning); + color: var(--color-light); +} + +.alert-info { + background-color: var(--color-info); + color: var(--color-light); +} + +.alert-success { + background-color: var(--color-success); + color: var(--color-light); +} + +/* utilities */ +.text-center { + text-align: center; +} + +.error { + color: var(--color-error); +} + +.full-width { + width: 100%; +} + +.center { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 70vh; +} + +@media (max-width: 768px) { + .header > .container { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 1rem; + } + .brand { + order: 1; + } + .card { + max-width: 360px; + } + .feature { + flex-direction: column; + } + .feature-content, + .feature-image { + width: 100%; + } + + .feature-image { + order: 1; + } + .feature-content { + order: 2; + text-align: center; + } + .feature-content .cta { + align-self: center; + } +} diff --git a/server/templates/base.html b/server/templates/base.html new file mode 100644 index 0000000..9172aaa --- /dev/null +++ b/server/templates/base.html @@ -0,0 +1,48 @@ +{% load static %} + + + + + My site + + + + +
+ {% if request.user.is_authenticated %} + Home + Create Post + Hi {{ request.user.username | title }} + Logout + {% else %} + Login + Register + {% endif %} +
+
+ {% if messages %} +
+ {% for message in messages %} +
+ {{ message }} +
+ {% endfor %} +
+ {% endif %} + + {% block content%} + {% endblock content %} +
+ + \ No newline at end of file diff --git a/server/templates/hello.html b/server/templates/hello.html new file mode 100644 index 0000000..cc269fb --- /dev/null +++ b/server/templates/hello.html @@ -0,0 +1,10 @@ + + + {% if name %} +

Hello {{ name }}

+ {% else %} +

Hello World

+ {% endif %} + + + diff --git a/server/templates/home.html b/server/templates/home.html new file mode 100644 index 0000000..f141318 --- /dev/null +++ b/server/templates/home.html @@ -0,0 +1,24 @@ +{% extends "base.html" %} + +{% block style %} +{% endblock style %} + +{% block content %} +

Create Post

+
+ {% csrf_token %} + {{ form.as_p }} + +
+

My Posts

+ {% for post in posts %} +

{{ post.title }}

+ Published on {{ post.published_at | date:"M d, Y" }} by {{ post.author | title}} +

{{ post.content }}

+ + {% if request.user.is_authenticated and request.user == post.author %} +

Edit

+

Delete

+ {% endif %} + {% endfor %} +{% endblock content %} \ No newline at end of file diff --git a/server/templates/login.html b/server/templates/login.html new file mode 100644 index 0000000..7031bad --- /dev/null +++ b/server/templates/login.html @@ -0,0 +1,27 @@ +{% extends "base.html" %} + +{% block style %} +.tabled form { display: table; } +.tabled p { display: table-row; } +.tabled label { display: table-cell; } +.tabled input { display: table-cell; } +{% endblock style %} + +{% block content %} +
+ {% comment %}
+ + +

+ +
+ +
{% endcomment %} +
+ {% csrf_token %} +

Login

+ {{form.as_p}} + +
+
+{% endblock content %} \ No newline at end of file diff --git a/server/templates/post_confirm_delete.html b/server/templates/post_confirm_delete.html new file mode 100644 index 0000000..fb4a27c --- /dev/null +++ b/server/templates/post_confirm_delete.html @@ -0,0 +1,14 @@ +{% extends 'base.html' %} + +{% block content %} + +
+ {% csrf_token %} +

Are you sure that you want to delete the post "{{ post.title }}"?

+
+ + Cancel +
+
+ +{% endblock content %} \ No newline at end of file diff --git a/server/templates/post_form.html b/server/templates/post_form.html new file mode 100644 index 0000000..045c7e6 --- /dev/null +++ b/server/templates/post_form.html @@ -0,0 +1,12 @@ +{% extends 'base.html' %} + +{% block content %} + +

{% if id %} Edit {% else %} New {% endif %} Post

+
+ {% csrf_token %} + {{ form.as_p }} + +
+ +{% endblock content %} \ No newline at end of file diff --git a/server/templates/register.html b/server/templates/register.html new file mode 100644 index 0000000..91e02b5 --- /dev/null +++ b/server/templates/register.html @@ -0,0 +1,33 @@ +{% extends "base.html" %} + +{% block style %} +.tabled form { display: table; } +.tabled p { display: table-row; } +.tabled label { display: table-cell; } +.tabled input { display: table-cell; } +{% endblock %} + +{% block content %} +
+
+ {% csrf_token %} +

Sign Up

+ {% comment %} {{ form.as_p }} + {% endcomment %} + {% for field in form %} +

+ {% if field.errors %} +

    + {% for error in field.errors %} +
  • {{ error }}
  • + {% endfor %} +
+ {% endif %} + {{ field.label_tag }} {{ field }} +

+ {% endfor %} + +

Already have an account? Login

+
+
+{% endblock content %} \ No newline at end of file