清理历史文件

master
LSH 2 months ago
parent db71794388
commit 8fc27bdd74

@ -1,18 +0,0 @@
<!--
如果你不认真勾选下面的内容,我可能会直接关闭你的 Issue。
提问之前,建议先阅读 https://github.com/ruby-china/How-To-Ask-Questions-The-Smart-Way
-->
**我确定我已经查看了** (标注`[ ]`为`[x]`)
- [ ] [DjangoBlog的readme](https://github.com/liangliangyy/DjangoBlog/blob/master/README.md)
- [ ] [配置说明](https://github.com/liangliangyy/DjangoBlog/blob/master/bin/config.md)
- [ ] [其他 Issues](https://github.com/liangliangyy/DjangoBlog/issues)
----
**我要申请** (标注`[ ]`为`[x]`)
- [ ] BUG 反馈
- [ ] 添加新的特性或者功能
- [ ] 请求技术支持

@ -1,47 +0,0 @@
name: "CodeQL"
on:
push:
branches:
- master
- dev
paths-ignore:
- '**/*.md'
- '**/*.css'
- '**/*.js'
- '**/*.yml'
- '**/*.txt'
pull_request:
branches:
- master
- dev
paths-ignore:
- '**/*.md'
- '**/*.css'
- '**/*.js'
- '**/*.yml'
- '**/*.txt'
schedule:
- cron: '30 1 * * 0'
jobs:
CodeQL-Build:
runs-on: ubuntu-latest
permissions:
security-events: write
actions: read
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
- name: Autobuild
uses: github/codeql-action/autobuild@v2
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2

@ -1,136 +0,0 @@
name: Django CI
on:
push:
branches:
- master
- dev
paths-ignore:
- '**/*.md'
- '**/*.css'
- '**/*.js'
pull_request:
branches:
- master
- dev
paths-ignore:
- '**/*.md'
- '**/*.css'
- '**/*.js'
jobs:
build-normal:
runs-on: ubuntu-latest
strategy:
max-parallel: 4
matrix:
python-version: ["3.10","3.11" ]
steps:
- name: Start MySQL
uses: samin/mysql-action@v1.3
with:
host port: 3306
container port: 3306
character set server: utf8mb4
collation server: utf8mb4_general_ci
mysql version: latest
mysql root password: root
mysql database: djangoblog
mysql user: root
mysql password: root
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
- name: Install Dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Run Tests
env:
DJANGO_MYSQL_PASSWORD: root
DJANGO_MYSQL_HOST: 127.0.0.1
run: |
python manage.py makemigrations
python manage.py migrate
python manage.py test
build-with-es:
runs-on: ubuntu-latest
strategy:
max-parallel: 4
matrix:
python-version: ["3.10","3.11" ]
steps:
- name: Start MySQL
uses: samin/mysql-action@v1.3
with:
host port: 3306
container port: 3306
character set server: utf8mb4
collation server: utf8mb4_general_ci
mysql version: latest
mysql root password: root
mysql database: djangoblog
mysql user: root
mysql password: root
- name: Configure sysctl limits
run: |
sudo swapoff -a
sudo sysctl -w vm.swappiness=1
sudo sysctl -w fs.file-max=262144
sudo sysctl -w vm.max_map_count=262144
- uses: miyataka/elasticsearch-github-actions@1
with:
stack-version: '7.12.1'
plugins: 'https://release.infinilabs.com/analysis-ik/stable/elasticsearch-analysis-ik-7.12.1.zip'
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
- name: Install Dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Run Tests
env:
DJANGO_MYSQL_PASSWORD: root
DJANGO_MYSQL_HOST: 127.0.0.1
DJANGO_ELASTICSEARCH_HOST: 127.0.0.1:9200
run: |
python manage.py makemigrations
python manage.py migrate
coverage run manage.py test
coverage xml
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v1
docker:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Build and push
uses: docker/build-push-action@v3
with:
context: .
push: false
tags: djangoblog/djangoblog:dev

@ -1,43 +0,0 @@
name: docker
on:
push:
paths-ignore:
- '**/*.md'
- '**/*.yml'
branches:
- 'master'
- 'dev'
jobs:
docker:
runs-on: ubuntu-latest
steps:
- name: Set env to docker dev tag
if: endsWith(github.ref, '/dev')
run: |
echo "DOCKER_TAG=test" >> $GITHUB_ENV
- name: Set env to docker latest tag
if: endsWith(github.ref, '/master')
run: |
echo "DOCKER_TAG=latest" >> $GITHUB_ENV
- name: Checkout
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v3
with:
context: .
push: true
tags: ${{ secrets.DOCKERHUB_USERNAME }}/djangoblog:${{env.DOCKER_TAG}}

@ -1,39 +0,0 @@
name: publish release
on:
release:
types: [ published ]
jobs:
docker:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Docker meta
id: meta
uses: docker/metadata-action@v3
with:
images: name/app
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v3
with:
context: .
push: true
platforms: |
linux/amd64
linux/arm64
linux/arm/v7
linux/arm/v6
linux/386
tags: ${{ secrets.DOCKERHUB_USERNAME }}/djangoblog:${{ github.event.release.tag_name }}

@ -1,60 +0,0 @@
from django import forms
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.forms import UserChangeForm
from django.contrib.auth.forms import UsernameField
from django.utils.translation import gettext_lazy as _
# Register your models here.
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',)
def clean_password2(self):
# Check that the two password entries match
password1 = self.cleaned_data.get("password1")
password2 = self.cleaned_data.get("password2")
if password1 and password2 and password1 != password2:
raise forms.ValidationError(_("passwords do not match"))
return password2
def save(self, commit=True):
# Save the provided password in hashed format
user = super().save(commit=False)
user.set_password(self.cleaned_data["password1"])
if commit:
user.source = 'adminsite'
user.save()
return user
class BlogUserChangeForm(UserChangeForm):
class Meta:
model = BlogUser
fields = '__all__'
field_classes = {'username': UsernameField}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
class BlogUserAdmin(UserAdmin):
form = BlogUserChangeForm
add_form = BlogUserCreationForm
list_display = (
'id',
'nickname',
'username',
'email',
'last_login',
'date_joined',
'source')
list_display_links = ('id', 'username')
ordering = ('-id',)
search_fields = ('username', 'nickname', 'email')

@ -1,103 +0,0 @@
# 导入Django表单模块
from django import forms
# 导入Django默认用户管理类
from django.contrib.auth.admin import UserAdmin
# 导入用户修改表单
from django.contrib.auth.forms import UserChangeForm
# 导入用户名字段
from django.contrib.auth.forms import UsernameField
# 导入国际化翻译函数
from django.utils.translation import gettext_lazy as _
# 注册模型到管理后台
# 导入自定义用户模型
from .models import BlogUser
class BlogUserCreationForm(forms.ModelForm):
"""自定义用户创建表单,用于管理员后台创建用户"""
# 密码字段1 - 输入密码
password1 = forms.CharField(
label=_('password'), # 字段标签:密码
widget=forms.PasswordInput # 使用密码输入控件
)
# 密码字段2 - 确认密码
password2 = forms.CharField(
label=_('Enter password again'), # 字段标签:再次输入密码
widget=forms.PasswordInput # 使用密码输入控件
)
class Meta:
# 指定使用的模型
model = BlogUser
# 表单包含的字段:仅邮箱
fields = ('email',)
def clean_password2(self):
"""清理和验证密码确认字段"""
# 从已清理数据中获取密码1
password1 = self.cleaned_data.get("password1")
# 从已清理数据中获取密码2
password2 = self.cleaned_data.get("password2")
# 检查两个密码是否存在且匹配
if password1 and password2 and password1 != password2:
# 如果不匹配,抛出验证错误
raise forms.ValidationError(_("passwords do not match"))
# 返回验证通过的密码2
return password2
def save(self, commit=True):
"""保存用户实例,处理密码哈希"""
# 调用父类save方法但不立即提交到数据库
user = super().save(commit=False)
# 使用Django的密码哈希方法设置密码
user.set_password(self.cleaned_data["password1"])
# 如果设置为立即提交
if commit:
# 设置用户来源为管理员站点
user.source = 'adminsite'
# 保存用户到数据库
user.save()
# 返回用户实例
return user
class BlogUserChangeForm(UserChangeForm):
"""自定义用户信息修改表单"""
class Meta:
# 指定使用的模型
model = BlogUser
# 包含所有字段
fields = '__all__'
# 字段类映射,用户名使用特定字段类
field_classes = {'username': UsernameField}
def __init__(self, *args, **kwargs):
"""初始化表单"""
# 调用父类初始化方法
super().__init__(*args, **kwargs)
class BlogUserAdmin(UserAdmin):
"""自定义用户管理类配置Django管理后台的用户界面"""
# 指定修改用户时使用的表单
form = BlogUserChangeForm
# 指定创建用户时使用的表单
add_form = BlogUserCreationForm
# 列表页面显示的字段
list_display = (
'id', # 用户ID
'nickname', # 昵称
'username', # 用户名
'email', # 邮箱
'last_login', # 最后登录时间
'date_joined', # 注册时间
'source' # 用户来源
)
# 列表中可作为链接点击的字段
list_display_links = ('id', 'username')
# 默认排序字段按ID降序排列
ordering = ('-id',)

@ -1,5 +0,0 @@
from django.apps import AppConfig
class AccountsConfig(AppConfig):
name = 'accounts'

@ -1,9 +0,0 @@
# 导入Django应用配置基类
from django.apps import AppConfig
class AccountsConfig(AppConfig):
"""账户应用的配置类"""
# 应用的名称Python路径
name = 'accounts'

@ -1,127 +0,0 @@
from django import forms
from django.contrib.auth import get_user_model, password_validation
from django.contrib.auth.forms import AuthenticationForm, UserCreationForm
from django.core.exceptions import ValidationError
from django.forms import widgets
from django.utils.translation import gettext_lazy as _
from . import utils
from .models import BlogUser
class LoginForm(AuthenticationForm):
def __init__(self, *args, **kwargs):
super(LoginForm, self).__init__(*args, **kwargs)
# 更新用户名字段的标签和提示信息,明确支持邮箱登录
self.fields['username'].label = _('用户名或邮箱')
self.fields['username'].widget = widgets.TextInput(
attrs={
'placeholder': "用户名或邮箱地址",
"class": "form-control",
'autocomplete': 'username'
})
self.fields['password'].widget = widgets.PasswordInput(
attrs={
'placeholder': "密码",
"class": "form-control",
'autocomplete': 'current-password'
})
class RegisterForm(UserCreationForm):
def __init__(self, *args, **kwargs):
super(RegisterForm, self).__init__(*args, **kwargs)
self.fields['username'].widget = widgets.TextInput(
attrs={'placeholder': "username", "class": "form-control"})
self.fields['email'].widget = widgets.EmailInput(
attrs={'placeholder': "email", "class": "form-control"})
self.fields['password1'].widget = widgets.PasswordInput(
attrs={'placeholder': "password", "class": "form-control"})
self.fields['password2'].widget = widgets.PasswordInput(
attrs={'placeholder': "repeat password", "class": "form-control"})
def clean_email(self):
email = self.cleaned_data['email']
if get_user_model().objects.filter(email=email).exists():
raise ValidationError(_("email already exists"))
return email
class Meta:
model = get_user_model()
fields = ("username", "email")
class ForgetPasswordForm(forms.Form):
new_password1 = forms.CharField(
label=_("New password"),
widget=forms.PasswordInput(
attrs={
"class": "form-control",
'placeholder': _("New password")
}
),
)
new_password2 = forms.CharField(
label="确认密码",
widget=forms.PasswordInput(
attrs={
"class": "form-control",
'placeholder': _("Confirm password")
}
),
)
email = forms.EmailField(
label='邮箱',
widget=forms.TextInput(
attrs={
'class': 'form-control',
'placeholder': _("Email")
}
),
)
code = forms.CharField(
label=_('Code'),
widget=forms.TextInput(
attrs={
'class': 'form-control',
'placeholder': _("Code")
}
),
)
def clean_new_password2(self):
password1 = self.data.get("new_password1")
password2 = self.data.get("new_password2")
if password1 and password2 and password1 != password2:
raise ValidationError(_("passwords do not match"))
password_validation.validate_password(password2)
return password2
def clean_email(self):
user_email = self.cleaned_data.get("email")
if not BlogUser.objects.filter(
email=user_email
).exists():
# todo 这里的报错提示可以判断一个邮箱是不是注册过,如果不想暴露可以修改
raise ValidationError(_("email does not exist"))
return user_email
def clean_code(self):
code = self.cleaned_data.get("code")
error = utils.verify(
email=self.cleaned_data.get("email"),
code=code,
)
if error:
raise ValidationError(error)
return code
class ForgetPasswordCodeForm(forms.Form):
email = forms.EmailField(
label=_('Email'),
)

@ -1,194 +0,0 @@
# 导入Django表单模块
from django import forms
# 导入用户模型获取函数和密码验证工具
from django.contrib.auth import get_user_model, password_validation
# 导入Django内置认证表单
from django.contrib.auth.forms import AuthenticationForm, UserCreationForm
# 导入验证异常
from django.core.exceptions import ValidationError
# 导入表单控件
from django.forms import widgets
# 导入国际化翻译函数
from django.utils.translation import gettext_lazy as _
# 导入工具函数
from . import utils
# 导入用户模型
from .models import BlogUser
class LoginForm(AuthenticationForm):
"""用户登录表单继承自Django内置认证表单"""
def __init__(self, *args, **kwargs):
"""初始化表单,自定义字段控件"""
# 调用父类初始化方法
super(LoginForm, self).__init__(*args, **kwargs)
# 自定义用户名字段控件文本输入框带占位符和CSS类
self.fields['username'].widget = widgets.TextInput(
attrs={
'placeholder': "username", # 输入框占位符文本
"class": "form-control" # CSS类名用于样式
}
)
# 自定义密码字段控件密码输入框带占位符和CSS类
self.fields['password'].widget = widgets.PasswordInput(
attrs={
'placeholder': "password", # 输入框占位符文本
"class": "form-control" # CSS类名用于样式
}
)
class RegisterForm(UserCreationForm):
"""用户注册表单继承自Django内置用户创建表单"""
def __init__(self, *args, **kwargs):
"""初始化表单,自定义所有字段的控件"""
# 调用父类初始化方法
super(RegisterForm, self).__init__(*args, **kwargs)
# 自定义用户名字段控件
self.fields['username'].widget = widgets.TextInput(
attrs={
'placeholder': "username", # 占位符:用户名
"class": "form-control" # CSS类
}
)
# 自定义邮箱字段控件
self.fields['email'].widget = widgets.EmailInput(
attrs={
'placeholder': "email", # 占位符:邮箱
"class": "form-control" # CSS类
}
)
# 自定义密码字段控件
self.fields['password1'].widget = widgets.PasswordInput(
attrs={
'placeholder': "password", # 占位符:密码
"class": "form-control" # CSS类
}
)
# 自定义密码确认字段控件
self.fields['password2'].widget = widgets.PasswordInput(
attrs={
'placeholder': "repeat password", # 占位符:重复密码
"class": "form-control" # CSS类
}
)
def clean_email(self):
"""清理和验证邮箱字段,确保邮箱唯一性"""
# 从已清理数据中获取邮箱
email = self.cleaned_data['email']
# 检查数据库中是否已存在该邮箱
if get_user_model().objects.filter(email=email).exists():
# 如果邮箱已存在,抛出验证错误
raise ValidationError(_("email already exists"))
# 返回验证通过的邮箱
return email
class Meta:
"""表单元数据配置"""
# 指定表单关联的模型
model = get_user_model()
# 表单包含的字段:用户名和邮箱
fields = ("username", "email")
class ForgetPasswordForm(forms.Form):
"""忘记密码重置表单"""
# 新密码字段1
new_password1 = forms.CharField(
label=_("New password"), # 字段标签:新密码
widget=forms.PasswordInput( # 使用密码输入控件
attrs={
"class": "form-control", # CSS类
'placeholder': _("New password") # 占位符:新密码
}
),
)
# 新密码字段2 - 确认密码
new_password2 = forms.CharField(
label="确认密码", # 字段标签:确认密码(硬编码中文)
widget=forms.PasswordInput( # 使用密码输入控件
attrs={
"class": "form-control", # CSS类
'placeholder': _("Confirm password") # 占位符:确认密码
}
),
)
# 邮箱字段
email = forms.EmailField(
label='邮箱', # 字段标签:邮箱(硬编码中文)
widget=forms.TextInput( # 使用文本输入控件
attrs={
'class': 'form-control', # CSS类
'placeholder': _("Email") # 占位符:邮箱
}
),
)
# 验证码字段
code = forms.CharField(
label=_('Code'), # 字段标签:验证码
widget=forms.TextInput( # 使用文本输入控件
attrs={
'class': 'form-control', # CSS类
'placeholder': _("Code") # 占位符:验证码
}
),
)
def clean_new_password2(self):
"""清理和验证密码确认字段"""
# 从原始数据中获取新密码1不使用cleaned_data因为可能还未验证
password1 = self.data.get("new_password1")
# 从原始数据中获取新密码2
password2 = self.data.get("new_password2")
# 检查两个密码是否存在且匹配
if password1 and password2 and password1 != password2:
# 如果不匹配,抛出验证错误
raise ValidationError(_("passwords do not match"))
# 使用Django内置密码验证器验证密码强度
password_validation.validate_password(password2)
# 返回验证通过的密码
return password2
def clean_email(self):
"""清理和验证邮箱字段,确保邮箱已注册"""
# 从已清理数据中获取邮箱
user_email = self.cleaned_data.get("email")
# 检查数据库中是否存在该邮箱的用户
if not BlogUser.objects.filter(email=user_email).exists():
# 安全提示:这里会暴露邮箱是否注册,可根据安全需求修改
# 如果邮箱不存在,抛出验证错误
raise ValidationError(_("email does not exist"))
# 返回验证通过的邮箱
return user_email
def clean_code(self):
"""清理和验证验证码字段"""
# 从已清理数据中获取验证码
code = self.cleaned_data.get("code")
# 调用工具函数验证验证码是否正确
error = utils.verify(
email=self.cleaned_data.get("email"), # 传入邮箱
code=code, # 传入验证码
)
# 如果验证返回错误信息
if error:
# 抛出验证错误
raise ValidationError(error)
# 返回验证通过的验证码
return code
class ForgetPasswordCodeForm(forms.Form):
"""获取忘记密码验证码的表单"""
# 邮箱字段
email = forms.EmailField(
label=_('Email'), # 字段标签:邮箱
)

@ -1,35 +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):
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)
source = models.CharField(_('create source'), max_length=100, blank=True)
def get_absolute_url(self):
return reverse(
'blog:author_detail', kwargs={
'author_name': self.username})
def __str__(self):
return self.email
def get_full_url(self):
site = get_current_site().domain
url = "https://{site}{path}".format(site=site,
path=self.get_absolute_url())
return url
class Meta:
ordering = ['-id']
verbose_name = _('user')
verbose_name_plural = verbose_name
get_latest_by = 'id'

@ -1,72 +0,0 @@
# 导入Django抽象用户基类
from django.contrib.auth.models import AbstractUser
# 导入Django数据库模型
from django.db import models
# 导入URL反向解析函数
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
# 在这里创建模型
class BlogUser(AbstractUser):
"""自定义用户模型继承自Django抽象用户基类"""
# 昵称字段最大长度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用于生成用户详情页链接"""
# 使用reverse反向解析URL传入用户名作为参数
return reverse(
'blog:author_detail', # URL模式名称
kwargs={'author_name': self.username} # URL参数作者用户名
)
def __str__(self):
"""对象的字符串表示,返回邮箱地址"""
return self.email
def get_full_url(self):
"""获取用户的完整URL包含域名"""
# 获取当前站点域名
site = get_current_site().domain
# 构建完整URLhttps://域名 + 用户详情页路径
url = "https://{site}{path}".format(
site=site, # 站点域名
path=self.get_absolute_url() # 用户详情页路径
)
# 返回完整URL
return url
class Meta:
"""模型的元数据配置"""
ordering = ['-id'] # 默认排序按ID降序排列
verbose_name = _('user') # 单数显示名称:用户
verbose_name_plural = verbose_name # 复数显示名称:与单数相同
get_latest_by = 'id' # 获取最新记录的依据字段ID

@ -1,207 +0,0 @@
from django.test import Client, RequestFactory, TestCase
from django.urls import reverse
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from accounts.models import BlogUser
from blog.models import Article, Category
from djangoblog.utils import *
from . import utils
# Create your tests here.
class AccountTest(TestCase):
def setUp(self):
self.client = Client()
self.factory = RequestFactory()
self.blog_user = BlogUser.objects.create_user(
username="test",
email="admin@admin.com",
password="12345678"
)
self.new_test = "xxx123--="
def test_validate_account(self):
site = get_current_site().domain
user = BlogUser.objects.create_superuser(
email="liangliangyy1@gmail.com",
username="liangliangyy1",
password="qwer!@#$ggg")
testuser = BlogUser.objects.get(username='liangliangyy1')
loginresult = self.client.login(
username='liangliangyy1',
password='qwer!@#$ggg')
self.assertEqual(loginresult, True)
response = self.client.get('/admin/')
self.assertEqual(response.status_code, 200)
category = Category()
category.name = "categoryaaa"
category.creation_time = timezone.now()
category.last_modify_time = timezone.now()
category.save()
article = Article()
article.title = "nicetitleaaa"
article.body = "nicecontentaaa"
article.author = user
article.category = category
article.type = 'a'
article.status = 'p'
article.save()
response = self.client.get(article.get_admin_url())
self.assertEqual(response.status_code, 200)
def test_validate_register(self):
self.assertEquals(
0, len(
BlogUser.objects.filter(
email='user123@user.com')))
response = self.client.post(reverse('account:register'), {
'username': 'user1233',
'email': 'user123@user.com',
'password1': 'password123!q@wE#R$T',
'password2': 'password123!q@wE#R$T',
})
self.assertEquals(
1, len(
BlogUser.objects.filter(
email='user123@user.com')))
user = BlogUser.objects.filter(email='user123@user.com')[0]
sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
path = reverse('accounts:result')
url = '{path}?type=validation&id={id}&sign={sign}'.format(
path=path, id=user.id, sign=sign)
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.client.login(username='user1233', password='password123!q@wE#R$T')
user = BlogUser.objects.filter(email='user123@user.com')[0]
user.is_superuser = True
user.is_staff = True
user.save()
delete_sidebar_cache()
category = Category()
category.name = "categoryaaa"
category.creation_time = timezone.now()
category.last_modify_time = timezone.now()
category.save()
article = Article()
article.category = category
article.title = "nicetitle333"
article.body = "nicecontentttt"
article.author = user
article.type = 'a'
article.status = 'p'
article.save()
response = self.client.get(article.get_admin_url())
self.assertEqual(response.status_code, 200)
response = self.client.get(reverse('account:logout'))
self.assertIn(response.status_code, [301, 302, 200])
response = self.client.get(article.get_admin_url())
self.assertIn(response.status_code, [301, 302, 200])
response = self.client.post(reverse('account:login'), {
'username': 'user1233',
'password': 'password123'
})
self.assertIn(response.status_code, [301, 302, 200])
response = self.client.get(article.get_admin_url())
self.assertIn(response.status_code, [301, 302, 200])
def test_verify_email_code(self):
to_email = "admin@admin.com"
code = generate_code()
utils.set_code(to_email, code)
utils.send_verify_email(to_email, code)
err = utils.verify("admin@admin.com", code)
self.assertEqual(err, None)
err = utils.verify("admin@123.com", code)
self.assertEqual(type(err), str)
def test_forget_password_email_code_success(self):
resp = self.client.post(
path=reverse("account:forget_password_code"),
data=dict(email="admin@admin.com")
)
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.content.decode("utf-8"), "ok")
def test_forget_password_email_code_fail(self):
resp = self.client.post(
path=reverse("account:forget_password_code"),
data=dict()
)
self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱")
resp = self.client.post(
path=reverse("account:forget_password_code"),
data=dict(email="admin@com")
)
self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱")
def test_forget_password_email_success(self):
code = generate_code()
utils.set_code(self.blog_user.email, code)
data = dict(
new_password1=self.new_test,
new_password2=self.new_test,
email=self.blog_user.email,
code=code,
)
resp = self.client.post(
path=reverse("account:forget_password"),
data=data
)
self.assertEqual(resp.status_code, 302)
# 验证用户密码是否修改成功
blog_user = BlogUser.objects.filter(
email=self.blog_user.email,
).first() # type: BlogUser
self.assertNotEqual(blog_user, None)
self.assertEqual(blog_user.check_password(data["new_password1"]), True)
def test_forget_password_email_not_user(self):
data = dict(
new_password1=self.new_test,
new_password2=self.new_test,
email="123@123.com",
code="123456",
)
resp = self.client.post(
path=reverse("account:forget_password"),
data=data
)
self.assertEqual(resp.status_code, 200)
def test_forget_password_email_code_error(self):
code = generate_code()
utils.set_code(self.blog_user.email, code)
data = dict(
new_password1=self.new_test,
new_password2=self.new_test,
email=self.blog_user.email,
code="111111",
)
resp = self.client.post(
path=reverse("account:forget_password"),
data=data
)
self.assertEqual(resp.status_code, 200)

