From 24aff844b655e020228af43a133c9208001c4cd4 Mon Sep 17 00:00:00 2001 From: Akash Singh Date: Sat, 18 Apr 2020 20:03:47 +0530 Subject: [PATCH 1/5] #1-project-setup: Django and Other Requirement Installed . Readme Added --- .idea/.gitignore | 2 + .idea/django-angular-blog.iml | 11 ++ .../inspectionProfiles/profiles_settings.xml | 6 + .idea/misc.xml | 4 + .idea/modules.xml | 8 ++ .idea/vcs.xml | 6 + backend/.gitignore | 103 +++++++++++++++ backend/blogyy/__init__.py | 0 backend/blogyy/asgi.py | 16 +++ backend/blogyy/settings.py | 120 ++++++++++++++++++ backend/blogyy/urls.py | 21 +++ backend/blogyy/wsgi.py | 16 +++ backend/db.sqlite3 | 0 backend/manage.py | 21 +++ backend/readme.md | 26 ++++ backend/requirement.txt | 5 + 16 files changed, 365 insertions(+) create mode 100644 .idea/.gitignore create mode 100644 .idea/django-angular-blog.iml create mode 100644 .idea/inspectionProfiles/profiles_settings.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml create mode 100644 backend/.gitignore create mode 100644 backend/blogyy/__init__.py create mode 100644 backend/blogyy/asgi.py create mode 100644 backend/blogyy/settings.py create mode 100644 backend/blogyy/urls.py create mode 100644 backend/blogyy/wsgi.py create mode 100644 backend/db.sqlite3 create mode 100755 backend/manage.py create mode 100644 backend/readme.md create mode 100644 backend/requirement.txt diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..e7e9d11 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,2 @@ +# Default ignored files +/workspace.xml diff --git a/.idea/django-angular-blog.iml b/.idea/django-angular-blog.iml new file mode 100644 index 0000000..2c1d26d --- /dev/null +++ b/.idea/django-angular-blog.iml @@ -0,0 +1,11 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..a2e120d --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..fa662f0 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000..aa2e503 --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,103 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# 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/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# dotenv +.env + +# virtualenv +.venv +venv/ +ENV/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.idea/ +BlogBackendProject/private.py \ No newline at end of file diff --git a/backend/blogyy/__init__.py b/backend/blogyy/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/blogyy/asgi.py b/backend/blogyy/asgi.py new file mode 100644 index 0000000..f596103 --- /dev/null +++ b/backend/blogyy/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for blogyy 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/3.0/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'blogyy.settings') + +application = get_asgi_application() diff --git a/backend/blogyy/settings.py b/backend/blogyy/settings.py new file mode 100644 index 0000000..b447147 --- /dev/null +++ b/backend/blogyy/settings.py @@ -0,0 +1,120 @@ +""" +Django settings for blogyy project. + +Generated by 'django-admin startproject' using Django 3.0.5. + +For more information on this file, see +https://docs.djangoproject.com/en/3.0/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/3.0/ref/settings/ +""" + +import os + +# Build paths inside the project like this: os.path.join(BASE_DIR, ...) +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = 'zl)(5k8=*z!$0igbrz*de-72@ouxbt8p!gwb@9#c0as&%$r!zo' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', +] + +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', +] + +ROOT_URLCONF = 'blogyy.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', + ], + }, + }, +] + +WSGI_APPLICATION = 'blogyy.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/3.0/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), + } +} + + +# Password validation +# https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/3.0/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/3.0/howto/static-files/ + +STATIC_URL = '/static/' diff --git a/backend/blogyy/urls.py b/backend/blogyy/urls.py new file mode 100644 index 0000000..22eac97 --- /dev/null +++ b/backend/blogyy/urls.py @@ -0,0 +1,21 @@ +"""blogyy URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/3.0/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 + +urlpatterns = [ + path('admin/', admin.site.urls), +] diff --git a/backend/blogyy/wsgi.py b/backend/blogyy/wsgi.py new file mode 100644 index 0000000..a2ce525 --- /dev/null +++ b/backend/blogyy/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for blogyy project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/3.0/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'blogyy.settings') + +application = get_wsgi_application() diff --git a/backend/db.sqlite3 b/backend/db.sqlite3 new file mode 100644 index 0000000..e69de29 diff --git a/backend/manage.py b/backend/manage.py new file mode 100755 index 0000000..46a9165 --- /dev/null +++ b/backend/manage.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'blogyy.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/backend/readme.md b/backend/readme.md new file mode 100644 index 0000000..862cc3f --- /dev/null +++ b/backend/readme.md @@ -0,0 +1,26 @@ +# Backend for Blogyy + +1. Python environment: Python 3.6.2 + +2. Mainly dependent +``` +Django==3.0.5 +djangorestframework==3.11.0 +``` + +## functionality + +> Note: For more technical stack dependencies, see the `requirements.txt` file + +## Api List + +> Will be Added Soon + + +``` +> python manage.py runserver +``` + + + +Copyright (c) 2020-present, \ No newline at end of file diff --git a/backend/requirement.txt b/backend/requirement.txt new file mode 100644 index 0000000..0788226 --- /dev/null +++ b/backend/requirement.txt @@ -0,0 +1,5 @@ +asgiref==3.2.7 +Django==3.0.5 +djangorestframework==3.11.0 +pytz==2019.3 +sqlparse==0.3.1 From f99b9fa65bebcc5d3b9d390d3ab7132abdb63f5c Mon Sep 17 00:00:00 2001 From: Akash Singh Date: Sat, 18 Apr 2020 21:01:13 +0530 Subject: [PATCH 2/5] #1-project-setup: Root App Created --- backend/app/__init__.py | 0 backend/app/root/__init__.py | 0 backend/app/root/admin.py | 3 +++ backend/app/root/apps.py | 5 +++++ backend/app/root/migrations/__init__.py | 0 backend/app/root/models.py | 3 +++ backend/app/root/tests.py | 3 +++ backend/app/root/views.py | 3 +++ 8 files changed, 17 insertions(+) create mode 100644 backend/app/__init__.py create mode 100644 backend/app/root/__init__.py create mode 100644 backend/app/root/admin.py create mode 100644 backend/app/root/apps.py create mode 100644 backend/app/root/migrations/__init__.py create mode 100644 backend/app/root/models.py create mode 100644 backend/app/root/tests.py create mode 100644 backend/app/root/views.py diff --git a/backend/app/__init__.py b/backend/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/root/__init__.py b/backend/app/root/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/root/admin.py b/backend/app/root/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/backend/app/root/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/backend/app/root/apps.py b/backend/app/root/apps.py new file mode 100644 index 0000000..4ff1019 --- /dev/null +++ b/backend/app/root/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class RootConfig(AppConfig): + name = 'root' diff --git a/backend/app/root/migrations/__init__.py b/backend/app/root/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/root/models.py b/backend/app/root/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/backend/app/root/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/backend/app/root/tests.py b/backend/app/root/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/backend/app/root/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/backend/app/root/views.py b/backend/app/root/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/backend/app/root/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. From bd7b34b2e2385e9db4aa7598338e842bc2aee4c1 Mon Sep 17 00:00:00 2001 From: Akash Singh Date: Sun, 19 Apr 2020 02:04:11 +0530 Subject: [PATCH 3/5] #2-base-model:Added The Basic Site Info Configuration The Data For Website --- backend/app/article/__init__.py | 0 backend/app/article/admin.py | 3 + backend/app/article/apps.py | 5 + backend/app/article/migrations/__init__.py | 0 backend/app/article/models.py | 3 + backend/app/article/tests.py | 3 + backend/app/article/views.py | 3 + backend/app/base/__init__.py | 0 backend/app/base/admin.py | 4 + backend/app/base/apps.py | 5 + backend/app/base/filters.py | 13 + backend/app/base/migrations/0001_initial.py | 108 +++++++ backend/app/base/migrations/__init__.py | 0 backend/app/base/models.py | 134 ++++++++ backend/app/base/serializer.py | 50 +++ backend/app/base/tests.py | 0 backend/app/base/utils.py | 19 ++ backend/app/base/views.py | 31 ++ backend/app/root/admin.py | 15 + backend/app/root/apps.py | 3 +- backend/app/root/filters.py | 57 ++++ backend/app/root/migrations/0001_initial.py | 158 ++++++++++ .../migrations/0002_auto_20200418_1841.py | 34 +++ backend/app/root/models.py | 285 +++++++++++++++++- backend/app/root/serializer.py | 137 +++++++++ backend/app/root/views.py | 156 +++++++++- .../e42b779c-0777-4ac6-8a4f-5fb0eeac2a79.png | Bin 0 -> 3440 bytes backend/base/site/image/20/04/profile.jpeg | Bin 0 -> 8096 bytes backend/blogyy/settings.py | 35 +++ backend/blogyy/urls.py | 31 +- backend/db.sqlite3 | Bin 0 -> 266240 bytes backend/requirement.txt | 16 + 32 files changed, 1303 insertions(+), 5 deletions(-) create mode 100644 backend/app/article/__init__.py create mode 100644 backend/app/article/admin.py create mode 100644 backend/app/article/apps.py create mode 100644 backend/app/article/migrations/__init__.py create mode 100644 backend/app/article/models.py create mode 100644 backend/app/article/tests.py create mode 100644 backend/app/article/views.py create mode 100644 backend/app/base/__init__.py create mode 100644 backend/app/base/admin.py create mode 100644 backend/app/base/apps.py create mode 100644 backend/app/base/filters.py create mode 100644 backend/app/base/migrations/0001_initial.py create mode 100644 backend/app/base/migrations/__init__.py create mode 100644 backend/app/base/models.py create mode 100644 backend/app/base/serializer.py create mode 100644 backend/app/base/tests.py create mode 100644 backend/app/base/utils.py create mode 100644 backend/app/base/views.py create mode 100644 backend/app/root/filters.py create mode 100644 backend/app/root/migrations/0001_initial.py create mode 100644 backend/app/root/migrations/0002_auto_20200418_1841.py create mode 100644 backend/app/root/serializer.py create mode 100644 backend/base/site/image/20/04/e42b779c-0777-4ac6-8a4f-5fb0eeac2a79.png create mode 100644 backend/base/site/image/20/04/profile.jpeg diff --git a/backend/app/article/__init__.py b/backend/app/article/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/article/admin.py b/backend/app/article/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/backend/app/article/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/backend/app/article/apps.py b/backend/app/article/apps.py new file mode 100644 index 0000000..8295b57 --- /dev/null +++ b/backend/app/article/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class ArticleConfig(AppConfig): + name = 'article' diff --git a/backend/app/article/migrations/__init__.py b/backend/app/article/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/article/models.py b/backend/app/article/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/backend/app/article/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/backend/app/article/tests.py b/backend/app/article/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/backend/app/article/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/backend/app/article/views.py b/backend/app/article/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/backend/app/article/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/backend/app/base/__init__.py b/backend/app/base/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/base/admin.py b/backend/app/base/admin.py new file mode 100644 index 0000000..1bf5592 --- /dev/null +++ b/backend/app/base/admin.py @@ -0,0 +1,4 @@ +from django.contrib import admin +from .models import SiteInfo +# Register your models here. +admin.site.register(SiteInfo) \ No newline at end of file diff --git a/backend/app/base/apps.py b/backend/app/base/apps.py new file mode 100644 index 0000000..ef566bd --- /dev/null +++ b/backend/app/base/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class BaseConfig(AppConfig): + name = 'app.base' diff --git a/backend/app/base/filters.py b/backend/app/base/filters.py new file mode 100644 index 0000000..c0b5d71 --- /dev/null +++ b/backend/app/base/filters.py @@ -0,0 +1,13 @@ +import django_filters +from .models import SiteInfo + + +class SiteInfoFilter(django_filters.rest_framework.FilterSet): + """ + to check Website Live Status + """ + class Meta: + model = SiteInfo + fields = ['is_live'] + + diff --git a/backend/app/base/migrations/0001_initial.py b/backend/app/base/migrations/0001_initial.py new file mode 100644 index 0000000..29201aa --- /dev/null +++ b/backend/app/base/migrations/0001_initial.py @@ -0,0 +1,108 @@ +# Generated by Django 3.0.5 on 2020-04-18 18:41 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('root', '0002_auto_20200418_1841'), + ] + + operations = [ + migrations.CreateModel( + name='BloggerInfo', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(default='', max_length=20)), + ('desc', models.CharField(default='', max_length=300)), + ('avatar', models.ImageField(blank=True, null=True, upload_to='base/avatar/image/%y/%m')), + ('background', models.ImageField(blank=True, null=True, upload_to='base/background/image/%y/%m')), + ('add_time', models.DateTimeField(auto_now_add=True, null=True)), + ], + ), + migrations.CreateModel( + name='NavigationLink', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=30)), + ('desc', models.CharField(max_length=100)), + ('image', models.ImageField(blank=True, null=True, upload_to='base/friendlink/image/%y/%m')), + ('url', models.CharField(max_length=200)), + ('target', models.CharField(blank=True, choices=[('_blank', 'Next Page'), ('_self', 'in same Frame'), ('_parent', 'parent'), ('_top', 'top')], max_length=10, null=True)), + ('add_time', models.DateTimeField(auto_now_add=True, null=True)), + ], + ), + migrations.CreateModel( + name='SiteInfo', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(default='', max_length=20)), + ('desc', models.CharField(default='', max_length=150)), + ('keywords', models.CharField(default='', max_length=300)), + ('icon', models.ImageField(blank=True, null=True, upload_to='base/site/image/%y/%m')), + ('background', models.ImageField(blank=True, null=True, upload_to='base/site/image/%y/%m')), + ('api_base_url', models.URLField(max_length=30)), + ('copyright', models.CharField(default='', max_length=100)), + ('copyright_desc', models.CharField(default='', max_length=300)), + ('icp', models.CharField(default='', max_length=20)), + ('is_live', models.BooleanField(default=False)), + ('is_force_refresh', models.BooleanField(default=False)), + ('force_refresh_time', models.DateTimeField(blank=True, null=True)), + ('access_password', models.CharField(blank=True, max_length=20, null=True)), + ('access_password_encrypt', models.CharField(blank=True, max_length=100, null=True)), + ('add_time', models.DateTimeField(auto_now_add=True, null=True)), + ], + ), + migrations.CreateModel( + name='SiteInfoNavigation', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(default='', max_length=20)), + ('index', models.IntegerField(default=0)), + ('add_time', models.DateTimeField(auto_now_add=True, null=True)), + ('navigation', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to='base.NavigationLink')), + ('site', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to='base.SiteInfo')), + ], + ), + migrations.AddField( + model_name='siteinfo', + name='navigations', + field=models.ManyToManyField(through='base.SiteInfoNavigation', to='base.NavigationLink'), + ), + migrations.CreateModel( + name='BloggerSocial', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(default='', max_length=20)), + ('index', models.IntegerField(default=0)), + ('add_time', models.DateTimeField(auto_now_add=True, null=True)), + ('blogger', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to='base.BloggerInfo')), + ('social', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='root.Social')), + ], + ), + migrations.CreateModel( + name='BloggerMaster', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(default='', max_length=20)), + ('index', models.IntegerField(default=0)), + ('add_time', models.DateTimeField(auto_now_add=True, null=True)), + ('blogger', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to='base.BloggerInfo')), + ('master', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to='root.Master')), + ], + ), + migrations.AddField( + model_name='bloggerinfo', + name='masters', + field=models.ManyToManyField(through='base.BloggerMaster', to='root.Master'), + ), + migrations.AddField( + model_name='bloggerinfo', + name='socials', + field=models.ManyToManyField(through='base.BloggerSocial', to='root.Social'), + ), + ] diff --git a/backend/app/base/migrations/__init__.py b/backend/app/base/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/base/models.py b/backend/app/base/models.py new file mode 100644 index 0000000..f988a86 --- /dev/null +++ b/backend/app/base/models.py @@ -0,0 +1,134 @@ +from django.db import models + +# Create your models here. +import hashlib +from django.db import models +from app.root.models import Social, Master + + +class NavigationLink(models.Model): + TARGET_TYPE = ( + ("_blank", "Next Page"), + ("_self","in same Frame"), + ("_parent", "parent"), + ("_top", "top") + ) + name = models.CharField(max_length=30) + desc = models.CharField(max_length=100) + image = models.ImageField(upload_to="base/friendlink/image/%y/%m", null=True, blank=True) + url = models.CharField(max_length=200,) + target = models.CharField(max_length=10, choices=TARGET_TYPE, null=True, blank=True) + add_time = models.DateTimeField(auto_now_add=True, null=True, blank=True) + + def save(self, *args, **kwargs): + super(NavigationLink, self).save(*args, **kwargs) + + class Meta: + pass + + def __str__(self): + return self.name + + +class SiteInfo(models.Model): + + """ + Config For Website + """ + name = models.CharField(default="", max_length=20) + desc = models.CharField(default="", max_length=150) + keywords = models.CharField(default="", max_length=300) + icon = models.ImageField(upload_to="base/site/image/%y/%m", null=True, blank=True) + background = models.ImageField(upload_to="base/site/image/%y/%m", null=True, blank=True) + api_base_url = models.URLField(max_length=30, null=False, blank=False,) + navigations = models.ManyToManyField(NavigationLink, through="SiteInfoNavigation", through_fields=( + 'site', 'navigation')) + copyright = models.CharField(default="", max_length=100,) + copyright_desc = models.CharField(default="", max_length=300) + icp = models.CharField(default="", max_length=20) + is_live = models.BooleanField(default=False) + is_force_refresh = models.BooleanField(default=False) + force_refresh_time = models.DateTimeField(null=True, blank=True,) + access_password = models.CharField(max_length=20, null=True, blank=True) + access_password_encrypt = models.CharField(max_length=100, null=True, blank=True) + add_time = models.DateTimeField(auto_now_add=True, null=True, blank=True) + + def __str__(self): + return self.name + + def save(self, *args, **kwargs): + if self.access_password: + md5 = hashlib.md5() + md5.update(self.access_password.encode('utf8')) + self.access_password_encrypt = md5.hexdigest() + else: + self.access_password_encrypt = '' + super(SiteInfo, self).save(*args, **kwargs) + + class Meta: + pass + + + +class BloggerInfo(models.Model): + name = models.CharField(default="", max_length=20) + desc = models.CharField(default="", max_length=300,) + avatar = models.ImageField(upload_to="base/avatar/image/%y/%m", null=True, blank=True,) + background = models.ImageField(upload_to="base/background/image/%y/%m", null=True, blank=True) + socials = models.ManyToManyField(Social, through='BloggerSocial', through_fields=('blogger', 'social')) + masters = models.ManyToManyField(Master, through='BloggerMaster', through_fields=('blogger', 'master')) + add_time = models.DateTimeField(auto_now_add=True, null=True, blank=True) + + def save(self, *args, **kwargs): + super(BloggerInfo, self).save(*args, **kwargs) + + class Meta: + pass + + def __str__(self): + return self.name + + +class BloggerSocial(models.Model): + name = models.CharField(default="", max_length=20) + blogger = models.ForeignKey(BloggerInfo ,on_delete=models.DO_NOTHING) + social = models.ForeignKey(Social,on_delete=models.CASCADE) + index = models.IntegerField(default=0) + add_time = models.DateTimeField(auto_now_add=True, null=True, blank=True) + + class Meta: + pass + + def __str__(self): + return self.name + + +class BloggerMaster(models.Model): + name = models.CharField(default="", max_length=20) + blogger = models.ForeignKey(BloggerInfo,on_delete=models.DO_NOTHING) + master = models.ForeignKey(Master,on_delete=models.DO_NOTHING) + index = models.IntegerField(default=0) + add_time = models.DateTimeField(auto_now_add=True, null=True, blank=True) + + class Meta: + pass + + def __str__(self): + return self.name + + +class SiteInfoNavigation(models.Model): + name = models.CharField(default="", max_length=20) + site = models.ForeignKey(SiteInfo,on_delete=models.DO_NOTHING) + navigation = models.ForeignKey(NavigationLink,on_delete=models.DO_NOTHING) + index = models.IntegerField(default=0) + add_time = models.DateTimeField(auto_now_add=True, null=True, blank=True) + + class Meta: + pass + + def __str__(self): + return self.name + + + diff --git a/backend/app/base/serializer.py b/backend/app/base/serializer.py new file mode 100644 index 0000000..6e186c9 --- /dev/null +++ b/backend/app/base/serializer.py @@ -0,0 +1,50 @@ +from rest_framework import serializers +from .models import SiteInfo, BloggerInfo, NavigationLink +from app.root.serializer import MasterSerializer, SocialSerializer +from django.conf import settings + + +class NavigationLinkSerializer(serializers.ModelSerializer): + class Meta: + model = NavigationLink + fields = "__all__" + + +class SiteInfoSerializer(serializers.ModelSerializer): + navigations = NavigationLinkSerializer(many=True) + icon = serializers.SerializerMethodField() + background = serializers.SerializerMethodField() + + def get_icon(self, site_info): + if site_info.icon: + return "{0}/{1}".format(settings.MEDIA_URL_PREFIX, site_info.icon) + + def get_background(self, blogger_info): + if blogger_info.background: + return "{0}/{1}".format(settings.MEDIA_URL_PREFIX, blogger_info.background) + + class Meta: + model = SiteInfo + fields = ( + 'name', 'desc', 'keywords', 'icon', 'background', 'api_base_url', 'is_live', + 'is_force_refresh', 'force_refresh_time', 'navigations', 'copyright', 'copyright_desc', + 'icp') + + +class BloggerInfoSerializer(serializers.ModelSerializer): + socials = SocialSerializer(many=True) + masters = MasterSerializer(many=True) + avatar = serializers.SerializerMethodField() + background = serializers.SerializerMethodField() + + def get_avatar(self, blogger_info): + if blogger_info.avatar: + return "{0}/{1}".format(settings.MEDIA_URL_PREFIX, blogger_info.avatar) + + def get_background(self, blogger_info): + if blogger_info.background: + return "{0}/{1}".format(settings.MEDIA_URL_PREFIX, blogger_info.background) + + class Meta: + model = BloggerInfo + fields = "__all__" diff --git a/backend/app/base/tests.py b/backend/app/base/tests.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/base/utils.py b/backend/app/base/utils.py new file mode 100644 index 0000000..eb98bfe --- /dev/null +++ b/backend/app/base/utils.py @@ -0,0 +1,19 @@ +from rest_framework.pagination import PageNumberPagination, LimitOffsetPagination + + +class CustomePageNumberPagination(PageNumberPagination): + page_size = 10 + page_size_query_param = 'page_size' + page_query_param = 'page' + max_page_size = 100 + +class CustomeLimitOffsetPagination(LimitOffsetPagination): + default_limit = 50 + limit_query_param = 'limit' + offset_query_param = 'offset' + max_limit = 100 + min_limit = 1 + min_offset = 0 + + +ALLOWED_PROTOCOLS = ['http', 'https', 'mailto'] \ No newline at end of file diff --git a/backend/app/base/views.py b/backend/app/base/views.py new file mode 100644 index 0000000..c8065e0 --- /dev/null +++ b/backend/app/base/views.py @@ -0,0 +1,31 @@ +from django.shortcuts import render + +# Create your views here. +# _*_ coding: utf-8 _*_ + + +from rest_framework import viewsets, filters +from django_filters.rest_framework import DjangoFilterBackend + +from .models import SiteInfo, BloggerInfo +from .serializer import BloggerInfoSerializer, SiteInfoSerializer +from .filters import SiteInfoFilter + + +class SiteInfoViewset(viewsets.ReadOnlyModelViewSet): + """ + List: + """ + queryset = SiteInfo.objects.all() + serializer_class = SiteInfoSerializer + filter_backends = (DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter) + filter_class = SiteInfoFilter +# + +class BloggerInfoViewset(viewsets.ReadOnlyModelViewSet): + """ + List: + """ + queryset = BloggerInfo.objects.all() + serializer_class = BloggerInfoSerializer + diff --git a/backend/app/root/admin.py b/backend/app/root/admin.py index 8c38f3f..95c5bbe 100644 --- a/backend/app/root/admin.py +++ b/backend/app/root/admin.py @@ -1,3 +1,18 @@ from django.contrib import admin # Register your models here. + +from .models import Category, Tag, License, PostBaseInfo, Banner, \ + Camera, Picture, Social, Master, PostTag + +# admin.site.r +admin.site.register(Category) +admin.site.register(Tag) +admin.site.register(License) +admin.site.register(PostBaseInfo) +admin.site.register(Banner) +admin.site.register(Camera) +admin.site.register(Picture) +admin.site.register(Social) +admin.site.register(Master) +admin.site.register(PostTag) diff --git a/backend/app/root/apps.py b/backend/app/root/apps.py index 4ff1019..9a92220 100644 --- a/backend/app/root/apps.py +++ b/backend/app/root/apps.py @@ -2,4 +2,5 @@ class RootConfig(AppConfig): - name = 'root' + name = 'app.root' + verbose_name = "Root App" \ No newline at end of file diff --git a/backend/app/root/filters.py b/backend/app/root/filters.py new file mode 100644 index 0000000..b345294 --- /dev/null +++ b/backend/app/root/filters.py @@ -0,0 +1,57 @@ +from django.db.models import Q +import django_filters +from .models import Category, Banner, PostBaseInfo + + +class CategoryFilter(django_filters.rest_framework.FilterSet): + """ + Category Filter + """ + level_min = django_filters.NumberFilter(field_name='category_level', lookup_expr='gte') + level_max = django_filters.NumberFilter(field_name='category_level', lookup_expr='lte') + + top_category = django_filters.NumberFilter(method='top_category_filter') + + def top_category_filter(self, queryset, name, value): + return queryset.filter(Q(id=value) | Q(parent_category_id=value) | Q( + parent_category__parent_category_id=value)) + + class Meta: + model = Category + fields = ['id', 'level_min', 'level_max', 'is_tab'] + + +class BannerFilter(django_filters.rest_framework.FilterSet): + """ + Banner Filter + """ + level_min = django_filters.NumberFilter(field_name='category_level', lookup_expr='gte') + level_max = django_filters.NumberFilter(field_name='category_level', lookup_expr='lte') + + top_category = django_filters.NumberFilter(method='top_category_filter') + + def top_category_filter(self, queryset, name, value): + return queryset.filter(Q(category_id=value) | Q(category__parent_category_id=value) | Q( + category__parent_category__parent_category_id=value)) + + class Meta: + model = Banner + fields = ['title', 'url', 'index'] + + +class PostBaseInfoFilter(django_filters.rest_framework.FilterSet): + """ + Base Post Currently filtering on Time Created + """ + time_min = django_filters.DateFilter(field_name='add_time', lookup_expr='gte') + time_max = django_filters.DateFilter(field_name='add_time', lookup_expr='lte') + + top_category = django_filters.NumberFilter(method='top_category_filter') + + def top_category_filter(self, queryset, name, value): + return queryset.filter(Q(category_id=value) | Q(category__parent_category_id=value) | Q( + category__parent_category__parent_category_id=value)) + + class Meta: + model = PostBaseInfo + fields = ['time_min', 'time_max', 'is_hot', 'is_recommend', 'post_type'] diff --git a/backend/app/root/migrations/0001_initial.py b/backend/app/root/migrations/0001_initial.py new file mode 100644 index 0000000..463ef05 --- /dev/null +++ b/backend/app/root/migrations/0001_initial.py @@ -0,0 +1,158 @@ +# Generated by Django 3.0.5 on 2020-04-18 17:30 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Camera', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('device', models.CharField(max_length=30)), + ('version', models.CharField(max_length=200)), + ('environment', models.CharField(max_length=200)), + ('add_time', models.DateTimeField(auto_now_add=True, null=True)), + ], + ), + migrations.CreateModel( + name='Category', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(default='', help_text='Root Category Name', max_length=30, verbose_name='Category Name')), + ('category_type', models.CharField(choices=[('articles', 'Articles'), ('articles/category', 'Article Classification')], help_text='Used to configure route ', max_length=30, verbose_name='Category Type ')), + ('desc', models.TextField(blank=True, help_text='category description', null=True, verbose_name='decription')), + ('image', models.ImageField(blank=True, help_text='图片', null=True, upload_to='comment/category/image/%Y/%m')), + ('category_level', models.CharField(choices=[('1', 'level1'), ('2', 'level2'), ('3', 'level3')], help_text='level', max_length=20, verbose_name='类目级别')), + ('is_active', models.BooleanField(default=True, help_text='Active', verbose_name='Active Status')), + ('is_tab', models.BooleanField(default=True, help_text='Tab', verbose_name='Tab Status')), + ('index', models.IntegerField(default=0, help_text='.', verbose_name='indexing')), + ('add_time', models.DateTimeField(auto_now_add=True, help_text='When was added', verbose_name='Add Time')), + ('parent_category', models.ForeignKey(blank=True, help_text='parent category', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='sub_category', to='root.Category', verbose_name='parent')), + ], + options={ + 'verbose_name': 'Classfication', + 'verbose_name_plural': 'Classficationlist', + }, + ), + migrations.CreateModel( + name='License', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=30)), + ('en_name', models.CharField(blank=True, max_length=30, null=True)), + ('desc', models.CharField(blank=True, max_length=255, null=True)), + ('en_desc', models.CharField(blank=True, max_length=255, null=True)), + ('link', models.URLField(blank=True, null=True)), + ('color', models.CharField(choices=[('#878D99', '灰色'), ('#409EFF', '蓝色'), ('#67C23A', '绿色'), ('#EB9E05', '黄色'), ('#FA5555', '红色')], default='blue', max_length=20)), + ('add_time', models.DateTimeField(auto_now_add=True, null=True)), + ], + ), + migrations.CreateModel( + name='Master', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=30)), + ('desc', models.CharField(blank=True, max_length=100, null=True)), + ('image', models.ImageField(blank=True, null=True, upload_to='comment/master/image/%y/%m')), + ('url', models.URLField()), + ('experience', models.FloatField(default=0)), + ('add_time', models.DateTimeField(auto_now_add=True, null=True)), + ], + ), + migrations.CreateModel( + name='PostBaseInfo', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=100)), + ('desc', models.CharField(blank=True, max_length=255, null=True)), + ('author', models.CharField(blank=True, max_length=20, null=True)), + ('post_type', models.CharField(blank=True, choices=[('article', 'article')], max_length=20, null=True)), + ('click_num', models.IntegerField(default=0)), + ('like_num', models.IntegerField(default=0)), + ('comment_num', models.IntegerField(default=0)), + ('front_image', models.ImageField(blank=True, null=True, upload_to='post/image/%y/%m')), + ('front_image_type', models.CharField(choices=[('0', 'small'), ('1', 'medium'), ('2', 'collection')], default='0', max_length=20)), + ('is_hot', models.BooleanField(default=False)), + ('is_recommend', models.BooleanField(default=False)), + ('is_banner', models.BooleanField(default=False)), + ('is_active', models.BooleanField(default=True)), + ('is_commentable', models.BooleanField(default=True)), + ('browse_password', models.CharField(blank=True, max_length=20, null=True)), + ('browse_password_encrypt', models.CharField(blank=True, max_length=100, null=True)), + ('index', models.IntegerField(default=0)), + ('add_time', models.DateTimeField(blank=True, null=True)), + ('category', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to='root.Category')), + ('license', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='root.License')), + ], + ), + migrations.CreateModel( + name='Social', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=30)), + ('desc', models.CharField(max_length=100)), + ('image', models.ImageField(blank=True, null=True, upload_to='comment/social/image/%y/%m')), + ('url', models.URLField()), + ('add_time', models.DateTimeField(auto_now_add=True, null=True)), + ], + ), + migrations.CreateModel( + name='Tag', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=30)), + ('description', models.TextField(max_length=30)), + ('color', models.CharField(default='blue', max_length=20)), + ('add_time', models.DateTimeField(auto_now_add=True, null=True)), + ('category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='root.Category')), + ], + ), + migrations.CreateModel( + name='PostTag', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('add_time', models.DateTimeField(auto_now_add=True, null=True)), + ('post', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='root.PostBaseInfo')), + ('tag', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='root.Tag')), + ], + ), + migrations.AddField( + model_name='postbaseinfo', + name='tags', + field=models.ManyToManyField(through='root.PostTag', to='root.Tag'), + ), + migrations.CreateModel( + name='Picture', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=100)), + ('en_title', models.CharField(blank=True, max_length=100, null=True)), + ('desc', models.CharField(blank=True, max_length=255, null=True)), + ('en_desc', models.CharField(blank=True, max_length=255, null=True)), + ('image', models.ImageField(blank=True, null=True, upload_to='comment/picture/image/%Y/%m')), + ('link', models.URLField(blank=True, null=True)), + ('add_time', models.DateTimeField(auto_now_add=True, null=True)), + ('camera', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='root.Camera')), + ], + ), + migrations.CreateModel( + name='Banner', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=100)), + ('image', models.ImageField(blank=True, null=True, upload_to='comment/banner/image/%y/%m')), + ('url', models.URLField()), + ('index', models.IntegerField(default=0)), + ('add_time', models.DateTimeField(auto_now_add=True, null=True)), + ('category', models.ForeignKey(default='1', on_delete=django.db.models.deletion.CASCADE, to='root.Category')), + ], + ), + ] diff --git a/backend/app/root/migrations/0002_auto_20200418_1841.py b/backend/app/root/migrations/0002_auto_20200418_1841.py new file mode 100644 index 0000000..ac6d520 --- /dev/null +++ b/backend/app/root/migrations/0002_auto_20200418_1841.py @@ -0,0 +1,34 @@ +# Generated by Django 3.0.5 on 2020-04-18 18:41 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('root', '0001_initial'), + ] + + operations = [ + migrations.RemoveField( + model_name='license', + name='en_desc', + ), + migrations.RemoveField( + model_name='license', + name='en_name', + ), + migrations.RemoveField( + model_name='picture', + name='en_desc', + ), + migrations.RemoveField( + model_name='picture', + name='en_title', + ), + migrations.AlterField( + model_name='category', + name='category_level', + field=models.CharField(choices=[('1', 'level1'), ('2', 'level2'), ('3', 'level3')], help_text='level', max_length=20), + ), + ] diff --git a/backend/app/root/models.py b/backend/app/root/models.py index 71a8362..ba06b1a 100644 --- a/backend/app/root/models.py +++ b/backend/app/root/models.py @@ -1,3 +1,286 @@ +import hashlib + from django.db import models +from django.conf import settings + +__author__ = 'Akash Singh' +__date__ = '2020-APR-18' + +CATEGORY_LEVEL = ( + ("1", "level1"), + ("2", "level2"), + ("3", "level3") +) +CATEGORY_TYPE = ( + ("articles", "Articles"), + ("articles/category", "Article Classification"), +) + + +class Category(models.Model): + """ + Model for Category Of Blog App + + FIELD OPTIONS DESCRIPTION + Null If True, Django will store empty values as NULL in the database. Default is False. + Blank If True, the field is allowed to be blank. Default is False. + db_column The name of the database column to use for this field. If this isn’t given, Django will use the field’s name. + Default The default value for the field. This can be a value or a callable object. If callable it will be called every time a new object is created. + help_text Extra “help” text to be displayed with the form widget. It’s useful for documentation even if your field isn’t used on a form. + primary_key If True, this field is the primary key for the model. + editable If False, the field will not be displayed in the admin or any other ModelForm. They are also skipped during model validation. Default is True. + error_messages The error_messages argument lets you override the default messages that the field will raise. Pass in a dictionary with keys matching the error messages you want to override. + help_text Extra “help” text to be displayed with the form widget. It’s useful for documentation even if your field isn’t used on a form. + verbose_name A human-readable name for the field. If the verbose name isn’t given, Django will automatically create it using the field’s attribute name, converting underscores to spaces. + validators A list of validators to run for this field. See the validators documentation for more information. + Unique If True, this field must be unique throughout the table. + + """ + CATEGORY_LEVEL = ( + ("1", "level1"), + ("2", "level2"), + ("3", "level3") + ) + CATEGORY_TYPE = ( + ("articles", "Articles"), + ("articles/category", "Article Classification"), + ) + name = models.CharField(max_length=30, default="", verbose_name="Category Name", help_text="Root Category Name") + category_type = models.CharField(max_length=30, choices=CATEGORY_TYPE, verbose_name="Category Type ",help_text="Used to configure route ") + desc = models.TextField(null=True, blank=True, verbose_name="decription", help_text="category description") + image = models.ImageField(upload_to="comment/category/image/%Y/%m", null=True, blank=True, help_text="图片") + category_level = models.CharField(max_length=20, choices=CATEGORY_LEVEL, help_text="level") + parent_category = models.ForeignKey("self", null=True, blank=True, verbose_name="parent", help_text="parent category",related_name="sub_category", on_delete=models.CASCADE) + + is_active = models.BooleanField(default=True, verbose_name="Active Status", help_text="Active") + is_tab = models.BooleanField(default=True, verbose_name="Tab Status", help_text="Tab") + index = models.IntegerField(default=0, verbose_name="indexing", help_text=".") + add_time = models.DateTimeField(auto_now_add=True, verbose_name="Add Time", help_text="When was added") + + class Meta: + verbose_name = "Classfication" + verbose_name_plural = verbose_name + 'list' + + def save(self, *args, **kwargs): + super(Category,self).save(*args, **kwargs) + + def __str__(self): + return '{0} - {1} - {2}'.format(self.name, self.get_category_type_display(), self.get_category_level_display()) + + +class Tag(models.Model): + """ + Tag That Category Post COmprises of + """ + name = models.CharField(max_length=30, null=False, blank=False, ) + description = models.TextField(max_length=30) + category = models.ForeignKey(Category, null=True, blank=True,on_delete=models.CASCADE) + color = models.CharField(max_length=20, default="blue", ) + add_time = models.DateTimeField(auto_now_add=True, null=True, blank=True) + + def save(self, *args, **kwargs): + if not self.description: + self.description = self.name + super(Tag, self).save(*args, **kwargs) + + class Meta: + pass + + def __str__(self): + return self.name + + +class License(models.Model): + """ + Lisence + """ + COLOR_TYPE = ( + ("#878D99", "灰色"), + ("#409EFF", "蓝色"), + ("#67C23A", "绿色"), + ("#EB9E05", "黄色"), + ("#FA5555", "红色") + ) + name = models.CharField(max_length=30, null=False, blank=False, ) + desc = models.CharField(max_length=255, null=True, blank=True,) + link = models.URLField(null=True, blank=True, ) + color = models.CharField(max_length=20, default="blue", choices=COLOR_TYPE) + add_time = models.DateTimeField(auto_now_add=True, null=True, blank=True) + + def save(self, *args, **kwargs): + super(License, self).save(*args, **kwargs) + + class Meta: + pass + + def __str__(self): + return self.name + + +class Camera(models.Model): + """ + Camera + """ + device = models.CharField(max_length=30 ) + version = models.CharField(max_length=200 ) + environment = models.CharField(max_length=200 ) + add_time = models.DateTimeField(auto_now_add=True, null=True, blank=True) + + class Meta: + pass + + def __str__(self): + return self.device + + +class Picture(models.Model): + """ + Picture + """ + title = models.CharField(max_length=100, null=False, blank=False, ) + desc = models.CharField(max_length=255, null=True, blank=True, ) + image = models.ImageField(upload_to="comment/picture/image/%Y/%m", null=True, blank=True) + camera = models.ForeignKey(Camera, null=True, blank=True,on_delete=models.CASCADE) + link = models.URLField(null=True, blank=True,) + add_time = models.DateTimeField(auto_now_add=True, null=True, blank=True) + + def save(self, *args, **kwargs): + super(Picture, self).save(*args, **kwargs) + + class Meta: + pass + + def __str__(self): + return self.title + + +class PostBaseInfo(models.Model): + """ + Post + """ + POST_TYPE = ( + ("article", "article"), + ) + FRONT_IMAGE_TYPE = ( + ("0", "small"), + ("1", 'medium'), + ("2", "collection") + ) + title = models.CharField(max_length=100, null=False, blank=False) + desc = models.CharField(max_length=255, null=True, blank=True) + author = models.CharField(max_length=20, null=True, blank=True) + category = models.ForeignKey(Category, null=False, blank=False,on_delete=models.DO_NOTHING ) + tags = models.ManyToManyField(Tag, through="PostTag", through_fields=('post', 'tag')) + post_type = models.CharField(max_length=20, choices=POST_TYPE, null=True, blank=True) + click_num = models.IntegerField(default=0) + like_num = models.IntegerField(default=0 ) + comment_num = models.IntegerField(default=0 ) + front_image = models.ImageField(upload_to="post/image/%y/%m", null=True, blank=True) + front_image_type = models.CharField(max_length=20, default="0", choices=FRONT_IMAGE_TYPE) + license = models.ForeignKey(License, null=True, blank=True,on_delete=models.CASCADE) + is_hot = models.BooleanField(default=False) + is_recommend = models.BooleanField(default=False ) + is_banner = models.BooleanField(default=False) + is_active = models.BooleanField(default=True) + is_commentable = models.BooleanField(default=True) + browse_password = models.CharField(max_length=20, null=True, blank=True) + browse_password_encrypt = models.CharField(max_length=100, null=True, blank=True) + index = models.IntegerField(default=0) + add_time = models.DateTimeField(null=True, blank=True) + + def save(self, *args, **kwargs): + + if self.browse_password and len(self.browse_password) > 0: + md5 = hashlib.md5() + md5.update(self.browse_password.encode('utf8')) + self.browse_password_encrypt = md5.hexdigest() + else: + self.browse_password_encrypt = None + super(PostBaseInfo, self).save(*args, **kwargs) + + # Will Make It Get From Enviroment + def get_absolute_url(self): + return '{0}/{1}/{2}'.format(settings.SITE_BASE_URL, self.post_type, self.id) + + class Meta: + pass + + def __str__(self): + return self.title + + +class PostTag(models.Model): + """ + Post tags + """ + post = models.ForeignKey(PostBaseInfo, null=False, blank=False,on_delete=models.CASCADE) + tag = models.ForeignKey(Tag, null=False, blank=False,on_delete=models.CASCADE) + add_time = models.DateTimeField(auto_now_add=True, null=True, blank=True) + + class Meta: + pass + + def __str__(self): + return self.tag.name + + +class Banner(models.Model): + """ + Banner for The Blog Special + """ + title = models.CharField(max_length=100) + image = models.ImageField(upload_to="comment/banner/image/%y/%m", null=True, blank=True, ) + url = models.URLField(max_length=200,) + category = models.ForeignKey(Category, default='1', null=False, blank=False,on_delete=models.CASCADE) + index = models.IntegerField(default=0, ) + add_time = models.DateTimeField(auto_now_add=True, null=True, blank=True) + + def save(self, *args, **kwargs): + super(Banner, self).save(*args, **kwargs) + + class Meta: + pass + + def __str__(self): + return self.title + + +class Social(models.Model): + """ + Social Media Handle of Blogyy + """ + name = models.CharField(max_length=30, ) + desc = models.CharField(max_length=100, ) + image = models.ImageField(upload_to="comment/social/image/%y/%m", null=True, blank=True) + url = models.URLField(max_length=200) + add_time = models.DateTimeField(auto_now_add=True, null=True, blank=True) + + def save(self, *args, **kwargs): + super(Social, self).save(*args, **kwargs) + + class Meta: + pass + + def __str__(self): + return self.name + + +class Master(models.Model): + """¡ + Main Model + """ + name = models.CharField(max_length=30) + desc = models.CharField(max_length=100, null=True, blank=True, ) + image = models.ImageField(upload_to="comment/master/image/%y/%m", null=True, blank=True) + url = models.URLField(max_length=200 ) + experience = models.FloatField(default=0) + add_time = models.DateTimeField(auto_now_add=True, null=True, blank=True) + + def save(self, *args, **kwargs): + super(Master, self).save(*args, **kwargs) + + class Meta: + pass -# Create your models here. + def __str__(self): + return self.name diff --git a/backend/app/root/serializer.py b/backend/app/root/serializer.py new file mode 100644 index 0000000..5abaea1 --- /dev/null +++ b/backend/app/root/serializer.py @@ -0,0 +1,137 @@ +from rest_framework import serializers + +from .models import Category, Tag, License, PostBaseInfo, Banner, \ + Camera, Picture, Social, Master, PostTag + +from blogyy.settings import MEDIA_URL_PREFIX + + +class OrderCategoryListSerializer(serializers.ListSerializer): + + def to_representation(self, data): + data = data.filter(is_active=True).order_by('index') + return super(OrderCategoryListSerializer, self).to_representation(data) + + +class CategorySerializer3(serializers.ModelSerializer): + + class Meta: + list_serializer_class = OrderCategoryListSerializer + model = Category + fields = "__all__" + + +class CategorySerializer2(serializers.ModelSerializer): + sub_category = CategorySerializer3(many=True) + + class Meta: + list_serializer_class = OrderCategoryListSerializer + model = Category + fields = "__all__" + + +class CategorySerializer(serializers.ModelSerializer): + sub_category = CategorySerializer2(many=True) + + class Meta: + model = Category + fields = "__all__" + + +class SingleLevelCategorySerializer(serializers.ModelSerializer): + + class Meta: + model = Category + fields = "__all__" + + +class TagSerializer(serializers.ModelSerializer): + related_post_num = serializers.SerializerMethodField() + + def get_related_post_num(self, tag): + return len(PostTag.objects.filter(tag__id=tag.id)) + + class Meta: + model = Tag + fields = ('name', 'color', 'related_post_num') + + +class LicenseSerializer(serializers.ModelSerializer): + + class Meta: + model = License + fields = "__all__" + + +class CameraSerializer(serializers.ModelSerializer): + + class Meta: + model = Camera + fields = "__all__" + + +class PictureSerializer(serializers.ModelSerializer): + camera = CameraSerializer() + + class Meta: + model = Picture + fields = "__all__" + + +class PostBaseInfoSerializer(serializers.ModelSerializer): + tags = TagSerializer(many=True) + front_image = serializers.SerializerMethodField() + need_auth = serializers.SerializerMethodField() + + def get_front_image(self, article): + if article.front_image: + return "{0}/{1}".format(MEDIA_URL_PREFIX, article.front_image) + + def get_need_auth(self, article): + if article.browse_password_encrypt: + return True + else: + return False + + class Meta: + model = PostBaseInfo + fields = ( + 'id', 'title', 'desc', 'tags', 'like_num', 'comment_num', 'click_num', 'front_image', 'front_image_type', + 'is_hot', + 'is_recommend', 'is_banner', 'is_commentable', + 'post_type', 'need_auth', 'add_time') + + +class BannerSerializer(serializers.ModelSerializer): + + class Meta: + model = Banner + fields = "__all__" + + +class SocialSerializer(serializers.ModelSerializer): + image = serializers.SerializerMethodField() + + def get_image(self, social): + if social.image: + return "{0}/{1}".format(MEDIA_URL_PREFIX, social.image) + + class Meta: + model = Social + fields = ('id', 'name', 'image', 'url', 'desc') + + +class MasterSerializer(serializers.ModelSerializer): + + class Meta: + model = Master + fields = "__all__" + + +class PostLikeSerializer(serializers.Serializer): + post_id = serializers.IntegerField(required=True, label='post') + + +class VerifyPostAuthSerializer(serializers.Serializer): + post_id = serializers.CharField(max_length=11, min_length=1, required=True, label='postid') + browse_auth = serializers.CharField(max_length=64, min_length=6, required=True, label='brouseauth') diff --git a/backend/app/root/views.py b/backend/app/root/views.py index 91ea44a..bcb349c 100644 --- a/backend/app/root/views.py +++ b/backend/app/root/views.py @@ -1,3 +1,155 @@ -from django.shortcuts import render +from rest_framework import mixins, status +from rest_framework.response import Response -# Create your views here. +from django_filters.rest_framework import DjangoFilterBackend +from rest_framework import viewsets, filters + +from .models import Category, Tag, Banner, PostBaseInfo +from .serializer import CategorySerializer, SingleLevelCategorySerializer, TagSerializer, BannerSerializer, PostBaseInfoSerializer, PostLikeSerializer, VerifyPostAuthSerializer +from .filters import CategoryFilter, BannerFilter, PostBaseInfoFilter +from app.base.utils import CustomeLimitOffsetPagination, CustomePageNumberPagination + + +class CategoryListViewset(viewsets.ReadOnlyModelViewSet): + """ + List: + Category + """ + queryset = Category.objects.filter(is_active=True) + pagination_class = CustomePageNumberPagination + serializer_class = CategorySerializer + filter_backends = (DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter) + filter_class = CategoryFilter + search_fields = ('name', 'category_type', 'desc') + ordering_fields = ('category_level', 'index') + ordering = ('index',) + + +class SingleLevelCategoryListViewset(viewsets.ReadOnlyModelViewSet): + """ + List: + One Level Category + """ + queryset = Category.objects.filter(is_active=True) + pagination_class = CustomePageNumberPagination + serializer_class = SingleLevelCategorySerializer + filter_backends = (DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter) + filter_class = CategoryFilter + search_fields = ('name', 'category_type', 'desc') + ordering_fields = ('category_level', 'index') + ordering = ('index',) + + +class TagListViewset(viewsets.ReadOnlyModelViewSet): + """ + List: + Tag List + """ + queryset = Tag.objects.all() + serializer_class = TagSerializer + search_fields = ('name', 'subname') + ordering_fields = ('name', 'subname') + + +class BannerListViewset(viewsets.ReadOnlyModelViewSet): + """ + List: + Banner list + """ + filter_backends = (DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter) + filter_class = BannerFilter + queryset = Banner.objects.all().order_by("-index") + serializer_class = BannerSerializer + + +class PostBaseInfoListViewset(viewsets.ReadOnlyModelViewSet): + """ + List: + Post Base Info + """ + queryset = PostBaseInfo.objects.filter(is_active=True) + pagination_class = CustomeLimitOffsetPagination + filter_backends = (DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter) + filter_class = PostBaseInfoFilter + search_fields = ('title', 'subtitle', 'abstract', 'desc') + ordering_fields = ('id', 'click_num', 'like_num', 'comment_num', 'add_time') + serializer_class = PostBaseInfoSerializer + + +class PostLikeViewset(mixins.CreateModelMixin, viewsets.GenericViewSet): + """ + PostLike + """ + serializer_class = PostLikeSerializer + + def create(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) # status 400 + + post_id = serializer.validated_data["post_id"] + + post = PostBaseInfo.objects.filter(id=post_id)[0] + + if post is not None: + post.like_num += 1 + post.save() + context = { + "post": post_id + } + return Response(context, status.HTTP_201_CREATED) + else: + context = { + "error": '500 Something Went Wrong' + } + return Response(context, status=status.HTTP_400_BAD_REQUEST) + self.perform_create(serializer) + headers = self.get_success_headers(serializer.data) + return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) + + +class VerifyPostAuthViewset(mixins.CreateModelMixin, viewsets.GenericViewSet): + """ + Verify Post Auth + """ + serializer_class = VerifyPostAuthSerializer + + def create(self, request, *args, **kwargs): + """ + """ + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) # status 400 + + post_id = serializer.validated_data['post_id'] + browse_auth = serializer.validated_data['browse_auth'] + + if post_id is None: + context = { + "error": 'No Post Id' + } + return Response(context, status=status.HTTP_400_BAD_REQUEST) + + if browse_auth is None: + context = { + "error": 'Auth Failed' + } + return Response(context, status=status.HTTP_400_BAD_REQUEST) + + posts = PostBaseInfo.objects.filter(id=post_id) + + if len(posts): + if posts[0].browse_password_encrypt == browse_auth: + context = { + 'post_id': posts[0].id + } + return Response(context, status.HTTP_202_ACCEPTED) + else: + context = { + 'error': 'Invalid' + } + return Response(context, status=status.HTTP_403_FORBIDDEN) + else: + context = { + 'error': 'No Post', + 'post_id': post_id + } + return Response(context, status=status.HTTP_400_BAD_REQUEST) \ No newline at end of file diff --git a/backend/base/site/image/20/04/e42b779c-0777-4ac6-8a4f-5fb0eeac2a79.png b/backend/base/site/image/20/04/e42b779c-0777-4ac6-8a4f-5fb0eeac2a79.png new file mode 100644 index 0000000000000000000000000000000000000000..ea189d6252c46594b72ad867589068ca8a49a4c3 GIT binary patch literal 3440 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

