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}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Section title
+
+
+
+
+ # |
+ Header |
+ Header |
+ Header |
+ Header |
+
+
+
+
+ 1,001 |
+ Lorem |
+ ipsum |
+ dolor |
+ sit |
+
+
+ 1,002 |
+ amet |
+ consectetur |
+ adipiscing |
+ elit |
+
+
+ 1,003 |
+ Integer |
+ nec |
+ odio |
+ Praesent |
+
+
+ 1,003 |
+ libero |
+ Sed |
+ cursus |
+ ante |
+
+
+ 1,004 |
+ dapibus |
+ diam |
+ Sed |
+ nisi |
+
+
+ 1,005 |
+ Nulla |
+ quis |
+ sem |
+ at |
+
+
+ 1,006 |
+ nibh |
+ elementum |
+ imperdiet |
+ Duis |
+
+
+ 1,007 |
+ sagittis |
+ ipsum |
+ Praesent |
+ mauris |
+
+
+ 1,008 |
+ Fusce |
+ nec |
+ tellus |
+ sed |
+
+
+ 1,009 |
+ augue |
+ semper |
+ porta |
+ Mauris |
+
+
+ 1,010 |
+ massa |
+ Vestibulum |
+ lacinia |
+ arcu |
+
+
+ 1,011 |
+ eget |
+ nulla |
+ Class |
+ aptent |
+
+
+ 1,012 |
+ taciti |
+ sociosqu |
+ ad |
+ litora |
+
+
+ 1,013 |
+ torquent |
+ per |
+ conubia |
+ nostra |
+
+
+ 1,014 |
+ per |
+ inceptos |
+ himenaeos |
+ Curabitur |
+
+
+ 1,015 |
+ sodales |
+ ligula |
+ in |
+ libero |
+
+
+
+
+
+{% 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") %}
+