@ -1,294 +0,0 @@
# 导入Django测试客户端、请求工厂、测试用例
from django.test import Client, RequestFactory, TestCase
# 导入URL反向解析
from django.urls import reverse
# 导入时区工具
from django.utils import timezone
# 导入延迟翻译函数
from django.utils.translation import gettext_lazy as _
# 导入账户模型
from accounts.models import BlogUser
# 导入博客模型
from blog.models import Article, Category
# 导入项目工具函数
from djangoblog.utils import *
# 导入当前应用的工具函数
from . import utils
# 在这里创建测试
class AccountTest(TestCase):
"""账户功能测试类"""
def setUp(self):
"""测试前置设置,每个测试方法执行前都会调用"""
# 创建测试客户端
self.client = Client()
# 创建请求工厂
self.factory = RequestFactory()
# 创建测试用户
self.blog_user = BlogUser.objects.create_user(
username="test", # 用户名
email="admin@admin.com", # 邮箱
password="12345678" # 密码
)
# 设置新测试密码
self.new_test = "xxx123--="
def test_validate_account(self):
"""测试账户验证功能"""
# 获取当前站点
site = get_current_site().domain
# 创建超级用户
user = BlogUser.objects.create_superuser(
email="liangliangyy1@gmail.com", # 邮箱
username="liangliangyy1", # 用户名
password="qwer!@#$ggg" # 密码
)
# 从数据库获取刚创建的用户
testuser = BlogUser.objects.get(username='liangliangyy1')
# 尝试登录
loginresult = self.client.login(
username='liangliangyy1', # 用户名
password='qwer!@#$ggg' # 密码
)
# 断言登录成功
self.assertEqual(loginresult, True)
# 访问管理员页面
response = self.client.get('/admin/')
# 断言页面访问成功
self.assertEqual(response.status_code, 200)
# 创建分类
category = Category()
category.name = "categoryaaa" # 分类名称
category.creation_time = timezone.now() # 创建时间
category.last_modify_time = timezone.now() # 最后修改时间
category.save()
# 创建文章
article = Article()
article.title = "nicetitleaaa" # 文章标题
article.body = "nicecontentaaa" # 文章内容
article.author = user # 文章作者
article.category = category # 文章分类
article.type = 'a' # 文章类型
article.status = 'p' # 文章状态:发布
article.save()
# 访问文章管理页面
response = self.client.get(article.get_admin_url())
# 断言页面访问成功
self.assertEqual(response.status_code, 200)
def test_validate_register(self):
"""测试用户注册功能"""
# 断言注册前邮箱不存在
self.assertEquals(
0, len(BlogUser.objects.filter(email='user123@user.com')))
# 发送注册POST请求
response = self.client.post(reverse('account:register'), {
'username': 'user1233', # 用户名
'email': 'user123@user.com', # 邮箱
'password1': 'password123!q@wE#R$T', # 密码
'password2': 'password123!q@wE#R$T', # 确认密码
})
# 断言注册后邮箱存在
self.assertEquals(
1, len(BlogUser.objects.filter(email='user123@user.com')))
# 获取刚注册的用户
user = BlogUser.objects.filter(email='user123@user.com')[0]
# 生成验证签名
sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
# 构建验证URL路径
path = reverse('accounts:result')
# 构建完整验证URL
url = '{path}?type=validation&id={id}&sign={sign}'.format(
path=path, # 路径
id=user.id, # 用户ID
sign=sign # 签名
)
# 访问验证URL
response = self.client.get(url)
# 断言页面访问成功
self.assertEqual(response.status_code, 200)
# 登录用户
self.client.login(username='user1233', password='password123!q@wE#R$T')
# 获取用户并设置为管理员
user = BlogUser.objects.filter(email='user123@user.com')[0]
user.is_superuser = True # 设置为超级用户
user.is_staff = True # 设置为工作人员
user.save()
# 删除侧边栏缓存
delete_sidebar_cache()
# 创建分类
category = Category()
category.name = "categoryaaa" # 分类名称
category.creation_time = timezone.now() # 创建时间
category.last_modify_time = timezone.now() # 最后修改时间
category.save()
# 创建文章
article = Article()
article.category = category # 文章分类
article.title = "nicetitle333" # 文章标题
article.body = "nicecontentttt" # 文章内容
article.author = user # 文章作者
article.type = 'a' # 文章类型
article.status = 'p' # 文章状态:发布
article.save()
# 访问文章管理页面
response = self.client.get(article.get_admin_url())
# 断言页面访问成功
self.assertEqual(response.status_code, 200)
# 登出用户
response = self.client.get(reverse('account:logout'))
# 断言登出成功(重定向状态码)
self.assertIn(response.status_code, [301, 302, 200])
# 再次访问文章管理页面(应该被重定向到登录页)
response = self.client.get(article.get_admin_url())
# 断言被重定向
self.assertIn(response.status_code, [301, 302, 200])
# 使用错误密码尝试登录
response = self.client.post(reverse('account:login'), {
'username': 'user1233', # 用户名
'password': 'password123' # 错误密码
})
# 断言登录失败(重定向状态码)
self.assertIn(response.status_code, [301, 302, 200])
# 再次访问文章管理页面(应该仍然被重定向)
response = self.client.get(article.get_admin_url())
# 断言被重定向
self.assertIn(response.status_code, [301, 302, 200])
def test_verify_email_code(self):
"""测试邮箱验证码功能"""
# 测试邮箱
to_email = "admin@admin.com"
# 生成验证码
code = generate_code()
# 设置验证码到缓存
utils.set_code(to_email, code)
# 发送验证邮件
utils.send_verify_email(to_email, code)
# 验证正确验证码
err = utils.verify("admin@admin.com", code)
# 断言验证成功返回None
self.assertEqual(err, None)
# 验证错误验证码
err = utils.verify("admin@123.com", code)
# 断言验证失败(返回错误信息字符串)
self.assertEqual(type(err), str)
def test_forget_password_email_code_success(self):
"""测试成功获取忘记密码验证码"""
# 发送获取验证码的POST请求
resp = self.client.post(
path=reverse("account:forget_password_code"), # URL路径
data=dict(email="admin@admin.com") # 请求数据:邮箱
)
# 断言响应状态码为200
self.assertEqual(resp.status_code, 200)
# 断言响应内容为"ok"
self.assertEqual(resp.content.decode("utf-8"), "ok")
def test_forget_password_email_code_fail(self):
"""测试获取忘记密码验证码失败情况"""
# 发送空数据的POST请求
resp = self.client.post(
path=reverse("account:forget_password_code"), # URL路径
data=dict() # 空数据
)
# 断言返回错误信息
self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱")
# 发送无效邮箱的POST请求
resp = self.client.post(
path=reverse("account:forget_password_code"), # URL路径
data=dict(email="admin@com") # 无效邮箱格式
)
# 断言返回错误信息
self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱")
def test_forget_password_email_success(self):
"""测试成功重置密码"""
# 生成验证码
code = generate_code()
# 设置验证码到缓存
utils.set_code(self.blog_user.email, code)
# 准备请求数据
data = dict(
new_password1=self.new_test, # 新密码
new_password2=self.new_test, # 确认密码
email=self.blog_user.email, # 邮箱
code=code, # 验证码
)
# 发送重置密码的POST请求
resp = self.client.post(
path=reverse("account:forget_password"), # URL路径
data=data # 请求数据
)
# 断言重定向响应状态码302
self.assertEqual(resp.status_code, 302)
# 验证用户密码是否修改成功
blog_user = BlogUser.objects.filter(
email=self.blog_user.email,
).first() # 类型注解BlogUser
# 断言用户存在
self.assertNotEqual(blog_user, None)
# 断言新密码验证通过
self.assertEqual(blog_user.check_password(data["new_password1"]), True)
def test_forget_password_email_not_user(self):
"""测试重置密码时邮箱不存在的情况"""
# 准备请求数据(不存在的邮箱)
data = dict(
new_password1=self.new_test, # 新密码
new_password2=self.new_test, # 确认密码
email="123@123.com", # 不存在的邮箱
code="123456", # 验证码
)
# 发送重置密码的POST请求
resp = self.client.post(
path=reverse("account:forget_password"), # URL路径
data=data # 请求数据
)
# 断言返回表单页面状态码200
self.assertEqual(resp.status_code, 200)
def test_forget_password_email_code_error(self):
"""测试重置密码时验证码错误的情况"""
# 生成验证码
code = generate_code()
# 设置验证码到缓存
utils.set_code(self.blog_user.email, code)
# 准备请求数据(错误的验证码)
data = dict(
new_password1=self.new_test, # 新密码
new_password2=self.new_test, # 确认密码
email=self.blog_user.email, # 邮箱
code="111111", # 错误的验证码
)
# 发送重置密码的POST请求
resp = self.client.post(
path=reverse("account:forget_password"), # URL路径
data=data # 请求数据
)
# 断言返回表单页面状态码200
self.assertEqual(resp.status_code, 200)

