Compare commits

..

16 Commits

@ -1,84 +0,0 @@
from django import forms
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.forms import UserChangeForm, UsernameField
from django.utils.translation import gettext_lazy as _
# 引入自定义用户模型
from .models import BlogUser
class BlogUserCreationForm(forms.ModelForm):
"""
后台创建用户表单
提供两个密码输入框确保管理员在后台创建用户时输入的密码一致
"""
password1 = forms.CharField(
label=_('password'), # 字段显示名,可翻译
widget=forms.PasswordInput # 密码输入框,输入时隐藏内容
)
password2 = forms.CharField(
label=_('Enter password again'), # 再次输入密码
widget=forms.PasswordInput
)
class Meta:
model = BlogUser
fields = ('email',) # 后台创建用户表单只要求输入 email
def clean_password2(self):
"""
验证两次输入的密码是否一致
"""
password1 = self.cleaned_data.get("password1")
password2 = self.cleaned_data.get("password2")
if password1 and password2 and password1 != password2:
raise forms.ValidationError(_("passwords do not match"))
return password2
def save(self, commit=True):
"""
保存用户并将密码以哈希形式存储
"""
user = super().save(commit=False)
user.set_password(self.cleaned_data["password1"]) # 设置哈希密码
if commit:
user.source = 'adminsite' # 标记用户来源为后台
user.save()
return user
class BlogUserChangeForm(UserChangeForm):
"""
后台修改用户表单继承 Django 自带 UserChangeForm
"""
class Meta:
model = BlogUser
fields = '__all__' # 显示模型的所有字段
field_classes = {'username': UsernameField} # 指定 username 字段类型
def __init__(self, *args, **kwargs):
"""
可以在此处扩展初始化逻辑目前直接调用父类初始化
"""
super().__init__(*args, **kwargs)
class BlogUserAdmin(UserAdmin):
"""
自定义后台管理 BlogUser 的显示和表单配置
"""
form = BlogUserChangeForm # 修改用户时使用的表单
add_form = BlogUserCreationForm # 创建用户时使用的表单
# 在列表页显示的字段
list_display = (
'id',
'nickname',
'username',
'email',
'last_login',
'date_joined',
'source'
)
list_display_links = ('id', 'username') # 哪些字段可以点击进入编辑页面
ordering = ('-id',) # 默认按 ID 降序排列

@ -1,72 +0,0 @@
from django.contrib.auth.models import AbstractUser
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 djangoblog.utils import get_current_site
# Create your models here.
class BlogUser(AbstractUser):
"""
博客用户模型继承 Django AbstractUser
添加了博客系统需要的额外字段如昵称创建时间修改时间和来源
"""
nickname = models.CharField(
_('nick name'), # 字段在 admin 或表单中的显示名称(可翻译)
max_length=100, # 昵称最大长度为 100
blank=True # 可以为空
)
creation_time = models.DateTimeField(
_('creation time'), # 创建时间字段
default=now # 默认值为当前时间
)
last_modify_time = models.DateTimeField(
_('last modify time'), # 最近修改时间
default=now
)
source = models.CharField(
_('create source'), # 用户来源字段,例如“注册、后台添加等”
max_length=100,
blank=True
)
def get_absolute_url(self):
"""
返回用户详情页的 URL用于在模板或视图中直接获取用户个人主页链接
这里使用 username 作为参数
"""
return reverse(
'blog:author_detail', kwargs={
'author_name': self.username
}
)
def __str__(self):
"""
返回对象的字符串表示这里使用 email方便在 admin 或调试时查看
"""
return self.email
def get_full_url(self):
"""
返回带域名的完整用户详情页 URL
例如https://example.com/blog/author/username
"""
site = get_current_site().domain # 获取当前站点域名
url = "https://{site}{path}".format(
site=site,
path=self.get_absolute_url()
)
return url
class Meta:
ordering = ['-id'] # 默认按 ID 降序排列
verbose_name = _('user') # 在 admin 中显示的名称
verbose_name_plural = verbose_name # 复数形式
get_latest_by = 'id' # get_latest 方法默认按 ID 获取最新对象

@ -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,600 +0,0 @@
/* cyrillic-ext */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 300;
font-stretch: 100%;
font-display: swap;
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtE6F15M.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C8A, 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-stretch: 100%;
font-display: swap;
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWvU6F15M.woff2) format('woff2');
unicode-range: U+0301, 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-stretch: 100%;
font-display: swap;
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtU6F15M.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 300;
font-stretch: 100%;
font-display: swap;
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWuk6F15M.woff2) format('woff2');
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
/* hebrew */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 300;
font-stretch: 100%;
font-display: swap;
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWu06F15M.woff2) format('woff2');
unicode-range: U+0307-0308, U+0590-05FF, U+200C-2010, U+20AA, U+25CC, U+FB1D-FB4F;
}
/* math */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 300;
font-stretch: 100%;
font-display: swap;
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWxU6F15M.woff2) format('woff2');
unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF;
}
/* symbols */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 300;
font-stretch: 100%;
font-display: swap;
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqW106F15M.woff2) format('woff2');
unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF;
}
/* vietnamese */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 300;
font-stretch: 100%;
font-display: swap;
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtk6F15M.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 300;
font-stretch: 100%;
font-display: swap;
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWt06F15M.woff2) format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 300;
font-stretch: 100%;
font-display: swap;
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWuU6F.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, 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-stretch: 100%;
font-display: swap;
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtE6F15M.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C8A, 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-stretch: 100%;
font-display: swap;
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWvU6F15M.woff2) format('woff2');
unicode-range: U+0301, 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-stretch: 100%;
font-display: swap;
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtU6F15M.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 400;
font-stretch: 100%;
font-display: swap;
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWuk6F15M.woff2) format('woff2');
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
/* hebrew */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 400;
font-stretch: 100%;
font-display: swap;
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWu06F15M.woff2) format('woff2');
unicode-range: U+0307-0308, U+0590-05FF, U+200C-2010, U+20AA, U+25CC, U+FB1D-FB4F;
}
/* math */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 400;
font-stretch: 100%;
font-display: swap;
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWxU6F15M.woff2) format('woff2');
unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF;
}
/* symbols */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 400;
font-stretch: 100%;
font-display: swap;
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqW106F15M.woff2) format('woff2');
unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF;
}
/* vietnamese */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 400;
font-stretch: 100%;
font-display: swap;
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtk6F15M.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 400;
font-stretch: 100%;
font-display: swap;
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWt06F15M.woff2) format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 400;
font-stretch: 100%;
font-display: swap;
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWuU6F.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, 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-stretch: 100%;
font-display: swap;
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtE6F15M.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C8A, 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-stretch: 100%;
font-display: swap;
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWvU6F15M.woff2) format('woff2');
unicode-range: U+0301, 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-stretch: 100%;
font-display: swap;
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtU6F15M.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 600;
font-stretch: 100%;
font-display: swap;
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWuk6F15M.woff2) format('woff2');
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
/* hebrew */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 600;
font-stretch: 100%;
font-display: swap;
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWu06F15M.woff2) format('woff2');
unicode-range: U+0307-0308, U+0590-05FF, U+200C-2010, U+20AA, U+25CC, U+FB1D-FB4F;
}
/* math */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 600;
font-stretch: 100%;
font-display: swap;
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWxU6F15M.woff2) format('woff2');
unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF;
}
/* symbols */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 600;
font-stretch: 100%;
font-display: swap;
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqW106F15M.woff2) format('woff2');
unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF;
}
/* vietnamese */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 600;
font-stretch: 100%;
font-display: swap;
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtk6F15M.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 600;
font-stretch: 100%;
font-display: swap;
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWt06F15M.woff2) format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 600;
font-stretch: 100%;
font-display: swap;
src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWuU6F.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, 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-stretch: 100%;
font-display: swap;
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSKmu1aB.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C8A, 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-stretch: 100%;
font-display: swap;
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSumu1aB.woff2) format('woff2');
unicode-range: U+0301, 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-stretch: 100%;
font-display: swap;
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSOmu1aB.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 300;
font-stretch: 100%;
font-display: swap;
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSymu1aB.woff2) format('woff2');
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
/* hebrew */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 300;
font-stretch: 100%;
font-display: swap;
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTS2mu1aB.woff2) format('woff2');
unicode-range: U+0307-0308, U+0590-05FF, U+200C-2010, U+20AA, U+25CC, U+FB1D-FB4F;
}
/* math */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 300;
font-stretch: 100%;
font-display: swap;
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTVOmu1aB.woff2) format('woff2');
unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF;
}
/* symbols */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 300;
font-stretch: 100%;
font-display: swap;
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTUGmu1aB.woff2) format('woff2');
unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF;
}
/* vietnamese */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 300;
font-stretch: 100%;
font-display: swap;
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSCmu1aB.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 300;
font-stretch: 100%;
font-display: swap;
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSGmu1aB.woff2) format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 300;
font-stretch: 100%;
font-display: swap;
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTS-muw.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, 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-stretch: 100%;
font-display: swap;
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSKmu1aB.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C8A, 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-stretch: 100%;
font-display: swap;
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSumu1aB.woff2) format('woff2');
unicode-range: U+0301, 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-stretch: 100%;
font-display: swap;
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSOmu1aB.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
font-stretch: 100%;
font-display: swap;
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSymu1aB.woff2) format('woff2');
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
/* hebrew */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
font-stretch: 100%;
font-display: swap;
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTS2mu1aB.woff2) format('woff2');
unicode-range: U+0307-0308, U+0590-05FF, U+200C-2010, U+20AA, U+25CC, U+FB1D-FB4F;
}
/* math */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
font-stretch: 100%;
font-display: swap;
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTVOmu1aB.woff2) format('woff2');
unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF;
}
/* symbols */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
font-stretch: 100%;
font-display: swap;
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTUGmu1aB.woff2) format('woff2');
unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF;
}
/* vietnamese */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
font-stretch: 100%;
font-display: swap;
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSCmu1aB.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
font-stretch: 100%;
font-display: swap;
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSGmu1aB.woff2) format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
font-stretch: 100%;
font-display: swap;
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTS-muw.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, 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-stretch: 100%;
font-display: swap;
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSKmu1aB.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C8A, 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-stretch: 100%;
font-display: swap;
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSumu1aB.woff2) format('woff2');
unicode-range: U+0301, 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-stretch: 100%;
font-display: swap;
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSOmu1aB.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 600;
font-stretch: 100%;
font-display: swap;
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSymu1aB.woff2) format('woff2');
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
/* hebrew */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 600;
font-stretch: 100%;
font-display: swap;
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTS2mu1aB.woff2) format('woff2');
unicode-range: U+0307-0308, U+0590-05FF, U+200C-2010, U+20AA, U+25CC, U+FB1D-FB4F;
}
/* math */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 600;
font-stretch: 100%;
font-display: swap;
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTVOmu1aB.woff2) format('woff2');
unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF;
}
/* symbols */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 600;
font-stretch: 100%;
font-display: swap;
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTUGmu1aB.woff2) format('woff2');
unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF;
}
/* vietnamese */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 600;
font-stretch: 100%;
font-display: swap;
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSCmu1aB.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 600;
font-stretch: 100%;
font-display: swap;
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSGmu1aB.woff2) format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 600;
font-stretch: 100%;
font-display: swap;
src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTS-muw.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, 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,142 +0,0 @@
/**
* MathJax 智能加载器
* 检测页面是否包含数学公式如果有则动态加载和配置MathJax
*/
(function() {
'use strict';
/**
* 检测页面是否包含数学公式
* @returns {boolean} 是否包含数学公式
*/
function hasMathFormulas() {
const content = document.body.textContent || document.body.innerText || '';
// 检测常见的数学公式语法
return /\$.*?\$|\$\$.*?\$\$|\\begin\{.*?\}|\\end\{.*?\}|\\[a-zA-Z]+\{/.test(content);
}
/**
* 配置MathJax
*/
function configureMathJax() {
window.MathJax = {
tex: {
// 行内公式和块级公式分隔符
inlineMath: [['$', '$']],
displayMath: [['$$', '$$']],
// 处理转义字符和LaTeX环境
processEscapes: true,
processEnvironments: true,
// 自动换行
tags: 'ams'
},
options: {
// 跳过这些HTML标签避免处理代码块等
skipHtmlTags: ['script', 'noscript', 'style', 'textarea', 'pre', 'code', 'a'],
// CSS类控制
ignoreHtmlClass: 'tex2jax_ignore',
processHtmlClass: 'tex2jax_process'
},
// 启动配置
startup: {
ready() {
console.log('MathJax配置完成开始初始化...');
MathJax.startup.defaultReady();
// 处理特定区域的数学公式
const contentEl = document.getElementById('content');
const commentsEl = document.getElementById('comments');
const promises = [];
if (contentEl) {
promises.push(MathJax.typesetPromise([contentEl]));
}
if (commentsEl) {
promises.push(MathJax.typesetPromise([commentsEl]));
}
// 等待所有渲染完成
Promise.all(promises).then(() => {
console.log('MathJax渲染完成');
// 触发自定义事件通知其他脚本MathJax已就绪
document.dispatchEvent(new CustomEvent('mathjaxReady'));
}).catch(error => {
console.error('MathJax渲染失败:', error);
});
}
},
// 输出配置
chtml: {
scale: 1,
minScale: 0.5,
matchFontHeight: false,
displayAlign: 'center',
displayIndent: '0'
}
};
}
/**
* 加载MathJax库
*/
function loadMathJax() {
console.log('检测到数学公式开始加载MathJax...');
const script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js';
script.async = true;
script.defer = true;
script.onload = function() {
console.log('MathJax库加载成功');
};
script.onerror = function() {
console.error('MathJax库加载失败尝试备用CDN...');
// 备用CDN
const fallbackScript = document.createElement('script');
fallbackScript.src = 'https://polyfill.io/v3/polyfill.min.js?features=es6';
fallbackScript.onload = function() {
const mathJaxScript = document.createElement('script');
mathJaxScript.src = 'https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-MML-AM_CHTML';
mathJaxScript.async = true;
document.head.appendChild(mathJaxScript);
};
document.head.appendChild(fallbackScript);
};
document.head.appendChild(script);
}
/**
* 初始化函数
*/
function init() {
// 等待DOM完全加载
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
return;
}
// 检测是否需要加载MathJax
if (hasMathFormulas()) {
// 先配置,再加载
configureMathJax();
loadMathJax();
} else {
console.log('未检测到数学公式跳过MathJax加载');
}
}
// 提供重新渲染的全局方法,供动态内容使用
window.rerenderMathJax = function(element) {
if (window.MathJax && window.MathJax.typesetPromise) {
const target = element || document.body;
return window.MathJax.typesetPromise([target]);
}
return Promise.resolve();
};
// 启动初始化
init();
})();

