diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..02ed0c7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +*.egg-info +__pycache__ +*.pyc +*.pyo +*.pyd +*.sqlite diff --git a/requirements.txt b/requirements.txt index c4d8b23..4d96442 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ +email-validator Flask Flask-Admin Flask-Login diff --git a/scribeengine/__init__.py b/scribeengine/__init__.py index 6585efa..678d7cd 100644 --- a/scribeengine/__init__.py +++ b/scribeengine/__init__.py @@ -29,6 +29,7 @@ from flask_mail import Mail from flask_themes2 import Themes, packaged_themes_loader, theme_paths_loader, load_themes_from from flask_user import UserManager +from scribeengine.admin import admin from scribeengine.config import read_config_from_file from scribeengine.db import db from scribeengine.models import User @@ -61,14 +62,13 @@ def create_app(config_file=None): Mail(application) Themes(application, app_identifier='ScribeEngine', loaders=[ _scribeengine_themes_loader, packaged_themes_loader, theme_paths_loader]) - print(application.root_path) - print(application.theme_manager.themes) # Set up database db.init_app(application) db.create_all(app=application) # Setup Flask-User UserManager(application, db, User) # Register all the blueprints + application.register_blueprint(admin) application.register_blueprint(blog) application.register_blueprint(account) # Return the application object diff --git a/scribeengine/admin.py b/scribeengine/admin.py new file mode 100644 index 0000000..65f7f9a --- /dev/null +++ b/scribeengine/admin.py @@ -0,0 +1,42 @@ +# -*- 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.admin` module sets up the admin interface +""" +from flask import Blueprint + +# from scribeengine.db import session +from scribeengine.helpers import render_admin +from scribeengine.models import Post + +admin = Blueprint('admin', __name__, url_prefix='/admin') + + +@admin.route('', methods=['GET']) +def index(): + return render_admin('index.html') + + +@admin.route('/posts', methods=['GET']) +def posts(): + posts = Post.query.limit(10).all() + return render_admin('posts.html', posts=posts) diff --git a/scribeengine/db.py b/scribeengine/db.py index 6a9ca68..18af498 100644 --- a/scribeengine/db.py +++ b/scribeengine/db.py @@ -39,3 +39,4 @@ Boolean = db.Boolean DateTime = db.DateTime relationship = db.relationship backref = db.backref +session = db.session diff --git a/scribeengine/helpers.py b/scribeengine/helpers.py index 146f23b..6c53cd2 100644 --- a/scribeengine/helpers.py +++ b/scribeengine/helpers.py @@ -51,3 +51,11 @@ def render(template, **context): """ context.update({'site': get_site_details(), 'archives': []}) return render_theme_template(get_current_theme(), template, **context) + + +def render_admin(template, **context): + """ + Render a template, after selecting a theme + """ + context.update({'site': get_site_details()}) + return render_theme_template('admin', template, **context) diff --git a/scribeengine/models.py b/scribeengine/models.py index ee1186b..be4c87f 100644 --- a/scribeengine/models.py +++ b/scribeengine/models.py @@ -22,18 +22,13 @@ """ The :mod:`~scribeengine.models` module contains all the database models """ +from datetime import datetime + +from box import Box 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) -) + relationship permissions_roles = Table( @@ -43,10 +38,10 @@ permissions_roles = Table( ) -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) +nodes_taxonomies = Table( + 'nodes_terms', + Column('node_id', Integer, ForeignKey('nod.id'), primary_key=True), + Column('taxonomy_id', Integer, ForeignKey('taxonomy.id'), primary_key=True) ) @@ -57,32 +52,90 @@ roles_users = Table( ) -class Category(Model): +class Taxonomy(Model): """ - This is a category for blog posts. + This is a grouping of related terms, like a tags or categories """ - __tablename__ = 'categories' + __tablename__ = 'taxonomies' 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) + slug = Column(String(255), nullable=False, index=True, unique=True) + + def __str__(self): + return self.name -class Comment(Model): +class Node(Model): """ - All blog posts have comments. This is a single comment. + Nodes are the basic content type """ - __tablename__ = 'comments' + __tablename__ = 'nodes' 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()) + slug = Column(String(255), nullable=False, index=True, unique=True) + type = Column(String(255), nullable=False, index=True) + created = Column(DateTime, nullable=False, default=datetime.utcnow) + modified = Column(DateTime, nullable=False, default=datetime.utcnow) + revision_id = Column(Integer, ForeignKey('revisions.id'), nullable=False, index=True) + + current_revision = relationship('Revision', backref='node') + + @property + def complete(self): + """Return a "full" or "complete" node, with all fields and data""" + node_dict = Box({ + 'id': self.id, + 'slug': self.slug, + 'type': self.type, + 'created': self.created, + 'modified': self.modified, + 'title': self.current_revision.title, + 'body': self.current_revision.body, + 'format': self.current_revision.format + }) + for field in self.fields: + node_dict[field.name] = { + 'name': field.name, + 'type': field.type, + 'title': field.field.title, + 'data': field.data + } + return node_dict + + +class Revision(Model): + """ + A version of a node + """ + __tablename__ = 'revisions' + + id = Column(Integer, primary_key=True) + version = Column(String(255), nullable=False) + title = Column(String(255), nullable=False) + body = Column(Text) + format = Column(Text, nullable=False) + slug = Column(String(255), nullable=False) + created = Column(DateTime, nullable=False, index=True, default=datetime.utcnow) + node_id = Column(Integer, ForeignKey('nodes.id'), nullable=False) + + node = relationship('Node', backref='revisions') + + +class Field(Model): + """ + A field is a model for extra field types on nodes + """ + __tablename__ = 'fields' + + id = Column(Integer, primary_key=True) + name = Column(String(255), nullable=False, unique=True, index=True) + type = Column(String(255), nullable=False) + title = Column(String(255), nullable=False) + description = Column(Text) + created = Column(DateTime, nullable=False, default=datetime.utcnow) + modified = Column(DateTime, nullable=False, default=datetime.utcnow) class File(Model): @@ -99,6 +152,9 @@ class File(Model): path = Column(String(255)) size = Column(Integer, default=0) + def __str__(self): + return self.filename + class MediaType(Model): """ @@ -111,20 +167,8 @@ class MediaType(Model): 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()) + def __str__(self): + return self.title class Permission(Model): @@ -137,26 +181,8 @@ class Permission(Model): 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) + def __str__(self): + return self.name class Role(Model): @@ -171,16 +197,23 @@ class Role(Model): permissions = relationship('Permission', backref='roles', secondary=permissions_roles) + def __str__(self): + return self.name -class Tag(Model): + +class Term(Model): """ - A tag, an unstructured category, for blog posts. + A term is an item in a taxonomy. A term can be a category name, or a tag in a blog post. """ - __tablename__ = 'tags' + __tablename__ = 'terms' id = Column(Integer, primary_key=True) name = Column(String(100), nullable=False) - url = Column(String(255), nullable=False, index=True) + slug = Column(String(255), nullable=False, index=True) + taxonomy_id = Column(Integer, ForeignKey('taxonomies.id')) + + def __str__(self): + return self.name class User(Model, UserMixin): @@ -204,7 +237,7 @@ class User(Model, UserMixin): comments = relationship('Comment', backref='user') files = relationship('File', backref='user') - posts = relationship('Post', backref='user') + posts = relationship('Post', backref='author') roles = relationship('Role', backref='users', secondary=roles_users) def has_permission(self, permission): @@ -229,6 +262,9 @@ class User(Model, UserMixin): else: return False + def __str__(self): + return str(id(self)) + class Variable(Model): """ diff --git a/scribeengine/themes/admin/info.json b/scribeengine/themes/admin/info.json new file mode 100644 index 0000000..1af7056 --- /dev/null +++ b/scribeengine/themes/admin/info.json @@ -0,0 +1,12 @@ +{ + "application": "ScribeEngine", + "identifier": "admin", + "name": "Admin", + "author": "Raoul Snyman", + "description": "A basic theme built using Bootstrap 4 and based on Bootstrap 4's blog example", + "website": "https://scribeengine.org", + "license": "GPL3+", + "preview": "screenshot.png", + "doctype": "html5", + "version": "0.1" +} diff --git a/scribeengine/themes/admin/static/css/dashboard.css b/scribeengine/themes/admin/static/css/dashboard.css new file mode 100644 index 0000000..8b0fa72 --- /dev/null +++ b/scribeengine/themes/admin/static/css/dashboard.css @@ -0,0 +1,100 @@ +body { + font-size: .875rem; +} + +.feather { + width: 16px; + height: 16px; + vertical-align: text-bottom; +} + +/* + * Sidebar + */ + +.sidebar { + position: fixed; + top: 0; + /* rtl:raw: + right: 0; + */ + bottom: 0; + /* rtl:remove */ + left: 0; + z-index: 100; /* Behind the navbar */ + padding: 48px 0 0; /* Height of navbar */ + box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1); +} + +@media (max-width: 767.98px) { + .sidebar { + top: 5rem; + } +} + +.sidebar-sticky { + position: relative; + top: 0; + height: calc(100vh - 48px); + padding-top: .5rem; + overflow-x: hidden; + overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */ +} + +.sidebar .nav-link { + font-weight: 500; + color: #333; +} + +.sidebar .nav-link .feather { + margin-right: 4px; + color: #727272; +} + +.sidebar .nav-link.active { + color: #007bff; +} + +.sidebar .nav-link:hover .feather, +.sidebar .nav-link.active .feather { + color: inherit; +} + +.sidebar-heading { + font-size: .75rem; + text-transform: uppercase; +} + +/* + * Navbar + */ + +.navbar-brand { + padding-top: .75rem; + padding-bottom: .75rem; + font-size: 1rem; + background-color: rgba(0, 0, 0, .25); + box-shadow: inset -1px 0 0 rgba(0, 0, 0, .25); +} + +.navbar .navbar-toggler { + top: .25rem; + right: 1rem; +} + +.navbar .form-control { + padding: .75rem 1rem; + border-width: 0; + border-radius: 0; +} + +.form-control-dark { + color: #fff; + background-color: rgba(255, 255, 255, .1); + border-color: rgba(255, 255, 255, .1); +} + +.form-control-dark:focus { + border-color: transparent; + box-shadow: 0 0 0 3px rgba(255, 255, 255, .25); +} diff --git a/scribeengine/themes/admin/templates/base.html b/scribeengine/themes/admin/templates/base.html new file mode 100644 index 0000000..56f66f3 --- /dev/null +++ b/scribeengine/themes/admin/templates/base.html @@ -0,0 +1,283 @@ + + + + + + {{title}} + + + + + + + + + + + + + + + + + +
+
+ + +
+
+