@ -1,28 +0,0 @@
from django.urls import path
from django.urls import re_path
from . import views
from .forms import LoginForm
app_name = "accounts"
urlpatterns = [re_path(r'^login/$',
views.LoginView.as_view(success_url='/'),
name='login',
kwargs={'authentication_form': LoginForm}),
re_path(r'^register/$',
views.RegisterView.as_view(success_url="/"),
name='register'),
re_path(r'^logout/$',
views.LogoutView.as_view(),
name='logout'),
path(r'account/result.html',
views.account_result,
name='result'),
re_path(r'^forget_password/$',
views.ForgetPasswordView.as_view(),
name='forget_password'),
re_path(r'^forget_password_code/$',
views.ForgetPasswordEmailCode.as_view(),
name='forget_password_code'),
]

@ -1,53 +0,0 @@
# 导入URL路径函数
from django.urls import path
# 导入正则URL路径函数兼容老版本
from django.urls import re_path
# 导入当前应用的视图
from . import views
# 导入登录表单
from .forms import LoginForm
# 应用命名空间用于URL反向解析
app_name = "accounts"
# URL模式列表
urlpatterns = [
# 登录URL - 使用正则表达式匹配 /login/ 路径
re_path(r'^login/$',
# 使用LoginView视图类登录成功后重定向到首页
views.LoginView.as_view(success_url='/'),
name='login', # URL名称login
# 传入额外参数:指定认证表单类
kwargs={'authentication_form': LoginForm}),
# 注册URL - 使用正则表达式匹配 /register/ 路径
re_path(r'^register/$',
# 使用RegisterView视图类注册成功后重定向到首页
views.RegisterView.as_view(success_url="/"),
name='register'), # URL名称register
# 登出URL - 使用正则表达式匹配 /logout/ 路径
re_path(r'^logout/$',
# 使用LogoutView视图类
views.LogoutView.as_view(),
name='logout'), # URL名称logout
# 账户结果页面URL - 使用path匹配固定路径
path(r'account/result.html',
# 使用account_result函数视图
views.account_result,
name='result'), # URL名称result
# 忘记密码URL - 使用正则表达式匹配 /forget_password/ 路径
re_path(r'^forget_password/$',
# 使用ForgetPasswordView视图类
views.ForgetPasswordView.as_view(),
name='forget_password'), # URL名称forget_password
# 获取忘记密码验证码URL - 使用正则表达式匹配 /forget_password_code/ 路径
re_path(r'^forget_password_code/$',
# 使用ForgetPasswordEmailCode视图类
views.ForgetPasswordEmailCode.as_view(),
name='forget_password_code'), # URL名称forget_password_code
]

@ -1,42 +0,0 @@
from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend
from django.db.models import Q
class EmailOrUsernameModelBackend(ModelBackend):
"""
允许使用用户名或邮箱登录
支持大小写不敏感的邮箱登录
"""
def authenticate(self, request, username=None, password=None, **kwargs):
if username is None or password is None:
return None
UserModel = get_user_model()
# 判断输入是否为邮箱格式
if '@' in username:
# 邮箱登录,不区分大小写
try:
user = UserModel.objects.get(email__iexact=username)
except UserModel.DoesNotExist:
return None
else:
# 用户名登录
try:
user = UserModel.objects.get(username=username)
except UserModel.DoesNotExist:
return None
# 验证密码并检查用户是否激活
if user.check_password(password) and self.user_can_authenticate(user):
return user
return None
def get_user(self, user_id):
UserModel = get_user_model()
try:
return UserModel.objects.get(pk=user_id)
except UserModel.DoesNotExist:
return None

@ -1,42 +0,0 @@
# 导入获取用户模型的函数
from django.contrib.auth import get_user_model
# 导入模型后端认证基类
from django.contrib.auth.backends import ModelBackend
class EmailOrUsernameModelBackend(ModelBackend):
"""
自定义认证后端允许使用用户名或邮箱登录
继承自Django的ModelBackend
"""
def authenticate(self, request, username=None, password=None, **kwargs):
"""用户认证方法"""
# 检查用户名中是否包含@符号(判断是否为邮箱)
if '@' in username:
# 如果是邮箱,设置查询参数为邮箱
kwargs = {'email': username}
else:
# 如果是用户名,设置查询参数为用户名
kwargs = {'username': username}
try:
# 根据查询参数获取用户对象
user = get_user_model().objects.get(**kwargs)
# 检查密码是否正确
if user.check_password(password):
# 密码正确,返回用户对象
return user
# 捕获用户不存在的异常
except get_user_model().DoesNotExist:
# 用户不存在返回None
return None
def get_user(self, username):
"""根据用户ID获取用户对象"""
try:
# 根据主键用户ID获取用户对象
return get_user_model().objects.get(pk=username)
# 捕获用户不存在的异常
except get_user_model().DoesNotExist:
# 用户不存在返回None
return None