@ -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,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,92 +0,0 @@
# 赵瑞萍评论模型后台管理配置模块用于自定义Django Admin评论管理界面
# 功能:配置评论列表展示、筛选、批量操作及自定义字段,支持国际化和快速跳转关联数据
# 核心特性:批量启用/禁用评论、作者/文章快速跳转、列表字段自定义显示
from django.contrib import admin
from django.urls import reverse # 用于反向生成Admin页面URL
from django.utils.html import format_html # 用于安全渲染HTML链接避免XSS风险
from django.utils.translation import gettext_lazy as _ # 用于后台字段名称国际化翻译
# 赵瑞萍:自定义批量操作函数——禁用选中的评论
def disable_commentstatus(modeladmin, request, queryset):
# 赵瑞萍将选中评论的is_enable字段批量更新为False实现批量隐藏评论
queryset.update(is_enable=False)
# 赵瑞萍:自定义批量操作函数——启用选中的评论
def enable_commentstatus(modeladmin, request, queryset):
# 赵瑞萍将选中评论的is_enable字段批量更新为True实现批量显示评论
queryset.update(is_enable=True)
# 赵瑞萍:为批量操作设置后台显示名称,支持国际化翻译
disable_commentstatus.short_description = _('Disable comments')
enable_commentstatus.short_description = _('Enable comments')
class CommentAdmin(admin.ModelAdmin):
"""
赵瑞萍评论模型的Admin配置类控制Django后台评论管理界面的各项功能
包括列表展示字段分页筛选编辑页字段批量操作等配置
"""
# 赵瑞萍列表页分页配置每页显示20条评论避免数据过多导致加载缓慢
list_per_page = 20
# 赵瑞萍:列表页显示的字段,包含基础字段和自定义跳转字段
list_display = (
'id', # 评论唯一ID用于快速标识
'body', # 评论正文内容,直观查看评论信息
'link_to_userinfo', # 自定义字段:评论作者的后台编辑页链接
'link_to_article', # 自定义字段:评论所属文章的后台编辑页链接
'is_enable', # 评论显示状态,快速判断是否启用
'creation_time' # 评论创建时间,追溯评论发布时间
)
# 赵瑞萍:列表页中可点击跳转至编辑页的字段,方便快速编辑
list_display_links = ('id', 'body', 'is_enable')
# 赵瑞萍右侧筛选器配置按评论显示状态is_enable筛选快速筛选启用/禁用评论
list_filter = ('is_enable',)
# 赵瑞萍:编辑页排除的字段,创建时间和最后修改时间不允许手动修改,由系统自动维护
exclude = ('creation_time', 'last_modify_time')
# 赵瑞萍:注册批量操作函数,在列表页提供"禁用评论"和"启用评论"的批量操作按钮
actions = [disable_commentstatus, enable_commentstatus]
def link_to_userinfo(self, obj):
"""
赵瑞萍自定义列表字段生成评论作者的后台编辑页链接
实现从评论直接跳转至作者详情页方便关联数据管理
参数obj当前评论对象
返回安全渲染的HTML链接标签
"""
# 赵瑞萍获取作者模型的app标签和模型名称用于反向生成URL
info = (obj.author._meta.app_label, obj.author._meta.model_name)
# 赵瑞萍反向生成作者模型的后台编辑页URL传入作者ID作为参数
link = reverse('admin:%s_%s_change' % info, args=(obj.author.id,))
# 赵瑞萍渲染HTML链接优先显示作者昵称无昵称则显示邮箱确保显示有意义的信息
return format_html(
u'<a href="%s">%s</a>' %
(link, obj.author.nickname if obj.author.nickname else obj.author.email)
)
def link_to_article(self, obj):
"""
赵瑞萍自定义列表字段生成评论所属文章的后台编辑页链接
实现从评论直接跳转至文章详情页方便关联数据核查
参数obj当前评论对象
返回安全渲染的HTML链接标签
"""
# 赵瑞萍获取文章模型的app标签和模型名称用于反向生成URL
info = (obj.article._meta.app_label, obj.article._meta.model_name)
# 赵瑞萍反向生成文章模型的后台编辑页URL传入文章ID作为参数
link = reverse('admin:%s_%s_change' % info, args=(obj.article.id,))
# 赵瑞萍渲染HTML链接显示文章标题直观标识关联文章
return format_html(u'<a href="%s">%s</a>' % (link, obj.article.title))
# 赵瑞萍:为自定义字段设置后台显示名称,支持国际化翻译,适配多语言环境
link_to_userinfo.short_description = _('User')
link_to_article.short_description = _('Article')

@ -1,18 +0,0 @@
# 赵瑞萍comments应用配置模块用于定义评论应用的元数据和初始化设置
# 该类是Django应用的核心配置类项目启动时自动加载用于识别和管理comments应用
from django.apps import AppConfig
class CommentsConfig(AppConfig):
"""
赵瑞萍comments应用的配置类继承自Django的AppConfig
用于声明应用的基本信息控制应用的初始化行为是Django识别应用的关键
"""
# 赵瑞萍:应用的唯一标识名称,必须与应用目录名完全一致
# 用于在settings.py的INSTALLED_APPS中注册应用Django通过该名称定位应用
name = 'comments'
# 赵瑞萍:可选配置,指定应用的 verbose 名称,用于后台管理界面显示
# 若不设置默认显示为name的值'comments'
verbose_name = '评论管理'

@ -1,24 +0,0 @@
# 赵瑞萍:评论表单模块,用于构建用户提交评论的表单,支持顶级评论和嵌套回复功能
# 基于Comment模型快速生成表单控制前端输入字段适配评论提交的数据收集需求
# 赵瑞萍导入Django表单核心模块提供表单基础构建能力
from django import forms
from django.forms import ModelForm # 导入模型表单类,实现表单与数据模型的快速绑定
from .models import Comment # 导入当前应用的Comment模型作为表单的数据源
class CommentForm(ModelForm):
"""
赵瑞萍评论提交表单类继承ModelForm实现与Comment模型的关联
扩展父评论ID字段以支持回复功能仅暴露核心输入项简化用户操作
"""
# 赵瑞萍定义父评论ID字段用于识别当前评论的回复目标实现嵌套回复
parent_comment_id = forms.IntegerField(
widget=forms.HiddenInput, # 使用隐藏输入组件,不在前端页面展示,仅用于后端传递数据
required=False # 设为非必填,顶级评论(直接评论文章)无需传入该字段
)
# 赵瑞萍:表单元数据配置类,定义模型关联、字段筛选等关键配置
class Meta:
model = Comment # 绑定Comment模型表单数据将直接映射到模型对应的字段
fields = ['body'] # 仅指定评论正文字段为前端输入项,其他字段(如作者、时间)由后端处理

@ -1,89 +0,0 @@
# 赵瑞萍评论模型迁移模块用于定义Comment模型在数据库中的初始结构
# 该模块通过Django迁移系统创建评论表支持用户对文章的评论及嵌套回复功能
# 生成于Django 4.1.7版本,包含字段定义、关联关系及数据库操作规则
# 赵瑞萍导入Django迁移所需核心模块
from django.conf import settings # 用于获取项目配置的用户模型
from django.db import migrations, models # 提供数据库迁移操作和字段类型
import django.db.models.deletion # 用于定义外键删除时的关联行为
import django.utils.timezone # 提供带时区的时间处理工具
class Migration(migrations.Migration):
"""
赵瑞萍迁移类封装评论模型的数据库结构创建逻辑
负责定义迁移依赖关系和具体的表创建操作确保数据库结构与模型一致
"""
# 赵瑞萍:标记为初始迁移,即首次创建该模型对应的数据库表
initial = True
# 赵瑞萍:迁移依赖配置,确保执行顺序正确
dependencies = [
('blog', '0001_initial'), # 依赖blog应用的初始迁移确保Article表已存在
migrations.swappable_dependency(settings.AUTH_USER_MODEL), # 依赖用户模型迁移,确保用户表存在
]
# 赵瑞萍数据库操作列表此处为创建Comment表的核心操作
operations = [
# 赵瑞萍创建Comment模型对应的数据库表
migrations.CreateModel(
name='Comment', # 模型名称对应数据库表名为comments_comment
fields=[
# 赵瑞萍:主键字段,自动增长的大整数类型
('id', models.BigAutoField(
auto_created=True, # 自动生成主键值,无需手动赋值
primary_key=True, # 设为主键,唯一标识每条评论记录
serialize=False, # 序列化模型时不包含该字段
verbose_name='ID' # 后台管理界面显示的字段名称
)),
# 赵瑞萍:评论正文字段,存储用户输入的评论内容
('body', models.TextField(
max_length=300, # 限制最大长度为300字符控制评论长度
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='修改时间' # 后台显示名称
)),
# 赵瑞萍:评论显示状态字段,控制评论是否在前端展示
('is_enable', models.BooleanField(
default=True, # 默认值为True新评论默认可见
verbose_name='是否显示' # 后台显示名称
)),
# 赵瑞萍:外键字段,关联文章模型,建立评论与所属文章的关系
('article', models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, # 级联删除:文章删除时,关联评论同步删除
to='blog.article', # 关联目标为blog应用的Article模型
verbose_name='文章' # 后台显示名称
)),
# 赵瑞萍:外键字段,关联用户模型,记录评论的发布者
('author', models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, # 级联删除:用户删除时,其发布的评论同步删除
to=settings.AUTH_USER_MODEL, # 关联目标为项目配置的用户模型
verbose_name='作者' # 后台显示名称
)),
# 赵瑞萍:外键自关联,实现评论的嵌套回复功能
('parent_comment', models.ForeignKey(
blank=True, # 表单提交时允许为空(一级评论无需上级评论)
null=True, # 数据库字段允许为NULL
on_delete=django.db.models.deletion.CASCADE, # 级联删除:上级评论删除时,回复评论同步删除
to='comments.comment', # 关联目标为当前应用的Comment模型
verbose_name='上级评论' # 后台显示名称
)),
],
# 赵瑞萍:模型元数据配置,定义表的显示和行为规则
options={
'verbose_name': '评论', # 模型的单数显示名称
'verbose_name_plural': '评论', # 模型的复数显示名称(中文无复数变化)
'ordering': ['-id'], # 默认排序规则按id降序最新评论优先显示
'get_latest_by': 'id', # 调用latest()方法时以id字段作为判断最新的依据
},
),
]

