Skip to content
Merged

Main #19

Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions apps/blog/admin.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
from django.contrib import admin
from .models import Post, PostComment, PostLike, PostDislike, PostCommentLike

from unfold.admin import ModelAdmin


@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
class PostAdmin(ModelAdmin):
list_display = ["title", "content", "author", "is_active"]
search_fields = ["title", "content"]
list_filter = ["author", "is_active"]
@@ -12,20 +14,20 @@ class PostAdmin(admin.ModelAdmin):


@admin.register(PostComment)
class PostCommentAdmin(admin.ModelAdmin):
class PostCommentAdmin(ModelAdmin):
pass


@admin.register(PostLike)
class PostLikeAdmin(admin.ModelAdmin):
class PostLikeAdmin(ModelAdmin):
pass


@admin.register(PostDislike)
class PostDislike(admin.ModelAdmin):
class PostDislike(ModelAdmin):
pass


@admin.register(PostCommentLike)
class PostCommentLikeAdmin(admin.ModelAdmin):
class PostCommentLikeAdmin(ModelAdmin):
pass
14 changes: 14 additions & 0 deletions apps/blog/filters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import django_filters

from .models import Post


class PostFilter(django_filters.FilterSet):
title = django_filters.CharFilter(lookup_expr="icontains")
description = django_filters.CharFilter(lookup_expr="icontains")
content = django_filters.CharFilter(lookup_expr="icontains")
created_at = django_filters.DateFromToRangeFilter()

class Meta:
model = Post
fields = ["title", "description", "content", "created_at"]
48 changes: 36 additions & 12 deletions apps/blog/utils.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,44 @@
from django.db.models import QuerySet
from django.db.models import Q
from django.core.paginator import Paginator, Page, EmptyPage, PageNotAnInteger
from django.contrib.postgres.search import SearchQuery, SearchRank, SearchVector
from django.core.paginator import (
Paginator,
Page,
EmptyPage,
PageNotAnInteger,
InvalidPage,
)
from django.conf import settings

from .models import PostLike, PostDislike, Post, PostComment


def get_search_model_queryset(
model_queryset: QuerySet, search_query: str = None
) -> QuerySet:
if not search_query:
def get_search_model_queryset(model_queryset: QuerySet, query: str = None) -> QuerySet:
if not query:
return model_queryset

search_query = model_queryset.filter(
Q(title__icontains=search_query)
| Q(description__icontains=search_query)
| Q(content__icontains=search_query)
)

return search_query
search_vector = SearchVector("title", "description", "content")
search_query = SearchQuery(query)

if settings.DATABASES["default"]["ENGINE"] == "django.db.backends.postgresql":
# PostgreSQL search
queryset = (
model_queryset.annotate(
search=search_vector,
rank=SearchRank(search_vector, search_query),
)
.filter(search=search_query)
.order_by("-rank")
)
else:
# SQLite3 search
queryset = model_queryset.filter(
Q(title__icontains=query)
| Q(description__icontains=query)
| Q(content__icontains=query)
)

return queryset


def get_pagination_obj(model_queryset: QuerySet, page: int = 1, size: int = 4) -> Page:
@@ -29,6 +50,9 @@ def get_pagination_obj(model_queryset: QuerySet, page: int = 1, size: int = 4) -
except PageNotAnInteger:
page_obj = paginator.page(1)

except InvalidPage:
page_obj = paginator.page(1)

except EmptyPage:
page_obj = paginator.page(paginator.num_pages)

1 change: 1 addition & 0 deletions apps/shared/management/commands/createadmin.py
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@
from django.core.management import BaseCommand

from dotenv import load_dotenv

load_dotenv()


11 changes: 7 additions & 4 deletions apps/users/admin.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin # noqa
from .models import User, UserProfile

from unfold.admin import ModelAdmin


@admin.register(User)
class UserAdmin(admin.ModelAdmin):
class UserAdmin(ModelAdmin):
list_display = ["username", "post_count"]
search_fields = ["first_name", "last_name", "username"]
search_fields = ["first_name", "last_name", "username", "email"]
list_display_links = ["username"]

