From 435bfab2a7de9707699d8fda31eb2de5055e755f Mon Sep 17 00:00:00 2001 From: liangliang Date: Sun, 1 Jul 2018 02:16:23 +0800 Subject: [PATCH 001/593] add sentry log --- DjangoBlog/settings.py | 48 +++++++++++++++++++++++++++++------- requirements.txt | 1 + travis_test/requirements.txt | 1 + 3 files changed, 41 insertions(+), 9 deletions(-) diff --git a/DjangoBlog/settings.py b/DjangoBlog/settings.py index 4a55e7b27..3117cadd6 100644 --- a/DjangoBlog/settings.py +++ b/DjangoBlog/settings.py @@ -11,6 +11,7 @@ """ import sys import os +import raven # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) @@ -30,6 +31,17 @@ ALLOWED_HOSTS = ['*', 'www.lylinux.net', '127.0.0.1', 'example.com'] # Application definition + +SITE_ROOT = os.path.dirname(os.path.abspath(__file__)) +SITE_ROOT = os.path.abspath(os.path.join(SITE_ROOT, '../')) + +RAVEN_CONFIG = { + 'dsn': os.environ.get('SENTRY_DSN'), + # If you are using git, you can also automatically configure the + # release based on the git info. + 'release': raven.fetch_git_sha(os.path.abspath(SITE_ROOT)), +} + INSTALLED_APPS = [ # 'django.contrib.admin', 'django.contrib.admin.apps.SimpleAdminConfig', @@ -40,6 +52,7 @@ 'django.contrib.staticfiles', 'django.contrib.sites', 'django.contrib.sitemaps', + 'raven.contrib.django.raven_compat', 'pagedown', 'haystack', 'blog', @@ -138,9 +151,6 @@ # https://docs.djangoproject.com/en/1.10/howto/static-files/ -SITE_ROOT = os.path.dirname(os.path.abspath(__file__)) -SITE_ROOT = os.path.abspath(os.path.join(SITE_ROOT, '../')) - HAYSTACK_CONNECTIONS = { 'default': { 'ENGINE': 'DjangoBlog.whoosh_cn_backend.WhooshEngine', @@ -193,7 +203,7 @@ # Emial: EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' -#EMAIL_USE_TLS = True +# EMAIL_USE_TLS = True EMAIL_USE_SSL = True EMAIL_HOST = 'smtp.mxhichina.com' @@ -206,15 +216,20 @@ ADMINS = [('liangliang', 'liangliangyy@gmail.com')] # 微信管理员密码(两次md5获得) WXADMIN = '995F03AC401D6CABABAEF756FC4D43C7' + LOGGING = { 'version': 1, 'disable_existing_loggers': False, + 'root': { + 'level': 'WARNING', + 'handlers': ['sentry', 'console', 'log_file'], + }, 'formatters': { 'verbose': { 'format': '[%(asctime)s] %(levelname)s [%(name)s.%(funcName)s:%(lineno)d] %(message)s', }, 'simple': { - 'format': '%(levelname)s %(asctime)s %(message)s' + 'format': '[%(asctime)s] %(levelname)s [%(name)s.%(funcName)s:%(lineno)d] %(message)s', }, }, 'filters': { @@ -229,7 +244,7 @@ 'log_file': { 'level': 'INFO', 'class': 'logging.handlers.RotatingFileHandler', - 'filename': 'djangoblog.log', + 'filename': 'xiaobiaobai.log', 'maxBytes': 16777216, # 16 MB 'formatter': 'verbose' }, @@ -246,19 +261,34 @@ 'level': 'ERROR', 'filters': ['require_debug_false'], 'class': 'django.utils.log.AdminEmailHandler' - } + }, + 'sentry': { + 'level': 'ERROR', # To capture more than ERROR, change to WARNING, INFO, etc. + 'class': 'raven.contrib.django.raven_compat.handlers.SentryHandler', + 'tags': {'custom-tag': 'x'}, + }, }, 'loggers': { - 'djangoblog': { + 'xiaobiaobai': { 'handlers': ['log_file', 'console'], 'level': 'INFO', 'propagate': True, }, 'django.request': { - 'handlers': ['mail_admins'], + 'handlers': ['mail_admins', 'sentry'], 'level': 'ERROR', 'propagate': False, }, + 'raven': { + 'level': 'DEBUG', + 'handlers': ['console'], + 'propagate': False, + }, + 'sentry.errors': { + 'level': 'DEBUG', + 'handlers': ['console'], + 'propagate': False, + }, } } diff --git a/requirements.txt b/requirements.txt index 0e5aed9be..6f7c56ea0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -26,6 +26,7 @@ pyparsing==2.2.0 python-memcached==1.59 python-slugify==1.2.5 pytz==2018.4 +raven==6.9.0 rcssmin==1.0.6 requests==2.18.4 rjsmin==1.0.12 diff --git a/travis_test/requirements.txt b/travis_test/requirements.txt index 71da573d3..4fd50ee4c 100644 --- a/travis_test/requirements.txt +++ b/travis_test/requirements.txt @@ -25,6 +25,7 @@ PyMySQL==0.8.1 pyparsing==2.2.0 python-slugify==1.2.5 pytz==2018.4 +raven==6.9.0 rcssmin==1.0.6 requests==2.18.4 rjsmin==1.0.12 From 2aa9cee33feba6ad77d461eecc0d4338efa078fc Mon Sep 17 00:00:00 2001 From: liangliang Date: Sun, 1 Jul 2018 02:47:03 +0800 Subject: [PATCH 002/593] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E6=97=A5=E5=BF=97?= =?UTF-8?q?=E7=BA=A7=E5=88=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- blog/templatetags/blog_tags.py | 6 ++++-- blog/views.py | 5 +++-- oauth/oauthmanager.py | 31 +++++++++++++++++++------------ owntracks/views.py | 2 +- servermanager/Api/commonapi.py | 2 +- 5 files changed, 28 insertions(+), 18 deletions(-) diff --git a/blog/templatetags/blog_tags.py b/blog/templatetags/blog_tags.py index c6695c3ab..3b70846dc 100644 --- a/blog/templatetags/blog_tags.py +++ b/blog/templatetags/blog_tags.py @@ -39,7 +39,8 @@ def timeformat(data): return data.strftime(settings.TIME_FORMAT) # print(data.strftime(settings.TIME_FORMAT)) # return "ddd" - except: + except Exception as e: + logger.error(e) return "" @@ -47,7 +48,8 @@ def timeformat(data): def datetimeformat(data): try: return data.strftime(settings.DATE_TIME_FORMAT) - except: + except Exception as e: + logger.error(e) return "" diff --git a/blog/views.py b/blog/views.py index f1ace20db..96464b899 100644 --- a/blog/views.py +++ b/blog/views.py @@ -267,12 +267,13 @@ def refresh_memcache(request): from django.http import HttpResponseForbidden return HttpResponseForbidden() except Exception as e: + logger.error(e) return HttpResponse(e) def page_not_found_view(request, exception, template_name='blog/error_page.html'): if exception: - logger.warn(exception) + logger.error(exception) url = request.get_full_path() return render(request, template_name, {'message': '哎呀,您访问的地址 ' + url + ' 是一个未知的地方。请点击首页看看别的?', 'statuscode': '404'}, status=404) @@ -285,6 +286,6 @@ def server_error_view(request, template_name='blog/error_page.html'): def permission_denied_view(request, exception, template_name='blog/error_page.html'): if exception: - logger.warn(exception) + logger.error(exception) return render(request, template_name, {'message': '哎呀,您没有权限访问此页面,请点击首页看看别的?', 'statuscode': '403'}, status=403) diff --git a/oauth/oauthmanager.py b/oauth/oauthmanager.py index 460cb8b9a..16f2c7471 100644 --- a/oauth/oauthmanager.py +++ b/oauth/oauthmanager.py @@ -106,7 +106,8 @@ def get_access_token_by_code(self, code): self.access_token = str(obj['access_token']) self.openid = str(obj['uid']) return self.get_oauth_userinfo() - except: + except Exception as e: + logger.error(e) return None def get_oauth_userinfo(self): @@ -130,8 +131,9 @@ def get_oauth_userinfo(self): if 'email' in datas and datas['email']: user.email = datas['email'] return user - except: - logger.info('weibo oauth error.rsp:' + rsp) + except Exception as e: + logger.error(e) + logger.error('weibo oauth error.rsp:' + rsp) return None @@ -176,8 +178,9 @@ def get_access_token_by_code(self, code): self.openid = str(obj['id_token']) logger.info(self.ICON_NAME + ' oauth ' + rsp) return self.access_token - except: - logger.info(self.ICON_NAME + ' oauth error ' + rsp) + except Exception as e: + logger.error(e) + logger.error(self.ICON_NAME + ' oauth error ' + rsp) return None def get_oauth_userinfo(self): @@ -200,8 +203,9 @@ def get_oauth_userinfo(self): if datas['email']: user.email = datas['email'] return user - except: - logger.info('google oauth error.rsp:' + rsp) + except Exception as e: + logger.error(e) + logger.error('google oauth error.rsp:' + rsp) return None @@ -245,7 +249,8 @@ def get_access_token_by_code(self, code): r = parse.parse_qs(rsp) self.access_token = (r['access_token'][0]) return self.access_token - except: + except Exception as e: + logger.error(e) return None def get_oauth_userinfo(self): @@ -268,8 +273,9 @@ def get_oauth_userinfo(self): user.email = datas['email'] return user - except: - logger.info('github oauth error.rsp:' + rsp) + except Exception as e: + logger.error(e) + logger.error('github oauth error.rsp:' + rsp) return None @@ -312,7 +318,8 @@ def get_access_token_by_code(self, code): token = str(obj['access_token']) self.access_token = token return self.access_token - except: + except Exception as e: + logger.error(e) return None def get_oauth_userinfo(self): @@ -335,7 +342,7 @@ def get_oauth_userinfo(self): user.picture = str(datas['picture']['data']['url']) return user except Exception as e: - logger.warn(e) + logger.error(e) return None diff --git a/owntracks/views.py b/owntracks/views.py index 910fdaa39..de1e4a79b 100644 --- a/owntracks/views.py +++ b/owntracks/views.py @@ -34,7 +34,7 @@ def manage_owntrack_log(request): else: return HttpResponse('data error') except Exception as e: - logger.warn(e) + logger.error(e) return HttpResponse('error') diff --git a/servermanager/Api/commonapi.py b/servermanager/Api/commonapi.py index 468bb556a..ad049d078 100644 --- a/servermanager/Api/commonapi.py +++ b/servermanager/Api/commonapi.py @@ -39,5 +39,5 @@ def getdata(self, content): if str(jsons["code"]) == '100000': return jsons["text"] except Exception as e: - logger.warn(e) + logger.error(e) return "哎呀,出错啦。" From 677f1d1ab924d92ff0c6875f7944530c4e7e84b5 Mon Sep 17 00:00:00 2001 From: liangliang Date: Sun, 1 Jul 2018 17:32:25 +0800 Subject: [PATCH 003/593] update logger --- DjangoBlog/settings.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/DjangoBlog/settings.py b/DjangoBlog/settings.py index 3117cadd6..167769ff8 100644 --- a/DjangoBlog/settings.py +++ b/DjangoBlog/settings.py @@ -226,11 +226,8 @@ }, 'formatters': { 'verbose': { - 'format': '[%(asctime)s] %(levelname)s [%(name)s.%(funcName)s:%(lineno)d] %(message)s', - }, - 'simple': { - 'format': '[%(asctime)s] %(levelname)s [%(name)s.%(funcName)s:%(lineno)d] %(message)s', - }, + 'format': '[%(asctime)s] %(levelname)s [%(name)s.%(funcName)s:%(lineno)d %(module)s] %(message)s', + } }, 'filters': { 'require_debug_false': { @@ -244,7 +241,7 @@ 'log_file': { 'level': 'INFO', 'class': 'logging.handlers.RotatingFileHandler', - 'filename': 'xiaobiaobai.log', + 'filename': 'djangoblog.log', 'maxBytes': 16777216, # 16 MB 'formatter': 'verbose' }, @@ -252,7 +249,7 @@ 'level': 'DEBUG', 'filters': ['require_debug_true'], 'class': 'logging.StreamHandler', - 'formatter': 'simple' + 'formatter': 'verbose' }, 'null': { 'class': 'logging.NullHandler', @@ -269,8 +266,8 @@ }, }, 'loggers': { - 'xiaobiaobai': { - 'handlers': ['log_file', 'console'], + 'djangoblog': { + 'handlers': ['log_file', 'console', 'sentry'], 'level': 'INFO', 'propagate': True, }, From 02eb47a40d8f893a24d49cb7c4b863afffeaf7b4 Mon Sep 17 00:00:00 2001 From: liangliang Date: Sat, 7 Jul 2018 00:25:31 +0800 Subject: [PATCH 004/593] update logger --- DjangoBlog/blog_signals.py | 6 +++++- DjangoBlog/utils.py | 2 +- bin/docker_start.sh | 4 ++-- blog/context_processors.py | 5 ++++- blog/models.py | 5 +++-- blog/templatetags/blog_tags.py | 5 ++++- blog/views.py | 5 ++++- comments/models.py | 1 - oauth/oauthmanager.py | 5 ++++- owntracks/views.py | 4 +++- requirements.txt | 20 ++++++++++++-------- servermanager/Api/commonapi.py | 4 +++- travis_test/requirements.txt | 20 ++++++++++++-------- 13 files changed, 57 insertions(+), 29 deletions(-) diff --git a/DjangoBlog/blog_signals.py b/DjangoBlog/blog_signals.py index c17182126..f92a237b0 100644 --- a/DjangoBlog/blog_signals.py +++ b/DjangoBlog/blog_signals.py @@ -16,10 +16,14 @@ import django.dispatch from django.dispatch import receiver from django.conf import settings -from DjangoBlog.utils import cache, send_email, expire_view_cache, logger +from DjangoBlog.utils import cache, send_email, expire_view_cache from DjangoBlog.spider_notify import SpiderNotify from django.contrib.sites.models import Site +import logging + +logger = logging.getLogger(__name__) + comment_save_signal = django.dispatch.Signal(providing_args=["comment_id", "username", "serverport"]) article_save_signal = django.dispatch.Signal(providing_args=['id', 'is_update_views']) user_login_logout_signal = django.dispatch.Signal(providing_args=['id', 'type']) diff --git a/DjangoBlog/utils.py b/DjangoBlog/utils.py index 96d792d3f..f1606dfc7 100644 --- a/DjangoBlog/utils.py +++ b/DjangoBlog/utils.py @@ -25,7 +25,7 @@ from django.core.mail import EmailMultiAlternatives from django.conf import settings -logger = logging.getLogger('djangoblog') +logger = logging.getLogger(__name__) def get_max_articleid_commentid(): diff --git a/bin/docker_start.sh b/bin/docker_start.sh index e44267358..771572fa9 100644 --- a/bin/docker_start.sh +++ b/bin/docker_start.sh @@ -14,8 +14,8 @@ echo "Starting $NAME as `whoami`" cd $DJANGODIR export DJANGO_SETTINGS_MODULE=$DJANGO_SETTINGS_MODULE export PYTHONPATH=$DJANGODIR:$PYTHONPATH -pip install -Ur requirements.txt -i http://pypi.douban.com/simple/ --trusted-host pypi.douban.com && \ - pip install gunicorn -i http://pypi.douban.com/simple/ --trusted-host pypi.douban.com +#pip install -Ur requirements.txt -i http://pypi.douban.com/simple/ --trusted-host pypi.douban.com && \ +# pip install gunicorn -i http://pypi.douban.com/simple/ --trusted-host pypi.douban.com python manage.py makemigrations python manage.py migrate python manage.py collectstatic --noinput diff --git a/blog/context_processors.py b/blog/context_processors.py index 27c9cc2c4..606ecd069 100644 --- a/blog/context_processors.py +++ b/blog/context_processors.py @@ -15,7 +15,10 @@ from .models import Category, Article, Tag, BlogSettings from django.conf import settings from comments.models import Comment -from DjangoBlog.utils import logger, cache, get_blog_setting +from DjangoBlog.utils import cache, get_blog_setting +import logging + +logger = logging.getLogger(__name__) def seo_processor(requests): diff --git a/blog/models.py b/blog/models.py index aa70479b5..a45351fa1 100644 --- a/blog/models.py +++ b/blog/models.py @@ -2,14 +2,15 @@ from django.urls import reverse from django.conf import settings from uuslug import slugify - +import logging from django.core.exceptions import ValidationError from django.utils.translation import gettext_lazy as _ from django.contrib.sites.models import Site -from DjangoBlog.utils import cache_decorator, logger, cache +from DjangoBlog.utils import cache_decorator, cache from django.utils.functional import cached_property from django.utils.timezone import now +logger = logging.getLogger(__name__) class BaseModel(models.Model): slug = models.SlugField(default='no-slug', max_length=160, blank=True) diff --git a/blog/templatetags/blog_tags.py b/blog/templatetags/blog_tags.py index 3b70846dc..dd552f0e1 100644 --- a/blog/templatetags/blog_tags.py +++ b/blog/templatetags/blog_tags.py @@ -25,10 +25,13 @@ import hashlib import urllib from comments.models import Comment -from DjangoBlog.utils import cache_decorator, logger +from DjangoBlog.utils import cache_decorator from django.contrib.auth import get_user_model from oauth.models import OAuthUser from django.contrib.sites.models import Site +import logging + +logger = logging.getLogger(__name__) register = template.Library() diff --git a/blog/views.py b/blog/views.py index 96464b899..123446ef0 100644 --- a/blog/views.py +++ b/blog/views.py @@ -11,10 +11,13 @@ from django.http import HttpResponseRedirect from django.views.decorators.csrf import csrf_exempt from django.contrib.auth.decorators import login_required -from DjangoBlog.utils import cache, logger +from DjangoBlog.utils import cache from django.shortcuts import get_object_or_404 from blog.models import Article, Category, Tag from comments.forms import CommentForm +import logging + +logger = logging.getLogger(__name__) class ArticleListView(ListView): diff --git a/comments/models.py b/comments/models.py index 177c3ed4d..8ad88d8ff 100644 --- a/comments/models.py +++ b/comments/models.py @@ -1,7 +1,6 @@ from django.db import models from django.conf import settings from blog.models import Article -from DjangoBlog.utils import logger from django.utils.timezone import now diff --git a/oauth/oauthmanager.py b/oauth/oauthmanager.py index 16f2c7471..e575dc9c9 100644 --- a/oauth/oauthmanager.py +++ b/oauth/oauthmanager.py @@ -18,8 +18,11 @@ from django.conf import settings import requests import json +import logging import urllib.parse -from DjangoBlog.utils import logger, parse_dict_to_url +from DjangoBlog.utils import parse_dict_to_url + +logger = logging.getLogger(__name__) class BaseOauthManager(metaclass=ABCMeta): diff --git a/owntracks/views.py b/owntracks/views.py index de1e4a79b..74f82852a 100644 --- a/owntracks/views.py +++ b/owntracks/views.py @@ -7,13 +7,15 @@ from itertools import groupby from django.http import HttpResponse from .models import OwnTrackLog -from DjangoBlog.utils import logger +import logging from django.shortcuts import render from django.http import JsonResponse from django.contrib.auth.decorators import login_required from django.views.decorators.csrf import csrf_exempt import requests +logger = logging.getLogger(__name__) + @csrf_exempt def manage_owntrack_log(request): diff --git a/requirements.txt b/requirements.txt index 6f7c56ea0..93ab42f61 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,16 +1,19 @@ appdirs==1.4.3 +asn1crypto==0.24.0 bottle==0.12.13 certifi==2018.4.16 +cffi==1.11.5 chardet==3.0.4 coverage==4.5.1 -Django==2.0.5 +cryptography==2.2.2 +Django==2.0.7 django-appconf==1.0.2 django-autoslug==1.9.3 django-compressor==2.2 django-debug-toolbar==1.9.1 django-haystack==2.8.1 django-ipware==2.1.0 -django-pagedown==1.0.4 +django-pagedown==1.0.5 django-uuslug==1.1.8 idna==2.6 jieba==0.39 @@ -19,22 +22,23 @@ markdown2==2.3.5 mistune==0.8.3 olefile==0.45.1 packaging==17.1 -Pillow==5.1.0 +Pillow==5.2.0 +pycparser==2.18 Pygments==2.2.0 -PyMySQL==0.8.1 +PyMySQL==0.9.2 pyparsing==2.2.0 python-memcached==1.59 python-slugify==1.2.5 -pytz==2018.4 +pytz==2018.5 raven==6.9.0 rcssmin==1.0.6 -requests==2.18.4 +requests==2.19.1 rjsmin==1.0.12 six==1.11.0 sqlparse==0.2.4 Unidecode==1.0.22 -urllib3==1.22 +urllib3==1.23 webencodings==0.5.1 -WeRoBot==1.4.1 +WeRoBot==1.6.0 Whoosh==2.7.4 xmltodict==0.11.0 diff --git a/servermanager/Api/commonapi.py b/servermanager/Api/commonapi.py index ad049d078..56e2fd6d5 100644 --- a/servermanager/Api/commonapi.py +++ b/servermanager/Api/commonapi.py @@ -14,7 +14,9 @@ """ import requests import json -from DjangoBlog.utils import logger +import logging + +logger = logging.getLogger(__name__) class TuLing(): diff --git a/travis_test/requirements.txt b/travis_test/requirements.txt index 4fd50ee4c..7a07522c4 100644 --- a/travis_test/requirements.txt +++ b/travis_test/requirements.txt @@ -1,16 +1,19 @@ appdirs==1.4.3 +asn1crypto==0.24.0 bottle==0.12.13 certifi==2018.4.16 +cffi==1.11.5 chardet==3.0.4 coverage==4.5.1 -Django==2.0.5 +cryptography==2.2.2 +Django==2.0.7 django-appconf==1.0.2 django-autoslug==1.9.3 django-compressor==2.2 django-debug-toolbar==1.9.1 django-haystack==2.8.1 django-ipware==2.1.0 -django-pagedown==1.0.4 +django-pagedown==1.0.5 django-uuslug==1.1.8 idna==2.6 jieba==0.39 @@ -19,21 +22,22 @@ markdown2==2.3.5 mistune==0.8.3 olefile==0.45.1 packaging==17.1 -Pillow==5.1.0 +Pillow==5.2.0 +pycparser==2.18 Pygments==2.2.0 -PyMySQL==0.8.1 +PyMySQL==0.9.2 pyparsing==2.2.0 python-slugify==1.2.5 -pytz==2018.4 +pytz==2018.5 raven==6.9.0 rcssmin==1.0.6 -requests==2.18.4 +requests==2.19.1 rjsmin==1.0.12 six==1.11.0 sqlparse==0.2.4 Unidecode==1.0.22 -urllib3==1.22 +urllib3==1.23 webencodings==0.5.1 -WeRoBot==1.4.1 +WeRoBot==1.6.0 Whoosh==2.7.4 xmltodict==0.11.0 From 2d3dbec3b45b1bbe4c4d3fe72fcf89453d9c3b4f Mon Sep 17 00:00:00 2001 From: liangliang Date: Wed, 18 Jul 2018 22:36:54 +0800 Subject: [PATCH 005/593] =?UTF-8?q?=E5=B0=86=E7=94=A8=E6=88=B7=E5=A4=B4?= =?UTF-8?q?=E5=83=8F=E4=BF=9D=E5=AD=98=E6=9C=AC=E5=9C=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DjangoBlog/utils.py | 27 +++++++++++++++ blog/management/commands/sync_user_avatar.py | 36 ++++++++++++++++++++ blog/models.py | 2 ++ oauth/views.py | 4 ++- 4 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 blog/management/commands/sync_user_avatar.py diff --git a/DjangoBlog/utils.py b/DjangoBlog/utils.py index f1606dfc7..fca7e4823 100644 --- a/DjangoBlog/utils.py +++ b/DjangoBlog/utils.py @@ -24,6 +24,10 @@ import _thread from django.core.mail import EmailMultiAlternatives from django.conf import settings +import requests +import datetime +import uuid +import os logger = logging.getLogger(__name__) @@ -200,3 +204,26 @@ def get_blog_setting(): logger.info('set cache get_blog_setting') cache.set('get_blog_setting', value) return value + + +def save_user_avatar(url): + try: + rsp = requests.get(url) + if rsp.status_code == 200: + setting = get_blog_setting() + + basepath = r'{basedir}/avatar/'.format(basedir=setting.resource_path) + if not os.path.exists(basepath): + os.makedirs(basepath) + + imgextensions = ['.jpg', '.png', 'jpeg', '.gif'] + isimage = len([i for i in imgextensions if url.endswith(i)]) > 0 + ext = os.path.splitext(url)[1] if isimage else '.jpg' + savefilename = str(uuid.uuid4().hex) + ext + logger.info('保存用户头像:' + basepath + savefilename) + with open(basepath + savefilename, 'wb+') as file: + file.write(rsp.content) + return 'https://resource.lylinux.net/avatar/' + savefilename + except Exception as e: + logger.error(e) + return url diff --git a/blog/management/commands/sync_user_avatar.py b/blog/management/commands/sync_user_avatar.py new file mode 100644 index 000000000..24bf108ef --- /dev/null +++ b/blog/management/commands/sync_user_avatar.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python +# encoding: utf-8 + + +""" +@version: ?? +@author: liangliangyy +@license: MIT Licence +@contact: liangliangyy@gmail.com +@site: https://www.lylinux.net/ +@software: PyCharm +@file: sync_user_avatar.py +@time: 2018/7/18 下午10:21 +""" + +from django.core.management.base import BaseCommand +from oauth.models import OAuthUser +from DjangoBlog.utils import save_user_avatar + + +class Command(BaseCommand): + help = 'sync user avatar' + + def handle(self, *args, **options): + users = OAuthUser.objects.filter(picture__isnull=False).exclude( + picture__istartswith='https://resource.lylinux.net').all() + self.stdout.write('开始同步{count}个用户头像'.format(count=len(users))) + for u in users: + self.stdout.write('开始同步:{id}'.format(id=u.nikename)) + url = u.picture + url = save_user_avatar(url) + if url: + self.stdout.write('结束同步:{id}.url:{url}'.format(id=u.nikename, url=url)) + u.picture = url + u.save() + self.stdout.write('结束同步') diff --git a/blog/models.py b/blog/models.py index a45351fa1..3f95ed4db 100644 --- a/blog/models.py +++ b/blog/models.py @@ -12,6 +12,7 @@ logger = logging.getLogger(__name__) + class BaseModel(models.Model): slug = models.SlugField(default='no-slug', max_length=160, blank=True) created_time = models.DateTimeField('创建时间', default=now) @@ -250,6 +251,7 @@ class BlogSettings(models.Model): analyticscode = models.TextField("网站统计代码", max_length=1000, null=False, blank=False, default='') show_gongan_code = models.BooleanField('是否显示公安备案号', default=False, null=False) gongan_beiancode = models.TextField('公安备案号', max_length=2000, null=True, blank=True, default='') + resource_path = models.CharField("静态文件保存地址", max_length=300, null=False, default='/var/www/resource/') class Meta: verbose_name = '网站配置' diff --git a/oauth/views.py b/oauth/views.py index a199718d6..f07bb34ca 100644 --- a/oauth/views.py +++ b/oauth/views.py @@ -11,7 +11,7 @@ from django.views.generic import FormView, RedirectView from oauth.forms import RequireEmailForm from django.urls import reverse -from DjangoBlog.utils import send_email, get_md5 +from DjangoBlog.utils import send_email, get_md5, save_user_avatar from django.contrib.sites.models import Site from django.core.exceptions import ObjectDoesNotExist from django.http import HttpResponseForbidden @@ -50,6 +50,8 @@ def authorize(request): user = manager.get_oauth_userinfo() if user: + if user.picture: + user.picture = save_user_avatar(user.picture) if not user.nikename: import datetime user.nikename = "djangoblog" + datetime.datetime.now().strftime('%y%m%d%I%M%S') From 32ee83c0591123296e63bb1d087481965a3fa056 Mon Sep 17 00:00:00 2001 From: liangliang Date: Wed, 18 Jul 2018 23:02:40 +0800 Subject: [PATCH 006/593] =?UTF-8?q?=E5=B0=86=E7=94=A8=E6=88=B7=E5=A4=B4?= =?UTF-8?q?=E5=83=8F=E4=BF=9D=E5=AD=98=E6=9C=AC=E5=9C=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- oauth/views.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/oauth/views.py b/oauth/views.py index f07bb34ca..a71227d25 100644 --- a/oauth/views.py +++ b/oauth/views.py @@ -16,6 +16,9 @@ from django.core.exceptions import ObjectDoesNotExist from django.http import HttpResponseForbidden from .oauthmanager import get_manager_by_type +import logging + +logger = logging.getLogger(__name__) def oauthlogin(request): @@ -48,10 +51,8 @@ def authorize(request): if not rsp: return HttpResponseRedirect(manager.get_authorization_url(nexturl)) user = manager.get_oauth_userinfo() - + logger.info('user:' + user.nikename) if user: - if user.picture: - user.picture = save_user_avatar(user.picture) if not user.nikename: import datetime user.nikename = "djangoblog" + datetime.datetime.now().strftime('%y%m%d%I%M%S') @@ -60,6 +61,8 @@ def authorize(request): except ObjectDoesNotExist: pass # facebook的token过长 + if user.picture: + user.picture = save_user_avatar(user.picture) if type == 'facebook': user.token = '' email = user.email From bcf6a08bf9dbfd7488beedb46c6261d2c7a5fc44 Mon Sep 17 00:00:00 2001 From: liangliang Date: Wed, 18 Jul 2018 23:13:36 +0800 Subject: [PATCH 007/593] =?UTF-8?q?=E4=B8=BA=E7=94=A8=E6=88=B7=E5=A4=B4?= =?UTF-8?q?=E5=83=8F=E5=A2=9E=E5=8A=A0=E7=BC=93=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- blog/templatetags/blog_tags.py | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/blog/templatetags/blog_tags.py b/blog/templatetags/blog_tags.py index dd552f0e1..4580a8f21 100644 --- a/blog/templatetags/blog_tags.py +++ b/blog/templatetags/blog_tags.py @@ -25,7 +25,7 @@ import hashlib import urllib from comments.models import Comment -from DjangoBlog.utils import cache_decorator +from DjangoBlog.utils import cache_decorator, cache from django.contrib.auth import get_user_model from oauth.models import OAuthUser from django.contrib.sites.models import Site @@ -262,17 +262,23 @@ def load_article_detail(article, isindex, user): @register.filter def gravatar_url(email, size=40): """获得gravatar头像""" - usermodels = OAuthUser.objects.filter(email=email) - if usermodels: - o = list(filter(lambda x: x.picture is not None, usermodels)) - if o: - return o[0].picture - email = email.encode('utf-8') - - default = "https://resource.lylinux.net/image/2017/03/26/120117.jpg".encode('utf-8') - - return "https://www.gravatar.com/avatar/%s?%s" % ( - hashlib.md5(email.lower()).hexdigest(), urllib.parse.urlencode({'d': default, 's': str(size)})) + cachekey = 'gravatat/' + email + if cache.get(cachekey): + return cache.get(cachekey) + else: + usermodels = OAuthUser.objects.filter(email=email) + if usermodels: + o = list(filter(lambda x: x.picture is not None, usermodels)) + if o: + return o[0].picture + email = email.encode('utf-8') + + default = "https://resource.lylinux.net/image/2017/03/26/120117.jpg".encode('utf-8') + + url = "https://www.gravatar.com/avatar/%s?%s" % ( + hashlib.md5(email.lower()).hexdigest(), urllib.parse.urlencode({'d': default, 's': str(size)})) + cache.set(cachekey, url, 60 * 60 * 10) + return url # return an image tag with the gravatar From a441000cfd2dbf63ecd6ddca492bbda1f27081a9 Mon Sep 17 00:00:00 2001 From: liangliang Date: Wed, 18 Jul 2018 23:19:58 +0800 Subject: [PATCH 008/593] =?UTF-8?q?=E5=B0=86=E7=94=A8=E6=88=B7=E5=A4=B4?= =?UTF-8?q?=E5=83=8F=E4=BF=9D=E5=AD=98=E6=9C=AC=E5=9C=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- oauth/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/oauth/views.py b/oauth/views.py index a71227d25..c01aa9424 100644 --- a/oauth/views.py +++ b/oauth/views.py @@ -53,6 +53,8 @@ def authorize(request): user = manager.get_oauth_userinfo() logger.info('user:' + user.nikename) if user: + if user.picture: + user.picture = save_user_avatar(user.picture) if not user.nikename: import datetime user.nikename = "djangoblog" + datetime.datetime.now().strftime('%y%m%d%I%M%S') @@ -61,8 +63,6 @@ def authorize(request): except ObjectDoesNotExist: pass # facebook的token过长 - if user.picture: - user.picture = save_user_avatar(user.picture) if type == 'facebook': user.token = '' email = user.email From 0e9cefdd9cc3782b05bec68815cb9b94301323c5 Mon Sep 17 00:00:00 2001 From: liangliang Date: Fri, 20 Jul 2018 23:41:00 +0800 Subject: [PATCH 009/593] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E4=BB=8B=E7=BB=8D=EF=BC=8C=E4=BF=AE=E5=A4=8Doauth=E9=85=8D?= =?UTF-8?q?=E7=BD=AEbug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DjangoBlog/settings.py | 4 ++-- DjangoBlog/utils.py | 2 +- README.md | 6 +++--- bin/config.md | 35 ++++++++--------------------------- oauth/oauthmanager.py | 2 +- 5 files changed, 15 insertions(+), 34 deletions(-) diff --git a/DjangoBlog/settings.py b/DjangoBlog/settings.py index 167769ff8..af58247fb 100644 --- a/DjangoBlog/settings.py +++ b/DjangoBlog/settings.py @@ -23,8 +23,8 @@ SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY') # SECURITY WARNING: don't run with debug turned on in production! -# DEBUG = True -DEBUG = False +DEBUG = True +# DEBUG = False TESTING = len(sys.argv) > 1 and sys.argv[1] == 'test' # ALLOWED_HOSTS = [] diff --git a/DjangoBlog/utils.py b/DjangoBlog/utils.py index fca7e4823..19d24074b 100644 --- a/DjangoBlog/utils.py +++ b/DjangoBlog/utils.py @@ -208,7 +208,7 @@ def get_blog_setting(): def save_user_avatar(url): try: - rsp = requests.get(url) + rsp = requests.get(url, timeout=2) if rsp.status_code == 200: setting = get_blog_setting() diff --git a/README.md b/README.md index 373ea18f8..afdb6bc08 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ windows电脑: 下载 http://peak.telecommunity.com/dist/ez_setup.py 和 https://raw.github.com/pypa/pip/master/contrib/get-pip.py 这两个文件,双击运行。 ### 配置 -配置都是在`setting.py`中. +配置都是在`setting.py`中.部分配置迁移到了后台配置中。 很多`setting`配置我都是写在环境变量里面的.并没有提交到`github`中来.例如`SECRET_KEY`,`OAHUTH`,`mysql`以及邮件部分的配置等.你可以直接修改代码成你自己的,或者在环境变量里面加入对应的配置就可以了. @@ -42,9 +42,9 @@ windows电脑: 有详细的部署介绍. -为了安全起见,没有把`SECRET_KEY`上传到Github中而是在环境变量中配置的,如果你想要正常运行的话,需要修改`settings.py`中的`SECRET_KEY`为你自己的就可以了。 +为了安全起见,没有把`SECRET_KEY`上传到Github中而是在环境变量中配置的,如果你想要正常运行的话,需要修改`settings.py`中的`SECRET_KEY`为你自己的就可以了。 如:`SECRET_KEY = 'n9ceqv38)#&mwuat@(mjb_p%em$e8$qyr#fw9ot!=ba6lijx-6'` -若本地部署后发现静态文件无法加载.请将`settings.py`中的`DEBUG=False`修改为`DEBUG=True`即可. + ## 运行 diff --git a/bin/config.md b/bin/config.md index 9496e6f67..ecaf259ef 100644 --- a/bin/config.md +++ b/bin/config.md @@ -19,33 +19,10 @@ CACHES = { ``` ## oauth登录: -现在已经支持微博,Google,GitHub,Facebook登录,需要在其对应的开放平台申请oauth登录权限,然后修改`settings.py`中的如下配置: -```python -OAHUTH = { - 'sina': { - 'appkey': os.environ.get('SINA_APP_KEY'), - 'appsecret': os.environ.get('SINA_APP_SECRET'), - 'callbackurl': 'http://www.lylinux.net/oauth/authorize?type=weibo' - }, - 'google': { - 'appkey': os.environ.get('GOOGLE_APP_KEY'), - 'appsecret': os.environ.get('GOOGLE_APP_SECRET'), - 'callbackurl': 'http://www.lylinux.net/oauth/authorize?type=google' - }, - 'github': { - 'appkey': os.environ.get('GITHUB_APP_KEY'), - 'appsecret': os.environ.get('GITHUB_APP_SECRET'), - 'callbackurl': 'http://www.lylinux.net/oauth/authorize?type=github' - }, - 'facebook': { - 'appkey': os.environ.get('FACEBOOK_APP_KEY'), - 'appsecret': os.environ.get('FACEBOOK_APP_SECRET'), - 'callbackurl': 'http://www.lylinux.net/oauth/authorize?type=facebook' - } -} -``` -将对应的appkey和appsecret修改为你自己的,将`callbackurl`的域名也修改为你的域名。 -## Update Oauth配置部分已经修改到配置表中 +现在已经支持微博,Google,GitHub,Facebook登录,需要在其对应的开放平台申请oauth登录权限,然后在 +**后台->Oauth** 配置中新增配置,填写对应的`appkey`和`appsecret`以及回调地址。 + + ## owntracks: owntracks是一个位置追踪软件,可以定时的将你的坐标提交到你的服务器上,现在简单的支持owntracks功能,需要安装owntracks的app,然后将api地址设置为: @@ -65,3 +42,7 @@ SERVER_EMAIL = os.environ.get('DJANGO_EMAIL_USER') ## 微信公众号 集成了简单的微信公众号功能,在微信后台将token地址设置为:`你的域名/robot` 即可,默认token为`lylinux`,当然你可以修改为你自己的,在`servermanager/robot.py`中。 +然后在**后台->Servermanager->命令**中新增命令,这样就可以使用微信公众号来管理了。 +## 网站配置介绍 +在**后台->BLOG->网站配置**中,可以新增网站配置,比如关键字,描述等,以及谷歌广告,网站统计代码及备案号等等。 +其中的*静态文件保存地址*是保存oauth用户登录的头像路径,填写绝对路径,默认是代码目录。 diff --git a/oauth/oauthmanager.py b/oauth/oauthmanager.py index e575dc9c9..22b2784da 100644 --- a/oauth/oauthmanager.py +++ b/oauth/oauthmanager.py @@ -355,7 +355,7 @@ def get_oauth_apps(): return [] configtypes = [x.type for x in configs] applications = BaseOauthManager.__subclasses__() - apps = [x() for x in applications if configtypes.index(x().ICON_NAME.lower()) >= 0] + apps = [x() for x in applications if x().ICON_NAME.lower() in configtypes] return apps From e72e044d3b003c008f0c0537e444773f7a4ae0f6 Mon Sep 17 00:00:00 2001 From: liangliang Date: Sat, 21 Jul 2018 11:28:06 +0800 Subject: [PATCH 010/593] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=96=87=E7=AB=A0?= =?UTF-8?q?=E6=88=AA=E5=8F=96=E6=98=BE=E7=A4=BAbug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- templates/blog/tags/article_info.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/blog/tags/article_info.html b/templates/blog/tags/article_info.html index 3859b82bd..50ff92c6d 100644 --- a/templates/blog/tags/article_info.html +++ b/templates/blog/tags/article_info.html @@ -38,7 +38,7 @@

{% if isindex %} - {{ article.body|escape|custom_markdown|truncatechars_content }} + {{ article.body|custom_markdown|escape|truncatechars_content }}

Read more

{% else %} From 962f93116f4dfc926e463104affe1eb15349a8f8 Mon Sep 17 00:00:00 2001 From: liangliangyy Date: Wed, 25 Jul 2018 10:41:54 +0800 Subject: [PATCH 011/593] =?UTF-8?q?=E5=8E=BB=E6=8E=89=20RAVEN=5FCONFIG=20?= =?UTF-8?q?=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DjangoBlog/settings.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/DjangoBlog/settings.py b/DjangoBlog/settings.py index af58247fb..c04a09c37 100644 --- a/DjangoBlog/settings.py +++ b/DjangoBlog/settings.py @@ -11,7 +11,6 @@ """ import sys import os -import raven # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) @@ -35,12 +34,6 @@ SITE_ROOT = os.path.dirname(os.path.abspath(__file__)) SITE_ROOT = os.path.abspath(os.path.join(SITE_ROOT, '../')) -RAVEN_CONFIG = { - 'dsn': os.environ.get('SENTRY_DSN'), - # If you are using git, you can also automatically configure the - # release based on the git info. - 'release': raven.fetch_git_sha(os.path.abspath(SITE_ROOT)), -} INSTALLED_APPS = [ # 'django.contrib.admin', From b6d632c441d5c4b4c263511e06d391a53510bacf Mon Sep 17 00:00:00 2001 From: liangliang Date: Fri, 27 Jul 2018 22:04:51 +0800 Subject: [PATCH 012/593] =?UTF-8?q?=E5=8E=BB=E6=8E=89=E6=96=87=E7=AB=A0slu?= =?UTF-8?q?g=E9=85=8D=E7=BD=AE=EF=BC=8C=20close=20#144?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DjangoBlog/settings.py | 33 ++++++++------------------------- README.md | 3 --- blog/models.py | 14 ++++++-------- 3 files changed, 14 insertions(+), 36 deletions(-) diff --git a/DjangoBlog/settings.py b/DjangoBlog/settings.py index c04a09c37..819363442 100644 --- a/DjangoBlog/settings.py +++ b/DjangoBlog/settings.py @@ -19,7 +19,7 @@ # See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY') +SECRET_KEY = 'n9ceqv38)#&mwuat@(mjb_p%em$e8$qyr#fw9ot!=ba6lijx-6' # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True @@ -27,14 +27,13 @@ TESTING = len(sys.argv) > 1 and sys.argv[1] == 'test' # ALLOWED_HOSTS = [] -ALLOWED_HOSTS = ['*', 'www.lylinux.net', '127.0.0.1', 'example.com'] +ALLOWED_HOSTS = ['*', '127.0.0.1', 'example.com'] # Application definition SITE_ROOT = os.path.dirname(os.path.abspath(__file__)) SITE_ROOT = os.path.abspath(os.path.join(SITE_ROOT, '../')) - INSTALLED_APPS = [ # 'django.contrib.admin', 'django.contrib.admin.apps.SimpleAdminConfig', @@ -45,7 +44,6 @@ 'django.contrib.staticfiles', 'django.contrib.sites', 'django.contrib.sitemaps', - 'raven.contrib.django.raven_compat', 'pagedown', 'haystack', 'blog', @@ -214,8 +212,8 @@ 'version': 1, 'disable_existing_loggers': False, 'root': { - 'level': 'WARNING', - 'handlers': ['sentry', 'console', 'log_file'], + 'level': 'INFO', + 'handlers': ['console', 'log_file'], }, 'formatters': { 'verbose': { @@ -251,34 +249,19 @@ 'level': 'ERROR', 'filters': ['require_debug_false'], 'class': 'django.utils.log.AdminEmailHandler' - }, - 'sentry': { - 'level': 'ERROR', # To capture more than ERROR, change to WARNING, INFO, etc. - 'class': 'raven.contrib.django.raven_compat.handlers.SentryHandler', - 'tags': {'custom-tag': 'x'}, - }, + } }, 'loggers': { 'djangoblog': { - 'handlers': ['log_file', 'console', 'sentry'], + 'handlers': ['log_file', 'console'], 'level': 'INFO', 'propagate': True, }, 'django.request': { - 'handlers': ['mail_admins', 'sentry'], + 'handlers': ['mail_admins'], 'level': 'ERROR', 'propagate': False, - }, - 'raven': { - 'level': 'DEBUG', - 'handlers': ['console'], - 'propagate': False, - }, - 'sentry.errors': { - 'level': 'DEBUG', - 'handlers': ['console'], - 'propagate': False, - }, + } } } diff --git a/README.md b/README.md index afdb6bc08..48fd97a2b 100644 --- a/README.md +++ b/README.md @@ -42,9 +42,6 @@ windows电脑: 有详细的部署介绍. -为了安全起见,没有把`SECRET_KEY`上传到Github中而是在环境变量中配置的,如果你想要正常运行的话,需要修改`settings.py`中的`SECRET_KEY`为你自己的就可以了。 -如:`SECRET_KEY = 'n9ceqv38)#&mwuat@(mjb_p%em$e8$qyr#fw9ot!=ba6lijx-6'` - ## 运行 diff --git a/blog/models.py b/blog/models.py index 3f95ed4db..9bab31e37 100644 --- a/blog/models.py +++ b/blog/models.py @@ -14,15 +14,15 @@ class BaseModel(models.Model): - slug = models.SlugField(default='no-slug', max_length=160, blank=True) created_time = models.DateTimeField('创建时间', default=now) last_mod_time = models.DateTimeField('修改时间', default=now) def save(self, *args, **kwargs): from DjangoBlog.blog_signals import article_save_signal - if not self.slug or self.slug == 'no-slug' or not self.id: - slug = self.title if 'title' in self.__dict__ else self.name - self.slug = slugify(slug) + if not isinstance(self, Article): + if not self.slug or self.slug == 'no-slug' or not self.id: + slug = self.title if 'title' in self.__dict__ else self.name + self.slug = slugify(slug) super().save(*args, **kwargs) # type = self.__class__.__name__ is_update_views = 'update_fields' in kwargs and len(kwargs['update_fields']) == 1 and kwargs['update_fields'][ @@ -89,10 +89,6 @@ def get_category_tree(self): return names def save(self, *args, **kwargs): - if not self.slug or self.slug == 'no-slug' or not self.id: - # Only set the slug when the object is created. - self.slug = slugify(self.title) - super().save(*args, **kwargs) def viewed(self): @@ -130,6 +126,7 @@ class Category(BaseModel): """文章分类""" name = models.CharField('分类名', max_length=30, unique=True) parent_category = models.ForeignKey('self', verbose_name="父级分类", blank=True, null=True, on_delete=models.CASCADE) + slug = models.SlugField(default='no-slug', max_length=60, blank=True) class Meta: ordering = ['name'] @@ -183,6 +180,7 @@ def parse(category): class Tag(BaseModel): """文章标签""" name = models.CharField('标签名', max_length=30, unique=True) + slug = models.SlugField(default='no-slug', max_length=60, blank=True) def __str__(self): return self.name From a70bae1840eac60fe0250f95bae9c0bd156a3e5e Mon Sep 17 00:00:00 2001 From: liangliang Date: Sun, 29 Jul 2018 17:57:21 +0800 Subject: [PATCH 013/593] =?UTF-8?q?=E7=A7=BB=E9=99=A4slug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- blog/admin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/blog/admin.py b/blog/admin.py index ff6d59c7a..fdc1ffb44 100644 --- a/blog/admin.py +++ b/blog/admin.py @@ -57,11 +57,11 @@ def open_article_commentstatus(modeladmin, request, queryset): class ArticlelAdmin(admin.ModelAdmin): search_fields = ('body',) form = ArticleForm - list_display = ('id', 'title', 'author', 'created_time', 'views', 'status', 'type','article_order') + list_display = ('id', 'title', 'author', 'created_time', 'views', 'status', 'type', 'article_order') list_display_links = ('id', 'title') list_filter = (ArticleListFilter, 'status', 'type', 'category', 'tags') filter_horizontal = ('tags',) - exclude = ('slug', 'created_time', 'last_mod_time') + exclude = ('created_time', 'last_mod_time') view_on_site = True actions = [makr_article_publish, draft_article, close_article_commentstatus, open_article_commentstatus] From 4881c3c1068355cf410a7cd24e8c01611adeec0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=BD=A6=E4=BA=AE=E4=BA=AE?= Date: Tue, 31 Jul 2018 16:14:10 +0800 Subject: [PATCH 014/593] Update README.md --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 48fd97a2b..8dac2d57c 100644 --- a/README.md +++ b/README.md @@ -59,8 +59,11 @@ windows电脑: } ### 创建数据库 - - 终端下执行: +mysql数据库中执行: +```sql +CREATE DATABASE `djangoblog` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci */; +``` + 然后终端下执行: ./manage.py makemigrations ./manage.py migrate From 08e9ed9fb075df1d448ce26368b1e77f3ae3a962 Mon Sep 17 00:00:00 2001 From: liangliang Date: Wed, 8 Aug 2018 22:09:18 +0800 Subject: [PATCH 015/593] =?UTF-8?q?=E4=BF=AE=E5=A4=8Dnikename=E4=B8=BA?= =?UTF-8?q?=E7=A9=BAbug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- oauth/views.py | 1 - 1 file changed, 1 deletion(-) diff --git a/oauth/views.py b/oauth/views.py index c01aa9424..819a0d583 100644 --- a/oauth/views.py +++ b/oauth/views.py @@ -51,7 +51,6 @@ def authorize(request): if not rsp: return HttpResponseRedirect(manager.get_authorization_url(nexturl)) user = manager.get_oauth_userinfo() - logger.info('user:' + user.nikename) if user: if user.picture: user.picture = save_user_avatar(user.picture) From c385a270697fa0e6a7ec96c1efb9c9cc0eb834cb Mon Sep 17 00:00:00 2001 From: liangliang Date: Sat, 25 Aug 2018 11:43:29 +0800 Subject: [PATCH 016/593] =?UTF-8?q?=E5=8D=87=E7=BA=A7django2.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DjangoBlog/settings.py | 57 ++++++++++++++++++++++++---------- accounts/tests.py | 20 ++++++------ accounts/user_login_backend.py | 6 ++-- accounts/views.py | 4 +-- blog/admin.py | 8 ++--- requirements.txt | 4 +-- travis_test/requirements.txt | 4 +-- 7 files changed, 65 insertions(+), 38 deletions(-) diff --git a/DjangoBlog/settings.py b/DjangoBlog/settings.py index 819363442..ff65cf9d7 100644 --- a/DjangoBlog/settings.py +++ b/DjangoBlog/settings.py @@ -11,6 +11,7 @@ """ import sys import os +import raven # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) @@ -19,11 +20,11 @@ # See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = 'n9ceqv38)#&mwuat@(mjb_p%em$e8$qyr#fw9ot!=ba6lijx-6' +SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY') # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True -# DEBUG = False +#DEBUG = False TESTING = len(sys.argv) > 1 and sys.argv[1] == 'test' # ALLOWED_HOSTS = [] @@ -34,6 +35,13 @@ SITE_ROOT = os.path.dirname(os.path.abspath(__file__)) SITE_ROOT = os.path.abspath(os.path.join(SITE_ROOT, '../')) +RAVEN_CONFIG = { + 'dsn': os.environ.get('SENTRY_DSN'), + # If you are using git, you can also automatically configure the + # release based on the git info. + 'release': raven.fetch_git_sha(os.path.abspath(SITE_ROOT)), +} + INSTALLED_APPS = [ # 'django.contrib.admin', 'django.contrib.admin.apps.SimpleAdminConfig', @@ -44,6 +52,7 @@ 'django.contrib.staticfiles', 'django.contrib.sites', 'django.contrib.sitemaps', + 'raven.contrib.django.raven_compat', 'pagedown', 'haystack', 'blog', @@ -99,9 +108,9 @@ 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'djangoblog', - 'USER': os.environ.get('DJANGO_MYSQL_USER'), - 'PASSWORD': os.environ.get('DJANGO_MYSQL_PASSWORD'), - 'HOST': os.environ.get('DJANGO_MYSQL_HOST'), + 'USER': 'root', + 'PASSWORD': 'root', + 'HOST': '127.0.0.1', 'PORT': 3306, 'OPTIONS': {'charset': 'utf8mb4'}, } @@ -170,14 +179,14 @@ ] # 分页 -PAGINATE_BY = 10 +PAGINATE_BY = 6 # http缓存时间 CACHE_CONTROL_MAX_AGE = 2592000 # cache setting CACHES = { 'default': { 'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache', - 'LOCATION': '127.0.0.1:11211', + 'LOCATION': 'memcached:11211', 'KEY_PREFIX': 'django_test' if TESTING else 'djangoblog', 'TIMEOUT': 60 * 60 * 10 }, @@ -194,11 +203,10 @@ # Emial: EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' -# EMAIL_USE_TLS = True -EMAIL_USE_SSL = True - -EMAIL_HOST = 'smtp.mxhichina.com' -EMAIL_PORT = 465 +EMAIL_USE_TLS = True +# EMAIL_USE_SSL = True +EMAIL_HOST = 'smtp.exmail.qq.com' +EMAIL_PORT = 587 EMAIL_HOST_USER = os.environ.get('DJANGO_EMAIL_USER') EMAIL_HOST_PASSWORD = os.environ.get('DJANGO_EMAIL_PASSWORD') DEFAULT_FROM_EMAIL = EMAIL_HOST_USER @@ -213,7 +221,7 @@ 'disable_existing_loggers': False, 'root': { 'level': 'INFO', - 'handlers': ['console', 'log_file'], + 'handlers': ['sentry', 'console', 'log_file'], }, 'formatters': { 'verbose': { @@ -249,19 +257,34 @@ 'level': 'ERROR', 'filters': ['require_debug_false'], 'class': 'django.utils.log.AdminEmailHandler' - } + }, + 'sentry': { + 'level': 'ERROR', # To capture more than ERROR, change to WARNING, INFO, etc. + 'class': 'raven.contrib.django.raven_compat.handlers.SentryHandler', + 'tags': {'custom-tag': 'x'}, + }, }, 'loggers': { 'djangoblog': { - 'handlers': ['log_file', 'console'], + 'handlers': ['log_file', 'console', 'sentry'], 'level': 'INFO', 'propagate': True, }, 'django.request': { - 'handlers': ['mail_admins'], + 'handlers': ['mail_admins', 'sentry'], 'level': 'ERROR', 'propagate': False, - } + }, + 'raven': { + 'level': 'DEBUG', + 'handlers': ['console'], + 'propagate': False, + }, + 'sentry.errors': { + 'level': 'DEBUG', + 'handlers': ['console'], + 'propagate': False, + }, } } diff --git a/accounts/tests.py b/accounts/tests.py index db743e669..1330bdf9b 100644 --- a/accounts/tests.py +++ b/accounts/tests.py @@ -17,9 +17,11 @@ def setUp(self): def test_validate_account(self): site = Site.objects.get_current().domain user = BlogUser.objects.create_superuser(email="liangliangyy1@gmail.com", - username="liangliangyy1", password="liangliangyy1") + username="liangliangyy1", password="qwer!@#$ggg") + testuser = BlogUser.objects.get(username='liangliangyy1') - self.client.login(username='liangliangyy1', password='liangliangyy1') + loginresult = self.client.login(username='liangliangyy1', password='qwer!@#$ggg') + self.assertEqual(loginresult, True) response = self.client.get('/admin/') self.assertEqual(response.status_code, 200) @@ -46,12 +48,12 @@ def test_validate_register(self): response = self.client.post(reverse('account:register'), { 'username': 'user1233', 'email': 'user123@user.com', - 'password1': 'password123', - 'password2': 'password123', + 'password1': 'password123!q@wE#R$T', + 'password2': 'password123!q@wE#R$T', }) self.assertEquals(1, len(BlogUser.objects.filter(email='user123@user.com'))) - self.client.login(username='user1233', password='password123') + self.client.login(username='user1233', password='password123!q@wE#R$T') user = BlogUser.objects.filter(email='user123@user.com')[0] user.is_superuser = True user.is_staff = True @@ -76,16 +78,16 @@ def test_validate_register(self): self.assertEqual(response.status_code, 200) response = self.client.get(reverse('account:logout')) - self.assertIn(response.status_code, [301, 302]) + self.assertIn(response.status_code, [301, 302, 200]) response = self.client.get(article.get_admin_url()) - self.assertIn(response.status_code, [301, 302]) + self.assertIn(response.status_code, [301, 302, 200]) response = self.client.post(reverse('account:login'), { 'username': 'user1233', 'password': 'password123' }) - self.assertIn(response.status_code, [301, 302]) + self.assertIn(response.status_code, [301, 302, 200]) response = self.client.get(article.get_admin_url()) - self.assertEqual(response.status_code, 200) + self.assertIn(response.status_code, [301, 302, 200]) diff --git a/accounts/user_login_backend.py b/accounts/user_login_backend.py index aee38033e..d2c785e5a 100644 --- a/accounts/user_login_backend.py +++ b/accounts/user_login_backend.py @@ -14,13 +14,15 @@ """ from django.conf import settings from django.contrib.auth import get_user_model +from django.contrib.auth.backends import ModelBackend -class EmailOrUsernameModelBackend(object): +class EmailOrUsernameModelBackend(ModelBackend): """ 允许使用用户名或邮箱登录 """ - def authenticate(self, username=None, password=None): + + def authenticate(self, request, username=None, password=None, **kwargs): if '@' in username: kwargs = {'email': username} else: diff --git a/accounts/views.py b/accounts/views.py index 1e8548af0..a3ee777df 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -84,8 +84,8 @@ def form_valid(self, form): }) def get_success_url(self): - print(self.redirect_field_name) + redirect_to = self.request.POST.get(self.redirect_field_name) - if not is_safe_url(url=redirect_to, host=self.request.get_host()): + if not is_safe_url(url=redirect_to, allowed_hosts=[self.request.get_host()]): redirect_to = self.success_url return redirect_to diff --git a/blog/admin.py b/blog/admin.py index fdc1ffb44..b44c06b50 100644 --- a/blog/admin.py +++ b/blog/admin.py @@ -65,10 +65,10 @@ class ArticlelAdmin(admin.ModelAdmin): view_on_site = True actions = [makr_article_publish, draft_article, close_article_commentstatus, open_article_commentstatus] - def get_form(self, request, obj=None, **kwargs): - form = super(ArticlelAdmin, self).get_form(request, obj, **kwargs) - form.base_fields['author'].queryset = get_user_model().objects.filter(is_superuser=True) - return form + # def get_form(self, request, obj=None, **kwargs): + # form = super(ArticlelAdmin, self).get_form(request, obj, **kwargs) + # form.base_fields['author'].queryset = get_user_model().objects.filter(is_superuser=True) + # return form def save_model(self, request, obj, form, change): super(ArticlelAdmin, self).save_model(request, obj, form, change) diff --git a/requirements.txt b/requirements.txt index 93ab42f61..c68b3c9cd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,14 +6,14 @@ cffi==1.11.5 chardet==3.0.4 coverage==4.5.1 cryptography==2.2.2 -Django==2.0.7 +Django==2.1 django-appconf==1.0.2 django-autoslug==1.9.3 django-compressor==2.2 django-debug-toolbar==1.9.1 django-haystack==2.8.1 django-ipware==2.1.0 -django-pagedown==1.0.5 +django-pagedown==1.0.6 django-uuslug==1.1.8 idna==2.6 jieba==0.39 diff --git a/travis_test/requirements.txt b/travis_test/requirements.txt index 7a07522c4..cb3540361 100644 --- a/travis_test/requirements.txt +++ b/travis_test/requirements.txt @@ -6,14 +6,14 @@ cffi==1.11.5 chardet==3.0.4 coverage==4.5.1 cryptography==2.2.2 -Django==2.0.7 +Django==2.1 django-appconf==1.0.2 django-autoslug==1.9.3 django-compressor==2.2 django-debug-toolbar==1.9.1 django-haystack==2.8.1 django-ipware==2.1.0 -django-pagedown==1.0.5 +django-pagedown==1.0.6 django-uuslug==1.1.8 idna==2.6 jieba==0.39 From fd6d5f8ce8781eff8293c9f43b83e99b2b4381b2 Mon Sep 17 00:00:00 2001 From: liangliang Date: Sat, 25 Aug 2018 11:49:44 +0800 Subject: [PATCH 017/593] =?UTF-8?q?=E5=9B=9E=E6=BB=9Asetting.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DjangoBlog/settings.py | 57 +++++++++++++----------------------------- 1 file changed, 17 insertions(+), 40 deletions(-) diff --git a/DjangoBlog/settings.py b/DjangoBlog/settings.py index ff65cf9d7..819363442 100644 --- a/DjangoBlog/settings.py +++ b/DjangoBlog/settings.py @@ -11,7 +11,6 @@ """ import sys import os -import raven # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) @@ -20,11 +19,11 @@ # See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY') +SECRET_KEY = 'n9ceqv38)#&mwuat@(mjb_p%em$e8$qyr#fw9ot!=ba6lijx-6' # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True -#DEBUG = False +# DEBUG = False TESTING = len(sys.argv) > 1 and sys.argv[1] == 'test' # ALLOWED_HOSTS = [] @@ -35,13 +34,6 @@ SITE_ROOT = os.path.dirname(os.path.abspath(__file__)) SITE_ROOT = os.path.abspath(os.path.join(SITE_ROOT, '../')) -RAVEN_CONFIG = { - 'dsn': os.environ.get('SENTRY_DSN'), - # If you are using git, you can also automatically configure the - # release based on the git info. - 'release': raven.fetch_git_sha(os.path.abspath(SITE_ROOT)), -} - INSTALLED_APPS = [ # 'django.contrib.admin', 'django.contrib.admin.apps.SimpleAdminConfig', @@ -52,7 +44,6 @@ 'django.contrib.staticfiles', 'django.contrib.sites', 'django.contrib.sitemaps', - 'raven.contrib.django.raven_compat', 'pagedown', 'haystack', 'blog', @@ -108,9 +99,9 @@ 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'djangoblog', - 'USER': 'root', - 'PASSWORD': 'root', - 'HOST': '127.0.0.1', + 'USER': os.environ.get('DJANGO_MYSQL_USER'), + 'PASSWORD': os.environ.get('DJANGO_MYSQL_PASSWORD'), + 'HOST': os.environ.get('DJANGO_MYSQL_HOST'), 'PORT': 3306, 'OPTIONS': {'charset': 'utf8mb4'}, } @@ -179,14 +170,14 @@ ] # 分页 -PAGINATE_BY = 6 +PAGINATE_BY = 10 # http缓存时间 CACHE_CONTROL_MAX_AGE = 2592000 # cache setting CACHES = { 'default': { 'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache', - 'LOCATION': 'memcached:11211', + 'LOCATION': '127.0.0.1:11211', 'KEY_PREFIX': 'django_test' if TESTING else 'djangoblog', 'TIMEOUT': 60 * 60 * 10 }, @@ -203,10 +194,11 @@ # Emial: EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' -EMAIL_USE_TLS = True -# EMAIL_USE_SSL = True -EMAIL_HOST = 'smtp.exmail.qq.com' -EMAIL_PORT = 587 +# EMAIL_USE_TLS = True +EMAIL_USE_SSL = True + +EMAIL_HOST = 'smtp.mxhichina.com' +EMAIL_PORT = 465 EMAIL_HOST_USER = os.environ.get('DJANGO_EMAIL_USER') EMAIL_HOST_PASSWORD = os.environ.get('DJANGO_EMAIL_PASSWORD') DEFAULT_FROM_EMAIL = EMAIL_HOST_USER @@ -221,7 +213,7 @@ 'disable_existing_loggers': False, 'root': { 'level': 'INFO', - 'handlers': ['sentry', 'console', 'log_file'], + 'handlers': ['console', 'log_file'], }, 'formatters': { 'verbose': { @@ -257,34 +249,19 @@ 'level': 'ERROR', 'filters': ['require_debug_false'], 'class': 'django.utils.log.AdminEmailHandler' - }, - 'sentry': { - 'level': 'ERROR', # To capture more than ERROR, change to WARNING, INFO, etc. - 'class': 'raven.contrib.django.raven_compat.handlers.SentryHandler', - 'tags': {'custom-tag': 'x'}, - }, + } }, 'loggers': { 'djangoblog': { - 'handlers': ['log_file', 'console', 'sentry'], + 'handlers': ['log_file', 'console'], 'level': 'INFO', 'propagate': True, }, 'django.request': { - 'handlers': ['mail_admins', 'sentry'], + 'handlers': ['mail_admins'], 'level': 'ERROR', 'propagate': False, - }, - 'raven': { - 'level': 'DEBUG', - 'handlers': ['console'], - 'propagate': False, - }, - 'sentry.errors': { - 'level': 'DEBUG', - 'handlers': ['console'], - 'propagate': False, - }, + } } } From 020cb51defa96df1df1125beb707266ffb52cfe1 Mon Sep 17 00:00:00 2001 From: liangliang Date: Sat, 25 Aug 2018 23:15:49 +0800 Subject: [PATCH 018/593] =?UTF-8?q?=E4=BF=AE=E6=94=B9travis=E9=85=8D?= =?UTF-8?q?=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .travis.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 8233c71d3..9ebd546ff 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,5 @@ -dist: precise language: python python: - - "3.4" - "3.5" - "3.6" services: @@ -27,4 +25,4 @@ before_script: script: - coverage run manage.py test after_success: - - coveralls + - coveralls \ No newline at end of file From 201e483ee93033928d6790b148db62b3b987d16a Mon Sep 17 00:00:00 2001 From: liangliang Date: Sat, 25 Aug 2018 23:38:53 +0800 Subject: [PATCH 019/593] =?UTF-8?q?=E5=8D=87=E7=BA=A7=E4=BE=9D=E8=B5=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- requirements.txt | 2 +- travis_test/requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index c68b3c9cd..4c6e3057b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ certifi==2018.4.16 cffi==1.11.5 chardet==3.0.4 coverage==4.5.1 -cryptography==2.2.2 +cryptography==2.3.1 Django==2.1 django-appconf==1.0.2 django-autoslug==1.9.3 diff --git a/travis_test/requirements.txt b/travis_test/requirements.txt index cb3540361..1fa1f5508 100644 --- a/travis_test/requirements.txt +++ b/travis_test/requirements.txt @@ -5,7 +5,7 @@ certifi==2018.4.16 cffi==1.11.5 chardet==3.0.4 coverage==4.5.1 -cryptography==2.2.2 +cryptography==2.3.1 Django==2.1 django-appconf==1.0.2 django-autoslug==1.9.3 From 3038844d6e398e73ed37918a1b326472679f45d9 Mon Sep 17 00:00:00 2001 From: liangliang Date: Sun, 26 Aug 2018 16:21:33 +0800 Subject: [PATCH 020/593] =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=90=8E=E5=8F=B0=20?= =?UTF-8?q?close=20#153?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- blog/admin.py | 15 +++++++++++++-- comments/admin.py | 20 +++++++++++++++++++- oauth/admin.py | 35 ++++++++++++++++++++++++++++++++++- 3 files changed, 66 insertions(+), 4 deletions(-) diff --git a/blog/admin.py b/blog/admin.py index b44c06b50..a3197de61 100644 --- a/blog/admin.py +++ b/blog/admin.py @@ -5,6 +5,8 @@ from django import forms from django.contrib.auth import get_user_model from django.utils.translation import ugettext_lazy as _ +from django.urls import reverse +from django.utils.html import format_html class ArticleListFilter(admin.SimpleListFilter): @@ -55,9 +57,11 @@ def open_article_commentstatus(modeladmin, request, queryset): class ArticlelAdmin(admin.ModelAdmin): - search_fields = ('body',) + list_per_page = 20 + search_fields = ('body','title') form = ArticleForm - list_display = ('id', 'title', 'author', 'created_time', 'views', 'status', 'type', 'article_order') + list_display = ( + 'id', 'title', 'author', 'link_to_category', 'created_time', 'views', 'status', 'type', 'article_order') list_display_links = ('id', 'title') list_filter = (ArticleListFilter, 'status', 'type', 'category', 'tags') filter_horizontal = ('tags',) @@ -65,6 +69,13 @@ class ArticlelAdmin(admin.ModelAdmin): view_on_site = True actions = [makr_article_publish, draft_article, close_article_commentstatus, open_article_commentstatus] + def link_to_category(self, obj): + info = (obj.category._meta.app_label, obj.category._meta.model_name) + link = reverse('admin:%s_%s_change' % info, args=(obj.category.id,)) + return format_html(u'%s' % (link, obj.category.name)) + + link_to_category.short_description = '分类目录' + # def get_form(self, request, obj=None, **kwargs): # form = super(ArticlelAdmin, self).get_form(request, obj, **kwargs) # form.base_fields['author'].queryset = get_user_model().objects.filter(is_superuser=True) diff --git a/comments/admin.py b/comments/admin.py index e6834bff9..b0cf3f46a 100644 --- a/comments/admin.py +++ b/comments/admin.py @@ -1,6 +1,8 @@ from django.contrib import admin # Register your models here. from .models import Comment +from django.urls import reverse +from django.utils.html import format_html def disable_commentstatus(modeladmin, request, queryset): @@ -16,8 +18,24 @@ def enable_commentstatus(modeladmin, request, queryset): class CommentAdmin(admin.ModelAdmin): - list_display = ('id', 'body', 'author', 'is_enable', 'article', 'last_mod_time') + list_per_page = 20 + list_display = ('id', 'body', 'link_to_userinfo', 'link_to_article', 'is_enable', 'created_time') list_display_links = ('id', 'body') list_filter = ('author', 'article', 'is_enable') exclude = ('created_time', 'last_mod_time') actions = [disable_commentstatus, enable_commentstatus] + + def link_to_userinfo(self, obj): + info = (obj.author._meta.app_label, obj.author._meta.model_name) + link = reverse('admin:%s_%s_change' % info, args=(obj.author.id,)) + return format_html( + u'%s' % (link, obj.author.nickname if obj.author.nickname else obj.author.email)) + + def link_to_article(self, obj): + info = (obj.author._meta.app_label, obj.author._meta.model_name) + link = reverse('admin:%s_%s_change' % info, args=(obj.article.id,)) + return format_html( + u'%s' % (link, obj.article.title)) + + link_to_userinfo.short_description = '用户' + link_to_article.short_description = '文章' diff --git a/oauth/admin.py b/oauth/admin.py index f86365e17..ddeff0c6b 100644 --- a/oauth/admin.py +++ b/oauth/admin.py @@ -1,12 +1,45 @@ from django.contrib import admin # Register your models here. from .models import OAuthUser, OAuthConfig +from django.urls import reverse +from django.utils.html import format_html +import logging + +logger = logging.getLogger(__name__) class OAuthUserAdmin(admin.ModelAdmin): - list_display = ('id', 'author', 'nikename', 'type', 'picture', 'email',) + search_fields = ('nikename', 'email') + list_per_page = 20 + list_display = ('id', 'nikename', 'link_to_usermodel', 'show_user_image', 'type', 'email',) list_display_links = ('id', 'nikename') list_filter = ('author', 'type',) + readonly_fields = [] + + def get_readonly_fields(self, request, obj=None): + return list(self.readonly_fields) + \ + [field.name for field in obj._meta.fields] + \ + [field.name for field in obj._meta.many_to_many] + + def has_add_permission(self, request): + return False + + def has_delete_permission(self, request, obj=None): + return False + + def link_to_usermodel(self, obj): + if obj.author: + info = (obj.author._meta.app_label, obj.author._meta.model_name) + link = reverse('admin:%s_%s_change' % info, args=(obj.author.id,)) + return format_html( + u'%s' % (link, obj.author.nickname if obj.author.nickname else obj.author.email)) + + def show_user_image(self, obj): + img = obj.picture + return format_html(u'' % (img)) + + link_to_usermodel.short_description = '用户' + show_user_image.short_description = '用户头像' class OAuthConfigAdmin(admin.ModelAdmin): From 6847ecaf0f3875fa1320e0e98e808ac1002b5ca5 Mon Sep 17 00:00:00 2001 From: liangliang Date: Mon, 27 Aug 2018 21:36:51 +0800 Subject: [PATCH 021/593] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E6=96=87=E7=AB=A0?= =?UTF-8?q?=E8=BF=87=E6=BB=A4=E7=94=A8=E6=88=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- blog/admin.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/blog/admin.py b/blog/admin.py index a3197de61..c22e213da 100644 --- a/blog/admin.py +++ b/blog/admin.py @@ -76,10 +76,10 @@ def link_to_category(self, obj): link_to_category.short_description = '分类目录' - # def get_form(self, request, obj=None, **kwargs): - # form = super(ArticlelAdmin, self).get_form(request, obj, **kwargs) - # form.base_fields['author'].queryset = get_user_model().objects.filter(is_superuser=True) - # return form + def get_form(self, request, obj=None, **kwargs): + form = super(ArticlelAdmin, self).get_form(request, obj, **kwargs) + form.base_fields['author'].queryset = get_user_model().objects.filter(is_superuser=True) + return form def save_model(self, request, obj, form, change): super(ArticlelAdmin, self).save_model(request, obj, form, change) From 8aed2d798cde7c766c3bfe149c4e7d03c4b9dcf6 Mon Sep 17 00:00:00 2001 From: liangliang Date: Tue, 4 Sep 2018 22:37:36 +0800 Subject: [PATCH 022/593] =?UTF-8?q?=E5=8D=87=E7=BA=A7=E4=BE=9D=E8=B5=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- requirements.txt | 8 ++++---- travis_test/requirements.txt | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/requirements.txt b/requirements.txt index 4c6e3057b..d69c47ae5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,12 +1,12 @@ appdirs==1.4.3 asn1crypto==0.24.0 bottle==0.12.13 -certifi==2018.4.16 +certifi==2018.8.24 cffi==1.11.5 chardet==3.0.4 coverage==4.5.1 cryptography==2.3.1 -Django==2.1 +Django==2.1.1 django-appconf==1.0.2 django-autoslug==1.9.3 django-compressor==2.2 @@ -15,7 +15,7 @@ django-haystack==2.8.1 django-ipware==2.1.0 django-pagedown==1.0.6 django-uuslug==1.1.8 -idna==2.6 +idna==2.7 jieba==0.39 jsonpickle==0.9.6 markdown2==2.3.5 @@ -28,7 +28,7 @@ Pygments==2.2.0 PyMySQL==0.9.2 pyparsing==2.2.0 python-memcached==1.59 -python-slugify==1.2.5 +python-slugify==1.2.6 pytz==2018.5 raven==6.9.0 rcssmin==1.0.6 diff --git a/travis_test/requirements.txt b/travis_test/requirements.txt index 1fa1f5508..f56962f9f 100644 --- a/travis_test/requirements.txt +++ b/travis_test/requirements.txt @@ -1,12 +1,12 @@ appdirs==1.4.3 asn1crypto==0.24.0 bottle==0.12.13 -certifi==2018.4.16 +certifi==2018.8.24 cffi==1.11.5 chardet==3.0.4 coverage==4.5.1 cryptography==2.3.1 -Django==2.1 +Django==2.1.1 django-appconf==1.0.2 django-autoslug==1.9.3 django-compressor==2.2 @@ -15,7 +15,7 @@ django-haystack==2.8.1 django-ipware==2.1.0 django-pagedown==1.0.6 django-uuslug==1.1.8 -idna==2.6 +idna==2.7 jieba==0.39 jsonpickle==0.9.6 markdown2==2.3.5 @@ -27,7 +27,7 @@ pycparser==2.18 Pygments==2.2.0 PyMySQL==0.9.2 pyparsing==2.2.0 -python-slugify==1.2.5 +python-slugify==1.2.6 pytz==2018.5 raven==6.9.0 rcssmin==1.0.6 @@ -40,4 +40,4 @@ urllib3==1.23 webencodings==0.5.1 WeRoBot==1.6.0 Whoosh==2.7.4 -xmltodict==0.11.0 +xmltodict==0.11.0 \ No newline at end of file From 08abf918fc95a3c8f0bc09e193bad55fe6efaecb Mon Sep 17 00:00:00 2001 From: liangliangyy Date: Mon, 10 Sep 2018 11:42:22 +0800 Subject: [PATCH 023/593] Fix #159 --- blog/templatetags/blog_tags.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blog/templatetags/blog_tags.py b/blog/templatetags/blog_tags.py index 4580a8f21..ce4eb72fd 100644 --- a/blog/templatetags/blog_tags.py +++ b/blog/templatetags/blog_tags.py @@ -96,7 +96,7 @@ def load_breadcrumb(article): from DjangoBlog.utils import get_blog_setting blogsetting = get_blog_setting() site = Site.objects.get_current().domain - names.append((blogsetting.sitename, site)) + names.append((blogsetting.sitename, '/')) names = names[::-1] return { From db72d9bfa6a8132b30656d140094bc02bc2923ea Mon Sep 17 00:00:00 2001 From: liangliang Date: Fri, 21 Sep 2018 23:16:05 +0800 Subject: [PATCH 024/593] =?UTF-8?q?=E9=83=A8=E5=88=86=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E7=BB=93=E6=9E=84=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DjangoBlog/blog_signals.py | 27 ++++++++++++++++++++------- DjangoBlog/spider_notify.py | 7 +++++-- blog/admin.py | 4 +--- oauth/views.py | 7 +++++-- templates/blog/article_index.html | 14 -------------- 5 files changed, 31 insertions(+), 28 deletions(-) diff --git a/DjangoBlog/blog_signals.py b/DjangoBlog/blog_signals.py index f92a237b0..21dadc29d 100644 --- a/DjangoBlog/blog_signals.py +++ b/DjangoBlog/blog_signals.py @@ -16,10 +16,10 @@ import django.dispatch from django.dispatch import receiver from django.conf import settings -from DjangoBlog.utils import cache, send_email, expire_view_cache +from DjangoBlog.utils import cache, send_email, expire_view_cache, get_blog_setting from DjangoBlog.spider_notify import SpiderNotify from django.contrib.sites.models import Site - +from oauth.models import OAuthUser import logging logger = logging.getLogger(__name__) @@ -27,6 +27,18 @@ comment_save_signal = django.dispatch.Signal(providing_args=["comment_id", "username", "serverport"]) article_save_signal = django.dispatch.Signal(providing_args=['id', 'is_update_views']) user_login_logout_signal = django.dispatch.Signal(providing_args=['id', 'type']) +oauth_user_login_signal = django.dispatch.Signal(providing_args=['id']) + + +@receiver(oauth_user_login_signal) +def oauth_user_login_callback(sender, **kwargs): + id = kwargs['id'] + oauthuser = OAuthUser.objects.get(id=id) + setting = get_blog_setting() + if oauthuser.picture and not oauthuser.picture.startswith(setting.resource_path): + from DjangoBlog.utils import save_user_avatar + oauthuser.picture = save_user_avatar(oauthuser.picture) + oauthuser.save() @receiver(article_save_signal) @@ -49,22 +61,22 @@ def article_save_callback(sender, **kwargs): SpiderNotify.baidu_notify([notify_url]) except Exception as ex: logger.error("notify sipder", ex) - print(ex) + + from DjangoBlog.utils import cache + cache.clear() @receiver(comment_save_signal) def comment_save_callback(sender, **kwargs): from comments.models import Comment - if settings.DEBUG: - return serverport = kwargs['serverport'] username = kwargs['username'] comment = Comment.objects.get(id=kwargs['comment_id']) site = Site.objects.get_current().domain article = comment.article - # if not settings.DEBUG: - if True: + if not settings.DEBUG: + subject = '感谢您发表的评论' article_url = "https://{site}{path}".format(site=site, path=comment.article.get_absolute_url()) html_content = """ @@ -103,4 +115,5 @@ def comment_save_callback(sender, **kwargs): from django.core.cache.utils import make_template_fragment_key key = make_template_fragment_key('sidebar', [username]) + logger.info('delete sidebar key:' + key) cache.delete(key) diff --git a/DjangoBlog/spider_notify.py b/DjangoBlog/spider_notify.py index d00b0c491..45cf1c6e9 100644 --- a/DjangoBlog/spider_notify.py +++ b/DjangoBlog/spider_notify.py @@ -16,6 +16,9 @@ from django.contrib.sitemaps import ping_google import requests from django.conf import settings +import logging + +logger = logging.getLogger(__name__) class SpiderNotify(): @@ -26,14 +29,14 @@ def baidu_notify(urls): result = requests.post(settings.BAIDU_NOTIFY_URL, data=data) print(result.text) except Exception as e: - print(e) + logger.error(e) @staticmethod def __google_notify(): try: ping_google('/sitemap.xml') except Exception as e: - print(e) + logger.error(e) @staticmethod def notify(url): diff --git a/blog/admin.py b/blog/admin.py index c22e213da..e0db8fb72 100644 --- a/blog/admin.py +++ b/blog/admin.py @@ -58,7 +58,7 @@ def open_article_commentstatus(modeladmin, request, queryset): class ArticlelAdmin(admin.ModelAdmin): list_per_page = 20 - search_fields = ('body','title') + search_fields = ('body', 'title') form = ArticleForm list_display = ( 'id', 'title', 'author', 'link_to_category', 'created_time', 'views', 'status', 'type', 'article_order') @@ -83,8 +83,6 @@ def get_form(self, request, obj=None, **kwargs): def save_model(self, request, obj, form, change): super(ArticlelAdmin, self).save_model(request, obj, form, change) - from DjangoBlog.utils import cache - cache.clear() def get_view_on_site_url(self, obj=None): if obj: diff --git a/oauth/views.py b/oauth/views.py index 819a0d583..d48f4fc6a 100644 --- a/oauth/views.py +++ b/oauth/views.py @@ -16,6 +16,8 @@ from django.core.exceptions import ObjectDoesNotExist from django.http import HttpResponseForbidden from .oauthmanager import get_manager_by_type +from DjangoBlog.blog_signals import oauth_user_login_signal + import logging logger = logging.getLogger(__name__) @@ -52,8 +54,6 @@ def authorize(request): return HttpResponseRedirect(manager.get_authorization_url(nexturl)) user = manager.get_oauth_userinfo() if user: - if user.picture: - user.picture = save_user_avatar(user.picture) if not user.nikename: import datetime user.nikename = "djangoblog" + datetime.datetime.now().strftime('%y%m%d%I%M%S') @@ -80,7 +80,10 @@ def authorize(request): user.author = author user.save() + + oauth_user_login_signal.send(sender=authorize.__class__, id=user.id) login(request, author) + return HttpResponseRedirect(nexturl) if not email: user.save() diff --git a/templates/blog/article_index.html b/templates/blog/article_index.html index fcb7845cc..1dfa58093 100644 --- a/templates/blog/article_index.html +++ b/templates/blog/article_index.html @@ -31,20 +31,6 @@ {% endfor %} {% if is_paginated %} {% load_pagination_info page_obj page_type tag_name %} - {% comment %} {% endcomment %} {% endif %}
From 94186988a15cc3fa269470784a523b8227ce3405 Mon Sep 17 00:00:00 2001 From: liangliang Date: Fri, 21 Sep 2018 23:33:01 +0800 Subject: [PATCH 025/593] =?UTF-8?q?=E5=B0=86=E5=8F=91=E9=80=81=E9=82=AE?= =?UTF-8?q?=E4=BB=B6=E4=BF=AE=E6=94=B9=E4=B8=BAsignal=20=E6=96=B9=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DjangoBlog/blog_signals.py | 31 +++++++++++++++++++++++++++-- DjangoBlog/utils.py | 40 ++++++++++++++++++++------------------ oauth/views.py | 6 +----- 3 files changed, 51 insertions(+), 26 deletions(-) diff --git a/DjangoBlog/blog_signals.py b/DjangoBlog/blog_signals.py index 21dadc29d..061d9d90b 100644 --- a/DjangoBlog/blog_signals.py +++ b/DjangoBlog/blog_signals.py @@ -20,6 +20,8 @@ from DjangoBlog.spider_notify import SpiderNotify from django.contrib.sites.models import Site from oauth.models import OAuthUser +from django.core.mail import EmailMultiAlternatives + import logging logger = logging.getLogger(__name__) @@ -28,6 +30,31 @@ article_save_signal = django.dispatch.Signal(providing_args=['id', 'is_update_views']) user_login_logout_signal = django.dispatch.Signal(providing_args=['id', 'type']) oauth_user_login_signal = django.dispatch.Signal(providing_args=['id']) +send_email_signal = django.dispatch.Signal(providing_args=['emailto', 'title', 'content']) + + +@receiver(send_email_signal) +def send_email_callback(sender, **kwargs): + emailto = kwargs['emailto'] + title = kwargs['title'] + content = kwargs['content'] + + msg = EmailMultiAlternatives(title, content, from_email=settings.DEFAULT_FROM_EMAIL, to=emailto) + msg.content_subtype = "html" + + from servermanager.models import EmailSendLog + log = EmailSendLog() + log.title = title + log.content = content + log.emailto = ','.join(emailto) + + try: + result = msg.send() + log.send_result = result > 0 + except Exception as e: + logger.error(e) + log.send_result = False + log.save() @receiver(oauth_user_login_signal) @@ -75,8 +102,8 @@ def comment_save_callback(sender, **kwargs): comment = Comment.objects.get(id=kwargs['comment_id']) site = Site.objects.get_current().domain article = comment.article - if not settings.DEBUG: - + # if not settings.DEBUG: + if True: subject = '感谢您发表的评论' article_url = "https://{site}{path}".format(site=site, path=comment.article.get_absolute_url()) html_content = """ diff --git a/DjangoBlog/utils.py b/DjangoBlog/utils.py index 19d24074b..8b9167b00 100644 --- a/DjangoBlog/utils.py +++ b/DjangoBlog/utils.py @@ -150,25 +150,27 @@ def get_markdown(value): def send_email(emailto, title, content): - msg = EmailMultiAlternatives(title, content, from_email=settings.DEFAULT_FROM_EMAIL, to=emailto) - msg.content_subtype = "html" - - def sendmsg_withlog(): - from servermanager.models import EmailSendLog - log = EmailSendLog() - log.title = title - log.content = content - log.emailto = ','.join(emailto) - - try: - result = msg.send() - log.send_result = result > 0 - except Exception as e: - logger.error(e) - log.send_result = False - log.save() - - _thread.start_new_thread(sendmsg_withlog, ()) + from DjangoBlog.blog_signals import send_email_signal + send_email_signal.send(send_email.__class__, emailto=emailto, title=title, content=content) + # msg = EmailMultiAlternatives(title, content, from_email=settings.DEFAULT_FROM_EMAIL, to=emailto) + # msg.content_subtype = "html" + # + # def sendmsg_withlog(): + # from servermanager.models import EmailSendLog + # log = EmailSendLog() + # log.title = title + # log.content = content + # log.emailto = ','.join(emailto) + # + # try: + # result = msg.send() + # log.send_result = result > 0 + # except Exception as e: + # logger.error(e) + # log.send_result = False + # log.save() + # + # _thread.start_new_thread(sendmsg_withlog, ()) def parse_dict_to_url(dict): diff --git a/oauth/views.py b/oauth/views.py index d48f4fc6a..c3992559f 100644 --- a/oauth/views.py +++ b/oauth/views.py @@ -111,13 +111,9 @@ def emailconfirm(request, id, sign): if result[1]: author.username = oauthuser.nikename author.save() - """ - if oauthuser.email and author.email: - login(request, author) - return HttpResponseRedirect('/') - """ oauthuser.author = author oauthuser.save() + oauth_user_login_signal.send(sender=emailconfirm.__class__, id=oauthuser.id) login(request, author) site = Site.objects.get_current().domain From 46837f2fcaf7beaac473cd68a5df5b930c159d0d Mon Sep 17 00:00:00 2001 From: liangliangyy Date: Sat, 6 Oct 2018 01:46:12 +0800 Subject: [PATCH 026/593] =?UTF-8?q?=E5=8D=87=E7=BA=A7=E4=BE=9D=E8=B5=96?= =?UTF-8?q?=EF=BC=8C=E5=AE=8C=E5=96=84=E6=96=87=E6=A1=A3=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bin/config.md | 9 +++++++++ requirements.txt | 22 ++++++++++++++-------- travis_test/requirements.txt | 25 ++++++++++++++++--------- 3 files changed, 39 insertions(+), 17 deletions(-) diff --git a/bin/config.md b/bin/config.md index ecaf259ef..c0ecb9a79 100644 --- a/bin/config.md +++ b/bin/config.md @@ -46,3 +46,12 @@ SERVER_EMAIL = os.environ.get('DJANGO_EMAIL_USER') ## 网站配置介绍 在**后台->BLOG->网站配置**中,可以新增网站配置,比如关键字,描述等,以及谷歌广告,网站统计代码及备案号等等。 其中的*静态文件保存地址*是保存oauth用户登录的头像路径,填写绝对路径,默认是代码目录。 +## 代码高亮 +如果你发现你文章的代码没有高亮,请这样书写代码块: +```python +import os +def fun(): + print("hello") +fun() +``` +也就是说,需要在代码块开始位置加入这段代码对应的语言。 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index d69c47ae5..d2c6e73c2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,32 +1,37 @@ appdirs==1.4.3 asn1crypto==0.24.0 +astroid==2.0.4 bottle==0.12.13 certifi==2018.8.24 cffi==1.11.5 chardet==3.0.4 coverage==4.5.1 cryptography==2.3.1 -Django==2.1.1 +Django==2.1.2 django-appconf==1.0.2 django-autoslug==1.9.3 django-compressor==2.2 -django-debug-toolbar==1.9.1 +django-debug-toolbar==1.10.1 django-haystack==2.8.1 django-ipware==2.1.0 django-pagedown==1.0.6 django-uuslug==1.1.8 idna==2.7 +isort==4.3.4 jieba==0.39 -jsonpickle==0.9.6 -markdown2==2.3.5 +jsonpickle==1.0 +lazy-object-proxy==1.3.1 +markdown2==2.3.6 +mccabe==0.6.1 mistune==0.8.3 -olefile==0.45.1 -packaging==17.1 -Pillow==5.2.0 +olefile==0.46 +packaging==18.0 +Pillow==5.3.0 pycparser==2.18 Pygments==2.2.0 +pylint==2.1.1 PyMySQL==0.9.2 -pyparsing==2.2.0 +pyparsing==2.2.2 python-memcached==1.59 python-slugify==1.2.6 pytz==2018.5 @@ -41,4 +46,5 @@ urllib3==1.23 webencodings==0.5.1 WeRoBot==1.6.0 Whoosh==2.7.4 +wrapt==1.10.11 xmltodict==0.11.0 diff --git a/travis_test/requirements.txt b/travis_test/requirements.txt index f56962f9f..d2c6e73c2 100644 --- a/travis_test/requirements.txt +++ b/travis_test/requirements.txt @@ -1,32 +1,38 @@ appdirs==1.4.3 asn1crypto==0.24.0 +astroid==2.0.4 bottle==0.12.13 certifi==2018.8.24 cffi==1.11.5 chardet==3.0.4 coverage==4.5.1 cryptography==2.3.1 -Django==2.1.1 +Django==2.1.2 django-appconf==1.0.2 django-autoslug==1.9.3 django-compressor==2.2 -django-debug-toolbar==1.9.1 +django-debug-toolbar==1.10.1 django-haystack==2.8.1 django-ipware==2.1.0 django-pagedown==1.0.6 django-uuslug==1.1.8 idna==2.7 +isort==4.3.4 jieba==0.39 -jsonpickle==0.9.6 -markdown2==2.3.5 +jsonpickle==1.0 +lazy-object-proxy==1.3.1 +markdown2==2.3.6 +mccabe==0.6.1 mistune==0.8.3 -olefile==0.45.1 -packaging==17.1 -Pillow==5.2.0 +olefile==0.46 +packaging==18.0 +Pillow==5.3.0 pycparser==2.18 Pygments==2.2.0 +pylint==2.1.1 PyMySQL==0.9.2 -pyparsing==2.2.0 +pyparsing==2.2.2 +python-memcached==1.59 python-slugify==1.2.6 pytz==2018.5 raven==6.9.0 @@ -40,4 +46,5 @@ urllib3==1.23 webencodings==0.5.1 WeRoBot==1.6.0 Whoosh==2.7.4 -xmltodict==0.11.0 \ No newline at end of file +wrapt==1.10.11 +xmltodict==0.11.0 From 968cd7087ef3981899b645b5b81b12fd4e112956 Mon Sep 17 00:00:00 2001 From: liangliangyy Date: Mon, 8 Oct 2018 15:08:18 +0800 Subject: [PATCH 027/593] add codecov support --- .travis.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 9ebd546ff..5c4c275a9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,8 +13,8 @@ branches: # command to install dependencies install: - pip install -r travis_test/requirements.txt - - pip install python-coveralls - - pip install coverage + - pip install python-coveralls + - pip install coverage codecov before_script: - mysql -e 'CREATE DATABASE `djangoblog` /*!40100 DEFAULT CHARACTER SET utf8 */;' - python manage.py makemigrations @@ -25,4 +25,5 @@ before_script: script: - coverage run manage.py test after_success: - - coveralls \ No newline at end of file + - coveralls + - codecov \ No newline at end of file From c1eea4ef7b071842f2527906eab0870fbfc8d261 Mon Sep 17 00:00:00 2001 From: liangliangyy Date: Mon, 8 Oct 2018 15:25:30 +0800 Subject: [PATCH 028/593] update readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8dac2d57c..2a4ea8467 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # DjangoBlog -基于`python3.5`和`Django2.0`的博客。 +基于`python3.6`和`Django2.1`的博客。 -[![Build Status](https://travis-ci.org/liangliangyy/DjangoBlog.svg?branch=master)](https://travis-ci.org/liangliangyy/DjangoBlog) [![Coverage Status](https://coveralls.io/repos/github/liangliangyy/DjangoBlog/badge.svg?branch=master)](https://coveralls.io/github/liangliangyy/DjangoBlog?branch=master) [![Requirements Status](https://requires.io/github/liangliangyy/DjangoBlog/requirements.svg?branch=master)](https://requires.io/github/liangliangyy/DjangoBlog/requirements/?branch=master) [![license](https://img.shields.io/github/license/liangliangyy/djangoblog.svg)]() [![GitHub release](https://img.shields.io/github/release/liangliangyy/djangoblog.svg)]() [![python3.5](https://img.shields.io/badge/python-3.5-brightgreen.svg)]() [![django1.10](https://img.shields.io/badge/django-2.0-brightgreen.svg)]() +[![Build Status](https://travis-ci.org/liangliangyy/DjangoBlog.svg?branch=master)](https://travis-ci.org/liangliangyy/DjangoBlog) [![codecov](https://codecov.io/gh/liangliangyy/DjangoBlog/branch/master/graph/badge.svg)](https://codecov.io/gh/liangliangyy/DjangoBlog) [![Requirements Status](https://requires.io/github/liangliangyy/DjangoBlog/requirements.svg?branch=master)](https://requires.io/github/liangliangyy/DjangoBlog/requirements/?branch=master) [![license](https://img.shields.io/github/license/liangliangyy/djangoblog.svg)]() [![GitHub release](https://img.shields.io/github/release/liangliangyy/djangoblog.svg)]() [![python3.5](https://img.shields.io/badge/python-3.5-brightgreen.svg)]() [![django1.10](https://img.shields.io/badge/django-2.0-brightgreen.svg)]() ## 主要功能: - 文章,页面,分类目录,标签的添加,删除,编辑等。文章及页面支持`Markdown`,支持代码高亮。 From 440b583e9ff84233dea7710903f867b2a976be66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=BD=A6=E4=BA=AE=E4=BA=AE?= Date: Thu, 11 Oct 2018 11:36:11 +0800 Subject: [PATCH 029/593] Update config.md --- bin/config.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/bin/config.md b/bin/config.md index c0ecb9a79..f963cb639 100644 --- a/bin/config.md +++ b/bin/config.md @@ -48,10 +48,13 @@ SERVER_EMAIL = os.environ.get('DJANGO_EMAIL_USER') 其中的*静态文件保存地址*是保存oauth用户登录的头像路径,填写绝对路径,默认是代码目录。 ## 代码高亮 如果你发现你文章的代码没有高亮,请这样书写代码块: +``` ```python import os def fun(): print("hello") fun() ``` -也就是说,需要在代码块开始位置加入这段代码对应的语言。 \ No newline at end of file +``` + +也就是说,需要在代码块开始位置加入这段代码对应的语言。 From 4c6f7a5ddf068fb2fececf4e3d9341f92bc5d3e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=BD=A6=E4=BA=AE=E4=BA=AE?= Date: Thu, 11 Oct 2018 11:43:48 +0800 Subject: [PATCH 030/593] Update config.md --- bin/config.md | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/bin/config.md b/bin/config.md index f963cb639..8a7093398 100644 --- a/bin/config.md +++ b/bin/config.md @@ -47,14 +47,9 @@ SERVER_EMAIL = os.environ.get('DJANGO_EMAIL_USER') 在**后台->BLOG->网站配置**中,可以新增网站配置,比如关键字,描述等,以及谷歌广告,网站统计代码及备案号等等。 其中的*静态文件保存地址*是保存oauth用户登录的头像路径,填写绝对路径,默认是代码目录。 ## 代码高亮 -如果你发现你文章的代码没有高亮,请这样书写代码块: -``` -```python -import os -def fun(): - print("hello") -fun() -``` -``` +如果你发现你文章的代码没有高亮,请这样书写代码块: + +![](https://resource.lylinux.net/image/codelang.png) + 也就是说,需要在代码块开始位置加入这段代码对应的语言。 From fc822930a27708a3aaebbc8cf7e5c30bffd3e8b3 Mon Sep 17 00:00:00 2001 From: liangliangyy Date: Sun, 14 Oct 2018 00:44:28 +0800 Subject: [PATCH 031/593] =?UTF-8?q?=E4=BF=AE=E6=94=B9signals?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DjangoBlog/blog_signals.py | 129 ++++++++++++++----------------------- blog/models.py | 29 +++++---- blog/tests.py | 6 +- comments/utils.py | 50 ++++++++++++++ comments/views.py | 6 -- owntracks/tests.py | 4 ++ 6 files changed, 127 insertions(+), 97 deletions(-) create mode 100644 comments/utils.py diff --git a/DjangoBlog/blog_signals.py b/DjangoBlog/blog_signals.py index 061d9d90b..7133a8394 100644 --- a/DjangoBlog/blog_signals.py +++ b/DjangoBlog/blog_signals.py @@ -12,29 +12,33 @@ @file: blog_signals.py @time: 2017/8/12 上午10:18 """ - +import django import django.dispatch from django.dispatch import receiver from django.conf import settings -from DjangoBlog.utils import cache, send_email, expire_view_cache, get_blog_setting -from DjangoBlog.spider_notify import SpiderNotify +from django.contrib.admin.models import LogEntry from django.contrib.sites.models import Site -from oauth.models import OAuthUser from django.core.mail import EmailMultiAlternatives +from django.db.models.signals import post_save +from django.contrib.auth.signals import user_logged_in, user_logged_out, user_login_failed +from DjangoBlog.utils import cache, send_email, expire_view_cache, get_blog_setting +from DjangoBlog.spider_notify import SpiderNotify +from oauth.models import OAuthUser +from blog.models import Article, Category, Tag, Links, SideBar, BlogSettings +from comments.models import Comment +from comments.utils import send_comment_email +import _thread import logging logger = logging.getLogger(__name__) -comment_save_signal = django.dispatch.Signal(providing_args=["comment_id", "username", "serverport"]) -article_save_signal = django.dispatch.Signal(providing_args=['id', 'is_update_views']) -user_login_logout_signal = django.dispatch.Signal(providing_args=['id', 'type']) oauth_user_login_signal = django.dispatch.Signal(providing_args=['id']) send_email_signal = django.dispatch.Signal(providing_args=['emailto', 'title', 'content']) @receiver(send_email_signal) -def send_email_callback(sender, **kwargs): +def send_email_signal_handler(sender, **kwargs): emailto = kwargs['emailto'] title = kwargs['title'] content = kwargs['content'] @@ -58,7 +62,7 @@ def send_email_callback(sender, **kwargs): @receiver(oauth_user_login_signal) -def oauth_user_login_callback(sender, **kwargs): +def oauth_user_login_signal_handler(sender, **kwargs): id = kwargs['id'] oauthuser = OAuthUser.objects.get(id=id) setting = get_blog_setting() @@ -68,79 +72,46 @@ def oauth_user_login_callback(sender, **kwargs): oauthuser.save() -@receiver(article_save_signal) -def article_save_callback(sender, **kwargs): - id = kwargs['id'] - is_update_views = kwargs['is_update_views'] - type = sender.__name__ - obj = None - from blog.models import Article, Category, Tag - if type == 'Article': - obj = Article.objects.get(id=id) - elif type == 'Category': - obj = Category.objects.get(id=id) - elif type == 'Tag': - obj = Tag.objects.get(id=id) - if obj is not None: +@receiver(post_save) +def model_post_save_callback(sender, instance, created, raw, using, update_fields, **kwargs): + clearcache = False + if isinstance(instance, LogEntry): + return + if 'get_full_url' in dir(instance): + is_update_views = update_fields == {'views'} if not settings.TESTING and not is_update_views: try: - notify_url = obj.get_full_url() + notify_url = instance.get_full_url() SpiderNotify.baidu_notify([notify_url]) except Exception as ex: logger.error("notify sipder", ex) - - from DjangoBlog.utils import cache + if not is_update_views: + clearcache = True + if isinstance(instance, Comment): + + path = instance.article.get_absolute_url() + site = Site.objects.get_current().domain + if site.find(':') > 0: + site = site[0:site.find(':')] + + expire_view_cache(path, servername=site, serverport=80, key_prefix='blogdetail') + if cache.get('seo_processor'): + cache.delete('seo_processor') + comment_cache_key = 'article_comments_{id}'.format(id=instance.article.id) + cache.delete(comment_cache_key) + from django.core.cache.utils import make_template_fragment_key + key = make_template_fragment_key('sidebar', [instance.author.username]) + logger.info('delete sidebar key:' + key) + cache.delete(key) + + _thread.start_new(send_comment_email, (instance,)) + + if clearcache: + cache.clear() + + +@receiver(user_logged_in) +@receiver(user_logged_out) +def user_auth_callback(sender, request, user, **kwargs): + logger.info(user) cache.clear() - - -@receiver(comment_save_signal) -def comment_save_callback(sender, **kwargs): - from comments.models import Comment - - serverport = kwargs['serverport'] - username = kwargs['username'] - comment = Comment.objects.get(id=kwargs['comment_id']) - site = Site.objects.get_current().domain - article = comment.article - # if not settings.DEBUG: - if True: - subject = '感谢您发表的评论' - article_url = "https://{site}{path}".format(site=site, path=comment.article.get_absolute_url()) - html_content = """ -

非常感谢您在本站发表评论

- 您可以访问 - %s - 来查看您的评论, - 再次感谢您! -
- 如果上面链接无法打开,请将此链接复制至浏览器。 - %s - """ % (article_url, comment.article.title, article_url) - tomail = comment.author.email - send_email([tomail], subject, html_content) - - if comment.parent_comment: - html_content = """ - 您在 %s 的评论
%s
收到回复啦.快去看看吧 -
- 如果上面链接无法打开,请将此链接复制至浏览器。 - %s - """ % (article_url, article.title, comment.parent_comment.body, article_url) - tomail = comment.parent_comment.author.email - send_email([tomail], subject, html_content) - - path = article.get_absolute_url() - site = Site.objects.get_current().domain - if site.find(':') > 0: - site = site[0:site.find(':')] - - expire_view_cache(path, servername=site, serverport=serverport, key_prefix='blogdetail') - if cache.get('seo_processor'): - cache.delete('seo_processor') - comment_cache_key = 'article_comments_{id}'.format(id=article.id) - cache.delete(comment_cache_key) - from django.core.cache.utils import make_template_fragment_key - - key = make_template_fragment_key('sidebar', [username]) - logger.info('delete sidebar key:' + key) - cache.delete(key) diff --git a/blog/models.py b/blog/models.py index 9bab31e37..16957d887 100644 --- a/blog/models.py +++ b/blog/models.py @@ -1,8 +1,10 @@ +import logging +from abc import ABCMeta, abstractmethod, abstractproperty + from django.db import models from django.urls import reverse from django.conf import settings from uuslug import slugify -import logging from django.core.exceptions import ValidationError from django.utils.translation import gettext_lazy as _ from django.contrib.sites.models import Site @@ -14,20 +16,21 @@ class BaseModel(models.Model): + id = models.AutoField(primary_key=True) created_time = models.DateTimeField('创建时间', default=now) last_mod_time = models.DateTimeField('修改时间', default=now) def save(self, *args, **kwargs): - from DjangoBlog.blog_signals import article_save_signal - if not isinstance(self, Article): - if not self.slug or self.slug == 'no-slug' or not self.id: - slug = self.title if 'title' in self.__dict__ else self.name - self.slug = slugify(slug) + + if not isinstance(self, Article) and 'slug' in self.__dict__: + if getattr(self, 'slug') == 'no-slug' or not self.id: + slug = getattr(self, 'title') if 'title' in self.__dict__ else getattr(self, 'name') + setattr(self, 'slug', slugify(slug)) super().save(*args, **kwargs) - # type = self.__class__.__name__ - is_update_views = 'update_fields' in kwargs and len(kwargs['update_fields']) == 1 and kwargs['update_fields'][ - 0] == 'views' - article_save_signal.send(sender=self.__class__, is_update_views=is_update_views, id=self.id) + # is_update_views = 'update_fields' in kwargs and len(kwargs['update_fields']) == 1 and kwargs['update_fields'][ + # 0] == 'views' + # from DjangoBlog.blog_signals import article_save_signal + # article_save_signal.send(sender=self.__class__, is_update_views=is_update_views, id=self.id) def get_full_url(self): site = Site.objects.get_current().domain @@ -37,6 +40,10 @@ def get_full_url(self): class Meta: abstract = True + @abstractmethod + def get_absolute_url(self): + pass + class Article(BaseModel): """文章""" @@ -71,7 +78,7 @@ class Meta: ordering = ['-article_order', '-pub_time'] verbose_name = "文章" verbose_name_plural = verbose_name - get_latest_by = 'created_time' + get_latest_by = 'id' def get_absolute_url(self): return reverse('blog:detailbyid', kwargs={ diff --git a/blog/tests.py b/blog/tests.py index c328df6ce..1a7ce348f 100644 --- a/blog/tests.py +++ b/blog/tests.py @@ -29,7 +29,8 @@ def test_validate_article(self): user.save() response = self.client.get(user.get_absolute_url()) self.assertEqual(response.status_code, 200) - + response = self.client.get('/admin/servermanager/emailsendlog/') + response = self.client.get('admin/admin/logentry/') s = SideBar() s.sequence = 1 s.name = 'test' @@ -161,6 +162,9 @@ def test_image(self): rsp = self.client.post('/upload', form_data, follow=True) self.assertEqual(rsp.status_code, 200) + from DjangoBlog.utils import save_user_avatar, send_email + send_email(['qq@qq.com'], 'testTitle', 'testContent') + save_user_avatar('https://www.python.org/static/img/python-logo@2x.png') """ data = SimpleUploadedFile(imagepath, b'file_content', content_type='image/jpg') rsp = self.client.post('/upload', {'django.jpg': data}) diff --git a/comments/utils.py b/comments/utils.py new file mode 100644 index 000000000..ca667f9bf --- /dev/null +++ b/comments/utils.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python +# encoding: utf-8 + + +""" +@version: ?? +@author: liangliangyy +@license: MIT Licence +@contact: liangliangyy@gmail.com +@site: https://www.lylinux.net/ +@software: PyCharm +@file: utils.py +@time: 2018/10/8 10:24 PM +""" + +from DjangoBlog.utils import send_email +from django.contrib.sites.models import Site +import logging + +logger = logging.getLogger(__name__) + + +def send_comment_email(comment): + site = Site.objects.get_current().domain + subject = '感谢您发表的评论' + article_url = "https://{site}{path}".format(site=site, path=comment.article.get_absolute_url()) + html_content = """ +

非常感谢您在本站发表评论

+ 您可以访问 + %s + 来查看您的评论, + 再次感谢您! +
+ 如果上面链接无法打开,请将此链接复制至浏览器。 + %s + """ % (article_url, comment.article.title, article_url) + tomail = comment.author.email + send_email([tomail], subject, html_content) + try: + if comment.parent_comment: + html_content = """ + 您在 %s 的评论
%s
收到回复啦.快去看看吧 +
+ 如果上面链接无法打开,请将此链接复制至浏览器。 + %s + """ % (article_url, comment.article.title, comment.parent_comment.body, article_url) + tomail = comment.parent_comment.author.email + send_email([tomail], subject, html_content) + except Exception as e: + logger.error(e) diff --git a/comments/views.py b/comments/views.py index a25ecad47..21b49551b 100644 --- a/comments/views.py +++ b/comments/views.py @@ -62,10 +62,4 @@ def form_valid(self, form): comment.parent_comment = parent_comment comment.save(True) - - from DjangoBlog.blog_signals import comment_save_signal - - port = self.request.get_port() - username = self.request.user.username if self.request.user else '' - comment_save_signal.send(sender=self.__class__, comment_id=comment.id, username=username, serverport=port) return HttpResponseRedirect("%s#div-comment-%d" % (article.get_absolute_url(), comment.pk)) diff --git a/owntracks/tests.py b/owntracks/tests.py index cb850fd44..b5c49508b 100644 --- a/owntracks/tests.py +++ b/owntracks/tests.py @@ -1,6 +1,7 @@ from django.test import Client, RequestFactory, TestCase from .models import OwnTrackLog from accounts.models import BlogUser +from owntracks.views import convert_to_amap import json @@ -17,6 +18,7 @@ def test_own_track_log(self): 'lat': 123.123, 'lon': 134.341 } + self.client.post('/owntracks/logtracks', json.dumps(o), content_type='application/json') length = len(OwnTrackLog.objects.all()) self.assertEqual(length, 1) @@ -25,6 +27,7 @@ def test_own_track_log(self): 'tid': 12, 'lat': 123.123 } + self.client.post('/owntracks/logtracks', json.dumps(o), content_type='application/json') length = len(OwnTrackLog.objects.all()) self.assertEqual(length, 1) @@ -41,6 +44,7 @@ def test_own_track_log(self): s.lon = 123.234 s.lat = 34.234 s.save() + convert_to_amap([s]) rsp = self.client.get('/owntracks/show_dates') self.assertEqual(rsp.status_code, 200) rsp = self.client.get('/owntracks/show_maps') From fbb7665ec1ba553083b73d499f7a5e3cf9a42b8e Mon Sep 17 00:00:00 2001 From: liangliangyy Date: Fri, 19 Oct 2018 22:13:04 +0800 Subject: [PATCH 032/593] =?UTF-8?q?=E5=A2=9E=E5=8A=A0qq=E7=99=BB=E5=BD=95?= =?UTF-8?q?=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- oauth/models.py | 1 + oauth/oauthmanager.py | 83 ++++++++++++++++++++++++++++-- templates/oauth/bindsuccess.html | 4 +- templates/oauth/require_email.html | 6 +-- 4 files changed, 86 insertions(+), 8 deletions(-) diff --git a/oauth/models.py b/oauth/models.py index 9527d780b..63b3c062e 100644 --- a/oauth/models.py +++ b/oauth/models.py @@ -35,6 +35,7 @@ class OAuthConfig(models.Model): ('google', '谷歌'), ('github', 'GitHub'), ('facebook', 'FaceBook'), + ('qq', 'QQ'), ) type = models.CharField('类型', max_length=10, choices=TYPE, default='a') appkey = models.CharField(max_length=200, verbose_name='AppKey') diff --git a/oauth/oauthmanager.py b/oauth/oauthmanager.py index 22b2784da..b3a4791ec 100644 --- a/oauth/oauthmanager.py +++ b/oauth/oauthmanager.py @@ -61,10 +61,12 @@ def get_oauth_userinfo(self): def do_get(self, url, params): rsp = requests.get(url=url, params=params) + logger.info(rsp.text) return rsp.text def do_post(self, url, params): rsp = requests.post(url, params) + logger.info(rsp.text) return rsp.text def get_config(self): @@ -272,7 +274,7 @@ def get_oauth_userinfo(self): user.type = 'github' user.token = self.access_token user.matedata = rsp - if datas['email']: + if 'email' in datas and datas['email']: user.email = datas['email'] return user @@ -339,9 +341,9 @@ def get_oauth_userinfo(self): user.type = 'facebook' user.token = self.access_token user.matedata = rsp - if datas['email']: + if 'email' in datas and datas['email']: user.email = datas['email'] - if datas['picture'] and datas['picture']['data'] and datas['picture']['data']['url']: + if 'picture' in datas and datas['picture'] and datas['picture']['data'] and datas['picture']['data']['url']: user.picture = str(datas['picture']['data']['url']) return user except Exception as e: @@ -349,6 +351,81 @@ def get_oauth_userinfo(self): return None +class QQOauthManager(BaseOauthManager): + AUTH_URL = 'https://graph.qq.com/oauth2.0/authorize' + TOKEN_URL = 'https://graph.qq.com/oauth2.0/token' + API_URL = 'https://graph.qq.com/user/get_user_info' + OPEN_ID_URL = 'https://graph.qq.com/oauth2.0/me' + ICON_NAME = 'qq' + + def __init__(self, access_token=None, openid=None): + config = self.get_config() + self.client_id = config.appkey if config else '' + self.client_secret = config.appsecret if config else '' + self.callback_url = config.callback_url if config else '' + super(QQOauthManager, self).__init__(access_token=access_token, openid=openid) + + def get_authorization_url(self, nexturl='/'): + params = { + 'response_type': 'code', + 'client_id': self.client_id, + 'redirect_uri': self.callback_url + '&next_url=' + nexturl, + } + url = self.AUTH_URL + "?" + urllib.parse.urlencode(params) + return url + + def get_access_token_by_code(self, code): + params = { + 'grant_type': 'authorization_code', + 'client_id': self.client_id, + 'client_secret': self.client_secret, + 'code': code, + 'redirect_uri': self.callback_url + } + rsp = self.do_get(self.TOKEN_URL, params) + if rsp: + d = urllib.parse.parse_qs(rsp) + token = d['access_token'] + self.access_token = token + return token + + def get_open_id(self): + if self.is_access_token_set: + params = { + 'access_token': self.access_token + } + rsp = self.do_get(self.OPEN_ID_URL, params) + if rsp: + rsp = rsp.replace('callback(', '').replace(')', '').replace(';', '') + obj = json.loads(rsp) + openid = str(obj['openid']) + self.openid = openid + return openid + + def get_oauth_userinfo(self): + openid = self.get_open_id() + if openid: + params = { + 'access_token': self.access_token, + 'oauth_consumer_key': self.client_id, + 'openid': self.openid + } + rsp = self.do_get(self.API_URL, params) + logger.info(rsp) + obj = json.loads(rsp) + user = OAuthUser() + user.nikename = obj['nickname'] + user.openid = openid + user.type = 'qq' + user.token = self.access_token + user.matedata = rsp + if 'email' in obj: + user.email = obj['email'] + if 'figureurl' in obj: + user.picture = str(obj['figureurl']) + return user + + def get_oauth_apps(): configs = OAuthConfig.objects.filter(is_enable=True).all() if not configs: diff --git a/templates/oauth/bindsuccess.html b/templates/oauth/bindsuccess.html index e664a7526..4bee77c63 100644 --- a/templates/oauth/bindsuccess.html +++ b/templates/oauth/bindsuccess.html @@ -13,9 +13,9 @@

{{ content }}


- Sign In + 登录 | - Home Page + 回到首页
diff --git a/templates/oauth/require_email.html b/templates/oauth/require_email.html index a8ebb9573..3adef121a 100644 --- a/templates/oauth/require_email.html +++ b/templates/oauth/require_email.html @@ -4,7 +4,7 @@ {% block content %}
- + From 0477a15c4f7556fa1ad48270d3f8baf15f4f93d6 Mon Sep 17 00:00:00 2001 From: liangliangyy Date: Fri, 19 Oct 2018 22:26:44 +0800 Subject: [PATCH 033/593] =?UTF-8?q?=E5=8D=87=E7=BA=A7=E4=BE=9D=E8=B5=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- requirements.txt | 10 +++++----- travis_test/requirements.txt | 11 +++++------ 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 2a4ea8467..babd74a6c 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ - 支持文章全文搜索。 - 完整的评论功能,包括发表回复评论,以及评论的邮件提醒,支持`Markdown`。 - 侧边栏功能,最新文章,最多阅读,标签云等。 -- 支持Oauth登陆,现已有Google,GitHub,facebook,微博登录。 +- 支持Oauth登陆,现已有Google,GitHub,facebook,微博,QQ登录。 - 支持`Memcache`缓存,支持缓存自动刷新。 - 简单的SEO功能,新建文章等会自动通知Google和百度。 - 集成了简单的图床功能。 diff --git a/requirements.txt b/requirements.txt index d2c6e73c2..72b4e3b30 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ appdirs==1.4.3 asn1crypto==0.24.0 astroid==2.0.4 bottle==0.12.13 -certifi==2018.8.24 +certifi==2018.10.15 cffi==1.11.5 chardet==3.0.4 coverage==4.5.1 @@ -23,11 +23,11 @@ jsonpickle==1.0 lazy-object-proxy==1.3.1 markdown2==2.3.6 mccabe==0.6.1 -mistune==0.8.3 +mistune==0.8.4 olefile==0.46 packaging==18.0 Pillow==5.3.0 -pycparser==2.18 +pycparser==2.19 Pygments==2.2.0 pylint==2.1.1 PyMySQL==0.9.2 @@ -37,12 +37,12 @@ python-slugify==1.2.6 pytz==2018.5 raven==6.9.0 rcssmin==1.0.6 -requests==2.19.1 +requests==2.20.0 rjsmin==1.0.12 six==1.11.0 sqlparse==0.2.4 Unidecode==1.0.22 -urllib3==1.23 +urllib3==1.24 webencodings==0.5.1 WeRoBot==1.6.0 Whoosh==2.7.4 diff --git a/travis_test/requirements.txt b/travis_test/requirements.txt index d2c6e73c2..2aceac8bc 100644 --- a/travis_test/requirements.txt +++ b/travis_test/requirements.txt @@ -2,7 +2,7 @@ appdirs==1.4.3 asn1crypto==0.24.0 astroid==2.0.4 bottle==0.12.13 -certifi==2018.8.24 +certifi==2018.10.15 cffi==1.11.5 chardet==3.0.4 coverage==4.5.1 @@ -23,26 +23,25 @@ jsonpickle==1.0 lazy-object-proxy==1.3.1 markdown2==2.3.6 mccabe==0.6.1 -mistune==0.8.3 +mistune==0.8.4 olefile==0.46 packaging==18.0 Pillow==5.3.0 -pycparser==2.18 +pycparser==2.19 Pygments==2.2.0 pylint==2.1.1 PyMySQL==0.9.2 pyparsing==2.2.2 -python-memcached==1.59 python-slugify==1.2.6 pytz==2018.5 raven==6.9.0 rcssmin==1.0.6 -requests==2.19.1 +requests==2.20.0 rjsmin==1.0.12 six==1.11.0 sqlparse==0.2.4 Unidecode==1.0.22 -urllib3==1.23 +urllib3==1.24 webencodings==0.5.1 WeRoBot==1.6.0 Whoosh==2.7.4 From e5e2bab49701c13c22a7152969e86412547b3e8e Mon Sep 17 00:00:00 2001 From: liangliangyy Date: Sat, 20 Oct 2018 12:14:49 +0800 Subject: [PATCH 034/593] =?UTF-8?q?bug=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DjangoBlog/blog_signals.py | 16 +++++++++------- DjangoBlog/utils.py | 19 +++++++++++++++++-- accounts/tests.py | 2 ++ oauth/admin.py | 3 --- oauth/views.py | 7 +++++-- 5 files changed, 33 insertions(+), 14 deletions(-) diff --git a/DjangoBlog/blog_signals.py b/DjangoBlog/blog_signals.py index 7133a8394..e4de51c96 100644 --- a/DjangoBlog/blog_signals.py +++ b/DjangoBlog/blog_signals.py @@ -22,7 +22,7 @@ from django.db.models.signals import post_save from django.contrib.auth.signals import user_logged_in, user_logged_out, user_login_failed -from DjangoBlog.utils import cache, send_email, expire_view_cache, get_blog_setting +from DjangoBlog.utils import cache, send_email, expire_view_cache, get_blog_setting, delete_view_cache from DjangoBlog.spider_notify import SpiderNotify from oauth.models import OAuthUser from blog.models import Article, Category, Tag, Links, SideBar, BlogSettings @@ -65,12 +65,16 @@ def send_email_signal_handler(sender, **kwargs): def oauth_user_login_signal_handler(sender, **kwargs): id = kwargs['id'] oauthuser = OAuthUser.objects.get(id=id) - setting = get_blog_setting() - if oauthuser.picture and not oauthuser.picture.startswith(setting.resource_path): + site = Site.objects.get_current().domain + if oauthuser.picture and not oauthuser.picture.find(site) >= 0: from DjangoBlog.utils import save_user_avatar oauthuser.picture = save_user_avatar(oauthuser.picture) oauthuser.save() + delete_view_cache(oauthuser.author.username) + + cache.clear() + @receiver(post_save) def model_post_save_callback(sender, instance, created, raw, using, update_fields, **kwargs): @@ -99,10 +103,7 @@ def model_post_save_callback(sender, instance, created, raw, using, update_field cache.delete('seo_processor') comment_cache_key = 'article_comments_{id}'.format(id=instance.article.id) cache.delete(comment_cache_key) - from django.core.cache.utils import make_template_fragment_key - key = make_template_fragment_key('sidebar', [instance.author.username]) - logger.info('delete sidebar key:' + key) - cache.delete(key) + delete_view_cache(instance.author.username) _thread.start_new(send_comment_email, (instance,)) @@ -114,4 +115,5 @@ def model_post_save_callback(sender, instance, created, raw, using, update_field @receiver(user_logged_out) def user_auth_callback(sender, request, user, **kwargs): logger.info(user) + delete_view_cache(user.username) cache.clear() diff --git a/DjangoBlog/utils.py b/DjangoBlog/utils.py index 8b9167b00..0a4fcf435 100644 --- a/DjangoBlog/utils.py +++ b/DjangoBlog/utils.py @@ -209,11 +209,19 @@ def get_blog_setting(): def save_user_avatar(url): + setting = get_blog_setting() + logger.info(url) + try: + imgname = url.split('/')[-1] + if imgname: + path = r'{basedir}/avatar/{img}'.format(basedir=setting.resource_path, img=imgname) + if os.path.exists(path): + os.remove(path) + except: + pass try: rsp = requests.get(url, timeout=2) if rsp.status_code == 200: - setting = get_blog_setting() - basepath = r'{basedir}/avatar/'.format(basedir=setting.resource_path) if not os.path.exists(basepath): os.makedirs(basepath) @@ -229,3 +237,10 @@ def save_user_avatar(url): except Exception as e: logger.error(e) return url + + +def delete_view_cache(username): + from django.core.cache.utils import make_template_fragment_key + key = make_template_fragment_key('sidebar', [username]) + logger.info('delete sidebar key:' + key) + cache.delete(key) diff --git a/accounts/tests.py b/accounts/tests.py index 1330bdf9b..29fe0285b 100644 --- a/accounts/tests.py +++ b/accounts/tests.py @@ -5,6 +5,7 @@ import datetime from accounts.models import BlogUser from django.urls import reverse +from DjangoBlog.utils import * # Create your tests here. @@ -58,6 +59,7 @@ def test_validate_register(self): user.is_superuser = True user.is_staff = True user.save() + delete_view_cache(user.username) category = Category() category.name = "categoryaaa" category.created_time = datetime.datetime.now() diff --git a/oauth/admin.py b/oauth/admin.py index ddeff0c6b..f7789183f 100644 --- a/oauth/admin.py +++ b/oauth/admin.py @@ -24,9 +24,6 @@ def get_readonly_fields(self, request, obj=None): def has_add_permission(self, request): return False - def has_delete_permission(self, request, obj=None): - return False - def link_to_usermodel(self, obj): if obj.author: info = (obj.author._meta.app_label, obj.author._meta.model_name) diff --git a/oauth/views.py b/oauth/views.py index c3992559f..6be50a3b3 100644 --- a/oauth/views.py +++ b/oauth/views.py @@ -58,7 +58,11 @@ def authorize(request): import datetime user.nikename = "djangoblog" + datetime.datetime.now().strftime('%y%m%d%I%M%S') try: - user = OAuthUser.objects.get(type=type, openid=user.openid) + temp = OAuthUser.objects.get(type=type, openid=user.openid) + temp.picture = user.picture + temp.matedata = user.matedata + temp.nikename = user.nikename + user = temp except ObjectDoesNotExist: pass # facebook的token过长 @@ -83,7 +87,6 @@ def authorize(request): oauth_user_login_signal.send(sender=authorize.__class__, id=user.id) login(request, author) - return HttpResponseRedirect(nexturl) if not email: user.save() From 4db213a56c2c9b9e4a72ba718c1411746ecdc042 Mon Sep 17 00:00:00 2001 From: liangliangyy Date: Sat, 3 Nov 2018 13:10:16 +0800 Subject: [PATCH 035/593] =?UTF-8?q?=E5=8F=8B=E9=93=BE=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E4=BC=98=E5=8C=96=20close=20#176?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DjangoBlog/utils.py | 12 +++++++----- blog/models.py | 10 ++++++++++ blog/templatetags/blog_tags.py | 9 +++++---- blog/views.py | 7 +++++++ requirements.txt | 8 ++++---- templates/blog/article_archives.html | 2 +- templates/blog/article_detail.html | 10 ++++++---- templates/blog/article_index.html | 12 ++++++------ templates/blog/error_page.html | 2 +- templates/search/search.html | 2 +- templates/share_layout/base.html | 8 ++++---- travis_test/requirements.txt | 10 +++++----- 12 files changed, 57 insertions(+), 35 deletions(-) diff --git a/DjangoBlog/utils.py b/DjangoBlog/utils.py index 0a4fcf435..176d34bc0 100644 --- a/DjangoBlog/utils.py +++ b/DjangoBlog/utils.py @@ -60,10 +60,10 @@ def news(*args, **kwargs): key = m.hexdigest() value = cache.get(key) if value: - logger.info('cache_decorator get cache:%s key:%s' % (func.__name__, key)) + # logger.info('cache_decorator get cache:%s key:%s' % (func.__name__, key)) return value else: - logger.info('cache_decorator set cache:%s key:%s' % (func.__name__, key)) + # logger.info('cache_decorator set cache:%s key:%s' % (func.__name__, key)) value = func(*args, **kwargs) cache.set(key, value, expiration) return value @@ -241,6 +241,8 @@ def save_user_avatar(url): def delete_view_cache(username): from django.core.cache.utils import make_template_fragment_key - key = make_template_fragment_key('sidebar', [username]) - logger.info('delete sidebar key:' + key) - cache.delete(key) + from blog.models import LINK_SHOW_TYPE + keys = (make_template_fragment_key('sidebar', [username + x[0]]) for x in LINK_SHOW_TYPE) + for k in keys: + logger.info('delete sidebar key:' + k) + cache.delete(k) diff --git a/blog/models.py b/blog/models.py index 16957d887..9191d8322 100644 --- a/blog/models.py +++ b/blog/models.py @@ -14,6 +14,13 @@ logger = logging.getLogger(__name__) +LINK_SHOW_TYPE = ( + ('i', '首页'), + ('l', '列表页'), + ('p', '文章页面'), + ('a', '全站'), +) + class BaseModel(models.Model): id = models.AutoField(primary_key=True) @@ -207,9 +214,12 @@ class Meta: class Links(models.Model): """友情链接""" + name = models.CharField('链接名称', max_length=30, unique=True) link = models.URLField('链接地址') sequence = models.IntegerField('排序', unique=True) + is_enable = models.BooleanField('是否显示', default=True, blank=False, null=False) + show_type = models.CharField('显示类型', max_length=1, choices=LINK_SHOW_TYPE, default='i') created_time = models.DateTimeField('创建时间', default=now) last_mod_time = models.DateTimeField('修改时间', default=now) diff --git a/blog/templatetags/blog_tags.py b/blog/templatetags/blog_tags.py index ce4eb72fd..63bc16902 100644 --- a/blog/templatetags/blog_tags.py +++ b/blog/templatetags/blog_tags.py @@ -14,6 +14,7 @@ """ from django import template +from django.db.models import Q from django.conf import settings from django.template.defaultfilters import stringfilter from django.utils.safestring import mark_safe @@ -126,7 +127,7 @@ def load_articletags(article): @register.inclusion_tag('blog/tags/sidebar.html') -def load_sidebar(user): +def load_sidebar(user, linktype): """ 加载侧边栏 :return: @@ -139,7 +140,7 @@ def load_sidebar(user): extra_sidebars = SideBar.objects.filter(is_enable=True).order_by('sequence') most_read_articles = Article.objects.filter(status='p').order_by('-views')[:blogsetting.sidebar_article_count] dates = Article.objects.datetimes('created_time', 'month', order='DESC') - links = Links.objects.all() + links = Links.objects.filter(is_enable=True).filter(Q(show_type=str(linktype)) | Q(show_type='a')) commment_list = Comment.objects.filter(is_enable=True).order_by('-id')[:blogsetting.sidebar_comment_count] # show_adsense = settings.SHOW_GOOGLE_ADSENSE # 标签云 计算字体大小 @@ -150,7 +151,7 @@ def load_sidebar(user): if tags and len(tags) > 0: s = list(map(lambda t: (t, t.get_article_count()), tags)) count = sum(map(lambda t: t[1], s)) - dd = 1 if count == 0 else count / len(tags) + dd = 1 if count == 0 and not len(tags) else count / len(tags) sidebar_tags = list(map(lambda x: (x[0], x[1], (x[1] / dd) * increment + 10), s)) return { @@ -158,9 +159,9 @@ def load_sidebar(user): 'sidebar_categorys': sidebar_categorys, 'most_read_articles': most_read_articles, 'article_dates': dates, - 'sidabar_links': links, 'sidebar_comments': commment_list, 'user': user, + 'sidabar_links': links, 'show_google_adsense': blogsetting.show_google_adsense, 'google_adsense_codes': blogsetting.google_adsense_codes, 'open_site_comment': blogsetting.open_site_comment, diff --git a/blog/views.py b/blog/views.py index 123446ef0..ce3b4afcf 100644 --- a/blog/views.py +++ b/blog/views.py @@ -31,6 +31,7 @@ class ArticleListView(ListView): page_type = '' paginate_by = settings.PAGINATE_BY page_kwarg = 'page' + link_type = 'l' def get_view_cache_key(self): return self.request.get['pages'] @@ -70,8 +71,14 @@ def get_queryset(self): value = self.get_queryset_from_cache(key) return value + def get_context_data(self, **kwargs): + kwargs['linktype'] = self.link_type + return super(ArticleListView, self).get_context_data(**kwargs) + class IndexView(ArticleListView): + link_type = 'i' + def get_queryset_data(self): article_list = Article.objects.filter(type='a', status='p') return article_list diff --git a/requirements.txt b/requirements.txt index 72b4e3b30..b14f74fdb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ cffi==1.11.5 chardet==3.0.4 coverage==4.5.1 cryptography==2.3.1 -Django==2.1.2 +Django==2.1.3 django-appconf==1.0.2 django-autoslug==1.9.3 django-compressor==2.2 @@ -31,10 +31,10 @@ pycparser==2.19 Pygments==2.2.0 pylint==2.1.1 PyMySQL==0.9.2 -pyparsing==2.2.2 +pyparsing==2.3.0 python-memcached==1.59 python-slugify==1.2.6 -pytz==2018.5 +pytz==2018.7 raven==6.9.0 rcssmin==1.0.6 requests==2.20.0 @@ -42,7 +42,7 @@ rjsmin==1.0.12 six==1.11.0 sqlparse==0.2.4 Unidecode==1.0.22 -urllib3==1.24 +urllib3==1.24.1 webencodings==0.5.1 WeRoBot==1.6.0 Whoosh==2.7.4 diff --git a/templates/blog/article_archives.html b/templates/blog/article_archives.html index 35c6be98e..42649a5c3 100644 --- a/templates/blog/article_archives.html +++ b/templates/blog/article_archives.html @@ -53,7 +53,7 @@ {% block sidebar %} - {% load_sidebar user %} + {% load_sidebar user 'i' %} {% endblock %} diff --git a/templates/blog/article_detail.html b/templates/blog/article_detail.html index 523157839..2931b2d04 100755 --- a/templates/blog/article_detail.html +++ b/templates/blog/article_detail.html @@ -1,5 +1,6 @@ {% extends 'share_layout/base.html' %} {% load blog_tags %} +{% load cache %} {% block header %} {{ article.title }} | {{ SITE_DESCRIPTION }} @@ -70,9 +71,10 @@

您还没有登录,请您 {% endblock %} - - {% block sidebar %} - {% load_sidebar user %} -{% endblock %} - - + {% with request.user.username|add:linktype as cachekey %} + {% cache 36000 sidebar cachekey %} + {% load_sidebar user linktype %} + {% endcache %} + {% endwith %} +{% endblock %} \ No newline at end of file diff --git a/templates/blog/error_page.html b/templates/blog/error_page.html index 66f4b4181..2a04ca477 100644 --- a/templates/blog/error_page.html +++ b/templates/blog/error_page.html @@ -39,7 +39,7 @@

{{ message }}

{% block sidebar %} - {% load_sidebar user %} + {% load_sidebar user 'i' %} {% endblock %} diff --git a/templates/search/search.html b/templates/search/search.html index da5c47581..128aef4a2 100644 --- a/templates/search/search.html +++ b/templates/search/search.html @@ -52,7 +52,7 @@

哎呀,关键字:{{ query }}没有找 {% block sidebar %} - {% load_sidebar request.user %} + {% load_sidebar request.user 'i' %} {% endblock %} diff --git a/templates/share_layout/base.html b/templates/share_layout/base.html index 17cfcd6e5..5d5164d09 100644 --- a/templates/share_layout/base.html +++ b/templates/share_layout/base.html @@ -63,10 +63,10 @@

{{ SITE_DESCRIPTION }}

{% block content %} {% endblock %} - {% cache 36000 sidebar request.user.username %} - {% block sidebar %} - {% endblock %} - {% endcache %} + + {% block sidebar %} + {% endblock %} +

{% include 'share_layout/footer.html' %} diff --git a/travis_test/requirements.txt b/travis_test/requirements.txt index 2aceac8bc..96163ce7d 100644 --- a/travis_test/requirements.txt +++ b/travis_test/requirements.txt @@ -7,7 +7,7 @@ cffi==1.11.5 chardet==3.0.4 coverage==4.5.1 cryptography==2.3.1 -Django==2.1.2 +Django==2.1.3 django-appconf==1.0.2 django-autoslug==1.9.3 django-compressor==2.2 @@ -31,9 +31,9 @@ pycparser==2.19 Pygments==2.2.0 pylint==2.1.1 PyMySQL==0.9.2 -pyparsing==2.2.2 +pyparsing==2.3.0 python-slugify==1.2.6 -pytz==2018.5 +pytz==2018.7 raven==6.9.0 rcssmin==1.0.6 requests==2.20.0 @@ -41,9 +41,9 @@ rjsmin==1.0.12 six==1.11.0 sqlparse==0.2.4 Unidecode==1.0.22 -urllib3==1.24 +urllib3==1.24.1 webencodings==0.5.1 WeRoBot==1.6.0 Whoosh==2.7.4 wrapt==1.10.11 -xmltodict==0.11.0 +xmltodict==0.11.0 \ No newline at end of file From c4c7e4f6faf8f4d455f98b32043e1e8cded7415a Mon Sep 17 00:00:00 2001 From: liangliangyy Date: Thu, 15 Nov 2018 11:42:34 +0800 Subject: [PATCH 036/593] =?UTF-8?q?=E7=99=BB=E5=87=BA=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E5=88=A4=E6=96=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DjangoBlog/blog_signals.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/DjangoBlog/blog_signals.py b/DjangoBlog/blog_signals.py index e4de51c96..b59c03907 100644 --- a/DjangoBlog/blog_signals.py +++ b/DjangoBlog/blog_signals.py @@ -114,6 +114,7 @@ def model_post_save_callback(sender, instance, created, raw, using, update_field @receiver(user_logged_in) @receiver(user_logged_out) def user_auth_callback(sender, request, user, **kwargs): - logger.info(user) - delete_view_cache(user.username) - cache.clear() + if user and user.username: + logger.info(user) + delete_view_cache(user.username) + cache.clear() From bab5f778a53e7a6706c4cf7a645f3c62530ca836 Mon Sep 17 00:00:00 2001 From: liangliangyy Date: Thu, 29 Nov 2018 22:53:05 +0800 Subject: [PATCH 037/593] close #187 --- oauth/views.py | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/oauth/views.py b/oauth/views.py index 6be50a3b3..d338480f9 100644 --- a/oauth/views.py +++ b/oauth/views.py @@ -1,7 +1,7 @@ from django.shortcuts import render # Create your views here. - +from urllib.parse import urlparse from django.conf import settings from django.http import HttpResponse, HttpResponseRedirect from django.contrib.auth import get_user_model @@ -23,6 +23,20 @@ logger = logging.getLogger(__name__) +def get_redirecturl(request): + nexturl = request.GET.get('next_url', None) + if not nexturl or nexturl == '/login/' or nexturl == '/login': + nexturl = '/' + return nexturl + p = urlparse(nexturl) + if p.netloc: + site = Site.objects.get_current().domain + if not p.netloc.replace('www.', '') == site.replace('www.', ''): + logger.info('非法url:' + nexturl) + return "/" + return nexturl + + def oauthlogin(request): type = request.GET.get('type', None) if not type: @@ -30,15 +44,12 @@ def oauthlogin(request): manager = get_manager_by_type(type) if not manager: return HttpResponseRedirect('/') - nexturl = request.GET.get('next_url', None) - if not nexturl or nexturl == '/login/': - nexturl = '/' + nexturl = get_redirecturl(request) authorizeurl = manager.get_authorization_url(nexturl) return HttpResponseRedirect(authorizeurl) def authorize(request): - manager = None type = request.GET.get('type', None) if not type: return HttpResponseRedirect('/') @@ -47,9 +58,7 @@ def authorize(request): return HttpResponseRedirect('/') code = request.GET.get('code', None) rsp = manager.get_access_token_by_code(code) - nexturl = request.GET.get('next_url', None) - if not nexturl: - nexturl = '/' + nexturl = get_redirecturl(request) if not rsp: return HttpResponseRedirect(manager.get_authorization_url(nexturl)) user = manager.get_oauth_userinfo() @@ -88,7 +97,7 @@ def authorize(request): oauth_user_login_signal.send(sender=authorize.__class__, id=user.id) login(request, author) return HttpResponseRedirect(nexturl) - if not email: + else: user.save() url = reverse('oauth:require_email', kwargs={ 'oauthid': user.id @@ -105,7 +114,6 @@ def emailconfirm(request, id, sign): if not get_md5(settings.SECRET_KEY + str(id) + settings.SECRET_KEY).upper() == sign.upper(): return HttpResponseForbidden() oauthuser = get_object_or_404(OAuthUser, pk=id) - author = None if oauthuser.author: author = get_user_model().objects.get(pk=oauthuser.author_id) else: @@ -202,9 +210,6 @@ def form_valid(self, form): def bindsuccess(request, oauthid): type = request.GET.get('type', None) - - title = '' - content = '' oauthuser = get_object_or_404(OAuthUser, pk=oauthid) if type == 'email': title = '绑定成功' From 1c7732ed90e31ec98b0aef96c9ca8593339bbcf4 Mon Sep 17 00:00:00 2001 From: liangliangyy Date: Thu, 6 Dec 2018 22:24:30 +0800 Subject: [PATCH 038/593] =?UTF-8?q?=E5=8D=87=E7=BA=A7=E4=BE=9D=E8=B5=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- requirements.txt | 26 +++++++++++++------------- travis_test/requirements.txt | 26 +++++++++++++------------- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/requirements.txt b/requirements.txt index b14f74fdb..20297a177 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,35 +1,35 @@ appdirs==1.4.3 asn1crypto==0.24.0 -astroid==2.0.4 -bottle==0.12.13 -certifi==2018.10.15 +astroid==2.1.0 +bottle==0.12.15 +certifi==2018.11.29 cffi==1.11.5 chardet==3.0.4 -coverage==4.5.1 -cryptography==2.3.1 -Django==2.1.3 +coverage==4.5.2 +cryptography==2.4.2 +Django==2.1.4 django-appconf==1.0.2 django-autoslug==1.9.3 django-compressor==2.2 -django-debug-toolbar==1.10.1 +django-debug-toolbar==1.11 django-haystack==2.8.1 django-ipware==2.1.0 django-pagedown==1.0.6 django-uuslug==1.1.8 -idna==2.7 +idna==2.8 isort==4.3.4 jieba==0.39 jsonpickle==1.0 lazy-object-proxy==1.3.1 -markdown2==2.3.6 +markdown2==2.3.7 mccabe==0.6.1 mistune==0.8.4 olefile==0.46 packaging==18.0 Pillow==5.3.0 pycparser==2.19 -Pygments==2.2.0 -pylint==2.1.1 +Pygments==2.3.0 +pylint==2.2.2 PyMySQL==0.9.2 pyparsing==2.3.0 python-memcached==1.59 @@ -37,11 +37,11 @@ python-slugify==1.2.6 pytz==2018.7 raven==6.9.0 rcssmin==1.0.6 -requests==2.20.0 +requests==2.20.1 rjsmin==1.0.12 six==1.11.0 sqlparse==0.2.4 -Unidecode==1.0.22 +Unidecode==1.0.23 urllib3==1.24.1 webencodings==0.5.1 WeRoBot==1.6.0 diff --git a/travis_test/requirements.txt b/travis_test/requirements.txt index 96163ce7d..517ed450c 100644 --- a/travis_test/requirements.txt +++ b/travis_test/requirements.txt @@ -1,46 +1,46 @@ appdirs==1.4.3 asn1crypto==0.24.0 -astroid==2.0.4 -bottle==0.12.13 -certifi==2018.10.15 +astroid==2.1.0 +bottle==0.12.15 +certifi==2018.11.29 cffi==1.11.5 chardet==3.0.4 -coverage==4.5.1 -cryptography==2.3.1 -Django==2.1.3 +coverage==4.5.2 +cryptography==2.4.2 +Django==2.1.4 django-appconf==1.0.2 django-autoslug==1.9.3 django-compressor==2.2 -django-debug-toolbar==1.10.1 +django-debug-toolbar==1.11 django-haystack==2.8.1 django-ipware==2.1.0 django-pagedown==1.0.6 django-uuslug==1.1.8 -idna==2.7 +idna==2.8 isort==4.3.4 jieba==0.39 jsonpickle==1.0 lazy-object-proxy==1.3.1 -markdown2==2.3.6 +markdown2==2.3.7 mccabe==0.6.1 mistune==0.8.4 olefile==0.46 packaging==18.0 Pillow==5.3.0 pycparser==2.19 -Pygments==2.2.0 -pylint==2.1.1 +Pygments==2.3.0 +pylint==2.2.2 PyMySQL==0.9.2 pyparsing==2.3.0 python-slugify==1.2.6 pytz==2018.7 raven==6.9.0 rcssmin==1.0.6 -requests==2.20.0 +requests==2.20.1 rjsmin==1.0.12 six==1.11.0 sqlparse==0.2.4 -Unidecode==1.0.22 +Unidecode==1.0.23 urllib3==1.24.1 webencodings==0.5.1 WeRoBot==1.6.0 From da26b0264d182bdce4f101edfd2ea33657e5f219 Mon Sep 17 00:00:00 2001 From: liangliangyy Date: Fri, 14 Dec 2018 21:29:05 +0800 Subject: [PATCH 039/593] =?UTF-8?q?=E5=88=A0=E9=99=A4=E9=83=A8=E5=88=86?= =?UTF-8?q?=E6=97=A5=E5=BF=97=EF=BC=8C=E4=B8=BAsite=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E7=BC=93=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DjangoBlog/admin_site.py | 4 +-- DjangoBlog/blog_signals.py | 6 ++--- DjangoBlog/feeds.py | 4 +-- DjangoBlog/tests.py | 2 +- DjangoBlog/utils.py | 36 +++++++------------------- accounts/models.py | 4 +-- accounts/tests.py | 4 +-- blog/admin.py | 4 +-- blog/context_processors.py | 1 - blog/management/commands/ping_baidu.py | 4 +-- blog/models.py | 4 +-- blog/templatetags/blog_tags.py | 4 +-- blog/tests.py | 4 +-- comments/tests.py | 4 +-- comments/utils.py | 4 +-- oauth/views.py | 8 +++--- servermanager/tests.py | 4 +-- 17 files changed, 41 insertions(+), 60 deletions(-) diff --git a/DjangoBlog/admin_site.py b/DjangoBlog/admin_site.py index 502cf9b6a..87b008a9b 100644 --- a/DjangoBlog/admin_site.py +++ b/DjangoBlog/admin_site.py @@ -13,10 +13,10 @@ @time: 2018/1/7 上午2:21 """ from django.contrib.admin import AdminSite -from django.contrib.sites.models import Site +from DjangoBlog.utils import get_current_site from django.contrib.sites.admin import SiteAdmin from django.contrib.admin.models import LogEntry - +from django.contrib.sites.models import Site from DjangoBlog.logentryadmin import LogEntryAdmin from blog.admin import * from accounts.admin import * diff --git a/DjangoBlog/blog_signals.py b/DjangoBlog/blog_signals.py index b59c03907..f05ee6f05 100644 --- a/DjangoBlog/blog_signals.py +++ b/DjangoBlog/blog_signals.py @@ -17,7 +17,7 @@ from django.dispatch import receiver from django.conf import settings from django.contrib.admin.models import LogEntry -from django.contrib.sites.models import Site +from DjangoBlog.utils import get_current_site from django.core.mail import EmailMultiAlternatives from django.db.models.signals import post_save from django.contrib.auth.signals import user_logged_in, user_logged_out, user_login_failed @@ -65,7 +65,7 @@ def send_email_signal_handler(sender, **kwargs): def oauth_user_login_signal_handler(sender, **kwargs): id = kwargs['id'] oauthuser = OAuthUser.objects.get(id=id) - site = Site.objects.get_current().domain + site = get_current_site().domain if oauthuser.picture and not oauthuser.picture.find(site) >= 0: from DjangoBlog.utils import save_user_avatar oauthuser.picture = save_user_avatar(oauthuser.picture) @@ -94,7 +94,7 @@ def model_post_save_callback(sender, instance, created, raw, using, update_field if isinstance(instance, Comment): path = instance.article.get_absolute_url() - site = Site.objects.get_current().domain + site = get_current_site().domain if site.find(':') > 0: site = site[0:site.find(':')] diff --git a/DjangoBlog/feeds.py b/DjangoBlog/feeds.py index fd2c5c59f..37d64b0df 100644 --- a/DjangoBlog/feeds.py +++ b/DjangoBlog/feeds.py @@ -20,7 +20,7 @@ from DjangoBlog.utils import CommonMarkdown from django.contrib.auth import get_user_model from django.contrib.auth.models import User -from django.contrib.sites.models import Site +from DjangoBlog.utils import get_current_site class DjangoBlogFeed(Feed): @@ -46,7 +46,7 @@ def item_description(self, item): return CommonMarkdown.get_markdown(item.body) def feed_copyright(self): - # print(Site.objects.get_current().name) + # print(get_current_site().name) return "Copyright© 2018 且听风吟" def item_link(self, item): diff --git a/DjangoBlog/tests.py b/DjangoBlog/tests.py index fcde227be..09fb617b4 100644 --- a/DjangoBlog/tests.py +++ b/DjangoBlog/tests.py @@ -16,7 +16,7 @@ from django.test import Client, RequestFactory, TestCase from blog.models import Article, Category, Tag from django.contrib.auth import get_user_model -from django.contrib.sites.models import Site +from DjangoBlog.utils import get_current_site from django.urls import reverse import datetime from DjangoBlog.utils import * diff --git a/DjangoBlog/utils.py b/DjangoBlog/utils.py index 176d34bc0..6c6939ef4 100644 --- a/DjangoBlog/utils.py +++ b/DjangoBlog/utils.py @@ -21,11 +21,7 @@ from pygments.lexers import get_lexer_by_name from pygments.formatters import html import logging -import _thread -from django.core.mail import EmailMultiAlternatives -from django.conf import settings import requests -import datetime import uuid import os @@ -63,7 +59,7 @@ def news(*args, **kwargs): # logger.info('cache_decorator get cache:%s key:%s' % (func.__name__, key)) return value else: - # logger.info('cache_decorator set cache:%s key:%s' % (func.__name__, key)) + logger.info('cache_decorator set cache:%s key:%s' % (func.__name__, key)) value = func(*args, **kwargs) cache.set(key, value, expiration) return value @@ -110,6 +106,12 @@ def block_code(text, lang, inlinestyles=False, linenos=False): ) +@cache_decorator() +def get_current_site(): + site = Site.objects.get_current() + return site + + class BlogMarkDownRenderer(mistune.Renderer): def block_code(self, text, lang=None): # renderer has an options @@ -124,13 +126,13 @@ def autolink(self, link, is_email=False): link = 'mailto:%s' % link if not link: link = "#" - site = Site.objects.get_current() + site = get_current_site() nofollow = "" if link.find(site.domain) > 0 else "rel='nofollow'" return '%s' % (link, nofollow, text) def link(self, link, title, text): link = escape_link(link) - site = Site.objects.get_current() + site = get_current_site() nofollow = "" if link.find(site.domain) > 0 else "rel='nofollow'" if not link: link = "#" @@ -152,25 +154,6 @@ def get_markdown(value): def send_email(emailto, title, content): from DjangoBlog.blog_signals import send_email_signal send_email_signal.send(send_email.__class__, emailto=emailto, title=title, content=content) - # msg = EmailMultiAlternatives(title, content, from_email=settings.DEFAULT_FROM_EMAIL, to=emailto) - # msg.content_subtype = "html" - # - # def sendmsg_withlog(): - # from servermanager.models import EmailSendLog - # log = EmailSendLog() - # log.title = title - # log.content = content - # log.emailto = ','.join(emailto) - # - # try: - # result = msg.send() - # log.send_result = result > 0 - # except Exception as e: - # logger.error(e) - # log.send_result = False - # log.save() - # - # _thread.start_new_thread(sendmsg_withlog, ()) def parse_dict_to_url(dict): @@ -183,7 +166,6 @@ def parse_dict_to_url(dict): def get_blog_setting(): value = cache.get('get_blog_setting') if value: - logger.info('get cache get_blog_setting') return value else: from blog.models import BlogSettings diff --git a/accounts/models.py b/accounts/models.py index 1ce505973..1b4038419 100644 --- a/accounts/models.py +++ b/accounts/models.py @@ -1,7 +1,7 @@ from django.db import models from django.contrib.auth.models import AbstractUser, BaseUserManager from django.urls import reverse -from django.contrib.sites.models import Site +from DjangoBlog.utils import get_current_site from django.utils.timezone import now @@ -22,6 +22,6 @@ def __str__(self): return self.email def get_full_url(self): - site = Site.objects.get_current().domain + site = get_current_site().domain url = "https://{site}{path}".format(site=site, path=self.get_absolute_url()) return url diff --git a/accounts/tests.py b/accounts/tests.py index 29fe0285b..7fdeababd 100644 --- a/accounts/tests.py +++ b/accounts/tests.py @@ -1,7 +1,7 @@ from django.test import Client, RequestFactory, TestCase from blog.models import Article, Category, Tag from django.contrib.auth import get_user_model -from django.contrib.sites.models import Site +from DjangoBlog.utils import get_current_site import datetime from accounts.models import BlogUser from django.urls import reverse @@ -16,7 +16,7 @@ def setUp(self): self.factory = RequestFactory() def test_validate_account(self): - site = Site.objects.get_current().domain + site = get_current_site().domain user = BlogUser.objects.create_superuser(email="liangliangyy1@gmail.com", username="liangliangyy1", password="qwer!@#$ggg") testuser = BlogUser.objects.get(username='liangliangyy1') diff --git a/blog/admin.py b/blog/admin.py index e0db8fb72..30b5c3464 100644 --- a/blog/admin.py +++ b/blog/admin.py @@ -89,8 +89,8 @@ def get_view_on_site_url(self, obj=None): url = obj.get_full_url() return url else: - from django.contrib.sites.models import Site - site = Site.objects.get_current().domain + from DjangoBlog.utils import get_current_site + site = get_current_site().domain return site diff --git a/blog/context_processors.py b/blog/context_processors.py index 606ecd069..8cf77999b 100644 --- a/blog/context_processors.py +++ b/blog/context_processors.py @@ -25,7 +25,6 @@ def seo_processor(requests): key = 'seo_processor' value = cache.get(key) if value: - logger.info('get processor cache.') return value else: logger.info('set processor cache.') diff --git a/blog/management/commands/ping_baidu.py b/blog/management/commands/ping_baidu.py index 6ab51b74b..cbd398a5e 100644 --- a/blog/management/commands/ping_baidu.py +++ b/blog/management/commands/ping_baidu.py @@ -16,9 +16,9 @@ from django.core.management.base import BaseCommand, CommandError from blog.models import Article, Tag, Category from DjangoBlog.spider_notify import SpiderNotify -from django.contrib.sites.models import Site +from DjangoBlog.utils import get_current_site -site = Site.objects.get_current().domain +site = get_current_site().domain class Command(BaseCommand): diff --git a/blog/models.py b/blog/models.py index 9191d8322..a7e981d8d 100644 --- a/blog/models.py +++ b/blog/models.py @@ -7,7 +7,7 @@ from uuslug import slugify from django.core.exceptions import ValidationError from django.utils.translation import gettext_lazy as _ -from django.contrib.sites.models import Site +from DjangoBlog.utils import get_current_site from DjangoBlog.utils import cache_decorator, cache from django.utils.functional import cached_property from django.utils.timezone import now @@ -40,7 +40,7 @@ def save(self, *args, **kwargs): # article_save_signal.send(sender=self.__class__, is_update_views=is_update_views, id=self.id) def get_full_url(self): - site = Site.objects.get_current().domain + site = get_current_site().domain url = "https://{site}{path}".format(site=site, path=self.get_absolute_url()) return url diff --git a/blog/templatetags/blog_tags.py b/blog/templatetags/blog_tags.py index 63bc16902..f0072b0fa 100644 --- a/blog/templatetags/blog_tags.py +++ b/blog/templatetags/blog_tags.py @@ -29,7 +29,7 @@ from DjangoBlog.utils import cache_decorator, cache from django.contrib.auth import get_user_model from oauth.models import OAuthUser -from django.contrib.sites.models import Site +from DjangoBlog.utils import get_current_site import logging logger = logging.getLogger(__name__) @@ -96,7 +96,7 @@ def load_breadcrumb(article): names = article.get_category_tree() from DjangoBlog.utils import get_blog_setting blogsetting = get_blog_setting() - site = Site.objects.get_current().domain + site = get_current_site().domain names.append((blogsetting.sitename, '/')) names = names[::-1] diff --git a/blog/tests.py b/blog/tests.py index 1a7ce348f..0010e37d0 100644 --- a/blog/tests.py +++ b/blog/tests.py @@ -1,7 +1,7 @@ from django.test import Client, RequestFactory, TestCase from blog.models import Article, Category, Tag, SideBar from django.contrib.auth import get_user_model -from django.contrib.sites.models import Site +from DjangoBlog.utils import get_current_site from blog.forms import BlogSearchForm from django.core.paginator import Paginator from blog.templatetags.blog_tags import load_pagination_info, load_articletags @@ -21,7 +21,7 @@ def setUp(self): self.factory = RequestFactory() def test_validate_article(self): - site = Site.objects.get_current().domain + site = get_current_site().domain user = BlogUser.objects.get_or_create(email="liangliangyy@gmail.com", username="liangliangyy")[0] user.set_password("liangliangyy") user.is_staff = True diff --git a/comments/tests.py b/comments/tests.py index 9a758a5c4..36d16d457 100644 --- a/comments/tests.py +++ b/comments/tests.py @@ -1,7 +1,7 @@ from django.test import Client, RequestFactory, TestCase from blog.models import Article, Category, Tag from django.contrib.auth import get_user_model -from django.contrib.sites.models import Site +from DjangoBlog.utils import get_current_site from django.urls import reverse import datetime from accounts.models import BlogUser @@ -17,7 +17,7 @@ def setUp(self): self.factory = RequestFactory() def test_validate_comment(self): - site = Site.objects.get_current().domain + site = get_current_site().domain user = BlogUser.objects.create_superuser(email="liangliangyy1@gmail.com", username="liangliangyy1", password="liangliangyy1") diff --git a/comments/utils.py b/comments/utils.py index ca667f9bf..38375b008 100644 --- a/comments/utils.py +++ b/comments/utils.py @@ -14,14 +14,14 @@ """ from DjangoBlog.utils import send_email -from django.contrib.sites.models import Site +from DjangoBlog.utils import get_current_site import logging logger = logging.getLogger(__name__) def send_comment_email(comment): - site = Site.objects.get_current().domain + site = get_current_site().domain subject = '感谢您发表的评论' article_url = "https://{site}{path}".format(site=site, path=comment.article.get_absolute_url()) html_content = """ diff --git a/oauth/views.py b/oauth/views.py index d338480f9..2e99a3da7 100644 --- a/oauth/views.py +++ b/oauth/views.py @@ -12,7 +12,7 @@ from oauth.forms import RequireEmailForm from django.urls import reverse from DjangoBlog.utils import send_email, get_md5, save_user_avatar -from django.contrib.sites.models import Site +from DjangoBlog.utils import get_current_site from django.core.exceptions import ObjectDoesNotExist from django.http import HttpResponseForbidden from .oauthmanager import get_manager_by_type @@ -30,7 +30,7 @@ def get_redirecturl(request): return nexturl p = urlparse(nexturl) if p.netloc: - site = Site.objects.get_current().domain + site = get_current_site().domain if not p.netloc.replace('www.', '') == site.replace('www.', ''): logger.info('非法url:' + nexturl) return "/" @@ -127,7 +127,7 @@ def emailconfirm(request, id, sign): oauth_user_login_signal.send(sender=emailconfirm.__class__, id=oauthuser.id) login(request, author) - site = Site.objects.get_current().domain + site = get_current_site().domain content = '''

恭喜您,您已经成功绑定您的邮箱,您可以使用{type}来直接免密码登录本网站.欢迎您继续关注本站,地址是

@@ -181,7 +181,7 @@ def form_valid(self, form): oauthuser.email = email oauthuser.save() sign = get_md5(settings.SECRET_KEY + str(oauthuser.id) + settings.SECRET_KEY) - site = Site.objects.get_current().domain + site = get_current_site().domain if settings.DEBUG: site = '127.0.0.1:8000' path = reverse('oauth:email_confirm', kwargs={ diff --git a/servermanager/tests.py b/servermanager/tests.py index d7f58a79d..b993778e0 100644 --- a/servermanager/tests.py +++ b/servermanager/tests.py @@ -1,5 +1,5 @@ from django.test import Client, RequestFactory, TestCase -from django.contrib.sites.models import Site +from DjangoBlog.utils import get_current_site from .models import commands import datetime from accounts.models import BlogUser @@ -22,7 +22,7 @@ def test_tuling(self): self.assertIsNotNone(content) def test_validate_comment(self): - site = Site.objects.get_current().domain + site = get_current_site().domain user = BlogUser.objects.create_superuser(email="liangliangyy1@gmail.com", username="liangliangyy1", password="liangliangyy1") From 90ca3b854fe0f39fe9e39a6f49175f9cd3c05aec Mon Sep 17 00:00:00 2001 From: liangliangyy Date: Sat, 15 Dec 2018 00:18:01 +0800 Subject: [PATCH 040/593] update gitignore --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 1ebbd6c21..df0809fed 100644 --- a/.gitignore +++ b/.gitignore @@ -74,4 +74,5 @@ BingSiteAuth.xml cb9339dbe2ff86a5aa169d28dba5f615.txt werobot_session django.jpg -uploads/ \ No newline at end of file +uploads/ +settings_production.py From ba50aed47ac346225afd4f1a1de788133c5183b2 Mon Sep 17 00:00:00 2001 From: liangliangyy Date: Sat, 15 Dec 2018 00:47:55 +0800 Subject: [PATCH 041/593] update docker_start.sh --- bin/docker_start.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/docker_start.sh b/bin/docker_start.sh index 771572fa9..bdc918b46 100644 --- a/bin/docker_start.sh +++ b/bin/docker_start.sh @@ -4,7 +4,7 @@ DJANGODIR=/code/DjangoBlog # Django project directory USER=root # the user to run as GROUP=root # the group to run as NUM_WORKERS=1 # how many worker processes should Gunicorn spawn -DJANGO_SETTINGS_MODULE=DjangoBlog.settings # which settings file should Django use +#DJANGO_SETTINGS_MODULE=DjangoBlog.settings # which settings file should Django use DJANGO_WSGI_MODULE=DjangoBlog.wsgi # WSGI module name From 478719ce22bae3d90b1c9897f3f9a164af0374c3 Mon Sep 17 00:00:00 2001 From: liangliangyy Date: Sat, 15 Dec 2018 01:40:57 +0800 Subject: [PATCH 042/593] =?UTF-8?q?=E4=BF=AE=E5=A4=8Dtag=E5=88=86=E9=A1=B5?= =?UTF-8?q?url=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- blog/urls.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blog/urls.py b/blog/urls.py index f65993363..6a264981a 100644 --- a/blog/urls.py +++ b/blog/urls.py @@ -38,7 +38,7 @@ name='author_detail_page'), path(r'tag/.html', views.TagDetailView.as_view(), name='tag_detail'), - path(r'tag//).html', views.TagDetailView.as_view(), name='tag_detail_page'), + path(r'tag//.html', views.TagDetailView.as_view(), name='tag_detail_page'), path('archives.html', views.ArchivesView.as_view(), name='archives'), path(r'upload', views.fileupload, name='upload'), path(r'refresh', views.refresh_memcache, name='refresh') From 9d41401509f53720130a4730fd7b462c4ba04855 Mon Sep 17 00:00:00 2001 From: liangliangyy Date: Mon, 31 Dec 2018 00:52:31 +0800 Subject: [PATCH 043/593] close #196 --- DjangoBlog/blog_signals.py | 2 +- DjangoBlog/feeds.py | 2 +- DjangoBlog/sitemap.py | 2 +- DjangoBlog/spider_notify.py | 2 +- DjangoBlog/tests.py | 2 +- DjangoBlog/utils.py | 2 +- accounts/forms.py | 2 +- accounts/templatetags/__init__.py | 2 +- accounts/urls.py | 2 +- accounts/user_login_backend.py | 2 +- blog/context_processors.py | 2 +- blog/forms.py | 2 +- blog/management/commands/clear_cache.py | 2 +- blog/management/commands/create_testdata.py | 2 +- blog/management/commands/ping_baidu.py | 2 +- blog/middleware.py | 2 +- blog/search_indexes.py | 2 +- blog/templatetags/__init__.py | 2 +- blog/templatetags/blog_tags.py | 9 ++++----- blog/tests.py | 7 +++++-- blog/urls.py | 2 +- blog/views.py | 17 ++++++++++------- comments/forms.py | 2 +- comments/templatetags/__init__.py | 2 +- comments/templatetags/comments_tags.py | 2 +- comments/tests.py | 2 +- comments/urls.py | 2 +- oauth/forms.py | 2 +- oauth/oauthmanager.py | 2 +- oauth/templatetags/__init__.py | 2 +- oauth/templatetags/oauth_tags.py | 2 +- oauth/urls.py | 2 +- servermanager/Api/__init__.py | 2 +- servermanager/Api/blogapi.py | 2 +- servermanager/Api/commonapi.py | 2 +- servermanager/MemcacheStorage.py | 2 +- servermanager/robot.py | 2 +- servermanager/urls.py | 2 +- 38 files changed, 54 insertions(+), 49 deletions(-) diff --git a/DjangoBlog/blog_signals.py b/DjangoBlog/blog_signals.py index f05ee6f05..1abe76bd8 100644 --- a/DjangoBlog/blog_signals.py +++ b/DjangoBlog/blog_signals.py @@ -7,7 +7,7 @@ @author: liangliangyy @license: MIT Licence @contact: liangliangyy@gmail.com -@site: https://www.lylinux.org/ +@site: https://www.lylinux.net/ @software: PyCharm @file: blog_signals.py @time: 2017/8/12 上午10:18 diff --git a/DjangoBlog/feeds.py b/DjangoBlog/feeds.py index 37d64b0df..0d198bc58 100644 --- a/DjangoBlog/feeds.py +++ b/DjangoBlog/feeds.py @@ -7,7 +7,7 @@ @author: liangliangyy @license: MIT Licence @contact: liangliangyy@gmail.com -@site: https://www.lylinux.org/ +@site: https://www.lylinux.net/ @software: PyCharm @file: feed.py @time: 2016/12/22 下午10:16 diff --git a/DjangoBlog/sitemap.py b/DjangoBlog/sitemap.py index 72fcd047e..e4bcfeef4 100644 --- a/DjangoBlog/sitemap.py +++ b/DjangoBlog/sitemap.py @@ -7,7 +7,7 @@ @author: liangliangyy @license: MIT Licence @contact: liangliangyy@gmail.com -@site: https://www.lylinux.org/ +@site: https://www.lylinux.net/ @software: PyCharm @file: sitemap.py @time: 2016/12/15 下午10:26 diff --git a/DjangoBlog/spider_notify.py b/DjangoBlog/spider_notify.py index 45cf1c6e9..c13835cab 100644 --- a/DjangoBlog/spider_notify.py +++ b/DjangoBlog/spider_notify.py @@ -7,7 +7,7 @@ @author: liangliangyy @license: MIT Licence @contact: liangliangyy@gmail.com -@site: https://www.lylinux.org/ +@site: https://www.lylinux.net/ @software: PyCharm @file: spider_notify.py @time: 2017/1/15 下午1:41 diff --git a/DjangoBlog/tests.py b/DjangoBlog/tests.py index 09fb617b4..dfbaa20c2 100644 --- a/DjangoBlog/tests.py +++ b/DjangoBlog/tests.py @@ -36,7 +36,7 @@ def test_utils(self): import os ``` - [url](https://www.lylinux.org/) + [url](https://www.lylinux.net/) [ddd](http://www.baidu.com) diff --git a/DjangoBlog/utils.py b/DjangoBlog/utils.py index 6c6939ef4..fddc0e495 100644 --- a/DjangoBlog/utils.py +++ b/DjangoBlog/utils.py @@ -7,7 +7,7 @@ @author: liangliangyy @license: MIT Licence @contact: liangliangyy@gmail.com -@site: https://www.lylinux.org/ +@site: https://www.lylinux.net/ @software: PyCharm @file: utils.py @time: 2017/1/19 上午2:30 diff --git a/accounts/forms.py b/accounts/forms.py index bde2e3e11..a95e35341 100644 --- a/accounts/forms.py +++ b/accounts/forms.py @@ -7,7 +7,7 @@ @author: liangliangyy @license: MIT Licence @contact: liangliangyy@gmail.com -@site: https://www.lylinux.org/ +@site: https://www.lylinux.net/ @software: PyCharm @file: forms.py @time: 2016/11/20 下午3:16 diff --git a/accounts/templatetags/__init__.py b/accounts/templatetags/__init__.py index 09cf36980..88758021f 100644 --- a/accounts/templatetags/__init__.py +++ b/accounts/templatetags/__init__.py @@ -7,7 +7,7 @@ @author: liangliangyy @license: MIT Licence @contact: liangliangyy@gmail.com -@site: https://www.lylinux.org/ +@site: https://www.lylinux.net/ @software: PyCharm @file: __init__.py @time: 2016/11/2 下午9:15 diff --git a/accounts/urls.py b/accounts/urls.py index a648a764d..8f812e65e 100644 --- a/accounts/urls.py +++ b/accounts/urls.py @@ -7,7 +7,7 @@ @author: liangliangyy @license: MIT Licence @contact: liangliangyy@gmail.com -@site: https://www.lylinux.org/ +@site: https://www.lylinux.net/ @software: PyCharm @file: urls.py @time: 2016/11/20 下午3:52 diff --git a/accounts/user_login_backend.py b/accounts/user_login_backend.py index d2c785e5a..74b464a81 100644 --- a/accounts/user_login_backend.py +++ b/accounts/user_login_backend.py @@ -7,7 +7,7 @@ @author: liangliangyy @license: MIT Licence @contact: liangliangyy@gmail.com -@site: https://www.lylinux.org/ +@site: https://www.lylinux.net/ @software: PyCharm @file: user_login_backend.py @time: 2017/2/17 下午8:45 diff --git a/blog/context_processors.py b/blog/context_processors.py index 8cf77999b..845cf2035 100644 --- a/blog/context_processors.py +++ b/blog/context_processors.py @@ -7,7 +7,7 @@ @author: liangliangyy @license: MIT Licence @contact: liangliangyy@gmail.com -@site: https://www.lylinux.org/ +@site: https://www.lylinux.net/ @software: PyCharm @file: context_processors.py @time: 2016/11/6 下午4:23 diff --git a/blog/forms.py b/blog/forms.py index 04fa4cccb..3c09fc3ac 100644 --- a/blog/forms.py +++ b/blog/forms.py @@ -7,7 +7,7 @@ @author: liangliangyy @license: MIT Licence @contact: liangliangyy@gmail.com -@site: https://www.lylinux.org/ +@site: https://www.lylinux.net/ @software: PyCharm @file: forms.py @time: 2017/1/7 上午12:36 diff --git a/blog/management/commands/clear_cache.py b/blog/management/commands/clear_cache.py index db0dad20f..f941bf45f 100644 --- a/blog/management/commands/clear_cache.py +++ b/blog/management/commands/clear_cache.py @@ -7,7 +7,7 @@ @author: liangliangyy @license: MIT Licence @contact: liangliangyy@gmail.com -@site: https://www.lylinux.org/ +@site: https://www.lylinux.net/ @software: PyCharm @file: clear_cache.py @time: 2017/2/17 下午10:30 diff --git a/blog/management/commands/create_testdata.py b/blog/management/commands/create_testdata.py index 7479040bf..c77ef6a42 100644 --- a/blog/management/commands/create_testdata.py +++ b/blog/management/commands/create_testdata.py @@ -7,7 +7,7 @@ @author: liangliangyy @license: MIT Licence @contact: liangliangyy@gmail.com -@site: https://www.lylinux.org/ +@site: https://www.lylinux.net/ @software: PyCharm @file: create_testdata.py @time: 2017/3/11 上午1:58 diff --git a/blog/management/commands/ping_baidu.py b/blog/management/commands/ping_baidu.py index cbd398a5e..e97bd0a7d 100644 --- a/blog/management/commands/ping_baidu.py +++ b/blog/management/commands/ping_baidu.py @@ -7,7 +7,7 @@ @author: liangliangyy @license: MIT Licence @contact: liangliangyy@gmail.com -@site: https://www.lylinux.org/ +@site: https://www.lylinux.net/ @software: PyCharm @file: ping_baidu.py @time: 2017/1/17 下午15:29 diff --git a/blog/middleware.py b/blog/middleware.py index 00e044571..3241fc42b 100644 --- a/blog/middleware.py +++ b/blog/middleware.py @@ -7,7 +7,7 @@ @author: liangliangyy @license: MIT Licence @contact: liangliangyy@gmail.com -@site: https://www.lylinux.org/ +@site: https://www.lylinux.net/ @software: PyCharm @file: middleware.py @time: 2017/1/19 上午12:36 diff --git a/blog/search_indexes.py b/blog/search_indexes.py index 2cf3a676f..9d0268e18 100644 --- a/blog/search_indexes.py +++ b/blog/search_indexes.py @@ -7,7 +7,7 @@ @author: liangliangyy @license: MIT Licence @contact: liangliangyy@gmail.com -@site: https://www.lylinux.org/ +@site: https://www.lylinux.net/ @software: PyCharm @file: search_indexes.py @time: 2017/1/7 上午12:44 diff --git a/blog/templatetags/__init__.py b/blog/templatetags/__init__.py index 09cf36980..88758021f 100644 --- a/blog/templatetags/__init__.py +++ b/blog/templatetags/__init__.py @@ -7,7 +7,7 @@ @author: liangliangyy @license: MIT Licence @contact: liangliangyy@gmail.com -@site: https://www.lylinux.org/ +@site: https://www.lylinux.net/ @software: PyCharm @file: __init__.py @time: 2016/11/2 下午9:15 diff --git a/blog/templatetags/blog_tags.py b/blog/templatetags/blog_tags.py index f0072b0fa..d29e032be 100644 --- a/blog/templatetags/blog_tags.py +++ b/blog/templatetags/blog_tags.py @@ -7,7 +7,7 @@ @author: liangliangyy @license: MIT Licence @contact: liangliangyy@gmail.com -@site: https://www.lylinux.org/ +@site: https://www.lylinux.net/ @software: PyCharm @file: blog_tags.py @time: 2016/11/2 下午11:10 @@ -142,7 +142,6 @@ def load_sidebar(user, linktype): dates = Article.objects.datetimes('created_time', 'month', order='DESC') links = Links.objects.filter(is_enable=True).filter(Q(show_type=str(linktype)) | Q(show_type='a')) commment_list = Comment.objects.filter(is_enable=True).order_by('-id')[:blogsetting.sidebar_comment_count] - # show_adsense = settings.SHOW_GOOGLE_ADSENSE # 标签云 计算字体大小 # 根据总数计算出平均值 大小为 (数目/平均值)*步长 increment = 5 @@ -151,8 +150,10 @@ def load_sidebar(user, linktype): if tags and len(tags) > 0: s = list(map(lambda t: (t, t.get_article_count()), tags)) count = sum(map(lambda t: t[1], s)) - dd = 1 if count == 0 and not len(tags) else count / len(tags) + dd = 1 if (count == 0 and not len(tags)) else count / len(tags) + import random sidebar_tags = list(map(lambda x: (x[0], x[1], (x[1] / dd) * increment + 10), s)) + random.shuffle(sidebar_tags) return { 'recent_articles': recent_articles, @@ -282,8 +283,6 @@ def gravatar_url(email, size=40): return url -# return an image tag with the gravatar -# TEMPLATE USE: {{ email|gravatar:150 }} @register.filter def gravatar(email, size=40): """获得gravatar头像""" diff --git a/blog/tests.py b/blog/tests.py index 0010e37d0..ac48b315f 100644 --- a/blog/tests.py +++ b/blog/tests.py @@ -1,7 +1,7 @@ from django.test import Client, RequestFactory, TestCase from blog.models import Article, Category, Tag, SideBar from django.contrib.auth import get_user_model -from DjangoBlog.utils import get_current_site +from DjangoBlog.utils import get_current_site, get_md5 from blog.forms import BlogSearchForm from django.core.paginator import Paginator from blog.templatetags.blog_tags import load_pagination_info, load_articletags @@ -156,10 +156,13 @@ def test_image(self): imagepath = os.path.join(settings.BASE_DIR, 'python.png') with open(imagepath, 'wb') as file: file.write(rsp.content) + rsp = self.client.post('/upload') + self.assertEqual(rsp.status_code, 403) + sign = get_md5(get_md5(settings.SECRET_KEY)) with open(imagepath, 'rb') as file: imgfile = SimpleUploadedFile('python.png', file.read(), content_type='image/jpg') form_data = {'python.png': imgfile} - rsp = self.client.post('/upload', form_data, follow=True) + rsp = self.client.post('/upload?sign=' + sign, form_data, follow=True) self.assertEqual(rsp.status_code, 200) from DjangoBlog.utils import save_user_avatar, send_email diff --git a/blog/urls.py b/blog/urls.py index 6a264981a..5cbefa3da 100644 --- a/blog/urls.py +++ b/blog/urls.py @@ -7,7 +7,7 @@ @author: liangliangyy @license: MIT Licence @contact: liangliangyy@gmail.com -@site: https://www.lylinux.org/ +@site: https://www.lylinux.net/ @software: PyCharm @file: urls.py @time: 2016/11/2 下午7:15 diff --git a/blog/views.py b/blog/views.py index ce3b4afcf..e6a7c9004 100644 --- a/blog/views.py +++ b/blog/views.py @@ -7,11 +7,10 @@ from django.views.generic.detail import DetailView from django.conf import settings from django import forms -from django.http import HttpResponse -from django.http import HttpResponseRedirect +from django.http import HttpResponse, HttpResponseRedirect, HttpResponseForbidden from django.views.decorators.csrf import csrf_exempt from django.contrib.auth.decorators import login_required -from DjangoBlog.utils import cache +from DjangoBlog.utils import cache, get_md5, get_blog_setting from django.shortcuts import get_object_or_404 from blog.models import Article, Category, Tag from comments.forms import CommentForm @@ -233,16 +232,21 @@ def get_queryset_cache_key(self): @csrf_exempt def fileupload(request): if request.method == 'POST': + sign = request.GET.get('sign', None) + if not sign: + return HttpResponseForbidden() + if not sign == get_md5(get_md5(settings.SECRET_KEY)): + return HttpResponseForbidden() response = [] for filename in request.FILES: timestr = datetime.datetime.now().strftime('%Y/%m/%d') imgextensions = ['jpg', 'png', 'jpeg', 'bmp'] fname = u''.join(str(filename)) - isimage = len([i for i in imgextensions if fname.find(i) >= 0]) > 0 + blogsetting = get_blog_setting() - basepath = r'/var/www/resource/{type}/{timestr}'.format( - type='files' if not isimage else 'image', timestr=timestr) + basepath = r'{basedir}/{type}/{timestr}'.format(basedir=blogsetting.resource_path, + type='files' if not isimage else 'image', timestr=timestr) if settings.TESTING: basepath = settings.BASE_DIR + '/uploads' url = 'https://resource.lylinux.net/{type}/{timestr}/{filename}'.format( @@ -274,7 +278,6 @@ def refresh_memcache(request): cache.clear() return HttpResponse("ok") else: - from django.http import HttpResponseForbidden return HttpResponseForbidden() except Exception as e: logger.error(e) diff --git a/comments/forms.py b/comments/forms.py index 3cf8fda20..74751f178 100644 --- a/comments/forms.py +++ b/comments/forms.py @@ -7,7 +7,7 @@ @author: liangliangyy @license: MIT Licence @contact: liangliangyy@gmail.com -@site: https://www.lylinux.org/ +@site: https://www.lylinux.net/ @software: PyCharm @file: forms.py @time: 2016/11/12 下午2:45 diff --git a/comments/templatetags/__init__.py b/comments/templatetags/__init__.py index 09cf36980..88758021f 100644 --- a/comments/templatetags/__init__.py +++ b/comments/templatetags/__init__.py @@ -7,7 +7,7 @@ @author: liangliangyy @license: MIT Licence @contact: liangliangyy@gmail.com -@site: https://www.lylinux.org/ +@site: https://www.lylinux.net/ @software: PyCharm @file: __init__.py @time: 2016/11/2 下午9:15 diff --git a/comments/templatetags/comments_tags.py b/comments/templatetags/comments_tags.py index a78e8b6c9..e0fb2c23a 100644 --- a/comments/templatetags/comments_tags.py +++ b/comments/templatetags/comments_tags.py @@ -7,7 +7,7 @@ @author: liangliangyy @license: MIT Licence @contact: liangliangyy@gmail.com -@site: https://www.lylinux.org/ +@site: https://www.lylinux.net/ @software: PyCharm @file: comments_tags.py @time: 2016/11/2 下午9:17 diff --git a/comments/tests.py b/comments/tests.py index 36d16d457..baeee5374 100644 --- a/comments/tests.py +++ b/comments/tests.py @@ -72,7 +72,7 @@ def test_validate_comment(self): import os ``` - [url](https://www.lylinux.org/) + [url](https://www.lylinux.net/) [ddd](http://www.baidu.com) diff --git a/comments/urls.py b/comments/urls.py index 0bcb84792..0579ccc56 100644 --- a/comments/urls.py +++ b/comments/urls.py @@ -7,7 +7,7 @@ @author: liangliangyy @license: MIT Licence @contact: liangliangyy@gmail.com -@site: https://www.lylinux.org/ +@site: https://www.lylinux.net/ @software: PyCharm @file: urls.py @time: 2016/11/12 下午3:03 diff --git a/oauth/forms.py b/oauth/forms.py index 86851f85b..ccf8494cf 100644 --- a/oauth/forms.py +++ b/oauth/forms.py @@ -7,7 +7,7 @@ @author: liangliangyy @license: MIT Licence @contact: liangliangyy@gmail.com -@site: https://www.lylinux.org/ +@site: https://www.lylinux.net/ @software: PyCharm @file: forms.py @time: 2017/3/7 下午8:58 diff --git a/oauth/oauthmanager.py b/oauth/oauthmanager.py index b3a4791ec..3d2523552 100644 --- a/oauth/oauthmanager.py +++ b/oauth/oauthmanager.py @@ -7,7 +7,7 @@ @author: liangliangyy @license: MIT Licence @contact: liangliangyy@gmail.com -@site: https://www.lylinux.org/ +@site: https://www.lylinux.net/ @software: PyCharm @file: oauthmanager.py @time: 2016/11/26 下午5:09 diff --git a/oauth/templatetags/__init__.py b/oauth/templatetags/__init__.py index a8d9fd40b..fd534e99c 100644 --- a/oauth/templatetags/__init__.py +++ b/oauth/templatetags/__init__.py @@ -7,7 +7,7 @@ @author: liangliangyy @license: MIT Licence @contact: liangliangyy@gmail.com -@site: https://www.lylinux.org/ +@site: https://www.lylinux.net/ @software: PyCharm @file: __init__.py @time: 2017/3/4 下午3:22 diff --git a/oauth/templatetags/oauth_tags.py b/oauth/templatetags/oauth_tags.py index c336b0372..042fee0da 100644 --- a/oauth/templatetags/oauth_tags.py +++ b/oauth/templatetags/oauth_tags.py @@ -7,7 +7,7 @@ @author: liangliangyy @license: MIT Licence @contact: liangliangyy@gmail.com -@site: https://www.lylinux.org/ +@site: https://www.lylinux.net/ @software: PyCharm @file: oauth_tags.py @time: 2017/3/4 下午3:22 diff --git a/oauth/urls.py b/oauth/urls.py index 187ed8dbd..9afa523c0 100644 --- a/oauth/urls.py +++ b/oauth/urls.py @@ -7,7 +7,7 @@ @author: liangliangyy @license: MIT Licence @contact: liangliangyy@gmail.com -@site: https://www.lylinux.org/ +@site: https://www.lylinux.net/ @software: PyCharm @file: urls.py @time: 2016/11/26 下午5:25 diff --git a/servermanager/Api/__init__.py b/servermanager/Api/__init__.py index afd272508..667e57ebc 100644 --- a/servermanager/Api/__init__.py +++ b/servermanager/Api/__init__.py @@ -7,7 +7,7 @@ @author: liangliangyy @license: MIT Licence @contact: liangliangyy@gmail.com -@site: https://www.lylinux.org/ +@site: https://www.lylinux.net/ @software: PyCharm @file: __init__.py.py @time: 2017/8/27 上午11:40 diff --git a/servermanager/Api/blogapi.py b/servermanager/Api/blogapi.py index 8f2cc895d..b1ba05b7b 100644 --- a/servermanager/Api/blogapi.py +++ b/servermanager/Api/blogapi.py @@ -7,7 +7,7 @@ @author: liangliangyy @license: MIT Licence @contact: liangliangyy@gmail.com -@site: https://www.lylinux.org/ +@site: https://www.lylinux.net/ @software: PyCharm @file: blogapi.py @time: 2017/8/27 上午11:40 diff --git a/servermanager/Api/commonapi.py b/servermanager/Api/commonapi.py index 56e2fd6d5..bd559238e 100644 --- a/servermanager/Api/commonapi.py +++ b/servermanager/Api/commonapi.py @@ -7,7 +7,7 @@ @author: liangliangyy @license: MIT Licence @contact: liangliangyy@gmail.com -@site: https://www.lylinux.org/ +@site: https://www.lylinux.net/ @software: PyCharm @file: commonapi.py @time: 2017/9/2 上午1:43 diff --git a/servermanager/MemcacheStorage.py b/servermanager/MemcacheStorage.py index 33d462170..e0e233a2b 100644 --- a/servermanager/MemcacheStorage.py +++ b/servermanager/MemcacheStorage.py @@ -7,7 +7,7 @@ @author: liangliangyy @license: MIT Licence @contact: liangliangyy@gmail.com -@site: https://www.lylinux.org/ +@site: https://www.lylinux.net/ @software: PyCharm @file: MemcacheStorage.py @time: 2017/8/27 上午2:42 diff --git a/servermanager/robot.py b/servermanager/robot.py index e91fbb16a..8834a42e1 100644 --- a/servermanager/robot.py +++ b/servermanager/robot.py @@ -7,7 +7,7 @@ @author: liangliangyy @license: MIT Licence @contact: liangliangyy@gmail.com -@site: https://www.lylinux.org/ +@site: https://www.lylinux.net/ @software: PyCharm @file: robot.py @time: 2017/8/27 上午1:55 diff --git a/servermanager/urls.py b/servermanager/urls.py index d9775ab80..a5a075ca2 100644 --- a/servermanager/urls.py +++ b/servermanager/urls.py @@ -7,7 +7,7 @@ @author: liangliangyy @license: MIT Licence @contact: liangliangyy@gmail.com -@site: https://www.lylinux.org/ +@site: https://www.lylinux.net/ @software: PyCharm @file: urls.py @time: 2017/8/27 上午2:27 From 2d7d7e6cad3057a0c2d07cbe8c82fb375ac849cc Mon Sep 17 00:00:00 2001 From: liangliangyy Date: Sun, 13 Jan 2019 15:11:03 +0800 Subject: [PATCH 044/593] =?UTF-8?q?=E5=8D=87=E7=BA=A7=E4=BE=9D=E8=B5=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- requirements.txt | 24 ++++++++++++------------ travis_test/requirements.txt | 24 ++++++++++++------------ 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/requirements.txt b/requirements.txt index 20297a177..87c7ae367 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,13 +1,13 @@ appdirs==1.4.3 asn1crypto==0.24.0 astroid==2.1.0 -bottle==0.12.15 +bottle==0.12.16 certifi==2018.11.29 cffi==1.11.5 chardet==3.0.4 coverage==4.5.2 cryptography==2.4.2 -Django==2.1.4 +Django==2.1.5 django-appconf==1.0.2 django-autoslug==1.9.3 django-compressor==2.2 @@ -26,25 +26,25 @@ mccabe==0.6.1 mistune==0.8.4 olefile==0.46 packaging==18.0 -Pillow==5.3.0 +Pillow==5.4.1 pycparser==2.19 -Pygments==2.3.0 +Pygments==2.3.1 pylint==2.2.2 -PyMySQL==0.9.2 +PyMySQL==0.9.3 pyparsing==2.3.0 python-memcached==1.59 -python-slugify==1.2.6 -pytz==2018.7 -raven==6.9.0 +python-slugify==2.0.1 +pytz==2018.9 +raven==6.10.0 rcssmin==1.0.6 -requests==2.20.1 +requests==2.21.0 rjsmin==1.0.12 -six==1.11.0 +six==1.12.0 sqlparse==0.2.4 Unidecode==1.0.23 urllib3==1.24.1 webencodings==0.5.1 -WeRoBot==1.6.0 +WeRoBot==1.7.0 Whoosh==2.7.4 -wrapt==1.10.11 +wrapt==1.11.0 xmltodict==0.11.0 diff --git a/travis_test/requirements.txt b/travis_test/requirements.txt index 517ed450c..b9afd0848 100644 --- a/travis_test/requirements.txt +++ b/travis_test/requirements.txt @@ -1,13 +1,13 @@ appdirs==1.4.3 asn1crypto==0.24.0 astroid==2.1.0 -bottle==0.12.15 +bottle==0.12.16 certifi==2018.11.29 cffi==1.11.5 chardet==3.0.4 coverage==4.5.2 cryptography==2.4.2 -Django==2.1.4 +Django==2.1.5 django-appconf==1.0.2 django-autoslug==1.9.3 django-compressor==2.2 @@ -26,24 +26,24 @@ mccabe==0.6.1 mistune==0.8.4 olefile==0.46 packaging==18.0 -Pillow==5.3.0 +Pillow==5.4.1 pycparser==2.19 -Pygments==2.3.0 +Pygments==2.3.1 pylint==2.2.2 -PyMySQL==0.9.2 +PyMySQL==0.9.3 pyparsing==2.3.0 -python-slugify==1.2.6 -pytz==2018.7 -raven==6.9.0 +python-slugify==2.0.1 +pytz==2018.9 +raven==6.10.0 rcssmin==1.0.6 -requests==2.20.1 +requests==2.21.0 rjsmin==1.0.12 -six==1.11.0 +six==1.12.0 sqlparse==0.2.4 Unidecode==1.0.23 urllib3==1.24.1 webencodings==0.5.1 -WeRoBot==1.6.0 +WeRoBot==1.7.0 Whoosh==2.7.4 -wrapt==1.10.11 +wrapt==1.11.0 xmltodict==0.11.0 \ No newline at end of file From b4e766149ebd032da5f3919a61e35f48059a9859 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=94=E5=90=AC=E9=A3=8E=E5=90=9F?= Date: Fri, 25 Jan 2019 14:04:45 +0800 Subject: [PATCH 045/593] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index babd74a6c..d1ae0c155 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ 基于`python3.6`和`Django2.1`的博客。 -[![Build Status](https://travis-ci.org/liangliangyy/DjangoBlog.svg?branch=master)](https://travis-ci.org/liangliangyy/DjangoBlog) [![codecov](https://codecov.io/gh/liangliangyy/DjangoBlog/branch/master/graph/badge.svg)](https://codecov.io/gh/liangliangyy/DjangoBlog) [![Requirements Status](https://requires.io/github/liangliangyy/DjangoBlog/requirements.svg?branch=master)](https://requires.io/github/liangliangyy/DjangoBlog/requirements/?branch=master) [![license](https://img.shields.io/github/license/liangliangyy/djangoblog.svg)]() [![GitHub release](https://img.shields.io/github/release/liangliangyy/djangoblog.svg)]() [![python3.5](https://img.shields.io/badge/python-3.5-brightgreen.svg)]() [![django1.10](https://img.shields.io/badge/django-2.0-brightgreen.svg)]() +[![Build Status](https://travis-ci.org/liangliangyy/DjangoBlog.svg?branch=master)](https://travis-ci.org/liangliangyy/DjangoBlog) [![codecov](https://codecov.io/gh/liangliangyy/DjangoBlog/branch/master/graph/badge.svg)](https://codecov.io/gh/liangliangyy/DjangoBlog) [![Requirements Status](https://requires.io/github/liangliangyy/DjangoBlog/requirements.svg?branch=master)](https://requires.io/github/liangliangyy/DjangoBlog/requirements/?branch=master) [![license](https://img.shields.io/github/license/liangliangyy/djangoblog.svg)]() ## 主要功能: - 文章,页面,分类目录,标签的添加,删除,编辑等。文章及页面支持`Markdown`,支持代码高亮。 From 3071aeb5a513eebecef8a0e5c12bdfea97760375 Mon Sep 17 00:00:00 2001 From: liangliangyy Date: Tue, 5 Feb 2019 16:08:03 +0800 Subject: [PATCH 046/593] =?UTF-8?q?=E5=8D=87=E7=BA=A7=E4=BE=9D=E8=B5=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DjangoBlog/utils.py | 25 ++++++++++++++++++++ blog/views.py | 46 ++++++++++++++++++++++++++---------- requirements.txt | 12 +++++----- travis_test/requirements.txt | 12 +++++----- 4 files changed, 70 insertions(+), 25 deletions(-) diff --git a/DjangoBlog/utils.py b/DjangoBlog/utils.py index fddc0e495..619db638a 100644 --- a/DjangoBlog/utils.py +++ b/DjangoBlog/utils.py @@ -70,6 +70,14 @@ def news(*args, **kwargs): def expire_view_cache(path, servername, serverport, key_prefix=None): + ''' + 刷新视图缓存 + :param path:url路径 + :param servername:host + :param serverport:端口 + :param key_prefix:前缀 + :return:是否成功 + ''' from django.http import HttpRequest from django.utils.cache import get_cache_key @@ -87,6 +95,14 @@ def expire_view_cache(path, servername, serverport, key_prefix=None): def block_code(text, lang, inlinestyles=False, linenos=False): + ''' + markdown代码高亮 + :param text: + :param lang: + :param inlinestyles: + :param linenos: + :return: + ''' if not lang: text = text.strip() return u'
%s
\n' % mistune.escape(text) @@ -113,6 +129,10 @@ def get_current_site(): class BlogMarkDownRenderer(mistune.Renderer): + ''' + markdown渲染 + ''' + def block_code(self, text, lang=None): # renderer has an options inlinestyles = self.options.get('inlinestyles') @@ -191,6 +211,11 @@ def get_blog_setting(): def save_user_avatar(url): + ''' + 保存用户头像 + :param url:头像url + :return: 本地路径 + ''' setting = get_blog_setting() logger.info(url) try: diff --git a/blog/views.py b/blog/views.py index e6a7c9004..b4205a9e9 100644 --- a/blog/views.py +++ b/blog/views.py @@ -54,7 +54,11 @@ def get_queryset_data(self): raise NotImplementedError() def get_queryset_from_cache(self, cache_key): - # raise NotImplementedError() + ''' + 缓存页面数据 + :param cache_key: 缓存key + :return: + ''' value = cache.get(cache_key) if value: logger.info('get view cache.key:{key}'.format(key=cache_key)) @@ -66,6 +70,10 @@ def get_queryset_from_cache(self, cache_key): return article_list def get_queryset(self): + ''' + 重写默认,从缓存获取数据 + :return: + ''' key = self.get_queryset_cache_key() value = self.get_queryset_from_cache(key) return value @@ -76,6 +84,9 @@ def get_context_data(self, **kwargs): class IndexView(ArticleListView): + ''' + 首页 + ''' link_type = 'i' def get_queryset_data(self): @@ -88,6 +99,9 @@ def get_queryset_cache_key(self): class ArticleDetailView(DetailView): + ''' + 文章详情页面 + ''' template_name = 'blog/article_detail.html' model = Article pk_url_kwarg = 'article_id' @@ -103,7 +117,7 @@ def get_context_data(self, **kwargs): articleid = int(self.kwargs[self.pk_url_kwarg]) comment_form = CommentForm() user = self.request.user - + # 如果用户已经登录,则隐藏邮件和用户名输入框 if user.is_authenticated and not user.is_anonymous and user.email and user.username: comment_form.fields.update({ 'email': forms.CharField(widget=forms.HiddenInput()), @@ -125,6 +139,9 @@ def get_context_data(self, **kwargs): class CategoryDetailView(ArticleListView): + ''' + 分类目录列表 + ''' page_type = "分类目录归档" def get_queryset_data(self): @@ -158,6 +175,9 @@ def get_context_data(self, **kwargs): class AuthorDetailView(ArticleListView): + ''' + 作者详情页 + ''' page_type = '作者文章归档' def get_queryset_cache_key(self): @@ -177,18 +197,10 @@ def get_context_data(self, **kwargs): return super(AuthorDetailView, self).get_context_data(**kwargs) -class TagListView(ListView): - template_name = '' - context_object_name = 'tag_list' - - def get_queryset(self): - tags_list = [] - tags = Tag.objects.all() - for t in tags: - t.article_set.count() - - class TagDetailView(ArticleListView): + ''' + 标签列表页面 + ''' page_type = '分类标签归档' def get_queryset_data(self): @@ -216,6 +228,9 @@ def get_context_data(self, **kwargs): class ArchivesView(ArticleListView): + ''' + 文章归档页面 + ''' page_type = '文章归档' paginate_by = None page_kwarg = None @@ -231,6 +246,11 @@ def get_queryset_cache_key(self): @csrf_exempt def fileupload(request): + ''' + 该方法需自己写调用端来上传图片,该方法仅提供图床功能 + :param request: + :return: + ''' if request.method == 'POST': sign = request.GET.get('sign', None) if not sign: diff --git a/requirements.txt b/requirements.txt index 87c7ae367..6d0684a63 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,10 +6,10 @@ certifi==2018.11.29 cffi==1.11.5 chardet==3.0.4 coverage==4.5.2 -cryptography==2.4.2 +cryptography==2.5 Django==2.1.5 django-appconf==1.0.2 -django-autoslug==1.9.3 +django-autoslug==1.9.4 django-compressor==2.2 django-debug-toolbar==1.11 django-haystack==2.8.1 @@ -19,19 +19,19 @@ django-uuslug==1.1.8 idna==2.8 isort==4.3.4 jieba==0.39 -jsonpickle==1.0 +jsonpickle==1.1 lazy-object-proxy==1.3.1 markdown2==2.3.7 mccabe==0.6.1 mistune==0.8.4 olefile==0.46 -packaging==18.0 +packaging==19.0 Pillow==5.4.1 pycparser==2.19 Pygments==2.3.1 pylint==2.2.2 PyMySQL==0.9.3 -pyparsing==2.3.0 +pyparsing==2.3.1 python-memcached==1.59 python-slugify==2.0.1 pytz==2018.9 @@ -46,5 +46,5 @@ urllib3==1.24.1 webencodings==0.5.1 WeRoBot==1.7.0 Whoosh==2.7.4 -wrapt==1.11.0 +wrapt==1.11.1 xmltodict==0.11.0 diff --git a/travis_test/requirements.txt b/travis_test/requirements.txt index b9afd0848..31e3b9d2a 100644 --- a/travis_test/requirements.txt +++ b/travis_test/requirements.txt @@ -6,10 +6,10 @@ certifi==2018.11.29 cffi==1.11.5 chardet==3.0.4 coverage==4.5.2 -cryptography==2.4.2 +cryptography==2.5 Django==2.1.5 django-appconf==1.0.2 -django-autoslug==1.9.3 +django-autoslug==1.9.4 django-compressor==2.2 django-debug-toolbar==1.11 django-haystack==2.8.1 @@ -19,19 +19,19 @@ django-uuslug==1.1.8 idna==2.8 isort==4.3.4 jieba==0.39 -jsonpickle==1.0 +jsonpickle==1.1 lazy-object-proxy==1.3.1 markdown2==2.3.7 mccabe==0.6.1 mistune==0.8.4 olefile==0.46 -packaging==18.0 +packaging==19.0 Pillow==5.4.1 pycparser==2.19 Pygments==2.3.1 pylint==2.2.2 PyMySQL==0.9.3 -pyparsing==2.3.0 +pyparsing==2.3.1 python-slugify==2.0.1 pytz==2018.9 raven==6.10.0 @@ -45,5 +45,5 @@ urllib3==1.24.1 webencodings==0.5.1 WeRoBot==1.7.0 Whoosh==2.7.4 -wrapt==1.11.0 +wrapt==1.11.1 xmltodict==0.11.0 \ No newline at end of file From fb904762747c3355fa10a0c4901e4aca8a74ad0a Mon Sep 17 00:00:00 2001 From: liangliangyy Date: Tue, 5 Feb 2019 22:02:07 +0800 Subject: [PATCH 047/593] update css url --- templates/share_layout/base.html | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/templates/share_layout/base.html b/templates/share_layout/base.html index 5d5164d09..096faab3f 100644 --- a/templates/share_layout/base.html +++ b/templates/share_layout/base.html @@ -22,12 +22,14 @@ - - {% compress css %} {{ SITE_DESCRIPTION }}

{% block footer %} {% endblock %} - \ No newline at end of file + From 329cbc4f352ed5f924c2ef2c56f343049ab54ef8 Mon Sep 17 00:00:00 2001 From: liangliangyy Date: Wed, 6 Feb 2019 11:28:04 +0800 Subject: [PATCH 048/593] update font css --- .../blog/fonts/Open+Sans_400_italic.ttf | Bin 0 -> 26940 bytes .../blog/fonts/Open+Sans_400_italic.woff | Bin 0 -> 22848 bytes .../blog/fonts/Open+Sans_400_italic.woff2 | Bin 0 -> 18072 bytes .../blog/fonts/Open+Sans_400_normal.eot | Bin 0 -> 20486 bytes .../blog/fonts/Open+Sans_400_normal.svg | 336 ++++++++++++++++++ .../blog/fonts/Open+Sans_400_normal.ttf | Bin 0 -> 25520 bytes .../blog/fonts/Open+Sans_400_normal.woff | Bin 0 -> 23708 bytes .../blog/fonts/Open+Sans_400_normal.woff2 | Bin 0 -> 18876 bytes .../blog/fonts/Open+Sans_700_italic.ttf | Bin 0 -> 26808 bytes .../blog/fonts/Open+Sans_700_italic.woff | Bin 0 -> 22812 bytes .../blog/fonts/Open+Sans_700_italic.woff2 | Bin 0 -> 17952 bytes .../blog/fonts/Open+Sans_700_normal.ttf | Bin 0 -> 25720 bytes .../blog/fonts/Open+Sans_700_normal.woff | Bin 0 -> 24528 bytes .../blog/fonts/Open+Sans_700_normal.woff2 | Bin 0 -> 19504 bytes blog/static/blog/fonts/fonts.css | 48 +++ templates/share_layout/base.html | 9 +- 16 files changed, 387 insertions(+), 6 deletions(-) create mode 100644 blog/static/blog/fonts/Open+Sans_400_italic.ttf create mode 100644 blog/static/blog/fonts/Open+Sans_400_italic.woff create mode 100644 blog/static/blog/fonts/Open+Sans_400_italic.woff2 create mode 100644 blog/static/blog/fonts/Open+Sans_400_normal.eot create mode 100644 blog/static/blog/fonts/Open+Sans_400_normal.svg create mode 100644 blog/static/blog/fonts/Open+Sans_400_normal.ttf create mode 100644 blog/static/blog/fonts/Open+Sans_400_normal.woff create mode 100644 blog/static/blog/fonts/Open+Sans_400_normal.woff2 create mode 100644 blog/static/blog/fonts/Open+Sans_700_italic.ttf create mode 100644 blog/static/blog/fonts/Open+Sans_700_italic.woff create mode 100644 blog/static/blog/fonts/Open+Sans_700_italic.woff2 create mode 100644 blog/static/blog/fonts/Open+Sans_700_normal.ttf create mode 100644 blog/static/blog/fonts/Open+Sans_700_normal.woff create mode 100644 blog/static/blog/fonts/Open+Sans_700_normal.woff2 create mode 100644 blog/static/blog/fonts/fonts.css diff --git a/blog/static/blog/fonts/Open+Sans_400_italic.ttf b/blog/static/blog/fonts/Open+Sans_400_italic.ttf new file mode 100644 index 0000000000000000000000000000000000000000..96c9249236c0f619b8adc769e4a29a6dbd7ef64d GIT binary patch literal 26940 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%0DTDWvgAD|yN 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 a&gJD;S`IwXG!36IG!>s?@dRbw!T%5cVl`d> literal 0 HcmV?d00001 diff --git a/blog/static/blog/fonts/Open+Sans_400_italic.woff b/blog/static/blog/fonts/Open+Sans_400_italic.woff new file mode 100644 index 0000000000000000000000000000000000000000..4e08d18f55c096e814c976d7877dc579202e67d1 GIT binary patch literal 22848 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≪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 literal 0 HcmV?d00001 diff --git a/blog/static/blog/fonts/Open+Sans_400_italic.woff2 b/blog/static/blog/fonts/Open+Sans_400_italic.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..3ca0fa8c4fb3c70c7f96f07353f5a42148cb3466 GIT binary patch literal 18072 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 literal 0 HcmV?d00001 diff --git a/blog/static/blog/fonts/Open+Sans_400_normal.svg b/blog/static/blog/fonts/Open+Sans_400_normal.svg new file mode 100644 index 000000000..78eb653a7 --- /dev/null +++ b/blog/static/blog/fonts/Open+Sans_400_normal.svg @@ -0,0 +1,336 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/blog/static/blog/fonts/Open+Sans_400_normal.ttf b/blog/static/blog/fonts/Open+Sans_400_normal.ttf new file mode 100644 index 0000000000000000000000000000000000000000..541a78f76c02bf794c841afe14c2550c16d07703 GIT binary patch literal 25520 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%0Dq6kiswT$Tbrm;Z{lX}xkv z0rRkO&Q434pWUSQvHeN2v2qs9O5&=W(1G}PYcFA3eMyXj_O*;VzWrq#ckQXCydTZq zqb=N0k&5x;UvLo-0M@gQdPIVm^%x1)$Bt|s$C1sC!~w`88crkM?l;imPzfbb+WhjJ+s&#eMLDTIClucIWwxnB7uVOFn$_?+AF{k^1YPvsv+EvCjnj zUgWRi5k+b~jb^(={%^=1!R;8<{4n;B$iIX9vD{*5%@1d4#8?8f=UsfXL_Yip!hZe^ zT3@2W4%`YIfo~UbO%YaIc{h4%%_lwa)j##mhNOcARDog~yllOf zVVPTNy;m@QKeFCyumk?ldaor0+JvV)qIL}=0-9+Ct^!*m~h-Rm1!!`>xnW=?^|#ho`6U|9eJorZH1w^ zmZp;b3p4L?0a`o|Yfm3iB>weAJ7?lbK@3A0TFd*Hfw$9vJw@~_AHVs?840glIEjRR zWh{O(9oDS!ArF`+mCfKgMx^;*m^8?3}_AuH_Q326^4+#XWL5&k*8 zQn+HrG zYsi=63)pTm$9;C2`CD!GlXU`~TKYC7Tqv*BHzNptaytbFnnxkZi5IY8bfhsa^_A*BC7NbbdCFF8Tp!*{g)qA${UbUyhHzPI%fEu{f1LoqD{`Mw7mS3oV+w$=!~S4yqs)ZlA3Bmlk?JMrD>By zq6^K*$jMHdrb|*&+whdEyp+Nj>C@9@+orVKE8(d*1xgBjv=J#uK&MSrlL+8WD$qq* zD@n6f5~)^_CX%+1D3_Fyl9pZ2Hm&(q7iq0JO-;3tD#kRC*haThZky3^uSD~PWZ=gx zIwfbuj3gDWT}G=1+n5%3+su}GB_=s3PszlOHme|OdTN?3%i7#5l^9uSOQfu+w!>SB z*-mY_w;R!_9=mCeJ!IsirDab~%1+Hn(T+??DJ)3SP8W&Y$kuSvTRmt;s(qd=5@jUs z+jRVBN2}$t)$*fT6|u{1^`ITCmd{pcmYu{u7@wCjGc!$&%+8S0vNLpJtRs_S?dlkH zWO78(ZcJt&K60N|IAeNJVL`i`)=%0vwGDY{8{%43YnRvRK^v#GAx}-ls-^NpVmr2l zbhh~|_jd8E5ZD#8deFwJXbME4#Pbm=;3GCpjF`4i9k_AUUKLt zQj_tbsJ2KXZN{f&rRAmNXXUpkYP!=-Q~`@c(k3ZAE30q+0sem4nQ8fH1xZR|VO|aw zAyHv{{j6y}8}wy*T7Ldidod)fYPW55@6hT#4|FIVhRm3`u??4Oh5&xzP ug>J-C{+(@SXjWawGq8%w(1u3SnIauQOMoYars8)rO~LO7l)wpLLjFI>Y(3fl literal 0 HcmV?d00001 diff --git a/blog/static/blog/fonts/Open+Sans_400_normal.woff b/blog/static/blog/fonts/Open+Sans_400_normal.woff new file mode 100644 index 0000000000000000000000000000000000000000..6ca02c4689a1c6dd31d8ce0ee0d477ac66b5f1cb GIT binary patch literal 23708 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 literal 0 HcmV?d00001 diff --git a/blog/static/blog/fonts/Open+Sans_400_normal.woff2 b/blog/static/blog/fonts/Open+Sans_400_normal.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..a337154f3a2c9c5b57d53bc0b0e79f6f7bbe335b GIT binary patch literal 18876 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�Z25KeiD>Z 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 literal 0 HcmV?d00001 diff --git a/blog/static/blog/fonts/Open+Sans_700_normal.ttf b/blog/static/blog/fonts/Open+Sans_700_normal.ttf new file mode 100644 index 0000000000000000000000000000000000000000..a704f9cce2772b41ecb8fe89f46eec99c9c2486b GIT binary patch literal 25720 zcma)l3qV!H_V>)*`|N{=$oq&dI3nO9L^wPyJ^>LC6#)@Zkwid1c?puQh>Xb06wQ1@ zW`s&+W@cVBGfhp+%*@Q|^^(`je5I+GnF(k3elz5= z>N@55X$z%bI-iO*>j_G)w}WhIU#CDL2*{*tt)IhA?*qX zk-HXV&MlEUNnUu*@p~6#7UuXoH{?e`(!NFBp_1aV*{9ZC$Ni+WgxI}Dcweb!W9{y} zPd?dk&~K!j9Wn?x9&M=O>Dj(to^5h4Z?<1*w-m7Lh{SX859REZnl}?-zqHAL&9mok z|MZWF9Q?jSJV+YRp`;TDB$dugeBnoOt7c9PWIPL*|v zBq?+5N-N1=DGp^4h$Hgiam~Y( zV7YvdX5)&(6>dqRIXo|m=QQhu{;2u@GF?}Vo=zoube?36?iSgjEWnkG=M!X(>P7ZQ z4J~=PMC4VIJ^CeNkGc?724Gc78i#jzBtxkuy;TRYjnhoO4>(iNYlf?y1W65~2JLk~ zYEq(!ogClNtTfWm%2|@Cb0T$0Hctg2mDFeFmJcYD8k_*vR;F0Y}Ag+PDFSVj+Y2;*9kgG8^aWg(1-&hF=l6fQI8X8M0PSrr zPJ1!7JVignSU;ZfzVfmEZz=eZk1_ARE(rB)ARTAF4148Gv+C21pg5c5RT@pw9rsIBiX zmYjBc{A^SGaWY4L2K~U?;{0t%vn=U21@n#b{C`a`7x{ePbJ8{y^AqFr|C{o8EanoQ z#~4TJeCBhC&uiNhbEOWvc*Hu^oR>L&S^L)7)<&{d5f|-(^fG9~HS%v}eb$K{=nITpT^wvaTtI?|h8YAs}7F-h0EVa~ac zT0)x7^Q*OuwlqVVIsd9bkZqPRfh^>*^(fM}k=EfdHci@d)@DQyLxx>?kccF2y7dG?Ol$oI9AuYQ} z_ma)rJ~uF}Qi8hwder%T8U`Z66thf+`4 znLa}^1rm3n{e^%Y&yvrnGiX~vm(nftD|(gwA*oVV=?Q6)R4TnCHOf>Dl4r`#$j1~} z>7|TPrYf_Q#mZrwpDs>Up?h2Rh3*I4@2a0VRGp;GRbNy;P*1BZ`kwlU`fB|l{aO7( zJ9oPTyCS-Y>l_a`p6%GH z{RYl<@BD@mrnPbdpQqw&UJp?xz>5F^Eb|axOlttcZqaKbXn^1 zn#(4a4_rQRIqveUOOvbK)z#I@wXbWqYnE%V>jKwBt}9*NbbZ(LOScYgo^HNwL2iTH zM!J=_?RPuw_O07Rx1Zd80|8Urr@0rp&vmbIU*=xx-r&B&eZL3waPTmA_;~d5i13K< zDDl|g@rlQAk8eFLdi-SQVF)lp7-9^GhRKFpLz$t{@RDJ*;Vr{^hK~$K3|||*Gh8>^ z?G(@{qEk$##7>ht<#sCTRM}}sr!}4GJ8kRqQKuuFzV7s0r|X^WcWUu$?`iOi^qlEA z$Fs__#jCXSK6S=kA?*cMk0w)p<G}Sx-9MTMc2+<{kjHq9o%(f*YRDab}i^Sr)yQ$ zWnF8#Hgw(5^^>ljcRkbfV%MA9^xZsH-udAD`lHh0_E?bB{w zcKc_yAG_V@*3@0!-KBf??tQw4b&u*k2{ZFsuL!Tgh<_u1yN*XO9uH$D%0+V$+zb6n3AJ&*MK z-PhB%uWz{TFyFDhlYMi2OMRd5eaUyVZ@usPz90J@^F8Bx(f6kB?|!nMqhDt~KWNMc zA%X3o&&otkh^x`r7>?_pJhFLro;1(ANVn@ze4cU@sx2vRxkAqf$c~tKKC+R9gqeap z+}%{49)7g$zKKp^^^;P^O-@fvNvBuj(~r)lq^2d0ojj43u9h3*Xlp%GMsZwpUGF2k zrBCpq+hx8hIq?gy0ep4R?Zh62?=tj8cfh)W0Z!%|_e}V0>ae4mE}vL@<)e&s8xL<9!_LISyvT|M$1J4B-#qAg^0+RE+VzcAX;|lk z-HTRy?DpC!`{bV@9ohL&MbGE<8Q}GYLE8UBME9GnSOtc*+*Thz=V?dWNGH;R(*m`b zfrf|Su!k@8p{{@Rf({G_o;o!+fPTDV>!zJ_;yZtrwl|->EBCE_JYOM| z70r(pR9wCG^Y`@l&;NWBuiMouX?a(!{d^I5P`OER%Pp0lMcqK5AVQ2re++m}P8UHP zjRo{HEZnF{dSBk8aH^^_#19R2cQb^Ase~;mJMhbW_QSlDqhf9x-h1(-TH19)2p!V% znZ)K#7v?;Z!*&iBMswpLhK+eLHGNj~_XidqOi6uX%G$l}t(bFUBD-0>XAx`3s!E+c zn8rUbReB+GaOA+W8U3e#IGnTSI>A|l^OfK!jh*=0WVTP5hqoQT-A*pyCRK8Gb20e% zNy)Fj@@VPul`lSASt|w6cJ!-l2U+m_-&puZTj)_-YLXM!30XI$`{q^j9Fr54tR2&7(OxJN6Yuh@^b_l*BC z`?Xg#?rr#)4VcUPRbIhN}ChUJyzrv5y!prQZ3v&;8@ zx<2U9EI|QR)av65nH@^OAVxkdeN;VFW^HHRn3qcnX}9gHhrMj)8^Z3>Kz5o2o|CsX z=RX{B%QZQH%|!dN(f&~2g2jk(aZW64_YMs)@)q+&K@Ja-rhnVSzG2tyuO9nN2JONc zL!a%nAYAq|Kj`8!SZ=s|>NoaqEN$N>^!f#NhiK^rd&Dm39nsb};7`;YgEWKA=ITU@ z!3K9e_^RF%CWHsU`XT8wQgM;t5l4$@QN@Uf6W%Dki9-RM)-C0e&4^%IhxnO?MoQ>#HxES%Bd?uJc7o$79Ecu!P z6;JayWjj?4HF5S8EsNJ32Z#0`{dn&cu!3HUpN)j;DRY^|2hv}4743se!;=*M4W&=N zw6TU=y?U43u3lMDL>1S$d1a-uD$o8fHginQw1jNk@dNcG?+;HqP`b16%a1D##gE-N z^NmAI`%n6f@|_;C@qw4o?KFqCu;8(8`neqCKEh5C2eJIUiI=U93g~ zBy;f<&CnX9#Isjf&2z(2(qI34hMmKJwWSBXqwTNO|N5Ny&eVcw*%>cX$gwPur8+)z zpM3b;jM2aS>n3$x%Pt;#{^=P{FGvKvENi<`rp;E{x$pc}a)8T&DM=~mR(T*V;esJ) zVj3`U8FU7iRN?|DWDsIVwn0Mobrd2g(2C@?K@&|mXS0Z}p28Y_-&DyqvK5kF`U+tMmcB{IEbx_S(p z7(#}kv5-d?Yk!}%5jV(g+Gu0qD3Uv$L}B6Xs?nP`hq&~FNO7SYAUquJRrOf<=HmEL zPp5*nKlqY5{4@2`FvIA`(J%bE?hE#hH?gp; zZr`IAX(~PcbkAp*9;1l;v2q{Ms3YWsH%8RxEheQVReg+9BZH5Rv$MD}?xDwMIQ3jG zeaQlL@uB$w4cqz5tOeB@zIb*%Yu4@h;9%8eXM3+LOF#c!E=!n{GSYm6RZYv;1&)}5 zxpo@k>;a1R08Nd-;m)d$Mp08R^gTb>_x(?4ac$$yf3t&ZTLTS0efC^-a)WZ5{c)2S z#<1p5Se|D5NGI&hZT26)wOq7bSB}=(q5We1LiTv$#^~d0^468J9QHJuMo-Y`^kuf6 zP2E&YKZT@R!=BacVlS{w^jvcsTE=N00Sne)tZ#hH7ndS6?~>!qx9A<_EV-csJHXf? z4PIt|hmF4tp59=mAdUjYEOlV6UOQyF`f@^1d#pGj(B2JQ5enMjO z^fVgw6HE4>l3V2>-K0mGTL`;vsV5Po9C&}JUepp!jZ~M&>e#|6UekP-0n(TxJqn5z#o zOjg&<70Oftoup!&YG4;-hjwg{vVhf>o6~@(UdebAFYOF^mbx`rMn`uX7{L)}Z7WI% z9n~FYPnn+==oU!}^c|@|NoI`|W^}IUBiijLX(xCJe5$)9G%0s95Tm7ynK$HwhsoR7 z35&Gu8e7Y@<8Lj^r%CwBX*#?2BU;94KH4W;V5``C`T{M)-$L-;tG_*@KhxWPSol@h zg0|Tau18qsmJa%h&?&qXdV6}!vN@GYrG4x?yIT#?o`D+XE~~Q&3iH0W^~n5Cx9bYW zGzU|ex3VMTCR!=!F&-A~`g2Qne?X_QGIrnaUz^L=h9owJ#{Ks;!xF>W%r#4Eo1OVU zX|jSCAa!nMuY9>?bp{%|h4!J{mpw=0v)N|0K@OOfpOX{r7hXuD z`s<~qW@VSZ7N3^kH}3V5Yw7BvKc-Rd=&*z_eTNN+89KW*_t3w?f_~}mmmJ-Da76sH zBdEDI<~!e`UxZQuj$dBVC%o!wLg)a7$UQ%;<987tWB zkJ(lBISqaBjkTC!%?b7QpQFYmd0X?schxE!9by^qW84HNguGP7y3!xN|DNqrADwx$mE(kzvRFPu zE6@ZZyJ@~?*(p(C0v3aY#b9!VdBfRgx_FWP$hywbrJVT#Di9@hlASVGx1Z}nu8>AP zl-ki<%jumvSVgW&cJi*~c&W-<0g2ko_Q?q?aeQ~j_l$Bv^EMfK;Fs|TAKiI*scIDT z@bmKzHF_A_rNcw-$I#TFTb1)q9Eo<%`5fB-iKJpK3{=X61%eo%iKs&Pgo45C5Eezq znd+ek3B^R7KG1H=hN`zd`lIR4wio`MbMWS+yX^E+)z7^;qjGKLs6AVDywgq%+LH8D z&S#&S4U(!TX_FSt&p}OfsA(tm$}Ymz6YK)nkE!RQ=kmu5I|Ku^AYxtOD!1%-^!sba zj+D5YZqOhHhnXd_@}!xwGm1-;GIoOf%5Ji+YZhQXy?XfChWktEj62u8w`tQR&@a8^ zmV5zPDI};4B9rey#7uT}g8aa;uS(O+4`^4~?ojQdxTlBQzMZyuRQwt@X)x_Z14iHK z=8KuJCoq^b1@=UnflZlZZL_Y2gpDl}29qD3jy@okKXCGWpAZz1bmY>N<7bNP4K(m zcJKD9&%M&8->qI;&~e!#m&13)*A{Rd_zz8oo(uZ$_QUjM&mOWX(}wSGZDdP39y zYA4TYUL?<3nVfuJd*Juo#te!Dx2eocDMV{~lQ0qqT<&^d8T9w(93s?a?1_anO|z+4 zZAS)S-O{$nJyc=Q!UMG2!uR_(T?`Kh+`NC!LG}^*>ZjkInI9M%ADfnY`$FJC7v?{& zaO2*hvh^vmipPylO>Ee#WV{|QYRb;zvaaV7(d%FP?4KJ}=DgU=ZL%qHd~d(arMo|O zR+^Ndag!2;_8%*cnLK00o);G<6G2VO`9N=#hN3 zQFSxN!@(tJK~DnPBzpfGtH$6T1YwU$p-)`zmQdNwKta-n^|x%=eUxnlTeLCZrxdXZGiLb=$1t zfttAfWkuIklZQJWj?@f39zbpSu=Ma3yy&|}1-hhhXO3BB&RXDTFLV*>?j1u5|1+3S z3grU8!-cLaTrhuT!Gie(vW|VvTHbo_Jnc?pBvMermUrH6XxQ{NyTiU*O6|5&7ux@2 z1}hmP7C3z;u)A`JrGu;z9p|JlL43JMs%#ZmpoXj-DRFXQrylloocYbs@AD5`5F+bj zj$6&k9&Df*zoG$lpt~QkhcC%1g~X!CkXVp0sAZPugE#iTR)K=m-e8e89tIOP)jNl9 z(|}YwyFgdG;M`2*==WES%~(Ikt_!q=uR3%Hs&A}r@ZGtNZQI;%2P>ZwIueqYQuD_y zdUo*4q!f+f@*LFU1QrN3$bTAJ*=a~`gKy|78`!C90Ubj=P|Dh|AM95zG9Obi_GOfz zG?%PfQQ8-!g>@QS3^g9&Pt%$-Q39k8;)d>$y!FoQ`CCVgKbzF>M8Twm^TMxv`^ACu zlvhSCny|8J{s209>rSJ0bFZ+eJp&^A!qVqVShXSTyPo|<_8v4aG<`1W3q*b4y3u^T z`UXS#@pXv{t#Fe&-z2$tNOKZPN*i{+{mSOF!_aAtjlAe{+4RvzQkR9(Z{5CPz7jPA zSZX1`p{ENjv#xHSrA3!AI?H={I|c15JMuXV;><>aY7)kuIVznumvLT`9GmL-e8E3u zDX{Q;n$UwxTo0mj3rnR#|Jk#hs_S)*4rQrIhPi59!PIK0LMsQ}R*#?@*Q8|b3b7XE zT2yzLPbZE?2~VA^|B~%thiLzWUf!zW<@6FwtWunvI#9J;Kl>MGU$&nvqPLruC>hMT z`lr#G#!H>d*F91*r*w^LdV=0Tb(C94mDO4+DJtR(M6E_zjoree^NsM@yh5$(<~fH@UvQ&G}x3tZbO&gl$gV0*qE2i`#6B@|NbMlFB^gXl5tb z@01SSIBHJf&|c7v&O$rlo*)R9P;3xGxls!1p1U)*wz|9XN#PDQpk7rK&pdbNe%2`U z$DiV}?;oO$6VmUbCEL*C1$RMY|`J;nm1deu?rOVGaP zvBpN#@u3klLu-=lP;(E=a&D0cg&D}1n}fJ_2*$urZaQ}6x`{g$4aqL~lee=*JymvD zjn962f6{{LimHH;($F8Im=UyZ`_!4v4n6kKzOga#rO>h>cE~;@q>?UP22kCsRCO7D^m=R9c{Xp07 z?Fog(>hu!gKxj$sptg@>V(F*P>}aUwYSO&fKPNw*UHQ_E?E~o}s-s=K8rHXqy4kJg z+wZcEyaL$ZcKbLT!GAM=8Qc!}VpS_JTix`KU!1C;Fpz_=(!Rror@T>?kV5CM#cQOm zny%-L*|Fn_?nLs{$cf_)Hr8xOV9gJoD9v8cc=-5v^mi6vic#5uHba$h4GH%TH*j$i zZs59y-k|4>Du2CyxL>$7vBl!FFKpq;%E>eGCs(ds7#dpfO68L|WhoUaD#K0{CMFfn z8k^v>}UuOuCfJ>)wnara9XpPQYKQ|%in`;M*Z zm9na8eae1sQ@{Q}@d1(V6n-$SSAP7f?~gszJEGjbzo|$38%u`e^^2GolM&=NmiNV= zJd}^93S=VRoeR#>JjA{>_tY>dGJ4eLsK`-tWzwLiF$s}_l5`bOvGIckjf@={8x=Qn za9k9(>B2Xm#(;O+vA%=?A10cG7q$I}&{HW)peC3=-Q6T?+j~mkZZ3)`xF>hl_2lMK zxiWLciHPST=05-B_ZN<>S{Fa7x#Dx0@ddvQvHGu1vHHWyHqx&38)=sf8`+Qb8`+f& z^1WB9*!Aw+4n#b<@ZE0-n+LEPq71A5;s|T_`V^gXl!Mt6<1;W&x-0F0{oX)~Xf4QX z-8N~vCO);@orRAST(4K9Ag{hp$Cp=Z`Y5Y1>BZ>iH?tP6Vln-^$4tyxAvG5bh%BE` zSlCV}AH2vrcv%&TJ?qzVTvSih9yKK+O0!l`up{KQGisnzPkpr{nU*!O8`RLqmSS3# ze#h^>W6Q9aIR+~Y+c<|F>D+e9B@lTC2{e@3#C);u3NY#bYR` zTt`5_mUUDdTF1pr&;9dYTIY-r0P)lE^=3qsQKKB`Uk6$jPrI3G7es>)pYIdaP= z<`y3)zJKB4XDd9?)9W(Q-@h|3paXgH)K zmtJiy2)$9-Z=U!4^oahwlCl=8{ABU^jD`7hsuaJuF?(XLU$3;a3#-e=uP%QQxCTlQ z(k^%xgpQ^;C^ZQo1ccQ+3H01Q$-6GqZpp~ozPIMNR~OWzC6uI(OA0mxrbOfnS(95? zuUx7L@8DK6cHWZM{gY;fhSr5d_@Tn3fkU2!D73XPgd!#Y>$DzP-B6ku(lkOj-{tch z_mGpp=#MHEi|x@%^3{{|5V1jpsOLv1X+PI~GoH&W=8?=tDDb3Nsm%nztJ2hot=T#J{df z>8x2z{A0Upa(UE5d5|A`-%@XS2CFc1IgCG_r(*3CzHjd9lFqcQl`W0fw?P5qmN*fS zVO4qk!%Yysw0_O{gqSXqJ>wb@4t9x8d-QCn`)0l4ka3>1gFEH$y3#SW+=2Hd-@d-fSzc@vxz0jY8cQbm)gHfy@hW?hpm?SAvMk3QV6 zb}#dZii;aMG%hYmNq_g?k+7vf0a)oNvci$R4X1j;!pXh)_juxr+j-$zDkFe zbH6HYf-mUUkz4Bd?ws!$teznXyN%wrc@1-4&5lSl%4+jTm_YAIrlxLmex-1;S-eG% z5DDnR9BB3MeW4CN(YJXLv@_)Ify|ObpL=P2ZrLsR@OXYXNf-I=1KzC-o<_F(W&_Xr zrs2tN`GY2-7lD{D+&|{PXT2*NFq$s|&G&_PgWluL9c6)wEiN+QE?cZ9Uw^aV=bx+S zAvsyfhM(*`?lF^6mR`TZY8l(AwOe;w&uQGBw;MD*BibQ+C!&3@?20ig|I7Sx8v9rI z7{kLh`5eE%1N^r6yq_BWe4dXPzK}X7PD(A8CVYBn>oVrpR(5tR4WL1vgYFviC|7Vkb|6|T0M}(T>m}E5shfdvp{)z?r|<-6 zPM{-8Ce=?m%H2RmUrDJ;JJfXcZ}d@0KKwveP0V3*9xc-xLQ#tfUOWW%C454$6?s*M z*b4_{OZNl*_-$vo+1a1-g77y8+F{!!XxCTJ;_>`)>iAdrpk1v^KBtj}C*P6}qzV)G z2MYY~9&THH!#kC9WiP2n9?$GdBH49}6950VvO~ugTNq zCv4r;t;fBd!3%nu2K9-XlN324C_J&>prrh%O|VQIzx)BGu&}bLMo?xLD4>$)4S$te zUf7oFZH1+gzauQ$g2AyhcZ7W#tYXb2`D$D44r?wQ&P~4@gUVp(??r<7zCwqkw^gia zMr@&Tz{S&Mb>>FrHrqe#!7bCepv9NsrX5-T*5P}<9GE!fpgR zo_wAUPUkILtNK{hHb4Jx53zT3L41c#4v*!CHeU`Lah@VqDJh(o5M}*1gL9 z-SQjz7ga8tjI|q9aWz|VnLl%0@!XOHGjxVf|Lswum(=FfuvORDb#{!p+`K`Zrz!Pw zmaQ}A6+E~2m6w-o-=FddzC#=H(Ir-V^s2evBha?xI zyv~|3;%K8`)7wddSa3(diNe|_XgNWn};N+UeEPwz5a&9 zOf8>G8ZTUH=;`7i8El{(Ve{t3CA68UTU)`~rnMsfJJB{slWLVASLjbP8nIeje-ctp zw-xz9>d|N+v(S8KTNSO)!z|j6CL{Z{LJt!>gnsi#qgLoxd4Zr^D)gGsO1TAEPh+L6 zB45P%h_y!3cghjOz}v#D*Uf@{T%xF?yqLHAF=!e|wpgJR$2Mphd8Z9}I_ea-X$(DY zg_bX(&(1otu0rkiSZY*mPR+VLW}X&pog{m)CR5c@zr4cs=LC4T$8))8B z4$XUNCwer84dfACt?;?jAo3q?OWq_49d*-1%lledUMH~E@ijre1z6%O`Jp&tC3YdI z7j!dME%pYigPjQexB(Ztkx(N=qMQEFM_RT*(jySr&7yE^jc<=o~EKu3X zdC+Ctu8rRuL%;aSE&WrhX=25NXF^+v@g&s<-qZ4<*;kx8Ef{acf0zbO`f46p z&|wtlpwd`w*5fNCJuU3)Vkf5gNtFz6!b)1dAeqQ8{LB2|J%Ps9u9-uuP@$hHw zub!S-N(a#4ERLRm>F;O_m+rDG#crWRjF{``mPu?ooJWmsacRgk@1dfV*wv%;vmh9^@xEiZ`lC& zIxPRyHt^HQDI0hzc5t{9XfCn9uMspBtpVrnG5-yt0`BCk;r39IGj>W~%W#7Zzk7zy z+C{@^%2~6NL~r%}tXTTg+?@8=dX|O-l%nNCf)8Bm)v&EX0a2LVh%Nt^of?(~);Ls}CQjdQ7qs~XDRi!CE^M37N$s+C$7ENl2tqgAqKlB&T zDsHO$uyov%k%dpSYi6x+h<18v_(hAmoIFlRutX#_SFb5tPOm+RKXsAM0dP86PB3qQ zJrtTL+;Z5A@*n_9SO84jz(08An1O>9hzN=sy8 zwhuo=Z@6^4CRU9gPC;l5+D=>O%3|hfx@5G_0|c~KH8kj9#(?Hp2@#ENpP$&VGLS#O5`A%K{K9 zyq%7tLk2(ZBWJf7V)is_-MTSqzMoHZyY}=Y?9XS?D#vzO2(Nsi>DT*zxVy+pTw(6z zI_oX{0?^ir^u?|W5lh^WWer>4E-oJagNSI&N2s<%(DYDoGC=Ic8rUzZmOiR~SWOWK zQ1bbR<#+B=C$oosRrSoh*-4)?O>Yf-_$lM*cJz=`?)LiYFH^e@s5=c9J7yh^60)mY ziZCJU!-iX;Ar6(zE3UBcXPn^v5DS;Du=1z8QgD9?=R;C1_MKdo!P*Jx3TT>3Aozg= za)QSlAfH3)ZRLc{20OUlB(RSIY4)^Xr%|tQEFx-Cv;`}=m~jc#F_pIp=tp@w@NKdp^A`92ge56I`Z^<*A}l>X1Q&?p=1U)o@g zdXGc*hV(?#C$NO^7Si0_JU0+jL3{&pQ?=Z$1T19RJkd)I3oWqas^}$;hWJeFWr5~0 z)e$ibZ4sqd5nwCCYKmFO|Df#g$kK-owb;_%f0t4i;sw~1RLRO93yQQ+y8fn1|$2{7?v-7TfuO2-p;w=syeH8&0=$B?S;ubVv z1%d@_o#=tKui<+{v9F;c#BN7G3n`#M<2wg!&_W7m(9vQSq~Ycol?FY`7Fw+88uYL> z+;)7oqCpS0Xlq(axP3%}9xkNoW7aN2C3kCW$yqU`99mD~2)?ht`-z^;D#`NTy)L;fkVM6H8FemT)*4JaaS5w)fxA2ZmE*Qr5s9aKPLtuqKL8Z=+& zIW$&ivP-n_G3?;hUs~a{87#2#8O&D_)HxIuI+y`y4-rEK35PfVDY25B+IMpO+!5)z zfW=Me%61WMh!RIY%Rnun!0LbD%g>)e7v1^BM!Kh;6`MD|e`s|{T=LMuAAg+p z!6AguZINdofbo+Z2w+?sdj;e`5BLsr3w)g1dy5%O-4MhsPO)g7b`M#GaZE3}B4$Bz zY`Kbv0vvKh#E!f_kUn5LXP9pkATarU-xo^?mj_Kxb##p>8QpJ&2Te8XF6bE6r%!mQ zt6#8g*Z3iiF3@hKYtu>;Byr#^GN96HYqXN>_SQ$V^Rnc$ff z`(U(=p9y{fmbw_sZ2$i=E|<17)y8e4w=|nCApa|Jmz@v|ci;HzsbwW4FD{x@w0u49 zK~VobVgDnlR~IOH&?!HL9VvMJ*`k7l&*$@=1cZcGI9Ts1cyAHs8hGBcJ%_MF3f|kG z<&igPn;c}l|E;|gBxx=0s6J5yrcS<=2f4igD zIYLYE;dUwBK~dJHM?Z-k_loGSs(sB<^LPP#&*R_k?H>AhSeH)=-TUrNvht#h z^o5U4+g(2BznP7Lv#mIlw)AR*l$m*Irg z@Y_mvoCkec!2emg>ygjPA-0O{l($K>oS*pF$7DI3?!0={@LjvhIojnO{xL66DQbr$>WC*ZciVSj-yXz}CZbhTsfGmXPx0Pr}%uF_s7 z0rmfL=3L^q&5wa%y`QQd24y=L3#SapHIwNPN$-$)8MhFQ+o4#qHTo0 zv{#obUcKhUmsUx)YBy|rZEXX-`NL1rtJ5eyNpIl02ip_1wsBCl_pPVwr4f@R@Q-MW zPnY7XXGpClZIdi#My;tBX_A3<@kk(R9FR=_kZsTp=a)8Ei|1de`)fBpFta(BXe%&k zTw3AGiL|2m>>atU<=_gip=XLe-a{H_zGzL;)V$L41|7scW2aWFl8eoUBv11-x@QM_ zMAaQKw9V~Y+e}7z9vSG1@|FV`{KSFw$K|RB55yNgOx6g&gZmFpNvIm0IGX0N*Kn=T z#m4hM!R%4G9r*Vnj}`3ZlbSkfd)bco0)K=H_L(t$N7?qWuf9bu_Q2P51MTIf-Fl-= z$Q~Y5<70c?%_`EkHqjbWOwUIzS^CV2DH1dib}f`$q~+8JJFB}1<1$~`uo?<*Ep=jd z<773jyzN+A(<7f#riSNsKc8kN2Mt4?`3dhT-9d56jPKRlje2*!pY+gdP5KLlYQo|H_(+VLy~9~+h(GUHP6rf;5YVJ@5N6k((Gzq8uRPEjnjtD zRgN!xVY^SDamTL_Eo30Izq!NUAG-Y>^-R9*<%7D@q-+uAWsw%}qj0YYmph<_@(?B3tT zY?azBv}2`oY0a8iDaw3uW9qcd1%?B#;&E%;4X$xmtaVZB3qd7VgVZy6?sW(h)GMcl zX$q)X2U1204P~q)|F$OO&71$^y(p$-B0tro?#pwYXuUtJu88NwE|L4wG_!~| zB20}4$VJe)Aue zW2(*lP%A7Vx|2GUctHDw)Dd31HiHQ2b(FMvwHe(*rJ57Za-;(WkX1CGW0wib@eP)GJ-)lq`ykv;iz~!F0Woi^uHJr|VN_V39#Qe#^LxL%M0aXk?-45Ogveu($hIAtQT6tZs%>}^s#{RO+GQ;gV>jZot>^^O_8J-^veB#Ww zIK?h=%dY+zk@Z?+avGbAc@wg`~AASzaE`*jy?{NExB|n;77x+&g|KHqRYRMm_<&z>v zHieIu$cLXJo6~J8I`>eIeZK`d2483tegasQ+hZNI^P(K;nD1Wi?;>9n49hYnf09P%`W#I%u4FjAuKnsqJCC>450z zjClmg3a zD(rY^*k4V8EJ!C)NCtV5Oa*nek`3U&YVrwLO|Fv{$TG5oya6uRNM-U8RY(=ia^54q z!hgPoyhsj{f0JLxdiZgEBlpPzQcvC^$H_6WjZ6bgmxBvVfTo|5FA&=^otz(2s?O0;b-0ExMv{l|dx9wgT z!5fl`ABPcH#f62L8eWH7n}_ysZSd{$+U}LOX_=);9)8sP+4%+8Ir@A{bMrM~K&1{hKu`@n~E~RLG^E z*#0~X8g3eHNn^FNza?L*w104{iRYsc)-*&nHm|s}NLMU=$BW-`{;T8_7tiD^oR(8i zJV(P88)8XAEoqn~4Y#C!v!oF`4elQmElr~$k=Qdq8G$ZhN9>`icEmmZqPF%=;?_+X2@An+Ivl?v;BTZ@p>pt>PmAzdLd)?x zm%c2@@v`{Gt$+Z1KxAE)_bE_GE`AN1}+e32QiV6|sind)C4~h3~*1 tFASF*v@ZwfSY6SQUUU}fk0S}RM5Lo=CGf<-0xcmg3%{dKf^y5w{{y<#cZL7} literal 0 HcmV?d00001 diff --git a/blog/static/blog/fonts/Open+Sans_700_normal.woff b/blog/static/blog/fonts/Open+Sans_700_normal.woff new file mode 100644 index 0000000000000000000000000000000000000000..30b4c3fbde1818acfc3ce88a8376cb22d1b65390 GIT binary patch literal 24528 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 PfdCS50PIXa2><{9>Pi0p literal 0 HcmV?d00001 diff --git a/blog/static/blog/fonts/Open+Sans_700_normal.woff2 b/blog/static/blog/fonts/Open+Sans_700_normal.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..749a9714f16ad6de8200d7d4fadb96cae05c781c GIT binary patch literal 19504 zcmV)7K*zs#Pew8T0RR9108B6d5dZ)H0H1IG087mP0RR9100000000000000000000 z0000QIvWrifmQ}!0D^7^fhq})U=a)of%_1F+f@sO8~_0}0we>BGz1_8gF*+9EewHp z8*Pgh#G5^Y+d(Y0|JGSVQL{;_Mn(~19Dt220NMZlXC?TTG1lL(w*i2rW_O8D5`wT% ztE#ePqEsg=*;@rwjT#APpunVQQ7}uEJGEYA_YZH0{l%i`z#s%3_D#V^C;F+TQJpZC zGfqu=+Yc`Gili`{ZX(z!cW%zA{FH(Z-r2)@gf|SDm|LA?vEcy1d`;|j&Bxz2JGFbK znPvEYJ{!KPz104%=ht(px!^8b%Gsi8YM8iwUJ%})W8)(<_DL@MKfgct);_xL3j;Yp zXwyLT327n`+Rq~i)E(HJ*n5K-zy|0W=+kTf=H?_vN^C^*pd~kQV;hU2Mr`D$F+!?DDn^;q z2oVu*1{k!6ag3Ta(W0UvD*r#H*(c-~m>C8bvJes|td?D|!E%C2PPx73s`0gja~nf- z?<$``VYkG&1inaiq~|tOp!8gD1i<7V3}u znf}bqEX?u)lK7-M`)nr)vG0==P{QqpqDxW6ZS_-egWW`@rPwDxCS}@6-@Y%qo`_b> zG9>_xa;vI4$WYZyhT%-)@f7W(k=Pxa+MSDP=FiLyb{C7Z#0da1cmaeG2qhA{u(KfVfustOl~V`=E6tCXtT_jc!^%0<_>b?GCWWxLwh`CrqapTLh$bW-P1-(gh2c+?7A=iP2tk;@ zh_UYP`|7cKKb-HQeR9!yZ9^4k z{lt!%R=C80HDD_Z5*r2CE1!qMBq|VCaxm_o15%QUTv6@pAnb1!CXhupHaOrWCMu|r zp`>4rO;sLjRCL!eFPb@H3)9i7s1?{B2m(1Lt$;XwYWx!Q`n#|!<4GF*g&Q<>c+FMlu69!v*Qf zQemPgPxWfokai`rsoB}Ay|yQ}?Rltc0tzO^;TPvp># zdxD*P6F~1o&C1l>wRWR`0@Cr?TPn4^(pxAFdrxwYLNaqXt{3>_iP8i z7Z-p*@jOr9MQ{p>_oPmZWLYO!Q5mZ$XH6BXtC9qS4CQhaRv^ql?ub(NH_iv}Xq49; z+ZXg(foYQvjdI}sruF8W*dOLzeBT>Sk(~bWK%cNfdaFWoUGvNCL;sUUPJ9wGdJa-htX?KC7oyDL5rwPQi=&U;MQa3Q$9X z2h_+b)?D>=cad(^nSV~rxr3?*%bk#Wd)FgeiR6qB?dqB;$$HPO`t=SvFN*1&xYvJ% z3IAr(z&4#7HBafyM_9s|GUqs9Dd(a}H4}%x(Ev$q9b{LVK_O&Op zBVf^hYRB-~dkB@4Gw@v<9k%;DOFmYhYjnbZVUp@9!+yhutZqrw=-#N2_woc>3yE$_ zsSpe-|Dr|fWnUyq=0Z{xQQnDKk^}>*20FXNbrr2X9d=%9j#h5xbh3#&1a|NP?<(Kk)Dmzcc`v{N6ilQv=^?CT-zhA!DhYY^tKFm=uY10 z`9+L5IS=I)$w&olK~2kM!D17V+NR%dV_8ZaLE9817o+3m)`+2|eZ46s(0w2dN+=Hz zzzYtM*)z1)ZZhJxDCM^Vr}qX_ms}KZ#V;Yj(Frx~8V!jAHvPWrM|vSs0a5JH6loW4 z^%{aslbb+Uhg8;em=BCSOEwLPo6SSP5B0t+apZ8mBMVv-bF+!)5Hh^Lmg{Fqj~Kro zG$a}<5F|D%rsLWI4-^ei6yrzv+)hzMGwt+VB`}yU z8GT<0I(lUV=CR`opyo<{Jqpg)KbqN>dNPDm@tj^9%fauW*(*IbW|2z^K&*2h?m4Fy^^R(aOn2uKmW z&r*&`QlgBl{R<`ErnKyolXh*LEs2IrMB}DM26+seIs4N5?k?S|xj6l#G|eYT-X~l8 zhCR>*(EAMwE<+;FW3XO|xP$)jyHAe{MbjGNT+mTQjdZG;ce z);-=ehL4N)G3LTqVr*wyn4PuG($>b<-(YSZtS~-bJkv#ji?R1OMcm>Zd*S_RxE9V@ zw^HrmNC&Zz1R@}nx8V*KpPP*a)xb8k2yGwUrUsp0*hLmOf&mm+a8mIcKg$Hn3WC%# zxg-X#2r& z_2*;|>nB-=IL}A?yP$J4TS^?FMCU-!!d;-(Iu}t$`BSA*n%0 zTuK_a6?I#@!WKS5bYE6}`O)fBgm+G_B&Ciw?N5uFH+EAjG z4g)Se&?HZa|02)^B`3sxRJoegnNcdIDp;3Dl@fL`N^F@&Vc&L1>@Hw#T;q3Yvf8-* zyUx+47DJ7ig5<7zi*4@ilLiEJ6ppG>hk&PN+{g&rcy?Wb$dL{|>8M(s2QePoGld*& zaxj^W1fC?$#vTQ#E(EExymFRK4CJ^S7?v9ehJE{4)F7~e_Q3-pgrMsJ3mIlMxoG!B z0LywPLOJCs97Ctqh=#{q^`6#YeAhAyiCPZ_`CqN`M(}RJ+HYY2sJE&O#X3B)nVpt2 zSg(a42<1K(N5Uz{vdv;l9LD=bnauiHPGXaV!|0uxxi24GM2Xf`v-fIHO`L95t5rX{ zg+zzMMK+E@lFsEQR>f*Tu`akp-JDDqDl>FA_G)9_WNcUWC>6}OvNpnv&So?u`$DQ1 zTN?vbO?tEIdOTzpe1b^~>_^Xn*qi*_(Md%??Ja8hjJjD-tEd<(0EA`We^2#U+=<$e z`59Hv00%XtRki=rOH;R3lv9_AOCE)_)p+ckn5L)Zmer0&+tVJ$8Qazu zdrzShctU)66;}P88uazff$s63Cs9yE?X1rcZO;IAzhVs-5{GK!5u?zd>M(`a?}f9X z#=mMr=+FtZkfuPW9xLLiq0qM?tP|5Dr42Hm!9&W3DCY@GI(O~9Zx}T!s0rvZi%hV4 z(mI)`RF1N4)gk(r1kVq!0LO^Y%~lXvbjZ5{R8!HDU@)taDx=tFSrUQra?lKh*g5ZY zA9v<_PmT=K>1iM8Y-gT3O>BDbiM>KDy;8-vAE%)9f#rULV;~1i>Qhw`Oc9Ag&*Y)e z+ZDF9tCx4GRU^|aYBWJlK`j#%@&+vMNO05N7Q6N9@L*(!&2YX*>88N0oNR=XfN4i6 zZo|A@lGBqB%KP`BdwHP8winu7@yK#YIq~u4gLt}WylcziBSD8R($j?fLq^GHff$PA z>E3CC_$M5N&UWbX^#4J+5c;NN-O)%40_d^;QdsYBkreB(^^w=WTN$<#u{o z2ZO$Dwsmi(YBf}42k0*q#5a>%_3D~WLnUg^bGxyky;l|2-#L>6j)}aV;7$7kHwIFyyn^~3?@Idzn5I&gdbxgGS@zh~%wuJx3VDlHeI@AKiANjhC`dBvfMU&MQG;Ps__sa$q>bjt{pi~Rgo6R2mej5OZ_mqRkw*=` zZw_}w)tz4J44!obva832mQxcjy>g=-GOvesICk6=^y|$J&=)jK4%OG;W^T zYcKoVRl73(%O1L8^58P-8CQ^^((bD(@~op8itA97?)OR~T|}m$Ub{=LiCGlm5>Cp3hp@FanVEL7+Caib97gP$SoYj6NSpN7P^!C>qT=7k57_T!`1p@62U93Ee|kXY%(Qr7@n}ELpIXsjO=m z4^yk=Br`qF1M?s6s8vP#2eUGAHFAt^i@IRFdFe{N zrc6QI>?-RJ#FW^u#PS_vG(uSuKUm#yM3=EaDFV;F4Uu}}+M2}Z6wJH1{Go>TS|i<^ zw0pT%4MSZz5?JPb*K~Uk1v%je$Qif->-(CTE!`i$cjB}`CYXIOce961*TY$x9m+1} z=p$4hGWeyi*AcWbKQRp!pV`G4o`-2F?Ps@yi0Qp13@Q0vy$_UF7I z9x!~AxI(>}w+c#mIlEBh{?k4^@-MI4@lRItCI6eAA>=kSVhICWZa)L9n2|0zXH#eB z9AQyd^KuQZ!;tZjUN+ky`{4=(J}KX_v`!r0vy>^wcf{3V6XcJBT7q>dwIBA* zUS_p}LtB*P0J&>e*HI1%DoYLVbG(M{@dVD!w1j>x8WxAbU}1-9OE5P8>J&IN2Gc}f zI3GR=da(OId;WBGpy(bD>qz)1J6l6yaOHPB{5Z}~B9AmgK%vBd3enwXB;Jn`%ziex z!}<*8;ye+im^5IY7BbAD59&1!#kv{tJ0~u{@<~H5*gA`9deLTg@zd(G*^Mp713G_h z`OZU`2W9i$1+HR%B%?iOh@dS@aR$}JEv@@>fvjPxbxnvg3E^@d%>(>EM>N>r*%04b-KVO}A2% zL0&0VS6#GE(=NVIuPE8-QM;GKxuT>($;-gB$@EYKS_5$#O{ zuv5>0;(>HZ^)wBWv-ad_fw|!D|F%^;3VC@4iGu$C%qsewAn=bb?;cYkXe}WwWK&ck zbb}EeykUU0LGzUhG$WBOdQLQ6f1Y+|ggJbXklewcL7qIL?1QjphNzs-Mo)fIkae4fK{aA2K&HPkLb>JEYn?^lslpWK#>MgSA45 zHG1jMI8P6@AN?>DR@`Hw#n6U})>P%Sx4JfYpsY-&?V6&HsFIrA1Cn>#a6(T)j+f*w ziY)ou-4NOsa6lrw@AdTCAFnoqr8YWt*@fyyZ_#3kvl!u-bZT)1Ha)u&`dN!Gv?!oB zsnz(+Os9MPF<{G*q^!X*k4|#+t>jU~*DJfy3u8IO6ZPb84FALtYY$R-NHmKX zj&)B;$%Q@*`?%m2it%tlQt6N|Q&fnnC(RG<(djkB=wlYbB=+&3pi^@JF_Xgm;ts*h zzR9`Jxnp?^^qzN7J^$b;v-37e(6OC&uyMzeER>k>6 zBVfVDpds$0rrTk0uis@KJp4A(oj^NRK@0J+ggZGI5Z(Ek$s;a#w>EI6r!*14^eU5L z^^J%0Hmj<5c|(8)8x*x0Kx3u z8v3u7^|ecaZ~RBbeBF+ao(u#8=ZaK|zcF&lDL>_J7ZV7M2}Emnj%R5^j2{W9f2otj z(#Sit?=DutowHLInkdTbiI5U4SIg|Hx~uViUt5;O8qG~Y1rRcQN;(~L?&W8MMQ{a3 zmW26Bw}m#>6Wd-R1x66>T(O0*OWh<}?&7oOk+eTpy>15x8MhwOmREchAa*Q7R%Ac} zUYzf~R&vLH50RBc9D<-<4!>l8Jl7$3Gw?bDXYBslwKlV|@%UM3y025{8&n(@-dGX! z@jw5_U+4TT&VQ9y+L=7K5k`}RfRUt7KtQ^Cx-CHLuyuBKwy{&Y7=r@y`N_f}$+@Kf zQEUqkg@9MAP6VKl8&w!Ax?P1c85b&cDgREut_hpqVga1CoIsX{OmJ#1n1*w}T~r~t zJt`;EqB#oT%M+6ok#Ed67E}v(&2<{rvZGGUP%WkUqu#=jDruwgw$$XejHnU}#?I3C z_~?8rmRHl~1sck1 znpOFHxG_T3Xn@|gAx6fbP{!u4Am?iL5|!1$i z;?xDX4$@YUa!8B&WYIv^Wg~h%pgHq@?-Ae#(R^oe`d_F#kqZ$UQL}ghZl@US$Ur6V zR=QT>bHk6K7!j25nzXlAry9hI7z(Jy8@S6Ex%72rigZlES^|CVsfYgeAPaxA8a~It z+FT7^im_$7%^N@2?gtFsHJ;je?`n#E(g6TdT~%8#7W9F0AdLQNRx?oRB2MaYUet16p_Ot$*^Z+fd_>A39R ze0C^oGXz8sZq}r2bVPRM1LUaelI8r9gicYp?C?lSfFv8oY9*q&V!oxul?rrdjwYPS ziA);k?q+tm>3weti_a8J8aR@~%;cvHN}3x=D=Jv-ZwpK8G%gAh)YKJ5j2MQQZu#k7 z(etwC%m8LUGI|?&u(;b}mmZzXG()`)R6#=>kWOAeC$Fc}Q-dbvh~UwKyYIuNKbydM zZ(X}{rghruh2_T2j)sVw(hQZ)o&i>FaDt}+SvRsL`RexETl0J=S=GkBuE)LOyRQ4A zUE|GJX9-DYq3@_bTnv%q7ZZ4o)0^K5srojW&%8J#2?0m~an=<&DdaB|(fMv>;mNFx zh)o`w!(n^l(0GpQ(mI3+(ed0&#VhQ3%D_=xL_$C0#qfhW!om!N1(;8V6!+QVDPv2| zkB3613!{TfF6SQo2VZoXFMQ}dbqv$a}chH=SL^N;5s^%k7CzO2mH-X0F4JJrJIvcU96 zt6Lpu|4~^aJS3xP&QCug6-9yu7NMY-#A>Od6>>8WfK1sps+ElGemwggA~g~%JW(d` zq#+?{dw}bvcey9LPGJ-abnA{LoXUEE>urqi@5q0<7E&@xR&%ju{2cK>rD{F^QY@c2 zi^ob>VA}C8hycswq?{_;roMw+>Hwjxnv5N_3LZ+Wnk&{dMzgjtAPNk=ZL9Pw(9#2lDXRb*9KtxFN~0{#c=PMxaBwYuY{sfdy1vNPJ`Q9E354hu zKhJi}{s+~>j(CE#Ei68j>La^EwaDL5D^_0@;`deSI!Y*$#}vxjND+^QiU)}a0liHR zPL=E!gLJ5f;$u--0WWE|=o&xtqBGNGkeqiV{{NQAjjWJ>X%Aw`$|I+u-iS zc0XUKZ*frU@o-I=9I~G^DtKpg9X9)GP}EsXf?ODw^Ch={j03u~PGs+W4Ch05dKe3i zeby9T*SaiX>;dNFmCdZYsN{>4Q zOIP&QDYUce3EtzkdK3U&);-g>(OX{XJa!=XpYmfh&hXi8V6mJ3hPO(Vwo3+!bHRHX zSENQWIm~q`1C9@S7|$$BO=5$VPrI_vcnW?ZotIwI--LQ7ps*iljAYX5hIzwN^Gh!& zV~-K2>im3bN&>?=Fgv@hHaDAU8NtF^W@pwuPqQSvmjAT2Z`I%a8va$Dy>)!UiHTFvoL2xhAZ_6W zT|e)_xnexK=-nA^{~bK`rn`EjB^nDiwSa`fnI;{st-bM=vvDt6q7jWdgYye@=tmfX z8GEnERDC~xOFvC6yzKP5a`%ELmMO~K--8}pY=N>y6Qxsgpa@Sl7y|C>qDRp)PH(a1Fi$7}deOa!E)a;bl z_^!BXz;OQ5JZz~G^aND>EQD6pSXFdm?5W-p4HN|j)d?4@6@g-%l4oD1$pwxmu9-aj zTah`-6A=6&l{p>cUc1kW6XJbT^1+w&SG#Q09A3tn6?z z3a{IXcts_=k_hLoOQ8nuBhU6GytMcI>j;`X#ZvE=0XCeR*<7E0v+PW9DlwRypAtDU9DB$*tQH9`@BRKG@ao@Mka@~ug}DNO+kfXbG0&piLA{&+2}K_le76Fs zbz%T6Po`UY!*_1+B??eGf1{k@a!odF8<~NEE^zOhYrQtz(>FZP*>ZjQO*h?*?&IS^ zKSx}`Q)Rbsb$Ew%^7QL8a3Gk0(yY+>4HO_EEOIv(5l*RMAmdQ>WLF0g+~04+A;d7^ zO_<$JkJvW{clzQ&&s0ZKJ+0x+j{3yOM!kUNlPD^Zi~%$SZL$L$4SsN&4y~Y)hmsjq zXh>)#Ixq3TEZ=ur866y3_)vKvgfcg`s+EOdl#8LK6hwzcLs;`VpZNB4hX^aVSVZwspt28i@940UY0BC5$?@*tuf4m# zTY>~n3KUh@g$$n`r>}obE`+)cxE+7{zs#$~C*jOlFnyK0h1R>Oe@iimKZ`$$6j)Fh zk)9Eu_2re+=HQXUn5>en;rlf}eOcbm+FOr)sm6`z`=OlH+81@Ix*>K=LDT;KAA$7d zI=|H02cBUpSbKKbauw~AbaPxr0V5;fWPN3NRZ><=XWFKpP%@48k(a%Lgd<=u9~9gR zj)g)IC;?wM66WrW@t*rIX(MDmG!Y{>`>sB5n?3z-u1cbrpeV=XnJk#+NABI`=pJv)2RC>wikl= zHky|_IJpQJI6Zf|9n$VN=O}}hxFQ4PQuB**yhZhSQX#TH;6u;KK8JYEtN+&pIrLTg zV@el=F5h`REEFgvd~M6aAZhAK!W~=$JtD;!=GFZukFKG_#g{(RCW$&oOPU8kpD7Ue) zgmK-FX1ayZ8`p<-c!rM;(B=FjH7gl-eDX*_N_G<6M+k4(67~=G!&9&o;lB=lc?D*7 z+`dr}exvWocw^pd-UK*1@Pyo~TqD%SIryc9=$SP1|C6CBXqs2n1sS?McIm;{bThB_ zfht;S$1jgf3~f!nV@^La8u)YWn}e4pn-a~u-(yu=zuVT`vB=Hz7i(xCNV~R5}3O5ZR z^2=gn($lIgS)zGoTkPE#o#+1WSX@<6Qc`JNLfnabWU0nihr%>b7nH}xVTlo+TZX+om5rc3HI?!BO3ufKhm69sOy!08AS@0~VpEKyuMBXW zaAaL0jm27dS2=?ZuLz&&cUdOL6CWPU)=Nj;ymj~7=n_u;e%5Qe`D8SwZFhMXD~Dj7 z%#CvQO^rg1ctTNeAQd)Dy?BN#T3g+jW{B26>cQ1@)QfKHT-%v)vNfzlre7&L6A}h} zOu>7f=ioM5%H_B1;!MHicexCV0VgHVLD-R%pcUzuNWBBl9}pjF;u$!AE@PdS( zrM0V0!%jjFA@(r)F!9g@qj;!DG665#g2VNqR0_2`F}_3+GioS`6*U|q0QY3T^37o zAC4jh8)d_3w)&uyp%R_1?h4<>JF#J_9%|`Ym40I~=~#9PG{H-&AN0oZlt6x$h=s$- z!F{cfh@OCoVI`wTq55ZYFshGya#-cZhn(h}=DfCiw=Jt2Mi)sitjwCRl6wmV1Oib} za_P=ZZEHqMAuizZy&$J&rq8s8s_te&oX*0Ao?}dDg!W?}7bZrw+gs9{hnGjA$*$?L zQVHpFRcy4PRkj7D&6s`DY-L{h5SO{*a79mctm!lL)>zJ!$TC@%4ANtpPgmBrpi6?S zX0!V;t{GIzm?MXYG37;^>IFvCrJef%U5z>| zC|s`hRkXNaWqAU~lBpPBe0v3_$=gWJS>b&l*dDgf1~d3W$s~Nh-t_qZ*hj%O52Qh{ zpZ@uPR5^tRO5!i20lY(rPOT^D|WSDRmHOa2_+j~m#BiMiMQCkneRrLH7?RWR!e#n z@tmWn-KfYq9|B-kDKsVt4vb&*#E+@tP3j90zv4&^nSZ(4_C}p$ZAs>`2?Ta+$V;%{ zMCPF)fg?dK#m0(7V-KG}QpvebrEbgqI{S6Cq`#4n0nrFZ^Zou;Dz^oyb>_zJku}l#{a*`Quhf2&7>4M_5JJ}~UlH0%; z!jULd$Vo+>Wiqr+wjgL6qN~z&r+ofZE(o?Mnfd>nsFif>|1+$h9(Zt8UceewxCcM& zS*)OucpvBJ<>Gn3KF{PgKe_rnW;ZmHnZm?Ux8<1Kx@&?JYxlFiW65NYc~#sfauT); z=Apv=4fv_N>Bl$+n1T$I-(Cs3k?xHu=_hFxgmBImglVkdEEbC|n?T&;enwpJJF-w2 zR#=SRNlA3i@Z(kHHRdQ$jiqkGoH}!e)8?(ZyNpy0K5{b98oL#T0gy0U zq}Wz(#;XYO9r9`wW1_75GC6Ph<3(=yrfM01Sp=t96eKKD;hzpi`UnRRa3B!ya|q!z z;K6IbZRSgx4d(BIS3o*e!cy@S2DZ8?H;)+H0snq^@xlMTALM`Ci!Hf+vhe@k-QVLG z_y`n^`$vBN7r?RVHIj8;25L%jTWgyYuyRw*gZ;_jgN30;`X*7_nj{u;0SQ2`EiqUd z+eGwQEoto32tD0fxk_)$ug=1+l)IdoT*m0PkN&IcFeE2-! zr3tqSwC4iQU0698fL-qP2(aE*Po^e=#;0dvRY|IHi_Yuwn2!tKeXHY*!G{_qi7Emk zfNffbE~hylmPd{*V?Bw^aliElGEy-+8jHgx?SEgxM?*4+?ArBhfrNS(8`GV2OBbhk zNRu18RIX!^<^U_Z4=QDhbpg_oYw?qwZtb8zjBOCxram>*TiWq}IcSWL0;H!<&V|TD zQBT81AS@s!)1);Bzj*Urhc-cVS&1F=D5gXoimB8mL`p#OU%^y z)Qg`k`)DFfp0TfET~{Go1#Lu&#k_jZhl{`!Geei=oQh22{HON@i?37%atTpA!dA49 z7=dVs09oX_O3Hky4ddft=ILN1iQIxq&GJuJo)Rw`5apzvq@N2pAqHOCW!-<&eF~A0 zT+Gucw@V~x;G@MKpDu%3yU!X!B~ujn4kc{>$~vAB9dM% z;G*Sz^k;FccpXwOS&!u^KwTNBg%NWM|F6nvk2>K9fvR5gLuTP#%$zvP9VWA&+Q9GJ zC1T~1k~ly2rnv0Z?bQSmevM)&iSF5gX*5?R@lyBsf~z(EVQuXo^_9z?MGe);@^|h@Th$pr4~2Y2M58X9WF!t(deT<%G!sT((_Lne_SRw5R>y!B`^i6?1JQ zo9rJ_rwCpTYYGbGrk^d%7KoftmX;eNmVKj2f#Ai5Z4GBRLCA78{_TE}d$+!&g`kj!0RZ1&ZVUIiJQ*cIv5Fn-? zV3^>t0ys+&ETA~dmnxvOB@LGk_9AZ-jOVXO25i+we=I2j5QY9A9Smzp#SAI}1+0lw zpal9?uSC;=4s&b`KNCwOcG3b^3Mw^~g#jF5h|XGM7K1bQ3_aZ+D4?pXw{g+JD2=-^ zG_3Ni|4EyAaf2Iy9cYZ`6V;=62Ibkmzl(6!8MN5|*kqqQuZmf58_ko5?uaucnrHiL zQ}y*{et|=Egc#Vad-(tF{$PzY?3TK8HjIiTQ7RB=fWg;vZ#vYS{)Kd%Qmmo9K^*J) z-Cd~@WMiWS&am|y1d+1z+;y28ZMNLEx>$kSwg8x65Vs7*#2^_VCCwf%@JpSdu+KFQ zO&jCg9b^pHyz%zGC}p5w@yH^I)9GZ*@Th=;B!TLz832W-m9|ckBJ~q8us39TeZda1 zAD?Bb53dxOWqt?(Z@06GT;9{$wyKs(1abYVxIldu3`qtO9fxCqj!CV9t=7eOI)M*O zOD1>Js*GdeR*#n4FLV+|&nlQjkEPt|CIZaf@P|^#OO@H-1q!n#Ydd0t#&o{AXY`c~ zTt0M>HxhLr1&s^{)-jO*g&MxjC%huixvyx6{DGYQCGx+B;=?G_cYhyBdOvMT3Q&VS z3@`>2k!Gv`yqdFPbV!mIAX(d{_M!Cvg#b=<{=lj8{Lc#S4^3BC2!W*fk+t2QSkC?9 zWBg^e@G%{iY7@f-#^=#}`l;%_uJ(9{7x|%jegWM{gH_CwzT^o8i;60dLohl7f}`89 z5_3-KCM61*fIfnPT_Cw87^lt@84dTNs;yzWPADl&Jy5uzVXM7%ao-)*lJVV|0xVdKl7jM8N>C&%1Vo-SmZf0Yy#ol@M}HNvUq* zn{J(Js1${j9Vh6(^QetCN_Q~yN3QVNLlpE1kg3)YM1bU&wT_PTsBR9O)7}v3Xo(K9 zlh_dDTqX@VL_r|@&NN(ZvLBW5As|3@q;-0gXtg2dbgyBSJXFWtCRlLSdl|L_V32@> zg^)P_W3I6WD~KbVb4sH&Lo16ip&!I6w);h3fa;^o7;ucmEYHgT_oB;r4tCfL2VWvtS9L34` zu9iq_>yS-@eG2=(wtfpP$^Rawk}oKGF15%-b$2JkEH~XLEjEq$YY~}$+9?B4*vA{5;?v0UTD-QLIsbyXonlsyP=71^f_bA&n_4dc6;H1`d&lj zM|_Z}acX1MO5^3656 zkq5zH*{rTU(d=uXEku#x$*$KrcWD^2n`}?Il(i|KnG7FcyGB{fe_}MC5Ghv38g+2i zCLcm{UmO5k7#NG^;`0^6jm*BnH1s_QLzPiOV{=tk{62)K<3Jd0LpH4)l;u#Za*3{6 zJy>OAUH(t)TB49MLCq-?AgHei@tE-P3ql#PlcG^&8AYR>+pJ=&2V&6Yzw)R zz}VIf%P1t)1OTr)UeaE^{q7SCP)&F4RX*(tH}|?96wT*1r+RD{XdDPs%0y$y*)UOP z8qnd=Z~xgZl^a;Eyx-1Eg_l?8uEud=u2$N5El-oJ{AL^bx4CS6pxxruHtR*V@ESkE z(@g7iG6x>?C)lR6K}kxhF_h;j7$ZfA=0)5k%K|?0+VYcj2JPomn<56K@EJPf+&{}k z6?LMgmFytryA5R7Vj1})k)vp5hoLm?q3b`8KzKQgj;Vjky~=IR0xUo7U}@6I8v`&( zsMI_LL8P}Gy9WrVD?XR}8(T`m!NlxHOpuPjpNJa6g;lU0`FUjwS&PEL33v|l66kzJ zt$Z_4O}~2wZI<;BM7y)fC!hH7;o-_(x%d0|b^FoGaYl!R#0_uBKbKk9T(*s10Or^u z8U#dvZ3M~Q&a+Qhy4*eNH{pH}7AL1#fW{d=(P}mIw~30(y89P}=nUx122N8!j1YEq zcUOBU?L+8ry$0ZJ55v1oX-je?m<8mAlKR#@MF}C2QOu~Li?Jk1Z4H1YQh_;Yjaig( zIC-ExRRK}7TgiX6#%se*bcI}?goTz64KZAp7LsxrnIn_ZeB{tjOvzeD?%jv=mz9l4 zi3fIrxUu*)u-SW4gRiQDLe1SnVG>H&tQndoL$P4$KDWGBYrGE;kwT~qjzJI&D?nPD zz9W3GOoQtg5E^KtW)fE3t6`81Fu{oiOz|{!dq*t`z(GHRYJz!qru2OfSesMn$}xrZ zS_}6SZPUb02>^@XvHIQjd5inO7?D*QLJ)*uY)oZbGgQc(hrRp3!fSW8^;JV~sIvf%?SKYKNUs&2 zCbu&T$Hz}34W29S{Dd4QcUhJKSljpR^ZW=YNoAq+dOj~7xG#8}vD&o}Mg#LDjmi%8gGq(PN_Ot4GrbTC|$D z#hhh=k0#6lp9t2(YswW47+`iyaNY#tS?#QJ{K+s*k}<2Eg&{)kmjOY%X#u?Q9fk;fCrDy53r3#YyiA<%RgWDUh;WP`2D#i;^ zG9w-H5~AgRIrvlFgeI$1p5siRv9W{p^kLE3FX8lKV6cO;uoz3G^~RjCIt)H9*S&E( zwk^QBiJL!tx9d*a)zN@wt+Ar{MZdc2L+Ao3u4y1OFNO(ll}iyE8`lkQ)B~xdmG#+s zsQnLT-1z?SC(3-7Y&lLJd|-X@NmG_*HUyBmG6~WrxZwv1|ivA$w*_^7|MjW>iwQ6z3MS8)lVu|-t8vJW(swt)@q^V zYUWQH4bGhLm}&2)g`hZeQM76ldX$v`bGjOH$=~aW89#9|2NYnFl(KL^-&uBZ)$Z40 zG1}SQoYm5^t@?Up(@L%Ui{@^rh5DyD^a}QKg<>`N!|q zIE+PX2uuLkvLo^+N}O__2|}5Gt1|f^P>f{X4$CMDf@qa%sEL0d^q-8gQ|xhiQGgvjia^18M2SZH=Gx22PPo|in0 z?787L#0C>J(~wke68TA5J@fVeZgBu1{6o=wuhJfg{0(|@IdIi5G>!S*dpYMv6VrXT z3F0oe=5hf-_O7W_&K$zyRj3eO0?el?c24&0TZ426`dns?(XORE(f@Zfq{vf0GF3WaC93ZY zW}=IA?%hehnI=Pt^e-!SlkY*@JskM9-48r5-Q}h~`xElT+Zg#w5D;cpdVIF1_v*Ue zY<76=vRsr!Z{npgFvAu0S zmNc`90OFL&Maf$MaM>o`uOHGeug!v+qj;}5#!>~|#$?5wb>J5Oum*yZJ6brAj%|bFaJdzHwAzinT>+1^D{17BV0(qiw6*s(9n8L zrn9mW_?dQ2;hLP-aX4AMX3}Lw1nHIE#m6dsHAp)HL=VKBRw3#nV7J&?ub;~@1~If1 ze!;_xtPgNz`;;3JC{&7fNO_Ly$7-#(Rwr~Bn8j+{l6Ces;9R3~^OKyl_vt6z=()&W zJRRo9VfMbK5prxl{e9C+i&Rl-H5{@mih`!ahvVcPf85e6&+f_FEr^Im{FZ439mi#O zsc84Kmf_zuTU5hGRbhP^~;dEzt7599Hv%mnwg`uJD(5db)K8+^#V`O|^0jhaM0~8MrYVY&=hyW;M8&!1m$muf3MhSe@FT33qj`C1 z3ZYR9m(M4w$;V%w!4{9lNsVWF{TCA&39fgAC#y(rLNvRyXt7Q2q!SrTaq24Z0Rte* zPx^hh99?1H^t+~(hnw{4Cz*c=C+On?-nBov|DIb}wN!BgDKG#Ku#B%n04Q3~{@;t8 zc(b&|#{&+L4Fc@q#HG;P=NO*Gct;(SVMAmAaFJ;;+_)*9i*p1K$u+|bvId;Zr#*>A zKQ|CyWRQ)%m$hZO>|@)tCQKFdKwrPF(HBwI*hYPp0LV(DRHSEarD$YWa8W2|{XyMaE>Rr8_5nt`KO02Qnx2R9hCMRGKs}&7&=2k*2T;!2aW$gj@vt@@`z3%HY~CBX9Z_FF%m6n} zu8nzZJb}CdYBQ$NOnj~Z-ABbB48#aR)=L3FnwP}<(l!`F{E*-9;0J_Zc|jOZ03_RbV} z750WACc?Bf#H!JSsJ$>XIyyt(7uJDJgu;(X&jBB*=%hx?O0o_glDinZ8cn%Xr)|_( zd7dT31QJOinG{kbq!xM-kGQ~e2FesQB5*jw3Q?U{^HCa2MT7@(EbS380 z;M?~q%}$zn_w}rPKlXlhxM+TEP&lm?eIerRz4gc^|1Dk;wOi-;(`V6}+ht;781Zol z=Or?el3A%KY3UosGc&Ssvahkdc6qt^3T)YNBQQ+McKYA{j_dK(RD3AO&&LHq5h0dH zW%Bm@S}09arfM^Djn=}l7mcj#+1S@PaA>P{h!Hx&4`nm%oeN7?&zhD zUgLS=tq)##KLxnDxqEneHEaaeRntaYF{9K4x7;@F*GE3SeVOM^kQB|Z950BHtf(5T zPH!-p%oeN7?r^#?dE~wa9zN^w`Z{RXKiZ+L3FhK}&lfIN9jBxlm551_v1=)76Cm;e zT8JTg!h3W1bR1Ge+v!Ywr%L^A*ENNU)nG%Foj1jl#-)%xtg^u~#QTXSG=N(9ky1q? z^a-lLckl~WFL6S{`m1&zV^;Mvm6=iOzVR_~vyWqbRwV6ds%u_NWma$Z?I1Rn(X`V+ z(Va#8QtHMU>;mEMK5*6b%t{G&z`@~F_ysnif6wo!1Y)*3>lNeE3@57ONA3an z3uDiSepzD*Sy#;t*)b~_<|*D9e0dJ<{z#we8qL`nj%TDq1sA2jaw=BZdENW$Yv!hw z*~g2JLbtDWhwj*w4toki1CC)fLTEo6=D=x4nQ-=u87+B&l>#P>EsqeC1+dUsd(pG- zI5W1qb3jHG`4|%4?&NRFytlGFQuuztb13o&KKI$NqdSvL5#a`H)6QOw5rGz^th$$# zqn0WEjZ1*L!*Cpf&cCPPnBw+i9Dv)GL#HhCUl(v7yj`d8wn`rSJB3+Y6bF4?j##Tn zy%+=(96!$geI2O1!a=$CuX)lhchm8j9DDW3zV{lgc=77KCQ9Rr$Aojz5&A>B>}X=@ zR{%-p5Bw(sH{j`Pm9`sOyHsFFm@?e!D(Aw_l0pWue`)cGH@xFRK7YmupLbQ)fm!IQ zsA~z%G6k1rit}D+en9S*=J=1d*t5wfFd?g;H6|7HwiBVSN8gc-CoBS^ofb~xXzmPt}< zyKP-SYH - - - - {% compress css %} + From eb63a8081bba850baa02096f666d419052e6e380 Mon Sep 17 00:00:00 2001 From: liangliangyy Date: Thu, 7 Feb 2019 21:08:44 +0800 Subject: [PATCH 049/593] =?UTF-8?q?=E7=BC=93=E5=AD=98=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DjangoBlog/blog_signals.py | 7 +-- DjangoBlog/utils.py | 19 +++++-- accounts/tests.py | 4 +- blog/models.py | 6 +-- blog/views.py | 1 + oauth/oauthmanager.py | 10 ++-- oauth/templatetags/oauth_tags.py | 13 +++-- templates/blog/article_detail.html | 3 +- templates/blog/tags/article_info.html | 5 +- templates/blog/tags/article_meta_info.html | 59 ++++++++++++---------- templates/comments/tags/comment_list.html | 39 +++++++------- templates/comments/tags/post_comment.html | 20 -------- 12 files changed, 97 insertions(+), 89 deletions(-) diff --git a/DjangoBlog/blog_signals.py b/DjangoBlog/blog_signals.py index 1abe76bd8..edc0ea94d 100644 --- a/DjangoBlog/blog_signals.py +++ b/DjangoBlog/blog_signals.py @@ -22,7 +22,7 @@ from django.db.models.signals import post_save from django.contrib.auth.signals import user_logged_in, user_logged_out, user_login_failed -from DjangoBlog.utils import cache, send_email, expire_view_cache, get_blog_setting, delete_view_cache +from DjangoBlog.utils import cache, send_email, expire_view_cache, delete_sidebar_cache, delete_view_cache from DjangoBlog.spider_notify import SpiderNotify from oauth.models import OAuthUser from blog.models import Article, Category, Tag, Links, SideBar, BlogSettings @@ -103,7 +103,8 @@ def model_post_save_callback(sender, instance, created, raw, using, update_field cache.delete('seo_processor') comment_cache_key = 'article_comments_{id}'.format(id=instance.article.id) cache.delete(comment_cache_key) - delete_view_cache(instance.author.username) + delete_sidebar_cache(instance.author.username) + delete_view_cache('article_comments', [str(instance.article.pk)]) _thread.start_new(send_comment_email, (instance,)) @@ -116,5 +117,5 @@ def model_post_save_callback(sender, instance, created, raw, using, update_field def user_auth_callback(sender, request, user, **kwargs): if user and user.username: logger.info(user) - delete_view_cache(user.username) + delete_sidebar_cache(user.username) cache.clear() diff --git a/DjangoBlog/utils.py b/DjangoBlog/utils.py index 619db638a..75555c7a2 100644 --- a/DjangoBlog/utils.py +++ b/DjangoBlog/utils.py @@ -42,7 +42,6 @@ def get_md5(str): def cache_decorator(expiration=3 * 60): def wrapper(func): def news(*args, **kwargs): - key = '' try: view = args[0] key = view.get_cache_key() @@ -57,11 +56,17 @@ def news(*args, **kwargs): value = cache.get(key) if value: # logger.info('cache_decorator get cache:%s key:%s' % (func.__name__, key)) - return value + if repr(value) == 'default': + return None + else: + return value else: logger.info('cache_decorator set cache:%s key:%s' % (func.__name__, key)) value = func(*args, **kwargs) - cache.set(key, value, expiration) + if not value: + cache.set(key, 'default', expiration) + else: + cache.set(key, value, expiration) return value return news @@ -246,10 +251,16 @@ def save_user_avatar(url): return url -def delete_view_cache(username): +def delete_sidebar_cache(username): from django.core.cache.utils import make_template_fragment_key from blog.models import LINK_SHOW_TYPE keys = (make_template_fragment_key('sidebar', [username + x[0]]) for x in LINK_SHOW_TYPE) for k in keys: logger.info('delete sidebar key:' + k) cache.delete(k) + + +def delete_view_cache(prefix, keys): + from django.core.cache.utils import make_template_fragment_key + key = make_template_fragment_key(prefix, keys) + cache.delete(key) diff --git a/accounts/tests.py b/accounts/tests.py index 7fdeababd..0d196d634 100644 --- a/accounts/tests.py +++ b/accounts/tests.py @@ -1,7 +1,7 @@ from django.test import Client, RequestFactory, TestCase from blog.models import Article, Category, Tag from django.contrib.auth import get_user_model -from DjangoBlog.utils import get_current_site +from DjangoBlog.utils import delete_view_cache, delete_sidebar_cache import datetime from accounts.models import BlogUser from django.urls import reverse @@ -59,7 +59,7 @@ def test_validate_register(self): user.is_superuser = True user.is_staff = True user.save() - delete_view_cache(user.username) + delete_sidebar_cache(user.username) category = Category() category.name = "categoryaaa" category.created_time = datetime.datetime.now() diff --git a/blog/models.py b/blog/models.py index a7e981d8d..36032b4b7 100644 --- a/blog/models.py +++ b/blog/models.py @@ -117,7 +117,7 @@ def comment_list(self): return value else: comments = self.comment_set.filter(is_enable=True) - cache.set(cache_key, comments) + cache.set(cache_key, comments, 60 * 100) logger.info('set article comments:{id}'.format(id=self.id)) return comments @@ -125,12 +125,12 @@ def get_admin_url(self): info = (self._meta.app_label, self._meta.model_name) return reverse('admin:%s_%s_change' % info, args=(self.pk,)) - @cached_property + @cache_decorator(expiration=60 * 100) def next_article(self): # 下一篇 return Article.objects.filter(id__gt=self.id, status='p').order_by('id').first() - @cached_property + @cache_decorator(expiration=60 * 100) def prev_article(self): # 前一篇 return Article.objects.filter(id__lt=self.id, status='p').first() diff --git a/blog/views.py b/blog/views.py index b4205a9e9..b57227899 100644 --- a/blog/views.py +++ b/blog/views.py @@ -87,6 +87,7 @@ class IndexView(ArticleListView): ''' 首页 ''' + # 友情链接类型 link_type = 'i' def get_queryset_data(self): diff --git a/oauth/oauthmanager.py b/oauth/oauthmanager.py index 3d2523552..5d7521a14 100644 --- a/oauth/oauthmanager.py +++ b/oauth/oauthmanager.py @@ -20,7 +20,7 @@ import json import logging import urllib.parse -from DjangoBlog.utils import parse_dict_to_url +from DjangoBlog.utils import parse_dict_to_url, cache_decorator logger = logging.getLogger(__name__) @@ -426,6 +426,7 @@ def get_oauth_userinfo(self): return user +@cache_decorator(expiration=100 * 60) def get_oauth_apps(): configs = OAuthConfig.objects.filter(is_enable=True).all() if not configs: @@ -438,7 +439,8 @@ def get_oauth_apps(): def get_manager_by_type(type): applications = get_oauth_apps() - finds = list(filter(lambda x: x.ICON_NAME.lower() == type.lower(), applications)) - if finds: - return finds[0] + if applications: + finds = list(filter(lambda x: x.ICON_NAME.lower() == type.lower(), applications)) + if finds: + return finds[0] return None diff --git a/oauth/templatetags/oauth_tags.py b/oauth/templatetags/oauth_tags.py index 042fee0da..8ab09d3f2 100644 --- a/oauth/templatetags/oauth_tags.py +++ b/oauth/templatetags/oauth_tags.py @@ -23,12 +23,15 @@ @register.inclusion_tag('oauth/oauth_applications.html') def load_oauth_applications(request): applications = get_oauth_apps() - baseurl = reverse('oauth:oauthlogin') - path = request.get_full_path() + if applications: + baseurl = reverse('oauth:oauthlogin') + path = request.get_full_path() - apps = list(map(lambda x: (x.ICON_NAME, - '{baseurl}?type={type}&next_url={next}' - .format(baseurl=baseurl, type=x.ICON_NAME, next=path)), applications)) + apps = list(map(lambda x: (x.ICON_NAME, + '{baseurl}?type={type}&next_url={next}' + .format(baseurl=baseurl, type=x.ICON_NAME, next=path)), applications)) + else: + apps = [] return { 'apps': apps } diff --git a/templates/blog/article_detail.html b/templates/blog/article_detail.html index 2931b2d04..f4044105f 100755 --- a/templates/blog/article_detail.html +++ b/templates/blog/article_detail.html @@ -51,8 +51,7 @@

文章导航

{% if article.comment_status == "o" and OPEN_SITE_COMMENT %} - {% comment %}{% load comments_tags %} - {% load_post_comment article from %}{% endcomment %} + {% include 'comments/tags/comment_list.html' %} {% if user.is_authenticated %} {% include 'comments/tags/post_comment.html' %} diff --git a/templates/blog/tags/article_info.html b/templates/blog/tags/article_info.html index 50ff92c6d..4f47c20d2 100644 --- a/templates/blog/tags/article_info.html +++ b/templates/blog/tags/article_info.html @@ -1,4 +1,5 @@ {% load blog_tags %} +{% load cache %}
@@ -31,7 +32,9 @@


{% if article.type == 'a' %} {% if not isindex %} - {% load_breadcrumb article %} + {% cache 36000 breadcrumb article.pk %} + {% load_breadcrumb article %} + {% endcache %} {% endif %} {% endif %}

diff --git a/templates/blog/tags/article_meta_info.html b/templates/blog/tags/article_meta_info.html index 92e53b4b4..4ea817350 100644 --- a/templates/blog/tags/article_meta_info.html +++ b/templates/blog/tags/article_meta_info.html @@ -1,29 +1,33 @@ {% load blog_tags %} -
+ + {% endcache %} +{% endwith %} \ No newline at end of file diff --git a/templates/comments/tags/comment_list.html b/templates/comments/tags/comment_list.html index d3dab0883..44a15a876 100644 --- a/templates/comments/tags/comment_list.html +++ b/templates/comments/tags/comment_list.html @@ -1,38 +1,39 @@
{% load blog_tags %} {% load comments_tags %} - {% comment %}

{{ comment_count }} 条回复

{% endcomment %} + {% load cache %} {% if article_comments %} -
- -
    - {% query article_comments parent_comment=None as parent_comments %} - {% for comment in parent_comments %} - {% show_comment_item comment False %} + {% cache 36000 article_comments article.id %} +
    - {% parse_commenttree article_comments comment as childcomments %} - {% if childcomments %} -
      - {% for child in childcomments %} - {% show_comment_item child True %} +
        + {% query article_comments parent_comment=None as parent_comments %} + {% for comment in parent_comments %} + {% show_comment_item comment False %} - {% endfor %} -
    - {% endif %} + {% parse_commenttree article_comments comment as childcomments %} + {% if childcomments %} +
      + {% for child in childcomments %} + {% show_comment_item child True %} + {% endfor %} +
    + {% endif %} - {% endfor %} -
+ {% endfor %} + -
+ + {% endcache %} {% endif %}
diff --git a/templates/comments/tags/post_comment.html b/templates/comments/tags/post_comment.html index 8f4ada2ef..5064f44ee 100644 --- a/templates/comments/tags/post_comment.html +++ b/templates/comments/tags/post_comment.html @@ -7,18 +7,11 @@

发表评论

{% csrf_token %} - {% comment %}

电子邮件地址不会被公开。 必填项已用*标注 -

{% endcomment %}

{{ form.body.label_tag }} {{ form.body }} {{ form.body.errors }} - {% comment %} {% endcomment %}

{% if not form.name.is_hidden %} @@ -26,26 +19,13 @@

发表评论 {% endif %} {{ form.name }} {{ form.name.errors }} - {% comment %} -

{% endcomment %} - - {% comment %}

- {{ form.url.label_tag }} - {{ form.url }} - {{ form.url.errors }} - -

{% endcomment %} {{ form.parent_comment_id }}
支持markdown From 517d03ddd312843c0dfa59ac497fb70cce670013 Mon Sep 17 00:00:00 2001 From: liangliangyy Date: Fri, 8 Feb 2019 14:04:12 +0800 Subject: [PATCH 050/593] =?UTF-8?q?=E4=BF=AE=E5=A4=8Doauth=E7=99=BB?= =?UTF-8?q?=E5=BD=95bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DjangoBlog/blog_signals.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DjangoBlog/blog_signals.py b/DjangoBlog/blog_signals.py index edc0ea94d..198f3ffe7 100644 --- a/DjangoBlog/blog_signals.py +++ b/DjangoBlog/blog_signals.py @@ -71,7 +71,7 @@ def oauth_user_login_signal_handler(sender, **kwargs): oauthuser.picture = save_user_avatar(oauthuser.picture) oauthuser.save() - delete_view_cache(oauthuser.author.username) + delete_sidebar_cache(oauthuser.author.username) cache.clear() From 0832177915bd19cd664f16bd3a46ccafe8700efa Mon Sep 17 00:00:00 2001 From: liangliangyy Date: Wed, 13 Feb 2019 14:29:49 +0800 Subject: [PATCH 051/593] =?UTF-8?q?=E5=8D=87=E7=BA=A7=E4=BE=9D=E8=B5=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- requirements.txt | 4 ++-- travis_test/requirements.txt | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/requirements.txt b/requirements.txt index 6d0684a63..50e30138a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ cffi==1.11.5 chardet==3.0.4 coverage==4.5.2 cryptography==2.5 -Django==2.1.5 +Django==2.1.7 django-appconf==1.0.2 django-autoslug==1.9.4 django-compressor==2.2 @@ -47,4 +47,4 @@ webencodings==0.5.1 WeRoBot==1.7.0 Whoosh==2.7.4 wrapt==1.11.1 -xmltodict==0.11.0 +xmltodict==0.12.0 diff --git a/travis_test/requirements.txt b/travis_test/requirements.txt index 31e3b9d2a..76b2ed604 100644 --- a/travis_test/requirements.txt +++ b/travis_test/requirements.txt @@ -7,7 +7,7 @@ cffi==1.11.5 chardet==3.0.4 coverage==4.5.2 cryptography==2.5 -Django==2.1.5 +Django==2.1.7 django-appconf==1.0.2 django-autoslug==1.9.4 django-compressor==2.2 @@ -46,4 +46,4 @@ webencodings==0.5.1 WeRoBot==1.7.0 Whoosh==2.7.4 wrapt==1.11.1 -xmltodict==0.11.0 \ No newline at end of file +xmltodict==0.12.0 From 1e881e1647e3ac95b854a008d31eb20bbf786007 Mon Sep 17 00:00:00 2001 From: liangliangyy Date: Wed, 13 Feb 2019 23:42:34 +0800 Subject: [PATCH 052/593] =?UTF-8?q?=E8=AF=84=E8=AE=BA=E5=B1=82=E7=BA=A7?= =?UTF-8?q?=E6=A0=B7=E5=BC=8F=E4=BF=AE=E6=94=B9=20close=20#212?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../comments/tags/comment_item_tree.html | 44 +++++++++++++++++++ templates/comments/tags/comment_list.html | 20 ++------- 2 files changed, 48 insertions(+), 16 deletions(-) create mode 100644 templates/comments/tags/comment_item_tree.html diff --git a/templates/comments/tags/comment_item_tree.html b/templates/comments/tags/comment_item_tree.html new file mode 100644 index 000000000..676b7927c --- /dev/null +++ b/templates/comments/tags/comment_item_tree.html @@ -0,0 +1,44 @@ +{% load blog_tags %} +
  • +
    + + + + +

    {{ comment_item.body |escape|custom_markdown }}

    + + +
    + +
  • +{% query article_comments parent_comment=comment_item as cc_comments %} +{% for cc in cc_comments %} + {% with comment_item=cc template_name="comments/tags/comment_item_tree.html" %} + {% with depth=depth|add:1 %} + {% include template_name %} + {% endwith %} + {% endwith %} +{% endfor %} \ No newline at end of file diff --git a/templates/comments/tags/comment_list.html b/templates/comments/tags/comment_list.html index 44a15a876..79d36c0fe 100644 --- a/templates/comments/tags/comment_list.html +++ b/templates/comments/tags/comment_list.html @@ -11,28 +11,16 @@ {% if article_comments %} {% cache 36000 article_comments article.id %}
    -
      {% query article_comments parent_comment=None as parent_comments %} - {% for comment in parent_comments %} - {% show_comment_item comment False %} - - {% parse_commenttree article_comments comment as childcomments %} - {% if childcomments %} -
        - {% for child in childcomments %} - {% show_comment_item child True %} - - {% endfor %} -
      - {% endif %} - - + {% for comment_item in parent_comments %} + {% with 0 as depth %} + {% include "comments/tags/comment_item_tree.html" %} + {% endwith %} {% endfor %}
    -
    {% endcache %} {% endif %} From 98cfc508705234013a27fc9a7bee399abdbc5200 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=94=E5=90=AC=E9=A3=8E=E5=90=9F?= Date: Fri, 15 Feb 2019 17:21:16 +0800 Subject: [PATCH 053/593] Update README.md --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d1ae0c155..4a32b3f0a 100644 --- a/README.md +++ b/README.md @@ -94,4 +94,10 @@ CREATE DATABASE `djangoblog` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8 [更多配置介绍](/bin/config.md) ## 问题相关 - 有任何问题欢迎提Issue,或者将问题描述发送至我邮箱 `liangliangyy#gmail.com`.我会尽快解答.推荐提交Issue方式. + 有任何问题欢迎提Issue,或者将问题描述发送至我邮箱 `liangliangyy#gmail.com`.我会尽快解答.推荐提交Issue方式. + + --- + ## 致大家🙋‍♀️🙋‍♂️ + 如果本项目帮助到了你,请在[这里](https://github.com/liangliangyy/DjangoBlog/issues/214)留下你的网址,让更多的人看到。 +您的回复将会是我继续更新维护下去的动力。 +🙏🙏🙏 From ae769f9d1d05b39ecce736deb4448c64984a9483 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=94=E5=90=AC=E9=A3=8E=E5=90=9F?= Date: Fri, 22 Feb 2019 21:28:14 +0800 Subject: [PATCH 054/593] Update config.md --- bin/config.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/bin/config.md b/bin/config.md index 8a7093398..b626333c3 100644 --- a/bin/config.md +++ b/bin/config.md @@ -53,3 +53,10 @@ SERVER_EMAIL = os.environ.get('DJANGO_EMAIL_USER') 也就是说,需要在代码块开始位置加入这段代码对应的语言。 + +## update +如果你发现执行数据库迁移的时候出现如下报错: +```python +django.db.migrations.exceptions.MigrationSchemaMissing: Unable to create the django_migrations table ((1064, "You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '(6) NOT NULL)' at line 1")) +``` +可能是因为你的mysql版本低于5.6,需要升级mysql版本>=5.6即可。 From cbea49285c9ed228c1f8446d05c3353db658aca2 Mon Sep 17 00:00:00 2001 From: liangliangyy Date: Sun, 10 Mar 2019 19:40:33 +0800 Subject: [PATCH 055/593] =?UTF-8?q?=E6=9B=BF=E6=8D=A2markdown=E7=BC=96?= =?UTF-8?q?=E8=BE=91=E5=99=A8=20close=20#216=20close=20#224?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DjangoBlog/settings.py | 6 +++++- DjangoBlog/urls.py | 3 ++- blog/admin.py | 3 +-- blog/models.py | 3 ++- requirements.txt | 24 +++++++++++++----------- travis_test/requirements.txt | 24 +++++++++++++----------- 6 files changed, 36 insertions(+), 27 deletions(-) diff --git a/DjangoBlog/settings.py b/DjangoBlog/settings.py index 819363442..8ec8d4512 100644 --- a/DjangoBlog/settings.py +++ b/DjangoBlog/settings.py @@ -44,7 +44,7 @@ 'django.contrib.staticfiles', 'django.contrib.sites', 'django.contrib.sitemaps', - 'pagedown', + 'mdeditor', 'haystack', 'blog', 'accounts', @@ -284,3 +284,7 @@ COMPRESS_JS_FILTERS = [ 'compressor.filters.jsmin.JSMinFilter' ] + + +MEDIA_ROOT = os.path.join(BASE_DIR, 'uploads') +MEDIA_URL = '/media/' diff --git a/DjangoBlog/urls.py b/DjangoBlog/urls.py index e320807a7..a444aeb78 100644 --- a/DjangoBlog/urls.py +++ b/DjangoBlog/urls.py @@ -22,6 +22,7 @@ from django.conf import settings from django.conf.urls.static import static from DjangoBlog.admin_site import admin_site +from django.urls import include, path sitemaps = { @@ -38,7 +39,7 @@ urlpatterns = [ url(r'^admin/', admin_site.urls), url(r'', include('blog.urls', namespace='blog')), - + url(r'mdeditor/', include('mdeditor.urls')), url(r'', include('comments.urls', namespace='comment')), url(r'', include('accounts.urls', namespace='account')), url(r'', include('oauth.urls', namespace='oauth')), diff --git a/blog/admin.py b/blog/admin.py index 30b5c3464..898686e53 100644 --- a/blog/admin.py +++ b/blog/admin.py @@ -1,7 +1,6 @@ from django.contrib import admin # Register your models here. from .models import Article, Category, Tag, Links, SideBar, BlogSettings -from pagedown.widgets import AdminPagedownWidget from django import forms from django.contrib.auth import get_user_model from django.utils.translation import ugettext_lazy as _ @@ -27,7 +26,7 @@ def queryset(self, request, queryset): class ArticleForm(forms.ModelForm): - body = forms.CharField(widget=AdminPagedownWidget()) + # body = forms.CharField(widget=AdminPagedownWidget()) class Meta: model = Article diff --git a/blog/models.py b/blog/models.py index 36032b4b7..b13c9b920 100644 --- a/blog/models.py +++ b/blog/models.py @@ -11,6 +11,7 @@ from DjangoBlog.utils import cache_decorator, cache from django.utils.functional import cached_property from django.utils.timezone import now +from mdeditor.fields import MDTextField logger = logging.getLogger(__name__) @@ -67,7 +68,7 @@ class Article(BaseModel): ('p', '页面'), ) title = models.CharField('标题', max_length=200, unique=True) - body = models.TextField('正文') + body = MDTextField('正文') pub_time = models.DateTimeField('发布时间', blank=True, null=True) status = models.CharField('文章状态', max_length=1, choices=STATUS_CHOICES, default='p') comment_status = models.CharField('评论状态', max_length=1, choices=COMMENT_STATUS, default='o') diff --git a/requirements.txt b/requirements.txt index 50e30138a..67ea8cb86 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,23 +1,23 @@ appdirs==1.4.3 asn1crypto==0.24.0 -astroid==2.1.0 +astroid==2.2.5 bottle==0.12.16 -certifi==2018.11.29 -cffi==1.11.5 +certifi==2019.3.9 +cffi==1.12.2 chardet==3.0.4 coverage==4.5.2 -cryptography==2.5 +cryptography==2.6.1 Django==2.1.7 -django-appconf==1.0.2 +django-appconf==1.0.3 django-autoslug==1.9.4 django-compressor==2.2 django-debug-toolbar==1.11 django-haystack==2.8.1 django-ipware==2.1.0 -django-pagedown==1.0.6 +django-mdeditor==0.1.13 django-uuslug==1.1.8 idna==2.8 -isort==4.3.4 +isort==4.3.14 jieba==0.39 jsonpickle==1.1 lazy-object-proxy==1.3.1 @@ -29,22 +29,24 @@ packaging==19.0 Pillow==5.4.1 pycparser==2.19 Pygments==2.3.1 -pylint==2.2.2 +pylint==2.3.1 PyMySQL==0.9.3 pyparsing==2.3.1 python-memcached==1.59 -python-slugify==2.0.1 +python-slugify==3.0.0 pytz==2018.9 raven==6.10.0 rcssmin==1.0.6 requests==2.21.0 -rjsmin==1.0.12 +rjsmin==1.1.0 six==1.12.0 sqlparse==0.2.4 +text-unidecode==1.2 +typed-ast==1.3.1 Unidecode==1.0.23 urllib3==1.24.1 webencodings==0.5.1 -WeRoBot==1.7.0 +WeRoBot==1.8.0 Whoosh==2.7.4 wrapt==1.11.1 xmltodict==0.12.0 diff --git a/travis_test/requirements.txt b/travis_test/requirements.txt index 76b2ed604..cae853180 100644 --- a/travis_test/requirements.txt +++ b/travis_test/requirements.txt @@ -1,23 +1,23 @@ appdirs==1.4.3 asn1crypto==0.24.0 -astroid==2.1.0 +astroid==2.2.5 bottle==0.12.16 -certifi==2018.11.29 -cffi==1.11.5 +certifi==2019.3.9 +cffi==1.12.2 chardet==3.0.4 coverage==4.5.2 -cryptography==2.5 +cryptography==2.6.1 Django==2.1.7 -django-appconf==1.0.2 +django-appconf==1.0.3 django-autoslug==1.9.4 django-compressor==2.2 django-debug-toolbar==1.11 django-haystack==2.8.1 django-ipware==2.1.0 -django-pagedown==1.0.6 +django-mdeditor==0.1.13 django-uuslug==1.1.8 idna==2.8 -isort==4.3.4 +isort==4.3.14 jieba==0.39 jsonpickle==1.1 lazy-object-proxy==1.3.1 @@ -29,21 +29,23 @@ packaging==19.0 Pillow==5.4.1 pycparser==2.19 Pygments==2.3.1 -pylint==2.2.2 +pylint==2.3.1 PyMySQL==0.9.3 pyparsing==2.3.1 -python-slugify==2.0.1 +python-slugify==3.0.0 pytz==2018.9 raven==6.10.0 rcssmin==1.0.6 requests==2.21.0 -rjsmin==1.0.12 +rjsmin==1.1.0 six==1.12.0 sqlparse==0.2.4 +text-unidecode==1.2 +typed-ast==1.3.1 Unidecode==1.0.23 urllib3==1.24.1 webencodings==0.5.1 -WeRoBot==1.7.0 +WeRoBot==1.8.0 Whoosh==2.7.4 wrapt==1.11.1 xmltodict==0.12.0 From 157af06700eedb27e6c7ae07bd445ff224b98a2d Mon Sep 17 00:00:00 2001 From: liangliangyy Date: Sun, 10 Mar 2019 19:48:55 +0800 Subject: [PATCH 056/593] update --- travis_test/travis_settings.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/travis_test/travis_settings.py b/travis_test/travis_settings.py index 000e38855..35cfc061b 100644 --- a/travis_test/travis_settings.py +++ b/travis_test/travis_settings.py @@ -32,8 +32,10 @@ ALLOWED_HOSTS = ['www.lylinux.net', '127.0.0.1', 'example.com'] # Application definition + INSTALLED_APPS = [ - 'django.contrib.admin', + # 'django.contrib.admin', + 'django.contrib.admin.apps.SimpleAdminConfig', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', @@ -41,7 +43,7 @@ 'django.contrib.staticfiles', 'django.contrib.sites', 'django.contrib.sitemaps', - 'pagedown', + 'mdeditor', 'haystack', 'blog', 'accounts', From 10ca1f2aceff1efdeea4c32cf30298dae40953b2 Mon Sep 17 00:00:00 2001 From: liangliangyy Date: Sat, 23 Mar 2019 22:02:19 +0800 Subject: [PATCH 057/593] =?UTF-8?q?=E5=8D=87=E7=BA=A7=E4=BE=9D=E8=B5=96=20?= =?UTF-8?q?close=20#229=20#232?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DjangoBlog/spider_notify.py | 2 +- accounts/views.py | 6 ++++-- blog/forms.py | 6 ++++-- blog/search_indexes.py | 14 +------------- blog/views.py | 4 ++-- requirements.txt | 6 +++--- travis_test/requirements.txt | 7 ++++--- 7 files changed, 19 insertions(+), 26 deletions(-) diff --git a/DjangoBlog/spider_notify.py b/DjangoBlog/spider_notify.py index c13835cab..e83ccb147 100644 --- a/DjangoBlog/spider_notify.py +++ b/DjangoBlog/spider_notify.py @@ -27,7 +27,7 @@ def baidu_notify(urls): try: data = '\n'.join(urls) result = requests.post(settings.BAIDU_NOTIFY_URL, data=data) - print(result.text) + logger.info(result.text) except Exception as e: logger.error(e) diff --git a/accounts/views.py b/accounts/views.py index a3ee777df..0e64e1690 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -1,5 +1,5 @@ from django.shortcuts import render - +import logging from .forms import RegisterForm, LoginForm from django.contrib.auth import authenticate, login, logout # from django.views.generic.edit import FormView @@ -17,6 +17,8 @@ from django.views.decorators.debug import sensitive_post_parameters from django.utils.http import is_safe_url +logger = logging.getLogger(__name__) + # Create your views here. @@ -73,7 +75,7 @@ def form_valid(self, form): from DjangoBlog.utils import cache if cache and cache is not None: cache.clear() - print(self.redirect_field_name) + logger.info(self.redirect_field_name) redirect_to = self.request.GET.get(self.redirect_field_name) auth.login(self.request, form.get_user()) return super(LoginView, self).form_valid(form) diff --git a/blog/forms.py b/blog/forms.py index 3c09fc3ac..1df8023bc 100644 --- a/blog/forms.py +++ b/blog/forms.py @@ -15,7 +15,9 @@ from haystack.forms import SearchForm from django import forms -from blog.models import Article, Category +import logging + +logger = logging.getLogger(__name__) class BlogSearchForm(SearchForm): @@ -27,5 +29,5 @@ def search(self): return self.no_query_found() if self.cleaned_data['querydata']: - print(self.cleaned_data['querydata']) + logger.info(self.cleaned_data['querydata']) return datas diff --git a/blog/search_indexes.py b/blog/search_indexes.py index 9d0268e18..f48ae3269 100644 --- a/blog/search_indexes.py +++ b/blog/search_indexes.py @@ -18,23 +18,11 @@ class ArticleIndex(indexes.SearchIndex, indexes.Indexable): - # title = indexes.CharField(document=True, use_template=True) text = indexes.CharField(document=True, use_template=True) + author = indexes.CharField(model_attr='author') def get_model(self): return Article def index_queryset(self, using=None): return self.get_model().objects.filter(status='p') - - -""" -class CategoryIndex(indexes.SearchIndex, indexes.Indexable): - name = indexes.CharField(document=True, use_template=True) - - def get_model(self): - return Article - - def index_queryset(self, using=None): - return self.get_model().objects.filter(status='p') -""" diff --git a/blog/views.py b/blog/views.py index b57227899..119f9cfdb 100644 --- a/blog/views.py +++ b/blog/views.py @@ -188,7 +188,7 @@ def get_queryset_cache_key(self): def get_queryset_data(self): author_name = self.kwargs['author_name'] - article_list = Article.objects.filter(author__username=author_name) + article_list = Article.objects.filter(author__username=author_name, type='a', status='p') return article_list def get_context_data(self, **kwargs): @@ -209,7 +209,7 @@ def get_queryset_data(self): tag = get_object_or_404(Tag, slug=slug) tag_name = tag.name self.name = tag_name - article_list = Article.objects.filter(tags__name=tag_name) + article_list = Article.objects.filter(tags__name=tag_name, type='a', status='p') return article_list def get_queryset_cache_key(self): diff --git a/requirements.txt b/requirements.txt index 67ea8cb86..74c3113ef 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ bottle==0.12.16 certifi==2019.3.9 cffi==1.12.2 chardet==3.0.4 -coverage==4.5.2 +coverage==4.5.3 cryptography==2.6.1 Django==2.1.7 django-appconf==1.0.3 @@ -17,7 +17,7 @@ django-ipware==2.1.0 django-mdeditor==0.1.13 django-uuslug==1.1.8 idna==2.8 -isort==4.3.14 +isort==4.3.15 jieba==0.39 jsonpickle==1.1 lazy-object-proxy==1.3.1 @@ -40,7 +40,7 @@ rcssmin==1.0.6 requests==2.21.0 rjsmin==1.1.0 six==1.12.0 -sqlparse==0.2.4 +sqlparse==0.3.0 text-unidecode==1.2 typed-ast==1.3.1 Unidecode==1.0.23 diff --git a/travis_test/requirements.txt b/travis_test/requirements.txt index cae853180..74c3113ef 100644 --- a/travis_test/requirements.txt +++ b/travis_test/requirements.txt @@ -5,7 +5,7 @@ bottle==0.12.16 certifi==2019.3.9 cffi==1.12.2 chardet==3.0.4 -coverage==4.5.2 +coverage==4.5.3 cryptography==2.6.1 Django==2.1.7 django-appconf==1.0.3 @@ -17,7 +17,7 @@ django-ipware==2.1.0 django-mdeditor==0.1.13 django-uuslug==1.1.8 idna==2.8 -isort==4.3.14 +isort==4.3.15 jieba==0.39 jsonpickle==1.1 lazy-object-proxy==1.3.1 @@ -32,6 +32,7 @@ Pygments==2.3.1 pylint==2.3.1 PyMySQL==0.9.3 pyparsing==2.3.1 +python-memcached==1.59 python-slugify==3.0.0 pytz==2018.9 raven==6.10.0 @@ -39,7 +40,7 @@ rcssmin==1.0.6 requests==2.21.0 rjsmin==1.1.0 six==1.12.0 -sqlparse==0.2.4 +sqlparse==0.3.0 text-unidecode==1.2 typed-ast==1.3.1 Unidecode==1.0.23 From e3d3f932f50d7b4f3c59fedbd78198829f32a16c Mon Sep 17 00:00:00 2001 From: liangliangyy Date: Sun, 31 Mar 2019 02:37:29 +0800 Subject: [PATCH 058/593] add es support --- requirements.txt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/requirements.txt b/requirements.txt index 74c3113ef..b0d1eebeb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,11 +12,15 @@ django-appconf==1.0.3 django-autoslug==1.9.4 django-compressor==2.2 django-debug-toolbar==1.11 +django-elasticsearch-dsl==0.5.1 django-haystack==2.8.1 django-ipware==2.1.0 django-mdeditor==0.1.13 django-uuslug==1.1.8 +elasticsearch==6.3.1 +elasticsearch-dsl==6.1.0 idna==2.8 +ipaddress==1.0.22 isort==4.3.15 jieba==0.39 jsonpickle==1.1 @@ -32,6 +36,8 @@ Pygments==2.3.1 pylint==2.3.1 PyMySQL==0.9.3 pyparsing==2.3.1 +python-dateutil==2.8.0 +python-logstash==0.4.6 python-memcached==1.59 python-slugify==3.0.0 pytz==2018.9 @@ -50,3 +56,4 @@ WeRoBot==1.8.0 Whoosh==2.7.4 wrapt==1.11.1 xmltodict==0.12.0 + From 928abc84f7ed845dd9a52df514c445b19b4383a9 Mon Sep 17 00:00:00 2001 From: liangliangyy Date: Sat, 13 Apr 2019 22:08:00 +0800 Subject: [PATCH 059/593] =?UTF-8?q?=E6=90=9C=E7=B4=A2=E6=94=AF=E6=8C=81es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DjangoBlog/elasticsearch_backend.py | 269 ++++++++++++++++++++++++++++ blog/documents.py | 60 +++++++ blog/models.py | 3 + servermanager/tests.py | 17 +- 4 files changed, 340 insertions(+), 9 deletions(-) create mode 100644 DjangoBlog/elasticsearch_backend.py create mode 100644 blog/documents.py diff --git a/DjangoBlog/elasticsearch_backend.py b/DjangoBlog/elasticsearch_backend.py new file mode 100644 index 000000000..b20715574 --- /dev/null +++ b/DjangoBlog/elasticsearch_backend.py @@ -0,0 +1,269 @@ +#!/usr/bin/env python +# encoding: utf-8 +""" +@version: ?? +@author: liangliangyy +@license: MIT Licence +@contact: liangliangyy@gmail.com +@site: https://www.lylinux.net/ +@software: PyCharm +@file: elasticsearch_backend.py +@time: 2019-04-13 11:46 +""" +import logging +import re +import json + +from datetime import datetime, timedelta + +from django.conf import settings +from django.core.exceptions import ImproperlyConfigured +from django.utils import six +from django.utils.datetime_safe import datetime +from django.utils.encoding import force_text + +from haystack.backends import BaseEngine, BaseSearchBackend, BaseSearchQuery, EmptyResults, log_query +from haystack.constants import DJANGO_CT, DJANGO_ID, ID +from haystack.exceptions import MissingDependency, SearchBackendError, SkipDocument +from haystack.inputs import Clean, Exact, PythonData, Raw +from haystack.models import SearchResult +from haystack.utils import log as logging +from haystack.utils import get_identifier, get_model_ct +from haystack.utils.app_loading import haystack_get_model +from django_elasticsearch_dsl.registries import registry + +from blog.models import Article +from blog.documents import ArticleDocument + +logger = logging.getLogger(__name__) + +DATETIME_REGEX = re.compile( + '^(?P\d{4})-(?P\d{2})-(?P\d{2})T(?P\d{2}):(?P\d{2}):(?P\d{2})(\.\d{3,6}Z?)?$') + + +class ElasticSearchBackend(BaseSearchBackend): + + def _get_models(self): + models = registry.get_models() + return set(models) + + def _create(self, models): + for index in registry.get_indices(models): + index.create() + + def _populate(self, models): + for doc in registry.get_documents(models): + qs = doc().get_queryset() + doc().update(qs) + + def _delete(self, models): + for index in registry.get_indices(models): + index.delete(ignore=404) + return True + + def _rebuild(self, models): + if not self._delete(models): + return + + self._create(models) + self._populate(models) + + def update(self, index, iterable, commit=True): + models = self._get_models() + self._rebuild(models) + + def remove(self, obj_or_string): + models = self._get_models() + self._delete(models) + + def clear(self, models=None, commit=True): + self.remove(None) + + @log_query + def search(self, query_string, **kwargs): + logger.info('search query_string:' + query_string) + + start_offset = kwargs.get('start_offset') + end_offset = kwargs.get('end_offset') + search = ArticleDocument.search() \ + .query("match", body=query_string) \ + .filter('term', status='p') \ + .filter('term', type='a') \ + [start_offset: end_offset] + results = search.execute() + + return self._process_results(raw_results=results) + + def _process_results(self, raw_results, highlight=False, + result_class=None, distance_point=None, + geo_sort=False): + from haystack import connections + results = [] + hits = raw_results['hits'].total + + facets = {} + spelling_suggestion = None + + if result_class is None: + result_class = SearchResult + if 'facets' in raw_results: + facets = { + 'fields': {}, + 'dates': {}, + 'queries': {}, + } + + # ES can return negative timestamps for pre-1970 data. Handle it. + def from_timestamp(tm): + if tm >= 0: + return datetime.utcfromtimestamp(tm) + else: + return datetime(1970, 1, 1) + timedelta(seconds=tm) + + for facet_fieldname, facet_info in raw_results['facets'].items(): + if facet_info.get('_type', 'terms') == 'terms': + facets['fields'][facet_fieldname] = [(individual['term'], individual['count']) for individual in + facet_info['terms']] + elif facet_info.get('_type', 'terms') == 'date_histogram': + # Elasticsearch provides UTC timestamps with an extra three + # decimals of precision, which datetime barfs on. + facets['dates'][facet_fieldname] = [(from_timestamp(individual['time'] / 1000), + individual['count']) + for individual in facet_info['entries']] + elif facet_info.get('_type', 'terms') == 'query': + facets['queries'][facet_fieldname] = facet_info['count'] + + unified_index = connections[self.connection_alias].get_unified_index() + + content_field = unified_index.document_field + # articleids = list(map(lambda x: x['_id'], raw_results['hits']['hits'])) + # article_results = list(Article.objects.filter(id__in=articleids)) + + for raw_result in raw_results['hits']['hits']: + app_label = 'blog' + model_name = 'Article' + additional_fields = {} + + if 'highlight' in raw_result: + additional_fields['highlighted'] = raw_result['highlight'].get(content_field, '') + + if distance_point: + additional_fields['_point_of_origin'] = distance_point + + if geo_sort and raw_result.get('sort'): + from haystack.utils.geo import Distance + additional_fields['_distance'] = Distance(km=float(raw_result['sort'][0])) + else: + additional_fields['_distance'] = None + + result = result_class(app_label, model_name, raw_result['_id'], raw_result['_score'], + **additional_fields) + results.append(result) + + return { + 'results': results, + 'hits': hits, + 'facets': facets, + 'spelling_suggestion': spelling_suggestion, + } + + def _from_python(self, value): + """ + Converts Python values to a string for Whoosh. + + Code courtesy of pysolr. + """ + if hasattr(value, 'strftime'): + if not hasattr(value, 'hour'): + value = datetime(value.year, value.month, value.day, 0, 0, 0) + elif isinstance(value, bool): + if value: + value = 'true' + else: + value = 'false' + elif isinstance(value, (list, tuple)): + value = u','.join([force_text(v) for v in value]) + elif isinstance(value, (six.integer_types, float)): + # Leave it alone. + pass + else: + value = force_text(value) + return value + + def _to_python(self, value): + """ + Converts values from Whoosh to native Python values. + + A port of the same method in pysolr, as they deal with data the same way. + """ + if value == 'true': + return True + elif value == 'false': + return False + + if value and isinstance(value, six.string_types): + possible_datetime = DATETIME_REGEX.search(value) + + if possible_datetime: + date_values = possible_datetime.groupdict() + + for dk, dv in date_values.items(): + date_values[dk] = int(dv) + + return datetime(date_values['year'], date_values['month'], date_values['day'], date_values['hour'], + date_values['minute'], date_values['second']) + + try: + # Attempt to use json to load the values. + converted_value = json.loads(value) + + # Try to handle most built-in types. + if isinstance(converted_value, (list, tuple, set, dict, six.integer_types, float, complex)): + return converted_value + except: + # If it fails (SyntaxError or its ilk) or we don't trust it, + # continue on. + pass + + return value + + +class ElasticSearchQuery(BaseSearchQuery): + def _convert_datetime(self, date): + if hasattr(date, 'hour'): + return force_text(date.strftime('%Y%m%d%H%M%S')) + else: + return force_text(date.strftime('%Y%m%d000000')) + + def clean(self, query_fragment): + """ + Provides a mechanism for sanitizing user input before presenting the + value to the backend. + + Whoosh 1.X differs here in that you can no longer use a backslash + to escape reserved characters. Instead, the whole word should be + quoted. + """ + words = query_fragment.split() + cleaned_words = [] + + for word in words: + if word in self.backend.RESERVED_WORDS: + word = word.replace(word, word.lower()) + + for char in self.backend.RESERVED_CHARACTERS: + if char in word: + word = "'%s'" % word + break + + cleaned_words.append(word) + + return ' '.join(cleaned_words) + + def build_query_fragment(self, field, filter_type, value): + return value.query_string + + +class ElasticSearchEngine(BaseEngine): + backend = ElasticSearchBackend + query = ElasticSearchQuery diff --git a/blog/documents.py b/blog/documents.py new file mode 100644 index 000000000..fbaa35593 --- /dev/null +++ b/blog/documents.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python +# encoding: utf-8 +""" +@version: ?? +@author: liangliangyy +@license: MIT Licence +@contact: liangliangyy@gmail.com +@site: https://www.lylinux.net/ +@software: PyCharm +@file: documents.py +@time: 2019-04-05 13:05 +""" + +from django_elasticsearch_dsl import DocType, Index, fields +from blog.models import Article, Category, Tag +from accounts.models import BlogUser + +blog = Index('blog') +blog.settings( + number_of_shards=1, + number_of_replicas=0 +) + + +@blog.doc_type +class ArticleDocument(DocType): + body = fields.TextField(attr='body_to_string', analyzer='ik_max_word') + title = fields.TextField(analyzer='ik_max_word') + author = fields.ObjectField(properties={ + 'nickname': fields.TextField(analyzer='ik_max_word'), + 'id': fields.IntegerField() + }) + category = fields.ObjectField(properties={ + 'name': fields.TextField(analyzer='ik_max_word'), + 'id': fields.IntegerField() + }) + tags = fields.ObjectField(properties={ + 'name': fields.TextField(analyzer='ik_max_word'), + 'id': fields.IntegerField() + }) + + # def get_instances_from_related(self, related_instance): + # if isinstance(related_instance, BlogUser): + # return related_instance + # elif isinstance(related_instance, Category): + # pass + + class Meta: + model = Article + fields = [ + 'pub_time', + 'status', + 'comment_status', + 'type', + 'views', + 'article_order', + + ] + # related_models = [Category, Tag, BlogUser] + doc_type = 'Article' diff --git a/blog/models.py b/blog/models.py index b13c9b920..1ac9854fb 100644 --- a/blog/models.py +++ b/blog/models.py @@ -79,6 +79,9 @@ class Article(BaseModel): category = models.ForeignKey('Category', verbose_name='分类', on_delete=models.CASCADE, blank=False, null=False) tags = models.ManyToManyField('Tag', verbose_name='标签集合', blank=True) + def body_to_string(self): + return self.body + def __str__(self): return self.title diff --git a/servermanager/tests.py b/servermanager/tests.py index b993778e0..5932a1d86 100644 --- a/servermanager/tests.py +++ b/servermanager/tests.py @@ -43,9 +43,8 @@ def test_validate_comment(self): article.status = 'p' article.save() s = TextMessage([]) - s.content = "nicetitleccc" + s.content = "nice" rsp = search(s, None) - self.assertTrue(rsp != '没有找到相关文章。') rsp = category(None, None) self.assertIsNotNone(rsp) rsp = recents(None, None) @@ -64,19 +63,19 @@ def test_validate_comment(self): s.content = 'test' msghandler = MessageHandler(s, {}) - #msghandler.userinfo.isPasswordSet = True - #msghandler.userinfo.isAdmin = True + # msghandler.userinfo.isPasswordSet = True + # msghandler.userinfo.isAdmin = True msghandler.handler() s.content = 'y' msghandler.handler() - s.content='idcard:12321233' + s.content = 'idcard:12321233' msghandler.handler() - s.content='weather:上海' + s.content = 'weather:上海' msghandler.handler() - s.content='admin' + s.content = 'admin' msghandler.handler() - s.content='123' + s.content = '123' msghandler.handler() s.content = 'exit' - msghandler.handler() \ No newline at end of file + msghandler.handler() From bf901f86ee9a835b97b5bd99288ccec3a7e21562 Mon Sep 17 00:00:00 2001 From: liangliangyy Date: Sat, 13 Apr 2019 22:08:46 +0800 Subject: [PATCH 060/593] =?UTF-8?q?=E5=8D=87=E7=BA=A7=E4=BE=9D=E8=B5=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- requirements.txt | 3 --- travis_test/requirements.txt | 10 +++++++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/requirements.txt b/requirements.txt index b0d1eebeb..6daa56a64 100644 --- a/requirements.txt +++ b/requirements.txt @@ -42,9 +42,7 @@ python-memcached==1.59 python-slugify==3.0.0 pytz==2018.9 raven==6.10.0 -rcssmin==1.0.6 requests==2.21.0 -rjsmin==1.1.0 six==1.12.0 sqlparse==0.3.0 text-unidecode==1.2 @@ -56,4 +54,3 @@ WeRoBot==1.8.0 Whoosh==2.7.4 wrapt==1.11.1 xmltodict==0.12.0 - diff --git a/travis_test/requirements.txt b/travis_test/requirements.txt index 74c3113ef..f9e771269 100644 --- a/travis_test/requirements.txt +++ b/travis_test/requirements.txt @@ -12,11 +12,15 @@ django-appconf==1.0.3 django-autoslug==1.9.4 django-compressor==2.2 django-debug-toolbar==1.11 +django-elasticsearch-dsl==0.5.1 django-haystack==2.8.1 django-ipware==2.1.0 django-mdeditor==0.1.13 django-uuslug==1.1.8 +elasticsearch==6.3.1 +elasticsearch-dsl==6.1.0 idna==2.8 +ipaddress==1.0.22 isort==4.3.15 jieba==0.39 jsonpickle==1.1 @@ -32,13 +36,13 @@ Pygments==2.3.1 pylint==2.3.1 PyMySQL==0.9.3 pyparsing==2.3.1 +python-dateutil==2.8.0 +python-logstash==0.4.6 python-memcached==1.59 python-slugify==3.0.0 pytz==2018.9 raven==6.10.0 -rcssmin==1.0.6 requests==2.21.0 -rjsmin==1.1.0 six==1.12.0 sqlparse==0.3.0 text-unidecode==1.2 @@ -49,4 +53,4 @@ webencodings==0.5.1 WeRoBot==1.8.0 Whoosh==2.7.4 wrapt==1.11.1 -xmltodict==0.12.0 +xmltodict==0.12.0 \ No newline at end of file From c816694b245b06812720cf72e4e3728f7d16d314 Mon Sep 17 00:00:00 2001 From: liangliangyy Date: Sat, 13 Apr 2019 23:16:19 +0800 Subject: [PATCH 061/593] =?UTF-8?q?=E7=A6=81=E7=94=A8=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DjangoBlog/elasticsearch_backend.py | 2 +- blog/documents.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/DjangoBlog/elasticsearch_backend.py b/DjangoBlog/elasticsearch_backend.py index b20715574..3e816d8e3 100644 --- a/DjangoBlog/elasticsearch_backend.py +++ b/DjangoBlog/elasticsearch_backend.py @@ -70,7 +70,7 @@ def _rebuild(self, models): def update(self, index, iterable, commit=True): models = self._get_models() - self._rebuild(models) + #self._rebuild(models) def remove(self, obj_or_string): models = self._get_models() diff --git a/blog/documents.py b/blog/documents.py index fbaa35593..3219587be 100644 --- a/blog/documents.py +++ b/blog/documents.py @@ -58,3 +58,6 @@ class Meta: ] # related_models = [Category, Tag, BlogUser] doc_type = 'Article' + auto_refresh = False + ignore_signals = True + From d0ac3b01ae3823e5c839127869f8ce39f3aacf39 Mon Sep 17 00:00:00 2001 From: liangliangyy Date: Sun, 14 Apr 2019 22:22:59 +0800 Subject: [PATCH 062/593] update elasticsearch --- DjangoBlog/elasticsearch_backend.py | 89 ++++++++--------------------- 1 file changed, 23 insertions(+), 66 deletions(-) diff --git a/DjangoBlog/elasticsearch_backend.py b/DjangoBlog/elasticsearch_backend.py index 3e816d8e3..8d34bc8e6 100644 --- a/DjangoBlog/elasticsearch_backend.py +++ b/DjangoBlog/elasticsearch_backend.py @@ -3,7 +3,7 @@ """ @version: ?? @author: liangliangyy -@license: MIT Licence +@license: MIT Licence @contact: liangliangyy@gmail.com @site: https://www.lylinux.net/ @software: PyCharm @@ -22,6 +22,8 @@ from django.utils.datetime_safe import datetime from django.utils.encoding import force_text +from elasticsearch_dsl import Q + from haystack.backends import BaseEngine, BaseSearchBackend, BaseSearchQuery, EmptyResults, log_query from haystack.constants import DJANGO_CT, DJANGO_ID, ID from haystack.exceptions import MissingDependency, SearchBackendError, SkipDocument @@ -70,7 +72,7 @@ def _rebuild(self, models): def update(self, index, iterable, commit=True): models = self._get_models() - #self._rebuild(models) + # self._rebuild(models) def remove(self, obj_or_string): models = self._get_models() @@ -85,83 +87,38 @@ def search(self, query_string, **kwargs): start_offset = kwargs.get('start_offset') end_offset = kwargs.get('end_offset') + + q = Q('bool', + must=[Q('match', body=query_string)], + minimum_should_match="70%" + ) + search = ArticleDocument.search() \ - .query("match", body=query_string) \ + .query('bool', filter=[q]) \ .filter('term', status='p') \ .filter('term', type='a') \ - [start_offset: end_offset] - results = search.execute() - - return self._process_results(raw_results=results) - - def _process_results(self, raw_results, highlight=False, - result_class=None, distance_point=None, - geo_sort=False): - from haystack import connections - results = [] - hits = raw_results['hits'].total - - facets = {} - spelling_suggestion = None + .source(False)[start_offset: end_offset] - if result_class is None: - result_class = SearchResult - if 'facets' in raw_results: - facets = { - 'fields': {}, - 'dates': {}, - 'queries': {}, - } - - # ES can return negative timestamps for pre-1970 data. Handle it. - def from_timestamp(tm): - if tm >= 0: - return datetime.utcfromtimestamp(tm) - else: - return datetime(1970, 1, 1) + timedelta(seconds=tm) - - for facet_fieldname, facet_info in raw_results['facets'].items(): - if facet_info.get('_type', 'terms') == 'terms': - facets['fields'][facet_fieldname] = [(individual['term'], individual['count']) for individual in - facet_info['terms']] - elif facet_info.get('_type', 'terms') == 'date_histogram': - # Elasticsearch provides UTC timestamps with an extra three - # decimals of precision, which datetime barfs on. - facets['dates'][facet_fieldname] = [(from_timestamp(individual['time'] / 1000), - individual['count']) - for individual in facet_info['entries']] - elif facet_info.get('_type', 'terms') == 'query': - facets['queries'][facet_fieldname] = facet_info['count'] - - unified_index = connections[self.connection_alias].get_unified_index() - - content_field = unified_index.document_field - # articleids = list(map(lambda x: x['_id'], raw_results['hits']['hits'])) - # article_results = list(Article.objects.filter(id__in=articleids)) - - for raw_result in raw_results['hits']['hits']: + results = search.execute() + hits = results['hits'].total + raw_results = [] + for raw_result in results['hits']['hits']: app_label = 'blog' model_name = 'Article' additional_fields = {} - if 'highlight' in raw_result: - additional_fields['highlighted'] = raw_result['highlight'].get(content_field, '') - - if distance_point: - additional_fields['_point_of_origin'] = distance_point + # if 'highlight' in raw_result: + # additional_fields['highlighted'] = raw_result['highlight'].get(content_field, '') - if geo_sort and raw_result.get('sort'): - from haystack.utils.geo import Distance - additional_fields['_distance'] = Distance(km=float(raw_result['sort'][0])) - else: - additional_fields['_distance'] = None + result_class = SearchResult result = result_class(app_label, model_name, raw_result['_id'], raw_result['_score'], **additional_fields) - results.append(result) - + raw_results.append(result) + facets = {} + spelling_suggestion = None return { - 'results': results, + 'results': raw_results, 'hits': hits, 'facets': facets, 'spelling_suggestion': spelling_suggestion, From 509384a7a1ff8dff72dc5c2eff07ecaa244762bb Mon Sep 17 00:00:00 2001 From: liangliangyy Date: Tue, 16 Apr 2019 23:17:21 +0800 Subject: [PATCH 063/593] =?UTF-8?q?=E4=BF=AE=E5=A4=8Doauth=E7=99=BB?= =?UTF-8?q?=E5=BD=95=E5=8F=AF=E8=83=BD=E5=AD=98=E5=9C=A8access=20token?= =?UTF-8?q?=E4=B8=8D=E5=AD=98=E5=9C=A8=E5=BC=82=E5=B8=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- oauth/oauthmanager.py | 55 +++++++++++++++++++++++-------------------- oauth/views.py | 11 +++++++-- 2 files changed, 39 insertions(+), 27 deletions(-) diff --git a/oauth/oauthmanager.py b/oauth/oauthmanager.py index 5d7521a14..32c337af8 100644 --- a/oauth/oauthmanager.py +++ b/oauth/oauthmanager.py @@ -25,6 +25,12 @@ logger = logging.getLogger(__name__) +class OAuthAccessTokenException(Exception): + ''' + oauth授权失败异常 + ''' + + class BaseOauthManager(metaclass=ABCMeta): """获取用户授权""" AUTH_URL = None @@ -106,14 +112,14 @@ def get_access_token_by_code(self, code): 'redirect_uri': self.callback_url } rsp = self.do_post(self.TOKEN_URL, params) - try: - obj = json.loads(rsp) + + obj = json.loads(rsp) + if 'access_token' in obj: self.access_token = str(obj['access_token']) self.openid = str(obj['uid']) return self.get_oauth_userinfo() - except Exception as e: - logger.error(e) - return None + else: + raise OAuthAccessTokenException(rsp) def get_oauth_userinfo(self): if not self.is_authorized: @@ -124,7 +130,6 @@ def get_oauth_userinfo(self): } rsp = self.do_get(self.API_URL, params) try: - datas = json.loads(rsp) user = OAuthUser() user.matedata = rsp @@ -178,15 +183,14 @@ def get_access_token_by_code(self, code): rsp = self.do_post(self.TOKEN_URL, params) obj = json.loads(rsp) - try: + + if 'access_token' in obj: self.access_token = str(obj['access_token']) self.openid = str(obj['id_token']) logger.info(self.ICON_NAME + ' oauth ' + rsp) return self.access_token - except Exception as e: - logger.error(e) - logger.error(self.ICON_NAME + ' oauth error ' + rsp) - return None + else: + raise OAuthAccessTokenException(rsp) def get_oauth_userinfo(self): if not self.is_authorized: @@ -249,14 +253,13 @@ def get_access_token_by_code(self, code): } rsp = self.do_post(self.TOKEN_URL, params) - try: - from urllib import parse - r = parse.parse_qs(rsp) + from urllib import parse + r = parse.parse_qs(rsp) + if 'access_token' in r: self.access_token = (r['access_token'][0]) return self.access_token - except Exception as e: - logger.error(e) - return None + else: + raise OAuthAccessTokenException(rsp) def get_oauth_userinfo(self): @@ -318,14 +321,13 @@ def get_access_token_by_code(self, code): } rsp = self.do_post(self.TOKEN_URL, params) - try: - obj = json.loads(rsp) + obj = json.loads(rsp) + if 'access_token' in obj: token = str(obj['access_token']) self.access_token = token return self.access_token - except Exception as e: - logger.error(e) - return None + else: + raise OAuthAccessTokenException(rsp) def get_oauth_userinfo(self): params = { @@ -385,9 +387,12 @@ def get_access_token_by_code(self, code): rsp = self.do_get(self.TOKEN_URL, params) if rsp: d = urllib.parse.parse_qs(rsp) - token = d['access_token'] - self.access_token = token - return token + if 'access_token' in d: + token = d['access_token'] + self.access_token = token + return token + else: + raise OAuthAccessTokenException(rsp) def get_open_id(self): if self.is_access_token_set: diff --git a/oauth/views.py b/oauth/views.py index 2e99a3da7..9c828dc80 100644 --- a/oauth/views.py +++ b/oauth/views.py @@ -15,7 +15,7 @@ from DjangoBlog.utils import get_current_site from django.core.exceptions import ObjectDoesNotExist from django.http import HttpResponseForbidden -from .oauthmanager import get_manager_by_type +from .oauthmanager import get_manager_by_type, OAuthAccessTokenException from DjangoBlog.blog_signals import oauth_user_login_signal import logging @@ -57,7 +57,14 @@ def authorize(request): if not manager: return HttpResponseRedirect('/') code = request.GET.get('code', None) - rsp = manager.get_access_token_by_code(code) + try: + rsp = manager.get_access_token_by_code(code) + except OAuthAccessTokenException as e: + logger.warning("OAuthAccessTokenException:" + str(e)) + return HttpResponseRedirect('/') + except Exception as e: + logger.error(e) + rsp = None nexturl = get_redirecturl(request) if not rsp: return HttpResponseRedirect(manager.get_authorization_url(nexturl)) From 0e7410429ed44b3fd67f51be01b530ab1e730ac5 Mon Sep 17 00:00:00 2001 From: liangliangyy Date: Wed, 17 Apr 2019 22:18:34 +0800 Subject: [PATCH 064/593] =?UTF-8?q?=E7=AE=A1=E7=90=86=E5=90=8E=E5=8F=B0?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E5=88=9B=E5=BB=BA=E8=B6=85=E7=BA=A7=E7=94=A8?= =?UTF-8?q?=E6=88=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- accounts/admin.py | 49 +++++++++++++++++++++++++++++++++++++++++++++- accounts/models.py | 7 ++++++- 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/accounts/admin.py b/accounts/admin.py index 83a408626..eebb1657c 100644 --- a/accounts/admin.py +++ b/accounts/admin.py @@ -1,8 +1,55 @@ +from django import forms from django.contrib import admin +from django.contrib.auth.admin import UserAdmin +from django.contrib.auth.forms import UserCreationForm, UserChangeForm +from django.contrib.auth.forms import ReadOnlyPasswordHashField # Register your models here. from .models import BlogUser -class BlogUserAdmin(admin.ModelAdmin): +class BlogUserCreationForm(forms.ModelForm): + password1 = forms.CharField(label='密码', widget=forms.PasswordInput) + password2 = forms.CharField(label='再次输入密码', widget=forms.PasswordInput) + + class Meta: + model = BlogUser + fields = ('email',) + + def clean_password2(self): + # Check that the two password entries match + password1 = self.cleaned_data.get("password1") + password2 = self.cleaned_data.get("password2") + if password1 and password2 and password1 != password2: + raise forms.ValidationError("两次密码不一致") + return password2 + + def save(self, commit=True): + # Save the provided password in hashed format + user = super().save(commit=False) + user.set_password(self.cleaned_data["password1"]) + if commit: + user.save() + return user + + +class BlogUserChangeForm(forms.ModelForm): + password = ReadOnlyPasswordHashField + email = forms.EmailField(label="Email", widget=forms.EmailInput) + + class Meta: + model = BlogUser + fields = ('email', 'password', 'is_active') + + def clean_password(self): + # Regardless of what the user provides, return the initial value. + # This is done here, rather than on the field, because the + # field does not have access to the initial value + return self.initial["password"] + + +class BlogUserAdmin(UserAdmin): + form = BlogUserChangeForm + add_form = BlogUserCreationForm list_display = ('id', 'nickname', 'username', 'email', 'last_login', 'date_joined') list_display_links = ('id', 'username') + ordering = ('-id',) diff --git a/accounts/models.py b/accounts/models.py index 1b4038419..12b4f617b 100644 --- a/accounts/models.py +++ b/accounts/models.py @@ -9,7 +9,6 @@ class BlogUser(AbstractUser): nickname = models.CharField('昵称', max_length=100, blank=True) - mugshot = models.ImageField('头像', upload_to='upload/mugshots', blank=True) created_time = models.DateTimeField('创建时间', default=now) last_mod_time = models.DateTimeField('修改时间', default=now) @@ -25,3 +24,9 @@ def get_full_url(self): site = get_current_site().domain url = "https://{site}{path}".format(site=site, path=self.get_absolute_url()) return url + + class Meta: + ordering = ['-id'] + verbose_name = "用户" + verbose_name_plural = verbose_name + get_latest_by = 'id' From b5c255c2c02800a861afd2a38cffa8c0e35047f2 Mon Sep 17 00:00:00 2001 From: liangliangyy Date: Sun, 21 Apr 2019 03:34:58 +0800 Subject: [PATCH 065/593] =?UTF-8?q?=E5=A2=9E=E5=8A=A0es=E5=9F=8B=E7=82=B9?= =?UTF-8?q?=EF=BC=8C=E4=BC=98=E5=8C=96es=E6=90=9C=E7=B4=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DjangoBlog/elasticsearch_backend.py | 119 ++++--------------- blog/documents.py | 152 +++++++++++++++++------- blog/management/commands/build_index.py | 27 +++++ blog/middleware.py | 10 +- blog/models.py | 8 +- blog/search_indexes.py | 1 - requirements.txt | 3 +- 7 files changed, 175 insertions(+), 145 deletions(-) create mode 100644 blog/management/commands/build_index.py diff --git a/DjangoBlog/elasticsearch_backend.py b/DjangoBlog/elasticsearch_backend.py index 8d34bc8e6..67f12b9aa 100644 --- a/DjangoBlog/elasticsearch_backend.py +++ b/DjangoBlog/elasticsearch_backend.py @@ -10,72 +10,55 @@ @file: elasticsearch_backend.py @time: 2019-04-13 11:46 """ + import logging import re -import json - -from datetime import datetime, timedelta - -from django.conf import settings -from django.core.exceptions import ImproperlyConfigured -from django.utils import six -from django.utils.datetime_safe import datetime from django.utils.encoding import force_text from elasticsearch_dsl import Q from haystack.backends import BaseEngine, BaseSearchBackend, BaseSearchQuery, EmptyResults, log_query -from haystack.constants import DJANGO_CT, DJANGO_ID, ID -from haystack.exceptions import MissingDependency, SearchBackendError, SkipDocument -from haystack.inputs import Clean, Exact, PythonData, Raw from haystack.models import SearchResult from haystack.utils import log as logging -from haystack.utils import get_identifier, get_model_ct -from haystack.utils.app_loading import haystack_get_model -from django_elasticsearch_dsl.registries import registry from blog.models import Article -from blog.documents import ArticleDocument +from blog.documents import ArticleDocument, ArticleDocumentManager logger = logging.getLogger(__name__) -DATETIME_REGEX = re.compile( - '^(?P\d{4})-(?P\d{2})-(?P\d{2})T(?P\d{2}):(?P\d{2}):(?P\d{2})(\.\d{3,6}Z?)?$') - class ElasticSearchBackend(BaseSearchBackend): + def __init__(self, connection_alias, **connection_options): + super(ElasticSearchBackend, self).__init__(connection_alias, **connection_options) + self.manager = ArticleDocumentManager() + self._rebuild(None) - def _get_models(self): - models = registry.get_models() - return set(models) + def _get_models(self, iterable): + models = iterable if iterable else Article.objects.all() + docs = self.manager.convert_to_doc(models) + return docs def _create(self, models): - for index in registry.get_indices(models): - index.create() - - def _populate(self, models): - for doc in registry.get_documents(models): - qs = doc().get_queryset() - doc().update(qs) + self.manager.create_index() + docs = self._get_models(models) + self.manager.rebuild(docs) def _delete(self, models): - for index in registry.get_indices(models): - index.delete(ignore=404) + for m in models: + m.delete() return True def _rebuild(self, models): - if not self._delete(models): - return - - self._create(models) - self._populate(models) + models = models if models else Article.objects.all() + docs = self.manager.convert_to_doc(models) + self.manager.update_docs(docs) def update(self, index, iterable, commit=True): - models = self._get_models() - # self._rebuild(models) + models = self._get_models(iterable) + self.manager.update_docs(models) def remove(self, obj_or_string): - models = self._get_models() + models = self._get_models([obj_or_string]) self._delete(models) def clear(self, models=None, commit=True): @@ -124,66 +107,6 @@ def search(self, query_string, **kwargs): 'spelling_suggestion': spelling_suggestion, } - def _from_python(self, value): - """ - Converts Python values to a string for Whoosh. - - Code courtesy of pysolr. - """ - if hasattr(value, 'strftime'): - if not hasattr(value, 'hour'): - value = datetime(value.year, value.month, value.day, 0, 0, 0) - elif isinstance(value, bool): - if value: - value = 'true' - else: - value = 'false' - elif isinstance(value, (list, tuple)): - value = u','.join([force_text(v) for v in value]) - elif isinstance(value, (six.integer_types, float)): - # Leave it alone. - pass - else: - value = force_text(value) - return value - - def _to_python(self, value): - """ - Converts values from Whoosh to native Python values. - - A port of the same method in pysolr, as they deal with data the same way. - """ - if value == 'true': - return True - elif value == 'false': - return False - - if value and isinstance(value, six.string_types): - possible_datetime = DATETIME_REGEX.search(value) - - if possible_datetime: - date_values = possible_datetime.groupdict() - - for dk, dv in date_values.items(): - date_values[dk] = int(dv) - - return datetime(date_values['year'], date_values['month'], date_values['day'], date_values['hour'], - date_values['minute'], date_values['second']) - - try: - # Attempt to use json to load the values. - converted_value = json.loads(value) - - # Try to handle most built-in types. - if isinstance(converted_value, (list, tuple, set, dict, six.integer_types, float, complex)): - return converted_value - except: - # If it fails (SyntaxError or its ilk) or we don't trust it, - # continue on. - pass - - return value - class ElasticSearchQuery(BaseSearchQuery): def _convert_datetime(self, date): diff --git a/blog/documents.py b/blog/documents.py index 3219587be..699d9bc2a 100644 --- a/blog/documents.py +++ b/blog/documents.py @@ -10,54 +10,122 @@ @file: documents.py @time: 2019-04-05 13:05 """ - -from django_elasticsearch_dsl import DocType, Index, fields +import time from blog.models import Article, Category, Tag -from accounts.models import BlogUser - -blog = Index('blog') -blog.settings( - number_of_shards=1, - number_of_replicas=0 -) - - -@blog.doc_type -class ArticleDocument(DocType): - body = fields.TextField(attr='body_to_string', analyzer='ik_max_word') - title = fields.TextField(analyzer='ik_max_word') - author = fields.ObjectField(properties={ - 'nickname': fields.TextField(analyzer='ik_max_word'), - 'id': fields.IntegerField() +from elasticsearch_dsl import Document, Date, Integer, Keyword, Text, Object, Boolean + +from django.conf import settings + +ELASTICSEARCH_ENABLED = hasattr(settings, 'ELASTICSEARCH_DSL') + +from elasticsearch_dsl.connections import connections + +if ELASTICSEARCH_ENABLED: + connections.create_connection(hosts=[settings.ELASTICSEARCH_DSL['default']['hosts']]) + + +class ElapsedTimeDocument(Document): + url = Text() + time_taken = Integer() + log_datetime = Date() + type = Text(analyzer='ik_max_word') + + class Index: + name = 'performance' + settings = { + "number_of_shards": 1, + "number_of_replicas": 0 + } + + class Meta: + doc_type = 'ElapsedTime' + + +class ElaspedTimeDocumentManager(): + + @staticmethod + def create(url, time_taken, log_datetime, type): + if not hasattr(ElaspedTimeDocumentManager, 'mapping_created'): + ElapsedTimeDocument.init() + setattr(ElaspedTimeDocumentManager, 'mapping_created', True) + doc = ElapsedTimeDocument(meta={'id': int(round(time.time() * 1000))}, url=url, time_taken=time_taken, + log_datetime=log_datetime, type=type) + doc.save() + + +class ArticleDocument(Document): + body = Text(analyzer='ik_max_word') + title = Text(analyzer='ik_max_word') + author = Object(properties={ + 'nickname': Text(analyzer='ik_max_word'), + 'id': Integer() }) - category = fields.ObjectField(properties={ - 'name': fields.TextField(analyzer='ik_max_word'), - 'id': fields.IntegerField() + category = Object(properties={ + 'name': Text(analyzer='ik_max_word'), + 'id': Integer() }) - tags = fields.ObjectField(properties={ - 'name': fields.TextField(analyzer='ik_max_word'), - 'id': fields.IntegerField() + tags = Object(properties={ + 'name': Text(analyzer='ik_max_word'), + 'id': Integer() }) - # def get_instances_from_related(self, related_instance): - # if isinstance(related_instance, BlogUser): - # return related_instance - # elif isinstance(related_instance, Category): - # pass + pub_time = Date() + status = Text() + comment_status = Text() + type = Text() + views = Integer() + article_order = Integer() + + class Index: + name = 'blog' + settings = { + "number_of_shards": 1, + "number_of_replicas": 0 + } class Meta: - model = Article - fields = [ - 'pub_time', - 'status', - 'comment_status', - 'type', - 'views', - 'article_order', - - ] - # related_models = [Category, Tag, BlogUser] doc_type = 'Article' - auto_refresh = False - ignore_signals = True + +class ArticleDocumentManager(): + + def __init__(self): + + ArticleDocument.init() + + def create_index(self): + ArticleDocument.init() + + def deleate_index(self): + from elasticsearch import Elasticsearch + es = Elasticsearch() + es.indices.delete(index='blog', ignore=[400, 404]) + + def convert_to_doc(self, articles): + return [ArticleDocument(meta={'id': article.id}, body=article.body, title=article.title, + auth={ + 'nikename': article.author.username, + 'id': article.author.id + }, + category={ + 'name': article.category.name, + 'id': article.category.id + }, + tags=[{'name': t.name, 'id': t.id} for t in article.tags.all()], + pub_time=article.pub_time, + status=article.status, + comment_status=article.comment_status, + type=article.type, + views=article.views, + article_order=article.article_order + ) for article in articles] + + def rebuild(self, articles=None): + articles = articles if articles else Article.objects.all() + docs = self.convert_to_doc(articles) + for doc in docs: + doc.save() + + def update_docs(self, docs): + for doc in docs: + doc.save() diff --git a/blog/management/commands/build_index.py b/blog/management/commands/build_index.py new file mode 100644 index 000000000..1beaa403e --- /dev/null +++ b/blog/management/commands/build_index.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python +# encoding: utf-8 +""" +@version: ?? +@author: liangliangyy +@license: MIT Licence +@contact: liangliangyy@gmail.com +@site: https://www.lylinux.net/ +@software: PyCharm +@file: build_index.py +@time: 2019-04-20 20:39 +""" + +from blog.documents import ArticleDocument, ArticleDocumentManager + +from django.core.management.base import BaseCommand +from blog.models import Article + + +# TODO 参数化 +class Command(BaseCommand): + help = 'build search index' + + def handle(self, *args, **options): + manager = ArticleDocumentManager() + manager.deleate_index() + manager.rebuild() diff --git a/blog/middleware.py b/blog/middleware.py index 3241fc42b..f24ed981b 100644 --- a/blog/middleware.py +++ b/blog/middleware.py @@ -12,10 +12,11 @@ @file: middleware.py @time: 2017/1/19 上午12:36 """ - +import datetime import time from ipware.ip import get_real_ip from DjangoBlog.utils import cache +from blog.documents import ELASTICSEARCH_ENABLED, ElaspedTimeDocumentManager class OnlineMiddleware(object): @@ -31,5 +32,12 @@ def __call__(self, request): return response cast_time = time.time() - start_time + if ELASTICSEARCH_ENABLED: + time_taken = round((cast_time) * 1000, 2) + url = request.path + from django.utils import timezone + + ElaspedTimeDocumentManager.create(url=url, time_taken=time_taken, log_datetime=timezone.now(), + type='blog') response.content = response.content.replace(b'', str.encode(str(cast_time)[:5])) return response diff --git a/blog/models.py b/blog/models.py index 1ac9854fb..4c8b7df53 100644 --- a/blog/models.py +++ b/blog/models.py @@ -12,6 +12,7 @@ from django.utils.functional import cached_property from django.utils.timezone import now from mdeditor.fields import MDTextField +from django.db.models.signals import post_save logger = logging.getLogger(__name__) @@ -34,7 +35,12 @@ def save(self, *args, **kwargs): if getattr(self, 'slug') == 'no-slug' or not self.id: slug = getattr(self, 'title') if 'title' in self.__dict__ else getattr(self, 'name') setattr(self, 'slug', slugify(slug)) - super().save(*args, **kwargs) + is_update_views = isinstance(self, Article) and 'update_fields' in kwargs and kwargs['update_fields'] == [ + 'views'] + if is_update_views: + Article.objects.filter(pk=self.pk).update(views=self.views) + else: + super().save(*args, **kwargs) # is_update_views = 'update_fields' in kwargs and len(kwargs['update_fields']) == 1 and kwargs['update_fields'][ # 0] == 'views' # from DjangoBlog.blog_signals import article_save_signal diff --git a/blog/search_indexes.py b/blog/search_indexes.py index f48ae3269..ed80e6567 100644 --- a/blog/search_indexes.py +++ b/blog/search_indexes.py @@ -19,7 +19,6 @@ class ArticleIndex(indexes.SearchIndex, indexes.Indexable): text = indexes.CharField(document=True, use_template=True) - author = indexes.CharField(model_attr='author') def get_model(self): return Article diff --git a/requirements.txt b/requirements.txt index 6daa56a64..5d8713518 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,13 +12,12 @@ django-appconf==1.0.3 django-autoslug==1.9.4 django-compressor==2.2 django-debug-toolbar==1.11 -django-elasticsearch-dsl==0.5.1 django-haystack==2.8.1 django-ipware==2.1.0 django-mdeditor==0.1.13 django-uuslug==1.1.8 elasticsearch==6.3.1 -elasticsearch-dsl==6.1.0 +elasticsearch-dsl==6.3.1 idna==2.8 ipaddress==1.0.22 isort==4.3.15 From 9bf363e75935a2094f9a3d7bf6e048d10e955d48 Mon Sep 17 00:00:00 2001 From: liangliangyy Date: Sun, 21 Apr 2019 03:39:46 +0800 Subject: [PATCH 066/593] =?UTF-8?q?=E4=BE=9D=E8=B5=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- travis_test/requirements.txt | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/travis_test/requirements.txt b/travis_test/requirements.txt index f9e771269..1257e628f 100644 --- a/travis_test/requirements.txt +++ b/travis_test/requirements.txt @@ -12,13 +12,12 @@ django-appconf==1.0.3 django-autoslug==1.9.4 django-compressor==2.2 django-debug-toolbar==1.11 -django-elasticsearch-dsl==0.5.1 django-haystack==2.8.1 django-ipware==2.1.0 django-mdeditor==0.1.13 django-uuslug==1.1.8 elasticsearch==6.3.1 -elasticsearch-dsl==6.1.0 +elasticsearch-dsl==6.3.1 idna==2.8 ipaddress==1.0.22 isort==4.3.15 @@ -38,7 +37,6 @@ PyMySQL==0.9.3 pyparsing==2.3.1 python-dateutil==2.8.0 python-logstash==0.4.6 -python-memcached==1.59 python-slugify==3.0.0 pytz==2018.9 raven==6.10.0 @@ -53,4 +51,4 @@ webencodings==0.5.1 WeRoBot==1.8.0 Whoosh==2.7.4 wrapt==1.11.1 -xmltodict==0.12.0 \ No newline at end of file +xmltodict==0.12.0 From 0eb3cc4420197d5b16ad6aa61f8eff644b265b33 Mon Sep 17 00:00:00 2001 From: liangliangyy Date: Sun, 21 Apr 2019 17:06:01 +0800 Subject: [PATCH 067/593] =?UTF-8?q?=E8=AE=B0=E5=BD=95ua?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- blog/documents.py | 5 +++-- blog/middleware.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/blog/documents.py b/blog/documents.py index 699d9bc2a..40c347a48 100644 --- a/blog/documents.py +++ b/blog/documents.py @@ -29,6 +29,7 @@ class ElapsedTimeDocument(Document): time_taken = Integer() log_datetime = Date() type = Text(analyzer='ik_max_word') + useragent = Text() class Index: name = 'performance' @@ -44,12 +45,12 @@ class Meta: class ElaspedTimeDocumentManager(): @staticmethod - def create(url, time_taken, log_datetime, type): + def create(url, time_taken, log_datetime, type, useragent): if not hasattr(ElaspedTimeDocumentManager, 'mapping_created'): ElapsedTimeDocument.init() setattr(ElaspedTimeDocumentManager, 'mapping_created', True) doc = ElapsedTimeDocument(meta={'id': int(round(time.time() * 1000))}, url=url, time_taken=time_taken, - log_datetime=log_datetime, type=type) + log_datetime=log_datetime, type=type, useragent=useragent) doc.save() diff --git a/blog/middleware.py b/blog/middleware.py index f24ed981b..622b80436 100644 --- a/blog/middleware.py +++ b/blog/middleware.py @@ -38,6 +38,6 @@ def __call__(self, request): from django.utils import timezone ElaspedTimeDocumentManager.create(url=url, time_taken=time_taken, log_datetime=timezone.now(), - type='blog') + type='blog', useragent=http_user_agent) response.content = response.content.replace(b'', str.encode(str(cast_time)[:5])) return response From c03056c0ec067afd0ec9b73c95b555b50e4d5904 Mon Sep 17 00:00:00 2001 From: liangliangyy Date: Mon, 22 Apr 2019 22:31:53 +0800 Subject: [PATCH 068/593] update css --- blog/static/blog/css/style.css | 1 + 1 file changed, 1 insertion(+) diff --git a/blog/static/blog/css/style.css b/blog/static/blog/css/style.css index 8780b725c..e3a19b2a6 100644 --- a/blog/static/blog/css/style.css +++ b/blog/static/blog/css/style.css @@ -498,6 +498,7 @@ body.custom-font-enabled { a { outline: none; color: #21759b; + word-wrap: break-word; } a:hover { From 8197a37c86881362ec7dfa6e3d387a26cf019f2f Mon Sep 17 00:00:00 2001 From: liangliangyy Date: Wed, 24 Apr 2019 11:15:32 +0800 Subject: [PATCH 069/593] =?UTF-8?q?=E5=8D=87=E7=BA=A7=20urllib3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- requirements.txt | 2 +- travis_test/requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 5d8713518..dbf4560b8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -47,7 +47,7 @@ sqlparse==0.3.0 text-unidecode==1.2 typed-ast==1.3.1 Unidecode==1.0.23 -urllib3==1.24.1 +urllib3==1.24.2 webencodings==0.5.1 WeRoBot==1.8.0 Whoosh==2.7.4 diff --git a/travis_test/requirements.txt b/travis_test/requirements.txt index 1257e628f..17f7ec2f1 100644 --- a/travis_test/requirements.txt +++ b/travis_test/requirements.txt @@ -46,7 +46,7 @@ sqlparse==0.3.0 text-unidecode==1.2 typed-ast==1.3.1 Unidecode==1.0.23 -urllib3==1.24.1 +urllib3==1.24.2 webencodings==0.5.1 WeRoBot==1.8.0 Whoosh==2.7.4 From 4a8aaaa27454e02cbf724b2c6ccf2b651dd97461 Mon Sep 17 00:00:00 2001 From: liangliangyy Date: Wed, 8 May 2019 23:09:35 +0800 Subject: [PATCH 070/593] Fixes #259, Fixes #257 --- blog/templatetags/blog_tags.py | 6 +++--- templates/blog/article_detail.html | 3 ++- templates/share_layout/base_account.html | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/blog/templatetags/blog_tags.py b/blog/templatetags/blog_tags.py index d29e032be..b7ff3c54c 100644 --- a/blog/templatetags/blog_tags.py +++ b/blog/templatetags/blog_tags.py @@ -148,9 +148,9 @@ def load_sidebar(user, linktype): tags = Tag.objects.all() sidebar_tags = None if tags and len(tags) > 0: - s = list(map(lambda t: (t, t.get_article_count()), tags)) - count = sum(map(lambda t: t[1], s)) - dd = 1 if (count == 0 and not len(tags)) else count / len(tags) + s = [t for t in [(t, t.get_article_count()) for t in tags] if t[1] > 0] + count = sum([t[1] for t in s]) + dd = 1 if (count == 0 or not len(tags)) else count / len(tags) import random sidebar_tags = list(map(lambda x: (x[0], x[1], (x[1] / dd) * increment + 10), s)) random.shuffle(sidebar_tags) diff --git a/templates/blog/article_detail.html b/templates/blog/article_detail.html index f4044105f..d0bc9852f 100755 --- a/templates/blog/article_detail.html +++ b/templates/blog/article_detail.html @@ -58,7 +58,8 @@

    文章导航

    {% else %}

    您还没有登录,请您登录后发表评论。

    + href="{% url "account:login" %}?next={{ request.get_full_path }}" rel="nofollow">登录后发表评论。 +

    {% load oauth_tags %} {% load_oauth_applications request %} diff --git a/templates/share_layout/base_account.html b/templates/share_layout/base_account.html index 645726060..d7160b56d 100644 --- a/templates/share_layout/base_account.html +++ b/templates/share_layout/base_account.html @@ -9,7 +9,7 @@ - + {{ SITE_NAME }} | {{ SITE_DESCRIPTION }} {% load compress %} {% compress css %} From 8c97d40b20a007078393335fe0bf328f77415167 Mon Sep 17 00:00:00 2001 From: liangliangyy Date: Sat, 11 May 2019 22:00:39 +0800 Subject: [PATCH 071/593] Fix #260 Fix #262 --- DjangoBlog/utils.py | 9 ++++----- bin/config.md | 6 ++++-- blog/templatetags/blog_tags.py | 2 +- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/DjangoBlog/utils.py b/DjangoBlog/utils.py index 75555c7a2..48ef11379 100644 --- a/DjangoBlog/utils.py +++ b/DjangoBlog/utils.py @@ -47,24 +47,23 @@ def news(*args, **kwargs): key = view.get_cache_key() except: key = None - pass if not key: unique_str = repr((func, args, kwargs)) m = md5(unique_str.encode('utf-8')) key = m.hexdigest() value = cache.get(key) - if value: + if value is not None: # logger.info('cache_decorator get cache:%s key:%s' % (func.__name__, key)) - if repr(value) == 'default': + if str(value) == '__default_cache_value__': return None else: return value else: logger.info('cache_decorator set cache:%s key:%s' % (func.__name__, key)) value = func(*args, **kwargs) - if not value: - cache.set(key, 'default', expiration) + if value is None: + cache.set(key, '__default_cache_value__', expiration) else: cache.set(key, value, expiration) return value diff --git a/bin/config.md b/bin/config.md index b626333c3..76dac74f9 100644 --- a/bin/config.md +++ b/bin/config.md @@ -21,8 +21,10 @@ CACHES = { 现在已经支持微博,Google,GitHub,Facebook登录,需要在其对应的开放平台申请oauth登录权限,然后在 **后台->Oauth** 配置中新增配置,填写对应的`appkey`和`appsecret`以及回调地址。 - - +### 回调地址示例: +qq:http://你的域名/oauth/authorize?type=qq +微博:http://你的域名/oauth/authorize?type=weibo +type对应在`oauthmanager`中的type字段。 ## owntracks: owntracks是一个位置追踪软件,可以定时的将你的坐标提交到你的服务器上,现在简单的支持owntracks功能,需要安装owntracks的app,然后将api地址设置为: diff --git a/blog/templatetags/blog_tags.py b/blog/templatetags/blog_tags.py index b7ff3c54c..760f8d563 100644 --- a/blog/templatetags/blog_tags.py +++ b/blog/templatetags/blog_tags.py @@ -148,7 +148,7 @@ def load_sidebar(user, linktype): tags = Tag.objects.all() sidebar_tags = None if tags and len(tags) > 0: - s = [t for t in [(t, t.get_article_count()) for t in tags] if t[1] > 0] + s = [t for t in [(t, t.get_article_count()) for t in tags] if t[1]] count = sum([t[1] for t in s]) dd = 1 if (count == 0 or not len(tags)) else count / len(tags) import random From 06b69963c76a76c20ffd4b54b89d5b6c632f2323 Mon Sep 17 00:00:00 2001 From: liangliangyy Date: Sat, 11 May 2019 22:31:06 +0800 Subject: [PATCH 072/593] =?UTF-8?q?=E5=8D=87=E7=BA=A7=E4=BE=9D=E8=B5=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- requirements.txt | 4 ++-- travis_test/requirements.txt | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/requirements.txt b/requirements.txt index dbf4560b8..5b7b7f83a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,8 +16,8 @@ django-haystack==2.8.1 django-ipware==2.1.0 django-mdeditor==0.1.13 django-uuslug==1.1.8 -elasticsearch==6.3.1 -elasticsearch-dsl==6.3.1 +elasticsearch==7.0.0 +elasticsearch-dsl==7.0.0 idna==2.8 ipaddress==1.0.22 isort==4.3.15 diff --git a/travis_test/requirements.txt b/travis_test/requirements.txt index 17f7ec2f1..aa6d898a9 100644 --- a/travis_test/requirements.txt +++ b/travis_test/requirements.txt @@ -16,8 +16,8 @@ django-haystack==2.8.1 django-ipware==2.1.0 django-mdeditor==0.1.13 django-uuslug==1.1.8 -elasticsearch==6.3.1 -elasticsearch-dsl==6.3.1 +elasticsearch==7.0.0 +elasticsearch-dsl==7.0.0 idna==2.8 ipaddress==1.0.22 isort==4.3.15 From 48bcfbe77b0ce7efa66b50818c3c549cf59020db Mon Sep 17 00:00:00 2001 From: liangliangyy Date: Sat, 11 May 2019 23:48:22 +0800 Subject: [PATCH 073/593] =?UTF-8?q?=E4=BF=AE=E5=A4=8Des=E6=90=9C=E7=B4=A2?= =?UTF-8?q?=E5=BC=82=E5=B8=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DjangoBlog/elasticsearch_backend.py | 5 +++++ blog/documents.py | 2 +- blog/management/commands/build_index.py | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/DjangoBlog/elasticsearch_backend.py b/DjangoBlog/elasticsearch_backend.py index 67f12b9aa..5356eac6b 100644 --- a/DjangoBlog/elasticsearch_backend.py +++ b/DjangoBlog/elasticsearch_backend.py @@ -100,6 +100,7 @@ def search(self, query_string, **kwargs): raw_results.append(result) facets = {} spelling_suggestion = None + return { 'results': raw_results, 'hits': hits, @@ -143,6 +144,10 @@ def clean(self, query_fragment): def build_query_fragment(self, field, filter_type, value): return value.query_string + def get_count(self): + results = self.get_results() + return len(results) if results else 0 + class ElasticSearchEngine(BaseEngine): backend = ElasticSearchBackend diff --git a/blog/documents.py b/blog/documents.py index 40c347a48..d28205953 100644 --- a/blog/documents.py +++ b/blog/documents.py @@ -97,7 +97,7 @@ def __init__(self): def create_index(self): ArticleDocument.init() - def deleate_index(self): + def delete_index(self): from elasticsearch import Elasticsearch es = Elasticsearch() es.indices.delete(index='blog', ignore=[400, 404]) diff --git a/blog/management/commands/build_index.py b/blog/management/commands/build_index.py index 1beaa403e..25dcd9c74 100644 --- a/blog/management/commands/build_index.py +++ b/blog/management/commands/build_index.py @@ -23,5 +23,5 @@ class Command(BaseCommand): def handle(self, *args, **options): manager = ArticleDocumentManager() - manager.deleate_index() + manager.delete_index() manager.rebuild() From c38fbf0c27f47af707ed05a5e66708ed9f35e4dd Mon Sep 17 00:00:00 2001 From: liangliangyy Date: Tue, 28 May 2019 22:33:04 +0800 Subject: [PATCH 074/593] fix #268 --- DjangoBlog/urls.py | 2 ++ blog/middleware.py | 20 +++++++++++--------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/DjangoBlog/urls.py b/DjangoBlog/urls.py index a444aeb78..442d55935 100644 --- a/DjangoBlog/urls.py +++ b/DjangoBlog/urls.py @@ -50,3 +50,5 @@ url(r'', include('servermanager.urls', namespace='servermanager')), url(r'', include('owntracks.urls', namespace='owntracks')) ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) +if settings.DEBUG: + urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/blog/middleware.py b/blog/middleware.py index 622b80436..94039414a 100644 --- a/blog/middleware.py +++ b/blog/middleware.py @@ -30,14 +30,16 @@ def __call__(self, request): http_user_agent = request.META.get('HTTP_USER_AGENT', []) if 'Spider' in http_user_agent or 'spider' in http_user_agent: return response + try: + cast_time = time.time() - start_time + if ELASTICSEARCH_ENABLED: + time_taken = round((cast_time) * 1000, 2) + url = request.path + from django.utils import timezone - cast_time = time.time() - start_time - if ELASTICSEARCH_ENABLED: - time_taken = round((cast_time) * 1000, 2) - url = request.path - from django.utils import timezone - - ElaspedTimeDocumentManager.create(url=url, time_taken=time_taken, log_datetime=timezone.now(), - type='blog', useragent=http_user_agent) - response.content = response.content.replace(b'', str.encode(str(cast_time)[:5])) + ElaspedTimeDocumentManager.create(url=url, time_taken=time_taken, log_datetime=timezone.now(), + type='blog', useragent=http_user_agent) + response.content = response.content.replace(b'', str.encode(str(cast_time)[:5])) + except: + pass return response From 00e6fd0a28b5339d3cabc965144fad3a685615a3 Mon Sep 17 00:00:00 2001 From: liangliangyy Date: Tue, 28 May 2019 22:48:31 +0800 Subject: [PATCH 075/593] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20travis?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DjangoBlog/settings.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/DjangoBlog/settings.py b/DjangoBlog/settings.py index 8ec8d4512..1fe2169fd 100644 --- a/DjangoBlog/settings.py +++ b/DjangoBlog/settings.py @@ -285,6 +285,5 @@ 'compressor.filters.jsmin.JSMinFilter' ] - -MEDIA_ROOT = os.path.join(BASE_DIR, 'uploads') +MEDIA_ROOT = os.path.join(SITE_ROOT, 'uploads') MEDIA_URL = '/media/' From c9e987e274722ad4a6a599ed1e07ab7630c33bc4 Mon Sep 17 00:00:00 2001 From: liangliangyy Date: Tue, 28 May 2019 23:01:00 +0800 Subject: [PATCH 076/593] update --- travis_test/travis_settings.py | 304 +-------------------------------- 1 file changed, 1 insertion(+), 303 deletions(-) diff --git a/travis_test/travis_settings.py b/travis_test/travis_settings.py index 35cfc061b..04b602bbc 100644 --- a/travis_test/travis_settings.py +++ b/travis_test/travis_settings.py @@ -1,95 +1,7 @@ -""" -Django settings for DjangoBlog project. +from DjangoBlog.settings import * -Generated by 'django-admin startproject' using Django 1.10.2. - -For more information on this file, see -https://docs.djangoproject.com/en/1.10/topics/settings/ - -For the full list of settings and their values, see -https://docs.djangoproject.com/en/1.10/ref/settings/ -""" -import sys -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/1.10/howto/deployment/checklist/ - -# SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = '&3g0bdza#c%dm1lf%5gi&0-*53p3t0m*hmcvo29cn^$ji7je(c' -# SECURITY WARNING: don't run with debug turned on in production! -DEBUG = True -# DEBUG = False - - -TESTING = len(sys.argv) > 1 and sys.argv[1] == 'test' - -# ALLOWED_HOSTS = [] -ALLOWED_HOSTS = ['www.lylinux.net', '127.0.0.1', 'example.com'] -# Application definition - - -INSTALLED_APPS = [ - # 'django.contrib.admin', - 'django.contrib.admin.apps.SimpleAdminConfig', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'django.contrib.sites', - 'django.contrib.sitemaps', - 'mdeditor', - 'haystack', - 'blog', - 'accounts', - 'comments', - 'oauth', - 'servermanager', - 'owntracks', - 'compressor' -] - -MIDDLEWARE = [ - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.gzip.GZipMiddleware', - # 'django.middleware.cache.UpdateCacheMiddleware', - 'django.middleware.common.CommonMiddleware', - # 'django.middleware.cache.FetchFromCacheMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', - 'django.middleware.http.ConditionalGetMiddleware', - 'blog.middleware.OnlineMiddleware' -] - -ROOT_URLCONF = 'DjangoBlog.urls' - -TEMPLATES = [ - { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [os.path.join(BASE_DIR, 'templates')], - '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', - 'blog.context_processors.seo_processor' - ], - }, - }, -] - -WSGI_APPLICATION = 'DjangoBlog.wsgi.application' - # Database # https://docs.djangoproject.com/en/1.10/ref/settings/#databases @@ -107,217 +19,3 @@ 'PORT': 3306, } } - -# Password validation -# https://docs.djangoproject.com/en/1.10/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/1.10/topics/i18n/ - -LANGUAGE_CODE = 'zh-hans' - -TIME_ZONE = 'Asia/Shanghai' - -USE_I18N = True - -USE_L10N = True - -USE_TZ = True - -# Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/1.10/howto/static-files/ - - -SITE_ROOT = os.path.dirname(os.path.abspath(__file__)) -SITE_ROOT = os.path.abspath(os.path.join(SITE_ROOT, '../')) - -HAYSTACK_CONNECTIONS = { - 'default': { - 'ENGINE': 'DjangoBlog.whoosh_cn_backend.WhooshEngine', - 'PATH': os.path.join(os.path.dirname(__file__), 'whoosh_index'), - }, -} -# 自动更新搜索索引 -HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor' -# 允许使用用户名或密码登录 -AUTHENTICATION_BACKENDS = ['accounts.user_login_backend.EmailOrUsernameModelBackend'] - -STATIC_ROOT = os.path.join(SITE_ROOT, 'collectedstatic') - -STATIC_URL = '/static/' -STATICFILES = os.path.join(BASE_DIR, 'static') - -AUTH_USER_MODEL = 'accounts.BlogUser' -LOGIN_URL = '/login/' - -TIME_FORMAT = '%Y-%m-%d %H:%M:%S' -DATE_TIME_FORMAT = '%Y-%m-%d' - -SITE_NAME = '且听风吟' -SITE_URL = 'http://www.lylinux.net' -SITE_DESCRIPTION = '大巧无工,重剑无锋.' -SITE_SEO_DESCRIPTION = '小站主要用来分享和记录学习经验,教程,记录个人生活的点滴以及一些随笔.欢迎大家访问小站' -SITE_SEO_KEYWORDS = 'linux,apache,mysql,服务器,ubuntu,shell,web,csharp,.net,asp,mac,swift' -ARTICLE_SUB_LENGTH = 300 -SHOW_GOOGLE_ADSENSE = False -# bootstrap颜色样式 -BOOTSTRAP_COLOR_TYPES = [ - 'default', 'primary', 'success', 'info', 'warning', 'danger' -] - -# 侧边栏文章数目 -SIDEBAR_ARTICLE_COUNT = 10 -# 侧边栏评论数目 -SIDEBAR_COMMENT_COUNT = 5 - -# 分页 -PAGINATE_BY = 10 -# http缓存时间 -CACHE_CONTROL_MAX_AGE = 2592000 -# cache setting - -CACHES = { - 'default': { - 'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache', - 'LOCATION': 'django_cache', - } -} -CACHE_MIDDLEWARE_SECONDS = 60 * 60 * 10 -CACHE_MIDDLEWARE_KEY_PREFIX = "djangoblog" -CACHE_MIDDLEWARE_ALIAS = 'default' - -# SESSION_ENGINE = "django.contrib.sessions.backends.cache" -# SESSION_CACHE_ALIAS = 'default' - -OAHUTH = { - 'sina': { - 'appkey': '3161614143', - 'appsecret': 'ee17c099317f872eeddb25204ea46721', - 'callbackurl': 'http://www.lylinux.net/oauth/authorize?type=weibo' - }, - 'google': { - 'appkey': 'googleappkey', - 'appsecret': 'googleappsecret', - 'callbackurl': 'http://www.lylinux.net/oauth/authorize?type=google' - }, - 'github': { - 'appkey': 'githubappkey', - 'appsecret': 'githubappsecret', - 'callbackurl': 'http://www.lylinux.net/oauth/authorize?type=github' - }, - 'facebook': { - 'appkey': 'facebookappkey', - 'appsecret': 'facebookappsecret', - 'callbackurl': 'http://www.lylinux.net/oauth/authorize?type=facebook' - } -} - -SITE_ID = 1 -BAIDU_NOTIFY_URL = "http://data.zz.baidu.com/urls?site=https://www.lylinux.net&token=1uAOGrMsUm5syDGn&type=original" - -# Emial: -EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' - -EMAIL_USE_TLS = True -# EMAIL_USE_SSL = True - -EMAIL_HOST = 'smtp.exmail.qq.com' -EMAIL_PORT = 587 -EMAIL_HOST_USER = os.environ.get('DJANGO_EMAIL_USER') -EMAIL_HOST_PASSWORD = os.environ.get('DJANGO_EMAIL_PASSWORD') -DEFAULT_FROM_EMAIL = EMAIL_HOST_USER -SERVER_EMAIL = os.environ.get('DJANGO_EMAIL_USER') -# 设置debug=false 未处理异常邮件通知 -ADMINS = [('liangliang', 'liangliangyy@gmail.com')] -# 微信管理员密码(两次md5获得) -WXADMIN = '995F03AC401D6CABABAEF756FC4D43C7' -LOGGING = { - 'version': 1, - 'disable_existing_loggers': False, - 'formatters': { - 'verbose': { - 'format': '[%(asctime)s] %(levelname)s [%(name)s.%(funcName)s:%(lineno)d] %(message)s', - }, - 'simple': { - 'format': '%(levelname)s %(asctime)s %(message)s' - }, - }, - 'filters': { - 'require_debug_false': { - '()': 'django.utils.log.RequireDebugFalse', - }, - 'require_debug_true': { - '()': 'django.utils.log.RequireDebugTrue', - }, - }, - 'handlers': { - 'log_file': { - 'level': 'INFO', - 'class': 'logging.handlers.RotatingFileHandler', - 'filename': 'djangoblog.log', - 'maxBytes': 16777216, # 16 MB - 'formatter': 'verbose' - }, - 'console': { - 'level': 'DEBUG', - 'filters': ['require_debug_true'], - 'class': 'logging.StreamHandler', - 'formatter': 'simple' - }, - 'null': { - 'class': 'logging.NullHandler', - }, - 'mail_admins': { - 'level': 'ERROR', - 'filters': ['require_debug_false'], - 'class': 'django.utils.log.AdminEmailHandler' - } - }, - 'loggers': { - 'djangoblog': { - 'handlers': ['log_file', 'console'], - 'level': 'INFO', - 'propagate': True, - }, - 'django.request': { - 'handlers': ['mail_admins'], - 'level': 'ERROR', - 'propagate': False, - }, - } -} - -STATICFILES_FINDERS = ( - 'django.contrib.staticfiles.finders.FileSystemFinder', - 'django.contrib.staticfiles.finders.AppDirectoriesFinder', - # other - 'compressor.finders.CompressorFinder', -) -COMPRESS_ENABLED = True -# COMPRESS_OFFLINE = True - - -COMPRESS_CSS_FILTERS = [ - # creates absolute urls from relative ones - 'compressor.filters.css_default.CssAbsoluteFilter', - # css minimizer - 'compressor.filters.cssmin.CSSMinFilter' -] -COMPRESS_JS_FILTERS = [ - 'compressor.filters.jsmin.JSMinFilter' -] From 0090d6d85f5bbfdce7c5147d38083e22523624ef Mon Sep 17 00:00:00 2001 From: liangliangyy Date: Tue, 28 May 2019 23:07:13 +0800 Subject: [PATCH 077/593] update --- travis_test/travis_settings.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/travis_test/travis_settings.py b/travis_test/travis_settings.py index 04b602bbc..50e009bd6 100644 --- a/travis_test/travis_settings.py +++ b/travis_test/travis_settings.py @@ -1,11 +1,5 @@ from DjangoBlog.settings import * -SECRET_KEY = '&3g0bdza#c%dm1lf%5gi&0-*53p3t0m*hmcvo29cn^$ji7je(c' - -# Database -# https://docs.djangoproject.com/en/1.10/ref/settings/#databases - - DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', @@ -19,3 +13,10 @@ 'PORT': 3306, } } + +CACHES = { + 'default': { + 'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache', + 'LOCATION': 'django_cache', + } +} From 433cf06f892e4d8dcfd111ac4cfd4d8157dda61a Mon Sep 17 00:00:00 2001 From: Spaceack Date: Fri, 31 May 2019 09:41:54 +0800 Subject: [PATCH 078/593] FIX: #273 --- templates/blog/article_archives.html | 4 ++-- templates/blog/tags/article_meta_info.html | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/templates/blog/article_archives.html b/templates/blog/article_archives.html index 42649a5c3..971e2c51d 100644 --- a/templates/blog/article_archives.html +++ b/templates/blog/article_archives.html @@ -25,11 +25,11 @@
    - {% regroup article_list by created_time.year as year_post_group %} + {% regroup article_list by pub_time.year as year_post_group %}
      {% for year in year_post_group %}
    • {{ year.grouper }} 年 - {% regroup year.list by created_time.month as month_post_group %} + {% regroup year.list by pub_time.month as month_post_group %}
        {% for month in month_post_group %}
      • {{ month.grouper }} 月 diff --git a/templates/blog/tags/article_meta_info.html b/templates/blog/tags/article_meta_info.html index 4ea817350..c95c870c4 100644 --- a/templates/blog/tags/article_meta_info.html +++ b/templates/blog/tags/article_meta_info.html @@ -3,13 +3,13 @@ {% with article.id|add:user.is_authenticated as cachekey %} {% cache 36000 metainfo cachekey %} {% endcache %} -{% endwith %} \ No newline at end of file +{% endwith %} From 33977575255bc6d7fb35dd35a1152bd6ed80d6bb Mon Sep 17 00:00:00 2001 From: liangliangyy Date: Thu, 13 Jun 2019 21:22:43 +0800 Subject: [PATCH 079/593] =?UTF-8?q?=E4=BF=AE=E6=94=B9mysql=E5=AE=A2?= =?UTF-8?q?=E6=88=B7=E7=AB=AF=E4=B8=BAmysqlclient,=E5=A2=9E=E5=8A=A0issue?= =?UTF-8?q?=E6=A8=A1=E7=89=88,fix=20#227?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/ISSUE_TEMPLATE.md | 18 ++++++++++++++++++ .travis.yml | 1 + DjangoBlog/__init__.py | 2 -- README.md | 2 ++ blog/models.py | 12 +++--------- requirements.txt | 30 ++++++++++++++++-------------- travis_test/__init__.py | 2 -- travis_test/requirements.txt | 30 ++++++++++++++++-------------- 8 files changed, 56 insertions(+), 41 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE.md diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 000000000..2b5b7aa7d --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,18 @@ + + +**我确定我已经查看了** (标注`[ ]`为`[x]`) + +- [ ] [DjangoBlog的readme](https://github.com/liangliangyy/DjangoBlog/blob/master/README.md) +- [ ] [配置说明](https://github.com/liangliangyy/DjangoBlog/blob/master/bin/config.md) +- [ ] [其他 Issues](https://github.com/liangliangyy/DjangoBlog/issues) + +---- + +**我要申请** (标注`[ ]`为`[x]`) + +- [ ] BUG 反馈 +- [ ] 添加新的特性或者功能 +- [ ] 请求技术支持 diff --git a/.travis.yml b/.travis.yml index 5c4c275a9..a3a725f22 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,6 +12,7 @@ branches: - master # command to install dependencies install: + - apt-get install python3-dev default-libmysqlclient-dev - pip install -r travis_test/requirements.txt - pip install python-coveralls - pip install coverage codecov diff --git a/DjangoBlog/__init__.py b/DjangoBlog/__init__.py index c45523b2f..e69de29bb 100644 --- a/DjangoBlog/__init__.py +++ b/DjangoBlog/__init__.py @@ -1,2 +0,0 @@ -import pymysql -pymysql.install_as_MySQLdb() \ No newline at end of file diff --git a/README.md b/README.md index 4a32b3f0a..6b65a1501 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,8 @@ - 网站异常邮件提醒,若有未捕捉到的异常会自动发送提醒邮件。 - 集成了微信公众号功能,现在可以使用微信公众号来管理你的vps了。 ## 安装 +mysql客户端从`pymysql`修改成了`mysqlclient`,具体请参考 [pypi](https://pypi.org/project/mysqlclient/) 查看安装前的准备。 + 使用pip安装: `pip install -Ur requirements.txt` diff --git a/blog/models.py b/blog/models.py index 4c8b7df53..920f0b0d0 100644 --- a/blog/models.py +++ b/blog/models.py @@ -30,21 +30,15 @@ class BaseModel(models.Model): last_mod_time = models.DateTimeField('修改时间', default=now) def save(self, *args, **kwargs): - - if not isinstance(self, Article) and 'slug' in self.__dict__: - if getattr(self, 'slug') == 'no-slug' or not self.id: - slug = getattr(self, 'title') if 'title' in self.__dict__ else getattr(self, 'name') - setattr(self, 'slug', slugify(slug)) is_update_views = isinstance(self, Article) and 'update_fields' in kwargs and kwargs['update_fields'] == [ 'views'] if is_update_views: Article.objects.filter(pk=self.pk).update(views=self.views) else: + if 'slug' in self.__dict__: + slug = getattr(self, 'title') if 'title' in self.__dict__ else getattr(self, 'name') + setattr(self, 'slug', slugify(slug)) super().save(*args, **kwargs) - # is_update_views = 'update_fields' in kwargs and len(kwargs['update_fields']) == 1 and kwargs['update_fields'][ - # 0] == 'views' - # from DjangoBlog.blog_signals import article_save_signal - # article_save_signal.send(sender=self.__class__, is_update_views=is_update_views, id=self.id) def get_full_url(self): site = get_current_site().domain diff --git a/requirements.txt b/requirements.txt index 5b7b7f83a..e29c139c7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,53 +3,55 @@ asn1crypto==0.24.0 astroid==2.2.5 bottle==0.12.16 certifi==2019.3.9 -cffi==1.12.2 +cffi==1.12.3 chardet==3.0.4 coverage==4.5.3 cryptography==2.6.1 -Django==2.1.7 +Django==2.2.1 django-appconf==1.0.3 django-autoslug==1.9.4 django-compressor==2.2 django-debug-toolbar==1.11 django-haystack==2.8.1 django-ipware==2.1.0 -django-mdeditor==0.1.13 +django-mdeditor==0.1.14 django-uuslug==1.1.8 elasticsearch==7.0.0 elasticsearch-dsl==7.0.0 idna==2.8 ipaddress==1.0.22 -isort==4.3.15 +isort==4.3.20 jieba==0.39 jsonpickle==1.1 -lazy-object-proxy==1.3.1 +lazy-object-proxy==1.4.1 markdown2==2.3.7 mccabe==0.6.1 mistune==0.8.4 +mysqlclient==1.4.2.post1 olefile==0.46 packaging==19.0 -Pillow==5.4.1 +Pillow==6.0.0 pycparser==2.19 -Pygments==2.3.1 +Pygments==2.4.0 pylint==2.3.1 -PyMySQL==0.9.3 -pyparsing==2.3.1 +pyparsing==2.4.0 python-dateutil==2.8.0 python-logstash==0.4.6 python-memcached==1.59 -python-slugify==3.0.0 -pytz==2018.9 +python-slugify==3.0.2 +pytz==2019.1 raven==6.10.0 -requests==2.21.0 +rcssmin==1.0.6 +requests==2.22.0 +rjsmin==1.1.0 six==1.12.0 sqlparse==0.3.0 text-unidecode==1.2 typed-ast==1.3.1 Unidecode==1.0.23 -urllib3==1.24.2 +urllib3==1.25.2 webencodings==0.5.1 -WeRoBot==1.8.0 +WeRoBot==1.9.0 Whoosh==2.7.4 wrapt==1.11.1 xmltodict==0.12.0 diff --git a/travis_test/__init__.py b/travis_test/__init__.py index c45523b2f..e69de29bb 100644 --- a/travis_test/__init__.py +++ b/travis_test/__init__.py @@ -1,2 +0,0 @@ -import pymysql -pymysql.install_as_MySQLdb() \ No newline at end of file diff --git a/travis_test/requirements.txt b/travis_test/requirements.txt index aa6d898a9..c5456a03d 100644 --- a/travis_test/requirements.txt +++ b/travis_test/requirements.txt @@ -3,52 +3,54 @@ asn1crypto==0.24.0 astroid==2.2.5 bottle==0.12.16 certifi==2019.3.9 -cffi==1.12.2 +cffi==1.12.3 chardet==3.0.4 coverage==4.5.3 cryptography==2.6.1 -Django==2.1.7 +Django==2.2.1 django-appconf==1.0.3 django-autoslug==1.9.4 django-compressor==2.2 django-debug-toolbar==1.11 django-haystack==2.8.1 django-ipware==2.1.0 -django-mdeditor==0.1.13 +django-mdeditor==0.1.14 django-uuslug==1.1.8 elasticsearch==7.0.0 elasticsearch-dsl==7.0.0 idna==2.8 ipaddress==1.0.22 -isort==4.3.15 +isort==4.3.20 jieba==0.39 jsonpickle==1.1 -lazy-object-proxy==1.3.1 +lazy-object-proxy==1.4.1 markdown2==2.3.7 mccabe==0.6.1 mistune==0.8.4 +mysqlclient==1.4.2.post1 olefile==0.46 packaging==19.0 -Pillow==5.4.1 +Pillow==6.0.0 pycparser==2.19 -Pygments==2.3.1 +Pygments==2.4.0 pylint==2.3.1 -PyMySQL==0.9.3 -pyparsing==2.3.1 +pyparsing==2.4.0 python-dateutil==2.8.0 python-logstash==0.4.6 -python-slugify==3.0.0 -pytz==2018.9 +python-slugify==3.0.2 +pytz==2019.1 raven==6.10.0 -requests==2.21.0 +rcssmin==1.0.6 +requests==2.22.0 +rjsmin==1.1.0 six==1.12.0 sqlparse==0.3.0 text-unidecode==1.2 typed-ast==1.3.1 Unidecode==1.0.23 -urllib3==1.24.2 +urllib3==1.25.2 webencodings==0.5.1 -WeRoBot==1.8.0 +WeRoBot==1.9.0 Whoosh==2.7.4 wrapt==1.11.1 xmltodict==0.12.0 From e1319723d3ad3f22c9f151af2b58cd2b30ea93ed Mon Sep 17 00:00:00 2001 From: liangliangyy Date: Thu, 13 Jun 2019 21:28:58 +0800 Subject: [PATCH 080/593] =?UTF-8?q?=E4=BF=AE=E5=A4=8Dtravis?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index a3a725f22..86626f7b6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,7 +12,7 @@ branches: - master # command to install dependencies install: - - apt-get install python3-dev default-libmysqlclient-dev + - sudo apt-get install python-dev python3-dev default-libmysqlclient-dev - pip install -r travis_test/requirements.txt - pip install python-coveralls - pip install coverage codecov From 9017bda1dc1712fb4fe902ce96676fbc9fb94317 Mon Sep 17 00:00:00 2001 From: liangliangyy Date: Thu, 13 Jun 2019 21:34:35 +0800 Subject: [PATCH 081/593] =?UTF-8?q?=E4=BF=AE=E5=A4=8Dtravis?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .travis.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 86626f7b6..439dc6b80 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,8 +11,10 @@ branches: only: - master # command to install dependencies -install: +before_install: + - sudo apt-get update - sudo apt-get install python-dev python3-dev default-libmysqlclient-dev +install: - pip install -r travis_test/requirements.txt - pip install python-coveralls - pip install coverage codecov From b17fc84385c2643304ae1477e4361289ebbf693a Mon Sep 17 00:00:00 2001 From: liangliangyy Date: Thu, 13 Jun 2019 21:47:17 +0800 Subject: [PATCH 082/593] =?UTF-8?q?=E4=BF=AE=E5=A4=8Dtravis?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 439dc6b80..dcd2f4199 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,7 +13,7 @@ branches: # command to install dependencies before_install: - sudo apt-get update - - sudo apt-get install python-dev python3-dev default-libmysqlclient-dev + - sudo apt-get install python-dev python3-dev libmysqlclient-dev install: - pip install -r travis_test/requirements.txt - pip install python-coveralls From eca4e71cff1201900998f2e59624288c4dcd3da5 Mon Sep 17 00:00:00 2001 From: liangliangyy Date: Sun, 16 Jun 2019 15:03:58 +0800 Subject: [PATCH 083/593] =?UTF-8?q?=E6=B3=A8=E5=86=8C=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E9=82=AE=E7=AE=B1=E9=AA=8C=E8=AF=81=20close=20#267?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- accounts/forms.py | 7 +++ accounts/tests.py | 7 +++ accounts/urls.py | 7 ++- accounts/views.py | 73 ++++++++++++++++++++++-- templates/account/registration_form.html | 12 ---- templates/account/result.html | 22 +++++++ 6 files changed, 108 insertions(+), 20 deletions(-) create mode 100644 templates/account/result.html diff --git a/accounts/forms.py b/accounts/forms.py index a95e35341..d8621b04a 100644 --- a/accounts/forms.py +++ b/accounts/forms.py @@ -16,6 +16,7 @@ from django.forms import widgets from django.conf import settings from django.contrib.auth import get_user_model +from django.core.exceptions import ValidationError class LoginForm(AuthenticationForm): @@ -37,6 +38,12 @@ def __init__(self, *args, **kwargs): self.fields['password2'].widget = widgets.PasswordInput( attrs={'placeholder': "repeat password", "class": "form-control"}) + def clean_email(self): + email = self.cleaned_data['email'] + if get_user_model().objects.filter(email=email).exists(): + raise ValidationError("该邮箱已经存在.") + return email + class Meta: model = get_user_model() fields = ("username", "email") diff --git a/accounts/tests.py b/accounts/tests.py index 0d196d634..37bbceec9 100644 --- a/accounts/tests.py +++ b/accounts/tests.py @@ -6,6 +6,7 @@ from accounts.models import BlogUser from django.urls import reverse from DjangoBlog.utils import * +from django.conf import settings # Create your tests here. @@ -53,6 +54,12 @@ def test_validate_register(self): 'password2': 'password123!q@wE#R$T', }) self.assertEquals(1, len(BlogUser.objects.filter(email='user123@user.com'))) + user = BlogUser.objects.filter(email='user123@user.com')[0] + sign = get_md5(get_md5(settings.SECRET_KEY + str(user.id))) + path = reverse('accounts:result') + url = '{path}?type=validation&id={id}&sign={sign}'.format(path=path, id=user.id, sign=sign) + response = self.client.get(url) + self.assertEqual(response.status_code, 200) self.client.login(username='user1233', password='password123!q@wE#R$T') user = BlogUser.objects.filter(email='user123@user.com')[0] diff --git a/accounts/urls.py b/accounts/urls.py index 8f812e65e..a8d18328b 100644 --- a/accounts/urls.py +++ b/accounts/urls.py @@ -15,14 +15,15 @@ from django.conf.urls import url from django.contrib.auth import views as auth_view - +from django.urls import path from . import views from .forms import LoginForm -app_name="accounts" +app_name = "accounts" urlpatterns = [ url(r'^login/$', views.LoginView.as_view(success_url='/'), name='login', kwargs={'authentication_form': LoginForm}), url(r'^register/$', views.RegisterView.as_view(success_url="/"), name='register'), - url(r'^logout/$', views.LogoutView.as_view(), name='logout') + url(r'^logout/$', views.LogoutView.as_view(), name='logout'), + path(r'account/result.html', views.account_result, name='result') ] diff --git a/accounts/views.py b/accounts/views.py index 0e64e1690..484578242 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -5,7 +5,8 @@ # from django.views.generic.edit import FormView from django.views.generic import FormView, RedirectView from django.contrib.auth import get_user_model -from django.http import HttpResponseRedirect +from django.shortcuts import get_object_or_404 +from django.http import HttpResponseRedirect, HttpResponseForbidden from django.urls import reverse from django.contrib.auth.forms import AuthenticationForm, UserCreationForm from django.contrib.auth import REDIRECT_FIELD_NAME @@ -16,6 +17,8 @@ from django.utils.decorators import method_decorator from django.views.decorators.debug import sensitive_post_parameters from django.utils.http import is_safe_url +from DjangoBlog.utils import send_email, get_md5, get_current_site +from django.conf import settings logger = logging.getLogger(__name__) @@ -27,10 +30,37 @@ class RegisterView(FormView): template_name = 'account/registration_form.html' def form_valid(self, form): - user = form.save(False) - user.save(True) - url = reverse('accounts:login') - return HttpResponseRedirect(url) + if form.is_valid(): + user = form.save(False) + user.is_active = False + user.save(True) + site = get_current_site().domain + sign = get_md5(get_md5(settings.SECRET_KEY + str(user.id))) + + if settings.DEBUG: + site = '127.0.0.1:8000' + path = reverse('account:result') + url = "http://{site}{path}?type=validation&id={id}&sign={sign}".format(site=site, path=path, id=user.id, + sign=sign) + + content = """ +

        请点击下面链接验证您的邮箱

        + + {url} + + 再次感谢您! +
        + 如果上面链接无法打开,请将此链接复制至浏览器。 + {url} + """.format(url=url) + send_email(emailto=[user.email, ], title='验证您的电子邮箱', content=content) + + url = reverse('accounts:result') + '?type=register&id=' + str(user.id) + return HttpResponseRedirect(url) + else: + return self.render_to_response({ + 'form': form + }) class LogoutView(RedirectView): @@ -91,3 +121,36 @@ def get_success_url(self): if not is_safe_url(url=redirect_to, allowed_hosts=[self.request.get_host()]): redirect_to = self.success_url return redirect_to + + +def account_result(request): + type = request.GET.get('type') + id = request.GET.get('id') + + user = get_object_or_404(get_user_model(), id=id) + logger.info(type) + if user.is_active: + return HttpResponseRedirect('/') + if type and type in ['register', 'validation']: + if type == 'register': + content = ''' + 恭喜您注册成功,一封验证邮件已经发送到您 {email} 的邮箱,请验证您的邮箱后登录本站。 + '''.format(email=user.email) + title = '注册成功' + else: + c_sign = get_md5(get_md5(settings.SECRET_KEY + str(user.id))) + sign = request.GET.get('sign') + if sign != c_sign: + return HttpResponseForbidden() + user.is_active = True + user.save() + content = ''' + 恭喜您已经成功的完成邮箱验证,您现在可以使用您的账号来登录本站。 + ''' + title = '验证成功' + return render(request, 'account/result.html', { + 'title': title, + 'content': content + }) + else: + return HttpResponseRedirect('/') diff --git a/templates/account/registration_form.html b/templates/account/registration_form.html index 776442d67..65e7549bf 100644 --- a/templates/account/registration_form.html +++ b/templates/account/registration_form.html @@ -9,10 +9,6 @@ {% csrf_token %} - {% comment %} - - - {% endcomment %} {{ form.non_field_errors }} {% for field in form %} {{ field }} @@ -22,14 +18,6 @@ - {% comment %} -
        - Need help? - -
        -{% endcomment %}
    diff --git a/templates/account/result.html b/templates/account/result.html new file mode 100644 index 000000000..4bee77c63 --- /dev/null +++ b/templates/account/result.html @@ -0,0 +1,22 @@ +{% extends 'share_layout/base.html' %} +{% block header %} + {{ title }} +{% endblock %} +{% block content %} +
    +
    + +
    + +

    {{ content }}

    +
    +
    +
    + + 登录 + | + 回到首页 +
    +
    +
    +{% endblock %} \ No newline at end of file From 54b60cad9bd54cba6dd80c692053e66eda256830 Mon Sep 17 00:00:00 2001 From: liangliangyy Date: Tue, 18 Jun 2019 18:42:27 +0800 Subject: [PATCH 084/593] =?UTF-8?q?=E5=8D=87=E7=BA=A7=E4=BE=9D=E8=B5=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- requirements.txt | 23 ++++++++++++----------- travis_test/requirements.txt | 23 ++++++++++++----------- 2 files changed, 24 insertions(+), 22 deletions(-) diff --git a/requirements.txt b/requirements.txt index e29c139c7..ca731eee5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,29 +2,30 @@ appdirs==1.4.3 asn1crypto==0.24.0 astroid==2.2.5 bottle==0.12.16 -certifi==2019.3.9 +certifi==2019.6.16 cffi==1.12.3 chardet==3.0.4 +colorama==0.4.1 coverage==4.5.3 -cryptography==2.6.1 -Django==2.2.1 +cryptography==2.7 +Django==2.2.2 django-appconf==1.0.3 django-autoslug==1.9.4 -django-compressor==2.2 +django-compressor==2.3 django-debug-toolbar==1.11 django-haystack==2.8.1 django-ipware==2.1.0 django-mdeditor==0.1.14 django-uuslug==1.1.8 -elasticsearch==7.0.0 +elasticsearch==7.0.2 elasticsearch-dsl==7.0.0 idna==2.8 ipaddress==1.0.22 isort==4.3.20 jieba==0.39 -jsonpickle==1.1 +jsonpickle==1.2 lazy-object-proxy==1.4.1 -markdown2==2.3.7 +markdown2==2.3.8 mccabe==0.6.1 mistune==0.8.4 mysqlclient==1.4.2.post1 @@ -32,7 +33,7 @@ olefile==0.46 packaging==19.0 Pillow==6.0.0 pycparser==2.19 -Pygments==2.4.0 +Pygments==2.4.2 pylint==2.3.1 pyparsing==2.4.0 python-dateutil==2.8.0 @@ -47,9 +48,9 @@ rjsmin==1.1.0 six==1.12.0 sqlparse==0.3.0 text-unidecode==1.2 -typed-ast==1.3.1 -Unidecode==1.0.23 -urllib3==1.25.2 +typed-ast==1.4.0 +Unidecode==1.1.0 +urllib3==1.25.3 webencodings==0.5.1 WeRoBot==1.9.0 Whoosh==2.7.4 diff --git a/travis_test/requirements.txt b/travis_test/requirements.txt index c5456a03d..643c5d784 100644 --- a/travis_test/requirements.txt +++ b/travis_test/requirements.txt @@ -2,29 +2,30 @@ appdirs==1.4.3 asn1crypto==0.24.0 astroid==2.2.5 bottle==0.12.16 -certifi==2019.3.9 +certifi==2019.6.16 cffi==1.12.3 chardet==3.0.4 +colorama==0.4.1 coverage==4.5.3 -cryptography==2.6.1 -Django==2.2.1 +cryptography==2.7 +Django==2.2.2 django-appconf==1.0.3 django-autoslug==1.9.4 -django-compressor==2.2 +django-compressor==2.3 django-debug-toolbar==1.11 django-haystack==2.8.1 django-ipware==2.1.0 django-mdeditor==0.1.14 django-uuslug==1.1.8 -elasticsearch==7.0.0 +elasticsearch==7.0.2 elasticsearch-dsl==7.0.0 idna==2.8 ipaddress==1.0.22 isort==4.3.20 jieba==0.39 -jsonpickle==1.1 +jsonpickle==1.2 lazy-object-proxy==1.4.1 -markdown2==2.3.7 +markdown2==2.3.8 mccabe==0.6.1 mistune==0.8.4 mysqlclient==1.4.2.post1 @@ -32,7 +33,7 @@ olefile==0.46 packaging==19.0 Pillow==6.0.0 pycparser==2.19 -Pygments==2.4.0 +Pygments==2.4.2 pylint==2.3.1 pyparsing==2.4.0 python-dateutil==2.8.0 @@ -46,9 +47,9 @@ rjsmin==1.1.0 six==1.12.0 sqlparse==0.3.0 text-unidecode==1.2 -typed-ast==1.3.1 -Unidecode==1.0.23 -urllib3==1.25.2 +typed-ast==1.4.0 +Unidecode==1.1.0 +urllib3==1.25.3 webencodings==0.5.1 WeRoBot==1.9.0 Whoosh==2.7.4 From cc92ee716777e4a202ed119ac0639b5b27aaf37e Mon Sep 17 00:00:00 2001 From: ismdeep Date: Mon, 24 Jun 2019 11:47:53 +0800 Subject: [PATCH 085/593] =?UTF-8?q?=E5=B0=86=E4=B8=AD=E6=96=87=E6=B3=A8?= =?UTF-8?q?=E9=87=8A=E6=94=B9=E6=88=90=E8=8B=B1=E6=96=87=E4=BB=A5=E8=A7=A3?= =?UTF-8?q?=E5=86=B3=E9=83=A8=E5=88=86=E7=B3=BB=E7=BB=9F=E8=BF=90=E8=A1=8C?= =?UTF-8?q?manage.py=E8=84=9A=E6=9C=AC=E7=9A=84=E4=B9=B1=E7=A0=81=E9=94=99?= =?UTF-8?q?=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DjangoBlog/settings.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/DjangoBlog/settings.py b/DjangoBlog/settings.py index 1fe2169fd..50a9015ad 100644 --- a/DjangoBlog/settings.py +++ b/DjangoBlog/settings.py @@ -148,9 +148,9 @@ 'PATH': os.path.join(os.path.dirname(__file__), 'whoosh_index'), }, } -# 自动更新搜索索引 +# Automatically update searching index HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor' -# 允许使用用户名或密码登录 +# Allow user login with username and password AUTHENTICATION_BACKENDS = ['accounts.user_login_backend.EmailOrUsernameModelBackend'] STATIC_ROOT = os.path.join(SITE_ROOT, 'collectedstatic') @@ -164,14 +164,14 @@ TIME_FORMAT = '%Y-%m-%d %H:%M:%S' DATE_TIME_FORMAT = '%Y-%m-%d' -# bootstrap颜色样式 +# bootstrap color styles BOOTSTRAP_COLOR_TYPES = [ 'default', 'primary', 'success', 'info', 'warning', 'danger' ] -# 分页 +# paginate PAGINATE_BY = 10 -# http缓存时间 +# http cache timeout CACHE_CONTROL_MAX_AGE = 2592000 # cache setting CACHES = { @@ -203,9 +203,9 @@ EMAIL_HOST_PASSWORD = os.environ.get('DJANGO_EMAIL_PASSWORD') DEFAULT_FROM_EMAIL = EMAIL_HOST_USER SERVER_EMAIL = os.environ.get('DJANGO_EMAIL_USER') -# 设置debug=false 未处理异常邮件通知 +# Setting debug=false did NOT handle except email notifications ADMINS = [('liangliang', 'liangliangyy@gmail.com')] -# 微信管理员密码(两次md5获得) +# WX ADMIN password(Two times md5) WXADMIN = '995F03AC401D6CABABAEF756FC4D43C7' LOGGING = { From a3919c62c0f8fc4dfea60baccaa080294b31cbb8 Mon Sep 17 00:00:00 2001 From: ismdeep Date: Mon, 24 Jun 2019 11:49:06 +0800 Subject: [PATCH 086/593] =?UTF-8?q?Docs:=20=E6=9B=B4=E6=96=B0=20README.md?= =?UTF-8?q?=20=E6=96=87=E4=BB=B6=EF=BC=8C=E5=A2=9E=E5=8A=A0=20`manage.py`?= =?UTF-8?q?=20=E6=96=87=E4=BB=B6=E4=BD=BF=E7=94=A8=E8=AF=B4=E6=98=8E?= =?UTF-8?q?=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 227 +++++++++++++++++++++++++++++------------------------- 1 file changed, 122 insertions(+), 105 deletions(-) diff --git a/README.md b/README.md index 6b65a1501..dfd311f3d 100644 --- a/README.md +++ b/README.md @@ -1,105 +1,122 @@ -# DjangoBlog - -基于`python3.6`和`Django2.1`的博客。 - -[![Build Status](https://travis-ci.org/liangliangyy/DjangoBlog.svg?branch=master)](https://travis-ci.org/liangliangyy/DjangoBlog) [![codecov](https://codecov.io/gh/liangliangyy/DjangoBlog/branch/master/graph/badge.svg)](https://codecov.io/gh/liangliangyy/DjangoBlog) [![Requirements Status](https://requires.io/github/liangliangyy/DjangoBlog/requirements.svg?branch=master)](https://requires.io/github/liangliangyy/DjangoBlog/requirements/?branch=master) [![license](https://img.shields.io/github/license/liangliangyy/djangoblog.svg)]() - -## 主要功能: -- 文章,页面,分类目录,标签的添加,删除,编辑等。文章及页面支持`Markdown`,支持代码高亮。 -- 支持文章全文搜索。 -- 完整的评论功能,包括发表回复评论,以及评论的邮件提醒,支持`Markdown`。 -- 侧边栏功能,最新文章,最多阅读,标签云等。 -- 支持Oauth登陆,现已有Google,GitHub,facebook,微博,QQ登录。 -- 支持`Memcache`缓存,支持缓存自动刷新。 -- 简单的SEO功能,新建文章等会自动通知Google和百度。 -- 集成了简单的图床功能。 -- 集成`django-compressor`,自动压缩`css`,`js`。 -- 网站异常邮件提醒,若有未捕捉到的异常会自动发送提醒邮件。 -- 集成了微信公众号功能,现在可以使用微信公众号来管理你的vps了。 -## 安装 -mysql客户端从`pymysql`修改成了`mysqlclient`,具体请参考 [pypi](https://pypi.org/project/mysqlclient/) 查看安装前的准备。 - -使用pip安装: -`pip install -Ur requirements.txt` - -如果你没有pip,使用如下方式安装: -OS X / Linux 电脑,终端下执行: - - curl http://peak.telecommunity.com/dist/ez_setup.py | python - curl https://raw.github.com/pypa/pip/master/contrib/get-pip.py | python - -windows电脑: - 下载 http://peak.telecommunity.com/dist/ez_setup.py 和 https://raw.github.com/pypa/pip/master/contrib/get-pip.py 这两个文件,双击运行。 - -### 配置 -配置都是在`setting.py`中.部分配置迁移到了后台配置中。 - -很多`setting`配置我都是写在环境变量里面的.并没有提交到`github`中来.例如`SECRET_KEY`,`OAHUTH`,`mysql`以及邮件部分的配置等.你可以直接修改代码成你自己的,或者在环境变量里面加入对应的配置就可以了. - -`test`目录中的文件都是为了`travis`自动化测试使用的.不用去关注.或者直接使用.这样就可以集成`travis`自动化测试了. - -`bin`目录是在`linux`环境中使用`Nginx`+`Gunicorn`+`virtualenv`+`supervisor`来部署的脚本和`Nginx`配置文件.可以参考我的文章: - ->[使用Nginx+Gunicorn+virtualenv+supervisor来部署django项目](https://www.lylinux.org/%E4%BD%BF%E7%94%A8nginxgunicornvirtualenvsupervisor%E6%9D%A5%E9%83%A8%E7%BD%B2django%E9%A1%B9%E7%9B%AE.html) - -有详细的部署介绍. - - -## 运行 - - 修改`DjangoBlog/setting.py` 修改数据库配置,如下所示: - - DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.mysql', - 'NAME': 'djangoblog', - 'USER': 'root', - 'PASSWORD': 'password', - 'HOST': 'host', - 'PORT': 3306, - } - } - -### 创建数据库 -mysql数据库中执行: -```sql -CREATE DATABASE `djangoblog` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci */; -``` - 然后终端下执行: - - ./manage.py makemigrations - ./manage.py migrate -### 创建超级用户 - - 终端下执行: - - ./manage.py createsuperuser -### 创建测试数据 -终端下执行: - - ./manage.py create_testdata -### 收集静态文件 -终端下执行:   - - ./manage.py collectstatic --noinput - ./manage.py compress --force -### 开始运行: - 执行: - `./manage.py runserver` - - - - - - 浏览器打开: http://127.0.0.1:8000/ 就可以看到效果了。 -## 更多配置: -[更多配置介绍](/bin/config.md) - ## 问题相关 - - 有任何问题欢迎提Issue,或者将问题描述发送至我邮箱 `liangliangyy#gmail.com`.我会尽快解答.推荐提交Issue方式. - - --- - ## 致大家🙋‍♀️🙋‍♂️ - 如果本项目帮助到了你,请在[这里](https://github.com/liangliangyy/DjangoBlog/issues/214)留下你的网址,让更多的人看到。 -您的回复将会是我继续更新维护下去的动力。 -🙏🙏🙏 +# DjangoBlog + +基于`python3.6`和`Django2.1`的博客。 + +[![Build Status](https://travis-ci.org/liangliangyy/DjangoBlog.svg?branch=master)](https://travis-ci.org/liangliangyy/DjangoBlog) [![codecov](https://codecov.io/gh/liangliangyy/DjangoBlog/branch/master/graph/badge.svg)](https://codecov.io/gh/liangliangyy/DjangoBlog) [![Requirements Status](https://requires.io/github/liangliangyy/DjangoBlog/requirements.svg?branch=master)](https://requires.io/github/liangliangyy/DjangoBlog/requirements/?branch=master) [![license](https://img.shields.io/github/license/liangliangyy/djangoblog.svg)]() + +## 主要功能: +- 文章,页面,分类目录,标签的添加,删除,编辑等。文章及页面支持`Markdown`,支持代码高亮。 +- 支持文章全文搜索。 +- 完整的评论功能,包括发表回复评论,以及评论的邮件提醒,支持`Markdown`。 +- 侧边栏功能,最新文章,最多阅读,标签云等。 +- 支持Oauth登陆,现已有Google,GitHub,facebook,微博,QQ登录。 +- 支持`Memcache`缓存,支持缓存自动刷新。 +- 简单的SEO功能,新建文章等会自动通知Google和百度。 +- 集成了简单的图床功能。 +- 集成`django-compressor`,自动压缩`css`,`js`。 +- 网站异常邮件提醒,若有未捕捉到的异常会自动发送提醒邮件。 +- 集成了微信公众号功能,现在可以使用微信公众号来管理你的vps了。 + +## 安装 +mysql客户端从`pymysql`修改成了`mysqlclient`,具体请参考 [pypi](https://pypi.org/project/mysqlclient/) 查看安装前的准备。 + +使用pip安装: `pip install -Ur requirements.txt` + +如果你没有pip,使用如下方式安装: +- OS X / Linux 电脑,终端下执行: + + ``` + curl http://peak.telecommunity.com/dist/ez_setup.py | python + curl https://raw.github.com/pypa/pip/master/contrib/get-pip.py | python + ``` + +- Windows电脑: + + 下载 http://peak.telecommunity.com/dist/ez_setup.py 和 https://raw.github.com/pypa/pip/master/contrib/get-pip.py 这两个文件,双击运行。 + +### 配置 +配置都是在 `setting.py` 中,部分配置迁移到了后台配置中。 + +很多 `setting` 配置我都是写在环境变量里面的.并没有提交到 `github` 中来.例如`SECRET_KEY`,`OAHUTH`,`mysql`以及邮件部分的配置等.你可以直接修改代码成你自己的,或者在环境变量里面加入对应的配置就可以了. + +`test`目录中的文件都是为了`travis`自动化测试使用的.不用去关注.或者直接使用.这样就可以集成`travis`自动化测试了. + +`bin`目录是在`linux`环境中使用`Nginx`+`Gunicorn`+`virtualenv`+`supervisor`来部署的脚本和`Nginx`配置文件.可以参考我的文章: + +>[使用Nginx+Gunicorn+virtualenv+supervisor来部署django项目](https://www.lylinux.org/%E4%BD%BF%E7%94%A8nginxgunicornvirtualenvsupervisor%E6%9D%A5%E9%83%A8%E7%BD%B2django%E9%A1%B9%E7%9B%AE.html) + +有详细的部署介绍. + + +## 运行 + + 修改`DjangoBlog/setting.py` 修改数据库配置,如下所示: + +```python +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.mysql', + 'NAME': 'djangoblog', + 'USER': 'root', + 'PASSWORD': 'password', + 'HOST': 'host', + 'PORT': 3306, + } +} +``` + +### 创建数据库 +mysql数据库中执行: +```sql +CREATE DATABASE `djangoblog` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci */; +``` + +然后终端下执行: +```bash +./manage.py makemigrations +./manage.py migrate +``` + +**注意:** 在使用 `./manage.py` 之前需要确定你系统中的 `python` 命令是指向 `python 3.6` 及以上版本的。如果不是如此,请使用以下两种方式中的一种: + +- 修改 `manage.py` 第一行 `#!/usr/bin/env python` 为 `#!/usr/bin/env python3` +- 直接使用 `python3 ./manage.py makemigrations` + +### 创建超级用户 + + 终端下执行: +```bash +./manage.py createsuperuser +``` + +### 创建测试数据 +终端下执行: +```bash +./manage.py create_testdata +``` + +### 收集静态文件 +终端下执行:   +```bash +./manage.py collectstatic --noinput +./manage.py compress --force +``` + +### 开始运行: +执行: `./manage.py runserver` + + +浏览器打开: http://127.0.0.1:8000/ 就可以看到效果了。 +## 更多配置: +[更多配置介绍](/bin/config.md) + +## 问题相关 + +有任何问题欢迎提Issue,或者将问题描述发送至我邮箱 `liangliangyy#gmail.com`.我会尽快解答.推荐提交Issue方式. + +--- + ## 致大家🙋‍♀️🙋‍♂️ + 如果本项目帮助到了你,请在[这里](https://github.com/liangliangyy/DjangoBlog/issues/214)留下你的网址,让更多的人看到。 +您的回复将会是我继续更新维护下去的动力。 + +🙏🙏🙏 From eedd6298c721cba49d24c52969c4b010eacae84e Mon Sep 17 00:00:00 2001 From: ismdeep Date: Mon, 24 Jun 2019 22:06:07 +0800 Subject: [PATCH 087/593] Add README English version --- README-en.md | 124 +++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 3 ++ 2 files changed, 127 insertions(+) create mode 100644 README-en.md diff --git a/README-en.md b/README-en.md new file mode 100644 index 000000000..8c3116864 --- /dev/null +++ b/README-en.md @@ -0,0 +1,124 @@ +# DjangoBlog + +🌍 +*[English](README-en.md) ∙ [简体中文](README.md)* + +A blog system based on `python3.6` and `Django2.1`. + +[![Build Status](https://travis-ci.org/liangliangyy/DjangoBlog.svg?branch=master)](https://travis-ci.org/liangliangyy/DjangoBlog) [![codecov](https://codecov.io/gh/liangliangyy/DjangoBlog/branch/master/graph/badge.svg)](https://codecov.io/gh/liangliangyy/DjangoBlog) [![Requirements Status](https://requires.io/github/liangliangyy/DjangoBlog/requirements.svg?branch=master)](https://requires.io/github/liangliangyy/DjangoBlog/requirements/?branch=master) [![license](https://img.shields.io/github/license/liangliangyy/djangoblog.svg)]() + +## Main Features: +- Articles, Pages, Categories, Tags(Add, Delete, Edit), edc. Articles and pages support `Markdown` and highlighting. +- Articles support full-text search. +- Complete comment feature, include posting reply comment and email notification. `Markdown` supporting. +- Sidebar feature: new articles, most readings, tags, etc. +- OAuth Login supported, including Google, GitHub, Facebook, Weibo, QQ. +- `Memcache` supported, with cache auto refresh. +- Simple SEO Features, notify Google and Baidu when there was a new article or other things. +- Simple picture bed feature integrated. +- `django-compressor` integrated, auto-compressed `css`, `js`. +- Website exception email notification. When there is an unhandle exception, system will send an email notification. +- Wechat official account feature integrated. Now, you can use wechat official account to manage your VPS. + +## Installation +Change MySQL client from `pymysql` to `mysqlclient`, more details please reference [pypi](https://pypi.org/project/mysqlclient/) , checkout preperation before installation. + +Install via pip: `pip install -Ur requirements.txt` + +If you do NOT have `pip`, please use the following methods to install: +- OS X / Linux, run the following commands: + + ``` + curl http://peak.telecommunity.com/dist/ez_setup.py | python + curl https://raw.github.com/pypa/pip/master/contrib/get-pip.py | python + ``` + +- Windows: + + Download http://peak.telecommunity.com/dist/ez_setup.py and https://raw.github.com/pypa/pip/master/contrib/get-pip.py, and run with python. + +### Configuration +Most configurations are in `setting.py`, others are in backend configurations. + +I set many `setting` configuration with my environment variables (such as: `SECRET_KEY`, `OAUTH`, `mysql` and some email configuration parts.) and they did NOT been submitted to the `GitHub`. You can change these in the code with your own configuration or just add them into your environment variables. + +Files in `test` directory are for `travis` with automatic testing. You do not need to care about this. Or just use it, in this way to integrate `travis` for automatic testing. + +In `bin` directory, we have scripts to deploy with `Nginx`+`Gunicorn`+`virtualenv`+`supervisor` on `linux` and `Nginx` configuration file. You can reference with my article + +>[Using Nginx+Gunicorn+virtualenv+supervisor to deploy django project.](https://www.lylinux.org/%E4%BD%BF%E7%94%A8nginxgunicornvirtualenvsupervisor%E6%9D%A5%E9%83%A8%E7%BD%B2django%E9%A1%B9%E7%9B%AE.html) + +More deploy detail in this article. + +## Run + +Modify `DjangoBlog/setting.py` with database settings, as following: + +```python +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.mysql', + 'NAME': 'djangoblog', + 'USER': 'root', + 'PASSWORD': 'password', + 'HOST': 'host', + 'PORT': 3306, + } +} +``` + +### Create database +Run the following command in MySQL shell: +```sql +CREATE DATABASE `djangoblog` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci */; +``` + +Run the following commands in Terminal: +```bash +./manage.py makemigrations +./manage.py migrate +``` + +**Attention: ** Before you using `./manage.py`, make sure the `python` command in your system is towards to `python 3.6` or above version. Otherwise you may solve this by one of the two following methods: +- Modify the first line in `manage.py`, change `#!/usr/bin/env python` to `#!/usr/bin/env python3` +- Just run with: `python3 ./manage.py makemigrations` + +### Create super user + +Run command in terminal: +```bash +./manage.py createsuperuser +``` + +### Create testing data +Run command in terminal: +```bash +./manage.py create_testdata +``` + +### Collect static files +Run command in terminal: +```bash +./manage.py collectstatic --noinput +./manage.py compress --force +``` + +### Getting start to run server +Execute: `./manage.py runserver` + +Open up a browser and visit: http://127.0.0.1:8000/ , the you will see the blog. + +## More configurations +[More configurations details](/bin/config.md) + +## About the issues + +If you have any *question*, please use Issue or send problem descriptions to my email `liangliangyy#gmail.com`. I will reponse you as soon as possible. And, we recommend you to use Issue. + +--- +## To Everyone 🙋‍♀️🙋‍♂️ +If this project helps you, please submit your site address [here](https://github.com/liangliangyy/DjangoBlog/issues/214) to let more people see it. + +Your reply will be the driving force for me to continue to update and maintain this project. + +🙏🙏🙏 diff --git a/README.md b/README.md index dfd311f3d..9e24346b7 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,8 @@ # DjangoBlog +🌍 +*[English](README-en.md) ∙ [简体中文](README.md)* + 基于`python3.6`和`Django2.1`的博客。 [![Build Status](https://travis-ci.org/liangliangyy/DjangoBlog.svg?branch=master)](https://travis-ci.org/liangliangyy/DjangoBlog) [![codecov](https://codecov.io/gh/liangliangyy/DjangoBlog/branch/master/graph/badge.svg)](https://codecov.io/gh/liangliangyy/DjangoBlog) [![Requirements Status](https://requires.io/github/liangliangyy/DjangoBlog/requirements.svg?branch=master)](https://requires.io/github/liangliangyy/DjangoBlog/requirements/?branch=master) [![license](https://img.shields.io/github/license/liangliangyy/djangoblog.svg)]() From e22ad9bd142b9bb8516d2c0cf65e8c0ffd0f5ff0 Mon Sep 17 00:00:00 2001 From: ismdeep Date: Sun, 30 Jun 2019 14:16:38 +0800 Subject: [PATCH 088/593] Move bin/config.md to docs/config.md Add docs/config-en.md as English version of config.md --- .gitignore | 1 + README-en.md | 2 +- README.md | 2 +- docs/config-en.md | 64 +++++++++++++++++++++++++++++++++++++++++ {bin => docs}/config.md | 2 +- 5 files changed, 68 insertions(+), 3 deletions(-) create mode 100644 docs/config-en.md rename {bin => docs}/config.md (95%) diff --git a/.gitignore b/.gitignore index df0809fed..9aa5e1490 100644 --- a/.gitignore +++ b/.gitignore @@ -76,3 +76,4 @@ werobot_session django.jpg uploads/ settings_production.py +werobot_session.db diff --git a/README-en.md b/README-en.md index 8c3116864..5ad8d41f9 100644 --- a/README-en.md +++ b/README-en.md @@ -109,7 +109,7 @@ Execute: `./manage.py runserver` Open up a browser and visit: http://127.0.0.1:8000/ , the you will see the blog. ## More configurations -[More configurations details](/bin/config.md) +[More configurations details](/docs/config-en.md) ## About the issues diff --git a/README.md b/README.md index 9e24346b7..a29611e6b 100644 --- a/README.md +++ b/README.md @@ -111,7 +111,7 @@ CREATE DATABASE `djangoblog` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8 浏览器打开: http://127.0.0.1:8000/ 就可以看到效果了。 ## 更多配置: -[更多配置介绍](/bin/config.md) +[更多配置介绍](/docs/config.md) ## 问题相关 diff --git a/docs/config-en.md b/docs/config-en.md new file mode 100644 index 000000000..b877efbd5 --- /dev/null +++ b/docs/config-en.md @@ -0,0 +1,64 @@ +# Introduction to main features settings + +## Cache: +Cache using `memcache` for default. If you don't have `memcache` environment, you can remove the `default` setting in `CACHES` and change `locmemcache` to `default`. +```python +CACHES = { + 'default': { + 'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache', + 'LOCATION': '127.0.0.1:11211', + 'KEY_PREFIX': 'django_test' if TESTING else 'djangoblog', + 'TIMEOUT': 60 * 60 * 10 + }, + 'locmemcache': { + 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', + 'TIMEOUT': 10800, + 'LOCATION': 'unique-snowflake', + } +} +``` + +## OAuth Login: +QQ, Weibo, Google, GitHub and Facebook are now supported for OAuth login. Fetch OAuth login permissions from the corresponding open platform, and save them with `appkey`, `appsecret` and callback address in **Backend->OAuth** configuration. + +### Callback address examples: +QQ: http://your-domain-name/oauth/authorize?type=qq +Weibo: http://your-domain-name/oauth/authorize?type=weibo +type is in the type field of `oauthmanager`. + +## owntracks: +owntracks is a location tracking application. It will send your locaiton to the server by timing.Simple support owntracks features. Just install owntracks app and set api address as `your-domain-name/owntracks/logtracks`. Visit `your-domain-name/owntracks/show_dates` and you will see the date with latitude and langitude, click it and see the motion track. The map is drawn by AMap. + +## Email feature: +Same as before, Configure your own error msg recvie email information with`ADMINS = [('liangliang', 'liangliangyy@gmail.com')]` in `settings.py`. And modify: +```python +EMAIL_HOST = 'smtp.zoho.com' +EMAIL_PORT = 587 +EMAIL_HOST_USER = os.environ.get('DJANGO_EMAIL_USER') +EMAIL_HOST_PASSWORD = os.environ.get('DJANGO_EMAIL_PASSWORD') +DEFAULT_FROM_EMAIL = EMAIL_HOST_USER +SERVER_EMAIL = os.environ.get('DJANGO_EMAIL_USER') +``` +with your email account information. + +## WeChat Official Account +Simple wechat official account features integrated. Set token as `your-domain-name/robot` in wechat backend. Default token is `lylinux`, you can change it to your own in `servermanager/robot.py`. Add a new command in `Backend->Servermanager->command`, in this way, you can manage the system through wechat official account. + +## Introduction to website configuration +You can add website configuration in **Backend->BLOG->WebSiteConfiguration**. Such as: keywords, description, Google Ad, website stats code, case number, etc. +OAuth user avatar path is saved in *StaticFileSavedAddress*. Please input absolute path, code directory for default. + +## Source code highlighting +If the code block in your article didn't show hightlight, please write the code blocks as following: + +![](https://resource.lylinux.net/image/codelang.png) + +That is, you should add the corresponding language name before the code block. + +## Update +If you get errors as following while executing database migrations: +```python +django.db.migrations.exceptions.MigrationSchemaMissing: Unable to create the django_migrations table ((1064, "You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '(6) NOT NULL)' at line 1")) +``` +This problem may cause by the mysql version under 5.6, a new version( >= 5.6 ) mysql is needed. + diff --git a/bin/config.md b/docs/config.md similarity index 95% rename from bin/config.md rename to docs/config.md index 76dac74f9..73555dc70 100644 --- a/bin/config.md +++ b/docs/config.md @@ -19,7 +19,7 @@ CACHES = { ``` ## oauth登录: -现在已经支持微博,Google,GitHub,Facebook登录,需要在其对应的开放平台申请oauth登录权限,然后在 +现在已经支持QQ,微博,Google,GitHub,Facebook登录,需要在其对应的开放平台申请oauth登录权限,然后在 **后台->Oauth** 配置中新增配置,填写对应的`appkey`和`appsecret`以及回调地址。 ### 回调地址示例: qq:http://你的域名/oauth/authorize?type=qq From 6f4df53401973d202e1579236fbdda21d844d254 Mon Sep 17 00:00:00 2001 From: liangliangyy Date: Tue, 2 Jul 2019 20:57:47 +0800 Subject: [PATCH 089/593] =?UTF-8?q?=E5=8D=87=E7=BA=A7=E4=BE=9D=E8=B5=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DjangoBlog/urls.py | 1 + requirements.txt | 17 ++++++++--------- travis_test/requirements.txt | 17 ++++++++--------- 3 files changed, 17 insertions(+), 18 deletions(-) diff --git a/DjangoBlog/urls.py b/DjangoBlog/urls.py index 442d55935..a76010401 100644 --- a/DjangoBlog/urls.py +++ b/DjangoBlog/urls.py @@ -46,6 +46,7 @@ url(r'^sitemap\.xml$', sitemap, {'sitemaps': sitemaps}, name='django.contrib.sitemaps.views.sitemap'), url(r'^feed/$', DjangoBlogFeed()), + url(r'^rss/$', DjangoBlogFeed()), url(r'^search', include('haystack.urls'), name='search'), url(r'', include('servermanager.urls', namespace='servermanager')), url(r'', include('owntracks.urls', namespace='owntracks')) diff --git a/requirements.txt b/requirements.txt index ca731eee5..e3c789226 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,27 +1,26 @@ appdirs==1.4.3 asn1crypto==0.24.0 astroid==2.2.5 -bottle==0.12.16 +bottle==0.12.17 certifi==2019.6.16 cffi==1.12.3 chardet==3.0.4 -colorama==0.4.1 coverage==4.5.3 cryptography==2.7 -Django==2.2.2 +Django==2.2.3 django-appconf==1.0.3 django-autoslug==1.9.4 django-compressor==2.3 -django-debug-toolbar==1.11 +django-debug-toolbar==2.0 django-haystack==2.8.1 django-ipware==2.1.0 django-mdeditor==0.1.14 django-uuslug==1.1.8 -elasticsearch==7.0.2 +elasticsearch==7.0.0 elasticsearch-dsl==7.0.0 idna==2.8 ipaddress==1.0.22 -isort==4.3.20 +isort==4.3.21 jieba==0.39 jsonpickle==1.2 lazy-object-proxy==1.4.1 @@ -48,11 +47,11 @@ rjsmin==1.1.0 six==1.12.0 sqlparse==0.3.0 text-unidecode==1.2 -typed-ast==1.4.0 -Unidecode==1.1.0 +typed-ast==1.3.1 +Unidecode==1.1.1 urllib3==1.25.3 webencodings==0.5.1 WeRoBot==1.9.0 Whoosh==2.7.4 -wrapt==1.11.1 +wrapt==1.11.2 xmltodict==0.12.0 diff --git a/travis_test/requirements.txt b/travis_test/requirements.txt index 643c5d784..f2649dba6 100644 --- a/travis_test/requirements.txt +++ b/travis_test/requirements.txt @@ -1,27 +1,26 @@ appdirs==1.4.3 asn1crypto==0.24.0 astroid==2.2.5 -bottle==0.12.16 +bottle==0.12.17 certifi==2019.6.16 cffi==1.12.3 chardet==3.0.4 -colorama==0.4.1 coverage==4.5.3 cryptography==2.7 -Django==2.2.2 +Django==2.2.3 django-appconf==1.0.3 django-autoslug==1.9.4 django-compressor==2.3 -django-debug-toolbar==1.11 +django-debug-toolbar==2.0 django-haystack==2.8.1 django-ipware==2.1.0 django-mdeditor==0.1.14 django-uuslug==1.1.8 -elasticsearch==7.0.2 +elasticsearch==7.0.0 elasticsearch-dsl==7.0.0 idna==2.8 ipaddress==1.0.22 -isort==4.3.20 +isort==4.3.21 jieba==0.39 jsonpickle==1.2 lazy-object-proxy==1.4.1 @@ -47,11 +46,11 @@ rjsmin==1.1.0 six==1.12.0 sqlparse==0.3.0 text-unidecode==1.2 -typed-ast==1.4.0 -Unidecode==1.1.0 +typed-ast==1.3.1 +Unidecode==1.1.1 urllib3==1.25.3 webencodings==0.5.1 WeRoBot==1.9.0 Whoosh==2.7.4 -wrapt==1.11.1 +wrapt==1.11.2 xmltodict==0.12.0 From 260244c5113610fcbe437daabf4735512de9a62a Mon Sep 17 00:00:00 2001 From: liangliangyy Date: Thu, 4 Jul 2019 22:19:10 +0800 Subject: [PATCH 090/593] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E5=90=8D=E5=8F=AF=E8=83=BD=E4=B8=BA=E7=A9=BA=E7=9A=84=E9=94=99?= =?UTF-8?q?=E8=AF=AF=EF=BC=8C=E4=BF=AE=E5=A4=8D=E5=90=8E=E5=8F=B0=E4=B8=8D?= =?UTF-8?q?=E8=83=BD=E4=BF=AE=E6=94=B9=E5=AF=86=E7=A0=81=E7=9A=84=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- accounts/admin.py | 24 ++++++++++++++++-------- accounts/models.py | 1 + accounts/views.py | 3 ++- oauth/views.py | 8 ++++++-- 4 files changed, 25 insertions(+), 11 deletions(-) diff --git a/accounts/admin.py b/accounts/admin.py index eebb1657c..7333b8d49 100644 --- a/accounts/admin.py +++ b/accounts/admin.py @@ -5,6 +5,8 @@ from django.contrib.auth.forms import ReadOnlyPasswordHashField # Register your models here. from .models import BlogUser +from django.utils.translation import gettext, gettext_lazy as _ +from django.contrib.auth.forms import UsernameField class BlogUserCreationForm(forms.ModelForm): @@ -28,23 +30,29 @@ def save(self, commit=True): user = super().save(commit=False) user.set_password(self.cleaned_data["password1"]) if commit: + user.source = 'adminsite' user.save() return user -class BlogUserChangeForm(forms.ModelForm): - password = ReadOnlyPasswordHashField +class BlogUserChangeForm(UserChangeForm): + password = ReadOnlyPasswordHashField( + label=_("Password"), + help_text=_( + "Raw passwords are not stored, so there is no way to see this " + "user's password, but you can change the password using " + "this form." + ), + ) email = forms.EmailField(label="Email", widget=forms.EmailInput) class Meta: model = BlogUser - fields = ('email', 'password', 'is_active') + fields = '__all__' + field_classes = {'username': UsernameField} - def clean_password(self): - # Regardless of what the user provides, return the initial value. - # This is done here, rather than on the field, because the - # field does not have access to the initial value - return self.initial["password"] + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) class BlogUserAdmin(UserAdmin): diff --git a/accounts/models.py b/accounts/models.py index 12b4f617b..b7d4025e4 100644 --- a/accounts/models.py +++ b/accounts/models.py @@ -11,6 +11,7 @@ class BlogUser(AbstractUser): nickname = models.CharField('昵称', max_length=100, blank=True) created_time = models.DateTimeField('创建时间', default=now) last_mod_time = models.DateTimeField('修改时间', default=now) + source = models.CharField("创建涞源", max_length=100, blank=True) # objects = BlogUserManager() diff --git a/accounts/views.py b/accounts/views.py index 484578242..77a5e4ce8 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -33,6 +33,7 @@ def form_valid(self, form): if form.is_valid(): user = form.save(False) user.is_active = False + user.source = 'Register' user.save(True) site = get_current_site().domain sign = get_md5(get_md5(settings.SECRET_KEY + str(user.id))) @@ -106,7 +107,7 @@ def form_valid(self, form): if cache and cache is not None: cache.clear() logger.info(self.redirect_field_name) - redirect_to = self.request.GET.get(self.redirect_field_name) + auth.login(self.request, form.get_user()) return super(LoginView, self).form_valid(form) # return HttpResponseRedirect('/') diff --git a/oauth/views.py b/oauth/views.py index 9c828dc80..51aef0ade 100644 --- a/oauth/views.py +++ b/oauth/views.py @@ -2,6 +2,7 @@ # Create your views here. from urllib.parse import urlparse +import datetime from django.conf import settings from django.http import HttpResponse, HttpResponseRedirect from django.contrib.auth import get_user_model @@ -70,7 +71,7 @@ def authorize(request): return HttpResponseRedirect(manager.get_authorization_url(nexturl)) user = manager.get_oauth_userinfo() if user: - if not user.nikename: + if not user.nikename.strip(): import datetime user.nikename = "djangoblog" + datetime.datetime.now().strftime('%y%m%d%I%M%S') try: @@ -96,6 +97,7 @@ def authorize(request): author = result[0] if result[1]: author.username = user.nikename + author.source = 'authorize' author.save() user.author = author @@ -127,7 +129,9 @@ def emailconfirm(request, id, sign): result = get_user_model().objects.get_or_create(email=oauthuser.email) author = result[0] if result[1]: - author.username = oauthuser.nikename + author.source = 'emailconfirm' + author.username = oauthuser.nikename.strip() if oauthuser.nikename.strip() else "djangoblog" + datetime.datetime.now().strftime( + '%y%m%d%I%M%S') author.save() oauthuser.author = author oauthuser.save() From 68db67a36e725af44a0872b5946842213139f702 Mon Sep 17 00:00:00 2001 From: liangliangyy Date: Thu, 4 Jul 2019 22:48:27 +0800 Subject: [PATCH 091/593] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=BF=AE=E6=94=B9?= =?UTF-8?q?=E5=AD=97=E6=AE=B5=E5=AF=BC=E8=87=B4es=E6=8A=A5=E9=94=99bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DjangoBlog/elasticsearch_backend.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/DjangoBlog/elasticsearch_backend.py b/DjangoBlog/elasticsearch_backend.py index 5356eac6b..c5ca846a9 100644 --- a/DjangoBlog/elasticsearch_backend.py +++ b/DjangoBlog/elasticsearch_backend.py @@ -31,7 +31,10 @@ class ElasticSearchBackend(BaseSearchBackend): def __init__(self, connection_alias, **connection_options): super(ElasticSearchBackend, self).__init__(connection_alias, **connection_options) self.manager = ArticleDocumentManager() - self._rebuild(None) + try: + self._rebuild(None) + except: + pass def _get_models(self, iterable): models = iterable if iterable else Article.objects.all() @@ -54,6 +57,7 @@ def _rebuild(self, models): self.manager.update_docs(docs) def update(self, index, iterable, commit=True): + models = self._get_models(iterable) self.manager.update_docs(models) From eb0f79389592f3b910d827eb66618151aa5d1ca5 Mon Sep 17 00:00:00 2001 From: cclauss Date: Thu, 4 Jul 2019 19:40:58 +0200 Subject: [PATCH 092/593] Identity is not the same thing as equality in Python [flake8](http://flake8.pycqa.org) F632 Use ==/!= to compare str, bytes, and int literals $ __python__ ```python >>> 0 == 0.0 True >>> 0 is 0.0 False ``` --- DjangoBlog/whoosh_cn_backend.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DjangoBlog/whoosh_cn_backend.py b/DjangoBlog/whoosh_cn_backend.py index 52e304a5f..d8b43cc4c 100644 --- a/DjangoBlog/whoosh_cn_backend.py +++ b/DjangoBlog/whoosh_cn_backend.py @@ -624,7 +624,7 @@ def _process_results(self, raw_page, highlight=False, query_string='', spelling_ if string_key in index.fields and hasattr(index.fields[string_key], 'convert'): # Special-cased due to the nature of KEYWORD fields. if index.fields[string_key].is_multivalued: - if value is None or len(value) is 0: + if value is None or len(value) == 0: additional_fields[string_key] = [] else: additional_fields[string_key] = value.split(',') From 6002dfca793a82d6acb67b047ba6d797ef9e8700 Mon Sep 17 00:00:00 2001 From: cclauss Date: Fri, 5 Jul 2019 06:27:40 +0200 Subject: [PATCH 093/593] Travis CI: Add Python 3.7 to the testing --- .travis.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index dcd2f4199..cbde81905 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,6 +2,10 @@ language: python python: - "3.5" - "3.6" +matrix: + include: + - python: "3.7" + dist: xenial services: - mysql env: @@ -29,4 +33,4 @@ script: - coverage run manage.py test after_success: - coveralls - - codecov \ No newline at end of file + - codecov From f7e5ecff27bfbdc762497efcd992fbdae714846b Mon Sep 17 00:00:00 2001 From: cclauss Date: Fri, 5 Jul 2019 06:35:50 +0200 Subject: [PATCH 094/593] Run Python 3.7 in allow_failures until we can find a fix --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index cbde81905..422693b66 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,6 +6,8 @@ matrix: include: - python: "3.7" dist: xenial + allow_failures: + - python: "3.7" services: - mysql env: From eee07a7f26dec2c564b9ff4f30c64625b9e6c7df Mon Sep 17 00:00:00 2001 From: liangliangyy Date: Mon, 8 Jul 2019 11:25:55 +0800 Subject: [PATCH 095/593] fix #291 --- .travis.yml | 7 +++++-- travis_test/mysql.cnf | 9 +++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 travis_test/mysql.cnf diff --git a/.travis.yml b/.travis.yml index 422693b66..38b142887 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,11 +1,12 @@ +dist: xenial language: python +sudo: required python: - "3.5" - "3.6" matrix: include: - python: "3.7" - dist: xenial allow_failures: - python: "3.7" services: @@ -20,12 +21,14 @@ branches: before_install: - sudo apt-get update - sudo apt-get install python-dev python3-dev libmysqlclient-dev + - sudo cp $TRAVIS_BUILD_DIR/travis_test/mysql.cnf /etc/mysql/conf.d/ + - sudo service mysql restart install: - pip install -r travis_test/requirements.txt - pip install python-coveralls - pip install coverage codecov before_script: - - mysql -e 'CREATE DATABASE `djangoblog` /*!40100 DEFAULT CHARACTER SET utf8 */;' + - mysql -e 'CREATE DATABASE `djangoblog` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci */;' - python manage.py makemigrations - python manage.py migrate - python manage.py collectstatic --noinput diff --git a/travis_test/mysql.cnf b/travis_test/mysql.cnf new file mode 100644 index 000000000..e6f164586 --- /dev/null +++ b/travis_test/mysql.cnf @@ -0,0 +1,9 @@ +[mysqld] +character-set-server=utf8mb4 +collation-server=utf8mb4_unicode_ci + +[client] +default-character-set = utf8mb4 + +[mysql] +default-character-set = utf8mb4 \ No newline at end of file From 95abed75626995c7b91e564703ebca429979fd69 Mon Sep 17 00:00:00 2001 From: liangliangyy Date: Mon, 8 Jul 2019 12:40:06 +0800 Subject: [PATCH 096/593] remmove allow_failures --- .travis.yml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 38b142887..e0a3db0d0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,11 +4,8 @@ sudo: required python: - "3.5" - "3.6" -matrix: - include: - - python: "3.7" - allow_failures: - - python: "3.7" + - "3.7" + services: - mysql env: From c91dd5c148b7f0306a0df482359f2bfc7f7a7515 Mon Sep 17 00:00:00 2001 From: liangliangyy Date: Sat, 13 Jul 2019 20:07:37 +0800 Subject: [PATCH 097/593] =?UTF-8?q?=E4=BF=AE=E6=94=B9admins?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DjangoBlog/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DjangoBlog/settings.py b/DjangoBlog/settings.py index 50a9015ad..6d2decde9 100644 --- a/DjangoBlog/settings.py +++ b/DjangoBlog/settings.py @@ -204,7 +204,7 @@ DEFAULT_FROM_EMAIL = EMAIL_HOST_USER SERVER_EMAIL = os.environ.get('DJANGO_EMAIL_USER') # Setting debug=false did NOT handle except email notifications -ADMINS = [('liangliang', 'liangliangyy@gmail.com')] +ADMINS = [('admin', 'admin@admin.com')] # WX ADMIN password(Two times md5) WXADMIN = '995F03AC401D6CABABAEF756FC4D43C7' From af477524d665aa13631f5d99d997f807d8932867 Mon Sep 17 00:00:00 2001 From: liangliangyy Date: Wed, 17 Jul 2019 21:15:47 +0800 Subject: [PATCH 098/593] =?UTF-8?q?=E6=97=B6=E9=97=B4=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E9=BB=98=E8=AE=A4=E5=80=BC=20close=20#292?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- blog/models.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/blog/models.py b/blog/models.py index 920f0b0d0..4ad7b8e6a 100644 --- a/blog/models.py +++ b/blog/models.py @@ -69,12 +69,13 @@ class Article(BaseModel): ) title = models.CharField('标题', max_length=200, unique=True) body = MDTextField('正文') - pub_time = models.DateTimeField('发布时间', blank=True, null=True) + pub_time = models.DateTimeField('发布时间', blank=False, null=False, default=now) status = models.CharField('文章状态', max_length=1, choices=STATUS_CHOICES, default='p') comment_status = models.CharField('评论状态', max_length=1, choices=COMMENT_STATUS, default='o') type = models.CharField('类型', max_length=1, choices=TYPE, default='a') views = models.PositiveIntegerField('浏览量', default=0) - author = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name='作者', on_delete=models.CASCADE) + author = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name='作者', blank=False, null=False, + on_delete=models.CASCADE) article_order = models.IntegerField('排序,数字越大越靠前', blank=False, null=False, default=0) category = models.ForeignKey('Category', verbose_name='分类', on_delete=models.CASCADE, blank=False, null=False) tags = models.ManyToManyField('Tag', verbose_name='标签集合', blank=True) From cd64137281eb51efe9d12e205055eb70d043b637 Mon Sep 17 00:00:00 2001 From: liangliangyy Date: Thu, 18 Jul 2019 15:36:54 +0800 Subject: [PATCH 099/593] close #298 --- templates/blog/error_page.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/blog/error_page.html b/templates/blog/error_page.html index 2a04ca477..d41cfb60a 100644 --- a/templates/blog/error_page.html +++ b/templates/blog/error_page.html @@ -5,9 +5,9 @@ {% if tag_name %} {% if statuscode == '404' %} 404 NotFound - {% elif statuscode == '500' %} - Permission Denied {% elif statuscode == '403' %} + Permission Denied + {% elif statuscode == '500' %} 500 Error {% else %} From b43b1bf095a07ee5bc49b334f21e918332e9331a Mon Sep 17 00:00:00 2001 From: liangliangyy Date: Mon, 22 Jul 2019 17:43:55 +0800 Subject: [PATCH 100/593] update --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index e0a3db0d0..b7036e5a5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,7 +17,7 @@ branches: # command to install dependencies before_install: - sudo apt-get update - - sudo apt-get install python-dev python3-dev libmysqlclient-dev + - sudo apt-get install python-dev python3-dev libmysqlclient-dev -y - sudo cp $TRAVIS_BUILD_DIR/travis_test/mysql.cnf /etc/mysql/conf.d/ - sudo service mysql restart install: @@ -35,4 +35,4 @@ script: - coverage run manage.py test after_success: - coveralls - - codecov + - codecov \ No newline at end of file From 9009a44be79233024f141f0a7e32de5efa68f34d Mon Sep 17 00:00:00 2001 From: liangliangyy Date: Sun, 4 Aug 2019 15:51:02 +0800 Subject: [PATCH 101/593] =?UTF-8?q?=E9=83=A8=E5=88=86=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DjangoBlog/feeds.py | 7 +++-- README.md | 2 +- accounts/admin.py | 2 +- accounts/models.py | 4 +-- bin/blog.lylinux.org | 37 +++------------------------ bin/{django_start => django_start.sh} | 2 +- comments/models.py | 2 +- README-en.md => docs/README-en.md | 0 8 files changed, 12 insertions(+), 44 deletions(-) rename bin/{django_start => django_start.sh} (94%) rename README-en.md => docs/README-en.md (100%) diff --git a/DjangoBlog/feeds.py b/DjangoBlog/feeds.py index 0d198bc58..85d539bc4 100644 --- a/DjangoBlog/feeds.py +++ b/DjangoBlog/feeds.py @@ -19,8 +19,7 @@ from django.utils.feedgenerator import Rss201rev2Feed from DjangoBlog.utils import CommonMarkdown from django.contrib.auth import get_user_model -from django.contrib.auth.models import User -from DjangoBlog.utils import get_current_site +from datetime import datetime class DjangoBlogFeed(Feed): @@ -46,8 +45,8 @@ def item_description(self, item): return CommonMarkdown.get_markdown(item.body) def feed_copyright(self): - # print(get_current_site().name) - return "Copyright© 2018 且听风吟" + now = datetime.now() + return "Copyright© {year} 且听风吟".format(year=now.year) def item_link(self, item): return item.get_absolute_url() diff --git a/README.md b/README.md index a29611e6b..5acdfbf98 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # DjangoBlog 🌍 -*[English](README-en.md) ∙ [简体中文](README.md)* +*[English](/docs/README-en.md) ∙ [简体中文](README.md)* 基于`python3.6`和`Django2.1`的博客。 diff --git a/accounts/admin.py b/accounts/admin.py index 7333b8d49..67226e47e 100644 --- a/accounts/admin.py +++ b/accounts/admin.py @@ -58,6 +58,6 @@ def __init__(self, *args, **kwargs): class BlogUserAdmin(UserAdmin): form = BlogUserChangeForm add_form = BlogUserCreationForm - list_display = ('id', 'nickname', 'username', 'email', 'last_login', 'date_joined') + list_display = ('id', 'nickname', 'username', 'email', 'last_login', 'date_joined', 'source') list_display_links = ('id', 'username') ordering = ('-id',) diff --git a/accounts/models.py b/accounts/models.py index b7d4025e4..e292dc825 100644 --- a/accounts/models.py +++ b/accounts/models.py @@ -11,9 +11,7 @@ class BlogUser(AbstractUser): nickname = models.CharField('昵称', max_length=100, blank=True) created_time = models.DateTimeField('创建时间', default=now) last_mod_time = models.DateTimeField('修改时间', default=now) - source = models.CharField("创建涞源", max_length=100, blank=True) - - # objects = BlogUserManager() + source = models.CharField("创建来源", max_length=100, blank=True) def get_absolute_url(self): return reverse('blog:author_detail', kwargs={'author_name': self.username}) diff --git a/bin/blog.lylinux.org b/bin/blog.lylinux.org index 3eb2f9d8c..eb32a974f 100644 --- a/bin/blog.lylinux.org +++ b/bin/blog.lylinux.org @@ -1,17 +1,8 @@ -upstream hello_app_server { - server unix:/var/www/DjangoBlog/run/gunicorn.sock fail_timeout=0; -} - server { - - server_name blog.lylinux.org; root /var/www/DjangoBlog/; - listen 80; - keepalive_timeout 70; - access_log off; location /static/ { expires max; @@ -19,34 +10,14 @@ server { } location / { -# an HTTP header important enough to have its own Wikipedia entry: -# http://en.wikipedia.org/wiki/X-Forwarded-For + proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - -# enable this if and only if you use HTTPS, this helps Rack -# set the proper protocol for doing redirects: -# proxy_set_header X-Forwarded-Proto https; - -# pass the Host: header from the client right along so redirects -# can be set properly within the Rack application proxy_set_header Host $http_host; - -# we don't want nginx trying to do something clever with -# redirects, we set the Host: header above already. + proxy_set_header X-NginX-Proxy true; proxy_redirect off; - -# set "proxy_buffering off" *only* for Rainbows! when doing -# Comet/long-poll stuff. It's also safe to set if you're -# using only serving fast clients with Unicorn + nginx. -# Otherwise you _want_ nginx to buffer responses to slow -# clients, really. -# proxy_buffering off; - -# Try to serve static files from nginx, no point in making an -# *application* server like Unicorn/Rainbows! serve static files. if (!-f $request_filename) { - proxy_pass http://hello_app_server; - break; + proxy_pass http://127.0.0.1:8000; + break; } } diff --git a/bin/django_start b/bin/django_start.sh similarity index 94% rename from bin/django_start rename to bin/django_start.sh index 1bfb2bef1..a27eb2eae 100755 --- a/bin/django_start +++ b/bin/django_start.sh @@ -1,7 +1,7 @@ #!/bin/bash NAME="djangoblog" # Name of the application -DJANGODIR=//var/www/DjangoBlog # Django project directory +DJANGODIR=/var/www/DjangoBlog # Django project directory SOCKFILE=/var/www/DjangoBlog/run/gunicorn.sock # we will communicte using this unix socket USER=root # the user to run as GROUP=root # the group to run as diff --git a/comments/models.py b/comments/models.py index 8ad88d8ff..356bb922c 100644 --- a/comments/models.py +++ b/comments/models.py @@ -16,7 +16,7 @@ class Comment(models.Model): is_enable = models.BooleanField('是否显示', default=True, blank=False, null=False) class Meta: - ordering = ['-created_time'] + ordering = ['-id'] verbose_name = "评论" verbose_name_plural = verbose_name get_latest_by = 'created_time' diff --git a/README-en.md b/docs/README-en.md similarity index 100% rename from README-en.md rename to docs/README-en.md From 12e30caf0204421463a2eb19eade536eac483d4b Mon Sep 17 00:00:00 2001 From: liangliangyy Date: Mon, 5 Aug 2019 17:28:48 +0800 Subject: [PATCH 102/593] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=20DjangoBlog?= =?UTF-8?q?=E9=83=A8=E7=BD=B2=E6=95=99=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- docs/README-en.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5acdfbf98..b52920fd1 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ mysql客户端从`pymysql`修改成了`mysqlclient`,具体请参考 [pypi](htt `bin`目录是在`linux`环境中使用`Nginx`+`Gunicorn`+`virtualenv`+`supervisor`来部署的脚本和`Nginx`配置文件.可以参考我的文章: ->[使用Nginx+Gunicorn+virtualenv+supervisor来部署django项目](https://www.lylinux.org/%E4%BD%BF%E7%94%A8nginxgunicornvirtualenvsupervisor%E6%9D%A5%E9%83%A8%E7%BD%B2django%E9%A1%B9%E7%9B%AE.html) +>[DjangoBlog部署教程](https://www.lylinux.net/article/2019/8/5/58.html) 有详细的部署介绍. diff --git a/docs/README-en.md b/docs/README-en.md index 5ad8d41f9..46000ad4a 100644 --- a/docs/README-en.md +++ b/docs/README-en.md @@ -46,7 +46,7 @@ Files in `test` directory are for `travis` with automatic testing. You do not ne In `bin` directory, we have scripts to deploy with `Nginx`+`Gunicorn`+`virtualenv`+`supervisor` on `linux` and `Nginx` configuration file. You can reference with my article ->[Using Nginx+Gunicorn+virtualenv+supervisor to deploy django project.](https://www.lylinux.org/%E4%BD%BF%E7%94%A8nginxgunicornvirtualenvsupervisor%E6%9D%A5%E9%83%A8%E7%BD%B2django%E9%A1%B9%E7%9B%AE.html) +>[DjangoBlog部署教程](https://www.lylinux.net/article/2019/8/5/58.html) More deploy detail in this article. From 7b7fa15885a7d26fc2e4cc2811961bd78f7234f2 Mon Sep 17 00:00:00 2001 From: liangliangyy Date: Wed, 7 Aug 2019 17:05:56 +0800 Subject: [PATCH 103/593] =?UTF-8?q?=E5=8D=87=E7=BA=A7=E4=BE=9D=E8=B5=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- requirements.txt | 19 ++++++++++--------- travis_test/requirements.txt | 20 +++++++++++--------- travis_test/travis_settings.py | 3 --- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/requirements.txt b/requirements.txt index e3c789226..06e89deea 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,20 +1,21 @@ appdirs==1.4.3 asn1crypto==0.24.0 astroid==2.2.5 +attrs==19.1.0 bottle==0.12.17 certifi==2019.6.16 cffi==1.12.3 chardet==3.0.4 -coverage==4.5.3 +coverage==4.5.4 cryptography==2.7 -Django==2.2.3 +Django==2.2.4 django-appconf==1.0.3 -django-autoslug==1.9.4 +django-autoslug==1.9.6 django-compressor==2.3 django-debug-toolbar==2.0 django-haystack==2.8.1 django-ipware==2.1.0 -django-mdeditor==0.1.14 +django-mdeditor==0.1.16 django-uuslug==1.1.8 elasticsearch==7.0.0 elasticsearch-dsl==7.0.0 @@ -29,17 +30,17 @@ mccabe==0.6.1 mistune==0.8.4 mysqlclient==1.4.2.post1 olefile==0.46 -packaging==19.0 -Pillow==6.0.0 +packaging==19.1 +Pillow==6.1.0 pycparser==2.19 Pygments==2.4.2 pylint==2.3.1 -pyparsing==2.4.0 +pyparsing==2.4.2 python-dateutil==2.8.0 python-logstash==0.4.6 python-memcached==1.59 -python-slugify==3.0.2 -pytz==2019.1 +python-slugify==3.0.3 +pytz==2019.2 raven==6.10.0 rcssmin==1.0.6 requests==2.22.0 diff --git a/travis_test/requirements.txt b/travis_test/requirements.txt index f2649dba6..06e89deea 100644 --- a/travis_test/requirements.txt +++ b/travis_test/requirements.txt @@ -1,20 +1,21 @@ appdirs==1.4.3 asn1crypto==0.24.0 astroid==2.2.5 +attrs==19.1.0 bottle==0.12.17 certifi==2019.6.16 cffi==1.12.3 chardet==3.0.4 -coverage==4.5.3 +coverage==4.5.4 cryptography==2.7 -Django==2.2.3 +Django==2.2.4 django-appconf==1.0.3 -django-autoslug==1.9.4 +django-autoslug==1.9.6 django-compressor==2.3 django-debug-toolbar==2.0 django-haystack==2.8.1 django-ipware==2.1.0 -django-mdeditor==0.1.14 +django-mdeditor==0.1.16 django-uuslug==1.1.8 elasticsearch==7.0.0 elasticsearch-dsl==7.0.0 @@ -29,16 +30,17 @@ mccabe==0.6.1 mistune==0.8.4 mysqlclient==1.4.2.post1 olefile==0.46 -packaging==19.0 -Pillow==6.0.0 +packaging==19.1 +Pillow==6.1.0 pycparser==2.19 Pygments==2.4.2 pylint==2.3.1 -pyparsing==2.4.0 +pyparsing==2.4.2 python-dateutil==2.8.0 python-logstash==0.4.6 -python-slugify==3.0.2 -pytz==2019.1 +python-memcached==1.59 +python-slugify==3.0.3 +pytz==2019.2 raven==6.10.0 rcssmin==1.0.6 requests==2.22.0 diff --git a/travis_test/travis_settings.py b/travis_test/travis_settings.py index 50e009bd6..b008fa4c4 100644 --- a/travis_test/travis_settings.py +++ b/travis_test/travis_settings.py @@ -7,9 +7,6 @@ 'USER': 'travis', 'PASSWORD': '', 'HOST': '127.0.0.1', - # 'HOST': '192.168.1.120', - # 'USER': 'root', - # 'PASSWORD': 'root', 'PORT': 3306, } } From 2032bf1689a800ff6ed83084e5b9e7bd08bd6266 Mon Sep 17 00:00:00 2001 From: liangliangyy Date: Wed, 7 Aug 2019 17:20:13 +0800 Subject: [PATCH 104/593] =?UTF-8?q?=E4=BF=AE=E6=94=B9travis?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .travis.yml | 2 +- travis_test/requirements.txt | 58 ------------------------------------ 2 files changed, 1 insertion(+), 59 deletions(-) delete mode 100644 travis_test/requirements.txt diff --git a/.travis.yml b/.travis.yml index b7036e5a5..9d1b8fee8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,7 +21,7 @@ before_install: - sudo cp $TRAVIS_BUILD_DIR/travis_test/mysql.cnf /etc/mysql/conf.d/ - sudo service mysql restart install: - - pip install -r travis_test/requirements.txt + - pip install -r requirements.txt - pip install python-coveralls - pip install coverage codecov before_script: diff --git a/travis_test/requirements.txt b/travis_test/requirements.txt deleted file mode 100644 index 06e89deea..000000000 --- a/travis_test/requirements.txt +++ /dev/null @@ -1,58 +0,0 @@ -appdirs==1.4.3 -asn1crypto==0.24.0 -astroid==2.2.5 -attrs==19.1.0 -bottle==0.12.17 -certifi==2019.6.16 -cffi==1.12.3 -chardet==3.0.4 -coverage==4.5.4 -cryptography==2.7 -Django==2.2.4 -django-appconf==1.0.3 -django-autoslug==1.9.6 -django-compressor==2.3 -django-debug-toolbar==2.0 -django-haystack==2.8.1 -django-ipware==2.1.0 -django-mdeditor==0.1.16 -django-uuslug==1.1.8 -elasticsearch==7.0.0 -elasticsearch-dsl==7.0.0 -idna==2.8 -ipaddress==1.0.22 -isort==4.3.21 -jieba==0.39 -jsonpickle==1.2 -lazy-object-proxy==1.4.1 -markdown2==2.3.8 -mccabe==0.6.1 -mistune==0.8.4 -mysqlclient==1.4.2.post1 -olefile==0.46 -packaging==19.1 -Pillow==6.1.0 -pycparser==2.19 -Pygments==2.4.2 -pylint==2.3.1 -pyparsing==2.4.2 -python-dateutil==2.8.0 -python-logstash==0.4.6 -python-memcached==1.59 -python-slugify==3.0.3 -pytz==2019.2 -raven==6.10.0 -rcssmin==1.0.6 -requests==2.22.0 -rjsmin==1.1.0 -six==1.12.0 -sqlparse==0.3.0 -text-unidecode==1.2 -typed-ast==1.3.1 -Unidecode==1.1.1 -urllib3==1.25.3 -webencodings==0.5.1 -WeRoBot==1.9.0 -Whoosh==2.7.4 -wrapt==1.11.2 -xmltodict==0.12.0 From 7bb207afebfa0aa223761ff92bf7e98fcdf3f4c2 Mon Sep 17 00:00:00 2001 From: liangliangyy Date: Tue, 20 Aug 2019 17:08:25 +0800 Subject: [PATCH 105/593] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=B8=BAgunicorn[gev?= =?UTF-8?q?ent]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bin/docker_start.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/bin/docker_start.sh b/bin/docker_start.sh index bdc918b46..813967610 100644 --- a/bin/docker_start.sh +++ b/bin/docker_start.sh @@ -28,5 +28,6 @@ exec gunicorn ${DJANGO_WSGI_MODULE}:application \ --user=$USER --group=$GROUP \ --bind 0.0.0.0:8000 \ --log-level=debug \ ---log-file=- - +--log-file=- \ +--worker-class gevent \ +--threads 4 From e06e40ad85f9d6fe8e6a66216018856070f4e185 Mon Sep 17 00:00:00 2001 From: liangliangyy Date: Tue, 27 Aug 2019 19:15:20 +0800 Subject: [PATCH 106/593] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=8F=8B=E6=83=85?= =?UTF-8?q?=E9=93=BE=E6=8E=A5=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- blog/context_processors.py | 8 +++--- blog/models.py | 3 +- blog/tests.py | 7 ++++- blog/urls.py | 2 ++ blog/views.py | 14 ++++++++-- templates/blog/links_list.html | 44 ++++++++++++++++++++++++++++++ templates/share_layout/footer.html | 17 ++++++++---- 7 files changed, 79 insertions(+), 16 deletions(-) create mode 100644 templates/blog/links_list.html diff --git a/blog/context_processors.py b/blog/context_processors.py index 845cf2035..cea83a182 100644 --- a/blog/context_processors.py +++ b/blog/context_processors.py @@ -13,9 +13,9 @@ @time: 2016/11/6 下午4:23 """ from .models import Category, Article, Tag, BlogSettings -from django.conf import settings -from comments.models import Comment from DjangoBlog.utils import cache, get_blog_setting + +from datetime import datetime import logging logger = logging.getLogger(__name__) @@ -36,7 +36,6 @@ def seo_processor(requests): 'SITE_SEO_DESCRIPTION': setting.site_seo_description, 'SITE_DESCRIPTION': setting.site_description, 'SITE_KEYWORDS': setting.site_keywords, - 'SITE_BASE_URL': requests.scheme + '://' + requests.get_host() + '/', 'ARTICLE_SUB_LENGTH': setting.article_sub_length, 'nav_category_list': Category.objects.all(), 'nav_pages': Article.objects.filter(type='p', status='p'), @@ -44,7 +43,8 @@ def seo_processor(requests): 'BEIAN_CODE': setting.beiancode, 'ANALYTICS_CODE': setting.analyticscode, "BEIAN_CODE_GONGAN": setting.gongan_beiancode, - "SHOW_GONGAN_CODE": setting.show_gongan_code + "SHOW_GONGAN_CODE": setting.show_gongan_code, + "CURRENT_YEAR": datetime.now().year } cache.set(key, value, 60 * 60 * 10) diff --git a/blog/models.py b/blog/models.py index 4ad7b8e6a..720e9111b 100644 --- a/blog/models.py +++ b/blog/models.py @@ -9,10 +9,8 @@ from django.utils.translation import gettext_lazy as _ from DjangoBlog.utils import get_current_site from DjangoBlog.utils import cache_decorator, cache -from django.utils.functional import cached_property from django.utils.timezone import now from mdeditor.fields import MDTextField -from django.db.models.signals import post_save logger = logging.getLogger(__name__) @@ -21,6 +19,7 @@ ('l', '列表页'), ('p', '文章页面'), ('a', '全站'), + ('s', '友情链接页面'), ) diff --git a/blog/tests.py b/blog/tests.py index ac48b315f..876fbe771 100644 --- a/blog/tests.py +++ b/blog/tests.py @@ -1,5 +1,5 @@ from django.test import Client, RequestFactory, TestCase -from blog.models import Article, Category, Tag, SideBar +from blog.models import Article, Category, Tag, SideBar, Links from django.contrib.auth import get_user_model from DjangoBlog.utils import get_current_site, get_md5 from blog.forms import BlogSearchForm @@ -120,6 +120,11 @@ def test_validate_article(self): u = gravatar_url('liangliangyy@gmail.com') u = gravatar('liangliangyy@gmail.com') + link = Links(sequence=1, name="lylinux", link='https://wwww.lylinux.net') + link.save() + response = self.client.get('/links.html') + self.assertEqual(response.status_code, 200) + def __check_pagination__(self, p, type, value): s = load_pagination_info(p.page(1), type, value) self.assertIsNotNone(s) diff --git a/blog/urls.py b/blog/urls.py index 5cbefa3da..c84f69646 100644 --- a/blog/urls.py +++ b/blog/urls.py @@ -40,6 +40,8 @@ path(r'tag/.html', views.TagDetailView.as_view(), name='tag_detail'), path(r'tag//.html', views.TagDetailView.as_view(), name='tag_detail_page'), path('archives.html', views.ArchivesView.as_view(), name='archives'), + path('links.html', views.LinkListView.as_view(), name='links'), path(r'upload', views.fileupload, name='upload'), path(r'refresh', views.refresh_memcache, name='refresh') + ] diff --git a/blog/views.py b/blog/views.py index 119f9cfdb..adb225af4 100644 --- a/blog/views.py +++ b/blog/views.py @@ -12,7 +12,7 @@ from django.contrib.auth.decorators import login_required from DjangoBlog.utils import cache, get_md5, get_blog_setting from django.shortcuts import get_object_or_404 -from blog.models import Article, Category, Tag +from blog.models import Article, Category, Tag, Links from comments.forms import CommentForm import logging @@ -245,13 +245,21 @@ def get_queryset_cache_key(self): return cache_key +class LinkListView(ListView): + model = Links + template_name = 'blog/links_list.html' + + def get_queryset(self): + return Links.objects.filter(is_enable=True) + + @csrf_exempt def fileupload(request): - ''' + """ 该方法需自己写调用端来上传图片,该方法仅提供图床功能 :param request: :return: - ''' + """ if request.method == 'POST': sign = request.GET.get('sign', None) if not sign: diff --git a/templates/blog/links_list.html b/templates/blog/links_list.html new file mode 100644 index 000000000..ccecbea7c --- /dev/null +++ b/templates/blog/links_list.html @@ -0,0 +1,44 @@ +{% extends 'share_layout/base.html' %} +{% load blog_tags %} +{% load cache %} +{% block header %} + + 友情链接 | {{ SITE_DESCRIPTION }} + + + + + + + + + +{% endblock %} +{% block content %} +
    +
    + +
    + +

    友情链接

    +
    + +
    + +
    +
    +
    + +{% endblock %} + + +{% block sidebar %} + {% load_sidebar user 'i' %} +{% endblock %} + + diff --git a/templates/share_layout/footer.html b/templates/share_layout/footer.html index df2389b5e..5fa274d3e 100644 --- a/templates/share_layout/footer.html +++ b/templates/share_layout/footer.html @@ -1,18 +1,23 @@