Compare commits

...

No commits in common. 'master' and 'zwb_branch' have entirely different histories.

8
.idea/.gitignore vendored

@ -1,8 +0,0 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

@ -1,37 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="FacetManager">
<facet type="django" name="Django">
<configuration>
<option name="rootFolder" value="$MODULE_DIR$/src" />
<option name="settingsModule" value="settings.py" />
<option name="manageScript" value="$MODULE_DIR$/src/manage.py" />
<option name="environment" value="&lt;map/&gt;" />
<option name="doNotUseTestRunner" value="false" />
<option name="trackFilePattern" value="migrations" />
</configuration>
</facet>
</component>
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
</content>
<orderEntry type="jdk" jdkName="Python 3.10 (DjangoBlog)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="PyDocumentationSettings">
<option name="format" value="PLAIN" />
<option name="myDocStringFormat" value="Plain" />
</component>
<component name="TemplatesService">
<option name="TEMPLATE_CONFIGURATION" value="Jinja2" />
<option name="TEMPLATE_FOLDERS">
<list>
<option value="$MODULE_DIR$/src/templates" />
</list>
</option>
</component>
<component name="TestRunnerService">
<option name="PROJECT_TEST_RUNNER" value="py.test" />
</component>
</module>

@ -1,6 +0,0 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.10 (DjangoBlog)" project-jdk-type="Python SDK" />
</project>

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/DjangoBlog.iml" filepath="$PROJECT_DIR$/.idea/DjangoBlog.iml" />
</modules>
</component>
</project>

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

Binary file not shown.

Binary file not shown.

Binary file not shown.

@ -1,12 +0,0 @@
bin/data/
# virtualenv
venv/
collectedstatic/
djangoblog/whoosh_index/
uploads/
settings_production.py
*.md
docs/
logs/
static/
.github/