def get_post_count(self):
return self.post_count


@admin.register(UserProfile)
class UserProfileAdmin(admin.ModelAdmin):
pass
class UserProfileAdmin(ModelAdmin):
list_display = ["user", "avatar", "bio"]
3 changes: 3 additions & 0 deletions apps/users/api_endpoints/users/User/tests.py
Original file line number Diff line number Diff line change
@@ -102,3 +102,6 @@ def _create_user():
_check_user_error_field()
_check_user_passwords_field()
_create_user()

def test_api_user_update(self):
pass
20 changes: 20 additions & 0 deletions apps/users/api_endpoints/users/UserProfile/tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from rest_framework.test import APITestCase
from rest_framework_simplejwt.tokens import RefreshToken
from apps.users.models import UserProfile, User # noqa


class UserProfileApiTestCase(APITestCase):
def setUp(self) -> None:
self.username = "Admin"
self.email = "admin@gmail.com"
self.password = "password"
user = User.objects.create(
username=self.username,
email=self.email,
)
user.set_password(self.password)
user.save()
self.user = user
refresh = RefreshToken.for_user(self.user)
self.token = str(refresh.access_token)
return super().setUp()
1 change: 1 addition & 0 deletions apps/users/middleware.py
Original file line number Diff line number Diff line change
@@ -10,6 +10,7 @@

class JWTAuthMiddleware(MiddlewareMixin):
def process_request(self, request):
# If admin auth for session
if request.path.startswith("/admin"):
return

20 changes: 10 additions & 10 deletions core/asgi.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
"""
ASGI config for config project.

It exposes the ASGI callable as a module-level variable named ``application``.

For more information on this file, see
https://docs.djangoproject.com/en/5.0/howto/deployment/asgi/
"""

import os

from django.core.asgi import get_asgi_application

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "core.settings")
from dotenv import load_dotenv

load_dotenv()

os.environ.setdefault(
"DJANGO_SETTINGS_MODULE",
os.getenv("DJANGO_SETTINGS_MODULE", "core.settings.development"),
)

application = get_asgi_application()

app = application
4 changes: 4 additions & 0 deletions core/config/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
from .apps import * # noqa
from .jwt import * # noqa
from .rest_framework import * # noqa
from .unfold_navigation import * # noqa
from .unfold import * # noqa

# from .cheditor5 import * # noqa
18 changes: 18 additions & 0 deletions core/config/apps.py
Original file line number Diff line number Diff line change
@@ -17,7 +17,25 @@
]

THIRD_PARTY_APPS = [
# Admin panel
"unfold",
"unfold.contrib.filters",
"unfold.contrib.forms",
"unfold.contrib.import_export",
"unfold.contrib.guardian",
"unfold.contrib.simple_history",
# Translation
"modeltranslation",
#
"django_ckeditor_5",
# Translation pannel
"rosetta",
# DRF Swaggers
"drf_spectacular",
"drf_spectacular_sidecar",
# Rest Framework
"rest_framework",
# Rest Framework JWT (Json web token)s
"rest_framework_simplejwt",
"rest_framework_simplejwt.token_blacklist",
]
140 changes: 140 additions & 0 deletions core/config/cheditor5.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
customColorPalette = [
{"color": "hsl(4, 90%, 58%)", "label": "Red"},
{"color": "hsl(340, 82%, 52%)", "label": "Pink"},
{"color": "hsl(291, 64%, 42%)", "label": "Purple"},
{"color": "hsl(262, 52%, 47%)", "label": "Deep Purple"},
{"color": "hsl(231, 48%, 48%)", "label": "Indigo"},
{"color": "hsl(207, 90%, 54%)", "label": "Blue"},
]

