├── 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 |
10 | {{ forms.render(form) }} 11 | 12 |
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 |
10 | {{ forms.render(form) }} 11 | 12 |
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 |
11 | {{ forms.render(form) }} 12 | 13 |
14 |
15 |
16 |
17 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/main/misc/jiathis_share.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 | 6 | 7 | 8 |
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 | 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 |
6 |
7 |
8 | {% import "_form.html" as forms %} 9 |
10 | {{ forms.render(form) }} 11 | 12 |
13 |
14 | 15 | 16 | Click here if you forget your password? 17 |
18 |
19 |
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 |
6 |
7 | 11 |
12 |
13 |
14 |
15 |
16 |
17 | {% import "_form.html" as forms %} 18 |
19 | {{ forms.render(form) }} 20 | 21 |
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 |
6 |
7 |
8 |
9 |
10 |

{{ blog_meta.name }}

11 |
12 | {{ blog_meta.subtitle }} 13 |
14 |
15 |
16 |
17 |
18 | {% endblock %} 19 | 20 | {% block main %} 21 | 22 |
23 |
24 |

Nothing Found

25 |
26 |
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 |
11 | {{ forms.render(form) }} 12 | {% if g.identity.allow_su %} 13 |
14 | 17 |
18 | 21 |
22 | {% endif %} 23 | 24 |
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 | 11 |
12 | 13 | {% elif field.type == "RadioField" %} 14 | {{ field.label }} 15 | {% for subfield in field %} 16 |
17 | 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 |
9 | {{ forms.render(form) }} 10 | 11 |
12 |
13 | 14 | 15 | {% for comment in comments %} 16 | 17 | 21 | 31 | 34 | 35 | {% endfor %} 36 |
18 | 19 | user-avatar 20 | 22 |

{{ comment.author }} 23 | {% if comment.homepage %} 24 | 25 | {% endif %} 26 |

27 |
28 | {{ comment.html_content|safe }} 29 |
30 |
32 | {{ comment.pub_time.strftime('%Y/%m/%d') }} 33 |
-------------------------------------------------------------------------------- /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 | 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 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | {% for post in posts.items %} 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | {% else %} 30 | 31 | {% endfor %} 32 | 33 |
No.TitlePublishUpdateVisitedVerbose
{{ loop.index }}{{ post.post.title }}{{ post.post.pub_time.strftime('%Y-%m-%d %H:%M:%S') }}{{ post.post.update_time.strftime('%Y-%m-%d %H:%M:%S') }}{{ post.visit_count }}{{ post.verbose_count_base }}
No records yet
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 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | {% for user in users %} 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 31 | 32 | {% else %} 33 | 34 | {% endfor %} 35 | 36 |
No.UsernameEmailIs ActiveEmail ConfirmedIs SuperuserActions
{{ loop.index }}{{ user.username }}{{ user.email }}{{ user.is_active }}{{ user.is_email_confirmed }}{{ user.is_superuser }} 29 | 30 |
No user yet
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 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | {% for tracker in trackers.items %} 27 | 28 | 29 | 30 | 31 | 32 | 33 | {% else %} 34 | 35 | {% endfor %} 36 | 37 |
No.IPAgentUpdate
{{ loop.index }}{{ tracker.ip }}{{ tracker.user_agent }}{{ tracker.create_time.strftime('%Y-%m-%d %H:%M:%S') }}
No records yet
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 |
6 |
7 | 11 |
12 |
13 |
14 |
15 |
16 |
17 | {% import "_form.html" as forms %} 18 |
19 | {{ forms.render(form) }} 20 | 21 |
22 | 23 |
24 | 25 | {% if user.is_email_confirmed %} 26 |
27 |

Your email has been confirmed!

28 |
29 | {% else %} 30 |
31 | {% if email_resend_flag %} 32 | {% set msg = 'Your confirmation email has been sent, click the button to resend email' %} 33 | {% set button = 'Resend email' %} 34 | {% else %} 35 | {% set msg = 'Click the button to send confirmation email' %} 36 | {% set button = 'Confirm email' %} 37 | {% endif %} 38 |

