├── app
├── main
│ ├── __init__.py
│ ├── ext.py
│ ├── errors.py
│ ├── signals.py
│ ├── forms.py
│ ├── urls.py
│ └── models.py
├── tests
│ ├── __init__.py
│ ├── test_basics.py
│ ├── test_client.py
│ └── test_models.py
├── accounts
│ ├── __init__.py
│ ├── urls.py
│ ├── misc.py
│ ├── permissions.py
│ ├── models.py
│ └── forms.py
├── static
│ ├── img
│ │ ├── favicon.ico
│ │ ├── favicon-16x16.png
│ │ ├── favicon-32x32.png
│ │ └── favicon-96x96.png
│ ├── css
│ │ ├── octblog.css
│ │ ├── share.min.css
│ │ ├── clean-blog.min.css
│ │ └── clean-blog.css
│ └── fonts
│ │ ├── iconfont.eot
│ │ ├── iconfont.ttf
│ │ ├── iconfont.woff
│ │ └── iconfont.svg
├── pip.conf
├── requirements.txt
├── templates
│ ├── blog_admin
│ │ ├── 401.html
│ │ ├── 404.html
│ │ ├── 403.html
│ │ ├── import_comments.html
│ │ ├── post_statistics.html
│ │ ├── post_statistics_detail.html
│ │ ├── su_export.html
│ │ ├── widgets.html
│ │ ├── posts.html
│ │ ├── widget.html
│ │ ├── su_posts.html
│ │ ├── su_post.html
│ │ ├── index.html
│ │ ├── comments.html
│ │ └── post.html
│ ├── accounts
│ │ ├── email
│ │ │ ├── confirm.txt
│ │ │ ├── reset_password.txt
│ │ │ ├── confirm.html
│ │ │ └── reset_password.html
│ │ ├── reset_password.html
│ │ ├── registration.html
│ │ ├── login.html
│ │ ├── settings.html
│ │ ├── user.html
│ │ ├── su_users.html
│ │ ├── password.html
│ │ └── users.html
│ ├── main
│ │ ├── sitemap.xml
│ │ ├── misc
│ │ │ ├── jiathis_share.html
│ │ │ ├── duoshuo.html
│ │ │ └── post_footer.html
│ │ ├── 404.html
│ │ ├── comments.html
│ │ ├── archive.html
│ │ ├── wechat_detail.html
│ │ ├── wechat_list.html
│ │ ├── post.html
│ │ ├── author.html
│ │ └── index.html
│ ├── _msg.html
│ ├── _form.html
│ ├── _pagination.html
│ ├── base.html
│ └── admin_base.html
├── octblog_nginx.conf
├── Dockerfile
├── manage.py
├── sources.list
├── Dockerfile_py3
├── Dockerfile_py2
├── supervisord.conf
├── OctBlog
│ ├── __init__.py
│ └── config.py
└── readme.md
├── .env
├── requirements_test.py
├── requirements.txt
├── .travis.yml
├── docker-compose.yml
├── .gitignore
├── docker-compose_no_swarm.yml
└── README.md
/app/main/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/accounts/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.env:
--------------------------------------------------------------------------------
1 | config=prd
2 | MONGO_HOST=mongo
3 | allow_registration=true
4 | allow_su_creation=true
--------------------------------------------------------------------------------
/requirements_test.py:
--------------------------------------------------------------------------------
1 | -r requirements.txt
2 |
3 | Faker
4 | factory_boy
5 | mongomock
6 | coverage
7 |
--------------------------------------------------------------------------------
/app/static/img/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flyhigher139/OctBlog/HEAD/app/static/img/favicon.ico
--------------------------------------------------------------------------------
/app/static/css/octblog.css:
--------------------------------------------------------------------------------
1 | .category-list{
2 | border: 0px;
3 | }
4 |
5 | .group-list{
6 | border: 0px;
7 | }
--------------------------------------------------------------------------------
/app/static/fonts/iconfont.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flyhigher139/OctBlog/HEAD/app/static/fonts/iconfont.eot
--------------------------------------------------------------------------------
/app/static/fonts/iconfont.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flyhigher139/OctBlog/HEAD/app/static/fonts/iconfont.ttf
--------------------------------------------------------------------------------
/app/static/fonts/iconfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flyhigher139/OctBlog/HEAD/app/static/fonts/iconfont.woff
--------------------------------------------------------------------------------
/app/static/img/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flyhigher139/OctBlog/HEAD/app/static/img/favicon-16x16.png
--------------------------------------------------------------------------------
/app/static/img/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flyhigher139/OctBlog/HEAD/app/static/img/favicon-32x32.png
--------------------------------------------------------------------------------
/app/static/img/favicon-96x96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flyhigher139/OctBlog/HEAD/app/static/img/favicon-96x96.png
--------------------------------------------------------------------------------
/app/pip.conf:
--------------------------------------------------------------------------------
1 | [global]
2 | index-url = http://mirrors.aliyun.com/pypi/simple/
3 |
4 | [install]
5 | trusted-host=mirrors.aliyun.com
--------------------------------------------------------------------------------
/app/main/ext.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | import requests
5 |
6 | def submit_url_to_baidu(baidu_url, url):
7 | res = requests.post(baidu_url, data=url)
8 | return res
--------------------------------------------------------------------------------
/app/requirements.txt:
--------------------------------------------------------------------------------
1 | flask
2 | flask-script
3 | flask-login
4 | flask-admin
5 | flask_mail
6 | Flask-WTF
7 | flask-principal
8 | WTForms
9 | mongoengine
10 | flask_mongoengine
11 | markdown2
12 | bleach
13 | python-dateutil
14 | requests
15 | flask-moment
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | flask
2 | flask-script
3 | flask-login
4 | flask-admin
5 | flask_mail
6 | Flask-WTF
7 | flask-principal
8 | WTForms
9 | mongoengine
10 | flask_mongoengine
11 | markdown2
12 | bleach
13 | python-dateutil
14 | requests
15 | flask-moment
--------------------------------------------------------------------------------
/app/templates/blog_admin/401.html:
--------------------------------------------------------------------------------
1 | {% extends 'admin_base.html' %}
2 |
3 | {%block main %}
4 |
5 |
6 |
401 Error
7 |
Your requests is unauthorized
8 |
9 |
10 |
11 | {% endblock %}
12 |
13 | {% block js %}
14 |
15 | {% endblock %}
--------------------------------------------------------------------------------
/app/templates/accounts/email/confirm.txt:
--------------------------------------------------------------------------------
1 | Dear {{ user.username }},
2 |
3 | Welcome to OctBlog!
4 |
5 | To confirm your email please click on the following link:
6 |
7 | {{ url_for('accounts.confirm_email', token=token, _external=True) }}
8 |
9 | Sincerely,
10 |
11 | OctBlog Developer
12 |
13 | Note: replies to this email address are not monitored.
--------------------------------------------------------------------------------
/app/templates/blog_admin/404.html:
--------------------------------------------------------------------------------
1 | {% extends 'admin_base.html' %}
2 |
3 | {%block main %}
4 |
5 |
6 |
404 Error
7 |
The page does not exist!
8 |
9 |
10 |
11 | {% endblock %}
12 |
13 | {% block js %}
14 |
15 | {% endblock %}
--------------------------------------------------------------------------------
/app/templates/accounts/email/reset_password.txt:
--------------------------------------------------------------------------------
1 | Dear {{ user.username }},
2 |
3 | To reset your password click on the following link:
4 |
5 | {{ url_for('accounts.reset_password', token=token, _external=True) }}
6 |
7 | If you have not requested a password reset simply ignore this message.
8 |
9 | Sincerely,
10 |
11 | OctBlog Developer
12 |
13 | Note: replies to this email address are not monitored.
14 |
--------------------------------------------------------------------------------
/app/templates/main/sitemap.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | {% for page in pages %}
4 |
5 | {{page[0]|safe}}
6 | {{page[1]}}
7 | {{page[2]}}
8 | {{page[3]}}
9 |
10 | {% endfor %}
11 |
--------------------------------------------------------------------------------
/app/templates/blog_admin/403.html:
--------------------------------------------------------------------------------
1 | {% extends 'admin_base.html' %}
2 |
3 | {%block main %}
4 |
5 |
6 |
403 Error
7 | {% if msg %}
8 |
{{ msg }}
9 | {% else %}
10 |
Your requests is forbidden
11 | {% endif %}
12 |
13 |
14 |
15 | {% endblock %}
16 |
17 | {% block js %}
18 |
19 | {% endblock %}
--------------------------------------------------------------------------------
/app/octblog_nginx.conf:
--------------------------------------------------------------------------------
1 | server {
2 | listen 8000;
3 |
4 | server_name localhost;
5 |
6 | access_log /var/log/nginx/access.log;
7 | error_log /var/log/nginx/error.log;
8 |
9 | location / {
10 | proxy_pass http://127.0.0.1:4000/;
11 | proxy_redirect off;
12 |
13 | proxy_set_header Host $http_host;
14 | proxy_set_header X-Real-IP $remote_addr;
15 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
16 | }
17 | }
--------------------------------------------------------------------------------
/app/templates/accounts/email/confirm.html:
--------------------------------------------------------------------------------
1 | Dear {{ user.username }},
2 | Welcome to OctBlog !
3 | To confirm your account please click here .
4 | Alternatively, you can paste the following link in your browser's address bar:
5 | {{ url_for('accounts.confirm_email', token=token, _external=True) }}
6 | Sincerely,
7 | OctBlog Developer
8 | Note: replies to this email address are not monitored.
9 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: python
2 | python:
3 | # - "2.6"
4 | - "2.7"
5 | # - "3.2"
6 | # - "3.3"
7 | # - "3.4"
8 | - "3.5"
9 | - "3.5-dev" # 3.5 development branch
10 | - "3.6"
11 | - "3.6-dev" # 3.6 development branch
12 | # - "3.7-dev" # 3.7 development branch
13 | # - "nightly" # currently points to 3.7-dev
14 |
15 |
16 | # before_install: # something
17 |
18 | services:
19 | - mongodb
20 |
21 | # command to install dependencies
22 | install: "pip install -r requirements.txt"
23 | # command to run tests
24 | script: "python app/manage.py test"
25 |
--------------------------------------------------------------------------------
/app/templates/accounts/email/reset_password.html:
--------------------------------------------------------------------------------
1 | Dear {{ user.username }},
2 | To reset your password click here .
3 | Alternatively, you can paste the following link in your browser's address bar:
4 | {{ url_for('accounts.reset_password', token=token, _external=True) }}
5 | If you have not requested a password reset simply ignore this message.
6 | Sincerely,
7 | OctBlog developer
8 | Note: replies to this email address are not monitored.
9 |
--------------------------------------------------------------------------------
/app/templates/accounts/reset_password.html:
--------------------------------------------------------------------------------
1 | {% extends "admin_base.html" %}
2 | {% block title %}Reset Password{% endblock %}
3 | {% block main %}
4 |
5 |
6 |
7 |
8 | {% import "_form.html" as forms %}
9 |
13 |
14 |
15 |
16 | {% endblock %}
--------------------------------------------------------------------------------
/app/tests/test_basics.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | from flask import current_app
3 | from OctBlog import create_app
4 |
5 |
6 | class BasicsTestCase(unittest.TestCase):
7 | def setUp(self):
8 | self.app = create_app('testing')
9 | self.app_context = self.app.app_context()
10 | self.app_context.push()
11 |
12 | def tearDown(self):
13 | self.app_context.pop()
14 |
15 | def test_app_exists(self):
16 | self.assertFalse(current_app is None)
17 |
18 | def test_app_is_testing(self):
19 | self.assertTrue(current_app.config['TESTING'])
20 |
--------------------------------------------------------------------------------
/app/templates/accounts/registration.html:
--------------------------------------------------------------------------------
1 | {% extends "admin_base.html" %}
2 | {% block title %}Registration{% endblock %}
3 | {% block main %}
4 |
5 |
6 |
7 |
8 | {% import "_form.html" as forms %}
9 |
13 |
14 |
15 |
16 | {% endblock %}
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3"
2 | services:
3 | octblog:
4 | image: gevin/octblog
5 | deploy:
6 | replicas: 3
7 | resources:
8 | limits:
9 | cpus: "0.1"
10 | memory: 100M
11 | ports:
12 | - "8000:8000"
13 | depends_on:
14 | - mongo
15 | env_file: .env
16 | networks:
17 | - webnet
18 | mongo:
19 | image: mongo:3.2
20 | volumes:
21 | - /Users/gevin/projects/data/mongodb:/data/db
22 | deploy:
23 | placement:
24 | constraints: [node.role == manager]
25 | networks:
26 | - webnet
27 |
28 | networks:
29 | webnet:
--------------------------------------------------------------------------------
/app/templates/blog_admin/import_comments.html:
--------------------------------------------------------------------------------
1 | {% extends "admin_base.html" %}
2 | {% block title %}Import Comments{% endblock %}
3 | {% block main %}
4 |
5 |
6 |
7 |
8 |
Import Comments from duoshuo
9 | {% import "_form.html" as forms %}
10 |
14 |
15 |
16 |
17 | {% endblock %}
--------------------------------------------------------------------------------
/app/templates/main/misc/jiathis_share.html:
--------------------------------------------------------------------------------
1 |
2 |
9 |
12 |
13 |
--------------------------------------------------------------------------------
/app/Dockerfile:
--------------------------------------------------------------------------------
1 | # MAINTAINER Gevin
2 | # DOCKER-VERSION 18.03.0-ce, build 0520e24
3 |
4 | FROM python:3.6.5-alpine3.7
5 | LABEL maintainer="flyhigher139@gmail.com"
6 | # COPY pip.conf /root/.pip/pip.conf
7 |
8 | RUN mkdir -p /usr/src/app && \
9 | mkdir -p /var/log/gunicorn
10 |
11 | WORKDIR /usr/src/app
12 | COPY requirements.txt /usr/src/app/requirements.txt
13 |
14 | RUN pip install --no-cache-dir gunicorn && \
15 | pip install --no-cache-dir -r /usr/src/app/requirements.txt && \
16 | pip install --ignore-installed six
17 |
18 | COPY . /usr/src/app
19 |
20 |
21 | ENV PORT 8000
22 | EXPOSE 8000 5000
23 |
24 | CMD ["/usr/local/bin/gunicorn", "-w", "2", "-b", ":8000", "manage:app"]
--------------------------------------------------------------------------------
/app/templates/_msg.html:
--------------------------------------------------------------------------------
1 | {% macro render_msg() %}
2 |
3 | {% with messages = get_flashed_messages(with_categories=true) %}
4 | {% if messages %}
5 |
6 | {% for category, message in messages %}
7 | {% if category == 'message' %}
8 | {% set category='info' %}
9 | {% endif %}
10 |
11 | ×
12 | {{ message }}
13 |
14 | {% endfor %}
15 |
16 | {% endif %}
17 | {% endwith %}
18 |
19 | {% endmacro %}
--------------------------------------------------------------------------------
/app/templates/accounts/login.html:
--------------------------------------------------------------------------------
1 | {% extends "admin_base.html" %}
2 | {% block title %}Login{% endblock %}
3 | {% block main %}
4 |
5 |
20 | {% endblock %}
--------------------------------------------------------------------------------
/app/templates/main/misc/duoshuo.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
16 |
--------------------------------------------------------------------------------
/app/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | import os, sys
4 | sys.path.append(os.path.abspath(os.path.dirname(__file__)))
5 |
6 | # from flask.ext.script import Manager, Server
7 | from flask_script import Manager, Server
8 |
9 | from OctBlog import create_app
10 | app = create_app(os.getenv('config') or 'default')
11 |
12 | # from OctBlog import app
13 |
14 |
15 | manager = Manager(app)
16 |
17 | # Turn on debugger by default and reloader
18 | manager.add_command("runserver", Server(
19 | use_debugger = True,
20 | use_reloader = True,
21 | host = '0.0.0.0',
22 | port = 5000)
23 | )
24 |
25 | @manager.command
26 | def test():
27 | import unittest
28 | tests = unittest.TestLoader().discover('tests')
29 | unittest.TextTestRunner(verbosity=2).run(tests)
30 |
31 | if __name__ == "__main__":
32 | manager.run()
33 |
--------------------------------------------------------------------------------
/app/sources.list:
--------------------------------------------------------------------------------
1 | deb http://mirrors.aliyun.com/ubuntu/ trusty main restricted universe multiverse
2 | deb http://mirrors.aliyun.com/ubuntu/ trusty-security main restricted universe multiverse
3 | deb http://mirrors.aliyun.com/ubuntu/ trusty-updates main restricted universe multiverse
4 | deb http://mirrors.aliyun.com/ubuntu/ trusty-proposed main restricted universe multiverse
5 | deb http://mirrors.aliyun.com/ubuntu/ trusty-backports main restricted universe multiverse
6 | deb-src http://mirrors.aliyun.com/ubuntu/ trusty main restricted universe multiverse
7 | deb-src http://mirrors.aliyun.com/ubuntu/ trusty-security main restricted universe multiverse
8 | deb-src http://mirrors.aliyun.com/ubuntu/ trusty-updates main restricted universe multiverse
9 | deb-src http://mirrors.aliyun.com/ubuntu/ trusty-proposed main restricted universe multiverse
10 | deb-src http://mirrors.aliyun.com/ubuntu/ trusty-backports main restricted universe multiverse
--------------------------------------------------------------------------------
/app/templates/accounts/settings.html:
--------------------------------------------------------------------------------
1 | {% extends 'admin_base.html' %}
2 | {% block title %} Settings: Update Profile {% endblock %}
3 |
4 | {% block main %}
5 |
13 |
14 |
15 |
16 |
17 | {% import "_form.html" as forms %}
18 |
22 |
23 |
24 |
25 | {% endblock %}
--------------------------------------------------------------------------------
/app/main/errors.py:
--------------------------------------------------------------------------------
1 | from flask import render_template
2 |
3 | from .views import get_base_data
4 |
5 | def handle_unmatchable(*args, **kwargs):
6 | # return 'unmatchable page', 404
7 | data = get_base_data()
8 | return render_template('main/404.html', **data), 404
9 |
10 | def page_not_found(e):
11 | data = get_base_data()
12 | return render_template('main/404.html', **data), 404
13 | # return '404 page', 404
14 |
15 | def handle_bad_request(e):
16 | return 'bad request!', 400
17 |
18 | def handle_forbidden(e):
19 | # return 'request forbidden', 403
20 | return render_template('blog_admin/403.html', msg=e.description), 403
21 |
22 | def handle_unauthorized(e):
23 | # return 'request forbidden', 403
24 | return render_template('blog_admin/401.html'), 401
25 |
26 | def admin_page_not_found(e):
27 | # return render_template('404.html'), 404
28 | # return 'admin 404 page', 404
29 | return render_template('blog_admin/404.html'), 404
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 |
3 | .DS_Store
4 | .vscode/
5 |
6 | __pycache__/
7 | *.py[cod]
8 |
9 | # C extensions
10 | *.so
11 |
12 | # Distribution / packaging
13 | .Python
14 | env/
15 | build/
16 | develop-eggs/
17 | dist/
18 | downloads/
19 | eggs/
20 | .eggs/
21 | lib/
22 | lib64/
23 | parts/
24 | sdist/
25 | var/
26 | *.egg-info/
27 | .installed.cfg
28 | *.egg
29 |
30 | # PyInstaller
31 | # Usually these files are written by a python script from a template
32 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
33 | *.manifest
34 | *.spec
35 |
36 | # Installer logs
37 | pip-log.txt
38 | pip-delete-this-directory.txt
39 |
40 | # Unit test / coverage reports
41 | htmlcov/
42 | .tox/
43 | .coverage
44 | .coverage.*
45 | .cache
46 | nosetests.xml
47 | coverage.xml
48 | *,cover
49 |
50 | # Translations
51 | *.mo
52 | *.pot
53 |
54 | # Django stuff:
55 | *.log
56 |
57 | # Sphinx documentation
58 | docs/_build/
59 |
60 | # PyBuilder
61 | target/
62 |
63 | exports/
--------------------------------------------------------------------------------
/app/templates/main/404.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 | {% block title %} 404 Not Found {% endblock %}
3 |
4 | {% block header %}
5 |
18 | {% endblock %}
19 |
20 | {% block main %}
21 |
22 |
27 |
28 |
29 | {% endblock %}
--------------------------------------------------------------------------------
/app/templates/accounts/user.html:
--------------------------------------------------------------------------------
1 | {% extends 'admin_base.html' %}
2 | {% block title %} Edit User {% endblock %}
3 |
4 | {% block main %}
5 |
6 |
7 |
User Details
8 |
9 | {% import "_form.html" as forms %}
10 |
25 |
26 |
27 | {% endblock %}
--------------------------------------------------------------------------------
/docker-compose_no_swarm.yml:
--------------------------------------------------------------------------------
1 | version: '2'
2 | services:
3 | blog:
4 | # restart: always
5 | image: gevin/octblog:0.4
6 | # ports:
7 | # - "8000:8000"
8 | # - "5000:5000"
9 | links:
10 | - mongo:mongo
11 | volumes:
12 | - blog-static:/usr/src/app/static
13 | env_file: .env
14 | environment:
15 | - VIRTUAL_HOST=localhost
16 | - VIRTUAL_PORT=8000
17 |
18 | mongo:
19 | # restart: always
20 | image: mongo:3.2
21 | volumes:
22 | - /Users/gevin/projects/data/mongodb:/data/db
23 | # ports:
24 | # - "27017:27017"
25 |
26 | blog-static-nginx:
27 | # restart: always
28 | image: nginx:stable-alpine
29 | # ports:
30 | # - "8000:80"
31 | volumes:
32 | - blog-static:/usr/share/nginx/html/static:ro
33 | environment:
34 | - VIRTUAL_HOST=localhost
35 | - VIRTUAL_PORT=80
36 |
37 | nginx-proxy:
38 | # restart: always
39 | image: jwilder/nginx-proxy:alpine
40 | ports:
41 | - "80:80"
42 | volumes:
43 | - /var/run/docker.sock:/tmp/docker.sock:ro
44 |
45 | volumes:
46 | blog-static:
--------------------------------------------------------------------------------
/app/Dockerfile_py3:
--------------------------------------------------------------------------------
1 | # MAINTAINER Gevin
2 | # DOCKER-VERSION 18.03.0-ce, build 0520e24
3 |
4 | FROM ubuntu:14.04
5 | LABEL maintainer="flyhigher139@gmail.com"
6 | COPY sources.list /etc/apt/sources.list
7 | COPY pip.conf /root/.pip/pip.conf
8 |
9 | RUN apt-get update && apt-get install -y \
10 | build-essential \
11 | python3 \
12 | python3-dev \
13 | python3-pip \
14 | && apt-get clean all \
15 | && rm -rf /var/lib/apt/lists/* \
16 | && pip3 install -U pip
17 |
18 | RUN mkdir -p /etc/supervisor.conf.d && \
19 | mkdir -p /var/log/supervisor && \
20 | mkdir -p /usr/src/app && \
21 | mkdir -p /var/log/gunicorn
22 |
23 | WORKDIR /usr/src/app
24 | COPY requirements.txt /usr/src/app/requirements.txt
25 |
26 | RUN pip3 install --no-cache-dir gunicorn && \
27 | pip3 install --no-cache-dir -r /usr/src/app/requirements.txt && \
28 | pip3 install --ignore-installed six
29 |
30 | COPY . /usr/src/app
31 |
32 |
33 | ENV PORT 8000
34 | EXPOSE 8000 5000
35 |
36 | CMD ["/usr/local/bin/gunicorn", "-w", "2", "-b", ":8000", "manage:app"]
--------------------------------------------------------------------------------
/app/templates/_form.html:
--------------------------------------------------------------------------------
1 | {% macro render(form) -%}
2 |
3 | {% for field in form %}
4 | {% if field.type in ['CSRFTokenField', 'HiddenField'] %}
5 | {{ field() }}
6 | {% elif field.type == "BooleanField" %}
7 |
8 |
9 | {{ field() }} {{ field.label }}
10 |
11 |
12 |
13 | {% elif field.type == "RadioField" %}
14 | {{ field.label }}
15 | {% for subfield in field %}
16 |
17 | {{ subfield }} {{ subfield.label }}
18 |
19 | {% endfor %}
20 |
21 |
22 | {% else %}
23 |
24 |
25 | {{ field.label }}
26 | {% if field.type == "TextAreaField" %}
27 | {{ field(class_="form-control", rows=10) }}
28 | {% else %}
29 | {{ field(class_="form-control") }}
30 | {% endif %}
31 | {% if field.errors or field.help_text %}
32 |
33 | {% if field.errors %}
34 | {{ field.errors|join(' ') }}
35 | {% else %}
36 | {{ field.help_text }}
37 | {% endif %}
38 |
39 | {% endif %}
40 |
41 | {% endif %}
42 | {% endfor %}
43 |
44 | {% endmacro %}
--------------------------------------------------------------------------------
/app/Dockerfile_py2:
--------------------------------------------------------------------------------
1 | # MAINTAINER Gevin
2 | # DOCKER-VERSION 18.03.0-ce, build 0520e24
3 |
4 | FROM ubuntu:14.04
5 | LABEL maintainer="flyhigher139@gmail.com"
6 | COPY sources.list /etc/apt/sources.list
7 | COPY pip.conf /root/.pip/pip.conf
8 |
9 | RUN apt-get update && apt-get install -y \
10 | vim \
11 | nginx \
12 | build-essential \
13 | python-dev \
14 | python-pip \
15 | && apt-get clean all \
16 | && rm -rf /var/lib/apt/lists/* \
17 | && pip install -U pip
18 |
19 | RUN mkdir -p /etc/supervisor.conf.d && \
20 | mkdir -p /var/log/supervisor && \
21 | mkdir -p /usr/src/app && \
22 | mkdir -p /var/log/gunicorn
23 |
24 | WORKDIR /usr/src/app
25 | COPY requirements.txt /usr/src/app/requirements.txt
26 |
27 | RUN pip install --no-cache-dir supervisor gunicorn && \
28 | pip install --no-cache-dir -r /usr/src/app/requirements.txt && \
29 | # to fix six bugs
30 | pip install --ignore-installed six
31 |
32 |
33 | COPY . /usr/src/app
34 |
35 | RUN echo "daemon off;" >> /etc/nginx/nginx.conf && \
36 | ln -s /usr/src/app/octblog_nginx.conf /etc/nginx/sites-enabled
37 |
38 | ENV PORT 8000
39 | EXPOSE 8000 5000
40 |
41 | # CMD ["/usr/local/bin/supervisord", "-n"]
42 | CMD ["/usr/local/bin/supervisord", "-n", "-c", "/usr/src/app/supervisord.conf"]
--------------------------------------------------------------------------------
/app/templates/main/misc/post_footer.html:
--------------------------------------------------------------------------------
1 | {% if display_copyright %}
2 |
3 |
4 |
5 |
{{ copyright_msg }}
6 |
7 |
8 |
9 |
10 | {% endif %}
11 |
12 |
13 | {% if allow_donate %}
14 |
15 |
16 |
17 |
{{ donation_msg }}
18 |
19 | |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | {% endif %}
30 |
31 | {% if display_wechat %}
32 |
33 |
34 |
35 |
{{ wechat_msg }}
36 |
37 |
38 |
39 |
40 | {% endif %}
--------------------------------------------------------------------------------
/app/tests/test_client.py:
--------------------------------------------------------------------------------
1 | # import unittest
2 | # from flask import current_app, url_for
3 | # from OctBlog import create_app, db
4 | # from accounts import models as accounts_models
5 | # from main import models as main_models
6 | #
7 | #
8 | # class ModelTestCase(unittest.TestCase):
9 | # def setUp(self):
10 | # self.app = create_app('testing')
11 | # self.app_context = self.app.app_context()
12 | # self.app_context.push()
13 | # self.client = self.app.test_client(use_cookies=True)
14 | #
15 | # def tearDown(self):
16 | # db_name = current_app.config['MONGODB_SETTINGS']['DB']
17 | # db.connection.drop_database(db_name)
18 | # self.app_context.pop()
19 | #
20 | # def test_home_page(self):
21 | # response = self.client.get(url_for('main.index'))
22 | # self.assertTrue(response.status_code==200)
23 | #
24 | # # def test_register_and_login(self):
25 | # # response = self.client.post(url_for('accounts.register'), data={
26 | # # 'username': 'octblog',
27 | # # 'email': 'octblog@example.com',
28 | # # 'password': 'octblog',
29 | # # 'password2': 'octblog'
30 | # # })
31 | # #
32 | # # self.app.logger.debug(response.status_code)
33 | # # self.assertTrue(response.status_code==302)
34 |
--------------------------------------------------------------------------------
/app/templates/main/comments.html:
--------------------------------------------------------------------------------
1 |
2 | Comments
3 |
4 | {% import "_msg.html" as messages %}
5 | {{ messages.render_msg() }}
6 |
7 | {% import "_form.html" as forms %}
8 |
12 |
13 |
14 |
15 | {% for comment in comments %}
16 |
17 |
18 |
19 |
20 |
21 |
22 | {{ comment.author }}
23 | {% if comment.homepage %}
24 |
25 | {% endif %}
26 |
27 |
28 | {{ comment.html_content|safe }}
29 |
30 |
31 |
32 | {{ comment.pub_time.strftime('%Y/%m/%d') }}
33 |
34 |
35 | {% endfor %}
36 |
--------------------------------------------------------------------------------
/app/supervisord.conf:
--------------------------------------------------------------------------------
1 | [supervisord]
2 | logfile = /tmp/supervisord.log
3 | logfile_maxbytes = 50MB
4 | logfile_backups=10
5 | loglevel = info
6 | pidfile = /tmp/supervisord.pid
7 | nodaemon = false
8 | minfds = 1024
9 | minprocs = 200
10 | umask = 022
11 | user = root
12 | identifier = supervisor
13 | directory = /tmp
14 | nocleanup = true
15 | childlogdir = /tmp
16 | strip_ansi = false
17 |
18 | [program:app-gunicorn]
19 | command = /usr/local/bin/gunicorn -w 2 -b :4000 manage:app
20 | autostart=true
21 | autorestart=true
22 | stdout_logfile=/var/log/supervisor/%(program_name)s.log
23 | stderr_logfile=/var/log/supervisor/%(program_name)s.log
24 |
25 | [program:nginx-app]
26 | command = service nginx start
27 | autostart=true
28 | autorestart=true
29 | stdout_logfile=/var/log/supervisor/%(program_name)s.log
30 | stderr_logfile=/var/log/supervisor/%(program_name)s.log
31 |
32 | # [program:celery-app]
33 | # command = celery -A gitmark worker -l info
34 | # autostart=true
35 | # autorestart=true
36 | # stdout_logfile=/var/log/supervisor/%(program_name)s.log
37 | # stderr_logfile=/var/log/supervisor/%(program_name)s.log
38 |
39 | # [program:redis-app]
40 | # command = service redis-server start
41 | # autostart=true
42 | # autorestart=true
43 | # stdout_logfile=/var/log/supervisor/%(program_name)s.log
44 | # stderr_logfile=/var/log/supervisor/%(program_name)s.log
45 |
--------------------------------------------------------------------------------
/app/templates/_pagination.html:
--------------------------------------------------------------------------------
1 | {% macro render_pagination(pagination) %}
2 |
3 |
41 |
42 | {% endmacro %}
--------------------------------------------------------------------------------
/app/accounts/urls.py:
--------------------------------------------------------------------------------
1 | from flask import Blueprint
2 |
3 | from . import views
4 | from main import errors
5 |
6 | accounts = Blueprint('accounts', __name__)
7 |
8 | accounts.add_url_rule('/login/', 'login', views.login, methods=['GET', 'POST'])
9 | accounts.add_url_rule('/logout/', 'logout', views.logout)
10 | accounts.add_url_rule('/registration/', 'register', views.register, methods=['GET', 'POST'])
11 | accounts.add_url_rule('/registration/su', 'register_su', views.register, defaults={'create_su':True}, methods=['GET', 'POST'])
12 | accounts.add_url_rule('/add-user/', 'add_user', views.add_user, methods=['GET', 'POST'])
13 | accounts.add_url_rule('/users/', view_func=views.Users.as_view('users'))
14 | accounts.add_url_rule('/users/edit/', view_func=views.User.as_view('edit_user'))
15 | accounts.add_url_rule('/su-users/', view_func=views.SuUsers.as_view('su_users'))
16 | accounts.add_url_rule('/su-users/edit/', view_func=views.SuUser.as_view('su_edit_user'))
17 | accounts.add_url_rule('/user/settings/', view_func=views.Profile.as_view('settings'))
18 | accounts.add_url_rule('/user/password/', view_func=views.Password.as_view('password'))
19 | accounts.add_url_rule('/user/email-confirm//', view_func=views.ConfirmEmail.as_view('confirm_email'))
20 | accounts.add_url_rule('/user/reset-password/', view_func=views.ResetPasswordRequest.as_view('reset_password_request'))
21 | accounts.add_url_rule('/user/reset-password//', view_func=views.ResetPassword.as_view('reset_password'))
22 |
23 | accounts.errorhandler(403)(errors.handle_forbidden)
--------------------------------------------------------------------------------
/app/OctBlog/__init__.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from flask import Flask
4 | # from flask.ext.mongoengine import MongoEngine
5 | # from flask.ext.login import LoginManager
6 | # from flask.ext.principal import Principal
7 | from flask_mongoengine import MongoEngine
8 | from flask_login import LoginManager
9 | from flask_principal import Principal
10 | from flask_moment import Moment
11 | from flask_mail import Mail
12 |
13 | from .config import config
14 |
15 | db = MongoEngine()
16 |
17 | login_manager = LoginManager()
18 | login_manager.session_protection = 'basic'
19 | login_manager.login_view = 'accounts.login'
20 |
21 | principals = Principal()
22 |
23 | mail = Mail()
24 |
25 | moment = Moment()
26 |
27 | def create_app(config_name):
28 | app = Flask(__name__,
29 | template_folder=config[config_name].TEMPLATE_PATH, static_folder=config[config_name].STATIC_PATH)
30 | app.config.from_object(config[config_name])
31 |
32 | config[config_name].init_app(app)
33 |
34 | db.init_app(app)
35 | login_manager.init_app(app)
36 | principals.init_app(app)
37 | mail.init_app(app)
38 | moment.init_app(app)
39 |
40 | from main.urls import main as main_blueprint, blog_admin as blog_admin_blueprint
41 | from accounts.urls import accounts as accounts_blueprint
42 | app.register_blueprint(main_blueprint)
43 | app.register_blueprint(blog_admin_blueprint, url_prefix='/admin')
44 | app.register_blueprint(accounts_blueprint, url_prefix='/accounts')
45 |
46 | return app
47 |
48 | app = create_app(os.getenv('config') or 'default')
--------------------------------------------------------------------------------
/app/templates/blog_admin/post_statistics.html:
--------------------------------------------------------------------------------
1 | {% extends "admin_base.html" %}
2 | {% block title %}Posts Statistics{% endblock %}
3 | {% block main %}
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | No.
12 | Title
13 | Publish
14 | Update
15 | Visited
16 | Verbose
17 |
18 |
19 |
20 | {% for post in posts.items %}
21 |
22 | {{ loop.index }}
23 | {{ post.post.title }}
24 | {{ post.post.pub_time.strftime('%Y-%m-%d %H:%M:%S') }}
25 | {{ post.post.update_time.strftime('%Y-%m-%d %H:%M:%S') }}
26 | {{ post.visit_count }}
27 | {{ post.verbose_count_base }}
28 |
29 | {% else %}
30 | No records yet
31 | {% endfor %}
32 |
33 |
34 | {% import '_pagination.html' as pagination %}
35 | {{ pagination.render_pagination(posts) }}
36 |
37 |
38 |
39 | {% endblock %}
40 |
--------------------------------------------------------------------------------
/app/templates/accounts/su_users.html:
--------------------------------------------------------------------------------
1 | {% extends 'admin_base.html' %}
2 |
3 | {% block title %}Users{% endblock %}
4 | {% block main %}
5 |
6 |
7 |
8 |
9 |
10 | No.
11 | Username
12 | Email
13 | Is Active
14 | Email Confirmed
15 | Is Superuser
16 | Actions
17 |
18 |
19 |
20 | {% for user in users %}
21 |
22 | {{ loop.index }}
23 | {{ user.username }}
24 | {{ user.email }}
25 | {{ user.is_active }}
26 | {{ user.is_email_confirmed }}
27 | {{ user.is_superuser }}
28 |
29 |
30 |
31 |
32 | {% else %}
33 | No user yet
34 | {% endfor %}
35 |
36 |
37 |
38 |
39 |
40 |
41 |
Filter By Role
42 |
43 |
44 | Not yet
45 |
46 |
47 |
48 | {% endblock %}
49 | {% block js %}
50 | {% endblock %}
--------------------------------------------------------------------------------
/app/templates/blog_admin/post_statistics_detail.html:
--------------------------------------------------------------------------------
1 | {% extends "admin_base.html" %}
2 | {% block title %}Posts Statistics{% endblock %}
3 | {% block main %}
4 |
5 |
6 |
7 | {{ post.title }}
8 |
9 |
10 |
Visits: {{ post_statistics.visit_count }}
11 |
Visits Verbose: {{ post_statistics.verbose_count_base }}
12 |
Detail: {{ post.title }}
13 |
14 |
15 |
Tracker
16 |
17 |
18 |
19 | No.
20 | IP
21 | Agent
22 | Update
23 |
24 |
25 |
26 | {% for tracker in trackers.items %}
27 |
28 | {{ loop.index }}
29 | {{ tracker.ip }}
30 | {{ tracker.user_agent }}
31 | {{ tracker.create_time.strftime('%Y-%m-%d %H:%M:%S') }}
32 |
33 | {% else %}
34 | No records yet
35 | {% endfor %}
36 |
37 |
38 | {% import '_pagination.html' as pagination %}
39 | {{ pagination.render_pagination(trackers) }}
40 |
41 |
42 |
43 | {% endblock %}
44 |
--------------------------------------------------------------------------------
/app/templates/accounts/password.html:
--------------------------------------------------------------------------------
1 | {% extends 'admin_base.html' %}
2 | {% block title %} Settings: Change Password {% endblock %}
3 |
4 | {% block main %}
5 |
13 |
14 |
15 |
16 |
17 | {% import "_form.html" as forms %}
18 |
22 |
23 |
24 |
25 | {% if user.is_email_confirmed %}
26 |
27 | Your email has been confirmed!
28 |
29 | {% else %}
30 |
42 | {% endif %}
43 |
44 |
45 |
46 |
47 | {% endblock %}
--------------------------------------------------------------------------------
/app/main/signals.py:
--------------------------------------------------------------------------------
1 | from __future__ import print_function
2 | from flask import request, current_app
3 | from blinker import Namespace
4 |
5 | from . import models, ext
6 | from OctBlog.config import OctBlogSettings
7 |
8 | search_engine_submit_urls = OctBlogSettings['search_engine_submit_urls']
9 |
10 | octblog_signals = Namespace()
11 | post_visited = octblog_signals.signal('post-visited')
12 | post_pubished = octblog_signals.signal('post-published')
13 |
14 | @post_visited.connect
15 | def on_post_visited(sender, post, **extra):
16 | tracker = models.Tracker()
17 | tracker.post = post
18 |
19 | # if request.headers.getlist("X-Forwarded-For"):
20 | # ip = request.headers.getlist("X-Forwarded-For")[0]
21 | # else:
22 | # ip = request.remote_addr
23 |
24 | proxy_list = request.headers.getlist('X-Forwarded-For')
25 | tracker.ip = request.remote_addr if not proxy_list else proxy_list[0]
26 |
27 | tracker.user_agent = request.headers.get('User-Agent')
28 | tracker.save()
29 |
30 | try:
31 | post_statistic = models.PostStatistics.objects.get(post=post)
32 | except models.PostStatistics.DoesNotExist:
33 | post_statistic = models.PostStatistics()
34 | post_statistic.post = post
35 |
36 | from random import randint
37 | post_statistic.verbose_count_base = randint(500, 5000)
38 |
39 | post_statistic.save()
40 |
41 | post_statistic.modify(inc__visit_count=1)
42 |
43 |
44 | @post_pubished.connect
45 | def on_post_pubished(sender, post, **extra):
46 | post_url = request.host + post.get_absolute_url()
47 | # print post_url
48 | baidu_url = search_engine_submit_urls['baidu']
49 | if baidu_url:
50 | # print 'Ready to post to baidu'
51 | res = ext.submit_url_to_baidu(baidu_url, post_url)
52 | print(res.status_code, res.text)
53 | else:
54 | print('Not ready to submit urls yet')
--------------------------------------------------------------------------------
/app/templates/blog_admin/su_export.html:
--------------------------------------------------------------------------------
1 | {% extends "admin_base.html" %}
2 | {% block title %}Export{% endblock %}
3 | {% block main %}
4 |
5 |
6 |
7 |
Export Content
8 |
9 |
44 |
45 |
46 |
47 | {% endblock %}
48 | {% block js %}
49 |
52 |
53 | {% endblock %}
--------------------------------------------------------------------------------
/app/accounts/misc.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | from threading import Thread
4 | from flask import current_app, render_template
5 | from flask_mail import Message
6 | from OctBlog import mail
7 |
8 | def send_user_confirm_mail(to, user, token):
9 | title = 'OctBlog confirm user email'
10 | msg = Message(title)
11 | msg.sender = current_app._get_current_object().config['MAIL_USERNAME']
12 | msg.recipients = [to]
13 |
14 | template_name = 'accounts/email/confirm.txt'
15 |
16 | msg.body = render_template(template_name, user=user, token=token)
17 | msg.html = render_template(template_name, user=user, token=token)
18 |
19 | mail.send(msg)
20 |
21 |
22 | def send_async_email(app, msg):
23 | with app.app_context():
24 | mail.send(msg)
25 |
26 |
27 | def send_email(to, subject, template_txt, template_html=None, **kwargs):
28 | app = current_app._get_current_object()
29 |
30 | msg = Message(subject)
31 | msg.sender = current_app._get_current_object().config['MAIL_USERNAME']
32 | msg.recipients = [to]
33 |
34 | if not template_html:
35 | template_html = template_txt
36 |
37 | msg.body = render_template(template_txt, **kwargs)
38 | msg.html = render_template(template_html, **kwargs)
39 | thr = Thread(target=send_async_email, args=[app, msg])
40 | thr.start()
41 | return thr
42 |
43 | def send_user_confirm_mail2(to, user, token):
44 | title = 'OctBlog confirm user email'
45 | template_txt = 'accounts/email/confirm.txt'
46 | template_html = 'accounts/email/confirm.html'
47 |
48 | return send_email(to, title, template_txt, template_html, user=user, token=token)
49 |
50 | def send_reset_password_mail(to, user, token):
51 | title = 'OctBlog reset password'
52 | template_txt = 'accounts/email/reset_password.txt'
53 | template_html = 'accounts/email/reset_password.html'
54 |
55 | return send_email(to, title, template_txt, template_html, user=user, token=token)
56 |
--------------------------------------------------------------------------------
/app/accounts/permissions.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | from flask import current_app
5 | # from flask.ext.principal import Permission, RoleNeed, UserNeed, identity_loaded
6 | # from flask.ext.login import current_user
7 | from flask_principal import Permission, RoleNeed, UserNeed, identity_loaded
8 | from flask_login import current_user
9 |
10 | # admin_need = RoleNeed('admin')
11 | # editor_need = RoleNeed('editor')
12 | # writer_need = RoleNeed('writer')
13 | # reader_need = RoleNeed('reader')
14 | su_need = RoleNeed('su')
15 |
16 | su_permission = Permission(su_need)
17 | admin_permission = Permission(RoleNeed('admin')).union(su_permission)
18 | editor_permission = Permission(RoleNeed('editor')).union(admin_permission)
19 | writer_permission = Permission(RoleNeed('writer')).union(editor_permission)
20 | reader_permission = Permission(RoleNeed('reader')).union(writer_permission)
21 |
22 |
23 | @identity_loaded.connect # Both of this and the following works
24 | # @identity_loaded.connect_via(current_app)
25 | def on_identity_loaded(sender, identity):
26 | # Set the identity user object
27 | identity.user = current_user
28 |
29 | # Add the UserNeed to the identity
30 | if hasattr(current_user, 'username'):
31 | identity.provides.add(UserNeed(current_user.username))
32 |
33 | # Assuming the User model has a list of roles, update the
34 | # identity with the roles that the user provides
35 | if hasattr(current_user, 'role'):
36 | # for role in current_user.roles:
37 | identity.provides.add(RoleNeed(current_user.role))
38 |
39 | # if current_user.is_superuser:
40 | if hasattr(current_user, 'is_superuser') and current_user.is_superuser:
41 | identity.provides.add(su_need)
42 | # return current_user.role
43 |
44 | identity.allow_su = su_permission.allows(identity)
45 | identity.allow_edit = editor_permission.allows(identity)
46 | identity.allow_admin = admin_permission.allows(identity)
47 | identity.allow_write = writer_permission.allows(identity)
--------------------------------------------------------------------------------
/app/tests/test_models.py:
--------------------------------------------------------------------------------
1 | # import unittest
2 | # from flask import current_app
3 | # from OctBlog import create_app, db
4 | # from accounts import models as accounts_models
5 | # from main import models as main_models
6 | #
7 | #
8 | # class ModelTestCase(unittest.TestCase):
9 | # def setUp(self):
10 | # self.app = create_app('testing')
11 | # self.app_context = self.app.app_context()
12 | # self.app_context.push()
13 | #
14 | # def tearDown(self):
15 | # db_name = current_app.config['MONGODB_SETTINGS']['DB']
16 | # db.connection.drop_database(db_name)
17 | # self.app_context.pop()
18 | #
19 | #
20 | # def test_db_is_testing(self):
21 | # self.assertTrue(current_app.config['MONGODB_SETTINGS'].get('DB')=='OctBlogTest')
22 | #
23 | #
24 | # def test_create_user(self):
25 | # user = accounts_models.User()
26 | #
27 | # user.username = 'octblog'
28 | # user.email = 'octblog@example.com'
29 | # user.password = 'octblog_password'
30 | # user.is_superuser = False
31 | # user.role = 'editor'
32 | # user.display_name = 'OctBlog'
33 | # user.biography = 'Octblog description'
34 | # user.homepage_url = 'http://blog.igevin.info'
35 | #
36 | # user.save()
37 | #
38 | # created_user = accounts_models.User.objects.get(username='octblog')
39 | #
40 | # self.assertTrue(created_user is not None and created_user.email=='octblog@example.com')
41 | #
42 | # def test_create_post(self):
43 | # post = main_models.Post()
44 | #
45 | # post.title = 'title'
46 | # post.slug = 'slug'
47 | # post.fix_slug = '1'
48 | # post.abstract = 'abstract'
49 | # post.raw = 'content'
50 | # user = accounts_models.User()
51 | # user.username='user'
52 | # user.password='password'
53 | # user.save()
54 | # post.author = user
55 | # post.category = 'category1'
56 | # post.tags = ['tag1']
57 | #
58 | # post.save()
59 | #
60 | # post = main_models.Post.objects.get(slug='slug')
61 | #
62 | # self.assertTrue(post is not None and post.title=='title')
63 |
--------------------------------------------------------------------------------
/app/templates/main/archive.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 |
3 | {% block title %}{{ blog_meta.name }}{% endblock %}
4 | {% block header %}
5 |
18 | {% endblock %}
19 |
20 | {% block main %}
21 |
22 |
23 |
24 |
25 | {% for post in posts.items %}
26 |
27 |
28 |
29 |
30 |
31 |
Abstract: {{ post.abstract }}
32 |
Author: {{ post.author }}
33 |
Date: {{ moment(post.pub_time).format('YYYY/MM/DD, h:mm a') }}
34 |
35 |
36 |
37 |
38 | {% endfor %}
39 |
40 |
41 |
42 | {% import '_pagination.html' as pagination %}
43 | {{ pagination.render_pagination(posts) }}
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | {% endblock %}
52 |
53 | {% block js %}
54 | {{ moment.include_moment(local_js="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.18.1/moment-with-locales.min.js") }}
55 |
62 | {% endblock %}
--------------------------------------------------------------------------------
/app/templates/blog_admin/widgets.html:
--------------------------------------------------------------------------------
1 | {% extends "admin_base.html" %}
2 | {% block title %}Widgets{% endblock %}
3 | {% block main %}
4 |
5 |
6 |
7 |
8 |
New Widget
9 |
10 |
11 |
12 | No.
13 | Title
14 | Priority
15 | Update Time
16 | Actions
17 |
18 |
19 |
20 | {% for widget in widgets %}
21 |
22 | {{ loop.index }}
23 |
24 | {{ widget.title }}
25 |
26 | {{ widget.priority }}
27 | {{ widget.update_time.strftime('%Y-%m-%d %H:%M:%S') }}
28 |
29 |
30 |  
31 |
32 |
33 |
34 | {% else %}
35 | No widgets yet
36 | {% endfor %}
37 |
38 |
39 |
40 |
41 |
42 |
43 | {% endblock %}
44 | {% block js %}
45 |
68 |
69 | {% endblock %}
--------------------------------------------------------------------------------
/app/templates/accounts/users.html:
--------------------------------------------------------------------------------
1 | {% extends 'admin_base.html' %}
2 |
3 | {% block title %}Users{% endblock %}
4 | {% block main %}
5 |
6 |
7 |
8 |
9 |
10 | No.
11 | Username
12 | Email
13 | Is Active
14 | Last Login
15 | Create Date
16 | Actions
17 |
18 |
19 |
20 | {% for user in users %}
21 |
22 | {{ loop.index }}
23 | {{ user.username }}
24 | {{ user.email }}
25 | {{ user.is_active }}
26 | {{ user.last_login.strftime('%Y/%m/%d %H:%M:%S') }}
27 | {{ user.create_time.strftime('%Y/%m/%d') }}
28 |
29 |
30 |  
31 |
32 |
33 |
34 | {% else %}
35 | No user yet
36 | {% endfor %}
37 |
38 |
39 |
40 |
41 |
42 |
43 |
Filter By Role
44 |
45 |
46 | Not yet
47 |
48 |
49 |
50 | {% endblock %}
51 | {% block js %}
52 |
77 | {% endblock %}
--------------------------------------------------------------------------------
/app/readme.md:
--------------------------------------------------------------------------------
1 | Welcome to OctBlog
2 | ====================
3 |
4 | >OctBlog is powered by Flask and MongoDB, here are some instructions on how to run it.
5 |
6 | ## How to run it ?
7 |
8 | ### Install requirements
9 |
10 | ```
11 | (sudo) pip install -r requirements.txt
12 | ```
13 |
14 | ### Create/update datebase
15 |
16 | MongoDB is flexible, migrating database is not necessary.
17 |
18 |
19 | ### Run OctBlog
20 |
21 | Run OctBlog with this command:
22 |
23 | ```bash
24 | python manage.py runserver
25 | ```
26 |
27 | Then you can visit the blog with url: `http://127.0.0.1:5000`
28 |
29 | If you want to customize `manage.py`, checkout [Flask-Script](https://flask-script.readthedocs.org/en/latest/)
30 |
31 | ### Get started with OctBlog
32 |
33 | #### 1\. Create a superuser to administrate OctBlog
34 |
35 | Visit the following url and create a superuser
36 |
37 | `http://127.0.0.1:5000/accounts/registration/su`
38 |
39 | If the url is forbidden, you need to modify your configurations to allow the creation.
40 |
41 | #### 2\. Administrate OctBlog
42 |
43 | The admin home is: `http://127.0.0.1:5000/admin`
44 |
45 | You will be redirected to login page if you haven't logged in
46 |
47 | #### 3\. Modify the default configurations
48 |
49 | You either change settings in `app/OctBlog/config.py` file, or set the environment variables defined in this file.
50 |
51 | **Setting environment variables is recommended, and once the configuration is changed, you need to restart the service.**
52 |
53 |
54 | ### OctBlog settings
55 |
56 | By default, OctBlog uses `dev` settings, `prd` is used in product environment. You can overwrite these settings or create your custom settings and switch to it.
57 |
58 | #### How to switch settings
59 |
60 | If you don't want to use the default settings, just set a settings environment vairable.
61 |
62 | I usually set the environment vairable in bash with `export` command. For example, if I want to run OctBlog in product environment, I will switch to prd settings like this:
63 |
64 | ```
65 | export config=prd
66 | ```
67 |
68 | ## Deploy OctBlog
69 |
70 | I recommend you to deploy OctBlog with `Ubuntu + nginx + gunicorn`.
71 |
72 | [Here](http://flask.pocoo.org/docs/0.10/deploying/wsgi-standalone/) is an instruction, and it is enough.
73 |
74 | *Deploying OctBlog with docker is another recommended option*
75 |
76 | ### What's more
77 |
78 | If you find a bug or want to add a new feature, just issue me.
79 |
80 | Want to contribute? Please fork OctBlog and pull request to me.
81 |
82 | I'm not good at frontend development, so I used a free bootstrap blog theme. If you can redesign the blog theme and admin interface, I'll appriciate your work very much!
83 |
--------------------------------------------------------------------------------
/app/main/forms.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | # from flask.ext.mongoengine.wtf import model_form
5 | from flask_mongoengine.wtf import model_form
6 | from flask_wtf import FlaskForm
7 | from wtforms import StringField, PasswordField, BooleanField, TextAreaField, HiddenField, RadioField, FileField, IntegerField
8 | from wtforms import widgets, ValidationError
9 | from wtforms.validators import Required, Length, Email, Regexp, EqualTo, URL, Optional
10 |
11 | from . import models
12 |
13 | class PostForm(FlaskForm):
14 | title = StringField('Title', validators=[Required()])
15 | slug = StringField('Slug', validators=[Required()])
16 | weight = IntegerField('Weight', default=10)
17 | raw = TextAreaField('Content')
18 | abstract = TextAreaField('Abstract')
19 | category = StringField('Category')
20 | tags_str = StringField('Tags')
21 | post_id = HiddenField('post_id')
22 | post_type = HiddenField('post_type')
23 | from_draft = HiddenField('from_draft')
24 |
25 | def validate_slug(self, field):
26 | if self.from_draft.data and self.from_draft.data == 'true':
27 | posts = models.Draft.objects.filter(slug=field.data)
28 | else:
29 | posts = models.Post.objects.filter(slug=field.data)
30 | if posts.count() > 0:
31 | if not self.post_id.data or str(posts[0].id) != self.post_id.data:
32 | raise ValidationError('slug already in use')
33 |
34 | SuPostForm = model_form(models.Post, exclude=['pub_time', 'update_time', 'content_html', 'category', 'tags', 'post_type'])
35 |
36 | class WidgetForm(FlaskForm):
37 | title = StringField('Title', validators=[Required()])
38 | content = TextAreaField('Content', validators=[Required()])
39 | content_type = RadioField('Content Type', choices=[('markdown', 'markdown'), ('html', 'html')], default='html')
40 | priority = IntegerField(default=1000000)
41 |
42 | class CommentForm(FlaskForm):
43 | email = StringField('* Email', validators=[Required(), Length(1,128), Email()])
44 | author = StringField('* Name', validators=[Required(), Length(1,128)])
45 | homepage = StringField('Homepage', validators=[URL(), Optional()])
46 | content = TextAreaField('* Comment markdown ', validators=[Required()])
47 | comment_id = HiddenField('comment_id')
48 |
49 | class SessionCommentForm(FlaskForm):
50 | email = HiddenField('* Email')
51 | author = HiddenField('* Name')
52 | homepage = HiddenField('Homepage')
53 | content = TextAreaField('* Comment', validators=[Required()])
54 | comment_id = HiddenField('comment_id')
55 |
56 | class ImportCommentForm(FlaskForm):
57 | content = TextAreaField('Content')
58 | json_file = FileField('Json File')
59 | import_format = RadioField('Import Format', choices=[('text', 'text'), ('file', 'file')], default='text')
--------------------------------------------------------------------------------
/app/templates/blog_admin/posts.html:
--------------------------------------------------------------------------------
1 | {% extends "admin_base.html" %}
2 | {% block title %}{{ post_type|capitalize }}s {% if is_draft %} -- draft {% endif %}{% endblock %}
3 | {% block main %}
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | No.
12 | Title
13 | Abstract
14 | Author
15 | Publish
16 | Update
17 | Weight
18 | Actions
19 |
20 |
21 |
22 | {% for post in posts.items %}
23 |
24 | {{ loop.index }}
25 |
26 | {{ post.title }}
27 |
28 | {{ post.abstract }}
29 | {{ post.author.username }}
30 | {{ post.pub_time.strftime('%Y-%m-%d %H:%M:%S') }}
31 | {{ post.update_time.strftime('%Y-%m-%d %H:%M:%S') }}
32 | {{ post.weight }}
33 |
34 |
35 |  
36 |
37 |
38 |
39 | {% else %}
40 | No {{ post_type }}s yet
41 | {% endfor %}
42 |
43 |
44 | {% import '_pagination.html' as pagination %}
45 | {{ pagination.render_pagination(posts) }}
46 |
47 |
48 |
49 | {% endblock %}
50 | {% block js %}
51 |
75 | {% endblock %}
--------------------------------------------------------------------------------
/app/templates/blog_admin/widget.html:
--------------------------------------------------------------------------------
1 | {% extends "admin_base.html" %}
2 |
3 | {% block title %}
4 | Edit
5 | {% endblock %}
6 |
7 | {% block css %}
8 |
9 | {% endblock %}
10 |
11 | {% block main %}
12 |
13 |
28 |
29 |
Edit
30 |
31 | {% import "_form.html" as forms %}
32 |
60 |
61 |
62 | {% endblock %}
63 |
64 | {% block js %}
65 |
66 |
67 |
68 |
90 | {% endblock %}
--------------------------------------------------------------------------------
/app/static/css/share.min.css:
--------------------------------------------------------------------------------
1 | @font-face{font-family:"iconfont";src:url("../fonts/iconfont.eot");src:url("../fonts/iconfont.eot?#iefix") format("embedded-opentype"),url("../fonts/iconfont.woff") format("woff"),url("../fonts/iconfont.ttf") format("truetype"),url("../fonts/iconfont.svg#iconfont") format("svg")}.iconfont{font-family:"iconfont" !important;font-size:16px;font-style:normal;-webkit-font-smoothing:antialiased;-webkit-text-stroke-width:0.2px;-moz-osx-font-smoothing:grayscale}.icon-tencent:before{content:"\e607"}.icon-qq:before{content:"\e601"}.icon-weibo:before{content:"\e602"}.icon-linkedin:before{content:"\e600"}.icon-wechat:before{content:"\e603"}.icon-douban:before{content:"\e604"}.icon-qzone:before{content:"\e606"}.icon-diandian:before{content:"\e608"}.icon-facebook:before{content:"\e609"}.icon-google:before{content:"\e60b"}.icon-twitter:before{content:"\e60c"}.share-component a{position:relative;text-decoration:none;margin:3px;display:inline-block}.share-component .iconfont{position:relative;display:inline-block;width:32px;height:32px;font-size:20px;border-radius:50%;line-height:32px;border:1px solid #eee;text-align:center;transition:background 0.6s ease-out 0s}.share-component .iconfont:hover{background:#f4f4f4;color:#fff}.share-component .icon-weibo{line-height:28px;color:#E6162D;border-color:#E6162D}.share-component .icon-weibo:hover{background:#E6162D}.share-component .icon-tencent{line-height:28px;color:#56b6e7;border-color:#56b6e7}.share-component .icon-tencent:hover{background:#56b6e7}.share-component .icon-qq{line-height:28px;color:#56b6e7;border-color:#56b6e7}.share-component .icon-qq:hover{background:#56b6e7}.share-component .icon-qzone{line-height:29px;color:#ffe21f;border-color:#ffe21f}.share-component .icon-qzone:hover{background:#ffe21f}.share-component .icon-douban{line-height:28px;color:#33b045;border-color:#33b045}.share-component .icon-douban:hover{background:#33b045}.share-component .icon-linkedin{line-height:28px;color:#0077B5;border-color:#0077B5}.share-component .icon-linkedin:hover{background:#0077B5}.share-component .icon-facebook{color:#44619D;border-color:#44619D}.share-component .icon-facebook:hover{background:#44619D}.share-component .icon-google{color:#db4437;border-color:#db4437}.share-component .icon-google:hover{background:#db4437}.share-component .icon-twitter{color:#55acee;border-color:#55acee}.share-component .icon-twitter:hover{background:#55acee}.share-component .icon-diandian{color:#307DCA;border-color:#307DCA}.share-component .icon-diandian:hover{background:#307DCA}.share-component .icon-wechat{line-height:28px;position:relative;color:#7bc549;border-color:#7bc549}.share-component .icon-wechat:hover{background:#7bc549}.share-component .icon-wechat .wechat-qrcode{opacity:0;filter:alpha(opacity=0);visibility:hidden;position:absolute;z-index:9;top:-205px;left:-84px;width:200px;height:192px;color:#666;font-size:12px;text-align:center;background-color:#fff;box-shadow:0 2px 10px #aaa;transition:all 200ms;-webkit-tansition:all 350ms;-moz-transition:all 350ms}.share-component .icon-wechat .wechat-qrcode h4{font-weight:normal;height:26px;line-height:26px;font-size:12px;background-color:#f3f3f3;margin:0;padding:0;color:#777}.share-component .icon-wechat .wechat-qrcode .qrcode{width:105px;margin:10px auto}.share-component .icon-wechat .wechat-qrcode .help p{font-weight:normal;line-height:16px;padding:0;margin:0}.share-component .icon-wechat .wechat-qrcode:after{content:'';position:absolute;left:50%;margin-left:-6px;bottom:-13px;width:0;height:0;border-width:8px 6px 6px 6px;border-style:solid;border-color:#fff transparent transparent transparent}.share-component .icon-wechat:hover .wechat-qrcode{opacity:1;filter:alpha(opacity=100);visibility:visible}
2 |
--------------------------------------------------------------------------------
/app/templates/blog_admin/su_posts.html:
--------------------------------------------------------------------------------
1 | {% extends "admin_base.html" %}
2 | {% block title %}All Posts of all types{% endblock %}
3 | {% block main %}
4 |
5 |
6 |
7 |
All
8 | {% for post_type in post_types %}
9 |
10 | {% if post_type == cur_type %}
11 |
12 | {{ post_type }}
13 |
14 |
15 |
16 | {% else %}
17 |
18 | {{ post_type }}
19 |
20 |
21 | {% endif %}
22 |
23 |
24 |
25 | {% endfor %}
26 |
27 |
28 |
29 |
30 | No.
31 | Title
32 | Abstract
33 | Author
34 | Publish Date
35 | Update Date
36 | PostType
37 | Actions
38 |
39 |
40 |
41 | {% for post in posts.items %}
42 |
43 | {{ loop.index }}
44 |
45 | {{ post.title }}
46 |
47 | {{ post.abstract }}
48 | {{ post.author.username }}
49 | {{ post.pub_time.strftime('%Y-%m-%d %H:%M:%S') }}
50 | {{ post.update_time.strftime('%Y-%m-%d %H:%M:%S') }}
51 | {{ post.post_type }}
52 |
53 |
54 |  
55 |
56 |
57 |
58 | {% else %}
59 | No posts yet
60 | {% endfor %}
61 |
62 |
63 |
64 | {% import '_pagination.html' as pagination %}
65 | {{ pagination.render_pagination(posts) }}
66 |
67 |
68 |
69 |
70 | {% endblock %}
71 | {% block js %}
72 |
102 |
103 | {% endblock %}
--------------------------------------------------------------------------------
/app/templates/blog_admin/su_post.html:
--------------------------------------------------------------------------------
1 | {% extends "admin_base.html" %}
2 |
3 | {% block title %}
4 | Edit
5 | {% endblock %}
6 |
7 | {% block css %}
8 |
9 | {% endblock %}
10 |
11 | {% block main %}
12 |
13 |
28 |
29 |
Edit
30 |
31 | {% import "_form.html" as forms %}
32 |
67 |
68 |
69 | {% endblock %}
70 |
71 | {% block js %}
72 |
73 |
74 |
75 |
108 | {% endblock %}
--------------------------------------------------------------------------------
/app/accounts/models.py:
--------------------------------------------------------------------------------
1 | from __future__ import unicode_literals
2 | import datetime
3 | from flask import current_app
4 | from flask_login import UserMixin
5 | from werkzeug.security import generate_password_hash, check_password_hash
6 | from itsdangerous import TimedJSONWebSignatureSerializer
7 | from OctBlog import db, login_manager
8 |
9 | # ROLES = ('admin', 'editor', 'writer', 'reader')
10 | ROLES = (('admin', 'admin'),
11 | ('editor', 'editor'),
12 | ('writer', 'writer'),
13 | ('reader', 'reader'))
14 | SOCIAL_NETWORKS = {
15 | 'weibo': {'fa_icon': 'fa fa-weibo', 'url': None},
16 | 'weixin': {'fa_icon': 'fa fa-weixin', 'url': None},
17 | 'twitter': {'fa_icon': 'fa fa fa-twitter', 'url': None},
18 | 'github': {'fa_icon': 'fa fa-github', 'url': None},
19 | 'facebook': {'fa_icon': 'fa fa-facebook', 'url': None},
20 | 'linkedin': {'fa_icon': 'fa fa-linkedin', 'url': None},
21 | }
22 |
23 | class User(UserMixin, db.Document):
24 | username = db.StringField(max_length=255, required=True)
25 | email = db.EmailField(max_length=255)
26 | password_hash = db.StringField(required=True)
27 | create_time = db.DateTimeField(default=datetime.datetime.now, required=True)
28 | last_login = db.DateTimeField(default=datetime.datetime.now, required=True)
29 | is_email_confirmed = db.BooleanField(default=False)
30 | # is_active = db.BooleanField(default=True)
31 | is_superuser = db.BooleanField(default=False)
32 | role = db.StringField(max_length=32, default='reader', choices=ROLES)
33 | display_name = db.StringField(max_length=255, default=username)
34 | biography = db.StringField()
35 | social_networks = db.DictField(default=SOCIAL_NETWORKS)
36 | homepage_url = db.URLField()
37 |
38 | confirm_email_sent_time = db.DateTimeField()
39 |
40 | @property
41 | def password(self):
42 | raise AttributeError('password is not a readle attribute')
43 |
44 | @password.setter
45 | def password(self, password):
46 | self.password_hash = generate_password_hash(password)
47 |
48 | def verify_password(self, password):
49 | return check_password_hash(self.password_hash, password)
50 |
51 | def generate_confirmation_token(self, expiration=3600):
52 | serializer = TimedJSONWebSignatureSerializer(current_app.config['SECRET_KEY'], expiration)
53 | return serializer.dumps({'confirm':self.username})
54 |
55 | def confirm_email(self, token, expiration=3600):
56 | s = TimedJSONWebSignatureSerializer(current_app.config['SECRET_KEY'])
57 | try:
58 | data = s.loads(token)
59 | except Exception:
60 | return False
61 | if data.get('confirm') != self.username:
62 | return False
63 | self.is_email_confirmed = True
64 | self.save()
65 | return True
66 |
67 | def generate_reset_token(self, expiration=3600):
68 | serializer = TimedJSONWebSignatureSerializer(current_app.config['SECRET_KEY'], expiration)
69 | return serializer.dumps({'reset': self.username})
70 |
71 | @staticmethod
72 | def reset_password(token, new_password):
73 | serializer = TimedJSONWebSignatureSerializer(current_app.config['SECRET_KEY'])
74 | try:
75 | data = serializer.loads(token)
76 | except:
77 | return False
78 |
79 | try:
80 | user = User.objects.get(username=data.get('reset'))
81 | except Exception:
82 | return False
83 |
84 | user.password = new_password
85 | user.save()
86 | return True
87 |
88 | def get_id(self):
89 | try:
90 | # return unicode(self.username)
91 | return self.username
92 |
93 | except AttributeError:
94 | raise NotImplementedError('No `username` attribute - override `get_id`')
95 |
96 | def __unicode__(self):
97 | return self.username
98 |
99 |
100 |
101 | @login_manager.user_loader
102 | def load_user(username):
103 | try:
104 | user = User.objects.get(username=username)
105 | except User.DoesNotExist:
106 | user = None
107 | return user
108 |
109 |
--------------------------------------------------------------------------------
/app/templates/blog_admin/index.html:
--------------------------------------------------------------------------------
1 | {% extends 'admin_base.html' %}
2 |
3 | {%block main %}
4 |
5 |
6 |
Blog Meta Data
7 |
8 | Blog Name:
9 | {{ blog_meta.name }}
10 |
11 |
12 | Blog Subtitle:
13 | {{ blog_meta.subtitle }}
14 |
15 |
16 | Blog Description:
17 | {{ blog_meta.description }}
18 |
19 |
20 | Blog Owner:
21 | {{ blog_meta.owner }}
22 |
23 |
24 | Blog Keywords:
25 | {{ blog_meta.keywords }}
26 |
27 |
28 |
29 |
30 | About User
31 |
32 |
33 |
34 |
35 | Username Name:
36 | {{ user.username }}
37 |
38 |
39 | Display Name:
40 | {{ user.display_name }}
41 |
42 |
43 | Biography:
44 | {{ user.biography }}
45 |
46 |
47 | Social Network:
48 |
49 | {% if user.social_networks['github']['url'] %}
50 |
51 | {% endif %}
52 | {% if user.social_networks['twitter']['url'] %}
53 |
54 | {% endif %}
55 | {% if user.social_networks['weibo']['url'] %}
56 |
57 | {% endif %}
58 | {% if user.social_networks['facebook']['url'] %}
59 |
60 | {% endif %}
61 | {% if user.social_networks['linkedin']['url'] %}
62 |
63 | {% endif %}
64 | {% if user.social_networks['weixin']['url'] %}
65 |
66 |
67 |
68 |
69 |
70 |
71 | {% endif %}
72 |
73 |
74 |
75 | Homepage:
76 |
77 | {% if user.homepage_url %}
78 | {{ user.homepage_url }}
79 | {% else %}
80 | Not yet
81 | {% endif %}
82 |
83 |
84 |
85 |
86 | {% endblock %}
87 |
88 | {% block js %}
89 |
100 | {% endblock %}
--------------------------------------------------------------------------------
/app/accounts/forms.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | from flask_wtf import FlaskForm
5 | from wtforms import StringField, PasswordField, BooleanField, SelectField, SubmitField, ValidationError
6 | from wtforms.validators import Required, Length, Email, Regexp, EqualTo, URL, Optional, DataRequired
7 | # from flask.ext.login import current_user
8 | # from flask.ext.mongoengine.wtf import model_form
9 | from flask_login import current_user
10 | from flask_mongoengine.wtf import model_form
11 |
12 | from . import models
13 |
14 | class LoginForm(FlaskForm):
15 | username = StringField()
16 | password = PasswordField()
17 | remember_me = BooleanField('Keep me logged in')
18 |
19 | class RegistrationForm(FlaskForm):
20 | username = StringField('Username', validators=[Required(), Length(1,64),
21 | Regexp('^[A-Za-z0-9_.]*$', 0, 'Usernames must have only letters, numbers dots or underscores')])
22 | email = StringField('Email', validators=[Required(), Length(1,128), Email()])
23 | password = PasswordField('Password', validators=[Required(), EqualTo('password2', message='Passwords must match')])
24 | password2 = PasswordField('Confirm password', validators=[Required()])
25 |
26 | def validate_username(self, field):
27 | if models.User.objects.filter(username=field.data).count() > 0:
28 | raise ValidationError('Username already in use')
29 |
30 | def validate_email(self, field):
31 | if models.User.objects.filter(email=field.data).count() > 0:
32 | raise ValidationError('Email already in registered')
33 |
34 | class UserForm(FlaskForm):
35 | email = StringField('Email', validators=[Required(), Length(1,128), Email()])
36 | # is_active = BooleanField('Is activie')
37 | # is_superuser = BooleanField('Is superuser')
38 | role = SelectField('Role', choices=models.ROLES)
39 |
40 | # SuUserForm = model_form(models.User, exclude=['create_time', 'last_login', 'password_hash'])
41 |
42 | class SuUserForm(FlaskForm):
43 | email = StringField('Email', validators=[Required(), Length(1,128), Email()])
44 | is_superuser = BooleanField('Is superuser')
45 | is_email_confirmed = BooleanField('Is Email Confirmed')
46 | role = SelectField('Role', choices=models.ROLES)
47 | display_name = StringField('Display Name', validators=[Length(1,128)])
48 | biography = StringField('Biograpyh')
49 | homepage_url = StringField('Homepage', validators=[URL(), Optional()])
50 | weibo = StringField('Weibo', validators=[URL(), Optional()])
51 | weixin = StringField('Weixin', validators=[Optional(), URL()])
52 | twitter = StringField('Twitter', validators=[URL(), Optional()])
53 | github = StringField('github', validators=[URL(), Optional()])
54 | facebook = StringField('Facebook', validators=[URL(), Optional()])
55 | linkedin = StringField('Linkedin', validators=[URL(), Optional()])
56 |
57 | # ProfileForm = model_form(models.User, exclude=['username', 'password_hash', 'create_time', 'last_login',
58 | # 'is_email_confirmed', 'is_superuser', 'role'])
59 | class ProfileForm(FlaskForm):
60 | email = StringField('Email', validators=[Required(), Length(1,128), Email()])
61 | display_name = StringField('Display Name', validators=[Length(1,128)])
62 | biography = StringField('Biograpyh')
63 | homepage_url = StringField('Homepage', validators=[URL(), Optional()])
64 | weibo = StringField('Weibo', validators=[URL(), Optional()])
65 | weixin = StringField('Weixin', validators=[Optional(), URL()])
66 | twitter = StringField('Twitter', validators=[URL(), Optional()])
67 | github = StringField('github', validators=[URL(), Optional()])
68 | facebook = StringField('Facebook', validators=[URL(), Optional()])
69 | linkedin = StringField('Linkedin', validators=[URL(), Optional()])
70 |
71 | class PasswordForm(FlaskForm):
72 | current_password = PasswordField('Current Password', validators=[Required()])
73 | new_password = PasswordField('New Password', validators=[Required(), EqualTo('password2', message='Passwords must match')])
74 | password2 = PasswordField('Confirm password', validators=[Required()])
75 |
76 | def validate_current_password(self, field):
77 | if not current_user.verify_password(field.data):
78 | raise ValidationError('Current password is wrong')
79 |
80 | class PasswordResetRequestForm(FlaskForm):
81 | email = StringField('Email', validators=[Required(), Length(1,128), Email()])
82 |
83 | class PasswordResetForm(FlaskForm):
84 | password = PasswordField('New Password', validators=[DataRequired(), EqualTo('password2', message='Passwords must match')])
85 | password2 = PasswordField('Confirm password', validators=[DataRequired()])
86 |
--------------------------------------------------------------------------------
/app/main/urls.py:
--------------------------------------------------------------------------------
1 | from flask import Blueprint, g
2 |
3 | from . import views, admin_views, errors
4 | from OctBlog.config import OctBlogSettings
5 |
6 | main = Blueprint('main', __name__)
7 |
8 | main.add_url_rule('/', 'index', views.list_posts)
9 | main.add_url_rule('/posts/', 'posts', views.list_posts)
10 | main.add_url_rule('/wechats/', 'wechats', views.list_wechats)
11 | main.add_url_rule('/posts//', 'post_detail', views.post_detail, methods=['GET', 'POST'])
12 | main.add_url_rule('/post//', 'post_detail_fix', views.post_detail, defaults={'fix':True})
13 | main.add_url_rule('/posts//preview/', 'post_preview', views.post_detail, defaults={'is_preview':True})
14 | main.add_url_rule('/posts///preview/', 'post_general_preview', views.post_detail_general)
15 | main.add_url_rule('/pages//', 'page_detail', views.post_detail, defaults={'post_type':'page'}, methods=['GET', 'POST'])
16 | main.add_url_rule('/wechats//', 'wechat_detail', views.post_detail, defaults={'post_type':'wechat'})
17 | main.add_url_rule('/archive/', 'archive', views.archive)
18 | main.add_url_rule('/users//', 'author_detail', views.author_detail)
19 | main.add_url_rule('/atom/', 'recent_feed', views.recent_feed)
20 | main.add_url_rule('/sitemap.xml/', 'sitemap', views.sitemap)
21 | main.errorhandler(404)(errors.page_not_found)
22 | main.errorhandler(401)(errors.handle_unauthorized)
23 | main.add_url_rule('/', 'handle_unmatchable', errors.handle_unmatchable)
24 |
25 |
26 | blog_admin = Blueprint('blog_admin', __name__)
27 |
28 | blog_admin.add_url_rule('/', view_func=admin_views.AdminIndex.as_view('index'))
29 |
30 | blog_admin.add_url_rule('/posts/', view_func=admin_views.PostsList.as_view('posts'))
31 | blog_admin.add_url_rule('/posts/draft/', view_func=admin_views.DraftList.as_view('drafts'))
32 | blog_admin.add_url_rule('/new-post/', view_func=admin_views.Post.as_view('new_post'))
33 | blog_admin.add_url_rule('/posts//', view_func=admin_views.Post.as_view('edit_post'))
34 |
35 | blog_admin.add_url_rule('/pages/', view_func=admin_views.PostsList.as_view('pages'), defaults={'post_type':'page'})
36 | blog_admin.add_url_rule('/pages/draft/', view_func=admin_views.DraftList.as_view('page_drafts'), defaults={'post_type':'page'})
37 | blog_admin.add_url_rule('/new-page/', view_func=admin_views.Post.as_view('new_page'), defaults={'post_type':'page'})
38 |
39 | blog_admin.add_url_rule('/wechats/', view_func=admin_views.PostsList.as_view('wechats'), defaults={'post_type':'wechat'})
40 | blog_admin.add_url_rule('/wechats/draft/', view_func=admin_views.DraftList.as_view('wechat_drafts'), defaults={'post_type':'wechat'})
41 | blog_admin.add_url_rule('/new-wechat/', view_func=admin_views.Post.as_view('new_wechat'), defaults={'post_type':'wechat'})
42 |
43 | blog_admin.add_url_rule('/posts/statistics/', view_func=admin_views.PostStatisticList.as_view('post_statistics'))
44 | blog_admin.add_url_rule('/posts/statistics//', view_func=admin_views.PostStatisticDetail.as_view('post_statistics_detail'))
45 |
46 | blog_admin.add_url_rule('/posts/comments/', view_func=admin_views.Comment.as_view('comments'))
47 | blog_admin.add_url_rule('/posts/comments/approved/', view_func=admin_views.Comment.as_view('comments_approved'), defaults={'status':'approved'})
48 | blog_admin.add_url_rule('/posts/comments/spam/', view_func=admin_views.Comment.as_view('comments_spam'), defaults={'status':'spam'})
49 | blog_admin.add_url_rule('/posts/comments//action/', view_func=admin_views.Comment.as_view('comment_action'))
50 | blog_admin.add_url_rule('/posts/comments/import/', view_func=admin_views.ImportCommentView.as_view('import_comments'))
51 | blog_admin.add_url_rule('/posts/comments/action/', view_func=admin_views.Comments.as_view('comments_action'))
52 |
53 | blog_admin.add_url_rule('/su/posts/', view_func=admin_views.SuPostsList.as_view('su_posts'))
54 | blog_admin.add_url_rule('/su/posts//', view_func=admin_views.SuPost.as_view('su_post_edit'))
55 | blog_admin.add_url_rule('/su/widgets/', view_func=admin_views.WidgetList.as_view('su_widgets'))
56 | blog_admin.add_url_rule('/su/widgets/create/', view_func=admin_views.Widget.as_view('su_widget'))
57 | blog_admin.add_url_rule('/su/widgets//', view_func=admin_views.Widget.as_view('su_widget_edit'))
58 | blog_admin.add_url_rule('/su/export/', view_func=admin_views.SuExportView.as_view('su_export'))
59 |
60 | blog_admin.errorhandler(404)(errors.admin_page_not_found)
61 | blog_admin.errorhandler(401)(errors.handle_unauthorized)
62 | blog_admin.errorhandler(403)(errors.handle_forbidden)
63 |
64 | ALLOW_WECHAT_PORT = OctBlogSettings['allow_wechat_port']
65 |
66 | @blog_admin.before_app_request
67 | def before_request():
68 | g.allow_wechat_port = ALLOW_WECHAT_PORT
69 |
70 |
--------------------------------------------------------------------------------
/app/templates/main/wechat_detail.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 | {% block title %} {{ post.title }} {% endblock %}
3 |
4 | {% block custom_head %}
5 |
6 | {% endblock %}
7 |
8 | {% block header %}
9 |
10 |
26 | {% endblock %}
27 | {% block main %}
28 |
29 |
30 |
31 |
32 |
33 | {{post.content_html|safe}}
34 |
35 |
36 |
37 | {% if not post.post_type=='page' %}
38 | {% if display_copyright %}
39 |
40 |
41 |
42 |
{{ copyright_msg }}
43 |
44 |
45 |
46 |
47 | {% endif %}
48 |
49 | {% if allow_share_article %}
50 |
56 | {% endif %}
57 |
58 | {% if allow_donate %}
59 |
60 |
61 |
62 |
{{ donation_msg }}
63 |
64 | |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 | {% endif %}
77 |
78 | {% if display_wechat %}
79 |
80 |
81 |
82 |
{{ wechat_msg }}
83 |
84 |
85 |
86 |
87 | {% endif %}
88 |
89 | {% endif %}
90 |
91 |
92 |
93 |
94 |
95 | {% if allow_comment %}
96 |
97 |
98 |
99 |
100 | {{ comment_html|safe }}
101 |
102 |
103 |
104 |
105 | {% endif %}
106 |
107 | {% endblock %}
108 |
109 | {% block js %}
110 | {{ moment.include_moment(local_js="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.18.1/moment-with-locales.min.js") }}
111 |
112 |
124 | {% endblock %}
125 |
--------------------------------------------------------------------------------
/app/templates/main/wechat_list.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 |
3 | {% block title %}{{ blog_meta.name }}{% endblock %}
4 | {% block header %}
5 |
18 | {% endblock %}
19 |
20 | {% block main %}
21 |
22 |
23 |
24 | {% for post in posts.items %}
25 |
26 |
27 |
28 | {{ post.title }}
29 |
30 |
31 |
32 | {{ post.abstract }}
33 |
34 |
Posted by {{ post.author.display_name }} on {{ moment(post.pub_time).format('YYYY/MM/DD, h:mm a') }}
35 |
Tags:
36 | {% for tag in post.tags %} {{ tag }} {% endfor %}
37 |
38 |
39 | {% if not loop.last %}
40 |
41 | {% endif %}
42 | {% else %}
43 |
44 | No articles found here
45 |
46 | {% endfor %}
47 |
48 |
62 |
63 |
64 |
65 |
109 |
110 |
111 |
112 |
113 | {% endblock %}
114 |
115 | {% block js %}
116 | {{ moment.include_moment(local_js="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.18.1/moment-with-locales.min.js") }}
117 |
124 | {% endblock %}
--------------------------------------------------------------------------------
/app/templates/main/post.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 | {% block title %} {{ post.title }} {% endblock %}
3 |
4 | {% block custom_head %}
5 |
6 | {% endblock %}
7 |
8 | {% block header %}
9 |
10 |
36 | {% endblock %}
37 | {% block main %}
38 |
39 |
40 |
41 |
42 |
43 | {{post.content_html|safe}}
44 |
45 |
46 |
47 | {% if not post.post_type=='page' %}
48 | {% if display_copyright %}
49 |
50 |
51 |
52 |
{{ copyright_msg }}
53 |
54 |
55 |
56 |
57 | {% endif %}
58 |
59 | {% if allow_share_article %}
60 |
66 | {% endif %}
67 |
68 | {% if allow_donate %}
69 |
70 |
71 |
72 |
{{ donation_msg }}
73 |
74 | |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 | {% endif %}
87 |
88 | {% if display_wechat %}
89 |
90 |
91 |
92 |
{{ wechat_msg }}
93 |
94 |
95 |
96 |
97 | {% endif %}
98 |
99 | {% endif %}
100 |
101 |
102 |
103 |
104 |
105 | {% if allow_comment %}
106 |
107 |
108 |
109 |
110 | {{ comment_html|safe }}
111 |
112 |
113 |
114 |
115 | {% endif %}
116 |
117 | {% endblock %}
118 |
119 | {% block js %}
120 | {{ moment.include_moment(local_js="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.18.1/moment-with-locales.min.js") }}
121 |
122 |
134 | {% endblock %}
--------------------------------------------------------------------------------
/app/templates/blog_admin/comments.html:
--------------------------------------------------------------------------------
1 | {% extends 'admin_base.html' %}
2 | {% block title %}Comments{% endblock %}
3 | {% block main %}
4 |
5 |
Comments
6 |
7 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | Author
38 | Comment
39 | Action
40 |
41 |
42 | {% for comment in comments.items %}
43 |
44 |
45 |
46 | {{ comment.author }}
47 |
48 |
49 |
50 | {% if comment.homepage %}
51 |
52 | {% endif %}
53 |
54 |
55 |
56 |
57 | {{ comment.html_content|safe }}
58 |
59 |
60 |
61 |
62 | {{ comment.post_title }}
63 |
64 |
65 |
66 | {{ comment.update_time.strftime('%Y-%m-%d %H:%M:%S') }}
67 |
68 |
69 |
70 | {% if status!='approved' %}
71 | Approve
72 | {% endif %}
73 | Delete
74 |
75 |
76 | {% else %}
77 | No records yet!
78 | {% endfor %}
79 |
80 |
81 |
82 |
83 | {% import '_pagination.html' as pagination %}
84 | {{ pagination.render_pagination(comments) }}
85 |
86 | {% endblock %}
87 |
88 | {% block js %}
89 |
151 | {% endblock %}
152 |
--------------------------------------------------------------------------------
/app/templates/main/author.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 |
3 | {% block title %} Author | {{ user.display_name }} {% endblock %}
4 | {% block header %}
5 |
6 |
19 | {% endblock %}
20 | {% block main %}
21 |
22 |
23 |
24 |
25 |
26 |
About {{ user.display_name }}
27 |
28 |
29 | Username:
30 | {{ user.username }}
31 |
32 |
33 | Biography:
34 | {{ user.biography }}
35 |
36 |
37 | Social Network:
38 |
39 | {% if user.social_networks['github']['url'] %}
40 |
41 | {% endif %}
42 | {% if user.social_networks['twitter']['url'] %}
43 |
44 | {% endif %}
45 | {% if user.social_networks['weibo']['url'] %}
46 |
47 | {% endif %}
48 | {% if user.social_networks['facebook']['url'] %}
49 |
50 | {% endif %}
51 | {% if user.social_networks['linkedin']['url'] %}
52 |
53 | {% endif %}
54 | {% if user.social_networks['weixin']['url'] %}
55 |
56 |
57 |
58 |
59 |
60 |
61 | {% endif %}
62 |
63 |
64 |
65 | Homepage:
66 |
67 | {% if user.homepage_url %}
68 | {{ user.homepage_url }}
69 | {% else %}
70 | Not yet
71 | {% endif %}
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
Articles
82 |
83 | {% for post in posts.items %}
84 | {{ post.title }}
85 | {% else %}
86 | No articles found here
87 | {% endfor %}
88 |
89 |
90 |
100 |
101 |
102 |
103 |
104 |
105 | {% endblock %}
106 |
107 | {% block js %}
108 |
119 | {% endblock %}
120 |
--------------------------------------------------------------------------------
/app/templates/main/index.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 |
3 | {% block title %}{{ blog_meta.name }}{% endblock %}
4 | {% block header %}
5 |
6 |
19 | {% endblock %}
20 |
21 | {% block main %}
22 |
23 |
24 |
25 | {% for post in posts.items %}
26 |
27 |
28 |
29 | {{ post.title }}
30 |
31 |
32 |
33 | {{ post.abstract }}
34 |
35 |
Posted by {{ post.author.display_name }} on {{ moment(post.pub_time).format('YYYY/MM/DD, h:mm a') }}
36 |
category: {{ post.category }}
37 |
Tags:
38 | {% for tag in post.tags %} {{ tag }} {% endfor %}
39 |
40 |
41 | {% if not loop.last %}
42 |
43 | {% endif %}
44 | {% else %}
45 |
46 | No articles found here
47 |
48 | {% endfor %}
49 |
50 |
65 |
66 |
67 |
68 |
128 |
129 |
130 |
131 |
132 | {% endblock %}
133 |
134 | {% block js %}
135 | {{ moment.include_moment(local_js="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.18.1/moment-with-locales.min.js") }}
136 |
143 | {% endblock %}
--------------------------------------------------------------------------------
/app/OctBlog/config.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | from __future__ import unicode_literals
4 | import os, sys, datetime
5 |
6 | def get_env_value(key, default_value=''):
7 | if sys.version_info < (3, 0):
8 | return os.environ.get(key, default_value).decode('utf8')
9 | else:
10 | return os.environ.get(key, default_value)
11 |
12 | OctBlogSettings = {
13 | 'post_types': ('post', 'page'), # deprecated
14 | 'allow_registration': os.environ.get('allow_registration', 'false').lower() == 'true',
15 | 'allow_su_creation': os.environ.get('allow_su_creation', 'false').lower() == 'true',
16 | 'allow_donate': os.environ.get('allow_donate', 'true').lower() == 'true',
17 | 'auto_role': os.environ.get('auto_role', 'reader').lower(),
18 | 'blog_meta': {
19 | 'name': get_env_value('name', 'Oct Blog'),
20 | 'subtitle': get_env_value('subtitle', 'Oct Blog Subtitle'),
21 | 'description': get_env_value('description', 'Oct Blog Description'),
22 | 'wechat_name': get_env_value('wechat_name', 'Oct Blog Wechat Root'),
23 | 'wechat_subtitle': get_env_value('wechat_subtitle', 'Oct Blog Wechat Subtitle'),
24 | 'owner': get_env_value('owner', 'Gevin'),
25 | 'keywords': get_env_value('keywords', 'python,django,flask,docker,MongoDB'),
26 | 'google_site_verification': os.environ.get('google_site_verification') or '12345678',
27 | 'baidu_site_verification': os.environ.get('baidu_site_verification') or '87654321',
28 | 'sogou_site_verification': os.environ.get('sogou_site_verification') or '87654321',
29 | },
30 | 'search_engine_submit_urls':{
31 | 'baidu': os.environ.get('baidu_submit_url')
32 | },
33 | 'pagination':{
34 | 'per_page': int(os.environ.get('per_page', 5)),
35 | 'admin_per_page': int(os.environ.get('admin_per_page', 10)),
36 | 'archive_per_page': int(os.environ.get('archive_per_page', 20)),
37 | },
38 | 'blog_comment':{
39 | 'allow_comment': os.environ.get('allow_comment', 'true').lower() == 'true',
40 | 'comment_type': os.environ.get('comment_type', 'octblog').lower(), # currently, OctBlog only supports duoshuo comment
41 | 'comment_opt':{
42 | 'octblog': 'oct-blog', # shotname of octblog
43 | 'duoshuo': 'oct-blog', # shotname of duoshuo
44 | }
45 | },
46 | 'donation': {
47 | 'allow_donate': os.environ.get('allow_donate', 'true').lower() == 'true',
48 | 'donation_msg': get_env_value('donation_msg', 'You can donate to me if the article makes sense to you'),
49 | 'donation_img_url': os.environ.get('donation_img_url') or 'http://gevin-zone.igevin.info/pay-2.jpg?imageView/2/h/210'
50 | },
51 | 'wechat': {
52 | 'display_wechat': os.environ.get('display_wechat', 'true').lower() == 'true',
53 | 'wechat_msg': get_env_value('wechat_msg', 'Welcome to follow my wechat'),
54 | 'wechat_image_url': os.environ.get('wechat_image_url') or 'http://free.igevin.info/gevin-view.jpg?imageView/2/w/150',
55 | 'wechat_title': get_env_value('wechat_title', 'GevinView'),
56 | },
57 | 'copyright': {
58 | 'display_copyright': os.environ.get('allow_display_copyright', 'true').lower() == 'true',
59 | 'copyright_msg': get_env_value('copyright_msg', 'The article is not allowed to repost unless author authorized')
60 | },
61 | 'only_abstract_in_feed': os.environ.get('only_abstract_in_feed', 'false').lower() == 'true',
62 | 'allow_share_article': os.environ.get('allow_share_article', 'true').lower() == 'true',
63 | 'allow_wechat_port': os.environ.get('allow_wechat_port', 'false').lower() == 'true',
64 | 'gavatar_cdn_base': os.environ.get('gavatar_cdn_base', '//cdn.v2ex.com/gravatar/'),
65 | 'gavatar_default_image': os.environ.get('gavatar_default_image', 'http://free.igevin.info/user-avatar.jpg'),
66 | 'background_image': {
67 | 'home': os.environ.get('bg_home') or 'http://free.igevin.info/mayblog-home-bg.jpg',
68 | 'post': os.environ.get('bg_post') or 'http://free.igevin.info/mayblog-home-bg.jpg',
69 | 'about': os.environ.get('bg_about') or 'http://free.igevin.info/mayblog-about-bg.jpg',
70 | 'qiniu': os.environ.get('qiniu') or 'http://assets.qiniu.com/qiniu-transparent.png',
71 | },
72 | 'daovoice':{
73 | 'allow_daovoice': (os.environ.get('allow_daovoice', 'false').lower() == 'true' and os.environ.get('daovoice_app_id') is not None),
74 | 'app_id': os.environ.get('daovoice_app_id'),
75 | }
76 |
77 | }
78 |
79 | class Config(object):
80 | DEBUG = False
81 | TESTING = False
82 |
83 | BASE_DIR = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
84 |
85 | SECRET_KEY = os.environ.get('SECRET_KEY') or 'fjdljLJDL08_80jflKzcznv*c'
86 | MONGODB_SETTINGS = {'DB': 'OctBlog'}
87 |
88 | TEMPLATE_PATH = os.path.join(BASE_DIR, 'templates').replace('\\', '/')
89 | STATIC_PATH = os.path.join(BASE_DIR, 'static').replace('\\', '/')
90 | EXPORT_PATH = os.path.join(BASE_DIR, 'exports').replace('\\', '/')
91 |
92 | if not os.path.exists(EXPORT_PATH):
93 | os.makedirs(EXPORT_PATH)
94 |
95 | REMEMBER_COOKIE_DURATION = datetime.timedelta(hours=3)
96 |
97 |
98 | #########################
99 | # email server
100 | #########################
101 | # MAIL_SERVER = 'smtp.gmail.com'
102 | MAIL_SERVER = os.environ.get('MAIL_SERVER') or 'smtp.mxhichina.com'
103 | MAIL_PORT = int(os.environ.get('MAIL_PORT', 465))
104 | MAIL_USE_TLS = os.environ.get('MAIL_USE_TLS', 'false').lower() == 'true'
105 | MAIL_USE_SSL = os.environ.get('MAIL_USE_SSL', 'true').lower() == 'true'
106 | MAIL_USERNAME = os.environ.get('MAIL_USERNAME')
107 | MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')
108 |
109 |
110 | @staticmethod
111 | def init_app(app):
112 | pass
113 |
114 | class DevConfig(Config):
115 | DEBUG = True
116 |
117 | class PrdConfig(Config):
118 | # DEBUG = False
119 | DEBUG = os.environ.get('DEBUG', 'false').lower() == 'true'
120 | MONGODB_SETTINGS = {
121 | 'db': os.environ.get('DB_NAME') or 'OctBlog',
122 | 'host': os.environ.get('MONGO_HOST') or 'localhost',
123 | # 'port': 12345
124 | }
125 |
126 | class TestingConfig(Config):
127 | TESTING = True
128 | DEBUG = True
129 |
130 | WTF_CSRF_ENABLED = False
131 | MONGODB_SETTINGS = {
132 | 'db': 'OctBlogTest',
133 | 'is_mock':True
134 | }
135 |
136 | config = {
137 | 'dev': DevConfig,
138 | 'prd': PrdConfig,
139 | 'testing': TestingConfig,
140 | 'default': DevConfig,
141 | }
142 |
--------------------------------------------------------------------------------
/app/static/css/clean-blog.min.css:
--------------------------------------------------------------------------------
1 | /*!
2 | * Clean Blog v1.0.0 (http://startbootstrap.com)
3 | * Copyright 2014 Start Bootstrap
4 | * Licensed under Apache 2.0 (https://github.com/IronSummitMedia/startbootstrap/blob/gh-pages/LICENSE)
5 | */
6 |
7 | body{font-family:Lora,'Times New Roman',serif;font-size:20px;color:#404040}p{line-height:1.5;margin:30px 0}p a{text-decoration:underline}h1,h2,h3,h4,h5,h6{font-family:'Open Sans','Helvetica Neue',Helvetica,Arial,sans-serif;font-weight:800}a{color:#404040}a:hover,a:focus{color:#0085a1}a img:hover,a img:focus{cursor:zoom-in}blockquote{color:gray;font-style:italic}hr.small{max-width:100px;margin:15px auto;border-width:4px;border-color:#fff}.navbar-custom{position:absolute;top:0;left:0;width:100%;z-index:3;font-family:'Open Sans','Helvetica Neue',Helvetica,Arial,sans-serif}.navbar-custom .navbar-brand{font-weight:800}.navbar-custom .nav li a{text-transform:uppercase;font-size:12px;font-weight:800;letter-spacing:1px}@media only screen and (min-width:768px){.navbar-custom{background:0 0;border-bottom:1px solid transparent}.navbar-custom .navbar-brand{color:#fff;padding:20px}.navbar-custom .navbar-brand:hover,.navbar-custom .navbar-brand:focus{color:rgba(255,255,255,.8)}.navbar-custom .nav li a{color:#fff;padding:20px}.navbar-custom .nav li a:hover,.navbar-custom .nav li a:focus{color:rgba(255,255,255,.8)}}@media only screen and (min-width:1170px){.navbar-custom{-webkit-transition:background-color .3s;-moz-transition:background-color .3s;transition:background-color .3s;-webkit-transform:translate3d(0,0,0);-moz-transform:translate3d(0,0,0);-ms-transform:translate3d(0,0,0);-o-transform:translate3d(0,0,0);transform:translate3d(0,0,0);-webkit-backface-visibility:hidden;backface-visibility:hidden}.navbar-custom.is-fixed{position:fixed;top:-61px;background-color:rgba(255,255,255,.9);border-bottom:1px solid #f2f2f2;-webkit-transition:-webkit-transform .3s;-moz-transition:-moz-transform .3s;transition:transform .3s}.navbar-custom.is-fixed .navbar-brand{color:#404040}.navbar-custom.is-fixed .navbar-brand:hover,.navbar-custom.is-fixed .navbar-brand:focus{color:#0085a1}.navbar-custom.is-fixed .nav li a{color:#404040}.navbar-custom.is-fixed .nav li a:hover,.navbar-custom.is-fixed .nav li a:focus{color:#0085a1}.navbar-custom.is-visible{-webkit-transform:translate3d(0,100%,0);-moz-transform:translate3d(0,100%,0);-ms-transform:translate3d(0,100%,0);-o-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}}.intro-header{background-color:gray;background:no-repeat center center;background-attachment:scroll;-webkit-background-size:cover;-moz-background-size:cover;background-size:cover;-o-background-size:cover;margin-bottom:50px}.intro-header .site-heading,.intro-header .post-heading,.intro-header .page-heading{padding:100px 0 50px;color:#fff}@media only screen and (min-width:768px){.intro-header .site-heading,.intro-header .post-heading,.intro-header .page-heading{padding:150px 0}}.intro-header .site-heading,.intro-header .page-heading{text-align:center}.intro-header .site-heading h1,.intro-header .page-heading h1{margin-top:0;font-size:50px}.intro-header .site-heading .subheading,.intro-header .page-heading .subheading{font-size:24px;line-height:1.1;display:block;font-family:'Open Sans','Helvetica Neue',Helvetica,Arial,sans-serif;font-weight:300;margin:10px 0 0}@media only screen and (min-width:768px){.intro-header .site-heading h1,.intro-header .page-heading h1{font-size:80px}}.intro-header .post-heading h1{font-size:35px}.intro-header .post-heading .subheading,.intro-header .post-heading .meta{line-height:1.1;display:block}.intro-header .post-heading .subheading{font-family:'Open Sans','Helvetica Neue',Helvetica,Arial,sans-serif;font-size:24px;margin:10px 0 30px;font-weight:600}.intro-header .post-heading .meta{font-family:Lora,'Times New Roman',serif;font-style:italic;font-weight:300;font-size:20px}.intro-header .post-heading .meta a{color:#fff}@media only screen and (min-width:768px){.intro-header .post-heading h1{font-size:55px}.intro-header .post-heading .subheading{font-size:30px}}.post-preview>a{color:#404040}.post-preview>a:hover,.post-preview>a:focus{text-decoration:none;color:#0085a1}.post-preview>a>.post-title{font-size:30px;margin-top:30px;margin-bottom:10px}.post-preview>a>.post-subtitle{margin:0;font-weight:300;margin-bottom:10px}.post-preview>.post-meta{color:gray;font-size:18px;font-style:italic;margin-top:0}.post-preview>.post-meta>a{text-decoration:none;color:#404040}.post-preview>.post-meta>a:hover,.post-preview>.post-meta>a:focus{color:#0085a1;text-decoration:underline}@media only screen and (min-width:768px){.post-preview>a>.post-title{font-size:36px}}.section-heading{font-size:36px;margin-top:60px;font-weight:700}.caption{text-align:center;font-size:14px;padding:10px;font-style:italic;margin:0;display:block;border-bottom-right-radius:5px;border-bottom-left-radius:5px}footer{padding:50px 0 65px}footer .list-inline{margin:0;padding:0}footer .copyright{font-size:14px;text-align:center;margin-bottom:0}.floating-label-form-group{font-size:14px;position:relative;margin-bottom:0;padding-bottom:.5em;border-bottom:1px solid #eee}.floating-label-form-group input,.floating-label-form-group textarea{z-index:1;position:relative;padding-right:0;padding-left:0;border:none;border-radius:0;font-size:1.5em;background:0 0;box-shadow:none!important;resize:none}.floating-label-form-group label{display:block;z-index:0;position:relative;top:2em;margin:0;font-size:.85em;line-height:1.764705882em;vertical-align:middle;vertical-align:baseline;opacity:0;-webkit-transition:top .3s ease,opacity .3s ease;-moz-transition:top .3s ease,opacity .3s ease;-ms-transition:top .3s ease,opacity .3s ease;transition:top .3s ease,opacity .3s ease}.floating-label-form-group::not(:first-child){padding-left:14px;border-left:1px solid #eee}.floating-label-form-group-with-value label{top:0;opacity:1}.floating-label-form-group-with-focus label{color:#0085a1}form .row:first-child .floating-label-form-group{border-top:1px solid #eee}.btn{font-family:'Open Sans','Helvetica Neue',Helvetica,Arial,sans-serif;text-transform:uppercase;font-size:14px;font-weight:800;letter-spacing:1px;border-radius:0;padding:15px 25px}.btn-lg{font-size:16px;padding:25px 35px}.btn-default:hover,.btn-default:focus{background-color:#0085a1;border:1px solid #0085a1;color:#fff}.pager{margin:20px 0 0}.pager li>a,.pager li>span{font-family:'Open Sans','Helvetica Neue',Helvetica,Arial,sans-serif;text-transform:uppercase;font-size:14px;font-weight:800;letter-spacing:1px;padding:15px 25px;background-color:#fff;border-radius:0}.pager li>a:hover,.pager li>a:focus{color:#fff;background-color:#0085a1;border:1px solid #0085a1}.pager .disabled>a,.pager .disabled>a:hover,.pager .disabled>a:focus,.pager .disabled>span{color:gray;background-color:#404040;cursor:not-allowed}::-moz-selection{color:#fff;text-shadow:none;background:#0085a1}::selection{color:#fff;text-shadow:none;background:#0085a1}img::selection{color:#fff;background:0 0}img::-moz-selection{color:#fff;background:0 0}body{webkit-tap-highlight-color:#0085a1}
8 |
--------------------------------------------------------------------------------
/app/templates/blog_admin/post.html:
--------------------------------------------------------------------------------
1 | {% extends "admin_base.html" %}
2 |
3 | {% block title %}
4 | {% if edit_flag %}
5 | Edit {{ form.post_type.data|capitalize }}
6 | {% else %}
7 | New {{ form.post_type.data|capitalize }}
8 | {% endif %}
9 | {% endblock %}
10 |
11 | {% block css %}
12 |
13 | {% endblock %}
14 |
15 | {% block main %}
16 |
17 |
118 | {% endblock %}
119 |
120 | {% block js %}
121 |
122 |
123 |
124 |
155 | {% endblock %}
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # About OctBlog
2 |
3 | [](https://gitter.im/flyhigher139/OctBlog?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
4 | [](http://microbadger.com/images/gevin/octblog "Get your own image badge on microbadger.com") [](http://microbadger.com/images/gevin/octblog "Get your own version badge on microbadger.com") [](https://travis-ci.org/flyhigher139/OctBlog)
5 |
6 | [OctBlog](https://github.com/flyhigher139/OctBlog) is almost the same with [MayBlog](https://github.com/flyhigher139/MayBlog) except that it is powered by [Flask](http://flask.pocoo.org/) and [MongoDB](https://www.mongodb.org/) rather than [Django](https://www.djangoproject.com/) and SQL Databases.
7 |
8 | And as my customary, I named it OctBlog as OctBlog was started in October, 2015
9 |
10 | OctBlog offers every function in MayBlog, and aims to do it better, its features are as follow:
11 |
12 | - Multiple user
13 | - OctBlog roles: su, admin, editor, writer, reader
14 | - Blog features: posts, pages, tags, and categories
15 | - Markdown support
16 | - Admin interface
17 | - Change configurations by configuration file or environment variable
18 | - Multiple comment plugin
19 | - User defined widgets
20 | - Deploy with docker
21 | - Sort posts by weight
22 |
23 | ## Demo
24 |
25 | [Gevin's Blog](https://blog.igevin.info/) is powered by OctBlog
26 |
27 | ## Explanation
28 |
29 | The weight is used to order articles, and if you want to hidden an article from the article list, weight is also qualified:
30 |
31 | The default weight for each article is 10, if a article's weight is heavier than 10, it will be firstly displayed, and if the weight is negative, the article will be never displayed in the article list
32 |
33 | ## Dependency
34 |
35 | ### Backend
36 |
37 | - Flask
38 | - flask-script
39 | - flask-login
40 | - flask-admin
41 | - Flask-WTF
42 | - flask-principal
43 | - flask_mongoengine
44 | - WTForms
45 | - mongoengine
46 | - markdown2
47 | - bleach
48 |
49 | ### Frontend
50 |
51 | - jQuery
52 | - BootStrap
53 | - [Clean Blog theme](http://startbootstrap.com/template-overviews/clean-blog/)
54 | - bootbox.js
55 | - bootstrap-markdown.js
56 | - bootstrap-datetimepicker.js
57 | - Font Awesome
58 | - highlight.js
59 |
60 | ## How to run OctBlog ?
61 |
62 | ### Run from source code
63 |
64 | If you want to see more about the source code, checkout the [source code readme](app)
65 |
66 |
67 | ### Run by docker(recommended)
68 |
69 | Run OctBlog by docker is recommended, here are some instruction:
70 |
71 | #### First Run
72 |
73 | 1\. Get your OctBlog image
74 |
75 | In command line, switch to OctBlog root directory, and run the following command to build your own OctBlog image:
76 |
77 | ```bash
78 | cd app
79 | (sudo) docker build -t gevin/octblog:0.1 .
80 |
81 | # Now you can take a cup of coffee and wait for a few minutes :)
82 | ```
83 |
84 | Alternatively, pull Octblog image from DockerHub(**recommended**):
85 |
86 | ```bash
87 | (sudo) docker pull gevin/octblog:0.1
88 | ```
89 |
90 | 2\. Create your `docker-compose.yml`
91 |
92 | You need to create a docker-compose file similar to the `docker-compose_no_swarm.yml` file
93 |
94 | Replace ```/Users/gevin/projects/data/mongodb``` with a path on your machine
95 |
96 |
97 |
98 | 3\. Run OctBlog
99 |
100 | ```bash
101 | (sudo) docker-compose up -d
102 | ```
103 |
104 | Then you can visit OctBlog in your brower at `http://localhost`
105 |
106 | All environment variables can be found in `/OctBlog/config.py`
107 |
108 | A `.env` file example:
109 |
110 | ```
111 | DEBUG=false
112 | config=prd
113 | MONGO_HOST=mongo
114 | allow_registration=true
115 | allow_su_creation=true
116 |
117 | name=Gevin's Blog
118 | subtitle=技术、生活都要折腾
119 | description=技术、生活都要折腾
120 |
121 | wechat_name=GevinView @
122 | wechat_subtitle=技术、生活都要折腾
123 |
124 | copyright_msg=注:转载本文,请与Gevin联系
125 | donation_msg=如果您觉得Gevin的文章有价值,就请Gevin喝杯茶吧!
126 | wechat_msg=欢迎关注我的微信公众账号
127 |
128 | google_site_verification=
129 | allow_comment=true
130 |
131 |
132 | allow_daovoice=true
133 | daovoice_app_id=
134 | ```
135 |
136 | 3\. Get into OctBlog container
137 |
138 | Maybe you would like to dig into the container, the following command will help:
139 |
140 | ```bash
141 | # Specify OctBlog container ID, eg:12345678
142 | (sudo) docker ps
143 |
144 | # Get into OctBlog container
145 | (sudo) docker exec -it 12345678 bash
146 |
147 | ```
148 |
149 | #### After first run
150 |
151 | - Start OctBlog
152 |
153 | ```bash
154 | (sudo) docker-compose start
155 | ```
156 |
157 | - Stop OctBlog
158 |
159 | ```bash
160 | (sudo) docker-compose stop
161 | ```
162 |
163 | ### Run by docker with swarm mode
164 |
165 | #### Preparation
166 |
167 | If you want to use docker swarm mode, you need to activate this mode first:
168 |
169 | ```
170 | docker swarm init
171 | ```
172 |
173 | This command activates your docker swarm mode and make it as a swarm manager
174 |
175 | Then you can join other swarm node to this manager with `docker swarm join`
176 |
177 | #### Run OctBlog
178 |
179 | You need to create a docker-compose file similar to the `docker-compose.yml` file.
180 |
181 | Then, start your application:
182 |
183 | ```
184 | docker stack deploy -c docker-compose.yml octblog
185 | ```
186 |
187 | review your application:
188 |
189 | ```
190 | docker stack ps octblog
191 | ```
192 |
193 | remove your application:
194 |
195 | ```
196 | docker stack rm octblog
197 | ```
198 |
199 | You can refer to [Docker Documentation](https://docs.docker.com/) for more docker swarm usages.
200 |
201 | ### Get started with OctBlog
202 |
203 | #### 1\. Create a superuser to administrate OctBlog
204 |
205 | Visit the following url and create a superuser
206 |
207 | `http://localhost:8000/accounts/registration/su`
208 |
209 | If the url is forbidden, you need to modify your configurations to allow the creation.
210 |
211 | #### 2\. Administrate OctBlog
212 |
213 | The admin home is: `http://localhost:8000/admin`
214 |
215 | You will be redirected to login page if you haven't logged in
216 |
217 | #### 3\. Modify the default configurations
218 |
219 | You either change settings in `app/OctBlog/config.py` file, or set environment variables defined in that file.
220 |
221 | **Setting environment variables is recommended, and once the configuration is changed, you need to restart the service.**
222 |
223 |
224 |
225 | ## License
226 |
227 | OctBlog is under [GPL2](https://github.com/flyhigher139/OctBlog/blob/dev/LICENSE)
228 |
229 | ## What's more
230 |
231 | If you find a bug or want to add a new feature, just issue me.
232 |
233 | Want to contribute? Please fork OctBlog and pull request to me.
234 |
235 | I'm not good at frontend development, so I used a free bootstrap blog theme. If you can redesign the blog theme and admin interface, I'll appriciate your work very much!
236 |
--------------------------------------------------------------------------------
/app/static/fonts/iconfont.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Created by FontForge 20120731 at Thu Nov 26 14:21:15 2015
6 | By Ads
7 |
8 |
9 |
10 |
24 |
26 |
28 |
30 |
32 |
36 |
39 |
44 |
50 |
55 |
57 |
61 |
65 |
68 |
70 |
75 |
79 |
80 |
81 |
--------------------------------------------------------------------------------
/app/templates/base.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | {% if post %}
9 |
10 |
11 |
12 |
13 | {% else %}
14 |
15 |
16 |
17 |
18 | {% endif %}
19 |
20 |
21 |
22 |
23 |
24 |
25 | {% block title %}Oct Blog{% endblock %}
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
47 |
48 | {% block custom_head %}{% endblock %}
49 |
50 |
51 |
58 |
59 | {% if allow_daovoice %}
60 |
61 | {% endif %}
62 |
63 |
64 |
65 |
66 |
67 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
86 |
87 |
111 |
112 |
113 |
114 |
115 |
116 |
117 | {%block header %}{% endblock %}
118 |
119 | {%block main %}{% endblock %}
120 |
121 |
122 |
123 |
124 |
125 |
151 |
Copyright © Oct Blog 2015
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 | {% if allow_daovoice %}
166 | {% if current_user.is_authenticated %}
167 |
177 |
178 | {% else %}
179 |
185 |
186 | {% endif %}
187 | {% endif %}
188 |
189 |
190 | {% block js %}
191 | {% endblock %}
192 |
193 |
--------------------------------------------------------------------------------
/app/main/models.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | import datetime, hashlib, urllib
5 | from flask import url_for
6 |
7 | import markdown2, bleach
8 |
9 | from OctBlog import db
10 | from OctBlog.config import OctBlogSettings
11 | from accounts.models import User
12 |
13 | def get_clean_html_content(html_content):
14 | allowed_tags = ['a', 'abbr', 'acronym', 'b', 'blockquote', 'code',
15 | 'em', 'i', 'li', 'ol', 'pre', 'strong', 'ul',
16 | 'h1', 'h2', 'h3', 'h4', 'h5', 'p', 'hr', 'img',
17 | 'table', 'thead', 'tbody', 'tr', 'th', 'td',
18 | 'sup', 'sub']
19 |
20 | allowed_attrs = {
21 | '*': ['class'],
22 | 'a': ['href', 'rel', 'name'],
23 | 'img': ['alt', 'src', 'title'],
24 | }
25 | html_content = bleach.linkify(bleach.clean(html_content, tags=allowed_tags, attributes=allowed_attrs, strip=True))
26 | return html_content
27 |
28 |
29 |
30 | POST_TYPE_CHOICES = ('post', 'page', 'wechat')
31 | GAVATAR_CDN_BASE = OctBlogSettings['gavatar_cdn_base']
32 | GAVATAR_DEFAULT_IMAGE = OctBlogSettings['gavatar_default_image']
33 |
34 | class Post(db.Document):
35 | title = db.StringField(max_length=255, default='new blog', required=True)
36 | slug = db.StringField(max_length=255, required=True, unique=True)
37 | fix_slug = db.StringField(max_length=255, required=False)
38 | abstract = db.StringField()
39 | raw = db.StringField(required=True)
40 | pub_time = db.DateTimeField()
41 | update_time = db.DateTimeField()
42 | content_html = db.StringField(required=True)
43 | author = db.ReferenceField(User)
44 | category = db.StringField(max_length=64)
45 | tags = db.ListField(db.StringField(max_length=30))
46 | is_draft = db.BooleanField(default=False)
47 | post_type = db.StringField(max_length=64, default='post')
48 | weight = db.IntField(default=10)
49 |
50 | def get_absolute_url(self):
51 | # return url_for('main.post_detail', slug=self.slug)
52 |
53 | router = {
54 | 'post': url_for('main.post_detail', slug=self.slug, _external=True),
55 | 'page': url_for('main.page_detail', slug=self.slug, _external=True),
56 | 'wechat': url_for('main.wechat_detail', slug=self.slug, _external=True),
57 | }
58 |
59 | return router[self.post_type]
60 |
61 | def save(self, allow_set_time=False, *args, **kwargs):
62 | if not allow_set_time:
63 | now = datetime.datetime.utcnow()
64 | if not self.pub_time:
65 | self.pub_time = now
66 | self.update_time = now
67 | # self.content_html = self.raw
68 | self.content_html = markdown2.markdown(self.raw, extras=['code-friendly', 'fenced-code-blocks', 'tables'])
69 | self.content_html = get_clean_html_content(self.content_html)
70 | return super(Post, self).save(*args, **kwargs)
71 |
72 | def set_post_date(self, pub_time, update_time):
73 | self.pub_time = pub_time
74 | self.update_time = update_time
75 | return self.save(allow_set_time=True)
76 |
77 | def to_dict(self):
78 | post_dict = {}
79 | post_dict['title'] = self.title
80 | post_dict['slug'] = self.slug
81 | post_dict['abstract'] = self.abstract
82 | post_dict['raw'] = self.raw
83 | post_dict['pub_time'] = self.pub_time.strftime('%Y-%m-%d %H:%M:%S')
84 | post_dict['update_time'] = self.update_time.strftime('%Y-%m-%d %H:%M:%S')
85 | post_dict['content_html'] = self.content_html
86 | post_dict['author'] = self.author.username
87 | post_dict['category'] = self.category
88 | post_dict['tags'] = self.tags
89 | post_dict['post_type'] = self.post_type
90 |
91 | return post_dict
92 |
93 |
94 | def __unicode__(self):
95 | return self.title
96 |
97 | meta = {
98 | 'allow_inheritance': True,
99 | 'indexes': ['slug'],
100 | 'ordering': ['-pub_time']
101 | }
102 |
103 | # class Post(PostBase):
104 | # fix_slug = db.StringField(max_length=255, required=False)
105 | # category = db.StringField(max_length=64, default='default')
106 | # tags = db.ListField(db.StringField(max_length=30))
107 | # is_draft = db.BooleanField(default=False)
108 |
109 | class Draft(db.Document):
110 | title = db.StringField(max_length=255, default='new blog', required=True)
111 | slug = db.StringField(max_length=255, required=True, unique=True)
112 | # fix_slug = db.StringField(max_length=255, required=False)
113 | abstract = db.StringField()
114 | raw = db.StringField(required=True)
115 | pub_time = db.DateTimeField()
116 | update_time = db.DateTimeField()
117 | content_html = db.StringField(required=True)
118 | author = db.ReferenceField(User)
119 | category = db.StringField(max_length=64, default='default')
120 | tags = db.ListField(db.StringField(max_length=30))
121 | is_draft = db.BooleanField(default=True)
122 | post_type = db.StringField(max_length=64, default='post')
123 | weight = db.IntField(default=10)
124 |
125 | def save(self, *args, **kwargs):
126 | now = datetime.datetime.utcnow()
127 | if not self.pub_time:
128 | self.pub_time = now
129 | self.update_time = now
130 | self.content_html = markdown2.markdown(self.raw, extras=['code-friendly', 'fenced-code-blocks', 'tables'])
131 | self.content_html = get_clean_html_content(self.content_html)
132 | return super(Draft, self).save(*args, **kwargs)
133 |
134 |
135 | def __unicode__(self):
136 | return self.title
137 |
138 | meta = {
139 | 'allow_inheritance': True,
140 | 'indexes': ['slug'],
141 | 'ordering': ['-update_time']
142 | }
143 |
144 | class Tracker(db.Document):
145 | post = db.ReferenceField(Post)
146 | ip = db.StringField()
147 | user_agent = db.StringField()
148 | create_time = db.DateTimeField()
149 |
150 | def save(self, *args, **kwargs):
151 | if not self.create_time:
152 | self.create_time = datetime.datetime.utcnow()
153 | return super(Tracker, self).save(*args, **kwargs)
154 |
155 | def __unicode__(self):
156 | return self.ip
157 |
158 | meta = {
159 | 'allow_inheritance': True,
160 | 'indexes': ['ip'],
161 | 'ordering': ['-create_time']
162 | }
163 |
164 |
165 | class PostStatistics(db.Document):
166 | post = db.ReferenceField(Post)
167 | visit_count = db.IntField(default=0)
168 | verbose_count_base = db.IntField(default=0)
169 |
170 | class Widget(db.Document):
171 | title = db.StringField(default='widget')
172 | md_content = db.StringField()
173 | html_content = db.StringField()
174 | allow_post_types = db.ListField(db.StringField())
175 | priority = db.IntField(default=1000000)
176 | update_time = db.DateTimeField()
177 |
178 | def save(self, *args, **kwargs):
179 | if self.md_content:
180 | self.html_content = markdown2.markdown(self.md_content, extras=['code-friendly', 'fenced-code-blocks', 'tables'])
181 |
182 | self.html_content = get_clean_html_content(self.html_content)
183 |
184 | if not self.update_time:
185 | self.update_time = datetime.datetime.utcnow()
186 |
187 | return super(Widget, self).save(*args, **kwargs)
188 |
189 | def __unicode__(self):
190 | return self.title
191 |
192 | meta = {
193 | # 'allow_inheritance': True,
194 | 'ordering': ['priority']
195 | }
196 |
197 | COMMENT_STATUS = ('approved', 'pending', 'spam', 'deleted')
198 | class Comment(db.Document):
199 | author = db.StringField(required=True)
200 | email = db.EmailField(max_length=255)
201 | homepage = db.URLField()
202 | # post = db.ReferenceField(Post)
203 | post_slug = db.StringField(required=True)
204 | post_title = db.StringField(default='default article')
205 | md_content = db.StringField()
206 | html_content = db.StringField()
207 | pub_time = db.DateTimeField()
208 | update_time = db.DateTimeField()
209 | replay_to = db.ReferenceField('self')
210 | status = db.StringField(choices=COMMENT_STATUS, default='pending')
211 | misc = db.StringField() # If the comment is imported, this field will store something useful
212 | gavatar_id = db.StringField(default='00000000000')
213 |
214 | def reset_gavatar_id(self):
215 | if not self.email:
216 | self.gavatar_id = '00000000000'
217 | return
218 | self.gavatar_id = hashlib.md5(self.email.lower().encode('utf-8')).hexdigest()
219 | # self.gavatar_id = hashlib.md5(self.email.lower()).hexdigest()
220 |
221 | def save(self, *args, **kwargs):
222 | if self.md_content:
223 | html_content = markdown2.markdown(self.md_content, extras=['code-friendly', 'fenced-code-blocks', 'tables', 'nofollow'])
224 | self.html_content = get_clean_html_content(html_content)
225 |
226 | if not self.pub_time:
227 | self.pub_time = datetime.datetime.utcnow()
228 |
229 | self.update_time = datetime.datetime.utcnow()
230 |
231 | if self.gavatar_id=='00000000000':
232 | self.reset_gavatar_id()
233 |
234 | return super(Comment, self).save(*args, **kwargs)
235 |
236 | def get_gavatar_url(self, base_url=GAVATAR_CDN_BASE, img_size=0, default_image_url=None):
237 | gavatar_url = base_url + self.gavatar_id
238 | params = {}
239 | if img_size:
240 | params['s'] = str(img_size)
241 | if default_image_url:
242 | params['d'] = default_image_url
243 |
244 | if params:
245 | gavatar_url = '{0}?{1}'.format(gavatar_url, urllib.urlencode(params))
246 |
247 | return gavatar_url
248 |
249 | def __unicode__(self):
250 | return self.md_content[:64]
251 |
252 | meta = {
253 | 'ordering': ['-update_time']
254 | }
255 |
--------------------------------------------------------------------------------
/app/static/css/clean-blog.css:
--------------------------------------------------------------------------------
1 | /*!
2 | * Clean Blog v1.0.0 (http://startbootstrap.com)
3 | * Copyright 2014 Start Bootstrap
4 | * Licensed under Apache 2.0 (https://github.com/IronSummitMedia/startbootstrap/blob/gh-pages/LICENSE)
5 | */
6 |
7 | body {
8 | /*font-family: 'Lora', 'Times New Roman', serif;*/
9 | font-family: "lucida grande", "lucida sans unicode", lucida, helvetica, "STHeiti Light", "Microsoft YaHei", "WenQuanYi Micro Hei", sans-serif;
10 | font-size: 18px;
11 | color: #404040;
12 | }
13 | p {
14 | line-height: 1.7;
15 | margin: 30px 0;
16 | }
17 | p a {
18 | text-decoration: underline;
19 | }
20 | h1,
21 | h2,
22 | h3,
23 | h4,
24 | h5,
25 | h6 {
26 | /*font-family: 'Open Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;*/
27 | font-family: 'Open Sans', 'Helvetica Neue', Helvetica, Arial, "STHeiti Light", "Microsoft YaHei", 'Hiragino Sans GB', 'SimHei', sans-serif;
28 | font-weight: 800;
29 | }
30 | a {
31 | color: #404040;
32 | }
33 | a:hover,
34 | a:focus {
35 | color: #0085a1;
36 | }
37 | a img:hover,
38 | a img:focus {
39 | cursor: zoom-in;
40 | }
41 | blockquote {
42 | color: #808080;
43 | font-style: italic;
44 | }
45 | hr.small {
46 | max-width: 100px;
47 | margin: 15px auto;
48 | border-width: 4px;
49 | border-color: white;
50 | }
51 | .navbar-custom {
52 | position: absolute;
53 | top: 0;
54 | left: 0;
55 | width: 100%;
56 | z-index: 3;
57 | font-family: 'Open Sans', 'Helvetica Neue', Helvetica, Arial, "STHeiti Light", "Microsoft YaHei", 'Hiragino Sans GB', 'SimHei', sans-serif;
58 | }
59 | .navbar-custom .navbar-brand {
60 | font-weight: 800;
61 | }
62 | .navbar-custom .nav li a {
63 | text-transform: uppercase;
64 | font-size: 12px;
65 | font-weight: 800;
66 | letter-spacing: 1px;
67 | }
68 | @media only screen and (min-width: 768px) {
69 | .navbar-custom {
70 | background: transparent;
71 | border-bottom: 1px solid transparent;
72 | }
73 | .navbar-custom .navbar-brand {
74 | color: white;
75 | padding: 20px;
76 | }
77 | .navbar-custom .navbar-brand:hover,
78 | .navbar-custom .navbar-brand:focus {
79 | color: rgba(255, 255, 255, 0.8);
80 | }
81 | .navbar-custom .nav li a {
82 | color: white;
83 | padding: 20px;
84 | }
85 | .navbar-custom .nav li a:hover,
86 | .navbar-custom .nav li a:focus {
87 | color: rgba(255, 255, 255, 0.8);
88 | }
89 | }
90 | @media only screen and (min-width: 1170px) {
91 | .navbar-custom {
92 | -webkit-transition: background-color 0.3s;
93 | -moz-transition: background-color 0.3s;
94 | transition: background-color 0.3s;
95 | /* Force Hardware Acceleration in WebKit */
96 | -webkit-transform: translate3d(0, 0, 0);
97 | -moz-transform: translate3d(0, 0, 0);
98 | -ms-transform: translate3d(0, 0, 0);
99 | -o-transform: translate3d(0, 0, 0);
100 | transform: translate3d(0, 0, 0);
101 | -webkit-backface-visibility: hidden;
102 | backface-visibility: hidden;
103 | }
104 | .navbar-custom.is-fixed {
105 | /* when the user scrolls down, we hide the header right above the viewport */
106 | position: fixed;
107 | top: -61px;
108 | background-color: rgba(255, 255, 255, 0.9);
109 | border-bottom: 1px solid #f2f2f2;
110 | -webkit-transition: -webkit-transform 0.3s;
111 | -moz-transition: -moz-transform 0.3s;
112 | transition: transform 0.3s;
113 | }
114 | .navbar-custom.is-fixed .navbar-brand {
115 | color: #404040;
116 | }
117 | .navbar-custom.is-fixed .navbar-brand:hover,
118 | .navbar-custom.is-fixed .navbar-brand:focus {
119 | color: #0085a1;
120 | }
121 | .navbar-custom.is-fixed .nav li a {
122 | color: #404040;
123 | }
124 | .navbar-custom.is-fixed .nav li a:hover,
125 | .navbar-custom.is-fixed .nav li a:focus {
126 | color: #0085a1;
127 | }
128 | .navbar-custom.is-visible {
129 | /* if the user changes the scrolling direction, we show the header */
130 | -webkit-transform: translate3d(0, 100%, 0);
131 | -moz-transform: translate3d(0, 100%, 0);
132 | -ms-transform: translate3d(0, 100%, 0);
133 | -o-transform: translate3d(0, 100%, 0);
134 | transform: translate3d(0, 100%, 0);
135 | }
136 | }
137 | .intro-header {
138 | background-color: #808080;
139 | background: no-repeat center center;
140 | background-attachment: scroll;
141 | -webkit-background-size: cover;
142 | -moz-background-size: cover;
143 | background-size: cover;
144 | -o-background-size: cover;
145 | margin-bottom: 50px;
146 | }
147 | .intro-header .site-heading,
148 | .intro-header .post-heading,
149 | .intro-header .page-heading {
150 | padding: 100px 0 50px;
151 | color: white;
152 | }
153 | @media only screen and (min-width: 768px) {
154 | .intro-header .site-heading,
155 | .intro-header .post-heading,
156 | .intro-header .page-heading {
157 | padding: 150px 0;
158 | }
159 | }
160 | .intro-header .site-heading,
161 | .intro-header .page-heading {
162 | text-align: center;
163 | }
164 | .intro-header .site-heading h1,
165 | .intro-header .page-heading h1 {
166 | margin-top: 0;
167 | font-size: 50px;
168 | }
169 | .intro-header .site-heading .subheading,
170 | .intro-header .page-heading .subheading {
171 | font-size: 24px;
172 | line-height: 1.1;
173 | display: block;
174 | font-family: 'Open Sans', 'Helvetica Neue', Helvetica, Arial, "STHeiti Light", "Microsoft YaHei", 'Hiragino Sans GB', 'SimHei', sans-serif;
175 | font-weight: 300;
176 | margin: 10px 0 0;
177 | }
178 | @media only screen and (min-width: 768px) {
179 | .intro-header .site-heading h1,
180 | .intro-header .page-heading h1 {
181 | font-size: 80px;
182 | }
183 | }
184 | .intro-header .post-heading h1 {
185 | font-size: 35px;
186 | }
187 | .intro-header .post-heading .subheading,
188 | .intro-header .post-heading .meta {
189 | line-height: 1.1;
190 | display: block;
191 | }
192 | .intro-header .post-heading .subheading {
193 | font-family: 'Open Sans', 'Helvetica Neue', Helvetica, Arial, "STHeiti Light", "Microsoft YaHei", 'Hiragino Sans GB', 'SimHei', sans-serif;
194 | font-size: 24px;
195 | margin: 10px 0 30px;
196 | font-weight: 600;
197 | }
198 | .intro-header .post-heading .meta {
199 | font-family: "lucida grande", "lucida sans unicode", lucida, helvetica, "STHeiti Light", "Microsoft YaHei", "WenQuanYi Micro Hei", sans-serif;
200 | font-style: italic;
201 | font-weight: 300;
202 | font-size: 16px;
203 | }
204 | .intro-header .post-heading .meta a {
205 | color: white;
206 | }
207 | @media only screen and (min-width: 768px) {
208 | .intro-header .post-heading h1 {
209 | font-size: 55px;
210 | }
211 | .intro-header .post-heading .subheading {
212 | font-size: 30px;
213 | }
214 | }
215 | .post-preview > a {
216 | color: #404040;
217 | }
218 | .post-preview > a:hover,
219 | .post-preview > a:focus {
220 | text-decoration: none;
221 | color: #0085a1;
222 | }
223 | .post-preview > a > .post-title {
224 | font-size: 30px;
225 | margin-top: 30px;
226 | margin-bottom: 10px;
227 | }
228 | .post-preview > a > .post-subtitle {
229 | margin: 0;
230 | font-weight: 300;
231 | margin-bottom: 10px;
232 | }
233 | .post-preview > .post-meta {
234 | color: #808080;
235 | font-size: 18px;
236 | font-style: italic;
237 | margin-top: 0;
238 | }
239 | .post-preview > .post-meta > a {
240 | text-decoration: none;
241 | color: #404040;
242 | }
243 | .post-preview > .post-meta > a:hover,
244 | .post-preview > .post-meta > a:focus {
245 | color: #0085a1;
246 | text-decoration: underline;
247 | }
248 | @media only screen and (min-width: 768px) {
249 | .post-preview > a > .post-title {
250 | font-size: 36px;
251 | }
252 | }
253 | .section-heading {
254 | font-size: 36px;
255 | margin-top: 60px;
256 | font-weight: 700;
257 | }
258 | .caption {
259 | text-align: center;
260 | font-size: 14px;
261 | padding: 10px;
262 | font-style: italic;
263 | margin: 0;
264 | display: block;
265 | border-bottom-right-radius: 5px;
266 | border-bottom-left-radius: 5px;
267 | }
268 | footer {
269 | padding: 50px 0 65px;
270 | }
271 | footer .list-inline {
272 | margin: 0;
273 | padding: 0;
274 | }
275 | footer .copyright {
276 | font-size: 14px;
277 | text-align: center;
278 | margin-bottom: 0;
279 | }
280 | .floating-label-form-group {
281 | font-size: 14px;
282 | position: relative;
283 | margin-bottom: 0;
284 | padding-bottom: 0.5em;
285 | border-bottom: 1px solid #eeeeee;
286 | }
287 | .floating-label-form-group input,
288 | .floating-label-form-group textarea {
289 | z-index: 1;
290 | position: relative;
291 | padding-right: 0;
292 | padding-left: 0;
293 | border: none;
294 | border-radius: 0;
295 | font-size: 1.5em;
296 | background: none;
297 | box-shadow: none !important;
298 | resize: none;
299 | }
300 | .floating-label-form-group label {
301 | display: block;
302 | z-index: 0;
303 | position: relative;
304 | top: 2em;
305 | margin: 0;
306 | font-size: 0.85em;
307 | line-height: 1.764705882em;
308 | vertical-align: middle;
309 | vertical-align: baseline;
310 | opacity: 0;
311 | -webkit-transition: top 0.3s ease,opacity 0.3s ease;
312 | -moz-transition: top 0.3s ease,opacity 0.3s ease;
313 | -ms-transition: top 0.3s ease,opacity 0.3s ease;
314 | transition: top 0.3s ease,opacity 0.3s ease;
315 | }
316 | .floating-label-form-group::not(:first-child) {
317 | padding-left: 14px;
318 | border-left: 1px solid #eeeeee;
319 | }
320 | .floating-label-form-group-with-value label {
321 | top: 0;
322 | opacity: 1;
323 | }
324 | .floating-label-form-group-with-focus label {
325 | color: #0085a1;
326 | }
327 | form .row:first-child .floating-label-form-group {
328 | border-top: 1px solid #eeeeee;
329 | }
330 | .btn {
331 | font-family: 'Open Sans', 'Helvetica Neue', Helvetica, Arial, "STHeiti Light", "Microsoft YaHei", 'Hiragino Sans GB', 'SimHei', sans-serif;
332 | text-transform: uppercase;
333 | font-size: 14px;
334 | font-weight: 800;
335 | letter-spacing: 1px;
336 | border-radius: 0;
337 | padding: 15px 25px;
338 | }
339 | .btn-lg {
340 | font-size: 16px;
341 | padding: 25px 35px;
342 | }
343 | .btn-default:hover,
344 | .btn-default:focus {
345 | background-color: #0085a1;
346 | border: 1px solid #0085a1;
347 | color: white;
348 | }
349 | .pager {
350 | margin: 20px 0 0;
351 | }
352 | .pager li > a,
353 | .pager li > span {
354 | font-family: 'Open Sans', 'Helvetica Neue', Helvetica, Arial, "STHeiti Light", "Microsoft YaHei", 'Hiragino Sans GB', 'SimHei', sans-serif;
355 | text-transform: uppercase;
356 | font-size: 14px;
357 | font-weight: 800;
358 | letter-spacing: 1px;
359 | padding: 15px 25px;
360 | background-color: white;
361 | border-radius: 0;
362 | }
363 | .pager li > a:hover,
364 | .pager li > a:focus {
365 | color: white;
366 | background-color: #0085a1;
367 | border: 1px solid #0085a1;
368 | }
369 | .pager .disabled > a,
370 | .pager .disabled > a:hover,
371 | .pager .disabled > a:focus,
372 | .pager .disabled > span {
373 | color: #808080;
374 | background-color: #404040;
375 | cursor: not-allowed;
376 | }
377 | ::-moz-selection {
378 | color: white;
379 | text-shadow: none;
380 | background: #0085a1;
381 | }
382 | ::selection {
383 | color: white;
384 | text-shadow: none;
385 | background: #0085a1;
386 | }
387 | img {
388 | max-width:100%;
389 | max-height:100%;
390 | }
391 | img::selection {
392 | color: white;
393 | background: transparent;
394 | }
395 | img::-moz-selection {
396 | color: white;
397 | background: transparent;
398 | }
399 | body {
400 | webkit-tap-highlight-color: #0085a1;
401 | line-height:1.8;
402 | }
403 |
--------------------------------------------------------------------------------
/app/templates/admin_base.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | {% block title %}Otc Blog Admin{% endblock %}
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | {% block css %}{% endblock %}
24 |
25 |
32 |
33 |
34 |
35 |
39 |
40 |
41 | {% import "_msg.html" as messages %}
42 | {{ messages.render_msg() }}
43 |
44 |
45 |
46 |
47 |
56 |
57 |
58 |
59 |
60 | Home (current)
61 |
62 | {% if g.identity.allow_write %}
63 |
64 | Posts
65 |
80 |
81 |
82 | {% if g.allow_wechat_port %}
83 |
84 | WeChat
85 |
91 |
92 | {% endif %}
93 |
94 |
95 | {% endif %}
96 |
97 | {% if g.identity.allow_edit %}
98 |
99 | Pages
100 |
106 |
107 |
108 | Comments
109 |
117 |
118 | {% endif %}
119 |
120 | {% if g.identity.allow_admin %}
121 |
122 | Users
123 |
131 |
132 | {% endif %}
133 |
134 | {% if g.identity.allow_admin %}
135 |
136 | Super Admin
137 |
148 |
149 | {% endif %}
150 |
151 |
152 |
153 |
154 |
155 |
175 |
176 |
177 |
178 |
179 |
180 |
181 | {%block header %}{% endblock %}
182 |
183 |
184 |
185 | {%block main %}{% endblock %}
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
220 |
Copyright © Oct Blog 2015
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 | {% block js %}
233 | {% endblock %}
234 |
235 |
236 |
--------------------------------------------------------------------------------