diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..10b731c --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,5 @@ +# 默认忽略的文件 +/shelf/ +/workspace.xml +# 基于编辑器的 HTTP 客户端请求 +/httpRequests/ diff --git a/.idea/DjangoBlog-master.iml b/.idea/DjangoBlog-master.iml new file mode 100644 index 0000000..d2720cc --- /dev/null +++ b/.idea/DjangoBlog-master.iml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..5668ada --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..5494c00 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/DjangoBlog-master/.coveragerc b/DjangoBlog-master/.coveragerc new file mode 100644 index 0000000..9757484 --- /dev/null +++ b/DjangoBlog-master/.coveragerc @@ -0,0 +1,10 @@ +[run] +source = . +include = *.py +omit = + *migrations* + *tests* + *.html + *whoosh_cn_backend* + *settings.py* + *venv* diff --git a/DjangoBlog-master/.dockerignore b/DjangoBlog-master/.dockerignore new file mode 100644 index 0000000..2818c38 --- /dev/null +++ b/DjangoBlog-master/.dockerignore @@ -0,0 +1,11 @@ +bin/data/ +# virtualenv +venv/ +collectedstatic/ +djangoblog/whoosh_index/ +uploads/ +settings_production.py +*.md +docs/ +logs/ +static/ \ No newline at end of file diff --git a/DjangoBlog-master/.gitattributes b/DjangoBlog-master/.gitattributes new file mode 100644 index 0000000..fd52ece --- /dev/null +++ b/DjangoBlog-master/.gitattributes @@ -0,0 +1,6 @@ +blog/static/* linguist-vendored +*.js linguist-vendored +*.css linguist-vendored +* text=auto +*.sh text eol=lf +*.conf text eol=lf \ No newline at end of file diff --git a/DjangoBlog-master/.github/ISSUE_TEMPLATE.md b/DjangoBlog-master/.github/ISSUE_TEMPLATE.md new file mode 100644 index 0000000..2b5b7aa --- /dev/null +++ b/DjangoBlog-master/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,18 @@ + + +**我确定我已经查看了** (标注`[ ]`为`[x]`) + +- [ ] [DjangoBlog的readme](https://github.com/liangliangyy/DjangoBlog/blob/master/README.md) +- [ ] [配置说明](https://github.com/liangliangyy/DjangoBlog/blob/master/bin/config.md) +- [ ] [其他 Issues](https://github.com/liangliangyy/DjangoBlog/issues) + +---- + +**我要申请** (标注`[ ]`为`[x]`) + +- [ ] BUG 反馈 +- [ ] 添加新的特性或者功能 +- [ ] 请求技术支持 diff --git a/DjangoBlog-master/.github/workflows/codeql-analysis.yml b/DjangoBlog-master/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000..6b76522 --- /dev/null +++ b/DjangoBlog-master/.github/workflows/codeql-analysis.yml @@ -0,0 +1,47 @@ +name: "CodeQL" + +on: + push: + branches: + - master + - dev + paths-ignore: + - '**/*.md' + - '**/*.css' + - '**/*.js' + - '**/*.yml' + - '**/*.txt' + pull_request: + branches: + - master + - dev + paths-ignore: + - '**/*.md' + - '**/*.css' + - '**/*.js' + - '**/*.yml' + - '**/*.txt' + schedule: + - cron: '30 1 * * 0' + + +jobs: + CodeQL-Build: + runs-on: ubuntu-latest + permissions: + security-events: write + actions: read + contents: read + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 \ No newline at end of file diff --git a/DjangoBlog-master/.github/workflows/django.yml b/DjangoBlog-master/.github/workflows/django.yml new file mode 100644 index 0000000..94baea9 --- /dev/null +++ b/DjangoBlog-master/.github/workflows/django.yml @@ -0,0 +1,136 @@ +name: Django CI + +on: + push: + branches: + - master + - dev + paths-ignore: + - '**/*.md' + - '**/*.css' + - '**/*.js' + pull_request: + branches: + - master + - dev + paths-ignore: + - '**/*.md' + - '**/*.css' + - '**/*.js' + +jobs: + build-normal: + runs-on: ubuntu-latest + strategy: + max-parallel: 4 + matrix: + python-version: ["3.10","3.11" ] + + steps: + - name: Start MySQL + uses: samin/mysql-action@v1.3 + with: + host port: 3306 + container port: 3306 + character set server: utf8mb4 + collation server: utf8mb4_general_ci + mysql version: latest + mysql root password: root + mysql database: djangoblog + mysql user: root + mysql password: root + + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + cache: 'pip' + - name: Install Dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + - name: Run Tests + env: + DJANGO_MYSQL_PASSWORD: root + DJANGO_MYSQL_HOST: 127.0.0.1 + run: | + python manage.py makemigrations + python manage.py migrate + python manage.py test + + build-with-es: + runs-on: ubuntu-latest + strategy: + max-parallel: 4 + matrix: + python-version: ["3.10","3.11" ] + + steps: + - name: Start MySQL + uses: samin/mysql-action@v1.3 + with: + host port: 3306 + container port: 3306 + character set server: utf8mb4 + collation server: utf8mb4_general_ci + mysql version: latest + mysql root password: root + mysql database: djangoblog + mysql user: root + mysql password: root + + - name: Configure sysctl limits + run: | + sudo swapoff -a + sudo sysctl -w vm.swappiness=1 + sudo sysctl -w fs.file-max=262144 + sudo sysctl -w vm.max_map_count=262144 + + - uses: miyataka/elasticsearch-github-actions@1 + + with: + stack-version: '7.12.1' + plugins: 'https://release.infinilabs.com/analysis-ik/stable/elasticsearch-analysis-ik-7.12.1.zip' + + + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + cache: 'pip' + - name: Install Dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + - name: Run Tests + env: + DJANGO_MYSQL_PASSWORD: root + DJANGO_MYSQL_HOST: 127.0.0.1 + DJANGO_ELASTICSEARCH_HOST: 127.0.0.1:9200 + run: | + python manage.py makemigrations + python manage.py migrate + coverage run manage.py test + coverage xml + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v1 + + docker: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Build and push + uses: docker/build-push-action@v3 + with: + context: . + push: false + tags: djangoblog/djangoblog:dev diff --git a/DjangoBlog-master/.github/workflows/docker.yml b/DjangoBlog-master/.github/workflows/docker.yml new file mode 100644 index 0000000..a312e2f --- /dev/null +++ b/DjangoBlog-master/.github/workflows/docker.yml @@ -0,0 +1,43 @@ +name: docker + +on: + push: + paths-ignore: + - '**/*.md' + - '**/*.yml' + branches: + - 'master' + - 'dev' + +jobs: + docker: + runs-on: ubuntu-latest + steps: + - name: Set env to docker dev tag + if: endsWith(github.ref, '/dev') + run: | + echo "DOCKER_TAG=test" >> $GITHUB_ENV + - name: Set env to docker latest tag + if: endsWith(github.ref, '/master') + run: | + echo "DOCKER_TAG=latest" >> $GITHUB_ENV + - name: Checkout + uses: actions/checkout@v3 + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Login to DockerHub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Build and push + uses: docker/build-push-action@v3 + with: + context: . + push: true + tags: ${{ secrets.DOCKERHUB_USERNAME }}/djangoblog:${{env.DOCKER_TAG}} + + diff --git a/DjangoBlog-master/.github/workflows/publish-release.yml b/DjangoBlog-master/.github/workflows/publish-release.yml new file mode 100644 index 0000000..5eb0853 --- /dev/null +++ b/DjangoBlog-master/.github/workflows/publish-release.yml @@ -0,0 +1,39 @@ +name: publish release + +on: + release: + types: [ published ] + +jobs: + docker: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Docker meta + id: meta + uses: docker/metadata-action@v3 + with: + images: name/app + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + - name: Login to DockerHub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Build and push + uses: docker/build-push-action@v3 + with: + context: . + push: true + platforms: | + linux/amd64 + linux/arm64 + linux/arm/v7 + linux/arm/v6 + linux/386 + tags: ${{ secrets.DOCKERHUB_USERNAME }}/djangoblog:${{ github.event.release.tag_name }} diff --git a/DjangoBlog-master/.gitignore b/DjangoBlog-master/.gitignore new file mode 100644 index 0000000..3015816 --- /dev/null +++ b/DjangoBlog-master/.gitignore @@ -0,0 +1,80 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover + +# Translations +*.pot + +# Django stuff: +*.log +logs/ + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + + +# PyCharm +# http://www.jetbrains.com/pycharm/webhelp/project.html +.idea +.iml +static/ +# virtualenv +venv/ + +collectedstatic/ +djangoblog/whoosh_index/ +google93fd32dbd906620a.html +baidu_verify_FlHL7cUyC9.html +BingSiteAuth.xml +cb9339dbe2ff86a5aa169d28dba5f615.txt +werobot_session.* +django.jpg +uploads/ +settings_production.py +werobot_session.db +bin/datas/ diff --git a/DjangoBlog-master/Dockerfile b/DjangoBlog-master/Dockerfile new file mode 100644 index 0000000..80b46ac --- /dev/null +++ b/DjangoBlog-master/Dockerfile @@ -0,0 +1,15 @@ +FROM python:3.11 +ENV PYTHONUNBUFFERED 1 +WORKDIR /code/djangoblog/ +RUN apt-get update && \ + apt-get install default-libmysqlclient-dev gettext -y && \ + rm -rf /var/lib/apt/lists/* +ADD requirements.txt requirements.txt +RUN pip install --upgrade pip && \ + pip install --no-cache-dir -r requirements.txt && \ + pip install --no-cache-dir gunicorn[gevent] && \ + pip cache purge + +ADD . . +RUN chmod +x /code/djangoblog/deploy/entrypoint.sh +ENTRYPOINT ["/code/djangoblog/deploy/entrypoint.sh"] diff --git a/DjangoBlog-master/LICENSE b/DjangoBlog-master/LICENSE new file mode 100644 index 0000000..3b08474 --- /dev/null +++ b/DjangoBlog-master/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2025 车亮亮 + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/DjangoBlog-master/README.md b/DjangoBlog-master/README.md new file mode 100644 index 0000000..56aa4cc --- /dev/null +++ b/DjangoBlog-master/README.md @@ -0,0 +1,158 @@ +# DjangoBlog + +

+ Django CI + CodeQL + codecov + license +

+ +

+ 一款功能强大、设计优雅的现代化博客系统 +
+ English简体中文 +

+ +--- + +DjangoBlog 是一款基于 Python 3.10 和 Django 4.0 构建的高性能博客平台。它不仅提供了传统博客的所有核心功能,还通过一个灵活的插件系统,让您可以轻松扩展和定制您的网站。无论您是个人博主、技术爱好者还是内容创作者,DjangoBlog 都旨在为您提供一个稳定、高效且易于维护的写作和发布环境。 + +## ✨ 特性亮点 + +- **强大的内容管理**: 支持文章、独立页面、分类和标签的完整管理。内置强大的 Markdown 编辑器,支持代码语法高亮。 +- **全文搜索**: 集成搜索引擎,提供快速、精准的文章内容搜索。 +- **互动评论系统**: 支持回复、邮件提醒等功能,评论内容同样支持 Markdown。 +- **灵活的侧边栏**: 可自定义展示最新文章、最多阅读、标签云等模块。 +- **社交化登录**: 内置 OAuth 支持,已集成 Google, GitHub, Facebook, 微博, QQ 等主流平台。 +- **高性能缓存**: 原生支持 Redis 缓存,并提供自动刷新机制,确保网站高速响应。 +- **SEO 友好**: 具备基础 SEO 功能,新内容发布后可自动通知 Google 和百度。 +- **便捷的插件系统**: 通过创建独立的插件来扩展博客功能,代码解耦,易于维护。我们已经通过插件实现了文章浏览计数、SEO 优化等功能! +- **集成图床**: 内置简单的图床功能,方便图片上传和管理。 +- **自动化前端**: 集成 `django-compressor`,自动压缩和优化 CSS 及 JavaScript 文件。 +- **健壮的运维**: 内置网站异常邮件提醒和微信公众号管理功能。 + +## 🛠️ 技术栈 + +- **后端**: Python 3.10, Django 4.0 +- **数据库**: MySQL, SQLite (可配置) +- **缓存**: Redis +- **前端**: HTML5, CSS3, JavaScript +- **搜索**: Whoosh, Elasticsearch (可配置) +- **编辑器**: Markdown (mdeditor) + +## 🚀 快速开始 + +### 1. 环境准备 + +确保您的系统中已安装 Python 3.10+ 和 MySQL/MariaDB。 + +### 2. 克隆与安装 + +```bash +# 克隆项目到本地 +git clone https://github.com/liangliangyy/DjangoBlog.git +cd DjangoBlog + +# 安装依赖 +pip install -r requirements.txt +``` + +### 3. 项目配置 + +- **数据库**: + 打开 `djangoblog/settings.py` 文件,找到 `DATABASES` 配置项,修改为您的 MySQL 连接信息。 + + ```python + DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.mysql', + 'NAME': 'djangoblog', + 'USER': 'root', + 'PASSWORD': 'your_password', + 'HOST': '127.0.0.1', + 'PORT': 3306, + } + } + ``` + 在 MySQL 中创建数据库: + ```sql + CREATE DATABASE `djangoblog` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + ``` + +- **更多配置**: + 关于邮件发送、OAuth 登录、缓存等更多高级配置,请参阅我们的 [详细配置文档](/docs/config.md)。 + +### 4. 初始化数据库 + +```bash +python manage.py makemigrations +python manage.py migrate + +# 创建一个超级管理员账户 +python manage.py createsuperuser +``` + +### 5. 运行项目 + +```bash +# (可选) 生成一些测试数据 +python manage.py create_testdata + +# (可选) 收集和压缩静态文件 +python manage.py collectstatic --noinput +python manage.py compress --force + +# 启动开发服务器 +python manage.py runserver +``` + +现在,在您的浏览器中访问 `http://127.0.0.1:8000/`,您应该能看到 DjangoBlog 的首页了! + +## 部署 + +- **传统部署**: 我们为您准备了非常详细的 [服务器部署教程](https://www.lylinux.net/article/2019/8/5/58.html)。 +- **Docker 部署**: 项目已全面支持 Docker。如果您熟悉容器化技术,请参考 [Docker 部署文档](/docs/docker.md) 来快速启动。 +- **Kubernetes 部署**: 我们也提供了完整的 [Kubernetes 部署指南](/docs/k8s.md),助您轻松上云。 + +## 🧩 插件系统 + +插件系统是 DjangoBlog 的核心特色之一。它允许您在不修改核心代码的情况下,通过编写独立的插件来为您的博客添加新功能。 + +- **工作原理**: 插件通过在预定义的“钩子”上注册回调函数来工作。例如,当一篇文章被渲染时,`after_article_body_get` 钩子会被触发,所有注册到此钩子的函数都会被执行。 +- **现有插件**: `view_count`(浏览计数), `seo_optimizer`(SEO优化)等都是通过插件系统实现的。 +- **开发您自己的插件**: 只需在 `plugins` 目录下创建一个新的文件夹,并编写您的 `plugin.py`。欢迎探索并为 DjangoBlog 社区贡献您的创意! + +## 🤝 贡献指南 + +我们热烈欢迎任何形式的贡献!如果您有好的想法或发现了 Bug,请随时提交 Issue 或 Pull Request。 + +## 📄 许可证 + +本项目基于 [MIT License](LICENSE) 开源。 + +--- + +## ❤️ 支持与赞助 + +如果您觉得这个项目对您有帮助,并且希望支持我继续维护和开发新功能,欢迎请我喝杯咖啡!您的每一份支持都是我前进的最大动力。 + +

+ 支付宝赞助 + 微信赞助 +

+

+ (左) 支付宝 / (右) 微信 +

+ +## 🙏 鸣谢 + +特别感谢 **JetBrains** 为本项目提供的免费开源许可证。 + +

+ + JetBrains Logo + +

+ +--- +> 如果本项目帮助到了你,请在[这里](https://github.com/liangliangyy/DjangoBlog/issues/214)留下你的网址,让更多的人看到。您的回复将会是我继续更新维护下去的动力。 diff --git a/DjangoBlog-master/accounts/__init__.py b/DjangoBlog-master/accounts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/DjangoBlog-master/accounts/admin.py b/DjangoBlog-master/accounts/admin.py new file mode 100644 index 0000000..32e483c --- /dev/null +++ b/DjangoBlog-master/accounts/admin.py @@ -0,0 +1,59 @@ +from django import forms +from django.contrib.auth.admin import UserAdmin +from django.contrib.auth.forms import UserChangeForm +from django.contrib.auth.forms import UsernameField +from django.utils.translation import gettext_lazy as _ + +# Register your models here. +from .models import BlogUser + + +class BlogUserCreationForm(forms.ModelForm): + password1 = forms.CharField(label=_('password'), widget=forms.PasswordInput) + password2 = forms.CharField(label=_('Enter password again'), widget=forms.PasswordInput) + + class Meta: + model = BlogUser + fields = ('email',) + + def clean_password2(self): + # Check that the two password entries match + password1 = self.cleaned_data.get("password1") + password2 = self.cleaned_data.get("password2") + if password1 and password2 and password1 != password2: + raise forms.ValidationError(_("passwords do not match")) + return password2 + + def save(self, commit=True): + # Save the provided password in hashed format + user = super().save(commit=False) + user.set_password(self.cleaned_data["password1"]) + if commit: + user.source = 'adminsite' + user.save() + return user + + +class BlogUserChangeForm(UserChangeForm): + class Meta: + model = BlogUser + fields = '__all__' + field_classes = {'username': UsernameField} + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + +class BlogUserAdmin(UserAdmin): + form = BlogUserChangeForm + add_form = BlogUserCreationForm + list_display = ( + 'id', + 'nickname', + 'username', + 'email', + 'last_login', + 'date_joined', + 'source') + list_display_links = ('id', 'username') + ordering = ('-id',) diff --git a/DjangoBlog-master/accounts/apps.py b/DjangoBlog-master/accounts/apps.py new file mode 100644 index 0000000..9b3fc5a --- /dev/null +++ b/DjangoBlog-master/accounts/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class AccountsConfig(AppConfig): + name = 'accounts' diff --git a/DjangoBlog-master/accounts/forms.py b/DjangoBlog-master/accounts/forms.py new file mode 100644 index 0000000..fce4137 --- /dev/null +++ b/DjangoBlog-master/accounts/forms.py @@ -0,0 +1,117 @@ +from django import forms +from django.contrib.auth import get_user_model, password_validation +from django.contrib.auth.forms import AuthenticationForm, UserCreationForm +from django.core.exceptions import ValidationError +from django.forms import widgets +from django.utils.translation import gettext_lazy as _ +from . import utils +from .models import BlogUser + + +class LoginForm(AuthenticationForm): + def __init__(self, *args, **kwargs): + super(LoginForm, self).__init__(*args, **kwargs) + self.fields['username'].widget = widgets.TextInput( + attrs={'placeholder': "username", "class": "form-control"}) + self.fields['password'].widget = widgets.PasswordInput( + attrs={'placeholder': "password", "class": "form-control"}) + + +class RegisterForm(UserCreationForm): + def __init__(self, *args, **kwargs): + super(RegisterForm, self).__init__(*args, **kwargs) + + self.fields['username'].widget = widgets.TextInput( + attrs={'placeholder': "username", "class": "form-control"}) + self.fields['email'].widget = widgets.EmailInput( + attrs={'placeholder': "email", "class": "form-control"}) + self.fields['password1'].widget = widgets.PasswordInput( + attrs={'placeholder': "password", "class": "form-control"}) + self.fields['password2'].widget = widgets.PasswordInput( + attrs={'placeholder': "repeat password", "class": "form-control"}) + + def clean_email(self): + email = self.cleaned_data['email'] + if get_user_model().objects.filter(email=email).exists(): + raise ValidationError(_("email already exists")) + return email + + class Meta: + model = get_user_model() + fields = ("username", "email") + + +class ForgetPasswordForm(forms.Form): + new_password1 = forms.CharField( + label=_("New password"), + widget=forms.PasswordInput( + attrs={ + "class": "form-control", + 'placeholder': _("New password") + } + ), + ) + + new_password2 = forms.CharField( + label="确认密码", + widget=forms.PasswordInput( + attrs={ + "class": "form-control", + 'placeholder': _("Confirm password") + } + ), + ) + + email = forms.EmailField( + label='邮箱', + widget=forms.TextInput( + attrs={ + 'class': 'form-control', + 'placeholder': _("Email") + } + ), + ) + + code = forms.CharField( + label=_('Code'), + widget=forms.TextInput( + attrs={ + 'class': 'form-control', + 'placeholder': _("Code") + } + ), + ) + + def clean_new_password2(self): + password1 = self.data.get("new_password1") + password2 = self.data.get("new_password2") + if password1 and password2 and password1 != password2: + raise ValidationError(_("passwords do not match")) + password_validation.validate_password(password2) + + return password2 + + def clean_email(self): + user_email = self.cleaned_data.get("email") + if not BlogUser.objects.filter( + email=user_email + ).exists(): + # todo 这里的报错提示可以判断一个邮箱是不是注册过,如果不想暴露可以修改 + raise ValidationError(_("email does not exist")) + return user_email + + def clean_code(self): + code = self.cleaned_data.get("code") + error = utils.verify( + email=self.cleaned_data.get("email"), + code=code, + ) + if error: + raise ValidationError(error) + return code + + +class ForgetPasswordCodeForm(forms.Form): + email = forms.EmailField( + label=_('Email'), + ) diff --git a/DjangoBlog-master/accounts/migrations/0001_initial.py b/DjangoBlog-master/accounts/migrations/0001_initial.py new file mode 100644 index 0000000..d2fbcab --- /dev/null +++ b/DjangoBlog-master/accounts/migrations/0001_initial.py @@ -0,0 +1,49 @@ +# Generated by Django 4.1.7 on 2023-03-02 07:14 + +import django.contrib.auth.models +import django.contrib.auth.validators +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('auth', '0012_alter_user_first_name_max_length'), + ] + + operations = [ + migrations.CreateModel( + name='BlogUser', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('password', models.CharField(max_length=128, verbose_name='password')), + ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), + ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), + ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), + ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), + ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), + ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), + ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), + ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), + ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), + ('nickname', models.CharField(blank=True, max_length=100, verbose_name='昵称')), + ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')), + ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')), + ('source', models.CharField(blank=True, max_length=100, verbose_name='创建来源')), + ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')), + ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')), + ], + options={ + 'verbose_name': '用户', + 'verbose_name_plural': '用户', + 'ordering': ['-id'], + 'get_latest_by': 'id', + }, + managers=[ + ('objects', django.contrib.auth.models.UserManager()), + ], + ), + ] diff --git a/DjangoBlog-master/accounts/migrations/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.py b/DjangoBlog-master/accounts/migrations/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.py new file mode 100644 index 0000000..1a9f509 --- /dev/null +++ b/DjangoBlog-master/accounts/migrations/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.py @@ -0,0 +1,46 @@ +# Generated by Django 4.2.5 on 2023-09-06 13:13 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0001_initial'), + ] + + operations = [ + migrations.AlterModelOptions( + name='bloguser', + options={'get_latest_by': 'id', 'ordering': ['-id'], 'verbose_name': 'user', 'verbose_name_plural': 'user'}, + ), + migrations.RemoveField( + model_name='bloguser', + name='created_time', + ), + migrations.RemoveField( + model_name='bloguser', + name='last_mod_time', + ), + migrations.AddField( + model_name='bloguser', + name='creation_time', + field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'), + ), + migrations.AddField( + model_name='bloguser', + name='last_modify_time', + field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'), + ), + migrations.AlterField( + model_name='bloguser', + name='nickname', + field=models.CharField(blank=True, max_length=100, verbose_name='nick name'), + ), + migrations.AlterField( + model_name='bloguser', + name='source', + field=models.CharField(blank=True, max_length=100, verbose_name='create source'), + ), + ] diff --git a/DjangoBlog-master/accounts/migrations/__init__.py b/DjangoBlog-master/accounts/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/DjangoBlog-master/accounts/models.py b/DjangoBlog-master/accounts/models.py new file mode 100644 index 0000000..3baddbb --- /dev/null +++ b/DjangoBlog-master/accounts/models.py @@ -0,0 +1,35 @@ +from django.contrib.auth.models import AbstractUser +from django.db import models +from django.urls import reverse +from django.utils.timezone import now +from django.utils.translation import gettext_lazy as _ +from djangoblog.utils import get_current_site + + +# Create your models here. + +class BlogUser(AbstractUser): + nickname = models.CharField(_('nick name'), max_length=100, blank=True) + creation_time = models.DateTimeField(_('creation time'), default=now) + last_modify_time = models.DateTimeField(_('last modify time'), default=now) + source = models.CharField(_('create source'), max_length=100, blank=True) + + def get_absolute_url(self): + return reverse( + 'blog:author_detail', kwargs={ + 'author_name': self.username}) + + def __str__(self): + return self.email + + def get_full_url(self): + site = get_current_site().domain + url = "https://{site}{path}".format(site=site, + path=self.get_absolute_url()) + return url + + class Meta: + ordering = ['-id'] + verbose_name = _('user') + verbose_name_plural = verbose_name + get_latest_by = 'id' diff --git a/DjangoBlog-master/accounts/templatetags/__init__.py b/DjangoBlog-master/accounts/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/DjangoBlog-master/accounts/tests.py b/DjangoBlog-master/accounts/tests.py new file mode 100644 index 0000000..6893411 --- /dev/null +++ b/DjangoBlog-master/accounts/tests.py @@ -0,0 +1,207 @@ +from django.test import Client, RequestFactory, TestCase +from django.urls import reverse +from django.utils import timezone +from django.utils.translation import gettext_lazy as _ + +from accounts.models import BlogUser +from blog.models import Article, Category +from djangoblog.utils import * +from . import utils + + +# Create your tests here. + +class AccountTest(TestCase): + def setUp(self): + self.client = Client() + self.factory = RequestFactory() + self.blog_user = BlogUser.objects.create_user( + username="test", + email="admin@admin.com", + password="12345678" + ) + self.new_test = "xxx123--=" + + def test_validate_account(self): + site = get_current_site().domain + user = BlogUser.objects.create_superuser( + email="liangliangyy1@gmail.com", + username="liangliangyy1", + password="qwer!@#$ggg") + testuser = BlogUser.objects.get(username='liangliangyy1') + + loginresult = self.client.login( + username='liangliangyy1', + password='qwer!@#$ggg') + self.assertEqual(loginresult, True) + response = self.client.get('/admin/') + self.assertEqual(response.status_code, 200) + + category = Category() + category.name = "categoryaaa" + category.creation_time = timezone.now() + category.last_modify_time = timezone.now() + category.save() + + article = Article() + article.title = "nicetitleaaa" + article.body = "nicecontentaaa" + article.author = user + article.category = category + article.type = 'a' + article.status = 'p' + article.save() + + response = self.client.get(article.get_admin_url()) + self.assertEqual(response.status_code, 200) + + def test_validate_register(self): + self.assertEquals( + 0, len( + BlogUser.objects.filter( + email='user123@user.com'))) + response = self.client.post(reverse('account:register'), { + 'username': 'user1233', + 'email': 'user123@user.com', + 'password1': 'password123!q@wE#R$T', + 'password2': 'password123!q@wE#R$T', + }) + self.assertEquals( + 1, len( + BlogUser.objects.filter( + email='user123@user.com'))) + user = BlogUser.objects.filter(email='user123@user.com')[0] + sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id))) + path = reverse('accounts:result') + url = '{path}?type=validation&id={id}&sign={sign}'.format( + path=path, id=user.id, sign=sign) + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + + self.client.login(username='user1233', password='password123!q@wE#R$T') + user = BlogUser.objects.filter(email='user123@user.com')[0] + user.is_superuser = True + user.is_staff = True + user.save() + delete_sidebar_cache() + category = Category() + category.name = "categoryaaa" + category.creation_time = timezone.now() + category.last_modify_time = timezone.now() + category.save() + + article = Article() + article.category = category + article.title = "nicetitle333" + article.body = "nicecontentttt" + article.author = user + + article.type = 'a' + article.status = 'p' + article.save() + + response = self.client.get(article.get_admin_url()) + self.assertEqual(response.status_code, 200) + + response = self.client.get(reverse('account:logout')) + self.assertIn(response.status_code, [301, 302, 200]) + + response = self.client.get(article.get_admin_url()) + self.assertIn(response.status_code, [301, 302, 200]) + + response = self.client.post(reverse('account:login'), { + 'username': 'user1233', + 'password': 'password123' + }) + self.assertIn(response.status_code, [301, 302, 200]) + + response = self.client.get(article.get_admin_url()) + self.assertIn(response.status_code, [301, 302, 200]) + + def test_verify_email_code(self): + to_email = "admin@admin.com" + code = generate_code() + utils.set_code(to_email, code) + utils.send_verify_email(to_email, code) + + err = utils.verify("admin@admin.com", code) + self.assertEqual(err, None) + + err = utils.verify("admin@123.com", code) + self.assertEqual(type(err), str) + + def test_forget_password_email_code_success(self): + resp = self.client.post( + path=reverse("account:forget_password_code"), + data=dict(email="admin@admin.com") + ) + + self.assertEqual(resp.status_code, 200) + self.assertEqual(resp.content.decode("utf-8"), "ok") + + def test_forget_password_email_code_fail(self): + resp = self.client.post( + path=reverse("account:forget_password_code"), + data=dict() + ) + self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱") + + resp = self.client.post( + path=reverse("account:forget_password_code"), + data=dict(email="admin@com") + ) + self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱") + + def test_forget_password_email_success(self): + code = generate_code() + utils.set_code(self.blog_user.email, code) + data = dict( + new_password1=self.new_test, + new_password2=self.new_test, + email=self.blog_user.email, + code=code, + ) + resp = self.client.post( + path=reverse("account:forget_password"), + data=data + ) + self.assertEqual(resp.status_code, 302) + + # 验证用户密码是否修改成功 + blog_user = BlogUser.objects.filter( + email=self.blog_user.email, + ).first() # type: BlogUser + self.assertNotEqual(blog_user, None) + self.assertEqual(blog_user.check_password(data["new_password1"]), True) + + def test_forget_password_email_not_user(self): + data = dict( + new_password1=self.new_test, + new_password2=self.new_test, + email="123@123.com", + code="123456", + ) + resp = self.client.post( + path=reverse("account:forget_password"), + data=data + ) + + self.assertEqual(resp.status_code, 200) + + + def test_forget_password_email_code_error(self): + code = generate_code() + utils.set_code(self.blog_user.email, code) + data = dict( + new_password1=self.new_test, + new_password2=self.new_test, + email=self.blog_user.email, + code="111111", + ) + resp = self.client.post( + path=reverse("account:forget_password"), + data=data + ) + + self.assertEqual(resp.status_code, 200) + diff --git a/DjangoBlog-master/accounts/urls.py b/DjangoBlog-master/accounts/urls.py new file mode 100644 index 0000000..107a801 --- /dev/null +++ b/DjangoBlog-master/accounts/urls.py @@ -0,0 +1,28 @@ +from django.urls import path +from django.urls import re_path + +from . import views +from .forms import LoginForm + +app_name = "accounts" + +urlpatterns = [re_path(r'^login/$', + views.LoginView.as_view(success_url='/'), + name='login', + kwargs={'authentication_form': LoginForm}), + re_path(r'^register/$', + views.RegisterView.as_view(success_url="/"), + name='register'), + re_path(r'^logout/$', + views.LogoutView.as_view(), + name='logout'), + path(r'account/result.html', + views.account_result, + name='result'), + re_path(r'^forget_password/$', + views.ForgetPasswordView.as_view(), + name='forget_password'), + re_path(r'^forget_password_code/$', + views.ForgetPasswordEmailCode.as_view(), + name='forget_password_code'), + ] diff --git a/DjangoBlog-master/accounts/user_login_backend.py b/DjangoBlog-master/accounts/user_login_backend.py new file mode 100644 index 0000000..73cdca1 --- /dev/null +++ b/DjangoBlog-master/accounts/user_login_backend.py @@ -0,0 +1,26 @@ +from django.contrib.auth import get_user_model +from django.contrib.auth.backends import ModelBackend + + +class EmailOrUsernameModelBackend(ModelBackend): + """ + 允许使用用户名或邮箱登录 + """ + + def authenticate(self, request, username=None, password=None, **kwargs): + if '@' in username: + kwargs = {'email': username} + else: + kwargs = {'username': username} + try: + user = get_user_model().objects.get(**kwargs) + if user.check_password(password): + return user + except get_user_model().DoesNotExist: + return None + + def get_user(self, username): + try: + return get_user_model().objects.get(pk=username) + except get_user_model().DoesNotExist: + return None diff --git a/DjangoBlog-master/accounts/utils.py b/DjangoBlog-master/accounts/utils.py new file mode 100644 index 0000000..4b94bdf --- /dev/null +++ b/DjangoBlog-master/accounts/utils.py @@ -0,0 +1,49 @@ +import typing +from datetime import timedelta + +from django.core.cache import cache +from django.utils.translation import gettext +from django.utils.translation import gettext_lazy as _ + +from djangoblog.utils import send_email + +_code_ttl = timedelta(minutes=5) + + +def send_verify_email(to_mail: str, code: str, subject: str = _("Verify Email")): + """发送重设密码验证码 + Args: + to_mail: 接受邮箱 + subject: 邮件主题 + code: 验证码 + """ + html_content = _( + "You are resetting the password, the verification code is:%(code)s, valid within 5 minutes, please keep it " + "properly") % {'code': code} + send_email([to_mail], subject, html_content) + + +def verify(email: str, code: str) -> typing.Optional[str]: + """验证code是否有效 + Args: + email: 请求邮箱 + code: 验证码 + Return: + 如果有错误就返回错误str + Node: + 这里的错误处理不太合理,应该采用raise抛出 + 否测调用方也需要对error进行处理 + """ + cache_code = get_code(email) + if cache_code != code: + return gettext("Verification code error") + + +def set_code(email: str, code: str): + """设置code""" + cache.set(email, code, _code_ttl.seconds) + + +def get_code(email: str) -> typing.Optional[str]: + """获取code""" + return cache.get(email) diff --git a/DjangoBlog-master/accounts/views.py b/DjangoBlog-master/accounts/views.py new file mode 100644 index 0000000..ae67aec --- /dev/null +++ b/DjangoBlog-master/accounts/views.py @@ -0,0 +1,204 @@ +import logging +from django.utils.translation import gettext_lazy as _ +from django.conf import settings +from django.contrib import auth +from django.contrib.auth import REDIRECT_FIELD_NAME +from django.contrib.auth import get_user_model +from django.contrib.auth import logout +from django.contrib.auth.forms import AuthenticationForm +from django.contrib.auth.hashers import make_password +from django.http import HttpResponseRedirect, HttpResponseForbidden +from django.http.request import HttpRequest +from django.http.response import HttpResponse +from django.shortcuts import get_object_or_404 +from django.shortcuts import render +from django.urls import reverse +from django.utils.decorators import method_decorator +from django.utils.http import url_has_allowed_host_and_scheme +from django.views import View +from django.views.decorators.cache import never_cache +from django.views.decorators.csrf import csrf_protect +from django.views.decorators.debug import sensitive_post_parameters +from django.views.generic import FormView, RedirectView + +from djangoblog.utils import send_email, get_sha256, get_current_site, generate_code, delete_sidebar_cache +from . import utils +from .forms import RegisterForm, LoginForm, ForgetPasswordForm, ForgetPasswordCodeForm +from .models import BlogUser + +logger = logging.getLogger(__name__) + + +# Create your views here. + +class RegisterView(FormView): + form_class = RegisterForm + template_name = 'account/registration_form.html' + + @method_decorator(csrf_protect) + def dispatch(self, *args, **kwargs): + return super(RegisterView, self).dispatch(*args, **kwargs) + + def form_valid(self, form): + if form.is_valid(): + user = form.save(False) + user.is_active = False + user.source = 'Register' + user.save(True) + site = get_current_site().domain + sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id))) + + if settings.DEBUG: + site = '127.0.0.1:8000' + path = reverse('account:result') + url = "http://{site}{path}?type=validation&id={id}&sign={sign}".format( + site=site, path=path, id=user.id, sign=sign) + + content = """ +

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

+ + {url} + + 再次感谢您! +
+ 如果上面链接无法打开,请将此链接复制至浏览器。 + {url} + """.format(url=url) + send_email( + emailto=[ + user.email, + ], + title='验证您的电子邮箱', + content=content) + + url = reverse('accounts:result') + \ + '?type=register&id=' + str(user.id) + return HttpResponseRedirect(url) + else: + return self.render_to_response({ + 'form': form + }) + + +class LogoutView(RedirectView): + url = '/login/' + + @method_decorator(never_cache) + def dispatch(self, request, *args, **kwargs): + return super(LogoutView, self).dispatch(request, *args, **kwargs) + + def get(self, request, *args, **kwargs): + logout(request) + delete_sidebar_cache() + return super(LogoutView, self).get(request, *args, **kwargs) + + +class LoginView(FormView): + form_class = LoginForm + template_name = 'account/login.html' + success_url = '/' + redirect_field_name = REDIRECT_FIELD_NAME + login_ttl = 2626560 # 一个月的时间 + + @method_decorator(sensitive_post_parameters('password')) + @method_decorator(csrf_protect) + @method_decorator(never_cache) + def dispatch(self, request, *args, **kwargs): + + return super(LoginView, self).dispatch(request, *args, **kwargs) + + def get_context_data(self, **kwargs): + redirect_to = self.request.GET.get(self.redirect_field_name) + if redirect_to is None: + redirect_to = '/' + kwargs['redirect_to'] = redirect_to + + return super(LoginView, self).get_context_data(**kwargs) + + def form_valid(self, form): + form = AuthenticationForm(data=self.request.POST, request=self.request) + + if form.is_valid(): + delete_sidebar_cache() + logger.info(self.redirect_field_name) + + auth.login(self.request, form.get_user()) + if self.request.POST.get("remember"): + self.request.session.set_expiry(self.login_ttl) + return super(LoginView, self).form_valid(form) + # return HttpResponseRedirect('/') + else: + return self.render_to_response({ + 'form': form + }) + + def get_success_url(self): + + redirect_to = self.request.POST.get(self.redirect_field_name) + if not url_has_allowed_host_and_scheme( + url=redirect_to, allowed_hosts=[ + self.request.get_host()]): + redirect_to = self.success_url + return redirect_to + + +def account_result(request): + type = request.GET.get('type') + id = request.GET.get('id') + + user = get_object_or_404(get_user_model(), id=id) + logger.info(type) + if user.is_active: + return HttpResponseRedirect('/') + if type and type in ['register', 'validation']: + if type == 'register': + content = ''' + 恭喜您注册成功,一封验证邮件已经发送到您的邮箱,请验证您的邮箱后登录本站。 + ''' + title = '注册成功' + else: + c_sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id))) + sign = request.GET.get('sign') + if sign != c_sign: + return HttpResponseForbidden() + user.is_active = True + user.save() + content = ''' + 恭喜您已经成功的完成邮箱验证,您现在可以使用您的账号来登录本站。 + ''' + title = '验证成功' + return render(request, 'account/result.html', { + 'title': title, + 'content': content + }) + else: + return HttpResponseRedirect('/') + + +class ForgetPasswordView(FormView): + form_class = ForgetPasswordForm + template_name = 'account/forget_password.html' + + def form_valid(self, form): + if form.is_valid(): + blog_user = BlogUser.objects.filter(email=form.cleaned_data.get("email")).get() + blog_user.password = make_password(form.cleaned_data["new_password2"]) + blog_user.save() + return HttpResponseRedirect('/login/') + else: + return self.render_to_response({'form': form}) + + +class ForgetPasswordEmailCode(View): + + def post(self, request: HttpRequest): + form = ForgetPasswordCodeForm(request.POST) + if not form.is_valid(): + return HttpResponse("错误的邮箱") + to_email = form.cleaned_data["email"] + + code = generate_code() + utils.send_verify_email(to_email, code) + utils.set_code(to_email, code) + + return HttpResponse("ok") diff --git a/DjangoBlog-master/blog/__init__.py b/DjangoBlog-master/blog/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/DjangoBlog-master/blog/admin.py b/DjangoBlog-master/blog/admin.py new file mode 100644 index 0000000..46c3420 --- /dev/null +++ b/DjangoBlog-master/blog/admin.py @@ -0,0 +1,112 @@ +from django import forms +from django.contrib import admin +from django.contrib.auth import get_user_model +from django.urls import reverse +from django.utils.html import format_html +from django.utils.translation import gettext_lazy as _ + +# Register your models here. +from .models import Article + + +class ArticleForm(forms.ModelForm): + # body = forms.CharField(widget=AdminPagedownWidget()) + + class Meta: + model = Article + fields = '__all__' + + +def makr_article_publish(modeladmin, request, queryset): + queryset.update(status='p') + + +def draft_article(modeladmin, request, queryset): + queryset.update(status='d') + + +def close_article_commentstatus(modeladmin, request, queryset): + queryset.update(comment_status='c') + + +def open_article_commentstatus(modeladmin, request, queryset): + queryset.update(comment_status='o') + + +makr_article_publish.short_description = _('Publish selected articles') +draft_article.short_description = _('Draft selected articles') +close_article_commentstatus.short_description = _('Close article comments') +open_article_commentstatus.short_description = _('Open article comments') + + +class ArticlelAdmin(admin.ModelAdmin): + list_per_page = 20 + search_fields = ('body', 'title') + form = ArticleForm + list_display = ( + 'id', + 'title', + 'author', + 'link_to_category', + 'creation_time', + 'views', + 'status', + 'type', + 'article_order') + list_display_links = ('id', 'title') + list_filter = ('status', 'type', 'category') + filter_horizontal = ('tags',) + exclude = ('creation_time', 'last_modify_time') + view_on_site = True + actions = [ + makr_article_publish, + draft_article, + close_article_commentstatus, + open_article_commentstatus] + + def link_to_category(self, obj): + info = (obj.category._meta.app_label, obj.category._meta.model_name) + link = reverse('admin:%s_%s_change' % info, args=(obj.category.id,)) + return format_html(u'%s' % (link, obj.category.name)) + + link_to_category.short_description = _('category') + + def get_form(self, request, obj=None, **kwargs): + form = super(ArticlelAdmin, self).get_form(request, obj, **kwargs) + form.base_fields['author'].queryset = get_user_model( + ).objects.filter(is_superuser=True) + return form + + def save_model(self, request, obj, form, change): + super(ArticlelAdmin, self).save_model(request, obj, form, change) + + def get_view_on_site_url(self, obj=None): + if obj: + url = obj.get_full_url() + return url + else: + from djangoblog.utils import get_current_site + site = get_current_site().domain + return site + + +class TagAdmin(admin.ModelAdmin): + exclude = ('slug', 'last_mod_time', 'creation_time') + + +class CategoryAdmin(admin.ModelAdmin): + list_display = ('name', 'parent_category', 'index') + exclude = ('slug', 'last_mod_time', 'creation_time') + + +class LinksAdmin(admin.ModelAdmin): + exclude = ('last_mod_time', 'creation_time') + + +class SideBarAdmin(admin.ModelAdmin): + list_display = ('name', 'content', 'is_enable', 'sequence') + exclude = ('last_mod_time', 'creation_time') + + +class BlogSettingsAdmin(admin.ModelAdmin): + pass diff --git a/DjangoBlog-master/blog/apps.py b/DjangoBlog-master/blog/apps.py new file mode 100644 index 0000000..7930587 --- /dev/null +++ b/DjangoBlog-master/blog/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class BlogConfig(AppConfig): + name = 'blog' diff --git a/DjangoBlog-master/blog/context_processors.py b/DjangoBlog-master/blog/context_processors.py new file mode 100644 index 0000000..73e3088 --- /dev/null +++ b/DjangoBlog-master/blog/context_processors.py @@ -0,0 +1,43 @@ +import logging + +from django.utils import timezone + +from djangoblog.utils import cache, get_blog_setting +from .models import Category, Article + +logger = logging.getLogger(__name__) + + +def seo_processor(requests): + key = 'seo_processor' + value = cache.get(key) + if value: + return value + else: + logger.info('set processor cache.') + setting = get_blog_setting() + value = { + 'SITE_NAME': setting.site_name, + 'SHOW_GOOGLE_ADSENSE': setting.show_google_adsense, + 'GOOGLE_ADSENSE_CODES': setting.google_adsense_codes, + 'SITE_SEO_DESCRIPTION': setting.site_seo_description, + 'SITE_DESCRIPTION': setting.site_description, + 'SITE_KEYWORDS': setting.site_keywords, + 'SITE_BASE_URL': requests.scheme + '://' + requests.get_host() + '/', + 'ARTICLE_SUB_LENGTH': setting.article_sub_length, + 'nav_category_list': Category.objects.all(), + 'nav_pages': Article.objects.filter( + type='p', + status='p'), + 'OPEN_SITE_COMMENT': setting.open_site_comment, + 'BEIAN_CODE': setting.beian_code, + 'ANALYTICS_CODE': setting.analytics_code, + "BEIAN_CODE_GONGAN": setting.gongan_beiancode, + "SHOW_GONGAN_CODE": setting.show_gongan_code, + "CURRENT_YEAR": timezone.now().year, + "GLOBAL_HEADER": setting.global_header, + "GLOBAL_FOOTER": setting.global_footer, + "COMMENT_NEED_REVIEW": setting.comment_need_review, + } + cache.set(key, value, 60 * 60 * 10) + return value diff --git a/DjangoBlog-master/blog/documents.py b/DjangoBlog-master/blog/documents.py new file mode 100644 index 0000000..0f1db7b --- /dev/null +++ b/DjangoBlog-master/blog/documents.py @@ -0,0 +1,213 @@ +import time + +import elasticsearch.client +from django.conf import settings +from elasticsearch_dsl import Document, InnerDoc, Date, Integer, Long, Text, Object, GeoPoint, Keyword, Boolean +from elasticsearch_dsl.connections import connections + +from blog.models import Article + +ELASTICSEARCH_ENABLED = hasattr(settings, 'ELASTICSEARCH_DSL') + +if ELASTICSEARCH_ENABLED: + connections.create_connection( + hosts=[settings.ELASTICSEARCH_DSL['default']['hosts']]) + from elasticsearch import Elasticsearch + + es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts']) + from elasticsearch.client import IngestClient + + c = IngestClient(es) + try: + c.get_pipeline('geoip') + except elasticsearch.exceptions.NotFoundError: + c.put_pipeline('geoip', body='''{ + "description" : "Add geoip info", + "processors" : [ + { + "geoip" : { + "field" : "ip" + } + } + ] + }''') + + +class GeoIp(InnerDoc): + continent_name = Keyword() + country_iso_code = Keyword() + country_name = Keyword() + location = GeoPoint() + + +class UserAgentBrowser(InnerDoc): + Family = Keyword() + Version = Keyword() + + +class UserAgentOS(UserAgentBrowser): + pass + + +class UserAgentDevice(InnerDoc): + Family = Keyword() + Brand = Keyword() + Model = Keyword() + + +class UserAgent(InnerDoc): + browser = Object(UserAgentBrowser, required=False) + os = Object(UserAgentOS, required=False) + device = Object(UserAgentDevice, required=False) + string = Text() + is_bot = Boolean() + + +class ElapsedTimeDocument(Document): + url = Keyword() + time_taken = Long() + log_datetime = Date() + ip = Keyword() + geoip = Object(GeoIp, required=False) + useragent = Object(UserAgent, required=False) + + class Index: + name = 'performance' + settings = { + "number_of_shards": 1, + "number_of_replicas": 0 + } + + class Meta: + doc_type = 'ElapsedTime' + + +class ElaspedTimeDocumentManager: + @staticmethod + def build_index(): + from elasticsearch import Elasticsearch + client = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts']) + res = client.indices.exists(index="performance") + if not res: + ElapsedTimeDocument.init() + + @staticmethod + def delete_index(): + from elasticsearch import Elasticsearch + es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts']) + es.indices.delete(index='performance', ignore=[400, 404]) + + @staticmethod + def create(url, time_taken, log_datetime, useragent, ip): + ElaspedTimeDocumentManager.build_index() + ua = UserAgent() + ua.browser = UserAgentBrowser() + ua.browser.Family = useragent.browser.family + ua.browser.Version = useragent.browser.version_string + + ua.os = UserAgentOS() + ua.os.Family = useragent.os.family + ua.os.Version = useragent.os.version_string + + ua.device = UserAgentDevice() + ua.device.Family = useragent.device.family + ua.device.Brand = useragent.device.brand + ua.device.Model = useragent.device.model + ua.string = useragent.ua_string + ua.is_bot = useragent.is_bot + + doc = ElapsedTimeDocument( + meta={ + 'id': int( + round( + time.time() * + 1000)) + }, + url=url, + time_taken=time_taken, + log_datetime=log_datetime, + useragent=ua, ip=ip) + doc.save(pipeline="geoip") + + +class ArticleDocument(Document): + body = Text(analyzer='ik_max_word', search_analyzer='ik_smart') + title = Text(analyzer='ik_max_word', search_analyzer='ik_smart') + author = Object(properties={ + 'nickname': Text(analyzer='ik_max_word', search_analyzer='ik_smart'), + 'id': Integer() + }) + category = Object(properties={ + 'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'), + 'id': Integer() + }) + tags = Object(properties={ + 'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'), + 'id': Integer() + }) + + pub_time = Date() + status = Text() + comment_status = Text() + type = Text() + views = Integer() + article_order = Integer() + + class Index: + name = 'blog' + settings = { + "number_of_shards": 1, + "number_of_replicas": 0 + } + + class Meta: + doc_type = 'Article' + + +class ArticleDocumentManager(): + + def __init__(self): + self.create_index() + + def create_index(self): + ArticleDocument.init() + + def delete_index(self): + from elasticsearch import Elasticsearch + es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts']) + es.indices.delete(index='blog', ignore=[400, 404]) + + def convert_to_doc(self, articles): + return [ + ArticleDocument( + meta={ + 'id': article.id}, + body=article.body, + title=article.title, + author={ + 'nickname': article.author.username, + 'id': article.author.id}, + category={ + 'name': article.category.name, + 'id': article.category.id}, + tags=[ + { + 'name': t.name, + 'id': t.id} for t in article.tags.all()], + pub_time=article.pub_time, + status=article.status, + comment_status=article.comment_status, + type=article.type, + views=article.views, + article_order=article.article_order) for article in articles] + + def rebuild(self, articles=None): + ArticleDocument.init() + articles = articles if articles else Article.objects.all() + docs = self.convert_to_doc(articles) + for doc in docs: + doc.save() + + def update_docs(self, docs): + for doc in docs: + doc.save() diff --git a/DjangoBlog-master/blog/forms.py b/DjangoBlog-master/blog/forms.py new file mode 100644 index 0000000..715be76 --- /dev/null +++ b/DjangoBlog-master/blog/forms.py @@ -0,0 +1,19 @@ +import logging + +from django import forms +from haystack.forms import SearchForm + +logger = logging.getLogger(__name__) + + +class BlogSearchForm(SearchForm): + querydata = forms.CharField(required=True) + + def search(self): + datas = super(BlogSearchForm, self).search() + if not self.is_valid(): + return self.no_query_found() + + if self.cleaned_data['querydata']: + logger.info(self.cleaned_data['querydata']) + return datas diff --git a/DjangoBlog-master/blog/management/__init__.py b/DjangoBlog-master/blog/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/DjangoBlog-master/blog/management/commands/__init__.py b/DjangoBlog-master/blog/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/DjangoBlog-master/blog/management/commands/build_index.py b/DjangoBlog-master/blog/management/commands/build_index.py new file mode 100644 index 0000000..3c4acd7 --- /dev/null +++ b/DjangoBlog-master/blog/management/commands/build_index.py @@ -0,0 +1,18 @@ +from django.core.management.base import BaseCommand + +from blog.documents import ElapsedTimeDocument, ArticleDocumentManager, ElaspedTimeDocumentManager, \ + ELASTICSEARCH_ENABLED + + +# TODO 参数化 +class Command(BaseCommand): + help = 'build search index' + + def handle(self, *args, **options): + if ELASTICSEARCH_ENABLED: + ElaspedTimeDocumentManager.build_index() + manager = ElapsedTimeDocument() + manager.init() + manager = ArticleDocumentManager() + manager.delete_index() + manager.rebuild() diff --git a/DjangoBlog-master/blog/management/commands/build_search_words.py b/DjangoBlog-master/blog/management/commands/build_search_words.py new file mode 100644 index 0000000..cfe7e0d --- /dev/null +++ b/DjangoBlog-master/blog/management/commands/build_search_words.py @@ -0,0 +1,13 @@ +from django.core.management.base import BaseCommand + +from blog.models import Tag, Category + + +# TODO 参数化 +class Command(BaseCommand): + help = 'build search words' + + def handle(self, *args, **options): + datas = set([t.name for t in Tag.objects.all()] + + [t.name for t in Category.objects.all()]) + print('\n'.join(datas)) diff --git a/DjangoBlog-master/blog/management/commands/clear_cache.py b/DjangoBlog-master/blog/management/commands/clear_cache.py new file mode 100644 index 0000000..0d66172 --- /dev/null +++ b/DjangoBlog-master/blog/management/commands/clear_cache.py @@ -0,0 +1,11 @@ +from django.core.management.base import BaseCommand + +from djangoblog.utils import cache + + +class Command(BaseCommand): + help = 'clear the whole cache' + + def handle(self, *args, **options): + cache.clear() + self.stdout.write(self.style.SUCCESS('Cleared cache\n')) diff --git a/DjangoBlog-master/blog/management/commands/create_testdata.py b/DjangoBlog-master/blog/management/commands/create_testdata.py new file mode 100644 index 0000000..675d2ba --- /dev/null +++ b/DjangoBlog-master/blog/management/commands/create_testdata.py @@ -0,0 +1,40 @@ +from django.contrib.auth import get_user_model +from django.contrib.auth.hashers import make_password +from django.core.management.base import BaseCommand + +from blog.models import Article, Tag, Category + + +class Command(BaseCommand): + help = 'create test datas' + + def handle(self, *args, **options): + user = get_user_model().objects.get_or_create( + email='test@test.com', username='测试用户', password=make_password('test!q@w#eTYU'))[0] + + pcategory = Category.objects.get_or_create( + name='我是父类目', parent_category=None)[0] + + category = Category.objects.get_or_create( + name='子类目', parent_category=pcategory)[0] + + category.save() + basetag = Tag() + basetag.name = "标签" + basetag.save() + for i in range(1, 20): + article = Article.objects.get_or_create( + category=category, + title='nice title ' + str(i), + body='nice content ' + str(i), + author=user)[0] + tag = Tag() + tag.name = "标签" + str(i) + tag.save() + article.tags.add(tag) + article.tags.add(basetag) + article.save() + + from djangoblog.utils import cache + cache.clear() + self.stdout.write(self.style.SUCCESS('created test datas \n')) diff --git a/DjangoBlog-master/blog/management/commands/ping_baidu.py b/DjangoBlog-master/blog/management/commands/ping_baidu.py new file mode 100644 index 0000000..2c7fbdd --- /dev/null +++ b/DjangoBlog-master/blog/management/commands/ping_baidu.py @@ -0,0 +1,50 @@ +from django.core.management.base import BaseCommand + +from djangoblog.spider_notify import SpiderNotify +from djangoblog.utils import get_current_site +from blog.models import Article, Tag, Category + +site = get_current_site().domain + + +class Command(BaseCommand): + help = 'notify baidu url' + + def add_arguments(self, parser): + parser.add_argument( + 'data_type', + type=str, + choices=[ + 'all', + 'article', + 'tag', + 'category'], + help='article : all article,tag : all tag,category: all category,all: All of these') + + def get_full_url(self, path): + url = "https://{site}{path}".format(site=site, path=path) + return url + + def handle(self, *args, **options): + type = options['data_type'] + self.stdout.write('start get %s' % type) + + urls = [] + if type == 'article' or type == 'all': + for article in Article.objects.filter(status='p'): + urls.append(article.get_full_url()) + if type == 'tag' or type == 'all': + for tag in Tag.objects.all(): + url = tag.get_absolute_url() + urls.append(self.get_full_url(url)) + if type == 'category' or type == 'all': + for category in Category.objects.all(): + url = category.get_absolute_url() + urls.append(self.get_full_url(url)) + + self.stdout.write( + self.style.SUCCESS( + 'start notify %d urls' % + len(urls))) + SpiderNotify.baidu_notify(urls) + self.stdout.write(self.style.SUCCESS('finish notify')) diff --git a/DjangoBlog-master/blog/management/commands/sync_user_avatar.py b/DjangoBlog-master/blog/management/commands/sync_user_avatar.py new file mode 100644 index 0000000..d0f4612 --- /dev/null +++ b/DjangoBlog-master/blog/management/commands/sync_user_avatar.py @@ -0,0 +1,47 @@ +import requests +from django.core.management.base import BaseCommand +from django.templatetags.static import static + +from djangoblog.utils import save_user_avatar +from oauth.models import OAuthUser +from oauth.oauthmanager import get_manager_by_type + + +class Command(BaseCommand): + help = 'sync user avatar' + + def test_picture(self, url): + try: + if requests.get(url, timeout=2).status_code == 200: + return True + except: + pass + + def handle(self, *args, **options): + static_url = static("../") + users = OAuthUser.objects.all() + self.stdout.write(f'开始同步{len(users)}个用户头像') + for u in users: + self.stdout.write(f'开始同步:{u.nickname}') + url = u.picture + if url: + if url.startswith(static_url): + if self.test_picture(url): + continue + else: + if u.metadata: + manage = get_manager_by_type(u.type) + url = manage.get_picture(u.metadata) + url = save_user_avatar(url) + else: + url = static('blog/img/avatar.png') + else: + url = save_user_avatar(url) + else: + url = static('blog/img/avatar.png') + if url: + self.stdout.write( + f'结束同步:{u.nickname}.url:{url}') + u.picture = url + u.save() + self.stdout.write('结束同步') diff --git a/DjangoBlog-master/blog/middleware.py b/DjangoBlog-master/blog/middleware.py new file mode 100644 index 0000000..94dd70c --- /dev/null +++ b/DjangoBlog-master/blog/middleware.py @@ -0,0 +1,42 @@ +import logging +import time + +from ipware import get_client_ip +from user_agents import parse + +from blog.documents import ELASTICSEARCH_ENABLED, ElaspedTimeDocumentManager + +logger = logging.getLogger(__name__) + + +class OnlineMiddleware(object): + def __init__(self, get_response=None): + self.get_response = get_response + super().__init__() + + def __call__(self, request): + ''' page render time ''' + start_time = time.time() + response = self.get_response(request) + http_user_agent = request.META.get('HTTP_USER_AGENT', '') + ip, _ = get_client_ip(request) + user_agent = parse(http_user_agent) + if not response.streaming: + try: + cast_time = time.time() - start_time + if ELASTICSEARCH_ENABLED: + time_taken = round((cast_time) * 1000, 2) + url = request.path + from django.utils import timezone + ElaspedTimeDocumentManager.create( + url=url, + time_taken=time_taken, + log_datetime=timezone.now(), + useragent=user_agent, + ip=ip) + response.content = response.content.replace( + b'', str.encode(str(cast_time)[:5])) + except Exception as e: + logger.error("Error OnlineMiddleware: %s" % e) + + return response diff --git a/DjangoBlog-master/blog/migrations/0001_initial.py b/DjangoBlog-master/blog/migrations/0001_initial.py new file mode 100644 index 0000000..3d391b6 --- /dev/null +++ b/DjangoBlog-master/blog/migrations/0001_initial.py @@ -0,0 +1,137 @@ +# Generated by Django 4.1.7 on 2023-03-02 07:14 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone +import mdeditor.fields + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='BlogSettings', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('sitename', models.CharField(default='', max_length=200, verbose_name='网站名称')), + ('site_description', models.TextField(default='', max_length=1000, verbose_name='网站描述')), + ('site_seo_description', models.TextField(default='', max_length=1000, verbose_name='网站SEO描述')), + ('site_keywords', models.TextField(default='', max_length=1000, verbose_name='网站关键字')), + ('article_sub_length', models.IntegerField(default=300, verbose_name='文章摘要长度')), + ('sidebar_article_count', models.IntegerField(default=10, verbose_name='侧边栏文章数目')), + ('sidebar_comment_count', models.IntegerField(default=5, verbose_name='侧边栏评论数目')), + ('article_comment_count', models.IntegerField(default=5, verbose_name='文章页面默认显示评论数目')), + ('show_google_adsense', models.BooleanField(default=False, verbose_name='是否显示谷歌广告')), + ('google_adsense_codes', models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='广告内容')), + ('open_site_comment', models.BooleanField(default=True, verbose_name='是否打开网站评论功能')), + ('beiancode', models.CharField(blank=True, default='', max_length=2000, null=True, verbose_name='备案号')), + ('analyticscode', models.TextField(default='', max_length=1000, verbose_name='网站统计代码')), + ('show_gongan_code', models.BooleanField(default=False, verbose_name='是否显示公安备案号')), + ('gongan_beiancode', models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='公安备案号')), + ], + options={ + 'verbose_name': '网站配置', + 'verbose_name_plural': '网站配置', + }, + ), + migrations.CreateModel( + name='Links', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=30, unique=True, verbose_name='链接名称')), + ('link', models.URLField(verbose_name='链接地址')), + ('sequence', models.IntegerField(unique=True, verbose_name='排序')), + ('is_enable', models.BooleanField(default=True, verbose_name='是否显示')), + ('show_type', models.CharField(choices=[('i', '首页'), ('l', '列表页'), ('p', '文章页面'), ('a', '全站'), ('s', '友情链接页面')], default='i', max_length=1, verbose_name='显示类型')), + ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')), + ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')), + ], + options={ + 'verbose_name': '友情链接', + 'verbose_name_plural': '友情链接', + 'ordering': ['sequence'], + }, + ), + migrations.CreateModel( + name='SideBar', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100, verbose_name='标题')), + ('content', models.TextField(verbose_name='内容')), + ('sequence', models.IntegerField(unique=True, verbose_name='排序')), + ('is_enable', models.BooleanField(default=True, verbose_name='是否启用')), + ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')), + ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')), + ], + options={ + 'verbose_name': '侧边栏', + 'verbose_name_plural': '侧边栏', + 'ordering': ['sequence'], + }, + ), + migrations.CreateModel( + name='Tag', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')), + ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')), + ('name', models.CharField(max_length=30, unique=True, verbose_name='标签名')), + ('slug', models.SlugField(blank=True, default='no-slug', max_length=60)), + ], + options={ + 'verbose_name': '标签', + 'verbose_name_plural': '标签', + 'ordering': ['name'], + }, + ), + migrations.CreateModel( + name='Category', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')), + ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')), + ('name', models.CharField(max_length=30, unique=True, verbose_name='分类名')), + ('slug', models.SlugField(blank=True, default='no-slug', max_length=60)), + ('index', models.IntegerField(default=0, verbose_name='权重排序-越大越靠前')), + ('parent_category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='父级分类')), + ], + options={ + 'verbose_name': '分类', + 'verbose_name_plural': '分类', + 'ordering': ['-index'], + }, + ), + migrations.CreateModel( + name='Article', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')), + ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')), + ('title', models.CharField(max_length=200, unique=True, verbose_name='标题')), + ('body', mdeditor.fields.MDTextField(verbose_name='正文')), + ('pub_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='发布时间')), + ('status', models.CharField(choices=[('d', '草稿'), ('p', '发表')], default='p', max_length=1, verbose_name='文章状态')), + ('comment_status', models.CharField(choices=[('o', '打开'), ('c', '关闭')], default='o', max_length=1, verbose_name='评论状态')), + ('type', models.CharField(choices=[('a', '文章'), ('p', '页面')], default='a', max_length=1, verbose_name='类型')), + ('views', models.PositiveIntegerField(default=0, verbose_name='浏览量')), + ('article_order', models.IntegerField(default=0, verbose_name='排序,数字越大越靠前')), + ('show_toc', models.BooleanField(default=False, verbose_name='是否显示toc目录')), + ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='作者')), + ('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='分类')), + ('tags', models.ManyToManyField(blank=True, to='blog.tag', verbose_name='标签集合')), + ], + options={ + 'verbose_name': '文章', + 'verbose_name_plural': '文章', + 'ordering': ['-article_order', '-pub_time'], + 'get_latest_by': 'id', + }, + ), + ] diff --git a/DjangoBlog-master/blog/migrations/0002_blogsettings_global_footer_and_more.py b/DjangoBlog-master/blog/migrations/0002_blogsettings_global_footer_and_more.py new file mode 100644 index 0000000..adbaa36 --- /dev/null +++ b/DjangoBlog-master/blog/migrations/0002_blogsettings_global_footer_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 4.1.7 on 2023-03-29 06:08 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('blog', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='blogsettings', + name='global_footer', + field=models.TextField(blank=True, default='', null=True, verbose_name='公共尾部'), + ), + migrations.AddField( + model_name='blogsettings', + name='global_header', + field=models.TextField(blank=True, default='', null=True, verbose_name='公共头部'), + ), + ] diff --git a/DjangoBlog-master/blog/migrations/0003_blogsettings_comment_need_review.py b/DjangoBlog-master/blog/migrations/0003_blogsettings_comment_need_review.py new file mode 100644 index 0000000..e9f5502 --- /dev/null +++ b/DjangoBlog-master/blog/migrations/0003_blogsettings_comment_need_review.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.1 on 2023-05-09 07:45 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ('blog', '0002_blogsettings_global_footer_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='blogsettings', + name='comment_need_review', + field=models.BooleanField(default=False, verbose_name='评论是否需要审核'), + ), + ] diff --git a/DjangoBlog-master/blog/migrations/0004_rename_analyticscode_blogsettings_analytics_code_and_more.py b/DjangoBlog-master/blog/migrations/0004_rename_analyticscode_blogsettings_analytics_code_and_more.py new file mode 100644 index 0000000..ceb1398 --- /dev/null +++ b/DjangoBlog-master/blog/migrations/0004_rename_analyticscode_blogsettings_analytics_code_and_more.py @@ -0,0 +1,27 @@ +# Generated by Django 4.2.1 on 2023-05-09 07:51 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ('blog', '0003_blogsettings_comment_need_review'), + ] + + operations = [ + migrations.RenameField( + model_name='blogsettings', + old_name='analyticscode', + new_name='analytics_code', + ), + migrations.RenameField( + model_name='blogsettings', + old_name='beiancode', + new_name='beian_code', + ), + migrations.RenameField( + model_name='blogsettings', + old_name='sitename', + new_name='site_name', + ), + ] diff --git a/DjangoBlog-master/blog/migrations/0005_alter_article_options_alter_category_options_and_more.py b/DjangoBlog-master/blog/migrations/0005_alter_article_options_alter_category_options_and_more.py new file mode 100644 index 0000000..d08e853 --- /dev/null +++ b/DjangoBlog-master/blog/migrations/0005_alter_article_options_alter_category_options_and_more.py @@ -0,0 +1,300 @@ +# Generated by Django 4.2.5 on 2023-09-06 13:13 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone +import mdeditor.fields + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('blog', '0004_rename_analyticscode_blogsettings_analytics_code_and_more'), + ] + + operations = [ + migrations.AlterModelOptions( + name='article', + options={'get_latest_by': 'id', 'ordering': ['-article_order', '-pub_time'], 'verbose_name': 'article', 'verbose_name_plural': 'article'}, + ), + migrations.AlterModelOptions( + name='category', + options={'ordering': ['-index'], 'verbose_name': 'category', 'verbose_name_plural': 'category'}, + ), + migrations.AlterModelOptions( + name='links', + options={'ordering': ['sequence'], 'verbose_name': 'link', 'verbose_name_plural': 'link'}, + ), + migrations.AlterModelOptions( + name='sidebar', + options={'ordering': ['sequence'], 'verbose_name': 'sidebar', 'verbose_name_plural': 'sidebar'}, + ), + migrations.AlterModelOptions( + name='tag', + options={'ordering': ['name'], 'verbose_name': 'tag', 'verbose_name_plural': 'tag'}, + ), + migrations.RemoveField( + model_name='article', + name='created_time', + ), + migrations.RemoveField( + model_name='article', + name='last_mod_time', + ), + migrations.RemoveField( + model_name='category', + name='created_time', + ), + migrations.RemoveField( + model_name='category', + name='last_mod_time', + ), + migrations.RemoveField( + model_name='links', + name='created_time', + ), + migrations.RemoveField( + model_name='sidebar', + name='created_time', + ), + migrations.RemoveField( + model_name='tag', + name='created_time', + ), + migrations.RemoveField( + model_name='tag', + name='last_mod_time', + ), + migrations.AddField( + model_name='article', + name='creation_time', + field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'), + ), + migrations.AddField( + model_name='article', + name='last_modify_time', + field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'), + ), + migrations.AddField( + model_name='category', + name='creation_time', + field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'), + ), + migrations.AddField( + model_name='category', + name='last_modify_time', + field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'), + ), + migrations.AddField( + model_name='links', + name='creation_time', + field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'), + ), + migrations.AddField( + model_name='sidebar', + name='creation_time', + field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'), + ), + migrations.AddField( + model_name='tag', + name='creation_time', + field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'), + ), + migrations.AddField( + model_name='tag', + name='last_modify_time', + field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'), + ), + migrations.AlterField( + model_name='article', + name='article_order', + field=models.IntegerField(default=0, verbose_name='order'), + ), + migrations.AlterField( + model_name='article', + name='author', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='author'), + ), + migrations.AlterField( + model_name='article', + name='body', + field=mdeditor.fields.MDTextField(verbose_name='body'), + ), + migrations.AlterField( + model_name='article', + name='category', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='category'), + ), + migrations.AlterField( + model_name='article', + name='comment_status', + field=models.CharField(choices=[('o', 'Open'), ('c', 'Close')], default='o', max_length=1, verbose_name='comment status'), + ), + migrations.AlterField( + model_name='article', + name='pub_time', + field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='publish time'), + ), + migrations.AlterField( + model_name='article', + name='show_toc', + field=models.BooleanField(default=False, verbose_name='show toc'), + ), + migrations.AlterField( + model_name='article', + name='status', + field=models.CharField(choices=[('d', 'Draft'), ('p', 'Published')], default='p', max_length=1, verbose_name='status'), + ), + migrations.AlterField( + model_name='article', + name='tags', + field=models.ManyToManyField(blank=True, to='blog.tag', verbose_name='tag'), + ), + migrations.AlterField( + model_name='article', + name='title', + field=models.CharField(max_length=200, unique=True, verbose_name='title'), + ), + migrations.AlterField( + model_name='article', + name='type', + field=models.CharField(choices=[('a', 'Article'), ('p', 'Page')], default='a', max_length=1, verbose_name='type'), + ), + migrations.AlterField( + model_name='article', + name='views', + field=models.PositiveIntegerField(default=0, verbose_name='views'), + ), + migrations.AlterField( + model_name='blogsettings', + name='article_comment_count', + field=models.IntegerField(default=5, verbose_name='article comment count'), + ), + migrations.AlterField( + model_name='blogsettings', + name='article_sub_length', + field=models.IntegerField(default=300, verbose_name='article sub length'), + ), + migrations.AlterField( + model_name='blogsettings', + name='google_adsense_codes', + field=models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='adsense code'), + ), + migrations.AlterField( + model_name='blogsettings', + name='open_site_comment', + field=models.BooleanField(default=True, verbose_name='open site comment'), + ), + migrations.AlterField( + model_name='blogsettings', + name='show_google_adsense', + field=models.BooleanField(default=False, verbose_name='show adsense'), + ), + migrations.AlterField( + model_name='blogsettings', + name='sidebar_article_count', + field=models.IntegerField(default=10, verbose_name='sidebar article count'), + ), + migrations.AlterField( + model_name='blogsettings', + name='sidebar_comment_count', + field=models.IntegerField(default=5, verbose_name='sidebar comment count'), + ), + migrations.AlterField( + model_name='blogsettings', + name='site_description', + field=models.TextField(default='', max_length=1000, verbose_name='site description'), + ), + migrations.AlterField( + model_name='blogsettings', + name='site_keywords', + field=models.TextField(default='', max_length=1000, verbose_name='site keywords'), + ), + migrations.AlterField( + model_name='blogsettings', + name='site_name', + field=models.CharField(default='', max_length=200, verbose_name='site name'), + ), + migrations.AlterField( + model_name='blogsettings', + name='site_seo_description', + field=models.TextField(default='', max_length=1000, verbose_name='site seo description'), + ), + migrations.AlterField( + model_name='category', + name='index', + field=models.IntegerField(default=0, verbose_name='index'), + ), + migrations.AlterField( + model_name='category', + name='name', + field=models.CharField(max_length=30, unique=True, verbose_name='category name'), + ), + migrations.AlterField( + model_name='category', + name='parent_category', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='parent category'), + ), + migrations.AlterField( + model_name='links', + name='is_enable', + field=models.BooleanField(default=True, verbose_name='is show'), + ), + migrations.AlterField( + model_name='links', + name='last_mod_time', + field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'), + ), + migrations.AlterField( + model_name='links', + name='link', + field=models.URLField(verbose_name='link'), + ), + migrations.AlterField( + model_name='links', + name='name', + field=models.CharField(max_length=30, unique=True, verbose_name='link name'), + ), + migrations.AlterField( + model_name='links', + name='sequence', + field=models.IntegerField(unique=True, verbose_name='order'), + ), + migrations.AlterField( + model_name='links', + name='show_type', + field=models.CharField(choices=[('i', 'index'), ('l', 'list'), ('p', 'post'), ('a', 'all'), ('s', 'slide')], default='i', max_length=1, verbose_name='show type'), + ), + migrations.AlterField( + model_name='sidebar', + name='content', + field=models.TextField(verbose_name='content'), + ), + migrations.AlterField( + model_name='sidebar', + name='is_enable', + field=models.BooleanField(default=True, verbose_name='is enable'), + ), + migrations.AlterField( + model_name='sidebar', + name='last_mod_time', + field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'), + ), + migrations.AlterField( + model_name='sidebar', + name='name', + field=models.CharField(max_length=100, verbose_name='title'), + ), + migrations.AlterField( + model_name='sidebar', + name='sequence', + field=models.IntegerField(unique=True, verbose_name='order'), + ), + migrations.AlterField( + model_name='tag', + name='name', + field=models.CharField(max_length=30, unique=True, verbose_name='tag name'), + ), + ] diff --git a/DjangoBlog-master/blog/migrations/0006_alter_blogsettings_options.py b/DjangoBlog-master/blog/migrations/0006_alter_blogsettings_options.py new file mode 100644 index 0000000..e36feb4 --- /dev/null +++ b/DjangoBlog-master/blog/migrations/0006_alter_blogsettings_options.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.7 on 2024-01-26 02:41 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('blog', '0005_alter_article_options_alter_category_options_and_more'), + ] + + operations = [ + migrations.AlterModelOptions( + name='blogsettings', + options={'verbose_name': 'Website configuration', 'verbose_name_plural': 'Website configuration'}, + ), + ] diff --git a/DjangoBlog-master/blog/migrations/__init__.py b/DjangoBlog-master/blog/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/DjangoBlog-master/blog/models.py b/DjangoBlog-master/blog/models.py new file mode 100644 index 0000000..083788b --- /dev/null +++ b/DjangoBlog-master/blog/models.py @@ -0,0 +1,376 @@ +import logging +import re +from abc import abstractmethod + +from django.conf import settings +from django.core.exceptions import ValidationError +from django.db import models +from django.urls import reverse +from django.utils.timezone import now +from django.utils.translation import gettext_lazy as _ +from mdeditor.fields import MDTextField +from uuslug import slugify + +from djangoblog.utils import cache_decorator, cache +from djangoblog.utils import get_current_site + +logger = logging.getLogger(__name__) + + +class LinkShowType(models.TextChoices): + I = ('i', _('index')) + L = ('l', _('list')) + P = ('p', _('post')) + A = ('a', _('all')) + S = ('s', _('slide')) + + +class BaseModel(models.Model): + id = models.AutoField(primary_key=True) + creation_time = models.DateTimeField(_('creation time'), default=now) + last_modify_time = models.DateTimeField(_('modify time'), default=now) + + def save(self, *args, **kwargs): + is_update_views = isinstance( + self, + Article) and 'update_fields' in kwargs and kwargs['update_fields'] == ['views'] + if is_update_views: + Article.objects.filter(pk=self.pk).update(views=self.views) + else: + if 'slug' in self.__dict__: + slug = getattr( + self, 'title') if 'title' in self.__dict__ else getattr( + self, 'name') + setattr(self, 'slug', slugify(slug)) + super().save(*args, **kwargs) + + def get_full_url(self): + site = get_current_site().domain + url = "https://{site}{path}".format(site=site, + path=self.get_absolute_url()) + return url + + class Meta: + abstract = True + + @abstractmethod + def get_absolute_url(self): + pass + + +class Article(BaseModel): + """文章""" + STATUS_CHOICES = ( + ('d', _('Draft')), + ('p', _('Published')), + ) + COMMENT_STATUS = ( + ('o', _('Open')), + ('c', _('Close')), + ) + TYPE = ( + ('a', _('Article')), + ('p', _('Page')), + ) + title = models.CharField(_('title'), max_length=200, unique=True) + body = MDTextField(_('body')) + pub_time = models.DateTimeField( + _('publish time'), blank=False, null=False, default=now) + status = models.CharField( + _('status'), + max_length=1, + choices=STATUS_CHOICES, + default='p') + comment_status = models.CharField( + _('comment status'), + max_length=1, + choices=COMMENT_STATUS, + default='o') + type = models.CharField(_('type'), max_length=1, choices=TYPE, default='a') + views = models.PositiveIntegerField(_('views'), default=0) + author = models.ForeignKey( + settings.AUTH_USER_MODEL, + verbose_name=_('author'), + blank=False, + null=False, + on_delete=models.CASCADE) + article_order = models.IntegerField( + _('order'), blank=False, null=False, default=0) + show_toc = models.BooleanField(_('show toc'), blank=False, null=False, default=False) + category = models.ForeignKey( + 'Category', + verbose_name=_('category'), + on_delete=models.CASCADE, + blank=False, + null=False) + tags = models.ManyToManyField('Tag', verbose_name=_('tag'), blank=True) + + def body_to_string(self): + return self.body + + def __str__(self): + return self.title + + class Meta: + ordering = ['-article_order', '-pub_time'] + verbose_name = _('article') + verbose_name_plural = verbose_name + get_latest_by = 'id' + + def get_absolute_url(self): + return reverse('blog:detailbyid', kwargs={ + 'article_id': self.id, + 'year': self.creation_time.year, + 'month': self.creation_time.month, + 'day': self.creation_time.day + }) + + @cache_decorator(60 * 60 * 10) + def get_category_tree(self): + tree = self.category.get_category_tree() + names = list(map(lambda c: (c.name, c.get_absolute_url()), tree)) + + return names + + def save(self, *args, **kwargs): + super().save(*args, **kwargs) + + def viewed(self): + self.views += 1 + self.save(update_fields=['views']) + + def comment_list(self): + cache_key = 'article_comments_{id}'.format(id=self.id) + value = cache.get(cache_key) + if value: + logger.info('get article comments:{id}'.format(id=self.id)) + return value + else: + comments = self.comment_set.filter(is_enable=True).order_by('-id') + cache.set(cache_key, comments, 60 * 100) + logger.info('set article comments:{id}'.format(id=self.id)) + return comments + + def get_admin_url(self): + info = (self._meta.app_label, self._meta.model_name) + return reverse('admin:%s_%s_change' % info, args=(self.pk,)) + + @cache_decorator(expiration=60 * 100) + def next_article(self): + # 下一篇 + return Article.objects.filter( + id__gt=self.id, status='p').order_by('id').first() + + @cache_decorator(expiration=60 * 100) + def prev_article(self): + # 前一篇 + return Article.objects.filter(id__lt=self.id, status='p').first() + + def get_first_image_url(self): + """ + Get the first image url from article.body. + :return: + """ + match = re.search(r'!\[.*?\]\((.+?)\)', self.body) + if match: + return match.group(1) + return "" + + +class Category(BaseModel): + """文章分类""" + name = models.CharField(_('category name'), max_length=30, unique=True) + parent_category = models.ForeignKey( + 'self', + verbose_name=_('parent category'), + blank=True, + null=True, + on_delete=models.CASCADE) + slug = models.SlugField(default='no-slug', max_length=60, blank=True) + index = models.IntegerField(default=0, verbose_name=_('index')) + + class Meta: + ordering = ['-index'] + verbose_name = _('category') + verbose_name_plural = verbose_name + + def get_absolute_url(self): + return reverse( + 'blog:category_detail', kwargs={ + 'category_name': self.slug}) + + def __str__(self): + return self.name + + @cache_decorator(60 * 60 * 10) + def get_category_tree(self): + """ + 递归获得分类目录的父级 + :return: + """ + categorys = [] + + def parse(category): + categorys.append(category) + if category.parent_category: + parse(category.parent_category) + + parse(self) + return categorys + + @cache_decorator(60 * 60 * 10) + def get_sub_categorys(self): + """ + 获得当前分类目录所有子集 + :return: + """ + categorys = [] + all_categorys = Category.objects.all() + + def parse(category): + if category not in categorys: + categorys.append(category) + childs = all_categorys.filter(parent_category=category) + for child in childs: + if category not in categorys: + categorys.append(child) + parse(child) + + parse(self) + return categorys + + +class Tag(BaseModel): + """文章标签""" + name = models.CharField(_('tag name'), max_length=30, unique=True) + slug = models.SlugField(default='no-slug', max_length=60, blank=True) + + def __str__(self): + return self.name + + def get_absolute_url(self): + return reverse('blog:tag_detail', kwargs={'tag_name': self.slug}) + + @cache_decorator(60 * 60 * 10) + def get_article_count(self): + return Article.objects.filter(tags__name=self.name).distinct().count() + + class Meta: + ordering = ['name'] + verbose_name = _('tag') + verbose_name_plural = verbose_name + + +class Links(models.Model): + """友情链接""" + + name = models.CharField(_('link name'), max_length=30, unique=True) + link = models.URLField(_('link')) + sequence = models.IntegerField(_('order'), unique=True) + is_enable = models.BooleanField( + _('is show'), default=True, blank=False, null=False) + show_type = models.CharField( + _('show type'), + max_length=1, + choices=LinkShowType.choices, + default=LinkShowType.I) + creation_time = models.DateTimeField(_('creation time'), default=now) + last_mod_time = models.DateTimeField(_('modify time'), default=now) + + class Meta: + ordering = ['sequence'] + verbose_name = _('link') + verbose_name_plural = verbose_name + + def __str__(self): + return self.name + + +class SideBar(models.Model): + """侧边栏,可以展示一些html内容""" + name = models.CharField(_('title'), max_length=100) + content = models.TextField(_('content')) + sequence = models.IntegerField(_('order'), unique=True) + is_enable = models.BooleanField(_('is enable'), default=True) + creation_time = models.DateTimeField(_('creation time'), default=now) + last_mod_time = models.DateTimeField(_('modify time'), default=now) + + class Meta: + ordering = ['sequence'] + verbose_name = _('sidebar') + verbose_name_plural = verbose_name + + def __str__(self): + return self.name + + +class BlogSettings(models.Model): + """blog的配置""" + site_name = models.CharField( + _('site name'), + max_length=200, + null=False, + blank=False, + default='') + site_description = models.TextField( + _('site description'), + max_length=1000, + null=False, + blank=False, + default='') + site_seo_description = models.TextField( + _('site seo description'), max_length=1000, null=False, blank=False, default='') + site_keywords = models.TextField( + _('site keywords'), + max_length=1000, + null=False, + blank=False, + default='') + article_sub_length = models.IntegerField(_('article sub length'), default=300) + sidebar_article_count = models.IntegerField(_('sidebar article count'), default=10) + sidebar_comment_count = models.IntegerField(_('sidebar comment count'), default=5) + article_comment_count = models.IntegerField(_('article comment count'), default=5) + show_google_adsense = models.BooleanField(_('show adsense'), default=False) + google_adsense_codes = models.TextField( + _('adsense code'), max_length=2000, null=True, blank=True, default='') + open_site_comment = models.BooleanField(_('open site comment'), default=True) + global_header = models.TextField("公共头部", null=True, blank=True, default='') + global_footer = models.TextField("公共尾部", null=True, blank=True, default='') + beian_code = models.CharField( + '备案号', + max_length=2000, + null=True, + blank=True, + default='') + analytics_code = models.TextField( + "网站统计代码", + max_length=1000, + null=False, + blank=False, + default='') + show_gongan_code = models.BooleanField( + '是否显示公安备案号', default=False, null=False) + gongan_beiancode = models.TextField( + '公安备案号', + max_length=2000, + null=True, + blank=True, + default='') + comment_need_review = models.BooleanField( + '评论是否需要审核', default=False, null=False) + + class Meta: + verbose_name = _('Website configuration') + verbose_name_plural = verbose_name + + def __str__(self): + return self.site_name + + def clean(self): + if BlogSettings.objects.exclude(id=self.id).count(): + raise ValidationError(_('There can only be one configuration')) + + def save(self, *args, **kwargs): + super().save(*args, **kwargs) + from djangoblog.utils import cache + cache.clear() diff --git a/DjangoBlog-master/blog/search_indexes.py b/DjangoBlog-master/blog/search_indexes.py new file mode 100644 index 0000000..7f1dfac --- /dev/null +++ b/DjangoBlog-master/blog/search_indexes.py @@ -0,0 +1,13 @@ +from haystack import indexes + +from blog.models import Article + + +class ArticleIndex(indexes.SearchIndex, indexes.Indexable): + text = indexes.CharField(document=True, use_template=True) + + def get_model(self): + return Article + + def index_queryset(self, using=None): + return self.get_model().objects.filter(status='p') diff --git a/DjangoBlog-master/blog/templatetags/__init__.py b/DjangoBlog-master/blog/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/DjangoBlog-master/blog/templatetags/blog_tags.py b/DjangoBlog-master/blog/templatetags/blog_tags.py new file mode 100644 index 0000000..d6cd5d5 --- /dev/null +++ b/DjangoBlog-master/blog/templatetags/blog_tags.py @@ -0,0 +1,344 @@ +import hashlib +import logging +import random +import urllib + +from django import template +from django.conf import settings +from django.db.models import Q +from django.shortcuts import get_object_or_404 +from django.template.defaultfilters import stringfilter +from django.templatetags.static import static +from django.urls import reverse +from django.utils.safestring import mark_safe + +from blog.models import Article, Category, Tag, Links, SideBar, LinkShowType +from comments.models import Comment +from djangoblog.utils import CommonMarkdown, sanitize_html +from djangoblog.utils import cache +from djangoblog.utils import get_current_site +from oauth.models import OAuthUser +from djangoblog.plugin_manage import hooks + +logger = logging.getLogger(__name__) + +register = template.Library() + + +@register.simple_tag(takes_context=True) +def head_meta(context): + return mark_safe(hooks.apply_filters('head_meta', '', context)) + + +@register.simple_tag +def timeformat(data): + try: + return data.strftime(settings.TIME_FORMAT) + except Exception as e: + logger.error(e) + return "" + + +@register.simple_tag +def datetimeformat(data): + try: + return data.strftime(settings.DATE_TIME_FORMAT) + except Exception as e: + logger.error(e) + return "" + + +@register.filter() +@stringfilter +def custom_markdown(content): + return mark_safe(CommonMarkdown.get_markdown(content)) + + +@register.simple_tag +def get_markdown_toc(content): + from djangoblog.utils import CommonMarkdown + body, toc = CommonMarkdown.get_markdown_with_toc(content) + return mark_safe(toc) + + +@register.filter() +@stringfilter +def comment_markdown(content): + content = CommonMarkdown.get_markdown(content) + return mark_safe(sanitize_html(content)) + + +@register.filter(is_safe=True) +@stringfilter +def truncatechars_content(content): + """ + 获得文章内容的摘要 + :param content: + :return: + """ + from django.template.defaultfilters import truncatechars_html + from djangoblog.utils import get_blog_setting + blogsetting = get_blog_setting() + return truncatechars_html(content, blogsetting.article_sub_length) + + +@register.filter(is_safe=True) +@stringfilter +def truncate(content): + from django.utils.html import strip_tags + + return strip_tags(content)[:150] + + +@register.inclusion_tag('blog/tags/breadcrumb.html') +def load_breadcrumb(article): + """ + 获得文章面包屑 + :param article: + :return: + """ + names = article.get_category_tree() + from djangoblog.utils import get_blog_setting + blogsetting = get_blog_setting() + site = get_current_site().domain + names.append((blogsetting.site_name, '/')) + names = names[::-1] + + return { + 'names': names, + 'title': article.title, + 'count': len(names) + 1 + } + + +@register.inclusion_tag('blog/tags/article_tag_list.html') +def load_articletags(article): + """ + 文章标签 + :param article: + :return: + """ + tags = article.tags.all() + tags_list = [] + for tag in tags: + url = tag.get_absolute_url() + count = tag.get_article_count() + tags_list.append(( + url, count, tag, random.choice(settings.BOOTSTRAP_COLOR_TYPES) + )) + return { + 'article_tags_list': tags_list + } + + +@register.inclusion_tag('blog/tags/sidebar.html') +def load_sidebar(user, linktype): + """ + 加载侧边栏 + :return: + """ + value = cache.get("sidebar" + linktype) + if value: + value['user'] = user + return value + else: + logger.info('load sidebar') + from djangoblog.utils import get_blog_setting + blogsetting = get_blog_setting() + recent_articles = Article.objects.filter( + status='p')[:blogsetting.sidebar_article_count] + sidebar_categorys = Category.objects.all() + extra_sidebars = SideBar.objects.filter( + is_enable=True).order_by('sequence') + most_read_articles = Article.objects.filter(status='p').order_by( + '-views')[:blogsetting.sidebar_article_count] + dates = Article.objects.datetimes('creation_time', 'month', order='DESC') + links = Links.objects.filter(is_enable=True).filter( + Q(show_type=str(linktype)) | Q(show_type=LinkShowType.A)) + commment_list = Comment.objects.filter(is_enable=True).order_by( + '-id')[:blogsetting.sidebar_comment_count] + # 标签云 计算字体大小 + # 根据总数计算出平均值 大小为 (数目/平均值)*步长 + increment = 5 + tags = Tag.objects.all() + sidebar_tags = None + if tags and len(tags) > 0: + s = [t for t in [(t, t.get_article_count()) for t in tags] if t[1]] + count = sum([t[1] for t in s]) + dd = 1 if (count == 0 or not len(tags)) else count / len(tags) + import random + sidebar_tags = list( + map(lambda x: (x[0], x[1], (x[1] / dd) * increment + 10), s)) + random.shuffle(sidebar_tags) + + value = { + 'recent_articles': recent_articles, + 'sidebar_categorys': sidebar_categorys, + 'most_read_articles': most_read_articles, + 'article_dates': dates, + 'sidebar_comments': commment_list, + 'sidabar_links': links, + 'show_google_adsense': blogsetting.show_google_adsense, + 'google_adsense_codes': blogsetting.google_adsense_codes, + 'open_site_comment': blogsetting.open_site_comment, + 'show_gongan_code': blogsetting.show_gongan_code, + 'sidebar_tags': sidebar_tags, + 'extra_sidebars': extra_sidebars + } + cache.set("sidebar" + linktype, value, 60 * 60 * 60 * 3) + logger.info('set sidebar cache.key:{key}'.format(key="sidebar" + linktype)) + value['user'] = user + return value + + +@register.inclusion_tag('blog/tags/article_meta_info.html') +def load_article_metas(article, user): + """ + 获得文章meta信息 + :param article: + :return: + """ + return { + 'article': article, + 'user': user + } + + +@register.inclusion_tag('blog/tags/article_pagination.html') +def load_pagination_info(page_obj, page_type, tag_name): + previous_url = '' + next_url = '' + if page_type == '': + if page_obj.has_next(): + next_number = page_obj.next_page_number() + next_url = reverse('blog:index_page', kwargs={'page': next_number}) + if page_obj.has_previous(): + previous_number = page_obj.previous_page_number() + previous_url = reverse( + 'blog:index_page', kwargs={ + 'page': previous_number}) + if page_type == '分类标签归档': + tag = get_object_or_404(Tag, name=tag_name) + if page_obj.has_next(): + next_number = page_obj.next_page_number() + next_url = reverse( + 'blog:tag_detail_page', + kwargs={ + 'page': next_number, + 'tag_name': tag.slug}) + if page_obj.has_previous(): + previous_number = page_obj.previous_page_number() + previous_url = reverse( + 'blog:tag_detail_page', + kwargs={ + 'page': previous_number, + 'tag_name': tag.slug}) + if page_type == '作者文章归档': + if page_obj.has_next(): + next_number = page_obj.next_page_number() + next_url = reverse( + 'blog:author_detail_page', + kwargs={ + 'page': next_number, + 'author_name': tag_name}) + if page_obj.has_previous(): + previous_number = page_obj.previous_page_number() + previous_url = reverse( + 'blog:author_detail_page', + kwargs={ + 'page': previous_number, + 'author_name': tag_name}) + + if page_type == '分类目录归档': + category = get_object_or_404(Category, name=tag_name) + if page_obj.has_next(): + next_number = page_obj.next_page_number() + next_url = reverse( + 'blog:category_detail_page', + kwargs={ + 'page': next_number, + 'category_name': category.slug}) + if page_obj.has_previous(): + previous_number = page_obj.previous_page_number() + previous_url = reverse( + 'blog:category_detail_page', + kwargs={ + 'page': previous_number, + 'category_name': category.slug}) + + return { + 'previous_url': previous_url, + 'next_url': next_url, + 'page_obj': page_obj + } + + +@register.inclusion_tag('blog/tags/article_info.html') +def load_article_detail(article, isindex, user): + """ + 加载文章详情 + :param article: + :param isindex:是否列表页,若是列表页只显示摘要 + :return: + """ + from djangoblog.utils import get_blog_setting + blogsetting = get_blog_setting() + + return { + 'article': article, + 'isindex': isindex, + 'user': user, + 'open_site_comment': blogsetting.open_site_comment, + } + + +# return only the URL of the gravatar +# TEMPLATE USE: {{ email|gravatar_url:150 }} +@register.filter +def gravatar_url(email, size=40): + """获得gravatar头像""" + cachekey = 'gravatat/' + email + url = cache.get(cachekey) + if url: + return url + else: + usermodels = OAuthUser.objects.filter(email=email) + if usermodels: + o = list(filter(lambda x: x.picture is not None, usermodels)) + if o: + return o[0].picture + email = email.encode('utf-8') + + default = static('blog/img/avatar.png') + + url = "https://www.gravatar.com/avatar/%s?%s" % (hashlib.md5( + email.lower()).hexdigest(), urllib.parse.urlencode({'d': default, 's': str(size)})) + cache.set(cachekey, url, 60 * 60 * 10) + logger.info('set gravatar cache.key:{key}'.format(key=cachekey)) + return url + + +@register.filter +def gravatar(email, size=40): + """获得gravatar头像""" + url = gravatar_url(email, size) + return mark_safe( + '' % + (url, size, size)) + + +@register.simple_tag +def query(qs, **kwargs): + """ template tag which allows queryset filtering. Usage: + {% query books author=author as mybooks %} + {% for book in mybooks %} + ... + {% endfor %} + """ + return qs.filter(**kwargs) + + +@register.filter +def addstr(arg1, arg2): + """concatenate arg1 & arg2""" + return str(arg1) + str(arg2) diff --git a/DjangoBlog-master/blog/tests.py b/DjangoBlog-master/blog/tests.py new file mode 100644 index 0000000..ee13505 --- /dev/null +++ b/DjangoBlog-master/blog/tests.py @@ -0,0 +1,232 @@ +import os + +from django.conf import settings +from django.core.files.uploadedfile import SimpleUploadedFile +from django.core.management import call_command +from django.core.paginator import Paginator +from django.templatetags.static import static +from django.test import Client, RequestFactory, TestCase +from django.urls import reverse +from django.utils import timezone + +from accounts.models import BlogUser +from blog.forms import BlogSearchForm +from blog.models import Article, Category, Tag, SideBar, Links +from blog.templatetags.blog_tags import load_pagination_info, load_articletags +from djangoblog.utils import get_current_site, get_sha256 +from oauth.models import OAuthUser, OAuthConfig + + +# Create your tests here. + +class ArticleTest(TestCase): + def setUp(self): + self.client = Client() + self.factory = RequestFactory() + + def test_validate_article(self): + site = get_current_site().domain + user = BlogUser.objects.get_or_create( + email="liangliangyy@gmail.com", + username="liangliangyy")[0] + user.set_password("liangliangyy") + user.is_staff = True + user.is_superuser = True + user.save() + response = self.client.get(user.get_absolute_url()) + self.assertEqual(response.status_code, 200) + response = self.client.get('/admin/servermanager/emailsendlog/') + response = self.client.get('admin/admin/logentry/') + s = SideBar() + s.sequence = 1 + s.name = 'test' + s.content = 'test content' + s.is_enable = True + s.save() + + category = Category() + category.name = "category" + category.creation_time = timezone.now() + category.last_mod_time = timezone.now() + category.save() + + tag = Tag() + tag.name = "nicetag" + tag.save() + + article = Article() + article.title = "nicetitle" + article.body = "nicecontent" + article.author = user + article.category = category + article.type = 'a' + article.status = 'p' + + article.save() + self.assertEqual(0, article.tags.count()) + article.tags.add(tag) + article.save() + self.assertEqual(1, article.tags.count()) + + for i in range(20): + article = Article() + article.title = "nicetitle" + str(i) + article.body = "nicetitle" + str(i) + article.author = user + article.category = category + article.type = 'a' + article.status = 'p' + article.save() + article.tags.add(tag) + article.save() + from blog.documents import ELASTICSEARCH_ENABLED + if ELASTICSEARCH_ENABLED: + call_command("build_index") + response = self.client.get('/search', {'q': 'nicetitle'}) + self.assertEqual(response.status_code, 200) + + response = self.client.get(article.get_absolute_url()) + self.assertEqual(response.status_code, 200) + from djangoblog.spider_notify import SpiderNotify + SpiderNotify.notify(article.get_absolute_url()) + response = self.client.get(tag.get_absolute_url()) + self.assertEqual(response.status_code, 200) + + response = self.client.get(category.get_absolute_url()) + self.assertEqual(response.status_code, 200) + + response = self.client.get('/search', {'q': 'django'}) + self.assertEqual(response.status_code, 200) + s = load_articletags(article) + self.assertIsNotNone(s) + + self.client.login(username='liangliangyy', password='liangliangyy') + + response = self.client.get(reverse('blog:archives')) + self.assertEqual(response.status_code, 200) + + p = Paginator(Article.objects.all(), settings.PAGINATE_BY) + self.check_pagination(p, '', '') + + p = Paginator(Article.objects.filter(tags=tag), settings.PAGINATE_BY) + self.check_pagination(p, '分类标签归档', tag.slug) + + p = Paginator( + Article.objects.filter( + author__username='liangliangyy'), settings.PAGINATE_BY) + self.check_pagination(p, '作者文章归档', 'liangliangyy') + + p = Paginator(Article.objects.filter(category=category), settings.PAGINATE_BY) + self.check_pagination(p, '分类目录归档', category.slug) + + f = BlogSearchForm() + f.search() + # self.client.login(username='liangliangyy', password='liangliangyy') + from djangoblog.spider_notify import SpiderNotify + SpiderNotify.baidu_notify([article.get_full_url()]) + + from blog.templatetags.blog_tags import gravatar_url, gravatar + u = gravatar_url('liangliangyy@gmail.com') + u = gravatar('liangliangyy@gmail.com') + + link = Links( + sequence=1, + name="lylinux", + link='https://wwww.lylinux.net') + link.save() + response = self.client.get('/links.html') + self.assertEqual(response.status_code, 200) + + response = self.client.get('/feed/') + self.assertEqual(response.status_code, 200) + + response = self.client.get('/sitemap.xml') + self.assertEqual(response.status_code, 200) + + self.client.get("/admin/blog/article/1/delete/") + self.client.get('/admin/servermanager/emailsendlog/') + self.client.get('/admin/admin/logentry/') + self.client.get('/admin/admin/logentry/1/change/') + + def check_pagination(self, p, type, value): + for page in range(1, p.num_pages + 1): + s = load_pagination_info(p.page(page), type, value) + self.assertIsNotNone(s) + if s['previous_url']: + response = self.client.get(s['previous_url']) + self.assertEqual(response.status_code, 200) + if s['next_url']: + response = self.client.get(s['next_url']) + self.assertEqual(response.status_code, 200) + + def test_image(self): + import requests + rsp = requests.get( + 'https://www.python.org/static/img/python-logo.png') + imagepath = os.path.join(settings.BASE_DIR, 'python.png') + with open(imagepath, 'wb') as file: + file.write(rsp.content) + rsp = self.client.post('/upload') + self.assertEqual(rsp.status_code, 403) + sign = get_sha256(get_sha256(settings.SECRET_KEY)) + with open(imagepath, 'rb') as file: + imgfile = SimpleUploadedFile( + 'python.png', file.read(), content_type='image/jpg') + form_data = {'python.png': imgfile} + rsp = self.client.post( + '/upload?sign=' + sign, form_data, follow=True) + self.assertEqual(rsp.status_code, 200) + os.remove(imagepath) + from djangoblog.utils import save_user_avatar, send_email + send_email(['qq@qq.com'], 'testTitle', 'testContent') + save_user_avatar( + 'https://www.python.org/static/img/python-logo.png') + + def test_errorpage(self): + rsp = self.client.get('/eee') + self.assertEqual(rsp.status_code, 404) + + def test_commands(self): + user = BlogUser.objects.get_or_create( + email="liangliangyy@gmail.com", + username="liangliangyy")[0] + user.set_password("liangliangyy") + user.is_staff = True + user.is_superuser = True + user.save() + + c = OAuthConfig() + c.type = 'qq' + c.appkey = 'appkey' + c.appsecret = 'appsecret' + c.save() + + u = OAuthUser() + u.type = 'qq' + u.openid = 'openid' + u.user = user + u.picture = static("/blog/img/avatar.png") + u.metadata = ''' +{ +"figureurl": "https://qzapp.qlogo.cn/qzapp/101513904/C740E30B4113EAA80E0D9918ABC78E82/30" +}''' + u.save() + + u = OAuthUser() + u.type = 'qq' + u.openid = 'openid1' + u.picture = 'https://qzapp.qlogo.cn/qzapp/101513904/C740E30B4113EAA80E0D9918ABC78E82/30' + u.metadata = ''' + { + "figureurl": "https://qzapp.qlogo.cn/qzapp/101513904/C740E30B4113EAA80E0D9918ABC78E82/30" + }''' + u.save() + + from blog.documents import ELASTICSEARCH_ENABLED + if ELASTICSEARCH_ENABLED: + call_command("build_index") + call_command("ping_baidu", "all") + call_command("create_testdata") + call_command("clear_cache") + call_command("sync_user_avatar") + call_command("build_search_words") diff --git a/DjangoBlog-master/blog/urls.py b/DjangoBlog-master/blog/urls.py new file mode 100644 index 0000000..adf2703 --- /dev/null +++ b/DjangoBlog-master/blog/urls.py @@ -0,0 +1,62 @@ +from django.urls import path +from django.views.decorators.cache import cache_page + +from . import views + +app_name = "blog" +urlpatterns = [ + path( + r'', + views.IndexView.as_view(), + name='index'), + path( + r'page//', + views.IndexView.as_view(), + name='index_page'), + path( + r'article////.html', + views.ArticleDetailView.as_view(), + name='detailbyid'), + path( + r'category/.html', + views.CategoryDetailView.as_view(), + name='category_detail'), + path( + r'category//.html', + views.CategoryDetailView.as_view(), + name='category_detail_page'), + path( + r'author/.html', + views.AuthorDetailView.as_view(), + name='author_detail'), + path( + r'author//.html', + views.AuthorDetailView.as_view(), + name='author_detail_page'), + path( + r'tag/.html', + views.TagDetailView.as_view(), + name='tag_detail'), + path( + r'tag//.html', + views.TagDetailView.as_view(), + name='tag_detail_page'), + path( + 'archives.html', + cache_page( + 60 * 60)( + views.ArchivesView.as_view()), + name='archives'), + path( + 'links.html', + views.LinkListView.as_view(), + name='links'), + path( + r'upload', + views.fileupload, + name='upload'), + path( + r'clean', + views.clean_cache_view, + name='clean'), +] diff --git a/DjangoBlog-master/blog/views.py b/DjangoBlog-master/blog/views.py new file mode 100644 index 0000000..d5dc7ec --- /dev/null +++ b/DjangoBlog-master/blog/views.py @@ -0,0 +1,379 @@ +import logging +import os +import uuid + +from django.conf import settings +from django.core.paginator import Paginator +from django.http import HttpResponse, HttpResponseForbidden +from django.shortcuts import get_object_or_404 +from django.shortcuts import render +from django.templatetags.static import static +from django.utils import timezone +from django.utils.translation import gettext_lazy as _ +from django.views.decorators.csrf import csrf_exempt +from django.views.generic.detail import DetailView +from django.views.generic.list import ListView +from haystack.views import SearchView + +from blog.models import Article, Category, LinkShowType, Links, Tag +from comments.forms import CommentForm +from djangoblog.plugin_manage import hooks +from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME +from djangoblog.utils import cache, get_blog_setting, get_sha256 + +logger = logging.getLogger(__name__) + + +class ArticleListView(ListView): + # template_name属性用于指定使用哪个模板进行渲染 + template_name = 'blog/article_index.html' + + # context_object_name属性用于给上下文变量取名(在模板中使用该名字) + context_object_name = 'article_list' + + # 页面类型,分类目录或标签列表等 + page_type = '' + paginate_by = settings.PAGINATE_BY + page_kwarg = 'page' + link_type = LinkShowType.L + + def get_view_cache_key(self): + return self.request.get['pages'] + + @property + def page_number(self): + page_kwarg = self.page_kwarg + page = self.kwargs.get( + page_kwarg) or self.request.GET.get(page_kwarg) or 1 + return page + + def get_queryset_cache_key(self): + """ + 子类重写.获得queryset的缓存key + """ + raise NotImplementedError() + + def get_queryset_data(self): + """ + 子类重写.获取queryset的数据 + """ + raise NotImplementedError() + + def get_queryset_from_cache(self, cache_key): + ''' + 缓存页面数据 + :param cache_key: 缓存key + :return: + ''' + value = cache.get(cache_key) + if value: + logger.info('get view cache.key:{key}'.format(key=cache_key)) + return value + else: + article_list = self.get_queryset_data() + cache.set(cache_key, article_list) + logger.info('set view cache.key:{key}'.format(key=cache_key)) + return article_list + + def get_queryset(self): + ''' + 重写默认,从缓存获取数据 + :return: + ''' + key = self.get_queryset_cache_key() + value = self.get_queryset_from_cache(key) + return value + + def get_context_data(self, **kwargs): + kwargs['linktype'] = self.link_type + return super(ArticleListView, self).get_context_data(**kwargs) + + +class IndexView(ArticleListView): + ''' + 首页 + ''' + # 友情链接类型 + link_type = LinkShowType.I + + def get_queryset_data(self): + article_list = Article.objects.filter(type='a', status='p') + return article_list + + def get_queryset_cache_key(self): + cache_key = 'index_{page}'.format(page=self.page_number) + return cache_key + + +class ArticleDetailView(DetailView): + ''' + 文章详情页面 + ''' + template_name = 'blog/article_detail.html' + model = Article + pk_url_kwarg = 'article_id' + context_object_name = "article" + + def get_context_data(self, **kwargs): + comment_form = CommentForm() + + article_comments = self.object.comment_list() + parent_comments = article_comments.filter(parent_comment=None) + blog_setting = get_blog_setting() + paginator = Paginator(parent_comments, blog_setting.article_comment_count) + page = self.request.GET.get('comment_page', '1') + if not page.isnumeric(): + page = 1 + else: + page = int(page) + if page < 1: + page = 1 + if page > paginator.num_pages: + page = paginator.num_pages + + p_comments = paginator.page(page) + next_page = p_comments.next_page_number() if p_comments.has_next() else None + prev_page = p_comments.previous_page_number() if p_comments.has_previous() else None + + if next_page: + kwargs[ + 'comment_next_page_url'] = self.object.get_absolute_url() + f'?comment_page={next_page}#commentlist-container' + if prev_page: + kwargs[ + 'comment_prev_page_url'] = self.object.get_absolute_url() + f'?comment_page={prev_page}#commentlist-container' + kwargs['form'] = comment_form + kwargs['article_comments'] = article_comments + kwargs['p_comments'] = p_comments + kwargs['comment_count'] = len( + article_comments) if article_comments else 0 + + kwargs['next_article'] = self.object.next_article + kwargs['prev_article'] = self.object.prev_article + + context = super(ArticleDetailView, self).get_context_data(**kwargs) + article = self.object + # 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 + + +class CategoryDetailView(ArticleListView): + ''' + 分类目录列表 + ''' + page_type = "分类目录归档" + + def get_queryset_data(self): + slug = self.kwargs['category_name'] + category = get_object_or_404(Category, slug=slug) + + categoryname = category.name + self.categoryname = categoryname + categorynames = list( + map(lambda c: c.name, category.get_sub_categorys())) + article_list = Article.objects.filter( + category__name__in=categorynames, status='p') + return article_list + + def get_queryset_cache_key(self): + slug = self.kwargs['category_name'] + category = get_object_or_404(Category, slug=slug) + categoryname = category.name + self.categoryname = categoryname + cache_key = 'category_list_{categoryname}_{page}'.format( + categoryname=categoryname, page=self.page_number) + return cache_key + + def get_context_data(self, **kwargs): + + categoryname = self.categoryname + try: + categoryname = categoryname.split('/')[-1] + except BaseException: + pass + kwargs['page_type'] = CategoryDetailView.page_type + kwargs['tag_name'] = categoryname + return super(CategoryDetailView, self).get_context_data(**kwargs) + + +class AuthorDetailView(ArticleListView): + ''' + 作者详情页 + ''' + page_type = '作者文章归档' + + def get_queryset_cache_key(self): + from uuslug import slugify + author_name = slugify(self.kwargs['author_name']) + cache_key = 'author_{author_name}_{page}'.format( + author_name=author_name, page=self.page_number) + return cache_key + + def get_queryset_data(self): + author_name = self.kwargs['author_name'] + article_list = Article.objects.filter( + author__username=author_name, type='a', status='p') + return article_list + + def get_context_data(self, **kwargs): + author_name = self.kwargs['author_name'] + kwargs['page_type'] = AuthorDetailView.page_type + kwargs['tag_name'] = author_name + return super(AuthorDetailView, self).get_context_data(**kwargs) + + +class TagDetailView(ArticleListView): + ''' + 标签列表页面 + ''' + page_type = '分类标签归档' + + def get_queryset_data(self): + slug = self.kwargs['tag_name'] + tag = get_object_or_404(Tag, slug=slug) + tag_name = tag.name + self.name = tag_name + article_list = Article.objects.filter( + tags__name=tag_name, type='a', status='p') + return article_list + + def get_queryset_cache_key(self): + slug = self.kwargs['tag_name'] + tag = get_object_or_404(Tag, slug=slug) + tag_name = tag.name + self.name = tag_name + cache_key = 'tag_{tag_name}_{page}'.format( + tag_name=tag_name, page=self.page_number) + return cache_key + + def get_context_data(self, **kwargs): + # tag_name = self.kwargs['tag_name'] + tag_name = self.name + kwargs['page_type'] = TagDetailView.page_type + kwargs['tag_name'] = tag_name + return super(TagDetailView, self).get_context_data(**kwargs) + + +class ArchivesView(ArticleListView): + ''' + 文章归档页面 + ''' + page_type = '文章归档' + paginate_by = None + page_kwarg = None + template_name = 'blog/article_archives.html' + + def get_queryset_data(self): + return Article.objects.filter(status='p').all() + + def get_queryset_cache_key(self): + cache_key = 'archives' + return cache_key + + +class LinkListView(ListView): + model = Links + template_name = 'blog/links_list.html' + + def get_queryset(self): + return Links.objects.filter(is_enable=True) + + +class EsSearchView(SearchView): + def get_context(self): + paginator, page = self.build_page() + context = { + "query": self.query, + "form": self.form, + "page": page, + "paginator": paginator, + "suggestion": None, + } + if hasattr(self.results, "query") and self.results.query.backend.include_spelling: + context["suggestion"] = self.results.query.get_spelling_suggestion() + context.update(self.extra_context()) + + return context + + +@csrf_exempt +def fileupload(request): + """ + 该方法需自己写调用端来上传图片,该方法仅提供图床功能 + :param request: + :return: + """ + if request.method == 'POST': + sign = request.GET.get('sign', None) + if not sign: + return HttpResponseForbidden() + if not sign == get_sha256(get_sha256(settings.SECRET_KEY)): + return HttpResponseForbidden() + response = [] + for filename in request.FILES: + timestr = timezone.now().strftime('%Y/%m/%d') + imgextensions = ['jpg', 'png', 'jpeg', 'bmp'] + fname = u''.join(str(filename)) + isimage = len([i for i in imgextensions if fname.find(i) >= 0]) > 0 + base_dir = os.path.join(settings.STATICFILES, "files" if not isimage else "image", timestr) + if not os.path.exists(base_dir): + os.makedirs(base_dir) + savepath = os.path.normpath(os.path.join(base_dir, f"{uuid.uuid4().hex}{os.path.splitext(filename)[-1]}")) + if not savepath.startswith(base_dir): + return HttpResponse("only for post") + with open(savepath, 'wb+') as wfile: + for chunk in request.FILES[filename].chunks(): + wfile.write(chunk) + if isimage: + from PIL import Image + image = Image.open(savepath) + image.save(savepath, quality=20, optimize=True) + url = static(savepath) + response.append(url) + return HttpResponse(response) + + else: + return HttpResponse("only for post") + + +def page_not_found_view( + request, + exception, + template_name='blog/error_page.html'): + if exception: + logger.error(exception) + url = request.get_full_path() + return render(request, + template_name, + {'message': _('Sorry, the page you requested is not found, please click the home page to see other?'), + 'statuscode': '404'}, + status=404) + + +def server_error_view(request, template_name='blog/error_page.html'): + return render(request, + template_name, + {'message': _('Sorry, the server is busy, please click the home page to see other?'), + 'statuscode': '500'}, + status=500) + + +def permission_denied_view( + request, + exception, + template_name='blog/error_page.html'): + if exception: + logger.error(exception) + return render( + request, template_name, { + 'message': _('Sorry, you do not have permission to access this page?'), + 'statuscode': '403'}, status=403) + + +def clean_cache_view(request): + cache.clear() + return HttpResponse('ok') diff --git a/DjangoBlog-master/comments/__init__.py b/DjangoBlog-master/comments/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/DjangoBlog-master/comments/admin.py b/DjangoBlog-master/comments/admin.py new file mode 100644 index 0000000..a814f3f --- /dev/null +++ b/DjangoBlog-master/comments/admin.py @@ -0,0 +1,47 @@ +from django.contrib import admin +from django.urls import reverse +from django.utils.html import format_html +from django.utils.translation import gettext_lazy as _ + + +def disable_commentstatus(modeladmin, request, queryset): + queryset.update(is_enable=False) + + +def enable_commentstatus(modeladmin, request, queryset): + queryset.update(is_enable=True) + + +disable_commentstatus.short_description = _('Disable comments') +enable_commentstatus.short_description = _('Enable comments') + + +class CommentAdmin(admin.ModelAdmin): + list_per_page = 20 + list_display = ( + 'id', + 'body', + 'link_to_userinfo', + 'link_to_article', + 'is_enable', + 'creation_time') + list_display_links = ('id', 'body', 'is_enable') + list_filter = ('is_enable',) + exclude = ('creation_time', 'last_modify_time') + actions = [disable_commentstatus, enable_commentstatus] + + def link_to_userinfo(self, obj): + info = (obj.author._meta.app_label, obj.author._meta.model_name) + link = reverse('admin:%s_%s_change' % info, args=(obj.author.id,)) + return format_html( + u'%s' % + (link, obj.author.nickname if obj.author.nickname else obj.author.email)) + + def link_to_article(self, obj): + info = (obj.article._meta.app_label, obj.article._meta.model_name) + link = reverse('admin:%s_%s_change' % info, args=(obj.article.id,)) + return format_html( + u'%s' % (link, obj.article.title)) + + link_to_userinfo.short_description = _('User') + link_to_article.short_description = _('Article') diff --git a/DjangoBlog-master/comments/apps.py b/DjangoBlog-master/comments/apps.py new file mode 100644 index 0000000..ff01b77 --- /dev/null +++ b/DjangoBlog-master/comments/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class CommentsConfig(AppConfig): + name = 'comments' diff --git a/DjangoBlog-master/comments/forms.py b/DjangoBlog-master/comments/forms.py new file mode 100644 index 0000000..e83737d --- /dev/null +++ b/DjangoBlog-master/comments/forms.py @@ -0,0 +1,13 @@ +from django import forms +from django.forms import ModelForm + +from .models import Comment + + +class CommentForm(ModelForm): + parent_comment_id = forms.IntegerField( + widget=forms.HiddenInput, required=False) + + class Meta: + model = Comment + fields = ['body'] diff --git a/DjangoBlog-master/comments/migrations/0001_initial.py b/DjangoBlog-master/comments/migrations/0001_initial.py new file mode 100644 index 0000000..61d1e53 --- /dev/null +++ b/DjangoBlog-master/comments/migrations/0001_initial.py @@ -0,0 +1,38 @@ +# Generated by Django 4.1.7 on 2023-03-02 07:14 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('blog', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Comment', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('body', models.TextField(max_length=300, verbose_name='正文')), + ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')), + ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')), + ('is_enable', models.BooleanField(default=True, verbose_name='是否显示')), + ('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.article', verbose_name='文章')), + ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='作者')), + ('parent_comment', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='comments.comment', verbose_name='上级评论')), + ], + options={ + 'verbose_name': '评论', + 'verbose_name_plural': '评论', + 'ordering': ['-id'], + 'get_latest_by': 'id', + }, + ), + ] diff --git a/DjangoBlog-master/comments/migrations/0002_alter_comment_is_enable.py b/DjangoBlog-master/comments/migrations/0002_alter_comment_is_enable.py new file mode 100644 index 0000000..17c44db --- /dev/null +++ b/DjangoBlog-master/comments/migrations/0002_alter_comment_is_enable.py @@ -0,0 +1,18 @@ +# Generated by Django 4.1.7 on 2023-04-24 13:48 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('comments', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='comment', + name='is_enable', + field=models.BooleanField(default=False, verbose_name='是否显示'), + ), + ] diff --git a/DjangoBlog-master/comments/migrations/0003_alter_comment_options_remove_comment_created_time_and_more.py b/DjangoBlog-master/comments/migrations/0003_alter_comment_options_remove_comment_created_time_and_more.py new file mode 100644 index 0000000..a1ca970 --- /dev/null +++ b/DjangoBlog-master/comments/migrations/0003_alter_comment_options_remove_comment_created_time_and_more.py @@ -0,0 +1,60 @@ +# Generated by Django 4.2.5 on 2023-09-06 13:13 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('blog', '0005_alter_article_options_alter_category_options_and_more'), + ('comments', '0002_alter_comment_is_enable'), + ] + + operations = [ + migrations.AlterModelOptions( + name='comment', + options={'get_latest_by': 'id', 'ordering': ['-id'], 'verbose_name': 'comment', 'verbose_name_plural': 'comment'}, + ), + migrations.RemoveField( + model_name='comment', + name='created_time', + ), + migrations.RemoveField( + model_name='comment', + name='last_mod_time', + ), + migrations.AddField( + model_name='comment', + name='creation_time', + field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'), + ), + migrations.AddField( + model_name='comment', + name='last_modify_time', + field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'), + ), + migrations.AlterField( + model_name='comment', + name='article', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.article', verbose_name='article'), + ), + 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'), + ), + migrations.AlterField( + model_name='comment', + name='is_enable', + field=models.BooleanField(default=False, verbose_name='enable'), + ), + migrations.AlterField( + model_name='comment', + name='parent_comment', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='comments.comment', verbose_name='parent comment'), + ), + ] diff --git a/DjangoBlog-master/comments/migrations/__init__.py b/DjangoBlog-master/comments/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/DjangoBlog-master/comments/models.py b/DjangoBlog-master/comments/models.py new file mode 100644 index 0000000..7c3bbc8 --- /dev/null +++ b/DjangoBlog-master/comments/models.py @@ -0,0 +1,39 @@ +from django.conf import settings +from django.db import models +from django.utils.timezone import now +from django.utils.translation import gettext_lazy as _ + +from blog.models import Article + + +# Create your models here. + +class Comment(models.Model): + body = models.TextField('正文', max_length=300) + creation_time = models.DateTimeField(_('creation time'), default=now) + last_modify_time = models.DateTimeField(_('last modify time'), default=now) + author = models.ForeignKey( + settings.AUTH_USER_MODEL, + verbose_name=_('author'), + on_delete=models.CASCADE) + article = models.ForeignKey( + Article, + verbose_name=_('article'), + on_delete=models.CASCADE) + parent_comment = models.ForeignKey( + 'self', + verbose_name=_('parent comment'), + blank=True, + null=True, + on_delete=models.CASCADE) + is_enable = models.BooleanField(_('enable'), + default=False, blank=False, null=False) + + class Meta: + ordering = ['-id'] + verbose_name = _('comment') + verbose_name_plural = verbose_name + get_latest_by = 'id' + + def __str__(self): + return self.body diff --git a/DjangoBlog-master/comments/templatetags/__init__.py b/DjangoBlog-master/comments/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/DjangoBlog-master/comments/templatetags/comments_tags.py b/DjangoBlog-master/comments/templatetags/comments_tags.py new file mode 100644 index 0000000..fde02b4 --- /dev/null +++ b/DjangoBlog-master/comments/templatetags/comments_tags.py @@ -0,0 +1,30 @@ +from django import template + +register = template.Library() + + +@register.simple_tag +def parse_commenttree(commentlist, comment): + """获得当前评论子评论的列表 + 用法: {% parse_commenttree article_comments comment as childcomments %} + """ + datas = [] + + def parse(c): + childs = commentlist.filter(parent_comment=c, is_enable=True) + for child in childs: + datas.append(child) + parse(child) + + parse(comment) + return datas + + +@register.inclusion_tag('comments/tags/comment_item.html') +def show_comment_item(comment, ischild): + """评论""" + depth = 1 if ischild else 2 + return { + 'comment_item': comment, + 'depth': depth + } diff --git a/DjangoBlog-master/comments/tests.py b/DjangoBlog-master/comments/tests.py new file mode 100644 index 0000000..2a7f55f --- /dev/null +++ b/DjangoBlog-master/comments/tests.py @@ -0,0 +1,109 @@ +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 + + +# Create your tests here. + +class CommentsTest(TransactionTestCase): + def setUp(self): + self.client = Client() + self.factory = RequestFactory() + from blog.models import BlogSettings + value = BlogSettings() + value.comment_need_review = True + value.save() + + self.user = BlogUser.objects.create_superuser( + email="liangliangyy1@gmail.com", + username="liangliangyy1", + password="liangliangyy1") + + def update_article_comment_status(self, article): + comments = article.comment_set.all() + for comment in comments: + comment.is_enable = True + comment.save() + + def test_validate_comment(self): + self.client.login(username='liangliangyy1', password='liangliangyy1') + + category = Category() + category.name = "categoryccc" + category.save() + + article = Article() + article.title = "nicetitleccc" + article.body = "nicecontentccc" + article.author = self.user + article.category = category + article.type = 'a' + article.status = 'p' + article.save() + + comment_url = reverse( + 'comments:postcomment', kwargs={ + 'article_id': article.id}) + + response = self.client.post(comment_url, + { + 'body': '123ffffffffff' + }) + + self.assertEqual(response.status_code, 302) + + article = Article.objects.get(pk=article.pk) + self.assertEqual(len(article.comment_list()), 0) + self.update_article_comment_status(article) + + self.assertEqual(len(article.comment_list()), 1) + + response = self.client.post(comment_url, + { + 'body': '123ffffffffff', + }) + + self.assertEqual(response.status_code, 302) + + article = Article.objects.get(pk=article.pk) + self.update_article_comment_status(article) + self.assertEqual(len(article.comment_list()), 2) + parent_comment_id = article.comment_list()[0].id + + response = self.client.post(comment_url, + { + 'body': ''' + # Title1 + + ```python + import os + ``` + + [url](https://www.lylinux.net/) + + [ddd](http://www.baidu.com) + + + ''', + 'parent_comment_id': parent_comment_id + }) + + self.assertEqual(response.status_code, 302) + self.update_article_comment_status(article) + article = Article.objects.get(pk=article.pk) + self.assertEqual(len(article.comment_list()), 3) + comment = Comment.objects.get(id=parent_comment_id) + tree = parse_commenttree(article.comment_list(), comment) + self.assertEqual(len(tree), 1) + data = show_comment_item(comment, True) + self.assertIsNotNone(data) + s = get_max_articleid_commentid() + self.assertIsNotNone(s) + + from comments.utils import send_comment_email + send_comment_email(comment) diff --git a/DjangoBlog-master/comments/urls.py b/DjangoBlog-master/comments/urls.py new file mode 100644 index 0000000..7df3fab --- /dev/null +++ b/DjangoBlog-master/comments/urls.py @@ -0,0 +1,11 @@ +from django.urls import path + +from . import views + +app_name = "comments" +urlpatterns = [ + path( + 'article//postcomment', + views.CommentPostView.as_view(), + name='postcomment'), +] diff --git a/DjangoBlog-master/comments/utils.py b/DjangoBlog-master/comments/utils.py new file mode 100644 index 0000000..f01dba7 --- /dev/null +++ b/DjangoBlog-master/comments/utils.py @@ -0,0 +1,38 @@ +import logging + +from django.utils.translation import gettext_lazy as _ + +from djangoblog.utils import get_current_site +from djangoblog.utils import send_email + +logger = logging.getLogger(__name__) + + +def send_comment_email(comment): + site = get_current_site().domain + subject = _('Thanks for your comment') + article_url = f"https://{site}{comment.article.get_absolute_url()}" + html_content = _("""

Thank you very much for your comments on this site

+ You can visit %(article_title)s + to review your comments, + Thank you again! +
+ If the link above cannot be opened, please copy this link to your browser. + %(article_url)s""") % {'article_url': article_url, 'article_title': comment.article.title} + tomail = comment.author.email + send_email([tomail], subject, html_content) + try: + if comment.parent_comment: + html_content = _("""Your comment on %(article_title)s
has + received a reply.
%(comment_body)s +
+ go check it out! +
+ If the link above cannot be opened, please copy this link to your browser. + %(article_url)s + """) % {'article_url': article_url, 'article_title': comment.article.title, + 'comment_body': comment.parent_comment.body} + tomail = comment.parent_comment.author.email + send_email([tomail], subject, html_content) + except Exception as e: + logger.error(e) diff --git a/DjangoBlog-master/comments/views.py b/DjangoBlog-master/comments/views.py new file mode 100644 index 0000000..ad9b2b9 --- /dev/null +++ b/DjangoBlog-master/comments/views.py @@ -0,0 +1,63 @@ +# Create your views here. +from django.core.exceptions import ValidationError +from django.http import HttpResponseRedirect +from django.shortcuts import get_object_or_404 +from django.utils.decorators import method_decorator +from django.views.decorators.csrf import csrf_protect +from django.views.generic.edit import FormView + +from accounts.models import BlogUser +from blog.models import Article +from .forms import CommentForm +from .models import Comment + + +class CommentPostView(FormView): + form_class = CommentForm + template_name = 'blog/article_detail.html' + + @method_decorator(csrf_protect) + def dispatch(self, *args, **kwargs): + return super(CommentPostView, self).dispatch(*args, **kwargs) + + def get(self, request, *args, **kwargs): + article_id = self.kwargs['article_id'] + article = get_object_or_404(Article, pk=article_id) + url = article.get_absolute_url() + return HttpResponseRedirect(url + "#comments") + + def form_invalid(self, form): + article_id = self.kwargs['article_id'] + article = get_object_or_404(Article, pk=article_id) + + return self.render_to_response({ + 'form': form, + 'article': article + }) + + def form_valid(self, form): + """提交的数据验证合法后的逻辑""" + user = self.request.user + author = BlogUser.objects.get(pk=user.pk) + article_id = self.kwargs['article_id'] + article = get_object_or_404(Article, pk=article_id) + + if article.comment_status == 'c' or article.status == 'c': + raise ValidationError("该文章评论已关闭.") + comment = form.save(False) + comment.article = article + from djangoblog.utils import get_blog_setting + settings = get_blog_setting() + if not settings.comment_need_review: + comment.is_enable = True + comment.author = author + + if form.cleaned_data['parent_comment_id']: + parent_comment = Comment.objects.get( + pk=form.cleaned_data['parent_comment_id']) + comment.parent_comment = parent_comment + + comment.save(True) + return HttpResponseRedirect( + "%s#div-comment-%d" % + (article.get_absolute_url(), comment.pk)) diff --git a/DjangoBlog-master/db.sqlite3 b/DjangoBlog-master/db.sqlite3 new file mode 100644 index 0000000..e69de29 diff --git a/DjangoBlog-master/deploy/docker-compose/docker-compose.es.yml b/DjangoBlog-master/deploy/docker-compose/docker-compose.es.yml new file mode 100644 index 0000000..83e35ff --- /dev/null +++ b/DjangoBlog-master/deploy/docker-compose/docker-compose.es.yml @@ -0,0 +1,48 @@ +version: '3' + +services: + es: + image: liangliangyy/elasticsearch-analysis-ik:8.6.1 + container_name: es + restart: always + environment: + - discovery.type=single-node + - "ES_JAVA_OPTS=-Xms512m -Xmx512m" + ports: + - 9200:9200 + volumes: + - ./bin/datas/es/:/usr/share/elasticsearch/data/ + + kibana: + image: kibana:8.6.1 + restart: always + container_name: kibana + ports: + - 5601:5601 + environment: + - ELASTICSEARCH_HOSTS=http://es:9200 + + djangoblog: + build: . + restart: always + command: bash -c 'sh /code/djangoblog/bin/docker_start.sh' + ports: + - "8000:8000" + volumes: + - ./collectedstatic:/code/djangoblog/collectedstatic + - ./uploads:/code/djangoblog/uploads + environment: + - DJANGO_MYSQL_DATABASE=djangoblog + - DJANGO_MYSQL_USER=root + - DJANGO_MYSQL_PASSWORD=DjAnGoBlOg!2!Q@W#E + - DJANGO_MYSQL_HOST=db + - DJANGO_MYSQL_PORT=3306 + - DJANGO_MEMCACHED_LOCATION=memcached:11211 + - DJANGO_ELASTICSEARCH_HOST=es:9200 + links: + - db + - memcached + depends_on: + - db + container_name: djangoblog + diff --git a/DjangoBlog-master/deploy/docker-compose/docker-compose.yml b/DjangoBlog-master/deploy/docker-compose/docker-compose.yml new file mode 100644 index 0000000..9609af3 --- /dev/null +++ b/DjangoBlog-master/deploy/docker-compose/docker-compose.yml @@ -0,0 +1,60 @@ +version: '3' + +services: + db: + image: mysql:latest + restart: always + environment: + - MYSQL_DATABASE=djangoblog + - MYSQL_ROOT_PASSWORD=DjAnGoBlOg!2!Q@W#E + ports: + - 3306:3306 + volumes: + - ./bin/datas/mysql/:/var/lib/mysql + depends_on: + - redis + container_name: db + + djangoblog: + build: + context: ../../ + restart: always + command: bash -c 'sh /code/djangoblog/bin/docker_start.sh' + ports: + - "8000:8000" + volumes: + - ./collectedstatic:/code/djangoblog/collectedstatic + - ./logs:/code/djangoblog/logs + - ./uploads:/code/djangoblog/uploads + environment: + - DJANGO_MYSQL_DATABASE=djangoblog + - DJANGO_MYSQL_USER=root + - DJANGO_MYSQL_PASSWORD=DjAnGoBlOg!2!Q@W#E + - DJANGO_MYSQL_HOST=db + - DJANGO_MYSQL_PORT=3306 + - DJANGO_REDIS_URL=redis:6379 + links: + - db + - redis + depends_on: + - db + container_name: djangoblog + nginx: + restart: always + image: nginx:latest + ports: + - "80:80" + - "443:443" + volumes: + - ./bin/nginx.conf:/etc/nginx/nginx.conf + - ./collectedstatic:/code/djangoblog/collectedstatic + links: + - djangoblog:djangoblog + container_name: nginx + + redis: + restart: always + image: redis:latest + container_name: redis + ports: + - "6379:6379" diff --git a/DjangoBlog-master/deploy/entrypoint.sh b/DjangoBlog-master/deploy/entrypoint.sh new file mode 100644 index 0000000..2fb6491 --- /dev/null +++ b/DjangoBlog-master/deploy/entrypoint.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +NAME="djangoblog" +DJANGODIR=/code/djangoblog +USER=root +GROUP=root +NUM_WORKERS=1 +DJANGO_WSGI_MODULE=djangoblog.wsgi + + +echo "Starting $NAME as `whoami`" + +cd $DJANGODIR + +export PYTHONPATH=$DJANGODIR:$PYTHONPATH + +python manage.py makemigrations && \ + python manage.py migrate && \ + python manage.py collectstatic --noinput && \ + python manage.py compress --force && \ + python manage.py build_index && \ + python manage.py compilemessages || exit 1 + +exec gunicorn ${DJANGO_WSGI_MODULE}:application \ +--name $NAME \ +--workers $NUM_WORKERS \ +--user=$USER --group=$GROUP \ +--bind 0.0.0.0:8000 \ +--log-level=debug \ +--log-file=- \ +--worker-class gevent \ +--threads 4 diff --git a/DjangoBlog-master/deploy/k8s/configmap.yaml b/DjangoBlog-master/deploy/k8s/configmap.yaml new file mode 100644 index 0000000..835d4ad --- /dev/null +++ b/DjangoBlog-master/deploy/k8s/configmap.yaml @@ -0,0 +1,119 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: web-nginx-config + namespace: djangoblog +data: + nginx.conf: | + user nginx; + worker_processes auto; + error_log /var/log/nginx/error.log notice; + pid /var/run/nginx.pid; + + events { + worker_connections 1024; + multi_accept on; + use epoll; + } + + http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + + sendfile on; + keepalive_timeout 65; + gzip on; + gzip_disable "msie6"; + + gzip_vary on; + gzip_proxied any; + gzip_comp_level 8; + gzip_buffers 16 8k; + gzip_http_version 1.1; + gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png; + + # Include server configurations + include /etc/nginx/conf.d/*.conf; + } + djangoblog.conf: | + server { + server_name lylinux.net; + root /code/djangoblog/collectedstatic/; + listen 80; + keepalive_timeout 70; + location /static/ { + expires max; + alias /code/djangoblog/collectedstatic/; + } + + location ~* (robots\.txt|ads\.txt|favicon\.ico|favion\.ico|crossdomain\.xml|google93fd32dbd906620a\.html|BingSiteAuth\.xml|baidu_verify_Ijeny6KrmS\.html)$ { + root /resource/djangopub; + expires 1d; + access_log off; + error_log off; + } + + location / { + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Host $http_host; + proxy_set_header X-NginX-Proxy true; + proxy_redirect off; + if (!-f $request_filename) { + proxy_pass http://djangoblog:8000; + break; + } + } + } + server { + server_name www.lylinux.net; + listen 80; + return 301 https://lylinux.net$request_uri; + } + resource.lylinux.net.conf: | + server { + index index.html index.htm; + server_name resource.lylinux.net; + root /resource/; + + location /djangoblog/ { + alias /code/djangoblog/collectedstatic/; + } + + access_log off; + error_log off; + include lylinux/resource.conf; + } + lylinux.resource.conf: | + expires max; + access_log off; + log_not_found off; + add_header Pragma public; + add_header Cache-Control "public"; + add_header "Access-Control-Allow-Origin" "*"; + +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: djangoblog-env + namespace: djangoblog +data: + DJANGO_MYSQL_DATABASE: djangoblog + DJANGO_MYSQL_USER: db_user + DJANGO_MYSQL_PASSWORD: db_password + DJANGO_MYSQL_HOST: db_host + DJANGO_MYSQL_PORT: db_port + DJANGO_REDIS_URL: "redis:6379" + DJANGO_DEBUG: "False" + MYSQL_ROOT_PASSWORD: db_password + MYSQL_DATABASE: djangoblog + MYSQL_PASSWORD: db_password + DJANGO_SECRET_KEY: xxxxxxxxxxxxxxxxxxxxxxxxxxxxx + diff --git a/DjangoBlog-master/deploy/k8s/deployment.yaml b/DjangoBlog-master/deploy/k8s/deployment.yaml new file mode 100644 index 0000000..414fdcc --- /dev/null +++ b/DjangoBlog-master/deploy/k8s/deployment.yaml @@ -0,0 +1,274 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: djangoblog + namespace: djangoblog + labels: + app: djangoblog +spec: + replicas: 3 + selector: + matchLabels: + app: djangoblog + template: + metadata: + labels: + app: djangoblog + spec: + containers: + - name: djangoblog + image: liangliangyy/djangoblog:latest + imagePullPolicy: Always + ports: + - containerPort: 8000 + envFrom: + - configMapRef: + name: djangoblog-env + readinessProbe: + httpGet: + path: / + port: 8000 + initialDelaySeconds: 10 + periodSeconds: 30 + livenessProbe: + httpGet: + path: / + port: 8000 + initialDelaySeconds: 10 + periodSeconds: 30 + resources: + requests: + cpu: 10m + memory: 100Mi + limits: + cpu: "2" + memory: 2Gi + volumeMounts: + - name: djangoblog + mountPath: /code/djangoblog/collectedstatic + - name: resource + mountPath: /resource + volumes: + - name: djangoblog + persistentVolumeClaim: + claimName: djangoblog-pvc + - name: resource + persistentVolumeClaim: + claimName: resource-pvc + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: redis + namespace: djangoblog + labels: + app: redis +spec: + replicas: 1 + selector: + matchLabels: + app: redis + template: + metadata: + labels: + app: redis + spec: + containers: + - name: redis + image: redis:latest + imagePullPolicy: IfNotPresent + ports: + - containerPort: 6379 + resources: + requests: + cpu: 10m + memory: 100Mi + limits: + cpu: 200m + memory: 2Gi + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: db + namespace: djangoblog + labels: + app: db +spec: + replicas: 1 + selector: + matchLabels: + app: db + template: + metadata: + labels: + app: db + spec: + containers: + - name: db + image: mysql:latest + imagePullPolicy: IfNotPresent + ports: + - containerPort: 3306 + envFrom: + - configMapRef: + name: djangoblog-env + readinessProbe: + exec: + command: + - mysqladmin + - ping + - "-h" + - "127.0.0.1" + - "-u" + - "root" + - "-p$MYSQL_ROOT_PASSWORD" + initialDelaySeconds: 10 + periodSeconds: 10 + livenessProbe: + exec: + command: + - mysqladmin + - ping + - "-h" + - "127.0.0.1" + - "-u" + - "root" + - "-p$MYSQL_ROOT_PASSWORD" + initialDelaySeconds: 10 + periodSeconds: 10 + resources: + requests: + cpu: 10m + memory: 100Mi + limits: + cpu: "2" + memory: 2Gi + volumeMounts: + - name: db-data + mountPath: /var/lib/mysql + volumes: + - name: db-data + persistentVolumeClaim: + claimName: db-pvc + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx + namespace: djangoblog + labels: + app: nginx +spec: + replicas: 1 + selector: + matchLabels: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:latest + imagePullPolicy: IfNotPresent + ports: + - containerPort: 80 + resources: + requests: + cpu: 10m + memory: 100Mi + limits: + cpu: "2" + memory: 2Gi + volumeMounts: + - name: nginx-config + mountPath: /etc/nginx/nginx.conf + subPath: nginx.conf + - name: nginx-config + mountPath: /etc/nginx/conf.d/default.conf + subPath: djangoblog.conf + - name: nginx-config + mountPath: /etc/nginx/conf.d/resource.lylinux.net.conf + subPath: resource.lylinux.net.conf + - name: nginx-config + mountPath: /etc/nginx/lylinux/resource.conf + subPath: lylinux.resource.conf + - name: djangoblog-pvc + mountPath: /code/djangoblog/collectedstatic + - name: resource-pvc + mountPath: /resource + volumes: + - name: nginx-config + configMap: + name: web-nginx-config + - name: djangoblog-pvc + persistentVolumeClaim: + claimName: djangoblog-pvc + - name: resource-pvc + persistentVolumeClaim: + claimName: resource-pvc + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: elasticsearch + namespace: djangoblog + labels: + app: elasticsearch +spec: + replicas: 1 + selector: + matchLabels: + app: elasticsearch + template: + metadata: + labels: + app: elasticsearch + spec: + containers: + - name: elasticsearch + image: liangliangyy/elasticsearch-analysis-ik:8.6.1 + imagePullPolicy: IfNotPresent + env: + - name: discovery.type + value: single-node + - name: ES_JAVA_OPTS + value: "-Xms256m -Xmx256m" + - name: xpack.security.enabled + value: "false" + - name: xpack.monitoring.templates.enabled + value: "false" + ports: + - containerPort: 9200 + resources: + requests: + cpu: 10m + memory: 100Mi + limits: + cpu: "2" + memory: 2Gi + readinessProbe: + httpGet: + path: / + port: 9200 + initialDelaySeconds: 15 + periodSeconds: 30 + livenessProbe: + httpGet: + path: / + port: 9200 + initialDelaySeconds: 15 + periodSeconds: 30 + volumeMounts: + - name: elasticsearch-data + mountPath: /usr/share/elasticsearch/data/ + volumes: + - name: elasticsearch-data + persistentVolumeClaim: + claimName: elasticsearch-pvc diff --git a/DjangoBlog-master/deploy/k8s/gateway.yaml b/DjangoBlog-master/deploy/k8s/gateway.yaml new file mode 100644 index 0000000..a8de073 --- /dev/null +++ b/DjangoBlog-master/deploy/k8s/gateway.yaml @@ -0,0 +1,17 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: nginx + namespace: djangoblog +spec: + ingressClassName: nginx + rules: + - http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: nginx + port: + number: 80 \ No newline at end of file diff --git a/DjangoBlog-master/deploy/k8s/pv.yaml b/DjangoBlog-master/deploy/k8s/pv.yaml new file mode 100644 index 0000000..874b72f --- /dev/null +++ b/DjangoBlog-master/deploy/k8s/pv.yaml @@ -0,0 +1,94 @@ +apiVersion: v1 +kind: PersistentVolume +metadata: + name: local-pv-db +spec: + capacity: + storage: 10Gi + volumeMode: Filesystem + accessModes: + - ReadWriteOnce + persistentVolumeReclaimPolicy: Retain + storageClassName: local-storage + local: + path: /mnt/local-storage-db + nodeAffinity: + required: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/hostname + operator: In + values: + - master +--- +apiVersion: v1 +kind: PersistentVolume +metadata: + name: local-pv-djangoblog +spec: + capacity: + storage: 5Gi + volumeMode: Filesystem + accessModes: + - ReadWriteOnce + persistentVolumeReclaimPolicy: Retain + storageClassName: local-storage + local: + path: /mnt/local-storage-djangoblog + nodeAffinity: + required: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/hostname + operator: In + values: + - master + + +--- +apiVersion: v1 +kind: PersistentVolume +metadata: + name: local-pv-resource +spec: + capacity: + storage: 5Gi + volumeMode: Filesystem + accessModes: + - ReadWriteOnce + persistentVolumeReclaimPolicy: Retain + storageClassName: local-storage + local: + path: /mnt/resource/ + nodeAffinity: + required: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/hostname + operator: In + values: + - master + +--- +apiVersion: v1 +kind: PersistentVolume +metadata: + name: local-pv-elasticsearch +spec: + capacity: + storage: 5Gi + volumeMode: Filesystem + accessModes: + - ReadWriteOnce + persistentVolumeReclaimPolicy: Retain + storageClassName: local-storage + local: + path: /mnt/local-storage-elasticsearch + nodeAffinity: + required: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/hostname + operator: In + values: + - master \ No newline at end of file diff --git a/DjangoBlog-master/deploy/k8s/pvc.yaml b/DjangoBlog-master/deploy/k8s/pvc.yaml new file mode 100644 index 0000000..ef238c5 --- /dev/null +++ b/DjangoBlog-master/deploy/k8s/pvc.yaml @@ -0,0 +1,60 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: db-pvc + namespace: djangoblog +spec: + storageClassName: local-storage + volumeName: local-pv-db + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 10Gi + + +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: djangoblog-pvc + namespace: djangoblog +spec: + volumeName: local-pv-djangoblog + storageClassName: local-storage + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 5Gi + +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: resource-pvc + namespace: djangoblog +spec: + volumeName: local-pv-resource + storageClassName: local-storage + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 5Gi + +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: elasticsearch-pvc + namespace: djangoblog +spec: + volumeName: local-pv-elasticsearch + storageClassName: local-storage + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 5Gi + \ No newline at end of file diff --git a/DjangoBlog-master/deploy/k8s/service.yaml b/DjangoBlog-master/deploy/k8s/service.yaml new file mode 100644 index 0000000..4ef2931 --- /dev/null +++ b/DjangoBlog-master/deploy/k8s/service.yaml @@ -0,0 +1,80 @@ +apiVersion: v1 +kind: Service +metadata: + name: djangoblog + namespace: djangoblog + labels: + app: djangoblog +spec: + selector: + app: djangoblog + ports: + - protocol: TCP + port: 8000 + targetPort: 8000 + type: ClusterIP +--- +apiVersion: v1 +kind: Service +metadata: + name: nginx + namespace: djangoblog + labels: + app: nginx +spec: + selector: + app: nginx + ports: + - protocol: TCP + port: 80 + targetPort: 80 + type: ClusterIP +--- +apiVersion: v1 +kind: Service +metadata: + name: redis + namespace: djangoblog + labels: + app: redis +spec: + selector: + app: redis + ports: + - protocol: TCP + port: 6379 + targetPort: 6379 + type: ClusterIP +--- +apiVersion: v1 +kind: Service +metadata: + name: db + namespace: djangoblog + labels: + app: db +spec: + selector: + app: db + ports: + - protocol: TCP + port: 3306 + targetPort: 3306 + type: ClusterIP +--- +apiVersion: v1 +kind: Service +metadata: + name: elasticsearch + namespace: djangoblog + labels: + app: elasticsearch +spec: + selector: + app: elasticsearch + ports: + - protocol: TCP + port: 9200 + targetPort: 9200 + type: ClusterIP + diff --git a/DjangoBlog-master/deploy/k8s/storageclass.yaml b/DjangoBlog-master/deploy/k8s/storageclass.yaml new file mode 100644 index 0000000..5d5a14c --- /dev/null +++ b/DjangoBlog-master/deploy/k8s/storageclass.yaml @@ -0,0 +1,10 @@ +apiVersion: storage.k8s.io/v1 +kind: StorageClass +metadata: + name: local-storage + annotations: + storageclass.kubernetes.io/is-default-class: "true" +provisioner: kubernetes.io/no-provisioner +volumeBindingMode: Immediate + + diff --git a/DjangoBlog-master/deploy/nginx.conf b/DjangoBlog-master/deploy/nginx.conf new file mode 100644 index 0000000..32161d8 --- /dev/null +++ b/DjangoBlog-master/deploy/nginx.conf @@ -0,0 +1,50 @@ +user nginx; +worker_processes auto; + +error_log /var/log/nginx/error.log notice; +pid /var/run/nginx.pid; + + +events { + worker_connections 1024; +} + + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + + sendfile on; + #tcp_nopush on; + + keepalive_timeout 65; + + #gzip on; + + server { + root /code/djangoblog/collectedstatic/; + listen 80; + keepalive_timeout 70; + location /static/ { + expires max; + alias /code/djangoblog/collectedstatic/; + } + location / { + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Host $http_host; + proxy_set_header X-NginX-Proxy true; + proxy_redirect off; + if (!-f $request_filename) { + proxy_pass http://djangoblog:8000; + break; + } + } + } +} diff --git a/DjangoBlog-master/djangoblog/__init__.py b/DjangoBlog-master/djangoblog/__init__.py new file mode 100644 index 0000000..1e205f4 --- /dev/null +++ b/DjangoBlog-master/djangoblog/__init__.py @@ -0,0 +1 @@ +default_app_config = 'djangoblog.apps.DjangoblogAppConfig' diff --git a/DjangoBlog-master/djangoblog/admin_site.py b/DjangoBlog-master/djangoblog/admin_site.py new file mode 100644 index 0000000..f120405 --- /dev/null +++ b/DjangoBlog-master/djangoblog/admin_site.py @@ -0,0 +1,64 @@ +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 + +from accounts.admin import * +from blog.admin import * +from blog.models import * +from comments.admin import * +from comments.models import * +from djangoblog.logentryadmin import LogEntryAdmin +from oauth.admin import * +from oauth.models import * +from owntracks.admin import * +from owntracks.models import * +from servermanager.admin import * +from servermanager.models import * + + +class DjangoBlogAdminSite(AdminSite): + site_header = 'djangoblog administration' + site_title = 'djangoblog site admin' + + def __init__(self, name='admin'): + super().__init__(name) + + def has_permission(self, request): + return request.user.is_superuser + + # def get_urls(self): + # urls = super().get_urls() + # from django.urls import path + # from blog.views import refresh_memcache + # + # my_urls = [ + # path('refresh/', self.admin_view(refresh_memcache), name="refresh"), + # ] + # return urls + my_urls + + +admin_site = DjangoBlogAdminSite(name='admin') + +admin_site.register(Article, ArticlelAdmin) +admin_site.register(Category, CategoryAdmin) +admin_site.register(Tag, TagAdmin) +admin_site.register(Links, LinksAdmin) +admin_site.register(SideBar, SideBarAdmin) +admin_site.register(BlogSettings, BlogSettingsAdmin) + +admin_site.register(commands, CommandsAdmin) +admin_site.register(EmailSendLog, EmailSendLogAdmin) + +admin_site.register(BlogUser, BlogUserAdmin) + +admin_site.register(Comment, CommentAdmin) + +admin_site.register(OAuthUser, OAuthUserAdmin) +admin_site.register(OAuthConfig, OAuthConfigAdmin) + +admin_site.register(OwnTrackLog, OwnTrackLogsAdmin) + +admin_site.register(Site, SiteAdmin) + +admin_site.register(LogEntry, LogEntryAdmin) diff --git a/DjangoBlog-master/djangoblog/apps.py b/DjangoBlog-master/djangoblog/apps.py new file mode 100644 index 0000000..d29e318 --- /dev/null +++ b/DjangoBlog-master/djangoblog/apps.py @@ -0,0 +1,11 @@ +from django.apps import AppConfig + +class DjangoblogAppConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'djangoblog' + + def ready(self): + super().ready() + # Import and load plugins here + from .plugin_manage.loader import load_plugins + load_plugins() \ No newline at end of file diff --git a/DjangoBlog-master/djangoblog/blog_signals.py b/DjangoBlog-master/djangoblog/blog_signals.py new file mode 100644 index 0000000..393f441 --- /dev/null +++ b/DjangoBlog-master/djangoblog/blog_signals.py @@ -0,0 +1,122 @@ +import _thread +import logging + +import django.dispatch +from django.conf import settings +from django.contrib.admin.models import LogEntry +from django.contrib.auth.signals import user_logged_in, user_logged_out +from django.core.mail import EmailMultiAlternatives +from django.db.models.signals import post_save +from django.dispatch import receiver + +from comments.models import Comment +from comments.utils import send_comment_email +from djangoblog.spider_notify import SpiderNotify +from djangoblog.utils import cache, expire_view_cache, delete_sidebar_cache, delete_view_cache +from djangoblog.utils import get_current_site +from oauth.models import OAuthUser + +logger = logging.getLogger(__name__) + +oauth_user_login_signal = django.dispatch.Signal(['id']) +send_email_signal = django.dispatch.Signal( + ['emailto', 'title', 'content']) + + +@receiver(send_email_signal) +def send_email_signal_handler(sender, **kwargs): + emailto = kwargs['emailto'] + title = kwargs['title'] + content = kwargs['content'] + + msg = EmailMultiAlternatives( + title, + content, + from_email=settings.DEFAULT_FROM_EMAIL, + to=emailto) + msg.content_subtype = "html" + + from servermanager.models import EmailSendLog + log = EmailSendLog() + log.title = title + log.content = content + log.emailto = ','.join(emailto) + + try: + result = msg.send() + log.send_result = result > 0 + except Exception as e: + logger.error(f"失败邮箱号: {emailto}, {e}") + log.send_result = False + log.save() + + +@receiver(oauth_user_login_signal) +def oauth_user_login_signal_handler(sender, **kwargs): + id = kwargs['id'] + oauthuser = OAuthUser.objects.get(id=id) + site = get_current_site().domain + if oauthuser.picture and not oauthuser.picture.find(site) >= 0: + from djangoblog.utils import save_user_avatar + oauthuser.picture = save_user_avatar(oauthuser.picture) + oauthuser.save() + + delete_sidebar_cache() + + +@receiver(post_save) +def model_post_save_callback( + sender, + instance, + created, + raw, + using, + update_fields, + **kwargs): + clearcache = False + if isinstance(instance, LogEntry): + return + if 'get_full_url' in dir(instance): + is_update_views = update_fields == {'views'} + if not settings.TESTING and not is_update_views: + try: + notify_url = instance.get_full_url() + SpiderNotify.baidu_notify([notify_url]) + except Exception as ex: + logger.error("notify sipder", ex) + if not is_update_views: + clearcache = True + + if isinstance(instance, Comment): + if instance.is_enable: + path = instance.article.get_absolute_url() + site = get_current_site().domain + if site.find(':') > 0: + site = site[0:site.find(':')] + + expire_view_cache( + path, + servername=site, + serverport=80, + key_prefix='blogdetail') + if cache.get('seo_processor'): + cache.delete('seo_processor') + comment_cache_key = 'article_comments_{id}'.format( + id=instance.article.id) + cache.delete(comment_cache_key) + delete_sidebar_cache() + delete_view_cache('article_comments', [str(instance.article.pk)]) + + _thread.start_new_thread(send_comment_email, (instance,)) + + if clearcache: + cache.clear() + + +@receiver(user_logged_in) +@receiver(user_logged_out) +def user_auth_callback(sender, request, user, **kwargs): + if user and user.username: + logger.info(user) + delete_sidebar_cache() + # cache.clear() diff --git a/DjangoBlog-master/djangoblog/elasticsearch_backend.py b/DjangoBlog-master/djangoblog/elasticsearch_backend.py new file mode 100644 index 0000000..4afe498 --- /dev/null +++ b/DjangoBlog-master/djangoblog/elasticsearch_backend.py @@ -0,0 +1,183 @@ +from django.utils.encoding import force_str +from elasticsearch_dsl import Q +from haystack.backends import BaseEngine, BaseSearchBackend, BaseSearchQuery, log_query +from haystack.forms import ModelSearchForm +from haystack.models import SearchResult +from haystack.utils import log as logging + +from blog.documents import ArticleDocument, ArticleDocumentManager +from blog.models import Article + +logger = logging.getLogger(__name__) + + +class ElasticSearchBackend(BaseSearchBackend): + def __init__(self, connection_alias, **connection_options): + super( + ElasticSearchBackend, + self).__init__( + connection_alias, + **connection_options) + self.manager = ArticleDocumentManager() + self.include_spelling = True + + def _get_models(self, iterable): + models = iterable if iterable and iterable[0] else Article.objects.all() + docs = self.manager.convert_to_doc(models) + return docs + + def _create(self, models): + self.manager.create_index() + docs = self._get_models(models) + self.manager.rebuild(docs) + + def _delete(self, models): + for m in models: + m.delete() + return True + + def _rebuild(self, models): + models = models if models else Article.objects.all() + docs = self.manager.convert_to_doc(models) + self.manager.update_docs(docs) + + def update(self, index, iterable, commit=True): + + models = self._get_models(iterable) + self.manager.update_docs(models) + + def remove(self, obj_or_string): + models = self._get_models([obj_or_string]) + self._delete(models) + + def clear(self, models=None, commit=True): + self.remove(None) + + @staticmethod + def get_suggestion(query: str) -> str: + """获取推荐词, 如果没有找到添加原搜索词""" + + search = ArticleDocument.search() \ + .query("match", body=query) \ + .suggest('suggest_search', query, term={'field': 'body'}) \ + .execute() + + keywords = [] + for suggest in search.suggest.suggest_search: + if suggest["options"]: + keywords.append(suggest["options"][0]["text"]) + else: + keywords.append(suggest["text"]) + + return ' '.join(keywords) + + @log_query + def search(self, query_string, **kwargs): + logger.info('search query_string:' + query_string) + + start_offset = kwargs.get('start_offset') + end_offset = kwargs.get('end_offset') + + # 推荐词搜索 + if getattr(self, "is_suggest", None): + suggestion = self.get_suggestion(query_string) + else: + suggestion = query_string + + q = Q('bool', + should=[Q('match', body=suggestion), Q('match', title=suggestion)], + minimum_should_match="70%") + + search = ArticleDocument.search() \ + .query('bool', filter=[q]) \ + .filter('term', status='p') \ + .filter('term', type='a') \ + .source(False)[start_offset: end_offset] + + results = search.execute() + hits = results['hits'].total + raw_results = [] + for raw_result in results['hits']['hits']: + app_label = 'blog' + model_name = 'Article' + additional_fields = {} + + result_class = SearchResult + + result = result_class( + app_label, + model_name, + raw_result['_id'], + raw_result['_score'], + **additional_fields) + raw_results.append(result) + facets = {} + spelling_suggestion = None if query_string == suggestion else suggestion + + return { + 'results': raw_results, + 'hits': hits, + 'facets': facets, + 'spelling_suggestion': spelling_suggestion, + } + + +class ElasticSearchQuery(BaseSearchQuery): + def _convert_datetime(self, date): + if hasattr(date, 'hour'): + return force_str(date.strftime('%Y%m%d%H%M%S')) + else: + return force_str(date.strftime('%Y%m%d000000')) + + def clean(self, query_fragment): + """ + 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.replace(word, word.lower()) + + for char in self.backend.RESERVED_CHARACTERS: + if char in word: + word = "'%s'" % word + break + + cleaned_words.append(word) + + return ' '.join(cleaned_words) + + def build_query_fragment(self, field, filter_type, value): + return value.query_string + + def get_count(self): + results = self.get_results() + return len(results) if results else 0 + + def get_spelling_suggestion(self, preferred_query=None): + return self._spelling_suggestion + + def build_params(self, spelling_query=None): + kwargs = super(ElasticSearchQuery, self).build_params(spelling_query=spelling_query) + return kwargs + + +class ElasticSearchModelSearchForm(ModelSearchForm): + + def search(self): + # 是否建议搜索 + self.searchqueryset.query.backend.is_suggest = self.data.get("is_suggest") != "no" + sqs = super().search() + return sqs + + +class ElasticSearchEngine(BaseEngine): + backend = ElasticSearchBackend + query = ElasticSearchQuery diff --git a/DjangoBlog-master/djangoblog/feeds.py b/DjangoBlog-master/djangoblog/feeds.py new file mode 100644 index 0000000..8c4e851 --- /dev/null +++ b/DjangoBlog-master/djangoblog/feeds.py @@ -0,0 +1,40 @@ +from django.contrib.auth import get_user_model +from django.contrib.syndication.views import Feed +from django.utils import timezone +from django.utils.feedgenerator import Rss201rev2Feed + +from blog.models import Article +from djangoblog.utils import CommonMarkdown + + +class DjangoBlogFeed(Feed): + feed_type = Rss201rev2Feed + + description = '大巧无工,重剑无锋.' + title = "且听风吟 大巧无工,重剑无锋. " + link = "/feed/" + + def author_name(self): + return get_user_model().objects.first().nickname + + def author_link(self): + return get_user_model().objects.first().get_absolute_url() + + def items(self): + return Article.objects.filter(type='a', status='p').order_by('-pub_time')[:5] + + def item_title(self, item): + return item.title + + def item_description(self, item): + return CommonMarkdown.get_markdown(item.body) + + def feed_copyright(self): + now = timezone.now() + return "Copyright© {year} 且听风吟".format(year=now.year) + + def item_link(self, item): + return item.get_absolute_url() + + def item_guid(self, item): + return diff --git a/DjangoBlog-master/djangoblog/logentryadmin.py b/DjangoBlog-master/djangoblog/logentryadmin.py new file mode 100644 index 0000000..2f6a535 --- /dev/null +++ b/DjangoBlog-master/djangoblog/logentryadmin.py @@ -0,0 +1,91 @@ +from django.contrib import admin +from django.contrib.admin.models import DELETION +from django.contrib.contenttypes.models import ContentType +from django.urls import reverse, NoReverseMatch +from django.utils.encoding import force_str +from django.utils.html import escape +from django.utils.safestring import mark_safe +from django.utils.translation import gettext_lazy as _ + + +class LogEntryAdmin(admin.ModelAdmin): + list_filter = [ + 'content_type' + ] + + search_fields = [ + 'object_repr', + 'change_message' + ] + + list_display_links = [ + 'action_time', + 'get_change_message', + ] + list_display = [ + 'action_time', + 'user_link', + 'content_type', + 'object_link', + 'get_change_message', + ] + + def has_add_permission(self, request): + return False + + def has_change_permission(self, request, obj=None): + return ( + request.user.is_superuser or + request.user.has_perm('admin.change_logentry') + ) and request.method != 'POST' + + def has_delete_permission(self, request, obj=None): + return False + + def object_link(self, obj): + object_link = escape(obj.object_repr) + content_type = obj.content_type + + if obj.action_flag != DELETION and content_type is not None: + # try returning an actual link instead of object repr string + try: + url = reverse( + 'admin:{}_{}_change'.format(content_type.app_label, + content_type.model), + args=[obj.object_id] + ) + object_link = '{}'.format(url, object_link) + except NoReverseMatch: + pass + return mark_safe(object_link) + + object_link.admin_order_field = 'object_repr' + object_link.short_description = _('object') + + def user_link(self, obj): + content_type = ContentType.objects.get_for_model(type(obj.user)) + user_link = escape(force_str(obj.user)) + try: + # try returning an actual link instead of object repr string + url = reverse( + 'admin:{}_{}_change'.format(content_type.app_label, + content_type.model), + args=[obj.user.pk] + ) + user_link = '{}'.format(url, user_link) + except NoReverseMatch: + pass + return mark_safe(user_link) + + user_link.admin_order_field = 'user' + user_link.short_description = _('user') + + def get_queryset(self, request): + queryset = super(LogEntryAdmin, self).get_queryset(request) + return queryset.prefetch_related('content_type') + + def get_actions(self, request): + actions = super(LogEntryAdmin, self).get_actions(request) + if 'delete_selected' in actions: + del actions['delete_selected'] + return actions diff --git a/DjangoBlog-master/djangoblog/plugin_manage/base_plugin.py b/DjangoBlog-master/djangoblog/plugin_manage/base_plugin.py new file mode 100644 index 0000000..2b4be5c --- /dev/null +++ b/DjangoBlog-master/djangoblog/plugin_manage/base_plugin.py @@ -0,0 +1,41 @@ +import logging + +logger = logging.getLogger(__name__) + + +class BasePlugin: + # 插件元数据 + PLUGIN_NAME = None + PLUGIN_DESCRIPTION = None + PLUGIN_VERSION = None + + def __init__(self): + if not all([self.PLUGIN_NAME, self.PLUGIN_DESCRIPTION, self.PLUGIN_VERSION]): + raise ValueError("Plugin metadata (PLUGIN_NAME, PLUGIN_DESCRIPTION, PLUGIN_VERSION) must be defined.") + self.init_plugin() + self.register_hooks() + + def init_plugin(self): + """ + 插件初始化逻辑 + 子类可以重写此方法来实现特定的初始化操作 + """ + logger.info(f'{self.PLUGIN_NAME} initialized.') + + def register_hooks(self): + """ + 注册插件钩子 + 子类可以重写此方法来注册特定的钩子 + """ + pass + + def get_plugin_info(self): + """ + 获取插件信息 + :return: 包含插件元数据的字典 + """ + return { + 'name': self.PLUGIN_NAME, + 'description': self.PLUGIN_DESCRIPTION, + 'version': self.PLUGIN_VERSION + } diff --git a/DjangoBlog-master/djangoblog/plugin_manage/hook_constants.py b/DjangoBlog-master/djangoblog/plugin_manage/hook_constants.py new file mode 100644 index 0000000..6685b7c --- /dev/null +++ b/DjangoBlog-master/djangoblog/plugin_manage/hook_constants.py @@ -0,0 +1,7 @@ +ARTICLE_DETAIL_LOAD = 'article_detail_load' +ARTICLE_CREATE = 'article_create' +ARTICLE_UPDATE = 'article_update' +ARTICLE_DELETE = 'article_delete' + +ARTICLE_CONTENT_HOOK_NAME = "the_content" + diff --git a/DjangoBlog-master/djangoblog/plugin_manage/hooks.py b/DjangoBlog-master/djangoblog/plugin_manage/hooks.py new file mode 100644 index 0000000..d712540 --- /dev/null +++ b/DjangoBlog-master/djangoblog/plugin_manage/hooks.py @@ -0,0 +1,44 @@ +import logging + +logger = logging.getLogger(__name__) + +_hooks = {} + + +def register(hook_name: str, callback: callable): + """ + 注册一个钩子回调。 + """ + if hook_name not in _hooks: + _hooks[hook_name] = [] + _hooks[hook_name].append(callback) + logger.debug(f"Registered hook '{hook_name}' with callback '{callback.__name__}'") + + +def run_action(hook_name: str, *args, **kwargs): + """ + 执行一个 Action Hook。 + 它会按顺序执行所有注册到该钩子上的回调函数。 + """ + if hook_name in _hooks: + logger.debug(f"Running action hook '{hook_name}'") + for callback in _hooks[hook_name]: + try: + callback(*args, **kwargs) + except Exception as e: + logger.error(f"Error running action hook '{hook_name}' callback '{callback.__name__}': {e}", exc_info=True) + + +def apply_filters(hook_name: str, value, *args, **kwargs): + """ + 执行一个 Filter Hook。 + 它会把 value 依次传递给所有注册的回调函数进行处理。 + """ + if hook_name in _hooks: + logger.debug(f"Applying filter hook '{hook_name}'") + for callback in _hooks[hook_name]: + try: + value = callback(value, *args, **kwargs) + except Exception as e: + logger.error(f"Error applying filter hook '{hook_name}' callback '{callback.__name__}': {e}", exc_info=True) + return value diff --git a/DjangoBlog-master/djangoblog/plugin_manage/loader.py b/DjangoBlog-master/djangoblog/plugin_manage/loader.py new file mode 100644 index 0000000..12e824b --- /dev/null +++ b/DjangoBlog-master/djangoblog/plugin_manage/loader.py @@ -0,0 +1,19 @@ +import os +import logging +from django.conf import settings + +logger = logging.getLogger(__name__) + +def load_plugins(): + """ + Dynamically loads and initializes plugins from the 'plugins' directory. + This function is intended to be called when the Django app registry is ready. + """ + for plugin_name in settings.ACTIVE_PLUGINS: + plugin_path = os.path.join(settings.PLUGINS_DIR, plugin_name) + if os.path.isdir(plugin_path) and os.path.exists(os.path.join(plugin_path, 'plugin.py')): + try: + __import__(f'plugins.{plugin_name}.plugin') + logger.info(f"Successfully loaded plugin: {plugin_name}") + except ImportError as e: + logger.error(f"Failed to import plugin: {plugin_name}", exc_info=e) \ No newline at end of file diff --git a/DjangoBlog-master/djangoblog/settings.py b/DjangoBlog-master/djangoblog/settings.py new file mode 100644 index 0000000..d2d7f28 --- /dev/null +++ b/DjangoBlog-master/djangoblog/settings.py @@ -0,0 +1,343 @@ +""" +Django settings for djangoblog project. + +Generated by 'django-admin startproject' using Django 1.10.2. + +For more information on this file, see +https://docs.djangoproject.com/en/1.10/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/1.10/ref/settings/ +""" +import os +import sys +from pathlib import Path + +from django.utils.translation import gettext_lazy as _ + + +def env_to_bool(env, default): + str_val = os.environ.get(env) + return default if str_val is None else str_val == 'True' + + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = os.environ.get( + 'DJANGO_SECRET_KEY') or 'n9ceqv38)#&mwuat@(mjb_p%em$e8$qyr#fw9ot!=ba6lijx-6' +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = env_to_bool('DJANGO_DEBUG', True) +# DEBUG = False +TESTING = len(sys.argv) > 1 and sys.argv[1] == 'test' + +# ALLOWED_HOSTS = [] +ALLOWED_HOSTS = ['*', '127.0.0.1', 'example.com'] +# django 4.0新增配置 +CSRF_TRUSTED_ORIGINS = ['http://example.com'] +# Application definition + + +INSTALLED_APPS = [ + # 'django.contrib.admin', + 'django.contrib.admin.apps.SimpleAdminConfig', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'django.contrib.sites', + 'django.contrib.sitemaps', + 'mdeditor', + 'haystack', + 'blog', + 'accounts', + 'comments', + 'oauth', + 'servermanager', + 'owntracks', + 'compressor', + 'djangoblog' +] + +MIDDLEWARE = [ + + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.locale.LocaleMiddleware', + 'django.middleware.gzip.GZipMiddleware', + # 'django.middleware.cache.UpdateCacheMiddleware', + 'django.middleware.common.CommonMiddleware', + # 'django.middleware.cache.FetchFromCacheMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'django.middleware.http.ConditionalGetMiddleware', + 'blog.middleware.OnlineMiddleware' +] + +ROOT_URLCONF = 'djangoblog.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [os.path.join(BASE_DIR, 'templates')], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + 'blog.context_processors.seo_processor' + ], + }, + }, +] + +WSGI_APPLICATION = 'djangoblog.wsgi.application' + +# Database +# https://docs.djangoproject.com/en/1.10/ref/settings/#databases + + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.mysql', + 'NAME': os.environ.get('DJANGO_MYSQL_DATABASE') or 'djangoblog', + 'USER': os.environ.get('DJANGO_MYSQL_USER') or 'root', + 'PASSWORD': os.environ.get('DJANGO_MYSQL_PASSWORD') or '050816', + 'HOST': os.environ.get('DJANGO_MYSQL_HOST') or '127.0.0.1', + 'PORT': int( + os.environ.get('DJANGO_MYSQL_PORT') or 3306), + 'OPTIONS': { + 'charset': 'utf8mb4'}, + }} + +# Password validation +# https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + +LANGUAGES = ( + ('en', _('English')), + ('zh-hans', _('Simplified Chinese')), + ('zh-hant', _('Traditional Chinese')), +) +LOCALE_PATHS = ( + os.path.join(BASE_DIR, 'locale'), +) + +LANGUAGE_CODE = 'zh-hans' + +TIME_ZONE = 'Asia/Shanghai' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = False + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/1.10/howto/static-files/ + + +HAYSTACK_CONNECTIONS = { + 'default': { + 'ENGINE': 'djangoblog.whoosh_cn_backend.WhooshEngine', + 'PATH': os.path.join(os.path.dirname(__file__), 'whoosh_index'), + }, +} +# Automatically update searching index +HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor' +# Allow user login with username and password +AUTHENTICATION_BACKENDS = [ + 'accounts.user_login_backend.EmailOrUsernameModelBackend'] + +STATIC_ROOT = os.path.join(BASE_DIR, 'collectedstatic') + +STATIC_URL = '/static/' +STATICFILES = os.path.join(BASE_DIR, 'static') + +AUTH_USER_MODEL = 'accounts.BlogUser' +LOGIN_URL = '/login/' + +TIME_FORMAT = '%Y-%m-%d %H:%M:%S' +DATE_TIME_FORMAT = '%Y-%m-%d' + +# bootstrap color styles +BOOTSTRAP_COLOR_TYPES = [ + 'default', 'primary', 'success', 'info', 'warning', 'danger' +] + +# paginate +PAGINATE_BY = 10 +# http cache timeout +CACHE_CONTROL_MAX_AGE = 2592000 +# cache setting +CACHES = { + 'default': { + 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', + 'TIMEOUT': 10800, + 'LOCATION': 'unique-snowflake', + } +} +# 使用redis作为缓存 +if os.environ.get("DJANGO_REDIS_URL"): + CACHES = { + 'default': { + 'BACKEND': 'django.core.cache.backends.redis.RedisCache', + 'LOCATION': f'redis://{os.environ.get("DJANGO_REDIS_URL")}', + } + } + +SITE_ID = 1 +BAIDU_NOTIFY_URL = os.environ.get('DJANGO_BAIDU_NOTIFY_URL') \ + or 'http://data.zz.baidu.com/urls?site=https://www.lylinux.net&token=1uAOGrMsUm5syDGn' + +# Email: +EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' +EMAIL_USE_TLS = env_to_bool('DJANGO_EMAIL_TLS', False) +EMAIL_USE_SSL = env_to_bool('DJANGO_EMAIL_SSL', True) +EMAIL_HOST = os.environ.get('DJANGO_EMAIL_HOST') or 'smtp.mxhichina.com' +EMAIL_PORT = int(os.environ.get('DJANGO_EMAIL_PORT') or 465) +EMAIL_HOST_USER = os.environ.get('DJANGO_EMAIL_USER') +EMAIL_HOST_PASSWORD = os.environ.get('DJANGO_EMAIL_PASSWORD') +DEFAULT_FROM_EMAIL = EMAIL_HOST_USER +SERVER_EMAIL = EMAIL_HOST_USER +# Setting debug=false did NOT handle except email notifications +ADMINS = [('admin', os.environ.get('DJANGO_ADMIN_EMAIL') or 'admin@admin.com')] +# WX ADMIN password(Two times md5) +WXADMIN = os.environ.get( + 'DJANGO_WXADMIN_PASSWORD') or '995F03AC401D6CABABAEF756FC4D43C7' + +LOG_PATH = os.path.join(BASE_DIR, 'logs') +if not os.path.exists(LOG_PATH): + os.makedirs(LOG_PATH, exist_ok=True) + +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'root': { + 'level': 'INFO', + 'handlers': ['console', 'log_file'], + }, + 'formatters': { + 'verbose': { + 'format': '[%(asctime)s] %(levelname)s [%(name)s.%(funcName)s:%(lineno)d %(module)s] %(message)s', + } + }, + 'filters': { + 'require_debug_false': { + '()': 'django.utils.log.RequireDebugFalse', + }, + 'require_debug_true': { + '()': 'django.utils.log.RequireDebugTrue', + }, + }, + 'handlers': { + 'log_file': { + 'level': 'INFO', + 'class': 'logging.handlers.TimedRotatingFileHandler', + 'filename': os.path.join(LOG_PATH, 'djangoblog.log'), + 'when': 'D', + 'formatter': 'verbose', + 'interval': 1, + 'delay': True, + 'backupCount': 5, + 'encoding': 'utf-8' + }, + 'console': { + 'level': 'DEBUG', + 'filters': ['require_debug_true'], + 'class': 'logging.StreamHandler', + 'formatter': 'verbose' + }, + 'null': { + 'class': 'logging.NullHandler', + }, + 'mail_admins': { + 'level': 'ERROR', + 'filters': ['require_debug_false'], + 'class': 'django.utils.log.AdminEmailHandler' + } + }, + 'loggers': { + 'djangoblog': { + 'handlers': ['log_file', 'console'], + 'level': 'INFO', + 'propagate': True, + }, + 'django.request': { + 'handlers': ['mail_admins'], + 'level': 'ERROR', + 'propagate': False, + } + } +} + +STATICFILES_FINDERS = ( + 'django.contrib.staticfiles.finders.FileSystemFinder', + 'django.contrib.staticfiles.finders.AppDirectoriesFinder', + # other + 'compressor.finders.CompressorFinder', +) +COMPRESS_ENABLED = True +# COMPRESS_OFFLINE = True + + +COMPRESS_CSS_FILTERS = [ + # creates absolute urls from relative ones + 'compressor.filters.css_default.CssAbsoluteFilter', + # css minimizer + 'compressor.filters.cssmin.CSSMinFilter' +] +COMPRESS_JS_FILTERS = [ + 'compressor.filters.jsmin.JSMinFilter' +] + +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'): + ELASTICSEARCH_DSL = { + 'default': { + 'hosts': os.environ.get('DJANGO_ELASTICSEARCH_HOST') + }, + } + HAYSTACK_CONNECTIONS = { + 'default': { + 'ENGINE': 'djangoblog.elasticsearch_backend.ElasticSearchEngine', + }, + } + +# Plugin System +PLUGINS_DIR = BASE_DIR / 'plugins' +ACTIVE_PLUGINS = [ + 'article_copyright', + 'reading_time', + 'external_links', + 'view_count', + 'seo_optimizer' +] \ No newline at end of file diff --git a/DjangoBlog-master/djangoblog/sitemap.py b/DjangoBlog-master/djangoblog/sitemap.py new file mode 100644 index 0000000..8b7d446 --- /dev/null +++ b/DjangoBlog-master/djangoblog/sitemap.py @@ -0,0 +1,59 @@ +from django.contrib.sitemaps import Sitemap +from django.urls import reverse + +from blog.models import Article, Category, Tag + + +class StaticViewSitemap(Sitemap): + priority = 0.5 + changefreq = 'daily' + + def items(self): + return ['blog:index', ] + + def location(self, item): + return reverse(item) + + +class ArticleSiteMap(Sitemap): + changefreq = "monthly" + priority = "0.6" + + def items(self): + return Article.objects.filter(status='p') + + def lastmod(self, obj): + return obj.last_modify_time + + +class CategorySiteMap(Sitemap): + changefreq = "Weekly" + priority = "0.6" + + def items(self): + return Category.objects.all() + + def lastmod(self, obj): + return obj.last_modify_time + + +class TagSiteMap(Sitemap): + changefreq = "Weekly" + priority = "0.3" + + def items(self): + return Tag.objects.all() + + def lastmod(self, obj): + return obj.last_modify_time + + +class UserSiteMap(Sitemap): + changefreq = "Weekly" + priority = "0.3" + + def items(self): + return list(set(map(lambda x: x.author, Article.objects.all()))) + + def lastmod(self, obj): + return obj.date_joined diff --git a/DjangoBlog-master/djangoblog/spider_notify.py b/DjangoBlog-master/djangoblog/spider_notify.py new file mode 100644 index 0000000..7b909e9 --- /dev/null +++ b/DjangoBlog-master/djangoblog/spider_notify.py @@ -0,0 +1,21 @@ +import logging + +import requests +from django.conf import settings + +logger = logging.getLogger(__name__) + + +class SpiderNotify(): + @staticmethod + def baidu_notify(urls): + try: + data = '\n'.join(urls) + result = requests.post(settings.BAIDU_NOTIFY_URL, data=data) + logger.info(result.text) + except Exception as e: + logger.error(e) + + @staticmethod + def notify(url): + SpiderNotify.baidu_notify(url) diff --git a/DjangoBlog-master/djangoblog/tests.py b/DjangoBlog-master/djangoblog/tests.py new file mode 100644 index 0000000..01237d9 --- /dev/null +++ b/DjangoBlog-master/djangoblog/tests.py @@ -0,0 +1,32 @@ +from django.test import TestCase + +from djangoblog.utils import * + + +class DjangoBlogTest(TestCase): + def setUp(self): + pass + + def test_utils(self): + md5 = get_sha256('test') + self.assertIsNotNone(md5) + c = CommonMarkdown.get_markdown(''' + # Title1 + + ```python + import os + ``` + + [url](https://www.lylinux.net/) + + [ddd](http://www.baidu.com) + + + ''') + self.assertIsNotNone(c) + d = { + 'd': 'key1', + 'd2': 'key2' + } + data = parse_dict_to_url(d) + self.assertIsNotNone(data) diff --git a/DjangoBlog-master/djangoblog/urls.py b/DjangoBlog-master/djangoblog/urls.py new file mode 100644 index 0000000..4aae58a --- /dev/null +++ b/DjangoBlog-master/djangoblog/urls.py @@ -0,0 +1,64 @@ +"""djangoblog URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/1.10/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.conf.urls import url, include + 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) +""" +from django.conf import settings +from django.conf.urls.i18n import i18n_patterns +from django.conf.urls.static import static +from django.contrib.sitemaps.views import sitemap +from django.urls import path, include +from django.urls import re_path +from haystack.views import search_view_factory + +from blog.views import EsSearchView +from djangoblog.admin_site import admin_site +from djangoblog.elasticsearch_backend import ElasticSearchModelSearchForm +from djangoblog.feeds import DjangoBlogFeed +from djangoblog.sitemap import ArticleSiteMap, CategorySiteMap, StaticViewSitemap, TagSiteMap, UserSiteMap + +sitemaps = { + + 'blog': ArticleSiteMap, + 'Category': CategorySiteMap, + 'Tag': TagSiteMap, + 'User': UserSiteMap, + 'static': StaticViewSitemap +} + +handler404 = 'blog.views.page_not_found_view' +handler500 = 'blog.views.server_error_view' +handle403 = 'blog.views.permission_denied_view' + +urlpatterns = [ + path('i18n/', include('django.conf.urls.i18n')), +] +urlpatterns += i18n_patterns( + re_path(r'^admin/', admin_site.urls), + re_path(r'', include('blog.urls', namespace='blog')), + re_path(r'mdeditor/', include('mdeditor.urls')), + re_path(r'', include('comments.urls', namespace='comment')), + re_path(r'', include('accounts.urls', namespace='account')), + re_path(r'', include('oauth.urls', namespace='oauth')), + re_path(r'^sitemap\.xml$', sitemap, {'sitemaps': sitemaps}, + name='django.contrib.sitemaps.views.sitemap'), + re_path(r'^feed/$', DjangoBlogFeed()), + re_path(r'^rss/$', DjangoBlogFeed()), + re_path('^search', search_view_factory(view_class=EsSearchView, form_class=ElasticSearchModelSearchForm), + name='search'), + re_path(r'', include('servermanager.urls', namespace='servermanager')), + re_path(r'', include('owntracks.urls', namespace='owntracks')) + , prefix_default_language=False) + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) +if settings.DEBUG: + urlpatterns += static(settings.MEDIA_URL, + document_root=settings.MEDIA_ROOT) diff --git a/DjangoBlog-master/djangoblog/utils.py b/DjangoBlog-master/djangoblog/utils.py new file mode 100644 index 0000000..57f63dc --- /dev/null +++ b/DjangoBlog-master/djangoblog/utils.py @@ -0,0 +1,232 @@ +#!/usr/bin/env python +# encoding: utf-8 + + +import logging +import os +import random +import string +import uuid +from hashlib import sha256 + +import bleach +import markdown +import requests +from django.conf import settings +from django.contrib.sites.models import Site +from django.core.cache import cache +from django.templatetags.static import static + +logger = logging.getLogger(__name__) + + +def get_max_articleid_commentid(): + from blog.models import Article + from comments.models import Comment + return (Article.objects.latest().pk, Comment.objects.latest().pk) + + +def get_sha256(str): + m = sha256(str.encode('utf-8')) + return m.hexdigest() + + +def cache_decorator(expiration=3 * 60): + def wrapper(func): + def news(*args, **kwargs): + try: + view = args[0] + key = view.get_cache_key() + except: + key = None + if not key: + unique_str = repr((func, args, kwargs)) + + m = sha256(unique_str.encode('utf-8')) + key = m.hexdigest() + value = cache.get(key) + if value is not None: + # logger.info('cache_decorator get cache:%s key:%s' % (func.__name__, key)) + if str(value) == '__default_cache_value__': + return None + else: + return value + else: + logger.debug( + 'cache_decorator set cache:%s key:%s' % + (func.__name__, key)) + value = func(*args, **kwargs) + if value is None: + cache.set(key, '__default_cache_value__', expiration) + else: + cache.set(key, value, expiration) + return value + + return news + + return wrapper + + +def expire_view_cache(path, servername, serverport, key_prefix=None): + ''' + 刷新视图缓存 + :param path:url路径 + :param servername:host + :param serverport:端口 + :param key_prefix:前缀 + :return:是否成功 + ''' + from django.http import HttpRequest + from django.utils.cache import get_cache_key + + request = HttpRequest() + request.META = {'SERVER_NAME': servername, 'SERVER_PORT': serverport} + request.path = path + + key = get_cache_key(request, key_prefix=key_prefix, cache=cache) + if key: + logger.info('expire_view_cache:get key:{path}'.format(path=path)) + if cache.get(key): + cache.delete(key) + return True + return False + + +@cache_decorator() +def get_current_site(): + site = Site.objects.get_current() + return site + + +class CommonMarkdown: + @staticmethod + def _convert_markdown(value): + md = markdown.Markdown( + extensions=[ + 'extra', + 'codehilite', + 'toc', + 'tables', + ] + ) + body = md.convert(value) + toc = md.toc + return body, toc + + @staticmethod + def get_markdown_with_toc(value): + body, toc = CommonMarkdown._convert_markdown(value) + return body, toc + + @staticmethod + def get_markdown(value): + body, toc = CommonMarkdown._convert_markdown(value) + return body + + +def send_email(emailto, title, content): + from djangoblog.blog_signals import send_email_signal + send_email_signal.send( + send_email.__class__, + emailto=emailto, + title=title, + content=content) + + +def generate_code() -> str: + """生成随机数验证码""" + return ''.join(random.sample(string.digits, 6)) + + +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 + + +def get_blog_setting(): + value = cache.get('get_blog_setting') + if value: + return value + else: + from blog.models import BlogSettings + if not BlogSettings.objects.count(): + setting = BlogSettings() + setting.site_name = 'djangoblog' + setting.site_description = '基于Django的博客系统' + setting.site_seo_description = '基于Django的博客系统' + setting.site_keywords = 'Django,Python' + setting.article_sub_length = 300 + setting.sidebar_article_count = 10 + setting.sidebar_comment_count = 5 + setting.show_google_adsense = False + setting.open_site_comment = True + setting.analytics_code = '' + setting.beian_code = '' + setting.show_gongan_code = False + setting.comment_need_review = False + setting.save() + value = BlogSettings.objects.first() + logger.info('set cache get_blog_setting') + cache.set('get_blog_setting', value) + return value + + +def save_user_avatar(url): + ''' + 保存用户头像 + :param url:头像url + :return: 本地路径 + ''' + logger.info(url) + + try: + basedir = os.path.join(settings.STATICFILES, 'avatar') + rsp = requests.get(url, timeout=2) + if rsp.status_code == 200: + if not os.path.exists(basedir): + os.makedirs(basedir) + + image_extensions = ['.jpg', '.png', 'jpeg', '.gif'] + isimage = len([i for i in image_extensions if url.endswith(i)]) > 0 + ext = os.path.splitext(url)[1] if isimage else '.jpg' + save_filename = str(uuid.uuid4().hex) + ext + logger.info('保存用户头像:' + basedir + save_filename) + with open(os.path.join(basedir, save_filename), 'wb+') as file: + file.write(rsp.content) + return static('avatar/' + save_filename) + except Exception as e: + logger.error(e) + return static('blog/img/avatar.png') + + +def delete_sidebar_cache(): + from blog.models import LinkShowType + keys = ["sidebar" + x for x in LinkShowType.values] + for k in keys: + logger.info('delete sidebar key:' + k) + cache.delete(k) + + +def delete_view_cache(prefix, keys): + from django.core.cache.utils import make_template_fragment_key + key = make_template_fragment_key(prefix, keys) + cache.delete(key) + + +def get_resource_url(): + if settings.STATIC_URL: + return settings.STATIC_URL + else: + site = get_current_site() + return 'http://' + site.domain + '/static/' + + +ALLOWED_TAGS = ['a', 'abbr', 'acronym', 'b', 'blockquote', 'code', 'em', 'i', 'li', 'ol', 'pre', 'strong', 'ul', 'h1', + 'h2', 'p'] +ALLOWED_ATTRIBUTES = {'a': ['href', 'title'], 'abbr': ['title'], 'acronym': ['title']} + + +def sanitize_html(html): + return bleach.clean(html, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRIBUTES) diff --git a/DjangoBlog-master/djangoblog/whoosh_cn_backend.py b/DjangoBlog-master/djangoblog/whoosh_cn_backend.py new file mode 100644 index 0000000..04e3f7f --- /dev/null +++ b/DjangoBlog-master/djangoblog/whoosh_cn_backend.py @@ -0,0 +1,1044 @@ +# encoding: utf-8 + +from __future__ import absolute_import, division, print_function, unicode_literals + +import json +import os +import re +import shutil +import threading +import warnings + +import six +from django.conf import settings +from django.core.exceptions import ImproperlyConfigured +from datetime import datetime +from django.utils.encoding import force_str +from haystack.backends import BaseEngine, BaseSearchBackend, BaseSearchQuery, EmptyResults, log_query +from haystack.constants import DJANGO_CT, DJANGO_ID, ID +from haystack.exceptions import MissingDependency, SearchBackendError, SkipDocument +from haystack.inputs import Clean, Exact, PythonData, Raw +from haystack.models import SearchResult +from haystack.utils import get_identifier, get_model_ct +from haystack.utils import log as logging +from haystack.utils.app_loading import haystack_get_model +from jieba.analyse import ChineseAnalyzer +from whoosh import index +from whoosh.analysis import StemmingAnalyzer +from whoosh.fields import BOOLEAN, DATETIME, IDLIST, KEYWORD, NGRAM, NGRAMWORDS, NUMERIC, Schema, TEXT +from whoosh.fields import ID as WHOOSH_ID +from whoosh.filedb.filestore import FileStorage, RamStorage +from whoosh.highlight import ContextFragmenter, HtmlFormatter +from whoosh.highlight import highlight as whoosh_highlight +from whoosh.qparser import QueryParser +from whoosh.searching import ResultsPage +from whoosh.writing import AsyncWriter + +try: + import whoosh +except ImportError: + raise MissingDependency( + "The 'whoosh' backend requires the installation of 'Whoosh'. Please refer to the documentation.") + +# 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.") + +# 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 + + +class WhooshHtmlFormatter(HtmlFormatter): + """ + 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' + + +class WhooshSearchBackend(BaseSearchBackend): + # Word reserved by Whoosh for special use. + RESERVED_WORDS = ( + 'AND', + 'NOT', + 'OR', + 'TO', + ) + + # Characters reserved by Whoosh for special use. + # The '\\' must come first, so as not to overwrite the other slash + # replacements. + RESERVED_CHARACTERS = ( + '\\', '+', '-', '&&', '||', '!', '(', ')', '{', '}', + '[', ']', '^', '"', '~', '*', '?', ':', '.', + ) + + def __init__(self, connection_alias, **connection_options): + super( + WhooshSearchBackend, + self).__init__( + connection_alias, + **connection_options) + self.setup_complete = False + self.use_file_storage = True + self.post_limit = getattr( + connection_options, + 'POST_LIMIT', + 128 * 1024 * 1024) + self.path = connection_options.get('PATH') + + if connection_options.get('STORAGE', 'file') != 'file': + self.use_file_storage = False + + if self.use_file_storage and not self.path: + raise ImproperlyConfigured( + "You must specify a 'PATH' in your settings for connection '%s'." % + connection_alias) + + self.log = logging.getLogger('haystack') + + def setup(self): + """ + 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) + + 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 + + self.content_field_name, self.schema = self.build_schema( + connections[self.connection_alias].get_unified_index().all_searchfields()) + self.parser = QueryParser(self.content_field_name, schema=self.schema) + + if new_index is True: + self.index = self.storage.create_index(self.schema) + else: + try: + self.index = self.storage.open_index(schema=self.schema) + except index.EmptyIndexError: + self.index = self.storage.create_index(self.schema) + + self.setup_complete = True + + def build_schema(self, fields): + 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) + else: + schema_fields[field_class.index_fieldname] = KEYWORD( + stored=True, commas=True, scorable=True, field_boost=field_class.boost) + elif field_class.field_type in ['date', 'datetime']: + schema_fields[field_class.index_fieldname] = DATETIME( + stored=field_class.stored, sortable=True) + elif field_class.field_type == 'integer': + schema_fields[field_class.index_fieldname] = NUMERIC( + stored=field_class.stored, numtype=int, field_boost=field_class.boost) + elif field_class.field_type == 'float': + schema_fields[field_class.index_fieldname] = NUMERIC( + stored=field_class.stored, numtype=float, field_boost=field_class.boost) + elif field_class.field_type == 'boolean': + # Field boost 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) + 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) + else: + # 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 + + # 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.") + + return (content_field_name, Schema(**schema_fields)) + + def update(self, index, iterable, commit=True): + if not self.setup_complete: + self.setup() + + self.index = self.index.refresh() + writer = AsyncWriter(self.index) + + for obj in iterable: + try: + doc = index.full_prepare(obj) + except SkipDocument: + self.log.debug(u"Indexing for object `%s` skipped", obj) + else: + # 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]) + + # Document boosts aren't supported in Whoosh 2.5.0+. + if 'boost' in doc: + del doc['boost'] + + try: + writer.update_document(**doc) + except Exception as e: + if not self.silently_fail: + raise + + # 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: + # For now, commit no matter what, as we run into locking issues + # otherwise. + writer.commit() + + def remove(self, obj_or_string, commit=True): + if not self.setup_complete: + self.setup() + + self.index = self.index.refresh() + whoosh_id = get_identifier(obj_or_string) + + try: + self.index.delete_by_query( + q=self.parser.parse( + u'%s:"%s"' % + (ID, whoosh_id))) + except Exception as e: + if not self.silently_fail: + raise + + self.log.error( + "Failed to remove document '%s' from Whoosh: %s", + whoosh_id, + e, + exc_info=True) + + def clear(self, models=None, commit=True): + if not self.setup_complete: + self.setup() + + self.index = self.index.refresh() + + if models is not None: + assert isinstance(models, (list, tuple)) + + try: + if models is None: + self.delete_index() + else: + models_to_delete = [] + + for model in models: + models_to_delete.append( + u"%s:%s" % + (DJANGO_CT, get_model_ct(model))) + + self.index.delete_by_query( + q=self.parser.parse( + u" OR ".join(models_to_delete))) + except Exception as e: + if not self.silently_fail: + raise + + if models is not None: + self.log.error( + "Failed to clear Whoosh index of models '%s': %s", + ','.join(models_to_delete), + e, + exc_info=True) + else: + self.log.error( + "Failed to clear Whoosh index: %s", e, exc_info=True) + + def delete_index(self): + # 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() + + # Recreate everything. + self.setup() + + def optimize(self): + if not self.setup_complete: + self.setup() + + self.index = self.index.refresh() + self.index.optimize() + + def calculate_page(self, start_offset=0, end_offset=None): + # 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 + + 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 + + @log_query + def search( + self, + query_string, + sort_by=None, + start_offset=0, + end_offset=None, + fields='', + highlight=False, + facets=None, + date_facets=None, + query_facets=None, + narrow_queries=None, + spelling_query=None, + within=None, + dwithin=None, + distance_point=None, + models=None, + limit_to_registered_models=None, + result_class=None, + **kwargs): + if not self.setup_complete: + self.setup() + + # A zero length query should return no results. + if len(query_string) == 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, + } + + 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") + + 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] + + if facets is not None: + warnings.warn( + "Whoosh does not handle faceting.", + Warning, + stacklevel=2) + + if date_facets is not None: + warnings.warn( + "Whoosh does not handle date faceting.", + Warning, + stacklevel=2) + + if query_facets is not None: + warnings.warn( + "Whoosh does not handle query faceting.", + Warning, + stacklevel=2) + + narrowed_results = None + self.index = self.index.refresh() + + if limit_to_registered_models is None: + limit_to_registered_models = getattr( + settings, 'HAYSTACK_LIMIT_TO_REGISTERED_MODELS', True) + + if models and len(models): + model_choices = sorted(get_model_ct(model) for model in models) + elif limit_to_registered_models: + # 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: + if narrow_queries is None: + narrow_queries = set() + + narrow_queries.add(' OR '.join( + ['%s:%s' % (DJANGO_CT, rm) for rm in model_choices])) + + narrow_searcher = None + + if narrow_queries is not None: + # 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) + + if len(recent_narrowed_results) <= 0: + return { + 'results': [], + 'hits': 0, + } + + if narrowed_results: + narrowed_results.filter(recent_narrowed_results) + else: + narrowed_results = recent_narrowed_results + + self.index = self.index.refresh() + + if self.index.doc_count(): + searcher = self.index.searcher() + parsed_query = self.parser.parse(query_string) + + # In the event of an invalid/stopworded query, recover gracefully. + if parsed_query is None: + return { + 'results': [], + 'hits': 0, + } + + page_num, page_length = self.calculate_page( + start_offset, end_offset) + + search_kwargs = { + 'pagelen': page_length, + 'sortedby': sort_by, + 'reverse': reverse, + } + + # 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 + ) + 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, + highlight=highlight, + query_string=query_string, + spelling_query=spelling_query, + result_class=result_class) + searcher.close() + + if hasattr(narrow_searcher, 'close'): + narrow_searcher.close() + + return results + else: + if self.include_spelling: + if spelling_query: + spelling_suggestion = self.create_spelling_suggestion( + spelling_query) + else: + spelling_suggestion = self.create_spelling_suggestion( + query_string) + else: + spelling_suggestion = None + + return { + 'results': [], + 'hits': 0, + 'spelling_suggestion': spelling_suggestion, + } + + def more_like_this( + self, + model_instance, + additional_query_string=None, + start_offset=0, + end_offset=None, + models=None, + limit_to_registered_models=None, + result_class=None, + **kwargs): + if not self.setup_complete: + self.setup() + + # 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() + + if limit_to_registered_models is None: + limit_to_registered_models = getattr( + settings, 'HAYSTACK_LIMIT_TO_REGISTERED_MODELS', True) + + if models and len(models): + model_choices = sorted(get_model_ct(model) for model in models) + elif limit_to_registered_models: + # 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: + if narrow_queries is None: + narrow_queries = set() + + narrow_queries.add(' OR '.join( + ['%s:%s' % (DJANGO_CT, rm) for rm in model_choices])) + + if additional_query_string and additional_query_string != '*': + narrow_queries.add(additional_query_string) + + narrow_searcher = None + + if narrow_queries is not None: + # 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) + + if len(recent_narrowed_results) <= 0: + return { + 'results': [], + 'hits': 0, + } + + if narrowed_results: + narrowed_results.filter(recent_narrowed_results) + else: + narrowed_results = recent_narrowed_results + + page_num, page_length = self.calculate_page(start_offset, end_offset) + + self.index = self.index.refresh() + raw_results = EmptyResults() + + if self.index.doc_count(): + query = "%s:%s" % (ID, get_identifier(model_instance)) + searcher = self.index.searcher() + parsed_query = self.parser.parse(query) + results = searcher.search(parsed_query) + + if len(results): + 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, + '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() + indexed_models = unified_index.get_indexed_models() + + for doc_offset, raw_result in enumerate(raw_page): + score = raw_page.score(doc_offset) or 0 + app_label, model_name = raw_result[DJANGO_CT].split('.') + additional_fields = {} + model = haystack_get_model(app_label, model_name) + + if model and model in indexed_models: + for key, value in raw_result.items(): + index = unified_index.get_index(model) + string_key = str(key) + + if string_key in index.fields and hasattr( + index.fields[string_key], 'convert'): + # Special-cased due to the nature of KEYWORD fields. + if index.fields[string_key].is_multivalued: + if value is None or len(value) == 0: + additional_fields[string_key] = [] + else: + additional_fields[string_key] = value.split( + ',') + else: + additional_fields[string_key] = index.fields[string_key].convert( + value) + else: + additional_fields[string_key] = self._to_python(value) + + 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) + results.append(result) + else: + hits -= 1 + + if self.include_spelling: + if spelling_query: + spelling_suggestion = self.create_spelling_suggestion( + spelling_query) + else: + spelling_suggestion = self.create_spelling_suggestion( + query_string) + + return { + 'results': results, + 'hits': hits, + 'facets': facets, + 'spelling_suggestion': spelling_suggestion, + } + + def create_spelling_suggestion(self, query_string): + spelling_suggestion = None + reader = self.index.reader() + corrector = reader.corrector(self.content_field_name) + cleaned_query = force_str(query_string) + + if not query_string: + return spelling_suggestion + + # 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 len(suggestions) > 0: + suggested_words.append(suggestions[0]) + + spelling_suggestion = ' '.join(suggested_words) + return spelling_suggestion + + def _from_python(self, value): + """ + 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): + 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 + + def _to_python(self, value): + """ + 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['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) + + # 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 + + +class WhooshSearchQuery(BaseSearchQuery): + def _convert_datetime(self, date): + if hasattr(date, 'hour'): + return force_str(date.strftime('%Y%m%d%H%M%S')) + else: + return force_str(date.strftime('%Y%m%d000000')) + + def clean(self, query_fragment): + """ + 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.replace(word, word.lower()) + + for char in self.backend.RESERVED_CHARACTERS: + if char in word: + word = "'%s'" % word + break + + cleaned_words.append(word) + + return ' '.join(cleaned_words) + + def build_query_fragment(self, field, filter_type, value): + from haystack import connections + query_frag = '' + is_datetime = False + + if not hasattr(value, 'input_type_name'): + # 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) + + # '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', + 'contains': '*%s*', + 'endswith': "*%s", + 'startswith': "%s*", + 'exact': '%s', + 'gt': "{%s to}", + 'gte': "[%s to]", + 'lt': "{to %s}", + 'lte': "[to %s]", + 'fuzzy': u'%s~', + } + + if value.post_process is False: + query_frag = prepared_value + else: + if filter_type in [ + 'content', + 'contains', + 'startswith', + 'endswith', + 'fuzzy']: + if value.input_type_name == 'exact': + query_frag = prepared_value + else: + # 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 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]) + + if hasattr(prepared_value[0], 'strftime'): + start = self._convert_datetime(start) + + if hasattr(prepared_value[1], 'strftime'): + end = self._convert_datetime(end) + + query_frag = u"[%s to %s]" % (start, end) + elif filter_type == 'exact': + if value.input_type_name == 'exact': + query_frag = prepared_value + else: + prepared_value = Exact(prepared_value).prepare(self) + query_frag = filter_types[filter_type] % prepared_value + else: + if is_datetime is True: + prepared_value = self._convert_datetime(prepared_value) + + query_frag = filter_types[filter_type] % prepared_value + + if len(query_frag) and not isinstance(value, Raw): + if not query_frag.startswith('(') and not query_frag.endswith(')'): + query_frag = "(%s)" % query_frag + + return u"%s%s" % (index_fieldname, query_frag) + + # 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): + backend = WhooshSearchBackend + query = WhooshSearchQuery diff --git a/DjangoBlog-master/djangoblog/wsgi.py b/DjangoBlog-master/djangoblog/wsgi.py new file mode 100644 index 0000000..2295efd --- /dev/null +++ b/DjangoBlog-master/djangoblog/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for djangoblog project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/1.10/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "djangoblog.settings") + +application = get_wsgi_application() diff --git a/DjangoBlog-master/docs/README-en.md b/DjangoBlog-master/docs/README-en.md new file mode 100644 index 0000000..37ea069 --- /dev/null +++ b/DjangoBlog-master/docs/README-en.md @@ -0,0 +1,158 @@ +# DjangoBlog + +

+ Django CI + CodeQL + codecov + license +

+ +

+ A powerful, elegant, and modern blog system. +
+ English简体中文 +

+ +--- + +DjangoBlog is a high-performance blog platform built with Python 3.10 and Django 4.0. It not only provides all the core functionalities of a traditional blog but also features a flexible plugin system, allowing you to easily extend and customize your website. Whether you are a personal blogger, a tech enthusiast, or a content creator, DjangoBlog aims to provide a stable, efficient, and easy-to-maintain environment for writing and publishing. + +## ✨ Features + +- **Powerful Content Management**: Full support for managing articles, standalone pages, categories, and tags. Comes with a powerful built-in Markdown editor with syntax highlighting. +- **Full-Text Search**: Integrated search engine for fast and accurate content searching. +- **Interactive Comment System**: Supports replies, email notifications, and Markdown formatting in comments. +- **Flexible Sidebar**: Customizable modules for displaying recent articles, most viewed posts, tag cloud, and more. +- **Social Login**: Built-in OAuth support, with integrations for Google, GitHub, Facebook, Weibo, QQ, and other major platforms. +- **High-Performance Caching**: Native support for Redis caching with an automatic refresh mechanism to ensure high-speed website responses. +- **SEO Friendly**: Basic SEO features are included, with automatic notifications to Google and Baidu upon new content publication. +- **Extensible Plugin System**: Extend blog functionalities by creating standalone plugins, ensuring decoupled and maintainable code. We have already implemented features like view counting and SEO optimization through plugins! +- **Integrated Image Hosting**: A simple, built-in image hosting feature for easy uploads and management. +- **Automated Frontend**: Integrated with `django-compressor` to automatically compress and optimize CSS and JavaScript files. +- **Robust Operations**: Built-in email notifications for website exceptions and management capabilities through a WeChat Official Account. + +## 🛠️ Tech Stack + +- **Backend**: Python 3.10, Django 4.0 +- **Database**: MySQL, SQLite (configurable) +- **Cache**: Redis +- **Frontend**: HTML5, CSS3, JavaScript +- **Search**: Whoosh, Elasticsearch (configurable) +- **Editor**: Markdown (mdeditor) + +## 🚀 Getting Started + +### 1. Prerequisites + +Ensure you have Python 3.10+ and MySQL/MariaDB installed on your system. + +### 2. Clone & Installation + +```bash +# Clone the project to your local machine +git clone https://github.com/liangliangyy/DjangoBlog.git +cd DjangoBlog + +# Install dependencies +pip install -r requirements.txt +``` + +### 3. Project Configuration + +- **Database**: + Open `djangoblog/settings.py`, locate the `DATABASES` section, and update it with your MySQL connection details. + + ```python + DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.mysql', + 'NAME': 'djangoblog', + 'USER': 'root', + 'PASSWORD': 'your_password', + 'HOST': '127.0.0.1', + 'PORT': 3306, + } + } + ``` + Create the database in MySQL: + ```sql + CREATE DATABASE `djangoblog` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + ``` + +- **More Configurations**: + For advanced settings such as email, OAuth, caching, and more, please refer to our [Detailed Configuration Guide](/docs/config-en.md). + +### 4. Database Initialization + +```bash +python manage.py makemigrations +python manage.py migrate + +# Create a superuser account +python manage.py createsuperuser +``` + +### 5. Running the Project + +```bash +# (Optional) Generate some test data +python manage.py create_testdata + +# (Optional) Collect and compress static files +python manage.py collectstatic --noinput +python manage.py compress --force + +# Start the development server +python manage.py runserver +``` + +Now, open your browser and navigate to `http://127.0.0.1:8000/`. You should see the DjangoBlog homepage! + +## Deployment + +- **Traditional Deployment**: A detailed guide for server deployment is available here: [Deployment Tutorial](https://www.lylinux.net/article/2019/8/5/58.html) (in Chinese). +- **Docker Deployment**: This project fully supports Docker. If you are familiar with containerization, please refer to the [Docker Deployment Guide](/docs/docker-en.md) for a quick start. +- **Kubernetes Deployment**: We also provide a complete [Kubernetes Deployment Guide](/docs/k8s-en.md) to help you go cloud-native easily. + +## 🧩 Plugin System + +The plugin system is a core feature of DjangoBlog. It allows you to add new functionalities to your blog without modifying the core codebase by writing standalone plugins. + +- **How it Works**: Plugins operate by registering callback functions to predefined "hooks". For instance, when an article is rendered, the `after_article_body_get` hook is triggered, and all functions registered to this hook are executed. +- **Existing Plugins**: Features like `view_count` and `seo_optimizer` are implemented through this plugin system. +- **Develop Your Own Plugin**: Simply create a new folder under the `plugins` directory and write your `plugin.py`. We welcome you to explore and contribute your creative ideas to the DjangoBlog community! + +## 🤝 Contributing + +We warmly welcome contributions of any kind! If you have great ideas or have found a bug, please feel free to open an issue or submit a pull request. + +## 📄 License + +This project is open-sourced under the [MIT License](LICENSE). + +--- + +## ❤️ Support & Sponsorship + +If you find this project helpful and wish to support its continued maintenance and development, please consider buying me a coffee! Your support is my greatest motivation. + +

+ Alipay Sponsorship + WeChat Sponsorship +

+

+ (Left) Alipay / (Right) WeChat +

+ +## 🙏 Acknowledgements + +A special thanks to **JetBrains** for providing a free open-source license for this project. + +

+ + JetBrains Logo + +

+ +--- +> If this project has helped you, please leave your website URL [here](https://github.com/liangliangyy/DjangoBlog/issues/214) to let more people see it. Your feedback is the driving force for my continued updates and maintenance. diff --git a/DjangoBlog-master/docs/config-en.md b/DjangoBlog-master/docs/config-en.md new file mode 100644 index 0000000..b877efb --- /dev/null +++ b/DjangoBlog-master/docs/config-en.md @@ -0,0 +1,64 @@ +# Introduction to main features settings + +## Cache: +Cache using `memcache` for default. If you don't have `memcache` environment, you can remove the `default` setting in `CACHES` and change `locmemcache` to `default`. +```python +CACHES = { + 'default': { + 'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache', + 'LOCATION': '127.0.0.1:11211', + 'KEY_PREFIX': 'django_test' if TESTING else 'djangoblog', + 'TIMEOUT': 60 * 60 * 10 + }, + 'locmemcache': { + 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', + 'TIMEOUT': 10800, + 'LOCATION': 'unique-snowflake', + } +} +``` + +## OAuth Login: +QQ, Weibo, Google, GitHub and Facebook are now supported for OAuth login. Fetch OAuth login permissions from the corresponding open platform, and save them with `appkey`, `appsecret` and callback address in **Backend->OAuth** configuration. + +### Callback address examples: +QQ: http://your-domain-name/oauth/authorize?type=qq +Weibo: http://your-domain-name/oauth/authorize?type=weibo +type is in the type field of `oauthmanager`. + +## owntracks: +owntracks is a location tracking application. It will send your locaiton to the server by timing.Simple support owntracks features. Just install owntracks app and set api address as `your-domain-name/owntracks/logtracks`. Visit `your-domain-name/owntracks/show_dates` and you will see the date with latitude and langitude, click it and see the motion track. The map is drawn by AMap. + +## Email feature: +Same as before, Configure your own error msg recvie email information with`ADMINS = [('liangliang', 'liangliangyy@gmail.com')]` in `settings.py`. And modify: +```python +EMAIL_HOST = 'smtp.zoho.com' +EMAIL_PORT = 587 +EMAIL_HOST_USER = os.environ.get('DJANGO_EMAIL_USER') +EMAIL_HOST_PASSWORD = os.environ.get('DJANGO_EMAIL_PASSWORD') +DEFAULT_FROM_EMAIL = EMAIL_HOST_USER +SERVER_EMAIL = os.environ.get('DJANGO_EMAIL_USER') +``` +with your email account information. + +## WeChat Official Account +Simple wechat official account features integrated. Set token as `your-domain-name/robot` in wechat backend. Default token is `lylinux`, you can change it to your own in `servermanager/robot.py`. Add a new command in `Backend->Servermanager->command`, in this way, you can manage the system through wechat official account. + +## Introduction to website configuration +You can add website configuration in **Backend->BLOG->WebSiteConfiguration**. Such as: keywords, description, Google Ad, website stats code, case number, etc. +OAuth user avatar path is saved in *StaticFileSavedAddress*. Please input absolute path, code directory for default. + +## Source code highlighting +If the code block in your article didn't show hightlight, please write the code blocks as following: + +![](https://resource.lylinux.net/image/codelang.png) + +That is, you should add the corresponding language name before the code block. + +## Update +If you get errors as following while executing database migrations: +```python +django.db.migrations.exceptions.MigrationSchemaMissing: Unable to create the django_migrations table ((1064, "You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '(6) NOT NULL)' at line 1")) +``` +This problem may cause by the mysql version under 5.6, a new version( >= 5.6 ) mysql is needed. + diff --git a/DjangoBlog-master/docs/config.md b/DjangoBlog-master/docs/config.md new file mode 100644 index 0000000..24673a3 --- /dev/null +++ b/DjangoBlog-master/docs/config.md @@ -0,0 +1,58 @@ +# 主要功能配置介绍: + +## 缓存: +缓存默认使用`localmem`缓存,如果你有`redis`环境,可以设置`DJANGO_REDIS_URL`环境变量,则会自动使用该redis来作为缓存,或者你也可以直接修改如下代码来使用。 +https://github.com/liangliangyy/DjangoBlog/blob/ffcb2c3711de805f2067dd3c1c57449cd24d84ee/djangoblog/settings.py#L185-L199 + + +## oauth登录: + +现在已经支持QQ,微博,Google,GitHub,Facebook登录,需要在其对应的开放平台申请oauth登录权限,然后在 +**后台->Oauth** 配置中新增配置,填写对应的`appkey`和`appsecret`以及回调地址。 +### 回调地址示例: +qq:http://你的域名/oauth/authorize?type=qq +微博:http://你的域名/oauth/authorize?type=weibo +type对应在`oauthmanager`中的type字段。 + +## owntracks: +owntracks是一个位置追踪软件,可以定时的将你的坐标提交到你的服务器上,现在简单的支持owntracks功能,需要安装owntracks的app,然后将api地址设置为: +`你的域名/owntracks/logtracks`就可以了。然后访问`你的域名/owntracks/show_dates`就可以看到有经纬度记录的日期,点击之后就可以看到运动轨迹了。地图是使用高德地图绘制。 + +## 邮件功能: +同样,将`settings.py`中的`ADMINS = [('liangliang', 'liangliangyy@gmail.com')]`配置为你自己的错误接收邮箱,另外修改: +```python +EMAIL_HOST = 'smtp.zoho.com' +EMAIL_PORT = 587 +EMAIL_HOST_USER = os.environ.get('DJANGO_EMAIL_USER') +EMAIL_HOST_PASSWORD = os.environ.get('DJANGO_EMAIL_PASSWORD') +DEFAULT_FROM_EMAIL = EMAIL_HOST_USER +SERVER_EMAIL = os.environ.get('DJANGO_EMAIL_USER') +``` +为你自己的邮箱配置。 + +## 微信公众号 +集成了简单的微信公众号功能,在微信后台将token地址设置为:`你的域名/robot` 即可,默认token为`lylinux`,当然你可以修改为你自己的,在`servermanager/robot.py`中。 +然后在**后台->Servermanager->命令**中新增命令,这样就可以使用微信公众号来管理了。 +## 网站配置介绍 +在**后台->BLOG->网站配置**中,可以新增网站配置,比如关键字,描述等,以及谷歌广告,网站统计代码及备案号等等。 +其中的*静态文件保存地址*是保存oauth用户登录的头像路径,填写绝对路径,默认是代码目录。 +## 代码高亮 +如果你发现你文章的代码没有高亮,请这样书写代码块: + +![](https://resource.lylinux.net/image/codelang.png) + + +也就是说,需要在代码块开始位置加入这段代码对应的语言。 + +## update +如果你发现执行数据库迁移的时候出现如下报错: +```python +django.db.migrations.exceptions.MigrationSchemaMissing: Unable to create the django_migrations table ((1064, "You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '(6) NOT NULL)' at line 1")) +``` +可能是因为你的mysql版本低于5.6,需要升级mysql版本>=5.6即可。 + + +django 4.0登录可能会报错CSRF,需要配置下`settings.py`中的`CSRF_TRUSTED_ORIGINS` + +https://github.com/liangliangyy/DjangoBlog/blob/master/djangoblog/settings.py#L39 + diff --git a/DjangoBlog-master/docs/docker-en.md b/DjangoBlog-master/docs/docker-en.md new file mode 100644 index 0000000..8d5d59e --- /dev/null +++ b/DjangoBlog-master/docs/docker-en.md @@ -0,0 +1,114 @@ +# Deploying DjangoBlog with Docker + +![Docker Pulls](https://img.shields.io/docker/pulls/liangliangyy/djangoblog) +![Docker Image Version (latest by date)](https://img.shields.io/docker/v/liangliangyy/djangoblog?sort=date) +![Docker Image Size (latest by date)](https://img.shields.io/docker/image-size/liangliangyy/djangoblog) + +This project fully supports containerized deployment using Docker, providing you with a fast, consistent, and isolated runtime environment. We recommend using `docker-compose` to launch the entire blog service stack with a single command. + +## 1. Prerequisites + +Before you begin, please ensure you have the following software installed on your system: +- [Docker Engine](https://docs.docker.com/engine/install/) +- [Docker Compose](https://docs.docker.com/compose/install/) (Included with Docker Desktop for Mac and Windows) + +## 2. Recommended Method: Using `docker-compose` (One-Click Deployment) + +This is the simplest and most recommended way to deploy. It automatically creates and manages the Django application, a MySQL database, and an optional Elasticsearch service for you. + +### Step 1: Start the Basic Services + +From the project's root directory, run the following command: + +```bash +# Build and start the containers in detached mode (includes Django app and MySQL) +docker-compose up -d --build +``` + +`docker-compose` will read the `docker-compose.yml` file, pull the necessary images, build the project image, and start all services. + +- **Access Your Blog**: Once the services are up, you can access the blog by navigating to `http://127.0.0.1` in your browser. +- **Data Persistence**: MySQL data files will be stored in the `data/mysql` directory within the project root, ensuring that your data persists across container restarts. + +### Step 2: (Optional) Enable Elasticsearch for Full-Text Search + +If you want to use Elasticsearch for more powerful full-text search capabilities, you can include the `docker-compose.es.yml` configuration file: + +```bash +# Build and start all services in detached mode (Django, MySQL, Elasticsearch) +docker-compose -f docker-compose.yml -f deploy/docker-compose/docker-compose.es.yml up -d --build +``` +- **Data Persistence**: Elasticsearch data will be stored in the `data/elasticsearch` directory. + +### Step 3: First-Time Initialization + +After the containers start for the first time, you'll need to execute some initialization commands inside the application container. + +```bash +# Get a shell inside the djangoblog application container (named 'web') +docker-compose exec web bash + +# Inside the container, run the following commands: +# Create a superuser account (follow the prompts to set username, email, and password) +python manage.py createsuperuser + +# (Optional) Create some test data +python manage.py create_testdata + +# (Optional, if ES is enabled) Create the search index +python manage.py rebuild_index + +# Exit the container +exit +``` + +## 3. Alternative Method: Using the Standalone Docker Image + +If you already have an external MySQL database running, you can run the DjangoBlog application image by itself. + +```bash +# Pull the latest image from Docker Hub +docker pull liangliangyy/djangoblog:latest + +# Run the container and connect it to your external database +docker run -d \ + -p 8000:8000 \ + -e DJANGO_SECRET_KEY='your-strong-secret-key' \ + -e DJANGO_MYSQL_HOST='your-mysql-host' \ + -e DJANGO_MYSQL_USER='your-mysql-user' \ + -e DJANGO_MYSQL_PASSWORD='your-mysql-password' \ + -e DJANGO_MYSQL_DATABASE='djangoblog' \ + --name djangoblog \ + liangliangyy/djangoblog:latest +``` + +- **Access Your Blog**: After startup, visit `http://127.0.0.1:8000`. +- **Create Superuser**: `docker exec -it djangoblog python manage.py createsuperuser` + +## 4. Configuration (Environment Variables) + +Most of the project's configuration is managed through environment variables. You can modify them in the `docker-compose.yml` file or pass them using the `-e` flag with the `docker run` command. + +| Environment Variable | Default/Example Value | Notes | +|---------------------------|--------------------------------------------------------------------------|---------------------------------------------------------------------| +| `DJANGO_SECRET_KEY` | `your-strong-secret-key` | **Must be changed to a random, complex string!** | +| `DJANGO_DEBUG` | `False` | Toggles Django's debug mode. | +| `DJANGO_MYSQL_HOST` | `mysql` | Database hostname. | +| `DJANGO_MYSQL_PORT` | `3306` | Database port. | +| `DJANGO_MYSQL_DATABASE` | `djangoblog` | Database name. | +| `DJANGO_MYSQL_USER` | `root` | Database username. | +| `DJANGO_MYSQL_PASSWORD` | `djangoblog_123` | Database password. | +| `DJANGO_REDIS_URL` | `redis:6379/0` | Redis connection URL (for caching). | +| `DJANGO_ELASTICSEARCH_HOST`| `elasticsearch:9200` | Elasticsearch host address. | +| `DJANGO_EMAIL_HOST` | `smtp.example.org` | Email server address. | +| `DJANGO_EMAIL_PORT` | `465` | Email server port. | +| `DJANGO_EMAIL_USER` | `user@example.org` | Email account username. | +| `DJANGO_EMAIL_PASSWORD` | `your-email-password` | Email account password. | +| `DJANGO_EMAIL_USE_SSL` | `True` | Whether to use SSL. | +| `DJANGO_EMAIL_USE_TLS` | `False` | Whether to use TLS. | +| `DJANGO_ADMIN_EMAIL` | `admin@example.org` | Admin email for receiving error reports. | +| `DJANGO_BAIDU_NOTIFY_URL` | `http://data.zz.baidu.com/...` | Push API from [Baidu Webmaster Tools](https://ziyuan.baidu.com/linksubmit/index). | + +--- + +After deployment, please review and adjust these environment variables according to your needs, especially `DJANGO_SECRET_KEY` and the database and email settings. \ No newline at end of file diff --git a/DjangoBlog-master/docs/docker.md b/DjangoBlog-master/docs/docker.md new file mode 100644 index 0000000..e7c255a --- /dev/null +++ b/DjangoBlog-master/docs/docker.md @@ -0,0 +1,114 @@ +# 使用 Docker 部署 DjangoBlog + +![Docker Pulls](https://img.shields.io/docker/pulls/liangliangyy/djangoblog) +![Docker Image Version (latest by date)](https://img.shields.io/docker/v/liangliangyy/djangoblog?sort=date) +![Docker Image Size (latest by date)](https://img.shields.io/docker/image-size/liangliangyy/djangoblog) + +本项目全面支持使用 Docker 进行容器化部署,为您提供了快速、一致且隔离的运行环境。我们推荐使用 `docker-compose` 来一键启动整个博客服务栈。 + +## 1. 环境准备 + +在开始之前,请确保您的系统中已经安装了以下软件: +- [Docker Engine](https://docs.docker.com/engine/install/) +- [Docker Compose](https://docs.docker.com/compose/install/) (对于 Docker Desktop 用户,它已内置) + +## 2. 推荐方式:使用 `docker-compose` (一键部署) + +这是最简单、最推荐的部署方式。它会自动为您创建并管理 Django 应用、MySQL 数据库,以及可选的 Elasticsearch 服务。 + +### 步骤 1: 启动基础服务 + +在项目根目录下,执行以下命令: + +```bash +# 构建并以后台模式启动容器 (包含 Django 应用和 MySQL) +docker-compose up -d --build +``` + +`docker-compose` 会读取 `docker-compose.yml` 文件,自动拉取所需镜像、构建项目镜像,并启动所有服务。 + +- **访问您的博客**: 服务启动后,在浏览器中访问 `http://127.0.0.1` 即可看到博客首页。 +- **数据持久化**: MySQL 的数据文件将存储在项目根目录下的 `data/mysql` 文件夹中,确保数据在容器重启后不丢失。 + +### 步骤 2: (可选) 启用 Elasticsearch 全文搜索 + +如果您希望使用 Elasticsearch 提供更强大的全文搜索功能,可以额外加载 `docker-compose.es.yml` 配置文件: + +```bash +# 构建并以后台模式启动所有服务 (Django, MySQL, Elasticsearch) +docker-compose -f docker-compose.yml -f deploy/docker-compose/docker-compose.es.yml up -d --build +``` +- **数据持久化**: Elasticsearch 的数据将存储在 `data/elasticsearch` 文件夹中。 + +### 步骤 3: 首次运行的初始化操作 + +当容器首次启动后,您需要进入容器来执行一些初始化命令。 + +```bash +# 进入 djangoblog 应用容器 +docker-compose exec web bash + +# 在容器内执行以下命令: +# 创建超级管理员账户 (请按照提示设置用户名、邮箱和密码) +python manage.py createsuperuser + +# (可选) 创建一些测试数据 +python manage.py create_testdata + +# (可选,如果启用了 ES) 创建索引 +python manage.py rebuild_index + +# 退出容器 +exit +``` + +## 3. 备选方式:使用独立的 Docker 镜像 + +如果您已经拥有一个正在运行的外部 MySQL 数据库,您也可以只运行 DjangoBlog 的应用镜像。 + +```bash +# 从 Docker Hub 拉取最新镜像 +docker pull liangliangyy/djangoblog:latest + +# 运行容器,并链接到您的外部数据库 +docker run -d \ + -p 8000:8000 \ + -e DJANGO_SECRET_KEY='your-strong-secret-key' \ + -e DJANGO_MYSQL_HOST='your-mysql-host' \ + -e DJANGO_MYSQL_USER='your-mysql-user' \ + -e DJANGO_MYSQL_PASSWORD='your-mysql-password' \ + -e DJANGO_MYSQL_DATABASE='djangoblog' \ + --name djangoblog \ + liangliangyy/djangoblog:latest +``` + +- **访问您的博客**: 启动完成后,访问 `http://127.0.0.1:8000`。 +- **创建管理员**: `docker exec -it djangoblog python manage.py createsuperuser` + +## 4. 配置说明 (环境变量) + +本项目的大部分配置都通过环境变量来管理。您可以在 `docker-compose.yml` 文件中修改它们,或者在使用 `docker run` 命令时通过 `-e` 参数传入。 + +| 环境变量名称 | 默认值/示例 | 备注 | +|-------------------------|--------------------------------------------------------------------------|---------------------------------------------------------------------| +| `DJANGO_SECRET_KEY` | `your-strong-secret-key` | **请务必修改为一个随机且复杂的字符串!** | +| `DJANGO_DEBUG` | `False` | 是否开启 Django 的调试模式 | +| `DJANGO_MYSQL_HOST` | `mysql` | 数据库主机名 | +| `DJANGO_MYSQL_PORT` | `3306` | 数据库端口 | +| `DJANGO_MYSQL_DATABASE` | `djangoblog` | 数据库名称 | +| `DJANGO_MYSQL_USER` | `root` | 数据库用户名 | +| `DJANGO_MYSQL_PASSWORD` | `djangoblog_123` | 数据库密码 | +| `DJANGO_REDIS_URL` | `redis:6379/0` | Redis 连接地址 (用于缓存) | +| `DJANGO_ELASTICSEARCH_HOST` | `elasticsearch:9200` | Elasticsearch 主机地址 | +| `DJANGO_EMAIL_HOST` | `smtp.example.org` | 邮件服务器地址 | +| `DJANGO_EMAIL_PORT` | `465` | 邮件服务器端口 | +| `DJANGO_EMAIL_USER` | `user@example.org` | 邮件账户 | +| `DJANGO_EMAIL_PASSWORD` | `your-email-password` | 邮件密码 | +| `DJANGO_EMAIL_USE_SSL` | `True` | 是否使用 SSL | +| `DJANGO_EMAIL_USE_TLS` | `False` | 是否使用 TLS | +| `DJANGO_ADMIN_EMAIL` | `admin@example.org` | 接收异常报告的管理员邮箱 | +| `DJANGO_BAIDU_NOTIFY_URL` | `http://data.zz.baidu.com/...` | [百度站长平台](https://ziyuan.baidu.com/linksubmit/index) 的推送接口 | + +--- + +部署完成后,请务必检查并根据您的实际需求调整这些环境变量,特别是 `DJANGO_SECRET_KEY` 和数据库、邮件相关的配置。 diff --git a/DjangoBlog-master/docs/es.md b/DjangoBlog-master/docs/es.md new file mode 100644 index 0000000..97226c5 --- /dev/null +++ b/DjangoBlog-master/docs/es.md @@ -0,0 +1,28 @@ +# 集成Elasticsearch +如果你已经有了`Elasticsearch`环境,那么可以将搜索从`Whoosh`换成`Elasticsearch`,集成方式也很简单, +首先需要注意如下几点: +1. 你的`Elasticsearch`支持`ik`中文分词 +2. 你的`Elasticsearch`版本>=7.3.0 + +接下来在`settings.py`做如下改动即可: +- 增加es链接,如下所示: +```python +ELASTICSEARCH_DSL = { + 'default': { + 'hosts': '127.0.0.1:9200' + }, +} +``` +- 修改`HAYSTACK`配置: +```python +HAYSTACK_CONNECTIONS = { + 'default': { + 'ENGINE': 'djangoblog.elasticsearch_backend.ElasticSearchEngine', + }, +} +``` +然后终端执行: +```shell script +./manage.py build_index +``` +这将会在你的es中创建两个索引,分别是`blog`和`performance`,其中`blog`索引就是搜索所使用的,而`performance`会记录每个请求的响应时间,以供将来优化使用。 \ No newline at end of file diff --git a/DjangoBlog-master/docs/imgs/alipay.jpg b/DjangoBlog-master/docs/imgs/alipay.jpg new file mode 100644 index 0000000..424d70a Binary files /dev/null and b/DjangoBlog-master/docs/imgs/alipay.jpg differ diff --git a/DjangoBlog-master/docs/imgs/pycharm_logo.png b/DjangoBlog-master/docs/imgs/pycharm_logo.png new file mode 100644 index 0000000..7f2a4b0 Binary files /dev/null and b/DjangoBlog-master/docs/imgs/pycharm_logo.png differ diff --git a/DjangoBlog-master/docs/imgs/wechat.jpg b/DjangoBlog-master/docs/imgs/wechat.jpg new file mode 100644 index 0000000..7edf525 Binary files /dev/null and b/DjangoBlog-master/docs/imgs/wechat.jpg differ diff --git a/DjangoBlog-master/docs/k8s-en.md b/DjangoBlog-master/docs/k8s-en.md new file mode 100644 index 0000000..20e9527 --- /dev/null +++ b/DjangoBlog-master/docs/k8s-en.md @@ -0,0 +1,141 @@ +# Deploying DjangoBlog with Kubernetes + +This document guides you through deploying the DjangoBlog application on a Kubernetes (K8s) cluster. We provide a complete set of `.yaml` configuration files in the `deploy/k8s` directory to deploy a full service stack, including the DjangoBlog application, Nginx, MySQL, Redis, and Elasticsearch. + +## Architecture Overview + +This deployment utilizes a microservices-based, cloud-native architecture: + +- **Core Components**: Each core service (DjangoBlog, Nginx, MySQL, Redis, Elasticsearch) runs as a separate `Deployment`. +- **Configuration Management**: Nginx configurations and Django application environment variables are managed via `ConfigMap`. **Note: For sensitive information like passwords, using `Secret` is highly recommended.** +- **Service Discovery**: All services are exposed internally within the cluster as `ClusterIP` type `Service`, enabling communication via service names. +- **External Access**: An `Ingress` resource is used to route external HTTP traffic to the Nginx service, which acts as the single entry point for the entire blog application. +- **Data Persistence**: A `local-storage` solution based on node-local paths is used. This requires you to manually create storage directories on a specific K8s node and statically bind them using `PersistentVolume` (PV) and `PersistentVolumeClaim` (PVC). + +## 1. Prerequisites + +Before you begin, please ensure you have the following: + +- A running Kubernetes cluster. +- The `kubectl` command-line tool configured to connect to your cluster. +- An [Nginx Ingress Controller](https://kubernetes.github.io/ingress-nginx/deploy/) installed and configured in your cluster. +- Filesystem access to one of the nodes in your cluster (defaulted to `master` in the configs) to create local storage directories. + +## 2. Deployment Steps + +### Step 1: Create a Namespace + +We recommend deploying all DjangoBlog-related resources in a dedicated namespace for better management. + +```bash +# Create a namespace named 'djangoblog' +kubectl create namespace djangoblog +``` + +### Step 2: Configure Persistent Storage + +This setup uses Local Persistent Volumes. You need to create the data storage directories on a node within your cluster (the default is the `master` node in `pv.yaml`). + +```bash +# Log in to your master node +ssh user@master-node + +# Create the required storage directories +sudo mkdir -p /mnt/local-storage-db +sudo mkdir -p /mnt/local-storage-djangoblog +sudo mkdir -p /mnt/resource/ +sudo mkdir -p /mnt/local-storage-elasticsearch + +# Log out from the node +exit +``` +**Note**: If you wish to store data on a different node or use different paths, you must modify the `nodeAffinity` and `local.path` settings in the `deploy/k8s/pv.yaml` file. + +After creating the directories, apply the storage-related configurations: + +```bash +# Apply the StorageClass +kubectl apply -f deploy/k8s/storageclass.yaml + +# Apply the PersistentVolumes (PVs) +kubectl apply -f deploy/k8s/pv.yaml + +# Apply the PersistentVolumeClaims (PVCs) +kubectl apply -f deploy/k8s/pvc.yaml +``` + +### Step 3: Configure the Application + +Before deploying the application, you need to edit the `deploy/k8s/configmap.yaml` file to modify sensitive information and custom settings. + +**It is strongly recommended to change the following fields:** +- `DJANGO_SECRET_KEY`: Change to a random, complex string. +- `DJANGO_MYSQL_PASSWORD` and `MYSQL_ROOT_PASSWORD`: Change to your own secure database password. + +```bash +# Edit the ConfigMap file +vim deploy/k8s/configmap.yaml + +# Apply the configuration +kubectl apply -f deploy/k8s/configmap.yaml +``` + +### Step 4: Deploy the Application Stack + +Now, we can deploy all the core services. + +```bash +# Deploy the Deployments (DjangoBlog, MySQL, Redis, Nginx, ES) +kubectl apply -f deploy/k8s/deployment.yaml + +# Deploy the Services (to create internal endpoints for the Deployments) +kubectl apply -f deploy/k8s/service.yaml +``` + +The deployment may take some time. You can run the following command to check if all Pods are running successfully (STATUS should be `Running`): + +```bash +kubectl get pods -n djangoblog -w +``` + +### Step 5: Expose the Application Externally + +Finally, expose the Nginx service to external traffic by applying the `Ingress` rule. + +```bash +# Apply the Ingress rule +kubectl apply -f deploy/k8s/gateway.yaml +``` + +Once deployed, you can access your blog via the external IP address of your Ingress Controller. Use the following command to find the address: + +```bash +kubectl get ingress -n djangoblog +``` + +### Step 6: First-Time Initialization + +Similar to the Docker deployment, you need to get a shell into the DjangoBlog application Pod to perform database initialization and create a superuser on the first run. + +```bash +# First, get the name of a djangoblog pod +kubectl get pods -n djangoblog | grep djangoblog + +# Exec into one of the Pods (replace [pod-name] with the name from the previous step) +kubectl exec -it [pod-name] -n djangoblog -- bash + +# Inside the Pod, run the following commands: +# Create a superuser account (follow the prompts) +python manage.py createsuperuser + +# (Optional) Create some test data +python manage.py create_testdata + +# (Optional, if ES is enabled) Create the search index +python manage.py rebuild_index + +# Exit the Pod +exit +``` + +Congratulations! You have successfully deployed DjangoBlog on your Kubernetes cluster. \ No newline at end of file diff --git a/DjangoBlog-master/docs/k8s.md b/DjangoBlog-master/docs/k8s.md new file mode 100644 index 0000000..9da3c28 --- /dev/null +++ b/DjangoBlog-master/docs/k8s.md @@ -0,0 +1,141 @@ +# 使用 Kubernetes 部署 DjangoBlog + +本文档将指导您如何在 Kubernetes (K8s) 集群上部署 DjangoBlog 应用。我们提供了一套完整的 `.yaml` 配置文件,位于 `deploy/k8s` 目录下,用于部署一个包含 DjangoBlog 应用、Nginx、MySQL、Redis 和 Elasticsearch 的完整服务栈。 + +## 架构概览 + +本次部署采用的是微服务化的云原生架构: + +- **核心组件**: 每个核心服务 (DjangoBlog, Nginx, MySQL, Redis, Elasticsearch) 都将作为独立的 `Deployment` 运行。 +- **配置管理**: Nginx 的配置文件和 Django 应用的环境变量通过 `ConfigMap` 进行管理。**注意:敏感信息(如密码)建议使用 `Secret` 进行管理。** +- **服务发现**: 所有服务都通过 `ClusterIP` 类型的 `Service` 在集群内部暴露,并通过服务名相互通信。 +- **外部访问**: 使用 `Ingress` 资源将外部的 HTTP 流量路由到 Nginx 服务,作为整个博客应用的统一入口。 +- **数据持久化**: 采用基于节点本地路径的 `local-storage` 方案。这需要您在指定的 K8s 节点上手动创建存储目录,并通过 `PersistentVolume` (PV) 和 `PersistentVolumeClaim` (PVC) 进行静态绑定。 + +## 1. 环境准备 + +在开始之前,请确保您已具备以下环境: + +- 一个正在运行的 Kubernetes 集群。 +- `kubectl` 命令行工具已配置并能够连接到您的集群。 +- 集群中已安装并配置好 [Nginx Ingress Controller](https://kubernetes.github.io/ingress-nginx/deploy/)。 +- 对集群中的一个节点(默认为 `master`)拥有文件系统访问权限,用于创建本地存储目录。 + +## 2. 部署步骤 + +### 步骤 1: 创建命名空间 + +我们建议将 DjangoBlog 相关的所有资源都部署在一个独立的命名空间中,便于管理。 + +```bash +# 创建一个名为 djangoblog 的命名空间 +kubectl create namespace djangoblog +``` + +### 步骤 2: 配置持久化存储 + +此方案使用本地持久卷 (Local Persistent Volume)。您需要在集群的一个节点上(在 `pv.yaml` 文件中默认为 `master` 节点)创建用于数据存储的目录。 + +```bash +# 登录到您的 master 节点 +ssh user@master-node + +# 创建所需的存储目录 +sudo mkdir -p /mnt/local-storage-db +sudo mkdir -p /mnt/local-storage-djangoblog +sudo mkdir -p /mnt/resource/ +sudo mkdir -p /mnt/local-storage-elasticsearch + +# 退出节点 +exit +``` +**注意**: 如果您希望将数据存储在其他节点或使用不同的路径,请务必修改 `deploy/k8s/pv.yaml` 文件中 `nodeAffinity` 和 `local.path` 的配置。 + +创建目录后,应用存储相关的配置文件: + +```bash +# 应用 StorageClass +kubectl apply -f deploy/k8s/storageclass.yaml + +# 应用 PersistentVolume (PV) +kubectl apply -f deploy/k8s/pv.yaml + +# 应用 PersistentVolumeClaim (PVC) +kubectl apply -f deploy/k8s/pvc.yaml +``` + +### 步骤 3: 配置应用 + +在部署应用之前,您需要编辑 `deploy/k8s/configmap.yaml` 文件,修改其中的敏感信息和个性化配置。 + +**强烈建议修改以下字段:** +- `DJANGO_SECRET_KEY`: 修改为一个随机且复杂的字符串。 +- `DJANGO_MYSQL_PASSWORD` 和 `MYSQL_ROOT_PASSWORD`: 修改为您自己的数据库密码。 + +```bash +# 编辑 ConfigMap 文件 +vim deploy/k8s/configmap.yaml + +# 应用配置 +kubectl apply -f deploy/k8s/configmap.yaml +``` + +### 步骤 4: 部署应用服务栈 + +现在,我们可以部署所有的核心服务了。 + +```bash +# 部署 Deployments (DjangoBlog, MySQL, Redis, Nginx, ES) +kubectl apply -f deploy/k8s/deployment.yaml + +# 部署 Services (为 Deployments 创建内部访问端点) +kubectl apply -f deploy/k8s/service.yaml +``` + +部署需要一些时间,您可以运行以下命令检查所有 Pod 是否都已成功运行 (STATUS 为 `Running`): + +```bash +kubectl get pods -n djangoblog -w +``` + +### 步骤 5: 暴露应用到外部 + +最后,通过应用 `Ingress` 规则来将外部流量引导至我们的 Nginx 服务。 + +```bash +# 应用 Ingress 规则 +kubectl apply -f deploy/k8s/gateway.yaml +``` + +部署完成后,您可以通过 Ingress Controller 的外部 IP 地址来访问您的博客。执行以下命令获取地址: + +```bash +kubectl get ingress -n djangoblog +``` + +### 步骤 6: 首次运行的初始化操作 + +与 Docker 部署类似,首次运行时,您需要进入 DjangoBlog 应用的 Pod 来执行数据库初始化和创建管理员账户。 + +```bash +# 首先,获取 djangoblog pod 的名称 +kubectl get pods -n djangoblog | grep djangoblog + +# 进入其中一个 Pod (将 [pod-name] 替换为上一步获取到的名称) +kubectl exec -it [pod-name] -n djangoblog -- bash + +# 在 Pod 内部执行以下命令: +# 创建超级管理员账户 (请按照提示操作) +python manage.py createsuperuser + +# (可选) 创建测试数据 +python manage.py create_testdata + +# (可选,如果启用了 ES) 创建索引 +python manage.py rebuild_index + +# 退出 Pod +exit +``` + +至此,您已成功在 Kubernetes 集群上完成了 DjangoBlog 的部署! \ No newline at end of file diff --git a/DjangoBlog-master/get-pip.py b/DjangoBlog-master/get-pip.py new file mode 100644 index 0000000..b86d2d7 --- /dev/null +++ b/DjangoBlog-master/get-pip.py @@ -0,0 +1,4094 @@ +#!/usr/bin/env python +# +# Hi There! +# +# You may be wondering what this giant blob of binary data here is, you might +# even be worried that we're up to something nefarious (good for you for being +# paranoid!). This is a base85 encoding of a zip file, this zip file contains +# an entire copy of pip (version 25.2). +# +# Pip is a thing that installs packages, pip itself is a package that someone +# might want to install, especially if they're looking to run this get-pip.py +# script. Pip has a lot of code to deal with the security of installing +# packages, various edge cases on various platforms, and other such sort of +# "tribal knowledge" that has been encoded in its code base. Because of this +# we basically include an entire copy of pip inside this blob. We do this +# because the alternatives are attempt to implement a "minipip" that probably +# doesn't do things correctly and has weird edge cases, or compress pip itself +# down into a single file. +# +# If you're wondering how this is created, it is generated using +# `scripts/generate.py` in https://github.com/pypa/get-pip. + +import sys + +this_python = sys.version_info[:2] +min_version = (3, 9) +if this_python < min_version: + message_parts = [ + "This script does not work on Python {}.{}.".format(*this_python), + "The minimum supported Python version is {}.{}.".format(*min_version), + "Please use https://bootstrap.pypa.io/pip/{}.{}/get-pip.py instead.".format(*this_python), + ] + print("ERROR: " + " ".join(message_parts)) + sys.exit(1) + + +import os.path +import pkgutil +import shutil +import tempfile +import argparse +import importlib +from base64 import b85decode + + +def include_setuptools(args): + """ + Install setuptools only if absent, not excluded and when using Python <3.12. + """ + cli = not args.no_setuptools + env = not os.environ.get("PIP_NO_SETUPTOOLS") + absent = not importlib.util.find_spec("setuptools") + python_lt_3_12 = this_python < (3, 12) + return cli and env and absent and python_lt_3_12 + + +def include_wheel(args): + """ + Install wheel only if absent, not excluded and when using Python <3.12. + """ + cli = not args.no_wheel + env = not os.environ.get("PIP_NO_WHEEL") + absent = not importlib.util.find_spec("wheel") + python_lt_3_12 = this_python < (3, 12) + return cli and env and absent and python_lt_3_12 + + +def determine_pip_install_arguments(): + pre_parser = argparse.ArgumentParser() + pre_parser.add_argument("--no-setuptools", action="store_true") + pre_parser.add_argument("--no-wheel", action="store_true") + pre, args = pre_parser.parse_known_args() + + args.append("pip") + + if include_setuptools(pre): + args.append("setuptools") + + if include_wheel(pre): + args.append("wheel") + + return ["install", "--upgrade", "--force-reinstall"] + args + + +def monkeypatch_for_cert(tmpdir): + """Patches `pip install` to provide default certificate with the lowest priority. + + This ensures that the bundled certificates are used unless the user specifies a + custom cert via any of pip's option passing mechanisms (config, env-var, CLI). + + A monkeypatch is the easiest way to achieve this, without messing too much with + the rest of pip's internals. + """ + from pip._internal.commands.install import InstallCommand + + # We want to be using the internal certificates. + cert_path = os.path.join(tmpdir, "cacert.pem") + with open(cert_path, "wb") as cert: + cert.write(pkgutil.get_data("pip._vendor.certifi", "cacert.pem")) + + install_parse_args = InstallCommand.parse_args + + def cert_parse_args(self, args): + if not self.parser.get_default_values().cert: + # There are no user provided cert -- force use of bundled cert + self.parser.defaults["cert"] = cert_path # calculated above + return install_parse_args(self, args) + + InstallCommand.parse_args = cert_parse_args + + +def bootstrap(tmpdir): + monkeypatch_for_cert(tmpdir) + + # Execute the included pip and use it to install the latest pip and + # any user-requested packages from PyPI. + from pip._internal.cli.main import main as pip_entry_point + args = determine_pip_install_arguments() + sys.exit(pip_entry_point(args)) + + +def main(): + tmpdir = None + try: + # Create a temporary working directory + tmpdir = tempfile.mkdtemp() + + # Unpack the zipfile into the temporary directory + pip_zip = os.path.join(tmpdir, "pip.zip") + with open(pip_zip, "wb") as fp: + fp.write(b85decode(DATA.replace(b"\n", b""))) + + # Add the zipfile to sys.path so that we can import it + sys.path.insert(0, pip_zip) + + # Run the bootstrap + bootstrap(tmpdir=tmpdir) + finally: + # Clean up our temporary working directory + if tmpdir: + shutil.rmtree(tmpdir, ignore_errors=True) + + +DATA = b""" +P)h>@6aWAK2mo!S{#s3Wt_Jr2003bD000jF003}la4%n9X>MtBUtcb8c|DLpOT<77h41q#LNB_YQ&<$ +Wpx{ODA|AYn471yGJJ4o^By}nH<4r3y3}kqDJU(8>d4v$UNhih-AdMxnLL|x>HDwa#Lik1&``i5ys{O +6sSI)T>I~Zf4%g85bU`V2$qWlFv@RkY9x~v^sKS3gG9N1J<_0cB*dyy6ync@J?@2`+)m|?_73SDNH1m +Q44N##Nyp9zk}k_QAiaw;m`t${CQjcuD2R10cRV;bZN1QB}GB28VNlJ?WVAM;q3xy0Hs>m)Lv)OR-z_ +kJT=cefN`4j(o;KdD7}B;~neQ)O!@oH>?){D;D59n!zJ*0OWIP)h>@6aWAK2mo!S{#tMMuh+Z*00344 +000jF003}la4%n9ZDDC{Utcb8d0kRVZ`&{ozWY}Y>A}w6x;YO^fgN`(b{hsmkm;C+EeWC=&-?Ww6}xG +tlOFsY{3x1&j|n^%bl>sHMezi0IQbhwS%PsL*f~aY2^Jr( +->p)--qr&r|06wh_J-^zioU^jr0+!=aW4nhPeobk*_rwE$UrRTF%uPDhR9M-K~vi3o9aPFu(U7!^k`W +x6h>k&oK#&LP;J12Kqozi_UZ*<5gUDHflV(D$5 +>Ngu-j_8O}-GQayT6qQ_`U(Y(5Jc!MA_5XQK;CTEzILQ%Xs!nw#k_qM*6|!Zx_q%18hbh`kOD>^skBu +Fv(VE>^-plXGz~WsF|4^#(oqV6-A5cpJ1QY-O00;nWrv6$b9NL@J0ssK21pojQ0001RX>c!JUvOz~Ep +l~kZe?;`UoLQYeN@3t+dvS#`zr=>s4W>6K`)J(suBt+7m_C7P*Jqn*kiKP-Zi^x8Vm97ow4HtB3d7k_ +3q4@H*elL48zF=v&JLfMrOWj!LOBr%+xKI0%}z!@JG>}1_`+;3bd#_png);=!({95G*;RP$dHWYgu6q +(t+~ynt(SH?HY@OEPW4OnY^LsOzICFR+LuN#(GM7QZkTw`wq@akA00W41?epJ|6vouG$k?r;H||RV}? +*sZ#kCvNdME!C~h4tZH0`K5PA&Ue9*HC8mrN1Vf$HG5Ce46>%u6si1CaYaR$F^`9v?oe%x +HmCkq%ube_QHI0%9qi-C=B-bUMXJOVfd-_|$+b+3)Z0O+yk8>=Bq!}P%@9d#Ko6vo06kjO(e0jq+Er3 +1Fog}EJ_-N6?)U^D8GrE&<1){$hBjq*{q%TM@5v3{zM8>CqoYQCvE5$ay0=NE`9J~qnI`PcFDOH5RWU +L{{4^7Fdq_jtz1n~(#}@eZqB#XNvZR(?$m1Ea}yok5^+y9u5{fs*t?vel|#FCj^!z1;M(wpdA}3Fy6v +@1|`VP3ETIX!9sXF!^+P7IZW3&r7vRE18kw(YAvVXVR}PmCn(=muOr{Rn|jrt}I`IgUDcZH4}^J&HQ4 +r5cBDDNhaDxM#H}le2avLdHz``or9UGuR7Q{QWQH|-}g-Vv488rDx`+r$OcX<+oY0CKBScE(q +OYH%+3ORI-+t}Fpfg@dzoOrXIx!Q*=&Fog7m%eVG$KD<+J>6@3s6e~1QY-O00;nWrv6&Z$G|nf0000U +0RR9D0001RX>c!ac`kH$aAjmAj&U%1)EvFVxRjSzi=C +>J@cMk87yJyz4~-Qcqlg}hXv}1CF`fEox?~SG{pae%Dy$pBG>tnWs3{>FohpTZSG@fe-hAmws@4PFv7 +Mv`H@JnAXTbgKqwrl)IWaYE>+%OsO9KQH000080BxrJS`BUSY&Zb`0RI6102u%P0B~t=FJEbHbY*gGV +Qep7UukY>bYEXCaCvo6!AitH486}+g!Qroon8cWL64rqlQ)qv+oo+`Ix{4xOTmvft*+w1T=EEymzS5G +^8`)P&pIkbFlCdRayLg5@77)Z?^5SnhsFz(_JIdEKqS#uQGSCDc+L +soerpw6J(yuVII!C7b}u8@K>~$Qkl)R)*@YZCXf1>s5u{}*Dxjlzn!*BNA;k4U#vU0{YZf*+QtvkKXc +D38Xbmz%=um^@b_s$AqiT^uT@R$=eDrOe>avtjThKL$%qaEE_1&#M8{GPTuke_Zmy&Jz`<4^08Sj12$)E-e568UO$QaA|NaUukZ1WpZv|Y%gMUX>4R)Wo~vZ +aCzMtU31*F@m;?Hp&!C)c}(J`&P}eJ*p?GbU0GwxX)~G|21nu~Ry>jgKuH%>|Gm2lfCNFF>`tFj&DcD +GyNmta#gb7Jz3R%UV5jw6DVusPYRx(&3btz`D^*i*T{blOCh8%5E{$xb$VmSww6cT! +{-EB9?9HRE;NXu?$|Cw8rBuCD>j-8RboYPr6t%B{Oqr563~Ll33JgT9x%HCyIQRf(K7aC^R&g^;3nQ| +5XClk#ou;Lfy=d|L?vqz|p=NnA>vi!IM@FJZNRj-oq&UoK9w*FT-U{_ENKj|sbwjc#BYtB1C1)9g;vM +I+M|sBn}wy%SQQ5-o|J&Fz5V9)K3h3U$a8E@xGflLkH7WevR7yh`(`OcgLz12Ko+w4%=J;v9~n*CSjD +owZpigCiL--ohNMPM4KRt!wluI{onK{m0Ye@V#<<7Nq! +BS4#2xPO96svdM+&wUG0w%7*3Rs9NmXtSDvg3vmAiHq3P+4++D^@HD$E>jIsAH+GiQ34^~G;s7oltV9 +A_0B-v&F3jp%T0BMoxnpft$RyScgj;A!Oq^$gV4E*4YJ+dEo|v#jQO@V>>#$q5*ffklh1jrocxEf +cn->w+!+%yidQ;tK9I?BBypg3mWbN+}>4)GX$Jw_uF|i08kPqSkE00oAvV@XW-+F-Eo(CR+Tx}vmu0Kf +7z&~Wp%j2w#aZY+q};uUH>YLt+pexLA7xR#2Q*hqQ;ZJ`Sde4Tp;p-}f7p?o=W2rV#cy`-Vjs+ +L%ltWR|7vA?m}89P*$bi)2^;Z9Y)eA%wmf!pnMvfu?fF9Jt}(lj^{12O~8;$2j+?CIq(aG}{3n7CZ*N +^s83`;>9;!d59OkqH4RE-|@1d;)w)eAe@3)L8V}Q4V!`+uv`kR!L_yrZ}t0^-&zHIAO$I~t=#jf{0$& +MJl5<`mtZe7_8-{xhAXa7&mcjaYE$0oip75T2@@&7%0`7LXpYTrwuLsZ%WyDuT)I;u +53L63;9Oi!1-CQ}bw+d#anoBJ9gabIF|-=Go_vW~hz=dQojsAP(3@J@fn`x|I^LXN;^KxJ$zARr)QL6 +m~Uz4H%}E{^H#YUBtB8+pRUck{QMj)3ggS>VwG5t#YY9#Q+~_!;IR3s%@H>)wq2*R>5t@v$j~ +jQ(FwEP8?6T{vnswz-tAkDAPYdsP)fm9Hw4HA3dA43ghRI)mIcOFPG&tM5kEV=c1TG)Xfq~ot!=}`U} +?0BE|h2rF$;xrRkEEuEtL(!%-3?Sz`%V7~ +O0hXWU`;*U?eJ`UC*Fa+DkwSM52uPZr>{Q!xEfJJ&rhjGE8=K*cNY!C2dd2H2-^eUvm`x=SRi)TUg!+ +eqGeN|apI_MGHBXH(YDZb~S5u@eGx@ffXYOj6O)Vg`B4w(ZjJk4~mpCo9!w=X6-}-C|gsF_G25Rk +SQYfC2RC+s$SX>KzU=TJnsb&}bg`cdN~6!hEp9lF*lgwFE5K&?C6;5ujid32Nm|=@H`~~iINeuWk#LP4wx2CU#wXVm2Ty@AJ1-{7j}de*Y37jBB<$OhFt8~ +R@N>rW-uIYE+#F8TlPeL!Cq5-D}NJaD5D#H!mtGYRB)zdX$$d-eHwT#+(MT*>dO$aMG?V^dG3O1cG%N +%IzGN1pCyI%!Kc5MN9+M2z53u`N9N+~|)2tSUcmrEwT+3JPluqL`SiHxgU*` +?JqgqFu-Yx$W4K9jlan}a@UYpXaXPOG#OQ*CDa5&aS+O{Z~toJXu5@yC$_$F%t^@64Vb*=|z^%YV!*1oY^5bo*wb%)oL?=rx_Cg(qK&Jcrk(Thc=JCPkkkyrkxl5(P)ep1zyp(# +$CL<*52>whIS91tJ!VlhfK4e!Qjd0qXMs*C~PsKr76uhJzvf4Ob`9-C#bV%5KBj9Xc%I{_>7b^p)eJ} +35_8=VPM7qKMFW!6bS<>LiXL-NjIQ|XIKMHISkv3K8sNjvu%JVrZ^+7a%uCZM?nJs{Ts<1n(>Q0aB9W +52wm9IT)7|=7o}u1L6YTL6c2Cxi9 +;i1RInID69`X!7Tv*ZP3so4SZ}d7Dxf`M;VG?|uvhRI3!j +yS-q-*p%}wii@NG!^T!yv8`}crkil_&UrqJ}I!IAG$;xK*tcj(XfF$LJl95q6D@DEn)Cj_2SV~aLr<^ +}t^_!^3&cz|@5m%2JIfu6@ni`4kSF&0{GeK6B1Mt+H%W1agi*~dgZk<3Ygil%JtN@!Mr;(_f&e#7fh? +db#|@M9=9Vb0A?Gd12=G2L@{iyxwxnK*_ToxAa7c$``}MEn>LJxq?#(_+51<3O{lJ281kpOvjqppn$oFsj#1+g-MH(0)#+}^PT#5UR>xisqnp +5)3iUC&t+dm`?JKY?hwVja*!#ifdlbCP;XjhW)MfM>oNmrJf5(bxX4<}#mL*UfBfxq#a=h`qFUNUlzs +Z}?k_#Gee3LPl{hO;-Wxub$322y{1P$aDl=aw1TPE!*H?*Py=P0T9ek{ViO*HY*3g9cO|%(T(AQ7_#r +_j1G-`OZA1P*JEn|Zt12Fe%G`e)3S0$vMHU5`4y{zqZ82_C&@7r(!7l#YOw*fx@yGuAc;|Y368Jpggk +bW_lhnUA_fODz`hW?B0egghGY2wCw=Md{Pe!=!7LjJ!6o8%>Q4(E+N+e06Ph@v=^vXRg={0C4=0|XQR +000O8ZKnQOuGux}3=9AOd?)|_761SMaA|NaUukZ1WpZv|Y%gPBV`ybAaCzNYZI9c=5&nL^VynOiRmeQ +oaNJgaiaO3YcHx)6cHj?(1F;fU()uVa!zE8v+Wz-GGrJ_0B6U7DOnz&ig#G)MPSwDpy-2Udd +c;)K2BqWHLEBTbE`hl4M<1b*Yj>=v`sTO2|Anm8`VM?b(?h+e*95^s;&UXieUH&&+0{^G)+;tfO1Vig +q+af54-gs15~|H{(z8gJ-1BMV#EIJT+xp$knwZs&OSZwpsgLZe~K~l`4x;;ct@5o$8O=sM3P*+bGtR& +RUWynVVd%WTt;sOfF=G3Fk74)u)vzSRu{67x@kLq#w3QWzTe`c+@<5ZVwRV8(qH3Yunksr(K +mR*_7ubcHR@%@&idt#(n%Uq2uX3^tH?TQ2(wQ9w)W5<+-nV@ORCf7Po?P)5IrPT)6I;jlFcXmepph~f +593$SS>Lrgw39C3sre|kw-oE($#p~p6@7}ylUO#ztf%OwpYHWzfx!|2=salJK91qN{swkZc`v1~aXSGwbUsKoB{$i^yP!AQ}B*H5O?c&k38dZTO=K@n%GO +j;{AKwC+kK#CN3?EPR$?IrSFP7+ITAq_UENtwnBw$R$#9;(zCMkjdxbJ+_v{RHT&( +-+3x-&UL$3@vyh9k|eS!ThBrYV2d^X+T@;LR%Ttmz|*+hH*KfDzJik=sdQunI5svW$BDIttWi7FZKR{ +)Zp)&-oz4&Vg_k96p1l!*SJpF63Yl{RG2@xG>_t}{_YnPiMShG;q#61G^~g%>YFmk=YMK3Lbgojd*VV +RTr_~&Nz7ijl*vlLQjM%7L!Gjc%s-Wn{P;K#qQ|i)N5)L;Ldsx)uUsuh}fZRu7p;WYwEcWBumtbfm=~ +;t8z1dQJz5$PzMpPq!5t%tJUYBx1`92^g^Rzo{B};I#G&PB9toH=^fGej&=>6{#@IINdA}R0_5+8e#h +WtO%4DS>n!E$wO*?dtO!o@0hv@4C~#C +zjk9=hfNApfbPOR}EK*Yu7Y9=LQqk(N&aq>50KreNstAn7@90vrpRaMye`S} +JC)yo)*&x=DL&rvae?v|S$0nbX7|Xfvm?EH-VNurGzbD2hNx9_4zFKq1LVWgCf*u_fPpGmrk_H`NON +37xVJr{89JtX#Ti+cs#$l>ZJ8mokg|H<-LUd&eY*TVPs)4 +YetD|GYn^|Hb%#J;}{eQtP35L9lR{Itp(kBFoZJy^S#TO5T(yoFMgIpFG$jGcnjnH{*su#N=<7P^w7iDS6CcSv(q +8^UvGHu`_fzFRyMErxxVz&bLilDYi6H2>t=RRnNuG|t9m1vI0YAG#HD*-uWC?lB<=}dj1f-`gQVNb%9 +ejf$`76IdY^$yRtn_fK1eWz)T`x6Gd5&xZbxYw6TL^>Bb2W?%03WEUGXJ|eCvp_Wfp1*-_1!8fSw%fn +;^`+w6N3iTwxLD=~K?sV`0mDV#4}_<+#0XrlB0&vi&SzZR@jx{k-Am@tZO7-H<}zchDN)x&kduhaR#z +zX?I7~M$+H7lL@`jtCJGoF+x8UXTzEJ*Xj)_tPE%9Xbm$GOyxuJ_M9r7usl%owuOzS(C5MhI*HC|=Ut +e}!O6jl8^-r~m#YMb{MSSjKD+J1^`58SNd0FFAbfhbgy2Hw(vj%}RcnW(S#1G;)rb&nhZfQxyi@lAdG +aN&RuBHQ~L9r;lq+ycBQp$FXJC#YVFzBH!QLG7MEER@Tidanp?fa(A`6Bgm9NfO2WB*`5_FKK$I_$Cs +zHHRTE;Sy(O}XUMKi*LF)4%WR`ZX%3#qZejy%&?=d{@E^=x~Ei@?onL^=YF6cht~}|31Ve|06lKLj;6 +6f}#rBl56(P9IKN3oN%JdFNW7vQMjNpkn;m5%wLtc?|k=@_srY^nd}zua#Yx>^3WfkkMrk+yMv0lv!} +h4?m}(!7Hvq9@_^23)thjz=12*AT+GLb-z`o-u)zWD#wt$Lvff04S^7*~Y|zqC;D%{j+;h1Z{~=gWNh +jl*Q=?p`yOR}%q=EtpBwi4ra4j{O14=Gz-Fql-vr$5UB}eD8o_~j*Q2ybH>77+hLO3P2u@Uv7WI9yuS +K?dFkX*)sR@QZ@=dxUF&sDX$8xx(Y#wNc224g;F>XoQvbP0lfxuly2<0mgp`uDG&qfv~t9BTb|GxfbooR;$ZkOa)tU-M +SM?&qOh3M)%yOQCc`{BTJ)>)N +Ijk_ZB%s{zPyJXzlTCYt_9e@M0miY^}L;0nxevKC^r(TpKE^{}RM}t=Tbb%g=zrlUG +Z<(;RER=*&R6JDvI8Zz0G`E{=m+xM0|0^d6==I;eL;7gB`t4qvx~P~&JU*c*3Hq%lv()Nih~u7>*UC$ +7GNb^iYS^Os>rh%GT99dn-@rdXf{bQTO~T;t6&_R{tf!$?s@I%1zxrOd+Dub%PH0?6!bJAM>9{jxw3E +r#$vRP^F>575{4qW>78oxrI)cRm?rJr@rAr&E69cFY2m_;=^0BtDAV{|kwq$;=*!Oq;rQ*?+b4)0@>! +p*Kp~U_)6pWtybG&6xrGaP>Os7mt}}=XJg|0b&~|E571~5Unc^cSB@f3d3$iU?I(05Z12;sGgcSOWCw +^{4s=TU*fvxCny!4YWF|TZJVqJ`tvI?bEC?Asp%C*b*i-w#Qy?F)ms3^8tc8^zxnRXoL~8MYF9ti8lL +IjC+8!(ZhMzo07sqw;lVeLA5kzMk8S9B@|Z^JC-j2>x{c{MCk+m{rO#cAMW4wVXCeAI#Lz(Z=tj49>( +U(08^$fiWbfJ7fk^zz^tA$Hl=K5Av8*?OZ!p|B2*|Q_UdJ!DPq_mVWtRy3pt;6YB3F9>TY$g`*Pu_Q$ +V>WaCyaB|8LtkmjB&<1y{wOHpWqRb{C5q1HQY=B<%!~CPmV +AW?>jw#iCPV$ofk?P%3!`YU3kid7odG7(>fMtQfDcc@xsE2}7^aIIjr{q0;;|lj6Rp5|K!?s#1Wt755ui0ribEs +973u;t?Y!$~djWijH1a#dZ_~!DvM9L=>%e^{$dpB+_kJRJDlnyr_A<8jZ|%8f+O?N>=u}tXRdFw4VxP +->JRQpjSnfXfYBGq5O&jM5yN@;D3S*Oe?8q}?(GV}$T +zdsq3aLjBC66x3i0r%ipdKFNbp7Iz- +>f`W)YYvj=xjJ_q&C@`nYfGq*nUeYmog0i!FFr@K{yI8)cX4@jdO{oTWThYjzz#MtE +1`d{#7AiC7aXWWvG!G0kv2o8<4T4jy=2bRYH@$MKE0X;J5U37pyw0ObzRC;x=vTT;@$b>Psiu;;5I96 +;!Ll)-m6+}#cydMdCm3N$J2SB(pu{0i_d9Q@8WDJPV>98DsuJ?9$1hz4`hC)H?X`uoPCH4qu!pKpW0> +7=i@703Ja{9D&o3^OWM`=wh(zyZR +0HcA4z_QAi!BBaxJ3f0mB{Be>2uNarB?`cg8$~k9r?}k;Z?LdZ-lg1KTQj8&?U~#(JZQUA2;8EFH!c6 +?^9i`8k5c5a#>`)^`t@CSfJZEM7t7BhtyEV8Sb**1Ss9aaAjF&*R_-4>e95Sc^Z+mL%9B&-S9MUsZ$MkM0I^J(0)=(>YM4~V;Atdi%q0$#)@`FZ~mLvY-E}?^Qu6G&&W=P7De(j&h05a(NYX2079 +vSj$389jM90&glCevmCqt|!dA${Gh>Y$+?Xk>DE{}d*cFLboS7sHkV1R9|~H0QhSX*MLgX=f?n(PtwXvzx6-9p9gfE`(5E@ +Z6s#7!*-I%+En&;d>|JOf)KaUE;=0gRAU2wv$5nTHXwri8sWGPAUfB4p{=dWzLWMjV;}@gjrQK~5P_G +?)oato~PA6gk-l{b3hHZ(0|wb|uH62JB?iJcErk;0P^p;|z_^Zl9RY&uZlN}&a!KOqBUe|QW+Vs~8k;W12vSM40OaXCiVMdI2)cQWbi^uK985#vM4cW~Ce^%t}V&s*e +M-o6q)$yH2o?4Gg{S&F)fS6_%4T}$EXTX3S$4z8NK6C#~mt64AVpqR`g#h$6?q~6Qr68JoLl$r7lPHD +-kHlInReoENvdh8&g)!V~^-!ca6JJeQBp8xRpjpuKR1o^Q{T8=!+1`55))~ZNCY*7Oy0`sFvO?9nUSC +~D+X6tZLL7M@#=(0Mvv1B}PZ7$~w$l{(t6)^9}Lkhd6ZRFbyxo#&jCBUkUE8f^(tN0;B%}`)L#>}2tPqYN0bp{2%!c$5#)prK60X +zS)Oq-paJIb*gay@!CLcxU}Iri?d=J^CNMH4WB|D~+XoFE1@9z+to6Ix9_fa!&_x0|aZ5D>6?fcR(l7 +l@x6Hz~lfyFQpzOPnhRftL9=+9TQ($Sz6scv86Z+A3;-Aw!E8LZEuyX7`n+&OkyJrDXdg_I8XF$1LiM +HHgS%YIJY2hZs7V(}+ne{HyrIfIx6m>?BwQtL8-vJg}6o}cY4Y4?A(uzH0>#s6VtQDc_)bQR +t8$Z{L0{H9T@&1?C~dpzDNp1jRAz_c6T9A>)pu%({9)O2l)`X{SYzPZ#B$h;7+eSg)1lkMDl$jg_V0k +%n8k6D^cIf>)`to)f`siG(v=*Sg4e2^u8EcWE|(d&Zd=K0oQtUgk0E>Rx7($n3@5>SamAvk)0n +8-L*(o~UlF@mPDQT~$f0ypFbDOSOlWLuwTEOPFW94(Df{Z)t41SFL%Z>F$4+=Ide{{8ptpK`z913RV1 +CtA9szP8k{GSrvpwSj(>{|CFyB?WNV&whQr|y{d-#EZEmdlyo<%=oV?5w{;9%%t|T_e68u}0lm1WDoU +AK?FsIK2^q?|zm{G`OZkZL+CRXk$mKO_2mz6ORiW3vX5s7LI5%cptXL%yalhe;uocH}S4;E_T|<0eY9 +CSKGvLRA4&jylUwo+2JE)$hiXDzL?Xp)+8*Equ4=8!M9wFFV9Sn}Ux1%;{VeM@{>3iy_q2;#?so2mNKzVK7=P927D#n@}C+sn$DEwgKdQP^fqycmh-xy^yzG^Z~ +tj3#u~iUHp=d?_wS9F{#|$7g_l>iu(&R7P|ZzGw;=ecpNtK9DzEWw@%Y2MvvN3q_KJP- +1J|+Qxh9wr}mhBTby2WnVaT2IqE&_6cUUaoi%QSH4wu!r+$^P)GZRz32zHpMSYT_x5w*)H`W}+B?4?R +?j&fFt~E1Wx}2D_f3 +@yWAsvwrMl8vZ=iF)Kgdc*(3mbFhw3nc{^_MnTq`2K9k`3F1RUe6DpuoTjDTQlfFwf!(Xisd(6?Ye5v +pnRzEeX?3ZHA)+#zaEO`m3eAMdB+J~u_yUIvEF(vw1mXB +?U;+b_J|$h=KF^!;gkXDwSz+gl_?%>ic<*M@t@Y7FgeNNMl!DVH{S*Nq)Ce}_zLRWQAC^G$dwYdwpC) +X_d2zeV7GNrv3iWpPtsSs3%qiG%6zr#!_|c_Qimf!axP;aWV}u +I=|6t_b~f?hbN+wwtj!PKdwoMH<_>G!*(tbwp=w74k}Ylilkr1K_Pf6sGu- +wzHW`lj2S=`^&vmeMZrZ~i<@dGb2bnN>4X+w062kaBFOL8P82M^fXk3KSD~{Y(#%akB3+ju-^&8~z}^ +7zo3OIzkW)1iDj@40528XL_<&zWB>*X~!#vQfppm8qK{K;o0d3I?vuGmt`1a=9WitV2YqS$jWB6j2Bh|b;|IN|sjgkM^D{N4CW*eE)+P%haSb)pt>h|!7q&py7HP +1Pa-Mj-hC;(YW1b@i0A#~enjrAgUK&bg|65lyjNhj4H~UV}9*SQexI6CkVhazBnti%rs?4z0o~*rrlc +cK5;=RskXdTV{h2ZRK9!a|ad-q6`RE;2aTja$Sr5sk8ZEe$bgNUoF1j&P#0}&k$OriLFk?Z%T=jSu=z +PM|C2ci2N;K;7l)eOj5?{a2sD-u{if1J^p1~{tq-l>}r2Y8t4P}|_(fNFcDR%(9)d!UD)A1&RfrBYT$ +#9{jr6jPm=#t~X?jjG8%XK{jKfrT(;Bov;|0DnundO60)U)^l@aVzPyX)$*@8G4E3!mrv7oDxnZ3S}s=Qc +0>z@$elqSVe*3{~RKB^)%ZVg^7=CzEF}T?rfT7cA{%P5UJ0+|X(BwTkLGDqL>#gYCBis8us()dS^Wa6{pI&-VIC1UZl +li4bZz8Q*yn3aBS{(hb{Dz?2zwPTb7i?c?)>VUnLWF0FtV)so(2pe!TpvOh{pH@}_|q3?!)t6$tOwey +7$Uk5Fr&r(PhlLsu7d}CM8pH&Q54!==RA%dx-(dRs2(T5=>GsvO9KQH000080BxrJS`A5tEZrag04{L +=02=@R0B~t=FJEbHbY*gGVQepDcw=R7bZKvHb1ras-97Dc<3@7-^%P@lQcKz-*7n`Gu9(Pm@~+mq<^5 +13t?#a6g#v*gi4zF$Fo3i)!>UxC;Zk{xyhGmPo+SM-GZ=hP)SjzS7nQvtftjA3o}T`mk>`1*A49n=;w +)97R5B99GSAAzGAj$Q4#L}DA^oGH%Vn&@DvQcQinkvwF2%d|m!ilwqR2$NT4#A7=6SXfLAoiHak>!qG +5!`e>v-)?EpnDFbfSnuM4D7mUE*Rz7<)PDDyg825~wP3s|c4Y%> +DDYZ(;WxY(?51$4{HKc1aVpT9bN{=@5c-;cx_1QjHj4zxOVK%^}9nU>Jg&*h)XIF~D#7Iwhb>0OY-(R +uZ1AA~!ZE0|HE^N;+r-7$~Dr4N)T1LwqwNSB5%sS&*z@;m$?UON_w9spCw)+>z*tO4@)6b2cn;gL&@juwpLNMx|<= +1F#c?8S>T%cVCIUv#y%gU-F5(k)MD=XLa_hC}~PS)2|KL0_$txEKH@WcSlq5~R1smw73NxXxh`C?(F~ +^=Y1G`M@d_J$(?pmrE&Tu-eFSLSieQ1hVe>A`W0wRm7nMVm?$bPEziP!agW)brcDy$aaPWnrHcnBsnS ++ko>6Da-XS4ItOJqh)*Q~F)Pz>DY7|oaU71YhfGQL0PLdJm?0P*w9CqzQ6O+DsTAlR<%`n*;Lr;J8v# +L=X$*TE)@?3<_@S(|34>Gwi2~)BhDjMof)+$kjN=4}Q8Oce1L_XdQ}hhF1<2@9jK|`B3DQdhxilDp{z +Psk%}A)h4E10dOBf3PAVYu{fgZran?$;Vf&W0}tPvJ#5+ELf0Bc}fau3oipxM!w$WuVYyM}FJp4v|JabdTkY=DON +t7@g`V%p$ttzPOCa9?UTRU2Fy*ix(gt#HZuTD5Qj^4|6$P4Wi6R)XV+I87B;GO)7i98Ifg?zo)r>@JR +E3G`z*VUVv=* +uT7~6lQdCqW?kDLQz;;;G(7o!T9dKBf-L~QGG-hJy0-HiyAV-7H|5wRDr-o)8Qq;v+g1>HB?z-u(M;~qEg_ +$=SSvNl?4TI;s@TW3p6cHL5c>uhPsuiM)1ovrOk=+^c-w63@HhTm2X>LdlxJk^S~U&eLY9r%;6sXJ+! +zLgAcZQzLmF+ufojGHy`aHl)_z)DH7JcPHAsK%W=vxz16O>^&R%W!sJizU(Z*236bJ-eQWE9Ys&RPf4 +cK=K^efC@?oJuyE}VH{7hG}+W8&l7RR%8WD*X)w(LBUWy?OypgVS~pCaW3GJD!_{`FV<6TCtg!917@I +#XXO22sgw6GIe|VTtFu;+sDlio*p#Ax( +)re%=vF#Q11%%+(Ap}iTx96Pl_kx;`z67yXW=1Z~@h}Z$qm&V+hd84TaW=L8oNs2i?{ApmnW-Y*V{;M +eS;?fXZO@vkXK?B>XxSu3A<6X*3wN=6NuY4xgt5`6=@ZH{glTBoWLfPO>#ivRfYJg7(9?_l6AMtp&65 +bQIwvEK2Z<$Rkwngx$vH6r(G1a?twR#_wf{-h+_FD@6w;bbGp={*^U7&AcrQrS9<}A< +tB*Wr6beMENMGe}{Z#C3;5Mx;OekoJ2-mJO1Uu3HUO>}WXuGd&?z^7Z?jKHHUfb8|hCZeZq{I+X`_QH +y>8(a8exBJ+1_c}Z{b`L%unP71h%Be4W%WAEyzA^mf-gDqb?M;O6P;5DTH_+gRy9W0eLlB*+_6G6DuQ +g#}q*s88PvOWWH8H^K|CF{xBZV%mT#8%^4p3uIoJprGmNdek?MB>Kii3x;@#`>$^%+zKs%O1$KvJnvT +9@8V0fTSATEGy&EV~1{h-ekB{RUYoJC+FJT}Ua;PS3<|zWS}0gTzLdkt_n3Gf4fl4C8qm64&~kgWu5E +6-2aOmVuqccpSs(UcVT_Vgx-^AsHQgvHT$w^ +gvg@JUp1JDHOl(`f~A6jdXZcSzhWf(cJrsR}pZ#*?LRffH5l*m0g?A57Lk9;`IZC2$3=qvd@&vZ+DefEwM;@ll9XShVY(0F +otL!D1VD1`XTKx=K#+j2G5aTh1`i0G2DWTJFlCr?CoHD(O^DIRvMY0RC+|)^JK!*#+dfG^qedG&xQrf9LL`d7Y!c;qTx4U| +Ce?-mNflVNiaM7;6PVb*uj3I!z*A_1J}@lo0aO&Q83gf2$h{2X68&Ak8tNfx71={-6M-p8t`_wlBjp6 +JQ%TfxM>xynU7VGQtjZU0e(+FF6=goSH~Dl)92U5AWFA$!(jDy`=g7f#Ou6);Z@5>NmuCo6H{%&SN+V +K_=}IjeZO40$^ZyfD3DB1j(jshPY6lKX47n=lHJ})S;Q3khOr7mG_F5z=7378+gX2vVnI>hc_9#Bnhf +*uL#G%oEXK`#MR)1~j1r6+HH@;A}lIvr-g#d0VoDB=O3V~JwlkYOnDCb2@y-H*bCtbsv8P%|94b<`>? +EFTe8dUchK2i}WovEt`*mB)|zmzgTDT#V{pM2EqahFMUi?K&d)O<)`Hdq@X8SFlAPFn_9gXAK22vP&- +FMQaOd0wuIp$o90SIyHiMvM0ItmWw}B?p^yJ1S{um`WT^WQ*-FhMLu|G(GWjYXR+^@+jyi6(-!Jhut9 +~Yd6n#LP*+>dR=)N#XgFmEG;VY%T_%&-b&#beT_YRlOoa9o$D+e+{*V1G9@V+I>{UL!j +Im4I{h3YsqE^*AyQpvY`$4v7AFwZH(NMfLR6J(G$5?9D_$hn2bo7rPlm&G&f;)xIZJH+n*;LWltl(M< +$VGrTG-b)t^`PbDuz_B}Tx&L-?!yg5bg%`rUYipz#&{Zg>;=75#oBQ#O;j=hjr-T2yxwbf_`RwnBF|X +{AqXE$6mEo${>csag&~pxbH!wbbgcgWn%C{omSRIcMYSkqQ0lOX9OP3bc0#+1$};>(LToANg7G8S)vA +iN@J=$E`2QnGQj*Y159+joNW;*OgKh02t<2eM^y;o@iXvcq~7HBNdI&o*1*W^hs4e+G3o7a~kIHx{}| +OVFd$C!z=<}9;Z&~gDaJ$Xlkkn*y^gdf_~Svs+^9cjpwzV-IX!A1a~->UucB2n^=R=*SdzpIV17Y6_r)H<26Pb?x +|MkjGVS9025y1Ql{4?gxbLpL##Uh6Q8ZSyaY3-q)5)ZOp;A!EFVQwjP(P^I9tm^`y-$xN?TjEzIbNcd +dHtCfZ7-0)xKvQFEAhgi^-Mm&Mq7Y?Vw>`26QEb`#OTW9<-k&a>W)s1z!7RHf$6dbr6{9n_5pBV$aYJ +MW6ZnXz{o0Lt!Uar0|5c^&xj{>xeUR*z+Zctic|!*e?U?*)a`zys)sxEDt|TxM`<;%JFTrKMu!$AsPZwH*W&}#j^~L^`PuBi)xvLU1r +T?Snw1Rwp8y +F8I)${;*oZ-r-D5aiG8AG55N}l#Ke&=orB4f(KSuvRP09j?w4gv7U_? +J%$0b+k(5{`O`=8VOvjJUFDwi`HU`2!#@9fbNP?4vGRPMvShIf%0F2J&ol9I9I1macodg>#FV`9RTruVJKrwC{>(?IIql^|vOIznWiE$9}a4_xF_Hx)OF +TmPZkUA)=31ScBIl9VbPv$TwPBm3!G)fCLz!92_oV?Rhip5ztcqlF8+~gf_9VIAbkb@s@j%GNnMW=68 +k9$%}BpfPe=4Z`}$ce{d{8hE5Xb*RbDGVv|jzVJ#jE@{S6ZA`MF-okqrK#(s@+;Gc2nM4Bz}0wODj{T +9dno*lXGr3dj)b_D6(=Gq`tWKX8#r+bXB?f^d~jK@3O~E8ttuDK)+7_=1*u3&XBB|c)@4HBDA=Yr(_E +@7fn5pfI5jWPSGWaR{>Y_Y+m^#GxBls%A<;PH=W^iJ$gG&E9em}|X`=B~Gq +SJ^vsy9X(5G)u|XoH#0$R!#@dI;RT1Zj=38p|C30+?6JjKSd`Tr-s^C+kWZ820X$?&hw{Sxi#`IIXEz +8JSuURa-e_<&Z_9qNJzsxeqp6i~$oiIX68!lIkihz##*buFt_aIZvHZ`j97oH#Gg~lW2<3*U7VI-L!= +!8<(!@3`H;MOHH6I;vs=_E#w2lN~Nqw6K7v$jW(ggAmfv@B#reLa}F$O^EMrr5YIaNAK#RO@xANpB?d0Jgu1XJmCHaftbep;-gigf&(A +Cp?K(Emh~3B&D4Zfis(D^*ZUZ+%Hdle1HD>@^t#@SdiA@099tlfyC +~DKC!B6xL72?dWP9LHr{vVSoM=c6$os|;eKC615LNkiezY1KXr%a)Xx&NcTCO8qF$nV1?d-3r&~yVyZ5el>7K67m|@ESo6!AMq1vvF<|9Cq-5#R2N_L3l +7^H~8&$crYtq5WeMeWLTpy;1E$9;hK!tC~0)%R@ToHr820+u2tlNx+RosaB)uYbxBLG?8%_|)WUmZGz +-m(o%Zw;L^t2 +|`q1{Xb*8IcL|s0ug-I)06X1v#$CEwcBZwG6)g%{O52hmkz=pO&6;e?Uxp_4TiQ{hL +Go=>+yYaDo280q2MQQ+M#4py8Y-GzF$gr&&IrFIBk`t^!^AM0>7jjlF~^fj(sg!-pqAR(izAwC%qcL; +sxFox-a3lI$Z;{tajWI>#g&FbWF}D#a%i$QvwwrK>np-I6fZ(;$L_D~tNIxteO=G2OkPlXh$+>1+7{< +JA(Nk!2%Ql{E7P@&Tch8Z<)ve8Lg$R_LVZo}m9=?wRM71Ggz=&Kc%#C$@6uxk&+oS{GEEZD_GjHA<74 +wpJ22g!DnQv1EfS+stgYZL(&{p`&o`Yy+-ostcD7E#O;Ir|MKd7=mKeQq!CU8#831o(N2pQ>@h$J4Z0 +FGVwXy-S#dkzLRgrnHsc@cUEZqzN$AyC!G@^I;)Ak7}sytj(L)>epLOgb1V!a&7r+qZpMSi$VoZQ?QZ +QW=GCb;_hvbt6NHCV^>){g4r|E!j5Af(;)nXnxh`X*-}P6YRKR518;09^=2u%~Qsm?#Nc*NeCLdWB_E>x;Hn7%lDdGg`S<@EFqm#63NPTsuvJUf8OQ@>!}-(dQ~iVHi}I?7?Fw1yv|oZp?H?&?Ihz>gj$fai$GP +bRo6#BJj>Y8?7R(VC;S@H<=$56>{#uWxCZ7g9xC-jg5%O1P6b822Ee8sQ2@!Mf)vJyoGf?Z +Mr%uXwHjt0@2vDmT+rR}XPWPyI{h1u1)rytF$wlSwSe)7J$q=nEIYt+Z|+)mFo%qo{31bywGB1){&ds +Gp_Mi3X4g9Wed~n4sB*Irkh|5jrC*L#KdPtZvbOp8==cqU<14^tc_++Ng|hBdr6DlaI#)$mBLmLVo{= +_`buzurVKbI2eEZPGMdylM?yd+{D^2yPt?R&!=xsFD@utK#6d1_Wt7 +aAIudDOwQ*5)8i3X(JWBtv=^4S)ed5UtSB&GX6of7Yd4!$WRAh(0t@b_^VW%r5>8|TsiRT9~`JVJ +Mfbxz0Juxexp-CP_0Y=sz@Z%7FjSegUNV%*>O3v=`@X0VDzcJcMM;{+>(`R-F2fgus?K;ugf!L`5~5{ +Ra;L}xH4s%G@K=##?OMC{LZ!l{;%>xq&yvgP&8c;r`$cALShJc|MIF0Q1p` +p^GL#zOUPbzxuK+>eU8Hq|Pu>Qnr2U2I~4OY3pXNQ>QTecVs#=HYXJwvy!c(Db|8jU+Cq;T(In)!Eg6 +gSK7M!?2&3_Y=-kyew9_uHX$7-%?D%*tuvN#$Mu+aixD*d)YJn82`V&YANtrUi~0DsjdE#I2z0FbUUj +Hy@4CeT`lbi0V>?KiwHK3YyXs17AN@RV;MCv;xp7Uj=8y6A6E)DdU%?R0(PF*L445W?lxnsdyuZ-ZtR +K=COM72P{8vA#t;`vp&F#XkHwQLNGJ7NTkEiF~!D#hx6QD80V@di#V;Ck@r{I +`FvQ7zHC)Y5%A^gZaq9KQ%b{^dG-2%tQ^%puS(U6u)t_Y*k4CqL%U{O1 +4G&GA=lwX@kdx3^7O*9S^$n3i~=)59pFh~6D;j{4=c4#Pp^3@YqhzKhOx9~4sxa`pZ2d`oL?6ZBHsdN +RDE}fjfeW0pi;%Jd=EwOjKW}&`3pS&5m&YBabmY3YDL(_zN?2!+eguQ#98ltwOyC!vv4c>W@!s7N+RX +Gui4e4ZK^Sl~UEzQLF>GSvJFWB=(!c|vQ^l;vx_Up2CHmTa$Z{#8fH@LY-KI!1wqqlZs0F3QYkE_xz? +Dz}q^Z%nIea)3V!p_rSX?>Lux!d@sA9OUE@4*StH^*}_9gJ?ObEUpSn-e(Tn`Iov1g7T~_p})jl;u>b +pKyJ2js>}0fbE12<&Ju6uytVD-Xd|vOL`!Cn7gqKVu_xxr*m56p*wx_aA16dla4OEbl*W{)PYRHTrjPR&06xcxz#Nrd!%r)n^rsCCrQ8#fu#43Ie+P}yq4{>pLQKA_yw +vV46L>(_E`=ch+1tjbM0v7iNU#7m2@|6AltfxpcJD+e!e#5%j?5V)^+Utiz49T1m+~0`1;rWS0ksUAD +r-5MAsD7_x}Xbym8l>N)Gf@f(>gFs~;OO7HD@2Gy|{v0t5-7L)p)80oyL{!>fXR_%_rD-wiUPsMIl$~qJ3T +{-&B~*J+6Y;c65ZT`PEL?Z=Dt|UQR`INUB_+C0bA<2vnH~UsIKTiT5@z9FfGUxY(PC!8O!>Scb5Tt4I +bHh(@ivusbEM8&SOI9OIQbdew!qEhhkequT&fiQNcEEZ=;E5=}6*MTrqS?C%e@l>nbZ;!wBzx2O+YVwenv7k +b1)8J{-$8BJbTj$tY7R6dko;^c`UT@ZcA7{^G`b^R7zR!wm9f#^!Mb&Y&yw9G1s*NzYI_}^xzWPG-*a +2iF(2YQJ-Pr?sq{h!IxyE$sk-qX4=wnR%ekINQbg50dM!2g|?^7>saKd1G1)Iqbq4*}mIU7NTC%P^$s +9zDxHRbw|I>?x;ZbHoY(k41$jfN1DVK|~Wg8$Ujf&SV50#Hi>1QY-O00;nWrv6&&p3pDp0000| +0RR9M0001RX>c!JX>N37a&BR4FKuCIZZ2?nJ&-|51ThSS@BI~FPA+JB5!3-adi3N?q;#k0OrX=2B%=) +YDA&~G0FCQ;)UBUab>Bi_i(Ap|1gi@*orCP)o6EVRWsN4jeWrR$kAENvTKk!xy!t*gsaQ6aKbTr +Uw7>4(S1bR0{LH@!BsWQ|YVd}m$Bgn+mR6v)Y{lgBTlrx+W7!xv*suiYZVNEfz{A)*G#at +ne8i)xe6xtVm?&UUJ+YkLc>QFA1+p}2?pr$>7QJZ8Ei&@Zg-?8x2JZk<9b7UjLwuF5qQ|IMK_<{=8~^|s0001RX>c!JX>N37a&BR4FK~Hqa&Ky7V{|TX +dDU6pZ`?QzexJWW=RRZ%%s4G@#bpP`9(J)^T!6hruq|*8K`_wR@{AgfoctL^ +0Hbs#X`SGLL8og)hbvwAhi1nJueXosUTq*6ilUmu?EWGo-I=QI^C%gDo+gXb`YTb3BrWH|%;(O7!q3? +tjafe>2ZT$L^I?Vk0UiQ`cS*S*vs^|3+--1|W#J>k=#9k;jYP?g=ypzoz)2CF0R8APhyGneiMW2~6pE +3BkQ`YgWBfmZvqs=0`Caw2}d!s*t4?hpGi+flv@=s|k77f|tcd|Kp0Sa +BGwLJ8MMC*3zy!Y28}4YwOHhZ$7Uuq9t4Y$TnJcD>B;%I9A0Z*bbhnTA<*9;mQ)uxM2{DdGb{{v0pGy +Sh7|dhL21t28+tD@t!}4b${r67}nt0ZayfqS~P2`2UCl61Gd&qo5RYh0+Zl(W|2lu-LCqqrMkR+!HRL +K3PV?*85H}E2MfmiA{>XPvD+lq{81ODQpJKj6d{!OP;78l)!?7?PSj7yJ94aeoJF@g&6~t+w8if;SO| +yc-@p6YisH`x!+zIFELbi~k$}j8_dVzdk6_Fi4X4K$1?r4*UZW3RQ53&{Da`Ez%R|Fj*@ +&*sSLKGXCHn`rqykRB4C2)|OHvCzqJ$R$1EB(C)ade7$JX^E2M;$% +M~5Z0ds>PXUn8Li!veq}5o`VAR7)ihv81SM%PW9E6Tmg5r2t5flqlOY4(tf7S-?SYUR-;(^4Aco_CpL +D4F#M|O)%-?UU(ZZ58-uHqV^n8OUbANC?#(>6=;FSJbA950}QIEiX?c^vCOH-g +%{AvbT8CV=oXLwklv6cx5ePvMi(ZD5k7NcToXY(F1uN193xV?eBX$DlUSGeRTZ&ng6U7P&bI;v84aLJzs2jTnnv1QKa?e4DT9g380UxW`glm9T9YzR!iGC1>AiSfmM+(AQw2YK7Kf;XWgW<^v1g1?!BhU +$;*nH?*e%w`1@GI=H*Tg+q)D^(|4cj@_+tu}TBW*oc=*HCeDs8sc?ix^L8Ejil`!8$cGfOab3BeaeVx +X5Bv4r3NEQ;p(-MgE2Z?Ati+F}cuh7AsL*AQ@y+OJ_G1eCtMdH?>0sXX%vn~3{KTII!)AztVSSBG)&2&VyP{rrT-oTz{^9QJ-dNvb!l^ejdLHL5 +!x9$=2x56~F|&4Y;=XLut~J?{eBpT;urMbv}e8o@D)9sxw)O==B$hG8~Ld5Jw_haGy%@bEN($0f%1H7 +k$T73^}3i8e7lhRp?Vgvxda@HDXDxfBkJY3d +(8hcGaqJHfK$kx&4G@PN{Vj);L3=qziSuRC_kxos3t5RD*DiLrp&M10LXoa=pJW$Y}R3-WQe-kT?Pin +MRp7aACw(TFlZbcLmkEHVJB^#SR@2zB-62F-HaXWBN=#z7?bIBL~^m-(46E7z_KTCNcc*GiYZ$>v|gf +Go-n<7X*dsU@-_~1Bl;4zB31!Ajs}I8ak{*tGH?cz+i +!FSKwBw+v3h$bfiiOWr_u_ciFuR&0`yyW+agZXyX3N-`>!gR-0@AoKbXd^3It<&Tx-zS?9^$Q-Ac=%4 +aU2(GI!?I!~LwJBEKA$0U|51{3n%aUfHE#PUH&Y+tdj$G?XvCRje3!scBO7GV9)b=j00D+g4eY>PxN?E1q`m2EZ>!kP$mfYQ +Us0+BU;;ga%~I9f0ORv_w5OMyg!IM!)JfkOr(Hhkyy4=-Iw(>E%R1aH1UQ5^WGRg^ys-GEhW?%?iTwj +?h&OP_T%S^ePpB0BfmVn6cA9DtN1ADStR?boyvwcWT7+VB6iI+(08_;Fg*O`o$yD%2If*?Z>;^1)53y +RtNyc~XG@gy;J^{;_;06}AfjjYy?4&!OnepheYfGmy#)Kq8jI6`cD(&ngr| +Y23W%&XmV&xOmSGw(6XBPzxO+HAvqMCWVd>-#o;+;&i$scl{=QEWv9EArzsP=Mz)$|buD$Kg{;+L;m( +Rob1inz~mt?Efh6*rmQ`bTw8i;@__m04b8N^!*oF6 +@zo(1!+e{mbv`!&+sOI9^r=TwBTjdRXXMMooHpf-A#w`1PfzIKoBqT_3u|$1Dd|sopgyygR<8*gI<)J+1_Z +FZ!{41+r{o;Huq?N^9!+W@NX94Y7UMos5g=k5jMf&0V{707bStX`FFul&AB0H$$FMy$}0k2DZ`iP;e* +XArr(>mKBVF79Z&tfSlQXyq=64|_rZuw!ulx|n-DlSY>9dowv-i)w*_?f|Ieqpki*%cbiYWYQ8QyP^I+#!l37)C0Aq(?@T_2 +dND8yYNQ%shOyaNJQ`myJ8EccN#ae(8+kDb3o%U_pkw*2$y>E`rwxgz74TuyOaJ}39NAQM`Z7-hmQg- ++85`JjX&XYes{p+TQ-WbNSTLtwG>gAFT0t}g+%rn+zivwnJ*12hgp!2t-Ow&;PLu=nkORA0%gFskPFL +TS>b!R!G=Iby9p{mebC;Kd)2IHCZ-1d^u(T@e8jSWw)YlbZ|RpsRpN_=F6#Il5#oz=>>l9nJB$W+pBm +YO?HfdGgogii%wibWCp|Erg^7ASTsCJzB^Yzt4Gt{POnVylo|<{Uxh9?!N!X59ZtI_^KyJMolGhp{s- +O;g*s*tYQh0`>q)da?bXg6|#P+**>c^OgTe~(=AMPn#@Q>HtJ`@F2xBFb5@BWKEiCM2{EVNH7yj`>O= +3hFo;oWn0n~r0tY!erphMn7d;ZJ>;1fITa&^mKO6^{r^^02aevAp^T^*aQzE}YT<=n2HUlkQ4L+uN9~(oMbozB} +7dqS%_NufoW-R3#&LlZ#+Xj-T*DeOHCG_4hmEBP>|BS!&X=qORv7N2Afm4e +*2v-5KW9C8#Yo%onwF_;M6`o4{G)q2pLhaDqVC1YSQtJ`5t8p~}^+qT4mf7BAEjG)gIWQpx+aqi2rEF +N5M$5-r!%)@+tz-I@A&)Qr!ooJwmMI#wC)TY=<5{PaEQi2PAaf5I+&#q(ik>dT30crHqjJ8RmJU{7VP +W*)D7Na;B^PuJ})d`3HJz#!FaLhnLv0yZCN +c1lShx=jNtGNqdL>v(c6sI$D$2Y-`W=_F>dq(irWUOOxEpV7?hA3OE0S+`*^Qd{@9t%H^l8nBbz-13N +Ku)=*?IfWxy}G@*1twm3`YE5x|vu8d!1l!w4TXqqJ3+MiiZyE< +d%aisSYbO0oAoup<4)rKs}iB?fKlHg~qT>RBLY~G+t$v+XN9%cN+({9b$IQNBvS +AGCo)->XM}yRnpRmzjhnI?`-th$MU*sWlWXnLK;_ +(qEI(QFRMZ*tW`Y64Spld>I*kUd7?54a9Ov2(b`$i_jjxZ}cxOuhyfOyIR{Q3@U#Rk0GIyP!r2NrI~2V?8+;524_ +aWGjxwRkA`OFD+yce(1tTE_;q&^E#F9o7$l2Qc6G4~w<%+*_ugq_rJ?Wm_Hf=?EoL+Zse+$3W~i(d$2 +|XJ8TLeM+261~s^xle!5sE$@{S{9iye55W0?~9w0eT^*`jL0YUa`p^h*v^y-5-MyS;brj^1Oa5X}M11 +|%h^x3$?@jnk-ERP<#xuu;w$gRs9%lszO-6wClLzRm_Cvp*yg#NhQPp@HRjrj8^J +LNo$QLb)ik=9+rNWX{7D6CJu_MLIvuSJ7{(4EJ<$zVWFnyb_sw6sSvIIspv_ZPdD)d8)eyOwj3)&1|wEp?x{HOH!_s@SgPhY +(HiKcE)lBL~Px=kXHlkNnrr1DlY+H7OO9&B*b-`euhSiHiG^QS&}dp2_j=Ahp~_!Eamtfs@6Z&POBy|4%|a`pP^&AJ>ZP +iv7?;bilLws+lkk3Kwx$z&ad`9!y)?t3$m#w65z*4AhWqoqllA>ta=)oqr1TGHgb+%RfEA6AS10Z>Z= +1QY-O00;nWrv6&t>%1Yo3jhGwD*yl<0001RX>c!JX>N37a&BR4FL!8VWo%z!b!lv5WpXZXdEFXoZ`{W +5`~8YFjUYUPVr?UK+5;{?Ys-OwBn~7e{a^^hirl+n#3N}gDOqvz-#at=KSWmZqe#X>*P@fK%U$^N5=%pU4WPwuYBXW`AD^VM20CkAr8x&78^C{}TwmhI ++*0u|7cDUyY(!wM%StHff34-v;=E8ZbGVM8=LPS%!GzHAR`%jZAPL)xJ|i_}MF}=gUJm_O2o6v7qC2o(PfZhDXpgnFcWahqU;iO;PQ~|%6YC8s1*Is_DxgCj5suCj{Hhw-e +g`0UT3`9%R6ulmu=VNLV8YERhb)J1_cXkk!Az<4&GmZsB#EWvw~J&L+2bGV@uXI8H~*VT<3Nr{JUDAm +_^67CQ7B?8b8uloKd{Uvh`|}iTxgM?3%0-u(lIv-W*$yXxv5ddw%lAlas$?>BTqe6ZiJ$n-ds_5?uY} +ho9e`oxc40`6(d#2MYL1$f4>17l^hJ?<;V__fzw@NZ8WM#cH)G#ExZoQ}-MU&C(dl4FjiMvy<=HC1`E +4V(>uJekDK%$Fdd};`;-z4ZmKZ9~mHi=OFq9gxyJzveR^*LJnz8o~2 +LCX7slM%js&OBKBnn_QoOq#B#khY{)}1fM=2Q#Txj-X2xZb)s(X{wU1#77?{E_wFA2pQ==W_RHxn? +8S>G%!ahiOT^*cw3EsWGHY+a*?tGdaUsvw$h&S3TJpZ&GbjX1&;r9c9(nzl%Fy`7>IC3W&y*P-ikkh4 +{oK@IGqSBv=US^Xiid=cT9*(+1Vpx)%)Z664Ji>ci`Z29A*vEo-EbrmKL~$@m6GvmUREeiSXLJSqiez +i@s<_F*9v1|tQMk>=rzUy(-A*`Q7|aUj1@Zj@lB-`P?>1L9%LY%aq7(|ts@)e1uYxmqQA{=FV7ilkzX +}KpR!jCQAN9Yi-x+Qcn|Tn0cRmLIhIVvLD7ZA1hbfmQVSP2o3+$nK5!v33v_0eP^Ce=25}VLFm{^}vS +bNF9z|@KQ}62#zlX?9fq=YW6pQ5r;@5R%B;t@m!bt*B*G`V1?>#7PAYK}~|sn>T +{)Wz5OO^UQ_B67>fWOJ+?2RM;VCy=P03Rpj8dj0$l^UNY+Xx1Y`h{^`!|HwFpQ7g6CDBTf~Mo$tv2_< +)C7neQZho2OL@MG?xwv>dH&el6bIIuL9mpiJYrMJ-+V6noM{%$B%DqhfXaRX~ +wYI~@y#lx_O7R>xkTw1LbiBT;ClyY~QL9C6JRH}DF?5@mk(b+avR6GH3lLm}Pp-E6|6e)skU?rJGtlP +sB4VRX?{nl$I^APC`pXW&sK^zcX^jbJ&f?(NCq6SUUWq@to-M0d44gck6J3djV~Fbv#dojs$d65FA8! +SmhoN?IJxtp=858&13&Fx{o_RyJx!lQYYK&!P)bQPWu1$WHQKY2i78Ds4wP?LVz3XxB9-? +-Qnmc;w@;Ww4^9*s5-Ji%iacAAi82au{X^{Bap{IfjeM}9i;JKkZqDC}Ib*!^dtsE@BP_z50xdh +)`nzr46Mrg8SSPWA;2~C5pR87aGLcD6&cG??Rvva9ZEl)5CBR*8AnFE)EY!+rr-u2~<=NPRbLtKPlw) +n&p#UjERO|9J>NyJWmBa41}e%f_F4}=HL(^QL|-tevl#~Pc0;vW6&D@muA_=jlUz`tHd@EI77nshWC6 +=5_{ODS!lxJ>n=Ekuj05Uo^wrK?z6trDYwK7aaGo$|{5K$|No5%slW*@bx!wzEuZ&B{XH+BoDP@r*JV +TV4{FTEZrkw)JXnXY^ie({|lRq4)$PgI^vvxoP#r4?EFZ2l}6}0aAPK>AXa-6LO_@r)#HQ=2iZoHobS +pz$7wE(8wva8X-S$tL2#a855VH1`P +|~{5$P9ozL!s3}jn{Fa$lUl6|Ksqn4Km+k`N)a4t6Y;-gg952-zF; +7@*&qvX1>4z@Z)kPja}U1CQqt28nr3sO`nkA|a$Jy0=Y_&9%QSaYRl*bZn#(s`OSd@EUI9}fuc|!i9b9*jr%~}^RVVOK;i>e +hXOo5vpoTi$e35;H5~;>#ut}pyc&!e?W4m*?_uaH& +m(bb>=jAsbtZ2m{`u~XhwDGI8ZFq{QAai2EqJU**UcoVQj1gS#j_t@%pD=x+l!4La+o%Oaf@zL7gq`vml_+P9{3mL$XX&BEPufR;on;)oCc9O8 +A-lS7E4w95z0Len@bNvRy>i*uPgG`ab5()9bRLeA|HDHBvN7+nIXE#;pZUxk7a$HiDww%S>kqtVa>#Q +b3Vpmz$hcR>_YXj!!iR;Q+=OW-c5)4M@%|!#AJc^^j_hrnOegkj;e&XlI!OJw5m2N)`w=pTY+x3V%CP +qI8sXptriVvK4V}Qipm{a`3j{1SXp?rqze5Oab- +Y%d2FLkFP0+FJc5-m$KZs^BXm^=TDeJ$dO1rP-o6e8!Rn2v0i}$ncdR!H_GOXwPH(Syn=hJNZn4O8d> +69H7%}rf3ykOAY?LkBO)_Hty>LEso3=G}eEtiaLzX`y8;2p1f@8(PN@F(Yx3{_8gBdo_RAf0hpb#V)b +1ai?e!~UQy={nK!PINE>#wh5(bpdN7`mYfKNC0m(2AR$-^sllyl4fR^RFaIH`Amg2Et4LzUt2sK)={a +xBDaAGVuk>FBfh0?++y!jqSz^ZJy(kXV1pMixWE+V_M+AfP5^@|Mf+XJf&k9RL6TaA|NaU +ukZ1WpZv|Y%gPMX)j-2X>MtBUtcb8c_oZ74g(rfczF|*SohJr-pVW +e08mQ<1QY-O00;nWrv6&TRWc!JX>N37a&BR4FJo+JFJX0bZ)0z5aBO9CX>V +>WaCx0s-H+Qg5`Xt!!BZbrLcMB}M_*kaNEdg(<(fs3;vNh`=E}6qMkaM6wYvuHkG~myNTeihlKLTQlb +r9F-wc^7%ihZ)uZcNPLt6R7lqR$@vX<8)oceUrWSq3J4b&kQA8M>#21_noFsy#c +VX*$i@F9 +~Q$Zt~Im_W%0KadW+a-tg7wd_=lNkH|fir61`!%E8m2(tTk_tl#}K7Xj*e)#wO`@7F~8G#Id9D*>H-Y +TaweiVar5m&{J5;1ZLd8CSCDriqk6|;g`#WXt2>w`;!;1XP%XQ)~ApAR2@`;-*~Ty^!#TRfB%v+CAQQ +bFw35#=W4ye)OxaFZ9CSF7zn0R#}Cd_v?zZ3}x6B00#cJEhRzwHLW@d~V4r$Cf?UtcB7Ld-i;%lvH!1 +SfRM$^L#y%;<=MOBnTSUaBcD=vE3=3J8)vNUHAwLlp{u!pl7xs+9>iro3ucNf9x^LRcjDrh%<1|AL19 +bBDIG788N^bNs=QEVC$EOlEH?MDPu-l+r$`(%rj_N&1pkws==n46KmR=m@5hRk_YTosF9A&sm7l`j!I?_pV6kLr_rvi05xzuohKt*+Tu$f6oZ*Vb5{FeR)iqo24r<=HJ1Ns1iaKZ +0x$#WNI|Ez`ALezdVoyfUXw>Jg|E(C-R5=0$S0sWG{|84b3x(Sg|HRDb-S2g{lYDL6RS8ag?H{>5|X0 +Z^pGHZ-VS;g;eLosEBPgH%b;T-Kje$BGFzaKVhwI08*QrNV&BkOipqND+`{>UjM8fU?br0dP9-^!d}9 +vLpxi3wU3DAtt1B)L2r#9>|sOjeYjQG5oX&0TK^S&5mp1rG&PP0Ro&S9KJWhu|Pxi_Kb?JB(D{nT875 +A)SS?G>}a8RvI(LaFk4%iHP)UezinXO{4W$dcvlDDwjUx~HB|; +hb+q$3b%1?jX-+#RO^r`yi-TV8gO`(PSI9G@G$>Br=wY0SC1yx5~1T}Hp&J?gyHBuZa_y)Y(0)#~wyE +0+CT|}S?lMB=?k)0hI9=Yr^;Hib$1804QAa-`6kCfZ!POu$moqRCm+4ucyofVs(w^rfjxF*Sp3>|!^$ +f%>{&r9;?O!Z%#rFI{@{T#}?7rdP@%AbdkJ&XZdw4Iq1_@x(!-$vrT#Kq!y6|qD>=6KwM(VLMYe`LXC +TU>+tuC!qM3|uP9#i2gteKp!jE8EFgPS?_Io0xP(rNX40T)NVIYa<_viB4V`L{er~ +5?siN34&?9^Cs6o-M#8TcT}Ir}60*<%4tfz#MjTS`h!X(mmgMIm!79saJ4Oh1BMj85G0O^q%o?)x2cQ +H-O^3EE1K-`OZ-%C#$UpEs +RKZjn0T&~UuwfKt;-(P=yV>&Nz-00{oNu&6lWd?E1-a2H8_o`%OVCJn|M5~Kx--|J*%*HRI+Pz0+qUS +X_Qavcu4X?LychqW->3%z7Ld$<`wrZ2^fBz^z1z$G%t56f4# +N3Z(xAgdf%+@Nx)7yuo<4XHgCjbyRh#(!^OFO?fC~p6GnFzMtN|M9fddF?Xc6hMgx7ueZf<%;UF{bkQ +JmOJ@?9Kgc!KPBKPQ;1y|4T;sT6@cMjjNVkY0i!#;bYJe(Uo1L-6av0>CFR=dmm%Du*{CdON*e47WnJ +Hs1*8_a$oufwy@=*i{v_Bt#$ZPJGg4-qLa#DK8=HbAJQFkE!QUDPtS`DzqGSP`bqpTef?p9_VxDe+H^-knz=E~<=4_Z&>)&BcI+B(ogEr2xAEX;6AIs|F{e<9({s1`JCS%hf8v_CW@?#6qz; +6xk(X(C2kWu0Y$8A{J+S_@b)Os_J7=!Kk^&p*$`@OamgpORMgfjBn74=2E9jA*?E64!S*Xjfep%Y{{* +_Pa5{3FK3efCu*8Q+?k51ADjeql=Uorpg>?UjXHUgI?iksjmTH_Ss9W}fxyX8EVxy1F4)rWu0zZ}mPA +*4Fv%U&M#f@*ypIZxUDq_GhX;SK~IL-D7ZrQ*krsK0?k+y);cDi;=&#@aj=G2ErP6+Oru>=YCP4<;k{ +4JO)7n|kRm6h1n?LLPHYNm#_$P{g;iK7MI(*$LFWa`_kC;pRmT!%caPQ(UGgpV3XuU)~`9)7zetGb?5 +~^5Ar6@zPyzywO~vAdB%SC|)>g_JiZ1;V%j$*BI$*#A$2-CqQk0yO*G5&!PLg3uz34S0pN9~ +b8<_LV#1_PmRIBrSq~pe5R7Ba1pxTCbbtfA4Qdy)AEY1$wA($jan!W;pK|T@VBp!b*0jw_B0MEJ}p6Q +%0{wCL)*ktBYKv(gr~=8m)}pGR{{;UKq(aQ`=0N9Pp{mMXq#eN278r(d)HJ*S()%q|=qES9O+W!dO}V +w$+>6*RQz|kz9)CvnOAjsffQ9Ng=J@%l8?GR6Ca6yZnUI(lhVZt) +n;0o9+ZcR10SyB}Fir&=yyWsFukYZOFa7GT>SdlD!{DT1nuBzwTSxTH?M&HI7DG +$v`bw@uk>vJgnPl&@*@_%!j-j>CY+=DyENz8+JYsmR@9%H#r|$Lc{`&p=_UVCNJk4*e_|^QLw|1Wvzb +tM)EZR%&=l73K=Qr1j_k3~t$lu*QEv}}c$q*CrC6ZotUGf(qE~Vp9mOrkI(Z;V_DX~tq%~f=#vex=M# +id{ygtWE1koiuV&yZXyqu$<~k@6ut3`?ICD +R0l7N{w&jnJccjc_$^PLS(z6ZuYBz&zOAxRg={Tzo*BKKvXoSxsDDYv)A7@|LdIiNo7N+KLj;hKHv7k +YChz51+1%{OFUrko>HD#+6TK7(r)8{is8X!SXsZSDO^naV$GJ*ve!DeSyA12E7*`Y4+tWWwi^|T|5(Z`XgKDRC-?Mu|O5Q +9uslQHvr&17|)!Xx*$YtM*nn|5&R;f{3*vh=dq4B??P3Im{l*$c3;wcvp(y44*R!L9%M>?n$sth-C1T +gx=3QW6)Up~%Qee4%9g*`8tFT$k7Cg+@KVi?bglI;5}C?fI3THe4cef~G}0J}&f;v9hC>he6X6}VY+> +4^Hfdkr91Q0hiBN_DTk%_e7L@h +Q%5{?hk1N^jS&2#^(442ML|9U!fR;1tZx`4j}P|nOeCGg;4zB1$vT{?62Y8U+z1QD +HskW-lm)5g4rS1iJi`94%uw2zHGU=Vu#a%EPfmzPBaIA=P^c{zI-hT|L~g-|^d&_rECWB^8Ie&i^>h; +XEEAp<+od#Tgz?Qkm;ABHH54*}?*P|;a%Ors1iskcTtT8oEhPCDQ#g@l<5;BbeJnqY~sW%|%(Ah=%CF?R|BMK+%D3LzLE|tRtUBa_rAcNw$Ne{e +HdU>(7B=lFA)yb{X;&L?{Vh9K5l?lwjx|2x9gnAejUR_PF=g$Jces@qkuNC?9O#UxKGI;-XgY#Wn*5>zzvU#jq +>GFEQF7}lEm@Z7ok()d0z`VrU9yGQ>QTfic(o(HR)lCnF%~9AJwv>@55Vox&icA5FDPbg4+pu#eGw^# +{n^-?%@MTFwQ+aX+u`V6|5CC;>GqiA_r_vQ7!e2Y+vmctC;p+P0>8Ha0+XO<#0*AQ92PD2*%D>3{QVS +E$sRuGemVd((Zjr2<#mffTMYP&T(d?d{t8o_^nZ~2b}HX^I)tVg9gQci7YN+Z> +{bSYRY7IXc2mGRc4@n~<-dpV6ZS!}ZDEmx(RM1=WcW5^DIrDa3d6D?Vs+!Mv1)cyjp3z*tD4q%Yu>pQ +h-$+n!bTJ!RH$Vl$+jI)SO*hJ)%vd80^ug`=xa%3QaBUKGe9C6;2rWXG)xsd}&_eBDXM +!o0ep{WEbVRAAltLQ_oE7nA4`+YoD;VoVzGQLn@ZVvrulx={r9OO+(Du*V;6*>s_hK`tZz!F3|27n^L +k8z_2}g4j*tu)Q(GS;S_b-p>m2lq$5O}m`U{AJE*tMUUP6(SC@WGveSCyw?fJM_Y|TBxc}OzV-LFu8a +_ahmM;auqf1N`jrJCs%~&9cTnAlx!>}ySX+ZRP?EE-me|8c9z-kqYT9KrlOpnp$L^{!CR^hG|8e{9^q +gi)0*sYyv+OsYzy-~<9l3g%LAw-6|WLCjqVQ}~%lAYLFE>kkvn6IRuZwcPA@=60?wOqkX*q_|~v5x=z +=EM0v9{Ba*{Nm>Niod%)e|);Xei-QRA^syQ4CbPpFzJE#=m{R-U@M&@dPgNneGli9jsPnOoUu1HWW52 +FLjgkz|EVdCYJ)a&e1x${Z;I7%x2E}aenxJ8dYbL?4PqmFv!D(Z%}@F^XEu|OS}C}nSRo4ZLZK~|)9#mAv5Q!Ex+?OEX|t+ +pr|`di^qNG_?@R8J$av2#i^R`Y>*%L>i(rYH_2_+(LDCPmjST3cG&^22nv)Hn&_vhm~j_-}T?;504N0{PJ;ryYNUW-yOs@JSS +<-{M}v2o?HYT`Y6zAb>;|M%$LU!ql$weM% +w#r)fNx>CABoittk05ior@lx?#wuTARJb5PVa&HGMXaZ6uZt>29Kq&VdBDE4?DS{0O2j%GNT2i^Xz38 +_(ENssru&Vh(=N5Az3mcf%6engx;gSwRH~=5X7V4a$n{6l`u|J#IM8iQ9VhLsvps7ELQ(=H0{5w>B*c +8sv92Yp2mGbTm#5|KAja~hU%rn115ir?1QY-O00;nWrv6%SwSB@EA^-p&eEc!JX>N37a& +BR4FJo+JFJo5L0P8a+N*PwMRG9HGt<-4ujy$-QFKr*(n`-%xoWbqs8w3b)OA&^SM|Zc#X_r9l@-%$ +m20)ol~!4;no`a5JS((qQx;P_RFie1{#=yJpH)`WO^d{@`H&{djSc+Filg)tY6t_t +jbNYkqqN%;$3?E%W0Pn^i;gbDFPp&CpEG2HH%|0FCl&vqIF$lcNIG^B7h +ro#gs}YXB|8$t{pgS;eb#dXwU82?KWQKu%?mP18L4hfa!g88kJd5>N9irdd^{-EmpWvul6gUG4O{sTO +*%?UqZRx|6CZJ6q)CGz}WOJwG{1j*pICoFqTIJR1U0H%VPhlT`(~?p+s%WjWJ%9nZ^ZnKlW~Z&Q`I0r +w-79&_P6t+QgL-xH&+J-;}6^ZI%6;^_Rv$@%U^% +dDQ7Ca^P_ast$NaDdbd3v_76I)?sVQCUBsrA?B-!NCD=D^;agtrOl(iN>|)^YV^cw8&M)mTZ^xHSNZM +8hxi;mxUf5DENna9zV|zE^I()M~nz*l)6xB`pzB2-0L7dpoT})bt8 +47WoPHkDDnD#tRhyU-Rfa!}`D#$Lc*i_~2JD`!JpZ+v`Uvihl=w)rW_1y~?wuKNt)S+~8$)@YrB{nci +50lR)Xy-?5qjW4i>54si!x4eji=HyGjE;~Op1Yv6)~Lk3k*<>YrgZKwkEr_^YqfX%O$zu=$?4(VNOwyU8U5CT#`MiE^Z+6bY?>1-xQ? +aPNOL&ezeE6YMm7kYY=%s`|im1eo(n%86DzC)?UTYIc{$QxyM`OVFIQqaeQqy#28ucm-TAi*FSr7owKWLAlB~Siz7<7r722pOud|i~Fl=#s7(LPZxky +nAV7M&63ro1ciGM&}JCvkMjTZ~#Qft08d(59eoEs*MRT}?IBuQE_Ot>TI$qk5&M**u%7yM+c}1xj3vE +OA2w2*3o$^%}?t#EfMND2PEv#J)fq%0Q`LS^-k(Bok64cVg7-UlKim8qg0GJpvjo+`Lg>n5~0dkSGM% +4}VEDZ2(@;7@S2RsL_b&a%AWdc7cJ0-+|fxyBCUo`Nnh +yLXO=qZXQ6f042R_Gvi~-ID&_LH+0RmuxRi9dTeWB*f+VvmQ=a$y)x}4@{#hqp=2HNM30XM(+_4MT2% +H)`T{zdg8oIwO+?~gSa*ft5M-^YJDISZSb(s){~HvR3|mm#<-XuTU5ovmR|(yFywFwrOOAJ~b8Zk`0? +2Me9AlCiamF5@^h<`x1L%Z&+ER)Hnr2+$$lMhHZHfsxxBMqYuK=YA1>J&ev5U>M`q7>Wl-3X}D9f)>M +G!x>TgU=$8wYb>B}faqzewYK%=={j%NZ1x7}Hfr<=cKo_n00rkD0zA8IRh6ss8hsb#x~a1n`7l781tq +OsPe8E(9<6fwWMXwQ1I<{N)`0Mm)g|x(J$nRd42S?b0o*?TL1(}#(YKP3j%}bTgg +#Ux?a9z7ZnLT^$k%b3RvAu(g)uS)A4^lx@7EOxoB$)0{`)$E@d2b}o#p^?KePy(k%JR$KXavwHru&-@ +EEKek$(vB2CGks63QIJp^{%3~9_A0=xhXWph*^)yaQdoSeNrdiCnp(N9OOkDj0WpiafB;HituP*}CuZ-(4 +9m{L&0gES;hi-5acq%@Gd9hKF_gv|kBCS|?fM)v4azyu=<%M602<+^BE1o@>ui|5%b@GDpaWMHdx6I* +WqY|Pm#Yv4J1LeaGf3K!Z%YE3KiOf}h3*D|-F1noaZ|G+*t2W^j92XitY_uXpI`FaKT2Z>K$&~wd`w5 +w5PdlJdd!D`f5i(bOAkB~bFLuo;%#?38b1b&02>mDQgyBi?^3C8D_Jd{>E#zj;^D(D_m<8d)2Al09i6 +)e-LEM_DROZCgq+3T0DpAXf^+1Z;j26cS)^5W(3(JLc2eK@(k1@mnXa6XXD*SPq}B;AuJ4TKRu2-$%L +S{^i;l?EIx%4}*@mw(iUmr=TIN>m_`BH-_QjvrNe7hQ$umWrJ?KPUFGK}(g)t%eRr8fscMzr3Yf&s^b>%&^GdajOJ +Oc{Sr{_y-K_&?)OqRDd-o6qPTnO;}!7UJ57;$mndl<=_5g +d=}(eF0b^9w(n>-~_`I7D2_<%*mHwqu#kjozR_mOzbL5Y&5a4}mf=P=Z;syV$uWW<1&%erLBmego|pT +0+hGQ})P7K@-QVF|Ft@2*wWUSl$5dL3}--IG8vded>3+FPkoaz@}gfWHqVn +Xtm4cFmoBo0mM+bd0YkX+{XDA`8H_ru6LWgnbx~an_Jlu-UeVXw}>bTC6dfGRn*oVJfcsc1`~6s$6tJ +LiC7!svFP3GKdz`^3?;W%2y+)w1NEoMbiJ>VPK{eSxL?Ne$+iU7*Zy>VO5bJ6^-`q`=&Y-Tj->$>j3% +o=fmVgSQ!g$qPSIypXxeXa=L*S;@PHtBsO7%xQKR68% +1@w7Yl^>N@K^u1$OI!W!$x%21p63F*fEf?lwN21_(Jv=OAk4<@5%>G7!76c7_VN1Kk7l2bwB*ra%d+n +>u0k+xL!_0$-E(k?hWXVl<(<1%(};ftG{CG}|yh#{xzp?qS3|M9y>1Qx5v~o^~i2A!DtHlj5H{{4>En +so|nNvM#+cBV}0nd2sELYz;(&ji8Vp^R&~BiNi+m_+P&sfZa4*3{^iH&|v_4yBlOH{G1HHs>GB3ema; +4Cn45|4m@2wlmxfc3TzyZl~=H(A9N`ZZFys~htrdvMjAHN47A+iBZXkmUyFXB$r_zTu|N5jb0ty{1cf +n{>D<9AO;G(@Yt=hHf2EE+us`dHzry@_$e+;^ot4wIb<_JGQ2vvVmlb?a)Ap)N^9cAy$ +jcdBJtetBiOe%M~pCNAKzdqDD-lJ7Jxbc014HyX+$+5)| +2UZ?jVDyrh7952{+H$2=Wu1Xa%4cACmKNfVIz2g6Uw`?`$`fM1G&H}_(do+p9dGba8fZ=x2wr@Wry1G +l27u@ub-L8yf(Vyxsx+~vdN7|R_6tq7R>R@H1-kBHif$?HWujdimzU=RJHhp-ae>60rl?b08|@N3f@D +?0!Ucz2J~hSaoL9+)oe9#aHF;U({ +91YP9N4B}g2Vhtjr{%YOWhV6V@j1#+4Bz3^mh*aYW`N3o)|(@1TzVk28V@`cW!-!VmymWNY`3-GKF|l +Pxn18EZ_Y1XAN_RlASUrsTHSCMDRzYT#?P^YS7sGOE$~o=O5bWkWx2jysI2j^H!A_ph@?e&tML#O8Er +);&S?Fdw^8Or5PSm0rr~yRK(?^w!GH9A@f<`BZ^xzs<6Sk85J+j)86wJ$l0X;%xsmLXuq)q@xrAX*n6 +veN`)HFxB2>ga1#eY|HxJ4d;`|;y59TEPMsX;u#g^akAB8Rrf(<3jC!k7)s4@_?CAd6zJd%*lebK>_T +hTcNZK-8WKzn8s~cl5qXDmgWw!#=u-%jxAqw%Hg)#`yoA9YMMg@08 +R3sQY(T=Kcah6*VmM*f7s)I&gB0SPbjh`mg0;|9*)21=ev+=D25ArZ#TqEff2MYt-+w_38PMPM2ulDZ +|NS3ZLmNtRPf&tX7_HM(_)iJaahF|vwtpgUA+yvAZ;|i6Zhu#u}Zd@Y@ZKNWq#g?3e7*KRJrP+wR3ui +?JC&{|FMc-!hEuLKy0e$xcqAgav?S?3s`5uNjTCKV9bX`?walzE3dCzHA2~Ix@1Y~OP0!KpBc4wQ`o|_5LX9PwP_!8P!1(y`gz7a_X>)&TwVV_$*zS4##N=T-O`-s&s3wqSM^K47l^U19faXp=a)q#)Dq9s+HZXg&sp@odq(G(- +BY9*1S5}Ay(ot*2*VGA8wwuUt{A)wR-o#7j{hcS8-rh+%U9HfZcKV=T8;DF*)%Yr=SOW*FqkUY6`hS# +-Hj1%X(Wua}e#10I!81@3)YqR&bWPoI^U_j{&bN6z<{CCrvcuU=|;`dEXOLshuOi}c$!vLvzAqo0k +v$aSN;zFob>EP_c2^if&sQ9GQl|&z< +AiE2KkRwrDb2%EPXlwypC8ulGzhjG(rwrO8+1S+_3+IB?cnjpi$!nOx{u7g;l +EG?V&jU9C#IXv<0lY&|f%&D}q9|E1pw-v&zvmYe@(c##%X{%VMi)&Gz#aTVU>I%cRGvOzIZNS1Vt>%DL@`UPRfiH78)Tc)9}(?c)>{SATW9?P`)h8`&}lQ)y}({vP*{#l*kz>495Zxp4}%}CWOjm2 +;5RNY%Q+OH}#@e0^4CQU^Dco&Om*qo{hHvqHKsjvMtdm)hsHl?TRpH?-p0&E?{_I4-eJT)=Ud^r>UVh +1gCg@0V@XU$_x5U9qkkIS$}ls(qzcebRNKp^5UW+vJ%a(p&@=<3G~6q5Y#F=ubLq#Q(gtt*2+(=en^S=2zaZ*98Ipxqm~eL)?v(}PYmRr_ +j@Pm-q9jy;B^!hL+hQm1Q?@fL6c!++-TYF4nLvwKKm^`1WeRDFQ27UT!VRCitmHulP4DaeF&hkU11NX +*$bts8*2=e*e;m~iFfF%hY)`GvAD0KX*=yPiJOY0-7JaaWL0OPca^eNU!F(SOsA3wKWG-)J+~lT)#|J +*gml?2LVr^@v;Y3L2@L0dL!|6QvI2v6CV*U1gYa!TLN!7Bj(la+1bpT8X361%I=LnPXjDase!%KriVlY3pUle2R}-$3lPZmPHU*v;XUI077* +eZvCaqdl4lhN13xC|gY*w4(uTOjJbIOmcgO7X!+N9SG>mI0&tKnO^H{!=ryFFmLHE{KulhObZHszX4t +U_)bn-0QL{MOA}{{}B#qisn&GSJK+SeKa=k_##nm_{H-;vv$o?oXGLV4yZ?0;Mr%DG8>IpN+tpCC07L +St%6$_tXrLa9>Sa6MXJvod{;uPIN$X@pF~%oWhhQhd~WT%yyQdA#~pH2eS250atZL$^e{!trR8&2+?3 +Gb>#;LmC0Z_f*U-+(A7@I(QZEwH{9h33cz+ag0{j!e750*hIH@_>i?ABx7~phWVh{4p_6hp&$h0ihjl +r;K;Ky-S+L)l>z9VMPWR80dL}AS6fXd6g62CDA(X6n3430k*Ho18mus0~Cl9@4RR +>r_@!GYc}6dndF_Y4Hml&}6{#1L5Dz<8&OdJ!ZTlR?)*$UA6z10_|x0J|CJb|`=vJ0wv#eQ$#Y5uF27 +hKY^vsxnH)2?jX|l&&kdL-w4UzC2A{zj0)Bh}+PnwcxHhO)at8IqrxY6RdG^6A)b}<}4n`h8mG7j)yQ +7N?>r~KbySCH~jG5mR*f{E);fTUU;zK2A%Z*#K2}Cc>_4iBoSP#8**>8F(mc_Pm_H7cE|&}{A9|q2>( +*`*+(@;PR9O>qKpQxqU0I1o6II3;_D;=3+xBJ_=Q(~>88sJ0G8LN>QUrhow8<;;|+icYz0`DH88q63( +BJ<+57g88Q1l9^g9^axaK*@vX<%s$bmEW_2*)FJW;r{?$T;jw@fY0+Xn)a43b?SiKE8?>Se)Km|-#i5 +A*5WrJI`0Y_i7I-llfTDKbOkbkx!(yPuph%eccNcgU8^MI8S~#rxl3eGnUesBP`@-7qqscK4jHg1e@M +`B(NHJvNS71vXhtXtr%9@Yy@Slkdlw69jW!;e+myN%r|hGKT&0QKN+77ckxqjAK +FIT2^tm>ofvIWefGpONlW1E(CoCZE;fFHpaMiNgUOaZ@hy-+V8IX$*U$U5yofk`m0i$uS_K{B8Uj3KJ +|wJ1xaKa1QYsNs2}%N5BM2wiq(%Y3*+`G)}8VgCx={kCAraXMAmOz5mA#K3fQ&oL9 ++bx%E<)6(4mS#1aSvoY6kL*qit}Iy|8+ZqTcu{%_QvOWSFKgt{)f0r|U}NZG}D-3}PDfpSDL3iY@kAM +vQuFNY>~*;|Mj^KgM0TwJb5)@%=t8gXDv{JNuKkYa4n?jlRUGj<5Vt +5x?rWAV0*w%%fr91QN2>US|oIlyEBwq9&SztwNiU+!F-T90t-27Mm!m1X~50bPrqqRwQUK*qhzI5Q1PE@3d1-?JdXWs;b1yDOyP4fS=(Liv +Rj9qooWgNP^SUyf!wm71KZhH3C1ek-ySxW3*{8*wuccv`dvz@O(a&|+w+rz34{#e=pmM|2=gd9l3Ex8 +Ad{SzRT~AKTd*o8FqDljbcq?F`+M!tbWXtnl7)_fej>&BwOGK&veOv@o*=C-saZt{*=f)Y*4&QZBi-9 +>y*@3N?Y_-jE$0j{G!&-hSE56hhtAV`$E>cZFk&H~)xS@~GCAL=MP9Wl-y&i;j_40DnUZY(;x#m +RfUzwrlb1Ibg1@Q!v#gdXe5{0A=8GpHG`~$RQpy?+5rVT-$pw6TSyvOYAbsNW>3Ue~w_58@&+|6Ixp1 +(z7iOooxx#$dEgXeDl&vZ9b2Vj6lSddM`u!V6`rz;A7<9-%fr#{Q2mWfJc-=_tzi2I%7veEnv@wqDSD +o|6ofVavLWAjJM;-Ay9{jar2CqK(3Z7Njrj*XqE>Wnq76+>3(&ng6A ++Ho$RHR9y<>hgBuxep;{Vt<)65GvL)Jr)iOj|UD&IQEb^c +GRRJz>4To@#F)#pae3c+e@mDaE$H6!~T-rgLUdtkc^g*S(TalI_4CI&3n5-40U-i#tOV{M-4dznA^Ea +E#q1F(0ph15=)8id*Q(1o;Zp<7UV(x~3RHzi%=-ChpHsZhM)<+@7d}n)6&`t*3PLLeGswZhW{X*&H^E +#P;$;<~rTq6sOjY^fnU=4AK%)P#hx8*SxCsJOqG-`=>f6=wcH(fJc>vA=d0gh^m1=ol5sTGf0}&^FzmPwow6inNE#samGf1?X@i^gqxz|MUFK>oCQKoLVI?FB>L7+WM +YZq8Z!>z?py;_CbVR$6cL1lIB*r%F0 +^W@y538Qp7%zbIY(7R|Pcnl$tuQ+bq9te#geuL7MmKr>DT4Hbnk39^{z*?6C@D%ImM}yo0mAGYWendY +`6^@Oa;jPddPH)4`OhK>&FpNgn=&)X-Uw!?r<5shDGM#Cl^0w)US2YNiC57(?FPVDCR5)2R42K4cpeo +pV%bGUk`wtX3^!`H}$s1C(-~J$T2l5}97bgXS7z8Z_t>JeqXbf^%wm_d(@$zb5RB&EU+z?oKKMsH`BN +LbM0_QEK`xsG$S9F2FA!V;AA-tTIoVnZ)hVW(-9wKm(1nE0T6uvKkZn+dnQfFdL9o?7sWO>N|LHI;_) +NT7#oH}L`_L&Z4SKIhUv*ZN9tTvpl2835LUC`;R4Wgtm8Fk3Lha*-e>`5HA2$U#tjwHzp$|RSUJ^QGv +_5pgr-1VK9d$=r1tf>d=B$9PX-;d|_fGkJ@QRjBRBjGw`tj|Dk_6W^;d>Yec~_IIaO3G!X>*@> +Nh_%^UG%GHT4;D|N~mC`uEPBu+qlDDOM+ZN>|@yz~?_ub&@ZM^p~;rnM0ZckpTM((zX-cQq$)Xu%gE} +Pu--+SexeXw{Yf53xGCc$}YvAq|!gtinb@NQiV@!CrzoG@M12yCG+&^JMi1y+1-<0uUG9tpT$`a3(_m +nXDd;t9O`bqNM5rea)_Xr5#Atwn&G)_yhm?inZ?X;Ei<>cp2$e+2%pu1r`AXZ5->p)i@nqU1j{)%hbObsIrx*L& +I%b{L7+CGb)9$c~48Pxb>&Xd}2QPC+Y0D@(dvO1Aaz&7EAZSK8~Vd5yPMsBxrG*9csj|*cz++WlV{05 +tWlX0URV{Qzm7#ld`wZ-HH6Kw+sB`LAIOpW&k;t%=;+nnB2^Jp|WSnV*ARu`WcsqC}IB+zAktAoT}@( +7cWT9kW8?HjDRF?S-@oVA+w2#0SDj`(&*@QNr2zw+M@b#Nf>NKQK6#))==>=5LGV-?Bm6+yiXB{W{Av +G;mJdi9i>w|iTYD+}IHA_A~|@!TtZ#1Fboux@l&aX=3TChuo1p2B`E2Z7p_W+(S;ATx0jD6KiRpZ_YWrB?gI% +g%x3!l^0Qf8{RIiqy|%~zN?nP|^XZNFe*sWS0|XQR000O8ZKnQO2nrlwJplj!F#`YqBme*aaA|NaUuk +Z1WpZv|Y%gPMX)j}MZEaz0WM5-%ZggdMbS`jtg;GII!!QuM`xRDCja2f0N=2Xw5C^yraj7cHO*UnPV@ +K<4X?~B>5Qm1+?!l6GXU8)mm*gs$J7XYcbk?VOlS`~>M=YPf0%>+6$_`~24s3AK1MWATN$0%mCNJ9RE +znFp9FX5w=by8Fsp>`@5jQp3ZBLRlHN0yQLS=H%-=-6lP1^0fCe)S}8SNkuLgYq!FPE+gj79%09-i%7 +$WR58QYo~^N=-eOGLbj8^3_@B=56_14Kz*Ch)af1*hoWLRXQ9$+JmY`Z9I%nBS`cd=#I#{k@a5Q@4Z1 +{%zD*|oyp-`56Xje2#Hz8=aq`9M%ox7h^ZO4+xkNOIOFaV)K-IFir6jDYGX!Rm!phnKp!q)^ZKbmHF+7$1$>>Tx1iT}A#%p>iXe=cW-%v{f1QY-O00;nWrv6&i*KcmD2LJ$7761Su0001RX> +c!JX>N37a&BR4FJo+JFKKRMWq4m>Z*6U1Ze%WSd97G$Z{s!={jOg@cs`7cwUud$ZLz_6(IS&hgH2}!l +Wc)vFc27twz-i-ElI_3AOF4Q@}ZaI>0+@$fXL!~@$%dkXR}#yr7Fv1WmvBEdtMbRFS#+ou)|K~JGSRX +CMzp6&n-JhyJNhT4`$%qoO&{jzU#0E#u{Z9}U|{+tFDuI;)hl$ +L}y$D)mwPo*ReJ6~Os$%4djg|r?q{$49N;X>US(a@Y+h~zxfenO}vfP5lBuV&Is_ho?;!kDbn>iXUXV +X-mOsShJ=lM?fbyeG%YZHxQKk>2=f!7{uGB&K-|NQYq1n-vldbS*O_(Y*#vx<_;-T5jXRoE<4<#LBr;$B~oFg3A`=i}qb3r1LhH(2;`o* +ii%oeRYEr7(z+2P$~^|8|y5ob#GNj&GGw(Q1zz1CXk6T=Q%SFq=dX+~DLaEE1@+KHw(mhgGC{jZHWht +5vV|Hs(q$GYTpqx3#_Fjy{%&l#NCXJDrjmUgE;$M)0rAKu-+yLxw<-QV72SGPCUZ|@h8F)bA@us1C?$ +E;Mj&kOSqeCc5)w3shEc~A^4fYD`L79boV2%#gZ`9wGcOi@;%|KItXjl%#v!*)+{Ib}TZQ1;NsvdBX0 +%ok}Z$Gx~P0DPs*vbhoE#(}=WIX!S8;%Je!be$)OBaOl@`^nS?3%uHS`CDh(UI6DsryP-*x$2TNx`f| +Kv!ql=PP_~`(Ah+}{-d$_(;yDRW5`rwg9e1{nSEQ}=ztknI +`F}8^%9|visvwH-HWnVbjH!Pra7$%^OhT1 +>$if3ug!s~x)Od_{yImx4ZX~aD3{)J3~L&=^vkv*rVCx5;(f_`em(V!LnTH3IDF> +C4%IcHa4ltQZ}54;bt4pLp8W4y2wtIxG+j1vxjsg*ydrtRv~*>+(z{U3<)r%3Vwq%X6+<_uf0A-pRjU +KOo*QS>-SS5Y74Op`j0<^3l{p)vphg7J-1uIOF+T>HC>JSV20W6pa#E4$MsLxIq}3)a9tM^D1ejudt( +^&>O_2l#@TsCtBa>j-_iZCJKoY9 +B(kZr-Z{jfij))G{=d~^A +Y5nXCyr&x=fQl}8G)hmch|wy?)(S!gi47BUJW#Orb{=+HoETnqF818Ot3d^ur7?hIz|JN+)$N_xsJfqn`I5jaYV`JcyT?vOx3(26ptlt;gXlIspE}Wl5H$Q;8@x@(~V(th +vJ?7Sx2X{#gx!+WpyX$W~EdI7xGRfMo5_DuDsUwy1^{@2T)4`1QY-O00;nWrv6&p(Y%5b1pom53IG5Z +0001RX>c!JX>N37a&BR4FJo+JFKuCIZZ2?neO7I6+cpsXu3y0^7$y(4f_@F)0Y%$k0fMgRI_yIb2()y +PxX`3PQg*zT|Gwu)$%@l-0g_1My*~Hco$@^Y3K^{I3Q*)igvDW}Q!J_~4v%uSt(V>9b%%gQEm`mN8febUUL~b +`OlGgGmY}K9i8l`5xWzI +`Gb#E0)EN)Qhe!2u;X`U`5?a(?Q+iQEL54JV*pKI1#z +igz%j89ujR!4m9Mb!~wxDyd@`pix`?lv9q;sqhanuDP=Z;vo%2-c1PpKfz2Yhh>-EJB~D54Jq(eSJL@ +D1D1%BMYb_*4(}ytlo9k;X+;AFXsq8hGYac_r2G*|XM%B>u)yNQUAh+R4Z%%-dgXo;xmoW6Dm&n%Ef#rN}N8N~{b&sM2xQ3*Qbpmi)U^mH0N1}!nthfkhqyaxACh*e>E=1sdu}1=uY +jGs;XzVdrtI2V>CkICbUkD0+kJ7{vDs!-p#0bO2f^jKsTY-E5EaCt(CgCh=ZU)#U4s4z%9#ok^n+-ww +qDbg2!_l$k4#4n0a#q$oR}*nC#Zm6z#4qw-Rl^jfflR|@K|R)_ClGvLE4)T}#(^f%M9X4f2pDT^kE{z +No+Dg=qZd6x^TOW(0!Plk!ALDV-DFpI;vY=3Qo8Pd{=Ls3n6DWgB!ol17Cq?vFo{O9CH_`>+l-ouNuE +!*-L`pE#nDyet7!kD4V20ivx!xrCzW8HNiSPWn7Jd?z*Z}SEGH7gc%g=5*mNo0dTzzkfr5L3uNpvWPP +56#sc3a-UKnBJbqZr5EbH%x%c;B{i2)iMkC~c)b=FXu=*T$S2(5g$_rX1ge~^|Tz{USoV)GL;!%fBjs +Wzg<(eY(E2<(PCg3B#Mst41i{PEHT4>ODQ +nH1THv2HJQq=6{nkUxNo)|D5UtJoHhpEMgvQlNa&lw!v%eW?_M=gnBQN}~$%Yq7u)XYMvv9bzFc2uV_ +B-D&%NN%@bYww{vP<}Xsqqq)_L<2!3w)nU&IT)zfg!~zBPJkvt&?AY}p?Jgf}Jku$a!D!IBnsloqS4` +fX@9_Byjk?sk-ZxcRK>RdS|6+wlS|zyB9zHK$Aw3^0#+WreU|PBI=oCD*`G?P+fBk$T{)XJY`Rdz})| +cQvc^WQ-xCx6lXY4)_HK;33@AMtSD;!{8m!aNdYRj{#_!*l3j0cQ7>?d7ro3B3<%2_FX1yv2~H4eiFR +$ZwgQan446-(!IFWY|yKTFDy7ODV>wuqgE9r&qC%L3yirac_%!vzcFVhH$3IYO0ANe6pI^X8a&5&NA7 +5P)h>@6aWAK2mo!S{#vIUr7vIx008L_001BW003}la4%nJZggdGZeeUMV{B!ge6w7nMn^r0V` +-C*qaIiI-xizW%lkWDdk_FRY<*1#lmbY2Xg<(q2I@@==E^{XCz2O5hx$1S^K4p~F*n2SMvP_keQ79?dO8*@C16O#olNv?jpQw)EgH`AROOU~3n$i^>Kc{Vz$yf#BIAgiWVVER1lI1wC${<| +rw-Vug|Fm=dZX8q5psxiTF<3P_kMDH?DK3)vZ{rM$DD!s8+p*m`BXgo9bp83cNND6gOs0PULFtoO-XX +Sp6iAE|H+j=j|A58rd~Loc!}+z9@dl4~1W?F4W}-`4)(qV&)U7QUz>*-LY4(@G&U5{`yi!eKpwzieJ1#s>xN=?_+3}4eA*saGjKh<2kgO}0(H8|A*nwKW8Flg +!tNHGocjfpk4Tzg7^?Sw-Yl5nw_8+To9I3OJj;N9gErII~^>@yU3qU-jan(Vj)es}UtH$bH@{td{&e=?`Z}>dgz^H|J5~u0 +enUR96&LpssuQ{)P@11k2atO5i4YvvB|`P)Ri2&peHrsqYz*)eIC!Wv${}p=2MuSIlEJ{z(j_*QjZROL#`P#2vXJb;7hQ$g(;qtl4&tFP>? +=(+-O!;&|s-eVPu2i)hjGWhApNI^cw)NeGNZ5Y`O13XnMjbWQh=dJ1iS@9H$GvOF$!hp=1z{g}d@{f< +>K4)Xu_6AYVJglSO3kdU&*ONXe&b`>=$@i~9eGpDH=0(9%m}FYvCf0Z}4EqSQXG$rD(6PJ-9)m4*GoE +P|s^-MXRjTZQYy>yb5WN?@DpK|!Jx<8gKQZAHjo%>~n}o$zh&2(R_^tNmk#NN6Fgf(zR5-3?seCl*cM +Ct%sd@V0#mFaR9jhQv}%%|s9{QTQ(Eqn_GMPnq`55>DWLoVBb3fRf`2pd$8U<{cj7j^ak+WN_xt0Yco +&fBLz&@HV2tYxDsty_$CdhwQ;FNuIp+b21NQj48$=#&82G8EsoE>}uZ7`22XjD5+I-2rBzb0+ +|rkQH&mF4$X!uM}vy7&Y5qA2_gAXHLR6M9}K=5!|kKqh+YGM&7g2Ah?;tucm|k>|anz0|XQR000O8ZK +nQOI!Rn$1q}cIuqprm8vp2%U#eQ34*3Yv9}OtiMz|QR@8}RD~TKq=k=MPk|g;nm5hj1anX>L +c9L~OGS#<9k}MY6P8>*4YEx@M)OoYSk)E%*y-SVxuU+^7bp)%?m0kbT>BOiJ@!9a~_=pWglL-TQyOD_(v4`2OSUMA2;duJ5!g+^iSj!)1K?s_Wb)VoE3-nIE|Hd+u08OI +6Tb?F*bx^j*CqQgupP2~jT=C~?r%Gbj3d$JAT>EG>$L9zbE)VzDS|Dkb^QF{O^bvbudOx&u|PZ0dBBz +iBE4j8xIW}i6n}g=8OXI=LjaAAD300RJBODjtAa5c#Ny?&qt2_pwNcG~(0&dQ5i@>z}&2ywk>O_bUGHyFq@->TYe-lKXgp1W(^LK8Tpx9&d}ivBo~!lIt$3=_NC}Glf@0KdvKb +V2!t_aRD=V;!4=_>Kqk^5BO2<|@XU>N4@`)1^YdEVq8Farv(;;E)R|f;Zdw-X!1`B`L +dJNq+z!;hySmbx@D{}1hogkShD3EXv#7aZqE~H1PQ`*KD+oq?l4&V)_gMD-rd8toL+QPv%wlUk*Px!p +k{oUYL>vC6QG)9V`%~LiG%`vZ-Ve4Pmup4@`PF@Da~DBT^g~sYL0+>>l@t8I04~*#TrRJFYo +b5rJ^~&mx`INU%*oze)C}HMf{s{fgSyWJ}9$TD&uhj~6zkvqU>>z*3BTJwWoyRv3(Tw)gkY}puA!Lj#p0ym1q&0SP^R0GnW=~DrQk`SSy!txEBVil9QRJHIrG0xMuz}^6d6;3%NC +=iXj3JKIKX{|b<+820GPb#y1bhrbI^=tb1L*$LR)G4O-AlHmeXV2+zh@x%fVp1)&V?dj24eS8b7sm4l +NLVcfM8+H-}eww;a`u7!~QS^SC#Fv0yFc>PzW1H)zl>Qds`J7S!5l1}>l`BOn0w7KFRftbMeSD +itvQ{9{n2N4rXA;+BhwJ!B0EO{7BX3|4WyD3bNiNAPL(A>!iqI?5tfRUy5Q;k9@f*m-u5iyoh^QHJUO}rWW_Z57c00(!u`+K(2nS{c*jfg* +y)^4iMXw*5r+;ZX0sf|rUZj>9+EW3+SH}r08+@`J31|}u=NIN|9qU$@?4m1Xyt3|XSQjQu*s|ENokW# +_4(!9r&}umnnF?T>nk7UMg%uMIiA{w9BL_>I1lDwFoRf?gw<>S$beM0gW{`yb}%fc)&{`rg3@&@8+9WvFZD}!0-o@K@X;~8Xj>eYHVDr2zK#61FtDSoq$Z}Gt0qg;= +H9bg4BTk{ujS6+k{t9?=f{t2Wqj|$CLY({*;#T<5KvNT(hHvXArQ4rXT!2Y)*B@S?Fiej#y3_5+7mF^MeO_W_lqc|720Ha=0lrNOl~xit#CJu_=B#32vFKq(N$xDH6!HJcs0f@J(#Lmzj#@A?%D4h~A4nknOs~XWOl&A5n_FQOQ(F+G*X_0_h;n8I9rfDCK+;O$3~?)TUhsUG&5FjMz59Pvq(Uk^Zf=yd5x(Cp +$nGV?)Ds5V7P2oFte9h +U&Yi(I^zj8dK_Sg`hUGd?*~S=UT>2o|2J7;AUd@w%VK^OBI-EMX9;ikQZbpo>BAfE3QikS$W6@CJF6h +wy8ez2vu)a4?TK97VRIv~ldV9bUA9Bzwo|TyJski}50n8jQoa-XBLP`#RV>!jx8L5OjMByD2ZnvVlj{ +0aeUYlNgCrZcx#Y0WrGxEN!mWZk@us3l{F7rPF%G=W$F8VG8H$&s&Ap{!$)whCH0b2JTp@HgcW24tVu +GC-sZLu++pP%l%gavX9)YD0;W%_}e?y+>;3?bbhi9V~-M*%5}4(l(kO6v^s=PLflqZI8i%`y}Kh%m;;R)*g`r{aVnEa))7i%cfqlHr^AzvR}PsM@6aWAK2mo!S{#xtoA!B|8002A^001HY003}la4% +nJZggdGZeeUMV{BFa%FRKUt(c$b1ras#aK&k+%^!t>sJu!Me8U+8X!FwP!DyRG(Z{@_M(@~LZ +GFQmkpOxNXpxdO1HLZf8AZ*`AWe62Q8fusnI*PokZbfON< +T?oSQAf31UrNrQ>}F^5x(rI*fB;2}R4I9D@adX^e?pmCX^ssOH(|jA<=oAB$LutolJ&8Hzo?4b^oBKA +aSNi7DpRc7WsQ9a3PRJgvwP!z369#Waa>v$D~C|+VRL4 +m^Ykp0ajpk=e(ZU@BJw+K^f_26_5kxeW`oRV19wf&>$A>Kw&R6$J&3z4Go*)IZCifc5Yi!t;2ocH+LT +ptbStxLg~`-9U9#3|15n>(>a^qdn63i(3bOW~!MN5d%ZHyh7Dqus<6dH(aj4!39O#=7tAd8dnN4H((5 +Kgo0Aon7ykCVD4ZUJ)tykz5055m~qyF*_jWJ#D!mds#siN6xaEAg7(#oxpw!vYew`HAcuJF%vcBCJdG +x>d*z#A}2O8;FB!NwzxFu=TBju#G4R=tO8vs)>tfT?+n!+$CCSal}>a_*p$Sa2|%Z{B|`pV_G&hJ%H( +7=dV)raDRX9yC%9+;*C&EzLpcxDcL_*e&*r92E8DmDKC;f)Q8VC`-WUu@F}8AH)QQTKfD2=B*nuc&9M +L<>*;=(Q32FD(m+rhTsmYm-VoDU$DA^Sas6qo4mMVYca$&73ScS{_Xws@Gtaylgj6Y{2nbo0WMV^tDt +JGyM@4oS6=4u{;p>Z^1ADJ_!H9`JaHd(d~z~~KXn+of1;!RBnt!}9(A)l*p`VY+P`n7|$S}IcRTJh1c +x-V6e92{-3+@y*8QA0S0q+7OHJ#PInKv*QkCDxxJPph!YGX`t?na@dAE?s@>%l?T4E()h8_aLAh45lz +X$*W}-Fdkj%KSkV?x34=6(Z12)n0WYOh@Oo6pVFW!?~hnx$<5FuN3Txj59D`N*)!Rm0XN^ +-~51yxgW%QQp8y#zf$vn&vYr_c9DH$VK)$xbTo$ZDM-NXGh9(EnVI%OM9Ug-C<>NXgtK +@Z>DbA12i!kTzcXicvz!L%!kf2VcK$4=w4(qZeLX;aD8WSx?vjsuj*o!4_uWKHfmHTNIHsIM3;u6e6> +ff9Zlda@|DnB*4@SfjIdkfN(laM$kQ;HddcUSuy{dHtu|^`1KO9;vy(!e==dDri57=fv0$Y_Mr{#52e +046fxqt`i77!Laa|ZJ@ys3uEN%_vcEibsto;T%iEAet1vz&+QeC~Ae`>=4tvOs>eLTc-> +jj^dOz*){^O#N&TB=tQ63(q!#3u*{hEsB5nr?s%XW3{l)V;xy;rjW_PvZ9_txIDj>vfa2>Dz#DY5+|Y +3zNd>Qid}hcJMA(Ske$At)t8JyWb2+Nu)&WuGYNn~}-h!UDpYRHi-tw<(FAu2-iGi +}tYU@RjyG`*t-_AM_mCP_+5y5l*o8W>wVuSi`KEM-bj$@7NrQz>MHjAuUGiKa(hFxe1XE6sDlD@E>Ek +qHTtWUPcoR-~M%6=78x&F1s@Y_^x;M3Q7*SG8nG((+w*nN*hQ4Qj1pc{Ocpecb~VqlgcbMg!1wj8lmZ3u4R&pddoD$R=kuy +i73`PE5T6vp(kTkj-e5{g`i6f(RH; +yNh%Nh+GPC5CpQYGJDddXWNOsRfZKU^tOWhn|q}LXu0wfA2EL(1`d2x5$Hcr!(29(L(pwbe1Pz@1-%C +KBTiXng4L{dt&SG{CxGi+kdB%qiD55{t-s64PLDVypjik8Q%ZoJWLA=-jfXCd!k30mCU%6VxIAVefeC +XJRDz*^W=6878gUW@$Jv^~NFwhfBY0oJiXH>M}ZK(e3Vz(5UTji=MeZ80P8<3>Lc7?mJ#!ZKdbUCvhF +?i|d>M3+|T4By*jO$@{bSPm%qYPOtOG7jqOQj;7|aU64^*r+#n>Mg51&zVo|tn)!iyZl~_^{XTNFtHM +OjmH*g&(Iz=Jio)P`|w!-1wxda$|U2`KdD=21=Z9l)~v=&w@nGW4;&Oru;^&PM(#I(_+&+_%i36XyCPun;4Oz~K7dF+Y8HC +gfJXA)!Qvs5VSq$r9}VS!F%E_;NYN4SeLG`R2y>GD4j+(ja2C63jUL$yr3C>>p~DVM-0jxva@h?welY +Cre3s9M4&o4?L>M&WJO$@Z3nJ3AmgIb7g^uBB5DnNAkdT7V2<9kZB(DbSwDIpS*Jy-8TnILO%|5{n5u +baJgA0tn#+Z08MbmL+5bF(?p}5Z!&I!;>)0Bf1M5EMzBu?TU%uN;jMqzv6rCc#1ek*BvM=k(RG@QwvQ4ISP#Mu}RWI>g)%;vrH+YZY<=R4c^m4? +bdJ|}wTdE&&zIF3n6bG7PKA6=t~qX4g)kW<9p(AaeyUI@1Fg0Gcx{yL`HIPqQIX=2xnAGP+~a2g33gh +cU*ZO9I8L49W?&P6V$T{|4MwX@(^@n1g>j%6o+1GpHp#xeQRs<+R9xX%Mk_k1lG-Z*K9!bD{dD;vMmf +d+4>Cq$i!f;{~LfS3=gSUhNruyyD|OHc1-e<_CLgm09R>+0LTLKdN@w;Sg%1@)|M{r1gp +~&ylMdgIL;ak)Co3l`Oj*735>=g9QO}E8XW8hZMU*!(`Rq6=ysz;eHtKs<+1ldIZ_k{Vf#JUI}?AP@i +(Cas<7EjhwY#&hK4#|8~lZIIvWR6c;yXMGSvfH3D|dkkZS~E_hl8Bmcvdo3Ok;I_6LdDyBm}76EZ{#W +7?NK%k;?7yLQ)b3f)469if$2M-zK*1DP2I6RV-4!5=L70H>*v9qpy+Hu}TO2IBp|Cz;dM$D9C$j;!B= +-|Ulkt}OQ6RudW$_mBRN&Y{KK)9N8bj0f+-&8Yf>hQ7kfo3k0WxhGjZ9)OrxCW4MOCfS6P<;ju3WK5# +PmFQgL#>qlX#Rh>0qcu$ZAbuzW9BYgJtQ&{sZ8XfHK-sL=6jLz3DGs(=r-s$#L4rrZ6c2gNEH*LkA0S3rktm8jhUj$6~coMw +FBDg2mYMw{KS;OpAE~wK-#1hx4`}!Ib7$XN@4^{?XNh_UQ{mQFuiLpCkHwJ!X)!>&|Mt1MBvS0swtVaw3w|T-@!4+GslD)I6!>^O2w(nF?zwON6%~iuDjlt +E9#D6TqI$7&{LN6XLoTh*k%!@5`H~l`zU7v^QArSB@Ci>BNNr>gf&EQIkHIY?*YGnKgR&Y&hvQS5Zdv +uVNCL5C!!^5%#mA;>Onv5n`F?Ly?EF#Nir~jakx1kTHhwdgZF?y2G%hiHjGIIRxcjB`vlG{=MkJ57aP +FnWB42pPGIv9Tt0xwC*kozEIt{BzX^jE;_o+L?;_k?h`A@@&B;V#P0g*S_$le0#K}6t7k4l8mcdW8cwEF}6AVM6n+-`o}WqQ|y9U)fo?X@ZPf=-<2^sszgGw3?%i0+GF@O-H#iu +2!EHvTJ6smlDdSD2M7njaJQq-hI-&&I=#pi{U)7@Iv%Y(Q>{J2d))(KK?sip5!^kX9!w2V#eU5FYenO +PqAf)zBn?Y3j5$-rNEEBET%uwyI<%_fqO11I2P5VyeGynrxyXyo!6|8t$_Ha*^t5nXZkXhBG4%UuLm2 +X38ta!FswH{!V84uZv@2>qVYoL<^|JUy6_fT2<4G|dLl)tg4T?D{&F+%9r5OiDL2bfHb#!SCoS8{*LFKU6Fk +ym>{i^iqUxq8HU|0RN{0+_}wqv1LiV1W}vFhXmQtvV~$#JP(apm0JFZAHVK2umvdQDI!+Gm~R$IyJ!i +kZNMg_*<<`@S|Penm^A`OXnL?i^u0*{F_tni&29y(796qt;8=Eyl)*RiYz~hnw)A?=^P)S70DTeH_Ro +vrwTWK&w4Qvg{bEXuzO(k8f3>XlccM5mgciwcS63NNbqv_%~nMXefUM$8tUxx +K&-0HWC>tTP4&=g}vP)C-x(wg@|y1GI!gf=rwS$I&?o#D?iuoAxMWP0b3gLzxka?3rybrAN&@>`<8d(eZ@OfL3ps@ +F;zYAQ1M9l!0xv_MQ|Kal#*ko&L6@uF7$^Z!z4H|GeZD-m;(p-gOT=5Xm0ZhYHeg +PC-+*q=t#8Lh{y}Jr*&?=0?;?tFhp^5(tq_G`yYHqsowmey~+=RDeucF?-TAg`1SU{H|4oFCk-v`g0Y +B!FD$O8q{zDv$8TN*IAVYXtB?$>mXqXzwqX$#SC;H}L1p6(>s!A_T5oL5JlwTX_La~(nAm?E7MeZT$A +fo&u%}tE_vSA6e^5&U1QY-O00;nWrv6&80%$xQ2><}X8~^|v0001RX>c!JX>N37a&BR4FJo+JFLQ8dZ +f<3Ab1ras%~@@a+qe<_u3teYIK<9*wMnl%1dDFkL$YZWO?QKJf_z900xi)tv9zd?(mK9g|9hVysTa%E +ZgM>o-4AO^91e%``pkG~^oFJBvaV_)(v-q%Yt6bTdEm2ErZ_pM3uHGV3E7u^e6O_Y +y??e!NGQ7gHyF&`C$|_AK?HNv*_Gu%mfSD;!mG>cPk8$?`_)MarO`)6MCwViH8=h`Sl_coNBD +?cY)T2|;!5pi>^ZB;oL9o0S^@j#u&}5+EkdhHN(n)@DfNs_R5DC1V<)f&e??fgFVti9jg@A2?xNX?My +2UqTUkKu@l?HWek;?w9#u!UG^WVOYPEqHcoOP*0?sWhwBw1|vR;KhuQBvcMMZbU0Ak?K2PoTP4(Rym{ +GH$&yg=}oc6nyt +q-))Bh$>f7?y^vh=whReA`^&&5Hc-D8tpqIhp=HZ*yg=`7op6&rB0Lr1^3*wi3$Ddd1x+F#j!>dCx=IM_}y6~kiR&8_;P*8C@4n!` +QXHXoKf`Gt~E}=*9|WaIVK|Rx)g=0eU$q83C$hWHol*RS#jnuzx)5Qm*-Kx17U$DTWQpVTpG01s0jcl +9cD&X12knQO9N5~0#gRBLymZa_jp{b9SCb$L|+tUY*AB-PqiZy2s}`RSEx6NU0%Juo&_#45eZfwqdFC +kAeXjOda&Bk79Mk?3I +yYJmDe&yJCSC!v1nhrD-M$BP72`eaF&h;l`&4Mb|B`|K{<2 +edxUMo^=DoX0W6wEJ?zM%?x1={s58$ZQ?-_}EEBLHyWhmc5@YeOKYl +`8T>rkL+Gd`0FmGXD;jEnqA|<;q;Mrg-=;s%tr2ZJNQ&tEV60b4RffQlp_q#~Z8yI#%1am@s4ao}qtQK7bci?iv&vE9AMXOAq1-g{ObFL*7D`O|elY8K>Z}=u%#Iv3hWYMCd +x)QTrAr^`Zsbsqp{cWRpsm4hA@L^hfj18>JTe;?0=%z*4$iF_{LlsSp-1fx3kT?;ha!Rz8|ZH@3Nm#5 +8nvQd*m~9C5qqVJ$FTbvAgsxqU~_Wv&sqvsfLBOtp{%x{GBB^VOf#!LZ@&Q!^ybmQo4XP?Na|h%p2$@ +@8o*Bi`3y%b0q53f30ruecYZLZY)(E8pS_#z71P6uJX|1qvfo!EomSc+*JFL8SRP|JESN|5?G?^rJlf +*<=JM4=`trlg{QAa?)xJDieEQ2D{-ok){^t7Qk(G7)u1uunGv~rKAQkAXV0V(!FHqX>4NurygB$s^TZ8FaSQvg!IXmLdZZhMyZ8-`Q@ +OGwZLbHC9=_e)5Gf68(qpYtv0XRU277+(lVMOYtif)uWqiB#Ge$c!JX +>N37a&BR4FJo_QZDDR?b1z?CX>MtBUtcb8d7W2FkK;BBzWY}Y?xD#dllB~73T&s_c7Q$>=(ar!79Pc> +V_{^;mE_F2kN;j$vJ*S;Z0pU%5ucw(ilWjqP2P(9NgNPzqrH$?>H`R+ppgxc>RRZ+B`L`zr8PBVS=sv +5VwQod8)H2Pt&JC68tsy#r^Szw`>2!~r7#a7gO(OG7QSs1MiJjN?78Q@)?M$9U~Ds~Jz6c)Hdk`H7Y? +%?*&u%xmr2t9x!0wE+mVx1I^v`oNNZEH3bRJ|Y6H0v&SfmU0oRh8?b3CUd7bASIuW5T=s>`})p +-S`gbKy610_q(;7enPq8c!*Z(6@212^zSi@jp4qme}jZc)f!Pr)iT&Z}rZU{}x5N1mf?LwXVCKJh8pB0Q!1unDqd +%ay@aYVkyy^5_Ck6i%Xrm1{7UrOc^-I(V)K1bLQaM>1sTaWI>4NFPD!U;rFaj^GPg2wRwEz1}7h$z4d +ngY$L+qcpy!4F8n?{ZgrES4yS3F@F_!fj_^y?Vz?fOWsFPZ)S!=+Yjf(ZF}ZAeo|%fr=&OkZwUc^#b#6NKD +xe=f$(!U=+NW9b5yvtE&kC;i!~kQa8BvSUIHXVIx+qe>NcoNtCLCdb<3P^ew&V(f(Fz_X^m!NS5++CV +mLu(T*b7W0m9^&P+cN$m3dx@An1%-{X***1WYCbF&ET=LF~dlja42NS%TAI!t^tZw*s$>!@jPxetW1(W@~5_wC{)5tYVM02BUCgC%4%G +X`wI$blxot3=Xf7AKN*#)+i$OL)R~Jc`aZdH*CyQeX+WMVhaMXT^%20y_u;=c`vw&%z$ujvpm!xcO=T +x{K)PWyIgRV1Q11Y~wmdE*rjc?}nW4BjY@FA0*G{>}WeN^j{AX8hR(g@dI<;k5ozE^!(b!e_7w_XO9KQH000080BxrJT6-Z#q9_Rf045*+03QGV0 +B~t=FJEbHbY*gGVQepBZ*6U1Ze(*WV_{=xWiD`e%^7QN<2Lepeg&b$B2r^3+3ghv81)_wd%0eLMH(dO +eQ^y%mS|h8EUF@9Z!XAxznLK=k&>Lp^+PW}Y>S*3&ig@SyHZq&33-y~Uy6TKhbk^l)|J>XQK~YoBxh# +#-*H}ZsrS@FnH8I6=X0Fr@hayhdb7;RFv<$Wt0KDL%;t+KpDTW2|!4^r{nQP40Kn~kM~4Od@ag&!v#?l +PIQCx~hnt4L=50lnpf$mSgpKZ=TY$D=4XIXOx4SV~ZHvgJk|-+TwN69#{t=h63%g)DiZ80s0e;|)hXP +9-~RInP;w6v7h%y{%WqB66lzrYy~>8N1zz15;bBp>Yi;is~UWKv}UC1`2sM1z@B6hXmjw2Vu)Muo!IjkyP?_xEh{c1$_FRM?Xf6E*lM1BWSasQiz@zm4H}E~yx4<1@2Z;9b}bQa-dYO(-j| +iwpn`iVsMg*Rs*x>PMwe_cooe0B7uy=FH~|Al*xoLeJyfkl*5dZINms5YDPOZVO(UU0$(KA|2WY4k +XxRYbMKbt7&7P<;K60xgeHr3qS(wBQDMG0GayGW^g43EiK`aMoK=_=@TrVmqD>|0(!ZFvnhQFmZy_3U +UPZgO?3A8niCRbn*{jL3i-pd`6LJgOTP`PLoaM4JPsLST73|Tq26&q{(+ohai6{^vSadbxp$iV-aJ2` +B95jGrFawG4)&td!*Z}LNXTZrZ8)$maz#Z6Ar@5*wNMjRboOZ?<9mUlKo?18$I5f`G}aNSb_`NwBy?GpC^!IE0NwsSI{Zy<`925Z +Jp6(xVQlKEJ2|a$ERbTr6n~@~tK0GXgj}WSA=s`5HvfhcW-X444wE +*$~tl&=xloIGzNvXw#gUCV~Wc^j7{hjw7gD?Vu?$5_`iriK$4wj@;b;Htx7cA>GBzHtHVU5gL1WyiMe +LE2V>#gg%7^vgpd#A^gMt4#;gy1x_3ZwUl9ql3Q?kjqJ&K@({AQNDL41ojsX2wv}ap`>SpiIESpg+GX +uV2o=5{j=FF>Rq%|s*I@l1Mh-%g_sIqrAV#4MyeoiklUVSfJ*I`?7YVN#tW>IiL%y2E|S +L7bpO#x4TyP}8S}mgecWs(aa47y2TG35YGFoH#SZwwmzYV`V3uf$bMEa-yl`?1(1{9z#k +@6AUzL7er(Pa}?#)(|jyA?11lf%F6?yj|U0N0J#anvusU7kO%Rh{L^*t(SrXp9>24Wx`111EY(kbHy~ +&@ovAM&G(nLmJ>6Smp}xdpu*8-(7rorg|FB9lF?v?dBtE!%BX{@be?JZsc{oGFJFC0Xo3|5|NY(l7I88(;Q_GtYyS;VTE{Vw|t)lIBZ2pF(W?86@|prduzjGb1q*6e_?_%T +>0kR9Eo9zRo14Q62T(zG7d9LeTDT91Y9$wb=ZdH9M5#;1dWXBh6InZd9@Y;886tZinp*b6qIQGp=Gx= +8X8?@ElO6P;*}3=(!*DPgol6)!sT0JH6t4K~dqEa+RMITJEOvFV-X#ua9CP0ynM+Z>lM940 +RD-6_#RwIHsyj+-Qa?tpAY5X-KlMF3Pj&B5EuOLdOUX=H-g%g4ka)VZ}FqA0^B*)0(GO> +;)n41y(=axDgy6;3Fi-yU5gVNJDfrv;v3yWl+KpEke3p5v@TjNou+2Mq7G=nz84|=s32y_7X`Gka7&? +7kK@#7Sf|?sbn*#_VZqX@Fpd7+I#Z+9wU|Y#VK*zxR2=c*>#|2$JK9pO$e54CszQ?gIN$ScXwBl=gop +s*{cpr{({sYz(U@KPkTQ^&4Q}*N9DG~>M3Z$tZQU4QXz)EbGV*V5uMetezUzv3Dugfxu0Ow()*6&9Qk +0IE&2J;^{N%m_E+qyMl_uruI?;T>wlwTqNI#M|NW-5Fw@2TLjI8}`dLlKd +s(A)J9YP(?<~U<3OA(m)^IROj4OWn83vfp~3oH1z6^+BssF9Gf^Yg>u5Na@6aWAK2mo!S{#yKdYEi%f006`Y001 +8V003}la4%nJZggdGZeeUMV{dJ3VQyq|FJowBV{0yOd9_wsYuhjse)q391PV4}X3v4`CG@Rq&~AMRK` +8dIqg9qXT~hMzcjT+mIPFU(0+DsD-*-Ma5lyR&1F7p;s5+^PZa}xL<;Eh2-uTR=N1ODvSZW^x2F=X36-U_jfJ8vndL_3DuKR*6^T*K4v?e_lZX(_Tn$4l<`7*V5RC05)^$55Sw(W*h^ +mO^!lhC88^&5I)zFJp={0b1P=YkBd)Yh)JPIy&~s`ZhRYIUF104r6x~DpE0mC1r5SmU{3`NGh+JmO42 +IDcqSkUfUSXRO)tuM%ZEuc{PGM(cZP*B}o_}8u&I&GpCFX-C4?*;tevEB}r09ZY^vBxM57`@SSZsfUD +K&8I7pUFhPO=t%V~$A&ej-LWwP^5>+A4<`kK9DMYQbL1yr;7xaCi@lapCjK|LGE#;zv;1AupYmKg9N7 +4&J+E0mSzF@pcEy`*QgLyMZat++n^dyCKJL}9ogD>ByX}1}&0>ndwplafUQLTfe@*hqz&89>|dx%{Vc +2!AHIC=;v-byEp_H%S3qA(xoH=S&YQoA&VbwllGmXij|+%qdFyAcQ9QI%%v#Y}UdHx6^pSu$-aZGcGo +gN5saL>!NVY%ZctNDOO57$X{Xap*oDd@kZbfk{c{DE(#Z<_b%E1Fks>YjtOVK7$m)ehsfjQ5->7P}@h%EHqN+qU-#E8AJ81h?Nh1l(R919`@qqglAlKZkxwHCs_mvg5{0w&{VSz&`%e>~!Z% +0rPR^K{g|UshZfgAq0(;RVzU1qDt_Ge4P0nu%ny@?4X)C#-O3ea8L-E}3*BfX?k3}>y5>buU4hU2HqU +#{WFHbtS3DltTM)g1FlL4j0clcQS&~PyN1yw8)OXPY*7c*NbF#c8e2T)4`1QY-O00;nWrv6$8(q8hL1 +polD5dZ)r0001RX>c!JX>N37a&BR4FJo_QZDDR?b1!3WZE$R5bZKvHE^v9JSZ#0HHW2>qUvW^3h3YJ& +>9@I1vDR~e0BvK~?ux=OG)1BvF0!POl=I^Gzwby%)~nmj(}zSN?;hU!9nCgL%q55?(PP=Gq>u7}dr5D +_;|;`#Ogt_qqs93b1QwXpVcQg$5fWjpql`M_`Pv&wbe9jWNKT +1VWbOqBZf!*0?dNrBP@PJ(p@G(&DJ7cJigZyz3&nKy +Yh4KV!7WNaC1}RZakWNr0AV~`Thf@$<@St`+0s{*lz4xLjMw8y}1bJ_99Bd5KJiL-|r8vxqcL^7y`QT +(oll+zNtIxlz%cVG|?b?d2X5POtBygSQF)q#Y^iqdwEt`RKXInZa>B11wqSl4>1n}n(Vm2s-_}j^m#f +Rf0|pDrg@a3(@xnzm&>}jx{SqEuvkDJ^>*pj3(-K4O16cC%#H(L>;+yb<5e2@uw1?dc}Js)AZMU<6&2 +{bSS6mk8-VZOd5T9{I$)goGJLA=X|sXR|GhLc4F#9~+;TYFdyG0051zdB9pcrT2W5wlNP9wP@A&W3VEkEU)M$*Dy^r;ZIBIA1+v +FB`4TAoxiBIcbTa0E?bv;dAvnBs&w;KOC^2h`S)pJn#~q5rIE +Lx)|I52W9LE#P*)K;q*#@i^i@y~pdWfde!+s)l0fgg3uFHto8AKJ0)1Q#hco4tg86*$&+Fy6smhr>=W +H`m?Uk6#AS5M}tx3ceChv%mIRlKg_WJyBv>jFodiAr&;xt*lrsD~hdWJ;lz}QOv2!>9-V +{s=KAp7GrO0CvjxpY_YNZA>l4~n9u+0Ci+jEC%&!?)^20K}^=l`pi9%7oBtxnaf1M +Oth8dxXjR_TOI4UH*|AKFsgG1Mv77z +d|g03mj3FI`8q0TBbdgkqM42Qm8S$C3w8j%WH;J1v23)lZIXpSZvx?PWzrdiAt-{q(uo*tr`7hz^~c5 +womrP@EE)-(|}~)=CV511Y$*SP05YJQ+Yg*c+E&^aPp8+@A$*Nf*rY +;2qI%SBe-jyncm|+94EyhxMWp7q?`qtT@n29&0|XQR000O8ZKnQOX6zHCjbBdaA|NaUu +kZ1WpZv|Y%gPPZEaz0WOFZLZ*FF3XLWL6bZKvHE^v9h8f$Ocy7jw%1*f8j49L3eJ{s2-=+d+c*4q|Mb +N9hB1lgi&t+J$%QW7J}f8RNWq(thGG`;8ow6;W^_nUMo$|H-Sw5c1xqljh4suVSgi=wRKIx7n~o9TCX +x!-5S-aMAle3Q+#66KCdIZF{;S=Ci6Bsa+Zi1UW)wfa;64zu#6I0fuH&hvPi^O@RFWmOnuMa@MK=ix5 +T!fh;hv@4ItxJV4-xq6%ek-&T-0Vv_pfLwg}^65*!uCC9|FRrdeAmum4FY=f`!F@pSV$1BpZq4qheE`%t>*zTszav5qf7y!V|i#me0(;&x8?#zDV6|A)n*s +aKF9+gd9HT7(UXt`i3W9ASxexP@L6czCioMS$l&31V#rK=5X3SWQDmn635dGz^vT*P}0Sa)&G(182A+ +`?B!U*U}QUNu{7X1P>v7A((Xy<{KYe^wtji+6Q)!(C|Rmx3wpf*q)OCM&+n(u^l_jfN)~pt*qmX_E_2 +AXx$I(LT_=0s<8**?X=)ZqSPvNMG)X~NiumTn>=A#&XN*>n?o!UFD3%`>R0T>kQlh6+ou|HWgs#IsR?QZ +TYZa17y6pahx5kd^`4HuZp9Qr;GM87E<#9XZ4Xd;LoJagP9tVFe)IMZ(P9vdB+Fhp-w{UQ1HD`DMzAv +f!*t*<#U1E&}+}VS&nBEF?rGi-Af7zQB_JsNH0uZsME}@B*I7;s}91>dBG;R!Eg8Lo)EFqXv_=;svWqf<=B6neB;ifM>-`UWaYe)0b>tlu%Fv|4);_=MAt7x#g? +SzTZep3ad6t;NMG3OKklsB3~#6`)xz<|HPK{Vxhv8{i;3}Ra_rn0iR}107HzA1=uuz900H`rl-Lu)}K +d-Lflm=D4WGe5~)4Xmpo7BZ1EfW2$o*9`0#bOJJLQGb^w06gRmOCd4cmDYG%FiMwif$Ac=VrmiPdS9A +Sm5xGM*qS1Xq`(Z5hu1J44l!VhlpGb$ov*Ag<Cck%Y}>!&Zw8zQm;;wBauwi6QlAG#RGM +CDDE@Fa8r=N*sp+4D80^V*o^)DraokEt)JAsQxh_%6=nm=e8fR1~J!ldSo1aSMWtr2x!C-k%c?1ASCH +T===dQI7dJQekEtl?oSru2j252z1aVviB20QfDoo9>V{%mNMFlvZ?%6fvbmkmopBKbaF7;F|N9tkz~7 +iEo;%W;On*O)dNbqxVG5@lv_5NF1rX0!$QICUELytw^SV9_egafu0|-Um|Z256=+o>m1%k#AupSK*s^ +w=M!Q3HJEY#BX!iC|<~2L%VGJ83TZeyTn}HY)omNFtkTwI|56LAn`isRLo?0!EzM(7clxSH5>L5|Ijb +xeq1$)b(oj*eVroFLU(iskkVS>cZ^{WlNgLc-!w{(P6y?)4X&=^U}A34V^PZMMQOF75nTyX)t%`xoxK +Y;|y3;$hapbnTZc;Z{6=W}=e+jUT +ax-3=S14{}AEJ5(PF{VW$iN5_bnDa&G^eO5kVbIYyQ3TorGRwcc@UT*$BrqOTBX-1oq`d&DZpE?&@vT +a%Bl%ViLgZ;PHOClE2Z-Qyp4sS3pI?p)#z`H{fAF~d-I_Kv +D&O@psUNT7ad4j3?r`Dx-DHo->?VA-l=j;^j9{ +yIdQNvSG66=^IYqeJ$NvJ|r9t?wURkMf7`3{LkEB@;<_Jdb*>(~GJx>7PpM41FdoShcB@7)FDoZb>4g +7JX0NS=#4^@LF0c#|%AqBneLPH6)8ml^phL(LmZmF~{mp7EhZCd}7RDsLlh;XtMj;(3067sK)mQ-mA= +L*pIn2c|@n;3jc$gbc1S!PoQE8~V6kx4N$X0q=QXE+%AxLBU&(dn%|eFSC}%8FH-2|TkCC8U06L{l!h +Sy6Sk_PN=VV$?*_k99Rf3YbMA7(_t`VFc;a_JUp3Dz%k2?IdoUDHAwWI;cx0b%+?4!$k{8U!qEIiGrx +M>)0YlN+%Q10ZsuJ6~U*nk+2KZFv;`-aY>O#6y}2zmZBFSMZz_bt?-n)yK(VH5*nexI-?3MIBu^36OC +cn9pg372eeB@S&<+E8~ZpK(5tm)oW-6iSSmE`(3wFo+{VBpgo*?Ww^a9I3L3`hT8xJPUa#~W#l6cZQ# +}4wSZ%*YgDH)4Mdy(OE%B&9tr^+orZDwf=4mTqmxrzbt%eeC+z(0>Jc>*F)^h69#c2}_yq8fa89_QQP +%pYCPKx^$kiA&Nk!9>~)i%Ufhg^t0{dg77Hy6V*1dZ5qwN|&LJa5X`DI)8V;H{jFNkL^d+2a8JdJV95PPbh+$Ryp_Q&ou)d@D8 ++h-F*q#Lv7l%ALc2c)`9NGIO(7oYi_3Z|9p^4$|PA8urY@WuAS*Q`5K(dx$PmdTplp0ASA&;*HatDIl +quJZE_G#3fE@cR2jlKtFs;@raRcazu!31@j6-dSVS8-3`Kb-aE%jq+P?$;CH?! +b57qFtQletLGV$UQJmqx8-K&>+wA6<(taP8r?-Lm$ifUD*`L)Nz9zPI-vjkLRs!4P4B+4R_Smr)|lAN +69?-te&R$=;=N+H$hh|9;|%r1vq|Z_*)Kk&qFoK;nRa>#iQK-sbK8IUSRH-sTe_9w8R5hipV#`$;pU+ +=Xu5HUg{Ww8%n(IwND85rMjgO+5P~deskQyZY)z`-(Z?_vZEeRk()XD%L?c^syNC$D0;;Gy!Sw*DfnH +*cc_UGGANQ>y)PcZk(;sf%x|Uk?#*vMApe=B?p`m0F8eO~h{1sAxqBuuJ#?ER+}33VL%y}qvgb%6K2g +J7uz#YFcIY=L#Qq7`1$5mhZufHon&rW+h`iUW_I{9J7KZUUGA98xe-Mxt=EptbM}B5?bl%di3Mz(-pTH3B)&f0Xg1nw~9jFcQ@b$7d4P +&{?_>tR)aE?QUoaWw7n+CXi3$kg@mbCGGNNB#FZiYK?Px>48v15ir?1QY-O00;nWrv6$aZx9Ik2mk<; +8UO$v0001RX>c!JX>N37a&BR4FJo_QZDDR?b1!6NVs&ROaCx0s-EZ5v6@T|%!Rex?1h_(r`!u62&?PB +$fv&f^Nw$Y9AZUrUxk{o&Qb~<0|NGA2heXPD8!Qj8OrG=O`96p8R>_u7x*g1*IHg3iy;O#tl`tQ-A;77@Kx&Yqz`(rMO4|Fo+-@(=08|7aKC2`J=bC5Z?Ya5egjM6CLo91kKJJup%;BgA9 +z`(yT^+bC+}Cp~6KG&)**8uNQ#)>Ge=vIx>F_;bjm=(aM?Ug;HirL! +eLPS2F!Lb_0pvEgVqYw-L>ctBpJLlEXq?jF!LD;cv*poxt-+ug?t?#8At~~8}eB9yb{|321z42uFHt}W +25DgDBj8k$nov~#H`|^S9~kJ6RC(Ew(tZglZ&ouh8j^YX+d9IOh^_O%z8JjgjGm9B|zy?_l+=lMnHpi +#ogO`vITgg08^7~38tzo3fM<%Nw!{RxPeO}=KW&f#J*9i)4<;XiIF60ZKcf6ut94yu#-=5sNrrQcM>^ +C-~<#Uk;6(5+=sd0kSXR;ct#KB(X)+v>(r@WlmAA6`itMNu}&)1kqsw9-*Z(lZI7KiYNgQ<=)fQvqOx +>KMCXB-qi;|E9w9diFks&>jFvYTBh*Jf1jNUii@$=5V1?dD*+BKvgjHr>O>-c!gMahAg`4k~Ng9f`sZde1+uyhU9J@E(Ltlt(RocAqwiW#Hw2yhvT< +{0?*iuZ>6W(BxXN~$<<{s{@tfHBEyqXuoXk&3@25M5g)8vOJXvmSWF1?wA*NilTn0SMU!{0!mu)ixvY +Yx!q_{f#r#2q;z~=L!2x$7YrXOO%ghibamRC;6`sv2UY#6xjgdZL$j!nnz*mOHhD|!ufP0!)Fr0jtD& +b!klmh54pG=9pp$bK1W$7I2#Wt7HQyF8^E*`P-0)*sDt$k6zTC0NTZr>hOTaS9U#zblg*jd9A^f*SHf +^78avu)drV?KXxd)csD0;mQp22N_GK+pr11z<+)t5N3vB=b*s+6B{Hnxeu#N1h6rXus=TS2XYpYo|n)TT%%U}Q?X3u@u7^OlF^$JQbq?S?4nj7^nIAVfbEjuGPOEqu;_ +7k@|2dd=IIA?FdSj_7(oNhm{E=G1+!jaI|fQ&`~a6-9K7~bl*hg(@`%e7`ZTePw%LW0erxP4*nB#uN) +QmoXia}n}n*Q!nO+s+mfh;uA}W;B|QZ4bO8gNFK(oj{1XikkrhAJe5s8H(iFks0Q;o^08Gw#mgHPniq +yr}d0r4p#ghwz>HI%csx3UEHh^B?lM?p@dG8&mQCI*fiS=%Q#bY` +CAm~nY&bEBFe+XO828x0RvpJe!X47=;mY-hr3xYe3Y0K73n}>^>QH@f#)QUYQ7!NMT +t`560*vFW>qh3Out0{)VIq46Vu;DyZ<41uJw~SM!+K|FFYJ9$*!lqK0!stu<74ss!Yk_)j?YYagH>VJ{#SSRNhoODPowxT6l-(4(jYO>T*9#ooZ|6JEibwjlFt>7+0g=|0x-%(2I6J +Hl*Ch_Eo7xJu>JrMoESfwG84FtkAVZz;Om5u?AW2BYN?#?qaCwUztNVw|n)XQ)y*!S3uFg#`fLN{nRo +VHjtY5NofXVC~3zpUoe_hS+t`HQ+AxpFt?w1|CAT+I@^fR@c5W^7)Jg42c=6pFF6gO +g+GX4NF$r+=_8fGl3Hu9SZZ(gV*mo+J2WUkyq>UU?cpqC +UMzp&AQTb`hQT`FWe#6w45!|sE>2>;#vck^`v&6P$O81NT^*<98593;_4nJW>ziwO@sa-J_RHnxD*%K +0v%gQH-Y1F9z+K$_@%e{PGlDZPCw>j=d71k?wCtpI=Oyi;`zis+mA=*XwSEn@H@r-08m +Q<1QY-O00;nWrv6&l*H{qV1pokn6aWAs0001RX>c!JX>N37a&BR4FJo_QZDDR?b1!6XcW!KNVPr0Fd8 +JreZ`(E$e)q2+JQR}wTiYIn0YMcENH<_WF%(PJmmpwi>DXpNkt#_!b$|U1DM})3IjubdzKG{Sp6^^~U +N=fxBGh)v<#xuqM`dOkt!kng+c0f_g!KNmns=8*f>V^M-&n?$bgS2-YWl5g7-O*A2lWlWOm9fmW2DMc%9OVArySqQ`@25mm1Dg3 +$=j)b>s{5o?v+@aaufSQ55T;b25#A6k@eD!?$nBtfq5`(8JWvC+Z4eCl0+_Ak!sInKWv>FHX*JM_>m` +v&*G%xQKpSRvh>7MFsA_G~+Vk2h)QywxHNMB^HwbTOl|Ia7E>zIkqK}yz4Sws_GD;<@fzrinHY)`)hP ++YFQYcmhw`8ZDTHMSCzUK3}|Lz4ygV?bx`o*2|X6A`2^6v2NFOoUeE4y=;%L=}LS&-J?QHUPZ6VpJ+H +ypIdgYbvDd(vq22`wS57Q^~mRWdP9s$eRIBL<~Wf2}MKqgq`8VY=M$y<_uyh$UgdC}P^i0cS`RjB!Wv +aL=OyqGeownWI~Fyxh6{_q+nHib(>@9LO^}5UYYI2f~n52|yq3xmMDfiR3j5J3*BXaQYvuK@ibdJ-&p +z!i~1%Aq3SUc|8f*EqMqYSb2L~P2~-;+@@_=D(HdT5p;m~#`R=OxY!TYM7S*az$okG{ +X4FUIzA6wF~rgbIOtuUi=SejreB%htjT0AS>g7AF+sJos@M4~s^S4bCke-E7jw*2g7Tw>UWLfUMOs2o +vd38^0cnY4?C*DGtc}Y=v}YUAx6$*dFqeGxsNQUVnI;jHNEeE3_Z;6cOO1PrpEp{MZY`S%GTPu{N9H8 +l5`M!)F*H_Zx$B+`xO;g?dOV8b}jIOmYH6Pwz3KP%pD_!KeFuLa_^3NjqYi6G1}}zK@f^pREYrDKR-o +kvyf0({;0WxvWkmUwSDDMuNLy;6)3+zS!*Kij>snp+dTFkMADYQPC4%p^(ZBae|%4r(tT^6CyfR)}zw +d%{@4T;9UgauL_r7lXju)CXS6{NOLJYWco6Ac-i}Abp4ALyljwndqrF0=z(QvMAHXZv_I_`QiS|PF0@ +jvivic=_Xk(vr6PH{APPG;ihxS$#V8kxsRT{g-C%)lO%2{;i$C$Y1u4TG^jabDP1cm~Yq +ywU*&|%^Kfc?-7xDd{Y1c#2&02%lxRkPDsG(ubKiE??YTyqYJB8jw}iZ#Gd4^6+CqyP-A4P4-= +Y#3S1#Mm#xP{MS)7zi-PD6MfUVSz{C-uiW1mb286ADK4{-ut-A0wptGVv;7B9O9KQH000080BxrJTK- +mFO7H^!02>Sd03ZMW0B~t=FJEbHbY*gGVQepBZ*6U1Ze(*WW^!d^dSxzfd97AWkJ~m7z57=X0t(2eEu +`&jQT4J-fLxNpCcSJI0xgYYb}5qKkWRO0{(Fa{Bulc5Zo1vW8sdEN&Ew37&S>ixKH+L_b;q=EhFb(Cl +HfNkJ>V({jW9(elmn}{ENUqRt-5Q-P-ul$6pI~4sA}DHTs4E$ZTh@sf{r_nWYho-CU+liZ(rZtT{6)I +ERQ)9dkgT_c!l(J$;3M$v7oFBcY`*%Mv7f!9$UKgs)0SL@SweHD)h!!K&*CN(C3cVkDwaN*KGR>+i5L +tR*e2EIB!)zv^auAN{9mPHT#+6KW*3#KduM24IE>bH{p +5v(4wOT-*S6!XNa02;Zh=Wki!8~2vzoZFC2ycmIB^7qR2UL!+kh>`&H07_3MJu9Q}u!zLGEK=-f0CVSi^BabPsx1oDA|&Bc`{t*=BLFoglFg62V=WO_5qC +@H)gOGj0sk`OJFHy>#XM!~DDh$B*2W*41{(E>Q4ubtd)AlC55f&@o!k8nbNMu^b&qaUp{Tm-{${`#<;o9=a*V?}6470rc1zWxt@P$n1gNVBLq7ym0N*XZ(V;s8Dh6 +CNwQ-)Z-aZ#g1$bAcZ0%h#+(Jvk54p!h=zl}`rKm>b9S$rfRLS=U=vt<~P-uQqf4>(l75Ud55K>6;?n^sCo~#$!ugA!u9>;!vU +$W-^mP?aQ!1{YW(HkwQ+U&G2Qv<=rj7i>2no&jic*0QF-vKDGhvUOc +fOni2*I0;|&v<^-FJr*d#U9a(dl#AaS;Y9ym_Jeq5THZoTVCv%;UHIDM|HJ1z2lHh1YmEoZ3XtCX#!B +>WV^W){4C>PL6Hog6Z`iT_0Z>Z=1QY-O00;nWrv6$iZ4yx00 +ssJy1^@sa0001RX>c!JX>N37a&BR4FJo_QZDDR?b1!INb7(Gbd3{w;Z<{a>e&<(MNR>b$)}EtLsj9Md +($q>x)|alb95}$Mv6*e!P1FAS&IUqA+wlVS-F?3MKHnkQP8&xWZW}2MkrH924g2W%7k)Y6qmG+f*!a9R_M0ns+>Zuhvj=A)^XQc3@X7+A)D>?Zkv(ax9jW8ZueGX?}W +6}7cobDy1KvHe%Wu>&DHJ(qxajp+s|RK`42`bY%w7i;3X@XUOm~2ycpqt>E3m{iy}`D7?Mv+(Hh*>u$ +M5VcngfBQ4|%DTT5;*ud(c8e5dP(;1kDjc;ha@0^vajqAUE8y5N#@ym;a@T1?T1XP{B=-(#IX3a-cjN +N=GeFTyoswF==vVYq>xy)e*%a+Xv=0&$v@8uvTkjvpihB%zRTiG?5Y2_L=nhvp={=!~w(0}3y+vXA7W +hq@$`(jjwED{X*{r?F>H<8hJq$o4U@c|IMu5bEtWvVNB)3|@K!Sh#HZ9uVXXzY}96dsMD;{TOJ1Z` +0joLKrnF%kD%j5T~MWaOlHeXzer3){726=29R`?3M;=OeF@{%oF8jxKcqf&#W$3(miDA}4I0bc1pe`8 +u{({i^gHk6lv&eK^g)2kp;Mn=)rW1nYu3u|0SVc +mmu<}nC~4K(X0Dx$O-{1!=`=F=IBVd07;I9jmhOE6|xlmXWpHMD+ow9F*tQ*!jUcl1Iwv2Wba2(L0ZZ +R9r?zp(4?YG|Y!zb`Cm;K8!_@Qg5TX1Zd&8Wi^v-kUdXhL +Us(+BhS~BFNTga_~bm9oir$`=F5~&7^WjN;GNS$n+vCX1Q)#p$ZQ$bcarZnRBsIXMjehs1(raaiY+$fuD7z6y`MbXfuM?CjkNA$9i0<+dWc->*^C*DvUwi=`w1??SgRA9keKvW5xl+b +Q=vMB>%t5za$Rf5yJGRmMUh>WC8H{o;C}!XmFD{j+0VDyrfw_ns~vtvHl-WO9KQH000080BxrJTB>8q +TQ>#(0DBYw03QGV0B~t=FJEbHbY*gGVQepBZ*6U1Ze(*WX>Md?crI{xl~`?y+c*&Zo?oGJDcIne_G>s +7%CL+L9~TyA9j@?>Bn26gx{dAL3Y=8O=QNs_X>sP*v^5VHBjQ5bb&{280>o10E +=6a<#H+-z8`6cgy#C%J!?BVy}7Il3EiN__lU3z6?a*+m;w2-VZ$%lDOS}aXS)e_J(5*!raaWcFfTQZd +=W4D0-}DU +9KG(ywCCVu$MaRTbRPLx6N7;}%9`~_2rhE;F*xLs51;wSnjBrxUhgkSqza(Ih5{E(>{rU}F5KdIKI2n +T?bNGhzA~$y_2Lq92Pn4+G#h_aFUZcDbmNwAK`!xU5B^x~CXTjFJ8c;Mm=RFG +jP0#Y|-Wr+!3h4VTpeXcwRI*^KwLq86ymC7zap>%0hfC^vrT3S^McaW&rYPG6cETxG|>%DXqrrBDDNR +lM_`Gv_IFhMFbM5n_9;d@MVt|6nU0Uc?_5LtMB{e$iqhe81vGE#U#O5TH>-^57t_9zu=>CaQ2*S+9-W +OG%bKkWhUW5EsCA>1^TP0bX1y35d;_vn%{vi8EJ{q_D6Qx;%U&b} +;_#&`s=U6GhwJY3-2%k4aq1ki4?^I +3h!1?52ij#XVOt6Z6>bN}kCuq4!~GO)4XuX_6u+qjZ8oM?<*0?KsP?woT!-a$I|Zg7&_Nk`jBHL^)F* +4Bg$h=mZ< +|nC`Y;5AT|GB%w|b4T@Q8I}yD4G?Q4J%G7LCj$Aq|Wf_Ar#bmKhp2>WNU5sYvoUOOBG_JEs5#luXj3! +O>Y3(`V6}^^I=Blgakv(peOOq1&9XO-&{T-1h`(*8#dn?kpb;4yrd+6I(3RXAs92jzHOs;o7Fwp~jJ$ +ckAt>wrSA5<032(c?MrnSR;PNaJ%Zp5g%#Q?MU0EihFSyW!i-DX)p$4y6>%n9yo1A^?Dt3SM*Eg6!=3 +uCYI%byhgX2w@W-a3!=$k!Yyn|7lPmcJm?#qojt(Sg2PCqHX$IDmv!Aic19}}r9qh7RnzoS#)D5$>%& +rujZfv$KBGjuQe7Qt4}YkRnB9@4{ESsZbzU +;1Yd3muB*zk;8=m3l}Ai}(iUuVrH1g1QBkBOroEu*)Y!YXf`FY3Qhyy3>PU6^&C@V11diZDZel;~Yt0 +0gEG72({TCjapfF;!V1Sk$CPcSk5CP;c5JQi%ex#*VNd3Gj~R-g(=P_ER6Rj{V0-#Br&LKG5M{dX6nD +Hu}8?Rn%Oyh%B?2SVr9E%98t>q}05B*rQ5$T9jVocki6#IDdYOJZ)0Zs|%R5zd3L0jdlxA%nh!=?X-c +c@b_TooYSgmQ3k6*J#+^9O_^%9WzXqj`+kt=>zNL(&^77!JwZWjm=XEWf60n~KR=jD_quGV_rOf3$+e|h@v=p`QHm;s335Y&^A-4gCefUXU*6o6FOFrMCGk=lM?5W^u)K}#XnVO70uZ7&@@iv +hqp8WO?rJ^%r3E!h0;f3?^Bj!=joFV{vUv;20cC{g@HDm2Jbo6Sr!X^?!Tzig0JDtR{sM~O9KQH0000 +80BxrJTBDHp_(TK%0BH;W03iSX0B~t=FJEbHbY*gGVQepBZ*6U1Ze(*WX>N0HWn*+MaCxOx-*4MC5Pt +VxaZoW#0vus`HQ*In+5sJgWQa6oI&E +{Zq3%YY1Hx@w*%73}+(GEIy>IfG#zpK+`kawah*$b*#o3i88GxzSwmYZiZHo|K>@LoEG>O~l>T2$`R+ +KB2H4LT!~V_&RRQ<$h@WMHh&im{m!!lO}KmX#C{S+zBlL`ZuDSUz!!tkP}ERUNHvho9F>tmCe?WK?6S +@BZ`c?fd)tHIo({uesw9`UgdPC#*B#srRXEp}}j7NlpkUZAp1{+(k#$)lxjM?zvg5ZZPSMOi7aMkvYv +sw%Pc(qk(-SKuKBr9b`FUinqvEvDs`Y$*qMC%63?}u`&7hS?o4Ie_57=&k+Rfh)`)tLpjg~;PB7M9kq +g4iQv248C~}k62M@{Pbm27{_Z0jv}w63lk@OJG)fy}2EX=#K8Z3fl>8^tV^H^&H%Nk*48Tw1zk!Ew{* +TEKQ4x4uvr&;-l!pRteuIx%;cher@ZysA<8ii}4Dy5s>D}B&U2&Oh7h+y{=svT~=j1h+oAiMN5pylaCDB`k|NV5nJ7j3zO +7gK7*k9VfGXfEouZlA3v{&&Z+J0(w`d)M_Yl><|S)H@&c#tMgJ_Cs||Tcq?YW&R!I#hGE)o2I0IXxwP +Y8l(Fz|7E-MT}(tYy$S@}!WdyU?%XYK)6g#;@PU@aW1uKK?untZw5lUk&nzVu6Cc%6rZpify^^zy6M> +~SE=uZW~*L=i~dn1bN94W1mI0x9IZOR$FD?IN?2fTheVj3E2*^U(cwM(P +Cties(_2aH(cOJ%o{k;{DF7Mdfn;qwwn~G}2QlH1zSz&8)NJXyx?f#nR#pe7bvg2aY%PEm#_6i*FO)d +-^ur#M~d0{m_c3H3nU8RJuipWXmE3x_Qy=;uZyxd6cKz)*t!D-ON4lajCoY3$1=|u +Hw-5j33bp(xLxVQ8^Rg7HU5;9w74A(yYLlh_o<)#Qhf1WUzOCnX5Js<+^>UhoyIu__cNcv87jaEMQ2I +SfNj{L6iJ(U*dfzGiP@L4*A;W3zJx+1Ndk$(95+T-o}RJU57`9F9)rtNa~$S0e5AFwJ7d-Rwx=R-9?+ +9lV#BKwU@aVflBpfGT$o2SqdWS=o5z8~;;sS~Zm8btp01XEtp(pMP=QHay_^4klV6Q__Vkhq3KV!ke% +6BoRCG%y|F6foukV-QIdvn2%*2T(iLG6M9L_W=bV<6ApigXaA|NaUukZ1WpZv|Y%gPPZEaz0WOFZR +ZgX^DY-}!YdCfg*bKEwP-}Ni7bh*UYouT5?CAX#8Nu6UW8}E5-m1QTlrKlJViIEs{$O#{k=e)k(e*FM +IfCNVmyY_A>6>CVK(dce8`UT9ot~Mf$*InDyGLA*Q*;aKclCrGYq|K|cIXFmHO{$E9< +1KYV*C(e>GQKn-}I=-pYeD4p?p5ZBjQ<4|n$N^z_Zi*;)MK$;;EZ_$etm*>KZs(q0w$O1C_PuLtT)zK +!B*S!PuoZIkqUa+#Nx5f0es)@f2!Wu7KQ{@*e#lZ_nrtmW@8R@+4KLn^m4W8M2jc?~_Y)4KYwd-vw0r +*EEK>BieSFWZ>Na_~r;B$u)-cKT%_i*;OeZI-k$i_ +Mr^9AE2Oxox*`)RO)i;iMdW0OKZauc#{k2G*`=$o48}4;zsbwM?>|L2E9m8Xgwa4U8`m?0qF=fm4+qWZJb-B=st9>!jX^w92 +HYN*J$cp^sSY)GQcx50;S@B;o;(Th?V#M85fmN208LPmaY8|Mt}ri*cq?nm|L%o}UrRXZXb1w8)J-ZL +*4GWTIizx6pky$$J^MNqs5Xc)M$_KtLw?f+)s7`%O}2x^s9S;PVD#G_U1GmhCfsKd0x&n>a0GQg+++e$?Nb>YLbeOG3VhJGgmJurCWs>gVMz- +A+WII-pKfz+s%n>UWv+`cNdj9%nul#ZIbzaD3*R*n@yR@K(;+r~e6HribmU}77or~%c_XLY4FX4%w7u +;i$H>pbjo~iu6&5B(N!Vk(_tw32exNia${GAqgzJ(2zoBXhW|iaX4Orj7 +IjjWalVf0u8fl=uB%GX9Oo=i(QUhSFNuLzTSQ4urRa?SN>gM2}Al8Xx&&DvOJw1+R2L}gfku;5#SEZx +EvErN^6V^cx*zzsdegO4|XdfTYyM;L2oxTtus^bc`W>`=e>|@PLbPbAC-9s16R;IvHV0)xG|Mct(IsY +4wU37I}p1%Z5SC}`M;!7?E4PYk)&>%RXsk%DFoin|Hg$&JDs9+mTil!1xw?#rWdbuNJMEtREG%AFmQMBrX@{`YWHYe;g*MBebCf6@4z2kEV}Jwoq%TqH7Z3Pzubt==OBs(vKLEy5#wh2#WaTzDG6(fbu}zfbQrHCi&>6A5i +)nda6<%o5sQM)nkH->XvqWtR1PIY=cv54}R7~iR-DDu@@z@{U5Q@O3Sivq}0_$8OsVFG0lE_v4rifiw&5l9(Z85zzQfulh!$q(Lw`vG_;-@W%mNsfN+`e;`4w~>ydmGm?QjO#k}SO6?Hrz +2@ik$RdeOnU%k5ou5mKaaV%<0z<>Q!FpqUm4hJjwL4#uC0hN0g!~SW~BWv)~5u{BCRosGZNRalG#yyfwU2n=F!4yCGRmT(x7sCW- +sU(gEhM1RtbK!Lw6e8r&5oEGZ~N`rsPfo-=qE} +kpz3TBIV|p4x_mnIR*PL`l5mhhE6IMwNw=J$}P2tr`p_2Q3P+Kam8$13)`61)jOi0&UH+JQ(^Y$%TJnYL&Uz#c;?{roQrn)SUup2Bn9fzp4~Ofb>1V3%Q`(~l3F7UgUhR6GIdHU_HMBP8CH +t#H2fYpSkoV3ml|r*D7!Ja1A@-=q>hXX42U5R0ck-Dlzi2{w%CJrKaQ!0tEOflAHGq~6_IC4qVLz^+^ +JG9TDkZ6Wa$)ot8==;{*opC(=<^}R>NcnO7dnEQ_IqkIbW7E3!?NYxGeCBCeyZtIGgy+slPYU7Fl;YQ +^^_4JzN_n5txb?XlJ)!f2vcAsva9nj?Z0q@GdN+4BV5U+mu;U^1Y6uT9!*%6gJ|kapM^mpqq=r8llZBV9Oz_M2ZbaaYELK +X9dZufkbocR#Lr=1t-VL$8I{7=PPCnZzL6jt_U1^DFuL>B+fY9jgU +B&JQo@-4DDO6bhHl^QG?+icH7DH1yz!(pkgf#kwO?3&M9c5(&}UuNDCZxKKZY=CvRRoefjbqi@!g8_4 +KbN&slOYv=7z~${h$F2>sJin0hQhLFT4%rU_PonjhyU5JI2t6h>9i3)rf`rOh!88d(}%lN2j9s#rxX? +6N9Q{jTys-iBI8RC?4AoS_z+Io<7+CR&fvL+P4q=?dfCrdDKp)OKbs0e4z=AjN1%B*h)vnSOY8^-Pr+ +h1Sv-#^$ipqOBGxKC?iJCGf6m?&l8Z0XArFUiBD^Ts~L|ZH$$nb$VqTH$XHA4lc_N_F3S>M +C-cdQtV=PY@6j0eVj9*PHK?9~pYkH(O=d#n=(cM@rL0Ni)$8FpT;BqXJZc`)G%aNSB4cX(D8PyIif%4MCcsDj#p2#w`8fG=&_f^!-y1sLgsthS_>J34+suuM9Hebf5UMjS9 +gpSEq)$}^`<=*)p|X8rQuW1squlwWjyLe%UX5K8)$YbVm2P7yPY488hSR(Q5*aOZ)r3tG~&2fBUWHQs +NFc}(~R@J7y;8_dD+QmY@ApU#P~j|CEJfYtwCm->)nj)quMYfd`rVrATC{b4BvyLu8TT?*}kr3kcgCU0zWIau+7*FJyUEtFpRI5#SrcAU^&D2H-2@Sh6V%B8 +&{mlYe$?#x&k{V4+$h<+m;#4l_<*SmowEProoQE_VW&>%N<`dl%5u36_U{Z@rhXMHT4Z{x;$RRGb51aPV3aP-=-nU;63r0~OrUEc1 +NL?-jqZ8;k(8uIvuwB?6mdTL59^&j`1*{yW8*3i=W`wP)%XebT!sd+Kk^`Q#c(8o=gn*M|5p7{>^gXQ +pnpG2Hma_Ga4#?3*?DF*=Pu&#QOoC^}2(-rzNP-G-TxGZG6N}4<+Kp_jmpsBrrLCb;!YPpNlA7lALeW +INlRe=v?I_nN+=jqMFeb`zS_RwYaX)QWAvGg2R3I}s>>cL}hWffD$ChHyy#1b6MMKDH$SY^&VhNz2|+ +%JNmGH?lP|4n~@LY_(ChYZYWD~(|^LS-(38}JfknO0c*u?)I)z4&u5qnMu`D1H!3ly#eJI7AP2U=AOb +?egonKF^ahX`XXM_cWtaykk5nRco6AEU@}FWDbys_|5yUHYeK70}NXR7qG2c&y>Um7h`hbhc-I3&E19 +l9)Do?1Saa4#{}Q3lS%#w;wSi-3Uzes`SCDO=vE{ZNg`dwl*Ox^HzIe}^KYRu!qWLRR@o%aWc>gx=VK20_PftpB(NlIJEj&6f +&mvU+VNajZ|fv4K+&LHpa=d9#?0_Dj1$ltKt)gX6+euOwuH_v#w}dGc+c?;h+NJoWn*jp-h;n7ecdOn0atli0OJfJia>-RKt%FCtntQ6l=$iZCEH +X&#hCmV&6WXd+hICxP2YRyt5JIU`3{jHT!O%ZE*~!&WE6^v_DrlnEk;z5xyRD)d?C5)0nKx6_@}k32v +*&^SBO>BMquKZ`Fr03$uzMC|W6oqKTQu8h79_ +jGRJwqym<$Nq*V%$LI}8$yR@V{MT)VB6rSn>SoC_| +IoKcQJR8$rA%O><>9LA%Z+kGN)We%OKzrg&O{aSSzA*E@Vm;DIi!4>TZG+L0{DWaQshHZ77@6v-Z?)2 +g4a-Yx!51t)vw_e9af$=*t7>9;I*TF{{+^5Ay)$7wZn=k3AU;-M8ns98o!Z8ZK1W%#jWpv2({8d2i%>(;Y-siQ(;8=A^+ozF|&q9QTX)C{WzP857Erx*r0 +b4@)8@=wV?#ETW{?8Lr1c_{?+5OV|VSg(tAkYu~)M0L-EsCiF*B8Waf3bujl>tg4D#UeC=F{WUkCo%S +~Gl@EoHgH;9l@3zD8rxp1lczPQDwr)D%Fpgea78Lsg-b#v1b2S4( +w^`g#`)|NfDLec_(`ZRHEdfN2KnuV%q@zcjZycQ)3E+PFpQCu +yRgAK-|DtOV>rx+_;jbha7O3HizgQzRP{$!kc_=f7Gp#t_MmF(R6nv~whcf|Uu*&nq%p_&B +^FddY}k1nq1%L^X&cN;QxrcTsT=<9%TF7TjCn!d(^k6llPyzw)j!E|Fk_wh5yGW=Ln&zmAI-`|ny`1! +&VC2at<>@D<61fm)7MVGp;De2m31CAn>y6S6EZh?ew8dJ}WcWYfsB#6bHJKzI`Aj~QIf5k{^TfPGy`A +*iT-n6xpqki6Q)BX*9Dm=7;)Eoozn4vT{wRSXy(n?BQ%!dS=SvHU_&N#G!Oi)bKzK`!Qyy$Hmf4sJzu +&2O>%(IDG8AD&1RXGSJx`=zQHz{LUzx5pPV8`O-3iGk4Hf)@jbtFz%wkcF7+C?3Is}9EK{81p7s +q1rl2OPWTTPCn>EA%9elJQd=e(2r!&Hh_}Gtp{^a7*e8LuRavwXex(!T6Ke`BcX;0a2x2aK=-ZC +`(v8K0#zx|E$be4uAlg;#?Hpje;fr!g6fd2=R+l-|mNx2GaepnQNH*Pz8a*F^fTPDS$lwhET!7rtX>{ +3Up=RQEIV1H5!Luee6(rHpg4TC72qbmFoqiB;ab$ZGNl@ndwu4G!6ZJd{YFT!KyA6TvdrSm%HU^7L5@ +PGXy%FgxoJln`F^&kB!m80W-_%ZWH*Xgjw2jb*KhtfBoXAtyvUiC*{E4-a +4aj&jiB@JETq%P^f)5JOk&1><++9d&MM;;W9rzTmJW9y_~Z-Vc*$^hwTvNzQbVU`px_Q6ho0DB;;aq| +MR7+l1Oh!Y@#)D%>fGMt6JQnfdS2KS~}>0&0WQTyh^x=Pvd@dMyq$EU_Map_+=pg@KXovsxjUm3%5!f+>#zbHJW^>xcPrr#`L!d5b+W+R7RY3=jh|Cxsc^@5|S^E{sRu^BPnw=5>xYI!Z1?13~EVo%EA +j0eE+B=Jy9)R9z|Q1a=5)QEJ#mLoBJ_N<*etoT{BTqN@*7B1O4t-leYxbY#`G;`BulEEGq&_DjpCeZ( +72&3iJ3w=9gTqhPHmB%`7-=ZNFpb@E7QHH$o&hqA;iFvD{K6&~hK@BouDxSj>JrW=1xUOoN(ZrtT}qZlA`DWY*G|C_eIt3Vw|D9Csx2XXmPOrGL9ShpoFxEGE#1egrVsecKN}u`9$>MC4lygR!V) +Iu6;L-M?3K0ZhOk^Hk-q-qkvQAtMQcyJ%ye9ajFW_J0Fph2LQeS2`pKU!o?M7Ofs+SrmjT;>R5zr6*I +l75_cGVqlB=a!itvg4brX*w0gS@0XR4%JGrZRojiw+WAJOl{p}$FgL?=(Nn4u>ENu|1x2{FeSpl3Ql$ +tcjh$-Iy1JCpm>WR2VR=U-NHu?n25H=UE1_(hv=W&*v#mE#E|;$h +7zq>cFZk%}>8p$!a5xQ{H{7lP-ox-WBt?Q}Axc2wo-f8x)OQG)cpR?3AqR8J%aq{BTPfuSWGf!TSSvJkc3dF1ESDeD+i*|% +xaIcL4r7L2i7*gV~Qlr#CJp_u0ExoJELER4Bh^-h{I0z%BlSL;?>(@CX3n;^UEu=b_H$pT1!zINC!anfJV?EGbz>T^6EB;0^DNWyQw`VyoevylY62J-N2Sl-*^Fz3Lx0Gqr +D_0$0(`0`nd*g?nY-q@dwN>1sdhmk*a)Xj=1 +{*`LZCs{8IYJt}MF1iGD^{OTC4S=1;ss|1VHW0|XQR000O8ZKnQOX){rSd<_5q$}|7~9smFUaA|NaUu +kZ1WpZv|Y%gPPZEaz0WOFZUX>)WgaCzlg-EZSI5`Xt!L8vGq9UP&34d6aBm)$NdXcxD8Ee=7Tr7hZ4B +TFhtC2{xmf4>=iiIgZOyV>5$`602%;cz%#{LM&P-X2I%Y_DqJ?Q7cQ^uSK7yk} +g%0qxp1%{{Cuf7PQ)V!rsBSB+%6p>@8gE4SsLgu^MV#tHMPDcNJOX%7e5l*4W?_?owUcc~}lRtvCMFJ +O7e+%ez%>+E +pow(*AyLR+d*DC{1!$TN4JS;OW869O?6b-wL+G4wJy55-H9cKN;n^tblulfvL$)om1ITR%dQ6>s=f;2eX7$%mHTfi{HiU~jALogP`Jm7AIhK?>CMZ@6##4eMcD)u8df^5?*dHKLMeX91oAlPxc;5}g98 +UDHB>=Wy-{{zHDdhts|1JyX-YfE{4Kib=?~9dCE!#-yuT$EajmLd&u+6DdUTHY4Xhke}Oz4X%g7Iix$ +JgImEZNF9X|@4QWxqmgs#O`{pAlENwx5)UFl@(}?@M*CW>UeQ{xaB9!$Zj~tATkcNv9+&L1#qX!$_nm +BUhrdRA%l+$!Kb8T+x8C>Hh;O+Q@GeY2B7c$m^;G`4g&X~A3m@*Qc6zhbK?paU>}iehB=>L=?WrKUx? +R&6w#B5gBxOqgO{vf&$&{0(u8-%_w2+77*-LbF-UGmzoLk6M5}A0pxqoTuBhlBXJC2w5KzF$9Ib<&On +LaRps2o*V!2Q2ZvoBBxz>fcgqo~A=9?#A>U>iu<1hHMo?pfV-2$G1o(?m{wJlZ{$F<$}fKw8u+A`JmI +oC+ql{DixFzIwJTKTvr}Ls4&1Hi@pa%1fYN=nst;O>g#X1p%{4hk250Ib(k^{D$R6l08r78bc~{VO&r +PMChSyb{E8KKvE-8wj_F^C0w4W(8*)k2QWm=G~t=$xKX7Nf+O(f5|EPIt9^%ijA;FSUE{!>=jJm1f{P +OE1RRPP4qn1`p{tS`A`m=n$vP;I06$pTT_@Yc~jOHg}UC-h|K$jjpmI8)Nu|v4*Gt-OI@KW!tn ++0P>(xW*-U@k*$-z~kZv(H%;oHiRk?;etN%?ilCwgf3Qd$ze#2zY6NTwIuLMFbK|sMqldCuIw +11S&&?giMbkk8=BlT+=^~u1be|x@YwMsBKCPZQ0jY5DEAy46>@4dw_nroAJ;#XOc7wG5OAm5D46Un%m3iW+^C?k0fJRm;2eF=%Y>I~z +LNJ^hF8X3XG@QV@3RIFS9L~2l&omaq9YEt1*j=Vx7ch%y#avTjlxcwhmmNpit)^JEm~`Zf0iRBvfpm~G>MAy@h&;^LI;pWt=ozVSs#^gv3Z916H__HjmKtN)u +3ZUQJCpM6ufCUhYV96bMgzgJAc6Vakrtz1bpjE?OnlAS#2u}PeF`msvKOi4Fw$&(H;=KaU8Rr<#THLx +&E55kBb=Q_)gJ43292?{lqZE&)EW;vsDx>z +YIdhYGIy1IsN(L9;7B8^HJcPB7tMgUMwAz00m5X0u|7>18Wri2s^nk=DNL)pf^;KJoz_?vIptydo&2C +j??+Ae+fXIW6#ow@fy?wcItk+Huha|Vr{azsF*ZJq?w;}W2BbNDiCjEJWt05l2v~>nsXDsSsh=0q3~L +;}BK+hhW>+=9y^Tp15@37JNZEHa$mg}WmkPq*rAaD9NfjFNSi#sG16lOrCFK$L7znatBk-XKDd-U}lsm;Lt*TZDP+2>C^04L!=lcpS;U2xmo^LHxU8?`y4&I`aqXNSek +fs1}SPO3{TP22Gpo^YWuzFN}8YZsJ^fIwkiKm1qTtV-;h&}%t-Tx{erc~rVqhj2CuvP)9l@;0?BSp)b +v^^j$}#9$*CAXV;liIZB)3@=AE6UUd2 +GVlvF`!C$GaeT}|VKgh8!9L3ZV@ME4HDHE(BW|`u}siD^BY=bUVgH$;_M=iqnG*nd7?jw$snDIIMn5= +>XQ~h8Tlx(peT)yQ;9o>VN` +LB>pS`mVv`f9LGUU>1dL(nFqt(#U=Ph@{S0ucRF?G%0$ghyeLpgl3tDg0T;o&aJU=*&NRZS1;lA0`fG +GL+=^SEVTAJC{v*&Qp)=v2}rcJ1`lpcf;L!YqCYWSs!$j)lL;Oxf_PZbuC0Osy~ZaW0tuD6rUWc6id{ +qf#)Z4@$M4R;!ObEc_G+GW8zT=%unR+iEq8%M}i^afT8DfNE-3D?5P-bt~QEg^gPoLbX~z)>xy2da4wR+A^P!y?QLU}s4m0>d0@ +J8jexdk9~6ZEq>c~Tv@`^bE&M>OS)+m>gSzayX&{|2Fs9B?si7&h|XXi%r5r5IL$PM*s(cMmI +}FdRAJ5hnF4tmTDiVol7uAr?`_qj$$3KlMjlm6KB{63Wy(Aua0Vhp1?#};gdYGgzO98;j^v?*5$ZX4S +y@uzJ99S^+YjHvSH3RW=H6P`J{-~SP&Qy`h{t==i|gXUpRTU16EAJ(jQR?we{SWgwl_`eQIp#PVWTnqK60ArhlL0ZR@f@f+Be4cX&f^-?ugrqnAyiI{)MV8Ttfu(X2upQ9%7wdNdZcE`Z}`7^J2#B=OOJ +RyACxPI+2f8CrR}bOdZ&aVd$V|Y{!f{k_pB^azO$(J9@X%Faj#`ZJ7V`c~(RbZW5O^ZmqbXYebefa}R +6WP)vk)#G37c7*!0N>PPWAzuE?^cZP6cFy3rbnT4|i{=sI6Vkev>R?0|XQR000O8ZKnQO)EF~i2L}KE9Txxq9smFUaA|NaUukZ1WpZv|Y%gPPZEaz0WOFZUZ)0mNaCx0rZ +ExE)5dQ98L1-u-1G3tEST_V!u_4QV09hL}X*VD!2u7l1E)=PflwJM%9bY7ijvUlBU|SOJj(5);??tR> +r7}qOda+irL9#V1QJNy=7rqeQ(XiGMv*GM%Gp3zHWh5 +nq|iqQb49=5QZ@|{)xeZ45PUw-Zyi&#p~77H^~^orF|)xXr$lsZwpbpMs|_)o2GJGOgW$W*mzP&}cXK +Qa&~cr&J(uOv2!^}M5Yu=|IIFN<#%snY7NnWAUba*bXLA17F<$v!Y!oSY1;X +K9%zz0=0tx4HqO-`asyB1Rqz}Q(`CH7)gLJFmMRS!zPO*EO^Yiw+I|oBe +}ZdEBUQgxaJM-s_KKv~p^Ncyu~?LxXpL?F_Yel@^T=3UIHi&#asOBU-d=sW{`=~~{U0}I<#-{^%O_Gp +EZ9IZ8w%=Fw56h=qGa&cYJahC>??5m?KMg*r36w?9LT;?y+ob1z^T0gNVl|PYX&gnf%(hj9cq>Q59~v +o;^T8rgYa>s3IM>~F@aPdzmbL_Ejv|GL{zz9TTABX0AWP9mPmK4g;OINlU6O~fMlW^SOt-)lML-PtlZ +eWx2yt*0#TxSf1F5W(M^fd +GqMfIC8H4fX-1sO*#i;ohX)Y@|RTmYj!LP7IJX`RG`8(P{7@gv#J)gkFJBHF^kf{}nx~=vVZPM_Ah&_ +a5bWeuU@UtV7?UhY&O?=l|76e_YZ^MZ&xT&Jpx3c-=rwv^hl;KdsjA_)r<(Hh60P2&4^q&JAQK_` +IENf;>2$tlNJk2nNya3d-?H}O42AQ-Y9%| +@IPBr`a&Bb9XUH~`w?%R@MySm#C>@w5yvrPYhAO#oL8?s!s^tu>{BIln&OtWzx@2`8GE14>}$W*ee +mz8A+=_;Z5s?!j??)qa!e4_pQ^bn4QH}KP_C&ulQ~2R-VnW^XPgIFLzrp&NK}NC%jb?0iqeFkAx|{O! +L^cIdo<&E363VtG)^zqwySKCbQUw~AOwC*yy2T{qTTWowg{214ZAcO5|lW+TSDEg05(=_?)TB}#KTRCVhBg6ySjwk{2F|3L0jeG;IhXw;W5!>#3hpwF2>GzdXx*7k*);$Gs>A`a#RuvI^c)O|M^Gr6H9gtH&o3hWnSFoK9;oCxCJa{CnFKVFW24Gqh_o}0_Cl4&wQ|Cps-NyZR9 +J1iHYyf +TlDtrn|#84Ey5gBGF5gBNDw8AOsPF?PNU3CC6(L>E=jdSyZvY|5u<3`aiUPla+)InCt&CqmaY%1h#{` +|cQW-}gSDH9zS(1F@wkZmW`|^N1Rlg;FTe&3_;3#dR1nB~`%rXQdg47i5X9l-=GRbcnJUJh%zpg5MIl +~9cPAW7A52Avsc6ae67W@6GdKxVQc-MsbVKXDNQ_)*Xeaf$U)g>qd9Jo^`^IhdkB*mm8!i+Y5Y0-kXf0C@Y +MhtGNQR68-Wzti+z?Sv}VqWg5bRSM2o5u7L +dDzZ&fiIoeZyCF5jck@b`a&)}l)Q{Lwx5?eh~bmG(l$TZy-0X*!K3-{a4tM%*>kGpG2{+eZI?E@A+eu +es~uD=UVdz~?|g;;j|+z5*RrL{Y}BS0F80xw6YaL1`1OPU(LKz0exTkdIIZc#m{S79b7R +*!Ohjaeic(hK&LZi|di7fp9Si8&#Q5^}e^5&U1QY-O00;nWrv6%W=?2nj2LJ$;761Ss0001RX>c!JX> +N37a&BR4FJo_QZDDR?b1!pcVRB<=E^v9RSX*z~HWYr(uOL(ukXBphudPPPKBp|Tcjb +CoHTq>^}<|NGA2MG_^Y-NO_hk;rrBcP`9EmV1(An_AbBWf|f7s+5{gAxcd(FNHceacf1n-Ey(@pK4cY +UNm0^eZU-7l0DJxx3shB7DgBCSYc26HFu*C0 +NA1cRacPn0{*)0=!DbtFszfd$gy=78?LeFGnQ)OQ=x7(G#g=SLF0xeHJQy^NG`#lx87jbGoA7hd1YoB +Am6l!sd7yO=!$G8=Qzg8e1XHASQ-(?psuTS5cEwb0|ZZ2O<$l~Vo^lY&h6T#7L3aE&K+*-{!YfQ=1hvB$np> +{+eDk#Gy$aD?MgU6R6$_RnG5+UH06M#-@evN9$us%;>5_R6|jM!xIoPFw36kPEK7^Mm1pq;d@D+*G>{ +xMmza0r7TB+4&8$EG9HDuhIsRC&Vv~^R3vyWsHgh^4kg%Mt_l8cTEkoQ;7*D4>n)Ftt25Y@#NHRT-#& +!_dw^YufoA>7fCX*X;j9^+pNy=(jgd{swRPzWEfdD@#HxO>UgDP}o15Vx=;KxoBdFo!0j;iD&^x70G2 +`K}=wR1b$%CfHF=aVonNgGpH3lqx;agm+5iaH@wZoBw1(S|H&gUvSt+JU7D+>?}pXWhCIM;9D$2RY_W +I7hY`>jnY+26YAyNg5@MEVBYIKn=~JiRg}N1sldI%N`rz#*J2pBh@}14R72?>4Xsye9s}pvpb&a9q=H +wy52%Jg94mGrl(v{tq*Y$IzEtKyCLRj*6b<%H;a?y^Hr<4k`Pz`zk)MXF`6~$$(o0sN!P5#m)Np`?yg%@#2#JM8FlyAnAfhsJmCPygziA8#PHs^C6720~w0-czlSkTt+kEfEl@}=~s@hRLTz8ZD;P;^ +xEV<%p)ckvtA8SuFQtz4WCW);o|)L)v3uhsA9m+3&^@9P7X8jV8o^7_bIF#S06=pZ2{d`Y>!bOH~C(Y +JA+y}=tB$}Dr7zkE@I6bwh=E$ACb5P-3uYuOICyJQINIh3;XS=X=&!~^^O@TV*5EBK3AZZ7R7;3y9Gt +qcioiy$;cgr+SKeBF>O2Kkloq~?QnusB^Ea>3NBa*ia`&wN!SN~0Edy&H$~`@K&c^jjI61E=E>5)e6M +##TK9YY0th;za%_W(eH>y33P@NUOe3G;;?kmY)n=mW?QYkDwk7Zbvm%tc2&0R&bBU9%?WcqdS6Ku{ouZ`z>Gy%2vji66JF7wR!rn&ZXFYu~L8E+-MGjfV8rRQxzeT$<#?61*tP-4H%DWeo3NrWt7`1Zif2$)EgVT5NBQ19y=egN6tS2RxH`?SS5gljBW0@p5(yLeZVt;`-(J`Pr)(d8&vr9>%ReTvz~yK>3c?Z%T942iHz-Mq0@cO#^MC6!=8vFR#xQ*E4)a??BvCvVy^J{7T +%(MUQjapnrmEkS8Y7DaiPgM8jP3k2QUKf9UymEC(KjjzBzJfE*riY;YE`+~Cq9EpF~x25gT2(OO`y9* +^8|U1fFY0ULH!>8lDe#1D3cwbRUcHGIuX)O%rPy%{zLXQ3NPEoC?oj*<;i3sWj=2G*BmzpoziF +wDUrNq+*_)B7PBm!s!fFcEk9P(q%$rLf8Y2}U|@%iDx3qbV>XLp0&j>wN>7Rj?Auct(EiU$g$tR*F~X +sLeNvVKS`j-|)HHYkQZD(Xxq;hHDgH)t^|8Z8~zSQ}Ro{x&}gCkRP89v-SzWfAg7MW(#!_2pDrVTsN8 +roQ^KT*~#45+`kM{{1yB59@sEE{5I-w^if1`m=XN^K;of)&&W?nlKxe45eKR}ecUiLC$xzkaN$Ve2fK +DmI_Jnu;yD_}GVSeMd-yf2cpW +gd89{FdMsY~!O>sC{&8lHASU|O69-7q=%7f?$B1QY-O00;nWrv6$`2c?+22><|s9{>Oz0001RX>c!JX +>N37a&BR4FJo_QZDDR?b1!pfZ+9+md7T++Z`(NXyMG0tVi5@^>vs2EfN?6&%cgt9_R=D0cRvJyKuffv +jVx&?Dv7su|NUl$B1KBFdpM)DC30Sz_b@xz99Wj^deuvjWvo23jZ}=+b)&c{o4Q-AhPA5M@5_2`UOOf +6ZHEN%rm93v;0fR6hRTnk7Luz*#_XMfr)?#MCs@;%O{-ciJ7G|M=2b5`y{C>Xa4V|fM2g?~Qi_A9RcBD&)la-Ci<@!l9EDz$l|{{Y-PC0c=)a4s1`%B5WesPkd6ncn6%R;v|oV|61BT2HNPeg${P +dRb)^--;@v?+yA)#MZCzce7&fdBr|GlpPW;L4b~tgdeXyvcLW1Pb~aJRMq;iZk}uQQIlk!Z{CBdYWA- +|2~k3H@&|CDOf?ypw|NpEuXfL*?dx3it!5Ya9jf7yS!F +3?y3Z~swbjgZvKRo~tnGUrHdb)Xgxdx|ftCH7>9e7!>2f+(4sivP;*7VSKKn#>%WMS+ZFy@}!L7ZZA- +?P6D9AC2}Neo!I2jh!uh&>FcCm`}k_Ck|I_uxg54OGHTR6BBnYdFs=5} +R@S?+65V7OqDrN~(hjFwl?I(cu@%TUL#60R-}0&x{?>!2+B6^|DbV(=uPRnS__1c^*oC;Fj+x;3gE3R +$u!&awxhW0G1V{*}hJ$B&+4ODrZS1Qi8nr6>nzR|SLARSBg~p7_J-nf4ci_|aq;H7(QQalRtkQ7a5p~ +cf&s^3JE@419X~RBN0#XeFhj=Or!MHgU!#se#iN9SEL>{prNQj2r_^lW62rh!WM?FZQl1ODSe)#McXa5h+h#Ar8EXa2}OGdG)5ivppcQEHxk8|U+xInpvA0x+ +#~MqWvhK8b~#6wtPlbd7C_mPYA!mb0m#5^%sJCcJh)c~6p_@f>FZ*{zUi<_fq%)b;OF5$C34C{hN{;# +haKzFVcts#79iDaW}PHCx9Qi(-ABoWq~|AGGMY&ocL657;2tYO@cH_u>kq$N2OhXA-PRRn~CGM?t#mbZ0y}-diXa*Nu9Md1=9+C!Fq_ +gHfT$&}8NSgC9Y+f%-ty(n|}F5*#ELcl=?(5}g-#;9#325(|#9fH<0b{s{>F50B2| +{Sdh*&z#67nNZ)t&E@OGlXD$W&_avUU~0bqea3Ya5M%%B +J`%WgtXVowJ3zlXj{LH_7kk(Y3$BjX(n@PGaOJ61HJt1lFEg>nbOww{+tQ5BPXf*AyKgc-!^4rfWjUM +R!_pW?Un&M=5UAqW0KZ|Qf@4A)*y6q*EJpS!WJPjgD;Zh=_psBtd%#H1uP_;VZ1spDr0!TUJ+(Ghj#& +l4URrr}fvJ!HGFIW(W4NgP2;*EpH_bO))y%o|nj$#i^j6w#-9QVsEsDJGKE(kK$w2R18+lM6|==aG#Nb!qR64?GrXpOkR~4@Lf!od*q_Y}#t1&JI)!m-? +lDG;A)HTP*h1;zm_R;-^k%s47%_A>o`+u#_aRd(9mHNsaKa9+Qx<+7vp+|4S)lX%{0`-ex#xlEiZiF> +G>WrcYmxE-xl>AcZ|}hzlwr+N#`)rey%2U#^Dfe3H>=md)j-iCXy@*36zC+|6O)gB#2b5~HyQPZPG9d +vUqLU{+v8aZSYO?U1D%RN-^3sk(>tD%#OdT0C2<-EE6@4ndr=+-#fvqkLY@)wEdd0xs~(a@&t +Qvl-JH@T{l!{`o?tgummh9^z=m$&CUZLd&JvPQ5l*~THw$Mus~AM~?KIq&O^WU6zfem91QY-O00;nWr +v6$m&KKa_1ONaW4*&oo0001RX>c!JX>N37a&BR4FJo_QZDDR?b1!vnX>N0LVQg$JaCx0qS#R4o5PtWs +7zik00S?h;qsmL#hhh&bknIACAfOp(Y!i`4ElJt&7X9x#qz+4_+%_vDaYW8N-yD`zt&M|H?{-q{7F(k +$(6y_Du?Vug{7aM#+C_U=%bM{6szMu9i~L1+KGrx{+MYfaN-LR*QvQovi3-Q8QaLnAlq@ghz)%&WQ)* +?e5VB`sk>|RqgerQj`}Xx3i&O=^Uh+JkwWOzG{Y}0~b&cOT;TlT<3LHFp_;P>$>EU6H1z&S)edvilD- +uzb!XJJzMw>Y{ga7pOA?BP88fmaX7{Vb%uA_oHGdWK6=Q{~MLMT4B +$rjv=2K<)9`hHDuPs@5;uKS{!=lw!8|^T6oMg57DqYAe!NSLb)6bDIq>r|xJ_@6_+;Y<>E-W`%2Wu7{ +m}nsD5z2Pxehp_!9p93$ns7A{ctsk-mX+b=f2u?#Y&{f* +HKB`!p+29wYj|xVB5C+$+uv_?}F$jgUt}U6KYb59>-5xfZ`sDUnfkoHUtUmPwo6QQ2dztTpJb)6#0pS +_<6irYXDs-X{PIy}jq|{V|0OAl1wz4EeM(TvS`KFt_Y-~mfrZ#HZQV)&=A

C1V3G2t@96WQmWZDkAQ!XiAqoK26Vk;ts-OiFfvbXOCr_O>GWVo +-5$?)DhXngJ*mR+PEyN;NlcSioY%|%{@fGZ+c^1NPT-=vlB|l%mbEVfSrN{s0@Aa^>6^oUio<`x%c!< +tkI(yPTWW{MsV~k(FZrm|jRRp{V5<#K3*Aang=PeLJ1Xk}OAMG|$(vj{4>9Ju7pIUJ-rBZI-^Q;GXpA +gyrng3-LKinQ?dY%TWbvKRO}%`-nrchWr9*8tl+3Ejvt4&nenrh{M{gcJk2GYSLQ&Bu7}r$jrBvlXIy +Ge8VVZYW%T8#g&_4t{%bn)uIA`$n+i>ku;=8%WYcODvKtCk2`Pmj1g%`7mPNmf}iLuD~821N$EM3s2l +Q+5b`k5EIj>JZ8Jx0sVvgb~p$O(Fx2*J|Mh;64H^FxtlUhJcPlF#ZuT8eR1ui>2mxl>dK1>D9J9{55- +IOn0?knpvpacx8VifgRn4#JW+%>J2AKsw2_?=*eBhpHLLo`%G8oVOLv6YolKka`^h#{1l_dI)FT=QR7 +XS)RjRygP)x#0S>w}WWEok>CRw{B=|(4yog5YQ4r`wG3$kRD +^nFDqX|H}O_|1L{pUGoZFNiQmS2KOd7P>CHv`Ui$MBjdkkbwbgbxlKry_?ldfc=tNcR`7-ZM;sG5+d^ +CC{mD3eKM9&VPe=jfKJKPgm-JbThHsNeIkCP@ElH1M40j4x|;uK5L6vNJaZ1-3jjC{SO*yWK|61h +=ln@CEYR|C->UKg^YGk-#(M6?T-wPUKW4c7 +)(==+Py7R=0dHlj&$QCrY8mubO^Z9nesEr7hHmdT+~bOVcBi8_R;&6$CzhA(-cb~|2|9q14E(h3bbsO +`1yS+YlAw|OCf*KKo7l(F2l2Bno?I$1Y=USEH>xtW5%*8=?9y1_x)2d$N!LZ}8bcZl@96|(fdxz)UQ0 +NqL1fX$)g5Aie{nQ^bP8M!7lW}T0w$k?y0Ey?qm?;y_ +?v)QbW+?e2M-Z077GZt5lnfZL~{@;-Xk6~OFTd_x1sFY?0)oP}XaKp&qN@$=ip)LfNbmk;@tiXOZUAt +<~hI|7gWykH(;Z#+DHq2r~@N&;oNL-xPY*b|*xdv9Yni^pb`oMzmQ}z*OxKq>(mL +uOwW&lXMljgW&r7Fx7+gjV0tM%Hop$G92M^RTQ)Eerw7zYNsC +rf&WO6VAe$_nmz#-?L#NM>lPUHFv2T~uq@8DXk}1^abv_n?3N<;zqtXu)G-a1?(s=OVIH$$8Njz>zX| +3{mQFDDr7MB54Ef3FxHZEqK38o3VokbWW-A;6=1WW^Y1I0R(S`BXOc6+&b(MZpsm(mz2HTgZ)guOTwt!lNxElpI1$;5-xn(XBT3Crfj +(%P+V;Y|AZ$fbda)H8pt>Q|O|;Y?J1&kj2#W|ln`H)T2>CWuS@t`ZxnwJc_!i+S}9-=+Ht!5gXWxg4S +|Hw$K)|C{oOZ2ze)WLpAmld#f&MLM1+e0jA8sT#Mdqlt`L7JGN-bS$T`KGiIgUvWpTOT&DnafCmS`kx +ct`Ieh4#O$J8`0B7&v?F7`F@tVq?#QLj&)oN;5d5{V!q>~e>=u{6B2I$FU! +qf0Fh0|5?JAQc3(L6*=R&M~^#z*3|GwH}bnc6ssxMv;9G#w$ljxIRA;W`2 +zb;Ik&P1^%VMSDiitZ$Uecr$V1dop%FbPTk=()E+`-y(MDWg5s<{P7gaHyJpebX@EO=vhW{wdJeNdWo& +o#SWCtI3`dR_kEJpUzdAPpU1cIqW3s$g*m=ktP5tW`C%{^AKO)sP#zc1XX5GVgFYifm2fmOEp5sB&^- +)jNboZDyU%vLRW7GL`Om+sWjEpF9QCBMQ3ykBacR`E*WU}Kg4G#&q|1!jiX&br@fim|8Bp0A3VVT=d> +W6eYz7o1aHMe)iB&yu~2BzcF=|q)mx_7BOSzN{JR)k80@l%RBho(;r-$O1%{eaUS2&M%tA`~tR#&V*y +rM#j07mpo9c^V!jw3(c4OOo$e=s@y|aQWeBKgsQ;EeZ@;+U9&i#f8MJfik3Ph+`pI$TR%em#kTGlIeZ +(Vk-Nb#86yZGBp3~-PA@f!o|&}z{QkDoiG%el^ZHJod1xeT;;UEcT`$l6nDwpE|;>3~NJu&3QbQE{p5KI9Q>LebyU_WH+b-Y|WhGTs{yem)UAKec!JX>N37a&BR4 +FJx(RbaH88b#!TOZgVeRUukY>bYEXCaCwzd&1%Ci48Hp*M0Z#VT^^u^!C*V|H0TC91}9NkQO9m&XQ6L +DC22N~7)lK}$ny82PpR28aH3O|lMTpLo2()qn2I)#vpy@dN1*29IKWFG9bJak=!L3pG5EfmC_Y@vCEi +ES9T3e#@YNT$@QmmFlM5fT`NeL>a);-Z7#p-fDOF(&jXq)?i{`cC37zxb`=tUoQDc)JU8@y5Jtx!J4E +V)jvBS`^^`IecgUCWSAnfTIZGu{m1H%KnRTOk#5F&RTI+AI%>GZ`zf#8R<;eVX)dB6*_vQ|;LvKid#@ +&d-~sf5-BGSDHEj5?rrY+NP5g1E7j=4!IpWMdq4oJfXOXXC8>;bGXs?w1j6rLJDKLKhzUXokhFr)s|W +ggE(pK+VKw=%n_$OvN2}T#I1hU%g|RA5cpJ1QY-O00;nWrv6%{tDgJS0ssIe2LJ#g0001RX>c!JX>N3 +7a&BR4FJx(RbaH88b#!TOZgVeUVRL0JaCxm&O>f&U488kT5WOr068``L9oBZi77Qzfp_iQ;*`gCIvgA +r~vjF??qom0BsN2&51c)v2y~jsVX^rlns@lN~2CE80*K6Z|DWx5ALMxkP>0RAqtq*sHUZ_n9efsrdSv +`DRK73m}e#v4)FZ!Yq%ArwA79BdqnPWxGET*)3`u$m0XGA@Co~cBG&nbMRtYcFC#OFa6>`*z<)n1a98 +PI0}ryhjl6{?q!+`m|=7h$yWSVwd;S(Y`DSqpbHNn?#WmpJoVtzu8-tCLhbTK{DyIoFFp{CHNuRp_xzV*q}lal?+Ne4WbN +QyM5Q6R^bHeV>=s%mCYwsVhAPOMrEi;wWA6_!Z>*r7)=%Zm{pKOGTNSlwHX7r8$9g<;2?>W!vkcNkT~ +{XD9y1P^KgDJ8()E9$Um$$~9&M{{U!k8V(Z=|I>EL&&TNLq^b58(8K(NX2YU-KSl!v;?tnv4_*`qm>! +4*6X4-*weaJ{A_}%G$6Bly{6%ol8{5g8-Mrk!gnTN7$)c{dDL#z^)gg|d})j(&nFxuA2Zqv#Vkd?Y=!fpP1

i!2%O9KQH00 +0080BxrJTJ}LVvYY_`0HFf_04M+e0B~t=FJEbHbY*gGVQepCX>)XPX<~JBX>V?GFKKRbbYX04Wn?aJd +3933irX*{z3VFmpR%y^2jo!NZo_sdET!zFCm~zoWQZ)OBYDBlkMG#3?Iff+*?Mo@ym_OJLl0VaLmn8l +2I^zr495GAO{U+?TGq*!b68QB|GTS}R!8D@>TF$tI2&y66a&0jA +V_Y``}#+q!Fx-X(}Rimi8HnLYB=Z;h}kh2>7`>Hiv9Q-?R@ICtc;!`%xAuFX?XHtUY#$LAE-ShrX+E! +Kd6C-01xg-GsEea4#dp1X`)l7UlQ34w}-f!E-%%rhNEc@}=YRI9`hvtD+iFW01mrl!BXgwTC7Ix54cR+p{hti!oi|b8gq^M+r;)1yD-^1QY-O00;nWrv6&0Y3Ef<2LJ#l8vp<#0001 +RX>c!JX>N37a&BR4FJx(RbaH88b#!TOZgVelWNCABE^v9(S#6KgMiBm{Eo_S_A-N<^!vTW1pRzj9Bv1@9n8Lz5Zb1 +mvh&1UYqvfge*wViF~NM4sE2zppl@^qhg#XbDD*TN@r&0s|#SbvEkA{&6I +|QZEDH%1)gRVK%rQNvs+*d&o2=e#dOWqU<_>m5#t()B?H6|FV-re-iy;0?BRoNS?#}|^CfI~zGrV0=( +b6lJ%jBQLEO}DQy;$#?+46M15)qJrvf(2B1{**#0-gIuAtm3*!}M@bjki==XC`Z1ZU{1KoG=ckB)_66 +5h8$0;Phrm3ZGmxo7J=6NpnOs>_sI`_l+K`ex)I#e*qr=oTf$E~|JkZDnaFRJS_YNWL{C)5VSn&ODY` +n2i1sLkh1B`XggxW4`De*i1rXyYo2jOp%Rc_o?G7X26kib(puqb3)a&N>^-M*JZz)UqF7D*-ZsBDqJ^ +LjWczRJ+FDenmuOx9qL-D>s`4df)#+pwG$Pv>kBZ<@|+TmfXFN;qTAVwpuwy&n2|vbmi71&kOYlkq=} +Ptmsv6_`4M3h<`s9LL%T6_YL*-tfJNO!fw5%`G!!qdiXm#90rVH!=%ML?#aMaO`DW0k$C!5M3+}U~D! +~;+og^O^3~q)hh<0@v?JzgUJf&>h&ft84GmC-w$CQ7Jg=>o?L{K8KPzX((3bEE+GgRAqMo7U+%!+hQ-K^~z-1}W_!@GK*Nyg3ue7JN4k~9P(?Lu@IbhoH5 +7W6zYEBZJnYjPQeam^75w9c<;(dCas()tfGG%!mSD6u`2`;;vsn#mf6v;MejSEI)*aRFKa1WFWy9USBBC*D5xgXedV%R5)Q0ntFCLE +QFXLtv0rg$avJ~Ns!A2RJM;C->j&MEQ<0H?_95O>$X-Rd~AJ*yp|^HxeCb6AL|HdmqmM-LRQ1{nlZE< +g>%!YU+b;}N|W%IJh(^mby+=%vr+F7z{AjSjosH(>({jH-*|J35iq)ut3Vk!#6zLYZzKIP8#3ZNOo2= +8>i8;G-=u2M~+E`)Xv&W?OQB*!qoXo|sB^H6A7^S%&6DuH4;@yK$Q=soaOfxJP+;?V5DZ%@1gA+{Qn# +eIIVWXRdV#D(B((>HczGiyZ_erpxWlH2h~29AnxK;U}Yu9Ix*Qu6l_$%+_&yOP18wFiO5hCS8iEI7!B +8fJ>yLc;>{r?acNO{d+~#9SOJKP-Nv}!u7GDI#5tPahQ +w+pO_qvlji;Ml}eN4bm`*hi?#seNf9iH|(F= +gCvh#E>|nH0>J@O*l!M9=)6*dzW@ut&{8Am+^;=%;8GO)^Pt26|ffWuibPrHr1HNZpMY~q@ZxPew6`&U5n;XG +KIa2ngok%eQ8@FT#;$$% +VvRy|RVmSj(H3~5Za6F9lW@n&epH(Fd}LVai*tCDL1hNf8{u2gkB +0Hr1m238?Q3keR$`a5>&=@(g}R~ebw@6aWAK2mo!S{#tsgox(Z-002}4001Na003}la4%nJZggdGZeeUMWNCABa%p09bZK +vHb1!#jWo2wGaCvo8!H&}~5WVLsMm?oc$p=&d!7c?>pcR6ZR-7y+p0rjS+t^N5L3})8dy^)m%_$zwdw +w%-rgf$R#@asg4jBX4S>pn5txe!T8tsdsjX7bkQt!w6r(fT9?EcH{{_Dfz=hqA?ZN*Ngg>jYT&5`d3Q +1wA7Kf)TWjg}2p@((i2JDfwM4(K#jm5@F-S@-Grs^%U?{D+!ucoRO(GOjUn7`WhpkGQe``tFEnj@+c^ +C#ld6K4ABJKvX%n$G~3Vk%E-pEDN|_k|LT_uiR<-w404^gpqb{PoSu#N&2*Z!jTZ2At+ux}?h+@xQK_+8sEtwQ>*j#Xk>w< +T*3a+LKrYAHQu~X4P)h>@6aWAK2mo!S{#tN@f^{DN0012T0018V003}la4%nJZggdGZeeUMX>Md?crR +aHX>MtBUtcb8c~eqS^2|#~tx(9!D@iR%OfJdH&r?XwPf1l$QsM#tP)h>@6aWAK2mo!S{#qMXbR&5a00 +22Z001BW003}la4%nJZggdGZeeUMX>Md?crRmbY;0v?bZ>GlaCzk$ZFAeU`MZ7vj=3zo)DQT +bGw6cS-B3-Sva#V<{4{SW~1*ke1af`|W#P01zN4+imx4x?C)Qz=H?R`wJKZ!R%rq#WpP}vFn;$t)!^e +qTLiqZ0o!$rGRg3E#w_NdtX#HFYB_DSz9-8#@`ZEcTFZ$IFDxmK%A|cdMlD--L+jKlSCBTUEQ=It*W| +B+oG=2Y^KLF2pZEv60j-btyC(#mS%9>Rj_4U+F3;_8*ku8RafSFSzlil)wTIq+euwhmc=UGrHztd(k8jcms=9)pfViza3vdU|Vf~J8QO +)%84$N5&O9S}zzpBc-$Ozwz;{i3!xyl$Gh0fL>sr*AGYMFsS&(sE+y*RtK$4aigQl$VHv+Atao%6_ji@LTb?ZKL~P%>H9O`AtRAsusit{=1sZX1QF8WSh3YKE +M)KxRF~K!jTp@V=hkqhJVGs#n*Ku7o-Saq2AZ^jR2C1f4)2yyatX`f!UMdyKmpeGnxp}+BG17Eg4AIL +gvnbNWgl8DhjlhvXRe}sA`@Wo1N${?!=356Tmapdon`!0T_UET-JLa#ykd5Xj-)|+D*t)=U_Z@oN`&p +%8XqhxZtxz(8Iw`LwM^QXU+&>3p67uB$BV|_RF((pir%>+VHi}xH$tO!h$puZIXmamg@*?B27(D28q0 +pu@^}$Rn}9yr!{_5x1%#zXf7Nd#JpbwJI3xr5Ciw8KhcE&6WyOiMr5VX7p1!**^pp`jh3^8wPge34f| +ehWOhSRpow*Ife0}e@{I(WW*DG^EI3sqQgIH-QZmLsaJ!q4GSq)1e(?G6gO)0DXhAe6P{P2Eircg-a` +Ws2pZO9upq#-fW}Fo&UKqHi2_g}ESKU7EF!z{fPkt;3n-q-iY9=!5_tj?*waV} +Tgbr^gl+ZLI-gAi0aRY5cv=hEiWYKtDx;)2`SAKGUwKl(lg6=>H63xTF0{;@79-@)PMax_9qf0?`bB} +>sc{#E?6d>+3z2EqDWp7(re;*~ct_a{6N--80w)w5RY(~6wU3X-iHgqIBkgvnV-(K6<|=n)3%PZ{A;$ +PZ+eb|6&9r5a-WYgs{pjd~#Oc9`b@OWRG_4gj=!P(1;;EvU5k-S04S0n-h(v@F4N!GBaO02*Ni0t&bu +uHVSRvs*d^?CmYrSpfL)R{ZD1a{)@;L3%R)09w6A3dCRmJ0(^JkfT;(%k`3O#zV=P-G3;)x$G%3b)&)cPAk7%ja|ekW@6nReeJ6BC6d0`3xch5UNGwAU@ZX7|7+2XlZ +Hd0HA#RDQNlt1dylL7C%F5(WL8jk)iP>H=3mwH0&`iqH~a9Zc+tA8-U0*X>|=}2Q+~Jk=^dcPT|NtJz +#E@bxe<@?y7vq&ITBWfrqx(_88#7fEat1Hg#1V2FvRzlW2EE&+d3I}$kCHOe(|U0{_5Z*^5O*TWmA`EFf7v?)%(O>AC3>62rSsKAEaQ5EiAb5RShDI=M(4?lyPBT%pd^F +6*+YcLe&_v8tu|vdEkg+@z%;=YQ0#hA)tgnTth(?kQJ6;{wlt!N(?LZ=#o_@hk+@$EQEN_KoGUP^{be +Dh;*YF)fQWhCdo-*m}YSXR16wZ!3(Sba6S6GgC<$JZfOYAUxP5Q0f=DH#t+0`_|)?}mVv<=$t42qf<& +sw5jk1L5r^bXQM*UxXhvMz{2rIPVWOEXov5Z@O&N?e&W*{)Cs^pjQ=mWa%OWI+OPV`# +HkCiwEh%w7NZYvA#*N{g!uvK%wH#D3A +y+tEtUMC5A5m7-)%33S6SNc8wgLqR%QmDgvmdNAJVUcv{AEQC;|8gNIrl?;$R|p2M^p?!SwkMm&D+7L +=dJh0L4E-l1W2gc9=S4sPR2=IQKL@G=n!t<=>BrBnd1@6=A849daP;o~V)td&wV27(ozL5>deqg)GrQ +qWoqMs|#6@^Cv%taJ3Op17R6e5t+2Vs+9Ps#K}o1Dg9VdV=zV!&jd1?_guj!12aP{dL~%Px4XnBOAW9H3dmCU?7E)A_97*5PML9-*UnVCf~D~k~JoOvP-INpCmV=ZnW&tyU)=p +~g1%tlM|#dG7wX+NU7LvzqP+p`S;ho5DoOL**uTs5;g^{w22c_zKU+|tIRAsm;q9m|3>ke@1Ss7`&qX ++fcg<=BEOx8)b)l*|emz9tW2c;qgpTA(Snk!RU&iflteRD#-r!uBz*w|2tfVG4(?oM@t$Upd32R2L5g +7Q=A&o{!qp9nf|qb+Q|avjc?fWYKWF(lZ`{L-8l=jbR*+g`8AGf+g=#G6s`2XaT;`r1GO)mZvNWCk{5 +!-p`t%5{6A6uP7gGtOHBjPzQdF9NlDEK%tKp^$e7}zbi2J?Ro!zgN_?1cN_z7DHRn&y#4m|8?x#txgt +v7Ame7;VoGT1{tVJ`NUv9YQf#R@5y5(W*N3}+kesy%#?XyioAS6H!5V;E$M>HZRd<;M^DNKoL}Ogo+2 +=*p+7_~2c7=(<?a!Qeo*;0kg7Dn85`}iSND;D-r&LyN65g*o +<)o3=56gUem9g6~uF1_-D-3TiMXkC%-Yb->N`F{#jf2zcZP&WnLoq%u#&k$+M=lu}9*v(IsR{Hm4KhpfrGJD`g$3j_2F8R~j)!cWrZ3C*barG){Q +h_Y&po@RB&@{<1+j~{y%{_KPQRF6KEEiX?2*{GieZ*au+EOj(8Sh+XUh| +h&eFYU!0yImYRyYy5MFAw|ncR(4mHDR41lKo$$Yj-ZXug>P@3v!!6lm)G9_uaiL(Z(7(QjU_QsEXb(g +DGI!FKCnC*rtkoAKs-l9>VnfCgsHy{3toQ>AY-kb1vCjh$z}A7WF0i<+B(>OG?}lJd2(jGE8QWBdwnLp_{$3i5PwvADErvk-t2OD<~3RL)v +XCQK<%{2IXRSfpnG)Lk5ZV2Q_(Rz?4>-zeh_3?C@NXK#Wofv1ATqF<{B!*=&@^tCIp7sj+6lfy@Wb7O +utFW6fpNTjGP`K^w-q9qQ)tQow&Mf4hzqi8rwZ_|6rm`i>tU@ZSPDx(i)s=cV%nojun{fRd9_$_Yndg +*h+UxxK@^O8{O?n3~k1gZE()f_rB5cz=PjaR!^@D?ivMzk<lXbAW +b4Q3|xvHM#-^)&6g8+zlX-uQ3!UbaZ_!qu*+Atc +0G<`raRTkgY2p;Rd{$cc(IasU_~4B>#~#J+>w{$WgcVMIaVQp+dyYdrMrmF?d~-Zge(^m-u?z(Rb4^` +C<2-7bo)th@HH-~((4g7@o8mqwOKj`HcLSQZV{RN%U#uxUJgPeX^_3y}G_e&wORX0II`ILDmJu?zcBz +-50Y+GITJ;0a5tecV6S?C!mN;phE2J}=B2Jo5qrh?ZNO)x8GrK_q+E;AwL(TdeR(Cobs?E;DzF?9G1= +26S9lGopFaheo`>d(sOed=Ct6jKC#1ub5d7#mpTz`Os8P +XqguIcy8)mh1w0Mlr_qs+1Ym4iu)>l4GPOAEZ%X@q}&+hLxT)+f-)|rbw(q*3(*?96Gz2;!$MiVc#qg +UcHL(v%gRFfwC0ykhabIei5vzZjbKcw&ECa(ZkMBgD1xHAq*C1v3g*Q|m_UmQDnHGou^>Oz%sw>cu=XuWqXW`OP&lv=C-8~0N=5RM2s)`SN*?*0W9BhOtmz82WI5|M#jx +yPj7LlM$`xJT&FXBfXfm0mmToqDkR;0!HArC3>4z(8|EDHW;ZxQSWIiQao-aa5{;IdV)S;|DLW25&Jk +|{1V+O`Zya$w*D2|k_nBA^P5*lImQ!zahY^9BY@>PorpY1j%U=~)EAVOpBwjM_ILHQV)bCS4Wc`zR((PyC>i9iIF_Gd1X1yHeeXTv3x{h#h;J +hVOh}a@o*a{HeGHr?5(?c^hrMA3x+gd{>gfTP|YzOVEqsiM-1?gx3W?EX2M#0bouvb;V;4PjJ7x)R0a +R4ba2GDIIPp2Vw8#<3nlor&4||Xqk#o27&F3kuB#0IQb(03`qb0B +~t=FJEbHbY*gGVQepHZe(S6FK}UFYhh<)UuJ1;WMy(LaCzlDYjfL1lHc_!u=J%2CKNVKZtHHjp>G{WP +F(9au2|XJmDdGB(_)30evCX*lQT~}7BO&YZ+I<-o= +I(c#P;^{0YtCf1E+R0>cbhK{j%Oua&yKdL0JWtBYZQXQ9QB`$UbY)$&M@Mo^Rl7_5eZ8xeU0rWleWC1 +B<6YR)=jUa0u75U)yIR(pjam}mtTYC0eg5r?F8#2@o{A0s`l*5+v!khudRuO@{7O};y2-Z1@@;YMfNZyFS*}afv?GnPt~fUso)+h0O< +^)NrtPw*>Z)88oAOtcSH-0od-4j90K^Tcsw;SI^>@BHOY*OaR>`khfY}yJtCFJSFS*=xNmEuZDL}kTy +;~|GWa|_BYt?qr@uN+niq*~2s_hDx;1v`6o3ibi@@$9CKCinc^{$dTGKilql-fKgHwtN>C#l_Su@l92 +alRrr5f5j=O?9IYdsbF&AM1-V8QTUfwlN@=^-68p3}$0h0)r__Vj2eV|6_e~1i-p(E{iT-)>YTk8x7? +NU46_KN1jIXT>M9xwyJ2B7kRs^w+7jlbnPWx8huihUA}G9y0s6>OHL6;wF-mnsbFR94d(&N5PCB +KFFX-CHv)4GA`Wc$qSmQ?h63oJn>X%*Fs7qCKu?J{fWjTl&{Tv|k41|wrhMTW}{?!^<=ebry0tP}! +_V(3gy1~?@~0Ose)#e9Wdn+`9(ppmI3NVjJ4`snCrLlX-?%p|8d3D7(TvHmsE3*<1|Ct3fh+if@MRXTk=OZY1ceU^Y +wdj0yy^!E}l1@!->Zs0kmnf=cdu`{!&U@HKuJ4N(XYMtaveq7bz9I)8LJ9Ap;Eb&D2!nCaGc?^Nq+^# +&YYS^O#nDgWewq3ydNwH}`hE9{?hjfYO2RC{A`=f#lzDi&bb{9ZE_%AsxVUI|7-6m#tXkm-K+=A$X-8 +i`}yNjeCjPJJ$PnKhQ%nwcy5ac_R9nsx62;AZ_7}2BT%VQc +b9PELU9ZIefP)2=%&SPW&Do?UW_RqSk(#v9-!aARMB%4mX$28zL`*=+qIsw45x>afKK2Z#pb)rGR(pi +;j+E=^8P^{%%fkA~ir?H%*f@cGLc5=aB?O6)BY%np8H9#xm}FhRw%X?r;{0&jV@xwT_`?R?AM +}*RB%?fSb}+~zvM5Lt<4%>Lg_E7Ye^Z`)s~9fG`pJr2SEo!mGqIph)&B$~D$?ersNMoW`Zq*j+*w_Bx +RG9Z{RUgmP_a=CIs>#^UPRgilp1R4lHvsAU;+AbxP!D?*xE^);p8c9h7(T;HeD=GYW;faV}_9Hjnb@m3`*7`BNjV8~!^Z3IvY_A1D67!GI_t=Sd@5LnErK;yH3N2v?7%97Iy*c@SXz!p +^O;I4q`E($PDL8|Hv>P$h$fS!7{-IU7`@ueLFyGLp*K_8w$mnEQdp*C9$Xsc@6BmtKyL2F8Hj@X`QNa +_VJMA_xJU5KFCuPv_&(Mc(qqpD+X8LKmPlew*(qek~DsBx-$g?f^0;OYKzPwTgh-1%V5#NHd#U;aCJU +RTHs_=le%O$L8jQLK3pKeGAz<2?m>!Q3fL6Ayive}b)IMb~nQG@DH9{&3N6vD=vXgL8GocKrH6j%JYEC(93YGHXHhj+)YgfbU*xP;h(7NJaZbEjb2 +@zsnrSy4TNF!G+B!m!2^^czUQzrHN`m^kW-KutE4zDO0X?aetdWO{WDQ-lUnrp8nEMfI +8N^d%P9px9Hfx(dQ3;C1_O?v1~y+G$>|(93pU15fLpeJg;9+nF*age=#L#v{lVz*@-*vOxZ1p%!l-WjQN#b_t=ui%Sl4mWmn9o@)LtBw8#k3(OzzneMK&zbMehJ!E@+n{clweovl6JYlvn*X>SBp +8I-x}SJE@rc&=0>pVwt*Dn7v+47B9@+sr{Dadq?cV}dA`chIq1Oi7(Gjx(x=Y +K)Y54!^UAMMSmLy6S`#`qHIECUPlF~jouWd|zb`X;`>thI!z$z6A(TZ5e?>jPyaV$xqa^B5Wnex6(8a +uFyy`KrQUkwN`%5y2W$fX1;qcJCcSVWbAq460Wyl!gPhO~cxcH|U!Ji>v~J`1|UGBoMA9V{3202r3ZD +NebFX%iWK1@5x#(kOutZ6=9jkadO-P)?jlTwsxBXQFQf?T_BXj0<9hQ<^yThGBX!faNnV0xN>vTksXZOPUc!n_R#D2bk-TTu_`@DS=t+Zv`ruMc3-FIAL0_%wz#W))?b&=_LF{W53T#AfeDG~W!92pwsq{-y-`j5>3iP2pw&1*BAgpH +pci9icNk8RnITigA6UrPq4gdMHiQ%2iyQR_ixxKyR}T_5+x)G%Nw3HU>zLaV@y)XA^{DDZZBuBQwj4* +Ht@MgbkaUtwrifPymfWg5hbz;FxbBa2GL!u<+BGW(!vce)MT~BT>UG +m0vGrace(1%j-UsOI6Inyym5A{2qG%J!g<`Aylh#BjIT*%kC^4e!Xs~<7g<)G$76RxOooZkQ5|?z&i7 +PyT)@eA0zS{0pG~r2n|NP)wa$Uqx&$lA624dzBn0`#@31OMkyUulFUSFU_Bbhd!9AV5yhB2d!UFc9Gm +AWQ4*dZ!f7Kk{P&{5MR7|rR(w_<%Z0NwS|x>!PVx%|;cBcyc8v59TR!18*rGtbv= +RTLKjCInxOy2s)sjiGH=D3sG<+0Y}6b8LgFc?*7?;<+XDRL +lcoF*UzhA=ExQ$(;WH#eY!nxR3HveAMOm8+Hv1|Bj#zfx`4$h1%W)DPKSWUa*4ydO{amgZk5saliS0u +sQ9r)tIOlV%>+1(Q|TE{le^qpBT(hZz{EO6|=++PVukwrk!?zBeJ%Yjq#~$5A$Bs?Q2Q*mAR^0tMm<7 +pIHDk;?#i-Q-oB!$zfA^rgLG#p_-oS)2qNG}dFyFqAhSX?5FOvjtHh)?S73S6UfBbRl1Yv_ITZ(}W|zc_m#y@B&}yD56af~8<_k)@Phs|?_y ++iMTRo|`1=no|IYtWC2(mX#!lK>tIkXYi*NX~BXHh~QE;3*Y +G2hksg}jI-EC-3Z0k$o7~0w=h}hf>ZKR;;w49tO>Oe%JO6oJDSQ}*7FjVoV_&K6hA)u68XsPpEf_CmW +Q%(z6VB*wN1|7-THDi9{ob>QDvzgIy(Eanq%kj4O_S&>Sv%KQk-`d@Z9V>mKYcZ#c`+~D~YKOT>@81;bsqL{<)~h8pd6WLGzMNBXPAKpur+>A;-6s +O%ZYG2vkP8ORMzov1Op98`32aHBCpUc7*p{z`c9A~SQi1Y5at5^9&5_Mm*Wk&LuhKG`0z7$&ie%0}DR +_&MgT%L5M#7`Mj|GEvLAp8_{Di9*=NL_M(E`t4+Ia1$*)V&<61;tEp6z~-ibq---$0?-r;cmJS}3yIM +*ITM(d*)_W*`QPb$2}X%_73qcicrU@pnTAJ|P302E7&;=84k5f@E`Q^pzy{+Rdt7e7fAwaH^k4{%q>D +K`+a8j7!d(pgZPN8DxKXSiK`JbJ7O*d02poGab46X;yicz%?3I%t)iJ9khUUSvQnK3}Y3wojjXc*%}J ++Wl~pv)Q#C5a6lns?43i?WMts1=4MtAkt`T8HO3pcYoaZZ_ko-}pC{$&9jivGcQZXw%uQ4cDmFo$gn9 +gfUF;|pqpIA{>c5o>|1hU~$jowNup6>Ycmj&3^F0RdUh)$Ki)pEs)fx|8nC;!!gaAcVtg=9`_)BSQp@ +l||$KF1}yV(gp1CQ$Q>WHaUA1Yv|0%dGMZt^olCqM!}G=m`@JUg_|w^FUp9nGl>-EK=I68IS5Ov@cod +s^iJ>7ClozH +*6h+E1@IbXw5zuOLp$F+R8bmTm1w#5<;^{|JunO}kHa%beO=8EDll2HY5=mt6gtX$>6+A$$d(av_@={ +$8jJ9E1jmTIZ>_WWCZNxm^! +uC`zEx+BpU1`U6D+kNN6Rp%fU6Oi{|$8b1`O@<9U3+XNhmyGMhk*RHPzn@uqZw)>PB{tbIe8F0&w+nT +7J7*UT`3eez(R&063kK9oop6W!ZM$DhB#+9vO>GNx9)$+#m7OV55Slql3e#=Od*MCHf&Kpy?!AJVCXL +L_aih2{c(M^L&4djDSYo^cRSErnQn^J?GzT8Ju#ZO{0%anp22kvLQtwQX5DfUir5Zr2A|l!Lgkg +OO+j1vJKINv*JmnZ0YHMa++Ua*e0S&yWzPnGf6?z$@Hx|8ejXrhGhU-l9RL`h&Z$<&VYAyR-?lo& +ubJ;Wk*0XSFj(Qj@I$v#Z7%)E?N0Onmuy^pfauf#oTh@CLv`Hzqcz?61fnM55h#(TeMTmG;pAw7*Y

f@hE8$z7u+({MhRrRHqRoQgibFOy4-9Q++a!Y57R`a|`H=@!bfMPOJ1%T1CX9c_tgv@kVt!oDSe +CYE{Q%&=$22N5vLG`|V +8#JD*gS?yU}k?{-}h=IR72LBKn(#Lb*|MV8CgigJ@1QRtS4Iei{Q`(_*(-Yv|r4j|54=aGdev^ydh^K +$l@}eq#*(u3mt0{m6-wB2WIYW(9mjKz~72*e{ITE6&LHyu+@L79};^7elO6E5u6z4(lMAPgy7yj_eBh +LN8h(p0qGdWzaa2`#u8G865mm1;J6CIVT$z}lBUxMlj#+;e)GsTW(i8M%9d&)^G1{epw*-|C%C$M7M5 +7CB1dlZn3U6V8J5tB!y&ZNuqa<vOh)AoZ@4t1i) +Oc`<;c<@I8*s)nD*I{q*0A|=|5oIMe5*gIf+bbPY~tX<%vQD27v8@|isUgtO&uWS*EnK?Zr^&iEdJ9U +wHTr4k4ULIGPz+c*n5XspwGwh{0t;!`;PnwNs2Z+J@4*?{9U(%rbRY+qZ=z9`QW`20}wZ9`q_F&sUkU +L_!{684W7bp2CttY7hSn4B|M$|9K +6>KLkb*R{tp)a~o7WP|Y~xm{GYR7qAt8&v4OHc8W>;bn#Yg7!Sx>W +Zy$QV)dR8rs4L4VGz(PI-55dT4c2DJx@tdelH579rQ5j4W8Vhq;J@v)0Xd7%NiTF1u@hKhYxnm=I~BD(%I*SdVmY>ZY*#N`Ga{@KUip{P?Py9s5SqCcH9m^@IjM22b>x)V-G_4HE%esJ|H0b+fbFX +7@ifk3$tOs-UeWlBJ}>l347cY>9eq_|UQeTkDp{#ez?%a_j_IP`qEBz19`P*GK>jY3s~IM7RIC66Bo6 +xjtc%woYPm;-J}+SFOiL78NO1x^~fK?$~taq4obkQ?+!fk#{IjY|3Aj8C?sliA7GkI;8hVlkWj^u-qY +u*5djBVm*89qh1)6oZFNe}X4ZDI%*210*j_z89pZ%XShDnmQR6O>_gaH(PT<5w?qWS3 +*1`bxlB+nbApt@ANdUBEp0p+|5|{1A+t&;#0Jylj+quE@UG%OfT-L<&LEDjckeX$oh#@T~vcmbi>~2- +qJju^VZb`d#&PM-Rh0WsLhhEc~A_{jOce-!bJfqC1VNn1PGvqn1B0^Uom>tEzC339#nO=8N=vZI=N39 +E%&l_O_8@^7UHWHadlRMn_(;@~#{r#pIk>VlFsKnG;pOEQd`@ZOThnzT_HQ?Z~I^I$K;E091>;*e!mT6W}__7%WNzymnIFFk$uC6J}ExqDho&(Pt?!DxN)?aX53jVI>keX +XW4g)ZlNOo27p*D0>OBPlD&%hGhit^8Qun;L1=qB0Jm6H*UAHRUhOQf52SmNpY2f>4Y}RE^}(5xKZD@2PN277 +XckIzpk-A1$K-5gR{ljT+R4g`RikxshuTfRbKq`vTmeRls^3=722JGkq+-_#H!GE5r6TAs3RG&*$wjOWIaD9Q^)srS+-PT0aq4tzg<|2qi^GJ#_1tp*^S&|etd1zZaGv)V6-DB-4K@qPW=<7h(3NeD +*&Smt>dHPz`k+nSqbt8Ix`0>qz1iH@+;wbWN{>LR9fu>@$s1!mhdef%*Ut~y?ny@N6;-qSz2yt +T@bJE4iOr$7Q{PaY#VF%%7x~zwa-eL0b>@cZ(CY{%07v_Dq8NL6 +gh@_h8a;Ty7j4vHK=*8Gd>(}+6k>Emd@GQ}DX5P@7Eia0)$_V*V_S)cJ6OktizyyV%snVm68;=r&b1= +}DxG2)1G}$T6{uPnK0a8{FhC_DqIk4C=Eyuf(!b13cP=WC2=k<*V?J+j8r=TpIP|rnqrMTu#l+n<}<= +wb!Pb#F|>Ip1Usn3>ubMp01-%dp$`sHAP4XC>abSX_Puj0ul5d4VG*H5_5V3ECybX +FU*uRq+YQp3;<=WcwT&4A#d#(eF^cJ#bVW5dX5fuhY^uHNKw;dmVXG-{hHTB^!>IsJ8SubvyNPV00d1 +!BO@~%2b%6)#TrCGsE^vdOj8C@#|;dI_@9LJ8%Or&Q0qNR(-GF;iVIP|K(hCtS4%cm}G#clRPAHzmF` +;YYFK!vNPV&59%i)jJop=Nk%v*wsFfkAp5IyL<*MuF1c=B0y^o#ZjDV_dQsa_D8}uP=YF2h@3I1q#nQ +fF>MrT@u+w?kr;~g)lR21L1vyOP8`hA6maq2o;b_hM-9Kh4#iKD1S?Q_Wqp~#Q<*}MPcV1uy!kwtpdo +M?1K-s{V5BJ=H#V5W!-EH=a+;D0QhdvazWloR(Xq(NF-S)gGR*G}k?fO30yL;N)@cN;5prI9M&oM)%c +AzKkw&()Gi!;YEb&w&6Ao$E|mV<#{Pmjm3gEhr<3hfhkY_CneDO-N6A~b(qcU&tSSQOW-_#z*1TJTd- +@ws1%!(F}-t%R{;-r-4|BtYh^)wn@H>P^s;GD-0ypp?L-cMKf`f>=6{q6d_al2f)~rMd+bL2?d{dfb! +i0I;)|hc*^Hq@mS@DBN|$^$z<)(ad5esqkZJaGRH0Bsp$mx&(>0LAm0Ua0U)(v7mT~$eTcc1 +RRe}*Dn5_i`711)y4{2aG)+C27UqSl0On?--zl`Yo7wMisIqTj-SW(XM~0j!IHa(V2rX) +fr3OH!{}KSZQxz6ldnJH&w@NJd8GNOQHAA8%(Y`lMy!Y$4_+?yZPCtylTEd&FyS-BaK +lp}2e9@DCy4N?7`y4ASUkOm|FC{9L-4bjtOi_qX^D&*6*A){$=<2R0@S)d6E74Lb#6M{x=a(lbF`8Z{ +XgE$IuZ+RHp?9&j$l?MdsIWTdAEtijICpGYNa?*zW(Qk2O~lDc?;-+3z1K!^N(8-%YAo1_9~&VY-DDk +6O6j#XTlMX(XjXfj(fe3r@FhW%;1obPgq0i+wjOkF!-u06S@N)tX77Dgq7V>gM+Y?^)W;^oM8x#5 +_Q{!C7SDjry>Ly$^=yPXx{u8x7D{ah4qw>|FaW1_S(zW7q1F^1%bH;WU$-!`a<5X}+!2!x@W3V*aXw* +!=klJV~JI3T)ycg`6n&VX3 +%4(N10Vtis=*D$lnZ)?hgx+$b9HpN!8;(el07%ro8)&z|U0_kc&FqU&HpT&HNSmXQS&`XYYF%p-dFG{ +^D(EBu(+ENc6J~X2P%i;dIuJin?XpSFbpWinvdYP`+?iiZcCVQA5oPXxBf7cU##LMu7`*v7dc6!lVg6 +T-bW0)`aTsf+YD3#Dk01GZlbR1it)yw6s(TD5S)JC;`(Y42Ps#}F-L`8C}?2OmXn+t)*j*L!X0$jh$p +rnn89_mPWu}3ITY{rVYt%#~glc +dkv-Ta86pc?zw?A~CdP6c514a_Nm>58wY-R&S!h%rbaCr7n0zLsl17S1g~eyAm%(tEhsK=4mP7AM4Z+ +#NGqj`r4^T@31QY-O00;nWrv6&rz}3a-2LJ%SApig#0001RX>c!JX>N37a&BR4FKKRMWq2=hZ*_8GWp +gfYdF>h7Zre8W-Cset7-|=`wr6iHR&2`_ARC4@-5vr%Q6$>tLW=@P=d#Pc@0>$P6eZeCnzqA+1w~<#= +YBu*TB{vP)Aio&HBVC}c9qhWWo4;sW`!!vXjGp`wb_Vr(|p~D9k=%tH_d@Eqcu{?m6SXu+=g7i*V$g$ +LgaP;lVn$U19xh<&aNbPKtLV%HFw`+QqrBftVEThH@qyAPO2=w&QP{wZ-s2RPDTv=PRpB2isDTnj6D& +Om)VZbx;MXWIhWy0o|UQ;Io$rvQ-54#+VIpMrF0!2xSLy~)VY~l3vvi&h5%;EiW5qFSyT25J!iFZ&7t73NsYNVC?Fi0vN^0N|xz-D{eS +QXe=K6aQ@>N!=Qe=c!-svrhUoC(}7UV_pLmp;<;FprD}N$D};ucA@3h9#|VW;A$GTR=Mr;5B0(rGn6a +7!Lbp3nG`Y%Gwq&#C#-#YU#hW1>)UoABCJmone6eE7B^z6QXFo?S<2HcO6NtfYNoJ+*&(&MVLdOT>e* +;cyG=f{OdjVA^--4Q&8>p2Am23@}LQ)Ewreww$C9zi^3^{~ID5W9BDYN@GLC7q~&lf-o6k!*uKD1W9f +ru`(UD*H>EaZqX^T>$ZDf1J&nD&SnU|aGc^=dB}nX#iN!gv55HiNwCTTqobCo@*6+DnS=D+NaA>D!VZ +(1>;{vy`g_&X|Qz=rV0Tuc3rMtN~w9Lgz~ianKWQ{i&B0|A2JukDAeh&=Ud;AVoi90k}u(pf{7Os-Pz +l$^au*B_#B|ws!OPjA8F%yS=e&n?c@ZjM^fjZpJ`QYIw{yJCCWbc->+G!nnY0p;2s+ixE)fD0RV_%yn +lTpn~Q@jT&z>4V3IP?5nlm@FS*QK!)Gtyt2{GKI9h_;;0~D>pv#QP?%rl|O^BMb|Tc}C(R3~ss#+C`O5VIoYg{a|8H(6-15M^i1@Sc +g)E#L8Qw2xq2;5T1AVYT58AQY2%f-uWrM?J_i5Xk3fyvRIY+^_Q=51%rl<8XoVLSW@ayre(C1*+Z)E( +=Dx+~Gi@LPI^`J!bWH2p4Np!@ym|q;941Rq{+hWMk7jQX!*-Q0X~PfX-t__Ni +g~(*k>%~{GuU{U*;kc>ay!FJpaJO$$0JwE7C;-|5{Kck!lN*&h#ZzRRpBa1L;wP6@D0Qjfz+(lLK~a7 +V*Or47`8UQr5&rd=UW`r%6ShuzH5B2dWnv4LVvWXNRJ^);`H?V^(3E?#r0qj&hnmgG92q2`}XKzx$G& +vM=}p~jmOK^HCB_hulY_{{>Z$2XTAn``kze}(%`0fr&_S!+3J(#R4558LEVFYO$Nra>88y;orH(;=oE +whAzQ++4(E0PN1~TM@XuCc +*jwc+l{$A5wMszwdmksQ&~JTg%%%v0m+kEK;|nI`>H!+;HmY>_RWv-&P*s#s^eGepdl9F60Y(%Wc@?T +c=A}zioGKBkLL|YxkApxLVdOnst5#sE_K=wcP>hV+aBo`|+-MUXENrNm7O${SvoYO&Uv07r3LH;)@v` +N#c;SCLg{Zwur%AN}#pPA=~BQ~YdyjZYt2m$1amo))sG61t4dN0xO9KQH000080BxrJTAugY$L03!eZ0B~t=FJEbHbY*gGVQepKZ)0I}X>V?GFJE7 +2ZfSI1UoLQY&07C++qM<|U4I2ew>N2%sW{2f##d!?61VZqNqup$w3qoPN`xfV6rmBM9j)vCzV`(HL5h +-{uDdO_)iww`y!Y^VUx9g{Rw9b#Raq4>ibT3vtD+Qfo~tr0QdEGSi`+E#*qu$(Ju)oxVPe{(Ac3< +xpJGr>?bCnZnYUS^-aX>4&p7FTekAxqEsd*M*#s_;!sQlF9C6H)&B;aVGN{yQ--290ZK2Jds6Ie>Y-= +c3#xdOE`_SleFIx@bCTQ>D%bV2}m8iJbOE&v4t$7Rs54GqE(v1e{W==f#>?8E@lz%Jx@RJSgIqXKSlE +*UddZkT=S?-OBt=>*>${-nujV4{NnhqH|X`ED9$nf$5?!%uR)WUfY$`L<2239j_pw->5p|V?tflqaS0 +e|3;{%2LyU>Wh!fe4HpH`oPCyvA>h*evWm$lP2Gk+YI;XLI6y>0gOAnKaZ=d93rgo>~(FZ +0?1u#$|d}G6dpbJW5{c&(vBu6=pmg}r7E0G^aD^dx&7^014aj0Y18MF(9C|b+ +H2o;GO3Je07T!tN1{i8Se#WL_SJn@jugSBT6OW8Q&;XZdjQ_RZOw?|Kl^Wg3p>(Ku79Sh$guL9fjuD?NOh-xqNqWej +afMzvKsTDtB=<223JHwewxX3DgG)qr$wQPny@j`+M}3(jt$2m_yR{LN +OT-QA(IHq03u5XK`5~cZItSBXgUUU%*v18B8M0+u3E=ci^tR3f&lh{(HC`483kx?=I>gpnv6ZgLc(t| +*>9~Lig_^l*^6Soj37X{onYFN$FK*2x@(^;^;NkMzjJFtZpf7FPEO@!X}y40#ngw^e8ImB@F<*iObUP +@`!6mb4QDZY-+4lK8rx>_7MOk&Q?d$+wbNm0(7;Iqy9WGwsaBHR2x`YL5;lVi23wVbZ@G;(;*6r9215 +c9EG6JS7G~2!t2K_HznKvk)xxyCJF{z)s6E?$MCm>U)NLcYz8W}M7*1)ke&8t(PtxhnAYgEx(@W$ujd +PRXb)4Tz`Yz%m0lnWU{3Ye=z@SBkznw+{;0D8PDOW*x{e^lUl!T2DA+oR1OOVdi+LUvY2m<%Rma#za^QB%{OFr?i=X2{`vBRCJ +Jir5B4%#mRZM!{cWf4(MKEATfv?WN6*VOKtd*(y24QWrd?q0#pz^#=nxEu5zya{5mhVX6YzXush=YZJ +%0p32c-&V!$o#v@#YP+(#1wCuNVG5E!tMZ+5)t-Hx|QMi{?Yw+~fkZ8)?7{9br9Lnor1l{jE#`FwMkU +o1E8?ukl+NLm$jzzGe1DHk38%Ke(;7>#+MJxa(*9HK1Y-3P_1-x07Yl~kE*V +hWQ+J}I}}X +LCCU)pw{ba{9mNs3bld+U~}|(?>^74gbtGeFwO+tx##7fndcfc7R8?n_>rh1h`qHD+r?%IBlB91tF>rpu2+XpX +4AJG28+fJU_<;I`;o!ojDx?k;WHyy7d=U828pf_|b1wxj?kT-TPRz{MX6nHmk#r!&ZP~v$EI;gpDA{Z#g~OYh^%f6B8 +yvtHod5auX=X^1ppR=KY<7B6TgEY>1-&tEW>?K=y^E(>T97Wf!5)a`dt4uw{KfU4jy|B2-Lu)T3=Bg?%ZC1#m)85@Kjbyd(dpQcgGL+ +E%Uh^xe}M@MM?^svQ&=+kadO11zRAQR$Y}~zdX!C~_FcZic`*@AlvCQLW6l3##S}icM^2ph+k4I@(^j@IgPW3QY1fq(ljW3ptQ?C?3^!1Ly`j9g}6Fs%*JiWFQy!7-`M*Df5P?dgkx(i;Q;Ef0(N(BF +n{lujO>L_XM)q@oQUP`AkM@(=7U)E?17KVZmr#oPrnkIOd5BRRH*a_ +BVBlza?M%-?X(1F$wC|Qq4*$w4<&sjOOMzKd$9!q!M3+c3FHgYF1Hv2Z!iaB!a?~^d-}MIsBO{q;|ph +O9ZdV$w!eLL*MvYL-Zlt*+Z8Tua2R68)UR30 +^~_Dq5A*E?K6P(fgCKs-TMuhuqq_DiY7d@q6AUFy?64jX1rm+7`Gp=DXOrQ=16OgC*Ptb6<0Xm@uSjT +m6r(hbenf{vOQ?%IR2GF9cW<^uhwX?=75j{9(679KsDE2Hh?Ek3O)B(=AU#ocP%&n%i}v+d4Q{nVrV& +AR~+zZ)|%+mpI4-1Z>e&AJ;3b$6<(v*3Zj^eG6qK5k)P=+Uzm6YlI_9UlUK_e`hHjHg8){mZ`z5q +emu)qYEJvb|kg;teFW!D2@9f$>SD4|W3(R|(=bNFYH1`(!;|?BCpYR4IPbmvJL@w0ew_OjJ`hz_=N~u +2{9Y4B4EA2zg$S-23U%b5ti1l4Gg7Qv06GuA|6hoQl64|YJ{Vi(Wh({=fPvGGdTmBA)`g`wXfYzS7s( +rBCc_ep3x7pFFn~$as-p-d0FRcccM+}u<+kMpEcbAy;lK|8My%?BMJCnPW$VCfgepeQgqc*sV?ce1$w +){a?w|9?fTWs8|P?PC_GB3WrJo~}k$3Xlo(lsjbMqQ&bPS>J;lg2{k8hC|22R0nHN*j6*2v!6CWtZz3 +yh5w0m{C0gi76hFcb&$!7IXl9Qd^|h4uO+qlwf-#ogqE1iM^!zJZgIuQe#?Y{0kS^9=Xo`p^=J0Ug;6az#;)1t1r7rwoAL +sX4DbW<4A1_#4>AqT?*dG_IS}%#X1`A!Z^+&SUs#xHfS<2_32Ys2UTL*SZ1qck6fXdDl7n{-oxn!x#0 +nFy7~FM&&+6*b*PvpaW?0<3TPoASJm4WV=3D`ia(wNGdAU$Iz!EBL%yUF9tpu3caC5uQOgt}HgE6z84kNJ<+6ee1&oX&2Lm;cwV +g`1`Txw2OIxH*Xbf*0m`nw~?mS77y;oD7Su9S1FfZF2PR((+LI@z&8B*Obc`s@aN**|WLWsH%%5=Biv +N#Nrhk#k7#O7zn8xqd(Z44k6p2s|0-d0xZueO%Fi3NS0l0$r2we%`b-#kfgoj`0B}%heu6XgJDa{lx| +^%W?VOB?Y{+#L}}joGT38?7fL(J)uzQnR^iN1E51+02S4rt1#-qI1m4iK51Ven+%)a>2wnFM&Gq%&L* +rthZkc+mQm;kY?0*g#4_!!X*FLXbbMrMTRNQ;I)BbF0PXAGRxRc=4Jy4^I3Umg;W@8mEkyu@U_d4CP) +`s#1`?Y~;ZRx_y?QYcv0bf*bfD1XZ&>hbS>jy@p1ovRQPraP-bybxhuc>^rD@PM@7(4*x^Lana)_iB2R7as-A^OEc8QREj$`)WxaBbc@e0^#J*D6bmR>IL#`J24>D8|+>J(L +{t2B!X`|JX9PIPGK>Izlmlyprd-Z22r_}VJ6(<2;WzO!+tlu3nCNOc^S9r^Q +@Nt(^WBswt?s9N^L+kLFCn}qpK}6sm(M+c-Ov7apU>7_|NHYX_3ORQ=ig9E0|XQR000O8ZKnQO@MJ@% +-3R~xR~G;PB>(^baA|NaUukZ1WpZv|Y%gqYV_|e@Z*FrhUu0=>baixTY;!JfdDU2LZ`-;R{_bBvC>SD +Vj*Mc!-Qd9+F529!Sl0qehjs`8ftDzntt@IJ701ik-@fPYMK4Z#@33z(f>O2)l_n87ECtA^15cmc2%`t!WK!A%w`wt<4%CGuGK*{ThQdv8eWfwMt@NHq6q&&URX4V6#8A#}&A1S=ejUl0Lj%|HNw}NXpxawX(f5Y^z9&Rjf0Dl@jV$S(ARf% +tl#~+Zjg)A%$v>cpwFY?OLl;ZAhZr?kwB!2LWxqh{8fisHWT*(L6}48nPvWiPA9qm8ubJbMz)f?(r-* +nw{K3lM2c})3^cauy-p2+gaP1)pEI&cGqn(_-ff6TfT&4OKD6e%<^}?{o$2!Jh4__iN`{;mZfjqi5snzUa%i6BDm?SaKGoCYs(FU1i`i1!l-ThsZ4w_N8^?4sL&7u<@rdr427$=CrW86K_JfMqh8r<9V$Z)Q +o$qCX{|`b_HqK@gO`+L3hBX7_JGYkRdBgX}xcO{0D@8SOp*J*jxk0Or&=vo}t)aB_ifvG*VC*2kukTb +KWB+FFDbLsL%WrC>?Ac9eRLxO~FL7qgR2$7Iur=$2PBIEzBviIlKM?>?!OWSwj5oef|?t()Z +s0KN=-GxMF}+AMG*qNn+7c3D~^-i=&uq0~eJSX5(+e&63i)u)7fova4GfAi*5gOA+8gy~&k3N +2gzBOyL_$@O)kt|?O2MYT=tgBsWxE0^{x=1~m6^GnX`rS@RdtC6)_n<;rZ?@0m<{k@R|>gbK2{|}K1NF+Rl1+Jz8GU?CX9upSKGMma3(#Aq{wRu}OHx<+6|kZc)SyHIO{hxuhqLQ|A2kkBKbgIv%$GysG^JVo652sli>740Q#{sxP~px0Bu14jicx*6t80 +#69jvHj%72O6PZ2S?7Yu?xd|Y6_xh!4TgBK5_$BG&T`!Ru3Yt&dXlZ{25y(0!XAMw_ +S~1-qo=U9|D}l~-q~U3M)id!UAqqZ~naF6;wEU|u!c|THbFypZ>{dzo$NtKM$O~`#7N4!osg>%4>`Dwhet}THp=~EIsgWfZ70OA)sbh`5`1Q +IHon^n@0JdjP(o<4n=_sL5i=;3SS4Dp}_5fR*sj+%2KCiOGQ*kmU-)b@=t0=+~17x+w39)2zAt`sM|B +QesRu2qSjGM~=OYU%vj$%%dh7?y*_qt~gtN7()P9$mxbiKl@LI$3FoZs5TRCN`fNQT9{TfWw;Uh+i2Tact^uL;E2i +G{k(71-hNedbg^}96T09oTM82k--<`u-$3~Tmo?wup6R8AF)dzX!3m*pX_qOzUH)bn>IUDXc+7ToUtj +RrT|}(79=B^Bu9eet91)UAPc#eAa|>AFTQ^W4wps$9{n+5Uc14#?1g{+O*1uT@`4j0VU&a!ByrHEmpq +Ys;6r6);FBwnd;f&9Pm(S2%JQJGw$gL9u!90VBLg{1gWy7a}d|tWgbAX2nzASc3Q5WiuOYr8J-mhE}` +*O@+$UUIFd?2WFy<#%6QzJqns|*Ge>iFN6&o&#?Tn7Reb^?Nah8^n| +Yv5&n`rWf0?YX;A@Mk<4t0(YTKfi?zDCm-6;ljLN9%iB=7jZ}%gUsy+Q(RU1Hnxrs7TenWfUltk4kV! +`jz~h{uAc$nV&qb~j145=_m~7dm-qYhBW}(|ptzAu?(Iq)>;R5dVQTpN`SXgsZJ5GvGP_Bmzu0 +jjZUVPCF4^2Io}QW9IELgnFEXId$^R8&%>pl@b8%?tbFbM53KwJ`ae)h0|XQR000O8ZKnQOf~Afk;|T +x&Bpv_&B>(^baA|NaUukZ1WpZv|Y%gqYV_|e@Z*FrhUvqhLV{dL|X=g5Qd9_(>Z`-;R{_bBvs2^f?in +4CNJ_N{s+%DbCZq@`%yKD#wnU?67jZ6xn6vycL-}fAnk|B$jAZ)d3DP8kgYGo!9|5>3Q9;A9`-)hsoGTZQFrqxnDOeRZ0E@hddQq`hTyhy}zCQ1w1?sKW0cp>xK +Y$ev>tzIvrvb)CYlM&UWGBqy>`-xvU2D{d|C`@83NQ->?_UDVgU!=cW{OdMm4u4FxsU<|8WK}o35bCK +FuNtMGY}%+?RB8X*>y<3H>9)#NTs?@i(z=e>gQ(Lr|DvmOEfs!0iON`oq)D}TPL~y5i;b=xClfNLsMu +L+*#xpz_Oof4DlALW*<><##op^$7^W4&xtBQ3Yqo4uW(}F(@h7#!F&bD-6k9f*(-Cs9ZC1R79<-PprDY|S^7)8)y +W@CRY*-0@0ec-K?Bhxp0leCXNxU+d$X-qT0sx5e@{qqR)KRWN9%-LE=dodIoO@&EZ4%pFPj2Z?Vk3CG24ivqF_r$oXYBaTY@u~=I$`*UqUh~PKr1r+c?tDl*7 +fPb8I2V#px7J1?q+<`(MxpZlSXoR@8TqiHq0?ELt?oVy;I=_4JS@q|0WQ`Vw9UnU}Xg%cxf@dv2 +Xkr64gQx`yHXYk?78A!aFyDHGw4+_Lk?e-0lg%`1ny3NQZuxD69_d|9-ZV4)lRmR +hfjGq3ZtER2`vkvULb#V{*KsY(Xftym(EP(+Nov~%~75OyT=|jkh-_ozzxL2Kyhj2zicJG7-q+9R<=l +DVJ1JbUt4D@Za@}}&R`<2@49^byJ_sVqBd>T6MH`&|3F6Xw+cMD!*{!lU5Bwtj7o&Q@0b7Yp7-vskXDn!6!;Wa94`F!V$IfjPp$n}^}__|*~Rt%r)z4x`wy%WzOOS_LgPh2JpI$ZZMed3i1CJQxdyJM +QBoMHN7C%i0$%ERTJAVH&MWvIun)7>di#hr+RPVLME_1|HZ5bkp_j$Ow7ZFj{}`r{tbJ;T9=(rB1Kk=XwIt8n!`@-roRUGUeGTIIoLBNZkOR&q)awWy4*_^0-hP;**rQ@3vdN|b#5V0(O!3 +N74|@0q)d={k&#j#Am@EA+hBr(IX_dKK>I?272a)Ie7!fN#5Tmn6ga(#trTkIPt!kH|Pz&~Ii_sUL;3 +9RYG#iLBPtm)bG3630Z4JOt?7B_EW=>~3d1#7^nPCNiD9qJrJm&{8gBA?q!hZfF?ntYT*ckhiXAm~Ll +~Hu8>*L{ygB4lK!*4@7G8YqaMqJW)%MaYOjvY3)w+q^$Il%!Qr)x&ztkvDN_FF8P_Zx&}$l<^~63ciz +d*hi(X`(ebf$Wz3>>An$9RO}YOasP**_bbz{-3~a{jFK-Z2gWBK=rPuU3BCe$vtF2?J+XmGg@D;laro +~fsM>ERq-A9*yove*67WOzE%FmS)r=!ho!fu85f!^7}qUJ=y)~U2K~Ob`=>IqQHr +mR$9hNjUhEKz#CQA#1W`>~FDhZ5iW|&>;21ct*D~+&m^8@J+&QWXX_&DBHPv8Zd&2eLID_d~G!#-A4v +x)1v*My_)T(a+-Ht!|x3JsO*(VpB(LON1xvR^8Whn{ZYd1ga^ZqRyase0^qCn7m#CACISB#zDkXnafUVtd=be}$NZBOameL +e79$8_e$*p9UKXxXx&$wl-T@@Pou)8q@RG^IUb);-5ds#f^h?HFWGYs#*h==SJ7qOkWY@Dc9|)tPT@` +YSQ&6p=#C{Q3Ob=yTk!Xetq|aBssEz6&`UD733^+#hci2f3<}Wo=q;|270iE6v--^tyf4)i~RBjHVuB +2e<-lMQWRHn?c%VHvfn%kv{l}Y{-tG{5^h=+k&sTrrmd4?wbf}+n8`?vp~h6>0iGd;tnP(-&^&qGfML +KUEwSCPFI<4KB;=0G^RQ!G{W%&LRLZ4JeBh*XKS8a-v(a~_;T0>qUE|D#qLl4^w|~~h;F2cToLMm;r{ +fEe|~anukuf>TnRdf_HT%WhZ@6aWAK2mo!S{# +p)^Ai#J8008z00018V003}la4%nJZggdGZeeUMY;R*>bZKvHb1!0Hb7d}Yd7W0edkvUs0UH4O +eM%e0Rb**I0|AEjsjnh7I8qV$fdLqwae}gu-UO0FS*67UBMorR)~eO8Du;sce7#Nl}54;4M)@^*%c=*b +moPq5h9iQtF+RZ1N`TMUE!1&Vont}921Y1ajo)Q{7Xm>#^UXpFg~%PYdhiNQ~dZ8#$aK?Z1fWxiW^%9 +TdrCs(iM|So@k|PB>B!SJPBiRUQ4#&e*4U+#-CyPYDha_S2S#u45`$-*cvW_%##DJ+!{`>>|HxkkI_xDkdeOw>_weAml`f8()};f3ML38U(I;IEzeF7f*-yYHVW^cuXczIsv@R2fbbL*4aFErbp +uF!%&Aa_xP}FYATV**8U;=GFDyFsqO!b~1)a85ga4|yy}Fyh!{55EtJr@ +|`05fSQD&dy8cE=?q^_?8UQPSlxr6BR>!4mHTZV6v&wt*1oPYXob8$WUGQasf9F2m108mQ<1QY-O00; +nWrv6$PE{`c!JX>N37a&BR4FKuOXVPs)+VJ}}_X>MtBUtcb8d9_$wZ`(E$ef +O^*yoWlSDj-eQ4MtNeS<@j{(*|3*Jp_SFOLWYI77db$=V3p7_wq}WWT$Dnt@_w9i@6lxzO0GyzE +E>}&RuqJ<)>0Wl>slIWxUBVP&R7O_;}Cq2F(=JjtDgqtTe@(mUpbKVD)A+8i5m1=%s8#RWzCv4n8P`V#N%tsG*Ys^6WK1oT +tvt7(+EnrihUQ&Jg%aX0v!Q4<~0cA{9JY-`y69Y1ycfX;}0T(#sS)8gXn>g3JI)d>Ux&NPGkxUq%^sdx!}2#IbylzS3}gMdeskp&> +>Gk66lyvTh-A3BI+`1?7c;`8*+VmV0V(q2w08w-uRysGnY`hCMy-<<$Y`1(b}d);%?e;DAIzSy +SlUUmv4evVmHv#g^!1ZRKf*cpi@{2BKXP%)+j1x +LGDDouds1T~B^t+cTx4%E<3jG|Yb96Nass3^Ty=ch+#IQ4Dp?wosWA;slw61{ef~axjSkJdry0Hm}DI +l*Rbw;9xJ3uN;tv4`gs?qoV?Q(kf4f#f&87Gb|A_7n9vko9!SkTeYLU@13m$k(5hT-UU#5ZQ}8C9{fv +b$hxc16jab~YK)o;_h#flY%^Oi0I(Nzw&JGf08}I|-GVQQnz4#iPT$`{V@*Ggn-d{yNBf;R6gSQV4!B +oX{D)A$urpMCm$JkhBHMY02qLil?O-JkzH62dEs0>}i6*vJt<+n=7!>O0G4MmiORB#N^$sFe5baQL(R +fs{UX|EXppq(CKS>L9LlulHn%5=69^@`l>Qu2}B}c?}C+otn8__Xt2A0FSy4NwzJ9=}*+8Jz;7^hm+$ +K#J0nZ+c!GzFxhhH{E{mI4IG)&3Y&(F5ez3YB8w5J`1IMK`-jxvhYW;4`ym;#qD`=Ii!=+-tM76>$vH3vPZmsGLD77ne1mP%@zBZbi23 +CY9VudO@@EcXFVQs7g-F(^GH0&4vl%Ke7?V_#06gQz +o>^6H;@vzF))BKo=Oho?H%X64UONOLd(qQQ>N@iIc;D3YVwUU3r5rPjwdWnlKCCnanB=pfwi3><|xwF3FBYTUh4anPkYr^=u;DSXe-O>(@v(e)l> +XTH*SqOcT5Y>Odv3sjl?Q=V3+j*iUvjGsZF1b5^`pJL7Bxo;iycvU8YLm|JpH1+h07}(8t2zVX@aLXW +a(Q-1UVisOo<`7;@z#HeZ*hyP)z_g;>HMo}6wQBWiALNo{0Lq@6aWA +K2mo!S{#x>KZh`s(005T?0018V003}la4%nJZggdGZeeUMZDn*}WMOn+FJEeNZ*DGdd5u<4Z`(Kwe)q +2s+=tnK?c&~w06idBQfzn4HmvEr6oH}GbgadebdsFbxV!&;lx#PNoeUFDG$yG}B*l-^4OzY!MKc3ADU +_&5X^hmf+uI#8WwT8(QQyj<7DBxSv8emvV!WVma_c~ +FoDB}kIV7l+9y0Z6?Wx(~+$zC2W-=xC2xs;>9E8GSjDatU9o~+pFkR?IpLqZndbuOtc%%^KvN24=s3#RSN+|T;(&OjS5NpyZt|N7z&W?o +H%t-iC0iTm?)4H%^>h&g!EHC?BQy?ON~e^YJk6I19Kf@&NB0YxRE98rbG9AAB*$=wvP)Bpq%whhx|K@$}UzArWx0Uatx)6dh_sTVOLf*e6MX +Y*__FduVmX|W+MU|dz$M>{Jg&fCXkhxe*a;eT@G`s38?#8%O{72SW;E{4 +crHZJ+wpU0Xt>1fB=M=%&K+nEM*X>IhB-QqW++p=(6!P#c#jjf5%U^=TnX +KOQI4J~EPiw_$(;|;tE_t4^5Bi4qefbP_Dlmo3AaMVUBRM-bB3pelc@pO$zbO>EUhu*tn2PUOe)t_tc +5yvrNvmM9x!WrC=E`W{jf=B4U__+Rv9{%U}7^}+$7QucPWz&MYA@j^t=T?{!^N9mB)*z{(_WeV7PPD{ +Mc9dN}u%j7^$1xrZwr?`DBUUlfl2?2<5rNlepAyL>!uyeLGuU0k#kw9c@^VP3=FTA%*MEWe)3SUC-jB`bsU>ITkn5RO9 +KQH000080BxrJTD6{K9VHq708C>503HAU0B~t=FJEbHbY*gGVQepLWprU=VRT_HVqtS-E^v9(JZp2?M +v~wCE2iK}ne>rxvbV|Qn9|*8V>zcizfP8u)T(sgkQh>kKmdyYO>-UJZ@=!IHy)H^IoVoO9Fo9HPft(3 +r)O4mxnW7N+BVx-BnitlRarNT7e(3dCMyg1;DLU&l<&+}vEf-BZ-kV5EzHB!wpcc0nM?EF7g-kOdtR> +BS+O=hE8bk?*~0v+Es(!u)hf%ygB9YnEc09}35%F7mIk@+?c)i2FB+Z|qMoqRM$~+f3;nGrYXzm*S1_ +{aJbJ+3@ATD#K97sN`tp)*M0&ohpgaA3U6)M>>ZebGtN=0SE3g2FhsFlCwW#}8<^&Ig7T7Jw?ytG{Tc?c$)pvPOeR +R+4FS`m+y;==9;`0QX2L|gjx7|_BvYeZ++Z5vS;cFf&5i#Aso%(jFngHcDF%I0&vki<6Q`J91nMqu+fVhv_Ymiaq@ZzgQ9ZCF!h&=qjpmG!lPUD@f)6 +e3^6>?c~V`JC_rs6Z^7KDD3E;~2q#B}9ct^!V$R=ZtU4ZJ~%n;EXtsvYF43VDtHREL+(hpwhtos2X4q +8iDbu0>gzCUX4QQBzUT*_=lMN88inC+3XDUbG>H3?)0gcjW7q-N7MM?%S(22#G*KkoR|V8NJLsmAU=L +xOuy=BXhe?v^AQ{2tI62D1G6Q1S{^BIGl1us5>7`NqpxP_jbaTH+mThUxSEd|=5Vb1+B@GcKuf)bb6wkRuBkNo0T6# +-`njE%uG@S|AiOgG5XB7_8AR=qR;b-HX_b(oBR0$}nff)gmlBx}T#Cg8xAKq)6?|P>H=hdCk80`dgR< +TZ;l6ca&w$B6$Vcw%kQDE-M#^J&XwYAG}x#cp(%_Fb#}UmqHKKP6^5@Eq6r@6F``#EVo%s-w|`ZY)}U +g_`PVlT+&19^P6Ve0^t_p8LAlkN~juoD*Z?r+KkqL5w09cuQ4k6jn?o8t`-$g0kQk+Y`VJ=B3JTCUuU +55;bX!qN8M9!*I)|^1-rWzddOp>{u1<`P)|JPxkS;Z#V$Nh;< +IW$gFt9D*FkLjC!YQ#s}O-Xvk?+<{{n4b4Xhb=uy|X@ZH3;oFqoYB1H(FVMtEYVtPC{x_2?Lhv}ID1U +vF&PojHMg&IB1MNrs$OWrtSL2Yesbd`C?Bk-6VGi2TS#>X`}oK8_=Q7y$b@Mu~=8Z!>H#1pAf3)PO^O +!Vs;?qCj_K1XMJ@tRD9qBOv4tiWRUO`UizrVsfO;a&Ra}2|(P|hz)s8;JIYQG8{lUZO;qj4{tP90y;I +5<+fg8R7^IB*NY4cG&l#q2k>io1=1tYR^gQ9bI)LKh_Cx51Y*R|qs)rw1{|}%PChMZ9)7++X&E(5(pu +U!`W}eyJ;uPf7G9(EKS#fGQRdl_p?3j^QZTz%C +EyWBF!31DkVk<-+Hc_855As_uAdm~WJXc0k3FJKB~3xqL`~U9#j(lIsi0BgrBQCbINsORNNkGf +%Y>3uYI!I0yY0WE~(rbzzB-bPH!Ix72e!$MNlaF?3e`1M!~Ds-)na=yd^%EboxMF}Y|q0&Rowezx4^yq<&!3!NoM$Dd7!W88N8<$rE) +)*fGc0rUM8DR|k=;J{+E;+!|xCW9#{{WqWcE68v56oMGe^BA02|3q_+i6U;YXfBvF(xK;DAk1r>Fw*S +2KrX>>0C#T;xIDW?M)+WzxhEg2S2q1%oUt%y{s(g{l+X3sI~YzsE%phHKxuf#q>Hl5+YTzBW@A2|2b? +Y-G8#=`!0H5Q;J$_+!u}DgYUFjtbpxU;ub=^#Epk27mY>sLKrb|I;Nq@9HIROQkX@zk5>Exes!$%+#l +7q&2-F1S2i)Q!Q)`CA4KPxVfatdHS+{>Rf)S065j^Zm>)Dr~b=0Qj83(_hZGVCsTmWN?ehJdxHVnYQ0 +a&l^zx3dAfO`U<+7`-~m#j!LbQF8^$d3DvV2wv*4oZV*nRW|Z0*->qz1fZEUxGIEhX{WMnm=Se|NQfi +Ps=eHsxV8wUQgWUS4a}zC7$J|fS-84WQlqLbxW-@g}0XE!(NHywKA65)hc_B%7CK^$;egSaWA~?V+Y3 +xx<6%#0lmRc)iDIhVB&-fxg$aL`b^rvv*tRt-cb*q#x}~Fca;M@2i63Z!o5esRA@%^Hi6O_Q~hEp-kT +i8bi)g9ZPLJ!fT9&DBS_N~h1}}c-KKJtiXqPM{4S@_{8S>anYD-{LIH79u7wy01$sh!%iG?ZpKi6m(P +QG^$2##x2}~(sc8udmAxJs)S%INk^KY-%`ntuUNF}b*D6(XU#-B|~0wHgsBuOSzy!3e`Aqw^wqfR_-n +V*}cQQ$u5ybLlN>KMy`%6YKB!o7q}Fu-4vS)PTRk>y$imN|3V!1QM{?Du=k@5F(5)P5<>*|2p85|NS}A67~0DN$bBRVa-8Ua&y1(1EmH +XY+ZtLFQ8t!^z&6Vct*Bd`G6Pir5sFH1JOs7gCLBeX*6L`5)G2VC6pWmY%y!mQ@6>KmK*Gg}$wG@{(_cK)+Fr42*kE +ehL)Jtz+naZR}M~OHz8Y)zD8SERzT%P49~t@dwX6O&T!aqoGnPFWaWF8@N>heD|!}7K-C~M +VZn)gIMTBmY6Z6$8bs`jbLZkQ2^jANHys9rvm@4tHAvC2rB;bn^!LfTMSo|nHFPoBwG1JRTmZScnz`5 +I>d=+PcSfVLMr?Viss;B2LGZ-r}^RfH~)`TD^zFQMy*HV!G*cOg4Y9jp|g&*1MyIosiM=DZ_baOKR-E +(x(oX1jUqVv5pxRqd#5C^-%>;o#s$wHR)8QDv&6)Bs;ISq>s}vel6tYrslSv-@m8S#ZJdTm=dC5GR^6&RCZ&*jT2rW)O&(!v?G!3A#n(Pz-bAkw?2og$YW2MbB5674uaQzR?G;O_uY +q<&BFRX->70A#{!(T7$HhL>mz0V5<{OG+^9(n*Fc_JFWz5iyP+HoS0ot9+d#o(t@{hNyhW_r1it>@%O5L;~eOZ +#e0G?tDq_GvWsY%peiZB#88-F839qESh4|a}g0U=Kt{;<1k@)pBc7>70GsfIC)TLnNtdR=kM%ab2Z&Q +No2GHLRQRH!;**Fvd!oNm_8Uh-AB^Wfn^PuL2KFQpyLF^=n^+!Zd###Hj>W)k73(WEb?9j28Q;-uPxZ +{V%Bf8=g}cBZm^`kMM4B4gma&=keKZE-0LXrjHeO(z +tz6l{pbPAyE#9u8QRBzrZ&DZ&sCqhgDLzK@9T>u)re +{g!s6VBxoP8>`BQOn|8#a*ZP3Y7q!|=bc@qw0ZhjnO6+K$lQ6+JVWKyuGC)%!0Pwc$hzmRqBZ_6F-S3 +Gy${}ao!CCUcVOpbc=oWeBRYRlm)Pt%DV}_HHSZvZQH3_O6C$?%s9U(Ay0cba>B`lVcgJhX_U|7QX*! +}h{BQ8aUdLex}!OI!nu>jA0wK=4(5>R`iaH62RE-u@sfzEoDrQi>$rR>2h+;#|&JfTj^0Kskj-(&q=t +MTNN#=q49dj5z}V_NSRo+A%g2kGKHuz#Ztlq`fPFhh@)f=}=43rIJeGx_%GiQDnBRcdO4g+R-7#fy}R +C@QoQ;8HbM)9P)!%{x-|sV9OclF-nWIj?bf6@^qurwUjHLg`$X;!^syhokmYjwH1`0^4|5*_9#cgQG9`Hrv`^{A6T9p93WstoFL6SC(#=x3p-I@Suhxgt +7L6hF77el3Xqe~8u{o;`@2zm*K#V|9?#0)Cvy$F~J#SUG5U_@H($|7xs8pR}+ET>;Am;ozMVT#NZ{% +kY>Qrv}1S5vru1e&5M!TV^D?wv7HUaO{>|80T`8*62V;V*mHhM)6J;PX75Glhr_Dpd{Y%x!$ +%1-r+y;5Z|Ej}c(BW=xyK9o+U5Epw7@gN>1Mwvl0PiuvXp|^>oIn&Ih+i|mb3R#s7q}YrHqubLIn=s3N2wVg|V2b#Ms+m(ub_2x&44L%aq7;x{J_qhre`5`A8L`K} +0!K&P>I#dhF6b>PW4{(!ry8Ht+nm2hdf67>%2)O}N(ym-a{#4FI?l)VkCo9b4JBBJZv{5>ax(l8K|GH^1ltAd#`8!@r|w7IhB~^&-2^M4q;;MDQfw2nW$I^O43uA#k +KVI@-!+g9n4NPT2T^XmnKW4$}ANB%hxtA*P1_QhbP_j{&pT%{@>5_Uh3V>llXZTn#l+H;vHy?m05*1* +7?Bb!!oN7r^fl^E4gx451c@UJR1^uCY22J%~Gf00-B>Q-CE_y$e%?qx~7}@JV-Rj#IUEVd#ztO5T6@t +!H}MsyAlO)#;i;ONbszV{|cAA$jiokTRyq<6(VEHKQ=Q_Z+K6ExaA}+NpyXe(5#z%lDI^v20q?dQytC +#5H6L)!|1M^lTe4Qdy%YrQ-88ZOAZ!P1MKlwO$~ntj?t8A_0UI7Vs6MH){cDpXLqvMNykbLqjROw2CxAJQn@PzGELWfudUst~Fd6jHm7TcN$en*T~wefB +RQgBDbQv<1}^J&3Aez`b3$~_fz;AbaAV63EBFl6lRN0k;g$Hwm7_BkN2F~Hltm%M!^I&^t~CmFcH~@o +i%5D_-V6#5VO`0h?+KD!9{k~)wkEJ9T!0apyN7Jg+WBC4g@rHi5dTf)`TGRUe^Nu_%?6wt`>VGOJ{8S +yT@%Xl!{Qd!O+(kg|dEv;hsa&g?X@oJ{dIgAHHy=FTe2vT`dzvI%?NN$G*V@ZwGE>&{xT9_@3!v6_+c +B>Ak(#NJ-OXE5oy{j$QR>?y|hzR`*XK`zZIs%55rL9?bvTh7^7>-t9eR4pTZJx?K9F*gQ1(SK}v7pX4g^7AqQiY>+Vki8frmA*5J!f=~YGZzx6{m&LGd!1~UHDs@|>XBr}= +`VZ<^gI`v>Zz?o%mswRSl*6DPuo{#=%eBP$D1lI$?x>fQ<>`MZKcm7!gT9x)oAH#!BTh!r%*td;h959 +Q34tCtryyD4}(elbq(f^Jvj;DY5a{6zVznU-lGft+LkIl33KlPOJ(+v(c73r^ +XzLrOT!s!n$U!9$NfBfd;P}#mVnB94FE!|Cr6YwuO#iRjw@p@}KV4W`{8+{?iJ#)MD{A@#eSw*MGoss +|ut0loMXCDfQm(l|hWPhnUXUZbq&!_+wH~_1{T?2WYRWo-@wb^+0ZVPs8vtpTVQ(>=xkpnR8`*2{|3v +#WfR(hzxT~yMUt_LFvj~lVKv<~p!f@4;^EQ~Ji>PJ-Q>RhnX*;bcXc%`~rI`)qIYKTMYW#E#2tk+vI4 +1OB*F|ht)eGMty)iI2qxv&e1m-MY5-i|=A%GTRjAJY`?X$kFBEOfh%a>u%N+WE|Lx>CmpM!Gc~+k_Xk +&fh-_8gid;*jq;zz^kMhv%wEm7$RjoaVtS-3K49 +&Pq^kt6Dku$eD>Z=`Zdxf_e{F%MjN1;Lg*Epz~5b-ukeHG#)M5`yL54QN}N_D-L|A{kDlx@8q1*2ewD +=&BO?wdazq1?F1y`XD{?mjWR%DEPN1EToqf^M_DyvtmJ1_}~cSp4!E5JCa&A;bRzP)h>@6aWAK2mo!S +{#u!AD5K8{001y4001Wd003}la4%nJZggdGZeeUMZDn*}WMOn+FK}yTUvg!0Z*_8GWpgfYd9504Z`-) +}yMG0tVw-eub-S+yGOTxPhhR%r?79_L5Gb@nJ6vQ@Bc=G9uK#_{^N^HCNp^ePf})Wq@_Bz}c3K@+QSA +Dz*PN^XDqGoot!(q+Ni>zxeslY{FSIbK* +QGGCUBFTK$dQX&i+}f0i-Tx7;}8FBT)T9%?_@okd8hP&*Ya6ZNa325yituTX{l&n=@O(Tp$&+{!}^DP +{S_xjLyQ~yP`Iz5cw_@IZg`!GUrN!EUi{(f<_XBGK0XLhuYr+9+=z}>yyI`QMy33{F@1|$h269SmV}X +u{l2JW^XNhQ_oC(9LxIPNT9ssZ=Ybrg8SJH!rmgvj&aYYHg-q|~Asms)NH)dMa;F8a>~nE&OcrFc7Ff +RJAjinyeuS0R(sWvGd$QYM^-a_1(|ZND!?h==RfDEu7q6S0dXL1@o0TydZc*^M29LaAx5>L#H*bI* +RbEjVO=*j;-K`&n~Q~#B+0d3d9T%ztbpk5fp;W)!@S%X%H1%MI|oHmRN(rQuL#VC2Wc3ujbaDh2B#`f +stUtGV91?fz$f!oFn+=iB63fb1zd?8ghbiMt|(F?>fL$(zVY$r7T8>~?Wq&yZq2N{4ARNi%Ri8tSbp& +b!Q_KQf%yI7WXA{z_KYthVGrC4U>tEV-mOygh*t{;>J$h9%6>X53lJn#SsioA_n||z3TU9-e4aBDdl|)L4$T5W%oeKN=vYIrgrp(Mw7?XaS1*AopUsb$+aAogYSBMa6*QakHcErgjN+=SsVorYx>!WNtxQb@O% +$Tm88&YkffxGDY`zPzw;3I4UJ?Q0I^#%lg7>(d&e5Pwn<78IWw7=(K`V~hm#2jHrJ67UbGy>xxbCzKT +8mrnEIV1v^brKUv(>V*A=m;u7^msQ6fTdi85yVEG62qH;}~ZZ^dpBQj^Z%M;yEs5ZewAttd>FN+Z*e&pLjBoa}7L!^@${H5m8P1S`px5^AR;i>-0_9VN22(wYcWU{d#J#H2gnN?Npn*Oo)fb3S9_8G6Ee!L_lQ_gsTDBSPEV +FhivLe2nP>jUvi(Qz%EJ!)fqHdV`4_9n%Us6Z)3}^aR)*fbH|t>0!D&QUvSR>9oC(*Xq4Q%`S9xXs}H +Y|&780i_2;RazpUf9Eb+b2mFI08eI9f%Ews~!{PiH3EIuFUID8qmeY!&I~!ki$tTb3=kJsGvC$4)@$F=n|H!L<%JwZhE)?IkZVsFO)C%8Qr4yn} +KVphwT5zAmFssag9a~XDv%`1;Vj&WkYtfDO7EK0Wgf>^kpYLUq?=XSK@Bu +aRnEYb4cJp%i>mlK48tJj>e39y_9dDhi`d=3hW)9{qFMs5LBjQArQdb-Y_#h_#&&X|LU^(kkCM_sx90 +^24*0FwL0%ESm$kD!(Fq!3jacH&4Z3DFYxYkc;$RSyR5=j{EEH +nYK>R^vB*r@a71m2(pNjnG5sdo>6Z3Q^2Y&xw>YbU)puaf1O0W6%>5tgkG4XYHk0v$F~)FHt}e3yryJ +UlmyK#`<@hzrj|9tUCoxfus0~gvoS*MkzL +r1t@2;j>G~rNr0AeJ{8?(S&L-lO7?++FeaiOonx7dU@3i~=Yp5LRee*D@uqRCb!DlKiWc;IR20(Tk1V +$97VGR5AQ%MAvzmoB(UrhSbJ60cd8#3tfW;H^Kv@_HXxO}F2to=o>GzR23mp3RG|K_o!K4%6Ar#xu1a +u%E2Xpz9*C5oCYdZjUx(tA1cpuhD$^*&OLOEuwEf};9l|gR%|IER@qW{N5;`)mI4=ix3HRYkLM@dE(c +KMC{J+g!IZvVU?rJA``H)swtdku4k7Fezsb{|=2b{}#2L4$BGq~Axzmfbt6u-NiNdL#x6N?c7DhQ#$( +)Oiz)4Z?AbH@0pU>Vny!_kaG=%jk6@MR!(?&V?j@>z+=2!KzB=Tckyg>LhRjCut6+#*XJhnbi@MoC8&e6kOtxcYJQ`zKy&^mP +6l;3Txky1r0L`i_WZ5D(M<0IJ}UhP(0>-)*&Bf)it%VJj7HoY7S2V6@kc<5lI^KsZpH$#>#f3N)=bY|a8?s)hUggCVQ1!#DQG|n +iI1lq9`5By1j*whYaiB*N!HYp+FO{#yvekN_rDcKCtj +o1pCdGQa8>Y4>X!-wS1l3uXBg%l~KOst`uudvj?i=`cU!Auf-&t;PbkjTmpa{tglBi};)w7DbS=Qt*7 +*h%$%o&i2(|6&OcA7u&IN-suJh7sn1q!WO{NeS_5rO&I#dQ{eC-Tb>l +K9RmVCs3+li!gzoY$j5u=PHNEc0CmlbVn+e|KcSe1tKbWOEIV+F{tyOfd~6h?6TVeYLd(u7Vx-a#nc< +8^mXAPhZ>58QrZ2($&91NO(@2`IP>g_{tfEm05U4JUQ(mx-sPU@#T}QX=$TDwEe(u0@6aWAK2mo!S{#toKX +s%%Z004&o001li003}la4%nJZggdGZeeUMZDn*}WMOn+FKKOXZ*p{OX<{#5UukY>bYEXCaCu8B%Fk8M +i%-ccE-6;X%q_?-Dp7C&a*8sON=q{H^SII=3R3gR;PS3{Wtm0!dAX^1C0tzb@rgM(@$m|_3eie_j=ru ++Itoh227vfT8kAzWxIoH)>XjhID+Ca&&BIVlQ7~Z*6d4bS`jtl~&ts+%^z>*H;Yo#o9m?67$3GTCKf7T*I(;c{ww4=FALRXFIB@b_j!$RYj`ntqUZKu|Wi7jnA@iE +&bDKQ+6_lMg%eKSf90w6ZX9_cN4~y*%x%9BUsgM58tckl)c4{J*UBL%?WfDv)^$Rw +PqMido#flb8db-o%3vwWvRVu8>8)tqkN)I;2|dW4owIIzkr93x*&;e+RH&-lOWhXKC?76P1XKQm4W~8 +(M_tgXf}9lInhV16n$Ew6PevwK{$aF|!RRt#9&e<(*h(L=g*Sj3or$RUdH{72c`($j5EV_HkuvmoHur +_{&kGY@)~f{-UfzhX%_r7J7FMg(74_p9g{=s*6HeK|Q;*|hH~+{wc&e>w!F!p!{bQ><$w!#PpWT~mg7djJM$cugWPC$@ULOM=xF2&%$6)e@zuafthO?e(QQII~bHrRC&Js8d@(Ljveq +OX}l0LV~jf)P&%!xw5?+=$eV8#&XFJJ2hlgmL6p|*$85d3`tkRRlrt +x_0>RM5b8i6AJGB#mby4-B5JNH{OAa`~3CL$@dXzeYV)?!xZ%?URag{jsjQyV!DIa?aGC7>t6bX1TY_ +zINdJ7Su>7DWAMkb*H$?7In0?QAlYYhn&5fH{9oQ4^SVkWV(s-dmeaf8KoCm)=G5!QJp%zjj5N$((pc +F+(k0MIp)e_D0QFeeeu+YaDjf6(Nj_aN&WL*5j$iu|pLn8isIP?k?ExPtF!7YT42@XJTXk={;pDl?xxmKQf3URd^>SnWJT#7-zJHd^wyxY>s-sQ<2f^O`WHGz!#}^fzSfW +CRPsAu1N7d=z!i`#5#dPK0|4-dU@gft?B>L|lQJIKtpUfuAwf*{IlWc>5nvO9KQH000080BxrJS~|I +kZsG|507D=E04e|g0B~t=FJEbHbY*gGVQepLWprU=VRT_HX>D+Ca&&BIVlQ80X>)XQE^v9p8C`GOxbf +Y;f{-5~;bf(0dq8gi7xi6|!f2B-T3-*SyO6mO<>gr`Dk7E5+Vy|$%utj>Noyxh^@Dds4d?rNi$?8Oni +j2T8g>(1#AnQVw;-o-=nJP<>5 +&DF0GDr2N{y1irMl^g=itd|HP0T*l%>F4pwW7*8Y)U%>S3vXc^1eGa2ZVO-!V?C68P_I0iB2p; +E$bxxEUH{JNzJo+eg_wl2C&OU>_laH%>Nk9p1ax_S@w56s%FPa`5^L-dd7}m6+I~s9w`0L&1)8mRnTRi$sL2}c@@4fI*vx<33Q@-FmrA!ao(p@jgu|IX*>E81>HJ +4Hc`LPPg7obzz}+8)L5tdj1nUK_!4`#p8)ODRY!vLwya4uqL&bI=hopM!4G-nq!L>`O}wpHp7t& +IM9L4onsbg|XaINStIP*V?LmY_D)0pLsrT{7`cO&1s&%jKAGD523n3fA+NV`#+HDL+LQ-Uj2`24!n58|47^Jr2qD{voju +bAuEvnJbL0>5rO;NxX3I_D>|;5|MFA;aR??)Y^(p+*qAcS636-f=F5}*G$vnA&DsiQ>{6ZecHV2@y9& +V9Sl`12tR&4neK}YQD&yXN1vGN#5$p1Avw+#62JnR7CJzLTKLM{wi+p4^mmHfSPkU`$8)C1!W#mZHvq +%-;O5(8+(z@Wp?AY*?_63t>~3B&|4j}F=GLUTqv_TcRMo99+KF(-}9|S9V6nSGVfJeZBo_JA!UUAen_c +{y6i3&`bulr9gTxDiQ2r9|(He@3p&P@;I%3{w-VmQ^;+=$HX|Op +#|1Wb%2FB;16PygrEpEC7gWlK&@J^63>`pbqKgn(iEmXs$Qv63zz>UbEoCyX%X$7uOfTGIT=Xq@?C>rvGT9tDj&*Hcguu>uEJku2ih4BZnz +s50yNFQV2O`saSu#fBFnMM?31KOW-l(h8Mll_MjA-^mWzE-OR7v$6aVvMX_V-<~pwX#yi6L2z|B(I#S +B4bxyp#-8IP_m&rVKyTa_+_#b)Rb@B^^*%pcMmLv4hb{)B$p~P{;NjuJi;DENlwtQd%XmK2fHvtb91*VJ`*bECSZ1cc +InL40r|0dkH}q|kDxbqR8j?oSnFfzB)|0&XlJ3wBG1XZkYg^m(z<)nuViP{D=@+cO>LQ_cWnS+2L9?K +Mqkx4-8VbEg?h+>M?yw)-?&)1B51)qi3#v}3(y78_KNJ)dv4#gAn-8U +Y*I*eqn;%jf}CB`*LS7l-;Im8|-(URK@O%;qG*;bbMFf98S@i(aT6wX1%Ob{h*2{1u@kOJ@!Hd?V9AC +ebb$n(jr2k8gmqfRtdjJvd)ww|5lC!X73ns}warF}}h&(s$;rMU6;`kq_P`yTM30hZa`3aTd4pmbeGn +Dcye$uq$<=4-U*>{}z;xeok#zh)&L?B|%-_gML_4$v@F><`JC-+dP^@Q*Wk$#EXNqMsGS2ls;Oeo1Tb +xgq;di^LAakfe;tREd^q!3P@CR?s=(G6;R2Yfve;`JP(toz!}?5IqGLgS%zu;?UhREqd8r&?*}Tm8@) +Oc>omwdoMtNwV~#RQt>>D685Xl9gtPF;h!w02{E4AJvQYKuoAZBz(l7xaGC1KL +J?wqt`6Jz-%@Dt?{W&(k0N_`~NxTR9Q}Z~o+OwrI +Apq?RBv*b8AK9eIIa=Z&0|1xf)&Fh6N*J8gJ`e46xxVSDb@)5b?vCo9V_|_yL~IOpQKKaw5-8xHzMOL +B&1#{5%*AA|@S|^X)m5CICx +zytx<6eB!YM0wDbLU)f!?ilvYpup|Ozl@`)NI9MYHo}zEU4plW=LfN@NWXqs^AZ>N-@hp}90Z>Z=1QY +-O00;nWrv6$D2vauy1^@un6aWAz0001RX>c!JX>N37a&BR4FKuOXVPs)+VJ~TIaBp&SY-wUIUuAA~b1 +rasrC9B5+cp&a@2B8A6qNy&56~gQHY~*oZ0WET=&&FVXz9x4LW`P6C3Uff-?^l|rDhoxAc-uIhxg;$b +FW;~_AJu0Xna#snhJfcZ0&_AOY4=_wsf0KdoTVo`XJBbRj$11o)~*P>hjoqw643Vls{oZ_t9PYC!cjy +XhWNV&(Ewel!eozI%J&@Z#~t@+j=kF(_amhS-ad{DxB7>dROsAWj66)r7M{}P?_6WRw}z!Y?*AlHm=) +|Yy^JZmJiD4{Nstpyn!jDI@3Npxv}*bk3Lg=)8ETfX-l1j+v%{hQnjP>kulPu2ic8n(Iwn~eSOsAK{@ +Jd`2p8IYUgWxXxNW^xN}`T=-QTND*YN#@J8}^2nF?ief;VA`seie_Yc=M@87<=X^oI+X3t2(aAN*TYe +_upS0rgKj^xuEhNQix(arEcmxWD@%>pAjSYF+qJ@#RP9)Asm!fLw%7Ur35dNf{i1@g`{xh8|DnS&sDf21JOTpf_y>K?*+0tC_XL=aJM6W0ZNG(WZClTwF0)Q +0XbDljP5B5VN@)!&l(EvZ%lpH8i4VRN=NC(KugDvi*69+n}2W{)!9&(n4oadH>!^1;|Bf7#CmG!7Jtq +d%UaB6042~!>sx~is(a+ac?Z5#^wKy|Hi>P+#CQ~&PjcMb44bk#Vu;CHtw +LPcr6M0?(FYJVfw +7>`0mTn*hWTY#L3S0~1|ov_^9MYK@r&=Kv^G;w5#P}nz!&A@xUEz>}$AQJ91C-inI$9FIWcaz-+w*aq +G*k%IqUMv-)YbCcMukMDE+uE2CnX|42so}tM>%SK7@4-oyuF#1}7YHDN8c6Y8bKX_Y=jrn{{Ub1C9@W +K(pa~+$n~+?|;e|!WEDYCRJo!w5%o?gI+0s`m%L1&aFC#E_vW7jCpyD1reDonGIRj$AE-0J5W+Ir7wly(EPwvv?XN=B*-1Z;9G|tI=FGEESbKH*LM0**v#oBXto_ +Qa{?j2wol=Xx!zM72|4oU9LF74L*gG)@t<(93(5=?^n=w(`2WHjw-vPlxvSpN$&q<|2Ni_ar +o(Yd}VZbFL=~)VTfoau?0ss`LsrlR*erdrp7nI@`gu^Nh6Fg5jM1i6OYT-uic0t23_8X6U23xa4^htQ +xH=q4*B|^jrJEA3@CIB#e=B4qI2bk9s!@IHG1$dSb+dQQB*WNsmWRMz@6i$I31G>F9-X$w6TB%rgxD^ +Srbnn544k$e7kmPR1Anej7IrjU6%oHr=JnmODC9l)xBj(X7Oku9+d|-!*tt^D|q)li_aT^% +4vJc2%opeo6=uhISE3uVbfVR)nKz@B`VQfWZvf~sKE2569Ie44!h2(HnSj)C{`7D;g?N!bF1)0Fkl0N +w)Y@ck4!0?4HFkfu9yM@jpego7q9cwp?!~sT!<|_Zh9L^WX0J@tV`X5wrcFt~T-g7mFO&Z>$Mla#X?4 +`UtV=>f52+XbB7Z$fT^6rbHZX9tUfn5}j@50#_OiniM&%m$3z?x*n1+1Ahi#JECOWY;Cite7k13`#nV|5QCNI`HJh@|%4esjFB4D3rmPm|_#HU}FC-3~dzQR +~rcB;I=JsCAJm@zs$P+}ZwSFJzy7>oCO9KQH000080BxrJTAxyjqdEWp06qW!03ZMW0B~t=FJEbHbY* +gGVQepLZ)9a`b1z?CX>MtBUtcb8c~eqSa#SctOwLYBPgN+%NGwrE&d)1J%*-oR$jwhl%_)Wm7Nr&xr5 +2~=l_&slW=Uphv7VBW5*GkaO9KQH000080BxrJTFc*SJ4XQk0Pz9<03iSX0B~t=FJEbHbY*gGVQepLZ +)9a`b1!3IZe(d>VRU6KaCwzc%TB{E5WM><7M~ia_yZypA#p-N91!AykhLV6%G9wVd)ou3|BjtFNx2nY +ypNgP8M_OZ=1QY-O00;nWrv6%+kdbzj2LJ$@82|tx0001RX>c!J +X>N37a&BR4FKusRWo&aVWNC6`V{~72a%?Ved9_$=kJ~m9{;praQwyBcdX41E0X5(@i)QZ-pb3hFvj?2 +xQ@9ef=B+IGNXdzByZ`-WhA$#zJ4w^6KkV{lIPZLBs5DJ~z512??fJ7bO_QWj?Uv3rfECg$+po+V%Pqo+s2KRaHC$;wY;6}xYELKE84mi1)ZJo+rqJY)d|IKYfDt}ju$o8T4+b$mkdRB*v +aP3ZC*BqDf=0BZ@8MWp!uq{9%o))E3*Oh=oGsaJ>`mJUFb4l68N2 +w=FB++kZT}D*+w-~?%%p&%B;o1+=~lA6P_jCBsU&ZPIf|!cg<=^bC6f&(ON;nZ0ur}zn$WTlv0&v2j; +!5rI;v%(JKhvxLPR4Qvw$s^TAX*Or^8>BJ`lIRs7JQpn)OX&1FKra9zc~xHt`z|k>JNH^&GP#FpKyGv +C5VBZx2e9e_qh$kq;?B$`M=Q<{4$q!{&_CLD0XzmEb+{&9>JaxNk2QH=NS*o)8~-(bb0>?RwxPYzB_z@pB!4TqOHjSvgFV3Cu1mly(E?YQ2GAEA-LKayvIInAs>$Iq= +vxyei9Hr@z@yKNcgU_lnsa)D=LH>oBv3XU9nxe>ucT#dp0az +WpUvO_ohQa=$L?DKFzzT;mqwg`dsR5+WqbJ-F7E6af@ooHuUG`mj;a%Vy1tw!3)U~YN-6>uQ9O7mpJ| +Nz0IrQ#u68_5qTH?cO#P**6qiyroj@|O&KIUJGJ6;@2JWvQVX+~(wrBjDkTbCy}Cgao$T?aw`XL?u5Z +jvt2DSMtpX9abq>;}oGOdYfSJ!Q@Ps#G?6B#g?M$qeJCIrD8>dc-2u5#kjl-AVk!U!0i_-X-(EC6BT7 +wX6o$x&Aib^En~G41i;Im}H+iu=1kT_I?s-0DFCjgt4gTos3hPL6akyKeiIe5y84Sb%RYAx(*^IU7;nlB6FqLdwWwsUsdwlxs^o!9!wB4c%Vx~MpHkLx|E4^eOgseeQ)$fB +nUi&udH;bsgPLNXgd;vWeM^9^AYBwf6=FnV+U=%6Lt-b3GJ_gWY$l%P7jcMIBNv@(G$8Xiy__LEqK0D-2agxn +gyyH)kco5T_n}oK+j08VhS>ntH?;uMC4R@C8U|~4#`6))UaGd)iUz;4D2XYr;hDxXJl)^mmjvgy8dUQVgwqY7S`giFguixH3JN^LjM^vuhfM6_{E+npZWNKmC!T@Ftly;!PV1dOH50X}%0yG~*dfi2CEjML@ +MCS^7%d5&lDe^5&U1QY-O00;nWrv6(0Bv7eR0{{T02><{l0001RX>c!JX>N37a&BR4FKusRWo&aVW^Z +zBVRT<(Z*FvQZ)`4bd977TZ`3dlzUNmA=8_e=L0lshAt4Z4xN@i}%S~pxrjDK3PPe7>zccpcVTYv$gb +%y%Jid94_ttd4`F`+&Mb1HXy|EsI*2W7jjdn$`4>G;%D}Il +LQ4_;oI&ZJw7ko1s$wc^DL=y`y3vXceK3?ltSMC=}Al-_aIYLkbUT#v|#PoUXRAAE2BCb;W`8yZML6T +dVPQ0UJ#vFq22edVq>mS(o81%K7>OKeU$>CT@-C!9twkK(DM}(u$9=w58x<=R*@E+}>KZNZm97HT=3=wH3`wWhX#p-}V5yt^M6Lld(x5WVv&M&1iZDY-|=-g~K{Rd1 +`6<-$1(2No&}{UnM1|4JeQ7LX$1B$LLU>{qYp-Y +W*gDE=8r5M#YW{JD@D%jDJZQX^Wqv5_h4@3P|#XOX6K>1i87tAks>@9)3QWrz$L_)^A0VbW +G7cpTVY_nQVQsOIEPi&l{9C|n%?r)glBt5}X{?$w% +e>h9n$kwt?u7`TxJp_xevY%#MtI=k?j$Q*QlqPIP@6aWAK2mo!S{#rxar +foz7000*Y001ih003}la4%nJZggdGZeeUMZEs{{Y;!MZZgX^DY;0k4X>V>{a%FIDa&#_md4*POZ`(Ey +{_bCKaK6NVB_}JIwFdSEYy}1kYmsfAf(;Hbd9g=b8JAm8Zp9Oc0d0cc@{4hjbT!Mg~E+Wt3(O;vLI}DAf&UTIDi(raMmf^Zoo=l{b^#ykqko^oS_ +TYnCa%LXSrjY`qAo1C}G6$`Us7OU^mQc_CHZWO(+Bo>kvd{AY!6AF`dHCk%3O@Yt0q$;Z;D2v)JQf-p +z~I08zw#+`E?n8jJ$4_B@D0K4#4&;0Ac&18r{K__j-aEs!9n;wo>)OgOA;YjR8Wet!30m}DoVsXAP9G +iAlBHHctIeWBME^~6}rg969ws%Alk|bvsh^_H`H`N+!pybQ?Ue8)je-D2y}hwr=PEtayDoslfiQZ>W1*op)(7z+ybR0Si5G_Nu(ppHdosd*E3 +j$mp?fk$vTG$B~OUac%zpW7q#Eu9evtGkQbfk{E@o2rxA$N^~jRNsLzMiX+As~hE +d<8c))#g%$yESr^Ni!ODT-=+xaxhb1J#wygwTKiQI{;B=a!Y;OE8M9R?xtK-hUP;~w}+^yZYf`B|BZ%nI*u8M)gmK!NqAj;CTq{i5IM+D!N4L6T0>jIMXL({kfk0fZ +!a#LM`Mb`-Hq18NVqZ!<6x#{S!H-#ZMCo9^QsH$JSl8`4Ens$>@F|X{=iWL!RCA}xouWoN|-@cjBXoO +Exx^NvGPvYm<7f?$B1QY-O00;nWrv6%CEiua982|twRR91S0001RX>c!JX>N37a&BR4FKusRWo&aVY- +w(5E^v9(JpFgvwvoT}Jrx-OMzC|cQg4P||KSyY$i +XKf#>YgOE?MY}W&jWpl7rYwt#WGxyc_ZEmvR+pvBXzoN@WCo@0#CnausZTCik(ELzsmycR#68*#_BIc +ry#PLM|NHex`t141v%kK0`Ii|rJ*_tU<)v83{B5@`nlz>u??hnsTPQFb~{_E|@%hMOHUTz<`6l&QIdrrT$_g)n$^B27qMRQXq$;)hHF0wWiUAs&XND +B1lCu&_5?t@CUWt}zkc=R?a80h=TA?cpPT|#QK**U@!?T83-H^cufEXVjvoK +cd^<92hxqR*e;gvlPb@n}K%1XrHGkW5GTx)6;M=0QLQY`fg<*&d3Q@_JGO*GOuS%{iWS+~MmJwWwvXd +$a-YyFjFf5Wh7`v=-6$J1cDR&;MI@JajK$vP1tPoBU)QjMJw9s=s%mJE-3P%DqKt~V^L`_sV@ALW7CR +-NQ@l5)*30Zr +1gl|%X=#lTWHV8tcwicw`q%8E#MV5tjmDhYy^Yet3trBEoZK-gSfbeA}H<-lAwHK;Yvl$XYFjJ{+6^Qb(Za}_Py$FN$u2J39UAyjD7ABH30~NLbAkKkPRWd1BiK0m(XznQsW{U +>`FZx)rRR#B{q6fAeF`8+OGUvewNu7#%uzDci5@G+jGiXkKovjoq&5Nwp(M#yE(*@)F4k1Tx^1Kp5i>2gazY`HrT-q--uGl;o5oJ$Q)Qw%urLM +tf}TbQSSE%zLTxM4&Q)5uztadiWLuftKtG=0;Q)0^Rd#s19VJL0n}N69xR(Hfl3r4?xMqyy`eI6_c>3 +~{}(D9pdl@=<>G18TfmAg*g}>P+%jCOhUE{JpdE+WWCynmyXt=P;6rsBTr*ifpCDNbVnIic9&9@Xs1W}R5BoGU9#wikM(G&njh +fhM#KcWHC4_qD}XWbMvc@Ov#N5j{ZxB@c{VK1(9sr`<;?$EoWQF-F7(G64+dLDUA +dMa9J9Q(9DA+rSeI9ed@EQ&^9EQPVOM-lS8$Zb*UnmuKB4B?t6Ih=y|)CRn^l}o_^$lr{h`@z#v)ia# +%2XGSV8+OqNQ^IUjTrOMVzoRm0z_SCv!QhR}j**>Z-IY0U>V=&ncvYcK(cPkJ3COskm3EBvczIi(KS; +KqE&(46&;bT(ZqTc&EeAC6J;YE31bU=3L1+kTUN}x{xd+72ADZA&QC%HDdo*|kKab3F!%SKt0agOWw` +mn>=_q{g;6WJo!s+QHWfM%srr8`gwa8=gvB8K>x?1(XmRM8%8Z7H11%S0LaY3a!_&$ +-s&pXwU077=Iq@fnNn`{2DYc@|PM#Fk$rM=TDBli9=L>$&0_deD&t!+0)aLcv`7M_=jX@hAskee#&j= +0o*58%xa)@(JmU%^0~cO3;U_ola34k=cl&G|e2D>;;d2;8v=d^nnJ +@$m>Gkve;v$c;!Ezo#Fz)!D&!(YuGW^{j~v~n3%Xl%Q4=mgmeUvTzh6TH}o)L-kq`9Ahc?)s=O5|OidzNeg^;58 +ThU?7KOcqCoLzXE|701FxAdn_{q`3kOM;ukgyFMK~~HMMt|3o&*&L#sQZ~Qjm9V{;6UDek%1w~wF$uI +m$H(LC~>)6hG}ID;hYj8^7z36PAxt_hWaBYy=Jp-!mo3qG*wqRdQ)52resdEk=Z_bRbX%kA|(n|>vl8 +aj3p5>(!jL9HfI#~gvme0O!JBWM1&3cg`f9>VS#le=dU2^nG-5>OA;JWWO{ +=8E#SSn%NH6N&u=U*+$7gJ(REtpv7^lmkLSCUx$=JM*T!{-+mt8AU6cUoF3!zBgY7?+q%$^1dq0n`}+ +?7N&6Grh57mUJfLLl%3nl|fNtJ(}2!Ln`F5Acs7wK_5!0LZn|QAmA(_?Y3v@A%bKcfE@_VLPV$PmQXj +1CfAY#6BI<)-CW{jfrnOH|aCSmO5`F*?miC>MDSW5^gJcO9ddGt~a30D)-SwR97%{BZcxGn3$KXHGcEv!R!@tm`) +G7PvzyM%@OJLGdJMXv929Jc?$c>a%yjT?5gMi#Fy^Dh7LD@rufgyVFhp1X6R&Jj|@9|j_GpjWfQiI8& +izEFX)JgiN2$Ww)GL-D6IA%PoxUm)XOnUC00yP_=7^MU8TAMKNP$=JUyTThiD0k-*iAU;~^Ax7i?Y&2 +6%hHQG|gJ+z31i>Z-h7AAUeTu*a1QCmC(pDd#e^swT?`z|Y_4eh06q-?<0g$=Ap-2kNb5R={`iuJH8Z +(HD<(wj7UszzOJkXiH%}f{scL3|a?_Rv=nV_(DPgOY=P1^s&j9zT3ETKa$od86> +Vjzvq%!YjZQj5YV2>D-(**K|jqI5Ce-=9x~_XRX^W@gSMw1RTJrSF?brO>=L +g~T{X=L|7Nh*H;YJIxQ&dcAr5!h=RZo$jaI?oyxoGUtFJ2B=UXdPa-Xfy8KuMnyk02}=|VdpJtIGv}n +)jmE4stl6B;pyEd3{rrwaER9x)Z*=r7Vn5$g`o|B#wJ^e~`pjo|40j+k%4xd3;TAjUv=*ehlgw;6<={5Y~Z+f`tN(a=^6BMu&RHTLPks8-2z$z|f*L* +x`h8FO-A;)_`4D)*$&Hst_S%9hO``D66z~7E38|+OJ|YnnFinlUdjynGz>^fgciP;_taVfK0X=S$dRY +LE#b^64QC`;SM*qhp4mkqz?M##GK!Ghl;z7k$4R@r9Yh6_VvSsiIE>Z|J{__jYB)7l?2J*_rpeU~*vUa22A*yHl4qjLNG85iAP24KBe7XhLH+BYL5X+pcP=YsQbUPsUz(KF;CiE(Z;a=_?{#nJkDT1xdw}EN@&P__XBgOt-d{uzYz=f|BPhzG95a=JpadCrMXJQbwNNIki#(i +WG-zOYdE${D>DCC653e1`e0s^QYI39KXmkkeVdOBcEQeIS7f)am%W%@V_>F|4pS=nb#5~+ +^9l8%dx`!FZ3B_HDxUiZ)LOFyrRbdN8J@)iP?*2!twqBAN?WB*d~gtz@Z7~D)m&S46#1LY(JJTcS>jw +-wtT@HtO9Eo^|zs-n`A0_Xc-Y1w-Jv8(o{p3V%#^+XT4z#gns^?aJ&bFg7_lY+VR?W57aStQR$4&i^d +#$YGl(q7#9{H-9N;;tR +A8d;IEHnb&Vuq(MoJvIPFvwd$SkwH=tSycG?M&|0<@{rf@78H1&2Yc=d#o@el2{LiZPVd75Lsj0Rxcb +-WFW9qn|vdxbi^+JFWkPk`CJ43jjZ3tQQJZ1S6RP)zOl9aJKPaN=NV?oxL^{f?M6$uBn+C{0yAV$ICn +N*?*|N}U6>EH;N$2y@^?NMq>c?fcdTQ+LXbMa%UMu&$g{$kb`b +A&-OFfGR)rpDgI;SMPzSbpj6AWbr9Oc$Y<3-4R8>AVXfioQP#0NlkNn)TgIBpB!jqDwXK8@7|pV)c7+ +SqOu>KZNdh%6mEE3-IiRK6LMD +OJaaOjOWFR`nEif;*b|ffv}mFiwR7ieQgs^ymo0XKnz{lw+|)6A6j~}toQAD)u>^)HY8vzd)=~JtF)Q +RQ&HLM+2|A9t&e2rHI9oxcVqg;ck}106gok|)^<7b^qj%O-xP?IjAVrmxT`tjY1AnlwcSqI`V%*ftz< +B73kkmEBF1XIg%aXc^pT5Ua45P6b5@a%iO}BsgIEJg>*`zN*ECu>tdE#H1JItu(9oBq-Ei`*Ix_E(&y +Ed3hnRarIbD8sgrs1-lkKt@5Q_q98fpjQ62I&Syd>au0FIz`EV3Fx~Y8zJW){U{cpKkq;MX#L9wc37U +ix-f0-086+CS+(*_TsI3@B|?(l4f_DXeGVV@qKhRvHhvqHu7a#REed_By!_w=7&!^ujuXy*i +BXz1ak@<-Lke31{j*b98IDbRf_8(r?8E9X)m$Bz;eDqT0;dNZ>L{}#P_dhkCF557M8*?iIeCh5W1=cX +2?39B*%`IpGSHq`!Zff%$6xX&)YMnafD>k;3 +UO|&;KxfqL!Zk#K76Bqg-z*m5qn}idg%S4#@(YIRdSbYYl!<;D#aAzm_7!;CSC*c|sBv9mHFuF)P>S6 +AR&Ua7iU$edMR_x#!>(z1a?)r=}e^>gloqGUpuH4;iWoR2#ZRKClnFPrSXIn=VgxjzQk=Dx54NR~X@2RnEezk7IQ8vbY+woYCs+Sz_?Kg9DK8!k)7^cGdIdsmJr-v)Lj1 +vT+8!1TPNe{01~SfR||#JbkI5WiVUS$@o9uq}x8@C7p4Azx=|8{CQau0U_|t`+z?()2FzfYG5#BFZx5 +7PSKlP8NO40{yhVb1`@aU#GqdO#;V8pO^nh_smIeFX^0s3WShNiuRv%;~eZjwB^=0s;l^3#fA3@`1U| +bSLoi*#g3?dV^VBy6qxfazD0n?UGKXB)RU~T4ZkYDeOHXBn=I^gx4OLMw|ww>L+oo-ZfdM`Jm_nhbq< +f}{s!8>0%-2)d}W( +L#W1cytjz(`9ucGFIt0{XlI(`vAobgLp`HZq2K3o*@;E~IBE7<*~@B#H6HHtTQy@W4JfK?RE#Vz6u$e +T$8wY%qLI;l^# +(HN??$W_zY2B&S6(O!GxlIRgZnrRXM$9$YMuKeI++WY8Wii=H;um=1$|k2Xy0`*i9h2A%np$<~O%c7V +KPEy24-mB +WDOrQd2lgo-T146RO{NdbJTs0bs|q{diwz9`bE+cmh~x{w94x)!7x(n!+f&c-<#(O_&wq*?h&0&Q3)+dLq&8~t(Ii0fyLksI>}~Hc6Fy%DH0PJXjVT +^)$EPL_XyRB3k9&`y|}D&sqd=4nMPkYopS6aPv~MNt90r_{Nxr&lz$?GVfqBe1W@9D +H4!?em)q9=akMhs1~-*NxblF6@7%hg54;PW!=rTGHdb={4NRbMG^;&zJdgSuVhiYkRa1|XB{*Km9b1l +-%f3}J7vFuccJ?rJcBTR%`|JAh|X^5oOl-eRc`2;leZZ_wbz=bm+h#iUIDY_wXdAAbkLA`YJ3C@$P07 +o=KE4@5DvTKD_kYJXVb5}0mis;nS$TlqWwP4S?d#I#9(vHXZA)fbL`w?O-TvN!?4fE!jHJm0`c>mEKF +UTA)ZBWe+=&ldR1eCJFWbRG1Mu@$PlPmcYqYjA!`<9^CL&i^zOPL&Ojv{#wgaq)}wgu{{T=+0|XQR00 +0O8ZKnQO);oVH)CB+lLl^)69smFUaA|NaUukZ1WpZv|Y%gtZWMyn~FK~HmZ)0mNaCzkzYj4{&@VkEn; +qHTkxx)6m)t}czDuhaMoq@7RCJoBYE2CPZ6O( +S_7S4*bp0O9w)IWYpE&aHIl9I($OXD1Whd>Qn(;seFCsfDstp0op*A +3*-@9SnhVYmhwh!Z^ViQ`@*mI6Px-5pUrzbkQ?@|!CaKO#sUR}bkL6D3^y)(rU +3^TI-^L#j((O0Xcruw}ux8Totxz)KH5^|f8ZSf+5eYZ<){NQHAJ|H3RZC_7y{_ykoHD7wb7xxjS=7D2dytt7*bh@pjZ>A1{#p0--mL4kVPgFi0)iw@pXsO +4$tK_(sLfw?#4kohAxHNiRm0@@ivTKNeEkQjK!Akl@i$hedF5CLTd+N!ZO!fwFM2d>qze#FJ!W$3&0w +aM3e^*&Cxucf*pN;rPc-!}m2e{j#x6}MmxgB0`d9dhTwT!Fct~LWl&;4g(8($_PvOh +rv^DIAmzZZ53@r6S~T%|5a0K@y+p}wL-(*gR`wz%K=6h<%{E10=zYt3aTm^EZuMOfrcW%{(n<1wPMmY +1`(m%IERX5gQB~|w53nX4>zVhT9MF1Mt!XTERNmWef}6g7h*D)|3_q^tf$n0)R>A^SNHeI+)C<&JNvj +1wCA2@W2wJVS5XBvoSM9>{NiM;0`W_zZo5IJa#MY^W+uQ6wsWYUWFnCzj)nyB4yex?rs>D;Figj;H&2&v%w4)a5coI{2jP>(*}g +&RY-qncb4M-LTMPXQ)9FV7_xQdrabnp+z1vn;scULEodm#>yv-k0v5(Pdcy2RS1Mx#U?YSQme_lYpzf +o^G5s!U*But>6RT_FYe=)TFX4aRL;P#Q!apiMY!k~(H(|6d1t5s9o8X@_MJF;TmFda`Rw0ybt%; +`~pzQWGct_OH=yB_Dww8A-bIob`oEUi{&w&{h2UFUJdJVYrERLJ9$@pU7#d;9Ua3XVzsA*hzYzbNeuM ++-LnKKX8Xq??9fD~&FQ^{64IA>oUTNO0?nKsFt93Y*W)Q#*cXbXA7&QUiiIw$-k?Z7IuX^S0tub}+XP +^5BIPoeESI;ea42)jfhO2qXRn69^3)*nQCJT4Zrr@M8l4%@6I`yt^h@m6tXe3MMQ~$R2yfhA(uB@uMB +R=Mm8d4L*ZLydHr9AoK2R`eHmAEf0qyPcJYbHFu?nBvfnc!g|$?b3iyNG?q+`PINTU^sKLP*%9smFUaA|NaUu +kZ1WpZv|Y%gtZWMyn~FLPsPWo>0HaCv1>OH0Hs5WeSE4E3V0wg+zu3yQ1=A_(h6M9OSB-2~I7%*?+0^ +`_mfU9gu-zBJz}S(cfn$e_oL1W+Vek4hEny*lu%D29OQV+&#(H6*ziE+A~ECB^`8N4)d2hgbvd&<9)x +I{yvWR-leNgN?1yL1!RD#;JBPFN!K!$-ASv*yfQp#a%IKrnRC&?z9P47?ik8wr(#CTxOylSXD`M$KvFtk?#-1koGREBh5ndtRIEb-cJ+) +#d&HN#Mr5~503rmz>$PlsdnIf$NQ#s(%}>|p}NHYT8N;@6aWAK2mo!S{#pv*y3AMx005g20 +01Na003}la4%nJZggdGZeeUMZEs{{Y;!MjWnpq-XkT+Pqa3-Een<~TYirla*_I>8d$%dgZ|^hG`rX$SYVxv{Mx%Lo=9#f7Yi%4;P8z3mVOJ_F6#8&b)uDa +W-A1jgdZ>lV+l9syh1&627%NwMqf3?vC(=S#D=j+UpY5nBU6g!X^%GKPp}-o3_H0Hf6Af0UXBuUn*)#_yzlNe_|%WC`1G;+Q2OYBApljb+6uI1&5;fbQi|NnE9i +rO}XaEPwUC2mQDQ@Wxz_yr%BvMklrUL6`($ed=@YvY|F|079kp`A?4y4>C=0k7n2fnED3Hzy0MKtNfhirzkiy +E?g?0_x*HKYSzv`@(r|zz$J#$s#rg+qXS@v|%IxpbVkZ?RyOEy$BDHr|aVAOSB#fc{Is;e;!wii6N%d +^-La$wdEwhLv-U-!-8^pTIFY{k<8R(F6tU|dcJxPQ{d?y7|_)PksUJe?&^lgO6&@mZ78)B`~B71U&%T +IyDmg3ap1xbvFN)oLI5rW*3lIy>BEnsmF}oCo_I5idDmumnhRCMeZ6jG9xVjL_m~VJ8TK5G7ysk&u!n +Qprny8tY_}}RovUs1NZ37B^Lt?`xF0Xd&=bw6)`N3G$VPr@P=PFZToI!A?B?cbgL=J}C@)`e))oxitw +B*Ea@VK>#2wdBsQ*EiI0{uzcKNcae`sfc>V?)TZ*PSdp+7g3kk?I6b^k(P|N?T>!@3 +}EU8=$*-)s>?Zvo7&LERu+3t-6--tjj#mx9F=_N(Ky@NF%|$mNEsjO6Aez+zQQ3IEV#umP)J(5odX@K +l(InL%*!KkHiBF5iAJ-5#jc1@V}K>6+M)x1)I#%Z0{x==2jU%q$)^b+0gXeBhUTpjA>FrcafDizF`(!iW&+g9JinhJA&=dPEylk5=~7?wyX8i=if +g6;k!-v`TdJ;U-0&VOiK&;TTms6dvUUXA3FD73l+7Yt_iPw=d!tO(5Q`2Rxap~52>s}%@YdH3kb6^=v +rbVDq_kJ9|p2r3laT)wr*}1P%PkfZS+xPG}#1O_{HW?t#jlo%P!z)p$^-g^ +{;sJW!*~8G)GsVQTYZ#8YBjQKmUq&;Ek9jSpHF?-7MI!9`oVg;+7X4n-l)KQRkUtVA5jHrgkn6!Hi=D +?Q*7b>RF!&Pwvql)%iN_$%J$J2u(F*MG`f690N0?k29$`25!2HT_W8>jro~DlsL>KKie7ncjt +S*zkAJ+(GXpCI=SpmBIL^!V+D+KEoZQ)M*LRD+r_0fw36&XAPzta9VHEXg=>=C>rG*9j;HcYSYj=J!( +E46kx(_|BW2tJIf5!&6t}%H5%VMvrpc580#5Shi^~VFP?ZFRN0g)eB-K}0^Oei*`n4@$SS7V0+-9mVq +o#H^WXAgRX{ZH1Sr9Q@d`hG<4Y~)jJ+yAsm&n|VO7MdZ4zuP(}|*(c#XOyea&M)#B?u-PRSzOxNwX#u +R;2hVQLg1j}uM4>CzX`8A9hz2^w3dQgQAS#?y>oGIaLBPE_qA=3u1_AFZEB842X%ewh28EzSKQ$Mo+v +3=*ULb8TUBHcY*r18H8*eJR|o{svG>0|XQR000O8ZKnQO=H8j>3j+WE;0FKzCjbBdaA|NaUukZ1WpZv +|Y%gtZWMyn~FLPyVWn*+{Z*E_3a%E<7E^v8uRZDN%Fbux?R}k)@Ezo)|?63eGF!XU;gRR33!(b>j-Dr +^|Pn6RZ*pDA2*>U71ImC&?$47o2>BgEC%ChNQZ?P;vwwU3*TN8JtO*3Wh7?#AQ5iy!;Xu8oI)-v<;W%|A$+Hp3Zw14{=T6B=db|U9k%5Y3dB +G_qLt3Ni#y}mKPNB!589^X0`muc^j0&HO}NPf5jZlyvJW=U5-@%NATqr@PlH8!6AVZfsR{ +&0g~x$#n42x5swVy4MbX|lOQ!e1w1w5y4VjVb&bzqfv|8l^a}or?XjB-Fy8nHHLAXVRUf8O`k%o#NmW +bY8n@n_H}BI3+#kP6c}zQdxuc2S5~l6=^{`SYclo!$`7$o}JT46Ka~kV}o485HhYR8@_LVmYxjBLdcv +GZ;J+Aw4KC)z>i)6E1CAvLiI2-Dn$Lu_HoxK^aW_nDG6SCPh`Jb8|E%fk3_8(A70|XQR000O8ZKnQOC +YogT9t8jZlMny^B>(^baA|NaUukZ1WpZv|Y%gtZWMyn~FLYsYXJvF>aCvlSZ*DGddA(ObZ|gP;zUNnP +>0t@rCt&YwmjD^iLkH}#6!=T+-lEAkv +<-5BeEYi&ID(rC9@b!+Z|_Z7)EvSIm)(511g;l(pQN_}LWAKk0|13!*srS`&Vu2}1(a;z|QgT@b1Nq@ +=5dPm`N*EV!7$_!qFb?5`?S0?u+*QPZ7y4_m3;?PN_d?gC1yKsE=erwY>?mw>Bi5P1SM*r;~9?MG#D6qRwv?v<>l4ce)$XLwhTviWjS}k +cdwpeaHXg)K&hqA^G&bggsKJ$y>~lM()mZ{?GyY9zfpqs;cq0Hy0-)0V$hFxE*UV!uIf>)-eEpX;SNx +CNpuSzeh!bjAq^dp=-M^h@_HQCZP=QigccDih$hz{U%n?llOpYwI0(!Q<;S% +6lwAP~6L{~3r2EXH?JxhmX&r+$xTqSimu~~2p2LXpxDs)mu2E2NYvd-aFQZsl!|HQI$v=8d43t4)_JE +{>QN=acK+L%kf#olg6XC$lg&Chcea3RVdWq@; +0?4+z(%kmgU^^yHs0}Z_vz{LmLPFMygg(vHC-@ +sQe^aGWi`}+YPKhao-#h5e`azCFujUU?wbadd)Q_oxLqxjMATlhHT%m*y{UP#!6VG3J3Q__K5a+ZAwh +&eRHnc5MG6ZPNf%P#pM;=+`#L;a6Z+;k-Sn_*BPbCLSVIGJuuHcVInGrZssdqT)9S4YAc+{A;A +H4zU9Z5AX3{IJYEbXPh9MS}`d%38Oa$bHOoUc~2esTuehK0O6EcK{{tzs4A4_U%7*t|0=a*$hbuUM`P +eMVozr_>GE*0lF`sZeQ_$6{7UEXf6)c9@imhQ0XzO`*)VFcJ`ammA(x@2CWYcn2nCYao26VY_anZ_~` +kHgkLmzltH`dT8;NNFev9ZuJ%tLZ*V){Fci#z2m=& +**e?U}kLWA`au`DH@9yEpk3T^e>WPCVQ9M(D7K-{}Da$Zd^CPE$m!_1v+Mv8Z&;H!Dk=hT1{y?l}jns>P3>hc8P>zcYpK3t2oa)UJDwLCN#}(JoeqrNE)(}uqn2}_%Y1! +b0CyjVMEhF$iAB^)XSFPv?xia^&vVott;bE<4VTJ#Ogm#O9KQH000080BxrJTI3cWzv~770G1U10384 +T0B~t=FJEbHbY*gGVQepLZ)9a`b1!#jWo2wGaCya8ZExE)5dQ98!Kni#w~ny3E6^Epx^+m0e%XSyXg? +&Wz(};sRu(lXO##corq6st}%+)WZE@N=dd@IS)^_fRj@Z2PmFMnak6SrN?}g +e*R_CpNu`s?B*`QK%);iZKH~OE2M-)ZTF=x|w~emlTxa^YrYk-+#iZ#jfS5<;Y7tSSd1Dwo8rV^XPh=eLRm+?aCvaFU +|B@s5)yESJCT}7f=6qdE(cm-t<&00b-r&GLA#1vgJ(F*q94f+Nn4?7QdTZ%^OKX71cy=Q-h_3;*NM_# +74eH%Iu0AlBd95SExiUccR=CN~|>{C#O~-b0h{Nah#P#;@OU*G1LlUIcZ1ZwvlDBj{4d*=Jc0ijzf&p +nm2hOKC-#6i~hAXm3G&ww-a9^;J&g6^z4SJTro4$KK)NkL|Pvk4oJ~0;3%WQ{l@Kpvy(?|rP0)uDVSw +t00Zc3SXfZE_zJ29i-(gJk|qQ4papET@< +?+>At{c#(Ge_ThPmNj~82T7QwW2*(ec&5f%AlTnNOo7_xf|bw2~_g|g+YY3EHB;eO5yyl!r7u|zo?^q +jgz^_x9CFn??FA$_;qpHll{4DmMM>PunuH65Zifwa!Yv> +zrE<$3z{Rv5tOx^y*qnjOXdSX&BwbP&R9&?y2rs|Gl~~t48?t(~$ofw&x9FzYqlVa43K}f+ +eIB3WzPGMYharEyDZ&GvdK&NmmeA$bNP)s5T~835#mH#Aa-&*K3TUx_@C6r`hNJG@mmd`541?c +(n{C(khm%T47eB0oJj6+b+9oI+>@fks66&dPr5U; +o&rG|c!JX>N37a&BR4FK%UYcW-iQFJE72ZfSI1UoLQYQ&LiL&d)1J%*-oRC@3vT&8bw#OD!qSFUr;hauQ2 +YQxr-|GIKIZGEc!JX>N37a&BR4FK% +UYcW-iQFJX0bXfAMh#XM_s+sKjM^(zp#u9n=RVLRT9W7RRWUiq=Ab+kU)n^elBfFMcr_cWNM=^5*uFcf`uy+6rUDMR7Y+rPONqmY`sS$bdGjYTE9QfRoIilmDOET6LjU1mTRyy-u!X4I4&)C(%@MLzBz^4!w3 +w;5j_d{>L?m=8Z+A4XcV>^Dy2&Wtu$bsYXiP$%FFCPHj4KJPN^B6LhrN}NpeLyf#wmaUqL_clU{OG8m +tt3mkty!M8z{NbDA07St>>saB&tBwDPv1QG@#V9pFzz{M3^4boYyj=##H!KE@_v3y2m@xv2?(S7A`qz +^vv>5qytX&{Q#i83miRWPwxK3P*UAHPO>b!ej!n4_TIeG!Y2}~=q%%t>}+TnL$U}N$0TfFh9 +O~$aF*X+nM?#viaXfSmTn$>T@uBY@JaUS0IX?0k7(8dU2W@fi`WP-_hr#E2c=)&cit-szMmlL+iq|+o +Wn*zw{zNRWD81d)$}8CABSfasQ3I|!L2*Vn_=&w@{OQ1tT!EzN5}~@u1Dl7EY}BL)xe1Xe5t`IOWtm4 +u+}2sEl{?&Eb69MHPVq=APSMcEFXNI{V}_rS(>ep)=XQVikrpwl*lO6!(iakI4_7j +^n(BZ}@?Rn)y3Rd7I0cyEE}r>I&(L<84g0eJeF6achv*{ULm=8XFw4Ny?^{dpvDL&x#7eTC`EfAd +Gf3v-U-KQEY(oFbp1p#1##1Hv%AII6)w;7)>>ExDJ?iI}Eo0rq=eW8iui!^b}?%J=e}(4Pck#tZ6M7; +{#qAE=ZrF=5;4}7)k(MJ$d_6rZ@hX_7oIicPoAY7#R7lw4U0cA@yK|0x}r1?G4+3K?una7z+o?w(s}i +_};w@7|mgo0Uz%DRzRr1@2RQxRt;a>d;I86e}4S%kKaLtu)hCbRXuq0_+j%icmNL?>VhQ5O!kJi+vOAZ4(T>vJG84&d!pfkYkcVj1;`Wv%*IyZA!dkJ$P1%->c<1NVY{x +F-n&)=o5{_*ywU(QZ{d3JX4%Tq{4(sP)T4*mM>pHqh=0)@`x(^)sPGv6QYREniPqOa<9=}dY0>~Ckkz +I-_v1%Zh>?A7V3XZ{#Ka|!?K&ACfgKrNG$!$QO~MT2`dO)d^@7iZ4Yp6778tM@Y?8=79h_O5t|&lj(o +1^%{jCceI$eSJ~6PDnyA7T1ZfF9ejKIu-u&`pRO{! +X;kC!$ZBqFRcSZOdz0~__MR8MoV|KwdNlO(_@1v}5va|@*J4!(S$00YE$7U(;s8)*U(a;y@xbRLspT+ +B-8hm!640q(J7ANH)oOqA)_!7K>s1f*O|m0T5_g +n)wWjoJweiqcaP8=}s+5gRgACU15f6pfK@H*8!g_KZEdTnzvIcLH1lrctcQAri!zu33|PVCB$Lj0p57 +0Eec*$R&;O4fh62ag?CBNCigR1N( +!l>}D;`;fm#Cj=T|QZ7?R!AqbaHbAp;z4ATD{+Pu8(XF6^fGn-1zo;ioz&QgR4M5S1IG>2kDtQ^N&hW +?CnJ-#ZE7!aGKCvS3Ebi5pXlm^b8^x$zW{}tO>DBJI3(XU7o@y~2;hJRU76=2y@ylrBo +n*D=cOQQL==<-Flc&7>rl)Mn&OoxEa?`?!*vR(evgm5kqFO|tf>}e|16#s&`~HAj^h!_qql_Hk=EqG^ +W&wLVF}Ojjj{+muZ*k(gbMH3R737ZETM1uA74w-o;J`2`B8M7395R-HNofM2A_w=GfIwgk-fgBfbB#O +@7Fn_1Lyk4Eo|3oc0Pu|W&-u_+5hq~*g0mP=PRfhga`qdqTUty6M83{WEeOz3;uzShXkF+A_h>Dq_7% +p2Qyc+cqpmYj5}~2Di&ki96Ew&Le867G+*V>@CyO(EP^B2`l^MKpf(_XK6VM%V0HMkh_Br@9_5lxY#B +tKpqJIZUcCKxhf!C_!0>kitT%5hT#4uc4cH`o3UiwlggU!sMQ+=^X5lRM{=K^a!7`uL?$Es +FISxE_Pw0HfQm0fv;yvs7gAwXc0%6W1Ci4bIv7#x6L@&Ip)cA++J|>a95~N9lZ0<3YvSPK3LEOWbjfY +~`QLZAh63DVU>KZ^J=F6v-6`K*NeoB}Q73wZQ@rDJ +~ez$3qp4A`1S%1qKHBzz2xmTarzAbS&i-G`Q{$h#?; +pmV2SUs_iu`8*Fwc`mo79Dy!0Dh+fVM2g6cDW5DQr)m0H?g;HjkB9DLU?@f^l1&Y{Ag<~i%w%pI<_-j +lxXUeBPmE@0{1qkcLmTo`6 +QtLDnJ>9Hi+R^bc)w2*80^zDkC@6ya`YTY37MsDLBEK9tOx;~$d!Y%5&iqK2riN-FRrDyY`zrA|hQ(Z +M{x;z@H=9Pog7g9@_6)8!T*slE=(#VPmK;GziWBzP1=-V#Sy?HSF+vnzu#zeYI= +s3rN6S3UV8s}t0XVLi4KLa3jHK9##25?=8-BC5()tkDdb8di>{QCN3vMc&>tB=*JwSq^P)ru2DwL*ha +9YDBi*)FgFUP_PS#uU5akWl+svB91(OJAh}u4G?PJIN2iPV1V9GYc{F63OlwKPi%_{6hYV +%9`s0&lNEF@ctY5*qB@ueD!>VWBfua|YTbPtj1R2avKbI3)rO~7M}e1vzT|<-{FS{jgqO>r=~`pPv_L +Y$5ao8@nSp|1l$Sw=axej4=JGpY_z)ys2C?!j>>*BGM%%j3+|9F27W314=^P7R_w4G_HOiG8e47(D3t50em<2P?H?ZLJ06$@|Fj +J;b|-!h@kN)(FO~!NYQ5j9=UVMZh{Q$(noU5k_z!`G;0Az|!R1{qyj+wPjasb~F{HX*-sigJJXNTYCw +Xn_tP+(TTiXj_{ffbthdUxbs}ibEpBS%MOu9NZi($aLNYR8AxT*$f3nMlu~*MX5{zHi14^rfHFj%J&4Ikg&f)(x(U)NV_kXlQNGV +*E2^@cj*3t^|X$XIx?xa9Xy)sF+8lOp0L#=JD1?D74}fDJ^iw)-D&clxz=eUop#(8QAlEcTk5!P9(aN +Xdv|tSSPuHtPPfJ>Ya-d0I@$vTijSFq|RwV22PR +Yr+KA%c`y>yO~)ai)C=}uuWU0bU0TN7C=M1$RpW639Ugs0#bEjMKGrv6jsyr*0hveyZ#Regh5a%#bI{M)V;*l^| +op?+IcH+SOax*ufc8pxY$hA`(qTem>qK=Kw>(W$Yai)1zQK0c6DG5<{g&pyvwk4@g1&bIAlWH+%Bx9h +~+o~ZfJLkkSN(yTLs-pSeLf%!X?g?J7G!!uaY0(l@sRVpua=$I-hkA->>fHmYS@!c!~P*<9;*t2O-2( +r-d#>iprEg{Zt$2K{h5D*=1e7PYccRV(_lbrAtvgzJuzaLnU-8g&$WKx!t`)58{evFjM%9pM#gS@4Z? +5Xo2YE`3rf~poPei>AmA!`^q)QI4fNr)Smz{o+KWm`Uw8mcG6`3fr=9Z1NhBMt8nBd= +_R+BH@w74xYea@M=2DIdvk|loWG9M}PIW5Z?7O4mGLFHb{Q7q~KI9ce2-mqS$uljIApTMi*dphRQubY9q#WFetSymWswkEerwGcTNh&ZfJY2}HXDu +hJHq7VTjJUx#IIg-L7!;`)Ttp=;T<@^G74nchdrvAJ853sSE-jw#Y+p$e8ddSpp#w^u?sQVe1Am`^We +_WG;>AC^OqqAD +XY2G=boPkWM+yDYagxqWbGkhtW{qHeAG$sh&?mQ3<=Tb%D_@{>X44AJiMkI#rA=RyHC^J9w{4q8vJ8Kfb+Ue`cZ;(Mi~_S5?;^ +rxdny4jINn6kd^|AUw8N@{{llOU=T*g$S_Ug>AYY#Qs5Olo+L8dEcE^wr)d59I+= +hW|~&swq6B$kXa4T{f1L-YPegD?f7TxA!hRwSSbN9=O_1==`xZV<4~+DOLP<1yA>Mwdo||AY`j`gv}; +gz2s)en9br!pt$W+LIcWEJXWJjWoyJUXEnoU1B-M+WH~5E0>-KddI=N0;iR)dn^mZFRC)L|u#?xy3(H +JlN--~fSo{&K5Du$i~W8UMEUqeu`GtR^h+2W!z_J^8 +mdbY`W+mn-G`1ym(x1)MIK#3m0lqbgZ}|3xbI6r^hGHB1|_!cVn!1FP>%J?k=N_uR11EmA?gVUbCMQ) +>HYMLi2wGD6e#yw*j7BbLDL93OP|X(nIj?tTl{}eO9KQH000080BxrJTIMpgzzqig0JanW03HAU0B~t +=FJEbHbY*gGVQepMWpsCMa%(SRVPj}zE^vA6SZ$Bowh{imzk*l;snqCnMw|M>7A_he>gIr=F=7Y#a5x +B*M4jl9NR{N?dV&1-JVR1%yL)k7`Y{V6w=2#JXP%jPX5{U5`^|?B?^wmFRxsHQo#=&f+)1tS?RLA_G) +DKVD4M<78&MP_qm5%+DILV@W)q)jJASk6-pMZLR=Vp%%k;)iGmlq~hjy`)$T;T+WPD{XI6z_6^*yat4+M{)%pHfqQm1)yJZ +2yF7WMil2Y*KO!&S`&qe&;m6LP~i^3qIV2X4u@P*hcQK3?qqI&%!3(B~*cSNmV$QzZt+zJ32Mie{D=I +Ih}kst$kId)|ntQj&`mnC@1$ea$G#ti*90BRsUSYLPf*VU0P>_Es?QL}63x1cl=UDPl-7x>K7Y>-o`} +Lu0d2MARgLW7@ETk&Xh*H(usLD~urfoEa1rHES)Y$D{p&9w9_401a}1Ob9~v8upCfHM!xfS&o)Q#X^o +d@0@=^ot-f&6)q46D~jMQD=8nk@fX<$kvHYrfhDbTgWX+UKSP4^zG#Ek+N^b-dq6>66LIkR{fLjzxz)XOGxh@S +{I`V4TM4>;Sesk1XbW|)%u`1&1-}RINu%L-Qp2MsGJB-lu9qEmMdv{GH4HcfO)hh)r{!KYQP*CExArl +Gk=-*#A!6a?rjrL^WhG@;$*nPnLUsgBlCT76Ee+wrUMj8phrSD1pK0JLlEGe0^}%NQ6yG$O*S{kj#+0 +s0TI>A-3wQQSvFF1DA3Z@d$MdZBAnP`B$96@OJF@=k~E(@NarK?lFvq7&juHcqn{&P4&1fTe}#ZtQNL<;aVNjL!Xx07O8uEd7|b*RhXO!1)DB-)qo~mvkr8T1Os=wzul+oOLiB2-cPczGqlYKy +Zob;Dp`Y`u|Io*>5Mo4k0?KiMSYYXbFw!`2ocI{FGnods#g +QwRQr%Wy)y&{jG292Ky>~lfU=SQ;l@@24x_ojmq<{6V``j;TT!Bl-X@q~CqHesg;6NwpVAIZfA +Z)ExqxYr*0PO8Uq5TOgGsurMt>OE{ILA)Q}WuQJUiwLckPZ~cylupvjC*#~_(fyo#?_UvJv!A_;$~#< +?u@l|b<6Fj3H8$F$>C63;AkK+_h-VkL{X+K#5?*slV5yGxWsMDM2Qmt#$x?bwN&fXn>hqf|#%v|L&E5 +1$`x|`-ujbf@ha;Wq@?)|v4*-{rp*oY5^?A@`G`0330+L}y4XAm$qrCtHKE0`jDTsEQ=>LM_zj%->B8 +-uFFBUH2^;t~WTTSKPc;&C5UlgSgy?%-m`hT!759syUQP8tXQI6&D$NDk1`|n#n#?6oxkm-`Re5_jKQ +cf}-{PVQ&ISel#blwAWe!-O!LRq`?c{afg@?L~F?#o~`wL=B@e^26J-vN_g@6aWAK2mo!S{#tjBT<}2;004?H001EX003}la4%nJZggdGZeeUMZe?_LZ*prdWN&wFY;R#?E^v9 +BTI+M$xDo%ZzXDYzQ>vxuBz=6)R5#5fxr?WHjqU4w$apvq2}!6al1GrXl{Ei*cNY%=B&GPOnK%*&EU> +%3eFF3O{B?C-78TFgMixS`d)e+--BjB~D8*!%%Z#^Gqmuc2KAUZtYR}Si)3seA(v-=4T{SJ^Wm&bnl~ +t)`GxJRBxh#@BP~lr)9~RYiE6c6@xtDv<9%`ZNgG%WovZ^RVMg$YS&g?oDt!VhV5HWkt>lz8s=$yAaE +4Wgye>2iO(j#_lTiaJ}udhE{!EQVA4mJRk?Asq@$(zH)N5g9^>m+jA_Qcr$9vA2TfqIoT|iOt%%;vo}tF!K^-U4y2 +ou6@%qRpX0BmY|Z77fDvg#KIY@?N6wUEgF3POh*M(?>g)QwUW*< +a*Up2R{szz&z)w)Ok+2X-z^6=^6#MnZXw|3||_&znX)Eh`Lp6!Gke8o57Corp5_%LQtr8qOCF{ +g+CkQMM?$n^t`Xs{r8A!eJNp3~RLlO$K2)Vx&KP{!=AggerxD<$N*qH|lO4fdeV%6^OHsCGg($b}Zho_%DH?7 +p`Zq5+VQ}>?aWLNy#PoGBS^$?nzID;^Z3?0WZ`7uMD7ld$pW;QeO~4C!T>Sr1uOh%Rg8<6q +Omxk<)oR@d8;tv#ie-PR{@Z=Mo}A+ZRhr&(QpqWO(|w?`D?Hzi9PC7%Eb&)};py{kB1t5b{myzXVJ^i +)W+)8(==K53Nu)S`Q#c+ydHEQDtnkB7c!!yaZxZ1Mv;GfN)p#$N=yo)Nz{HRrhEN8e@eLlcV5kim?}* +ESL}9?g5`O%|04Qp6anvRu;j(#msIs5P@3>UijR*=c5Ip;1zy0^RNAgZ!G%inSJCDU3@zaUilkYd|S) +|!VC8+l*nj5Cs!XWVTBNQTCz3IYPBbV2(YQ5`7a8CKaI=TQw8wtmosjmI3~P+5MZ|3BP|Q)W7K?h0}3 +{zA(7EsheSOC%<9c%gQ4zDyc2h#h!*Sz=UOj5{PeM3gaGJLx)uA0!ybCpPRi;#b0O_J0=VJC>!$PO`E +y|3(chlGV1HuIpS}3&iM2fvz6__gNUwD;BeMy@o95EV6uAhfj6A%FipE9hj0MP^PHB2m0!Ni%p1bm07 +X(MZ@X8kjqzI*KCH%GnS03Oo3)@C(KQ# +puB*yS0;eImLOLpG2uEDDk_!Kyxk>huEKrT9YY?i`t9aD)o9`wZ9Zl&k9S4S+zFmOA^HE4p%~&}?lXg +A%-YOSBgac&RpDc)p!})ZLnw?rASS4alK1-mNGo;UCngA%`3;PpV4udig;#8k&L9N|@_p2^x3J)L;&E +tv!hRKy8GyfQD~A=@zM~2+>IU2j#K>GW;AX1k0J#;vgOJV(j}oK{8CQeZxM_%nip`T`&QSBHV5o!KL~ +r}Y!3t{ZffC@}O@~xx2!rE^m?Jz$GQ0$11fQP;R<}&%f*1X=x^4uPx8QzDDy^K0iP?rjB?Ufw&9hsOq +)tp=FqIKLV$kn^q2B@ZJcNoTAIc8gZxA_>N76H`fw!O(0B})Nw~V*6&~Y$h$_|6qqJ>0S +X1r#Y$XR@P<9eh`nd+3rf0wbxtoIe<`ybk+mnkEFOo`EB<@56!-?ihEKHYd|(PnJpAt4#0@KlsoOGU? +dCYy{wMTY;Zs>xK +`Carx$4QkjO~aABdDFHe}B0swzeS9Usz=1owar8}&noLIu&Aa3ky=%#C04i{3Y&9i3JXy_i~0OU(tX- +xqYm@&skj6R0J^iHmTcC%S}IGK1Xmpc|pz_Y8*%f>vG&ln5|(0B?&6A`#+~ruXeWaQk0GQzh07P3}Vp +aT`55vC+2a$_$_qElzSxx-;0buoK4fk_S&Q8A{VApb~q(sQa<>IwAeb6E&hn0VL}J_R=;|Xe{ZM69UA +(V$%)^qmTgm!2*~YyGdO1YJEd>%=jP@nz$wHn2&Yn%nPA|bcN +JwQuAbUMn3uJ2J1PJvhBe)#E6p}X9ub{s>|zu;a}sE=i3-}9Wn&S{JV;S!e;u=Env7?SDHKsf0g&wlW +*Qfvlt@4WS%emAdFGle@<|xzWy?Cu`5nDL3>YE|SBXx3FoGe4o-0?ckmFmOY_=KJd=Yw)=|7b$BfF3I +yBU|3hZj8QsG+LC^YwzA->>Hj>~;IuV5m%ohgBG|j2``oama0`T4y|&F52{-BRfO)vBD`3ZC$hrqs=1 +RV^812D`R>J8-b0mMAT?CM`Zxiaav-Nyla=13}f6hTs^docsBve<^Z95<3V%oNUU+lwU9MMxGA*kS^; +idoj%bK^syaJ0)hJT@w9IHYI}Mr^{&Hp`ht7qhJ~T4pU}%ZXguQwT@vv{H4-s6#)wE +S618p*&ip0EpDhB_1RlbhQTp2qmTq`HN5wHt`#iY?)iqmZ}MLZ$bDlTZkXS@6QNtL}MI!Wx8Hb3nsdU +?#`pklpWKS${9d+;^(qF`+?}I*Q(nY!ngkFnlcL12|vmRWSnTh0ozXL`5iJUH4d??m;PeQ`NQb(qa6B +EGB&(YtzbMY)12WoUbwY$w*5!p`8Z4w*yE&pPy3NNYhp}gc>Uz*6*;FPV`irKUFa^Yq6*KVjhp^`7PZ +eCbxLxEOPVXu@h{EgBBrdN7jrVr=f`;Yt-lYbG56wB2V|2Nv(S9Ekoz)&eZup1NR`Nv@5#V{G1l4=7bv<6$0%7>7WOFhQ@PL!_=04>bTCh< +V3`W0P>*k+s;$5+Rg~XkrSS+Sw~#w27vjW>qe*KJJg}lTR-%uPm{P_O-v)!AtDV{($|wd~>L-~>D##f_MKyc(Wk+7IRlvg|w`{sdeIV5kw+%B}e%?2`OF +FPWflBnP1fJdkQ-QFwl#hry#0DabV&v9Int6ZcEazAt1yItm@RC*X)Id=4RK-h<6`847*V6~%$s*?Gv +*o#aLiG1PS>)ND9bpZK>PMTi32(Qqjb`btQLp(bMb0u4>YTP#yz@f`FI>{%!k4HH_}IG()En(~Q==ZD +iH(;gc}t44xV^TMI?QPI?We;>rEo?&N|7K_G;m)sloji>gVX}!}D$)v#bF=~)2Paifr_Vo=D7vfCQfle9BJMY9<1O64r|JJ$yOF(MD +Ox#}4Va&pxj!Vcc@}RSdI*=e0aOR++h})Yl69fld!z3`dSa(zD%hj!<+hZ0{&c4ltmaw$Znn6`zTIX~ +uYtQoSXW)RS_!rl*KF%|3T>$k=9nkJ5#acniXrsl1U;=dJ#*qqw|DPapeD~{P&knAz8vsDzuKi54C~zzIzE5clDe6WK{F)A3+M_vR +a2A@l*O9w!fgihMx-P2q^h_{22oNA?a2nmdR-pt(u#YFy5VN)Cbr(Z-sy7DI^8`DqQERVYq+*OJ&(HB8vF~H;oxjpIXiQqsyHslB4HF@>Ws?GRm_Ur&s-Le6ORU +ExDt-Yrm7~}vL7sA$NZzl!xaZv6b`PUb?L(-l68*}hKnoUOhP3YJhGJ +Y+fFL3-t+ +U@lthtJXS<9nhmZqC5lP-$U6OR2m=)C~KeyZrF0NikrTn>EmY*N}Pb~JxP*Lzs|4|J7~yiv@Y2GlC)|g88%B(OU*YYoiBiqGg&c)dt*L0IZ+1^~KTY +x^66a=)HNlJ<%NhV=#*2vzk<4G$wNI64-T{ZLyPDUp;`b}GrcNb?$np>!)C%|?P)h>@6aWAK2mo!S{# +x@5Ww_o600841001KZ003}la4%nJZggdGZeeUMZe?_LZ*prdY+-tNUw3F_Wo#~Rd6ijTQ{y-if9F%Ey +bqh~IWE}8+-B=I25zXE8>SehwrWz8OR?odgChGZdm!xXcfanIY{`iO3=iPM{a@X`?iNuLec;bK_VxM` +E5D1Hy}!Hr6h+a=Nl}+umZnA9w6#c6Cbv~tH;ijtHoTFgHYX=(%9Vn53$~1q@s{hgxE23xg=xNM(^kk +L@+f9eE=`l7%=B?1gi705MfXn2N*a+hod)|URibF(?n5oto5m7nrBd(}8j$c+)^YtyXi@W~tUD^*h^I +!$RY%~y*>cU-!cjG1TNP4?j^-oZiu_LC2VU>4;Eki(RZ_1zs&l=wUq8zM_dj|5RaVFqGxy*etYnp>-- +XW0I;nW}z}FxyX&b3bZywjBmKj&_S)>~5IOMEtU`?_ubKyBJZa&^!e!NRB-hcV{@AUKa*UOmQ3R6LV2 +KpuVUu&+Ct!Q}88}98k;vF1MEmtkZHD>=2+p^yMfd>_{wP@U#x0Sr|4}uD)Te+qBvUww7?ZjC8Xvw;z%dV!zu+!&*>DZb1 +~lFV>wuYf4sXwPS^7vJFW=KS_E)A4wbu!mQ^~WA=8(9dU{lz;)k#-Wt-cYubR{A8;g!}8I1oDfk^v&CvdVrRV#PD$+ed?D)1AvmINRptKy-(gc3PkAco-`%&8USH|K_IIV=fpC)usJdNKqS%qbdQxXa52)vCwr6tZ93zPn1m|#rQ!TB +X9{X)38viRjB~$7LQ~068d&5;S4GIe@Wq{4 +mzHiw^WDhBfFAy1e^$)AlFo|3p?HL?bdY>{5gm0!ma?*k`y|Ftb_U@N!VeusYE6V3E&Q+!R*X8dRS1Lt`_>GVwX2p +)D^52aGS9!@kkNj+DF`*xaB+SH61;wMH!MN3MiyJ3Ek|U^YT%vlII=#ZTZs#9}lrHD9(o0Fb4DxXu%4 +GlF2k4z+=EsRzT=Dx`_P7P8lo+f^DUqkWluwp+TLJJ>AaE)7T204Vm^)q(ZFSJSrkSzl*2TIL4I`!eM$zw^RQSq1h@cHg2O^n8cE~#FnwVFePn!PO4T-^Ckk@DM{C1V6n_%h8$^YHioEjF-h>M$j?X2z0Rza)YwJ&x`$Ixb?Bl_WXDS&TLf$ +dcu()gtY$=QsY>rwe24GZ|Omd)D=_=*E8jU5fW2#08a_X99R_aa9Z6Z{Db^lYOTG?4#9L%KcgU&K>3$9tWT);t +t%xsbl=!F%^gy#@fqowvTfo?4>xnEBuLV{zgK8{&fybO&X({R1dDw>zt_g^7QxVb-u6EtV_3^L;aAucdSVIT7h0$0H_ +X=oF-dY$HY};}2`lSKbD`x7Mp<>m +gw?rz@Q%mH1Rk-HVT$b$}%HIN+{&8mmGF7W#quGV#_NGI%|Gon7y3c70sTwsHhzq@O2e`D8`7C3Z;Rl +gPW`i&=OS8#fCt2R9Q1M`^Lw3QM|BE&uX4e+||co8n?E*c=j^!G}Q9qc{4)@!p@_;l72V4Vh27}g-}2 +u|RD1JTIQyZPxQYV^G$zlNRn2^k7mL$2W^o8lF4BqVy?+q|!Lm8ZI9~FP2lrV>tQ4-g#n*Ij3 +S~N>KC{lMHhj%043x6+yyxq0|3(Ar{+{21uRV?Rc!PSn%aNJ6k@Q75_faUCL9*`lHe~Z}zLOFJnp|yK +(3wzQx27}xo_)t4zGKTkIr|8Un0QLS`5F^=d3rg6-rifDD&B(3No5F>6Nj5MsBhP~p^6lnm@r-xVcU= +z+w(k(MfHPCAfw@)f1_1U-|Dt4Fg#Wpl1a3JCrz6R^82p28;$t+@L;H3eJ5!xO7Et^fiyICyMjCAoVnQ6#lm7!yO9KQH000080BxrJT +Kl3g;~N+N0Q5=#03ZMW0B~t=FJEbHbY*gGVQepMWpsCMa%(ShWpi_BZ*DGddF4E7bK6Fe-}x(Mp}I1d +P|(A1ZNe&Eog&L#Ze&Yk$w^)5RPYcOk`sXd76XW49RK(0?wJ7w5BhPnyI)SJ(8N4?dV2aj4SJq;E%O_ +pl+04b)0n9$&$FVG>54^JEGF=omRzRTz>|dKGCx$gh~!d6qg0gJtaxCB_(vsFiDQ>?RTaFn;1*)dA7x +eqBhT|jqh*n8SQsv=vMNLvGP%LUU}Tz=G)av{rcG?POoFPEiLy=0Dvio4YZ_!$f6I9s7qAc8@Vm-V`# +s54E5P4AZsbOkyIj;`v-(?}@N$_I8~d!*h(P^RE%G9ZoK@7W{#q4DA{Rl<3ngsZmKR7krS~0WNg^WJW +xyAa-TZ5j3ZPh4OxPQq=P-W4Zjhj9B=kf;7RhhM`y&Q_xBq>85uX3;;{2ES+t(9nI!kx@%Tk-O +AetGfg?EU3!cztzs`Saz~`7aZOK))T(-)?WOyQn#BpMkUi=1*!k*M)$C74bXWJz?*J%HbTvXCSDus3P +QZ)euaHIZR5GEajtkDWkGK-yA^z#7;H0AWXiHM*};cEO%aJ;Xnj&y +fq`cLthoxyMC~8Gf>WzC0)EF+*gt$DRXuu9$s`Wp;6of9!uFFanlMsC=4n45m{xNLf+({P&|ZTUUba= +tlynKfL7G{z-dv)Pi>Gqb^bheP^6-YId?jkFgADveH+^Alkv;9Yld;Xt9*t1LL+8w@`T*GSlG^+*O!) +?E4d}lc5`e2h!Vnl+R(T@sKujl$KCIfjBaL|nl%kIN`}+}b%qqfo;c7o}B(QIL7~uejMtaPEU?bPP<(|MPZkF3nFMb4uBs)M=0=4wtT0|MglV +l`r0V!v%FmkB^YRQ=IAW;$P39A2`KCwv^KWT=@wN=Ctjs7@E8utDg{8!=V<>+u~7kIufUuNP%bqP;2o +f_GYAw5D1-{uMGa$z0USg_rm;kBhyfHJ=lBZ1aEe7hNx+1(698G-#o*vLORLQSq~HLb@+_|sUf4A!3` +Qq{;~9g}f&sYp@ScH$fLt69d8FYC)5yvUnd?!;LT}69i})c9;OQmV7O1nxfJ%s;rcYV$B{+&W{d#vjr +_EJ>l_bd71K%;Jak8s{BS^Zq5NouPj{@;Q;%-p*7Cd@b6}SeF*K()=7G245T`f@S9_727AAue@l1hOv +9liMBr=LdW^YG&Buk&|TZ{J+Jy$ydodpAG(`SRihgvU2*boK`d0Jy^fEEG70*y{cKet!8fq|f({vfj;8{&P9ghtSooT7r`np3R5Z$P5yXnAT3115lB~Xz+nd-a_86N8XJ}(x +iDbN`f(-{qGkCOh(c!l5xEzge9gXK3WTKH$Fk5Or*~Lr+VFJeqb4QmuL5%lv?JTRkGb$D{;LjJf~2TI +a!ULSe^T-fF*_O4QNf;z5G+|*>T?34Gk8oKd&u?(qoD8sNsbU0L1>>2dTLYHSX62|2O$F5i;vVv1A&c +8i0ee*P1#2ecLZNNIC|8W#uJ1V$1D6aX=dj;AT{>ez$wrzcC>yFRcnI^+&K$_qT1DDMV5qxfaQaJJ1{i{tW +%~vhZ+Jb+Zzl42r7K|coYdO3vAC5XWe~k_uB5J&+os9OkUsxzh^S_TWcZf`A=gKnJ1K&-a^eP-$u~Cc +h3@xPKFao!CqGwr~ms8c@Em4$7BB@ye!`n*Vs`Y&qb-WpwoO0ELrvBjT=Pvr2t|uzeP5x$I--)TIHf5 +ML9Er0^BT!p?z#b-L_!jNDpfENj-GfYCs`jLz`)|&)jY!#Q0_rbM`c4Pj@HxlV1NHGWX4SI0_8D%GkS +~4D4&%hA|%Xzfo-V&qor#x;PScI`9LN_+_l%+;Mzcu3b9Q3K6joesL$Zj-w5L7*h9p_w}Lcpk%7nwsm)W-&MTMQjPtA30F653p5C_Bw$QrAbpjP(VWf{Krq@Mt1uk`oN8D{R +Ze}Mtgx8On(K~-5T7_?h1Ealviya^~f5C$uhWeZBhqgc8DY3pHT;HmV=+IlF-#S@ElxSU%Z9zKK%9K- +Oc>!t)2$^ta9>ZNQPkOp8StL4yeZwG@wj9+Z)%M)PZP101WWD6mW*NkOk|~fns%%qai7g8*JAy0xrcc +Jpv{jTnr-prFK};9e7$?f$sZ2Ga$k@fIqM$mk9(i3WCzKOm@aHg9ly1DT`fOv1&fagD;3fjR0y31w2<2GY8Ji?aQn8w-Z*%jR0t~}HKOx_5L}ovVaGe*j)@Y9676oFr@I$d0W3 +7G%(XcUVKK7Ro;OAZeWFS=H0#6SnI3r(yf_U`nE%rv`Z4<9bg^Kk$EPQU$1e_#e;WT!`s~BNWQb106N +I5r$$keQIzW+eoCxM?cwh4}h};LU@d|^IQfDg*a~b0VG#H3Iq}f)PxEW_H1O)t$_PoFp}@@$SEEt_WOW)P7&VlQB$4yB0AR8 +ot|HeF?KCYZM54U&%!$HK$M_WezQ4J67oNSw@Qc2_Ji9$>luR@GGwP)tM0nLtBR3fIKGN7aXu7{c>v8 +|-sMl^-=Kd4=h*wdcyg)O)-&%EsAC0NUku&@V;%bvCpL#(>0)yfOk(&A5gc4-l7}HBd89s=enjsC1!+ +;@Ylz{lMOH9DMx-mB$zH@(KL=z)Vh#}(!BM2In4Kn5;W7@m?XvUYI9ye3UD*(xev726_gr;nnWWW)Bn +tUrgO?2Zuw32+x=$~dTP){y*2J)Y{kYSZ(NF)k?jXi&MGP!%9Vj@`z@ITEx+vjIcC~oh0oTv?Cf&%}U +n=tncwJ>Qt9yv5rISjN=9ETWtP;r)(3Q=jmsMF))b~nI6h0{Q80QZawf>K&I@T+gp?5X^#n~S2z=sFB +q+CUDhcBU+$wGB4a%~7SuEckg>3N^oC+2VH*m6HwxPy;q|0&-vC&6}VFH=x+~s$)t9$neAQ5B(v=4$m +kK_5rnVU=#ZTgZUOlHkkTL@1q0d)0BM#2q2Oo)%w~YQqS0JvRE!9Y=7onNgHVf0Tn)x9ySqfkmV<+I# +!!fOJxfb-k*ZE5aJ#HTzwB6tS}~|=H}eMy9L$FJiJ^v0;dnW>i$mGg2#B>IUBlL?0XU)doB3qool=fZ +vg@Dv|Tj&u}f>lrASxh+MBYP2AJ2{GwG76^6-rzC(N;w?GOLd@mlsBG5W4G#Xv6>3vb*X8zq?%BuILq +Zg7Q^*cH1+wPvI-K59hL18svr{*hI*VRH#)6zm)MB-1*BM^7jgJdR1QK72rbJJM4=d_a2ZyJD2eVpUO +w7slg7HM2L`fkq8ID1amNO;s}->>6<7mKQN_s!n8rQ(EfVOu9fe7ifV_?t_&=f1=WE9*m23Ktl;81Ez +r+g9+0u8gkei94@-HFbI>g&<2)Vb2_=x?j1p$$k1S6?aW-N!LI3cj9|g0F_RC*r%6}Vln+Bp(}v+byY +!u7fEqeZox8|=`v1G{9)LnCFaUL_#K7U=YC-6L)&Y@yYQ|MLMxjBCuLVDD_Ch_*-RHhNHJAN>99}%uJ +tkG$XqLyp0SXMdO(Y2o^mY*3O!g*yfa9+4Sz`7G*3Et1;nnwj3kQdI +Gv|9-t=N>i`M2Fk5PePpX!TcfI_33Q_%ExF0tMCliIDoVzM7=4OCB +*(FFvyKqJhusz3p7fY_r02OecN3%=&A?Bx0Bk3arjS;5)kKZO_iq@|WFJ;!@k#Oi0K-pAjgt0%q6Amb +yq6#YO=(-!~^%oqY}Z~t9P`?!!*0;f0B&hh6|$+S}^)bxTAshRRKuJ*V1*!#7pIYbv@M2#er3e9Hl|H +Ldm)3F0BIKz)|k2pir-%sEFr^ITCIsLj0%`_Oo)KMAOleV4w1`FIVCPsD(imedtj2%04!9%iy8MMo$Y +$n8V5rANeAMc2CGv;3T$ut&ESnQ-sDa_~My?=>&V6Rs3Z;FBWri;I>QLB?XVk4VMP)aOPRSEXeHF!&j +qS)UAgNyb34Khls<96WR0ecG$vC6@0kk#O&&ccFE2fClGcSBF-!@xC-xDvX+Lp)m7O+(2uV4X!GsIWJ +IkV0ty2y=n6i0gtDn*<2lQpA)KnX_`FqT#-KDqysuZ|^{~c8 +qdMAQi^x-pVA=UOdC56;sLtTlfWL930R&J3(>GU1H9IDT&O2Xu9bRYX(|ts|9<;Hhddj;|oXgjSGQT^rYd);Nc3 +l_Becy(lWM8Zn9ie-0)EzTEou%djpT%G*SEIrjy8+h3!NI+G|D;}ZRG4sD!NM&h6b+YIUw1P1)g47fod1P@1z~|TP +O7-0JM}(E$t+i#;}VP(V^bLpYJ#GDcldbka6P&Uz9rTdDRDKHfujmS&uI{x835oqS72iY9O@x0 +(`m4eoROW<7I7V-Z?M{|2m4QauoKk@$*b(C>7gqFpT-SV52+zt=Q_Qh$uo?+Pu~>yyd> +Owq{*fbm;d49M+o+n)QGO+6XlJ?UAV+JrcQ1LQH{s8x6*UJ*u{E7r!AW)6319LUX5to$IqF`-S(@=m2 +0*f3l;i>K2_~TOtm+(CrS+(k8>L+f0q_gjmdM0xe1*YD3cTgUI8E@JbQ&?(kWFzQusCN1b+#Q>}ndOA +jvy(1|f-L{s7;{_~gOQ~MSPQ+yijy?H$g`FjM-e6kH6^`uYZHZ=VjPJp#yK1^}sjowDmd@->PGal5eZC7;Yy)Ubqz}{@cj8v;PX5TaKs7}2Osdgu`J+SQlmA(S>xXZ)NHV3FRu+eYd?9>}@3ie| +7Ilth5y`caD9OF3RMNF?#X*ZT)1e99+w%4AWe1Ue;FVL>mt`Z(YV;v0_HMf)XE5xlg!zMHJ-chj`Xw$sAHI?h6BsSMy0V9u+}WLK@6DJ>e2I*z)96Lf&O0pncwP!1%tod{ze +t34kwv2KBe{L4je=7B>bHo%?n$-jD5g3EqTk=fS_}H(V{xIy!YLEbScc8}cBVn^+V*Pw)@EkCUZ8n)G +SpSSx;NB`FnjkIS;>UKqZ+rl}R|r9YMev|EBgm*#o +j!{R67yTg^t;m&o#wVjoUF)xLUs%h +gnb1TqQ__&H^yA7Nhs#bdy;Vz2gB`I>Vz&Dv2@7X&b4jU-(r)Z!$VGy+-_V=rh=lN9K$P=oL?Y?3}LK +ycdKs)|_~6=s>kF{?&ZoHufW7O`+d}rgN9Xh=M~~S0UQp4Cw;{0-GN`!FRkNM3Op-Vt-s#y>R +UzOb#1t8uPRx2?0uY<2auS+RetuIX`vFdHavb_LQ5A3B=uFegrE`%Paf#*~26S;JLUmLjDRg$eF9ZLz +l5wN6A;MIm~b=(eUdNins~swB3>1Q?XJ;{~UEv>&I^k2_F#D(H +`bO#2TZujHhXxK?<5Q;-5m!=Mk1TD3fk01E)&Y$JW6iz+^~rbv_eGLW&?2%+hr9w3wk8#H^F0YBCr~L +i{Ty)z>!3X1ona%;WegRx*{ +aDt*E0C6V%@6aWAK2mo!S{#t6N^GMJI008?B0015U003}la4%nJZg +gdGZeeUMZe?_LZ*prdb#!TLb1rasrB~Z-+eQ$5*H;Xb2U~?C$nsr?3%Icx2Q8c;b`YRJAS}tDv=zl=c +9&L^!2jNvU0y^=j+;~uGQG>0IdeaXwOT@`k_1C5RceMJ2th70r7gV16t%EQcRDe@mdR{5eZVwQdZ_Vp +j>ekdQbm}UQsf<)jI`jUgiI~!R3tfzt)<~y@2~ULQJsKN~SXcxLjgt4Tv; +?oBR8}V2&a}ZD55OL1{UYWFc9Dh$4(2Sd)q?MMn|b**uOd$P{MMI!*LvJPwaf#z*6GadHp@r>6&}y%LrT5`RNVz(w52CMX`TmJ*3dvADH!dH`HzgqCt?DJoTzehdkz +r3ggp%@~mlhvZ2Vea<>ZMQmB0!0fBcifY*bMX~Dz7Ll&)+^L~xkg3XZD;9KHhlD}z)ldV(w1v0&7pT_ +6OvtqFjt$yE13nA&w>7`+3dVA2SXEXQWuGdYbyNk2g%awAJu1by%6#Za2ot%a=CD3$q)HP!VQ>g=qcV +*1#497QNTK0q0Dh5%OTx#A^sMGGoYOaQ7tshP!qLbjx%y-RUCc<&TIGz7Z3weqz_HRBwMTFJyjHPilM +!yjle7d#Qk+HK3}xfVxJ(Uu%09vy@`+Q-iCvKJ9;rlbmw;25siN8`OiNC(3p7upVdv)h^6L8C-2@^T+ +K8qRfw|`9U-u!({V&mfz_yrC|y$%Dv^5&= +2UWaTkhdA!04ROSV(y?r1cEm&KJYgQ-i(OE?~lY8_kI-*3Q+yWiRx9^q9YsV2dD1K+&iM;F>$C+>tq= +<%^zDeH9#2MF5nlh987FSI+qvwj8|0c%>^2sAg;6P#kdDb%kv?*%{f%9bn+r{O$L7sh*19@eed2X{X; +)BwZ+D99JEqv<=Yg)0{I(e-2B^|b%~Im#(Bkxd>2q+-8#GYT~P5i3DFAu6mH3{1O=`@=O!@EcSk +1JRl?&37>m!aE$rxJO@G|PSz%*Sq=v($;+AA(kvGeV_e9F;h?L0KZ`E*);lkw0mSE}5<|}?l1#HHjS9 +`3Dm$up_#}l(+dAeP$!Z6o0opm)S@%np82(1pHV&@E9<7tIc`xDuOeanh3eQRdhZhixYLeHOVT{ +~uW8Pb1zowwti*K)XY<=Dyxo#*^ILAh +nv{s#s-+;u-LJY;+b|JaY?50VlT>ZXjR|5%Q(AD{hWM_DRS&mbvmMqF=|`lSf_^o6!f +U#Ka<@J&_$Gl9HiH}^Wn$@Z)*h(a}#Idl8<6z(-7Xk>8Q+;vkIf83;b(wlz)P)h>@6aW +AK2mo!S{#w(#bqmP?001Tj0018V003}la4%nJZggdGZeeUMZe?_LZ*prdcx`NQaAPiTd3{w)kJB&^z2 +{e0kyc8ithrYz2UZJeRB5&@#p=!&>=jYXtO*nZjEv%81;yASUl-$fy# +7Byu;IWUS#3a1=uf3)pu9>Kk6^W#n{1-ak3H%6OlwJKy%3)W&@53mvSQ;7VpsBuCLXBsXnuu>alk%-1 +33FW-`RYdS<@n%!^2<$;yw>{U85%vi=I&vxbUP3h1G2{m95sJR%N&JH1O;eU&qR7(?hOB$u=>gowHkL +4^cm<55Q55BpTkA*s+;%YtnasKx1x`cbDD1}QXHfv*WOu~~y9+0fXTg2yNan5u();hI3hhC|cmcs2mdCZ06e;nd+%crj%t +QWOfr|t#;GTu^wr^B21%#G%fm=?96#=ucZih0mh&IJH`vcp?hyz7LgeTC-(}S9{-t@7(l-(3$0cY?P# +s1Doy&!7D4nGUr&WtrgWXHyYFQJvIx9dZ%qU9&2--G#m$uT{m;|dQ99LevhM9-aMu_WGFx_84>e#?!( +T?+a5ewqK1O%=gt;7@V=zv8$k~3l<7jS#6FHYlw®c0-^&;Kq`dqmV%GB6R*g<;KQK8mDK`KoQGz0 +n5yVl2?e1{sK@-0|XQR000O8ZKnQO000000ssI200000Bme*aaA|NaUukZ1WpZv|Y%gzcWpZJ3X>V?G +FJE72ZfSI1UoLQY0{~D<0|XQR000O8ZKnQOixY~tNe2J`1{VMTApigXaA|NaUukZ1WpZv|Y%gzcWpZJ +3X>V?GFJowBV{0yOd7W6>ZrnByec!Jj)(9d2iax-F`_Nq4pt%%DkUqEzfh$qFtR+$*V3U#bFYuW!JIVZYb%#_Hw^6!4x%8738IXzxEyT`-NejOvm?rs~(3zF|DWftf#yy2dXqve +#am7X?ov|qBc@?+P_q+8EdE9+z;TGf0Jzp{5+*yhvp0W^oI7V`7jRnoV!yGd?>412#r+b6cbQVD)!IF +73-NW!PdKZ?IBS^4?*MuJru+qr-J2&q(;N%Q@xR@hva=`4SHOVnhGg+ZI>zmOAl$fWlCetovx45V|-c +;Nl=1zWy7^md_UR*dV!wauqu92wNinMHW157oeVZ4kVGtSgV=68w8**Rsk6wyFRZo4TV~1MqBxJOjTVH`xrH{L-Sy1^P0C7# +HI*yCfaBejRzR1LYUw{So{-Qqky*v+YH~=14@FMk^O=!RL{0tSc23XPY{~5V%!0Sq*ja?RyppgG@ug!<)Oox +EGJKXKA~$5q_dmS4ulETA^E@z)SkxDYg$UeNO(DGD3NASIx-DTfB8BsDhV9b<1t>)PBXg*ykAVZaqX}WwR>wsb&K({@eG@mBOY2oo{6J@0?IgSnBTuc_zhU17M)~NPsCv0k +CMWO%RCZUdbCKNT8!cb|R4Sjw%6I%(m8uWq2&HWsiLX6!1uI3mevgu*K=;tbb7B6kx@m1B*B?McC&tj +IfR&oGpQMNn+^gqa3@Y;s+rWy9^!i`WmpYbI2zDLeCMHdHl7pMG%$G418Q8yajAKdjx^ZHFkClsj@7K +n_sr6VcatStFopb+_-mS&p=b{JR-n}h8So+P+0MqCR74ny6|A#q1%j#(t9>p?E^Ww%Bm9rgvC%cK3-F +5{8M9LhvgK-6M@_VcxM=0KzC`O!uN7C=O(MPYMf_S4gd-SyeCfZ3zIlwBl0=g9y4YT2{L>cZj4s4lEr +T2X+_{z#sxqytYUPT~*r2V|&7uwioVaXYvO8JXh +J`Fs%~($)K(>;eXS@_HVc*P7I+2vNA_h4L#(3POF-V&{FJFl{Jd(P+mbd-mQ5H0OZi&aIyZ~hnIlu|G +1i0!PO{UFgI8;|^}M=@xHR_IdSEqgIh-7wpwL&cz{D`LD+U#)nZCk!=j*dBaU7pD=$Q^3<2b35M5%jo +9V6M+0*q78x?{nD+_3vAV7NS1y{fEb75KO3Oe_^dW@7qwo)odlguVSV?ceLnNWbW&#`XBRn`k+md;&G +UfQq%s?H$O%VrHg$a!_eR@9I+>#QBB(1QWOO7798(~`!Q^>xDMY8R!Ry>)6@TM2Dc47Y6wv!-*IycUlpq +c_By9OaozB^LIzax~hd;NRTDYffatC$hLp}_Wb#)ZGK|+bOu1~XC{zK@esCA;HQXeMT22uISM?@x^K} +T3*QI|_q`*_r_Ue|hHQ2+#|vu(5X!b-6$Z=eU7oKs29w$1Wxk~P`9vkxFl+xU;p&p5`8q-E_nZNb3=S +XXYtRaQ549EN=GO@v80PdI8DGZwVKgv$Aq)9#7k%Jz1Q$`;>`*z=+3$=EaJK=WMA*)xcMJzuU%W9>S` +Lb4u?zBAHoU#Z8JFtVXvC!U@;6a&1|#v<<9i_qLxAe}W{1+1o5G?XhHTKiMu&~P=Yb#J{pBX}nrA#X6 +^TjV?GFJ^LOWqM^UaCyxdU2o*J@qK>PuGa_G5V#WMRZc6aBxQTg^?%O{KO{;kY4>_9P~vdbQp4eVb7rVq@I6VAyitu{Nk +WQ!&4nVgs<@)6;FVk~>|DyrlBH(OPL=%Op{O3*Pc9cZ;`1P5x=kI@D_$vDR7@<%-x&O%io@eO1y9?On +WY(3G%cx=OuA+5)RN={D>DPA4mIrTU~cH1WgnUv(3sbCQOC(6t1>R)nx>y9(iu0ZC|kTYTwmwhy|J`F_${8hA!*>lQjEfcrsFEqR2`#r5PkU}LDElVx(YazH +8L$POyqBJ@LlKr)iN)+1$7cR*IQwggc3&E>BtJD}0!TuGeK!p-b3JPec2^R!LVdzKzD#67G%L;|+S&q +LhL7&S-G_{e2D@xXyz^}jja((k5xxF4+q16*-g#hD88c`-YR@ST|(R)K>QnUK|@4rp#q+mG{tV+QE&? +=cA8)u#|Nabfe7baS9;~u#GS;ej +3`Tw`>UHP6W}giSBTzh9{n$A(zFC@$_5b&4-)KxYS%Vf+Wgy=Q{+U&`otzNu%j-g~%77&azXv8AYL27 +00GmlHW|k$H=LXWqzc1NM_qc3dBWD}r!%eiZ-n38VWu*VlxzKb0ONPF6yArgGQX6s_#! +`&Jw)9M@x-k%T4-Z+l`5)+btrtf-f6#fAxxV&N)u*{IAK5EAX_9CMC};iHZrLl1vlf>r9!zAI20U=0icD3XACNg&N`2&Rdm+&85vFm`v?JX$5e2m;Yxa`MDT# +Q-bN^NF#VC;<_f@SHp$;X5i}YuL$d?o-DNZ-BtChad$0(FZVUB&|@Fl1N;wr`oj^S7Jp3QGhAu0`M3{ +-Y!F$igag)C)DT2vhC3Xiu2Gs6pv^Ogr;c&j>X6m6_q~i@&O`R*Nj4dbP^=0*fSaXR;lA}&+Xb^fh_o +EHAFB9h!8R?d)A7E?{FpPfk^+TJ>hsN0D#x53VUlJ0{U??{S|HxJWed?2`FpJ;pht-kho|xF$RR%Q_p +bN?HnBZr9w%hJq#{_bJn%+ldw4E-{J^XB3PaQwelo>)`I*zw0TiwE8yvqJPW=!4=W&L&nq~r&jYUoI} +Z>N7YPoz+HrZF6aY0(nUt%HflX$RBpkkg+ZB5TkN#3_;P_bOjAB-OV!^bbjlYS;eFBH}o{h0iamxI`i +8MU=OHmz#xmlVAz%k9T5RpyqhU0+QyWVsXtUcqV{C?-&?D +g+c0cKE@K_WRHH*Z4sB|A=u0WB*c=K{9`hGpASMNi=TsVIpoGYOoNXTIxmws7VzCX`CU~J%Vrw}!BQi +PoD~0ytWDXaJ9Uuk7?ZCVUUexF7g%r15*GXh>8Y-Ay<@RG`gK`#_eS1B%9B!vHzb(HrV|{bGc6%!Fc} +jhKuaXv??|5q^^eytwHLkqY)Qc>3o(dXu!o1udtIb6Eafmy#FX>zw?E?}edI-ULP`J+*_ +lnaqG{l-vGSy!r(+g2>^+1%V>%hd$FSPK-mku9nZFI{h( +^4>+9p+uqI8>Xx-u0w$KhFgYYdKbpr!z1d!R@GbrKawc;2BHJ)4Tz-*i5g8kv9T`))VF#DFlu$@H98R +E)u%YllJK)^QZpl!w|=ms$;Bhs%%h5e8tIYk`NF_*e{r~=m!0Q(-f?zE|q8a?VNPpsy&`Pc&3E9xXRZ}u`AWL+4_AQd +u*$dBZ^V@00h);%|VBPZEL=bmuHX|u;vGya_yRVcZDR3XF;fT1`VaV}3o#_pi9ydWRoM9QJ;=1+LG_o +S#51t%GhyFFlc)OHltLDzut&CIkBG$lRjiyvv(Fc9&Qd}4=9N%z}~lIJyf*7g*`-b{pkri>jB4?{eDn +CMnukIiv^^1Q}0Mw1-7?F#$0x&v$b=ejAjK5As^wj%sATPbnq_QVBlH5{Dy(>3E=LzC6RhP>bID%k@~ +55!-CLojT1kvw?OC2r +ssEIJzCm1f4ZMQno)`N+n?QYFAE3GHHJz6^99RD5L?buCKo(d)b^m4q<5o#Js+Xc5Mxr{Nu#T6J$3gQ +Ue1l>yf8%|=1tjmquE1|d8oTR74o3Il=g3vU(N3|co{M1 +(CFZPV3X2YCRt#e2qFm-GM#=lr8Pd2Ia4*tQu&zsnm=K3@F>Dh7tMqT8;rUo-@e-h=uf%@>#baAis9P +dawG*=T6*{ri*NYH#DsF(w?&G&gs7a6ud<7l=$BvEt5Gbt)!!Nx~OP?Q0a=`?R&1vT3zC)GYKg5XJ5S +XmK1_O!?_SYYj`0CLSZLhL@!0=8+$KM$GF!*ZKcY&M!_>uSX&mnuTdek8#`i4K{k&^ZyIa$DQUALh!g +Y0+Z^UJq!w?Nl}pMn|z=fg5V +yz2+o&y-bDz>8%^4<4TU@F$nbs?uObDebczYsan^KsJ#SK5Y+*-Ep#XKKf@cDF9cN=s> +uk%^>lLR7v?hWVGD&yQat_iPX&@^~KFx#n?A?XAA+*R$Oh8ji4TW?1ZXZ;lJS3j}z7{hs{r-M6Is>_w +7VFbP{PO&+U`7Mer+tPe%P4r=r;pXT3Ch>)9&i>G4bQoK*p{Ly!0T%`SADQW8({5?eQmq9*WU0y2oD#z4!xAO9KQH0 +00080BxrJTEF+inrsJp8mTP?Dwkfh2AxXY9m +%kbZXyRtBsKGWwTeJZAy4v)}qIbhyKv10*2Wi+vA16>DK*u)8YiOT0E1LR%hF;*^A|p8!wll+_z2F3t +87qPfO8fXXd+oxmW$MRoZ^hxZm^+_EGU*Ygn*aQ`bVS)^@}fz3SwuGBa*uFV~gS8qUHra9?qYR`$E9T +-m2@;3GePCE#h8?=S!B&C})M=T9I1{l)9Qotds>TP&|ty=l6lmFxHN3OW?HLO0o3)=gcmWmW#HmbKig +uqzy*>fnfq)uF65OI2UnZeQXTPwVTlYigv$3#OUsa=k+eyNUPmy;_D(mO%TZq5ZN`KOIV-Dhz$=h=u6 +{^tVTBzjzX~%Z;NxtW_H=YkDT&=LyWc?wjuTuB)C)y?a;dLyOPVW=;+8%U7k|%YMD{-@L1VBDHPvBH- +hvUDtH{^()m&B=L)S4fDbRVUPUewd!x0?)~Ei##kr4zFF(RLT0%wmGwq_Fw|DdEq?4g@6fl;=*wK^y; +++hv(zd)!@0Op{nECI$d?_i*{DhbweU2Thpw{YKI+!H@>;#9x6O%uRaw8cJzm1^Cz{^ulyakgiyv~0Y +d+v&)7;cmBOSjyF`x5?VtZBc=i@S(h?(X2<>i~Vvc6JpfoYZQ2h4)Uw8VkAHy*?Y@$6Kf4K%7lOSiK9 +n!m|)YFbbzED0p1Mq2w0sO@P_5_`$t%jW7TKNmOmY_Z&_3O11 +lVelMPgW8lm>dpD!A)YMq%E+PeY&(!1uVbInr?~(b)}Ryl<9KJ-*j1owQGV(kw>#ctU+Q(bAKg~+xI~ +4{o4_@Jh}u2v%8g>?z)zjpNZ6q~^gG^Spc7B-v{(BUMAK3J5R+)K)A}5w(mWHjyHln$!E~B +E!6Vw2^>ZM4}Nk0DEV}!8Oa-ys%HH8`k(2V%0Q;l>gX9Jq)oS}}CeHtTpkw*mCJrOr29XNHNZ3WL +G;XP<(}2UfvxS|)?=u?09LciWK>ujGSgAk*Y7aCIx|CV~SQ4+BTJa|E=cep;@qrigf1Gq4^q%=HgUu9 +%(D81p8Jb2NN60r%=`$ZtB^qXCxCqc-oh)Y51Wqk;8p#2%AiXrje=f*`%|+F$4Szt#xwux{s?lW+xD* +707{Z?)4zR)ls*xV>iRL!n5H&SyVy|d|f`8|QBcNo#bOQv`Ne4HwKecO{3(!IftB!bd_+=I>cj11+P5 ++0-tn{yV21<)&l?;(w(Tbm#&r-g3Sy4;pXiqa +ET$f{Fvb9BwYL4aqS`YVeFKVAB#WSK1|8vcro!JYMZ-z966up7zGJQ`_kPHuK)=NTE%Pe~BKOfu5gwM +`hz?FW!@AQ$&UrFxJ6cbXs`QuO;4k99cjJRR2dGyAjTWv&fn)No0}sr+5RrzNi>Dr%Z$>LxUYV{|a`( +R5@nvw+``@>zbrm7`B`bIQzMbpNJ$!)KsfZ8V`evRx5nuW3MMo_`(h62sCA*WjaX{$I0h%${K{vOq*x +YV6PR{M_J}bBph_XtvwA_<=wFfM2K*Pf-AW_QyG%sOgWR0{^T1N^MX%V3)Z7colgCe*seBC}tuQ6Vso +(|6+6*U`B6%bU;20R#hX7#p(#OJevO47K2o7tu!D^d;xs&?(NI-6+pucE=bmf!T2lFA87p-u<3%4wd& +7xbLiG)BLKX>ivuGSOi);or4o53X*?l}$aV?%88taE0J_Br#qj7KZ<)*kC)4j}@q!}{H7G<6iR`rGA# +EKy6wHkmfKTP#P5a1B+^l{CD>p}40b`1rU87aZanNXg(7gy^N%#X)4}(mOoSiIz0~}a+?)c5HH}C^>z +}RZ1+;DcebJ8V;8fQjqL(m6hMg$Il79jZVmH6iI*SK)-lW%tA+S`{Kin}%)oQ3R4P!`^2{%V;X>rh}h1iJ^+>n>XPD{StUc-IJda +1vjwEBu-PKEK;z1D5$RCS1OU?fB#9pMBga_I{!~u1?2nx?|1B*fV~owB{3EwKK6>Ie$&ow63iy${Co* +N05QR|Jss@IEf10{FhH_(uGh=Ad#YdQ(#{=f(iAcu+j_rV_z%f%8ZCRJt-e6w}qd9_vG^8g3p4pPRf` +1BDSDtC;%LGaU{6eu06L82_$CHrSsizYPi67m_u3myeOW~ls5qORfAXWzSYc!VEbxvVDCa@9CNdW?In +6Jy`psNV|5-X8IzHT~1-?SkJDcj}%%EFul0va;M$etc(Sdn&1FebavblAm@O<7N48F1Dd +LpmNuARTQll0`j@1`Q$v194_>HNg8QeW)D`7Gur$S7U}Su2huuwwX@I6x>Lia~baI$vocOY|M-fT)uI +;ni-HppBX&V{PBn|Yo1*OexTVPwg%A~21$r`ZycYTr+jjs&P=Q%o2f#)Rp)^$7n6B*$Y^83CbQ_92E& +e|%}357C3f&R>H$K^frlX0XX0mF8c@TeW6+FJjI?qJJD7+A+o+%9g?RJw?eG8m*%xBkH)vYxqaahS0C +Q=-Q1sXRZvVyqYT`?gE{M)4K{39C?$()pC&G-G$Da_t#GkQ1Wjy+cz$qUEr7A9hw1s~d27WJW#MeLOe +APvqshS6JPh?wKWg4&Upuz5fSzm(fSLs8ay-Agv&>Hln`7M6zm7|9GRr1{VsEYuQPfgo-M8kP<4zWZOCjYo +Fg!Q|=hgmvwje5Yf=f>(=U=U>XZ@RJv;|zd+ATK-W1RFwhdykBV1|VSoE+Ws3sz69F$+rijDSHLRP;N +nNg-dl5@oFP8x`;$HT=@%aaE}u+i*o;(qls^|NX0YPJPKP6yZC3}lD_5x;p@fVqqUCyya8SVti@3~1~r)!Ud90rd*XU1l%h|3KO94G^DTQVaDIo?H}G +Qx46H#bIQD9fh@>M?0S^yCOykZB%E2xgQuA%*6;+$at*e&}i8NRnU`Kw&&Hn}T +#GByboK%rK}Z&qY#P4SP-ylx3{VVXfX{_%u}}n7dV>*Kr +`|&lQ|!o%B5T9Je8xKnu>y%Jr4MfeFt1Ar5r<@x@G298{2jcb7AZxI&_Y3QPlBTmK8-0rv0we!B8BXu +t=UFVC(9DEMG>Gm>PoWI7}8sCcA7@5069 +rn7X#W1C4S`>$ZICxRk;;-l~M%WQHMp4HUw%slKo6Te&BF1_*mcM|sOWN{=U@agicQ%c_|iR3N&dXH& +_s*xx8jwF=^GOlAJhJnXR>Ppob%MI~4?s{p^-!xK#qsnVet_a9##g^M3;xQBoD(WfCXT*qLPO~*CIjf +E%wFWj7L@DY`ac;m>TCtLJA-yn~Sd1ygBwE9Be2bPE6Rfr3K#++(u^|W1YB0u0580mZa*|OtHoF)nW! +ZOhUzsQ%t|{J>b2c-pj^l*s@)>+#gsF0iC@}_6pZA?o$!el`7h6>mz6yD-*NPo@T?1tQH2i^##|(y!ysd3NdOq|h?QLzq0Z*ePea+kh$u5I +i4LR7J6QYYNz~?^_>%H$qA&OG4;T~VfHf76&Qr7hKyz|HTTFZ%dLb(Xt_m#{DbS#`SxazU6oA{Co$Lj +sx(C5_G&hk2BilnEb*g6GH=Xio>{?Ju~L)d?jVO*lHAyeNSsJw$SS0=?DGxj=gX<#85GozjE#pejiK+ka5Zmkih`J +_{FjOY`>yshe~1D?I_jYUB{j$Pv!5pD>#uc#^wP_jM;%&}Xd257uyG+V0~Wn&ECbKZGOWdj&DYIVrO|gdmB?pN^u3t39Qgl=v;j9Zt{~0bII8?f21bjvWMm%| +rDCFEVz!l+e=w)q1+kgz>$;Ljz^s|}3QYm6sgia=P9MG;Z0~j-iRWg)ZHr1(aFn2UGP5_sq2eA!|`(% +qlb9@$f1`N;Q;@LnlOe`?wipf{2-dKDWhdo9vHfrkon$H;h#DQagA&?|M6YL}hpy8}PpSg%V9id5WQ- +KG5e)aV7(UV7)kIbQK3|>ia8$7A8cD(F~iHGNYYc0aa8ZE>6IAXQ{A?jU#TVmAyEVo-SVGW$^G+LfX@ +lU$dWQM9|n?|`zTX!-GfeKKD&$0bv>9XH#&% +QGHSXbBd#r2MhcTKfF#7-d-Lhmv1!&w7E_=S4l4~RGU(*>({G->1#WFy1h(}NJnsnf4Q^9TBgz+cMv9 +RQ8;4ZPLiF6-!J$#6;OPX{?m-)5Ui4!H>zMrx|=jgzlZVC%+F2wD0>6wbS@ji5~xJ#x^W2l_Rf +tS~}CM!m_E2l|B(>5Y;%CR=}F5<>v;Pg|iC!5Wo~sT(dCOl +b&oAX8D8(4vb5n{zBuJRtn?YbJbHpfCik_cyXgQb0)`jfzyo4VD#nZ-n8b)f$Oj@K}kiJ}A70D8a +w*(PMn|fL5;hcHOjC#@%4XRfbUrj7OVL8XgE}G9~xofjyiDbMb(<;{n$vJTPYM0lmxsz6k)tZ@VW`_z +HPm*(=!!CE;K~@EuxHGH|$NFp&iZVyK(}LoL`|t3DX+2SOqk-d?&O^$&2~TpikiCT%+`wuq)MQD{j{d +KkNbQ?-79##m|vj6>n{;~`05jXJk)mCH_PoIWIvOZ=BpkV&f_$-9R%q_xV}EY$&u^?4gszpmd*T< +zO#+LXJ5rrWrdpy%WPtBT+s9B$gstGbwo_;7-bf1i>r>Q+?Ap5a)#TCr#z)wp7Impk^!cBvrH7fIjH6&x%%gRB@!ez>nHKr98j*rkjeIG72nS<^GaQXtyiZVbH{ZBG!r4 +`7aoYRHElOe4KEY$tOPf(!n&nO3(7hNd0#SUHlqaC_mT;QF!VSsyPC2nf3eQ7Uj@`Cg;i1CZgFdYBJCY$A4*6Ba +M(z$h{@hY +Ea8GdAez`TqxKh`$_I3BRcEtrNhYBtf9S>KccAaUcMLsWS~NixiM3X~fVpqbmdi9%gPvhoP@%OT-QQ80XUy6<64@z%pOLD9 +Nt4Q7uxK*yNKb=EokRgXyJTf8s9#Phb_T*DXFI4CmF&GnlV5UyW<0GHdg$o!51sCLuRj6EKZc}O?NX>$&^#XMPWpz?V2-@d(>*V-ZuwE9aX3^Nc +TVKVyP#^VsidjWb2Gylqol7P;!U77Ez0-yE4<0{aVr92>$H`q~sVeV^dvojo7`DkQCshK +M>YTjNN=P%#hCTG7lKDx{N>7aMlS(cf8g|-POJ}#onW&t*~e3j73808V75ldh-RWJ~=(Rg7Ml +^c~&x%N5;_;7G+<8ozH5b{3}1*OzLHUZ!kbCml{6#B#L35#mj1J| +GfyhUQcjo~hO5fl(&665o_zC`5ECRSb|;bfRheQ3dfE`05I#5K8LQwL*Xl +aQhF6{YOLg$_LrCyN7d$SdhP-R{2hn>1TCF)2LhI(EH`p +6vGZf0kGQ3UU#8sjpi{S%0XD3&S^~fvCbR2e{qY63o^W?>S}$UIVG&B>=2lJMD{4_0g8$T%f3%8njwA +jPVOx7QI9UVh;SmfGE%=2wQ;u_|0zM51i^3?HUH*hbdjM!Cb&z*6odCO=TB`rc}%q*Or&8*`XhY!#fd +%08K!hcgkU@23i6MztX~NHy?gsod_=cO6JvU3- +f9y+mVtK100|gCn~NQD)e^6Kdewl*ax-ogF$j{eSB)}BWa`8uQ0XHHHWJm+p+DTCVml9tx{`mv7Eir6 +r#B#b`-o#2$yDiBp!ac=0)}`#^$v_u}IF6wY`$>m7t7*1vApUPAshR5XrarR}b1lg$|cn7^x +Vntpl8}A!>lOTa^_k=OF|!=_{f>-tEFt8wR_>ft4QiKXE`90hj;;{QQr9T71MWewlP=xRAj9k`$KOP4mdixlmR)4{~y(Q>7?=F54I`bMo6q +mDcdTK8*w7&^ALn*mkZA>sO%3lSP8B8@#X79$nc6IKLZ(qj4n7B?&XwWTNR}FTdzg`lc7d0y|5L+v^8 +YgPjW7h{=B4#P2)5i4q;z8&26fzFu-IA?$14(zlak!lEPPjkmm|%ubLHj{hi3fy9|+4q`rBVSJqFtJR +{nJlxVZx}5$sz91+2LNliTPJy0(+~t#eroPTzA0JE0yT1n^CTsdDJuc58plJhsg`|&M<}9qv+>AJ77{B%%W`y{~ +7WE`trt`2tEhRgqo+zjL|NUA60JfJ7VkI>b2!&N&8f@9U;(##}}S?M5`9x%^Ix$Gn!m-fG|cE-roD-` +FTNZ-p;WRxRL4()G%}rEwm-9Sy70N3}b>|5-r4{vZLjT;D-ElelL8SQ@2$PX48n{{c`-0|XQR000O8Z +KnQO000000ssI200000DgXcgaA|NaUukZ1WpZv|Y%gzcWpZJ3X>V?GFJg6RY-BHAUukY>bYEXCaCrj& +P)h>@6aWAK2mo!S{#vVU?XZpp005&B001ul003}la4%nJZggdGZeeUMZ*XODVRUJ4ZgVeUb!lv5FJg6 +RY-C?_a$#d@WpXZXd97I6ZreBzefL)oE{c^4R}aNLEDU6^y`+nvMX^Xz>_bo}v_#u%WKt!mB;IcSea{ +Sajg>YJYamH2&h>ES%%~e@_ae(Sb*P=nGNJdCbwNmDY>+`)<7czxS#C{GccIkl_Q_8A-Qa6!w_9zt?N +{q(8?v6;vQ# +$&<`Yp3?ggem1Z=+dV_?W?TNu5*g-Z=jVUro(JT*(fJY+O*QG3h((@l4VBjf!1O+oBh$lk~aU`OCupR +E7V5RRUw1Ql)1^8)2um8o;}NV(rlGq2_M`-JpaAN1yX6ss)MHavg$Sb+mGOy_HBA!jOj75QA1B}D1^db*;JxI6-whsKs;TXNfFBISy1l00CGUA) +$TASiz&Ivwm5}Ypgi@2p7!dBD_6YZ$mnbgL=$;h`6;jlg&*z_v{#vW@AasGO_2xi#Q^gYMZ +QPnzAoyb=Ejh5M=Kh4;h^wn%&?#q?EUvCnafWz?7KGo~TQAsbUOTUEfY%Z>wU31_wTE{^7)cX@xQYA% +A^_7?p1Ss0tnGg7h1#!GL9imQJt`sBwnkF$FNwt&*T^CpYz`Xrl4S|O6f2{jX5(HUi8O1vf&Sb(K_*U +jW2P&I<-|-MjGW^Fe(e<6p%_=gDpqbE!(y02?5CInM}Ax{$9_&p=@EW99>C2z!$%|EcZ}Z%jOB35Wwa +Tsct5&)CA~UrFYz}}Be_YjuwIR~tdZ11*O!+{nz~Ac=+V6$GUBPG2(Hy()weU7LPhkJXl +n#+C++%}`kj37;@1RvV7A;ADgp8B)Lo%bZ|y2FfDjZ;?$R$-o1}+&*;pO~CyS!3O!aj&QAv~0juS_{z +bp!BpJR1mo-s-G?LnU>7*Bz@X`@R;(zgz?9G!)J+w*=c3s__z@wr?p~nmR*8ZIGcezL?ug-I1n9xakox+j|#UCesAq{UGV}&f=o0;ANkRG$CJW +V$|QXTJSR5Q@y5YiNc?z$~B=50>5I3yqQAW_tb3s&^mNaWes02Bi>&HSgkfxfyrc(_rTKj +ehltIu*RGi*!vm$9NL!eyTLeyj8=X4j@68MEI}>FD75y?<=(3c%QW}K-p4dHox%M<>!t?!4_+eLq~a> +|m~wgaYk5;Y!;SI^7OvWLvio0(N34_dSv5P`fK*n*AkpZp>=~-tfdRi1w00VDcVqK7UF_zFSVx~&mT~ +LUxHDVSIbnT>q_J0FXA7?LP9t7GrfG!tTRY$2=zeMV^5ym#y+r>SJy{P!mHz-xO9KQH000080BxrJTG +j4RKZDn*}WMOn+E^v8;lud8jF +bsz8{uP9Gm<_lFb{GZ($PV4QkHdyy>)lW+CT43(f+VNSzaJ&VPTV$~F19}Sl6pug&+}jCg%n=E4i#GA +Nh|2}j#}7i3r>%=MUdpYrOg<2U7hE7meF9e^`KprZLJ=_kg1zJDyeO44#w(xY<<&d{ovqu_IjjVUW@j +DPyOJHFQV_!p4*f1XqD*eO*q=1+AnNx)Sj$X11kTT5k9qOVu+JfkpYjN%6WDb@Ah}xaBM9)cVn%#RXi +{;_&y`}Ca6bQW#wyr7(?%BH!Drv?5Nnjc_iO8&QP2Tyf1cmS;pgF8cB8%x#Rg)vJ6k#LzZO{w^M46)l +#78UNfZC3p^EU66e(g>J>=V;s92_sm)5b`W${%QJ52aM1e55gwc`O@wDxPA~!e|a4i=FN+1QdH@EQd! +>2ln0)Nntmeb%noP(DqDLLJ*p4`RnwBPu_1S0DtjN*ZmT<3$9%zP?@K)o4aDd_GWjI!l!;5c1VK_Xd)g +1e0ef*XXTRdu}in6f@_=SvBR^Mw@6aWAK2mo!S{#u&`&2f7I0089$001)p003}la +4%nJZggdGZeeUMZ*XODVRUJ4ZgVeUb!lv5FKuOXVPs)+VP9orX>?&?Y-KKRd3{t(Z`&{oz57=X-eET2 +dOP(HAUkyHJ`Nj-ZFfVqm{_eW36h*P|9+Gdx$)Z67h4wjNWMq1EX$wh8E2ls9u->Vg;pT-UJS6)7Mz~ +#fWU?GR`heYb!}OeMIk1mtq1LjVrTUPMwqrcpyJxL=4h4lCbVbr4po36+1%37-ZuF~#gv6+q7q;rD9@js@6V{PXJAKkJL%u<6PPjU6gL(y=w#Qq`0+<5~A1^bqOgOx_M +=C=Z5}(~^)V8O04RI|*%+!3$Iiw}9Dv*R!+Ee!_j5apT6m8zvwC%kXYE@RVa<7YjP)h>@6aWAK2mo!S +{#qhmoB1dM004~$001!n003}la4%nJZggdGZeeUMZ*XODVRUJ4ZgVeUb!lv5FKuOXVPs)+VP9-zXJKP +`E^v8`Ro{!$Fc5z4UlAfg3wuRh7wUs}CxVKoZz8#DGwsD|lai$BBL44A(j@J=d#8QLW_Esl^UZ`(`VJ +fy+;P%_0>houiVV7!E$Or&1N6K-602v^0uj3bLGXK{tmDHFs=PKvo86iC( +|nkWZo%4(LIUY^#^{F20mQDTE`>*c23uQj^W?N1EDMFFR7QReZ1X4dJ(_l!ansvJ+~vfQqZV^!tN3A4 +2jj$qG|BKY-d{-rt<&aM6ux<2NRzr)_u$?JS;?5iQqud8i6AOX>0jaw0k8Z{_ke@J&%(j1CbUyI-l6_j +?X*Cjju87?iHbE53%d%ekdO}NuD7KEw%%Cg_I%3&}-nZs1%1$qAOSXm)dPTy$*#N8ogTEYM4kiEoY46 +@4v#3t_U>Tg=A6|>!n#g}JaQ)lD&0~;gPm(=pe?$3sG8I3?>HXWId6lf6TVP$yMZG_TlEFQ$39FY2pO +kg}DY1BI7P#ifRDK9cxt6)1e|aVNp>0m*wYW8)jwOAm8rxl728SapN)1Eh` +iH6|D%jB<^6-HFagN2ZkRE#2%E(|A+1bRiBH@t4RKcW7m0Y%XwleNx+s +6fqEe&sP);LN|;7A4C{fA68csM3?=6keROT$!jY{{oa{rXFKBtEdQfu_I9n>DyC2~E?K=8$gSiwTrEK +j4p;dQE5uVn|GY4hZ*m_mF^A>~I!_v(n`O)8tG5C+PrWp)dnAK#2RCHbkvYrII*&Pu=`c1>Ar?*JV6> +##43~0w%$)$$<)xY*9)o#YDIq@~Si7J?`SzHXkvNGQ~l#Qs-4@HdUF9EtPe&X?p*eal2&aHB{xYK9%o +aJ^aoi{0r^kK~8qkU9(hn%PIdbk9g%ZQ314d>$VqvR?Fl+64JSL<)%x7ivu=S*Vn?{Twh(5Rpj_?r#062T)4`1QY-O00;nWrv6$e=n +SDh0ssJx1pojr0001RX>c!JX>N37a&BR4FK=*Va$$67Z*FrhVs&Y3WG{DUWo2w%Wn^h|VPb4$E^v8$R +LgFiFc97I6(gibAl+JBWpSl+ku+5+^%3O|PV?bm24D3xsRZI)SY}F}QBZ8SWtG4p1HVGt5scM`c&$?H$G19(K?7RWFw1DewWmPo=M{p*VQi**3GY6H6a0biO5!mF$4pD$h +ey*VZtwtl$_w?7xt_Q)W6)E+Z4S5ruacOOTJ#OaLs)4Fq`}H{Mg9?uX1McT^a*|TT;EW5YOVBssrgO(KTHB#D8DdJVa=SuDAVN4_Yo#$pi!7q)U& +6Q=261GBj2YY5WDZ$F_#-llKG2dCsJV?GFJg6RY +-BHYXk}$=UuOK;;g5Wf3Yuqp~jSX<~e-4+O-J#07WC1`;bJ!J!hmPR&rDN-P*c)e-< +dxsA_C_7DCpgvd<$r;Xk^Ub4CvL$8N^rlx(mV~vPREAI?q@jjMp;xQ0#^r9u#BTVMI`632uU3s!(XlQ +snJ}OP<$1+ft~(|KC_Si@9f&e4o|EYf<2tXQQ&72^2f<+p>gUBYn7%Voa$RnF#_K`0rJtajZ`Mqk@~{ +WsB~?59HyZnGr(^|sbfBV~m(%i^G*Y$Hl$C5-Dr&E9Wl;h}iUF>C2j<4DrDZ8-iy&vK)vAVODX&2H+@ +vdlzq!s!!qMnkZPeX5+LXQNdUHu|XT8eE=B<6ZbVrgTxpvxz!-NTcodp)(M)UAwq_hTIq4j5cOXnY%6 +Eg7G*u%+B;TGKLTM!0~ew=H3`XG{(6JZTOYAth_o`{2GVB3v-~o7= +k+-97@2}qd@$)~>ecNjwo6Q5Lt;9DYiQdb@-Ef_TNyl09a}c#YFtbk+^fQ}IpGM2wZ^2j^)hw<0ARc4 +Ur+}bBIxFD>1CbDglEaQxN6-F1aGRc^&%Mb(k`97fYHvQiyKl73Box0z`9TLY$R%LLPHKKY1b)OZ5w)O}`i%6Sk*;9I?O)V*=Qg)3zfOt +M!zT8XdJ6R8x~@`GAP!#L=C9zd(-)k(sHWS#@KULcl@Dgv1gR0*VQiW;ZA5N!+Qm_Qsv@DIuUdg)Rc( +eGb5*h+tZet*P}3DfG?<5ChYTj#i&AUiLwExZr|db13o +dBq7|PSLGfgpM-kMz>s|n(S2o)H9hB^C?+SrqB?b88rA_^?aavsbKG<4&FPG>R7+7TY3R(iW{#|ztoB +BGyz>rCnA$XHp+Z7pny*RB#N@DN)t(#}=VS|bZ7UzZHabFypJxbGbd7lDGd8Ygc6OXrVB6Kg*aWe$Ey +0?m4f2qxntegtVYaZ7LW81YdtBOtE||fq`^^TmY&z)Py#Dc?_}HI#k#&w2&Kj< +A!O;UF*Kjmhw%4iA-yUEG75d5kvkF%Vut5%kNzd55?BgmKX(K>%b6)v*7G7)Ho6q$#-w^i)317TK7== +aB=ayUkm9sO;lKbUK+Y4u%iTfq587r@F=y(cQa+J +%U})w+bAg%d4m*g9rtbI`kM%tgjohbHUq$rl7Y0Tz0KI@ac`J7 +fH-O=pPmUF3?oRK}$#Z(PX_k;IEc*X~&vJRgs=nT1X4}>CiGYIF6?e;ihfl^W>S#&0#>bBD4nj-B(y2 +gQYz}jHO;wN3!{)UMHn~E*h=wKQ`62t1sY4P$D7U?bAe+@6aWAK2mo!S{#wltulh6q001%o001li003}la4%nJZggdGZeeUMZ*XODVRUJ4ZgVebZgX^D +Y-}%IUukY>bYEXCaCuWwQgX{LQpn9uDa}bORwzo%Ni0cCQ7Fk*$jmD)NzBQ~%u81&NKDR7OiwM=Q&Lj +m0sv4;0|XQR000O8ZKnQO4dFq?90C9U9|ZsaGXMYpaA|NaUukZ1WpZv|Y%gzcWpZJ3X>V?GFKKRbbYX +04FJ)wDbYWs_WnXM%XJKP`E^v8$RNIQ%Fcf_ER|rEPDOrC&zqi(FKNIMMW1R#dlyyu><^(FD4RWydKtux#d9vWVZuFV-Z~}??sj2I_ss@8Ip +>*)h8=#bkS)*};DTOZeu~oHT630=fF_&OHKSNI~dc%SC!i2n_IWLX(OJHyQNflv+6?tG!L>p2loZBh-Mq`iEC4X6p84k +O2+(VCzHDCs-CMV>*Jh?}aCucqioH0&k6Nx6_rTFuTQ34PtDI`YR#3-{xV3qxo(8TA_{e?Piuq%iFyO +H(gz--TruKk(O4sFL-=zuE1`ySX4XQp&aL#sXmBjd0XqVo5J7Ktqb)lbuWGUW~{dKef1wuO9KQH0000 +80BxrJT9&SI0=ppq05oj?04V?f0B~t=FJEbHbY*gGVQepNaAk5~bZKvHb1!Lbb97;BY%h0cWo2wGaCy +x=YjfL1lHc_!CeY;!Iuv9(i8srQvgg=J*0qW4v+PZF9TyCeLkSTGzyK(j(J8F(+GOrj_{+0=E_bZS*KDlgkED~h~4Rav=I^G#kX@ke*2Rdji#brGp~US`c!)!E`aJJr~2opnhRM +SFXzrdq2sU2VEeqtjI7Yl57WWz}U}UX|_Mo;+JrMWGk`%syJzd7)u^_oVFfyROLR_DOqTzVaXMT~VE$ +!eq_Qs!i&wJ2O8UZN9ZTw6(=7pAT^z1~x-2fDUq1RbnBx~Kah+l#5Bmd+UIrQwRs%XvI#aWh@Q?<$qc(c +li7Qh1~figAs=(aV|QJTkl3O&~w3gns^*o$F?IAEc@2aH_jhz +r(MoF1uzs?8V#*D4~81k6Brj`64Uw-*j4LYwb@m2WD)_tVr~`g|3Olrr%@UH^k$|O;a^v-HK|F1-)>Z +>+IL6N!NJ^|6b^(h4*89*SgD=S(k+r{1#a95v;$-=bO$F0kI5R-RbpDxMTStjzt?dT?QQ{qoV;o7cHQ ++0bWjl)y=Sv9zQ>R_#%Dz{D<_%;};JeJ$UgTee&SPPb~<9sxf!3=h*lF|HeRWaN1Qtos8!i&NT@X*${ +4b2EUJBAnvLJ&jNRHj_QsnR@x~O#MAEvey@rPwRL7X5zx8fJnMZZvX3Y!^3GbJ)k{EeM +DisLKuj-c?_Jt?u2S6)|vXhfsKNpc|iSP>H3ASywE90ZsvDQ&s5lv^$%S^2IUKrbSM|F-Z`fW%s_kA5 +T!bBBV3Z1BSI+N|W>EHDPSi2+%HKAV-n1InDd2 +B*Wx12cfQTsKkKuXL}Xlt7t|orYb7+B^WsPAOa?d2oHdB{JrT`2VX_kvXoW+E@e&R$WQA)miA^(LPza +?Qg6Gns#Lhe;nyMdIqa%KeoW0}r7xNdNWm2?1dNQ`C(0^2UVsA59GLC=3%d^JoIy6fe?k#8uv)PAxTX55G%5Rxa^LJh81{0btg-#t9l6U5 +lf{^T?nhnmD)~QNBYP#l8U9g&P}(+qQ|5WQi~-J_RY@lz<@`aP8=wnDZL)?-cGR8@zfy#~z$(jYcQz|2^KJ#G{@rWv9PL?Qq= +c-$U5V<5=#u(ONm66EPb9f5r4VBAOsCd&y;rn&;**2ZWo%m#LbC+gguMtL_?;cwaGV8^!=C^ci(9jU8 +pwn{en@FB`VnnOyHz-^AxNYLs{y%%ll@fL8S_bNGUs!dI+>x~_S?AUwz%7=lR^BSh*KfB0^jmNje&SA +Zk^@jA`e8g}<+unSqJ~SbrI1XtxuQr`|m%>&}t1^A|;Kg5Jc{a0t2NLWx5f#3u|MK)Q2%|?D@V>@<1& +htftuh458ch-aWFCSgkCDAW5Y*y3C7#@C-D73Fn-z4|3Mjs=0Nf7HZ0%Ga?3>HH)il3QSc)(*EA#d=p +$$9iiB$cbXk`(uaR`V_b1jHHUxu5b~>^)8cXaK +mBaahMTSNri~-<`D2HFj9I@%y7bvL+PtzVf)g2web-Z%Jy-u>_GWOt248DHSYFU-H&>e=&2zm!;$SV; +mHV4hrogvu)bwV;G4{{wg#}ROczp@iCa|OR=4PK*;{Rj9Z&L`FYfp;LdJyDOL3dA40(B(zmR3!^L99b +k>TNim}pxWNAWl@ijm6g*ps?EEW1%0T3roa$({j%=7O9TvlUYzBi!7Fs$h?Q_Ps#+;9N-kiI7uM@T=& +&UfSo!${nphlH!FD6qO3*PWm+?nntj{muN+zN!MW^97u$Wz}d^N(7GsCX|A>8|^-Bm!Ekke3juiMaic +>jVN=*fikH^2>$MBuJ{31HWClL+|ENGTCR^myH#req=@pT6E3TORzAdP_88nZl0*I@KTdJ$D4{ov +KSI|(snZBAG+yy=InTkjmpas@z0Ph26B4q;n4>+Gl5xs!Da^>cG9SCW4>&g-8`W6^a`Qi-L?rV|6Awq +@)hqt3K)72Ug;D(GI!j8?C;D>@(#GMK#0ntZ#4kLCzsBN_Y_e8ItjRPD(qjpon^A@E6Oh7c=6g)ev4i +3ud0G}K%LJmw!Nai*%tc+Y>5z=RX0j7d=7^+1yyp1$Ovi(CI_+U_AIs5V+p~+@?umqnC@oWtQJ0`Q2$ +kpEixs!Hzu|%Z>9UR(oU|BMsevke+A&*nYLsW(%x~0?4RLGM8A_4{=Vm^8L;&`Tz@g$nV41i(j47iW1 +h8W00%S0}rg}(Xy&D3KFg2eo|&bGvm;JN8{HEa>!T`)j^i4~x~$VF2GjJlIjgZW#E7;_7?3sVrbpjv} +9l9PYrOFfZ}(J!U!XvcM8Po^l2DI&T|L47sVWy@%iA2S=$e$@-Myq@lnQAyG-jEn^C0T^*l%ov_WkHA +rpX2uCdQzqd>K1PX)%%L;DE3_or0|*0ZxFd;x8?kE=^&Fw#O8{JoIKr*G&dwPFq=V7UW)l>)E^0(F3X +lamu%TKR_1FxSqb>XS0j)`iSBNe;EDHlLB>ff@uuNN5<&q*9nc@fegIJ8fZbdsu6lyM-Fye;5V9<;gMB9C}DarnvQY)Mc#);9m>{VrUIKSN?@|H(FRAo|vKxT(9eH +D=+#n2_{srr2)`l2a~3NMU}-CM?qsA5malAXH7{@67!Q?1f2v>3)p~l;#y(mfEQh84*RgRwWu&ujAX0 +}*fy7jdUj6x>M!8LUSRMR#z5$;i3nZ7*1#Sxzl=@$b$)u*0f}(yAhN0j>}b_p<_ig>?)!Eg+PR26N+Z +>nr2&)@j+>l&FPIB%2b9+ +jgVd!!N(O|CKK}#GJ8mD)s`!5=!SlA?*x~bdEMbUPWooQblCAh#=|ERq3jwZt;$A^b~~6Tx?Z|*1_K( +JYuX&l6KO1>QBjz3S+^D-Wxv*JQ&AKVkX#pTB@J&atW&e)3GASe+K*x+r7~kW4&O%vqddMzUN#Q6;pM +YufVX-mZOU56w^`|2sv+!W-}`xEx2p|9*6rtiTVMak!CV^zoqYkQheETgSpzZ+hvQck*3m)0-@H52Xz +c;@GuM=V}h_3ruR{f;U|nBBL|Sqwl(`QGF+Iey@p;32m&P_rfv(Ts?I~w%@x2{|-} +uD$m0Vu|&I0K<0~0k{`!jq1YN%F}tjU_i8FGYZ1rA75rZGz(eism`7X~q)eGK&{75X=%&dLz8eSZWVP +SiK*^U!zTL`@N>sK)o3OKU1H!Kz9V{oLd;)+h`a|8FC8r0ETs)N`2YuWY?e6 +RUP+$PWz*TvFsi&)G5Lp!K%Z|4U8!nq7fO@1L-!88+hoE*}M&|_U`K*3GPx46@})dn+x>0*Y9(0&?jU +QV^9v~+sjh97}knBe7imRqGLpIt~}B4K2)wI@0;lj(-T1xb_7S%g`Q&Xoxj?MoeIAYCTxO>b{s}FqX} +ue_61lFqfOgdFWOih&UyiICpv<3A^rWHtY9=7KuA#e2A7A}@Vuh99|(zsDb+BmCle9&+l8@7WOxbCD4 +J8>w0lI>&wA4VO$nrKZner3CHPQ|%r~$Q>TN8_z=WEC@`g27)Cl|&7&HeRMus3*6lD8>;N8?*%K{6r{ +bW?IY%e)O&q{A!K%q-U_6S6{<`$RfmI3&>go%){=v<_%^AxgwaOhG$K2X +i1DsC^Pmn8=Dp+(6kBi!zI_GHXuQvp&)qu(zotrei$s09A=?r#5WqtRWey$*doBWtOj*j;+wu=sDHUa +b_qqmh)1oS|AEa&GdXW)yNCjG^Zd*uLTfW80_q +ZErWm_+(wwU#j|xp2jf^t4e!vE7TSsnx2F7@O$j&!W(omT5YDRTsr6%nHz_%2U$GWB6#t~L|Opi?BsmI+LffUUpPiwwg=-ljkb6Z;~AY|waQqN?U7oiBFH>*tlA>GMs_kyWN0MDhykM8@NJu +gdl*F0tmo|XW*1r=cVO53O-*@-X3j~1_N>uVFR~AU^zsv+k@MgkY7~QIWZ`hAaC9rKRJ2%{P@}9XX(> +tFCIUAa`NVllDN{^8QK?FlM}_+X(c!q?`f(+9+{nEf~6qxJ*^O|axmmkR&>%C9GFe5Jyr#FV0GqyeOCkvrW9ViYLPycyp1MP;GJ-c?DPTUq#u#1Wz +;=!Xi;qki!v!^a0r{a4HC7J}j8S?pX54sQ0G&O;AXGXWpG}-k0E|O*^ubeD;T*0ouOzzw@V2Tb&v2x! ++^nu6Z(i?(x1$a+o{T^>K!G#BzUedtoGCNx2`WUCFRYw>3a~UH@f8aOk~hYx$+6liS!dniEN*aO@h{8 +I;$IT@ck<`SpZ^VTcRq3H3)`Q8_N;ha7`U&_xsT6gJ2=a4(r2-e?*B?&^M@0r8=1j}Q8^fN6@Wf?5Aj +FFydH~=-^dQZT1NRI7={@nnDl$O#Rm`{0LKX1FwI2$XkaaC4RsD +k|rNkXS=U{!eBbshg4LF>``sHded<#0)vp3Q`$)S9oW3&cv8Iz=-6gcE^Ngi1Dop=26_M-rNISb7ql%m}U>0@}pFGIhpzhJ|}A*4M99L +gxPkqJ}F7Wc`1N!2l{rpWuMbDb1QaLo#7I^`c8Znt{c!K>!KqGDP=9bQFp>t7JP5>(rD7323QUI;m4M +81M)yC#)BcJ>h7I;U#Q#kXEF+dPdQP52D%h{&nygCMRWw8@7bXsSTC +@c$0obPyAi}gAv_}ZZHNBL-8#VQvqK&16!td>V}B~xY_bccSB<_4k3Z%uMn%lQt&`0PI>m#tNt2ye_D +WA(UaL3XO^w;2WT?RLvx**<7g>=nO`y@UN@ifqIr!J$$vv=v%YL~;OH;>9+~~KPyg}FVL~EO$bjP>2$ +~>u>)5AUt69S}~ibERvfbFBheGr5PptcT-6TdPQs68Oa_-F#T5*_@wykwTQS`t;XZm)Pc4kj>$Pe#ts +;J8}Y(|G{YTxyyS3VW~kG;s(E$wd>~!5Em7rQQ?ALeKKrJ8c(x-zPS!X@Wy?^6pk^-Ekp0D#ilsQx>r +$Wr1vNywWYkndJ!5n?}FElNlR~URz~m$O57pRlQneZKs>WGhkCY$dNs_343A*3-YSCqUaYV;M{pn%dT +i2d>Zrc6MHW2)0n?-9c=51NOJ-S^QyN}a9r{-O>Yf!1bWC>b9#aKRWLO8P{b`P#=JEdihQrN9D$HuuV +x2dg!OV<(wTShr!kg#OwbtLGa-D>{=@2)fo(nD$Ek#E+F9>xj?{qT(JCyPB;X^N3jBP;9`KJ^ux%^Cg +k~EwwKKC@&A3S9$;8M>W%V|jsfT~Ob9btq{qX$GAMbu?>t8q^gxph}(x^x_N!Q{}K41Zw&H4`hbLV3S +$Km0`nH*B&XZ2Q&EAt6Az!V;FhHU%|;O+`U*#K9EZ8Qvn=gQB{y;dU=MEoLX?9%>A_@R56)9iiepO}2Ml5|daEzI{gkz8d4X2xKG~xCk2#kugCJV~b@T4n(2|I;;I9uKW +=W94sS%#^#cNR#nc8Pu(yi9r{yZwW*k_8((;=IDod?IY +Y^v9*FaEy`7;)nsr#S%N8%5&F8u(_=0AacX!A2-jE;qi}*j{2iVj@=PES_N@4CoqjobF;N +DQ-VG=H1y%!F^7r&^#VdWR?=HYNR2_^ijqL(H3`WmBV${Yi|a-05ormnmNf{;nT=0cOno#Yt>9J$D_) +a1`KSyM~UJ?Fq4x4sqw9PseX;nxWLn_I~c*f5;9OO(l4Js#gl1&_wbH&Zg^* +?At$?7W+d@hD-Ypa-EmeWvLwUtn<+$o14kC8mG}Jim7^%)KEy}$$-p`p1XiKI%D%wDVZ>N_#2F{M2^#B{iz(1r=5#}+$6T|dq^x&ts2crSA~xL+=&c*k_lNmVJepqo8RZlHG|i@k;=U{=>E6<{7-u#N`6bA%@eitc9FK#rdfz~BR-Bvz}jnGIg>^i9Qya +QPj0+WM;|OVeBfV)aYm!KO$kzC4{&2qev#E&+T?*VY{l*aa5n`nTinJbCqbnq(u{**X!uNgC?|ZhdSo +)Rn_H}8(pj5xUZc@QAIayMimNezBM?Ge(N`5a0d8{*&ff^UkX5;~uXlVoJQ$51cMqd~9 +Tq~6YYNHL#KSbei&#C_4RvvIelmDW=^;WDFbntVpc?Uj(;uTzIrGLHxyYWF)8D=a=^&X#oWZyL(Zi5RG^V*P8rBPn)LhLGTnuE}o2D$ +$ZPO?xbsh~pY`$J)Xg+8M6w0gVAbw}UFi_5UYfx(WCi65M#Y`- +x+90!b_-bI#Dj%dK`?uRW#&JgcxkDuOAbQ$A4XW7R-yWb47Pj8-XSO-InR+C_jncuMTmxNwC@%%5;%) +Oc&);f~gv^Px2Ry$@(0qZ~ggf!|F+0o|EonLNs!*vjMzq)hB%_dnC)qGB~;@d2PQLTMqkBVWDF`c+7r +3TAh%O@w{%+bQEu*Jgrs>EAAK#|F@&v~`n((N1V*xcuNH{UGIbw@Q~G<_*t;w2n@{^g;!06Uu_brnU| +W(B5n*_5N%3A#%O1*#OTwyuWB+>8g}U!Zw6oc>cYzUR*v3o)_WvWN50LjtI+zkm7oe{xtreA1f^ISaR +WS5x)?P!MYOX#?okKWSinn%=-{&?6ei=b$@|YAiJ!j@wH?n?nyfA@@f%9ma;+vD7K>5WI?JM49V}SJ* +ICIpX{$x{T`m7kS<=cLNGD$NgUvA<=CC`}{rmUTzpLMa>&;C~_kVa=SH7;Z64uaIlf +>f2sUFE1-@eV4M$fPUt!GZ#|1-h3P0eBM{)O@$$}i!)eiyrN*~bPNl=U%^_!G!C-x; +6|9bmm3UFZs6VAMYSh%sJ9Kw9>$>kwKMIRx?-E?@spDm4}SRJ_<7{rW85P^2$68)9<2hqDts=t7|-#b ++%IL+-(7O_s|UWfPFx^fbansk9dUxy%jZ8Rx>S$;e;C5S{{M(zjFH{@55dc>|B1mQT1KVKf7yf}X3U6j7^Y6v~{l}8`MOcrd^!xJ592w} ++)iVz=kz5mXIg#hgNGQLuUuc!1wpL9Jpy)gIA*n5Vk3tjplie}MXP;;=2Jb)aTr^JvPgvF5&8WJ%J>FyP)p%cZPQ&?pfTE_1owIFn-)CkPi+qc6@|D#J_V +cZ_lD%=`i4Zg$32jG5720E3;(f^(%dHj@1!m{vY0_y{=xAm4UUi0Baw6mIsihTUKMg*%>WRyqrCuKA5 +QTh{ia=c8x(P;p7;U6*IK_s(^rv8GwP0Ed>58jSAc4H#`Lv{8y=t!=Adz;b0(e7UjYE$E@=sUW!?Jd$RP=AlB +)yE_)1H|9>mCX>Um(Pf+GkD(1Dm?+5j-c89Aj*%(k{O08Oz!UJw_PuB-bdaN2F +;jSN3BOJ|6|E>WWS9JHA1JOv4_L1j&?`x>#&)n+>!kjHk$1HH&9Ch1QY-O00;nWrv6%*mps%31ONas3 +;+Nf0001RX>c!JX>N37a&BR4FLGsZFJE72ZfSI1UoLQYtyf)(+c*?`_pcCH7Hr5E_BG(LUAA2cOABq^ +hCwLyb)u~zc_ca0?8krKD_N4|+8tVGU=sW4=-#7qKg9G}T?nQ9Xh#i{5`Gz!wuDKkEVEomQxw70Dj}e +qY{4Yd>BQx!=$)5ry=KQYy7~hoXl9k(lMnDXf^1>Jcg(U@Fk`?(Y%&!SWZ*+hxwN1q6Ll+iJ!o~(U<^ +GnowohP^vBx(dvXGcaATPeG$A#}4m9X3m!svv)ZrM>9Y?&h6s>_qt>KaOTtGw&7#z^?x`H^}Cr9-%6{DW2CC&wDVVum>O2?LQD>HG1fvC-lk$M^8(NrN`!$0}tTz19)Nl*=!f>=i#Lm*_fESx)BVm{TYjaw^ +Ovfc$nidNIhWqlj5U|NDw8p9dB;A4N4}Bkf&3i%JZ;#MUHzBhvYg}8pWtT7s+y6JO)6*{JP(u2oYhmB +r(rxeYUY(>M})psx{M>^&D+=SDlesgJYYp$kY7Rw3m4}Ge|1g?B^$w!in1)?&dS7rjxL@Z_)FQa&Zxy +2Yx2t5qx87B`GRXEN_>i>lXGtfqCIhY&aREJn#qO{6SMA0)bBhj$ho_~jRqeef&H{t;O2QW!MP!LQTr +xc;848W4+=So4ux4&*;cjfURq>`(*z4_lyQr%s}#9fd3Z*H{F59)M+aul?gm^QIw49nVoq8md#*3xj> +%ntFt84?9l$HJz*>DD#!k?d53I&M7Z41z0z2Sg6;%IYnT)7&}oZpov^s|+GH;bIljoi`@ +2fA-A);cmO*DatKH}DN5`V7$@L=v-e;kK-nC7*CL8wQ+X<@$~-Q-g=S8t^{@Id3ig2T)4`1QY-O00;n +Wrv6%I*uI9=6952^M*sjJ0001RX>c!JX>N37a&BR4FLGsZFJo_Rb98cbV{~tFb1ras-8}DeKi^;h +80uN8SG^OCfk*HK69rI*~)Gf8`syY_vsU4|kdiD!yn3DS<@`hV~40w4gAl69Gv_vTe|XN%-wvDn|c03 +8IuAJgovsJ5)w*CpTcs!iKMRBR&}c2&tXEz9rtKf0o!XLHH!)20xeWSM}cO_#Nzk+a!{9cM+(Q?}y`e +3Pu*r7is6k|U};&soa$=|{kt?YioYiOn>6S^UUnQY_eB+#^|lV$aelXR@nn(X=pc&RNd$B1>Dq+6o0C +x>f=Kh?l0$%2dkPj+Zr%+HioCw5Bz0Ti&p)%6S9RS9!WF%A!5cL%_ruE?b3#(_Aqv4wjL^!uC9^TAXv +uStnt7UP}HzYknx&T~V>RsPVa)k`=hbDucPjW;R}LJPU$gHrq5}&yr-*wOzxL1Vw=>O{+>MX#j)uy-d +^x^SuyjsVG0Al`t{1i$yyb1uoI`8Zls%bs+U}B8Byew +PzuGsaA!6(pUC&YiilYkp)aS_4d2mpw`e^{fG+#$EK8QL(oqm?n{7}oa<-3y +bXO84LnqM4|9lcJvmxL(8Kq`w{C@q-Mb*=&~c4NJf+7j?paXq!}qxb!81TM@hbJ!?B~^4D;+z+3E}?9 +Za&w@adL57-+HM%`w+unGPi#xJAr=KA{YH@CMhZ*HUi3>L)5h@cgE7=jq+PO)Kog$X~z@=7E<+&oazZocwO*YK +TKOk`&wr6^V +x4-U1S0ipO#;4n{*Em%}P56uWuf1S{7g4T1$8Vmhgu#ei3P_#t)E#;)?8@O_^@x3JCl1{hO;RbziB>1 +uL{8#w;mi6#P71N}=udrg +l_2biR5M$n&f;BIK_@gMQP(py{j0}a|o4W(W5Uj%(=Ly;*Lv|_t +i_ZQ83YUneOoa>xSQ7@a=Z>R)}@lT(8p~Z+|JfHRK5#tBu17q$gp5MBVIh#n)0mz$ChEd;dXSAf|&{U +2e{_HG*498Rb$KMNr>NR+%ejXo3nwRY>DiWuU#HK|YQ?Fr6QW0V;sk& +l>LB@m8$lk@&+BPak^^`-Z<(Re*Y@K*ygG2|G-xnqPndVxM64K+nx>V=jxa!8ErZC7&zPcd%2r? +!OmYXHZu-p8wOa%Y(nqmaGu5JsV{`l +iZAA}m2K_gq@mHE97pa#VNu;s&x^TYWNNz=X>wZx*v)fa9t)4xaze`7 +)T^#e|z&Dz1#>@xaN?ZS7?dk6dui@pQ6~pYGw@#f|xe40L@VscKNAHdZVHlpvfVD$=Z?oCOw<`yqqrv +s{>QukPT|TcfdSQEs~t@L3B;xQt2-Dl;-#PuCj1+Pf#N^(oc4aiVDXNe-ID)Acuji4`gp}IPy$Yy^`W +2X#n~`FSZa!tR(RPgFgJ*@$|kZ4>s%l{`>lF3xr5mUB=NFW7u`keETH2Fb0Y>e(rvGV3-7+pq0UVd@gOYJ3LCP_lnGwHDT3?ngu`4GyZXvdNByvu +lkja;yfWG}a&mkY!1-b+ZSv?@qW145(W +5!2sEr%Hw`R+W1JQ1ulSbyzYD +S;ieHk@@n;=>A;fcmaC!f&?hc60w(}DAi9q16Tl_qf9_q~>R;+6zx!y|lJY9qQK$4sNjpuilT4^t5&` +LKgZn(IQ>C(#wPF_JG!d*Wc`C1&GsP5`Y3Q;LsoTXoVg-zvVcB4oBdHyvtJSC%v}BJs*MBrm@C{Hs1y +fdmy(Ha-J(0p|t$9I17P^-(;524%7ZB+n){&iMZ3kJiyZZ-GvxF*zV6^a$Hp-Rhj&wkIUx=$*6 +8v&vTLR?a^_7TP8uvo|0^>_+UjOFv|5~!OfQo7Yw2-32eM1egVsBI6rFyn4#X2n$-MzrLE89V<(IUdA +Z$a}SYgJplA2c~iu`Ml7v||Eb0Zn2WuTuauZ}vsoGc%zbBRK^L6K#qu_Li}I;8CcUc$`kM$1(ety0!t +G>5cESb^D4p#^|*F5Uaa8XbW$v6D#}GKK59)@~$k9A&NYty*OUNHxM;*{>d{T0K +|xt9@)Vb7j8RN4{dN{7Vbws^rSO@JMvkVl}vSSVm4jFVk&?^t()@5O*>ui#u2f)th9%NG +ii=djWDqD5e9#!VpVfYID6Qfl*f;pa9vs#ay2osm#Vj3AsNwYVDw)*i$Lj0`S4b$1dLjPTc!5yBI15z +}~9M0hEVBih*OG7-|n$fvRP`Fyu>)nIiZ_e7+GKWMQH1KbT|?KrquZmucC;o#=Nx +o?Nuf;D=q2?QjzX%2moo=ww3IH0Trirs!bD#OX2OY>-b3ItWN|Rge)|F +Xnw*$XGK{>~MI$FeYS!;(E|{-zHa7TsU~e&xU0?#7`D&QjG|{wvp{8|qrY&_L4^KJv7vFpupRWpRL1Hn=QBYxPGT7np4H(jOnG&IRWL^dVW^SJ;!vzb=0*R<}3qAWpEIVP{In( +y7$@7S+@{TcPTynVbXH*&CR8|c-fAo^*)Ms1#@AqKfCpINUj{s$F>K=T*>CI=Y&l?X;AzdfgO_81`wRY!Gp7D7rwySfDV?OvKb +N{PxJUFH+Aj%kT%%cvsw|jN$oEhW@DhXY*qHGyS5L6gPwr1_iT4V2JP6PgeI#pY`)mYO?3cIR8w?xL} +I!`Q&x@BkQ;Dnbt)7Sj1?;u{7&ogpq}?(dKcr|AH^BP99@qlP^jos!c#blpw~M)pFe7J&-Afe$Az390 +s3Cc9ENjF#Ecj01FBj&93h@;eT>oT{RJImEzKxhcZK}`GLltq;^0p9WM6$~UrX!IBe#|u6#HZJr**-t +NNw>vIOemMLOwpsWaXl^xTbOhl^b{xMBvFjz>q#$F<*e`&t<<+gm$>VoM3^;8y+RosHtP(xRO7qws^x +aCYWFkUPHR{(5{f$9b+p{erkN__zNi;dhM_YZsMExRiNr~Jkb^%r`xsoXC+Mdh?`O5##MC$k&f~WX$8 +sBQ=bAgV5N>LDzwIfQTrXY2~3!z_67mI)gZ(EizoU`@JQHWgnU}9^p)PvbapVuCWdE!T78PqSvG~N4YTi{yPd<0^rjzR!@BeA-@{e2ehgqjPJR?sxbLgqdder&1IAC9=ZWswU$uUSCoI9y5ieIqHa(_Rp>zc|lvcrrsy?HFN_KpnLX4L|`(NsQf4I~eH?a- +mEOa&8r#DDq_7z-VXC5~?(ck?c7%X9E!UNPkxVq^o^0D@*x4cmw)se2AYfOX|$tJj|{zQGdM-4hSLvg +Ga07%=8_PIS7kB+yYpD~wnTEk~Ffpwx`qH%Prqf4n{pNAaj;;IyKvX}dB+r>SSLwgvW?ezX!{aftD>9 +=9hsh+HAk)=AzR;?#hMdLo1NiXf0iZ9e_Tfr`zdInqz?SvB&wVmqo>dQ)>9E4;`TS(4j))Z?xc +S99D26DAQFPYy7rmcH*h@RD5Qww^XMeqKK4IP3+gY^3uNBY@)UF4NS|HBRKGg`{p81*w_nl=Oe|I|`` +ragYN<2JChe%RfZ?_*o;X +a#Nm51;(_P=n>6^r7pJIL01TqRvtw$6mAZMo>NwP`39O;ZlGqey_@QGEy?l?{CbK*BEUtJ9Wgt3~iFG +LzbZfw`e*np5MOz-gMM?r=^`S28hCekNb55smM1|^4FC$@LDW8>xmw5#53{I{6z-V&g$UAeM#m>}IFz +7BSXOFL7PzqN{!FEEn>cAX{;iej%VR&@dL}i+uN8%WD0Dp8m@(lNh>BvckdBU?j;}d_nCsD_NJVP@fM +BiVIU&Q-g@D_?kKTo<)Y@Fp`3&8A(4qebQ8~*0wAe#)1zPwis_>>l$A#Tka6L>s?;~4aeYMrG&&aCIf +MQs*SiTjD9`~Q@q&+urFTudG1@2Sv0_}*g}_ydClyS|M^gMFQJ-!*YR^eWD^e^xT`$@G$&^NGY^9Ep2 +;il&KClRrM4n*7t0(n&&HaOd{Vuci1?`|bwcpE+6<-X#z~6MW4rGzF19QMRC$-==b`({AXzkg+mv)=i +!sDURuUe>#L9%+(+{pH9nZ#Hp;D08Xdo%CSC^pA*zsNjgEEm#J%x91*y$LS`Z7L`ytxZ2FMLIr&vTf!S>JPgkU}&Cl#GyfHGW#G4oE-X+Yobl-N3YYL +#soBGZ$xSsqblyz5d(lvU-hzJdOM2Xn!_?e2ur|6dl$7PgMFs`wzTI_A7cDL))F}n<9=V{@rBZG{SMv>{o1Vyt8y)Cmd~4{H;K2+IAx ++%#XM6TL{!8$`xj740|XQR000O8ZKnQOF7-zkX#)TNItl;)DF6TfaA|NaUukZ1WpZv|Y%g+UaW8UZab +IL*aAj^}Wo~16UuSY}b#N|ld9_x-ZreBzz3VFmx>y1neSm=$ShUR|Kz3VXgS{ArLrYZ5mL^$}N-6~T_ +njd{*@|T>P_#b8v@{&PdGqEmtr2gd3r(eMVI1Ovb4@Zt7m2|@vbhc&|7uQp-F1Grv!_M^%|+B+s3cjr|jA1yG|`#m+{QDe-r)#|aDfU +*O(4&K3b*9qBd$43Pn3az#fR!i8H1Si5ShwZ+zDpXvgja!6~$QpAk)3$P;cP@I0Xa$X^eI)mSA~q@Qg +?412A4!=rJ-O?UUMb_D9dWlrr7KMBi8Z7TIFSiKLJt>UJRmJzhlEU>cVfyo@fqn1&=ouIBdZbJDGk_{ +k&&Q+%^2cse~hP3FSu+X)B|mJY3KjDN!{niV;Csj)~$xLGdq>9S``IaNnD>yw&M +HZjuJ!<8^m)uGcT``rV`6B*Lb{W$2Ui5Pr#wzP;DL0#;2x7Gx&;jOH(N|YqsN)0JS{Lo}clUgVH`gNx +Ob&lRQxb&)EEfaE|6=LVq>Wq5K0=glS*^7FRLb0(3C%>P@?jW5AxzJs4RTD_hT$zd5BRP=P41SXJ|4H +sLb#x%^!$F^T#(pqQ~Gk)&V3(V15a27&ZkFsk|LCssb@N~1C(zZJRb#qmf+cfS`7_FNh?F-qd_A{+Bm +blH-B+<*S-cDTuIeB|8Cx+wLMZbfa#&67j8sku%p@mb*5VYnKuuiWnP;90GC)9G0HaQ{~XwrEYHWw66 +*G^4+@zt){<0-O`4zMzvfkG&2GY-wW!|J&lpp$QOi^pV%Pi6;EUb}yeFqv;=V! +88&YFwa{{v7<0|XQR000O8ZKnQOiL~?JrWXJJ!A<}G9RL6TaA|NaUukZ1WpZv|Y%g+UaW8UZabIR>Y- +KKRdA&UQd)vm5zw@uyk4iq12?bep&#l=qS3l@fSKCr8C2fyVVUS#ks6haS1w``{{qLQbeFIBb&OMNP7 +O{`no!QxW?JWBJe(wYSuO_YdhUc|lt2E=ROe&G)*S$W}_EuG~VR5`_>Zamx%+gI+R5eTTyr`2pEppN8 +sj@|}mZkGZ_NVDBiY(&`noN|;7kZ9A@|;& +mT~s4>Qp2x#rrIx)I$30i5M1cCrbxEX^V8KoCt1T)UA-#-6J0atF?cTiem0F?{b~B@&nNGH98uAWd?$ +arO0o>cmT&Ki4;)CDk9tEz+%herIL%>(d6GqoEY+YFn5grp}0f1?~2TIFN`%}qOPu>B{m@vRnSha ++iZrlBPN@}?~!5$>~Rh>csGqdOrb-?qeZbPVW>gX|Hoim*X1wxkN5?*)o|P&VUv?#@73wMchmP5fnI} +u{BkM2A09;CebWd2IAHIRda;IS^ZX{QiX3rbH%XP^#12KhgY9}llVNGj9({BB?(gx>FFu^Sc=>i3AKy +J<+q7P@dd&fp?#A+D!(q?lA{8}qa=O=#yf+qPgsg`WXVcGc-r*|@)PT6_AWXF#mKaGw@0PD%)aR|4X*XRy0YRcUhS(@lZ@Ai0GA_)SU@V;luIt^M<~H?FH +2z%CQtl&dVYb>q3xg;3?uA0gw0#A0{3UI)72MnGC*m{4~Fc5P|IGSasXNM^7P#~OSM)@@_MW+iwJ7sp +Dx}UKa1Z^u|>j=U(?Un%Wv}Z2{Zwqq9Dy4fjFvafx>X$0vWPx(PT^HGhkFs6V|<{V-F{zmQBk4`K|`m +mkPt6J@7~W-H7#@dUg1_{%{nkfBHlZ#G&-E~{q5d5?XrFCj-RPU$Ite*up=Hn+dm@F&9;* +x!UCR6&z?Mc8c*N9I(-c$L05*)`v3@7bT*z`U5f7pv%|}61fm<{_Uq}J7eBqdFrA^NqF=9vh9jwCWkE +*Qynah%gE&St5XT_+dw(T=mBGxk9Ee2X~5u+L8muj`)mMY2M7 +J(w*u_&5qp`ik0@Q*W#F>)>yI&&Dac~N9i=9nqimUaS8LpA;wOoT-(ts<~^uh=hO;pUv0IP)Q3y6$1C +g|^tZOWQu$Qn>D5O%cXt$A@l~59ibD|Ft~EdCuB2pi*IZHs8fl>oI6_sRXfXMA>h)6vRA%@)fF&-(E+z*#tF{A+hfiEBTdbTHfPmhP2``EUg(MXYXA*h`QeNUS}`x`mWaVevZigRH!d +jDssPRuU_5{;uhb504%Zns8{oXm(i*3k)i^)o`hONcCqzfccO@PYCTUp99oB0xj2sKiV)cR?iLV_-=< +opuy}SXf*NPTkA&$j0Dv`j+Qjft94MDIhC&$AU9O#J{#Il_&5420Kb8w|&(7aN{-P@RdE`b{Gb`xuVz +S~35$`-42QQYIu!r>(RHFk9G*NH`1+r4HMFa+@XjWn=vIbdSo6_U{o>{GApZnOpnT7kEB4E`qs;m5Ea +0f0?XI~LaAqL18)EgT0Mv>8a${$b_VN7Te@HosV!0-n)?ekTSlvAS}_uH6hhg&qhF!peBA>!fB$S!TP +A->NnjK@I9zrC4D=lSa(R}v$3Ma#QAMl+7$q@v$OYITUXmn9UIFrm;VoKtAHgJlr>fv0S6L~$f%U%D(N{!+oa0DV +DQyt|63sy3H*hA?C=nKvw2g)=5`lfDxYndWf&xoqIwxQQpF{fU+T&fwFq{)L=RI15|USoA_FyJgh*4u +%$M#yBP{(GoWF;{aXO$|pl1iL3c{hnK=xgRLAP6BG?eE{pSna7k{&CR3qkNW?usO-Y!QU+K^>r>W&ij`iSH?i4O2ePRrE=6 +K$Xybd}_+7hDTLEtIVBne=f>4HCjx#IKb~E3c{O#QoBu#DOb9ssZnu2}eyaZPOM2Y^iZ+|ol;s8!~&=|9;DsSm;ui$@|siBRqr!9rt99~gMxdRq8Cw!2*-@z4F$(QzbA8ne~nKt9JT<+N +yT-6Rvai1bbKs0G&u)ql#!g34**H8+Van-0BbV{ayahnXN_X8eM=f@{3Eb|kRQ}U{OE)f^WFGD&Skj2 +DVHb?aTSvWyjbEyG(=iX3qGnWl6@Se8@T*ujU)GfbK;(D4)$O&X`i$hR1Exy{W%J5@QHsggwN2ER4su +5Gjm^io&5t=^|&)%0#f6dXtAY}QD_Bg#uI`5N)U)W@!1Q`Yc+VlQljw3Gan=W-Gd-HP@G+LnJj3{X+y +4M!-dvASuSIDpaC5_DXH~7*QSBTf|AWmTYZq3;p7@)%h0P9iCGgqsR$v_QpRj_Y?z!l~zCZDv7Bs7~=@H~{R0yiis10<(mr5lM+2XF&g%R=5Jp4odY=Q_E;kRju%70gC@u@#qj6+!<`TDJgfM-55NknqKf0qpLK8nIJ +~ujvuPP}wHg$pr_GAI)bpsR6dM(3JTcM08n@jqxk+mF_R$d=I`b|9fYS^uXB@v;YQX_Q*YgtEnTpdde +mi?pmdTB&*j&bG>FDeE`V?9c&^dgpOns<6IBrih$gv=4vBBkz7oPqq`5495AlMec_7xV~U-qkQYm|no +9Z3PSB6Y??oT>`I-7Lj(yi-3Pfm3RokPoGS@HbILG)1_gDa*4od3iK^{Vdew2Y%7M5M#8_{fklZBdDpJO#p_XT=wdH+{MO-RSv<&2INv)MJ|J-Ic=!c2Vyc%wOI>d<361?MCe +_hy6q>YOoMgT7f-3wFh}uSEFA=^_3E-(oVM2!%@F<&VbJz0YZie%B8NGZ$osR&7tNYYn;3%-=XgsfCr +LJz%?pD9FE(E=G>V?7dk%Z9U((_;YGJw+iBB}oKWnhZrKA$zN2ihqF(DGdVy#ozQtAozRHFddi5IV3@D@CFI={-UwdEZ?*iXTMoa79z`Z3r^xq8O +(U=Q|8WcYqoRvm8CT*9KzZ->bFR-J$Q%q_MbT~<=M +Ur@NL20;B1=+#*@Wb?(kJwQB+}(HA_k>?!du}(tB755!VzPJIQa7 +EcL9Tu?&{oUFNquf_8yz_eB8!-G;tN;pQH_?YMD7;-I^xmq`G-3f={h+nP +OWqmri4UF?^lUBM{Kispsmra_X8M4(`LdbR`v54JZy7d+71IX9a4S|)IY=5>_m7qnL^%6rp?YF@eJA#dv-Zuo-uR6O|_nTLKo`Sg8qUB0Yw=WjkCtlRo|BuQyFCZ1{wq3jLJdq~*f8a-GumB$D8;^GgcIws*sg|B27soX= +*ZC0!Byurd2&Xl?%HTeETuN_`yK)YSr&8EO~m*mc4STI1S)ep;*!IY{h}!?uM|WKPh}`twM{@|Lt}X} +4*?F0k*mYwLpKvIvzz#!<9hF5ncR__pces*z2X{e@CvI`@11DlLz-!}PVpA3!1N$PGEx>vwoP_;5`#Bofo@=5RY7AE6 +H4K~@4L1&;-U!?bPNegCBnP5K0Yd4G(`UU+W- +Lmd?9uHp)~rSF6-eLY3NqGJ~s +y=b4~~Bpr~&^F5ILE`&bC{kf7T~bqIZ-50*()VHlY_8R^0Wp*t7a1E=IB>i)5P7ylzFKPkXiy~R*4-s +Bz2X>i>`>sJ`oomJ`G`^u?_7cZNHJrW>W&RSn_W)9p7l7ZQD!@<#+SQ1nA +Pn0R7nUlZ>dPY4GFD!ppu`c-KMCsgMI%omm2&zhRW~b}EHgyG#PEZs|91Qv_J^Q`FO#D(EswCz5jOkb +W2|QIGsQO+>!boh3LFF^xAy@9{m}tAxc|`sfW`ESF9udrFr{+$rT9d&g{UMs-NU{zcA>8{hBIMF~-2j`Mn@t}ERk|^M9CL0lLL16-64~AoyQ<8z(K3zh +jR2*u`(2T}V052J4B!t0(f7h=uXzSbC2#$sLA~b5&5o%rz_3lRY*73*z?c~ +xsTEmM;~7NGG58Cb+=k8W`H)Nlz0C)6$fJn?Aq=S3jc_9}qO^GyxRZ!NzAxnT9AJk|LJ1ZkiAVBR%|F +-Lo_GNs%|_igr%bWAKDv}>c;PSdDNuwB2w1W~@tUxC +|DfLfPprG%0G)!$yv}`hHHi&0RVJ!=L`qQ%hO@^8xqc!RY1;3)8xB$S5W?fyK=by89z5p +!Ns80g)Rg$oy~~3i6N{bs%M#p~GB>&6OI_|WDliNmi-{)6g +p>m0gN!W5M+nF`Iv2|osrQKC5Yc~~_3c?Ug4ptz51;826llkv?N8jDxjGWy{!YXMc^>V4l0Q&P5VHSw24jF*S0ZuGCCesMU|mHPfm-EZ +qgoNciOo33fZhT6-@i;e2W6a9FVyqLsLUNmBrycT~uh;}2ZcKy_s!zfgpKbOrxjQp-Ir^}|kP_JIWPk +u2Sd+51({(XBqlNIVdE3QOabwk#doA)ISFx!{Idohfv_#f!!sR{%1upY!-w3Dv&qj`3!>e!1-y{)b0U +aKds#mSmD=ep9SY`eDJluiAMDB5x_PSw1E)?kkH4D!#9sKE;j7xqP=qJK5MV)p=)t&SIHl{KlT387%1^!BYHwP1?xpSPS7;r{j*` +{hE?)v5Lb0~7zyChdZ5QKXic{%9bh{#731boI8(2S@5NMB+2rLc!5vHuIgiPq~X(To}Qqc{wWs{53lyctf_YU +=jAD*qC`$BK46YKm7Alx$U#?e;{*?7)tQB|TC5pqG +u_izo3fA@Cp76l`{))4tyDV-+Vvcc=)Iq4_EA=ap$JpAZ&t~^d@rnry{6bMFd(q_uQ;@_T?O=wdVsXv +C78N05z4F*J>Vt{NmW*lL^WUSvK%jk-sHchU-PB+^|*0Y;z`$T>m4xLDo*uy*OudZ8har=z^)zn;RTd +E^tjvkH*Dy>DUVc;pIp|J!0pGs>amcMLCdS_LE9zFZFvCr*z>|-S2HCDW)v@YoSHM()NhD#Jg??>xPZ +mph*8h!aA;vK6xTI0J_4J@AA5LT?xK{G!2orlnNdJ~g5OUSSzIOf8r~M~>$WXHN;m|70_a&m=gs>VjH`_vIea}kL#av(Nv_1J45soWJzEsS$%|rnc6PRDN|-Xqu`Jo*oE9)mQ +~LWOV)RI2$k+jCxx}$em*oT-?U-B$wQWU}0NaCRQPRR!m*o|{+;+pBwg_Y8MR%AI*x`dnrY_lvX>$qC +1mH^}uF4h$ZaM-oWR;Z7*nx1@P9-#iBdRwEz_e)+_edmTSyy)bOb>*SU2H*4t=oDk3f@Wh8AY>Q>0eO +$+?KPF{{ms46{f%j=3pd@K2Lxd!uxOngG!1%o+lR|{ldaLhf!JYk$;^N=aN;Shzmi=kLOZyoGW$!(Ww +{x<40f1re@03V~2}X&2{LX%-3DhU6f7Hv7B=beI2KtVdQV;U?X`m#jAQl#aE`{Y|RZ3wqg|11WW2v=B +d6H2zU>uGR+N5e1P`=+b9M+nmk!7orxTZ@zD28JwHpp&-Q>!n)?ys8e^rG$@)9$>A9{9dru66?ywp>m +=0*qNH`6u$pYT2Y*bvXu&{%-$iSeX;ITnccx!(6h1670A@##8q`7^rk=QK|p93f!e{rU3KLB!a7&d}Q +?noOLCEzB}vZ$gH_y+cda5gv8v0#zxEUFdGAyZsYWw13tivcmzBFbaZ3Q=Ln%^*s|Go&&Wq`vD94Zm< +ZSm6>~9H~3jvVeFGDvKC0Si=cB>+}oT>n3Ou*;6^x4cGUFsKu2GWf&X;Y=T1%5>IrP$(kuJ_6S<&E|5 +6FoeM{a^YEO@pwlL(L^pcI3H^@Gwl& +!x3z#soW-oq6Ne8yoo;ZdLg=16jzOlLyJGXRngSpMOK$wWS`r6v51iPb?&urMsxI1bufWCFfPL1_`QI +OXwQoVDPj|SUT6(#bytfwM_|1Y0_i&bIas=IDc@~7z}ZOPQHxNQ&D`IS^+CECsLTziUM80keyE{aOgBbWjFT@F*d1!32qnd%{QCrf +^EDFUD%kj4o&cCtH^1IRpr#|5$!?0OJt00DqMUjfp=7f{(pZ#W8kfzs8v(U9i{f~pKQQMO0BbAZZa17 +xr|u1QP4)nGYXcumqu7kc~Mhh)%7;}Yp#ae5GguW;Di^k;S(l8=X=_ysa7`eGP=1GP}TJ5dF9RZwk=b +y!z{C0i`*j%ib$yZK!hst`BHzuXx&CFD1IJrSpp%dV+-Z`lSP*srW2Z+ZI--md2LHd4FK767%!EeJiu +#qm6_Ce1845fh@4*qhq2fh9^a8!x*9Zd71-fQLruF55(*o(0Y}G{L@rT_{{(-yN7g25JkGN7aaw#q0? +bs4tb!8zvxXPrHB$(h1pMNQ~0a31=tcHSA&-q*eED6fkHo>7@I@dTUEg=i&sG*Rk?k_HA +qmj>+f9vSC7}UGmb~F1yrZqake%OW9+pYf4~nU;ZUJo{KMyS9goB}JeBlUV&!MX%%{=;g6%}@`g_>G= +t!#SQJZeDg1X>-SDhl&|5hS|v!ULA{XuUwTN&O|>4tgTX5*KU}vp{W-(6r-$^of#NX88vA`v&cC_(a= +N7o%Xs14p9F4U#rNnYtO5-Wq7Ds~&EQNjfwf_&s{aHj)N8n6h9ykE0iO!k)2l%9``qx!vv$VPh<~z=Y +I5EMYP>?`bcTS1`~}<+H06Q&&(0yXhRMh&NJ+5o%?yHOUeHS@km72;u9QXw@iW_465r&-|#rXT&><$U +jtPSn_x2$nWe*M1wr#3`icZB#@n1h+-V?S|Au#Wm8x9Kj1b^AQCbQ97+O2jk4qi=4l85I<(?mLq{0IV +lPUx{v|0*4(=g#-lP`R5xJBKtR`FBx?-6UygveH2Z=*BpvLS7?~^3Esv=F!wkdaNu|P(hP;Z8PSmLyS +6plj=>5();F)@-q9MO6!`9PvM1J0OI(17lt?7h*5<0w}##^TCXs*u$a7zs^&sEx9{G`lVbob*NT9zW@ +N-F9mLw^rbGEz%{zp-;vnjn}#mn-bS>z2@$&ozW^!ZIz{~Xnof>sXogyzl=NMgm%8PXfrybqhqq~Xu= +IwRe~1zesQTun{$_zsuozau&XLIdv~j>=qM{i*yfyIT9m`)vc3{he>-QLCbHkz%nO95C>B_gY;W-MJi +wq(86bIl-nFq%)s|6YBb73=btIqW$u?#6`6lKUSr}ahRr*_lOQf^Rz>an{gmy}i0SM{0q4xh>8cda~7 +AT{ZYtNi3t=-!5)$pTB<&INoF%QpOgK%^n)b!iE0JJ@RmwI!+s{>m1jVgT`j^8r-`y;kYwq8*(TQ9?2 +g+2s{#9sY-4A8Okdet++2%5zJU+2bSPYH`4H>T+A(oD=Cro+%CqrK;V9?Wk#S;3{Q+w|KT-rWUxqlzL +^9;#cZ$ew$ob(`J*TyEr68>RCb$4vQ~;nrH`rRgNwbBan_9CkAuP1E%I76{aOV&VT%G`+ME?j*xSu}! +S#5kVp+n{t@pr<_=Q6}ZK&%3=F90m{==l4gJH>UKfDEsu&E8Sw?=eh(@1 +kuZ9UJlaoJnU(hUh=8n%pby;F>s-L(D@R^3?=%lZqVx$0{p3;EO3__O_G$v8Vx|V~?qZi9nNGT+uw45 +EcW=ReB1Bc;xYsJ}iU{OFOTfhzXGBOqAJ6%Q526EW2NULq(5bzK?`5)rmg-$PHY}uD(c03}nDcY?=lo2Ce8a^e;1%5G@MuZr@|*CI +%s_rI9+%A7x))_ZZA?otNLqchL3B?#4$gmK95x$Ck{SyhZ@w(^Zy9o+pn;?Q6w{X;DvzLFV;RhD4EdD +Y;2=BA+?@xiuV%%oByQ0n_X%n^2(*ROdRhHKO~^6Py5yv#A~=uUHx5rGcWjOk=qfpy~?LGs}sUQ)Qrb +YvZOymkAAk)zl-yapfAX6Ugz>bmf@T9fSC+7%A5PnhEKZ4IxvVU)-eBdV0>ixLpz(;<46k(6t!~7D{H +aCq@qoH_RmfwvDIv-#0jo@7wOWUGKWCN@O&dqfWAckwfjwHu?$5!D4Qv`U+QWy_R%hEkR%~iF+SC-s2nvbr)GAo!TnxF?X_5YT(1HB#zgEdM1`KTx|LB(p0RgDxQb;pAj|(o0ygX}k +a@%hz|jF{-+E1^w`SjsQ_>6UJ+Vh>&(R9dE;FKkQ5+N2kdEP^E)?3-%nRzhwr^WjP0Q-7@S|FiJ%|_C +U`;(`$-a8bj=jtBi!Z(;)PpjHt~O0~&2vIsl-^urLTV4yPQb`IAm=dQ*X2+pgvn*I>qfNmT9B^G)x}`Rqz{_03?5HKn!C$&;o>U$GY)Nvj81A-~_>wb$matb^(($0tY!8Kf+Uhl6 +I62d-Il!Vv;7&gF`z7ATLxjvE0EFnphqt +%t8>84DVbayF?3S!+>N!AVCSO4RQ-;pi%5sYwG!<=S3k~wDl@cR8Mranks!!#$H>M?Qz@MqJ +$XT4)Q?uj5UH$ZzhP@yr|0B!S7gyV=R;9lbaibC!7uKZ$sWN6fSJq{ZwdS&)tD +ELh66Sdmpe{ZpY3bs`R5<&9j&5NfhK#a`{x|0?(VjE+Z?13NDC{{>-Qg|W8xuwZLNlKJ}QymHc1|x7e +5en19JWPJml$NYP4!QNWyDK$FzzLdEZ`7~b;sz5LGyLz%#WK4TA8u2}iT>h0{_qUcI+|0I2SF~K$Qem +X4KqUFtHMkS;>_=o#pRB};(oDR*4l6G^qVA88D+^yA`1=4BkiHe*xp8Ze8)S`?om4-GjCkL +L}4Hycz!PEw46RKX3AyL0r>B(GLV|#~W=$+!d!Z@=r!cc6OI;f2)y>OU2lMu~zgE^FQPD!jaB059kUc +z>`#w3MnL|-KQYlhP9EL0+1P?WCIWm|xwUC_@Y1un=~IMtlp6x#MPm2~K +0ztfO9#lXd_eh`?>Sb}!r^G?6pVP7J#=FY0+QGMM-w19q6yu97-x~1ar(x%}skUG$4Xig}!ekNmeu_P +P}+Q+sFNixQGCx)^HSt&SEAs(THpKbaV3}IxUpSvuT2MW-|CKDBsg$7Y99FN>$XdrCAM|GA&9$Z(TVR +&u`LcMWEG?dpY+Mfi7ejc}8BCD-w)%sqMm)*FrZRr_*{fL`oLje6r3>E9jSd2Wx`#6 +!KS0Kq?||x=r-_nIeKe^j$2Um%6chM`mlMlskrJiMEkbR!->nFEKNd%#Ci)IcLb~k{nvUaT +2;f2?6b9Mb18zTWJF6K%^oq8(6Q53D>}6j1WSKh4ix_nqu~0Stu`LYh+^U67F4n4zrvd=ZUt@{L0`R@$)?=-K$G +1JqUamRUE1W`1*-Uy(isiCYGnJ<~=Q`fiGITFgqtrma{_)4x@&X=FfwE*Pe~pg{otx<$jR98U72uU=6 +nBn^t)b+S*m$`Pe@o?v$*Ga +vVd+D>skG^pNTQGA*3^ggo0a$u)NiHalCdt%(9kGbuzxwKP4H#om$#bO+W55TACi`<5abzjZLs?gbp1S*d`H|HW3%00&C&=DcTid3uq~(X%m0X;M?&>_GKGiCaG@@2*Q?+$q{ +r5F903^8&-5}7137!?0C9i8#-D(VK5iDKCU=p){-l#=?2qg#e0H)#LDq4|qSP-mpDpGBsB8FIH(aKL` +tdh)7E4`pvHUN2en)xxcM!4YgSA7)RiM{yDa-{mHm?ZbBBtUD!y+IXUGd5f%*`pP4bUOu;rk>I@xl2x +(W#&PY{L!#;2~Y1ovD>Zj(1W`JIzLuWl3AU~5=16RD7H;G2|^3#wwo?YGMo3pd$fKo#wDO0_?f35)QX +}aGbirMmcK7Wi3EE%W^cn`$e=fO>|xw$qodMuH%LA1F_6=jN2WAhK`v`9|K62LcfxFNdbIyv203$@#r +*~+k9Q2mc>nP^94Lq8F(f3o< +#brP?ww^73jIDD65_j7?_9`PFWAzPnwd7KNt8M8+BrFEVF($@_w6yTbOAp{?MXU8QjI|AGB!xfZ6xhg +mjA_4<7Gs>wrd*3r|ate`A3+9xj~W4{g{Dn{+Qgw3>bU0bRMU7(2b_X%78b|Lgu`bULV7wdczIt9mu< +gb!{1Ll)HSH{?5_8Bgyc48cSR(+@=w$qa_`-oe@e5jbfpd?TcZaAoTX(M5Yf)dNK2nMN*-fT=CK@kgP%0*jnEuS2U +y34jJDKQIgQ#AVYUDJ_Sk&sB$5?tAMCl!xi1s#5QRhQOIC`zLq7QX=A0J&MS*NrE#t!lUg=&)#^+-oTa0q+O$3aa;P(dr +JTg4^^q5EkK=jEY&gnMKUAnIM?4S2S^vSMs!YH}Az9piiWG>)EbSxHy3KMgXr%tczWTh=g@g5xiTskj=isX~%M +GZ0Q6;couF33`Whfbn#(nP44Y7(^23}{T}l$L!hZ{u$SX;59A{< +jWa}tQ{qtS*y%Otg}^@XF`Xj?$tV*cpy^>x;b?qli;QbUuS?9fmA;}-7*(w1EF?S#u-uh(8#iJxt2B2 +iSR*i;@z7VcrD1Mwo_4?AA!0mXiyk)x)6sWh&9Wy-5x=#c*aw{)TH;GHl94Sc&d5Ue@nWZJ@(>tm;g4M9NFLA)RLQo8gxHdB~**E(>E4%nkp=gOVH|GN2#-{EUZoaIqy +c`zSeoJ69Cehw`r$R^L`h`dJ!9HFIo`HU5*9JQTL$1_*kC2Y*{!4?l#vbbK>y@)x@)5XP-uB3uO{=>;nqMjOck<1Ikf5kNkZdMXnFHOrWpsAT{NmQB%ryWkytx`_0t65~XwK8(v;`E>$pWFWzN{G>m +~dsGfRGGW-_QA;ZIVRq#AD6a<)DIE5kFAL3rsau(exN8>6$Sk5H2gp1v){or%g>>7+pVQO>X*0-({Q> +0zd1-0(pLLI37!+E+! +06W&O9BNSPyb_>VZxwToF~_GZAZwqP{Mh_lwY+WI^DQLRt+YoHQJo +U1X-7L^P+kD6TZ6QVVKU~PCAEVRK}Nf!3VJ`F_>w?5uhL082UbWmiTeE#9lv!}_o-zJZ~LM`$0?(4^o +=juz`$gYPPPsAJ+3=_z3q)l8MwU~4OkXqRI`jE5n*DsKQ{=d0GdbA#=)>y6z%r1~K!3#YmXL2k8VWxnkJj@lLz=m-%)cvIW8qNV>?j?9$(q+Wkb;O +jr+Pag^0Qr^K!pf{>-P;?91XaSfi(}Z|-08Uc9;YzJ|J=to^*R3MKE6-j3(tAZe}eE{g5u?ex$&s+cm +;F+X}D{FV~$;$$G7t~eaXRpzRee9ei;a$PikCt8wh4ub7vhrjDA1L}2R!)#Pf?kHh}L%w +Gz|}?H-URyC2~$oo$Z>m?W6q_`o_*LGpwrH&_yWR(Jk4{2Io@Hdo9flc3DxkdHTiU?4KjzW<8Dvkw1F +$<>RmKPzz#N2*3J10Dt1_8v&YmWd0=TB_Wt52)N%3rMeug)LyV|YtQc7+QveKlLevJ@_%yCz|3vW;rz +H9-ne!WIcesL)90|v1+l?34gD3WNlY{FBx@sa>npHniC)`KHBJRp=fh=d?V06u6*uUls#bHvmR5-YxL+?#KJX_wFXNpg*x +L4VBpt4a +%-T;qXd>g3RPi~^@B$XkTUHYqn7;Ct<)!oBd6TA?Ke4(=eZ5m<*-i5YlH*iRgq?LjbMWb$Iowu^ykx^ +`#hgH3c|GL6a)Q}8#S5gt?ip>mO|YnI8{QrdXsapMQDxABBGif2>j4(M=m)B6j-hlxtRn*&1$OCgnDG +3lEx9(^8&`_elgkxo(o3OQsRz0exrpXqihx{6v|Hx`nCw<~$(pYaEoBM54W(x&)u300NmYp^VvkA03d +{@(I^dVEFd-$p0IL&2nLvuEJNU^>Sy*rbo8n?iGTbx=3e^5;W>Iyon2o?eO5%N*X^uNawgp6ivm*+4# +(wihfO-1K+~h+9}f;fox-&cIxU(C~HH7$m&j~;#V_+L(JHoQzUcE6SfY)* +wRJn`g-S1*8?99dmzyY&SSHW0MVVS&$8d>3C_2jl`%41wBsCs=v2@tHptwn||7q)vpEx7Of+L_^nPRX +q|JFQ_9m-CXhCP)h>@6aWAK2mo!S{#uk;6;L<>000aN000~S003}la4%nJZggdGZeeUMa%FKZa%FK}b +7gccaCxm(TW^~%6n^JdcszlY@dMaWOk$6`8CgM&=^&`O6V43u5?nd?1?%5WMjGLoH8br0i|&e1esvQ5YO{B*fbz`$<~lPQM0LBR>0WOjCZDdzjgrs=aaS0I-!w1_gbflUcocMYE{9{+8e +rfYHC!00XA=}3Frz1R6!!y7V@03og8__w&i?IsB05XlfKuckHeRZ +AhWCvn5=Pl}WZtY`{b@;RTV(I>RJWB^c_uCZ6@eJ;81+Sh>uZvrEUl=JF-(2f>_S=()!pY`f8tgd|KJ +6_*imijM6V`&fOLv9F1S>c2!eFTy*r?S3Mol37qZIvd0J +Ja1{>K>qSkPyFSY<$-L%EY5joGq#fEm**gDRfQ)*&_l+Ewc{qDhZOy1ExQ;_5<*n&EqHs_pR2b&=}0jQ>s?cj`r&>0rWYsV>m;q-ERy` +3bx4S}PqD;f-ojk{52*#IB(xNcQgk2Ym>S_`af9g}t~tb$ZhoUCz^nJ@>jwhb2BcW8`BUBs#x)&%*Mp +sa5SK%UjUl*TG!_(8=NKiRuT&1BUwpASYIlCA6agd2$l_{`UG7y-KwOwC9GjSkVVtYmHw?o#p~oEZd> +AQDO`D-C?-QpeYBu^lIK25hca$D5ZHdJdfnC9E)BafBOCBZgYEQU6&5~p&zUmZQIlAb>r$E8)msFI5x +&|*M)ir{sK@-0|XQR000O8ZKnQO*mO`p@EQOBBVGUiA^-pYaA|NaUukZ1WpZv|Y%g+UaW8UZabI +N0LVQg$JaCxmfYm?hHlHcc7aOrZHawJZYtx8?ZmG6>^ojup(Rd%w@)_96TiIBt$MKT068tr8M`}G3=0 +T7fYiS@zOP(Y*6?`|}>?wVbcW$S&v??jeG<*seIKFaI5>GQs9YI%01o~`%ws&AS~>IaSaCXe>lRa39a +&Dk13ubQe7D*}|}%aw-z4^fLQ@0)HOz3$=LvJyO=f3M1Ay6?+M4`1c0t;lddCYyb?QiJ+ +DMjp(ywS!Zo0*5%#Y{Joc=3jx@PJ}>e<*Yp1;mjV&$%H8jbad9UZad;R^(AKty49xgVUtS +ajpJwgAS0gx>dS8rw-NY!jM7R_OE-@+3Cd@Soi)IEUMOwZnx(uhDVZ{|_mbi2GN|1AiKdDMwI-w6p{c +6}$rG^Aea+N>x$!~Ejcd$??J3A}HI?Cj#E8Cu)%H%A94@v-_dl3O+=aKchdQ!@4vo9~AB`Db0EL!~6pdyzv;QOW){O{E +TL5$=0>rUjoi2AJn<*ce>^cx^qu1irw2s&!k{99QG8Len=5iO52{T{SbHkFvevJVPj8LgX+rfkYvQAg +L;1qkBxbxJed2(S?#=lFXC&k;O=rnRKTiT)RNf;IN)6wT|!g#0SuE5uf-i-Q{(;7P61^8jA7;{CM7iu}$0KL2a*+;&BsVEfM>={B18-7S{EJu~V06n +$9g=lM%CceKlukc=UM2>q}#-(?gfHGFjJ5XGBKf@;vW0@`BCg^XE4Q`s#_E4}RFS6auy(MUP{yfYIUW=ER4KOqZa2&E4#u8X(fL<)kiz30Hv(eAY0sQUkY>*JOPvF3piu`A4_iWn9(vjV +f3Cj7!$n-u|59$AZ5fT8G6vSO6qE(0}xKgx)g^CECzj^ib4{u&B4rRY>_Wc4R=5p0soml{RX5Sddtj( +pKn?~ONQ~?e$g4wK)=Er#iL?d806MkQTXD{$egM;v)L^Un6iA;HEf^O0U0v`_ot7e*5YTZ +BZ?Ly3O1ULXC>$K@M=du+mFow!KpLgWKVa!79O%w~BC*--qN$mwesF7R7#nX?u1V7#n +W7O_gEMk^o1wLjZ!7FtoY*|4AG3~NKtSTU5k*Ob*=72Kj(Et}=T4oOkHxZbS{A#Ivfq%?+ZIn2Kctmz +9&;v-6E#E}zjm>%9%rkR1&^!WkX`-kLi7ZTFGRco;lQs%%!YZ96Rwhda5K}}KX&hvA8evX<>)AfVCfp4Td{T-kJO1x|UnPK`osziPZx?Mvk0 +ncc*+(I}67!kQpL-?s^ZH|g|7@H}wXd*bS0p*cMfTkV+7fi)@bR&*0Ai%PO2cYe%B9k{Iu9ervWW>o7 +RH5rx_Goc>*r6`8N&GyXxtv7Xha)awq40S99d%hPrn5ndlVbf;d%vKS!3D#a*?XYH(ksZ` +RIWZgjAbuSPL=QmCN3P&W6cEtn>pyUWq4tg3x6I1s +d4hpk|dfFI4=?a^XC3_`1%SXF>;EJCtxVlTuyBdhZ~r_i6d) +OUV-8;y4A*sSOI%rfy^!_kx3)P*g8s5&EPIAvz*&tXQO4hY9=#k1v4nENJNh_*RMQU#esaRf;LzHdRt +DwGv_8cJDNV<=`cv8pl(WG3u{9wYZlSnSj&j;T77P)I9y_m!c&_ +{^?%`_UGW*H>sm=9#x+`?!U{XPHAArSHbvjJmSZXlsPGD;}BW&cbPz7Ri3TARgh@E;g#z<|QtnG)h*XIqR#$f%& +(Y(=3%m3#@{l?zi+(1ohWh3cdog0f2-cvxnYY}pWz&<~mR9z|yF997TvFU +kZDPD&F`2hZw>AwM}5G}T;S!s;Jr0~$GH!co{zig%FsMB=XRat&2MAko>Cn=O?kS!7eg6dp@PmuC&6Q +7(ilkQtfkk^kUHGoPLSm+D+)ZDx(D7r;9j^#Pubct83T;b)`lMPt4ms%hTpNpKwvrIZLaA>iZb_(4S& +1rR)5*KK^!7{Qs??33u7AR|FK&T(kT#vqNpDzK`9jMoWH5Qb4{l2^5lfF)q2{yqeeL_g*%?w=^gMA#TODb^R4Vf +8L8j=71BQz!ujIb$hD2(pI<1Eyjw+kY(Fb5tnq~*11J~7O<5N&Vk6pC6~+^x%W)E +jt5a^E#tb{tA)@*rP^Uh<%x)Tl +CWu|&D&w7o`6nYfB)Msd$sS-EO@z{Q?Veo>L}b?Lz+EOFaNkUI%*Ut-=sFdsaOf=2s#pNL5awgI;98pv9Yx=f}9Mt$Ca{gA1RkE57bDHU^vU<>|cIUwXvo`&BLswX5W@K=yry2EZ +V#atGMkfZV}^GL3PKrEAJ#s7okk@rj?h=7nbNsN=uyez^+-iafb5N3|AYhX(c;#o(|49RY}&^Z=GRC& +^yH558PiZF*)he(u1Yxf8drLx1`Z1S7PbI>`k*_`LzaV;Pe+3+lgUw_>Lcx}@SJt3+BV%=glgKYGw-s +Gjqj?s#^fPqfk&LNSdI^0;`k=<7<{llPDkbCXQekv;DbBZ0RiF1M(Esu;ORh(3i!ug>s?^GDq){IVa9 +TR2Brr~HHT~#8l_if^PeUdZ|$lM1>KyL{iMe +llVIZ3S#wCzE=mDPh~uhp0}1-AyCb*`EW!swL;Lls$~nY+;5WjjcBWUg-bt`)~fCz!F16}42^)H26?t +>LVxuKM(=BP;AotvoFz|K1^waPpc5cJq#(4c6QZKY7k85+rK9R3{w4={kcH^Lx-?*&9FmR^aXIozx1U +|`JaOmSRi&Wn@s|)hLSon-2L;aSY%Jf{Gc-Crz=w$?KQoh|J~A~m<;kU~;F+k@ZEnDVqsowf+k=_hL3 +VH=A)QAjx-DE?W&BSaC=R1Kv4&{*gTq=3CWaJrAV$A^#pmN*BFfXKZG%tn0|u&K9AUsj`k&ZEe$}>xa +Y)6jjYvYb6%HkAUqNiaqsaxA1Cfu>!2)au^QXsxm+j=bYf#3)h>af-^gzh>DXB +yT`@5v*s3lUX`=g9}vtZ>M=Trzm`0SXeA%P1;bpGuuMZ8^yj +OYg@SvJw@7{JdszPGKTU(jc&+D%-lS2U-_gG01$qkN6WgwKYiDGvSmyZ#w!lL~w17QA9FD2k>|T4`_3 +5f*W$a?Kdp#biP^QZw_pI6wTi?&^hFLpEoo~pZ+TA8NAS&uadP?-CA*z6>~^q^6bqgll?I%;n +;u9E1VF7+gINdZq4z-eMW5z%YtdW@(T_(OP2)=`l;-}g;M?f%`4dzS%GGMMw|R&>h-9kTs69FM=@w2g +geiG9bGkY8VOd!Sl|LyiQ(N@3M`uCHxWUKAP&w|ta_O#)=Qp1={ +q$PaF9V)umA4P4o+k|DGj$)|)sk1yGB?+Fjb}+S*MEGMO#nzveYJa4?0f3Gayxp&`<&ZBG7)b^u5K2= +5L19~fvurCIdk~J{UaCe2jx&{3d0h_N#0QR`?THAsS#Wfnuu-iQ$xd#RE12x*aIF%4l(h)h3HWblNSpHaKCs=E*>j{k7gr>fSQRpbs*Bq}Y#?c@RDO4_hUF%h`d@AOq&&BO<;>Hm$otsA0Nk +)Y^-QY5Xyh`_j0!=34Nv+6gkmNa_83$U4sKGX%;na(R=)KeAFs$-mC)*?&g>D2b0Tw!Y&(@oa{N1@Olo +D#*Q2Lr5%E+q~X@U;uWvC1kINa}LDNWk@hzPCd?X>Xvs%?P^KT&vRL4%F*Lgs{Vo1`%g$QxY6ITh9Aw +bduXu@Mw*~P5C%ig0U|+0IIvQvbgF4#nBaC!KGKDd>lh$BzM!lkJ3PN^I_zM_s%=E)${HRQKTHl&mUwvhWnHiV+-icPHdmzyz4e%AX@>xt^vesOr6|n9g73Wa8f3!cOrtkH{a(zNpG&JexUtHGsw_jENcRdVUR%!n`oOhGxw`soVr?_cqZGbo7gsqNu$G +V3LS@w;^2y-cnu7TBfQm3EuVJ%#T->~SLIIRwY2O)PVpFQcxV`n?1DaUr?$Ys3e;Ov(OvmS6x{S+SAF +|^ONHGGk;YInFeXISPOw^&& +>eQ^|hh+_4V9?rmD)=If%EtlxTZ&ql+Zz2x~Uj7OF$HD5Um8HJi>5I;%U)3LC3I#1Sgv*Jxb{jKpTWq +Jfx-`u}sIg7`c4sjC@M^@Cf`=rBOFhtKgUJsreLxAscF_8r!BA$BVubYEfOrjkXsd^RmP<7>iG5)kSE +a@3Zu4o}O$Adb4N`GYq#9lYGxCo!XY(ar=UzXxI>e-CWR>jbv#odZnZb|!L3n-oQJeDlb^*riF+`vbW; +I~&Y`a9;AuF1=olEp6(bP7>8p8*SC%p=LC51kyv_)sIx@yv7U0>w!Ajpm51O;K*S#=o)X&(0GXvOB^& +H#_2;+?q*n%TpaCJ2(ZiakWd$U1ly9|q0@TmzYe5!d&v5^WFzA%j7;*BnW5BnTVxSCdhasRs`_0Dd?n +1mE*x \ No newline at end of file diff --git a/DjangoBlog-master/locale/en/LC_MESSAGES/django.mo b/DjangoBlog-master/locale/en/LC_MESSAGES/django.mo new file mode 100644 index 0000000..f63669f Binary files /dev/null and b/DjangoBlog-master/locale/en/LC_MESSAGES/django.mo differ diff --git a/DjangoBlog-master/locale/en/LC_MESSAGES/django.po b/DjangoBlog-master/locale/en/LC_MESSAGES/django.po new file mode 100644 index 0000000..c80b30a --- /dev/null +++ b/DjangoBlog-master/locale/en/LC_MESSAGES/django.po @@ -0,0 +1,685 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-09-13 16:02+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: .\accounts\admin.py:12 +msgid "password" +msgstr "password" + +#: .\accounts\admin.py:13 +msgid "Enter password again" +msgstr "Enter password again" + +#: .\accounts\admin.py:24 .\accounts\forms.py:89 +msgid "passwords do not match" +msgstr "passwords do not match" + +#: .\accounts\forms.py:36 +msgid "email already exists" +msgstr "email already exists" + +#: .\accounts\forms.py:46 .\accounts\forms.py:50 +msgid "New password" +msgstr "New password" + +#: .\accounts\forms.py:60 +msgid "Confirm password" +msgstr "Confirm password" + +#: .\accounts\forms.py:70 .\accounts\forms.py:116 +msgid "Email" +msgstr "Email" + +#: .\accounts\forms.py:76 .\accounts\forms.py:80 +msgid "Code" +msgstr "Code" + +#: .\accounts\forms.py:100 .\accounts\tests.py:194 +msgid "email does not exist" +msgstr "email does not exist" + +#: .\accounts\models.py:12 .\oauth\models.py:17 +msgid "nick name" +msgstr "nick name" + +#: .\accounts\models.py:13 .\blog\models.py:29 .\blog\models.py:266 +#: .\blog\models.py:284 .\comments\models.py:13 .\oauth\models.py:23 +#: .\oauth\models.py:53 +msgid "creation time" +msgstr "creation time" + +#: .\accounts\models.py:14 .\comments\models.py:14 .\oauth\models.py:24 +#: .\oauth\models.py:54 +msgid "last modify time" +msgstr "last modify time" + +#: .\accounts\models.py:15 +msgid "create source" +msgstr "create source" + +#: .\accounts\models.py:33 .\djangoblog\logentryadmin.py:81 +msgid "user" +msgstr "user" + +#: .\accounts\tests.py:216 .\accounts\utils.py:39 +msgid "Verification code error" +msgstr "Verification code error" + +#: .\accounts\utils.py:13 +msgid "Verify Email" +msgstr "Verify Email" + +#: .\accounts\utils.py:21 +#, python-format +msgid "" +"You are resetting the password, the verification code is:%(code)s, valid " +"within 5 minutes, please keep it properly" +msgstr "" +"You are resetting the password, the verification code is:%(code)s, valid " +"within 5 minutes, please keep it properly" + +#: .\blog\admin.py:13 .\blog\models.py:92 .\comments\models.py:17 +#: .\oauth\models.py:12 +msgid "author" +msgstr "author" + +#: .\blog\admin.py:53 +msgid "Publish selected articles" +msgstr "Publish selected articles" + +#: .\blog\admin.py:54 +msgid "Draft selected articles" +msgstr "Draft selected articles" + +#: .\blog\admin.py:55 +msgid "Close article comments" +msgstr "Close article comments" + +#: .\blog\admin.py:56 +msgid "Open article comments" +msgstr "Open article comments" + +#: .\blog\admin.py:89 .\blog\models.py:101 .\blog\models.py:183 +#: .\templates\blog\tags\sidebar.html:40 +msgid "category" +msgstr "category" + +#: .\blog\models.py:20 .\blog\models.py:179 .\templates\share_layout\nav.html:8 +msgid "index" +msgstr "index" + +#: .\blog\models.py:21 +msgid "list" +msgstr "list" + +#: .\blog\models.py:22 +msgid "post" +msgstr "post" + +#: .\blog\models.py:23 +msgid "all" +msgstr "all" + +#: .\blog\models.py:24 +msgid "slide" +msgstr "slide" + +#: .\blog\models.py:30 .\blog\models.py:267 .\blog\models.py:285 +msgid "modify time" +msgstr "modify time" + +#: .\blog\models.py:63 +msgid "Draft" +msgstr "Draft" + +#: .\blog\models.py:64 +msgid "Published" +msgstr "Published" + +#: .\blog\models.py:67 +msgid "Open" +msgstr "Open" + +#: .\blog\models.py:68 +msgid "Close" +msgstr "Close" + +#: .\blog\models.py:71 .\comments\admin.py:47 +msgid "Article" +msgstr "Article" + +#: .\blog\models.py:72 +msgid "Page" +msgstr "Page" + +#: .\blog\models.py:74 .\blog\models.py:280 +msgid "title" +msgstr "title" + +#: .\blog\models.py:75 +msgid "body" +msgstr "body" + +#: .\blog\models.py:77 +msgid "publish time" +msgstr "publish time" + +#: .\blog\models.py:79 +msgid "status" +msgstr "status" + +#: .\blog\models.py:84 +msgid "comment status" +msgstr "comment status" + +#: .\blog\models.py:88 .\oauth\models.py:43 +msgid "type" +msgstr "type" + +#: .\blog\models.py:89 +msgid "views" +msgstr "views" + +#: .\blog\models.py:97 .\blog\models.py:258 .\blog\models.py:282 +msgid "order" +msgstr "order" + +#: .\blog\models.py:98 +msgid "show toc" +msgstr "show toc" + +#: .\blog\models.py:105 .\blog\models.py:249 +msgid "tag" +msgstr "tag" + +#: .\blog\models.py:115 .\comments\models.py:21 +msgid "article" +msgstr "article" + +#: .\blog\models.py:171 +msgid "category name" +msgstr "category name" + +#: .\blog\models.py:174 +msgid "parent category" +msgstr "parent category" + +#: .\blog\models.py:234 +msgid "tag name" +msgstr "tag name" + +#: .\blog\models.py:256 +msgid "link name" +msgstr "link name" + +#: .\blog\models.py:257 .\blog\models.py:271 +msgid "link" +msgstr "link" + +#: .\blog\models.py:260 +msgid "is show" +msgstr "is show" + +#: .\blog\models.py:262 +msgid "show type" +msgstr "show type" + +#: .\blog\models.py:281 +msgid "content" +msgstr "content" + +#: .\blog\models.py:283 .\oauth\models.py:52 +msgid "is enable" +msgstr "is enable" + +#: .\blog\models.py:289 +msgid "sidebar" +msgstr "sidebar" + +#: .\blog\models.py:299 +msgid "site name" +msgstr "site name" + +#: .\blog\models.py:305 +msgid "site description" +msgstr "site description" + +#: .\blog\models.py:311 +msgid "site seo description" +msgstr "site seo description" + +#: .\blog\models.py:313 +msgid "site keywords" +msgstr "site keywords" + +#: .\blog\models.py:318 +msgid "article sub length" +msgstr "article sub length" + +#: .\blog\models.py:319 +msgid "sidebar article count" +msgstr "sidebar article count" + +#: .\blog\models.py:320 +msgid "sidebar comment count" +msgstr "sidebar comment count" + +#: .\blog\models.py:321 +msgid "article comment count" +msgstr "article comment count" + +#: .\blog\models.py:322 +msgid "show adsense" +msgstr "show adsense" + +#: .\blog\models.py:324 +msgid "adsense code" +msgstr "adsense code" + +#: .\blog\models.py:325 +msgid "open site comment" +msgstr "open site comment" + +#: .\blog\models.py:352 +msgid "Website configuration" +msgstr "Website configuration" + +#: .\blog\models.py:360 +msgid "There can only be one configuration" +msgstr "There can only be one configuration" + +#: .\blog\views.py:348 +msgid "" +"Sorry, the page you requested is not found, please click the home page to " +"see other?" +msgstr "" +"Sorry, the page you requested is not found, please click the home page to " +"see other?" + +#: .\blog\views.py:356 +msgid "Sorry, the server is busy, please click the home page to see other?" +msgstr "Sorry, the server is busy, please click the home page to see other?" + +#: .\blog\views.py:369 +msgid "Sorry, you do not have permission to access this page?" +msgstr "Sorry, you do not have permission to access this page?" + +#: .\comments\admin.py:15 +msgid "Disable comments" +msgstr "Disable comments" + +#: .\comments\admin.py:16 +msgid "Enable comments" +msgstr "Enable comments" + +#: .\comments\admin.py:46 +msgid "User" +msgstr "User" + +#: .\comments\models.py:25 +msgid "parent comment" +msgstr "parent comment" + +#: .\comments\models.py:29 +msgid "enable" +msgstr "enable" + +#: .\comments\models.py:34 .\templates\blog\tags\article_info.html:30 +msgid "comment" +msgstr "comment" + +#: .\comments\utils.py:13 +msgid "Thanks for your comment" +msgstr "Thanks for your comment" + +#: .\comments\utils.py:15 +#, python-format +msgid "" +"

Thank you very much for your comments on this site

\n" +" You can visit %(article_title)s\n" +" to review your comments,\n" +" Thank you again!\n" +"
\n" +" If the link above cannot be opened, please copy this " +"link to your browser.\n" +" %(article_url)s" +msgstr "" +"

Thank you very much for your comments on this site

\n" +" You can visit %(article_title)s\n" +" to review your comments,\n" +" Thank you again!\n" +"
\n" +" If the link above cannot be opened, please copy this " +"link to your browser.\n" +" %(article_url)s" + +#: .\comments\utils.py:26 +#, python-format +msgid "" +"Your comment on " +"%(article_title)s
has \n" +" received a reply.
%(comment_body)s\n" +"
\n" +" go check it out!\n" +"
\n" +" If the link above cannot be opened, please copy this " +"link to your browser.\n" +" %(article_url)s\n" +" " +msgstr "" +"Your comment on " +"%(article_title)s
has \n" +" received a reply.
%(comment_body)s\n" +"
\n" +" go check it out!\n" +"
\n" +" If the link above cannot be opened, please copy this " +"link to your browser.\n" +" %(article_url)s\n" +" " + +#: .\djangoblog\logentryadmin.py:63 +msgid "object" +msgstr "object" + +#: .\djangoblog\settings.py:140 +msgid "English" +msgstr "English" + +#: .\djangoblog\settings.py:141 +msgid "Simplified Chinese" +msgstr "Simplified Chinese" + +#: .\djangoblog\settings.py:142 +msgid "Traditional Chinese" +msgstr "Traditional Chinese" + +#: .\oauth\models.py:30 +msgid "oauth user" +msgstr "oauth user" + +#: .\oauth\models.py:37 +msgid "weibo" +msgstr "weibo" + +#: .\oauth\models.py:38 +msgid "google" +msgstr "google" + +#: .\oauth\models.py:48 +msgid "callback url" +msgstr "callback url" + +#: .\oauth\models.py:59 +msgid "already exists" +msgstr "already exists" + +#: .\oauth\views.py:154 +#, python-format +msgid "" +"\n" +"

Congratulations, you have successfully bound your email address. You " +"can use\n" +" %(oauthuser_type)s to directly log in to this website without a " +"password.

\n" +" You are welcome to continue to follow this site, the address is\n" +" %(site)s\n" +" Thank you again!\n" +"
\n" +" If the link above cannot be opened, please copy this link to your " +"browser.\n" +" %(site)s\n" +" " +msgstr "" +"\n" +"

Congratulations, you have successfully bound your email address. You " +"can use\n" +" %(oauthuser_type)s to directly log in to this website without a " +"password.

\n" +" You are welcome to continue to follow this site, the address is\n" +" %(site)s\n" +" Thank you again!\n" +"
\n" +" If the link above cannot be opened, please copy this link to your " +"browser.\n" +" %(site)s\n" +" " + +#: .\oauth\views.py:165 +msgid "Congratulations on your successful binding!" +msgstr "Congratulations on your successful binding!" + +#: .\oauth\views.py:217 +#, python-format +msgid "" +"\n" +"

Please click the link below to bind your email

\n" +"\n" +" %(url)s\n" +"\n" +" Thank you again!\n" +"
\n" +" If the link above cannot be opened, please copy this link " +"to your browser.\n" +"
\n" +" %(url)s\n" +" " +msgstr "" +"\n" +"

Please click the link below to bind your email

\n" +"\n" +" %(url)s\n" +"\n" +" Thank you again!\n" +"
\n" +" If the link above cannot be opened, please copy this link " +"to your browser.\n" +"
\n" +" %(url)s\n" +" " + +#: .\oauth\views.py:228 .\oauth\views.py:240 +msgid "Bind your email" +msgstr "Bind your email" + +#: .\oauth\views.py:242 +msgid "" +"Congratulations, the binding is just one step away. Please log in to your " +"email to check the email to complete the binding. Thank you." +msgstr "" +"Congratulations, the binding is just one step away. Please log in to your " +"email to check the email to complete the binding. Thank you." + +#: .\oauth\views.py:245 +msgid "Binding successful" +msgstr "Binding successful" + +#: .\oauth\views.py:247 +#, python-format +msgid "" +"Congratulations, you have successfully bound your email address. You can use " +"%(oauthuser_type)s to directly log in to this website without a password. " +"You are welcome to continue to follow this site." +msgstr "" +"Congratulations, you have successfully bound your email address. You can use " +"%(oauthuser_type)s to directly log in to this website without a password. " +"You are welcome to continue to follow this site." + +#: .\templates\account\forget_password.html:7 +msgid "forget the password" +msgstr "forget the password" + +#: .\templates\account\forget_password.html:18 +msgid "get verification code" +msgstr "get verification code" + +#: .\templates\account\forget_password.html:19 +msgid "submit" +msgstr "submit" + +#: .\templates\account\login.html:36 +msgid "Create Account" +msgstr "Create Account" + +#: .\templates\account\login.html:42 +#, fuzzy +#| msgid "forget the password" +msgid "Forget Password" +msgstr "forget the password" + +#: .\templates\account\result.html:18 .\templates\blog\tags\sidebar.html:126 +msgid "login" +msgstr "login" + +#: .\templates\account\result.html:22 +msgid "back to the homepage" +msgstr "back to the homepage" + +#: .\templates\blog\article_archives.html:7 +#: .\templates\blog\article_archives.html:24 +msgid "article archive" +msgstr "article archive" + +#: .\templates\blog\article_archives.html:32 +msgid "year" +msgstr "year" + +#: .\templates\blog\article_archives.html:36 +msgid "month" +msgstr "month" + +#: .\templates\blog\tags\article_info.html:12 +msgid "pin to top" +msgstr "pin to top" + +#: .\templates\blog\tags\article_info.html:28 +msgid "comments" +msgstr "comments" + +#: .\templates\blog\tags\article_info.html:58 +msgid "toc" +msgstr "toc" + +#: .\templates\blog\tags\article_meta_info.html:6 +msgid "posted in" +msgstr "posted in" + +#: .\templates\blog\tags\article_meta_info.html:14 +msgid "and tagged" +msgstr "and tagged" + +#: .\templates\blog\tags\article_meta_info.html:25 +msgid "by " +msgstr "by" + +#: .\templates\blog\tags\article_meta_info.html:29 +#, python-format +msgid "" +"\n" +" title=\"View all articles published by " +"%(article.author.username)s\"\n" +" " +msgstr "" +"\n" +" title=\"View all articles published by " +"%(article.author.username)s\"\n" +" " + +#: .\templates\blog\tags\article_meta_info.html:44 +msgid "on" +msgstr "on" + +#: .\templates\blog\tags\article_meta_info.html:54 +msgid "edit" +msgstr "edit" + +#: .\templates\blog\tags\article_pagination.html:4 +msgid "article navigation" +msgstr "article navigation" + +#: .\templates\blog\tags\article_pagination.html:9 +msgid "earlier articles" +msgstr "earlier articles" + +#: .\templates\blog\tags\article_pagination.html:12 +msgid "newer articles" +msgstr "newer articles" + +#: .\templates\blog\tags\article_tag_list.html:5 +msgid "tags" +msgstr "tags" + +#: .\templates\blog\tags\sidebar.html:7 +msgid "search" +msgstr "search" + +#: .\templates\blog\tags\sidebar.html:50 +msgid "recent comments" +msgstr "recent comments" + +#: .\templates\blog\tags\sidebar.html:57 +msgid "published on" +msgstr "published on" + +#: .\templates\blog\tags\sidebar.html:65 +msgid "recent articles" +msgstr "recent articles" + +#: .\templates\blog\tags\sidebar.html:77 +msgid "bookmark" +msgstr "bookmark" + +#: .\templates\blog\tags\sidebar.html:96 +msgid "Tag Cloud" +msgstr "Tag Cloud" + +#: .\templates\blog\tags\sidebar.html:107 +msgid "Welcome to star or fork the source code of this site" +msgstr "Welcome to star or fork the source code of this site" + +#: .\templates\blog\tags\sidebar.html:118 +msgid "Function" +msgstr "Function" + +#: .\templates\blog\tags\sidebar.html:120 +msgid "management site" +msgstr "management site" + +#: .\templates\blog\tags\sidebar.html:122 +msgid "logout" +msgstr "logout" + +#: .\templates\blog\tags\sidebar.html:129 +msgid "Track record" +msgstr "Track record" + +#: .\templates\blog\tags\sidebar.html:135 +msgid "Click me to return to the top" +msgstr "Click me to return to the top" + +#: .\templates\oauth\oauth_applications.html:5 +#| msgid "login" +msgid "quick login" +msgstr "quick login" + +#: .\templates\share_layout\nav.html:26 +msgid "Article archive" +msgstr "Article archive" diff --git a/DjangoBlog-master/locale/zh_Hans/LC_MESSAGES/django.mo b/DjangoBlog-master/locale/zh_Hans/LC_MESSAGES/django.mo new file mode 100644 index 0000000..a2d36e9 Binary files /dev/null and b/DjangoBlog-master/locale/zh_Hans/LC_MESSAGES/django.mo differ diff --git a/DjangoBlog-master/locale/zh_Hans/LC_MESSAGES/django.po b/DjangoBlog-master/locale/zh_Hans/LC_MESSAGES/django.po new file mode 100644 index 0000000..200b7e6 --- /dev/null +++ b/DjangoBlog-master/locale/zh_Hans/LC_MESSAGES/django.po @@ -0,0 +1,667 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-09-13 16:02+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#: .\accounts\admin.py:12 +msgid "password" +msgstr "密码" + +#: .\accounts\admin.py:13 +msgid "Enter password again" +msgstr "再次输入密码" + +#: .\accounts\admin.py:24 .\accounts\forms.py:89 +msgid "passwords do not match" +msgstr "密码不匹配" + +#: .\accounts\forms.py:36 +msgid "email already exists" +msgstr "邮箱已存在" + +#: .\accounts\forms.py:46 .\accounts\forms.py:50 +msgid "New password" +msgstr "新密码" + +#: .\accounts\forms.py:60 +msgid "Confirm password" +msgstr "确认密码" + +#: .\accounts\forms.py:70 .\accounts\forms.py:116 +msgid "Email" +msgstr "邮箱" + +#: .\accounts\forms.py:76 .\accounts\forms.py:80 +msgid "Code" +msgstr "验证码" + +#: .\accounts\forms.py:100 .\accounts\tests.py:194 +msgid "email does not exist" +msgstr "邮箱不存在" + +#: .\accounts\models.py:12 .\oauth\models.py:17 +msgid "nick name" +msgstr "昵称" + +#: .\accounts\models.py:13 .\blog\models.py:29 .\blog\models.py:266 +#: .\blog\models.py:284 .\comments\models.py:13 .\oauth\models.py:23 +#: .\oauth\models.py:53 +msgid "creation time" +msgstr "创建时间" + +#: .\accounts\models.py:14 .\comments\models.py:14 .\oauth\models.py:24 +#: .\oauth\models.py:54 +msgid "last modify time" +msgstr "最后修改时间" + +#: .\accounts\models.py:15 +msgid "create source" +msgstr "来源" + +#: .\accounts\models.py:33 .\djangoblog\logentryadmin.py:81 +msgid "user" +msgstr "用户" + +#: .\accounts\tests.py:216 .\accounts\utils.py:39 +msgid "Verification code error" +msgstr "验证码错误" + +#: .\accounts\utils.py:13 +msgid "Verify Email" +msgstr "验证邮箱" + +#: .\accounts\utils.py:21 +#, python-format +msgid "" +"You are resetting the password, the verification code is:%(code)s, valid " +"within 5 minutes, please keep it properly" +msgstr "您正在重置密码,验证码为:%(code)s,5分钟内有效 请妥善保管." + +#: .\blog\admin.py:13 .\blog\models.py:92 .\comments\models.py:17 +#: .\oauth\models.py:12 +msgid "author" +msgstr "作者" + +#: .\blog\admin.py:53 +msgid "Publish selected articles" +msgstr "发布选中的文章" + +#: .\blog\admin.py:54 +msgid "Draft selected articles" +msgstr "选中文章设为草稿" + +#: .\blog\admin.py:55 +msgid "Close article comments" +msgstr "关闭文章评论" + +#: .\blog\admin.py:56 +msgid "Open article comments" +msgstr "打开文章评论" + +#: .\blog\admin.py:89 .\blog\models.py:101 .\blog\models.py:183 +#: .\templates\blog\tags\sidebar.html:40 +msgid "category" +msgstr "分类目录" + +#: .\blog\models.py:20 .\blog\models.py:179 .\templates\share_layout\nav.html:8 +msgid "index" +msgstr "首页" + +#: .\blog\models.py:21 +msgid "list" +msgstr "列表" + +#: .\blog\models.py:22 +msgid "post" +msgstr "文章" + +#: .\blog\models.py:23 +msgid "all" +msgstr "所有" + +#: .\blog\models.py:24 +msgid "slide" +msgstr "侧边栏" + +#: .\blog\models.py:30 .\blog\models.py:267 .\blog\models.py:285 +msgid "modify time" +msgstr "修改时间" + +#: .\blog\models.py:63 +msgid "Draft" +msgstr "草稿" + +#: .\blog\models.py:64 +msgid "Published" +msgstr "发布" + +#: .\blog\models.py:67 +msgid "Open" +msgstr "打开" + +#: .\blog\models.py:68 +msgid "Close" +msgstr "关闭" + +#: .\blog\models.py:71 .\comments\admin.py:47 +msgid "Article" +msgstr "文章" + +#: .\blog\models.py:72 +msgid "Page" +msgstr "页面" + +#: .\blog\models.py:74 .\blog\models.py:280 +msgid "title" +msgstr "标题" + +#: .\blog\models.py:75 +msgid "body" +msgstr "内容" + +#: .\blog\models.py:77 +msgid "publish time" +msgstr "发布时间" + +#: .\blog\models.py:79 +msgid "status" +msgstr "状态" + +#: .\blog\models.py:84 +msgid "comment status" +msgstr "评论状态" + +#: .\blog\models.py:88 .\oauth\models.py:43 +msgid "type" +msgstr "类型" + +#: .\blog\models.py:89 +msgid "views" +msgstr "阅读量" + +#: .\blog\models.py:97 .\blog\models.py:258 .\blog\models.py:282 +msgid "order" +msgstr "排序" + +#: .\blog\models.py:98 +msgid "show toc" +msgstr "显示目录" + +#: .\blog\models.py:105 .\blog\models.py:249 +msgid "tag" +msgstr "标签" + +#: .\blog\models.py:115 .\comments\models.py:21 +msgid "article" +msgstr "文章" + +#: .\blog\models.py:171 +msgid "category name" +msgstr "分类名" + +#: .\blog\models.py:174 +msgid "parent category" +msgstr "上级分类" + +#: .\blog\models.py:234 +msgid "tag name" +msgstr "标签名" + +#: .\blog\models.py:256 +msgid "link name" +msgstr "链接名" + +#: .\blog\models.py:257 .\blog\models.py:271 +msgid "link" +msgstr "链接" + +#: .\blog\models.py:260 +msgid "is show" +msgstr "是否显示" + +#: .\blog\models.py:262 +msgid "show type" +msgstr "显示类型" + +#: .\blog\models.py:281 +msgid "content" +msgstr "内容" + +#: .\blog\models.py:283 .\oauth\models.py:52 +msgid "is enable" +msgstr "是否启用" + +#: .\blog\models.py:289 +msgid "sidebar" +msgstr "侧边栏" + +#: .\blog\models.py:299 +msgid "site name" +msgstr "站点名称" + +#: .\blog\models.py:305 +msgid "site description" +msgstr "站点描述" + +#: .\blog\models.py:311 +msgid "site seo description" +msgstr "站点SEO描述" + +#: .\blog\models.py:313 +msgid "site keywords" +msgstr "关键字" + +#: .\blog\models.py:318 +msgid "article sub length" +msgstr "文章摘要长度" + +#: .\blog\models.py:319 +msgid "sidebar article count" +msgstr "侧边栏文章数目" + +#: .\blog\models.py:320 +msgid "sidebar comment count" +msgstr "侧边栏评论数目" + +#: .\blog\models.py:321 +msgid "article comment count" +msgstr "文章页面默认显示评论数目" + +#: .\blog\models.py:322 +msgid "show adsense" +msgstr "是否显示广告" + +#: .\blog\models.py:324 +msgid "adsense code" +msgstr "广告内容" + +#: .\blog\models.py:325 +msgid "open site comment" +msgstr "公共头部" + +#: .\blog\models.py:352 +msgid "Website configuration" +msgstr "网站配置" + +#: .\blog\models.py:360 +msgid "There can only be one configuration" +msgstr "只能有一个配置" + +#: .\blog\views.py:348 +msgid "" +"Sorry, the page you requested is not found, please click the home page to " +"see other?" +msgstr "抱歉,你所访问的页面找不到,请点击首页看看别的?" + +#: .\blog\views.py:356 +msgid "Sorry, the server is busy, please click the home page to see other?" +msgstr "抱歉,服务出错了,请点击首页看看别的?" + +#: .\blog\views.py:369 +msgid "Sorry, you do not have permission to access this page?" +msgstr "抱歉,你没用权限访问此页面。" + +#: .\comments\admin.py:15 +msgid "Disable comments" +msgstr "禁用评论" + +#: .\comments\admin.py:16 +msgid "Enable comments" +msgstr "启用评论" + +#: .\comments\admin.py:46 +msgid "User" +msgstr "用户" + +#: .\comments\models.py:25 +msgid "parent comment" +msgstr "上级评论" + +#: .\comments\models.py:29 +msgid "enable" +msgstr "启用" + +#: .\comments\models.py:34 .\templates\blog\tags\article_info.html:30 +msgid "comment" +msgstr "评论" + +#: .\comments\utils.py:13 +msgid "Thanks for your comment" +msgstr "感谢你的评论" + +#: .\comments\utils.py:15 +#, python-format +msgid "" +"

Thank you very much for your comments on this site

\n" +" You can visit %(article_title)s\n" +" to review your comments,\n" +" Thank you again!\n" +"
\n" +" If the link above cannot be opened, please copy this " +"link to your browser.\n" +" %(article_url)s" +msgstr "" +"

非常感谢您对此网站的评论

\n" +" 您可以访问%(article_title)s\n" +"查看您的评论,\n" +"再次感谢您!\n" +"
\n" +" 如果上面的链接打不开,请复制此链接链接到您的浏览器。\n" +"%(article_url)s" + +#: .\comments\utils.py:26 +#, python-format +msgid "" +"Your comment on " +"%(article_title)s
has \n" +" received a reply.
%(comment_body)s\n" +"
\n" +" go check it out!\n" +"
\n" +" If the link above cannot be opened, please copy this " +"link to your browser.\n" +" %(article_url)s\n" +" " +msgstr "" +"您对 %(article_title)s
" +"的评论有\n" +" 收到回复。
%(comment_body)s\n" +"
\n" +"快去看看吧!\n" +"
\n" +" 如果上面的链接打不开,请复制此链接链接到您的浏览器。\n" +" %(article_url)s\n" +" " + +#: .\djangoblog\logentryadmin.py:63 +msgid "object" +msgstr "对象" + +#: .\djangoblog\settings.py:140 +msgid "English" +msgstr "英文" + +#: .\djangoblog\settings.py:141 +msgid "Simplified Chinese" +msgstr "简体中文" + +#: .\djangoblog\settings.py:142 +msgid "Traditional Chinese" +msgstr "繁体中文" + +#: .\oauth\models.py:30 +msgid "oauth user" +msgstr "第三方用户" + +#: .\oauth\models.py:37 +msgid "weibo" +msgstr "微博" + +#: .\oauth\models.py:38 +msgid "google" +msgstr "谷歌" + +#: .\oauth\models.py:48 +msgid "callback url" +msgstr "回调地址" + +#: .\oauth\models.py:59 +msgid "already exists" +msgstr "已经存在" + +#: .\oauth\views.py:154 +#, python-format +msgid "" +"\n" +"

Congratulations, you have successfully bound your email address. You " +"can use\n" +" %(oauthuser_type)s to directly log in to this website without a " +"password.

\n" +" You are welcome to continue to follow this site, the address is\n" +" %(site)s\n" +" Thank you again!\n" +"
\n" +" If the link above cannot be opened, please copy this link to your " +"browser.\n" +" %(site)s\n" +" " +msgstr "" +"\n" +"

恭喜你已经绑定成功 你可以使用\n" +" %(oauthuser_type)s 来免密登录本站

\n" +" 欢迎继续关注本站, 地址是\n" +" %(site)s\n" +" 再次感谢你\n" +"
\n" +" 如果上面链接无法打开,请复制此链接到你的浏览器 \n" +" %(site)s\n" +" " + +#: .\oauth\views.py:165 +msgid "Congratulations on your successful binding!" +msgstr "恭喜你绑定成功" + +#: .\oauth\views.py:217 +#, python-format +msgid "" +"\n" +"

Please click the link below to bind your email

\n" +"\n" +" %(url)s\n" +"\n" +" Thank you again!\n" +"
\n" +" If the link above cannot be opened, please copy this link " +"to your browser.\n" +"
\n" +" %(url)s\n" +" " +msgstr "" +"\n" +"

请点击下面的链接绑定您的邮箱

\n" +"\n" +" %(url)s\n" +"\n" +"再次感谢您!\n" +"
\n" +"如果上面的链接打不开,请复制此链接到您的浏览器。\n" +"%(url)s\n" +" " + +#: .\oauth\views.py:228 .\oauth\views.py:240 +msgid "Bind your email" +msgstr "绑定邮箱" + +#: .\oauth\views.py:242 +msgid "" +"Congratulations, the binding is just one step away. Please log in to your " +"email to check the email to complete the binding. Thank you." +msgstr "恭喜您,还差一步就绑定成功了,请登录您的邮箱查看邮件完成绑定,谢谢。" + +#: .\oauth\views.py:245 +msgid "Binding successful" +msgstr "绑定成功" + +#: .\oauth\views.py:247 +#, python-format +msgid "" +"Congratulations, you have successfully bound your email address. You can use " +"%(oauthuser_type)s to directly log in to this website without a password. " +"You are welcome to continue to follow this site." +msgstr "" +"恭喜您绑定成功,您以后可以使用%(oauthuser_type)s来直接免密码登录本站啦,感谢" +"您对本站对关注。" + +#: .\templates\account\forget_password.html:7 +msgid "forget the password" +msgstr "忘记密码" + +#: .\templates\account\forget_password.html:18 +msgid "get verification code" +msgstr "获取验证码" + +#: .\templates\account\forget_password.html:19 +msgid "submit" +msgstr "提交" + +#: .\templates\account\login.html:36 +msgid "Create Account" +msgstr "创建账号" + +#: .\templates\account\login.html:42 +#| msgid "forget the password" +msgid "Forget Password" +msgstr "忘记密码" + +#: .\templates\account\result.html:18 .\templates\blog\tags\sidebar.html:126 +msgid "login" +msgstr "登录" + +#: .\templates\account\result.html:22 +msgid "back to the homepage" +msgstr "返回首页吧" + +#: .\templates\blog\article_archives.html:7 +#: .\templates\blog\article_archives.html:24 +msgid "article archive" +msgstr "文章归档" + +#: .\templates\blog\article_archives.html:32 +msgid "year" +msgstr "年" + +#: .\templates\blog\article_archives.html:36 +msgid "month" +msgstr "月" + +#: .\templates\blog\tags\article_info.html:12 +msgid "pin to top" +msgstr "置顶" + +#: .\templates\blog\tags\article_info.html:28 +msgid "comments" +msgstr "评论" + +#: .\templates\blog\tags\article_info.html:58 +msgid "toc" +msgstr "目录" + +#: .\templates\blog\tags\article_meta_info.html:6 +msgid "posted in" +msgstr "发布于" + +#: .\templates\blog\tags\article_meta_info.html:14 +msgid "and tagged" +msgstr "并标记为" + +#: .\templates\blog\tags\article_meta_info.html:25 +msgid "by " +msgstr "由" + +#: .\templates\blog\tags\article_meta_info.html:29 +#, python-format +msgid "" +"\n" +" title=\"View all articles published by " +"%(article.author.username)s\"\n" +" " +msgstr "" +"\n" +" title=\"查看所有由 %(article.author.username)s\"发布的文章\n" +" " + +#: .\templates\blog\tags\article_meta_info.html:44 +msgid "on" +msgstr "在" + +#: .\templates\blog\tags\article_meta_info.html:54 +msgid "edit" +msgstr "编辑" + +#: .\templates\blog\tags\article_pagination.html:4 +msgid "article navigation" +msgstr "文章导航" + +#: .\templates\blog\tags\article_pagination.html:9 +msgid "earlier articles" +msgstr "早期文章" + +#: .\templates\blog\tags\article_pagination.html:12 +msgid "newer articles" +msgstr "较新文章" + +#: .\templates\blog\tags\article_tag_list.html:5 +msgid "tags" +msgstr "标签" + +#: .\templates\blog\tags\sidebar.html:7 +msgid "search" +msgstr "搜索" + +#: .\templates\blog\tags\sidebar.html:50 +msgid "recent comments" +msgstr "近期评论" + +#: .\templates\blog\tags\sidebar.html:57 +msgid "published on" +msgstr "发表于" + +#: .\templates\blog\tags\sidebar.html:65 +msgid "recent articles" +msgstr "近期文章" + +#: .\templates\blog\tags\sidebar.html:77 +msgid "bookmark" +msgstr "书签" + +#: .\templates\blog\tags\sidebar.html:96 +msgid "Tag Cloud" +msgstr "标签云" + +#: .\templates\blog\tags\sidebar.html:107 +msgid "Welcome to star or fork the source code of this site" +msgstr "欢迎您STAR或者FORK本站源代码" + +#: .\templates\blog\tags\sidebar.html:118 +msgid "Function" +msgstr "功能" + +#: .\templates\blog\tags\sidebar.html:120 +msgid "management site" +msgstr "管理站点" + +#: .\templates\blog\tags\sidebar.html:122 +msgid "logout" +msgstr "登出" + +#: .\templates\blog\tags\sidebar.html:129 +msgid "Track record" +msgstr "运动轨迹记录" + +#: .\templates\blog\tags\sidebar.html:135 +msgid "Click me to return to the top" +msgstr "点我返回顶部" + +#: .\templates\oauth\oauth_applications.html:5 +#| msgid "login" +msgid "quick login" +msgstr "快捷登录" + +#: .\templates\share_layout\nav.html:26 +msgid "Article archive" +msgstr "文章归档" diff --git a/DjangoBlog-master/locale/zh_Hant/LC_MESSAGES/django.mo b/DjangoBlog-master/locale/zh_Hant/LC_MESSAGES/django.mo new file mode 100644 index 0000000..fe2ea17 Binary files /dev/null and b/DjangoBlog-master/locale/zh_Hant/LC_MESSAGES/django.mo differ diff --git a/DjangoBlog-master/locale/zh_Hant/LC_MESSAGES/django.po b/DjangoBlog-master/locale/zh_Hant/LC_MESSAGES/django.po new file mode 100644 index 0000000..a2920ce --- /dev/null +++ b/DjangoBlog-master/locale/zh_Hant/LC_MESSAGES/django.po @@ -0,0 +1,668 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-09-13 16:02+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#: .\accounts\admin.py:12 +msgid "password" +msgstr "密碼" + +#: .\accounts\admin.py:13 +msgid "Enter password again" +msgstr "再次輸入密碼" + +#: .\accounts\admin.py:24 .\accounts\forms.py:89 +msgid "passwords do not match" +msgstr "密碼不匹配" + +#: .\accounts\forms.py:36 +msgid "email already exists" +msgstr "郵箱已存在" + +#: .\accounts\forms.py:46 .\accounts\forms.py:50 +msgid "New password" +msgstr "新密碼" + +#: .\accounts\forms.py:60 +msgid "Confirm password" +msgstr "確認密碼" + +#: .\accounts\forms.py:70 .\accounts\forms.py:116 +msgid "Email" +msgstr "郵箱" + +#: .\accounts\forms.py:76 .\accounts\forms.py:80 +msgid "Code" +msgstr "驗證碼" + +#: .\accounts\forms.py:100 .\accounts\tests.py:194 +msgid "email does not exist" +msgstr "郵箱不存在" + +#: .\accounts\models.py:12 .\oauth\models.py:17 +msgid "nick name" +msgstr "昵稱" + +#: .\accounts\models.py:13 .\blog\models.py:29 .\blog\models.py:266 +#: .\blog\models.py:284 .\comments\models.py:13 .\oauth\models.py:23 +#: .\oauth\models.py:53 +msgid "creation time" +msgstr "創建時間" + +#: .\accounts\models.py:14 .\comments\models.py:14 .\oauth\models.py:24 +#: .\oauth\models.py:54 +msgid "last modify time" +msgstr "最後修改時間" + +#: .\accounts\models.py:15 +msgid "create source" +msgstr "來源" + +#: .\accounts\models.py:33 .\djangoblog\logentryadmin.py:81 +msgid "user" +msgstr "用戶" + +#: .\accounts\tests.py:216 .\accounts\utils.py:39 +msgid "Verification code error" +msgstr "驗證碼錯誤" + +#: .\accounts\utils.py:13 +msgid "Verify Email" +msgstr "驗證郵箱" + +#: .\accounts\utils.py:21 +#, python-format +msgid "" +"You are resetting the password, the verification code is:%(code)s, valid " +"within 5 minutes, please keep it properly" +msgstr "您正在重置密碼,驗證碼為:%(code)s,5分鐘內有效 請妥善保管." + +#: .\blog\admin.py:13 .\blog\models.py:92 .\comments\models.py:17 +#: .\oauth\models.py:12 +msgid "author" +msgstr "作者" + +#: .\blog\admin.py:53 +msgid "Publish selected articles" +msgstr "發布選中的文章" + +#: .\blog\admin.py:54 +msgid "Draft selected articles" +msgstr "選中文章設為草稿" + +#: .\blog\admin.py:55 +msgid "Close article comments" +msgstr "關閉文章評論" + +#: .\blog\admin.py:56 +msgid "Open article comments" +msgstr "打開文章評論" + +#: .\blog\admin.py:89 .\blog\models.py:101 .\blog\models.py:183 +#: .\templates\blog\tags\sidebar.html:40 +msgid "category" +msgstr "分類目錄" + +#: .\blog\models.py:20 .\blog\models.py:179 .\templates\share_layout\nav.html:8 +msgid "index" +msgstr "首頁" + +#: .\blog\models.py:21 +msgid "list" +msgstr "列表" + +#: .\blog\models.py:22 +msgid "post" +msgstr "文章" + +#: .\blog\models.py:23 +msgid "all" +msgstr "所有" + +#: .\blog\models.py:24 +msgid "slide" +msgstr "側邊欄" + +#: .\blog\models.py:30 .\blog\models.py:267 .\blog\models.py:285 +msgid "modify time" +msgstr "修改時間" + +#: .\blog\models.py:63 +msgid "Draft" +msgstr "草稿" + +#: .\blog\models.py:64 +msgid "Published" +msgstr "發布" + +#: .\blog\models.py:67 +msgid "Open" +msgstr "打開" + +#: .\blog\models.py:68 +msgid "Close" +msgstr "關閉" + +#: .\blog\models.py:71 .\comments\admin.py:47 +msgid "Article" +msgstr "文章" + +#: .\blog\models.py:72 +msgid "Page" +msgstr "頁面" + +#: .\blog\models.py:74 .\blog\models.py:280 +msgid "title" +msgstr "標題" + +#: .\blog\models.py:75 +msgid "body" +msgstr "內容" + +#: .\blog\models.py:77 +msgid "publish time" +msgstr "發布時間" + +#: .\blog\models.py:79 +msgid "status" +msgstr "狀態" + +#: .\blog\models.py:84 +msgid "comment status" +msgstr "評論狀態" + +#: .\blog\models.py:88 .\oauth\models.py:43 +msgid "type" +msgstr "類型" + +#: .\blog\models.py:89 +msgid "views" +msgstr "閱讀量" + +#: .\blog\models.py:97 .\blog\models.py:258 .\blog\models.py:282 +msgid "order" +msgstr "排序" + +#: .\blog\models.py:98 +msgid "show toc" +msgstr "顯示目錄" + +#: .\blog\models.py:105 .\blog\models.py:249 +msgid "tag" +msgstr "標簽" + +#: .\blog\models.py:115 .\comments\models.py:21 +msgid "article" +msgstr "文章" + +#: .\blog\models.py:171 +msgid "category name" +msgstr "分類名" + +#: .\blog\models.py:174 +msgid "parent category" +msgstr "上級分類" + +#: .\blog\models.py:234 +msgid "tag name" +msgstr "標簽名" + +#: .\blog\models.py:256 +msgid "link name" +msgstr "鏈接名" + +#: .\blog\models.py:257 .\blog\models.py:271 +msgid "link" +msgstr "鏈接" + +#: .\blog\models.py:260 +msgid "is show" +msgstr "是否顯示" + +#: .\blog\models.py:262 +msgid "show type" +msgstr "顯示類型" + +#: .\blog\models.py:281 +msgid "content" +msgstr "內容" + +#: .\blog\models.py:283 .\oauth\models.py:52 +msgid "is enable" +msgstr "是否啟用" + +#: .\blog\models.py:289 +msgid "sidebar" +msgstr "側邊欄" + +#: .\blog\models.py:299 +msgid "site name" +msgstr "站點名稱" + +#: .\blog\models.py:305 +msgid "site description" +msgstr "站點描述" + +#: .\blog\models.py:311 +msgid "site seo description" +msgstr "站點SEO描述" + +#: .\blog\models.py:313 +msgid "site keywords" +msgstr "關鍵字" + +#: .\blog\models.py:318 +msgid "article sub length" +msgstr "文章摘要長度" + +#: .\blog\models.py:319 +msgid "sidebar article count" +msgstr "側邊欄文章數目" + +#: .\blog\models.py:320 +msgid "sidebar comment count" +msgstr "側邊欄評論數目" + +#: .\blog\models.py:321 +msgid "article comment count" +msgstr "文章頁面默認顯示評論數目" + +#: .\blog\models.py:322 +msgid "show adsense" +msgstr "是否顯示廣告" + +#: .\blog\models.py:324 +msgid "adsense code" +msgstr "廣告內容" + +#: .\blog\models.py:325 +msgid "open site comment" +msgstr "公共頭部" + +#: .\blog\models.py:352 +msgid "Website configuration" +msgstr "網站配置" + +#: .\blog\models.py:360 +msgid "There can only be one configuration" +msgstr "只能有一個配置" + +#: .\blog\views.py:348 +msgid "" +"Sorry, the page you requested is not found, please click the home page to " +"see other?" +msgstr "抱歉,你所訪問的頁面找不到,請點擊首頁看看別的?" + +#: .\blog\views.py:356 +msgid "Sorry, the server is busy, please click the home page to see other?" +msgstr "抱歉,服務出錯了,請點擊首頁看看別的?" + +#: .\blog\views.py:369 +msgid "Sorry, you do not have permission to access this page?" +msgstr "抱歉,你沒用權限訪問此頁面。" + +#: .\comments\admin.py:15 +msgid "Disable comments" +msgstr "禁用評論" + +#: .\comments\admin.py:16 +msgid "Enable comments" +msgstr "啟用評論" + +#: .\comments\admin.py:46 +msgid "User" +msgstr "用戶" + +#: .\comments\models.py:25 +msgid "parent comment" +msgstr "上級評論" + +#: .\comments\models.py:29 +msgid "enable" +msgstr "啟用" + +#: .\comments\models.py:34 .\templates\blog\tags\article_info.html:30 +msgid "comment" +msgstr "評論" + +#: .\comments\utils.py:13 +msgid "Thanks for your comment" +msgstr "感謝你的評論" + +#: .\comments\utils.py:15 +#, python-format +msgid "" +"

Thank you very much for your comments on this site

\n" +" You can visit %(article_title)s\n" +" to review your comments,\n" +" Thank you again!\n" +"
\n" +" If the link above cannot be opened, please copy this " +"link to your browser.\n" +" %(article_url)s" +msgstr "" +"

非常感謝您對此網站的評論

\n" +" 您可以訪問%(article_title)s\n" +"查看您的評論,\n" +"再次感謝您!\n" +"
\n" +" 如果上面的鏈接打不開,請復製此鏈接鏈接到您的瀏覽器。\n" +"%(article_url)s" + +#: .\comments\utils.py:26 +#, python-format +msgid "" +"Your comment on " +"%(article_title)s
has \n" +" received a reply.
%(comment_body)s\n" +"
\n" +" go check it out!\n" +"
\n" +" If the link above cannot be opened, please copy this " +"link to your browser.\n" +" %(article_url)s\n" +" " +msgstr "" +"您對 %(article_title)s
" +"的評論有\n" +" 收到回復。
%(comment_body)s\n" +"
\n" +"快去看看吧!\n" +"
\n" +" 如果上面的鏈接打不開,請復製此鏈接鏈接到您的瀏覽器。\n" +" %(article_url)s\n" +" " + +#: .\djangoblog\logentryadmin.py:63 +msgid "object" +msgstr "對象" + +#: .\djangoblog\settings.py:140 +msgid "English" +msgstr "英文" + +#: .\djangoblog\settings.py:141 +msgid "Simplified Chinese" +msgstr "簡體中文" + +#: .\djangoblog\settings.py:142 +msgid "Traditional Chinese" +msgstr "繁體中文" + +#: .\oauth\models.py:30 +msgid "oauth user" +msgstr "第三方用戶" + +#: .\oauth\models.py:37 +msgid "weibo" +msgstr "微博" + +#: .\oauth\models.py:38 +msgid "google" +msgstr "谷歌" + +#: .\oauth\models.py:48 +msgid "callback url" +msgstr "回調地址" + +#: .\oauth\models.py:59 +msgid "already exists" +msgstr "已經存在" + +#: .\oauth\views.py:154 +#, python-format +msgid "" +"\n" +"

Congratulations, you have successfully bound your email address. You " +"can use\n" +" %(oauthuser_type)s to directly log in to this website without a " +"password.

\n" +" You are welcome to continue to follow this site, the address is\n" +" %(site)s\n" +" Thank you again!\n" +"
\n" +" If the link above cannot be opened, please copy this link to your " +"browser.\n" +" %(site)s\n" +" " +msgstr "" +"\n" +"

恭喜你已經綁定成功 你可以使用\n" +" %(oauthuser_type)s 來免密登錄本站

\n" +" 歡迎繼續關註本站, 地址是\n" +" %(site)s\n" +" 再次感謝你\n" +"
\n" +" 如果上面鏈接無法打開,請復製此鏈接到你的瀏覽器 \n" +" %(site)s\n" +" " + +#: .\oauth\views.py:165 +msgid "Congratulations on your successful binding!" +msgstr "恭喜你綁定成功" + +#: .\oauth\views.py:217 +#, python-format +msgid "" +"\n" +"

Please click the link below to bind your email

\n" +"\n" +" %(url)s\n" +"\n" +" Thank you again!\n" +"
\n" +" If the link above cannot be opened, please copy this link " +"to your browser.\n" +"
\n" +" %(url)s\n" +" " +msgstr "" +"\n" +"

請點擊下面的鏈接綁定您的郵箱

\n" +"\n" +" %(url)s\n" +"\n" +"再次感謝您!\n" +"
\n" +"如果上面的鏈接打不開,請復製此鏈接到您的瀏覽器。\n" +"%(url)s\n" +" " + +#: .\oauth\views.py:228 .\oauth\views.py:240 +msgid "Bind your email" +msgstr "綁定郵箱" + +#: .\oauth\views.py:242 +msgid "" +"Congratulations, the binding is just one step away. Please log in to your " +"email to check the email to complete the binding. Thank you." +msgstr "恭喜您,還差一步就綁定成功了,請登錄您的郵箱查看郵件完成綁定,謝謝。" + +#: .\oauth\views.py:245 +msgid "Binding successful" +msgstr "綁定成功" + +#: .\oauth\views.py:247 +#, python-format +msgid "" +"Congratulations, you have successfully bound your email address. You can use " +"%(oauthuser_type)s to directly log in to this website without a password. " +"You are welcome to continue to follow this site." +msgstr "" +"恭喜您綁定成功,您以後可以使用%(oauthuser_type)s來直接免密碼登錄本站啦,感謝" +"您對本站對關註。" + +#: .\templates\account\forget_password.html:7 +msgid "forget the password" +msgstr "忘記密碼" + +#: .\templates\account\forget_password.html:18 +msgid "get verification code" +msgstr "獲取驗證碼" + +#: .\templates\account\forget_password.html:19 +msgid "submit" +msgstr "提交" + +#: .\templates\account\login.html:36 +msgid "Create Account" +msgstr "創建賬號" + +#: .\templates\account\login.html:42 +#, fuzzy +#| msgid "forget the password" +msgid "Forget Password" +msgstr "忘記密碼" + +#: .\templates\account\result.html:18 .\templates\blog\tags\sidebar.html:126 +msgid "login" +msgstr "登錄" + +#: .\templates\account\result.html:22 +msgid "back to the homepage" +msgstr "返回首頁吧" + +#: .\templates\blog\article_archives.html:7 +#: .\templates\blog\article_archives.html:24 +msgid "article archive" +msgstr "文章歸檔" + +#: .\templates\blog\article_archives.html:32 +msgid "year" +msgstr "年" + +#: .\templates\blog\article_archives.html:36 +msgid "month" +msgstr "月" + +#: .\templates\blog\tags\article_info.html:12 +msgid "pin to top" +msgstr "置頂" + +#: .\templates\blog\tags\article_info.html:28 +msgid "comments" +msgstr "評論" + +#: .\templates\blog\tags\article_info.html:58 +msgid "toc" +msgstr "目錄" + +#: .\templates\blog\tags\article_meta_info.html:6 +msgid "posted in" +msgstr "發布於" + +#: .\templates\blog\tags\article_meta_info.html:14 +msgid "and tagged" +msgstr "並標記為" + +#: .\templates\blog\tags\article_meta_info.html:25 +msgid "by " +msgstr "由" + +#: .\templates\blog\tags\article_meta_info.html:29 +#, python-format +msgid "" +"\n" +" title=\"View all articles published by " +"%(article.author.username)s\"\n" +" " +msgstr "" +"\n" +" title=\"查看所有由 %(article.author.username)s\"發布的文章\n" +" " + +#: .\templates\blog\tags\article_meta_info.html:44 +msgid "on" +msgstr "在" + +#: .\templates\blog\tags\article_meta_info.html:54 +msgid "edit" +msgstr "編輯" + +#: .\templates\blog\tags\article_pagination.html:4 +msgid "article navigation" +msgstr "文章導航" + +#: .\templates\blog\tags\article_pagination.html:9 +msgid "earlier articles" +msgstr "早期文章" + +#: .\templates\blog\tags\article_pagination.html:12 +msgid "newer articles" +msgstr "較新文章" + +#: .\templates\blog\tags\article_tag_list.html:5 +msgid "tags" +msgstr "標簽" + +#: .\templates\blog\tags\sidebar.html:7 +msgid "search" +msgstr "搜索" + +#: .\templates\blog\tags\sidebar.html:50 +msgid "recent comments" +msgstr "近期評論" + +#: .\templates\blog\tags\sidebar.html:57 +msgid "published on" +msgstr "發表於" + +#: .\templates\blog\tags\sidebar.html:65 +msgid "recent articles" +msgstr "近期文章" + +#: .\templates\blog\tags\sidebar.html:77 +msgid "bookmark" +msgstr "書簽" + +#: .\templates\blog\tags\sidebar.html:96 +msgid "Tag Cloud" +msgstr "標簽雲" + +#: .\templates\blog\tags\sidebar.html:107 +msgid "Welcome to star or fork the source code of this site" +msgstr "歡迎您STAR或者FORK本站源代碼" + +#: .\templates\blog\tags\sidebar.html:118 +msgid "Function" +msgstr "功能" + +#: .\templates\blog\tags\sidebar.html:120 +msgid "management site" +msgstr "管理站點" + +#: .\templates\blog\tags\sidebar.html:122 +msgid "logout" +msgstr "登出" + +#: .\templates\blog\tags\sidebar.html:129 +msgid "Track record" +msgstr "運動軌跡記錄" + +#: .\templates\blog\tags\sidebar.html:135 +msgid "Click me to return to the top" +msgstr "點我返回頂部" + +#: .\templates\oauth\oauth_applications.html:5 +#| msgid "login" +msgid "quick login" +msgstr "快捷登錄" + +#: .\templates\share_layout\nav.html:26 +msgid "Article archive" +msgstr "文章歸檔" diff --git a/DjangoBlog-master/manage.py b/DjangoBlog-master/manage.py new file mode 100644 index 0000000..919ba74 --- /dev/null +++ b/DjangoBlog-master/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +import os +import sys + +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "djangoblog.settings") + try: + from django.core.management import execute_from_command_line + except ImportError: + # The above import may fail for some other reason. Ensure that the + # issue is really that Django is missing to avoid masking other + # exceptions on Python 2. + try: + import django + except ImportError: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) + raise + execute_from_command_line(sys.argv) diff --git a/DjangoBlog-master/oauth/__init__.py b/DjangoBlog-master/oauth/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/DjangoBlog-master/oauth/admin.py b/DjangoBlog-master/oauth/admin.py new file mode 100644 index 0000000..57eab5f --- /dev/null +++ b/DjangoBlog-master/oauth/admin.py @@ -0,0 +1,54 @@ +import logging + +from django.contrib import admin +# Register your models here. +from django.urls import reverse +from django.utils.html import format_html + +logger = logging.getLogger(__name__) + + +class OAuthUserAdmin(admin.ModelAdmin): + search_fields = ('nickname', 'email') + list_per_page = 20 + list_display = ( + 'id', + 'nickname', + 'link_to_usermodel', + 'show_user_image', + 'type', + 'email', + ) + list_display_links = ('id', 'nickname') + list_filter = ('author', 'type',) + readonly_fields = [] + + def get_readonly_fields(self, request, obj=None): + return list(self.readonly_fields) + \ + [field.name for field in obj._meta.fields] + \ + [field.name for field in obj._meta.many_to_many] + + def has_add_permission(self, request): + return False + + def link_to_usermodel(self, obj): + if obj.author: + info = (obj.author._meta.app_label, obj.author._meta.model_name) + link = reverse('admin:%s_%s_change' % info, args=(obj.author.id,)) + return format_html( + u'%s' % + (link, obj.author.nickname if obj.author.nickname else obj.author.email)) + + def show_user_image(self, obj): + img = obj.picture + return format_html( + u'' % + (img)) + + link_to_usermodel.short_description = '用户' + show_user_image.short_description = '用户头像' + + +class OAuthConfigAdmin(admin.ModelAdmin): + list_display = ('type', 'appkey', 'appsecret', 'is_enable') + list_filter = ('type',) diff --git a/DjangoBlog-master/oauth/apps.py b/DjangoBlog-master/oauth/apps.py new file mode 100644 index 0000000..17fcea2 --- /dev/null +++ b/DjangoBlog-master/oauth/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class OauthConfig(AppConfig): + name = 'oauth' diff --git a/DjangoBlog-master/oauth/forms.py b/DjangoBlog-master/oauth/forms.py new file mode 100644 index 0000000..0e4ede3 --- /dev/null +++ b/DjangoBlog-master/oauth/forms.py @@ -0,0 +1,12 @@ +from django.contrib.auth.forms import forms +from django.forms import widgets + + +class RequireEmailForm(forms.Form): + email = forms.EmailField(label='电子邮箱', required=True) + oauthid = forms.IntegerField(widget=forms.HiddenInput, required=False) + + def __init__(self, *args, **kwargs): + super(RequireEmailForm, self).__init__(*args, **kwargs) + self.fields['email'].widget = widgets.EmailInput( + attrs={'placeholder': "email", "class": "form-control"}) diff --git a/DjangoBlog-master/oauth/migrations/0001_initial.py b/DjangoBlog-master/oauth/migrations/0001_initial.py new file mode 100644 index 0000000..3aa3e03 --- /dev/null +++ b/DjangoBlog-master/oauth/migrations/0001_initial.py @@ -0,0 +1,57 @@ +# Generated by Django 4.1.7 on 2023-03-07 09:53 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='OAuthConfig', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('type', models.CharField(choices=[('weibo', '微博'), ('google', '谷歌'), ('github', 'GitHub'), ('facebook', 'FaceBook'), ('qq', 'QQ')], default='a', max_length=10, verbose_name='类型')), + ('appkey', models.CharField(max_length=200, verbose_name='AppKey')), + ('appsecret', models.CharField(max_length=200, verbose_name='AppSecret')), + ('callback_url', models.CharField(default='http://www.baidu.com', max_length=200, verbose_name='回调地址')), + ('is_enable', models.BooleanField(default=True, verbose_name='是否显示')), + ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')), + ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')), + ], + options={ + 'verbose_name': 'oauth配置', + 'verbose_name_plural': 'oauth配置', + 'ordering': ['-created_time'], + }, + ), + migrations.CreateModel( + name='OAuthUser', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('openid', models.CharField(max_length=50)), + ('nickname', models.CharField(max_length=50, verbose_name='昵称')), + ('token', models.CharField(blank=True, max_length=150, null=True)), + ('picture', models.CharField(blank=True, max_length=350, null=True)), + ('type', models.CharField(max_length=50)), + ('email', models.CharField(blank=True, max_length=50, null=True)), + ('metadata', models.TextField(blank=True, null=True)), + ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')), + ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')), + ('author', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='用户')), + ], + options={ + 'verbose_name': 'oauth用户', + 'verbose_name_plural': 'oauth用户', + 'ordering': ['-created_time'], + }, + ), + ] diff --git a/DjangoBlog-master/oauth/migrations/0002_alter_oauthconfig_options_alter_oauthuser_options_and_more.py b/DjangoBlog-master/oauth/migrations/0002_alter_oauthconfig_options_alter_oauthuser_options_and_more.py new file mode 100644 index 0000000..d5cc70e --- /dev/null +++ b/DjangoBlog-master/oauth/migrations/0002_alter_oauthconfig_options_alter_oauthuser_options_and_more.py @@ -0,0 +1,86 @@ +# Generated by Django 4.2.5 on 2023-09-06 13:13 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('oauth', '0001_initial'), + ] + + operations = [ + migrations.AlterModelOptions( + name='oauthconfig', + options={'ordering': ['-creation_time'], 'verbose_name': 'oauth配置', 'verbose_name_plural': 'oauth配置'}, + ), + migrations.AlterModelOptions( + name='oauthuser', + options={'ordering': ['-creation_time'], 'verbose_name': 'oauth user', 'verbose_name_plural': 'oauth user'}, + ), + migrations.RemoveField( + model_name='oauthconfig', + name='created_time', + ), + migrations.RemoveField( + model_name='oauthconfig', + name='last_mod_time', + ), + migrations.RemoveField( + model_name='oauthuser', + name='created_time', + ), + migrations.RemoveField( + model_name='oauthuser', + name='last_mod_time', + ), + migrations.AddField( + model_name='oauthconfig', + name='creation_time', + field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'), + ), + migrations.AddField( + model_name='oauthconfig', + name='last_modify_time', + field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'), + ), + migrations.AddField( + model_name='oauthuser', + name='creation_time', + field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'), + ), + migrations.AddField( + model_name='oauthuser', + name='last_modify_time', + field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'), + ), + migrations.AlterField( + model_name='oauthconfig', + name='callback_url', + field=models.CharField(default='', max_length=200, verbose_name='callback url'), + ), + migrations.AlterField( + model_name='oauthconfig', + name='is_enable', + field=models.BooleanField(default=True, verbose_name='is enable'), + ), + migrations.AlterField( + model_name='oauthconfig', + name='type', + field=models.CharField(choices=[('weibo', 'weibo'), ('google', 'google'), ('github', 'GitHub'), ('facebook', 'FaceBook'), ('qq', 'QQ')], default='a', max_length=10, verbose_name='type'), + ), + migrations.AlterField( + model_name='oauthuser', + name='author', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='author'), + ), + migrations.AlterField( + model_name='oauthuser', + name='nickname', + field=models.CharField(max_length=50, verbose_name='nickname'), + ), + ] diff --git a/DjangoBlog-master/oauth/migrations/0003_alter_oauthuser_nickname.py b/DjangoBlog-master/oauth/migrations/0003_alter_oauthuser_nickname.py new file mode 100644 index 0000000..6af08eb --- /dev/null +++ b/DjangoBlog-master/oauth/migrations/0003_alter_oauthuser_nickname.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.7 on 2024-01-26 02:41 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('oauth', '0002_alter_oauthconfig_options_alter_oauthuser_options_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='oauthuser', + name='nickname', + field=models.CharField(max_length=50, verbose_name='nick name'), + ), + ] diff --git a/DjangoBlog-master/oauth/migrations/__init__.py b/DjangoBlog-master/oauth/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/DjangoBlog-master/oauth/models.py b/DjangoBlog-master/oauth/models.py new file mode 100644 index 0000000..be838ed --- /dev/null +++ b/DjangoBlog-master/oauth/models.py @@ -0,0 +1,67 @@ +# Create your models here. +from django.conf import settings +from django.core.exceptions import ValidationError +from django.db import models +from django.utils.timezone import now +from django.utils.translation import gettext_lazy as _ + + +class OAuthUser(models.Model): + author = models.ForeignKey( + settings.AUTH_USER_MODEL, + verbose_name=_('author'), + blank=True, + null=True, + on_delete=models.CASCADE) + openid = models.CharField(max_length=50) + nickname = models.CharField(max_length=50, verbose_name=_('nick name')) + token = models.CharField(max_length=150, null=True, blank=True) + picture = models.CharField(max_length=350, blank=True, null=True) + type = models.CharField(blank=False, null=False, max_length=50) + email = models.CharField(max_length=50, null=True, blank=True) + metadata = models.TextField(null=True, blank=True) + creation_time = models.DateTimeField(_('creation time'), default=now) + last_modify_time = models.DateTimeField(_('last modify time'), default=now) + + def __str__(self): + return self.nickname + + class Meta: + verbose_name = _('oauth user') + verbose_name_plural = verbose_name + ordering = ['-creation_time'] + + +class OAuthConfig(models.Model): + TYPE = ( + ('weibo', _('weibo')), + ('google', _('google')), + ('github', 'GitHub'), + ('facebook', 'FaceBook'), + ('qq', 'QQ'), + ) + type = models.CharField(_('type'), max_length=10, choices=TYPE, default='a') + appkey = models.CharField(max_length=200, verbose_name='AppKey') + appsecret = models.CharField(max_length=200, verbose_name='AppSecret') + callback_url = models.CharField( + max_length=200, + verbose_name=_('callback url'), + blank=False, + default='') + is_enable = models.BooleanField( + _('is enable'), default=True, blank=False, null=False) + creation_time = models.DateTimeField(_('creation time'), default=now) + last_modify_time = models.DateTimeField(_('last modify time'), default=now) + + def clean(self): + if OAuthConfig.objects.filter( + type=self.type).exclude(id=self.id).count(): + raise ValidationError(_(self.type + _('already exists'))) + + def __str__(self): + return self.type + + class Meta: + verbose_name = 'oauth配置' + verbose_name_plural = verbose_name + ordering = ['-creation_time'] diff --git a/DjangoBlog-master/oauth/oauthmanager.py b/DjangoBlog-master/oauth/oauthmanager.py new file mode 100644 index 0000000..2e7ceef --- /dev/null +++ b/DjangoBlog-master/oauth/oauthmanager.py @@ -0,0 +1,504 @@ +import json +import logging +import os +import urllib.parse +from abc import ABCMeta, abstractmethod + +import requests + +from djangoblog.utils import cache_decorator +from oauth.models import OAuthUser, OAuthConfig + +logger = logging.getLogger(__name__) + + +class OAuthAccessTokenException(Exception): + ''' + oauth授权失败异常 + ''' + + +class BaseOauthManager(metaclass=ABCMeta): + """获取用户授权""" + AUTH_URL = None + """获取token""" + TOKEN_URL = None + """获取用户信息""" + API_URL = None + '''icon图标名''' + ICON_NAME = None + + def __init__(self, access_token=None, openid=None): + self.access_token = access_token + self.openid = openid + + @property + def is_access_token_set(self): + return self.access_token is not None + + @property + def is_authorized(self): + return self.is_access_token_set and self.access_token is not None and self.openid is not None + + @abstractmethod + def get_authorization_url(self, nexturl='/'): + pass + + @abstractmethod + def get_access_token_by_code(self, code): + pass + + @abstractmethod + def get_oauth_userinfo(self): + pass + + @abstractmethod + def get_picture(self, metadata): + pass + + def do_get(self, url, params, headers=None): + rsp = requests.get(url=url, params=params, headers=headers) + logger.info(rsp.text) + return rsp.text + + def do_post(self, url, params, headers=None): + rsp = requests.post(url, params, headers=headers) + logger.info(rsp.text) + return rsp.text + + def get_config(self): + value = OAuthConfig.objects.filter(type=self.ICON_NAME) + return value[0] if value else None + + +class WBOauthManager(BaseOauthManager): + AUTH_URL = 'https://api.weibo.com/oauth2/authorize' + TOKEN_URL = 'https://api.weibo.com/oauth2/access_token' + API_URL = 'https://api.weibo.com/2/users/show.json' + ICON_NAME = 'weibo' + + def __init__(self, access_token=None, openid=None): + config = self.get_config() + self.client_id = config.appkey if config else '' + self.client_secret = config.appsecret if config else '' + self.callback_url = config.callback_url if config else '' + super( + WBOauthManager, + self).__init__( + access_token=access_token, + openid=openid) + + def get_authorization_url(self, nexturl='/'): + params = { + 'client_id': self.client_id, + 'response_type': 'code', + 'redirect_uri': self.callback_url + '&next_url=' + nexturl + } + url = self.AUTH_URL + "?" + urllib.parse.urlencode(params) + return url + + def get_access_token_by_code(self, code): + + params = { + 'client_id': self.client_id, + 'client_secret': self.client_secret, + 'grant_type': 'authorization_code', + 'code': code, + 'redirect_uri': self.callback_url + } + rsp = self.do_post(self.TOKEN_URL, params) + + obj = json.loads(rsp) + if 'access_token' in obj: + self.access_token = str(obj['access_token']) + self.openid = str(obj['uid']) + return self.get_oauth_userinfo() + else: + raise OAuthAccessTokenException(rsp) + + def get_oauth_userinfo(self): + if not self.is_authorized: + return None + params = { + 'uid': self.openid, + 'access_token': self.access_token + } + rsp = self.do_get(self.API_URL, params) + try: + datas = json.loads(rsp) + user = OAuthUser() + user.metadata = rsp + user.picture = datas['avatar_large'] + user.nickname = datas['screen_name'] + user.openid = datas['id'] + user.type = 'weibo' + user.token = self.access_token + if 'email' in datas and datas['email']: + user.email = datas['email'] + return user + except Exception as e: + logger.error(e) + logger.error('weibo oauth error.rsp:' + rsp) + return None + + def get_picture(self, metadata): + datas = json.loads(metadata) + return datas['avatar_large'] + + +class ProxyManagerMixin: + def __init__(self, *args, **kwargs): + if os.environ.get("HTTP_PROXY"): + self.proxies = { + "http": os.environ.get("HTTP_PROXY"), + "https": os.environ.get("HTTP_PROXY") + } + else: + self.proxies = None + + def do_get(self, url, params, headers=None): + rsp = requests.get(url=url, params=params, headers=headers, proxies=self.proxies) + logger.info(rsp.text) + return rsp.text + + def do_post(self, url, params, headers=None): + rsp = requests.post(url, params, headers=headers, proxies=self.proxies) + logger.info(rsp.text) + return rsp.text + + +class GoogleOauthManager(ProxyManagerMixin, BaseOauthManager): + AUTH_URL = 'https://accounts.google.com/o/oauth2/v2/auth' + TOKEN_URL = 'https://www.googleapis.com/oauth2/v4/token' + API_URL = 'https://www.googleapis.com/oauth2/v3/userinfo' + ICON_NAME = 'google' + + def __init__(self, access_token=None, openid=None): + config = self.get_config() + self.client_id = config.appkey if config else '' + self.client_secret = config.appsecret if config else '' + self.callback_url = config.callback_url if config else '' + super( + GoogleOauthManager, + self).__init__( + access_token=access_token, + openid=openid) + + def get_authorization_url(self, nexturl='/'): + params = { + 'client_id': self.client_id, + 'response_type': 'code', + 'redirect_uri': self.callback_url, + 'scope': 'openid email', + } + url = self.AUTH_URL + "?" + urllib.parse.urlencode(params) + return url + + def get_access_token_by_code(self, code): + params = { + 'client_id': self.client_id, + 'client_secret': self.client_secret, + 'grant_type': 'authorization_code', + 'code': code, + + 'redirect_uri': self.callback_url + } + rsp = self.do_post(self.TOKEN_URL, params) + + obj = json.loads(rsp) + + if 'access_token' in obj: + self.access_token = str(obj['access_token']) + self.openid = str(obj['id_token']) + logger.info(self.ICON_NAME + ' oauth ' + rsp) + return self.access_token + else: + raise OAuthAccessTokenException(rsp) + + def get_oauth_userinfo(self): + if not self.is_authorized: + return None + params = { + 'access_token': self.access_token + } + rsp = self.do_get(self.API_URL, params) + try: + + datas = json.loads(rsp) + user = OAuthUser() + user.metadata = rsp + user.picture = datas['picture'] + user.nickname = datas['name'] + user.openid = datas['sub'] + user.token = self.access_token + user.type = 'google' + if datas['email']: + user.email = datas['email'] + return user + except Exception as e: + logger.error(e) + logger.error('google oauth error.rsp:' + rsp) + return None + + def get_picture(self, metadata): + datas = json.loads(metadata) + return datas['picture'] + + +class GitHubOauthManager(ProxyManagerMixin, BaseOauthManager): + AUTH_URL = 'https://github.com/login/oauth/authorize' + TOKEN_URL = 'https://github.com/login/oauth/access_token' + API_URL = 'https://api.github.com/user' + ICON_NAME = 'github' + + def __init__(self, access_token=None, openid=None): + config = self.get_config() + self.client_id = config.appkey if config else '' + self.client_secret = config.appsecret if config else '' + self.callback_url = config.callback_url if config else '' + super( + GitHubOauthManager, + self).__init__( + access_token=access_token, + openid=openid) + + def get_authorization_url(self, next_url='/'): + params = { + 'client_id': self.client_id, + 'response_type': 'code', + 'redirect_uri': f'{self.callback_url}&next_url={next_url}', + 'scope': 'user' + } + url = self.AUTH_URL + "?" + urllib.parse.urlencode(params) + return url + + def get_access_token_by_code(self, code): + params = { + 'client_id': self.client_id, + 'client_secret': self.client_secret, + 'grant_type': 'authorization_code', + 'code': code, + + 'redirect_uri': self.callback_url + } + rsp = self.do_post(self.TOKEN_URL, params) + + from urllib import parse + r = parse.parse_qs(rsp) + if 'access_token' in r: + self.access_token = (r['access_token'][0]) + return self.access_token + else: + raise OAuthAccessTokenException(rsp) + + def get_oauth_userinfo(self): + + rsp = self.do_get(self.API_URL, params={}, headers={ + "Authorization": "token " + self.access_token + }) + try: + datas = json.loads(rsp) + user = OAuthUser() + user.picture = datas['avatar_url'] + user.nickname = datas['name'] + user.openid = datas['id'] + user.type = 'github' + user.token = self.access_token + user.metadata = rsp + if 'email' in datas and datas['email']: + user.email = datas['email'] + return user + except Exception as e: + logger.error(e) + logger.error('github oauth error.rsp:' + rsp) + return None + + def get_picture(self, metadata): + datas = json.loads(metadata) + return datas['avatar_url'] + + +class FaceBookOauthManager(ProxyManagerMixin, BaseOauthManager): + AUTH_URL = 'https://www.facebook.com/v16.0/dialog/oauth' + TOKEN_URL = 'https://graph.facebook.com/v16.0/oauth/access_token' + API_URL = 'https://graph.facebook.com/me' + ICON_NAME = 'facebook' + + def __init__(self, access_token=None, openid=None): + config = self.get_config() + self.client_id = config.appkey if config else '' + self.client_secret = config.appsecret if config else '' + self.callback_url = config.callback_url if config else '' + super( + FaceBookOauthManager, + self).__init__( + access_token=access_token, + openid=openid) + + def get_authorization_url(self, next_url='/'): + params = { + 'client_id': self.client_id, + 'response_type': 'code', + 'redirect_uri': self.callback_url, + 'scope': 'email,public_profile' + } + url = self.AUTH_URL + "?" + urllib.parse.urlencode(params) + return url + + def get_access_token_by_code(self, code): + params = { + 'client_id': self.client_id, + 'client_secret': self.client_secret, + # 'grant_type': 'authorization_code', + 'code': code, + + 'redirect_uri': self.callback_url + } + rsp = self.do_post(self.TOKEN_URL, params) + + obj = json.loads(rsp) + if 'access_token' in obj: + token = str(obj['access_token']) + self.access_token = token + return self.access_token + else: + raise OAuthAccessTokenException(rsp) + + def get_oauth_userinfo(self): + params = { + 'access_token': self.access_token, + 'fields': 'id,name,picture,email' + } + try: + rsp = self.do_get(self.API_URL, params) + datas = json.loads(rsp) + user = OAuthUser() + user.nickname = datas['name'] + user.openid = datas['id'] + user.type = 'facebook' + user.token = self.access_token + user.metadata = rsp + if 'email' in datas and datas['email']: + user.email = datas['email'] + if 'picture' in datas and datas['picture'] and datas['picture']['data'] and datas['picture']['data']['url']: + user.picture = str(datas['picture']['data']['url']) + return user + except Exception as e: + logger.error(e) + return None + + def get_picture(self, metadata): + datas = json.loads(metadata) + return str(datas['picture']['data']['url']) + + +class QQOauthManager(BaseOauthManager): + AUTH_URL = 'https://graph.qq.com/oauth2.0/authorize' + TOKEN_URL = 'https://graph.qq.com/oauth2.0/token' + API_URL = 'https://graph.qq.com/user/get_user_info' + OPEN_ID_URL = 'https://graph.qq.com/oauth2.0/me' + ICON_NAME = 'qq' + + def __init__(self, access_token=None, openid=None): + config = self.get_config() + self.client_id = config.appkey if config else '' + self.client_secret = config.appsecret if config else '' + self.callback_url = config.callback_url if config else '' + super( + QQOauthManager, + self).__init__( + access_token=access_token, + openid=openid) + + def get_authorization_url(self, next_url='/'): + params = { + 'response_type': 'code', + 'client_id': self.client_id, + 'redirect_uri': self.callback_url + '&next_url=' + next_url, + } + url = self.AUTH_URL + "?" + urllib.parse.urlencode(params) + return url + + def get_access_token_by_code(self, code): + params = { + 'grant_type': 'authorization_code', + 'client_id': self.client_id, + 'client_secret': self.client_secret, + 'code': code, + 'redirect_uri': self.callback_url + } + rsp = self.do_get(self.TOKEN_URL, params) + if rsp: + d = urllib.parse.parse_qs(rsp) + if 'access_token' in d: + token = d['access_token'] + self.access_token = token[0] + return token + else: + raise OAuthAccessTokenException(rsp) + + def get_open_id(self): + if self.is_access_token_set: + params = { + 'access_token': self.access_token + } + rsp = self.do_get(self.OPEN_ID_URL, params) + if rsp: + rsp = rsp.replace( + 'callback(', '').replace( + ')', '').replace( + ';', '') + obj = json.loads(rsp) + openid = str(obj['openid']) + self.openid = openid + return openid + + def get_oauth_userinfo(self): + openid = self.get_open_id() + if openid: + params = { + 'access_token': self.access_token, + 'oauth_consumer_key': self.client_id, + 'openid': self.openid + } + rsp = self.do_get(self.API_URL, params) + logger.info(rsp) + obj = json.loads(rsp) + user = OAuthUser() + user.nickname = obj['nickname'] + user.openid = openid + user.type = 'qq' + user.token = self.access_token + user.metadata = rsp + if 'email' in obj: + user.email = obj['email'] + if 'figureurl' in obj: + user.picture = str(obj['figureurl']) + return user + + def get_picture(self, metadata): + datas = json.loads(metadata) + return str(datas['figureurl']) + + +@cache_decorator(expiration=100 * 60) +def get_oauth_apps(): + configs = OAuthConfig.objects.filter(is_enable=True).all() + if not configs: + return [] + configtypes = [x.type for x in configs] + applications = BaseOauthManager.__subclasses__() + apps = [x() for x in applications if x().ICON_NAME.lower() in configtypes] + return apps + + +def get_manager_by_type(type): + applications = get_oauth_apps() + if applications: + finds = list( + filter( + lambda x: x.ICON_NAME.lower() == type.lower(), + applications)) + if finds: + return finds[0] + return None diff --git a/DjangoBlog-master/oauth/templatetags/__init__.py b/DjangoBlog-master/oauth/templatetags/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/DjangoBlog-master/oauth/templatetags/__init__.py @@ -0,0 +1 @@ + diff --git a/DjangoBlog-master/oauth/templatetags/oauth_tags.py b/DjangoBlog-master/oauth/templatetags/oauth_tags.py new file mode 100644 index 0000000..7b687d5 --- /dev/null +++ b/DjangoBlog-master/oauth/templatetags/oauth_tags.py @@ -0,0 +1,22 @@ +from django import template +from django.urls import reverse + +from oauth.oauthmanager import get_oauth_apps + +register = template.Library() + + +@register.inclusion_tag('oauth/oauth_applications.html') +def load_oauth_applications(request): + applications = get_oauth_apps() + if applications: + baseurl = reverse('oauth:oauthlogin') + path = request.get_full_path() + + apps = list(map(lambda x: (x.ICON_NAME, '{baseurl}?type={type}&next_url={next}'.format( + baseurl=baseurl, type=x.ICON_NAME, next=path)), applications)) + else: + apps = [] + return { + 'apps': apps + } diff --git a/DjangoBlog-master/oauth/tests.py b/DjangoBlog-master/oauth/tests.py new file mode 100644 index 0000000..bb23b9b --- /dev/null +++ b/DjangoBlog-master/oauth/tests.py @@ -0,0 +1,249 @@ +import json +from unittest.mock import patch + +from django.conf import settings +from django.contrib import auth +from django.test import Client, RequestFactory, TestCase +from django.urls import reverse + +from djangoblog.utils import get_sha256 +from oauth.models import OAuthConfig +from oauth.oauthmanager import BaseOauthManager + + +# Create your tests here. +class OAuthConfigTest(TestCase): + def setUp(self): + self.client = Client() + self.factory = RequestFactory() + + def test_oauth_login_test(self): + c = OAuthConfig() + c.type = 'weibo' + c.appkey = 'appkey' + c.appsecret = 'appsecret' + c.save() + + response = self.client.get('/oauth/oauthlogin?type=weibo') + self.assertEqual(response.status_code, 302) + self.assertTrue("api.weibo.com" in response.url) + + response = self.client.get('/oauth/authorize?type=weibo&code=code') + self.assertEqual(response.status_code, 302) + self.assertEqual(response.url, '/') + + +class OauthLoginTest(TestCase): + def setUp(self) -> None: + self.client = Client() + self.factory = RequestFactory() + self.apps = self.init_apps() + + def init_apps(self): + applications = [p() for p in BaseOauthManager.__subclasses__()] + for application in applications: + c = OAuthConfig() + c.type = application.ICON_NAME.lower() + c.appkey = 'appkey' + c.appsecret = 'appsecret' + c.save() + return applications + + def get_app_by_type(self, type): + for app in self.apps: + if app.ICON_NAME.lower() == type: + return app + + @patch("oauth.oauthmanager.WBOauthManager.do_post") + @patch("oauth.oauthmanager.WBOauthManager.do_get") + def test_weibo_login(self, mock_do_get, mock_do_post): + weibo_app = self.get_app_by_type('weibo') + assert weibo_app + url = weibo_app.get_authorization_url() + mock_do_post.return_value = json.dumps({"access_token": "access_token", + "uid": "uid" + }) + mock_do_get.return_value = json.dumps({ + "avatar_large": "avatar_large", + "screen_name": "screen_name", + "id": "id", + "email": "email", + }) + userinfo = weibo_app.get_access_token_by_code('code') + self.assertEqual(userinfo.token, 'access_token') + self.assertEqual(userinfo.openid, 'id') + + @patch("oauth.oauthmanager.GoogleOauthManager.do_post") + @patch("oauth.oauthmanager.GoogleOauthManager.do_get") + def test_google_login(self, mock_do_get, mock_do_post): + google_app = self.get_app_by_type('google') + assert google_app + url = google_app.get_authorization_url() + mock_do_post.return_value = json.dumps({ + "access_token": "access_token", + "id_token": "id_token", + }) + mock_do_get.return_value = json.dumps({ + "picture": "picture", + "name": "name", + "sub": "sub", + "email": "email", + }) + token = google_app.get_access_token_by_code('code') + userinfo = google_app.get_oauth_userinfo() + self.assertEqual(userinfo.token, 'access_token') + self.assertEqual(userinfo.openid, 'sub') + + @patch("oauth.oauthmanager.GitHubOauthManager.do_post") + @patch("oauth.oauthmanager.GitHubOauthManager.do_get") + def test_github_login(self, mock_do_get, mock_do_post): + github_app = self.get_app_by_type('github') + assert github_app + url = github_app.get_authorization_url() + self.assertTrue("github.com" in url) + self.assertTrue("client_id" in url) + mock_do_post.return_value = "access_token=gho_16C7e42F292c6912E7710c838347Ae178B4a&scope=repo%2Cgist&token_type=bearer" + mock_do_get.return_value = json.dumps({ + "avatar_url": "avatar_url", + "name": "name", + "id": "id", + "email": "email", + }) + token = github_app.get_access_token_by_code('code') + userinfo = github_app.get_oauth_userinfo() + self.assertEqual(userinfo.token, 'gho_16C7e42F292c6912E7710c838347Ae178B4a') + self.assertEqual(userinfo.openid, 'id') + + @patch("oauth.oauthmanager.FaceBookOauthManager.do_post") + @patch("oauth.oauthmanager.FaceBookOauthManager.do_get") + def test_facebook_login(self, mock_do_get, mock_do_post): + facebook_app = self.get_app_by_type('facebook') + assert facebook_app + url = facebook_app.get_authorization_url() + self.assertTrue("facebook.com" in url) + mock_do_post.return_value = json.dumps({ + "access_token": "access_token", + }) + mock_do_get.return_value = json.dumps({ + "name": "name", + "id": "id", + "email": "email", + "picture": { + "data": { + "url": "url" + } + } + }) + token = facebook_app.get_access_token_by_code('code') + userinfo = facebook_app.get_oauth_userinfo() + self.assertEqual(userinfo.token, 'access_token') + + @patch("oauth.oauthmanager.QQOauthManager.do_get", side_effect=[ + 'access_token=access_token&expires_in=3600', + 'callback({"client_id":"appid","openid":"openid"} );', + json.dumps({ + "nickname": "nickname", + "email": "email", + "figureurl": "figureurl", + "openid": "openid", + }) + ]) + def test_qq_login(self, mock_do_get): + qq_app = self.get_app_by_type('qq') + assert qq_app + url = qq_app.get_authorization_url() + self.assertTrue("qq.com" in url) + token = qq_app.get_access_token_by_code('code') + userinfo = qq_app.get_oauth_userinfo() + self.assertEqual(userinfo.token, 'access_token') + + @patch("oauth.oauthmanager.WBOauthManager.do_post") + @patch("oauth.oauthmanager.WBOauthManager.do_get") + def test_weibo_authoriz_login_with_email(self, mock_do_get, mock_do_post): + + mock_do_post.return_value = json.dumps({"access_token": "access_token", + "uid": "uid" + }) + mock_user_info = { + "avatar_large": "avatar_large", + "screen_name": "screen_name1", + "id": "id", + "email": "email", + } + mock_do_get.return_value = json.dumps(mock_user_info) + + response = self.client.get('/oauth/oauthlogin?type=weibo') + self.assertEqual(response.status_code, 302) + self.assertTrue("api.weibo.com" in response.url) + + response = self.client.get('/oauth/authorize?type=weibo&code=code') + self.assertEqual(response.status_code, 302) + self.assertEqual(response.url, '/') + + user = auth.get_user(self.client) + assert user.is_authenticated + self.assertTrue(user.is_authenticated) + self.assertEqual(user.username, mock_user_info['screen_name']) + self.assertEqual(user.email, mock_user_info['email']) + self.client.logout() + + response = self.client.get('/oauth/authorize?type=weibo&code=code') + self.assertEqual(response.status_code, 302) + self.assertEqual(response.url, '/') + + user = auth.get_user(self.client) + assert user.is_authenticated + self.assertTrue(user.is_authenticated) + self.assertEqual(user.username, mock_user_info['screen_name']) + self.assertEqual(user.email, mock_user_info['email']) + + @patch("oauth.oauthmanager.WBOauthManager.do_post") + @patch("oauth.oauthmanager.WBOauthManager.do_get") + def test_weibo_authoriz_login_without_email(self, mock_do_get, mock_do_post): + + mock_do_post.return_value = json.dumps({"access_token": "access_token", + "uid": "uid" + }) + mock_user_info = { + "avatar_large": "avatar_large", + "screen_name": "screen_name1", + "id": "id", + } + mock_do_get.return_value = json.dumps(mock_user_info) + + response = self.client.get('/oauth/oauthlogin?type=weibo') + self.assertEqual(response.status_code, 302) + self.assertTrue("api.weibo.com" in response.url) + + response = self.client.get('/oauth/authorize?type=weibo&code=code') + + self.assertEqual(response.status_code, 302) + + oauth_user_id = int(response.url.split('/')[-1].split('.')[0]) + self.assertEqual(response.url, f'/oauth/requireemail/{oauth_user_id}.html') + + response = self.client.post(response.url, {'email': 'test@gmail.com', 'oauthid': oauth_user_id}) + + self.assertEqual(response.status_code, 302) + sign = get_sha256(settings.SECRET_KEY + + str(oauth_user_id) + settings.SECRET_KEY) + + url = reverse('oauth:bindsuccess', kwargs={ + 'oauthid': oauth_user_id, + }) + self.assertEqual(response.url, f'{url}?type=email') + + path = reverse('oauth:email_confirm', kwargs={ + 'id': oauth_user_id, + 'sign': sign + }) + response = self.client.get(path) + self.assertEqual(response.status_code, 302) + self.assertEqual(response.url, f'/oauth/bindsuccess/{oauth_user_id}.html?type=success') + user = auth.get_user(self.client) + from oauth.models import OAuthUser + oauth_user = OAuthUser.objects.get(author=user) + self.assertTrue(user.is_authenticated) + self.assertEqual(user.username, mock_user_info['screen_name']) + self.assertEqual(user.email, 'test@gmail.com') + self.assertEqual(oauth_user.pk, oauth_user_id) diff --git a/DjangoBlog-master/oauth/urls.py b/DjangoBlog-master/oauth/urls.py new file mode 100644 index 0000000..c4a12a0 --- /dev/null +++ b/DjangoBlog-master/oauth/urls.py @@ -0,0 +1,25 @@ +from django.urls import path + +from . import views + +app_name = "oauth" +urlpatterns = [ + path( + r'oauth/authorize', + views.authorize), + path( + r'oauth/requireemail/.html', + views.RequireEmailView.as_view(), + name='require_email'), + path( + r'oauth/emailconfirm//.html', + views.emailconfirm, + name='email_confirm'), + path( + r'oauth/bindsuccess/.html', + views.bindsuccess, + name='bindsuccess'), + path( + r'oauth/oauthlogin', + views.oauthlogin, + name='oauthlogin')] diff --git a/DjangoBlog-master/oauth/views.py b/DjangoBlog-master/oauth/views.py new file mode 100644 index 0000000..12e3a6e --- /dev/null +++ b/DjangoBlog-master/oauth/views.py @@ -0,0 +1,253 @@ +import logging +# Create your views here. +from urllib.parse import urlparse + +from django.conf import settings +from django.contrib.auth import get_user_model +from django.contrib.auth import login +from django.core.exceptions import ObjectDoesNotExist +from django.db import transaction +from django.http import HttpResponseForbidden +from django.http import HttpResponseRedirect +from django.shortcuts import get_object_or_404 +from django.shortcuts import render +from django.urls import reverse +from django.utils import timezone +from django.utils.translation import gettext_lazy as _ +from django.views.generic import FormView + +from djangoblog.blog_signals import oauth_user_login_signal +from djangoblog.utils import get_current_site +from djangoblog.utils import send_email, get_sha256 +from oauth.forms import RequireEmailForm +from .models import OAuthUser +from .oauthmanager import get_manager_by_type, OAuthAccessTokenException + +logger = logging.getLogger(__name__) + + +def get_redirecturl(request): + nexturl = request.GET.get('next_url', None) + if not nexturl or nexturl == '/login/' or nexturl == '/login': + nexturl = '/' + return nexturl + p = urlparse(nexturl) + if p.netloc: + site = get_current_site().domain + if not p.netloc.replace('www.', '') == site.replace('www.', ''): + logger.info('非法url:' + nexturl) + return "/" + return nexturl + + +def oauthlogin(request): + type = request.GET.get('type', None) + if not type: + return HttpResponseRedirect('/') + manager = get_manager_by_type(type) + if not manager: + return HttpResponseRedirect('/') + nexturl = get_redirecturl(request) + authorizeurl = manager.get_authorization_url(nexturl) + return HttpResponseRedirect(authorizeurl) + + +def authorize(request): + type = request.GET.get('type', None) + if not type: + return HttpResponseRedirect('/') + manager = get_manager_by_type(type) + if not manager: + return HttpResponseRedirect('/') + code = request.GET.get('code', None) + try: + rsp = manager.get_access_token_by_code(code) + except OAuthAccessTokenException as e: + logger.warning("OAuthAccessTokenException:" + str(e)) + return HttpResponseRedirect('/') + except Exception as e: + logger.error(e) + rsp = None + nexturl = get_redirecturl(request) + if not rsp: + return HttpResponseRedirect(manager.get_authorization_url(nexturl)) + user = manager.get_oauth_userinfo() + if user: + if not user.nickname or not user.nickname.strip(): + user.nickname = "djangoblog" + timezone.now().strftime('%y%m%d%I%M%S') + try: + temp = OAuthUser.objects.get(type=type, openid=user.openid) + temp.picture = user.picture + temp.metadata = user.metadata + temp.nickname = user.nickname + user = temp + except ObjectDoesNotExist: + pass + # facebook的token过长 + if type == 'facebook': + user.token = '' + if user.email: + with transaction.atomic(): + author = None + try: + author = get_user_model().objects.get(id=user.author_id) + except ObjectDoesNotExist: + pass + if not author: + result = get_user_model().objects.get_or_create(email=user.email) + author = result[0] + if result[1]: + try: + get_user_model().objects.get(username=user.nickname) + except ObjectDoesNotExist: + author.username = user.nickname + else: + author.username = "djangoblog" + timezone.now().strftime('%y%m%d%I%M%S') + author.source = 'authorize' + author.save() + + user.author = author + user.save() + + oauth_user_login_signal.send( + sender=authorize.__class__, id=user.id) + login(request, author) + return HttpResponseRedirect(nexturl) + else: + user.save() + url = reverse('oauth:require_email', kwargs={ + 'oauthid': user.id + }) + + return HttpResponseRedirect(url) + else: + return HttpResponseRedirect(nexturl) + + +def emailconfirm(request, id, sign): + if not sign: + return HttpResponseForbidden() + if not get_sha256(settings.SECRET_KEY + + str(id) + + settings.SECRET_KEY).upper() == sign.upper(): + return HttpResponseForbidden() + oauthuser = get_object_or_404(OAuthUser, pk=id) + with transaction.atomic(): + if oauthuser.author: + author = get_user_model().objects.get(pk=oauthuser.author_id) + else: + result = get_user_model().objects.get_or_create(email=oauthuser.email) + author = result[0] + if result[1]: + author.source = 'emailconfirm' + author.username = oauthuser.nickname.strip() if oauthuser.nickname.strip( + ) else "djangoblog" + timezone.now().strftime('%y%m%d%I%M%S') + author.save() + oauthuser.author = author + oauthuser.save() + oauth_user_login_signal.send( + sender=emailconfirm.__class__, + id=oauthuser.id) + login(request, author) + + site = 'http://' + get_current_site().domain + content = _(''' +

Congratulations, you have successfully bound your email address. You can use + %(oauthuser_type)s to directly log in to this website without a password.

+ You are welcome to continue to follow this site, the address is + %(site)s + Thank you again! +
+ If the link above cannot be opened, please copy this link to your browser. + %(site)s + ''') % {'oauthuser_type': oauthuser.type, 'site': site} + + send_email(emailto=[oauthuser.email, ], title=_('Congratulations on your successful binding!'), content=content) + url = reverse('oauth:bindsuccess', kwargs={ + 'oauthid': id + }) + url = url + '?type=success' + return HttpResponseRedirect(url) + + +class RequireEmailView(FormView): + form_class = RequireEmailForm + template_name = 'oauth/require_email.html' + + def get(self, request, *args, **kwargs): + oauthid = self.kwargs['oauthid'] + oauthuser = get_object_or_404(OAuthUser, pk=oauthid) + if oauthuser.email: + pass + # return HttpResponseRedirect('/') + + return super(RequireEmailView, self).get(request, *args, **kwargs) + + def get_initial(self): + oauthid = self.kwargs['oauthid'] + return { + 'email': '', + 'oauthid': oauthid + } + + def get_context_data(self, **kwargs): + oauthid = self.kwargs['oauthid'] + oauthuser = get_object_or_404(OAuthUser, pk=oauthid) + if oauthuser.picture: + kwargs['picture'] = oauthuser.picture + return super(RequireEmailView, self).get_context_data(**kwargs) + + def form_valid(self, form): + email = form.cleaned_data['email'] + oauthid = form.cleaned_data['oauthid'] + oauthuser = get_object_or_404(OAuthUser, pk=oauthid) + oauthuser.email = email + oauthuser.save() + sign = get_sha256(settings.SECRET_KEY + + str(oauthuser.id) + settings.SECRET_KEY) + site = get_current_site().domain + if settings.DEBUG: + site = '127.0.0.1:8000' + path = reverse('oauth:email_confirm', kwargs={ + 'id': oauthid, + 'sign': sign + }) + url = "http://{site}{path}".format(site=site, path=path) + + content = _(""" +

Please click the link below to bind your email

+ + %(url)s + + Thank you again! +
+ If the link above cannot be opened, please copy this link to your browser. +
+ %(url)s + """) % {'url': url} + send_email(emailto=[email, ], title=_('Bind your email'), content=content) + url = reverse('oauth:bindsuccess', kwargs={ + 'oauthid': oauthid + }) + url = url + '?type=email' + return HttpResponseRedirect(url) + + +def bindsuccess(request, oauthid): + type = request.GET.get('type', None) + oauthuser = get_object_or_404(OAuthUser, pk=oauthid) + if type == 'email': + title = _('Bind your email') + content = _( + 'Congratulations, the binding is just one step away. ' + 'Please log in to your email to check the email to complete the binding. Thank you.') + else: + title = _('Binding successful') + content = _( + "Congratulations, you have successfully bound your email address. You can use %(oauthuser_type)s" + " to directly log in to this website without a password. You are welcome to continue to follow this site." % { + 'oauthuser_type': oauthuser.type}) + return render(request, 'oauth/bindsuccess.html', { + 'title': title, + 'content': content + }) diff --git a/DjangoBlog-master/owntracks/__init__.py b/DjangoBlog-master/owntracks/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/DjangoBlog-master/owntracks/admin.py b/DjangoBlog-master/owntracks/admin.py new file mode 100644 index 0000000..655b535 --- /dev/null +++ b/DjangoBlog-master/owntracks/admin.py @@ -0,0 +1,7 @@ +from django.contrib import admin + +# Register your models here. + + +class OwnTrackLogsAdmin(admin.ModelAdmin): + pass diff --git a/DjangoBlog-master/owntracks/apps.py b/DjangoBlog-master/owntracks/apps.py new file mode 100644 index 0000000..1bc5f12 --- /dev/null +++ b/DjangoBlog-master/owntracks/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class OwntracksConfig(AppConfig): + name = 'owntracks' diff --git a/DjangoBlog-master/owntracks/migrations/0001_initial.py b/DjangoBlog-master/owntracks/migrations/0001_initial.py new file mode 100644 index 0000000..9eee55c --- /dev/null +++ b/DjangoBlog-master/owntracks/migrations/0001_initial.py @@ -0,0 +1,31 @@ +# Generated by Django 4.1.7 on 2023-03-02 07:14 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='OwnTrackLog', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('tid', models.CharField(max_length=100, verbose_name='用户')), + ('lat', models.FloatField(verbose_name='纬度')), + ('lon', models.FloatField(verbose_name='经度')), + ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')), + ], + options={ + 'verbose_name': 'OwnTrackLogs', + 'verbose_name_plural': 'OwnTrackLogs', + 'ordering': ['created_time'], + 'get_latest_by': 'created_time', + }, + ), + ] diff --git a/DjangoBlog-master/owntracks/migrations/0002_alter_owntracklog_options_and_more.py b/DjangoBlog-master/owntracks/migrations/0002_alter_owntracklog_options_and_more.py new file mode 100644 index 0000000..b4f8dec --- /dev/null +++ b/DjangoBlog-master/owntracks/migrations/0002_alter_owntracklog_options_and_more.py @@ -0,0 +1,22 @@ +# Generated by Django 4.2.5 on 2023-09-06 13:19 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('owntracks', '0001_initial'), + ] + + operations = [ + migrations.AlterModelOptions( + name='owntracklog', + options={'get_latest_by': 'creation_time', 'ordering': ['creation_time'], 'verbose_name': 'OwnTrackLogs', 'verbose_name_plural': 'OwnTrackLogs'}, + ), + migrations.RenameField( + model_name='owntracklog', + old_name='created_time', + new_name='creation_time', + ), + ] diff --git a/DjangoBlog-master/owntracks/migrations/__init__.py b/DjangoBlog-master/owntracks/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/DjangoBlog-master/owntracks/models.py b/DjangoBlog-master/owntracks/models.py new file mode 100644 index 0000000..760942c --- /dev/null +++ b/DjangoBlog-master/owntracks/models.py @@ -0,0 +1,20 @@ +from django.db import models +from django.utils.timezone import now + + +# Create your models here. + +class OwnTrackLog(models.Model): + tid = models.CharField(max_length=100, null=False, verbose_name='用户') + lat = models.FloatField(verbose_name='纬度') + lon = models.FloatField(verbose_name='经度') + creation_time = models.DateTimeField('创建时间', default=now) + + def __str__(self): + return self.tid + + class Meta: + ordering = ['creation_time'] + verbose_name = "OwnTrackLogs" + verbose_name_plural = verbose_name + get_latest_by = 'creation_time' diff --git a/DjangoBlog-master/owntracks/tests.py b/DjangoBlog-master/owntracks/tests.py new file mode 100644 index 0000000..3b4b9d8 --- /dev/null +++ b/DjangoBlog-master/owntracks/tests.py @@ -0,0 +1,64 @@ +import json + +from django.test import Client, RequestFactory, TestCase + +from accounts.models import BlogUser +from .models import OwnTrackLog + + +# Create your tests here. + +class OwnTrackLogTest(TestCase): + def setUp(self): + self.client = Client() + self.factory = RequestFactory() + + def test_own_track_log(self): + o = { + 'tid': 12, + 'lat': 123.123, + 'lon': 134.341 + } + + self.client.post( + '/owntracks/logtracks', + json.dumps(o), + content_type='application/json') + length = len(OwnTrackLog.objects.all()) + self.assertEqual(length, 1) + + o = { + 'tid': 12, + 'lat': 123.123 + } + + self.client.post( + '/owntracks/logtracks', + json.dumps(o), + content_type='application/json') + length = len(OwnTrackLog.objects.all()) + self.assertEqual(length, 1) + + rsp = self.client.get('/owntracks/show_maps') + self.assertEqual(rsp.status_code, 302) + + user = BlogUser.objects.create_superuser( + email="liangliangyy1@gmail.com", + username="liangliangyy1", + password="liangliangyy1") + + self.client.login(username='liangliangyy1', password='liangliangyy1') + s = OwnTrackLog() + s.tid = 12 + s.lon = 123.234 + s.lat = 34.234 + s.save() + + rsp = self.client.get('/owntracks/show_dates') + self.assertEqual(rsp.status_code, 200) + rsp = self.client.get('/owntracks/show_maps') + self.assertEqual(rsp.status_code, 200) + rsp = self.client.get('/owntracks/get_datas') + self.assertEqual(rsp.status_code, 200) + rsp = self.client.get('/owntracks/get_datas?date=2018-02-26') + self.assertEqual(rsp.status_code, 200) diff --git a/DjangoBlog-master/owntracks/urls.py b/DjangoBlog-master/owntracks/urls.py new file mode 100644 index 0000000..c19ada8 --- /dev/null +++ b/DjangoBlog-master/owntracks/urls.py @@ -0,0 +1,12 @@ +from django.urls import path + +from . import views + +app_name = "owntracks" + +urlpatterns = [ + path('owntracks/logtracks', views.manage_owntrack_log, name='logtracks'), + path('owntracks/show_maps', views.show_maps, name='show_maps'), + path('owntracks/get_datas', views.get_datas, name='get_datas'), + path('owntracks/show_dates', views.show_log_dates, name='show_dates') +] diff --git a/DjangoBlog-master/owntracks/views.py b/DjangoBlog-master/owntracks/views.py new file mode 100644 index 0000000..4c72bdd --- /dev/null +++ b/DjangoBlog-master/owntracks/views.py @@ -0,0 +1,127 @@ +# Create your views here. +import datetime +import itertools +import json +import logging +from datetime import timezone +from itertools import groupby + +import django +import requests +from django.contrib.auth.decorators import login_required +from django.http import HttpResponse +from django.http import JsonResponse +from django.shortcuts import render +from django.views.decorators.csrf import csrf_exempt + +from .models import OwnTrackLog + +logger = logging.getLogger(__name__) + + +@csrf_exempt +def manage_owntrack_log(request): + try: + s = json.loads(request.read().decode('utf-8')) + tid = s['tid'] + lat = s['lat'] + lon = s['lon'] + + logger.info( + 'tid:{tid}.lat:{lat}.lon:{lon}'.format( + tid=tid, lat=lat, lon=lon)) + if tid and lat and lon: + m = OwnTrackLog() + m.tid = tid + m.lat = lat + m.lon = lon + m.save() + return HttpResponse('ok') + else: + return HttpResponse('data error') + except Exception as e: + logger.error(e) + return HttpResponse('error') + + +@login_required +def show_maps(request): + if request.user.is_superuser: + defaultdate = str(datetime.datetime.now(timezone.utc).date()) + date = request.GET.get('date', defaultdate) + context = { + 'date': date + } + return render(request, 'owntracks/show_maps.html', context) + else: + from django.http import HttpResponseForbidden + return HttpResponseForbidden() + + +@login_required +def show_log_dates(request): + dates = OwnTrackLog.objects.values_list('creation_time', flat=True) + results = list(sorted(set(map(lambda x: x.strftime('%Y-%m-%d'), dates)))) + + context = { + 'results': results + } + return render(request, 'owntracks/show_log_dates.html', context) + + +def convert_to_amap(locations): + convert_result = [] + it = iter(locations) + + item = list(itertools.islice(it, 30)) + while item: + datas = ';'.join( + set(map(lambda x: str(x.lon) + ',' + str(x.lat), item))) + + key = '8440a376dfc9743d8924bf0ad141f28e' + api = 'http://restapi.amap.com/v3/assistant/coordinate/convert' + query = { + 'key': key, + 'locations': datas, + 'coordsys': 'gps' + } + rsp = requests.get(url=api, params=query) + result = json.loads(rsp.text) + if "locations" in result: + convert_result.append(result['locations']) + item = list(itertools.islice(it, 30)) + + return ";".join(convert_result) + + +@login_required +def get_datas(request): + now = django.utils.timezone.now().replace(tzinfo=timezone.utc) + querydate = django.utils.timezone.datetime( + now.year, now.month, now.day, 0, 0, 0) + if request.GET.get('date', None): + 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) + nextdate = querydate + datetime.timedelta(days=1) + models = OwnTrackLog.objects.filter( + creation_time__range=(querydate, nextdate)) + result = list() + if models and len(models): + for tid, item in groupby( + sorted(models, key=lambda k: k.tid), key=lambda k: k.tid): + + d = dict() + 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(',')) + # 使用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) + return JsonResponse(result, safe=False) diff --git a/DjangoBlog-master/plugins/__init__.py b/DjangoBlog-master/plugins/__init__.py new file mode 100644 index 0000000..e88afca --- /dev/null +++ b/DjangoBlog-master/plugins/__init__.py @@ -0,0 +1 @@ +# This file makes this a Python package diff --git a/DjangoBlog-master/plugins/article_copyright/__init__.py b/DjangoBlog-master/plugins/article_copyright/__init__.py new file mode 100644 index 0000000..e88afca --- /dev/null +++ b/DjangoBlog-master/plugins/article_copyright/__init__.py @@ -0,0 +1 @@ +# This file makes this a Python package diff --git a/DjangoBlog-master/plugins/article_copyright/plugin.py b/DjangoBlog-master/plugins/article_copyright/plugin.py new file mode 100644 index 0000000..317fed2 --- /dev/null +++ b/DjangoBlog-master/plugins/article_copyright/plugin.py @@ -0,0 +1,32 @@ +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 ArticleCopyrightPlugin(BasePlugin): + PLUGIN_NAME = '文章结尾版权声明' + PLUGIN_DESCRIPTION = '一个在文章正文末尾添加版权声明的插件。' + PLUGIN_VERSION = '0.2.0' + PLUGIN_AUTHOR = 'liangliangyy' + + # 2. 实现 register_hooks 方法,专门用于注册钩子 + def register_hooks(self): + # 在这里将插件的方法注册到指定的钩子上 + hooks.register(ARTICLE_CONTENT_HOOK_NAME, self.add_copyright_to_content) + + def add_copyright_to_content(self, content, *args, **kwargs): + """ + 这个方法会被注册到 'the_content' 过滤器钩子上。 + 它接收原始内容,并返回添加了版权信息的新内容。 + """ + article = kwargs.get('article') + if not article: + return content + + copyright_info = f"\n

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

" + return content + copyright_info + + +# 3. 实例化插件。 +# 这会自动调用 BasePlugin.__init__,然后 BasePlugin.__init__ 会调用我们上面定义的 register_hooks 方法。 +plugin = ArticleCopyrightPlugin() diff --git a/DjangoBlog-master/plugins/external_links/__init__.py b/DjangoBlog-master/plugins/external_links/__init__.py new file mode 100644 index 0000000..e88afca --- /dev/null +++ b/DjangoBlog-master/plugins/external_links/__init__.py @@ -0,0 +1 @@ +# This file makes this a Python package diff --git a/DjangoBlog-master/plugins/external_links/plugin.py b/DjangoBlog-master/plugins/external_links/plugin.py new file mode 100644 index 0000000..5b2ef14 --- /dev/null +++ b/DjangoBlog-master/plugins/external_links/plugin.py @@ -0,0 +1,48 @@ +import re +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 ExternalLinksPlugin(BasePlugin): + PLUGIN_NAME = '外部链接处理器' + PLUGIN_DESCRIPTION = '自动为文章中的外部链接添加 target="_blank" 和 rel="noopener noreferrer" 属性。' + PLUGIN_VERSION = '0.1.0' + PLUGIN_AUTHOR = 'liangliangyy' + + def register_hooks(self): + hooks.register(ARTICLE_CONTENT_HOOK_NAME, self.process_external_links) + + def process_external_links(self, content, *args, **kwargs): + from djangoblog.utils import get_current_site + site_domain = get_current_site().domain + + # 正则表达式查找所有 标签 + link_pattern = re.compile(r'(]*?\s+)?href=")([^"]*)(".*?/a>)', re.IGNORECASE) + + def replacer(match): + # match.group(1) 是 ... + href = match.group(2) + + # 如果链接已经有 target 属性,则不处理 + if 'target=' in match.group(0).lower(): + return match.group(0) + + # 解析链接 + parsed_url = urlparse(href) + + # 如果链接是外部的 (有域名且域名不等于当前网站域名) + if parsed_url.netloc and parsed_url.netloc != site_domain: + # 添加 target 和 rel 属性 + return f'{match.group(1)}{href}" target="_blank" rel="noopener noreferrer"{match.group(3)}' + + # 否则返回原样 + return match.group(0) + + return link_pattern.sub(replacer, content) + + +plugin = ExternalLinksPlugin() diff --git a/DjangoBlog-master/plugins/reading_time/__init__.py b/DjangoBlog-master/plugins/reading_time/__init__.py new file mode 100644 index 0000000..e88afca --- /dev/null +++ b/DjangoBlog-master/plugins/reading_time/__init__.py @@ -0,0 +1 @@ +# This file makes this a Python package diff --git a/DjangoBlog-master/plugins/reading_time/plugin.py b/DjangoBlog-master/plugins/reading_time/plugin.py new file mode 100644 index 0000000..35f9db1 --- /dev/null +++ b/DjangoBlog-master/plugins/reading_time/plugin.py @@ -0,0 +1,43 @@ +import math +import re +from djangoblog.plugin_manage.base_plugin import BasePlugin +from djangoblog.plugin_manage import hooks +from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME + + +class ReadingTimePlugin(BasePlugin): + PLUGIN_NAME = '阅读时间预测' + PLUGIN_DESCRIPTION = '估算文章阅读时间并显示在文章开头。' + PLUGIN_VERSION = '0.1.0' + PLUGIN_AUTHOR = 'liangliangyy' + + def register_hooks(self): + hooks.register(ARTICLE_CONTENT_HOOK_NAME, self.add_reading_time) + + def add_reading_time(self, content, *args, **kwargs): + """ + 计算阅读时间并添加到内容开头。 + """ + # 移除HTML标签和空白字符,以获得纯文本 + clean_content = re.sub(r'<[^>]*>', '', content) + clean_content = clean_content.strip() + + # 中文和英文单词混合计数的一个简单方法 + # 匹配中文字符或连续的非中文字符(视为单词) + words = re.findall(r'[\u4e00-\u9fa5]|\w+', clean_content) + word_count = len(words) + + # 按平均每分钟200字的速度计算 + reading_speed = 200 + reading_minutes = math.ceil(word_count / reading_speed) + + # 如果阅读时间少于1分钟,则显示为1分钟 + if reading_minutes < 1: + reading_minutes = 1 + + reading_time_html = f'

预计阅读时间:{reading_minutes} 分钟

' + + return reading_time_html + content + + +plugin = ReadingTimePlugin() \ No newline at end of file diff --git a/DjangoBlog-master/plugins/seo_optimizer/__init__.py b/DjangoBlog-master/plugins/seo_optimizer/__init__.py new file mode 100644 index 0000000..e88afca --- /dev/null +++ b/DjangoBlog-master/plugins/seo_optimizer/__init__.py @@ -0,0 +1 @@ +# This file makes this a Python package diff --git a/DjangoBlog-master/plugins/seo_optimizer/plugin.py b/DjangoBlog-master/plugins/seo_optimizer/plugin.py new file mode 100644 index 0000000..b5b19a3 --- /dev/null +++ b/DjangoBlog-master/plugins/seo_optimizer/plugin.py @@ -0,0 +1,142 @@ +import json +from django.utils.html import strip_tags +from django.template.defaultfilters import truncatewords +from djangoblog.plugin_manage.base_plugin import BasePlugin +from djangoblog.plugin_manage import hooks +from blog.models import Article, Category, Tag +from djangoblog.utils import get_blog_setting + + +class SeoOptimizerPlugin(BasePlugin): + PLUGIN_NAME = 'SEO 优化器' + PLUGIN_DESCRIPTION = '为文章、页面等提供 SEO 优化,动态生成 meta 标签和 JSON-LD 结构化数据。' + PLUGIN_VERSION = '0.2.0' + PLUGIN_AUTHOR = 'liuangliangyy' + + def register_hooks(self): + hooks.register('head_meta', self.dispatch_seo_generation) + + def _get_article_seo_data(self, context, request, blog_setting): + article = context.get('article') + if not isinstance(article, Article): + return None + + description = strip_tags(article.body)[:150] + keywords = ",".join([tag.name for tag in article.tags.all()]) or blog_setting.site_keywords + + meta_tags = f''' + + + + + + + + + ''' + for tag in article.tags.all(): + meta_tags += f'' + meta_tags += f'' + + structured_data = { + "@context": "https://schema.org", + "@type": "Article", + "mainEntityOfPage": {"@type": "WebPage", "@id": request.build_absolute_uri()}, + "headline": article.title, + "description": description, + "image": request.build_absolute_uri(article.get_first_image_url()), + "datePublished": article.pub_time.isoformat(), + "dateModified": article.last_modify_time.isoformat(), + "author": {"@type": "Person", "name": article.author.username}, + "publisher": {"@type": "Organization", "name": blog_setting.site_name} + } + if not structured_data.get("image"): + del structured_data["image"] + + return { + "title": f"{article.title} | {blog_setting.site_name}", + "description": description, + "keywords": keywords, + "meta_tags": meta_tags, + "json_ld": structured_data + } + + def _get_category_seo_data(self, context, request, blog_setting): + category_name = context.get('tag_name') + if not category_name: + return None + + category = Category.objects.filter(name=category_name).first() + if not category: + return None + + title = f"{category.name} | {blog_setting.site_name}" + description = strip_tags(category.name) or blog_setting.site_description + keywords = category.name + + # BreadcrumbList structured data for category page + breadcrumb_items = [{"@type": "ListItem", "position": 1, "name": "首页", "item": request.build_absolute_uri('/')}] + breadcrumb_items.append({"@type": "ListItem", "position": 2, "name": category.name, "item": request.build_absolute_uri()}) + + structured_data = { + "@context": "https://schema.org", + "@type": "BreadcrumbList", + "itemListElement": breadcrumb_items + } + + return { + "title": title, + "description": description, + "keywords": keywords, + "meta_tags": "", + "json_ld": structured_data + } + + def _get_default_seo_data(self, context, request, blog_setting): + # Homepage and other default pages + structured_data = { + "@context": "https://schema.org", + "@type": "WebSite", + "url": request.build_absolute_uri('/'), + "potentialAction": { + "@type": "SearchAction", + "target": f"{request.build_absolute_uri('/search/')}?q={{search_term_string}}", + "query-input": "required name=search_term_string" + } + } + return { + "title": f"{blog_setting.site_name} | {blog_setting.site_description}", + "description": blog_setting.site_description, + "keywords": blog_setting.site_keywords, + "meta_tags": "", + "json_ld": structured_data + } + + def dispatch_seo_generation(self, metas, context): + request = context.get('request') + if not request: + return metas + + view_name = request.resolver_match.view_name + blog_setting = get_blog_setting() + + seo_data = None + if view_name == 'blog:detailbyid': + seo_data = self._get_article_seo_data(context, request, blog_setting) + elif view_name == 'blog:category_detail': + seo_data = self._get_category_seo_data(context, request, blog_setting) + + if not seo_data: + seo_data = self._get_default_seo_data(context, request, blog_setting) + + json_ld_script = f'' + + return f""" + {seo_data.get("title", "")} + + + {seo_data.get("meta_tags", "")} + {json_ld_script} + """ + +plugin = SeoOptimizerPlugin() diff --git a/DjangoBlog-master/plugins/view_count/__init__.py b/DjangoBlog-master/plugins/view_count/__init__.py new file mode 100644 index 0000000..8804fdf --- /dev/null +++ b/DjangoBlog-master/plugins/view_count/__init__.py @@ -0,0 +1 @@ +# This file makes this a Python package \ No newline at end of file diff --git a/DjangoBlog-master/plugins/view_count/plugin.py b/DjangoBlog-master/plugins/view_count/plugin.py new file mode 100644 index 0000000..15e9d94 --- /dev/null +++ b/DjangoBlog-master/plugins/view_count/plugin.py @@ -0,0 +1,18 @@ +from djangoblog.plugin_manage.base_plugin import BasePlugin +from djangoblog.plugin_manage import hooks + + +class ViewCountPlugin(BasePlugin): + PLUGIN_NAME = '文章浏览次数统计' + PLUGIN_DESCRIPTION = '统计文章的浏览次数' + PLUGIN_VERSION = '0.1.0' + PLUGIN_AUTHOR = 'liangliangyy' + + def register_hooks(self): + hooks.register('after_article_body_get', self.record_view) + + def record_view(self, article, *args, **kwargs): + article.viewed() + + +plugin = ViewCountPlugin() \ No newline at end of file diff --git a/DjangoBlog-master/requirements.txt b/DjangoBlog-master/requirements.txt new file mode 100644 index 0000000..9dc5c93 Binary files /dev/null and b/DjangoBlog-master/requirements.txt differ diff --git a/DjangoBlog-master/servermanager/MemcacheStorage.py b/DjangoBlog-master/servermanager/MemcacheStorage.py new file mode 100644 index 0000000..38a7990 --- /dev/null +++ b/DjangoBlog-master/servermanager/MemcacheStorage.py @@ -0,0 +1,32 @@ +from werobot.session import SessionStorage +from werobot.utils import json_loads, json_dumps + +from djangoblog.utils import cache + + +class MemcacheStorage(SessionStorage): + def __init__(self, prefix='ws_'): + self.prefix = prefix + self.cache = cache + + @property + def is_available(self): + value = "1" + self.set('checkavaliable', value=value) + return value == self.get('checkavaliable') + + def key_name(self, s): + return '{prefix}{s}'.format(prefix=self.prefix, s=s) + + def get(self, id): + id = self.key_name(id) + session_json = self.cache.get(id) or '{}' + return json_loads(session_json) + + def set(self, id, value): + id = self.key_name(id) + self.cache.set(id, json_dumps(value)) + + def delete(self, id): + id = self.key_name(id) + self.cache.delete(id) diff --git a/DjangoBlog-master/servermanager/__init__.py b/DjangoBlog-master/servermanager/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/DjangoBlog-master/servermanager/admin.py b/DjangoBlog-master/servermanager/admin.py new file mode 100644 index 0000000..f26f4f6 --- /dev/null +++ b/DjangoBlog-master/servermanager/admin.py @@ -0,0 +1,19 @@ +from django.contrib import admin +# Register your models here. + + +class CommandsAdmin(admin.ModelAdmin): + list_display = ('title', 'command', 'describe') + + +class EmailSendLogAdmin(admin.ModelAdmin): + list_display = ('title', 'emailto', 'send_result', 'creation_time') + readonly_fields = ( + 'title', + 'emailto', + 'send_result', + 'creation_time', + 'content') + + def has_add_permission(self, request): + return False diff --git a/DjangoBlog-master/servermanager/api/__init__.py b/DjangoBlog-master/servermanager/api/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/DjangoBlog-master/servermanager/api/__init__.py @@ -0,0 +1 @@ + diff --git a/DjangoBlog-master/servermanager/api/blogapi.py b/DjangoBlog-master/servermanager/api/blogapi.py new file mode 100644 index 0000000..8a4d6ac --- /dev/null +++ b/DjangoBlog-master/servermanager/api/blogapi.py @@ -0,0 +1,27 @@ +from haystack.query import SearchQuerySet + +from blog.models import Article, Category + + +class BlogApi: + def __init__(self): + self.searchqueryset = SearchQuerySet() + self.searchqueryset.auto_query('') + self.__max_takecount__ = 8 + + def search_articles(self, query): + sqs = self.searchqueryset.auto_query(query) + sqs = sqs.load_all() + return sqs[:self.__max_takecount__] + + def get_category_lists(self): + return Category.objects.all() + + def get_category_articles(self, categoryname): + articles = Article.objects.filter(category__name=categoryname) + if articles: + return articles[:self.__max_takecount__] + return None + + def get_recent_articles(self): + return Article.objects.all()[:self.__max_takecount__] diff --git a/DjangoBlog-master/servermanager/api/commonapi.py b/DjangoBlog-master/servermanager/api/commonapi.py new file mode 100644 index 0000000..83ad9ff --- /dev/null +++ b/DjangoBlog-master/servermanager/api/commonapi.py @@ -0,0 +1,64 @@ +import logging +import os + +import openai + +from servermanager.models import commands + +logger = logging.getLogger(__name__) + +openai.api_key = os.environ.get('OPENAI_API_KEY') +if os.environ.get('HTTP_PROXY'): + openai.proxy = os.environ.get('HTTP_PROXY') + + +class ChatGPT: + + @staticmethod + def chat(prompt): + try: + completion = openai.ChatCompletion.create(model="gpt-3.5-turbo", + messages=[{"role": "user", "content": prompt}]) + return completion.choices[0].message.content + except Exception as e: + logger.error(e) + return "服务器出错了" + + +class CommandHandler: + def __init__(self): + self.commands = commands.objects.all() + + def run(self, title): + """ + 运行命令 + :param title: 命令 + :return: 返回命令执行结果 + """ + cmd = list( + filter( + lambda x: x.title.upper() == title.upper(), + self.commands)) + if cmd: + return self.__run_command__(cmd[0].command) + else: + return "未找到相关命令,请输入hepme获得帮助。" + + def __run_command__(self, cmd): + try: + res = os.popen(cmd).read() + return res + except BaseException: + return '命令执行出错!' + + def get_help(self): + rsp = '' + for cmd in self.commands: + rsp += '{c}:{d}\n'.format(c=cmd.title, d=cmd.describe) + return rsp + + +if __name__ == '__main__': + chatbot = ChatGPT() + prompt = "写一篇1000字关于AI的论文" + print(chatbot.chat(prompt)) diff --git a/DjangoBlog-master/servermanager/apps.py b/DjangoBlog-master/servermanager/apps.py new file mode 100644 index 0000000..03cc38d --- /dev/null +++ b/DjangoBlog-master/servermanager/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class ServermanagerConfig(AppConfig): + name = 'servermanager' diff --git a/DjangoBlog-master/servermanager/migrations/0001_initial.py b/DjangoBlog-master/servermanager/migrations/0001_initial.py new file mode 100644 index 0000000..bbdbf77 --- /dev/null +++ b/DjangoBlog-master/servermanager/migrations/0001_initial.py @@ -0,0 +1,45 @@ +# Generated by Django 4.1.7 on 2023-03-02 07:14 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='commands', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=300, verbose_name='命令标题')), + ('command', models.CharField(max_length=2000, verbose_name='命令')), + ('describe', models.CharField(max_length=300, verbose_name='命令描述')), + ('created_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('last_mod_time', models.DateTimeField(auto_now=True, verbose_name='修改时间')), + ], + options={ + 'verbose_name': '命令', + 'verbose_name_plural': '命令', + }, + ), + migrations.CreateModel( + name='EmailSendLog', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('emailto', models.CharField(max_length=300, verbose_name='收件人')), + ('title', models.CharField(max_length=2000, verbose_name='邮件标题')), + ('content', models.TextField(verbose_name='邮件内容')), + ('send_result', models.BooleanField(default=False, verbose_name='结果')), + ('created_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ], + options={ + 'verbose_name': '邮件发送log', + 'verbose_name_plural': '邮件发送log', + 'ordering': ['-created_time'], + }, + ), + ] diff --git a/DjangoBlog-master/servermanager/migrations/0002_alter_emailsendlog_options_and_more.py b/DjangoBlog-master/servermanager/migrations/0002_alter_emailsendlog_options_and_more.py new file mode 100644 index 0000000..4858857 --- /dev/null +++ b/DjangoBlog-master/servermanager/migrations/0002_alter_emailsendlog_options_and_more.py @@ -0,0 +1,32 @@ +# Generated by Django 4.2.5 on 2023-09-06 13:19 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('servermanager', '0001_initial'), + ] + + operations = [ + migrations.AlterModelOptions( + name='emailsendlog', + options={'ordering': ['-creation_time'], 'verbose_name': '邮件发送log', 'verbose_name_plural': '邮件发送log'}, + ), + migrations.RenameField( + model_name='commands', + old_name='created_time', + new_name='creation_time', + ), + migrations.RenameField( + model_name='commands', + old_name='last_mod_time', + new_name='last_modify_time', + ), + migrations.RenameField( + model_name='emailsendlog', + old_name='created_time', + new_name='creation_time', + ), + ] diff --git a/DjangoBlog-master/servermanager/migrations/__init__.py b/DjangoBlog-master/servermanager/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/DjangoBlog-master/servermanager/models.py b/DjangoBlog-master/servermanager/models.py new file mode 100644 index 0000000..4326c65 --- /dev/null +++ b/DjangoBlog-master/servermanager/models.py @@ -0,0 +1,33 @@ +from django.db import models + + +# Create your models here. +class commands(models.Model): + title = models.CharField('命令标题', max_length=300) + command = models.CharField('命令', max_length=2000) + describe = models.CharField('命令描述', max_length=300) + creation_time = models.DateTimeField('创建时间', auto_now_add=True) + last_modify_time = models.DateTimeField('修改时间', auto_now=True) + + def __str__(self): + return self.title + + class Meta: + verbose_name = '命令' + verbose_name_plural = verbose_name + + +class EmailSendLog(models.Model): + emailto = models.CharField('收件人', max_length=300) + title = models.CharField('邮件标题', max_length=2000) + content = models.TextField('邮件内容') + send_result = models.BooleanField('结果', default=False) + creation_time = models.DateTimeField('创建时间', auto_now_add=True) + + def __str__(self): + return self.title + + class Meta: + verbose_name = '邮件发送log' + verbose_name_plural = verbose_name + ordering = ['-creation_time'] diff --git a/DjangoBlog-master/servermanager/robot.py b/DjangoBlog-master/servermanager/robot.py new file mode 100644 index 0000000..7b45736 --- /dev/null +++ b/DjangoBlog-master/servermanager/robot.py @@ -0,0 +1,187 @@ +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() +if memstorage.is_available: + robot.config['SESSION_STORAGE'] = memstorage +else: + 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') + +blogapi = BlogApi() +cmd_handler = CommandHandler() +logger = logging.getLogger(__name__) + + +def convert_to_article_reply(articles, message): + reply = ArticlesReply(message=message) + from blog.templatetags.blog_tags import truncatechars_content + for post in articles: + imgs = re.findall(r'(?:http\:|https\:)?\/\/.*\.(?:png|jpg)', post.body) + imgurl = '' + if imgs: + imgurl = imgs[0] + article = Article( + title=post.title, + description=truncatechars_content(post.body), + img=imgurl, + url=post.get_full_url() + ) + reply.add_article(article) + return reply + + +@robot.filter(re.compile(r"^\?.*")) +def search(message, session): + s = message.content + searchstr = str(s).replace('?', '') + result = blogapi.search_articles(searchstr) + if result: + articles = list(map(lambda x: x.object, result)) + reply = convert_to_article_reply(articles, message) + return reply + else: + return '没有找到相关文章。' + + +@robot.filter(re.compile(r'^category\s*$', re.I)) +def category(message, session): + categorys = blogapi.get_category_lists() + content = ','.join(map(lambda x: x.name, categorys)) + return '所有文章分类目录:' + content + + +@robot.filter(re.compile(r'^recent\s*$', re.I)) +def recents(message, session): + articles = blogapi.get_recent_articles() + if articles: + reply = convert_to_article_reply(articles, message) + return reply + else: + return "暂时还没有文章" + + +@robot.filter(re.compile('^help$', re.I)) +def help(message, session): + return '''欢迎关注! + 默认会与图灵机器人聊天~~ + 你可以通过下面这些命令来获得信息 + ?关键字搜索文章. + 如?python. + category获得文章分类目录及文章数. + category-***获得该分类目录文章 + 如category-python + recent获得最新文章 + help获得帮助. + weather:获得天气 + 如weather:西安 + idcard:获得身份证信息 + 如idcard:61048119xxxxxxxxxx + music:音乐搜索 + 如music:阴天快乐 + PS:以上标点符号都不支持中文标点~~ + ''' + + +@robot.filter(re.compile(r'^weather\:.*$', re.I)) +def weather(message, session): + return "建设中..." + + +@robot.filter(re.compile(r'^idcard\:.*$', re.I)) +def idcard(message, session): + return "建设中..." + + +@robot.handler +def echo(message, session): + handler = MessageHandler(message, session) + return handler.handler() + + +class MessageHandler: + def __init__(self, message, session): + userid = message.source + self.message = message + self.session = session + self.userid = userid + try: + info = session[userid] + self.userinfo = jsonpickle.decode(info) + except Exception as e: + userinfo = WxUserInfo() + self.userinfo = userinfo + + @property + def is_admin(self): + return self.userinfo.isAdmin + + @property + def is_password_set(self): + return self.userinfo.isPasswordSet + + def save_session(self): + info = jsonpickle.encode(self.userinfo) + self.session[self.userid] = info + + def handler(self): + info = self.message.content + + if self.userinfo.isAdmin and info.upper() == 'EXIT': + self.userinfo = WxUserInfo() + self.save_session() + return "退出成功" + if info.upper() == 'ADMIN': + self.userinfo.isAdmin = True + self.save_session() + return "输入管理员密码" + if self.userinfo.isAdmin and not self.userinfo.isPasswordSet: + passwd = settings.WXADMIN + if settings.TESTING: + passwd = '123' + if passwd.upper() == get_sha256(get_sha256(info)).upper(): + self.userinfo.isPasswordSet = True + self.save_session() + return "验证通过,请输入命令或者要执行的命令代码:输入helpme获得帮助" + else: + if self.userinfo.Count >= 3: + self.userinfo = WxUserInfo() + self.save_session() + return "超过验证次数" + self.userinfo.Count += 1 + self.save_session() + return "验证失败,请重新输入管理员密码:" + if self.userinfo.isAdmin and self.userinfo.isPasswordSet: + if self.userinfo.Command != '' and info.upper() == 'Y': + return cmd_handler.run(self.userinfo.Command) + else: + if info.upper() == 'HELPME': + return cmd_handler.get_help() + self.userinfo.Command = info + self.save_session() + return "确认执行: " + info + " 命令?" + + return ChatGPT.chat(info) + + +class WxUserInfo(): + def __init__(self): + self.isAdmin = False + self.isPasswordSet = False + self.Count = 0 + self.Command = '' diff --git a/DjangoBlog-master/servermanager/tests.py b/DjangoBlog-master/servermanager/tests.py new file mode 100644 index 0000000..22a6689 --- /dev/null +++ b/DjangoBlog-master/servermanager/tests.py @@ -0,0 +1,79 @@ +from django.test import Client, RequestFactory, TestCase +from django.utils import timezone +from werobot.messages.messages import TextMessage + +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): + def setUp(self): + self.client = Client() + self.factory = RequestFactory() + + def test_chat_gpt(self): + content = ChatGPT.chat("你好") + self.assertIsNotNone(content) + + def test_validate_comment(self): + user = BlogUser.objects.create_superuser( + email="liangliangyy1@gmail.com", + username="liangliangyy1", + password="liangliangyy1") + + self.client.login(username='liangliangyy1', password='liangliangyy1') + + c = Category() + c.name = "categoryccc" + c.save() + + article = Article() + article.title = "nicetitleccc" + article.body = "nicecontentccc" + article.author = user + article.category = c + article.type = 'a' + article.status = 'p' + article.save() + s = TextMessage([]) + s.content = "nice" + rsp = search(s, None) + rsp = category(None, None) + self.assertIsNotNone(rsp) + rsp = recents(None, None) + self.assertTrue(rsp != '暂时还没有文章') + + cmd = commands() + cmd.title = "test" + cmd.command = "ls" + cmd.describe = "test" + cmd.save() + + cmdhandler = CommandHandler() + rsp = cmdhandler.run('test') + self.assertIsNotNone(rsp) + s.source = 'u' + s.content = 'test' + msghandler = MessageHandler(s, {}) + + # msghandler.userinfo.isPasswordSet = True + # msghandler.userinfo.isAdmin = True + msghandler.handler() + s.content = 'y' + msghandler.handler() + s.content = 'idcard:12321233' + msghandler.handler() + s.content = 'weather:上海' + msghandler.handler() + s.content = 'admin' + msghandler.handler() + s.content = '123' + msghandler.handler() + + s.content = 'exit' + msghandler.handler() diff --git a/DjangoBlog-master/servermanager/urls.py b/DjangoBlog-master/servermanager/urls.py new file mode 100644 index 0000000..8d134d2 --- /dev/null +++ b/DjangoBlog-master/servermanager/urls.py @@ -0,0 +1,10 @@ +from django.urls import path +from werobot.contrib.django import make_view + +from .robot import robot + +app_name = "servermanager" +urlpatterns = [ + path(r'robot', make_view(robot)), + +] diff --git a/DjangoBlog-master/servermanager/views.py b/DjangoBlog-master/servermanager/views.py new file mode 100644 index 0000000..60f00ef --- /dev/null +++ b/DjangoBlog-master/servermanager/views.py @@ -0,0 +1 @@ +# Create your views here. diff --git a/DjangoBlog-master/templates/account/forget_password.html b/DjangoBlog-master/templates/account/forget_password.html new file mode 100644 index 0000000..3384531 --- /dev/null +++ b/DjangoBlog-master/templates/account/forget_password.html @@ -0,0 +1,30 @@ +{% extends 'share_layout/base_account.html' %} +{% load i18n %} +{% load static %} +{% block content %} +
+ + + + + +

+ Home Page + | + login page +

+ +
+{% endblock %} \ No newline at end of file diff --git a/DjangoBlog-master/templates/account/login.html b/DjangoBlog-master/templates/account/login.html new file mode 100644 index 0000000..cff8d33 --- /dev/null +++ b/DjangoBlog-master/templates/account/login.html @@ -0,0 +1,46 @@ +{% extends 'share_layout/base_account.html' %} +{% load static %} +{% load i18n %} +{% block content %} +
+ + + + + +

+ + {% trans 'Create Account' %} + + | + Home Page + | + + {% trans 'Forget Password' %} + +

+ +
+{% endblock %} \ No newline at end of file diff --git a/DjangoBlog-master/templates/account/registration_form.html b/DjangoBlog-master/templates/account/registration_form.html new file mode 100644 index 0000000..65e7549 --- /dev/null +++ b/DjangoBlog-master/templates/account/registration_form.html @@ -0,0 +1,29 @@ +{% extends 'share_layout/base_account.html' %} +{% load static %} +{% block content %} +
+ + + + + +

+ Sign In +

+ +
+{% endblock %} \ No newline at end of file diff --git a/DjangoBlog-master/templates/account/result.html b/DjangoBlog-master/templates/account/result.html new file mode 100644 index 0000000..23c9094 --- /dev/null +++ b/DjangoBlog-master/templates/account/result.html @@ -0,0 +1,27 @@ +{% extends 'share_layout/base.html' %} +{% load i18n %} +{% block header %} + {{ title }} +{% endblock %} +{% block content %} +
+ +
+{% endblock %} \ No newline at end of file diff --git a/DjangoBlog-master/templates/blog/article_archives.html b/DjangoBlog-master/templates/blog/article_archives.html new file mode 100644 index 0000000..959319e --- /dev/null +++ b/DjangoBlog-master/templates/blog/article_archives.html @@ -0,0 +1,60 @@ +{% extends 'share_layout/base.html' %} +{% load blog_tags %} +{% load cache %} +{% load i18n %} +{% block header %} + + {% trans 'article archive' %} | {{ SITE_DESCRIPTION }} + + + + + + + + + +{% endblock %} +{% block content %} +
+
+ +
+ +

{% trans 'article archive' %}

+
+ +
+ + {% regroup article_list by pub_time.year as year_post_group %} +
    + {% for year in year_post_group %} +
  • {{ year.grouper }} {% trans 'year' %} + {% regroup year.list by pub_time.month as month_post_group %} +
      + {% for month in month_post_group %} +
    • {{ month.grouper }} {% trans 'month' %} + +
    • + {% endfor %} +
    +
  • + {% endfor %} +
+
+
+
+ +{% endblock %} + + +{% block sidebar %} + {% load_sidebar user 'i' %} +{% endblock %} + + diff --git a/DjangoBlog-master/templates/blog/article_detail.html b/DjangoBlog-master/templates/blog/article_detail.html new file mode 100644 index 0000000..a74a0db --- /dev/null +++ b/DjangoBlog-master/templates/blog/article_detail.html @@ -0,0 +1,52 @@ +{% extends 'share_layout/base.html' %} +{% load blog_tags %} + +{% block header %} +{% endblock %} +{% block content %} +
+
+ {% load_article_detail article False user %} + + {% if article.type == 'a' %} + + {% endif %} + +
+ {% if article.comment_status == "o" and OPEN_SITE_COMMENT %} + + + {% include 'comments/tags/comment_list.html' %} + {% if user.is_authenticated %} + {% include 'comments/tags/post_comment.html' %} + {% else %} +
+

您还没有登录,请您登录后发表评论。 +

+ + {% load oauth_tags %} + {% load_oauth_applications request %} + +
+ {% endif %} + {% endif %} +
+ +{% endblock %} + +{% block sidebar %} + {% load_sidebar user "p" %} +{% endblock %} \ No newline at end of file diff --git a/DjangoBlog-master/templates/blog/article_index.html b/DjangoBlog-master/templates/blog/article_index.html new file mode 100644 index 0000000..0ee6150 --- /dev/null +++ b/DjangoBlog-master/templates/blog/article_index.html @@ -0,0 +1,42 @@ +{% extends 'share_layout/base.html' %} +{% load blog_tags %} +{% load cache %} +{% block header %} + {% if tag_name %} + {{ page_type }}:{{ tag_name }} | {{ SITE_DESCRIPTION }} + {% comment %}{% endcomment %} + {% else %} + {{ SITE_NAME }} | {{ SITE_DESCRIPTION }} + {% endif %} + + + + + + + +{% endblock %} +{% block content %} +
+
+ {% if page_type and tag_name %} +
+ +

{{ page_type }}:{{ tag_name }}

+
+ {% endif %} + + {% for article in article_list %} + {% load_article_detail article True user %} + {% endfor %} + {% if is_paginated %} + {% load_pagination_info page_obj page_type tag_name %} + + {% endif %} +
+
+ +{% endblock %} +{% block sidebar %} + {% load_sidebar user linktype %} +{% endblock %} \ No newline at end of file diff --git a/DjangoBlog-master/templates/blog/error_page.html b/DjangoBlog-master/templates/blog/error_page.html new file mode 100644 index 0000000..d41cfb6 --- /dev/null +++ b/DjangoBlog-master/templates/blog/error_page.html @@ -0,0 +1,45 @@ +{% extends 'share_layout/base.html' %} +{% load blog_tags %} +{% load cache %} +{% block header %} + {% if tag_name %} + {% if statuscode == '404' %} + 404 NotFound + {% elif statuscode == '403' %} + Permission Denied + {% elif statuscode == '500' %} + 500 Error + {% else %} + + {% endif %} + {% comment %}{% endcomment %} + {% else %} + {{ SITE_NAME }} | {{ SITE_DESCRIPTION }} + {% endif %} + + + + + + + +{% endblock %} +{% block content %} +
+
+ +
+

{{ message }}

+
+ +
+
+ +{% endblock %} + + +{% block sidebar %} + {% load_sidebar user 'i' %} +{% endblock %} + + diff --git a/DjangoBlog-master/templates/blog/links_list.html b/DjangoBlog-master/templates/blog/links_list.html new file mode 100644 index 0000000..ccecbea --- /dev/null +++ b/DjangoBlog-master/templates/blog/links_list.html @@ -0,0 +1,44 @@ +{% extends 'share_layout/base.html' %} +{% load blog_tags %} +{% load cache %} +{% block header %} + + 友情链接 | {{ SITE_DESCRIPTION }} + + + + + + + + + +{% endblock %} +{% block content %} +
+
+ +
+ +

友情链接

+
+ +
+ +
+
+
+ +{% endblock %} + + +{% block sidebar %} + {% load_sidebar user 'i' %} +{% endblock %} + + diff --git a/DjangoBlog-master/templates/blog/tags/article_info.html b/DjangoBlog-master/templates/blog/tags/article_info.html new file mode 100644 index 0000000..3deec44 --- /dev/null +++ b/DjangoBlog-master/templates/blog/tags/article_info.html @@ -0,0 +1,74 @@ +{% load blog_tags %} +{% load cache %} +{% load i18n %} +
+
+ +

+ {% if isindex %} + {% if article.article_order > 0 %} + 【{% trans 'pin to top' %}】{{ article.title }} + {% else %} + {{ article.title }} + {% endif %} + + {% else %} + {{ article.title }} + {% endif %} +

+ +
+ {% if article.type == 'a' %} + {% if not isindex %} + {% cache 36000 breadcrumb article.pk %} + {% load_breadcrumb article %} + {% endcache %} + {% endif %} + {% endif %} +
+ +
+ {% if isindex %} + {{ article.body|custom_markdown|escape|truncatechars_content }} +

Read more

+ {% else %} + + {% if article.show_toc %} + {% get_markdown_toc article.body as toc %} + {% trans 'toc' %}: + {{ toc|safe }} + +
+ {% endif %} +
+ + {{ article.body|custom_markdown|escape }} + +
+ {% endif %} + +
+ + {% load_article_metas article user %} + +
\ No newline at end of file diff --git a/DjangoBlog-master/templates/blog/tags/article_meta_info.html b/DjangoBlog-master/templates/blog/tags/article_meta_info.html new file mode 100644 index 0000000..cb6111c --- /dev/null +++ b/DjangoBlog-master/templates/blog/tags/article_meta_info.html @@ -0,0 +1,59 @@ +{% load i18n %} +{% load blog_tags %} + + + + + diff --git a/DjangoBlog-master/templates/blog/tags/article_pagination.html b/DjangoBlog-master/templates/blog/tags/article_pagination.html new file mode 100644 index 0000000..95514ff --- /dev/null +++ b/DjangoBlog-master/templates/blog/tags/article_pagination.html @@ -0,0 +1,17 @@ +{% load i18n %} + \ No newline at end of file diff --git a/DjangoBlog-master/templates/blog/tags/article_tag_list.html b/DjangoBlog-master/templates/blog/tags/article_tag_list.html new file mode 100644 index 0000000..c8ba474 --- /dev/null +++ b/DjangoBlog-master/templates/blog/tags/article_tag_list.html @@ -0,0 +1,19 @@ +{% load i18n %} +{% if article_tags_list %} +
+
+ {% trans 'tags' %} +
+
+ + {% for url,count,tag,color in article_tags_list %} + + {{ tag.name }} + {{ count }} + + {% endfor %} + +
+
+{% endif %} diff --git a/DjangoBlog-master/templates/blog/tags/breadcrumb.html b/DjangoBlog-master/templates/blog/tags/breadcrumb.html new file mode 100644 index 0000000..67087d5 --- /dev/null +++ b/DjangoBlog-master/templates/blog/tags/breadcrumb.html @@ -0,0 +1,19 @@ + + diff --git a/DjangoBlog-master/templates/blog/tags/sidebar.html b/DjangoBlog-master/templates/blog/tags/sidebar.html new file mode 100644 index 0000000..f70544c --- /dev/null +++ b/DjangoBlog-master/templates/blog/tags/sidebar.html @@ -0,0 +1,136 @@ +{% load blog_tags %} +{% load i18n %} + diff --git a/DjangoBlog-master/templates/comments/tags/comment_item.html b/DjangoBlog-master/templates/comments/tags/comment_item.html new file mode 100644 index 0000000..ebb0388 --- /dev/null +++ b/DjangoBlog-master/templates/comments/tags/comment_item.html @@ -0,0 +1,34 @@ +{% load blog_tags %} +
  • +
    + + + +

    {{ comment_item.body|escape|comment_markdown }}

    + +
    + +
  • \ No newline at end of file diff --git a/DjangoBlog-master/templates/comments/tags/comment_item_tree.html b/DjangoBlog-master/templates/comments/tags/comment_item_tree.html new file mode 100644 index 0000000..a9decd1 --- /dev/null +++ b/DjangoBlog-master/templates/comments/tags/comment_item_tree.html @@ -0,0 +1,54 @@ +{% load blog_tags %} +
  • +
    + + + +

    + {% if comment_item.parent_comment %} +

    + {% endif %} +

    + +

    {{ comment_item.body|escape|comment_markdown }}

    + + +
    + +
  • +{% query article_comments parent_comment=comment_item as cc_comments %} +{% for cc in cc_comments %} + {% with comment_item=cc template_name="comments/tags/comment_item_tree.html" %} + {% if depth >= 1 %} + {% include template_name %} + {% else %} + {% with depth=depth|add:1 %} + {% include template_name %} + {% endwith %} + {% endif %} + {% endwith %} +{% endfor %} \ No newline at end of file diff --git a/DjangoBlog-master/templates/comments/tags/comment_list.html b/DjangoBlog-master/templates/comments/tags/comment_list.html new file mode 100644 index 0000000..4092161 --- /dev/null +++ b/DjangoBlog-master/templates/comments/tags/comment_list.html @@ -0,0 +1,45 @@ + +
    + {% load blog_tags %} + {% load comments_tags %} + {% load cache %} + + + {% if article_comments %} +
    +
      + {# {% query article_comments parent_comment=None as parent_comments %}#} + {% for comment_item in p_comments %} + + {% with 0 as depth %} + {% include "comments/tags/comment_item_tree.html" %} + {% endwith %} + {% endfor %} + +
    + +
    +
    + {% endif %} +
    + +
    \ No newline at end of file diff --git a/DjangoBlog-master/templates/comments/tags/post_comment.html b/DjangoBlog-master/templates/comments/tags/post_comment.html new file mode 100644 index 0000000..3ae5a27 --- /dev/null +++ b/DjangoBlog-master/templates/comments/tags/post_comment.html @@ -0,0 +1,33 @@ +
    + +
    +

    发表评论 + +

    +
    {% csrf_token %} +

    + {{ form.body.label_tag }} + + {{ form.body }} + {{ form.body.errors }} +

    + {{ form.parent_comment_id }} +
    + {% if COMMENT_NEED_REVIEW %} + 支持markdown,评论经审核后才会显示。 + {% else %} + 支持markdown。 + {% endif %} + + +
    +
    +
    + +
    + + diff --git a/DjangoBlog-master/templates/oauth/bindsuccess.html b/DjangoBlog-master/templates/oauth/bindsuccess.html new file mode 100644 index 0000000..4bee77c --- /dev/null +++ b/DjangoBlog-master/templates/oauth/bindsuccess.html @@ -0,0 +1,22 @@ +{% extends 'share_layout/base.html' %} +{% block header %} + {{ title }} +{% endblock %} +{% block content %} +
    +
    + +
    + +

    {{ content }}

    +
    +
    +
    + + 登录 + | + 回到首页 +
    +
    +
    +{% endblock %} \ No newline at end of file diff --git a/DjangoBlog-master/templates/oauth/oauth_applications.html b/DjangoBlog-master/templates/oauth/oauth_applications.html new file mode 100644 index 0000000..a841ad2 --- /dev/null +++ b/DjangoBlog-master/templates/oauth/oauth_applications.html @@ -0,0 +1,13 @@ +{% load i18n %} + diff --git a/DjangoBlog-master/templates/oauth/require_email.html b/DjangoBlog-master/templates/oauth/require_email.html new file mode 100644 index 0000000..3adef12 --- /dev/null +++ b/DjangoBlog-master/templates/oauth/require_email.html @@ -0,0 +1,46 @@ +{% extends 'share_layout/base_account.html' %} + +{% load static %} +{% block content %} +
    + + + + + +

    + 登录 +

    + +
    +{% endblock %} \ No newline at end of file diff --git a/DjangoBlog-master/templates/owntracks/show_log_dates.html b/DjangoBlog-master/templates/owntracks/show_log_dates.html new file mode 100644 index 0000000..7dbba21 --- /dev/null +++ b/DjangoBlog-master/templates/owntracks/show_log_dates.html @@ -0,0 +1,17 @@ + + + + + 记录日期 + + + +
      + {% for date in results %} +
    • + {{ date }} +
    • + {% endfor %} +
    + + \ No newline at end of file diff --git a/DjangoBlog-master/templates/owntracks/show_maps.html b/DjangoBlog-master/templates/owntracks/show_maps.html new file mode 100644 index 0000000..3aeda36 --- /dev/null +++ b/DjangoBlog-master/templates/owntracks/show_maps.html @@ -0,0 +1,135 @@ + + + + + + + 运动轨迹 + + + +
    + + + + + + + + \ No newline at end of file diff --git a/DjangoBlog-master/templates/search/indexes/blog/article_text.txt b/DjangoBlog-master/templates/search/indexes/blog/article_text.txt new file mode 100644 index 0000000..4f9ca76 --- /dev/null +++ b/DjangoBlog-master/templates/search/indexes/blog/article_text.txt @@ -0,0 +1,3 @@ +{{ object.title }} +{{ object.author.username }} +{{ object.body }} \ No newline at end of file diff --git a/DjangoBlog-master/templates/search/search.html b/DjangoBlog-master/templates/search/search.html new file mode 100644 index 0000000..1404c60 --- /dev/null +++ b/DjangoBlog-master/templates/search/search.html @@ -0,0 +1,66 @@ +{% extends 'share_layout/base.html' %} +{% load blog_tags %} +{% block header %} + {{ SITE_NAME }} | {{ SITE_DESCRIPTION }} + + + + + + + +{% endblock %} +{% block content %} +
    +
    + {% if query %} +
    + {% if suggestion %} +

    + 已显示 “{{ suggestion }}” 的搜索结果。   + 仍然搜索:{{ query }}
    +

    + {% else %} +

    + 搜索:{{ query }}    +

    + {% endif %} +
    + {% endif %} + {% if query and page.object_list %} + {% for article in page.object_list %} + {% load_article_detail article.object True user %} + {% endfor %} + {% if page.has_previous or page.has_next %} + + + {% endif %} + {% else %} +
    + +

    哎呀,关键字:{{ query }}没有找到结果,要不换个词再试试?

    +
    + {% endif %} +
    +
    +{% endblock %} + + +{% block sidebar %} + {% load_sidebar request.user 'i' %} +{% endblock %} + + diff --git a/DjangoBlog-master/templates/share_layout/adsense.html b/DjangoBlog-master/templates/share_layout/adsense.html new file mode 100644 index 0000000..8f99c55 --- /dev/null +++ b/DjangoBlog-master/templates/share_layout/adsense.html @@ -0,0 +1,6 @@ + \ No newline at end of file diff --git a/DjangoBlog-master/templates/share_layout/base.html b/DjangoBlog-master/templates/share_layout/base.html new file mode 100644 index 0000000..75d0df5 --- /dev/null +++ b/DjangoBlog-master/templates/share_layout/base.html @@ -0,0 +1,123 @@ +{% load static %} +{% load cache %} +{% load i18n %} +{% load compress %} + + + + + + + + + + {% block header %} + {% block title %}{{ SITE_NAME }}{% endblock %} + + + {% endblock %} + {% load blog_tags %} + {% head_meta %} + + + + + + + + + + + {% compress css %} + + + + {% comment %}{% endcomment %} + + + + {% block compress_css %} + {% endblock %} + {% endcompress %} + {% if GLOBAL_HEADER %} + {{ GLOBAL_HEADER|safe }} + {% endif %} + + + +
    + +
    + + {% block content %} + {% endblock %} + + + {% block sidebar %} + {% endblock %} + + +
    + {% include 'share_layout/footer.html' %} +
    + + +
    + + {% compress js %} + + + + + + {% block compress_js %} + {% endblock %} + {% endcompress %} + {% block footer %} + {% endblock %} +
    + diff --git a/DjangoBlog-master/templates/share_layout/base_account.html b/DjangoBlog-master/templates/share_layout/base_account.html new file mode 100644 index 0000000..c00d842 --- /dev/null +++ b/DjangoBlog-master/templates/share_layout/base_account.html @@ -0,0 +1,47 @@ + + + + {% load static %} + + + + + + + + + {{ SITE_NAME }} | {{ SITE_DESCRIPTION }} + + {% load compress %} + {% compress css %} + + + + + + + + + + {% endcompress %} + {% compress js %} + + + {% endcompress %} + + + + + +{% block content %} +{% endblock %} + + + + + + + \ No newline at end of file diff --git a/DjangoBlog-master/templates/share_layout/footer.html b/DjangoBlog-master/templates/share_layout/footer.html new file mode 100644 index 0000000..cd86a29 --- /dev/null +++ b/DjangoBlog-master/templates/share_layout/footer.html @@ -0,0 +1,56 @@ + + + diff --git a/DjangoBlog-master/templates/share_layout/nav.html b/DjangoBlog-master/templates/share_layout/nav.html new file mode 100644 index 0000000..24d4da6 --- /dev/null +++ b/DjangoBlog-master/templates/share_layout/nav.html @@ -0,0 +1,30 @@ +{% load i18n %} + + \ No newline at end of file diff --git a/DjangoBlog-master/templates/share_layout/nav_node.html b/DjangoBlog-master/templates/share_layout/nav_node.html new file mode 100644 index 0000000..c266880 --- /dev/null +++ b/DjangoBlog-master/templates/share_layout/nav_node.html @@ -0,0 +1,19 @@ + + + diff --git a/main.py b/main.py new file mode 100644 index 0000000..eb389a0 --- /dev/null +++ b/main.py @@ -0,0 +1,16 @@ +# 这是一个示例 Python 脚本。 + +# 按 Shift+F10 执行或将其替换为您的代码。 +# 按 双击 Shift 在所有地方搜索类、文件、工具窗口、操作和设置。 + + +def print_hi(name): + # 在下面的代码行中使用断点来调试脚本。 + print(f'Hi, {name}') # 按 Ctrl+F8 切换断点。 + + +# 按装订区域中的绿色按钮以运行脚本。 +if __name__ == '__main__': + print_hi('PyCharm') + +# 访问 https://www.jetbrains.com/help/pycharm/ 获取 PyCharm 帮助 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..b737f29 --- /dev/null +++ b/setup.py @@ -0,0 +1,12 @@ +from distutils.core import setup + +setup( + name='DjangoBlog-master', + version='', + packages=[''], + url='', + license='', + author='鹿与溪', + author_email='', + description='' +)