Dashboard

+
+
+ + +
+ +
+
+ + + +

Section title

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
#HeaderHeaderHeaderHeader
1,001Loremipsumdolorsit
1,002ametconsecteturadipiscingelit
1,003IntegernecodioPraesent
1,003liberoSedcursusante
1,004dapibusdiamSednisi
1,005Nullaquissemat
1,006nibhelementumimperdietDuis
1,007sagittisipsumPraesentmauris
1,008Fuscenectellussed
1,009auguesemperportaMauris
1,010massaVestibulumlaciniaarcu
1,011egetnullaClassaptent
1,012tacitisociosquadlitora
1,013torquentperconubianostra
1,014perinceptoshimenaeosCurabitur
1,015sodalesligulainlibero
+
+
+{% block content $} +{$ endblock %} +
+
+ + + + + + + diff --git a/scribeengine/themes/admin/templates/index.html b/scribeengine/themes/admin/templates/index.html new file mode 100644 index 0000000..e1e51b5 --- /dev/null +++ b/scribeengine/themes/admin/templates/index.html @@ -0,0 +1,3 @@ +{% extends theme("base.html") %} +{% block content $} +{$ endblock %} diff --git a/scribeengine/themes/quill/info.json b/scribeengine/themes/quill/info.json index d0523ff..d9e06d4 100644 --- a/scribeengine/themes/quill/info.json +++ b/scribeengine/themes/quill/info.json @@ -3,7 +3,7 @@ "identifier": "quill", "name": "Quill", "author": "Raoul Snyman", - "description": "A basic theme built using Bootstrap 4 and based on Bootstrap 4's blog example", + "description": "An administration theme, based on the Bootstrap dashboard example", "website": "https://scribeengine.org", "license": "GPL3+", "preview": "screenshot.png", diff --git a/scribeengine/themes/quill/templates/base.html b/scribeengine/themes/quill/templates/base.html index 772f969..fe4f140 100644 --- a/scribeengine/themes/quill/templates/base.html +++ b/scribeengine/themes/quill/templates/base.html @@ -5,13 +5,14 @@ {{title}} - + {% include theme("header.html") %} {% block content %}{% endblock %} {% include theme("footer.html") %} + diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..aa079ec --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[flake8] +max-line-length=120