diff --git a/.idea/djangoBlogStudy.iml b/.idea/djangoBlogStudy.iml index 850ee37..7bd1d1b 100644 --- a/.idea/djangoBlogStudy.iml +++ b/.idea/djangoBlogStudy.iml @@ -2,7 +2,7 @@ - + diff --git a/src/.dockerignore b/src/.dockerignore index 2818c38..bd68a58 100644 --- a/src/.dockerignore +++ b/src/.dockerignore @@ -8,4 +8,5 @@ settings_production.py *.md docs/ logs/ -static/ \ No newline at end of file +static/ +.github/ diff --git a/src/.gitignore b/src/.gitignore index 3015816..76302b1 100644 --- a/src/.gitignore +++ b/src/.gitignore @@ -62,7 +62,6 @@ target/ # http://www.jetbrains.com/pycharm/webhelp/project.html .idea .iml -static/ # virtualenv venv/ diff --git a/src/accounts/admin.py b/src/accounts/admin.py index 271de0a..29d162a 100644 --- a/src/accounts/admin.py +++ b/src/accounts/admin.py @@ -7,34 +7,33 @@ from django.utils.translation import gettext_lazy as _ # Register your models here. from .models import BlogUser -# 用户创建表单 - 处理新用户创建时的密码验证和保存逻辑 + class BlogUserCreationForm(forms.ModelForm): - password1 = forms.CharField(label=_('password'), widget=forms.PasswordInput)# 密码字段 - 用户设置的密码输入 - password2 = forms.CharField(label=_('Enter password again'), widget=forms.PasswordInput)# 确认密码字段 - 再次输入密码用于验证一致性 + password1 = forms.CharField(label=_('password'), widget=forms.PasswordInput) + password2 = forms.CharField(label=_('Enter password again'), widget=forms.PasswordInput) class Meta: model = BlogUser fields = ('email',) - def clean_password2(self):# 密码验证方法 - 检查两次输入的密码是否一致 + def clean_password2(self): # Check that the two password entries match password1 = self.cleaned_data.get("password1") - password2 = self.cleaned_data.get("password2")# 获取两次输入的密码值 + password2 = self.cleaned_data.get("password2") if password1 and password2 and password1 != password2: - raise forms.ValidationError(_("passwords do not match"))# 密码一致性检查 - 如果两次输入不一致则抛出验证错误 + raise forms.ValidationError(_("passwords do not match")) return password2 - def save(self, commit=True):# 用户保存方法 - 处理密码哈希化和用户来源记录 + def save(self, commit=True): # Save the provided password in hashed format user = super().save(commit=False) user.set_password(self.cleaned_data["password1"]) - # 提交保存 - 设置用户来源为管理后台并保存到数据库 if commit: user.source = 'adminsite' user.save() return user -# 用户修改表单 - 继承Django默认用户修改表单 + class BlogUserChangeForm(UserChangeForm): class Meta: model = BlogUser @@ -44,7 +43,7 @@ class BlogUserChangeForm(UserChangeForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) -# 用户管理类 - 自定义用户模型在Django后台的显示和管理方式 + class BlogUserAdmin(UserAdmin): form = BlogUserChangeForm add_form = BlogUserCreationForm @@ -58,3 +57,4 @@ class BlogUserAdmin(UserAdmin): 'source') list_display_links = ('id', 'username') ordering = ('-id',) + search_fields = ('username', 'nickname', 'email') diff --git a/src/accounts/apps.py b/src/accounts/apps.py index e9be36f..9b3fc5a 100644 --- a/src/accounts/apps.py +++ b/src/accounts/apps.py @@ -1,5 +1,5 @@ from django.apps import AppConfig -# 账户应用配置类 + class AccountsConfig(AppConfig): name = 'accounts' diff --git a/src/accounts/forms.py b/src/accounts/forms.py index e62f722..fce4137 100644 --- a/src/accounts/forms.py +++ b/src/accounts/forms.py @@ -7,7 +7,7 @@ from django.utils.translation import gettext_lazy as _ from . import utils from .models import BlogUser -# 用户登录表单 - 处理用户登录认证 + class LoginForm(AuthenticationForm): def __init__(self, *args, **kwargs): super(LoginForm, self).__init__(*args, **kwargs) @@ -16,7 +16,7 @@ class LoginForm(AuthenticationForm): self.fields['password'].widget = widgets.PasswordInput( attrs={'placeholder': "password", "class": "form-control"}) -# 用户注册表单 - 处理新用户注册流程 + class RegisterForm(UserCreationForm): def __init__(self, *args, **kwargs): super(RegisterForm, self).__init__(*args, **kwargs) @@ -40,9 +40,8 @@ class RegisterForm(UserCreationForm): model = get_user_model() fields = ("username", "email") -# 密码重置表单 - 处理用户忘记密码时的重置流程 + class ForgetPasswordForm(forms.Form): - # 新密码字段 - 用户设置的新密码 new_password1 = forms.CharField( label=_("New password"), widget=forms.PasswordInput( @@ -52,7 +51,7 @@ class ForgetPasswordForm(forms.Form): } ), ) - # 确认密码字段 - 再次输入新密码用于验证 + new_password2 = forms.CharField( label="确认密码", widget=forms.PasswordInput( @@ -62,7 +61,7 @@ class ForgetPasswordForm(forms.Form): } ), ) - # 邮箱字段 - 用户注册时使用的邮箱地址 + email = forms.EmailField( label='邮箱', widget=forms.TextInput( @@ -72,7 +71,7 @@ class ForgetPasswordForm(forms.Form): } ), ) - # 验证码字段 - 邮箱接收的验证码 + code = forms.CharField( label=_('Code'), widget=forms.TextInput( @@ -83,7 +82,6 @@ class ForgetPasswordForm(forms.Form): ), ) - # 密码确认验证 - 检查两次输入的密码是否一致 def clean_new_password2(self): password1 = self.data.get("new_password1") password2 = self.data.get("new_password2") @@ -93,7 +91,6 @@ class ForgetPasswordForm(forms.Form): return password2 - # 邮箱验证 - 检查邮箱是否在系统中注册 def clean_email(self): user_email = self.cleaned_data.get("email") if not BlogUser.objects.filter( @@ -103,7 +100,6 @@ class ForgetPasswordForm(forms.Form): raise ValidationError(_("email does not exist")) return user_email - # 验证码验证 - 检查邮箱验证码是否正确 def clean_code(self): code = self.cleaned_data.get("code") error = utils.verify( @@ -114,7 +110,7 @@ class ForgetPasswordForm(forms.Form): raise ValidationError(error) return code -# 验证码请求表单 - 用于请求发送密码重置验证码 + class ForgetPasswordCodeForm(forms.Form): email = forms.EmailField( label=_('Email'), diff --git a/src/accounts/migrations/0001_initial.py b/src/accounts/migrations/0001_initial.py index 2c58aeb..d2fbcab 100644 --- a/src/accounts/migrations/0001_initial.py +++ b/src/accounts/migrations/0001_initial.py @@ -18,28 +18,28 @@ class Migration(migrations.Migration): migrations.CreateModel( name='BlogUser', fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),# 主键字段,唯一标识用户 - ('password', models.CharField(max_length=128, verbose_name='password')),# 认证字段,用户密码哈希值 - ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),# 登录信息字段 - ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), # 权限字段(超级用户状态标识) + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('password', models.CharField(max_length=128, verbose_name='password')), + ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), + ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), - ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),# 个人信息字段:用户姓氏 - ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),# 个人信息字段:用户名字 - ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),# 联系字段 - 用户邮箱地址 - ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),# 权限字段 - 后台管理权限标识 - ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),# 状态字段 - 用户账户激活状态 - ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),# 时间字段 - 用户注册时间 + ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), + ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), + ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), + ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), + ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), + ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), ('nickname', models.CharField(blank=True, max_length=100, verbose_name='昵称')), ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')), ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')), ('source', models.CharField(blank=True, max_length=100, verbose_name='创建来源')), - ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),# 权限关联字段 - 用户所属权限组 - ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')), # 权限关联字段 - 用户特定权限 + ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')), + ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')), ], options={ 'verbose_name': '用户', 'verbose_name_plural': '用户', - 'ordering': ['-id'],#按ID降序排列 + 'ordering': ['-id'], 'get_latest_by': 'id', }, managers=[ diff --git a/src/accounts/migrations/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.py b/src/accounts/migrations/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.py index e2881c8..1a9f509 100644 --- a/src/accounts/migrations/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.py +++ b/src/accounts/migrations/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.py @@ -11,40 +11,33 @@ class Migration(migrations.Migration): ] operations = [ - # 模型选项调整 - 更新用户模型的元数据配置 migrations.AlterModelOptions( name='bloguser', options={'get_latest_by': 'id', 'ordering': ['-id'], 'verbose_name': 'user', 'verbose_name_plural': 'user'}, ), - # 字段删除操作 - 移除旧的创建时间字段 migrations.RemoveField( model_name='bloguser', name='created_time', ), - # 字段删除操作 - 移除旧的最后修改时间字段 migrations.RemoveField( model_name='bloguser', name='last_mod_time', ), - # 字段添加操作 - 新增标准化的创建时间字段 migrations.AddField( model_name='bloguser', name='creation_time', field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'), ), - # 字段添加操作 - 新增标准化的最后修改时间字段 migrations.AddField( model_name='bloguser', name='last_modify_time', field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'), ), - # 字段调整操作 - 更新昵称字段的显示名称 migrations.AlterField( model_name='bloguser', name='nickname', field=models.CharField(blank=True, max_length=100, verbose_name='nick name'), ), - # 字段调整操作 - 更新来源字段的显示名称 migrations.AlterField( model_name='bloguser', name='source', diff --git a/src/accounts/models.py b/src/accounts/models.py index 049a10b..3baddbb 100644 --- a/src/accounts/models.py +++ b/src/accounts/models.py @@ -7,24 +7,21 @@ from djangoblog.utils import get_current_site # Create your models here. -# 博客用户模型类 - 继承Django抽象用户基类 + class BlogUser(AbstractUser): nickname = models.CharField(_('nick name'), max_length=100, blank=True) creation_time = models.DateTimeField(_('creation time'), default=now) last_modify_time = models.DateTimeField(_('last modify time'), default=now) source = models.CharField(_('create source'), max_length=100, blank=True) - # URL方法 - 获取用户详情页的相对URL路径 def get_absolute_url(self): return reverse( 'blog:author_detail', kwargs={ 'author_name': self.username}) - # 字符串表示 - 定义对象的字符串显示格式 def __str__(self): return self.email - # 完整URL方法 - 生成包含域名的用户完整详情页URL def get_full_url(self): site = get_current_site().domain url = "https://{site}{path}".format(site=site, diff --git a/src/accounts/tests.py b/src/accounts/tests.py index eefe446..6893411 100644 --- a/src/accounts/tests.py +++ b/src/accounts/tests.py @@ -10,7 +10,7 @@ from . import utils # Create your tests here. -# 账户功能测试类 - 继承Django测试基类 + class AccountTest(TestCase): def setUp(self): self.client = Client() @@ -22,7 +22,6 @@ class AccountTest(TestCase): ) self.new_test = "xxx123--=" - # 账户验证测试 - 测试超级用户登录和管理权限 def test_validate_account(self): site = get_current_site().domain user = BlogUser.objects.create_superuser( @@ -37,13 +36,13 @@ class AccountTest(TestCase): self.assertEqual(loginresult, True) response = self.client.get('/admin/') self.assertEqual(response.status_code, 200) - # 分类创建 - 为文章测试创建分类数据 + category = Category() category.name = "categoryaaa" category.creation_time = timezone.now() category.last_modify_time = timezone.now() category.save() - # 文章创建 - 创建测试文章数据 + article = Article() article.title = "nicetitleaaa" article.body = "nicecontentaaa" @@ -56,7 +55,6 @@ class AccountTest(TestCase): response = self.client.get(article.get_admin_url()) self.assertEqual(response.status_code, 200) - # 注册流程测试 - 测试用户完整注册流程 def test_validate_register(self): self.assertEquals( 0, len( @@ -120,7 +118,6 @@ class AccountTest(TestCase): 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() @@ -133,7 +130,6 @@ class AccountTest(TestCase): err = utils.verify("admin@123.com", code) self.assertEqual(type(err), str) - # 密码重置邮件发送成功测试 def test_forget_password_email_code_success(self): resp = self.client.post( path=reverse("account:forget_password_code"), @@ -143,7 +139,6 @@ class AccountTest(TestCase): self.assertEqual(resp.status_code, 200) self.assertEqual(resp.content.decode("utf-8"), "ok") - # 密码重置邮件发送失败测试 def test_forget_password_email_code_fail(self): resp = self.client.post( path=reverse("account:forget_password_code"), @@ -157,7 +152,6 @@ class AccountTest(TestCase): ) 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) @@ -180,7 +174,6 @@ class AccountTest(TestCase): self.assertNotEqual(blog_user, None) self.assertEqual(blog_user.check_password(data["new_password1"]), True) - # 不存在的用户密码重置测试 def test_forget_password_email_not_user(self): data = dict( new_password1=self.new_test, @@ -195,7 +188,7 @@ class AccountTest(TestCase): 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) diff --git a/src/accounts/urls.py b/src/accounts/urls.py index 4768eee..107a801 100644 --- a/src/accounts/urls.py +++ b/src/accounts/urls.py @@ -5,24 +5,24 @@ from . import views from .forms import LoginForm app_name = "accounts" -# URL路由配置 - 定义URL路径与视图函数的映射关系 -urlpatterns = [re_path(r'^login/$', # 用户登录路由 - 处理用户登录认证 + +urlpatterns = [re_path(r'^login/$', views.LoginView.as_view(success_url='/'), name='login', kwargs={'authentication_form': LoginForm}), - re_path(r'^register/$', # 用户注册路由 - 处理新用户注册 + re_path(r'^register/$', views.RegisterView.as_view(success_url="/"), name='register'), - re_path(r'^logout/$',# 用户退出路由 - 处理用户登出操作 + re_path(r'^logout/$', views.LogoutView.as_view(), name='logout'), - path(r'account/result.html',# 账户结果页面路由 - 显示操作结果信息(如验证结果) + path(r'account/result.html', views.account_result, name='result'), - re_path(r'^forget_password/$',# 密码重置路由 - 处理忘记密码重置请求 + re_path(r'^forget_password/$', views.ForgetPasswordView.as_view(), name='forget_password'), - re_path(r'^forget_password_code/$',# 密码重置验证码路由 - 处理密码重置验证码发送 + re_path(r'^forget_password_code/$', views.ForgetPasswordEmailCode.as_view(), name='forget_password_code'), ] diff --git a/src/accounts/user_login_backend.py b/src/accounts/user_login_backend.py index a039074..73cdca1 100644 --- a/src/accounts/user_login_backend.py +++ b/src/accounts/user_login_backend.py @@ -2,13 +2,11 @@ from django.contrib.auth import get_user_model from django.contrib.auth.backends import ModelBackend -# 用户认证方法 - 验证用户凭据并返回用户对象 class EmailOrUsernameModelBackend(ModelBackend): """ 允许使用用户名或邮箱登录 """ - # 用户认证方法 - 验证用户凭据并返回用户对象 def authenticate(self, request, username=None, password=None, **kwargs): if '@' in username: kwargs = {'email': username} @@ -21,7 +19,6 @@ class EmailOrUsernameModelBackend(ModelBackend): except get_user_model().DoesNotExist: return None - # 用户获取方法 - 根据用户ID获取用户对象 def get_user(self, username): try: return get_user_model().objects.get(pk=username) diff --git a/src/accounts/utils.py b/src/accounts/utils.py index 14665a7..4b94bdf 100644 --- a/src/accounts/utils.py +++ b/src/accounts/utils.py @@ -9,7 +9,7 @@ from djangoblog.utils import send_email _code_ttl = timedelta(minutes=5) -# 验证邮件发送函数 - 向指定邮箱发送验证码邮件 + def send_verify_email(to_mail: str, code: str, subject: str = _("Verify Email")): """发送重设密码验证码 Args: @@ -22,7 +22,7 @@ def send_verify_email(to_mail: str, code: str, subject: str = _("Verify Email")) "properly") % {'code': code} send_email([to_mail], subject, html_content) -# 验证码验证函数 - 检查用户输入的验证码是否正确 + def verify(email: str, code: str) -> typing.Optional[str]: """验证code是否有效 Args: @@ -38,12 +38,12 @@ def verify(email: str, code: str) -> typing.Optional[str]: if cache_code != code: return gettext("Verification code error") -# 验证码存储函数 - 将验证码保存到缓存系统 + def set_code(email: str, code: str): """设置code""" cache.set(email, code, _code_ttl.seconds) -# 验证码获取函数 - 从缓存系统中获取验证码 + def get_code(email: str) -> typing.Optional[str]: """获取code""" return cache.get(email) diff --git a/src/accounts/views.py b/src/accounts/views.py index a40d4ae..ae67aec 100644 --- a/src/accounts/views.py +++ b/src/accounts/views.py @@ -30,33 +30,30 @@ logger = logging.getLogger(__name__) # Create your views here. -# 用户注册视图 - 处理新用户注册流程 + class RegisterView(FormView): form_class = RegisterForm template_name = 'account/registration_form.html' - # 请求分发 - 添加CSRF保护装饰器 @method_decorator(csrf_protect) def dispatch(self, *args, **kwargs): return super(RegisterView, self).dispatch(*args, **kwargs) - # 表单验证成功处理 - 保存用户并发送验证邮件 def form_valid(self, form): if form.is_valid(): - # 用户创建 - 保存表单数据但不立即提交到数据库 user = form.save(False) user.is_active = False user.source = 'Register' user.save(True) site = get_current_site().domain sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id))) - # 开发环境调整 - 在调试模式下使用本地地址 + if settings.DEBUG: site = '127.0.0.1:8000' path = reverse('account:result') url = "http://{site}{path}?type=validation&id={id}&sign={sign}".format( site=site, path=path, id=user.id, sign=sign) - # 邮件内容构建 - 创建验证邮件的HTML内容 + content = """

请点击下面链接验证您的邮箱

@@ -67,7 +64,6 @@ class RegisterView(FormView): 如果上面链接无法打开,请将此链接复制至浏览器。 {url} """.format(url=url) - # 邮件发送 - 发送验证邮件到用户邮箱 send_email( emailto=[ user.email, @@ -83,7 +79,7 @@ class RegisterView(FormView): 'form': form }) -# 用户退出视图 - 处理用户登出操作 + class LogoutView(RedirectView): url = '/login/' @@ -96,7 +92,7 @@ class LogoutView(RedirectView): delete_sidebar_cache() return super(LogoutView, self).get(request, *args, **kwargs) -# 用户登录视图 - 处理用户登录认证 + class LoginView(FormView): form_class = LoginForm template_name = 'account/login.html' @@ -111,7 +107,6 @@ class LoginView(FormView): return super(LoginView, self).dispatch(request, *args, **kwargs) - # 上下文数据 - 获取重定向目标并添加到上下文 def get_context_data(self, **kwargs): redirect_to = self.request.GET.get(self.redirect_field_name) if redirect_to is None: @@ -120,7 +115,6 @@ class LoginView(FormView): return super(LoginView, self).get_context_data(**kwargs) - # 表单验证成功处理 - 执行用户登录操作 def form_valid(self, form): form = AuthenticationForm(data=self.request.POST, request=self.request) @@ -138,7 +132,6 @@ class LoginView(FormView): 'form': form }) - # 成功URL获取 - 处理登录后的重定向逻辑 def get_success_url(self): redirect_to = self.request.POST.get(self.redirect_field_name) @@ -148,7 +141,7 @@ class LoginView(FormView): redirect_to = self.success_url return redirect_to -# 账户结果页面视图 - 显示注册或验证结果 + def account_result(request): type = request.GET.get('type') id = request.GET.get('id') @@ -181,7 +174,7 @@ def account_result(request): else: return HttpResponseRedirect('/') -# 密码重置视图 - 处理用户忘记密码重置 + class ForgetPasswordView(FormView): form_class = ForgetPasswordForm template_name = 'account/forget_password.html' @@ -195,7 +188,7 @@ class ForgetPasswordView(FormView): else: return self.render_to_response({'form': form}) -# 密码重置验证码发送视图 - 处理验证码邮件发送 + class ForgetPasswordEmailCode(View): def post(self, request: HttpRequest): diff --git a/src/blog/admin.py b/src/blog/admin.py index 46c3420..f7ba264 100644 --- a/src/blog/admin.py +++ b/src/blog/admin.py @@ -6,7 +6,7 @@ from django.utils.html import format_html from django.utils.translation import gettext_lazy as _ # Register your models here. -from .models import Article +from .models import Article, Category, Tag, Links, SideBar, BlogSettings class ArticleForm(forms.ModelForm): @@ -55,6 +55,7 @@ class ArticlelAdmin(admin.ModelAdmin): 'article_order') list_display_links = ('id', 'title') list_filter = ('status', 'type', 'category') + date_hierarchy = 'creation_time' filter_horizontal = ('tags',) exclude = ('creation_time', 'last_modify_time') view_on_site = True @@ -63,6 +64,7 @@ class ArticlelAdmin(admin.ModelAdmin): draft_article, close_article_commentstatus, open_article_commentstatus] + raw_id_fields = ('author', 'category',) def link_to_category(self, obj): info = (obj.category._meta.app_label, obj.category._meta.model_name) @@ -89,6 +91,11 @@ class ArticlelAdmin(admin.ModelAdmin): site = get_current_site().domain return site + class Media: + js = ( + 'blog/js/ai_article_admin.js', # 加载我们刚才写的 JS + ) + class TagAdmin(admin.ModelAdmin): exclude = ('slug', 'last_mod_time', 'creation_time') diff --git a/src/blog/static/account/css/account.css b/src/blog/static/account/css/account.css new file mode 100644 index 0000000..7d4cec7 --- /dev/null +++ b/src/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/src/blog/static/account/js/account.js b/src/blog/static/account/js/account.js new file mode 100644 index 0000000..f1a8771 --- /dev/null +++ b/src/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/src/blog/static/assets/css/bootstrap.min.css b/src/blog/static/assets/css/bootstrap.min.css new file mode 100644 index 0000000..ed3905e --- /dev/null +++ b/src/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/src/blog/static/assets/css/docs.min.css b/src/blog/static/assets/css/docs.min.css new file mode 100644 index 0000000..3945197 --- /dev/null +++ b/src/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/src/blog/static/assets/css/ie10-viewport-bug-workaround.css b/src/blog/static/assets/css/ie10-viewport-bug-workaround.css new file mode 100644 index 0000000..4b9518e --- /dev/null +++ b/src/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/src/blog/static/assets/css/signin.css b/src/blog/static/assets/css/signin.css new file mode 100644 index 0000000..121fb0d --- /dev/null +++ b/src/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/src/blog/static/assets/css/todc-bootstrap.min.css b/src/blog/static/assets/css/todc-bootstrap.min.css new file mode 100644 index 0000000..66c9cb2 --- /dev/null +++ b/src/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/src/blog/static/assets/img/checkmark.png b/src/blog/static/assets/img/checkmark.png new file mode 100644 index 0000000..4bd0eb3 Binary files /dev/null and b/src/blog/static/assets/img/checkmark.png differ diff --git a/src/blog/static/assets/js/ie-emulation-modes-warning.js b/src/blog/static/assets/js/ie-emulation-modes-warning.js new file mode 100644 index 0000000..3f97ba5 --- /dev/null +++ b/src/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/src/blog/static/assets/js/ie10-viewport-bug-workaround.js b/src/blog/static/assets/js/ie10-viewport-bug-workaround.js new file mode 100644 index 0000000..479a6eb --- /dev/null +++ b/src/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/src/blog/static/blog/css/ai_chat.css b/src/blog/static/blog/css/ai_chat.css new file mode 100644 index 0000000..dc7da1d --- /dev/null +++ b/src/blog/static/blog/css/ai_chat.css @@ -0,0 +1,154 @@ +/* 悬浮按钮 */ +.ai-chat-btn { + position: fixed; + bottom: 30px; + right: 30px; + width: 60px; + height: 60px; + background: linear-gradient(135deg, #0A76F7, #00c6ff); + border-radius: 50%; + box-shadow: 0 4px 15px rgba(10, 118, 247, 0.4); + cursor: pointer; + z-index: 9999; + display: flex; + align-items: center; + justify-content: center; + transition: transform 0.3s ease; +} + +.ai-chat-btn:hover { + transform: scale(1.1); +} + +.ai-chat-btn svg { + width: 30px; + height: 30px; + fill: #fff; +} + +/* 聊天窗口容器 */ +.ai-chat-window { + position: fixed; + bottom: 100px; + right: 30px; + width: 380px; + height: 550px; + background: #fff; + border-radius: 16px; + box-shadow: 0 5px 30px rgba(0, 0, 0, 0.15); + display: none; /* 默认隐藏 */ + flex-direction: column; + z-index: 9998; + overflow: hidden; + font-family: system-ui, -apple-system, sans-serif; + border: 1px solid #eee; +} + +.ai-chat-window.active { + display: flex; + animation: slideIn 0.3s ease; +} + +@keyframes slideIn { + from { opacity: 0; transform: translateY(20px); } + to { opacity: 1; transform: translateY(0); } +} + +/* 头部 */ +.ai-header { + padding: 15px 20px; + background: #0A76F7; + color: #fff; + font-weight: 600; + display: flex; + justify-content: space-between; + align-items: center; +} + +.ai-header .close-btn { + cursor: pointer; + font-size: 20px; +} + +/* 消息区域 */ +.ai-messages { + flex: 1; + padding: 20px; + overflow-y: auto; + background: #f9f9f9; +} + +.message { + margin-bottom: 15px; + max-width: 85%; + line-height: 1.5; + font-size: 14px; + word-wrap: break-word; +} + +.message.user { + margin-left: auto; + background: #0A76F7; + color: #fff; + padding: 10px 15px; + border-radius: 12px 12px 0 12px; +} + +.message.ai { + margin-right: auto; + background: #fff; + color: #333; + padding: 10px 15px; + border-radius: 12px 12px 12px 0; + border: 1px solid #e0e0e0; +} + +/* 思考过程样式 */ +.message.thinking { + font-size: 12px; + color: #888; + font-style: italic; + border-left: 2px solid #ccc; + padding-left: 10px; + margin-bottom: 5px; + background: transparent; + border: none; + border-left: 3px solid #ddd; +} + +/* 输入区域 */ +.ai-input-area { + padding: 15px; + background: #fff; + border-top: 1px solid #eee; + display: flex; + gap: 10px; +} + +.ai-input-area input { + flex: 1; + padding: 10px; + border: 1px solid #ddd; + border-radius: 20px; + outline: none; + transition: border-color 0.2s; +} + +.ai-input-area input:focus { + border-color: #0A76F7; +} + +.ai-input-area button { + background: #0A76F7; + color: #fff; + border: none; + padding: 0 20px; + border-radius: 20px; + cursor: pointer; + font-weight: 600; +} + +.ai-input-area button:disabled { + background: #ccc; + cursor: not-allowed; +} \ No newline at end of file diff --git a/src/blog/static/blog/css/ie.css b/src/blog/static/blog/css/ie.css new file mode 100644 index 0000000..706f510 --- /dev/null +++ b/src/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/src/blog/static/blog/css/nprogress.css b/src/blog/static/blog/css/nprogress.css new file mode 100644 index 0000000..90c7b6c --- /dev/null +++ b/src/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/src/blog/static/blog/css/oauth_style.css b/src/blog/static/blog/css/oauth_style.css new file mode 100644 index 0000000..8af78af --- /dev/null +++ b/src/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/src/blog/static/blog/css/style.css b/src/blog/static/blog/css/style.css new file mode 100644 index 0000000..d43f7f3 --- /dev/null +++ b/src/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/src/blog/static/blog/css/theme.css b/src/blog/static/blog/css/theme.css new file mode 100644 index 0000000..560aec3 --- /dev/null +++ b/src/blog/static/blog/css/theme.css @@ -0,0 +1,597 @@ +/* + 色板: + - 主题蓝 (链接、高亮): #0A76F7 + - 背景灰蓝: #F4F7FC + - 内容区/卡片白: #FFFFFF + - 主文字黑: #333333 + - 辅助文字灰: #888888 + - 边框/分割线: #EAECEF +*/ + +/* 1. 全局与基础样式 */ +:root { + --theme-blue: #0A76F7; + --bg-color: #F4F7FC; + --card-bg: #FFFFFF; + --text-primary: #333333; + --text-secondary: #888888; + --border-color: #EAECEF; + --font-family-base: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; +} + +body { + font-family: var(--font-family-base); + background-color: var(--bg-color); + color: var(--text-primary); + line-height: 1.7; + font-size: 16px; + margin: 0; +} + +a { + color: var(--theme-blue); + text-decoration: none; + transition: color 0.2s ease-in-out; +} + +a:hover { + color: #085db8; + text-decoration: underline; +} + +img { + max-width: 100%; + height: auto; + border-radius: 8px; /* 给图片统一的圆角 */ +} + +/* 移除旧布局的边框和阴影 */ +#page.site { + padding: 0; + margin: 0; + max-width: 100%; + box-shadow: none; + background-color: transparent; +} + +.wrapper { + max-width: 1200px; /* 定义内容区域最大宽度 */ + margin: 0 auto; + padding: 20px; + display: flex; + gap: 24px; /* 主内容区和侧边栏的间距 */ +} + +/* === 2. 头部 (Header) 与导航栏 (Navigation) === */ +#masthead.site-header { + background-color: var(--card-bg); + padding: 0 20px; + border-bottom: 1px solid var(--border-color); + position: sticky; /* 导航栏吸顶 */ + top: 0; + z-index: 1000; + width: 100%; + box-sizing: border-box; +} + +.site-header .hgroup { + display: none; /* 隐藏旧的标题和描述,我们将用更现代的方式呈现 */ +} + +#site-navigation.main-navigation { + max-width: 1200px; + margin: 0 auto; + display: flex; + justify-content: space-between; /* Logo和菜单项两端对齐 */ + align-items: center; + height: 64px; +} + +/* 导航栏左侧的Logo */ +.main-navigation .nav-logo { + font-size: 24px; + font-weight: 700; + color: var(--text-primary); +} +.main-navigation .nav-logo a { + color: inherit; + text-decoration: none; +} +.main-navigation .nav-logo a:hover { + color: var(--theme-blue); +} + +/* 导航菜单项 */ +.main-navigation ul.nav-menu { + display: flex !important; /* 强制显示菜单 */ + list-style: none; + margin: 0; + padding: 0; + gap: 20px; +} + +.main-navigation li a { + color: var(--text-secondary); + font-weight: 500; + padding: 8px 12px; + border-radius: 6px; + transition: all 0.2s ease; + text-transform: none; /* 移除大写 */ + line-height: 1.5; +} + +.main-navigation li a:hover { + background-color: var(--bg-color); + color: var(--text-primary); + text-decoration: none; +} + +/* 当前激活的菜单项 */ +.main-navigation .current-menu-item > a, +.main-navigation .current_page_item > a { + background-color: var(--theme-blue); + color: #fff; +} + +/* 隐藏子菜单和旧的菜单切换按钮 */ +.main-navigation .sub-menu, +.menu-toggle { + display: none !important; +} + +/* === 3. 主内容区 (Main Content) === */ +#primary.site-content { + flex: 1; /* 占据剩余空间 */ + width: 100%; + margin: 0; +} + +/* 文章列表项的卡片样式 */ +.site-content article { + background-color: var(--card-bg); + border: 1px solid var(--border-color); + border-radius: 12px; + padding: 24px; + margin-bottom: 24px; + box-shadow: 0 4px 12px rgba(0,0,0,0.05); + transition: box-shadow 0.3s ease, transform 0.3s ease; +} + +.site-content article:hover { + transform: translateY(-5px); + box-shadow: 0 8px 20px rgba(0,0,0,0.08); +} + +.entry-header .entry-title { + font-size: 28px; + font-weight: 700; + margin-bottom: 12px; +} + +.entry-header .entry-title a { + color: var(--text-primary); + text-decoration: none; +} + +.entry-header .entry-title a:hover { + color: var(--theme-blue); +} + +/* 文章摘要/内容 */ +.entry-summary, .entry-content { + color: #555; + margin-bottom: 20px; +} + +.entry-content p { + margin-bottom: 1.5em; +} + +/* "Read more" 链接 */ +.entry-summary a.more-link, .read-more a { + display: inline-block; + font-weight: 600; + margin-top: 10px; +} + +/* 文章元信息 (作者、日期、分类、标签) */ +footer.entry-meta { + font-size: 14px; + color: var(--text-secondary); + border-top: 1px solid var(--border-color); + padding-top: 16px; + margin-top: 16px; +} + +footer.entry-meta a { + color: var(--text-secondary); + text-decoration: underline; + text-decoration-color: transparent; + transition: all 0.2s; +} + +footer.entry-meta a:hover { + color: var(--theme-blue); + text-decoration-color: var(--theme-blue); +} + +footer.entry-meta span { + margin-right: 15px; +} + +/* === 4. 侧边栏 (Sidebar) === */ +.widget-area { + width: 300px; /* 固定宽度 */ + flex-shrink: 0; + margin: 0; +} + +.widget-area .widget { + background-color: var(--card-bg); + border: 1px solid var(--border-color); + border-radius: 12px; + padding: 20px; + margin-bottom: 24px; + box-shadow: 0 4px 12px rgba(0,0,0,0.05); +} + +.widget-area .widget-title { + font-size: 18px; + font-weight: 700; + color: var(--text-primary); + margin-bottom: 16px; + padding-bottom: 10px; + border-bottom: 2px solid var(--theme-blue); +} + +.widget-area .widget ul { + list-style: none; + padding: 0; + margin: 0; +} + +.widget-area .widget li { + margin-bottom: 10px; +} + +.widget-area .widget li a { + color: #555; + text-decoration: none; + display: flex; + justify-content: space-between; +} + +.widget-area .widget li a:hover { + color: var(--theme-blue); +} + +/* 搜索框样式 */ +#searchform #s { + width: 100%; + padding: 10px; + border: 1px solid var(--border-color); + border-radius: 6px; + box-sizing: border-box; +} + +#searchform #searchsubmit { + display: none; +} + + +/* === 5. 页脚 (Footer) === */ +footer[role="contentinfo"] { + background-color: #2c3e50; /* 深蓝灰色背景 */ + color: #bdc3c7; /* 浅灰色文字 */ + padding: 40px 20px; + text-align: center; + font-size: 14px; + border-top: none; + max-width: 100%; +} + +footer[role="contentinfo"] a { + color: #ecf0f1; /* 白色链接 */ +} + +footer[role="contentinfo"] a:hover { + color: var(--theme-blue); +} + +.site-info { + margin-bottom: 10px; +} + +/* === 6. 文章详情页特定样式 === */ +.entry-content h1, .entry-content h2, .entry-content h3 { + font-weight: 700; + margin-top: 2em; + margin-bottom: 1em; +} +.entry-content h1 { font-size: 2em; } +.entry-content h2 { font-size: 1.5em; border-bottom: 1px solid var(--border-color); padding-bottom: .3em;} +.entry-content h3 { font-size: 1.25em; } + +.entry-content blockquote { + border-left: 4px solid var(--theme-blue); + background-color: var(--bg-color); + padding: 15px 20px; + margin: 20px 0; + font-style: italic; + color: #666; +} + +/* 代码块样式 */ +.entry-content pre { + background-color: #2d2d2d; + color: #f8f8f2; + padding: 20px; + border-radius: 8px; + overflow-x: auto; + border: none; +} +.entry-content code { + background-color: #e8e8e8; + padding: .2em .4em; + margin: 0; + font-size: 85%; + border-radius: 3px; + font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace; +} +.entry-content pre code { + background: none; + padding: 0; +} + +/* === 7. 分页导航 === */ +.pagination { + display: flex; + justify-content: center; + gap: 10px; + margin: 40px 0; + list-style: none; +} +.pagination .page-item a, .pagination .page-item span { + display: block; + padding: 10px 15px; + border: 1px solid var(--border-color); + border-radius: 6px; + background-color: var(--card-bg); + color: var(--text-secondary); + transition: all 0.2s; +} +.pagination .page-item a:hover { + border-color: var(--theme-blue); + color: var(--theme-blue); + text-decoration: none; +} +.pagination .page-item.active span { + background-color: var(--theme-blue); + border-color: var(--theme-blue); + color: #fff; +} + +/* 响应式设计 */ +@media screen and (max-width: 768px) { + .wrapper { + flex-direction: column; + } + .widget-area { + width: 100%; + } + .main-navigation .nav-menu { + display: none !important; + } +} + +/* === 8. 文章导航 (上一篇/下一篇) 卡片化 === */ +.nav-single { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 20px; + margin: 40px 0; + border-top: none; +} + +.nav-single .nav-previous, +.nav-single .nav-next { + width: 100%; + text-align: left; +} + +.nav-single a { + display: block; + padding: 20px; + border: 1px solid var(--border-color); + border-radius: 12px; + background-color: var(--card-bg); + box-shadow: 0 4px 12px rgba(0,0,0,0.05); + transition: all 0.3s ease; + height: 100%; /* 保证两张卡片等高 */ + box-sizing: border-box; +} + +.nav-single a:hover { + transform: translateY(-5px); + box-shadow: 0 8px 20px rgba(0,0,0,0.08); + border-color: var(--theme-blue); + text-decoration: none; +} + +/* 导航卡片内的标题和提示文字 */ +.nav-single .meta-nav { + display: block; + font-size: 14px; + font-weight: 500; + color: var(--text-secondary); + margin-bottom: 8px; +} + +.nav-single .nav-next { + text-align: right; /* 下一篇卡片内容右对齐 */ +} + +.nav-single .nav-next a { + text-align: right; +} + +/* === 9. 评论区 UI 优化 === */ + +/* 评论区整体容器 */ +.comments-area { + background-color: var(--card-bg); + border: 1px solid var(--border-color); + border-radius: 12px; + padding: 30px; + margin-top: 40px; + box-shadow: 0 4px 12px rgba(0,0,0,0.05); +} + +/* 评论区标题,如“发表评论” */ +.comments-area .comments-title, +.comments-area #reply-title { + font-size: 22px; + font-weight: 700; + color: var(--text-primary); + margin-bottom: 24px; + padding-bottom: 10px; + border-bottom: 1px solid var(--border-color); +} + +/* 评论列表 */ +.commentlist { + list-style: none; + padding: 0; +} + +.commentlist .comment { + border-bottom: 1px solid var(--border-color); + padding: 20px 0; +} +.commentlist .comment:last-child { + border-bottom: none; +} + +/* 评论头部:头像、作者、时间 */ +.commentlist .comment-author .avatar { + float: left; + margin-right: 15px; + border-radius: 50%; /* 圆形头像 */ + box-shadow: none; +} +.commentlist .fn { /* 评论作者 */ + font-weight: 600; + color: var(--text-primary); +} +.commentlist .comment-meta a { /* 评论时间 */ + font-size: 14px; + color: var(--text-secondary); +} + +/* 评论内容 */ +.comment-content { + padding-top: 10px; + clear: both; +} + +/* 评论回复按钮 */ +.reply a { + font-size: 14px; + font-weight: 600; +} + +/* 评论表单 */ +#respond form { + margin-top: 20px; +} + +#respond textarea { + width: 100%; + padding: 12px; + border: 1px solid var(--border-color); + border-radius: 8px; + min-height: 120px; + font-family: var(--font-family-base); + font-size: 16px; + box-sizing: border-box; + transition: border-color 0.2s; +} + +#respond textarea:focus { + outline: none; + border-color: var(--theme-blue); + box-shadow: 0 0 0 3px rgba(10, 118, 247, 0.2); +} + +/* 发表评论按钮 */ +#respond .form-submit input[type="submit"] { + background-color: var(--theme-blue); + color: #fff; + border: none; + padding: 10px 24px; + font-size: 16px; + font-weight: 600; + border-radius: 8px; + cursor: pointer; + transition: background-color 0.2s; + box-shadow: none; +} + +#respond .form-submit input[type="submit"]:hover { + background-color: #085db8; +} + +/* “支持markdown”提示文字 */ +#respond .comment-notes { + font-size: 14px; + color: var(--text-secondary); + margin-top: 10px; +} + +/* 登录后才能评论的提示 */ +.comments-area .comment-meta { + font-size: 16px; + font-weight: 500; +} + +/* ===================== + 夜间模式 (Dark Mode) + ===================== */ +[data-theme="dark"] { + --theme-blue: #3d8bfd; /* 稍微亮一点的蓝 */ + --bg-color: #121212; + --card-bg: #1e1e1e; + --text-primary: #e0e0e0; + --text-secondary: #a0a0a0; + --border-color: #333333; +} + +[data-theme="dark"] img { + filter: brightness(0.9); /* 图片稍微压暗一点,护眼 */ +} + +/* 评论区代码块在深色模式下的微调 */ +[data-theme="dark"] .entry-content pre { + background-color: #111; + border: 1px solid #333; +} + +[data-theme="dark"] .ai-chat-window { + background: #1e1e1e; + border-color: #333; +} + +[data-theme="dark"] .ai-input-area { + background: #1e1e1e; + border-color: #333; +} +[data-theme="dark"] .ai-input-area input { + background: #2d2d2d; + color: #fff; + border-color: #444; +} +[data-theme="dark"] .message.ai { + background: #2d2d2d; + color: #eee; + border-color: #444; +} \ No newline at end of file diff --git a/src/blog/static/blog/fonts/fonts.css b/src/blog/static/blog/fonts/fonts.css new file mode 100644 index 0000000..c1a29cf --- /dev/null +++ b/src/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/src/blog/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OUehpOqc.woff2 b/src/blog/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OUehpOqc.woff2 new file mode 100644 index 0000000..2c47cc5 Binary files /dev/null and b/src/blog/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OUehpOqc.woff2 differ diff --git a/src/blog/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OUuhp.woff2 b/src/blog/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OUuhp.woff2 new file mode 100644 index 0000000..601706a Binary files /dev/null and b/src/blog/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OUuhp.woff2 differ diff --git a/src/blog/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OVuhpOqc.woff2 b/src/blog/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OVuhpOqc.woff2 new file mode 100644 index 0000000..119f1d7 Binary files /dev/null and b/src/blog/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OVuhpOqc.woff2 differ diff --git a/src/blog/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OX-hpOqc.woff2 b/src/blog/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OX-hpOqc.woff2 new file mode 100644 index 0000000..d56688f Binary files /dev/null and b/src/blog/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OX-hpOqc.woff2 differ diff --git a/src/blog/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OXOhpOqc.woff2 b/src/blog/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OXOhpOqc.woff2 new file mode 100644 index 0000000..e1f546c Binary files /dev/null and b/src/blog/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OXOhpOqc.woff2 differ diff --git a/src/blog/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OXehpOqc.woff2 b/src/blog/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OXehpOqc.woff2 new file mode 100644 index 0000000..0f17e3d Binary files /dev/null and b/src/blog/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OXehpOqc.woff2 differ diff --git a/src/blog/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OXuhpOqc.woff2 b/src/blog/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OXuhpOqc.woff2 new file mode 100644 index 0000000..50d8183 Binary files /dev/null and b/src/blog/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OXuhpOqc.woff2 differ diff --git a/src/blog/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOUehpOqc.woff2 b/src/blog/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOUehpOqc.woff2 new file mode 100644 index 0000000..b935198 Binary files /dev/null and b/src/blog/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOUehpOqc.woff2 differ diff --git a/src/blog/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOUuhp.woff2 b/src/blog/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOUuhp.woff2 new file mode 100644 index 0000000..d77bb4c Binary files /dev/null and b/src/blog/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOUuhp.woff2 differ diff --git a/src/blog/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOVuhpOqc.woff2 b/src/blog/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOVuhpOqc.woff2 new file mode 100644 index 0000000..e293ffc Binary files /dev/null and b/src/blog/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOVuhpOqc.woff2 differ diff --git a/src/blog/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOX-hpOqc.woff2 b/src/blog/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOX-hpOqc.woff2 new file mode 100644 index 0000000..46fd61b Binary files /dev/null and b/src/blog/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOX-hpOqc.woff2 differ diff --git a/src/blog/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOXOhpOqc.woff2 b/src/blog/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOXOhpOqc.woff2 new file mode 100644 index 0000000..88a1616 Binary files /dev/null and b/src/blog/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOXOhpOqc.woff2 differ diff --git a/src/blog/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOXehpOqc.woff2 b/src/blog/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOXehpOqc.woff2 new file mode 100644 index 0000000..2100b6b Binary files /dev/null and b/src/blog/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOXehpOqc.woff2 differ diff --git a/src/blog/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOXuhpOqc.woff2 b/src/blog/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOXuhpOqc.woff2 new file mode 100644 index 0000000..d54c7c0 Binary files /dev/null and b/src/blog/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOXuhpOqc.woff2 differ diff --git a/src/blog/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0Udc1UAw.woff2 b/src/blog/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0Udc1UAw.woff2 new file mode 100644 index 0000000..683014d Binary files /dev/null and b/src/blog/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0Udc1UAw.woff2 differ diff --git a/src/blog/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0Vdc1UAw.woff2 b/src/blog/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0Vdc1UAw.woff2 new file mode 100644 index 0000000..72eb246 Binary files /dev/null and b/src/blog/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0Vdc1UAw.woff2 differ diff --git a/src/blog/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0Wdc1UAw.woff2 b/src/blog/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0Wdc1UAw.woff2 new file mode 100644 index 0000000..6da5562 Binary files /dev/null and b/src/blog/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0Wdc1UAw.woff2 differ diff --git a/src/blog/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0Xdc1UAw.woff2 b/src/blog/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0Xdc1UAw.woff2 new file mode 100644 index 0000000..2f22c67 Binary files /dev/null and b/src/blog/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0Xdc1UAw.woff2 differ diff --git a/src/blog/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0Zdc0.woff2 b/src/blog/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0Zdc0.woff2 new file mode 100644 index 0000000..28c6c76 Binary files /dev/null and b/src/blog/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0Zdc0.woff2 differ diff --git a/src/blog/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0adc1UAw.woff2 b/src/blog/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0adc1UAw.woff2 new file mode 100644 index 0000000..fdeb9a4 Binary files /dev/null and b/src/blog/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0adc1UAw.woff2 differ diff --git a/src/blog/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0ddc1UAw.woff2 b/src/blog/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0ddc1UAw.woff2 new file mode 100644 index 0000000..2a48105 Binary files /dev/null and b/src/blog/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0ddc1UAw.woff2 differ diff --git a/src/blog/static/blog/fonts/mem8YaGs126MiZpBA-UFUZ0bbck.woff2 b/src/blog/static/blog/fonts/mem8YaGs126MiZpBA-UFUZ0bbck.woff2 new file mode 100644 index 0000000..1ddef14 Binary files /dev/null and b/src/blog/static/blog/fonts/mem8YaGs126MiZpBA-UFUZ0bbck.woff2 differ diff --git a/src/blog/static/blog/fonts/mem8YaGs126MiZpBA-UFVZ0b.woff2 b/src/blog/static/blog/fonts/mem8YaGs126MiZpBA-UFVZ0b.woff2 new file mode 100644 index 0000000..1d5e847 Binary files /dev/null and b/src/blog/static/blog/fonts/mem8YaGs126MiZpBA-UFVZ0b.woff2 differ diff --git a/src/blog/static/blog/fonts/mem8YaGs126MiZpBA-UFVp0bbck.woff2 b/src/blog/static/blog/fonts/mem8YaGs126MiZpBA-UFVp0bbck.woff2 new file mode 100644 index 0000000..0e22822 Binary files /dev/null and b/src/blog/static/blog/fonts/mem8YaGs126MiZpBA-UFVp0bbck.woff2 differ diff --git a/src/blog/static/blog/fonts/mem8YaGs126MiZpBA-UFW50bbck.woff2 b/src/blog/static/blog/fonts/mem8YaGs126MiZpBA-UFW50bbck.woff2 new file mode 100644 index 0000000..f621005 Binary files /dev/null and b/src/blog/static/blog/fonts/mem8YaGs126MiZpBA-UFW50bbck.woff2 differ diff --git a/src/blog/static/blog/fonts/mem8YaGs126MiZpBA-UFWJ0bbck.woff2 b/src/blog/static/blog/fonts/mem8YaGs126MiZpBA-UFWJ0bbck.woff2 new file mode 100644 index 0000000..49018f9 Binary files /dev/null and b/src/blog/static/blog/fonts/mem8YaGs126MiZpBA-UFWJ0bbck.woff2 differ diff --git a/src/blog/static/blog/fonts/mem8YaGs126MiZpBA-UFWZ0bbck.woff2 b/src/blog/static/blog/fonts/mem8YaGs126MiZpBA-UFWZ0bbck.woff2 new file mode 100644 index 0000000..a69a2ef Binary files /dev/null and b/src/blog/static/blog/fonts/mem8YaGs126MiZpBA-UFWZ0bbck.woff2 differ diff --git a/src/blog/static/blog/fonts/mem8YaGs126MiZpBA-UFWp0bbck.woff2 b/src/blog/static/blog/fonts/mem8YaGs126MiZpBA-UFWp0bbck.woff2 new file mode 100644 index 0000000..fb5fb99 Binary files /dev/null and b/src/blog/static/blog/fonts/mem8YaGs126MiZpBA-UFWp0bbck.woff2 differ diff --git a/src/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hkIqOjjg.woff2 b/src/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hkIqOjjg.woff2 new file mode 100644 index 0000000..db9a5bd Binary files /dev/null and b/src/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hkIqOjjg.woff2 differ diff --git a/src/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hlIqOjjg.woff2 b/src/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hlIqOjjg.woff2 new file mode 100644 index 0000000..7a9e2e3 Binary files /dev/null and b/src/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hlIqOjjg.woff2 differ diff --git a/src/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hmIqOjjg.woff2 b/src/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hmIqOjjg.woff2 new file mode 100644 index 0000000..a9d17c0 Binary files /dev/null and b/src/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hmIqOjjg.woff2 differ diff --git a/src/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hnIqOjjg.woff2 b/src/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hnIqOjjg.woff2 new file mode 100644 index 0000000..b76038f Binary files /dev/null and b/src/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hnIqOjjg.woff2 differ diff --git a/src/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hoIqOjjg.woff2 b/src/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hoIqOjjg.woff2 new file mode 100644 index 0000000..06a53d5 Binary files /dev/null and b/src/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hoIqOjjg.woff2 differ diff --git a/src/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hrIqM.woff2 b/src/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hrIqM.woff2 new file mode 100644 index 0000000..94dc4e4 Binary files /dev/null and b/src/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hrIqM.woff2 differ diff --git a/src/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hvIqOjjg.woff2 b/src/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hvIqOjjg.woff2 new file mode 100644 index 0000000..8197c39 Binary files /dev/null and b/src/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hvIqOjjg.woff2 differ diff --git a/src/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhkIqOjjg.woff2 b/src/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhkIqOjjg.woff2 new file mode 100644 index 0000000..b9cd540 Binary files /dev/null and b/src/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhkIqOjjg.woff2 differ diff --git a/src/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhlIqOjjg.woff2 b/src/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhlIqOjjg.woff2 new file mode 100644 index 0000000..fa2e381 Binary files /dev/null and b/src/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhlIqOjjg.woff2 differ diff --git a/src/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhmIqOjjg.woff2 b/src/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhmIqOjjg.woff2 new file mode 100644 index 0000000..da3f7ec Binary files /dev/null and b/src/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhmIqOjjg.woff2 differ diff --git a/src/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhnIqOjjg.woff2 b/src/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhnIqOjjg.woff2 new file mode 100644 index 0000000..0b42119 Binary files /dev/null and b/src/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhnIqOjjg.woff2 differ diff --git a/src/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhoIqOjjg.woff2 b/src/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhoIqOjjg.woff2 new file mode 100644 index 0000000..36bdef1 Binary files /dev/null and b/src/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhoIqOjjg.woff2 differ diff --git a/src/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhrIqM.woff2 b/src/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhrIqM.woff2 new file mode 100644 index 0000000..4b60ed4 Binary files /dev/null and b/src/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhrIqM.woff2 differ diff --git a/src/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhvIqOjjg.woff2 b/src/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhvIqOjjg.woff2 new file mode 100644 index 0000000..d214090 Binary files /dev/null and b/src/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhvIqOjjg.woff2 differ diff --git a/src/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqW106F15M.woff2 b/src/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqW106F15M.woff2 new file mode 100644 index 0000000..0fb066c Binary files /dev/null and b/src/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqW106F15M.woff2 differ diff --git a/src/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWt06F15M.woff2 b/src/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWt06F15M.woff2 new file mode 100644 index 0000000..bc2aea0 Binary files /dev/null and b/src/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWt06F15M.woff2 differ diff --git a/src/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtE6F15M.woff2 b/src/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtE6F15M.woff2 new file mode 100644 index 0000000..fcce594 Binary files /dev/null and b/src/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtE6F15M.woff2 differ diff --git a/src/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtU6F15M.woff2 b/src/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtU6F15M.woff2 new file mode 100644 index 0000000..ffc8e9c Binary files /dev/null and b/src/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtU6F15M.woff2 differ diff --git a/src/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtk6F15M.woff2 b/src/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtk6F15M.woff2 new file mode 100644 index 0000000..6375e9c Binary files /dev/null and b/src/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtk6F15M.woff2 differ diff --git a/src/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWu06F15M.woff2 b/src/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWu06F15M.woff2 new file mode 100644 index 0000000..2e849f6 Binary files /dev/null and b/src/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWu06F15M.woff2 differ diff --git a/src/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWuU6F.woff2 b/src/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWuU6F.woff2 new file mode 100644 index 0000000..5de3fea Binary files /dev/null and b/src/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWuU6F.woff2 differ diff --git a/src/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWuk6F15M.woff2 b/src/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWuk6F15M.woff2 new file mode 100644 index 0000000..e5c936b Binary files /dev/null and b/src/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWuk6F15M.woff2 differ diff --git a/src/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWvU6F15M.woff2 b/src/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWvU6F15M.woff2 new file mode 100644 index 0000000..5cf8aff Binary files /dev/null and b/src/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWvU6F15M.woff2 differ diff --git a/src/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWxU6F15M.woff2 b/src/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWxU6F15M.woff2 new file mode 100644 index 0000000..bdc12e8 Binary files /dev/null and b/src/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWxU6F15M.woff2 differ diff --git a/src/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTS-muw.woff2 b/src/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTS-muw.woff2 new file mode 100644 index 0000000..b5d54e7 Binary files /dev/null and b/src/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTS-muw.woff2 differ diff --git a/src/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTS2mu1aB.woff2 b/src/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTS2mu1aB.woff2 new file mode 100644 index 0000000..bed5b67 Binary files /dev/null and b/src/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTS2mu1aB.woff2 differ diff --git a/src/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSCmu1aB.woff2 b/src/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSCmu1aB.woff2 new file mode 100644 index 0000000..9164ccb Binary files /dev/null and b/src/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSCmu1aB.woff2 differ diff --git a/src/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSGmu1aB.woff2 b/src/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSGmu1aB.woff2 new file mode 100644 index 0000000..08bed85 Binary files /dev/null and b/src/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSGmu1aB.woff2 differ diff --git a/src/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSKmu1aB.woff2 b/src/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSKmu1aB.woff2 new file mode 100644 index 0000000..307b214 Binary files /dev/null and b/src/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSKmu1aB.woff2 differ diff --git a/src/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSOmu1aB.woff2 b/src/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSOmu1aB.woff2 new file mode 100644 index 0000000..0b0b3a4 Binary files /dev/null and b/src/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSOmu1aB.woff2 differ diff --git a/src/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSumu1aB.woff2 b/src/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSumu1aB.woff2 new file mode 100644 index 0000000..4bce1d0 Binary files /dev/null and b/src/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSumu1aB.woff2 differ diff --git a/src/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSymu1aB.woff2 b/src/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSymu1aB.woff2 new file mode 100644 index 0000000..5bd7b8f Binary files /dev/null and b/src/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSymu1aB.woff2 differ diff --git a/src/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTUGmu1aB.woff2 b/src/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTUGmu1aB.woff2 new file mode 100644 index 0000000..b969602 Binary files /dev/null and b/src/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTUGmu1aB.woff2 differ diff --git a/src/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTVOmu1aB.woff2 b/src/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTVOmu1aB.woff2 new file mode 100644 index 0000000..a804b10 Binary files /dev/null and b/src/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTVOmu1aB.woff2 differ diff --git a/src/blog/static/blog/fonts/open-sans.css b/src/blog/static/blog/fonts/open-sans.css new file mode 100644 index 0000000..e6dd4a9 --- /dev/null +++ b/src/blog/static/blog/fonts/open-sans.css @@ -0,0 +1,600 @@ +/* cyrillic-ext */ +@font-face { + font-family: 'Open Sans'; + font-style: italic; + font-weight: 300; + font-stretch: 100%; + font-display: swap; + src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtE6F15M.woff2) format('woff2'); + unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; +} +/* cyrillic */ +@font-face { + font-family: 'Open Sans'; + font-style: italic; + font-weight: 300; + font-stretch: 100%; + font-display: swap; + src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWvU6F15M.woff2) format('woff2'); + unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} +/* greek-ext */ +@font-face { + font-family: 'Open Sans'; + font-style: italic; + font-weight: 300; + font-stretch: 100%; + font-display: swap; + src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtU6F15M.woff2) format('woff2'); + unicode-range: U+1F00-1FFF; +} +/* greek */ +@font-face { + font-family: 'Open Sans'; + font-style: italic; + font-weight: 300; + font-stretch: 100%; + font-display: swap; + src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWuk6F15M.woff2) format('woff2'); + unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF; +} +/* hebrew */ +@font-face { + font-family: 'Open Sans'; + font-style: italic; + font-weight: 300; + font-stretch: 100%; + font-display: swap; + src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWu06F15M.woff2) format('woff2'); + unicode-range: U+0307-0308, U+0590-05FF, U+200C-2010, U+20AA, U+25CC, U+FB1D-FB4F; +} +/* math */ +@font-face { + font-family: 'Open Sans'; + font-style: italic; + font-weight: 300; + font-stretch: 100%; + font-display: swap; + src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWxU6F15M.woff2) format('woff2'); + unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF; +} +/* symbols */ +@font-face { + font-family: 'Open Sans'; + font-style: italic; + font-weight: 300; + font-stretch: 100%; + font-display: swap; + src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqW106F15M.woff2) format('woff2'); + unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF; +} +/* vietnamese */ +@font-face { + font-family: 'Open Sans'; + font-style: italic; + font-weight: 300; + font-stretch: 100%; + font-display: swap; + src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtk6F15M.woff2) format('woff2'); + unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB; +} +/* latin-ext */ +@font-face { + font-family: 'Open Sans'; + font-style: italic; + font-weight: 300; + font-stretch: 100%; + font-display: swap; + src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWt06F15M.woff2) format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'Open Sans'; + font-style: italic; + font-weight: 300; + font-stretch: 100%; + font-display: swap; + src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWuU6F.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +/* cyrillic-ext */ +@font-face { + font-family: 'Open Sans'; + font-style: italic; + font-weight: 400; + font-stretch: 100%; + font-display: swap; + src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtE6F15M.woff2) format('woff2'); + unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; +} +/* cyrillic */ +@font-face { + font-family: 'Open Sans'; + font-style: italic; + font-weight: 400; + font-stretch: 100%; + font-display: swap; + src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWvU6F15M.woff2) format('woff2'); + unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} +/* greek-ext */ +@font-face { + font-family: 'Open Sans'; + font-style: italic; + font-weight: 400; + font-stretch: 100%; + font-display: swap; + src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtU6F15M.woff2) format('woff2'); + unicode-range: U+1F00-1FFF; +} +/* greek */ +@font-face { + font-family: 'Open Sans'; + font-style: italic; + font-weight: 400; + font-stretch: 100%; + font-display: swap; + src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWuk6F15M.woff2) format('woff2'); + unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF; +} +/* hebrew */ +@font-face { + font-family: 'Open Sans'; + font-style: italic; + font-weight: 400; + font-stretch: 100%; + font-display: swap; + src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWu06F15M.woff2) format('woff2'); + unicode-range: U+0307-0308, U+0590-05FF, U+200C-2010, U+20AA, U+25CC, U+FB1D-FB4F; +} +/* math */ +@font-face { + font-family: 'Open Sans'; + font-style: italic; + font-weight: 400; + font-stretch: 100%; + font-display: swap; + src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWxU6F15M.woff2) format('woff2'); + unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF; +} +/* symbols */ +@font-face { + font-family: 'Open Sans'; + font-style: italic; + font-weight: 400; + font-stretch: 100%; + font-display: swap; + src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqW106F15M.woff2) format('woff2'); + unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF; +} +/* vietnamese */ +@font-face { + font-family: 'Open Sans'; + font-style: italic; + font-weight: 400; + font-stretch: 100%; + font-display: swap; + src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtk6F15M.woff2) format('woff2'); + unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB; +} +/* latin-ext */ +@font-face { + font-family: 'Open Sans'; + font-style: italic; + font-weight: 400; + font-stretch: 100%; + font-display: swap; + src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWt06F15M.woff2) format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'Open Sans'; + font-style: italic; + font-weight: 400; + font-stretch: 100%; + font-display: swap; + src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWuU6F.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +/* cyrillic-ext */ +@font-face { + font-family: 'Open Sans'; + font-style: italic; + font-weight: 600; + font-stretch: 100%; + font-display: swap; + src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtE6F15M.woff2) format('woff2'); + unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; +} +/* cyrillic */ +@font-face { + font-family: 'Open Sans'; + font-style: italic; + font-weight: 600; + font-stretch: 100%; + font-display: swap; + src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWvU6F15M.woff2) format('woff2'); + unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} +/* greek-ext */ +@font-face { + font-family: 'Open Sans'; + font-style: italic; + font-weight: 600; + font-stretch: 100%; + font-display: swap; + src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtU6F15M.woff2) format('woff2'); + unicode-range: U+1F00-1FFF; +} +/* greek */ +@font-face { + font-family: 'Open Sans'; + font-style: italic; + font-weight: 600; + font-stretch: 100%; + font-display: swap; + src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWuk6F15M.woff2) format('woff2'); + unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF; +} +/* hebrew */ +@font-face { + font-family: 'Open Sans'; + font-style: italic; + font-weight: 600; + font-stretch: 100%; + font-display: swap; + src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWu06F15M.woff2) format('woff2'); + unicode-range: U+0307-0308, U+0590-05FF, U+200C-2010, U+20AA, U+25CC, U+FB1D-FB4F; +} +/* math */ +@font-face { + font-family: 'Open Sans'; + font-style: italic; + font-weight: 600; + font-stretch: 100%; + font-display: swap; + src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWxU6F15M.woff2) format('woff2'); + unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF; +} +/* symbols */ +@font-face { + font-family: 'Open Sans'; + font-style: italic; + font-weight: 600; + font-stretch: 100%; + font-display: swap; + src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqW106F15M.woff2) format('woff2'); + unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF; +} +/* vietnamese */ +@font-face { + font-family: 'Open Sans'; + font-style: italic; + font-weight: 600; + font-stretch: 100%; + font-display: swap; + src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtk6F15M.woff2) format('woff2'); + unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB; +} +/* latin-ext */ +@font-face { + font-family: 'Open Sans'; + font-style: italic; + font-weight: 600; + font-stretch: 100%; + font-display: swap; + src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWt06F15M.woff2) format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'Open Sans'; + font-style: italic; + font-weight: 600; + font-stretch: 100%; + font-display: swap; + src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWuU6F.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +/* cyrillic-ext */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 300; + font-stretch: 100%; + font-display: swap; + src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSKmu1aB.woff2) format('woff2'); + unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; +} +/* cyrillic */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 300; + font-stretch: 100%; + font-display: swap; + src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSumu1aB.woff2) format('woff2'); + unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} +/* greek-ext */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 300; + font-stretch: 100%; + font-display: swap; + src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSOmu1aB.woff2) format('woff2'); + unicode-range: U+1F00-1FFF; +} +/* greek */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 300; + font-stretch: 100%; + font-display: swap; + src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSymu1aB.woff2) format('woff2'); + unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF; +} +/* hebrew */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 300; + font-stretch: 100%; + font-display: swap; + src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTS2mu1aB.woff2) format('woff2'); + unicode-range: U+0307-0308, U+0590-05FF, U+200C-2010, U+20AA, U+25CC, U+FB1D-FB4F; +} +/* math */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 300; + font-stretch: 100%; + font-display: swap; + src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTVOmu1aB.woff2) format('woff2'); + unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF; +} +/* symbols */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 300; + font-stretch: 100%; + font-display: swap; + src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTUGmu1aB.woff2) format('woff2'); + unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF; +} +/* vietnamese */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 300; + font-stretch: 100%; + font-display: swap; + src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSCmu1aB.woff2) format('woff2'); + unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB; +} +/* latin-ext */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 300; + font-stretch: 100%; + font-display: swap; + src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSGmu1aB.woff2) format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 300; + font-stretch: 100%; + font-display: swap; + src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTS-muw.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +/* cyrillic-ext */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 400; + font-stretch: 100%; + font-display: swap; + src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSKmu1aB.woff2) format('woff2'); + unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; +} +/* cyrillic */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 400; + font-stretch: 100%; + font-display: swap; + src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSumu1aB.woff2) format('woff2'); + unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} +/* greek-ext */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 400; + font-stretch: 100%; + font-display: swap; + src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSOmu1aB.woff2) format('woff2'); + unicode-range: U+1F00-1FFF; +} +/* greek */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 400; + font-stretch: 100%; + font-display: swap; + src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSymu1aB.woff2) format('woff2'); + unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF; +} +/* hebrew */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 400; + font-stretch: 100%; + font-display: swap; + src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTS2mu1aB.woff2) format('woff2'); + unicode-range: U+0307-0308, U+0590-05FF, U+200C-2010, U+20AA, U+25CC, U+FB1D-FB4F; +} +/* math */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 400; + font-stretch: 100%; + font-display: swap; + src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTVOmu1aB.woff2) format('woff2'); + unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF; +} +/* symbols */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 400; + font-stretch: 100%; + font-display: swap; + src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTUGmu1aB.woff2) format('woff2'); + unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF; +} +/* vietnamese */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 400; + font-stretch: 100%; + font-display: swap; + src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSCmu1aB.woff2) format('woff2'); + unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB; +} +/* latin-ext */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 400; + font-stretch: 100%; + font-display: swap; + src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSGmu1aB.woff2) format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 400; + font-stretch: 100%; + font-display: swap; + src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTS-muw.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +/* cyrillic-ext */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 600; + font-stretch: 100%; + font-display: swap; + src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSKmu1aB.woff2) format('woff2'); + unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; +} +/* cyrillic */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 600; + font-stretch: 100%; + font-display: swap; + src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSumu1aB.woff2) format('woff2'); + unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} +/* greek-ext */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 600; + font-stretch: 100%; + font-display: swap; + src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSOmu1aB.woff2) format('woff2'); + unicode-range: U+1F00-1FFF; +} +/* greek */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 600; + font-stretch: 100%; + font-display: swap; + src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSymu1aB.woff2) format('woff2'); + unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF; +} +/* hebrew */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 600; + font-stretch: 100%; + font-display: swap; + src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTS2mu1aB.woff2) format('woff2'); + unicode-range: U+0307-0308, U+0590-05FF, U+200C-2010, U+20AA, U+25CC, U+FB1D-FB4F; +} +/* math */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 600; + font-stretch: 100%; + font-display: swap; + src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTVOmu1aB.woff2) format('woff2'); + unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF; +} +/* symbols */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 600; + font-stretch: 100%; + font-display: swap; + src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTUGmu1aB.woff2) format('woff2'); + unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF; +} +/* vietnamese */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 600; + font-stretch: 100%; + font-display: swap; + src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSCmu1aB.woff2) format('woff2'); + unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB; +} +/* latin-ext */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 600; + font-stretch: 100%; + font-display: swap; + src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSGmu1aB.woff2) format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 600; + font-stretch: 100%; + font-display: swap; + src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTS-muw.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} diff --git a/src/blog/static/blog/img/avatar.png b/src/blog/static/blog/img/avatar.png new file mode 100644 index 0000000..320756f Binary files /dev/null and b/src/blog/static/blog/img/avatar.png differ diff --git a/src/blog/static/blog/img/icon-sn.svg b/src/blog/static/blog/img/icon-sn.svg new file mode 100644 index 0000000..2c2da0a --- /dev/null +++ b/src/blog/static/blog/img/icon-sn.svg @@ -0,0 +1 @@ +icon-sn \ No newline at end of file diff --git a/src/blog/static/blog/js/ai_article_admin.js b/src/blog/static/blog/js/ai_article_admin.js new file mode 100644 index 0000000..4837dca --- /dev/null +++ b/src/blog/static/blog/js/ai_article_admin.js @@ -0,0 +1,193 @@ +document.addEventListener('DOMContentLoaded', function() { + console.log("AI Admin Script Loaded v2"); + + // 1. 寻找页面右上角的工具栏 (保持不变的逻辑) + let objectTools = document.querySelector('ul.object-tools'); + if (!objectTools) { + objectTools = document.createElement('ul'); + objectTools.className = 'object-tools'; + const contentTitle = document.querySelector('#content h1'); + if (contentTitle) { + contentTitle.insertAdjacentElement('afterend', objectTools); + objectTools.style.float = 'right'; + objectTools.style.marginTop = '-40px'; + objectTools.style.position = 'relative'; + objectTools.style.zIndex = '999'; + } else { + const contentMain = document.getElementById('content-main'); + if (contentMain) { + contentMain.insertBefore(objectTools, contentMain.firstChild); + } + } + } + + // 2. 动态创建按钮 (保持不变) + const li = document.createElement('li'); + const btn = document.createElement('a'); + btn.href = '#'; + btn.innerHTML = '🎨 AI 文生图'; + btn.className = 'historylink'; + btn.setAttribute('style', 'background-color: #0A76F7 !important; color: white !important; border-radius: 4px; padding: 5px 15px; text-decoration: none; display: inline-block;'); + li.appendChild(btn); + objectTools.appendChild(li); + + // 3. 动态创建弹窗 HTML (界面大改版) + const modalHtml = ` + + `; + + if (!document.getElementById('ai-modal')) { + document.body.insertAdjacentHTML('beforeend', modalHtml); + } + + // 4. 绑定事件逻辑 + const modal = document.getElementById('ai-modal'); + const loading = document.querySelector('.ai-loading'); + const resultArea = document.getElementById('ai-result-area'); + const promptInput = document.getElementById('ai-prompt-input'); + const previewImg = document.getElementById('ai-preview-img'); + const hiddenUrlInput = document.getElementById('ai-hidden-url'); + const downloadBtn = document.getElementById('ai-btn-download'); + const copyBtn = document.getElementById('ai-btn-copy'); + + // 打开弹窗 + btn.addEventListener('click', function(e) { + e.preventDefault(); + modal.style.display = 'block'; + }); + + // 关闭弹窗 + document.getElementById('ai-btn-cancel').addEventListener('click', function(e) { + e.preventDefault(); + modal.style.display = 'none'; + loading.style.display = 'none'; + }); + + // 复制功能 + copyBtn.addEventListener('click', function(e) { + e.preventDefault(); + if (hiddenUrlInput.value) { + navigator.clipboard.writeText(hiddenUrlInput.value).then(() => { + const originalText = copyBtn.innerText; + copyBtn.innerText = '✅ 已复制!'; + copyBtn.style.backgroundColor = '#198754'; + setTimeout(() => { + copyBtn.innerText = originalText; + copyBtn.style.backgroundColor = '#0A76F7'; + }, 2000); + }); + } + }); + + // 获取 Cookie + function getCookie(name) { + let cookieValue = null; + if (document.cookie && document.cookie !== '') { + const cookies = document.cookie.split(';'); + for (let i = 0; i < cookies.length; i++) { + const cookie = cookies[i].trim(); + if (cookie.substring(0, name.length + 1) === (name + '=')) { + cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); + break; + } + } + } + return cookieValue; + } + + // 生成逻辑 + document.getElementById('ai-btn-confirm').addEventListener('click', function(e) { + e.preventDefault(); + const prompt = promptInput.value; + if (!prompt) { alert('请输入描述!'); return; } + + loading.style.display = 'block'; + resultArea.style.display = 'none'; + + // 重置按钮状态 + copyBtn.innerText = '📋 复制链接到文章'; + copyBtn.style.backgroundColor = '#0A76F7'; + + fetch('/api/plugins/image/generate/', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': getCookie('csrftoken') + }, + body: JSON.stringify({prompt: prompt}) + }) + .then(response => response.json()) + .then(data => { + loading.style.display = 'none'; + if (data.status === 'success') { + resultArea.style.display = 'block'; + + // 1. 设置预览图 + previewImg.src = data.url; + + // 2. 设置隐藏的 Markdown 用于复制 + const markdownUrl = "![](" + data.url + ")"; + hiddenUrlInput.value = markdownUrl; + + // 3. 设置下载按钮链接 + downloadBtn.href = data.url; + // 尝试从 URL 提取文件名,例如 ai_123456.png + const fileName = data.url.split('/').pop(); + downloadBtn.setAttribute('download', fileName); + + } else { + alert('生成失败:' + data.message); + } + }) + .catch(error => { + loading.style.display = 'none'; + alert('网络请求错误:' + error); + }); + }); +}); \ No newline at end of file diff --git a/src/blog/static/blog/js/ai_chat.js b/src/blog/static/blog/js/ai_chat.js new file mode 100644 index 0000000..ac4d994 --- /dev/null +++ b/src/blog/static/blog/js/ai_chat.js @@ -0,0 +1,112 @@ +document.addEventListener('DOMContentLoaded', function() { + const chatBtn = document.getElementById('aiChatBtn'); + const chatWindow = document.getElementById('aiChatWindow'); + const closeBtn = document.querySelector('.ai-header .close-btn'); + const sendBtn = document.getElementById('aiSendBtn'); + const userInput = document.getElementById('aiUserInput'); + const messagesContainer = document.getElementById('aiMessages'); + + // 切换窗口显示 + chatBtn.addEventListener('click', () => { + chatWindow.classList.toggle('active'); + if(chatWindow.classList.contains('active')) { + userInput.focus(); + } + }); + + closeBtn.addEventListener('click', () => { + chatWindow.classList.remove('active'); + }); + + // 发送消息逻辑 + async function sendMessage() { + const text = userInput.value.trim(); + if (!text) return; + + // 1. 添加用户消息 + appendMessage('user', text); + userInput.value = ''; + userInput.disabled = true; + sendBtn.disabled = true; + + // 2. 创建AI消息占位符 + const thinkingDiv = appendMessage('thinking', 'DeepSeek 正在思考...'); + const answerDiv = appendMessage('ai', ''); + + try { + const response = await fetch('/api/plugins/ai/chat/', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + // 如果views.py去掉了@csrf_exempt,这里需要加上 'X-CSRFToken': getCookie('csrftoken') + }, + body: JSON.stringify({ message: text }) + }); + + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + let thinkingText = ''; + let answerText = ''; + + // 3. 读取流式数据 + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + const chunk = decoder.decode(value, { stream: true }); + const lines = chunk.split('\n\n'); + + for (const line of lines) { + if (line.startsWith('data: ')) { + try { + const jsonStr = line.replace('data: ', ''); + const data = JSON.parse(jsonStr); + + if (data.type === 'thinking') { + thinkingText += data.content; + thinkingDiv.textContent = thinkingText; // 实时更新思考内容 + } else if (data.type === 'answer') { + if (thinkingDiv.textContent === 'DeepSeek 正在思考...') { + thinkingDiv.style.display = 'none'; // 如果没真的思考,就隐藏占位 + } + answerText += data.content; + answerDiv.innerHTML = formatText(answerText); // 简单格式化 + messagesContainer.scrollTop = messagesContainer.scrollHeight; + } else if (data.type === 'error') { + answerDiv.textContent = "Error: " + data.content; + } + } catch (e) { + console.error('JSON Parse error', e); + } + } + } + } + } catch (error) { + console.error('Fetch error:', error); + appendMessage('ai', '抱歉,网络连接出现问题。'); + } finally { + userInput.disabled = false; + sendBtn.disabled = false; + userInput.focus(); + } + } + + sendBtn.addEventListener('click', sendMessage); + userInput.addEventListener('keypress', (e) => { + if (e.key === 'Enter') sendMessage(); + }); + + function appendMessage(type, content) { + const div = document.createElement('div'); + div.className = `message ${type}`; + div.textContent = content; + messagesContainer.appendChild(div); + messagesContainer.scrollTop = messagesContainer.scrollHeight; + return div; + } + + // 简单的换行转br处理,如果后续需要markdown支持可以引入marked.js + function formatText(text) { + return text.replace(/\n/g, '
'); + } +}); \ No newline at end of file diff --git a/src/blog/static/blog/js/blog.js b/src/blog/static/blog/js/blog.js new file mode 100644 index 0000000..c50dd7d --- /dev/null +++ b/src/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/src/blog/static/blog/js/html5.js b/src/blog/static/blog/js/html5.js new file mode 100644 index 0000000..6168aac --- /dev/null +++ b/src/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 { + console.log('MathJax渲染完成'); + // 触发自定义事件,通知其他脚本MathJax已就绪 + document.dispatchEvent(new CustomEvent('mathjaxReady')); + }).catch(error => { + console.error('MathJax渲染失败:', error); + }); + } + }, + // 输出配置 + chtml: { + scale: 1, + minScale: 0.5, + matchFontHeight: false, + displayAlign: 'center', + displayIndent: '0' + } + }; + } + + /** + * 加载MathJax库 + */ + function loadMathJax() { + console.log('检测到数学公式,开始加载MathJax...'); + + const script = document.createElement('script'); + script.src = 'https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js'; + script.async = true; + script.defer = true; + + script.onload = function() { + console.log('MathJax库加载成功'); + }; + + script.onerror = function() { + console.error('MathJax库加载失败,尝试备用CDN...'); + // 备用CDN + const fallbackScript = document.createElement('script'); + fallbackScript.src = 'https://polyfill.io/v3/polyfill.min.js?features=es6'; + fallbackScript.onload = function() { + const mathJaxScript = document.createElement('script'); + mathJaxScript.src = 'https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-MML-AM_CHTML'; + mathJaxScript.async = true; + document.head.appendChild(mathJaxScript); + }; + document.head.appendChild(fallbackScript); + }; + + document.head.appendChild(script); + } + + /** + * 初始化函数 + */ + function init() { + // 等待DOM完全加载 + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); + return; + } + + // 检测是否需要加载MathJax + if (hasMathFormulas()) { + // 先配置,再加载 + configureMathJax(); + loadMathJax(); + } else { + console.log('未检测到数学公式,跳过MathJax加载'); + } + } + + // 提供重新渲染的全局方法,供动态内容使用 + window.rerenderMathJax = function(element) { + if (window.MathJax && window.MathJax.typesetPromise) { + const target = element || document.body; + return window.MathJax.typesetPromise([target]); + } + return Promise.resolve(); + }; + + // 启动初始化 + init(); +})(); diff --git a/src/blog/static/blog/js/navigation.js b/src/blog/static/blog/js/navigation.js new file mode 100644 index 0000000..f7141bf --- /dev/null +++ b/src/blog/static/blog/js/navigation.js @@ -0,0 +1,55 @@ +/** + * Handles toggling the navigation menu for small screens and + * accessibility for submenu items. + */ +( function() { + var nav = document.getElementById( 'site-navigation' ), button, menu; + if ( ! nav ) { + return; + } + + button = nav.getElementsByTagName( 'button' )[0]; + menu = nav.getElementsByTagName( 'ul' )[0]; + if ( ! button ) { + return; + } + + // Hide button if menu is missing or empty. + if ( ! menu || ! menu.childNodes.length ) { + button.style.display = 'none'; + return; + } + + button.onclick = function() { + if ( -1 === menu.className.indexOf( 'nav-menu' ) ) { + menu.className = 'nav-menu'; + } + + if ( -1 !== button.className.indexOf( 'toggled-on' ) ) { + button.className = button.className.replace( ' toggled-on', '' ); + menu.className = menu.className.replace( ' toggled-on', '' ); + } else { + button.className += ' toggled-on'; + menu.className += ' toggled-on'; + } + }; +} )(); + +// Better focus for hidden submenu items for accessibility. +( function( $ ) { + $( '.main-navigation' ).find( 'a' ).on( 'focus.twentytwelve blur.twentytwelve', function() { + $( this ).parents( '.menu-item, .page_item' ).toggleClass( 'focus' ); + } ); + + if ( 'ontouchstart' in window ) { + $('body').on( 'touchstart.twentytwelve', '.menu-item-has-children > a, .page_item_has_children > a', function( e ) { + var el = $( this ).parent( 'li' ); + + if ( ! el.hasClass( 'focus' ) ) { + e.preventDefault(); + el.toggleClass( 'focus' ); + el.siblings( '.focus').removeClass( 'focus' ); + } + } ); + } +} )( jQuery ); diff --git a/src/blog/static/blog/js/nprogress.js b/src/blog/static/blog/js/nprogress.js new file mode 100644 index 0000000..d29c2aa --- /dev/null +++ b/src/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/src/blog/static/mathjax/js/mathjax-config.js b/src/blog/static/mathjax/js/mathjax-config.js new file mode 100644 index 0000000..158ba65 --- /dev/null +++ b/src/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/src/blog/static/pygments/default.css b/src/blog/static/pygments/default.css new file mode 100644 index 0000000..73e6e49 --- /dev/null +++ b/src/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/src/blog/templatetags/blog_tags.py b/src/blog/templatetags/blog_tags.py index d6cd5d5..024f2c8 100644 --- a/src/blog/templatetags/blog_tags.py +++ b/src/blog/templatetags/blog_tags.py @@ -51,7 +51,75 @@ def datetimeformat(data): @register.filter() @stringfilter def custom_markdown(content): - return mark_safe(CommonMarkdown.get_markdown(content)) + """ + 通用markdown过滤器,应用文章内容插件 + 主要用于文章内容处理 + """ + html_content = CommonMarkdown.get_markdown(content) + + # 然后应用插件过滤器优化HTML + from djangoblog.plugin_manage import hooks + from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME + optimized_html = hooks.apply_filters(ARTICLE_CONTENT_HOOK_NAME, html_content) + + return mark_safe(optimized_html) + + +@register.filter() +@stringfilter +def sidebar_markdown(content): + html_content = CommonMarkdown.get_markdown(content) + return mark_safe(html_content) + + +@register.simple_tag(takes_context=True) +def render_article_content(context, article, is_summary=False): + """ + 渲染文章内容,包含完整的上下文信息供插件使用 + + Args: + context: 模板上下文 + article: 文章对象 + is_summary: 是否为摘要模式(首页使用) + """ + if not article or not hasattr(article, 'body'): + return '' + + # 先转换Markdown为HTML + html_content = CommonMarkdown.get_markdown(article.body) + + # 如果是摘要模式,先截断内容再应用插件 + if is_summary: + # 截断HTML内容到合适的长度(约300字符) + from django.utils.html import strip_tags + from django.template.defaultfilters import truncatechars + + # 先去除HTML标签,截断纯文本,然后重新转换为HTML + plain_text = strip_tags(html_content) + truncated_text = truncatechars(plain_text, 300) + + # 重新转换截断后的文本为HTML(简化版,避免复杂的插件处理) + html_content = CommonMarkdown.get_markdown(truncated_text) + + # 然后应用插件过滤器,传递完整的上下文 + from djangoblog.plugin_manage import hooks + from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME + + # 获取request对象 + request = context.get('request') + + # 应用所有文章内容相关的插件 + # 注意:摘要模式下某些插件(如版权声明)可能不适用 + optimized_html = hooks.apply_filters( + ARTICLE_CONTENT_HOOK_NAME, + html_content, + article=article, + request=request, + context=context, + is_summary=is_summary # 传递摘要标志,插件可以据此调整行为 + ) + + return mark_safe(optimized_html) @register.simple_tag @@ -292,38 +360,49 @@ def load_article_detail(article, isindex, user): } -# return only the URL of the gravatar -# TEMPLATE USE: {{ email|gravatar_url:150 }} +# 返回用户头像URL +# 模板使用方法: {{ email|gravatar_url:150 }} @register.filter def gravatar_url(email, size=40): - """获得gravatar头像""" - cachekey = 'gravatat/' + email + """获得用户头像 - 优先使用OAuth头像,否则使用默认头像""" + cachekey = 'avatar/' + 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 + + # 检查OAuth用户是否有自定义头像 + usermodels = OAuthUser.objects.filter(email=email) + if usermodels: + # 过滤出有头像的用户 + users_with_picture = list(filter(lambda x: x.picture is not None, usermodels)) + if users_with_picture: + # 获取默认头像路径用于比较 + default_avatar_path = static('blog/img/avatar.png') + + # 优先选择非默认头像的用户,否则选择第一个 + non_default_users = [u for u in users_with_picture if u.picture != default_avatar_path and not u.picture.endswith('/avatar.png')] + selected_user = non_default_users[0] if non_default_users else users_with_picture[0] + + url = selected_user.picture + cache.set(cachekey, url, 60 * 60 * 24) # 缓存24小时 + + avatar_type = 'non-default' if non_default_users else 'default' + logger.info('Using {} OAuth avatar for {} from {}'.format(avatar_type, email, selected_user.type)) + return url + + # 使用默认头像 + url = static('blog/img/avatar.png') + cache.set(cachekey, url, 60 * 60 * 24) # 缓存24小时 + logger.info('Using default avatar for {}'.format(email)) + return url @register.filter def gravatar(email, size=40): - """获得gravatar头像""" + """获得用户头像HTML标签""" url = gravatar_url(email, size) return mark_safe( - '' % + '用户头像' % (url, size, size)) @@ -342,3 +421,134 @@ def query(qs, **kwargs): def addstr(arg1, arg2): """concatenate arg1 & arg2""" return str(arg1) + str(arg2) + + +# === 插件系统模板标签 === + +@register.simple_tag(takes_context=True) +def render_plugin_widgets(context, position, **kwargs): + """ + 渲染指定位置的所有插件组件 + + Args: + context: 模板上下文 + position: 位置标识 + **kwargs: 传递给插件的额外参数 + + Returns: + 按优先级排序的所有插件HTML内容 + """ + from djangoblog.plugin_manage.loader import get_loaded_plugins + + widgets = [] + + for plugin in get_loaded_plugins(): + try: + widget_data = plugin.render_position_widget( + position=position, + context=context, + **kwargs + ) + if widget_data: + widgets.append(widget_data) + except Exception as e: + logger.error(f"Error rendering widget from plugin {plugin.PLUGIN_NAME}: {e}") + + # 按优先级排序(数字越小优先级越高) + widgets.sort(key=lambda x: x['priority']) + + # 合并HTML内容 + html_parts = [widget['html'] for widget in widgets] + return mark_safe(''.join(html_parts)) + + +@register.simple_tag(takes_context=True) +def plugin_head_resources(context): + """渲染所有插件的head资源(仅自定义HTML,CSS已集成到压缩系统)""" + from djangoblog.plugin_manage.loader import get_loaded_plugins + + resources = [] + + for plugin in get_loaded_plugins(): + try: + # 只处理自定义head HTML(CSS文件已通过压缩系统处理) + head_html = plugin.get_head_html(context) + if head_html: + resources.append(head_html) + + except Exception as e: + logger.error(f"Error loading head resources from plugin {plugin.PLUGIN_NAME}: {e}") + + return mark_safe('\n'.join(resources)) + + +@register.simple_tag(takes_context=True) +def plugin_body_resources(context): + """渲染所有插件的body资源(仅自定义HTML,JS已集成到压缩系统)""" + from djangoblog.plugin_manage.loader import get_loaded_plugins + + resources = [] + + for plugin in get_loaded_plugins(): + try: + # 只处理自定义body HTML(JS文件已通过压缩系统处理) + body_html = plugin.get_body_html(context) + if body_html: + resources.append(body_html) + + except Exception as e: + logger.error(f"Error loading body resources from plugin {plugin.PLUGIN_NAME}: {e}") + + return mark_safe('\n'.join(resources)) + + +@register.inclusion_tag('plugins/css_includes.html') +def plugin_compressed_css(): + """插件CSS压缩包含模板""" + from djangoblog.plugin_manage.loader import get_loaded_plugins + + css_files = [] + for plugin in get_loaded_plugins(): + for css_file in plugin.get_css_files(): + css_url = plugin.get_static_url(css_file) + css_files.append(css_url) + + return {'css_files': css_files} + + +@register.inclusion_tag('plugins/js_includes.html') +def plugin_compressed_js(): + """插件JS压缩包含模板""" + from djangoblog.plugin_manage.loader import get_loaded_plugins + + js_files = [] + for plugin in get_loaded_plugins(): + for js_file in plugin.get_js_files(): + js_url = plugin.get_static_url(js_file) + js_files.append(js_url) + + return {'js_files': js_files} + + + + +@register.simple_tag(takes_context=True) +def plugin_widget(context, plugin_name, widget_type='default', **kwargs): + """ + 渲染指定插件的组件 + + 使用方式: + {% plugin_widget 'article_recommendation' 'bottom' article=article count=5 %} + """ + from djangoblog.plugin_manage.loader import get_plugin_by_slug + + plugin = get_plugin_by_slug(plugin_name) + if plugin and hasattr(plugin, 'render_template'): + try: + widget_context = {**context.flatten(), **kwargs} + template_name = f"{widget_type}.html" + return mark_safe(plugin.render_template(template_name, widget_context)) + except Exception as e: + logger.error(f"Error rendering plugin widget {plugin_name}.{widget_type}: {e}") + + return "" \ No newline at end of file diff --git a/src/blog/views.py b/src/blog/views.py index d5dc7ec..773bb75 100644 --- a/src/blog/views.py +++ b/src/blog/views.py @@ -152,12 +152,13 @@ class ArticleDetailView(DetailView): context = super(ArticleDetailView, self).get_context_data(**kwargs) article = self.object + + # 触发文章详情加载钩子,让插件可以添加额外的上下文数据 + from djangoblog.plugin_manage.hook_constants import ARTICLE_DETAIL_LOAD + hooks.run_action(ARTICLE_DETAIL_LOAD, article=article, context=context, request=self.request) + # Action Hook, 通知插件"文章详情已获取" hooks.run_action('after_article_body_get', article=article, request=self.request) - # # Filter Hook, 允许插件修改文章正文 - article.body = hooks.apply_filters(ARTICLE_CONTENT_HOOK_NAME, article.body, article=article, - request=self.request) - return context diff --git a/src/codecov.yml b/src/codecov.yml new file mode 100644 index 0000000..2298829 --- /dev/null +++ b/src/codecov.yml @@ -0,0 +1,87 @@ +codecov: + require_ci_to_pass: yes + +coverage: + precision: 2 + round: down + range: "70...100" + + status: + project: + default: + target: auto + threshold: 1% + informational: true + patch: + default: + target: auto + threshold: 1% + informational: true + +parsers: + gcov: + branch_detection: + conditional: yes + loop: yes + method: no + macro: no + +comment: + layout: "reach,diff,flags,tree" + behavior: default + require_changes: no + +ignore: + # Django 相关 + - "*/migrations/*" + - "manage.py" + - "*/settings.py" + - "*/wsgi.py" + - "*/asgi.py" + + # 测试相关 + - "*/tests/*" + - "*/test_*.py" + - "*/*test*.py" + + # 静态文件和模板 + - "*/static/*" + - "*/templates/*" + - "*/collectedstatic/*" + + # 国际化文件 + - "*/locale/*" + - "**/*.po" + - "**/*.mo" + + # 文档和部署 + - "*/docs/*" + - "*/deploy/*" + - "README*.md" + - "LICENSE" + - "Dockerfile" + - "docker-compose*.yml" + - "*.yaml" + - "*.yml" + + # 开发环境 + - "*/venv/*" + - "*/__pycache__/*" + - "*.pyc" + - ".coverage" + - "coverage.xml" + + # 日志文件 + - "*/logs/*" + - "*.log" + + # 特定文件 + - "*/whoosh_cn_backend.py" # 搜索后端 + - "*/elasticsearch_backend.py" # 搜索后端 + - "*/MemcacheStorage.py" # 缓存存储 + - "*/robot.py" # 机器人相关 + + # 配置文件 + - "codecov.yml" + - ".coveragerc" + - "requirements*.txt" diff --git a/src/comments/admin.py b/src/comments/admin.py index 8021a33..dbde14f 100644 --- a/src/comments/admin.py +++ b/src/comments/admin.py @@ -1,112 +1,49 @@ -# 导入 Django 管理后台模块 from django.contrib import admin - -# 导入 reverse 函数,用于反向解析 URL(根据命名 URL 生成实际路径) from django.urls import reverse - -# 导入 format_html,用于安全地格式化 HTML 字符串(防止 XSS) from django.utils.html import format_html - -# 导入 gettext_lazy 作为 _,用于字符串国际化(延迟翻译) from django.utils.translation import gettext_lazy as _ -#ssj -# 自定义管理员操作:批量禁用选中的评论 def disable_commentstatus(modeladmin, request, queryset): - """ - 将选中的评论设置为“不启用”状态(即不显示) - 此函数将在评论管理界面作为批量操作使用 - """ - # 批量更新:将 queryset 中所有评论的 is_enable 字段设为 False queryset.update(is_enable=False) -#ssj -# 自定义管理员操作:批量启用选中的评论 def enable_commentstatus(modeladmin, request, queryset): - """ - 将选中的评论设置为“启用”状态(即显示) - 此函数将在评论管理界面作为批量操作使用 - """ - # 批量更新:将 queryset 中所有评论的 is_enable 字段设为 True queryset.update(is_enable=True) -#ssj -# 为自定义操作设置在管理界面中显示的描述文本(支持国际化) -disable_commentstatus.short_description = _('Disable comments') # 显示为“禁用评论” -enable_commentstatus.short_description = _('Enable comments') # 显示为“启用评论” +disable_commentstatus.short_description = _('Disable comments') +enable_commentstatus.short_description = _('Enable comments') -#ssj -# 定义 Comment 模型在 Django 管理后台的显示和操作配置 class CommentAdmin(admin.ModelAdmin): - # 设置评论列表每页显示 20 条数据 list_per_page = 20 - - # 定义在评论列表页面显示的字段列 list_display = ( - 'id', # 评论的数据库 ID - 'body', # 评论正文内容 - 'link_to_userinfo', # 自定义方法:显示作者信息(带链接) - 'link_to_article', # 自定义方法:显示所属文章(带链接) - 'is_enable', # 是否启用(布尔值,显示为开关) - 'creation_time' # 评论创建时间 - ) - - # 设置哪些字段可以点击,跳转到该评论的编辑页面 + 'id', + 'body', + 'link_to_userinfo', + 'link_to_article', + 'is_enable', + 'creation_time') list_display_links = ('id', 'body', 'is_enable') - - # 添加右侧过滤侧边栏,允许按 is_enable 字段(是否启用)筛选评论 list_filter = ('is_enable',) - - # 在添加或编辑评论时,从表单中排除这两个字段 - # 因为 creation_time 和 last_modify_time 通常由代码自动处理(如默认值或保存时更新) exclude = ('creation_time', 'last_modify_time') - - # 注册自定义的批量操作,允许管理员在列表页选择多条评论执行“启用”或“禁用” actions = [disable_commentstatus, enable_commentstatus] + raw_id_fields = ('author', 'article') + search_fields = ('body',) -#ssj def link_to_userinfo(self, obj): - """ - 自定义列表字段:生成指向评论作者用户信息编辑页面的超链接 - - 参数: - obj: 当前评论对象 - - 返回: - HTML 格式的链接,链接文本为用户的昵称(若有),否则为邮箱 - """ - # 获取作者用户模型的 app_label 和 model_name(如 'auth', 'user') info = (obj.author._meta.app_label, obj.author._meta.model_name) - # 使用 reverse 构造 Django 管理后台中该用户的编辑页面 URL link = reverse('admin:%s_%s_change' % info, args=(obj.author.id,)) - # 返回一个安全的 HTML 链接 return format_html( u'%s' % (link, obj.author.nickname if obj.author.nickname else obj.author.email)) -#ssj def link_to_article(self, obj): - """ - 自定义列表字段:生成指向评论所属文章编辑页面的超链接 - - 参数: - obj: 当前评论对象 - - 返回: - HTML 格式的链接,链接文本为文章标题 - """ - # 获取文章模型的 app_label 和 model_name(如 'blog', 'article') info = (obj.article._meta.app_label, obj.article._meta.model_name) - # 构造该文章在管理后台的编辑页面 URL link = reverse('admin:%s_%s_change' % info, args=(obj.article.id,)) - # 返回一个安全的 HTML 链接 return format_html( u'%s' % (link, obj.article.title)) - # 为自定义的列表字段设置列标题(支持国际化) - link_to_userinfo.short_description = _('User') # 列标题显示为“用户” - link_to_article.short_description = _('Article') # 列标题显示为“文章” \ No newline at end of file + link_to_userinfo.short_description = _('User') + link_to_article.short_description = _('Article') diff --git a/src/comments/apps.py b/src/comments/apps.py index 9bd0622..ff01b77 100644 --- a/src/comments/apps.py +++ b/src/comments/apps.py @@ -1,9 +1,5 @@ -# 导入 Django 应用配置基类 from django.apps import AppConfig -#ssj -# 定义 comments 应用的配置类 + class CommentsConfig(AppConfig): - # 指定该配置对应的 Django 应用的完整 Python 路径 - # 即当前应用的包名(位于 INSTALLED_APPS 中) name = 'comments' diff --git a/src/comments/forms.py b/src/comments/forms.py index 2b8af47..e83737d 100644 --- a/src/comments/forms.py +++ b/src/comments/forms.py @@ -3,18 +3,11 @@ from django.forms import ModelForm from .models import Comment -#ssj -# 定义一个用于处理评论数据的表单类,继承自 Django 的 ModelForm + class CommentForm(ModelForm): - # 自定义字段:parent_comment_id - # 用于存储当前评论所回复的父评论的 ID parent_comment_id = forms.IntegerField( - widget=forms.HiddenInput, # 使用隐藏输入框(HTML ),不在页面上显示 - required=False # 非必填字段,因为一级评论没有父评论 - ) + widget=forms.HiddenInput, required=False) -#ssj class Meta: - model = Comment # 关联的数据库模型为 Comment - fields = ['body'] # 表单中需要包含的模型字段,仅包含 'body'(评论正文) - # 注意:其他字段如 author、article、creation_time 等通常在视图中自动填充,不暴露给用户 \ No newline at end of file + model = Comment + fields = ['body'] diff --git a/src/comments/migrations/0001_initial.py b/src/comments/migrations/0001_initial.py index e330305..61d1e53 100644 --- a/src/comments/migrations/0001_initial.py +++ b/src/comments/migrations/0001_initial.py @@ -1,74 +1,38 @@ -#ssj +# 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 -#ssj + class Migration(migrations.Migration): - # 表示这是一个初始迁移(即创建模型的第一次迁移) + initial = True - # 定义该迁移所依赖的其他迁移 - # 只有当这些依赖的迁移执行完成后,当前迁移才会执行 dependencies = [ - # 依赖于 blog 应用下的 '0001_initial' 迁移 - # 确保 blog 应用中的模型(如 Article)已创建 ('blog', '0001_initial'), - # 依赖于用户模型的迁移 - # 使用 migrations.swappable_dependency 和 settings.AUTH_USER_MODEL - # 是为了支持自定义用户模型(即项目可能使用了非默认的 User 模型) - # Django 会自动解析为实际使用的用户模型对应的迁移 migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] - # 定义本次迁移要执行的数据库操作列表 operations = [ - # 创建一个名为 'Comment' 的数据库模型(对应一张数据表) migrations.CreateModel( - name='Comment', # 模型名称 - fields=[ # 模型包含的字段列表 + name='Comment', + fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - # 主键字段:id - # 使用 BigAutoField(64位整数),自动递增,作为主键,不序列化为单独字段(serialize=False) ('body', models.TextField(max_length=300, verbose_name='正文')), - # 正文字段:body - # 文本字段,最大长度300字符,显示名称为“正文” ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')), - # 创建时间字段:created_time - # DateTimeField,默认值为当前时间(使用 django.utils.timezone.now 函数) - # 显示名称为“创建时间” ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')), - # 最后修改时间字段:last_mod_time - # DateTimeField,默认值也为当前时间(初始创建时与创建时间相同) - # 后续可通过逻辑更新 - # 显示名称为“修改时间” ('is_enable', models.BooleanField(default=True, verbose_name='是否显示')), - # 是否启用字段:is_enable - # BooleanField,默认为 True,表示评论是否显示 - # 用于软删除或审核功能 ('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.article', verbose_name='文章')), - # 外键字段:article - # 关联到 blog 应用中的 Article 模型('blog.article') - # on_delete=models.CASCADE:当文章被删除时,该评论也会被级联删除 - # 显示名称为“文章” ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='作者')), - # 外键字段:author - # 关联到当前项目配置的用户模型(支持自定义用户) - # 当用户被删除时,该用户的评论也会被级联删除 - # 显示名称为“作者” ('parent_comment', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='comments.comment', verbose_name='上级评论')), - # 外键字段:parent_comment - # 实现评论的嵌套/回复功能,指向同一模型(comments.Comment)的其他评论 - # blank=True, null=True:允许为空,表示可以是一级评论(顶级评论) - # on_delete=CASCADE:上级评论被删除时,其子评论也一并删除 - # 显示名称为“上级评论” ], - options={ # 模型的元选项(Meta options) - 'verbose_name': '评论', # 单数形式的可读名称 - 'verbose_name_plural': '评论', # 复数形式的可读名称(此处未复数化,与单数相同) - 'ordering': ['-id'], # 默认排序:按 id 降序排列(最新的在前) - 'get_latest_by': 'id', # 使用 latest() 方法时,默认按 id 获取最新一条记录 + options={ + 'verbose_name': '评论', + 'verbose_name_plural': '评论', + 'ordering': ['-id'], + 'get_latest_by': 'id', }, ), ] diff --git a/src/comments/migrations/0002_alter_comment_is_enable.py b/src/comments/migrations/0002_alter_comment_is_enable.py index df05a28..17c44db 100644 --- a/src/comments/migrations/0002_alter_comment_is_enable.py +++ b/src/comments/migrations/0002_alter_comment_is_enable.py @@ -1,28 +1,18 @@ -#ssj +# Generated by Django 4.1.7 on 2023-04-24 13:48 + from django.db import migrations, models -# migrations: 用于定义数据库迁移操作 -# models: 用于定义模型字段类型 -#ssj + class Migration(migrations.Migration): -# 定义一个迁移类,继承自 django.db.migrations.Migration dependencies = [ ('comments', '0001_initial'), ] -# 当前迁移的依赖列表 -# 表示必须先执行 comments 应用下的 '0001_initial' 迁移(即初始创建 Comment 模型的迁移) -# 之后才能执行当前迁移 - - -#ssj - operations = [ # 定义本次迁移要执行的数据库操作列表 - migrations.AlterField( # 执行一个“修改字段”的操作 - model_name='comment', # 要修改的模型名称(对应 comments 应用中的 Comment 模型) - name='is_enable', # 要修改的字段名 - field=models.BooleanField( # 新的字段定义 - default=False, # 将默认值从之前的 True 修改为 False - verbose_name='是否显示'), # 字段的可读名称保持不变 + operations = [ + migrations.AlterField( + model_name='comment', + name='is_enable', + field=models.BooleanField(default=False, verbose_name='是否显示'), ), ] diff --git a/src/comments/migrations/0003_alter_comment_options_remove_comment_created_time_and_more.py b/src/comments/migrations/0003_alter_comment_options_remove_comment_created_time_and_more.py index 8d42e73..a1ca970 100644 --- a/src/comments/migrations/0003_alter_comment_options_remove_comment_created_time_and_more.py +++ b/src/comments/migrations/0003_alter_comment_options_remove_comment_created_time_and_more.py @@ -3,66 +3,55 @@ from django.conf import settings from django.db import migrations, models import django.db.models.deletion -import django.utils.timezone # 用于引用 AUTH_USER_MODEL +import django.utils.timezone + -#ssj class Migration(migrations.Migration): - dependencies = [ # 当前迁移所依赖的其他迁移,必须按顺序执行完这些依赖迁移后,才能执行本迁移 - migrations.swappable_dependency(settings.AUTH_USER_MODEL), # 依赖于 blog 应用的第 5 个迁移(可能修改了 Article 或 Category 的选项) - ('blog', '0005_alter_article_options_alter_category_options_and_more'), # 依赖于 blog 应用的第 5 个迁移(可能修改了 Article 或 Category 的选项) - ('comments', '0002_alter_comment_is_enable'), # 依赖于 comments 应用的第 2 个迁移(之前已修改过 is_enable 字段的默认值) + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('blog', '0005_alter_article_options_alter_category_options_and_more'), + ('comments', '0002_alter_comment_is_enable'), ] -#ssj - operations = [ # 定义本次迁移要执行的所有数据库操作 - # 1. 修改模型的元选项(Meta options) + operations = [ migrations.AlterModelOptions( - name='comment', # 作用于 Comment 模型 + name='comment', options={'get_latest_by': 'id', 'ordering': ['-id'], 'verbose_name': 'comment', 'verbose_name_plural': 'comment'}, - # 单数名称改为英文小写 # 复数名称也改为英文 # 排序方式不变:按 id 降序(最新在前) # 获取最新记录仍按 id ), - # 2. 删除旧的创建时间字段(created_time) migrations.RemoveField( model_name='comment', name='created_time', ), - # 3. 删除旧的最后修改时间字段(last_mod_time) migrations.RemoveField( model_name='comment', name='last_mod_time', ), - # 4. 添加新的创建时间字段(creation_time),替代 created_time migrations.AddField( model_name='comment', name='creation_time', field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'), ), - # 5. 添加新的最后修改时间字段(last_modify_time),替代 last_mod_time migrations.AddField( model_name='comment', name='last_modify_time', field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'), ), - # 6. 修改 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'), ), - # 7. 修改 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'), ), - # 8. 修改 is_enable 字段:更新 verbose_name 为英文,并保持 default=False migrations.AlterField( model_name='comment', name='is_enable', field=models.BooleanField(default=False, verbose_name='enable'), ), - # 9. 修改 parent_comment 字段:更新 verbose_name 为英文 migrations.AlterField( model_name='comment', name='parent_comment', diff --git a/src/comments/models.py b/src/comments/models.py index ff59af9..7c3bbc8 100644 --- a/src/comments/models.py +++ b/src/comments/models.py @@ -3,84 +3,37 @@ from django.db import models from django.utils.timezone import now from django.utils.translation import gettext_lazy as _ -from ..blog.models import Article +from blog.models import Article -#ssj -# 定义评论模型,用于存储用户对文章的评论数据 +# Create your models here. + class Comment(models.Model): - # 评论正文内容 - # 使用 TextField 存储较长文本,限制最大长度为 300 字符 - # verbose_name 设置为 '正文',在管理后台等界面中显示为字段标签 body = models.TextField('正文', max_length=300) - - # 评论创建时间 - # 自动记录评论的创建时间,默认值为当前时间(now) - # verbose_name 为国际化字符串 'creation time' creation_time = models.DateTimeField(_('creation time'), default=now) - - # 评论最后修改时间 - # 记录评论最后一次被修改的时间,默认值为当前时间(通常在保存时更新) - # verbose_name 为国际化字符串 'last modify time' last_modify_time = models.DateTimeField(_('last modify time'), default=now) - - # 评论作者 - # 外键关联到用户模型(支持自定义用户模型) - # verbose_name 为 'author'(国际化),on_delete=models.CASCADE 表示用户删除时,其评论也级联删除 author = models.ForeignKey( settings.AUTH_USER_MODEL, verbose_name=_('author'), on_delete=models.CASCADE) - - # 所属文章 - # 外键关联到 Article 模型,表示该评论属于哪篇文章 - # verbose_name 为 'article'(国际化),级联删除:文章删除时,其所有评论也被删除 article = models.ForeignKey( Article, verbose_name=_('article'), on_delete=models.CASCADE) - - # 父级评论(用于实现嵌套评论/回复功能) - # 外键指向自身('self'),实现树形结构 - # blank=True, null=True 表示可以为空(即一级评论没有父评论) - # verbose_name 为 'parent comment'(国际化),级联删除:父评论删除时,其子评论也删除 parent_comment = models.ForeignKey( 'self', verbose_name=_('parent comment'), blank=True, null=True, on_delete=models.CASCADE) - - # 是否启用(是否显示) - # BooleanField 用于控制评论是否公开显示 - # default=False 表示默认不启用(需审核或手动开启) - # blank=False, null=False 表示该字段不可为空,必须有值 - # verbose_name 为 'enable'(国际化) is_enable = models.BooleanField(_('enable'), default=False, blank=False, null=False) -#ssj class Meta: - """ - 模型元数据类:定义模型的元信息,如排序、名称等 - """ - # 默认排序:按 id 降序排列(最新的评论在前) ordering = ['-id'] - - # 模型的可读名称(单数形式),用于管理后台等界面显示 verbose_name = _('comment') - - # 模型的可读名称(复数形式),此处与单数相同 verbose_name_plural = verbose_name - - # 指定获取最新一条记录时依据的字段 get_latest_by = 'id' -#ssj def __str__(self): - """ - 返回该评论对象的字符串表示 - 通常在管理后台或调试时显示 - 此处返回评论的正文内容(body) - """ return self.body diff --git a/src/comments/templatetags/comments_tags.py b/src/comments/templatetags/comments_tags.py index de28aa1..fde02b4 100644 --- a/src/comments/templatetags/comments_tags.py +++ b/src/comments/templatetags/comments_tags.py @@ -1,7 +1,5 @@ -# 导入 Django 模板系统模块 from django import template -# 创建一个模板标签库实例,用于注册自定义模板标签和过滤器 register = template.Library() @@ -10,31 +8,23 @@ def parse_commenttree(commentlist, comment): """获得当前评论子评论的列表 用法: {% parse_commenttree article_comments comment as childcomments %} """ - # 存储所有子评论的列表 datas = [] def parse(c): - # 从 commentlist 中筛选出 parent_comment 指向当前评论 c 且 is_enable=True 的子评论 childs = commentlist.filter(parent_comment=c, is_enable=True) for child in childs: - # 将符合条件的子评论添加到结果列表中 datas.append(child) - # 递归查找该子评论的子评论,实现深度优先遍历 parse(child) - # 从传入的根评论开始递归解析 parse(comment) - # 返回最终收集到的所有子评论列表 return datas @register.inclusion_tag('comments/tags/comment_item.html') def show_comment_item(comment, ischild): """评论""" - # 根据是否为子评论设置缩进层级 - # 如果是子评论(ischild=True),depth=1;否则 depth=2 depth = 1 if ischild else 2 return { - 'comment_item': comment, # 当前评论对象 - 'depth': depth # 缩进层级,用于模板中控制显示样式 + 'comment_item': comment, + 'depth': depth } diff --git a/src/comments/tests.py b/src/comments/tests.py index 1ca8c86..2a7f55f 100644 --- a/src/comments/tests.py +++ b/src/comments/tests.py @@ -1,111 +1,80 @@ 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 +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 # Create your tests here. -#ssj -# 定义一个基于事务的测试用例类,用于测试 comments 应用的相关功能 class CommentsTest(TransactionTestCase): def setUp(self): - # 创建 Django 测试客户端,用于模拟用户请求 self.client = Client() - - # 创建请求工厂,用于在视图测试中构造请求对象 self.factory = RequestFactory() - - # 导入博客设置模型(由于导入在方法内,可能为了延迟加载或避免循环导入) - from ..blog.models import BlogSettings - - # 创建一个博客全局设置实例 + 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") -#ssj def update_article_comment_status(self, article): - # 获取该文章的所有评论 comments = article.comment_set.all() for comment in comments: - comment.is_enable = True # 启用评论 - comment.save() # 保存更改 + comment.is_enable = True + comment.save() -#ssj 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.type = 'a' + article.status = 'p' article.save() - # 反向解析获取评论提交的 URL(根据命名空间和参数) comment_url = reverse( 'comments:postcomment', kwargs={ 'article_id': article.id}) - # 模拟 POST 请求发表第一条评论 response = self.client.post(comment_url, { 'body': '123ffffffffff' }) - # 断言:响应状态码应为 302(重定向),表示评论提交成功 self.assertEqual(response.status_code, 302) - # 重新获取文章对象(刷新数据) article = Article.objects.get(pk=article.pk) - # 此时评论未审核,comment_list() 返回的可见评论数应为 0 self.assertEqual(len(article.comment_list()), 0) - - # 手动启用该评论(模拟审核通过) self.update_article_comment_status(article) - # 此时可见评论数应为 1 + 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) - # 总共应有 2 条可见评论 self.assertEqual(len(article.comment_list()), 2) - - # 获取第一条评论的 ID,用于后续回复(作为父评论) parent_comment_id = article.comment_list()[0].id - # 发表一条回复(嵌套评论),包含复杂 Markdown 内容 response = self.client.post(comment_url, { 'body': ''' @@ -119,37 +88,22 @@ class CommentsTest(TransactionTestCase): [ddd](http://www.baidu.com) + ''', - 'parent_comment_id': parent_comment_id # 指定父评论 + '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) - # 总评论数应为 3(2 条一级 + 1 条子评论) self.assertEqual(len(article.comment_list()), 3) - - # 获取父评论对象 comment = Comment.objects.get(id=parent_comment_id) - # 解析从该父评论开始的所有子评论树 tree = parse_commenttree(article.comment_list(), comment) - # 子评论树中应包含 1 个子评论 self.assertEqual(len(tree), 1) - - # 测试包含标签 show_comment_item 是否正常返回数据 data = show_comment_item(comment, True) - # 返回值不应为 None self.assertIsNotNone(data) - - # 调用工具函数获取最大文章ID和评论ID(可能是用于生成唯一标识) s = get_max_articleid_commentid() - # 函数应返回有效值 self.assertIsNotNone(s) - # 从 utils 模块导入发送评论邮件函数 - from ..comments.utils import send_comment_email - # 测试发送评论通知邮件功能 - send_comment_email(comment) \ No newline at end of file + from comments.utils import send_comment_email + send_comment_email(comment) diff --git a/src/comments/urls.py b/src/comments/urls.py index 72cc92d..7df3fab 100644 --- a/src/comments/urls.py +++ b/src/comments/urls.py @@ -2,29 +2,10 @@ from django.urls import path from . import views -# 定义当前应用(comments)的 URL 命名空间 -# 在项目其他地方可以通过 'comments:xxx' 的方式反向解析 URL app_name = "comments" - -#ssj -# 定义 comments 应用的 URL 路由列表 urlpatterns = [ - # 路由配置:将特定 URL 模式映射到对应的视图处理逻辑 path( - # URL 模式: - # - 以 'article/' 开头 - # - 接一个整数类型的 article_id 参数(通过 捕获) - # - 然后是 'postcomment' 路径 - # 例如:/comments/article/123/postcomment/ 'article//postcomment', - - # 视图处理类: - # 使用基于类的视图 (Class-Based View) CommentPostView 的 as_view() 方法 - # as_view() 将类视图转换为可调用的视图函数,供 URL 路由使用 views.CommentPostView.as_view(), - - # URL 名称: - # 为该路由设置一个唯一名称 'postcomment' - # 在模板或代码中可通过 {% url 'comments:postcomment' article_id=123 %} 的方式引用 name='postcomment'), -] \ No newline at end of file +] diff --git a/src/comments/utils.py b/src/comments/utils.py index 053e5ac..f01dba7 100644 --- a/src/comments/utils.py +++ b/src/comments/utils.py @@ -2,49 +2,27 @@ import logging from django.utils.translation import gettext_lazy as _ -from ..djangoblog.utils import get_current_site -from ..djangoblog.utils import send_email +from djangoblog.utils import get_current_site +from djangoblog.utils import send_email -# 获取当前模块的 logger 实例,用于记录日志 logger = logging.getLogger(__name__) -#ssj def send_comment_email(comment): - # 获取当前站点的域名(如 example.com) site = get_current_site().domain - - # 邮件主题(支持国际化) subject = _('Thanks for your comment') - - # 构造文章的完整绝对 URL(使用 HTTPS) article_url = f"https://{site}{comment.article.get_absolute_url()}" - - # 构造发送给评论作者的 HTML 邮件内容 - # 内容包含感谢语、文章链接和标题,并支持变量替换 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 - } - - # 收件人邮箱:当前发表评论的用户 + %(article_url)s""") % {'article_url': article_url, 'article_title': comment.article.title} tomail = comment.author.email - - # 调用发送邮件的通用函数,发送感谢邮件 send_email([tomail], subject, html_content) - -#ssj try: - # 如果当前评论是回复(即存在父评论) if comment.parent_comment: - # 构造发送给被回复用户的 HTML 邮件内容 - # 通知其评论收到了新的回复 html_content = _("""Your comment on %(article_title)s
has received a reply.
%(comment_body)s
@@ -52,19 +30,9 @@ def send_comment_email(comment):
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 # 被回复的原始评论内容 - } - - # 收件人邮箱:被回复的评论的作者 + """) % {'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) - -#ssj except Exception as e: - # 如果在发送通知邮件过程中发生异常(如用户无邮箱等),记录错误日志 - logger.error(e) \ No newline at end of file + logger.error(e) diff --git a/src/comments/views.py b/src/comments/views.py index 562426f..ad9b2b9 100644 --- a/src/comments/views.py +++ b/src/comments/views.py @@ -6,111 +6,58 @@ 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 accounts.models import BlogUser +from blog.models import Article from .forms import CommentForm from .models import Comment -#ssj -# 定义一个基于类的视图,用于处理用户提交评论的请求 class CommentPostView(FormView): - # 指定该视图使用的表单类 form_class = CommentForm - - # 指定表单验证失败时,或需要渲染响应时使用的模板 template_name = 'blog/article_detail.html' -#ssj @method_decorator(csrf_protect) def dispatch(self, *args, **kwargs): return super(CommentPostView, self).dispatch(*args, **kwargs) -#ssj def get(self, request, *args, **kwargs): - # 从 URL 参数中获取文章 ID article_id = self.kwargs['article_id'] - - # 根据 ID 获取文章对象,若不存在则返回 404 错误 article = get_object_or_404(Article, pk=article_id) - - # 获取文章的绝对 URL url = article.get_absolute_url() - - # 重定向到文章页面,并锚定到 id="comments" 的元素(通常是评论列表) return HttpResponseRedirect(url + "#comments") -#ssj def form_invalid(self, form): - # 获取文章 ID article_id = self.kwargs['article_id'] - - # 获取对应的文章对象(404 安全获取) article = get_object_or_404(Article, pk=article_id) - # 渲染模板,将表单(含错误)和文章对象传递给模板 return self.render_to_response({ 'form': form, 'article': article }) -#ssj def form_valid(self, form): """提交的数据验证合法后的逻辑""" - - # 获取当前登录的用户 user = self.request.user - - # 获取用户对应的 BlogUser 对象 author = BlogUser.objects.get(pk=user.pk) - - # 获取 URL 中的文章 ID article_id = self.kwargs['article_id'] - - # 获取对应的文章对象(404 安全获取) article = get_object_or_404(Article, pk=article_id) -#ssj - # 检查文章是否允许评论 - # 如果文章的评论状态为 'c'(关闭)或文章状态为 'c'(草稿等),则禁止评论 if article.comment_status == 'c' or article.status == 'c': - # 抛出验证错误,阻止评论提交 raise ValidationError("该文章评论已关闭.") - - # 调用表单的 save 方法,但暂不提交到数据库(commit=False) comment = form.save(False) - - # 设置评论所属的文章 comment.article = article - - # 导入博客设置工具函数 - from ..djangoblog.utils import get_blog_setting - - # 获取当前博客的全局设置 + from djangoblog.utils import get_blog_setting settings = get_blog_setting() - - # 如果全局设置中不需要审核评论,则直接启用该评论 if not settings.comment_need_review: comment.is_enable = True - - # 设置评论作者 comment.author = author -#ssj - # 检查表单中是否包含父评论 ID(即是否为回复) if form.cleaned_data['parent_comment_id']: - # 根据父评论 ID 获取父评论对象 parent_comment = Comment.objects.get( pk=form.cleaned_data['parent_comment_id']) - # 建立父子评论关系 comment.parent_comment = parent_comment - # 将评论对象保存到数据库(此时所有字段已设置完毕) comment.save(True) - -#ssj - # 重定向到文章详情页,并定位到刚刚发表的评论 - # 使用锚点 #div-comment-{id} 定位到具体评论 return HttpResponseRedirect( "%s#div-comment-%d" % - (article.get_absolute_url(), comment.pk)) \ No newline at end of file + (article.get_absolute_url(), comment.pk)) diff --git a/src/deploy/k8s/deployment.yaml b/src/deploy/k8s/deployment.yaml index 414fdcc..b50c411 100644 --- a/src/deploy/k8s/deployment.yaml +++ b/src/deploy/k8s/deployment.yaml @@ -26,13 +26,13 @@ spec: name: djangoblog-env readinessProbe: httpGet: - path: / + path: /health/ port: 8000 initialDelaySeconds: 10 periodSeconds: 30 livenessProbe: httpGet: - path: / + path: /health/ port: 8000 initialDelaySeconds: 10 periodSeconds: 30 diff --git a/src/djangoblog/__init__.py b/src/djangoblog/__init__.py index b04347d..1e205f4 100644 --- a/src/djangoblog/__init__.py +++ b/src/djangoblog/__init__.py @@ -1,19 +1 @@ -#wwc -# 指定该 Django 应用的默认配置类。 -# 'default_app_config' 是一个特殊变量,Django 在加载应用时会自动识别它, -# 并使用指定的 AppConfig 子类来配置该应用的行为。 -# -# 作用说明: -# - 'djangoblog.apps.DjangoblogAppConfig' 是一个完整的 Python 导入路径, -# 指向 djangoblog 应用目录下 apps.py 文件中的 DjangoblogAppConfig 类。 -# - 该类通常用于自定义应用的初始化行为,例如: -# - 在应用启动时注册信号处理器 -# - 动态加载插件(如调用 load_plugins()) -# - 设置应用别名(verbose_name) -# - 执行其他启动时需要运行的代码 -# -# 注意: -# 从 Django 3.2 开始,显式设置 default_app_config 已不再是推荐做法, -# 更推荐在应用的 __init__.py 中直接导入 AppConfig 类,或在 INSTALLED_APPS 中使用完整的配置类路径。 -# 但在一些较老版本的 Django(如 1.7 ~ 3.1)中,这是启用自定义应用配置的主要方式。 -default_app_config = 'djangoblog.apps.DjangoblogAppConfig' \ No newline at end of file +default_app_config = 'djangoblog.apps.DjangoblogAppConfig' diff --git a/src/djangoblog/admin_site.py b/src/djangoblog/admin_site.py index 213d792..f120405 100644 --- a/src/djangoblog/admin_site.py +++ b/src/djangoblog/admin_site.py @@ -1,70 +1,32 @@ -# 导入 Django 管理后台相关模块 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 * # OwnTracks 位置追踪后台配置 -from ..owntracks.models import * # OwnTracks 模型 -from ..servermanager.admin import * # 服务器命令管理后台配置 -from ..servermanager.models import * # 服务器命令模型 - -#wwc -class DjangoBlogAdminSite(AdminSite): - """ - 自定义 Django 管理后台站点类,用于替代默认的 admin.site。 - - 功能说明: - - 继承自 Django 的 AdminSite,可高度定制管理后台的行为和界面。 - - 设置了自定义的页面标题和头部文本。 - - 限制访问权限,仅超级用户可访问。 - - 属性说明: - site_header: 管理后台页面顶部显示的标题(浏览器标签页标题)。 - site_title: 管理后台页面中显示的主标题(通常在左上角)。 - """ - site_header = 'djangoblog administration' # 浏览器标签和页面标题 - site_title = 'djangoblog site admin' # 页面中显示的主标题 - -#wwc - def __init__(self, name='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 * - 参数: - name (str): 站点名称,默认为 'admin'。 - 用于 URL 命名空间等场景。 - """ - super().__init__(name) # 调用父类初始化方法 -#wwc - def has_permission(self, request): - """ - 重写权限检查方法,控制谁可以访问此管理后台。 +class DjangoBlogAdminSite(AdminSite): + site_header = 'djangoblog administration' + site_title = 'djangoblog site admin' - 参数: - request (HttpRequest): 当前请求对象。 + def __init__(self, name='admin'): + super().__init__(name) - 返回值: - bool: 仅当用户是超级用户(is_superuser)时返回 True,否则返回 False。 - 这比 is_staff 更严格,确保只有最高权限用户可访问。 - """ + def has_permission(self, request): return request.user.is_superuser - # 被注释的 get_urls 方法示例: - # 可用于向管理后台添加自定义视图(如刷新缓存等运维功能) - # 示例中原本注册了一个 /admin/refresh/ 路径,指向 refresh_memcache 视图 - # 通过 self.admin_view 包装,确保只有通过认证的管理员才能访问 - # # def get_urls(self): # urls = super().get_urls() # from django.urls import path @@ -76,46 +38,27 @@ class DjangoBlogAdminSite(AdminSite): # return urls + my_urls -# 实例化自定义的管理站点 admin_site = DjangoBlogAdminSite(name='admin') -""" -创建一个 DjangoBlogAdminSite 的实例,命名为 'admin'。 -这个实例将作为整个项目的管理后台入口,替代 Django 默认的 admin.site。 -所有后续的 register 调用都使用这个自定义站点。 -""" - -# 注册博客功能相关模型到自定义管理后台 -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) # 评论模型 + 自定义管理配置 - -# 注册第三方登录相关模型 -admin_site.register(OAuthUser, OAuthUserAdmin) # OAuth 用户模型 + 自定义管理配置 -admin_site.register(OAuthConfig, OAuthConfigAdmin) # OAuth 配置模型 + 自定义管理配置 - -# 注册位置追踪相关模型 -admin_site.register(OwnTrackLog, OwnTrackLogsAdmin) # OwnTrack 日志模型 + 自定义管理配置 - -# 注册 Django 内置的 Site 模型(使用默认 SiteAdmin) + +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) + +admin_site.register(OAuthUser, OAuthUserAdmin) +admin_site.register(OAuthConfig, OAuthConfigAdmin) + +admin_site.register(OwnTrackLog, OwnTrackLogsAdmin) + admin_site.register(Site, SiteAdmin) -# 注册 Django 内置的管理操作日志 LogEntry 模型(使用自定义的 LogEntryAdmin) admin_site.register(LogEntry, LogEntryAdmin) -""" -通过以上 register 调用,所有注册的模型都将在自定义的管理后台界面中显示, -并使用对应的 Admin 类进行展示和操作控制。 -最终,这个 admin_site 实例会在 urls.py 中作为管理后台的视图入口使用。 -""" \ No newline at end of file diff --git a/src/djangoblog/apps.py b/src/djangoblog/apps.py index 99851fc..d29e318 100644 --- a/src/djangoblog/apps.py +++ b/src/djangoblog/apps.py @@ -1,41 +1,11 @@ from django.apps import AppConfig -#wwc class DjangoblogAppConfig(AppConfig): - """ - Django 应用配置类,用于定义 'djangoblog' 应用的配置信息。 - - 这个类继承自 Django 的 AppConfig,是应用级别的配置中心, - 在 Django 启动时被加载,用于指定应用的行为和初始化逻辑。 - """ - - # 设置此应用中所有模型默认使用的主键字段类型。 - # 使用 BigAutoField 表示默认主键为 64 位整数(支持更大范围的 ID), - # 可避免在数据量大时出现整数溢出问题。 default_auto_field = 'django.db.models.BigAutoField' - - # 指定该应用的完整 Python 导入路径。 - # 必须与应用在项目中的实际路径一致,Django 通过此属性识别应用。 name = 'djangoblog' -#wwc - def ready(self): - """ - 应用准备就绪时调用的方法。 - Django 在应用注册系统完全加载后会自动调用此方法,是执行应用级初始化代码的推荐入口点。 - 常用于: - - 启动后台任务(如 Celery) - - 注册信号处理器(signal handlers) - - 动态加载插件或模块 - - 初始化缓存等资源 - - 注意:此方法在整个 Django 启动过程中只会被调用一次。 - """ - # 调用父类的 ready() 方法,确保父类的初始化逻辑正常执行 + def ready(self): super().ready() - - # 导入插件加载函数并执行插件加载 - # 通过在 ready() 中调用 load_plugins(),确保插件系统在 Django 完全启动后才被激活 - # 这样可以安全地访问 Django 的 ORM 和其他已注册的组件 + # Import and load plugins here from .plugin_manage.loader import load_plugins - load_plugins() \ No newline at end of file + load_plugins() \ No newline at end of file diff --git a/src/djangoblog/blog_signals.py b/src/djangoblog/blog_signals.py index 56c9ab1..393f441 100644 --- a/src/djangoblog/blog_signals.py +++ b/src/djangoblog/blog_signals.py @@ -9,108 +9,61 @@ 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 - -# 获取当前模块的日志记录器,用于记录信号处理过程中的运行信息和错误 +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 = logging.getLogger(__name__) -# 自定义信号:OAuth 用户登录信号 -# 用于在 OAuth 用户登录成功后触发特定逻辑(如头像处理、缓存清理) -# 参数:id - OAuthUser 对象的数据库 ID oauth_user_login_signal = django.dispatch.Signal(['id']) +send_email_signal = django.dispatch.Signal( + ['emailto', 'title', 'content']) -# 自定义信号:发送邮件信号 -# 用于解耦邮件发送逻辑,其他模块可通过触发此信号来请求发送邮件 -# 参数: -# - emailto: 收件人邮箱列表 -# - title: 邮件标题 -# - content: 邮件正文(HTML) -send_email_signal = django.dispatch.Signal(['emailto', 'title', 'content']) -#wwc @receiver(send_email_signal) def send_email_signal_handler(sender, **kwargs): - """ - 处理「发送邮件」信号的回调函数。 - - 当系统中触发 send_email_signal 时,Django 会调用此函数发送实际邮件, - 并将发送结果记录到数据库日志中。 - - 参数: - sender: 发送信号的对象(通常不使用) - **kwargs: 信号传递的参数,包含 emailto, title, content - - 流程: - 1. 从 kwargs 中提取邮件信息 - 2. 创建 EmailMultiAlternatives 对象(支持 HTML 邮件) - 3. 记录邮件发送日志(EmailSendLog) - 4. 尝试发送邮件,捕获异常并记录结果 - 5. 保存日志到数据库 - """ emailto = kwargs['emailto'] title = kwargs['title'] content = kwargs['content'] - # 创建支持 HTML 内容的邮件对象 msg = EmailMultiAlternatives( title, content, from_email=settings.DEFAULT_FROM_EMAIL, to=emailto) - msg.content_subtype = "html" # 设置内容类型为 HTML + msg.content_subtype = "html" - # 创建邮件发送日志对象 - from ..servermanager.models import EmailSendLog + from servermanager.models import EmailSendLog log = EmailSendLog() log.title = title log.content = content - log.emailto = ','.join(emailto) # 将邮箱列表转为逗号分隔字符串 + log.emailto = ','.join(emailto) try: - # 发送邮件,返回值为成功发送的邮件数量 result = msg.send() - log.send_result = result > 0 # 成功发送至少一封即为成功 + log.send_result = result > 0 except Exception as e: - # 发送失败时记录错误日志,并标记发送结果为失败 logger.error(f"失败邮箱号: {emailto}, {e}") log.send_result = False - # 无论成功或失败,都保存日志记录 log.save() -#wwc + @receiver(oauth_user_login_signal) def oauth_user_login_signal_handler(sender, **kwargs): - """ - 处理「OAuth 用户登录」信号的回调函数。 - - 当 OAuth 用户登录成功后触发,主要执行: - 1. 检查并处理用户头像 URL(确保为本站域名) - 2. 清理侧边栏缓存(确保显示最新信息) - - 参数: - sender: 发送信号的对象 - **kwargs: 包含 id(OAuthUser 的主键) - """ id = kwargs['id'] oauthuser = OAuthUser.objects.get(id=id) site = get_current_site().domain - - # 如果用户头像 URL 不包含当前站点域名,则重新保存头像 - # 防止外链失效,提升稳定性 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() -#wwc + @receiver(post_save) def model_post_save_callback( sender, @@ -120,101 +73,50 @@ def model_post_save_callback( using, update_fields, **kwargs): - """ - 全局模型保存后信号处理器。 - 监听所有模型的 post_save 信号,根据保存的模型类型执行相应逻辑。 - - 主要功能: - 1. 文章/页面更新时通知搜索引擎(百度主动推送) - 2. 评论发布时清理相关缓存并发送邮件通知 - 3. 特定模型更新后清理缓存 - - 参数: - sender: 保存的模型类 - instance: 保存的模型实例 - created: 是否为新建对象(True=新增,False=更新) - raw: 是否为原始数据导入(如 loaddata) - using: 使用的数据库别名 - update_fields: 本次保存更新的字段集合(可选) - **kwargs: 其他参数 - - 注意:LogEntry 的保存不触发此逻辑,避免无限循环。 - """ - clearcache = False # 标记是否需要清理缓存 - - # 如果保存的是管理日志(LogEntry),直接返回,避免性能问题或循环触发 + clearcache = False if isinstance(instance, LogEntry): return - - # 如果该模型实现了 get_full_url 方法(如文章、页面等) if 'get_full_url' in dir(instance): - # 检查是否仅更新了 'views' 字段(阅读量) is_update_views = update_fields == {'views'} - # 非测试环境且非阅读量更新时,向百度推送 URL 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 spider", 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(':')] # 去除端口号 + 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) + 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() -#wwc + @receiver(user_logged_in) @receiver(user_logged_out) def user_auth_callback(sender, request, user, **kwargs): - """ - 用户登录/登出信号处理器。 - 当用户登录或登出时触发,用于清理与用户状态相关的缓存。 - - 目前主要功能: - - 清理侧边栏缓存(如登录状态、用户信息等) - - 参数: - sender: User 模型 - request: 当前请求对象 - user: 用户对象 - **kwargs: 其他参数 - """ if user and user.username: - logger.info(user) # 记录登录/登出的用户名 - delete_sidebar_cache() # 清理侧边栏缓存,确保显示正确的登录状态 - # cache.clear() # 当前注释,避免过度清理 \ No newline at end of file + logger.info(user) + delete_sidebar_cache() + # cache.clear() diff --git a/src/djangoblog/elasticsearch_backend.py b/src/djangoblog/elasticsearch_backend.py index 3bac2ba..4afe498 100644 --- a/src/djangoblog/elasticsearch_backend.py +++ b/src/djangoblog/elasticsearch_backend.py @@ -5,127 +5,58 @@ from haystack.forms import ModelSearchForm from haystack.models import SearchResult from haystack.utils import log as logging -from ..blog.documents import ArticleDocument, ArticleDocumentManager -from ..blog.models import Article +from blog.documents import ArticleDocument, ArticleDocumentManager +from blog.models import Article -# 获取当前模块的日志记录器,用于记录搜索相关的日志信息 logger = logging.getLogger(__name__) -#wwc + class ElasticSearchBackend(BaseSearchBackend): - """ - 自定义 Elasticsearch 搜索后端,继承自 Haystack 的 BaseSearchBackend。 - 负责与 Elasticsearch 交互,实现索引的创建、更新、删除、查询等操作。 - 使用自定义的 ArticleDocumentManager 来管理文档索引。 - """ -#wwc def __init__(self, connection_alias, **connection_options): - """ - 初始化搜索后端。 + super( + ElasticSearchBackend, + self).__init__( + connection_alias, + **connection_options) + self.manager = ArticleDocumentManager() + self.include_spelling = True - 参数: - connection_alias: 连接别名(Haystack 配置中的别名) - **connection_options: 连接选项(如主机、端口等) - """ - super(ElasticSearchBackend, self).__init__(connection_alias, **connection_options) - self.manager = ArticleDocumentManager() # 实例化文档管理器 - self.include_spelling = True # 启用拼写建议功能 -#wwc def _get_models(self, iterable): - """ - 将模型实例或查询集转换为可索引的文档对象列表。 - - 参数: - iterable: 模型实例列表或查询集,若为空则默认使用所有 Article - - 返回: - 文档对象列表(用于索引) - """ models = iterable if iterable and iterable[0] else Article.objects.all() docs = self.manager.convert_to_doc(models) return docs -#wwc + def _create(self, models): - """ - 创建新的索引并填充数据。 + self.manager.create_index() + docs = self._get_models(models) + self.manager.rebuild(docs) - 参数: - models: 要索引的模型数据(可选) - """ - self.manager.create_index() # 创建索引结构 - docs = self._get_models(models) # 转换为文档 - self.manager.rebuild(docs) # 重建索引数据 -#wwc def _delete(self, models): - """ - 从索引中删除指定的文档。 - - 参数: - models: 要删除的文档对象列表 - - 返回: - bool: 删除是否成功 - """ for m in models: - m.delete() # 调用文档对象的 delete 方法 + m.delete() return True -#wwc - def _rebuild(self, models): - """ - 重建索引。如果 models 为空,则重建所有文章索引。 - 参数: - models: 要重建索引的数据(可选) - """ + def _rebuild(self, models): models = models if models else Article.objects.all() docs = self.manager.convert_to_doc(models) - self.manager.update_docs(docs) # 更新文档到索引 -#wwc + self.manager.update_docs(docs) + def update(self, index, iterable, commit=True): - """ - 更新索引中的文档。 - 参数: - index: 索引对象(未使用) - iterable: 要更新的模型实例或查询集 - commit: 是否立即提交(未使用) - """ models = self._get_models(iterable) - self.manager.update_docs(models) # 将转换后的文档更新到索引 -#wwc - def remove(self, obj_or_string): - """ - 从索引中移除单个对象或字符串。 + self.manager.update_docs(models) - 参数: - obj_or_string: 要移除的对象或标识符 - """ + def remove(self, obj_or_string): models = self._get_models([obj_or_string]) self._delete(models) -#wwc + def clear(self, models=None, commit=True): - """ - 清空整个索引(删除所有文档)。 + self.remove(None) - 参数: - models: 模型列表(未使用) - commit: 是否立即提交(未使用) - """ - self.remove(None) # 调用 remove 方法清空 -#wwc @staticmethod def get_suggestion(query: str) -> str: - """ - 根据用户输入的查询词,获取拼写建议(搜索推荐)。 - 使用 Elasticsearch 的 term suggester 功能。 - - 参数: - query (str): 用户输入的原始搜索词 + """获取推荐词, 如果没有找到添加原搜索词""" - 返回: - str: 推荐的搜索词(多个词用空格连接) - """ - # 执行搜索并获取建议 search = ArticleDocument.search() \ .query("match", body=query) \ .suggest('suggest_search', query, term={'field': 'body'}) \ @@ -134,44 +65,29 @@ class ElasticSearchBackend(BaseSearchBackend): keywords = [] for suggest in search.suggest.suggest_search: if suggest["options"]: - keywords.append(suggest["options"][0]["text"]) # 使用第一个建议 + keywords.append(suggest["options"][0]["text"]) else: - keywords.append(suggest["text"]) # 无建议则使用原词 + keywords.append(suggest["text"]) + return ' '.join(keywords) -#wwc + @log_query def search(self, query_string, **kwargs): - """ - 执行搜索查询。 - - 参数: - query_string: 用户输入的搜索关键词 - **kwargs: 其他搜索参数(如分页偏移量) - - 返回: - dict: 包含搜索结果、命中数、拼写建议等信息的字典 - """ 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 - # 构建布尔查询:在 body 和 title 字段中匹配,至少 70% 的 should 条件匹配 q = Q('bool', should=[Q('match', body=suggestion), Q('match', title=suggestion)], minimum_should_match="70%") - # 构建搜索请求: - # - 查询条件:q - # - 过滤:status='p'(已发布),type='a'(文章) - # - 不返回源数据(source=False) - # - 分页:[start_offset: end_offset] search = ArticleDocument.search() \ .query('bool', filter=[q]) \ .filter('term', status='p') \ @@ -179,14 +95,13 @@ class ElasticSearchBackend(BaseSearchBackend): .source(False)[start_offset: end_offset] results = search.execute() - hits = results['hits'].total # 总命中数 + hits = results['hits'].total raw_results = [] - - # 将 Elasticsearch 返回的结果转换为 Haystack 的 SearchResult 对象 for raw_result in results['hits']['hits']: app_label = 'blog' model_name = 'Article' additional_fields = {} + result_class = SearchResult result = result_class( @@ -196,50 +111,32 @@ class ElasticSearchBackend(BaseSearchBackend): raw_result['_score'], **additional_fields) raw_results.append(result) - - facets = {} # 聚合结果(当前未使用) - # 如果建议词与原词不同,则提供拼写建议 + facets = {} spelling_suggestion = None if query_string == suggestion else suggestion return { - 'results': raw_results, # 搜索结果列表 - 'hits': hits, # 总命中数 - 'facets': facets, # 聚合信息 - 'spelling_suggestion': spelling_suggestion, # 拼写建议 + 'results': raw_results, + 'hits': hits, + 'facets': facets, + 'spelling_suggestion': spelling_suggestion, } -#wwc + class ElasticSearchQuery(BaseSearchQuery): - """ - 自定义搜索查询类,继承自 Haystack 的 BaseSearchQuery。 - 负责构建和处理搜索查询语句。 - """ -#wwc def _convert_datetime(self, date): - """ - 将日期时间对象转换为字符串格式,用于索引查询。 - - 参数: - date: 日期或 datetime 对象 - - 返回: - str: 格式化后的字符串(YYYYMMDDHHMMSS 或 YYYYMMDD000000) - """ if hasattr(date, 'hour'): return force_str(date.strftime('%Y%m%d%H%M%S')) else: return force_str(date.strftime('%Y%m%d000000')) -#wwc + def clean(self, query_fragment): """ - 清理用户输入的查询片段,防止特殊字符引发语法错误。 - 将包含保留字符的词用引号包围。 - - 参数: - query_fragment: 查询片段字符串 + Provides a mechanism for sanitizing user input before presenting the + value to the backend. - 返回: - str: 清理后的查询字符串 + Whoosh 1.X differs here in that you can no longer use a backslash + to escape reserved characters. Instead, the whole word should be + quoted. """ words = query_fragment.split() cleaned_words = [] @@ -256,81 +153,31 @@ class ElasticSearchQuery(BaseSearchQuery): cleaned_words.append(word) return ' '.join(cleaned_words) -#wwc - def build_query_fragment(self, field, filter_type, value): - """ - 构建查询片段。此处直接返回 value.query_string。 - - 参数: - field: 字段名(未使用) - filter_type: 过滤类型(未使用) - value: 查询值对象,包含 query_string 属性 - 返回: - str: 查询字符串 - """ + def build_query_fragment(self, field, filter_type, value): return value.query_string -#wwc - def get_count(self): - """ - 获取搜索结果总数。 - 返回: - int: 结果数量 - """ + def get_count(self): results = self.get_results() return len(results) if results else 0 -#wwc - def get_spelling_suggestion(self, preferred_query=None): - """ - 获取拼写建议。 - - 参数: - preferred_query: 优先使用的查询词(未使用) - 返回: - str: 拼写建议 - """ + def get_spelling_suggestion(self, preferred_query=None): return self._spelling_suggestion -#wwc - def build_params(self, spelling_query=None): - """ - 构建传递给后端的参数字典。 - 参数: - spelling_query: 拼写建议查询词 - - 返回: - dict: 参数字典 - """ + def build_params(self, spelling_query=None): kwargs = super(ElasticSearchQuery, self).build_params(spelling_query=spelling_query) return kwargs -#wwc + class ElasticSearchModelSearchForm(ModelSearchForm): - """ - 自定义搜索表单,继承自 Haystack 的 ModelSearchForm。 - 用于处理用户提交的搜索请求,支持启用/禁用拼写建议。 - """ -#wwc - def search(self): - """ - 重写 search 方法,在搜索前设置是否启用拼写建议。 - 返回: - SearchQuerySet: 搜索结果集 - """ - # 根据表单数据决定是否启用建议搜索 + def search(self): + # 是否建议搜索 self.searchqueryset.query.backend.is_suggest = self.data.get("is_suggest") != "no" sqs = super().search() return sqs -#wwc + class ElasticSearchEngine(BaseEngine): - """ - 自定义搜索引擎,集成 Backend、Query 和 Form。 - 在 Haystack 配置中引用此类来启用自定义搜索功能。 - """ - backend = ElasticSearchBackend # 使用自定义后端 - query = ElasticSearchQuery # 使用自定义查询类 - # form = ElasticSearchModelSearchForm # 原代码未启用,但设计意图是使用自定义表单 \ No newline at end of file + backend = ElasticSearchBackend + query = ElasticSearchQuery diff --git a/src/djangoblog/feeds.py b/src/djangoblog/feeds.py index f7caeb0..8c4e851 100644 --- a/src/djangoblog/feeds.py +++ b/src/djangoblog/feeds.py @@ -3,131 +3,38 @@ 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 +from blog.models import Article +from djangoblog.utils import CommonMarkdown -#wwc -class DjangoBlogFeed(Feed): - """ - RSS 订阅源生成器,继承自 Django 的 Feed 类。 - 用于为网站提供文章的 RSS 订阅功能,允许用户通过 RSS 阅读器获取最新文章。 - """ - # 指定生成的 RSS 版本格式为 RSS 2.0 +class DjangoBlogFeed(Feed): feed_type = Rss201rev2Feed - # 订阅源的描述信息 description = '大巧无工,重剑无锋.' - - # 订阅源的标题 title = "且听风吟 大巧无工,重剑无锋. " - - # 订阅源的相对 URL 路径 link = "/feed/" -#wwc - def author_name(self): - """ - 获取订阅源作者的名称。 - - 从用户模型中获取第一个用户(通常为站长)的昵称作为作者名。 - 返回: - str: 作者昵称(nickname) - """ + def author_name(self): return get_user_model().objects.first().nickname -#wwc - def author_link(self): - """ - 获取订阅源作者的个人主页链接。 - 返回第一个用户的绝对 URL(通常是作者页面或主页)。 - - 返回: - str: 作者主页的 URL - """ + def author_link(self): return get_user_model().objects.first().get_absolute_url() -#wwc - def items(self): - """ - 获取订阅源包含的文章列表。 - - 查询已发布(status='p')且类型为文章(type='a')的文章, - 按发布时间倒序排列,仅返回最新的 5 篇。 - 返回: - QuerySet: 包含最多 5 篇最新文章的查询集 - """ + def items(self): return Article.objects.filter(type='a', status='p').order_by('-pub_time')[:5] -#wwc - def item_title(self, item): - """ - 获取每篇文章在订阅源中的标题。 - 参数: - item (Article): 文章模型实例 - - 返回: - str: 文章标题 - """ + def item_title(self, item): return item.title -#wwc - def item_description(self, item): - """ - 获取每篇文章在订阅源中的描述内容。 - - 将文章正文(body)使用 Markdown 渲染为 HTML 格式后返回, - 以便在 RSS 阅读器中正确显示格式。 - 参数: - item (Article): 文章模型实例 - - 返回: - str: 渲染后的 HTML 内容 - """ + def item_description(self, item): return CommonMarkdown.get_markdown(item.body) -#wwc - def feed_copyright(self): - """ - 获取订阅源的版权信息。 - - 动态生成包含当前年份的版权字符串。 - 返回: - str: 版权声明,如 "Copyright© 2025 且听风吟" - """ + def feed_copyright(self): now = timezone.now() return "Copyright© {year} 且听风吟".format(year=now.year) -#wwc - def item_link(self, item): - """ - 获取每篇文章在订阅源中的链接。 - 使用文章自身的 get_absolute_url 方法生成绝对路径 URL。 - - 参数: - item (Article): 文章模型实例 - - 返回: - str: 文章的绝对 URL - """ + def item_link(self, item): return item.get_absolute_url() -#wwc - def item_guid(self, item): - """ - 获取每篇文章的全局唯一标识符(GUID)。 - GUID 用于 RSS 阅读器识别文章是否为新内容。通常应返回文章的唯一标识, - 例如 ID 或 URL。但此方法当前未完成(缺少返回值),可能导致订阅异常。 - - 参数: - item (Article): 文章模型实例 - - 返回: - str: 文章的唯一标识(当前未实现,需补充如 return item.id 或 return item.get_absolute_url()) - """ - # 注意:此方法当前没有 return 语句,是一个不完整的实现! - # 正确做法应类似: - # return str(item.id) - # 或 - # return item.get_absolute_url() - pass # 占位符,实际运行会返回 None + def item_guid(self, item): + return diff --git a/src/djangoblog/logentryadmin.py b/src/djangoblog/logentryadmin.py index 8da123a..2f6a535 100644 --- a/src/djangoblog/logentryadmin.py +++ b/src/djangoblog/logentryadmin.py @@ -8,115 +8,46 @@ from django.utils.safestring import mark_safe from django.utils.translation import gettext_lazy as _ -# 导入必要的模块: -# - admin: Django 管理后台核心模块 -# - DELETION: 表示删除操作的常量 -# - ContentType: 用于处理模型的通用外键关系 -# - reverse, NoReverseMatch: URL 反向解析相关工具 -# - escape: HTML 转义函数,防止 XSS -# - mark_safe: 标记字符串为安全 HTML(不转义) -# - gettext_lazy: 国际化文本标记 - -#wwc class LogEntryAdmin(admin.ModelAdmin): - """ - 自定义 Django 管理后台中日志条目(LogEntry)的管理界面。 - 用于展示用户在后台执行的操作记录(如添加、修改、删除)。 - 此类增强了默认的 LogEntry 显示功能,支持链接跳转、权限控制等。 - """ - - # 在列表页右侧显示按内容类型(即模型)过滤的侧边栏 list_filter = [ 'content_type' ] - # 支持通过对象表示名(object_repr)和变更消息(change_message)进行搜索 search_fields = [ 'object_repr', 'change_message' ] - # 设置列表页中可点击进入编辑的字段链接 list_display_links = [ - 'action_time', # 操作时间 - 'get_change_message', # 变更消息 + 'action_time', + 'get_change_message', ] - - # 定义列表页显示的字段列 list_display = [ - 'action_time', # 操作发生的时间 - 'user_link', # 执行操作的用户(带链接) - 'content_type', # 操作的对象类型(如 blog.Article) - 'object_link', # 被操作的对象实例(带链接) - 'get_change_message', # 操作详情消息 + 'action_time', + 'user_link', + 'content_type', + 'object_link', + 'get_change_message', ] -#wwc - def has_add_permission(self, request): - """ - 控制是否允许添加新的日志条目。 - 日志由系统自动生成,不允许手动添加。 - - 参数: - request: 当前 HTTP 请求对象 - - 返回: - bool: 始终返回 False,禁止添加权限 - """ + def has_add_permission(self, request): return False -#wwc - def has_change_permission(self, request, obj=None): - """ - 控制是否允许修改日志条目。 - 仅超级用户或拥有 'admin.change_logentry' 权限的用户可以查看(GET 请求), - 但不允许通过 POST 请求修改(即不可编辑保存)。 - - 参数: - request: 当前 HTTP 请求对象 - obj: 当前操作的日志对象(可选) - - 返回: - bool: 是否具有更改权限 - """ + def has_change_permission(self, request, obj=None): return ( request.user.is_superuser or request.user.has_perm('admin.change_logentry') ) and request.method != 'POST' -#wwc - def has_delete_permission(self, request, obj=None): - """ - 控制是否允许删除日志条目。 - 出于审计和安全考虑,禁止删除任何日志记录。 - - 参数: - request: 当前 HTTP 请求对象 - obj: 当前操作的日志对象(可选) - - 返回: - bool: 始终返回 False,禁止删除权限 - """ + def has_delete_permission(self, request, obj=None): return False -#wwc - def object_link(self, obj): - """ - 将被操作对象的名称转换为可点击的超链接(如果可能)。 - - 如果操作不是删除(DELETION),且 content_type 存在,则尝试生成指向该对象 - 在管理后台编辑页面的链接。否则仅显示原始文本。 - 参数: - obj (LogEntry): 日志条目实例 - - 返回: - str: HTML 安全的链接或纯文本 - """ - object_link = escape(obj.object_repr) # 转义原始对象表示,防止 XSS + 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 returning an actual link instead of object repr string try: url = reverse( 'admin:{}_{}_change'.format(content_type.app_label, @@ -125,29 +56,17 @@ class LogEntryAdmin(admin.ModelAdmin): ) object_link = '{}'.format(url, object_link) except NoReverseMatch: - # 如果 URL 反向解析失败(如模型未注册到 admin),则使用原始文本 pass - return mark_safe(object_link) # 标记为安全 HTML 输出 + return mark_safe(object_link) - # 设置此字段在列表页可排序,排序依据为数据库字段 'object_repr' object_link.admin_order_field = 'object_repr' - # 设置此字段在列表页显示的列标题(支持国际化) object_link.short_description = _('object') -#wwc - def user_link(self, obj): - """ - 将操作用户的名称转换为可点击的超链接,指向该用户在管理后台的编辑页面。 - - 参数: - obj (LogEntry): 日志条目实例 - 返回: - str: HTML 安全的用户链接或纯文本 - """ - content_type = ContentType.objects.get_for_model(type(obj.user)) # 获取用户模型的 ContentType - user_link = escape(force_str(obj.user)) # 转义用户名 + def user_link(self, obj): + content_type = ContentType.objects.get_for_model(type(obj.user)) + user_link = escape(force_str(obj.user)) try: - # 生成用户管理页面的 URL + # try returning an actual link instead of object repr string url = reverse( 'admin:{}_{}_change'.format(content_type.app_label, content_type.model), @@ -155,45 +74,18 @@ class LogEntryAdmin(admin.ModelAdmin): ) user_link = '{}'.format(url, user_link) except NoReverseMatch: - # 如果无法生成链接(如 auth.User 未正确注册),则使用原始文本 pass - return mark_safe(user_link) # 标记为安全 HTML 输出 + return mark_safe(user_link) - # 设置此字段在列表页可排序,排序依据为数据库字段 'user' user_link.admin_order_field = 'user' - # 设置此字段在列表页显示的列标题(支持国际化) user_link.short_description = _('user') -#wwc - def get_queryset(self, request): - """ - 自定义查询集,优化数据库查询性能。 - 使用 prefetch_related 提前加载 content_type 外键关联数据, - 避免在列表渲染时产生 N+1 查询问题。 - - 参数: - request: 当前 HTTP 请求对象 - - 返回: - QuerySet: 优化后的 LogEntry 查询集 - """ + def get_queryset(self, request): queryset = super(LogEntryAdmin, self).get_queryset(request) return queryset.prefetch_related('content_type') -#wwc - def get_actions(self, request): - """ - 自定义可用的操作列表(如批量删除)。 - 移除默认的 "删除选中项" 操作,因为已通过 has_delete_permission 禁止删除, - 此处再次确保该操作不会出现在操作下拉菜单中。 - - 参数: - request: 当前 HTTP 请求对象 - - 返回: - dict: 过滤后的可用操作字典 - """ + 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 + return actions diff --git a/src/djangoblog/plugin_manage/base_plugin.py b/src/djangoblog/plugin_manage/base_plugin.py index 5460f0e..df1ce0b 100644 --- a/src/djangoblog/plugin_manage/base_plugin.py +++ b/src/djangoblog/plugin_manage/base_plugin.py @@ -1,23 +1,45 @@ import logging +from pathlib import Path + +from django.template import TemplateDoesNotExist +from django.template.loader import render_to_string logger = logging.getLogger(__name__) -#wwc class BasePlugin: # 插件元数据 PLUGIN_NAME = None PLUGIN_DESCRIPTION = None PLUGIN_VERSION = None + PLUGIN_AUTHOR = None + + # 插件配置 + SUPPORTED_POSITIONS = [] # 支持的显示位置 + DEFAULT_PRIORITY = 100 # 默认优先级(数字越小优先级越高) + POSITION_PRIORITIES = {} # 各位置的优先级 {'sidebar': 50, 'article_bottom': 80} -#wwc 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.plugin_dir = self._get_plugin_directory() + self.plugin_slug = self._get_plugin_slug() + self.init_plugin() self.register_hooks() -#wwc + def _get_plugin_directory(self): + """获取插件目录路径""" + import inspect + plugin_file = inspect.getfile(self.__class__) + return Path(plugin_file).parent + + def _get_plugin_slug(self): + """获取插件标识符(目录名)""" + return self.plugin_dir.name + def init_plugin(self): """ 插件初始化逻辑 @@ -25,7 +47,6 @@ class BasePlugin: """ logger.info(f'{self.PLUGIN_NAME} initialized.') -#wwc def register_hooks(self): """ 注册插件钩子 @@ -33,7 +54,129 @@ class BasePlugin: """ pass -#wwc + # === 位置渲染系统 === + def render_position_widget(self, position, context, **kwargs): + """ + 根据位置渲染插件组件 + + Args: + position: 位置标识 + context: 模板上下文 + **kwargs: 额外参数 + + Returns: + dict: {'html': 'HTML内容', 'priority': 优先级} 或 None + """ + if position not in self.SUPPORTED_POSITIONS: + return None + + # 检查条件显示 + if not self.should_display(position, context, **kwargs): + return None + + # 调用具体的位置渲染方法 + method_name = f'render_{position}_widget' + if hasattr(self, method_name): + html = getattr(self, method_name)(context, **kwargs) + if html: + priority = self.POSITION_PRIORITIES.get(position, self.DEFAULT_PRIORITY) + return { + 'html': html, + 'priority': priority, + 'plugin_name': self.PLUGIN_NAME + } + + return None + + def should_display(self, position, context, **kwargs): + """ + 判断插件是否应该在指定位置显示 + 子类可重写此方法实现条件显示逻辑 + + Args: + position: 位置标识 + context: 模板上下文 + **kwargs: 额外参数 + + Returns: + bool: 是否显示 + """ + return True + + # === 各位置渲染方法 - 子类重写 === + def render_sidebar_widget(self, context, **kwargs): + """渲染侧边栏组件""" + return None + + def render_article_bottom_widget(self, context, **kwargs): + """渲染文章底部组件""" + return None + + def render_article_top_widget(self, context, **kwargs): + """渲染文章顶部组件""" + return None + + def render_header_widget(self, context, **kwargs): + """渲染页头组件""" + return None + + def render_footer_widget(self, context, **kwargs): + """渲染页脚组件""" + return None + + def render_comment_before_widget(self, context, **kwargs): + """渲染评论前组件""" + return None + + def render_comment_after_widget(self, context, **kwargs): + """渲染评论后组件""" + return None + + # === 模板系统 === + def render_template(self, template_name, context=None): + """ + 渲染插件模板 + + Args: + template_name: 模板文件名 + context: 模板上下文 + + Returns: + HTML字符串 + """ + if context is None: + context = {} + + template_path = f"plugins/{self.plugin_slug}/{template_name}" + + try: + return render_to_string(template_path, context) + except TemplateDoesNotExist: + logger.warning(f"Plugin template not found: {template_path}") + return "" + + # === 静态资源系统 === + def get_static_url(self, static_file): + """获取插件静态文件URL""" + from django.templatetags.static import static + return static(f"{self.plugin_slug}/static/{self.plugin_slug}/{static_file}") + + def get_css_files(self): + """获取插件CSS文件列表""" + return [] + + def get_js_files(self): + """获取插件JavaScript文件列表""" + return [] + + def get_head_html(self, context=None): + """获取需要插入到中的HTML内容""" + return "" + + def get_body_html(self, context=None): + """获取需要插入到底部的HTML内容""" + return "" + def get_plugin_info(self): """ 获取插件信息 @@ -42,5 +185,10 @@ class BasePlugin: return { 'name': self.PLUGIN_NAME, 'description': self.PLUGIN_DESCRIPTION, - 'version': self.PLUGIN_VERSION + 'version': self.PLUGIN_VERSION, + 'author': self.PLUGIN_AUTHOR, + 'slug': self.plugin_slug, + 'directory': str(self.plugin_dir), + 'supported_positions': self.SUPPORTED_POSITIONS, + 'priorities': self.POSITION_PRIORITIES } diff --git a/src/djangoblog/plugin_manage/hook_constants.py b/src/djangoblog/plugin_manage/hook_constants.py index 54c679e..8ed4e89 100644 --- a/src/djangoblog/plugin_manage/hook_constants.py +++ b/src/djangoblog/plugin_manage/hook_constants.py @@ -1,22 +1,22 @@ -#wwc -# 定义一个常量,表示“加载文章详情”的动作类型或事件标识。 -# 通常用于状态管理、事件分发或日志记录中,标识当前操作是获取或展示某篇文章的详细内容。 ARTICLE_DETAIL_LOAD = 'article_detail_load' - -# 定义一个常量,表示“创建文章”的动作类型或事件标识。 -# 用于标识用户或系统正在执行新增一篇文章的操作。 ARTICLE_CREATE = 'article_create' - -# 定义一个常量,表示“更新文章”的动作类型或事件标识。 -# 用于标识对已存在文章进行修改或保存更新内容的操作。 ARTICLE_UPDATE = 'article_update' - -# 定义一个常量,表示“删除文章”的动作类型或事件标识。 -# 用于标识将某篇文章从系统中移除的操作。 ARTICLE_DELETE = 'article_delete' -# 定义一个钩子(Hook)的名称,用于在内容输出时触发某些处理逻辑。 -# 在 WordPress 等系统中,"the_content" 是一个典型的过滤器钩子, -# 表示在显示文章正文前可以对内容进行修改或增强(如添加版权信息、格式化等)。 -# 此常量可用于挂载内容处理函数,实现插件或模块化内容扩展。 -ARTICLE_CONTENT_HOOK_NAME = "the_content" \ No newline at end of file +ARTICLE_CONTENT_HOOK_NAME = "the_content" + +# 位置钩子常量 +POSITION_HOOKS = { + 'article_top': 'article_top_widgets', + 'article_bottom': 'article_bottom_widgets', + 'sidebar': 'sidebar_widgets', + 'header': 'header_widgets', + 'footer': 'footer_widgets', + 'comment_before': 'comment_before_widgets', + 'comment_after': 'comment_after_widgets', +} + +# 资源注入钩子 +HEAD_RESOURCES_HOOK = 'head_resources' +BODY_RESOURCES_HOOK = 'body_resources' + diff --git a/src/djangoblog/plugin_manage/hooks.py b/src/djangoblog/plugin_manage/hooks.py index 67667df..d712540 100644 --- a/src/djangoblog/plugin_manage/hooks.py +++ b/src/djangoblog/plugin_manage/hooks.py @@ -4,7 +4,7 @@ logger = logging.getLogger(__name__) _hooks = {} -#wwc + def register(hook_name: str, callback: callable): """ 注册一个钩子回调。 @@ -14,7 +14,7 @@ def register(hook_name: str, callback: callable): _hooks[hook_name].append(callback) logger.debug(f"Registered hook '{hook_name}' with callback '{callback.__name__}'") -#wwc + def run_action(hook_name: str, *args, **kwargs): """ 执行一个 Action Hook。 @@ -28,7 +28,7 @@ def run_action(hook_name: str, *args, **kwargs): except Exception as e: logger.error(f"Error running action hook '{hook_name}' callback '{callback.__name__}': {e}", exc_info=True) -#wwc + def apply_filters(hook_name: str, value, *args, **kwargs): """ 执行一个 Filter Hook。 diff --git a/src/djangoblog/plugin_manage/loader.py b/src/djangoblog/plugin_manage/loader.py index a60c27a..ee750d0 100644 --- a/src/djangoblog/plugin_manage/loader.py +++ b/src/djangoblog/plugin_manage/loader.py @@ -2,46 +2,63 @@ import os import logging from django.conf import settings -# 获取当前模块的日志记录器,用于输出插件加载过程中的日志信息(如成功或失败记录) logger = logging.getLogger(__name__) -#wwc +# 全局插件注册表 +_loaded_plugins = [] + def load_plugins(): """ - 动态加载并初始化插件。 - - 功能说明: - 该函数用于在 Django 项目启动时,自动发现并加载配置在 `settings.ACTIVE_PLUGINS` 中的插件。 - 它会检查每个插件是否存在于指定的插件目录中,并确认其包含入口文件 `plugin.py`。 - 如果条件满足,则导入该插件模块,从而执行其内部的初始化代码(如注册信号、钩子、视图、中间件等)。 - - 调用时机: - 通常在 Django 的 `AppConfig.ready()` 方法中调用此函数,以确保 Django 的应用注册系统已完全就绪, - 避免因过早导入而导致的依赖问题或模型未加载异常。 - - 执行逻辑: - 1. 遍历 settings 中定义的活跃插件列表(ACTIVE_PLUGINS)。 - 2. 对每个插件,构造其在文件系统中的路径。 - 3. 判断该路径是否为一个目录,且包含名为 'plugin.py' 的文件(插件入口)。 - 4. 若满足条件,则使用 __import__ 动态导入该模块。 - 5. 成功导入后记录一条 info 级别的日志;若导入失败,则捕获 ImportError 异常并记录错误日志。 - - 注意事项: - - 此函数不返回任何值,也不对插件进行实例化,仅通过导入模块来触发其副作用(如注册行为)。 - - 插件必须遵循目录结构规范:`plugins/<插件名>/plugin.py`。 - - 错误日志中包含异常堆栈(exc_info=True),便于调试和排查插件加载失败原因。 + Dynamically loads and initializes plugins from the 'plugins' directory. + This function is intended to be called when the Django app registry is ready. """ - # 遍历 settings 中配置的已激活插件名称列表 + global _loaded_plugins + _loaded_plugins = [] + for plugin_name in settings.ACTIVE_PLUGINS: - # 构建当前插件的完整文件路径 plugin_path = os.path.join(settings.PLUGINS_DIR, plugin_name) - # 检查该插件路径是否为有效目录,且包含 plugin.py 入口文件 if os.path.isdir(plugin_path) and os.path.exists(os.path.join(plugin_path, 'plugin.py')): try: - # 动态导入插件模块,触发其初始化逻辑 - __import__(f'plugins.{plugin_name}.plugin') - # 记录插件加载成功的日志 - logger.info(f"Successfully loaded plugin: {plugin_name}") + # 导入插件模块 + plugin_module = __import__(f'plugins.{plugin_name}.plugin', fromlist=['plugin']) + + # 获取插件实例 + if hasattr(plugin_module, 'plugin'): + plugin_instance = plugin_module.plugin + _loaded_plugins.append(plugin_instance) + logger.info(f"Successfully loaded plugin: {plugin_name} - {plugin_instance.PLUGIN_NAME}") + else: + logger.warning(f"Plugin {plugin_name} does not have 'plugin' instance") + except ImportError as e: - # 记录插件加载失败的错误日志,并包含完整的异常堆栈信息 - logger.error(f"Failed to import plugin: {plugin_name}", exc_info=e) \ No newline at end of file + logger.error(f"Failed to import plugin: {plugin_name}", exc_info=e) + except AttributeError as e: + logger.error(f"Failed to get plugin instance: {plugin_name}", exc_info=e) + except Exception as e: + logger.error(f"Unexpected error loading plugin: {plugin_name}", exc_info=e) + +def get_loaded_plugins(): + """获取所有已加载的插件""" + return _loaded_plugins + +def get_plugin_by_name(plugin_name): + """根据名称获取插件""" + for plugin in _loaded_plugins: + if plugin.plugin_slug == plugin_name: + return plugin + return None + +def get_plugin_by_slug(plugin_slug): + """根据slug获取插件""" + for plugin in _loaded_plugins: + if plugin.plugin_slug == plugin_slug: + return plugin + return None + +def get_plugins_info(): + """获取所有插件的信息""" + return [plugin.get_plugin_info() for plugin in _loaded_plugins] + +def get_plugins_by_position(position): + """获取支持指定位置的插件""" + return [plugin for plugin in _loaded_plugins if position in plugin.SUPPORTED_POSITIONS] \ No newline at end of file diff --git a/src/djangoblog/settings.py b/src/djangoblog/settings.py index 0586fc8..ca6c235 100644 --- a/src/djangoblog/settings.py +++ b/src/djangoblog/settings.py @@ -109,10 +109,10 @@ WSGI_APPLICATION = 'djangoblog.wsgi.application' DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', - 'NAME': os.environ.get('DJANGO_MYSQL_DATABASE') or 'djangoblog', + 'NAME': os.environ.get('DJANGO_MYSQL_DATABASE') or 'djangoblog3', 'USER': os.environ.get('DJANGO_MYSQL_USER') or 'root', - 'PASSWORD': os.environ.get('DJANGO_MYSQL_PASSWORD') or 'wwc147', - 'HOST': os.environ.get('DJANGO_MYSQL_HOST') or '127.0.0.1', + 'PASSWORD': os.environ.get('DJANGO_MYSQL_PASSWORD') or 'root', + 'HOST': os.environ.get('DJANGO_MYSQL_HOST') or 'localhost', 'PORT': int( os.environ.get('DJANGO_MYSQL_PORT') or 3306), 'OPTIONS': { @@ -177,6 +177,11 @@ STATIC_ROOT = os.path.join(BASE_DIR, 'collectedstatic') STATIC_URL = '/static/' STATICFILES = os.path.join(BASE_DIR, 'static') +# 添加插件静态文件目录 +STATICFILES_DIRS = [ + os.path.join(BASE_DIR, 'plugins'), # 让Django能找到插件的静态文件 +] + AUTH_USER_MODEL = 'accounts.BlogUser' LOGIN_URL = '/login/' @@ -285,11 +290,6 @@ LOGGING = { 'handlers': ['log_file', 'console'], 'level': 'INFO', 'propagate': True, - }, - 'django.request': { - 'handlers': ['mail_admins'], - 'level': 'ERROR', - 'propagate': False, } } } @@ -301,23 +301,63 @@ STATICFILES_FINDERS = ( 'compressor.finders.CompressorFinder', ) COMPRESS_ENABLED = True -# COMPRESS_OFFLINE = True +# 根据环境变量决定是否启用离线压缩 +COMPRESS_OFFLINE = os.environ.get('COMPRESS_OFFLINE', 'False').lower() == 'true' + +# 压缩输出目录 +COMPRESS_OUTPUT_DIR = 'compressed' +# 压缩文件名模板 - 包含哈希值用于缓存破坏 +COMPRESS_CSS_HASHING_METHOD = 'mtime' +COMPRESS_JS_HASHING_METHOD = 'mtime' +# 高级CSS压缩过滤器 COMPRESS_CSS_FILTERS = [ - # creates absolute urls from relative ones + # 创建绝对URL 'compressor.filters.css_default.CssAbsoluteFilter', - # css minimizer - 'compressor.filters.cssmin.CSSMinFilter' + # CSS压缩器 - 高压缩等级 + 'compressor.filters.cssmin.CSSCompressorFilter', ] + +# 高级JS压缩过滤器 COMPRESS_JS_FILTERS = [ - 'compressor.filters.jsmin.JSMinFilter' + # JS压缩器 - 高压缩等级 + 'compressor.filters.jsmin.SlimItFilter', ] +# 压缩缓存配置 +COMPRESS_CACHE_BACKEND = 'default' +COMPRESS_CACHE_KEY_FUNCTION = 'compressor.cache.simple_cachekey' + +# 预压缩配置 +COMPRESS_PRECOMPILERS = ( + # 支持SCSS/SASS + ('text/x-scss', 'django_libsass.SassCompiler'), + ('text/x-sass', 'django_libsass.SassCompiler'), +) + +# 压缩性能优化 +COMPRESS_MINT_DELAY = 30 # 压缩延迟(秒) +COMPRESS_MTIME_DELAY = 10 # 修改时间检查延迟 +COMPRESS_REBUILD_TIMEOUT = 2592000 # 重建超时(30天) + +# 压缩等级配置 +COMPRESS_CSS_COMPRESSOR = 'compressor.css.CssCompressor' +COMPRESS_JS_COMPRESSOR = 'compressor.js.JsCompressor' + +# 静态文件缓存配置 +STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage' + +# 浏览器缓存配置(通过中间件或服务器配置) +COMPRESS_URL = STATIC_URL +COMPRESS_ROOT = STATIC_ROOT + MEDIA_ROOT = os.path.join(BASE_DIR, 'uploads') MEDIA_URL = '/media/' X_FRAME_OPTIONS = 'SAMEORIGIN' + + DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' if os.environ.get('DJANGO_ELASTICSEARCH_HOST'): @@ -339,5 +379,8 @@ ACTIVE_PLUGINS = [ 'reading_time', 'external_links', 'view_count', - 'seo_optimizer' -] \ No newline at end of file + 'seo_optimizer', + 'image_lazy_loading', + 'article_recommendation', +] + diff --git a/src/djangoblog/sitemap.py b/src/djangoblog/sitemap.py index ce548ad..8b7d446 100644 --- a/src/djangoblog/sitemap.py +++ b/src/djangoblog/sitemap.py @@ -1,181 +1,59 @@ from django.contrib.sitemaps import Sitemap from django.urls import reverse -from ..blog.models import Article, Category, Tag +from blog.models import Article, Category, Tag -#wwc -class StaticViewSitemap(Sitemap): - """ - 静态页面的站点地图(Sitemap)类。 - 用于为网站的固定页面(如首页)生成 sitemap 条目。 - """ - # 默认优先级:0.5(表示普通重要性) +class StaticViewSitemap(Sitemap): priority = 0.5 - # 默认更新频率:每天 changefreq = 'daily' -#wwc - def items(self): - """ - 定义需要包含在 sitemap 中的静态视图名称列表。 - 返回: - list: 包含 Django URL 命名的列表,此处仅包含首页 'blog:index' - """ + def items(self): return ['blog:index', ] -#wwc - def location(self, item): - """ - 根据 items() 返回的 URL 名称,生成对应的绝对路径。 - 参数: - item (str): URL 命名,如 'blog:index' - - 返回: - str: 反向解析后的 URL 路径(如 '/') - """ + def location(self, item): return reverse(item) -#wwc -class ArticleSiteMap(Sitemap): - """ - 文章(Article)模型的站点地图类。 - 为所有已发布的文章生成 sitemap 条目。 - """ - # 更新频率:每月 +class ArticleSiteMap(Sitemap): changefreq = "monthly" - # 优先级:0.6(相对重要) priority = "0.6" -#wwc - def items(self): - """ - 获取所有已发布(status='p')的文章对象。 - 返回: - QuerySet: 所有已发布文章的查询集 - """ + def items(self): return Article.objects.filter(status='p') -#wwc - def lastmod(self, obj): - """ - 获取每篇文章的最后修改时间,用于搜索引擎判断内容更新。 - - 参数: - obj (Article): 文章实例 - 返回: - datetime: 文章的 last_modify_time 字段值 - """ + def lastmod(self, obj): return obj.last_modify_time -#wwc -class CategorySiteMap(Sitemap): - """ - 分类(Category)模型的站点地图类。 - 为所有文章分类生成 sitemap 条目。 - """ - # 更新频率:每周(注意:此处拼写应为 'weekly' 小写) +class CategorySiteMap(Sitemap): changefreq = "Weekly" - # 优先级:0.6 priority = "0.6" -#wwc - def items(self): - """ - 获取所有分类对象。 - 返回: - QuerySet: 所有分类的查询集 - """ + def items(self): return Category.objects.all() -#wwc - def lastmod(self, obj): - """ - 获取每个分类的最后修改时间。 - 参数: - obj (Category): 分类实例 - - 返回: - datetime: 分类的 last_modify_time 字段值 - """ + def lastmod(self, obj): return obj.last_modify_time -#wwc -class TagSiteMap(Sitemap): - """ - 标签(Tag)模型的站点地图类。 - 为所有标签生成 sitemap 条目。 - """ - # 更新频率:每周 +class TagSiteMap(Sitemap): changefreq = "Weekly" - # 优先级:0.3(相对较低) priority = "0.3" -#wwc - def items(self): - """ - 获取所有标签对象。 - 返回: - QuerySet: 所有标签的查询集 - """ + def items(self): return Tag.objects.all() -#wwc - def lastmod(self, obj): - """ - 获取每个标签的最后修改时间。 - - 参数: - obj (Tag): 标签实例 - 返回: - datetime: 标签的 last_modify_time 字段值 - """ + def lastmod(self, obj): return obj.last_modify_time -#wwc -class UserSiteMap(Sitemap): - """ - 用户(作者)的站点地图类。 - 为所有发布过文章的作者生成 sitemap 条目。 - """ - # 更新频率:每周 +class UserSiteMap(Sitemap): changefreq = "Weekly" - # 优先级:0.3 priority = "0.3" -#wwc - def items(self): - """ - 获取所有发布过文章的作者(去重)。 - - 实现方式: - 1. 查询所有文章 - 2. 提取每篇文章的作者(author 字段) - 3. 使用 set 去重 - 4. 转换为列表返回 - 注意:此方法效率较低,尤其在文章量大时,建议优化为: - return User.objects.filter(article__status='p').distinct() - - 返回: - list: 去重后的用户(作者)对象列表 - """ + def items(self): return list(set(map(lambda x: x.author, Article.objects.all()))) -#wwc - def lastmod(self, obj): - """ - 获取每个用户的最后修改时间。 - - 此处使用的是用户的注册时间(date_joined),而非内容更新时间。 - 对于作者而言,更合理的 lastmod 可能是其发布的最后一篇文章的时间。 - 参数: - obj (User): 用户实例 - - 返回: - datetime: 用户的 date_joined 字段值(注册时间) - """ - return obj.date_joined \ No newline at end of file + def lastmod(self, obj): + return obj.date_joined diff --git a/src/djangoblog/spider_notify.py b/src/djangoblog/spider_notify.py index 948323b..7b909e9 100644 --- a/src/djangoblog/spider_notify.py +++ b/src/djangoblog/spider_notify.py @@ -1,58 +1,21 @@ import logging + import requests from django.conf import settings -# 获取当前模块的日志记录器,用于输出日志信息 logger = logging.getLogger(__name__) -#wwc + class SpiderNotify(): - """ - 蜘蛛推送通知工具类,用于向搜索引擎(如百度)主动推送网站新内容的URL, - 帮助搜索引擎更快发现和收录网页。 - """ -#wwc @staticmethod def baidu_notify(urls): - """ - 向百度搜索引擎推送一组URL链接(主动抓取通知)。 - - 使用百度提供的链接提交API,将新发布的页面URL推送给百度爬虫, - 以加快收录速度。 - - 参数: - urls (list 或 str): 要推送的URL列表,例如 ['https://example.com/article/1', 'https://example.com/article/2'] - 注意:函数期望接收一个列表,但代码中直接使用了 '\n'.join,因此传入必须为可迭代的URL列表。 - - 实现逻辑: - 1. 将 URL 列表用换行符连接成字符串 - 2. 向 settings.BAIDU_NOTIFY_URL 指定的百度推送接口发送 POST 请求 - 3. 记录百度返回的响应结果到日志 - 4. 捕获并记录任何异常 - - 注意: - - 需在 Django 的 settings 中配置 BAIDU_NOTIFY_URL(百度提供的推送接口地址) - - 百度推送接口要求每次最多提交2000条URL - """ try: - data = '\n'.join(urls) # 将多个 URL 用换行符拼接,符合百度推送接口格式要求 - result = requests.post(settings.BAIDU_NOTIFY_URL, data=data) # 发送 POST 请求到百度推送接口 - logger.info(result.text) # 记录百度服务器返回的响应内容(如成功/失败信息) + data = '\n'.join(urls) + result = requests.post(settings.BAIDU_NOTIFY_URL, data=data) + logger.info(result.text) except Exception as e: - logger.error(e) # 记录请求过程中发生的任何异常(如网络错误、超时等) -#wwc + logger.error(e) + @staticmethod def notify(url): - """ - 通用通知接口,当前默认调用百度推送。 - - 参数: - url (list): 要推送的URL列表(注意:参数名是单数 'url',但实际应传入列表) - - 注意: - 此方法存在命名误导: - - 方法名是 notify,参数是 url(单数),但内部调用 baidu_notify,而 baidu_notify 期望接收一个列表 - - 因此实际使用时仍需传入列表,例如 notify(['https://example.com/post']) - - 更合理的命名应为 notify_urls 或参数名为 urls - """ - SpiderNotify.baidu_notify(url) # 调用百度推送方法,传入URL列表 \ No newline at end of file + SpiderNotify.baidu_notify(url) diff --git a/src/djangoblog/tests.py b/src/djangoblog/tests.py index c031fa0..01237d9 100644 --- a/src/djangoblog/tests.py +++ b/src/djangoblog/tests.py @@ -1,44 +1,15 @@ from django.test import TestCase -# 从项目工具模块导入所有工具函数和类,用于测试 -from ..djangoblog.utils import * +from djangoblog.utils import * -#wwc -class DjangoBlogTest(TestCase): - """ - DjangoBlog 应用的测试用例基类(目前仅包含工具函数测试)。 - 继承自 Django 的 TestCase,提供数据库支持、断言方法和测试生命周期管理。 - """ +class DjangoBlogTest(TestCase): def setUp(self): - """ - 测试前置准备方法,在每个测试方法执行前自动调用。 - - 当前为空实现,表示该测试类无需在每次测试前进行初始化操作。 - 若后续需要创建测试数据(如用户、文章等),可在此方法中完成。 - """ pass -#wwc - def test_utils(self): - """ - 测试工具模块(djangoblog.utils)中的核心功能: - 1. SHA256 哈希生成 - 2. Markdown 内容解析与渲染 - - 此方法验证工具函数是否能正确处理输入并返回预期结果。 - """ - - # 测试 get_sha256 函数:计算字符串 'test' 的 SHA256 哈希值 - md5 = get_sha256('test') # 注意:函数名为 get_sha256,但变量名误写为 md5(应为 sha256) - # 断言:验证哈希值不为 None,确保函数成功返回了有效结果 + def test_utils(self): + md5 = get_sha256('test') self.assertIsNotNone(md5) - - # 测试 CommonMarkdown.get_markdown 静态方法:将 Markdown 字符串转换为 HTML - # 输入包含多种 Markdown 元素的文本: - # - 一级标题 (# Title1) - # - Python 代码块 (```python ... ```) - # - 两个超链接 ([text](url)) c = CommonMarkdown.get_markdown(''' # Title1 @@ -49,7 +20,13 @@ class DjangoBlogTest(TestCase): [url](https://www.lylinux.net/) [ddd](http://www.baidu.com) - ''') - # 注意:此测试方法未对返回的 HTML 内容进行断言验证(如 self.assertIn('

', c)), - # 仅执行了转换操作,未检查输出是否符合预期。建议补充相关断言以增强测试完整性。 \ No newline at end of file + + ''') + self.assertIsNotNone(c) + d = { + 'd': 'key1', + 'd2': 'key2' + } + data = parse_dict_to_url(d) + self.assertIsNotNone(data) diff --git a/src/djangoblog/urls.py b/src/djangoblog/urls.py index 0b4ae3c..371b8a4 100644 --- a/src/djangoblog/urls.py +++ b/src/djangoblog/urls.py @@ -1,97 +1,80 @@ """djangoblog URL Configuration -主 URL 配置文件,定义了整个网站的 URL 路由规则。 -将不同的 URL 模式映射到对应的视图函数或类,实现请求分发。 +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/1.10/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.conf.urls import url, include + 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) """ - -# 导入必要的模块 from django.conf import settings -from django.conf.urls.i18n import i18n_patterns # 支持国际化 URL 模式 -from django.conf.urls.static import static # 用于在开发环境提供静态和媒体文件服务 -from django.contrib.sitemaps.views import sitemap # 生成站点地图(sitemap.xml)的视图 -from django.urls import path, include # 标准 URL 映射工具 -from django.urls import re_path # 支持正则表达式匹配的 URL 映射 -from haystack.views import search_view_factory # Haystack 搜索框架的视图工厂 - -# 导入项目内自定义组件 -from ..blog.views import EsSearchView # 自定义的 Elasticsearch 搜索视图 -from ..djangoblog.admin_site import admin_site # 自定义的 Django 管理后台实例 -from ..djangoblog.elasticsearch_backend import ElasticSearchModelSearchForm # 搜索表单类 -from ..djangoblog.feeds import DjangoBlogFeed # RSS 订阅源 -from ..djangoblog.sitemap import ArticleSiteMap, CategorySiteMap, StaticViewSitemap, TagSiteMap, UserSiteMap # 站点地图类 +from django.conf.urls.i18n import i18n_patterns +from django.conf.urls.static import static +from django.contrib.sitemaps.views import sitemap +from django.urls import path, include +from django.urls import re_path +from haystack.views import search_view_factory +from django.http import JsonResponse +import time + +from blog.views import EsSearchView +from djangoblog.admin_site import admin_site +from djangoblog.elasticsearch_backend import ElasticSearchModelSearchForm +from djangoblog.feeds import DjangoBlogFeed +from djangoblog.sitemap import ArticleSiteMap, CategorySiteMap, StaticViewSitemap, TagSiteMap, UserSiteMap -#wwc -# 定义站点地图(sitemap)的映射字典 -# 将不同的 sitemap 类按类别注册,用于生成 sitemap.xml sitemaps = { - 'blog': ArticleSiteMap, # 文章内容地图 - 'Category': CategorySiteMap, # 分类地图 - 'Tag': TagSiteMap, # 标签地图 - 'User': UserSiteMap, # 用户(作者)地图 - 'static': StaticViewSitemap # 静态页面地图(如首页) + + 'blog': ArticleSiteMap, + 'Category': CategorySiteMap, + 'Tag': TagSiteMap, + 'User': UserSiteMap, + 'static': StaticViewSitemap } -# 自定义错误页面处理视图 -# 当发生相应错误时,Django 会调用这些视图函数来渲染错误页面 -handler404 = 'blog.views.page_not_found_view' # 404 页面未找到 -handler500 = 'blog.views.server_error_view' # 500 服务器内部错误 -handle403 = 'blog.views.permission_denied_view' # 403 权限拒绝(注意:变量名应为 handler403 才生效) +handler404 = 'blog.views.page_not_found_view' +handler500 = 'blog.views.server_error_view' +handle403 = 'blog.views.permission_denied_view' + + +def health_check(request): + """ + 健康检查接口 + 简单返回服务健康状态 + """ + return JsonResponse({ + 'status': 'healthy', + 'timestamp': time.time() + }) -# 主 URL 模式列表 urlpatterns = [ - # 提供 Django 内置的国际化(i18n)URL 支持,如 /i18n/setlang/ 用于切换语言 path('i18n/', include('django.conf.urls.i18n')), + path('health/', health_check, name='health_check'), + path('api/plugins/ai/', include('plugins.ai_assistant.urls')), + path('api/plugins/image/', include('plugins.ai_image.urls')), ] - -# 使用 i18n_patterns 包裹一组 URL,使其支持多语言前缀(如 /en/, /zh-hans/) -# prefix_default_language=False 表示默认语言不添加前缀 urlpatterns += i18n_patterns( - # 管理后台 URL,访问路径为 /admin/ re_path(r'^admin/', admin_site.urls), - - # 博客应用主 URL,包含 blog 应用的所有路由,命名空间为 'blog' - # 注意:空正则 r'' 会匹配根路径,但由于在 i18n_patterns 内,实际路径为 // 或 /(默认语言) re_path(r'', include('blog.urls', namespace='blog')), - - # Markdown 编辑器(mdeditor)的资源和上传路由 re_path(r'mdeditor/', include('mdeditor.urls')), - - # 评论系统路由,命名空间为 'comment' re_path(r'', include('comments.urls', namespace='comment')), - - # 用户账户系统路由(注册、登录、个人中心等),命名空间为 'account' re_path(r'', include('accounts.urls', namespace='account')), - - # 第三方登录(OAuth)路由,命名空间为 'oauth' re_path(r'', include('oauth.urls', namespace='oauth')), - - # 站点地图路由:访问 /sitemap.xml 时,调用 sitemap 视图并传入 sitemaps 字典 re_path(r'^sitemap\.xml$', sitemap, {'sitemaps': sitemaps}, name='django.contrib.sitemaps.views.sitemap'), - - # RSS 订阅源路由:访问 /feed/ 或 /rss/ 时,返回 DjangoBlogFeed 生成的 RSS 内容 re_path(r'^feed/$', DjangoBlogFeed()), re_path(r'^rss/$', DjangoBlogFeed()), - - # 搜索功能路由:使用 Haystack 的视图工厂创建基于 Elasticsearch 的搜索视图 - # 搜索表单使用自定义的 ElasticSearchModelSearchForm - # 访问路径如 /search?q=关键词 re_path('^search', search_view_factory(view_class=EsSearchView, form_class=ElasticSearchModelSearchForm), name='search'), - - # 服务器管理模块路由(可能用于系统监控或运维),命名空间为 'servermanager' re_path(r'', include('servermanager.urls', namespace='servermanager')), - - # OwnTracks 路由(可能用于位置追踪服务),命名空间为 'owntracks' re_path(r'', include('owntracks.urls', namespace='owntracks')) - , prefix_default_language=False) # i18n_patterns 的结束和参数 - -# 为静态文件 URL 添加处理规则 -# 在开发模式下(DEBUG=True),Django 自动提供 STATIC_URL 和 MEDIA_URL 的文件服务 -# static() 会将 URL 前缀映射到指定的文件系统路径 -urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) - -# 如果处于调试模式,额外添加媒体文件(如用户上传的图片)的 URL 映射 + , prefix_default_language=False) + 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 + document_root=settings.MEDIA_ROOT) diff --git a/src/djangoblog/utils.py b/src/djangoblog/utils.py index ad0a37c..91d2b91 100644 --- a/src/djangoblog/utils.py +++ b/src/djangoblog/utils.py @@ -1,13 +1,7 @@ #!/usr/bin/env python # encoding: utf-8 -""" -djangoblog 工具函数模块(utils.py) -提供博客系统所需的通用工具函数,包括缓存、安全、Markdown解析、邮件发送、文件处理等功能。 -""" - -# 导入标准库和第三方库 import logging import os import random @@ -15,62 +9,29 @@ import string import uuid from hashlib import sha256 -import bleach # 用于清理 HTML 标签,防止 XSS 攻击 -import markdown # 用于将 Markdown 文本转换为 HTML -import requests # 用于发起 HTTP 请求 +import bleach +import markdown +import requests from django.conf import settings -from django.contrib.sites.models import Site # Django 的站点框架 -from django.core.cache import cache # 使用 Django 缓存后端 -from django.templatetags.static import static # 用于生成静态文件 URL +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。 - - 返回: - tuple: (最新文章的主键, 最新评论的主键) - """ - from ..blog.models import Article - from ..comments.models import Comment + from blog.models import Article + from comments.models import Comment return (Article.objects.latest().pk, Comment.objects.latest().pk) -#wwc -def get_sha256(str): - """ - 计算输入字符串的 SHA256 哈希值。 - 参数: - str (str): 要哈希的字符串 - - 返回: - str: SHA256 哈希值的十六进制字符串表示 - """ +def get_sha256(str): m = sha256(str.encode('utf-8')) return m.hexdigest() -#wwc -def cache_decorator(expiration=3 * 60): - """ - 缓存装饰器,用于为函数添加缓存功能,避免重复计算或数据库查询。 - 参数: - expiration (int): 缓存过期时间(秒),默认 3 分钟 - - 返回: - decorator: 可用于装饰函数的装饰器 - - 工作原理: - 1. 尝试从视图对象获取缓存键(通过 get_cache_key 方法) - 2. 如果没有,则基于函数名和参数生成唯一哈希作为键 - 3. 查询缓存,命中则返回缓存值 - 4. 未命中则执行原函数,将结果存入缓存并返回 - 5. 特殊处理返回值为 None 的情况,避免缓存穿透 - """ -#wwc +def cache_decorator(expiration=3 * 60): def wrapper(func): def news(*args, **kwargs): try: @@ -80,10 +41,12 @@ def cache_decorator(expiration=3 * 60): 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: @@ -103,25 +66,16 @@ def cache_decorator(expiration=3 * 60): return wrapper -#wwc -def expire_view_cache(path, servername, serverport, key_prefix=None): - """ - 手动清除特定视图的缓存(用于内容更新后刷新缓存)。 - - 参数: - path (str): URL 路径(如 '/article/1/') - servername (str): 服务器域名 - serverport (str): 服务器端口 - key_prefix (str, optional): 缓存键前缀 - 返回: - bool: 缓存是否成功删除 - - 实现: - 1. 构造一个模拟的 HttpRequest 对象 - 2. 使用 Django 的 get_cache_key 工具生成与视图缓存对应的键 - 3. 如果键存在且缓存中有值,则删除该缓存 - """ +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 @@ -137,139 +91,66 @@ def expire_view_cache(path, servername, serverport, key_prefix=None): return True return False -#wwc + @cache_decorator() def get_current_site(): - """ - 获取当前 Django 站点对象,并使用缓存优化性能。 - - 返回: - Site: 当前站点实例(包含域名、名称等信息) - """ site = Site.objects.get_current() return site -#wwc + class CommonMarkdown: - """ - 提供统一的 Markdown 解析功能,支持代码高亮、目录生成等。 - """ -#wwc @staticmethod def _convert_markdown(value): - """ - 内部方法:将 Markdown 字符串转换为 HTML,并提取目录(TOC)。 - - 参数: - value (str): Markdown 格式的文本 - - 返回: - tuple: (HTML 内容字符串, 目录 HTML 字符串) - """ md = markdown.Markdown( extensions=[ - 'extra', # 标准扩展(表格、脚注等) - 'codehilite', # 代码高亮 - 'toc', # 自动生成目录 - 'tables', # 表格支持 + 'extra', + 'codehilite', + 'toc', + 'tables', ] ) body = md.convert(value) toc = md.toc return body, toc -#wwc + @staticmethod def get_markdown_with_toc(value): - """ - 解析 Markdown 文本,同时返回 HTML 内容和目录。 - - 参数: - value (str): Markdown 文本 - - 返回: - tuple: (HTML 内容, TOC 目录) - """ body, toc = CommonMarkdown._convert_markdown(value) return body, toc -#wwc + @staticmethod def get_markdown(value): - """ - 仅解析 Markdown 文本为 HTML 内容,不返回目录。 - - 参数: - value (str): Markdown 文本 - - 返回: - str: 转换后的 HTML 字符串 - """ body, toc = CommonMarkdown._convert_markdown(value) return body -#wwc -def send_email(emailto, title, content): - """ - 发送邮件的快捷方法,通过 Django 信号机制解耦。 - 参数: - emailto (str): 收件人邮箱 - title (str): 邮件标题 - content (str): 邮件正文 - - 实现: - 触发自定义的 send_email_signal 信号,由信号处理器完成实际的邮件发送逻辑。 - """ - from ..djangoblog.blog_signals import send_email_signal +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) -#wwc -def generate_code() -> str: - """ - 生成一个 6 位的随机数字验证码。 - 返回: - str: 6 位数字组成的字符串(如 '123456') - """ +def generate_code() -> str: + """生成随机数验证码""" return ''.join(random.sample(string.digits, 6)) -#wwc -def parse_dict_to_url(dict): - """ - 将字典转换为 URL 查询参数字符串(键值对用 & 连接)。 - - 参数: - dict (dict): 要转换的字典 - - 返回: - str: URL 编码后的查询字符串(如 'key1=value1&key2=value2') - 注意: - 使用 urllib.parse.quote 对键和值进行 URL 编码,safe='/' 表示斜杠不编码。 - """ +def parse_dict_to_url(dict): from urllib.parse import quote url = '&'.join(['{}={}'.format(quote(k, safe='/'), quote(v, safe='/')) for k, v in dict.items()]) return url -#wwc -def get_blog_setting(): - """ - 获取博客系统设置,优先从缓存读取,未命中则从数据库获取并缓存。 - - 如果数据库中没有设置记录,则创建一个默认设置并保存。 - 返回: - BlogSettings: 博客设置模型实例 - """ +def get_blog_setting(): value = cache.get('get_blog_setting') if value: return value else: - from ..blog.models import BlogSettings + from blog.models import BlogSettings if not BlogSettings.objects.count(): setting = BlogSettings() setting.site_name = 'djangoblog' @@ -291,32 +172,22 @@ def get_blog_setting(): cache.set('get_blog_setting', value) return value -#wwc -def save_user_avatar(url): - """ - 从指定 URL 下载用户头像并保存到本地静态文件目录。 - - 参数: - url (str): 头像图片的远程 URL - 返回: - str: 保存后的本地静态文件 URL(如 '/static/avatar/abc123.jpg') - 下载失败时返回默认头像路径。 - - 流程: - 1. 创建本地头像存储目录 - 2. 下载图片内容 - 3. 根据原始 URL 判断文件类型,生成唯一文件名 - 4. 保存文件 - 5. 返回静态 URL - """ +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' @@ -329,50 +200,22 @@ def save_user_avatar(url): logger.error(e) return static('blog/img/avatar.png') -#wwc -def delete_sidebar_cache(): - """ - 清除侧边栏所有缓存。 - - 侧边栏内容(如最新文章、热门评论)通常会被缓存以提高性能。 - 当内容更新时,需调用此函数清除相关缓存。 - 实现: - 遍历 LinkShowType 的所有值,删除以 'sidebar' 为前缀的缓存键。 - """ - from ..blog.models import LinkShowType +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) -#wwc -def delete_view_cache(prefix, keys): - """ - 删除基于模板片段缓存(@cache)的缓存。 - - 参数: - prefix (str): 缓存片段的名称(与模板中 cache 标签的第一个参数对应) - keys (list): 缓存的变量列表(用于生成唯一键) - 实现: - 使用 make_template_fragment_key 生成正确的缓存键,然后删除。 - """ +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) -#wwc -def get_resource_url(): - """ - 获取静态资源的基础 URL。 - - 如果设置了 STATIC_URL,则直接返回。 - 否则,构建一个完整的 URL(http://domain/static/)。 - 返回: - str: 静态资源基础 URL - """ +def get_resource_url(): if settings.STATIC_URL: return settings.STATIC_URL else: @@ -380,21 +223,50 @@ def get_resource_url(): return 'http://' + site.domain + '/static/' -# 定义允许在用户内容中使用的 HTML 标签和属性 -# 用于 sanitize_html 函数,防止 XSS 攻击 ALLOWED_TAGS = ['a', 'abbr', 'acronym', 'b', 'blockquote', 'code', 'em', 'i', 'li', 'ol', 'pre', 'strong', 'ul', 'h1', - 'h2', 'p'] -ALLOWED_ATTRIBUTES = {'a': ['href', 'title'], 'abbr': ['title'], 'acronym': ['title']} + 'h2', 'p', 'span', 'div'] + +# 安全的class值白名单 - 只允许代码高亮相关的class +ALLOWED_CLASSES = [ + 'codehilite', 'highlight', 'hll', 'c', 'err', 'k', 'l', 'n', 'o', 'p', 'cm', 'cp', 'c1', 'cs', + 'gd', 'ge', 'gr', 'gh', 'gi', 'go', 'gp', 'gs', 'gu', 'gt', 'kc', 'kd', 'kn', 'kp', 'kr', 'kt', + 'ld', 'm', 'mf', 'mh', 'mi', 'mo', 'na', 'nb', 'nc', 'no', 'nd', 'ni', 'ne', 'nf', 'nl', 'nn', + 'nt', 'nv', 'ow', 'w', 'mb', 'mh', 'mi', 'mo', 'sb', 'sc', 'sd', 'se', 'sh', 'si', 'sx', 's2', + 's1', 'ss', 'bp', 'vc', 'vg', 'vi', 'il' +] + +def class_filter(tag, name, value): + """自定义class属性过滤器""" + if name == 'class': + # 只允许预定义的安全class值 + allowed_classes = [cls for cls in value.split() if cls in ALLOWED_CLASSES] + return ' '.join(allowed_classes) if allowed_classes else False + return value + +# 安全的属性白名单 +ALLOWED_ATTRIBUTES = { + 'a': ['href', 'title'], + 'abbr': ['title'], + 'acronym': ['title'], + 'span': class_filter, + 'div': class_filter, + 'pre': class_filter, + 'code': class_filter +} + +# 安全的协议白名单 - 防止javascript:等危险协议 +ALLOWED_PROTOCOLS = ['http', 'https', 'mailto'] -#wwc def sanitize_html(html): """ - 清理 HTML 内容,移除不安全的标签和属性,防止跨站脚本(XSS)攻击。 - - 参数: - html (str): 待清理的 HTML 字符串 - - 返回: - str: 清理后的 HTML 字符串,仅包含 ALLOWED_TAGS 和 ALLOWED_ATTRIBUTES 中定义的内容 + 安全的HTML清理函数 + 使用bleach库进行白名单过滤,防止XSS攻击 """ - return bleach.clean(html, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRIBUTES) \ No newline at end of file + return bleach.clean( + html, + tags=ALLOWED_TAGS, + attributes=ALLOWED_ATTRIBUTES, + protocols=ALLOWED_PROTOCOLS, # 限制允许的协议 + strip=True, # 移除不允许的标签而不是转义 + strip_comments=True # 移除HTML注释 + ) diff --git a/src/djangoblog/whoosh_cn_backend.py b/src/djangoblog/whoosh_cn_backend.py index e8338dc..04e3f7f 100644 --- a/src/djangoblog/whoosh_cn_backend.py +++ b/src/djangoblog/whoosh_cn_backend.py @@ -22,7 +22,7 @@ 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 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 @@ -40,33 +40,30 @@ except ImportError: raise MissingDependency( "The 'whoosh' backend requires the installation of 'Whoosh'. Please refer to the documentation.") -# 检查 Whoosh 版本是否满足最低要求(2.5.0) +# Handle minimum requirement. if not hasattr(whoosh, '__version__') or whoosh.__version__ < (2, 5, 0): raise MissingDependency( "The 'whoosh' backend requires version 2.5.0 or greater.") -# 使用线程局部存储,用于 RAM 存储模式下的共享存储 +# Bubble up the correct error. + 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 -#wwc + class WhooshHtmlFormatter(HtmlFormatter): """ - 自定义高亮格式化器,使用简单标签包裹匹配关键词。 - 与 Solr、Elasticsearch 等后端保持一致的高亮样式。 + This is a HtmlFormatter simpler than the whoosh.HtmlFormatter. + We use it to have consistent results across backends. Specifically, + Solr, Xapian and Elasticsearch are using this formatting. """ template = '<%(tag)s>%(t)s' -#wwc -class WhooshSearchBackend(BaseSearchBackend): - """ - Whoosh 搜索后端实现类,负责与 Whoosh 引擎交互,执行索引、搜索、删除等操作。 - 继承自 haystack 的 BaseSearchBackend。 - """ - # Whoosh 中的保留关键字,不能直接用于查询 +class WhooshSearchBackend(BaseSearchBackend): + # Word reserved by Whoosh for special use. RESERVED_WORDS = ( 'AND', 'NOT', @@ -74,66 +71,69 @@ class WhooshSearchBackend(BaseSearchBackend): 'TO', ) - # Whoosh 中的保留字符,需要转义或加引号处理 + # Characters reserved by Whoosh for special use. + # The '\\' must come first, so as not to overwrite the other slash + # replacements. RESERVED_CHARACTERS = ( '\\', '+', '-', '&&', '||', '!', '(', ')', '{', '}', '[', ']', '^', '"', '~', '*', '?', ':', '.', ) -#wwc + def __init__(self, connection_alias, **connection_options): - """ - 初始化 Whoosh 后端连接。 - :param connection_alias: 连接别名(如 'default') - :param connection_options: 配置选项,如 PATH、STORAGE 等 - """ - 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) # POST 请求大小限制 - self.path = connection_options.get('PATH') # 索引文件存储路径 + 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) + self.path = connection_options.get('PATH') if connection_options.get('STORAGE', 'file') != 'file': - self.use_file_storage = False # 使用 RAM 存储 + 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) + "You must specify a 'PATH' in your settings for connection '%s'." % + connection_alias) + + self.log = logging.getLogger('haystack') - self.log = logging.getLogger('haystack') # 日志记录器 -#wwc def setup(self): """ - 初始化索引环境:创建目录、构建 schema、打开或创建索引。 - 延迟加载,首次使用时调用。 + Defers loading until needed. """ from haystack import connections new_index = False - # 创建索引目录 + # Make sure the index is there. 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) + "The path to your Whoosh index '%s' is not writable for the current user/group." % + self.path) - # 选择存储方式:文件 or 内存 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) # 查询解析器 + self.parser = QueryParser(self.content_field_name, schema=self.schema) - # 打开或创建索引 if new_index is True: self.index = self.storage.create_index(self.schema) else: @@ -143,94 +143,108 @@ class WhooshSearchBackend(BaseSearchBackend): self.index = self.storage.create_index(self.schema) self.setup_complete = True -#wwc + def build_schema(self, fields): - """ - 根据 Django 模型字段定义构建 Whoosh 的 Schema(索引结构)。 - :param fields: 所有注册的 SearchField 字段 - :return: (content_field_name, schema) 内容字段名和 schema 对象 - """ schema_fields = { ID: WHOOSH_ID(stored=True, unique=True), DJANGO_CT: WHOOSH_ID(stored=True), DJANGO_ID: WHOOSH_ID(stored=True), } + # Grab the number of keys that are hard-coded into Haystack. + # We'll use this to (possibly) fail slightly more gracefully later. 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) + 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) + 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) + 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) + schema_fields[field_class.index_fieldname] = NUMERIC( + stored=field_class.stored, numtype=float, field_boost=field_class.boost) elif field_class.field_type == 'boolean': - schema_fields[field_class.index_fieldname] = BOOLEAN(stored=field_class.stored) + # Field boost isn't supported on BOOLEAN as of 1.8.2. + schema_fields[field_class.index_fieldname] = BOOLEAN( + stored=field_class.stored) elif field_class.field_type == 'ngram': - schema_fields[field_class.index_fieldname] = NGRAM(minsize=3, maxsize=15, stored=field_class.stored, field_boost=field_class.boost) + 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': - schema_fields[field_class.index_fieldname] = NGRAMWORDS(minsize=2, maxsize=15, at='start', stored=field_class.stored, field_boost=field_class.boost) + schema_fields[field_class.index_fieldname] = NGRAMWORDS(minsize=2, maxsize=15, at='start', + stored=field_class.stored, + field_boost=field_class.boost) else: - # 默认文本字段,使用中文分词器 - schema_fields[field_class.index_fieldname] = TEXT(stored=True, analyzer=ChineseAnalyzer(), field_boost=field_class.boost, sortable=True) + # schema_fields[field_class.index_fieldname] = TEXT(stored=True, analyzer=StemmingAnalyzer(), field_boost=field_class.boost, sortable=True) + 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 # 支持拼写检查 + schema_fields[field_class.index_fieldname].spelling = True + # Fail more gracefully than relying on the backend to die if no fields + # are found. if len(schema_fields) <= initial_key_count: - raise SearchBackendError("No fields were found in any search_indexes. Please correct this before attempting to search.") + raise SearchBackendError( + "No fields were found in any search_indexes. Please correct this before attempting to search.") return (content_field_name, Schema(**schema_fields)) -#wwc + def update(self, index, iterable, commit=True): - """ - 更新索引文档(添加或更新)。 - :param index: SearchIndex 实例 - :param iterable: 要索引的对象集合(如 QuerySet) - :param commit: 是否立即提交(实际被忽略,Always commit) - """ if not self.setup_complete: self.setup() self.index = self.index.refresh() - writer = AsyncWriter(self.index) # 异步写入,避免锁 + writer = AsyncWriter(self.index) for obj in iterable: try: - doc = index.full_prepare(obj) # 准备文档数据 + doc = index.full_prepare(obj) except SkipDocument: self.log.debug(u"Indexing for object `%s` skipped", obj) else: + # Really make sure it's unicode, because Whoosh won't have it any + # other way. for key in doc: - doc[key] = self._from_python(doc[key]) # 转换为 Whoosh 可接受类型 + doc[key] = self._from_python(doc[key]) + # Document boosts aren't supported in Whoosh 2.5.0+. if 'boost' in doc: - del doc['boost'] # Whoosh 2.5+ 不支持文档级 boost + 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)}}) + + # We'll log the object identifier but won't include the actual object + # to avoid the possibility of that generating encoding errors while + # processing the log message: + 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() # 提交写入 -#wwc + # For now, commit no matter what, as we run into locking issues + # otherwise. + writer.commit() + def remove(self, obj_or_string, commit=True): - """ - 从索引中删除一个文档。 - :param obj_or_string: 要删除的对象或其 ID 字符串 - :param commit: 是否立即提交 - """ if not self.setup_complete: self.setup() @@ -238,18 +252,21 @@ class WhooshSearchBackend(BaseSearchBackend): whoosh_id = get_identifier(obj_or_string) try: - self.index.delete_by_query(q=self.parser.parse(u'%s:"%s"' % (ID, whoosh_id))) + 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) -#wwc + + self.log.error( + "Failed to remove document '%s' from Whoosh: %s", + whoosh_id, + e, + exc_info=True) + def clear(self, models=None, commit=True): - """ - 清空索引。可指定模型或清空全部。 - :param models: 要清除的模型列表 - :param commit: 是否提交 - """ if not self.setup_complete: self.setup() @@ -260,119 +277,176 @@ class WhooshSearchBackend(BaseSearchBackend): try: if models is None: - self.delete_index() # 删除整个索引 + 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))) + 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) + 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) -#wwc + self.log.error( + "Failed to clear Whoosh index: %s", e, exc_info=True) + def delete_index(self): - """ - 彻底删除索引目录并重建。 - """ + # Per the Whoosh mailing list, if wiping out everything from the index, + # it's much more efficient to simply delete the index files. 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() # 重新初始化 -#wwc + + # Recreate everything. + self.setup() + def optimize(self): - """ - 优化索引(合并段),提升搜索性能。 - """ if not self.setup_complete: self.setup() + self.index = self.index.refresh() self.index.optimize() -#wwc + def calculate_page(self, start_offset=0, end_offset=None): - """ - 计算分页参数(页码和每页数量),适配 Whoosh 的分页机制。 - :return: (page_num, page_length) - """ + # Prevent against Whoosh throwing an error. Requires an end_offset + # greater than 0. if end_offset is not None and end_offset <= 0: end_offset = 1 + + # Determine the page. + 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 - page_num = int(start_offset / page_length) + 1 # Whoosh 页码从 1 开始 + + if page_length and page_length > 0: + page_num = int(start_offset / page_length) + + # Increment because Whoosh uses 1-based page numbers. + page_num += 1 return page_num, page_length -#wwc + @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): - """ - 执行搜索查询。 - :param query_string: 查询字符串 - :param sort_by: 排序字段 - :param start_offset, end_offset: 分页偏移 - :param highlight: 是否高亮 - :param narrow_queries: 窄化查询(过滤) - :param models: 限制模型范围 - :param result_class: 搜索结果类 - :return: { 'results': [...], 'hits': int, 'spelling_suggestion': str } - """ + 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() + # A zero length query should return no results. if len(query_string) == 0: - return {'results': [], 'hits': 0} + return { + 'results': [], + 'hits': 0, + } query_string = force_str(query_string) + # A one-character query (non-wildcard) gets nabbed by a stopwords + # filter and should yield zero results. if len(query_string) <= 1 and query_string != u'*': - return {'results': [], 'hits': 0} + return { + 'results': [], + 'hits': 0, + } reverse = False + if sort_by is not None: - # 处理排序方向(仅支持全升或全降) + # Determine if we need to reverse the results and if Whoosh can + # handle what it's being asked to sort by. Reversing is an + # all-or-nothing action, unfortunately. 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") + 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] - # 不支持 facets if facets is not None: - warnings.warn("Whoosh does not handle faceting.", Warning, stacklevel=2) + 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) + 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) + 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) + 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: + # Using narrow queries, limit the results to only models handled + # with the current routers. model_choices = self.build_models_list() else: model_choices = [] @@ -380,15 +454,27 @@ class WhooshSearchBackend(BaseSearchBackend): 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_queries.add(' OR '.join( + ['%s:%s' % (DJANGO_CT, rm) for rm in model_choices])) narrow_searcher = None + if narrow_queries is not None: + # Potentially expensive? I don't see another way to do it in + # 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) + recent_narrowed_results = narrow_searcher.search( + self.parser.parse(force_str(nq)), limit=None) + if len(recent_narrowed_results) <= 0: - return {'results': [], 'hits': 0} + return { + 'results': [], + 'hits': 0, + } + if narrowed_results: narrowed_results.filter(recent_narrowed_results) else: @@ -399,82 +485,150 @@ class WhooshSearchBackend(BaseSearchBackend): if self.index.doc_count(): searcher = self.index.searcher() parsed_query = self.parser.parse(query_string) + + # In the event of an invalid/stopworded query, recover gracefully. if parsed_query is None: - return {'results': [], 'hits': 0} + return { + 'results': [], + 'hits': 0, + } - page_num, page_length = self.calculate_page(start_offset, end_offset) + page_num, page_length = self.calculate_page( + start_offset, end_offset) search_kwargs = { 'pagelen': page_length, 'sortedby': sort_by, 'reverse': reverse, } + + # Handle the case where the results have been narrowed. if narrowed_results is not None: search_kwargs['filter'] = narrowed_results try: - raw_page = searcher.search_page(parsed_query, page_num, **search_kwargs) + 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} - if raw_page.pagenum < page_num: - return {'results': [], 'hits': 0, 'spelling_suggestion': None} + 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) + # Because as of Whoosh 2.5.1, it will return the wrong page of + # results if you request something too high. :( + 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: - spelling_suggestion = self.create_spelling_suggestion(spelling_query or query_string) \ - if self.include_spelling else None - return {'results': [], 'hits': 0, 'spelling_suggestion': spelling_suggestion} -#wwc - 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): - """ - 基于某文档查找相似文档("More Like This" 功能)。 - """ + 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() + # Deferred models will have a different class ("RealClass_Deferred_fieldname") + # which won't be in our registry: model_klass = model_instance._meta.concrete_model + field_name = self.content_field_name narrow_queries = set() narrowed_results = None self.index = self.index.refresh() - # 模型过滤逻辑同 search if limit_to_registered_models is None: - limit_to_registered_models = getattr(settings, 'HAYSTACK_LIMIT_TO_REGISTERED_MODELS', True) + 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: + # Using narrow queries, limit the results to only models handled + # with the current routers. model_choices = self.build_models_list() else: model_choices = [] if len(model_choices) > 0: - narrow_queries.add(' OR '.join(['%s:%s' % (DJANGO_CT, rm) for rm in model_choices])) + 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: + + if narrow_queries is not None: + # Potentially expensive? I don't see another way to do it in + # 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) + recent_narrowed_results = narrow_searcher.search( + self.parser.parse(force_str(nq)), limit=None) + if len(recent_narrowed_results) <= 0: - return {'results': [], 'hits': 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() @@ -483,37 +637,61 @@ class WhooshSearchBackend(BaseSearchBackend): searcher = self.index.searcher() parsed_query = self.parser.parse(query) results = searcher.search(parsed_query) + if len(results): - raw_results = results[0].more_like_this(field_name, top=end_offset) - if narrowed_results and hasattr(raw_results, 'filter'): + raw_results = results[0].more_like_this( + field_name, top=end_offset) + + # Handle the case where the results have been narrowed. + 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} - if raw_page.pagenum < page_num: - return {'results': [], 'hits': 0} - results = self._process_results(raw_page, result_class=result_class) - searcher.close() - if hasattr(narrow_searcher, 'close'): - narrow_searcher.close() - return results - return {'results': [], 'hits': 0} -#wwc - def _process_results(self, raw_page, highlight=False, query_string='', spelling_query=None, result_class=None): - """ - 处理原始搜索结果,转换为 SearchResult 对象列表。 - :param raw_page: Whoosh 返回的原始结果页 - :param highlight: 是否生成高亮文本 - :return: { 'results': [...], 'hits': int, 'spelling_suggestion': str } - """ + + 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, + } + + # Because as of Whoosh 2.5.1, it will return the wrong page of + # results if you request something too high. :( + 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): from haystack import connections results = [] + + # It's important to grab the hits first before slicing. Otherwise, this + # can cause pagination failures. hits = len(raw_page) + if result_class is None: result_class = SearchResult + facets = {} spelling_suggestion = None unified_index = connections[self.connection_alias].get_unified_index() @@ -529,31 +707,58 @@ class WhooshSearchBackend(BaseSearchBackend): 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'): + + if string_key in index.fields and hasattr( + index.fields[string_key], 'convert'): + # Special-cased due to the nature of KEYWORD fields. if index.fields[string_key].is_multivalued: - additional_fields[string_key] = value.split(',') if value else [] + 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) + additional_fields[string_key] = index.fields[string_key].convert( + value) else: additional_fields[string_key] = self._to_python(value) - del additional_fields[DJANGO_CT] - del additional_fields[DJANGO_ID] + + del (additional_fields[DJANGO_CT]) + del (additional_fields[DJANGO_ID]) if highlight: sa = StemmingAnalyzer() formatter = WhooshHtmlFormatter('em') 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], score, **additional_fields) + 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], + score, + **additional_fields) results.append(result) else: hits -= 1 if self.include_spelling: - spelling_suggestion = self.create_spelling_suggestion(spelling_query or query_string) + if spelling_query: + spelling_suggestion = self.create_spelling_suggestion( + spelling_query) + else: + spelling_suggestion = self.create_spelling_suggestion( + query_string) return { 'results': results, @@ -561,122 +766,175 @@ class WhooshSearchBackend(BaseSearchBackend): 'facets': facets, 'spelling_suggestion': spelling_suggestion, } -#wwc + def create_spelling_suggestion(self, query_string): - """ - 生成拼写纠错建议。 - :param query_string: 原始查询字符串 - :return: 建议字符串或 None - """ - if not query_string: - return None + 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 + + # Clean the string. 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, '') + + # Break it down. query_words = cleaned_query.split() suggested_words = [] + for word in query_words: suggestions = corrector.suggest(word, limit=1) - if suggestions: + + if len(suggestions) > 0: suggested_words.append(suggestions[0]) - return ' '.join(suggested_words) -#wwc + + spelling_suggestion = ' '.join(suggested_words) + return spelling_suggestion + def _from_python(self, value): """ - 将 Python 值转换为 Whoosh 可索引的字符串。 + Converts Python values to a string for Whoosh. + + Code courtesy of 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): - value = 'true' if value else 'false' + 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)): + # Leave it alone. pass else: value = force_str(value) return value -#wwc + def _to_python(self, value): """ - 将 Whoosh 存储的值转换回 Python 类型。 + Converts values from Whoosh to native Python values. + + A port of the same method in pysolr, as they deal with data the same way. """ 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(**date_values) + + return datetime( + date_values['year'], + date_values['month'], + date_values['day'], + date_values['hour'], + date_values['minute'], + date_values['second']) + try: + # Attempt to use json to load the values. converted_value = json.loads(value) - if isinstance(converted_value, (list, tuple, set, dict, six.integer_types, float, complex)): + + # Try to handle most built-in types. + if isinstance( + converted_value, + (list, + tuple, + set, + dict, + six.integer_types, + float, + complex)): return converted_value except BaseException: + # If it fails (SyntaxError or its ilk) or we don't trust it, + # continue on. pass + return value -#wwc -class WhooshSearchQuery(BaseSearchQuery): - """ - Whoosh 查询构建器,负责将 Django 查询语法转换为 Whoosh 查询字符串。 - """ +class WhooshSearchQuery(BaseSearchQuery): def _convert_datetime(self, date): - """将 datetime 转换为 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')) -#wwc + def clean(self, query_fragment): """ - 清理用户输入的查询片段,避免保留字和字符引发语法错误。 + Provides a mechanism for sanitizing user input before presenting the + value to the backend. + + Whoosh 1.X differs here in that you can no longer use a backslash + to escape reserved characters. Instead, the whole word should be + quoted. """ words = query_fragment.split() cleaned_words = [] + for word in words: if word in self.backend.RESERVED_WORDS: - word = word.lower() + 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) -#wwc + def build_query_fragment(self, field, filter_type, value): - """ - 构建单个查询片段,如 "title:django" 或 "pub_date:[20200101 TO 20201231]" - """ from haystack import connections query_frag = '' is_datetime = False if not hasattr(value, 'input_type_name'): + # Handle when we've got a ``ValuesListQuerySet``... if hasattr(value, 'values_list'): value = list(value) + if hasattr(value, 'strftime'): is_datetime = True + if isinstance(value, six.string_types) and value != ' ': + # It's not an ``InputType``. Assume ``Clean``. value = Clean(value) else: value = PythonData(value) + # Prepare the query using the InputType. prepared_value = value.prepare(self) + if not isinstance(prepared_value, (set, list, tuple)): + # Then convert whatever we get back to what pysolr wants if needed. prepared_value = self.backend._from_python(prepared_value) - index_fieldname = '' if field == 'content' else u'%s:' % connections[self._using].get_unified_index().get_index_fieldname(field) + # 'content' is a special reserved word, much like 'pk' in + # Django's ORM layer. It indicates 'no special field'. + if field == 'content': + index_fieldname = '' + else: + index_fieldname = u'%s:' % connections[self._using].get_unified_index( + ).get_index_fieldname(field) filter_types = { 'content': '%s', @@ -694,45 +952,93 @@ class WhooshSearchQuery(BaseSearchQuery): if value.post_process is False: query_frag = prepared_value else: - if filter_type in ['content', 'contains', 'startswith', 'endswith', 'fuzzy']: - terms = [] - possible_values = prepared_value.split(' ') if isinstance(prepared_value, six.string_types) else [prepared_value] - if is_datetime: - possible_values = [self._convert_datetime(pv) for pv in possible_values] - for pv in possible_values: - terms.append(filter_types[filter_type] % self.backend._from_python(pv)) - query_frag = terms[0] if len(terms) == 1 else u"(%s)" % " AND ".join(terms) + if filter_type in [ + 'content', + 'contains', + 'startswith', + 'endswith', + 'fuzzy']: + if value.input_type_name == 'exact': + query_frag = prepared_value + else: + # Iterate over terms & incorportate the converted form of + # each into the query. + 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) elif filter_type == 'in': in_options = [] - for pv in prepared_value: - is_dt = hasattr(pv, 'strftime') - pv = self.backend._from_python(pv) - pv = self._convert_datetime(pv) if is_dt else pv - in_options.append('"%s"' % pv if isinstance(pv, six.string_types) and not is_dt else '%s' % pv) + + 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) elif filter_type == 'range': start = self.backend._from_python(prepared_value[0]) end = self.backend._from_python(prepared_value[1]) - start = self._convert_datetime(start) if hasattr(prepared_value[0], 'strftime') else start - end = self._convert_datetime(end) if hasattr(prepared_value[1], 'strftime') else end + + 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': - prepared_value = Exact(prepared_value).prepare(self) - query_frag = filter_types[filter_type] % prepared_value + 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: + if is_datetime is True: prepared_value = self._convert_datetime(prepared_value) + query_frag = filter_types[filter_type] % prepared_value - if query_frag and not isinstance(value, Raw) and not query_frag.startswith('(') and not query_frag.endswith(')'): - query_frag = "(%s)" % query_frag + 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) -#wwc + # if not filter_type in ('in', 'range'): + # # 'in' is a bit of a special case, as we don't want to + # # convert a valid list/tuple to string. Defer handling it + # # until later... + # value = self.backend._from_python(value) + + class WhooshEngine(BaseEngine): - """ - Haystack 引擎注册类,绑定 Backend 和 Query 类。 - """ backend = WhooshSearchBackend - query = WhooshSearchQuery \ No newline at end of file + query = WhooshSearchQuery diff --git a/src/djangoblog/wsgi.py b/src/djangoblog/wsgi.py index b62d8ab..2295efd 100644 --- a/src/djangoblog/wsgi.py +++ b/src/djangoblog/wsgi.py @@ -7,19 +7,10 @@ For more information on this file, see https://docs.djangoproject.com/en/1.10/howto/deployment/wsgi/ """ -# 导入Python标准库中的os模块,用于与操作系统进行交互,例如设置环境变量 import os -# 从Django框架中导入get_wsgi_application函数 -# 该函数用于创建一个符合WSGI规范的应用程序对象,是Django与Web服务器(如Apache、Nginx、Gunicorn等)通信的入口 from django.core.wsgi import get_wsgi_application -# 设置环境变量DJANGO_SETTINGS_MODULE为'djangoblog.settings' -# 这行代码告诉Django项目应该使用哪个配置文件(settings模块) -# 在这个例子中,使用的是djangoblog项目下的settings.py文件来加载配置 os.environ.setdefault("DJANGO_SETTINGS_MODULE", "djangoblog.settings") -# 调用get_wsgi_application()函数,生成一个WSGI可调用的应用程序实例 -# 这个实例(赋值给变量application)是WSGI服务器用来处理HTTP请求的入口点 -# 所有进入Django应用的请求都会通过这个application对象进行分发和处理 -application = get_wsgi_application() \ No newline at end of file +application = get_wsgi_application() diff --git a/src/owntracks/__init__.py b/src/owntracks/__init__.py index de9bcf4..e69de29 100644 --- a/src/owntracks/__init__.py +++ b/src/owntracks/__init__.py @@ -1 +0,0 @@ -#gjl \ No newline at end of file diff --git a/src/owntracks/admin.py b/src/owntracks/admin.py index 028a390..655b535 100644 --- a/src/owntracks/admin.py +++ b/src/owntracks/admin.py @@ -1,11 +1,7 @@ -#gjl from django.contrib import admin # Register your models here. -# 定义一个 Django Admin 管理类,用于自定义管理界面中 OwnTrackLogs 模型的显示和行为 -# 当前未添加任何自定义配置,使用默认的管理界面设置 + class OwnTrackLogsAdmin(admin.ModelAdmin): - # gjl该类目前为空,表示使用 Django admin 的默认行为来管理 OwnTrackLogs 模型 - # gjl可在此处添加如 list_display, list_filter, search_fields 等属性来自定义管理界面 - pass \ No newline at end of file + pass diff --git a/src/owntracks/apps.py b/src/owntracks/apps.py index c3c97de..1bc5f12 100644 --- a/src/owntracks/apps.py +++ b/src/owntracks/apps.py @@ -1,13 +1,5 @@ -#gjl from django.apps import AppConfig -# OwntracksConfig 是 Django 应用 'owntracks' 的配置类。 -# 它继承自 Django 的 AppConfig,用于定义该应用的配置信息。 -# 当 Django 启动时,会自动发现并加载此配置,以正确初始化该应用。 class OwntracksConfig(AppConfig): - # gjl name 属性指定了该 Django 应用的完整 Python 导入路径。 - # gjl 这个路径告诉 Django 如何导入这个应用。在这个例子中, - # gjl 表示该应用位于 PYTHONPATH 下可以直接导入的 'owntracks' 模块(或包)中。 - # gjl 这个值通常与应用所在目录的名称一致。 - name = 'owntracks' \ No newline at end of file + name = 'owntracks' diff --git a/src/owntracks/migrations/0001_initial.py b/src/owntracks/migrations/0001_initial.py index ebb45da..9eee55c 100644 --- a/src/owntracks/migrations/0001_initial.py +++ b/src/owntracks/migrations/0001_initial.py @@ -1,78 +1,31 @@ -#gjl # Generated by Django 4.1.7 on 2023-03-02 07:14 -# 导入 Django 迁移系统所需的核心模块 -# migrations: 用于定义数据库模式变更 -# models: 用于定义模型字段类型 from django.db import migrations, models -# 导入 Django 的时区工具,用于提供默认时间 import django.utils.timezone -# 定义一个 Django 数据库迁移类 -# 该类继承自 migrations.Migration,由 Django 自动生成,用于描述对数据库模式的变更 class Migration(migrations.Migration): - # gjl标记此迁移为应用的初始迁移(initial migration) - # gjl当应用首次被迁移时,Django 会识别并执行此迁移 + initial = True - # 定义此迁移所依赖的其他迁移 - # 由于这是初始迁移且没有依赖其他应用的模型,因此为空列表 - # 如果模型外键关联了其他应用的模型,则需要在此列出对应的迁移 dependencies = [ ] - # gjl定义此迁移要执行的具体数据库操作列表 - # gjl每个操作是一个 Migration 操作类的实例,如 CreateModel、AddField 等 operations = [ - # 创建一个名为 'OwnTrackLog' 的新数据库模型(对应一张数据表) migrations.CreateModel( - # 指定要创建的模型名称 name='OwnTrackLog', - # 定义该模型包含的数据库字段 - # 每个字段由字段名和字段类型及参数组成 fields=[ - # gjl主键字段 'id':自增的 BigAutoField(64位整数) - # auto_created=True: 表示是 Django 自动创建的主键 - # primary_key=True: 指定为主键 - # serialize=False: 在序列化(如 dumpdata)时是否包含此字段 - # verbose_name='ID': 在管理后台等界面显示的可读名称 ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - - # 'tid' 字段:存储用户或设备标识,使用 CharField - # max_length=100: 最大长度为100个字符 - # verbose_name='用户': 在界面中显示为“用户” ('tid', models.CharField(max_length=100, verbose_name='用户')), - - # 'lat' 字段:存储纬度值,使用 FloatField(浮点数) - # verbose_name='纬度': 在界面中显示为“纬度” ('lat', models.FloatField(verbose_name='纬度')), - - # 'lon' 字段:存储经度值,使用 FloatField(浮点数) - # verbose_name='经度': 在界面中显示为“经度” ('lon', models.FloatField(verbose_name='经度')), - - # gjl 'created_time' 字段:记录创建时间,使用 DateTimeField - # gjl default=django.utils.timezone.now: 默认值为当前时间(注意:传入函数引用,非调用结果) - # gjk verbose_name='创建时间': 在界面中显示为“创建时间” ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')), ], - # gjl定义该模型的元数据选项(对应模型内部的 Meta 类) options={ - # verbose_name: 模型在管理后台的单数可读名称 'verbose_name': 'OwnTrackLogs', - - # verbose_name_plural: 模型在管理后台的复数可读名称 - # 此处与单数名称相同 'verbose_name_plural': 'OwnTrackLogs', - - # ordering: 查询该模型时的默认排序字段 - # 按 'created_time' 升序排列 'ordering': ['created_time'], - - # get_latest_by: 指定用于获取最新记录的字段 - # 调用 latest() 方法时会基于此字段排序 'get_latest_by': 'created_time', }, ), - ] \ No newline at end of file + ] diff --git a/src/owntracks/migrations/0002_alter_owntracklog_options_and_more.py b/src/owntracks/migrations/0002_alter_owntracklog_options_and_more.py index 32f149a..b4f8dec 100644 --- a/src/owntracks/migrations/0002_alter_owntracklog_options_and_more.py +++ b/src/owntracks/migrations/0002_alter_owntracklog_options_and_more.py @@ -1,52 +1,22 @@ -#gjl # Generated by Django 4.2.5 on 2023-09-06 13:19 -# 导入 Django 数据库迁移模块,用于执行数据库模式变更 from django.db import migrations -# 定义一个 Django 数据库迁移类 -# 该类继承自 migrations.Migration,由 Django 自动生成,用于描述对数据库模式的变更 -# 此迁移是继初始迁移之后的第二次迁移,用于修改现有模型 class Migration(migrations.Migration): - # gjl定义此迁移所依赖的其他迁移 - # gjl必须在 'owntracks' 应用的 '0001_initial' 迁移成功执行后,此迁移才能执行 - # gjl确保数据库状态的顺序性和一致性 + dependencies = [ ('owntracks', '0001_initial'), ] - # gjl定义此迁移要执行的具体数据库操作列表 - # gjl包含两个操作:修改模型选项和重命名字段 operations = [ - # gjl操作一:修改模型 'owntracklog' 的元数据选项(Meta options) - # gjl将模型的 verbose_name, verbose_name_plural, ordering, get_latest_by 等选项更新为新的值 - # gjl注意:此操作只修改 Django ORM 层的元数据,不直接修改数据库表结构 migrations.AlterModelOptions( - # gjl指定要修改选项的模型名称(小写) name='owntracklog', - # 新的元数据选项字典 - options={ - # 指定获取最新记录时依据的字段 - 'get_latest_by': 'creation_time', - # 指定模型实例的默认排序方式 - 'ordering': ['creation_time'], - # 在管理后台等界面中显示的单数名称 - 'verbose_name': 'OwnTrackLogs', - # 在管理后台等界面中显示的复数名称 - 'verbose_name_plural': 'OwnTrackLogs', - }, + options={'get_latest_by': 'creation_time', 'ordering': ['creation_time'], 'verbose_name': 'OwnTrackLogs', 'verbose_name_plural': 'OwnTrackLogs'}, ), - - # 操作二:将模型 'owntracklog' 中的数据库字段从 'created_time' 重命名为 'creation_time' - # 这是一个数据库级别的 DDL 操作,会实际执行 ALTER TABLE ... RENAME COLUMN 语句 - # 确保数据库字段名与 Django 模型代码中的字段名保持一致 migrations.RenameField( - # 指定要操作的模型名称 model_name='owntracklog', - # 旧的字段名(数据库中当前的列名) old_name='created_time', - # 新的字段名(要更改为的列名) new_name='creation_time', ), - ] \ No newline at end of file + ] diff --git a/src/owntracks/migrations/__init__.py b/src/owntracks/migrations/__init__.py index de9bcf4..e69de29 100644 --- a/src/owntracks/migrations/__init__.py +++ b/src/owntracks/migrations/__init__.py @@ -1 +0,0 @@ -#gjl \ No newline at end of file diff --git a/src/owntracks/models.py b/src/owntracks/models.py index 9227a60..760942c 100644 --- a/src/owntracks/models.py +++ b/src/owntracks/models.py @@ -1,57 +1,20 @@ -#gjl from django.db import models from django.utils.timezone import now # Create your models here. -# 定义一个名为 OwnTrackLog 的数据库模型,用于存储由 OwnTracks 应用程序上传的位置日志数据。 -# 该模型继承自 Django 的 models.Model 类,每个类属性对应数据库中的一列。 class OwnTrackLog(models.Model): - # gjl (Tracker ID): 标识发送位置信息的设备或用户。 - # gjl 使用 CharField 存储字符串,最大长度为100个字符,不允许为空(null=False)。 - # gjl verbose_name 设置为 '用户',在 Django 管理后台等界面中显示为“用户”。 tid = models.CharField(max_length=100, null=False, verbose_name='用户') - - # gjl lat: 存储地理位置的纬度坐标。 - # gjl 使用 FloatField 存储浮点数,表示纬度值(例如:39.9042)。 - # gjl verbose_name 设置为 '纬度',用于在管理后台中显示友好的字段名。 lat = models.FloatField(verbose_name='纬度') - - # gjl lon: 存储地理位置的经度坐标。 - # gjl 使用 FloatField 存储浮点数,表示经度值(例如:116.4074)。 - # gjlverbose_name 设置为 '经度',用于在管理后台中显示友好的字段名。 lon = models.FloatField(verbose_name='经度') - - # gjl creation_time: 记录该位置信息被创建的时间。 - # gjl使用 DateTimeField 存储日期和时间。 - # gjl第一个参数 '创建时间' 是该字段的 verbose_name(可读名称)。 - # gjldefault=now 表示默认值为当前时间(注意:这里传入的是函数引用 now,不是 now(),以避免迁移时时间固定)。 creation_time = models.DateTimeField('创建时间', default=now) - # gjl __str__(self) 方法定义了该模型实例对象在转换为字符串时的行为。 - # gjl 在 Django 管理后台、shell 或其他需要显示对象的地方,会显示此方法的返回值。 - # gjl此处返回 self.tid,即该记录对应的用户/设备ID,便于识别。 def __str__(self): return self.tid - # Meta 类是 Django 模型的内部类,用于定义模型的元数据(metadata),不作为一个字段存在。 class Meta: - # ordering: 指定查询该模型时的默认排序方式。 - # ['creation_time'] 表示按 creation_time 字段升序排列(最早的数据在前)。 - # 若需降序,可写为 ['-creation_time']。 ordering = ['creation_time'] - - # verbose_name: 指定该模型在 Django 管理后台或其他用户界面中的可读单数名称。 - # 这里设置为 "OwnTrackLogs",会在管理后台的菜单中显示为此名称。 verbose_name = "OwnTrackLogs" - - # verbose_name_plural: 指定该模型的可读复数名称。 - # 通常 Django 会自动加 's',但有时需要自定义。 - # 此处将其设置为与 verbose_name 相同,即单复数形式一致,都显示为 "OwnTrackLogs"。 verbose_name_plural = verbose_name - - # get_latest_by: 指定用于获取最新记录的字段。 - # 当调用模型管理器的 latest() 方法时,Django 会使用此字段进行排序并返回最新的对象。 - # 例如:OwnTrackLog.objects.latest() 将返回 creation_time 最晚的记录。 - get_latest_by = 'creation_time' \ No newline at end of file + get_latest_by = 'creation_time' diff --git a/src/owntracks/tests.py b/src/owntracks/tests.py index 191e161..3b4b9d8 100644 --- a/src/owntracks/tests.py +++ b/src/owntracks/tests.py @@ -1,116 +1,64 @@ -#gjl import json -# 导入 Django 测试框架所需的核心类和模块 from django.test import Client, RequestFactory, TestCase -# 导入项目中使用的自定义用户模型和位置日志模型,用于在测试中创建用户和验证数据 from accounts.models import BlogUser from .models import OwnTrackLog # Create your tests here. -# 定义一个测试类,用于测试 OwnTrackLog 相关的功能,如数据接收、地图展示、数据查询等 -# 继承自 Django 的 TestCase,提供数据库隔离、断言方法和测试工具 class OwnTrackLogTest(TestCase): - # gjl setUp 方法在每个测试方法执行前自动运行,用于准备测试所需的环境和对象 - # gjl 本方法中初始化了测试客户端和请求工厂,便于模拟 HTTP 请求 def setUp(self): - # 创建一个 Django 测试客户端实例,用于模拟浏览器发送 GET、POST 等请求 self.client = Client() - - # gjl 创建一个请求工厂实例,用于创建伪造的 HTTP 请求对象(如 HttpRequest) - # gjl 适用于需要直接调用视图函数的测试场景(本测试中未直接使用,但已准备) self.factory = RequestFactory() - # gjl test_own_track_log 是一个具体的测试用例,用于验证 OwnTrackLog 功能的正确性 - # gjl 测试内容包括:接收位置数据、数据完整性、权限控制、页面响应等 def test_own_track_log(self): - # gjl 构造一个模拟的位置数据字典,包含 tid(用户标识)、lat(纬度)、lon(经度) o = { 'tid': 12, 'lat': 123.123, 'lon': 134.341 } - # gjl 使用测试客户端发送一个 POST 请求到 '/owntracks/logtracks' 接口 - # gjl 将位置数据 o 序列化为 JSON 字符串,并设置内容类型为 application/json - # gjl 模拟 OwnTracks 客户端上传位置信息 self.client.post( '/owntracks/logtracks', json.dumps(o), content_type='application/json') - - # 查询数据库中 OwnTrackLog 模型的所有记录数量 length = len(OwnTrackLog.objects.all()) - - # 断言:验证数据库中记录数为 1,确保第一次 POST 请求成功写入一条数据 self.assertEqual(length, 1) - # 构造第二条位置数据,缺少 'lon' 字段(经度) o = { 'tid': 12, 'lat': 123.123 } - # 再次发送 POST 请求上传不完整的位置数据 self.client.post( '/owntracks/logtracks', json.dumps(o), content_type='application/json') - - # 再次查询数据库中的总记录数 length = len(OwnTrackLog.objects.all()) - - # 断言:验证记录数仍为 1,确保系统能正确处理不完整数据(如忽略或拒绝),未新增无效记录 self.assertEqual(length, 1) - # 发送 GET 请求访问 '/owntracks/show_maps' 页面(查看地图) rsp = self.client.get('/owntracks/show_maps') - - # 断言:检查响应状态码为 302(重定向),说明未登录用户访问地图页时被重定向(如跳转到登录页) self.assertEqual(rsp.status_code, 302) - # gjl 创建一个超级用户,用于测试需要登录权限的功能 - # gjl 使用 BlogUser 模型的 create_superuser 方法创建具有管理员权限的测试用户 user = BlogUser.objects.create_superuser( email="liangliangyy1@gmail.com", username="liangliangyy1", password="liangliangyy1") - # gjl 使用测试客户端以刚创建的超级用户身份登录 - # gjl 模拟用户登录过程,使后续请求具有认证状态 self.client.login(username='liangliangyy1', password='liangliangyy1') - - # 手动创建一条 OwnTrackLog 记录并保存到数据库 - # 直接操作模型,用于准备测试数据 s = OwnTrackLog() s.tid = 12 s.lon = 123.234 s.lat = 34.234 s.save() - # 发送 GET 请求访问 '/owntracks/show_dates' 页面(查看日期列表) rsp = self.client.get('/owntracks/show_dates') - - # 断言:检查响应状态码为 200(成功),说明登录后可以正常访问该页面 self.assertEqual(rsp.status_code, 200) - - # 发送 GET 请求访问 '/owntracks/show_maps' 页面(查看地图) rsp = self.client.get('/owntracks/show_maps') - - # 断言:检查响应状态码为 200(成功),说明登录后可以正常访问地图页面 self.assertEqual(rsp.status_code, 200) - - # 发送 GET 请求访问 '/owntracks/get_datas' 接口(获取位置数据) rsp = self.client.get('/owntracks/get_datas') - - # 断言:检查响应状态码为 200(成功),说明数据接口正常返回 self.assertEqual(rsp.status_code, 200) - - # 发送 GET 请求访问 '/owntracks/get_datas' 接口,并携带日期查询参数 date=2018-02-26 rsp = self.client.get('/owntracks/get_datas?date=2018-02-26') - - # 断言:检查响应状态码为 200(成功),说明带参数的数据查询功能正常 - self.assertEqual(rsp.status_code, 200) \ No newline at end of file + self.assertEqual(rsp.status_code, 200) diff --git a/src/owntracks/urls.py b/src/owntracks/urls.py index 2b87930..c19ada8 100644 --- a/src/owntracks/urls.py +++ b/src/owntracks/urls.py @@ -1,35 +1,12 @@ -#gjl from django.urls import path -# 导入当前应用(owntracks)的视图模块,用于将 URL 路由映射到具体的视图函数 from . import views -# 定义应用命名空间(application namespace) -# 允许在项目其他地方(如模板、reverse() 函数)通过 'owntracks:xxx' 的方式引用本应用的 URL -# 避免不同应用之间 URL 名称冲突 app_name = "owntracks" -# urlpatterns 是 Django 用于定义 URL 路由的核心变量 -# 它是一个列表,包含了一系列 path() 实例,每个实例定义了一个 URL 模式与视图函数的映射关系 urlpatterns = [ - # gjl 将 URL 路径 'owntracks/logtracks' 映射到 views 模块中的 manage_owntrack_log 视图函数 - # gjl 该接口用于接收 OwnTracks 客户端上传的位置日志数据(通常为 POST 请求) - # gjl 在模板或代码中可通过 {% url 'owntracks:logtracks' %} 反向解析此 URL path('owntracks/logtracks', views.manage_owntrack_log, name='logtracks'), - - # gjl将 URL 路径 'owntracks/show_maps' 映射到 views 模块中的 show_maps 视图函数 - # gjl该接口用于渲染并展示用户位置轨迹的地图页面(通常为 GET 请求) - # gjl需要用户登录后访问,显示历史轨迹 path('owntracks/show_maps', views.show_maps, name='show_maps'), - - # gjl 将 URL 路径 'owntracks/get_datas' 映射到 views 模块中的 get_datas 视图函数 - # gjl 该接口是一个数据接口(API),用于返回指定日期的位置轨迹数据(通常为 JSON 格式) - # gjl 前端地图页面通过 AJAX 调用此接口获取数据进行渲染 - # gjl 支持通过查询参数(如 ?date=...)指定日期 path('owntracks/get_datas', views.get_datas, name='get_datas'), - - # gjl 将 URL 路径 'owntracks/show_dates' 映射到 views 模块中的 show_log_dates 视图函数 - # gjl 该接口用于展示用户有轨迹记录的所有日期列表页面 - # gjl 通常作为地图页面的前置页面,让用户选择要查看的日期 path('owntracks/show_dates', views.show_log_dates, name='show_dates') -] \ No newline at end of file +] diff --git a/src/owntracks/views.py b/src/owntracks/views.py index 8268f94..4c72bdd 100644 --- a/src/owntracks/views.py +++ b/src/owntracks/views.py @@ -1,4 +1,3 @@ -#gjl # Create your views here. import datetime import itertools @@ -15,187 +14,114 @@ from django.http import JsonResponse from django.shortcuts import render from django.views.decorators.csrf import csrf_exempt -# 导入当前应用的模型,用于在视图中操作数据库 from .models import OwnTrackLog -# 获取一个 logger 实例,用于记录日志信息(如请求、错误等) logger = logging.getLogger(__name__) -# 视图函数:接收并处理 OwnTracks 客户端上传的位置日志数据 -# 使用 @csrf_exempt 装饰器免除 CSRF 验证,因为客户端可能无法提供 CSRF token -# 该函数预期接收 JSON 格式的 POST 请求 @csrf_exempt def manage_owntrack_log(request): try: - # gjl 读取请求体的原始数据,并解码为 UTF-8 字符串,然后解析为 Python 字典 s = json.loads(request.read().decode('utf-8')) - # gjl 从解析后的数据中提取 tid(用户/设备标识)、lat(纬度)、lon(经度) tid = s['tid'] lat = s['lat'] lon = s['lon'] - # gjl 记录一条 INFO 级别的日志,包含接收到的位置信息 logger.info( 'tid:{tid}.lat:{lat}.lon:{lon}'.format( tid=tid, lat=lat, lon=lon)) - - # 验证提取的数据是否都存在(非空) if tid and lat and lon: - # gjl 创建一个新的 OwnTrackLog 模型实例 m = OwnTrackLog() - # gjl 将接收到的数据赋值给模型字段 m.tid = tid m.lat = lat m.lon = lon - # gjl 保存到数据库(creation_time 会自动使用默认值 now) m.save() - # gjl 返回 HTTP 响应 'ok',表示数据接收和保存成功 return HttpResponse('ok') else: - # gjl 如果数据不完整,返回 'data error' return HttpResponse('data error') except Exception as e: - # gjl 捕获任何异常,记录错误日志,并返回 'error' 响应 logger.error(e) return HttpResponse('error') -# gjl 视图函数:渲染显示地图的 HTML 页面 -# gjl 使用 @login_required 装饰器,确保只有登录用户才能访问 @login_required def show_maps(request): - # gjl 进一步检查用户是否为超级用户(管理员) if request.user.is_superuser: - # gjl 获取当前 UTC 日期,作为默认日期 defaultdate = str(datetime.datetime.now(timezone.utc).date()) - # gjl 从 GET 请求参数中获取 'date',如果不存在则使用默认日期 date = request.GET.get('date', defaultdate) - # gjl 构造模板上下文,将日期传递给前端 context = { 'date': date } - # gjl 使用指定的 HTML 模板渲染响应 return render(request, 'owntracks/show_maps.html', context) else: - # gjl 如果不是超级用户,返回 403 禁止访问 from django.http import HttpResponseForbidden return HttpResponseForbidden() -# gjl 视图函数:渲染显示有轨迹记录的日期列表页面 -# gjl 使用 @login_required 装饰器,确保只有登录用户才能访问 @login_required def show_log_dates(request): - # gjl 查询所有 OwnTrackLog 记录的 creation_time 字段值 dates = OwnTrackLog.objects.values_list('creation_time', flat=True) - # gjl 将 datetime 对象转换为 'YYYY-MM-DD' 格式的字符串,并去重、排序 results = list(sorted(set(map(lambda x: x.strftime('%Y-%m-%d'), dates)))) - # gjl 构造模板上下文,将日期列表传递给前端 context = { 'results': results } - # gjl 使用指定的 HTML 模板渲染响应 return render(request, 'owntracks/show_log_dates.html', context) -# gjl 辅助函数:将 GPS 坐标(WGS-84)批量转换为高德地图坐标系(GCJ-02) -# gjl 由于高德 API 有数量限制,此处每次处理最多 30 个点 -# gjl 注意:此函数在 get_datas 中被注释掉,当前未使用 def convert_to_amap(locations): - convert_result = [] # 存储转换后的坐标结果 - it = iter(locations) # 创建一个迭代器 + convert_result = [] + it = iter(locations) - # gjl 每次从迭代器中取出最多 30 个位置点 item = list(itertools.islice(it, 30)) while item: - # gjl 将每个点的经度和纬度拼接成 "lon,lat" 字符串,用分号连接成一个列表 datas = ';'.join( set(map(lambda x: str(x.lon) + ',' + str(x.lat), item))) - # gjl 高德坐标转换 API 的固定参数 key = '8440a376dfc9743d8924bf0ad141f28e' api = 'http://restapi.amap.com/v3/assistant/coordinate/convert' - # 构造请求参数 query = { 'key': key, 'locations': datas, - 'coordsys': 'gps' # 指定原始坐标系为 GPS(WGS-84) + 'coordsys': 'gps' } - # 发送 GET 请求到高德 API rsp = requests.get(url=api, params=query) - # 解析 API 返回的 JSON 数据 result = json.loads(rsp.text) - # 如果返回结果包含转换后的坐标,则添加到结果列表 if "locations" in result: convert_result.append(result['locations']) - # 继续处理下一批最多 30 个点 item = list(itertools.islice(it, 30)) - # 将所有批次的转换结果合并成一个以分号分隔的字符串并返回 return ";".join(convert_result) -# gjl 视图函数:返回指定日期的位置轨迹数据,供前端地图使用 -# gjl 返回 JSON 格式的数据,通常由 AJAX 调用 -# gjl 使用 @login_required 装饰器,确保只有登录用户才能访问 @login_required def get_datas(request): - # 获取当前 UTC 时间 now = django.utils.timezone.now().replace(tzinfo=timezone.utc) - # 构造查询日期的起始时间(当天 00:00:00 UTC) querydate = django.utils.timezone.datetime( now.year, now.month, now.day, 0, 0, 0) - - # 如果请求中包含 'date' 参数,则使用该参数指定的日期 if request.GET.get('date', None): - # 解析 'YYYY-MM-DD' 格式的日期字符串 date = list(map(lambda x: int(x), request.GET.get('date').split('-'))) querydate = django.utils.timezone.datetime( date[0], date[1], date[2], 0, 0, 0) - - # gjl 计算查询结束时间(起始时间 + 1 天) nextdate = querydate + datetime.timedelta(days=1) - - # gjl查询指定日期范围内(当天)的所有位置记录 models = OwnTrackLog.objects.filter( creation_time__range=(querydate, nextdate)) - - # gjl 初始化结果列表 result = list() - - # glj 如果查询到记录,则按 tid(用户/设备)分组处理 if models and len(models): for tid, item in groupby( - # 先按 tid 排序,以便 groupby 正确分组 - sorted(models, key=lambda k: k.tid), - # 指定分组依据为 tid - key=lambda k: k.tid): + sorted(models, key=lambda k: k.tid), key=lambda k: k.tid): - # gjl 为每个 tid 创建一个数据字典 d = dict() - d["name"] = tid # 记录设备/用户名称 - paths = list() # 存储该设备的轨迹点序列 - - # gjl 方案一(当前启用):直接使用原始 GPS 坐标 - # gjl 遍历该设备当天的所有位置点,按时间排序 - for location in sorted(item, key=lambda x: x.creation_time): - # gjl 将每个点的经度和纬度作为字符串列表添加到 paths - paths.append([str(location.lon), str(location.lat)]) - - # # gjl 方案二(注释中):使用高德转换后的坐标 - # # gjl 先转换坐标,然后解析 + d["name"] = tid + paths = list() + # 使用高德转换后的经纬度 # locations = convert_to_amap( # sorted(item, key=lambda x: x.creation_time)) # for i in locations.split(';'): # paths.append(i.split(',')) - - # 将轨迹点列表赋值给字典的 "path" 键 + # 使用GPS原始经纬度 + for location in sorted(item, key=lambda x: x.creation_time): + paths.append([str(location.lon), str(location.lat)]) d["path"] = paths - # 将该设备的轨迹数据添加到总结果列表 result.append(d) - - # gjl 将结果列表序列化为 JSON 并返回 - # gjl safe=False 允许返回非字典类型的对象(如列表) - return JsonResponse(result, safe=False) \ No newline at end of file + return JsonResponse(result, safe=False) diff --git a/src/plugins/ai_assistant/__init__.py b/src/plugins/ai_assistant/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/plugins/ai_assistant/urls.py b/src/plugins/ai_assistant/urls.py new file mode 100644 index 0000000..a034e7d --- /dev/null +++ b/src/plugins/ai_assistant/urls.py @@ -0,0 +1,6 @@ +from django.urls import path +from . import views + +urlpatterns = [ + path('chat/', views.chat_api, name='ai_chat_api'), +] \ No newline at end of file diff --git a/src/plugins/ai_assistant/views.py b/src/plugins/ai_assistant/views.py new file mode 100644 index 0000000..eafbdf5 --- /dev/null +++ b/src/plugins/ai_assistant/views.py @@ -0,0 +1,82 @@ +import os +import json +from django.http import StreamingHttpResponse +from django.views.decorators.csrf import csrf_exempt +from django.views.decorators.http import require_POST +from openai import OpenAI + +# 初始化客户端 (硬编码 API Key) +API_KEY = "sk-ca7ca2c355ce408a809b77f8f8911700" +BASE_URL = "https://dashscope.aliyuncs.com/compatible-mode/v1" + +client = OpenAI( + api_key=API_KEY, + base_url=BASE_URL, +) + + +def stream_generator(user_input): + """ + 生成器函数,用于流式返回 DeepSeek 的响应 + """ + try: + messages = [ + {"role": "system", "content": "你是一个运行在技术博客上的智能助手,请用简洁、专业的技术语言回答用户的问题。"}, + {"role": "user", "content": user_input} + ] + + completion = client.chat.completions.create( + model="deepseek-v3.2-exp", # 使用指定的模型 + messages=messages, + extra_body={"enable_thinking": True}, # 开启思考模式 + stream=True, + stream_options={"include_usage": True}, + ) + + for chunk in completion: + # 检查是否有内容 + if not chunk.choices: + continue + + delta = chunk.choices[0].delta + + # 处理思考过程 (Reasoning Content) + if hasattr(delta, "reasoning_content") and delta.reasoning_content: + # 发送思考片段,标记为 type: thinking + data = json.dumps({ + "type": "thinking", + "content": delta.reasoning_content + }) + yield f"data: {data}\n\n" + + # 处理正式回复 (Content) + if hasattr(delta, "content") and delta.content: + # 发送回复片段,标记为 type: answer + data = json.dumps({ + "type": "answer", + "content": delta.content + }) + yield f"data: {data}\n\n" + + except Exception as e: + error_msg = json.dumps({"type": "error", "content": str(e)}) + yield f"data: {error_msg}\n\n" + + +@csrf_exempt # 简单起见暂时免除CSRF,或者需要在前端传递 csrftoken +@require_POST +def chat_api(request): + try: + data = json.loads(request.body) + user_input = data.get('message', '') + + if not user_input: + return StreamingHttpResponse("No input provided", status=400) + + # 使用 StreamingHttpResponse 实现流式响应 + response = StreamingHttpResponse(stream_generator(user_input), content_type='text/event-stream') + response['Cache-Control'] = 'no-cache' + return response + + except json.JSONDecodeError: + return StreamingHttpResponse("Invalid JSON", status=400) \ No newline at end of file diff --git a/src/plugins/ai_image/__init__.py b/src/plugins/ai_image/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/plugins/ai_image/urls.py b/src/plugins/ai_image/urls.py new file mode 100644 index 0000000..c6b35e3 --- /dev/null +++ b/src/plugins/ai_image/urls.py @@ -0,0 +1,6 @@ +from django.urls import path +from . import views + +urlpatterns = [ + path('generate/', views.generate_image_api, name='ai_image_generate'), +] \ No newline at end of file diff --git a/src/plugins/ai_image/views.py b/src/plugins/ai_image/views.py new file mode 100644 index 0000000..b2ce8e8 --- /dev/null +++ b/src/plugins/ai_image/views.py @@ -0,0 +1,76 @@ +import os +import json +import time +import requests +from http import HTTPStatus +from urllib.parse import urlparse, unquote +from pathlib import PurePosixPath +from django.conf import settings +from django.http import JsonResponse +from django.views.decorators.csrf import csrf_exempt +from django.contrib.admin.views.decorators import staff_member_required +from dashscope import ImageSynthesis + +# 硬编码 API Key (或者使用 os.getenv) +DASHSCOPE_API_KEY = "sk-ca7ca2c355ce408a809b77f8f8911700" # 请替换为你自己的 Key + + +@csrf_exempt +@staff_member_required # 只有管理员才能调用 +def generate_image_api(request): + if request.method != 'POST': + return JsonResponse({'status': 'error', 'message': '仅支持 POST 请求'}) + + try: + data = json.loads(request.body) + prompt = data.get('prompt', '') + + if not prompt: + return JsonResponse({'status': 'error', 'message': '请输入图片描述'}) + + # 1. 调用阿里云 DashScope + rsp = ImageSynthesis.call( + api_key=DASHSCOPE_API_KEY, + model=ImageSynthesis.Models.wanx_v1, + prompt=prompt, + n=1, + style='', # 默认水彩风格,可根据需要修改或让前端传参 + size='1024*1024' + ) + + if rsp.status_code == HTTPStatus.OK: + # 2. 获取图片 URL + img_url = rsp.output.results[0].url + + # 3. 下载并保存图片到本地 media 目录 + # 确保保存目录存在 + save_dir = os.path.join(settings.MEDIA_ROOT, 'ai_generated') + if not os.path.exists(save_dir): + os.makedirs(save_dir) + + # 生成文件名 (使用时间戳防止重名) + file_name = f"ai_{int(time.time())}.png" + local_path = os.path.join(save_dir, file_name) + + # 下载 + img_data = requests.get(img_url).content + with open(local_path, 'wb+') as f: + f.write(img_data) + + # 4. 返回本地访问 URL + # 假设 MEDIA_URL 是 /media/ + local_url = f"{settings.MEDIA_URL}ai_generated/{file_name}" + + return JsonResponse({ + 'status': 'success', + 'url': local_url, + 'msg': '生成成功!链接已复制' + }) + else: + return JsonResponse({ + 'status': 'error', + 'message': f"API错误: {rsp.message}" + }) + + except Exception as e: + return JsonResponse({'status': 'error', 'message': str(e)}) \ No newline at end of file diff --git a/src/plugins/article_copyright/plugin.py b/src/plugins/article_copyright/plugin.py index 317fed2..5dba3b3 100644 --- a/src/plugins/article_copyright/plugin.py +++ b/src/plugins/article_copyright/plugin.py @@ -22,6 +22,11 @@ class ArticleCopyrightPlugin(BasePlugin): article = kwargs.get('article') if not article: return content + + # 如果是摘要模式(首页),不添加版权声明 + is_summary = kwargs.get('is_summary', False) + if is_summary: + return content copyright_info = f"\n

本文由 {article.author.username} 原创,转载请注明出处。

" return content + copyright_info diff --git a/src/plugins/article_recommendation/__init__.py b/src/plugins/article_recommendation/__init__.py new file mode 100644 index 0000000..951f2ff --- /dev/null +++ b/src/plugins/article_recommendation/__init__.py @@ -0,0 +1 @@ +# 文章推荐插件 diff --git a/src/plugins/article_recommendation/plugin.py b/src/plugins/article_recommendation/plugin.py new file mode 100644 index 0000000..6656a07 --- /dev/null +++ b/src/plugins/article_recommendation/plugin.py @@ -0,0 +1,205 @@ +import logging +from djangoblog.plugin_manage.base_plugin import BasePlugin +from djangoblog.plugin_manage import hooks +from djangoblog.plugin_manage.hook_constants import ARTICLE_DETAIL_LOAD +from blog.models import Article + +logger = logging.getLogger(__name__) + + +class ArticleRecommendationPlugin(BasePlugin): + PLUGIN_NAME = '文章推荐' + PLUGIN_DESCRIPTION = '智能文章推荐系统,支持多位置展示' + PLUGIN_VERSION = '1.0.0' + PLUGIN_AUTHOR = 'liangliangyy' + + # 支持的位置 + SUPPORTED_POSITIONS = ['article_bottom'] + + # 各位置优先级 + POSITION_PRIORITIES = { + 'article_bottom': 80, # 文章底部优先级 + } + + # 插件配置 + CONFIG = { + 'article_bottom_count': 8, # 文章底部推荐数量 + 'sidebar_count': 5, # 侧边栏推荐数量 + 'enable_category_fallback': True, # 启用分类回退 + 'enable_popular_fallback': True, # 启用热门文章回退 + } + + def register_hooks(self): + """注册钩子""" + hooks.register(ARTICLE_DETAIL_LOAD, self.on_article_detail_load) + + def on_article_detail_load(self, article, context, request, *args, **kwargs): + """文章详情页加载时的处理""" + # 可以在这里预加载推荐数据到context中 + recommendations = self.get_recommendations(article) + context['article_recommendations'] = recommendations + + def should_display(self, position, context, **kwargs): + """条件显示逻辑""" + # 只在文章详情页底部显示 + if position == 'article_bottom': + article = kwargs.get('article') or context.get('article') + # 检查是否有文章对象,以及是否不是索引页面 + is_index = context.get('isindex', False) if hasattr(context, 'get') else False + return article is not None and not is_index + + return False + + def render_article_bottom_widget(self, context, **kwargs): + """渲染文章底部推荐""" + article = kwargs.get('article') or context.get('article') + if not article: + return None + + # 使用配置的数量,也可以通过kwargs覆盖 + count = kwargs.get('count', self.CONFIG['article_bottom_count']) + recommendations = self.get_recommendations(article, count=count) + if not recommendations: + return None + + # 将RequestContext转换为普通字典 + context_dict = {} + if hasattr(context, 'flatten'): + context_dict = context.flatten() + elif hasattr(context, 'dicts'): + # 合并所有上下文字典 + for d in context.dicts: + context_dict.update(d) + + template_context = { + 'recommendations': recommendations, + 'article': article, + 'title': '相关推荐', + **context_dict + } + + return self.render_template('bottom_widget.html', template_context) + + def render_sidebar_widget(self, context, **kwargs): + """渲染侧边栏推荐""" + article = context.get('article') + + # 使用配置的数量,也可以通过kwargs覆盖 + count = kwargs.get('count', self.CONFIG['sidebar_count']) + + if article: + # 文章页面,显示相关文章 + recommendations = self.get_recommendations(article, count=count) + title = '相关文章' + else: + # 其他页面,显示热门文章 + recommendations = self.get_popular_articles(count=count) + title = '热门推荐' + + if not recommendations: + return None + + # 将RequestContext转换为普通字典 + context_dict = {} + if hasattr(context, 'flatten'): + context_dict = context.flatten() + elif hasattr(context, 'dicts'): + # 合并所有上下文字典 + for d in context.dicts: + context_dict.update(d) + + template_context = { + 'recommendations': recommendations, + 'title': title, + **context_dict + } + + return self.render_template('sidebar_widget.html', template_context) + + def get_css_files(self): + """返回CSS文件""" + return ['css/recommendation.css'] + + def get_js_files(self): + """返回JS文件""" + return ['js/recommendation.js'] + + def get_recommendations(self, article, count=5): + """获取推荐文章""" + if not article: + return [] + + recommendations = [] + + # 1. 基于标签的推荐 + if article.tags.exists(): + tag_ids = list(article.tags.values_list('id', flat=True)) + tag_based = list(Article.objects.filter( + status='p', + tags__id__in=tag_ids + ).exclude( + id=article.id + ).exclude( + title__isnull=True + ).exclude( + title__exact='' + ).distinct().order_by('-views')[:count]) + recommendations.extend(tag_based) + + # 2. 如果数量不够,基于分类推荐 + if len(recommendations) < count and self.CONFIG['enable_category_fallback']: + needed = count - len(recommendations) + existing_ids = [r.id for r in recommendations] + [article.id] + + category_based = list(Article.objects.filter( + status='p', + category=article.category + ).exclude( + id__in=existing_ids + ).exclude( + title__isnull=True + ).exclude( + title__exact='' + ).order_by('-views')[:needed]) + recommendations.extend(category_based) + + # 3. 如果还是不够,推荐热门文章 + if len(recommendations) < count and self.CONFIG['enable_popular_fallback']: + needed = count - len(recommendations) + existing_ids = [r.id for r in recommendations] + [article.id] + + popular_articles = list(Article.objects.filter( + status='p' + ).exclude( + id__in=existing_ids + ).exclude( + title__isnull=True + ).exclude( + title__exact='' + ).order_by('-views')[:needed]) + recommendations.extend(popular_articles) + + # 过滤掉无效的推荐 + valid_recommendations = [] + for rec in recommendations: + if rec.title and len(rec.title.strip()) > 0: + valid_recommendations.append(rec) + else: + logger.warning(f"过滤掉空标题文章: ID={rec.id}, 标题='{rec.title}'") + + # 调试:记录推荐结果 + logger.info(f"原始推荐数量: {len(recommendations)}, 有效推荐数量: {len(valid_recommendations)}") + for i, rec in enumerate(valid_recommendations): + logger.info(f"推荐 {i+1}: ID={rec.id}, 标题='{rec.title}', 长度={len(rec.title)}") + + return valid_recommendations[:count] + + def get_popular_articles(self, count=3): + """获取热门文章""" + return list(Article.objects.filter( + status='p' + ).order_by('-views')[:count]) + + +# 实例化插件 +plugin = ArticleRecommendationPlugin() diff --git a/src/plugins/article_recommendation/static/article_recommendation/css/recommendation.css b/src/plugins/article_recommendation/static/article_recommendation/css/recommendation.css new file mode 100644 index 0000000..b223f41 --- /dev/null +++ b/src/plugins/article_recommendation/static/article_recommendation/css/recommendation.css @@ -0,0 +1,166 @@ +/* 文章推荐插件样式 - 与网站风格保持一致 */ + +/* 文章底部推荐样式 */ +.article-recommendations { + margin: 30px 0; + padding: 20px; + background: #fff; + border: 1px solid #e1e1e1; + border-radius: 3px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +} + +.recommendations-title { + margin: 0 0 15px 0; + font-size: 18px; + color: #444; + font-weight: bold; + padding-bottom: 8px; + border-bottom: 2px solid #21759b; + display: inline-block; +} + +.recommendations-icon { + margin-right: 5px; + font-size: 16px; +} + +.recommendations-grid { + display: grid; + gap: 15px; + grid-template-columns: 1fr; + margin-top: 15px; +} + +.recommendation-card { + background: #fff; + border: 1px solid #e1e1e1; + border-radius: 3px; + transition: all 0.2s ease; + overflow: hidden; +} + +.recommendation-card:hover { + border-color: #21759b; + box-shadow: 0 2px 5px rgba(33, 117, 155, 0.1); +} + +.recommendation-link { + display: block; + padding: 15px; + text-decoration: none; + color: inherit; +} + +.recommendation-title { + margin: 0 0 8px 0; + font-size: 15px; + font-weight: normal; + color: #444; + line-height: 1.4; + transition: color 0.2s ease; +} + +.recommendation-card:hover .recommendation-title { + color: #21759b; +} + +.recommendation-meta { + display: flex; + justify-content: space-between; + align-items: center; + font-size: 12px; + color: #757575; +} + +.recommendation-category { + background: #ebebeb; + color: #5e5e5e; + padding: 2px 6px; + border-radius: 2px; + font-size: 11px; + font-weight: normal; +} + +.recommendation-date { + font-weight: normal; + color: #757575; +} + +/* 侧边栏推荐样式 */ +.widget_recommendations { + margin-bottom: 20px; +} + +.widget_recommendations .widget-title { + font-size: 16px; + font-weight: bold; + margin-bottom: 15px; + color: #333; + border-bottom: 2px solid #007cba; + padding-bottom: 5px; +} + +.recommendations-list { + list-style: none; + padding: 0; + margin: 0; +} + +.recommendations-list .recommendation-item { + padding: 8px 0; + border-bottom: 1px solid #eee; + background: none; + border: none; + border-radius: 0; +} + +.recommendations-list .recommendation-item:last-child { + border-bottom: none; +} + +.recommendations-list .recommendation-item a { + color: #333; + text-decoration: none; + font-size: 14px; + line-height: 1.4; + display: block; + margin-bottom: 4px; + transition: color 0.3s ease; +} + +.recommendations-list .recommendation-item a:hover { + color: #007cba; +} + +.recommendations-list .recommendation-meta { + font-size: 11px; + color: #999; + margin: 0; +} + +.recommendations-list .recommendation-meta span { + margin-right: 10px; +} + +/* 响应式设计 - 分栏显示 */ +@media (min-width: 768px) { + .recommendations-grid { + grid-template-columns: repeat(2, 1fr); + gap: 15px; + } +} + +@media (min-width: 1024px) { + .recommendations-grid { + grid-template-columns: repeat(3, 1fr); + gap: 15px; + } +} + +@media (min-width: 1200px) { + .recommendations-grid { + grid-template-columns: repeat(4, 1fr); + gap: 15px; + } +} diff --git a/src/plugins/article_recommendation/static/article_recommendation/js/recommendation.js b/src/plugins/article_recommendation/static/article_recommendation/js/recommendation.js new file mode 100644 index 0000000..eb19211 --- /dev/null +++ b/src/plugins/article_recommendation/static/article_recommendation/js/recommendation.js @@ -0,0 +1,93 @@ +/** + * 文章推荐插件JavaScript + */ + +(function() { + 'use strict'; + + // 等待DOM加载完成 + document.addEventListener('DOMContentLoaded', function() { + initRecommendations(); + }); + + function initRecommendations() { + // 添加点击统计 + trackRecommendationClicks(); + + // 懒加载优化(如果需要) + lazyLoadRecommendations(); + } + + function trackRecommendationClicks() { + const recommendationLinks = document.querySelectorAll('.recommendation-item a'); + + recommendationLinks.forEach(function(link) { + link.addEventListener('click', function(e) { + // 可以在这里添加点击统计逻辑 + const articleTitle = this.textContent.trim(); + const articleUrl = this.href; + + // 发送统计数据到后端(可选) + if (typeof gtag !== 'undefined') { + gtag('event', 'click', { + 'event_category': 'recommendation', + 'event_label': articleTitle, + 'value': 1 + }); + } + + console.log('Recommendation clicked:', articleTitle, articleUrl); + }); + }); + } + + function lazyLoadRecommendations() { + // 如果推荐内容很多,可以实现懒加载 + const recommendationContainer = document.querySelector('.article-recommendations'); + + if (!recommendationContainer) { + return; + } + + // 检查是否在视窗中 + const observer = new IntersectionObserver(function(entries) { + entries.forEach(function(entry) { + if (entry.isIntersecting) { + entry.target.classList.add('loaded'); + observer.unobserve(entry.target); + } + }); + }, { + threshold: 0.1 + }); + + const recommendationItems = document.querySelectorAll('.recommendation-item'); + recommendationItems.forEach(function(item) { + observer.observe(item); + }); + } + + // 添加一些动画效果 + function addAnimations() { + const recommendationItems = document.querySelectorAll('.recommendation-item'); + + recommendationItems.forEach(function(item, index) { + item.style.opacity = '0'; + item.style.transform = 'translateY(20px)'; + item.style.transition = 'opacity 0.5s ease, transform 0.5s ease'; + + setTimeout(function() { + item.style.opacity = '1'; + item.style.transform = 'translateY(0)'; + }, index * 100); + }); + } + + // 如果需要,可以在这里添加更多功能 + window.ArticleRecommendation = { + init: initRecommendations, + track: trackRecommendationClicks, + animate: addAnimations + }; + +})(); diff --git a/src/plugins/image_lazy_loading/__init__.py b/src/plugins/image_lazy_loading/__init__.py new file mode 100644 index 0000000..2d27de0 --- /dev/null +++ b/src/plugins/image_lazy_loading/__init__.py @@ -0,0 +1 @@ +# Image Lazy Loading Plugin diff --git a/src/plugins/image_lazy_loading/plugin.py b/src/plugins/image_lazy_loading/plugin.py new file mode 100644 index 0000000..b4b9e0a --- /dev/null +++ b/src/plugins/image_lazy_loading/plugin.py @@ -0,0 +1,182 @@ +import re +import hashlib +from urllib.parse import urlparse +from djangoblog.plugin_manage.base_plugin import BasePlugin +from djangoblog.plugin_manage import hooks +from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME + + +class ImageOptimizationPlugin(BasePlugin): + PLUGIN_NAME = '图片性能优化插件' + PLUGIN_DESCRIPTION = '自动为文章中的图片添加懒加载、异步解码等性能优化属性,显著提升页面加载速度。' + PLUGIN_VERSION = '1.0.0' + PLUGIN_AUTHOR = 'liangliangyy' + + def __init__(self): + # 插件配置 + self.config = { + 'enable_lazy_loading': True, # 启用懒加载 + 'enable_async_decoding': True, # 启用异步解码 + 'add_loading_placeholder': True, # 添加加载占位符 + 'optimize_external_images': True, # 优化外部图片 + 'add_responsive_attributes': True, # 添加响应式属性 + 'skip_first_image': True, # 跳过第一张图片(LCP优化) + } + super().__init__() + + def register_hooks(self): + hooks.register(ARTICLE_CONTENT_HOOK_NAME, self.optimize_images) + + def optimize_images(self, content, *args, **kwargs): + """ + 优化文章中的图片标签 + """ + if not content: + return content + + # 正则表达式匹配 img 标签 + img_pattern = re.compile( + r']*?)(?:\s*/)?>', + re.IGNORECASE | re.DOTALL + ) + + image_count = 0 + + def replace_img_tag(match): + nonlocal image_count + image_count += 1 + + # 获取原始属性 + original_attrs = match.group(1) + + # 解析现有属性 + attrs = self._parse_img_attributes(original_attrs) + + # 应用优化 + optimized_attrs = self._apply_optimizations(attrs, image_count) + + # 重构 img 标签 + return self._build_img_tag(optimized_attrs) + + # 替换所有 img 标签 + optimized_content = img_pattern.sub(replace_img_tag, content) + + return optimized_content + + def _parse_img_attributes(self, attr_string): + """ + 解析 img 标签的属性 + """ + attrs = {} + + # 正则表达式匹配属性 + attr_pattern = re.compile(r'(\w+)=(["\'])(.*?)\2') + + for match in attr_pattern.finditer(attr_string): + attr_name = match.group(1).lower() + attr_value = match.group(3) + attrs[attr_name] = attr_value + + return attrs + + def _apply_optimizations(self, attrs, image_index): + """ + 应用各种图片优化 + """ + # 1. 懒加载优化(跳过第一张图片以优化LCP) + if self.config['enable_lazy_loading']: + if not (self.config['skip_first_image'] and image_index == 1): + if 'loading' not in attrs: + attrs['loading'] = 'lazy' + + # 2. 异步解码 + if self.config['enable_async_decoding']: + if 'decoding' not in attrs: + attrs['decoding'] = 'async' + + # 3. 添加样式优化 + current_style = attrs.get('style', '') + + # 确保图片不会超出容器 + if 'max-width' not in current_style: + if current_style and not current_style.endswith(';'): + current_style += ';' + current_style += 'max-width:100%;height:auto;' + attrs['style'] = current_style + + # 4. 添加 alt 属性(SEO和可访问性) + if 'alt' not in attrs: + # 尝试从图片URL生成有意义的alt文本 + src = attrs.get('src', '') + if src: + # 从文件名生成alt文本 + filename = src.split('/')[-1].split('.')[0] + # 移除常见的无意义字符 + clean_name = re.sub(r'[0-9a-f]{8,}', '', filename) # 移除长hash + clean_name = re.sub(r'[_-]+', ' ', clean_name).strip() + attrs['alt'] = clean_name if clean_name else '文章图片' + else: + attrs['alt'] = '文章图片' + + # 5. 外部图片优化 + if self.config['optimize_external_images'] and 'src' in attrs: + src = attrs['src'] + parsed_url = urlparse(src) + + # 如果是外部图片,添加 referrerpolicy + if parsed_url.netloc and parsed_url.netloc != self._get_current_domain(): + attrs['referrerpolicy'] = 'no-referrer-when-downgrade' + # 为外部图片添加crossorigin属性以支持性能监控 + if 'crossorigin' not in attrs: + attrs['crossorigin'] = 'anonymous' + + # 6. 响应式图片属性(如果配置启用) + if self.config['add_responsive_attributes']: + # 添加 sizes 属性(如果没有的话) + if 'sizes' not in attrs and 'srcset' not in attrs: + attrs['sizes'] = '(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 33vw' + + # 7. 添加图片唯一标识符用于性能追踪 + if 'data-img-id' not in attrs and 'src' in attrs: + img_hash = hashlib.md5(attrs['src'].encode()).hexdigest()[:8] + attrs['data-img-id'] = f'img-{img_hash}' + + # 8. 为第一张图片添加高优先级提示(LCP优化) + if image_index == 1 and self.config['skip_first_image']: + attrs['fetchpriority'] = 'high' + # 移除懒加载以确保快速加载 + if 'loading' in attrs: + del attrs['loading'] + + return attrs + + def _build_img_tag(self, attrs): + """ + 重新构建 img 标签 + """ + attr_strings = [] + + # 确保 src 属性在最前面 + if 'src' in attrs: + attr_strings.append(f'src="{attrs["src"]}"') + + # 添加其他属性 + for key, value in attrs.items(): + if key != 'src': # src 已经添加过了 + attr_strings.append(f'{key}="{value}"') + + return f'' + + def _get_current_domain(self): + """ + 获取当前网站域名 + """ + try: + from djangoblog.utils import get_current_site + return get_current_site().domain + except: + return '' + + +# 实例化插件 +plugin = ImageOptimizationPlugin() diff --git a/src/plugins/reading_time/plugin.py b/src/plugins/reading_time/plugin.py index 35f9db1..4b929d8 100644 --- a/src/plugins/reading_time/plugin.py +++ b/src/plugins/reading_time/plugin.py @@ -17,7 +17,15 @@ class ReadingTimePlugin(BasePlugin): def add_reading_time(self, content, *args, **kwargs): """ 计算阅读时间并添加到内容开头。 + 只在文章详情页显示,首页(文章列表页)不显示。 """ + # 检查是否为摘要模式(首页/文章列表页) + # 通过kwargs中的is_summary参数判断 + is_summary = kwargs.get('is_summary', False) + if is_summary: + # 如果是摘要模式(首页),直接返回原内容,不添加阅读时间 + return content + # 移除HTML标签和空白字符,以获得纯文本 clean_content = re.sub(r'<[^>]*>', '', content) clean_content = clean_content.strip() diff --git a/src/plugins/seo_optimizer/plugin.py b/src/plugins/seo_optimizer/plugin.py index b5b19a3..de12c15 100644 --- a/src/plugins/seo_optimizer/plugin.py +++ b/src/plugins/seo_optimizer/plugin.py @@ -97,6 +97,8 @@ class SeoOptimizerPlugin(BasePlugin): structured_data = { "@context": "https://schema.org", "@type": "WebSite", + "name": blog_setting.site_name, + "description": blog_setting.site_description, "url": request.build_absolute_uri('/'), "potentialAction": { "@type": "SearchAction", @@ -131,12 +133,15 @@ class SeoOptimizerPlugin(BasePlugin): json_ld_script = f'' - return f""" + seo_html = f""" {seo_data.get("title", "")} {seo_data.get("meta_tags", "")} {json_ld_script} """ + + # 将SEO内容追加到现有的metas内容上 + return metas + seo_html plugin = SeoOptimizerPlugin() diff --git a/src/requirements.txt b/src/requirements.txt index 9dc5c93..b19824d 100644 Binary files a/src/requirements.txt and b/src/requirements.txt differ diff --git a/src/servermanager/MemcacheStorage.py b/src/servermanager/MemcacheStorage.py index 5adc316..38a7990 100644 --- a/src/servermanager/MemcacheStorage.py +++ b/src/servermanager/MemcacheStorage.py @@ -1,114 +1,32 @@ -from werobot.session import SessionStorage # WeRoBot 框架的会话存储基类 -from werobot.utils import json_loads, json_dumps # WeRoBot 提供的 JSON 序列化/反序列化工具 +from werobot.session import SessionStorage +from werobot.utils import json_loads, json_dumps -from djangoblog.utils import cache # 项目中封装的缓存对象(如 Redis 或 Memcached) +from djangoblog.utils import cache class MemcacheStorage(SessionStorage): - # wjl - """ - 自定义的会话存储类,基于缓存系统(如 Redis/Memcached)实现 WeRoBot 微信机器人的会话管理。 - 用于在微信消息处理过程中持久化用户会话数据(如上下文、状态等)。 - - 通过将用户 ID 作为键,会话数据作为值,存储在缓存中,实现跨请求的会话跟踪。 - """ - def __init__(self, prefix='ws_'): - # wjl - """ - 初始化 MemcacheStorage 实例。 - - 参数: - prefix (str): 缓存键的前缀,默认为 'ws_'(weobot session 的缩写),用于避免键名冲突。 - - 功能: - - 设置键名前缀 - - 绑定项目全局的缓存实例(cache) - """ - self.prefix = prefix #wjl# 设置键名前缀 - self.cache = cache #wjl# 使用项目中已配置的缓存后端(如 Redis) + self.prefix = prefix + self.cache = cache @property def is_available(self): - """ - 检查当前会话存储是否可用(即缓存系统是否正常工作)。 - - 实现方式: - 1. 尝试写入一个测试键值对('checkavaliable': '1') - 2. 立即读取该键的值 - 3. 如果读取的值与写入的值一致,则认为存储可用 - - 返回: - bool: True 表示缓存可用;False 表示不可用(如缓存服务宕机) - """ value = "1" - self.set('checkavaliable', value=value) #wjl# 写入测试数据 - return value == self.get('checkavaliable') #wjl# 验证能否正确读取 + self.set('checkavaliable', value=value) + return value == self.get('checkavaliable') def key_name(self, s): - # wjl - """ - 为给定的会话 ID 生成带前缀的完整缓存键名。 - - 参数: - s (str): 原始会话 ID(通常是微信用户的 OpenID) - - 返回: - str: 格式为 '{prefix}{s}' 的完整键名,用于避免命名冲突。 - - 示例: - key_name('abc123') -> 'ws_abc123' - """ return '{prefix}{s}'.format(prefix=self.prefix, s=s) def get(self, id): - # wjl - """ - 根据会话 ID 从缓存中获取会话数据。 - - 参数: - id (str): 会话 ID(如用户 OpenID) - - 返回: - dict: 会话数据的字典;如果缓存中不存在,则返回空字典 {} - - 流程: - 1. 使用 key_name() 生成带前缀的键 - 2. 从缓存中获取 JSON 字符串 - 3. 使用 json_loads 将 JSON 字符串反序列化为 Python 字典 - """ id = self.key_name(id) - session_json = self.cache.get(id) or '{}' #wjl # 如果未找到,返回空 JSON 字符串 + session_json = self.cache.get(id) or '{}' return json_loads(session_json) def set(self, id, value): - # wjl - """ - 将会话数据存储到缓存中。 - - 参数: - id (str): 会话 ID - value (dict): 要存储的会话数据(Python 字典) - - 流程: - 1. 使用 key_name() 生成带前缀的键 - 2. 将字典序列化为 JSON 字符串 - 3. 存入缓存(使用默认过期时间) - """ id = self.key_name(id) - self.cache.set(id, json_dumps(value)) #wjl# 序列化并存储 + self.cache.set(id, json_dumps(value)) def delete(self, id): - # wjl - """ - 根据会话 ID 从缓存中删除会话数据。 - - 参数: - id (str): 会话 ID - - 流程: - 1. 使用 key_name() 生成带前缀的键 - 2. 调用缓存的 delete 方法删除该键 - """ id = self.key_name(id) - self.cache.delete(id) \ No newline at end of file + self.cache.delete(id) diff --git a/src/servermanager/admin.py b/src/servermanager/admin.py index 19a4a2a..f26f4f6 100644 --- a/src/servermanager/admin.py +++ b/src/servermanager/admin.py @@ -1,50 +1,19 @@ from django.contrib import admin - -#wjl# Register your models here. -#wjl# 本文件用于将数据模型注册到 Django 管理后台(Admin),并自定义其展示和操作方式。 -#wjl# 当前未立即注册模型,需在下方通过 admin.site.register() 显式注册。 +# Register your models here. class CommandsAdmin(admin.ModelAdmin): - # wjl - """ - Django Admin 配置类,用于自定义 'commands' 模型在管理后台中的列表展示。 - 控制在模型列表页面显示哪些字段。 - """ - - #wjl# 定义在管理后台的模型列表页面中要显示的字段列 - #wjl# 用户将看到三列:'title'(命令标题)、'command'(命令内容)、'describe'(描述) list_display = ('title', 'command', 'describe') class EmailSendLogAdmin(admin.ModelAdmin): - # wjl - """ - Django Admin 配置类,用于自定义 'EmailSendLog' 模型在管理后台中的行为。 - 该日志模型主要用于查看,禁止添加新记录,并将所有字段设为只读。 - """ - - #wjl# 定义在管理后台的模型列表页面中要显示的字段列 - #wjl# 显示:邮件标题、收件人、发送结果、创建时间 list_display = ('title', 'emailto', 'send_result', 'creation_time') - - #wjl # 定义在编辑或查看单条记录时,哪些字段为只读(不可编辑) - #wjl# 所有字段均设为只读,防止日志被意外修改 readonly_fields = ( 'title', 'emailto', 'send_result', 'creation_time', - 'content' - ) + 'content') - #wjl# 重写权限方法,禁止用户在管理后台添加新的 EmailSendLog 记录 - #wjl# 因为邮件日志应由程序自动创建,不允许手动添加 - #wjl# - #wjl# 参数: - #wjl # request: 当前的 HTTP 请求对象 - #wjl# - #wjl# 返回: - #wjl# bool: 始终返回 False,表示禁止添加操作 def has_add_permission(self, request): - return False \ No newline at end of file + return False diff --git a/src/servermanager/api/blogapi.py b/src/servermanager/api/blogapi.py index cd32064..8a4d6ac 100644 --- a/src/servermanager/api/blogapi.py +++ b/src/servermanager/api/blogapi.py @@ -4,83 +4,24 @@ from blog.models import Article, Category class BlogApi: - #wjl - """ - 博客 API 类,封装了与博客相关的数据查询功能,包括全文搜索、分类获取、分类文章获取和最新文章获取。 - """ - def __init__(self): - #wjl - """ - 初始化 BlogApi 实例。 - 创建一个 SearchQuerySet 对象用于全文搜索,并执行一个空查询以准备搜索环境。 - 同时设置默认的最大返回记录数。 - """ - self.searchqueryset = SearchQuerySet() # Haystack 的搜索查询集对象 - self.searchqueryset.auto_query('') # 执行空查询,初始化搜索状态 - self.__max_takecount__ = 8 # 设置最大返回文章数量为 8 篇 + self.searchqueryset = SearchQuerySet() + self.searchqueryset.auto_query('') + self.__max_takecount__ = 8 def search_articles(self, query): - #wjl - """ - 根据用户输入的关键词查询文章。 - - 参数: - query (str): 用户输入的搜索关键词。 - - 返回: - QuerySet: 包含匹配文章的查询集(最多返回 __max_takecount__ 篇文章), - 并已加载文章的完整模型实例(通过 load_all)。 - - 说明: - 使用 Haystack 的 auto_query 进行全文搜索,并加载所有关联的文章对象。 - """ - sqs = self.searchqueryset.auto_query(query) # 根据关键词执行全文搜索 - sqs = sqs.load_all() # 加载搜索结果对应的真实模型实例(Article 对象) - return sqs[:self.__max_takecount__] # 返回最多 __max_takecount__ 条搜索结果 + sqs = self.searchqueryset.auto_query(query) + sqs = sqs.load_all() + return sqs[:self.__max_takecount__] def get_category_lists(self): - #wjl - """ - 获取所有文章分类的列表。 - - 返回: - QuerySet: 包含数据库中所有 Category 对象的查询集。 - - 说明: - 此方法返回所有已创建的分类,可用于展示分类导航等。 - """ return Category.objects.all() def get_category_articles(self, categoryname): - #wjl - """ - 根据分类名称获取该分类下的文章列表。 - - 参数: - categoryname (str): 分类的名称。 - - 返回: - QuerySet 或 None: 如果存在该分类的文章,则返回最多 __max_takecount__ 篇文章的查询集; - 如果该分类下没有文章,则返回 None。 - - 说明: - 通过文章模型的外键关系(category__name)进行过滤,获取指定分类的文章。 - """ - articles = Article.objects.filter(category__name=categoryname) # 查询指定分类的文章 + articles = Article.objects.filter(category__name=categoryname) if articles: - return articles[:self.__max_takecount__] # 存在文章则返回最多 __max_takecount__ 篇 - return None # 无文章则返回 None + return articles[:self.__max_takecount__] + return None def get_recent_articles(self): - #wjl - """ - 获取最新的文章列表(按模型默认排序,通常是按创建时间倒序)。 - - 返回: - QuerySet: 包含最新文章的查询集(最多返回 __max_takecount__ 篇文章)。 - - 说明: - 此方法返回数据库中最新的若干篇文章,可用于展示“最新文章”或“热门文章”。 - """ - return Article.objects.all()[:self.__max_takecount__] \ No newline at end of file + return Article.objects.all()[:self.__max_takecount__] diff --git a/src/servermanager/api/commonapi.py b/src/servermanager/api/commonapi.py index 9efb09f..83ad9ff 100644 --- a/src/servermanager/api/commonapi.py +++ b/src/servermanager/api/commonapi.py @@ -5,137 +5,60 @@ import openai from servermanager.models import commands -#wjl# 获取当前模块的 logger,用于记录日志 logger = logging.getLogger(__name__) -#wjl# 从环境变量中读取 OpenAI API 密钥 openai.api_key = os.environ.get('OPENAI_API_KEY') - -#wjl# 如果设置了 HTTP 代理,则配置 openai 库使用该代理 if os.environ.get('HTTP_PROXY'): openai.proxy = os.environ.get('HTTP_PROXY') class ChatGPT: - #wjl - """ - 封装与 OpenAI 的 ChatGPT 模型进行交互的功能。 - 提供静态方法 `chat` 用于发送用户提示并获取模型回复。 - """ @staticmethod def chat(prompt): - # wjl - """ - 调用 OpenAI 的 GPT-3.5 Turbo 模型生成回复。 - - 参数: - prompt (str): 用户输入的提示文本(问题或请求)。 - - 返回: - str: 模型生成的回复内容;如果调用失败,则返回错误提示。 - - 流程: - 1. 使用 openai.ChatCompletion.create 发起请求,指定模型为 "gpt-3.5-turbo"。 - 2. 将用户提示作为 role="user" 的消息发送。 - 3. 提取并返回模型返回的第一条消息内容。 - 4. 如果发生异常(如网络错误、认证失败等),记录错误日志并返回友好提示。 - """ try: - completion = openai.ChatCompletion.create( - model="gpt-3.5-turbo", #wjl# 使用 GPT-3.5 Turbo 模型 - messages=[{"role": "user", "content": prompt}] #wjl# 构造对话消息 - ) - return completion.choices[0].message.content #wjl# 返回模型生成的文本 + completion = openai.ChatCompletion.create(model="gpt-3.5-turbo", + messages=[{"role": "user", "content": prompt}]) + return completion.choices[0].message.content except Exception as e: - logger.error(e) #wjl# 记录异常信息到日志 - return "服务器出错了" #wjl # 返回用户友好的错误提示 + logger.error(e) + return "服务器出错了" class CommandHandler: - # wjl - """ - 命令处理器类,用于管理、查找和执行预定义的系统命令。 - 从数据库加载命令列表,支持通过名称查找并执行命令,以及获取帮助信息。 - """ - def __init__(self): - # wjl - """ - 初始化 CommandHandler 实例。 - 从数据库中加载所有预定义的命令对象,存储在实例变量 self.commands 中。 - """ - self.commands = commands.objects.all() #wjl# 查询数据库中所有命令记录 + self.commands = commands.objects.all() def run(self, title): - # wjl """ - 根据命令标题查找并执行对应的系统命令。 - - 参数: - title (str): 用户输入的命令标题。 - - 返回: - str: 命令执行后的输出结果;如果未找到命令,则返回帮助提示。 - - 流程: - 1. 在 self.commands 列表中查找 title(忽略大小写)匹配的命令。 - 2. 如果找到匹配的命令,调用私有方法 __run_command__ 执行其对应的操作。 - 3. 如果未找到,则返回提示信息,引导用户输入 'hepme' 获取帮助。 + 运行命令 + :param title: 命令 + :return: 返回命令执行结果 """ cmd = list( filter( - lambda x: x.title.upper() == title.upper(), #wjl# 忽略大小写比较 + lambda x: x.title.upper() == title.upper(), self.commands)) if cmd: - return self.__run_command__(cmd[0].command) #wjl# 执行找到的命令 + return self.__run_command__(cmd[0].command) else: return "未找到相关命令,请输入hepme获得帮助。" def __run_command__(self, cmd): - # wjl - """ - 执行给定的系统命令(shell 命令)。 - - 参数: - cmd (str): 要执行的系统命令字符串。 - - 返回: - str: 命令执行的标准输出;如果执行出错,则返回错误提示。 - - 说明: - 使用 os.popen 打开一个管道来执行命令,并读取其输出。 - 捕获所有异常(包括执行失败、权限问题等),防止程序崩溃。 - """ try: - res = os.popen(cmd).read() #wjl# 执行命令并读取输出 + res = os.popen(cmd).read() return res except BaseException: return '命令执行出错!' def get_help(self): - # wjl - """ - 生成帮助信息,列出所有可用的命令及其描述。 - - 返回: - str: 格式化的帮助文本,每行包含一个命令的标题和描述,用冒号分隔。 - - 说明: - 遍历 self.commands 中的所有命令,拼接成一个字符串,用于向用户展示可用命令。 - """ rsp = '' for cmd in self.commands: - rsp += '{c}:{d}\n'.format(c=cmd.title, d=cmd.describe) #wjl# 拼接命令帮助信息 + rsp += '{c}:{d}\n'.format(c=cmd.title, d=cmd.describe) return rsp if __name__ == '__main__': - # wjl - """ - 主程序入口,用于测试 ChatGPT 类的功能。 - 创建 ChatGPT 实例,发送一个测试提示,并打印模型回复。 - """ chatbot = ChatGPT() prompt = "写一篇1000字关于AI的论文" - print(chatbot.chat(prompt)) \ No newline at end of file + print(chatbot.chat(prompt)) diff --git a/src/servermanager/apps.py b/src/servermanager/apps.py index 1e35ed7..03cc38d 100644 --- a/src/servermanager/apps.py +++ b/src/servermanager/apps.py @@ -1,23 +1,5 @@ -#wjl# 导入 AppConfig,它是 Django 中用于配置应用程序的基类 from django.apps import AppConfig -#wjl -class ServermanagerConfig(AppConfig): - """ - 这是 'servermanager' 应用的配置类。 - 它继承自 Django 的 AppConfig,用于定义该应用在 Django 项目中的元数据和初始化行为。 - - 当 Django 启动时,会扫描 INSTALLED_APPS 中的应用,并加载其对应的 AppConfig。 - 此类的作用是告诉 Django 这个应用的基本信息,例如它的 Python 路径(name)。 - """ - #wjl# 指定该应用的完整 Python 导入路径 - #wjl# Django 使用此属性来唯一标识和导入 'servermanager' 应用 - #wjl# 通常与应用所在文件夹的名称一致 +class ServermanagerConfig(AppConfig): name = 'servermanager' - - #wjl# 注意: - #wjl# - 该类目前只设置了最基本的 name 属性。 - #wjl# - 如果需要在应用启动时执行某些初始化代码,可以重写 ready() 方法。 - #wjl# 例如:注册信号处理器、启动后台任务等。 - #wjl# - 由于当前没有复杂的初始化需求,因此未定义其他方法或属性。 diff --git a/src/servermanager/migrations/0001_initial.py b/src/servermanager/migrations/0001_initial.py index 3c7398f..bbdbf77 100644 --- a/src/servermanager/migrations/0001_initial.py +++ b/src/servermanager/migrations/0001_initial.py @@ -1,70 +1,45 @@ -#wjl +# Generated by Django 4.1.7 on 2023-03-02 07:14 + from django.db import migrations, models class Migration(migrations.Migration): - # wjl - """ - Django 迁移类,定义了数据库模式的变更。 - 此迁移是应用的初始迁移(initial=True),用于创建两个新的数据模型表: - - commands:存储可执行命令的信息 - - EmailSendLog:记录邮件发送的日志信息 - """ - #wjl # 标记此迁移为“初始迁移”,表示这是应用的第一个迁移文件 initial = True - #wjl# 定义此迁移所依赖的其他迁移 - #wjl # 当前为空列表,表示该迁移不依赖于任何其他迁移(即它是最初的) dependencies = [ ] - #wjl# 定义此迁移要执行的数据库操作列表 operations = [ - # 第一个操作:创建名为 'commands' 的数据模型表 migrations.CreateModel( - name='commands', # 数据库表对应的模型名称 - fields=[ # 定义该模型包含的字段(即数据库表的列) - #wjl# 主键 ID 字段:自增的 BigAutoField + name='commands', + fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - #wjl# 命令标题字段:最大长度 300 的字符串 ('title', models.CharField(max_length=300, verbose_name='命令标题')), - #wjl# 实际执行的命令字段:最大长度 2000 的字符串 ('command', models.CharField(max_length=2000, verbose_name='命令')), - #wjl# 命令描述字段:最大长度 300 的字符串 ('describe', models.CharField(max_length=300, verbose_name='命令描述')), - #wjl# 创建时间字段:自动在对象创建时设置当前时间 ('created_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), - #wjl# 最后修改时间字段:每次对象保存时自动更新为当前时间 ('last_mod_time', models.DateTimeField(auto_now=True, verbose_name='修改时间')), ], - options={ #wjl# 模型的元选项配置 - 'verbose_name': '命令', #wjl# 单数形式的可读名称 - 'verbose_name_plural': '命令', #wjl# 复数形式的可读名称(中文通常与单数相同) + options={ + 'verbose_name': '命令', + 'verbose_name_plural': '命令', }, ), - - #wjl# 第二个操作:创建名为 'EmailSendLog' 的数据模型表 migrations.CreateModel( - name='EmailSendLog', #wjl# 数据库表对应的模型名称 - fields=[ #wjl# 定义该模型包含的字段(即数据库表的列) - #wjl# 主键 ID 字段:自增的 BigAutoField + name='EmailSendLog', + fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - #wjl# 收件人邮箱字段:最大长度 300 的字符串 ('emailto', models.CharField(max_length=300, verbose_name='收件人')), - #wjl# 邮件标题字段:最大长度 2000 的字符串 ('title', models.CharField(max_length=2000, verbose_name='邮件标题')), - #wjl# 邮件内容字段:长文本字段 ('content', models.TextField(verbose_name='邮件内容')), - #wjl# 发送结果字段:布尔值,默认为 False(表示失败) ('send_result', models.BooleanField(default=False, verbose_name='结果')), - #wjl# 创建时间字段:自动在记录创建时设置当前时间 ('created_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), ], - options={ # 模型的元选项配置 - 'verbose_name': '邮件发送log', #wjl# 单数形式的可读名称 - 'verbose_name_plural': '邮件发送log', #wjl# 复数形式的可读名称 - 'ordering': ['-created_time'], #wjl# 默认排序规则:按创建时间倒序排列(最新的在前) + options={ + 'verbose_name': '邮件发送log', + 'verbose_name_plural': '邮件发送log', + 'ordering': ['-created_time'], }, ), - ] \ No newline at end of file + ] diff --git a/src/servermanager/migrations/0002_alter_emailsendlog_options_and_more.py b/src/servermanager/migrations/0002_alter_emailsendlog_options_and_more.py index da9510e..4858857 100644 --- a/src/servermanager/migrations/0002_alter_emailsendlog_options_and_more.py +++ b/src/servermanager/migrations/0002_alter_emailsendlog_options_and_more.py @@ -1,52 +1,32 @@ +# Generated by Django 4.2.5 on 2023-09-06 13:19 + from django.db import migrations class Migration(migrations.Migration): - # wjl - """ - Django 迁移类,定义了对数据库模式的变更。 - 此迁移文件基于 'servermanager' 应用的初始迁移(0001_initial)进行修改, - 主要目的是统一字段命名规范(如将 created_time 改为 creation_time)并更新模型排序规则。 - """ - #wjl# 定义此迁移所依赖的其他迁移 - #wjl# 依赖于 servermanager 应用的 0001_initial 迁移,确保基础表已存在 dependencies = [ - ('servermanager', '0001_initial'), #wjl# 格式:(应用名, 迁移文件名) + ('servermanager', '0001_initial'), ] - #wjl# 定义此迁移要执行的数据库操作列表 operations = [ - #wjl# 操作 1:修改模型 EmailSendLog 的元选项(Meta options) migrations.AlterModelOptions( - name='emailsendlog', #wjl# 要修改的模型名称 - options={ - #wjl# 更新排序字段为 'creation_time'(倒序),确保最新记录排在前面 - 'ordering': ['-creation_time'], - #wjl# 保持模型的可读名称不变 - 'verbose_name': '邮件发送log', - 'verbose_name_plural': '邮件发送log' - }, + name='emailsendlog', + options={'ordering': ['-creation_time'], 'verbose_name': '邮件发送log', 'verbose_name_plural': '邮件发送log'}, ), - - #wjl # 操作 2:将 commands 模型中的字段 created_time 重命名为 creation_time migrations.RenameField( - model_name='commands', #wjl# 要操作的模型名称 - old_name='created_time', #wjl # 原字段名 - new_name='creation_time', #wjl# 新字段名 + model_name='commands', + old_name='created_time', + new_name='creation_time', ), - - #wjl# 操作 3:将 commands 模型中的字段 last_mod_time 重命名为 last_modify_time migrations.RenameField( - model_name='commands', #wjl# 要操作的模型名称 - old_name='last_mod_time', #wjl # 原字段名 - new_name='last_modify_time', #wjl# 新字段名 + model_name='commands', + old_name='last_mod_time', + new_name='last_modify_time', ), - - #wjl# 操作 4:将 emailsendlog 模型中的字段 created_time 重命名为 creation_time migrations.RenameField( - model_name='emailsendlog', #wjl# 要操作的模型名称 - old_name='created_time', #wjl# 原字段名 - new_name='creation_time', #wjl# 新字段名 + model_name='emailsendlog', + old_name='created_time', + new_name='creation_time', ), - ] \ No newline at end of file + ] diff --git a/src/servermanager/models.py b/src/servermanager/models.py index b09558a..4326c65 100644 --- a/src/servermanager/models.py +++ b/src/servermanager/models.py @@ -1,96 +1,33 @@ from django.db import models -#wjl# Create your models here. -#wjl# 定义了两个数据模型: -#wjl# 1. commands:用于存储可执行的系统命令 -#wjl# 2. EmailSendLog:用于记录邮件发送的历史日志 - - +# Create your models here. class commands(models.Model): - # wjl - """ - 数据模型:commands - 用于存储系统中预定义的可执行命令,例如服务器管理命令、脚本路径等。 - 可通过 Django Admin 管理后台进行增删改查。 - """ - - #wjl# 命令的标题/名称,用于标识该命令(如 "重启服务") title = models.CharField('命令标题', max_length=300) - - #wjl# 实际要执行的命令字符串(如 "systemctl restart nginx") command = models.CharField('命令', max_length=2000) - - #wjl# 对该命令的简要描述,说明其用途 describe = models.CharField('命令描述', max_length=300) - - #wjl# 记录该命令首次创建的时间,自动在创建时设置为当前时间 creation_time = models.DateTimeField('创建时间', auto_now_add=True) - - #wjl# 记录该命令最后一次修改的时间,每次保存时自动更新为当前时间 last_modify_time = models.DateTimeField('修改时间', auto_now=True) def __str__(self): - # wjl - """ - 返回该模型实例的字符串表示。 - 在 Django Admin 或外键显示时,将显示命令的标题。 - - 返回: - str: 命令的标题(title 字段) - """ return self.title class Meta: - # wjl - """ - 模型元数据配置。 - 定义模型的可读名称,用于在 Django Admin 后台显示。 - """ - verbose_name = '命令' #wjl# 单数形式的名称 - verbose_name_plural = verbose_name #wjl #wjl# 复数形式的名称(中文通常与单数相同) + verbose_name = '命令' + verbose_name_plural = verbose_name class EmailSendLog(models.Model): - # wjl - """ - 数据模型:EmailSendLog - 用于记录系统发送邮件的日志信息,包括收件人、内容、结果和时间。 - 所有字段均为只读,仅用于查看历史记录,不可手动修改。 - """ - - #wjl# 邮件收件人地址(支持多个,以逗号分隔) emailto = models.CharField('收件人', max_length=300) - - #wjl# 邮件标题 title = models.CharField('邮件标题', max_length=2000) - - #wjl # 邮件正文内容,支持长文本 content = models.TextField('邮件内容') - - #wjl# 邮件是否发送成功,True 表示成功,False 表示失败 send_result = models.BooleanField('结果', default=False) - - #wjl# 邮件记录创建时间,自动在插入时设置为当前时间 creation_time = models.DateTimeField('创建时间', auto_now_add=True) def __str__(self): - # wjl - """ - 返回该模型实例的字符串表示。 - 在 Django Admin 或外键显示时,将显示邮件的标题。 - - 返回: - str: 邮件的标题(title 字段) - """ return self.title class Meta: - # wjl - """ - 模型元数据配置。 - 定义模型的可读名称和默认排序规则。 - """ - verbose_name = '邮件发送log' #wjl# 单数形式的名称 - verbose_name_plural = verbose_name #wjl # 复数形式的名称 - ordering = ['-creation_time'] #wjl# 默认排序:按创建时间倒序排列(最新的在前) \ No newline at end of file + verbose_name = '邮件发送log' + verbose_name_plural = verbose_name + ordering = ['-creation_time'] diff --git a/src/servermanager/robot.py b/src/servermanager/robot.py index 9c72a14..7b45736 100644 --- a/src/servermanager/robot.py +++ b/src/servermanager/robot.py @@ -1,78 +1,46 @@ -import logging # 用于记录日志信息 -import os # 用于操作系统相关功能(如环境变量、文件路径) -import re # 正则表达式,用于文本匹配和提取 - -import jsonpickle # 将 Python 对象序列化为 JSON 字符串(支持复杂对象) -from django.conf import settings # Django 配置设置,如 BASE_DIR -from werobot import WeRoBot # WeRoBot 微信机器人框架核心类 -from werobot.replies import ArticlesReply, Article # 构造图文回复消息 -from werobot.session.filestorage import FileStorage # 文件存储会话后端(备用) - -# 项目内部工具与 API -from djangoblog.utils import get_sha256 # SHA256 加密函数,用于密码安全 -from servermanager.api.blogapi import BlogApi # 博客文章搜索/获取接口 -from servermanager.api.commonapi import ChatGPT, CommandHandler # 聊天与命令处理 -from .MemcacheStorage import MemcacheStorage # 自定义基于缓存的会话存储 - - -#wjl# 初始化微信机器人实例 -robot = WeRoBot( - token=os.environ.get('DJANGO_WEROBOT_TOKEN') or 'lylinux', #wjl# 微信公众号 Token - enable_session=True #wjl# 启用会话功能,用于维护用户状态 -) - -#wjl# 创建自定义的缓存会话存储实例(基于 Redis/Memcached) +import logging +import os +import re + +import jsonpickle +from django.conf import settings +from werobot import WeRoBot +from werobot.replies import ArticlesReply, Article +from werobot.session.filestorage import FileStorage + +from djangoblog.utils import get_sha256 +from servermanager.api.blogapi import BlogApi +from servermanager.api.commonapi import ChatGPT, CommandHandler +from .MemcacheStorage import MemcacheStorage + +robot = WeRoBot(token=os.environ.get('DJANGO_WEROBOT_TOKEN') + or 'lylinux', enable_session=True) memstorage = MemcacheStorage() - -#wjl# 检查缓存存储是否可用 if memstorage.is_available: - #wjl# 如果可用,使用 MemcacheStorage 作为会话后端 robot.config['SESSION_STORAGE'] = memstorage else: - #wjl# 如果不可用,回退到文件存储 - #wjl # 删除旧的会话文件(避免冲突或损坏) - session_file = os.path.join(settings.BASE_DIR, 'werobot_session') - if os.path.exists(session_file): - os.remove(session_file) - #wjl # 使用本地文件存储会话数据 + if os.path.exists(os.path.join(settings.BASE_DIR, 'werobot_session')): + os.remove(os.path.join(settings.BASE_DIR, 'werobot_session')) robot.config['SESSION_STORAGE'] = FileStorage(filename='werobot_session') -#wjl# 实例化博客 API 和命令处理器 -blogapi = BlogApi() #wjl # 提供博客文章查询服务 -cmd_handler = CommandHandler()#wjl # 提供服务器命令执行功能 -logger = logging.getLogger(__name__) #wjl# 获取当前模块的日志记录器 +blogapi = BlogApi() +cmd_handler = CommandHandler() +logger = logging.getLogger(__name__) def convert_to_article_reply(articles, message): - # wjl - """ - 将博客文章列表转换为微信图文消息回复格式。 - - 参数: - articles (list): 包含文章对象的列表(通常来自 blogapi 查询结果) - message: 当前微信消息对象,用于构造回复 - - 返回: - ArticlesReply: 可直接返回给用户的图文消息回复对象 - - 流程: - 1. 创建一个图文回复对象 - 2. 遍历每篇文章,提取标题、摘要、首张图片、链接 - 3. 构造 Article 对象并添加到回复中 - """ reply = ArticlesReply(message=message) - from blog.templatetags.blog_tags import truncatechars_content #wjl# 导入截断内容的模板标签 - + from blog.templatetags.blog_tags import truncatechars_content for post in articles: - #wjl# 使用正则从文章内容中提取第一张图片 URL(png/jpg) imgs = re.findall(r'(?:http\:|https\:)?\/\/.*\.(?:png|jpg)', post.body) - imgurl = imgs[0] if imgs else '' #wjl# 如果有图取第一张,否则为空 - + imgurl = '' + if imgs: + imgurl = imgs[0] article = Article( title=post.title, - description=truncatechars_content(post.body), #wjl# 截断内容作为描述 + description=truncatechars_content(post.body), img=imgurl, - url=post.get_full_url() #wjl# 文章完整 URL + url=post.get_full_url() ) reply.add_article(article) return reply @@ -80,24 +48,11 @@ def convert_to_article_reply(articles, message): @robot.filter(re.compile(r"^\?.*")) def search(message, session): - # wjl - """ - 处理以 '?' 开头的消息,用于搜索博客文章。 - - 示例: "?python" 搜索包含 python 的文章 - - 参数: - message: 微信消息对象 - session: 当前用户会话对象 - - 返回: - 图文消息 或 文本提示 - """ s = message.content - searchstr = str(s).replace('?', '') # 去掉问号 - result = blogapi.search_articles(searchstr) # 调用博客 API 搜索 + searchstr = str(s).replace('?', '') + result = blogapi.search_articles(searchstr) if result: - articles = list(map(lambda x: x.object, result)) # 提取文章对象 + articles = list(map(lambda x: x.object, result)) reply = convert_to_article_reply(articles, message) return reply else: @@ -106,35 +61,13 @@ def search(message, session): @robot.filter(re.compile(r'^category\s*$', re.I)) def category(message, session): - # wjl - """ - 处理 "category" 消息,返回所有文章分类目录。 - - 参数: - message: 微信消息对象 - session: 当前用户会话对象 - - 返回: - 文本消息:列出所有分类名称 - """ categorys = blogapi.get_category_lists() - content = ','.join(map(lambda x: x.name, categorys)) #wjl# 拼接分类名 + content = ','.join(map(lambda x: x.name, categorys)) return '所有文章分类目录:' + content @robot.filter(re.compile(r'^recent\s*$', re.I)) def recents(message, session): - # wjl - """ - 处理 "recent" 消息,返回最新发布的文章。 - - 参数: - message: 微信消息对象 - session: 当前用户会话对象 - - 返回: - 图文消息(最新文章) 或 文本提示 - """ articles = blogapi.get_recent_articles() if articles: reply = convert_to_article_reply(articles, message) @@ -145,17 +78,6 @@ def recents(message, session): @robot.filter(re.compile('^help$', re.I)) def help(message, session): - # wjl - """ - 处理 "help" 消息,返回帮助文档。 - - 参数: - message: 微信消息对象 - session: 当前用户会话对象 - - 返回: - 文本消息:详细的使用说明和命令列表 - """ return '''欢迎关注! 默认会与图灵机器人聊天~~ 你可以通过下面这些命令来获得信息 @@ -178,173 +100,88 @@ def help(message, session): @robot.filter(re.compile(r'^weather\:.*$', re.I)) def weather(message, session): - # wjl - """ - 处理 "weather:" 开头的消息(天气查询功能)。 - 当前为占位符,功能正在建设中。 - - 参数: - message: 微信消息对象 - session: 当前用户会话对象 - - 返回: - 文本消息:提示功能建设中 - """ return "建设中..." @robot.filter(re.compile(r'^idcard\:.*$', re.I)) def idcard(message, session): - # wjl - """ - 处理 "idcard:" 开头的消息(身份证信息查询功能)。 - 当前为占位符,功能正在建设中。 - - 参数: - message: 微信消息对象 - session: 当前用户会话对象 - - 返回: - 文本消息:提示功能建设中 - """ return "建设中..." @robot.handler def echo(message, session): - # wjl - """ - 默认消息处理器,当没有其他 filter 匹配时调用。 - 创建 MessageHandler 实例处理消息。 - - 参数: - message: 微信消息对象 - session: 当前用户会话对象 - - 返回: - 处理结果(文本或图文消息) - """ handler = MessageHandler(message, session) return handler.handler() class MessageHandler: - # wjl - """ - 消息处理器类,负责处理用户消息,尤其是管理员命令和认证流程。 - 使用会话(session)维护用户状态(是否管理员、是否已认证等)。 - """ - def __init__(self, message, session): + userid = message.source self.message = message self.session = session - self.userid = message.source #wjl# 用户唯一标识(OpenID) - - # 尝试从会话中加载用户信息 + self.userid = userid try: - info = session[self.userid] - self.userinfo = jsonpickle.decode(info) #wjl# 反序列化为 WxUserInfo 对象 + info = session[userid] + self.userinfo = jsonpickle.decode(info) except Exception as e: - #wjl# 如果出错(首次访问或会话丢失),创建默认用户信息 userinfo = WxUserInfo() self.userinfo = userinfo @property def is_admin(self): - #wjl"""判断当前用户是否为管理员""" return self.userinfo.isAdmin @property def is_password_set(self): - #wjl"""判断当前管理员是否已完成密码验证""" return self.userinfo.isPasswordSet def save_session(self): - # wjl - """ - 将当前用户信息保存回会话。 - 使用 jsonpickle 序列化对象,并存入 session。 - """ info = jsonpickle.encode(self.userinfo) self.session[self.userid] = info def handler(self): - # wjl - """ - 核心消息处理逻辑,根据用户状态和输入内容返回相应响应。 - - 处理流程: - 1. 管理员退出登录 - 2. 管理员登录请求 - 3. 管理员密码验证 - 4. 执行管理员命令 - 5. 默认:调用 ChatGPT 进行聊天 - - 返回: - str: 要回复给用户的消息内容 - """ info = self.message.content - #wjl# 退出管理员模式 if self.userinfo.isAdmin and info.upper() == 'EXIT': self.userinfo = WxUserInfo() self.save_session() return "退出成功" - - #wjl# 请求进入管理员模式 if info.upper() == 'ADMIN': self.userinfo.isAdmin = True self.save_session() return "输入管理员密码" - - #wjl# 管理员密码验证阶段 if self.userinfo.isAdmin and not self.userinfo.isPasswordSet: - #wjl# 获取配置中的管理员密码(SHA256 加密后) passwd = settings.WXADMIN if settings.TESTING: - passwd = '123' #wjl# 测试环境下使用简单密码 - #wjl# 验证用户输入的密码(双重 SHA256) + passwd = '123' if passwd.upper() == get_sha256(get_sha256(info)).upper(): self.userinfo.isPasswordSet = True self.save_session() - return #wjl"验证通过,请输入命令或者要执行的命令代码:输入helpme获得帮助" + return "验证通过,请输入命令或者要执行的命令代码:输入helpme获得帮助" else: - #wjl# 密码错误次数限制 if self.userinfo.Count >= 3: self.userinfo = WxUserInfo() self.save_session() - return #wjl"超过验证次数" + return "超过验证次数" self.userinfo.Count += 1 self.save_session() - return #wjl"验证失败,请重新输入管理员密码:" - - #wjl # 管理员已认证,可执行命令 + return "验证失败,请重新输入管理员密码:" if self.userinfo.isAdmin and self.userinfo.isPasswordSet: if self.userinfo.Command != '' and info.upper() == 'Y': - #wjl# 用户确认执行命令 return cmd_handler.run(self.userinfo.Command) else: if info.upper() == 'HELPME': - #wjl# 显示命令帮助 return cmd_handler.get_help() - #wjl# 记录待执行的命令,等待用户确认 self.userinfo.Command = info self.save_session() return "确认执行: " + info + " 命令?" - #wjl # 默认行为:调用 ChatGPT 进行普通聊天 return ChatGPT.chat(info) class WxUserInfo(): - # wjl - """ - 微信用户信息类,用于在会话中存储用户状态。 - 包括是否为管理员、是否已通过密码验证、尝试次数、待执行命令等。 - """ - def __init__(self): - self.isAdmin = False #wjl # 是否请求成为管理员 - self.isPasswordSet = False #wjl# 是否已通过密码验证 - self.Count = 0 #wjl # 密码尝试次数 - self.Command = '' #wjl# 待执行的命令 \ No newline at end of file + self.isAdmin = False + self.isPasswordSet = False + self.Count = 0 + self.Command = '' diff --git a/src/servermanager/tests.py b/src/servermanager/tests.py index 58e22fd..22a6689 100644 --- a/src/servermanager/tests.py +++ b/src/servermanager/tests.py @@ -1,152 +1,79 @@ from django.test import Client, RequestFactory, TestCase -from django.utils import timezone #wjl# 用于处理时间相关的测试 - -#wjl# 导入 WeRoBot 消息类,用于模拟微信消息 +from django.utils import timezone from werobot.messages.messages import TextMessage -#wjl# 导入项目中的模型和 API -from accounts.models import BlogUser #wjl# 用户模型(用于创建管理员) -from blog.models import Category, Article #wjl # 博客分类和文章模型 -from servermanager.api.commonapi import ChatGPT #wjl# 聊天功能接口 -from .models import commands #wjl# 命令模型 -from .robot import MessageHandler, CommandHandler #wjl# 核心机器人处理器 -from .robot import search, category, recents #wjl# 微信机器人命令函数 +from accounts.models import BlogUser +from blog.models import Category, Article +from servermanager.api.commonapi import ChatGPT +from .models import commands +from .robot import MessageHandler, CommandHandler +from .robot import search, category, recents +# Create your tests here. class ServerManagerTest(TestCase): - # wjl - """ - Django 测试用例类,用于对 servermanager 应用的核心功能进行单元测试。 - 测试内容包括: - - ChatGPT 聊天功能 - - 微信机器人命令响应(搜索、分类、最新文章) - - 服务器命令执行 - - 管理员会话流程 - """ - def setUp(self): - # wjl - """ - 在每个测试方法执行前自动运行的初始化方法。 - 设置测试所需的公共环境: - - 创建测试客户端(用于模拟 HTTP 请求) - - 创建请求工厂(用于构造请求对象) - - 注意:虽然此处创建了 factory,但在当前测试中并未实际使用。 - """ - self.client = Client() #wjl# Django 测试客户端 - self.factory = RequestFactory() #wjl# 请求工厂,用于创建模拟请求 + self.client = Client() + self.factory = RequestFactory() def test_chat_gpt(self): - # wjl - """ - 测试 ChatGPT 聊天功能是否正常工作。 - - 步骤: - 1. 调用 ChatGPT.chat 方法发送问候语 "你好" - 2. 断言返回内容不为 None,表示接口有响应 - - 目的:验证聊天接口的基本可用性。 - """ content = ChatGPT.chat("你好") self.assertIsNotNone(content) def test_validate_comment(self): - # wjl - """ - 综合测试方法,覆盖多个功能点。 - 名称 'validate_comment' 不准确,实际测试的是 servermanager 的核心功能。 - - 测试流程: - 1. 创建管理员用户并登录 - 2. 创建分类和文章用于测试搜索功能 - 3. 测试微信机器人的 search、category、recents 命令 - 4. 测试命令执行(CommandHandler) - 5. 模拟完整管理员会话流程(登录、执行命令、退出等) - """ - - #wjl # 1. 创建超级用户 user = BlogUser.objects.create_superuser( email="liangliangyy1@gmail.com", username="liangliangyy1", password="liangliangyy1") - #wjl# 使用测试客户端登录该用户 self.client.login(username='liangliangyy1', password='liangliangyy1') - #wjl# 2. 创建博客分类 c = Category() c.name = "categoryccc" c.save() - #wjl# 3. 创建一篇已发布的文章 article = Article() article.title = "nicetitleccc" article.body = "nicecontentccc" article.author = user article.category = c - article.type = 'a' #wjl# 文章类型 - article.status = 'p' #wjl # 发布状态 + article.type = 'a' + article.status = 'p' article.save() - - #wjl# 4. 模拟微信文本消息 s = TextMessage([]) - s.content = "nice" #wjl# 搜索关键词 - - #wjl # 5. 测试 search 命令 - #wjlrsp = search(s, None) # 调用 search 过滤器 - #wjl # 断言有响应(即使未找到文章也应返回提示) - - #wjl # 6. 测试 category 命令 + s.content = "nice" + rsp = search(s, None) rsp = category(None, None) - self.assertIsNotNone(rsp) # 确保返回了分类列表 - - #wjl# 7. 测试 recents 命令 + self.assertIsNotNone(rsp) rsp = recents(None, None) - #wjl# 断言返回结果不是“暂时还没有文章”,说明能获取到文章 self.assertTrue(rsp != '暂时还没有文章') - #wjl # 8. 测试命令执行功能 cmd = commands() cmd.title = "test" - cmd.command = "ls" #wjl # 测试命令(列出目录) + cmd.command = "ls" cmd.describe = "test" - cmd.save() #wjl # 保存到数据库 + cmd.save() - #wjl# 实例化命令处理器 cmdhandler = CommandHandler() - #wjl # 执行名为 'test' 的命令 rsp = cmdhandler.run('test') - #wjl# 断言命令执行有返回结果 self.assertIsNotNone(rsp) - - #wjl# 9. 模拟管理员会话流程 - s.source = 'u' #wjl# 设置用户标识(OpenID) - s.content = 'test' #wjl # 用户输入内容 - - #wjl# 创建消息处理器实例,传入模拟消息和空会话 + s.source = 'u' + s.content = 'test' msghandler = MessageHandler(s, {}) - #wjl# 模拟用户行为序列: - msghandler.handler() #wjl# 处理 "test" 消息(应提示确认) - - s.content = 'y' #wjl# 用户确认执行 - msghandler.handler() #wjl# 执行命令 - - s.content = 'idcard:12321233' #wjl# 尝试身份证查询(占位功能) + # msghandler.userinfo.isPasswordSet = True + # msghandler.userinfo.isAdmin = True msghandler.handler() - - s.content = 'weather:上海' #wjl# 尝试天气查询(占位功能) + s.content = 'y' msghandler.handler() - - s.content = 'admin' #wjl# 请求进入管理员模式 + s.content = 'idcard:12321233' msghandler.handler() - - s.content = '123' #wjl# 输入密码(测试环境下有效) + s.content = 'weather:上海' msghandler.handler() - - s.content = 'exit' #wjl # 退出管理员模式 + s.content = 'admin' + msghandler.handler() + s.content = '123' msghandler.handler() - #wjl# 注意:此测试未包含 assert 断言来验证每个步骤的结果, - #wjl# 主要是通过调用来检查是否抛出异常,确保代码路径可执行。 \ No newline at end of file + s.content = 'exit' + msghandler.handler() diff --git a/src/servermanager/urls.py b/src/servermanager/urls.py index dce3f1f..8d134d2 100644 --- a/src/servermanager/urls.py +++ b/src/servermanager/urls.py @@ -1,43 +1,10 @@ -#wjl# 导入 Django URL 路由模块 from django.urls import path -# 导入 WeRoBot 与 Django 集成的工具函数 from werobot.contrib.django import make_view -# 从当前应用的 robot 模块导入已配置的 WeRoBot 实例 from .robot import robot -#wjl# 定义应用命名空间 app_name = "servermanager" -""" -应用命名空间,用于在 Django 项目中唯一标识此应用的 URL。 -在模板或 reverse() 函数中可通过 'servermanager:xxx' 引用此应用的 URL。 -""" - -#wjl# 定义 URL 路由列表 urlpatterns = [ - #wjl# 将微信机器人接入点绑定到特定 URL 路径 path(r'robot', make_view(robot)), -#wjl - """ - URL 路由配置: - - path(r'robot', make_view(robot)) - - 功能说明: - - 将路径 '/robot' 映射到 WeRoBot 的请求处理视图。 - - 当微信服务器向该路径发送 GET(验证)或 POST(消息)请求时, - Django 会将其转发给 WeRoBot 框架处理。 - - make_view(robot) 是 werobot 提供的适配器函数,它将 WeRoBot 实例 - 转换为一个兼容 Django 的视图函数(View)。 - - 参数说明: - - r'robot': URL 路径,用户访问 /robot 时触发。 - 注意:此处使用了原始字符串(r''),但无特殊转义需求,可简写为 'robot'。 - - make_view(robot): 将 robot 实例封装为 Django 视图。 - 典型使用场景: - 在微信公众平台开发者配置中,将服务器地址(URL)设置为: - https://yourdomain.com/servermanager/robot/ - 并配合 Token 验证机器人身份。 - """ ] diff --git a/src/servermanager/views.py b/src/servermanager/views.py index 5fce73b..60f00ef 100644 --- a/src/servermanager/views.py +++ b/src/servermanager/views.py @@ -1,4 +1 @@ -#wjl# Create your views here. -""" -视图模块(views.py) -""" \ No newline at end of file +# Create your views here. diff --git a/src/templates/comments/tags/comment_item.html b/src/templates/comments/tags/comment_item.html index ebb0388..0693649 100644 --- a/src/templates/comments/tags/comment_item.html +++ b/src/templates/comments/tags/comment_item.html @@ -2,10 +2,13 @@
  • - + class="avatar avatar-96 photo" + loading="lazy" + decoding="async" + style="max-width:100%;height:auto;">
    - + class="avatar avatar-96 photo" + loading="lazy" + decoding="async" + style="max-width:100%;height:auto;"> + + + +
    + + + +
    + + +
    +
    + DeepSeek 技术助手 + × +
    +
    +
    你好!我是本站的技术助手,有什么可以帮你的吗?
    +
    +
    + + +
    +
    + + + \ No newline at end of file diff --git a/src/templates/plugins/article_recommendation/__init__.py b/src/templates/plugins/article_recommendation/__init__.py new file mode 100644 index 0000000..7d86a99 --- /dev/null +++ b/src/templates/plugins/article_recommendation/__init__.py @@ -0,0 +1 @@ +# 插件模板目录 diff --git a/src/templates/plugins/article_recommendation/bottom_widget.html b/src/templates/plugins/article_recommendation/bottom_widget.html new file mode 100644 index 0000000..829b7b4 --- /dev/null +++ b/src/templates/plugins/article_recommendation/bottom_widget.html @@ -0,0 +1,23 @@ +{% load i18n %} +
    +

    + 📖{{ title }} +

    +
    +
    diff --git a/src/templates/plugins/article_recommendation/sidebar_widget.html b/src/templates/plugins/article_recommendation/sidebar_widget.html new file mode 100644 index 0000000..5f1afbf --- /dev/null +++ b/src/templates/plugins/article_recommendation/sidebar_widget.html @@ -0,0 +1,17 @@ +{% load i18n %} + diff --git a/src/templates/plugins/css_includes.html b/src/templates/plugins/css_includes.html new file mode 100644 index 0000000..37029ae --- /dev/null +++ b/src/templates/plugins/css_includes.html @@ -0,0 +1,4 @@ +{% comment %}插件CSS文件包含模板 - 用于压缩{% endcomment %} +{% for css_file in css_files %} + +{% endfor %} diff --git a/src/templates/plugins/js_includes.html b/src/templates/plugins/js_includes.html new file mode 100644 index 0000000..2a315e3 --- /dev/null +++ b/src/templates/plugins/js_includes.html @@ -0,0 +1,4 @@ +{% comment %}插件JS文件包含模板 - 用于压缩{% endcomment %} +{% for js_file in js_files %} + +{% endfor %} diff --git a/src/templates/share_layout/base.html b/src/templates/share_layout/base.html index d5f523f..b85c8a6 100644 --- a/src/templates/share_layout/base.html +++ b/src/templates/share_layout/base.html @@ -26,7 +26,7 @@ {% compress css %} - + @@ -50,6 +50,41 @@ {% include 'share_layout/nav.html' %} + + + @@ -81,5 +116,7 @@ {% block footer %}{% endblock %} +{% include 'plugins/ai_chat_widget.html' %} + \ No newline at end of file