@ -1,6 +0,0 @@
blog/static/* linguist-vendored
*.js linguist-vendored
*.css linguist-vendored
* text=auto
*.sh text eol=lf
*.conf text eol=lf

80
src/.gitignore vendored

@ -1,80 +0,0 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*,cover
# Translations
*.pot
# Django stuff:
*.log
logs/
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# PyCharm
# http://www.jetbrains.com/pycharm/webhelp/project.html
.idea
.iml
# virtualenv
venv/
collectedstatic/
djangoblog/whoosh_index/
google93fd32dbd906620a.html
baidu_verify_FlHL7cUyC9.html
BingSiteAuth.xml
cb9339dbe2ff86a5aa169d28dba5f615.txt
werobot_session.*
django.jpg
uploads/
settings_production.py
werobot_session.db
bin/datas/
myenv/

@ -1 +0,0 @@
Subproject commit ef67f8db4fafce7e84c0e7bae23d5c5bf8869fac

@ -4,63 +4,46 @@ from django.contrib.auth.forms import UserChangeForm
from django.contrib.auth.forms import UsernameField
from django.utils.translation import gettext_lazy as _
# 注册模型到Django管理后台
# Register your models here.
from .models import BlogUser
# 自定义用户创建表单用于在Django管理后台创建新用户
class BlogUserCreationForm(forms.ModelForm):
# 定义两个密码字段,分别用于输入密码和确认密码
password1 = forms.CharField(label=_('password'), widget=forms.PasswordInput) # 密码输入框
password2 = forms.CharField(label=_('Enter password again'), widget=forms.PasswordInput) # 确认密码输入框
password1 = forms.CharField(label=_('password'), widget=forms.PasswordInput)
password2 = forms.CharField(label=_('Enter password again'), widget=forms.PasswordInput)
class Meta:
# 指定表单对应的模型
model = BlogUser
# 表单中包含的字段
fields = ('email',)
def clean_password2(self):
"""
验证两个密码输入是否一致
"""
password1 = self.cleaned_data.get("password1") # 获取第一个密码
password2 = self.cleaned_data.get("password2") # 获取第二个密码
# Check that the two password entries match
password1 = self.cleaned_data.get("password1")
password2 = self.cleaned_data.get("password2")
if password1 and password2 and password1 != password2:
# 如果两个密码不一致,抛出验证错误
raise forms.ValidationError(_("passwords do not match"))
return password2
def save(self, commit=True):
"""
保存用户实例并将密码以哈希格式存储
"""
user = super().save(commit=False) # 获取未保存的用户实例
user.set_password(self.cleaned_data["password1"]) # 设置哈希密码
# Save the provided password in hashed format
user = super().save(commit=False)
user.set_password(self.cleaned_data["password1"])
if commit:
user.source = 'adminsite' # 设置用户来源为管理后台
user.save() # 保存用户实例到数据库
user.source = 'adminsite'
user.save()
return user
# 自定义用户修改表单用于在Django管理后台修改用户信息
class BlogUserChangeForm(UserChangeForm):
class Meta:
# 指定表单对应的模型
model = BlogUser
# 表单中包含的所有字段
fields = '__all__'
# 自定义字段类型
field_classes = {'username': UsernameField}
def __init__(self, *args, **kwargs):
"""
初始化表单
"""
super().__init__(*args, **kwargs)
# 自定义用户管理类用于在Django管理后台管理用户
class BlogUserAdmin(UserAdmin):
form = BlogUserChangeForm
add_form = BlogUserCreationForm
@ -71,15 +54,6 @@ class BlogUserAdmin(UserAdmin):
'email',
'last_login',
'date_joined',
'source'
)
'source')
list_display_links = ('id', 'username')
ordering = ('-id',)
search_fields = ('username', 'nickname', 'email')
fieldsets = (
(None, {'fields': ('username', 'password')}),
(_('Personal info'), {'fields': ('email', 'nickname')}),
(_('Permissions'), {'fields': ('is_active', 'is_staff', 'is_superuser', 'groups', 'user_permissions')}),
(_('Important dates'), {'fields': ('last_login', 'date_joined')}),
)

@ -6,43 +6,30 @@ from django.utils.translation import gettext_lazy as _
from djangoblog.utils import get_current_site
# 创建自定义用户模型
# Create your models here.
class BlogUser(AbstractUser):
# 用户昵称字段允许为空最大长度为100
nickname = models.CharField(_('nick name'), max_length=100, blank=True)
# 用户创建时间字段,默认为当前时间
creation_time = models.DateTimeField(_('creation time'), default=now)
# 用户最后修改时间字段,默认为当前时间
last_modify_time = models.DateTimeField(_('last modify time'), default=now)
# 用户来源字段用于记录用户的创建来源允许为空最大长度为100
source = models.CharField(_('create source'), max_length=100, blank=True)
def get_absolute_url(self):
"""
获取用户的绝对URL用于跳转到用户的详情页面
"""
return reverse(
'blog:author_detail', kwargs={
'author_name': self.username}) # 根据用户名生成URL
'author_name': self.username})
def __str__(self):
"""
定义对象的字符串表示返回用户的邮箱
"""
return self.email
def get_full_url(self):
"""
获取用户的完整URL包括域名和路径
"""
site = get_current_site().domain # 获取当前站点的域名
site = get_current_site().domain
url = "https://{site}{path}".format(site=site,
path=self.get_absolute_url()) # 拼接完整URL
path=self.get_absolute_url())
return url
class Meta:
# 定义模型的元数据
ordering = ['-id'] # 默认按ID降序排列
verbose_name = _('user') # 模型的单数名称
verbose_name_plural = verbose_name # 模型的复数名称
get_latest_by = 'id' # 获取最新记录时使用的字段
ordering = ['-id']
verbose_name = _('user')
verbose_name_plural = verbose_name
get_latest_by = 'id'

@ -1,18 +1,26 @@
# filepath: f:\DjangoBlog\src\accounts\user_login_backend.py
from django.contrib.auth.backends import ModelBackend
from django.contrib.auth import get_user_model
from django.db import models
User = get_user_model()
from django.contrib.auth.backends import ModelBackend
class EmailOrUsernameModelBackend(ModelBackend):
"""
自定义认证后端支持通过用户名或邮箱登录
允许使用用户名或邮箱登录
"""
def authenticate(self, request, username=None, password=None, **kwargs):
if '@' in username:
kwargs = {'email': username}
else:
kwargs = {'username': username}
try:
# 尝试通过用户名或邮箱获取用户
user = User.objects.get(models.Q(username=username) | models.Q(email=username))
user = get_user_model().objects.get(**kwargs)
if user.check_password(password):
return user
except User.DoesNotExist:
return None
except get_user_model().DoesNotExist:
return None
def get_user(self, username):
try:
return get_user_model().objects.get(pk=username)
except get_user_model().DoesNotExist:
return None

@ -1,7 +1,3 @@
# 模块说明:
# 该模块定义了与用户账户相关的视图,包括注册、登录、注销、忘记密码等功能。
# 使用 Django 的类视图和表单视图来处理用户请求。
import logging
from django.utils.translation import gettext_lazy as _
from django.conf import settings
@ -32,24 +28,23 @@ from .models import BlogUser
logger = logging.getLogger(__name__)
# 注册视图
# Create your views here.
class RegisterView(FormView):
# 使用注册表单和模板
form_class = RegisterForm
template_name = 'account/registration_form.html'
@method_decorator(csrf_protect)
def dispatch(self, *args, **kwargs):
# 确保请求受 CSRF 保护
return super(RegisterView, self).dispatch(*args, **kwargs)
def form_valid(self, form):
# 表单验证通过后处理注册逻辑
if form.is_valid():
user = form.save(False) # 创建用户但不保存到数据库
user.is_active = False # 设置用户为未激活状态
user.source = 'Register' # 标记用户来源
user.save(True) # 保存用户到数据库
user = form.save(False)
user.is_active = False
user.source = 'Register'
user.save(True)
site = get_current_site().domain
sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
@ -75,74 +70,82 @@ class RegisterView(FormView):
],
title='验证您的电子邮箱',
content=content)
# 重定向到结果页面
url = reverse('accounts:result') + '?type=register&id=' + str(user.id)
url = reverse('accounts:result') + \
'?type=register&id=' + str(user.id)
return HttpResponseRedirect(url)
else:
# 如果表单无效,重新渲染表单
return self.render_to_response({'form': form})
return self.render_to_response({
'form': form
})
# 注销视图
class LogoutView(RedirectView):
url = '/login/' # 注销后重定向到登录页面
url = '/login/'
@method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs):
# 确保注销后页面不被缓存
return super(LogoutView, self).dispatch(request, *args, **kwargs)
def get(self, request, *args, **kwargs):
logout(request) # 执行注销操作
delete_sidebar_cache() # 清除侧边栏缓存
logout(request)
delete_sidebar_cache()
return super(LogoutView, self).get(request, *args, **kwargs)
# 登录视图
class LoginView(FormView):
# 使用登录表单和模板
form_class = LoginForm
template_name = 'account/login.html'
success_url = '/' # 登录成功后重定向的默认地址
success_url = '/'
redirect_field_name = REDIRECT_FIELD_NAME
login_ttl = 2626560 # 设置会话过期时间为一个月
login_ttl = 2626560 # 一个月的时间
@method_decorator(sensitive_post_parameters('password'))
@method_decorator(csrf_protect)
@method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs):
# 确保密码字段敏感、请求受 CSRF 保护且页面不被缓存
return super(LoginView, self).dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
# 获取上下文数据,添加重定向地址
redirect_to = self.request.GET.get(self.redirect_field_name, '/')
redirect_to = self.request.GET.get(self.redirect_field_name)
if redirect_to is None:
redirect_to = '/'
kwargs['redirect_to'] = redirect_to
return super(LoginView, self).get_context_data(**kwargs)
def form_valid(self, form):
# 表单验证通过后处理登录逻辑
form = AuthenticationForm(data=self.request.POST, request=self.request)
if form.is_valid():
delete_sidebar_cache() # 清除侧边栏缓存
auth.login(self.request, form.get_user()) # 执行登录操作
delete_sidebar_cache()
logger.info(self.redirect_field_name)
auth.login(self.request, form.get_user())
if self.request.POST.get("remember"):
self.request.session.set_expiry(self.login_ttl) # 设置会话过期时间
self.request.session.set_expiry(self.login_ttl)
return super(LoginView, self).form_valid(form)
# return HttpResponseRedirect('/')
else:
# 如果表单无效,重新渲染表单
return self.render_to_response({'form': form})
return self.render_to_response({
'form': form
})
def get_success_url(self):
# 获取登录成功后的重定向地址
redirect_to = self.request.POST.get(self.redirect_field_name, self.success_url)
if not url_has_allowed_host_and_scheme(url=redirect_to, allowed_hosts=[self.request.get_host()]):
redirect_to = self.request.POST.get(self.redirect_field_name)
if not url_has_allowed_host_and_scheme(
url=redirect_to, allowed_hosts=[
self.request.get_host()]):
redirect_to = self.success_url
return redirect_to
# 账户结果视图
def account_result(request):
# 处理注册或验证结果
type = request.GET.get('type')
id = request.GET.get('id')
user = get_object_or_404(get_user_model(), id=id)
logger.info(type)
if user.is_active:
@ -157,8 +160,8 @@ def account_result(request):
c_sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
sign = request.GET.get('sign')
if sign != c_sign:
return HttpResponseForbidden() # 验证失败返回 403
user.is_active = True # 激活用户
return HttpResponseForbidden()
user.is_active = True
user.save()
content = '''
恭喜您已经成功的完成邮箱验证您现在可以使用您的账号来登录本站
@ -171,30 +174,31 @@ def account_result(request):
else:
return HttpResponseRedirect('/')
# 忘记密码视图
class ForgetPasswordView(FormView):
form_class = ForgetPasswordForm
template_name = 'account/forget_password.html'
def form_valid(self, form):
# 表单验证通过后处理密码重置逻辑
if form.is_valid():
blog_user = BlogUser.objects.filter(email=form.cleaned_data.get("email")).get()
blog_user.password = make_password(form.cleaned_data["new_password2"]) # 设置新密码
blog_user.password = make_password(form.cleaned_data["new_password2"])
blog_user.save()
return HttpResponseRedirect('/login/') # 重定向到登录页面
return HttpResponseRedirect('/login/')
else:
return self.render_to_response({'form': form})
# 忘记密码验证码视图
class ForgetPasswordEmailCode(View):
def post(self, request: HttpRequest):
# 处理验证码发送请求
form = ForgetPasswordCodeForm(request.POST)
if not form.is_valid():
return HttpResponse("错误的邮箱")
to_email = form.cleaned_data["email"]
code = generate_code() # 生成验证码
utils.send_verify_email(to_email, code) # 发送验证码邮件
utils.set_code(to_email, code) # 缓存验证码
code = generate_code()
utils.send_verify_email(to_email, code)
utils.set_code(to_email, code)
return HttpResponse("ok")

@ -5,55 +5,44 @@ from django.urls import reverse
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
# 导入相关模型
from .models import Article, Category, Tag, Links, SideBar, BlogSettings
# Register your models here.
from .models import Article
# 自定义表单,用于文章模型的管理界面
class ArticleForm(forms.ModelForm):
# 可以在此处自定义字段的表单小部件
# body = forms.CharField(widget=AdminPagedownWidget())
class Meta:
model = Article
fields = '__all__'
# 定义批量操作函数
def makr_article_publish(modeladmin, request, queryset):
# 将选中的文章状态更新为“已发布”
queryset.update(status='p')
def draft_article(modeladmin, request, queryset):
# 将选中的文章状态更新为“草稿”
queryset.update(status='d')
def close_article_commentstatus(modeladmin, request, queryset):
# 将选中的文章评论状态更新为“关闭”
queryset.update(comment_status='c')
def open_article_commentstatus(modeladmin, request, queryset):
# 将选中的文章评论状态更新为“开启”
queryset.update(comment_status='o')
# 为批量操作函数添加描述
makr_article_publish.short_description = _('Publish selected articles')
draft_article.short_description = _('Draft selected articles')
close_article_commentstatus.short_description = _('Close article comments')
open_article_commentstatus.short_description = _('Open article comments')
# 自定义文章管理界面
class ArticlelAdmin(admin.ModelAdmin):
# 每页显示的记录数
list_per_page = 20
# 可搜索的字段
search_fields = ('body', 'title')
# 使用自定义表单
form = ArticleForm
# 列表显示的字段
list_display = (
'id',
'title',
@ -64,28 +53,17 @@ class ArticlelAdmin(admin.ModelAdmin):
'status',
'type',
'article_order')
# 可点击进入详情的字段
list_display_links = ('id', 'title')
# 列表过滤器
list_filter = ('status', 'type', 'category')
# 按日期层级显示
date_hierarchy = 'creation_time'
# 多对多字段的水平过滤器
filter_horizontal = ('tags',)
# 排除的字段
exclude = ('creation_time', 'last_modify_time')
# 是否允许在站点上查看
view_on_site = True
# 批量操作
actions = [
makr_article_publish,
draft_article,
close_article_commentstatus,
open_article_commentstatus]
# 使用原始 ID 字段显示外键
raw_id_fields = ('author', 'category',)
# 自定义显示分类链接
def link_to_category(self, obj):
info = (obj.category._meta.app_label, obj.category._meta.model_name)
link = reverse('admin:%s_%s_change' % info, args=(obj.category.id,))
@ -93,59 +71,42 @@ class ArticlelAdmin(admin.ModelAdmin):
link_to_category.short_description = _('category')
# 自定义表单的查询集
def get_form(self, request, obj=None, **kwargs):
form = super(ArticlelAdmin, self).get_form(request, obj, **kwargs)
# 限制作者字段仅显示超级用户
form.base_fields['author'].queryset = get_user_model(
).objects.filter(is_superuser=True)
return form
# 保存模型时的自定义逻辑
def save_model(self, request, obj, form, change):
super(ArticlelAdmin, self).save_model(request, obj, form, change)
# 自定义站点查看 URL
def get_view_on_site_url(self, obj=None):
if obj:
# 如果对象存在,返回其完整 URL
url = obj.get_full_url()
return url
else:
# 否则返回当前站点的域名
from djangoblog.utils import get_current_site
site = get_current_site().domain
return site
# 自定义标签管理界面
class TagAdmin(admin.ModelAdmin):
# 排除的字段
exclude = ('slug', 'last_mod_time', 'creation_time')
# 自定义分类管理界面
class CategoryAdmin(admin.ModelAdmin):
# 列表显示的字段
list_display = ('name', 'parent_category', 'index')
# 排除的字段
exclude = ('slug', 'last_mod_time', 'creation_time')
# 自定义链接管理界面
class LinksAdmin(admin.ModelAdmin):
# 排除的字段
exclude = ('last_mod_time', 'creation_time')
# 自定义侧边栏管理界面
class SideBarAdmin(admin.ModelAdmin):
# 列表显示的字段
list_display = ('name', 'content', 'is_enable', 'sequence')
# 排除的字段
exclude = ('last_mod_time', 'creation_time')
# 自定义博客设置管理界面
class BlogSettingsAdmin(admin.ModelAdmin):
pass

@ -7,11 +7,9 @@ from elasticsearch_dsl.connections import connections
from blog.models import Article
# 检查是否启用了 Elasticsearch
ELASTICSEARCH_ENABLED = hasattr(settings, 'ELASTICSEARCH_DSL')
if ELASTICSEARCH_ENABLED:
# 创建 Elasticsearch 连接
connections.create_connection(
hosts=[settings.ELASTICSEARCH_DSL['default']['hosts']])
from elasticsearch import Elasticsearch
@ -21,10 +19,8 @@ if ELASTICSEARCH_ENABLED:
c = IngestClient(es)
try:
# 检查是否存在名为 'geoip' 的 pipeline
c.get_pipeline('geoip')
except elasticsearch.exceptions.NotFoundError:
# 如果不存在,则创建 'geoip' pipeline
c.put_pipeline('geoip', body='''{
"description" : "Add geoip info",
"processors" : [
@ -37,7 +33,6 @@ if ELASTICSEARCH_ENABLED:
}''')
# 定义 GeoIP 信息的内部文档
class GeoIp(InnerDoc):
continent_name = Keyword()
country_iso_code = Keyword()
@ -45,25 +40,21 @@ class GeoIp(InnerDoc):
location = GeoPoint()
# 定义用户代理浏览器信息的内部文档
class UserAgentBrowser(InnerDoc):
Family = Keyword()
Version = Keyword()
# 定义用户代理操作系统信息的内部文档
class UserAgentOS(UserAgentBrowser):
pass
# 定义用户代理设备信息的内部文档
class UserAgentDevice(InnerDoc):
Family = Keyword()
Brand = Keyword()
Model = Keyword()
# 定义用户代理信息的内部文档
class UserAgent(InnerDoc):
browser = Object(UserAgentBrowser, required=False)
os = Object(UserAgentOS, required=False)
@ -72,7 +63,6 @@ class UserAgent(InnerDoc):
is_bot = Boolean()
# 定义性能日志的 Elasticsearch 文档
class ElapsedTimeDocument(Document):
url = Keyword()
time_taken = Long()
@ -82,7 +72,6 @@ class ElapsedTimeDocument(Document):
useragent = Object(UserAgent, required=False)
class Index:
# 定义索引名称和设置
name = 'performance'
settings = {
"number_of_shards": 1,
@ -93,11 +82,9 @@ class ElapsedTimeDocument(Document):
doc_type = 'ElapsedTime'
# 定义性能日志文档的管理器
class ElaspedTimeDocumentManager:
@staticmethod
def build_index():
# 创建 Elasticsearch 索引
from elasticsearch import Elasticsearch
client = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
res = client.indices.exists(index="performance")
@ -106,14 +93,12 @@ class ElaspedTimeDocumentManager:
@staticmethod
def delete_index():
# 删除 Elasticsearch 索引
from elasticsearch import Elasticsearch
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
es.indices.delete(index='performance', ignore=[400, 404])
@staticmethod
def create(url, time_taken, log_datetime, useragent, ip):
# 创建性能日志文档
ElaspedTimeDocumentManager.build_index()
ua = UserAgent()
ua.browser = UserAgentBrowser()
@ -145,7 +130,6 @@ class ElaspedTimeDocumentManager:
doc.save(pipeline="geoip")
# 定义文章的 Elasticsearch 文档
class ArticleDocument(Document):
body = Text(analyzer='ik_max_word', search_analyzer='ik_smart')
title = Text(analyzer='ik_max_word', search_analyzer='ik_smart')
@ -170,7 +154,6 @@ class ArticleDocument(Document):
article_order = Integer()
class Index:
# 定义索引名称和设置
name = 'blog'
settings = {
"number_of_shards": 1,
@ -181,25 +164,20 @@ class ArticleDocument(Document):
doc_type = 'Article'
# 定义文章文档的管理器
class ArticleDocumentManager():
def __init__(self):
# 初始化时创建索引
self.create_index()
def create_index(self):
# 创建 Elasticsearch 索引
ArticleDocument.init()
def delete_index(self):
# 删除 Elasticsearch 索引
from elasticsearch import Elasticsearch
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
es.indices.delete(index='blog', ignore=[400, 404])
def convert_to_doc(self, articles):
# 将文章对象转换为 Elasticsearch 文档
return [
ArticleDocument(
meta={
@ -224,7 +202,6 @@ class ArticleDocumentManager():
article_order=article.article_order) for article in articles]
def rebuild(self, articles=None):
# 重建索引并重新保存文档
ArticleDocument.init()
articles = articles if articles else Article.objects.all()
docs = self.convert_to_doc(articles)
@ -232,6 +209,5 @@ class ArticleDocumentManager():
doc.save()
def update_docs(self, docs):
# 更新文档
for doc in docs:
doc.save()
doc.save()

@ -1,71 +1,42 @@
import logging
import time
from ipware import get_client_ip # 用于获取客户端的 IP 地址
from user_agents import parse # 用于解析用户代理字符串
from ipware import get_client_ip
from user_agents import parse
from blog.documents import ELASTICSEARCH_ENABLED, ElaspedTimeDocumentManager # 导入 Elasticsearch 配置和文档管理器
from blog.documents import ELASTICSEARCH_ENABLED, ElaspedTimeDocumentManager
logger = logging.getLogger(__name__) # 设置日志记录器
logger = logging.getLogger(__name__)
class OnlineMiddleware(object):
def __init__(self, get_response=None):
"""
初始化中间件接收 Django get_response 方法
"""
self.get_response = get_response
super().__init__()
def __call__(self, request):
"""
中间件的主要逻辑用于记录页面渲染时间并将其存储到 Elasticsearch
"""
# 记录请求开始时间
''' page render time '''
start_time = time.time()
# 调用下一个中间件或视图
response = self.get_response(request)
# 获取 HTTP 用户代理字符串
http_user_agent = request.META.get('HTTP_USER_AGENT', '')
# 获取客户端 IP 地址
ip, _ = get_client_ip(request)
# 解析用户代理字符串
user_agent = parse(http_user_agent)
# 如果响应不是流式的,处理渲染时间
if not response.streaming:
try:
# 计算页面渲染时间
cast_time = time.time() - start_time
# 如果启用了 Elasticsearch将数据存储到 Elasticsearch
if ELASTICSEARCH_ENABLED:
time_taken = round((cast_time) * 1000, 2) # 将时间转换为毫秒并保留两位小数
url = request.path # 获取请求的 URL
# 获取当前时间
time_taken = round((cast_time) * 1000, 2)
url = request.path
from django.utils import timezone
# 创建 Elasticsearch 文档
ElaspedTimeDocumentManager.create(
url=url,
time_taken=time_taken,
log_datetime=timezone.now(),
useragent=user_agent,
ip=ip
)
# 替换响应内容中的占位符 <!!LOAD_TIMES!!> 为渲染时间
ip=ip)
response.content = response.content.replace(
b'<!!LOAD_TIMES!!>', str.encode(str(cast_time)[:5])
)
b'<!!LOAD_TIMES!!>', str.encode(str(cast_time)[:5]))
except Exception as e:
# 如果发生异常,记录错误日志
logger.error("Error OnlineMiddleware: %s" % e)
# 返回响应
return response
return response

@ -2017,7 +2017,12 @@ img#wpstats {
width: auto;
}
.commentlist .avatar {
height: 39px;
left: 2.2em;
top: 2.2em;
width: 39px;
}
.comments-area article header cite,
.comments-area article header time {
@ -2145,70 +2150,17 @@ div {
word-break: break-all;
}
/* 评论整体布局 - 使用相对定位实现头像左侧布局 */
.commentlist .comment-body {
position: relative;
padding-left: 60px; /* 为48px头像 + 12px间距留出空间 */
min-height: 48px; /* 确保有足够高度容纳头像 */
}
/* 评论作者信息 - 用户名和时间在同一行 */
.commentlist .comment-author {
display: inline-block;
margin: 0 10px 5px 0;
font-size: 13px;
position: relative;
}
.commentlist .comment-meta {
display: inline-block;
margin: 0 0 8px 0;
font-size: 12px;
color: #666;
}
.commentlist .comment-author,
.commentlist .comment-meta,
.commentlist .comment-awaiting-moderation {
float: left;
display: block;
font-size: 13px;
line-height: 22px;
}
/* 头像样式 - 绝对定位到左侧 */
.commentlist .comment-author .avatar {
position: absolute !important;
left: -60px; /* 定位到容器左侧 */
top: 0;
width: 48px !important;
height: 48px !important;
border-radius: 50%;
display: block;
object-fit: cover;
background-color: #f5f5f5;
border: 1px solid #ddd;
}
/* 评论作者名称样式 */
.commentlist .comment-author .fn {
display: inline;
margin: 0;
font-weight: 600;
color: #2e7bb8;
font-size: 13px;
}
.commentlist .comment-author .fn a {
color: #2e7bb8;
text-decoration: none;
}
.commentlist .comment-author .fn a:hover {
text-decoration: underline;
}
/* 评论内容样式 */
.commentlist .comment-body p {
margin: 5px 0 10px 0;
line-height: 1.5;
.commentlist .comment-author {
margin-right: 6px;
}
.commentlist .fn, .pinglist .ping-link {
@ -2222,15 +2174,13 @@ div {
display: none;
}
/* 通用头像样式 */
.commentlist .avatar {
width: 48px !important;
height: 48px !important;
border-radius: 50%;
display: block;
object-fit: cover;
background-color: #f5f5f5;
border: 1px solid #ddd;
position: absolute;
left: -60px;
top: 0;
width: 48px;
height: 48px;
border-radius: 100%;
}
.commentlist .comment-meta:before, .pinglist .ping-meta:before {
@ -2340,87 +2290,15 @@ div {
padding-left: 48px;
}
/* 嵌套评论整体布局 */
.commentlist li li .comment-body {
padding-left: 60px; /* 为48px头像 + 12px间距留出空间 */
min-height: 48px; /* 确保有足够高度容纳头像 */
}
/* 嵌套评论作者信息 */
.commentlist li li .comment-author {
display: inline-block;
margin: 0 8px 5px 0;
font-size: 12px; /* 稍小一点 */
.commentlist li li .avatar {
top: 0;
left: -48px;
width: 36px;
height: 36px;
}
.commentlist li li .comment-meta {
display: inline-block;
margin: 0 0 8px 0;
font-size: 11px; /* 稍小一点 */
color: #666;
}
/* 评论容器整体左移 - 使用更高优先级 */
#comments #commentlist-container.comment-tab {
margin-left: -15px !important; /* 在小屏幕上向左移动15px */
padding-left: 0 !important; /* 移除左内边距 */
position: relative !important; /* 确保定位正确 */
}
/* 在较大屏幕上进一步左移 */
@media screen and (min-width: 600px) {
#comments #commentlist-container.comment-tab {
margin-left: -30px !important; /* 在大屏幕上向左移动30px */
}
/* 响应式设计下的评论布局 - 保持48px头像 */
.commentlist .comment-body {
padding-left: 60px !important; /* 为48px头像 + 12px间距留出空间 */
min-height: 48px !important;
}
.commentlist .comment-author {
display: inline-block !important;
margin: 0 8px 5px 0 !important;
}
.commentlist .comment-meta {
display: inline-block !important;
margin: 0 0 8px 0 !important;
}
/* 响应式设计下头像保持48px */
.commentlist .comment-author .avatar {
left: -60px !important;
width: 48px !important;
height: 48px !important;
}
/* 嵌套评论在响应式设计下也保持48px头像 */
.commentlist li li .comment-body {
padding-left: 60px !important;
min-height: 48px !important;
}
.commentlist li li .comment-author .avatar {
left: -60px !important;
width: 48px !important;
height: 48px !important;
}
}
/* 嵌套评论头像 */
.commentlist li li .comment-author .avatar {
position: absolute !important;
left: -60px; /* 定位到容器左侧 */
top: 0;
width: 48px !important;
height: 48px !important;
border-radius: 50%;
display: block;
object-fit: cover;
background-color: #f5f5f5;
border: 1px solid #ddd;
left: 70px;
}
/* comments : nav
@ -2623,276 +2501,4 @@ li #reply-title {
height: 1px;
border: none;
/*border-top: 1px dashed #f5d6d6;*/
}
/* =============================================================================
============================================================================= */
/* 评论容器基础样式 */
.comment-body {
overflow-wrap: break-word;
word-wrap: break-word;
word-break: break-word;
max-width: 100%;
box-sizing: border-box;
}
/* 修复评论中的代码块溢出 */
.comment-content pre,
.comment-body pre {
white-space: pre-wrap !important;
word-wrap: break-word !important;
overflow-wrap: break-word !important;
max-width: 100% !important;
overflow-x: auto;
padding: 10px;
background-color: #f8f8f8;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 12px;
line-height: 1.4;
margin: 10px 0;
}
/* 修复评论中的行内代码 */
.comment-content code,
.comment-body code {
word-wrap: break-word !important;
overflow-wrap: break-word !important;
white-space: pre-wrap;
max-width: 100%;
display: inline-block;
vertical-align: top;
}
/* 修复评论中的长链接 */
.comment-content a,
.comment-body a {
word-wrap: break-word !important;
overflow-wrap: break-word !important;
word-break: break-all;
max-width: 100%;
}
/* 修复评论段落 */
.comment-content p,
.comment-body p {
word-wrap: break-word !important;
overflow-wrap: break-word !important;
max-width: 100%;
margin: 10px 0;
}
/* 特殊处理代码高亮块 - 关键修复! */
.comment-content .codehilite,
.comment-body .codehilite {
max-width: 100% !important;
overflow-x: auto;
margin: 10px 0;
background: #f8f8f8 !important;
border: 1px solid #ddd;
border-radius: 4px;
padding: 10px;
font-size: 12px;
line-height: 1.4;
/* 关键:防止内容撑开容器 */
width: 100%;
box-sizing: border-box;
display: block;
}
.comment-content .codehilite pre,
.comment-body .codehilite pre {
white-space: pre-wrap !important;
word-wrap: break-word !important;
overflow-wrap: break-word !important;
margin: 0 !important;
padding: 0 !important;
background: transparent !important;
border: none !important;
font-size: inherit;
line-height: inherit;
/* 确保pre标签不会超出父容器 */
max-width: 100%;
width: 100%;
box-sizing: border-box;
}
/* 修复代码高亮中的span标签 */
.comment-content .codehilite span,
.comment-body .codehilite span {
word-wrap: break-word !important;
overflow-wrap: break-word !important;
/* 防止行内元素导致的溢出 */
display: inline;
max-width: 100%;
}
/* 针对特定的代码高亮类 */
.comment-content .codehilite .kt,
.comment-content .codehilite .nf,
.comment-content .codehilite .n,
.comment-content .codehilite .p,
.comment-body .codehilite .kt,
.comment-body .codehilite .nf,
.comment-body .codehilite .n,
.comment-body .codehilite .p {
word-wrap: break-word !important;
overflow-wrap: break-word !important;
}
/* 搜索结果高亮样式 */
.search-result {
margin-bottom: 30px;
padding: 20px;
border: 1px solid #e1e1e1;
border-radius: 5px;
background: #fff;
}
.search-result .entry-title {
margin: 0 0 10px 0;
font-size: 1.5em;
}
.search-result .entry-title a {
color: #2c3e50;
text-decoration: none;
}
.search-result .entry-title a:hover {
color: #3498db;
}
.search-result .entry-meta {
color: #7f8c8d;
font-size: 0.9em;
margin-bottom: 15px;
}
.search-result .entry-meta span {
margin-right: 15px;
}
.search-excerpt {
line-height: 1.6;
color: #555;
}
.search-excerpt p {
margin: 10px 0;
}
/* 搜索关键词高亮 */
.search-excerpt em,
.search-result .entry-title em {
background-color: #fff3cd;
color: #856404;
font-style: normal;
font-weight: bold;
padding: 2px 4px;
border-radius: 3px;
}
.more-link {
color: #3498db;
text-decoration: none;
font-weight: bold;
}
.more-link:hover {
text-decoration: underline;
}
.comment-content .codehilite .w,
.comment-content .codehilite .o,
.comment-body .codehilite .kt,
.comment-body .codehilite .nf,
.comment-body .codehilite .n,
.comment-body .codehilite .p,
.comment-body .codehilite .w,
.comment-body .codehilite .o {
word-break: break-all;
overflow-wrap: break-word;
}
/* 修复评论列表项 */
.commentlist li {
max-width: 100%;
overflow: hidden;
box-sizing: border-box;
}
/* 确保评论内容不超出容器 */
.commentlist .comment-body {
max-width: calc(100% - 20px); /* 留出一些边距 */
margin-left: 10px;
margin-right: 10px;
overflow: hidden; /* 防止内容溢出 */
word-wrap: break-word;
}
/* 重要:限制评论列表项的最大宽度 */
.commentlist li[style*="margin-left"] {
max-width: calc(100% - 2rem) !important;
overflow: hidden;
box-sizing: border-box;
}
/* 特别处理深层嵌套的评论 */
.commentlist li[style*="margin-left: 3rem"],
.commentlist li[style*="margin-left: 6rem"],
.commentlist li[style*="margin-left: 9rem"] {
max-width: calc(100% - 1rem) !important;
}
/* 移动端优化 */
@media (max-width: 768px) {
.comment-content pre,
.comment-body pre {
font-size: 11px;
padding: 8px;
margin: 8px 0;
}
.commentlist .comment-body {
max-width: calc(100% - 10px);
margin-left: 5px;
margin-right: 5px;
}
/* 移动端评论缩进调整 */
.commentlist li[style*="margin-left"] {
margin-left: 1rem !important;
max-margin-left: 2rem !important;
}
}
/* 防止表格溢出 */
.comment-content table,
.comment-body table {
max-width: 100%;
overflow-x: auto;
display: block;
white-space: nowrap;
}
/* 修复图片溢出 */
.comment-content img,
.comment-body img {
max-width: 100% !important;
height: auto !important;
}
/* 修复引用块 */
.comment-content blockquote,
.comment-body blockquote {
max-width: 100%;
overflow-wrap: break-word;
word-wrap: break-word;
padding: 10px 15px;
margin: 10px 0;
border-left: 4px solid #ddd;
background-color: #f9f9f9;
}

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

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

Loading…
Cancel
Save