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