@ -1,49 +0,0 @@
import typing
from datetime import timedelta
from django.core.cache import cache
from django.utils.translation import gettext
from django.utils.translation import gettext_lazy as _
from djangoblog.utils import send_email
_code_ttl = timedelta(minutes=5)
def send_verify_email(to_mail: str, code: str, subject: str = _("Verify Email")):
"""发送重设密码验证码
Args:
to_mail: 接受邮箱
subject: 邮件主题
code: 验证码
"""
html_content = _(
"You are resetting the password, the verification code is%(code)s, valid within 5 minutes, please keep it "
"properly") % {'code': code}
send_email([to_mail], subject, html_content)
def verify(email: str, code: str) -> typing.Optional[str]:
"""验证code是否有效
Args:
email: 请求邮箱
code: 验证码
Return:
如果有错误就返回错误str
Node:
这里的错误处理不太合理应该采用raise抛出
否测调用方也需要对error进行处理
"""
cache_code = get_code(email)
if cache_code != code:
return gettext("Verification code error")
def set_code(email: str, code: str):
"""设置code"""
cache.set(email, code, _code_ttl.seconds)
def get_code(email: str) -> typing.Optional[str]:
"""获取code"""
return cache.get(email)

@ -1,70 +0,0 @@
# 导入类型提示模块
import typing
# 导入时间间隔类
from datetime import timedelta
# 导入Django缓存框架
from django.core.cache import cache
# 导入国际化翻译函数
from django.utils.translation import gettext
# 导入延迟翻译函数
from django.utils.translation import gettext_lazy as _
# 导入发送邮件的工具函数
from djangoblog.utils import send_email
# 验证码有效期5分钟
_code_ttl = timedelta(minutes=5)
def send_verify_email(to_mail: str, code: str, subject: str = _("Verify Email")):
"""发送验证邮件,用于密码重置等场景
Args:
to_mail: 接收邮件的邮箱地址
subject: 邮件主题默认为"验证邮箱"
code: 验证码内容
"""
# 构建邮件HTML内容包含验证码信息
html_content = _(
# 翻译文本:您正在重置密码,验证码是:{code}5分钟内有效请妥善保管
"You are resetting the password, the verification code is%(code)s, valid within 5 minutes, please keep it "
"properly"
) % {'code': code} # 将code插入到格式化字符串中
# 调用发送邮件函数
send_email([to_mail], subject, html_content)
def verify(email: str, code: str) -> typing.Optional[str]:
"""验证验证码是否有效
Args:
email: 请求验证的邮箱地址
code: 用户输入的验证码
Return:
如果验证失败返回错误信息字符串验证成功返回None
Note:
这里的错误处理不太合理应该采用raise抛出异常
否则调用方也需要对error进行处理
"""
# 从缓存中获取该邮箱对应的验证码
cache_code = get_code(email)
# 比较缓存中的验证码和用户输入的验证码
if cache_code != code:
# 如果不匹配,返回错误信息
return gettext("Verification code error")
# 验证成功返回None
def set_code(email: str, code: str):
"""将验证码设置到缓存中"""
# 使用cache.set方法key为邮箱value为验证码设置过期时间
cache.set(email, code, _code_ttl.seconds)
def get_code(email: str) -> typing.Optional[str]:
"""从缓存中获取验证码"""
# 使用cache.get方法根据邮箱获取验证码
return cache.get(email)

@ -1,204 +0,0 @@
import logging
from django.utils.translation import gettext_lazy as _
from django.conf import settings
from django.contrib import auth
from django.contrib.auth import REDIRECT_FIELD_NAME
from django.contrib.auth import get_user_model
from django.contrib.auth import logout
from django.contrib.auth.forms import AuthenticationForm
from django.contrib.auth.hashers import make_password
from django.http import HttpResponseRedirect, HttpResponseForbidden
from django.http.request import HttpRequest
from django.http.response import HttpResponse
from django.shortcuts import get_object_or_404
from django.shortcuts import render
from django.urls import reverse
from django.utils.decorators import method_decorator
from django.utils.http import url_has_allowed_host_and_scheme
from django.views import View
from django.views.decorators.cache import never_cache
from django.views.decorators.csrf import csrf_protect
from django.views.decorators.debug import sensitive_post_parameters
from django.views.generic import FormView, RedirectView
from djangoblog.utils import send_email, get_sha256, get_current_site, generate_code, delete_sidebar_cache
from . import utils
from .forms import RegisterForm, LoginForm, ForgetPasswordForm, ForgetPasswordCodeForm
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):
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)
site = get_current_site().domain
sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
if settings.DEBUG:
site = '127.0.0.1:8000'
path = reverse('account:result')
url = "http://{site}{path}?type=validation&id={id}&sign={sign}".format(
site=site, path=path, id=user.id, sign=sign)
content = """
<p>请点击下面链接验证您的邮箱</p>
<a href="{url}" rel="bookmark">{url}</a>
再次感谢您
<br />
如果上面链接无法打开请将此链接复制至浏览器
{url}
""".format(url=url)
send_email(
emailto=[
user.email,
],
title='验证您的电子邮箱',
content=content)
url = reverse('accounts:result') + \
'?type=register&id=' + str(user.id)
return HttpResponseRedirect(url)
else:
return self.render_to_response({
'form': form
})
class LogoutView(RedirectView):
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()
return super(LogoutView, self).get(request, *args, **kwargs)
class LoginView(FormView):
form_class = LoginForm
template_name = 'account/login.html'
success_url = '/'
redirect_field_name = REDIRECT_FIELD_NAME
login_ttl = 2626560 # 一个月的时间
@method_decorator(sensitive_post_parameters('password'))
@method_decorator(csrf_protect)
@method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs):
return super(LoginView, self).dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
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()
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)
return super(LoginView, self).form_valid(form)
# return HttpResponseRedirect('/')
else:
return self.render_to_response({
'form': form
})
def get_success_url(self):
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:
return HttpResponseRedirect('/')
if type and type in ['register', 'validation']:
if type == 'register':
content = '''
恭喜您注册成功一封验证邮件已经发送到您的邮箱请验证您的邮箱后登录本站
'''
title = '注册成功'
else:
c_sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
sign = request.GET.get('sign')
if sign != c_sign:
return HttpResponseForbidden()
user.is_active = True
user.save()
content = '''
恭喜您已经成功的完成邮箱验证您现在可以使用您的账号来登录本站
'''
title = '验证成功'
return render(request, 'account/result.html', {
'title': title,
'content': content
})
else:
return HttpResponseRedirect('/')
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.save()
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)
return HttpResponse("ok")

@ -1,327 +0,0 @@
# 导入日志模块
import logging
# 导入延迟翻译函数
from django.utils.translation import gettext_lazy as _
# 导入Django设置
from django.conf import settings
# 导入Django认证框架
from django.contrib import auth
# 导入重定向字段名常量
from django.contrib.auth import REDIRECT_FIELD_NAME
# 导入获取用户模型的函数
from django.contrib.auth import get_user_model
# 导入登出函数
from django.contrib.auth import logout
# 导入Django内置认证表单
from django.contrib.auth.forms import AuthenticationForm
# 导入密码哈希函数
from django.contrib.auth.hashers import make_password
# 导入HTTP响应重定向和禁止访问响应
from django.http import HttpResponseRedirect, HttpResponseForbidden
# 导入HTTP请求类型
from django.http.request import HttpRequest
# 导入HTTP响应类型
from django.http.response import HttpResponse
# 导入快捷函数获取对象或404错误
from django.shortcuts import get_object_or_404
# 导入快捷函数:渲染模板
from django.shortcuts import render
# 导入URL反向解析
from django.urls import reverse
# 导入方法装饰器
from django.utils.decorators import method_decorator
# 导入URL安全验证函数
from django.utils.http import url_has_allowed_host_and_scheme
# 导入基于类的视图基类
from django.views import View
# 导入禁止缓存装饰器
from django.views.decorators.cache import never_cache
# 导入CSRF保护装饰器
from django.views.decorators.csrf import csrf_protect
# 导入敏感参数保护装饰器
from django.views.decorators.debug import sensitive_post_parameters
# 导入通用视图类
from django.views.generic import FormView, RedirectView
# 导入项目工具函数
from djangoblog.utils import send_email, get_sha256, get_current_site, generate_code, delete_sidebar_cache
# 导入当前应用的工具函数
from . import utils
# 导入自定义表单
from .forms import RegisterForm, LoginForm, ForgetPasswordForm, ForgetPasswordCodeForm
# 导入用户模型
from .models import BlogUser
# 获取当前模块的日志器
logger = logging.getLogger(__name__)
# 在这里创建视图
class RegisterView(FormView):
"""用户注册视图"""
# 指定使用的表单类
form_class = RegisterForm
# 指定使用的模板
template_name = 'account/registration_form.html'
@method_decorator(csrf_protect) # CSRF保护装饰器
def dispatch(self, *args, **kwargs):
"""处理请求分发"""
# 调用父类的dispatch方法
return super(RegisterView, self).dispatch(*args, **kwargs)
def form_valid(self, form):
"""处理表单验证通过的情况"""
# 检查表单是否有效
if form.is_valid():
# 保存表单数据但不提交到数据库commit=False
user = form.save(False)
# 设置用户为未激活状态(需要邮箱验证)
user.is_active = False
# 设置用户来源为注册页面
user.source = 'Register'
# 保存用户到数据库
user.save(True)
# 获取当前站点域名
site = get_current_site().domain
# 生成验证签名:对密钥+用户ID进行双重SHA256哈希
sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
# 如果是调试模式,使用本地地址
if settings.DEBUG:
site = '127.0.0.1:8000'
# 获取结果页面的URL路径
path = reverse('account:result')
# 构建完整的验证URL
url = "http://{site}{path}?type=validation&id={id}&sign={sign}".format(
site=site, # 站点地址
path=path, # 结果页面路径
id=user.id, # 用户ID
sign=sign # 验证签名
)
# 构建邮件内容
content = """
<p>请点击下面链接验证您的邮箱</p>
<a href="{url}" rel="bookmark">{url}</a>
再次感谢您
<br />
如果上面链接无法打开请将此链接复制至浏览器
{url}
""".format(url=url)
# 发送验证邮件
send_email(
emailto=[user.email], # 收件人邮箱
title='验证您的电子邮箱', # 邮件标题
content=content # 邮件内容
)
# 构建注册结果页面URL
url = reverse('accounts:result') + '?type=register&id=' + str(user.id)
# 重定向到结果页面
return HttpResponseRedirect(url)
else:
# 表单无效,重新渲染表单并显示错误
return self.render_to_response({'form': form})
class LogoutView(RedirectView):
"""用户登出视图"""
# 登出后重定向的URL
url = '/login/'
@method_decorator(never_cache) # 禁止缓存装饰器
def dispatch(self, request, *args, **kwargs):
"""处理请求分发"""
# 调用父类的dispatch方法
return super(LogoutView, self).dispatch(request, *args, **kwargs)
def get(self, request, *args, **kwargs):
"""处理GET请求登出操作"""
# 调用Django登出函数
logout(request)
# 删除侧边栏缓存
delete_sidebar_cache()
# 调用父类的get方法进行重定向
return super(LogoutView, self).get(request, *args, **kwargs)
class LoginView(FormView):
"""用户登录视图"""
# 指定使用的表单类
form_class = LoginForm
# 指定使用的模板
template_name = 'account/login.html'
# 登录成功后的重定向URL
success_url = '/'
# 重定向字段名
redirect_field_name = REDIRECT_FIELD_NAME
# 登录会话保持时间:一个月(秒数)
login_ttl = 2626560
# 方法装饰器保护敏感参数、CSRF保护、禁止缓存
@method_decorator(sensitive_post_parameters('password'))
@method_decorator(csrf_protect)
@method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs):
"""处理请求分发"""
# 调用父类的dispatch方法
return super(LoginView, self).dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
"""获取模板上下文数据"""
# 从GET参数中获取重定向URL
redirect_to = self.request.GET.get(self.redirect_field_name)
# 如果重定向URL为空设置为首页
if redirect_to is None:
redirect_to = '/'
# 将重定向URL添加到上下文数据中
kwargs['redirect_to'] = redirect_to
# 调用父类方法获取基础上下文数据
return super(LoginView, self).get_context_data(**kwargs)
def form_valid(self, form):
"""处理表单验证通过的情况"""
# 创建认证表单实例使用POST数据
form = AuthenticationForm(data=self.request.POST, request=self.request)
# 检查表单是否有效
if form.is_valid():
# 删除侧边栏缓存
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)
# 调用父类的form_valid方法会处理重定向
return super(LoginView, self).form_valid(form)
else:
# 表单无效,重新渲染表单并显示错误
return self.render_to_response({'form': form})
def get_success_url(self):
"""获取登录成功后的重定向URL"""
# 从POST数据中获取重定向URL
redirect_to = self.request.POST.get(self.redirect_field_name)
# 验证重定向URL是否安全同源策略
if not url_has_allowed_host_and_scheme(
url=redirect_to,
allowed_hosts=[self.request.get_host()]):
# 如果不安全使用默认的成功URL
redirect_to = self.success_url
# 返回重定向URL
return redirect_to
def account_result(request):
"""账户操作结果页面视图函数"""
# 从GET参数获取操作类型
type = request.GET.get('type')
# 从GET参数获取用户ID
id = request.GET.get('id')
# 根据ID获取用户对象如果不存在返回404
user = get_object_or_404(get_user_model(), id=id)
# 记录日志
logger.info(type)
# 如果用户已激活,重定向到首页
if user.is_active:
return HttpResponseRedirect('/')
# 检查操作类型是否为注册或验证
if type and type in ['register', 'validation']:
# 如果是注册操作
if type == 'register':
# 设置注册成功的内容和标题
content = '''
恭喜您注册成功一封验证邮件已经发送到您的邮箱请验证您的邮箱后登录本站
'''
title = '注册成功'
else:
# 生成验证签名
c_sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
# 从GET参数获取签名
sign = request.GET.get('sign')
# 验证签名是否正确
if sign != c_sign:
# 签名错误,返回禁止访问
return HttpResponseForbidden()
# 激活用户账户
user.is_active = True
# 保存用户
user.save()
# 设置验证成功的内容和标题
content = '''
恭喜您已经成功的完成邮箱验证您现在可以使用您的账号来登录本站
'''
title = '验证成功'
# 渲染结果页面
return render(request, 'account/result.html', {
'title': title, # 页面标题
'content': content # 页面内容
})
else:
# 无效的操作类型,重定向到首页
return HttpResponseRedirect('/')
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.save()
# 重定向到登录页面
return HttpResponseRedirect('/login/')
else:
# 表单无效,重新渲染表单并显示错误
return self.render_to_response({'form': form})
class ForgetPasswordEmailCode(View):
"""获取忘记密码验证码的API视图"""
def post(self, request: HttpRequest):
"""处理POST请求"""
# 创建表单实例并验证数据
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)
# 返回成功响应
return HttpResponse("ok")