@ -1,33 +0,0 @@
# 赵瑞萍评论模型修改迁移模块用于调整Comment模型中is_enable字段的默认属性
# 该模块通过Django迁移系统更新评论表结构将评论默认显示状态从"显示"改为"隐藏"
# 生成于Django 4.1.7版本,专注于单字段属性的变更操作
# 赵瑞萍导入Django数据库迁移核心模块
from django.db import migrations, models # 提供迁移操作类和模型字段类型
class Migration(migrations.Migration):
"""
赵瑞萍迁移类封装评论模型字段的修改逻辑
负责将Comment模型的is_enable字段默认值从True改为False实现评论默认不显示的功能
"""
# 赵瑞萍:迁移依赖配置,确保执行顺序正确
dependencies = [
('comments', '0001_initial'), # 依赖comments应用的初始迁移确保Comment表已存在
]
# 赵瑞萍数据库操作列表此处为修改is_enable字段属性的核心操作
operations = [
# 赵瑞萍修改Comment模型的is_enable字段配置
migrations.AlterField(
model_name='comment', # 目标模型comments应用的Comment模型
name='is_enable', # 目标字段控制评论显示状态的is_enable字段
# 赵瑞萍:修改后的字段定义,仅变更默认值
field=models.BooleanField(
default=False, # 关键变更默认值从True改为False新评论默认不显示
verbose_name='是否显示' # 保持后台显示名称不变
),
),
]

@ -1,115 +0,0 @@
# 赵瑞萍Comment模型优化迁移模块用于对评论模型进行多维度结构调整
# 核心变更包括字段名规范化、verbose_name国际化中文转英文、元数据更新适配项目国际化需求
# 生成于Django 4.2.5版本,基于之前的迁移版本迭代修改
# 赵瑞萍导入Django迁移所需核心模块
from django.conf import settings # 用于获取项目配置的用户模型
from django.db import migrations, models # 提供数据库迁移操作和字段类型定义
import django.db.models.deletion # 用于配置外键删除时的关联行为
import django.utils.timezone # 提供带时区支持的时间处理工具
class Migration(migrations.Migration):
"""
赵瑞萍多字段修改迁移类封装Comment模型的结构优化逻辑
主要实现字段名标准化字段备注国际化同时保持模型核心功能不变
"""
# 赵瑞萍:迁移依赖配置,确保执行顺序符合依赖关系
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL), # 依赖用户模型迁移,确保用户表存在
('blog', '0005_alter_article_options_alter_category_options_and_more'), # 依赖blog应用的指定迁移版本
('comments', '0002_alter_comment_is_enable'), # 依赖comments应用的上一个迁移版本确保基础结构存在
]
# 赵瑞萍:数据库操作列表,包含字段增删改、元数据调整等操作
operations = [
# 赵瑞萍修改Comment模型的元数据配置适配国际化显示
migrations.AlterModelOptions(
name='comment', # 目标模型comments应用的Comment模型
# 关键变更verbose_name从中文"评论"改为英文"comment",保持排序和最新记录判断规则不变
options={
'get_latest_by': 'id', # 仍以id字段判断最新评论
'ordering': ['-id'], # 仍按id降序排序新评论在前
'verbose_name': 'comment', # 单数显示名称(国际化调整)
'verbose_name_plural': 'comment' # 复数显示名称(国际化调整)
},
),
# 赵瑞萍:删除原有创建时间字段,后续将替换为命名更规范的新字段
migrations.RemoveField(
model_name='comment',
name='created_time',
),
# 赵瑞萍:删除原有修改时间字段,后续替换为命名更规范的新字段
migrations.RemoveField(
model_name='comment',
name='last_mod_time',
),
# 赵瑞萍:添加新的创建时间字段,字段名规范化并更新备注
migrations.AddField(
model_name='comment',
name='creation_time', # 新字段名从created_time改为creation_time命名更规范
field=models.DateTimeField(
default=django.utils.timezone.now, # 默认值仍为当前时区时间
verbose_name='creation time' # 备注改为英文(国际化调整)
),
),
# 赵瑞萍:添加新的修改时间字段,字段名规范化并更新备注
migrations.AddField(
model_name='comment',
name='last_modify_time', # 新字段名从last_mod_time改为last_modify_time命名更规范
field=models.DateTimeField(
default=django.utils.timezone.now, # 默认值仍为当前时区时间
verbose_name='last modify time' # 备注改为英文(国际化调整)
),
),
# 赵瑞萍修改article外键字段的备注信息国际化调整
migrations.AlterField(
model_name='comment',
name='article', # 目标字段:关联文章的外键
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, # 级联删除规则保持不变
to='blog.article', # 关联目标保持不变blog应用的Article模型
verbose_name='article' # 备注从中文"文章"改为英文(国际化调整)
),
),
# 赵瑞萍修改author外键字段的备注信息国际化调整
migrations.AlterField(
model_name='comment',
name='author', # 目标字段:关联用户的外键
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, # 级联删除规则保持不变
to=settings.AUTH_USER_MODEL, # 关联目标保持不变(项目配置的用户模型)
verbose_name='author' # 备注从中文"作者"改为英文(国际化调整)
),
),
# 赵瑞萍修改is_enable字段的备注信息国际化调整
migrations.AlterField(
model_name='comment',
name='is_enable', # 目标字段:控制评论显示状态的字段
field=models.BooleanField(
default=False, # 默认值保持不变(新评论默认不显示)
verbose_name='enable' # 备注从中文"是否显示"改为英文(国际化调整)
),
),
# 赵瑞萍修改parent_comment外键字段的备注信息国际化调整
migrations.AlterField(
model_name='comment',
name='parent_comment', # 目标字段:实现嵌套回复的自关联外键
field=models.ForeignKey(
blank=True, # 允许表单为空(一级评论无需上级评论)
null=True, # 数据库字段允许为NULL
on_delete=django.db.models.deletion.CASCADE, # 级联删除规则保持不变
to='comments.comment', # 关联目标保持不变当前应用的Comment模型
verbose_name='parent comment' # 备注从中文"上级评论"改为英文(国际化调整)
),
),
]

@ -1,76 +0,0 @@
# 赵瑞萍:评论模型模块,定义评论数据结构及关联关系
# 功能:存储用户对文章的评论信息,支持嵌套回复,包含评论内容、时间、作者、关联文章等核心数据
# 特性:支持国际化显示、级联删除关联数据、默认排序规则及评论状态管理
# 赵瑞萍导入Django核心模块提供配置、数据库操作、时间处理和国际化支持
from django.conf import settings # 获取项目配置(如用户模型)
from django.db import models # 数据库模型基类及字段类型
from django.utils.timezone import now # 获取带时区的当前时间,用于时间字段默认值
from django.utils.translation import gettext_lazy as _ # 国际化翻译工具,实现多语言显示
# 赵瑞萍:导入关联模型,评论需与博客文章建立关联关系
from blog.models import Article # 博客文章模型,评论将关联到具体文章
class Comment(models.Model):
"""
赵瑞萍评论模型类映射数据库中存储评论的表结构
记录评论的内容时间作者关联文章父评论及显示状态等信息
支持评论嵌套回复和国际化管理
"""
# 赵瑞萍评论正文字段限制最大300字符避免内容过长
body = models.TextField('正文', max_length=300)
# 赵瑞萍:评论创建时间字段,默认值为当前时区时间,支持国际化显示
creation_time = models.DateTimeField(_('creation time'), default=now)
# 赵瑞萍:评论最后修改时间字段,默认值为当前时间,可通过代码更新为修改时的时间
last_modify_time = models.DateTimeField(_('last modify time'), default=now)
# 赵瑞萍:评论作者外键,关联项目配置的用户模型
# 级联删除:当用户账号被删除时,其发布的所有评论也会被删除
author = models.ForeignKey(
settings.AUTH_USER_MODEL,
verbose_name=_('author'), # 国际化字段名:作者
on_delete=models.CASCADE
)
# 赵瑞萍:关联文章外键,评论属于某篇具体文章
# 级联删除:当文章被删除时,其下所有评论也会被删除
article = models.ForeignKey(
Article,
verbose_name=_('article'), # 国际化字段名:文章
on_delete=models.CASCADE
)
# 赵瑞萍:父评论自关联外键,实现评论嵌套回复功能
# 允许为空:顶级评论(直接评论文章)无父评论
# 级联删除:当父评论被删除时,其所有回复评论也会被删除
parent_comment = models.ForeignKey(
'self',
verbose_name=_('parent comment'), # 国际化字段名:上级评论
blank=True, # 表单提交时允许为空
null=True, # 数据库中允许为NULL
on_delete=models.CASCADE
)
# 赵瑞萍:评论显示状态字段,用于评论审核功能
# 默认值为False需审核后显示不允许为空
is_enable = models.BooleanField(
_('enable'), # 国际化字段名:是否启用
default=False,
blank=False,
null=False
)
# 赵瑞萍:模型元数据配置,定义模型的整体行为和显示规则
class Meta:
ordering = ['-id'] # 默认排序按ID降序最新评论优先显示
verbose_name = _('comment') # 模型单数显示名称(国际化)
verbose_name_plural = verbose_name # 模型复数显示名称(与单数相同)
get_latest_by = 'id' # 使用latest()方法时按ID字段判断最新记录
# 赵瑞萍:定义模型实例的字符串表示形式
# 在后台管理和调试时,直观显示评论内容(取正文作为标识)
def __str__(self):
return self.body

@ -1,58 +0,0 @@
# 赵瑞萍:评论模板标签模块,提供解析评论嵌套结构和渲染评论项的自定义标签
# 功能支持在Django模板中处理评论树的层级关系生成嵌套评论的HTML展示结构
# 主要包含两个标签parse_commenttree解析子评论和show_comment_item渲染评论项
from django import template
# 赵瑞萍:注册模板标签库,使自定义标签可在模板中通过{% load 模块名 %}加载使用
register = template.Library()
@register.simple_tag
def parse_commenttree(commentlist, comment):
"""
赵瑞萍解析评论树的模板标签用于获取当前评论的所有嵌套子评论含多级回复
采用递归方式遍历评论的所有后代仅收集已启用的评论is_enable=True
参数
commentlist包含当前文章所有评论的查询集
comment当前评论对象需获取其下的所有子评论
返回按层级顺序排列的子评论列表
用法示例{% parse_commenttree article_comments comment as childcomments %}
"""
# 赵瑞萍:初始化列表,用于存储递归过程中收集到的所有子评论
datas = []
# 赵瑞萍:定义内部递归函数,用于深度优先遍历子评论
def parse(c):
# 赵瑞萍筛选出当前评论c的直接子评论且状态为已启用
# 条件parent_comment外键指向c且is_enable=True
childs = commentlist.filter(parent_comment=c, is_enable=True)
# 赵瑞萍:遍历每个直接子评论,进行递归处理
for child in childs:
datas.append(child) # 将子评论添加到结果列表
parse(child) # 递归调用,处理该子评论的下一级回复
# 赵瑞萍:从当前评论开始递归解析,收集所有嵌套子评论
parse(comment)
# 赵瑞萍:返回收集到的子评论列表,供模板循环渲染
return datas
@register.inclusion_tag('comments/tags/comment_item.html')
def show_comment_item(comment, ischild):
"""
赵瑞萍评论项渲染标签用于将单个评论对象渲染为HTML片段
关联模板文件comments/tags/comment_item.html传递评论数据和样式层级参数
参数
comment需要渲染的评论对象包含作者内容时间等属性
ischild布尔值标识该评论是否为子评论用于区分样式层级
返回传递给模板的上下文变量字典
"""
# 赵瑞萍:根据是否为子评论设置层级深度,用于前端样式区分(如缩进量)
# 子评论depth=1顶级评论depth=2可在模板中通过该值控制CSS样式
depth = 1 if ischild else 2
# 赵瑞萍:向模板传递上下文变量,模板中可通过{{ comment_item }}和{{ depth }}访问
return {
'comment_item': comment, # 评论对象,提供评论的核心数据
'depth': depth # 层级深度,用于控制评论的显示样式
}