{{ msg }}

39 | 40 |
41 |
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 |
10 |
11 | 12 | 13 |
14 | 18 |
19 |
20 | 24 |
25 |
26 |
27 | 28 | 29 |
30 | 34 |
35 |
36 | 40 |
41 |
42 | 43 |
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 |
6 |
7 |
8 |
9 |
10 |

{{ blog_meta.name }}

11 |
12 | {{ blog_meta.subtitle }} 13 |
14 |
15 |
16 |
17 |
18 | {% endblock %} 19 | 20 | {% block main %} 21 |
22 |
23 |
24 |
25 | {% for post in posts.items %} 26 |
27 |
28 | 29 |
30 |

{{ post.title }}


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 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | {% for widget in widgets %} 21 | 22 | 23 | 26 | 27 | 28 | 33 | 34 | {% else %} 35 | 36 | {% endfor %} 37 | 38 |
No.TitlePriorityUpdate TimeActions
{{ loop.index }} 24 | {{ widget.title }} 25 | {{ widget.priority }}{{ widget.update_time.strftime('%Y-%m-%d %H:%M:%S') }} 29 | 30 |   31 | 32 |
No widgets yet
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 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | {% for user in users %} 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 33 | 34 | {% else %} 35 | 36 | {% endfor %} 37 | 38 |
No.UsernameEmailIs ActiveLast LoginCreate DateActions
{{ loop.index }}{{ user.username }}{{ user.email }}{{ user.is_active }}{{ user.last_login.strftime('%Y/%m/%d %H:%M:%S') }}{{ user.create_time.strftime('%Y/%m/%d') }} 29 | 30 |   31 | 32 |
No user yet
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 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | {% for post in posts.items %} 23 | 24 | 25 | 28 | 29 | 30 | 31 | 32 | 33 | 38 | 39 | {% else %} 40 | 41 | {% endfor %} 42 | 43 |
No.TitleAbstractAuthorPublishUpdateWeightActions
{{ loop.index }} 26 | {{ post.title }} 27 | {{ post.abstract }}{{ post.author.username }}{{ post.pub_time.strftime('%Y-%m-%d %H:%M:%S') }}{{ post.update_time.strftime('%Y-%m-%d %H:%M:%S') }}{{ post.weight }} 34 | 35 |   36 | 37 |
No {{ post_type }}s yet
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 |
33 | {{ forms.render(form) }} 34 |
35 | 36 |
37 | 38 |
39 | 40 | 45 |
46 |

47 | 48 |
49 | 50 |
51 | 52 | 53 | 54 | 55 |
56 |
57 | 58 | 59 |
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 | 24 |   25 | {% endfor %} 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | {% for post in posts.items %} 42 | 43 | 44 | 47 | 48 | 49 | 50 | 51 | 52 | 57 | 58 | {% else %} 59 | 60 | {% endfor %} 61 | 62 |
No.TitleAbstractAuthorPublish DateUpdate DatePostTypeActions
{{ loop.index }} 45 | {{ post.title }} 46 | {{ post.abstract }}{{ post.author.username }}{{ post.pub_time.strftime('%Y-%m-%d %H:%M:%S') }}{{ post.update_time.strftime('%Y-%m-%d %H:%M:%S') }}{{ post.post_type }} 53 | 54 |   55 | 56 |
No posts yet
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 |
33 | {{ forms.render(form) }} 34 | 35 |
36 | 37 | 38 | 43 |
44 | 45 |
46 | 47 |
48 | 49 | 50 | 51 | 52 |
53 |
54 | 55 |
56 | 57 |
58 | 59 | 60 | 61 | 62 |
63 |
64 | 65 | 66 |
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 | 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 |
11 |
12 |
13 |
14 |
15 |

{{ post.title }}

16 | 17 | Posted by {{ post.author.display_name }} on {{ moment(post.pub_time).format('YYYY/MM/DD, h:mm a') }} 18 | Tags: 19 | {% for tag in post.tags %} {{ tag }} {% endfor %} 20 | 21 |
22 |
23 |
24 |
25 |
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 |
51 |
52 | 53 |
54 | 55 |
56 | {% endif %} 57 |
58 | {% if allow_donate %} 59 |
60 | 61 |
62 |

