diff --git a/doc/第7周代码注释/accounts/__init__.py b/doc/第7周代码注释/accounts/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/doc/第7周代码注释/accounts/admin.py b/doc/第7周代码注释/accounts/admin.py
new file mode 100644
index 00000000..84567a67
--- /dev/null
+++ b/doc/第7周代码注释/accounts/admin.py
@@ -0,0 +1,87 @@
+# 导入Django表单基类
+from django import forms
+# 导入Django内置的用户管理员类,用于扩展自定义用户的admin配置
+from django.contrib.auth.admin import UserAdmin
+# 导入Django内置的用户修改表单,用于扩展自定义用户的修改表单
+from django.contrib.auth.forms import UserChangeForm
+# 导入Django内置的用户名字段类,用于自定义用户名字段验证
+from django.contrib.auth.forms import UsernameField
+# 导入翻译工具,用于实现字段名称的国际化
+from django.utils.translation import gettext_lazy as _
+
+# 注册模型时需要导入自定义的用户模型
+from .models import BlogUser
+
+
+class BlogUserCreationForm(forms.ModelForm):
+ """自定义用户创建表单,用于在admin站点添加新用户时使用"""
+ # 密码字段,使用密码输入框(输入内容隐藏),标签支持国际化
+ password1 = forms.CharField(label=_('password'), widget=forms.PasswordInput)
+ # 确认密码字段,用于验证两次输入的密码是否一致
+ password2 = forms.CharField(label=_('Enter password again'), widget=forms.PasswordInput)
+
+ class Meta:
+ # 关联的模型为自定义的BlogUser
+ model = BlogUser
+ # 创建用户时需要填写的字段(这里仅指定email,其他字段可默认或后续补充)
+ fields = ('email',)
+
+ def clean_password2(self):
+ """验证两次输入的密码是否一致"""
+ # 获取清洗后的密码1和密码2
+ 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方法,先不提交到数据库(commit=False)
+ user = super().save(commit=False)
+ # 对密码进行加密处理(Django内置的密码哈希方法)
+ user.set_password(self.cleaned_data["password1"])
+ # 如果需要提交到数据库
+ if commit:
+ # 设置用户的创建来源为'adminsite'(表示通过admin站点创建)
+ user.source = 'adminsite'
+ user.save()
+ return user
+
+
+class BlogUserChangeForm(UserChangeForm):
+ """自定义用户修改表单,用于在admin站点编辑用户信息时使用"""
+ class Meta:
+ # 关联的模型为BlogUser
+ model = BlogUser
+ # 显示所有字段(可根据需要指定具体字段)
+ fields = '__all__'
+ # 指定用户名字段的处理类为UsernameField(提供内置验证)
+ field_classes = {'username': UsernameField}
+
+ def __init__(self, *args, **kwargs):
+ """初始化方法,调用父类的初始化逻辑"""
+ super().__init__(*args, **kwargs)
+
+
+class BlogUserAdmin(UserAdmin):
+ """自定义用户管理员类,用于在admin站点配置BlogUser的展示和操作"""
+ # 指定修改用户时使用的表单
+ 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',)
diff --git a/doc/第7周代码注释/accounts/apps.py b/doc/第7周代码注释/accounts/apps.py
new file mode 100644
index 00000000..76ad64e3
--- /dev/null
+++ b/doc/第7周代码注释/accounts/apps.py
@@ -0,0 +1,12 @@
+# 从Django的apps模块导入AppConfig类,用于定义应用的配置信息
+from django.apps import AppConfig
+
+
+class AccountsConfig(AppConfig):
+ """
+ accounts应用的配置类,用于设置应用的基本信息
+
+ 继承自Django的AppConfig,通过此类可以配置应用的名称、默认自动生成的主键类型等元数据
+ """
+ # 定义应用的名称,Django通过此名称识别该应用,与项目settings.py中INSTALLED_APPS里的配置对应
+ name = 'accounts'
\ No newline at end of file
diff --git a/doc/第7周代码注释/accounts/forms.py b/doc/第7周代码注释/accounts/forms.py
new file mode 100644
index 00000000..f334d8a6
--- /dev/null
+++ b/doc/第7周代码注释/accounts/forms.py
@@ -0,0 +1,153 @@
+# 导入Django表单基类
+from django import forms
+# 导入Django用户模型工具及密码验证功能
+from django.contrib.auth import get_user_model, password_validation
+# 导入Django内置的认证表单(登录、用户创建)
+from django.contrib.auth.forms import AuthenticationForm, UserCreationForm
+# 导入Django验证错误类,用于自定义表单验证
+from django.core.exceptions import ValidationError
+# 导入Django表单控件,用于自定义输入框样式
+from django.forms import widgets
+# 导入翻译工具,实现字段名称/提示的国际化
+from django.utils.translation import gettext_lazy as _
+# 导入自定义工具类(可能用于验证码验证等)
+from . import utils
+# 导入自定义用户模型
+from .models import BlogUser
+
+
+class LoginForm(AuthenticationForm):
+ """自定义登录表单,继承自Django内置的AuthenticationForm"""
+ 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类
+ self.fields['password'].widget = widgets.PasswordInput(
+ attrs={'placeholder': "password", "class": "form-control"})
+
+
+class RegisterForm(UserCreationForm):
+ """自定义注册表单,继承自Django内置的UserCreationForm"""
+ def __init__(self, *args, **kwargs):
+ """初始化方法,重写父类初始化逻辑以自定义表单控件样式"""
+ super(RegisterForm, self).__init__(*args, **kwargs)
+
+ # 自定义用户名字段控件:文本输入框,设置占位符和CSS类
+ 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"})
+ # 自定义密码1字段控件:密码输入框,设置占位符和CSS类
+ self.fields['password1'].widget = widgets.PasswordInput(
+ attrs={'placeholder': "password", "class": "form-control"})
+ # 自定义密码2字段控件:密码输入框(确认密码),设置占位符和CSS类
+ 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:
+ # 关联的用户模型(通过get_user_model获取项目配置的用户模型)
+ model = get_user_model()
+ # 注册表单需填写的字段:用户名和邮箱
+ fields = ("username", "email")
+
+
+class ForgetPasswordForm(forms.Form):
+ """忘记密码表单,用于用户重置密码(包含密码重置、邮箱验证、验证码验证)"""
+ # 新密码字段:标签国际化,使用密码输入框,设置CSS类和占位符
+ 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"))
+ # 使用Django内置密码验证器验证密码强度(如长度、复杂度等)
+ password_validation.validate_password(password2)
+ return password2
+
+ def clean_email(self):
+ """验证邮箱是否已注册(存在于用户表中)"""
+ user_email = self.cleaned_data.get("email")
+ # 如果该邮箱不存在于BlogUser表中,抛出验证错误
+ if not BlogUser.objects.filter(
+ email=user_email
+ ).exists():
+ # 注意:此处提示可能暴露邮箱是否注册,可根据需求修改
+ raise ValidationError(_("email does not exist"))
+ return user_email
+
+ def clean_code(self):
+ """验证验证码是否有效(调用自定义工具类的verify方法)"""
+ code = self.cleaned_data.get("code")
+ # 调用utils.verify验证邮箱和验证码是否匹配,返回错误信息(若有)
+ 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'),
+ )
\ No newline at end of file
diff --git a/doc/第7周代码注释/accounts/migrations/0001_initial.py b/doc/第7周代码注释/accounts/migrations/0001_initial.py
new file mode 100644
index 00000000..27fe10a1
--- /dev/null
+++ b/doc/第7周代码注释/accounts/migrations/0001_initial.py
@@ -0,0 +1,77 @@
+# Generated by Django 4.1.7 on 2023-03-02 07:14
+# 以上注释为Django自动生成,标识该迁移文件由Django 4.1.7版本在2023-03-02 07:14生成
+
+# 导入Django内置的用户模型相关模块
+import django.contrib.auth.models
+# 导入Django内置的用户验证器
+import django.contrib.auth.validators
+# 从django.db导入迁移和模型相关类
+from django.db import migrations, models
+# 导入Django的时区工具
+import django.utils.timezone
+
+
+# 定义迁移类,继承自migrations.Migration
+class Migration(migrations.Migration):
+
+ # 标识这是初始迁移(首次创建模型的迁移)
+ initial = True
+
+ # 依赖的其他迁移文件,这里依赖auth应用的0012_alter_user_first_name_max_length迁移
+ dependencies = [
+ ('auth', '0012_alter_user_first_name_max_length'),
+ ]
+
+ # 迁移操作列表,包含要执行的数据库操作
+ operations = [
+ # 创建BlogUser模型的迁移操作
+ migrations.CreateModel(
+ name='BlogUser', # 模型名称
+ fields=[ # 模型字段定义列表
+ # 自增主键字段,BigAutoField适用于大数据量场景
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ # 密码字段,最大长度128,显示名称为'password'
+ ('password', models.CharField(max_length=128, verbose_name='password')),
+ # 最后登录时间字段,可为空且允许空白,显示名称为'last login'
+ ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
+ # 是否为超级用户字段,默认False,包含帮助文本和显示名称
+ ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
+ # 用户名字段,包含错误信息、帮助文本、最大长度150、唯一约束、验证器和显示名称
+ ('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')),
+ # 名字段,允许空白,最大长度150,显示名称为'first name'
+ ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
+ # 姓字段,允许空白,最大长度150,显示名称为'last name'
+ ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
+ # 邮箱字段,允许空白,最大长度254,显示名称为'email address'
+ ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
+ # 是否为管理员(可登录admin站点),默认False,包含帮助文本和显示名称
+ ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
+ # 是否激活字段,默认True,包含帮助文本(建议通过此选项禁用账户而非删除)和显示名称
+ ('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'
+ ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
+ # 自定义昵称字段,允许空白,最大长度100,显示名称为'昵称'
+ ('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='修改时间')),
+ # 自定义创建来源字段,允许空白,最大长度100,显示名称为'创建来源'
+ ('source', models.CharField(blank=True, max_length=100, verbose_name='创建来源')),
+ # 与auth.Group的多对多关系,用于用户组权限管理,包含帮助文本、关联名称和显示名称
+ ('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')),
+ # 与auth.Permission的多对多关系,用于用户单独权限管理,包含帮助文本、关联名称和显示名称
+ ('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'], # 排序方式:按id降序
+ 'get_latest_by': 'id', # 获取最新记录的依据字段:id
+ },
+ managers=[ # 模型的管理器配置
+ # 使用Django内置的UserManager作为模型的管理器
+ ('objects', django.contrib.auth.models.UserManager()),
+ ],
+ ),
+ ]
\ No newline at end of file
diff --git a/doc/第7周代码注释/accounts/migrations/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.py b/doc/第7周代码注释/accounts/migrations/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.py
new file mode 100644
index 00000000..b9aceb98
--- /dev/null
+++ b/doc/第7周代码注释/accounts/migrations/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.py
@@ -0,0 +1,68 @@
+# Generated by Django 4.2.5 on 2023-09-06 13:13
+# 以上注释为Django自动生成,标识该迁移文件由Django 4.2.5版本在2023-09-06 13:13生成
+
+# 从django.db导入迁移和模型相关类,用于定义数据库迁移操作
+from django.db import migrations, models
+# 导入Django的时区工具,用于处理时间字段的默认值
+import django.utils.timezone
+
+
+# 定义迁移类,继承自migrations.Migration,用于描述数据库结构的变更
+class Migration(migrations.Migration):
+
+ # 依赖的其他迁移文件:依赖于accounts应用的0001_initial迁移
+ # 表示当前迁移需要在0001_initial迁移执行之后才能运行
+ dependencies = [
+ ('accounts', '0001_initial'),
+ ]
+
+ # 迁移操作列表,包含一系列对数据库模型的修改操作
+ operations = [
+ # 修改BlogUser模型的元数据配置
+ migrations.AlterModelOptions(
+ name='bloguser', # 目标模型名称
+ # 新的元数据选项:
+ # get_latest_by:指定通过id字段获取最新记录
+ # ordering:按id降序排序
+ # verbose_name/verbose_name_plural:模型的显示名称(单数和复数)改为'user'
+ options={'get_latest_by': 'id', 'ordering': ['-id'], 'verbose_name': 'user', 'verbose_name_plural': 'user'},
+ ),
+ # 从BlogUser模型中移除'created_time'字段
+ migrations.RemoveField(
+ model_name='bloguser', # 目标模型名称
+ name='created_time', # 要移除的字段名
+ ),
+ # 从BlogUser模型中移除'last_mod_time'字段
+ migrations.RemoveField(
+ model_name='bloguser', # 目标模型名称
+ name='last_mod_time', # 要移除的字段名
+ ),
+ # 向BlogUser模型添加'creation_time'字段
+ migrations.AddField(
+ model_name='bloguser', # 目标模型名称
+ name='creation_time', # 新增字段名
+ # 字段类型为DateTimeField,默认值为当前时区时间,显示名称为'creation time'
+ field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
+ ),
+ # 向BlogUser模型添加'last_modify_time'字段
+ migrations.AddField(
+ model_name='bloguser', # 目标模型名称
+ name='last_modify_time', # 新增字段名
+ # 字段类型为DateTimeField,默认值为当前时区时间,显示名称为'last modify time'
+ field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'),
+ ),
+ # 修改BlogUser模型的'nickname'字段属性
+ migrations.AlterField(
+ model_name='bloguser', # 目标模型名称
+ name='nickname', # 要修改的字段名
+ # 字段仍为CharField,允许空白,最大长度100,显示名称改为'nick name'
+ field=models.CharField(blank=True, max_length=100, verbose_name='nick name'),
+ ),
+ # 修改BlogUser模型的'source'字段属性
+ migrations.AlterField(
+ model_name='bloguser', # 目标模型名称
+ name='source', # 要修改的字段名
+ # 字段仍为CharField,允许空白,最大长度100,显示名称改为'create source'
+ field=models.CharField(blank=True, max_length=100, verbose_name='create source'),
+ ),
+ ]
\ No newline at end of file
diff --git a/doc/第7周代码注释/accounts/migrations/__init__.py b/doc/第7周代码注释/accounts/migrations/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/doc/第7周代码注释/accounts/models.py b/doc/第7周代码注释/accounts/models.py
new file mode 100644
index 00000000..6bf8a3d0
--- /dev/null
+++ b/doc/第7周代码注释/accounts/models.py
@@ -0,0 +1,64 @@
+# 导入Django内置的抽象用户模型,用于扩展自定义用户功能(包含基础用户名、密码等字段)
+from django.contrib.auth.models import AbstractUser
+# 导入Django模型相关类,用于定义数据库表结构
+from django.db import models
+# 导入reverse函数,用于通过URL名称生成对应的URL路径
+from django.urls import reverse
+# 导入now函数,用于获取当前时区的时间(作为字段默认值)
+from django.utils.timezone import now
+# 导入翻译工具,用于实现模型字段名称的国际化
+from django.utils.translation import gettext_lazy as _
+# 导入自定义工具函数get_current_site,用于获取当前站点的域名信息
+from djangoblog.utils import get_current_site
+
+
+class BlogUser(AbstractUser):
+ """
+ 自定义用户模型,继承自Django的AbstractUser
+ 扩展了内置用户模型,增加了昵称、创建时间、修改时间、创建来源等自定义字段
+ """
+ # 昵称字段:支持国际化标签,最大长度100,允许空白(不强制填写)
+ nickname = models.CharField(_('nick name'), max_length=100, blank=True)
+ # 创建时间字段:支持国际化标签,默认值为当前时间(调用now函数)
+ creation_time = models.DateTimeField(_('creation time'), default=now)
+ # 最后修改时间字段:支持国际化标签,默认值为当前时间
+ last_modify_time = models.DateTimeField(_('last modify time'), default=now)
+ # 创建来源字段:记录用户创建的渠道(如adminsite、frontend等),支持国际化标签,允许空白
+ source = models.CharField(_('create source'), max_length=100, blank=True)
+
+ def get_absolute_url(self):
+ """
+ 定义模型实例的绝对URL(标准Django方法)
+ 通过URL名称'blog:author_detail'生成用户详情页的URL,参数为用户名
+ """
+ return reverse(
+ 'blog:author_detail', kwargs={
+ 'author_name': self.username}) # kwargs传递URL所需的用户名参数
+
+ def __str__(self):
+ """
+ 定义模型实例的字符串表示
+ 当打印或引用用户实例时,返回用户的邮箱地址(便于识别)
+ """
+ return self.email
+
+ def get_full_url(self):
+ """
+ 生成用户详情页的完整URL(包含站点域名)
+ 结合当前站点域名和get_absolute_url生成的相对路径,组成完整链接
+ """
+ # 获取当前站点的域名(如www.example.com)
+ site = get_current_site().domain
+ # 拼接域名和相对路径,形成完整URL(使用HTTPS协议)
+ url = "https://{site}{path}".format(site=site,
+ path=self.get_absolute_url())
+ return url
+
+ class Meta:
+ """
+ 模型的元数据配置,用于定义模型的显示和行为规则
+ """
+ ordering = ['-id'] # 数据查询时的默认排序:按ID降序(最新创建的用户在前)
+ verbose_name = _('user') # 模型的单数显示名称(支持国际化)
+ verbose_name_plural = verbose_name # 模型的复数显示名称(与单数一致,避免英文复数变形问题)
+ get_latest_by = 'id' # 获取"最新记录"时的依据字段:按ID判断(ID最大的为最新)
\ No newline at end of file
diff --git a/doc/第7周代码注释/accounts/templatetags/__init__.py b/doc/第7周代码注释/accounts/templatetags/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/doc/第7周代码注释/accounts/tests.py b/doc/第7周代码注释/accounts/tests.py
new file mode 100644
index 00000000..ea663dfd
--- /dev/null
+++ b/doc/第7周代码注释/accounts/tests.py
@@ -0,0 +1,276 @@
+# 导入Django测试所需的核心类:Client模拟HTTP请求、RequestFactory创建请求对象、TestCase测试基类
+from django.test import Client, RequestFactory, TestCase
+# 导入reverse通过URL名称生成路径,用于测试中定位接口
+from django.urls import reverse
+# 导入timezone处理时间相关字段,用于创建测试数据
+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
+
+
+# 定义账户相关测试类,继承Django的TestCase(提供测试框架支持)
+class AccountTest(TestCase):
+ def setUp(self):
+ """
+ 测试初始化方法,在每个测试方法执行前自动调用
+ 用于创建共用的测试对象,避免代码重复
+ """
+ # 创建测试客户端,用于模拟用户发送HTTP请求
+ self.client = Client()
+ # 创建请求工厂,用于生成原始请求对象(按需使用)
+ self.factory = RequestFactory()
+ # 创建普通测试用户,用户名test、邮箱admin@admin.com、密码12345678
+ self.blog_user = BlogUser.objects.create_user(
+ username="test",
+ email="admin@admin.com",
+ password="12345678"
+ )
+ # 定义测试用的新密码,后续忘记密码测试中使用
+ self.new_test = "xxx123--="
+
+ def test_validate_account(self):
+ """测试账户基础功能:超级用户创建、登录验证、admin访问、内容创建与管理"""
+ # 获取当前站点域名(测试中未实际使用,保持原逻辑)
+ site = get_current_site().domain
+ # 创建超级用户(拥有admin管理权限)
+ 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')
+ # 断言登录成功(返回True)
+ self.assertEqual(loginresult, True)
+ # 模拟访问admin后台页面
+ response = self.client.get('/admin/')
+ # 断言admin页面访问成功(状态码200)
+ 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' # 假设'a'代表普通文章类型
+ article.status = 'p' # 假设'p'代表已发布状态
+ article.save()
+
+ # 模拟访问文章的admin管理页面(通过文章模型的get_admin_url方法获取路径)
+ response = self.client.get(article.get_admin_url())
+ # 断言文章管理页面访问成功(状态码200)
+ self.assertEqual(response.status_code, 200)
+
+ def test_validate_register(self):
+ """测试用户注册流程:注册请求、注册后用户存在性、邮箱验证、登录与权限升级、内容管理"""
+ # 注册前断言:邮箱为user123@user.com的用户不存在(初始状态)
+ self.assertEquals(
+ 0, len(
+ BlogUser.objects.filter(
+ email='user123@user.com')))
+ # 模拟发送注册请求:向account:register接口提交用户名、邮箱、两次一致的密码
+ 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',
+ })
+ # 注册后断言:邮箱为user123@user.com的用户存在(注册成功)
+ 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))) # 双重SHA256加密签名
+ path = reverse('accounts:result') # 验证结果页的URL路径
+ url = '{path}?type=validation&id={id}&sign={sign}'.format(
+ path=path, id=user.id, sign=sign) # 拼接完整验证URL
+ # 模拟访问邮箱验证链接
+ response = self.client.get(url)
+ # 断言验证页面访问成功(状态码200)
+ self.assertEqual(response.status_code, 200)
+
+ # 模拟刚注册的用户登录
+ self.client.login(username='user1233', password='password123!q@wE#R$T')
+ # 升级用户权限为超级用户(便于测试admin功能)
+ user = BlogUser.objects.filter(email='user123@user.com')[0]
+ user.is_superuser = True # 设为超级用户
+ user.is_staff = True # 允许登录admin
+ 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()
+
+ # 模拟访问文章的admin管理页面
+ response = self.client.get(article.get_admin_url())
+ self.assertEqual(response.status_code, 200)
+
+ # 模拟用户登出
+ response = self.client.get(reverse('account:logout'))
+ # 断言登出请求响应正常(允许301/302重定向或200成功)
+ self.assertIn(response.status_code, [301, 302, 200])
+
+ # 登出后尝试访问文章admin页面(应被拒绝或重定向)
+ 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])
+
+ # 错误登录后尝试访问文章admin页面(应被拒绝或重定向)
+ 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)
+
+ # 验证1:使用正确的邮箱和验证码,断言无错误返回(验证成功)
+ err = utils.verify("admin@admin.com", code)
+ self.assertEqual(err, None)
+
+ # 验证2:使用错误的邮箱(与存储的验证码不匹配),断言返回错误信息(字符串类型)
+ err = utils.verify("admin@123.com", code)
+ self.assertEqual(type(err), str)
+
+ def test_forget_password_email_code_success(self):
+ """测试获取忘记密码验证码的成功场景:提交正确邮箱,返回成功响应"""
+ # 模拟向account:forget_password_code接口提交正确邮箱
+ resp = self.client.post(
+ path=reverse("account:forget_password_code"),
+ data=dict(email="admin@admin.com")
+ )
+ # 断言请求成功(状态码200)且返回内容为"ok"(表示验证码发送成功)
+ self.assertEqual(resp.status_code, 200)
+ self.assertEqual(resp.content.decode("utf-8"), "ok")
+
+ def test_forget_password_email_code_fail(self):
+ """测试获取忘记密码验证码的失败场景:无邮箱、邮箱格式错误"""
+ # 失败场景1:不提交邮箱(空数据)
+ resp = self.client.post(
+ path=reverse("account:forget_password_code"),
+ data=dict()
+ )
+ # 断言返回"错误的邮箱"(参数缺失)
+ self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱")
+
+ # 失败场景2:提交格式错误的邮箱(admin@com不符合标准格式)
+ 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
+ )
+ # 断言重置成功(重定向到结果页,状态码302)
+ self.assertEqual(resp.status_code, 302)
+
+ # 验证密码是否真的修改成功:查询用户并校验新密码
+ blog_user = BlogUser.objects.filter(
+ email=self.blog_user.email,
+ ).first() # 获取用户实例
+ self.assertNotEqual(blog_user, None) # 断言用户存在
+ # 使用check_password方法验证新密码是否匹配(Django内置密码校验,自动处理哈希)
+ 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
+ )
+ # 断言请求响应正常(页面返回错误提示,状态码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", # 错误的验证码
+ )
+ # 模拟发送密码重置请求
+ resp = self.client.post(
+ path=reverse("account:forget_password"),
+ data=data
+ )
+ # 断言请求响应正常(页面返回验证码错误提示,状态码200)
+ self.assertEqual(resp.status_code, 200)
diff --git a/doc/第7周代码注释/accounts/urls.py b/doc/第7周代码注释/accounts/urls.py
new file mode 100644
index 00000000..cb4f795b
--- /dev/null
+++ b/doc/第7周代码注释/accounts/urls.py
@@ -0,0 +1,45 @@
+# 导入Django的URL路径定义工具:path用于精确路径匹配,re_path支持正则表达式匹配
+from django.urls import path
+from django.urls import re_path
+
+# 导入当前应用(accounts)的视图模块,包含登录、注册等业务逻辑处理
+from . import views
+# 导入当前应用的自定义登录表单,用于登录页面的表单渲染和验证
+from .forms import LoginForm
+
+# 定义应用的命名空间为"accounts",避免不同应用间URL名称冲突
+app_name = "accounts"
+
+# URL路由配置列表,映射URL路径到对应的视图
+urlpatterns = [
+ # 1. 登录页面URL
+ re_path(r'^login/$', # 正则匹配路径:以"login/"开头并结束(即精确匹配"/login/")
+ views.LoginView.as_view(success_url='/'), # 关联LoginView视图,登录成功后重定向到网站根路径("/")
+ name='login', # URL的命名,用于模板或视图中通过reverse('accounts:login')生成路径
+ kwargs={'authentication_form': LoginForm}), # 传递参数:指定登录使用自定义的LoginForm表单
+
+ # 2. 注册页面URL
+ re_path(r'^register/$', # 正则匹配路径:精确匹配"/register/"
+ views.RegisterView.as_view(success_url="/"), # 关联RegisterView视图,注册成功后重定向到网站根路径
+ name='register'), # URL命名,用于反向生成注册页面路径
+
+ # 3. 登出功能URL
+ re_path(r'^logout/$', # 正则匹配路径:精确匹配"/logout/"
+ views.LogoutView.as_view(), # 关联LogoutView视图,处理登出逻辑(默认登出后重定向到登录页)
+ name='logout'), # URL命名,用于反向生成登出路径
+
+ # 4. 账户操作结果页URL(如登录/注册/密码重置后的结果提示)
+ path(r'account/result.html', # 精确路径匹配:固定路径"/account/result.html"
+ views.account_result, # 关联普通函数视图account_result,处理结果页渲染
+ name='result'), # URL命名,用于反向生成结果页路径
+
+ # 5. 忘记密码页面URL(密码重置表单页)
+ re_path(r'^forget_password/$', # 正则匹配路径:精确匹配"/forget_password/"
+ views.ForgetPasswordView.as_view(), # 关联ForgetPasswordView视图,处理密码重置表单逻辑
+ name='forget_password'), # URL命名,用于反向生成忘记密码页面路径
+
+ # 6. 获取忘记密码验证码的URL(发送验证码到邮箱)
+ re_path(r'^forget_password_code/$', # 正则匹配路径:精确匹配"/forget_password_code/"
+ views.ForgetPasswordEmailCode.as_view(), # 关联ForgetPasswordEmailCode视图,处理发送验证码逻辑
+ name='forget_password_code'), # URL命名,用于反向生成获取验证码的路径
+]
\ No newline at end of file
diff --git a/doc/第7周代码注释/accounts/user_login_backend.py b/doc/第7周代码注释/accounts/user_login_backend.py
new file mode 100644
index 00000000..49561dcf
--- /dev/null
+++ b/doc/第7周代码注释/accounts/user_login_backend.py
@@ -0,0 +1,53 @@
+# 导入Django的用户模型工具,用于获取项目配置的用户模型(支持自定义用户模型)
+from django.contrib.auth import get_user_model
+# 导入Django内置的模型认证后端基类,用于扩展自定义认证逻辑
+from django.contrib.auth.backends import ModelBackend
+
+
+class EmailOrUsernameModelBackend(ModelBackend):
+ """
+ 自定义认证后端,继承自Django的ModelBackend
+ 功能:允许用户使用用户名或邮箱地址进行登录验证
+ """
+
+ def authenticate(self, request, username=None, password=None, **kwargs):
+ """
+ 重写认证方法,实现用户名/邮箱登录逻辑
+ :param request: 请求对象
+ :param username: 登录时输入的标识(可能是用户名或邮箱)
+ :param password: 登录密码
+ :param kwargs: 其他关键字参数
+ :return: 验证成功返回用户对象,失败返回None
+ """
+ # 判断输入的"username"是否包含@符号,若包含则视为邮箱登录
+ if '@' in username:
+ # 构建查询条件:使用email字段匹配
+ kwargs = {'email': username}
+ else:
+ # 否则视为用户名登录,构建查询条件:使用username字段匹配
+ kwargs = {'username': username}
+
+ try:
+ # 根据构建的条件查询用户(使用项目配置的用户模型)
+ user = get_user_model().objects.get(**kwargs)
+ # 验证查询到的用户密码是否正确(Django内置的密码校验,自动处理哈希对比)
+ if user.check_password(password):
+ # 密码正确,返回用户对象
+ return user
+ except get_user_model().DoesNotExist:
+ # 若用户不存在(查询失败),返回None表示认证失败
+ return None
+
+ def get_user(self, username):
+ """
+ 重写获取用户的方法,根据用户主键获取用户对象
+ (Django认证系统会调用此方法获取已认证用户的详细信息)
+ :param username: 实际为用户的主键(pk)
+ :return: 存在则返回用户对象,不存在返回None
+ """
+ try:
+ # 根据主键查询用户
+ return get_user_model().objects.get(pk=username)
+ except get_user_model().DoesNotExist:
+ # 用户不存在,返回None
+ return None
diff --git a/doc/第7周代码注释/accounts/utils.py b/doc/第7周代码注释/accounts/utils.py
new file mode 100644
index 00000000..f5a844fe
--- /dev/null
+++ b/doc/第7周代码注释/accounts/utils.py
@@ -0,0 +1,81 @@
+# 导入类型提示模块,用于定义函数参数和返回值的类型(增强代码可读性和类型检查)
+import typing
+# 导入timedelta,用于定义时间间隔(此处用于设置验证码有效期)
+from datetime import timedelta
+
+# 导入Django缓存模块,用于临时存储验证码(避免数据库频繁读写)
+from django.core.cache import cache
+# 导入Django翻译工具:gettext用于实时翻译字符串,gettext_lazy用于延迟翻译(适合定义常量时使用)
+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: 接收邮件的目标邮箱地址(字符串类型)
+ code: 生成的验证码(字符串类型,用于后续验证)
+ subject: 邮件主题,默认值为"Verify Email"(支持国际化,可根据语言设置自动翻译)
+ """
+ # 构造邮件的HTML内容:包含验证码和有效期提示,使用国际化翻译,通过%(code)s格式化插入验证码
+ 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函数发送邮件:参数为收件人列表、邮件主题、邮件内容
+ send_email([to_mail], subject, html_content)
+
+
+def verify(email: str, code: str) -> typing.Optional[str]:
+ """
+ 验证用户输入的验证码是否有效(与缓存中存储的验证码对比)
+
+ Args:
+ email: 用户提交的邮箱地址(用于匹配缓存中对应的验证码)
+ code: 用户输入的验证码(需要验证的字符串)
+
+ Return:
+ 验证失败时返回错误提示字符串(如"Verification code error"),验证成功时返回None
+
+ Note:
+ 原代码注释说明:当前错误处理方式不合理,建议通过raise抛出异常替代返回错误字符串,
+ 避免调用方需要额外处理返回的错误信息,使错误处理更符合Python规范
+ """
+ # 从缓存中获取该邮箱对应的验证码(调用get_code函数)
+ cache_code = get_code(email)
+ # 对比用户输入的验证码与缓存中的验证码,不一致则返回错误提示
+ if cache_code != code:
+ return gettext("Verification code error")
+
+
+def set_code(email: str, code: str):
+ """
+ 将邮箱与对应的验证码存储到Django缓存中,并设置过期时间(使用全局的_code_ttl)
+
+ Args:
+ email: 作为缓存键(key)的邮箱地址(确保每个邮箱的验证码唯一)
+ code: 作为缓存值(value)的验证码(需要存储的字符串)
+ """
+ # 调用cache.set存储数据:key=email,value=code,timeout=_code_ttl.seconds(有效期转换为秒数)
+ cache.set(email, code, _code_ttl.seconds)
+
+
+def get_code(email: str) -> typing.Optional[str]:
+ """
+ 根据邮箱地址从Django缓存中获取对应的验证码
+
+ Args:
+ email: 用于查询的缓存键(key),即目标邮箱地址
+
+ Return:
+ 缓存中存在该邮箱对应的验证码时返回字符串类型的验证码,不存在或过期时返回None
+ """
+ # 调用cache.get获取缓存值:key=email,不存在则返回None
+ return cache.get(email)
diff --git a/doc/第7周代码注释/accounts/views.py b/doc/第7周代码注释/accounts/views.py
new file mode 100644
index 00000000..9ea4ac06
--- /dev/null
+++ b/doc/第7周代码注释/accounts/views.py
@@ -0,0 +1,330 @@
+# 导入日志模块,用于记录视图操作中的关键信息(如请求类型、用户状态等)
+import logging
+# 导入Django国际化翻译工具,用于视图中文本的多语言支持
+from django.utils.translation import gettext_lazy as _
+# 导入Django项目配置,用于获取SECRET_KEY等全局设置
+from django.conf import settings
+# 导入Django认证相关模块:auth处理登录逻辑,REDIRECT_FIELD_NAME定义重定向参数名,get_user_model获取自定义用户模型
+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
+# 导入DjangoHTTP响应类:重定向、403禁止访问、基础响应
+from django.http import HttpResponseRedirect, HttpResponseForbidden
+from django.http.request import HttpRequest
+from django.http.response import HttpResponse
+# 导入Django快捷函数:get_object_or_404(获取对象不存在时返回404)、render(渲染模板)
+from django.shortcuts import get_object_or_404
+from django.shortcuts import render
+# 导入reverse函数,通过URL名称生成路径(避免硬编码路径)
+from django.urls import reverse
+# 导入Django视图装饰器:method_decorator(为类视图方法添加装饰器)、never_cache(禁止缓存)、csrf_protect(CSRF保护)、sensitive_post_parameters(保护敏感POST参数)
+from django.utils.decorators import method_decorator
+from django.utils.http import url_has_allowed_host_and_scheme # 验证重定向地址是否安全
+# 导入Django类视图基类:View(基础视图类)、FormView(处理表单的视图类)、RedirectView(处理重定向的视图类)
+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
+
+# 导入项目自定义工具函数:发送邮件、SHA256加密、获取当前站点、生成验证码、删除侧边栏缓存
+from djangoblog.utils import send_email, get_sha256, get_current_site, generate_code, delete_sidebar_cache
+# 导入当前应用(accounts)的工具函数(验证码相关操作)
+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):
+ """
+ 处理用户注册的类视图,继承自FormView(专门处理表单提交的视图基类)
+ 功能:展示注册表单、验证表单数据、创建未激活用户、发送邮箱验证链接、跳转注册结果页
+ """
+ form_class = RegisterForm # 关联的表单类:自定义的RegisterForm
+ template_name = 'account/registration_form.html' # 渲染的模板文件路径
+
+ @method_decorator(csrf_protect)
+ def dispatch(self, *args, **kwargs):
+ """
+ 重写dispatch方法,为视图添加CSRF保护装饰器
+ dispatch是类视图的入口方法,所有请求(GET/POST)都会先经过此方法
+ """
+ return super(RegisterView, self).dispatch(*args, **kwargs)
+
+ def form_valid(self, form):
+ """
+ 表单数据验证通过后执行的逻辑(用户提交的注册信息合法时)
+ """
+ if form.is_valid():
+ # 1. 创建用户但不立即提交到数据库(commit=False),后续手动设置额外字段
+ user = form.save(False)
+ # 2. 设置用户初始状态:未激活(需邮箱验证后激活)、注册来源为"Register"
+ user.is_active = False
+ user.source = 'Register'
+ # 3. 提交用户数据到数据库
+ user.save(True)
+
+ # 4. 生成邮箱验证链接:包含当前站点域名、验证路径、用户ID、加密签名
+ site = get_current_site().domain # 获取当前站点域名(如www.example.com)
+ # 双重SHA256加密:使用SECRET_KEY+用户ID生成签名,防止链接被篡改
+ sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
+ # 开发环境(DEBUG=True)下,站点域名替换为本地地址(127.0.0.1:8000)
+ if settings.DEBUG:
+ site = '127.0.0.1:8000'
+ # 获取验证结果页的路径(通过URL名称反向生成)
+ path = reverse('account:result')
+ # 拼接完整的邮箱验证链接
+ url = "http://{site}{path}?type=validation&id={id}&sign={sign}".format(
+ site=site, path=path, id=user.id, sign=sign)
+
+ # 5. 构造验证邮件内容(包含验证链接)
+ content = """
+
请点击下面链接验证您的邮箱
+
+ {url}
+
+ 再次感谢您!
+
+ 如果上面链接无法打开,请将此链接复制至浏览器。
+ {url}
+ """.format(url=url)
+ # 6. 发送验证邮件到用户注册邮箱
+ send_email(
+ emailto=[user.email], # 收件人列表
+ title='验证您的电子邮箱', # 邮件标题
+ content=content # 邮件HTML内容
+ )
+
+ # 7. 重定向到注册结果页(携带注册成功的类型和用户ID参数)
+ url = reverse('accounts:result') + '?type=register&id=' + str(user.id)
+ return HttpResponseRedirect(url)
+ else:
+ # 表单验证失败(如用户名已存在、邮箱格式错误),重新渲染表单并显示错误信息
+ return self.render_to_response({'form': form})
+
+
+class LogoutView(RedirectView):
+ """
+ 处理用户登出的类视图,继承自RedirectView(专门处理重定向的视图基类)
+ 功能:执行登出逻辑、删除侧边栏缓存、重定向到登录页
+ """
+ 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请求(用户访问登出URL时)
+ """
+ # 1. 执行Django内置的登出函数,清除用户的session信息
+ logout(request)
+ # 2. 删除侧边栏缓存(可能存储了用户相关信息,登出后需更新)
+ delete_sidebar_cache()
+ # 3. 调用父类的get方法,执行重定向到登录页
+ return super(LogoutView, self).get(request, *args, **kwargs)
+
+
+class LoginView(FormView):
+ """
+ 处理用户登录的类视图,继承自FormView
+ 功能:展示登录表单、验证登录信息、处理"记住我"功能、重定向到目标页面
+ """
+ form_class = LoginForm # 关联的表单类:自定义的LoginForm
+ template_name = 'account/login.html' # 渲染的登录模板路径
+ success_url = '/' # 登录成功后的默认重定向路径(网站根目录)
+ redirect_field_name = REDIRECT_FIELD_NAME # 重定向参数名(默认是"next")
+ login_ttl = 2626560 # 记住登录状态的有效期(秒),约等于一个月
+
+ @method_decorator(sensitive_post_parameters('password'))
+ @method_decorator(csrf_protect)
+ @method_decorator(never_cache)
+ def dispatch(self, request, *args, **kwargs):
+ """
+ 重写dispatch方法,添加三个装饰器:
+ 1. sensitive_post_parameters('password'):保护密码参数,不在错误日志中显示
+ 2. csrf_protect:开启CSRF保护,防止跨站请求伪造
+ 3. never_cache:禁止缓存登录页面,确保每次访问都是最新状态
+ """
+ return super(LoginView, self).dispatch(request, *args, **kwargs)
+
+ def get_context_data(self, **kwargs):
+ """
+ 扩展上下文数据,将重定向地址(next参数)传递到模板中
+ 模板中可根据该参数决定登录成功后跳转到哪里
+ """
+ # 从GET请求中获取重定向地址(如访问需要登录的页面时,会携带next参数)
+ 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):
+ """
+ 表单数据验证通过后执行的逻辑(用户提交的登录信息合法时)
+ 注意:此处重新初始化了AuthenticationForm,用于Django内置的登录验证
+ """
+ # 用请求的POST数据和request对象初始化Django内置的认证表单
+ form = AuthenticationForm(data=self.request.POST, request=self.request)
+
+ if form.is_valid():
+ # 1. 登录成功,删除侧边栏缓存(可能存储了未登录状态的内容)
+ delete_sidebar_cache()
+ # 2. 记录重定向参数名到日志(用于调试)
+ logger.info(self.redirect_field_name)
+
+ # 3. 执行Django内置的登录函数,将用户信息存入session
+ auth.login(self.request, form.get_user())
+ # 4. 处理"记住我"功能:如果用户勾选了"remember",设置session有效期
+ if self.request.POST.get("remember"):
+ self.request.session.set_expiry(self.login_ttl)
+ # 5. 调用父类的form_valid方法,执行重定向到成功页面
+ return super(LoginView, self).form_valid(form)
+ else:
+ # 表单验证失败(如用户名不存在、密码错误),重新渲染表单并显示错误
+ return self.render_to_response({'form': form})
+
+ def get_success_url(self):
+ """
+ 自定义登录成功后的重定向地址
+ 优先使用POST请求中的"next"参数(如果合法),否则使用默认的success_url
+ """
+ # 从POST请求中获取重定向地址(用户登录时提交的next参数)
+ 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):
+ """
+ 处理账户操作结果的函数视图(注册成功提示、邮箱验证结果)
+ 功能:根据URL参数(type和id)展示不同的结果信息,激活用户邮箱
+ """
+ # 从GET请求中获取操作类型(register/validation)和用户ID
+ type = request.GET.get('type')
+ id = request.GET.get('id')
+
+ # 根据用户ID查询用户,不存在则返回404页面
+ user = get_object_or_404(get_user_model(), id=id)
+ # 记录操作类型到日志
+ logger.info(type)
+
+ # 如果用户已激活(is_active=True),直接重定向到根目录(无需再展示结果)
+ if user.is_active:
+ return HttpResponseRedirect('/')
+
+ # 如果操作类型合法(属于register或validation),处理对应的逻辑
+ if type and type in ['register', 'validation']:
+ if type == 'register':
+ # 1. 注册成功场景:展示注册成功提示,告知用户验证邮件已发送
+ content = '''
+ 恭喜您注册成功,一封验证邮件已经发送到您的邮箱,请验证您的邮箱后登录本站。
+ '''
+ title = '注册成功'
+ else:
+ # 2. 邮箱验证场景:验证签名是否合法,合法则激活用户
+ # 重新生成签名(用于与请求中的sign参数对比)
+ c_sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
+ # 从GET请求中获取签名参数
+ sign = request.GET.get('sign')
+ # 如果签名不匹配(链接被篡改),返回403禁止访问
+ if sign != c_sign:
+ return HttpResponseForbidden()
+ # 签名匹配,激活用户(设置is_active=True)并保存
+ user.is_active = True
+ user.save()
+ # 展示验证成功提示
+ content = '''
+ 恭喜您已经成功的完成邮箱验证,您现在可以使用您的账号来登录本站。
+ '''
+ title = '验证成功'
+ # 渲染结果页面,传递标题和内容
+ return render(request, 'account/result.html', {
+ 'title': title,
+ 'content': content
+ })
+ else:
+ # 操作类型不合法,重定向到根目录
+ return HttpResponseRedirect('/')
+
+
+class ForgetPasswordView(FormView):
+ """
+ 处理忘记密码的类视图,继承自FormView
+ 功能:展示忘记密码表单、验证表单数据(验证码、邮箱、密码)、更新用户密码
+ """
+ form_class = ForgetPasswordForm # 关联的表单类:自定义的ForgetPasswordForm
+ template_name = 'account/forget_password.html' # 渲染的忘记密码模板路径
+
+ def form_valid(self, form):
+ """
+ 表单数据验证通过后执行的逻辑(验证码、邮箱、密码均合法时)
+ """
+ if form.is_valid():
+ # 1. 根据表单中的邮箱查询对应的用户
+ blog_user = BlogUser.objects.filter(email=form.cleaned_data.get("email")).get()
+ # 2. 用新密码(加密处理)更新用户密码
+ blog_user.password = make_password(form.cleaned_data["new_password2"])
+ # 3. 保存密码更新结果到数据库
+ blog_user.save()
+ # 4. 重定向到登录页(密码重置成功后需重新登录)
+ return HttpResponseRedirect('/login/')
+ else:
+ # 表单验证失败(如验证码错误、密码不一致),重新渲染表单并显示错误
+ return self.render_to_response({'form': form})
+
+
+class ForgetPasswordEmailCode(View):
+ """
+ 处理发送忘记密码验证码的类视图,继承自基础View类
+ 功能:接收邮箱、验证邮箱格式、生成验证码、发送验证邮件、存储验证码到缓存
+ """
+
+ def post(self, request: HttpRequest):
+ """
+ 处理POST请求(用户提交邮箱以获取验证码时)
+ """
+ # 1. 用请求的POST数据初始化验证码表单
+ form = ForgetPasswordCodeForm(request.POST)
+ # 2. 验证表单(主要验证邮箱格式是否合法)
+ if not form.is_valid():
+ # 表单验证失败(邮箱格式错误),返回"错误的邮箱"响应
+ return HttpResponse("错误的邮箱")
+
+ # 3. 表单验证通过,获取清洗后的邮箱地址
+ to_email = form.cleaned_data["email"]
+
+ # 4. 生成随机验证码
+ code = generate_code()
+ # 5. 发送验证码邮件到用户邮箱(调用utils中的send_verify_email函数)
+ utils.send_verify_email(to_email, code)
+ # 6. 将验证码存储到缓存(关联邮箱,设置有效期)
+ utils.set_code(to_email, code)
+
+ # 7. 发送成功,返回"ok"响应
+ return HttpResponse("ok")
diff --git a/doc/第7周代码注释/blog/__init__.py b/doc/第7周代码注释/blog/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/doc/第7周代码注释/blog/__pycache__/__init__.cpython-311.pyc b/doc/第7周代码注释/blog/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 00000000..e9fba0c4
Binary files /dev/null and b/doc/第7周代码注释/blog/__pycache__/__init__.cpython-311.pyc differ
diff --git a/doc/第7周代码注释/blog/__pycache__/admin.cpython-311.pyc b/doc/第7周代码注释/blog/__pycache__/admin.cpython-311.pyc
new file mode 100644
index 00000000..5e155074
Binary files /dev/null and b/doc/第7周代码注释/blog/__pycache__/admin.cpython-311.pyc differ
diff --git a/doc/第7周代码注释/blog/__pycache__/apps.cpython-311.pyc b/doc/第7周代码注释/blog/__pycache__/apps.cpython-311.pyc
new file mode 100644
index 00000000..8f83b3a6
Binary files /dev/null and b/doc/第7周代码注释/blog/__pycache__/apps.cpython-311.pyc differ
diff --git a/doc/第7周代码注释/blog/__pycache__/context_processors.cpython-311.pyc b/doc/第7周代码注释/blog/__pycache__/context_processors.cpython-311.pyc
new file mode 100644
index 00000000..56ece79f
Binary files /dev/null and b/doc/第7周代码注释/blog/__pycache__/context_processors.cpython-311.pyc differ
diff --git a/doc/第7周代码注释/blog/__pycache__/documents.cpython-311.pyc b/doc/第7周代码注释/blog/__pycache__/documents.cpython-311.pyc
new file mode 100644
index 00000000..3e59396e
Binary files /dev/null and b/doc/第7周代码注释/blog/__pycache__/documents.cpython-311.pyc differ
diff --git a/doc/第7周代码注释/blog/__pycache__/middleware.cpython-311.pyc b/doc/第7周代码注释/blog/__pycache__/middleware.cpython-311.pyc
new file mode 100644
index 00000000..16ee7686
Binary files /dev/null and b/doc/第7周代码注释/blog/__pycache__/middleware.cpython-311.pyc differ
diff --git a/doc/第7周代码注释/blog/__pycache__/models.cpython-311.pyc b/doc/第7周代码注释/blog/__pycache__/models.cpython-311.pyc
new file mode 100644
index 00000000..9f69feb4
Binary files /dev/null and b/doc/第7周代码注释/blog/__pycache__/models.cpython-311.pyc differ
diff --git a/doc/第7周代码注释/blog/__pycache__/search_indexes.cpython-311.pyc b/doc/第7周代码注释/blog/__pycache__/search_indexes.cpython-311.pyc
new file mode 100644
index 00000000..1f6f8b92
Binary files /dev/null and b/doc/第7周代码注释/blog/__pycache__/search_indexes.cpython-311.pyc differ
diff --git a/doc/第7周代码注释/blog/__pycache__/urls.cpython-311.pyc b/doc/第7周代码注释/blog/__pycache__/urls.cpython-311.pyc
new file mode 100644
index 00000000..0f273215
Binary files /dev/null and b/doc/第7周代码注释/blog/__pycache__/urls.cpython-311.pyc differ
diff --git a/doc/第7周代码注释/blog/__pycache__/views.cpython-311.pyc b/doc/第7周代码注释/blog/__pycache__/views.cpython-311.pyc
new file mode 100644
index 00000000..d827e639
Binary files /dev/null and b/doc/第7周代码注释/blog/__pycache__/views.cpython-311.pyc differ
diff --git a/doc/第7周代码注释/blog/admin.py b/doc/第7周代码注释/blog/admin.py
new file mode 100644
index 00000000..4517f441
--- /dev/null
+++ b/doc/第7周代码注释/blog/admin.py
@@ -0,0 +1,204 @@
+# 导入Django表单模块,用于创建自定义表单
+from django import forms
+# 导入Django admin模块,用于配置后台管理界面
+from django.contrib import admin
+# 导入Django用户模型获取工具,兼容自定义用户模型场景
+from django.contrib.auth import get_user_model
+# 导入Django URL反向解析模块,用于生成后台管理页面的链接
+from django.urls import reverse
+# 导入Django HTML格式化工具,用于在后台显示自定义HTML内容(如链接)
+from django.utils.html import format_html
+# 导入Django国际化翻译工具,用于实现后台文字的多语言支持
+from django.utils.translation import gettext_lazy as _
+
+# 注册自定义模型到admin后台的标识注释(固定写法)
+# Register your models here.
+# 从当前应用的models.py文件中导入Article模型(文章模型)
+from .models import Article
+
+
+# 自定义Article模型的列表页过滤器,继承自Django内置的SimpleListFilter
+class ArticleListFilter(admin.SimpleListFilter):
+ # 过滤器在后台页面显示的标题(支持国际化),这里是“作者”
+ title = _("author")
+ # 过滤器对应的URL参数名,用于URL中传递过滤条件(如?author=1)
+ parameter_name = 'author'
+
+ # 定义过滤器的选项列表,返回(值, 显示文本)格式的元组
+ def lookups(self, request, model_admin):
+ # 1. 从Article模型中获取所有文章的作者,用set去重(避免同一作者多次出现)
+ # 2. 转换为list便于遍历,map函数提取每篇文章的author字段
+ authors = list(set(map(lambda x: x.author, Article.objects.all())))
+ # 遍历去重后的作者列表,生成过滤器选项
+ for author in authors:
+ # 选项值为作者ID(用于数据库查询),显示文本为作者用户名(支持国际化)
+ yield (author.id, _(author.username))
+
+ # 根据过滤器选择的参数,过滤文章查询集(queryset)
+ def queryset(self, request, queryset):
+ # 获取当前过滤器选中的参数值(即作者ID)
+ id = self.value()
+ # 如果有选中的作者ID,返回该作者的所有文章
+ if id:
+ return queryset.filter(author__id__exact=id)
+ # 如果未选中任何作者,返回全部文章查询集
+ else:
+ return queryset
+
+
+# 自定义Article模型的表单类,继承自Django内置的ModelForm
+class ArticleForm(forms.ModelForm):
+ # 注释:此处原本计划使用AdminPagedownWidget富文本编辑器渲染body字段,暂未启用
+ # body = forms.CharField(widget=AdminPagedownWidget())
+
+ # 表单元数据配置类,用于关联模型与字段
+ class Meta:
+ # 关联的模型为Article(表示该表单用于操作Article模型数据)
+ model = Article
+ # 表单包含模型的所有字段(__all__为通配符,也可指定具体字段列表)
+ fields = '__all__'
+
+
+# 自定义批量操作函数:将选中的文章状态设为“已发布”(假设status='p'代表发布)
+def makr_article_publish(modeladmin, request, queryset):
+ # 批量更新选中的文章查询集,将status字段设为'p'
+ queryset.update(status='p')
+
+
+# 自定义批量操作函数:将选中的文章状态设为“草稿”(假设status='d'代表草稿)
+def draft_article(modeladmin, request, queryset):
+ # 批量更新选中的文章查询集,将status字段设为'd'
+ queryset.update(status='d')
+
+
+# 自定义批量操作函数:关闭选中文章的评论功能(假设comment_status='c'代表关闭)
+def close_article_commentstatus(modeladmin, request, queryset):
+ # 批量更新选中的文章查询集,将comment_status字段设为'c'
+ queryset.update(comment_status='c')
+
+
+# 自定义批量操作函数:开启选中文章的评论功能(假设comment_status='o'代表开启)
+def open_article_commentstatus(modeladmin, request, queryset):
+ # 批量更新选中的文章查询集,将comment_status字段设为'o'
+ queryset.update(comment_status='o')
+
+
+# 为批量操作函数设置后台显示名称(支持国际化),显示在“动作”下拉菜单中
+makr_article_publish.short_description = _('Publish selected articles') # “发布选中的文章”
+draft_article.short_description = _('Draft selected articles') # “将选中的文章设为草稿”
+close_article_commentstatus.short_description = _('Close article comments') # “关闭选中文章的评论”
+open_article_commentstatus.short_description = _('Open article comments') # “开启选中文章的评论”
+
+
+# 自定义Article模型的Admin配置类,继承自Django内置的ModelAdmin
+class ArticlelAdmin(admin.ModelAdmin):
+ # 后台列表页每页显示的文章数量,这里是20条/页
+ list_per_page = 20
+ # 后台列表页的搜索框配置,支持搜索文章的“内容(body)”和“标题(title)”字段
+ search_fields = ('body', 'title')
+ # 后台添加/编辑文章时使用的自定义表单,即上面定义的ArticleForm
+ form = ArticleForm
+ # 后台列表页显示的字段列表,按顺序展示
+ list_display = (
+ 'id', # 文章ID
+ 'title', # 文章标题
+ 'author', # 文章作者
+ 'link_to_category', # 自定义字段:文章分类(带跳转链接)
+ 'creation_time', # 文章创建时间
+ 'views', # 文章浏览量
+ 'status', # 文章状态(发布/草稿等)
+ 'type', # 文章类型(如原创/转载等,需在Article模型中定义)
+ 'article_order' # 文章排序权重(用于自定义排序)
+ )
+ # 后台列表页中,点击哪些字段可以跳转到文章编辑页
+ list_display_links = ('id', 'title')
+ # 后台列表页的过滤器配置,包含自定义的作者过滤器和内置字段过滤器
+ list_filter = (ArticleListFilter, 'status', 'type', 'category', 'tags')
+ # 多对多字段(tags标签)的编辑界面样式,使用水平选择框(默认是垂直)
+ filter_horizontal = ('tags',)
+ # 后台添加/编辑文章时,隐藏的字段(不允许管理员手动修改)
+ exclude = ('creation_time', 'last_modify_time')
+ # 是否显示“在站点上查看”按钮(跳转到文章的前台页面)
+ view_on_site = True
+ # 后台列表页的批量操作功能列表,关联上面定义的4个批量操作函数
+ actions = [
+ makr_article_publish,
+ draft_article,
+ close_article_commentstatus,
+ open_article_commentstatus]
+
+ # 自定义列表页字段:显示文章分类,并添加跳转到分类编辑页的链接
+ def link_to_category(self, obj):
+ # 1. 获取文章分类(obj.category)的模型元数据(应用名、模型名)
+ # 2. 用于生成admin后台分类编辑页的URL
+ info = (obj.category._meta.app_label, obj.category._meta.model_name)
+ # 3. 反向解析分类编辑页的URL,参数为分类ID(obj.category.id)
+ link = reverse('admin:%s_%s_change' % info, args=(obj.category.id,))
+ # 4. 生成HTML链接,显示分类名称,点击跳转到分类编辑页
+ return format_html(u'%s ' % (link, obj.category.name))
+
+ # 自定义字段“link_to_category”在后台列表页的显示标题(支持国际化)
+ link_to_category.short_description = _('category')
+
+ # 重写获取表单的方法,自定义作者字段(author)的可选值
+ def get_form(self, request, obj=None, **kwargs):
+ # 1. 先调用父类的get_form方法,获取默认表单
+ form = super(ArticlelAdmin, self).get_form(request, obj, **kwargs)
+ # 2. 限制作者字段的可选范围:仅显示超级用户(is_superuser=True)
+ form.base_fields['author'].queryset = get_user_model(
+ ).objects.filter(is_superuser=True)
+ # 3. 返回修改后的表单
+ return form
+
+ # 重写保存模型的方法(此处未修改逻辑,仅保留父类功能,便于后续扩展)
+ def save_model(self, request, obj, form, change):
+ # 调用父类的save_model方法,完成默认的保存逻辑(如更新时间、权限验证等)
+ super(ArticlelAdmin, self).save_model(request, obj, form, change)
+
+ # 重写“在站点上查看”的链接地址,跳转到文章的前台完整URL
+ def get_view_on_site_url(self, obj=None):
+ # 如果有具体的文章对象(obj不为空),调用文章模型的get_full_url方法获取前台URL
+ if obj:
+ url = obj.get_full_url()
+ return url
+ # 如果没有文章对象(如在列表页顶部的“查看站点”按钮),返回当前站点的域名
+ else:
+ # 从自定义工具模块导入获取当前站点的函数(需确保djangoblog.utils存在)
+ from djangoblog.utils import get_current_site
+ # 获取当前站点的域名(如www.example.com)
+ site = get_current_site().domain
+ return site
+
+
+# 自定义Tag模型(标签模型)的Admin配置类(假设Tag模型在models.py中定义)
+class TagAdmin(admin.ModelAdmin):
+ # 后台添加/编辑标签时,隐藏的字段(不允许手动修改)
+ exclude = ('slug', 'last_mod_time', 'creation_time')
+
+
+# 自定义Category模型(分类模型)的Admin配置类(假设Category模型在models.py中定义)
+class CategoryAdmin(admin.ModelAdmin):
+ # 后台分类列表页显示的字段:分类名称、父分类、排序索引
+ list_display = ('name', 'parent_category', 'index')
+ # 后台添加/编辑分类时,隐藏的字段(不允许手动修改)
+ exclude = ('slug', 'last_mod_time', 'creation_time')
+
+
+# 自定义Links模型(友情链接模型)的Admin配置类(假设Links模型在models.py中定义)
+class LinksAdmin(admin.ModelAdmin):
+ # 后台添加/编辑友情链接时,隐藏的字段(不允许手动修改)
+ exclude = ('last_mod_time', 'creation_time')
+
+
+# 自定义SideBar模型(侧边栏模型)的Admin配置类(假设SideBar模型在models.py中定义)
+class SideBarAdmin(admin.ModelAdmin):
+ # 后台侧边栏列表页显示的字段:名称、内容、是否启用、排序序号
+ list_display = ('name', 'content', 'is_enable', 'sequence')
+ # 后台添加/编辑侧边栏时,隐藏的字段(不允许手动修改)
+ exclude = ('last_mod_time', 'creation_time')
+
+
+# 自定义BlogSettings模型(博客设置模型)的Admin配置类(假设BlogSettings模型在models.py中定义)
+class BlogSettingsAdmin(admin.ModelAdmin):
+ # 空配置,使用ModelAdmin的默认功能(如需自定义可后续添加字段)
+ pass
\ No newline at end of file
diff --git a/doc/第7周代码注释/blog/apps.py b/doc/第7周代码注释/blog/apps.py
new file mode 100644
index 00000000..28e3f801
--- /dev/null
+++ b/doc/第7周代码注释/blog/apps.py
@@ -0,0 +1,9 @@
+# 导入Django的AppConfig类,该类用于定义单个Django应用的配置信息
+from django.apps import AppConfig
+
+
+# 定义当前应用(blog)的配置类,继承自Django提供的AppConfig基类
+class BlogConfig(AppConfig):
+ # 配置当前应用的唯一标识名称(即应用目录名),Django通过该名称识别和管理应用
+ # 这里'blog'表示当前配置对应的是名为'blog'的Django应用
+ name = 'blog'
\ No newline at end of file
diff --git a/doc/第7周代码注释/blog/context_processors.py b/doc/第7周代码注释/blog/context_processors.py
new file mode 100644
index 00000000..4517f441
--- /dev/null
+++ b/doc/第7周代码注释/blog/context_processors.py
@@ -0,0 +1,204 @@
+# 导入Django表单模块,用于创建自定义表单
+from django import forms
+# 导入Django admin模块,用于配置后台管理界面
+from django.contrib import admin
+# 导入Django用户模型获取工具,兼容自定义用户模型场景
+from django.contrib.auth import get_user_model
+# 导入Django URL反向解析模块,用于生成后台管理页面的链接
+from django.urls import reverse
+# 导入Django HTML格式化工具,用于在后台显示自定义HTML内容(如链接)
+from django.utils.html import format_html
+# 导入Django国际化翻译工具,用于实现后台文字的多语言支持
+from django.utils.translation import gettext_lazy as _
+
+# 注册自定义模型到admin后台的标识注释(固定写法)
+# Register your models here.
+# 从当前应用的models.py文件中导入Article模型(文章模型)
+from .models import Article
+
+
+# 自定义Article模型的列表页过滤器,继承自Django内置的SimpleListFilter
+class ArticleListFilter(admin.SimpleListFilter):
+ # 过滤器在后台页面显示的标题(支持国际化),这里是“作者”
+ title = _("author")
+ # 过滤器对应的URL参数名,用于URL中传递过滤条件(如?author=1)
+ parameter_name = 'author'
+
+ # 定义过滤器的选项列表,返回(值, 显示文本)格式的元组
+ def lookups(self, request, model_admin):
+ # 1. 从Article模型中获取所有文章的作者,用set去重(避免同一作者多次出现)
+ # 2. 转换为list便于遍历,map函数提取每篇文章的author字段
+ authors = list(set(map(lambda x: x.author, Article.objects.all())))
+ # 遍历去重后的作者列表,生成过滤器选项
+ for author in authors:
+ # 选项值为作者ID(用于数据库查询),显示文本为作者用户名(支持国际化)
+ yield (author.id, _(author.username))
+
+ # 根据过滤器选择的参数,过滤文章查询集(queryset)
+ def queryset(self, request, queryset):
+ # 获取当前过滤器选中的参数值(即作者ID)
+ id = self.value()
+ # 如果有选中的作者ID,返回该作者的所有文章
+ if id:
+ return queryset.filter(author__id__exact=id)
+ # 如果未选中任何作者,返回全部文章查询集
+ else:
+ return queryset
+
+
+# 自定义Article模型的表单类,继承自Django内置的ModelForm
+class ArticleForm(forms.ModelForm):
+ # 注释:此处原本计划使用AdminPagedownWidget富文本编辑器渲染body字段,暂未启用
+ # body = forms.CharField(widget=AdminPagedownWidget())
+
+ # 表单元数据配置类,用于关联模型与字段
+ class Meta:
+ # 关联的模型为Article(表示该表单用于操作Article模型数据)
+ model = Article
+ # 表单包含模型的所有字段(__all__为通配符,也可指定具体字段列表)
+ fields = '__all__'
+
+
+# 自定义批量操作函数:将选中的文章状态设为“已发布”(假设status='p'代表发布)
+def makr_article_publish(modeladmin, request, queryset):
+ # 批量更新选中的文章查询集,将status字段设为'p'
+ queryset.update(status='p')
+
+
+# 自定义批量操作函数:将选中的文章状态设为“草稿”(假设status='d'代表草稿)
+def draft_article(modeladmin, request, queryset):
+ # 批量更新选中的文章查询集,将status字段设为'd'
+ queryset.update(status='d')
+
+
+# 自定义批量操作函数:关闭选中文章的评论功能(假设comment_status='c'代表关闭)
+def close_article_commentstatus(modeladmin, request, queryset):
+ # 批量更新选中的文章查询集,将comment_status字段设为'c'
+ queryset.update(comment_status='c')
+
+
+# 自定义批量操作函数:开启选中文章的评论功能(假设comment_status='o'代表开启)
+def open_article_commentstatus(modeladmin, request, queryset):
+ # 批量更新选中的文章查询集,将comment_status字段设为'o'
+ queryset.update(comment_status='o')
+
+
+# 为批量操作函数设置后台显示名称(支持国际化),显示在“动作”下拉菜单中
+makr_article_publish.short_description = _('Publish selected articles') # “发布选中的文章”
+draft_article.short_description = _('Draft selected articles') # “将选中的文章设为草稿”
+close_article_commentstatus.short_description = _('Close article comments') # “关闭选中文章的评论”
+open_article_commentstatus.short_description = _('Open article comments') # “开启选中文章的评论”
+
+
+# 自定义Article模型的Admin配置类,继承自Django内置的ModelAdmin
+class ArticlelAdmin(admin.ModelAdmin):
+ # 后台列表页每页显示的文章数量,这里是20条/页
+ list_per_page = 20
+ # 后台列表页的搜索框配置,支持搜索文章的“内容(body)”和“标题(title)”字段
+ search_fields = ('body', 'title')
+ # 后台添加/编辑文章时使用的自定义表单,即上面定义的ArticleForm
+ form = ArticleForm
+ # 后台列表页显示的字段列表,按顺序展示
+ list_display = (
+ 'id', # 文章ID
+ 'title', # 文章标题
+ 'author', # 文章作者
+ 'link_to_category', # 自定义字段:文章分类(带跳转链接)
+ 'creation_time', # 文章创建时间
+ 'views', # 文章浏览量
+ 'status', # 文章状态(发布/草稿等)
+ 'type', # 文章类型(如原创/转载等,需在Article模型中定义)
+ 'article_order' # 文章排序权重(用于自定义排序)
+ )
+ # 后台列表页中,点击哪些字段可以跳转到文章编辑页
+ list_display_links = ('id', 'title')
+ # 后台列表页的过滤器配置,包含自定义的作者过滤器和内置字段过滤器
+ list_filter = (ArticleListFilter, 'status', 'type', 'category', 'tags')
+ # 多对多字段(tags标签)的编辑界面样式,使用水平选择框(默认是垂直)
+ filter_horizontal = ('tags',)
+ # 后台添加/编辑文章时,隐藏的字段(不允许管理员手动修改)
+ exclude = ('creation_time', 'last_modify_time')
+ # 是否显示“在站点上查看”按钮(跳转到文章的前台页面)
+ view_on_site = True
+ # 后台列表页的批量操作功能列表,关联上面定义的4个批量操作函数
+ actions = [
+ makr_article_publish,
+ draft_article,
+ close_article_commentstatus,
+ open_article_commentstatus]
+
+ # 自定义列表页字段:显示文章分类,并添加跳转到分类编辑页的链接
+ def link_to_category(self, obj):
+ # 1. 获取文章分类(obj.category)的模型元数据(应用名、模型名)
+ # 2. 用于生成admin后台分类编辑页的URL
+ info = (obj.category._meta.app_label, obj.category._meta.model_name)
+ # 3. 反向解析分类编辑页的URL,参数为分类ID(obj.category.id)
+ link = reverse('admin:%s_%s_change' % info, args=(obj.category.id,))
+ # 4. 生成HTML链接,显示分类名称,点击跳转到分类编辑页
+ return format_html(u'%s ' % (link, obj.category.name))
+
+ # 自定义字段“link_to_category”在后台列表页的显示标题(支持国际化)
+ link_to_category.short_description = _('category')
+
+ # 重写获取表单的方法,自定义作者字段(author)的可选值
+ def get_form(self, request, obj=None, **kwargs):
+ # 1. 先调用父类的get_form方法,获取默认表单
+ form = super(ArticlelAdmin, self).get_form(request, obj, **kwargs)
+ # 2. 限制作者字段的可选范围:仅显示超级用户(is_superuser=True)
+ form.base_fields['author'].queryset = get_user_model(
+ ).objects.filter(is_superuser=True)
+ # 3. 返回修改后的表单
+ return form
+
+ # 重写保存模型的方法(此处未修改逻辑,仅保留父类功能,便于后续扩展)
+ def save_model(self, request, obj, form, change):
+ # 调用父类的save_model方法,完成默认的保存逻辑(如更新时间、权限验证等)
+ super(ArticlelAdmin, self).save_model(request, obj, form, change)
+
+ # 重写“在站点上查看”的链接地址,跳转到文章的前台完整URL
+ def get_view_on_site_url(self, obj=None):
+ # 如果有具体的文章对象(obj不为空),调用文章模型的get_full_url方法获取前台URL
+ if obj:
+ url = obj.get_full_url()
+ return url
+ # 如果没有文章对象(如在列表页顶部的“查看站点”按钮),返回当前站点的域名
+ else:
+ # 从自定义工具模块导入获取当前站点的函数(需确保djangoblog.utils存在)
+ from djangoblog.utils import get_current_site
+ # 获取当前站点的域名(如www.example.com)
+ site = get_current_site().domain
+ return site
+
+
+# 自定义Tag模型(标签模型)的Admin配置类(假设Tag模型在models.py中定义)
+class TagAdmin(admin.ModelAdmin):
+ # 后台添加/编辑标签时,隐藏的字段(不允许手动修改)
+ exclude = ('slug', 'last_mod_time', 'creation_time')
+
+
+# 自定义Category模型(分类模型)的Admin配置类(假设Category模型在models.py中定义)
+class CategoryAdmin(admin.ModelAdmin):
+ # 后台分类列表页显示的字段:分类名称、父分类、排序索引
+ list_display = ('name', 'parent_category', 'index')
+ # 后台添加/编辑分类时,隐藏的字段(不允许手动修改)
+ exclude = ('slug', 'last_mod_time', 'creation_time')
+
+
+# 自定义Links模型(友情链接模型)的Admin配置类(假设Links模型在models.py中定义)
+class LinksAdmin(admin.ModelAdmin):
+ # 后台添加/编辑友情链接时,隐藏的字段(不允许手动修改)
+ exclude = ('last_mod_time', 'creation_time')
+
+
+# 自定义SideBar模型(侧边栏模型)的Admin配置类(假设SideBar模型在models.py中定义)
+class SideBarAdmin(admin.ModelAdmin):
+ # 后台侧边栏列表页显示的字段:名称、内容、是否启用、排序序号
+ list_display = ('name', 'content', 'is_enable', 'sequence')
+ # 后台添加/编辑侧边栏时,隐藏的字段(不允许手动修改)
+ exclude = ('last_mod_time', 'creation_time')
+
+
+# 自定义BlogSettings模型(博客设置模型)的Admin配置类(假设BlogSettings模型在models.py中定义)
+class BlogSettingsAdmin(admin.ModelAdmin):
+ # 空配置,使用ModelAdmin的默认功能(如需自定义可后续添加字段)
+ pass
\ No newline at end of file
diff --git a/doc/第7周代码注释/blog/documents.py b/doc/第7周代码注释/blog/documents.py
new file mode 100644
index 00000000..e7004184
--- /dev/null
+++ b/doc/第7周代码注释/blog/documents.py
@@ -0,0 +1,277 @@
+# 导入Python内置time模块,用于生成唯一ID(时间戳毫秒级)
+import time
+
+# 导入Elasticsearch客户端模块,用于直接操作Elasticsearch服务(如创建管道、删除索引)
+import elasticsearch.client
+# 导入Django配置模块,用于读取项目中的Elasticsearch配置(settings.py中)
+from django.conf import settings
+# 从elasticsearch-dsl库导入核心组件:
+# Document:Elasticsearch文档模型基类(类似Django的Model)
+# InnerDoc:嵌套文档基类(用于存储结构化子数据,如地理位置、用户代理信息)
+# 字段类型:Date(日期)、Integer(整数)、Long(长整数)、Text(可分词文本)、Object(对象类型)、GeoPoint(地理坐标)、Keyword(不可分词文本)、Boolean(布尔值)
+from elasticsearch_dsl import Document, InnerDoc, Date, Integer, Long, Text, Object, GeoPoint, Keyword, Boolean
+# 导入elasticsearch-dsl的连接管理模块,用于建立与Elasticsearch服务的连接
+from elasticsearch_dsl.connections import connections
+
+# 从当前应用(blog)的models.py导入Article模型,用于将文章数据同步到Elasticsearch
+from blog.models import Article
+
+# 判断项目是否启用Elasticsearch:检查settings.py中是否配置了ELASTICSEARCH_DSL
+ELASTICSEARCH_ENABLED = hasattr(settings, 'ELASTICSEARCH_DSL')
+
+# 如果启用了Elasticsearch,执行以下初始化操作
+if ELASTICSEARCH_ENABLED:
+ # 建立与Elasticsearch服务的连接:从settings中读取配置的主机地址(如['http://localhost:9200'])
+ connections.create_connection(
+ hosts=[settings.ELASTICSEARCH_DSL['default']['hosts']])
+ # 导入Elasticsearch原生客户端,用于执行更底层的操作(如创建索引、删除索引)
+ from elasticsearch import Elasticsearch
+
+ # 初始化Elasticsearch原生客户端,传入服务地址
+ es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
+ # 导入Elasticsearch的IngestClient(数据处理管道客户端),用于创建数据预处理管道
+ from elasticsearch.client import IngestClient
+
+ # 初始化IngestClient,绑定到上面创建的Elasticsearch客户端
+ c = IngestClient(es)
+ try:
+ # 尝试获取名为'geoip'的数据处理管道(用于解析IP地址对应的地理位置)
+ c.get_pipeline('geoip')
+ # 如果管道不存在(捕获NotFoundError异常),则创建该管道
+ except elasticsearch.exceptions.NotFoundError:
+ # 创建'geoip'管道:定义数据处理逻辑,通过geoip处理器解析IP地址
+ c.put_pipeline('geoip', body='''{
+ "description" : "Add geoip info", // 管道描述:添加地理位置信息
+ "processors" : [ // 处理器列表:定义数据处理步骤
+ {
+ "geoip" : { // geoip处理器:Elasticsearch内置,用于解析IP
+ "field" : "ip" // 待解析的字段:文档中的'ip'字段
+ }
+ }
+ ]
+ }''')
+
+
+# 定义GeoIp嵌套文档类(InnerDoc):存储IP地址解析后的地理位置信息
+class GeoIp(InnerDoc):
+ continent_name = Keyword() # 洲名(Keyword类型:不可分词,适合精确查询/排序)
+ country_iso_code = Keyword() # 国家ISO代码(如CN、US,Keyword类型)
+ country_name = Keyword() # 国家名称(Keyword类型)
+ location = GeoPoint() # 地理坐标(经纬度,GeoPoint类型:支持地理位置查询)
+
+
+# 定义UserAgentBrowser嵌套文档类:存储用户代理(UA)中的浏览器信息
+class UserAgentBrowser(InnerDoc):
+ Family = Keyword() # 浏览器家族(如Chrome、Firefox,Keyword类型)
+ Version = Keyword() # 浏览器版本(如120.0,Keyword类型)
+
+
+# 定义UserAgentOS嵌套文档类:存储用户代理中的操作系统信息,继承自UserAgentBrowser(结构一致)
+class UserAgentOS(UserAgentBrowser):
+ pass # 直接继承父类字段,无需额外定义
+
+
+# 定义UserAgentDevice嵌套文档类:存储用户代理中的设备信息
+class UserAgentDevice(InnerDoc):
+ Family = Keyword() # 设备家族(如iPhone、Windows,Keyword类型)
+ Brand = Keyword() # 设备品牌(如Apple、Huawei,Keyword类型)
+ Model = Keyword() # 设备型号(如iPhone 15,Keyword类型)
+
+
+# 定义UserAgent嵌套文档类:存储完整的用户代理信息(包含浏览器、OS、设备)
+class UserAgent(InnerDoc):
+ browser = Object(UserAgentBrowser, required=False) # 浏览器信息(Object类型:关联UserAgentBrowser)
+ os = Object(UserAgentOS, required=False) # 操作系统信息(Object类型:关联UserAgentOS)
+ device = Object(UserAgentDevice, required=False) # 设备信息(Object类型:关联UserAgentDevice)
+ string = Text() # 完整UA字符串(Text类型:可分词,支持模糊查询)
+ is_bot = Boolean() # 是否为爬虫(Boolean类型:true/false)
+
+
+# 定义ElapsedTimeDocument文档类:Elasticsearch中的"性能监控"文档模型(记录请求耗时、访问信息)
+class ElapsedTimeDocument(Document):
+ url = Keyword() # 访问URL(Keyword类型:精确匹配,不分词)
+ time_taken = Long() # 请求耗时(毫秒,Long类型:支持大范围数值存储)
+ log_datetime = Date() # 日志记录时间(Date类型:支持时间范围查询)
+ ip = Keyword() # 访问IP地址(Keyword类型:精确匹配)
+ geoip = Object(GeoIp, required=False) # 地理位置信息(Object类型:关联GeoIp嵌套文档,非必填)
+ useragent = Object(UserAgent, required=False) # 用户代理信息(Object类型:关联UserAgent嵌套文档,非必填)
+
+ # 定义文档对应的Elasticsearch索引配置
+ class Index:
+ name = 'performance' # 索引名称:Elasticsearch中存储性能数据的索引名
+ settings = { # 索引设置
+ "number_of_shards": 1, # 分片数:1个(小型索引无需多分片)
+ "number_of_replicas": 0 # 副本数:0个(开发/小型场景无需副本,节省资源)
+ }
+
+ # 定义文档元数据(兼容Elasticsearch旧版本,doc_type在7.x后已废弃,此处保留兼容)
+ class Meta:
+ doc_type = 'ElapsedTime' # 文档类型:标识索引中的文档类别
+
+
+# 定义ElaspedTimeDocumentManager类:ElapsedTimeDocument的管理类(封装索引创建、数据插入等操作)
+class ElaspedTimeDocumentManager:
+ # 静态方法:创建性能监控索引(如果不存在)
+ @staticmethod
+ def build_index():
+ # 导入Elasticsearch原生客户端
+ from elasticsearch import Elasticsearch
+ # 初始化客户端,读取settings中的Elasticsearch地址
+ client = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
+ # 检查名为'performance'的索引是否已存在
+ res = client.indices.exists(index="performance")
+ # 如果索引不存在,初始化ElapsedTimeDocument(创建索引及映射)
+ if not res:
+ ElapsedTimeDocument.init()
+
+ # 静态方法:删除性能监控索引
+ @staticmethod
+ def delete_index():
+ from elasticsearch import Elasticsearch
+ es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
+ # 删除'performance'索引,忽略400(请求错误)和404(索引不存在)异常
+ es.indices.delete(index='performance', ignore=[400, 404])
+
+ # 静态方法:创建性能监控文档(插入一条访问耗时记录)
+ @staticmethod
+ def create(url, time_taken, log_datetime, useragent, ip):
+ # 确保索引已创建(调用build_index方法)
+ ElaspedTimeDocumentManager.build_index()
+ # 初始化UserAgent嵌套文档对象
+ ua = UserAgent()
+ # 赋值浏览器信息:从传入的useragent对象中提取浏览器家族和版本
+ ua.browser = UserAgentBrowser()
+ ua.browser.Family = useragent.browser.family
+ ua.browser.Version = useragent.browser.version_string
+
+ # 赋值操作系统信息:从传入的useragent对象中提取OS家族和版本
+ ua.os = UserAgentOS()
+ ua.os.Family = useragent.os.family
+ ua.os.Version = useragent.os.version_string
+
+ # 赋值设备信息:从传入的useragent对象中提取设备家族、品牌、型号
+ ua.device = UserAgentDevice()
+ ua.device.Family = useragent.device.family
+ ua.device.Brand = useragent.device.brand
+ ua.device.Model = useragent.device.model
+ # 赋值完整UA字符串和是否为爬虫的标识
+ ua.string = useragent.ua_string
+ ua.is_bot = useragent.is_bot
+
+ # 初始化ElapsedTimeDocument文档对象,设置字段值
+ doc = ElapsedTimeDocument(
+ meta={
+ 'id': int(round(time.time() * 1000)) # 文档ID:毫秒级时间戳(确保唯一)
+ },
+ url=url, # 访问URL
+ time_taken=time_taken, # 请求耗时(毫秒)
+ log_datetime=log_datetime,# 记录时间
+ useragent=ua, # 用户代理信息(嵌套文档)
+ ip=ip) # 访问IP
+ # 保存文档到Elasticsearch,并指定使用'geoip'管道预处理(解析IP地址)
+ doc.save(pipeline="geoip")
+
+
+# 定义ArticleDocument文档类:Elasticsearch中的"文章"文档模型(用于文章搜索)
+class ArticleDocument(Document):
+ # 文章内容:Text类型,使用ik_max_word分词器(分词更细,适合全文搜索),搜索时用ik_smart(分词更粗,提升效率)
+ body = Text(analyzer='ik_max_word', search_analyzer='ik_smart')
+ # 文章标题:同上,支持中文分词搜索
+ title = Text(analyzer='ik_max_word', search_analyzer='ik_smart')
+ # 作者信息:Object类型,包含昵称(可分词)和ID(整数)
+ author = Object(properties={
+ 'nickname': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
+ 'id': Integer()
+ })
+ # 分类信息:Object类型,包含分类名称(可分词)和ID(整数)
+ category = Object(properties={
+ 'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
+ 'id': Integer()
+ })
+ # 标签信息:Object类型(数组),每个标签包含名称(可分词)和ID(整数)
+ tags = Object(properties={
+ 'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
+ 'id': Integer()
+ })
+
+ pub_time = Date() # 发布时间(Date类型:支持按时间排序/筛选)
+ status = Text() # 文章状态(如'p'=发布,'d'=草稿,Text类型)
+ comment_status = Text() # 评论状态(如'o'=开启,'c'=关闭,Text类型)
+ type = Text() # 文章类型(如'p'=页面,'a'=普通文章,Text类型)
+ views = Integer() # 浏览量(Integer类型:支持数值排序)
+ article_order = Integer() # 排序权重(Integer类型:用于自定义文章排序)
+
+ # 定义文档对应的Elasticsearch索引配置
+ class Index:
+ name = 'blog' # 索引名称:存储文章数据的索引名
+ settings = { # 索引设置
+ "number_of_shards": 1, # 分片数:1个(小型博客无需多分片)
+ "number_of_replicas": 0 # 副本数:0个(开发/小型场景节省资源)
+ }
+
+ # 文档元数据(兼容旧版本Elasticsearch的doc_type)
+ class Meta:
+ doc_type = 'Article' # 文档类型:标识为文章类文档
+
+
+# 定义ArticleDocumentManager类:ArticleDocument的管理类(封装文章索引的创建、重建、更新等操作)
+class ArticleDocumentManager():
+ # 构造方法:实例化管理类时自动创建文章索引(如果不存在)
+ def __init__(self):
+ self.create_index()
+
+ # 实例方法:创建文章索引(调用ArticleDocument的init方法,生成索引和字段映射)
+ def create_index(self):
+ ArticleDocument.init()
+
+ # 实例方法:删除文章索引
+ def delete_index(self):
+ from elasticsearch import Elasticsearch
+ es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
+ # 删除'blog'索引,忽略400和404异常
+ es.indices.delete(index='blog', ignore=[400, 404])
+
+ # 实例方法:将Django的Article模型对象列表转换为Elasticsearch的ArticleDocument列表
+ def convert_to_doc(self, articles):
+ # 列表推导式:遍历每篇文章,构建对应的ArticleDocument
+ return [
+ ArticleDocument(
+ meta={'id': article.id}, # 文档ID与Django Article模型ID一致(便于关联)
+ body=article.body, # 文章内容
+ title=article.title, # 文章标题
+ author={ # 作者信息:从Article模型的author字段提取
+ 'nickname': article.author.username,
+ 'id': article.author.id
+ },
+ category={ # 分类信息:从Article模型的category字段提取
+ 'name': article.category.name,
+ 'id': article.category.id
+ },
+ tags=[ # 标签信息:遍历Article模型的tags多对多字段,提取每个标签的名称和ID
+ {'name': t.name, 'id': t.id} for t in article.tags.all()
+ ],
+ pub_time=article.pub_time, # 发布时间
+ status=article.status, # 文章状态
+ comment_status=article.comment_status, # 评论状态
+ type=article.type, # 文章类型
+ views=article.views, # 浏览量
+ article_order=article.article_order # 排序权重
+ ) for article in articles]
+
+ # 实例方法:重建文章索引(全量同步文章数据到Elasticsearch)
+ def rebuild(self, articles=None):
+ # 确保索引已创建(初始化索引和映射)
+ ArticleDocument.init()
+ # 如果传入了articles参数,则同步指定文章;否则同步所有文章(Article.objects.all())
+ articles = articles if articles else Article.objects.all()
+ # 将Django Article对象转换为Elasticsearch文档列表
+ docs = self.convert_to_doc(articles)
+ # 遍历文档列表,逐个保存到Elasticsearch
+ for doc in docs:
+ doc.save()
+
+ # 实例方法:批量更新Elasticsearch中的文章文档
+ def update_docs(self, docs):
+ # 遍历文档列表,逐个保存(已存在的文档会执行更新操作)
+ for doc in docs:
+ doc.save()
\ No newline at end of file
diff --git a/doc/第7周代码注释/blog/forms.py b/doc/第7周代码注释/blog/forms.py
new file mode 100644
index 00000000..218eeaff
--- /dev/null
+++ b/doc/第7周代码注释/blog/forms.py
@@ -0,0 +1,38 @@
+# 导入Python内置logging模块,用于记录搜索相关日志(如搜索关键词)
+import logging
+
+# 导入Django表单基础模块,用于创建自定义表单字段
+from django import forms
+# 从Haystack库导入基础搜索表单类SearchForm,Haystack是Django的搜索引擎集成框架
+from haystack.forms import SearchForm
+
+# 创建日志记录器,日志名称与当前模块一致(__name__),便于定位日志来源(区分其他模块日志)
+logger = logging.getLogger(__name__)
+
+
+# 定义自定义搜索表单类BlogSearchForm,继承自Haystack提供的SearchForm(基础搜索表单)
+# 作用:扩展Haystack默认搜索表单,添加自定义字段和搜索逻辑
+class BlogSearchForm(SearchForm):
+ # 定义搜索输入字段:querydata(搜索关键词字段)
+ # required=True:表示该字段为必填项,用户必须输入关键词才能提交搜索
+ # CharField:单行文本输入框,适合接收搜索关键词
+ querydata = forms.CharField(required=True)
+
+ # 重写父类的search方法,自定义搜索逻辑(保留父类核心功能,添加日志记录)
+ def search(self):
+ # 1. 调用父类(SearchForm)的search方法,执行Haystack默认搜索流程
+ # 父类会自动处理索引查询、关键词匹配等核心逻辑,返回搜索结果集(SearchQuerySet对象)
+ datas = super(BlogSearchForm, self).search()
+
+ # 2. 验证表单数据是否合法(根据字段定义的规则,如required=True)
+ if not self.is_valid():
+ # 若表单数据不合法(如未输入关键词),调用父类的no_query_found方法返回默认空结果
+ return self.no_query_found()
+
+ # 3. 若表单验证通过,获取清理后的搜索关键词(cleaned_data是Django表单验证后的安全数据字典)
+ if self.cleaned_data['querydata']:
+ # 记录搜索日志:将用户输入的关键词写入日志(便于统计热门搜索、排查问题)
+ logger.info(self.cleaned_data['querydata'])
+
+ # 4. 返回搜索结果集(datas),该结果集会传递给搜索结果页面模板进行渲染
+ return datas
\ No newline at end of file
diff --git a/doc/第7周代码注释/blog/management/__init__.py b/doc/第7周代码注释/blog/management/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/doc/第7周代码注释/blog/management/__pycache__/__init__.cpython-311.pyc b/doc/第7周代码注释/blog/management/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 00000000..403b3089
Binary files /dev/null and b/doc/第7周代码注释/blog/management/__pycache__/__init__.cpython-311.pyc differ
diff --git a/doc/第7周代码注释/blog/management/commands/__init__.py b/doc/第7周代码注释/blog/management/commands/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/doc/第7周代码注释/blog/management/commands/build_index.py b/doc/第7周代码注释/blog/management/commands/build_index.py
new file mode 100644
index 00000000..3c4acd74
--- /dev/null
+++ b/doc/第7周代码注释/blog/management/commands/build_index.py
@@ -0,0 +1,18 @@
+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()
diff --git a/doc/第7周代码注释/blog/management/commands/build_search_words.py b/doc/第7周代码注释/blog/management/commands/build_search_words.py
new file mode 100644
index 00000000..cfe7e0d5
--- /dev/null
+++ b/doc/第7周代码注释/blog/management/commands/build_search_words.py
@@ -0,0 +1,13 @@
+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))
diff --git a/doc/第7周代码注释/blog/management/commands/clear_cache.py b/doc/第7周代码注释/blog/management/commands/clear_cache.py
new file mode 100644
index 00000000..0d66172c
--- /dev/null
+++ b/doc/第7周代码注释/blog/management/commands/clear_cache.py
@@ -0,0 +1,11 @@
+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'))
diff --git a/doc/第7周代码注释/blog/management/commands/create_testdata.py b/doc/第7周代码注释/blog/management/commands/create_testdata.py
new file mode 100644
index 00000000..675d2ba6
--- /dev/null
+++ b/doc/第7周代码注释/blog/management/commands/create_testdata.py
@@ -0,0 +1,40 @@
+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'))
diff --git a/doc/第7周代码注释/blog/management/commands/ping_baidu.py b/doc/第7周代码注释/blog/management/commands/ping_baidu.py
new file mode 100644
index 00000000..2c7fbdd6
--- /dev/null
+++ b/doc/第7周代码注释/blog/management/commands/ping_baidu.py
@@ -0,0 +1,50 @@
+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'))
diff --git a/doc/第7周代码注释/blog/management/commands/sync_user_avatar.py b/doc/第7周代码注释/blog/management/commands/sync_user_avatar.py
new file mode 100644
index 00000000..d0f46127
--- /dev/null
+++ b/doc/第7周代码注释/blog/management/commands/sync_user_avatar.py
@@ -0,0 +1,47 @@
+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('结束同步')
diff --git a/doc/第7周代码注释/blog/middleware.py b/doc/第7周代码注释/blog/middleware.py
new file mode 100644
index 00000000..b204d7fa
--- /dev/null
+++ b/doc/第7周代码注释/blog/middleware.py
@@ -0,0 +1,79 @@
+# 导入Python内置logging模块,用于记录中间件运行过程中的日志(如错误信息)
+import logging
+# 导入Python内置time模块,用于计算请求处理耗时(页面加载时间)
+import time
+
+# 导入ipware库的get_client_ip函数,用于获取请求客户端的真实IP地址(兼容多种部署场景)
+from ipware import get_client_ip
+# 导入user_agents库的parse函数,用于解析用户代理(UA)字符串,提取浏览器、设备、系统信息
+from user_agents import parse
+
+# 从当前应用的documents模块导入:
+# 1. ELASTICSEARCH_ENABLED:判断项目是否启用Elasticsearch(之前定义的全局变量)
+# 2. ElaspedTimeDocumentManager:性能监控文档管理类(用于将耗时数据存入Elasticsearch)
+from blog.documents import ELASTICSEARCH_ENABLED, ElaspedTimeDocumentManager
+
+# 创建日志记录器,日志名称与当前模块一致(__name__),便于定位日志来源(区分其他模块日志)
+logger = logging.getLogger(__name__)
+
+
+# 定义自定义中间件类OnlineMiddleware,遵循Django中间件接口规范
+# 作用:1. 计算请求处理耗时(页面加载时间);2. 记录访问IP、设备信息到Elasticsearch;3. 替换页面中的加载时间占位符
+class OnlineMiddleware(object):
+ # 中间件初始化方法,接收get_response参数(Django 1.10+中间件必需参数,代表后续中间件/视图的响应流程)
+ def __init__(self, get_response=None):
+ # 保存get_response到实例属性,后续在__call__方法中调用,确保请求流程继续向下执行
+ self.get_response = get_response
+ # 调用父类(object)的初始化方法,确保基础类功能正常(Python 2/3兼容写法)
+ super().__init__()
+
+ # 中间件核心执行方法,处理每个请求的入口和出口(请求到达时执行前半部分,响应返回时执行后半部分)
+ def __call__(self, request):
+ ''' page render time ''' # 注释:该方法用于计算页面渲染耗时
+ # 记录请求开始时间(时间戳,单位:秒),作为耗时计算的起始点
+ start_time = time.time()
+ # 调用后续中间件/视图函数,获取响应对象(response),此时请求已完成业务处理
+ response = self.get_response(request)
+
+ # 从请求的META信息中获取用户代理(UA)字符串:
+ # HTTP_USER_AGENT是请求头中的字段,包含浏览器、设备、系统等信息,默认值为空字符串
+ http_user_agent = request.META.get('HTTP_USER_AGENT', '')
+ # 调用get_client_ip函数获取客户端真实IP:
+ # 返回值为元组(ip地址, 是否为代理IP),此处仅取IP地址(_忽略代理标记)
+ ip, _ = get_client_ip(request)
+ # 解析UA字符串:调用parse函数将原始UA字符串转换为结构化对象(可通过属性获取浏览器/设备/系统信息)
+ user_agent = parse(http_user_agent)
+
+ # 判断响应是否为非流式响应(流式响应如文件下载,无需处理加载时间和替换占位符)
+ if not response.streaming:
+ try:
+ # 计算请求处理总耗时:当前时间 - 开始时间(单位:秒)
+ cast_time = time.time() - start_time
+ # 如果启用了Elasticsearch,将性能数据存入Elasticsearch
+ if ELASTICSEARCH_ENABLED:
+ # 耗时转换为毫秒(保留2位小数),更符合性能监控的常用单位
+ time_taken = round((cast_time) * 1000, 2)
+ # 获取请求的路径(如"/article/1/"),作为性能记录的URL标识
+ url = request.path
+ # 导入Django的timezone模块(延迟导入,避免循环导入问题),用于获取当前时间
+ from django.utils import timezone
+ # 调用ElaspedTimeDocumentManager的create方法,插入性能记录到Elasticsearch:
+ # 包含URL、耗时、记录时间、用户代理信息、IP地址
+ ElaspedTimeDocumentManager.create(
+ url=url,
+ time_taken=time_taken,
+ log_datetime=timezone.now(),
+ useragent=user_agent,
+ ip=ip)
+ # 替换响应内容中的占位符:
+ # 将页面中的''字符串替换为实际耗时(保留前5个字符,如"0.321")
+ # 注意:response.content是字节类型,需用str.encode将字符串耗时转换为字节
+ response.content = response.content.replace(
+ b'', str.encode(str(cast_time)[:5]))
+ # 捕获所有异常,避免中间件报错导致响应失败
+ except Exception as e:
+ # 记录异常日志:将错误信息写入日志,便于后续排查问题(如Elasticsearch连接失败、占位符替换失败)
+ logger.error("Error OnlineMiddleware: %s" % e)
+
+ # 返回处理后的响应对象,最终返回给客户端
+ return response
\ No newline at end of file
diff --git a/doc/第7周代码注释/blog/migrations/0001_initial.py b/doc/第7周代码注释/blog/migrations/0001_initial.py
new file mode 100644
index 00000000..3d391b62
--- /dev/null
+++ b/doc/第7周代码注释/blog/migrations/0001_initial.py
@@ -0,0 +1,137 @@
+# 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',
+ },
+ ),
+ ]
diff --git a/doc/第7周代码注释/blog/migrations/0002_blogsettings_global_footer_and_more.py b/doc/第7周代码注释/blog/migrations/0002_blogsettings_global_footer_and_more.py
new file mode 100644
index 00000000..adbaa36b
--- /dev/null
+++ b/doc/第7周代码注释/blog/migrations/0002_blogsettings_global_footer_and_more.py
@@ -0,0 +1,23 @@
+# 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='公共头部'),
+ ),
+ ]
diff --git a/doc/第7周代码注释/blog/migrations/0003_blogsettings_comment_need_review.py b/doc/第7周代码注释/blog/migrations/0003_blogsettings_comment_need_review.py
new file mode 100644
index 00000000..e9f55024
--- /dev/null
+++ b/doc/第7周代码注释/blog/migrations/0003_blogsettings_comment_need_review.py
@@ -0,0 +1,17 @@
+# 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='评论是否需要审核'),
+ ),
+ ]
diff --git a/doc/第7周代码注释/blog/migrations/0004_rename_analyticscode_blogsettings_analytics_code_and_more.py b/doc/第7周代码注释/blog/migrations/0004_rename_analyticscode_blogsettings_analytics_code_and_more.py
new file mode 100644
index 00000000..ceb13982
--- /dev/null
+++ b/doc/第7周代码注释/blog/migrations/0004_rename_analyticscode_blogsettings_analytics_code_and_more.py
@@ -0,0 +1,27 @@
+# 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',
+ ),
+ ]
diff --git a/doc/第7周代码注释/blog/migrations/0005_alter_article_options_alter_category_options_and_more.py b/doc/第7周代码注释/blog/migrations/0005_alter_article_options_alter_category_options_and_more.py
new file mode 100644
index 00000000..d08e8534
--- /dev/null
+++ b/doc/第7周代码注释/blog/migrations/0005_alter_article_options_alter_category_options_and_more.py
@@ -0,0 +1,300 @@
+# 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'),
+ ),
+ ]
diff --git a/doc/第7周代码注释/blog/migrations/0006_alter_blogsettings_options.py b/doc/第7周代码注释/blog/migrations/0006_alter_blogsettings_options.py
new file mode 100644
index 00000000..e36feb4c
--- /dev/null
+++ b/doc/第7周代码注释/blog/migrations/0006_alter_blogsettings_options.py
@@ -0,0 +1,17 @@
+# 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'},
+ ),
+ ]
diff --git a/doc/第7周代码注释/blog/migrations/__init__.py b/doc/第7周代码注释/blog/migrations/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/doc/第7周代码注释/blog/migrations/__pycache__/0001_initial.cpython-311.pyc b/doc/第7周代码注释/blog/migrations/__pycache__/0001_initial.cpython-311.pyc
new file mode 100644
index 00000000..514d4bca
Binary files /dev/null and b/doc/第7周代码注释/blog/migrations/__pycache__/0001_initial.cpython-311.pyc differ
diff --git a/doc/第7周代码注释/blog/migrations/__pycache__/0002_blogsettings_global_footer_and_more.cpython-311.pyc b/doc/第7周代码注释/blog/migrations/__pycache__/0002_blogsettings_global_footer_and_more.cpython-311.pyc
new file mode 100644
index 00000000..ca008a63
Binary files /dev/null and b/doc/第7周代码注释/blog/migrations/__pycache__/0002_blogsettings_global_footer_and_more.cpython-311.pyc differ
diff --git a/doc/第7周代码注释/blog/migrations/__pycache__/0003_blogsettings_comment_need_review.cpython-311.pyc b/doc/第7周代码注释/blog/migrations/__pycache__/0003_blogsettings_comment_need_review.cpython-311.pyc
new file mode 100644
index 00000000..e9d0dd7b
Binary files /dev/null and b/doc/第7周代码注释/blog/migrations/__pycache__/0003_blogsettings_comment_need_review.cpython-311.pyc differ
diff --git a/doc/第7周代码注释/blog/migrations/__pycache__/0004_rename_analyticscode_blogsettings_analytics_code_and_more.cpython-311.pyc b/doc/第7周代码注释/blog/migrations/__pycache__/0004_rename_analyticscode_blogsettings_analytics_code_and_more.cpython-311.pyc
new file mode 100644
index 00000000..1e0add2b
Binary files /dev/null and b/doc/第7周代码注释/blog/migrations/__pycache__/0004_rename_analyticscode_blogsettings_analytics_code_and_more.cpython-311.pyc differ
diff --git a/doc/第7周代码注释/blog/migrations/__pycache__/0005_alter_article_options_alter_category_options_and_more.cpython-311.pyc b/doc/第7周代码注释/blog/migrations/__pycache__/0005_alter_article_options_alter_category_options_and_more.cpython-311.pyc
new file mode 100644
index 00000000..c3acdc5b
Binary files /dev/null and b/doc/第7周代码注释/blog/migrations/__pycache__/0005_alter_article_options_alter_category_options_and_more.cpython-311.pyc differ
diff --git a/doc/第7周代码注释/blog/migrations/__pycache__/0006_alter_blogsettings_options.cpython-311.pyc b/doc/第7周代码注释/blog/migrations/__pycache__/0006_alter_blogsettings_options.cpython-311.pyc
new file mode 100644
index 00000000..979215a1
Binary files /dev/null and b/doc/第7周代码注释/blog/migrations/__pycache__/0006_alter_blogsettings_options.cpython-311.pyc differ
diff --git a/doc/第7周代码注释/blog/migrations/__pycache__/__init__.cpython-311.pyc b/doc/第7周代码注释/blog/migrations/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 00000000..fe2413f4
Binary files /dev/null and b/doc/第7周代码注释/blog/migrations/__pycache__/__init__.cpython-311.pyc differ
diff --git a/doc/第7周代码注释/blog/models.py b/doc/第7周代码注释/blog/models.py
new file mode 100644
index 00000000..70cc291c
--- /dev/null
+++ b/doc/第7周代码注释/blog/models.py
@@ -0,0 +1,515 @@
+# 导入Python内置logging模块,用于记录模型操作相关日志(如缓存命中/设置、数据验证错误)
+import logging
+# 从abc模块导入abstractmethod装饰器,用于定义抽象方法(强制子类实现)
+from abc import abstractmethod
+
+# 导入Django配置模块,用于获取项目配置(如AUTH_USER_MODEL)
+from django.conf import settings
+# 导入Django数据验证异常类,用于自定义数据验证逻辑(如博客配置唯一性校验)
+from django.core.exceptions import ValidationError
+# 导入Django模型核心模块,用于定义数据模型(对应数据库表)
+from django.db import models
+# 导入Django URL反向解析模块,用于生成模型的绝对URL
+from django.urls import reverse
+# 导入Django时区工具,用于处理时间字段(确保时间戳一致性)
+from django.utils.timezone import now
+# 导入Django国际化翻译工具,用于模型字段/选项的多语言支持
+from django.utils.translation import gettext_lazy as _
+# 导入MDTextField字段(来自mdeditor库),用于支持Markdown格式的富文本编辑
+from mdeditor.fields import MDTextField
+# 导入uuslug库的slugify函数,用于将中文标题/名称转换为URL友好的slug(如"我的博客"→"wo-de-bo-ke")
+from uuslug import slugify
+
+# 从自定义工具模块导入缓存相关工具:
+# 1. cache_decorator:缓存装饰器,用于缓存函数返回结果
+# 2. cache:缓存操作对象,用于直接读写缓存
+from djangoblog.utils import cache_decorator, cache
+# 从自定义工具模块导入获取当前站点信息的函数(用于生成完整URL)
+from djangoblog.utils import get_current_site
+
+# 创建日志记录器,日志名称与当前模块一致(__name__),便于定位日志来源
+logger = logging.getLogger(__name__)
+
+
+# 定义链接显示类型枚举类LinkShowType,继承自Django的TextChoices(枚举基类)
+# 作用:规范友情链接的显示位置选项,避免硬编码字符串
+class LinkShowType(models.TextChoices):
+ I = ('i', _('index')) # 首页显示
+ L = ('l', _('list')) # 列表页显示
+ P = ('p', _('post')) # 文章详情页显示
+ A = ('a', _('all')) # 所有页面显示
+ S = ('s', _('slide')) # 幻灯片区域显示
+
+
+# 定义抽象基础模型类BaseModel,继承自Django的models.Model
+# 作用:封装所有模型共有的字段和方法(如创建时间、修改时间、URL生成),避免代码重复
+# 注:abstract=True(Meta类中)表示该模型为抽象模型,不会生成数据库表,仅用于被子类继承
+class BaseModel(models.Model):
+ # 主键ID:自增整数类型(Django默认主键,此处显式定义以统一规范)
+ id = models.AutoField(primary_key=True)
+ # 创建时间:DateTimeField类型,默认值为当前时间(now()),支持国际化显示(_('creation time'))
+ creation_time = models.DateTimeField(_('creation time'), default=now)
+ # 最后修改时间:DateTimeField类型,默认值为当前时间,用于记录数据更新时间
+ last_modify_time = models.DateTimeField(_('modify time'), default=now)
+
+ # 重写save方法,扩展保存逻辑(处理slug生成和浏览量更新优化)
+ def save(self, *args, **kwargs):
+ # 判断是否为Article模型的浏览量更新操作:
+ # 1. 实例是Article类的实例
+ # 2. save方法传入了update_fields参数
+ # 3. 仅更新views字段(浏览量)
+ is_update_views = isinstance(
+ self,
+ Article) and 'update_fields' in kwargs and kwargs['update_fields'] == ['views']
+ # 如果是浏览量单独更新,直接执行SQL更新(避免触发完整save流程,提升性能)
+ if is_update_views:
+ Article.objects.filter(pk=self.pk).update(views=self.views)
+ # 非浏览量更新场景,执行正常保存逻辑
+ else:
+ # 判断当前模型是否有slug字段(需要生成URL友好标识的模型,如Category、Tag)
+ if 'slug' in self.__dict__:
+ # 确定slug的生成源:优先取title字段(如Article),无则取name字段(如Category、Tag)
+ slug = getattr(
+ self, 'title') if 'title' in self.__dict__ else getattr(
+ self, 'name')
+ # 调用slugify函数生成slug,并赋值给当前实例的slug字段
+ setattr(self, 'slug', slugify(slug))
+ # 调用父类的save方法,完成数据入库(必须调用,否则数据不会保存)
+ super().save(*args, **kwargs)
+
+ # 生成模型实例的完整URL(含域名),用于前端跳转、SEO等场景
+ def get_full_url(self):
+ # 获取当前站点的域名(如"www.example.com",通过get_current_site工具函数)
+ site = get_current_site().domain
+ # 拼接完整URL:协议(默认https)+ 域名 + 实例的相对URL(通过get_absolute_url获取)
+ url = "https://{site}{path}".format(site=site,
+ path=self.get_absolute_url())
+ return url
+
+ # 模型元数据配置
+ class Meta:
+ abstract = True # 标记为抽象模型,不生成数据库表
+
+ # 定义抽象方法get_absolute_url,强制子类实现
+ # 作用:每个具体模型必须提供自己的相对URL生成逻辑(如文章详情页URL、分类页URL)
+ @abstractmethod
+ def get_absolute_url(self):
+ pass
+
+
+# 定义文章模型Article,继承自抽象基础模型BaseModel
+class Article(BaseModel):
+ """文章模型:存储博客文章/页面数据(如博客文章、关于页、联系页等)"""
+ # 文章状态选项:元组形式,每个元素为(存储值,显示文本),支持国际化
+ STATUS_CHOICES = (
+ ('d', _('Draft')), # 'd':草稿状态
+ ('p', _('Published')),# 'p':已发布状态
+ )
+ # 评论状态选项:控制文章是否允许评论
+ COMMENT_STATUS = (
+ ('o', _('Open')), # 'o':开放评论
+ ('c', _('Close')), # 'c':关闭评论
+ )
+ # 文章类型选项:区分普通文章和独立页面
+ TYPE = (
+ ('a', _('Article')), # 'a':普通文章(如博客博文)
+ ('p', _('Page')), # 'p':独立页面(如关于页、隐私政策页)
+ )
+ # 文章标题:CharField类型,最大长度200,唯一约束(避免重复标题)
+ title = models.CharField(_('title'), max_length=200, unique=True)
+ # 文章内容:MDTextField类型,支持Markdown格式编辑(富文本)
+ body = MDTextField(_('body'))
+ # 发布时间:DateTimeField类型,必填,默认值为当前时间,用于控制文章发布时间点
+ pub_time = models.DateTimeField(
+ _('publish time'), blank=False, null=False, default=now)
+ # 文章状态:CharField类型,长度1,可选值为STATUS_CHOICES,默认已发布('p')
+ status = models.CharField(
+ _('status'),
+ max_length=1,
+ choices=STATUS_CHOICES,
+ default='p')
+ # 评论状态:CharField类型,长度1,可选值为COMMENT_STATUS,默认开放评论('o')
+ comment_status = models.CharField(
+ _('comment status'),
+ max_length=1,
+ choices=COMMENT_STATUS,
+ default='o')
+ # 文章类型:CharField类型,长度1,可选值为TYPE,默认普通文章('a')
+ type = models.CharField(_('type'), max_length=1, choices=TYPE, default='a')
+ # 浏览量:PositiveIntegerField类型(仅允许非负整数),默认0
+ views = models.PositiveIntegerField(_('views'), default=0)
+ # 作者:外键关联Django用户模型(settings.AUTH_USER_MODEL,兼容自定义用户模型)
+ # on_delete=models.CASCADE:用户被删除时,关联的文章也会被删除
+ author = models.ForeignKey(
+ settings.AUTH_USER_MODEL,
+ verbose_name=_('author'),
+ blank=False,
+ null=False,
+ on_delete=models.CASCADE)
+ # 文章排序权重:IntegerField类型,默认0,用于自定义文章显示顺序(值越大越靠前)
+ article_order = models.IntegerField(
+ _('order'), blank=False, null=False, default=0)
+ # 是否显示目录:BooleanField类型,默认False,控制文章详情页是否显示TOC(目录)
+ show_toc = models.BooleanField(_('show toc'), blank=False, null=False, default=False)
+ # 分类:外键关联Category模型(自应用内的分类模型)
+ # on_delete=models.CASCADE:分类被删除时,关联的文章也会被删除
+ category = models.ForeignKey(
+ 'Category',
+ verbose_name=_('category'),
+ on_delete=models.CASCADE,
+ blank=False,
+ null=False)
+ # 标签:多对多关联Tag模型(一篇文章可多个标签,一个标签可关联多篇文章),允许为空
+ tags = models.ManyToManyField('Tag', verbose_name=_('tag'), blank=True)
+
+ # 辅助方法:返回文章内容字符串(用于需要直接获取纯文本内容的场景)
+ def body_to_string(self):
+ return self.body
+
+ # 重写__str__方法,后台管理界面和打印实例时显示文章标题(友好显示)
+ def __str__(self):
+ return self.title
+
+ # 模型元数据配置
+ class Meta:
+ ordering = ['-article_order', '-pub_time'] # 默认排序:先按排序权重降序,再按发布时间降序
+ verbose_name = _('article') # 模型单数显示名称(支持国际化)
+ verbose_name_plural = verbose_name # 模型复数显示名称(与单数一致)
+ get_latest_by = 'id' # 指定获取最新记录的字段(按ID降序)
+
+ # 实现抽象基类的get_absolute_url方法:生成文章的相对URL
+ def get_absolute_url(self):
+ # 反向解析'blog:detailbyid'路由,传递文章ID、发布年月日作为URL参数
+ return reverse('blog:detailbyid', kwargs={
+ 'article_id': self.id,
+ 'year': self.creation_time.year,
+ 'month': self.creation_time.month,
+ 'day': self.creation_time.day
+ })
+
+ # 缓存装饰器:缓存结果10小时(60*60*10秒),避免重复查询数据库
+ @cache_decorator(60 * 60 * 10)
+ # 获取文章分类的层级关系(如"技术→Python→Django")
+ def get_category_tree(self):
+ # 调用分类模型的get_category_tree方法,获取当前文章分类的所有父级分类
+ tree = self.category.get_category_tree()
+ # 转换为(分类名称,分类URL)的元组列表,用于前端显示分类面包屑
+ names = list(map(lambda c: (c.name, c.get_absolute_url()), tree))
+ return names
+
+ # 重写save方法(此处仅调用父类方法,便于后续扩展自定义逻辑)
+ def save(self, *args, **kwargs):
+ super().save(*args, **kwargs)
+
+ # 文章浏览量递增方法:用于文章详情页访问时更新浏览量
+ def viewed(self):
+ self.views += 1 # 浏览量+1
+ # 仅更新views字段(通过update_fields参数优化,避免更新其他字段)
+ self.save(update_fields=['views'])
+
+ # 获取文章的评论列表(已启用的评论)
+ def comment_list(self):
+ # 定义缓存键:包含文章ID,确保不同文章的评论缓存不冲突
+ cache_key = 'article_comments_{id}'.format(id=self.id)
+ # 尝试从缓存获取评论列表
+ value = cache.get(cache_key)
+ if value:
+ # 缓存命中:记录日志,直接返回缓存的评论列表
+ logger.info('get article comments:{id}'.format(id=self.id))
+ return value
+ else:
+ # 缓存未命中:查询当前文章的已启用评论(按ID降序,最新评论在前)
+ comments = self.comment_set.filter(is_enable=True).order_by('-id')
+ # 将评论列表存入缓存,有效期100分钟(60*100秒)
+ cache.set(cache_key, comments, 60 * 100)
+ # 记录日志:缓存设置成功
+ logger.info('set article comments:{id}'.format(id=self.id))
+ return comments
+
+ # 生成文章在Django后台的编辑页URL(用于快速跳转到后台编辑)
+ def get_admin_url(self):
+ # 获取模型的元数据:(应用名,模型名)
+ info = (self._meta.app_label, self._meta.model_name)
+ # 反向解析admin的模型修改路由,传递文章主键
+ return reverse('admin:%s_%s_change' % info, args=(self.pk,))
+
+ # 缓存装饰器:缓存结果100分钟
+ @cache_decorator(expiration=60 * 100)
+ # 获取当前文章的下一篇文章(已发布状态,ID大于当前文章)
+ def next_article(self):
+ return Article.objects.filter(
+ id__gt=self.id, status='p').order_by('id').first()
+
+ # 缓存装饰器:缓存结果100分钟
+ @cache_decorator(expiration=60 * 100)
+ # 获取当前文章的前一篇文章(已发布状态,ID小于当前文章)
+ def prev_article(self):
+ return Article.objects.filter(id__lt=self.id, status='p').first()
+
+
+# 定义分类模型Category,继承自抽象基础模型BaseModel
+class Category(BaseModel):
+ """文章分类模型:存储博客文章的分类数据(支持层级分类,如父分类→子分类)"""
+ # 分类名称:CharField类型,最大长度30,唯一约束(避免重复分类名)
+ name = models.CharField(_('category name'), max_length=30, unique=True)
+ # 父分类:自关联外键(分类可作为其他分类的父分类),允许为空(顶级分类)
+ # on_delete=models.CASCADE:父分类被删除时,子分类也会被删除
+ parent_category = models.ForeignKey(
+ 'self',
+ verbose_name=_('parent category'),
+ blank=True,
+ null=True,
+ on_delete=models.CASCADE)
+ # 分类slug:URL友好标识,默认值'no-slug',用于生成分类页URL
+ slug = models.SlugField(default='no-slug', max_length=60, blank=True)
+ # 排序索引:IntegerField类型,默认0,用于控制分类显示顺序(值越大越靠前)
+ index = models.IntegerField(default=0, verbose_name=_('index'))
+
+ # 模型元数据配置
+ class Meta:
+ ordering = ['-index'] # 默认排序:按排序索引降序
+ verbose_name = _('category') # 模型单数显示名称
+ verbose_name_plural = verbose_name # 模型复数显示名称
+
+ # 实现抽象基类的get_absolute_url方法:生成分类的相对URL
+ def get_absolute_url(self):
+ # 反向解析'blog:category_detail'路由,传递分类slug作为参数
+ return reverse(
+ 'blog:category_detail', kwargs={
+ 'category_name': self.slug})
+
+ # 重写__str__方法,友好显示分类名称
+ def __str__(self):
+ return self.name
+
+ # 缓存装饰器:缓存结果10小时
+ @cache_decorator(60 * 60 * 10)
+ # 递归获取当前分类的所有父级分类(生成分类层级树,如子分类→父分类→顶级分类)
+ def get_category_tree(self):
+ """
+ 递归获得分类目录的父级
+ :return: 分类实例列表(当前分类 + 所有父级分类)
+ """
+ categorys = []
+
+ # 内部递归函数:解析分类的父级
+ def parse(category):
+ categorys.append(category) # 将当前分类加入列表
+ if category.parent_category: # 如果存在父分类,继续递归
+ parse(category.parent_category)
+
+ parse(self) # 从当前分类开始解析
+ return categorys
+
+ # 缓存装饰器:缓存结果10小时
+ @cache_decorator(60 * 60 * 10)
+ # 递归获取当前分类的所有子级分类(包括子分类的子分类)
+ def get_sub_categorys(self):
+ """
+ 获得当前分类目录所有子集
+ :return: 分类实例列表(当前分类 + 所有子级分类)
+ """
+ categorys = []
+ all_categorys = Category.objects.all() # 获取所有分类
+
+ # 内部递归函数:解析分类的子级
+ def parse(category):
+ if category not in categorys: # 避免重复添加(防止循环引用)
+ categorys.append(category)
+ # 查询当前分类的直接子分类
+ childs = all_categorys.filter(parent_category=category)
+ for child in childs: # 遍历子分类,递归解析
+ if category not in categorys:
+ categorys.append(child)
+ parse(child)
+
+ parse(self) # 从当前分类开始解析
+ return categorys
+
+
+# 定义标签模型Tag,继承自抽象基础模型BaseModel
+class Tag(BaseModel):
+ """文章标签模型:存储博客文章的标签数据(用于文章分类和搜索)"""
+ # 标签名称:CharField类型,最大长度30,唯一约束(避免重复标签名)
+ name = models.CharField(_('tag name'), max_length=30, unique=True)
+ # 标签slug:URL友好标识,默认值'no-slug',用于生成标签页URL
+ slug = models.SlugField(default='no-slug', max_length=60, blank=True)
+
+ # 重写__str__方法,友好显示标签名称
+ def __str__(self):
+ return self.name
+
+ # 实现抽象基类的get_absolute_url方法:生成标签的相对URL
+ def get_absolute_url(self):
+ # 反向解析'blog:tag_detail'路由,传递标签slug作为参数
+ return reverse('blog:tag_detail', kwargs={'tag_name': self.slug})
+
+ # 缓存装饰器:缓存结果10小时
+ @cache_decorator(60 * 60 * 10)
+ # 获取当前标签关联的文章数量(去重,避免重复计数)
+ def get_article_count(self):
+ return Article.objects.filter(tags__name=self.name).distinct().count()
+
+ # 模型元数据配置
+ class Meta:
+ ordering = ['name'] # 默认排序:按标签名称升序
+ verbose_name = _('tag') # 模型单数显示名称
+ verbose_name_plural = verbose_name # 模型复数显示名称
+
+
+# 定义友情链接模型Links(未继承BaseModel,单独定义时间字段)
+class Links(models.Model):
+ """友情链接模型:存储博客的友情链接数据"""
+ # 链接名称:CharField类型,最大长度30,唯一约束(避免重复链接名)
+ name = models.CharField(_('link name'), max_length=30, unique=True)
+ # 链接URL:URLField类型,自动验证URL格式(如http://、https://)
+ link = models.URLField(_('link'))
+ # 排序序号:IntegerField类型,唯一约束(控制友情链接显示顺序,值越小越靠前)
+ sequence = models.IntegerField(_('order'), unique=True)
+ # 是否启用:BooleanField类型,默认True,控制是否在前端显示该链接
+ is_enable = models.BooleanField(
+ _('is show'), default=True, blank=False, null=False)
+ # 显示位置:CharField类型,长度1,可选值为LinkShowType枚举,默认首页显示('i')
+ show_type = models.CharField(
+ _('show type'),
+ max_length=1,
+ choices=LinkShowType.choices,
+ default=LinkShowType.I)
+ # 创建时间:DateTimeField类型,默认当前时间
+ creation_time = models.DateTimeField(_('creation time'), default=now)
+ # 最后修改时间:DateTimeField类型,默认当前时间
+ last_mod_time = models.DateTimeField(_('modify time'), default=now)
+
+ # 模型元数据配置
+ class Meta:
+ ordering = ['sequence'] # 默认排序:按排序序号升序
+ verbose_name = _('link') # 模型单数显示名称
+ verbose_name_plural = verbose_name # 模型复数显示名称
+
+ # 重写__str__方法,友好显示链接名称
+ def __str__(self):
+ return self.name
+
+
+# 定义侧边栏模型SideBar(未继承BaseModel,单独定义时间字段)
+class SideBar(models.Model):
+ """侧边栏模型:存储博客侧边栏内容(支持自定义HTML内容,如公告、广告)"""
+ # 侧边栏标题:CharField类型,最大长度100
+ name = models.CharField(_('title'), max_length=100)
+ # 侧边栏内容:TextField类型,支持HTML文本(如公告、推荐文章列表)
+ content = models.TextField(_('content'))
+ # 排序序号:IntegerField类型,唯一约束(控制侧边栏显示顺序,值越小越靠前)
+ sequence = models.IntegerField(_('order'), unique=True)
+ # 是否启用:BooleanField类型,默认True,控制是否在前端显示该侧边栏
+ is_enable = models.BooleanField(_('is enable'), default=True)
+ # 创建时间:DateTimeField类型,默认当前时间
+ creation_time = models.DateTimeField(_('creation time'), default=now)
+ # 最后修改时间:DateTimeField类型,默认当前时间
+ last_mod_time = models.DateTimeField(_('modify time'), default=now)
+
+ # 模型元数据配置
+ class Meta:
+ ordering = ['sequence'] # 默认排序:按排序序号升序
+ verbose_name = _('sidebar') # 模型单数显示名称
+ verbose_name_plural = verbose_name # 模型复数显示名称
+
+ # 重写__str__方法,友好显示侧边栏标题
+ def __str__(self):
+ return self.name
+
+
+# 定义博客配置模型BlogSettings
+class BlogSettings(models.Model):
+ """博客全局配置模型:存储博客的全局设置(如站点名称、SEO信息、备案号等)"""
+ # 站点名称:CharField类型,必填,默认空字符串
+ site_name = models.CharField(
+ _('site name'),
+ max_length=200,
+ null=False,
+ blank=False,
+ default='')
+ # 站点描述:TextField类型,必填,用于前端显示站点简介(如首页底部)
+ site_description = models.TextField(
+ _('site description'),
+ max_length=1000,
+ null=False,
+ blank=False,
+ default='')
+ # 站点SEO描述:TextField类型,必填,用于网页meta标签的description(提升搜索引擎排名)
+ site_seo_description = models.TextField(
+ _('site seo description'), max_length=1000, null=False, blank=False, default='')
+ # 站点关键词:TextField类型,必填,用于网页meta标签的keywords(提升搜索引擎排名)
+ site_keywords = models.TextField(
+ _('site keywords'),
+ max_length=1000,
+ null=False,
+ blank=False,
+ default='')
+ # 文章摘要长度:IntegerField类型,默认300,控制前端显示文章摘要的字符数
+ article_sub_length = models.IntegerField(_('article sub length'), default=300)
+ # 侧边栏文章数量:IntegerField类型,默认10,控制侧边栏显示的最新/热门文章数量
+ sidebar_article_count = models.IntegerField(_('sidebar article count'), default=10)
+ # 侧边栏评论数量:IntegerField类型,默认5,控制侧边栏显示的最新评论数量
+ sidebar_comment_count = models.IntegerField(_('sidebar comment count'), default=5)
+ # 文章页评论数量:IntegerField类型,默认5,控制文章详情页默认显示的评论数量
+ article_comment_count = models.IntegerField(_('article comment count'), default=5)
+ # 是否显示谷歌广告:BooleanField类型,默认False,控制是否在前端显示谷歌广告
+ show_google_adsense = models.BooleanField(_('show adsense'), default=False)
+ # 谷歌广告代码:TextField类型,可选,存储谷歌广告的HTML代码
+ google_adsense_codes = models.TextField(
+ _('adsense code'), max_length=2000, null=True, blank=True, default='')
+ # 是否开放全站评论:BooleanField类型,默认True,控制整个站点是否允许评论
+ open_site_comment = models.BooleanField(_('open site comment'), default=True)
+ # 公共头部代码:TextField类型,可选,存储全局头部的自定义HTML(如额外CSS、JS)
+ global_header = models.TextField("公共头部", null=True, blank=True, default='')
+ # 公共尾部代码:TextField类型,可选,存储全局尾部的自定义HTML(如备案信息、统计代码)
+ global_footer = models.TextField("公共尾部", null=True, blank=True, default='')
+ # 备案号:CharField类型,可选,存储网站ICP备案号(如"粤ICP备xxxx号")
+ beian_code = models.CharField(
+ '备案号',
+ max_length=2000,
+ null=True,
+ blank=True,
+ default='')
+ # 网站统计代码:TextField类型,必填,存储统计工具的JS代码(如百度统计、谷歌分析)
+ analytics_code = models.TextField(
+ "网站统计代码",
+ max_length=1000,
+ null=False,
+ blank=False,
+ default='')
+ # 是否显示公安备案号:BooleanField类型,默认False,控制是否显示公安备案信息
+ show_gongan_code = models.BooleanField(
+ '是否显示公安备案号', default=False, null=False)
+ # 公安备案号:TextField类型,可选,存储公安备案号(如"粤公网安备xxxx号")
+ gongan_beiancode = models.TextField(
+ '公安备案号',
+ max_length=2000,
+ null=True,
+ blank=True,
+ default='')
+ # 评论是否需要审核:BooleanField类型,默认False,控制用户提交的评论是否需管理员审核后显示
+ comment_need_review = models.BooleanField(
+ '评论是否需要审核', default=False, null=False)
+
+ # 模型元数据配置
+ class Meta:
+ verbose_name = _('Website configuration') # 模型单数显示名称(网站配置)
+ verbose_name_plural = verbose_name # 模型复数显示名称
+
+ # 重写__str__方法,友好显示站点名称
+ def __str__(self):
+ return self.site_name
+
+ # 自定义数据验证方法:确保博客配置只能有一条记录(全局唯一配置)
+ def clean(self):
+ # 排除当前实例ID后,查询是否已有其他配置记录
+ if BlogSettings.objects.exclude(id=self.id).count():
+ # 若存在其他记录,抛出验证错误(阻止保存)
+ raise ValidationError(_('There can only be one configuration'))
+
+ # 重写save方法:保存配置后清空缓存(确保前端能立即获取最新配置)
+ def save(self, *args, **kwargs):
+ super().save(*args, **kwargs) # 调用父类save方法完成数据入库
+ from djangoblog.utils import cache # 延迟导入缓存模块,避免循环导入
+ cache.clear() # 清空所有缓存
\ No newline at end of file
diff --git a/doc/第7周代码注释/blog/search_indexes.py b/doc/第7周代码注释/blog/search_indexes.py
new file mode 100644
index 00000000..c9a10438
--- /dev/null
+++ b/doc/第7周代码注释/blog/search_indexes.py
@@ -0,0 +1,28 @@
+# 从Haystack库导入索引相关核心类:
+# 1. SearchIndex:搜索索引基类,定义搜索索引的核心结构(如搜索字段)
+# 2. Indexable:索引可访问性基类,要求子类实现get_model方法(指定关联的Django模型)
+from haystack import indexes
+
+# 从当前应用(blog)的models.py导入Article模型,用于将文章数据同步到搜索索引
+from blog.models import Article
+
+
+# 定义文章搜索索引类ArticleIndex,继承自SearchIndex(搜索索引核心)和Indexable(索引关联模型)
+# 作用:告诉Haystack如何构建Article模型的搜索索引,指定搜索字段、关联模型及索引数据范围
+class ArticleIndex(indexes.SearchIndex, indexes.Indexable):
+ # 定义核心搜索字段text:
+ # - document=True:标记该字段为Haystack的"文档字段"(全文搜索的核心字段,所有搜索都会基于该字段匹配)
+ # - use_template=True:指定使用模板来构建该字段的搜索内容(模板路径默认是 templates/search/indexes/blog/article_text.txt)
+ # 模板中可包含文章标题、正文、标签等需要被搜索的字段,Haystack会将这些内容整合为text字段用于搜索
+ text = indexes.CharField(document=True, use_template=True)
+
+ # 实现Indexable基类的强制方法:指定当前索引关联的Django模型
+ def get_model(self):
+ # 返回Article模型,告诉Haystack该索引是为Article模型构建的
+ return Article
+
+ # 定义索引查询集:指定哪些Article数据需要被纳入搜索索引
+ def index_queryset(self, using=None):
+ # using参数:指定使用的搜索引擎(如Elasticsearch、Whoosh),默认None使用配置的默认引擎
+ # 过滤条件:仅将状态为"已发布"(status='p')的文章纳入索引,草稿文章不参与搜索
+ return self.get_model().objects.filter(status='p')
\ No newline at end of file
diff --git a/doc/第7周代码注释/blog/static/account/css/account.css b/doc/第7周代码注释/blog/static/account/css/account.css
new file mode 100644
index 00000000..7d4cec79
--- /dev/null
+++ b/doc/第7周代码注释/blog/static/account/css/account.css
@@ -0,0 +1,9 @@
+.button {
+ border: none;
+ padding: 4px 80px;
+ text-align: center;
+ text-decoration: none;
+ display: inline-block;
+ font-size: 16px;
+ margin: 4px 2px;
+}
\ No newline at end of file
diff --git a/doc/第7周代码注释/blog/static/account/js/account.js b/doc/第7周代码注释/blog/static/account/js/account.js
new file mode 100644
index 00000000..f1a8771e
--- /dev/null
+++ b/doc/第7周代码注释/blog/static/account/js/account.js
@@ -0,0 +1,47 @@
+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("")
+ return
+ }
+ myErr.remove()
+ time(ts)
+ },
+ error: function (e) {
+ alert("发送失败,请重试")
+ }
+ }
+ );
+}
diff --git a/doc/第7周代码注释/blog/static/assets/css/bootstrap.min.css b/doc/第7周代码注释/blog/static/assets/css/bootstrap.min.css
new file mode 100644
index 00000000..ed3905e0
--- /dev/null
+++ b/doc/第7周代码注释/blog/static/assets/css/bootstrap.min.css
@@ -0,0 +1,6 @@
+/*!
+ * Bootstrap v3.3.7 (http://getbootstrap.com)
+ * Copyright 2011-2016 Twitter, Inc.
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
+ *//*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{margin:.67em 0;font-size:2em}mark{color:#000;background:#ff0}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{height:0;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{margin:0;font:inherit;color:inherit}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0}input{line-height:normal}input[type=checkbox],input[type=radio]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;-webkit-appearance:textfield}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{padding:.35em .625em .75em;margin:0 2px;border:1px solid silver}legend{padding:0;border:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-spacing:0;border-collapse:collapse}td,th{padding:0}/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */@media print{*,:after,:before{color:#000!important;text-shadow:none!important;background:0 0!important;-webkit-box-shadow:none!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="javascript:"]:after,a[href^="#"]:after{content:""}blockquote,pre{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}img{max-width:100%!important}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}.navbar{display:none}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000!important}.label{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #ddd!important}}@font-face{font-family:'Glyphicons Halflings';src:url(../fonts/glyphicons-halflings-regular.eot);src:url(../fonts/glyphicons-halflings-regular.eot?#iefix) format('embedded-opentype'),url(../fonts/glyphicons-halflings-regular.woff2) format('woff2'),url(../fonts/glyphicons-halflings-regular.woff) format('woff'),url(../fonts/glyphicons-halflings-regular.ttf) format('truetype'),url(../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular) format('svg')}.glyphicon{position:relative;top:1px;display:inline-block;font-family:'Glyphicons Halflings';font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.glyphicon-asterisk:before{content:"\002a"}.glyphicon-plus:before{content:"\002b"}.glyphicon-eur:before,.glyphicon-euro:before{content:"\20ac"}.glyphicon-minus:before{content:"\2212"}.glyphicon-cloud:before{content:"\2601"}.glyphicon-envelope:before{content:"\2709"}.glyphicon-pencil:before{content:"\270f"}.glyphicon-glass:before{content:"\e001"}.glyphicon-music:before{content:"\e002"}.glyphicon-search:before{content:"\e003"}.glyphicon-heart:before{content:"\e005"}.glyphicon-star:before{content:"\e006"}.glyphicon-star-empty:before{content:"\e007"}.glyphicon-user:before{content:"\e008"}.glyphicon-film:before{content:"\e009"}.glyphicon-th-large:before{content:"\e010"}.glyphicon-th:before{content:"\e011"}.glyphicon-th-list:before{content:"\e012"}.glyphicon-ok:before{content:"\e013"}.glyphicon-remove:before{content:"\e014"}.glyphicon-zoom-in:before{content:"\e015"}.glyphicon-zoom-out:before{content:"\e016"}.glyphicon-off:before{content:"\e017"}.glyphicon-signal:before{content:"\e018"}.glyphicon-cog:before{content:"\e019"}.glyphicon-trash:before{content:"\e020"}.glyphicon-home:before{content:"\e021"}.glyphicon-file:before{content:"\e022"}.glyphicon-time:before{content:"\e023"}.glyphicon-road:before{content:"\e024"}.glyphicon-download-alt:before{content:"\e025"}.glyphicon-download:before{content:"\e026"}.glyphicon-upload:before{content:"\e027"}.glyphicon-inbox:before{content:"\e028"}.glyphicon-play-circle:before{content:"\e029"}.glyphicon-repeat:before{content:"\e030"}.glyphicon-refresh:before{content:"\e031"}.glyphicon-list-alt:before{content:"\e032"}.glyphicon-lock:before{content:"\e033"}.glyphicon-flag:before{content:"\e034"}.glyphicon-headphones:before{content:"\e035"}.glyphicon-volume-off:before{content:"\e036"}.glyphicon-volume-down:before{content:"\e037"}.glyphicon-volume-up:before{content:"\e038"}.glyphicon-qrcode:before{content:"\e039"}.glyphicon-barcode:before{content:"\e040"}.glyphicon-tag:before{content:"\e041"}.glyphicon-tags:before{content:"\e042"}.glyphicon-book:before{content:"\e043"}.glyphicon-bookmark:before{content:"\e044"}.glyphicon-print:before{content:"\e045"}.glyphicon-camera:before{content:"\e046"}.glyphicon-font:before{content:"\e047"}.glyphicon-bold:before{content:"\e048"}.glyphicon-italic:before{content:"\e049"}.glyphicon-text-height:before{content:"\e050"}.glyphicon-text-width:before{content:"\e051"}.glyphicon-align-left:before{content:"\e052"}.glyphicon-align-center:before{content:"\e053"}.glyphicon-align-right:before{content:"\e054"}.glyphicon-align-justify:before{content:"\e055"}.glyphicon-list:before{content:"\e056"}.glyphicon-indent-left:before{content:"\e057"}.glyphicon-indent-right:before{content:"\e058"}.glyphicon-facetime-video:before{content:"\e059"}.glyphicon-picture:before{content:"\e060"}.glyphicon-map-marker:before{content:"\e062"}.glyphicon-adjust:before{content:"\e063"}.glyphicon-tint:before{content:"\e064"}.glyphicon-edit:before{content:"\e065"}.glyphicon-share:before{content:"\e066"}.glyphicon-check:before{content:"\e067"}.glyphicon-move:before{content:"\e068"}.glyphicon-step-backward:before{content:"\e069"}.glyphicon-fast-backward:before{content:"\e070"}.glyphicon-backward:before{content:"\e071"}.glyphicon-play:before{content:"\e072"}.glyphicon-pause:before{content:"\e073"}.glyphicon-stop:before{content:"\e074"}.glyphicon-forward:before{content:"\e075"}.glyphicon-fast-forward:before{content:"\e076"}.glyphicon-step-forward:before{content:"\e077"}.glyphicon-eject:before{content:"\e078"}.glyphicon-chevron-left:before{content:"\e079"}.glyphicon-chevron-right:before{content:"\e080"}.glyphicon-plus-sign:before{content:"\e081"}.glyphicon-minus-sign:before{content:"\e082"}.glyphicon-remove-sign:before{content:"\e083"}.glyphicon-ok-sign:before{content:"\e084"}.glyphicon-question-sign:before{content:"\e085"}.glyphicon-info-sign:before{content:"\e086"}.glyphicon-screenshot:before{content:"\e087"}.glyphicon-remove-circle:before{content:"\e088"}.glyphicon-ok-circle:before{content:"\e089"}.glyphicon-ban-circle:before{content:"\e090"}.glyphicon-arrow-left:before{content:"\e091"}.glyphicon-arrow-right:before{content:"\e092"}.glyphicon-arrow-up:before{content:"\e093"}.glyphicon-arrow-down:before{content:"\e094"}.glyphicon-share-alt:before{content:"\e095"}.glyphicon-resize-full:before{content:"\e096"}.glyphicon-resize-small:before{content:"\e097"}.glyphicon-exclamation-sign:before{content:"\e101"}.glyphicon-gift:before{content:"\e102"}.glyphicon-leaf:before{content:"\e103"}.glyphicon-fire:before{content:"\e104"}.glyphicon-eye-open:before{content:"\e105"}.glyphicon-eye-close:before{content:"\e106"}.glyphicon-warning-sign:before{content:"\e107"}.glyphicon-plane:before{content:"\e108"}.glyphicon-calendar:before{content:"\e109"}.glyphicon-random:before{content:"\e110"}.glyphicon-comment:before{content:"\e111"}.glyphicon-magnet:before{content:"\e112"}.glyphicon-chevron-up:before{content:"\e113"}.glyphicon-chevron-down:before{content:"\e114"}.glyphicon-retweet:before{content:"\e115"}.glyphicon-shopping-cart:before{content:"\e116"}.glyphicon-folder-close:before{content:"\e117"}.glyphicon-folder-open:before{content:"\e118"}.glyphicon-resize-vertical:before{content:"\e119"}.glyphicon-resize-horizontal:before{content:"\e120"}.glyphicon-hdd:before{content:"\e121"}.glyphicon-bullhorn:before{content:"\e122"}.glyphicon-bell:before{content:"\e123"}.glyphicon-certificate:before{content:"\e124"}.glyphicon-thumbs-up:before{content:"\e125"}.glyphicon-thumbs-down:before{content:"\e126"}.glyphicon-hand-right:before{content:"\e127"}.glyphicon-hand-left:before{content:"\e128"}.glyphicon-hand-up:before{content:"\e129"}.glyphicon-hand-down:before{content:"\e130"}.glyphicon-circle-arrow-right:before{content:"\e131"}.glyphicon-circle-arrow-left:before{content:"\e132"}.glyphicon-circle-arrow-up:before{content:"\e133"}.glyphicon-circle-arrow-down:before{content:"\e134"}.glyphicon-globe:before{content:"\e135"}.glyphicon-wrench:before{content:"\e136"}.glyphicon-tasks:before{content:"\e137"}.glyphicon-filter:before{content:"\e138"}.glyphicon-briefcase:before{content:"\e139"}.glyphicon-fullscreen:before{content:"\e140"}.glyphicon-dashboard:before{content:"\e141"}.glyphicon-paperclip:before{content:"\e142"}.glyphicon-heart-empty:before{content:"\e143"}.glyphicon-link:before{content:"\e144"}.glyphicon-phone:before{content:"\e145"}.glyphicon-pushpin:before{content:"\e146"}.glyphicon-usd:before{content:"\e148"}.glyphicon-gbp:before{content:"\e149"}.glyphicon-sort:before{content:"\e150"}.glyphicon-sort-by-alphabet:before{content:"\e151"}.glyphicon-sort-by-alphabet-alt:before{content:"\e152"}.glyphicon-sort-by-order:before{content:"\e153"}.glyphicon-sort-by-order-alt:before{content:"\e154"}.glyphicon-sort-by-attributes:before{content:"\e155"}.glyphicon-sort-by-attributes-alt:before{content:"\e156"}.glyphicon-unchecked:before{content:"\e157"}.glyphicon-expand:before{content:"\e158"}.glyphicon-collapse-down:before{content:"\e159"}.glyphicon-collapse-up:before{content:"\e160"}.glyphicon-log-in:before{content:"\e161"}.glyphicon-flash:before{content:"\e162"}.glyphicon-log-out:before{content:"\e163"}.glyphicon-new-window:before{content:"\e164"}.glyphicon-record:before{content:"\e165"}.glyphicon-save:before{content:"\e166"}.glyphicon-open:before{content:"\e167"}.glyphicon-saved:before{content:"\e168"}.glyphicon-import:before{content:"\e169"}.glyphicon-export:before{content:"\e170"}.glyphicon-send:before{content:"\e171"}.glyphicon-floppy-disk:before{content:"\e172"}.glyphicon-floppy-saved:before{content:"\e173"}.glyphicon-floppy-remove:before{content:"\e174"}.glyphicon-floppy-save:before{content:"\e175"}.glyphicon-floppy-open:before{content:"\e176"}.glyphicon-credit-card:before{content:"\e177"}.glyphicon-transfer:before{content:"\e178"}.glyphicon-cutlery:before{content:"\e179"}.glyphicon-header:before{content:"\e180"}.glyphicon-compressed:before{content:"\e181"}.glyphicon-earphone:before{content:"\e182"}.glyphicon-phone-alt:before{content:"\e183"}.glyphicon-tower:before{content:"\e184"}.glyphicon-stats:before{content:"\e185"}.glyphicon-sd-video:before{content:"\e186"}.glyphicon-hd-video:before{content:"\e187"}.glyphicon-subtitles:before{content:"\e188"}.glyphicon-sound-stereo:before{content:"\e189"}.glyphicon-sound-dolby:before{content:"\e190"}.glyphicon-sound-5-1:before{content:"\e191"}.glyphicon-sound-6-1:before{content:"\e192"}.glyphicon-sound-7-1:before{content:"\e193"}.glyphicon-copyright-mark:before{content:"\e194"}.glyphicon-registration-mark:before{content:"\e195"}.glyphicon-cloud-download:before{content:"\e197"}.glyphicon-cloud-upload:before{content:"\e198"}.glyphicon-tree-conifer:before{content:"\e199"}.glyphicon-tree-deciduous:before{content:"\e200"}.glyphicon-cd:before{content:"\e201"}.glyphicon-save-file:before{content:"\e202"}.glyphicon-open-file:before{content:"\e203"}.glyphicon-level-up:before{content:"\e204"}.glyphicon-copy:before{content:"\e205"}.glyphicon-paste:before{content:"\e206"}.glyphicon-alert:before{content:"\e209"}.glyphicon-equalizer:before{content:"\e210"}.glyphicon-king:before{content:"\e211"}.glyphicon-queen:before{content:"\e212"}.glyphicon-pawn:before{content:"\e213"}.glyphicon-bishop:before{content:"\e214"}.glyphicon-knight:before{content:"\e215"}.glyphicon-baby-formula:before{content:"\e216"}.glyphicon-tent:before{content:"\26fa"}.glyphicon-blackboard:before{content:"\e218"}.glyphicon-bed:before{content:"\e219"}.glyphicon-apple:before{content:"\f8ff"}.glyphicon-erase:before{content:"\e221"}.glyphicon-hourglass:before{content:"\231b"}.glyphicon-lamp:before{content:"\e223"}.glyphicon-duplicate:before{content:"\e224"}.glyphicon-piggy-bank:before{content:"\e225"}.glyphicon-scissors:before{content:"\e226"}.glyphicon-bitcoin:before{content:"\e227"}.glyphicon-btc:before{content:"\e227"}.glyphicon-xbt:before{content:"\e227"}.glyphicon-yen:before{content:"\00a5"}.glyphicon-jpy:before{content:"\00a5"}.glyphicon-ruble:before{content:"\20bd"}.glyphicon-rub:before{content:"\20bd"}.glyphicon-scale:before{content:"\e230"}.glyphicon-ice-lolly:before{content:"\e231"}.glyphicon-ice-lolly-tasted:before{content:"\e232"}.glyphicon-education:before{content:"\e233"}.glyphicon-option-horizontal:before{content:"\e234"}.glyphicon-option-vertical:before{content:"\e235"}.glyphicon-menu-hamburger:before{content:"\e236"}.glyphicon-modal-window:before{content:"\e237"}.glyphicon-oil:before{content:"\e238"}.glyphicon-grain:before{content:"\e239"}.glyphicon-sunglasses:before{content:"\e240"}.glyphicon-text-size:before{content:"\e241"}.glyphicon-text-color:before{content:"\e242"}.glyphicon-text-background:before{content:"\e243"}.glyphicon-object-align-top:before{content:"\e244"}.glyphicon-object-align-bottom:before{content:"\e245"}.glyphicon-object-align-horizontal:before{content:"\e246"}.glyphicon-object-align-left:before{content:"\e247"}.glyphicon-object-align-vertical:before{content:"\e248"}.glyphicon-object-align-right:before{content:"\e249"}.glyphicon-triangle-right:before{content:"\e250"}.glyphicon-triangle-left:before{content:"\e251"}.glyphicon-triangle-bottom:before{content:"\e252"}.glyphicon-triangle-top:before{content:"\e253"}.glyphicon-console:before{content:"\e254"}.glyphicon-superscript:before{content:"\e255"}.glyphicon-subscript:before{content:"\e256"}.glyphicon-menu-left:before{content:"\e257"}.glyphicon-menu-right:before{content:"\e258"}.glyphicon-menu-down:before{content:"\e259"}.glyphicon-menu-up:before{content:"\e260"}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}:after,:before{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:10px;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.42857143;color:#333;background-color:#fff}button,input,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#337ab7;text-decoration:none}a:focus,a:hover{color:#23527c;text-decoration:underline}a:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}figure{margin:0}img{vertical-align:middle}.carousel-inner>.item>a>img,.carousel-inner>.item>img,.img-responsive,.thumbnail a>img,.thumbnail>img{display:block;max-width:100%;height:auto}.img-rounded{border-radius:6px}.img-thumbnail{display:inline-block;max-width:100%;height:auto;padding:4px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}[role=button]{cursor:pointer}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-weight:400;line-height:1;color:#777}.h1,.h2,.h3,h1,h2,h3{margin-top:20px;margin-bottom:10px}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small{font-size:65%}.h4,.h5,.h6,h4,h5,h6{margin-top:10px;margin-bottom:10px}.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-size:75%}.h1,h1{font-size:36px}.h2,h2{font-size:30px}.h3,h3{font-size:24px}.h4,h4{font-size:18px}.h5,h5{font-size:14px}.h6,h6{font-size:12px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:300;line-height:1.4}@media (min-width:768px){.lead{font-size:21px}}.small,small{font-size:85%}.mark,mark{padding:.2em;background-color:#fcf8e3}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-nowrap{white-space:nowrap}.text-lowercase{text-transform:lowercase}.text-uppercase{text-transform:uppercase}.text-capitalize{text-transform:capitalize}.text-muted{color:#777}.text-primary{color:#337ab7}a.text-primary:focus,a.text-primary:hover{color:#286090}.text-success{color:#3c763d}a.text-success:focus,a.text-success:hover{color:#2b542c}.text-info{color:#31708f}a.text-info:focus,a.text-info:hover{color:#245269}.text-warning{color:#8a6d3b}a.text-warning:focus,a.text-warning:hover{color:#66512c}.text-danger{color:#a94442}a.text-danger:focus,a.text-danger:hover{color:#843534}.bg-primary{color:#fff;background-color:#337ab7}a.bg-primary:focus,a.bg-primary:hover{background-color:#286090}.bg-success{background-color:#dff0d8}a.bg-success:focus,a.bg-success:hover{background-color:#c1e2b3}.bg-info{background-color:#d9edf7}a.bg-info:focus,a.bg-info:hover{background-color:#afd9ee}.bg-warning{background-color:#fcf8e3}a.bg-warning:focus,a.bg-warning:hover{background-color:#f7ecb5}.bg-danger{background-color:#f2dede}a.bg-danger:focus,a.bg-danger:hover{background-color:#e4b9b9}.page-header{padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #eee}ol,ul{margin-top:0;margin-bottom:10px}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;margin-left:-5px;list-style:none}.list-inline>li{display:inline-block;padding-right:5px;padding-left:5px}dl{margin-top:0;margin-bottom:20px}dd,dt{line-height:1.42857143}dt{font-weight:700}dd{margin-left:0}@media (min-width:768px){.dl-horizontal dt{float:left;width:160px;overflow:hidden;clear:left;text-align:right;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}}abbr[data-original-title],abbr[title]{cursor:help;border-bottom:1px dotted #777}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10px 20px;margin:0 0 20px;font-size:17.5px;border-left:5px solid #eee}blockquote ol:last-child,blockquote p:last-child,blockquote ul:last-child{margin-bottom:0}blockquote .small,blockquote footer,blockquote small{display:block;font-size:80%;line-height:1.42857143;color:#777}blockquote .small:before,blockquote footer:before,blockquote small:before{content:'\2014 \00A0'}.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;text-align:right;border-right:5px solid #eee;border-left:0}.blockquote-reverse .small:before,.blockquote-reverse footer:before,.blockquote-reverse small:before,blockquote.pull-right .small:before,blockquote.pull-right footer:before,blockquote.pull-right small:before{content:''}.blockquote-reverse .small:after,.blockquote-reverse footer:after,.blockquote-reverse small:after,blockquote.pull-right .small:after,blockquote.pull-right footer:after,blockquote.pull-right small:after{content:'\00A0 \2014'}address{margin-bottom:20px;font-style:normal;line-height:1.42857143}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{padding:2px 4px;font-size:90%;color:#c7254e;background-color:#f9f2f4;border-radius:4px}kbd{padding:2px 4px;font-size:90%;color:#fff;background-color:#333;border-radius:3px;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.25);box-shadow:inset 0 -1px 0 rgba(0,0,0,.25)}kbd kbd{padding:0;font-size:100%;font-weight:700;-webkit-box-shadow:none;box-shadow:none}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:1.42857143;color:#333;word-break:break-all;word-wrap:break-word;background-color:#f5f5f5;border:1px solid #ccc;border-radius:4px}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:768px){.container{width:750px}}@media (min-width:992px){.container{width:970px}}@media (min-width:1200px){.container{width:1170px}}.container-fluid{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.row{margin-right:-15px;margin-left:-15px}.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{position:relative;min-height:1px;padding-right:15px;padding-left:15px}.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{float:left}.col-xs-12{width:100%}.col-xs-11{width:91.66666667%}.col-xs-10{width:83.33333333%}.col-xs-9{width:75%}.col-xs-8{width:66.66666667%}.col-xs-7{width:58.33333333%}.col-xs-6{width:50%}.col-xs-5{width:41.66666667%}.col-xs-4{width:33.33333333%}.col-xs-3{width:25%}.col-xs-2{width:16.66666667%}.col-xs-1{width:8.33333333%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666667%}.col-xs-pull-10{right:83.33333333%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666667%}.col-xs-pull-7{right:58.33333333%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666667%}.col-xs-pull-4{right:33.33333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.66666667%}.col-xs-pull-1{right:8.33333333%}.col-xs-pull-0{right:auto}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666667%}.col-xs-push-10{left:83.33333333%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666667%}.col-xs-push-7{left:58.33333333%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666667%}.col-xs-push-4{left:33.33333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.66666667%}.col-xs-push-1{left:8.33333333%}.col-xs-push-0{left:auto}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666667%}.col-xs-offset-10{margin-left:83.33333333%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666667%}.col-xs-offset-7{margin-left:58.33333333%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666667%}.col-xs-offset-4{margin-left:33.33333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.66666667%}.col-xs-offset-1{margin-left:8.33333333%}.col-xs-offset-0{margin-left:0}@media (min-width:768px){.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666667%}.col-sm-10{width:83.33333333%}.col-sm-9{width:75%}.col-sm-8{width:66.66666667%}.col-sm-7{width:58.33333333%}.col-sm-6{width:50%}.col-sm-5{width:41.66666667%}.col-sm-4{width:33.33333333%}.col-sm-3{width:25%}.col-sm-2{width:16.66666667%}.col-sm-1{width:8.33333333%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666667%}.col-sm-pull-10{right:83.33333333%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666667%}.col-sm-pull-7{right:58.33333333%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666667%}.col-sm-pull-4{right:33.33333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.66666667%}.col-sm-pull-1{right:8.33333333%}.col-sm-pull-0{right:auto}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666667%}.col-sm-push-10{left:83.33333333%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666667%}.col-sm-push-7{left:58.33333333%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666667%}.col-sm-push-4{left:33.33333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.66666667%}.col-sm-push-1{left:8.33333333%}.col-sm-push-0{left:auto}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666667%}.col-sm-offset-10{margin-left:83.33333333%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666667%}.col-sm-offset-7{margin-left:58.33333333%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666667%}.col-sm-offset-4{margin-left:33.33333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.66666667%}.col-sm-offset-1{margin-left:8.33333333%}.col-sm-offset-0{margin-left:0}}@media (min-width:992px){.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666667%}.col-md-10{width:83.33333333%}.col-md-9{width:75%}.col-md-8{width:66.66666667%}.col-md-7{width:58.33333333%}.col-md-6{width:50%}.col-md-5{width:41.66666667%}.col-md-4{width:33.33333333%}.col-md-3{width:25%}.col-md-2{width:16.66666667%}.col-md-1{width:8.33333333%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666667%}.col-md-pull-10{right:83.33333333%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666667%}.col-md-pull-7{right:58.33333333%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666667%}.col-md-pull-4{right:33.33333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.66666667%}.col-md-pull-1{right:8.33333333%}.col-md-pull-0{right:auto}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666667%}.col-md-push-10{left:83.33333333%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666667%}.col-md-push-7{left:58.33333333%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666667%}.col-md-push-4{left:33.33333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.66666667%}.col-md-push-1{left:8.33333333%}.col-md-push-0{left:auto}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666667%}.col-md-offset-10{margin-left:83.33333333%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666667%}.col-md-offset-7{margin-left:58.33333333%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666667%}.col-md-offset-4{margin-left:33.33333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.66666667%}.col-md-offset-1{margin-left:8.33333333%}.col-md-offset-0{margin-left:0}}@media (min-width:1200px){.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666667%}.col-lg-10{width:83.33333333%}.col-lg-9{width:75%}.col-lg-8{width:66.66666667%}.col-lg-7{width:58.33333333%}.col-lg-6{width:50%}.col-lg-5{width:41.66666667%}.col-lg-4{width:33.33333333%}.col-lg-3{width:25%}.col-lg-2{width:16.66666667%}.col-lg-1{width:8.33333333%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666667%}.col-lg-pull-10{right:83.33333333%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666667%}.col-lg-pull-7{right:58.33333333%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666667%}.col-lg-pull-4{right:33.33333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.66666667%}.col-lg-pull-1{right:8.33333333%}.col-lg-pull-0{right:auto}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666667%}.col-lg-push-10{left:83.33333333%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666667%}.col-lg-push-7{left:58.33333333%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666667%}.col-lg-push-4{left:33.33333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.66666667%}.col-lg-push-1{left:8.33333333%}.col-lg-push-0{left:auto}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666667%}.col-lg-offset-10{margin-left:83.33333333%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666667%}.col-lg-offset-7{margin-left:58.33333333%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666667%}.col-lg-offset-4{margin-left:33.33333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.66666667%}.col-lg-offset-1{margin-left:8.33333333%}.col-lg-offset-0{margin-left:0}}table{background-color:transparent}caption{padding-top:8px;padding-bottom:8px;color:#777;text-align:left}th{text-align:left}.table{width:100%;max-width:100%;margin-bottom:20px}.table>tbody>tr>td,.table>tbody>tr>th,.table>tfoot>tr>td,.table>tfoot>tr>th,.table>thead>tr>td,.table>thead>tr>th{padding:8px;line-height:1.42857143;vertical-align:top;border-top:1px solid #ddd}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #ddd}.table>caption+thead>tr:first-child>td,.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>td,.table>thead:first-child>tr:first-child>th{border-top:0}.table>tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed>tbody>tr>td,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>td,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>thead>tr>th{padding:5px}.table-bordered{border:1px solid #ddd}.table-bordered>tbody>tr>td,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>td,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border:1px solid #ddd}.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border-bottom-width:2px}.table-striped>tbody>tr:nth-of-type(odd){background-color:#f9f9f9}.table-hover>tbody>tr:hover{background-color:#f5f5f5}table col[class*=col-]{position:static;display:table-column;float:none}table td[class*=col-],table th[class*=col-]{position:static;display:table-cell;float:none}.table>tbody>tr.active>td,.table>tbody>tr.active>th,.table>tbody>tr>td.active,.table>tbody>tr>th.active,.table>tfoot>tr.active>td,.table>tfoot>tr.active>th,.table>tfoot>tr>td.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>thead>tr.active>th,.table>thead>tr>td.active,.table>thead>tr>th.active{background-color:#f5f5f5}.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr.active:hover>th,.table-hover>tbody>tr:hover>.active,.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover{background-color:#e8e8e8}.table>tbody>tr.success>td,.table>tbody>tr.success>th,.table>tbody>tr>td.success,.table>tbody>tr>th.success,.table>tfoot>tr.success>td,.table>tfoot>tr.success>th,.table>tfoot>tr>td.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>thead>tr.success>th,.table>thead>tr>td.success,.table>thead>tr>th.success{background-color:#dff0d8}.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr.success:hover>th,.table-hover>tbody>tr:hover>.success,.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover{background-color:#d0e9c6}.table>tbody>tr.info>td,.table>tbody>tr.info>th,.table>tbody>tr>td.info,.table>tbody>tr>th.info,.table>tfoot>tr.info>td,.table>tfoot>tr.info>th,.table>tfoot>tr>td.info,.table>tfoot>tr>th.info,.table>thead>tr.info>td,.table>thead>tr.info>th,.table>thead>tr>td.info,.table>thead>tr>th.info{background-color:#d9edf7}.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr.info:hover>th,.table-hover>tbody>tr:hover>.info,.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover{background-color:#c4e3f3}.table>tbody>tr.warning>td,.table>tbody>tr.warning>th,.table>tbody>tr>td.warning,.table>tbody>tr>th.warning,.table>tfoot>tr.warning>td,.table>tfoot>tr.warning>th,.table>tfoot>tr>td.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>thead>tr.warning>th,.table>thead>tr>td.warning,.table>thead>tr>th.warning{background-color:#fcf8e3}.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr.warning:hover>th,.table-hover>tbody>tr:hover>.warning,.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover{background-color:#faf2cc}.table>tbody>tr.danger>td,.table>tbody>tr.danger>th,.table>tbody>tr>td.danger,.table>tbody>tr>th.danger,.table>tfoot>tr.danger>td,.table>tfoot>tr.danger>th,.table>tfoot>tr>td.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>thead>tr.danger>th,.table>thead>tr>td.danger,.table>thead>tr>th.danger{background-color:#f2dede}.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr.danger:hover>th,.table-hover>tbody>tr:hover>.danger,.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover{background-color:#ebcccc}.table-responsive{min-height:.01%;overflow-x:auto}@media screen and (max-width:767px){.table-responsive{width:100%;margin-bottom:15px;overflow-y:hidden;-ms-overflow-style:-ms-autohiding-scrollbar;border:1px solid #ddd}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>td,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>thead>tr>th{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:inherit;color:#333;border:0;border-bottom:1px solid #e5e5e5}label{display:inline-block;max-width:100%;margin-bottom:5px;font-weight:700}input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type=checkbox],input[type=radio]{margin:4px 0 0;margin-top:1px\9;line-height:normal}input[type=file]{display:block}input[type=range]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type=file]:focus,input[type=checkbox]:focus,input[type=radio]:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}output{display:block;padding-top:7px;font-size:14px;line-height:1.42857143;color:#555}.form-control{display:block;width:100%;height:34px;padding:6px 12px;font-size:14px;line-height:1.42857143;color:#555;background-color:#fff;background-image:none;border:1px solid #ccc;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075);-webkit-transition:border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s}.form-control:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}.form-control::-moz-placeholder{color:#999;opacity:1}.form-control:-ms-input-placeholder{color:#999}.form-control::-webkit-input-placeholder{color:#999}.form-control::-ms-expand{background-color:transparent;border:0}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{background-color:#eee;opacity:1}.form-control[disabled],fieldset[disabled] .form-control{cursor:not-allowed}textarea.form-control{height:auto}input[type=search]{-webkit-appearance:none}@media screen and (-webkit-min-device-pixel-ratio:0){input[type=date].form-control,input[type=time].form-control,input[type=datetime-local].form-control,input[type=month].form-control{line-height:34px}.input-group-sm input[type=date],.input-group-sm input[type=time],.input-group-sm input[type=datetime-local],.input-group-sm input[type=month],input[type=date].input-sm,input[type=time].input-sm,input[type=datetime-local].input-sm,input[type=month].input-sm{line-height:30px}.input-group-lg input[type=date],.input-group-lg input[type=time],.input-group-lg input[type=datetime-local],.input-group-lg input[type=month],input[type=date].input-lg,input[type=time].input-lg,input[type=datetime-local].input-lg,input[type=month].input-lg{line-height:46px}}.form-group{margin-bottom:15px}.checkbox,.radio{position:relative;display:block;margin-top:10px;margin-bottom:10px}.checkbox label,.radio label{min-height:20px;padding-left:20px;margin-bottom:0;font-weight:400;cursor:pointer}.checkbox input[type=checkbox],.checkbox-inline input[type=checkbox],.radio input[type=radio],.radio-inline input[type=radio]{position:absolute;margin-top:4px\9;margin-left:-20px}.checkbox+.checkbox,.radio+.radio{margin-top:-5px}.checkbox-inline,.radio-inline{position:relative;display:inline-block;padding-left:20px;margin-bottom:0;font-weight:400;vertical-align:middle;cursor:pointer}.checkbox-inline+.checkbox-inline,.radio-inline+.radio-inline{margin-top:0;margin-left:10px}fieldset[disabled] input[type=checkbox],fieldset[disabled] input[type=radio],input[type=checkbox].disabled,input[type=checkbox][disabled],input[type=radio].disabled,input[type=radio][disabled]{cursor:not-allowed}.checkbox-inline.disabled,.radio-inline.disabled,fieldset[disabled] .checkbox-inline,fieldset[disabled] .radio-inline{cursor:not-allowed}.checkbox.disabled label,.radio.disabled label,fieldset[disabled] .checkbox label,fieldset[disabled] .radio label{cursor:not-allowed}.form-control-static{min-height:34px;padding-top:7px;padding-bottom:7px;margin-bottom:0}.form-control-static.input-lg,.form-control-static.input-sm{padding-right:0;padding-left:0}.input-sm{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-sm{height:30px;line-height:30px}select[multiple].input-sm,textarea.input-sm{height:auto}.form-group-sm .form-control{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.form-group-sm select.form-control{height:30px;line-height:30px}.form-group-sm select[multiple].form-control,.form-group-sm textarea.form-control{height:auto}.form-group-sm .form-control-static{height:30px;min-height:32px;padding:6px 10px;font-size:12px;line-height:1.5}.input-lg{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.input-lg{height:46px;line-height:46px}select[multiple].input-lg,textarea.input-lg{height:auto}.form-group-lg .form-control{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.form-group-lg select.form-control{height:46px;line-height:46px}.form-group-lg select[multiple].form-control,.form-group-lg textarea.form-control{height:auto}.form-group-lg .form-control-static{height:46px;min-height:38px;padding:11px 16px;font-size:18px;line-height:1.3333333}.has-feedback{position:relative}.has-feedback .form-control{padding-right:42.5px}.form-control-feedback{position:absolute;top:0;right:0;z-index:2;display:block;width:34px;height:34px;line-height:34px;text-align:center;pointer-events:none}.form-group-lg .form-control+.form-control-feedback,.input-group-lg+.form-control-feedback,.input-lg+.form-control-feedback{width:46px;height:46px;line-height:46px}.form-group-sm .form-control+.form-control-feedback,.input-group-sm+.form-control-feedback,.input-sm+.form-control-feedback{width:30px;height:30px;line-height:30px}.has-success .checkbox,.has-success .checkbox-inline,.has-success .control-label,.has-success .help-block,.has-success .radio,.has-success .radio-inline,.has-success.checkbox label,.has-success.checkbox-inline label,.has-success.radio label,.has-success.radio-inline label{color:#3c763d}.has-success .form-control{border-color:#3c763d;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-success .form-control:focus{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168}.has-success .input-group-addon{color:#3c763d;background-color:#dff0d8;border-color:#3c763d}.has-success .form-control-feedback{color:#3c763d}.has-warning .checkbox,.has-warning .checkbox-inline,.has-warning .control-label,.has-warning .help-block,.has-warning .radio,.has-warning .radio-inline,.has-warning.checkbox label,.has-warning.checkbox-inline label,.has-warning.radio label,.has-warning.radio-inline label{color:#8a6d3b}.has-warning .form-control{border-color:#8a6d3b;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-warning .form-control:focus{border-color:#66512c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b}.has-warning .input-group-addon{color:#8a6d3b;background-color:#fcf8e3;border-color:#8a6d3b}.has-warning .form-control-feedback{color:#8a6d3b}.has-error .checkbox,.has-error .checkbox-inline,.has-error .control-label,.has-error .help-block,.has-error .radio,.has-error .radio-inline,.has-error.checkbox label,.has-error.checkbox-inline label,.has-error.radio label,.has-error.radio-inline label{color:#a94442}.has-error .form-control{border-color:#a94442;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-error .form-control:focus{border-color:#843534;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483}.has-error .input-group-addon{color:#a94442;background-color:#f2dede;border-color:#a94442}.has-error .form-control-feedback{color:#a94442}.has-feedback label~.form-control-feedback{top:25px}.has-feedback label.sr-only~.form-control-feedback{top:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#737373}@media (min-width:768px){.form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-static{display:inline-block}.form-inline .input-group{display:inline-table;vertical-align:middle}.form-inline .input-group .form-control,.form-inline .input-group .input-group-addon,.form-inline .input-group .input-group-btn{width:auto}.form-inline .input-group>.form-control{width:100%}.form-inline .control-label{margin-bottom:0;vertical-align:middle}.form-inline .checkbox,.form-inline .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.form-inline .checkbox label,.form-inline .radio label{padding-left:0}.form-inline .checkbox input[type=checkbox],.form-inline .radio input[type=radio]{position:relative;margin-left:0}.form-inline .has-feedback .form-control-feedback{top:0}}.form-horizontal .checkbox,.form-horizontal .checkbox-inline,.form-horizontal .radio,.form-horizontal .radio-inline{padding-top:7px;margin-top:0;margin-bottom:0}.form-horizontal .checkbox,.form-horizontal .radio{min-height:27px}.form-horizontal .form-group{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.form-horizontal .control-label{padding-top:7px;margin-bottom:0;text-align:right}}.form-horizontal .has-feedback .form-control-feedback{right:15px}@media (min-width:768px){.form-horizontal .form-group-lg .control-label{padding-top:11px;font-size:18px}}@media (min-width:768px){.form-horizontal .form-group-sm .control-label{padding-top:6px;font-size:12px}}.btn{display:inline-block;padding:6px 12px;margin-bottom:0;font-size:14px;font-weight:400;line-height:1.42857143;text-align:center;white-space:nowrap;vertical-align:middle;-ms-touch-action:manipulation;touch-action:manipulation;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-image:none;border:1px solid transparent;border-radius:4px}.btn.active.focus,.btn.active:focus,.btn.focus,.btn:active.focus,.btn:active:focus,.btn:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn.focus,.btn:focus,.btn:hover{color:#333;text-decoration:none}.btn.active,.btn:active{background-image:none;outline:0;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{cursor:not-allowed;filter:alpha(opacity=65);-webkit-box-shadow:none;box-shadow:none;opacity:.65}a.btn.disabled,fieldset[disabled] a.btn{pointer-events:none}.btn-default{color:#333;background-color:#fff;border-color:#ccc}.btn-default.focus,.btn-default:focus{color:#333;background-color:#e6e6e6;border-color:#8c8c8c}.btn-default:hover{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default.active,.btn-default:active,.open>.dropdown-toggle.btn-default{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default.active.focus,.btn-default.active:focus,.btn-default.active:hover,.btn-default:active.focus,.btn-default:active:focus,.btn-default:active:hover,.open>.dropdown-toggle.btn-default.focus,.open>.dropdown-toggle.btn-default:focus,.open>.dropdown-toggle.btn-default:hover{color:#333;background-color:#d4d4d4;border-color:#8c8c8c}.btn-default.active,.btn-default:active,.open>.dropdown-toggle.btn-default{background-image:none}.btn-default.disabled.focus,.btn-default.disabled:focus,.btn-default.disabled:hover,.btn-default[disabled].focus,.btn-default[disabled]:focus,.btn-default[disabled]:hover,fieldset[disabled] .btn-default.focus,fieldset[disabled] .btn-default:focus,fieldset[disabled] .btn-default:hover{background-color:#fff;border-color:#ccc}.btn-default .badge{color:#fff;background-color:#333}.btn-primary{color:#fff;background-color:#337ab7;border-color:#2e6da4}.btn-primary.focus,.btn-primary:focus{color:#fff;background-color:#286090;border-color:#122b40}.btn-primary:hover{color:#fff;background-color:#286090;border-color:#204d74}.btn-primary.active,.btn-primary:active,.open>.dropdown-toggle.btn-primary{color:#fff;background-color:#286090;border-color:#204d74}.btn-primary.active.focus,.btn-primary.active:focus,.btn-primary.active:hover,.btn-primary:active.focus,.btn-primary:active:focus,.btn-primary:active:hover,.open>.dropdown-toggle.btn-primary.focus,.open>.dropdown-toggle.btn-primary:focus,.open>.dropdown-toggle.btn-primary:hover{color:#fff;background-color:#204d74;border-color:#122b40}.btn-primary.active,.btn-primary:active,.open>.dropdown-toggle.btn-primary{background-image:none}.btn-primary.disabled.focus,.btn-primary.disabled:focus,.btn-primary.disabled:hover,.btn-primary[disabled].focus,.btn-primary[disabled]:focus,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary.focus,fieldset[disabled] .btn-primary:focus,fieldset[disabled] .btn-primary:hover{background-color:#337ab7;border-color:#2e6da4}.btn-primary .badge{color:#337ab7;background-color:#fff}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-success.focus,.btn-success:focus{color:#fff;background-color:#449d44;border-color:#255625}.btn-success:hover{color:#fff;background-color:#449d44;border-color:#398439}.btn-success.active,.btn-success:active,.open>.dropdown-toggle.btn-success{color:#fff;background-color:#449d44;border-color:#398439}.btn-success.active.focus,.btn-success.active:focus,.btn-success.active:hover,.btn-success:active.focus,.btn-success:active:focus,.btn-success:active:hover,.open>.dropdown-toggle.btn-success.focus,.open>.dropdown-toggle.btn-success:focus,.open>.dropdown-toggle.btn-success:hover{color:#fff;background-color:#398439;border-color:#255625}.btn-success.active,.btn-success:active,.open>.dropdown-toggle.btn-success{background-image:none}.btn-success.disabled.focus,.btn-success.disabled:focus,.btn-success.disabled:hover,.btn-success[disabled].focus,.btn-success[disabled]:focus,.btn-success[disabled]:hover,fieldset[disabled] .btn-success.focus,fieldset[disabled] .btn-success:focus,fieldset[disabled] .btn-success:hover{background-color:#5cb85c;border-color:#4cae4c}.btn-success .badge{color:#5cb85c;background-color:#fff}.btn-info{color:#fff;background-color:#5bc0de;border-color:#46b8da}.btn-info.focus,.btn-info:focus{color:#fff;background-color:#31b0d5;border-color:#1b6d85}.btn-info:hover{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info.active,.btn-info:active,.open>.dropdown-toggle.btn-info{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info.active.focus,.btn-info.active:focus,.btn-info.active:hover,.btn-info:active.focus,.btn-info:active:focus,.btn-info:active:hover,.open>.dropdown-toggle.btn-info.focus,.open>.dropdown-toggle.btn-info:focus,.open>.dropdown-toggle.btn-info:hover{color:#fff;background-color:#269abc;border-color:#1b6d85}.btn-info.active,.btn-info:active,.open>.dropdown-toggle.btn-info{background-image:none}.btn-info.disabled.focus,.btn-info.disabled:focus,.btn-info.disabled:hover,.btn-info[disabled].focus,.btn-info[disabled]:focus,.btn-info[disabled]:hover,fieldset[disabled] .btn-info.focus,fieldset[disabled] .btn-info:focus,fieldset[disabled] .btn-info:hover{background-color:#5bc0de;border-color:#46b8da}.btn-info .badge{color:#5bc0de;background-color:#fff}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#eea236}.btn-warning.focus,.btn-warning:focus{color:#fff;background-color:#ec971f;border-color:#985f0d}.btn-warning:hover{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning.active,.btn-warning:active,.open>.dropdown-toggle.btn-warning{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning.active.focus,.btn-warning.active:focus,.btn-warning.active:hover,.btn-warning:active.focus,.btn-warning:active:focus,.btn-warning:active:hover,.open>.dropdown-toggle.btn-warning.focus,.open>.dropdown-toggle.btn-warning:focus,.open>.dropdown-toggle.btn-warning:hover{color:#fff;background-color:#d58512;border-color:#985f0d}.btn-warning.active,.btn-warning:active,.open>.dropdown-toggle.btn-warning{background-image:none}.btn-warning.disabled.focus,.btn-warning.disabled:focus,.btn-warning.disabled:hover,.btn-warning[disabled].focus,.btn-warning[disabled]:focus,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning.focus,fieldset[disabled] .btn-warning:focus,fieldset[disabled] .btn-warning:hover{background-color:#f0ad4e;border-color:#eea236}.btn-warning .badge{color:#f0ad4e;background-color:#fff}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.btn-danger.focus,.btn-danger:focus{color:#fff;background-color:#c9302c;border-color:#761c19}.btn-danger:hover{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger.active,.btn-danger:active,.open>.dropdown-toggle.btn-danger{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger.active.focus,.btn-danger.active:focus,.btn-danger.active:hover,.btn-danger:active.focus,.btn-danger:active:focus,.btn-danger:active:hover,.open>.dropdown-toggle.btn-danger.focus,.open>.dropdown-toggle.btn-danger:focus,.open>.dropdown-toggle.btn-danger:hover{color:#fff;background-color:#ac2925;border-color:#761c19}.btn-danger.active,.btn-danger:active,.open>.dropdown-toggle.btn-danger{background-image:none}.btn-danger.disabled.focus,.btn-danger.disabled:focus,.btn-danger.disabled:hover,.btn-danger[disabled].focus,.btn-danger[disabled]:focus,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger.focus,fieldset[disabled] .btn-danger:focus,fieldset[disabled] .btn-danger:hover{background-color:#d9534f;border-color:#d43f3a}.btn-danger .badge{color:#d9534f;background-color:#fff}.btn-link{font-weight:400;color:#337ab7;border-radius:0}.btn-link,.btn-link.active,.btn-link:active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:active,.btn-link:focus,.btn-link:hover{border-color:transparent}.btn-link:focus,.btn-link:hover{color:#23527c;text-decoration:underline;background-color:transparent}.btn-link[disabled]:focus,.btn-link[disabled]:hover,fieldset[disabled] .btn-link:focus,fieldset[disabled] .btn-link:hover{color:#777;text-decoration:none}.btn-group-lg>.btn,.btn-lg{padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.btn-group-sm>.btn,.btn-sm{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-group-xs>.btn,.btn-xs{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:5px}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}tr.collapse.in{display:table-row}tbody.collapse.in{display:table-row-group}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition-timing-function:ease;-o-transition-timing-function:ease;transition-timing-function:ease;-webkit-transition-duration:.35s;-o-transition-duration:.35s;transition-duration:.35s;-webkit-transition-property:height,visibility;-o-transition-property:height,visibility;transition-property:height,visibility}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px dashed;border-top:4px solid\9;border-right:4px solid transparent;border-left:4px solid transparent}.dropdown,.dropup{position:relative}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;font-size:14px;text-align:left;list-style:none;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.15);border-radius:4px;-webkit-box-shadow:0 6px 12px rgba(0,0,0,.175);box-shadow:0 6px 12px rgba(0,0,0,.175)}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:400;line-height:1.42857143;color:#333;white-space:nowrap}.dropdown-menu>li>a:focus,.dropdown-menu>li>a:hover{color:#262626;text-decoration:none;background-color:#f5f5f5}.dropdown-menu>.active>a,.dropdown-menu>.active>a:focus,.dropdown-menu>.active>a:hover{color:#fff;text-decoration:none;background-color:#337ab7;outline:0}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{color:#777}.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{text-decoration:none;cursor:not-allowed;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-menu-right{right:0;left:auto}.dropdown-menu-left{right:auto;left:0}.dropdown-header{display:block;padding:3px 20px;font-size:12px;line-height:1.42857143;color:#777;white-space:nowrap}.dropdown-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{content:"";border-top:0;border-bottom:4px dashed;border-bottom:4px solid\9}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:2px}@media (min-width:768px){.navbar-right .dropdown-menu{right:0;left:auto}.navbar-right .dropdown-menu-left{right:auto;left:0}}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;float:left}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:2}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{margin-left:-5px}.btn-toolbar .btn,.btn-toolbar .btn-group,.btn-toolbar .input-group{float:left}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-left:5px}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-bottom-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{padding-right:8px;padding-left:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-right:12px;padding-left:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn .caret{margin-left:0}.btn-lg .caret{border-width:5px 5px 0;border-bottom-width:0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-left-radius:4px;border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-top-right-radius:0}.btn-group-justified{display:table;width:100%;table-layout:fixed;border-collapse:separate}.btn-group-justified>.btn,.btn-group-justified>.btn-group{display:table-cell;float:none;width:1%}.btn-group-justified>.btn-group .btn{width:100%}.btn-group-justified>.btn-group .dropdown-menu{left:auto}[data-toggle=buttons]>.btn input[type=checkbox],[data-toggle=buttons]>.btn input[type=radio],[data-toggle=buttons]>.btn-group>.btn input[type=checkbox],[data-toggle=buttons]>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*=col-]{float:none;padding-right:0;padding-left:0}.input-group .form-control{position:relative;z-index:2;float:left;width:100%;margin-bottom:0}.input-group .form-control:focus{z-index:3}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{height:46px;line-height:46px}select[multiple].input-group-lg>.form-control,select[multiple].input-group-lg>.input-group-addon,select[multiple].input-group-lg>.input-group-btn>.btn,textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn{height:auto}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{height:30px;line-height:30px}select[multiple].input-group-sm>.form-control,select[multiple].input-group-sm>.input-group-addon,select[multiple].input-group-sm>.input-group-btn>.btn,textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn{height:auto}.input-group .form-control,.input-group-addon,.input-group-btn{display:table-cell}.input-group .form-control:not(:first-child):not(:last-child),.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:6px 12px;font-size:14px;font-weight:400;line-height:1;color:#555;text-align:center;background-color:#eee;border:1px solid #ccc;border-radius:4px}.input-group-addon.input-sm{padding:5px 10px;font-size:12px;border-radius:3px}.input-group-addon.input-lg{padding:10px 16px;font-size:18px;border-radius:6px}.input-group-addon input[type=checkbox],.input-group-addon input[type=radio]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn-group:not(:last-child)>.btn,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:first-child>.btn-group:not(:first-child)>.btn,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;font-size:0;white-space:nowrap}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:active,.input-group-btn>.btn:focus,.input-group-btn>.btn:hover{z-index:2}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{z-index:2;margin-left:-1px}.nav{padding-left:0;margin-bottom:0;list-style:none}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;padding:10px 15px}.nav>li>a:focus,.nav>li>a:hover{text-decoration:none;background-color:#eee}.nav>li.disabled>a{color:#777}.nav>li.disabled>a:focus,.nav>li.disabled>a:hover{color:#777;text-decoration:none;cursor:not-allowed;background-color:transparent}.nav .open>a,.nav .open>a:focus,.nav .open>a:hover{background-color:#eee;border-color:#337ab7}.nav .nav-divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.42857143;border:1px solid transparent;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover{border-color:#eee #eee #ddd}.nav-tabs>li.active>a,.nav-tabs>li.active>a:focus,.nav-tabs>li.active>a:hover{color:#555;cursor:default;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent}.nav-tabs.nav-justified{width:100%;border-bottom:0}.nav-tabs.nav-justified>li{float:none}.nav-tabs.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-tabs.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-tabs.nav-justified>li>a{margin-bottom:0}}.nav-tabs.nav-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border:1px solid #ddd}@media (min-width:768px){.nav-tabs.nav-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border-bottom-color:#fff}}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:4px}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:focus,.nav-pills>li.active>a:hover{color:#fff;background-color:#337ab7}.nav-stacked>li{float:none}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified{width:100%}.nav-justified>li{float:none}.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a{margin-bottom:0}}.nav-tabs-justified{border-bottom:0}.nav-tabs-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{border:1px solid #ddd}@media (min-width:768px){.nav-tabs-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{border-bottom-color:#fff}}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.navbar{position:relative;min-height:50px;margin-bottom:20px;border:1px solid transparent}@media (min-width:768px){.navbar{border-radius:4px}}@media (min-width:768px){.navbar-header{float:left}}.navbar-collapse{padding-right:15px;padding-left:15px;overflow-x:visible;-webkit-overflow-scrolling:touch;border-top:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1)}.navbar-collapse.in{overflow-y:auto}@media (min-width:768px){.navbar-collapse{width:auto;border-top:0;-webkit-box-shadow:none;box-shadow:none}.navbar-collapse.collapse{display:block!important;height:auto!important;padding-bottom:0;overflow:visible!important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse{padding-right:0;padding-left:0}}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{max-height:340px}@media (max-device-width:480px) and (orientation:landscape){.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{max-height:200px}}.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:0;margin-left:0}}.navbar-static-top{z-index:1000;border-width:0 0 1px}@media (min-width:768px){.navbar-static-top{border-radius:0}}.navbar-fixed-bottom,.navbar-fixed-top{position:fixed;right:0;left:0;z-index:1030}@media (min-width:768px){.navbar-fixed-bottom,.navbar-fixed-top{border-radius:0}}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.navbar-brand{float:left;height:50px;padding:15px 15px;font-size:18px;line-height:20px}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-brand>img{display:block}@media (min-width:768px){.navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;padding:9px 10px;margin-top:8px;margin-right:15px;margin-bottom:8px;background-color:transparent;background-image:none;border:1px solid transparent;border-radius:4px}.navbar-toggle:focus{outline:0}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}@media (min-width:768px){.navbar-toggle{display:none}}.navbar-nav{margin:7.5px -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:20px}@media (max-width:767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;-webkit-box-shadow:none;box-shadow:none}.navbar-nav .open .dropdown-menu .dropdown-header,.navbar-nav .open .dropdown-menu>li>a{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:20px}.navbar-nav .open .dropdown-menu>li>a:focus,.navbar-nav .open .dropdown-menu>li>a:hover{background-image:none}}@media (min-width:768px){.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:15px;padding-bottom:15px}}.navbar-form{padding:10px 15px;margin-top:8px;margin-right:-15px;margin-bottom:8px;margin-left:-15px;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1)}@media (min-width:768px){.navbar-form .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.navbar-form .form-control{display:inline-block;width:auto;vertical-align:middle}.navbar-form .form-control-static{display:inline-block}.navbar-form .input-group{display:inline-table;vertical-align:middle}.navbar-form .input-group .form-control,.navbar-form .input-group .input-group-addon,.navbar-form .input-group .input-group-btn{width:auto}.navbar-form .input-group>.form-control{width:100%}.navbar-form .control-label{margin-bottom:0;vertical-align:middle}.navbar-form .checkbox,.navbar-form .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.navbar-form .checkbox label,.navbar-form .radio label{padding-left:0}.navbar-form .checkbox input[type=checkbox],.navbar-form .radio input[type=radio]{position:relative;margin-left:0}.navbar-form .has-feedback .form-control-feedback{top:0}}@media (max-width:767px){.navbar-form .form-group{margin-bottom:5px}.navbar-form .form-group:last-child{margin-bottom:0}}@media (min-width:768px){.navbar-form{width:auto;padding-top:0;padding-bottom:0;margin-right:0;margin-left:0;border:0;-webkit-box-shadow:none;box-shadow:none}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-left-radius:0;border-top-right-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{margin-bottom:0;border-top-left-radius:4px;border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.navbar-btn{margin-top:8px;margin-bottom:8px}.navbar-btn.btn-sm{margin-top:10px;margin-bottom:10px}.navbar-btn.btn-xs{margin-top:14px;margin-bottom:14px}.navbar-text{margin-top:15px;margin-bottom:15px}@media (min-width:768px){.navbar-text{float:left;margin-right:15px;margin-left:15px}}@media (min-width:768px){.navbar-left{float:left!important}.navbar-right{float:right!important;margin-right:-15px}.navbar-right~.navbar-right{margin-right:0}}.navbar-default{background-color:#f8f8f8;border-color:#e7e7e7}.navbar-default .navbar-brand{color:#777}.navbar-default .navbar-brand:focus,.navbar-default .navbar-brand:hover{color:#5e5e5e;background-color:transparent}.navbar-default .navbar-text{color:#777}.navbar-default .navbar-nav>li>a{color:#777}.navbar-default .navbar-nav>li>a:focus,.navbar-default .navbar-nav>li>a:hover{color:#333;background-color:transparent}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:focus,.navbar-default .navbar-nav>.active>a:hover{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:focus,.navbar-default .navbar-nav>.disabled>a:hover{color:#ccc;background-color:transparent}.navbar-default .navbar-toggle{border-color:#ddd}.navbar-default .navbar-toggle:focus,.navbar-default .navbar-toggle:hover{background-color:#ddd}.navbar-default .navbar-toggle .icon-bar{background-color:#888}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#e7e7e7}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:focus,.navbar-default .navbar-nav>.open>a:hover{color:#555;background-color:#e7e7e7}@media (max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#777}.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover{color:#333;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#ccc;background-color:transparent}}.navbar-default .navbar-link{color:#777}.navbar-default .navbar-link:hover{color:#333}.navbar-default .btn-link{color:#777}.navbar-default .btn-link:focus,.navbar-default .btn-link:hover{color:#333}.navbar-default .btn-link[disabled]:focus,.navbar-default .btn-link[disabled]:hover,fieldset[disabled] .navbar-default .btn-link:focus,fieldset[disabled] .navbar-default .btn-link:hover{color:#ccc}.navbar-inverse{background-color:#222;border-color:#080808}.navbar-inverse .navbar-brand{color:#9d9d9d}.navbar-inverse .navbar-brand:focus,.navbar-inverse .navbar-brand:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-text{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a:focus,.navbar-inverse .navbar-nav>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:focus,.navbar-inverse .navbar-nav>.active>a:hover{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:focus,.navbar-inverse .navbar-nav>.disabled>a:hover{color:#444;background-color:transparent}.navbar-inverse .navbar-toggle{border-color:#333}.navbar-inverse .navbar-toggle:focus,.navbar-inverse .navbar-toggle:hover{background-color:#333}.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#101010}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:focus,.navbar-inverse .navbar-nav>.open>a:hover{color:#fff;background-color:#080808}@media (max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#444;background-color:transparent}}.navbar-inverse .navbar-link{color:#9d9d9d}.navbar-inverse .navbar-link:hover{color:#fff}.navbar-inverse .btn-link{color:#9d9d9d}.navbar-inverse .btn-link:focus,.navbar-inverse .btn-link:hover{color:#fff}.navbar-inverse .btn-link[disabled]:focus,.navbar-inverse .btn-link[disabled]:hover,fieldset[disabled] .navbar-inverse .btn-link:focus,fieldset[disabled] .navbar-inverse .btn-link:hover{color:#444}.breadcrumb{padding:8px 15px;margin-bottom:20px;list-style:none;background-color:#f5f5f5;border-radius:4px}.breadcrumb>li{display:inline-block}.breadcrumb>li+li:before{padding:0 5px;color:#ccc;content:"/\00a0"}.breadcrumb>.active{color:#777}.pagination{display:inline-block;padding-left:0;margin:20px 0;border-radius:4px}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:6px 12px;margin-left:-1px;line-height:1.42857143;color:#337ab7;text-decoration:none;background-color:#fff;border:1px solid #ddd}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-top-left-radius:4px;border-bottom-left-radius:4px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-top-right-radius:4px;border-bottom-right-radius:4px}.pagination>li>a:focus,.pagination>li>a:hover,.pagination>li>span:focus,.pagination>li>span:hover{z-index:2;color:#23527c;background-color:#eee;border-color:#ddd}.pagination>.active>a,.pagination>.active>a:focus,.pagination>.active>a:hover,.pagination>.active>span,.pagination>.active>span:focus,.pagination>.active>span:hover{z-index:3;color:#fff;cursor:default;background-color:#337ab7;border-color:#337ab7}.pagination>.disabled>a,.pagination>.disabled>a:focus,.pagination>.disabled>a:hover,.pagination>.disabled>span,.pagination>.disabled>span:focus,.pagination>.disabled>span:hover{color:#777;cursor:not-allowed;background-color:#fff;border-color:#ddd}.pagination-lg>li>a,.pagination-lg>li>span{padding:10px 16px;font-size:18px;line-height:1.3333333}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-top-left-radius:6px;border-bottom-left-radius:6px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-top-right-radius:6px;border-bottom-right-radius:6px}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px;line-height:1.5}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-top-left-radius:3px;border-bottom-left-radius:3px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-top-right-radius:3px;border-bottom-right-radius:3px}.pager{padding-left:0;margin:20px 0;text-align:center;list-style:none}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:15px}.pager li>a:focus,.pager li>a:hover{text-decoration:none;background-color:#eee}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:focus,.pager .disabled>a:hover,.pager .disabled>span{color:#777;cursor:not-allowed;background-color:#fff}.label{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}a.label:focus,a.label:hover{color:#fff;text-decoration:none;cursor:pointer}.label:empty{display:none}.btn .label{position:relative;top:-1px}.label-default{background-color:#777}.label-default[href]:focus,.label-default[href]:hover{background-color:#5e5e5e}.label-primary{background-color:#337ab7}.label-primary[href]:focus,.label-primary[href]:hover{background-color:#286090}.label-success{background-color:#5cb85c}.label-success[href]:focus,.label-success[href]:hover{background-color:#449d44}.label-info{background-color:#5bc0de}.label-info[href]:focus,.label-info[href]:hover{background-color:#31b0d5}.label-warning{background-color:#f0ad4e}.label-warning[href]:focus,.label-warning[href]:hover{background-color:#ec971f}.label-danger{background-color:#d9534f}.label-danger[href]:focus,.label-danger[href]:hover{background-color:#c9302c}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:middle;background-color:#777;border-radius:10px}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.btn-group-xs>.btn .badge,.btn-xs .badge{top:0;padding:1px 5px}a.badge:focus,a.badge:hover{color:#fff;text-decoration:none;cursor:pointer}.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#337ab7;background-color:#fff}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}.nav-pills>li>a>.badge{margin-left:3px}.jumbotron{padding-top:30px;padding-bottom:30px;margin-bottom:30px;color:inherit;background-color:#eee}.jumbotron .h1,.jumbotron h1{color:inherit}.jumbotron p{margin-bottom:15px;font-size:21px;font-weight:200}.jumbotron>hr{border-top-color:#d5d5d5}.container .jumbotron,.container-fluid .jumbotron{padding-right:15px;padding-left:15px;border-radius:6px}.jumbotron .container{max-width:100%}@media screen and (min-width:768px){.jumbotron{padding-top:48px;padding-bottom:48px}.container .jumbotron,.container-fluid .jumbotron{padding-right:60px;padding-left:60px}.jumbotron .h1,.jumbotron h1{font-size:63px}}.thumbnail{display:block;padding:4px;margin-bottom:20px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:border .2s ease-in-out;-o-transition:border .2s ease-in-out;transition:border .2s ease-in-out}.thumbnail a>img,.thumbnail>img{margin-right:auto;margin-left:auto}a.thumbnail.active,a.thumbnail:focus,a.thumbnail:hover{border-color:#337ab7}.thumbnail .caption{padding:9px;color:#333}.alert{padding:15px;margin-bottom:20px;border:1px solid transparent;border-radius:4px}.alert h4{margin-top:0;color:inherit}.alert .alert-link{font-weight:700}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable,.alert-dismissible{padding-right:35px}.alert-dismissable .close,.alert-dismissible .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.alert-success hr{border-top-color:#c9e2b3}.alert-success .alert-link{color:#2b542c}.alert-info{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.alert-info hr{border-top-color:#a6e1ec}.alert-info .alert-link{color:#245269}.alert-warning{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.alert-warning hr{border-top-color:#f7e1b5}.alert-warning .alert-link{color:#66512c}.alert-danger{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.alert-danger hr{border-top-color:#e4b9c0}.alert-danger .alert-link{color:#843534}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-o-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{height:20px;margin-bottom:20px;overflow:hidden;background-color:#f5f5f5;border-radius:4px;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.1);box-shadow:inset 0 1px 2px rgba(0,0,0,.1)}.progress-bar{float:left;width:0;height:100%;font-size:12px;line-height:20px;color:#fff;text-align:center;background-color:#337ab7;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);-webkit-transition:width .6s ease;-o-transition:width .6s ease;transition:width .6s ease}.progress-bar-striped,.progress-striped .progress-bar{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);-webkit-background-size:40px 40px;background-size:40px 40px}.progress-bar.active,.progress.active .progress-bar{-webkit-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-bar-success{background-color:#5cb85c}.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-info{background-color:#5bc0de}.progress-striped .progress-bar-info{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-warning{background-color:#f0ad4e}.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-danger{background-color:#d9534f}.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.media{margin-top:15px}.media:first-child{margin-top:0}.media,.media-body{overflow:hidden;zoom:1}.media-body{width:10000px}.media-object{display:block}.media-object.img-thumbnail{max-width:none}.media-right,.media>.pull-right{padding-left:10px}.media-left,.media>.pull-left{padding-right:10px}.media-body,.media-left,.media-right{display:table-cell;vertical-align:top}.media-middle{vertical-align:middle}.media-bottom{vertical-align:bottom}.media-heading{margin-top:0;margin-bottom:5px}.media-list{padding-left:0;list-style:none}.list-group{padding-left:0;margin-bottom:20px}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#fff;border:1px solid #ddd}.list-group-item:first-child{border-top-left-radius:4px;border-top-right-radius:4px}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}a.list-group-item,button.list-group-item{color:#555}a.list-group-item .list-group-item-heading,button.list-group-item .list-group-item-heading{color:#333}a.list-group-item:focus,a.list-group-item:hover,button.list-group-item:focus,button.list-group-item:hover{color:#555;text-decoration:none;background-color:#f5f5f5}button.list-group-item{width:100%;text-align:left}.list-group-item.disabled,.list-group-item.disabled:focus,.list-group-item.disabled:hover{color:#777;cursor:not-allowed;background-color:#eee}.list-group-item.disabled .list-group-item-heading,.list-group-item.disabled:focus .list-group-item-heading,.list-group-item.disabled:hover .list-group-item-heading{color:inherit}.list-group-item.disabled .list-group-item-text,.list-group-item.disabled:focus .list-group-item-text,.list-group-item.disabled:hover .list-group-item-text{color:#777}.list-group-item.active,.list-group-item.active:focus,.list-group-item.active:hover{z-index:2;color:#fff;background-color:#337ab7;border-color:#337ab7}.list-group-item.active .list-group-item-heading,.list-group-item.active .list-group-item-heading>.small,.list-group-item.active .list-group-item-heading>small,.list-group-item.active:focus .list-group-item-heading,.list-group-item.active:focus .list-group-item-heading>.small,.list-group-item.active:focus .list-group-item-heading>small,.list-group-item.active:hover .list-group-item-heading,.list-group-item.active:hover .list-group-item-heading>.small,.list-group-item.active:hover .list-group-item-heading>small{color:inherit}.list-group-item.active .list-group-item-text,.list-group-item.active:focus .list-group-item-text,.list-group-item.active:hover .list-group-item-text{color:#c7ddef}.list-group-item-success{color:#3c763d;background-color:#dff0d8}a.list-group-item-success,button.list-group-item-success{color:#3c763d}a.list-group-item-success .list-group-item-heading,button.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:focus,a.list-group-item-success:hover,button.list-group-item-success:focus,button.list-group-item-success:hover{color:#3c763d;background-color:#d0e9c6}a.list-group-item-success.active,a.list-group-item-success.active:focus,a.list-group-item-success.active:hover,button.list-group-item-success.active,button.list-group-item-success.active:focus,button.list-group-item-success.active:hover{color:#fff;background-color:#3c763d;border-color:#3c763d}.list-group-item-info{color:#31708f;background-color:#d9edf7}a.list-group-item-info,button.list-group-item-info{color:#31708f}a.list-group-item-info .list-group-item-heading,button.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:focus,a.list-group-item-info:hover,button.list-group-item-info:focus,button.list-group-item-info:hover{color:#31708f;background-color:#c4e3f3}a.list-group-item-info.active,a.list-group-item-info.active:focus,a.list-group-item-info.active:hover,button.list-group-item-info.active,button.list-group-item-info.active:focus,button.list-group-item-info.active:hover{color:#fff;background-color:#31708f;border-color:#31708f}.list-group-item-warning{color:#8a6d3b;background-color:#fcf8e3}a.list-group-item-warning,button.list-group-item-warning{color:#8a6d3b}a.list-group-item-warning .list-group-item-heading,button.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:focus,a.list-group-item-warning:hover,button.list-group-item-warning:focus,button.list-group-item-warning:hover{color:#8a6d3b;background-color:#faf2cc}a.list-group-item-warning.active,a.list-group-item-warning.active:focus,a.list-group-item-warning.active:hover,button.list-group-item-warning.active,button.list-group-item-warning.active:focus,button.list-group-item-warning.active:hover{color:#fff;background-color:#8a6d3b;border-color:#8a6d3b}.list-group-item-danger{color:#a94442;background-color:#f2dede}a.list-group-item-danger,button.list-group-item-danger{color:#a94442}a.list-group-item-danger .list-group-item-heading,button.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:focus,a.list-group-item-danger:hover,button.list-group-item-danger:focus,button.list-group-item-danger:hover{color:#a94442;background-color:#ebcccc}a.list-group-item-danger.active,a.list-group-item-danger.active:focus,a.list-group-item-danger.active:hover,button.list-group-item-danger.active,button.list-group-item-danger.active:focus,button.list-group-item-danger.active:hover{color:#fff;background-color:#a94442;border-color:#a94442}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:20px;background-color:#fff;border:1px solid transparent;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.05);box-shadow:0 1px 1px rgba(0,0,0,.05)}.panel-body{padding:15px}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-left-radius:3px;border-top-right-radius:3px}.panel-heading>.dropdown .dropdown-toggle{color:inherit}.panel-title{margin-top:0;margin-bottom:0;font-size:16px;color:inherit}.panel-title>.small,.panel-title>.small>a,.panel-title>a,.panel-title>small,.panel-title>small>a{color:inherit}.panel-footer{padding:10px 15px;background-color:#f5f5f5;border-top:1px solid #ddd;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.list-group,.panel>.panel-collapse>.list-group{margin-bottom:0}.panel>.list-group .list-group-item,.panel>.panel-collapse>.list-group .list-group-item{border-width:1px 0;border-radius:0}.panel>.list-group:first-child .list-group-item:first-child,.panel>.panel-collapse>.list-group:first-child .list-group-item:first-child{border-top:0;border-top-left-radius:3px;border-top-right-radius:3px}.panel>.list-group:last-child .list-group-item:last-child,.panel>.panel-collapse>.list-group:last-child .list-group-item:last-child{border-bottom:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.panel-heading+.panel-collapse>.list-group .list-group-item:first-child{border-top-left-radius:0;border-top-right-radius:0}.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.list-group+.panel-footer{border-top-width:0}.panel>.panel-collapse>.table,.panel>.table,.panel>.table-responsive>.table{margin-bottom:0}.panel>.panel-collapse>.table caption,.panel>.table caption,.panel>.table-responsive>.table caption{padding-right:15px;padding-left:15px}.panel>.table-responsive:first-child>.table:first-child,.panel>.table:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child,.panel>.table:first-child>thead:first-child>tr:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child{border-top-left-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child{border-top-right-radius:3px}.panel>.table-responsive:last-child>.table:last-child,.panel>.table:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child{border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child{border-bottom-right-radius:3px}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive,.panel>.table+.panel-body,.panel>.table-responsive+.panel-body{border-top:1px solid #ddd}.panel>.table>tbody:first-child>tr:first-child td,.panel>.table>tbody:first-child>tr:first-child th{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.panel>.table-bordered>tbody>tr:first-child>td,.panel>.table-bordered>tbody>tr:first-child>th,.panel>.table-bordered>thead>tr:first-child>td,.panel>.table-bordered>thead>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>th,.panel>.table-responsive>.table-bordered>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th{border-bottom:0}.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}.panel>.table-responsive{margin-bottom:0;border:0}.panel-group{margin-bottom:20px}.panel-group .panel{margin-bottom:0;border-radius:4px}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading{border-bottom:0}.panel-group .panel-heading+.panel-collapse>.list-group,.panel-group .panel-heading+.panel-collapse>.panel-body{border-top:1px solid #ddd}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #ddd}.panel-default{border-color:#ddd}.panel-default>.panel-heading{color:#333;background-color:#f5f5f5;border-color:#ddd}.panel-default>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ddd}.panel-default>.panel-heading .badge{color:#f5f5f5;background-color:#333}.panel-default>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ddd}.panel-primary{border-color:#337ab7}.panel-primary>.panel-heading{color:#fff;background-color:#337ab7;border-color:#337ab7}.panel-primary>.panel-heading+.panel-collapse>.panel-body{border-top-color:#337ab7}.panel-primary>.panel-heading .badge{color:#337ab7;background-color:#fff}.panel-primary>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#337ab7}.panel-success{border-color:#d6e9c6}.panel-success>.panel-heading{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.panel-success>.panel-heading+.panel-collapse>.panel-body{border-top-color:#d6e9c6}.panel-success>.panel-heading .badge{color:#dff0d8;background-color:#3c763d}.panel-success>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#d6e9c6}.panel-info{border-color:#bce8f1}.panel-info>.panel-heading{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.panel-info>.panel-heading+.panel-collapse>.panel-body{border-top-color:#bce8f1}.panel-info>.panel-heading .badge{color:#d9edf7;background-color:#31708f}.panel-info>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#bce8f1}.panel-warning{border-color:#faebcc}.panel-warning>.panel-heading{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.panel-warning>.panel-heading+.panel-collapse>.panel-body{border-top-color:#faebcc}.panel-warning>.panel-heading .badge{color:#fcf8e3;background-color:#8a6d3b}.panel-warning>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#faebcc}.panel-danger{border-color:#ebccd1}.panel-danger>.panel-heading{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.panel-danger>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ebccd1}.panel-danger>.panel-heading .badge{color:#f2dede;background-color:#a94442}.panel-danger>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ebccd1}.embed-responsive{position:relative;display:block;height:0;padding:0;overflow:hidden}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-16by9{padding-bottom:56.25%}.embed-responsive-4by3{padding-bottom:75%}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.05);box-shadow:inset 0 1px 1px rgba(0,0,0,.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,.15)}.well-lg{padding:24px;border-radius:6px}.well-sm{padding:9px;border-radius:3px}.close{float:right;font-size:21px;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;filter:alpha(opacity=20);opacity:.2}.close:focus,.close:hover{color:#000;text-decoration:none;cursor:pointer;filter:alpha(opacity=50);opacity:.5}button.close{-webkit-appearance:none;padding:0;cursor:pointer;background:0 0;border:0}.modal-open{overflow:hidden}.modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;display:none;overflow:hidden;-webkit-overflow-scrolling:touch;outline:0}.modal.fade .modal-dialog{-webkit-transition:-webkit-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:transform .3s ease-out;-webkit-transform:translate(0,-25%);-ms-transform:translate(0,-25%);-o-transform:translate(0,-25%);transform:translate(0,-25%)}.modal.in .modal-dialog{-webkit-transform:translate(0,0);-ms-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,0)}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #999;border:1px solid rgba(0,0,0,.2);border-radius:6px;outline:0;-webkit-box-shadow:0 3px 9px rgba(0,0,0,.5);box-shadow:0 3px 9px rgba(0,0,0,.5)}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{filter:alpha(opacity=0);opacity:0}.modal-backdrop.in{filter:alpha(opacity=50);opacity:.5}.modal-header{padding:15px;border-bottom:1px solid #e5e5e5}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.42857143}.modal-body{position:relative;padding:15px}.modal-footer{padding:15px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer .btn+.btn{margin-bottom:0;margin-left:5px}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:768px){.modal-dialog{width:600px;margin:30px auto}.modal-content{-webkit-box-shadow:0 5px 15px rgba(0,0,0,.5);box-shadow:0 5px 15px rgba(0,0,0,.5)}.modal-sm{width:300px}}@media (min-width:992px){.modal-lg{width:900px}}.tooltip{position:absolute;z-index:1070;display:block;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:12px;font-style:normal;font-weight:400;line-height:1.42857143;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;word-wrap:normal;white-space:normal;filter:alpha(opacity=0);opacity:0;line-break:auto}.tooltip.in{filter:alpha(opacity=90);opacity:.9}.tooltip.top{padding:5px 0;margin-top:-3px}.tooltip.right{padding:0 5px;margin-left:3px}.tooltip.bottom{padding:5px 0;margin-top:3px}.tooltip.left{padding:0 5px;margin-left:-3px}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;background-color:#000;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-left .tooltip-arrow{right:5px;bottom:0;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-right .tooltip-arrow{bottom:0;left:5px;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:#000}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-width:5px 0 5px 5px;border-left-color:#000}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-left .tooltip-arrow{top:0;right:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-right .tooltip-arrow{top:0;left:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.popover{position:absolute;top:0;left:0;z-index:1060;display:none;max-width:276px;padding:1px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;font-style:normal;font-weight:400;line-height:1.42857143;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;word-wrap:normal;white-space:normal;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2);line-break:auto}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover-title{padding:8px 14px;margin:0;font-size:14px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-radius:5px 5px 0 0}.popover-content{padding:9px 14px}.popover>.arrow,.popover>.arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover>.arrow{border-width:11px}.popover>.arrow:after{content:"";border-width:10px}.popover.top>.arrow{bottom:-11px;left:50%;margin-left:-11px;border-top-color:#999;border-top-color:rgba(0,0,0,.25);border-bottom-width:0}.popover.top>.arrow:after{bottom:1px;margin-left:-10px;content:" ";border-top-color:#fff;border-bottom-width:0}.popover.right>.arrow{top:50%;left:-11px;margin-top:-11px;border-right-color:#999;border-right-color:rgba(0,0,0,.25);border-left-width:0}.popover.right>.arrow:after{bottom:-10px;left:1px;content:" ";border-right-color:#fff;border-left-width:0}.popover.bottom>.arrow{top:-11px;left:50%;margin-left:-11px;border-top-width:0;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,.25)}.popover.bottom>.arrow:after{top:1px;margin-left:-10px;content:" ";border-top-width:0;border-bottom-color:#fff}.popover.left>.arrow{top:50%;right:-11px;margin-top:-11px;border-right-width:0;border-left-color:#999;border-left-color:rgba(0,0,0,.25)}.popover.left>.arrow:after{right:1px;bottom:-10px;content:" ";border-right-width:0;border-left-color:#fff}.carousel{position:relative}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner>.item{position:relative;display:none;-webkit-transition:.6s ease-in-out left;-o-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>a>img,.carousel-inner>.item>img{line-height:1}@media all and (transform-3d),(-webkit-transform-3d){.carousel-inner>.item{-webkit-transition:-webkit-transform .6s ease-in-out;-o-transition:-o-transform .6s ease-in-out;transition:transform .6s ease-in-out;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-perspective:1000px;perspective:1000px}.carousel-inner>.item.active.right,.carousel-inner>.item.next{left:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}.carousel-inner>.item.active.left,.carousel-inner>.item.prev{left:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}.carousel-inner>.item.active,.carousel-inner>.item.next.left,.carousel-inner>.item.prev.right{left:0;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:0;bottom:0;left:0;width:15%;font-size:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6);background-color:rgba(0,0,0,0);filter:alpha(opacity=50);opacity:.5}.carousel-control.left{background-image:-webkit-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.5)),to(rgba(0,0,0,.0001)));background-image:linear-gradient(to right,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1);background-repeat:repeat-x}.carousel-control.right{right:0;left:auto;background-image:-webkit-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.0001)),to(rgba(0,0,0,.5)));background-image:linear-gradient(to right,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1);background-repeat:repeat-x}.carousel-control:focus,.carousel-control:hover{color:#fff;text-decoration:none;filter:alpha(opacity=90);outline:0;opacity:.9}.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{position:absolute;top:50%;z-index:5;display:inline-block;margin-top:-10px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{left:50%;margin-left:-10px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{right:50%;margin-right:-10px}.carousel-control .icon-next,.carousel-control .icon-prev{width:20px;height:20px;font-family:serif;line-height:1}.carousel-control .icon-prev:before{content:'\2039'}.carousel-control .icon-next:before{content:'\203a'}.carousel-indicators{position:absolute;bottom:10px;left:50%;z-index:15;width:60%;padding-left:0;margin-left:-30%;text-align:center;list-style:none}.carousel-indicators li{display:inline-block;width:10px;height:10px;margin:1px;text-indent:-999px;cursor:pointer;background-color:#000\9;background-color:rgba(0,0,0,0);border:1px solid #fff;border-radius:10px}.carousel-indicators .active{width:12px;height:12px;margin:0;background-color:#fff}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6)}.carousel-caption .btn{text-shadow:none}@media screen and (min-width:768px){.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{width:30px;height:30px;margin-top:-10px;font-size:30px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{margin-left:-10px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{margin-right:-10px}.carousel-caption{right:20%;left:20%;padding-bottom:30px}.carousel-indicators{bottom:20px}}.btn-group-vertical>.btn-group:after,.btn-group-vertical>.btn-group:before,.btn-toolbar:after,.btn-toolbar:before,.clearfix:after,.clearfix:before,.container-fluid:after,.container-fluid:before,.container:after,.container:before,.dl-horizontal dd:after,.dl-horizontal dd:before,.form-horizontal .form-group:after,.form-horizontal .form-group:before,.modal-footer:after,.modal-footer:before,.modal-header:after,.modal-header:before,.nav:after,.nav:before,.navbar-collapse:after,.navbar-collapse:before,.navbar-header:after,.navbar-header:before,.navbar:after,.navbar:before,.pager:after,.pager:before,.panel-body:after,.panel-body:before,.row:after,.row:before{display:table;content:" "}.btn-group-vertical>.btn-group:after,.btn-toolbar:after,.clearfix:after,.container-fluid:after,.container:after,.dl-horizontal dd:after,.form-horizontal .form-group:after,.modal-footer:after,.modal-header:after,.nav:after,.navbar-collapse:after,.navbar-header:after,.navbar:after,.pager:after,.panel-body:after,.row:after{clear:both}.center-block{display:block;margin-right:auto;margin-left:auto}.pull-right{float:right!important}.pull-left{float:left!important}.hide{display:none!important}.show{display:block!important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none!important}.affix{position:fixed}@-ms-viewport{width:device-width}.visible-lg,.visible-md,.visible-sm,.visible-xs{display:none!important}.visible-lg-block,.visible-lg-inline,.visible-lg-inline-block,.visible-md-block,.visible-md-inline,.visible-md-inline-block,.visible-sm-block,.visible-sm-inline,.visible-sm-inline-block,.visible-xs-block,.visible-xs-inline,.visible-xs-inline-block{display:none!important}@media (max-width:767px){.visible-xs{display:block!important}table.visible-xs{display:table!important}tr.visible-xs{display:table-row!important}td.visible-xs,th.visible-xs{display:table-cell!important}}@media (max-width:767px){.visible-xs-block{display:block!important}}@media (max-width:767px){.visible-xs-inline{display:inline!important}}@media (max-width:767px){.visible-xs-inline-block{display:inline-block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm{display:block!important}table.visible-sm{display:table!important}tr.visible-sm{display:table-row!important}td.visible-sm,th.visible-sm{display:table-cell!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-block{display:block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline{display:inline!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline-block{display:inline-block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md{display:block!important}table.visible-md{display:table!important}tr.visible-md{display:table-row!important}td.visible-md,th.visible-md{display:table-cell!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-block{display:block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline{display:inline!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline-block{display:inline-block!important}}@media (min-width:1200px){.visible-lg{display:block!important}table.visible-lg{display:table!important}tr.visible-lg{display:table-row!important}td.visible-lg,th.visible-lg{display:table-cell!important}}@media (min-width:1200px){.visible-lg-block{display:block!important}}@media (min-width:1200px){.visible-lg-inline{display:inline!important}}@media (min-width:1200px){.visible-lg-inline-block{display:inline-block!important}}@media (max-width:767px){.hidden-xs{display:none!important}}@media (min-width:768px) and (max-width:991px){.hidden-sm{display:none!important}}@media (min-width:992px) and (max-width:1199px){.hidden-md{display:none!important}}@media (min-width:1200px){.hidden-lg{display:none!important}}.visible-print{display:none!important}@media print{.visible-print{display:block!important}table.visible-print{display:table!important}tr.visible-print{display:table-row!important}td.visible-print,th.visible-print{display:table-cell!important}}.visible-print-block{display:none!important}@media print{.visible-print-block{display:block!important}}.visible-print-inline{display:none!important}@media print{.visible-print-inline{display:inline!important}}.visible-print-inline-block{display:none!important}@media print{.visible-print-inline-block{display:inline-block!important}}@media print{.hidden-print{display:none!important}}
+/*# sourceMappingURL=bootstrap.min.css.map */
\ No newline at end of file
diff --git a/doc/第7周代码注释/blog/static/assets/css/docs.min.css b/doc/第7周代码注释/blog/static/assets/css/docs.min.css
new file mode 100644
index 00000000..39451975
--- /dev/null
+++ b/doc/第7周代码注释/blog/static/assets/css/docs.min.css
@@ -0,0 +1,11 @@
+/*!
+ * 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)
+ */@-ms-viewport{width:device-width}@-o-viewport{width:device-width}@viewport{width:device-width}.hll{background-color:#ffc}.c{color:#999}.err{color:#A00;background-color:#FAA}.k{color:#069}.o{color:#555}.cm{color:#999}.cp{color:#099}.c1{color:#999}.cs{color:#999}.gd{background-color:#FCC;border:1px solid #C00}.ge{font-style:italic}.gr{color:red}.gh{color:#030}.gi{background-color:#CFC;border:1px solid #0C0}.go{color:#AAA}.gp{color:#009}.gu{color:#030}.gt{color:#9C6}.kc{color:#069}.kd{color:#069}.kn{color:#069}.kp{color:#069}.kr{color:#069}.kt{color:#078}.m{color:#F60}.s{color:#d44950}.na{color:#4f9fcf}.nb{color:#366}.nc{color:#0A8}.no{color:#360}.nd{color:#99F}.ni{color:#999}.ne{color:#C00}.nf{color:#C0F}.nl{color:#99F}.nn{color:#0CF}.nt{color:#2f6f9f}.nv{color:#033}.ow{color:#000}.w{color:#bbb}.mf{color:#F60}.mh{color:#F60}.mi{color:#F60}.mo{color:#F60}.sb{color:#C30}.sc{color:#C30}.sd{color:#C30;font-style:italic}.s2{color:#C30}.se{color:#C30}.sh{color:#C30}.si{color:#A00}.sx{color:#C30}.sr{color:#3AA}.s1{color:#C30}.ss{color:#FC3}.bp{color:#366}.vc{color:#033}.vg{color:#033}.vi{color:#033}.il{color:#F60}.css .nt+.nt,.css .o,.css .o+.nt{color:#999}.select2-container{position:relative;display:inline-block;zoom:1;*display:inline;vertical-align:top;padding:0;border:0}.select2-container:hover{border:0;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.select2-container,.select2-drop,.select2-search,.select2-search input{-moz-box-sizing:border-box;-ms-box-sizing:border-box;-webkit-box-sizing:border-box;-khtml-box-sizing:border-box;box-sizing:border-box}.select2-container .select2-choice{display:block;overflow:hidden;text-decoration:none;padding:4px 12px;margin:0;color:#333;text-shadow:0 1px 0 #fff;white-space:nowrap;font-family:Arial,Helvetica,sans-serif;font-weight:700;font-size:13px;cursor:default;height:18px;background-color:#f3f3f3;background-image:-moz-linear-gradient(top,#f5f5f5,#f1f1f1);background-image:-webkit-gradient(linear,0 0,0 100%,from(#f5f5f5),to(#f1f1f1));background-image:-webkit-linear-gradient(top,#f5f5f5,#f1f1f1);background-image:-o-linear-gradient(top,#f5f5f5,#f1f1f1);background-image:linear-gradient(to bottom,#f5f5f5,#f1f1f1);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#fff1f1f1', GradientType=0);-webkit-background-clip:padding;-moz-background-clip:padding;background-clip:padding;border:1px solid #dcdcdc;-webkit-border-radius:2px;-moz-border-radius:2px;border-radius:2px;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;-moz-box-sizing:content-box;-ms-box-sizing:content-box;-webkit-box-sizing:content-box;-khtml-box-sizing:content-box;box-sizing:content-box}.select2-container .select2-choice:hover{color:#333;text-shadow:none;border-color:#c6c6c6;background-color:#f5f5f5;background-image:-moz-linear-gradient(top,#f8f8f8,#f1f1f1);background-image:-webkit-gradient(linear,0 0,0 100%,from(#f8f8f8),to(#f1f1f1));background-image:-webkit-linear-gradient(top,#f8f8f8,#f1f1f1);background-image:-o-linear-gradient(top,#f8f8f8,#f1f1f1);background-image:linear-gradient(to bottom,#f8f8f8,#f1f1f1);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff8f8f8', endColorstr='#fff1f1f1', GradientType=0);-webkit-box-shadow:0 1px 1px rgba(0,0,0,.1);-moz-box-shadow:0 1px 1px rgba(0,0,0,.1);box-shadow:0 1px 1px rgba(0,0,0,.1);background-position:0 0;-webkit-transition:none;-moz-transition:none;-o-transition:none;transition:none;z-index:2}.select2-container-active .select2-choice:hover{border:1px solid #4D90FE}.select2-container.select2-drop-above .select2-choice{background-image:-webkit-gradient(linear,left bottom,left top,color-stop(0,#eee),color-stop(.9,#fff));background-image:-webkit-linear-gradient(center bottom,#eee 0,#fff 90%);background-image:-moz-linear-gradient(center bottom,#eee 0,#fff 90%);background-image:-o-linear-gradient(bottom,#eee 0,#fff 90%);background-image:-ms-linear-gradient(top,#eee 0,#fff 90%);filter:progid:DXImageTransform.Microsoft.gradient( startColorstr='#eeeeee', endColorstr='#ffffff', GradientType=0 );background-image:linear-gradient(top,#eee 0,#fff 90%)}.select2-container .select2-choice span{margin-right:26px;display:block;overflow:hidden;white-space:nowrap;-o-text-overflow:ellipsis;-ms-text-overflow:ellipsis;text-overflow:ellipsis}.select2-container .select2-choice abbr{display:block;position:absolute;right:26px;top:8px;width:12px;height:12px;font-size:17px;line-height:16px;color:#595959;font-weight:700;cursor:pointer;text-decoration:none;border:0;outline:0}.select2-container .select2-choice abbr:hover{color:#222;cursor:pointer}.select2-drop-mask{position:absolute;left:0;top:0;z-index:9998;opacity:0}.select2-drop{background:#fff;color:#000;border:1px solid #aaa;position:absolute;top:100%;-webkit-box-shadow:0 2px 4px rgba(0,0,0,.2);-moz-box-shadow:0 2px 4px rgba(0,0,0,.2);-o-box-shadow:0 2px 4px rgba(0,0,0,.2);box-shadow:0 2px 4px rgba(0,0,0,.2);z-index:9999;width:100%;margin-top:1px}.select2-drop.select2-drop-above{margin-top:-1px;-webkit-box-shadow:0 -2px 4px rgba(0,0,0,.2);-moz-box-shadow:0 -2px 4px rgba(0,0,0,.2);-o-box-shadow:0 -2px 4px rgba(0,0,0,.2);box-shadow:0 -2px 4px rgba(0,0,0,.2)}.select2-container .select2-choice div{-webkit-border-radius:0 2px 2px 0;-moz-border-radius:0 2px 2px 0;border-radius:0 2px 2px 0;-moz-background-clip:padding;-webkit-background-clip:padding-box;background-clip:padding-box;position:absolute;right:0;top:0;display:block;height:100%;width:18px}.select2-container .select2-choice div b{background:url(/assets/img/select2.png) no-repeat -30px 2px;display:block;width:100%;height:100%}.select2-search{display:inline-block;white-space:nowrap;z-index:10000;min-height:26px;width:100%;margin:0;padding:4px 4px 0 4px}.select2-search-hidden{display:block;position:absolute;left:-10000px}.select2-search input{background:#fff url(/assets/img/select2.png) no-repeat 100% -22px;background:url(/assets/img/select2.png) no-repeat 100% -22px,-webkit-gradient(linear,left bottom,left top,color-stop(.85,#fff),color-stop(.99,#eee));background:url(/assets/img/select2.png) no-repeat 100% -22px,-webkit-linear-gradient(center bottom,#fff 85%,#eee 99%);background:url(/assets/img/select2.png) no-repeat 100% -22px,-moz-linear-gradient(center bottom,#fff 85%,#eee 99%);background:url(/assets/img/select2.png) no-repeat 100% -22px,-o-linear-gradient(bottom,#fff 85%,#eee 99%);background:url(/assets/img/select2.png) no-repeat 100% -22px,-ms-linear-gradient(top,#fff 85%,#eee 99%);background:url(/assets/img/select2.png) no-repeat 100% -22px,linear-gradient(top,#fff 85%,#eee 99%);padding:4px 20px 4px 5px;outline:0;border:1px solid #aaa;font-family:sans-serif;font-size:1em;width:100%;margin:0;height:auto!important;min-height:26px;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;border-radius:0;-moz-border-radius:0;-webkit-border-radius:0}.select2-drop.select2-drop-above .select2-search input{margin-top:4px}.select2-search input.select2-active{background:#fff url(../img/spinner.gif) no-repeat 100%;background:url(../img/spinner.gif) no-repeat 100%,-webkit-gradient(linear,left bottom,left top,color-stop(.85,#fff),color-stop(.99,#eee));background:url(../img/spinner.gif) no-repeat 100%,-webkit-linear-gradient(center bottom,#fff 85%,#eee 99%);background:url(../img/spinner.gif) no-repeat 100%,-moz-linear-gradient(center bottom,#fff 85%,#eee 99%);background:url(../img/spinner.gif) no-repeat 100%,-o-linear-gradient(bottom,#fff 85%,#eee 99%);background:url(../img/spinner.gif) no-repeat 100%,-ms-linear-gradient(top,#fff 85%,#eee 99%);background:url(../img/spinner.gif) no-repeat 100%,linear-gradient(top,#fff 85%,#eee 99%)}.select2-container-active .select2-choice,.select2-container-active .select2-choices{border:1px solid #4D90FE;outline:0}.select2-dropdown-open .select2-choice,.select2-dropdown-open .select2-choice:hover{background-color:#f4f4f4;background-image:-moz-linear-gradient(top,#f6f6f6,#f1f1f1);background-image:-webkit-gradient(linear,0 0,0 100%,from(#f6f6f6),to(#f1f1f1));background-image:-webkit-linear-gradient(top,#f6f6f6,#f1f1f1);background-image:-o-linear-gradient(top,#f6f6f6,#f1f1f1);background-image:linear-gradient(to bottom,#f6f6f6,#f1f1f1);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff6f6f6', endColorstr='#fff1f1f1', GradientType=0);-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.1);-moz-box-shadow:inset 0 1px 2px rgba(0,0,0,.1);box-shadow:inset 0 1px 2px rgba(0,0,0,.1)}.select2-dropdown-open .select2-choice div{background:0 0;border-left:none}.select2-results{margin:4px 1px 4px 0;padding:0;position:relative;overflow-x:hidden;overflow-y:auto;max-height:200px}.select2-results ul.select2-result-sub{margin:0}.select2-results ul.select2-result-sub>li .select2-result-label{padding-left:20px}.select2-results ul.select2-result-sub ul.select2-result-sub>li .select2-result-label{padding-left:40px}.select2-results ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub>li .select2-result-label{padding-left:60px}.select2-results ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub>li .select2-result-label{padding-left:80px}.select2-results ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub>li .select2-result-label{padding-left:100px}.select2-results ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub>li .select2-result-label{padding-left:110px}.select2-results ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub>li .select2-result-label{padding-left:120px}.select2-results li{list-style:none;display:list-item}.select2-results li.select2-result-with-children>.select2-result-label{font-weight:700}.select2-results .select2-result-label{padding:3px 7px 4px;margin:0;cursor:pointer}.select2-results .select2-highlighted{background:#eee}.select2-results li em{background:#feffde;font-style:normal}.select2-results .select2-highlighted em{background:0 0}.select2-results .select2-no-results,.select2-results .select2-searching,.select2-results .select2-selection-limit{background:#f4f4f4;display:list-item;padding-left:4px}.select2-results .select2-disabled{display:none}.select2-more-results.select2-active{background:#f4f4f4 url(../img/spinner.gif) no-repeat 100%}.select2-more-results{background:#f4f4f4;display:list-item}.select2-container.select2-container-disabled .select2-choice{color:#b3b3b3;border-color:#d9d9d9;background-color:#e6e6e6;background-image:none;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;text-shadow:none;cursor:default}.select2-container.select2-container-disabled .select2-choice div{opacity:.5;filter:alpha(opacity=50)}.select2-container-multi .select2-choices{background-color:#fff;border:1px solid #d9d9d9;border-top:1px solid silver;-webkit-border-radius:1px;-moz-border-radius:1px;border-radius:1px;margin:0;padding:0;cursor:text;overflow:hidden;height:auto!important;height:1%;position:relative}.select2-container-multi .select2-choices:hover{border:1px solid #b9b9b9;border-top:1px solid #a0a0a0;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.1);-moz-box-shadow:inset 0 1px 2px rgba(0,0,0,.1);box-shadow:inset 0 1px 2px rgba(0,0,0,.1)}.select2-container-multi .select2-choices{min-height:26px}.select2-container-multi.select2-container-active .select2-choices{border:1px solid #4D90FE;outline:0}.select2-container-multi .select2-choices li{float:left;list-style:none}.select2-container-multi .select2-choices .select2-search-field{white-space:nowrap;margin:0;padding:0}.select2-container-multi .select2-choices .select2-search-field input{color:#666;background:0 0!important;font-family:sans-serif;font-size:100%;height:23px;padding:5px;margin:1px 0;outline:0;border:0;-webkit-box-shadow:none;-moz-box-shadow:none;-o-box-shadow:none;box-shadow:none}.select2-container-multi .select2-choices .select2-search-field input.select2-active{background:#fff url(../img/spinner.gif) no-repeat 100%!important}.select2-default{color:#999!important}.select2-container-multi .select2-choices .select2-search-choice{-webkit-border-radius:2px;-moz-border-radius:2px;border-radius:2px;-moz-background-clip:padding;-webkit-background-clip:padding-box;background-clip:padding-box;background-color:#DAE4F6;color:#222;font-family:Arial;border:1px solid #DAE4F6;line-height:23px;padding:0 19px 0 5px;margin:1px;position:relative;cursor:default}.select2-container-multi .select2-choices .select2-search-choice span{cursor:default}.select2-container-multi .select2-choices .select2-search-choice-focus{background:#A6D7F5}.select2-search-choice-close{display:block;position:absolute;right:3px;top:4px;width:12px;height:13px;font-size:17px;line-height:16px;color:#444;font-weight:700;outline:0}.select2-search-choice-close:hover{text-decoration:none;color:#222;cursor:pointer}.select2-container-multi.select2-container-disabled .select2-choices{background-color:#f4f4f4;background-image:none;border:1px solid #ddd;cursor:default}.select2-container-multi.select2-container-disabled .select2-choices .select2-search-choice{background-image:none;background-color:#f4f4f4;border:1px solid #ddd;padding:3px 5px 3px 5px}.select2-container-multi.select2-container-disabled .select2-choices .select2-search-choice .select2-search-choice-close{display:none}.select2-result-selectable .select2-match,.select2-result-unselectable .select2-result-selectable .select2-match{font-weight:700}.select2-result-unselectable .select2-match{text-decoration:none}.select2-offscreen{position:absolute;left:-10000px}.select2-results::-webkit-scrollbar{height:16px;width:10px}.select2-results::-webkit-scrollbar-button:end:increment,.select2-results::-webkit-scrollbar-button:start:decrement{background-color:transparent;display:block;height:0}.select2-results::-webkit-scrollbar-track{background-clip:padding-box;border:solid transparent;border-width:0 0 0 4px}.select2-results::-webkit-scrollbar-track-piece{background-color:transparent;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.select2-results::-webkit-scrollbar:hover{background-color:#f3f3f3;border:1px solid #dbdbdb}.select2-results::-webkit-scrollbar-thumb:horizontal,.select2-results::-webkit-scrollbar-thumb:vertical{background-color:#c6c6c6;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.select2-results::-webkit-scrollbar-thumb{background-color:rgba(0,0,0,.2);border:solid transparent;border-width:0;-webkit-box-shadow:inset 1px 1px 0 rgba(0,0,0,.1),inset 0 -1px 0 rgba(0,0,0,.07);-moz-box-shadow:inset 1px 1px 0 rgba(0,0,0,.1),inset 0 -1px 0 rgba(0,0,0,.07);box-shadow:inset 1px 1px 0 rgba(0,0,0,.1),inset 0 -1px 0 rgba(0,0,0,.07);background-clip:padding-box}.select2-results::-webkit-scrollbar-thumb:hover{background-color:#949494}.select2-results::-webkit-scrollbar-thumb:active{background-color:rgba(0,0,0,.5);-webkit-box-shadow:inset 1px 1px 3px rgba(0,0,0,.35);-moz-box-shadow:inset 1px 1px 3px rgba(0,0,0,.35);box-shadow:inset 1px 1px 3px rgba(0,0,0,.35)}@media only screen and (-webkit-min-device-pixel-ratio:1.5){.select2-container .select2-choice div b,.select2-search input{background-image:url(/assets/img/select2x2.png)!important;background-repeat:no-repeat!important;background-size:60px 40px!important}.select2-search input{background-position:100% -21px!important}}/*!
+ * Bootstrap Docs (http://getbootstrap.com)
+ * Copyright 2011-2016 Twitter, Inc.
+ * Licensed under the Creative Commons Attribution 3.0 Unported License. For
+ * details, see https://creativecommons.org/licenses/by/3.0/.
+ */body{position:relative;padding-top:94px}.table code{font-size:13px;font-weight:400}h2 code,h3 code,h4 code{background-color:inherit}.btn-outline{color:#4d90fe;background-color:transparent;border-color:#4d90fe}.btn-outline:active,.btn-outline:focus,.btn-outline:hover{color:#fff;background-color:#4d90fe;border-color:#4d90fe}.btn-outline-inverse{color:#fff;background-color:transparent;border-color:#fff}.btn-outline-inverse:active,.btn-outline-inverse:focus,.btn-outline-inverse:hover{color:#2d87e2;text-shadow:none;background-color:#fff;border-color:#fff}#skippy{display:block;padding:1em;color:#777;background-color:#f1f1f1;outline:0}#skippy .skiplink-text{padding:.5em;outline:1px dotted}#content:focus{outline:0}.bs-docs-footer{padding-top:40px;padding-bottom:30px;margin-top:100px;color:#777;text-align:center;border-top:1px solid #e5e5e5}.bs-docs-footer-links{padding-left:0;margin-bottom:20px}.bs-docs-footer-links li{display:inline-block}.bs-docs-footer-links li+li{margin-left:15px}@media (min-width:768px){.bs-docs-footer{text-align:left}.bs-docs-footer p{margin-bottom:0}}.bs-docs-header,.bs-docs-masthead{position:relative;padding:30px 0;color:#b3d4f4;text-align:center;text-shadow:0 1px 0 rgba(0,0,0,.1);background-color:#2d87e2;background-image:-webkit-linear-gradient(top,#1b6ec1 0,#2d87e2 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#1b6ec1),to(#2d87e2));background-image:-o-linear-gradient(top,#1b6ec1 0,#2d87e2 100%);background-image:linear-gradient(to bottom,#1b6ec1 0,#2d87e2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#1b6ec1', endColorstr='#2d87e2', GradientType=0);background-repeat:repeat-x}.bs-docs-masthead .bs-docs-booticon{margin:0 auto 30px}.bs-docs-masthead h1{font-weight:300;line-height:1;color:#fff}.bs-docs-masthead .lead{margin:0 auto 30px;font-size:20px;color:#fff}.bs-docs-masthead .version{margin-top:-15px;color:#b3d4f4}.bs-docs-masthead .btn{width:100%;padding:15px 30px;font-size:20px}@media (min-width:480px){.bs-docs-masthead .btn{width:auto}}@media (min-width:768px){.bs-docs-masthead{padding:80px 0}.bs-docs-masthead h1{font-size:60px}.bs-docs-masthead .lead{font-size:24px}}@media (min-width:992px){.bs-docs-masthead .lead{width:80%;font-size:30px}}.bs-docs-header{margin-bottom:40px;font-size:20px}.bs-docs-header h1{margin-top:0;color:#fff}.bs-docs-header p{margin-bottom:0;font-weight:300;line-height:1.4}.bs-docs-header .container{position:relative}@media (min-width:768px){.bs-docs-header{padding-top:60px;padding-bottom:60px;font-size:24px;text-align:left}.bs-docs-header h1{font-size:60px;line-height:1}}@media (min-width:992px){.bs-docs-header h1,.bs-docs-header p{margin-right:380px}}.bs-docs-featurette{padding-top:40px;padding-bottom:40px;font-size:16px;line-height:1.5;color:#555;text-align:center;background-color:#fff;border-bottom:1px solid #e5e5e5}.bs-docs-featurette+.bs-docs-footer{margin-top:0;border-top:0}.bs-docs-featurette-title{margin-bottom:5px;font-size:30px;font-weight:400;color:#333}.half-rule{width:100px;margin:40px auto}.bs-docs-featurette h3{margin-bottom:5px;font-weight:400;color:#333}.bs-docs-featurette-img{display:block;margin-bottom:20px;color:#333}.bs-docs-featurette-img:hover{color:#337ab7;text-decoration:none}.bs-docs-featurette-img img{display:block;margin-bottom:15px}@media (min-width:480px){.bs-docs-featurette .img-responsive{margin-top:30px}}@media (min-width:768px){.bs-docs-featurette{padding-top:100px;padding-bottom:100px}.bs-docs-featurette-title{font-size:40px}.bs-docs-featurette .lead{max-width:80%;margin-right:auto;margin-left:auto}.bs-docs-featurette .img-responsive{margin-top:0}}.bs-docs-featured-sites{margin-right:-1px;margin-left:-1px}.bs-docs-featured-sites .col-xs-6{padding:1px}.bs-docs-featured-sites .img-responsive{margin-top:0}@media (min-width:768px){.bs-docs-featured-sites .col-sm-3:first-child img{border-top-left-radius:4px;border-bottom-left-radius:4px}.bs-docs-featured-sites .col-sm-3:last-child img{border-top-right-radius:4px;border-bottom-right-radius:4px}}.bs-examples .thumbnail{margin-bottom:10px}.bs-examples h4{margin-bottom:5px}.bs-examples p{margin-bottom:20px}@media (max-width:480px){.bs-examples{margin-right:-10px;margin-left:-10px}.bs-examples>[class^=col-]{padding-right:10px;padding-left:10px}}.bs-docs-sidebar.affix{position:static}@media (min-width:768px){.bs-docs-sidebar{padding-left:20px}}.bs-docs-sidenav{margin-top:50px;margin-bottom:20px}.bs-docs-sidebar .nav>li>a{display:block;padding:5px 20px;font-size:13px;font-weight:500;color:#222}.bs-docs-sidebar .nav>li>a:focus,.bs-docs-sidebar .nav>li>a:hover{text-decoration:none;background-color:#eee}.bs-docs-sidebar .nav>.active:focus>a,.bs-docs-sidebar .nav>.active:hover>a,.bs-docs-sidebar .nav>.active>a{color:#dd4b39;background-color:transparent}.bs-docs-sidebar .nav .nav{display:none;margin-bottom:8px}.bs-docs-sidebar .nav .nav>li>a{padding-top:1px;padding-bottom:1px;padding-left:30px;font-size:12px}.back-to-top,.bs-docs-theme-toggle{display:none;padding:4px 10px;margin-top:10px;margin-left:10px;font-size:12px;font-weight:500;color:#999}.back-to-top:hover,.bs-docs-theme-toggle:hover{color:#563d7c;text-decoration:none}.bs-docs-theme-toggle{margin-top:0}@media (min-width:768px){.back-to-top,.bs-docs-theme-toggle{display:block}}@media (min-width:992px){.bs-docs-sidebar .nav>.active>ul{display:block}.bs-docs-sidebar.affix,.bs-docs-sidebar.affix-bottom{width:213px}.bs-docs-sidebar.affix{position:fixed;top:80px}.bs-docs-sidebar.affix-bottom{position:absolute}.bs-docs-sidebar.affix .bs-docs-sidenav,.bs-docs-sidebar.affix-bottom .bs-docs-sidenav{margin-top:0;margin-bottom:0}}@media (min-width:1200px){.bs-docs-sidebar.affix,.bs-docs-sidebar.affix-bottom{width:263px}}.bs-docs-section{margin-bottom:60px}.bs-docs-section:last-child{margin-bottom:0}h1[id]{padding-top:20px;margin-top:0}.bs-callout{padding:20px;margin:20px 0;border:1px solid #eee;border-left-width:5px;border-radius:3px}.bs-callout h4{margin-top:0;margin-bottom:5px}.bs-callout p:last-child{margin-bottom:0}.bs-callout code{border-radius:3px}.bs-callout+.bs-callout{margin-top:-5px}.bs-callout-danger{border-left-color:#dd4b39}.bs-callout-danger h4{color:#c23321}.bs-callout-warning{border-left-color:#f1e7bc}.bs-callout-warning h4{color:#ba9e27}.bs-callout-info{border-left-color:#d0e3f0}.bs-callout-info h4{color:#3b86b9}.color-swatches{margin:0 -5px;overflow:hidden}.color-swatch{float:left;width:60px;height:60px;margin:0 5px;border-radius:3px}@media (min-width:768px){.color-swatch{width:100px;height:100px}}.color-swatches .gray-darker{background-color:#222}.color-swatches .gray-dark{background-color:#333}.color-swatches .gray{background-color:#555}.color-swatches .gray-light{background-color:#999}.color-swatches .gray-lighter{background-color:#eee}.color-swatches .brand-primary{background-color:#4d90fe}.color-swatches .brand-success{background-color:#35aa47}.color-swatches .brand-warning{background-color:#faa937}.color-swatches .brand-danger{background-color:#d84a38}.color-swatches .brand-info{background-color:#5bc0de}.color-swatches .bs-purple{background-color:#1b6ec1}.color-swatches .bs-purple-light{background-color:#c7bfd3}.color-swatches .bs-purple-lighter{background-color:#e5e1ea}.color-swatches .bs-gray{background-color:#f9f9f9}.bs-team .team-member{line-height:32px;color:#555}.bs-team .team-member:hover{color:#333;text-decoration:none}.bs-team .github-btn{float:right;width:180px;height:20px;margin-top:6px;border:none}.bs-team img{float:left;width:32px;margin-right:10px;border-radius:4px}.bs-docs-browser-bugs td p{margin-bottom:0}.bs-docs-browser-bugs th:first-child{width:18%}.show-grid{margin-bottom:15px}.show-grid [class^=col-]{padding-top:10px;padding-bottom:10px;background-color:#f9f9f9;border:1px solid #ddd}.bs-example{position:relative;padding:45px 15px 15px;margin:0 -15px 15px;border-color:#e5e5e5 #eee #eee;border-style:solid;border-width:1px 0;-webkit-box-shadow:inset 0 3px 6px rgba(0,0,0,.05);box-shadow:inset 0 3px 6px rgba(0,0,0,.05)}.bs-example:after{position:absolute;top:15px;left:15px;font-size:12px;font-weight:700;color:#959595;text-transform:uppercase;letter-spacing:1px;content:"Example"}.bs-example-padded-bottom{padding-bottom:24px}.bs-example+.highlight,.bs-example+.zero-clipboard+.highlight{margin:-15px -15px 15px;border-width:0 0 1px;border-radius:0}@media (min-width:768px){.bs-example{margin-right:0;margin-left:0;background-color:#fff;border-color:#ddd;border-width:1px;border-radius:4px 4px 0 0;-webkit-box-shadow:none;box-shadow:none}.bs-example+.highlight,.bs-example+.zero-clipboard+.highlight{margin-top:-16px;margin-right:0;margin-left:0;border-width:1px;border-bottom-right-radius:4px;border-bottom-left-radius:4px}.bs-example-standalone{border-radius:4px}}.bs-example .container{width:auto}.bs-example>.alert:last-child,.bs-example>.form-control:last-child,.bs-example>.jumbotron:last-child,.bs-example>.list-group:last-child,.bs-example>.navbar:last-child,.bs-example>.panel:last-child,.bs-example>.progress:last-child,.bs-example>.table-responsive:last-child>.table,.bs-example>.table:last-child,.bs-example>.well:last-child,.bs-example>blockquote:last-child,.bs-example>ol:last-child,.bs-example>p:last-child,.bs-example>ul:last-child{margin-bottom:0}.bs-example>p>.close{float:none}.bs-example-type .table .type-info{color:#999;vertical-align:middle}.bs-example-type .table td{padding:15px 0;border-color:#eee}.bs-example-type .table tr:first-child td{border-top:0}.bs-example-type h1,.bs-example-type h2,.bs-example-type h3,.bs-example-type h4,.bs-example-type h5,.bs-example-type h6{margin:0}.bs-example-bg-classes p{padding:15px}.bs-example>.img-circle,.bs-example>.img-rounded,.bs-example>.img-thumbnail{margin:5px}.bs-example>.table-responsive>.table{background-color:#fff}.bs-example>.btn,.bs-example>.btn-group{margin-top:5px;margin-bottom:5px}.bs-example>.btn-toolbar+.btn-toolbar{margin-top:10px}.bs-example .select2-container.form-control,.bs-example-control-sizing input[type=text]+input[type=text],.bs-example-control-sizing select{margin-top:10px}.bs-example-form .input-group{margin-bottom:10px}.bs-example>textarea.form-control{resize:vertical}.bs-example>.list-group{max-width:400px}.bs-example .navbar:last-child{margin-bottom:0}.bs-navbar-bottom-example,.bs-navbar-top-example{z-index:1;padding:0;overflow:hidden}.bs-navbar-bottom-example .navbar-header,.bs-navbar-top-example .navbar-header{margin-left:0}.bs-navbar-bottom-example .navbar-fixed-bottom,.bs-navbar-top-example .navbar-fixed-top{position:relative;margin-right:0;margin-left:0}.bs-navbar-top-example{padding-bottom:90px}.bs-navbar-top-example:after{top:auto;bottom:15px}.bs-navbar-top-example .navbar-fixed-top{top:-1px}.bs-navbar-bottom-example{padding-top:90px}.bs-navbar-bottom-example .navbar-fixed-bottom{bottom:-1px}.bs-navbar-bottom-example .navbar{margin-bottom:0}@media (min-width:768px){.bs-navbar-bottom-example .navbar-fixed-bottom,.bs-navbar-top-example .navbar-fixed-top{position:absolute}}.bs-example .pagination{margin-top:10px;margin-bottom:10px}.bs-example>.pager{margin-top:0}.bs-example>.scrollable{height:200px;overflow-y:auto}.bs-example-modal{background-color:#f5f5f5}.bs-example-modal .modal{position:relative;top:auto;right:auto;bottom:auto;left:auto;z-index:1;display:block}.bs-example-modal .modal-dialog{left:auto;margin-right:auto;margin-left:auto}.bs-example .dropup>.dropdown-toggle,.bs-example>.dropdown>.dropdown-toggle{float:left}.bs-example-submenu .dropdown>.dropdown-menu,.bs-example-submenu .dropup>.dropdown-menu,.bs-example>.dropdown>.dropdown-menu{position:static;display:block;margin-bottom:5px;clear:left}.bs-example-submenu .dropdown-menu{margin-right:20px}.bs-example-tabs .nav-tabs{margin-bottom:15px}.bs-example-tooltips{text-align:center}.bs-example-tooltips>.btn{margin-top:5px;margin-bottom:5px}.bs-example-tooltip .tooltip{position:relative;display:inline-block;margin:10px 20px;opacity:1}.bs-example-popover{padding-bottom:24px;background-color:#f9f9f9}.bs-example-popover .popover{position:relative;display:block;float:left;width:260px;margin:20px}.scrollspy-example{position:relative;height:200px;margin-top:10px;overflow:auto}.bs-example>.nav-pills-stacked-example{max-width:300px}#collapseExample .well{margin-bottom:0}.bs-events-table>tbody>tr>td:first-child,.bs-events-table>thead>tr>th:first-child{white-space:nowrap}.bs-events-table>thead>tr>th:first-child{width:150px}.js-options-table>thead>tr>th:nth-child(1),.js-options-table>thead>tr>th:nth-child(2){width:100px}.js-options-table>thead>tr>th:nth-child(3){width:50px}.highlight{padding:9px 14px;margin-bottom:14px;background-color:#f7f7f9;border:1px solid #e1e1e8;border-radius:4px}.highlight pre{padding:0;margin-top:0;margin-bottom:0;word-break:normal;white-space:nowrap;background-color:transparent;border:0}.highlight pre code{font-size:inherit;color:#333}.highlight pre code:first-child{display:inline-block;padding-right:45px}.table-responsive .highlight pre{white-space:normal}.bs-table th small,.responsive-utilities th small{display:block;font-weight:400;color:#999}.responsive-utilities tbody th{font-weight:400}.responsive-utilities td{text-align:center}.responsive-utilities td.is-visible{color:#468847;background-color:#dff0d8!important}.responsive-utilities td.is-hidden{color:#ccc;background-color:#f9f9f9!important}.responsive-utilities-test{margin-top:5px}.responsive-utilities-test .col-xs-6{margin-bottom:10px}.responsive-utilities-test span{display:block;padding:15px 10px;font-size:14px;font-weight:700;line-height:1.1;text-align:center;border-radius:4px}.hidden-on .col-xs-6 .hidden-lg,.hidden-on .col-xs-6 .hidden-md,.hidden-on .col-xs-6 .hidden-sm,.hidden-on .col-xs-6 .hidden-xs,.visible-on .col-xs-6 .hidden-lg,.visible-on .col-xs-6 .hidden-md,.visible-on .col-xs-6 .hidden-sm,.visible-on .col-xs-6 .hidden-xs{color:#999;border:1px solid #ddd}.hidden-on .col-xs-6 .visible-lg-block,.hidden-on .col-xs-6 .visible-md-block,.hidden-on .col-xs-6 .visible-sm-block,.hidden-on .col-xs-6 .visible-xs-block,.visible-on .col-xs-6 .visible-lg-block,.visible-on .col-xs-6 .visible-md-block,.visible-on .col-xs-6 .visible-sm-block,.visible-on .col-xs-6 .visible-xs-block{color:#468847;background-color:#dff0d8;border:1px solid #d6e9c6}.bs-glyphicons{margin:0 -10px 20px;overflow:hidden}.bs-glyphicons-list{padding-left:0;list-style:none}.bs-glyphicons li{float:left;width:25%;height:115px;padding:10px;margin:0 -1px -1px 0;font-size:10px;line-height:1.4;text-align:center;border:1px solid #ddd}.bs-glyphicons .glyphicon{margin-top:5px;margin-bottom:10px;font-size:24px}.bs-glyphicons .glyphicon-class{display:block;text-align:center;word-wrap:break-word}.bs-glyphicons li:hover{background-color:#eee}@media (min-width:768px){.bs-glyphicons{margin-right:0;margin-left:0}.bs-glyphicons li{width:12.5%;font-size:12px}}.bs-customizer .toggle{float:right;margin-top:25px}.bs-customizer label{margin-top:10px;font-weight:500;color:#555}.bs-customizer h2{padding-top:30px;margin-top:0;margin-bottom:5px}.bs-customizer h3{margin-bottom:0}.bs-customizer h4{margin-top:15px;margin-bottom:0}.bs-customizer .bs-callout h4{margin-top:0;margin-bottom:5px}.bs-customizer input[type=text]{font-family:Menlo,Monaco,Consolas,"Courier New",monospace;background-color:#fafafa}.bs-customizer .help-block{margin-bottom:5px;font-size:12px}#less-section label{font-weight:400}.bs-customize-download .btn-outline{padding:20px}.bs-customizer-alert{position:fixed;top:0;right:0;left:0;z-index:1030;padding:15px 0;color:#fff;background-color:#d9534f;border-bottom:1px solid #b94441;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.25);box-shadow:inset 0 1px 0 rgba(255,255,255,.25)}.bs-customizer-alert .close{margin-top:-4px;font-size:24px}.bs-customizer-alert p{margin-bottom:0}.bs-customizer-alert .glyphicon{margin-right:5px}.bs-customizer-alert pre{margin:10px 0 0;color:#fff;background-color:#a83c3a;border-color:#973634;-webkit-box-shadow:inset 0 2px 4px rgba(0,0,0,.05),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 2px 4px rgba(0,0,0,.05),0 1px 0 rgba(255,255,255,.1)}.bs-dropzone{position:relative;padding:20px;margin-bottom:20px;color:#777;text-align:center;border:2px dashed #eee;border-radius:4px}.bs-dropzone .import-header{margin-bottom:5px}.bs-dropzone .glyphicon-download-alt{font-size:40px}.bs-dropzone hr{width:100px}.bs-dropzone .lead{margin-bottom:10px;font-weight:400;color:#333}#import-manual-trigger{cursor:pointer}.bs-dropzone p:last-child{margin-bottom:0}.bs-brand-logos{display:table;width:100%;margin-bottom:15px;overflow:hidden;color:#1b6ec1;background-color:#f9f9f9;border-radius:4px}.bs-brand-item{padding:60px 0;text-align:center}.bs-brand-item+.bs-brand-item{border-top:1px solid #fff}.bs-brand-logos .inverse{color:#fff;background-color:#1b6ec1}.bs-brand-item h1,.bs-brand-item h3{margin-top:0;margin-bottom:0}.bs-brand-item .bs-docs-booticon{margin-right:auto;margin-left:auto}.bs-brand-item .glyphicon{width:30px;height:30px;margin:10px auto -10px;line-height:30px;color:#fff;border-radius:50%}.bs-brand-item .glyphicon-ok{background-color:#5cb85c}.bs-brand-item .glyphicon-remove{background-color:#d9534f}@media (min-width:768px){.bs-brand-item{display:table-cell;width:1%}.bs-brand-item+.bs-brand-item{border-top:0;border-left:1px solid #fff}.bs-brand-item h1{font-size:60px}}.zero-clipboard{position:relative;display:none}.btn-clipboard{position:absolute;top:0;right:0;z-index:10;display:block;padding:5px 8px;font-size:12px;color:#777;cursor:pointer;background-color:#fff;border:1px solid #e1e1e8;border-radius:0 4px 0 4px}.btn-clipboard-hover{color:#fff;background-color:#563d7c;border-color:#563d7c}@media (min-width:768px){.zero-clipboard{display:block}.bs-example+.zero-clipboard .btn-clipboard{top:-16px;border-top-right-radius:0}}.anchorjs-link{color:inherit}@media (max-width:480px){.anchorjs-link{display:none}}:hover>.anchorjs-link{opacity:.75;-webkit-transition:color .16s linear;-o-transition:color .16s linear;transition:color .16s linear}.anchorjs-link:focus,:hover>.anchorjs-link:hover{text-decoration:none;opacity:1}#focusedInput{border:1px solid #4d90fe!important;outline:0;outline:thin dotted\9;-webkit-box-shadow:none;box-shadow:none}.v4-tease{position:fixed;top:0;right:0;left:0;z-index:1030;display:block;padding:15px 20px;font-weight:700;color:#fff;text-align:center;background-color:#1b6ec1}.v4-tease:hover{color:#fff;text-decoration:none;background-color:#2d87e2}@media print{a[href]:after{content:""!important}}.bs-docs-navbar-masthead{top:48px}.bs-docs-dl-options h4{margin-top:15px;margin-bottom:5px}
+/*# sourceMappingURL=docs.min.css.map */
\ No newline at end of file
diff --git a/doc/第7周代码注释/blog/static/assets/css/ie10-viewport-bug-workaround.css b/doc/第7周代码注释/blog/static/assets/css/ie10-viewport-bug-workaround.css
new file mode 100644
index 00000000..4b9518e2
--- /dev/null
+++ b/doc/第7周代码注释/blog/static/assets/css/ie10-viewport-bug-workaround.css
@@ -0,0 +1,13 @@
+/*!
+ * 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; }
diff --git a/doc/第7周代码注释/blog/static/assets/css/signin.css b/doc/第7周代码注释/blog/static/assets/css/signin.css
new file mode 100644
index 00000000..121fb0d8
--- /dev/null
+++ b/doc/第7周代码注释/blog/static/assets/css/signin.css
@@ -0,0 +1,58 @@
+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;
+}
diff --git a/doc/第7周代码注释/blog/static/assets/css/todc-bootstrap.min.css b/doc/第7周代码注释/blog/static/assets/css/todc-bootstrap.min.css
new file mode 100644
index 00000000..66c9cb21
--- /dev/null
+++ b/doc/第7周代码注释/blog/static/assets/css/todc-bootstrap.min.css
@@ -0,0 +1,6 @@
+/*!
+ * TODC Bootstrap v3.3.7-3.3.7 (http://todc.github.com/todc-bootstrap/)
+ * Copyright 2011-2016 Tim O'Donnell
+ * Licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) license
+ */.panel-group .panel-heading a.collapsed:before,.panel-group .panel-heading a:before{position:relative;top:1px;display:inline-block;font-family:'Glyphicons Halflings';font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.caret-left,.caret-right,.collapse-caret.collapsed:before,.collapse-caret:before,.dropdown-submenu>a:after{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px dashed;border-top:4px solid\9;border-right:4px solid transparent;border-left:4px solid transparent}body{font-family:Arial,Helvetica,sans-serif;font-size:13px;line-height:1.4;color:#333;background-color:#fff}button,input,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#15c}a:focus,a:hover{color:#15c}.img-rounded{border-radius:1px}.img-thumbnail{display:inline-block;max-width:100%;height:auto;padding:0;line-height:1.4;background-color:#fff;border:3px solid #fff;border-radius:0;-webkit-box-shadow:0 0 0 1px #aaa;box-shadow:0 0 0 1px #aaa;-webkit-transition:none;-o-transition:none;transition:none}.caret-left,.caret-right,.collapse-caret.collapsed:before,.dropdown-submenu>a:after{vertical-align:baseline;border-top:4px solid transparent;border-right:0 dotted;border-bottom:4px solid transparent;border-left:4px solid}.caret-left{margin-right:2px;margin-left:0;border-right:4px solid;border-left:0 dotted}.scrollable-shadow{background:-webkit-gradient(linear,left top,left bottom,color-stop(30%,#fff),to(rgba(255,255,255,0))),-webkit-gradient(linear,left top,left bottom,from(rgba(255,255,255,0)),color-stop(70%,#fff)) 0 100%,radial-gradient(50% 0,farthest-side,rgba(0,0,0,.2),rgba(0,0,0,0)),radial-gradient(50% 100%,farthest-side,rgba(0,0,0,.2),rgba(0,0,0,0)) 0 100%;background:-webkit-linear-gradient(white 30%,rgba(255,255,255,0)),-webkit-linear-gradient(rgba(255,255,255,0),#fff 70%) 0 100%,-webkit-radial-gradient(50% 0,farthest-side,rgba(0,0,0,.2),rgba(0,0,0,0)),-webkit-radial-gradient(50% 100%,farthest-side,rgba(0,0,0,.2),rgba(0,0,0,0)) 0 100%;background:-o-linear-gradient(white 30%,rgba(255,255,255,0)),-o-linear-gradient(rgba(255,255,255,0),#fff 70%) 0 100%,-o-radial-gradient(50% 0,farthest-side,rgba(0,0,0,.2),rgba(0,0,0,0)),-o-radial-gradient(50% 100%,farthest-side,rgba(0,0,0,.2),rgba(0,0,0,0)) 0 100%;background:linear-gradient(white 30%,rgba(255,255,255,0)),linear-gradient(rgba(255,255,255,0),#fff 70%) 0 100%,radial-gradient(50% 0,farthest-side,rgba(0,0,0,.2),rgba(0,0,0,0)),radial-gradient(50% 100%,farthest-side,rgba(0,0,0,.2),rgba(0,0,0,0)) 0 100%;background:-webkit-gradient(linear,left top,left bottom,color-stop(30%,#fff),to(rgba(255,255,255,0))),-webkit-gradient(linear,left top,left bottom,from(rgba(255,255,255,0)),color-stop(70%,#fff)) 0 100%,radial-gradient(farthest-side at 50% 0,rgba(0,0,0,.2),rgba(0,0,0,0)),radial-gradient(farthest-side at 50% 100%,rgba(0,0,0,.2),rgba(0,0,0,0)) 0 100%;background:linear-gradient(white 30%,rgba(255,255,255,0)),linear-gradient(rgba(255,255,255,0),#fff 70%) 0 100%,radial-gradient(farthest-side at 50% 0,rgba(0,0,0,.2),rgba(0,0,0,0)),radial-gradient(farthest-side at 50% 100%,rgba(0,0,0,.2),rgba(0,0,0,0)) 0 100%;background-repeat:no-repeat;background-attachment:local,local,scroll,scroll;-webkit-background-size:100% 40px,100% 40px,100% 6px,100% 6px;background-size:100% 40px,100% 40px,100% 6px,100% 6px}.mark,mark{background-color:#f9edbe}.text-primary{color:#4d90fe}a.text-primary:focus,a.text-primary:hover{color:#1a70fe}.text-warning{color:#333}a.text-warning:focus,a.text-warning:hover{color:#1a1a1a}.bg-primary{color:#fff;background-color:#4d90fe}a.bg-primary:focus,a.bg-primary:hover{background-color:#1a70fe}.bg-warning{background-color:#f9edbe}a.bg-warning:focus,a.bg-warning:hover{background-color:#f5e08f}code{padding:2px 4px;border-radius:0}kbd{border-radius:1px}pre{padding:9px;margin:0 0 9px;font-size:12px;line-height:1.4;border-radius:0}table{background-color:transparent}caption{color:#999}.table{margin-bottom:18px}.table>tbody>tr>td,.table>tbody>tr>th,.table>tfoot>tr>td,.table>tfoot>tr>th,.table>thead>tr>td,.table>thead>tr>th{line-height:1.4;border-top:1px solid #ddd}.table>thead>tr>th{border-bottom:2px solid #ddd}.table>tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-bordered{border:1px solid #ddd}.table-bordered>tbody>tr>td,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>td,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border:1px solid #ddd}.table-striped>tbody>tr:nth-of-type(odd){background-color:#f9f9f9}.table-hover>tbody>tr:hover{background-color:#ffc}.table>tbody>tr.warning>td,.table>tbody>tr.warning>th,.table>tbody>tr>td.warning,.table>tbody>tr>th.warning,.table>tfoot>tr.warning>td,.table>tfoot>tr.warning>th,.table>tfoot>tr>td.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>thead>tr.warning>th,.table>thead>tr>td.warning,.table>thead>tr>th.warning{background-color:#f9edbe}.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr.warning:hover>th,.table-hover>tbody>tr:hover>.warning,.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover{background-color:#f7e7a7}@media screen and (max-width:767px){.table-responsive{margin-bottom:13.5px;border:1px solid #ddd}}legend{margin-bottom:18px;font-size:19.5px}input[type=radio],input[type=checkbox]{margin:2px 0 0}output{padding-top:6px;font-size:13px;line-height:1.4;color:#555}.form-control{height:30px;-webkit-appearance:none;padding:5px 8px;font-size:13px;line-height:1.4;background-color:#fff;border:1px solid #d9d9d9;border-top-color:silver;border-radius:2px;-webkit-box-shadow:none;box-shadow:none;-webkit-transition:none;-o-transition:none;transition:none}.form-control:hover{border:1px solid #b9b9b9;border-top-color:#a0a0a0;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.1);box-shadow:inset 0 1px 2px rgba(0,0,0,.1)}.form-control:focus{border-color:#4d90fe;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(77,144,254,.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(77,144,254,.6)}.form-control:focus{-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.3);box-shadow:inset 0 1px 2px rgba(0,0,0,.3)}.form-control::-ms-expand{background-color:transparent}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{background-color:#f1f1f1;border:1px solid #e5e5e5}.form-control[disabled]:active,.form-control[disabled]:focus,.form-control[disabled]:hover,.form-control[readonly]:active,.form-control[readonly]:focus,.form-control[readonly]:hover,fieldset[disabled] .form-control:active,fieldset[disabled] .form-control:focus,fieldset[disabled] .form-control:hover{border:1px solid #e5e5e5;-webkit-box-shadow:none;box-shadow:none}.form-control[readonly] .form-control{border:1px solid #d9d9d9}.form-control[readonly] .form-control:active,.form-control[readonly] .form-control:focus,.form-control[readonly] .form-control:hover{border:1px solid #d9d9d9}textarea.form-control{padding-right:4px}@media screen and (-webkit-min-device-pixel-ratio:0){input[type=date].form-control,input[type=time].form-control,input[type=datetime-local].form-control,input[type=month].form-control{line-height:30px}.input-group-sm input[type=date],.input-group-sm input[type=time],.input-group-sm input[type=datetime-local],.input-group-sm input[type=month],input[type=date].input-sm,input[type=time].input-sm,input[type=datetime-local].input-sm,input[type=month].input-sm{line-height:26px}.input-group-lg input[type=date],.input-group-lg input[type=time],.input-group-lg input[type=datetime-local],.input-group-lg input[type=month],input[type=date].input-lg,input[type=time].input-lg,input[type=datetime-local].input-lg,input[type=month].input-lg{line-height:38px}}.checkbox label,.radio label{min-height:18px}.checkbox input[type=checkbox],.checkbox-inline input[type=checkbox],.radio input[type=radio],.radio-inline input[type=radio],input[type=radio],input[type=checkbox]{position:relative;width:13px;width:16px\9;height:13px;height:16px\9;-webkit-appearance:none;background:#fff;border:1px solid #dcdcdc;border:1px solid transparent\9;border-radius:1px}.checkbox input[type=checkbox]:focus,.checkbox-inline input[type=checkbox]:focus,.radio input[type=radio]:focus,.radio-inline input[type=radio]:focus,input[type=radio]:focus,input[type=checkbox]:focus{border-color:#4d90fe;outline:0}.checkbox input[type=checkbox]:active,.checkbox-inline input[type=checkbox]:active,.radio input[type=radio]:active,.radio-inline input[type=radio]:active,input[type=radio]:active,input[type=checkbox]:active{background-color:#ebebeb;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffffffff', GradientType=0);border-color:#c6c6c6}.checkbox input[type=checkbox]:checked,.checkbox-inline input[type=checkbox]:checked,.radio input[type=radio]:checked,.radio-inline input[type=radio]:checked,input[type=radio]:checked,input[type=checkbox]:checked{background:#fff}.radio input[type=radio],.radio-inline input[type=radio],input[type=radio]{width:15px;width:18px\9;height:15px;height:18px\9;border-radius:1em}.radio input[type=radio]:checked::after,.radio-inline input[type=radio]:checked::after,input[type=radio]:checked::after{position:relative;top:3px;left:3px;display:block;width:7px;height:7px;content:'';background:#666;border-radius:1em}.checkbox input[type=checkbox]:hover,.checkbox-inline input[type=checkbox]:hover,input[type=checkbox]:hover{border-color:#c6c6c6;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.1);-webkit-box-shadow:none\9;box-shadow:inset 0 1px 1px rgba(0,0,0,.1);box-shadow:none\9}.checkbox input[type=checkbox]:checked::after,.checkbox-inline input[type=checkbox]:checked::after,input[type=checkbox]:checked::after{position:absolute;top:-6px;left:-5px;display:block;content:url(../img/checkmark.png)}.form-control-static{min-height:31px;padding-top:6px;padding-bottom:6px}.input-sm{height:26px;padding:3px 8px;font-size:12px;line-height:1.5;border-radius:1px}select.input-sm{height:26px;line-height:26px}select[multiple].input-sm,textarea.input-sm{height:auto}.form-group-sm .form-control{height:26px;padding:3px 8px;font-size:12px;line-height:1.5;border-radius:1px}.form-group-sm select.form-control{height:26px;line-height:26px}.form-group-sm .form-control-static{height:26px;min-height:30px;padding:4px 8px;font-size:12px;line-height:1.5}.input-lg{height:38px;padding:9px 14px;font-size:14px;line-height:1.3;border-radius:1px}select.input-lg{height:38px;line-height:38px}select[multiple].input-lg,textarea.input-lg{height:auto}.form-group-lg .form-control{height:38px;padding:9px 14px;font-size:14px;line-height:1.3;border-radius:1px}.form-group-lg select.form-control{height:38px;line-height:38px}.form-group-lg .form-control-static{height:38px;min-height:32px;padding:10px 14px;font-size:14px;line-height:1.3}.has-feedback .form-control{padding-right:37.5px}.form-control-feedback{top:23px;width:30px;height:30px;line-height:30px}.form-group-lg .form-control+.form-control-feedback,.input-group-lg+.form-control-feedback,.input-lg+.form-control-feedback{width:38px;height:38px;line-height:38px}.form-group-sm .form-control+.form-control-feedback,.input-group-sm+.form-control-feedback,.input-sm+.form-control-feedback{width:26px;height:26px;line-height:26px}.has-success .checkbox,.has-success .checkbox-inline,.has-success .control-label,.has-success .help-block,.has-success .radio,.has-success .radio-inline,.has-success.checkbox label,.has-success.checkbox-inline label,.has-success.radio label,.has-success.radio-inline label{color:#3c763d}.has-success .form-control{border-color:#3c763d;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-success .form-control:focus{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168}.has-success .input-group-addon{color:#3c763d;background-color:#dff0d8;border-color:#3c763d}.has-success .form-control-feedback{color:#3c763d}.has-success .form-control{-webkit-box-shadow:none;box-shadow:none}.has-success .form-control:hover{border-color:#3c763d;-webkit-box-shadow:0 1px 2px rgba(0,0,0,.1) inset;box-shadow:0 1px 2px rgba(0,0,0,.1) inset}.has-success .form-control:focus{border-color:#3c763d;-webkit-box-shadow:0 1px 2px rgba(0,0,0,.3) inset;box-shadow:0 1px 2px rgba(0,0,0,.3) inset}.has-warning .checkbox,.has-warning .checkbox-inline,.has-warning .control-label,.has-warning .help-block,.has-warning .radio,.has-warning .radio-inline,.has-warning.checkbox label,.has-warning.checkbox-inline label,.has-warning.radio label,.has-warning.radio-inline label{color:#e09b17}.has-warning .form-control{border-color:#e09b17;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-warning .form-control:focus{border-color:#b27b12;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #f0c36d;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #f0c36d}.has-warning .input-group-addon{color:#e09b17;background-color:#f9edbe;border-color:#e09b17}.has-warning .form-control-feedback{color:#e09b17}.has-warning .form-control{-webkit-box-shadow:none;box-shadow:none}.has-warning .form-control:hover{border-color:#e09b17;-webkit-box-shadow:0 1px 2px rgba(0,0,0,.1) inset;box-shadow:0 1px 2px rgba(0,0,0,.1) inset}.has-warning .form-control:focus{border-color:#e09b17;-webkit-box-shadow:0 1px 2px rgba(0,0,0,.3) inset;box-shadow:0 1px 2px rgba(0,0,0,.3) inset}.has-error .checkbox,.has-error .checkbox-inline,.has-error .control-label,.has-error .help-block,.has-error .radio,.has-error .radio-inline,.has-error.checkbox label,.has-error.checkbox-inline label,.has-error.radio label,.has-error.radio-inline label{color:#dd4b39}.has-error .form-control{border-color:#dd4b39;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-error .form-control:focus{border-color:#c23321;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ec9a90;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ec9a90}.has-error .input-group-addon{color:#dd4b39;background-color:#f2dede;border-color:#dd4b39}.has-error .form-control-feedback{color:#dd4b39}.has-error .form-control{-webkit-box-shadow:none;box-shadow:none}.has-error .form-control:hover{border-color:#dd4b39;-webkit-box-shadow:0 1px 2px rgba(0,0,0,.1) inset;box-shadow:0 1px 2px rgba(0,0,0,.1) inset}.has-error .form-control:focus{border-color:#dd4b39;-webkit-box-shadow:0 1px 2px rgba(0,0,0,.3) inset;box-shadow:0 1px 2px rgba(0,0,0,.3) inset}.has-feedback label~.form-control-feedback{top:23px}.help-block{color:#777}.form-horizontal .checkbox-inline,.form-horizontal .control-label,.form-horizontal .radio-inline{padding-top:5px}@media (min-width:768px){.form-inline .form-group,.navbar-form .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control,.navbar-form .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-static,.navbar-form .form-control-static{display:inline-block}.form-inline .input-group,.navbar-form .input-group{display:inline-table;vertical-align:middle}.form-inline .input-group .form-control,.form-inline .input-group .input-group-addon,.form-inline .input-group .input-group-btn,.navbar-form .input-group .form-control,.navbar-form .input-group .input-group-addon,.navbar-form .input-group .input-group-btn{width:auto}.form-inline .input-group>.form-control,.navbar-form .input-group>.form-control{width:100%}.form-inline .control-label,.navbar-form .control-label{margin-bottom:0;vertical-align:middle}.form-inline .checkbox,.form-inline .radio,.navbar-form .checkbox,.navbar-form .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.form-inline .checkbox label,.form-inline .radio label,.navbar-form .checkbox label,.navbar-form .radio label{padding-left:0}.form-inline .checkbox input[type=checkbox],.form-inline .radio input[type=radio],.navbar-form .checkbox input[type=checkbox],.navbar-form .radio input[type=radio]{position:relative;margin-bottom:-2px;margin-left:0}.form-inline .has-feedback .form-control-feedback,.navbar-form .has-feedback .form-control-feedback{top:0}}.form-horizontal .checkbox,.form-horizontal .checkbox-inline,.form-horizontal .radio,.form-horizontal .radio-inline{padding-top:6px}.form-horizontal .checkbox,.form-horizontal .radio{min-height:24px}@media (min-width:768px){.form-horizontal .control-label{padding-top:6px}.form-horizontal .has-feedback .form-control-feedback{top:0}}@media (min-width:768px){.form-horizontal .form-group-lg .control-label{padding-top:10px;font-size:14px}}@media (min-width:768px){.form-horizontal .form-group-sm .control-label{padding-top:4px;font-size:12px}}.btn{padding:5px 12px;font-size:13px;font-weight:700;line-height:18px;cursor:default;-webkit-background-clip:border-box;background-clip:border-box;border-radius:2px;-webkit-box-shadow:none;box-shadow:none}.btn:hover{-webkit-box-shadow:0 1px 1px rgba(0,0,0,.1);box-shadow:0 1px 1px rgba(0,0,0,.1)}.btn.active,.btn:active{-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.1);box-shadow:inset 0 1px 2px rgba(0,0,0,.1)}.btn-default{color:#333;text-shadow:0 1px rgba(0,0,0,.1);text-shadow:0 1px 0 #fff;background-color:#f3f3f3;background-image:-webkit-linear-gradient(top,#f5f5f5 0,#f1f1f1 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#f1f1f1 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#f1f1f1));background-image:linear-gradient(to bottom,#f5f5f5 0,#f1f1f1 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#fff1f1f1', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border:1px solid #dcdcdc}.btn-default:hover{text-shadow:0 1px rgba(0,0,0,.3);-webkit-box-shadow:0 1px 1px rgba(0,0,0,.2);box-shadow:0 1px 1px rgba(0,0,0,.2)}.btn-default.active,.btn-default.focus,.btn-default:active,.btn-default:focus,.btn-default:hover,.open>.dropdown-toggle.btn-default{color:#333;background-color:#e4e4e4;background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e4e4e4 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#e4e4e4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#e4e4e4));background-image:linear-gradient(to bottom,#f5f5f5 0,#e4e4e4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe4e4e4', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border:1px solid #cfcfcf}.btn-default.active,.btn-default:active,.open>.dropdown-toggle.btn-default{text-shadow:0 1px rgba(0,0,0,.3);background-image:-webkit-linear-gradient(top,#f5f5f5 0,#d8d8d8 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#d8d8d8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#d8d8d8));background-image:linear-gradient(to bottom,#f5f5f5 0,#d8d8d8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffd8d8d8', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border:1px solid #c3c3c3;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.3);box-shadow:inset 0 1px 2px rgba(0,0,0,.3)}.btn-default.focus,.btn-default:focus{border:1px solid #dcdcdc;-webkit-box-shadow:inset 0 0 0 1px #fff;box-shadow:inset 0 0 0 1px #fff}.btn-default.disabled.focus,.btn-default.disabled:focus,.btn-default.disabled:hover,.btn-default[disabled].focus,.btn-default[disabled]:focus,.btn-default[disabled]:hover,fieldset[disabled] .btn-default.focus,fieldset[disabled] .btn-default:focus,fieldset[disabled] .btn-default:hover{background-color:#f5f5f5;background-image:-webkit-linear-gradient(top,#f5f5f5 0,#f1f1f1 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#f1f1f1 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#f1f1f1));background-image:linear-gradient(to bottom,#f5f5f5 0,#f1f1f1 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#fff1f1f1', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border:1px solid #dcdcdc;-webkit-box-shadow:none;box-shadow:none}.btn-default .badge{color:#dcdcdc;background-color:#333}.btn-default:hover{text-shadow:none;background-image:-webkit-linear-gradient(top,#f8f8f8 0,#f1f1f1 100%);background-image:-o-linear-gradient(top,#f8f8f8 0,#f1f1f1 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f8f8f8),to(#f1f1f1));background-image:linear-gradient(to bottom,#f8f8f8 0,#f1f1f1 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff8f8f8', endColorstr='#fff1f1f1', GradientType=0);background-repeat:repeat-x;background-position:0 0;border-color:#c6c6c6;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.1);box-shadow:0 1px 1px rgba(0,0,0,.1);-webkit-transition:none;-o-transition:none;transition:none}.btn-default.active,.btn-default:active,.open .dropdown-toggle.btn-default{text-shadow:0 1px 0 #fff;background-color:#e8e8e8;background-image:-webkit-linear-gradient(top,#f6f6f6 0,#f1f1f1 100%);background-image:-o-linear-gradient(top,#f6f6f6 0,#f1f1f1 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f6f6f6),to(#f1f1f1));background-image:linear-gradient(to bottom,#f6f6f6 0,#f1f1f1 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff6f6f6', endColorstr='#fff1f1f1', GradientType=0);background-repeat:repeat-x;border:1px solid #dcdcdc;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.1);box-shadow:inset 0 1px 2px rgba(0,0,0,.1)}.btn-default.focus,.btn-default:focus{background-color:#f3f3f3;border-color:#4d90fe;outline-style:none}.btn-default.disabled.focus,.btn-default.disabled:active,.btn-default.disabled:focus,.btn-default.disabled:hover,.btn-default[disabled].focus,.btn-default[disabled]:active,.btn-default[disabled]:focus,.btn-default[disabled]:hover,fieldset[disabled] .btn-default.focus,fieldset[disabled] .btn-default:active,fieldset[disabled] .btn-default:focus,fieldset[disabled] .btn-default:hover{text-shadow:none;background-color:#f3f3f3}.btn-default .badge{color:#f3f3f3;text-shadow:none}.btn-primary{color:#fff;text-shadow:0 1px rgba(0,0,0,.1);background-image:-webkit-linear-gradient(top,#4d90fe 0,#4787ed 100%);background-image:-o-linear-gradient(top,#4d90fe 0,#4787ed 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#4d90fe),to(#4787ed));background-image:linear-gradient(to bottom,#4d90fe 0,#4787ed 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff4d90fe', endColorstr='#ff4787ed', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border:1px solid #3079ed}.btn-primary:hover{text-shadow:0 1px rgba(0,0,0,.3);-webkit-box-shadow:0 1px 1px rgba(0,0,0,.2);box-shadow:0 1px 1px rgba(0,0,0,.2)}.btn-primary.active,.btn-primary.focus,.btn-primary:active,.btn-primary:focus,.btn-primary:hover,.open>.dropdown-toggle.btn-primary{color:#fff;background-color:#3078eb;background-image:-webkit-linear-gradient(top,#4d90fe 0,#3078eb 100%);background-image:-o-linear-gradient(top,#4d90fe 0,#3078eb 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#4d90fe),to(#3078eb));background-image:linear-gradient(to bottom,#4d90fe 0,#3078eb 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff4d90fe', endColorstr='#ff3078eb', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border:1px solid #196aeb}.btn-primary.active,.btn-primary:active,.open>.dropdown-toggle.btn-primary{text-shadow:0 1px rgba(0,0,0,.3);background-image:-webkit-linear-gradient(top,#4d90fe 0,#1969e8 100%);background-image:-o-linear-gradient(top,#4d90fe 0,#1969e8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#4d90fe),to(#1969e8));background-image:linear-gradient(to bottom,#4d90fe 0,#1969e8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff4d90fe', endColorstr='#ff1969e8', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border:1px solid #135fd7;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.3);box-shadow:inset 0 1px 2px rgba(0,0,0,.3)}.btn-primary.focus,.btn-primary:focus{border:1px solid #3079ed;-webkit-box-shadow:inset 0 0 0 1px #fff;box-shadow:inset 0 0 0 1px #fff}.btn-primary.disabled.focus,.btn-primary.disabled:focus,.btn-primary.disabled:hover,.btn-primary[disabled].focus,.btn-primary[disabled]:focus,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary.focus,fieldset[disabled] .btn-primary:focus,fieldset[disabled] .btn-primary:hover{background-color:#4d90fe;background-image:-webkit-linear-gradient(top,#4d90fe 0,#4787ed 100%);background-image:-o-linear-gradient(top,#4d90fe 0,#4787ed 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#4d90fe),to(#4787ed));background-image:linear-gradient(to bottom,#4d90fe 0,#4787ed 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff4d90fe', endColorstr='#ff4787ed', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border:1px solid #3079ed;-webkit-box-shadow:none;box-shadow:none}.btn-primary .badge{color:#3079ed;background-color:#fff}.btn-success{color:#fff;text-shadow:0 1px rgba(0,0,0,.1);background-image:-webkit-linear-gradient(top,#35aa47 0,#35aa47 100%);background-image:-o-linear-gradient(top,#35aa47 0,#35aa47 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#35aa47),to(#35aa47));background-image:linear-gradient(to bottom,#35aa47 0,#35aa47 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff35aa47', endColorstr='#ff35aa47', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border:1px solid #359947}.btn-success:hover{text-shadow:0 1px rgba(0,0,0,.3);-webkit-box-shadow:0 1px 1px rgba(0,0,0,.2);box-shadow:0 1px 1px rgba(0,0,0,.2)}.btn-success.active,.btn-success.focus,.btn-success:active,.btn-success:focus,.btn-success:hover,.open>.dropdown-toggle.btn-success{color:#fff;background-color:#2f973f;background-image:-webkit-linear-gradient(top,#35aa47 0,#2f973f 100%);background-image:-o-linear-gradient(top,#35aa47 0,#2f973f 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#35aa47),to(#2f973f));background-image:linear-gradient(to bottom,#35aa47 0,#2f973f 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff35aa47', endColorstr='#ff2f973f', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border:1px solid #2e863e}.btn-success.active,.btn-success:active,.open>.dropdown-toggle.btn-success{text-shadow:0 1px rgba(0,0,0,.3);background-image:-webkit-linear-gradient(top,#35aa47 0,#298337 100%);background-image:-o-linear-gradient(top,#35aa47 0,#298337 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#35aa47),to(#298337));background-image:linear-gradient(to bottom,#35aa47 0,#298337 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff35aa47', endColorstr='#ff298337', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border:1px solid #287335;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.3);box-shadow:inset 0 1px 2px rgba(0,0,0,.3)}.btn-success.focus,.btn-success:focus{border:1px solid #359947;-webkit-box-shadow:inset 0 0 0 1px #fff;box-shadow:inset 0 0 0 1px #fff}.btn-success.disabled.focus,.btn-success.disabled:focus,.btn-success.disabled:hover,.btn-success[disabled].focus,.btn-success[disabled]:focus,.btn-success[disabled]:hover,fieldset[disabled] .btn-success.focus,fieldset[disabled] .btn-success:focus,fieldset[disabled] .btn-success:hover{background-color:#35aa47;background-image:-webkit-linear-gradient(top,#35aa47 0,#35aa47 100%);background-image:-o-linear-gradient(top,#35aa47 0,#35aa47 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#35aa47),to(#35aa47));background-image:linear-gradient(to bottom,#35aa47 0,#35aa47 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff35aa47', endColorstr='#ff35aa47', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border:1px solid #359947;-webkit-box-shadow:none;box-shadow:none}.btn-success .badge{color:#359947;background-color:#fff}.btn-info{color:#fff;text-shadow:0 1px rgba(0,0,0,.1);background-image:-webkit-linear-gradient(top,#5bc0de 0,#5bc0de 100%);background-image:-o-linear-gradient(top,#5bc0de 0,#5bc0de 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5bc0de),to(#5bc0de));background-image:linear-gradient(to bottom,#5bc0de 0,#5bc0de 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff5bc0de', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border:1px solid #46b8da}.btn-info:hover{text-shadow:0 1px rgba(0,0,0,.3);-webkit-box-shadow:0 1px 1px rgba(0,0,0,.2);box-shadow:0 1px 1px rgba(0,0,0,.2)}.btn-info.active,.btn-info.focus,.btn-info:active,.btn-info:focus,.btn-info:hover,.open>.dropdown-toggle.btn-info{color:#fff;background-color:#46b8da;background-image:-webkit-linear-gradient(top,#5bc0de 0,#46b8da 100%);background-image:-o-linear-gradient(top,#5bc0de 0,#46b8da 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5bc0de),to(#46b8da));background-image:linear-gradient(to bottom,#5bc0de 0,#46b8da 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff46b8da', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border:1px solid #31b0d5}.btn-info.active,.btn-info:active,.open>.dropdown-toggle.btn-info{text-shadow:0 1px rgba(0,0,0,.3);background-image:-webkit-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:-o-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5bc0de),to(#31b0d5));background-image:linear-gradient(to bottom,#5bc0de 0,#31b0d5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border:1px solid #28a1c5;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.3);box-shadow:inset 0 1px 2px rgba(0,0,0,.3)}.btn-info.focus,.btn-info:focus{border:1px solid #46b8da;-webkit-box-shadow:inset 0 0 0 1px #fff;box-shadow:inset 0 0 0 1px #fff}.btn-info.disabled.focus,.btn-info.disabled:focus,.btn-info.disabled:hover,.btn-info[disabled].focus,.btn-info[disabled]:focus,.btn-info[disabled]:hover,fieldset[disabled] .btn-info.focus,fieldset[disabled] .btn-info:focus,fieldset[disabled] .btn-info:hover{background-color:#5bc0de;background-image:-webkit-linear-gradient(top,#5bc0de 0,#5bc0de 100%);background-image:-o-linear-gradient(top,#5bc0de 0,#5bc0de 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5bc0de),to(#5bc0de));background-image:linear-gradient(to bottom,#5bc0de 0,#5bc0de 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff5bc0de', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border:1px solid #46b8da;-webkit-box-shadow:none;box-shadow:none}.btn-info .badge{color:#46b8da;background-color:#fff}.btn-warning{color:#fff;text-shadow:0 1px rgba(0,0,0,.1);background-image:-webkit-linear-gradient(top,#fbb450 0,#faa937 100%);background-image:-o-linear-gradient(top,#fbb450 0,#faa937 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fbb450),to(#faa937));background-image:linear-gradient(to bottom,#fbb450 0,#faa937 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffbb450', endColorstr='#fffaa937', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border:1px solid #faa328}.btn-warning:hover{text-shadow:0 1px rgba(0,0,0,.3);-webkit-box-shadow:0 1px 1px rgba(0,0,0,.2);box-shadow:0 1px 1px rgba(0,0,0,.2)}.btn-warning.active,.btn-warning.focus,.btn-warning:active,.btn-warning:focus,.btn-warning:hover,.open>.dropdown-toggle.btn-warning{color:#fff;background-color:#f99e1e;background-image:-webkit-linear-gradient(top,#fbb450 0,#f99e1e 100%);background-image:-o-linear-gradient(top,#fbb450 0,#f99e1e 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fbb450),to(#f99e1e));background-image:linear-gradient(to bottom,#fbb450 0,#f99e1e 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffbb450', endColorstr='#fff99e1e', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border:1px solid #f9980f}.btn-warning.active,.btn-warning:active,.open>.dropdown-toggle.btn-warning{text-shadow:0 1px rgba(0,0,0,.3);background-image:-webkit-linear-gradient(top,#fbb450 0,#f89306 100%);background-image:-o-linear-gradient(top,#fbb450 0,#f89306 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fbb450),to(#f89306));background-image:linear-gradient(to bottom,#fbb450 0,#f89306 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffbb450', endColorstr='#fff89306', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border:1px solid #e98b06;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.3);box-shadow:inset 0 1px 2px rgba(0,0,0,.3)}.btn-warning.focus,.btn-warning:focus{border:1px solid #faa328;-webkit-box-shadow:inset 0 0 0 1px #fff;box-shadow:inset 0 0 0 1px #fff}.btn-warning.disabled.focus,.btn-warning.disabled:focus,.btn-warning.disabled:hover,.btn-warning[disabled].focus,.btn-warning[disabled]:focus,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning.focus,fieldset[disabled] .btn-warning:focus,fieldset[disabled] .btn-warning:hover{background-color:#fbb450;background-image:-webkit-linear-gradient(top,#fbb450 0,#faa937 100%);background-image:-o-linear-gradient(top,#fbb450 0,#faa937 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fbb450),to(#faa937));background-image:linear-gradient(to bottom,#fbb450 0,#faa937 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffbb450', endColorstr='#fffaa937', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border:1px solid #faa328;-webkit-box-shadow:none;box-shadow:none}.btn-warning .badge{color:#faa328;background-color:#fff}.btn-danger{color:#fff;text-shadow:0 1px rgba(0,0,0,.1);background-image:-webkit-linear-gradient(top,#dd4b39 0,#d14836 100%);background-image:-o-linear-gradient(top,#dd4b39 0,#d14836 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dd4b39),to(#d14836));background-image:linear-gradient(to bottom,#dd4b39 0,#d14836 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdd4b39', endColorstr='#ffd14836', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border:1px solid #c6322a}.btn-danger:hover{text-shadow:0 1px rgba(0,0,0,.3);-webkit-box-shadow:0 1px 1px rgba(0,0,0,.2);box-shadow:0 1px 1px rgba(0,0,0,.2)}.btn-danger.active,.btn-danger.focus,.btn-danger:active,.btn-danger:focus,.btn-danger:hover,.open>.dropdown-toggle.btn-danger{color:#fff;background-color:#c13e2c;background-image:-webkit-linear-gradient(top,#dd4b39 0,#c13e2c 100%);background-image:-o-linear-gradient(top,#dd4b39 0,#c13e2c 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dd4b39),to(#c13e2c));background-image:linear-gradient(to bottom,#dd4b39 0,#c13e2c 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdd4b39', endColorstr='#ffc13e2c', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border:1px solid #b12d26}.btn-danger.active,.btn-danger:active,.open>.dropdown-toggle.btn-danger{text-shadow:0 1px rgba(0,0,0,.3);background-image:-webkit-linear-gradient(top,#dd4b39 0,#ad3727 100%);background-image:-o-linear-gradient(top,#dd4b39 0,#ad3727 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dd4b39),to(#ad3727));background-image:linear-gradient(to bottom,#dd4b39 0,#ad3727 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdd4b39', endColorstr='#ffad3727', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border:1px solid #9c2721;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.3);box-shadow:inset 0 1px 2px rgba(0,0,0,.3)}.btn-danger.focus,.btn-danger:focus{border:1px solid #c6322a;-webkit-box-shadow:inset 0 0 0 1px #fff;box-shadow:inset 0 0 0 1px #fff}.btn-danger.disabled.focus,.btn-danger.disabled:focus,.btn-danger.disabled:hover,.btn-danger[disabled].focus,.btn-danger[disabled]:focus,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger.focus,fieldset[disabled] .btn-danger:focus,fieldset[disabled] .btn-danger:hover{background-color:#dd4b39;background-image:-webkit-linear-gradient(top,#dd4b39 0,#d14836 100%);background-image:-o-linear-gradient(top,#dd4b39 0,#d14836 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dd4b39),to(#d14836));background-image:linear-gradient(to bottom,#dd4b39 0,#d14836 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdd4b39', endColorstr='#ffd14836', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border:1px solid #c6322a;-webkit-box-shadow:none;box-shadow:none}.btn-danger .badge{color:#c6322a;background-color:#fff}.btn-link{color:#15c}.btn-link,.btn-link.active,.btn-link:active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link.focus,.btn-link:active,.btn-link:focus,.btn-link:hover{border-color:transparent}.btn-link.focus,.btn-link:focus,.btn-link:hover{color:#15c;background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link[disabled]:focus .btn-link[disabled].focus,.btn-link[disabled]:focus fieldset[disabled] .btn-link.focus,.btn-link[disabled]:hover,fieldset[disabled] .btn-link:focus .btn-link[disabled].focus,fieldset[disabled] .btn-link:focus fieldset[disabled] .btn-link.focus,fieldset[disabled] .btn-link:hover{color:#333}.btn-group-lg>.btn,.btn-lg{padding:9px 14px;font-size:14px;line-height:1.3;border-radius:2px}.btn-group-sm>.btn,.btn-sm{padding:3px 8px;font-size:12px;line-height:1.5;border-radius:2px}.btn-group-xs>.btn,.btn-xs{padding:2px 6px;font-size:11px;line-height:1.25;border-radius:1px}.dropdown-menu{padding:6px 0;margin:1px 0 0;font-size:13px;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,.2);border-radius:0;-webkit-box-shadow:0 2px 4px rgba(0,0,0,.2);box-shadow:0 2px 4px rgba(0,0,0,.2)}.dropdown-menu .divider{height:1px;margin:8px 0;overflow:hidden;background-color:#ebebeb}.dropdown-menu>li>a{position:relative;padding:3px 30px}.dropdown-menu>li>a .glyphicon{position:absolute;top:4px;left:7px}.dropdown-menu li>a:focus,.dropdown-menu li>a:hover,.dropdown-submenu:focus>a,.dropdown-submenu:hover>a{color:#333;background-color:#eee;background-image:-webkit-linear-gradient(top,#eee 0,#eee 100%);background-image:-o-linear-gradient(top,#eee 0,#eee 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#eee),to(#eee));background-image:linear-gradient(to bottom,#eee 0,#eee 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffeeeeee', endColorstr='#ffeeeeee', GradientType=0);background-repeat:repeat-x}.dropdown-menu>.active>a,.dropdown-menu>.active>a:focus,.dropdown-menu>.active>a:hover{color:#333;background-color:#eee;background-image:-webkit-linear-gradient(top,#eee 0,#eee 100%);background-image:-o-linear-gradient(top,#eee 0,#eee 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#eee),to(#eee));background-image:linear-gradient(to bottom,#eee 0,#eee 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffeeeeee', endColorstr='#ffeeeeee', GradientType=0);background-repeat:repeat-x}.dropdown-header{color:#999}.dropdown-submenu{position:relative}.dropdown-submenu>.dropdown-menu{top:0;left:100%;margin-left:-1px;border-radius:0}.dropdown-submenu:hover>.dropdown-menu{display:block}.dropup .dropdown-submenu>.dropdown-menu{top:auto;bottom:0;margin-top:0;margin-bottom:-2px;border-radius:0}.dropdown-submenu>a:after{position:absolute;right:10px;margin-top:5px;content:""}.dropdown-submenu.dropdown-menu-left,.dropdown-submenu.pull-left{float:none!important}.dropdown-submenu.dropdown-menu-left>.dropdown-menu,.dropdown-submenu.pull-left>.dropdown-menu{left:-100%;margin-left:18px;border-radius:0}.btn-group-vertical>.btn:focus,.btn-group>.btn:focus{z-index:3}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-left:16px}.btn-group>.btn+.dropdown-toggle{-webkit-box-shadow:none;box-shadow:none}.btn-group>.dropdown-toggle:hover{-webkit-box-shadow:0 1px 1px rgba(0,0,0,.1);box-shadow:0 1px 1px rgba(0,0,0,.1)}.btn-group>.btn-danger.dropdown-toggle:hover,.btn-group>.btn-info.dropdown-toggle:hover,.btn-group>.btn-primary.dropdown-toggle:hover,.btn-group>.btn-success.dropdown-toggle:hover,.btn-group>.btn-warning.dropdown-toggle:hover{-webkit-box-shadow:0 1px 1px rgba(0,0,0,.2);box-shadow:0 1px 1px rgba(0,0,0,.2)}.btn-group>.btn.dropdown-toggle.active,.btn-group>.btn.dropdown-toggle:active{-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.1);box-shadow:inset 0 1px 2px rgba(0,0,0,.1)}.btn-group>.btn-danger.dropdown-toggle.active,.btn-group>.btn-danger.dropdown-toggle:active,.btn-group>.btn-info.dropdown-toggle.active,.btn-group>.btn-info.dropdown-toggle:active,.btn-group>.btn-primary.dropdown-toggle.active,.btn-group>.btn-primary.dropdown-toggle:active,.btn-group>.btn-success.dropdown-toggle.active,.btn-group>.btn-success.dropdown-toggle:active,.btn-group>.btn-warning.dropdown-toggle.active,.btn-group>.btn-warning.dropdown-toggle:active{-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.3);box-shadow:inset 0 1px 2px rgba(0,0,0,.3)}.btn-group>.btn-sm.dropdown-toggle{padding:5px 7px}.btn-group>.btn-lg.dropdown-toggle{padding:9px 9px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 1px 6px rgba(0,0,0,.15);box-shadow:inset 0 1px 6px rgba(0,0,0,.15)}.btn-group.open .btn.dropdown-toggle{background-color:#f3f3f3;background-image:none;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.1);box-shadow:inset 0 1px 2px rgba(0,0,0,.1)}.btn-group.open .btn-primary.dropdown-toggle{background-color:#4d90fe;background-image:none;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.3);box-shadow:inset 0 1px 2px rgba(0,0,0,.3)}.btn-group.open .btn-warning.dropdown-toggle{background-color:#faa937;background-image:none;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.3);box-shadow:inset 0 1px 2px rgba(0,0,0,.3)}.btn-group.open .btn-danger.dropdown-toggle{background-color:#d84a38;background-image:none;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.3);box-shadow:inset 0 1px 2px rgba(0,0,0,.3)}.btn-group.open .btn-success.dropdown-toggle{background-color:#35aa47;background-image:none;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.3);box-shadow:inset 0 1px 2px rgba(0,0,0,.3)}.btn-group.open .btn-info.dropdown-toggle{background-color:#5bc0de;background-image:none;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.3);box-shadow:inset 0 1px 2px rgba(0,0,0,.3)}.btn-lg .caret{border-width:5px 5px 0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-left-radius:2px;border-top-right-radius:2px}.btn-group-vertical>.btn:last-child:not(:first-child){border-bottom-right-radius:2px;border-bottom-left-radius:2px}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:38px;padding:9px 14px;font-size:14px;line-height:1.3;border-radius:1px}select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{height:38px;line-height:38px}select[multiple].input-group-lg>.form-control,select[multiple].input-group-lg>.input-group-addon,select[multiple].input-group-lg>.input-group-btn>.btn,textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn{height:auto}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:26px;padding:3px 8px;font-size:12px;line-height:1.5;border-radius:1px}select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{height:26px;line-height:26px}select[multiple].input-group-sm>.form-control,select[multiple].input-group-sm>.input-group-addon,select[multiple].input-group-sm>.input-group-btn>.btn,textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn{height:auto}.input-group .form-control,.input-group-addon,.input-group-btn{margin:0;border-radius:0}.input-group-addon{padding:5px 8px;font-size:13px;color:#555;border:1px solid #d9d9d9;border-top-color:silver;border-radius:2px}.input-group-addon.input-sm{padding:3px 8px;font-size:12px;border-radius:1px}.input-group-addon.input-lg{padding:9px 14px;font-size:14px;border-radius:1px}.input-group-addon input[type=radio],.input-group-addon input[type=checkbox]{margin-bottom:-3px}.nav>li.disabled>a{color:#999}.nav>li.disabled>a:focus,.nav>li.disabled>a:hover{color:#999}.nav .open>a,.nav .open>a:focus,.nav .open>a:hover{color:#fff;background-color:#999;border-color:#999}.nav-tabs>li>a{color:#666;border-radius:2px 2px 0 0}.nav-tabs>li>a:hover{border-color:#eee #eee #ddd}.nav-tabs>li.active>a,.nav-tabs>li.active>a:focus,.nav-tabs>li.active>a:hover{font-weight:700;color:#333}.nav-tabs-google>li{margin:0 -1px 0 0}.nav-tabs-google>li>a{padding:12px 8px;margin:0 8px;line-height:1.4;color:#777;border:3px solid transparent;border-width:3px 0;border-radius:0}.nav-tabs-google>li>a:first-of-type{margin-left:0}.nav-tabs-google>li>a:focus,.nav-tabs-google>li>a:hover{background-color:transparent;border-top-color:transparent}.nav-tabs-google>li>a:hover{color:#000;border-bottom-color:transparent}.nav-tabs-google>li>a:active{color:#dd4b39}.nav-tabs-google>li>a:focus{color:#000;outline:0}.nav-tabs-google>li.active>a,.nav-tabs-google>li.active>a:focus,.nav-tabs-google>li.active>a:hover{color:#dd4b39;border:3px solid transparent;border-width:3px 0;border-bottom-color:#dd4b39}.nav-pills>li>a{border-radius:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:focus,.nav-pills>li.active>a:hover{color:#fff;background-color:#4d90fe}.navbar{min-height:28px;margin-bottom:18px}@media (min-width:768px){.navbar{border-radius:2px}}.navbar-brand{height:28px;padding:5px 15px;font-size:14px;line-height:18px}.navbar-brand>.glyphicon{margin-top:0}@media (min-width:768px){.navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{margin-left:-15px}}.navbar-toggle{padding:5px 10px;margin-top:1px;margin-right:15px;margin-bottom:1px;border-radius:2px}.navbar-nav{margin:2px -15px}.navbar-nav>li>a{padding-top:5px;padding-bottom:5px;line-height:18px}@media (max-width:767px){.navbar-nav .open .dropdown-menu>li>a{line-height:18px}}@media (min-width:768px){.navbar-nav{margin:0}.navbar-nav>li>a{padding-top:5px;padding-bottom:5px}}.navbar-form{padding:10px 15px;margin-top:0;margin-right:-15px;margin-bottom:0;margin-left:-15px;-webkit-box-shadow:none;box-shadow:none}.navbar-form>.input-group .form-control{margin-top:1px;margin-bottom:1px}@media (min-width:768px){.navbar-form{padding-top:0;padding-bottom:0;margin-right:0;margin-left:0}}.navbar-form .form-control{height:26px;padding:3px 8px}.navbar .btn,.navbar-btn{padding:3px 8px;margin-top:1px;margin-bottom:1px}.navbar .btn.btn-sm,.navbar-btn.btn-sm{margin-top:1px;margin-bottom:1px}.navbar .btn.btn-xs,.navbar-btn.btn-xs{padding:2px 6px;margin-top:4px;margin-bottom:4px}.navbar-text{margin-top:5px;margin-bottom:5px}.navbar-default{background-color:#2d2d2d;border-color:#000}.navbar-default .navbar-brand{color:#999}.navbar-default .navbar-brand:focus,.navbar-default .navbar-brand:hover{color:#fff;background-color:transparent}.navbar-default .navbar-brand>.caret{border-top-color:#999;border-bottom-color:#999}.navbar-default .navbar-text{color:#999}.navbar-default .navbar-nav>li>a{color:#999}.navbar-default .navbar-nav>li>a:focus,.navbar-default .navbar-nav>li>a:hover{color:#fff;background-color:transparent}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:focus,.navbar-default .navbar-nav>.active>a:hover{color:#fff;background-color:#141414}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:focus,.navbar-default .navbar-nav>.disabled>a:hover{color:#555;background-color:transparent}.navbar-default .navbar-toggle{border-color:#222}.navbar-default .navbar-toggle:focus,.navbar-default .navbar-toggle:hover{background-color:#333}.navbar-default .navbar-toggle .icon-bar{background-color:#fff}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#000}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:focus,.navbar-default .navbar-nav>.open>a:hover{color:#fff;background-color:#141414}@media (max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#999}.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover{color:#fff;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover{color:#fff;background-color:#141414}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#555;background-color:transparent}}.navbar-default .navbar-link{color:#999}.navbar-default .navbar-link:hover{color:#fff}.navbar-default .btn-link{color:#999}.navbar-default .btn-link:focus,.navbar-default .btn-link:hover{color:#fff}.navbar-default .btn-link[disabled]:focus,.navbar-default .btn-link[disabled]:hover,fieldset[disabled] .navbar-default .btn-link:focus,fieldset[disabled] .navbar-default .btn-link:hover{color:#555}.navbar-inverse{background-color:#fafafa;border-color:#dbdbdb}.navbar-inverse .navbar-brand{color:#999}.navbar-inverse .navbar-brand:focus,.navbar-inverse .navbar-brand:hover{color:grey;background-color:transparent}.navbar-inverse .navbar-brand>.caret{border-top-color:#999;border-bottom-color:#999}.navbar-inverse .navbar-text{color:#999}.navbar-inverse .navbar-nav>li>a{color:#999}.navbar-inverse .navbar-nav>li>a:focus,.navbar-inverse .navbar-nav>li>a:hover{color:#333;background-color:transparent}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:focus,.navbar-inverse .navbar-nav>.active>a:hover{color:#333;background-color:#e1e1e1}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:focus,.navbar-inverse .navbar-nav>.disabled>a:hover{color:#ccc;background-color:transparent}.navbar-inverse .navbar-toggle{border-color:#ddd}.navbar-inverse .navbar-toggle:focus,.navbar-inverse .navbar-toggle:hover{background-color:#ddd}.navbar-inverse .navbar-toggle .icon-bar{background-color:#888}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#e8e8e8}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:focus,.navbar-inverse .navbar-nav>.open>a:hover{color:#333;background-color:#e1e1e1}@media (max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#dbdbdb}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#dbdbdb}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#999}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover{color:#333;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover{color:#333;background-color:#e1e1e1}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#ccc;background-color:transparent}}.navbar-inverse .navbar-link{color:#999}.navbar-inverse .navbar-link:hover{color:#333}.navbar-inverse .btn-link{color:#999}.navbar-inverse .btn-link:focus,.navbar-inverse .btn-link:hover{color:#333}.navbar-inverse .btn-link[disabled]:focus,.navbar-inverse .btn-link[disabled]:hover,fieldset[disabled] .navbar-inverse .btn-link:focus,fieldset[disabled] .navbar-inverse .btn-link:hover{color:#ccc}.navbar-masthead{min-height:44px;margin-bottom:18px}@media (min-width:768px){.navbar-masthead{border-radius:2px}}.navbar-masthead .navbar-static-top{z-index:1005}.navbar-masthead .navbar-fixed-bottom,.navbar-masthead .navbar-fixed-top{z-index:1029}.navbar-masthead .navbar-brand{height:44px;padding:13px 15px;font-size:20px}.navbar-masthead .navbar-brand>.glyphicon{margin-top:-3px}@media (min-width:768px){.navbar>.container .navbar-masthead .navbar-brand,.navbar>.container-fluid .navbar-masthead .navbar-brand{margin-left:-15px}}.navbar-masthead .navbar-toggle{margin-top:7px;margin-right:15px;margin-bottom:7px}.navbar-masthead .navbar-nav{margin:6px -15px}@media (min-width:768px){.navbar-masthead .navbar-nav{margin:6px 0}.navbar-masthead .navbar-nav>li>a{padding-top:8px;padding-bottom:6px}}.navbar-masthead .navbar-form{padding:10px 15px;margin-top:0;margin-right:-15px;margin-bottom:0;margin-left:-15px}.navbar-masthead .navbar-form>.input-group .form-control{margin-top:7px;margin-bottom:7px}@media (max-width:767px){.navbar-masthead .navbar-form .form-group{margin-bottom:5px}}@media (min-width:768px){.navbar-masthead .navbar-form{padding-top:0;padding-bottom:0;margin-right:0;margin-left:0}}.navbar-masthead .navbar-form .form-control{height:30px;padding:5px 8px}.navbar-masthead.navbar .btn,.navbar-masthead.navbar-btn{padding:5px 8px;margin-top:7px;margin-bottom:7px}.navbar-masthead.navbar .btn.btn-sm,.navbar-masthead.navbar-btn.btn-sm{padding:3px 8px;margin-top:9px;margin-bottom:9px}.navbar-masthead.navbar .btn.btn-xs,.navbar-masthead.navbar-btn.btn-xs{padding:2px 6px;margin-top:12px;margin-bottom:12px}.navbar-masthead .navbar-text{margin-top:13px;margin-bottom:13px}.navbar-masthead.navbar-default{background-color:#f1f1f1;border-color:#e5e5e5}.navbar-masthead.navbar-default .navbar-brand{color:#777}.navbar-masthead.navbar-default .navbar-brand:focus,.navbar-masthead.navbar-default .navbar-brand:hover{color:#777;background-color:transparent}.navbar-masthead.navbar-default .navbar-brand>.caret{border-top-color:#777;border-bottom-color:#777}.navbar-masthead.navbar-default .navbar-text{color:#777}.navbar-masthead.navbar-default .navbar-nav>li>a{color:#777}.navbar-masthead.navbar-default .navbar-nav>li>a:focus,.navbar-masthead.navbar-default .navbar-nav>li>a:hover{color:#333;background-color:transparent}.navbar-masthead.navbar-default .navbar-nav>.active>a,.navbar-masthead.navbar-default .navbar-nav>.active>a:focus,.navbar-masthead.navbar-default .navbar-nav>.active>a:hover{color:#333;background-color:#f1f1f1}.navbar-masthead.navbar-default .navbar-nav>.disabled>a,.navbar-masthead.navbar-default .navbar-nav>.disabled>a:focus,.navbar-masthead.navbar-default .navbar-nav>.disabled>a:hover{color:#bbb;background-color:transparent}.navbar-masthead.navbar-default .navbar-toggle{border-color:#dcdcdc}.navbar-masthead.navbar-default .navbar-toggle:focus,.navbar-masthead.navbar-default .navbar-toggle:hover{background-color:#e4e4e4}.navbar-masthead.navbar-default .navbar-toggle .icon-bar{background-color:#888}.navbar-masthead.navbar-default .navbar-collapse,.navbar-masthead.navbar-default .navbar-form{border-color:#dfdfdf}.navbar-masthead.navbar-default .navbar-nav>.open>a,.navbar-masthead.navbar-default .navbar-nav>.open>a:focus,.navbar-masthead.navbar-default .navbar-nav>.open>a:hover{color:#333;background-color:#f1f1f1}@media (max-width:767px){.navbar-masthead.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#777}.navbar-masthead.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-masthead.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover{color:#333;background-color:transparent}.navbar-masthead.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-masthead.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-masthead.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover{color:#333;background-color:#f1f1f1}.navbar-masthead.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-masthead.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-masthead.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#bbb;background-color:transparent}}.navbar-masthead.navbar-default .navbar-link{color:#777}.navbar-masthead.navbar-default .navbar-link:hover{color:#333}.navbar-masthead.navbar-default .btn-link{color:#777}.navbar-masthead.navbar-default .btn-link:focus,.navbar-masthead.navbar-default .btn-link:hover{color:#333}.navbar-masthead.navbar-default .btn-link[disabled]:focus,.navbar-masthead.navbar-default .btn-link[disabled]:hover,fieldset[disabled] .navbar-masthead.navbar-default .btn-link:focus,fieldset[disabled] .navbar-masthead.navbar-default .btn-link:hover{color:#bbb}.navbar-masthead.navbar-inverse{background-color:#444;border-color:#333}.navbar-masthead.navbar-inverse .navbar-brand{color:#fff}.navbar-masthead.navbar-inverse .navbar-brand:focus,.navbar-masthead.navbar-inverse .navbar-brand:hover{color:#fff;background-color:transparent}.navbar-masthead.navbar-inverse .navbar-brand>.caret{border-top-color:#fff;border-bottom-color:#fff}.navbar-masthead.navbar-inverse .navbar-text{color:#999}.navbar-masthead.navbar-inverse .navbar-nav>li>a{color:#fff}.navbar-masthead.navbar-inverse .navbar-nav>li>a:focus,.navbar-masthead.navbar-inverse .navbar-nav>li>a:hover{color:#bbb;background-color:transparent}.navbar-masthead.navbar-inverse .navbar-nav>.active>a,.navbar-masthead.navbar-inverse .navbar-nav>.active>a:focus,.navbar-masthead.navbar-inverse .navbar-nav>.active>a:hover{color:#bbb;background-color:#444}.navbar-masthead.navbar-inverse .navbar-nav>.disabled>a,.navbar-masthead.navbar-inverse .navbar-nav>.disabled>a:focus,.navbar-masthead.navbar-inverse .navbar-nav>.disabled>a:hover{color:#777;background-color:transparent}.navbar-masthead.navbar-inverse .navbar-toggle{border-color:#222}.navbar-masthead.navbar-inverse .navbar-toggle:focus,.navbar-masthead.navbar-inverse .navbar-toggle:hover{background-color:#333}.navbar-masthead.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-masthead.navbar-inverse .navbar-collapse,.navbar-masthead.navbar-inverse .navbar-form{border-color:#323232}.navbar-masthead.navbar-inverse .navbar-nav>.open>a,.navbar-masthead.navbar-inverse .navbar-nav>.open>a:focus,.navbar-masthead.navbar-inverse .navbar-nav>.open>a:hover{color:#bbb;background-color:#444}@media (max-width:767px){.navbar-masthead.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#333}.navbar-masthead.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#333}.navbar-masthead.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#fff}.navbar-masthead.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-masthead.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover{color:#bbb;background-color:transparent}.navbar-masthead.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-masthead.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-masthead.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover{color:#bbb;background-color:#444}.navbar-masthead.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-masthead.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-masthead.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#777;background-color:transparent}}.navbar-masthead.navbar-inverse .navbar-link{color:#fff}.navbar-masthead.navbar-inverse .navbar-link:hover{color:#bbb}.navbar-masthead.navbar-inverse .btn-link{color:#fff}.navbar-masthead.navbar-inverse .btn-link:focus,.navbar-masthead.navbar-inverse .btn-link:hover{color:#bbb}.navbar-masthead.navbar-inverse .btn-link[disabled]:focus,.navbar-masthead.navbar-inverse .btn-link[disabled]:hover,fieldset[disabled] .navbar-masthead.navbar-inverse .btn-link:focus,fieldset[disabled] .navbar-masthead.navbar-inverse .btn-link:hover{color:#777}.navbar-toolbar{min-height:36px;margin-bottom:18px}@media (min-width:768px){.navbar-toolbar{border-radius:2px}}.navbar-toolbar .navbar-static-top{z-index:1008}.navbar-toolbar .navbar-fixed-bottom,.navbar-toolbar .navbar-fixed-top{z-index:1028}.navbar-toolbar .navbar-brand{height:36px;padding:9px 15px;font-size:16px;font-weight:700}@media (min-width:768px){.navbar>.container .navbar-toolbar .navbar-brand,.navbar>.container-fluid .navbar-toolbar .navbar-brand{margin-left:-15px}}.navbar-toolbar .navbar-toggle{margin-top:3px;margin-right:15px;margin-bottom:3px}.navbar-toolbar .navbar-nav{margin:4px -15px}.navbar-toolbar .navbar-nav>li{position:relative}.navbar-toolbar .navbar-nav>li>a{padding:9px 15px}.navbar-toolbar .navbar-nav>li>a:focus,.navbar-toolbar .navbar-nav>li>a:hover{text-decoration:underline}.navbar-toolbar .navbar-nav>li>.dropdown-menu{margin-top:1px}.navbar-toolbar .navbar-nav>.active>a{font-weight:700}.navbar-toolbar .navbar-nav>.active>a:before{position:absolute;bottom:-1px;left:50%;display:inline-block;margin-left:-8px;content:'';border-right:8px solid transparent;border-bottom:8px solid transparent;border-left:8px solid transparent}.navbar-toolbar .navbar-nav>.active>a:after{position:absolute;bottom:-1px;left:50%;display:inline-block;margin-left:-7px;content:'';border-right:7px solid transparent;border-bottom:7px solid transparent;border-left:7px solid transparent}@media (min-width:768px){.navbar-toolbar .navbar-nav{margin:0}.navbar-toolbar .navbar-nav>li>a{padding-top:9px;padding-bottom:9px}}.navbar-toolbar .navbar-form{padding:10px 15px;margin-top:0;margin-right:-15px;margin-bottom:0;margin-left:-15px}.navbar-toolbar .navbar-form>.input-group .form-control{margin-top:3px;margin-bottom:3px}@media (max-width:767px){.navbar-toolbar .navbar-form .form-group{margin-bottom:5px}}@media (min-width:768px){.navbar-toolbar .navbar-form{padding-top:0;padding-bottom:0;margin-right:0;margin-left:0}}.navbar-toolbar .navbar-form .form-control{height:30px;padding:5px 8px}.navbar-toolbar .dropdown-menu{border-top:1px none}.navbar-toolbar.navbar .btn,.navbar-toolbar.navbar-btn{padding:5px 8px;margin-top:3px;margin-bottom:3px}.navbar-toolbar.navbar .btn.btn-sm,.navbar-toolbar.navbar-btn.btn-sm{padding:3px 8px;margin-top:5px;margin-bottom:5px}.navbar-toolbar.navbar .btn.btn-xs,.navbar-toolbar.navbar-btn.btn-xs{padding:2px 6px;margin-top:8px;margin-bottom:8px}.navbar-toolbar .navbar-text{margin-top:9px;margin-bottom:9px}.navbar-toolbar.navbar-default{background-color:#fff;border-color:#ebebeb}.navbar-toolbar.navbar-default .navbar-brand{color:#dd4b39}.navbar-toolbar.navbar-default .navbar-brand:focus,.navbar-toolbar.navbar-default .navbar-brand:hover{color:#dd4b39;background-color:transparent}.navbar-toolbar.navbar-default .navbar-brand>.caret{border-top-color:#dd4b39;border-bottom-color:#dd4b39}.navbar-toolbar.navbar-default .navbar-text{color:#777}.navbar-toolbar.navbar-default .navbar-nav>li>a{color:#777}.navbar-toolbar.navbar-default .navbar-nav>li>a:focus,.navbar-toolbar.navbar-default .navbar-nav>li>a:hover{color:#333;background-color:transparent}.navbar-toolbar.navbar-default .navbar-nav>.active>a,.navbar-toolbar.navbar-default .navbar-nav>.active>a:focus,.navbar-toolbar.navbar-default .navbar-nav>.active>a:hover{color:#333;background-color:#f2f2f2}.navbar-toolbar.navbar-default .navbar-nav>.active>a:before{border-bottom:8px solid #ebebeb}.navbar-toolbar.navbar-default .navbar-nav>.active>a:after{border-bottom:7px solid #fff}.navbar-toolbar.navbar-default .navbar-nav>.disabled>a,.navbar-toolbar.navbar-default .navbar-nav>.disabled>a:focus,.navbar-toolbar.navbar-default .navbar-nav>.disabled>a:hover{color:#bbb;background-color:transparent}.navbar-toolbar.navbar-default .navbar-toggle{border-color:#dcdcdc}.navbar-toolbar.navbar-default .navbar-toggle:focus,.navbar-toolbar.navbar-default .navbar-toggle:hover{background-color:#e4e4e4}.navbar-toolbar.navbar-default .navbar-toggle .icon-bar{background-color:#888}.navbar-toolbar.navbar-default .navbar-collapse,.navbar-toolbar.navbar-default .navbar-form{border-color:#ededed}.navbar-toolbar.navbar-default .navbar-nav>.open>a,.navbar-toolbar.navbar-default .navbar-nav>.open>a:focus,.navbar-toolbar.navbar-default .navbar-nav>.open>a:hover{color:#333;background-color:#f2f2f2}@media (max-width:767px){.navbar-toolbar.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#777}.navbar-toolbar.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-toolbar.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover{color:#333;background-color:transparent}.navbar-toolbar.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-toolbar.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-toolbar.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover{color:#333;background-color:#f2f2f2}.navbar-toolbar.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-toolbar.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-toolbar.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#bbb;background-color:transparent}}.navbar-toolbar.navbar-default .navbar-link{color:#777}.navbar-toolbar.navbar-default .navbar-link:hover{color:#333}.navbar-toolbar.navbar-default .btn-link{color:#777}.navbar-toolbar.navbar-default .btn-link:focus,.navbar-toolbar.navbar-default .btn-link:hover{color:#333}.navbar-toolbar.navbar-default .btn-link[disabled]:focus,.navbar-toolbar.navbar-default .btn-link[disabled]:hover,fieldset[disabled] .navbar-toolbar.navbar-default .btn-link:focus,fieldset[disabled] .navbar-toolbar.navbar-default .btn-link:hover{color:#bbb}.navbar-toolbar.navbar-inverse{background-color:#444;border-color:#333}.navbar-toolbar.navbar-inverse .navbar-brand{color:#fff}.navbar-toolbar.navbar-inverse .navbar-brand:focus,.navbar-toolbar.navbar-inverse .navbar-brand:hover{color:#fff;background-color:transparent}.navbar-toolbar.navbar-inverse .navbar-brand>.caret{border-top-color:#fff;border-bottom-color:#fff}.navbar-toolbar.navbar-inverse .navbar-text{color:#999}.navbar-toolbar.navbar-inverse .navbar-nav>li>a{color:#fff}.navbar-toolbar.navbar-inverse .navbar-nav>li>a:focus,.navbar-toolbar.navbar-inverse .navbar-nav>li>a:hover{color:#fff;background-color:transparent}.navbar-toolbar.navbar-inverse .navbar-nav>.active>a,.navbar-toolbar.navbar-inverse .navbar-nav>.active>a:focus,.navbar-toolbar.navbar-inverse .navbar-nav>.active>a:hover{color:#fff;background-color:#444}.navbar-toolbar.navbar-inverse .navbar-nav>.active>a:before{border-bottom:8px solid #333}.navbar-toolbar.navbar-inverse .navbar-nav>.active>a:after{border-bottom:7px solid #fff}.navbar-toolbar.navbar-inverse .navbar-nav>.disabled>a,.navbar-toolbar.navbar-inverse .navbar-nav>.disabled>a:focus,.navbar-toolbar.navbar-inverse .navbar-nav>.disabled>a:hover{color:#777;background-color:transparent}.navbar-toolbar.navbar-inverse .navbar-toggle{border-color:#222}.navbar-toolbar.navbar-inverse .navbar-toggle:focus,.navbar-toolbar.navbar-inverse .navbar-toggle:hover{background-color:#333}.navbar-toolbar.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-toolbar.navbar-inverse .navbar-collapse,.navbar-toolbar.navbar-inverse .navbar-form{border-color:#323232}.navbar-toolbar.navbar-inverse .navbar-nav>.open>a,.navbar-toolbar.navbar-inverse .navbar-nav>.open>a:focus,.navbar-toolbar.navbar-inverse .navbar-nav>.open>a:hover{color:#fff;background-color:#444}@media (max-width:767px){.navbar-toolbar.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#333}.navbar-toolbar.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#333}.navbar-toolbar.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#fff}.navbar-toolbar.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-toolbar.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover{color:#fff;background-color:transparent}.navbar-toolbar.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-toolbar.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-toolbar.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover{color:#fff;background-color:#444}.navbar-toolbar.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-toolbar.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-toolbar.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#777;background-color:transparent}}.navbar-toolbar.navbar-inverse .navbar-link{color:#fff}.navbar-toolbar.navbar-inverse .navbar-link:hover{color:#fff}.navbar-toolbar.navbar-inverse .btn-link{color:#fff}.navbar-toolbar.navbar-inverse .btn-link:focus,.navbar-toolbar.navbar-inverse .btn-link:hover{color:#fff}.navbar-toolbar.navbar-inverse .btn-link[disabled]:focus,.navbar-toolbar.navbar-inverse .btn-link[disabled]:hover,fieldset[disabled] .navbar-toolbar.navbar-inverse .btn-link:focus,fieldset[disabled] .navbar-toolbar.navbar-inverse .btn-link:hover{color:#777}.navbar-static-top{border-radius:0}.navbar-fixed-top,.navbar-static-top{border-width:1px 0}.navbar-fixed-bottom{border-width:1px 0}.navbar-fixed-bottom,.navbar-fixed-top{position:fixed;right:0;left:0;border-radius:0}.navbar-fixed-bottom,.navbar-fixed-top{border-radius:0}.navbar-fixed-top{top:0}.navbar-fixed-bottom{bottom:0;margin-bottom:0}.navbar-btn{padding:3px 8px;margin-top:1px}.btn.navbar-masthead-btn{margin-top:7px}.btn.navbar-toolbar-btn{margin-top:3px}.navbar-link{color:#999}.navbar-link:hover{color:#fff}.navbar-inverse .navbar-link{color:#999}.navbar-inverse .navbar-link:hover{color:#333}.navbar-form .checkbox-inline,.navbar-form .radio-inline{color:#999}.breadcrumb{padding:13px 15px;margin-bottom:18px;background-color:#f3f3f3;border-radius:2px}.breadcrumb>li+li{position:relative;display:inline-block;margin-left:20px}.breadcrumb>li+li:before{border-radius:5px}.breadcrumb>li+li:after,.breadcrumb>li+li:before{position:absolute;width:0;height:0;content:""}.breadcrumb>li+li:before{border:7px solid transparent}.breadcrumb>li+li:after{border:5px solid transparent}.breadcrumb>li+li:after,.breadcrumb>li+li:before{top:9px;left:100%}.breadcrumb>li+li:before{margin-top:-7px;border-left:7px solid;border-left-color:#777}.breadcrumb>li+li:after{margin-top:-5px;border-left:5px solid #f3f3f3}.breadcrumb>li+li:after,.breadcrumb>li+li:before{left:-16px}.breadcrumb>li+li:before{color:#999;content:""}.breadcrumb>li>a{color:#999}.breadcrumb>li>a:hover{color:#000}.breadcrumb>.active,.breadcrumb>.active>a{color:#000}.breadcrumb-inverse{background-color:#393832}.breadcrumb-inverse>li+li{position:relative;display:inline-block}.breadcrumb-inverse>li+li:before{border-radius:5px}.breadcrumb-inverse>li+li:after,.breadcrumb-inverse>li+li:before{position:absolute;width:0;height:0;content:""}.breadcrumb-inverse>li+li:before{border:7px solid transparent}.breadcrumb-inverse>li+li:after{border:5px solid transparent}.breadcrumb-inverse>li+li:after,.breadcrumb-inverse>li+li:before{top:9px;left:100%}.breadcrumb-inverse>li+li:before{margin-top:-7px;border-left:7px solid;border-left-color:#666}.breadcrumb-inverse>li+li:after{margin-top:-5px;border-left:5px solid #393832}.breadcrumb-inverse>li+li:after,.breadcrumb-inverse>li+li:before{left:-16px}.breadcrumb-inverse>li>a{color:#999}.breadcrumb-inverse>li>a:hover{color:#fff}.breadcrumb-inverse>.active,.breadcrumb-inverse>.active>a{color:#fff}.breadcrumb-sm{padding:4px 15px;background-color:#fff;border-bottom:1px solid #ebebeb}.breadcrumb-sm.breadcrumb-inverse{background-color:#393832}.pagination{margin:18px 0;border-radius:2px}.pagination>li>a,.pagination>li>span{padding:5px 12px;line-height:1.4;color:#333;background-color:#f3f3f3;border:1px solid #dcdcdc}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-top-left-radius:2px;border-bottom-left-radius:2px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-top-right-radius:2px;border-bottom-right-radius:2px}.pagination>li>a:focus,.pagination>li>a:hover,.pagination>li>span:focus,.pagination>li>span:hover{color:#333;background-color:#f5f5f5;border-color:#c6c6c6;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.1);box-shadow:0 1px 1px rgba(0,0,0,.1)}.pagination>li>a:active{background-color:#f4f4f4;background-image:-webkit-linear-gradient(top,#f6f6f6 0,#f1f1f1 100%);background-image:-o-linear-gradient(top,#f6f6f6 0,#f1f1f1 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f6f6f6),to(#f1f1f1));background-image:linear-gradient(to bottom,#f6f6f6 0,#f1f1f1 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff6f6f6', endColorstr='#fff1f1f1', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.1);box-shadow:inset 0 1px 2px rgba(0,0,0,.1)}.pagination>.active>a,.pagination>.active>a:focus,.pagination>.active>a:hover,.pagination>.active>span,.pagination>.active>span:focus,.pagination>.active>span:hover{color:#4d90fe;background-color:#f5f5f5;border-color:#c6c6c6;-webkit-box-shadow:none;box-shadow:none}.pagination>.disabled>a,.pagination>.disabled>a:focus,.pagination>.disabled>a:hover,.pagination>.disabled>span,.pagination>.disabled>span:focus,.pagination>.disabled>span:hover{color:#b3b3b3;text-shadow:none;background-color:#f3f3f3;background-image:-webkit-linear-gradient(top,#f5f5f5 0,#f1f1f1 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#f1f1f1 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#f1f1f1));background-image:linear-gradient(to bottom,#f5f5f5 0,#f1f1f1 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#fff1f1f1', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#d9d9d9;-webkit-box-shadow:none;box-shadow:none}.pagination-lg>li>a,.pagination-lg>li>span{padding:9px 14px;font-size:14px;line-height:1.3}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-top-left-radius:1px;border-bottom-left-radius:1px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-top-right-radius:1px;border-bottom-right-radius:1px}.pagination-sm>li>a,.pagination-sm>li>span{padding:3px 8px;font-size:12px;line-height:1.5}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-top-left-radius:1px;border-bottom-left-radius:1px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-top-right-radius:1px;border-bottom-right-radius:1px}.pager{margin:18px 0}.pager li>a,.pager li>span{padding:11px 24px;overflow:visible;font-size:14px;color:#777;text-decoration:none;white-space:nowrap;cursor:default;background-color:#fff;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);border:1px solid #5b5b5b;border:1px solid rgba(0,0,0,.1);border-radius:2px;outline:0;-webkit-box-shadow:0 2px 1px rgba(0,0,0,.1),0 0 1px rgba(0,0,0,.1);box-shadow:0 2px 1px rgba(0,0,0,.1),0 0 1px rgba(0,0,0,.1)}.pager li>a:focus,.pager li>a:hover{color:#444;background-color:#fff}.pager li>a:active{color:#444;background-color:#fff}.pager li .icon-prev{position:relative;display:inline-block;padding-right:8px}.pager li .icon-prev:before{border-radius:5px}.pager li .icon-prev:after,.pager li .icon-prev:before{position:absolute;width:0;height:0;content:""}.pager li .icon-prev:before{border:7px solid transparent}.pager li .icon-prev:after{border:4px solid transparent}.pager li .icon-prev:after,.pager li .icon-prev:before{top:-5px;right:100%}.pager li .icon-prev:before{margin-top:-7px;border-right:7px solid;border-right-color:inherit}.pager li .icon-prev:after{margin-top:-4px;border-right:4px solid #fff}.pager li .icon-next{position:relative;display:inline-block;padding-left:8px}.pager li .icon-next:before{border-radius:5px}.pager li .icon-next:after,.pager li .icon-next:before{position:absolute;width:0;height:0;content:""}.pager li .icon-next:before{border:7px solid transparent}.pager li .icon-next:after{border:4px solid transparent}.pager li .icon-next:after,.pager li .icon-next:before{top:-5px;left:100%}.pager li .icon-next:before{margin-top:-7px;border-left:7px solid;border-left-color:inherit}.pager li .icon-next:after{margin-top:-4px;border-left:4px solid #fff}.pager .disabled>a,.pager .disabled>a:focus,.pager .disabled>a:hover,.pager .disabled>span{color:#b3b3b3;background-color:#fafafa;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);border-color:#d9d9d9;-webkit-box-shadow:none;box-shadow:none}.pager .disabled .icon-prev{position:relative;display:inline-block;padding-right:8px}.pager .disabled .icon-prev:before{border-radius:5px}.pager .disabled .icon-prev:after,.pager .disabled .icon-prev:before{position:absolute;width:0;height:0;content:""}.pager .disabled .icon-prev:before{border:7px solid transparent}.pager .disabled .icon-prev:after{border:4px solid transparent}.pager .disabled .icon-prev:after,.pager .disabled .icon-prev:before{top:-5px;right:100%}.pager .disabled .icon-prev:before{margin-top:-7px;border-right:7px solid;border-right-color:#b3b3b3}.pager .disabled .icon-prev:after{margin-top:-4px;border-right:4px solid #fafafa}.pager .disabled .icon-next{position:relative;display:inline-block;padding-left:8px}.pager .disabled .icon-next:before{border-radius:5px}.pager .disabled .icon-next:after,.pager .disabled .icon-next:before{position:absolute;width:0;height:0;content:""}.pager .disabled .icon-next:before{border:7px solid transparent}.pager .disabled .icon-next:after{border:4px solid transparent}.pager .disabled .icon-next:after,.pager .disabled .icon-next:before{top:-5px;left:100%}.pager .disabled .icon-next:before{margin-top:-7px;border-left:7px solid;border-left-color:#b3b3b3}.pager .disabled .icon-next:after{margin-top:-4px;border-left:4px solid #fafafa}.label{font-size:80%;border-radius:0}.label-default{background-color:#999}.label-default[href]:focus,.label-default[href]:hover{background-color:grey}.label-primary{background-color:#4d90fe}.label-primary[href]:focus,.label-primary[href]:hover{background-color:#1a70fe}.label-success{background-color:#35aa47}.label-success[href]:focus,.label-success[href]:hover{background-color:#298337}.label-info{background-color:#5bc0de}.label-info[href]:focus,.label-info[href]:hover{background-color:#31b0d5}.label-warning{background-color:#faa937}.label-warning[href]:focus,.label-warning[href]:hover{background-color:#f89306}.label-danger{background-color:#d84a38}.label-danger[href]:focus,.label-danger[href]:hover{background-color:#b93524}.badge{font-size:12px}.btn-group-xs>.btn .badge,.btn-xs .badge{font-size:11px}.list-group-item.active>.badge,li.list-group-item.active a>.badge{color:#fff;background-color:#dd4b39}.nav-pills>.active>a>.badge{color:#15c;background-color:#fff}.jumbotron{color:inherit;background-color:#eee}.jumbotron .h1,.jumbotron h1{color:inherit}.jumbotron p{font-size:20px}.container .jumbotron,.container-fluid .jumbotron{border-radius:1px}@media screen and (min-width:768px){.jumbotron .h1,.jumbotron h1{font-size:59px}}.thumbnail{display:block;padding:0;margin-bottom:18px;line-height:1.4;background-color:#fff;border:1px solid #fff;border-radius:0}a.thumbnail.active,a.thumbnail:focus,a.thumbnail:hover{border-color:#fff;-webkit-box-shadow:0 0 0 1px #dedede;box-shadow:0 0 0 1px #dedede}.thumbnail .caption{padding:9px 4px;color:#000}.alert{padding:8px;margin-bottom:18px;border-radius:2px}.alert .alert-link{font-weight:700}.alert-dismissable,.alert-dismissible{padding-right:28px}.alert-success{color:#3c763d;background-color:#dff0d8;border-color:#a3d48e}.alert-success hr{border-top-color:#93cd7c}.alert-success .alert-link{color:#2b542c}.alert-info{color:#31708f;background-color:#d9edf7;border-color:#85c5e5}.alert-info hr{border-top-color:#70bbe1}.alert-info .alert-link{color:#245269}.alert-warning{color:#333;background-color:#f9edbe;border-color:#f0c36d}.alert-warning hr{border-top-color:#eeb956}.alert-warning .alert-link{color:#1a1a1a}.alert-danger{color:#a94442;background-color:#f2dede;border-color:#d59595}.alert-danger hr{border-top-color:#ce8383}.alert-danger .alert-link{color:#843534}.alert-danger,.alert-info,.alert-success,.alert-warning{text-shadow:0 1px 0 rgba(255,255,255,.5)}.progress{height:14px;height:18px;padding:1px;margin-bottom:18px;font-size:12px;background-color:transparent;background-image:none;border:1px solid #999;border-radius:0;-webkit-box-shadow:none;box-shadow:none}.progress-bar{line-height:1.25;background-color:#6188f5;background-image:none;-webkit-box-shadow:none;box-shadow:none}.progress-bar-striped,.progress-striped .progress-bar{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);-webkit-background-size:40px 40px;background-size:40px 40px}.progress-bar-success{background-color:#2f973f}.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-info{background-color:#53bddc}.progress-striped .progress-bar-info{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-warning{background-color:#fbb450}.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-danger{background-color:#c13e2c}.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.list-group-item{color:#222;background-color:#fff;border:1px solid #e5e5e5}.list-group-item:first-child{border-top-left-radius:0;border-top-right-radius:0}.list-group-item:last-child{border-bottom-right-radius:0;border-bottom-left-radius:0}.list-group-item .dropdown{display:none}.list-group-item .dropdown-toggle{display:inline-block;padding:5px 6px 5px 5px;color:#222}a.list-group-item,button.list-group-item{color:#555}a.list-group-item .list-group-item-heading,button.list-group-item .list-group-item-heading{color:#333}.list-group-item.active,.list-group-item.active:focus,.list-group-item.active:hover{font-weight:700;color:#dd4b39;background-color:transparent;border-color:#e5e5e5;border-left:4px solid #dd4b39;border-left-color:#dd4b39}.list-group-item.active .list-group-item-text,.list-group-item.active:focus .list-group-item-text,.list-group-item.active:hover .list-group-item-text{font-weight:400;color:#888}.list-group-item.active:focus,.list-group-item.active:hover{background-color:#eee}a.list-group-item:focus,a.list-group-item:hover,li.list-group-item a:focus,li.list-group-item a:hover{color:#555;text-decoration:none;background-color:#eee}li.list-group-item{padding:0;margin-bottom:0;border:0 none}li.list-group-item>a{display:block;padding:5px 17px;margin:0 0 0 14px;color:#222}li.list-group-item.active,li.list-group-item.active:focus,li.list-group-item.active:hover{background-color:transparent}li.list-group-item.active:focus>a,li.list-group-item.active:hover>a,li.list-group-item.active>a{margin-left:10px;color:#dd4b39}.list-group-item-success{color:#3c763d;background-color:#dff0d8}a.list-group-item-success,button.list-group-item-success{color:#3c763d}a.list-group-item-success .list-group-item-heading,button.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:focus,a.list-group-item-success:hover,button.list-group-item-success:focus,button.list-group-item-success:hover{color:#3c763d;background-color:#d0e9c6}a.list-group-item-success.active,a.list-group-item-success.active:focus,a.list-group-item-success.active:hover,button.list-group-item-success.active,button.list-group-item-success.active:focus,button.list-group-item-success.active:hover{color:#fff;background-color:#3c763d;border-color:#3c763d}.list-group-item-info{color:#31708f;background-color:#d9edf7}a.list-group-item-info,button.list-group-item-info{color:#31708f}a.list-group-item-info .list-group-item-heading,button.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:focus,a.list-group-item-info:hover,button.list-group-item-info:focus,button.list-group-item-info:hover{color:#31708f;background-color:#c4e3f3}a.list-group-item-info.active,a.list-group-item-info.active:focus,a.list-group-item-info.active:hover,button.list-group-item-info.active,button.list-group-item-info.active:focus,button.list-group-item-info.active:hover{color:#fff;background-color:#31708f;border-color:#31708f}.list-group-item-warning{color:#333;background-color:#f9edbe}a.list-group-item-warning,button.list-group-item-warning{color:#333}a.list-group-item-warning .list-group-item-heading,button.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:focus,a.list-group-item-warning:hover,button.list-group-item-warning:focus,button.list-group-item-warning:hover{color:#333;background-color:#f7e7a7}a.list-group-item-warning.active,a.list-group-item-warning.active:focus,a.list-group-item-warning.active:hover,button.list-group-item-warning.active,button.list-group-item-warning.active:focus,button.list-group-item-warning.active:hover{color:#fff;background-color:#333;border-color:#333}.list-group-item-danger{color:#a94442;background-color:#f2dede}a.list-group-item-danger,button.list-group-item-danger{color:#a94442}a.list-group-item-danger .list-group-item-heading,button.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:focus,a.list-group-item-danger:hover,button.list-group-item-danger:focus,button.list-group-item-danger:hover{color:#a94442;background-color:#ebcccc}a.list-group-item-danger.active,a.list-group-item-danger.active:focus,a.list-group-item-danger.active:hover,button.list-group-item-danger.active,button.list-group-item-danger.active:focus,button.list-group-item-danger.active:hover{color:#fff;background-color:#a94442;border-color:#a94442}.list-group-item-wrapper{margin-left:14px}.list-group-item-wrapper:hover>.dropdown{display:block}.list-group-item-wrapper>a{display:block;padding:5px 17px;margin:0;color:#222}.list-group-item-wrapper>.dropdown:hover+a{background-color:#eee}.list-group-item-wrapper>.dropdown.open{display:block}.list-group-item-wrapper>.dropdown.open+a{background-color:#eee}.list-group-item-wrapper>.dropdown>.dropdown-menu{margin-top:0}.list-group-header{display:block;padding:10px 30px 10px 15px;font-size:11px;font-weight:700;line-height:1.4;color:#999;text-shadow:0 1px 0 rgba(255,255,255,.5);text-transform:uppercase}li.list-group-header{padding:3px 15px}.list-group .list-group-header{margin-top:9px}.list-group-item-menu{padding:0;margin:0;border:0 none;border-radius:0;-webkit-box-shadow:none;box-shadow:none}.list-group-item-menu .list-group-item-wrapper>a{padding-left:30px}.list-group-item-menu .list-group-item-menu .list-group-item-wrapper>a{padding-left:44px}.list-group-item-menu>.list-group-item .collapse-caret{margin-left:28px}.collapse-caret{position:absolute;z-index:1;display:inline-block;width:17px;height:28px;margin-left:14px}.collapse-caret:before{position:absolute;top:12px;left:5px;margin-left:0;content:'';border-bottom:0 dotted}.collapse-caret:hover{background-color:#eee}.collapse-caret.collapsed:before{top:10px;left:6px}.list-group .divider{height:1px;margin:8px 0;margin-right:15px;margin-left:15px;overflow:hidden;background-color:#e5e5e5}.panel{word-wrap:break-word;background-color:#fff;border:1px solid transparent;border-bottom-width:2px;border-radius:3px;-webkit-box-shadow:none;box-shadow:none}.panel-body{padding:15px 20px}.panel-heading{padding:15px 20px;border-top-left-radius:3px;border-top-right-radius:3px}.panel-title{font-size:16px}.panel-footer{padding:15px 20px;background-color:#f8f8f8;border-top:1px solid #e5e5e5;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.list-group{padding:15px 20px;padding-top:0}.panel>.list-group:first-child .list-group-item:first-child{border-top-left-radius:2px;border-top-right-radius:2px}.panel>.list-group:last-child .list-group-item:last-child{border-bottom-right-radius:2px;border-bottom-left-radius:2px}.panel>.panel-collapse>.table caption,.panel>.table caption,.panel>.table-responsive>.table caption{padding-right:15px 20px;padding-left:15px 20px}.panel>.table-responsive:first-child>.table:first-child,.panel>.table:first-child{border-top-left-radius:2px;border-top-right-radius:2px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child,.panel>.table:first-child>thead:first-child>tr:first-child{border-top-left-radius:2px;border-top-right-radius:2px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child{border-top-left-radius:2px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child{border-top-right-radius:2px}.panel>.table-responsive:last-child>.table:last-child,.panel>.table:last-child{border-bottom-right-radius:2px;border-bottom-left-radius:2px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child{border-bottom-right-radius:2px;border-bottom-left-radius:2px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child{border-bottom-left-radius:2px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child{border-bottom-right-radius:2px}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive,.panel>.table+.panel-body,.panel>.table-responsive+.panel-body{border-top:1px solid #ddd}.panel-default{border-color:#d8d8d8}.panel-default>.panel-heading{color:#333;background-color:#fff;border-color:#fff}.panel-default>.panel-heading+.panel-collapse>.panel-body{border-top-color:#d8d8d8}.panel-default>.panel-heading .badge{color:#fff;background-color:#333}.panel-default>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#d8d8d8}.panel-primary{border-color:#4d90fe}.panel-primary>.panel-heading{color:#fff;background-color:#4d90fe;border-color:#4d90fe}.panel-primary>.panel-heading+.panel-collapse>.panel-body{border-top-color:#4d90fe}.panel-primary>.panel-heading .badge{color:#4d90fe;background-color:#fff}.panel-primary>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#4d90fe}.panel-success{border-color:#a3d48e}.panel-success>.panel-heading{color:#3c763d;background-color:#dff0d8;border-color:#a3d48e}.panel-success>.panel-heading+.panel-collapse>.panel-body{border-top-color:#a3d48e}.panel-success>.panel-heading .badge{color:#dff0d8;background-color:#3c763d}.panel-success>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#a3d48e}.panel-info{border-color:#85c5e5}.panel-info>.panel-heading{color:#31708f;background-color:#d9edf7;border-color:#85c5e5}.panel-info>.panel-heading+.panel-collapse>.panel-body{border-top-color:#85c5e5}.panel-info>.panel-heading .badge{color:#d9edf7;background-color:#31708f}.panel-info>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#85c5e5}.panel-warning{border-color:#f0c36d}.panel-warning>.panel-heading{color:#333;background-color:#f9edbe;border-color:#f0c36d}.panel-warning>.panel-heading+.panel-collapse>.panel-body{border-top-color:#f0c36d}.panel-warning>.panel-heading .badge{color:#f9edbe;background-color:#333}.panel-warning>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#f0c36d}.panel-danger{border-color:#d59595}.panel-danger>.panel-heading{color:#a94442;background-color:#f2dede;border-color:#d59595}.panel-danger>.panel-heading+.panel-collapse>.panel-body{border-top-color:#d59595}.panel-danger>.panel-heading .badge{color:#f2dede;background-color:#a94442}.panel-danger>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#d59595}.panel-group{margin-bottom:18px}.panel-group .panel{border-color:transparent;border-radius:0}.panel-group .panel+.panel{margin-top:-3px}.panel-group .panel-heading{padding:0 15px;background-color:#fafafa;border-top:1px dashed #ccc;border-bottom:1px dashed #ccc}.panel-group .panel-heading a{display:block;padding:10px 0 9px;color:#444;text-decoration:none}.panel-group .panel-heading a:before{margin-right:7px;content:"\e082"}.panel-group .panel-heading a:hover{background-color:#f5f5f5}.panel-group .panel-heading a:focus{outline:0}.panel-group .panel-heading a.collapsed:before{margin-right:7px;content:"\e081"}.panel-group .panel-heading .panel-title{font-size:13px}.panel-group .panel-heading+.panel-collapse>.list-group,.panel-group .panel-heading+.panel-collapse>.panel-body{border-top:0 none}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:0 none}.well{background-color:#f1f1f1;border:1px solid #e5e5e5;border-radius:0;-webkit-box-shadow:none;box-shadow:none}.well-lg{border-radius:0}.well-sm{border-radius:0}.scrollable::-webkit-scrollbar{width:10px;height:16px}.scrollable::-webkit-scrollbar:hover{background-color:#f3f3f3;border:1px solid #dbdbdb}.scrollable::-webkit-scrollbar-button:end:increment,.scrollable::-webkit-scrollbar-button:start:decrement{display:block;height:0;background-color:transparent}.scrollable::-webkit-scrollbar-track{-webkit-background-clip:padding-box;background-clip:padding-box;border:solid transparent;border-width:0 0 0 4px}.scrollable::-webkit-scrollbar-track-piece{background-color:transparent;border-radius:0}.scrollable::-webkit-scrollbar-thumb{background-color:#515151;background-color:rgba(0,0,0,.2);-webkit-background-clip:padding-box;background-clip:padding-box;border:solid transparent;border-width:0;-webkit-box-shadow:inset 1px 1px 0 rgba(0,0,0,.1),inset 0 -1px 0 rgba(0,0,0,.07);box-shadow:inset 1px 1px 0 rgba(0,0,0,.1),inset 0 -1px 0 rgba(0,0,0,.07)}.scrollable::-webkit-scrollbar-thumb:hover{background-color:#949494}.scrollable::-webkit-scrollbar-thumb:active{background-color:#3b3b3b;background-color:rgba(0,0,0,.5);-webkit-box-shadow:inset 1px 1px 3px rgba(0,0,0,.35);box-shadow:inset 1px 1px 3px rgba(0,0,0,.35)}.scrollable::-webkit-scrollbar-thumb:horizontal,.scrollable::-webkit-scrollbar-thumb:vertical{background-color:#c6c6c6;border-radius:0}.modal-content{color:#222;border:1px solid #aaa;border:1px solid rgba(0,0,0,.333);border-radius:0;-webkit-box-shadow:0 4px 16px rgba(0,0,0,.2);box-shadow:0 4px 16px rgba(0,0,0,.2)}.modal-backdrop{background-color:#fff}.modal-header .close{font-weight:400;filter:alpha(opacity=40);opacity:.4}.modal-body{padding:15px}.tooltip{font-family:Arial,Helvetica,sans-serif;font-size:11px;font-style:normal;font-weight:400;font-weight:700;line-height:1.4;line-height:1.25;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-break:break-word;word-spacing:normal;word-wrap:normal;white-space:normal;line-break:auto}.tooltip.in{filter:alpha(opacity=100);opacity:1}.tooltip-inner{padding:7px 9px;background-color:#2a2a2a;border:1px solid #fff;border-radius:0}.tooltip-arrow:before{position:absolute;z-index:-1;content:" ";border:7px solid transparent}.tooltip.top .tooltip-arrow,.tooltip.top-left .tooltip-arrow,.tooltip.top-right .tooltip-arrow{bottom:1px;border-top-color:#2a2a2a}.tooltip.top .tooltip-arrow:before,.tooltip.top-left .tooltip-arrow:before,.tooltip.top-right .tooltip-arrow:before{top:-5px;left:-7px;border-top-color:#fff;border-bottom:0 dotted}.tooltip.right .tooltip-arrow{left:1px;border-right-color:#2a2a2a}.tooltip.right .tooltip-arrow:before{top:-7px;right:-5px;border-right-color:#fff;border-left:0 dotted}.tooltip.left .tooltip-arrow{right:1px;border-left-color:#2a2a2a}.tooltip.left .tooltip-arrow:before{top:-7px;left:-5px;border-right:0 dotted;border-left-color:#fff}.tooltip.bottom .tooltip-arrow,.tooltip.bottom-left .tooltip-arrow,.tooltip.bottom-right .tooltip-arrow{top:1px;border-bottom-color:#2a2a2a}.tooltip.bottom .tooltip-arrow:before,.tooltip.bottom-left .tooltip-arrow:before,.tooltip.bottom-right .tooltip-arrow:before{bottom:-5px;left:-7px;border-top:0 dotted;border-bottom-color:#fff}.popover{padding:0;font-family:Arial,Helvetica,sans-serif;font-size:13px;font-style:normal;font-weight:400;line-height:1.4;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;word-wrap:normal;white-space:normal;background-color:#fff;border-radius:2px;-webkit-box-shadow:0 2px 10px rgba(0,0,0,.2);box-shadow:0 2px 10px rgba(0,0,0,.2);line-break:auto}.popover-footer,.popover-title{padding:10px;font-size:13px;background-color:#f5f5f5;border-bottom:1px solid #ccc;border-bottom:1px solid rgba(0,0,0,.2);border-radius:0}.popover-footer{border-top:1px solid #ccc;border-top:1px solid rgba(0,0,0,.2);border-bottom:none}.popover-content{padding:10px}.carousel{width:100%;padding:50px;overflow:hidden;background-color:#f5f5f5;background-image:-webkit-linear-gradient(top,#eee 0,#f5f5f5 100%),-webkit-linear-gradient(bottom,#eee 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#eee 0,#f5f5f5 100%),-o-linear-gradient(bottom,#eee 0,#f5f5f5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#eee),to(#f5f5f5)),-webkit-gradient(linear,left bottom,left top,from(#eee),to(#f5f5f5));background-image:linear-gradient(to bottom,#eee 0,#f5f5f5 100%),linear-gradient(to top,#eee 0,#f5f5f5 100%);background-repeat:no-repeat;background-position:0 0,0 100%;-webkit-background-size:100% 10px;background-size:100% 10px}.carousel-control{width:100px;color:#777;text-shadow:none;filter:alpha(opacity=33);opacity:.33}.carousel-control.left{background-image:none}.carousel-control.right{background-image:none}.carousel-control:focus,.carousel-control:hover{color:#777}.carousel-control .icon-next:before,.carousel-control .icon-prev:before{content:''}.carousel-control .icon-prev{position:relative;position:absolute;right:0;display:inline-block}.carousel-control .icon-prev:before{border-radius:20px}.carousel-control .icon-prev:after,.carousel-control .icon-prev:before{position:absolute;width:0;height:0;content:""}.carousel-control .icon-prev:before{border:22px solid transparent}.carousel-control .icon-prev:after{border:19px solid transparent}.carousel-control .icon-prev:after,.carousel-control .icon-prev:before{top:8px;right:100%}.carousel-control .icon-prev:before{margin-top:-22px;border-right:22px solid;border-right-color:#777}.carousel-control .icon-prev:after{margin-top:-19px;border-right:19px solid #f5f5f5}.carousel-control .icon-next{position:relative;position:absolute;right:0;left:50%;display:inline-block}.carousel-control .icon-next:before{border-radius:20px}.carousel-control .icon-next:after,.carousel-control .icon-next:before{position:absolute;width:0;height:0;content:""}.carousel-control .icon-next:before{border:22px solid transparent}.carousel-control .icon-next:after{border:19px solid transparent}.carousel-control .icon-next:after,.carousel-control .icon-next:before{top:8px;left:100%}.carousel-control .icon-next:before{margin-top:-22px;border-left:22px solid;border-left-color:#777}.carousel-control .icon-next:after{margin-top:-19px;border-left:19px solid #f5f5f5}.carousel-control .icon-next:after,.carousel-control .icon-next:before{left:50%}.carousel-indicators{bottom:5px;left:0;width:100%;margin-left:0}.carousel-indicators li{background-color:#c2c2c2;border:1px solid #c2c2c2}.carousel-indicators .active{width:10px;height:10px;margin:1px;background-color:#444;border:1px solid #444}.carousel-caption{right:0;bottom:0;left:0;padding:10px;color:#fff;text-shadow:none;background-color:#262626;background-color:rgba(0,0,0,.55)}
+/*# sourceMappingURL=todc-bootstrap.min.css.map */
\ No newline at end of file
diff --git a/doc/第7周代码注释/blog/static/assets/img/checkmark.png b/doc/第7周代码注释/blog/static/assets/img/checkmark.png
new file mode 100644
index 00000000..4bd0eb35
Binary files /dev/null and b/doc/第7周代码注释/blog/static/assets/img/checkmark.png differ
diff --git a/doc/第7周代码注释/blog/static/assets/js/ie-emulation-modes-warning.js b/doc/第7周代码注释/blog/static/assets/js/ie-emulation-modes-warning.js
new file mode 100644
index 00000000..3f97ba58
--- /dev/null
+++ b/doc/第7周代码注释/blog/static/assets/js/ie-emulation-modes-warning.js
@@ -0,0 +1,51 @@
+// 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!')
+ }
+})();
diff --git a/doc/第7周代码注释/blog/static/assets/js/ie10-viewport-bug-workaround.js b/doc/第7周代码注释/blog/static/assets/js/ie10-viewport-bug-workaround.js
new file mode 100644
index 00000000..479a6ebd
--- /dev/null
+++ b/doc/第7周代码注释/blog/static/assets/js/ie10-viewport-bug-workaround.js
@@ -0,0 +1,23 @@
+/*!
+ * 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)
+ }
+
+})();
diff --git a/doc/第7周代码注释/blog/static/blog/css/ie.css b/doc/第7周代码注释/blog/static/blog/css/ie.css
new file mode 100644
index 00000000..706f5103
--- /dev/null
+++ b/doc/第7周代码注释/blog/static/blog/css/ie.css
@@ -0,0 +1,273 @@
+/*
+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 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;
+}
\ No newline at end of file
diff --git a/doc/第7周代码注释/blog/static/blog/css/nprogress.css b/doc/第7周代码注释/blog/static/blog/css/nprogress.css
new file mode 100644
index 00000000..90c7b6c3
--- /dev/null
+++ b/doc/第7周代码注释/blog/static/blog/css/nprogress.css
@@ -0,0 +1,74 @@
+/* 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); }
+}
+
diff --git a/doc/第7周代码注释/blog/static/blog/css/oauth_style.css b/doc/第7周代码注释/blog/static/blog/css/oauth_style.css
new file mode 100644
index 00000000..8af78af2
--- /dev/null
+++ b/doc/第7周代码注释/blog/static/blog/css/oauth_style.css
@@ -0,0 +1,305 @@
+
+.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;
+}
\ No newline at end of file
diff --git a/doc/第7周代码注释/blog/static/blog/css/style.css b/doc/第7周代码注释/blog/static/blog/css/style.css
new file mode 100644
index 00000000..d43f7f38
--- /dev/null
+++ b/doc/第7周代码注释/blog/static/blog/css/style.css
@@ -0,0 +1,2504 @@
+html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, embed, figure, figcaption, footer, header, hgroup, menu, nav, output, ruby, section, summary, time, mark, audio, video {
+ margin: 0;
+ padding: 0;
+ border: 0;
+ font-size: 100%;
+ vertical-align: baseline;
+}
+
+body {
+ line-height: 1;
+}
+
+ol,
+ul {
+ list-style: none;
+}
+
+blockquote,
+q {
+ quotes: none;
+}
+
+blockquote:before,
+blockquote:after,
+q:before,
+q:after {
+ content: '';
+ content: none;
+}
+
+table {
+ border-collapse: collapse;
+ border-spacing: 0;
+}
+
+caption,
+th,
+td {
+ font-weight: normal;
+ text-align: left;
+}
+
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+ clear: both;
+}
+
+html {
+ overflow-y: scroll;
+ font-size: 100%;
+ -webkit-text-size-adjust: 100%;
+ -ms-text-size-adjust: 100%;
+}
+
+a:focus {
+ outline: thin dotted;
+}
+
+article,
+aside,
+details,
+figcaption,
+figure,
+footer,
+header,
+hgroup,
+nav,
+section {
+ display: block;
+}
+
+audio,
+canvas,
+video {
+ display: inline-block;
+}
+
+audio:not([controls]) {
+ display: none;
+}
+
+del {
+ color: #333;
+}
+
+ins {
+ background: #fff9c0;
+ text-decoration: none;
+}
+
+hr {
+ background-color: #ccc;
+ border: 0;
+ height: 1px;
+ margin: 24px;
+ margin-bottom: 1.714285714rem;
+}
+
+sub,
+sup {
+ font-size: 75%;
+ line-height: 0;
+ position: relative;
+ vertical-align: baseline;
+}
+
+sup {
+ top: -0.5em;
+}
+
+sub {
+ bottom: -0.25em;
+}
+
+small {
+ font-size: smaller;
+}
+
+img {
+ border: 0;
+ -ms-interpolation-mode: bicubic;
+}
+
+/* Clearing floats */
+.clear:after,
+.wrapper:after,
+.format-status .entry-header:after {
+ clear: both;
+}
+
+.clear:before,
+.clear:after,
+.wrapper:before,
+.wrapper:after,
+.format-status .entry-header:before,
+.format-status .entry-header:after {
+ display: table;
+ content: "";
+}
+
+
+/* =Repeatable patterns
+-------------------------------------------------------------- */
+
+/* Small headers */
+.archive-title,
+.page-title,
+.widget-title,
+.entry-content th,
+.comment-content th {
+ font-size: 11px;
+ font-size: 0.785714286rem;
+ line-height: 2.181818182;
+ font-weight: bold;
+ text-transform: uppercase;
+ color: #636363;
+}
+
+/* Shared Post Format styling */
+article.format-quote footer.entry-meta,
+article.format-link footer.entry-meta,
+article.format-status footer.entry-meta {
+ font-size: 11px;
+ font-size: 0.785714286rem;
+ line-height: 2.181818182;
+}
+
+/* Form fields, general styles first */
+button,
+input,
+select,
+textarea {
+ border: 1px solid #ccc;
+ border-radius: 3px;
+ font-family: inherit;
+ padding: 6px;
+ padding: 0.428571429rem;
+}
+
+button,
+input {
+ line-height: normal;
+}
+
+textarea {
+ font-size: 100%;
+ overflow: auto;
+ vertical-align: top;
+}
+
+/* Reset non-text input types */
+input[type="checkbox"],
+input[type="radio"],
+input[type="file"],
+input[type="hidden"],
+input[type="image"],
+input[type="color"] {
+ border: 0;
+ border-radius: 0;
+ padding: 0;
+}
+
+/* Buttons */
+.menu-toggle,
+input[type="submit"],
+input[type="button"],
+input[type="reset"],
+article.post-password-required input[type=submit],
+.bypostauthor cite span {
+ padding: 6px 10px;
+ padding: 0.428571429rem 0.714285714rem;
+ font-size: 11px;
+ font-size: 0.785714286rem;
+ line-height: 1.428571429;
+ font-weight: normal;
+ color: #7c7c7c;
+ background-color: #e6e6e6;
+ background-repeat: repeat-x;
+ background-image: -moz-linear-gradient(top, #f4f4f4, #e6e6e6);
+ background-image: -ms-linear-gradient(top, #f4f4f4, #e6e6e6);
+ background-image: -webkit-linear-gradient(top, #f4f4f4, #e6e6e6);
+ background-image: -o-linear-gradient(top, #f4f4f4, #e6e6e6);
+ background-image: linear-gradient(to bottom, #f4f4f4, #e6e6e6);
+ border: 1px solid #d2d2d2;
+ border-radius: 3px;
+ box-shadow: 0 1px 2px rgba(64, 64, 64, 0.1);
+}
+
+.menu-toggle,
+button,
+input[type="submit"],
+input[type="button"],
+input[type="reset"] {
+ cursor: pointer;
+}
+
+button[disabled],
+input[disabled] {
+ cursor: default;
+}
+
+.menu-toggle:hover,
+.menu-toggle:focus,
+button:hover,
+input[type="submit"]:hover,
+input[type="button"]:hover,
+input[type="reset"]:hover,
+article.post-password-required input[type=submit]:hover {
+ color: #5e5e5e;
+ background-color: #ebebeb;
+ background-repeat: repeat-x;
+ background-image: -moz-linear-gradient(top, #f9f9f9, #ebebeb);
+ background-image: -ms-linear-gradient(top, #f9f9f9, #ebebeb);
+ background-image: -webkit-linear-gradient(top, #f9f9f9, #ebebeb);
+ background-image: -o-linear-gradient(top, #f9f9f9, #ebebeb);
+ background-image: linear-gradient(to bottom, #f9f9f9, #ebebeb);
+}
+
+.menu-toggle:active,
+.menu-toggle.toggled-on,
+button:active,
+input[type="submit"]:active,
+input[type="button"]:active,
+input[type="reset"]:active {
+ color: #757575;
+ background-color: #e1e1e1;
+ background-repeat: repeat-x;
+ background-image: -moz-linear-gradient(top, #ebebeb, #e1e1e1);
+ background-image: -ms-linear-gradient(top, #ebebeb, #e1e1e1);
+ background-image: -webkit-linear-gradient(top, #ebebeb, #e1e1e1);
+ background-image: -o-linear-gradient(top, #ebebeb, #e1e1e1);
+ background-image: linear-gradient(to bottom, #ebebeb, #e1e1e1);
+ box-shadow: inset 0 0 8px 2px #c6c6c6, 0 1px 0 0 #f4f4f4;
+ border-color: transparent;
+}
+
+.bypostauthor cite span {
+ color: #fff;
+ background-color: #21759b;
+ background-image: none;
+ border: 1px solid #1f6f93;
+ border-radius: 2px;
+ box-shadow: none;
+ padding: 0;
+}
+
+/* Responsive images */
+.entry-content img,
+.comment-content img,
+.widget img {
+ max-width: 100%; /* Fluid images for posts, comments, and widgets */
+}
+
+img[class*="align"],
+img[class*="wp-image-"],
+img[class*="attachment-"] {
+ height: auto; /* Make sure images with WordPress-added height and width attributes are scaled correctly */
+}
+
+img.size-full,
+img.size-large,
+img.header-image,
+img.wp-post-image {
+ max-width: 100%;
+ height: auto; /* Make sure images with WordPress-added height and width attributes are scaled correctly */
+}
+
+/* Make sure videos and embeds fit their containers */
+embed,
+iframe,
+object,
+video {
+ max-width: 100%;
+}
+
+.entry-content .twitter-tweet-rendered {
+ max-width: 100% !important; /* Override the Twitter embed fixed width */
+}
+
+/* Images */
+.alignleft {
+ float: left;
+}
+
+.alignright {
+ float: right;
+}
+
+.aligncenter {
+ display: block;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+.entry-content img,
+.comment-content img,
+.widget img,
+img.header-image,
+.author-avatar img,
+img.wp-post-image {
+ /* Add fancy borders to all WordPress-added images but not things like badges and icons and the like */
+ border-radius: 3px;
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
+}
+
+.wp-caption {
+ max-width: 100%; /* Keep wide captions from overflowing their container. */
+ padding: 4px;
+}
+
+.wp-caption .wp-caption-text,
+.gallery-caption,
+.entry-caption {
+ font-style: italic;
+ font-size: 12px;
+ font-size: 0.857142857rem;
+ line-height: 2;
+ color: #757575;
+}
+
+img.wp-smiley,
+.rsswidget img {
+ border: 0;
+ border-radius: 0;
+ box-shadow: none;
+ margin-bottom: 0;
+ margin-top: 0;
+ padding: 0;
+}
+
+.entry-content dl.gallery-item {
+ margin: 0;
+}
+
+.gallery-item a,
+.gallery-caption {
+ width: 90%;
+}
+
+.gallery-item a {
+ display: block;
+}
+
+.gallery-caption a {
+ display: inline;
+}
+
+.gallery-columns-1 .gallery-item a {
+ max-width: 100%;
+ width: auto;
+}
+
+.gallery .gallery-icon img {
+ height: auto;
+ max-width: 90%;
+ padding: 5%;
+}
+
+.gallery-columns-1 .gallery-icon img {
+ padding: 3%;
+}
+
+/* Navigation */
+.site-content nav {
+ clear: both;
+ line-height: 2;
+ overflow: hidden;
+}
+
+#nav-above {
+ padding: 24px 0;
+ padding: 1.714285714rem 0;
+}
+
+#nav-above {
+ display: none;
+}
+
+.paged #nav-above {
+ display: block;
+}
+
+.nav-previous,
+.previous-image {
+ float: left;
+ width: 50%;
+}
+
+.nav-next,
+.next-image {
+ float: right;
+ text-align: right;
+ width: 50%;
+}
+
+.nav-single + .comments-area,
+#comment-nav-above {
+ margin: 48px 0;
+ margin: 3.428571429rem 0;
+}
+
+/* Author profiles */
+.author .archive-header {
+ margin-bottom: 24px;
+ margin-bottom: 1.714285714rem;
+}
+
+.author-info {
+ border-top: 1px solid #ededed;
+ margin: 24px 0;
+ margin: 1.714285714rem 0;
+ padding-top: 24px;
+ padding-top: 1.714285714rem;
+ overflow: hidden;
+}
+
+.author-description p {
+ color: #757575;
+ font-size: 13px;
+ font-size: 0.928571429rem;
+ line-height: 1.846153846;
+}
+
+.author.archive .author-info {
+ border-top: 0;
+ margin: 0 0 48px;
+ margin: 0 0 3.428571429rem;
+}
+
+.author.archive .author-avatar {
+ margin-top: 0;
+}
+
+
+/* =Basic structure
+-------------------------------------------------------------- */
+
+/* Body, links, basics */
+html {
+ font-size: 87.5%;
+}
+
+body {
+ font-size: 14px;
+ font-size: 1rem;
+ font-family: Helvetica, Arial, sans-serif;
+ text-rendering: optimizeLegibility;
+ color: #444;
+}
+
+body.custom-font-enabled {
+ font-family: "Open Sans", Helvetica, Arial, sans-serif;
+}
+
+a {
+ outline: none;
+ color: #21759b;
+}
+
+a:hover {
+ color: #0f3647;
+}
+
+/* Assistive text */
+.assistive-text,
+.site .screen-reader-text {
+ position: absolute !important;
+ clip: rect(1px, 1px, 1px, 1px);
+ overflow: hidden;
+ height: 1px;
+ width: 1px;
+}
+
+.main-navigation .assistive-text:focus,
+.site .screen-reader-text:hover,
+.site .screen-reader-text:active,
+.site .screen-reader-text:focus {
+ background: #fff;
+ border: 2px solid #333;
+ border-radius: 3px;
+ clip: auto !important;
+ color: #000;
+ display: block;
+ font-size: 12px;
+ height: auto;
+ padding: 12px;
+ position: absolute;
+ top: 5px;
+ left: 5px;
+ width: auto;
+ z-index: 100000; /* Above WP toolbar */
+}
+
+/* Page structure */
+.site {
+ padding: 0 24px;
+ padding: 0 1.714285714rem;
+ background-color: #fff;
+}
+
+.site-content {
+ margin: 24px 0 0;
+ margin: 1.714285714rem 0 0;
+}
+
+.widget-area {
+ margin: 24px 0 0;
+ margin: 1.714285714rem 0 0;
+}
+
+/* Header */
+.site-header {
+ padding: 24px 0;
+ padding: 1.714285714rem 0;
+}
+
+.site-header h1,
+.site-header h2 {
+ text-align: center;
+}
+
+.site-header h1 a,
+.site-header h2 a {
+ color: #515151;
+ display: inline-block;
+ text-decoration: none;
+}
+
+.site-header h1 a:hover,
+.site-header h2 a:hover {
+ color: #21759b;
+}
+
+.site-header h1 {
+ font-size: 24px;
+ font-size: 1.714285714rem;
+ line-height: 1.285714286;
+ margin-bottom: 14px;
+ margin-bottom: 1rem;
+}
+
+.site-header h2 {
+ font-weight: normal;
+ font-size: 13px;
+ font-size: 0.928571429rem;
+ line-height: 1.846153846;
+ color: #757575;
+}
+
+.header-image {
+ margin-top: 24px;
+ margin-top: 1.714285714rem;
+}
+
+/* Navigation Menu */
+.main-navigation {
+ margin-top: 24px;
+ margin-top: 1.714285714rem;
+ text-align: center;
+}
+
+.main-navigation li {
+ margin-top: 24px;
+ margin-top: 1.714285714rem;
+ font-size: 12px;
+ font-size: 0.857142857rem;
+ line-height: 1.42857143;
+}
+
+.main-navigation a {
+ color: #5e5e5e;
+}
+
+.main-navigation a:hover,
+.main-navigation a:focus {
+ color: #21759b;
+}
+
+.main-navigation ul.nav-menu,
+.main-navigation div.nav-menu > ul {
+ display: none;
+}
+
+.main-navigation ul.nav-menu.toggled-on,
+.menu-toggle {
+ display: inline-block;
+}
+
+/* Banner */
+section[role="banner"] {
+ margin-bottom: 48px;
+ margin-bottom: 3.428571429rem;
+}
+
+/* Sidebar */
+.widget-area .widget {
+ -webkit-hyphens: auto;
+ -moz-hyphens: auto;
+ hyphens: auto;
+ margin-bottom: 48px;
+ margin-bottom: 3.428571429rem;
+ word-wrap: break-word;
+}
+
+.widget-area .widget h3 {
+ margin-bottom: 24px;
+ margin-bottom: 1.714285714rem;
+}
+
+.widget-area .widget p,
+.widget-area .widget li,
+.widget-area .widget .textwidget {
+ font-size: 13px;
+ font-size: 0.928571429rem;
+ line-height: 1.846153846;
+}
+
+.widget-area .widget p {
+ margin-bottom: 24px;
+ margin-bottom: 1.714285714rem;
+}
+
+.widget-area .textwidget ul,
+.widget-area .textwidget ol {
+ list-style: disc outside;
+ margin: 0 0 24px;
+ margin: 0 0 1.714285714rem;
+}
+
+.widget-area .textwidget li > ul,
+.widget-area .textwidget li > ol {
+ margin-bottom: 0;
+}
+
+.widget-area .textwidget ol {
+ list-style: decimal;
+}
+
+.widget-area .textwidget li {
+ margin-left: 36px;
+ margin-left: 2.571428571rem;
+}
+
+.widget-area .widget a {
+ color: #757575;
+}
+
+.widget-area .widget a:hover {
+ color: #21759b;
+}
+
+.widget-area .widget a:visited {
+ color: #9f9f9f;
+}
+
+.widget-area #s {
+ width: 53.66666666666%; /* define a width to avoid dropping a wider submit button */
+}
+
+/* Footer */
+footer[role="contentinfo"] {
+ border-top: 1px solid #ededed;
+ clear: both;
+ font-size: 12px;
+ font-size: 0.857142857rem;
+ line-height: 2;
+ max-width: 960px;
+ max-width: 68.571428571rem;
+ margin-top: 24px;
+ margin-top: 1.714285714rem;
+ margin-left: auto;
+ margin-right: auto;
+ padding: 24px 0;
+ padding: 1.714285714rem 0;
+}
+
+footer[role="contentinfo"] a {
+ color: #686868;
+}
+
+footer[role="contentinfo"] a:hover {
+ color: #21759b;
+}
+
+.site-info span[role=separator] {
+ padding: 0 0.3em 0 0.6em;
+}
+
+.site-info span[role=separator]::before {
+ content: '\002f';
+}
+
+
+/* =Main content and comment content
+-------------------------------------------------------------- */
+
+.entry-meta {
+ clear: both;
+}
+
+.entry-header {
+ margin-bottom: 24px;
+ margin-bottom: 1.714285714rem;
+}
+
+.entry-header img.wp-post-image {
+ margin-bottom: 24px;
+ margin-bottom: 1.714285714rem;
+}
+
+.entry-header .entry-title {
+ font-size: 20px;
+ font-size: 1.428571429rem;
+ line-height: 1.2;
+ font-weight: normal;
+}
+
+.entry-header .entry-title a {
+ text-decoration: none;
+}
+
+.entry-header .entry-format {
+ margin-top: 24px;
+ margin-top: 1.714285714rem;
+ font-weight: normal;
+}
+
+.entry-header .comments-link {
+ margin-top: 24px;
+ margin-top: 1.714285714rem;
+ font-size: 13px;
+ font-size: 0.928571429rem;
+ line-height: 1.846153846;
+ color: #757575;
+}
+
+.comments-link a,
+.entry-meta a {
+ color: #757575;
+}
+
+.comments-link a:hover,
+.entry-meta a:hover {
+ color: #21759b;
+}
+
+article.sticky .featured-post {
+ border-top: 4px double #ededed;
+ border-bottom: 4px double #ededed;
+ color: #757575;
+ font-size: 13px;
+ font-size: 0.928571429rem;
+ line-height: 3.692307692;
+ margin-bottom: 24px;
+ margin-bottom: 1.714285714rem;
+ text-align: center;
+}
+
+.entry-content,
+.entry-summary,
+.mu_register {
+ line-height: 1.714285714;
+}
+
+.entry-content h1,
+.comment-content h1,
+.entry-content h2,
+.comment-content h2,
+.entry-content h3,
+.comment-content h3,
+.entry-content h4,
+.comment-content h4,
+.entry-content h5,
+.comment-content h5,
+.entry-content h6,
+.comment-content h6 {
+ margin: 24px 0;
+ margin: 1.714285714rem 0;
+ line-height: 1.714285714;
+}
+
+.entry-content h1,
+.comment-content h1 {
+ font-size: 21px;
+ font-size: 1.5rem;
+ line-height: 1.5;
+}
+
+.entry-content h2,
+.comment-content h2,
+.mu_register h2 {
+ font-size: 18px;
+ font-size: 1.285714286rem;
+ line-height: 1.6;
+}
+
+.entry-content h3,
+.comment-content h3 {
+ font-size: 16px;
+ font-size: 1.142857143rem;
+ line-height: 1.846153846;
+}
+
+.entry-content h4,
+.comment-content h4 {
+ font-size: 14px;
+ font-size: 1rem;
+ line-height: 1.846153846;
+}
+
+.entry-content h5,
+.comment-content h5 {
+ font-size: 13px;
+ font-size: 0.928571429rem;
+ line-height: 1.846153846;
+}
+
+.entry-content h6,
+.comment-content h6 {
+ font-size: 12px;
+ font-size: 0.857142857rem;
+ line-height: 1.846153846;
+}
+
+.entry-content p,
+.entry-summary p,
+.comment-content p,
+.mu_register p {
+ margin: 0 0 24px;
+ margin: 0 0 1.714285714rem;
+ line-height: 1.714285714;
+}
+
+.entry-content a:visited,
+.comment-content a:visited {
+ color: #9f9f9f;
+}
+
+.entry-content .more-link {
+ white-space: nowrap;
+}
+
+.entry-content ol,
+.comment-content ol,
+.entry-content ul,
+.comment-content ul,
+.mu_register ul {
+ margin: 0 0 24px;
+ margin: 0 0 1.714285714rem;
+ line-height: 1.714285714;
+}
+
+.entry-content ul ul,
+.comment-content ul ul,
+.entry-content ol ol,
+.comment-content ol ol,
+.entry-content ul ol,
+.comment-content ul ol,
+.entry-content ol ul,
+.comment-content ol ul {
+ margin-bottom: 0;
+}
+
+.entry-content ul,
+.comment-content ul,
+.mu_register ul {
+ list-style: disc outside;
+}
+
+.entry-content ol,
+.comment-content ol {
+ list-style: decimal outside;
+}
+
+.entry-content li,
+.comment-content li,
+.mu_register li {
+ margin: 0 0 0 36px;
+ margin: 0 0 0 2.571428571rem;
+}
+
+.entry-content blockquote,
+.comment-content blockquote {
+ margin-bottom: 24px;
+ margin-bottom: 1.714285714rem;
+ padding: 24px;
+ padding: 1.714285714rem;
+ font-style: italic;
+}
+
+.entry-content blockquote p:last-child,
+.comment-content blockquote p:last-child {
+ margin-bottom: 0;
+}
+
+.entry-content code,
+.comment-content code {
+ font-family: Consolas, Monaco, Lucida Console, monospace;
+ font-size: 12px;
+ font-size: 0.857142857rem;
+ line-height: 2;
+}
+
+.entry-content pre,
+.comment-content pre {
+ border: 1px solid #ededed;
+ color: #666;
+ font-family: Consolas, Monaco, Lucida Console, monospace;
+ font-size: 12px;
+ font-size: 0.857142857rem;
+ line-height: 1.714285714;
+ margin: 24px 0;
+ margin: 1.714285714rem 0;
+ overflow: auto;
+ padding: 24px;
+ padding: 1.714285714rem;
+}
+
+.entry-content pre code,
+.comment-content pre code {
+ display: block;
+}
+
+.entry-content abbr,
+.comment-content abbr,
+.entry-content dfn,
+.comment-content dfn,
+.entry-content acronym,
+.comment-content acronym {
+ border-bottom: 1px dotted #666;
+ cursor: help;
+}
+
+.entry-content address,
+.comment-content address {
+ display: block;
+ line-height: 1.714285714;
+ margin: 0 0 24px;
+ margin: 0 0 1.714285714rem;
+}
+
+img.alignleft,
+.wp-caption.alignleft {
+ margin: 12px 24px 12px 0;
+ margin: 0.857142857rem 1.714285714rem 0.857142857rem 0;
+}
+
+img.alignright,
+.wp-caption.alignright {
+ margin: 12px 0 12px 24px;
+ margin: 0.857142857rem 0 0.857142857rem 1.714285714rem;
+}
+
+img.aligncenter,
+.wp-caption.aligncenter {
+ clear: both;
+ margin-top: 12px;
+ margin-top: 0.857142857rem;
+ margin-bottom: 12px;
+ margin-bottom: 0.857142857rem;
+}
+
+.entry-content embed,
+.entry-content iframe,
+.entry-content object,
+.entry-content video {
+ margin-bottom: 24px;
+ margin-bottom: 1.714285714rem;
+}
+
+.entry-content dl,
+.comment-content dl {
+ margin: 0 24px;
+ margin: 0 1.714285714rem;
+}
+
+.entry-content dt,
+.comment-content dt {
+ font-weight: bold;
+ line-height: 1.714285714;
+}
+
+.entry-content dd,
+.comment-content dd {
+ line-height: 1.714285714;
+ margin-bottom: 24px;
+ margin-bottom: 1.714285714rem;
+}
+
+.entry-content table,
+.comment-content table {
+ border-bottom: 1px solid #ededed;
+ color: #757575;
+ font-size: 12px;
+ font-size: 0.857142857rem;
+ line-height: 2;
+ margin: 0 0 24px;
+ margin: 0 0 1.714285714rem;
+ width: 100%;
+}
+
+.entry-content table caption,
+.comment-content table caption {
+ font-size: 16px;
+ font-size: 1.142857143rem;
+ margin: 24px 0;
+ margin: 1.714285714rem 0;
+}
+
+.entry-content td,
+.comment-content td {
+ border-top: 1px solid #ededed;
+ padding: 6px 10px 6px 0;
+}
+
+.site-content article {
+ border-bottom: 4px double #ededed;
+ margin-bottom: 72px;
+ margin-bottom: 5.142857143rem;
+ padding-bottom: 24px;
+ padding-bottom: 1.714285714rem;
+ word-wrap: break-word;
+ -webkit-hyphens: auto;
+ -moz-hyphens: auto;
+ hyphens: auto;
+}
+
+.page-links {
+ clear: both;
+ line-height: 1.714285714;
+}
+
+footer.entry-meta {
+ margin-top: 24px;
+ margin-top: 1.714285714rem;
+ font-size: 13px;
+ font-size: 0.928571429rem;
+ line-height: 1.846153846;
+ color: #757575;
+}
+
+.single-author .entry-meta .by-author {
+ display: none;
+}
+
+.mu_register h2 {
+ color: #757575;
+ font-weight: normal;
+}
+
+
+/* =Archives
+-------------------------------------------------------------- */
+
+.archive-header,
+.page-header {
+ margin-bottom: 48px;
+ margin-bottom: 3.428571429rem;
+ padding-bottom: 22px;
+ padding-bottom: 1.571428571rem;
+ border-bottom: 1px solid #ededed;
+}
+
+.archive-meta {
+ color: #757575;
+ font-size: 12px;
+ font-size: 0.857142857rem;
+ line-height: 2;
+ margin-top: 22px;
+ margin-top: 1.571428571rem;
+}
+
+/* =Single audio/video attachment view
+-------------------------------------------------------------- */
+
+.attachment .entry-content .mejs-audio {
+ max-width: 400px;
+}
+
+.attachment .entry-content .mejs-container {
+ margin-bottom: 24px;
+}
+
+
+/* =Single image attachment view
+-------------------------------------------------------------- */
+
+.article.attachment {
+ overflow: hidden;
+}
+
+.image-attachment div.attachment {
+ text-align: center;
+}
+
+.image-attachment div.attachment p {
+ text-align: center;
+}
+
+.image-attachment div.attachment img {
+ display: block;
+ height: auto;
+ margin: 0 auto;
+ max-width: 100%;
+}
+
+.image-attachment .entry-caption {
+ margin-top: 8px;
+ margin-top: 0.571428571rem;
+}
+
+
+/* =Aside post format
+-------------------------------------------------------------- */
+
+article.format-aside h1 {
+ margin-bottom: 24px;
+ margin-bottom: 1.714285714rem;
+}
+
+article.format-aside h1 a {
+ text-decoration: none;
+ color: #4d525a;
+}
+
+article.format-aside h1 a:hover {
+ color: #2e3542;
+}
+
+article.format-aside .aside {
+ padding: 24px 24px 0;
+ padding: 1.714285714rem;
+ background: #d2e0f9;
+ border-left: 22px solid #a8bfe8;
+}
+
+article.format-aside p {
+ font-size: 13px;
+ font-size: 0.928571429rem;
+ line-height: 1.846153846;
+ color: #4a5466;
+}
+
+article.format-aside blockquote:last-child,
+article.format-aside p:last-child {
+ margin-bottom: 0;
+}
+
+
+/* =Post formats
+-------------------------------------------------------------- */
+
+/* Image posts */
+article.format-image footer h1 {
+ font-size: 13px;
+ font-size: 0.928571429rem;
+ line-height: 1.846153846;
+ font-weight: normal;
+}
+
+article.format-image footer h2 {
+ font-size: 11px;
+ font-size: 0.785714286rem;
+ line-height: 2.181818182;
+}
+
+article.format-image footer a h2 {
+ font-weight: normal;
+}
+
+/* Link posts */
+article.format-link header {
+ padding: 0 10px;
+ padding: 0 0.714285714rem;
+ float: right;
+ font-size: 11px;
+ font-size: 0.785714286rem;
+ line-height: 2.181818182;
+ font-weight: bold;
+ font-style: italic;
+ text-transform: uppercase;
+ color: #848484;
+ background-color: #ebebeb;
+ border-radius: 3px;
+}
+
+article.format-link .entry-content {
+ max-width: 80%;
+ float: left;
+}
+
+article.format-link .entry-content a {
+ font-size: 22px;
+ font-size: 1.571428571rem;
+ line-height: 1.090909091;
+ text-decoration: none;
+}
+
+/* Quote posts */
+article.format-quote .entry-content p {
+ margin: 0;
+ padding-bottom: 24px;
+ padding-bottom: 1.714285714rem;
+}
+
+article.format-quote .entry-content blockquote {
+ display: block;
+ padding: 24px 24px 0;
+ padding: 1.714285714rem 1.714285714rem 0;
+ font-size: 15px;
+ font-size: 1.071428571rem;
+ line-height: 1.6;
+ font-style: normal;
+ color: #6a6a6a;
+ background: #efefef;
+}
+
+/* Status posts */
+.format-status .entry-header {
+ margin-bottom: 24px;
+ margin-bottom: 1.714285714rem;
+}
+
+.format-status .entry-header header {
+ display: inline-block;
+}
+
+.format-status .entry-header h1 {
+ font-size: 15px;
+ font-size: 1.071428571rem;
+ font-weight: normal;
+ line-height: 1.6;
+ margin: 0;
+}
+
+.format-status .entry-header h2 {
+ font-size: 12px;
+ font-size: 0.857142857rem;
+ font-weight: normal;
+ line-height: 2;
+ margin: 0;
+}
+
+.format-status .entry-header header a {
+ color: #757575;
+}
+
+.format-status .entry-header header a:hover {
+ color: #21759b;
+}
+
+.format-status .entry-header img {
+ float: left;
+ margin-right: 21px;
+ margin-right: 1.5rem;
+}
+
+
+/* =Comments
+-------------------------------------------------------------- */
+
+.comments-title {
+ margin-bottom: 48px;
+ margin-bottom: 3.428571429rem;
+ font-size: 16px;
+ font-size: 1.142857143rem;
+ line-height: 1.5;
+ font-weight: normal;
+}
+
+.comments-area article {
+ margin: 24px 0;
+ margin: 1.714285714rem 0;
+}
+
+.comments-area article header {
+ margin: 0 0 48px;
+ margin: 0 0 3.428571429rem;
+ overflow: hidden;
+ position: relative;
+}
+
+.comments-area article header img {
+ float: left;
+ padding: 0;
+ line-height: 0;
+}
+
+.comments-area article header cite,
+.comments-area article header time {
+ display: block;
+ margin-left: 85px;
+ margin-left: 6.071428571rem;
+}
+
+.comments-area article header cite {
+ font-style: normal;
+ font-size: 15px;
+ font-size: 1.071428571rem;
+ line-height: 1.42857143;
+}
+
+.comments-area cite b {
+ font-weight: normal;
+}
+
+.comments-area article header time {
+ line-height: 1.714285714;
+ text-decoration: none;
+ font-size: 12px;
+ font-size: 0.857142857rem;
+ color: #5e5e5e;
+}
+
+.comments-area article header a {
+ text-decoration: none;
+ color: #5e5e5e;
+}
+
+.comments-area article header a:hover {
+ color: #21759b;
+}
+
+.comments-area article header cite a {
+ color: #444;
+}
+
+.comments-area article header cite a:hover {
+ text-decoration: underline;
+}
+
+.comments-area article header h4 {
+ position: absolute;
+ top: 0;
+ right: 0;
+ padding: 6px 12px;
+ padding: 0.428571429rem 0.857142857rem;
+ font-size: 12px;
+ font-size: 0.857142857rem;
+ font-weight: normal;
+ color: #fff;
+ background-color: #0088d0;
+ background-repeat: repeat-x;
+ background-image: -moz-linear-gradient(top, #009cee, #0088d0);
+ background-image: -ms-linear-gradient(top, #009cee, #0088d0);
+ background-image: -webkit-linear-gradient(top, #009cee, #0088d0);
+ background-image: -o-linear-gradient(top, #009cee, #0088d0);
+ background-image: linear-gradient(to bottom, #009cee, #0088d0);
+ border-radius: 3px;
+ border: 1px solid #007cbd;
+}
+
+.comments-area .bypostauthor cite span {
+ position: absolute;
+ margin-left: 5px;
+ margin-left: 0.357142857rem;
+ padding: 2px 5px;
+ padding: 0.142857143rem 0.357142857rem;
+ font-size: 10px;
+ font-size: 0.714285714rem;
+}
+
+.comments-area .bypostauthor cite b {
+ font-weight: bold;
+}
+
+a.comment-reply-link,
+a.comment-edit-link {
+ color: #686868;
+ font-size: 13px;
+ font-size: 0.928571429rem;
+ line-height: 1.846153846;
+}
+
+a.comment-reply-link:hover,
+a.comment-edit-link:hover {
+ color: #21759b;
+}
+
+.commentlist .pingback {
+ line-height: 1.714285714;
+ margin-bottom: 24px;
+ margin-bottom: 1.714285714rem;
+}
+
+/* Comment form */
+#respond {
+ margin-top: 48px;
+ margin-top: 3.428571429rem;
+}
+
+#respond h3#reply-title {
+ font-size: 16px;
+ font-size: 1.142857143rem;
+ line-height: 1.5;
+}
+
+#respond h3#reply-title #cancel-comment-reply-link {
+ margin-left: 10px;
+ margin-left: 0.714285714rem;
+ font-weight: normal;
+ font-size: 12px;
+ font-size: 0.857142857rem;
+}
+
+#respond form {
+ margin: 24px 0;
+ margin: 1.714285714rem 0;
+}
+
+#respond form p {
+ margin: 11px 0;
+ margin: 0.785714286rem 0;
+}
+
+#respond form p.logged-in-as {
+ margin-bottom: 24px;
+ margin-bottom: 1.714285714rem;
+}
+
+#respond form label {
+ display: block;
+ line-height: 1.714285714;
+}
+
+#respond form input[type="text"],
+#respond form textarea {
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+ font-size: 12px;
+ font-size: 0.857142857rem;
+ line-height: 1.714285714;
+ padding: 10px;
+ padding: 0.714285714rem;
+ width: 100%;
+}
+
+#respond form p.form-allowed-tags {
+ margin: 0;
+ font-size: 12px;
+ font-size: 0.857142857rem;
+ line-height: 2;
+ color: #5e5e5e;
+}
+
+#respond #wp-comment-cookies-consent {
+ margin: 0 10px 0 0;
+}
+
+#respond .comment-form-cookies-consent label {
+ display: inline;
+}
+
+.required {
+ color: red;
+}
+
+
+/* =Front page template
+-------------------------------------------------------------- */
+
+.entry-page-image {
+ margin-bottom: 14px;
+ margin-bottom: 1rem;
+}
+
+.template-front-page .site-content article {
+ border: 0;
+ margin-bottom: 0;
+}
+
+.template-front-page .widget-area {
+ clear: both;
+ float: none;
+ width: auto;
+ padding-top: 24px;
+ padding-top: 1.714285714rem;
+ border-top: 1px solid #ededed;
+}
+
+.template-front-page .widget-area .widget li {
+ margin: 8px 0 0;
+ margin: 0.571428571rem 0 0;
+ font-size: 13px;
+ font-size: 0.928571429rem;
+ line-height: 1.714285714;
+ list-style-type: square;
+ list-style-position: inside;
+}
+
+.template-front-page .widget-area .widget li a {
+ color: #757575;
+}
+
+.template-front-page .widget-area .widget li a:hover {
+ color: #21759b;
+}
+
+.template-front-page .widget-area .widget_text img {
+ float: left;
+ margin: 8px 24px 8px 0;
+ margin: 0.571428571rem 1.714285714rem 0.571428571rem 0;
+}
+
+
+/* =Widgets
+-------------------------------------------------------------- */
+
+.widget select {
+ max-width: 100%;
+}
+
+.widget-area .widget ul ul {
+ margin-left: 12px;
+ margin-left: 0.857142857rem;
+}
+
+.widget_rss li {
+ margin: 12px 0;
+ margin: 0.857142857rem 0;
+}
+
+.widget_recent_entries .post-date,
+.widget_rss .rss-date {
+ color: #aaa;
+ font-size: 11px;
+ font-size: 0.785714286rem;
+ margin-left: 12px;
+ margin-left: 0.857142857rem;
+}
+
+.wp-calendar-nav,
+#wp-calendar {
+ margin: 0;
+ width: 100%;
+ font-size: 13px;
+ font-size: 0.928571429rem;
+ line-height: 1.846153846;
+ color: #686868;
+}
+
+#wp-calendar th,
+#wp-calendar td,
+#wp-calendar caption {
+ text-align: left;
+}
+
+.wp-calendar-nav {
+ display: table;
+}
+
+.wp-calendar-nav span {
+ display: table-cell;
+}
+
+.wp-calendar-nav-next,
+#wp-calendar #next {
+ padding-right: 24px;
+ padding-right: 1.714285714rem;
+ text-align: right;
+}
+
+.widget_search label {
+ display: block;
+ font-size: 13px;
+ font-size: 0.928571429rem;
+ line-height: 1.846153846;
+}
+
+.widget_twitter li {
+ list-style-type: none;
+}
+
+.widget_twitter .timesince {
+ display: block;
+ text-align: right;
+}
+
+.tagcloud ul {
+ list-style-type: none;
+}
+
+.tagcloud ul li {
+ display: inline-block;
+}
+
+.widget-area .widget.widget_tag_cloud li {
+ line-height: 1;
+}
+
+.template-front-page .widget-area .widget.widget_tag_cloud li {
+ margin: 0;
+}
+
+.widget-area .gallery-columns-2.gallery-size-full .gallery-icon img,
+.widget-area .gallery-columns-3.gallery-size-full .gallery-icon img,
+.widget-area .gallery-columns-4.gallery-size-full .gallery-icon img,
+.widget-area .gallery-columns-5.gallery-size-full .gallery-icon img,
+.widget-area .gallery-columns-6 .gallery-icon img,
+.widget-area .gallery-columns-7 .gallery-icon img,
+.widget-area .gallery-columns-8 .gallery-icon img,
+.widget-area .gallery-columns-9 .gallery-icon img {
+ height: auto;
+ max-width: 80%;
+}
+
+/* =Plugins
+----------------------------------------------- */
+
+img#wpstats {
+ display: block;
+ margin: 0 auto 24px;
+ margin: 0 auto 1.714285714rem;
+}
+
+
+/* =Media queries
+-------------------------------------------------------------- */
+
+/* Does the same thing as ,
+ * but in the future W3C standard way. -ms- prefix is required for IE10+ to
+ * render responsive styling in Windows 8 "snapped" views; IE10+ does not honor
+ * the meta tag. See https://core.trac.wordpress.org/ticket/25888.
+ */
+@-ms-viewport {
+ width: device-width;
+}
+
+@viewport {
+ width: device-width;
+}
+
+/* Minimum width of 600 pixels. */
+@media screen and (min-width: 600px) {
+ .author-avatar {
+ float: left;
+ margin-top: 8px;
+ margin-top: 0.571428571rem;
+ }
+
+ .author-description {
+ float: right;
+ width: 80%;
+ }
+
+ .site {
+ margin: 0 auto;
+ max-width: 960px;
+ max-width: 68.571428571rem;
+ overflow: hidden;
+ }
+
+ .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;
+ font-size: 1.857142857rem;
+ line-height: 1.846153846;
+ margin-bottom: 0;
+ }
+
+ .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;
+ }
+
+ .main-navigation li a {
+ border-bottom: 0;
+ color: #6a6a6a;
+ line-height: 3.692307692;
+ text-transform: uppercase;
+ white-space: nowrap;
+ }
+
+ .main-navigation li a:hover,
+ .main-navigation li a:focus {
+ color: #000;
+ }
+
+ .main-navigation li {
+ margin: 0 40px 0 0;
+ margin: 0 2.857142857rem 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);
+ }
+
+ .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;
+ }
+
+ .main-navigation li ul li a {
+ background: #efefef;
+ border-bottom: 1px solid #ededed;
+ display: block;
+ font-size: 11px;
+ font-size: 0.785714286rem;
+ line-height: 2.181818182;
+ padding: 8px 10px;
+ padding: 0.571428571rem 0.714285714rem;
+ width: 180px;
+ width: 12.85714286rem;
+ white-space: normal;
+ }
+
+ .main-navigation li ul li a:hover,
+ .main-navigation li ul li a:focus {
+ 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;
+ }
+
+ .menu-toggle {
+ display: none;
+ }
+
+ .entry-header .entry-title {
+ font-size: 22px;
+ font-size: 1.571428571rem;
+ }
+
+ #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%;
+ }
+
+ .template-front-page .widget-area .widget,
+ .template-front-page.two-sidebars .widget-area .front-widgets {
+ float: left;
+ width: 51.875%;
+ margin-bottom: 24px;
+ margin-bottom: 1.714285714rem;
+ }
+
+ .template-front-page .widget-area .widget:nth-child(odd) {
+ clear: right;
+ }
+
+ .template-front-page .widget-area .widget:nth-child(even),
+ .template-front-page.two-sidebars .widget-area .front-widgets + .front-widgets {
+ float: right;
+ width: 39.0625%;
+ margin: 0 0 24px;
+ margin: 0 0 1.714285714rem;
+ }
+
+ .template-front-page.two-sidebars .widget,
+ .template-front-page.two-sidebars .widget:nth-child(even) {
+ float: none;
+ width: auto;
+ }
+
+ .commentlist .children {
+ margin-left: 48px;
+ margin-left: 3.428571429rem;
+ }
+}
+
+/* Minimum width of 960 pixels. */
+@media screen and (min-width: 960px) {
+ body {
+ background-color: #e6e6e6;
+ }
+
+ body .site {
+ padding: 0 40px;
+ padding: 0 2.857142857rem;
+ margin-top: 48px;
+ margin-top: 3.428571429rem;
+ margin-bottom: 48px;
+ margin-bottom: 3.428571429rem;
+ box-shadow: 0 2px 6px rgba(100, 100, 100, 0.3);
+ }
+
+ body.custom-background-empty {
+ background-color: #fff;
+ }
+
+ body.custom-background-empty .site,
+ body.custom-background-white .site {
+ padding: 0;
+ margin-top: 0;
+ margin-bottom: 0;
+ box-shadow: none;
+ }
+}
+
+
+/* =Print
+----------------------------------------------- */
+
+@media print {
+ body {
+ background: none !important;
+ color: #000;
+ font-size: 10pt;
+ }
+
+ footer a[rel=bookmark]:link:after,
+ footer a[rel=bookmark]:visited:after {
+ content: " [" attr(href) "] "; /* Show URLs */
+ }
+
+ a {
+ text-decoration: none;
+ }
+
+ .entry-content img,
+ .comment-content img,
+ .author-avatar img,
+ img.wp-post-image {
+ border-radius: 0;
+ box-shadow: none;
+ }
+
+ .site {
+ clear: both !important;
+ display: block !important;
+ float: none !important;
+ max-width: 100%;
+ position: relative !important;
+ }
+
+ .site-header {
+ margin-bottom: 72px;
+ margin-bottom: 5.142857143rem;
+ text-align: left;
+ }
+
+ .site-header h1 {
+ font-size: 21pt;
+ line-height: 1;
+ text-align: left;
+ }
+
+ .site-header h2 {
+ color: #000;
+ font-size: 10pt;
+ text-align: left;
+ }
+
+ .site-header h1 a,
+ .site-header h2 a {
+ color: #000;
+ }
+
+ .author-avatar,
+ #colophon,
+ #respond,
+ .commentlist .comment-edit-link,
+ .commentlist .reply,
+ .entry-header .comments-link,
+ .entry-meta .edit-link a,
+ .page-link,
+ .site-content nav,
+ .widget-area,
+ img.header-image,
+ .main-navigation {
+ display: none;
+ }
+
+ .wrapper {
+ border-top: none;
+ box-shadow: none;
+ }
+
+ .site-content {
+ margin: 0;
+ width: auto;
+ }
+
+ .entry-header .entry-title,
+ .entry-title {
+ font-size: 21pt;
+ }
+
+ footer.entry-meta,
+ footer.entry-meta a {
+ color: #444;
+ font-size: 10pt;
+ }
+
+ .author-description {
+ float: none;
+ width: auto;
+ }
+
+ /* Comments */
+ .commentlist > li.comment {
+ background: none;
+ position: relative;
+ width: auto;
+ }
+
+ .commentlist .avatar {
+ height: 39px;
+ left: 2.2em;
+ top: 2.2em;
+ width: 39px;
+ }
+
+ .comments-area article header cite,
+ .comments-area article header time {
+ margin-left: 50px;
+ margin-left: 3.57142857rem;
+ }
+}
+
+.breadcrumb
+div {
+ display: inline;
+ font-size: 13px;
+ margin-left: -3px;
+}
+
+#wp-auto-top {
+ position: fixed;
+ top: 45%;
+ right: 50%;
+ display: block;
+ margin-right: -540px;
+ z-index: 9999;
+}
+
+#wp-auto-top-top, #wp-auto-top-comment, #wp-auto-top-bottom {
+ background: url(https://www.lylinux.org/wp-content/plugins/wp-auto-top/img/1.png) no-repeat;
+ position: relative;
+ cursor: pointer;
+ height: 25px;
+ width: 29px;
+ margin: 10px 0 0;
+}
+
+#wp-auto-top-comment {
+ background-position: left -30px;
+ height: 32px;
+}
+
+#wp-auto-top-bottom {
+ background-position: left -68px;
+}
+
+#wp-auto-top-comment:hover {
+ background-position: right -30px;
+}
+
+#wp-auto-top-top:hover {
+ background-position: right 0;
+}
+
+#wp-auto-top-bottom:hover {
+ background-position: right -68px;
+}
+
+.widget-login {
+ margin-top: 15px !important;
+}
+
+/* ------------------------------------------------------------------------- *
+ * Comments
+/* ------------------------------------------------------------------------- */
+#comments {
+ margin-top: 20px;
+}
+
+#pinglist-container {
+ display: none;
+}
+
+.comment-tabs {
+ margin-bottom: 20px;
+ font-size: 15px;
+ border-bottom: 2px solid #e5e5e5;
+}
+
+.comment-tabs li {
+ float: left;
+ margin-bottom: -2px;
+}
+
+.comment-tabs li a {
+ display: block;
+ padding: 0 10px 10px;
+ font-weight: 600;
+ color: #aaa;
+ border-bottom: 2px solid #e5e5e5;
+}
+
+.comment-tabs li a:hover {
+ color: #444;
+ border-color: #ccc;
+}
+
+.comment-tabs li span {
+ margin-left: 8px;
+ padding: 0 6px;
+ border-radius: 4px;
+ background-color: #e5e5e5;
+}
+
+.comment-tabs li i {
+ margin-right: 6px;
+}
+
+.comment-tabs li.active a {
+ color: #e8554e;
+ border-bottom-color: #e8554e;
+}
+
+.commentlist, .pinglist {
+ margin-bottom: 20px;
+}
+
+.commentlist li, .pinglist li {
+ padding-left: 60px;
+ font-size: 14px;
+ line-height: 22px;
+ font-weight: 400;
+}
+
+.commentlist .comment-body, .pinglist li {
+ position: relative;
+ padding-bottom: 20px;
+ clear: both;
+ word-break: break-all;
+}
+
+.commentlist .comment-author,
+.commentlist .comment-meta,
+.commentlist .comment-awaiting-moderation {
+ float: left;
+ display: block;
+ font-size: 13px;
+ line-height: 22px;
+}
+
+.commentlist .comment-author {
+ margin-right: 6px;
+}
+
+.commentlist .fn, .pinglist .ping-link {
+ color: #444;
+ font-size: 13px;
+ font-style: normal;
+ font-weight: 600;
+}
+
+.commentlist .says {
+ display: none;
+}
+
+.commentlist .avatar {
+ position: absolute;
+ left: -60px;
+ top: 0;
+ width: 48px;
+ height: 48px;
+ border-radius: 100%;
+}
+
+.commentlist .comment-meta:before, .pinglist .ping-meta:before {
+
+ vertical-align: 4%;
+ margin-right: 3px;
+ font-size: 10px;
+ font-family: FontAwesome;
+ color: #ccc;
+}
+
+.commentlist .comment-meta a, .pinglist .ping-meta {
+ color: #aaa;
+}
+
+.commentlist .reply {
+ font-size: 13px;
+ line-height: 16px;
+}
+
+.commentlist .reply a,
+.commentlist .comment-reply-chain {
+ color: #aaa;
+}
+
+.commentlist .reply a:hover,
+.commentlist .comment-reply-chain:hover {
+ color: #444;
+}
+
+.comment-awaiting-moderation {
+ color: #e8554e;
+ font-style: normal;
+}
+
+/* pings */
+.pinglist li {
+ padding-left: 0;
+}
+
+/* comment text */
+.commentlist .comment-body p {
+ margin-bottom: 8px;
+ color: #777;
+ clear: both;
+}
+
+.commentlist .comment-body strong {
+ font-weight: 600;
+}
+
+.commentlist .comment-body ol li {
+ margin-left: 2em;
+ padding: 0;
+ list-style: decimal;
+}
+
+.commentlist .comment-body ul li {
+ margin-left: 2em;
+ padding: 0;
+ list-style: square;
+}
+
+/* post author & admin comment */
+.commentlist li.bypostauthor > .comment-body:after,
+.commentlist li.comment-author-admin > .comment-body:after {
+ display: block;
+ position: absolute;
+ content: "\f040";
+ width: 12px;
+ line-height: 12px;
+ font-style: normal;
+ font-family: FontAwesome;
+ text-align: center;
+ color: #fff;
+ background-color: #e8554e;
+}
+
+.commentlist li.comment-author-admin > .comment-body:after {
+ content: "\f005"; /* star for admin */
+}
+
+.commentlist li.bypostauthor > .comment-body:after,
+.commentlist li.comment-author-admin > .comment-body:after {
+ padding: 3px;
+ top: 32px;
+ left: -28px;
+ font-size: 12px;
+ border-radius: 100%;
+}
+
+.commentlist li li.bypostauthor > .comment-body:after,
+.commentlist li li.comment-author-admin > .comment-body:after {
+ padding: 2px;
+ top: 22px;
+ left: -26px;
+ font-size: 10px;
+ border-radius: 100%;
+}
+
+/* child comment */
+.commentlist li ul {
+}
+
+.commentlist li li {
+ margin: 0;
+ padding-left: 48px;
+}
+
+.commentlist li li .avatar {
+ top: 0;
+ left: -48px;
+ width: 36px;
+ height: 36px;
+}
+
+.commentlist li li .comment-meta {
+ left: 70px;
+}
+
+/* comments : nav
+/* ------------------------------------ */
+.comments-nav {
+ margin-bottom: 20px;
+}
+
+.comments-nav a {
+ font-weight: 600;
+}
+
+.comments-nav .nav-previous {
+ float: left;
+}
+
+.comments-nav .nav-next {
+ float: right;
+}
+
+/* comments : form
+/* ------------------------------------ */
+.logged-in-as,
+.comment-notes,
+.form-allowed-tags {
+ display: none;
+}
+
+#respond {
+ position: relative;
+}
+
+#reply-title {
+ margin-bottom: 20px;
+}
+
+li #reply-title {
+ margin: 0 !important;
+ padding: 0;
+ height: 0;
+ font-size: 0;
+ border-top: 0;
+}
+
+#cancel-comment-reply-link {
+ float: right;
+ bottom: 26px;
+ right: 20px;
+ font-size: 12px;
+ color: #999;
+}
+
+#cancel-comment-reply-link:hover {
+ color: #777;
+}
+
+#commentform {
+ margin-bottom: 20px;
+ padding: 10px 20px 20px;
+ border-radius: 4px;
+ background-color: #e5e5e5;
+}
+
+#commentform p.comment-form-author {
+ float: left;
+ width: 48%;
+}
+
+#commentform p.comment-form-email {
+ float: right;
+ width: 48%;
+}
+
+#commentform p.comment-form-url,
+#commentform p.comment-form-comment {
+ clear: both;
+}
+
+#commentform label {
+ display: block;
+ padding: 6px 0;
+ font-weight: 600;
+}
+
+#commentform input[type="text"],
+#commentform textarea {
+ max-width: 100%;
+ width: 100%;
+}
+
+#commentform textarea {
+ height: 100px;
+}
+
+#commentform p.form-submit {
+ margin-top: 10px;
+}
+
+.logged-in #reply-title {
+ margin-bottom: 20px;
+}
+
+.logged-in #commentform p.comment-form-comment {
+ margin-top: 10px;
+}
+
+.logged-in #commentform p.comment-form-comment label {
+ display: none;
+}
+
+.heading,
+#reply-title {
+ margin-bottom: 1em;
+ font-size: 18px;
+ font-weight: 600;
+ text-transform: uppercase;
+ color: #222;
+}
+
+.heading i {
+ margin-right: 6px;
+ font-size: 22px;
+}
+
+.group:before {
+ content: "";
+ display: table;
+}
+
+.group:after {
+ content: "";
+ display: table;
+ clear: both;
+}
+
+.cancel-comment {
+ margin: 0;
+ padding: 0;
+ border: 0;
+ font: inherit;
+ vertical-align: baseline;
+}
+
+#rocket {
+ position: fixed;
+ right: 50px;
+ bottom: 50px;
+ display: block;
+ visibility: hidden;
+ width: 26px;
+ height: 48px;
+ background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABoAAAB8CAYAAAB356CJAAAACXBIWXMAAAsTAAALEwEAmpwYAAAKTWlDQ1BQaG90b3Nob3AgSUNDIHByb2ZpbGUAAHjanVN3WJP3Fj7f92UPVkLY8LGXbIEAIiOsCMgQWaIQkgBhhBASQMWFiApWFBURnEhVxILVCkidiOKgKLhnQYqIWotVXDjuH9yntX167+3t+9f7vOec5/zOec8PgBESJpHmomoAOVKFPDrYH49PSMTJvYACFUjgBCAQ5svCZwXFAADwA3l4fnSwP/wBr28AAgBw1S4kEsfh/4O6UCZXACCRAOAiEucLAZBSAMguVMgUAMgYALBTs2QKAJQAAGx5fEIiAKoNAOz0ST4FANipk9wXANiiHKkIAI0BAJkoRyQCQLsAYFWBUiwCwMIAoKxAIi4EwK4BgFm2MkcCgL0FAHaOWJAPQGAAgJlCLMwAIDgCAEMeE80DIEwDoDDSv+CpX3CFuEgBAMDLlc2XS9IzFLiV0Bp38vDg4iHiwmyxQmEXKRBmCeQinJebIxNI5wNMzgwAABr50cH+OD+Q5+bk4eZm52zv9MWi/mvwbyI+IfHf/ryMAgQAEE7P79pf5eXWA3DHAbB1v2upWwDaVgBo3/ldM9sJoFoK0Hr5i3k4/EAenqFQyDwdHAoLC+0lYqG9MOOLPv8z4W/gi372/EAe/tt68ABxmkCZrcCjg/1xYW52rlKO58sEQjFu9+cj/seFf/2OKdHiNLFcLBWK8ViJuFAiTcd5uVKRRCHJleIS6X8y8R+W/QmTdw0ArIZPwE62B7XLbMB+7gECiw5Y0nYAQH7zLYwaC5EAEGc0Mnn3AACTv/mPQCsBAM2XpOMAALzoGFyolBdMxggAAESggSqwQQcMwRSswA6cwR28wBcCYQZEQAwkwDwQQgbkgBwKoRiWQRlUwDrYBLWwAxqgEZrhELTBMTgN5+ASXIHrcBcGYBiewhi8hgkEQcgIE2EhOogRYo7YIs4IF5mOBCJhSDSSgKQg6YgUUSLFyHKkAqlCapFdSCPyLXIUOY1cQPqQ28ggMor8irxHMZSBslED1AJ1QLmoHxqKxqBz0XQ0D12AlqJr0Rq0Hj2AtqKn0UvodXQAfYqOY4DRMQ5mjNlhXIyHRWCJWBomxxZj5Vg1Vo81Yx1YN3YVG8CeYe8IJAKLgBPsCF6EEMJsgpCQR1hMWEOoJewjtBK6CFcJg4Qxwicik6hPtCV6EvnEeGI6sZBYRqwm7iEeIZ4lXicOE1+TSCQOyZLkTgohJZAySQtJa0jbSC2kU6Q+0hBpnEwm65Btyd7kCLKArCCXkbeQD5BPkvvJw+S3FDrFiOJMCaIkUqSUEko1ZT/lBKWfMkKZoKpRzame1AiqiDqfWkltoHZQL1OHqRM0dZolzZsWQ8ukLaPV0JppZ2n3aC/pdLoJ3YMeRZfQl9Jr6Afp5+mD9HcMDYYNg8dIYigZaxl7GacYtxkvmUymBdOXmchUMNcyG5lnmA+Yb1VYKvYqfBWRyhKVOpVWlX6V56pUVXNVP9V5qgtUq1UPq15WfaZGVbNQ46kJ1Bar1akdVbupNq7OUndSj1DPUV+jvl/9gvpjDbKGhUaghkijVGO3xhmNIRbGMmXxWELWclYD6yxrmE1iW7L57Ex2Bfsbdi97TFNDc6pmrGaRZp3mcc0BDsax4PA52ZxKziHODc57LQMtPy2x1mqtZq1+rTfaetq+2mLtcu0W7eva73VwnUCdLJ31Om0693UJuja6UbqFutt1z+o+02PreekJ9cr1Dund0Uf1bfSj9Rfq79bv0R83MDQINpAZbDE4Y/DMkGPoa5hpuNHwhOGoEctoupHEaKPRSaMnuCbuh2fjNXgXPmasbxxirDTeZdxrPGFiaTLbpMSkxeS+Kc2Ua5pmutG003TMzMgs3KzYrMnsjjnVnGueYb7ZvNv8jYWlRZzFSos2i8eW2pZ8ywWWTZb3rJhWPlZ5VvVW16xJ1lzrLOtt1ldsUBtXmwybOpvLtqitm63Edptt3xTiFI8p0in1U27aMez87ArsmuwG7Tn2YfYl9m32zx3MHBId1jt0O3xydHXMdmxwvOuk4TTDqcSpw+lXZxtnoXOd8zUXpkuQyxKXdpcXU22niqdun3rLleUa7rrStdP1o5u7m9yt2W3U3cw9xX2r+00umxvJXcM970H08PdY4nHM452nm6fC85DnL152Xlle+70eT7OcJp7WMG3I28Rb4L3Le2A6Pj1l+s7pAz7GPgKfep+Hvqa+It89viN+1n6Zfgf8nvs7+sv9j/i/4XnyFvFOBWABwQHlAb2BGoGzA2sDHwSZBKUHNQWNBbsGLww+FUIMCQ1ZH3KTb8AX8hv5YzPcZyya0RXKCJ0VWhv6MMwmTB7WEY6GzwjfEH5vpvlM6cy2CIjgR2yIuB9pGZkX+X0UKSoyqi7qUbRTdHF09yzWrORZ+2e9jvGPqYy5O9tqtnJ2Z6xqbFJsY+ybuIC4qriBeIf4RfGXEnQTJAntieTE2MQ9ieNzAudsmjOc5JpUlnRjruXcorkX5unOy553PFk1WZB8OIWYEpeyP+WDIEJQLxhP5aduTR0T8oSbhU9FvqKNolGxt7hKPJLmnVaV9jjdO31D+miGT0Z1xjMJT1IreZEZkrkj801WRNberM/ZcdktOZSclJyjUg1plrQr1zC3KLdPZisrkw3keeZtyhuTh8r35CP5c/PbFWyFTNGjtFKuUA4WTC+oK3hbGFt4uEi9SFrUM99m/ur5IwuCFny9kLBQuLCz2Lh4WfHgIr9FuxYji1MXdy4xXVK6ZHhp8NJ9y2jLspb9UOJYUlXyannc8o5Sg9KlpUMrglc0lamUycturvRauWMVYZVkVe9ql9VbVn8qF5VfrHCsqK74sEa45uJXTl/VfPV5bdra3kq3yu3rSOuk626s91m/r0q9akHV0IbwDa0b8Y3lG19tSt50oXpq9Y7NtM3KzQM1YTXtW8y2rNvyoTaj9nqdf13LVv2tq7e+2Sba1r/dd3vzDoMdFTve75TsvLUreFdrvUV99W7S7oLdjxpiG7q/5n7duEd3T8Wej3ulewf2Re/ranRvbNyvv7+yCW1SNo0eSDpw5ZuAb9qb7Zp3tXBaKg7CQeXBJ9+mfHvjUOihzsPcw83fmX+39QjrSHkr0jq/dawto22gPaG97+iMo50dXh1Hvrf/fu8x42N1xzWPV56gnSg98fnkgpPjp2Snnp1OPz3Umdx590z8mWtdUV29Z0PPnj8XdO5Mt1/3yfPe549d8Lxw9CL3Ytslt0utPa49R35w/eFIr1tv62X3y+1XPK509E3rO9Hv03/6asDVc9f41y5dn3m978bsG7duJt0cuCW69fh29u0XdwruTNxdeo94r/y+2v3qB/oP6n+0/rFlwG3g+GDAYM/DWQ/vDgmHnv6U/9OH4dJHzEfVI0YjjY+dHx8bDRq98mTOk+GnsqcTz8p+Vv9563Or59/94vtLz1j82PAL+YvPv655qfNy76uprzrHI8cfvM55PfGm/K3O233vuO+638e9H5ko/ED+UPPR+mPHp9BP9z7nfP78L/eE8/sl0p8zAAAAIGNIUk0AAHolAACAgwAA+f8AAIDpAAB1MAAA6mAAADqYAAAXb5JfxUYAAAbdSURBVHja5NlbbBRVGAfw5VID+LAK8cEoxqTgmw8kPPhwipTGxJTDUAVBQBMNKtZboiDE2ES8pFEjGhNkkCrin3JbZo4YCqloUOoKJCDIRWyRAgW6R3dobU2bJtj6+eCMTqczs2d3Zh6Mm3xpdvc7++vMnHNmzvlSRJQqJgA8B8AC8EQx7YoBxgD4CAC54i0Ao2KDAIwCsNGDOPF6nNBLAYgTiyNDAKYDGCwA/Q7gtpIhAKMBHC+AOPF5FGiBIuLEXaVCR4uEzKIhAHcViRCAP4OuVRi0pgSIACwvFurw/ohhGJTP56m7u5vy+TwZhuEHHVKGANzmh3R3d48IH2wQwPWq0CIv5ByJN/L5vN9RzVKF3vQ29kOcULlOQZAZ8YjWq0JHI1wjAvClKnTJr+sq9joCcEoV6itxDDmRU4UoYvT8f6GeiFCXKpSLCJ1XhU5GhI6oQs0RoT2qUENESFeFlkeEXlCFZkeEqlWhWyNCtxSE7GdsPSL0AYAxgRCACQB2xzAzEAABYMIIyEYOxIQ4sR/AOC+UiRlxYvM/EID5CSFO1DjQoYShFmfFMJgwdC0FYHzCCAEYck5dZ8LQWQdCwpAe19xWKCocqAzA1YSQiwBGuwfs2yHJpwDcEBJHQtqu9s4MU0KSHy+wBF0c1NsATPabVL/ye6IBML4AVAbgik/bvUGz9zyf5HrFTY9VPm0XBkFlAH7xrN5uVYQmAuh3P0Q6M3fQje81V/LWIne+1gY9oPglTwLQai+Wby8SugnAj/Y2W7nqqnyUz2cagDb7P24DoAXshI2Nsl9XZXdXb/etintjMBswVrJxQ0H3rMG4oYEAaOA/e+rqAqC6uKHyAKg8VsjGDnqQg7Hve9tQrQeqTQpKuybOfgDpRCDParAhkZKBC5pmQ9MShWysvtg2RSOZTKYu0WqLYRhjTdMUQghqbGxMrtpimuYuIQQJIWj79u3JVFsMw3jHQYQQfhuC0asthmFUCiGG3JAQgjZv3hxftaW5uXmMEOJnLyKEoK1bt8ZXbTEMY5kfIoSgHTt2xFdtEUK0BkE7d+6Mp9piGMY9QYgQgkzTjKfaYprmJvcPn/vhOHV8+D511j5EuUWzqXPZEmpd9x59/102WrVFCPGrG7myopZkzUyS2ox/Ijf3bjq/8mkvpl5tMQzjDvfRdKx7l+TcmZR7bAH1nThGf167Rn0njlHn0gcoV1NJrWvXlFZtMQzjaTfU+eQSknMqqP+n0+R+9Z05RXJOBXUsW1xatcUwjAY3lLu/iuScCvJ7SW0GXVlUXVq1xTTN/cOghfcGH5E2w++I1Kot3vFzceP6vy++5xrlli6gXM1MOvOxXlq1RQiR946by6tXkpw7vNfJmko698qL1NzUVFq1RQgx4DdIL2z7lDqfephyD2l05dlH6ELjRj9EvdoSNiMozA7qtQlVSAjx34H6IkJdqlBXROi86oBtjwgdUYUOR4T2qEJmREhXnVTrI0IvqEJLIg7YalWoXAUKqSwXrrZIzsZIzvSfT5woCTr2zdckOftAchZcbZGcTZCc7ZacUfu+vQWhTCYzAjq9vZEkZyQ5E5KzkdUWGzlgJ9GFjetLgtrerXcgkpztl5yN80IZVwJdWvVMQcizqiAAdPHZR90QSc7+rbZIzuZ7vqTcfZXUdvp0KOR9/j78bQvlaiq9EEnOahzokM+X1P7FnlBoy5Ytw69P4yd+CEnOWlKSs9GSs0G/hI41bxQ1WNtffj4IupaSnI0P+JJyD1bT8aNHlbr24ZYWys2rCoKGnFPXGYS1N+1S6nFnPtaDEJKcnXUgBCVdfrHWF9q2bdswqGPZ4jBId6DZIUnUnm0J7Qgnd5lhCEnOKhyoTHJ2NSjx0qurQifTCytqw5CLkrPR7gH7dkhy6HaZ5OzbkLarvTPDlJDkRQWg+UG9TXI22W9S/conWUrOrisAjbVPkbft3qDZe55P8qsqmx6SsxU+bRcGQWWSs19ciX9Izm5WhG6UnPW52vY4M3fQje81V3JR1RbJ2Vr32Cl0h50kOWuVnHVIzm4vErpJcvaj5MySnKlVWyRnw7bHLF1L9WbTWm823dabTZP9V7N0bUQ7yVnp1RZL16p69k0eshHqzaapZ9/kIUvX4q22WLqW7cpMJzfUlZlOlq5l44YGrQ3VwyBrQzVZujYYNzRg6Rr1tkz8G2qZSJaukaVrA7GfOkvX6LemqdSbTdNvTVMdKPZTV2fpGl3dNIt6s2m6ummWA9XFDZXbP0zdn93pIGTpWnncUMrStYMugOz3qSSgWg9UmxSUtnSt30b67feJQClL1xpsqMH5LClomg1NSxpKWbpW736v0v6vAQCo4CbBrd8RBQAAAABJRU5ErkJggg==") no-repeat 50% 0;
+ cursor: pointer;
+ -webkit-transition: all 0s;
+ transition: all 0s;
+}
+
+#rocket:hover {
+ background-position: 50% -62px;
+}
+
+#rocket.show {
+ visibility: visible;
+ opacity: 1;
+}
+
+#rocket.move {
+ background-position: 50% -62px;
+ -webkit-animation: toTop .8s ease-in;
+ animation: toTop .8s ease-in;
+ animation-fill-mode: forwards;
+ -webkit-animation-fill-mode: forwards;
+}
+
+.comment-markdown {
+ float: right;
+ font-size: small;
+}
+
+.breadcrumb {
+ margin-bottom: 20px;
+ list-style: none;
+ border-radius: 4px;
+}
+
+.breadcrumb > li {
+ display: inline-block;
+}
+
+.breadcrumb > li + li:before {
+ color: #ccc;
+ content: "/\00a0";
+}
+
+.breadcrumb > .active {
+ color: #777;
+}
+
+.break_line {
+ height: 1px;
+ border: none;
+ /*border-top: 1px dashed #f5d6d6;*/
+}
\ No newline at end of file
diff --git a/doc/第7周代码注释/blog/static/blog/fonts/fonts.css b/doc/第7周代码注释/blog/static/blog/fonts/fonts.css
new file mode 100644
index 00000000..c1a29cf0
--- /dev/null
+++ b/doc/第7周代码注释/blog/static/blog/fonts/fonts.css
@@ -0,0 +1,378 @@
+/* cyrillic-ext */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: italic;
+ font-weight: 300;
+ font-display: fallback;
+ src: url(memnYaGs126MiZpBA-UFUKWyV9hmIqOjjg.woff2) format('woff2');
+ unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
+}
+/* cyrillic */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: italic;
+ font-weight: 300;
+ font-display: fallback;
+ src: url(memnYaGs126MiZpBA-UFUKWyV9hvIqOjjg.woff2) format('woff2');
+ unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
+}
+/* greek-ext */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: italic;
+ font-weight: 300;
+ font-display: fallback;
+ src: url(memnYaGs126MiZpBA-UFUKWyV9hnIqOjjg.woff2) format('woff2');
+ unicode-range: U+1F00-1FFF;
+}
+/* greek */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: italic;
+ font-weight: 300;
+ font-display: fallback;
+ src: url(memnYaGs126MiZpBA-UFUKWyV9hoIqOjjg.woff2) format('woff2');
+ unicode-range: U+0370-03FF;
+}
+/* vietnamese */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: italic;
+ font-weight: 300;
+ font-display: fallback;
+ src: url(memnYaGs126MiZpBA-UFUKWyV9hkIqOjjg.woff2) format('woff2');
+ unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
+}
+/* latin-ext */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: italic;
+ font-weight: 300;
+ font-display: fallback;
+ src: url(memnYaGs126MiZpBA-UFUKWyV9hlIqOjjg.woff2) format('woff2');
+ unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+/* latin */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: italic;
+ font-weight: 300;
+ font-display: fallback;
+ src: url(memnYaGs126MiZpBA-UFUKWyV9hrIqM.woff2) format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+/* cyrillic-ext */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: italic;
+ font-weight: 400;
+ font-display: fallback;
+ src: url(mem6YaGs126MiZpBA-UFUK0Udc1UAw.woff2) format('woff2');
+ unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
+}
+/* cyrillic */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: italic;
+ font-weight: 400;
+ font-display: fallback;
+ src: url(mem6YaGs126MiZpBA-UFUK0ddc1UAw.woff2) format('woff2');
+ unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
+}
+/* greek-ext */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: italic;
+ font-weight: 400;
+ font-display: fallback;
+ src: url(mem6YaGs126MiZpBA-UFUK0Vdc1UAw.woff2) format('woff2');
+ unicode-range: U+1F00-1FFF;
+}
+/* greek */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: italic;
+ font-weight: 400;
+ font-display: fallback;
+ src: url(mem6YaGs126MiZpBA-UFUK0adc1UAw.woff2) format('woff2');
+ unicode-range: U+0370-03FF;
+}
+/* vietnamese */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: italic;
+ font-weight: 400;
+ font-display: fallback;
+ src: url(mem6YaGs126MiZpBA-UFUK0Wdc1UAw.woff2) format('woff2');
+ unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
+}
+/* latin-ext */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: italic;
+ font-weight: 400;
+ font-display: fallback;
+ src: url(mem6YaGs126MiZpBA-UFUK0Xdc1UAw.woff2) format('woff2');
+ unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+/* latin */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: italic;
+ font-weight: 400;
+ font-display: fallback;
+ src: url(mem6YaGs126MiZpBA-UFUK0Zdc0.woff2) format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+/* cyrillic-ext */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: italic;
+ font-weight: 600;
+ font-display: fallback;
+ src: url(memnYaGs126MiZpBA-UFUKXGUdhmIqOjjg.woff2) format('woff2');
+ unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
+}
+/* cyrillic */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: italic;
+ font-weight: 600;
+ font-display: fallback;
+ src: url(memnYaGs126MiZpBA-UFUKXGUdhvIqOjjg.woff2) format('woff2');
+ unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
+}
+/* greek-ext */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: italic;
+ font-weight: 600;
+ font-display: fallback;
+ src: url(memnYaGs126MiZpBA-UFUKXGUdhnIqOjjg.woff2) format('woff2');
+ unicode-range: U+1F00-1FFF;
+}
+/* greek */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: italic;
+ font-weight: 600;
+ font-display: fallback;
+ src: url(memnYaGs126MiZpBA-UFUKXGUdhoIqOjjg.woff2) format('woff2');
+ unicode-range: U+0370-03FF;
+}
+/* vietnamese */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: italic;
+ font-weight: 600;
+ font-display: fallback;
+ src: url(memnYaGs126MiZpBA-UFUKXGUdhkIqOjjg.woff2) format('woff2');
+ unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
+}
+/* latin-ext */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: italic;
+ font-weight: 600;
+ font-display: fallback;
+ src: url(memnYaGs126MiZpBA-UFUKXGUdhlIqOjjg.woff2) format('woff2');
+ unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+/* latin */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: italic;
+ font-weight: 600;
+ font-display: fallback;
+ src: url(memnYaGs126MiZpBA-UFUKXGUdhrIqM.woff2) format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+/* cyrillic-ext */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: normal;
+ font-weight: 300;
+ font-display: fallback;
+ src: url(mem5YaGs126MiZpBA-UN_r8OX-hpOqc.woff2) format('woff2');
+ unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
+}
+/* cyrillic */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: normal;
+ font-weight: 300;
+ font-display: fallback;
+ src: url(mem5YaGs126MiZpBA-UN_r8OVuhpOqc.woff2) format('woff2');
+ unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
+}
+/* greek-ext */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: normal;
+ font-weight: 300;
+ font-display: fallback;
+ src: url(mem5YaGs126MiZpBA-UN_r8OXuhpOqc.woff2) format('woff2');
+ unicode-range: U+1F00-1FFF;
+}
+/* greek */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: normal;
+ font-weight: 300;
+ font-display: fallback;
+ src: url(mem5YaGs126MiZpBA-UN_r8OUehpOqc.woff2) format('woff2');
+ unicode-range: U+0370-03FF;
+}
+/* vietnamese */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: normal;
+ font-weight: 300;
+ font-display: fallback;
+ src: url(mem5YaGs126MiZpBA-UN_r8OXehpOqc.woff2) format('woff2');
+ unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
+}
+/* latin-ext */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: normal;
+ font-weight: 300;
+ font-display: fallback;
+ src: url(mem5YaGs126MiZpBA-UN_r8OXOhpOqc.woff2) format('woff2');
+ unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+/* latin */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: normal;
+ font-weight: 300;
+ font-display: fallback;
+ src: url(mem5YaGs126MiZpBA-UN_r8OUuhp.woff2) format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+/* cyrillic-ext */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: normal;
+ font-weight: 400;
+ font-display: fallback;
+ src: url(mem8YaGs126MiZpBA-UFWJ0bbck.woff2) format('woff2');
+ unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
+}
+/* cyrillic */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: normal;
+ font-weight: 400;
+ font-display: fallback;
+ src: url(mem8YaGs126MiZpBA-UFUZ0bbck.woff2) format('woff2');
+ unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
+}
+/* greek-ext */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: normal;
+ font-weight: 400;
+ font-display: fallback;
+ src: url(mem8YaGs126MiZpBA-UFWZ0bbck.woff2) format('woff2');
+ unicode-range: U+1F00-1FFF;
+}
+/* greek */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: normal;
+ font-weight: 400;
+ font-display: fallback;
+ src: url(mem8YaGs126MiZpBA-UFVp0bbck.woff2) format('woff2');
+ unicode-range: U+0370-03FF;
+}
+/* vietnamese */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: normal;
+ font-weight: 400;
+ font-display: fallback;
+ src: url(mem8YaGs126MiZpBA-UFWp0bbck.woff2) format('woff2');
+ unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
+}
+/* latin-ext */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: normal;
+ font-weight: 400;
+ font-display: fallback;
+ src: url(mem8YaGs126MiZpBA-UFW50bbck.woff2) format('woff2');
+ unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+/* latin */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: normal;
+ font-weight: 400;
+ font-display: fallback;
+ src: url(mem8YaGs126MiZpBA-UFVZ0b.woff2) format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+/* cyrillic-ext */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: normal;
+ font-weight: 600;
+ font-display: fallback;
+ src: url(mem5YaGs126MiZpBA-UNirkOX-hpOqc.woff2) format('woff2');
+ unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
+}
+/* cyrillic */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: normal;
+ font-weight: 600;
+ font-display: fallback;
+ src: url(mem5YaGs126MiZpBA-UNirkOVuhpOqc.woff2) format('woff2');
+ unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
+}
+/* greek-ext */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: normal;
+ font-weight: 600;
+ font-display: fallback;
+ src: url(mem5YaGs126MiZpBA-UNirkOXuhpOqc.woff2) format('woff2');
+ unicode-range: U+1F00-1FFF;
+}
+/* greek */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: normal;
+ font-weight: 600;
+ font-display: fallback;
+ src: url(mem5YaGs126MiZpBA-UNirkOUehpOqc.woff2) format('woff2');
+ unicode-range: U+0370-03FF;
+}
+/* vietnamese */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: normal;
+ font-weight: 600;
+ font-display: fallback;
+ src: url(mem5YaGs126MiZpBA-UNirkOXehpOqc.woff2) format('woff2');
+ unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
+}
+/* latin-ext */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: normal;
+ font-weight: 600;
+ font-display: fallback;
+ src: url(mem5YaGs126MiZpBA-UNirkOXOhpOqc.woff2) format('woff2');
+ unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+/* latin */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: normal;
+ font-weight: 600;
+ font-display: fallback;
+ src: url(mem5YaGs126MiZpBA-UNirkOUuhp.woff2) format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
diff --git a/doc/第7周代码注释/blog/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OUehpOqc.woff2 b/doc/第7周代码注释/blog/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OUehpOqc.woff2
new file mode 100644
index 00000000..2c47cc54
Binary files /dev/null and b/doc/第7周代码注释/blog/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OUehpOqc.woff2 differ
diff --git a/doc/第7周代码注释/blog/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OUuhp.woff2 b/doc/第7周代码注释/blog/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OUuhp.woff2
new file mode 100644
index 00000000..601706aa
Binary files /dev/null and b/doc/第7周代码注释/blog/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OUuhp.woff2 differ
diff --git a/doc/第7周代码注释/blog/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OVuhpOqc.woff2 b/doc/第7周代码注释/blog/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OVuhpOqc.woff2
new file mode 100644
index 00000000..119f1d71
Binary files /dev/null and b/doc/第7周代码注释/blog/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OVuhpOqc.woff2 differ
diff --git a/doc/第7周代码注释/blog/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OX-hpOqc.woff2 b/doc/第7周代码注释/blog/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OX-hpOqc.woff2
new file mode 100644
index 00000000..d56688ff
Binary files /dev/null and b/doc/第7周代码注释/blog/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OX-hpOqc.woff2 differ
diff --git a/doc/第7周代码注释/blog/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OXOhpOqc.woff2 b/doc/第7周代码注释/blog/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OXOhpOqc.woff2
new file mode 100644
index 00000000..e1f546c2
Binary files /dev/null and b/doc/第7周代码注释/blog/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OXOhpOqc.woff2 differ
diff --git a/doc/第7周代码注释/blog/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OXehpOqc.woff2 b/doc/第7周代码注释/blog/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OXehpOqc.woff2
new file mode 100644
index 00000000..0f17e3d3
Binary files /dev/null and b/doc/第7周代码注释/blog/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OXehpOqc.woff2 differ
diff --git a/doc/第7周代码注释/blog/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OXuhpOqc.woff2 b/doc/第7周代码注释/blog/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OXuhpOqc.woff2
new file mode 100644
index 00000000..50d81832
Binary files /dev/null and b/doc/第7周代码注释/blog/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OXuhpOqc.woff2 differ
diff --git a/doc/第7周代码注释/blog/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOUehpOqc.woff2 b/doc/第7周代码注释/blog/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOUehpOqc.woff2
new file mode 100644
index 00000000..b9351989
Binary files /dev/null and b/doc/第7周代码注释/blog/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOUehpOqc.woff2 differ
diff --git a/doc/第7周代码注释/blog/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOUuhp.woff2 b/doc/第7周代码注释/blog/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOUuhp.woff2
new file mode 100644
index 00000000..d77bb4c3
Binary files /dev/null and b/doc/第7周代码注释/blog/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOUuhp.woff2 differ
diff --git a/doc/第7周代码注释/blog/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOVuhpOqc.woff2 b/doc/第7周代码注释/blog/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOVuhpOqc.woff2
new file mode 100644
index 00000000..e293ffce
Binary files /dev/null and b/doc/第7周代码注释/blog/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOVuhpOqc.woff2 differ
diff --git a/doc/第7周代码注释/blog/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOX-hpOqc.woff2 b/doc/第7周代码注释/blog/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOX-hpOqc.woff2
new file mode 100644
index 00000000..46fd61bf
Binary files /dev/null and b/doc/第7周代码注释/blog/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOX-hpOqc.woff2 differ
diff --git a/doc/第7周代码注释/blog/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOXOhpOqc.woff2 b/doc/第7周代码注释/blog/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOXOhpOqc.woff2
new file mode 100644
index 00000000..88a1616a
Binary files /dev/null and b/doc/第7周代码注释/blog/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOXOhpOqc.woff2 differ
diff --git a/doc/第7周代码注释/blog/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOXehpOqc.woff2 b/doc/第7周代码注释/blog/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOXehpOqc.woff2
new file mode 100644
index 00000000..2100b6bb
Binary files /dev/null and b/doc/第7周代码注释/blog/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOXehpOqc.woff2 differ
diff --git a/doc/第7周代码注释/blog/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOXuhpOqc.woff2 b/doc/第7周代码注释/blog/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOXuhpOqc.woff2
new file mode 100644
index 00000000..d54c7c0f
Binary files /dev/null and b/doc/第7周代码注释/blog/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOXuhpOqc.woff2 differ
diff --git a/doc/第7周代码注释/blog/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0Udc1UAw.woff2 b/doc/第7周代码注释/blog/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0Udc1UAw.woff2
new file mode 100644
index 00000000..683014d7
Binary files /dev/null and b/doc/第7周代码注释/blog/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0Udc1UAw.woff2 differ
diff --git a/doc/第7周代码注释/blog/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0Vdc1UAw.woff2 b/doc/第7周代码注释/blog/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0Vdc1UAw.woff2
new file mode 100644
index 00000000..72eb246f
Binary files /dev/null and b/doc/第7周代码注释/blog/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0Vdc1UAw.woff2 differ
diff --git a/doc/第7周代码注释/blog/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0Wdc1UAw.woff2 b/doc/第7周代码注释/blog/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0Wdc1UAw.woff2
new file mode 100644
index 00000000..6da55624
Binary files /dev/null and b/doc/第7周代码注释/blog/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0Wdc1UAw.woff2 differ
diff --git a/doc/第7周代码注释/blog/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0Xdc1UAw.woff2 b/doc/第7周代码注释/blog/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0Xdc1UAw.woff2
new file mode 100644
index 00000000..2f22c670
Binary files /dev/null and b/doc/第7周代码注释/blog/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0Xdc1UAw.woff2 differ
diff --git a/doc/第7周代码注释/blog/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0Zdc0.woff2 b/doc/第7周代码注释/blog/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0Zdc0.woff2
new file mode 100644
index 00000000..28c6c76e
Binary files /dev/null and b/doc/第7周代码注释/blog/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0Zdc0.woff2 differ
diff --git a/doc/第7周代码注释/blog/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0adc1UAw.woff2 b/doc/第7周代码注释/blog/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0adc1UAw.woff2
new file mode 100644
index 00000000..fdeb9a4a
Binary files /dev/null and b/doc/第7周代码注释/blog/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0adc1UAw.woff2 differ
diff --git a/doc/第7周代码注释/blog/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0ddc1UAw.woff2 b/doc/第7周代码注释/blog/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0ddc1UAw.woff2
new file mode 100644
index 00000000..2a48105c
Binary files /dev/null and b/doc/第7周代码注释/blog/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0ddc1UAw.woff2 differ
diff --git a/doc/第7周代码注释/blog/static/blog/fonts/mem8YaGs126MiZpBA-UFUZ0bbck.woff2 b/doc/第7周代码注释/blog/static/blog/fonts/mem8YaGs126MiZpBA-UFUZ0bbck.woff2
new file mode 100644
index 00000000..1ddef142
Binary files /dev/null and b/doc/第7周代码注释/blog/static/blog/fonts/mem8YaGs126MiZpBA-UFUZ0bbck.woff2 differ
diff --git a/doc/第7周代码注释/blog/static/blog/fonts/mem8YaGs126MiZpBA-UFVZ0b.woff2 b/doc/第7周代码注释/blog/static/blog/fonts/mem8YaGs126MiZpBA-UFVZ0b.woff2
new file mode 100644
index 00000000..1d5e847b
Binary files /dev/null and b/doc/第7周代码注释/blog/static/blog/fonts/mem8YaGs126MiZpBA-UFVZ0b.woff2 differ
diff --git a/doc/第7周代码注释/blog/static/blog/fonts/mem8YaGs126MiZpBA-UFVp0bbck.woff2 b/doc/第7周代码注释/blog/static/blog/fonts/mem8YaGs126MiZpBA-UFVp0bbck.woff2
new file mode 100644
index 00000000..0e22822b
Binary files /dev/null and b/doc/第7周代码注释/blog/static/blog/fonts/mem8YaGs126MiZpBA-UFVp0bbck.woff2 differ
diff --git a/doc/第7周代码注释/blog/static/blog/fonts/mem8YaGs126MiZpBA-UFW50bbck.woff2 b/doc/第7周代码注释/blog/static/blog/fonts/mem8YaGs126MiZpBA-UFW50bbck.woff2
new file mode 100644
index 00000000..f6210055
Binary files /dev/null and b/doc/第7周代码注释/blog/static/blog/fonts/mem8YaGs126MiZpBA-UFW50bbck.woff2 differ
diff --git a/doc/第7周代码注释/blog/static/blog/fonts/mem8YaGs126MiZpBA-UFWJ0bbck.woff2 b/doc/第7周代码注释/blog/static/blog/fonts/mem8YaGs126MiZpBA-UFWJ0bbck.woff2
new file mode 100644
index 00000000..49018f9c
Binary files /dev/null and b/doc/第7周代码注释/blog/static/blog/fonts/mem8YaGs126MiZpBA-UFWJ0bbck.woff2 differ
diff --git a/doc/第7周代码注释/blog/static/blog/fonts/mem8YaGs126MiZpBA-UFWZ0bbck.woff2 b/doc/第7周代码注释/blog/static/blog/fonts/mem8YaGs126MiZpBA-UFWZ0bbck.woff2
new file mode 100644
index 00000000..a69a2efa
Binary files /dev/null and b/doc/第7周代码注释/blog/static/blog/fonts/mem8YaGs126MiZpBA-UFWZ0bbck.woff2 differ
diff --git a/doc/第7周代码注释/blog/static/blog/fonts/mem8YaGs126MiZpBA-UFWp0bbck.woff2 b/doc/第7周代码注释/blog/static/blog/fonts/mem8YaGs126MiZpBA-UFWp0bbck.woff2
new file mode 100644
index 00000000..fb5fb994
Binary files /dev/null and b/doc/第7周代码注释/blog/static/blog/fonts/mem8YaGs126MiZpBA-UFWp0bbck.woff2 differ
diff --git a/doc/第7周代码注释/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hkIqOjjg.woff2 b/doc/第7周代码注释/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hkIqOjjg.woff2
new file mode 100644
index 00000000..db9a5bdb
Binary files /dev/null and b/doc/第7周代码注释/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hkIqOjjg.woff2 differ
diff --git a/doc/第7周代码注释/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hlIqOjjg.woff2 b/doc/第7周代码注释/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hlIqOjjg.woff2
new file mode 100644
index 00000000..7a9e2e36
Binary files /dev/null and b/doc/第7周代码注释/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hlIqOjjg.woff2 differ
diff --git a/doc/第7周代码注释/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hmIqOjjg.woff2 b/doc/第7周代码注释/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hmIqOjjg.woff2
new file mode 100644
index 00000000..a9d17c0f
Binary files /dev/null and b/doc/第7周代码注释/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hmIqOjjg.woff2 differ
diff --git a/doc/第7周代码注释/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hnIqOjjg.woff2 b/doc/第7周代码注释/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hnIqOjjg.woff2
new file mode 100644
index 00000000..b76038f8
Binary files /dev/null and b/doc/第7周代码注释/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hnIqOjjg.woff2 differ
diff --git a/doc/第7周代码注释/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hoIqOjjg.woff2 b/doc/第7周代码注释/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hoIqOjjg.woff2
new file mode 100644
index 00000000..06a53d53
Binary files /dev/null and b/doc/第7周代码注释/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hoIqOjjg.woff2 differ
diff --git a/doc/第7周代码注释/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hrIqM.woff2 b/doc/第7周代码注释/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hrIqM.woff2
new file mode 100644
index 00000000..94dc4e47
Binary files /dev/null and b/doc/第7周代码注释/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hrIqM.woff2 differ
diff --git a/doc/第7周代码注释/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hvIqOjjg.woff2 b/doc/第7周代码注释/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hvIqOjjg.woff2
new file mode 100644
index 00000000..8197c399
Binary files /dev/null and b/doc/第7周代码注释/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hvIqOjjg.woff2 differ
diff --git a/doc/第7周代码注释/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhkIqOjjg.woff2 b/doc/第7周代码注释/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhkIqOjjg.woff2
new file mode 100644
index 00000000..b9cd540a
Binary files /dev/null and b/doc/第7周代码注释/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhkIqOjjg.woff2 differ
diff --git a/doc/第7周代码注释/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhlIqOjjg.woff2 b/doc/第7周代码注释/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhlIqOjjg.woff2
new file mode 100644
index 00000000..fa2e381c
Binary files /dev/null and b/doc/第7周代码注释/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhlIqOjjg.woff2 differ
diff --git a/doc/第7周代码注释/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhmIqOjjg.woff2 b/doc/第7周代码注释/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhmIqOjjg.woff2
new file mode 100644
index 00000000..da3f7ecf
Binary files /dev/null and b/doc/第7周代码注释/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhmIqOjjg.woff2 differ
diff --git a/doc/第7周代码注释/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhnIqOjjg.woff2 b/doc/第7周代码注释/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhnIqOjjg.woff2
new file mode 100644
index 00000000..0b421190
Binary files /dev/null and b/doc/第7周代码注释/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhnIqOjjg.woff2 differ
diff --git a/doc/第7周代码注释/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhoIqOjjg.woff2 b/doc/第7周代码注释/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhoIqOjjg.woff2
new file mode 100644
index 00000000..36bdef19
Binary files /dev/null and b/doc/第7周代码注释/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhoIqOjjg.woff2 differ
diff --git a/doc/第7周代码注释/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhrIqM.woff2 b/doc/第7周代码注释/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhrIqM.woff2
new file mode 100644
index 00000000..4b60ed41
Binary files /dev/null and b/doc/第7周代码注释/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhrIqM.woff2 differ
diff --git a/doc/第7周代码注释/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhvIqOjjg.woff2 b/doc/第7周代码注释/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhvIqOjjg.woff2
new file mode 100644
index 00000000..d2140906
Binary files /dev/null and b/doc/第7周代码注释/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhvIqOjjg.woff2 differ
diff --git a/doc/第7周代码注释/blog/static/blog/img/avatar.png b/doc/第7周代码注释/blog/static/blog/img/avatar.png
new file mode 100644
index 00000000..320756f0
Binary files /dev/null and b/doc/第7周代码注释/blog/static/blog/img/avatar.png differ
diff --git a/doc/第7周代码注释/blog/static/blog/img/icon-sn.svg b/doc/第7周代码注释/blog/static/blog/img/icon-sn.svg
new file mode 100644
index 00000000..2c2da0af
--- /dev/null
+++ b/doc/第7周代码注释/blog/static/blog/img/icon-sn.svg
@@ -0,0 +1 @@
+icon-sn
\ No newline at end of file
diff --git a/doc/第7周代码注释/blog/static/blog/js/blog.js b/doc/第7周代码注释/blog/static/blog/js/blog.js
new file mode 100644
index 00000000..c50dd7d7
--- /dev/null
+++ b/doc/第7周代码注释/blog/static/blog/js/blog.js
@@ -0,0 +1,91 @@
+/**
+ * Created by liangliang on 2016/11/20.
+ */
+
+
+function do_reply(parentid) {
+ console.log(parentid);
+ $("#id_parent_comment_id").val(parentid)
+ $("#commentform").appendTo($("#div-comment-" + parentid));
+ $("#reply-title").hide();
+ $("#cancel_comment").show();
+}
+
+function cancel_reply() {
+ $("#reply-title").show();
+ $("#cancel_comment").hide();
+ $("#id_parent_comment_id").val('')
+ $("#commentform").appendTo($("#respond"));
+}
+
+NProgress.start();
+NProgress.set(0.4);
+//Increment
+var interval = setInterval(function () {
+ NProgress.inc();
+}, 1000);
+$(document).ready(function () {
+ NProgress.done();
+ clearInterval(interval);
+});
+
+
+/** 侧边栏回到顶部 */
+var rocket = $('#rocket');
+
+$(window).on('scroll', debounce(slideTopSet, 300));
+
+function debounce(func, wait) {
+ var timeout;
+ return function () {
+ clearTimeout(timeout);
+ timeout = setTimeout(func, wait);
+ };
+}
+
+function slideTopSet() {
+ var top = $(document).scrollTop();
+
+ if (top > 200) {
+ rocket.addClass('show');
+ } else {
+ rocket.removeClass('show');
+ }
+}
+
+$(document).on('click', '#rocket', function (event) {
+ rocket.addClass('move');
+ $('body, html').animate({
+ scrollTop: 0
+ }, 800);
+});
+$(document).on('animationEnd', function () {
+ setTimeout(function () {
+ rocket.removeClass('move');
+ }, 400);
+
+});
+$(document).on('webkitAnimationEnd', function () {
+ setTimeout(function () {
+ rocket.removeClass('move');
+ }, 400);
+});
+
+
+window.onload = function () {
+ var replyLinks = document.querySelectorAll(".comment-reply-link");
+ for (var i = 0; i < replyLinks.length; i++) {
+ replyLinks[i].onclick = function () {
+ var pk = this.getAttribute("data-pk");
+ do_reply(pk);
+ };
+ }
+};
+
+// $(document).ready(function () {
+// var form = $('#i18n-form');
+// var selector = $('.i18n-select');
+// selector.on('change', function () {
+// form.submit();
+// });
+// });
\ No newline at end of file
diff --git a/doc/第7周代码注释/blog/static/blog/js/html5.js b/doc/第7周代码注释/blog/static/blog/js/html5.js
new file mode 100644
index 00000000..6168aacd
--- /dev/null
+++ b/doc/第7周代码注释/blog/static/blog/js/html5.js
@@ -0,0 +1,8 @@
+/*
+ HTML5 Shiv v3.7.0 | @afarkas @jdalton @jon_neal @rem | MIT/GPL2 Licensed
+*/
+(function(l,f){function m(){var a=e.elements;return"string"==typeof a?a.split(" "):a}function i(a){var b=n[a[o]];b||(b={},h++,a[o]=h,n[h]=b);return b}function p(a,b,c){b||(b=f);if(g)return b.createElement(a);c||(c=i(b));b=c.cache[a]?c.cache[a].cloneNode():r.test(a)?(c.cache[a]=c.createElem(a)).cloneNode():c.createElem(a);return b.canHaveChildren&&!s.test(a)?c.frag.appendChild(b):b}function t(a,b){if(!b.cache)b.cache={},b.createElem=a.createElement,b.createFrag=a.createDocumentFragment,b.frag=b.createFrag();
+a.createElement=function(c){return!e.shivMethods?b.createElem(c):p(c,a,b)};a.createDocumentFragment=Function("h,f","return function(){var n=f.cloneNode(),c=n.createElement;h.shivMethods&&("+m().join().replace(/[\w\-]+/g,function(a){b.createElem(a);b.frag.createElement(a);return'c("'+a+'")'})+");return n}")(e,b.frag)}function q(a){a||(a=f);var b=i(a);if(e.shivCSS&&!j&&!b.hasCSS){var c,d=a;c=d.createElement("p");d=d.getElementsByTagName("head")[0]||d.documentElement;c.innerHTML="x";
+c=d.insertBefore(c.lastChild,d.firstChild);b.hasCSS=!!c}g||t(a,b);return a}var k=l.html5||{},s=/^<|^(?:button|map|select|textarea|object|iframe|option|optgroup)$/i,r=/^(?:a|b|code|div|fieldset|h1|h2|h3|h4|h5|h6|i|label|li|ol|p|q|span|strong|style|table|tbody|td|th|tr|ul)$/i,j,o="_html5shiv",h=0,n={},g;(function(){try{var a=f.createElement("a");a.innerHTML=" ";j="hidden"in a;var b;if(!(b=1==a.childNodes.length)){f.createElement("a");var c=f.createDocumentFragment();b="undefined"==typeof c.cloneNode||
+"undefined"==typeof c.createDocumentFragment||"undefined"==typeof c.createElement}g=b}catch(d){g=j=!0}})();var e={elements:k.elements||"abbr article aside audio bdi canvas data datalist details dialog figcaption figure footer header hgroup main mark meter nav output progress section summary template time video",version:"3.7.0",shivCSS:!1!==k.shivCSS,supportsUnknownElements:g,shivMethods:!1!==k.shivMethods,type:"default",shivDocument:q,createElement:p,createDocumentFragment:function(a,b){a||(a=f);
+if(g)return a.createDocumentFragment();for(var b=b||i(a),c=b.frag.cloneNode(),d=0,e=m(),h=e.length;d+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(p.childNodes),p.childNodes),t[p.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!N[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&(U.test(t)||z.test(t))){(f=ee.test(t)&&ye(e.parentNode)||e)===e&&d.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=S)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+xe(l[o]);c=l.join(",")}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){N(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return g(t.replace($,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[S]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e&&e.namespaceURI,n=e&&(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:p;return r!=C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),p!=C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=S,!C.getElementsByName||!C.getElementsByName(S).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML=" ",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+S+"-]").length||v.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||v.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+S+"+*").length||v.push(".#.+[+~]"),e.querySelectorAll("\\\f"),v.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML=" ";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},j=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&y(p,e)?-1:t==C||t.ownerDocument==p&&y(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&E&&!N[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?S.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?S.grep(e,function(e){return e===n!==r}):"string"!=typeof n?S.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(S.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||D,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof S?t[0]:t,S.merge(this,S.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),N.test(r[1])&&S.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(S):S.makeArray(e,this)}).prototype=S.fn,D=S(E);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}S.fn.extend({has:function(e){var t=S(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=E.createDocumentFragment().appendChild(E.createElement("div")),(fe=E.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),y.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="",y.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML=" ",y.option=!!ce.lastChild;var ge={thead:[1,""],col:[2,""],tr:[2,""],td:[3,""],_default:[0,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?S.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;n",""]);var me=/<|?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function je(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&S(e).children("tbody")[0]||e}function De(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function qe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Le(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var _t,zt=[],Ut=/(=)\?(?=&|$)|\?\?/;S.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=zt.pop()||S.expando+"_"+wt.guid++;return this[e]=!0,e}}),S.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Ut.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Ut.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Ut,"$1"+r):!1!==e.jsonp&&(e.url+=(Tt.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||S.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?S(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,zt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),y.createHTMLDocument=((_t=E.implementation.createHTMLDocument("").body).innerHTML="",2===_t.childNodes.length),S.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&S(o).remove(),S.merge([],i.childNodes)));var r,i,o},S.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(S.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},S.expr.pseudos.animated=function(t){return S.grep(S.timers,function(e){return t===e.elem}).length},S.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=S.css(e,"position"),c=S(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=S.css(e,"top"),u=S.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,S.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},S.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){S.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===S.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===S.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=S(e).offset()).top+=S.css(e,"borderTopWidth",!0),i.left+=S.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-S.css(r,"marginTop",!0),left:t.left-i.left-S.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===S.css(e,"position"))e=e.offsetParent;return e||re})}}),S.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;S.fn[t]=function(e){return $(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),S.each(["top","left"],function(e,n){S.cssHooks[n]=Fe(y.pixelPosition,function(e,t){if(t)return t=We(e,n),Pe.test(t)?S(e).position()[n]+"px":t})}),S.each({Height:"height",Width:"width"},function(a,s){S.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){S.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return $(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?S.css(e,t,i):S.style(e,t,n,i)},s,n?e:void 0,n)}})}),S.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){S.fn[t]=function(e){return this.on(t,e)}}),S.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),S.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){S.fn[n]=function(e,t){return 0 a, .page_item_has_children > a', function( e ) {
+ var el = $( this ).parent( 'li' );
+
+ if ( ! el.hasClass( 'focus' ) ) {
+ e.preventDefault();
+ el.toggleClass( 'focus' );
+ el.siblings( '.focus').removeClass( 'focus' );
+ }
+ } );
+ }
+} )( jQuery );
diff --git a/doc/第7周代码注释/blog/static/blog/js/nprogress.js b/doc/第7周代码注释/blog/static/blog/js/nprogress.js
new file mode 100644
index 00000000..d29c2aac
--- /dev/null
+++ b/doc/第7周代码注释/blog/static/blog/js/nprogress.js
@@ -0,0 +1,480 @@
+/* NProgress, (c) 2013, 2014 Rico Sta. Cruz - http://ricostacruz.com/nprogress
+ * @license MIT */
+
+;(function(root, factory) {
+
+ if (typeof define === 'function' && define.amd) {
+ define(factory);
+ } else if (typeof exports === 'object') {
+ module.exports = factory();
+ } else {
+ root.NProgress = factory();
+ }
+
+})(this, function() {
+ var NProgress = {};
+
+ NProgress.version = '0.2.0';
+
+ var Settings = NProgress.settings = {
+ minimum: 0.08,
+ easing: 'linear',
+ positionUsing: '',
+ speed: 200,
+ trickle: true,
+ trickleSpeed: 200,
+ showSpinner: true,
+ barSelector: '[role="bar"]',
+ spinnerSelector: '[role="spinner"]',
+ parent: 'body',
+ template: ''
+ };
+
+ /**
+ * Updates configuration.
+ *
+ * NProgress.configure({
+ * minimum: 0.1
+ * });
+ */
+ NProgress.configure = function(options) {
+ var key, value;
+ for (key in options) {
+ value = options[key];
+ if (value !== undefined && options.hasOwnProperty(key)) Settings[key] = value;
+ }
+
+ return this;
+ };
+
+ /**
+ * Last number.
+ */
+
+ NProgress.status = null;
+
+ /**
+ * Sets the progress bar status, where `n` is a number from `0.0` to `1.0`.
+ *
+ * NProgress.set(0.4);
+ * NProgress.set(1.0);
+ */
+
+ NProgress.set = function(n) {
+ var started = NProgress.isStarted();
+
+ n = clamp(n, Settings.minimum, 1);
+ NProgress.status = (n === 1 ? null : n);
+
+ var progress = NProgress.render(!started),
+ bar = progress.querySelector(Settings.barSelector),
+ speed = Settings.speed,
+ ease = Settings.easing;
+
+ progress.offsetWidth; /* Repaint */
+
+ queue(function(next) {
+ // Set positionUsing if it hasn't already been set
+ if (Settings.positionUsing === '') Settings.positionUsing = NProgress.getPositioningCSS();
+
+ // Add transition
+ css(bar, barPositionCSS(n, speed, ease));
+
+ if (n === 1) {
+ // Fade out
+ css(progress, {
+ transition: 'none',
+ opacity: 1
+ });
+ progress.offsetWidth; /* Repaint */
+
+ setTimeout(function() {
+ css(progress, {
+ transition: 'all ' + speed + 'ms linear',
+ opacity: 0
+ });
+ setTimeout(function() {
+ NProgress.remove();
+ next();
+ }, speed);
+ }, speed);
+ } else {
+ setTimeout(next, speed);
+ }
+ });
+
+ return this;
+ };
+
+ NProgress.isStarted = function() {
+ return typeof NProgress.status === 'number';
+ };
+
+ /**
+ * Shows the progress bar.
+ * This is the same as setting the status to 0%, except that it doesn't go backwards.
+ *
+ * NProgress.start();
+ *
+ */
+ NProgress.start = function() {
+ if (!NProgress.status) NProgress.set(0);
+
+ var work = function() {
+ setTimeout(function() {
+ if (!NProgress.status) return;
+ NProgress.trickle();
+ work();
+ }, Settings.trickleSpeed);
+ };
+
+ if (Settings.trickle) work();
+
+ return this;
+ };
+
+ /**
+ * Hides the progress bar.
+ * This is the *sort of* the same as setting the status to 100%, with the
+ * difference being `done()` makes some placebo effect of some realistic motion.
+ *
+ * NProgress.done();
+ *
+ * If `true` is passed, it will show the progress bar even if its hidden.
+ *
+ * NProgress.done(true);
+ */
+
+ NProgress.done = function(force) {
+ if (!force && !NProgress.status) return this;
+
+ return NProgress.inc(0.3 + 0.5 * Math.random()).set(1);
+ };
+
+ /**
+ * Increments by a random amount.
+ */
+
+ NProgress.inc = function(amount) {
+ var n = NProgress.status;
+
+ if (!n) {
+ return NProgress.start();
+ } else if(n > 1) {
+
+ } else {
+ if (typeof amount !== 'number') {
+ if (n >= 0 && n < 0.2) { amount = 0.1; }
+ else if (n >= 0.2 && n < 0.5) { amount = 0.04; }
+ else if (n >= 0.5 && n < 0.8) { amount = 0.02; }
+ else if (n >= 0.8 && n < 0.99) { amount = 0.005; }
+ else { amount = 0; }
+ }
+
+ n = clamp(n + amount, 0, 0.994);
+ return NProgress.set(n);
+ }
+ };
+
+ NProgress.trickle = function() {
+ return NProgress.inc();
+ };
+
+ /**
+ * Waits for all supplied jQuery promises and
+ * increases the progress as the promises resolve.
+ *
+ * @param $promise jQUery Promise
+ */
+ (function() {
+ var initial = 0, current = 0;
+
+ NProgress.promise = function($promise) {
+ if (!$promise || $promise.state() === "resolved") {
+ return this;
+ }
+
+ if (current === 0) {
+ NProgress.start();
+ }
+
+ initial++;
+ current++;
+
+ $promise.always(function() {
+ current--;
+ if (current === 0) {
+ initial = 0;
+ NProgress.done();
+ } else {
+ NProgress.set((initial - current) / initial);
+ }
+ });
+
+ return this;
+ };
+
+ })();
+
+ /**
+ * (Internal) renders the progress bar markup based on the `template`
+ * setting.
+ */
+
+ NProgress.render = function(fromStart) {
+ if (NProgress.isRendered()) return document.getElementById('nprogress');
+
+ addClass(document.documentElement, 'nprogress-busy');
+
+ var progress = document.createElement('div');
+ progress.id = 'nprogress';
+ progress.innerHTML = Settings.template;
+
+ var bar = progress.querySelector(Settings.barSelector),
+ perc = fromStart ? '-100' : toBarPerc(NProgress.status || 0),
+ parent = document.querySelector(Settings.parent),
+ spinner;
+
+ css(bar, {
+ transition: 'all 0 linear',
+ transform: 'translate3d(' + perc + '%,0,0)'
+ });
+
+ if (!Settings.showSpinner) {
+ spinner = progress.querySelector(Settings.spinnerSelector);
+ spinner && removeElement(spinner);
+ }
+
+ if (parent != document.body) {
+ addClass(parent, 'nprogress-custom-parent');
+ }
+
+ parent.appendChild(progress);
+ return progress;
+ };
+
+ /**
+ * Removes the element. Opposite of render().
+ */
+
+ NProgress.remove = function() {
+ removeClass(document.documentElement, 'nprogress-busy');
+ removeClass(document.querySelector(Settings.parent), 'nprogress-custom-parent');
+ var progress = document.getElementById('nprogress');
+ progress && removeElement(progress);
+ };
+
+ /**
+ * Checks if the progress bar is rendered.
+ */
+
+ NProgress.isRendered = function() {
+ return !!document.getElementById('nprogress');
+ };
+
+ /**
+ * Determine which positioning CSS rule to use.
+ */
+
+ NProgress.getPositioningCSS = function() {
+ // Sniff on document.body.style
+ var bodyStyle = document.body.style;
+
+ // Sniff prefixes
+ var vendorPrefix = ('WebkitTransform' in bodyStyle) ? 'Webkit' :
+ ('MozTransform' in bodyStyle) ? 'Moz' :
+ ('msTransform' in bodyStyle) ? 'ms' :
+ ('OTransform' in bodyStyle) ? 'O' : '';
+
+ if (vendorPrefix + 'Perspective' in bodyStyle) {
+ // Modern browsers with 3D support, e.g. Webkit, IE10
+ return 'translate3d';
+ } else if (vendorPrefix + 'Transform' in bodyStyle) {
+ // Browsers without 3D support, e.g. IE9
+ return 'translate';
+ } else {
+ // Browsers without translate() support, e.g. IE7-8
+ return 'margin';
+ }
+ };
+
+ /**
+ * Helpers
+ */
+
+ function clamp(n, min, max) {
+ if (n < min) return min;
+ if (n > max) return max;
+ return n;
+ }
+
+ /**
+ * (Internal) converts a percentage (`0..1`) to a bar translateX
+ * percentage (`-100%..0%`).
+ */
+
+ function toBarPerc(n) {
+ return (-1 + n) * 100;
+ }
+
+
+ /**
+ * (Internal) returns the correct CSS for changing the bar's
+ * position given an n percentage, and speed and ease from Settings
+ */
+
+ function barPositionCSS(n, speed, ease) {
+ var barCSS;
+
+ if (Settings.positionUsing === 'translate3d') {
+ barCSS = { transform: 'translate3d('+toBarPerc(n)+'%,0,0)' };
+ } else if (Settings.positionUsing === 'translate') {
+ barCSS = { transform: 'translate('+toBarPerc(n)+'%,0)' };
+ } else {
+ barCSS = { 'margin-left': toBarPerc(n)+'%' };
+ }
+
+ barCSS.transition = 'all '+speed+'ms '+ease;
+
+ return barCSS;
+ }
+
+ /**
+ * (Internal) Queues a function to be executed.
+ */
+
+ var queue = (function() {
+ var pending = [];
+
+ function next() {
+ var fn = pending.shift();
+ if (fn) {
+ fn(next);
+ }
+ }
+
+ return function(fn) {
+ pending.push(fn);
+ if (pending.length == 1) next();
+ };
+ })();
+
+ /**
+ * (Internal) Applies css properties to an element, similar to the jQuery
+ * css method.
+ *
+ * While this helper does assist with vendor prefixed property names, it
+ * does not perform any manipulation of values prior to setting styles.
+ */
+
+ var css = (function() {
+ var cssPrefixes = [ 'Webkit', 'O', 'Moz', 'ms' ],
+ cssProps = {};
+
+ function camelCase(string) {
+ return string.replace(/^-ms-/, 'ms-').replace(/-([\da-z])/gi, function(match, letter) {
+ return letter.toUpperCase();
+ });
+ }
+
+ function getVendorProp(name) {
+ var style = document.body.style;
+ if (name in style) return name;
+
+ var i = cssPrefixes.length,
+ capName = name.charAt(0).toUpperCase() + name.slice(1),
+ vendorName;
+ while (i--) {
+ vendorName = cssPrefixes[i] + capName;
+ if (vendorName in style) return vendorName;
+ }
+
+ return name;
+ }
+
+ function getStyleProp(name) {
+ name = camelCase(name);
+ return cssProps[name] || (cssProps[name] = getVendorProp(name));
+ }
+
+ function applyCss(element, prop, value) {
+ prop = getStyleProp(prop);
+ element.style[prop] = value;
+ }
+
+ return function(element, properties) {
+ var args = arguments,
+ prop,
+ value;
+
+ if (args.length == 2) {
+ for (prop in properties) {
+ value = properties[prop];
+ if (value !== undefined && properties.hasOwnProperty(prop)) applyCss(element, prop, value);
+ }
+ } else {
+ applyCss(element, args[1], args[2]);
+ }
+ }
+ })();
+
+ /**
+ * (Internal) Determines if an element or space separated list of class names contains a class name.
+ */
+
+ function hasClass(element, name) {
+ var list = typeof element == 'string' ? element : classList(element);
+ return list.indexOf(' ' + name + ' ') >= 0;
+ }
+
+ /**
+ * (Internal) Adds a class to an element.
+ */
+
+ function addClass(element, name) {
+ var oldList = classList(element),
+ newList = oldList + name;
+
+ if (hasClass(oldList, name)) return;
+
+ // Trim the opening space.
+ element.className = newList.substring(1);
+ }
+
+ /**
+ * (Internal) Removes a class from an element.
+ */
+
+ function removeClass(element, name) {
+ var oldList = classList(element),
+ newList;
+
+ if (!hasClass(element, name)) return;
+
+ // Replace the class name.
+ newList = oldList.replace(' ' + name + ' ', ' ');
+
+ // Trim the opening and closing spaces.
+ element.className = newList.substring(1, newList.length - 1);
+ }
+
+ /**
+ * (Internal) Gets a space separated list of the class names on the element.
+ * The list is wrapped with a single space on each end to facilitate finding
+ * matches within the list.
+ */
+
+ function classList(element) {
+ return (' ' + (element && element.className || '') + ' ').replace(/\s+/gi, ' ');
+ }
+
+ /**
+ * (Internal) Removes an element from the DOM.
+ */
+
+ function removeElement(element) {
+ element && element.parentNode && element.parentNode.removeChild(element);
+ }
+
+ return NProgress;
+});
diff --git a/doc/第7周代码注释/blog/static/mathjax/js/mathjax-config.js b/doc/第7周代码注释/blog/static/mathjax/js/mathjax-config.js
new file mode 100644
index 00000000..158ba65a
--- /dev/null
+++ b/doc/第7周代码注释/blog/static/mathjax/js/mathjax-config.js
@@ -0,0 +1,21 @@
+$(function () {
+ MathJax.Hub.Config({
+ showProcessingMessages: false, //关闭js加载过程信息
+ messageStyle: "none", //不显示信息
+ extensions: ["tex2jax.js"], jax: ["input/TeX", "output/HTML-CSS"], displayAlign: "left", tex2jax: {
+ inlineMath: [["$", "$"]], //行内公式选择$
+ displayMath: [["$$", "$$"]], //段内公式选择$$
+ skipTags: ['script', 'noscript', 'style', 'textarea', 'pre', 'code', 'a'], //避开某些标签
+ }, "HTML-CSS": {
+ availableFonts: ["STIX", "TeX"], //可选字体
+ showMathMenu: false //关闭右击菜单显示
+ }
+ });
+ // 识别范围 => 文章内容、评论内容标签
+ const contentId = document.getElementById("content");
+ const commentId = document.getElementById("comments");
+ MathJax.Hub.Queue(["Typeset", MathJax.Hub, contentId, commentId]);
+})
+
+
+
diff --git a/doc/第7周代码注释/blog/static/pygments/default.css b/doc/第7周代码注释/blog/static/pygments/default.css
new file mode 100644
index 00000000..73e6e493
--- /dev/null
+++ b/doc/第7周代码注释/blog/static/pygments/default.css
@@ -0,0 +1,293 @@
+.codehilite .hll {
+ background-color: #ffffcc
+}
+
+.codehilite {
+ background: #ffffff;
+}
+
+.codehilite .c {
+ color: #177500
+}
+
+/* Comment */
+.codehilite .err {
+ color: #000000
+}
+
+/* Error */
+.codehilite .k {
+ color: #A90D91
+}
+
+/* Keyword */
+.codehilite .l {
+ color: #1C01CE
+}
+
+/* Literal */
+.codehilite .n {
+ color: #000000
+}
+
+/* Name */
+.codehilite .o {
+ color: #000000
+}
+
+/* Operator */
+.codehilite .ch {
+ color: #177500
+}
+
+/* Comment.Hashbang */
+.codehilite .cm {
+ color: #177500
+}
+
+/* Comment.Multiline */
+.codehilite .cp {
+ color: #633820
+}
+
+/* Comment.Preproc */
+.codehilite .cpf {
+ color: #177500
+}
+
+/* Comment.PreprocFile */
+.codehilite .c1 {
+ color: #177500
+}
+
+/* Comment.Single */
+.codehilite .cs {
+ color: #177500
+}
+
+/* Comment.Special */
+.codehilite .kc {
+ color: #A90D91
+}
+
+/* Keyword.Constant */
+.codehilite .kd {
+ color: #A90D91
+}
+
+/* Keyword.Declaration */
+.codehilite .kn {
+ color: #A90D91
+}
+
+/* Keyword.Namespace */
+.codehilite .kp {
+ color: #A90D91
+}
+
+/* Keyword.Pseudo */
+.codehilite .kr {
+ color: #A90D91
+}
+
+/* Keyword.Reserved */
+.codehilite .kt {
+ color: #A90D91
+}
+
+/* Keyword.Type */
+.codehilite .ld {
+ color: #1C01CE
+}
+
+/* Literal.Date */
+.codehilite .m {
+ color: #1C01CE
+}
+
+/* Literal.Number */
+.codehilite .s {
+ color: #C41A16
+}
+
+/* Literal.String */
+.codehilite .na {
+ color: #836C28
+}
+
+/* Name.Attribute */
+.codehilite .nb {
+ color: #A90D91
+}
+
+/* Name.Builtin */
+.codehilite .nc {
+ color: #3F6E75
+}
+
+/* Name.Class */
+.codehilite .no {
+ color: #000000
+}
+
+/* Name.Constant */
+.codehilite .nd {
+ color: #000000
+}
+
+/* Name.Decorator */
+.codehilite .ni {
+ color: #000000
+}
+
+/* Name.Entity */
+.codehilite .ne {
+ color: #000000
+}
+
+/* Name.Exception */
+.codehilite .nf {
+ color: #000000
+}
+
+/* Name.Function */
+.codehilite .nl {
+ color: #000000
+}
+
+/* Name.Label */
+.codehilite .nn {
+ color: #000000
+}
+
+/* Name.Namespace */
+.codehilite .nx {
+ color: #000000
+}
+
+/* Name.Other */
+.codehilite .py {
+ color: #000000
+}
+
+/* Name.Property */
+.codehilite .nt {
+ color: #000000
+}
+
+/* Name.Tag */
+.codehilite .nv {
+ color: #000000
+}
+
+/* Name.Variable */
+.codehilite .ow {
+ color: #000000
+}
+
+/* Operator.Word */
+.codehilite .mb {
+ color: #1C01CE
+}
+
+/* Literal.Number.Bin */
+.codehilite .mf {
+ color: #1C01CE
+}
+
+/* Literal.Number.Float */
+.codehilite .mh {
+ color: #1C01CE
+}
+
+/* Literal.Number.Hex */
+.codehilite .mi {
+ color: #1C01CE
+}
+
+/* Literal.Number.Integer */
+.codehilite .mo {
+ color: #1C01CE
+}
+
+/* Literal.Number.Oct */
+.codehilite .sb {
+ color: #C41A16
+}
+
+/* Literal.String.Backtick */
+.codehilite .sc {
+ color: #2300CE
+}
+
+/* Literal.String.Char */
+.codehilite .sd {
+ color: #C41A16
+}
+
+/* Literal.String.Doc */
+.codehilite .s2 {
+ color: #C41A16
+}
+
+/* Literal.String.Double */
+.codehilite .se {
+ color: #C41A16
+}
+
+/* Literal.String.Escape */
+.codehilite .sh {
+ color: #C41A16
+}
+
+/* Literal.String.Heredoc */
+.codehilite .si {
+ color: #C41A16
+}
+
+/* Literal.String.Interpol */
+.codehilite .sx {
+ color: #C41A16
+}
+
+/* Literal.String.Other */
+.codehilite .sr {
+ color: #C41A16
+}
+
+/* Literal.String.Regex */
+.codehilite .s1 {
+ color: #C41A16
+}
+
+/* Literal.String.Single */
+.codehilite .ss {
+ color: #C41A16
+}
+
+/* Literal.String.Symbol */
+.codehilite .bp {
+ color: #5B269A
+}
+
+/* Name.Builtin.Pseudo */
+.codehilite .vc {
+ color: #000000
+}
+
+/* Name.Variable.Class */
+.codehilite .vg {
+ color: #000000
+}
+
+/* Name.Variable.Global */
+.codehilite .vi {
+ color: #000000
+}
+
+/* Name.Variable.Instance */
+.codehilite .il {
+ color: #1C01CE
+}
+
+/* Literal.Number.Integer.Long */
\ No newline at end of file
diff --git a/doc/第7周代码注释/blog/templatetags/__init__.py b/doc/第7周代码注释/blog/templatetags/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/doc/第7周代码注释/blog/templatetags/__pycache__/__init__.cpython-311.pyc b/doc/第7周代码注释/blog/templatetags/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 00000000..be542fab
Binary files /dev/null and b/doc/第7周代码注释/blog/templatetags/__pycache__/__init__.cpython-311.pyc differ
diff --git a/doc/第7周代码注释/blog/templatetags/__pycache__/blog_tags.cpython-311.pyc b/doc/第7周代码注释/blog/templatetags/__pycache__/blog_tags.cpython-311.pyc
new file mode 100644
index 00000000..0ef16004
Binary files /dev/null and b/doc/第7周代码注释/blog/templatetags/__pycache__/blog_tags.cpython-311.pyc differ
diff --git a/doc/第7周代码注释/blog/templatetags/blog_tags.py b/doc/第7周代码注释/blog/templatetags/blog_tags.py
new file mode 100644
index 00000000..110b22b9
--- /dev/null
+++ b/doc/第7周代码注释/blog/templatetags/blog_tags.py
@@ -0,0 +1,338 @@
+import hashlib
+import logging
+import random
+import urllib
+
+from django import template
+from django.conf import settings
+from django.db.models import Q
+from django.shortcuts import get_object_or_404
+from django.template.defaultfilters import stringfilter
+from django.templatetags.static import static
+from django.urls import reverse
+from django.utils.safestring import mark_safe
+
+from blog.models import Article, Category, Tag, Links, SideBar, LinkShowType
+from comments.models import Comment
+from djangoblog.utils import CommonMarkdown, sanitize_html
+from djangoblog.utils import cache
+from djangoblog.utils import get_current_site
+from oauth.models import OAuthUser
+
+logger = logging.getLogger(__name__)
+
+register = template.Library()
+
+
+@register.simple_tag
+def timeformat(data):
+ try:
+ return data.strftime(settings.TIME_FORMAT)
+ except Exception as e:
+ logger.error(e)
+ return ""
+
+
+@register.simple_tag
+def datetimeformat(data):
+ try:
+ return data.strftime(settings.DATE_TIME_FORMAT)
+ except Exception as e:
+ logger.error(e)
+ return ""
+
+
+@register.filter()
+@stringfilter
+def custom_markdown(content):
+ return mark_safe(CommonMarkdown.get_markdown(content))
+
+
+@register.simple_tag
+def get_markdown_toc(content):
+ from djangoblog.utils import CommonMarkdown
+ body, toc = CommonMarkdown.get_markdown_with_toc(content)
+ return mark_safe(toc)
+
+
+@register.filter()
+@stringfilter
+def comment_markdown(content):
+ content = CommonMarkdown.get_markdown(content)
+ return mark_safe(sanitize_html(content))
+
+
+@register.filter(is_safe=True)
+@stringfilter
+def truncatechars_content(content):
+ """
+ 获得文章内容的摘要
+ :param content:
+ :return:
+ """
+ from django.template.defaultfilters import truncatechars_html
+ from djangoblog.utils import get_blog_setting
+ blogsetting = get_blog_setting()
+ return truncatechars_html(content, blogsetting.article_sub_length)
+
+
+@register.filter(is_safe=True)
+@stringfilter
+def truncate(content):
+ from django.utils.html import strip_tags
+
+ return strip_tags(content)[:150]
+
+
+@register.inclusion_tag('blog/tags/breadcrumb.html')
+def load_breadcrumb(article):
+ """
+ 获得文章面包屑
+ :param article:
+ :return:
+ """
+ names = article.get_category_tree()
+ from djangoblog.utils import get_blog_setting
+ blogsetting = get_blog_setting()
+ site = get_current_site().domain
+ names.append((blogsetting.site_name, '/'))
+ names = names[::-1]
+
+ return {
+ 'names': names,
+ 'title': article.title,
+ 'count': len(names) + 1
+ }
+
+
+@register.inclusion_tag('blog/tags/article_tag_list.html')
+def load_articletags(article):
+ """
+ 文章标签
+ :param article:
+ :return:
+ """
+ tags = article.tags.all()
+ tags_list = []
+ for tag in tags:
+ url = tag.get_absolute_url()
+ count = tag.get_article_count()
+ tags_list.append((
+ url, count, tag, random.choice(settings.BOOTSTRAP_COLOR_TYPES)
+ ))
+ return {
+ 'article_tags_list': tags_list
+ }
+
+
+@register.inclusion_tag('blog/tags/sidebar.html')
+def load_sidebar(user, linktype):
+ """
+ 加载侧边栏
+ :return:
+ """
+ value = cache.get("sidebar" + linktype)
+ if value:
+ value['user'] = user
+ return value
+ else:
+ logger.info('load sidebar')
+ from djangoblog.utils import get_blog_setting
+ blogsetting = get_blog_setting()
+ recent_articles = Article.objects.filter(
+ status='p')[:blogsetting.sidebar_article_count]
+ sidebar_categorys = Category.objects.all()
+ extra_sidebars = SideBar.objects.filter(
+ is_enable=True).order_by('sequence')
+ most_read_articles = Article.objects.filter(status='p').order_by(
+ '-views')[:blogsetting.sidebar_article_count]
+ dates = Article.objects.datetimes('creation_time', 'month', order='DESC')
+ links = Links.objects.filter(is_enable=True).filter(
+ Q(show_type=str(linktype)) | Q(show_type=LinkShowType.A))
+ commment_list = Comment.objects.filter(is_enable=True).order_by(
+ '-id')[:blogsetting.sidebar_comment_count]
+ # 标签云 计算字体大小
+ # 根据总数计算出平均值 大小为 (数目/平均值)*步长
+ increment = 5
+ tags = Tag.objects.all()
+ sidebar_tags = None
+ if tags and len(tags) > 0:
+ s = [t for t in [(t, t.get_article_count()) for t in tags] if t[1]]
+ count = sum([t[1] for t in s])
+ dd = 1 if (count == 0 or not len(tags)) else count / len(tags)
+ import random
+ sidebar_tags = list(
+ map(lambda x: (x[0], x[1], (x[1] / dd) * increment + 10), s))
+ random.shuffle(sidebar_tags)
+
+ value = {
+ 'recent_articles': recent_articles,
+ 'sidebar_categorys': sidebar_categorys,
+ 'most_read_articles': most_read_articles,
+ 'article_dates': dates,
+ 'sidebar_comments': commment_list,
+ 'sidabar_links': links,
+ 'show_google_adsense': blogsetting.show_google_adsense,
+ 'google_adsense_codes': blogsetting.google_adsense_codes,
+ 'open_site_comment': blogsetting.open_site_comment,
+ 'show_gongan_code': blogsetting.show_gongan_code,
+ 'sidebar_tags': sidebar_tags,
+ 'extra_sidebars': extra_sidebars
+ }
+ cache.set("sidebar" + linktype, value, 60 * 60 * 60 * 3)
+ logger.info('set sidebar cache.key:{key}'.format(key="sidebar" + linktype))
+ value['user'] = user
+ return value
+
+
+@register.inclusion_tag('blog/tags/article_meta_info.html')
+def load_article_metas(article, user):
+ """
+ 获得文章meta信息
+ :param article:
+ :return:
+ """
+ return {
+ 'article': article,
+ 'user': user
+ }
+
+
+@register.inclusion_tag('blog/tags/article_pagination.html')
+def load_pagination_info(page_obj, page_type, tag_name):
+ previous_url = ''
+ next_url = ''
+ if page_type == '':
+ if page_obj.has_next():
+ next_number = page_obj.next_page_number()
+ next_url = reverse('blog:index_page', kwargs={'page': next_number})
+ if page_obj.has_previous():
+ previous_number = page_obj.previous_page_number()
+ previous_url = reverse(
+ 'blog:index_page', kwargs={
+ 'page': previous_number})
+ if page_type == '分类标签归档':
+ tag = get_object_or_404(Tag, name=tag_name)
+ if page_obj.has_next():
+ next_number = page_obj.next_page_number()
+ next_url = reverse(
+ 'blog:tag_detail_page',
+ kwargs={
+ 'page': next_number,
+ 'tag_name': tag.slug})
+ if page_obj.has_previous():
+ previous_number = page_obj.previous_page_number()
+ previous_url = reverse(
+ 'blog:tag_detail_page',
+ kwargs={
+ 'page': previous_number,
+ 'tag_name': tag.slug})
+ if page_type == '作者文章归档':
+ if page_obj.has_next():
+ next_number = page_obj.next_page_number()
+ next_url = reverse(
+ 'blog:author_detail_page',
+ kwargs={
+ 'page': next_number,
+ 'author_name': tag_name})
+ if page_obj.has_previous():
+ previous_number = page_obj.previous_page_number()
+ previous_url = reverse(
+ 'blog:author_detail_page',
+ kwargs={
+ 'page': previous_number,
+ 'author_name': tag_name})
+
+ if page_type == '分类目录归档':
+ category = get_object_or_404(Category, name=tag_name)
+ if page_obj.has_next():
+ next_number = page_obj.next_page_number()
+ next_url = reverse(
+ 'blog:category_detail_page',
+ kwargs={
+ 'page': next_number,
+ 'category_name': category.slug})
+ if page_obj.has_previous():
+ previous_number = page_obj.previous_page_number()
+ previous_url = reverse(
+ 'blog:category_detail_page',
+ kwargs={
+ 'page': previous_number,
+ 'category_name': category.slug})
+
+ return {
+ 'previous_url': previous_url,
+ 'next_url': next_url,
+ 'page_obj': page_obj
+ }
+
+
+@register.inclusion_tag('blog/tags/article_info.html')
+def load_article_detail(article, isindex, user):
+ """
+ 加载文章详情
+ :param article:
+ :param isindex:是否列表页,若是列表页只显示摘要
+ :return:
+ """
+ from djangoblog.utils import get_blog_setting
+ blogsetting = get_blog_setting()
+
+ return {
+ 'article': article,
+ 'isindex': isindex,
+ 'user': user,
+ 'open_site_comment': blogsetting.open_site_comment,
+ }
+
+
+# return only the URL of the gravatar
+# TEMPLATE USE: {{ email|gravatar_url:150 }}
+@register.filter
+def gravatar_url(email, size=40):
+ """获得gravatar头像"""
+ cachekey = 'gravatat/' + email
+ url = cache.get(cachekey)
+ if url:
+ return url
+ else:
+ usermodels = OAuthUser.objects.filter(email=email)
+ if usermodels:
+ o = list(filter(lambda x: x.picture is not None, usermodels))
+ if o:
+ return o[0].picture
+ email = email.encode('utf-8')
+
+ default = static('blog/img/avatar.png')
+
+ url = "https://www.gravatar.com/avatar/%s?%s" % (hashlib.md5(
+ email.lower()).hexdigest(), urllib.parse.urlencode({'d': default, 's': str(size)}))
+ cache.set(cachekey, url, 60 * 60 * 10)
+ logger.info('set gravatar cache.key:{key}'.format(key=cachekey))
+ return url
+
+
+@register.filter
+def gravatar(email, size=40):
+ """获得gravatar头像"""
+ url = gravatar_url(email, size)
+ return mark_safe(
+ ' ' %
+ (url, size, size))
+
+
+@register.simple_tag
+def query(qs, **kwargs):
+ """ template tag which allows queryset filtering. Usage:
+ {% query books author=author as mybooks %}
+ {% for book in mybooks %}
+ ...
+ {% endfor %}
+ """
+ return qs.filter(**kwargs)
+
+
+@register.filter
+def addstr(arg1, arg2):
+ """concatenate arg1 & arg2"""
+ return str(arg1) + str(arg2)
diff --git a/doc/第7周代码注释/blog/tests.py b/doc/第7周代码注释/blog/tests.py
new file mode 100644
index 00000000..28446c09
--- /dev/null
+++ b/doc/第7周代码注释/blog/tests.py
@@ -0,0 +1,346 @@
+# 导入Python内置os模块,用于文件路径操作(如图片文件的保存与删除)
+import os
+
+# 导入Django项目配置模块,用于获取项目设置(如分页数量、文件上传路径)
+from django.conf import settings
+# 导入Django文件上传相关类,用于模拟文件上传请求(如图片上传测试)
+from django.core.files.uploadedfile import SimpleUploadedFile
+# 导入Django管理命令调用函数,用于在测试中执行自定义管理命令(如重建搜索索引)
+from django.core.management import call_command
+# 导入Django分页类,用于测试分页功能
+from django.core.paginator import Paginator
+# 导入Django静态文件模板标签,用于生成静态文件URL(如测试用户头像)
+from django.templatetags.static import static
+# 导入Django测试核心模块:
+# 1. Client:模拟HTTP客户端,用于发送GET/POST请求并获取响应
+# 2. RequestFactory:生成请求对象,用于测试视图函数/模板标签
+# 3. TestCase:Django测试基类,提供测试环境初始化、断言方法等
+from django.test import Client, RequestFactory, TestCase
+# 导入Django URL反向解析模块,用于生成测试用的URL(避免硬编码)
+from django.urls import reverse
+# 导入Django时区工具,用于处理时间字段(如模型创建时间)
+from django.utils import timezone
+
+# 从accounts应用导入用户模型BlogUser(自定义用户模型,替代Django默认用户模型)
+from accounts.models import BlogUser
+# 从当前应用(blog)导入搜索表单BlogSearchForm,用于测试表单功能
+from blog.forms import BlogSearchForm
+# 从当前应用导入核心模型:文章、分类、标签、侧边栏、友情链接
+from blog.models import Article, Category, Tag, SideBar, Links
+# 从当前应用导入自定义模板标签函数,用于测试模板标签的逻辑正确性
+from blog.templatetags.blog_tags import load_pagination_info, load_articletags
+# 从自定义工具模块导入工具函数:
+# 1. get_current_site:获取当前站点信息(用于生成完整URL)
+# 2. get_sha256:生成SHA256加密字符串(用于测试文件上传签名验证)
+from djangoblog.utils import get_current_site, get_sha256
+# 从oauth应用导入第三方登录相关模型(OAuth用户、OAuth配置),用于测试第三方登录数据
+from oauth.models import OAuthUser, OAuthConfig
+
+
+# 测试类创建标识注释(Django测试框架约定:测试类需继承TestCase)
+# Create your tests here.
+
+# 定义文章相关测试类ArticleTest,继承自Django的TestCase(测试基类)
+# 作用:集中测试博客核心功能,包括模型操作、URL访问、搜索、分页、文件上传等
+class ArticleTest(TestCase):
+ # 测试初始化方法:在每个测试方法执行前自动调用,用于准备测试环境
+ def setUp(self):
+ # 初始化HTTP客户端(模拟用户发送请求)
+ self.client = Client()
+ # 初始化请求工厂(用于生成原始请求对象,测试视图/模板标签时使用)
+ self.factory = RequestFactory()
+
+ # 核心测试方法:测试文章相关完整流程(模型创建、关联、URL访问、搜索等)
+ def test_validate_article(self):
+ # 获取当前站点域名(用于验证完整URL生成)
+ site = get_current_site().domain
+ # 创建/获取测试用户:邮箱、用户名固定,若已存在则直接获取(get_or_create返回元组,取第0个元素)
+ user = BlogUser.objects.get_or_create(
+ email="liangliangyy@gmail.com",
+ username="liangliangyy")[0]
+ # 设置用户密码(set_password会自动加密,避免明文存储)
+ user.set_password("liangliangyy")
+ # 设置用户为 staff(后台管理权限)和 superuser(超级管理员权限)
+ user.is_staff = True
+ user.is_superuser = True
+ # 保存用户信息到数据库
+ user.save()
+
+ # 模拟访问用户个人主页,验证响应状态码为200(正常访问)
+ response = self.client.get(user.get_absolute_url())
+ self.assertEqual(response.status_code, 200)
+
+ # 模拟访问后台邮件发送日志页面(测试后台URL可达性)
+ response = self.client.get('/admin/servermanager/emailsendlog/')
+ # 模拟访问后台操作日志列表页面(测试后台URL可达性)
+ response = self.client.get('admin/admin/logentry/')
+
+ # 创建测试侧边栏数据
+ s = SideBar()
+ s.sequence = 1 # 排序序号(控制显示顺序)
+ s.name = 'test' # 侧边栏标题
+ s.content = 'test content' # 侧边栏内容(HTML文本)
+ s.is_enable = True # 启用侧边栏
+ s.save() # 保存到数据库
+
+ # 创建测试分类数据
+ category = Category()
+ category.name = "category" # 分类名称
+ category.creation_time = timezone.now() # 创建时间
+ category.last_mod_time = timezone.now() # 最后修改时间
+ category.save() # 保存到数据库
+
+ # 创建测试标签数据
+ tag = Tag()
+ tag.name = "nicetag" # 标签名称
+ tag.save() # 保存到数据库
+
+ # 创建测试文章数据
+ article = Article()
+ article.title = "nicetitle" # 文章标题
+ article.body = "nicecontent" # 文章内容
+ article.author = user # 关联作者(测试用户)
+ article.category = category # 关联分类
+ article.type = 'a' # 文章类型:普通文章('a'=Article)
+ article.status = 'p' # 文章状态:已发布('p'=Published)
+ article.save() # 保存到数据库
+
+ # 验证文章初始标签数量为0(未添加标签)
+ self.assertEqual(0, article.tags.count())
+ # 为文章添加标签(多对多关联)
+ article.tags.add(tag)
+ article.save() # 再次保存,更新关联关系
+ # 验证文章标签数量为1(添加成功)
+ self.assertEqual(1, article.tags.count())
+
+ # 批量创建20篇测试文章(用于测试分页功能)
+ for i in range(20):
+ article = Article()
+ article.title = "nicetitle" + str(i) # 标题带序号,避免重复
+ article.body = "nicetitle" + str(i) # 内容与标题一致
+ article.author = user # 关联同一测试用户
+ article.category = category # 关联同一分类
+ article.type = 'a' # 普通文章类型
+ article.status = 'p' # 已发布状态
+ article.save() # 保存文章
+ article.tags.add(tag) # 关联同一标签
+ article.save() # 更新关联关系
+
+ # 导入Elasticsearch启用标识(判断是否启用搜索引擎)
+ from blog.documents import ELASTICSEARCH_ENABLED
+ # 若启用Elasticsearch,测试搜索功能
+ if ELASTICSEARCH_ENABLED:
+ call_command("build_index") # 执行自定义管理命令,重建搜索索引
+ # 模拟发送搜索请求,关键词为'nicetitle'
+ response = self.client.get('/search', {'q': 'nicetitle'})
+ self.assertEqual(response.status_code, 200) # 验证搜索页面正常响应
+
+ # 模拟访问最后一篇测试文章的详情页,验证响应状态码为200
+ response = self.client.get(article.get_absolute_url())
+ self.assertEqual(response.status_code, 200)
+
+ # 导入爬虫通知工具类,测试文章发布后通知搜索引擎(如百度)
+ from djangoblog.spider_notify import SpiderNotify
+ SpiderNotify.notify(article.get_absolute_url()) # 通知搜索引擎该文章URL
+
+ # 模拟访问标签详情页,验证响应状态码为200
+ response = self.client.get(tag.get_absolute_url())
+ self.assertEqual(response.status_code, 200)
+
+ # 模拟访问分类详情页,验证响应状态码为200
+ response = self.client.get(category.get_absolute_url())
+ self.assertEqual(response.status_code, 200)
+
+ # 模拟搜索不存在的关键词'django',验证搜索页面正常响应(状态码200)
+ response = self.client.get('/search', {'q': 'django'})
+ self.assertEqual(response.status_code, 200)
+
+ # 测试自定义模板标签load_articletags,验证返回结果不为空
+ s = load_articletags(article)
+ self.assertIsNotNone(s)
+
+ # 模拟用户登录(使用测试用户的用户名和密码)
+ self.client.login(username='liangliangyy', password='liangliangyy')
+
+ # 模拟访问文章归档页面,验证响应状态码为200
+ response = self.client.get(reverse('blog:archives'))
+ self.assertEqual(response.status_code, 200)
+
+ # 测试所有文章的分页功能:使用项目配置的分页数量(settings.PAGINATE_BY)
+ p = Paginator(Article.objects.all(), settings.PAGINATE_BY)
+ self.check_pagination(p, '', '') # 调用自定义分页测试方法
+
+ # 测试标签归档的分页功能:筛选指定标签的文章
+ p = Paginator(Article.objects.filter(tags=tag), settings.PAGINATE_BY)
+ self.check_pagination(p, '分类标签归档', tag.slug) # 传递分页类型和标签slug
+
+ # 测试作者归档的分页功能:筛选指定作者的文章
+ p = Paginator(
+ Article.objects.filter(
+ author__username='liangliangyy'), settings.PAGINATE_BY)
+ self.check_pagination(p, '作者文章归档', 'liangliangyy') # 传递分页类型和作者名
+
+ # 测试分类归档的分页功能:筛选指定分类的文章
+ p = Paginator(Article.objects.filter(category=category), settings.PAGINATE_BY)
+ self.check_pagination(p, '分类目录归档', category.slug) # 传递分页类型和分类slug
+
+ # 测试搜索表单:初始化表单并调用search方法(验证表单逻辑无报错)
+ f = BlogSearchForm()
+ f.search()
+
+ # 测试百度爬虫通知功能:通知单篇文章的完整URL
+ from djangoblog.spider_notify import SpiderNotify
+ SpiderNotify.baidu_notify([article.get_full_url()])
+
+ # 测试头像相关模板标签:验证gravatar_url和gravatar函数正常返回结果
+ from blog.templatetags.blog_tags import gravatar_url, gravatar
+ u = gravatar_url('liangliangyy@gmail.com') # 生成Gravatar头像URL
+ u = gravatar('liangliangyy@gmail.com') # 生成Gravatar头像HTML标签
+
+ # 创建测试友情链接数据
+ link = Links(
+ sequence=1, # 排序序号
+ name="lylinux", # 链接名称
+ link='https://wwww.lylinux.net') # 链接URL
+ link.save() # 保存到数据库
+
+ # 模拟访问友情链接页面,验证响应状态码为200
+ response = self.client.get('/links.html')
+ self.assertEqual(response.status_code, 200)
+
+ # 模拟访问RSS订阅 feed 页面,验证响应状态码为200
+ response = self.client.get('/feed/')
+ self.assertEqual(response.status_code, 200)
+
+ # 模拟访问站点地图页面,验证响应状态码为200
+ response = self.client.get('/sitemap.xml')
+ self.assertEqual(response.status_code, 200)
+
+ # 模拟访问后台文章删除页面(测试后台操作可达性)
+ self.client.get("/admin/blog/article/1/delete/")
+ # 模拟访问后台邮件发送日志页面(重复访问,验证稳定性)
+ self.client.get('/admin/servermanager/emailsendlog/')
+ # 模拟访问后台操作日志列表页面(重复访问,验证稳定性)
+ self.client.get('/admin/admin/logentry/')
+ # 模拟访问后台操作日志编辑页面(测试后台详情页可达性)
+ self.client.get('/admin/admin/logentry/1/change/')
+
+ # 自定义分页测试方法:验证分页逻辑和分页URL的可达性
+ def check_pagination(self, p, type, value):
+ # 遍历所有分页页面(从第1页到最后一页)
+ for page in range(1, p.num_pages + 1):
+ # 调用模板标签load_pagination_info,获取分页信息(上一页URL、下一页URL等)
+ s = load_pagination_info(p.page(page), type, value)
+ self.assertIsNotNone(s) # 验证分页信息不为空
+ # 若存在上一页URL,模拟访问并验证响应状态码为200
+ if s['previous_url']:
+ response = self.client.get(s['previous_url'])
+ self.assertEqual(response.status_code, 200)
+ # 若存在下一页URL,模拟访问并验证响应状态码为200
+ if s['next_url']:
+ response = self.client.get(s['next_url'])
+ self.assertEqual(response.status_code, 200)
+
+ # 测试方法:测试图片上传功能(含未授权上传、授权上传验证)
+ def test_image(self):
+ # 导入requests库(需提前安装),用于下载测试图片
+ import requests
+ # 下载Python官网logo图片,作为测试上传文件
+ rsp = requests.get(
+ 'https://www.python.org/static/img/python-logo@2x.png')
+ # 定义图片保存路径:项目根目录下的'python.png'
+ imagepath = os.path.join(settings.BASE_DIR, 'python.png')
+ # 将下载的图片内容写入本地文件
+ with open(imagepath, 'wb') as file:
+ file.write(rsp.content)
+
+ # 模拟未授权的图片上传请求(未带签名),验证响应状态码为403(禁止访问)
+ rsp = self.client.post('/upload')
+ self.assertEqual(rsp.status_code, 403)
+
+ # 生成上传授权签名:双重SHA256加密项目SECRET_KEY(与后端上传接口的签名验证逻辑一致)
+ sign = get_sha256(get_sha256(settings.SECRET_KEY))
+ # 读取本地测试图片文件,创建SimpleUploadedFile对象(模拟文件上传数据)
+ with open(imagepath, 'rb') as file:
+ imgfile = SimpleUploadedFile(
+ 'python.png', file.read(), content_type='image/jpg') # 文件名、文件内容、MIME类型
+ form_data = {'python.png': imgfile} # 构造表单数据(键为文件名,值为文件对象)
+ # 模拟带签名的图片上传请求,跟随重定向(follow=True)
+ rsp = self.client.post(
+ '/upload?sign=' + sign, form_data, follow=True)
+ self.assertEqual(rsp.status_code, 200) # 验证授权上传成功,响应状态码为200
+
+ # 删除本地测试图片文件(清理测试残留)
+ os.remove(imagepath)
+
+ # 测试工具函数:保存用户头像、发送邮件
+ from djangoblog.utils import save_user_avatar, send_email
+ send_email(['qq@qq.com'], 'testTitle', 'testContent') # 发送测试邮件(收件人、标题、内容)
+ # 从URL保存用户头像(测试图片下载与保存逻辑)
+ save_user_avatar(
+ 'https://www.python.org/static/img/python-logo@2x.png')
+
+ # 测试方法:测试错误页面(404页面)
+ def test_errorpage(self):
+ # 模拟访问不存在的URL('/eee'),验证响应状态码为404(页面未找到)
+ rsp = self.client.get('/eee')
+ self.assertEqual(rsp.status_code, 404)
+
+ # 测试方法:测试自定义管理命令(如重建索引、清理缓存等)
+ def test_commands(self):
+ # 创建/获取测试用户(与test_validate_article方法一致)
+ user = BlogUser.objects.get_or_create(
+ email="liangliangyy@gmail.com",
+ username="liangliangyy")[0]
+ user.set_password("liangliangyy")
+ user.is_staff = True
+ user.is_superuser = True
+ user.save()
+
+ # 创建第三方登录配置(QQ登录)
+ c = OAuthConfig()
+ c.type = 'qq' # 登录类型:QQ
+ c.appkey = 'appkey' # 测试用appkey
+ c.appsecret = 'appsecret' # 测试用appsecret
+ c.save() # 保存到数据库
+
+ # 创建第三方登录用户(关联测试用户)
+ u = OAuthUser()
+ u.type = 'qq' # 登录类型:QQ
+ u.openid = 'openid' # 测试用openid(第三方平台用户唯一标识)
+ u.user = user # 关联本地测试用户
+ u.picture = static("/blog/img/avatar.png") # 静态文件中的默认头像
+ u.metadata = '''
+{
+"figureurl": "https://qzapp.qlogo.cn/qzapp/101513904/C740E30B4113EAA80E0D9918ABC78E82/30"
+}''' # 第三方用户元数据(如QQ头像URL)
+ u.save() # 保存到数据库
+
+ # 创建另一个第三方登录用户(不关联本地用户,测试头像URL直接赋值)
+ u = OAuthUser()
+ u.type = 'qq' # 登录类型:QQ
+ u.openid = 'openid1' # 不同的openid
+ # 直接赋值头像URL(而非静态文件)
+ u.picture = 'https://qzapp.qlogo.cn/qzapp/101513904/C740E30B4113EAA80E0D9918ABC78E82/30'
+ u.metadata = '''
+ {
+ "figureurl": "https://qzapp.qlogo.cn/qzapp/101513904/C740E30B4113EAA80E0D9918ABC78E82/30"
+ }''' # 第三方用户元数据
+ u.save() # 保存到数据库
+
+ # 导入Elasticsearch启用标识
+ from blog.documents import ELASTICSEARCH_ENABLED
+ # 若启用Elasticsearch,执行重建搜索索引命令
+ if ELASTICSEARCH_ENABLED:
+ call_command("build_index")
+
+ # 执行自定义管理命令:通知百度爬虫(参数"all"表示通知所有文章)
+ call_command("ping_baidu", "all")
+ # 执行自定义管理命令:创建测试数据
+ call_command("create_testdata")
+ # 执行自定义管理命令:清理缓存
+ call_command("clear_cache")
+ # 执行自定义管理命令:同步用户头像(从第三方平台同步头像到本地)
+ call_command("sync_user_avatar")
+ # 执行自定义管理命令:构建搜索关键词(优化搜索体验)
+ call_command("build_search_words")
\ No newline at end of file
diff --git a/doc/第7周代码注释/blog/urls.py b/doc/第7周代码注释/blog/urls.py
new file mode 100644
index 00000000..accf21e4
--- /dev/null
+++ b/doc/第7周代码注释/blog/urls.py
@@ -0,0 +1,92 @@
+# 导入Django URL路由核心模块,用于定义路由规则
+from django.urls import path
+# 导入Django缓存装饰器,用于为视图添加页面缓存(提升访问性能)
+from django.views.decorators.cache import cache_page
+
+# 从当前应用(blog)导入视图模块,路由将映射到对应的视图类/函数
+from . import views
+
+# 定义应用命名空间(app_name):用于模板URL反向解析时区分不同应用的路由
+# 示例:模板中使用 {% url 'blog:index' %} 反向生成首页URL
+app_name = "blog"
+
+# 定义URL路由列表:每个path对应一个路由规则,按匹配优先级排序
+urlpatterns = [
+ # 1. 首页路由:匹配根路径(空字符串)
+ path(
+ r'', # 路由路径:根路径(网站首页)
+ views.IndexView.as_view(), # 关联视图:IndexView类视图的as_view()方法(类视图转为可调用视图)
+ name='index'), # 路由名称:用于反向解析,名称为'index'
+
+ # 2. 分页首页路由:匹配带页码的首页(如/page/2/)
+ path(
+ r'page//', # 路由路径:为路径参数,int指定参数类型为整数,page为参数名
+ views.IndexView.as_view(), # 复用首页视图类,视图会通过page参数处理分页逻辑
+ name='index_page'), # 路由名称:用于反向解析分页首页,名称为'index_page'
+
+ # 3. 文章详情页路由(按ID匹配):匹配带发布日期和文章ID的URL(如/article/2025/11/5/10.html)
+ path(
+ r'article////.html', # 路径参数:年、月、日(整数)、文章ID(整数)
+ views.ArticleDetailView.as_view(), # 关联文章详情视图类
+ name='detailbyid'), # 路由名称:用于反向解析文章详情页,名称为'detailbyid'
+
+ # 4. 分类详情页路由:匹配分类slug对应的页面(如/category/python.html)
+ path(
+ r'category/.html', # 路径参数:,slug类型支持字母、数字、连字符、下划线
+ views.CategoryDetailView.as_view(), # 关联分类详情视图类
+ name='category_detail'), # 路由名称:用于反向解析分类首页,名称为'category_detail'
+
+ # 5. 分类分页路由:匹配带页码的分类页(如/category/python/3.html)
+ path(
+ r'category//.html', # 路径参数:分类slug + 分页页码(整数)
+ views.CategoryDetailView.as_view(), # 复用分类详情视图类,处理分页逻辑
+ name='category_detail_page'), # 路由名称:用于反向解析分类分页,名称为'category_detail_page'
+
+ # 6. 作者文章列表路由:匹配作者名称对应的文章列表页(如/author/liangliangyy.html)
+ path(
+ r'author/.html', # 路径参数:author_name(字符串类型,默认不限制字符,匹配作者用户名)
+ views.AuthorDetailView.as_view(), # 关联作者文章列表视图类
+ name='author_detail'), # 路由名称:用于反向解析作者首页,名称为'author_detail'
+
+ # 7. 作者文章分页路由:匹配带页码的作者文章列表页(如/author/liangliangyy/2.html)
+ path(
+ r'author//.html', # 路径参数:作者名称 + 分页页码(整数)
+ views.AuthorDetailView.as_view(), # 复用作者文章列表视图类,处理分页逻辑
+ name='author_detail_page'), # 路由名称:用于反向解析作者分页,名称为'author_detail_page'
+
+ # 8. 标签详情页路由:匹配标签slug对应的文章列表页(如/tag/django.html)
+ path(
+ r'tag/.html', # 路径参数:,标签的URL友好标识
+ views.TagDetailView.as_view(), # 关联标签详情视图类
+ name='tag_detail'), # 路由名称:用于反向解析标签首页,名称为'tag_detail'
+
+ # 9. 标签分页路由:匹配带页码的标签文章列表页(如/tag/django/4.html)
+ path(
+ r'tag//.html', # 路径参数:标签slug + 分页页码(整数)
+ views.TagDetailView.as_view(), # 复用标签详情视图类,处理分页逻辑
+ name='tag_detail_page'), # 路由名称:用于反向解析标签分页,名称为'tag_detail_page'
+
+ # 10. 文章归档页路由:匹配archives.html路径(如/archives.html)
+ path(
+ 'archives.html', # 路由路径:固定为'archives.html'
+ cache_page(60 * 60)(views.ArchivesView.as_view()), # 添加页面缓存:缓存60*60=3600秒(1小时),提升访问性能
+ name='archives'), # 路由名称:用于反向解析归档页,名称为'archives'
+
+ # 11. 友情链接页路由:匹配links.html路径(如/links.html)
+ path(
+ 'links.html', # 路由路径:固定为'links.html'
+ views.LinkListView.as_view(), # 关联友情链接列表视图类
+ name='links'), # 路由名称:用于反向解析友情链接页,名称为'links'
+
+ # 12. 文件上传接口路由:匹配'upload'路径(如/upload)
+ path(
+ r'upload', # 路由路径:固定为'upload'(无后缀)
+ views.fileupload, # 关联文件上传视图函数(非类视图,直接关联函数)
+ name='upload'), # 路由名称:用于反向解析上传接口,名称为'upload'
+
+ # 13. 清理缓存接口路由:匹配'clean'路径(如/clean)
+ path(
+ r'clean', # 路由路径:固定为'clean'(无后缀)
+ views.clean_cache_view, # 关联清理缓存视图函数
+ name='clean'), # 路由名称:用于反向解析清理缓存接口,名称为'clean'
+]
\ No newline at end of file
diff --git a/doc/第7周代码注释/blog/views.py b/doc/第7周代码注释/blog/views.py
new file mode 100644
index 00000000..568c1c7b
--- /dev/null
+++ b/doc/第7周代码注释/blog/views.py
@@ -0,0 +1,506 @@
+# 导入Python内置logging模块,用于记录视图运行日志(如缓存命中、错误信息)
+import logging
+# 导入Python内置os模块,用于文件路径操作(如文件上传时创建目录)
+import os
+# 导入Python内置uuid模块,用于生成唯一文件名(避免文件上传时重名)
+import uuid
+
+# 导入Django项目配置模块,用于获取项目设置(如分页数量、文件上传路径)
+from django.conf import settings
+# 导入Django分页类,用于处理评论分页逻辑
+from django.core.paginator import Paginator
+# 导入DjangoHTTP响应类:
+# 1. HttpResponse:返回普通响应
+# 2. HttpResponseForbidden:返回403禁止访问响应
+from django.http import HttpResponse, HttpResponseForbidden
+# 导入Django快捷函数:
+# 1. get_object_or_404:获取对象,不存在则返回404页面
+# 2. render:渲染模板并返回响应
+from django.shortcuts import get_object_or_404
+from django.shortcuts import render
+# 导入Django静态文件模板标签,用于生成静态文件URL(如上传文件的访问URL)
+from django.templatetags.static import static
+# 导入Django时区工具,用于处理时间相关操作(如文件上传目录按日期划分)
+from django.utils import timezone
+# 导入Django国际化翻译工具,用于错误信息的多语言支持
+from django.utils.translation import gettext_lazy as _
+# 导入Django CSRF豁免装饰器,用于文件上传接口(避免CSRF验证)
+from django.views.decorators.csrf import csrf_exempt
+# 导入Django通用视图:
+# 1. DetailView:详情页通用视图(适用于单条数据展示,如文章详情)
+# 2. ListView:列表页通用视图(适用于多条数据展示,如文章列表、分类列表)
+from django.views.generic.detail import DetailView
+from django.views.generic.list import ListView
+# 导入Haystack搜索视图,用于实现全文搜索功能
+from haystack.views import SearchView
+
+# 从当前应用导入核心模型:文章、分类、链接显示类型、友情链接、标签
+from blog.models import Article, Category, LinkShowType, Links, Tag
+# 从comments应用导入评论表单,用于文章详情页的评论提交
+from comments.forms import CommentForm
+# 从自定义工具模块导入工具函数:
+# 1. cache:缓存操作对象,用于读写缓存
+# 2. get_blog_setting:获取博客全局配置(如评论分页数量)
+# 3. get_sha256:生成SHA256加密字符串(用于文件上传签名验证)
+from djangoblog.utils import cache, get_blog_setting, get_sha256
+
+# 创建日志记录器,日志名称与当前模块一致(__name__),便于定位日志来源
+logger = logging.getLogger(__name__)
+
+
+# 定义文章列表基础视图类ArticleListView,继承自Django的ListView(列表页通用视图)
+# 作用:封装所有列表类视图(首页、分类列表、标签列表、作者列表)的公共逻辑(分页、缓存、上下文处理)
+class ArticleListView(ListView):
+ # template_name:指定列表页渲染模板(所有子类可复用或重写)
+ template_name = 'blog/article_index.html'
+
+ # context_object_name:指定模板中使用的上下文变量名(模板中通过{{ article_list }}访问列表数据)
+ context_object_name = 'article_list'
+
+ # page_type:页面类型标识(用于模板显示标题,如"分类目录归档"),子类需重写
+ page_type = ''
+ # paginate_by:分页数量,从项目配置中读取(settings.PAGINATE_BY)
+ paginate_by = settings.PAGINATE_BY
+ # page_kwarg:分页参数名(URL中用于传递页码的参数,默认'page')
+ page_kwarg = 'page'
+ # link_type:友情链接显示类型(关联LinkShowType枚举,控制不同页面显示不同链接)
+ link_type = LinkShowType.L
+
+ # 定义获取视图缓存key的方法(当前未实际使用,预留扩展)
+ def get_view_cache_key(self):
+ return self.request.get['pages']
+
+ # 页码属性:通过@property装饰器将方法转为属性,统一获取当前页码(从URL参数或默认值1)
+ @property
+ def page_number(self):
+ page_kwarg = self.page_kwarg
+ # 从URL路径参数、GET参数中获取页码,均无则默认1
+ page = self.kwargs.get(
+ page_kwarg) or self.request.GET.get(page_kwarg) or 1
+ return page
+
+ # 定义获取查询集缓存key的抽象方法(子类必须重写,确保缓存key唯一)
+ def get_queryset_cache_key(self):
+ """
+ 子类重写.获得queryset的缓存key
+ """
+ raise NotImplementedError()
+
+ # 定义获取查询集数据的抽象方法(子类必须重写,实现具体数据查询逻辑)
+ def get_queryset_data(self):
+ """
+ 子类重写.获取queryset的数据
+ """
+ raise NotImplementedError()
+
+ # 从缓存中获取查询集数据(缓存命中则直接返回,未命中则查询并缓存)
+ def get_queryset_from_cache(self, cache_key):
+ '''
+ 缓存页面数据
+ :param cache_key: 缓存key(唯一标识缓存数据)
+ :return: 文章列表数据(从缓存或数据库获取)
+ '''
+ value = cache.get(cache_key) # 尝试从缓存获取数据
+ if value:
+ # 缓存命中:记录日志并返回数据
+ logger.info('get view cache.key:{key}'.format(key=cache_key))
+ return value
+ else:
+ # 缓存未命中:调用子类实现的get_queryset_data获取数据库数据
+ article_list = self.get_queryset_data()
+ cache.set(cache_key, article_list) # 将数据存入缓存
+ logger.info('set view cache.key:{key}'.format(key=cache_key))
+ return article_list
+
+ # 重写ListView的get_queryset方法:从缓存获取数据(替代默认直接查询数据库)
+ def get_queryset(self):
+ '''
+ 重写默认,从缓存获取数据
+ :return: 文章列表查询集
+ '''
+ key = self.get_queryset_cache_key() # 获取子类定义的缓存key
+ value = self.get_queryset_from_cache(key) # 从缓存获取数据
+ return value
+
+ # 重写ListView的get_context_data方法:添加额外上下文变量(友情链接显示类型)
+ def get_context_data(self, **kwargs):
+ kwargs['linktype'] = self.link_type # 传递友情链接显示类型到模板
+ # 调用父类方法,保留原有上下文数据(如分页数据、文章列表)
+ return super(ArticleListView, self).get_context_data(** kwargs)
+
+
+# 定义首页视图类IndexView,继承自ArticleListView(复用列表页公共逻辑)
+class IndexView(ArticleListView):
+ '''
+ 首页视图:展示已发布的普通文章列表
+ '''
+ # 重写友情链接显示类型:首页显示(LinkShowType.I)
+ link_type = LinkShowType.I
+
+ # 实现父类抽象方法:获取首页文章数据(查询已发布的普通文章)
+ def get_queryset_data(self):
+ # 筛选条件:type='a'(普通文章)、status='p'(已发布)
+ article_list = Article.objects.filter(type='a', status='p')
+ return article_list
+
+ # 实现父类抽象方法:生成首页缓存key(包含页码,确保不同分页缓存独立)
+ def get_queryset_cache_key(self):
+ cache_key = 'index_{page}'.format(page=self.page_number)
+ return cache_key
+
+
+# 定义文章详情页视图类ArticleDetailView,继承自Django的DetailView(详情页通用视图)
+class ArticleDetailView(DetailView):
+ '''
+ 文章详情页面视图:展示单篇文章详情、评论列表及评论分页
+ '''
+ template_name = 'blog/article_detail.html' # 详情页渲染模板
+ model = Article # 关联的模型(自动从数据库查询文章数据)
+ pk_url_kwarg = 'article_id' # URL中传递文章ID的参数名(与路由配置一致)
+ context_object_name = "article" # 模板中使用的上下文变量名({{ article }}访问文章数据)
+
+ # 重写DetailView的get_object方法:获取文章对象后更新浏览量
+ def get_object(self, queryset=None):
+ # 调用父类方法获取文章对象
+ obj = super(ArticleDetailView, self).get_object()
+ obj.viewed() # 调用Article模型的viewed方法,浏览量+1
+ self.object = obj # 保存文章对象到实例属性
+ return obj
+
+ # 重写DetailView的get_context_data方法:添加评论表单、评论列表、分页等额外上下文
+ def get_context_data(self, **kwargs):
+ comment_form = CommentForm() # 初始化评论表单(供用户提交评论)
+
+ # 获取当前文章的评论列表(从缓存或数据库,Article模型的comment_list方法已实现缓存)
+ article_comments = self.object.comment_list()
+ # 筛选顶级评论(parent_comment=None,无父评论的评论)
+ parent_comments = article_comments.filter(parent_comment=None)
+ # 获取博客全局配置(如文章页评论分页数量)
+ blog_setting = get_blog_setting()
+ # 初始化评论分页器(按配置的评论数量分页)
+ paginator = Paginator(parent_comments, blog_setting.article_comment_count)
+
+ # 从GET参数获取评论页码,默认1(若参数非法则重置为1)
+ page = self.request.GET.get('comment_page', '1')
+ if not page.isnumeric():
+ page = 1
+ else:
+ page = int(page)
+ if page < 1:
+ page = 1
+ if page > paginator.num_pages:
+ page = paginator.num_pages
+
+ # 获取当前页的评论数据
+ p_comments = paginator.page(page)
+ # 计算下一页、上一页页码(无则为None)
+ next_page = p_comments.next_page_number() if p_comments.has_next() else None
+ prev_page = p_comments.previous_page_number() if p_comments.has_previous() else None
+
+ # 生成下一页评论URL(含锚点#commentlist-container,直接跳转到评论区)
+ if next_page:
+ kwargs[
+ 'comment_next_page_url'] = self.object.get_absolute_url() + f'?comment_page={next_page}#commentlist-container'
+ # 生成上一页评论URL
+ if prev_page:
+ kwargs[
+ 'comment_prev_page_url'] = self.object.get_absolute_url() + f'?comment_page={prev_page}#commentlist-container'
+
+ # 向上下文添加额外数据(供模板使用)
+ kwargs['form'] = comment_form # 评论表单
+ kwargs['article_comments'] = article_comments # 所有评论
+ kwargs['p_comments'] = p_comments # 当前页评论
+ kwargs['comment_count'] = len(
+ article_comments) if article_comments else 0 # 评论总数
+
+ # 上一篇、下一篇文章(从缓存获取,Article模型的next_article/prev_article方法已实现缓存)
+ kwargs['next_article'] = self.object.next_article
+ kwargs['prev_article'] = self.object.prev_article
+
+ # 调用父类方法,保留原有上下文数据(如文章对象)
+ return super(ArticleDetailView, self).get_context_data(** kwargs)
+
+
+# 定义分类详情视图类CategoryDetailView,继承自ArticleListView
+class CategoryDetailView(ArticleListView):
+ '''
+ 分类目录列表视图:展示指定分类及子分类下的已发布文章
+ '''
+ page_type = "分类目录归档" # 页面类型标识(模板中显示该标题)
+
+ # 实现父类抽象方法:获取分类下的文章数据
+ def get_queryset_data(self):
+ # 从URL路径参数获取分类slug(与路由配置的对应)
+ slug = self.kwargs['category_name']
+ # 获取分类对象,不存在则返回404
+ category = get_object_or_404(Category, slug=slug)
+
+ categoryname = category.name # 分类名称
+ self.categoryname = categoryname # 保存到实例属性,供后续生成缓存key和上下文使用
+ # 获取当前分类的所有子分类名称(含自身)
+ categorynames = list(
+ map(lambda c: c.name, category.get_sub_categorys()))
+ # 筛选条件:分类名称在子分类列表中、状态为已发布(status='p')
+ article_list = Article.objects.filter(
+ category__name__in=categorynames, status='p')
+ return article_list
+
+ # 实现父类抽象方法:生成分类列表缓存key(含分类名称和页码,确保缓存唯一)
+ def get_queryset_cache_key(self):
+ slug = self.kwargs['category_name']
+ category = get_object_or_404(Category, slug=slug)
+ categoryname = category.name
+ self.categoryname = categoryname
+ cache_key = 'category_list_{categoryname}_{page}'.format(
+ categoryname=categoryname, page=self.page_number)
+ return cache_key
+
+ # 重写父类的get_context_data方法:添加分类相关上下文(页面类型、分类名称)
+ def get_context_data(self, **kwargs):
+ categoryname = self.categoryname
+ # 处理分类名称(若含'/',取最后一部分,避免多级分类显示异常)
+ try:
+ categoryname = categoryname.split('/')[-1]
+ except BaseException:
+ pass
+ kwargs['page_type'] = CategoryDetailView.page_type # 页面类型
+ kwargs['tag_name'] = categoryname # 分类名称(模板中显示)
+ return super(CategoryDetailView, self).get_context_data(** kwargs)
+
+
+# 定义作者详情视图类AuthorDetailView,继承自ArticleListView
+class AuthorDetailView(ArticleListView):
+ '''
+ 作者文章列表视图:展示指定作者发布的已发布文章
+ '''
+ page_type = '作者文章归档' # 页面类型标识
+
+ # 实现父类抽象方法:生成作者文章列表缓存key
+ def get_queryset_cache_key(self):
+ from uuslug import slugify # 延迟导入slugify函数,避免循环导入
+ # 从URL路径参数获取作者名称,转换为slug格式(确保缓存key统一)
+ author_name = slugify(self.kwargs['author_name'])
+ cache_key = 'author_{author_name}_{page}'.format(
+ author_name=author_name, page=self.page_number)
+ return cache_key
+
+ # 实现父类抽象方法:获取指定作者的文章数据
+ def get_queryset_data(self):
+ # 从URL路径参数获取作者名称
+ author_name = self.kwargs['author_name']
+ # 筛选条件:作者用户名匹配、类型为普通文章(type='a')、状态为已发布(status='p')
+ article_list = Article.objects.filter(
+ author__username=author_name, type='a', status='p')
+ return article_list
+
+ # 重写父类的get_context_data方法:添加作者相关上下文(页面类型、作者名称)
+ def get_context_data(self, **kwargs):
+ author_name = self.kwargs['author_name']
+ kwargs['page_type'] = AuthorDetailView.page_type # 页面类型
+ kwargs['tag_name'] = author_name # 作者名称(模板中显示)
+ return super(AuthorDetailView, self).get_context_data(** kwargs)
+
+
+# 定义标签详情视图类TagDetailView,继承自ArticleListView
+class TagDetailView(ArticleListView):
+ '''
+ 标签列表视图:展示指定标签下的已发布文章
+ '''
+ page_type = '分类标签归档' # 页面类型标识
+
+ # 实现父类抽象方法:获取标签下的文章数据
+ def get_queryset_data(self):
+ # 从URL路径参数获取标签slug
+ slug = self.kwargs['tag_name']
+ # 获取标签对象,不存在则返回404
+ tag = get_object_or_404(Tag, slug=slug)
+ tag_name = tag.name # 标签名称
+ self.name = tag_name # 保存到实例属性
+ # 筛选条件:标签名称匹配、状态为已发布(status='p')
+ article_list = Article.objects.filter(
+ tags__name=tag_name, type='a', status='p')
+ return article_list
+
+ # 实现父类抽象方法:生成标签列表缓存key(含标签名称和页码)
+ def get_queryset_cache_key(self):
+ slug = self.kwargs['tag_name']
+ tag = get_object_or_404(Tag, slug=slug)
+ tag_name = tag.name
+ self.name = tag_name
+ cache_key = 'tag_{tag_name}_{page}'.format(
+ tag_name=tag_name, page=self.page_number)
+ return cache_key
+
+ # 重写父类的get_context_data方法:添加标签相关上下文(页面类型、标签名称)
+ def get_context_data(self, **kwargs):
+ tag_name = self.name # 从实例属性获取标签名称
+ kwargs['page_type'] = TagDetailView.page_type # 页面类型
+ kwargs['tag_name'] = tag_name # 标签名称(模板中显示)
+ return super(TagDetailView, self).get_context_data(** kwargs)
+
+
+# 定义文章归档视图类ArchivesView,继承自ArticleListView
+class ArchivesView(ArticleListView):
+ '''
+ 文章归档页面视图:展示所有已发布文章(按时间归档)
+ '''
+ page_type = '文章归档' # 页面类型标识
+ paginate_by = None # 关闭分页(归档页显示所有文章,不分页)
+ page_kwarg = None # 无需分页参数
+ template_name = 'blog/article_archives.html' # 归档页专用模板
+
+ # 实现父类抽象方法:获取所有已发布文章
+ def get_queryset_data(self):
+ return Article.objects.filter(status='p').all()
+
+ # 实现父类抽象方法:生成归档页缓存key(无页码,因不分页)
+ def get_queryset_cache_key(self):
+ cache_key = 'archives'
+ return cache_key
+
+
+# 定义友情链接列表视图类LinkListView,继承自Django的ListView
+class LinkListView(ListView):
+ model = Links # 关联Links模型
+ template_name = 'blog/links_list.html' # 友情链接页面模板
+
+ # 重写get_queryset方法:仅查询已启用的友情链接
+ def get_queryset(self):
+ return Links.objects.filter(is_enable=True)
+
+
+# 定义搜索视图类EsSearchView,继承自Haystack的SearchView
+class EsSearchView(SearchView):
+ # 重写get_context方法:构建搜索结果页面的上下文数据(分页、搜索关键词、拼写建议)
+ def get_context(self):
+ # 构建分页器和当前页数据(Haystack内置方法)
+ paginator, page = self.build_page()
+ # 基础上下文数据:搜索关键词、搜索表单、分页数据、拼写建议
+ context = {
+ "query": self.query, # 搜索关键词
+ "form": self.form, # 搜索表单
+ "page": page, # 当前页搜索结果
+ "paginator": paginator, # 分页器
+ "suggestion": None, # 拼写建议(默认None)
+ }
+ # 若搜索引擎支持拼写建议,获取建议词
+ if hasattr(self.results, "query") and self.results.query.backend.include_spelling:
+ context["suggestion"] = self.results.query.get_spelling_suggestion()
+ # 添加额外上下文数据(子类可扩展)
+ context.update(self.extra_context())
+
+ return context
+
+
+# CSRF豁免装饰器:关闭CSRF验证(文件上传接口通常由第三方工具调用,无需CSRF令牌)
+@csrf_exempt
+def fileupload(request):
+ """
+ 文件上传接口:提供图床功能,支持图片/文件上传,返回文件访问URL
+ 注意:需通过调用端传递正确签名才能上传,仅允许POST请求
+ :param request: HTTP请求对象
+ :return: 上传成功返回文件URL列表,失败返回403/错误信息
+ """
+ # 仅允许POST请求(文件上传需用POST)
+ if request.method == 'POST':
+ # 从GET参数获取签名(用于验证上传权限)
+ sign = request.GET.get('sign', None)
+ if not sign:
+ return HttpResponseForbidden() # 无签名,返回403禁止访问
+ # 验证签名:双重SHA256加密项目SECRET_KEY,与传递的sign比对(防止未授权上传)
+ if not sign == get_sha256(get_sha256(settings.SECRET_KEY)):
+ return HttpResponseForbidden() # 签名不匹配,返回403
+
+ response = [] # 存储上传成功的文件URL
+ # 遍历请求中的所有上传文件(支持多文件同时上传)
+ for filename in request.FILES:
+ # 生成日期字符串(按年/月/日划分上传目录,便于管理)
+ timestr = timezone.now().strftime('%Y/%m/%d')
+ # 支持的图片格式后缀
+ imgextensions = ['jpg', 'png', 'jpeg', 'bmp']
+ # 文件名字符串处理
+ fname = u''.join(str(filename))
+ # 判断是否为图片文件(检查文件名是否包含图片后缀)
+ isimage = len([i for i in imgextensions if fname.find(i) >= 0]) > 0
+
+ # 定义文件保存根目录:
+ # - 图片文件保存到 static/files/image/年/月/日
+ # - 其他文件保存到 static/files/files/年/月/日
+ base_dir = os.path.join(settings.STATICFILES, "files" if not isimage else "image", timestr)
+ # 若目录不存在,创建多级目录
+ if not os.path.exists(base_dir):
+ os.makedirs(base_dir)
+ # 生成唯一文件名:UUID+原文件后缀(避免重名)
+ savepath = os.path.normpath(os.path.join(base_dir, f"{uuid.uuid4().hex}{os.path.splitext(filename)[-1]}"))
+
+ # 安全校验:确保保存路径在基础目录内(防止路径穿越攻击)
+ if not savepath.startswith(base_dir):
+ return HttpResponse("only for post")
+
+ # 写入文件:分块写入(处理大文件上传,避免内存溢出)
+ with open(savepath, 'wb+') as wfile:
+ for chunk in request.FILES[filename].chunks():
+ wfile.write(chunk)
+
+ # 若为图片文件,压缩图片(降低文件大小,提升访问速度)
+ if isimage:
+ from PIL import Image # 延迟导入PIL库(图片处理)
+ image = Image.open(savepath)
+ image.save(savepath, quality=20, optimize=True) # 质量20,开启优化
+
+ # 生成文件访问URL(通过static标签生成静态文件URL)
+ url = static(savepath)
+ response.append(url) # 将URL添加到响应列表
+
+ # 返回URL列表(字符串格式)
+ return HttpResponse(response)
+
+ else:
+ # 非POST请求,返回错误信息
+ return HttpResponse("only for post")
+
+
+# 404页面未找到视图:处理所有不存在的URL请求
+def page_not_found_view(
+ request,
+ exception,
+ template_name='blog/error_page.html'):
+ if exception:
+ logger.error(exception) # 记录异常信息到日志
+ url = request.get_full_path() # 获取用户访问的不存在的URL
+ # 渲染404错误页面,传递错误信息和状态码
+ return render(request,
+ template_name,
+ {'message': _('Sorry, the page you requested is not found, please click the home page to see other?'),
+ 'statuscode': '404'},
+ status=404)
+
+
+# 500服务器错误视图:处理服务器内部错误
+def server_error_view(request, template_name='blog/error_page.html'):
+ # 渲染500错误页面,传递错误信息和状态码
+ return render(request,
+ template_name,
+ {'message': _('Sorry, the server is busy, please click the home page to see other?'),
+ 'statuscode': '500'},
+ status=500)
+
+
+# 403权限拒绝视图:处理无权限访问的请求
+def permission_denied_view(
+ request,
+ exception,
+ template_name='blog/error_page.html'):
+ if exception:
+ logger.error(exception) # 记录异常信息到日志
+ # 渲染403错误页面,传递错误信息和状态码
+ return render(
+ request, template_name, {
+ 'message': _('Sorry, you do not have permission to access this page?'),
+ 'statuscode': '403'}, status=403)
+
+
+# 清理缓存视图:清空所有缓存(用于手动刷新缓存)
+def clean_cache_view(request):
+ cache.clear() # 调用缓存工具的clear方法,清空所有缓存
+ return HttpResponse('ok') # 返回成功响应
\ No newline at end of file
diff --git a/doc/第7周代码注释/comments/__init__.py b/doc/第7周代码注释/comments/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/doc/第7周代码注释/comments/__pycache__/__init__.cpython-312.pyc b/doc/第7周代码注释/comments/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 00000000..ac870989
Binary files /dev/null and b/doc/第7周代码注释/comments/__pycache__/__init__.cpython-312.pyc differ
diff --git a/doc/第7周代码注释/comments/__pycache__/admin.cpython-312.pyc b/doc/第7周代码注释/comments/__pycache__/admin.cpython-312.pyc
new file mode 100644
index 00000000..2036388e
Binary files /dev/null and b/doc/第7周代码注释/comments/__pycache__/admin.cpython-312.pyc differ
diff --git a/doc/第7周代码注释/comments/__pycache__/apps.cpython-312.pyc b/doc/第7周代码注释/comments/__pycache__/apps.cpython-312.pyc
new file mode 100644
index 00000000..e87c7337
Binary files /dev/null and b/doc/第7周代码注释/comments/__pycache__/apps.cpython-312.pyc differ
diff --git a/doc/第7周代码注释/comments/__pycache__/forms.cpython-312.pyc b/doc/第7周代码注释/comments/__pycache__/forms.cpython-312.pyc
new file mode 100644
index 00000000..7b9669c1
Binary files /dev/null and b/doc/第7周代码注释/comments/__pycache__/forms.cpython-312.pyc differ
diff --git a/doc/第7周代码注释/comments/__pycache__/models.cpython-312.pyc b/doc/第7周代码注释/comments/__pycache__/models.cpython-312.pyc
new file mode 100644
index 00000000..ef0bd34d
Binary files /dev/null and b/doc/第7周代码注释/comments/__pycache__/models.cpython-312.pyc differ
diff --git a/doc/第7周代码注释/comments/__pycache__/urls.cpython-312.pyc b/doc/第7周代码注释/comments/__pycache__/urls.cpython-312.pyc
new file mode 100644
index 00000000..6f4548c9
Binary files /dev/null and b/doc/第7周代码注释/comments/__pycache__/urls.cpython-312.pyc differ
diff --git a/doc/第7周代码注释/comments/__pycache__/utils.cpython-312.pyc b/doc/第7周代码注释/comments/__pycache__/utils.cpython-312.pyc
new file mode 100644
index 00000000..8871618c
Binary files /dev/null and b/doc/第7周代码注释/comments/__pycache__/utils.cpython-312.pyc differ
diff --git a/doc/第7周代码注释/comments/__pycache__/views.cpython-312.pyc b/doc/第7周代码注释/comments/__pycache__/views.cpython-312.pyc
new file mode 100644
index 00000000..a68878ff
Binary files /dev/null and b/doc/第7周代码注释/comments/__pycache__/views.cpython-312.pyc differ
diff --git a/doc/第7周代码注释/comments/admin.py b/doc/第7周代码注释/comments/admin.py
new file mode 100644
index 00000000..30331b36
--- /dev/null
+++ b/doc/第7周代码注释/comments/admin.py
@@ -0,0 +1,69 @@
+from django.contrib import admin
+from django.urls import reverse
+from django.utils.html import format_html
+from django.utils.translation import gettext_lazy as _
+
+
+# 批量禁用评论(将 is_enable 设为 False)
+def disable_commentstatus(modeladmin, request, queryset):
+ queryset.update(is_enable=False)
+
+
+# 批量启用评论(将 is_enable 设为 True)
+def enable_commentstatus(modeladmin, request, queryset):
+ queryset.update(is_enable=True)
+
+
+# 定义动作名称,在 Django Admin 批量操作菜单中显示的文字
+disable_commentstatus.short_description = _('Disable comments')
+enable_commentstatus.short_description = _('Enable comments')
+
+
+class CommentAdmin(admin.ModelAdmin):
+ # 每页显示评论数量
+ list_per_page = 20
+
+ # 在评论列表中显示哪些字段
+ list_display = (
+ 'id',
+ 'body', # 评论正文
+ 'link_to_userinfo', # 用户信息(带链接)
+ 'link_to_article', # 所属文章(带链接)
+ 'is_enable', # 是否启用
+ 'creation_time' # 创建时间
+ )
+
+ # 可以点击哪些字段进入编辑页面
+ list_display_links = ('id', 'body', 'is_enable')
+
+ # 过滤器(后台右侧筛选功能)
+ list_filter = ('is_enable',)
+
+ # 排除不需要在后台编辑的字段(自动时间字段不应手动修改)
+ exclude = ('creation_time', 'last_modify_time')
+
+ # 批量操作按钮
+ actions = [disable_commentstatus, enable_commentstatus]
+
+ # 显示用户信息,并可点击跳转到用户编辑页面
+ def link_to_userinfo(self, obj):
+ # 获取目标 admin change 页面的 URL 路径
+ info = (obj.author._meta.app_label, obj.author._meta.model_name)
+ link = reverse('admin:%s_%s_change' % info, args=(obj.author.id,))
+ # 显示用户昵称,若无昵称显示 email
+ return format_html(
+ '{} '.format(
+ link,
+ obj.author.nickname if obj.author.nickname else obj.author.email
+ )
+ )
+
+ # 显示所属文章,并可点击跳转到文章编辑页面
+ def link_to_article(self, obj):
+ info = (obj.article._meta.app_label, obj.article._meta.model_name)
+ link = reverse('admin:%s_%s_change' % info, args=(obj.article.id,))
+ return format_html('{} '.format(link, obj.article.title))
+
+ # 设置在后台列表中显示的列标题
+ link_to_userinfo.short_description = _('User')
+ link_to_article.short_description = _('Article')
diff --git a/doc/第7周代码注释/comments/apps.py b/doc/第7周代码注释/comments/apps.py
new file mode 100644
index 00000000..b81f7484
--- /dev/null
+++ b/doc/第7周代码注释/comments/apps.py
@@ -0,0 +1,10 @@
+from django.apps import AppConfig
+
+
+class CommentsConfig(AppConfig):
+ # 指定该 App 在项目中的名称(即所在目录名)
+ name = 'comments'
+
+ # (可选)可以在这里做初始化操作,如引入 signals
+ # def ready(self):
+ # import comments.signals
diff --git a/doc/第7周代码注释/comments/forms.py b/doc/第7周代码注释/comments/forms.py
new file mode 100644
index 00000000..c1f1affd
--- /dev/null
+++ b/doc/第7周代码注释/comments/forms.py
@@ -0,0 +1,21 @@
+from django import forms
+from django.forms import ModelForm
+
+from .models import Comment
+
+
+class CommentForm(ModelForm):
+ # 用于存储父评论的 ID(实现评论回复功能)
+ # 该字段不会显示到页面中(HiddenInput),允许为空(一级评论时为空)
+ parent_comment_id = forms.IntegerField(
+ widget=forms.HiddenInput,
+ required=False
+ )
+
+ class Meta:
+ # 指定该表单操作的模型为 Comment
+ model = Comment
+
+ # 只允许用户输入评论内容(body)
+ # 其他字段(如 author、article、parent_comment)将在视图中自动赋值
+ fields = ['body']
diff --git a/doc/第7周代码注释/comments/migrations/0001_initial.py b/doc/第7周代码注释/comments/migrations/0001_initial.py
new file mode 100644
index 00000000..7d3dc2e4
--- /dev/null
+++ b/doc/第7周代码注释/comments/migrations/0001_initial.py
@@ -0,0 +1,65 @@
+# 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
+
+
+class Migration(migrations.Migration):
+
+ # 表示这是该 app 的第一个迁移文件
+ initial = True
+
+ dependencies = [
+ # 依赖 blog 应用的第一条迁移文件,确保 Article 模型已经被创建
+ ('blog', '0001_initial'),
+ # 依赖 Django 的用户模型(可自定义 AUTH_USER_MODEL)
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ]
+
+ operations = [
+ # 创建 Comment 模型
+ migrations.CreateModel(
+ name='Comment',
+ fields=[
+ # 主键 id,自动递增
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+
+ # 评论内容,最大长度 300 字
+ ('body', models.TextField(max_length=300, verbose_name='正文')),
+
+ # 评论创建时间,默认取当前时间
+ ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
+
+ # 评论最后一次修改时间,默认也为当前时间
+ ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
+
+ # 评论是否显示,后台可控制隐藏
+ ('is_enable', models.BooleanField(default=True, verbose_name='是否显示')),
+
+ # 评论所属文章,一个评论只能属于一篇文章
+ # CASCADE 表示当文章删除时,该评论也会被删除
+ ('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.article', verbose_name='文章')),
+
+ # 评论作者,关联用户模型
+ ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='作者')),
+
+ # 父评论,用于实现评论/回复功能
+ # 允许为空(空代表这是一级评论)
+ # 父评论删除时,子评论也删除
+ ('parent_comment', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='comments.comment', verbose_name='上级评论')),
+ ],
+ options={
+ # 后台显示名
+ 'verbose_name': '评论',
+ 'verbose_name_plural': '评论',
+
+ # 查询时默认按 id 倒序排列(新的评论排最前)
+ 'ordering': ['-id'],
+
+ # get_latest_by 用于 Django 的 latest() 方法
+ 'get_latest_by': 'id',
+ },
+ ),
+ ]
diff --git a/doc/第7周代码注释/comments/migrations/0002_alter_comment_is_enable.py b/doc/第7周代码注释/comments/migrations/0002_alter_comment_is_enable.py
new file mode 100644
index 00000000..57b2a1d4
--- /dev/null
+++ b/doc/第7周代码注释/comments/migrations/0002_alter_comment_is_enable.py
@@ -0,0 +1,21 @@
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ # 当前迁移文件依赖于 comments 应用的 0001 初始迁移文件
+ dependencies = [
+ ('comments', '0001_initial'),
+ ]
+
+ operations = [
+ # 修改 Comment 模型中 is_enable 字段的属性
+ migrations.AlterField(
+ model_name='comment', # 要修改的模型名称
+ name='is_enable', # 要修改的字段名
+ field=models.BooleanField(
+ default=False, # 将默认值改为 False(即默认评论不显示)
+ verbose_name='是否显示' # 后台显示名称
+ ),
+ ),
+ ]
diff --git a/doc/第7周代码注释/comments/migrations/0003_alter_comment_options_remove_comment_created_time_and_more.py b/doc/第7周代码注释/comments/migrations/0003_alter_comment_options_remove_comment_created_time_and_more.py
new file mode 100644
index 00000000..b3d58c3f
--- /dev/null
+++ b/doc/第7周代码注释/comments/migrations/0003_alter_comment_options_remove_comment_created_time_and_more.py
@@ -0,0 +1,111 @@
+# 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
+
+
+class Migration(migrations.Migration):
+
+ # 迁移依赖顺序,确保其他相关模型先完成迁移
+ dependencies = [
+ # 依赖用户模型(可自定义 AUTH_USER_MODEL)
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+
+ # 依赖 blog 应用的某次迁移文件
+ ('blog', '0005_alter_article_options_alter_category_options_and_more'),
+
+ # 依赖 comments 应用之前的迁移调整(包括 is_enable 字段的变更)
+ ('comments', '0002_alter_comment_is_enable'),
+ ]
+
+ operations = [
+ # 修改模型的元选项(Meta 类中的配置)
+ migrations.AlterModelOptions(
+ name='comment',
+ options={
+ 'get_latest_by': 'id', # 使用 id 作为 latest() 的默认排序依据
+ 'ordering': ['-id'], # 查询结果默认按 id 降序排列(新评论在前)
+ 'verbose_name': 'comment', # 后台显示名称(单数)
+ 'verbose_name_plural': 'comment', # 后台显示名称(复数)
+ },
+ ),
+
+ # 删除原来用于记录创建时间的字段 created_time
+ migrations.RemoveField(
+ model_name='comment',
+ name='created_time',
+ ),
+
+ # 删除原来用于记录修改时间的字段 last_mod_time
+ migrations.RemoveField(
+ model_name='comment',
+ name='last_mod_time',
+ ),
+
+ # 新增评论创建时间字段(命名和 verbose_name 英文化)
+ migrations.AddField(
+ model_name='comment',
+ name='creation_time',
+ field=models.DateTimeField(
+ default=django.utils.timezone.now,
+ verbose_name='creation time',
+ ),
+ ),
+
+ # 新增评论最后修改时间字段(命名和 verbose_name 英文化)
+ migrations.AddField(
+ model_name='comment',
+ name='last_modify_time',
+ field=models.DateTimeField(
+ default=django.utils.timezone.now,
+ verbose_name='last modify time',
+ ),
+ ),
+
+ # 修改外键 article 字段的 verbose_name 显示文本
+ migrations.AlterField(
+ model_name='comment',
+ name='article',
+ field=models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE, # 文章删除时,本评论也删除
+ to='blog.article',
+ verbose_name='article',
+ ),
+ ),
+
+ # 修改外键 author 字段的 verbose_name 显示文本
+ migrations.AlterField(
+ model_name='comment',
+ name='author',
+ field=models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE, # 用户删除时,评论也删除
+ to=settings.AUTH_USER_MODEL,
+ verbose_name='author',
+ ),
+ ),
+
+ # 修改评论是否显示字段,将 verbose_name 英文化,默认不显示
+ migrations.AlterField(
+ model_name='comment',
+ name='is_enable',
+ field=models.BooleanField(
+ default=False,
+ verbose_name='enable', # 从中文变为英文显示
+ ),
+ ),
+
+ # 修改父评论字段(用于实现评论回复结构)
+ migrations.AlterField(
+ model_name='comment',
+ name='parent_comment',
+ field=models.ForeignKey(
+ blank=True, # 表单中允许为空
+ null=True, # 数据库中允许为 null
+ on_delete=django.db.models.deletion.CASCADE, # 父评论删除时子评论也删除
+ to='comments.comment',
+ verbose_name='parent comment',
+ ),
+ ),
+ ]
diff --git a/doc/第7周代码注释/comments/migrations/__init__.py b/doc/第7周代码注释/comments/migrations/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/doc/第7周代码注释/comments/migrations/__pycache__/0001_initial.cpython-312.pyc b/doc/第7周代码注释/comments/migrations/__pycache__/0001_initial.cpython-312.pyc
new file mode 100644
index 00000000..f6437038
Binary files /dev/null and b/doc/第7周代码注释/comments/migrations/__pycache__/0001_initial.cpython-312.pyc differ
diff --git a/doc/第7周代码注释/comments/migrations/__pycache__/0002_alter_comment_is_enable.cpython-312.pyc b/doc/第7周代码注释/comments/migrations/__pycache__/0002_alter_comment_is_enable.cpython-312.pyc
new file mode 100644
index 00000000..b1709e85
Binary files /dev/null and b/doc/第7周代码注释/comments/migrations/__pycache__/0002_alter_comment_is_enable.cpython-312.pyc differ
diff --git a/doc/第7周代码注释/comments/migrations/__pycache__/0003_alter_comment_options_remove_comment_created_time_and_more.cpython-312.pyc b/doc/第7周代码注释/comments/migrations/__pycache__/0003_alter_comment_options_remove_comment_created_time_and_more.cpython-312.pyc
new file mode 100644
index 00000000..eaf69163
Binary files /dev/null and b/doc/第7周代码注释/comments/migrations/__pycache__/0003_alter_comment_options_remove_comment_created_time_and_more.cpython-312.pyc differ
diff --git a/doc/第7周代码注释/comments/migrations/__pycache__/__init__.cpython-312.pyc b/doc/第7周代码注释/comments/migrations/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 00000000..928c5150
Binary files /dev/null and b/doc/第7周代码注释/comments/migrations/__pycache__/__init__.cpython-312.pyc differ
diff --git a/doc/第7周代码注释/comments/models.py b/doc/第7周代码注释/comments/models.py
new file mode 100644
index 00000000..c8b5565c
--- /dev/null
+++ b/doc/第7周代码注释/comments/models.py
@@ -0,0 +1,71 @@
+from django.conf import settings
+from django.db import models
+from django.utils.timezone import now
+from django.utils.translation import gettext_lazy as _
+
+from blog.models import Article
+
+
+class Comment(models.Model):
+ # 评论正文,限制最大输入长度 300,使用 TextField 以便输入多行内容
+ body = models.TextField('正文', max_length=300)
+
+ # 评论创建时间,默认使用 timezone.now(可自动获取当前时区时间)
+ creation_time = models.DateTimeField(
+ _('creation time'),
+ default=now
+ )
+
+ # 评论最后修改时间,通常用于编辑评论功能,但如果不编辑也会保持不变
+ last_modify_time = models.DateTimeField(
+ _('last modify time'),
+ default=now
+ )
+
+ # 评论作者,关联到用户模型,用户删除时,对应评论也一并删除
+ author = models.ForeignKey(
+ settings.AUTH_USER_MODEL,
+ verbose_name=_('author'),
+ on_delete=models.CASCADE
+ )
+
+ # 评论所属文章,关联到 Article 模型,文章被删除时,其下所有评论也被删除
+ article = models.ForeignKey(
+ Article,
+ verbose_name=_('article'),
+ on_delete=models.CASCADE
+ )
+
+ # 父评论,用于构建"评论回复"树结构
+ # 若为空 → 表示为一级评论;不为空 → 表示为某条评论的子评论
+ parent_comment = models.ForeignKey(
+ 'self', # 自关联
+ verbose_name=_('parent comment'),
+ blank=True, # 表单中允许为空
+ null=True, # 数据库允许为 null
+ on_delete=models.CASCADE # 父评论删除时,子评论也被删除
+ )
+
+ # 是否启用评论(常用于需要审核评论是否展示)
+ # 默认为 False → 新评论不会立刻显示,需要管理员审核启用
+ is_enable = models.BooleanField(
+ _('enable'),
+ default=False,
+ blank=False,
+ null=False
+ )
+
+ class Meta:
+ # 默认按 id 倒序排列 → 新评论显示在前
+ ordering = ['-id']
+
+ # Django Admin 后台显示的模型名称
+ verbose_name = _('comment')
+ verbose_name_plural = verbose_name
+
+ # latest() 方法依据 id 获取最新对象
+ get_latest_by = 'id'
+
+ def __str__(self):
+ # 后台及 shell 打印对象时显示评论内容
+ return self.body
diff --git a/doc/第7周代码注释/comments/templatetags/__init__.py b/doc/第7周代码注释/comments/templatetags/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/doc/第7周代码注释/comments/templatetags/__pycache__/__init__.cpython-312.pyc b/doc/第7周代码注释/comments/templatetags/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 00000000..7acd91cf
Binary files /dev/null and b/doc/第7周代码注释/comments/templatetags/__pycache__/__init__.cpython-312.pyc differ
diff --git a/doc/第7周代码注释/comments/templatetags/__pycache__/comments_tags.cpython-312.pyc b/doc/第7周代码注释/comments/templatetags/__pycache__/comments_tags.cpython-312.pyc
new file mode 100644
index 00000000..3acc0110
Binary files /dev/null and b/doc/第7周代码注释/comments/templatetags/__pycache__/comments_tags.cpython-312.pyc differ
diff --git a/doc/第7周代码注释/comments/templatetags/comments_tags.py b/doc/第7周代码注释/comments/templatetags/comments_tags.py
new file mode 100644
index 00000000..9b368d38
--- /dev/null
+++ b/doc/第7周代码注释/comments/templatetags/comments_tags.py
@@ -0,0 +1,55 @@
+from django import template
+
+# 注册一个自定义标签库
+register = template.Library()
+
+
+@register.simple_tag
+def parse_commenttree(commentlist, comment):
+ """
+ 获取指定评论的所有子评论(包括多级递归子评论)
+
+ 用法示例(在模板中):
+ {% parse_commenttree article_comments comment as childcomments %}
+
+ 参数解释:
+ commentlist:所有评论的查询集合(一般是 article.comments.all())
+ comment:当前评论对象
+
+ 返回值:
+ datas:按层级顺序递归展开的所有子评论列表
+ """
+ datas = [] # 用于存储递归解析得到的子评论
+
+ def parse(c):
+ # 找到当前评论 c 的直接子评论(过滤掉未启用的)
+ childs = commentlist.filter(parent_comment=c, is_enable=True)
+ for child in childs:
+ datas.append(child) # 保存子评论
+ parse(child) # 递归查找子评论的子评论
+
+ parse(comment)
+ return datas # 返回递归展开的所有子级评论
+
+
+@register.inclusion_tag('comments/tags/comment_item.html')
+def show_comment_item(comment, ischild):
+ """
+ 渲染评论项组件
+
+ 用法示例(在模板中):
+ {% show_comment_item comment True %}
+
+ 参数:
+ comment:需要渲染的评论对象
+ ischild:是否为子评论(用于模板样式控制,如缩进/层级)
+
+ depth 解释:
+ depth = 1 → 子评论,缩进更深
+ depth = 2 → 顶级评论,缩进较浅
+ """
+ depth = 1 if ischild else 2
+ return {
+ 'comment_item': comment, # 提供给模板的评论对象
+ 'depth': depth # 让模板根据层级调整样式
+ }
diff --git a/doc/第7周代码注释/comments/tests.py b/doc/第7周代码注释/comments/tests.py
new file mode 100644
index 00000000..4e85e820
--- /dev/null
+++ b/doc/第7周代码注释/comments/tests.py
@@ -0,0 +1,136 @@
+from django.test import Client, RequestFactory, TransactionTestCase
+from django.urls import reverse
+
+from accounts.models import BlogUser
+from blog.models import Category, Article
+from comments.models import Comment
+from comments.templatetags.comments_tags import *
+from djangoblog.utils import get_max_articleid_commentid
+
+
+class CommentsTest(TransactionTestCase):
+ """
+ 评论相关功能测试类
+ 使用 TransactionTestCase 允许测试包含事务的数据库操作
+ """
+
+ def setUp(self):
+ """
+ 测试初始化工作:
+ - 创建请求客户端
+ - 配置博客系统为“评论需要审核”
+ - 创建一个超级管理员用户(用于登录发表评论)
+ """
+ self.client = Client()
+ self.factory = RequestFactory()
+
+ from blog.models import BlogSettings
+ value = BlogSettings()
+ value.comment_need_review = True # 开启评论审核,提交的评论默认不显示
+ value.save()
+
+ # 创建可登录的超级管理员用户
+ self.user = BlogUser.objects.create_superuser(
+ email="liangliangyy1@gmail.com",
+ username="liangliangyy1",
+ password="liangliangyy1"
+ )
+
+ def update_article_comment_status(self, article):
+ """
+ 将文章下所有评论改为 is_enable=True
+ 模拟管理员审核通过评论(使评论显示)
+ """
+ comments = article.comment_set.all()
+ for comment in comments:
+ comment.is_enable = True
+ comment.save()
+
+ def test_validate_comment(self):
+ """
+ 测试评论提交、审核、评论树解析等功能流程
+ """
+ # 登录用户
+ self.client.login(username='liangliangyy1', password='liangliangyy1')
+
+ # 创建分类
+ category = Category()
+ category.name = "categoryccc"
+ category.save()
+
+ # 创建文章
+ article = Article()
+ article.title = "nicetitleccc"
+ article.body = "nicecontentccc"
+ article.author = self.user
+ article.category = category
+ article.type = 'a'
+ article.status = 'p'
+ article.save()
+
+ # 文章评论提交 URL
+ comment_url = reverse('comments:postcomment', kwargs={'article_id': article.id})
+
+ # 用户提交第一条评论
+ response = self.client.post(comment_url, {'body': '123ffffffffff'})
+ self.assertEqual(response.status_code, 302) # 正常应重定向(提交成功)
+
+ # 因为评论需要审核,未批准前评论数为 0
+ article = Article.objects.get(pk=article.pk)
+ self.assertEqual(len(article.comment_list()), 0)
+
+ # 模拟管理员审核评论
+ self.update_article_comment_status(article)
+ self.assertEqual(len(article.comment_list()), 1)
+
+ # 提交第二条评论
+ response = self.client.post(comment_url, {'body': '123ffffffffff'})
+ self.assertEqual(response.status_code, 302)
+
+ article = Article.objects.get(pk=article.pk)
+ self.update_article_comment_status(article)
+ self.assertEqual(len(article.comment_list()), 2)
+
+ # 回复第一条评论(测试 parent_comment 功能)
+ parent_comment_id = article.comment_list()[0].id
+
+ response = self.client.post(comment_url, {
+ 'body': '''
+ # Title1
+
+ ```python
+ import os
+ ```
+
+ [url](https://www.lylinux.net/)
+
+ [ddd](http://www.baidu.com)
+ ''',
+ 'parent_comment_id': parent_comment_id
+ })
+ self.assertEqual(response.status_code, 302)
+
+ # 通过审核
+ self.update_article_comment_status(article)
+
+ # 再次获取文章
+ article = Article.objects.get(pk=article.pk)
+ self.assertEqual(len(article.comment_list()), 3)
+
+ # 测试评论树解析
+ comment = Comment.objects.get(id=parent_comment_id)
+ tree = parse_commenttree(article.comment_list(), comment)
+ self.assertEqual(len(tree), 1) # 第一条评论应当有 1 个子评论
+
+ # 渲染评论组件标签是否正常返回
+ data = show_comment_item(comment, True)
+ self.assertIsNotNone(data)
+
+ # 测试工具函数获取最大文章/评论 id
+ s = get_max_articleid_commentid()
+ self.assertIsNotNone(s)
+
+ # 测试发送评论邮件通知(若配置邮件服务则会成功)
+ from comments.utils import send_comment_email
+ send_comment_email(comment)
+ send_comment_email(comment)
diff --git a/doc/第7周代码注释/comments/urls.py b/doc/第7周代码注释/comments/urls.py
new file mode 100644
index 00000000..16700351
--- /dev/null
+++ b/doc/第7周代码注释/comments/urls.py
@@ -0,0 +1,42 @@
+from django.contrib.auth.mixins import LoginRequiredMixin
+from django.http import HttpResponseRedirect
+from django.shortcuts import get_object_or_404
+from django.views.generic import View
+
+from blog.models import Article
+from .models import Comment
+from .forms import CommentForm
+
+
+class CommentPostView(LoginRequiredMixin, View):
+ """
+ 负责处理评论提交
+ """
+
+ def post(self, request, article_id):
+ # 获取目标文章
+ article = get_object_or_404(Article, pk=article_id)
+
+ form = CommentForm(request.POST)
+ if form.is_valid():
+ # 获取评论内容
+ body = form.cleaned_data['body'].strip()
+ parent_id = form.cleaned_data.get('parent_comment_id')
+
+ comment = Comment()
+ comment.article = article
+ comment.author = request.user
+ comment.body = body
+
+ # 判断是否是子评论(回复)
+ if parent_id:
+ try:
+ parent_comment = Comment.objects.get(id=parent_id)
+ comment.parent_comment = parent_comment
+ except Comment.DoesNotExist:
+ pass
+
+ comment.save()
+
+ # 评论成功后返回文章页面
+ return HttpResponseRedirect(article.get_absolute_url())
diff --git a/doc/第7周代码注释/comments/utils.py b/doc/第7周代码注释/comments/utils.py
new file mode 100644
index 00000000..91cd2a04
--- /dev/null
+++ b/doc/第7周代码注释/comments/utils.py
@@ -0,0 +1,67 @@
+import logging
+
+from django.utils.translation import gettext_lazy as _
+
+from djangoblog.utils import get_current_site
+from djangoblog.utils import send_email
+
+logger = logging.getLogger(__name__)
+
+
+def send_comment_email(comment):
+ """
+ 当用户发表评论后,给评论者发送邮件通知;
+ 如果该评论是回复别人的,则同时给被回复的用户发送提醒邮件。
+ """
+ # 获取当前站点域名
+ site = get_current_site().domain
+
+ # 邮件主题
+ subject = _('Thanks for your comment')
+
+ # 构造文章访问 URL(以便用户点进查看)
+ article_url = f"https://{site}{comment.article.get_absolute_url()}"
+
+ # 给评论者自己发送的邮件内容
+ html_content = _("""Thank you very much for your comments on this site
+ You can visit %(article_title)s
+ to review your comments,
+ Thank you again!
+
+ If the link above cannot be opened, please copy this link to your browser.
+ %(article_url)s""") % {
+ 'article_url': article_url,
+ 'article_title': comment.article.title
+ }
+
+ # 收件人 = 评论的作者本人
+ tomail = comment.author.email
+
+ # 发送邮件
+ send_email([tomail], subject, html_content)
+
+ try:
+ # 如果该评论存在父评论(说明是回复行为)
+ if comment.parent_comment:
+ # 给被回复的人发送通知邮件
+ html_content = _("""Your comment on %(article_title)s has
+ received a reply. %(comment_body)s
+
+ go check it out!
+
+ If the link above cannot be opened, please copy this link to your browser.
+ %(article_url)s
+ """) % {
+ 'article_url': article_url,
+ 'article_title': comment.article.title,
+ 'comment_body': comment.parent_comment.body
+ }
+ # 父评论的作者邮箱
+ tomail = comment.parent_comment.author.email
+
+ # 向被回复者发送邮件
+ send_email([tomail], subject, html_content)
+
+ except Exception as e:
+ # 出现错误则记录日志,但不影响评论正常流程
+ logger.error(e)
diff --git a/doc/第7周代码注释/comments/views.py b/doc/第7周代码注释/comments/views.py
new file mode 100644
index 00000000..1fafde4b
--- /dev/null
+++ b/doc/第7周代码注释/comments/views.py
@@ -0,0 +1,85 @@
+# Create your views here.
+from django.core.exceptions import ValidationError
+from django.http import HttpResponseRedirect
+from django.shortcuts import get_object_or_404
+from django.utils.decorators import method_decorator
+from django.views.decorators.csrf import csrf_protect
+from django.views.generic.edit import FormView
+
+from accounts.models import BlogUser
+from blog.models import Article
+from .forms import CommentForm
+from .models import Comment
+
+
+class CommentPostView(FormView):
+ """
+ 处理文章评论提交的视图
+ 使用 FormView 来处理表单提交
+ """
+ form_class = CommentForm # 使用的表单类
+ template_name = 'blog/article_detail.html' # 表单出错时重新渲染的模板
+
+ @method_decorator(csrf_protect)
+ def dispatch(self, *args, **kwargs):
+ # 强制开启 CSRF 防护
+ return super(CommentPostView, self).dispatch(*args, **kwargs)
+
+ def get(self, request, *args, **kwargs):
+ """
+ GET 请求时直接跳回文章详情页(因为评论应为 POST 行为)
+ """
+ article_id = self.kwargs['article_id']
+ article = get_object_or_404(Article, pk=article_id)
+ url = article.get_absolute_url()
+ return HttpResponseRedirect(url + "#comments") # 跳转到评论区域
+
+ def form_invalid(self, form):
+ """
+ 表单校验失败时重新渲染页面并显示错误信息
+ """
+ article_id = self.kwargs['article_id']
+ article = get_object_or_404(Article, pk=article_id)
+
+ return self.render_to_response({
+ 'form': form,
+ 'article': article
+ })
+
+ def form_valid(self, form):
+ """
+ 表单提交且内容合法时执行写入数据库逻辑
+ """
+ user = self.request.user
+ author = BlogUser.objects.get(pk=user.pk) # 获取当前评论的用户对象
+ article_id = self.kwargs['article_id']
+ article = get_object_or_404(Article, pk=article_id)
+
+ # 判断文章是否允许评论 (‘c’ 表示关闭评论状态)
+ if article.comment_status == 'c' or article.status == 'c':
+ raise ValidationError("该文章评论已关闭.")
+
+ # 创建一个未保存的 comment 对象
+ comment = form.save(False)
+ comment.article = article
+
+ # 获取博客配置:是否需要审核评论
+ from djangoblog.utils import get_blog_setting
+ settings = get_blog_setting()
+ if not settings.comment_need_review:
+ comment.is_enable = True # 如果无需审核,直接设为可显示
+
+ comment.author = author
+
+ # 处理父评论(即:回复某条评论)
+ if form.cleaned_data['parent_comment_id']:
+ parent_comment = Comment.objects.get(
+ pk=form.cleaned_data['parent_comment_id'])
+ comment.parent_comment = parent_comment
+
+ comment.save(True) # 保存评论
+
+ # 评论成功后跳回评论处
+ return HttpResponseRedirect(
+ "%s#div-comment-%d" %
+ (article.get_absolute_url(), comment.pk))
diff --git a/doc/第7周代码注释/djangoblog/__init__.py b/doc/第7周代码注释/djangoblog/__init__.py
new file mode 100644
index 00000000..131e2b39
--- /dev/null
+++ b/doc/第7周代码注释/djangoblog/__init__.py
@@ -0,0 +1,3 @@
+# 设置Django应用的默认配置类
+# 当Django启动时,会自动使用这里指定的应用配置类
+default_app_config = 'djangoblog.apps.DjangoblogAppConfig'
\ No newline at end of file
diff --git a/doc/第7周代码注释/djangoblog/__pycache__/__init__.cpython-312.pyc b/doc/第7周代码注释/djangoblog/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 00000000..73b17e9b
Binary files /dev/null and b/doc/第7周代码注释/djangoblog/__pycache__/__init__.cpython-312.pyc differ
diff --git a/doc/第7周代码注释/djangoblog/__pycache__/admin_site.cpython-312.pyc b/doc/第7周代码注释/djangoblog/__pycache__/admin_site.cpython-312.pyc
new file mode 100644
index 00000000..ec978bdf
Binary files /dev/null and b/doc/第7周代码注释/djangoblog/__pycache__/admin_site.cpython-312.pyc differ
diff --git a/doc/第7周代码注释/djangoblog/__pycache__/apps.cpython-312.pyc b/doc/第7周代码注释/djangoblog/__pycache__/apps.cpython-312.pyc
new file mode 100644
index 00000000..e0827a58
Binary files /dev/null and b/doc/第7周代码注释/djangoblog/__pycache__/apps.cpython-312.pyc differ
diff --git a/doc/第7周代码注释/djangoblog/__pycache__/blog_signals.cpython-312.pyc b/doc/第7周代码注释/djangoblog/__pycache__/blog_signals.cpython-312.pyc
new file mode 100644
index 00000000..f72e9051
Binary files /dev/null and b/doc/第7周代码注释/djangoblog/__pycache__/blog_signals.cpython-312.pyc differ
diff --git a/doc/第7周代码注释/djangoblog/__pycache__/elasticsearch_backend.cpython-312.pyc b/doc/第7周代码注释/djangoblog/__pycache__/elasticsearch_backend.cpython-312.pyc
new file mode 100644
index 00000000..c2b4adcc
Binary files /dev/null and b/doc/第7周代码注释/djangoblog/__pycache__/elasticsearch_backend.cpython-312.pyc differ
diff --git a/doc/第7周代码注释/djangoblog/__pycache__/feeds.cpython-312.pyc b/doc/第7周代码注释/djangoblog/__pycache__/feeds.cpython-312.pyc
new file mode 100644
index 00000000..f99b8fca
Binary files /dev/null and b/doc/第7周代码注释/djangoblog/__pycache__/feeds.cpython-312.pyc differ
diff --git a/doc/第7周代码注释/djangoblog/__pycache__/logentryadmin.cpython-312.pyc b/doc/第7周代码注释/djangoblog/__pycache__/logentryadmin.cpython-312.pyc
new file mode 100644
index 00000000..0402c986
Binary files /dev/null and b/doc/第7周代码注释/djangoblog/__pycache__/logentryadmin.cpython-312.pyc differ
diff --git a/doc/第7周代码注释/djangoblog/__pycache__/settings.cpython-312.pyc b/doc/第7周代码注释/djangoblog/__pycache__/settings.cpython-312.pyc
new file mode 100644
index 00000000..702c393b
Binary files /dev/null and b/doc/第7周代码注释/djangoblog/__pycache__/settings.cpython-312.pyc differ
diff --git a/doc/第7周代码注释/djangoblog/__pycache__/sitemap.cpython-312.pyc b/doc/第7周代码注释/djangoblog/__pycache__/sitemap.cpython-312.pyc
new file mode 100644
index 00000000..1f2ec1de
Binary files /dev/null and b/doc/第7周代码注释/djangoblog/__pycache__/sitemap.cpython-312.pyc differ
diff --git a/doc/第7周代码注释/djangoblog/__pycache__/spider_notify.cpython-312.pyc b/doc/第7周代码注释/djangoblog/__pycache__/spider_notify.cpython-312.pyc
new file mode 100644
index 00000000..b2fd7e88
Binary files /dev/null and b/doc/第7周代码注释/djangoblog/__pycache__/spider_notify.cpython-312.pyc differ
diff --git a/doc/第7周代码注释/djangoblog/__pycache__/urls.cpython-312.pyc b/doc/第7周代码注释/djangoblog/__pycache__/urls.cpython-312.pyc
new file mode 100644
index 00000000..fd5c1607
Binary files /dev/null and b/doc/第7周代码注释/djangoblog/__pycache__/urls.cpython-312.pyc differ
diff --git a/doc/第7周代码注释/djangoblog/__pycache__/utils.cpython-312.pyc b/doc/第7周代码注释/djangoblog/__pycache__/utils.cpython-312.pyc
new file mode 100644
index 00000000..63178cf8
Binary files /dev/null and b/doc/第7周代码注释/djangoblog/__pycache__/utils.cpython-312.pyc differ
diff --git a/doc/第7周代码注释/djangoblog/__pycache__/whoosh_cn_backend.cpython-312.pyc b/doc/第7周代码注释/djangoblog/__pycache__/whoosh_cn_backend.cpython-312.pyc
new file mode 100644
index 00000000..4da1117c
Binary files /dev/null and b/doc/第7周代码注释/djangoblog/__pycache__/whoosh_cn_backend.cpython-312.pyc differ
diff --git a/doc/第7周代码注释/djangoblog/__pycache__/wsgi.cpython-312.pyc b/doc/第7周代码注释/djangoblog/__pycache__/wsgi.cpython-312.pyc
new file mode 100644
index 00000000..8012d133
Binary files /dev/null and b/doc/第7周代码注释/djangoblog/__pycache__/wsgi.cpython-312.pyc differ
diff --git a/doc/第7周代码注释/djangoblog/admin_site.py b/doc/第7周代码注释/djangoblog/admin_site.py
new file mode 100644
index 00000000..bf7c5adb
--- /dev/null
+++ b/doc/第7周代码注释/djangoblog/admin_site.py
@@ -0,0 +1,81 @@
+from django.contrib.admin import AdminSite
+from django.contrib.admin.models import LogEntry
+from django.contrib.sites.admin import SiteAdmin
+from django.contrib.sites.models import Site
+
+# 导入各个应用的admin模块和模型
+from accounts.admin import *
+from blog.admin import *
+from blog.models import *
+from comments.admin import *
+from comments.models import *
+from djangoblog.logentryadmin import LogEntryAdmin
+from oauth.admin import *
+from oauth.models import *
+from owntracks.admin import *
+from owntracks.models import *
+from servermanager.admin import *
+from servermanager.models import *
+
+
+class DjangoBlogAdminSite(AdminSite):
+ """自定义DjangoBlog管理站点"""
+
+ # 管理站点头部标题
+ site_header = 'djangoblog administration'
+ # 管理站点页面标题
+ site_title = 'djangoblog site admin'
+
+ def __init__(self, name='admin'):
+ """初始化管理站点"""
+ super().__init__(name)
+
+ def has_permission(self, request):
+ """检查用户权限:只允许超级用户访问"""
+ return request.user.is_superuser
+
+ # 以下是注释掉的URL配置示例,可用于添加自定义管理页面
+ # def get_urls(self):
+ # urls = super().get_urls()
+ # from django.urls import path
+ # from blog.views import refresh_memcache
+ #
+ # my_urls = [
+ # path('refresh/', self.admin_view(refresh_memcache), name="refresh"),
+ # ]
+ # return urls + my_urls
+
+
+# 创建DjangoBlog管理站点实例
+admin_site = DjangoBlogAdminSite(name='admin')
+
+# 注册博客相关的模型和管理类
+admin_site.register(Article, ArticlelAdmin)
+admin_site.register(Category, CategoryAdmin)
+admin_site.register(Tag, TagAdmin)
+admin_site.register(Links, LinksAdmin)
+admin_site.register(SideBar, SideBarAdmin)
+admin_site.register(BlogSettings, BlogSettingsAdmin)
+
+# 注册服务器管理相关的模型
+admin_site.register(commands, CommandsAdmin)
+admin_site.register(EmailSendLog, EmailSendLogAdmin)
+
+# 注册用户账户模型
+admin_site.register(BlogUser, BlogUserAdmin)
+
+# 注册评论模型
+admin_site.register(Comment, CommentAdmin)
+
+# 注册OAuth认证相关模型
+admin_site.register(OAuthUser, OAuthUserAdmin)
+admin_site.register(OAuthConfig, OAuthConfigAdmin)
+
+# 注册OwnTracks位置跟踪模型
+admin_site.register(OwnTrackLog, OwnTrackLogsAdmin)
+
+# 注册Django站点模型
+admin_site.register(Site, SiteAdmin)
+
+# 注册Django日志条目模型
+admin_site.register(LogEntry, LogEntryAdmin)
\ No newline at end of file
diff --git a/doc/第7周代码注释/djangoblog/apps.py b/doc/第7周代码注释/djangoblog/apps.py
new file mode 100644
index 00000000..f2f0771f
--- /dev/null
+++ b/doc/第7周代码注释/djangoblog/apps.py
@@ -0,0 +1,20 @@
+from django.apps import AppConfig
+
+
+class DjangoblogAppConfig(AppConfig):
+ """Djangoblog应用的配置类"""
+
+ # 设置默认的自动主键字段类型为BigAutoField(64位自增整数)
+ default_auto_field = 'django.db.models.BigAutoField'
+ # 指定应用名称
+ name = 'djangoblog'
+
+ def ready(self):
+ """应用准备就绪时执行的方法"""
+ # 调用父类的ready方法
+ super().ready()
+
+ # 导入并加载插件
+ # 这里在应用启动时自动加载所有插件
+ from .plugin_manage.loader import load_plugins
+ load_plugins()
\ No newline at end of file
diff --git a/doc/第7周代码注释/djangoblog/blog_signals.py b/doc/第7周代码注释/djangoblog/blog_signals.py
new file mode 100644
index 00000000..56b1b699
--- /dev/null
+++ b/doc/第7周代码注释/djangoblog/blog_signals.py
@@ -0,0 +1,162 @@
+import _thread
+import logging
+
+import django.dispatch
+from django.conf import settings
+from django.contrib.admin.models import LogEntry
+from django.contrib.auth.signals import user_logged_in, user_logged_out
+from django.core.mail import EmailMultiAlternatives
+from django.db.models.signals import post_save
+from django.dispatch import receiver
+
+# 导入自定义模型和工具函数
+from comments.models import Comment
+from comments.utils import send_comment_email
+from djangoblog.spider_notify import SpiderNotify
+from djangoblog.utils import cache, expire_view_cache, delete_sidebar_cache, delete_view_cache
+from djangoblog.utils import get_current_site
+from oauth.models import OAuthUser
+
+# 获取logger实例
+logger = logging.getLogger(__name__)
+
+# 定义自定义信号
+# OAuth用户登录信号,传递用户id
+oauth_user_login_signal = django.dispatch.Signal(['id'])
+# 发送邮件信号,传递收件人、标题和内容
+send_email_signal = django.dispatch.Signal(
+ ['emailto', 'title', 'content'])
+
+
+@receiver(send_email_signal)
+def send_email_signal_handler(sender, **kwargs):
+ """发送邮件信号处理器"""
+ # 从信号参数中获取邮件相关信息
+ emailto = kwargs['emailto']
+ title = kwargs['title']
+ content = kwargs['content']
+
+ # 创建邮件消息对象
+ msg = EmailMultiAlternatives(
+ title,
+ content,
+ from_email=settings.DEFAULT_FROM_EMAIL,
+ to=emailto)
+ msg.content_subtype = "html" # 设置邮件内容类型为HTML
+
+ # 记录邮件发送日志
+ from servermanager.models import EmailSendLog
+ log = EmailSendLog()
+ log.title = title
+ log.content = content
+ log.emailto = ','.join(emailto)
+
+ try:
+ # 尝试发送邮件
+ result = msg.send()
+ log.send_result = result > 0 # 发送成功结果为True
+ except Exception as e:
+ # 记录发送失败日志
+ logger.error(f"失败邮箱号: {emailto}, {e}")
+ log.send_result = False
+ log.save() # 保存邮件发送日志
+
+
+@receiver(oauth_user_login_signal)
+def oauth_user_login_signal_handler(sender, **kwargs):
+ """OAuth用户登录信号处理器"""
+ id = kwargs['id']
+ oauthuser = OAuthUser.objects.get(id=id)
+ site = get_current_site().domain
+
+ # 如果用户头像不在当前站点域名下,则保存头像到本地
+ if oauthuser.picture and not oauthuser.picture.find(site) >= 0:
+ from djangoblog.utils import save_user_avatar
+ oauthuser.picture = save_user_avatar(oauthuser.picture)
+ oauthuser.save()
+
+ # 删除侧边栏缓存
+ delete_sidebar_cache()
+
+
+@receiver(post_save)
+def model_post_save_callback(
+ sender,
+ instance,
+ created,
+ raw,
+ using,
+ update_fields,
+ **kwargs):
+ """模型保存后的通用回调函数"""
+ clearcache = False
+
+ # 如果是LogEntry(Django管理员日志),直接返回
+ if isinstance(instance, LogEntry):
+ return
+
+ # 检查实例是否有get_full_url方法(通常是有URL的模型)
+ if 'get_full_url' in dir(instance):
+ # 判断是否只是更新浏览量
+ is_update_views = update_fields == {'views'}
+
+ # 如果不是测试环境且不是更新浏览量,则通知搜索引擎
+ if not settings.TESTING and not is_update_views:
+ try:
+ notify_url = instance.get_full_url()
+ SpiderNotify.baidu_notify([notify_url]) # 通知百度搜索引擎
+ except Exception as ex:
+ logger.error("notify sipder", ex)
+
+ # 如果不是更新浏览量,设置清除缓存标志
+ if not is_update_views:
+ clearcache = True
+
+ # 如果是评论模型
+ if isinstance(instance, Comment):
+ # 如果评论是启用的
+ if instance.is_enable:
+ path = instance.article.get_absolute_url()
+ site = get_current_site().domain
+
+ # 处理站点域名(移除端口号)
+ if site.find(':') > 0:
+ site = site[0:site.find(':')]
+
+ # 使文章详情页缓存过期
+ expire_view_cache(
+ path,
+ servername=site,
+ serverport=80,
+ key_prefix='blogdetail')
+
+ # 删除SEO处理器缓存
+ if cache.get('seo_processor'):
+ cache.delete('seo_processor')
+
+ # 删除文章评论缓存
+ comment_cache_key = 'article_comments_{id}'.format(
+ id=instance.article.id)
+ cache.delete(comment_cache_key)
+
+ # 删除侧边栏缓存
+ delete_sidebar_cache()
+ # 删除文章评论视图缓存
+ delete_view_cache('article_comments', [str(instance.article.pk)])
+
+ # 在新线程中发送评论通知邮件
+ _thread.start_new_thread(send_comment_email, (instance,))
+
+ # 如果需要清除缓存
+ if clearcache:
+ cache.clear() # 清除所有缓存
+
+
+@receiver(user_logged_in)
+@receiver(user_logged_out)
+def user_auth_callback(sender, request, user, **kwargs):
+ """用户登录/登出信号处理器"""
+ if user and user.username:
+ logger.info(user) # 记录用户信息
+ delete_sidebar_cache() # 删除侧边栏缓存
+ # cache.clear() # 注释掉的清除所有缓存代码
\ No newline at end of file
diff --git a/doc/第7周代码注释/djangoblog/elasticsearch_backend.py b/doc/第7周代码注释/djangoblog/elasticsearch_backend.py
new file mode 100644
index 00000000..293d3e24
--- /dev/null
+++ b/doc/第7周代码注释/djangoblog/elasticsearch_backend.py
@@ -0,0 +1,216 @@
+from django.utils.encoding import force_str
+from elasticsearch_dsl import Q
+from haystack.backends import BaseEngine, BaseSearchBackend, BaseSearchQuery, log_query
+from haystack.forms import ModelSearchForm
+from haystack.models import SearchResult
+from haystack.utils import log as logging
+
+# 导入自定义的Elasticsearch文档和模型
+from blog.documents import ArticleDocument, ArticleDocumentManager
+from blog.models import Article
+
+# 获取日志记录器
+logger = logging.getLogger(__name__)
+
+
+class ElasticSearchBackend(BaseSearchBackend):
+ """Elasticsearch搜索后端实现"""
+
+ def __init__(self, connection_alias, **connection_options):
+ """初始化Elasticsearch后端"""
+ super(
+ ElasticSearchBackend,
+ self).__init__(
+ connection_alias,
+ **connection_options)
+ self.manager = ArticleDocumentManager() # 文章文档管理器
+ self.include_spelling = True # 是否包含拼写建议
+
+ def _get_models(self, iterable):
+ """获取模型实例并转换为文档"""
+ # 如果有提供模型列表则使用,否则获取所有文章
+ models = iterable if iterable and iterable[0] else Article.objects.all()
+ docs = self.manager.convert_to_doc(models) # 将模型转换为Elasticsearch文档
+ return docs
+
+ def _create(self, models):
+ """创建索引并重建文档"""
+ self.manager.create_index() # 创建Elasticsearch索引
+ docs = self._get_models(models)
+ self.manager.rebuild(docs) # 重建所有文档
+
+ def _delete(self, models):
+ """删除文档"""
+ for m in models:
+ m.delete()
+ return True
+
+ def _rebuild(self, models):
+ """重建索引文档"""
+ models = models if models else Article.objects.all()
+ docs = self.manager.convert_to_doc(models)
+ self.manager.update_docs(docs) # 更新文档
+
+ def update(self, index, iterable, commit=True):
+ """更新文档"""
+ models = self._get_models(iterable)
+ self.manager.update_docs(models)
+
+ def remove(self, obj_or_string):
+ """移除指定对象"""
+ models = self._get_models([obj_or_string])
+ self._delete(models)
+
+ def clear(self, models=None, commit=True):
+ """清空索引"""
+ self.remove(None)
+
+ @staticmethod
+ def get_suggestion(query: str) -> str:
+ """获取搜索建议词,如果没有找到建议词则返回原搜索词"""
+
+ # 构建搜索建议查询
+ search = ArticleDocument.search() \
+ .query("match", body=query) \
+ .suggest('suggest_search', query, term={'field': 'body'}) \
+ .execute()
+
+ keywords = []
+ # 处理建议结果
+ for suggest in search.suggest.suggest_search:
+ if suggest["options"]:
+ keywords.append(suggest["options"][0]["text"]) # 使用建议词
+ else:
+ keywords.append(suggest["text"]) # 使用原词
+
+ return ' '.join(keywords)
+
+ @log_query # 记录查询日志的装饰器
+ def search(self, query_string, **kwargs):
+ """执行搜索查询"""
+ logger.info('search query_string:' + query_string)
+
+ # 获取分页参数
+ start_offset = kwargs.get('start_offset')
+ end_offset = kwargs.get('end_offset')
+
+ # 推荐词搜索:如果启用建议,则获取建议词
+ if getattr(self, "is_suggest", None):
+ suggestion = self.get_suggestion(query_string)
+ else:
+ suggestion = query_string
+
+ # 构建搜索查询:在标题和正文中匹配,设置最小匹配度70%
+ q = Q('bool',
+ should=[Q('match', body=suggestion), Q('match', title=suggestion)],
+ minimum_should_match="70%")
+
+ # 执行搜索:过滤已发布的状态为p且类型为a的文章
+ search = ArticleDocument.search() \
+ .query('bool', filter=[q]) \
+ .filter('term', status='p') \
+ .filter('term', type='a') \
+ .source(False)[start_offset: end_offset] # 不返回源文档内容,只返回元数据
+
+ results = search.execute()
+ hits = results['hits'].total # 总命中数
+ raw_results = []
+
+ # 处理搜索结果
+ for raw_result in results['hits']['hits']:
+ app_label = 'blog'
+ model_name = 'Article'
+ additional_fields = {}
+
+ result_class = SearchResult
+
+ # 创建搜索结果对象
+ result = result_class(
+ app_label,
+ model_name,
+ raw_result['_id'], # 文档ID
+ raw_result['_score'], # 相关性分数
+ **additional_fields)
+ raw_results.append(result)
+
+ facets = {}
+ # 如果查询词与建议词不同,则设置拼写建议
+ spelling_suggestion = None if query_string == suggestion else suggestion
+
+ return {
+ 'results': raw_results, # 搜索结果列表
+ 'hits': hits, # 总命中数
+ 'facets': facets, # 分面搜索数据
+ 'spelling_suggestion': spelling_suggestion, # 拼写建议
+ }
+
+
+class ElasticSearchQuery(BaseSearchQuery):
+ """Elasticsearch查询构建器"""
+
+ def _convert_datetime(self, date):
+ """转换日期时间格式"""
+ if hasattr(date, 'hour'):
+ return force_str(date.strftime('%Y%m%d%H%M%S')) # 包含时间的格式
+ else:
+ return force_str(date.strftime('%Y%m%d000000')) # 只包含日期的格式
+
+ def clean(self, query_fragment):
+ """
+ 清理用户输入的查询片段,转义保留字符
+
+ Whoosh 1.X与此不同,不再使用反斜杠转义保留字符,
+ 而是应该引用整个单词。
+ """
+ words = query_fragment.split()
+ cleaned_words = []
+
+ for word in words:
+ # 处理保留字
+ if word in self.backend.RESERVED_WORDS:
+ word = word.replace(word, word.lower())
+
+ # 处理保留字符
+ for char in self.backend.RESERVED_CHARACTERS:
+ if char in word:
+ word = "'%s'" % word # 用引号包围包含保留字符的单词
+ break
+
+ cleaned_words.append(word)
+
+ return ' '.join(cleaned_words)
+
+ def build_query_fragment(self, field, filter_type, value):
+ """构建查询片段"""
+ return value.query_string
+
+ def get_count(self):
+ """获取搜索结果数量"""
+ results = self.get_results()
+ return len(results) if results else 0
+
+ def get_spelling_suggestion(self, preferred_query=None):
+ """获取拼写建议"""
+ return self._spelling_suggestion
+
+ def build_params(self, spelling_query=None):
+ """构建查询参数"""
+ kwargs = super(ElasticSearchQuery, self).build_params(spelling_query=spelling_query)
+ return kwargs
+
+
+class ElasticSearchModelSearchForm(ModelSearchForm):
+ """Elasticsearch模型搜索表单"""
+
+ def search(self):
+ """执行搜索,根据参数决定是否使用建议搜索"""
+ # 是否建议搜索:从请求数据中获取is_suggest参数
+ self.searchqueryset.query.backend.is_suggest = self.data.get("is_suggest") != "no"
+ sqs = super().search() # 调用父类搜索方法
+ return sqs
+
+
+class ElasticSearchEngine(BaseEngine):
+ """Elasticsearch搜索引擎"""
+ backend = ElasticSearchBackend # 指定后端类
+ query = ElasticSearchQuery # 指定查询类
\ No newline at end of file
diff --git a/doc/第7周代码注释/djangoblog/feeds.py b/doc/第7周代码注释/djangoblog/feeds.py
new file mode 100644
index 00000000..422f2b7f
--- /dev/null
+++ b/doc/第7周代码注释/djangoblog/feeds.py
@@ -0,0 +1,58 @@
+from django.contrib.auth import get_user_model
+from django.contrib.syndication.views import Feed
+from django.utils import timezone
+from django.utils.feedgenerator import Rss201rev2Feed
+
+# 导入自定义模型和工具
+from blog.models import Article
+from djangoblog.utils import CommonMarkdown
+
+
+class DjangoBlogFeed(Feed):
+ """DjangoBlog的RSS订阅源类"""
+
+ # 指定Feed类型为RSS 2.0
+ feed_type = Rss201rev2Feed
+
+ # Feed的描述信息
+ description = '大巧无工,重剑无锋.'
+ # Feed的标题
+ title = "且听风吟 大巧无工,重剑无锋. "
+ # Feed的链接地址
+ link = "/feed/"
+
+ def author_name(self):
+ """获取作者名称 - 返回第一个用户的昵称"""
+ return get_user_model().objects.first().nickname
+
+ def author_link(self):
+ """获取作者链接 - 返回第一个用户的绝对URL"""
+ return get_user_model().objects.first().get_absolute_url()
+
+ def items(self):
+ """获取要在Feed中显示的项目列表"""
+ # 返回最近发布的5篇文章,过滤条件:类型为'article'且状态为'published'
+ return Article.objects.filter(type='a', status='p').order_by('-pub_time')[:5]
+
+ def item_title(self, item):
+ """获取单个项目的标题"""
+ return item.title
+
+ def item_description(self, item):
+ """获取单个项目的描述 - 将Markdown内容转换为HTML"""
+ return CommonMarkdown.get_markdown(item.body)
+
+ def feed_copyright(self):
+ """获取Feed的版权信息"""
+ now = timezone.now()
+ return "Copyright© {year} 且听风吟".format(year=now.year)
+
+ def item_link(self, item):
+ """获取单个项目的链接"""
+ return item.get_absolute_url()
+
+ def item_guid(self, item):
+ """获取单个项目的全局唯一标识符(当前未实现)"""
+ # 注意:这个方法目前没有返回值,可能需要根据需求实现
+ # 通常应该返回一个唯一标识项目的字符串,如文章的ID或永久链接
+ pass
\ No newline at end of file
diff --git a/doc/第7周代码注释/djangoblog/logentryadmin.py b/doc/第7周代码注释/djangoblog/logentryadmin.py
new file mode 100644
index 00000000..2be7a80f
--- /dev/null
+++ b/doc/第7周代码注释/djangoblog/logentryadmin.py
@@ -0,0 +1,113 @@
+from django.contrib import admin
+from django.contrib.admin.models import DELETION
+from django.contrib.contenttypes.models import ContentType
+from django.urls import reverse, NoReverseMatch
+from django.utils.encoding import force_str
+from django.utils.html import escape
+from django.utils.safestring import mark_safe
+from django.utils.translation import gettext_lazy as _
+
+
+class LogEntryAdmin(admin.ModelAdmin):
+ """Django管理员日志条目的自定义管理界面"""
+
+ # 列表页过滤器配置:按内容类型过滤
+ list_filter = [
+ 'content_type'
+ ]
+
+ # 搜索字段配置:可按对象表示和变更消息搜索
+ search_fields = [
+ 'object_repr',
+ 'change_message'
+ ]
+
+ # 列表页中可点击的链接字段
+ list_display_links = [
+ 'action_time',
+ 'get_change_message',
+ ]
+
+ # 列表页显示的字段
+ list_display = [
+ 'action_time', # 操作时间
+ 'user_link', # 用户链接(自定义)
+ 'content_type', # 内容类型
+ 'object_link', # 对象链接(自定义)
+ 'get_change_message', # 变更消息
+ ]
+
+ def has_add_permission(self, request):
+ """禁止添加新的日志条目"""
+ return False
+
+ def has_change_permission(self, request, obj=None):
+ """修改权限控制:只允许超级用户或具有特定权限的用户查看(不允许POST修改)"""
+ return (
+ request.user.is_superuser or
+ request.user.has_perm('admin.change_logentry')
+ ) and request.method != 'POST'
+
+ def has_delete_permission(self, request, obj=None):
+ """禁止删除日志条目"""
+ return False
+
+ def object_link(self, obj):
+ """生成对象链接的显示"""
+ object_link = escape(obj.object_repr) # 转义对象表示字符串
+ content_type = obj.content_type
+
+ # 如果不是删除操作且内容类型存在,尝试生成可点击的链接
+ if obj.action_flag != DELETION and content_type is not None:
+ try:
+ # 构建管理员修改页面的URL
+ url = reverse(
+ 'admin:{}_{}_change'.format(content_type.app_label,
+ content_type.model),
+ args=[obj.object_id]
+ )
+ # 创建HTML链接
+ object_link = '{} '.format(url, object_link)
+ except NoReverseMatch:
+ # 如果无法生成URL,保持原样
+ pass
+ return mark_safe(object_link) # 标记为安全HTML
+
+ # 设置对象链接字段的排序和显示名称
+ object_link.admin_order_field = 'object_repr'
+ object_link.short_description = _('object')
+
+ def user_link(self, obj):
+ """生成用户链接的显示"""
+ content_type = ContentType.objects.get_for_model(type(obj.user))
+ user_link = escape(force_str(obj.user)) # 转义用户字符串
+
+ try:
+ # 构建用户修改页面的URL
+ url = reverse(
+ 'admin:{}_{}_change'.format(content_type.app_label,
+ content_type.model),
+ args=[obj.user.pk]
+ )
+ # 创建HTML链接
+ user_link = '{} '.format(url, user_link)
+ except NoReverseMatch:
+ # 如果无法生成URL,保持原样
+ pass
+ return mark_safe(user_link) # 标记为安全HTML
+
+ # 设置用户链接字段的排序和显示名称
+ user_link.admin_order_field = 'user'
+ user_link.short_description = _('user')
+
+ def get_queryset(self, request):
+ """获取查询集,预取content_type关系以提高性能"""
+ queryset = super(LogEntryAdmin, self).get_queryset(request)
+ return queryset.prefetch_related('content_type')
+
+ def get_actions(self, request):
+ """获取可用的批量操作,移除删除选中操作"""
+ actions = super(LogEntryAdmin, self).get_actions(request)
+ if 'delete_selected' in actions:
+ del actions['delete_selected'] # 移除批量删除选项
+ return actions
\ No newline at end of file
diff --git a/doc/第7周代码注释/djangoblog/plugin_manage/__pycache__/base_plugin.cpython-312.pyc b/doc/第7周代码注释/djangoblog/plugin_manage/__pycache__/base_plugin.cpython-312.pyc
new file mode 100644
index 00000000..7d4a31ad
Binary files /dev/null and b/doc/第7周代码注释/djangoblog/plugin_manage/__pycache__/base_plugin.cpython-312.pyc differ
diff --git a/doc/第7周代码注释/djangoblog/plugin_manage/__pycache__/hook_constants.cpython-312.pyc b/doc/第7周代码注释/djangoblog/plugin_manage/__pycache__/hook_constants.cpython-312.pyc
new file mode 100644
index 00000000..2cb78751
Binary files /dev/null and b/doc/第7周代码注释/djangoblog/plugin_manage/__pycache__/hook_constants.cpython-312.pyc differ
diff --git a/doc/第7周代码注释/djangoblog/plugin_manage/__pycache__/hooks.cpython-312.pyc b/doc/第7周代码注释/djangoblog/plugin_manage/__pycache__/hooks.cpython-312.pyc
new file mode 100644
index 00000000..a5f5e359
Binary files /dev/null and b/doc/第7周代码注释/djangoblog/plugin_manage/__pycache__/hooks.cpython-312.pyc differ
diff --git a/doc/第7周代码注释/djangoblog/plugin_manage/__pycache__/loader.cpython-312.pyc b/doc/第7周代码注释/djangoblog/plugin_manage/__pycache__/loader.cpython-312.pyc
new file mode 100644
index 00000000..fbe5eaf6
Binary files /dev/null and b/doc/第7周代码注释/djangoblog/plugin_manage/__pycache__/loader.cpython-312.pyc differ
diff --git a/doc/第7周代码注释/djangoblog/plugin_manage/base_plugin.py b/doc/第7周代码注释/djangoblog/plugin_manage/base_plugin.py
new file mode 100644
index 00000000..2b4be5cb
--- /dev/null
+++ b/doc/第7周代码注释/djangoblog/plugin_manage/base_plugin.py
@@ -0,0 +1,41 @@
+import logging
+
+logger = logging.getLogger(__name__)
+
+
+class BasePlugin:
+ # 插件元数据
+ PLUGIN_NAME = None
+ PLUGIN_DESCRIPTION = None
+ PLUGIN_VERSION = None
+
+ def __init__(self):
+ if not all([self.PLUGIN_NAME, self.PLUGIN_DESCRIPTION, self.PLUGIN_VERSION]):
+ raise ValueError("Plugin metadata (PLUGIN_NAME, PLUGIN_DESCRIPTION, PLUGIN_VERSION) must be defined.")
+ self.init_plugin()
+ self.register_hooks()
+
+ def init_plugin(self):
+ """
+ 插件初始化逻辑
+ 子类可以重写此方法来实现特定的初始化操作
+ """
+ logger.info(f'{self.PLUGIN_NAME} initialized.')
+
+ def register_hooks(self):
+ """
+ 注册插件钩子
+ 子类可以重写此方法来注册特定的钩子
+ """
+ pass
+
+ def get_plugin_info(self):
+ """
+ 获取插件信息
+ :return: 包含插件元数据的字典
+ """
+ return {
+ 'name': self.PLUGIN_NAME,
+ 'description': self.PLUGIN_DESCRIPTION,
+ 'version': self.PLUGIN_VERSION
+ }
diff --git a/doc/第7周代码注释/djangoblog/plugin_manage/hook_constants.py b/doc/第7周代码注释/djangoblog/plugin_manage/hook_constants.py
new file mode 100644
index 00000000..6685b7ce
--- /dev/null
+++ b/doc/第7周代码注释/djangoblog/plugin_manage/hook_constants.py
@@ -0,0 +1,7 @@
+ARTICLE_DETAIL_LOAD = 'article_detail_load'
+ARTICLE_CREATE = 'article_create'
+ARTICLE_UPDATE = 'article_update'
+ARTICLE_DELETE = 'article_delete'
+
+ARTICLE_CONTENT_HOOK_NAME = "the_content"
+
diff --git a/doc/第7周代码注释/djangoblog/plugin_manage/hooks.py b/doc/第7周代码注释/djangoblog/plugin_manage/hooks.py
new file mode 100644
index 00000000..d7125402
--- /dev/null
+++ b/doc/第7周代码注释/djangoblog/plugin_manage/hooks.py
@@ -0,0 +1,44 @@
+import logging
+
+logger = logging.getLogger(__name__)
+
+_hooks = {}
+
+
+def register(hook_name: str, callback: callable):
+ """
+ 注册一个钩子回调。
+ """
+ if hook_name not in _hooks:
+ _hooks[hook_name] = []
+ _hooks[hook_name].append(callback)
+ logger.debug(f"Registered hook '{hook_name}' with callback '{callback.__name__}'")
+
+
+def run_action(hook_name: str, *args, **kwargs):
+ """
+ 执行一个 Action Hook。
+ 它会按顺序执行所有注册到该钩子上的回调函数。
+ """
+ if hook_name in _hooks:
+ logger.debug(f"Running action hook '{hook_name}'")
+ for callback in _hooks[hook_name]:
+ try:
+ callback(*args, **kwargs)
+ except Exception as e:
+ logger.error(f"Error running action hook '{hook_name}' callback '{callback.__name__}': {e}", exc_info=True)
+
+
+def apply_filters(hook_name: str, value, *args, **kwargs):
+ """
+ 执行一个 Filter Hook。
+ 它会把 value 依次传递给所有注册的回调函数进行处理。
+ """
+ if hook_name in _hooks:
+ logger.debug(f"Applying filter hook '{hook_name}'")
+ for callback in _hooks[hook_name]:
+ try:
+ value = callback(value, *args, **kwargs)
+ except Exception as e:
+ logger.error(f"Error applying filter hook '{hook_name}' callback '{callback.__name__}': {e}", exc_info=True)
+ return value
diff --git a/doc/第7周代码注释/djangoblog/plugin_manage/loader.py b/doc/第7周代码注释/djangoblog/plugin_manage/loader.py
new file mode 100644
index 00000000..12e824ba
--- /dev/null
+++ b/doc/第7周代码注释/djangoblog/plugin_manage/loader.py
@@ -0,0 +1,19 @@
+import os
+import logging
+from django.conf import settings
+
+logger = logging.getLogger(__name__)
+
+def load_plugins():
+ """
+ Dynamically loads and initializes plugins from the 'plugins' directory.
+ This function is intended to be called when the Django app registry is ready.
+ """
+ for plugin_name in settings.ACTIVE_PLUGINS:
+ plugin_path = os.path.join(settings.PLUGINS_DIR, plugin_name)
+ if os.path.isdir(plugin_path) and os.path.exists(os.path.join(plugin_path, 'plugin.py')):
+ try:
+ __import__(f'plugins.{plugin_name}.plugin')
+ logger.info(f"Successfully loaded plugin: {plugin_name}")
+ except ImportError as e:
+ logger.error(f"Failed to import plugin: {plugin_name}", exc_info=e)
\ No newline at end of file
diff --git a/doc/第7周代码注释/djangoblog/settings.py b/doc/第7周代码注释/djangoblog/settings.py
new file mode 100644
index 00000000..c152ac8e
--- /dev/null
+++ b/doc/第7周代码注释/djangoblog/settings.py
@@ -0,0 +1,345 @@
+"""
+Django settings for djangoblog project.
+
+Generated by 'django-admin startproject' using Django 1.10.2.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/1.10/topics/settings/
+
+For the full list of settings and their values, see
+https://docs.djangoproject.com/en/1.10/ref/settings/
+"""
+import os
+import sys
+from pathlib import Path
+
+from django.utils.translation import gettext_lazy as _
+
+
+def env_to_bool(env, default):
+ """将环境变量转换为布尔值"""
+ str_val = os.environ.get(env)
+ return default if str_val is None else str_val == 'True'
+
+
+# 构建项目内部路径:BASE_DIR / 'subdir'
+BASE_DIR = Path(__file__).resolve().parent.parent
+
+# 快速开发配置 - 不适用于生产环境
+# 参见 https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/
+
+# 安全警告:在生产环境中保持密钥保密!
+SECRET_KEY = os.environ.get(
+ 'DJANGO_SECRET_KEY') or 'n9ceqv38)#&mwuat@(mjb_p%em$e8$qyr#fw9ot!=ba6lijx-6'
+# 安全警告:在生产环境中不要开启调试模式!
+DEBUG = env_to_bool('DJANGO_DEBUG', True)
+# DEBUG = False
+TESTING = len(sys.argv) > 1 and sys.argv[1] == 'test' # 检测是否在测试模式
+
+# 允许的主机名
+# ALLOWED_HOSTS = []
+ALLOWED_HOSTS = ['*', '127.0.0.1', 'example.com']
+# Django 4.0新增配置:受信任的CSRF来源
+CSRF_TRUSTED_ORIGINS = ['http://example.com']
+
+# 应用定义
+INSTALLED_APPS = [
+ # 'django.contrib.admin',
+ 'django.contrib.admin.apps.SimpleAdminConfig', # 使用简化的管理员配置
+ 'django.contrib.auth', # 认证系统
+ 'django.contrib.contenttypes', # 内容类型框架
+ 'django.contrib.sessions', # 会话框架
+ 'django.contrib.messages', # 消息框架
+ 'django.contrib.staticfiles', # 静态文件管理
+ 'django.contrib.sites', # 站点框架
+ 'django.contrib.sitemaps', # 站点地图
+ 'mdeditor', # Markdown编辑器
+ 'haystack', # 搜索框架
+ 'blog', # 博客应用
+ 'accounts', # 账户应用
+ 'comments', # 评论应用
+ 'oauth', # OAuth认证
+ 'servermanager', # 服务器管理
+ 'owntracks', # 位置跟踪
+ 'compressor', # 静态文件压缩
+ 'djangoblog' # 主应用
+]
+
+# 中间件配置
+MIDDLEWARE = [
+ 'django.middleware.security.SecurityMiddleware', # 安全中间件
+ 'django.contrib.sessions.middleware.SessionMiddleware', # 会话中间件
+ 'django.middleware.locale.LocaleMiddleware', # 国际化中间件
+ 'django.middleware.gzip.GZipMiddleware', # Gzip压缩
+ # 'django.middleware.cache.UpdateCacheMiddleware', # 缓存更新(注释)
+ 'django.middleware.common.CommonMiddleware', # 通用中间件
+ # 'django.middleware.cache.FetchFromCacheMiddleware', # 缓存获取(注释)
+ 'django.middleware.csrf.CsrfViewMiddleware', # CSRF保护
+ 'django.contrib.auth.middleware.AuthenticationMiddleware', # 认证中间件
+ 'django.contrib.messages.middleware.MessageMiddleware', # 消息中间件
+ 'django.middleware.clickjacking.XFrameOptionsMiddleware', # 点击劫持保护
+ 'django.middleware.http.ConditionalGetMiddleware', # 条件GET请求
+ 'blog.middleware.OnlineMiddleware' # 自定义在线用户中间件
+]
+
+# 根URL配置
+ROOT_URLCONF = 'djangoblog.urls'
+
+# 模板配置
+TEMPLATES = [
+ {
+ 'BACKEND': 'django.template.backends.django.DjangoTemplates', # Django模板引擎
+ 'DIRS': [os.path.join(BASE_DIR, 'templates')], # 模板目录
+ 'APP_DIRS': True, # 启用应用模板目录
+ 'OPTIONS': {
+ 'context_processors': [ # 上下文处理器
+ 'django.template.context_processors.debug', # 调试信息
+ 'django.template.context_processors.request', # 请求对象
+ 'django.contrib.auth.context_processors.auth', # 认证信息
+ 'django.contrib.messages.context_processors.messages', # 消息框架
+ 'blog.context_processors.seo_processor' # 自定义SEO处理器
+ ],
+ },
+ },
+]
+
+# WSGI应用配置
+WSGI_APPLICATION = 'djangoblog.wsgi.application'
+
+# 数据库配置
+# https://docs.djangoproject.com/en/1.10/ref/settings/#databases
+DATABASES = {
+ 'default': {
+ 'ENGINE': 'django.db.backends.mysql', # MySQL数据库引擎
+ 'NAME': os.environ.get('DJANGO_MYSQL_DATABASE') or 'djangoblog', # 数据库名
+ 'USER': os.environ.get('DJANGO_MYSQL_USER') or 'django_user', # 用户名
+ 'PASSWORD': os.environ.get('DJANGO_MYSQL_PASSWORD') or 'wzm216921', # 密码
+ 'HOST': os.environ.get('DJANGO_MYSQL_HOST') or '127.0.0.1', # 主机
+ 'PORT': int(
+ os.environ.get('DJANGO_MYSQL_PORT') or 3306), # 端口
+ 'OPTIONS': {
+ 'charset': 'utf8mb4'}, # 字符集配置
+ }}
+
+# 密码验证
+# https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators
+AUTH_PASSWORD_VALIDATORS = [
+ {
+ 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', # 用户属性相似性验证
+ },
+ {
+ 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', # 最小长度验证
+ },
+ {
+ 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', # 常见密码验证
+ },
+ {
+ 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', # 数字密码验证
+ },
+]
+
+# 国际化配置
+LANGUAGES = (
+ ('en', _('English')), # 英语
+ ('zh-hans', _('Simplified Chinese')), # 简体中文
+ ('zh-hant', _('Traditional Chinese')), # 繁体中文
+)
+LOCALE_PATHS = (
+ os.path.join(BASE_DIR, 'locale'), # 本地化文件路径
+)
+
+LANGUAGE_CODE = 'zh-hans' # 默认语言
+TIME_ZONE = 'Asia/Shanghai' # 时区
+USE_I18N = True # 启用国际化
+USE_L10N = True # 启用本地化
+USE_TZ = False # 不使用时区支持
+
+# 静态文件 (CSS, JavaScript, Images)
+# https://docs.djangoproject.com/en/1.10/howto/static-files/
+
+# Haystack搜索配置
+HAYSTACK_CONNECTIONS = {
+ 'default': {
+ 'ENGINE': 'djangoblog.whoosh_cn_backend.WhooshEngine', # Whoosh搜索引擎
+ 'PATH': os.path.join(os.path.dirname(__file__), 'whoosh_index'), # 索引路径
+ },
+}
+# 自动更新搜索索引
+HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor'
+# 允许用户使用邮箱或用户名登录
+AUTHENTICATION_BACKENDS = [
+ 'accounts.user_login_backend.EmailOrUsernameModelBackend']
+
+STATIC_ROOT = os.path.join(BASE_DIR, 'collectedstatic') # 静态文件收集目录
+STATIC_URL = '/static/' # 静态文件URL
+STATICFILES = os.path.join(BASE_DIR, 'static') # 静态文件目录
+
+AUTH_USER_MODEL = 'accounts.BlogUser' # 自定义用户模型
+LOGIN_URL = '/login/' # 登录URL
+
+TIME_FORMAT = '%Y-%m-%d %H:%M:%S' # 时间格式
+DATE_TIME_FORMAT = '%Y-%m-%d' # 日期格式
+
+# bootstrap颜色样式
+BOOTSTRAP_COLOR_TYPES = [
+ 'default', 'primary', 'success', 'info', 'warning', 'danger'
+]
+
+# 分页设置
+PAGINATE_BY = 10
+# HTTP缓存超时时间
+CACHE_CONTROL_MAX_AGE = 2592000
+# 缓存设置
+CACHES = {
+ 'default': {
+ 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', # 本地内存缓存
+ 'TIMEOUT': 10800, # 缓存超时时间(3小时)
+ 'LOCATION': 'unique-snowflake', # 缓存位置标识
+ }
+}
+# 使用redis作为缓存
+if os.environ.get("DJANGO_REDIS_URL"):
+ CACHES = {
+ 'default': {
+ 'BACKEND': 'django.core.cache.backends.redis.RedisCache', # Redis缓存后端
+ 'LOCATION': f'redis://{os.environ.get("DJANGO_REDIS_URL")}', # Redis连接URL
+ }
+ }
+
+SITE_ID = 1 # 站点ID
+# 百度站长平台通知URL
+BAIDU_NOTIFY_URL = os.environ.get('DJANGO_BAIDU_NOTIFY_URL') \
+ or 'http://data.zz.baidu.com/urls?site=https://www.lylinux.net&token=1uAOGrMsUm5syDGn'
+
+# 邮箱配置:
+EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' # SMTP后端
+EMAIL_USE_TLS = env_to_bool('DJANGO_EMAIL_TLS', False) # 是否使用TLS
+EMAIL_USE_SSL = env_to_bool('DJANGO_EMAIL_SSL', True) # 是否使用SSL
+EMAIL_HOST = os.environ.get('DJANGO_EMAIL_HOST') or 'smtp.mxhichina.com' # SMTP主机
+EMAIL_PORT = int(os.environ.get('DJANGO_EMAIL_PORT') or 465) # SMTP端口
+EMAIL_HOST_USER = os.environ.get('DJANGO_EMAIL_USER') # 邮箱用户
+EMAIL_HOST_PASSWORD = os.environ.get('DJANGO_EMAIL_PASSWORD') # 邮箱密码
+DEFAULT_FROM_EMAIL = EMAIL_HOST_USER # 默认发件人
+SERVER_EMAIL = EMAIL_HOST_USER # 服务器邮箱
+# 设置debug=false不会处理异常邮件通知
+ADMINS = [('admin', os.environ.get('DJANGO_ADMIN_EMAIL') or 'admin@admin.com')] # 管理员邮箱
+# 微信管理员密码(两次MD5加密)
+WXADMIN = os.environ.get(
+ 'DJANGO_WXADMIN_PASSWORD') or '995F03AC401D6CABABAEF756FC4D43C7'
+
+# 日志配置
+LOG_PATH = os.path.join(BASE_DIR, 'logs') # 日志路径
+if not os.path.exists(LOG_PATH):
+ os.makedirs(LOG_PATH, exist_ok=True) # 创建日志目录
+
+LOGGING = {
+ 'version': 1, # 日志配置版本
+ 'disable_existing_loggers': False, # 不禁用现有日志记录器
+ 'root': {
+ 'level': 'INFO', # 根日志级别
+ 'handlers': ['console', 'log_file'], # 处理器
+ },
+ 'formatters': { # 日志格式
+ 'verbose': {
+ 'format': '[%(asctime)s] %(levelname)s [%(name)s.%(funcName)s:%(lineno)d %(module)s] %(message)s', # 详细格式
+ }
+ },
+ 'filters': { # 过滤器
+ 'require_debug_false': {
+ '()': 'django.utils.log.RequireDebugFalse', # 要求调试模式为False
+ },
+ 'require_debug_true': {
+ '()': 'django.utils.log.RequireDebugTrue', # 要求调试模式为True
+ },
+ },
+ 'handlers': { # 处理器
+ 'log_file': {
+ 'level': 'INFO', # 日志级别
+ 'class': 'logging.handlers.TimedRotatingFileHandler', # 按时间轮转的文件处理器
+ 'filename': os.path.join(LOG_PATH, 'djangoblog.log'), # 日志文件路径
+ 'when': 'D', # 按天轮转
+ 'formatter': 'verbose', # 使用详细格式
+ 'interval': 1, # 间隔1天
+ 'delay': True, # 延迟创建
+ 'backupCount': 5, # 保留5个备份
+ 'encoding': 'utf-8' # 文件编码
+ },
+ 'console': { # 控制台处理器
+ 'level': 'DEBUG',
+ 'filters': ['require_debug_true'], # 仅在调试模式下生效
+ 'class': 'logging.StreamHandler',
+ 'formatter': 'verbose'
+ },
+ 'null': { # 空处理器
+ 'class': 'logging.NullHandler',
+ },
+ 'mail_admins': { # 管理员邮件处理器
+ 'level': 'ERROR',
+ 'filters': ['require_debug_false'], # 仅在非调试模式下生效
+ 'class': 'django.utils.log.AdminEmailHandler'
+ }
+ },
+ 'loggers': { # 日志记录器
+ 'djangoblog': {
+ 'handlers': ['log_file', 'console'],
+ 'level': 'INFO',
+ 'propagate': True, # 向上传播
+ },
+ 'django.request': { # Django请求日志
+ 'handlers': ['mail_admins'], # 发送邮件给管理员
+ 'level': 'ERROR',
+ 'propagate': False, # 不向上传播
+ }
+ }
+}
+
+# 静态文件查找器
+STATICFILES_FINDERS = (
+ 'django.contrib.staticfiles.finders.FileSystemFinder', # 文件系统查找器
+ 'django.contrib.staticfiles.finders.AppDirectoriesFinder', # 应用目录查找器
+ # other
+ 'compressor.finders.CompressorFinder', # 压缩文件查找器
+)
+COMPRESS_ENABLED = True # 启用压缩
+# COMPRESS_OFFLINE = True # 离线压缩(注释)
+
+# CSS压缩过滤器
+COMPRESS_CSS_FILTERS = [
+ # 从相对URL创建绝对URL
+ 'compressor.filters.css_default.CssAbsoluteFilter',
+ # CSS压缩器
+ 'compressor.filters.cssmin.CSSMinFilter'
+]
+# JS压缩过滤器
+COMPRESS_JS_FILTERS = [
+ 'compressor.filters.jsmin.JSMinFilter' # JS压缩器
+]
+
+MEDIA_ROOT = os.path.join(BASE_DIR, 'uploads') # 媒体文件根目录
+MEDIA_URL = '/media/' # 媒体文件URL
+X_FRAME_OPTIONS = 'SAMEORIGIN' # 帧选项:同源策略
+
+DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' # 默认自增字段类型
+
+# Elasticsearch配置
+if os.environ.get('DJANGO_ELASTICSEARCH_HOST'):
+ ELASTICSEARCH_DSL = {
+ 'default': {
+ 'hosts': os.environ.get('DJANGO_ELASTICSEARCH_HOST') # Elasticsearch主机
+ },
+ }
+ HAYSTACK_CONNECTIONS = {
+ 'default': {
+ 'ENGINE': 'djangoblog.elasticsearch_backend.ElasticSearchEngine', # Elasticsearch引擎
+ },
+ }
+
+# 插件系统
+PLUGINS_DIR = BASE_DIR / 'plugins' # 插件目录
+ACTIVE_PLUGINS = [ # 激活的插件列表
+ 'article_copyright', # 文章版权
+ 'reading_time', # 阅读时间
+ 'external_links', # 外部链接
+ 'view_count', # 浏览量统计
+ 'seo_optimizer' # SEO优化
+]
\ No newline at end of file
diff --git a/doc/第7周代码注释/djangoblog/sitemap.py b/doc/第7周代码注释/djangoblog/sitemap.py
new file mode 100644
index 00000000..156d36d1
--- /dev/null
+++ b/doc/第7周代码注释/djangoblog/sitemap.py
@@ -0,0 +1,81 @@
+from django.contrib.sitemaps import Sitemap
+from django.urls import reverse
+
+# 导入博客相关模型
+from blog.models import Article, Category, Tag
+
+
+class StaticViewSitemap(Sitemap):
+ """静态页面站点地图"""
+
+ priority = 0.5 # 优先级(0.0-1.0)
+ changefreq = 'daily' # 更新频率:每天
+
+ def items(self):
+ """返回包含在站点地图中的项目列表"""
+ return ['blog:index', ] # 博客首页
+
+ def location(self, item):
+ """返回每个项目的绝对URL"""
+ return reverse(item) # 通过反向解析生成URL
+
+
+class ArticleSiteMap(Sitemap):
+ """文章站点地图"""
+
+ changefreq = "monthly" # 更新频率:每月
+ priority = "0.6" # 优先级:0.6
+
+ def items(self):
+ """返回所有已发布的文章"""
+ return Article.objects.filter(status='p') # 状态为'p'(已发布)的文章
+
+ def lastmod(self, obj):
+ """返回文章的最后修改时间"""
+ return obj.last_modify_time # 文章的最后修改时间
+
+
+class CategorySiteMap(Sitemap):
+ """分类站点地图"""
+
+ changefreq = "Weekly" # 更新频率:每周
+ priority = "0.6" # 优先级:0.6
+
+ def items(self):
+ """返回所有分类"""
+ return Category.objects.all() # 所有分类
+
+ def lastmod(self, obj):
+ """返回分类的最后修改时间"""
+ return obj.last_modify_time # 分类的最后修改时间
+
+
+class TagSiteMap(Sitemap):
+ """标签站点地图"""
+
+ changefreq = "Weekly" # 更新频率:每周
+ priority = "0.3" # 优先级:0.3(标签页优先级较低)
+
+ def items(self):
+ """返回所有标签"""
+ return Tag.objects.all() # 所有标签
+
+ def lastmod(self, obj):
+ """返回标签的最后修改时间"""
+ return obj.last_modify_time # 标签的最后修改时间
+
+
+class UserSiteMap(Sitemap):
+ """用户站点地图"""
+
+ changefreq = "Weekly" # 更新频率:每周
+ priority = "0.3" # 优先级:0.3(用户页优先级较低)
+
+ def items(self):
+ """返回所有有文章的作者(去重)"""
+ # 获取所有文章的作者,并通过set去重,再转换为列表
+ return list(set(map(lambda x: x.author, Article.objects.all())))
+
+ def lastmod(self, obj):
+ """返回用户的注册时间"""
+ return obj.date_joined # 用户的注册时间
\ No newline at end of file
diff --git a/doc/第7周代码注释/djangoblog/spider_notify.py b/doc/第7周代码注释/djangoblog/spider_notify.py
new file mode 100644
index 00000000..936f8e8b
--- /dev/null
+++ b/doc/第7周代码注释/djangoblog/spider_notify.py
@@ -0,0 +1,40 @@
+import logging
+
+import requests
+from django.conf import settings
+
+# 获取日志记录器
+logger = logging.getLogger(__name__)
+
+
+class SpiderNotify():
+ """搜索引擎爬虫通知类"""
+
+ @staticmethod
+ def baidu_notify(urls):
+ """
+ 向百度站长平台提交链接,通知百度爬虫抓取更新内容
+
+ Args:
+ urls: 需要通知的URL列表
+ """
+ try:
+ # 将URL列表转换为换行分隔的字符串格式
+ data = '\n'.join(urls)
+ # 向百度站长平台API提交URL数据
+ result = requests.post(settings.BAIDU_NOTIFY_URL, data=data)
+ # 记录API返回结果
+ logger.info(result.text)
+ except Exception as e:
+ # 记录通知过程中的错误
+ logger.error(e)
+
+ @staticmethod
+ def notify(url):
+ """
+ 通用的爬虫通知方法(目前仅支持百度)
+
+ Args:
+ url: 需要通知的URL
+ """
+ SpiderNotify.baidu_notify(url)
\ No newline at end of file
diff --git a/doc/第7周代码注释/djangoblog/tests.py b/doc/第7周代码注释/djangoblog/tests.py
new file mode 100644
index 00000000..ba2a5a86
--- /dev/null
+++ b/doc/第7周代码注释/djangoblog/tests.py
@@ -0,0 +1,43 @@
+from django.test import TestCase
+
+# 导入工具函数
+from djangoblog.utils import *
+
+
+class DjangoBlogTest(TestCase):
+ """DjangoBlog应用测试类"""
+
+ def setUp(self):
+ """测试初始化方法"""
+ # 可以在这里设置测试数据,当前为空
+ pass
+
+ def test_utils(self):
+ """测试工具函数"""
+ # 测试SHA256加密函数
+ md5 = get_sha256('test')
+ self.assertIsNotNone(md5) # 断言加密结果不为空
+
+ # 测试Markdown转换函数
+ c = CommonMarkdown.get_markdown('''
+ # Title1
+
+ ```python
+ import os
+ ```
+
+ [url](https://www.lylinux.net/)
+
+ [ddd](http://www.baidu.com)
+
+
+ ''')
+ self.assertIsNotNone(c) # 断言Markdown转换结果不为空
+
+ # 测试字典转URL参数字符串函数
+ d = {
+ 'd': 'key1',
+ 'd2': 'key2'
+ }
+ data = parse_dict_to_url(d)
+ self.assertIsNotNone(data) # 断言转换结果不为空
\ No newline at end of file
diff --git a/doc/第7周代码注释/djangoblog/urls.py b/doc/第7周代码注释/djangoblog/urls.py
new file mode 100644
index 00000000..38115337
--- /dev/null
+++ b/doc/第7周代码注释/djangoblog/urls.py
@@ -0,0 +1,73 @@
+"""djangoblog URL 配置
+
+`urlpatterns` 列表将 URL 路由到视图。更多信息请参阅:
+ https://docs.djangoproject.com/en/1.10/topics/http/urls/
+示例:
+函数视图
+ 1. 导入: from my_app import views
+ 2. 添加 URL 到 urlpatterns: url(r'^$', views.home, name='home')
+基于类的视图
+ 1. 导入: from other_app.views import Home
+ 2. 添加 URL 到 urlpatterns: url(r'^$', Home.as_view(), name='home')
+包含其他 URLconf
+ 1. 导入 include() 函数: from django.conf.urls import url, include
+ 2. 添加 URL 到 urlpatterns: url(r'^blog/', include('blog.urls'))
+"""
+from django.conf import settings
+from django.conf.urls.i18n import i18n_patterns # 国际化URL模式
+from django.conf.urls.static import static # 静态文件服务
+from django.contrib.sitemaps.views import sitemap # 站点地图视图
+from django.urls import path, include
+from django.urls import re_path
+from haystack.views import search_view_factory # Haystack搜索视图工厂
+
+# 导入自定义视图和组件
+from blog.views import EsSearchView
+from djangoblog.admin_site import admin_site # 自定义管理员站点
+from djangoblog.elasticsearch_backend import ElasticSearchModelSearchForm # Elasticsearch搜索表单
+from djangoblog.feeds import DjangoBlogFeed # RSS订阅源
+from djangoblog.sitemap import ArticleSiteMap, CategorySiteMap, StaticViewSitemap, TagSiteMap, UserSiteMap
+
+# 站点地图配置字典
+sitemaps = {
+ 'blog': ArticleSiteMap, # 文章站点地图
+ 'Category': CategorySiteMap, # 分类站点地图
+ 'Tag': TagSiteMap, # 标签站点地图
+ 'User': UserSiteMap, # 用户站点地图
+ 'static': StaticViewSitemap # 静态页面站点地图
+}
+
+# 自定义错误处理视图
+handler404 = 'blog.views.page_not_found_view' # 404页面未找到
+handler500 = 'blog.views.server_error_view' # 500服务器错误
+handle403 = 'blog.views.permission_denied_view' # 403权限拒绝
+
+# 基本URL模式
+urlpatterns = [
+ path('i18n/', include('django.conf.urls.i18n')), # 国际化URL
+]
+
+# 添加国际化URL模式
+urlpatterns += i18n_patterns(
+ re_path(r'^admin/', admin_site.urls), # 自定义管理员后台URL
+ re_path(r'', include('blog.urls', namespace='blog')), # 博客应用URL
+ re_path(r'mdeditor/', include('mdeditor.urls')), # Markdown编辑器URL
+ re_path(r'', include('comments.urls', namespace='comment')), # 评论应用URL
+ re_path(r'', include('accounts.urls', namespace='account')), # 账户应用URL
+ re_path(r'', include('oauth.urls', namespace='oauth')), # OAuth认证URL
+ re_path(r'^sitemap\.xml$', sitemap, {'sitemaps': sitemaps}, # 站点地图XML
+ name='django.contrib.sitemaps.views.sitemap'),
+ re_path(r'^feed/$', DjangoBlogFeed()), # RSS订阅源URL
+ re_path(r'^rss/$', DjangoBlogFeed()), # RSS订阅源别名
+ # 搜索URL,使用自定义的Elasticsearch视图和表单
+ re_path('^search', search_view_factory(view_class=EsSearchView, form_class=ElasticSearchModelSearchForm),
+ name='search'),
+ re_path(r'', include('servermanager.urls', namespace='servermanager')), # 服务器管理URL
+ re_path(r'', include('owntracks.urls', namespace='owntracks')), # OwnTracks位置跟踪URL
+ prefix_default_language=False # 不在默认语言的URL前添加语言前缀
+) + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) # 静态文件服务
+
+# 调试模式下启用媒体文件服务
+if settings.DEBUG:
+ urlpatterns += static(settings.MEDIA_URL,
+ document_root=settings.MEDIA_ROOT)
\ No newline at end of file
diff --git a/doc/第7周代码注释/djangoblog/utils.py b/doc/第7周代码注释/djangoblog/utils.py
new file mode 100644
index 00000000..f4518efa
--- /dev/null
+++ b/doc/第7周代码注释/djangoblog/utils.py
@@ -0,0 +1,262 @@
+#!/usr/bin/env python
+# encoding: utf-8
+
+
+import logging
+import os
+import random
+import string
+import uuid
+from hashlib import sha256
+
+import bleach
+import markdown
+import requests
+from django.conf import settings
+from django.contrib.sites.models import Site
+from django.core.cache import cache
+from django.templatetags.static import static
+
+# 获取日志记录器
+logger = logging.getLogger(__name__)
+
+
+def get_max_articleid_commentid():
+ """获取最新的文章ID和评论ID"""
+ from blog.models import Article
+ from comments.models import Comment
+ return (Article.objects.latest().pk, Comment.objects.latest().pk)
+
+
+def get_sha256(str):
+ """计算字符串的SHA256哈希值"""
+ m = sha256(str.encode('utf-8'))
+ return m.hexdigest()
+
+
+def cache_decorator(expiration=3 * 60):
+ """缓存装饰器,用于缓存函数结果
+
+ Args:
+ expiration: 缓存过期时间,默认3分钟
+ """
+ def wrapper(func):
+ def news(*args, **kwargs):
+ try:
+ # 尝试从视图获取缓存键
+ view = args[0]
+ key = view.get_cache_key()
+ except:
+ key = None
+ if not key:
+ # 如果没有缓存键,根据函数和参数生成唯一键
+ unique_str = repr((func, args, kwargs))
+ m = sha256(unique_str.encode('utf-8'))
+ key = m.hexdigest()
+ value = cache.get(key)
+ if value is not None:
+ # logger.info('cache_decorator get cache:%s key:%s' % (func.__name__, key))
+ if str(value) == '__default_cache_value__':
+ return None
+ else:
+ return value
+ else:
+ logger.debug(
+ 'cache_decorator set cache:%s key:%s' %
+ (func.__name__, key))
+ value = func(*args, **kwargs)
+ if value is None:
+ # 如果函数返回None,设置默认缓存值
+ cache.set(key, '__default_cache_value__', expiration)
+ else:
+ cache.set(key, value, expiration)
+ return value
+
+ return news
+
+ return wrapper
+
+
+def expire_view_cache(path, servername, serverport, key_prefix=None):
+ '''
+ 刷新视图缓存
+ :param path:url路径
+ :param servername:host
+ :param serverport:端口
+ :param key_prefix:前缀
+ :return:是否成功
+ '''
+ from django.http import HttpRequest
+ from django.utils.cache import get_cache_key
+
+ # 创建模拟请求对象
+ request = HttpRequest()
+ request.META = {'SERVER_NAME': servername, 'SERVER_PORT': serverport}
+ request.path = path
+
+ # 获取缓存键并删除缓存
+ key = get_cache_key(request, key_prefix=key_prefix, cache=cache)
+ if key:
+ logger.info('expire_view_cache:get key:{path}'.format(path=path))
+ if cache.get(key):
+ cache.delete(key)
+ return True
+ return False
+
+
+@cache_decorator()
+def get_current_site():
+ """获取当前站点(带缓存)"""
+ site = Site.objects.get_current()
+ return site
+
+
+class CommonMarkdown:
+ """Markdown处理工具类"""
+
+ @staticmethod
+ def _convert_markdown(value):
+ """内部方法:转换Markdown文本为HTML"""
+ md = markdown.Markdown(
+ extensions=[
+ 'extra', # 额外扩展
+ 'codehilite', # 代码高亮
+ 'toc', # 目录生成
+ 'tables', # 表格支持
+ ]
+ )
+ body = md.convert(value) # 转换Markdown为HTML
+ toc = md.toc # 获取目录
+ return body, toc
+
+ @staticmethod
+ def get_markdown_with_toc(value):
+ """获取带目录的Markdown转换结果"""
+ body, toc = CommonMarkdown._convert_markdown(value)
+ return body, toc
+
+ @staticmethod
+ def get_markdown(value):
+ """获取Markdown转换结果(不带目录)"""
+ body, toc = CommonMarkdown._convert_markdown(value)
+ return body
+
+
+def send_email(emailto, title, content):
+ """发送邮件(通过信号机制)"""
+ from djangoblog.blog_signals import send_email_signal
+ send_email_signal.send(
+ send_email.__class__,
+ emailto=emailto,
+ title=title,
+ content=content)
+
+
+def generate_code() -> str:
+ """生成6位随机数验证码"""
+ return ''.join(random.sample(string.digits, 6))
+
+
+def parse_dict_to_url(dict):
+ """将字典转换为URL参数字符串"""
+ from urllib.parse import quote
+ url = '&'.join(['{}={}'.format(quote(k, safe='/'), quote(v, safe='/'))
+ for k, v in dict.items()])
+ return url
+
+
+def get_blog_setting():
+ """获取博客设置(带缓存)"""
+ value = cache.get('get_blog_setting')
+ if value:
+ return value
+ else:
+ from blog.models import BlogSettings
+ # 如果数据库中没有博客设置,创建默认设置
+ if not BlogSettings.objects.count():
+ setting = BlogSettings()
+ setting.site_name = 'djangoblog'
+ setting.site_description = '基于Django的博客系统'
+ setting.site_seo_description = '基于Django的博客系统'
+ setting.site_keywords = 'Django,Python'
+ setting.article_sub_length = 300
+ setting.sidebar_article_count = 10
+ setting.sidebar_comment_count = 5
+ setting.show_google_adsense = False
+ setting.open_site_comment = True
+ setting.analytics_code = ''
+ setting.beian_code = ''
+ setting.show_gongan_code = False
+ setting.comment_need_review = False
+ setting.save()
+ value = BlogSettings.objects.first()
+ logger.info('set cache get_blog_setting')
+ cache.set('get_blog_setting', value) # 设置缓存
+ return value
+
+
+def save_user_avatar(url):
+ '''
+ 保存用户头像到本地
+ :param url:头像url
+ :return: 本地路径
+ '''
+ logger.info(url)
+
+ try:
+ basedir = os.path.join(settings.STATICFILES, 'avatar')
+ rsp = requests.get(url, timeout=2) # 下载头像
+ if rsp.status_code == 200:
+ if not os.path.exists(basedir):
+ os.makedirs(basedir) # 创建头像目录
+
+ # 检查文件是否为图片格式
+ image_extensions = ['.jpg', '.png', 'jpeg', '.gif']
+ isimage = len([i for i in image_extensions if url.endswith(i)]) > 0
+ ext = os.path.splitext(url)[1] if isimage else '.jpg'
+ save_filename = str(uuid.uuid4().hex) + ext # 生成唯一文件名
+ logger.info('保存用户头像:' + basedir + save_filename)
+ # 保存头像文件
+ with open(os.path.join(basedir, save_filename), 'wb+') as file:
+ file.write(rsp.content)
+ return static('avatar/' + save_filename) # 返回静态文件URL
+ except Exception as e:
+ logger.error(e)
+ return static('blog/img/avatar.png') # 返回默认头像
+
+
+def delete_sidebar_cache():
+ """删除侧边栏相关缓存"""
+ from blog.models import LinkShowType
+ keys = ["sidebar" + x for x in LinkShowType.values]
+ for k in keys:
+ logger.info('delete sidebar key:' + k)
+ cache.delete(k)
+
+
+def delete_view_cache(prefix, keys):
+ """删除视图缓存"""
+ from django.core.cache.utils import make_template_fragment_key
+ key = make_template_fragment_key(prefix, keys)
+ cache.delete(key)
+
+
+def get_resource_url():
+ """获取资源URL(静态文件URL)"""
+ if settings.STATIC_URL:
+ return settings.STATIC_URL
+ else:
+ site = get_current_site()
+ return 'http://' + site.domain + '/static/'
+
+
+# HTML清理允许的标签
+ALLOWED_TAGS = ['a', 'abbr', 'acronym', 'b', 'blockquote', 'code', 'em', 'i', 'li', 'ol', 'pre', 'strong', 'ul', 'h1',
+ 'h2', 'p']
+# HTML清理允许的属性
+ALLOWED_ATTRIBUTES = {'a': ['href', 'title'], 'abbr': ['title'], 'acronym': ['title']}
+
+
+def sanitize_html(html):
+ """清理HTML,移除不安全的标签和属性"""
+ return bleach.clean(html, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRIBUTES)
\ No newline at end of file
diff --git a/doc/第7周代码注释/djangoblog/whoosh_cn_backend.py b/doc/第7周代码注释/djangoblog/whoosh_cn_backend.py
new file mode 100644
index 00000000..b99f83d7
--- /dev/null
+++ b/doc/第7周代码注释/djangoblog/whoosh_cn_backend.py
@@ -0,0 +1,1073 @@
+# encoding: utf-8
+# 文件编码声明
+
+from __future__ import absolute_import, division, print_function, unicode_literals
+# 兼容Python 2和3的导入
+
+import json
+import os
+import re
+import shutil
+import threading
+import warnings
+
+import six
+from django.conf import settings
+from django.core.exceptions import ImproperlyConfigured
+from datetime import datetime
+from django.utils.encoding import force_str
+from haystack.backends import BaseEngine, BaseSearchBackend, BaseSearchQuery, EmptyResults, log_query
+from haystack.constants import DJANGO_CT, DJANGO_ID, ID
+from haystack.exceptions import MissingDependency, SearchBackendError, SkipDocument
+from haystack.inputs import Clean, Exact, PythonData, Raw
+from haystack.models import SearchResult
+from haystack.utils import get_identifier, get_model_ct
+from haystack.utils import log as logging
+from haystack.utils.app_loading import haystack_get_model
+from jieba.analyse import ChineseAnalyzer # 中文分词器
+from whoosh import index
+from whoosh.analysis import StemmingAnalyzer
+from whoosh.fields import BOOLEAN, DATETIME, IDLIST, KEYWORD, NGRAM, NGRAMWORDS, NUMERIC, Schema, TEXT
+from whoosh.fields import ID as WHOOSH_ID
+from whoosh.filedb.filestore import FileStorage, RamStorage
+from whoosh.highlight import ContextFragmenter, HtmlFormatter
+from whoosh.highlight import highlight as whoosh_highlight
+from whoosh.qparser import QueryParser
+from whoosh.searching import ResultsPage
+from whoosh.writing import AsyncWriter
+
+try:
+ import whoosh
+except ImportError:
+ # 如果Whoosh没有安装,抛出缺失依赖异常
+ raise MissingDependency(
+ "The 'whoosh' backend requires the installation of 'Whoosh'. Please refer to the documentation.")
+
+# 处理最低版本要求
+if not hasattr(whoosh, '__version__') or whoosh.__version__ < (2, 5, 0):
+ raise MissingDependency(
+ "The 'whoosh' backend requires version 2.5.0 or greater.")
+
+# 日期时间正则表达式,用于解析日期时间字符串
+DATETIME_REGEX = re.compile(
+ '^(?P\d{4})-(?P\d{2})-(?P\d{2})T(?P\d{2}):(?P\d{2}):(?P\d{2})(\.\d{3,6}Z?)?$')
+LOCALS = threading.local() # 线程本地存储
+LOCALS.RAM_STORE = None
+
+
+class WhooshHtmlFormatter(HtmlFormatter):
+ """
+ 简化的Whoosh HTML格式化器
+ 我们使用它来在不同后端之间获得一致的结果。
+ 具体来说,Solr、Xapian和Elasticsearch都使用这种格式化。
+ """
+ template = '<%(tag)s>%(t)s%(tag)s>'
+
+
+class WhooshSearchBackend(BaseSearchBackend):
+ """Whoosh搜索引擎后端实现"""
+
+ # Whoosh保留的特殊用途单词
+ RESERVED_WORDS = (
+ 'AND',
+ 'NOT',
+ 'OR',
+ 'TO',
+ )
+
+ # Whoosh保留的特殊用途字符
+ # '\\'必须放在前面,以免覆盖其他斜杠替换
+ RESERVED_CHARACTERS = (
+ '\\', '+', '-', '&&', '||', '!', '(', ')', '{', '}',
+ '[', ']', '^', '"', '~', '*', '?', ':', '.',
+ )
+
+ def __init__(self, connection_alias, **connection_options):
+ super(
+ WhooshSearchBackend,
+ self).__init__(
+ connection_alias,
+ **connection_options)
+ self.setup_complete = False # 设置完成标志
+ self.use_file_storage = True # 是否使用文件存储
+ self.post_limit = getattr(
+ connection_options,
+ 'POST_LIMIT',
+ 128 * 1024 * 1024) # 帖子大小限制,默认128MB
+ self.path = connection_options.get('PATH') # 索引存储路径
+
+ if connection_options.get('STORAGE', 'file') != 'file':
+ self.use_file_storage = False # 不使用文件存储,使用内存存储
+
+ if self.use_file_storage and not self.path:
+ raise ImproperlyConfigured(
+ "You must specify a 'PATH' in your settings for connection '%s'." %
+ connection_alias)
+
+ self.log = logging.getLogger('haystack') # 日志记录器
+
+ def setup(self):
+ """
+ 延迟加载,直到需要时才设置
+ """
+ from haystack import connections
+ new_index = False # 是否创建新索引
+
+ # 确保索引目录存在
+ if self.use_file_storage and not os.path.exists(self.path):
+ os.makedirs(self.path)
+ new_index = True
+
+ if self.use_file_storage and not os.access(self.path, os.W_OK):
+ raise IOError(
+ "The path to your Whoosh index '%s' is not writable for the current user/group." %
+ self.path)
+
+ # 选择存储类型:文件存储或内存存储
+ if self.use_file_storage:
+ self.storage = FileStorage(self.path)
+ else:
+ global LOCALS
+
+ if getattr(LOCALS, 'RAM_STORE', None) is None:
+ LOCALS.RAM_STORE = RamStorage()
+
+ self.storage = LOCALS.RAM_STORE
+
+ # 构建schema和内容字段名
+ self.content_field_name, self.schema = self.build_schema(
+ connections[self.connection_alias].get_unified_index().all_searchfields())
+ self.parser = QueryParser(self.content_field_name, schema=self.schema) # 查询解析器
+
+ # 创建或打开索引
+ if new_index is True:
+ self.index = self.storage.create_index(self.schema)
+ else:
+ try:
+ self.index = self.storage.open_index(schema=self.schema)
+ except index.EmptyIndexError:
+ self.index = self.storage.create_index(self.schema)
+
+ self.setup_complete = True # 标记设置完成
+
+ def build_schema(self, fields):
+ """构建Whoosh索引schema"""
+ schema_fields = {
+ ID: WHOOSH_ID(stored=True, unique=True), # 唯一标识符
+ DJANGO_CT: WHOOSH_ID(stored=True), # Django内容类型
+ DJANGO_ID: WHOOSH_ID(stored=True), # Django对象ID
+ }
+ # 获取Haystack硬编码的键数量
+ initial_key_count = len(schema_fields)
+ content_field_name = '' # 内容字段名
+
+ for field_name, field_class in fields.items():
+ if field_class.is_multivalued: # 多值字段
+ if field_class.indexed is False:
+ schema_fields[field_class.index_fieldname] = IDLIST(
+ stored=True, field_boost=field_class.boost)
+ else:
+ schema_fields[field_class.index_fieldname] = KEYWORD(
+ stored=True, commas=True, scorable=True, field_boost=field_class.boost)
+ elif field_class.field_type in ['date', 'datetime']: # 日期时间字段
+ schema_fields[field_class.index_fieldname] = DATETIME(
+ stored=field_class.stored, sortable=True)
+ elif field_class.field_type == 'integer': # 整数字段
+ schema_fields[field_class.index_fieldname] = NUMERIC(
+ stored=field_class.stored, numtype=int, field_boost=field_class.boost)
+ elif field_class.field_type == 'float': # 浮点数字段
+ schema_fields[field_class.index_fieldname] = NUMERIC(
+ stored=field_class.stored, numtype=float, field_boost=field_class.boost)
+ elif field_class.field_type == 'boolean': # 布尔字段
+ # Field boost在1.8.2版本中不支持BOOLEAN
+ schema_fields[field_class.index_fieldname] = BOOLEAN(
+ stored=field_class.stored)
+ elif field_class.field_type == 'ngram': # N-gram字段
+ schema_fields[field_class.index_fieldname] = NGRAM(
+ minsize=3, maxsize=15, stored=field_class.stored, field_boost=field_class.boost)
+ elif field_class.field_type == 'edge_ngram': # 边缘N-gram字段
+ schema_fields[field_class.index_fieldname] = NGRAMWORDS(minsize=2, maxsize=15, at='start',
+ stored=field_class.stored,
+ field_boost=field_class.boost)
+ else:
+ # 默认使用中文分析器的文本字段
+ # 原代码使用StemmingAnalyzer,现改为ChineseAnalyzer以支持中文分词
+ schema_fields[field_class.index_fieldname] = TEXT(
+ stored=True, analyzer=ChineseAnalyzer(), field_boost=field_class.boost, sortable=True)
+ if field_class.document is True: # 主文档字段
+ content_field_name = field_class.index_fieldname
+ schema_fields[field_class.index_fieldname].spelling = True # 启用拼写建议
+
+ # 如果没有找到字段,优雅地失败
+ if len(schema_fields) <= initial_key_count:
+ raise SearchBackendError(
+ "No fields were found in any search_indexes. Please correct this before attempting to search.")
+
+ return (content_field_name, Schema(**schema_fields))
+
+ def update(self, index, iterable, commit=True):
+ """更新索引文档"""
+ if not self.setup_complete:
+ self.setup()
+
+ self.index = self.index.refresh() # 刷新索引
+ writer = AsyncWriter(self.index) # 异步写入器
+
+ for obj in iterable:
+ try:
+ doc = index.full_prepare(obj) # 准备文档
+ except SkipDocument:
+ self.log.debug(u"Indexing for object `%s` skipped", obj)
+ else:
+ # 确保所有值都是Unicode,因为Whoosh只接受Unicode
+ for key in doc:
+ doc[key] = self._from_python(doc[key])
+
+ # Whoosh 2.5.0+不支持文档boost
+ if 'boost' in doc:
+ del doc['boost']
+
+ try:
+ writer.update_document(**doc) # 更新文档
+ except Exception as e:
+ if not self.silently_fail:
+ raise
+
+ # 记录对象标识符但不包含实际对象,避免处理日志消息时产生编码错误
+ self.log.error(
+ u"%s while preparing object for update" %
+ e.__class__.__name__,
+ exc_info=True,
+ extra={
+ "data": {
+ "index": index,
+ "object": get_identifier(obj)}})
+
+ if len(iterable) > 0:
+ # 目前无论如何都要提交,否则会遇到锁定问题
+ writer.commit()
+
+ def remove(self, obj_or_string, commit=True):
+ """从索引中移除文档"""
+ if not self.setup_complete:
+ self.setup()
+
+ self.index = self.index.refresh()
+ whoosh_id = get_identifier(obj_or_string)
+
+ try:
+ # 通过查询删除文档
+ self.index.delete_by_query(
+ q=self.parser.parse(
+ u'%s:"%s"' %
+ (ID, whoosh_id)))
+ except Exception as e:
+ if not self.silently_fail:
+ raise
+
+ self.log.error(
+ "Failed to remove document '%s' from Whoosh: %s",
+ whoosh_id,
+ e,
+ exc_info=True)
+
+ def clear(self, models=None, commit=True):
+ """清空索引"""
+ if not self.setup_complete:
+ self.setup()
+
+ self.index = self.index.refresh()
+
+ if models is not None:
+ assert isinstance(models, (list, tuple))
+
+ try:
+ if models is None:
+ self.delete_index() # 完全删除索引
+ else:
+ models_to_delete = []
+
+ for model in models:
+ models_to_delete.append(
+ u"%s:%s" %
+ (DJANGO_CT, get_model_ct(model)))
+
+ # 通过查询删除指定模型的文档
+ self.index.delete_by_query(
+ q=self.parser.parse(
+ u" OR ".join(models_to_delete)))
+ except Exception as e:
+ if not self.silently_fail:
+ raise
+
+ if models is not None:
+ self.log.error(
+ "Failed to clear Whoosh index of models '%s': %s",
+ ','.join(models_to_delete),
+ e,
+ exc_info=True)
+ else:
+ self.log.error(
+ "Failed to clear Whoosh index: %s", e, exc_info=True)
+
+ def delete_index(self):
+ """删除整个索引"""
+ # 根据Whoosh邮件列表,如果要清除索引中的所有内容,直接删除索引文件更高效
+ if self.use_file_storage and os.path.exists(self.path):
+ shutil.rmtree(self.path)
+ elif not self.use_file_storage:
+ self.storage.clean()
+
+ # 重新创建所有内容
+ self.setup()
+
+ def optimize(self):
+ """优化索引"""
+ if not self.setup_complete:
+ self.setup()
+
+ self.index = self.index.refresh()
+ self.index.optimize()
+
+ def calculate_page(self, start_offset=0, end_offset=None):
+ """计算分页信息"""
+ # 防止Whoosh抛出错误。需要end_offset大于0
+ if end_offset is not None and end_offset <= 0:
+ end_offset = 1
+
+ # 确定页码
+ page_num = 0
+
+ if end_offset is None:
+ end_offset = 1000000 # 默认大数
+
+ if start_offset is None:
+ start_offset = 0
+
+ page_length = end_offset - start_offset # 页面长度
+
+ if page_length and page_length > 0:
+ page_num = int(start_offset / page_length)
+
+ # 递增,因为Whoosh使用基于1的页码
+ page_num += 1
+ return page_num, page_length
+
+ @log_query # 记录查询日志的装饰器
+ def search(
+ self,
+ query_string,
+ sort_by=None,
+ start_offset=0,
+ end_offset=None,
+ fields='',
+ highlight=False,
+ facets=None,
+ date_facets=None,
+ query_facets=None,
+ narrow_queries=None,
+ spelling_query=None,
+ within=None,
+ dwithin=None,
+ distance_point=None,
+ models=None,
+ limit_to_registered_models=None,
+ result_class=None,
+ **kwargs):
+ """执行搜索查询"""
+ if not self.setup_complete:
+ self.setup()
+
+ # 零长度查询应该返回无结果
+ if len(query_string) == 0:
+ return {
+ 'results': [],
+ 'hits': 0,
+ }
+
+ query_string = force_str(query_string) # 确保查询字符串是字符串
+
+ # 单字符查询(非通配符)会被停用词过滤器捕获,应该返回零结果
+ if len(query_string) <= 1 and query_string != u'*':
+ return {
+ 'results': [],
+ 'hits': 0,
+ }
+
+ reverse = False # 是否反转排序
+
+ if sort_by is not None:
+ # 确定是否需要反转结果以及Whoosh是否可以处理被要求排序的字段
+ # 反转是一个全有或全无的操作
+ sort_by_list = []
+ reverse_counter = 0
+
+ for order_by in sort_by:
+ if order_by.startswith('-'): # 降序排序
+ reverse_counter += 1
+
+ if reverse_counter and reverse_counter != len(sort_by):
+ raise SearchBackendError("Whoosh requires all order_by fields"
+ " to use the same sort direction")
+
+ for order_by in sort_by:
+ if order_by.startswith('-'):
+ sort_by_list.append(order_by[1:]) # 移除负号
+
+ if len(sort_by_list) == 1:
+ reverse = True
+ else:
+ sort_by_list.append(order_by)
+
+ if len(sort_by_list) == 1:
+ reverse = False
+
+ sort_by = sort_by_list[0] # Whoosh只支持单字段排序
+
+ # Whoosh不支持分面搜索,发出警告
+ if facets is not None:
+ warnings.warn(
+ "Whoosh does not handle faceting.",
+ Warning,
+ stacklevel=2)
+
+ if date_facets is not None:
+ warnings.warn(
+ "Whoosh does not handle date faceting.",
+ Warning,
+ stacklevel=2)
+
+ if query_facets is not None:
+ warnings.warn(
+ "Whoosh does not handle query faceting.",
+ Warning,
+ stacklevel=2)
+
+ narrowed_results = None
+ self.index = self.index.refresh()
+
+ # 限制到注册的模型
+ if limit_to_registered_models is None:
+ limit_to_registered_models = getattr(
+ settings, 'HAYSTACK_LIMIT_TO_REGISTERED_MODELS', True)
+
+ # 构建模型选择列表
+ if models and len(models):
+ model_choices = sorted(get_model_ct(model) for model in models)
+ elif limit_to_registered_models:
+ # 使用窄查询,将结果限制为当前路由器处理的模型
+ model_choices = self.build_models_list()
+ else:
+ model_choices = []
+
+ # 如果有模型选择,添加到窄查询中
+ if len(model_choices) > 0:
+ if narrow_queries is None:
+ narrow_queries = set()
+
+ narrow_queries.add(' OR '.join(
+ ['%s:%s' % (DJANGO_CT, rm) for rm in model_choices]))
+
+ narrow_searcher = None
+
+ # 处理窄查询
+ if narrow_queries is not None:
+ # 可能很昂贵?在Whoosh中没有看到其他方法...
+ narrow_searcher = self.index.searcher()
+
+ for nq in narrow_queries:
+ recent_narrowed_results = narrow_searcher.search(
+ self.parser.parse(force_str(nq)), limit=None)
+
+ if len(recent_narrowed_results) <= 0:
+ return {
+ 'results': [],
+ 'hits': 0,
+ }
+
+ if narrowed_results:
+ narrowed_results.filter(recent_narrowed_results)
+ else:
+ narrowed_results = recent_narrowed_results
+
+ self.index = self.index.refresh()
+
+ # 如果索引中有文档,执行搜索
+ if self.index.doc_count():
+ searcher = self.index.searcher() # 创建搜索器
+ parsed_query = self.parser.parse(query_string) # 解析查询
+
+ # 如果查询无效/包含停用词,优雅地恢复
+ if parsed_query is None:
+ return {
+ 'results': [],
+ 'hits': 0,
+ }
+
+ page_num, page_length = self.calculate_page(
+ start_offset, end_offset)
+
+ search_kwargs = {
+ 'pagelen': page_length, # 页面长度
+ 'sortedby': sort_by, # 排序字段
+ 'reverse': reverse, # 是否反转
+ }
+
+ # 处理结果已被窄化的情况
+ if narrowed_results is not None:
+ search_kwargs['filter'] = narrowed_results
+
+ try:
+ # 执行分页搜索
+ raw_page = searcher.search_page(
+ parsed_query,
+ page_num,
+ **search_kwargs
+ )
+ except ValueError:
+ if not self.silently_fail:
+ raise
+
+ return {
+ 'results': [],
+ 'hits': 0,
+ 'spelling_suggestion': None,
+ }
+
+ # 由于Whoosh 2.5.1的问题,如果请求的页码过高,它会返回错误的页面
+ if raw_page.pagenum < page_num:
+ return {
+ 'results': [],
+ 'hits': 0,
+ 'spelling_suggestion': None,
+ }
+
+ # 处理搜索结果
+ results = self._process_results(
+ raw_page,
+ highlight=highlight,
+ query_string=query_string,
+ spelling_query=spelling_query,
+ result_class=result_class)
+ searcher.close() # 关闭搜索器
+
+ if hasattr(narrow_searcher, 'close'):
+ narrow_searcher.close()
+
+ return results
+ else:
+ # 如果没有文档,处理拼写建议
+ if self.include_spelling:
+ if spelling_query:
+ spelling_suggestion = self.create_spelling_suggestion(
+ spelling_query)
+ else:
+ spelling_suggestion = self.create_spelling_suggestion(
+ query_string)
+ else:
+ spelling_suggestion = None
+
+ return {
+ 'results': [],
+ 'hits': 0,
+ 'spelling_suggestion': spelling_suggestion,
+ }
+
+def more_like_this(
+ self,
+ model_instance,
+ additional_query_string=None,
+ start_offset=0,
+ end_offset=None,
+ models=None,
+ limit_to_registered_models=None,
+ result_class=None,
+ **kwargs):
+ """查找相似文档(基于内容的推荐)"""
+ if not self.setup_complete:
+ self.setup()
+
+ # 延迟模型会有不同的类名("RealClass_Deferred_fieldname"),不在我们的注册表中
+ model_klass = model_instance._meta.concrete_model # 获取具体模型类
+
+ field_name = self.content_field_name
+ narrow_queries = set()
+ narrowed_results = None
+ self.index = self.index.refresh()
+
+ if limit_to_registered_models is None:
+ limit_to_registered_models = getattr(
+ settings, 'HAYSTACK_LIMIT_TO_REGISTERED_MODELS', True)
+
+ # 构建模型选择列表
+ if models and len(models):
+ model_choices = sorted(get_model_ct(model) for model in models)
+ elif limit_to_registered_models:
+ # 使用窄查询,将结果限制为当前路由器处理的模型
+ model_choices = self.build_models_list()
+ else:
+ model_choices = []
+
+ if len(model_choices) > 0:
+ if narrow_queries is None:
+ narrow_queries = set()
+
+ narrow_queries.add(' OR '.join(
+ ['%s:%s' % (DJANGO_CT, rm) for rm in model_choices]))
+
+ # 添加额外的查询字符串
+ if additional_query_string and additional_query_string != '*':
+ narrow_queries.add(additional_query_string)
+
+ narrow_searcher = None
+
+ # 处理窄查询
+ if narrow_queries is not None:
+ # 可能很昂贵?在Whoosh中没有看到其他方法...
+ narrow_searcher = self.index.searcher()
+
+ for nq in narrow_queries:
+ recent_narrowed_results = narrow_searcher.search(
+ self.parser.parse(force_str(nq)), limit=None)
+
+ if len(recent_narrowed_results) <= 0:
+ return {
+ 'results': [],
+ 'hits': 0,
+ }
+
+ if narrowed_results:
+ narrowed_results.filter(recent_narrowed_results)
+ else:
+ narrowed_results = recent_narrowed_results
+
+ page_num, page_length = self.calculate_page(start_offset, end_offset)
+
+ self.index = self.index.refresh()
+ raw_results = EmptyResults() # 空结果集
+
+ if self.index.doc_count():
+ # 构建查询:查找指定模型实例
+ query = "%s:%s" % (ID, get_identifier(model_instance))
+ searcher = self.index.searcher()
+ parsed_query = self.parser.parse(query)
+ results = searcher.search(parsed_query)
+
+ if len(results):
+ # 使用Whoosh的more_like_this功能查找相似文档
+ raw_results = results[0].more_like_this(
+ field_name, top=end_offset)
+
+ # 处理结果已被窄化的情况
+ if narrowed_results is not None and hasattr(raw_results, 'filter'):
+ raw_results.filter(narrowed_results)
+
+ try:
+ raw_page = ResultsPage(raw_results, page_num, page_length)
+ except ValueError:
+ if not self.silently_fail:
+ raise
+
+ return {
+ 'results': [],
+ 'hits': 0,
+ 'spelling_suggestion': None,
+ }
+
+ # 由于Whoosh 2.5.1的问题,如果请求的页码过高,它会返回错误的页面
+ if raw_page.pagenum < page_num:
+ return {
+ 'results': [],
+ 'hits': 0,
+ 'spelling_suggestion': None,
+ }
+
+ results = self._process_results(raw_page, result_class=result_class)
+ searcher.close()
+
+ if hasattr(narrow_searcher, 'close'):
+ narrow_searcher.close()
+
+ return results
+
+def _process_results(
+ self,
+ raw_page,
+ highlight=False,
+ query_string='',
+ spelling_query=None,
+ result_class=None):
+ """处理原始搜索结果,转换为SearchResult对象"""
+ from haystack import connections
+ results = []
+
+ # 在切片之前先获取命中数很重要,否则可能导致分页失败
+ hits = len(raw_page)
+
+ if result_class is None:
+ result_class = SearchResult
+
+ facets = {} # 分面数据(Whoosh不支持)
+ spelling_suggestion = None # 拼写建议
+ unified_index = connections[self.connection_alias].get_unified_index()
+ indexed_models = unified_index.get_indexed_models() # 已索引的模型
+
+ # 处理每个搜索结果
+ for doc_offset, raw_result in enumerate(raw_page):
+ score = raw_page.score(doc_offset) or 0 # 相关性分数
+ app_label, model_name = raw_result[DJANGO_CT].split('.') # 解析应用标签和模型名
+ additional_fields = {}
+ model = haystack_get_model(app_label, model_name) # 获取模型类
+
+ if model and model in indexed_models:
+ # 处理每个字段的值
+ for key, value in raw_result.items():
+ index = unified_index.get_index(model)
+ string_key = str(key)
+
+ if string_key in index.fields and hasattr(
+ index.fields[string_key], 'convert'):
+ # 由于KEYWORD字段的性质需要特殊处理
+ if index.fields[string_key].is_multivalued: # 多值字段
+ if value is None or len(value) == 0:
+ additional_fields[string_key] = []
+ else:
+ additional_fields[string_key] = value.split(',') # 逗号分隔的值
+ else:
+ additional_fields[string_key] = index.fields[string_key].convert(
+ value) # 使用字段的转换方法
+ else:
+ additional_fields[string_key] = self._to_python(value) # 转换为Python类型
+
+ # 删除内部使用的字段
+ del (additional_fields[DJANGO_CT])
+ del (additional_fields[DJANGO_ID])
+
+ # 如果需要高亮显示
+ if highlight:
+ sa = StemmingAnalyzer() # 词干分析器
+ formatter = WhooshHtmlFormatter('em') # HTML格式化器
+ terms = [token.text for token in sa(query_string)] # 提取查询词
+
+ # 执行高亮
+ whoosh_result = whoosh_highlight(
+ additional_fields.get(self.content_field_name), # 内容字段
+ terms, # 查询词
+ sa, # 分析器
+ ContextFragmenter(), # 上下文片段生成器
+ formatter # 格式化器
+ )
+ additional_fields['highlighted'] = {
+ self.content_field_name: [whoosh_result], # 高亮结果
+ }
+
+ # 创建搜索结果对象
+ result = result_class(
+ app_label,
+ model_name,
+ raw_result[DJANGO_ID], # 对象ID
+ score, # 相关性分数
+ **additional_fields) # 额外字段
+ results.append(result)
+ else:
+ hits -= 1 # 减少命中数(如果模型不在索引中)
+
+ # 生成拼写建议
+ if self.include_spelling:
+ if spelling_query:
+ spelling_suggestion = self.create_spelling_suggestion(
+ spelling_query)
+ else:
+ spelling_suggestion = self.create_spelling_suggestion(
+ query_string)
+
+ return {
+ 'results': results, # 搜索结果列表
+ 'hits': hits, # 命中数
+ 'facets': facets, # 分面数据
+ 'spelling_suggestion': spelling_suggestion, # 拼写建议
+ }
+
+def create_spelling_suggestion(self, query_string):
+ """创建拼写建议"""
+ spelling_suggestion = None
+ reader = self.index.reader() # 索引读取器
+ corrector = reader.corrector(self.content_field_name) # 拼写校正器
+ cleaned_query = force_str(query_string)
+
+ if not query_string:
+ return spelling_suggestion
+
+ # 清理字符串:移除保留词
+ for rev_word in self.RESERVED_WORDS:
+ cleaned_query = cleaned_query.replace(rev_word, '')
+
+ # 清理字符串:移除保留字符
+ for rev_char in self.RESERVED_CHARACTERS:
+ cleaned_query = cleaned_query.replace(rev_char, '')
+
+ # 分解查询词
+ query_words = cleaned_query.split()
+ suggested_words = []
+
+ # 为每个词获取拼写建议
+ for word in query_words:
+ suggestions = corrector.suggest(word, limit=1) # 获取第一个建议
+
+ if len(suggestions) > 0:
+ suggested_words.append(suggestions[0]) # 使用建议词
+ else:
+ suggested_words.append(word) # 如果没有建议,使用原词
+
+ spelling_suggestion = ' '.join(suggested_words)
+ return spelling_suggestion
+
+def _from_python(self, value):
+ """
+ 将Python值转换为Whoosh使用的字符串
+
+ 代码源自pysolr
+ """
+ if hasattr(value, 'strftime'): # 日期时间对象
+ if not hasattr(value, 'hour'): # 如果没有时间部分
+ value = datetime(value.year, value.month, value.day, 0, 0, 0) # 设置为午夜
+ elif isinstance(value, bool): # 布尔值
+ if value:
+ value = 'true'
+ else:
+ value = 'false'
+ elif isinstance(value, (list, tuple)): # 列表或元组
+ value = u','.join([force_str(v) for v in value]) # 转换为逗号分隔的字符串
+ elif isinstance(value, (six.integer_types, float)): # 数字类型
+ # 保持原样
+ pass
+ else:
+ value = force_str(value) # 转换为字符串
+ return value
+
+def _to_python(self, value):
+ """
+ 将Whoosh的值转换为原生Python值
+
+ 移植自pysolr中的相同方法,因为它们处理数据的方式相同
+ """
+ if value == 'true': # 布尔真值
+ return True
+ elif value == 'false': # 布尔假值
+ return False
+
+ if value and isinstance(value, six.string_types):
+ possible_datetime = DATETIME_REGEX.search(value) # 尝试匹配日期时间格式
+
+ if possible_datetime:
+ date_values = possible_datetime.groupdict()
+
+ for dk, dv in date_values.items():
+ date_values[dk] = int(dv) # 转换为整数
+
+ return datetime( # 返回datetime对象
+ date_values['year'],
+ date_values['month'],
+ date_values['day'],
+ date_values['hour'],
+ date_values['minute'],
+ date_values['second'])
+
+ try:
+ # 尝试使用json加载值
+ converted_value = json.loads(value)
+
+ # 尝试处理大多数内置类型
+ if isinstance(
+ converted_value,
+ (list,
+ tuple,
+ set,
+ dict,
+ six.integer_types,
+ float,
+ complex)):
+ return converted_value
+ except BaseException:
+ # 如果失败(SyntaxError或其同类)或者我们不信任它,继续处理
+ pass
+
+ return value # 返回原始值
+
+
+class WhooshSearchQuery(BaseSearchQuery):
+ """Whoosh搜索查询构建器"""
+
+ def _convert_datetime(self, date):
+ """转换日期时间为Whoosh格式"""
+ if hasattr(date, 'hour'): # 包含时间的日期时间
+ return force_str(date.strftime('%Y%m%d%H%M%S'))
+ else: # 仅日期
+ return force_str(date.strftime('%Y%m%d000000'))
+
+ def clean(self, query_fragment):
+ """
+ 在将值呈现给后端之前,提供清理用户输入的机制
+
+ Whoosh 1.X在这里有所不同,不再使用反斜杠转义保留字符。
+ 相反,应该引用整个单词。
+ """
+ words = query_fragment.split()
+ cleaned_words = []
+
+ for word in words:
+ if word in self.backend.RESERVED_WORDS: # 保留词
+ word = word.replace(word, word.lower()) # 转换为小写
+
+ for char in self.backend.RESERVED_CHARACTERS: # 保留字符
+ if char in word:
+ word = "'%s'" % word # 用引号包围单词
+ break
+
+ cleaned_words.append(word)
+
+ return ' '.join(cleaned_words) # 重新组合为字符串
+
+ def build_query_fragment(self, field, filter_type, value):
+ """构建查询片段"""
+ from haystack import connections
+ query_frag = ''
+ is_datetime = False
+
+ if not hasattr(value, 'input_type_name'):
+ # 处理ValuesListQuerySet...
+ if hasattr(value, 'values_list'):
+ value = list(value)
+
+ if hasattr(value, 'strftime'):
+ is_datetime = True
+
+ if isinstance(value, six.string_types) and value != ' ':
+ # 不是InputType,假设是Clean
+ value = Clean(value)
+ else:
+ value = PythonData(value)
+
+ # 使用InputType准备查询
+ prepared_value = value.prepare(self)
+
+ if not isinstance(prepared_value, (set, list, tuple)):
+ # 然后根据需要将我们得到的任何内容转换为pysolr需要的格式
+ prepared_value = self.backend._from_python(prepared_value)
+
+ # 'content'是一个特殊的保留词,类似于Django ORM层中的'pk'
+ # 它表示"没有特殊字段"
+ if field == 'content':
+ index_fieldname = '' # 无字段名
+ else:
+ index_fieldname = u'%s:' % connections[self._using].get_unified_index(
+ ).get_index_fieldname(field) # 获取索引字段名
+
+ # 过滤器类型映射
+ filter_types = {
+ 'content': '%s', # 内容搜索
+ 'contains': '*%s*', # 包含
+ 'endswith': "*%s", # 以...结尾
+ 'startswith': "%s*", # 以...开头
+ 'exact': '%s', # 精确匹配
+ 'gt': "{%s to}", # 大于
+ 'gte': "[%s to]", # 大于等于
+ 'lt': "{to %s}", # 小于
+ 'lte': "[to %s]", # 小于等于
+ 'fuzzy': u'%s~', # 模糊搜索
+ }
+
+ if value.post_process is False: # 不进行后处理
+ query_frag = prepared_value
+ else:
+ if filter_type in [
+ 'content',
+ 'contains',
+ 'startswith',
+ 'endswith',
+ 'fuzzy']:
+ if value.input_type_name == 'exact': # 精确输入类型
+ query_frag = prepared_value
+ else:
+ # 遍历术语并将每个术语的转换形式合并到查询中
+ terms = []
+
+ if isinstance(prepared_value, six.string_types):
+ possible_values = prepared_value.split(' ') # 按空格分割
+ else:
+ if is_datetime is True:
+ prepared_value = self._convert_datetime(
+ prepared_value) # 转换日期时间
+
+ possible_values = [prepared_value]
+
+ for possible_value in possible_values:
+ terms.append(
+ filter_types[filter_type] %
+ self.backend._from_python(possible_value)) # 应用过滤器
+
+ if len(terms) == 1:
+ query_frag = terms[0]
+ else:
+ query_frag = u"(%s)" % " AND ".join(terms) # 使用AND连接
+ elif filter_type == 'in': # 在...中
+ in_options = []
+
+ for possible_value in prepared_value:
+ is_datetime = False
+
+ if hasattr(possible_value, 'strftime'):
+ is_datetime = True
+
+ pv = self.backend._from_python(possible_value)
+
+ if is_datetime is True:
+ pv = self._convert_datetime(pv) # 转换日期时间
+
+ if isinstance(pv, six.string_types) and not is_datetime:
+ in_options.append('"%s"' % pv) # 字符串用引号包围
+ else:
+ in_options.append('%s' % pv)
+
+ query_frag = "(%s)" % " OR ".join(in_options) # 使用OR连接
+ elif filter_type == 'range': # 范围查询
+ start = self.backend._from_python(prepared_value[0]) # 起始值
+ end = self.backend._from_python(prepared_value[1]) # 结束值
+
+ if hasattr(prepared_value[0], 'strftime'):
+ start = self._convert_datetime(start) # 转换起始日期时间
+
+ if hasattr(prepared_value[1], 'strftime'):
+ end = self._convert_datetime(end) # 转换结束日期时间
+
+ query_frag = u"[%s to %s]" % (start, end) # 范围格式
+ elif filter_type == 'exact': # 精确匹配
+ if value.input_type_name == 'exact':
+ query_frag = prepared_value
+ else:
+ prepared_value = Exact(prepared_value).prepare(self) # 准备精确值
+ query_frag = filter_types[filter_type] % prepared_value
+ else:
+ if is_datetime is True:
+ prepared_value = self._convert_datetime(prepared_value) # 转换日期时间
+
+ query_frag = filter_types[filter_type] % prepared_value # 应用过滤器
+
+ # 添加括号(如果不是原始值)
+ if len(query_frag) and not isinstance(value, Raw):
+ if not query_frag.startswith('(') and not query_frag.endswith(')'):
+ query_frag = "(%s)" % query_frag
+
+ return u"%s%s" % (index_fieldname, query_frag) # 组合字段名和查询片段
+
+ # 注释掉的代码:处理'in'和'range'之外的情况
+ # if not filter_type in ('in', 'range'):
+ # # 'in'是一个特殊情况,因为我们不想将有效的列表/元组转换为字符串
+ # # 推迟处理...
+ # value = self.backend._from_python(value)
+
+
+class WhooshEngine(BaseEngine):
+ """Whoosh搜索引擎"""
+ backend = WhooshSearchBackend # 指定后端类
+ query = WhooshSearchQuery # 指定查询类
\ No newline at end of file
diff --git a/doc/第7周代码注释/djangoblog/whoosh_index/MAIN_42r12l6ettujonsl.seg b/doc/第7周代码注释/djangoblog/whoosh_index/MAIN_42r12l6ettujonsl.seg
new file mode 100644
index 00000000..5474081b
Binary files /dev/null and b/doc/第7周代码注释/djangoblog/whoosh_index/MAIN_42r12l6ettujonsl.seg differ
diff --git a/doc/第7周代码注释/djangoblog/whoosh_index/MAIN_5pgjjy1dha5pf9z3.seg b/doc/第7周代码注释/djangoblog/whoosh_index/MAIN_5pgjjy1dha5pf9z3.seg
new file mode 100644
index 00000000..aa44dffa
Binary files /dev/null and b/doc/第7周代码注释/djangoblog/whoosh_index/MAIN_5pgjjy1dha5pf9z3.seg differ
diff --git a/doc/第7周代码注释/djangoblog/whoosh_index/MAIN_9gx20noi535vbqnt.seg b/doc/第7周代码注释/djangoblog/whoosh_index/MAIN_9gx20noi535vbqnt.seg
new file mode 100644
index 00000000..14554f18
Binary files /dev/null and b/doc/第7周代码注释/djangoblog/whoosh_index/MAIN_9gx20noi535vbqnt.seg differ
diff --git a/doc/第7周代码注释/djangoblog/whoosh_index/MAIN_WRITELOCK b/doc/第7周代码注释/djangoblog/whoosh_index/MAIN_WRITELOCK
new file mode 100644
index 00000000..e69de29b
diff --git a/doc/第7周代码注释/djangoblog/whoosh_index/MAIN_cjpg11pn6uobbc9d.seg b/doc/第7周代码注释/djangoblog/whoosh_index/MAIN_cjpg11pn6uobbc9d.seg
new file mode 100644
index 00000000..76be9799
Binary files /dev/null and b/doc/第7周代码注释/djangoblog/whoosh_index/MAIN_cjpg11pn6uobbc9d.seg differ
diff --git a/doc/第7周代码注释/djangoblog/whoosh_index/MAIN_qi5zgb3e8fsmpwaw.seg b/doc/第7周代码注释/djangoblog/whoosh_index/MAIN_qi5zgb3e8fsmpwaw.seg
new file mode 100644
index 00000000..6f0e349f
Binary files /dev/null and b/doc/第7周代码注释/djangoblog/whoosh_index/MAIN_qi5zgb3e8fsmpwaw.seg differ
diff --git a/doc/第7周代码注释/djangoblog/whoosh_index/_MAIN_60.toc b/doc/第7周代码注释/djangoblog/whoosh_index/_MAIN_60.toc
new file mode 100644
index 00000000..7acbe53a
Binary files /dev/null and b/doc/第7周代码注释/djangoblog/whoosh_index/_MAIN_60.toc differ
diff --git a/doc/第7周代码注释/djangoblog/wsgi.py b/doc/第7周代码注释/djangoblog/wsgi.py
new file mode 100644
index 00000000..50d18e79
--- /dev/null
+++ b/doc/第7周代码注释/djangoblog/wsgi.py
@@ -0,0 +1,20 @@
+"""
+WSGI config for djangoblog project.
+
+它将WSGI可调用对象暴露为名为``application``的模块级变量。
+
+有关此文件的更多信息,请参阅:
+https://docs.djangoproject.com/en/1.10/howto/deployment/wsgi/
+"""
+
+import os
+
+# 导入Django的WSGI应用获取函数
+from django.core.wsgi import get_wsgi_application
+
+# 设置Django的默认设置模块
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "djangoblog.settings")
+
+# 获取WSGI应用实例
+# 这个application变量将被WSGI服务器(如Gunicorn、uWSGI)使用来服务Django应用
+application = get_wsgi_application()
\ No newline at end of file
diff --git a/doc/第7周代码注释/oauth/__init__.py b/doc/第7周代码注释/oauth/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/doc/第7周代码注释/oauth/__pycache__/__init__.cpython-312.pyc b/doc/第7周代码注释/oauth/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 00000000..3669ef78
Binary files /dev/null and b/doc/第7周代码注释/oauth/__pycache__/__init__.cpython-312.pyc differ
diff --git a/doc/第7周代码注释/oauth/__pycache__/admin.cpython-312.pyc b/doc/第7周代码注释/oauth/__pycache__/admin.cpython-312.pyc
new file mode 100644
index 00000000..d98229ff
Binary files /dev/null and b/doc/第7周代码注释/oauth/__pycache__/admin.cpython-312.pyc differ
diff --git a/doc/第7周代码注释/oauth/__pycache__/apps.cpython-312.pyc b/doc/第7周代码注释/oauth/__pycache__/apps.cpython-312.pyc
new file mode 100644
index 00000000..e184d29f
Binary files /dev/null and b/doc/第7周代码注释/oauth/__pycache__/apps.cpython-312.pyc differ
diff --git a/doc/第7周代码注释/oauth/__pycache__/forms.cpython-312.pyc b/doc/第7周代码注释/oauth/__pycache__/forms.cpython-312.pyc
new file mode 100644
index 00000000..4db809ad
Binary files /dev/null and b/doc/第7周代码注释/oauth/__pycache__/forms.cpython-312.pyc differ
diff --git a/doc/第7周代码注释/oauth/__pycache__/models.cpython-312.pyc b/doc/第7周代码注释/oauth/__pycache__/models.cpython-312.pyc
new file mode 100644
index 00000000..e0091312
Binary files /dev/null and b/doc/第7周代码注释/oauth/__pycache__/models.cpython-312.pyc differ
diff --git a/doc/第7周代码注释/oauth/__pycache__/oauthmanager.cpython-312.pyc b/doc/第7周代码注释/oauth/__pycache__/oauthmanager.cpython-312.pyc
new file mode 100644
index 00000000..248bc4c7
Binary files /dev/null and b/doc/第7周代码注释/oauth/__pycache__/oauthmanager.cpython-312.pyc differ
diff --git a/doc/第7周代码注释/oauth/__pycache__/urls.cpython-312.pyc b/doc/第7周代码注释/oauth/__pycache__/urls.cpython-312.pyc
new file mode 100644
index 00000000..403bd927
Binary files /dev/null and b/doc/第7周代码注释/oauth/__pycache__/urls.cpython-312.pyc differ
diff --git a/doc/第7周代码注释/oauth/__pycache__/views.cpython-312.pyc b/doc/第7周代码注释/oauth/__pycache__/views.cpython-312.pyc
new file mode 100644
index 00000000..8a6266e8
Binary files /dev/null and b/doc/第7周代码注释/oauth/__pycache__/views.cpython-312.pyc differ
diff --git a/doc/第7周代码注释/oauth/admin.py b/doc/第7周代码注释/oauth/admin.py
new file mode 100644
index 00000000..6f52fd50
--- /dev/null
+++ b/doc/第7周代码注释/oauth/admin.py
@@ -0,0 +1,93 @@
+import logging
+
+from django.contrib import admin
+# 注册模型到admin站点
+from django.urls import reverse
+from django.utils.html import format_html
+
+# 初始化日志记录器,用于记录当前模块的日志信息
+logger = logging.getLogger(__name__)
+
+
+class OAuthUserAdmin(admin.ModelAdmin):
+ """
+ OAuthUser模型的Admin管理类,用于在Django admin后台管理第三方登录用户信息
+ """
+ # 配置搜索字段,支持通过昵称和邮箱搜索
+ search_fields = ('nickname', 'email')
+ # 配置每页显示20条记录
+ list_per_page = 20
+ # 配置列表页显示的字段,包括自定义字段
+ list_display = (
+ 'id',
+ 'nickname', # 昵称
+ 'link_to_usermodel', # 关联的本地用户(自定义链接字段)
+ 'show_user_image', # 用户头像(自定义图片显示字段)
+ 'type', # 第三方平台类型
+ 'email', # 邮箱
+ )
+ # 配置列表页中可点击跳转编辑页的字段
+ list_display_links = ('id', 'nickname')
+ # 配置列表页的过滤条件
+ list_filter = ('author', 'type',)
+ # 初始只读字段列表(后续会动态扩展)
+ readonly_fields = []
+
+ def get_readonly_fields(self, request, obj=None):
+ """
+ 重写只读字段方法,当编辑对象时,将所有字段设为只读
+ (新增时obj为None,不生效;编辑时obj存在,所有字段只读)
+ """
+ if obj: # 编辑已有对象时
+ # 合并初始只读字段 + 模型所有普通字段 + 所有多对多字段
+ return list(self.readonly_fields) + \
+ [field.name for field in obj._meta.fields] + \
+ [field.name for field in obj._meta.many_to_many]
+ return self.readonly_fields # 新增时使用初始只读字段
+
+ def has_add_permission(self, request):
+ """
+ 禁用在admin后台手动添加OAuthUser的权限(第三方用户信息应通过登录自动创建)
+ """
+ return False
+
+ def link_to_usermodel(self, obj):
+ """
+ 自定义列表字段:生成关联本地用户的admin编辑页链接
+ """
+ if obj.author: # 如果存在关联的本地用户
+ # 获取关联用户模型的app标签和模型名称
+ info = (obj.author._meta.app_label, obj.author._meta.model_name)
+ # 反转生成用户编辑页的URL
+ link = reverse('admin:%s_%s_change' % info, args=(obj.author.id,))
+ # 返回带链接的HTML(使用format_html确保安全渲染)
+ return format_html(
+ u'%s ' %
+ (link, obj.author.nickname if obj.author.nickname else obj.author.email)
+ # 显示昵称,若昵称不存在则显示邮箱
+ )
+ return None # 无关联用户时返回空
+
+ def show_user_image(self, obj):
+ """
+ 自定义列表字段:显示用户头像图片
+ """
+ img = obj.picture # 获取头像图片URL
+ if img: # 若头像存在
+ # 返回图片HTML标签,限制宽高为50px
+ return format_html(u' ' % (img))
+ return None # 无头像时返回空
+
+ # 定义自定义字段在列表页的显示名称
+ link_to_usermodel.short_description = '用户'
+ show_user_image.short_description = '用户头像'
+
+
+class OAuthConfigAdmin(admin.ModelAdmin):
+ """
+ OAuthConfig模型的Admin管理类,用于在Django admin后台管理第三方登录配置信息
+ """
+ # 配置列表页显示的字段
+ list_display = ('type', 'appkey', 'appsecret', 'is_enable')
+ # 配置列表页的过滤条件(按第三方平台类型过滤)
+ list_filter = ('type',)
\ No newline at end of file
diff --git a/doc/第7周代码注释/oauth/apps.py b/doc/第7周代码注释/oauth/apps.py
new file mode 100644
index 00000000..e6924786
--- /dev/null
+++ b/doc/第7周代码注释/oauth/apps.py
@@ -0,0 +1,11 @@
+# 导入Django的AppConfig类,用于配置应用的基本信息
+from django.apps import AppConfig
+
+
+class OauthConfig(AppConfig):
+ """
+ oauth应用的配置类,用于定义应用的核心信息
+ 继承自Django的AppConfig,是Django应用配置的标准方式
+ """
+ # 应用的名称,Django通过该名称识别和管理当前应用
+ name = 'oauth'
\ No newline at end of file
diff --git a/doc/第7周代码注释/oauth/forms.py b/doc/第7周代码注释/oauth/forms.py
new file mode 100644
index 00000000..dee74516
--- /dev/null
+++ b/doc/第7周代码注释/oauth/forms.py
@@ -0,0 +1,26 @@
+# 导入Django表单基础类和控件模块
+from django.contrib.auth.forms import forms
+from django.forms import widgets
+
+
+class RequireEmailForm(forms.Form):
+ """
+ 用于收集用户电子邮箱的表单类
+ 通常在OAuth第三方登录时,若用户未提供邮箱信息,用于补充收集
+ """
+ # 电子邮箱字段:使用EmailField进行格式验证,标签为"电子邮箱",且为必填项
+ email = forms.EmailField(label='电子邮箱', required=True)
+ # OAuth用户ID字段:隐藏控件(HiddenInput),用于关联第三方登录用户,非必填
+ oauthid = forms.IntegerField(widget=forms.HiddenInput, required=False)
+
+ def __init__(self, *args, **kwargs):
+ """
+ 重写初始化方法,自定义表单字段的控件属性
+ 主要用于设置邮箱输入框的占位符和CSS样式类
+ """
+ # 调用父类的初始化方法,确保表单正常初始化
+ super(RequireEmailForm, self).__init__(*args, **kwargs)
+ # 为email字段设置自定义控件:EmailInput
+ # 添加placeholder提示文本和form-control的CSS类(通常用于Bootstrap样式)
+ self.fields['email'].widget = widgets.EmailInput(
+ attrs={'placeholder': "email", "class": "form-control"})
\ No newline at end of file
diff --git a/doc/第7周代码注释/oauth/migrations/0001_initial.py b/doc/第7周代码注释/oauth/migrations/0001_initial.py
new file mode 100644
index 00000000..ac105f03
--- /dev/null
+++ b/doc/第7周代码注释/oauth/migrations/0001_initial.py
@@ -0,0 +1,84 @@
+# Generated by Django 4.1.7 on 2023-03-07 09:53
+# 导入Django相关模块
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+import django.utils.timezone
+
+
+class Migration(migrations.Migration):
+ """迁移类,定义数据库表结构的变更操作"""
+
+ # 标识为初始迁移(首次创建表结构)
+ initial = True
+
+ # 依赖的其他迁移,此处依赖于用户模型的迁移
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ]
+
+ # 具体的迁移操作列表
+ operations = [
+ # 创建OAuthConfig模型(存储OAuth第三方登录的配置信息)
+ migrations.CreateModel(
+ name='OAuthConfig',
+ fields=[
+ # 自增主键ID
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ # OAuth类型字段,限定可选值为常见第三方平台,默认值为'a'(可能需要后续调整)
+ ('type', models.CharField(choices=[('weibo', '微博'), ('google', '谷歌'), ('github', 'GitHub'), ('facebook', 'FaceBook'), ('qq', 'QQ')], default='a', max_length=10, verbose_name='类型')),
+ # 应用AppKey字段
+ ('appkey', models.CharField(max_length=200, verbose_name='AppKey')),
+ # 应用AppSecret字段
+ ('appsecret', models.CharField(max_length=200, verbose_name='AppSecret')),
+ # 回调地址字段,默认值为百度首页
+ ('callback_url', models.CharField(default='http://www.baidu.com', max_length=200, verbose_name='回调地址')),
+ # 是否启用该OAuth配置的布尔字段,默认启用
+ ('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': 'oauth配置', # 单数显示名称
+ 'verbose_name_plural': 'oauth配置', # 复数显示名称
+ 'ordering': ['-created_time'], # 排序方式:按创建时间倒序
+ },
+ ),
+ # 创建OAuthUser模型(存储通过OAuth登录的用户信息)
+ migrations.CreateModel(
+ name='OAuthUser',
+ fields=[
+ # 自增主键ID
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ # 第三方平台的openid
+ ('openid', models.CharField(max_length=50)),
+ # 第三方平台的用户昵称
+ ('nickname', models.CharField(max_length=50, verbose_name='昵称')),
+ # 访问令牌,可为空
+ ('token', models.CharField(blank=True, max_length=150, null=True)),
+ # 头像图片地址,可为空
+ ('picture', models.CharField(blank=True, max_length=350, null=True)),
+ # OAuth类型(对应第三方平台)
+ ('type', models.CharField(max_length=50)),
+ # 邮箱地址,可为空
+ ('email', models.CharField(blank=True, max_length=50, null=True)),
+ # 额外元数据,文本类型,可为空
+ ('metadata', models.TextField(blank=True, null=True)),
+ # 创建时间字段,默认值为当前时间
+ ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
+ # 最后修改时间字段,默认值为当前时间
+ ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
+ # 关联到本地用户模型的外键,可为空,级联删除
+ ('author', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='用户')),
+ ],
+ # 模型元数据配置
+ options={
+ 'verbose_name': 'oauth用户', # 单数显示名称
+ 'verbose_name_plural': 'oauth用户', # 复数显示名称
+ 'ordering': ['-created_time'], # 排序方式:按创建时间倒序
+ },
+ ),
+ ]
\ No newline at end of file
diff --git a/doc/第7周代码注释/oauth/migrations/0002_alter_oauthconfig_options_alter_oauthuser_options_and_more.py b/doc/第7周代码注释/oauth/migrations/0002_alter_oauthconfig_options_alter_oauthuser_options_and_more.py
new file mode 100644
index 00000000..a16d1eb1
--- /dev/null
+++ b/doc/第7周代码注释/oauth/migrations/0002_alter_oauthconfig_options_alter_oauthuser_options_and_more.py
@@ -0,0 +1,111 @@
+# Generated by Django 4.2.5 on 2023-09-06 13:13
+# 导入Django配置、数据库迁移相关模块
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+import django.utils.timezone
+
+
+class Migration(migrations.Migration):
+ """迁移类,定义对OAuth相关模型的结构修改操作"""
+
+ # 依赖的迁移:依赖于用户模型的迁移和oauth应用的初始迁移(0001_initial)
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ('oauth', '0001_initial'),
+ ]
+
+ # 具体的迁移操作列表
+ operations = [
+ # 修改OAuthConfig模型的元选项
+ migrations.AlterModelOptions(
+ name='oauthconfig',
+ # 排序方式改为按creation_time倒序;显示名称保持为"oauth配置"
+ options={'ordering': ['-creation_time'], 'verbose_name': 'oauth配置', 'verbose_name_plural': 'oauth配置'},
+ ),
+ # 修改OAuthUser模型的元选项
+ migrations.AlterModelOptions(
+ name='oauthuser',
+ # 排序方式改为按creation_time倒序;显示名称改为英文"oauth user"
+ options={'ordering': ['-creation_time'], 'verbose_name': 'oauth user', 'verbose_name_plural': 'oauth user'},
+ ),
+ # 移除OAuthConfig模型中的created_time字段(旧时间字段)
+ migrations.RemoveField(
+ model_name='oauthconfig',
+ name='created_time',
+ ),
+ # 移除OAuthConfig模型中的last_mod_time字段(旧修改时间字段)
+ migrations.RemoveField(
+ model_name='oauthconfig',
+ name='last_mod_time',
+ ),
+ # 移除OAuthUser模型中的created_time字段(旧时间字段)
+ migrations.RemoveField(
+ model_name='oauthuser',
+ name='created_time',
+ ),
+ # 移除OAuthUser模型中的last_mod_time字段(旧修改时间字段)
+ migrations.RemoveField(
+ model_name='oauthuser',
+ name='last_mod_time',
+ ),
+ # 为OAuthConfig模型添加creation_time字段(新创建时间字段)
+ migrations.AddField(
+ model_name='oauthconfig',
+ name='creation_time',
+ field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
+ ),
+ # 为OAuthConfig模型添加last_modify_time字段(新修改时间字段)
+ migrations.AddField(
+ model_name='oauthconfig',
+ name='last_modify_time',
+ field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'),
+ ),
+ # 为OAuthUser模型添加creation_time字段(新创建时间字段)
+ migrations.AddField(
+ model_name='oauthuser',
+ name='creation_time',
+ field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
+ ),
+ # 为OAuthUser模型添加last_modify_time字段(新修改时间字段)
+ migrations.AddField(
+ model_name='oauthuser',
+ name='last_modify_time',
+ field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'),
+ ),
+ # 修改OAuthConfig模型的callback_url字段属性
+ migrations.AlterField(
+ model_name='oauthconfig',
+ name='callback_url',
+ # 默认值从"http://www.baidu.com"改为空字符串;显示名称改为英文"callback url"
+ field=models.CharField(default='', max_length=200, verbose_name='callback url'),
+ ),
+ # 修改OAuthConfig模型的is_enable字段属性
+ migrations.AlterField(
+ model_name='oauthconfig',
+ name='is_enable',
+ # 显示名称改为英文"is enable"
+ field=models.BooleanField(default=True, verbose_name='is enable'),
+ ),
+ # 修改OAuthConfig模型的type字段属性
+ migrations.AlterField(
+ model_name='oauthconfig',
+ name='type',
+ # 选项中的显示文本部分改为英文(如"微博"改为"weibo");显示名称改为英文"type"
+ field=models.CharField(choices=[('weibo', 'weibo'), ('google', 'google'), ('github', 'GitHub'), ('facebook', 'FaceBook'), ('qq', 'QQ')], default='a', max_length=10, verbose_name='type'),
+ ),
+ # 修改OAuthUser模型的author字段属性
+ migrations.AlterField(
+ model_name='oauthuser',
+ name='author',
+ # 显示名称改为英文"author"
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='author'),
+ ),
+ # 修改OAuthUser模型的nickname字段属性
+ migrations.AlterField(
+ model_name='oauthuser',
+ name='nickname',
+ # 显示名称改为英文"nickname"
+ field=models.CharField(max_length=50, verbose_name='nickname'),
+ ),
+ ]
\ No newline at end of file
diff --git a/doc/第7周代码注释/oauth/migrations/0003_alter_oauthuser_nickname.py b/doc/第7周代码注释/oauth/migrations/0003_alter_oauthuser_nickname.py
new file mode 100644
index 00000000..29ef5b65
--- /dev/null
+++ b/doc/第7周代码注释/oauth/migrations/0003_alter_oauthuser_nickname.py
@@ -0,0 +1,23 @@
+# Generated by Django 4.2.7 on 2024-01-26 02:41
+# 导入Django数据库迁移相关模块
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ """迁移类,定义对OAuthUser模型的字段修改操作"""
+
+ # 依赖的迁移:依赖于oauth应用的上一个迁移文件(0002_...)
+ dependencies = [
+ ('oauth', '0002_alter_oauthconfig_options_alter_oauthuser_options_and_more'),
+ ]
+
+ # 具体的迁移操作列表
+ operations = [
+ # 修改OAuthUser模型的nickname字段属性
+ migrations.AlterField(
+ model_name='oauthuser', # 目标模型为OAuthUser
+ name='nickname', # 目标字段为nickname
+ # 字段的verbose_name从'nickname'修改为'nick name',其他属性(如max_length=50)保持不变
+ field=models.CharField(max_length=50, verbose_name='nick name'),
+ ),
+ ]
\ No newline at end of file
diff --git a/doc/第7周代码注释/oauth/migrations/__init__.py b/doc/第7周代码注释/oauth/migrations/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/doc/第7周代码注释/oauth/migrations/__pycache__/0001_initial.cpython-312.pyc b/doc/第7周代码注释/oauth/migrations/__pycache__/0001_initial.cpython-312.pyc
new file mode 100644
index 00000000..77ca92b7
Binary files /dev/null and b/doc/第7周代码注释/oauth/migrations/__pycache__/0001_initial.cpython-312.pyc differ
diff --git a/doc/第7周代码注释/oauth/migrations/__pycache__/0002_alter_oauthconfig_options_alter_oauthuser_options_and_more.cpython-312.pyc b/doc/第7周代码注释/oauth/migrations/__pycache__/0002_alter_oauthconfig_options_alter_oauthuser_options_and_more.cpython-312.pyc
new file mode 100644
index 00000000..1ee61403
Binary files /dev/null and b/doc/第7周代码注释/oauth/migrations/__pycache__/0002_alter_oauthconfig_options_alter_oauthuser_options_and_more.cpython-312.pyc differ
diff --git a/doc/第7周代码注释/oauth/migrations/__pycache__/0003_alter_oauthuser_nickname.cpython-312.pyc b/doc/第7周代码注释/oauth/migrations/__pycache__/0003_alter_oauthuser_nickname.cpython-312.pyc
new file mode 100644
index 00000000..79319326
Binary files /dev/null and b/doc/第7周代码注释/oauth/migrations/__pycache__/0003_alter_oauthuser_nickname.cpython-312.pyc differ
diff --git a/doc/第7周代码注释/oauth/migrations/__pycache__/__init__.cpython-312.pyc b/doc/第7周代码注释/oauth/migrations/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 00000000..14920455
Binary files /dev/null and b/doc/第7周代码注释/oauth/migrations/__pycache__/__init__.cpython-312.pyc differ
diff --git a/doc/第7周代码注释/oauth/models.py b/doc/第7周代码注释/oauth/models.py
new file mode 100644
index 00000000..afe3eef4
--- /dev/null
+++ b/doc/第7周代码注释/oauth/models.py
@@ -0,0 +1,99 @@
+# Create your models here.
+# 导入Django配置、模型相关模块及工具类
+from django.conf import settings
+from django.core.exceptions import ValidationError # 用于数据验证抛出异常
+from django.db import models
+from django.utils.timezone import now # 用于获取当前时间
+from django.utils.translation import gettext_lazy as _ # 用于国际化翻译
+
+
+class OAuthUser(models.Model):
+ """
+ OAuthUser模型:存储通过第三方OAuth登录的用户信息
+ 关联本地用户模型,记录第三方平台的用户标识、昵称、头像等核心信息
+ """
+ # 关联本地用户模型(AUTH_USER_MODEL),可为空,级联删除
+ author = models.ForeignKey(
+ settings.AUTH_USER_MODEL,
+ verbose_name=_('author'), # 国际化字段名:作者/用户
+ blank=True, # 表单提交时可空
+ null=True, # 数据库中可空
+ on_delete=models.CASCADE) # 关联用户删除时,该记录同步删除
+ # 第三方平台用户唯一标识(如微博、GitHub的openid)
+ openid = models.CharField(max_length=50)
+ # 用户昵称,支持国际化显示
+ nickname = models.CharField(max_length=50, verbose_name=_('nick name'))
+ # 第三方平台访问令牌,可为空
+ token = models.CharField(max_length=150, null=True, blank=True)
+ # 用户头像图片URL,可为空
+ picture = models.CharField(max_length=350, blank=True, null=True)
+ # OAuth登录平台类型(如weibo、github),非空
+ type = models.CharField(blank=False, null=False, max_length=50)
+ # 用户邮箱,可为空
+ email = models.CharField(max_length=50, null=True, blank=True)
+ # 额外元数据(存储第三方返回的其他信息),文本类型,可为空
+ metadata = models.TextField(null=True, blank=True)
+ # 创建时间,默认当前时间,支持国际化
+ creation_time = models.DateTimeField(_('creation time'), default=now)
+ # 最后修改时间,默认当前时间,支持国际化
+ last_modify_time = models.DateTimeField(_('last modify time'), default=now)
+
+ def __str__(self):
+ """模型实例的字符串表示:返回用户昵称"""
+ return self.nickname
+
+ class Meta:
+ verbose_name = _('oauth user') # 模型单数显示名(国际化)
+ verbose_name_plural = verbose_name # 模型复数显示名(与单数一致)
+ ordering = ['-creation_time'] # 排序规则:按创建时间倒序
+
+
+class OAuthConfig(models.Model):
+ """
+ OAuthConfig模型:存储第三方OAuth登录的平台配置信息
+ 记录各平台的AppKey、AppSecret、回调地址等核心配置
+ """
+ # 第三方平台类型选项(元组形式,用于choices参数)
+ TYPE = (
+ ('weibo', _('weibo')), # 微博(支持国际化)
+ ('google', _('google')), # 谷歌(支持国际化)
+ ('github', 'GitHub'), # GitHub(固定显示)
+ ('facebook', 'FaceBook'), # FaceBook(固定显示)
+ ('qq', 'QQ'), # QQ(固定显示)
+ )
+ # 平台类型,关联TYPE选项,默认值为'a',支持国际化
+ type = models.CharField(_('type'), max_length=10, choices=TYPE, default='a')
+ # 应用AppKey(第三方平台分配的客户端ID)
+ appkey = models.CharField(max_length=200, verbose_name='AppKey')
+ # 应用AppSecret(第三方平台分配的客户端密钥)
+ appsecret = models.CharField(max_length=200, verbose_name='AppSecret')
+ # 回调地址(OAuth授权成功后的跳转地址),非空,默认空字符串,支持国际化
+ callback_url = models.CharField(
+ max_length=200,
+ verbose_name=_('callback url'),
+ blank=False, # 表单提交时不可空
+ default='')
+ # 是否启用该配置,默认启用,非空
+ is_enable = models.BooleanField(
+ _('is enable'), default=True, blank=False, null=False)
+ # 创建时间,默认当前时间,支持国际化
+ creation_time = models.DateTimeField(_('creation time'), default=now)
+ # 最后修改时间,默认当前时间,支持国际化
+ last_modify_time = models.DateTimeField(_('last modify time'), default=now)
+
+ def clean(self):
+ """数据验证方法:确保同一平台类型的配置不重复"""
+ # 排除当前记录(编辑时),查询是否已存在同类型的配置
+ if OAuthConfig.objects.filter(
+ type=self.type).exclude(id=self.id).count():
+ # 抛出验证异常,提示该平台配置已存在
+ raise ValidationError(_(self.type + _('already exists')))
+
+ def __str__(self):
+ """模型实例的字符串表示:返回平台类型"""
+ return self.type
+
+ class Meta:
+ verbose_name = 'oauth配置' # 模型单数显示名(中文)
+ verbose_name_plural = verbose_name # 模型复数显示名(与单数一致)
+ ordering = ['-creation_time'] # 排序规则:按创建时间倒序
\ No newline at end of file
diff --git a/doc/第7周代码注释/oauth/oauthmanager.py b/doc/第7周代码注释/oauth/oauthmanager.py
new file mode 100644
index 00000000..206ed0e5
--- /dev/null
+++ b/doc/第7周代码注释/oauth/oauthmanager.py
@@ -0,0 +1,593 @@
+import json
+import logging
+import os
+import urllib.parse
+from abc import ABCMeta, abstractmethod # 用于定义抽象基类
+
+import requests # 用于发送HTTP请求
+
+from djangoblog.utils import cache_decorator # 导入缓存装饰器
+from oauth.models import OAuthUser, OAuthConfig # 导入OAuth相关模型
+
+# 初始化日志记录器,用于记录当前模块的日志信息
+logger = logging.getLogger(__name__)
+
+
+class OAuthAccessTokenException(Exception):
+ '''
+ 自定义异常:OAuth授权过程中获取Access Token失败时抛出
+ '''
+
+
+class BaseOauthManager(metaclass=ABCMeta):
+ """
+ OAuth抽象基类:定义第三方登录的通用接口和基础方法
+ 所有第三方平台的OAuth管理器都需继承此类并实现抽象方法
+ """
+ # 子类需重写:授权页面URL(用户跳转授权用)
+ AUTH_URL = None
+ # 子类需重写:获取Access Token的URL
+ TOKEN_URL = None
+ # 子类需重写:获取用户信息的API URL
+ API_URL = None
+ # 子类需重写:平台图标名称(对应OAuthConfig的type字段)
+ ICON_NAME = None
+
+ def __init__(self, access_token=None, openid=None):
+ """
+ 初始化OAuth管理器
+ :param access_token: 第三方平台返回的访问令牌
+ :param openid: 第三方平台用户唯一标识
+ """
+ self.access_token = access_token # 存储访问令牌
+ self.openid = openid # 存储用户唯一标识
+
+ @property
+ def is_access_token_set(self):
+ """属性:判断Access Token是否已设置"""
+ return self.access_token is not None
+
+ @property
+ def is_authorized(self):
+ """属性:判断是否已完成授权(Access Token和OpenID均存在)"""
+ return self.is_access_token_set and self.access_token is not None and self.openid is not None
+
+ @abstractmethod
+ def get_authorization_url(self, nexturl='/'):
+ """
+ 抽象方法:生成第三方授权页面的URL
+ :param nexturl: 授权成功后跳转的页面地址
+ :return: 完整的授权URL字符串
+ """
+ pass
+
+ @abstractmethod
+ def get_access_token_by_code(self, code):
+ """
+ 抽象方法:通过授权码(code)获取Access Token
+ :param code: 第三方平台返回的授权码
+ :return: 成功返回用户信息或Token,失败抛出异常
+ """
+ pass
+
+ @abstractmethod
+ def get_oauth_userinfo(self):
+ """
+ 抽象方法:通过Access Token获取第三方用户信息
+ :return: 构造好的OAuthUser对象,失败返回None
+ """
+ pass
+
+ @abstractmethod
+ def get_picture(self, metadata):
+ """
+ 抽象方法:从用户元数据中提取头像URL
+ :param metadata: 存储用户信息的元数据(JSON字符串)
+ :return: 头像URL字符串
+ """
+ pass
+
+ def do_get(self, url, params, headers=None):
+ """
+ 基础方法:发送GET请求(子类可重写)
+ :param url: 请求地址
+ :param params: 请求参数
+ :param headers: 请求头
+ :return: 响应文本内容
+ """
+ rsp = requests.get(url=url, params=params, headers=headers)
+ logger.info(rsp.text) # 记录响应日志
+ return rsp.text
+
+ def do_post(self, url, params, headers=None):
+ """
+ 基础方法:发送POST请求(子类可重写)
+ :param url: 请求地址
+ :param params: 请求参数
+ :param headers: 请求头
+ :return: 响应文本内容
+ """
+ rsp = requests.post(url, params, headers=headers)
+ logger.info(rsp.text) # 记录响应日志
+ return rsp.text
+
+ def get_config(self):
+ """
+ 获取当前平台的OAuth配置(从OAuthConfig模型中查询)
+ :return: OAuthConfig对象,不存在返回None
+ """
+ value = OAuthConfig.objects.filter(type=self.ICON_NAME)
+ return value[0] if value else None
+
+
+class WBOauthManager(BaseOauthManager):
+ """微博OAuth登录管理器:实现微博第三方登录的具体逻辑"""
+ AUTH_URL = 'https://api.weibo.com/oauth2/authorize' # 微博授权URL
+ TOKEN_URL = 'https://api.weibo.com/oauth2/access_token' # 微博Token获取URL
+ API_URL = 'https://api.weibo.com/2/users/show.json' # 微博用户信息API
+ ICON_NAME = 'weibo' # 对应配置中的平台类型
+
+ def __init__(self, access_token=None, openid=None):
+ # 先获取微博的OAuth配置
+ config = self.get_config()
+ self.client_id = config.appkey if config else '' # 应用ID
+ self.client_secret = config.appsecret if config else '' # 应用密钥
+ self.callback_url = config.callback_url if config else '' # 回调地址
+ # 调用父类初始化方法
+ super(WBOauthManager, self).__init__(access_token=access_token, openid=openid)
+
+ def get_authorization_url(self, nexturl='/'):
+ """生成微博授权URL,拼接跳转地址参数"""
+ params = {
+ 'client_id': self.client_id,
+ 'response_type': 'code', # 授权类型为code
+ 'redirect_uri': self.callback_url + '&next_url=' + nexturl # 回调地址+登录后跳转地址
+ }
+ # 拼接参数生成完整URL
+ url = self.AUTH_URL + "?" + urllib.parse.urlencode(params)
+ return url
+
+ def get_access_token_by_code(self, code):
+ """通过授权码获取微博Access Token,并调用用户信息接口"""
+ params = {
+ 'client_id': self.client_id,
+ 'client_secret': self.client_secret,
+ 'grant_type': 'authorization_code', # 授权模式
+ 'code': code, # 授权码
+ 'redirect_uri': self.callback_url # 回调地址(需与授权时一致)
+ }
+ # 发送POST请求获取Token
+ rsp = self.do_post(self.TOKEN_URL, params)
+ obj = json.loads(rsp)
+
+ # 成功获取Token则继续获取用户信息
+ if 'access_token' in obj:
+ self.access_token = str(obj['access_token'])
+ self.openid = str(obj['uid']) # 微博用户唯一标识(uid)
+ return self.get_oauth_userinfo()
+ else:
+ # 失败则抛出异常
+ raise OAuthAccessTokenException(rsp)
+
+ def get_oauth_userinfo(self):
+ """通过Access Token获取微博用户信息,构造OAuthUser对象"""
+ if not self.is_authorized:
+ return None # 未授权则返回None
+
+ params = {
+ 'uid': self.openid,
+ 'access_token': self.access_token
+ }
+ # 发送GET请求获取用户信息
+ rsp = self.do_get(self.API_URL, params)
+ try:
+ datas = json.loads(rsp)
+ user = OAuthUser()
+ user.metadata = rsp # 存储原始响应数据
+ user.picture = datas['avatar_large'] # 大尺寸头像
+ user.nickname = datas['screen_name'] # 微博昵称
+ user.openid = datas['id'] # 用户唯一标识
+ user.type = 'weibo' # 平台类型
+ user.token = self.access_token # 存储Access Token
+ # 若返回邮箱则赋值
+ if 'email' in datas and datas['email']:
+ user.email = datas['email']
+ return user
+ except Exception as e:
+ logger.error(e)
+ logger.error('weibo oauth error.rsp:' + rsp)
+ return None
+
+ def get_picture(self, metadata):
+ """从元数据中提取微博用户头像URL"""
+ datas = json.loads(metadata)
+ return datas['avatar_large']
+
+
+class ProxyManagerMixin:
+ """
+ 代理混入类:为HTTP请求添加代理支持
+ 需与BaseOauthManager组合使用(适用于需要代理访问的平台,如谷歌、GitHub)
+ """
+ def __init__(self, *args, **kwargs):
+ # 从环境变量读取代理配置
+ if os.environ.get("HTTP_PROXY"):
+ self.proxies = {
+ "http": os.environ.get("HTTP_PROXY"),
+ "https": os.environ.get("HTTP_PROXY")
+ }
+ else:
+ self.proxies = None # 无代理则为None
+ # 调用父类初始化方法(注意:混入类需放在继承列表前面)
+ super().__init__(*args, **kwargs)
+
+ def do_get(self, url, params, headers=None):
+ """重写GET方法,添加代理参数"""
+ rsp = requests.get(url=url, params=params, headers=headers, proxies=self.proxies)
+ logger.info(rsp.text)
+ return rsp.text
+
+ def do_post(self, url, params, headers=None):
+ """重写POST方法,添加代理参数"""
+ rsp = requests.post(url, params, headers=headers, proxies=self.proxies)
+ logger.info(rsp.text)
+ return rsp.text
+
+
+class GoogleOauthManager(ProxyManagerMixin, BaseOauthManager):
+ """谷歌OAuth登录管理器:集成代理支持,实现谷歌第三方登录逻辑"""
+ AUTH_URL = 'https://accounts.google.com/o/oauth2/v2/auth' # 谷歌授权URL
+ TOKEN_URL = 'https://www.googleapis.com/oauth2/v4/token' # 谷歌Token获取URL
+ API_URL = 'https://www.googleapis.com/oauth2/v3/userinfo' # 谷歌用户信息API
+ ICON_NAME = 'google' # 对应配置中的平台类型
+
+ def __init__(self, access_token=None, openid=None):
+ # 获取谷歌OAuth配置
+ config = self.get_config()
+ self.client_id = config.appkey if config else ''
+ self.client_secret = config.appsecret if config else ''
+ self.callback_url = config.callback_url if config else ''
+ # 调用父类(ProxyManagerMixin)初始化方法
+ super(GoogleOauthManager, self).__init__(access_token=access_token, openid=openid)
+
+ def get_authorization_url(self, nexturl='/'):
+ """生成谷歌授权URL,请求openid和email权限"""
+ params = {
+ 'client_id': self.client_id,
+ 'response_type': 'code',
+ 'redirect_uri': self.callback_url,
+ 'scope': 'openid email', # 授权范围:获取用户标识和邮箱
+ }
+ url = self.AUTH_URL + "?" + urllib.parse.urlencode(params)
+ return url
+
+ def get_access_token_by_code(self, code):
+ """通过授权码获取谷歌Access Token"""
+ params = {
+ 'client_id': self.client_id,
+ 'client_secret': self.client_secret,
+ 'grant_type': 'authorization_code',
+ 'code': code,
+ 'redirect_uri': self.callback_url
+ }
+ rsp = self.do_post(self.TOKEN_URL, params)
+ obj = json.loads(rsp)
+
+ if 'access_token' in obj:
+ self.access_token = str(obj['access_token'])
+ self.openid = str(obj['id_token']) # 谷歌用户唯一标识(id_token)
+ logger.info(self.ICON_NAME + ' oauth ' + rsp)
+ return self.access_token
+ else:
+ raise OAuthAccessTokenException(rsp)
+
+ def get_oauth_userinfo(self):
+ """通过Access Token获取谷歌用户信息"""
+ if not self.is_authorized:
+ return None
+
+ params = {'access_token': self.access_token}
+ rsp = self.do_get(self.API_URL, params)
+ try:
+ datas = json.loads(rsp)
+ user = OAuthUser()
+ user.metadata = rsp
+ user.picture = datas['picture'] # 头像URL
+ user.nickname = datas['name'] # 用户名
+ user.openid = datas['sub'] # 用户唯一标识
+ user.token = self.access_token
+ user.type = 'google'
+ if datas['email']:
+ user.email = datas['email'] # 邮箱(谷歌授权时已请求)
+ return user
+ except Exception as e:
+ logger.error(e)
+ logger.error('google oauth error.rsp:' + rsp)
+ return None
+
+ def get_picture(self, metadata):
+ """从元数据中提取谷歌用户头像URL"""
+ datas = json.loads(metadata)
+ return datas['picture']
+
+
+class GitHubOauthManager(ProxyManagerMixin, BaseOauthManager):
+ """GitHub OAuth登录管理器:集成代理支持,实现GitHub第三方登录逻辑"""
+ AUTH_URL = 'https://github.com/login/oauth/authorize' # GitHub授权URL
+ TOKEN_URL = 'https://github.com/login/oauth/access_token' # GitHub Token获取URL
+ API_URL = 'https://api.github.com/user' # GitHub用户信息API
+ ICON_NAME = 'github' # 对应配置中的平台类型
+
+ def __init__(self, access_token=None, openid=None):
+ # 获取GitHub OAuth配置
+ config = self.get_config()
+ self.client_id = config.appkey if config else ''
+ self.client_secret = config.appsecret if config else ''
+ self.callback_url = config.callback_url if config else ''
+ # 调用父类初始化方法
+ super(GitHubOauthManager, self).__init__(access_token=access_token, openid=openid)
+
+ def get_authorization_url(self, next_url='/'):
+ """生成GitHub授权URL,请求user权限"""
+ params = {
+ 'client_id': self.client_id,
+ 'response_type': 'code',
+ 'redirect_uri': f'{self.callback_url}&next_url={next_url}', # 回调+跳转地址
+ 'scope': 'user' # 授权范围:获取用户基本信息
+ }
+ url = self.AUTH_URL + "?" + urllib.parse.urlencode(params)
+ return url
+
+ def get_access_token_by_code(self, code):
+ """通过授权码获取GitHub Access Token(返回格式为query string)"""
+ params = {
+ 'client_id': self.client_id,
+ 'client_secret': self.client_secret,
+ 'grant_type': 'authorization_code',
+ 'code': code,
+ 'redirect_uri': self.callback_url
+ }
+ rsp = self.do_post(self.TOKEN_URL, params)
+
+ # GitHub返回的Token是query string格式,需解析
+ from urllib import parse
+ r = parse.parse_qs(rsp)
+ if 'access_token' in r:
+ self.access_token = (r['access_token'][0]) # 取第一个值(列表格式)
+ return self.access_token
+ else:
+ raise OAuthAccessTokenException(rsp)
+
+ def get_oauth_userinfo(self):
+ """通过Access Token获取GitHub用户信息(需在请求头中携带Token)"""
+ # GitHub需在请求头中传递Token,而非URL参数
+ rsp = self.do_get(self.API_URL, params={}, headers={
+ "Authorization": "token " + self.access_token
+ })
+ try:
+ datas = json.loads(rsp)
+ user = OAuthUser()
+ user.picture = datas['avatar_url'] # 头像URL
+ user.nickname = datas['name'] # 用户名(可能为None,优先显示login)
+ user.openid = datas['id'] # 用户唯一ID
+ user.type = 'github'
+ user.token = self.access_token
+ user.metadata = rsp
+ # 若返回邮箱则赋值(GitHub部分用户邮箱可能为None)
+ if 'email' in datas and datas['email']:
+ user.email = datas['email']
+ return user
+ except Exception as e:
+ logger.error(e)
+ logger.error('github oauth error.rsp:' + rsp)
+ return None
+
+ def get_picture(self, metadata):
+ """从元数据中提取GitHub用户头像URL"""
+ datas = json.loads(metadata)
+ return datas['avatar_url']
+
+
+class FaceBookOauthManager(ProxyManagerMixin, BaseOauthManager):
+ """Facebook OAuth登录管理器:集成代理支持,实现Facebook第三方登录逻辑"""
+ AUTH_URL = 'https://www.facebook.com/v16.0/dialog/oauth' # Facebook授权URL(v16.0版本)
+ TOKEN_URL = 'https://graph.facebook.com/v16.0/oauth/access_token' # Facebook Token获取URL
+ API_URL = 'https://graph.facebook.com/me' # Facebook用户信息API
+ ICON_NAME = 'facebook' # 对应配置中的平台类型
+
+ def __init__(self, access_token=None, openid=None):
+ # 获取Facebook OAuth配置
+ config = self.get_config()
+ self.client_id = config.appkey if config else ''
+ self.client_secret = config.appsecret if config else ''
+ self.callback_url = config.callback_url if config else ''
+ # 调用父类初始化方法
+ super(FaceBookOauthManager, self).__init__(access_token=access_token, openid=openid)
+
+ def get_authorization_url(self, next_url='/'):
+ """生成Facebook授权URL,请求email和公开资料权限"""
+ params = {
+ 'client_id': self.client_id,
+ 'response_type': 'code',
+ 'redirect_uri': self.callback_url,
+ 'scope': 'email,public_profile' # 授权范围
+ }
+ url = self.AUTH_URL + "?" + urllib.parse.urlencode(params)
+ return url
+
+ def get_access_token_by_code(self, code):
+ """通过授权码获取Facebook Access Token"""
+ params = {
+ 'client_id': self.client_id,
+ 'client_secret': self.client_secret,
+ 'code': code,
+ 'redirect_uri': self.callback_url
+ }
+ rsp = self.do_post(self.TOKEN_URL, params)
+ obj = json.loads(rsp)
+
+ if 'access_token' in obj:
+ token = str(obj['access_token'])
+ self.access_token = token
+ return self.access_token
+ else:
+ raise OAuthAccessTokenException(rsp)
+
+ def get_oauth_userinfo(self):
+ """通过Access Token获取Facebook用户信息(需指定返回字段)"""
+ params = {
+ 'access_token': self.access_token,
+ 'fields': 'id,name,picture,email' # 指定返回的字段(减少冗余)
+ }
+ try:
+ rsp = self.do_get(self.API_URL, params)
+ datas = json.loads(rsp)
+ user = OAuthUser()
+ user.nickname = datas['name'] # 用户名
+ user.openid = datas['id'] # 用户唯一标识
+ user.type = 'facebook'
+ user.token = self.access_token
+ user.metadata = rsp
+ # 赋值邮箱(可能为None)
+ if 'email' in datas and datas['email']:
+ user.email = datas['email']
+ # 处理头像(Facebook头像嵌套在picture.data.url中)
+ if 'picture' in datas and datas['picture'] and datas['picture']['data'] and datas['picture']['data']['url']:
+ user.picture = str(datas['picture']['data']['url'])
+ return user
+ except Exception as e:
+ logger.error(e)
+ return None
+
+ def get_picture(self, metadata):
+ """从元数据中提取Facebook用户头像URL(处理嵌套结构)"""
+ datas = json.loads(metadata)
+ return str(datas['picture']['data']['url'])
+
+
+class QQOauthManager(BaseOauthManager):
+ """QQ OAuth登录管理器:实现QQ第三方登录逻辑(需单独获取OpenID)"""
+ AUTH_URL = 'https://graph.qq.com/oauth2.0/authorize' # QQ授权URL
+ TOKEN_URL = 'https://graph.qq.com/oauth2.0/token' # QQ Token获取URL
+ API_URL = 'https://graph.qq.com/user/get_user_info' # QQ用户信息API
+ OPEN_ID_URL = 'https://graph.qq.com/oauth2.0/me' # QQ OpenID获取URL(单独接口)
+ ICON_NAME = 'qq' # 对应配置中的平台类型
+
+ def __init__(self, access_token=None, openid=None):
+ # 获取QQ OAuth配置
+ config = self.get_config()
+ self.client_id = config.appkey if config else ''
+ self.client_secret = config.appsecret if config else ''
+ self.callback_url = config.callback_url if config else ''
+ # 调用父类初始化方法
+ super(QQOauthManager, self).__init__(access_token=access_token, openid=openid)
+
+ def get_authorization_url(self, next_url='/'):
+ """生成QQ授权URL,拼接跳转地址"""
+ params = {
+ 'response_type': 'code',
+ 'client_id': self.client_id,
+ 'redirect_uri': self.callback_url + '&next_url=' + next_url,
+ }
+ url = self.AUTH_URL + "?" + urllib.parse.urlencode(params)
+ return url
+
+ def get_access_token_by_code(self, code):
+ """通过授权码获取QQ Access Token(返回格式为query string)"""
+ params = {
+ 'grant_type': 'authorization_code',
+ 'client_id': self.client_id,
+ 'client_secret': self.client_secret,
+ 'code': code,
+ 'redirect_uri': self.callback_url
+ }
+ # QQ获取Token使用GET请求
+ rsp = self.do_get(self.TOKEN_URL, params)
+ if rsp:
+ # 解析query string格式的响应
+ d = urllib.parse.parse_qs(rsp)
+ if 'access_token' in d:
+ token = d['access_token']
+ self.access_token = token[0] # 取第一个值
+ return token
+ else:
+ raise OAuthAccessTokenException(rsp)
+
+ def get_open_id(self):
+ """单独获取QQ用户的OpenID(QQ OAuth特殊流程)"""
+ if self.is_access_token_set:
+ params = {'access_token': self.access_token}
+ rsp = self.do_get(self.OPEN_ID_URL, params)
+ if rsp:
+ # QQ返回的OpenID格式为callback包裹的JSON,需处理格式
+ rsp = rsp.replace('callback(', '').replace(')', '').replace(';', '')
+ obj = json.loads(rsp)
+ openid = str(obj['openid'])
+ self.openid = openid
+ return openid
+
+ def get_oauth_userinfo(self):
+ """获取QQ用户信息(需先获取OpenID)"""
+ openid = self.get_open_id()
+ if openid:
+ params = {
+ 'access_token': self.access_token,
+ 'oauth_consumer_key': self.client_id, # QQ需额外传递client_id
+ 'openid': self.openid
+ }
+ rsp = self.do_get(self.API_URL, params)
+ logger.info(rsp)
+ obj = json.loads(rsp)
+ user = OAuthUser()
+ user.nickname = obj['nickname'] # 昵称
+ user.openid = openid # 唯一标识
+ user.type = 'qq'
+ user.token = self.access_token
+ user.metadata = rsp
+ # 赋值邮箱(可能为None)
+ if 'email' in obj:
+ user.email = obj['email']
+ # 赋值头像(figureurl为QQ头像URL)
+ if 'figureurl' in obj:
+ user.picture = str(obj['figureurl'])
+ return user
+
+ def get_picture(self, metadata):
+ """从元数据中提取QQ用户头像URL"""
+ datas = json.loads(metadata)
+ return str(datas['figureurl'])
+
+
+@cache_decorator(expiration=100 * 60) # 缓存100分钟,减少数据库查询
+def get_oauth_apps():
+ """
+ 获取所有启用的OAuth应用管理器实例
+ :return: 启用的OAuthManager实例列表
+ """
+ # 查询所有已启用的OAuth配置
+ configs = OAuthConfig.objects.filter(is_enable=True).all()
+ if not configs:
+ return [] # 无启用配置则返回空列表
+
+ # 提取已启用的平台类型
+ configtypes = [x.type for x in configs]
+ # 获取BaseOauthManager的所有子类(各平台实现类)
+ applications = BaseOauthManager.__subclasses__()
+ # 筛选出已启用的平台实例
+ apps = [x() for x in applications if x().ICON_NAME.lower() in configtypes]
+ return apps
+
+
+def get_manager_by_type(type):
+ """
+ 根据平台类型获取对应的OAuth管理器实例
+ :param type: 平台类型(如weibo、github)
+ :return: 对应的OAuthManager实例,不存在返回None
+ """
+ applications = get_oauth_apps()
+ if applications:
+ # 忽略大小写匹配平台类型
+ finds = list(filter(lambda x: x.ICON_NAME.lower() == type.lower(), applications))
+ if finds:
+ return finds[0] # 返回第一个匹配的实例
+ return None
\ No newline at end of file
diff --git a/doc/第7周代码注释/oauth/templatetags/__init__.py b/doc/第7周代码注释/oauth/templatetags/__init__.py
new file mode 100644
index 00000000..8b137891
--- /dev/null
+++ b/doc/第7周代码注释/oauth/templatetags/__init__.py
@@ -0,0 +1 @@
+
diff --git a/doc/第7周代码注释/oauth/templatetags/__pycache__/__init__.cpython-312.pyc b/doc/第7周代码注释/oauth/templatetags/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 00000000..a663cf8f
Binary files /dev/null and b/doc/第7周代码注释/oauth/templatetags/__pycache__/__init__.cpython-312.pyc differ
diff --git a/doc/第7周代码注释/oauth/templatetags/__pycache__/oauth_tags.cpython-312.pyc b/doc/第7周代码注释/oauth/templatetags/__pycache__/oauth_tags.cpython-312.pyc
new file mode 100644
index 00000000..9809fa69
Binary files /dev/null and b/doc/第7周代码注释/oauth/templatetags/__pycache__/oauth_tags.cpython-312.pyc differ
diff --git a/doc/第7周代码注释/oauth/templatetags/oauth_tags.py b/doc/第7周代码注释/oauth/templatetags/oauth_tags.py
new file mode 100644
index 00000000..9e2f3411
--- /dev/null
+++ b/doc/第7周代码注释/oauth/templatetags/oauth_tags.py
@@ -0,0 +1,54 @@
+# 导入Django模板相关模块和URL反转函数
+from django import template
+from django.urls import reverse
+
+# 导入获取OAuth应用配置的工具函数
+from oauth.oauthmanager import get_oauth_apps
+
+# 注册一个模板标签库,用于在模板中使用自定义标签
+register = template.Library()
+
+
+@register.inclusion_tag('oauth/oauth_applications.html')
+def load_oauth_applications(request):
+ """
+ 自定义模板包含标签,用于加载可用的OAuth登录应用信息并传递给模板
+
+ 功能:
+ 1. 获取所有启用的OAuth应用配置
+ 2. 为每个应用构建对应的登录URL(包含类型和跳转路径)
+ 3. 将处理后的应用列表传递给'oauth/oauth_applications.html'模板
+
+ 参数:
+ request: 当前请求对象,用于获取当前完整路径(登录后跳转使用)
+
+ 返回:
+ 包含应用列表的字典,供模板渲染使用
+ """
+ # 获取所有可用的OAuth应用配置
+ applications = get_oauth_apps()
+
+ if applications:
+ # 生成OAuth登录的基础URL(通过URL名称反转得到)
+ baseurl = reverse('oauth:oauthlogin')
+ # 获取当前请求的完整路径(用于登录成功后跳转回原页面)
+ path = request.get_full_path()
+
+ # 处理每个应用,生成包含图标名称和完整登录URL的元组列表
+ apps = list(map(lambda x: (
+ x.ICON_NAME, # 应用图标名称
+ # 构建登录URL,包含应用类型和跳转路径参数
+ '{baseurl}?type={type}&next_url={next}'.format(
+ baseurl=baseurl,
+ type=x.ICON_NAME,
+ next=path
+ )
+ ), applications))
+ else:
+ # 若没有可用应用,返回空列表
+ apps = []
+
+ # 返回上下文数据,供模板使用
+ return {
+ 'apps': apps
+ }
\ No newline at end of file
diff --git a/doc/第7周代码注释/oauth/tests.py b/doc/第7周代码注释/oauth/tests.py
new file mode 100644
index 00000000..58883572
--- /dev/null
+++ b/doc/第7周代码注释/oauth/tests.py
@@ -0,0 +1,319 @@
+import json
+from unittest.mock import patch # 用于模拟外部依赖(如第三方API调用)
+
+from django.conf import settings
+from django.contrib import auth # 用于用户认证相关操作
+from django.test import Client, RequestFactory, TestCase # Django测试工具
+from django.urls import reverse # 用于反向解析URL
+
+from djangoblog.utils import get_sha256 # 导入自定义加密工具函数
+from oauth.models import OAuthConfig # 导入OAuth配置模型
+from oauth.oauthmanager import BaseOauthManager # 导入OAuth基础管理器
+
+
+# Create your tests here.
+class OAuthConfigTest(TestCase):
+ """测试OAuth配置模型及基础登录流程"""
+
+ def setUp(self):
+ """测试前初始化:创建客户端和请求工厂"""
+ self.client = Client() # 用于模拟HTTP请求的客户端
+ self.factory = RequestFactory() # 用于创建请求对象的工厂
+
+ def test_oauth_login_test(self):
+ """测试OAuth配置创建后,登录链接和授权回调的正确性"""
+ # 创建一个微博的OAuth配置
+ c = OAuthConfig()
+ c.type = 'weibo'
+ c.appkey = 'appkey'
+ c.appsecret = 'appsecret'
+ c.save()
+
+ # 测试访问微博OAuth登录链接是否重定向到正确的授权页面
+ response = self.client.get('/oauth/oauthlogin?type=weibo')
+ self.assertEqual(response.status_code, 302) # 验证重定向状态码
+ self.assertTrue("api.weibo.com" in response.url) # 验证重定向到微博API
+
+ # 测试授权回调接口是否正常重定向
+ response = self.client.get('/oauth/authorize?type=weibo&code=code')
+ self.assertEqual(response.status_code, 302) # 验证重定向状态码
+ self.assertEqual(response.url, '/') # 验证默认重定向到首页
+
+
+class OauthLoginTest(TestCase):
+ """测试各第三方平台的OAuth登录流程"""
+
+ def setUp(self) -> None:
+ """测试前初始化:创建客户端、请求工厂,并初始化所有平台的OAuth配置"""
+ self.client = Client()
+ self.factory = RequestFactory()
+ self.apps = self.init_apps() # 初始化所有启用的OAuth应用
+
+ def init_apps(self):
+ """为所有BaseOauthManager的子类(各平台管理器)创建对应的OAuth配置"""
+ # 获取所有第三方平台的OAuth管理器实例
+ applications = [p() for p in BaseOauthManager.__subclasses__()]
+ # 为每个平台创建默认配置并保存到数据库
+ for application in applications:
+ c = OAuthConfig()
+ c.type = application.ICON_NAME.lower()
+ c.appkey = 'appkey'
+ c.appsecret = 'appsecret'
+ c.save()
+ return applications
+
+ def get_app_by_type(self, type):
+ """根据平台类型获取对应的OAuth管理器实例"""
+ for app in self.apps:
+ if app.ICON_NAME.lower() == type:
+ return app
+
+ @patch("oauth.oauthmanager.WBOauthManager.do_post")
+ @patch("oauth.oauthmanager.WBOauthManager.do_get")
+ def test_weibo_login(self, mock_do_get, mock_do_post):
+ """测试微博登录流程:获取授权链接、Token及用户信息"""
+ # 获取微博OAuth管理器实例
+ weibo_app = self.get_app_by_type('weibo')
+ assert weibo_app # 确保实例存在
+
+ # 验证授权链接生成(无需mock,直接调用方法)
+ url = weibo_app.get_authorization_url()
+
+ # 模拟获取Token的响应
+ mock_do_post.return_value = json.dumps({
+ "access_token": "access_token",
+ "uid": "uid"
+ })
+ # 模拟获取用户信息的响应
+ mock_do_get.return_value = json.dumps({
+ "avatar_large": "avatar_large",
+ "screen_name": "screen_name",
+ "id": "id",
+ "email": "email",
+ })
+
+ # 测试通过code获取用户信息
+ userinfo = weibo_app.get_access_token_by_code('code')
+ # 验证用户信息是否正确
+ self.assertEqual(userinfo.token, 'access_token')
+ self.assertEqual(userinfo.openid, 'id')
+
+ @patch("oauth.oauthmanager.GoogleOauthManager.do_post")
+ @patch("oauth.oauthmanager.GoogleOauthManager.do_get")
+ def test_google_login(self, mock_do_get, mock_do_post):
+ """测试谷歌登录流程:获取Token及用户信息"""
+ google_app = self.get_app_by_type('google')
+ assert google_app
+
+ # 验证授权链接生成
+ url = google_app.get_authorization_url()
+
+ # 模拟获取Token的响应
+ mock_do_post.return_value = json.dumps({
+ "access_token": "access_token",
+ "id_token": "id_token",
+ })
+ # 模拟获取用户信息的响应
+ mock_do_get.return_value = json.dumps({
+ "picture": "picture",
+ "name": "name",
+ "sub": "sub",
+ "email": "email",
+ })
+
+ # 测试获取Token和用户信息
+ token = google_app.get_access_token_by_code('code')
+ userinfo = google_app.get_oauth_userinfo()
+ # 验证用户信息
+ self.assertEqual(userinfo.token, 'access_token')
+ self.assertEqual(userinfo.openid, 'sub')
+
+ @patch("oauth.oauthmanager.GitHubOauthManager.do_post")
+ @patch("oauth.oauthmanager.GitHubOauthManager.do_get")
+ def test_github_login(self, mock_do_get, mock_do_post):
+ """测试GitHub登录流程:验证授权链接、Token及用户信息"""
+ github_app = self.get_app_by_type('github')
+ assert github_app
+
+ # 验证授权链接包含GitHub域名和client_id参数
+ url = github_app.get_authorization_url()
+ self.assertTrue("github.com" in url)
+ self.assertTrue("client_id" in url)
+
+ # 模拟GitHub返回的Token(query string格式)
+ mock_do_post.return_value = "access_token=gho_16C7e42F292c6912E7710c838347Ae178B4a&scope=repo%2Cgist&token_type=bearer"
+ # 模拟用户信息响应
+ mock_do_get.return_value = json.dumps({
+ "avatar_url": "avatar_url",
+ "name": "name",
+ "id": "id",
+ "email": "email",
+ })
+
+ # 测试获取Token和用户信息
+ token = github_app.get_access_token_by_code('code')
+ userinfo = github_app.get_oauth_userinfo()
+ # 验证Token和openid
+ self.assertEqual(userinfo.token, 'gho_16C7e42F292c6912E7710c838347Ae178B4a')
+ self.assertEqual(userinfo.openid, 'id')
+
+ @patch("oauth.oauthmanager.FaceBookOauthManager.do_post")
+ @patch("oauth.oauthmanager.FaceBookOauthManager.do_get")
+ def test_facebook_login(self, mock_do_get, mock_do_post):
+ """测试Facebook登录流程:验证授权链接、Token及用户信息"""
+ facebook_app = self.get_app_by_type('facebook')
+ assert facebook_app
+
+ # 验证授权链接包含Facebook域名
+ url = facebook_app.get_authorization_url()
+ self.assertTrue("facebook.com" in url)
+
+ # 模拟获取Token的响应
+ mock_do_post.return_value = json.dumps({
+ "access_token": "access_token",
+ })
+ # 模拟用户信息响应(包含嵌套的头像结构)
+ mock_do_get.return_value = json.dumps({
+ "name": "name",
+ "id": "id",
+ "email": "email",
+ "picture": {
+ "data": {
+ "url": "url"
+ }
+ }
+ })
+
+ # 测试获取Token和用户信息
+ token = facebook_app.get_access_token_by_code('code')
+ userinfo = facebook_app.get_oauth_userinfo()
+ # 验证Token
+ self.assertEqual(userinfo.token, 'access_token')
+
+ @patch("oauth.oauthmanager.QQOauthManager.do_get", side_effect=[
+ # 模拟三次GET请求的响应:1.获取Token 2.获取OpenID 3.获取用户信息
+ 'access_token=access_token&expires_in=3600',
+ 'callback({"client_id":"appid","openid":"openid"} );',
+ json.dumps({
+ "nickname": "nickname",
+ "email": "email",
+ "figureurl": "figureurl",
+ "openid": "openid",
+ })
+ ])
+ def test_qq_login(self, mock_do_get):
+ """测试QQ登录流程(QQ需单独获取OpenID)"""
+ qq_app = self.get_app_by_type('qq')
+ assert qq_app
+
+ # 验证授权链接包含QQ域名
+ url = qq_app.get_authorization_url()
+ self.assertTrue("qq.com" in url)
+
+ # 测试获取Token和用户信息
+ token = qq_app.get_access_token_by_code('code')
+ userinfo = qq_app.get_oauth_userinfo()
+ # 验证Token
+ self.assertEqual(userinfo.token, 'access_token')
+
+ @patch("oauth.oauthmanager.WBOauthManager.do_post")
+ @patch("oauth.oauthmanager.WBOauthManager.do_get")
+ def test_weibo_authoriz_login_with_email(self, mock_do_get, mock_do_post):
+ """测试微博登录(用户提供邮箱):验证自动注册登录流程"""
+ # 模拟获取Token的响应
+ mock_do_post.return_value = json.dumps({
+ "access_token": "access_token",
+ "uid": "uid"
+ })
+ # 模拟包含邮箱的用户信息
+ mock_user_info = {
+ "avatar_large": "avatar_large",
+ "screen_name": "screen_name1",
+ "id": "id",
+ "email": "email",
+ }
+ mock_do_get.return_value = json.dumps(mock_user_info)
+
+ # 测试访问登录链接是否重定向到微博授权页
+ response = self.client.get('/oauth/oauthlogin?type=weibo')
+ self.assertEqual(response.status_code, 302)
+ self.assertTrue("api.weibo.com" in response.url)
+
+ # 测试授权回调后是否重定向到首页
+ response = self.client.get('/oauth/authorize?type=weibo&code=code')
+ self.assertEqual(response.status_code, 302)
+ self.assertEqual(response.url, '/')
+
+ # 验证用户已登录,信息正确
+ user = auth.get_user(self.client)
+ assert user.is_authenticated
+ self.assertTrue(user.is_authenticated)
+ self.assertEqual(user.username, mock_user_info['screen_name'])
+ self.assertEqual(user.email, mock_user_info['email'])
+
+ # 测试退出登录后再次登录是否正常
+ self.client.logout()
+ response = self.client.get('/oauth/authorize?type=weibo&code=code')
+ self.assertEqual(response.status_code, 302)
+ self.assertEqual(response.url, '/')
+
+ # 再次验证用户登录状态和信息
+ user = auth.get_user(self.client)
+ assert user.is_authenticated
+ self.assertTrue(user.is_authenticated)
+ self.assertEqual(user.username, mock_user_info['screen_name'])
+ self.assertEqual(user.email, mock_user_info['email'])
+
+ @patch("oauth.oauthmanager.WBOauthManager.do_post")
+ @patch("oauth.oauthmanager.WBOauthManager.do_get")
+ def test_weibo_authoriz_login_without_email(self, mock_do_get, mock_do_post):
+ """测试微博登录(用户未提供邮箱):验证补充邮箱、绑定及确认流程"""
+ # 模拟获取Token的响应
+ mock_do_post.return_value = json.dumps({
+ "access_token": "access_token",
+ "uid": "uid"
+ })
+ # 模拟不包含邮箱的用户信息
+ mock_user_info = {
+ "avatar_large": "avatar_large",
+ "screen_name": "screen_name1",
+ "id": "id",
+ }
+ mock_do_get.return_value = json.dumps(mock_user_info)
+
+ # 测试访问登录链接
+ response = self.client.get('/oauth/oauthlogin?type=weibo')
+ self.assertEqual(response.status_code, 302)
+ self.assertTrue("api.weibo.com" in response.url)
+
+ # 测试授权后是否跳转到补充邮箱页面
+ response = self.client.get('/oauth/authorize?type=weibo&code=code')
+ self.assertEqual(response.status_code, 302)
+
+ # 提取补充邮箱页面的OAuth用户ID
+ oauth_user_id = int(response.url.split('/')[-1].split('.')[0])
+ self.assertEqual(response.url, f'/oauth/requireemail/{oauth_user_id}.html')
+
+ # 测试提交邮箱后是否跳转到绑定成功页
+ response = self.client.post(response.url, {'email': 'test@gmail.com', 'oauthid': oauth_user_id})
+ self.assertEqual(response.status_code, 302)
+
+ # 生成验证签名(模拟邮箱确认流程)
+ sign = get_sha256(settings.SECRET_KEY + str(oauth_user_id) + settings.SECRET_KEY)
+ url = reverse('oauth:bindsuccess', kwargs={'oauthid': oauth_user_id})
+ self.assertEqual(response.url, f'{url}?type=email')
+
+ # 测试邮箱确认后是否跳转到成功页,且用户登录状态正确
+ path = reverse('oauth:email_confirm', kwargs={'id': oauth_user_id, 'sign': sign})
+ response = self.client.get(path)
+ self.assertEqual(response.status_code, 302)
+ self.assertEqual(response.url, f'/oauth/bindsuccess/{oauth_user_id}.html?type=success')
+
+ # 验证用户信息(用户名、邮箱)及OAuth用户关联正确
+ user = auth.get_user(self.client)
+ from oauth.models import OAuthUser
+ oauth_user = OAuthUser.objects.get(author=user)
+ self.assertTrue(user.is_authenticated)
+ self.assertEqual(user.username, mock_user_info['screen_name'])
+ self.assertEqual(user.email, 'test@gmail.com')
+ self.assertEqual(oauth_user.pk, oauth_user_id)
\ No newline at end of file
diff --git a/doc/第7周代码注释/oauth/urls.py b/doc/第7周代码注释/oauth/urls.py
new file mode 100644
index 00000000..75a14d98
--- /dev/null
+++ b/doc/第7周代码注释/oauth/urls.py
@@ -0,0 +1,40 @@
+# 导入Django的URL路径配置模块
+from django.urls import path
+
+# 导入当前应用(oauth)的视图模块
+from . import views
+
+# 定义应用命名空间,用于模板或反向解析时指定应用(如:{% url 'oauth:oauthlogin' %})
+app_name = "oauth"
+
+# URL路由配置列表:映射URL路径到对应的视图函数/类
+urlpatterns = [
+ # 1. OAuth授权回调接口:接收第三方平台返回的授权码(code),处理后续登录逻辑
+ path(
+ r'oauth/authorize',
+ views.authorize), # 对应视图函数:authorize
+
+ # 2. 补充邮箱页面:第三方登录时用户未提供邮箱,跳转至此页面补充
+ path(
+ r'oauth/requireemail/.html', # 路径参数:oauthid(OAuthUser的ID)
+ views.RequireEmailView.as_view(), # 对应基于类的视图:RequireEmailView
+ name='require_email'), # 路由名称:用于反向解析
+
+ # 3. 邮箱确认接口:验证用户补充邮箱的有效性(通过sign签名验证)
+ path(
+ r'oauth/emailconfirm//.html', # 路径参数:id(OAuthUser的ID)、sign(验证签名)
+ views.emailconfirm, # 对应视图函数:emailconfirm
+ name='email_confirm'), # 路由名称:用于反向解析
+
+ # 4. 绑定成功页面:邮箱补充或账号绑定完成后,展示成功提示
+ path(
+ r'oauth/bindsuccess/.html', # 路径参数:oauthid(OAuthUser的ID)
+ views.bindsuccess, # 对应视图函数:bindsuccess
+ name='bindsuccess'), # 路由名称:用于反向解析
+
+ # 5. OAuth登录入口:生成第三方平台的授权链接,跳转至第三方授权页面
+ path(
+ r'oauth/oauthlogin',
+ views.oauthlogin, # 对应视图函数:oauthlogin
+ name='oauthlogin') # 路由名称:用于反向解析
+]
\ No newline at end of file
diff --git a/doc/第7周代码注释/oauth/views.py b/doc/第7周代码注释/oauth/views.py
new file mode 100644
index 00000000..835c61b8
--- /dev/null
+++ b/doc/第7周代码注释/oauth/views.py
@@ -0,0 +1,313 @@
+import logging
+# Create your views here.
+from urllib.parse import urlparse # 用于解析URL,验证跳转地址合法性
+
+from django.conf import settings
+from django.contrib.auth import get_user_model # 用于动态获取用户模型
+from django.contrib.auth import login # 用于用户登录
+from django.core.exceptions import ObjectDoesNotExist # 用于处理对象不存在异常
+from django.db import transaction # 用于数据库事务处理,确保操作原子性
+from django.http import HttpResponseForbidden # 用于返回403禁止访问响应
+from django.http import HttpResponseRedirect # 用于HTTP重定向
+from django.shortcuts import get_object_or_404 # 用于获取对象,不存在则返回404
+from django.shortcuts import render # 用于渲染模板
+from django.urls import reverse # 用于反向解析URL
+from django.utils import timezone # 用于获取当前时间
+from django.utils.translation import gettext_lazy as _ # 用于国际化翻译
+from django.views.generic import FormView # 用于基于类的表单视图
+
+from djangoblog.blog_signals import oauth_user_login_signal # 导入oauth登录信号
+from djangoblog.utils import get_current_site # 用于获取当前站点信息
+from djangoblog.utils import send_email, get_sha256 # 导入发送邮件和加密工具函数
+from oauth.forms import RequireEmailForm # 导入补充邮箱的表单
+from .models import OAuthUser # 导入OAuth用户模型
+from .oauthmanager import get_manager_by_type, OAuthAccessTokenException # 导入OAuth管理器和异常
+
+# 初始化日志记录器
+logger = logging.getLogger(__name__)
+
+
+def get_redirecturl(request):
+ """
+ 处理并验证跳转URL,确保跳转地址安全(仅允许本站域名)
+ :param request: 请求对象
+ :return: 验证后的合法跳转URL,默认返回'/'
+ """
+ nexturl = request.GET.get('next_url', None)
+ # 过滤非法或默认的跳转地址
+ if not nexturl or nexturl == '/login/' or nexturl == '/login':
+ nexturl = '/'
+ return nexturl
+ # 解析URL,验证域名是否为本站
+ p = urlparse(nexturl)
+ if p.netloc: # 存在域名部分时验证
+ site = get_current_site().domain
+ # 移除www.前缀后比较,确保域名一致
+ if not p.netloc.replace('www.', '') == site.replace('www.', ''):
+ logger.info('非法url:' + nexturl)
+ return "/"
+ return nexturl
+
+
+def oauthlogin(request):
+ """
+ OAuth登录入口:根据平台类型生成第三方授权链接并跳转
+ :param request: 请求对象,包含'type'参数(如weibo、github)
+ :return: 重定向到第三方平台授权页面
+ """
+ type = request.GET.get('type', None)
+ if not type: # 未指定平台类型,跳转到首页
+ return HttpResponseRedirect('/')
+ # 获取对应平台的OAuth管理器
+ manager = get_manager_by_type(type)
+ if not manager: # 管理器不存在,跳转到首页
+ return HttpResponseRedirect('/')
+ # 获取合法的跳转地址(授权成功后返回的页面)
+ nexturl = get_redirecturl(request)
+ # 生成第三方平台的授权URL
+ authorizeurl = manager.get_authorization_url(nexturl)
+ # 重定向到授权页面
+ return HttpResponseRedirect(authorizeurl)
+
+
+def authorize(request):
+ """
+ OAuth授权回调处理:接收第三方平台返回的code,获取用户信息并完成登录
+ :param request: 请求对象,包含'type'(平台类型)和'code'(授权码)
+ :return: 重定向到目标页面或补充邮箱页面
+ """
+ type = request.GET.get('type', None)
+ if not type:
+ return HttpResponseRedirect('/')
+ # 获取对应平台的OAuth管理器
+ manager = get_manager_by_type(type)
+ if not manager:
+ return HttpResponseRedirect('/')
+ # 获取授权码
+ code = request.GET.get('code', None)
+ try:
+ # 通过code获取access token
+ rsp = manager.get_access_token_by_code(code)
+ except OAuthAccessTokenException as e:
+ logger.warning("OAuthAccessTokenException:" + str(e))
+ return HttpResponseRedirect('/')
+ except Exception as e:
+ logger.error(e)
+ rsp = None
+ # 获取授权成功后的跳转地址
+ nexturl = get_redirecturl(request)
+ if not rsp: # 获取token失败,重新跳转授权
+ return HttpResponseRedirect(manager.get_authorization_url(nexturl))
+
+ # 通过token获取第三方用户信息
+ user = manager.get_oauth_userinfo()
+ if user:
+ # 处理用户昵称为空的情况,生成默认昵称
+ if not user.nickname or not user.nickname.strip():
+ user.nickname = "djangoblog" + timezone.now().strftime('%y%m%d%I%M%S')
+ try:
+ # 若用户已存在(同平台+同openid),更新用户信息
+ temp = OAuthUser.objects.get(type=type, openid=user.openid)
+ temp.picture = user.picture
+ temp.metadata = user.metadata
+ temp.nickname = user.nickname
+ user = temp
+ except ObjectDoesNotExist:
+ # 用户不存在,使用新用户对象
+ pass
+
+ # Facebook的token过长,不存储
+ if type == 'facebook':
+ user.token = ''
+
+ # 若用户提供了邮箱,直接关联或创建本地用户并登录
+ if user.email:
+ with transaction.atomic(): # 数据库事务:确保操作原子性
+ author = None
+ try:
+ # 尝试获取已关联的本地用户
+ author = get_user_model().objects.get(id=user.author_id)
+ except ObjectDoesNotExist:
+ pass
+ if not author:
+ # 不存在则创建或获取本地用户(通过邮箱匹配)
+ result = get_user_model().objects.get_or_create(email=user.email)
+ author = result[0]
+ # 若为新创建的用户,设置用户名和来源
+ if result[1]:
+ try:
+ # 检查用户名是否已存在
+ get_user_model().objects.get(username=user.nickname)
+ except ObjectDoesNotExist:
+ author.username = user.nickname
+ else:
+ # 用户名已存在,生成唯一用户名
+ author.username = "djangoblog" + timezone.now().strftime('%y%m%d%I%M%S')
+ author.source = 'authorize' # 标记来源为oauth授权
+ author.save()
+ # 关联本地用户并保存
+ user.author = author
+ user.save()
+ # 发送oauth用户登录信号(用于后续处理,如日志、统计等)
+ oauth_user_login_signal.send(
+ sender=authorize.__class__, id=user.id)
+ # 登录用户
+ login(request, author)
+ # 重定向到目标页面
+ return HttpResponseRedirect(nexturl)
+ else:
+ # 未提供邮箱,保存用户信息并跳转到补充邮箱页面
+ user.save()
+ url = reverse('oauth:require_email', kwargs={'oauthid': user.id})
+ return HttpResponseRedirect(url)
+ else:
+ # 获取用户信息失败,跳转到目标页面
+ return HttpResponseRedirect(nexturl)
+
+
+def emailconfirm(request, id, sign):
+ """
+ 邮箱确认处理:验证签名合法性,完成用户与邮箱的绑定并登录
+ :param request: 请求对象
+ :param id: OAuthUser的ID
+ :param sign: 验证签名(基于SECRET_KEY和id生成)
+ :return: 重定向到绑定成功页面
+ """
+ if not sign: # 签名为空,返回403
+ return HttpResponseForbidden()
+ # 验证签名合法性(忽略大小写)
+ if not get_sha256(settings.SECRET_KEY + str(id) + settings.SECRET_KEY).upper() == sign.upper():
+ return HttpResponseForbidden()
+ # 获取对应的OAuth用户
+ oauthuser = get_object_or_404(OAuthUser, pk=id)
+
+ with transaction.atomic(): # 数据库事务
+ if oauthuser.author:
+ # 已关联本地用户,直接获取
+ author = get_user_model().objects.get(pk=oauthuser.author_id)
+ else:
+ # 未关联,通过邮箱创建或获取本地用户
+ result = get_user_model().objects.get_or_create(email=oauthuser.email)
+ author = result[0]
+ if result[1]: # 新创建用户
+ author.source = 'emailconfirm' # 标记来源为邮箱确认
+ # 设置用户名为OAuth用户的昵称(或生成默认)
+ author.username = oauthuser.nickname.strip() if oauthuser.nickname.strip() else "djangoblog" + timezone.now().strftime(
+ '%y%m%d%I%M%S')
+ author.save()
+ # 关联本地用户并保存
+ oauthuser.author = author
+ oauthuser.save()
+
+ # 发送登录信号
+ oauth_user_login_signal.send(sender=emailconfirm.__class__, id=oauthuser.id)
+ # 登录用户
+ login(request, author)
+
+ # 发送绑定成功邮件
+ site = 'http://' + get_current_site().domain
+ content = _('''
+ Congratulations, you have successfully bound your email address. You can use
+ %(oauthuser_type)s to directly log in to this website without a password.
+ You are welcome to continue to follow this site, the address is
+ %(site)s
+ Thank you again!
+
+ If the link above cannot be opened, please copy this link to your browser.
+ %(site)s
+ ''') % {'oauthuser_type': oauthuser.type, 'site': site}
+ send_email(emailto=[oauthuser.email, ], title=_('Congratulations on your successful binding!'), content=content)
+
+ # 重定向到绑定成功页面
+ url = reverse('oauth:bindsuccess', kwargs={'oauthid': id})
+ url = url + '?type=success'
+ return HttpResponseRedirect(url)
+
+
+class RequireEmailView(FormView):
+ """
+ 补充邮箱的类视图:显示表单收集用户邮箱,发送确认链接
+ """
+ form_class = RequireEmailForm # 使用的表单类
+ template_name = 'oauth/require_email.html' # 渲染的模板
+
+ def get(self, request, *args, **kwargs):
+ """处理GET请求:获取OAuth用户,若已填写邮箱则跳转(注释中为跳转逻辑,实际未启用)"""
+ oauthid = self.kwargs['oauthid']
+ oauthuser = get_object_or_404(OAuthUser, pk=oauthid)
+ if oauthuser.email:
+ pass # 若已填写邮箱,可在此处添加跳转逻辑
+ return super(RequireEmailView, self).get(request, *args, **kwargs)
+
+ def get_initial(self):
+ """初始化表单数据:预设oauthid字段"""
+ oauthid = self.kwargs['oauthid']
+ return {'email': '', 'oauthid': oauthid}
+
+ def get_context_data(self, **kwargs):
+ """补充上下文数据:添加用户头像(若存在)"""
+ oauthid = self.kwargs['oauthid']
+ oauthuser = get_object_or_404(OAuthUser, pk=oauthid)
+ if oauthuser.picture:
+ kwargs['picture'] = oauthuser.picture
+ return super(RequireEmailView, self).get_context_data(**kwargs)
+
+ def form_valid(self, form):
+ """处理表单验证通过:保存邮箱,生成确认链接并发送邮件"""
+ email = form.cleaned_data['email']
+ oauthid = form.cleaned_data['oauthid']
+ oauthuser = get_object_or_404(OAuthUser, pk=oauthid)
+ # 保存用户邮箱
+ oauthuser.email = email
+ oauthuser.save()
+ # 生成验证签名
+ sign = get_sha256(settings.SECRET_KEY + str(oauthuser.id) + settings.SECRET_KEY)
+ # 获取当前站点域名
+ site = get_current_site().domain
+ if settings.DEBUG: # 开发环境使用本地地址
+ site = '127.0.0.1:8000'
+ # 生成邮箱确认链接
+ path = reverse('oauth:email_confirm', kwargs={'id': oauthid, 'sign': sign})
+ url = "http://{site}{path}".format(site=site, path=path)
+ # 发送确认邮件
+ content = _("""
+ Please click the link below to bind your email
+
+ %(url)s
+
+ Thank you again!
+
+ If the link above cannot be opened, please copy this link to your browser.
+
+ %(url)s
+ """) % {'url': url}
+ send_email(emailto=[email, ], title=_('Bind your email'), content=content)
+ # 重定向到绑定成功页面(提示查收邮件)
+ url = reverse('oauth:bindsuccess', kwargs={'oauthid': oauthid})
+ url = url + '?type=email'
+ return HttpResponseRedirect(url)
+
+
+def bindsuccess(request, oauthid):
+ """
+ 绑定成功页面:根据类型显示不同的成功信息
+ :param request: 请求对象,包含'type'参数(email/success)
+ :param oauthid: OAuthUser的ID
+ :return: 渲染绑定成功模板
+ """
+ type = request.GET.get('type', None)
+ oauthuser = get_object_or_404(OAuthUser, pk=oauthid)
+ if type == 'email':
+ # 已发送验证邮件的提示
+ title = _('Bind your email')
+ content = _(
+ 'Congratulations, the binding is just one step away. '
+ 'Please log in to your email to check the email to complete the binding. Thank you.')
+ else:
+ # 绑定完成的提示
+ title = _('Binding successful')
+ content = _(
+ "Congratulations, you have successfully bound your email address. You can use %(oauthuser_type)s"
+ " to directly log in to this website without a password. You are welcome to continue to follow this site." % {
+ 'oauthuser_type': oauthuser.type})
+ return render(request, 'oauth/bindsuccess.html', {'title': title, 'content': content})
\ No newline at end of file