From 45ee572660295bfd547fdbca06fbf24596a6a4ea Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Thu, 8 Jun 2017 23:25:41 -0700 Subject: [PATCH] Initial porting of ScribeEngine to Flask --- README.rst | 28 ++++ config.ini.default | 15 ++ devserver.py | 3 + requirements.txt | 12 ++ scribeengine/__init__.py | 61 +++++++++ scribeengine/config.py | 69 ++++++++++ scribeengine/db.py | 41 ++++++ scribeengine/models.py | 241 +++++++++++++++++++++++++++++++++ scribeengine/themes.py | 41 ++++++ scribeengine/views/__init__.py | 24 ++++ scribeengine/views/account.py | 35 +++++ setup.py | 39 ++++++ tests | 0 13 files changed, 609 insertions(+) create mode 100644 README.rst create mode 100644 config.ini.default create mode 100644 devserver.py create mode 100644 requirements.txt create mode 100644 scribeengine/__init__.py create mode 100644 scribeengine/config.py create mode 100644 scribeengine/db.py create mode 100644 scribeengine/models.py create mode 100644 scribeengine/themes.py create mode 100644 scribeengine/views/__init__.py create mode 100644 scribeengine/views/account.py create mode 100644 setup.py create mode 100644 tests diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..b2511b5 --- /dev/null +++ b/README.rst @@ -0,0 +1,28 @@ +ScribeEngine +============ + +ScribeEngine is an open source blog engine written in Python and Flask. + +Installation and Setup +---------------------- + +Install ScribeEngine using ``pip``:: + + pip install ScribeEngine + +Create a config file with these options set:: + + [mail] + server = mail.example.com + port = 25 + use_tls = false + username = me + password = secret + + [sqlalchemy] + database_uri = sqlite:///scribeengine.sqlite + +Run a local server:: + + $ python -m scribeengine.application.run() + diff --git a/config.ini.default b/config.ini.default new file mode 100644 index 0000000..7ec8ee4 --- /dev/null +++ b/config.ini.default @@ -0,0 +1,15 @@ +[sqlalchemy] + + +[mail] +username = email@example.com +password = password +default_sender = sender +server = smtp.gmail.com +port = 465 +use_ssl = true +use_tls = false + +[theme] +paths = +default = diff --git a/devserver.py b/devserver.py new file mode 100644 index 0000000..f2af6e6 --- /dev/null +++ b/devserver.py @@ -0,0 +1,3 @@ +#!/usr/bin/env python3 +from scribeengine import application +application.run(host='0.0.0.0', port=8080, debug=True) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e7d1a10 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,12 @@ +Flask +Flask-Admin +Flask-Login +Flask-Mail +Flask-SQLAlchemy +Flask-Themes +Flask-Uploads +Flask-User +Flask-WTF +passlib +py-bcrypt +pycrypto diff --git a/scribeengine/__init__.py b/scribeengine/__init__.py new file mode 100644 index 0000000..426794f --- /dev/null +++ b/scribeengine/__init__.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=80 tabstop=4 softtabstop=4 + +############################################################################### +# ScribeEngine - Open Source Blog Software # +# --------------------------------------------------------------------------- # +# Copyright (c) 2010-2017 Raoul Snyman # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### +""" +The :mod:`~scribeengine` module sets up and runs ScribeEngine +""" +import os + +from flask import Flask +from flask_mail import Mail +from flask_themes import setup_themes +from flask_user import SQLAlchemyAdapter, UserManager + +from scribeengine.config import read_config_from_file +from scribeengine.db import db +from scribeengine.models import User + + +def create_app(config_file=None): + """ + Create the application object + """ + application = Flask('ScribeEngine') + # Set up configuration + if not config_file: + if os.environ.get('SCRIBEENGINE_CONFIG'): + config_file = os.environ['SCRIBEENGINE_CONFIG'] + elif os.path.exists('sundaybuilder.conf'): + config_file = 'sundaybuilder.conf' + if config_file: + application.config.update(read_config_from_file(config_file)) + # Set up mail, themes + Mail(application) + setup_themes(application, app_identifier='ScribeEngine') + # Set up database + db.init_app(application) + db.create_all() + # Setup Flask-User + db_adapter = SQLAlchemyAdapter(db, User) # Register the User model + user_manager = UserManager(db_adapter, application) # Initialize Flask-User + # Register all the blueprints + # Return the application object + return application diff --git a/scribeengine/config.py b/scribeengine/config.py new file mode 100644 index 0000000..c711760 --- /dev/null +++ b/scribeengine/config.py @@ -0,0 +1,69 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=80 tabstop=4 softtabstop=4 + +############################################################################### +# ScribeEngine - Open Source Blog Software # +# --------------------------------------------------------------------------- # +# Copyright (c) 2010-2017 Raoul Snyman # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### +""" +The :mod:`~scribeengine.config` module contains helper classes for config handling +""" +from configparser import ConfigParser + +BOOLEAN_VALUES = ['yes', 'true', 'on', 'no', 'false', 'off'] + + +def _fix_special_cases(config): + """ + Deal with special cases + """ + if 'THEME_PATHS' in config: + config['THEME_PATHS'] = [p.strip() for p in config['THEME_PATHS'].split(',') if p.strip()] + + +def read_config_from_file(filename): + """ + Read the Flask configuration from a config file + """ + flask_config = {} + config = ConfigParser() + config.read(filename) + for section in config.sections(): + for option in config.options(section): + # Get the value, skip it if it is blank + string_value = config.get(section, option) + if not string_value: + continue + # Try to figure out what type it is + if string_value.isnumeric() and '.' in string_value: + value = config.getfloat(section, option) + elif string_value.isnumeric(): + value = config.getint(section, option) + elif string_value.lower() in BOOLEAN_VALUES: + value = config.getboolean(section, option) + else: + value = string_value + # Set up the configuration key + if section == 'flask': + # Options in the flask section don't need FLASK_* + key = option.upper() + else: + key = '{}_{}'.format(section, option).upper() + # Save this into our flask config dictionary + flask_config[key] = value + _fix_special_cases(flask_config) + return flask_config diff --git a/scribeengine/db.py b/scribeengine/db.py new file mode 100644 index 0000000..6a9ca68 --- /dev/null +++ b/scribeengine/db.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=80 tabstop=4 softtabstop=4 + +############################################################################### +# ScribeEngine - Open Source Blog Software # +# --------------------------------------------------------------------------- # +# Copyright (c) 2010-2017 Raoul Snyman # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### +""" +The :mod:`~scribeengine.db` module sets up SQLAlchemy +""" +from flask_sqlalchemy import SQLAlchemy + +# Get the db object +db = SQLAlchemy() + +# Extract the classes to make them prettier +Model = db.Model +Table = db.Table +Column = db.Column +ForeignKey = db.ForeignKey +String = db.String +Text = db.Text +Integer = db.Integer +Boolean = db.Boolean +DateTime = db.DateTime +relationship = db.relationship +backref = db.backref diff --git a/scribeengine/models.py b/scribeengine/models.py new file mode 100644 index 0000000..c1d9cc8 --- /dev/null +++ b/scribeengine/models.py @@ -0,0 +1,241 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=80 tabstop=4 softtabstop=4 + +############################################################################### +# ScribeEngine - Open Source Blog Software # +# --------------------------------------------------------------------------- # +# Copyright (c) 2010-2017 Raoul Snyman # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### +""" +The :mod:`~scribeengine.models` module contains all the database models +""" +from flask_user import UserMixin +from sqlalchemy import func + +from scribeengine.db import Model, Table, Column, ForeignKey, String, Text, Integer, Boolean, DateTime, \ + relationship, backref + + +categories_posts = Table( + 'categories_posts', + Column('category_id', Integer, ForeignKey('categories.id'), primary_key=True), + Column('post_id', Integer, ForeignKey('posts.id'), primary_key=True) +) + + +permissions_roles = Table( + 'permissions_roles', + Column('permission_id', Integer, ForeignKey('permissions.id'), primary_key=True), + Column('role_id', Integer, ForeignKey('roles.id'), primary_key=True) +) + + +posts_tags = Table( + 'posts_tags', + Column('post_id', Integer, ForeignKey('posts.id'), primary_key=True), + Column('tag_id', Integer, ForeignKey('tags.id'), primary_key=True) +) + + +roles_users = Table( + 'roles_users', + Column('user_id', Integer, ForeignKey('users.id'), primary_key=True), + Column('role_id', Integer, ForeignKey('roles.id'), primary_key=True) +) + + +class Category(Model): + """ + This is a category for blog posts. + """ + __tablename__ = 'categories' + + id = Column(Integer, primary_key=True) + name = Column(String(100), nullable=False) + description = Column(Text) + url = Column(String(255), nullable=False, index=True, unique=True) + + +class Comment(Model): + """ + All blog posts have comments. This is a single comment. + """ + __tablename__ = 'comments' + + id = Column(Integer, primary_key=True) + post_id = Column(Integer, ForeignKey('posts.id'), nullable=True) + user_id = Column(Integer, ForeignKey('users.id'), nullable=False) + title = Column(String(100), nullable=False) + body = Column(Text, nullable=False) + status = Column(String(10), default='moderated') + created = Column(DateTime, server_default=func.now()) + modified = Column(DateTime, server_default=func.now()) + + +class File(Model): + """ + A file in the media library. + """ + __tablename__ = 'files' + + id = Column(Integer, primary_key=True) + user_id = Column(Integer, ForeignKey('users.id'), nullable=False) + media_type_id = Column(Integer, ForeignKey('media_types.id'), nullable=False) + filename = Column(String(255), nullable=False, index=True) + mimetype = Column(String(255)) + path = Column(String(255)) + size = Column(Integer, default=0) + + +class MediaType(Model): + """ + Distinguishes between different types of media. + """ + __tablename__ = 'media_types' + + id = Column(Integer, primary_key=True) + title = Column(String(255), nullable=False, index=True) + + files = relationship('File', backref='media_type') + + +class Page(Model): + """ + A page on the blog. This is separate from a blog entry, for things like + about pages. + """ + __tablename__ = 'pages' + + id = Column(Integer, primary_key=True) + title = Column(String(255), nullable=False) + body = Column(Text) + url = Column(String(255), nullable=False, index=True, unique=True) + created = Column(DateTime, server_default=func.now()) + modified = Column(DateTime, server_default=func.now()) + + +class Permission(Model): + """ + A single permission. + """ + __tablename__ = 'permissions' + + id = Column(Integer, primary_key=True) + name = Column(String(80), nullable=False, index=True) + description = Column(Text) + + +class Post(Model): + """ + The most import part of all of this, the blog post. + """ + __tablename__ = 'posts' + + id = Column(Integer, primary_key=True) + user_id = Column(Integer, ForeignKey('users.id'), nullable=False) + title = Column(String(255), nullable=False, index=True) + body = Column(Text, nullable=False, index=True) + url = Column(String(255), nullable=False, index=True) + status = Column(String(10), default='draft', index=True) + comment_status = Column(String(10), default='open') + created = Column(DateTime, server_default=func.now()) + modified = Column(DateTime, server_default=func.now()) + + categories = relationship('Category', backref='posts', secondary=categories_posts) + comments = relationship('Comment', backref='post', order_by='Comment.created.asc()') + tags = relationship('Tag', backref=backref('posts', order_by='Post.created.desc()'), secondary=posts_tags) + + +class Role(Model): + """ + A role defines a set of permissions. + """ + __tablename__ = 'roles' + + id = Column(Integer, primary_key=True) + name = Column(String(80), nullable=False, index=True) + description = Column(Text) + + permissions = relationship('Permission', backref='roles', secondary=permissions_roles) + + +class Tag(Model): + """ + A tag, an unstructured category, for blog posts. + """ + __tablename__ = 'tags' + + id = Column(Integer, primary_key=True) + name = Column(String(100), nullable=False) + url = Column(String(255), nullable=False, index=True) + + +class User(Model, UserMixin): + """ + The user. + """ + __tablename__ = 'users' + + id = Column(Integer, primary_key=True) + email = Column(String(200), nullable=False, unique=True, index=True) + username = Column(String(200), nullable=False, unique=True, index=True) + password = Column(String(64), nullable=False) + nick = Column(String(50), nullable=False, index=True) + first_name = Column(String(100), default='') + last_name = Column(String(100), default='') + homepage = Column(String(200), default='') + timezone = Column(String(200), default='UTC') + activation_key = Column(String(40), default=None) + confirmed_at = Column(DateTime) + active = Column('is_active', Boolean, nullable=False, server_default='0') + + comments = relationship('Comment', backref='user'), + files = relationship('File', backref='user'), + posts = relationship('Post', backref='user'), + roles = relationship('Role', backref='users', secondary=roles_users) + + def has_permission(self, permission): + if isinstance(permission, str): + for role in self.roles: + for perm in role.permissions: + if perm.name == permission: + return True + return False + elif isinstance(permission, Permission): + for role in self.roles: + for perm in role.permissions: + if perm == permission: + return True + return False + elif isinstance(permission, list): + for role in self.roles: + for perm in role.permissions: + if perm.name in permission: + return True + return False + else: + return False + + +class Variable(Model): + """ + System variables. + """ + __tablename__ = 'variables' + + key = Column(String(100), primary_key=True, index=True) + value = Column(String(100), nullable=False) + type = Column(String(10), default='string') diff --git a/scribeengine/themes.py b/scribeengine/themes.py new file mode 100644 index 0000000..099f1cd --- /dev/null +++ b/scribeengine/themes.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=80 tabstop=4 softtabstop=4 + +############################################################################### +# ScribeEngine - Open Source Blog Software # +# --------------------------------------------------------------------------- # +# Copyright (c) 2010-2017 Raoul Snyman # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### +""" +The :mod:`~scribeengine.themes` module contains some theme helper methods +""" +from flask import current_app +from flask_themes import get_theme, render_theme_template + + +def get_current_theme(): + """ + Determine the current theme. + """ + ident = current_app.config.get('THEME_DEFAULT', 'quill') + return get_theme(ident) + + +def render(template, **context): + """ + Render a template, after selecting a theme + """ + return render_theme_template(get_current_theme(), template, **context) diff --git a/scribeengine/views/__init__.py b/scribeengine/views/__init__.py new file mode 100644 index 0000000..5e927aa --- /dev/null +++ b/scribeengine/views/__init__.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=80 tabstop=4 softtabstop=4 + +############################################################################### +# ScribeEngine - Open Source Blog Software # +# --------------------------------------------------------------------------- # +# Copyright (c) 2010-2017 Raoul Snyman # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### +""" +The :mod:`~scribeengine.views` module contains the views for ScribeEngine +""" diff --git a/scribeengine/views/account.py b/scribeengine/views/account.py new file mode 100644 index 0000000..0a081b7 --- /dev/null +++ b/scribeengine/views/account.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=80 tabstop=4 softtabstop=4 + +############################################################################### +# ScribeEngine - Open Source Blog Software # +# --------------------------------------------------------------------------- # +# Copyright (c) 2010-2017 Raoul Snyman # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### +""" +The :mod:`~scribeengine.views.account` module contains the account views +""" +from flask import Blueprint +from pytz import all_timezones + +from scribeengine.themes import render + +account = Blueprint('account', __file__, prefix='/account') + + +@account.route('', methods=['GET']) +def index(self): + return render('/account/index.html', page_title='My Account', timezones=all_timezones) diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..07cf10c --- /dev/null +++ b/setup.py @@ -0,0 +1,39 @@ +""" +The ScribeEngine package +""" +import os +from codecs import open +from setuptools import setup, find_packages + +HERE = os.path.abspath(os.path.dirname(__file__)) + +with open(os.path.join(HERE, 'README.rst'), encoding='utf8') as f: + LONG_DESCRIPTION = f.read() +with open(os.path.join(HERE, 'requirements.txt'), encoding='utf8') as f: + INSTALL_REQUIRES = [line for line in f] + + +setup( + name='ScribeEngine', + version='0.2', + description='A blog engine written in Python', + long_description=LONG_DESCRIPTION, + url='https://launchpad.net/scribeengine', + author='Raoul Snyman', + author_email='raoul@snyman.info', + license='GPLv3+', + classifiers=[ + 'Development Status :: 4 - Beta', + 'Environment :: Web Environment,' + 'Framework :: Flask', + 'Intended Audience :: End Users/Desktop', + 'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)', + 'Operating System :: OS Independent', + 'Programming Language :: Python :: 3', + 'Topic :: Internet :: WWW/HTTP :: Dynamic Content :: Content Management System', + 'Topic :: Internet :: WWW/HTTP :: WSGI :: Application' + ], + keywords='website blog', + packages=find_packages(), + install_requires=INSTALL_REQUIRES +) diff --git a/tests b/tests new file mode 100644 index 0000000..e69de29