diff --git a/.gitignore b/.gitignore
index 256dc36..e393685 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,9 +1,164 @@
-Include/
-Lib/
-Scripts/
+bin/
+static/
+static-cdn-local
+# Byte-compiled / optimized / DLL files
__pycache__/
+*.py[cod]
+*$py.class
-*.env
+# C extensions
+*.so
-staticfiles/
\ No newline at end of file
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+cover/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+.pybuilder/
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+# For a library or package, you might want to ignore these files since the code is
+# intended to run in multiple environments; otherwise, check them in:
+# .python-version
+
+# pipenv
+# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+# However, in case of collaboration, if having platform-specific dependencies or dependencies
+# having no cross-platform support, pipenv may install dependencies that don't work, or not
+# install all needed dependencies.
+#Pipfile.lock
+
+# poetry
+# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
+# This is especially recommended for binary packages to ensure reproducibility, and is more
+# commonly ignored for libraries.
+# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
+#poetry.lock
+
+# pdm
+# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
+#pdm.lock
+# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
+# in version control.
+# https://pdm.fming.dev/#use-with-ide
+.pdm.toml
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# pytype static type analyzer
+.pytype/
+
+# Cython debug symbols
+cython_debug/
+
+# PyCharm
+# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
+# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
+# and can be added to the global gitignore or merged into this file. For a more nuclear
+# option (not recommended) you can uncomment the following to ignore the entire idea folder.
+#.idea/
\ No newline at end of file
diff --git a/Procfile b/Procfile
index 48301a7..2fd4371 100644
--- a/Procfile
+++ b/Procfile
@@ -1,2 +1,2 @@
-release: bash ./release-tasks.sh
+release: bash commands/release-tasks.sh
web: gunicorn blog.wsgi --log-file -
\ No newline at end of file
diff --git a/README.md b/README.md
index 8e9d26a..378cfc7 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,3 @@
-
# Blog Backend
Simple Content Management System build on Django and Django Rest Framework.
@@ -7,14 +6,14 @@ Simple Content Management System build on Django and Django Rest Framework.
- Clone Repo
-```
+```bash
mkdir blog_backend
cd blog_backend
git clone https://github.com/Arvind-4/Blog-Backend.git .
```
- Install Virtual Environment and Dependencies
-```
+```bash
pip install virtualenv
python -m venv .
source Scripts/activate
@@ -22,14 +21,14 @@ pip install -r requirements.txt
```
-> **_NOTE:_** To Install Dev Dependencies run command
+
- Run Local Server
-```
+```bash
python manage.py runserver
-```
+```
\ No newline at end of file
diff --git a/blog/asgi.py b/blog/asgi.py
index a1e27d4..39f1f40 100644
--- a/blog/asgi.py
+++ b/blog/asgi.py
@@ -11,6 +11,6 @@
from django.core.asgi import get_asgi_application
-os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'blog.settings')
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "blog.settings")
application = get_asgi_application()
diff --git a/blog/settings/__init__.py b/blog/settings/__init__.py
index 3b89387..d4d39b0 100644
--- a/blog/settings/__init__.py
+++ b/blog/settings/__init__.py
@@ -1 +1,11 @@
-from .production import *
\ No newline at end of file
+from blog.env import config
+
+DJANGO_LIVE = config("DJANGO_LIVE", cast=bool)
+
+if DJANGO_LIVE:
+ print("Loading production settings...")
+ from blog.settings.production import *
+
+else:
+ print("Loading developement settings...")
+ from blog.settings.dev import *
diff --git a/blog/settings/base.py b/blog/settings/base.py
index d05832b..36165a7 100644
--- a/blog/settings/base.py
+++ b/blog/settings/base.py
@@ -11,83 +11,57 @@
"""
from pathlib import Path
-import os
-import sys
-
-from django.conf import settings
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent.parent
-# print('The Testion is ', TESTING, sys.argv[1])
-
# Application definition
INSTALLED_APPS = [
- 'django.contrib.admin',
- 'django.contrib.auth',
- 'django.contrib.contenttypes',
- 'django.contrib.sessions',
- 'django.contrib.messages',
-
- 'whitenoise.runserver_nostatic',
-
- 'django.contrib.staticfiles',
-
- 'rest_framework',
- 'corsheaders',
-
- 'posts',
+ "django.contrib.admin",
+ "django.contrib.auth",
+ "django.contrib.contenttypes",
+ "django.contrib.sessions",
+ "django.contrib.messages",
+ "django.contrib.staticfiles",
+ "rest_framework",
+ "drf_spectacular",
+ "corsheaders",
+ "posts",
]
MIDDLEWARE = [
- 'django.middleware.security.SecurityMiddleware',
- 'whitenoise.middleware.WhiteNoiseMiddleware',
- 'corsheaders.middleware.CorsMiddleware',
- 'django_brotli.middleware.BrotliMiddleware',
- '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',
+ "django.middleware.security.SecurityMiddleware",
+ "whitenoise.middleware.WhiteNoiseMiddleware",
+ "corsheaders.middleware.CorsMiddleware",
+ "django.contrib.sessions.middleware.SessionMiddleware",
+ "django.middleware.common.CommonMiddleware",
+ "django.middleware.csrf.CsrfViewMiddleware",
+ "django.contrib.auth.middleware.AuthenticationMiddleware",
+ "django.contrib.messages.middleware.MessageMiddleware",
+ "django.middleware.clickjacking.XFrameOptionsMiddleware",
]
-ROOT_URLCONF = 'blog.urls'
+ROOT_URLCONF = "blog.urls"
TEMPLATES = [
{
- 'BACKEND': 'django.template.backends.django.DjangoTemplates',
- 'DIRS': [],
- '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',
+ "BACKEND": "django.template.backends.django.DjangoTemplates",
+ "DIRS": [],
+ "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 = 'blog.wsgi.application'
-
-
-# Database
-# https://docs.djangoproject.com/en/3.2/ref/settings/#databases
-
-
-DATABASES = {
- 'default': {
- 'ENGINE': 'django.db.backends.sqlite3',
- 'NAME': BASE_DIR / 'db.sqlite3',
- 'TEST': {
- 'ENGINE': 'django.db.backends.sqlite3',
- 'NAME': BASE_DIR / 'testdb.sqlite3',
- }
- }
-}
+WSGI_APPLICATION = "blog.wsgi.application"
+ASGI_APPLICATION = "blog.asgi.application"
# Password validation
@@ -95,16 +69,16 @@
AUTH_PASSWORD_VALIDATORS = [
{
- 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
+ "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
},
{
- 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
+ "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
},
{
- 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
+ "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
},
{
- 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
+ "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
},
]
@@ -112,9 +86,9 @@
# Internationalization
# https://docs.djangoproject.com/en/3.2/topics/i18n/
-LANGUAGE_CODE = 'en-us'
+LANGUAGE_CODE = "en-us"
-TIME_ZONE = 'UTC'
+TIME_ZONE = "Asia/Kolkata"
USE_I18N = True
@@ -126,9 +100,9 @@
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.2/howto/static-files/
-STATIC_URL = '/static/'
+STATIC_URL = "/static/"
# Default primary key field type
# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field
-DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
\ No newline at end of file
+DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
diff --git a/blog/settings/production.py b/blog/settings/production.py
index 4c94cb0..89c5f0e 100644
--- a/blog/settings/production.py
+++ b/blog/settings/production.py
@@ -1,35 +1,31 @@
-from .base import *
+from blog.env import config
+from blog.settings.base import *
-import os
-import django_heroku
-import dj_database_url
+MEDIA_URL = "/media/"
-# CORS_ALLOWED_ORIGINS = [str('FRONTEND_URL')]
+STATIC_ROOT = BASE_DIR / "static-cdn-local"
+MEDIA_ROOT = BASE_DIR / "media-cdn-local"
-STATIC_ROOT = BASE_DIR / 'static'
+DEBUG = config("DJANGO_DEBUG", cast=bool)
+SECRET_KEY = config("DJANGO_SECRET_KEY", cast=str)
+ADMIN_URL = config("DJANGO_ADMIN_URL", cast=str)
-STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
-MEDIA_URL = '/media/'
+CORS_ALLOWED_ORIGINS = []
+CORS_ALLOWED_ORIGINS.extend(
+ config(
+ "DJANGO_CORS_ALLOWED_ORIGINS", cast=lambda v: [s.strip() for s in v.split(",")]
+ )
+)
-MEDIA_ROOT = BASE_DIR / 'media'
-SECRET_KEY = str(os.environ.get('SECRET_KEY'))
+ALLOWED_HOSTS = []
+ALLOWED_HOSTS.extend(
+ config("DJANGO_ALLOWED_HOSTS", cast=lambda v: [s.strip() for s in v.split(",")])
+)
-ADMIN_URL = str(os.environ.get('ADMIN_URL'))
+from blog.db import * # noqa
-DEBUG = False
-
-ALLOWED_HOSTS = ['.herokuapp.com']
-
-db_from_env = dj_database_url.config(conn_max_age=500)
-DATABASES['default'].update(db_from_env)
-DATABASES['default']['ATOMIC_REQUESTS'] = True
-
-SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
-SESSION_COOKIE_SECURE = True
-CSRF_COOKIE_SECURE = True
-SECURE_SSL_REDIRECT = True
-
-if 'HEROKU' in os.environ:
- django_heroku.settings(locals())
\ No newline at end of file
+DJANGO_LIVE = config("DJANGO_LIVE", cast=bool)
+if DJANGO_LIVE:
+ from blog.https import * # noqa
diff --git a/blog/urls.py b/blog/urls.py
index 4d5c0f4..3c43f11 100644
--- a/blog/urls.py
+++ b/blog/urls.py
@@ -13,13 +13,24 @@
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
+from django.contrib import admin
from django.conf import settings
+from django.urls import path, include
from django.conf.urls.static import static
+from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView
+drf_spectacular_patterns = [
+ path('api/schema/', SpectacularAPIView.as_view(), name='schema'),
+ path('api/schema/swagger-ui/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'),
+ path('api/schema/redoc/', SpectacularRedocView.as_view(url_name='schema'), name='redoc'),
+]
+
+
urlpatterns = [
path(settings.ADMIN_URL, admin.site.urls),
- path('api/posts/', include('posts.api.urls')),
-] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
\ No newline at end of file
+ path("api/posts/", include("posts.api.urls")),
+] + drf_spectacular_patterns
+
+if settings.DEBUG:
+ urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
diff --git a/blog/wsgi.py b/blog/wsgi.py
index 9376d9a..e063bf6 100644
--- a/blog/wsgi.py
+++ b/blog/wsgi.py
@@ -11,6 +11,6 @@
from django.core.wsgi import get_wsgi_application
-os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'blog.settings')
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "blog.settings")
application = get_wsgi_application()
diff --git a/manage.py b/manage.py
index 31b2300..75a510d 100644
--- a/manage.py
+++ b/manage.py
@@ -2,13 +2,11 @@
"""Django's command-line utility for administrative tasks."""
import os
import sys
-import dotenv
def main():
- dotenv.read_dotenv()
"""Run administrative tasks."""
- os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'blog.settings')
+ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "blog.settings")
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
@@ -20,5 +18,5 @@ def main():
execute_from_command_line(sys.argv)
-if __name__ == '__main__':
+if __name__ == "__main__":
main()
diff --git a/posts/admin.py b/posts/admin.py
index 254ce5b..3f2131b 100644
--- a/posts/admin.py
+++ b/posts/admin.py
@@ -4,13 +4,20 @@
# Register your models here.
+
class PostAdmin(admin.ModelAdmin):
- list_display = ['id', '__str__','updated_at', 'created_at', ]
- search_fields = ['id', 'title', 'author']
+ list_display = [
+ "id",
+ "__str__",
+ "updated_at",
+ "created_at",
+ ]
+ search_fields = ["id", "title", "author"]
+
+ class Meta:
+ model = Post
- class Meta:
- model = Post
admin.site.register(Post, PostAdmin)
-admin.site.register(Tag)
\ No newline at end of file
+admin.site.register(Tag)
diff --git a/posts/api/serializers.py b/posts/api/serializers.py
index 54611c3..3371b36 100644
--- a/posts/api/serializers.py
+++ b/posts/api/serializers.py
@@ -2,38 +2,42 @@
from posts.models import Post, Tag
+
class TagSerializer(serializers.ModelSerializer):
class Meta:
model = Tag
- fields = ('tag',)
+ fields = ("tag",)
+
class PostSerializer(serializers.ModelSerializer):
tags = serializers.SerializerMethodField()
+
class Meta:
model = Post
fields = (
- 'title',
- 'slug',
- 'author',
- 'image_field',
- 'tags',
- 'width_field',
- 'height_field',
- 'image_alt',
- 'reading_time',
- 'short_summary',
- 'content',
- 'draft',
- 'created_at',
- 'updated_at',
+ "title",
+ "slug",
+ "author",
+ "image_field",
+ "tags",
+ "width_field",
+ "height_field",
+ "image_alt",
+ "reading_time",
+ "short_summary",
+ "content",
+ "draft",
+ "created_at",
+ "updated_at",
)
-
+
def get_tags(self, obj, *args, **kwargs):
- obj = Post.objects.filter(slug=obj.slug)
- if obj.exists():
- qs_ = obj.first()
- qs = qs_.tags.all()
- else:
- qs = []
- response = TagSerializer(qs, many=True).data
- return response
+ try:
+ _obj = Post.objects.get(slug__iexact=obj.slug)
+ return TagSerializer(_obj.tags.all(), many=True).data
+ except Post.DoesNotExist:
+ return TagSerializer([], many=True).data
+ except Post.MultipleObjectsReturned:
+ return TagSerializer([], many=True).data
+ except Exception as e:
+ return TagSerializer([], many=True).data
diff --git a/posts/api/urls.py b/posts/api/urls.py
index 61ec76f..a6f1472 100644
--- a/posts/api/urls.py
+++ b/posts/api/urls.py
@@ -8,14 +8,10 @@
)
urlpatterns = [
-
# Posts
-
- path('list-view/', PostListAPIView.as_view(), name='list_view'),
- path('/detail-view/', SinglePostAPIView.as_view(), name='detail_view'),
-
+ path("list-view/", PostListAPIView.as_view(), name="list_view"),
+ path("/detail-view/", SinglePostAPIView.as_view(), name="detail_view"),
# Tags
-
- path('tags/lists/', TagListView.as_view(), name='tags_list'),
- path('tags//', TagAPIView.as_view(), name='tags_filter'),
-]
\ No newline at end of file
+ path("tags/lists/", TagListView.as_view(), name="tags_list"),
+ path("tags//", TagAPIView.as_view(), name="tags_filter"),
+]
diff --git a/posts/api/views.py b/posts/api/views.py
index 8bff746..f257680 100644
--- a/posts/api/views.py
+++ b/posts/api/views.py
@@ -7,34 +7,40 @@
from posts.models import Post, Tag
from .serializers import PostSerializer, TagSerializer
+
class PostListAPIView(ListAPIView):
queryset = Post.objects.all()[:5]
serializer_class = PostSerializer
+
class SinglePostAPIView(RetrieveAPIView):
def get_queryset(self, *args, **kwargs):
- slug = self.kwargs.get('slug')
+ slug = self.kwargs.get("slug")
if slug:
- qs = Post.objects.filter(slug__iexact=slug)
+ qs = Post.objects.filter(slug__iexact=slug)
if qs.exists():
obj = qs
else:
obj = []
return obj
+
serializer_class = PostSerializer
- lookup_field = 'slug'
+ lookup_field = "slug"
+
class TagAPIView(ListAPIView):
def get_queryset(self, *args, **kwargs):
- slug = self.kwargs.get('slug')
+ slug = self.kwargs.get("slug")
if slug:
qs = Post.objects.filter(tags__tag__icontains=slug)
if qs.exists():
return qs
- else:
+ else:
return []
+
serializer_class = PostSerializer
+
class TagListView(ListAPIView):
queryset = Tag.objects.all()
- serializer_class = TagSerializer
\ No newline at end of file
+ serializer_class = TagSerializer
diff --git a/posts/apps.py b/posts/apps.py
index 2c2b982..d39b3e1 100644
--- a/posts/apps.py
+++ b/posts/apps.py
@@ -2,4 +2,4 @@
class PostsConfig(AppConfig):
- name = 'posts'
+ name = "posts"
diff --git a/posts/migrations/0001_initial.py b/posts/migrations/0001_initial.py
index 5b0d9dc..2ba91df 100644
--- a/posts/migrations/0001_initial.py
+++ b/posts/migrations/0001_initial.py
@@ -8,41 +8,74 @@ class Migration(migrations.Migration):
initial = True
- dependencies = [
- ]
+ dependencies = []
operations = [
migrations.CreateModel(
- name='Tag',
+ name="Tag",
fields=[
- ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('tag', models.CharField(max_length=50)),
+ (
+ "id",
+ models.BigAutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("tag", models.CharField(max_length=50)),
],
),
migrations.CreateModel(
- name='Post',
+ name="Post",
fields=[
- ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('title', models.CharField(max_length=225, unique=True)),
- ('slug', models.SlugField(blank=True, null=True, unique=True)),
- ('author', models.CharField(max_length=225)),
- ('author_profile_image', models.ImageField(blank=True, height_field='height_field_author', null=True, upload_to=posts.models.author_profile_images_directory_path, width_field='width_field_author')),
- ('width_field_author', models.IntegerField(default=100)),
- ('height_field_author', models.IntegerField(default=100)),
- ('image_field', models.ImageField(blank=True, height_field='height_field', null=True, upload_to=posts.models.post_images_directory_path, width_field='width_field')),
- ('width_field', models.IntegerField(default=400)),
- ('height_field', models.IntegerField(default=400)),
- ('image_alt', models.CharField(blank=True, max_length=225)),
- ('reading_time', models.CharField(blank=True, max_length=225)),
- ('short_summary', models.TextField()),
- ('content', models.TextField()),
- ('draft', models.BooleanField(default=False)),
- ('created_at', models.DateTimeField(auto_now_add=True)),
- ('updated_at', models.DateTimeField(auto_now=True)),
- ('tags', models.ManyToManyField(blank=True, to='posts.Tag')),
+ (
+ "id",
+ models.BigAutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("title", models.CharField(max_length=225, unique=True)),
+ ("slug", models.SlugField(blank=True, null=True, unique=True)),
+ ("author", models.CharField(max_length=225)),
+ (
+ "author_profile_image",
+ models.ImageField(
+ blank=True,
+ height_field="height_field_author",
+ null=True,
+ upload_to=posts.models.author_profile_images_directory_path,
+ width_field="width_field_author",
+ ),
+ ),
+ ("width_field_author", models.IntegerField(default=100)),
+ ("height_field_author", models.IntegerField(default=100)),
+ (
+ "image_field",
+ models.ImageField(
+ blank=True,
+ height_field="height_field",
+ null=True,
+ upload_to=posts.models.post_images_directory_path,
+ width_field="width_field",
+ ),
+ ),
+ ("width_field", models.IntegerField(default=400)),
+ ("height_field", models.IntegerField(default=400)),
+ ("image_alt", models.CharField(blank=True, max_length=225)),
+ ("reading_time", models.CharField(blank=True, max_length=225)),
+ ("short_summary", models.TextField()),
+ ("content", models.TextField()),
+ ("draft", models.BooleanField(default=False)),
+ ("created_at", models.DateTimeField(auto_now_add=True)),
+ ("updated_at", models.DateTimeField(auto_now=True)),
+ ("tags", models.ManyToManyField(blank=True, to="posts.Tag")),
],
options={
- 'ordering': ['-created_at', '-updated_at'],
+ "ordering": ["-created_at", "-updated_at"],
},
),
]
diff --git a/posts/migrations/0002_alter_post_draft.py b/posts/migrations/0002_alter_post_draft.py
new file mode 100644
index 0000000..fa35433
--- /dev/null
+++ b/posts/migrations/0002_alter_post_draft.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.2.25 on 2024-03-11 09:14
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('posts', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='post',
+ name='draft',
+ field=models.BooleanField(default=True),
+ ),
+ ]
diff --git a/posts/models.py b/posts/models.py
index 668970d..c645ff5 100644
--- a/posts/models.py
+++ b/posts/models.py
@@ -1,13 +1,28 @@
-import uuid
from django.db import models
-from django.conf import settings
-from django.db.models.signals import pre_save
from django.utils.text import slugify
+from django.db.models.signals import pre_save
-from .utils import get_read_time
+from posts.utils import (
+ get_read_time,
+ save_images,
+)
# Create your models here.
+post_images_directory_name = "post-images"
+author_profile_images_directory_name = "author-profile-images"
+
+
+def post_images_directory_path(instance, file_name):
+ return save_images(file_name=file_name, directory_name=post_images_directory_name)
+
+
+def author_profile_images_directory_path(instance, file_name):
+ return save_images(
+ file_name=file_name, directory_name=author_profile_images_directory_name
+ )
+
+
class Tag(models.Model):
tag = models.CharField(max_length=50)
@@ -18,24 +33,31 @@ def save(self, *args, **kwargs):
self.tag = slugify(self.tag)
return super(Tag, self).save(*args, **kwargs)
-def post_images_directory_path(instance, filename):
- return '/'.join(['PostImages', str(uuid.uuid4().hex + ".png")])
-
-def author_profile_images_directory_path(instance, filename):
- return '/'.join(['AuthorProfileImage', str(uuid.uuid4().hex + ".png")])
class Post(models.Model):
title = models.CharField(max_length=225, unique=True)
slug = models.SlugField(null=True, blank=True, unique=True)
author = models.CharField(max_length=225)
- author_profile_image = models.ImageField(blank=True, null=True, width_field='width_field_author', height_field='height_field_author', upload_to=author_profile_images_directory_path)
+ author_profile_image = models.ImageField(
+ blank=True,
+ null=True,
+ width_field="width_field_author",
+ height_field="height_field_author",
+ upload_to=author_profile_images_directory_path,
+ )
width_field_author = models.IntegerField(default=100)
height_field_author = models.IntegerField(default=100)
- image_field = models.ImageField(blank=True, null=True, width_field='width_field', height_field='height_field', upload_to=post_images_directory_path)
-
+ image_field = models.ImageField(
+ blank=True,
+ null=True,
+ width_field="width_field",
+ height_field="height_field",
+ upload_to=post_images_directory_path,
+ )
+
tags = models.ManyToManyField(Tag, blank=True)
width_field = models.IntegerField(default=400)
height_field = models.IntegerField(default=400)
@@ -43,17 +65,16 @@ class Post(models.Model):
reading_time = models.CharField(max_length=225, blank=True)
short_summary = models.TextField()
content = models.TextField()
- draft = models.BooleanField(default=False)
+ draft = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
- return f'{self.title} | {self.author}'
+ return f"{self.title} | {self.author}"
class Meta:
- ordering = ['-created_at', '-updated_at']
+ ordering = ["-created_at", "-updated_at"]
-# Signals
def pre_save_slug_and_image_alt_generator(sender, instance, *args, **kwargs):
if not instance.slug:
@@ -61,12 +82,13 @@ def pre_save_slug_and_image_alt_generator(sender, instance, *args, **kwargs):
instance.image_alt = slugify(instance.title)
instance.save()
-pre_save.connect(pre_save_slug_and_image_alt_generator, sender=Post)
def pre_save_reading_time_generator(sender, instance, *args, **kwargs):
if not instance.reading_time:
generated_time = get_read_time(str(instance.content))
- instance.reading_time = f'{generated_time} min'
+ instance.reading_time = f"{generated_time} min"
instance.save()
-pre_save.connect(pre_save_reading_time_generator, sender=Post)
\ No newline at end of file
+
+pre_save.connect(pre_save_slug_and_image_alt_generator, sender=Post)
+pre_save.connect(pre_save_reading_time_generator, sender=Post)
diff --git a/posts/tests.py b/posts/tests.py
index 67bfd9c..ce8ecdc 100644
--- a/posts/tests.py
+++ b/posts/tests.py
@@ -6,15 +6,16 @@
# Create your tests here.
+
class BlogPostTestCase(TestCase):
def setUp(self, *args, **kwargs):
self.client = Client()
def test_get_post_list_view(self, *args, **kwargs):
- response = self.client.get(reverse('list_view'), {}, follow=True)
+ response = self.client.get(reverse("list_view"), {}, follow=True)
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_get_tags_list_view(self, *args, **kwargs):
- response = self.client.get(reverse('tags_list'), {}, follow=True)
- self.assertEqual(response.status_code, status.HTTP_200_OK)
\ No newline at end of file
+ response = self.client.get(reverse("tags_list"), {}, follow=True)
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
diff --git a/posts/utils.py b/posts/utils.py
index 618e2fc..e37087d 100644
--- a/posts/utils.py
+++ b/posts/utils.py
@@ -1,17 +1,29 @@
-import datetime
-import math
import re
-
+import uuid
+import math
+from typing import Tuple
from django.utils.html import strip_tags
+
def count_words(html_string):
word_string = strip_tags(html_string)
- matching_words = re.findall(r'\w+', word_string)
+ matching_words = re.findall(r"\w+", word_string)
count = len(matching_words)
return count
def get_read_time(html_string):
count = count_words(html_string)
- read_time_min = math.ceil(count/38.0)
- return int(read_time_min)
\ No newline at end of file
+ read_time_min = math.ceil(count / 38.0)
+ return int(read_time_min)
+
+
+def get_image_filename(file_name) -> Tuple[str, str]:
+ extension = file_name.split(".")[-1]
+ _file_name = file_name.split(".")[0]
+ return _file_name, extension
+
+
+def save_images(file_name, directory_name):
+ _file_name, extension = get_image_filename(file_name)
+ return "/".join([directory_name, f"{_file_name}-{uuid.uuid4().hex}.{extension}"])
diff --git a/pyvenv.cfg b/pyvenv.cfg
index f5ffe96..202de59 100644
--- a/pyvenv.cfg
+++ b/pyvenv.cfg
@@ -1,3 +1,8 @@
-home = E:\Python39
+home = /usr/local/python/3.10.13/bin
+implementation = CPython
+version_info = 3.10.13.final.0
+virtualenv = 20.25.1
include-system-site-packages = false
-version = 3.9.2
+base-prefix = /usr/local/python/3.10.13
+base-exec-prefix = /usr/local/python/3.10.13
+base-executable = /usr/local/python/3.10.13/bin/python3.10
diff --git a/requirements.txt b/requirements.txt
index c9d8668..101e119 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,15 +1,19 @@
-asgiref==3.4.1
-Brotli==1.0.9
-dj-database-url==0.5.0
-Django==3.2.9
-django-brotli==0.2.0
-django-cors-headers==3.10.0
-django-dotenv==1.4.2
-django-heroku==0.3.1
-djangorestframework==3.12.4
-gunicorn==20.1.0
-Pillow==8.4.0
-psycopg2==2.9.2
-pytz==2021.3
-sqlparse==0.4.2
-whitenoise==5.3.0
+asgiref==3.7.2 ; python_version >= "3.10" and python_version < "4.0"
+attrs==23.2.0 ; python_version >= "3.10" and python_version < "4.0"
+django-cors-headers==4.3.1 ; python_version >= "3.10" and python_version < "4.0"
+django==3.2.25 ; python_version >= "3.10" and python_version < "4.0"
+djangorestframework==3.14.0 ; python_version >= "3.10" and python_version < "4.0"
+drf-spectacular==0.27.1 ; python_version >= "3.10" and python_version < "4.0"
+inflection==0.5.1 ; python_version >= "3.10" and python_version < "4.0"
+jsonschema-specifications==2023.12.1 ; python_version >= "3.10" and python_version < "4.0"
+jsonschema==4.21.1 ; python_version >= "3.10" and python_version < "4.0"
+pillow==10.2.0 ; python_version >= "3.10" and python_version < "4.0"
+python-decouple==3.8 ; python_version >= "3.10" and python_version < "4.0"
+pytz==2024.1 ; python_version >= "3.10" and python_version < "4.0"
+pyyaml==6.0.1 ; python_version >= "3.10" and python_version < "4.0"
+referencing==0.33.0 ; python_version >= "3.10" and python_version < "4.0"
+rpds-py==0.18.0 ; python_version >= "3.10" and python_version < "4.0"
+sqlparse==0.4.4 ; python_version >= "3.10" and python_version < "4.0"
+typing-extensions==4.10.0 ; python_version >= "3.10" and python_version < "3.11"
+uritemplate==4.1.1 ; python_version >= "3.10" and python_version < "4.0"
+whitenoise==6.6.0 ; python_version >= "3.10" and python_version < "4.0"
diff --git a/runtime.txt b/runtime.txt
index 9a60461..d1ba85d 100644
--- a/runtime.txt
+++ b/runtime.txt
@@ -1 +1 @@
-python-3.9.2
\ No newline at end of file
+python-3.10.13
\ No newline at end of file