@ -1,146 +0,0 @@
# 赵瑞萍:评论功能测试模块,用于验证评论发布、回复、审核及显示等核心流程
# 基于Django TransactionTestCase支持数据库事务操作覆盖评论功能的关键场景测试
# 赵瑞萍导入Django测试核心工具提供请求模拟、URL解析等能力
from django.test import Client, RequestFactory, TransactionTestCase
from django.urls import reverse # 用于反向解析视图URL避免硬编码
# 赵瑞萍:导入测试所需的模型和工具,覆盖用户、文章、评论等关联数据
from accounts.models import BlogUser # 自定义用户模型,用于创建测试用户
from blog.models import Category, Article, BlogSettings # 博客分类、文章、设置模型
from comments.models import Comment # 评论模型,测试核心对象
from comments.templatetags.comments_tags import * # 评论模板标签,用于测试评论树解析
from djangoblog.utils import get_max_articleid_commentid # 工具函数,辅助测试数据处理
class CommentsTest(TransactionTestCase):
"""
赵瑞萍评论功能测试类继承TransactionTestCase支持事务回滚确保测试独立性
覆盖场景评论发布回复评论评论审核状态评论列表显示等核心功能
"""
def setUp(self):
"""
赵瑞萍测试前置初始化方法每个测试函数执行前自动调用
初始化测试客户端请求工厂测试数据用户博客设置为测试提供基础环境
"""
# 赵瑞萍创建测试客户端用于模拟用户发起HTTP请求如提交评论
self.client = Client()
# 赵瑞萍:创建请求工厂,用于构造自定义请求对象(灵活模拟请求场景)
self.factory = RequestFactory()
# 赵瑞萍:配置博客评论设置,开启"评论需要审核"功能(模拟真实业务场景)
value = BlogSettings()
value.comment_need_review = True # 评论需审核后才显示
value.save() # 保存设置到数据库
# 赵瑞萍:创建超级用户,用于测试登录状态下的评论发布权限
self.user = BlogUser.objects.create_superuser(
email="liangliangyy1@gmail.com",
username="liangliangyy1",
password="liangliangyy1"
)
def update_article_comment_status(self, article):
"""
赵瑞萍测试辅助方法用于批量更新文章评论为"启用"状态模拟审核通过
参数article - 目标文章对象需启用其下所有评论
"""
# 赵瑞萍:获取该文章关联的所有评论查询集
comments = article.comment_set.all()
# 赵瑞萍遍历评论将is_enable设为True审核通过并保存
for comment in comments:
comment.is_enable = True
comment.save()
def test_validate_comment(self):
"""
赵瑞萍核心测试方法验证评论发布回复审核显示等完整流程
包含登录验证文章创建普通评论发布嵌套回复Markdown内容测试
"""
# 赵瑞萍:模拟用户登录,获取评论发布权限(需登录才能评论)
self.client.login(username='liangliangyy1', password='liangliangyy1')
# 赵瑞萍:创建测试分类(文章必须关联分类,满足模型约束)
category = Category()
category.name = "categoryccc"
category.save()
# 赵瑞萍:创建测试文章(评论需关联文章,为评论提供目标对象)
article = Article()
article.title = "nicetitleccc" # 文章标题
article.body = "nicecontentccc" # 文章内容
article.author = self.user # 关联创建者(超级用户)
article.category = category # 关联分类
article.type = 'a' # 假设'a'为普通文章类型(符合项目定义)
article.status = 'p' # 假设'p'为已发布状态(只有已发布文章可评论)
article.save()
# 赵瑞萍反向解析评论提交视图的URL传入文章ID参数
comment_url = reverse(
'comments:postcomment', kwargs={'article_id': article.id}
)
# 赵瑞萍测试场景1 - 发布第一条普通评论
response = self.client.post(
comment_url,
{'body': '123ffffffffff'} # 评论内容
)
# 赵瑞萍验证评论提交后是否重定向通常跳回文章详情页状态码302
self.assertEqual(response.status_code, 302)
# 赵瑞萍:从数据库刷新文章对象(获取最新关联的评论数据)
article = Article.objects.get(pk=article.pk)
# 赵瑞萍未审核的评论is_enable=False评论列表长度应为0
self.assertEqual(len(article.comment_list()), 0)
# 赵瑞萍:调用辅助方法模拟审核通过,启用评论
self.update_article_comment_status(article)
# 赵瑞萍审核后评论应显示列表长度应为1
self.assertEqual(len(article.comment_list()), 1)
# 赵瑞萍测试场景2 - 发布第二条普通评论(验证多评论存储)
response = self.client.post(
comment_url,
{'body': '123ffffffffff'}
)
self.assertEqual(response.status_code, 302) # 验证重定向
article = Article.objects.get(pk=article.pk)
self.update_article_comment_status(article)
self.assertEqual(len(article.comment_list()), 2) # 验证评论数量增加
# 赵瑞萍测试场景3 - 回复评论(嵌套回复功能)
# 获取第一条评论的ID作为父评论ID建立回复关联
parent_comment_id = article.comment_list()[0].id
# 赵瑞萍发布带Markdown格式的回复测试富文本内容支持
response = self.client.post(
comment_url,
{
'body': '''
# Title1 Markdown标题
```python
import os # Markdown代码块
''',
'parent_comment_id': parent_comment_id # 传入父评论ID标识为回复
}
)
# 赵瑞萍:验证回复提交是否成功(重定向)
self.assertEqual(response.status_code, 302)
# 赵瑞萍:刷新文章对象并启用所有评论(模拟审核)
article = Article.objects.get(pk=article.pk)
self.update_article_comment_status(article)
# 赵瑞萍验证总评论数原有2条+1条回复=3条
self.assertEqual(len(article.comment_list()), 3)
# 赵瑞萍测试场景4 - 验证评论树解析(通过模板标签解析嵌套关系)
# 获取所有已启用的评论查询集
comment_list = Comment.objects.filter(is_enable=True)
# 获取第一条评论(父评论)
parent_comment = comment_list.first()
# 调用模板标签函数,解析该父评论的所有子评论
child_comments = parse_commenttree(comment_list, parent_comment)
# 赵瑞萍验证子评论数量仅1条回复
self.assertEqual(len(child_comments), 1)
# 赵瑞萍验证子评论的父评论ID是否正确匹配
self.assertEqual(child_comments[0].parent_comment.id, parent_comment.id)

@ -1,23 +0,0 @@
# 赵瑞萍评论应用URL配置模块用于定义评论相关的URL路由规则
# 核心功能映射评论提交的URL路径到对应视图支持通过文章ID关联评论目标
# 赵瑞萍导入Django URL核心函数用于定义路径匹配规则
from django.urls import path
# 赵瑞萍:导入当前应用的视图模块,关联评论提交的处理逻辑
from . import views
# 赵瑞萍:定义应用命名空间"comments"避免多应用间URL名称冲突
# 模板中引用格式:{% url 'comments:postcomment' article_id %}
app_name = "comments"
# 赵瑞萍URL模式列表存储该应用的所有路由规则
urlpatterns = [
# 赵瑞萍:评论提交路由,用于处理用户发布/回复评论的请求
path(
'article/<int:article_id>/postcomment', # URL路径包含整数类型的文章ID参数article_id
# 关联视图类将CommentPostView类视图转换为可调用的视图函数
views.CommentPostView.as_view(),
name='postcomment' # URL名称用于反向解析如reverse('comments:postcomment')
),
]

@ -1,73 +0,0 @@
# 赵瑞萍:评论邮件通知模块,用于评论提交后发送邮件通知
# 核心功能:向评论者发送感谢邮件,向被回复者发送评论回复通知,支持多语言和链接跳转
# 赵瑞萍:导入日志模块,记录邮件发送过程中的信息和异常
import logging
# 赵瑞萍导入Django国际化翻译工具实现邮件内容多语言支持
from django.utils.translation import gettext_lazy as _
# 赵瑞萍:导入项目工具函数,获取站点信息和发送邮件
from djangoblog.utils import get_current_site # 获取当前站点域名,用于构建文章链接
from djangoblog.utils import send_email # 项目封装的邮件发送函数
# 赵瑞萍:创建当前模块的日志记录器,命名为当前模块名,便于日志定位
logger = logging.getLogger(__name__)
def send_comment_email(comment):
"""
赵瑞萍发送评论相关邮件通知的核心函数
分两种场景发送邮件
1. 向当前评论的发布者发送感谢评论邮件
2. 若当前评论是回复其他评论有父评论向被回复者发送回复通知邮件
参数comment - 已保存的Comment模型实例包含评论作者关联文章父评论等信息
"""
# 赵瑞萍获取当前站点的域名如www.example.com用于构建完整的文章访问链接
site = get_current_site().domain
# 赵瑞萍:邮件主题(支持多语言,根据项目语言配置自动切换)
subject = _('Thanks for your comment')
# 赵瑞萍构建评论所属文章的完整URLHTTPS协议+域名+文章绝对路径)
article_url = f"https://{site}{comment.article.get_absolute_url()}"
# 赵瑞萍构建给评论者的感谢邮件内容HTML格式支持超链接
# 使用字符串格式化替换占位符注入文章URL和标题
html_content = _("""<p>Thank you very much for your comments on this site</p>
You can visit <a href="%(article_url)s" rel="bookmark">%(article_title)s</a>
to review your comments,
Thank you again!
<br />
If the link above cannot be opened, please copy this link to your browser.
%(article_url)s""") % {
'article_url': article_url, # 文章完整访问链接
'article_title': comment.article.title # 评论所属文章的标题
}
# 赵瑞萍:获取当前评论者的邮箱地址(从评论作者关联的用户模型中获取)
tomail = comment.author.email
# 赵瑞萍:调用邮件发送函数,向评论者发送感谢邮件
send_email([tomail], subject, html_content)
try:
# 赵瑞萍:判断当前评论是否有父评论(即是否是对其他评论的回复)
if comment.parent_comment:
# 赵瑞萍构建给被回复者的邮件内容HTML格式告知其评论收到回复
html_content = _("""Your comment on <a href="%(article_url)s" rel="bookmark">%(article_title)s</a><br/> has
received a reply. <br/> %(comment_body)s
<br/>
go check it out!
<br/>
If the link above cannot be opened, please copy this link to your browser.
%(article_url)s
""") % {
'article_url': article_url, # 文章完整访问链接
'article_title': comment.article.title, # 文章标题
'comment_body': comment.parent_comment.body # 被回复的原评论内容
}
# 赵瑞萍:获取被回复评论者的邮箱地址(父评论的作者邮箱)
tomail = comment.parent_comment.author.email
# 赵瑞萍:发送回复通知邮件给被回复者
send_email([tomail], subject, html_content)
except Exception as e:
# 赵瑞萍:捕获邮件发送过程中的所有异常,记录错误日志(不中断程序执行)
logger.error(e)

@ -1,115 +0,0 @@
# 赵瑞萍:评论提交视图模块,用于处理用户评论发布、数据验证、业务逻辑处理及响应返回
# 基于Django FormView实现支持CSRF保护、表单验证、评论状态控制及回复功能处理
# 赵瑞萍导入Django核心组件提供异常处理、响应、数据查询及装饰器支持
from django.core.exceptions import ValidationError # 数据验证异常,用于抛出评论相关业务错误
from django.http import HttpResponseRedirect # 重定向响应类,用于评论成功后跳转
from django.shortcuts import get_object_or_404 # 安全查询数据不存在则返回404
from django.utils.decorators import method_decorator # 为类视图方法添加装饰器
from django.views.decorators.csrf import csrf_protect # CSRF保护装饰器防止跨站请求伪造
from django.views.generic.edit import FormView # 表单处理通用类视图,简化表单逻辑
# 赵瑞萍:导入关联模型和表单,支撑评论业务数据处理
from accounts.models import BlogUser # 自定义用户模型,用于关联评论作者
from blog.models import Article # 文章模型,评论需关联具体文章
from .forms import CommentForm # 评论表单,用于前端输入验证
from .models import Comment # 评论模型,用于数据存储
class CommentPostView(FormView):
"""
赵瑞萍评论提交处理类视图继承FormView封装表单处理流程
核心功能接收评论提交请求验证数据合法性处理评论保存逻辑返回对应响应
支持场景普通评论发布评论回复评论审核状态控制CSRF防护
"""
# 赵瑞萍指定表单类为CommentForm用于数据验证和字段映射
form_class = CommentForm
# 赵瑞萍:指定模板为文章详情页,用于表单错误时重新渲染页面并显示错误
template_name = 'blog/article_detail.html'
@method_decorator(csrf_protect)
def dispatch(self, *args, **kwargs):
"""
赵瑞萍重写dispatch方法添加CSRF保护
通过method_decorator将csrf_protect装饰器应用到请求分发流程确保所有请求经过CSRF验证
防止跨站请求伪造攻击保护评论提交接口安全
"""
# 赵瑞萍调用父类dispatch方法维持原有请求分发逻辑
return super(CommentPostView, self).dispatch(*args, **kwargs)
def get(self, request, *args, **kwargs):
"""
赵瑞萍处理GET请求避免直接通过URL访问评论提交接口
当用户以GET方式访问时重定向到对应文章详情页的评论区
"""
# 赵瑞萍从URL参数中获取文章ID确定评论所属文章
article_id = self.kwargs['article_id']
# 赵瑞萍查询文章对象不存在则返回404
article = get_object_or_404(Article, pk=article_id)
# 赵瑞萍获取文章绝对URL拼接评论区锚点重定向到评论区
url = article.get_absolute_url()
return HttpResponseRedirect(url + "#comments")
def form_invalid(self, form):
"""
赵瑞萍表单数据验证失败时的处理方法
当用户输入不符合规则如评论为空长度超限重新渲染文章详情页并携带错误信息
"""
# 赵瑞萍从URL参数获取文章ID关联当前评论的文章
article_id = self.kwargs['article_id']
# 赵瑞萍:查询文章对象,确保页面渲染时有文章数据
article = get_object_or_404(Article, pk=article_id)
# 赵瑞萍:返回渲染后的页面,携带包含错误信息的表单和文章对象
return self.render_to_response({
'form': form, # 含错误提示的表单对象,前端可渲染错误信息
'article': article # 文章对象,用于页面展示文章内容
})
def form_valid(self, form):
"""
赵瑞萍表单数据验证通过后的核心业务逻辑处理
完成评论对象的构建关联数据设置业务规则校验及保存最后重定向到评论位置
"""
# 赵瑞萍:获取当前登录用户对象(需登录才能评论)
user = self.request.user
# 赵瑞萍根据用户ID查询BlogUser实例作为评论作者
author = BlogUser.objects.get(pk=user.pk)
# 赵瑞萍从URL参数获取文章ID确定评论所属文章
article_id = self.kwargs['article_id']
# 赵瑞萍查询文章对象不存在则返回404
article = get_object_or_404(Article, pk=article_id)
# 赵瑞萍:业务规则校验——检查文章是否允许评论
# 若文章评论状态为'c'(关闭)或文章状态为'c'(关闭),抛出验证异常
if article.comment_status == 'c' or article.status == 'c':
raise ValidationError("该文章评论已关闭.")
# 赵瑞萍保存表单数据但不提交到数据库commit=False预留字段补充空间
comment = form.save(False)
comment.article = article # 关联评论到对应的文章
# 赵瑞萍:获取博客系统设置,判断评论是否需要审核
from djangoblog.utils import get_blog_setting
settings = get_blog_setting()
# 赵瑞萍:若无需审核,直接设置评论为启用状态(可立即显示)
if not settings.comment_need_review:
comment.is_enable = True
comment.author = author # 关联评论到作者
# 赵瑞萍处理评论回复功能——判断是否存在父评论ID
if form.cleaned_data['parent_comment_id']:
# 赵瑞萍根据父评论ID查询父评论对象建立回复关联
parent_comment = Comment.objects.get(
pk=form.cleaned_data['parent_comment_id'])
comment.parent_comment = parent_comment # 关联当前评论到父评论
# 赵瑞萍:最终将评论数据提交到数据库保存
comment.save(True)
# 赵瑞萍:重定向到文章详情页中当前评论的锚点位置,方便用户查看自己的评论
return HttpResponseRedirect(
"%s#div-comment-%d" %
(article.get_absolute_url(), comment.pk) # 拼接文章URL和评论锚点如#div-comment-1
)