>cYufR= 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 Date: Sun, 19 Apr 2020 13:29:20 +0530 Subject: [PATCH 4/5] Create readme.md --- frontend/readme.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 frontend/readme.md diff --git a/frontend/readme.md b/frontend/readme.md new file mode 100644 index 0000000..37dcabe --- /dev/null +++ b/frontend/readme.md @@ -0,0 +1,11 @@ +this is the readme of the frontend structure. +Requirements: + - Angular 9 + - RxJs + - Lodash + - ngx-logger + +Theming: + - Sass + - Bootstrap + From 21c563f1f5a4e99d3481350e90add58a68ca4795 Mon Sep 17 00:00:00 2001 From: Akash Singh Date: Sun, 19 Apr 2020 18:05:10 +0530 Subject: [PATCH 5/5] #6-article: Created app for Article --- backend/app/article/admin.py | 12 ++++ backend/app/article/apps.py | 2 +- backend/app/article/filters.py | 19 ++++++ .../app/article/migrations/0001_initial.py | 34 +++++++++++ backend/app/article/models.py | 37 ++++++++++++ backend/app/article/serializer.py | 48 +++++++++++++++ backend/app/article/views.py | 57 +++++++++++++++++- backend/app/base/utils.py | 40 ++++++++++++ backend/app/root/models.py | 4 +- backend/app/root/views.py | 2 +- backend/blogyy/settings.py | 5 +- backend/blogyy/urls.py | 5 ++ .../category/image/2020/04/download.png | Bin 0 -> 3090 bytes backend/db.sqlite3 | Bin 266240 -> 290816 bytes 14 files changed, 259 insertions(+), 6 deletions(-) create mode 100644 backend/app/article/filters.py create mode 100644 backend/app/article/migrations/0001_initial.py create mode 100644 backend/app/article/serializer.py create mode 100644 backend/comment/category/image/2020/04/download.png diff --git a/backend/app/article/admin.py b/backend/app/article/admin.py index 8c38f3f..14f6de0 100644 --- a/backend/app/article/admin.py +++ b/backend/app/article/admin.py @@ -1,3 +1,15 @@ from django.contrib import admin # Register your models here. +from .models import ArticleInfo, ArticleDetail + +# +# class ArticleinfoInline(admin.TabularInline): +# model = ArticleDetail +# +# # @admin.register(ArticleInfo) +# class ArticleDetail(admin.ModelAdmin): +# inlines = [ArticleinfoInline] + +admin.site.register(ArticleInfo) +admin.site.register(ArticleDetail) diff --git a/backend/app/article/apps.py b/backend/app/article/apps.py index 8295b57..2312bc4 100644 --- a/backend/app/article/apps.py +++ b/backend/app/article/apps.py @@ -2,4 +2,4 @@ class ArticleConfig(AppConfig): - name = 'article' + name = 'app.article' diff --git a/backend/app/article/filters.py b/backend/app/article/filters.py new file mode 100644 index 0000000..4d79514 --- /dev/null +++ b/backend/app/article/filters.py @@ -0,0 +1,19 @@ +from django.db.models import Q +import django_filters +from .models import ArticleInfo + + +class ArticleFilter(django_filters.rest_framework.FilterSet): + + time_min = django_filters.DateFilter(field_name='add_time', lookup_expr='gte') + time_max = django_filters.DateFilter(field_name='add_time', lookup_expr='lte') + + top_category = django_filters.NumberFilter(method='top_category_filter') + + def top_category_filter(self, queryset, name, value): + return queryset.filter(Q(category_id=value) | Q(category__parent_category_id=value) | Q( + category__parent_category__parent_category_id=value)) + + class Meta: + model = ArticleInfo + fields = ['time_min', 'time_max', 'is_hot', 'is_recommend', 'is_banner'] diff --git a/backend/app/article/migrations/0001_initial.py b/backend/app/article/migrations/0001_initial.py new file mode 100644 index 0000000..7fd3d6b --- /dev/null +++ b/backend/app/article/migrations/0001_initial.py @@ -0,0 +1,34 @@ +# Generated by Django 3.0.5 on 2020-04-19 09:08 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('root', '0002_auto_20200418_1841'), + ] + + operations = [ + migrations.CreateModel( + name='ArticleInfo', + fields=[ + ('postbaseinfo_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='root.PostBaseInfo')), + ], + bases=('root.postbaseinfo',), + ), + migrations.CreateModel( + name='ArticleDetail', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('origin_content', models.TextField(blank=True)), + ('formatted_content', models.TextField()), + ('add_time', models.DateTimeField(blank=True, null=True)), + ('update_time', models.DateTimeField(blank=True, null=True)), + ('article_info', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='details', to='article.ArticleInfo')), + ], + ), + ] diff --git a/backend/app/article/models.py b/backend/app/article/models.py index 71a8362..d8869cd 100644 --- a/backend/app/article/models.py +++ b/backend/app/article/models.py @@ -1,3 +1,40 @@ from django.db import models # Create your models here. +from django.db import models +import markdown + +from app.root.models import Category, Tag, PostBaseInfo +from app.base.utils import MARKDOWN_EXTENSIONS + + +class ArticleInfo(PostBaseInfo): + + def save(self, *args, **kwargs): + self.post_type = 'article' + super(ArticleInfo, self).save(*args, **kwargs) + + def __str__(self): + return self.title + + class Meta: + pass + + + +class ArticleDetail(models.Model): + article_info = models.ForeignKey(ArticleInfo, null=True, blank=True, related_name='details',on_delete=models.CASCADE) + origin_content = models.TextField(null=False, blank=True) + formatted_content = models.TextField() + add_time = models.DateTimeField(null=True, blank=True) + update_time = models.DateTimeField(null=True, blank=True,) + + def save(self, *args, **kwargs): + self.formatted_content = markdown.markdown(self.origin_content, extensions=MARKDOWN_EXTENSIONS,lazy_ol=False) + super(ArticleDetail, self).save(*args, **kwargs) + + def __str__(self): + return self.article_info.title + + class Meta: + pass diff --git a/backend/app/article/serializer.py b/backend/app/article/serializer.py new file mode 100644 index 0000000..e51e00f --- /dev/null +++ b/backend/app/article/serializer.py @@ -0,0 +1,48 @@ +# _*_ coding: utf-8 _*_ + +from rest_framework import serializers + +from .models import ArticleInfo, ArticleDetail +from app.root.serializer import SingleLevelCategorySerializer, TagSerializer, LicenseSerializer +from django.conf import settings + + +class ArticleDetailSerializer(serializers.ModelSerializer): + class Meta: + model = ArticleDetail + fields = ( 'formatted_content', 'add_time', 'update_time') + + +class ArticleDetailInfoSerializer(serializers.ModelSerializer): + category = SingleLevelCategorySerializer() + tags = TagSerializer(many=True) + license = LicenseSerializer() + details = ArticleDetailSerializer(many=True) + browse_auth = serializers.CharField(required=False, max_length=100, write_only=True) + + class Meta: + model = ArticleInfo + exclude = ('browse_password', 'browse_password_encrypt') + + +class ArticleBaseInfoSerializer(serializers.ModelSerializer): + tags = TagSerializer(many=True) + front_image = serializers.SerializerMethodField() + need_auth = serializers.SerializerMethodField() + + def get_front_image(self, article): + if article.front_image: + return "{0}/{1}".format(settings.MEDIA_URL_PREFIX, article.front_image) + + def get_need_auth(self, article): + if article.browse_password_encrypt: + return True + else: + return False + + class Meta: + model = ArticleInfo + fields = ( + 'id', 'title', 'desc', 'author', 'tags', 'click_num', 'like_num', 'comment_num', 'post_type', + 'front_image', 'is_recommend', 'is_hot', 'is_banner', 'is_commentable', 'need_auth', + 'front_image_type', 'index', 'add_time') diff --git a/backend/app/article/views.py b/backend/app/article/views.py index 91ea44a..b5668cd 100644 --- a/backend/app/article/views.py +++ b/backend/app/article/views.py @@ -1,3 +1,58 @@ from django.shortcuts import render -# Create your views here. + +from django_filters.rest_framework import DjangoFilterBackend +from rest_framework import viewsets, filters, status, mixins +from rest_framework.response import Response + +from .models import ArticleInfo +from .serializer import ArticleBaseInfoSerializer, ArticleDetailInfoSerializer +from .filters import ArticleFilter +from app.base.utils import CustomeLimitOffsetPagination + + +class ArticleBaseInfoListViewset(viewsets.ReadOnlyModelViewSet): + queryset = ArticleInfo.objects.filter(is_active=True) + serializer_class = ArticleBaseInfoSerializer + + filter_backends = (DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter) + filter_class = ArticleFilter + search_fields = ('title', 'subtitle', 'abstract', 'desc') + ordering_fields = ('click_num', 'like_num', 'comment_num', 'index', 'add_time') + ordering = ('-index', '-add_time') + pagination_class = CustomeLimitOffsetPagination + + def retrieve(self, request, *args, **kwargs): + instance = self.get_object() + instance.click_num += 1 + instance.save() + serializer = self.get_serializer(instance) + return Response(serializer.data) + + +class ArticleDetailInfoListViewset(mixins.RetrieveModelMixin, viewsets.GenericViewSet): + queryset = ArticleInfo.objects.filter(is_active=True) + serializer_class = ArticleDetailInfoSerializer + + filter_backends = (DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter) + search_fields = ('title', 'subtitle', 'abstract', 'desc') + ordering_fields = ('click_num', 'like_num', 'comment_num', 'add_time') + + pagination_class = CustomeLimitOffsetPagination + + def retrieve(self, request, *args, **kwargs): + instance = self.get_object() + if instance.browse_password_encrypt: + browse_auth = "" + if 'browse_auth' in request.query_params: + browse_auth = request.query_params['browse_auth'] + if browse_auth != instance.browse_password_encrypt: + context = { + "error": "Protected Article 401 UNAUTHORIZED" + } + return Response(context, status=status.HTTP_401_UNAUTHORIZED) + + instance.click_num += 1 + instance.save() + serializer = self.get_serializer(instance) + return Response(serializer.data) \ No newline at end of file diff --git a/backend/app/base/utils.py b/backend/app/base/utils.py index eb98bfe..719ffaf 100644 --- a/backend/app/base/utils.py +++ b/backend/app/base/utils.py @@ -16,4 +16,44 @@ class CustomeLimitOffsetPagination(LimitOffsetPagination): min_offset = 0 +# python markdown extension default +MARKDOWN_EXTENSIONS_DEFAULT = [ + 'markdown.extensions.abbr', + 'markdown.extensions.attr_list', + 'markdown.extensions.def_list', + 'markdown.extensions.footnotes', + 'markdown.extensions.tables', + 'markdown.extensions.admonition', + 'markdown.extensions.codehilite', + 'markdown.extensions.meta', + 'markdown.extensions.nl2br', + 'markdown.extensions.sane_lists', + 'markdown.extensions.smarty', + 'markdown.extensions.toc', + 'markdown.extensions.wikilinks' +] + +# python markdown extension pymdownx +# https://facelessuser.github.io/pymdown-extensions/usage_notes/ +MARKDOWN_EXTENSIONS_PYMDOWNX = [ + 'pymdownx.extra', + 'pymdownx.superfences', + 'pymdownx.magiclink', + 'pymdownx.tilde', + 'pymdownx.emoji', + 'pymdownx.tasklist', + 'pymdownx.superfences', + 'pymdownx.details', + 'pymdownx.highlight', + 'pymdownx.inlinehilite', + 'pymdownx.keys', + 'pymdownx.progressbar', + 'pymdownx.critic', + 'pymdownx.arithmatex' +] + +MARKDOWN_EXTENSIONS = MARKDOWN_EXTENSIONS_DEFAULT + MARKDOWN_EXTENSIONS_PYMDOWNX + + + ALLOWED_PROTOCOLS = ['http', 'https', 'mailto'] \ No newline at end of file diff --git a/backend/app/root/models.py b/backend/app/root/models.py index ba06b1a..4806aa0 100644 --- a/backend/app/root/models.py +++ b/backend/app/root/models.py @@ -2,7 +2,7 @@ from django.db import models from django.conf import settings - +import django.apps __author__ = 'Akash Singh' __date__ = '2020-APR-18' @@ -48,7 +48,7 @@ class Category(models.Model): name = models.CharField(max_length=30, default="", verbose_name="Category Name", help_text="Root Category Name") category_type = models.CharField(max_length=30, choices=CATEGORY_TYPE, verbose_name="Category Type ",help_text="Used to configure route ") desc = models.TextField(null=True, blank=True, verbose_name="decription", help_text="category description") - image = models.ImageField(upload_to="comment/category/image/%Y/%m", null=True, blank=True, help_text="图片") + image = models.ImageField(upload_to="comment/category/image/%Y/%m", null=True, blank=True) category_level = models.CharField(max_length=20, choices=CATEGORY_LEVEL, help_text="level") parent_category = models.ForeignKey("self", null=True, blank=True, verbose_name="parent", help_text="parent category",related_name="sub_category", on_delete=models.CASCADE) diff --git a/backend/app/root/views.py b/backend/app/root/views.py index bcb349c..08843dc 100644 --- a/backend/app/root/views.py +++ b/backend/app/root/views.py @@ -137,7 +137,7 @@ def create(self, request, *args, **kwargs): posts = PostBaseInfo.objects.filter(id=post_id) if len(posts): - if posts[0].browse_password_encrypt == browse_auth: + if posts[0].browse_password_encrypt != browse_auth: context = { 'post_id': posts[0].id } diff --git a/backend/blogyy/settings.py b/backend/blogyy/settings.py index d569cca..f81dded 100644 --- a/backend/blogyy/settings.py +++ b/backend/blogyy/settings.py @@ -43,12 +43,15 @@ 'app.root.apps.RootConfig', 'app.base.apps.BaseConfig', +'app.article.apps.ArticleConfig', ] EXTRA_APPS = [ 'rest_framework', 'django_filters', -'rest_framework_swagger' + 'rest_framework_swagger', + 'markdown', + ] INSTALLED_APPS += PERSONAL_APPS +EXTRA_APPS diff --git a/backend/blogyy/urls.py b/backend/blogyy/urls.py index 1b0ebfe..e0ecc08 100644 --- a/backend/blogyy/urls.py +++ b/backend/blogyy/urls.py @@ -20,6 +20,7 @@ from rest_framework.authtoken import views from rest_framework_swagger.views import get_swagger_view +from app.article.views import ArticleBaseInfoListViewset, ArticleDetailInfoListViewset from app.base.views import SiteInfoViewset, BloggerInfoViewset from app.root.views import CategoryListViewset, SingleLevelCategoryListViewset, TagListViewset, \ BannerListViewset, PostBaseInfoListViewset, PostLikeViewset, VerifyPostAuthViewset @@ -39,6 +40,8 @@ router.register(r'verifyPostAuth', VerifyPostAuthViewset, basename="verifyPostAuth") router.register(r'likePost', PostLikeViewset, basename="likePost") +router.register(r'post', ArticleBaseInfoListViewset, basename="articleBaseInfos") +router.register(r'postDetail', ArticleDetailInfoListViewset, basename="articleDetailInfos") schema_view = get_swagger_view(title='Bloggy API') @@ -47,4 +50,6 @@ url(r'^$', schema_view), path('admin/', admin.site.urls), url(r'^api/', include(router.urls)), + + # url(r'^lol/', ) ] diff --git a/backend/comment/category/image/2020/04/download.png b/backend/comment/category/image/2020/04/download.png new file mode 100644 index 0000000000000000000000000000000000000000..521dd3283dfe0d9d251d1767b24f6b469df3d0b3 GIT binary patch literal 3090 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