@ -1,59 +0,0 @@
from django import forms
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.forms import UserChangeForm
from django.contrib.auth.forms import UsernameField
from django.utils.translation import gettext_lazy as _
# Register your models here.
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',)
def clean_password2(self):
# Check that the two password entries match
password1 = self.cleaned_data.get("password1")
password2 = self.cleaned_data.get("password2")
if password1 and password2 and password1 != password2:
raise forms.ValidationError(_("passwords do not match"))
return password2
def save(self, commit=True):
# Save the provided password in hashed format
user = super().save(commit=False)
user.set_password(self.cleaned_data["password1"])
if commit:
user.source = 'adminsite'
user.save()
return user
class BlogUserChangeForm(UserChangeForm):
class Meta:
model = BlogUser
fields = '__all__'
field_classes = {'username': UsernameField}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
class BlogUserAdmin(UserAdmin):
form = BlogUserChangeForm
add_form = BlogUserCreationForm
list_display = (
'id',
'nickname',
'username',
'email',
'last_login',
'date_joined',
'source')
list_display_links = ('id', 'username')
ordering = ('-id',)

@ -1,5 +0,0 @@
from django.apps import AppConfig
class AccountsConfig(AppConfig):
name = 'accounts'

@ -1,117 +0,0 @@
from django import forms
from django.contrib.auth import get_user_model, password_validation
from django.contrib.auth.forms import AuthenticationForm, UserCreationForm
from django.core.exceptions import ValidationError
from django.forms import widgets
from django.utils.translation import gettext_lazy as _
from . import utils
from .models import BlogUser
class LoginForm(AuthenticationForm):
def __init__(self, *args, **kwargs):
super(LoginForm, self).__init__(*args, **kwargs)
self.fields['username'].widget = widgets.TextInput(
attrs={'placeholder': "username", "class": "form-control"})
self.fields['password'].widget = widgets.PasswordInput(
attrs={'placeholder': "password", "class": "form-control"})
class RegisterForm(UserCreationForm):
def __init__(self, *args, **kwargs):
super(RegisterForm, self).__init__(*args, **kwargs)
self.fields['username'].widget = widgets.TextInput(
attrs={'placeholder': "username", "class": "form-control"})
self.fields['email'].widget = widgets.EmailInput(
attrs={'placeholder': "email", "class": "form-control"})
self.fields['password1'].widget = widgets.PasswordInput(
attrs={'placeholder': "password", "class": "form-control"})
self.fields['password2'].widget = widgets.PasswordInput(
attrs={'placeholder': "repeat password", "class": "form-control"})
def clean_email(self):
email = self.cleaned_data['email']
if get_user_model().objects.filter(email=email).exists():
raise ValidationError(_("email already exists"))
return email
class Meta:
model = get_user_model()
fields = ("username", "email")
class ForgetPasswordForm(forms.Form):
new_password1 = forms.CharField(
label=_("New password"),
widget=forms.PasswordInput(
attrs={
"class": "form-control",
'placeholder': _("New password")
}
),
)
new_password2 = forms.CharField(
label="确认密码",
widget=forms.PasswordInput(
attrs={
"class": "form-control",
'placeholder': _("Confirm password")
}
),
)
email = forms.EmailField(
label='邮箱',
widget=forms.TextInput(
attrs={
'class': 'form-control',
'placeholder': _("Email")
}
),
)
code = forms.CharField(
label=_('Code'),
widget=forms.TextInput(
attrs={
'class': 'form-control',
'placeholder': _("Code")
}
),
)
def clean_new_password2(self):
password1 = self.data.get("new_password1")
password2 = self.data.get("new_password2")
if password1 and password2 and password1 != password2:
raise ValidationError(_("passwords do not match"))
password_validation.validate_password(password2)
return password2
def clean_email(self):
user_email = self.cleaned_data.get("email")
if not BlogUser.objects.filter(
email=user_email
).exists():
# todo 这里的报错提示可以判断一个邮箱是不是注册过,如果不想暴露可以修改
raise ValidationError(_("email does not exist"))
return user_email
def clean_code(self):
code = self.cleaned_data.get("code")
error = utils.verify(
email=self.cleaned_data.get("email"),
code=code,
)
if error:
raise ValidationError(error)
return code
class ForgetPasswordCodeForm(forms.Form):
email = forms.EmailField(
label=_('Email'),
)

@ -1,49 +0,0 @@
# Generated by Django 4.1.7 on 2023-03-02 07:14
import django.contrib.auth.models
import django.contrib.auth.validators
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
]
operations = [
migrations.CreateModel(
name='BlogUser',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('nickname', models.CharField(blank=True, max_length=100, 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='修改时间')),
('source', models.CharField(blank=True, max_length=100, verbose_name='创建来源')),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
],
options={
'verbose_name': '用户',
'verbose_name_plural': '用户',
'ordering': ['-id'],
'get_latest_by': 'id',
},
managers=[
('objects', django.contrib.auth.models.UserManager()),
],
),
]

@ -1,46 +0,0 @@
# Generated by Django 4.2.5 on 2023-09-06 13:13
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('accounts', '0001_initial'),
]
operations = [
migrations.AlterModelOptions(
name='bloguser',
options={'get_latest_by': 'id', 'ordering': ['-id'], 'verbose_name': 'user', 'verbose_name_plural': 'user'},
),
migrations.RemoveField(
model_name='bloguser',
name='created_time',
),
migrations.RemoveField(
model_name='bloguser',
name='last_mod_time',
),
migrations.AddField(
model_name='bloguser',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
migrations.AddField(
model_name='bloguser',
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'),
),
migrations.AlterField(
model_name='bloguser',
name='nickname',
field=models.CharField(blank=True, max_length=100, verbose_name='nick name'),
),
migrations.AlterField(
model_name='bloguser',
name='source',
field=models.CharField(blank=True, max_length=100, verbose_name='create source'),
),
]

@ -1,35 +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):
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)
source = models.CharField(_('create source'), max_length=100, blank=True)
def get_absolute_url(self):
return reverse(
'blog:author_detail', kwargs={
'author_name': self.username})
def __str__(self):
return self.email
def get_full_url(self):
site = get_current_site().domain
url = "https://{site}{path}".format(site=site,
path=self.get_absolute_url())
return url
class Meta:
ordering = ['-id']
verbose_name = _('user')
verbose_name_plural = verbose_name
get_latest_by = 'id'

@ -1,207 +0,0 @@
from django.test import Client, RequestFactory, TestCase
from django.urls import reverse
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from accounts.models import BlogUser
from blog.models import Article, Category
from djangoblog.utils import *
from . import utils
# Create your tests here.
class AccountTest(TestCase):
def setUp(self):
self.client = Client()
self.factory = RequestFactory()
self.blog_user = BlogUser.objects.create_user(
username="test",
email="admin@admin.com",
password="12345678"
)
self.new_test = "xxx123--="
def test_validate_account(self):
site = get_current_site().domain
user = BlogUser.objects.create_superuser(
email="liangliangyy1@gmail.com",
username="liangliangyy1",
password="qwer!@#$ggg")
testuser = BlogUser.objects.get(username='liangliangyy1')
loginresult = self.client.login(
username='liangliangyy1',
password='qwer!@#$ggg')
self.assertEqual(loginresult, True)
response = self.client.get('/admin/')
self.assertEqual(response.status_code, 200)
category = Category()
category.name = "categoryaaa"
category.creation_time = timezone.now()
category.last_modify_time = timezone.now()
category.save()
article = Article()
article.title = "nicetitleaaa"
article.body = "nicecontentaaa"
article.author = user
article.category = category
article.type = 'a'
article.status = 'p'
article.save()
response = self.client.get(article.get_admin_url())
self.assertEqual(response.status_code, 200)
def test_validate_register(self):
self.assertEquals(
0, len(
BlogUser.objects.filter(
email='user123@user.com')))
response = self.client.post(reverse('account:register'), {
'username': 'user1233',
'email': 'user123@user.com',
'password1': 'password123!q@wE#R$T',
'password2': 'password123!q@wE#R$T',
})
self.assertEquals(
1, len(
BlogUser.objects.filter(
email='user123@user.com')))
user = BlogUser.objects.filter(email='user123@user.com')[0]
sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
path = reverse('accounts:result')
url = '{path}?type=validation&id={id}&sign={sign}'.format(
path=path, id=user.id, sign=sign)
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.client.login(username='user1233', password='password123!q@wE#R$T')
user = BlogUser.objects.filter(email='user123@user.com')[0]
user.is_superuser = True
user.is_staff = True
user.save()
delete_sidebar_cache()
category = Category()
category.name = "categoryaaa"
category.creation_time = timezone.now()
category.last_modify_time = timezone.now()
category.save()
article = Article()
article.category = category
article.title = "nicetitle333"
article.body = "nicecontentttt"
article.author = user
article.type = 'a'
article.status = 'p'
article.save()
response = self.client.get(article.get_admin_url())
self.assertEqual(response.status_code, 200)
response = self.client.get(reverse('account:logout'))
self.assertIn(response.status_code, [301, 302, 200])
response = self.client.get(article.get_admin_url())
self.assertIn(response.status_code, [301, 302, 200])
response = self.client.post(reverse('account:login'), {
'username': 'user1233',
'password': 'password123'
})
self.assertIn(response.status_code, [301, 302, 200])
response = self.client.get(article.get_admin_url())
self.assertIn(response.status_code, [301, 302, 200])
def test_verify_email_code(self):
to_email = "admin@admin.com"
code = generate_code()
utils.set_code(to_email, code)
utils.send_verify_email(to_email, code)
err = utils.verify("admin@admin.com", code)
self.assertEqual(err, None)
err = utils.verify("admin@123.com", code)
self.assertEqual(type(err), str)
def test_forget_password_email_code_success(self):
resp = self.client.post(
path=reverse("account:forget_password_code"),
data=dict(email="admin@admin.com")
)
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.content.decode("utf-8"), "ok")
def test_forget_password_email_code_fail(self):
resp = self.client.post(
path=reverse("account:forget_password_code"),
data=dict()
)
self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱")
resp = self.client.post(
path=reverse("account:forget_password_code"),
data=dict(email="admin@com")
)
self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱")
def test_forget_password_email_success(self):
code = generate_code()
utils.set_code(self.blog_user.email, code)
data = dict(
new_password1=self.new_test,
new_password2=self.new_test,
email=self.blog_user.email,
code=code,
)
resp = self.client.post(
path=reverse("account:forget_password"),
data=data
)
self.assertEqual(resp.status_code, 302)
# 验证用户密码是否修改成功
blog_user = BlogUser.objects.filter(
email=self.blog_user.email,
).first() # type: BlogUser
self.assertNotEqual(blog_user, None)
self.assertEqual(blog_user.check_password(data["new_password1"]), True)
def test_forget_password_email_not_user(self):
data = dict(
new_password1=self.new_test,
new_password2=self.new_test,
email="123@123.com",
code="123456",
)
resp = self.client.post(
path=reverse("account:forget_password"),
data=data
)
self.assertEqual(resp.status_code, 200)
def test_forget_password_email_code_error(self):
code = generate_code()
utils.set_code(self.blog_user.email, code)
data = dict(
new_password1=self.new_test,
new_password2=self.new_test,
email=self.blog_user.email,
code="111111",
)
resp = self.client.post(
path=reverse("account:forget_password"),
data=data
)
self.assertEqual(resp.status_code, 200)