@ -1,109 +0,0 @@
#gq:
# 从 Django 内置的 admin 模块导入 AdminSite 基类
from django.contrib.admin import AdminSite
# 导入 LogEntry 模型,用于记录管理员操作日志
from django.contrib.admin.models import LogEntry
# 导入 Site 模型及其默认的 Admin 配置
from django.contrib.sites.admin import SiteAdmin
from django.contrib.sites.models import Site
# 批量导入各个自定义 App 的 Admin 配置和模型
# 这种星号(*)导入方式在项目规模较小时很方便,但大型项目中可能影响代码可读性
from accounts.admin import *
from blog.admin import *
from blog.models import *
from comments.admin import *
from comments.models import *
# 导入自定义的 LogEntryAdmin用于自定义操作日志的后台显示
from djangoblog.logentryadmin import LogEntryAdmin
from oauth.admin import *
from oauth.models import *
from owntracks.admin import *
from owntracks.models import *
from servermanager.admin import *
from servermanager.models import *
class DjangoBlogAdminSite(AdminSite):
"""
自定义的 Admin 站点类继承自 Django AdminSite
用于定制 Admin 后台的外观和行为
"""
# 定制 Admin 后台顶部的标题
site_header = 'djangoblog administration'
# 定制浏览器标签页上的标题
site_title = 'djangoblog site admin'
def __init__(self, name='admin'):
"""
初始化方法
:param name: 站点的名称默认是 'admin'这会影响 URL 反向解析等
"""
super().__init__(name)
def has_permission(self, request):
"""
重写权限检查方法
这个方法决定了一个请求是否有权限访问 Admin 后台
:param request: 当前的 HTTP 请求对象
:return: Boolean表示是否允许访问
"""
# 只有超级用户(superuser)才能访问这个自定义的 Admin 站点
# 这是一个比默认更严格的权限控制
return request.user.is_superuser
# def get_urls(self):
# """
# (已注释)重写 get_urls 方法来添加自定义的 URL 路由。
# 这是一个示例,展示了如何在 Admin 后台中加入自己的视图。
# """
# # 先获取父类的所有 URL
# urls = super().get_urls()
# from django.urls import path
# # 导入一个自定义的视图函数,用于刷新缓存
# from blog.views import refresh_memcache
#
# # 定义自己的 URL 模式
# my_urls = [
# # 使用 self.admin_view() 包装自定义视图,以确保它受到 Admin 权限保护
# path('refresh/', self.admin_view(refresh_memcache), name="refresh"),
# ]
# # 返回合并后的 URL 列表
# return urls + my_urls
# 创建一个自定义 Admin 站点的实例
# 这个实例将被用于注册所有的模型
admin_site = DjangoBlogAdminSite(name='admin')
# --- 开始注册各个 App 的模型到自定义的 admin_site ---
# 注册 blog App 的模型
admin_site.register(Article, ArticlelAdmin)
admin_site.register(Category, CategoryAdmin)
admin_site.register(Tag, TagAdmin)
admin_site.register(Links, LinksAdmin)
admin_site.register(SideBar, SideBarAdmin)
admin_site.register(BlogSettings, BlogSettingsAdmin)
# 注册 servermanager App 的模型
admin_site.register(commands, CommandsAdmin)
admin_site.register(EmailSendLog, EmailSendLogAdmin)
# 注册 accounts App 的模型
admin_site.register(BlogUser, BlogUserAdmin)
# 注册 comments App 的模型
admin_site.register(Comment, CommentAdmin)
# 注册 oauth App 的模型
admin_site.register(OAuthUser, OAuthUserAdmin)
admin_site.register(OAuthConfig, OAuthConfigAdmin)
# 注册 owntracks App 的模型
admin_site.register(OwnTrackLog, OwnTrackLogsAdmin)
# 注册 Django 内置的 Site 模型
admin_site.register(Site, SiteAdmin)
# 注册 Django 内置的 LogEntry 模型,使用自定义的 LogEntryAdmin 配置
admin_site.register(LogEntry, LogEntryAdmin)

@ -1,14 +0,0 @@
#gq:
from django.apps import AppConfig
class DjangoblogAppConfig(AppConfig):
# 默认主键类型BigAutoField自增 BigInteger避免ID溢出
default_auto_field = 'django.db.models.BigAutoField'
# 应用唯一标识与settings.py中INSTALLED_APPS配置一致
name = 'djangoblog'
def ready(self):
super().ready() # 执行父类启动逻辑
# 应用启动时自动加载所有插件
from .plugin_manage.loader import load_plugins
load_plugins()

@ -1,49 +0,0 @@
#gq:
from django.contrib.auth import get_user_model
from django.contrib.syndication.views import Feed # Django内置RSS/Atom订阅生成视图
from django.utils import timezone
from django.utils.feedgenerator import Rss201rev2Feed # RSS 2.0标准生成器
from blog.models import Article # 博客文章模型
from djangoblog.utils import CommonMarkdown # Markdown解析工具将Markdown转为HTML
class DjangoBlogFeed(Feed):
"""博客RSS订阅Feed类生成符合RSS 2.0标准的订阅内容"""
feed_type = Rss201rev2Feed # 指定Feed类型为RSS 2.0
description = '大巧无工,重剑无锋.' # Feed描述
title = "且听风吟 大巧无工,重剑无锋. " # Feed标题订阅列表中显示
link = "/feed/" # Feed的URL地址
def author_name(self):
"""Feed作者名称取系统第一个用户的昵称"""
return get_user_model().objects.first().nickname
def author_link(self):
"""Feed作者链接取系统第一个用户的个人主页URL"""
return get_user_model().objects.first().get_absolute_url()
def items(self):
"""Feed订阅的内容列表最新5篇已发布status='p'的文章type='a'"""
return Article.objects.filter(type='a', status='p').order_by('-pub_time')[:5]
def item_title(self, item):
"""单个订阅项(文章)的标题:使用文章标题"""
return item.title
def item_description(self, item):
"""单个订阅项的描述将文章Markdown正文转为HTML"""
return CommonMarkdown.get_markdown(item.body)
def feed_copyright(self):
"""Feed版权信息显示当前年份的版权声明"""
now = timezone.now()
return "Copyright© {year} 且听风吟".format(year=now.year)
def item_link(self, item):
"""单个订阅项的链接文章详情页URL"""
return item.get_absolute_url()
def item_guid(self, item):
"""单个订阅项的唯一标识(预留方法,暂未实现)"""
return

@ -1,66 +0,0 @@
#gq:
from django.contrib import admin
from django.contrib.admin.models import DELETION
from django.contrib.contenttypes.models import ContentType
from django.urls import reverse, NoReverseMatch
from django.utils.encoding import force_str
from django.utils.html import escape
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
class LogEntryAdmin(admin.ModelAdmin):
"""Admin操作日志自定义管理类优化展示与权限控制"""
list_filter = ['content_type'] # 按内容类型筛选
search_fields = ['object_repr', 'change_message'] # 搜索对象描述、操作信息
list_display_links = ['action_time', 'get_change_message'] # 可点击跳转字段
list_display = ['action_time', 'user_link', 'content_type', 'object_link', 'get_change_message'] # 列表展示字段
def has_add_permission(self, request):
"""禁用添加:日志自动生成,不允许手动添加"""
return False
def has_change_permission(self, request, obj=None):
"""仅超级用户/有权限用户可查看禁止POST修改"""
return (request.user.is_superuser or request.user.has_perm('admin.change_logentry')) and request.method != 'POST'
def has_delete_permission(self, request, obj=None):
"""禁用删除:日志需保留"""
return False
def object_link(self, obj):
"""操作对象字段非删除操作显示Admin编辑链接"""
object_link = escape(obj.object_repr)
if obj.action_flag != DELETION and obj.content_type:
try:
url = reverse(f'admin:{obj.content_type.app_label}_{obj.content_type.model}_change', args=[obj.object_id])
object_link = f'<a href="{url}">{object_link}</a>'
except NoReverseMatch:
pass
return mark_safe(object_link)
object_link.admin_order_field = 'object_repr'
object_link.short_description = _('object')
def user_link(self, obj):
"""操作用户字段显示用户Admin编辑链接"""
content_type = ContentType.objects.get_for_model(type(obj.user))
user_link = escape(force_str(obj.user))
try:
url = reverse(f'admin:{content_type.app_label}_{content_type.model}_change', args=[obj.user.pk])
user_link = f'<a href="{url}">{user_link}</a>'
except NoReverseMatch:
pass
return mark_safe(user_link)
user_link.admin_order_field = 'user'
user_link.short_description = _('user')
def get_queryset(self, request):
"""预加载content_type优化查询性能"""
return super().get_queryset(request).prefetch_related('content_type')
def get_actions(self, request):
"""移除批量删除操作"""
actions = super().get_actions(request)
del actions['delete_selected']
return actions

@ -1,30 +0,0 @@
#gq:
import os
import logging
from django.conf import settings
# 获取当前模块的日志记录器,用于输出插件加载过程中的日志
logger = logging.getLogger(__name__)
def load_plugins():
"""
动态加载并初始化 plugins 目录下的插件
应在 Django App Registry 完全就绪后调用 AppConfig.ready()
遍历 settings.ACTIVE_PLUGINS 列表依次导入每个插件的 plugin.py 模块
"""
# 遍历配置中启用的插件列表
for plugin_name in settings.ACTIVE_PLUGINS:
# 拼接插件目录绝对路径
plugin_path = os.path.join(settings.PLUGINS_DIR, plugin_name)
# 仅当目录存在且目录下包含 plugin.py 文件时才尝试导入
if os.path.isdir(plugin_path) and os.path.exists(os.path.join(plugin_path, 'plugin.py')):
try:
# 使用 __import__ 动态导入插件模块
# 导入路径示例plugins.<plugin_name>.plugin
__import__(f'plugins.{plugin_name}.plugin')
logger.info(f"Successfully loaded plugin: {plugin_name}")
except ImportError as e:
# 导入失败时记录错误日志,包含异常堆栈
logger.error(f"Failed to import plugin: {plugin_name}", exc_info=e)