{{ donation_msg }}

63 | 64 | | 65 | 66 |
67 | 68 | 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 |
6 |
7 |
8 |
9 |
10 |

{{ blog_meta.wechat_name | safe }}

11 |
12 | {{ blog_meta.wechat_subtitle }} 13 |
14 |
15 |
16 |
17 |
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 | 35 | 38 |
39 | {% if not loop.last %} 40 |
41 | {% endif %} 42 | {% else %} 43 |

44 | No articles found here 45 |

46 | {% endfor %} 47 | 48 |
    49 | {% if posts.has_next %} 50 | 55 | {% endif %} 56 | {% if posts.has_prev %} 57 | 60 | {% endif %} 61 |
62 |
63 | 64 | 65 |
66 |

Search


67 |
68 |
69 |
70 |
71 | 72 |
73 |
74 | 75 |
76 | 77 |
78 | 79 |

Tags


80 | {% for tag in tags %} 81 | 96 |   97 | {% endfor %} 98 | 99 | 100 | {% for widget in widgets %} 101 |

{{ widget.title | safe }}


102 |
103 | {{ widget.html_content | safe }} 104 |
105 | {% endfor %} 106 | 107 | 108 |
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 |
11 |
12 |
13 |
14 |
15 |

{{ post.title }}

16 | 17 | Posted by {{ post.author.display_name }} on {{ moment(post.pub_time).format('YYYY/MM/DD, h:mm a') }} 18 | {% if post.category%} 19 | Category: 20 | {{ post.category }} 21 | {% endif %} 22 | Tags: 23 | {% for tag in post.tags %} {{ tag }} {% endfor %} 24 | 25 | {% if g.identity.allow_edit or post.author==current_user%} 26 | 27 | 28 | 29 | 30 | {% endif %} 31 |
32 |
33 |
34 |
35 |
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 |
61 |
62 | 63 |
64 | 65 |
66 | {% endif %} 67 |
68 | {% if allow_donate %} 69 |
70 | 71 |
72 |

{{ donation_msg }}

73 | 74 | | 75 | 76 |
77 | 78 | 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 |
8 |
9 | 14 |
15 |
16 |
17 |
18 | 19 |
20 |
21 | 24 | 25 |
26 |
27 | 28 |
29 |
30 |
31 | 32 |
33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | {% for comment in comments.items %} 43 | 44 | 45 | 55 | 69 | 75 | 76 | {% else %} 77 | 78 | {% endfor %} 79 | 80 |
AuthorCommentAction
46 |

{{ comment.author }}

47 |

48 | 49 | 50 | {% if comment.homepage %} 51 | 52 | {% endif %} 53 |

54 |
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 |
70 | {% if status!='approved' %} 71 | Approve 72 | {% endif %} 73 | Delete 74 |
No records yet!
81 | 82 | Clear 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 |
7 |
8 |
9 |
10 |
11 |

About Author

12 |
13 | {{ blog_meta.subtitle }} 14 |
15 |
16 |
17 |
18 |
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 | 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 |
    91 | {% if posts.has_next %} 92 | 95 | {% endif %} 96 | {% if posts.has_prev %} 97 | 98 | {% endif %} 99 |
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 |
7 |
8 |
9 |
10 |
11 |

{{ blog_meta.name }}

12 |
13 | {{ blog_meta.subtitle }} 14 |
15 |
16 |
17 |
18 |
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 | 36 | 37 | 40 |
41 | {% if not loop.last %} 42 |
43 | {% endif %} 44 | {% else %} 45 |

46 | No articles found here 47 |

48 | {% endfor %} 49 | 50 |
    51 | {% if posts.has_next %} 52 | 57 | {% endif %} 58 | {% if posts.has_prev %} 59 | 63 | {% endif %} 64 |
65 |
66 | 67 | 68 |
69 |

Search