CKEDITOR_5_CONFIGS = {
"default": {
"toolbar": [
"heading",
"|",
"bold",
"italic",
"link",
"bulletedList",
"numberedList",
"blockQuote",
"imageUpload",
],
},
"extends": {
"blockToolbar": [
"paragraph",
"heading1",
"heading2",
"heading3",
"|",
"bulletedList",
"numberedList",
"|",
"blockQuote",
],
"toolbar": [
"heading",
"|",
"outdent",
"indent",
"|",
"bold",
"italic",
"link",
"underline",
"strikethrough",
"code",
"subscript",
"superscript",
"highlight",
"|",
"codeBlock",
"sourceEditing",
"insertImage",
"bulletedList",
"numberedList",
"todoList",
"|",
"blockQuote",
"imageUpload",
"|",
"fontSize",
"fontFamily",
"fontColor",
"fontBackgroundColor",
"mediaEmbed",
"removeFormat",
"insertTable",
],
"image": {
"toolbar": [
"imageTextAlternative",
"|",
"imageStyle:alignLeft",
"imageStyle:alignRight",
"imageStyle:alignCenter",
"imageStyle:side",
"|",
],
"styles": [
"full",
"side",
"alignLeft",
"alignRight",
"alignCenter",
],
},
"table": {
"contentToolbar": [
"tableColumn",
"tableRow",
"mergeTableCells",
"tableProperties",
"tableCellProperties",
],
"tableProperties": {
"borderColors": customColorPalette,
"backgroundColors": customColorPalette,
},
"tableCellProperties": {
"borderColors": customColorPalette,
"backgroundColors": customColorPalette,
},
},
"heading": {
"options": [
{
"model": "paragraph",
"title": "Paragraph",
"class": "ck-heading_paragraph",
},
{
"model": "heading1",
"view": "h1",
"title": "Heading 1",
"class": "ck-heading_heading1",
},
{
"model": "heading2",
"view": "h2",
"title": "Heading 2",
"class": "ck-heading_heading2",
},
{
"model": "heading3",
"view": "h3",
"title": "Heading 3",
"class": "ck-heading_heading3",
},
]
},
},
"list": {
"properties": {
"styles": "true",
"startIndex": "true",
"reversed": "true",
}
},
}
15 changes: 11 additions & 4 deletions core/config/jwt.py
Original file line number Diff line number Diff line change
@@ -16,8 +16,8 @@


SIMPLE_JWT = {
"ACCESS_TOKEN_LIFETIME": timedelta(days=7),
"REFRESH_TOKEN_LIFETIME": timedelta(days=31),
"ACCESS_TOKEN_LIFETIME": timedelta(days=1),
"REFRESH_TOKEN_LIFETIME": timedelta(days=7),
"ROTATE_REFRESH_TOKENS": True,
"BLACKLIST_AFTER_ROTATION": True,
"UPDATE_LAST_LOGIN": False,
@@ -35,8 +35,15 @@
"USER_AUTHENTICATION_RULE": "rest_framework_simplejwt.authentication.default_user_authentication_rule",
"AUTH_TOKEN_CLASSES": ("rest_framework_simplejwt.tokens.AccessToken",),
"TOKEN_TYPE_CLAIM": "token_type",
"TOKEN_USER_CLASS": "rest_framework_simplejwt.models.TokenUser",
"JTI_CLAIM": "jti",
"SLIDING_TOKEN_REFRESH_EXP_CLAIM": "refresh_exp",
"SLIDING_TOKEN_LIFETIME": timedelta(days=7),
"SLIDING_TOKEN_REFRESH_LIFETIME": timedelta(days=31),
"SLIDING_TOKEN_LIFETIME": timedelta(days=60),
"SLIDING_TOKEN_REFRESH_LIFETIME": timedelta(days=1),
"TOKEN_OBTAIN_SERIALIZER": "rest_framework_simplejwt.serializers.TokenObtainPairSerializer",
"TOKEN_REFRESH_SERIALIZER": "rest_framework_simplejwt.serializers.TokenRefreshSerializer",
"TOKEN_VERIFY_SERIALIZER": "rest_framework_simplejwt.serializers.TokenVerifySerializer",
"TOKEN_BLACKLIST_SERIALIZER": "rest_framework_simplejwt.serializers.TokenBlacklistSerializer",
"SLIDING_TOKEN_OBTAIN_SERIALIZER": "rest_framework_simplejwt.serializers.TokenObtainSlidingSerializer",
"SLIDING_TOKEN_REFRESH_SERIALIZER": "rest_framework_simplejwt.serializers.TokenRefreshSlidingSerializer",
}
67 changes: 67 additions & 0 deletions core/config/unfold.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
from django.templatetags.static import static