@ -1,70 +0,0 @@
#gq:
from django.contrib.sitemaps import Sitemap # Django内置站点地图生成类
from django.urls import reverse # URL反向解析
from blog.models import Article, Category, Tag # 导入博客相关模型
class StaticViewSitemap(Sitemap):
"""静态页面站点地图:如首页"""
priority = 0.5 # 页面优先级0.0-1.0
changefreq = 'daily' # 内容更新频率
def items(self):
"""返回要包含的静态页面名称列表对应URL名称"""
return ['blog:index', ]
def location(self, item):
"""返回每个静态页面的URL"""
return reverse(item)
class ArticleSiteMap(Sitemap):
"""文章页面站点地图"""
changefreq = "monthly" # 文章更新频率
priority = "0.6" # 文章页面优先级
def items(self):
"""返回所有已发布status='p')的文章"""
return Article.objects.filter(status='p')
def lastmod(self, obj):
"""返回文章的最后修改时间"""
return obj.last_modify_time
class CategorySiteMap(Sitemap):
"""分类页面站点地图"""
changefreq = "Weekly" # 分类更新频率
priority = "0.6" # 分类页面优先级
def items(self):
"""返回所有分类"""
return Category.objects.all()
def lastmod(self, obj):
"""返回分类的最后修改时间"""
return obj.last_modify_time
class TagSiteMap(Sitemap):
"""标签页面站点地图"""
changefreq = "Weekly" # 标签更新频率
priority = "0.3" # 标签页面优先级(低于文章和分类)
def items(self):
"""返回所有标签"""
return Tag.objects.all()
def lastmod(self, obj):
"""返回标签的最后修改时间"""
return obj.last_modify_time
class UserSiteMap(Sitemap):
"""用户页面站点地图"""
changefreq = "Weekly" # 用户信息更新频率
priority = "0.3" # 用户页面优先级
def items(self):
"""返回所有发布过文章的不重复作者列表"""
return list(set(map(lambda x: x.author, Article.objects.all())))
def lastmod(self, obj):
"""返回用户的注册时间(作为站点地图的最后更新时间)"""
return obj.date_joined

@ -1,26 +0,0 @@
#gq:
import logging
import requests # HTTP请求库用于向搜索引擎提交URL
from django.conf import settings # 导入Django项目配置
logger = logging.getLogger(__name__) # 初始化日志对象
class SpiderNotify():
"""搜索引擎爬虫通知类向百度等搜索引擎提交新URL加速收录"""
@staticmethod
def baidu_notify(urls):
"""向百度搜索引擎提交URL列表请求收录"""
try:
# 格式化URL数据每行一个URL百度要求的提交格式
data = '\n'.join(urls)
# 发送POST请求到百度收录接口配置在settings.BAIDU_NOTIFY_URL
result = requests.post(settings.BAIDU_NOTIFY_URL, data=data)
logger.info(result.text) # 记录提交结果
except Exception as e:
logger.error(e) # 记录提交失败异常
@staticmethod
def notify(url):
"""通用通知方法:调用百度收录提交(可扩展支持其他搜索引擎)"""
SpiderNotify.baidu_notify(url)

@ -1,76 +0,0 @@
#gq:
"""djangoblog URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/1.10/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: url(r'^$', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.conf.urls import url, include
2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
"""
from django.conf import settings
from django.conf.urls.i18n import i18n_patterns
from django.conf.urls.static import static
from django.contrib.sitemaps.views import sitemap
from django.urls import path, include, re_path
from haystack.views import search_view_factory
from blog.views import EsSearchView
from djangoblog.admin_site import admin_site
from djangoblog.elasticsearch_backend import ElasticSearchModelSearchForm
from djangoblog.feeds import DjangoBlogFeed
from djangoblog.sitemap import (
ArticleSiteMap, CategorySiteMap, StaticViewSitemap, TagSiteMap, UserSiteMap
)
# 站点地图配置
sitemaps = {
'blog': ArticleSiteMap, # 文章
'Category': CategorySiteMap, # 分类
'Tag': TagSiteMap, # 标签
'User': UserSiteMap, # 用户
'static': StaticViewSitemap # 静态页面
}
# 自定义错误页面
handler404 = 'blog.views.page_not_found_view' # 404
handler500 = 'blog.views.server_error_view' # 500
handle403 = 'blog.views.permission_denied_view'# 403
urlpatterns = [
path('i18n/', include('django.conf.urls.i18n')), # 国际化
]
# 国际化URL多语言支持
urlpatterns += i18n_patterns(
re_path(r'^admin/', admin_site.urls), # 后台管理
re_path(r'', include('blog.urls', namespace='blog')), # 博客主体
re_path(r'mdeditor/', include('mdeditor.urls')), # Markdown编辑器
re_path(r'', include('comments.urls', namespace='comment')), # 评论
re_path(r'', include('accounts.urls', namespace='account')), # 账户
re_path(r'', include('oauth.urls', namespace='oauth')), # 第三方登录
re_path(r'^sitemap\.xml$', sitemap, {'sitemaps': sitemaps}), # 站点地图
re_path(r'^feed/$', DjangoBlogFeed()), # RSS订阅
re_path(r'^rss/$', DjangoBlogFeed()), # RSS订阅(备用)
# Elasticsearch搜索
re_path('^search', search_view_factory(
view_class=EsSearchView,
form_class=ElasticSearchModelSearchForm
), name='search'),
re_path(r'', include('servermanager.urls', namespace='servermanager')), # 服务器管理
re_path(r'', include('owntracks.urls', namespace='owntracks')), # 位置跟踪
prefix_default_language=False
)
# 静态文件URL
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
# 开发环境媒体文件URL
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

@ -1,210 +0,0 @@
#gq:
#!/usr/bin/env python
# encoding: utf-8
import logging
import os
import random
import string
import uuid
from hashlib import sha256
from urllib.parse import quote
import bleach
import markdown
import requests
from django.conf import settings
from django.contrib.sites.models import Site
from django.core.cache import cache
from django.core.cache.utils import make_template_fragment_key
from django.http import HttpRequest
from django.templatetags.static import static
from django.utils.cache import get_cache_key
logger = logging.getLogger(__name__)
def get_max_articleid_commentid():
"""获取最新文章和评论的ID"""
from blog.models import Article
from comments.models import Comment
return (Article.objects.latest().pk, Comment.objects.latest().pk)
def get_sha256(str):
"""计算字符串的SHA-256哈希值"""
m = sha256(str.encode('utf-8'))
return m.hexdigest()
def cache_decorator(expiration=3 * 60):
"""函数缓存装饰器默认缓存3分钟"""
def wrapper(func):
def news(*args, **kwargs):
try:
# 尝试从请求对象获取缓存键
view = args[0]
key = view.get_cache_key()
except:
# 否则根据函数和参数生成唯一键
unique_str = repr((func, args, kwargs))
key = get_sha256(unique_str)
value = cache.get(key)
if value is not None:
# 返回缓存值,处理空值标记
return None if str(value) == '__default_cache_value__' else value
# 缓存未命中,执行函数并缓存结果
logger.debug(f'cache_decorator set cache:{func.__name__} key:{key}')
value = func(*args, **kwargs)
cache.set(key, value if value is not None else '__default_cache_value__', expiration)
return value
return news
return wrapper
def expire_view_cache(path, servername, serverport, key_prefix=None):
"""刷新指定URL的视图缓存"""
request = HttpRequest()
request.META = {'SERVER_NAME': servername, 'SERVER_PORT': serverport}
request.path = path
key = get_cache_key(request, key_prefix=key_prefix, cache=cache)
if key:
logger.info(f'expire_view_cache:get key:{path}')
cache.delete(key)
return True
return False
@cache_decorator()
def get_current_site():
"""获取当前站点信息(带缓存)"""
return Site.objects.get_current()
class CommonMarkdown:
"""Markdown解析工具类"""
@staticmethod
def _convert_markdown(value):
"""内部方法执行Markdown转换返回HTML和目录"""
md = markdown.Markdown(extensions=['extra', 'codehilite', 'toc', 'tables'])
return md.convert(value), md.toc
@staticmethod
def get_markdown_with_toc(value):
"""转换Markdown为HTML含目录"""
return CommonMarkdown._convert_markdown(value)
@staticmethod
def get_markdown(value):
"""转换Markdown为HTML不含目录"""
body, _ = CommonMarkdown._convert_markdown(value)
return body
def send_email(emailto, title, content):
"""发送邮件(通过信号解耦)"""
from djangoblog.blog_signals import send_email_signal
send_email_signal.send(send_email.__class__, emailto=emailto, title=title, content=content)
def generate_code() -> str:
"""生成6位随机数字验证码"""
return ''.join(random.sample(string.digits, 6))
def parse_dict_to_url(dict):
"""将字典转换为URL查询字符串"""
return '&'.join([f'{quote(k, safe="/")}={quote(v, safe="/")}' for k, v in dict.items()])
def get_blog_setting():
"""获取博客系统设置(带缓存,无数据时初始化)"""
value = cache.get('get_blog_setting')
if value:
return value
from blog.models import BlogSettings
if not BlogSettings.objects.count():
# 初始化默认设置
setting = BlogSettings(
site_name='djangoblog',
site_description='基于Django的博客系统',
site_seo_description='基于Django的博客系统',
site_keywords='Django,Python',
article_sub_length=300,
sidebar_article_count=10,
sidebar_comment_count=5,
show_google_adsense=False,
open_site_comment=True,
analytics_code='',
beian_code='',
show_gongan_code=False,
comment_need_review=False
)
setting.save()
value = BlogSettings.objects.first()
cache.set('get_blog_setting', value)
return value
def save_user_avatar(url):
"""下载并保存用户头像到本地返回静态文件URL"""
try:
basedir = os.path.join(settings.STATICFILES, 'avatar')
rsp = requests.get(url, timeout=2)
if rsp.status_code == 200:
os.makedirs(basedir, exist_ok=True)
# 确定文件扩展名
ext = os.path.splitext(url)[1] if any(
url.endswith(ext) for ext in ['.jpg', '.png', 'jpeg', '.gif']) else '.jpg'
save_filename = f'{uuid.uuid4().hex}{ext}'
with open(os.path.join(basedir, save_filename), 'wb+') as file:
file.write(rsp.content)
return static(f'avatar/{save_filename}')
except Exception as e:
logger.error(e)
return static('blog/img/avatar.png') # 返回默认头像
def delete_sidebar_cache():
"""删除侧边栏相关缓存"""
from blog.models import LinkShowType
keys = [f"sidebar{x}" for x in LinkShowType.values]
for k in keys:
logger.info(f'delete sidebar key:{k}')
cache.delete(k)
def delete_view_cache(prefix, keys):
"""删除指定模板片段缓存"""
key = make_template_fragment_key(prefix, keys)
cache.delete(key)
def get_resource_url():
"""获取静态资源基础URL"""
if settings.STATIC_URL:
return settings.STATIC_URL
site = get_current_site()
return f'http://{site.domain}/static/'
# HTML清理配置
ALLOWED_TAGS = ['a', 'abbr', 'acronym', 'b', 'blockquote', 'code', 'em', 'i', 'li', 'ol', 'pre', 'strong', 'ul', 'h1',
'h2', 'p']
ALLOWED_ATTRIBUTES = {'a': ['href', 'title'], 'abbr': ['title'], 'acronym': ['title']}
def sanitize_html(html):
"""清理HTML只保留允许的标签和属性"""
return bleach.clean(html, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRIBUTES)

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Before

Width:  |  Height:  |  Size: 129 KiB

After

Width:  |  Height:  |  Size: 129 KiB

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

@ -1,22 +0,0 @@
from django.contrib.auth.forms import forms
from django.forms import widgets
# 定义一个表单类,用于在 OAuth 登录时补充或验证邮箱信息
class RequireEmailForm(forms.Form):
# 邮箱字段,必填
email = forms.EmailField(label='电子邮箱', required=True)
# OAuth 用户 ID隐藏字段用于在后台提交时识别用户
oauthid = forms.IntegerField(widget=forms.HiddenInput, required=False)
# 构造函数,用来自定义表单字段的显示样式
def __init__(self, *args, **kwargs):
# 调用父类初始化方法
super(RequireEmailForm, self).__init__(*args, **kwargs)
# 自定义 email 输入框的 HTML 样式与属性
self.fields['email'].widget = widgets.EmailInput(
attrs={
'placeholder': "email", # 输入框占位提示文字
"class": "form-control" # Bootstrap 样式类,统一表单外观
}
)

