From c6423de5e478ae5cac7ae77459b47fb66f61401d Mon Sep 17 00:00:00 2001 From: Jimmy Oty Date: Sun, 12 Mar 2023 12:22:37 +0300 Subject: [PATCH 01/25] Documentation (#45) * Added comments for the blog * Add created_at & updated_at in Category model. * Added verification code that resets * Added verification code that resets * Added password validation * Code change to follow PEP 8 * Delete auto-assign.yml * Delete assign.yml * Delete auto-assign.yml * Delete assign.yml * test comment model (#16) * Add Date Published feature to blog (#18) * Add Date Published feature to blog * Alter field status on stori * Change default for status field in stori * Reactions model (#19) * reactions model * Delete 0002_reaction.py * Create 0005_remove_stori_published_date_alter_stori_status_and_more.py * create postgresql database (#24) Co-authored-by: sangkips * Revert "create postgresql database (#24)" (#32) This reverts commit 97e25f0c9d003e26a0f393ce05e862f5c8705cc8. * Postgres env (#34) * Postgress Database setup with Environment Variables * Add psycopg2==2.9.5 to requirements.txt * workflow env * Add localhost to database host * add migrations * Worked on BlogService to enable fetch, create, update and deletion of blogs(stori) (#27) * delete migrations Signed-off-by: JimmyTron * Added Nested comments (#31) the parent_comment relates to comments and a reply it gets. Child comments are comments under a parent comment. Checking for parent_comment existence makes sure that there can only exist one parent comment and any other replies under parent_comment is a child. * Update Readme --------- Signed-off-by: JimmyTron Co-authored-by: madbunny Co-authored-by: Collins Omariba Co-authored-by: raykipkorir Co-authored-by: Hellen Wainaina Co-authored-by: Moshood Owolabi <98819100+Msdot001@users.noreply.github.com> Co-authored-by: Omariba Collins <88287473+Collins-Omariba@users.noreply.github.com> Co-authored-by: Kipkoech Sang Co-authored-by: Eugene Kwaka Co-authored-by: Bunny <98146814+aibunny@users.noreply.github.com> --- README.md | 233 +++++++++++----------------- accounts/migrations/0001_initial.py | 3 +- 2 files changed, 92 insertions(+), 144 deletions(-) diff --git a/README.md b/README.md index 4a2a653..f353507 100644 --- a/README.md +++ b/README.md @@ -1,173 +1,122 @@ -# Team-Rio-Django - -Backend for Team Rio Written in Django for the Space Ya Tech Project - ![Python](https://img.shields.io/badge/Python-14354C?style=for-the-badge&logo=python&logoColor=white) ![Django](https://img.shields.io/badge/Django-092E20?style=for-the-badge&logo=django&logoColor=white) ![postgresql](https://img.shields.io/badge/PostgreSQL-316192?style=for-the-badge&logo=postgresql&logoColor=white) [![Django CI](https://github.com/SpaceyaTech/Team-Rio-Django/actions/workflows/django.yml/badge.svg?event=push)](https://github.com/SpaceyaTech/Team-Rio-Django/actions/workflows/django.yml) -## Table of contents -- [Overview](#overview) -- [Contributing](#contribution-guide) -- [Commit message](#commit-message-template) -- [Handling Phone Numbers](#phonenumberfield) -- [Authentication](#authentication) -- [Tests](#to-run-and-create-unittests) -- [Admin Site Titles](#changing-the-site-titles) -- [Blog Api](#creating-the-blog-api) -- [Blog Admin](#blog-admin) +# Mastori: A SpaceYaTech Blog App made in Django +Mastori is a community-driven open-source project that aims to provide a simple and efficient blogging platform built with the Django Rest Framework. -## Overview +# Overview The SpaceYaTech Content Management system is an open-application that lets users to quickly publish content and share it with ease to their audience. Inspired by existing CMSes like Hashnode, Wordpress, DEV and Joomla, we felt the need to create an African CMS created by young Africans looking to learn by contributing to Open Source. SpaceYaTech opted for a CMS as the debut open source project because of the technicalities involved in creating, maintaining and scaling a CMS. A CMS poses great technical challenges and a great learning opportunity for those looking to grow their tech skills. -For a more detailed overview of the project, read through the [CMS Backend wiki](https://github.com/SpaceyaTech/CMS-Backend-Repository/wiki) - -## Contribution guide - -Get to read the [Contributions guide](https://github.com/SpaceyaTech/Team-Rio-Django/blob/main/contributions.md) here. - -## Commit message template - -Just so that we have all our commit messages to be more readable and sensible it is recomended we use a template for the commit messages. Here is a [commit message template](https://github.com/SpaceyaTech/Team-Rio-Django/wiki/Commit-Messages) that one should follow when making your Contributions - -## PhoneNumberField - -In order to use **PhoneNumber_field** for localization option, perform the following: - -- Install phonenumber minimal metadata - -```bash -pip install "django-phonenumber-field[phonenumberslite]" -``` - -or - -- Install phonenumber extended features (e.g. geocoding) - -```bash -pip install "django-phonenumber-field[phonenumbers]" -``` - -- Add `phonenumber_field` to the list of the installed apps in your `settings.py` file INSTALLED_APPS: - -```python -INSTALLED_APPS = [ - # Other apps… - "phonenumber_field", -] -``` - -- Model field section add the following: - -```python -from phonenumber_field.modelfields import PhoneNumberField - -phone_number = PhoneNumberField(blank=True) -``` - - -## AUTHENTICATION +For a more detailed overview of the project, read through the [CMS Backend wiki](https://github.com/SpaceyaTech/CMS-Backend-Repository/wiki) + +The project is designed to help developers build their own blogging website or add blogging functionality to an the SpaceYaTech website with ease. +# Endpoints +> + +# Features +Mastori provides the following features: + +* Create, edit and delete blog posts +* Publish, unpublish or delete blog posts +* Tagging and categorizing posts +* Searching for posts by title, content, tags or categories +* User authentication and authorization +* User profile management +* Installation +> To install Mastori, follow these steps: + +- Clone the repository: + ```bash + + git clone https://github.com/yourusername/mastori.git + ``` +- Create a virtual environment and activate it: + + ```bash + + python -m venv env + source env/bin/activate + ``` +- Install the required packages: + ```bash + pip install -r requirements.txt + ``` +- Set up the database: + ```bash + python manage.py migrate + ``` +- Create a superuser: + ```bash + python manage.py createsuperuser + ``` +- Run the server: + ```bash + python manage.py runserver + ``` +# Usage +Once the server is running, you can access the API at `http://localhost:8000/api/`. You can use any HTTP client to interact with the API, such as curl or httpie. Alternatively, you can use the built-in API explorer by navigating to `http://localhost:8000/api/docs/` in your web browser. + +To access the admin panel, navigate to `http://localhost:8000/admin/` and log in using the credentials of the superuser you created earlier. + +## The blog Api -For authentication you follow this instructions: - -- Since we are using djangorest framework. Install the django rest framework library and add it to the INSTALLED_APPS as a third party app. - -- Create the serializers by adding a new file in your accounts app and name it serializers.py - -- In the serializers file create the needed serializers in this case , UserSerializer, RegisterSerializer, AddAccountSerializer. - -- Create the api views in the views.py file in the accounts app. In this case the UserView, RegisterView, AddAccountView - -- Create the corresponding urls by creating a urls.py file in your accounts app. Make sure to include your app urls in your projects urls.py file in the following way - -```python -from django.urls import path, include - -urlpatterns = [ - #Other patterns - - path("", include("accounts.urls")) -] -``` - -- Install the djangorestframework jwt library +The blog api **{{baseurl}}/blog/** +shows a list of all available blog posts (Stori/Mastori) +The model naming is abitrary and can be subject to change if need be +Ther is also need to filter out the various blogposts in relation ti their tittle or date posted hence the filter -```bash -pip install djangorestframework_simplejwt -``` +![Screenshot from 2023-01-03 00-59-16](https://user-images.githubusercontent.com/23496280/210282497-96bb8b6f-544d-4454-8b01-e3aea9b8745d.png) -- In your projects settings.py configure the REST_FRAMEWORK settings to use JWT and set the AUTH_HEADER_TYPE as JWT. For the access token lifetime i've set it to 1 day for testing purposes. -```python -REST_FRAMEWORK = { - 'DEFAULT_AUTHENTICATION_CLASSES': ( - 'rest_framework_simplejwt.authentication.JWTAuthentication', - ), -} +# Contributing +We welcome contributions from the community. To contribute, follow these steps: -SIMPLE_JWT = { - 'AUTH_HEADER_TYPES': ('JWT',), - 'ACCESS_TOKEN_LIFETIME': timedelta(days=1), -} +* Fork the repository +* Create a new branch +* Make your changes and commit them +* Push your changes to your forked repository +* Create a pull request -``` +Please make sure to follow the coding style and conventions used in the project. -- In your urls.py add the following: +Get to read the [Contributions guide](https://github.com/SpaceyaTech/Team-Rio-Django/blob/main/contributions.md) here. -```python -from rest_framework_simplejwt import views as jwt_views +## Commit message template -urlpatterns = [ - #Other patterns +Just so that we have all our commit messages to be more readable and sensible it is recomended we use a template for the commit messages. Here is a [commit message template](https://github.com/SpaceyaTech/Team-Rio-Django/wiki/Commit-Messages) that one should follow when making your Contributions - path('api/token/', jwt_views.TokenObtainPairView.as_view(), name='token_obtain_pair'), - path('api/token/refresh/', jwt_views.TokenRefreshView.as_view(), name='token_refresh'), - ] +# License +Mastori is licensed under the MIT License. See LICENSE for more information. -``` +# Contributor Features +> Enviroment setup By @wanjirumurira [82d55a4](https://github.com/SpaceyaTech/blog/commit/82d55a45ea0421c3918a7b2ae2b5808486f879a3) -- Add various permissions to your apis in the views.py file. +> Project Setup By @sangkips [82bc556](https://github.com/SpaceyaTech/blog/commit/82bc556935ac134024b659233e012bb5c5da4fda) -## To Run and Create unittests +> User & Accounts By @hellen-22 [1818b63](https://github.com/SpaceyaTech/blog/commit/1818b6304d203d0077bf54ff67151756db5d324a) -Add your tests in the test file in the test folder which is located in the app you want to test. -To run all tests -```python -python3 manage.py test -``` -To run tests in a particular app +> PhoneNumberField By @sangkips [615c011](https://github.com/SpaceyaTech/blog/commit/615c01194cebc205a743451bea9c5164e74bdf75) -```bash -python3 manage.py test [appname] -``` -## Changing the site titles -Having the site titles for the project to read **SpacryaTech** -![image](https://user-images.githubusercontent.com/23496280/204856386-3105fb57-a020-47c7-a789-8943099f3e44.png) +> Authentication (JWT) By @hellen-22 [c2d7a90](https://github.com/SpaceyaTech/blog/commit/c2d7a90c9e644ba1c49ab02eb43d6e08da7022a3) -## Creating the blog Api +> Throttling policy By @Collins-Omariba [9159a8e](https://github.com/SpaceyaTech/blog/commit/9159a8ed389b3c7482c4b60c5fdc5576013bafd3) -The blog api **{{baseurl}}/blog/** -shows a list of all available blog posts (Stori/Mastori) -The model naming is abitrary and can be subject to change if need be -Ther is also need to filter out the various blogposts in relation ti their tittle or date posted hence the filter +> Verification richtext editor By @aibunny [25f991b](https://github.com/SpaceyaTech/blog/commit/25f991bed6aa163fe464a6bf2b28ccb6bbd82630) -![Screenshot from 2023-01-03 00-59-16](https://user-images.githubusercontent.com/23496280/210282497-96bb8b6f-544d-4454-8b01-e3aea9b8745d.png) +> Fixed Workflow Build Error (commit #100) By @mosesmbadi [b40a5b4](https://github.com/SpaceyaTech/blog/commit/b40a5b4e0bdadb22bf099c8829d6d6e4dcc91fe7) -## Blog admin +> Nested Comments By @aibunny [bbab06c](https://github.com/SpaceyaTech/blog/commit/bbab06c95da6ec1506469aa1d3652b7b52c17a6f) -Here the implementation is more similar to the api but for the admin the search fields are title and content -```python - search_fields = ['title', 'content'] -``` -theres also an addition of a slug field this is in anticipation of creation of the detail view -the slug could be used to generate more elaborate urls for specific blogposts (mastori) -Filtering has also been implemented here by filtering on the basis of the post ststus -```python - list_filter = ("status",) - ``` - here a post is either a draft or published -![Screenshot from 2023-01-03 01-00-46](https://user-images.githubusercontent.com/23496280/210282501-cfb7ebf1-c95b-48c2-96dc-407000045a00.png) +## Authentication +[JWT Authentication by:](https://github.com/SpaceyaTech/Team-Rio-Django/wiki/Authentication) [Hellen](@hellen-22) +## Phone Numbers +## Api Throttling +[Api Throttling by:](https://github.com/SpaceyaTech/Team-Rio-Django/wiki/API-THROTTLING)[Collins](@Collins-Omariba) +## Nested Comments +[Nested Comments by:](https://github.com/SpaceyaTech/Team-Rio-Django/wiki/Nested-Comments) [Fredrick](@aibunny) +# Contributors diff --git a/accounts/migrations/0001_initial.py b/accounts/migrations/0001_initial.py index 84b444f..c752996 100644 --- a/accounts/migrations/0001_initial.py +++ b/accounts/migrations/0001_initial.py @@ -1,9 +1,8 @@ - # Generated by Django 4.1.3 on 2023-03-03 12:25 - from django.db import migrations class Migration(migrations.Migration): dependencies = [] + operations = [] From 7c2be7e1ffed5c2da6da56e0c581716b9b438f0d Mon Sep 17 00:00:00 2001 From: Jimmy Oty Date: Sun, 12 Mar 2023 13:24:08 +0300 Subject: [PATCH 02/25] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f353507..232870f 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ![Django](https://img.shields.io/badge/Django-092E20?style=for-the-badge&logo=django&logoColor=white) ![postgresql](https://img.shields.io/badge/PostgreSQL-316192?style=for-the-badge&logo=postgresql&logoColor=white) -[![Django CI](https://github.com/SpaceyaTech/Team-Rio-Django/actions/workflows/django.yml/badge.svg?event=push)](https://github.com/SpaceyaTech/Team-Rio-Django/actions/workflows/django.yml) +[![CI Django & Postgres Tests](https://github.com/SpaceyaTech/blog/actions/workflows/django-postgres-ci.yml/badge.svg)](https://github.com/SpaceyaTech/blog/actions/workflows/django-postgres-ci.yml) # Mastori: A SpaceYaTech Blog App made in Django Mastori is a community-driven open-source project that aims to provide a simple and efficient blogging platform built with the Django Rest Framework. From 9f953238bcb98994bf8e1e013826bfeeaf16947b Mon Sep 17 00:00:00 2001 From: Jimmy Oty Date: Mon, 13 Mar 2023 00:45:14 +0300 Subject: [PATCH 03/25] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 232870f..1d6e874 100644 --- a/README.md +++ b/README.md @@ -119,4 +119,6 @@ Mastori is licensed under the MIT License. See LICENSE for more information. [Api Throttling by:](https://github.com/SpaceyaTech/Team-Rio-Django/wiki/API-THROTTLING)[Collins](@Collins-Omariba) ## Nested Comments [Nested Comments by:](https://github.com/SpaceyaTech/Team-Rio-Django/wiki/Nested-Comments) [Fredrick](@aibunny) + # Contributors +[![Contributors](https://contrib.rocks/image?repo=SpaceyaTech/blog)](https://github.com/SpaceyaTech/blog/graphs/contributors) From c7a9cb62a95afe9f14a15dbb368a9f31a5e2e48f Mon Sep 17 00:00:00 2001 From: Moses Mbadi Date: Mon, 13 Mar 2023 08:59:45 +0300 Subject: [PATCH 04/25] Containerized blog --- Dockerfile | 7 +++++++ docker-compose.yml | 8 ++++++++ 2 files changed, 15 insertions(+) create mode 100644 Dockerfile create mode 100644 docker-compose.yml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..7190339 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,7 @@ +FROM python:3 +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 +WORKDIR /code +COPY requirements.txt /code/ +RUN pip install -r requirements.txt +COPY . /code/ \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..3f65de1 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,8 @@ +services: + web: + build: . + command: python manage.py runserver 0.0.0.0:8000 + volumes: + - .:/code + ports: + - "8000:8000" From 18f4b7e7ef82a13a0f7e3d11b53edcd15a099022 Mon Sep 17 00:00:00 2001 From: Jimmy Oty Date: Sat, 18 Mar 2023 09:21:59 +0300 Subject: [PATCH 05/25] Documentation update (#110) (#50) * Documentation (#45) * Added comments for the blog * Add created_at & updated_at in Category model. * Added verification code that resets * Added verification code that resets * Added password validation * Code change to follow PEP 8 * Delete auto-assign.yml * Delete assign.yml * Delete auto-assign.yml * Delete assign.yml * test comment model (#16) * Add Date Published feature to blog (#18) * Add Date Published feature to blog * Alter field status on stori * Change default for status field in stori * Reactions model (#19) * reactions model * Delete 0002_reaction.py * Create 0005_remove_stori_published_date_alter_stori_status_and_more.py * create postgresql database (#24) * Revert "create postgresql database (#24)" (#32) This reverts commit 97e25f0c9d003e26a0f393ce05e862f5c8705cc8. * Postgres env (#34) * Postgress Database setup with Environment Variables * Add psycopg2==2.9.5 to requirements.txt * workflow env * Add localhost to database host * add migrations * Worked on BlogService to enable fetch, create, update and deletion of blogs(stori) (#27) * delete migrations * Added Nested comments (#31) the parent_comment relates to comments and a reply it gets. Child comments are comments under a parent comment. Checking for parent_comment existence makes sure that there can only exist one parent comment and any other replies under parent_comment is a child. * Update Readme --------- * Update README.md * Update README.md --------- Signed-off-by: JimmyTron Co-authored-by: madbunny Co-authored-by: Collins Omariba Co-authored-by: raykipkorir Co-authored-by: Hellen Wainaina Co-authored-by: Moshood Owolabi <98819100+Msdot001@users.noreply.github.com> Co-authored-by: Omariba Collins <88287473+Collins-Omariba@users.noreply.github.com> Co-authored-by: Kipkoech Sang Co-authored-by: Eugene Kwaka Co-authored-by: Bunny <98146814+aibunny@users.noreply.github.com> From 5b6f6fbdff8711a381dee88178c81d1a4bc0331d Mon Sep 17 00:00:00 2001 From: Gibson-Gichuru Date: Sat, 18 Mar 2023 09:25:09 +0300 Subject: [PATCH 06/25] Application containerization --- .dockerignore | 3 +++ Dockerfile | 27 ++++++++++++++++++++++++ compose.yml | 58 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 88 insertions(+) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 compose.yml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..71d676c --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +.env +env/ +Dockerfile \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..5227356 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,27 @@ +FROM python:3.10-alpine + +WORKDIR /app + +ADD requirements.txt /app/requirements.txt + +RUN set -ex \ + && apk add --no-cache --virtual .build-deps postgresql-dev build-base \ + && python -m venv /env \ + && /env/bin/pip install --upgrade pip \ + && /env/bin/pip install --no-cache-dir -r /app/requirements.txt \ + && runDeps="$(scanelf --needed --nobanner --recursive /env \ + | awk '{ gsub(/,/, "\nso:", $2); print "so:" $2 }' \ + | sort -u \ + | xargs -r apk info --installed \ + | sort -u)" \ + && apk add --virtual rundeps $runDeps \ + && apk del .build-deps + +COPY . ./ + +ENV VIRTUAL_ENV /env +ENV PATH /env/bin:$PATH + +RUN chmod +x setup.sh + +CMD ["sh", "setup.sh"] diff --git a/compose.yml b/compose.yml new file mode 100644 index 0000000..9bf6b4a --- /dev/null +++ b/compose.yml @@ -0,0 +1,58 @@ +version: "3.8" + +services: + + application: + + build: . + + ports: + - 80:8000 + + depends_on: + + database: + + condition: service_healthy + + environment: + + - DATABASE_DB=${DATABASE_DB} + - DATABASE_USER=${DATABASE_USER} + - DATABASE_PASSWORD=${DATABASE_PASSWORD} + - DATABASE_HOST=${DATABASE_HOST} + - DATABASE_PORT=${DATABASE_PORT} + - ADMIN_USERNAME=${ADMIN_USERNAME} + - ADMIN_EMAIL=${ADMIN_EMAIL} + - ADMIN_PASSWORD=${ADMIN_PASSWORD} + + command: gunicorn --bind 0.0.0.0:8000 --workers ${WORKERS} CMS.wsgi + + database: + + image: postgres:alpine + + expose: + - 5432 + + environment: + - POSTGRES_USER=${DATABASE_USER} + - POSTGRES_PASSWORD=${DATABASE_PASSWORD} + - POSTGRES_DB=${DATABASE_DB} + + healthcheck: + test: ["CMD", "pg_isready",'-U${DATABASE_USER}', '-d${DATABASE_DB}'] + interval: 10s + timeout: 5s + retries: 5 + + volumes: + - type: volume + source: pgdata + target: /var/lib/postgresql/data + +volumes: + + pgdata: + + driver: local \ No newline at end of file From b8ad8de245435dbb0d4d1f510fa7f67004f6f2bd Mon Sep 17 00:00:00 2001 From: Gibson-Gichuru Date: Sat, 18 Mar 2023 09:25:56 +0300 Subject: [PATCH 07/25] Custom super user creation command this command is used to create a super user without prompting the user from the commandline command used while spining the application's container --- accounts/management/__init__.py | 0 accounts/management/commands/__init__.py | 0 accounts/management/commands/setup_admin.py | 30 +++++++++++++++++++++ 3 files changed, 30 insertions(+) create mode 100644 accounts/management/__init__.py create mode 100644 accounts/management/commands/__init__.py create mode 100644 accounts/management/commands/setup_admin.py diff --git a/accounts/management/__init__.py b/accounts/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/accounts/management/commands/__init__.py b/accounts/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/accounts/management/commands/setup_admin.py b/accounts/management/commands/setup_admin.py new file mode 100644 index 0000000..9d24fe4 --- /dev/null +++ b/accounts/management/commands/setup_admin.py @@ -0,0 +1,30 @@ +from django.contrib.auth import get_user_model +from django.core.management.base import BaseCommand + +class Command(BaseCommand): + + help = "create a super user non-interactively" + + + def add_arguments(self, parser): + + parser.add_argument("--username", help="Admin's username", required=True) + + parser.add_argument("--email", help="Admin's email", required=True) + + parser.add_argument("--password", help="Admin's password", required=True) + + + def handle(self, *args, **options): + + User = get_user_model() + + if not User.objects.filter( + username=options['username'] + ).exists(): + + User.objects.create_superuser( + username=options['username'], + email=options['email'], + password=options['password'] + ) \ No newline at end of file From d16921c01604208006ad9efecd4fcfe73b956bb5 Mon Sep 17 00:00:00 2001 From: Gibson-Gichuru Date: Sat, 18 Mar 2023 09:29:08 +0300 Subject: [PATCH 08/25] docker container migration and super user creation script --- setup.sh | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 setup.sh diff --git a/setup.sh b/setup.sh new file mode 100644 index 0000000..7f16c24 --- /dev/null +++ b/setup.sh @@ -0,0 +1,11 @@ +#!/bin/sh + +function deploy(){ + + python manage.py migrate + + python manage.py setup_admin --username ${ADMIN_USERNAME} --email ${ADMIN_EMAIL} --password ${ADMIN_PASSWORD} + +} + +deploy From 5fb346356caf0a3f033c6dc2e47176071bf5cbde Mon Sep 17 00:00:00 2001 From: Gibson-Gichuru Date: Sat, 18 Mar 2023 09:30:16 +0300 Subject: [PATCH 09/25] Application ENV support the application now fetches the env variables from .env file --- CMS/settings.py | 20 +++++++++++++++----- requirements.txt | 2 ++ 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/CMS/settings.py b/CMS/settings.py index 6b06d11..00cb832 100644 --- a/CMS/settings.py +++ b/CMS/settings.py @@ -12,8 +12,16 @@ from datetime import timedelta from pathlib import Path import os +import environ +# load environment variables +env_path = os.path.abspath(os.path.dirname("env")) +env = environ.Env() +environ.Env.read_env( + os.path.join(env_path, ".env") +) + # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent @@ -95,11 +103,11 @@ DATABASES = { 'default': { "ENGINE": "django.db.backends.postgresql_psycopg2", - "NAME": os.environ.get("DATABASE_DB"), - "USER": os.environ.get("DATABASE_USER"), - "PASSWORD": os.environ.get("DATABASE_PASSWORD"), - "HOST": os.environ.get("DATABASE_HOST"), - "PORT": os.environ.get("DATABASE_PORT"), + "NAME": env("DATABASE_DB"), + "USER": env("DATABASE_USER"), + "PASSWORD": env("DATABASE_PASSWORD"), + "HOST": env("DATABASE_HOST"), + "PORT": env("DATABASE_PORT"), } } @@ -236,3 +244,5 @@ # minimum user's password length during registration USER_PASSWORD_LENGTH = 8 + + diff --git a/requirements.txt b/requirements.txt index d4de9f5..5568268 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,6 +5,7 @@ click==8.1.3 Django==4.1.3 django-ckeditor==6.5.1 django-cors-headers==3.14.0 +django-environ==0.10.0 django-filter==22.1 django-jazzmin==2.6.0 django-js-asset==2.0.0 @@ -13,6 +14,7 @@ djangorestframework==3.14.0 djangorestframework-simplejwt==5.2.2 drf-nested-routers==0.93.4 exceptiongroup==1.1.0 +gunicorn==20.1.0 iniconfig==2.0.0 model-bakery==1.9.0 mypy-extensions==0.4.3 From f1c41a25046ad598594e29d64493bf944aeba7f3 Mon Sep 17 00:00:00 2001 From: Gibson-Gichuru Date: Sat, 18 Mar 2023 09:31:12 +0300 Subject: [PATCH 10/25] env sample file update --- .env.sample | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.env.sample b/.env.sample index 59386a9..93a8e0f 100644 --- a/.env.sample +++ b/.env.sample @@ -2,5 +2,11 @@ DATABASE_DB=dbname DATABASE_USER=dbuser DATABASE_PASSWORD=dbpassword +# set this to the name of the database container incase the application is run as a container DATABASE_HOST=localhost -DATABASE_PORT=5432 \ No newline at end of file +DATABASE_PORT=5432 +WORKERS= 1 # gunicorn number of workers set this to equal the number of available cpu cores * 1.5 +# while launching a container, a new super user is created if none exists +ADMIN_USERNAME=admin +ADMIN_EMAIL=adminemail +ADMIN_PASSWORD=adminpass \ No newline at end of file From 557761fa18768ed0b712654eb92df25428ca0464 Mon Sep 17 00:00:00 2001 From: Gibson-Gichuru Date: Sat, 18 Mar 2023 09:31:38 +0300 Subject: [PATCH 11/25] latest migrations --- .../0003_alter_user_verification_code.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 accounts/migrations/0003_alter_user_verification_code.py diff --git a/accounts/migrations/0003_alter_user_verification_code.py b/accounts/migrations/0003_alter_user_verification_code.py new file mode 100644 index 0000000..c99a9c5 --- /dev/null +++ b/accounts/migrations/0003_alter_user_verification_code.py @@ -0,0 +1,18 @@ +# Generated by Django 4.1.3 on 2023-03-18 06:11 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("accounts", "0002_initial"), + ] + + operations = [ + migrations.AlterField( + model_name="user", + name="verification_code", + field=models.CharField(default="794494", max_length=6, unique=True), + ), + ] From 93b8a227a21ee4e74b7ce41cd98cb1a438b03c8e Mon Sep 17 00:00:00 2001 From: Raymond Kipkorir Date: Sat, 18 Mar 2023 10:06:59 +0300 Subject: [PATCH 12/25] Added Password validation (#96) * Added password validation * Code change to follow PEP 8 --------- Co-authored-by: Jimmy Oty --- CMS/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMS/settings.py b/CMS/settings.py index 6b06d11..f1b1328 100644 --- a/CMS/settings.py +++ b/CMS/settings.py @@ -230,9 +230,9 @@ } + # cors configurations CORS_ALLOW_ALL_ORIGINS = True - # minimum user's password length during registration USER_PASSWORD_LENGTH = 8 From 3c9017d797372666caa71d289764028571b3bc03 Mon Sep 17 00:00:00 2001 From: Hellen Wainaina Date: Mon, 20 Mar 2023 10:08:51 +0300 Subject: [PATCH 13/25] enable creation of just one account --- accounts/serializers.py | 18 ------------------ accounts/urls.py | 7 +------ accounts/views.py | 12 ------------ 3 files changed, 1 insertion(+), 36 deletions(-) diff --git a/accounts/serializers.py b/accounts/serializers.py index 741aa46..01d3b6a 100644 --- a/accounts/serializers.py +++ b/accounts/serializers.py @@ -104,21 +104,3 @@ class Meta: model = Account fields = ['id', 'user', 'confirm_password', 'account_name', 'display_picture', 'bio'] - -class AddAccountSerializer(serializers.ModelSerializer): - """Serializer that enables addition of a new account to an existing user""" - - def create(self, validated_data): - user_id = self.context['user_id'] - account_name = validated_data['account_name'] - bio = validated_data['bio'] - - user = User.objects.get(pk=user_id) - - account = Account.objects.create(user=user, account_name=account_name, bio=bio) - - return account - - class Meta: - model = Account - fields = ['id','account_name', 'bio'] diff --git a/accounts/urls.py b/accounts/urls.py index add92cb..77eea33 100644 --- a/accounts/urls.py +++ b/accounts/urls.py @@ -6,9 +6,4 @@ router.register('users', views.UserViewSet, basename='users') router.register('register', views.RegisterAccountViewSet, basename='register') -"""Nesting to enable an account to be created under an existing user""" -users_router = routers.NestedDefaultRouter(router, 'users', lookup='user') -users_router.register('add_account', views.AddUserAccountViewSet, basename='add_account') - - -urlpatterns = router.urls + users_router.urls \ No newline at end of file +urlpatterns = router.urls \ No newline at end of file diff --git a/accounts/views.py b/accounts/views.py index d0811e8..a03251a 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -58,15 +58,3 @@ class RegisterAccountViewSet(CreateModelMixin, GenericViewSet): throttle_classes = [UserRateThrottle, AccountsRateThrottle] -class AddUserAccountViewSet(ModelViewSet): - """Api view for a user to add another new account""" - - serializer_class = AddAccountSerializer - permission_classes = [IsAuthenticated] - throttle_classes = [UserRateThrottle, AccountsRateThrottle] - - def get_serializer_context(self): - return {'user_id': self.kwargs['user_pk']} - - def get_queryset(self): - return Account.objects.filter(user_id=self.kwargs['user_pk']) From 6c7f40f2f2f03349f476903f7459ce446d1236a7 Mon Sep 17 00:00:00 2001 From: Gibson-Gichuru Date: Mon, 20 Mar 2023 19:06:15 +0300 Subject: [PATCH 14/25] Docker compose file update --- docker-compose.yml | 60 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 55 insertions(+), 5 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 3f65de1..9bf6b4a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,8 +1,58 @@ +version: "3.8" + services: - web: + + application: + build: . - command: python manage.py runserver 0.0.0.0:8000 - volumes: - - .:/code + ports: - - "8000:8000" + - 80:8000 + + depends_on: + + database: + + condition: service_healthy + + environment: + + - DATABASE_DB=${DATABASE_DB} + - DATABASE_USER=${DATABASE_USER} + - DATABASE_PASSWORD=${DATABASE_PASSWORD} + - DATABASE_HOST=${DATABASE_HOST} + - DATABASE_PORT=${DATABASE_PORT} + - ADMIN_USERNAME=${ADMIN_USERNAME} + - ADMIN_EMAIL=${ADMIN_EMAIL} + - ADMIN_PASSWORD=${ADMIN_PASSWORD} + + command: gunicorn --bind 0.0.0.0:8000 --workers ${WORKERS} CMS.wsgi + + database: + + image: postgres:alpine + + expose: + - 5432 + + environment: + - POSTGRES_USER=${DATABASE_USER} + - POSTGRES_PASSWORD=${DATABASE_PASSWORD} + - POSTGRES_DB=${DATABASE_DB} + + healthcheck: + test: ["CMD", "pg_isready",'-U${DATABASE_USER}', '-d${DATABASE_DB}'] + interval: 10s + timeout: 5s + retries: 5 + + volumes: + - type: volume + source: pgdata + target: /var/lib/postgresql/data + +volumes: + + pgdata: + + driver: local \ No newline at end of file From c72144d74988d6ec0b4a7c948d698c40e01806b8 Mon Sep 17 00:00:00 2001 From: Gibson-Gichuru Date: Tue, 21 Mar 2023 12:59:23 +0300 Subject: [PATCH 15/25] Celery Integration --- CMS/__init__.py | 3 +++ CMS/celery.py | 35 +++++++++++++++++++++++++++++++++++ CMS/settings.py | 5 +++++ compose.yml | 39 +++++++++++++++++++++++++++++---------- requirements.txt | 13 +++++++++++++ 5 files changed, 85 insertions(+), 10 deletions(-) create mode 100644 CMS/celery.py diff --git a/CMS/__init__.py b/CMS/__init__.py index e69de29..53af868 100644 --- a/CMS/__init__.py +++ b/CMS/__init__.py @@ -0,0 +1,3 @@ +from .celery import app as celery_app + +__all__ = ("celery_app") \ No newline at end of file diff --git a/CMS/celery.py b/CMS/celery.py new file mode 100644 index 0000000..8060f8d --- /dev/null +++ b/CMS/celery.py @@ -0,0 +1,35 @@ +import os +from celery import Celery + +# set the default value of DJANGO_SETTINGS so that celery can find the our django app +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "CMS.settings") + +# create Celery instance with 'celery_core' as the name. this name will be used to run our celery worker. +# eg >> cerely app=celery_core worker + +app = Celery("celery_core") + +# load celery config values from our django app +# settings the namespace assures that there would be no crashes with other DJANGO settings +# with the namespace set. any celery settings should be defined with CELERY_ prefix + +app.config_from_object("django.conf.settings", namespace="CELERY") + +# well we should be able to discover task within our django application + +""" +Any callable with shared_task would be discoverd as a task + +example + +from celery import shared_task + +@shared_task +def test_task(): + + print('this is a task') + + return True + +""" +app.autodiscover_tasks() \ No newline at end of file diff --git a/CMS/settings.py b/CMS/settings.py index 8da2368..d9f2940 100644 --- a/CMS/settings.py +++ b/CMS/settings.py @@ -245,4 +245,9 @@ # minimum user's password length during registration USER_PASSWORD_LENGTH = 8 +# celery configurations +# By default celery will use Redis as the message broker +# RabitMQ or AWS Simple Queue can be used as well +CELERY_BROKER_URL = os.environ.get("CELERY_BROKER", "redis://redis:6379/0") +CELERY_RESULT_BACKEND = os.environ.get("CELERY_BROKER", "redis://redis:6379/0") \ No newline at end of file diff --git a/compose.yml b/compose.yml index 9bf6b4a..fbdb917 100644 --- a/compose.yml +++ b/compose.yml @@ -1,5 +1,17 @@ version: "3.8" +x-environment: &commonEnvironment + - DATABASE_DB=${DATABASE_DB} + - DATABASE_USER=${DATABASE_USER} + - DATABASE_PASSWORD=${DATABASE_PASSWORD} + - DATABASE_HOST=database + - DATABASE_PORT=${DATABASE_PORT} + - ADMIN_USERNAME=${ADMIN_USERNAME} + - ADMIN_EMAIL=${ADMIN_EMAIL} + - ADMIN_PASSWORD=${ADMIN_PASSWORD} + - CELERY_BROKER=${CELERY_BROKER} + - SECRET_KEY=${SECRET_KEY} + services: application: @@ -15,19 +27,26 @@ services: condition: service_healthy - environment: - - - DATABASE_DB=${DATABASE_DB} - - DATABASE_USER=${DATABASE_USER} - - DATABASE_PASSWORD=${DATABASE_PASSWORD} - - DATABASE_HOST=${DATABASE_HOST} - - DATABASE_PORT=${DATABASE_PORT} - - ADMIN_USERNAME=${ADMIN_USERNAME} - - ADMIN_EMAIL=${ADMIN_EMAIL} - - ADMIN_PASSWORD=${ADMIN_PASSWORD} + environment: *commonEnvironment command: gunicorn --bind 0.0.0.0:8000 --workers ${WORKERS} CMS.wsgi + celery: + + build: . + + depends_on: + - application + - redis + + command: celery app=celery_core workers --loglevel=info + + environment: *commonEnvironment + + redis: + + image: redis:7-alpine + database: image: postgres:alpine diff --git a/requirements.txt b/requirements.txt index 5568268..a85e2f3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,14 @@ +amqp==5.1.1 asgiref==3.5.2 +async-timeout==4.0.2 attrs==22.2.0 +billiard==3.6.4.0 black==22.12.0 +celery==5.2.7 click==8.1.3 +click-didyoumean==0.3.0 +click-plugins==1.1.1 +click-repl==0.2.0 Django==4.1.3 django-ckeditor==6.5.1 django-cors-headers==3.14.0 @@ -16,6 +23,7 @@ drf-nested-routers==0.93.4 exceptiongroup==1.1.0 gunicorn==20.1.0 iniconfig==2.0.0 +kombu==5.2.4 model-bakery==1.9.0 mypy-extensions==0.4.3 packaging==23.0 @@ -24,11 +32,16 @@ phonenumbers==8.13.0 Pillow==9.3.0 platformdirs==2.6.2 pluggy==1.0.0 +prompt-toolkit==3.0.38 psycopg2==2.9.5 PyJWT==2.6.0 pytest==7.2.0 pytest-django==4.5.2 pytz==2022.6 +redis==4.5.2 +six==1.16.0 sqlparse==0.4.3 tomli==2.0.1 tzdata==2022.6 +vine==5.0.0 +wcwidth==0.2.6 From d54166e0b6ca098a8dd2b5982431ae82a1479964 Mon Sep 17 00:00:00 2001 From: Gibson-Gichuru Date: Wed, 22 Mar 2023 16:25:11 +0300 Subject: [PATCH 16/25] mail support init --- docker-compose.yml | 58 ----------------------- mail/__init__.py | 28 +++++++++++ mail/apps.py | 6 +++ mail/email.py | 43 +++++++++++++++++ mail/migrations/__init__.py | 0 mail/tasks.py | 37 +++++++++++++++ mail/templates/templated_email/test.email | 10 ++++ mail/tests.py | 28 +++++++++++ 8 files changed, 152 insertions(+), 58 deletions(-) delete mode 100644 docker-compose.yml create mode 100644 mail/__init__.py create mode 100644 mail/apps.py create mode 100644 mail/email.py create mode 100644 mail/migrations/__init__.py create mode 100644 mail/tasks.py create mode 100644 mail/templates/templated_email/test.email create mode 100644 mail/tests.py diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 9bf6b4a..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,58 +0,0 @@ -version: "3.8" - -services: - - application: - - build: . - - ports: - - 80:8000 - - depends_on: - - database: - - condition: service_healthy - - environment: - - - DATABASE_DB=${DATABASE_DB} - - DATABASE_USER=${DATABASE_USER} - - DATABASE_PASSWORD=${DATABASE_PASSWORD} - - DATABASE_HOST=${DATABASE_HOST} - - DATABASE_PORT=${DATABASE_PORT} - - ADMIN_USERNAME=${ADMIN_USERNAME} - - ADMIN_EMAIL=${ADMIN_EMAIL} - - ADMIN_PASSWORD=${ADMIN_PASSWORD} - - command: gunicorn --bind 0.0.0.0:8000 --workers ${WORKERS} CMS.wsgi - - database: - - image: postgres:alpine - - expose: - - 5432 - - environment: - - POSTGRES_USER=${DATABASE_USER} - - POSTGRES_PASSWORD=${DATABASE_PASSWORD} - - POSTGRES_DB=${DATABASE_DB} - - healthcheck: - test: ["CMD", "pg_isready",'-U${DATABASE_USER}', '-d${DATABASE_DB}'] - interval: 10s - timeout: 5s - retries: 5 - - volumes: - - type: volume - source: pgdata - target: /var/lib/postgresql/data - -volumes: - - pgdata: - - driver: local \ No newline at end of file diff --git a/mail/__init__.py b/mail/__init__.py new file mode 100644 index 0000000..98ac69e --- /dev/null +++ b/mail/__init__.py @@ -0,0 +1,28 @@ +""" +This Package handles spaceyatech blog mail services. + +The Package contains 1. email module which handles the mail logic + 2. templates/templated_email directory which is used to register email templates + +Registering Email templates + +email templates are just files that are used ...... + +as specied in the applications settings each email template file should have a .email extention + +Django templating style can be used to write a template file. + +example + +{% block subject %}My subject for {{username}}{% endblock %} +{% block plain %} + Hi {{full_name}}, + + You just signed up for my website, using: + username: {{username}} + join date: {{signup_date}} + + Thanks, you rock! +{% endblock %} + +""" \ No newline at end of file diff --git a/mail/apps.py b/mail/apps.py new file mode 100644 index 0000000..43a83ba --- /dev/null +++ b/mail/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class MailConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "mail" diff --git a/mail/email.py b/mail/email.py new file mode 100644 index 0000000..b3444fa --- /dev/null +++ b/mail/email.py @@ -0,0 +1,43 @@ +import os +from mail.tasks import _async_send_email +from django.conf import settings + +base_dir = os.path.abspath(os.path.dirname(__file__)) + +def send_email(context:dict, template:str, recipient:str) -> str: + + """ + Send templated emails to users + this function acts as a proxy to _async_send_email + + :param context: a dict that contains info to be passed to the template + :param template: some template + :recipient: recipient's email address + :return: a unique id from a scheduled task. + + usecase: suppose a user it to be send a confirmation email. + + >>> send_email( + context={username='JohnDoe', token='sometoken'}, + template=confirmation, + recipient='johnDoe@spaceyatech.com' + ) + + the function call schedules a task via the _async_send_email function + and returns a unique id pointing to the given task or raise a + FileNotFoundError if the template passed is not found in + ../blog/mail/templates/tempated_email/ directory + """ + + if not os.path.exists(os.path.join(base_dir,f"templates/templated_email/{template}.email")): + + raise FileNotFoundError(f"template file {template} not found") + + task = _async_send_email.delay( + context=context, + template=template, + from_email=settings.EMAIL_HOST_USER, + recipients=[recipient] + ) + + return task.id diff --git a/mail/migrations/__init__.py b/mail/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mail/tasks.py b/mail/tasks.py new file mode 100644 index 0000000..4df1b60 --- /dev/null +++ b/mail/tasks.py @@ -0,0 +1,37 @@ +from templated_email import send_templated_mail +from celery import shared_task + +@shared_task +def _async_send_email(context:dict, template:str, from_email:str, recipients:list[str]) ->None: + + """ + Schedule a task to send an email + + :param context: a dict that contains info to be passed to the template + :param template: some template + :param recipients: a list containing recipients' email addresses + + usage: _async_send_email.delay(context, template, from_email, recipients) + + the above call should return a unique id for the task scheduled + + the template passed should exist in the ../blog/mail/templates/tempated_email/ directory + + otherwise to handle FileNotFound exceptions on the current django process use send_email + + """ + + send_templated_mail( + from_email=from_email, + recipient_list=recipients, + context=context, + template_name=template, + ) + +def _error_callback(): + + pass + +def _success_callback(): + + pass \ No newline at end of file diff --git a/mail/templates/templated_email/test.email b/mail/templates/templated_email/test.email new file mode 100644 index 0000000..7490f63 --- /dev/null +++ b/mail/templates/templated_email/test.email @@ -0,0 +1,10 @@ +{% block subject %}My subject for {{username}}{% endblock %} +{% block plain %} + Hi {{full_name}}, + + You just signed up for my website, using: + username: {{username}} + join date: {{signup_date}} + + Thanks, you rock! +{% endblock %} \ No newline at end of file diff --git a/mail/tests.py b/mail/tests.py new file mode 100644 index 0000000..f54f197 --- /dev/null +++ b/mail/tests.py @@ -0,0 +1,28 @@ +from django.test import TestCase +from unittest.mock import patch +from mail.email import send_email +# Create your tests here. + +class TestMail(TestCase): + + @patch("mail.email.os", autospec=True) + @patch("mail.email._async_send_email", autospec=True) + def test_send_email(self, send_temp_mock, os_mock): + + recipients = ["test"] + + context = {} + + send_email(context=context, template="test", recipients=recipients) + + send_temp_mock.delay.assert_called_with( + context=context, + template="test", + recipients = recipients, + ) + + os_mock.path.exists.return_value = False + + with self.assertRaises(FileNotFoundError): + + send_email(context=context, template="test", recipients=recipients) From 408a8a19173d86bf913771e23147527804d61ed6 Mon Sep 17 00:00:00 2001 From: Gibson-Gichuru Date: Wed, 22 Mar 2023 16:26:11 +0300 Subject: [PATCH 17/25] Compose environment refactor --- compose.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/compose.yml b/compose.yml index fbdb917..435b8f2 100644 --- a/compose.yml +++ b/compose.yml @@ -11,6 +11,13 @@ x-environment: &commonEnvironment - ADMIN_PASSWORD=${ADMIN_PASSWORD} - CELERY_BROKER=${CELERY_BROKER} - SECRET_KEY=${SECRET_KEY} + - EMAIL_HOST=${EMAIL_HOST} + - EMAIL_USE_TLS=${EMAIL_USE_TLS} + - EMAIL_PORT=${EMAIL_PORT} + - EMAIL_USE_SSL=${EMAIL_USE_SSL} + - EMAIL_HOST_USER=${EMAIL_HOST_USER} + - EMAIL_HOST_PASSWORD=${EMAIL_HOST_PASSWORD} + - EMAIL_SENDER=${EMAIL_SENDER} services: @@ -39,7 +46,7 @@ services: - application - redis - command: celery app=celery_core workers --loglevel=info + command: celery --app=CMS worker --loglevel=info environment: *commonEnvironment From a4c2f4bae0d33834eadecd0f8746c1cd08ca3ffe Mon Sep 17 00:00:00 2001 From: Gibson-Gichuru Date: Wed, 22 Mar 2023 16:26:48 +0300 Subject: [PATCH 18/25] celery update --- CMS/__init__.py | 4 ++-- CMS/celery.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CMS/__init__.py b/CMS/__init__.py index 53af868..2ee6585 100644 --- a/CMS/__init__.py +++ b/CMS/__init__.py @@ -1,3 +1,3 @@ -from .celery import app as celery_app +from CMS.celery import app as celery_app -__all__ = ("celery_app") \ No newline at end of file +__all__ = ("celery_app",) \ No newline at end of file diff --git a/CMS/celery.py b/CMS/celery.py index 8060f8d..538d92c 100644 --- a/CMS/celery.py +++ b/CMS/celery.py @@ -7,7 +7,7 @@ # create Celery instance with 'celery_core' as the name. this name will be used to run our celery worker. # eg >> cerely app=celery_core worker -app = Celery("celery_core") +app = Celery("CMS") # load celery config values from our django app # settings the namespace assures that there would be no crashes with other DJANGO settings From 1a9dff2186f2d95d19cd84c84db11bcafa600170 Mon Sep 17 00:00:00 2001 From: Gibson-Gichuru Date: Wed, 22 Mar 2023 16:27:09 +0300 Subject: [PATCH 19/25] celery update --- CMS/settings.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/CMS/settings.py b/CMS/settings.py index d9f2940..45c7faf 100644 --- a/CMS/settings.py +++ b/CMS/settings.py @@ -52,6 +52,7 @@ 'rest_framework', 'accounts', 'blog', + 'mail', 'phonenumber_field', #Adding the django filters module 'django_filters', @@ -248,6 +249,18 @@ # celery configurations # By default celery will use Redis as the message broker # RabitMQ or AWS Simple Queue can be used as well -CELERY_BROKER_URL = os.environ.get("CELERY_BROKER", "redis://redis:6379/0") +CELERY_BROKER_URL = os.environ.get('CELERY_BROKER', 'redis://redis:6379/0') -CELERY_RESULT_BACKEND = os.environ.get("CELERY_BROKER", "redis://redis:6379/0") \ No newline at end of file +CELERY_RESULT_BACKEND = os.environ.get('CELERY_BROKER', 'redis://redis:6379/0') + +# mail service configuation + +TEMPLATED_EMAIL_BACKEND = 'templated_email.backends.vanilla_django.TemplateBackend' + +TEMPLATED_EMAIL_FILE_EXTENSION = 'email' + +EMAIL_HOST = env("EMAIL_HOST") +EMAIL_PORT = env("EMAIL_PORT") +EMAIL_USE_SSL = True +EMAIL_HOST_USER = env("EMAIL_HOST_USER") +EMAIL_HOST_PASSWORD = env("EMAIL_HOST_PASSWORD") \ No newline at end of file From 5251ecfbe36ef6b8833bcc63e3aec6d7292a7293 Mon Sep 17 00:00:00 2001 From: Gibson-Gichuru Date: Wed, 22 Mar 2023 16:27:50 +0300 Subject: [PATCH 20/25] .env.sample update --- .env.sample | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/.env.sample b/.env.sample index 93a8e0f..d6b2c91 100644 --- a/.env.sample +++ b/.env.sample @@ -1,3 +1,4 @@ +SECRET_KEY="some key" # :) ofcourse this should be secret # DATABASE DATABASE_DB=dbname DATABASE_USER=dbuser @@ -9,4 +10,17 @@ WORKERS= 1 # gunicorn number of workers set this to equal the number of availabl # while launching a container, a new super user is created if none exists ADMIN_USERNAME=admin ADMIN_EMAIL=adminemail -ADMIN_PASSWORD=adminpass \ No newline at end of file +ADMIN_PASSWORD=adminpass +# Celery broker can be set to any message broker compatible with celery i.e RabitMQ or AWS Simple Queue +# when not set CELERY_BROKER defaults to redis as the message broker +CELERY_BROKER="redis://redis:port/" +EMAIL_HOST="smtp.yourserver.com" +EMAIL_USE_TLS=False +EMAIL_PORT=465 +EMAIL_USE_SSL=True +# smtp server username +EMAIL_HOST_USER="yourname" +# smtp server password +EMAIL_HOST_PASSWORD="password" +# the sender of the email. in most cases this would equal to EMAIL_HOST_USER +EMAIL_SENDER="emailsender@yourserver.com" \ No newline at end of file From a82d305aa86c486db01868850ac982e335fffbaf Mon Sep 17 00:00:00 2001 From: Gibson-Gichuru Date: Thu, 23 Mar 2023 14:29:09 +0300 Subject: [PATCH 21/25] Code documentation --- CMS/celery.py | 22 +++++++++------------- CMS/settings.py | 2 +- mail/__init__.py | 17 ++++++++--------- mail/email.py | 33 +++++++++++++++++---------------- mail/tasks.py | 35 ++++++++++++++++++++++------------- mail/tests.py | 14 ++++++++++---- 6 files changed, 67 insertions(+), 56 deletions(-) diff --git a/CMS/celery.py b/CMS/celery.py index 538d92c..25e80c8 100644 --- a/CMS/celery.py +++ b/CMS/celery.py @@ -1,35 +1,31 @@ import os from celery import Celery -# set the default value of DJANGO_SETTINGS so that celery can find the our django app +# Set the default value of DJANGO_SETTINGS_MODULE so that Celery can find our Django app. os.environ.setdefault("DJANGO_SETTINGS_MODULE", "CMS.settings") -# create Celery instance with 'celery_core' as the name. this name will be used to run our celery worker. -# eg >> cerely app=celery_core worker +# Create a Celery instance with 'CMS' as the name. This name will be used to run our Celery worker. +# E.g. >> celery app=CMS worker app = Celery("CMS") -# load celery config values from our django app -# settings the namespace assures that there would be no crashes with other DJANGO settings -# with the namespace set. any celery settings should be defined with CELERY_ prefix +# Load Celery config values from our Django app settings. The namespace assures that there would be no crashes +# with other Django settings. With the namespace set, any Celery settings should be defined with the CELERY_ prefix. app.config_from_object("django.conf.settings", namespace="CELERY") -# well we should be able to discover task within our django application +# We should be able to discover tasks within our Django application. """ -Any callable with shared_task would be discoverd as a task +Any callable with shared_task would be discovered as a task. -example +Example: from celery import shared_task @shared_task def test_task(): - - print('this is a task') - + print('This is a task') return True - """ app.autodiscover_tasks() \ No newline at end of file diff --git a/CMS/settings.py b/CMS/settings.py index 45c7faf..a655f93 100644 --- a/CMS/settings.py +++ b/CMS/settings.py @@ -261,6 +261,6 @@ EMAIL_HOST = env("EMAIL_HOST") EMAIL_PORT = env("EMAIL_PORT") -EMAIL_USE_SSL = True +EMAIL_USE_TLS = True EMAIL_HOST_USER = env("EMAIL_HOST_USER") EMAIL_HOST_PASSWORD = env("EMAIL_HOST_PASSWORD") \ No newline at end of file diff --git a/mail/__init__.py b/mail/__init__.py index 98ac69e..c25da24 100644 --- a/mail/__init__.py +++ b/mail/__init__.py @@ -1,18 +1,18 @@ """ -This Package handles spaceyatech blog mail services. +This package handles Spaceyatech blog mail services. -The Package contains 1. email module which handles the mail logic - 2. templates/templated_email directory which is used to register email templates +The package contains: +1. An email module which handles the mail logic. +2. A templates/templated_email directory which is used to register email templates. -Registering Email templates +Registering Email Templates: -email templates are just files that are used ...... - -as specied in the applications settings each email template file should have a .email extention +Email templates are just files that are used to standardize the format and content of emails sent by the application. +As specified in the application's settings, each email template file should have a .email extension. Django templating style can be used to write a template file. -example +Example: {% block subject %}My subject for {{username}}{% endblock %} {% block plain %} @@ -24,5 +24,4 @@ Thanks, you rock! {% endblock %} - """ \ No newline at end of file diff --git a/mail/email.py b/mail/email.py index b3444fa..4ed1d3f 100644 --- a/mail/email.py +++ b/mail/email.py @@ -7,26 +7,27 @@ def send_email(context:dict, template:str, recipient:str) -> str: """ - Send templated emails to users - this function acts as a proxy to _async_send_email + Send templated emails to users. + This function acts as a proxy to `_async_send_email`. - :param context: a dict that contains info to be passed to the template - :param template: some template - :recipient: recipient's email address - :return: a unique id from a scheduled task. + :param context: A dictionary that contains information to be passed to the template. + :param template: A string that represents the name of the template to be used for the email. + :param recipient: A string that represents the email address of the recipient. + :return: A unique ID from a scheduled task. - usecase: suppose a user it to be send a confirmation email. + Use case: Suppose a user is to be sent a confirmation email. + Example usage: >>> send_email( - context={username='JohnDoe', token='sometoken'}, - template=confirmation, - recipient='johnDoe@spaceyatech.com' - ) - - the function call schedules a task via the _async_send_email function - and returns a unique id pointing to the given task or raise a - FileNotFoundError if the template passed is not found in - ../blog/mail/templates/tempated_email/ directory + context={'username': 'JohnDoe', 'token': 'sometoken'}, + template='confirmation', + recipient='johnDoe@spaceyatech.com' + ) + + The function call schedules a task via the `_async_send_email` function + and returns a unique ID pointing to the given task or raises a + `FileNotFoundError` if the template passed is not found in the + `../blog/mail/templates/templated_email/` directory. """ if not os.path.exists(os.path.join(base_dir,f"templates/templated_email/{template}.email")): diff --git a/mail/tasks.py b/mail/tasks.py index 4df1b60..93a60f8 100644 --- a/mail/tasks.py +++ b/mail/tasks.py @@ -1,24 +1,29 @@ from templated_email import send_templated_mail from celery import shared_task +from celery.signals import task_failure @shared_task def _async_send_email(context:dict, template:str, from_email:str, recipients:list[str]) ->None: """ - Schedule a task to send an email + Schedule a task to send an email asynchronously. - :param context: a dict that contains info to be passed to the template - :param template: some template - :param recipients: a list containing recipients' email addresses + Args: + context (dict): A dictionary containing information to be passed to the email template. + template (str): The name of the email template file. + from_email (str): The email address of the sender. + recipients (list[str]): A list of email addresses of the recipients. - usage: _async_send_email.delay(context, template, from_email, recipients) + Usage: + _async_send_email.delay(context, template, from_email, recipients) - the above call should return a unique id for the task scheduled - - the template passed should exist in the ../blog/mail/templates/tempated_email/ directory - - otherwise to handle FileNotFound exceptions on the current django process use send_email + Returns: + None. + Note: + The "send_templated_mail" function is used to send the email using the provided information. + This function is asynchronous and runs in the background, which allows the application to continue processing + without waiting for the email to be sent. """ send_templated_mail( @@ -28,10 +33,14 @@ def _async_send_email(context:dict, template:str, from_email:str, recipients:lis template_name=template, ) -def _error_callback(): +def _on_mail_error_callback(sender=None, task_id=None, exeption=None, **kwargs): + + # This function is a placeholder for handling errors that may occur during email sending. + # It is not yet implemented, so it currently does nothing. + + # However, the plan is to eventually use a global application logging service to log any errors that occur during email sending. pass -def _success_callback(): - pass \ No newline at end of file +task_failure.connect(_on_mail_error_callback, sender=_async_send_email) \ No newline at end of file diff --git a/mail/tests.py b/mail/tests.py index f54f197..cbd3873 100644 --- a/mail/tests.py +++ b/mail/tests.py @@ -1,6 +1,7 @@ from django.test import TestCase from unittest.mock import patch from mail.email import send_email +from django.conf import settings # Create your tests here. class TestMail(TestCase): @@ -9,20 +10,25 @@ class TestMail(TestCase): @patch("mail.email._async_send_email", autospec=True) def test_send_email(self, send_temp_mock, os_mock): - recipients = ["test"] + recipient = "test" context = {} - send_email(context=context, template="test", recipients=recipients) + send_email(context=context, template="test", recipient=recipient) send_temp_mock.delay.assert_called_with( context=context, template="test", - recipients = recipients, + from_email=settings.EMAIL_HOST_USER, + recipients = [recipient], ) os_mock.path.exists.return_value = False with self.assertRaises(FileNotFoundError): - send_email(context=context, template="test", recipients=recipients) + send_email( + context=context, + template="test", + recipient=recipient + ) From a101293aef940cd5b53282d09ce1c114c92849a0 Mon Sep 17 00:00:00 2001 From: Gibson-Gichuru Date: Thu, 23 Mar 2023 14:29:50 +0300 Subject: [PATCH 22/25] email template sample --- mail/templates/templated_email/test.email | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mail/templates/templated_email/test.email b/mail/templates/templated_email/test.email index 7490f63..3f848e0 100644 --- a/mail/templates/templated_email/test.email +++ b/mail/templates/templated_email/test.email @@ -2,7 +2,8 @@ {% block plain %} Hi {{full_name}}, - You just signed up for my website, using: + Hey You! This should work as an example template + username: {{username}} join date: {{signup_date}} From 6b0dee176c33e6716c1f3c63a2f02730516b591f Mon Sep 17 00:00:00 2001 From: Gibson-Gichuru Date: Thu, 23 Mar 2023 15:10:35 +0300 Subject: [PATCH 23/25] CI/CD env update --- .github/workflows/django-postgres-ci.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/django-postgres-ci.yml b/.github/workflows/django-postgres-ci.yml index 58ac225..3c868c5 100644 --- a/.github/workflows/django-postgres-ci.yml +++ b/.github/workflows/django-postgres-ci.yml @@ -50,6 +50,11 @@ jobs: DATABASE_DB: djtesting DATABASE_PORT: 5432 DATABASE_HOST: localhost + EMAIL_HOST: test + EMAIL_PORT: 123 + EMAIL_HOST_USER: test + EMAIL_HOST_PASSWORD: test + run: | python manage.py migrate python manage.py test From 260b0ece15d6a4da862e9df192070e4bccfecf3b Mon Sep 17 00:00:00 2001 From: Gibson-Gichuru Date: Thu, 23 Mar 2023 15:16:46 +0300 Subject: [PATCH 24/25] dependencies list update --- requirements.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/requirements.txt b/requirements.txt index a85e2f3..4c710f9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,6 +17,8 @@ django-filter==22.1 django-jazzmin==2.6.0 django-js-asset==2.0.0 django-phonenumber-field==7.0.0 +django-render-block==0.9.2 +django-templated-email==3.0.1 djangorestframework==3.14.0 djangorestframework-simplejwt==5.2.2 drf-nested-routers==0.93.4 From fe1f1f95ffeafcbfdca2df59c1ccf614816c114e Mon Sep 17 00:00:00 2001 From: Gibson-Gichuru Date: Thu, 23 Mar 2023 16:28:24 +0300 Subject: [PATCH 25/25] env.sample update --- .env.sample | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.env.sample b/.env.sample index d6b2c91..2c9433d 100644 --- a/.env.sample +++ b/.env.sample @@ -15,9 +15,9 @@ ADMIN_PASSWORD=adminpass # when not set CELERY_BROKER defaults to redis as the message broker CELERY_BROKER="redis://redis:port/" EMAIL_HOST="smtp.yourserver.com" -EMAIL_USE_TLS=False -EMAIL_PORT=465 -EMAIL_USE_SSL=True +# Make sure you update your firewall rules to allow outgoing/incoming traffic on EMAIL_PORT +EMAIL_USE_TLS=True +EMAIL_PORT=587 # smtp server username EMAIL_HOST_USER="yourname" # smtp server password