diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..ef8ee40 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,20 @@ +# .coveragerc to control coverrage.py +[run] +source = . +omit = + *venv/* + *venv/bin/* + *venv/include/* + *venv/lib/* + *manage.py + */settings.py + *apps.py + *migrations/* + *asgi.py + *wsgi.py + + +[report] +#fail_under = 100 +show_missing = True +#skip_covered = True \ No newline at end of file diff --git a/.gitignore b/.gitignore index 86809b7..79176af 100644 --- a/.gitignore +++ b/.gitignore @@ -130,3 +130,8 @@ dmypy.json # vscode .vscode/ + +# asset/media/static +asset/ +media/ +static/ diff --git a/BaseDRF/asgi.py b/BaseDRF/asgi.py index bdb8255..4c0a012 100644 --- a/BaseDRF/asgi.py +++ b/BaseDRF/asgi.py @@ -11,6 +11,6 @@ from django.core.asgi import get_asgi_application -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'BaseDRF.settings') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "BaseDRF.settings") application = get_asgi_application() diff --git a/BaseDRF/models.py b/BaseDRF/models.py new file mode 100644 index 0000000..eb512a6 --- /dev/null +++ b/BaseDRF/models.py @@ -0,0 +1,15 @@ +from django.db import models +from django.utils.translation import ugettext as _ + + +class TimestampModel(models.Model): + created_at = models.DateTimeField( + _("created at"), + auto_now_add=True + ) + updated_at = models.DateTimeField( + _("updated at"), + auto_now=True + ) + class Meta: + abstract = True \ No newline at end of file diff --git a/BaseDRF/settings.py b/BaseDRF/settings.py index f2a5c3d..f882d96 100644 --- a/BaseDRF/settings.py +++ b/BaseDRF/settings.py @@ -11,6 +11,7 @@ """ from pathlib import Path +import os # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve(strict=True).parent.parent @@ -20,7 +21,7 @@ # See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = '^=+i9yxmh1++h22rq$by&nfb3wco-u92#iuosw0i7l5zl@a_)j' +SECRET_KEY = "^=+i9yxmh1++h22rq$by&nfb3wco-u92#iuosw0i7l5zl@a_)j" # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True @@ -31,52 +32,57 @@ # Application definition INSTALLED_APPS = [ - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + 'rest_framework', + "seo", + ] MIDDLEWARE = [ - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", + "seo.middleware.IncludeSEOInfo", ] -ROOT_URLCONF = 'BaseDRF.urls' + +ROOT_URLCONF = "BaseDRF.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 = 'BaseDRF.wsgi.application' +WSGI_APPLICATION = "BaseDRF.wsgi.application" # Database # https://docs.djangoproject.com/en/3.1/ref/settings/#databases DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': BASE_DIR / 'db.sqlite3', + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": BASE_DIR / "db.sqlite3", } } @@ -86,16 +92,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", }, ] @@ -103,9 +109,9 @@ # Internationalization # https://docs.djangoproject.com/en/3.1/topics/i18n/ -LANGUAGE_CODE = 'en-us' +LANGUAGE_CODE = "en-us" -TIME_ZONE = 'UTC' +TIME_ZONE = "UTC" USE_I18N = True @@ -117,4 +123,23 @@ # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/3.1/howto/static-files/ -STATIC_URL = '/static/' +STATIC_URL = "/static/" +STATIC_ROOT = os.path.join(BASE_DIR, "asset") +STATICFILES_DIRS = (os.path.join(BASE_DIR, "static"),) + +# Media +MEDIA_ROOT = os.path.join(BASE_DIR, "media") +MEDIA_URL = "/media/" + + +REST_FRAMEWORK = { + "DEFAULT_PERMISSION_CLASSES": [ + # 'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly' + ] +} + + +SITE_PROTOCOL = "http" + +AUTH_USER_MODEL = 'auth.User' + diff --git a/BaseDRF/urls.py b/BaseDRF/urls.py index 39fb3d9..77b0f3b 100644 --- a/BaseDRF/urls.py +++ b/BaseDRF/urls.py @@ -1,21 +1,18 @@ -"""BaseDRF URL Configuration - -The `urlpatterns` list routes URLs to views. For more information please see: - https://docs.djangoproject.com/en/3.1/topics/http/urls/ -Examples: -Function views - 1. Add an import: from my_app import views - 2. Add a URL to urlpatterns: path('', views.home, name='home') -Class-based views - 1. Add an import: from other_app.views import Home - 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') -Including another URLconf - 1. Import the include() function: from django.urls import include, path - 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) -""" from django.contrib import admin -from django.urls import path +from django.urls import path, include + +from django.conf.urls.static import static +from django.conf import settings + +from rest_framework import routers + +router = routers.DefaultRouter() + -urlpatterns = [ - path('admin/', admin.site.urls), -] +urlpatterns = ( + [ + path("admin/", admin.site.urls), + ] + + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) + + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) +) diff --git a/BaseDRF/wsgi.py b/BaseDRF/wsgi.py index d4e0edd..eba3a8c 100644 --- a/BaseDRF/wsgi.py +++ b/BaseDRF/wsgi.py @@ -11,6 +11,6 @@ from django.core.wsgi import get_wsgi_application -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'BaseDRF.settings') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "BaseDRF.settings") application = get_wsgi_application() diff --git a/blog/__init__.py b/blog/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/blog/admin.py b/blog/admin.py new file mode 100644 index 0000000..c62e573 --- /dev/null +++ b/blog/admin.py @@ -0,0 +1,48 @@ +from django.contrib import admin +import imagekit +from . import models +from django.utils.html import format_html +from imagekit.admin import AdminThumbnail +from mptt.admin import MPTTModelAdmin + +from django import forms +from ckeditor.widgets import CKEditorWidget + + +class PostAdminForm(forms.ModelForm): + content_b = forms.CharField(widget=CKEditorWidget()) + class Meta: + model = models.Post + fields = ('__all__') + + +@admin.register(models.BlogCategory) +class BlogCategoryAdmin(MPTTModelAdmin): + mptt_level_indent = 20 + list_display = ("id", "name", "parent") + list_display_link = "id" + list_editable = ("parent", "name") + sortable = "-id" + + +@admin.register(models.Post) +class PostAdmin(admin.ModelAdmin): + form = PostAdminForm + admin_thumbnail = AdminThumbnail(image_field="image") + list_display = ( + "id", + "title", + "author", + "is_published", + "category", + "admin_thumbnail", + ) + list_editable = ("title", "author", "is_published", "category") + + +@admin.register(models.PostComment) +class PostCommentAdmin(MPTTModelAdmin): + mptt_level_indent = 20 + list_display = ("id", "author", "post", "is_published", "parent") + list_editable = ("author", "post", "is_published", "parent") + sortable = "-id" diff --git a/blog/apps.py b/blog/apps.py new file mode 100644 index 0000000..7930587 --- /dev/null +++ b/blog/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class BlogConfig(AppConfig): + name = 'blog' diff --git a/blog/migrations/0001_initial.py b/blog/migrations/0001_initial.py new file mode 100644 index 0000000..4d0b08a --- /dev/null +++ b/blog/migrations/0001_initial.py @@ -0,0 +1,72 @@ +# Generated by Django 3.1 on 2021-10-15 13:15 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import mptt.fields +import tinymce.models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Category', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=500, unique=True)), + ('lft', models.PositiveIntegerField(editable=False)), + ('rght', models.PositiveIntegerField(editable=False)), + ('tree_id', models.PositiveIntegerField(db_index=True, editable=False)), + ('level', models.PositiveIntegerField(editable=False)), + ('parent', mptt.fields.TreeForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='blog.category')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='Post', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='created at')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='updated at')), + ('title', models.CharField(max_length=1000, unique=True, verbose_name='title of post')), + ('slug', models.SlugField(blank=True, max_length=1000, null=True, unique=True, verbose_name='slug of post')), + ('image', models.ImageField(blank='True', null='True', upload_to='PostImages/', verbose_name='title image')), + ('content', tinymce.models.HTMLField(blank=True, null=True, verbose_name='content of post, use one just h1 for header in content')), + ('is_published', models.BooleanField(verbose_name='can publish post?')), + ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='author')), + ('category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='category of post')), + ], + options={ + 'ordering': ['-created_at'], + }, + ), + migrations.CreateModel( + name='PostComment', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='created at')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='updated at')), + ('content', models.TextField(verbose_name='text of post')), + ('is_published', models.BooleanField(default=True, verbose_name='can publish comment?')), + ('lft', models.PositiveIntegerField(editable=False)), + ('rght', models.PositiveIntegerField(editable=False)), + ('tree_id', models.PositiveIntegerField(db_index=True, editable=False)), + ('level', models.PositiveIntegerField(editable=False)), + ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='author')), + ('parent', mptt.fields.TreeForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='blog.postcomment')), + ('post', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.post', verbose_name='post')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/blog/migrations/0002_auto_20211112_1455.py b/blog/migrations/0002_auto_20211112_1455.py new file mode 100644 index 0000000..9201f8b --- /dev/null +++ b/blog/migrations/0002_auto_20211112_1455.py @@ -0,0 +1,17 @@ +# Generated by Django 3.1 on 2021-11-12 14:55 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('blog', '0001_initial'), + ] + + operations = [ + migrations.RenameModel( + old_name='Category', + new_name='BlogCategory', + ), + ] diff --git a/blog/migrations/0003_post_content_b.py b/blog/migrations/0003_post_content_b.py new file mode 100644 index 0000000..69a37d5 --- /dev/null +++ b/blog/migrations/0003_post_content_b.py @@ -0,0 +1,19 @@ +# Generated by Django 3.1 on 2021-11-12 16:23 + +import ckeditor.fields +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('blog', '0002_auto_20211112_1455'), + ] + + operations = [ + migrations.AddField( + model_name='post', + name='content_b', + field=ckeditor.fields.RichTextField(blank=True, null=True), + ), + ] diff --git a/blog/migrations/0004_auto_20211112_1625.py b/blog/migrations/0004_auto_20211112_1625.py new file mode 100644 index 0000000..08f63cf --- /dev/null +++ b/blog/migrations/0004_auto_20211112_1625.py @@ -0,0 +1,19 @@ +# Generated by Django 3.1 on 2021-11-12 16:25 + +import ckeditor_uploader.fields +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('blog', '0003_post_content_b'), + ] + + operations = [ + migrations.AlterField( + model_name='post', + name='content_b', + field=ckeditor_uploader.fields.RichTextUploadingField(blank=True, null=True), + ), + ] diff --git a/blog/migrations/__init__.py b/blog/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/blog/models.py b/blog/models.py new file mode 100644 index 0000000..02e8d65 --- /dev/null +++ b/blog/models.py @@ -0,0 +1,95 @@ +import imagekit +from BaseDRF.models import TimestampModel +from django.db import models + +from django.utils.translation import ugettext as _ +from django.utils.text import slugify + +from tinymce.models import HTMLField +from imagekit.models import ImageSpecField +from imagekit.processors import ResizeToFill +from mptt.models import MPTTModel, TreeForeignKey +from BaseDRF import settings +from ckeditor_uploader.fields import RichTextUploadingField + + +class BlogCategory(MPTTModel): + name = models.CharField(max_length=500, unique=True) + parent = TreeForeignKey( + "self", on_delete=models.CASCADE, null=True, blank=True, related_name="children" + ) + + class MPTTMeta: + order_insertion_by = ["name"] + + def __str__(self) -> str: + return self.name + + +class Post(TimestampModel): + title = models.CharField(_("title of post"), max_length=1000, unique=True) + slug = models.SlugField( + _("slug of post"), max_length=1000, unique=True, null=True, blank=True + ) + author = models.ForeignKey( + settings.AUTH_USER_MODEL, verbose_name=_("author"), on_delete=models.CASCADE + ) + image = models.ImageField( + _("title image"), upload_to="PostImages/", null="True", blank="True" + ) + thumbnail_image = ImageSpecField( + source="image", + processors=[ResizeToFill(200, 200)], + format="webp", + options={"quality": 60}, + ) + webp_image = ImageSpecField( + source="image", + # processors=[ResizeToFill(1280, 720)], + format="webp", + options={"quality": 70}, + ) + content = HTMLField( + _("content of post, use one just h1 for header in content"), + blank=True, + null=True, + ) + content_b = RichTextUploadingField(null=True, blank=True) + is_published = models.BooleanField(_("can publish post?")) + category = models.ForeignKey( + "blog.BlogCategory", + verbose_name=_("category of post"), + on_delete=models.CASCADE, + null=True, + blank=True, + ) + + class Meta: + ordering = ["-created_at"] + + def __str__(self) -> str: + return self.title + + def save(self, *args, **kwargs): + self.slug = slugify(self.title) + super(Post, self).save(*args, **kwargs) + + +class PostComment(TimestampModel, MPTTModel): + """ + comment of post + """ + + author = models.ForeignKey( + settings.AUTH_USER_MODEL, verbose_name=_("author"), on_delete=models.CASCADE + ) + post = models.ForeignKey("blog.Post", verbose_name="post", on_delete=models.CASCADE) + content = models.TextField(_("text of post")) + is_published = models.BooleanField(_("can publish comment?"), default=True) + parent = TreeForeignKey( + "self", on_delete=models.CASCADE, null=True, blank=True, related_name="children" + ) + + def __str__(self) -> str: + name = str(self.author.username) + "/" + str(self.post.title) + return name diff --git a/blog/serializers.py b/blog/serializers.py new file mode 100644 index 0000000..59c6532 --- /dev/null +++ b/blog/serializers.py @@ -0,0 +1,22 @@ +from os import read +from rest_framework import serializers +from . import models +from django.conf import settings + +from django.contrib.auth import get_user_model +User = get_user_model() + +class UserModelSerializer(serializers.ModelSerializer): + class Meta: + model = settings.AUTH_USER_MODEL + fields = ("username", "first_name", "last_name") + + +class PostModelSerializer(serializers.ModelSerializer): + thumbnail = serializers.CharField(source="thumbnail_image.url") + webp_image = serializers.CharField(source="webp_image.url") + # author_detail = UserModelSerializer(source="author", read_only=True) + + class Meta: + model = models.Post + exclude = ("image",) diff --git a/blog/tests.py b/blog/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/blog/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/blog/urls.py b/blog/urls.py new file mode 100644 index 0000000..50750df --- /dev/null +++ b/blog/urls.py @@ -0,0 +1,9 @@ +from django.urls import path +from . import views +from rest_framework import routers + +router = routers.SimpleRouter() + +router.register(r"post", views.PostModelViewSet) + +urlpatterns = [] + router.urls diff --git a/blog/views.py b/blog/views.py new file mode 100644 index 0000000..6cb7759 --- /dev/null +++ b/blog/views.py @@ -0,0 +1,10 @@ +from rest_framework.viewsets import ModelViewSet + + +from . import serializers +from . import models + + +class PostModelViewSet(ModelViewSet): + serializer_class = serializers.PostModelSerializer + queryset = models.Post.objects.all() diff --git a/manage.py b/manage.py index 6d8d992..e8675a3 100755 --- a/manage.py +++ b/manage.py @@ -6,7 +6,7 @@ def main(): """Run administrative tasks.""" - os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'BaseDRF.settings') + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "BaseDRF.settings") try: from django.core.management import execute_from_command_line except ImportError as exc: @@ -18,5 +18,5 @@ def main(): execute_from_command_line(sys.argv) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/requirements.txt b/requirements.txt index 1c0373d..aaae433 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,16 @@ asgiref==3.2.10 Django==3.1 pytz==2021.1 sqlparse==0.4.1 +Pillow==8.3.2 +djangorestframework==3.12.4 +markdown==3.3.4 +django-filter==21.1 +django-imagekit==4.1.0 +django-mptt==0.13.4 +django-ckeditor==6.1.0 +djangorestframework-simplejwt==5.0.0 +pyotp==2.6.0 +pytest==6.2.5 +pytest-django==4.4.0 +pytest-cov==3.0.0 +mixer==7.2.0 \ No newline at end of file diff --git a/seo/__init__.py b/seo/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/seo/admin.py b/seo/admin.py new file mode 100644 index 0000000..e7b438a --- /dev/null +++ b/seo/admin.py @@ -0,0 +1,22 @@ +from django.contrib import admin +from .models import SocialMeta, GenarallMeta, Page + +# Register your models here. + + +class SocialMetaInline(admin.TabularInline): + model = SocialMeta + max_num = 1 + + +class GenarallMetaInline(admin.TabularInline): + model = GenarallMeta + max_num = 1 + + +@admin.register(Page) +class PageAdmin(admin.ModelAdmin): + inlines = (SocialMetaInline, GenarallMetaInline) + list_display = ("id", "url", "redirect_to", "redirect_status", "operation") + list_editable = ("url", "operation", "redirect_status") + search_fields = ("id", "redirect_status") diff --git a/seo/apps.py b/seo/apps.py new file mode 100644 index 0000000..e5354a5 --- /dev/null +++ b/seo/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class CeoConfig(AppConfig): + name = "seo" diff --git a/seo/middleware.py b/seo/middleware.py new file mode 100644 index 0000000..60bc2f8 --- /dev/null +++ b/seo/middleware.py @@ -0,0 +1,37 @@ +from django.shortcuts import redirect + +from seo.models import Page +from seo.serializers import CombineSerializer +from .utils import delete_space_and_slash + + + +class IncludeSEOInfo: + def __init__(self, get_response): + self.get_response = get_response + super().__init__() + + def __call__(self, request, *args, **kwargs): + + response = self.get_response(request) + + return response + + def process_view(self, request, view_func, view_args, view_kwargs): + request_url = delete_space_and_slash(request.get_full_path()) + page = Page.objects.filter(url=request_url) + if len(page) > 0: + if page[0].operation == "redirect": + return redirect(page[0].redirect_to, status=page[0].redirect_status) + + def process_exception(self, request, response): + return response + + def process_template_response(self, request, response): + request_url = delete_space_and_slash(request.get_full_path()) + page = Page.objects.filter(url=request_url) + if len(page) > 0: + if page[0].operation == "seo_info": + serializer = CombineSerializer(page[0]) + response.data.setdefault("seo", serializer.data) + return response diff --git a/seo/migrations/0001_initial.py b/seo/migrations/0001_initial.py new file mode 100644 index 0000000..7ba51c4 --- /dev/null +++ b/seo/migrations/0001_initial.py @@ -0,0 +1,160 @@ +# Generated by Django 3.1 on 2021-10-15 13:11 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="Page", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("url", models.TextField(unique=True, verbose_name="base url")), + ("redirect_to", models.TextField(verbose_name="redirect url")), + ( + "redirect_status", + models.IntegerField( + blank=True, + choices=[ + (301, "301"), + (302, "302"), + (303, "303"), + (307, "307"), + (308, "308"), + ], + null=True, + verbose_name="redirect status code", + ), + ), + ( + "operation", + models.CharField( + blank=True, + choices=[("redirect", "redirect"), ("seo_info", "seo_info")], + max_length=100, + null=True, + verbose_name="operation of page", + ), + ), + ], + ), + migrations.CreateModel( + name="SocialMeta", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "meta_name_twitter_card", + models.TextField( + verbose_name="name='twitter:card'#summery_large_image" + ), + ), + ( + "meta_name_twitter_label1", + models.TextField( + verbose_name="name='twitter:label1'#estimate time for read post" + ), + ), + ( + "meta_name_twitter_data1", + models.TextField(verbose_name="name='twitter:data1'#10 minutes"), + ), + ( + "meta_property_og_title", + models.TextField( + verbose_name="property='og:title'#main title of page" + ), + ), + ( + "meta_property_og_description", + models.TextField( + verbose_name="property='og:description'#meta description of page" + ), + ), + ( + "link_rel_canonical", + models.TextField(verbose_name="rel='canonical'#example.com"), + ), + ( + "page", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + to="seo.page", + verbose_name="page instance", + ), + ), + ], + ), + migrations.CreateModel( + name="GenarallMeta", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("head_title", models.TextField(verbose_name="title of head")), + ( + "meta_description", + models.TextField(verbose_name="description of head"), + ), + ( + "meta_name_robot_follow_type", + models.PositiveSmallIntegerField( + choices=[(0, "follow"), (1, "nofollow")], + default=0, + verbose_name="meta name robot follow type", + ), + ), + ( + "meta_name_robot_index_type", + models.PositiveSmallIntegerField( + choices=[(0, "index"), (1, "noindex")], + default=0, + verbose_name="meta name robot index type", + ), + ), + ( + "meta_name_robot_max_image_preview_type", + models.PositiveSmallIntegerField( + choices=[(0, "non"), (1, "standard"), (2, "large")], + default=2, + verbose_name="meta name robot max-image-preview type", + ), + ), + ( + "page", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + to="seo.page", + verbose_name="page instance", + ), + ), + ], + ), + ] diff --git a/seo/migrations/0002_auto_20211112_1322.py b/seo/migrations/0002_auto_20211112_1322.py new file mode 100644 index 0000000..a2914c7 --- /dev/null +++ b/seo/migrations/0002_auto_20211112_1322.py @@ -0,0 +1,23 @@ +# Generated by Django 3.1 on 2021-11-12 13:22 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('seo', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='page', + name='operation', + field=models.CharField(blank=True, choices=[('redirect', 'operation redirect'), ('seo_info', 'operation include SEO info')], max_length=100, null=True, verbose_name='operation of page'), + ), + migrations.AlterField( + model_name='page', + name='redirect_status', + field=models.IntegerField(blank=True, choices=[(301, '301:Permanent, Cacheable, Request GET/POST may change'), (302, '302:Temporary, not Cacheable by default, Request GET/POST may change'), (303, '303:Temporary, never Cacheable, Request always GET'), (307, '307:Temporary, not Cacheable by default, Request may not change'), (308, '308:Permanent, by default Cacheable, Request may not change')], null=True, verbose_name='redirect status code'), + ), + ] diff --git a/seo/migrations/__init__.py b/seo/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/seo/models.py b/seo/models.py new file mode 100644 index 0000000..f7f7c90 --- /dev/null +++ b/seo/models.py @@ -0,0 +1,124 @@ +from django.db import models +from django.utils.translation import ugettext as _ + +from .utils import delete_space_and_slash + + +class Page(models.Model): + url = models.TextField(_("base url"), unique=True) + redirect_to = models.TextField(_("redirect url")) + + REDIRECT_STATUS_301 = 301 + REDIRECT_STATUS_302 = 302 + REDIRECT_STATUS_303 = 303 + REDIRECT_STATUS_307 = 307 + REDIRECT_STATUS_308 = 308 + REDIRECT_STATUS_TYPE = ( + (REDIRECT_STATUS_301, "301:Permanent, Cacheable, Request GET/POST may change"), + ( + REDIRECT_STATUS_302, + "302:Temporary, not Cacheable by default, Request GET/POST may change", + ), + (REDIRECT_STATUS_303, "303:Temporary, never Cacheable, Request always GET"), + ( + REDIRECT_STATUS_307, + "307:Temporary, not Cacheable by default, Request may not change", + ), + ( + REDIRECT_STATUS_308, + "308:Permanent, by default Cacheable, Request may not change", + ), + ) + redirect_status = models.IntegerField( + _("redirect status code"), + null=True, blank=True, + choices=REDIRECT_STATUS_TYPE + ) + + OPERATION_REDIRECT = "redirect" + OPERATION_INCLUDE_SEO_INFO = "seo_info" + OPERATIONCHOICE = ( + (OPERATION_REDIRECT, "operation redirect"), + (OPERATION_INCLUDE_SEO_INFO, "operation include SEO info"), + ) + operation = models.CharField( + _("operation of page"), + max_length=100, + choices=OPERATIONCHOICE, + null=True, + blank=True, + ) + + def __str__(self) -> str: + return super().__str__() + + def save(self, *args, **kwargs): + self.url = delete_space_and_slash(self.url) + self.redirect_to = delete_space_and_slash(self.redirect_to) + super(Page, self).save(*args, **kwargs) + + +class SocialMeta(models.Model): + page = models.OneToOneField( + "seo.Page", on_delete=models.CASCADE, verbose_name=_("page instance") + ) + meta_name_twitter_card = models.TextField( + _("name='twitter:card'#summery_large_image") + ) + meta_name_twitter_label1 = models.TextField( + _("name='twitter:label1'#estimate time for read post") + ) + meta_name_twitter_data1 = models.TextField(_("name='twitter:data1'#10 minutes")) + meta_property_og_title = models.TextField( + _("property='og:title'#main title of page") + ) + meta_property_og_description = models.TextField( + _("property='og:description'#meta description of page") + ) + link_rel_canonical = models.TextField(_("rel='canonical'#example.com")) + + +class GenarallMeta(models.Model): + page = models.OneToOneField( + "seo.Page", on_delete=models.CASCADE, verbose_name=_("page instance") + ) + head_title = models.TextField( + _("title of head"), + ) + meta_description = models.TextField( + _("description of head"), + ) + META_NAME_ROBOTS_FOLLOW = 0 + META_NAME_ROBOTS_NOFOLOW = 1 + META_NAME_ROBOTS_FOLOW_TYPE = ( + (META_NAME_ROBOTS_FOLLOW, "follow"), + (META_NAME_ROBOTS_NOFOLOW, "nofollow"), + ) + meta_name_robot_follow_type = models.PositiveSmallIntegerField( + _("meta name robot follow type"), choices=META_NAME_ROBOTS_FOLOW_TYPE, default=0 + ) + META_NAME_ROBOTS_INDEX = 0 + META_NAME_ROBOTS_NOINDEX = 1 + META_NAME_ROBOTS_INDEX_TYPE = ( + (META_NAME_ROBOTS_INDEX, "index"), + (META_NAME_ROBOTS_NOINDEX, "noindex"), + ) + meta_name_robot_index_type = models.PositiveSmallIntegerField( + _("meta name robot index type"), choices=META_NAME_ROBOTS_INDEX_TYPE, default=0 + ) + META_NAME_ROBOTS_MAX_IMAGE_PREVIEW_NONE = 0 + META_NAME_ROBOTS_MAX_IMAGE_PREVIEW_STANDARD = 1 + META_NAME_ROBOTS_MAX_IMAGE_PREVIEW_LARGE = 2 + META_NAME_ROBOTS_MAX_IMAGE_PREVIEW_TYPE = ( + (META_NAME_ROBOTS_MAX_IMAGE_PREVIEW_NONE, "non"), + (META_NAME_ROBOTS_MAX_IMAGE_PREVIEW_STANDARD, "standard"), + (META_NAME_ROBOTS_MAX_IMAGE_PREVIEW_LARGE, "large"), + ) + meta_name_robot_max_image_preview_type = models.PositiveSmallIntegerField( + _("meta name robot max-image-preview type"), + choices=META_NAME_ROBOTS_MAX_IMAGE_PREVIEW_TYPE, + default=2, + ) + + def __str__(self) -> str: + return "header of: " + self.head_title diff --git a/seo/serializers.py b/seo/serializers.py new file mode 100644 index 0000000..fba981a --- /dev/null +++ b/seo/serializers.py @@ -0,0 +1,40 @@ +from rest_framework import serializers +from rest_framework.fields import SerializerMethodField +from .models import Page, SocialMeta, GenarallMeta + + +class PageModelSerializer(serializers.ModelSerializer): + class Meta: + model = Page + fields = "__all__" + + +class SocialMetaModelSerializer(serializers.ModelSerializer): + class Meta: + model = SocialMeta + exclude = ("id", "page") + + +class GenarallMetaModelSerializer(serializers.ModelSerializer): + class Meta: + model = GenarallMeta + exclude = ("id", "page") + + +class CombineSerializer(serializers.Serializer): + social_meta = serializers.SerializerMethodField("get_social") + generall_meta = serializers.SerializerMethodField("get_generall") + + def get_social(self, obj): + social_mata = SocialMeta.objects.get(page=obj) + data = SocialMetaModelSerializer( + social_mata, + ).data + return data + + def get_generall(self, obj): + generall_meta = GenarallMeta.objects.get(page=obj) + data = GenarallMetaModelSerializer( + generall_meta, + ).data + return data diff --git a/seo/tests.py b/seo/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/seo/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/seo/utils.py b/seo/utils.py new file mode 100644 index 0000000..40fff71 --- /dev/null +++ b/seo/utils.py @@ -0,0 +1,11 @@ +def delete_space_and_slash(aim_str): + len_str = len(aim_str) + condition = True + while condition == True: + aim_str = aim_str.strip("/").strip(" ") + if len_str > len(aim_str): + condition = True + len_str = len(aim_str) + else: + condition = False + return aim_str diff --git a/seo/views.py b/seo/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/seo/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here.