@ -1,28 +0,0 @@
from django.urls import path
from django.urls import re_path
from . import views
from .forms import LoginForm
app_name = "accounts"
urlpatterns = [re_path(r'^login/$',
views.LoginView.as_view(success_url='/'),
name='login',
kwargs={'authentication_form': LoginForm}),
re_path(r'^register/$',
views.RegisterView.as_view(success_url="/"),
name='register'),
re_path(r'^logout/$',
views.LogoutView.as_view(),
name='logout'),
path(r'account/result.html',
views.account_result,
name='result'),
re_path(r'^forget_password/$',
views.ForgetPasswordView.as_view(),
name='forget_password'),
re_path(r'^forget_password_code/$',
views.ForgetPasswordEmailCode.as_view(),
name='forget_password_code'),
]

@ -1,26 +0,0 @@
from django.contrib.auth import 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 = get_user_model().objects.get(**kwargs)
if user.check_password(password):
return user
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,49 +0,0 @@
import typing
from datetime import timedelta
from django.core.cache import cache
from django.utils.translation import gettext
from django.utils.translation import gettext_lazy as _
from djangoblog.utils import send_email
_code_ttl = timedelta(minutes=5)
def send_verify_email(to_mail: str, code: str, subject: str = _("Verify Email")):
"""发送重设密码验证码
Args:
to_mail: 接受邮箱
subject: 邮件主题
code: 验证码
"""
html_content = _(
"You are resetting the password, the verification code is%(code)s, valid within 5 minutes, please keep it "
"properly") % {'code': code}
send_email([to_mail], subject, html_content)
def verify(email: str, code: str) -> typing.Optional[str]:
"""验证code是否有效
Args:
email: 请求邮箱
code: 验证码
Return:
如果有错误就返回错误str
Node:
这里的错误处理不太合理应该采用raise抛出
否测调用方也需要对error进行处理
"""
cache_code = get_code(email)
if cache_code != code:
return gettext("Verification code error")
def set_code(email: str, code: str):
"""设置code"""
cache.set(email, code, _code_ttl.seconds)
def get_code(email: str) -> typing.Optional[str]:
"""获取code"""
return cache.get(email)

@ -1,204 +0,0 @@
import logging
from django.utils.translation import gettext_lazy as _
from django.conf import settings
from django.contrib import auth
from django.contrib.auth import REDIRECT_FIELD_NAME
from django.contrib.auth import get_user_model
from django.contrib.auth import logout
from django.contrib.auth.forms import AuthenticationForm
from django.contrib.auth.hashers import make_password
from django.http import HttpResponseRedirect, HttpResponseForbidden
from django.http.request import HttpRequest
from django.http.response import HttpResponse
from django.shortcuts import get_object_or_404
from django.shortcuts import render
from django.urls import reverse
from django.utils.decorators import method_decorator
from django.utils.http import url_has_allowed_host_and_scheme
from django.views import View
from django.views.decorators.cache import never_cache
from django.views.decorators.csrf import csrf_protect
from django.views.decorators.debug import sensitive_post_parameters
from django.views.generic import FormView, RedirectView
from djangoblog.utils import send_email, get_sha256, get_current_site, generate_code, delete_sidebar_cache
from . import utils
from .forms import RegisterForm, LoginForm, ForgetPasswordForm, ForgetPasswordCodeForm
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):
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)
site = get_current_site().domain
sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
if settings.DEBUG:
site = '127.0.0.1:8000'
path = reverse('account:result')
url = "http://{site}{path}?type=validation&id={id}&sign={sign}".format(
site=site, path=path, id=user.id, sign=sign)
content = """
<p>请点击下面链接验证您的邮箱</p>
<a href="{url}" rel="bookmark">{url}</a>
再次感谢您
<br />
如果上面链接无法打开请将此链接复制至浏览器
{url}
""".format(url=url)
send_email(
emailto=[
user.email,
],
title='验证您的电子邮箱',
content=content)
url = reverse('accounts:result') + \
'?type=register&id=' + str(user.id)
return HttpResponseRedirect(url)
else:
return self.render_to_response({
'form': form
})
class LogoutView(RedirectView):
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()
return super(LogoutView, self).get(request, *args, **kwargs)
class LoginView(FormView):
form_class = LoginForm
template_name = 'account/login.html'
success_url = '/'
redirect_field_name = REDIRECT_FIELD_NAME
login_ttl = 2626560 # 一个月的时间
@method_decorator(sensitive_post_parameters('password'))
@method_decorator(csrf_protect)
@method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs):
return super(LoginView, self).dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
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()
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)
return super(LoginView, self).form_valid(form)
# return HttpResponseRedirect('/')
else:
return self.render_to_response({
'form': form
})
def get_success_url(self):
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:
return HttpResponseRedirect('/')
if type and type in ['register', 'validation']:
if type == 'register':
content = '''
恭喜您注册成功一封验证邮件已经发送到您的邮箱请验证您的邮箱后登录本站
'''
title = '注册成功'
else:
c_sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
sign = request.GET.get('sign')
if sign != c_sign:
return HttpResponseForbidden()
user.is_active = True
user.save()
content = '''
恭喜您已经成功的完成邮箱验证您现在可以使用您的账号来登录本站
'''
title = '验证成功'
return render(request, 'account/result.html', {
'title': title,
'content': content
})
else:
return HttpResponseRedirect('/')
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.save()
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)
return HttpResponse("ok")

@ -1,133 +0,0 @@
"""
LJX: Django后台管理配置模块
负责blog应用中各模型在Django admin后台的显示和操作配置
包括文章分类标签友情链接侧边栏等模型的后台管理界面设置
"""
from django import forms
from django.contrib import admin
from django.contrib.auth import get_user_model
from django.urls import reverse
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
# Register your models here.
from .models import Article
class ArticleForm(forms.ModelForm):
"""LJX: 文章表单类,用于后台文章编辑"""
# body = forms.CharField(widget=AdminPagedownWidget())
class Meta:
model = Article
fields = '__all__'
def makr_article_publish(modeladmin, request, queryset):
"""LJX: 批量发布文章的管理动作"""
queryset.update(status='p')
def draft_article(modeladmin, request, queryset):
"""LJX: 批量将文章设为草稿的管理动作"""
queryset.update(status='d')
def close_article_commentstatus(modeladmin, request, queryset):
"""LJX: 批量关闭文章评论的管理动作"""
queryset.update(comment_status='c')
def open_article_commentstatus(modeladmin, request, queryset):
"""LJX: 批量开启文章评论的管理动作"""
queryset.update(comment_status='o')
# LJX: 设置管理动作的描述信息
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):
"""LJX: 文章模型的后台管理配置"""
list_per_page = 20 # LJX: 每页显示20条记录
search_fields = ('body', 'title') # LJX: 可搜索的字段
form = ArticleForm # LJX: 使用自定义表单
list_display = (
'id',
'title',
'author',
'link_to_category', # LJX: 自定义字段显示分类链接
'creation_time',
'views',
'status',
'type',
'article_order')
list_display_links = ('id', 'title') # LJX: 可点击的字段
list_filter = ('status', 'type', 'category') # LJX: 右侧过滤器
filter_horizontal = ('tags',) # LJX: 水平选择器用于多对多字段
exclude = ('creation_time', 'last_modify_time') # LJX: 排除的字段
view_on_site = True # LJX: 显示"在站点查看"按钮
actions = [ # LJX: 可用的批量动作
makr_article_publish,
draft_article,
close_article_commentstatus,
open_article_commentstatus]
def link_to_category(self, obj):
"""LJX: 自定义方法,显示分类名称并链接到分类编辑页面"""
info = (obj.category._meta.app_label, obj.category._meta.model_name)
link = reverse('admin:%s_%s_change' % info, args=(obj.category.id,))
return format_html(u'<a href="%s">%s</a>' % (link, obj.category.name))
link_to_category.short_description = _('category')
def get_form(self, request, obj=None, **kwargs):
"""LJX: 重写获取表单方法,限制作者只能选择超级用户"""
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):
"""LJX: 重写保存模型方法"""
super(ArticlelAdmin, self).save_model(request, obj, form, change)
def get_view_on_site_url(self, obj=None):
"""LJX: 获取在站点查看的URL"""
if obj:
url = obj.get_full_url() # LJX: 使用文章的完整URL
return url
else:
from djangoblog.utils import get_current_site
site = get_current_site().domain
return site
class TagAdmin(admin.ModelAdmin):
"""LJX: 标签模型的后台管理配置"""
exclude = ('slug', 'last_mod_time', 'creation_time') # LJX: 排除自动生成的字段
class CategoryAdmin(admin.ModelAdmin):
"""LJX: 分类模型的后台管理配置"""
list_display = ('name', 'parent_category', 'index') # LJX: 列表显示字段
exclude = ('slug', 'last_mod_time', 'creation_time') # LJX: 排除自动生成的字段
class LinksAdmin(admin.ModelAdmin):
"""LJX: 友情链接模型的后台管理配置"""
exclude = ('last_mod_time', 'creation_time') # LJX: 排除时间字段
class SideBarAdmin(admin.ModelAdmin):
"""LJX: 侧边栏模型的后台管理配置"""
list_display = ('name', 'content', 'is_enable', 'sequence') # LJX: 列表显示字段
exclude = ('last_mod_time', 'creation_time') # LJX: 排除时间字段
class BlogSettingsAdmin(admin.ModelAdmin):
"""LJX: 博客设置模型的后台管理配置"""
pass

@ -1,10 +0,0 @@
"""
LJX: Blog应用配置模块
定义blog应用的配置信息包括应用名称等基础设置
"""
from django.apps import AppConfig
class BlogConfig(AppConfig):
"""LJX: Blog应用配置类"""
name = 'blog' # LJX: 应用名称

@ -1,49 +0,0 @@
"""
LJX: 模板上下文处理器模块
为所有模板提供全局的上下文变量包括SEO信息导航数据网站设置等
这些变量在所有模板中都可以直接使用
"""
import logging
from django.utils import timezone
from djangoblog.utils import cache, get_blog_setting
from .models import Category, Article
logger = logging.getLogger(__name__)
def seo_processor(requests):
"""LJX: SEO上下文处理器为模板提供SEO相关变量"""
key = 'seo_processor'
value = cache.get(key)
if value:
return value # LJX: 如果缓存中存在,直接返回
else:
logger.info('set processor cache.')
setting = get_blog_setting() # LJX: 获取博客设置
value = {
'SITE_NAME': setting.site_name, # LJX: 网站名称
'SHOW_GOOGLE_ADSENSE': setting.show_google_adsense, # LJX: 是否显示Google广告
'GOOGLE_ADSENSE_CODES': setting.google_adsense_codes, # LJX: 广告代码
'SITE_SEO_DESCRIPTION': setting.site_seo_description, # LJX: SEO描述
'SITE_DESCRIPTION': setting.site_description, # LJX: 网站描述
'SITE_KEYWORDS': setting.site_keywords, # LJX: 网站关键词
'SITE_BASE_URL': requests.scheme + '://' + requests.get_host() + '/', # LJX: 网站基础URL
'ARTICLE_SUB_LENGTH': setting.article_sub_length, # LJX: 文章摘要长度
'nav_category_list': Category.objects.all(), # LJX: 导航分类列表
'nav_pages': Article.objects.filter( # LJX: 导航页面
type='p',
status='p'),
'OPEN_SITE_COMMENT': setting.open_site_comment, # LJX: 是否开启评论
'BEIAN_CODE': setting.beian_code, # LJX: 备案号
'ANALYTICS_CODE': setting.analytics_code, # LJX: 统计代码
"BEIAN_CODE_GONGAN": setting.gongan_beiancode, # LJX: 公安备案号
"SHOW_GONGAN_CODE": setting.show_gongan_code, # LJX: 是否显示公安备案
"CURRENT_YEAR": timezone.now().year, # LJX: 当前年份
"GLOBAL_HEADER": setting.global_header, # LJX: 全局头部
"GLOBAL_FOOTER": setting.global_footer, # LJX: 全局尾部
"COMMENT_NEED_REVIEW": setting.comment_need_review, # LJX: 评论是否需要审核
}
cache.set(key, value, 60 * 60 * 10) # LJX: 缓存10小时
return value