from . import unfold_navigation as navigation


UNFOLD = {
"SITE_TITLE": "Akromjon BLOG",
"SITE_HEADER": "Akromjon BLOG",
"SITE_URL": "/",
"SITE_ICON": {
"light": lambda request: static("images/django-logo.png"),
"dark": lambda request: static("images/django-logo.png"),
},
"SITE_FAVICONS": [
{
"rel": "icon",
"sizes": "32x32",
"type": "image/svg+xml",
"href": lambda request: static("images/django-logo.png"),
},
],
"SITE_SYMBOL": "speed",
"SHOW_HISTORY": True,
"SHOW_VIEW_ON_SITE": True,
"STYLES": [
lambda request: static("css/tailwind.css"),
],
"LOGIN": {
"image": lambda request: static("images/login.jpg"),
},
"COLORS": {
"font": {
"subtle-light": "107 114 128",
"subtle-dark": "156 163 175",
"default-light": "75 85 99",
"default-dark": "209 213 219",
"important-light": "17 24 39",
"important-dark": "243 244 246",
},
"primary": {
"50": "65 144 176",
"100": "65 144 176",
"200": "65 144 176",
"300": "65 144 176",
"400": "65 144 176",
"500": "65 144 176",
"600": "65 144 176",
"700": "65 144 176",
"800": "65 144 176",
"900": "65 144 176",
"950": "65 144 176",
},
},
"EXTENSIONS": {
"modeltranslation": {
"flags": {
"uz": "uz",
"ru": "🇷🇺",
}
},
},
"SIDEBAR": {
"show_search": True,
"show_all_applications": True,
"navigation": navigation.PAGES,
},
}
51 changes: 51 additions & 0 deletions core/config/unfold_navigation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _


def user_has_group_or_permission(user, permission):
if user.is_superuser:
return True

group_names = user.groups.values_list("name", flat=True)
if not group_names:
return True

return user.groups.filter(permissions__codename=permission).exists()


PAGES = [
{
"seperator": True,
"items": [
{
"title": _("Home"),
"icon": "home",
"link": reverse_lazy("admin:index"),
},
],
},
{
"seperator": True,
"title": _("Users"),
"items": [
{
"title": _("Groups"),
"icon": "person_add",
"link": reverse_lazy("admin:auth_group_changelist"),
"permission": lambda request: user_has_group_or_permission(
request.user,
"view_group",
),
},
{
"title": _("Users"),
"icon": "person_add",
"link": reverse_lazy("admin:users_user_changelist"),
"permission": lambda request: user_has_group_or_permission(
request.user,
"view_user",
),
},
],
},
]
28 changes: 22 additions & 6 deletions core/settings/base.py
Original file line number Diff line number Diff line change
@@ -2,6 +2,8 @@

from pathlib import Path

from django.utils.translation import gettext_lazy

from core.config import * # noqa

from dotenv import load_dotenv
@@ -16,19 +18,23 @@

ALLOWED_HOSTS = str(os.getenv("ALLOWED_HOSTS")).split(",")

# CSRF_TRUSTED_ORIGINS = str(os.getenv("CSRF_TRUSTED_ORIGINS")).split(",")

INSTALLED_APPS = DEFAULT_APPS + PROJECT_APPS + THIRD_PARTY_APPS # NOQA
INSTALLED_APPS = THIRD_PARTY_APPS + DEFAULT_APPS + PROJECT_APPS # noqa


MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.BrokenLinkEmailsMiddleware",
# "corsheaders.middleware.CorsMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.locale.LocaleMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
"apps.users.middleware.JWTAuthMiddleware",
"apps.users.middleware.JWTAuthMiddleware", # My Jwt Auth Middleware
]

ROOT_URLCONF = "core.urls"
@@ -60,6 +66,7 @@
}

WSGI_APPLICATION = "core.wsgi.application"
ASGI_APPLICATION = "core.asgi.application"