@ -1,77 +0,0 @@
#gq:
# 导入正则表达式模块用于匹配和处理HTML中的<a>标签
import re
# 导入URL解析模块用于解析链接的域名等信息
from urllib.parse import urlparse
# 导入Django博客系统的插件基类当前插件需继承此类实现标准化功能
from djangoblog.plugin_manage.base_plugin import BasePlugin
# 导入插件钩子管理模块,用于注册和触发插件功能
from djangoblog.plugin_manage import hooks
# 导入文章内容钩子常量,指定插件要作用的具体钩子位置
from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME
# 定义外部链接处理插件类继承自插件基类BasePlugin
class ExternalLinksPlugin(BasePlugin):
# 插件名称,用于在插件管理界面展示
PLUGIN_NAME = '外部链接处理器'
# 插件功能描述,说明插件的核心作用
PLUGIN_DESCRIPTION = '自动为文章中的外部链接添加 target="_blank" 和 rel="noopener noreferrer" 属性。'
# 插件版本号,用于版本管理和更新识别
PLUGIN_VERSION = '0.1.0'
# 插件作者信息
PLUGIN_AUTHOR = 'liangliangyy'
# 注册插件钩子的方法,插件加载时会自动调用
def register_hooks(self):
# 将当前插件的process_external_links方法注册到文章内容处理钩子上
# 意味着文章内容渲染前,会自动执行该方法处理链接
hooks.register(ARTICLE_CONTENT_HOOK_NAME, self.process_external_links)
# 核心方法:处理文章内容中的外部链接
# 参数content为文章原始HTML内容*args和**kwargs用于接收额外参数预留扩展性
def process_external_links(self, content, *args, **kwargs):
# 导入Django工具函数用于获取当前网站的域名如example.com
from djangoblog.utils import get_current_site
# 获取当前网站的域名,用于判断链接是否为"外部链接"
site_domain = get_current_site().domain
# 定义正则表达式用于匹配HTML中的<a>标签
# 匹配规则:<a 开头 + 任意属性(可选) + href=" + 链接地址 + " + 剩余属性及闭合标签
# re.IGNORECASE表示忽略大小写如<A>和<a>都能匹配)
link_pattern = re.compile(r'(<a\s+(?:[^>]*?\s+)?href=")([^"]*)(".*?/a>)', re.IGNORECASE)
# 定义正则替换的回调函数,每匹配到一个<a>标签就会执行一次
def replacer(match):
# 拆分匹配结果为3个分组
# group(1)<a 到 href=" 的部分
# group(2)href属性中的链接地址
# group(3)href=" 之后到 </a> 的部分
href = match.group(2)
# 检查当前<a>标签是否已包含target属性不区分大小写如Target或TARGET
# 若已存在target属性则不做处理直接返回原标签
if 'target=' in match.group(0).lower():
return match.group(0)
# 解析当前链接地址,获取其域名、路径等结构化信息
parsed_url = urlparse(href)
# 判断链接是否为外部链接:
# 1. parsed_url.netloc不为空排除无域名的链接如相对路径 /about
# 2. 链接的域名netloc不等于当前网站域名site_domain
if parsed_url.netloc and parsed_url.netloc != site_domain:
# 若为外部链接在href闭合后添加 target="_blank"(新窗口打开)
# 和 rel="noopener noreferrer"(安全属性,防止窗口劫持)
return f'{match.group(1)}{href}" target="_blank" rel="noopener noreferrer"{match.group(3)}'
# 若为内部链接(或无域名链接),不做修改,直接返回原标签
return match.group(0)
# 使用正则表达式替换文章内容中的所有<a>标签执行replacer回调函数
# 返回处理后的文章内容
return link_pattern.sub(replacer, content)
# 实例化插件类,使插件系统能识别并加载该插件
plugin = ExternalLinksPlugin()

@ -1,70 +0,0 @@
#gq:
# 导入数学运算模块,用于向上取整计算
import math
# 导入正则表达式模块用于处理HTML内容和文本分词
import re
# 导入Django博客插件基类当前插件需继承此类
from djangoblog.plugin_manage.base_plugin import BasePlugin
# 导入插件钩子管理模块,用于注册插件功能
from djangoblog.plugin_manage import hooks
# 导入文章内容钩子常量,指定插件作用的位置
from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME
class ReadingTimePlugin(BasePlugin):
# 插件名称,用于插件管理界面展示
PLUGIN_NAME = '阅读时间预测'
# 插件功能描述,说明核心作用
PLUGIN_DESCRIPTION = '估算文章阅读时间并显示在文章开头。'
# 插件版本号
PLUGIN_VERSION = '0.1.0'
# 插件作者信息
PLUGIN_AUTHOR = 'liangliangyy'
def register_hooks(self):
"""注册插件钩子,将处理方法绑定到文章内容钩子"""
# 当文章内容被渲染时触发add_reading_time方法
hooks.register(ARTICLE_CONTENT_HOOK_NAME, self.add_reading_time)
def add_reading_time(self, content, *args, **kwargs):
"""
计算文章阅读时间并添加到内容开头
参数:
content: 文章原始HTML内容
*args, **kwargs: 预留参数用于接收额外信息
返回:
添加了阅读时间信息的HTML内容
"""
# 1. 清理内容使用正则移除所有HTML标签<...>格式),保留纯文本
clean_content = re.sub(r'<[^>]*>', '', content)
# 移除文本前后的空白字符(空格、换行等)
clean_content = clean_content.strip()
# 2. 计数单词/字符:
# 正则匹配规则:匹配单个中文字符([\u4e00-\u9fa5])或连续的非中文字符(视为英文单词,\w+
# 这样处理中英文混合的内容,兼顾两种语言的计数逻辑
words = re.findall(r'[\u4e00-\u9fa5]|\w+', clean_content)
# 统计匹配到的元素数量(中文按单个字算,英文按单词算)
word_count = len(words)
# 3. 计算阅读时间:
# 设定平均阅读速度为每分钟200字中英文通用的经验值
reading_speed = 200
# 总字数除以阅读速度,向上取整得到分钟数
reading_minutes = math.ceil(word_count / reading_speed)
# 处理边界情况如果计算结果小于1分钟强制显示为1分钟
if reading_minutes < 1:
reading_minutes = 1
# 4. 生成阅读时间的HTML片段
# 使用浅灰色文字,斜体样式,显示在段落中
reading_time_html = f'<p style="color: #888;"><em>预计阅读时间:{reading_minutes} 分钟</em></p>'
# 将阅读时间信息添加到文章内容开头并返回
return reading_time_html + content
# 实例化插件,使插件系统能够识别并加载
plugin = ReadingTimePlugin()

@ -1,226 +0,0 @@
#gq:
# 导入JSON模块用于将结构化数据转换为JSON格式
import json
# 导入Django工具函数用于移除HTML标签提取纯文本
from django.utils.html import strip_tags
# 导入Django模板过滤器当前未使用预留用于文本截断
from django.template.defaultfilters import truncatewords
# 导入插件基类,所有插件需继承此类实现标准化接口
from djangoblog.plugin_manage.base_plugin import BasePlugin
# 导入插件钩子管理模块,用于注册插件功能到指定钩子
from djangoblog.plugin_manage import hooks
# 导入博客数据模型,用于获取文章、分类、标签等数据
from blog.models import Article, Category, Tag
# 导入工具函数,用于获取博客站点的基础配置(如站点名称、关键词等)
from djangoblog.utils import get_blog_setting
class SeoOptimizerPlugin(BasePlugin):
# 插件名称,用于在插件管理界面展示
PLUGIN_NAME = 'SEO 优化器'
# 插件功能描述,说明插件的核心作用
PLUGIN_DESCRIPTION = '为文章、页面等提供 SEO 优化,动态生成 meta 标签和 JSON-LD 结构化数据。'
# 插件版本号,用于版本管理
PLUGIN_VERSION = '0.2.0'
# 插件作者信息
PLUGIN_AUTHOR = 'liuangliangyy'
def register_hooks(self):
"""注册插件钩子将SEO处理逻辑绑定到页面<head>区域的meta标签生成环节"""
# 当系统渲染<head>中的meta标签时触发dispatch_seo_generation方法
hooks.register('head_meta', self.dispatch_seo_generation)
def _get_article_seo_data(self, context, request, blog_setting):
"""
生成文章详情页的SEO数据私有方法仅内部调用
参数
context模板上下文包含当前页面的文章对象等数据
requestHTTP请求对象用于构建绝对URL
blog_setting博客站点配置信息如站点名称关键词
返回
包含SEO相关数据的字典若上下文无有效文章对象则返回None
"""
# 从上下文获取文章对象
article = context.get('article')
# 校验是否为有效的Article实例避免非文章页误处理
if not isinstance(article, Article):
return None
# 生成描述信息移除文章内容中的HTML标签截取前150字符符合多数搜索引擎的描述长度建议
description = strip_tags(article.body)[:150]
# 生成关键词:优先使用文章标签,若无标签则使用站点默认关键词
keywords = ",".join([tag.name for tag in article.tags.all()]) or blog_setting.site_keywords
# 生成Open GraphOG协议标签用于优化社交平台分享效果
meta_tags = f'''
<meta property="og:type" content="article"/> <!-- 内容类型为文章 -->
<meta property="og:title" content="{article.title}"/> <!-- 社交分享标题 -->
<meta property="og:description" content="{description}"/> <!-- 社交分享描述 -->
<meta property="og:url" content="{request.build_absolute_uri()}"/> <!-- 文章完整URL -->
<meta property="article:published_time" content="{article.pub_time.isoformat()}"/> <!-- 发布时间ISO标准格式 -->
<meta property="article:modified_time" content="{article.last_modify_time.isoformat()}"/> <!-- 最后修改时间 -->
<meta property="article:author" content="{article.author.username}"/> <!-- 作者信息 -->
<meta property="article:section" content="{article.category.name}"/> <!-- 所属分类 -->
'''
# 为文章的每个标签添加OG标签
for tag in article.tags.all():
meta_tags += f'<meta property="article:tag" content="{tag.name}"/>'
# 添加站点名称OG标签关联文章所属站点
meta_tags += f'<meta property="og:site_name" content="{blog_setting.site_name}"/>'
# 生成JSON-LD结构化数据帮助搜索引擎理解页面内容结构
structured_data = {
"@context": "https://schema.org", # 遵循schema.org的结构化数据标准
"@type": "Article", # 内容类型为文章
"mainEntityOfPage": { # 声明页面的主体内容
"@type": "WebPage",
"@id": request.build_absolute_uri() # 页面唯一标识完整URL
},
"headline": article.title, # 文章标题
"description": description, # 文章描述
# 文章首图通过绝对URL访问增强内容丰富度
"image": request.build_absolute_uri(article.get_first_image_url()),
"datePublished": article.pub_time.isoformat(), # 发布时间
"dateModified": article.last_modify_time.isoformat(), # 修改时间
"author": { # 作者信息
"@type": "Person",
"name": article.author.username
},
"publisher": { # 发布机构信息
"@type": "Organization",
"name": blog_setting.site_name
}
}
# 若文章无图片则移除image字段避免空值影响结构化数据有效性
if not structured_data.get("image"):
del structured_data["image"]
# 返回整合后的文章页SEO数据
return {
"title": f"{article.title} | {blog_setting.site_name}", # 页面标题(文章标题+站点名称,增强品牌关联)
"description": description,
"keywords": keywords,
"meta_tags": meta_tags, # 包含OG标签的HTML片段
"json_ld": structured_data # JSON-LD结构化数据
}
def _get_category_seo_data(self, context, request, blog_setting):
"""生成分类页面的SEO数据私有方法"""
# 从上下文获取分类名称变量名tag_name可能为笔误实际应为分类名称
category_name = context.get('tag_name')
if not category_name:
return None
# 根据名称查询分类对象
category = Category.objects.filter(name=category_name).first()
if not category: # 若分类不存在返回None
return None
# 页面标题:分类名称+站点名称
title = f"{category.name} | {blog_setting.site_name}"
# 描述:使用分类名称或站点默认描述
description = strip_tags(category.name) or blog_setting.site_description
# 关键词:使用分类名称
keywords = category.name
# 生成面包屑导航的JSON-LD数据帮助搜索引擎理解页面在站点中的层级位置
breadcrumb_items = [
# 首页面包屑项位置1
{"@type": "ListItem", "position": 1, "name": "首页", "item": request.build_absolute_uri('/')}
]
# 当前分类面包屑项位置2
breadcrumb_items.append({
"@type": "ListItem",
"position": 2,
"name": category.name,
"item": request.build_absolute_uri()
})
structured_data = {
"@context": "https://schema.org",
"@type": "BreadcrumbList", # 类型为面包屑列表
"itemListElement": breadcrumb_items # 面包屑项列表
}
# 返回分类页SEO数据
return {
"title": title,
"description": description,
"keywords": keywords,
"meta_tags": "", # 分类页暂无需额外meta标签
"json_ld": structured_data
}
def _get_default_seo_data(self, context, request, blog_setting):
"""生成默认页面如首页、未匹配的页面的SEO数据私有方法"""
# 生成网站级别的JSON-LD数据包含站点基本信息和搜索功能描述
structured_data = {
"@context": "https://schema.org",
"@type": "WebSite", # 类型为网站
"url": request.build_absolute_uri('/'), # 网站首页URL
# 描述站点的搜索功能(帮助搜索引擎识别并支持站内搜索)
"potentialAction": {
"@type": "SearchAction",
# 搜索结果页URL模板{search_term_string}为搜索关键词占位符)
"target": f"{request.build_absolute_uri('/search/')}?q={{search_term_string}}",
"query-input": "required name=search_term_string" # 声明搜索参数为必填项
}
}
# 返回默认页SEO数据
return {
"title": f"{blog_setting.site_name} | {blog_setting.site_description}", # 首页标题(站点名称+描述)
"description": blog_setting.site_description, # 站点描述
"keywords": blog_setting.site_keywords, # 站点默认关键词
"meta_tags": "", # 默认页无需额外meta标签
"json_ld": structured_data
}
def dispatch_seo_generation(self, metas, context):
"""
分发SEO数据生成逻辑核心方法根据当前页面类型调用对应的数据生成方法
参数
metas原始的meta标签内容未使用预留用于扩展
context模板上下文包含请求对象和页面数据
返回
生成的完整SEO标签包含titlemeta标签JSON-LD脚本等
"""
# 从上下文获取请求对象用于判断页面类型和构建URL
request = context.get('request')
if not request: # 若无请求对象,返回原始内容
return metas
# 获取当前视图的名称通过Django的URL解析器用于区分页面类型
view_name = request.resolver_match.view_name
# 获取博客站点配置
blog_setting = get_blog_setting()
# 根据页面类型视图名称生成对应的SEO数据
seo_data = None
if view_name == 'blog:detailbyid':
# 文章详情页调用文章SEO数据生成方法
seo_data = self._get_article_seo_data(context, request, blog_setting)
elif view_name == 'blog:category_detail':
# 分类详情页调用分类SEO数据生成方法
seo_data = self._get_category_seo_data(context, request, blog_setting)
# 若未匹配到特定页面类型使用默认SEO数据
if not seo_data:
seo_data = self._get_default_seo_data(context, request, blog_setting)
# 生成JSON-LD脚本标签将结构化数据转换为JSON字符串确保非ASCII字符正常显示
json_ld_script = f'<script type="application/ld+json">{json.dumps(seo_data.get("json_ld", {}), ensure_ascii=False, indent=4)}</script>'
# 拼接所有SEO相关标签并返回最终会被插入到页面的<head>区域
return f"""
<title>{seo_data.get("title", "")}</title> <!-- 页面标题SEO核心要素 -->
<meta name="description" content="{seo_data.get("description", "")}"> <!-- 描述标签影响搜索结果展示 -->
<meta name="keywords" content="{seo_data.get("keywords", "")}"> <!-- 关键词标签 -->
{seo_data.get("meta_tags", "")} <!-- 额外meta标签如Open Graph -->
{json_ld_script} <!-- JSON-LD结构化数据脚本提升搜索结果丰富度 -->
"""
# 实例化插件,使插件系统能够识别并加载该插件
plugin = SeoOptimizerPlugin()