@ -1,243 +0,0 @@
"""
LJX: Elasticsearch文档定义模块
定义Elasticsearch索引的文档结构和数据模型
用于博客文章的全文搜索和性能监控数据的存储
"""
import time
import elasticsearch.client
from django.conf import settings
from elasticsearch_dsl import Document, InnerDoc, Date, Integer, Long, Text, Object, GeoPoint, Keyword, Boolean
from elasticsearch_dsl.connections import connections
from blog.models import Article
# LJX: 检查是否启用Elasticsearch
ELASTICSEARCH_ENABLED = hasattr(settings, 'ELASTICSEARCH_DSL')
if ELASTICSEARCH_ENABLED:
# LJX: 创建Elasticsearch连接
connections.create_connection(
hosts=[settings.ELASTICSEARCH_DSL['default']['hosts']])
from elasticsearch import Elasticsearch
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
from elasticsearch.client import IngestClient
c = IngestClient(es)
try:
c.get_pipeline('geoip') # LJX: 检查geoip管道是否存在
except elasticsearch.exceptions.NotFoundError:
# LJX: 创建geoip处理管道用于IP地址地理位置解析
c.put_pipeline('geoip', body='''{
"description" : "Add geoip info",
"processors" : [
{
"geoip" : {
"field" : "ip"
}
}
]
}''')
class GeoIp(InnerDoc):
"""LJX: IP地理位置信息内嵌文档"""
continent_name = Keyword() # LJX: 大洲名称
country_iso_code = Keyword() # LJX: 国家ISO代码
country_name = Keyword() # LJX: 国家名称
location = GeoPoint() # LJX: 地理位置坐标
class UserAgentBrowser(InnerDoc):
"""LJX: 用户代理浏览器信息"""
Family = Keyword() # LJX: 浏览器家族
Version = Keyword() # LJX: 浏览器版本
class UserAgentOS(UserAgentBrowser):
"""LJX: 用户代理操作系统信息"""
pass
class UserAgentDevice(InnerDoc):
"""LJX: 用户代理设备信息"""
Family = Keyword() # LJX: 设备家族
Brand = Keyword() # LJX: 设备品牌
Model = Keyword() # LJX: 设备型号
class UserAgent(InnerDoc):
"""LJX: 完整的用户代理信息"""
browser = Object(UserAgentBrowser, required=False) # LJX: 浏览器信息
os = Object(UserAgentOS, required=False) # LJX: 操作系统信息
device = Object(UserAgentDevice, required=False) # LJX: 设备信息
string = Text() # LJX: 原始用户代理字符串
is_bot = Boolean() # LJX: 是否是爬虫
class ElapsedTimeDocument(Document):
"""LJX: 性能监控耗时文档,记录页面加载时间等性能数据"""
url = Keyword() # LJX: 请求URL
time_taken = Long() # LJX: 耗时(毫秒)
log_datetime = Date() # LJX: 日志时间
ip = Keyword() # LJX: IP地址
geoip = Object(GeoIp, required=False) # LJX: 地理位置信息
useragent = Object(UserAgent, required=False) # LJX: 用户代理信息
class Index:
"""LJX: 索引配置"""
name = 'performance' # LJX: 索引名称
settings = {
"number_of_shards": 1, # LJX: 分片数量
"number_of_replicas": 0 # LJX: 副本数量
}
class Meta:
doc_type = 'ElapsedTime' # LJX: 文档类型
class ElaspedTimeDocumentManager:
"""LJX: 性能监控文档管理器"""
@staticmethod
def build_index():
"""LJX: 构建性能监控索引"""
from elasticsearch import Elasticsearch
client = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
res = client.indices.exists(index="performance")
if not res:
ElapsedTimeDocument.init() # LJX: 初始化索引
@staticmethod
def delete_index():
"""LJX: 删除性能监控索引"""
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):
"""LJX: 创建性能监控记录"""
ElaspedTimeDocumentManager.build_index()
# LJX: 构建用户代理信息对象
ua = UserAgent()
ua.browser = UserAgentBrowser()
ua.browser.Family = useragent.browser.family
ua.browser.Version = useragent.browser.version_string
ua.os = UserAgentOS()
ua.os.Family = useragent.os.family
ua.os.Version = useragent.os.version_string
ua.device = UserAgentDevice()
ua.device.Family = useragent.device.family
ua.device.Brand = useragent.device.brand
ua.device.Model = useragent.device.model
ua.string = useragent.ua_string
ua.is_bot = useragent.is_bot
# LJX: 创建文档并保存使用geoip管道处理IP地址
doc = ElapsedTimeDocument(
meta={
'id': int(
round(
time.time() *
1000)) # LJX: 使用时间戳作为文档ID
},
url=url,
time_taken=time_taken,
log_datetime=log_datetime,
useragent=ua, ip=ip)
doc.save(pipeline="geoip")
class ArticleDocument(Document):
"""LJX: 博客文章搜索文档,用于全文搜索"""
body = Text(analyzer='ik_max_word', search_analyzer='ik_smart') # LJX: 文章内容使用IK分词器
title = Text(analyzer='ik_max_word', search_analyzer='ik_smart') # LJX: 文章标题
author = Object(properties={ # LJX: 作者信息
'nickname': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
'id': Integer()
})
category = Object(properties={ # LJX: 分类信息
'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
'id': Integer()
})
tags = Object(properties={ # LJX: 标签信息
'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
'id': Integer()
})
# LJX: 文章元数据字段
pub_time = Date() # LJX: 发布时间
status = Text() # LJX: 状态
comment_status = Text() # LJX: 评论状态
type = Text() # LJX: 类型
views = Integer() # LJX: 浏览量
article_order = Integer() # LJX: 文章排序
class Index:
"""LJX: 文章索引配置"""
name = 'blog' # LJX: 索引名称
settings = {
"number_of_shards": 1,
"number_of_replicas": 0
}
class Meta:
doc_type = 'Article' # LJX: 文档类型
class ArticleDocumentManager():
"""LJX: 文章文档管理器,处理文章搜索索引的创建和更新"""
def __init__(self):
self.create_index() # LJX: 初始化时创建索引
def create_index(self):
"""LJX: 创建文章索引"""
ArticleDocument.init()
def delete_index(self):
"""LJX: 删除文章索引"""
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):
"""LJX: 将文章对象转换为搜索文档对象"""
return [
ArticleDocument(
meta={
'id': article.id}, # LJX: 使用文章ID作为文档ID
body=article.body,
title=article.title,
author={
'nickname': article.author.username,
'id': article.author.id},
category={
'name': article.category.name,
'id': article.category.id},
tags=[
{
'name': t.name,
'id': t.id} for t in article.tags.all()], # LJX: 转换标签列表
pub_time=article.pub_time,
status=article.status,
comment_status=article.comment_status,
type=article.type,
views=article.views,
article_order=article.article_order) for article in articles]
def rebuild(self, articles=None):
"""LJX: 重建搜索索引"""
ArticleDocument.init()
articles = articles if articles else Article.objects.all() # LJX: 如果没有指定文章,则使用所有文章
docs = self.convert_to_doc(articles)
for doc in docs:
doc.save() # LJX: 保存所有文档到索引
def update_docs(self, docs):
"""LJX: 更新搜索文档"""
for doc in docs:
doc.save()

@ -1,25 +0,0 @@
"""
LJX: 表单定义模块
定义博客搜索相关的表单类和验证逻辑
"""
import logging
from django import forms
from haystack.forms import SearchForm
logger = logging.getLogger(__name__)
class BlogSearchForm(SearchForm):
"""LJX: 博客搜索表单继承自Haystack的SearchForm"""
querydata = forms.CharField(required=True) # LJX: 搜索查询字段,必须填写
def search(self):
"""LJX: 执行搜索操作"""
datas = super(BlogSearchForm, self).search() # LJX: 调用父类搜索方法
if not self.is_valid(): # LJX: 表单验证
return self.no_query_found() # LJX: 如果没有查询条件,返回空结果
if self.cleaned_data['querydata']:
logger.info(self.cleaned_data['querydata']) # LJX: 记录搜索关键词
return datas

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

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

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

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

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

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

@ -1,51 +0,0 @@
"""
LJX: 中间件模块
定义自定义中间件用于处理请求和响应的额外逻辑
包括性能监控用户访问统计等功能
"""
import logging
import time
from ipware import get_client_ip
from user_agents import parse
from blog.documents import ELASTICSEARCH_ENABLED, ElaspedTimeDocumentManager
logger = logging.getLogger(__name__)
class OnlineMiddleware(object):
"""LJX: 在线中间件,用于监控页面渲染时间和用户访问信息"""
def __init__(self, get_response=None):
self.get_response = get_response
super().__init__()
def __call__(self, request):
''' LJX: 页面渲染时间监控 '''
start_time = time.time() # LJX: 记录开始时间
response = self.get_response(request) # LJX: 获取响应
http_user_agent = request.META.get('HTTP_USER_AGENT', '') # LJX: 获取用户代理
ip, _ = get_client_ip(request) # LJX: 获取客户端IP
user_agent = parse(http_user_agent) # LJX: 解析用户代理信息
if not response.streaming: # LJX: 如果不是流式响应
try:
cast_time = time.time() - start_time # LJX: 计算渲染耗时
if ELASTICSEARCH_ENABLED:
time_taken = round((cast_time) * 1000, 2) # LJX: 转换为毫秒
url = request.path # LJX: 请求路径
from django.utils import timezone
# LJX: 创建性能监控记录
ElaspedTimeDocumentManager.create(
url=url,
time_taken=time_taken,
log_datetime=timezone.now(),
useragent=user_agent,
ip=ip)
# LJX: 在响应内容中替换加载时间占位符
response.content = response.content.replace(
b'<!!LOAD_TIMES!!>', str.encode(str(cast_time)[:5]))
except Exception as e:
logger.error("Error OnlineMiddleware: %s" % e) # LJX: 记录错误日志
return response

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

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

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

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

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

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

