Compare commits

..

No commits in common. 'ly_branch' and 'main' have entirely different histories.

162
.gitignore vendored

@ -0,0 +1,162 @@
# ---> Python
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/

@ -0,0 +1,9 @@
MIT License
Copyright (c) <year> <copyright holders>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

@ -0,0 +1,2 @@
# DjangoBlog-Maintenance-Analysis

@ -1,63 +0,0 @@
{% extends 'share_layout/base.html' %} {# 继承基础模板,复用基础模板的结构和样式 #}
{% load blog_tags %} {# 加载自定义的blog_tags模板标签库以便使用其中的模板标签 #}
{% load cache %} {# 加载缓存模板标签库,用于缓存部分页面内容 #}
{% load i18n %} {# 加载国际化模板标签库,支持多语言翻译 #}
{% block header %} {# 定义页面头部的block用于填充页面头部相关内容 #}
<title>{% trans 'article archive' %} | {{ SITE_DESCRIPTION }}</title>
{# 设置页面标题,使用国际化翻译显示“文章归档”,并拼接站点描述 #}
<meta name="description" content="{{ SITE_SEO_DESCRIPTION }}"/>
<meta name="keywords" content="{{ SITE_KEYWORDS }}"/> {# 设置页面描述元数据 #}
<meta property="og:type" content="blog"/>
<meta property="og:title" content="{{ SITE_NAME }}"/>
<meta property="og:description" content="{{ SITE_DESCRIPTION }}"/>
<meta property="og:url" content="{{ SITE_BASE_URL }}"/>
<meta property="og:site_name" content="{{ SITE_NAME }}"/>
{% endblock %}
{% block content %} {# 定义页面主要内容的block #}
<div id="primary" class="site-content"> {# 主要内容的外层容器设置id和class用于样式控制 #}
<div id="content" role="main"> {# 主要内容区域role属性表明其主要内容的角色 #}
<header class="archive-header"> {# 文章归档部分的头部容器 #}
<p class="archive-title">{% trans 'article archive' %}</p> {# 显示“文章归档”的标题 #}
</header><!-- .archive-header -->
<div class="entry-content"> {# 文章归档的内容区域 #}
{% regroup article_list by pub_time.year as year_post_group %} {# 将文章列表按发布时间的年份分组结果保存到year_post_group #}
<ul>
{% for year in year_post_group %}
{# 遍历按年份分组的结果 #}
<li>{{ year.grouper }} {% trans 'year' %} {# 显示年份 #}
{% regroup year.list by pub_time.month as month_post_group %}
<ul>
{% for month in month_post_group %} {# 遍历按月份分组的结果 #}
<li>{{ month.grouper }} {% trans 'month' %}
{# 显示月份,以及“月”的国际化翻译 #}
<ul>
{% for article in month.list %}
<li><a href="{{ article.get_absolute_url }}">{{ article.title }}</a>
</li> {# 显示文章标题,并设置链接到文章详情页 #}
{% endfor %}
</ul>
</li>
{% endfor %}
</ul>
</li>
{% endfor %}
</ul>
</div>
</div><!-- #content -->
</div><!-- #primary -->
{% endblock %}
{% block sidebar %}
{% load_sidebar user 'i' %}
{% endblock %}

@ -1,66 +0,0 @@
{# 继承基础模板,复用基础模板(如导航栏、页脚、通用样式等)的结构和公共内容 #}
{% extends 'share_layout/base.html' %}
{# 加载自定义的blog_tags模板标签库用于使用库中定义的自定义模板标签 #}
{% load blog_tags %}
{% block header %}
{% endblock %}
{# 定义页面主要内容blockcontent承载当前页面的核心展示内容 #}
{% block content %}
{# 主要内容外层容器通过id和class控制样式确保与站点整体布局风格统一 #}
<div id="primary" class="site-content">
{# 核心内容区域role="main"是ARIA属性用于辅助设备识别这是页面主要内容区 #}
<div id="content" role="main">
{# 调用blog_tags库中的load_article_detail自定义标签渲染文章详情内容 #}
{% load_article_detail article False user %}
{# 条件判断:如果当前文章的类型为'a',则显示文章导航栏 #}
{% if article.type == 'a' %}
{# 文章导航容器,用于展示上一篇/下一篇文章的跳转链接 #}
<nav class="nav-single">
<h3 class="assistive-text">文章导航</h3>
{% if next_article %}
<span class="nav-previous"><a href="{{ next_article.get_absolute_url }}" rel="prev"><span
class="meta-nav">&larr;</span> {{ next_article.title }}</a></span>
{% endif %}
{% if prev_article %}
<span class="nav-next"><a href="{{ prev_article.get_absolute_url }}"
rel="next">{{ prev_article.title }} <span
class="meta-nav">&rarr;</span></a></span>
{% endif %}
</nav><!-- .nav-single -->
{% endif %}
</div><!-- #content -->
{# 文章导航容器,用于展示上一篇/下一篇文章的跳转链接 #}
{% if article.comment_status == "o" and OPEN_SITE_COMMENT %}
{# 引入评论列表模板comments/tags/comment_list.html渲染当前文章已有的评论内容 #}
{% include 'comments/tags/comment_list.html' %}
{# 条件判断如果用户已登录user.is_authenticated则显示“发表评论”的表单模板 #}
{% if user.is_authenticated %}
{% include 'comments/tags/post_comment.html' %}
{# 若用户未登录,提示登录后才能评论,并提供登录链接和第三方登录入口 #}
{% else %}
<div class="comments-area">
<h3 class="comment-meta">您还没有登录,请您<a
href="{% url "account:login" %}?next={{ request.get_full_path }}" rel="nofollow">登录</a>后发表评论。
</h3>
{# 登录链接next参数携带当前页面的完整URL确保用户登录后能跳转回当前文章页 #}
{% load oauth_tags %}
{% load_oauth_applications request %}
</div>
{% endif %}
{% endif %}
</div><!-- #primary -->
{% endblock %}
{% block sidebar %}
{% load_sidebar user "p" %}
{% endblock %}

@ -1,51 +0,0 @@
{# 继承基础模板,复用基础模板中的公共结构(如导航栏、页脚)和样式 #}
{% extends 'share_layout/base.html' %}
{% load blog_tags %}
{% load cache %}
{% block header %}
{% if tag_name %}
<title>{{ page_type }}:{{ tag_name }} | {{ SITE_DESCRIPTION }}</title>
{% comment %}<meta name="description" content="{{ page_type }}:{{ tag_name }}"/>{% endcomment %}
{% else %}
<title>{{ SITE_NAME }} | {{ SITE_DESCRIPTION }}</title>
{% endif %}
{# 页面元数据设置站点SEO描述用于搜索引擎抓取时展示的页面简介 #}
<meta name="description" content="{{ SITE_SEO_DESCRIPTION }}"/>
<meta name="keywords" content="{{ SITE_KEYWORDS }}"/>
<meta property="og:type" content="blog"/>
<meta property="og:title" content="{{ SITE_NAME }}"/>
<meta property="og:description" content="{{ SITE_DESCRIPTION }}"/>
<meta property="og:url" content="{{ SITE_BASE_URL }}"/>
<meta property="og:site_name" content="{{ SITE_NAME }}"/>
{% endblock %}
{# 定义页面主要内容content的内容块承载文章列表和分页等核心内容 #}
{% block content %}
<div id="primary" class="site-content">
<div id="content" role="main">
{% if page_type and tag_name %}
<header class="archive-header">
<p class="archive-title">{{ page_type }}<span>{{ tag_name }}</span></p>
</header><!-- .archive-header -->
{% endif %}
{# 循环遍历文章列表article_list逐个渲染文章内容 #}
{% for article in article_list %}
{% load_article_detail article True user %}
{% endfor %}
{# 条件判断如果文章列表支持分页is_paginated为True则渲染分页控件 #}
{% if is_paginated %}
{% load_pagination_info page_obj page_type tag_name %}
{% endif %}
</div><!-- #content -->
</div><!-- #primary -->
{% endblock %}
{# 定义页面侧边栏sidebar的内容块承载侧边栏组件 #}
{% block sidebar %}
{# 调用自定义标签load_sidebar渲染侧边栏内容 #}
{% load_sidebar user linktype %}
{% endblock %}

@ -1,212 +0,0 @@
#ly:
# coding:utf-8
# ==========================================
# Blog模块 - Django Admin配置
# ==========================================
from django import forms
from django.contrib import admin
from django.contrib.auth import get_user_model
from django.urls import reverse
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
from .models import Article
# ==========================================
# 文章表单
# ==========================================
class ArticleForm(forms.ModelForm):
"""
文章编辑表单
用于Django Admin中编辑文章
"""
# body = forms.CharField(widget=AdminPagedownWidget())
class Meta:
model = Article
fields = '__all__'
# ==========================================
# 文章管理操作函数
# ==========================================
def makr_article_publish(modeladmin, request, queryset):
"""
批量发布选中的文章
将选中文章的状态改为'p'已发布
问题函数名拼写错误应为 mark_article_publish
"""
queryset.update(status='p')
def draft_article(modeladmin, request, queryset):
"""
批量把选中的文章设为草稿
将选中文章的状态改为'd'草稿
"""
queryset.update(status='d')
def close_article_commentstatus(modeladmin, request, queryset):
"""
批量关闭选中文章的评论
将选中文章的评论状态改为'c'关闭
"""
queryset.update(comment_status='c')
def open_article_commentstatus(modeladmin, request, queryset):
"""
批量开启选中文章的评论
将选中文章的评论状态改为'o'开放
"""
queryset.update(comment_status='o')
# 设置操作函数在Admin中的显示描述
makr_article_publish.short_description = _('Publish selected articles')
draft_article.short_description = _('Draft selected articles')
close_article_commentstatus.short_description = _('Close article comments')
open_article_commentstatus.short_description = _('Open article comments')
# ==========================================
# 文章Admin配置
# ==========================================
class ArticlelAdmin(admin.ModelAdmin):
"""
文章管理配置
在Django Admin中管理文章
"""
# 每页显示的记录数
list_per_page = 20
# 搜索字段 - 可以搜索文章标题和内容
search_fields = ('body', 'title')
# 使用自定义表单
form = ArticleForm
# 列表显示的字段
list_display = (
'id',
'title',
'author',
'link_to_category',
'creation_time',
'views',
'status',
'type',
'article_order')
# 可点击的列(点击可打开详情)
list_display_links = ('id', 'title')
# 过滤器字段
list_filter = ('status', 'type', 'category')
# 多选字段的水平显示
filter_horizontal = ('tags',)
# 不显示的字段(由系统自动管理)
exclude = ('creation_time', 'last_modify_time')
# 是否显示"查看网站"链接
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'<a href="%s">%s</a>' % (link, obj.category.name))
link_to_category.short_description = _('category')
def get_form(self, request, obj=None, **kwargs):
"""
获取表单
只允许超级用户作为文章作者
问题如果没有超级用户queryset会为空UI会显示为空列表
应该有备用方案或提示信息
"""
form = super(ArticlelAdmin, self).get_form(request, obj, **kwargs)
# 将author字段的选项限制为超级用户
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)
def get_view_on_site_url(self, obj=None):
"""
获取"查看网站"链接
返回文章在网站上的完整URL
"""
if obj:
# 返回文章的完整URL
url = obj.get_full_url()
return url
else:
# 返回网站首页URL
from djangoblog.utils import get_current_site
site = get_current_site().domain
return site
# ==========================================
# 标签Admin配置
# ==========================================
class TagAdmin(admin.ModelAdmin):
"""标签管理"""
# 不显示的字段(由系统自动管理)
exclude = ('slug', 'last_mod_time', 'creation_time')
# ==========================================
# 分类Admin配置
# ==========================================
class CategoryAdmin(admin.ModelAdmin):
"""分类管理"""
# 列表显示的字段
list_display = ('name', 'parent_category', 'index')
# 不显示的字段(由系统自动管理)
exclude = ('slug', 'last_mod_time', 'creation_time')
# ==========================================
# 友情链接Admin配置
# ==========================================
class LinksAdmin(admin.ModelAdmin):
"""友情链接管理"""
# 不显示的字段(由系统自动管理)
exclude = ('last_mod_time', 'creation_time')
# ==========================================
# 侧边栏Admin配置
# ==========================================
class SideBarAdmin(admin.ModelAdmin):
"""侧边栏管理"""
# 列表显示的字段
list_display = ('name', 'content', 'is_enable', 'sequence')
# 不显示的字段(由系统自动管理)
exclude = ('last_mod_time', 'creation_time')
# ==========================================
# 博客配置Admin
# ==========================================
class BlogSettingsAdmin(admin.ModelAdmin):
"""
博客配置管理
问题此类为空实现应该删除或添加实际配置
"""
pass

@ -1,16 +0,0 @@
#ly:
# coding:utf-8
# ==========================================
# Blog模块 - 应用配置
# ==========================================
from django.apps import AppConfig
class BlogConfig(AppConfig):
"""
Blog模块应用配置
用于Django应用初始化和配置
"""
# 应用名称
name = 'blog'

@ -1,91 +0,0 @@
#ly:
# coding:utf-8
# ==========================================
# Blog模块 - 模板上下文处理器
# ==========================================
# 为所有模板提供全局上下文数据
import logging
from django.utils import timezone
from djangoblog.utils import cache, get_blog_setting
from .models import Category, Article
logger = logging.getLogger(__name__)
# ==========================================
# SEO信息上下文处理器
# ==========================================
def seo_processor(requests):
"""
为所有模板提供SEO和网站配置相关的上下文数据
此处理器会被自动调用以便在所有模板中使用相关变量
缓存10小时以提高性能
参数:
requests: HTTP请求对象
返回:
上下文字典包含SEO和配置信息
"""
# 缓存键
key = 'seo_processor'
# 尝试从缓存获取
value = cache.get(key)
if value:
# 缓存命中,直接返回
return value
else:
# 缓存未命中,重新生成
logger.info('set processor cache.')
# 获取博客配置
setting = get_blog_setting()
value = {
# ========== 网站基本信息 ==========
'SITE_NAME': setting.site_name, # 网站名称
'SITE_DESCRIPTION': setting.site_description, # 网站描述
'SITE_SEO_DESCRIPTION': setting.site_seo_description, # SEO描述
'SITE_KEYWORDS': setting.site_keywords, # 网站关键词
# ========== 网站URL ==========
# 【问题】缓存的是处理器返回值但其中包含了requests.scheme和requests.get_host()
# 这些值可能会因请求而异http/https, 不同host缓存可能导致错误
'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'
type='p',
status='p'),
# ========== 评论配置 ==========
'OPEN_SITE_COMMENT': setting.open_site_comment, # 是否开放评论
'COMMENT_NEED_REVIEW': setting.comment_need_review, # 评论是否需要审核
# ========== 广告配置 ==========
'SHOW_GOOGLE_ADSENSE': setting.show_google_adsense, # 是否显示Google AdSense
'GOOGLE_ADSENSE_CODES': setting.google_adsense_codes, # Google AdSense代码
# ========== 备案信息(中国特有) ==========
'BEIAN_CODE': setting.beian_code, # ICP备案号
'BEIAN_CODE_GONGAN': setting.gongan_beiancode, # 公安备案号
'SHOW_GONGAN_CODE': setting.show_gongan_code, # 是否显示公安备案号
# ========== 网站分析代码 ==========
'ANALYTICS_CODE': setting.analytics_code, # 网站统计代码
# ========== 其他 ==========
'CURRENT_YEAR': timezone.now().year, # 当前年份
'GLOBAL_HEADER': setting.global_header, # 公共头部HTML
'GLOBAL_FOOTER': setting.global_footer, # 公共尾部HTML
}
# 缓存10小时
cache.set(key, value, 60 * 60 * 10)
return value

@ -1,386 +0,0 @@
#ly:
# coding:utf-8
# ==========================================
# Blog模块 - Elasticsearch文档定义
# ==========================================
import time
import elasticsearch.client
from django.conf import settings
from elasticsearch_dsl import Document, InnerDoc, Date, Integer, Long, Text, Object, GeoPoint, Keyword, Boolean
from elasticsearch_dsl.connections import connections
from blog.models import Article
# 检查是否启用了Elasticsearch
ELASTICSEARCH_ENABLED = hasattr(settings, 'ELASTICSEARCH_DSL')
# Elasticsearch初始化和配置
if ELASTICSEARCH_ENABLED:
# 创建Elasticsearch连接
connections.create_connection(
hosts=[settings.ELASTICSEARCH_DSL['default']['hosts']])
from elasticsearch import Elasticsearch
# 创建ES客户端实例
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
from elasticsearch.client import IngestClient
# 创建Ingest API客户端用于管理管道
c = IngestClient(es)
try:
# 检查'geoip'管道是否存在
c.get_pipeline('geoip')
except elasticsearch.exceptions.NotFoundError:
# 如果不存在创建geoip管道用于地理位置查询
c.put_pipeline('geoip', body='''{
"description" : "Add geoip info",
"processors" : [
{
"geoip" : {
"field" : "ip"
}
}
]
}''')
# ==========================================
# Elasticsearch文档内部字段定义
# ==========================================
class GeoIp(InnerDoc):
"""
地理位置信息
由Elasticsearch的geoip处理器生成
"""
# 大陆名称
continent_name = Keyword()
# 国家ISO编码
country_iso_code = Keyword()
# 国家名称
country_name = Keyword()
# 地理坐标(用于地图显示)
location = GeoPoint()
class UserAgentBrowser(InnerDoc):
"""浏览器信息"""
# 浏览器名称
Family = Keyword()
# 浏览器版本
Version = Keyword()
class UserAgentOS(UserAgentBrowser):
"""
操作系统信息
继承自UserAgentBrowser
"""
pass
class UserAgentDevice(InnerDoc):
"""设备信息"""
# 设备类型
Family = Keyword()
# 品牌
Brand = Keyword()
# 型号
Model = Keyword()
class UserAgent(InnerDoc):
"""
User-Agent信息
包含浏览器操作系统设备等信息
"""
# 浏览器信息
browser = Object(UserAgentBrowser, required=False)
# 操作系统信息
os = Object(UserAgentOS, required=False)
# 设备信息
device = Object(UserAgentDevice, required=False)
# 原始User-Agent字符串
string = Text()
# 是否为机器人
is_bot = Boolean()
# ==========================================
# 页面加载性能文档
# ==========================================
class ElapsedTimeDocument(Document):
"""
Elasticsearch文档 - 页面加载性能数据
记录每个页面请求的加载时间用户信息等
"""
# 请求的URL路径
url = Keyword()
# 加载时间(毫秒)
time_taken = Long()
# 记录时间
log_datetime = Date()
# 客户端IP地址
ip = Keyword()
# 地理位置信息由geoip处理器生成
geoip = Object(GeoIp, required=False)
# User-Agent信息浏览器、操作系统等
useragent = Object(UserAgent, required=False)
class Index:
# 索引名称
name = 'performance'
# 索引设置
settings = {
"number_of_shards": 1, # 分片数
"number_of_replicas": 0 # 副本数
}
class Meta:
# 文档类型
doc_type = 'ElapsedTime'
# ==========================================
# 性能文档管理器
# ==========================================
class ElaspedTimeDocumentManager:
"""
页面加载性能数据管理器
问题类名拼写错误应为 ElapsedTimeDocumentManager
"""
@staticmethod
def build_index():
"""
构建性能索引
如果索引不存在则创建它
"""
from elasticsearch import Elasticsearch
client = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
# 检查索引是否存在
res = client.indices.exists(index="performance")
if not res:
# 创建索引
ElapsedTimeDocument.init()
@staticmethod
def delete_index():
"""删除性能索引"""
from elasticsearch import Elasticsearch
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
# 删除索引忽略404错误
es.indices.delete(index='performance', ignore=[400, 404])
@staticmethod
def create(url, time_taken, log_datetime, useragent, ip):
"""
创建并保存一条性能记录
参数:
url: 请求的URL路径
time_taken: 加载时间毫秒
log_datetime: 记录时间
useragent: User-Agent对象
ip: 客户端IP地址
"""
# 确保索引存在
ElaspedTimeDocumentManager.build_index()
# 构建User-Agent信息对象
ua = UserAgent()
# 浏览器信息
ua.browser = UserAgentBrowser()
ua.browser.Family = useragent.browser.family
ua.browser.Version = useragent.browser.version_string
# 操作系统信息
ua.os = UserAgentOS()
ua.os.Family = useragent.os.family
ua.os.Version = useragent.os.version_string
# 设备信息
ua.device = UserAgentDevice()
ua.device.Family = useragent.device.family
ua.device.Brand = useragent.device.brand
ua.device.Model = useragent.device.model
# 原始User-Agent字符串
ua.string = useragent.ua_string
# 是否为机器人
ua.is_bot = useragent.is_bot
# 创建性能文档
doc = ElapsedTimeDocument(
meta={
# 使用当前时间戳作为文档ID
'id': int(round(time.time() * 1000))
},
url=url,
time_taken=time_taken,
log_datetime=log_datetime,
useragent=ua,
ip=ip)
# 保存文档使用geoip管道自动添加地理位置信息
doc.save(pipeline="geoip")
# ==========================================
# 文章搜索文档
# ==========================================
class ArticleDocument(Document):
"""
Elasticsearch文档 - 文章搜索索引
用于全文搜索文章内容
分析器说明
- ik_max_word: 中文最细粒度分词
- ik_smart: 中文最粗粒度分词
"""
# 文章内容 - 使用ik分析器处理中文
body = Text(analyzer='ik_max_word', search_analyzer='ik_smart')
# 文章标题
title = Text(analyzer='ik_max_word', search_analyzer='ik_smart')
# 文章作者信息
author = Object(properties={
'nickname': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
'id': Integer()
})
# 文章分类信息
category = Object(properties={
'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
'id': Integer()
})
# 文章标签列表
tags = Object(properties={
'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
'id': Integer()
})
# 发布时间
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:
# 文档类型
doc_type = 'Article'
# ==========================================
# 文章文档管理器
# ==========================================
class ArticleDocumentManager():
"""
文章文档管理器
管理Elasticsearch中的文章索引
"""
def __init__(self):
"""初始化时创建索引"""
self.create_index()
def create_index(self):
"""创建文章索引"""
ArticleDocument.init()
def delete_index(self):
"""删除文章索引"""
from elasticsearch import Elasticsearch
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
# 删除索引忽略404错误
es.indices.delete(index='blog', ignore=[400, 404])
def convert_to_doc(self, articles):
"""
将Article模型对象转换为ArticleDocument对象
参数:
articles: Article对象的QuerySet或列表
返回:
ArticleDocument对象列表
"""
return [
ArticleDocument(
meta={'id': article.id}, # 文档ID = 文章ID
body=article.body,
title=article.title,
author={
'nickname': 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: Article对象QuerySet或列表默认为所有已发布文章
"""
# 重新初始化索引(会删除旧索引)
ArticleDocument.init()
# 获取要索引的文章
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):
"""
更新文档
参数:
docs: ArticleDocument对象列表
"""
for doc in docs:
doc.save()

@ -1,46 +0,0 @@
#ly:
# coding:utf-8
# ==========================================
# Blog模块 - 表单定义
# ==========================================
import logging
from django import forms
from haystack.forms import SearchForm
logger = logging.getLogger(__name__)
# ==========================================
# 博客搜索表单
# ==========================================
class BlogSearchForm(SearchForm):
"""
继承自haystack的搜索表单
用于处理博客文章的全文搜索
"""
# 搜索关键词字段 - 必填
querydata = forms.CharField(required=True)
def search(self):
"""
执行搜索
验证表单数据并返回搜索结果
返回: 搜索结果QuerySet
"""
# 调用父类的search()方法获取搜索结果
datas = super(BlogSearchForm, self).search()
# 检查表单是否有效
if not self.is_valid():
# 返回空结果
return self.no_query_found()
# 【问题】:直接记录用户搜索查询词,可能记录敏感信息
# 【问题】:没有进行任何有意义的处理,此代码无用
if self.cleaned_data['querydata']:
logger.info(self.cleaned_data['querydata'])
return datas

@ -1,19 +0,0 @@
from django.core.management.base import BaseCommand
from blog.documents import ElapsedTimeDocument, ArticleDocumentManager, ElaspedTimeDocumentManager, \
ELASTICSEARCH_ENABLED
#ly:
# coding:utf-8
# TODO 参数化
class Command(BaseCommand):
help = 'build search index'
def handle(self, *args, **options):
if ELASTICSEARCH_ENABLED:
ElaspedTimeDocumentManager.build_index()
manager = ElapsedTimeDocument()
manager.init()
manager = ArticleDocumentManager()
manager.delete_index()
manager.rebuild()

@ -1,14 +0,0 @@
from django.core.management.base import BaseCommand
from blog.models import Tag, Category
#ly:
# coding:utf-8
# TODO 参数化
class Command(BaseCommand):
help = 'build search words'
def handle(self, *args, **options):
datas = set([t.name for t in Tag.objects.all()] +
[t.name for t in Category.objects.all()])
print('\n'.join(datas))

@ -1,12 +0,0 @@
from django.core.management.base import BaseCommand
from djangoblog.utils import cache
#ly:
# coding:utf-8
class Command(BaseCommand):
help = 'clear the whole cache'
def handle(self, *args, **options):
cache.clear()
self.stdout.write(self.style.SUCCESS('Cleared cache\n'))

@ -1,41 +0,0 @@
from django.contrib.auth import get_user_model
from django.contrib.auth.hashers import make_password
from django.core.management.base import BaseCommand
from blog.models import Article, Tag, Category
#ly:
# coding:utf-8
class Command(BaseCommand):
help = 'create test datas'
def handle(self, *args, **options):
user = get_user_model().objects.get_or_create(
email='test@test.com', username='测试用户', password=make_password('test!q@w#eTYU'))[0]
pcategory = Category.objects.get_or_create(
name='我是父类目', parent_category=None)[0]
category = Category.objects.get_or_create(
name='子类目', parent_category=pcategory)[0]
category.save()
basetag = Tag()
basetag.name = "标签"
basetag.save()
for i in range(1, 20):
article = Article.objects.get_or_create(
category=category,
title='nice title ' + str(i),
body='nice content ' + str(i),
author=user)[0]
tag = Tag()
tag.name = "标签" + str(i)
tag.save()
article.tags.add(tag)
article.tags.add(basetag)
article.save()
from djangoblog.utils import cache
cache.clear()
self.stdout.write(self.style.SUCCESS('created test datas \n'))

@ -1,51 +0,0 @@
from django.core.management.base import BaseCommand
from djangoblog.spider_notify import SpiderNotify
from djangoblog.utils import get_current_site
from blog.models import Article, Tag, Category
site = get_current_site().domain
#ly:
# coding:utf-8
class Command(BaseCommand):
help = 'notify baidu url'
def add_arguments(self, parser):
parser.add_argument(
'data_type',
type=str,
choices=[
'all',
'article',
'tag',
'category'],
help='article : all article,tag : all tag,category: all category,all: All of these')
def get_full_url(self, path):
url = "https://{site}{path}".format(site=site, path=path)
return url
def handle(self, *args, **options):
type = options['data_type']
self.stdout.write('start get %s' % type)
urls = []
if type == 'article' or type == 'all':
for article in Article.objects.filter(status='p'):
urls.append(article.get_full_url())
if type == 'tag' or type == 'all':
for tag in Tag.objects.all():
url = tag.get_absolute_url()
urls.append(self.get_full_url(url))
if type == 'category' or type == 'all':
for category in Category.objects.all():
url = category.get_absolute_url()
urls.append(self.get_full_url(url))
self.stdout.write(
self.style.SUCCESS(
'start notify %d urls' %
len(urls)))
SpiderNotify.baidu_notify(urls)
self.stdout.write(self.style.SUCCESS('finish notify'))

@ -1,48 +0,0 @@
import requests
from django.core.management.base import BaseCommand
from django.templatetags.static import static
from djangoblog.utils import save_user_avatar
from oauth.models import OAuthUser
from oauth.oauthmanager import get_manager_by_type
#ly:
# coding:utf-8
class Command(BaseCommand):
help = 'sync user avatar'
def test_picture(self, url):
try:
if requests.get(url, timeout=2).status_code == 200:
return True
except:
pass
def handle(self, *args, **options):
static_url = static("../")
users = OAuthUser.objects.all()
self.stdout.write(f'开始同步{len(users)}个用户头像')
for u in users:
self.stdout.write(f'开始同步:{u.nickname}')
url = u.picture
if url:
if url.startswith(static_url):
if self.test_picture(url):
continue
else:
if u.metadata:
manage = get_manager_by_type(u.type)
url = manage.get_picture(u.metadata)
url = save_user_avatar(url)
else:
url = static('blog/img/avatar.png')
else:
url = save_user_avatar(url)
else:
url = static('blog/img/avatar.png')
if url:
self.stdout.write(
f'结束同步:{u.nickname}.url:{url}')
u.picture = url
u.save()
self.stdout.write('结束同步')

@ -1,87 +0,0 @@
#ly:
# coding:utf-8
# ==========================================
# Blog模块 - 中间件
# ==========================================
# 用于记录页面加载时间和用户信息
import logging
import time
from ipware import get_client_ip
from user_agents import parse
from blog.documents import ELASTICSEARCH_ENABLED, ElaspedTimeDocumentManager
logger = logging.getLogger(__name__)
# ==========================================
# 在线中间件 - 记录页面加载性能
# ==========================================
class OnlineMiddleware(object):
"""
中间件记录页面加载时间和用户信息
功能
1. 计算每个请求的响应时间
2. 获取用户的IP地址和User-Agent信息
3. 如果启用了Elasticsearch将性能数据存储到ES
4. 将加载时间插入到响应内容中用于在页面显示
"""
def __init__(self, get_response=None):
"""初始化中间件"""
self.get_response = get_response
super().__init__()
def __call__(self, request):
"""
处理请求和响应
参数:
request: HTTP请求对象
返回:
response: HTTP响应对象
"""
# 记录请求开始时间(用于计算加载时间)
start_time = time.time()
# 获取响应对象
response = self.get_response(request)
# 获取用户代理字符串
http_user_agent = request.META.get('HTTP_USER_AGENT', '')
# 获取客户端IP地址
ip, _ = get_client_ip(request)
# 解析User-Agent信息包括浏览器、操作系统、设备等
user_agent = parse(http_user_agent)
# 只处理非流式响应普通HTTP响应
if not response.streaming:
try:
# 计算页面加载时间(秒)
cast_time = time.time() - start_time
# 如果启用了Elasticsearch记录性能数据
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(),
useragent=user_agent,
ip=ip)
# 【问题】对整个response内容进行字符串替换对大文件性能影响大
# 应该使用流式处理或在模板层面处理
response.content = response.content.replace(
b'<!!LOAD_TIMES!!>', str.encode(str(cast_time)[:5]))
except Exception as e:
# 【问题】:捕获所有异常,可能隐藏其他错误
# 应该捕获具体的异常类型
logger.error("Error OnlineMiddleware: %s" % e)
return response

@ -1,138 +0,0 @@
# Generated by Django 4.1.7 on 2023-03-02 07:14
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import mdeditor.fields
#ly:
# coding:utf-8
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='BlogSettings',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('sitename', models.CharField(default='', max_length=200, verbose_name='网站名称')),
('site_description', models.TextField(default='', max_length=1000, verbose_name='网站描述')),
('site_seo_description', models.TextField(default='', max_length=1000, verbose_name='网站SEO描述')),
('site_keywords', models.TextField(default='', max_length=1000, verbose_name='网站关键字')),
('article_sub_length', models.IntegerField(default=300, verbose_name='文章摘要长度')),
('sidebar_article_count', models.IntegerField(default=10, verbose_name='侧边栏文章数目')),
('sidebar_comment_count', models.IntegerField(default=5, verbose_name='侧边栏评论数目')),
('article_comment_count', models.IntegerField(default=5, verbose_name='文章页面默认显示评论数目')),
('show_google_adsense', models.BooleanField(default=False, verbose_name='是否显示谷歌广告')),
('google_adsense_codes', models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='广告内容')),
('open_site_comment', models.BooleanField(default=True, verbose_name='是否打开网站评论功能')),
('beiancode', models.CharField(blank=True, default='', max_length=2000, null=True, verbose_name='备案号')),
('analyticscode', models.TextField(default='', max_length=1000, verbose_name='网站统计代码')),
('show_gongan_code', models.BooleanField(default=False, verbose_name='是否显示公安备案号')),
('gongan_beiancode', models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='公安备案号')),
],
options={
'verbose_name': '网站配置',
'verbose_name_plural': '网站配置',
},
),
migrations.CreateModel(
name='Links',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=30, unique=True, verbose_name='链接名称')),
('link', models.URLField(verbose_name='链接地址')),
('sequence', models.IntegerField(unique=True, verbose_name='排序')),
('is_enable', models.BooleanField(default=True, verbose_name='是否显示')),
('show_type', models.CharField(choices=[('i', '首页'), ('l', '列表页'), ('p', '文章页面'), ('a', '全站'), ('s', '友情链接页面')], default='i', max_length=1, verbose_name='显示类型')),
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
],
options={
'verbose_name': '友情链接',
'verbose_name_plural': '友情链接',
'ordering': ['sequence'],
},
),
migrations.CreateModel(
name='SideBar',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100, verbose_name='标题')),
('content', models.TextField(verbose_name='内容')),
('sequence', models.IntegerField(unique=True, verbose_name='排序')),
('is_enable', models.BooleanField(default=True, verbose_name='是否启用')),
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
],
options={
'verbose_name': '侧边栏',
'verbose_name_plural': '侧边栏',
'ordering': ['sequence'],
},
),
migrations.CreateModel(
name='Tag',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
('name', models.CharField(max_length=30, unique=True, verbose_name='标签名')),
('slug', models.SlugField(blank=True, default='no-slug', max_length=60)),
],
options={
'verbose_name': '标签',
'verbose_name_plural': '标签',
'ordering': ['name'],
},
),
migrations.CreateModel(
name='Category',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
('name', models.CharField(max_length=30, unique=True, verbose_name='分类名')),
('slug', models.SlugField(blank=True, default='no-slug', max_length=60)),
('index', models.IntegerField(default=0, verbose_name='权重排序-越大越靠前')),
('parent_category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='父级分类')),
],
options={
'verbose_name': '分类',
'verbose_name_plural': '分类',
'ordering': ['-index'],
},
),
migrations.CreateModel(
name='Article',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
('title', models.CharField(max_length=200, unique=True, verbose_name='标题')),
('body', mdeditor.fields.MDTextField(verbose_name='正文')),
('pub_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='发布时间')),
('status', models.CharField(choices=[('d', '草稿'), ('p', '发表')], default='p', max_length=1, verbose_name='文章状态')),
('comment_status', models.CharField(choices=[('o', '打开'), ('c', '关闭')], default='o', max_length=1, verbose_name='评论状态')),
('type', models.CharField(choices=[('a', '文章'), ('p', '页面')], default='a', max_length=1, verbose_name='类型')),
('views', models.PositiveIntegerField(default=0, verbose_name='浏览量')),
('article_order', models.IntegerField(default=0, verbose_name='排序,数字越大越靠前')),
('show_toc', models.BooleanField(default=False, verbose_name='是否显示toc目录')),
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='作者')),
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='分类')),
('tags', models.ManyToManyField(blank=True, to='blog.tag', verbose_name='标签集合')),
],
options={
'verbose_name': '文章',
'verbose_name_plural': '文章',
'ordering': ['-article_order', '-pub_time'],
'get_latest_by': 'id',
},
),
]

@ -1,24 +0,0 @@
# Generated by Django 4.1.7 on 2023-03-29 06:08
from django.db import migrations, models
#ly:
# coding:utf-8
class Migration(migrations.Migration):
dependencies = [
('blog', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='blogsettings',
name='global_footer',
field=models.TextField(blank=True, default='', null=True, verbose_name='公共尾部'),
),
migrations.AddField(
model_name='blogsettings',
name='global_header',
field=models.TextField(blank=True, default='', null=True, verbose_name='公共头部'),
),
]

@ -1,18 +0,0 @@
# Generated by Django 4.2.1 on 2023-05-09 07:45
from django.db import migrations, models
#ly:
# coding:utf-8
class Migration(migrations.Migration):
dependencies = [
('blog', '0002_blogsettings_global_footer_and_more'),
]
operations = [
migrations.AddField(
model_name='blogsettings',
name='comment_need_review',
field=models.BooleanField(default=False, verbose_name='评论是否需要审核'),
),
]

@ -1,28 +0,0 @@
# Generated by Django 4.2.1 on 2023-05-09 07:51
from django.db import migrations
#ly:
# coding:utf-8
class Migration(migrations.Migration):
dependencies = [
('blog', '0003_blogsettings_comment_need_review'),
]
operations = [
migrations.RenameField(
model_name='blogsettings',
old_name='analyticscode',
new_name='analytics_code',
),
migrations.RenameField(
model_name='blogsettings',
old_name='beiancode',
new_name='beian_code',
),
migrations.RenameField(
model_name='blogsettings',
old_name='sitename',
new_name='site_name',
),
]

@ -1,301 +0,0 @@
# Generated by Django 4.2.5 on 2023-09-06 13:13
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import mdeditor.fields
#ly:
# coding:utf-8
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('blog', '0004_rename_analyticscode_blogsettings_analytics_code_and_more'),
]
operations = [
migrations.AlterModelOptions(
name='article',
options={'get_latest_by': 'id', 'ordering': ['-article_order', '-pub_time'], 'verbose_name': 'article', 'verbose_name_plural': 'article'},
),
migrations.AlterModelOptions(
name='category',
options={'ordering': ['-index'], 'verbose_name': 'category', 'verbose_name_plural': 'category'},
),
migrations.AlterModelOptions(
name='links',
options={'ordering': ['sequence'], 'verbose_name': 'link', 'verbose_name_plural': 'link'},
),
migrations.AlterModelOptions(
name='sidebar',
options={'ordering': ['sequence'], 'verbose_name': 'sidebar', 'verbose_name_plural': 'sidebar'},
),
migrations.AlterModelOptions(
name='tag',
options={'ordering': ['name'], 'verbose_name': 'tag', 'verbose_name_plural': 'tag'},
),
migrations.RemoveField(
model_name='article',
name='created_time',
),
migrations.RemoveField(
model_name='article',
name='last_mod_time',
),
migrations.RemoveField(
model_name='category',
name='created_time',
),
migrations.RemoveField(
model_name='category',
name='last_mod_time',
),
migrations.RemoveField(
model_name='links',
name='created_time',
),
migrations.RemoveField(
model_name='sidebar',
name='created_time',
),
migrations.RemoveField(
model_name='tag',
name='created_time',
),
migrations.RemoveField(
model_name='tag',
name='last_mod_time',
),
migrations.AddField(
model_name='article',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
migrations.AddField(
model_name='article',
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
),
migrations.AddField(
model_name='category',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
migrations.AddField(
model_name='category',
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
),
migrations.AddField(
model_name='links',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
migrations.AddField(
model_name='sidebar',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
migrations.AddField(
model_name='tag',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
migrations.AddField(
model_name='tag',
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
),
migrations.AlterField(
model_name='article',
name='article_order',
field=models.IntegerField(default=0, verbose_name='order'),
),
migrations.AlterField(
model_name='article',
name='author',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='author'),
),
migrations.AlterField(
model_name='article',
name='body',
field=mdeditor.fields.MDTextField(verbose_name='body'),
),
migrations.AlterField(
model_name='article',
name='category',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='category'),
),
migrations.AlterField(
model_name='article',
name='comment_status',
field=models.CharField(choices=[('o', 'Open'), ('c', 'Close')], default='o', max_length=1, verbose_name='comment status'),
),
migrations.AlterField(
model_name='article',
name='pub_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='publish time'),
),
migrations.AlterField(
model_name='article',
name='show_toc',
field=models.BooleanField(default=False, verbose_name='show toc'),
),
migrations.AlterField(
model_name='article',
name='status',
field=models.CharField(choices=[('d', 'Draft'), ('p', 'Published')], default='p', max_length=1, verbose_name='status'),
),
migrations.AlterField(
model_name='article',
name='tags',
field=models.ManyToManyField(blank=True, to='blog.tag', verbose_name='tag'),
),
migrations.AlterField(
model_name='article',
name='title',
field=models.CharField(max_length=200, unique=True, verbose_name='title'),
),
migrations.AlterField(
model_name='article',
name='type',
field=models.CharField(choices=[('a', 'Article'), ('p', 'Page')], default='a', max_length=1, verbose_name='type'),
),
migrations.AlterField(
model_name='article',
name='views',
field=models.PositiveIntegerField(default=0, verbose_name='views'),
),
migrations.AlterField(
model_name='blogsettings',
name='article_comment_count',
field=models.IntegerField(default=5, verbose_name='article comment count'),
),
migrations.AlterField(
model_name='blogsettings',
name='article_sub_length',
field=models.IntegerField(default=300, verbose_name='article sub length'),
),
migrations.AlterField(
model_name='blogsettings',
name='google_adsense_codes',
field=models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='adsense code'),
),
migrations.AlterField(
model_name='blogsettings',
name='open_site_comment',
field=models.BooleanField(default=True, verbose_name='open site comment'),
),
migrations.AlterField(
model_name='blogsettings',
name='show_google_adsense',
field=models.BooleanField(default=False, verbose_name='show adsense'),
),
migrations.AlterField(
model_name='blogsettings',
name='sidebar_article_count',
field=models.IntegerField(default=10, verbose_name='sidebar article count'),
),
migrations.AlterField(
model_name='blogsettings',
name='sidebar_comment_count',
field=models.IntegerField(default=5, verbose_name='sidebar comment count'),
),
migrations.AlterField(
model_name='blogsettings',
name='site_description',
field=models.TextField(default='', max_length=1000, verbose_name='site description'),
),
migrations.AlterField(
model_name='blogsettings',
name='site_keywords',
field=models.TextField(default='', max_length=1000, verbose_name='site keywords'),
),
migrations.AlterField(
model_name='blogsettings',
name='site_name',
field=models.CharField(default='', max_length=200, verbose_name='site name'),
),
migrations.AlterField(
model_name='blogsettings',
name='site_seo_description',
field=models.TextField(default='', max_length=1000, verbose_name='site seo description'),
),
migrations.AlterField(
model_name='category',
name='index',
field=models.IntegerField(default=0, verbose_name='index'),
),
migrations.AlterField(
model_name='category',
name='name',
field=models.CharField(max_length=30, unique=True, verbose_name='category name'),
),
migrations.AlterField(
model_name='category',
name='parent_category',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='parent category'),
),
migrations.AlterField(
model_name='links',
name='is_enable',
field=models.BooleanField(default=True, verbose_name='is show'),
),
migrations.AlterField(
model_name='links',
name='last_mod_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
),
migrations.AlterField(
model_name='links',
name='link',
field=models.URLField(verbose_name='link'),
),
migrations.AlterField(
model_name='links',
name='name',
field=models.CharField(max_length=30, unique=True, verbose_name='link name'),
),
migrations.AlterField(
model_name='links',
name='sequence',
field=models.IntegerField(unique=True, verbose_name='order'),
),
migrations.AlterField(
model_name='links',
name='show_type',
field=models.CharField(choices=[('i', 'index'), ('l', 'list'), ('p', 'post'), ('a', 'all'), ('s', 'slide')], default='i', max_length=1, verbose_name='show type'),
),
migrations.AlterField(
model_name='sidebar',
name='content',
field=models.TextField(verbose_name='content'),
),
migrations.AlterField(
model_name='sidebar',
name='is_enable',
field=models.BooleanField(default=True, verbose_name='is enable'),
),
migrations.AlterField(
model_name='sidebar',
name='last_mod_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
),
migrations.AlterField(
model_name='sidebar',
name='name',
field=models.CharField(max_length=100, verbose_name='title'),
),
migrations.AlterField(
model_name='sidebar',
name='sequence',
field=models.IntegerField(unique=True, verbose_name='order'),
),
migrations.AlterField(
model_name='tag',
name='name',
field=models.CharField(max_length=30, unique=True, verbose_name='tag name'),
),
]

@ -1,18 +0,0 @@
# Generated by Django 4.2.7 on 2024-01-26 02:41
from django.db import migrations
#ly:
# coding:utf-8
class Migration(migrations.Migration):
dependencies = [
('blog', '0005_alter_article_options_alter_category_options_and_more'),
]
operations = [
migrations.AlterModelOptions(
name='blogsettings',
options={'verbose_name': 'Website configuration', 'verbose_name_plural': 'Website configuration'},
),
]

@ -1,630 +0,0 @@
#ly:
# coding:utf-8
# ==========================================
# Blog模块 - 数据模型定义
# ==========================================
# 包含文章、分类、标签、友情链接等核心数据模型
import logging
import re
from abc import abstractmethod
from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import models
from django.urls import reverse
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from mdeditor.fields import MDTextField
from uuslug import slugify
from djangoblog.utils import cache_decorator, cache
from djangoblog.utils import get_current_site
logger = logging.getLogger(__name__)
# ==========================================
# 友情链接显示类型枚举
# ==========================================
class LinkShowType(models.TextChoices):
"""
定义友情链接的显示位置
- I: 在首页显示
- L: 在列表页显示
- P: 在文章页显示
- A: 在所有页面显示
- S: 在幻灯片中显示
"""
I = ('i', _('index'))
L = ('l', _('list'))
P = ('p', _('post'))
A = ('a', _('all'))
S = ('s', _('slide'))
# ==========================================
# 基础模型类 - 所有Blog模型的父类
# ==========================================
class BaseModel(models.Model):
"""
Blog模块所有模型的基类
提供通用的字段和方法ID创建时间修改时间等
"""
# 主键ID
id = models.AutoField(primary_key=True)
# 创建时间 - 自动设置为当前时间
creation_time = models.DateTimeField(_('creation time'), default=now)
# 最后修改时间 - 自动设置为当前时间
last_modify_time = models.DateTimeField(_('modify time'), default=now)
def save(self, *args, **kwargs):
"""
重写保存方法
功能
1. 对Article只更新views时使用update()方法提高性能
2. 自动生成slug字段从title或name字段
问题当更新views时不会更新last_modify_time时间戳
"""
# 检查是否仅更新views字段用于提高统计性能
is_update_views = isinstance(
self,
Article) and 'update_fields' in kwargs and kwargs['update_fields'] == ['views']
if is_update_views:
# 直接使用update()更新绕过save()方法
Article.objects.filter(pk=self.pk).update(views=self.views)
else:
# 自动生成slug字段URL友好的标识符
if 'slug' in self.__dict__:
# 优先使用title否则使用name
slug = getattr(
self, 'title') if 'title' in self.__dict__ else getattr(
self, 'name')
# 使用uuslug库生成URL友好的slug
setattr(self, 'slug', slugify(slug))
super().save(*args, **kwargs)
def get_full_url(self):
"""
获取完整的网址包含域名和协议
用于生成SEO友好的完整URL
返回格式: https://example.com/path/to/article/
"""
site = get_current_site().domain
url = "https://{site}{path}".format(site=site,
path=self.get_absolute_url())
return url
class Meta:
# 抽象模型 - 不会在数据库中创建表
abstract = True
@abstractmethod
def get_absolute_url(self):
"""
抽象方法 - 子类必须实现
返回该对象的相对URL地址: /article/2024/01/01/1.html
"""
pass
# ==========================================
# 文章模型
# ==========================================
class Article(BaseModel):
"""
博客文章模型
存储文章的标题内容发布状态分类标签等信息
"""
# 文章发布状态选项
STATUS_CHOICES = (
('d', _('Draft')), # d: 草稿
('p', _('Published')), # p: 已发布
)
# 文章评论状态选项
COMMENT_STATUS = (
('o', _('Open')), # o: 允许评论
('c', _('Close')), # c: 关闭评论
)
# 文章类型选项
TYPE = (
('a', _('Article')), # a: 普通文章
('p', _('Page')), # p: 页面(如关于、友链等)
)
# ========== 文章基本内容字段 ==========
# 文章标题 - 唯一最多200字符
title = models.CharField(_('title'), max_length=200, unique=True)
# 文章正文 - 使用Markdown编辑器字段支持富文本编辑
body = MDTextField(_('body'))
# 文章发布时间 - 默认为当前时间
pub_time = models.DateTimeField(
_('publish time'), blank=False, null=False, default=now)
# ========== 文章状态字段 ==========
# 文章发布状态(草稿或已发布)
status = models.CharField(
_('status'),
max_length=1,
choices=STATUS_CHOICES,
default='p')
# 文章评论状态(开放或关闭)
comment_status = models.CharField(
_('comment status'),
max_length=1,
choices=COMMENT_STATUS,
default='o')
# 文章类型(普通文章或独立页面)
type = models.CharField(_('type'), max_length=1, choices=TYPE, default='a')
# ========== 统计字段 ==========
# 文章阅读次数 - 每次访问文章时递增
views = models.PositiveIntegerField(_('views'), default=0)
# ========== 关联字段 ==========
# 文章作者 - 关联到Django User模型
author = models.ForeignKey(
settings.AUTH_USER_MODEL,
verbose_name=_('author'),
blank=False,
null=False,
on_delete=models.CASCADE)
# ========== 排序和显示字段 ==========
# 文章排序值 - 值越大越靠前显示,用于置顶
article_order = models.IntegerField(
_('order'), blank=False, null=False, default=0)
# 是否显示文章目录(Table of Contents)
show_toc = models.BooleanField(_('show toc'), blank=False, null=False, default=False)
# 文章所属分类 - 外键关联Category必填
category = models.ForeignKey(
'Category',
verbose_name=_('category'),
on_delete=models.CASCADE,
blank=False,
null=False)
# 文章标签 - 多对多关系,一篇文章可有多个标签,可选
tags = models.ManyToManyField('Tag', verbose_name=_('tag'), blank=True)
def body_to_string(self):
"""获取文章内容的字符串表示"""
return self.body
def __str__(self):
"""字符串表示 - 显示文章标题"""
return self.title
class Meta:
# 排序先按article_order降序置顶在前再按pub_time降序新文章在前
ordering = ['-article_order', '-pub_time']
verbose_name = _('article')
verbose_name_plural = verbose_name
get_latest_by = 'id'
def get_absolute_url(self):
"""
获取文章的相对URL
URL格式: /article/YYYY/MM/DD/article_id.html
这样的格式便于SEO和URL可读性
"""
return reverse('blog:detailbyid', kwargs={
'article_id': self.id,
'year': self.creation_time.year,
'month': self.creation_time.month,
'day': self.creation_time.day
})
@cache_decorator(60 * 60 * 10)
def get_category_tree(self):
"""
获取该文章分类的完整分类树从当前分类到根分类
返回格式: [(分类名, 分类URL), ...]
缓存时间: 10小时
"""
tree = self.category.get_category_tree()
# 提取分类名和URL
names = list(map(lambda c: (c.name, c.get_absolute_url()), tree))
return names
def save(self, *args, **kwargs):
"""
保存文章
此实现是冗余的只是调用父类方法可以删除或扩展功能
"""
super().save(*args, **kwargs)
def viewed(self):
"""
记录文章被浏览 - 增加阅读计数
使用update()方法直接更新views字段提高性能不经过save()方法
"""
self.views += 1
self.save(update_fields=['views'])
def comment_list(self):
"""
获取文章的评论列表只包含已启用的评论
使用缓存避免重复查询缓存时间: 100分钟
返回: QuerySet对象包含所有启用的评论按ID降序排列
"""
cache_key = 'article_comments_{id}'.format(id=self.id)
value = cache.get(cache_key)
if value:
# 缓存命中
logger.info('get article comments:{id}'.format(id=self.id))
return value
else:
# 缓存未命中,从数据库查询
comments = self.comment_set.filter(is_enable=True).order_by('-id')
cache.set(cache_key, comments, 60 * 100)
logger.info('set article comments:{id}'.format(id=self.id))
return comments
def get_admin_url(self):
"""获取Django Admin中该文章的编辑URL"""
info = (self._meta.app_label, self._meta.model_name)
return reverse('admin:%s_%s_change' % info, args=(self.pk,))
@cache_decorator(expiration=60 * 100)
def next_article(self):
"""
获取下一篇文章ID更大且已发布的文章
缓存时间: 100分钟
返回: Article对象或None
"""
return Article.objects.filter(
id__gt=self.id, status='p').order_by('id').first()
@cache_decorator(expiration=60 * 100)
def prev_article(self):
"""
获取上一篇文章ID更小且已发布的文章
缓存时间: 100分钟
返回: Article对象或None
"""
return Article.objects.filter(id__lt=self.id, status='p').first()
def get_first_image_url(self):
"""
从文章Markdown内容中提取第一张图片的URL
使用正则表达式匹配Markdown图片语法: ![alt](url)
返回: 图片URL字符串如果没有找到则返回空字符串
"""
# 正则表达式匹配Markdown图片语法
match = re.search(r'!\[.*?\]\((.+?)\)', self.body)
if match:
return match.group(1)
return ""
# ==========================================
# 文章分类模型
# ==========================================
class Category(BaseModel):
"""
文章分类模型
支持多级分类结构通过parent_category实现自引用的树形结构
"""
# 分类名称 - 唯一最多30字符
name = models.CharField(_('category name'), max_length=30, unique=True)
# 父分类 - 自引用,支持无限级分类,可选
parent_category = models.ForeignKey(
'self',
verbose_name=_('parent category'),
blank=True,
null=True,
on_delete=models.CASCADE)
# URL友好的标识符 - 自动生成用于URL中
slug = models.SlugField(default='no-slug', max_length=60, blank=True)
# 排序索引 - 越大越靠前显示
index = models.IntegerField(default=0, verbose_name=_('index'))
class Meta:
# 按index降序排列排序值大的在前
ordering = ['-index']
verbose_name = _('category')
verbose_name_plural = verbose_name
def get_absolute_url(self):
"""
获取分类页面的相对URL
URL格式: /category/{slug}.html
"""
return reverse(
'blog:category_detail', kwargs={
'category_name': self.slug})
def __str__(self):
"""字符串表示 - 显示分类名称"""
return self.name
@cache_decorator(60 * 60 * 10)
def get_category_tree(self):
"""
递归获取该分类的完整分类树从该分类到根分类
返回分类链表最后一个是根分类
缓存时间: 10小时
问题如果存在循环引用某个分类的parent指向自己或形成环
会导致无限递归应该添加深度限制和循环检测
"""
categorys = []
def parse(category):
# 将分类添加到列表
categorys.append(category)
# 如果有父分类,递归获取
if category.parent_category:
parse(category.parent_category)
parse(self)
return categorys
@cache_decorator(60 * 60 * 10)
def get_sub_categorys(self):
"""
获取该分类的所有子分类包括嵌套的子分类
返回所有直接和间接子分类
缓存时间: 10小时
问题逻辑有误第235行检查的是category而非child
当childs为空时child未定义会导致UnboundLocalError
"""
categorys = []
# 预加载所有分类到内存
all_categorys = Category.objects.all()
def parse(category):
# 如果分类不在列表中,添加它
if category not in categorys:
categorys.append(category)
# 获取该分类的直接子分类
childs = all_categorys.filter(parent_category=category)
for child in childs:
# 【问题】检查的是category而非child
if category not in categorys:
categorys.append(child)
# 【问题】当childs为空时child未定义
parse(child)
parse(self)
return categorys
# ==========================================
# 文章标签模型
# ==========================================
class Tag(BaseModel):
"""
文章标签模型
用于给文章添加灵活的分类标签相比分类更加灵活
"""
# 标签名称 - 唯一最多30字符
name = models.CharField(_('tag name'), max_length=30, unique=True)
# URL友好的标识符 - 自动生成
slug = models.SlugField(default='no-slug', max_length=60, blank=True)
def __str__(self):
"""字符串表示 - 显示标签名称"""
return self.name
def get_absolute_url(self):
"""
获取标签页面的相对URL
URL格式: /tag/{slug}.html
"""
return reverse('blog:tag_detail', kwargs={'tag_name': self.slug})
@cache_decorator(60 * 60 * 10)
def get_article_count(self):
"""
获取该标签下的文章数量
使用distinct()是因为一篇文章可能有多个标签
缓存时间: 10小时
返回: 文章数量
"""
return Article.objects.filter(tags__name=self.name).distinct().count()
class Meta:
# 按标签名称字母序排列
ordering = ['name']
verbose_name = _('tag')
verbose_name_plural = verbose_name
# ==========================================
# 友情链接模型
# ==========================================
class Links(models.Model):
"""
友情链接模型
用于管理博客的友情链接支持在不同页面显示
"""
# 链接名称 - 唯一最多30字符
name = models.CharField(_('link name'), max_length=30, unique=True)
# 链接URL地址
link = models.URLField(_('link'))
# 排序序号 - 唯一,控制显示顺序(小的排前)
sequence = models.IntegerField(_('order'), unique=True)
# 是否启用显示
is_enable = models.BooleanField(
_('is show'), default=True, blank=False, null=False)
# 显示位置 - 在首页、列表页、文章页等位置显示
show_type = models.CharField(
_('show type'),
max_length=1,
choices=LinkShowType.choices,
default=LinkShowType.I)
# 创建时间
creation_time = models.DateTimeField(_('creation time'), default=now)
# 最后修改时间
last_mod_time = models.DateTimeField(_('modify time'), default=now)
class Meta:
# 按sequence升序排列小的排前
ordering = ['sequence']
verbose_name = _('link')
verbose_name_plural = verbose_name
def __str__(self):
"""字符串表示 - 显示链接名称"""
return self.name
# ==========================================
# 侧边栏模型
# ==========================================
class SideBar(models.Model):
"""
侧边栏小部件模型
用于在侧边栏显示自定义的HTML内容如广告公告等
"""
# 侧边栏标题/名称
name = models.CharField(_('title'), max_length=100)
# 侧边栏内容 - 支持HTML
content = models.TextField(_('content'))
# 排序序号 - 唯一,控制显示顺序(小的排前)
sequence = models.IntegerField(_('order'), unique=True)
# 是否启用显示
is_enable = models.BooleanField(_('is enable'), default=True)
# 创建时间
creation_time = models.DateTimeField(_('creation time'), default=now)
# 最后修改时间
last_mod_time = models.DateTimeField(_('modify time'), default=now)
class Meta:
# 按sequence升序排列
ordering = ['sequence']
verbose_name = _('sidebar')
verbose_name_plural = verbose_name
def __str__(self):
"""字符串表示 - 显示侧边栏名称"""
return self.name
# ==========================================
# 博客全局设置模型
# ==========================================
class BlogSettings(models.Model):
"""
博客全局配置模型
该模型应该只有一条记录包含所有博客全局配置
通过clean()方法确保只有一条记录
"""
# ========== 基本信息字段 ==========
# 网站名称
site_name = models.CharField(
_('site name'),
max_length=200,
null=False,
blank=False,
default='')
# 网站简介/描述
site_description = models.TextField(
_('site description'),
max_length=1000,
null=False,
blank=False,
default='')
# SEO描述用于meta description标签
site_seo_description = models.TextField(
_('site seo description'), max_length=1000, null=False, blank=False, default='')
# 网站关键词用于SEO meta keywords标签
site_keywords = models.TextField(
_('site keywords'),
max_length=1000,
null=False,
blank=False,
default='')
# ========== 文章显示配置 ==========
# 文章摘要长度(首页列表显示的字数)
article_sub_length = models.IntegerField(_('article sub length'), default=300)
# 侧边栏显示的最新文章数
sidebar_article_count = models.IntegerField(_('sidebar article count'), default=10)
# 侧边栏显示的最新评论数
sidebar_comment_count = models.IntegerField(_('sidebar comment count'), default=5)
# 文章详情页显示的评论数
article_comment_count = models.IntegerField(_('article comment count'), default=5)
# ========== Google AdSense广告配置 ==========
# 是否显示Google AdSense广告
show_google_adsense = models.BooleanField(_('show adsense'), default=False)
# Google AdSense代码
google_adsense_codes = models.TextField(
_('adsense code'), max_length=2000, null=True, blank=True, default='')
# ========== 评论配置 ==========
# 是否开放网站评论功能
open_site_comment = models.BooleanField(_('open site comment'), default=True)
# 评论是否需要管理员审核false:直接发布 true:需要审核)
comment_need_review = models.BooleanField(
'评论是否需要审核', default=False, null=False)
# ========== 公共HTML代码 ==========
# 公共头部HTML会显示在所有页面的<head>中)
global_header = models.TextField("公共头部", null=True, blank=True, default='')
# 公共尾部HTML会显示在所有页面的底部
global_footer = models.TextField("公共尾部", null=True, blank=True, default='')
# ========== ICP备案信息中国特有 ==========
# ICP备案号
beian_code = models.CharField(
'备案号',
max_length=2000,
null=True,
blank=True,
default='')
# ========== 网站分析/统计代码 ==========
# 网站统计代码如Google Analytics、百度统计等JavaScript代码
analytics_code = 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='')
class Meta:
verbose_name = _('Website configuration')
verbose_name_plural = verbose_name
def __str__(self):
"""字符串表示 - 显示网站名称"""
return self.site_name
def clean(self):
"""
验证数据完整性
确保数据库中只有一条设置记录
"""
if BlogSettings.objects.exclude(id=self.id).count():
raise ValidationError(_('There can only be one configuration'))
def save(self, *args, **kwargs):
"""
保存设置时清空所有缓存
因为设置变更后需要立即生效所以清除所有缓存强制重新生成
"""
super().save(*args, **kwargs)
from djangoblog.utils import cache
# 清空所有缓存,强制重新生成
cache.clear()

@ -1,43 +0,0 @@
#ly:
# coding:utf-8
# ==========================================
# Blog模块 - Haystack搜索索引配置
# ==========================================
from haystack import indexes
from blog.models import Article
# ==========================================
# 文章搜索索引
# ==========================================
class ArticleIndex(indexes.SearchIndex, indexes.Indexable):
"""
文章搜索索引定义
用于Haystack搜索框架支持Whoosh等本地搜索引擎
索引的数据来自模板search/indexes/blog/article_text.txt
"""
# 文档字段 - 搜索的主要内容
# use_template=True 表示从模板文件获取内容
text = indexes.CharField(document=True, use_template=True)
def get_model(self):
"""
获取要索引的模型
返回: Article模型
"""
return Article
def index_queryset(self, using=None):
"""
获取要被索引的QuerySet
只索引已发布的文章status='p'
参数:
using: 搜索后端名称可选
返回:
QuerySet对象
"""
return self.get_model().objects.filter(status='p')

@ -1,9 +0,0 @@
.button {
border: none;
padding: 4px 80px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
margin: 4px 2px;
}

@ -1,47 +0,0 @@
let wait = 60;
function time(o) {
if (wait == 0) {
o.removeAttribute("disabled");
o.value = "获取验证码";
wait = 60
return false
} else {
o.setAttribute("disabled", true);
o.value = "重新发送(" + wait + ")";
wait--;
setTimeout(function () {
time(o)
},
1000)
}
}
document.getElementById("btn").onclick = function () {
let id_email = $("#id_email")
let token = $("*[name='csrfmiddlewaretoken']").val()
let ts = this
let myErr = $("#myErr")
$.ajax(
{
url: "/forget_password_code/",
type: "POST",
data: {
"email": id_email.val(),
"csrfmiddlewaretoken": token
},
success: function (result) {
if (result != "ok") {
myErr.remove()
id_email.after("<ul className='errorlist' id='myErr'><li>" + result + "</li></ul>")
return
}
myErr.remove()
time(ts)
},
error: function (e) {
alert("发送失败,请重试")
}
}
);
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -1,13 +0,0 @@
/*!
* IE10 viewport hack for Surface/desktop Windows 8 bug
* Copyright 2014-2015 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
*/
/*
* See the Getting Started docs for more information:
* http://getbootstrap.com/getting-started/#support-ie10-width
*/
@-ms-viewport { width: device-width; }
@-o-viewport { width: device-width; }
@viewport { width: device-width; }

@ -1,58 +0,0 @@
body {
padding-top: 40px;
padding-bottom: 40px;
background-color: #fff;
}
.form-signin {
max-width: 330px;
padding: 15px;
margin: 0 auto;
}
.form-signin-heading {
margin: 0 0 15px;
font-size: 18px;
font-weight: 400;
color: #555;
}
.form-signin .checkbox {
margin-bottom: 10px;
font-weight: normal;
}
.form-signin .form-control {
position: relative;
height: auto;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
padding: 10px;
font-size: 16px;
}
.form-signin .form-control:focus {
z-index: 2;
}
.form-signin input[type="email"] {
margin-bottom: 10px;
}
.form-signin input[type="password"] {
margin-bottom: 10px;
}
.card {
width: 304px;
padding: 20px 25px 30px;
margin: 0 auto 25px;
background-color: #f7f7f7;
border-radius: 2px;
-webkit-box-shadow: 0 2px 2px rgba(0, 0, 0, .3);
box-shadow: 0 2px 2px rgba(0, 0, 0, .3);
}
.card-signin {
width: 354px;
padding: 40px;
}
.card-signin .profile-img {
display: block;
width: 96px;
height: 96px;
margin: 0 auto 10px;
}

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 221 B

@ -1,51 +0,0 @@
// NOTICE!! DO NOT USE ANY OF THIS JAVASCRIPT
// IT'S JUST JUNK FOR OUR DOCS!
// ++++++++++++++++++++++++++++++++++++++++++
/*!
* Copyright 2014-2015 Twitter, Inc.
*
* Licensed under the Creative Commons Attribution 3.0 Unported License. For
* details, see https://creativecommons.org/licenses/by/3.0/.
*/
// Intended to prevent false-positive bug reports about Bootstrap not working properly in old versions of IE due to folks testing using IE's unreliable emulation modes.
(function () {
'use strict';
function emulatedIEMajorVersion() {
var groups = /MSIE ([0-9.]+)/.exec(window.navigator.userAgent)
if (groups === null) {
return null
}
var ieVersionNum = parseInt(groups[1], 10)
var ieMajorVersion = Math.floor(ieVersionNum)
return ieMajorVersion
}
function actualNonEmulatedIEMajorVersion() {
// Detects the actual version of IE in use, even if it's in an older-IE emulation mode.
// IE JavaScript conditional compilation docs: https://msdn.microsoft.com/library/121hztk3%28v=vs.94%29.aspx
// @cc_on docs: https://msdn.microsoft.com/library/8ka90k2e%28v=vs.94%29.aspx
var jscriptVersion = new Function('/*@cc_on return @_jscript_version; @*/')() // jshint ignore:line
if (jscriptVersion === undefined) {
return 11 // IE11+ not in emulation mode
}
if (jscriptVersion < 9) {
return 8 // IE8 (or lower; haven't tested on IE<8)
}
return jscriptVersion // IE9 or IE10 in any mode, or IE11 in non-IE11 mode
}
var ua = window.navigator.userAgent
if (ua.indexOf('Opera') > -1 || ua.indexOf('Presto') > -1) {
return // Opera, which might pretend to be IE
}
var emulated = emulatedIEMajorVersion()
if (emulated === null) {
return // Not IE
}
var nonEmulated = actualNonEmulatedIEMajorVersion()
if (emulated !== nonEmulated) {
window.alert('WARNING: You appear to be using IE' + nonEmulated + ' in IE' + emulated + ' emulation mode.\nIE emulation modes can behave significantly differently from ACTUAL older versions of IE.\nPLEASE DON\'T FILE BOOTSTRAP BUGS based on testing in IE emulation modes!')
}
})();

@ -1,23 +0,0 @@
/*!
* IE10 viewport hack for Surface/desktop Windows 8 bug
* Copyright 2014-2015 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
*/
// See the Getting Started docs for more information:
// http://getbootstrap.com/getting-started/#support-ie10-width
(function () {
'use strict';
if (navigator.userAgent.match(/IEMobile\/10\.0/)) {
var msViewportStyle = document.createElement('style')
msViewportStyle.appendChild(
document.createTextNode(
'@-ms-viewport{width:auto!important}'
)
)
document.querySelector('head').appendChild(msViewportStyle)
}
})();

@ -1,273 +0,0 @@
/*
Styles for older IE versions (previous to IE9).
*/
body {
background-color: #e6e6e6;
}
body.custom-background-empty {
background-color: #fff;
}
body.custom-background-empty .site,
body.custom-background-white .site {
box-shadow: none;
margin-bottom: 0;
margin-top: 0;
padding: 0;
}
.assistive-text,
.site .screen-reader-text {
clip: rect(1px 1px 1px 1px);
}
.full-width .site-content {
float: none;
width: 100%;
}
img.size-full,
img.size-large,
img.header-image,
img.wp-post-image,
img[class*="align"],
img[class*="wp-image-"],
img[class*="attachment-"] {
width: auto; /* Prevent stretching of full-size and large-size images with height and width attributes in IE8 */
}
.author-avatar {
float: left;
margin-top: 8px;
margin-top: 0.571428571rem;
}
.author-description {
float: right;
width: 80%;
}
.site {
box-shadow: 0 2px 6px rgba(100, 100, 100, 0.3);
margin: 48px auto;
max-width: 960px;
overflow: hidden;
padding: 0 40px;
}
.site-content {
float: left;
width: 65.104166667%;
}
body.template-front-page .site-content,
body.attachment .site-content,
body.full-width .site-content {
width: 100%;
}
.widget-area {
float: right;
width: 26.041666667%;
}
.site-header h1,
.site-header h2 {
text-align: left;
}
.site-header h1 {
font-size: 26px;
line-height: 1.846153846;
}
.main-navigation ul.nav-menu,
.main-navigation div.nav-menu > ul {
border-bottom: 1px solid #ededed;
border-top: 1px solid #ededed;
display: inline-block !important;
text-align: left;
width: 100%;
}
.main-navigation ul {
margin: 0;
text-indent: 0;
}
.main-navigation li a,
.main-navigation li {
display: inline-block;
text-decoration: none;
}
.ie7 .main-navigation li a,
.ie7 .main-navigation li {
display: inline;
}
.main-navigation li a {
border-bottom: 0;
color: #6a6a6a;
line-height: 3.692307692;
text-transform: uppercase;
}
.main-navigation li a:hover {
color: #000;
}
.main-navigation li {
margin: 0 40px 0 0;
position: relative;
}
.main-navigation li ul {
margin: 0;
padding: 0;
position: absolute;
top: 100%;
z-index: 1;
height: 1px;
width: 1px;
overflow: hidden;
clip: rect(1px, 1px, 1px, 1px);
}
.ie7 .main-navigation li ul {
clip: inherit;
display: none;
left: 0;
overflow: visible;
}
.main-navigation li ul ul,
.ie7 .main-navigation li ul ul {
top: 0;
left: 100%;
}
.main-navigation ul li:hover > ul,
.main-navigation ul li:focus > ul,
.main-navigation .focus > ul {
border-left: 0;
clip: inherit;
overflow: inherit;
height: inherit;
width: inherit;
}
.ie7 .main-navigation ul li:hover > ul,
.ie7 .main-navigation ul li:focus > ul {
display: block;
}
.main-navigation li ul li a {
background: #efefef;
border-bottom: 1px solid #ededed;
display: block;
font-size: 11px;
line-height: 2.181818182;
padding: 8px 10px;
width: 180px;
}
.main-navigation li ul li a:hover {
background: #e3e3e3;
color: #444;
}
.main-navigation .current-menu-item > a,
.main-navigation .current-menu-ancestor > a,
.main-navigation .current_page_item > a,
.main-navigation .current_page_ancestor > a {
color: #636363;
font-weight: bold;
}
.main-navigation .menu-toggle {
display: none;
}
.entry-header .entry-title {
font-size: 22px;
}
#respond form input[type="text"] {
width: 46.333333333%;
}
#respond form textarea.blog-textarea {
width: 79.666666667%;
}
.template-front-page .site-content,
.template-front-page article {
overflow: hidden;
}
.template-front-page.has-post-thumbnail article {
float: left;
width: 47.916666667%;
}
.entry-page-image {
float: right;
margin-bottom: 0;
width: 47.916666667%;
}
/* IE Front Page Template Widget fix */
.template-front-page .widget-area {
clear: both;
}
.template-front-page .widget {
width: 100% !important;
border: none;
}
.template-front-page .widget-area .widget,
.template-front-page .first.front-widgets,
.template-front-page.two-sidebars .widget-area .front-widgets {
float: left;
margin-bottom: 24px;
width: 51.875%;
}
.template-front-page .second.front-widgets,
.template-front-page .widget-area .widget:nth-child(odd) {
clear: right;
}
.template-front-page .first.front-widgets,
.template-front-page .second.front-widgets,
.template-front-page.two-sidebars .widget-area .front-widgets + .front-widgets {
float: right;
margin: 0 0 24px;
width: 39.0625%;
}
.template-front-page.two-sidebars .widget,
.template-front-page.two-sidebars .widget:nth-child(even) {
float: none;
width: auto;
}
/* add input font for <IE9 Password Box to make the bullets show up */
input[type="password"] {
font-family: Helvetica, Arial, sans-serif;
}
/* RTL overrides for IE7 and IE8
-------------------------------------------------------------- */
.rtl .site-header h1,
.rtl .site-header h2 {
text-align: right;
}
.rtl .widget-area,
.rtl .author-description {
float: left;
}
.rtl .author-avatar,
.rtl .site-content {
float: right;
}
.rtl .main-navigation ul.nav-menu,
.rtl .main-navigation div.nav-menu > ul {
text-align: right;
}
.rtl .main-navigation ul li ul li,
.rtl .main-navigation ul li ul li ul li {
margin-left: 40px;
margin-right: auto;
}
.rtl .main-navigation li ul ul {
position: absolute;
bottom: 0;
right: 100%;
z-index: 1;
}
.ie7 .rtl .main-navigation li ul ul {
position: absolute;
bottom: 0;
right: 100%;
z-index: 1;
}
.ie7 .rtl .main-navigation ul li {
z-index: 99;
}
.ie7 .rtl .main-navigation li ul {
position: absolute;
bottom: 100%;
right: 0;
z-index: 1;
}
.ie7 .rtl .main-navigation li {
margin-right: auto;
margin-left: 40px;
}
.ie7 .rtl .main-navigation li ul ul ul {
position: relative;
z-index: 1;
}

@ -1,74 +0,0 @@
/* Make clicks pass-through */
#nprogress {
pointer-events: none;
}
#nprogress .bar {
background: red;
position: fixed;
z-index: 1031;
top: 0;
left: 0;
width: 100%;
height: 2px;
}
/* Fancy blur effect */
#nprogress .peg {
display: block;
position: absolute;
right: 0px;
width: 100px;
height: 100%;
box-shadow: 0 0 10px #29d, 0 0 5px #29d;
opacity: 1.0;
-webkit-transform: rotate(3deg) translate(0px, -4px);
-ms-transform: rotate(3deg) translate(0px, -4px);
transform: rotate(3deg) translate(0px, -4px);
}
/* Remove these to get rid of the spinner */
#nprogress .spinner {
display: block;
position: fixed;
z-index: 1031;
top: 15px;
right: 15px;
}
#nprogress .spinner-icon {
width: 18px;
height: 18px;
box-sizing: border-box;
border: solid 2px transparent;
border-top-color: red;
border-left-color: red;
border-radius: 50%;
-webkit-animation: nprogress-spinner 400ms linear infinite;
animation: nprogress-spinner 400ms linear infinite;
}
.nprogress-custom-parent {
overflow: hidden;
position: relative;
}
.nprogress-custom-parent #nprogress .spinner,
.nprogress-custom-parent #nprogress .bar {
position: absolute;
}
@-webkit-keyframes nprogress-spinner {
0% { -webkit-transform: rotate(0deg); }
100% { -webkit-transform: rotate(360deg); }
}
@keyframes nprogress-spinner {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}

@ -1,305 +0,0 @@
.icon-sn-google {
background-position: 0 -28px;
}
.icon-sn-bg-google {
background-color: #4285f4;
background-position: 0 0;
}
.fa-sn-google {
color: #4285f4;
}
.icon-sn-github {
background-position: -28px -28px;
}
.icon-sn-bg-github {
background-color: #333;
background-position: -28px 0;
}
.fa-sn-github {
color: #333;
}
.icon-sn-weibo {
background-position: -56px -28px;
}
.icon-sn-bg-weibo {
background-color: #e90d24;
background-position: -56px 0;
}
.fa-sn-weibo {
color: #e90d24;
}
.icon-sn-qq {
background-position: -84px -28px;
}
.icon-sn-bg-qq {
background-color: #0098e6;
background-position: -84px 0;
}
.fa-sn-qq {
color: #0098e6;
}
.icon-sn-twitter {
background-position: -112px -28px;
}
.icon-sn-bg-twitter {
background-color: #50abf1;
background-position: -112px 0;
}
.fa-sn-twitter {
color: #50abf1;
}
.icon-sn-facebook {
background-position: -140px -28px;
}
.icon-sn-bg-facebook {
background-color: #4862a3;
background-position: -140px 0;
}
.fa-sn-facebook {
color: #4862a3;
}
.icon-sn-renren {
background-position: -168px -28px;
}
.icon-sn-bg-renren {
background-color: #197bc8;
background-position: -168px 0;
}
.fa-sn-renren {
color: #197bc8;
}
.icon-sn-tqq {
background-position: -196px -28px;
}
.icon-sn-bg-tqq {
background-color: #1f9ed2;
background-position: -196px 0;
}
.fa-sn-tqq {
color: #1f9ed2;
}
.icon-sn-douban {
background-position: -224px -28px;
}
.icon-sn-bg-douban {
background-color: #279738;
background-position: -224px 0;
}
.fa-sn-douban {
color: #279738;
}
.icon-sn-weixin {
background-position: -252px -28px;
}
.icon-sn-bg-weixin {
background-color: #00b500;
background-position: -252px 0;
}
.fa-sn-weixin {
color: #00b500;
}
.icon-sn-dotted {
background-position: -280px -28px;
}
.icon-sn-bg-dotted {
background-color: #eee;
background-position: -280px 0;
}
.fa-sn-dotted {
color: #eee;
}
.icon-sn-site {
background-position: -308px -28px;
}
.icon-sn-bg-site {
background-color: #00b500;
background-position: -308px 0;
}
.fa-sn-site {
color: #00b500;
}
.icon-sn-linkedin {
background-position: -336px -28px;
}
.icon-sn-bg-linkedin {
background-color: #0077b9;
background-position: -336px 0;
}
.fa-sn-linkedin {
color: #0077b9;
}
[class*=icon-sn-] {
display: inline-block;
background-image: url('../img/icon-sn.svg');
background-repeat: no-repeat;
width: 28px;
height: 28px;
vertical-align: middle;
background-size: auto 56px;
}
[class*=icon-sn-]:hover {
opacity: .8;
filter: alpha(opacity=80);
}
.btn-sn-google {
background: #4285f4;
}
.btn-sn-google:active, .btn-sn-google:focus, .btn-sn-google:hover {
background: #2a75f3;
}
.btn-sn-github {
background: #333;
}
.btn-sn-github:active, .btn-sn-github:focus, .btn-sn-github:hover {
background: #262626;
}
.btn-sn-weibo {
background: #e90d24;
}
.btn-sn-weibo:active, .btn-sn-weibo:focus, .btn-sn-weibo:hover {
background: #d10c20;
}
.btn-sn-qq {
background: #0098e6;
}
.btn-sn-qq:active, .btn-sn-qq:focus, .btn-sn-qq:hover {
background: #0087cd;
}
.btn-sn-twitter {
background: #50abf1;
}
.btn-sn-twitter:active, .btn-sn-twitter:focus, .btn-sn-twitter:hover {
background: #38a0ef;
}
.btn-sn-facebook {
background: #4862a3;
}
.btn-sn-facebook:active, .btn-sn-facebook:focus, .btn-sn-facebook:hover {
background: #405791;
}
.btn-sn-renren {
background: #197bc8;
}
.btn-sn-renren:active, .btn-sn-renren:focus, .btn-sn-renren:hover {
background: #166db1;
}
.btn-sn-tqq {
background: #1f9ed2;
}
.btn-sn-tqq:active, .btn-sn-tqq:focus, .btn-sn-tqq:hover {
background: #1c8dbc;
}
.btn-sn-douban {
background: #279738;
}
.btn-sn-douban:active, .btn-sn-douban:focus, .btn-sn-douban:hover {
background: #228330;
}
.btn-sn-weixin {
background: #00b500;
}
.btn-sn-weixin:active, .btn-sn-weixin:focus, .btn-sn-weixin:hover {
background: #009c00;
}
.btn-sn-dotted {
background: #eee;
}
.btn-sn-dotted:active, .btn-sn-dotted:focus, .btn-sn-dotted:hover {
background: #e1e1e1;
}
.btn-sn-site {
background: #00b500;
}
.btn-sn-site:active, .btn-sn-site:focus, .btn-sn-site:hover {
background: #009c00;
}
.btn-sn-linkedin {
background: #0077b9;
}
.btn-sn-linkedin:active, .btn-sn-linkedin:focus, .btn-sn-linkedin:hover {
background: #0067a0;
}
[class*=btn-sn-], [class*=btn-sn-]:active, [class*=btn-sn-]:focus, [class*=btn-sn-]:hover {
border: none;
color: #fff;
}
.btn-sn-more {
padding: 0;
}
.btn-sn-more, .btn-sn-more:active, .btn-sn-more:hover {
box-shadow: none;
}
[class*=btn-sn-] [class*=icon-sn-] {
background-color: transparent;
}

File diff suppressed because one or more lines are too long

@ -1,378 +0,0 @@
/* cyrillic-ext */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 300;
font-display: fallback;
src: url(memnYaGs126MiZpBA-UFUKWyV9hmIqOjjg.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 300;
font-display: fallback;
src: url(memnYaGs126MiZpBA-UFUKWyV9hvIqOjjg.woff2) format('woff2');
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 300;
font-display: fallback;
src: url(memnYaGs126MiZpBA-UFUKWyV9hnIqOjjg.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 300;
font-display: fallback;
src: url(memnYaGs126MiZpBA-UFUKWyV9hoIqOjjg.woff2) format('woff2');
unicode-range: U+0370-03FF;
}
/* vietnamese */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 300;
font-display: fallback;
src: url(memnYaGs126MiZpBA-UFUKWyV9hkIqOjjg.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 300;
font-display: fallback;
src: url(memnYaGs126MiZpBA-UFUKWyV9hlIqOjjg.woff2) format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 300;
font-display: fallback;
src: url(memnYaGs126MiZpBA-UFUKWyV9hrIqM.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 400;
font-display: fallback;
src: url(mem6YaGs126MiZpBA-UFUK0Udc1UAw.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 400;
font-display: fallback;
src: url(mem6YaGs126MiZpBA-UFUK0ddc1UAw.woff2) format('woff2');
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 400;
font-display: fallback;
src: url(mem6YaGs126MiZpBA-UFUK0Vdc1UAw.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 400;
font-display: fallback;
src: url(mem6YaGs126MiZpBA-UFUK0adc1UAw.woff2) format('woff2');
unicode-range: U+0370-03FF;
}
/* vietnamese */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 400;
font-display: fallback;
src: url(mem6YaGs126MiZpBA-UFUK0Wdc1UAw.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 400;
font-display: fallback;
src: url(mem6YaGs126MiZpBA-UFUK0Xdc1UAw.woff2) format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 400;
font-display: fallback;
src: url(mem6YaGs126MiZpBA-UFUK0Zdc0.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 600;
font-display: fallback;
src: url(memnYaGs126MiZpBA-UFUKXGUdhmIqOjjg.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 600;
font-display: fallback;
src: url(memnYaGs126MiZpBA-UFUKXGUdhvIqOjjg.woff2) format('woff2');
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 600;
font-display: fallback;
src: url(memnYaGs126MiZpBA-UFUKXGUdhnIqOjjg.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 600;
font-display: fallback;
src: url(memnYaGs126MiZpBA-UFUKXGUdhoIqOjjg.woff2) format('woff2');
unicode-range: U+0370-03FF;
}
/* vietnamese */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 600;
font-display: fallback;
src: url(memnYaGs126MiZpBA-UFUKXGUdhkIqOjjg.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 600;
font-display: fallback;
src: url(memnYaGs126MiZpBA-UFUKXGUdhlIqOjjg.woff2) format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 600;
font-display: fallback;
src: url(memnYaGs126MiZpBA-UFUKXGUdhrIqM.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 300;
font-display: fallback;
src: url(mem5YaGs126MiZpBA-UN_r8OX-hpOqc.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 300;
font-display: fallback;
src: url(mem5YaGs126MiZpBA-UN_r8OVuhpOqc.woff2) format('woff2');
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 300;
font-display: fallback;
src: url(mem5YaGs126MiZpBA-UN_r8OXuhpOqc.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 300;
font-display: fallback;
src: url(mem5YaGs126MiZpBA-UN_r8OUehpOqc.woff2) format('woff2');
unicode-range: U+0370-03FF;
}
/* vietnamese */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 300;
font-display: fallback;
src: url(mem5YaGs126MiZpBA-UN_r8OXehpOqc.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 300;
font-display: fallback;
src: url(mem5YaGs126MiZpBA-UN_r8OXOhpOqc.woff2) format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 300;
font-display: fallback;
src: url(mem5YaGs126MiZpBA-UN_r8OUuhp.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
font-display: fallback;
src: url(mem8YaGs126MiZpBA-UFWJ0bbck.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
font-display: fallback;
src: url(mem8YaGs126MiZpBA-UFUZ0bbck.woff2) format('woff2');
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
font-display: fallback;
src: url(mem8YaGs126MiZpBA-UFWZ0bbck.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
font-display: fallback;
src: url(mem8YaGs126MiZpBA-UFVp0bbck.woff2) format('woff2');
unicode-range: U+0370-03FF;
}
/* vietnamese */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
font-display: fallback;
src: url(mem8YaGs126MiZpBA-UFWp0bbck.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
font-display: fallback;
src: url(mem8YaGs126MiZpBA-UFW50bbck.woff2) format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
font-display: fallback;
src: url(mem8YaGs126MiZpBA-UFVZ0b.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 600;
font-display: fallback;
src: url(mem5YaGs126MiZpBA-UNirkOX-hpOqc.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 600;
font-display: fallback;
src: url(mem5YaGs126MiZpBA-UNirkOVuhpOqc.woff2) format('woff2');
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 600;
font-display: fallback;
src: url(mem5YaGs126MiZpBA-UNirkOXuhpOqc.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 600;
font-display: fallback;
src: url(mem5YaGs126MiZpBA-UNirkOUehpOqc.woff2) format('woff2');
unicode-range: U+0370-03FF;
}
/* vietnamese */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 600;
font-display: fallback;
src: url(mem5YaGs126MiZpBA-UNirkOXehpOqc.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 600;
font-display: fallback;
src: url(mem5YaGs126MiZpBA-UNirkOXOhpOqc.woff2) format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 600;
font-display: fallback;
src: url(mem5YaGs126MiZpBA-UNirkOUuhp.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 25 KiB

@ -1,91 +0,0 @@
/**
* Created by liangliang on 2016/11/20.
*/
function do_reply(parentid) {
console.log(parentid);
$("#id_parent_comment_id").val(parentid)
$("#commentform").appendTo($("#div-comment-" + parentid));
$("#reply-title").hide();
$("#cancel_comment").show();
}
function cancel_reply() {
$("#reply-title").show();
$("#cancel_comment").hide();
$("#id_parent_comment_id").val('')
$("#commentform").appendTo($("#respond"));
}
NProgress.start();
NProgress.set(0.4);
//Increment
var interval = setInterval(function () {
NProgress.inc();
}, 1000);
$(document).ready(function () {
NProgress.done();
clearInterval(interval);
});
/** 侧边栏回到顶部 */
var rocket = $('#rocket');
$(window).on('scroll', debounce(slideTopSet, 300));
function debounce(func, wait) {
var timeout;
return function () {
clearTimeout(timeout);
timeout = setTimeout(func, wait);
};
}
function slideTopSet() {
var top = $(document).scrollTop();
if (top > 200) {
rocket.addClass('show');
} else {
rocket.removeClass('show');
}
}
$(document).on('click', '#rocket', function (event) {
rocket.addClass('move');
$('body, html').animate({
scrollTop: 0
}, 800);
});
$(document).on('animationEnd', function () {
setTimeout(function () {
rocket.removeClass('move');
}, 400);
});
$(document).on('webkitAnimationEnd', function () {
setTimeout(function () {
rocket.removeClass('move');
}, 400);
});
window.onload = function () {
var replyLinks = document.querySelectorAll(".comment-reply-link");
for (var i = 0; i < replyLinks.length; i++) {
replyLinks[i].onclick = function () {
var pk = this.getAttribute("data-pk");
do_reply(pk);
};
}
};
// $(document).ready(function () {
// var form = $('#i18n-form');
// var selector = $('.i18n-select');
// selector.on('change', function () {
// form.submit();
// });
// });

@ -1,8 +0,0 @@
/*
HTML5 Shiv v3.7.0 | @afarkas @jdalton @jon_neal @rem | MIT/GPL2 Licensed
*/
(function(l,f){function m(){var a=e.elements;return"string"==typeof a?a.split(" "):a}function i(a){var b=n[a[o]];b||(b={},h++,a[o]=h,n[h]=b);return b}function p(a,b,c){b||(b=f);if(g)return b.createElement(a);c||(c=i(b));b=c.cache[a]?c.cache[a].cloneNode():r.test(a)?(c.cache[a]=c.createElem(a)).cloneNode():c.createElem(a);return b.canHaveChildren&&!s.test(a)?c.frag.appendChild(b):b}function t(a,b){if(!b.cache)b.cache={},b.createElem=a.createElement,b.createFrag=a.createDocumentFragment,b.frag=b.createFrag();
a.createElement=function(c){return!e.shivMethods?b.createElem(c):p(c,a,b)};a.createDocumentFragment=Function("h,f","return function(){var n=f.cloneNode(),c=n.createElement;h.shivMethods&&("+m().join().replace(/[\w\-]+/g,function(a){b.createElem(a);b.frag.createElement(a);return'c("'+a+'")'})+");return n}")(e,b.frag)}function q(a){a||(a=f);var b=i(a);if(e.shivCSS&&!j&&!b.hasCSS){var c,d=a;c=d.createElement("p");d=d.getElementsByTagName("head")[0]||d.documentElement;c.innerHTML="x<style>article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}mark{background:#FF0;color:#000}template{display:none}</style>";
c=d.insertBefore(c.lastChild,d.firstChild);b.hasCSS=!!c}g||t(a,b);return a}var k=l.html5||{},s=/^<|^(?:button|map|select|textarea|object|iframe|option|optgroup)$/i,r=/^(?:a|b|code|div|fieldset|h1|h2|h3|h4|h5|h6|i|label|li|ol|p|q|span|strong|style|table|tbody|td|th|tr|ul)$/i,j,o="_html5shiv",h=0,n={},g;(function(){try{var a=f.createElement("a");a.innerHTML="<xyz></xyz>";j="hidden"in a;var b;if(!(b=1==a.childNodes.length)){f.createElement("a");var c=f.createDocumentFragment();b="undefined"==typeof c.cloneNode||
"undefined"==typeof c.createDocumentFragment||"undefined"==typeof c.createElement}g=b}catch(d){g=j=!0}})();var e={elements:k.elements||"abbr article aside audio bdi canvas data datalist details dialog figcaption figure footer header hgroup main mark meter nav output progress section summary template time video",version:"3.7.0",shivCSS:!1!==k.shivCSS,supportsUnknownElements:g,shivMethods:!1!==k.shivMethods,type:"default",shivDocument:q,createElement:p,createDocumentFragment:function(a,b){a||(a=f);
if(g)return a.createDocumentFragment();for(var b=b||i(a),c=b.frag.cloneNode(),d=0,e=m(),h=e.length;d<h;d++)c.createElement(e[d]);return c}};l.html5=e;q(f)})(this,document);

File diff suppressed because one or more lines are too long

@ -1,55 +0,0 @@
/**
* Handles toggling the navigation menu for small screens and
* accessibility for submenu items.
*/
( function() {
var nav = document.getElementById( 'site-navigation' ), button, menu;
if ( ! nav ) {
return;
}
button = nav.getElementsByTagName( 'button' )[0];
menu = nav.getElementsByTagName( 'ul' )[0];
if ( ! button ) {
return;
}
// Hide button if menu is missing or empty.
if ( ! menu || ! menu.childNodes.length ) {
button.style.display = 'none';
return;
}
button.onclick = function() {
if ( -1 === menu.className.indexOf( 'nav-menu' ) ) {
menu.className = 'nav-menu';
}
if ( -1 !== button.className.indexOf( 'toggled-on' ) ) {
button.className = button.className.replace( ' toggled-on', '' );
menu.className = menu.className.replace( ' toggled-on', '' );
} else {
button.className += ' toggled-on';
menu.className += ' toggled-on';
}
};
} )();
// Better focus for hidden submenu items for accessibility.
( function( $ ) {
$( '.main-navigation' ).find( 'a' ).on( 'focus.twentytwelve blur.twentytwelve', function() {
$( this ).parents( '.menu-item, .page_item' ).toggleClass( 'focus' );
} );
if ( 'ontouchstart' in window ) {
$('body').on( 'touchstart.twentytwelve', '.menu-item-has-children > a, .page_item_has_children > a', function( e ) {
var el = $( this ).parent( 'li' );
if ( ! el.hasClass( 'focus' ) ) {
e.preventDefault();
el.toggleClass( 'focus' );
el.siblings( '.focus').removeClass( 'focus' );
}
} );
}
} )( jQuery );

@ -1,480 +0,0 @@
/* NProgress, (c) 2013, 2014 Rico Sta. Cruz - http://ricostacruz.com/nprogress
* @license MIT */
;(function(root, factory) {
if (typeof define === 'function' && define.amd) {
define(factory);
} else if (typeof exports === 'object') {
module.exports = factory();
} else {
root.NProgress = factory();
}
})(this, function() {
var NProgress = {};
NProgress.version = '0.2.0';
var Settings = NProgress.settings = {
minimum: 0.08,
easing: 'linear',
positionUsing: '',
speed: 200,
trickle: true,
trickleSpeed: 200,
showSpinner: true,
barSelector: '[role="bar"]',
spinnerSelector: '[role="spinner"]',
parent: 'body',
template: '<div class="bar" role="bar"><div class="peg"></div></div><div class="spinner" role="spinner"><div class="spinner-icon"></div></div>'
};
/**
* Updates configuration.
*
* NProgress.configure({
* minimum: 0.1
* });
*/
NProgress.configure = function(options) {
var key, value;
for (key in options) {
value = options[key];
if (value !== undefined && options.hasOwnProperty(key)) Settings[key] = value;
}
return this;
};
/**
* Last number.
*/
NProgress.status = null;
/**
* Sets the progress bar status, where `n` is a number from `0.0` to `1.0`.
*
* NProgress.set(0.4);
* NProgress.set(1.0);
*/
NProgress.set = function(n) {
var started = NProgress.isStarted();
n = clamp(n, Settings.minimum, 1);
NProgress.status = (n === 1 ? null : n);
var progress = NProgress.render(!started),
bar = progress.querySelector(Settings.barSelector),
speed = Settings.speed,
ease = Settings.easing;
progress.offsetWidth; /* Repaint */
queue(function(next) {
// Set positionUsing if it hasn't already been set
if (Settings.positionUsing === '') Settings.positionUsing = NProgress.getPositioningCSS();
// Add transition
css(bar, barPositionCSS(n, speed, ease));
if (n === 1) {
// Fade out
css(progress, {
transition: 'none',
opacity: 1
});
progress.offsetWidth; /* Repaint */
setTimeout(function() {
css(progress, {
transition: 'all ' + speed + 'ms linear',
opacity: 0
});
setTimeout(function() {
NProgress.remove();
next();
}, speed);
}, speed);
} else {
setTimeout(next, speed);
}
});
return this;
};
NProgress.isStarted = function() {
return typeof NProgress.status === 'number';
};
/**
* Shows the progress bar.
* This is the same as setting the status to 0%, except that it doesn't go backwards.
*
* NProgress.start();
*
*/
NProgress.start = function() {
if (!NProgress.status) NProgress.set(0);
var work = function() {
setTimeout(function() {
if (!NProgress.status) return;
NProgress.trickle();
work();
}, Settings.trickleSpeed);
};
if (Settings.trickle) work();
return this;
};
/**
* Hides the progress bar.
* This is the *sort of* the same as setting the status to 100%, with the
* difference being `done()` makes some placebo effect of some realistic motion.
*
* NProgress.done();
*
* If `true` is passed, it will show the progress bar even if its hidden.
*
* NProgress.done(true);
*/
NProgress.done = function(force) {
if (!force && !NProgress.status) return this;
return NProgress.inc(0.3 + 0.5 * Math.random()).set(1);
};
/**
* Increments by a random amount.
*/
NProgress.inc = function(amount) {
var n = NProgress.status;
if (!n) {
return NProgress.start();
} else if(n > 1) {
} else {
if (typeof amount !== 'number') {
if (n >= 0 && n < 0.2) { amount = 0.1; }
else if (n >= 0.2 && n < 0.5) { amount = 0.04; }
else if (n >= 0.5 && n < 0.8) { amount = 0.02; }
else if (n >= 0.8 && n < 0.99) { amount = 0.005; }
else { amount = 0; }
}
n = clamp(n + amount, 0, 0.994);
return NProgress.set(n);
}
};
NProgress.trickle = function() {
return NProgress.inc();
};
/**
* Waits for all supplied jQuery promises and
* increases the progress as the promises resolve.
*
* @param $promise jQUery Promise
*/
(function() {
var initial = 0, current = 0;
NProgress.promise = function($promise) {
if (!$promise || $promise.state() === "resolved") {
return this;
}
if (current === 0) {
NProgress.start();
}
initial++;
current++;
$promise.always(function() {
current--;
if (current === 0) {
initial = 0;
NProgress.done();
} else {
NProgress.set((initial - current) / initial);
}
});
return this;
};
})();
/**
* (Internal) renders the progress bar markup based on the `template`
* setting.
*/
NProgress.render = function(fromStart) {
if (NProgress.isRendered()) return document.getElementById('nprogress');
addClass(document.documentElement, 'nprogress-busy');
var progress = document.createElement('div');
progress.id = 'nprogress';
progress.innerHTML = Settings.template;
var bar = progress.querySelector(Settings.barSelector),
perc = fromStart ? '-100' : toBarPerc(NProgress.status || 0),
parent = document.querySelector(Settings.parent),
spinner;
css(bar, {
transition: 'all 0 linear',
transform: 'translate3d(' + perc + '%,0,0)'
});
if (!Settings.showSpinner) {
spinner = progress.querySelector(Settings.spinnerSelector);
spinner && removeElement(spinner);
}
if (parent != document.body) {
addClass(parent, 'nprogress-custom-parent');
}
parent.appendChild(progress);
return progress;
};
/**
* Removes the element. Opposite of render().
*/
NProgress.remove = function() {
removeClass(document.documentElement, 'nprogress-busy');
removeClass(document.querySelector(Settings.parent), 'nprogress-custom-parent');
var progress = document.getElementById('nprogress');
progress && removeElement(progress);
};
/**
* Checks if the progress bar is rendered.
*/
NProgress.isRendered = function() {
return !!document.getElementById('nprogress');
};
/**
* Determine which positioning CSS rule to use.
*/
NProgress.getPositioningCSS = function() {
// Sniff on document.body.style
var bodyStyle = document.body.style;
// Sniff prefixes
var vendorPrefix = ('WebkitTransform' in bodyStyle) ? 'Webkit' :
('MozTransform' in bodyStyle) ? 'Moz' :
('msTransform' in bodyStyle) ? 'ms' :
('OTransform' in bodyStyle) ? 'O' : '';
if (vendorPrefix + 'Perspective' in bodyStyle) {
// Modern browsers with 3D support, e.g. Webkit, IE10
return 'translate3d';
} else if (vendorPrefix + 'Transform' in bodyStyle) {
// Browsers without 3D support, e.g. IE9
return 'translate';
} else {
// Browsers without translate() support, e.g. IE7-8
return 'margin';
}
};
/**
* Helpers
*/
function clamp(n, min, max) {
if (n < min) return min;
if (n > max) return max;
return n;
}
/**
* (Internal) converts a percentage (`0..1`) to a bar translateX
* percentage (`-100%..0%`).
*/
function toBarPerc(n) {
return (-1 + n) * 100;
}
/**
* (Internal) returns the correct CSS for changing the bar's
* position given an n percentage, and speed and ease from Settings
*/
function barPositionCSS(n, speed, ease) {
var barCSS;
if (Settings.positionUsing === 'translate3d') {
barCSS = { transform: 'translate3d('+toBarPerc(n)+'%,0,0)' };
} else if (Settings.positionUsing === 'translate') {
barCSS = { transform: 'translate('+toBarPerc(n)+'%,0)' };
} else {
barCSS = { 'margin-left': toBarPerc(n)+'%' };
}
barCSS.transition = 'all '+speed+'ms '+ease;
return barCSS;
}
/**
* (Internal) Queues a function to be executed.
*/
var queue = (function() {
var pending = [];
function next() {
var fn = pending.shift();
if (fn) {
fn(next);
}
}
return function(fn) {
pending.push(fn);
if (pending.length == 1) next();
};
})();
/**
* (Internal) Applies css properties to an element, similar to the jQuery
* css method.
*
* While this helper does assist with vendor prefixed property names, it
* does not perform any manipulation of values prior to setting styles.
*/
var css = (function() {
var cssPrefixes = [ 'Webkit', 'O', 'Moz', 'ms' ],
cssProps = {};
function camelCase(string) {
return string.replace(/^-ms-/, 'ms-').replace(/-([\da-z])/gi, function(match, letter) {
return letter.toUpperCase();
});
}
function getVendorProp(name) {
var style = document.body.style;
if (name in style) return name;
var i = cssPrefixes.length,
capName = name.charAt(0).toUpperCase() + name.slice(1),
vendorName;
while (i--) {
vendorName = cssPrefixes[i] + capName;
if (vendorName in style) return vendorName;
}
return name;
}
function getStyleProp(name) {
name = camelCase(name);
return cssProps[name] || (cssProps[name] = getVendorProp(name));
}
function applyCss(element, prop, value) {
prop = getStyleProp(prop);
element.style[prop] = value;
}
return function(element, properties) {
var args = arguments,
prop,
value;
if (args.length == 2) {
for (prop in properties) {
value = properties[prop];
if (value !== undefined && properties.hasOwnProperty(prop)) applyCss(element, prop, value);
}
} else {
applyCss(element, args[1], args[2]);
}
}
})();
/**
* (Internal) Determines if an element or space separated list of class names contains a class name.
*/
function hasClass(element, name) {
var list = typeof element == 'string' ? element : classList(element);
return list.indexOf(' ' + name + ' ') >= 0;
}
/**
* (Internal) Adds a class to an element.
*/
function addClass(element, name) {
var oldList = classList(element),
newList = oldList + name;
if (hasClass(oldList, name)) return;
// Trim the opening space.
element.className = newList.substring(1);
}
/**
* (Internal) Removes a class from an element.
*/
function removeClass(element, name) {
var oldList = classList(element),
newList;
if (!hasClass(element, name)) return;
// Replace the class name.
newList = oldList.replace(' ' + name + ' ', ' ');
// Trim the opening and closing spaces.
element.className = newList.substring(1, newList.length - 1);
}
/**
* (Internal) Gets a space separated list of the class names on the element.
* The list is wrapped with a single space on each end to facilitate finding
* matches within the list.
*/
function classList(element) {
return (' ' + (element && element.className || '') + ' ').replace(/\s+/gi, ' ');
}
/**
* (Internal) Removes an element from the DOM.
*/
function removeElement(element) {
element && element.parentNode && element.parentNode.removeChild(element);
}
return NProgress;
});

@ -1,21 +0,0 @@
$(function () {
MathJax.Hub.Config({
showProcessingMessages: false, //关闭js加载过程信息
messageStyle: "none", //不显示信息
extensions: ["tex2jax.js"], jax: ["input/TeX", "output/HTML-CSS"], displayAlign: "left", tex2jax: {
inlineMath: [["$", "$"]], //行内公式选择$
displayMath: [["$$", "$$"]], //段内公式选择$$
skipTags: ['script', 'noscript', 'style', 'textarea', 'pre', 'code', 'a'], //避开某些标签
}, "HTML-CSS": {
availableFonts: ["STIX", "TeX"], //可选字体
showMathMenu: false //关闭右击菜单显示
}
});
// 识别范围 => 文章内容、评论内容标签
const contentId = document.getElementById("content");
const commentId = document.getElementById("comments");
MathJax.Hub.Queue(["Typeset", MathJax.Hub, contentId, commentId]);
})

@ -1,293 +0,0 @@
.codehilite .hll {
background-color: #ffffcc
}
.codehilite {
background: #ffffff;
}
.codehilite .c {
color: #177500
}
/* Comment */
.codehilite .err {
color: #000000
}
/* Error */
.codehilite .k {
color: #A90D91
}
/* Keyword */
.codehilite .l {
color: #1C01CE
}
/* Literal */
.codehilite .n {
color: #000000
}
/* Name */
.codehilite .o {
color: #000000
}
/* Operator */
.codehilite .ch {
color: #177500
}
/* Comment.Hashbang */
.codehilite .cm {
color: #177500
}
/* Comment.Multiline */
.codehilite .cp {
color: #633820
}
/* Comment.Preproc */
.codehilite .cpf {
color: #177500
}
/* Comment.PreprocFile */
.codehilite .c1 {
color: #177500
}
/* Comment.Single */
.codehilite .cs {
color: #177500
}
/* Comment.Special */
.codehilite .kc {
color: #A90D91
}
/* Keyword.Constant */
.codehilite .kd {
color: #A90D91
}
/* Keyword.Declaration */
.codehilite .kn {
color: #A90D91
}
/* Keyword.Namespace */
.codehilite .kp {
color: #A90D91
}
/* Keyword.Pseudo */
.codehilite .kr {
color: #A90D91
}
/* Keyword.Reserved */
.codehilite .kt {
color: #A90D91
}
/* Keyword.Type */
.codehilite .ld {
color: #1C01CE
}
/* Literal.Date */
.codehilite .m {
color: #1C01CE
}
/* Literal.Number */
.codehilite .s {
color: #C41A16
}
/* Literal.String */
.codehilite .na {
color: #836C28
}
/* Name.Attribute */
.codehilite .nb {
color: #A90D91
}
/* Name.Builtin */
.codehilite .nc {
color: #3F6E75
}
/* Name.Class */
.codehilite .no {
color: #000000
}
/* Name.Constant */
.codehilite .nd {
color: #000000
}
/* Name.Decorator */
.codehilite .ni {
color: #000000
}
/* Name.Entity */
.codehilite .ne {
color: #000000
}
/* Name.Exception */
.codehilite .nf {
color: #000000
}
/* Name.Function */
.codehilite .nl {
color: #000000
}
/* Name.Label */
.codehilite .nn {
color: #000000
}
/* Name.Namespace */
.codehilite .nx {
color: #000000
}
/* Name.Other */
.codehilite .py {
color: #000000
}
/* Name.Property */
.codehilite .nt {
color: #000000
}
/* Name.Tag */
.codehilite .nv {
color: #000000
}
/* Name.Variable */
.codehilite .ow {
color: #000000
}
/* Operator.Word */
.codehilite .mb {
color: #1C01CE
}
/* Literal.Number.Bin */
.codehilite .mf {
color: #1C01CE
}
/* Literal.Number.Float */
.codehilite .mh {
color: #1C01CE
}
/* Literal.Number.Hex */
.codehilite .mi {
color: #1C01CE
}
/* Literal.Number.Integer */
.codehilite .mo {
color: #1C01CE
}
/* Literal.Number.Oct */
.codehilite .sb {
color: #C41A16
}
/* Literal.String.Backtick */
.codehilite .sc {
color: #2300CE
}
/* Literal.String.Char */
.codehilite .sd {
color: #C41A16
}
/* Literal.String.Doc */
.codehilite .s2 {
color: #C41A16
}
/* Literal.String.Double */
.codehilite .se {
color: #C41A16
}
/* Literal.String.Escape */
.codehilite .sh {
color: #C41A16
}
/* Literal.String.Heredoc */
.codehilite .si {
color: #C41A16
}
/* Literal.String.Interpol */
.codehilite .sx {
color: #C41A16
}
/* Literal.String.Other */
.codehilite .sr {
color: #C41A16
}
/* Literal.String.Regex */
.codehilite .s1 {
color: #C41A16
}
/* Literal.String.Single */
.codehilite .ss {
color: #C41A16
}
/* Literal.String.Symbol */
.codehilite .bp {
color: #5B269A
}
/* Name.Builtin.Pseudo */
.codehilite .vc {
color: #000000
}
/* Name.Variable.Class */
.codehilite .vg {
color: #000000
}
/* Name.Variable.Global */
.codehilite .vi {
color: #000000
}
/* Name.Variable.Instance */
.codehilite .il {
color: #1C01CE
}
/* Literal.Number.Integer.Long */

@ -1,346 +0,0 @@
#ly:
# coding:utf-8
import hashlib
import logging
import random
import urllib
from django import template
from django.conf import settings
from django.db.models import Q
from django.shortcuts import get_object_or_404
from django.template.defaultfilters import stringfilter
from django.templatetags.static import static
from django.urls import reverse
from django.utils.safestring import mark_safe
from blog.models import Article, Category, Tag, Links, SideBar, LinkShowType
from comments.models import Comment
from djangoblog.utils import CommonMarkdown, sanitize_html
from djangoblog.utils import cache
from djangoblog.utils import get_current_site
from oauth.models import OAuthUser
from djangoblog.plugin_manage import hooks
logger = logging.getLogger(__name__)
register = template.Library()
@register.simple_tag(takes_context=True)
def head_meta(context):
return mark_safe(hooks.apply_filters('head_meta', '', context))
@register.simple_tag
def timeformat(data):
try:
return data.strftime(settings.TIME_FORMAT)
except Exception as e:
logger.error(e)
return ""
@register.simple_tag
def datetimeformat(data):
try:
return data.strftime(settings.DATE_TIME_FORMAT)
except Exception as e:
logger.error(e)
return ""
@register.filter()
@stringfilter
def custom_markdown(content):
return mark_safe(CommonMarkdown.get_markdown(content))
@register.simple_tag
def get_markdown_toc(content):
from djangoblog.utils import CommonMarkdown
body, toc = CommonMarkdown.get_markdown_with_toc(content)
return mark_safe(toc)
@register.filter()
@stringfilter
def comment_markdown(content):
content = CommonMarkdown.get_markdown(content)
return mark_safe(sanitize_html(content))
@register.filter(is_safe=True)
@stringfilter
def truncatechars_content(content):
"""
获得文章内容的摘要
:param content:
:return:
"""
from django.template.defaultfilters import truncatechars_html
from djangoblog.utils import get_blog_setting
blogsetting = get_blog_setting()
return truncatechars_html(content, blogsetting.article_sub_length)
@register.filter(is_safe=True)
@stringfilter
def truncate(content):
from django.utils.html import strip_tags
return strip_tags(content)[:150]
@register.inclusion_tag('blog/tags/breadcrumb.html')
def load_breadcrumb(article):
"""
获得文章面包屑
:param article:
:return:
"""
names = article.get_category_tree()
from djangoblog.utils import get_blog_setting
blogsetting = get_blog_setting()
site = get_current_site().domain
names.append((blogsetting.site_name, '/'))
names = names[::-1]
return {
'names': names,
'title': article.title,
'count': len(names) + 1
}
@register.inclusion_tag('blog/tags/article_tag_list.html')
def load_articletags(article):
"""
文章标签
:param article:
:return:
"""
tags = article.tags.all()
tags_list = []
for tag in tags:
url = tag.get_absolute_url()
count = tag.get_article_count()
tags_list.append((
url, count, tag, random.choice(settings.BOOTSTRAP_COLOR_TYPES)
))
return {
'article_tags_list': tags_list
}
@register.inclusion_tag('blog/tags/sidebar.html')
def load_sidebar(user, linktype):
"""
加载侧边栏
:return:
"""
value = cache.get("sidebar" + linktype)
if value:
value['user'] = user
return value
else:
logger.info('load sidebar')
from djangoblog.utils import get_blog_setting
blogsetting = get_blog_setting()
recent_articles = Article.objects.filter(
status='p')[:blogsetting.sidebar_article_count]
sidebar_categorys = Category.objects.all()
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('creation_time', 'month', order='DESC')
links = Links.objects.filter(is_enable=True).filter(
Q(show_type=str(linktype)) | Q(show_type=LinkShowType.A))
commment_list = Comment.objects.filter(is_enable=True).order_by(
'-id')[:blogsetting.sidebar_comment_count]
# 标签云 计算字体大小
# 根据总数计算出平均值 大小为 (数目/平均值)*步长
increment = 5
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]]
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)
value = {
'recent_articles': recent_articles,
'sidebar_categorys': sidebar_categorys,
'most_read_articles': most_read_articles,
'article_dates': dates,
'sidebar_comments': commment_list,
'sidabar_links': links,
'show_google_adsense': blogsetting.show_google_adsense,
'google_adsense_codes': blogsetting.google_adsense_codes,
'open_site_comment': blogsetting.open_site_comment,
'show_gongan_code': blogsetting.show_gongan_code,
'sidebar_tags': sidebar_tags,
'extra_sidebars': extra_sidebars
}
cache.set("sidebar" + linktype, value, 60 * 60 * 60 * 3)
logger.info('set sidebar cache.key:{key}'.format(key="sidebar" + linktype))
value['user'] = user
return value
@register.inclusion_tag('blog/tags/article_meta_info.html')
def load_article_metas(article, user):
"""
获得文章meta信息
:param article:
:return:
"""
return {
'article': article,
'user': user
}
@register.inclusion_tag('blog/tags/article_pagination.html')
def load_pagination_info(page_obj, page_type, tag_name):
previous_url = ''
next_url = ''
if page_type == '':
if page_obj.has_next():
next_number = page_obj.next_page_number()
next_url = reverse('blog:index_page', kwargs={'page': next_number})
if page_obj.has_previous():
previous_number = page_obj.previous_page_number()
previous_url = reverse(
'blog:index_page', kwargs={
'page': previous_number})
if page_type == '分类标签归档':
tag = get_object_or_404(Tag, name=tag_name)
if page_obj.has_next():
next_number = page_obj.next_page_number()
next_url = reverse(
'blog:tag_detail_page',
kwargs={
'page': next_number,
'tag_name': tag.slug})
if page_obj.has_previous():
previous_number = page_obj.previous_page_number()
previous_url = reverse(
'blog:tag_detail_page',
kwargs={
'page': previous_number,
'tag_name': tag.slug})
if page_type == '作者文章归档':
if page_obj.has_next():
next_number = page_obj.next_page_number()
next_url = reverse(
'blog:author_detail_page',
kwargs={
'page': next_number,
'author_name': tag_name})
if page_obj.has_previous():
previous_number = page_obj.previous_page_number()
previous_url = reverse(
'blog:author_detail_page',
kwargs={
'page': previous_number,
'author_name': tag_name})
if page_type == '分类目录归档':
category = get_object_or_404(Category, name=tag_name)
if page_obj.has_next():
next_number = page_obj.next_page_number()
next_url = reverse(
'blog:category_detail_page',
kwargs={
'page': next_number,
'category_name': category.slug})
if page_obj.has_previous():
previous_number = page_obj.previous_page_number()
previous_url = reverse(
'blog:category_detail_page',
kwargs={
'page': previous_number,
'category_name': category.slug})
return {
'previous_url': previous_url,
'next_url': next_url,
'page_obj': page_obj
}
@register.inclusion_tag('blog/tags/article_info.html')
def load_article_detail(article, isindex, user):
"""
加载文章详情
:param article:
:param isindex:是否列表页若是列表页只显示摘要
:return:
"""
from djangoblog.utils import get_blog_setting
blogsetting = get_blog_setting()
return {
'article': article,
'isindex': isindex,
'user': user,
'open_site_comment': blogsetting.open_site_comment,
}
# return only the URL of the gravatar
# TEMPLATE USE: {{ email|gravatar_url:150 }}
@register.filter
def gravatar_url(email, size=40):
"""获得gravatar头像"""
cachekey = 'gravatat/' + email
url = cache.get(cachekey)
if url:
return url
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 = static('blog/img/avatar.png')
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)
logger.info('set gravatar cache.key:{key}'.format(key=cachekey))
return url
@register.filter
def gravatar(email, size=40):
"""获得gravatar头像"""
url = gravatar_url(email, size)
return mark_safe(
'<img src="%s" height="%d" width="%d">' %
(url, size, size))
@register.simple_tag
def query(qs, **kwargs):
""" template tag which allows queryset filtering. Usage:
{% query books author=author as mybooks %}
{% for book in mybooks %}
...
{% endfor %}
"""
return qs.filter(**kwargs)
@register.filter
def addstr(arg1, arg2):
"""concatenate arg1 & arg2"""
return str(arg1) + str(arg2)

@ -1,234 +0,0 @@
#ly:
# coding:utf-8
import os
from django.conf import settings
from django.core.files.uploadedfile import SimpleUploadedFile
from django.core.management import call_command
from django.core.paginator import Paginator
from django.templatetags.static import static
from django.test import Client, RequestFactory, TestCase
from django.urls import reverse
from django.utils import timezone
from accounts.models import BlogUser
from blog.forms import BlogSearchForm
from blog.models import Article, Category, Tag, SideBar, Links
from blog.templatetags.blog_tags import load_pagination_info, load_articletags
from djangoblog.utils import get_current_site, get_sha256
from oauth.models import OAuthUser, OAuthConfig
# Create your tests here.
class ArticleTest(TestCase):
def setUp(self):
self.client = Client()
self.factory = RequestFactory()
def test_validate_article(self):
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
user.is_superuser = True
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'
s.content = 'test content'
s.is_enable = True
s.save()
category = Category()
category.name = "category"
category.creation_time = timezone.now()
category.last_mod_time = timezone.now()
category.save()
tag = Tag()
tag.name = "nicetag"
tag.save()
article = Article()
article.title = "nicetitle"
article.body = "nicecontent"
article.author = user
article.category = category
article.type = 'a'
article.status = 'p'
article.save()
self.assertEqual(0, article.tags.count())
article.tags.add(tag)
article.save()
self.assertEqual(1, article.tags.count())
for i in range(20):
article = Article()
article.title = "nicetitle" + str(i)
article.body = "nicetitle" + str(i)
article.author = user
article.category = category
article.type = 'a'
article.status = 'p'
article.save()
article.tags.add(tag)
article.save()
from blog.documents import ELASTICSEARCH_ENABLED
if ELASTICSEARCH_ENABLED:
call_command("build_index")
response = self.client.get('/search', {'q': 'nicetitle'})
self.assertEqual(response.status_code, 200)
response = self.client.get(article.get_absolute_url())
self.assertEqual(response.status_code, 200)
from djangoblog.spider_notify import SpiderNotify
SpiderNotify.notify(article.get_absolute_url())
response = self.client.get(tag.get_absolute_url())
self.assertEqual(response.status_code, 200)
response = self.client.get(category.get_absolute_url())
self.assertEqual(response.status_code, 200)
response = self.client.get('/search', {'q': 'django'})
self.assertEqual(response.status_code, 200)
s = load_articletags(article)
self.assertIsNotNone(s)
self.client.login(username='liangliangyy', password='liangliangyy')
response = self.client.get(reverse('blog:archives'))
self.assertEqual(response.status_code, 200)
p = Paginator(Article.objects.all(), settings.PAGINATE_BY)
self.check_pagination(p, '', '')
p = Paginator(Article.objects.filter(tags=tag), settings.PAGINATE_BY)
self.check_pagination(p, '分类标签归档', tag.slug)
p = Paginator(
Article.objects.filter(
author__username='liangliangyy'), settings.PAGINATE_BY)
self.check_pagination(p, '作者文章归档', 'liangliangyy')
p = Paginator(Article.objects.filter(category=category), settings.PAGINATE_BY)
self.check_pagination(p, '分类目录归档', category.slug)
f = BlogSearchForm()
f.search()
# self.client.login(username='liangliangyy', password='liangliangyy')
from djangoblog.spider_notify import SpiderNotify
SpiderNotify.baidu_notify([article.get_full_url()])
from blog.templatetags.blog_tags import gravatar_url, gravatar
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)
response = self.client.get('/feed/')
self.assertEqual(response.status_code, 200)
response = self.client.get('/sitemap.xml')
self.assertEqual(response.status_code, 200)
self.client.get("/admin/blog/article/1/delete/")
self.client.get('/admin/servermanager/emailsendlog/')
self.client.get('/admin/admin/logentry/')
self.client.get('/admin/admin/logentry/1/change/')
def check_pagination(self, p, type, value):
for page in range(1, p.num_pages + 1):
s = load_pagination_info(p.page(page), type, value)
self.assertIsNotNone(s)
if s['previous_url']:
response = self.client.get(s['previous_url'])
self.assertEqual(response.status_code, 200)
if s['next_url']:
response = self.client.get(s['next_url'])
self.assertEqual(response.status_code, 200)
def test_image(self):
import requests
rsp = requests.get(
'https://www.python.org/static/img/python-logo.png')
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_sha256(get_sha256(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?sign=' + sign, form_data, follow=True)
self.assertEqual(rsp.status_code, 200)
os.remove(imagepath)
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.png')
def test_errorpage(self):
rsp = self.client.get('/eee')
self.assertEqual(rsp.status_code, 404)
def test_commands(self):
user = BlogUser.objects.get_or_create(
email="liangliangyy@gmail.com",
username="liangliangyy")[0]
user.set_password("liangliangyy")
user.is_staff = True
user.is_superuser = True
user.save()
c = OAuthConfig()
c.type = 'qq'
c.appkey = 'appkey'
c.appsecret = 'appsecret'
c.save()
u = OAuthUser()
u.type = 'qq'
u.openid = 'openid'
u.user = user
u.picture = static("/blog/img/avatar.png")
u.metadata = '''
{
"figureurl": "https://qzapp.qlogo.cn/qzapp/101513904/C740E30B4113EAA80E0D9918ABC78E82/30"
}'''
u.save()
u = OAuthUser()
u.type = 'qq'
u.openid = 'openid1'
u.picture = 'https://qzapp.qlogo.cn/qzapp/101513904/C740E30B4113EAA80E0D9918ABC78E82/30'
u.metadata = '''
{
"figureurl": "https://qzapp.qlogo.cn/qzapp/101513904/C740E30B4113EAA80E0D9918ABC78E82/30"
}'''
u.save()
from blog.documents import ELASTICSEARCH_ENABLED
if ELASTICSEARCH_ENABLED:
call_command("build_index")
call_command("ping_baidu", "all")
call_command("create_testdata")
call_command("clear_cache")
call_command("sync_user_avatar")
call_command("build_search_words")

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save