@ -1,45 +0,0 @@
#gq:
# 导入Django博客系统的插件基类所有自定义插件需继承此类以实现标准化接口
from djangoblog.plugin_manage.base_plugin import BasePlugin
# 导入插件钩子管理模块,用于将插件功能绑定到系统预设的钩子点
from djangoblog.plugin_manage import hooks
# 定义文章浏览次数统计插件类继承自插件基类BasePlugin
class ViewCountPlugin(BasePlugin):
# 插件名称:在插件管理界面展示,用于区分不同插件
PLUGIN_NAME = '文章浏览次数统计'
# 插件功能描述:说明插件的核心作用,方便管理员理解用途
PLUGIN_DESCRIPTION = '统计文章的浏览次数'
# 插件版本号:用于版本管理,便于后续更新和兼容性判断
PLUGIN_VERSION = '0.1.0'
# 插件作者信息:标注开发者,便于维护和沟通
PLUGIN_AUTHOR = 'liangliangyy'
def register_hooks(self):
"""
注册插件钩子将统计逻辑绑定到系统的特定触发点
作用告诉插件系统在哪个时机执行当前插件的功能
"""
# 绑定规则:
# 1. 'after_article_body_get' 是系统预设的钩子名称,代表“文章内容获取完成后”的时机
# 2. self.record_view 是当前插件的核心方法,即钩子触发时要执行的逻辑
# 场景:当用户访问文章详情页,系统成功获取文章内容后,自动触发浏览次数统计
hooks.register('after_article_body_get', self.record_view)
def record_view(self, article, *args, **kwargs):
"""
核心统计方法执行文章浏览次数的记录操作
参数说明
article钩子传递的文章对象即当前被访问的文章必须是Article模型实例
*args, **kwargs预留参数用于接收钩子传递的额外信息如请求对象等保证扩展性
"""
# 调用文章对象的viewed()方法:
# 该方法应由Article模型预先实现通常逻辑为“将view_count字段+1并保存到数据库”
# 插件通过调用模型方法实现统计,解耦插件与数据模型的直接操作,符合设计规范
article.viewed()
# 实例化插件类:
# 插件系统会扫描并加载该实例,使上述注册的钩子和功能生效
plugin = ViewCountPlugin()

@ -1,48 +0,0 @@
# 马莹:导入微信机器人(werobot)的会话存储基类,用于自定义会话存储方式
from werobot.session import SessionStorage
# 马莹导入微信机器人的JSON处理工具用于数据的序列化和反序列化
from werobot.utils import json_loads, json_dumps
# 马莹导入当前Django项目中djangoblog应用的缓存工具
from djangoblog.utils import cache
# 马莹定义基于Memcache的会话存储类继承自werobot的SessionStorage基类
class MemcacheStorage(SessionStorage):
# 马莹:初始化方法,设置缓存键的前缀,默认为'ws_'
def __init__(self, prefix='ws_'):
self.prefix = prefix # 马莹:存储缓存键的前缀
self.cache = cache # 马莹:引用导入的缓存工具实例
# 马莹:用于检查缓存存储是否可用的属性
@property
def is_available(self):
value = "1" # 马莹:定义测试值
# 马莹:尝试设置一个测试键值对到缓存中
self.set('checkavaliable', value=value)
# 马莹:通过比较设置的值和获取的值是否一致,判断缓存是否可用
return value == self.get('checkavaliable')
# 马莹:生成带前缀的缓存键名,避免键名冲突
def key_name(self, s):
return '{prefix}{s}'.format(prefix=self.prefix, s=s)
# 马莹从缓存中获取指定ID对应的会话数据
def get(self, id):
id = self.key_name(id) # 马莹:生成带前缀的缓存键
# 马莹从缓存中获取数据若不存在则返回空字典的JSON字符串
session_json = self.cache.get(id) or '{}'
# 马莹将JSON字符串反序列化为Python字典并返回
return json_loads(session_json)
# 马莹:将会话数据存入缓存
def set(self, id, value):
id = self.key_name(id) # 马莹:生成带前缀的缓存键
# 马莹将Python对象序列化为JSON字符串后存入缓存
self.cache.set(id, json_dumps(value))
# 马莹从缓存中删除指定ID的会话数据
def delete(self, id):
id = self.key_name(id) # 马莹:生成带前缀的缓存键
# 马莹:从缓存中删除该键对应的记录
self.cache.delete(id)

@ -1,28 +0,0 @@
# 马莹导入Django的admin模块用于后台管理功能
from django.contrib import admin
# 马莹:注册模型的地方(后续会在这里注册需要管理的模型)
# Register your models here.
# 马莹定义Commands模型的后台管理类
class CommandsAdmin(admin.ModelAdmin):
# 马莹:在后台列表页展示的字段:标题、命令、描述
list_display = ('title', 'command', 'describe')
# 马莹定义EmailSendLog模型的后台管理类
class EmailSendLogAdmin(admin.ModelAdmin):
# 马莹:在后台列表页展示的字段:标题、收件人、发送结果、创建时间
list_display = ('title', 'emailto', 'send_result', 'creation_time')
# 马莹:设置为只读的字段(无法在后台编辑)
readonly_fields = (
'title', # 标题
'emailto', # 收件人
'send_result', # 发送结果
'creation_time', # 创建时间
'content' # 邮件内容
)
# 马莹:重写添加权限方法,禁止在后台手动添加记录
def has_add_permission(self, request):
return False

@ -1,28 +0,0 @@
from haystack.query import SearchQuerySet
from blog.models import Article, Category
class BlogApi:
def __init__(self):
self.searchqueryset = SearchQuerySet() # 马莹:初始化搜索查询集,用于处理文章搜索功能
self.searchqueryset.auto_query('') # 马莹:执行空查询,初始化搜索结果集(可能用于后续叠加过滤条件)
self.__max_takecount__ = 8 #马莹: 定义私有变量限制各类查询的最大返回数量为8条
def search_articles(self, query):
sqs = self.searchqueryset.auto_query(query) # 马莹:使用搜索查询集执行自动查询(可能包含分词、过滤等处理)
sqs = sqs.load_all() # 马莹:预加载所有关联数据,减少数据库查询次数(优化性能)
return sqs[:self.__max_takecount__] # 马莹限制返回结果数量返回前N条匹配的文章
def get_category_lists(self):
return Category.objects.all() # 马莹:返回所有分类对象(未限制数量,通常分类数量较少)
def get_category_articles(self, categoryname):
articles = Article.objects.filter(category__name=categoryname) #马莹: 过滤出指定分类下的所有文章(通过外键关联查询)
if articles:
return articles[:self.__max_takecount__]
return None # 马莹若存在符合条件的文章返回前N条否则返回None
def get_recent_articles(self):
return Article.objects.all()[:self.__max_takecount__]
# 马莹返回所有文章的前N条依赖于Article模型的默认排序设置

@ -1,103 +0,0 @@
import logging # 马莹;导入日志模块,用于记录程序运行过程中的日志信息
import os # 马莹导入os模块用于与操作系统交互如获取环境变量、执行系统命令等
import openai # 马莹:导入openai模块用于调用OpenAI的API服务
from servermanager.models import commands # 马莹从servermanager应用的models模块中导入commands模型用于操作命令相关的数据
logger = logging.getLogger(__name__) # 马莹:创建日志记录器,名称为当前模块名,用于记录该模块的日志
openai.api_key = os.environ.get('OPENAI_API_KEY') # 马莹从环境变量中获取OpenAI的API密钥并设置为openai模块的API密钥
if os.environ.get('HTTP_PROXY'): # 马莹检查环境变量中是否设置了HTTP代理如果有则为openai模块设置代理
openai.proxy = os.environ.get('HTTP_PROXY')
class ChatGPT:
"""
ChatGPT类用于与OpenAI的GPT模型进行交互实现聊天功能
"""
@staticmethod
def chat(prompt):
"""
静态方法发送提示信息给GPT模型并获取回复
:param prompt: 用户输入的提示信息字符串
:return: GPT模型的回复内容字符串若出错则返回"服务器出错了"
"""
try:
# 马莹调用OpenAI的ChatCompletion接口使用gpt-3.5-turbo模型
# 马莹:messages参数为消息列表包含用户角色和对应的内容
completion = openai.ChatCompletion.create(model="gpt-3.5-turbo",
messages=[{"role": "user", "content": prompt}])
# 马莹:从返回结果中提取第一个选择的消息内容并返回
return completion.choices[0].message.content
except Exception as e:
# 马莹:捕获异常并记录错误日志
logger.error(e)
# 马莹:返回错误提示信息
return "服务器出错了"
class CommandHandler:
"""
命令处理器类用于处理和执行系统命令以及提供命令帮助信息
"""
def __init__(self):
"""
初始化方法加载所有的命令数据
从commands模型中查询所有命令记录并存储在实例变量self.commands中
"""
self.commands = commands.objects.all()
def run(self, title):
"""
运行命令
:param title: 命令
:return: 返回命令执行结果
"""
# 马莹使用filter函数筛选出标题不区分大小写与输入title匹配的命令
# 马莹:将筛选结果转换为列表
cmd = list(
filter(
lambda x: x.title.upper() == title.upper(),
self.commands))
# 马莹:如果找到匹配的命令
if cmd:
# 马莹调用私有方法执行命令传入命令的具体内容cmd[0].command
return self.__run_command__(cmd[0].command)
else:
# 马莹:未找到命令时,返回提示信息
return "未找到相关命令请输入hepme获得帮助。"
def __run_command__(self, cmd):
"""
私有方法用于执行具体的系统命令
:param cmd: 要执行的系统命令字符串
:return: 命令执行的输出结果字符串若执行出错返回错误提示
"""
try:
# 马莹使用os.popen执行命令并读取命令的输出结果
res = os.popen(cmd).read()
return res
except BaseException:
# 马莹:捕获所有基本异常,返回命令执行出错的提示
return '命令执行出错!'
def get_help(self):
"""
获取所有命令的帮助信息
:return: 包含所有命令标题和描述的字符串每条命令占一行
"""
rsp = ''
# 马莹:遍历所有命令,拼接命令标题和描述信息
for cmd in self.commands:
rsp += '{c}:{d}\n'.format(c=cmd.title, d=cmd.describe)
return rsp
# 马莹:当该模块作为主程序运行时执行以下代码
if __name__ == '__main__':
chatbot = ChatGPT()
prompt = "写一篇1000字关于AI的论文"
print(chatbot.chat(prompt))

@ -1,9 +0,0 @@
# 马莹导入Django的AppConfig类用于配置应用的元数据和行为
from django.apps import AppConfig
# 马莹定义名为ServermanagerConfig的应用配置类继承自AppConfig
class ServermanagerConfig(AppConfig):
# 马莹:指定当前应用的名称为'servermanager'
# 马莹这个名称会被Django用于识别应用通常与应用的目录名一致
name = 'servermanager'

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

Loading…
Cancel
Save