@ -1,397 +0,0 @@
"""
LJX: 数据模型定义模块
定义博客系统的核心数据模型包括文章分类标签友情链接等
使用Django的ORM进行数据库映射和操作
"""
import logging
import re
from abc import abstractmethod
from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import models
from django.urls import reverse
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from mdeditor.fields import MDTextField
from uuslug import slugify
from djangoblog.utils import cache_decorator, cache
from djangoblog.utils import get_current_site
logger = logging.getLogger(__name__)
class LinkShowType(models.TextChoices):
"""LJX: 链接显示类型选择枚举"""
I = ('i', _('index')) # LJX: 首页显示
L = ('l', _('list')) # LJX: 列表页显示
P = ('p', _('post')) # LJX: 文章页显示
A = ('a', _('all')) # LJX: 所有页面显示
S = ('s', _('slide')) # LJX: 幻灯片显示
class BaseModel(models.Model):
"""LJX: 基础模型类,提供公共字段和方法"""
id = models.AutoField(primary_key=True) # LJX: 自增主键
creation_time = models.DateTimeField(_('creation time'), default=now) # LJX: 创建时间
last_modify_time = models.DateTimeField(_('modify time'), default=now) # LJX: 最后修改时间
def save(self, *args, **kwargs):
"""LJX: 重写保存方法,添加自动处理逻辑"""
is_update_views = isinstance(
self,
Article) and 'update_fields' in kwargs and kwargs['update_fields'] == ['views'] # LJX: 检查是否是更新浏览量
if is_update_views:
Article.objects.filter(pk=self.pk).update(views=self.views) # LJX: 直接更新浏览量,避免递归
else:
if 'slug' in self.__dict__: # LJX: 如果有slug字段自动生成
slug = getattr(
self, 'title') if 'title' in self.__dict__ else getattr(
self, 'name') # LJX: 根据title或name生成slug
setattr(self, 'slug', slugify(slug)) # LJX: 使用uuslug生成友好的URL
super().save(*args, **kwargs) # LJX: 调用父类保存方法
def get_full_url(self):
"""LJX: 获取完整URL"""
site = get_current_site().domain # LJX: 获取当前站点域名
url = "https://{site}{path}".format(site=site,
path=self.get_absolute_url()) # LJX: 拼接完整URL
return url
class Meta:
abstract = True # LJX: 抽象基类,不会创建数据库表
@abstractmethod
def get_absolute_url(self):
"""LJX: 抽象方法子类必须实现获取绝对URL的方法"""
pass
class Article(BaseModel):
"""LJX: 文章模型,博客系统的核心数据模型"""
STATUS_CHOICES = ( # LJX: 文章状态选择
('d', _('Draft')), # LJX: 草稿
('p', _('Published')), # LJX: 已发布
)
COMMENT_STATUS = ( # LJX: 评论状态选择
('o', _('Open')), # LJX: 开启评论
('c', _('Close')), # LJX: 关闭评论
)
TYPE = ( # LJX: 文章类型选择
('a', _('Article')), # LJX: 普通文章
('p', _('Page')), # LJX: 页面
)
# LJX: 文章核心字段
title = models.CharField(_('title'), max_length=200, unique=True) # LJX: 标题,唯一
body = MDTextField(_('body')) # LJX: 内容使用Markdown编辑器
pub_time = models.DateTimeField( # LJX: 发布时间
_('publish time'), blank=False, null=False, default=now)
status = models.CharField( # LJX: 状态
_('status'),
max_length=1,
choices=STATUS_CHOICES,
default='p') # LJX: 默认已发布
comment_status = models.CharField( # LJX: 评论状态
_('comment status'),
max_length=1,
choices=COMMENT_STATUS,
default='o') # LJX: 默认开启评论
type = models.CharField(_('type'), max_length=1, choices=TYPE, default='a') # LJX: 类型,默认普通文章
views = models.PositiveIntegerField(_('views'), default=0) # LJX: 浏览量
author = models.ForeignKey( # LJX: 作者,外键关联用户模型
settings.AUTH_USER_MODEL,
verbose_name=_('author'),
blank=False,
null=False,
on_delete=models.CASCADE) # LJX: 级联删除
article_order = models.IntegerField( # LJX: 文章排序
_('order'), blank=False, null=False, default=0)
show_toc = models.BooleanField(_('show toc'), blank=False, null=False, default=False) # LJX: 是否显示目录
category = models.ForeignKey( # LJX: 分类,外键关联分类模型
'Category',
verbose_name=_('category'),
on_delete=models.CASCADE,
blank=False,
null=False)
tags = models.ManyToManyField('Tag', verbose_name=_('tag'), blank=True) # LJX: 标签,多对多关系
def body_to_string(self):
"""LJX: 将文章内容转换为字符串"""
return self.body
def __str__(self):
"""LJX: 字符串表示,返回文章标题"""
return self.title
class Meta:
ordering = ['-article_order', '-pub_time'] # LJX: 默认按排序和发布时间降序
verbose_name = _('article') # LJX: 单数名称
verbose_name_plural = verbose_name # LJX: 复数名称
get_latest_by = 'id' # LJX: 最新记录按ID
def get_absolute_url(self):
"""LJX: 获取文章绝对URL用于生成文章详情页链接"""
return reverse('blog:detailbyid', kwargs={
'article_id': self.id,
'year': self.creation_time.year, # LJX: 包含年月日用于SEO友好的URL
'month': self.creation_time.month,
'day': self.creation_time.day
})
@cache_decorator(60 * 60 * 10) # LJX: 缓存10小时
def get_category_tree(self):
"""LJX: 获取分类树,返回分类的层级结构"""
tree = self.category.get_category_tree() # LJX: 调用分类的获取分类树方法
names = list(map(lambda c: (c.name, c.get_absolute_url()), tree)) # LJX: 转换为名称和URL的元组列表
return names
def save(self, *args, **kwargs):
"""LJX: 重写保存方法"""
super().save(*args, **kwargs)
def viewed(self):
"""LJX: 增加文章浏览量"""
self.views += 1
self.save(update_fields=['views']) # LJX: 只更新views字段
def comment_list(self):
"""LJX: 获取文章评论列表,使用缓存提高性能"""
cache_key = 'article_comments_{id}'.format(id=self.id)
value = cache.get(cache_key)
if value:
logger.info('get article comments:{id}'.format(id=self.id))
return value # LJX: 如果缓存中存在,直接返回
else:
comments = self.comment_set.filter(is_enable=True).order_by('-id') # LJX: 获取已启用的评论按ID降序
cache.set(cache_key, comments, 60 * 100) # LJX: 缓存100分钟
logger.info('set article comments:{id}'.format(id=self.id))
return comments
def get_admin_url(self):
"""LJX: 获取文章在Admin后台的URL"""
info = (self._meta.app_label, self._meta.model_name)
return reverse('admin:%s_%s_change' % info, args=(self.pk,))
@cache_decorator(expiration=60 * 100) # LJX: 缓存100分钟
def next_article(self):
"""LJX: 获取下一篇文章"""
return Article.objects.filter(
id__gt=self.id, status='p').order_by('id').first() # LJX: ID大于当前文章的第一篇已发布文章
@cache_decorator(expiration=60 * 100) # LJX: 缓存100分钟
def prev_article(self):
"""LJX: 获取上一篇文章"""
return Article.objects.filter(id__lt=self.id, status='p').first() # LJX: ID小于当前文章的第一篇已发布文章
def get_first_image_url(self):
"""LJX: 从文章内容中提取第一张图片的URL"""
match = re.search(r'!\[.*?\]\((.+?)\)', self.body) # LJX: 使用正则匹配Markdown图片语法
if match:
return match.group(1) # LJX: 返回图片URL
return ""
class Category(BaseModel):
"""LJX: 文章分类模型,支持多级分类结构"""
name = models.CharField(_('category name'), max_length=30, unique=True) # LJX: 分类名称,唯一
parent_category = models.ForeignKey( # LJX: 父级分类,支持分类层级
'self',
verbose_name=_('parent category'),
blank=True,
null=True,
on_delete=models.CASCADE) # LJX: 自关联外键
slug = models.SlugField(default='no-slug', max_length=60, blank=True) # LJX: URL友好名称
index = models.IntegerField(default=0, verbose_name=_('index')) # LJX: 排序索引
class Meta:
ordering = ['-index'] # LJX: 按索引降序排列
verbose_name = _('category') # LJX: 单数名称
verbose_name_plural = verbose_name # LJX: 复数名称
def get_absolute_url(self):
"""LJX: 获取分类绝对URL"""
return reverse(
'blog:category_detail', kwargs={
'category_name': self.slug}) # LJX: 使用slug作为URL参数
def __str__(self):
"""LJX: 字符串表示,返回分类名称"""
return self.name
@cache_decorator(60 * 60 * 10) # LJX: 缓存10小时
def get_category_tree(self):
"""LJX: 递归获得分类目录的父级,返回从当前分类到根分类的路径"""
categorys = [] # LJX: 存储分类路径
def parse(category):
"""LJX: 递归解析分类父级"""
categorys.append(category)
if category.parent_category: # LJX: 如果存在父分类,继续递归
parse(category.parent_category)
parse(self) # LJX: 从当前分类开始解析
return categorys
@cache_decorator(60 * 60 * 10) # LJX: 缓存10小时
def get_sub_categorys(self):
"""LJX: 获得当前分类目录所有子集,包括所有下级分类"""
categorys = [] # LJX: 存储所有子分类
all_categorys = Category.objects.all() # LJX: 获取所有分类
def parse(category):
"""LJX: 递归解析子分类"""
if category not in categorys:
categorys.append(category) # LJX: 添加当前分类
childs = all_categorys.filter(parent_category=category) # LJX: 查找直接子分类
for child in childs:
if category not in categorys:
categorys.append(child) # LJX: 添加子分类
parse(child) # LJX: 递归解析子分类的子分类
parse(self) # LJX: 从当前分类开始解析
return categorys
class Tag(BaseModel):
"""LJX: 文章标签模型,用于文章分类和检索"""
name = models.CharField(_('tag name'), max_length=30, unique=True) # LJX: 标签名称,唯一
slug = models.SlugField(default='no-slug', max_length=60, blank=True) # LJX: URL友好名称
def __str__(self):
"""LJX: 字符串表示,返回标签名称"""
return self.name
def get_absolute_url(self):
"""LJX: 获取标签绝对URL"""
return reverse('blog:tag_detail', kwargs={'tag_name': self.slug}) # LJX: 使用slug作为URL参数
@cache_decorator(60 * 60 * 10) # LJX: 缓存10小时
def get_article_count(self):
"""LJX: 获取使用该标签的文章数量"""
return Article.objects.filter(tags__name=self.name).distinct().count() # LJX: 去重计数
class Meta:
ordering = ['name'] # LJX: 按名称排序
verbose_name = _('tag') # LJX: 单数名称
verbose_name_plural = verbose_name # LJX: 复数名称
class Links(models.Model):
"""LJX: 友情链接模型,管理网站的外部链接"""
name = models.CharField(_('link name'), max_length=30, unique=True) # LJX: 链接名称,唯一
link = models.URLField(_('link')) # LJX: 链接地址
sequence = models.IntegerField(_('order'), unique=True) # LJX: 显示顺序,唯一
is_enable = models.BooleanField( # LJX: 是否启用显示
_('is show'), default=True, blank=False, null=False)
show_type = models.CharField( # LJX: 显示类型
_('show type'),
max_length=1,
choices=LinkShowType.choices,
default=LinkShowType.I) # LJX: 默认在首页显示
creation_time = models.DateTimeField(_('creation time'), default=now) # LJX: 创建时间
last_mod_time = models.DateTimeField(_('modify time'), default=now) # LJX: 最后修改时间
class Meta:
ordering = ['sequence'] # LJX: 按顺序排序
verbose_name = _('link') # LJX: 单数名称
verbose_name_plural = verbose_name # LJX: 复数名称
def __str__(self):
"""LJX: 字符串表示,返回链接名称"""
return self.name
class SideBar(models.Model):
"""LJX: 侧边栏模型可以展示一些HTML内容"""
name = models.CharField(_('title'), max_length=100) # LJX: 侧边栏标题
content = models.TextField(_('content')) # LJX: 侧边栏内容支持HTML
sequence = models.IntegerField(_('order'), unique=True) # LJX: 显示顺序,唯一
is_enable = models.BooleanField(_('is enable'), default=True) # LJX: 是否启用
creation_time = models.DateTimeField(_('creation time'), default=now) # LJX: 创建时间
last_mod_time = models.DateTimeField(_('modify time'), default=now) # LJX: 最后修改时间
class Meta:
ordering = ['sequence'] # LJX: 按顺序排序
verbose_name = _('sidebar') # LJX: 单数名称
verbose_name_plural = verbose_name # LJX: 复数名称
def __str__(self):
"""LJX: 字符串表示,返回侧边栏名称"""
return self.name
class BlogSettings(models.Model):
"""LJX: 博客设置模型,存储博客的全局配置信息"""
site_name = models.CharField( # LJX: 网站名称
_('site name'),
max_length=200,
null=False,
blank=False,
default='')
site_description = models.TextField( # LJX: 网站描述
_('site description'),
max_length=1000,
null=False,
blank=False,
default='')
site_seo_description = models.TextField( # LJX: 网站SEO描述
_('site seo description'), max_length=1000, null=False, blank=False, default='')
site_keywords = models.TextField( # LJX: 网站关键词
_('site keywords'),
max_length=1000,
null=False,
blank=False,
default='')
article_sub_length = models.IntegerField(_('article sub length'), default=300) # LJX: 文章摘要长度
sidebar_article_count = models.IntegerField(_('sidebar article count'), default=10) # LJX: 侧边栏文章数量
sidebar_comment_count = models.IntegerField(_('sidebar comment count'), default=5) # LJX: 侧边栏评论数量
article_comment_count = models.IntegerField(_('article comment count'), default=5) # LJX: 文章评论显示数量
show_google_adsense = models.BooleanField(_('show adsense'), default=False) # LJX: 是否显示Google广告
google_adsense_codes = models.TextField( # LJX: Google广告代码
_('adsense code'), max_length=2000, null=True, blank=True, default='')
open_site_comment = models.BooleanField(_('open site comment'), default=True) # LJX: 是否开启全站评论
global_header = models.TextField("公共头部", null=True, blank=True, default='') # LJX: 全局头部HTML
global_footer = models.TextField("公共尾部", null=True, blank=True, default='') # LJX: 全局尾部HTML
beian_code = models.CharField( # LJX: 备案号
'备案号',
max_length=2000,
null=True,
blank=True,
default='')
analytics_code = models.TextField( # LJX: 网站统计代码
"网站统计代码",
max_length=1000,
null=False,
blank=False,
default='')
show_gongan_code = models.BooleanField( # LJX: 是否显示公安备案号
'是否显示公安备案号', default=False, null=False)
gongan_beiancode = models.TextField( # LJX: 公安备案号
'公安备案号',
max_length=2000,
null=True,
blank=True,
default='')
comment_need_review = models.BooleanField( # LJX: 评论是否需要审核
'评论是否需要审核', default=False, null=False)
class Meta:
verbose_name = _('Website configuration') # LJX: 单数名称
verbose_name_plural = verbose_name # LJX: 复数名称
def __str__(self):
"""LJX: 字符串表示,返回网站名称"""
return self.site_name
def clean(self):
"""LJX: 数据清洗验证,确保只能有一个配置实例"""
if BlogSettings.objects.exclude(id=self.id).count():
raise ValidationError(_('There can only be one configuration')) # LJX: 只能有一个配置
def save(self, *args, **kwargs):
"""LJX: 重写保存方法,保存后清除缓存"""
super().save(*args, **kwargs)
from djangoblog.utils import cache
cache.clear() # LJX: 清除缓存,使配置立即生效

@ -1,20 +0,0 @@
"""
LJX: Haystack搜索索引配置模块
定义Django Haystack的搜索索引配置用于全文搜索功能
"""
from haystack import indexes
from blog.models import Article
class ArticleIndex(indexes.SearchIndex, indexes.Indexable):
"""LJX: 文章搜索索引类,定义文章的搜索字段和索引行为"""
text = indexes.CharField(document=True, use_template=True) # LJX: 主搜索字段,使用模板定义
def get_model(self):
"""LJX: 返回要索引的模型类"""
return Article
def index_queryset(self, using=None):
"""LJX: 返回要索引的查询集,只索引已发布的文章"""
return self.get_model().objects.filter(status='p') # LJX: 只索引已发布状态的文章

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

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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

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

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 221 B

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

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

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

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

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

File diff suppressed because one or more lines are too long

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

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

Loading…
Cancel
Save