70 |
71 |
72 |
73 |
74 | 75 |
76 |
77 | 78 |
79 | 80 |
81 | 82 |

Tags


83 | {% for tag in tags %} 84 | 99 |   100 | {% endfor %} 101 | 102 | 103 |

Category


104 |
105 | {% for category in category_cursor %} 106 | {% if category and category.name %} 107 | {% if cur_category == category.name %} 108 | {{ category.name }} 109 | 110 |  {{ category.count }}  111 | {% else %} 112 | {{ category.name }} 113 |  {{ category.count }}  114 | {% endif %} 115 | {% endif %} 116 | {% endfor %} 117 |
118 | 119 | 120 | {% for widget in widgets %} 121 |

{{ widget.title | safe }}


122 |
123 | {{ widget.html_content | safe }} 124 |
125 | {% endfor %} 126 | 127 |
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 |
18 |
19 |
20 |
21 | {% import "_form.html" as forms %} 22 | {{ form.csrf_token() }} 23 | {{ form.post_id() }} 24 | {{ form.post_type() }} 25 | {{ form.from_draft() }} 26 | {% if form.title.errors %} 27 |
28 | {{ form.title.label }} 29 | {{ form.title(class_="form-control") }} 30 | {{ form.title.errors|join(' ') }} 31 |
32 | {% else %} 33 |
34 | {{ form.title.label }} 35 | {{ form.title(class_="form-control") }} 36 |
37 | {% endif %} 38 | 39 | {% if form.slug.errors %} 40 |
41 | {{ form.slug.label }} 42 | {{ form.slug(class_="form-control") }} 43 | 44 | {{ form.slug.errors|join(' ') }} 45 |
46 | {% else %} 47 |
48 | {{ form.slug.label }} 49 | {{ form.slug(class_="form-control") }} 50 | 51 |
52 | {% endif %} 53 | 54 |
55 | {{ form.weight.label }} 56 | {{ form.weight(class_="form-control") }} 57 | 58 |
59 | 60 |
61 | {{ form.raw.label }} 62 | 63 | {{ form.raw(class_="form-control", rows=30, data_provide="markdown") }} 64 |
65 | 66 |
67 | {{ form.abstract.label }} 68 | {{ form.abstract(class_="form-control", rows=5) }} 69 |
70 | 71 |
72 | 73 |
74 |
75 |

Tags

76 |
77 |
78 | {{ form.tags_str(class_="form-control") }} 79 |
80 | 81 | 86 |
87 |
88 |
89 | 90 |
91 |

Category

92 |
93 |
94 | {{ form.category(class_="form-control") }} 95 |
96 | 97 | 102 |
103 |
104 |
105 |
106 |



107 |
108 | 109 | 110 | 111 |
112 | 113 |
114 |
115 | 116 | 117 |
118 | {% endblock %} 119 | 120 | {% block js %} 121 | 122 | 123 | 124 | 155 | {% endblock %} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # About OctBlog 2 | 3 | [![Join the chat at https://gitter.im/flyhigher139/OctBlog](https://badges.gitter.im/flyhigher139/OctBlog.svg)](https://gitter.im/flyhigher139/OctBlog?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 4 | [![](https://images.microbadger.com/badges/image/gevin/octblog.svg)](http://microbadger.com/images/gevin/octblog "Get your own image badge on microbadger.com") [![](https://images.microbadger.com/badges/version/gevin/octblog.svg)](http://microbadger.com/images/gevin/octblog "Get your own version badge on microbadger.com") [![Build Status](https://travis-ci.org/flyhigher139/OctBlog.svg?branch=master)](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 | 115 | 116 | 117 | {%block header %}{% endblock %} 118 | 119 | {%block main %}{% endblock %} 120 | 121 | 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 | 177 | 178 | 179 | 180 | 181 | {%block header %}{% endblock %} 182 |
183 | 184 | 185 | {%block main %}{% endblock %} 186 |
187 |
188 | 189 | 190 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | {% block js %} 233 | {% endblock %} 234 | 235 | 236 | --------------------------------------------------------------------------------