AUTH_PASSWORD_VALIDATORS = [
{
@@ -81,24 +88,33 @@
TIME_ZONE = "Asia/Tashkent"

USE_I18N = True
# USE_L10N = True

USE_TZ = True

gettext = lambda s: gettext_lazy(s) # noqa

LANGUAGES = (
("ru", gettext("Russia")),
("en", gettext("English")),
("uz", gettext("Uzbek")),
)

LOCALE_PATHS = [os.path.join(BASE_DIR, "locale")]

LOGIN_URL = "/users/login/"
LOGIN_REDIRECT_URL = "/"

STATIC_URL = "static/"
STATIC_ROOT = BASE_DIR.joinpath("staticfiles")
STATICFILES_DIRS = [BASE_DIR.joinpath("static")]
STATICFILES_DIRS = [str(BASE_DIR.joinpath("static"))]
STATIC_ROOT = str(BASE_DIR.joinpath("staticfiles"))

# AUTHENTICATION_BACKENDS = (
# 'apps.users.authentication.JWTAdminAuthentication',
# 'django.contrib.auth.backends.ModelBackend',
# )

MEDIA_URL = "media/"
MEDIA_ROOT = BASE_DIR.joinpath("media/")
MEDIA_ROOT = str(BASE_DIR.joinpath("media/"))

DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"

14 changes: 10 additions & 4 deletions core/urls.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from django.conf.urls import handler400, handler403, handler404, handler500 # noqa
from django.conf.urls.i18n import i18n_patterns # noqa: F401
from django.conf.urls.static import static
from django.conf import settings

@@ -24,6 +25,12 @@
# URLs
urlpatterns = [
path("admin/", admin.site.urls),
path("i18n", include("django.conf.urls.i18n")),
path("rosetta/", include("rosetta.urls")),
]

# Translated urls
urlpatterns += i18n_patterns(
path("", include("apps.blog.urls", namespace="blog")),
path("users/", include("apps.users.urls", namespace="users")),
path("robots.txt", TemplateView.as_view(template_name="bunin/robots.txt")),
@@ -33,7 +40,7 @@
{"sitemaps": sitemaps},
name="django.contrib.sitemaps.views.sitemap",
),
]
)

# API Endpoints
urlpatterns += [
@@ -43,9 +50,8 @@
path("api/v1/blogs/", include(blog_api_router.urls)),
]

if settings.DEBUG:
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)


handler400 = "apps.shared.views.bad_request_view" # noqa
1 change: 1 addition & 0 deletions core/wsgi.py
Original file line number Diff line number Diff line change
@@ -3,6 +3,7 @@
from django.core.wsgi import get_wsgi_application

from dotenv import load_dotenv

load_dotenv()

os.environ.setdefault(
1,329 changes: 1,329 additions & 0 deletions locale/ru/LC_MESSAGES/django.po

Large diffs are not rendered by default.

1,327 changes: 1,327 additions & 0 deletions locale/uz/LC_MESSAGES/django.po

Large diffs are not rendered by default.

3 changes: 0 additions & 3 deletions manage.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys

@@ -9,7 +7,6 @@


def main():
"""Run administrative tasks."""

os.environ.setdefault(
"DJANGO_SETTINGS_MODULE",
Binary file added media/avatars/1727198193049.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -9,3 +9,9 @@ markdown2==2.5.1
pillow==10.4.0
psycopg2-binary==2.9.10
python-dotenv==1.0.1
django-unfold
django-modeltranslation==0.18.11
drf-spectacular==0.27.1
drf-spectacular-sidecar==2024.3.4
django-rosetta==0.10.0
django-ckeditor-5==0.2.13
489 changes: 489 additions & 0 deletions search_system.md

Large diffs are not rendered by default.

945 changes: 945 additions & 0 deletions static/css/tailwind.css

Large diffs are not rendered by default.

Binary file added static/images/django-logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added static/images/login.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion templates/blog/home.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@

{% load blog_tags %}
<main role="main" class="container" id="main-content">
<div class="row">
<div class="col-md-8">