Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow to create custom pages and add them to the menu. #2898

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,5 @@ gdrive_credentials
client_secrets.json
gmail.json
/.key

pages/
108 changes: 108 additions & 0 deletions cps/editpage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import os
import flask
from flask import Blueprint, Flask, abort, request
from functools import wraps
from pathlib import Path
from flask_login import current_user, login_required
from werkzeug.exceptions import NotFound

from .render_template import render_title_template
from . import logger, config, ub
from .constants import CONFIG_DIR as _CONFIG_DIR

log = logger.create()

editpage = Blueprint('editpage', __name__)

def edit_required(f):
@wraps(f)
def inner(*args, **kwargs):
if current_user.role_edit() or current_user.role_admin():
return f(*args, **kwargs)
abort(403)

return inner

def _get_checkbox(dictionary, field, default):
new_value = dictionary.get(field, default)
convertor = lambda y: y == "on"
new_value = convertor(new_value)

return new_value

@editpage.route("/admin/page/<string:file>", methods=["GET", "POST"])
@login_required
@edit_required
def edit_page(file):
doc = ""
title = ""
name = ""
icon = "file"
is_enabled = True
order = 0
position = "0"

page = ub.session.query(ub.Page).filter(ub.Page.id == file).first()

try:
title = page.title
name = page.name
icon = page.icon
is_enabled = page.is_enabled
order = page.order
position = page.position
silenceway marked this conversation as resolved.
Show resolved Hide resolved
except AttributeError:
if file != "new":
abort(404)

if request.method == "POST":
to_save = request.form.to_dict()
title = to_save.get("title", "").strip()
name = to_save.get("name", "").strip()
icon = to_save.get("icon", "").strip()
position = to_save.get("position", "").strip()
order = int(to_save.get("order", 0))
content = to_save.get("content", "").strip()
is_enabled = _get_checkbox(to_save, "is_enabled", True)

if page:
page.title = title
page.name = name
page.icon = icon
page.is_enabled = is_enabled
page.order = order
page.position = position
ub.session_commit("Page edited {}".format(file))
else:
new_page = ub.Page(title=title, name=name, icon=icon, is_enabled=is_enabled, order=order, position=position)
ub.session.add(new_page)
ub.session_commit("Page added {}".format(file))

if (file == "new"):
file = str(new_page.id)
dir_config_path = os.path.join(_CONFIG_DIR, 'pages')
file_name = Path(name + '.md')
file_path = dir_config_path / file_name
os.makedirs(dir_config_path, exist_ok=True)

try:
with open(file_path, 'w') as f:
f.write(content)
f.close()
except Exception as ex:
log.error(ex)

if file != "new":
try:
dir_config_path = Path(_CONFIG_DIR) / 'pages'
file_path = dir_config_path / f"{name}.md"

with open(file_path, 'r') as f:
doc = f.read()
except NotFound:
log.error("'%s' was accessed but file doesn't exists." % file)

else:
doc = "## New file\n\nInformation"

return render_title_template("edit_page.html", title=title, name=name, icon=icon, is_enabled=is_enabled, order=order, position=position, content=doc, file=file)
28 changes: 28 additions & 0 deletions cps/listpages.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import flask
import json
from flask import Blueprint, jsonify, make_response,abort
from flask_login import current_user, login_required
from functools import wraps
from flask_babel import gettext as _

from .render_template import render_title_template
from . import ub, db

listpages = Blueprint('listpages', __name__)

def edit_required(f):
@wraps(f)
def inner(*args, **kwargs):
if current_user.role_edit() or current_user.role_admin():
return f(*args, **kwargs)
abort(403)

return inner

@listpages.route("/admin/pages/", methods=["GET"])
@login_required
@edit_required
def show_list():
pages = ub.session.query(ub.Page).order_by(ub.Page.position).order_by(ub.Page.order).all()

return render_title_template('list_pages.html', title=_("Pages List"), page="book_table", pages=pages)
6 changes: 6 additions & 0 deletions cps/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ def main():
from .gdrive import gdrive
from .editbooks import editbook
from .about import about
from .page import page
from .listpages import listpages
from .editpage import editpage
from .search import search
from .search_metadata import meta
from .shelf import shelf
Expand Down Expand Up @@ -65,6 +68,9 @@ def main():
limiter.limit("3/minute",key_func=request_username)(opds)
app.register_blueprint(jinjia)
app.register_blueprint(about)
app.register_blueprint(page)
app.register_blueprint(listpages)
app.register_blueprint(editpage)
app.register_blueprint(shelf)
app.register_blueprint(admi)
app.register_blueprint(remotelogin)
Expand Down
38 changes: 38 additions & 0 deletions cps/page.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import os
import flask
import markdown
from flask import abort
from pathlib import Path
from flask_babel import gettext as _
from werkzeug.exceptions import NotFound

from . import logger, config, ub
from .render_template import render_title_template
from .constants import CONFIG_DIR as _CONFIG_DIR

page = flask.Blueprint('page', __name__)

log = logger.create()

@page.route('/page/<string:file>', methods=['GET'])
def get_page(file):
page = ub.session.query(ub.Page)\
.filter(ub.Page.name == file)\
.filter(ub.Page.is_enabled)\
.first()

if not page:
log.error(f"'{file}' was accessed but is not enabled or it's not in database.")
abort(404)

try:
dir_config_path = Path(_CONFIG_DIR) / 'pages'
file_path = dir_config_path / f"{file}.md"
with open(file_path, 'r') as f:
temp_md = f.read()
body = markdown.markdown(temp_md)

return render_title_template('page.html', body=body, title=page.title, page=page.name)
except NotFound:
log.error("'%s' was accessed but file doesn't exists." % file)
abort(404)
14 changes: 12 additions & 2 deletions cps/render_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,14 +104,24 @@ def get_sidebar_config(kwargs=None):
g.shelves_access = ub.session.query(ub.Shelf).filter(
or_(ub.Shelf.is_public == 1, ub.Shelf.user_id == current_user.id)).order_by(ub.Shelf.name).all()

return sidebar, simple
top_pages = ub.session.query(ub.Page)\
.filter(ub.Page.position == "1")\
.filter(ub.Page.is_enabled)\
.order_by(ub.Page.order)
bottom_pages = ub.session.query(ub.Page)\
.filter(ub.Page.position == "0")\
.filter(ub.Page.is_enabled)\
.order_by(ub.Page.order)

return sidebar, simple, top_pages, bottom_pages


# Returns the template for rendering and includes the instance name
def render_title_template(*args, **kwargs):
sidebar, simple = get_sidebar_config(kwargs)
sidebar, simple, top_pages, bottom_pages = get_sidebar_config(kwargs)
try:
return render_template(instance=config.config_calibre_web_title, sidebar=sidebar, simple=simple,
top_pages=top_pages, bottom_pages=bottom_pages,
accept=constants.EXTENSIONS_UPLOAD,
*args, **kwargs)
except PermissionError:
Expand Down
1 change: 1 addition & 0 deletions cps/templates/admin.html
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ <h2>{{_('Configuration')}}</h2>
<a class="btn btn-default" id="db_config" href="{{url_for('admin.db_configuration')}}">{{_('Edit Calibre Database Configuration')}}</a>
<a class="btn btn-default" id="basic_config" href="{{url_for('admin.configuration')}}">{{_('Edit Basic Configuration')}}</a>
<a class="btn btn-default" id="view_config" href="{{url_for('admin.view_configuration')}}">{{_('Edit UI Configuration')}}</a>
<a class="btn btn-default" id="list_pages" href="{{url_for('listpages.show_list')}}">{{_('List Pages')}}</a>
</div>
</div>
{% if feature_support['scheduler'] %}
Expand Down
45 changes: 45 additions & 0 deletions cps/templates/edit_page.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{% extends "layout.html" %}
{% block body %}
<div class="discover">
<div><a class="session" href="{{url_for('listpages.show_list')}}">{{_('Back')}}</a></div>
<h2>{{_('Edit page')}}</h2>
<form role="form" class="col-md-10 col-lg-6" method="POST" action="{{ url_for('editpage.edit_page', file=file) }}"
autocomplete="off">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="form-group">
<label for="title">{{_('Title')}}</label>
<input type="text" class="form-control" name="title" id="title" value="{{ title }}" required>
</div>
<div class="form-group">
<label for="name">{{_('Name')}}</label>
<input type="text" class="form-control" name="name" id="name" value="{{ name }}" required>
</div>
<div class="form-group">
<label for="icon">{{_('Icon')}}</label>
<input type="text" class="form-control" name="icon" id="icon" value="{{ icon }}" required>
<a href="https://www.w3schools.com/bootstrap/bootstrap_ref_comp_glyphs.asp" target="_blank" rel="noopener">{{_('Icons list')}}</a>
</div>
<div class="form-group">
<label for="content">{{_('Content')}}</label>
<textarea class="form-control" name="content" id="content" rows="15">{{ content }}</textarea>
</div>
<div class="form-group">
<label for="position">{{_('Position')}}</label>
<select name="position" id="position" class="form-control">
<option value="0" {% if position=="0" %}selected{% endif %}>{{ _("Sidebar Bottom") }}</option>
<option value="1" {% if position=="1" %}selected{% endif %}>{{ _("Sidebar Top") }}</option>
</select>
</div>
<div class="form-group">
<input type="checkbox" id="is_enabled" name="is_enabled" {% if is_enabled %}checked{% endif %}>
<label for="is_enabled">{{_('Enabled')}}</label>
</div>
<div class="form-group">
<label for="order">{{_('Order')}}</label>
<input type="number" class="form-control" name="order" id="order" value="{{ order }}" required>
</div>
<button type="submit" name="submit" id="page_submit" class="btn btn-default">{{_('Save')}}</button>
<a href="{{ url_for('admin.admin') }}" id="config_back" class="btn btn-default">{{_('Cancel')}}</a>
</form>
</div>
{% endblock %}
6 changes: 6 additions & 0 deletions cps/templates/layout.html
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,9 @@ <h3>{{_('Uploading...')}}</h3>
<div class="col-sm-2">
<nav class="navigation">
<ul class="list-unstyled" id="scnd-nav" intent in-standard-append="nav.navigation" in-mobile-after="#main-nav" in-mobile-class="nav navbar-nav">
{% for element in top_pages %}
<li id="nav_{{element['name']}}" {% if page == element['name'] %}class="active"{% endif %}><a href="{{url_for('page.get_page', file=element['name'])}}"><span class="glyphicon glyphicon-{{element['icon']}}"></span> {{element['title']}}</a></li>
{% endfor %}
<li class="nav-head hidden-xs">{{_('Browse')}}</li>
{% for element in sidebar %}
{% if current_user.check_visibility(element['visibility']) and element['public'] %}
Expand All @@ -157,6 +160,9 @@ <h3>{{_('Uploading...')}}</h3>
<li id="nav_createshelf" class="create-shelf"><a href="{{url_for('shelf.create_shelf')}}">{{_('Create a Shelf')}}</a></li>
<li id="nav_about" {% if page == 'stat' %}class="active"{% endif %}><a href="{{url_for('about.stats')}}"><span class="glyphicon glyphicon-info-sign"></span> {{_('About')}}</a></li>
{% endif %}
{% for element in bottom_pages %}
<li id="nav_{{element['name']}}" {% if page == element['name'] %}class="active"{% endif %}><a href="{{url_for('page.get_page', file=element['name'])}}"><span class="glyphicon glyphicon-{{element['icon']}}"></span> {{element['title']}}</a></li>
{% endfor %}
{% endif %}

</ul>
Expand Down
52 changes: 52 additions & 0 deletions cps/templates/list_pages.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
{% extends "layout.html" %}
{% block header %}
<link href="{{ url_for('static', filename='css/libs/bootstrap-table.min.css') }}" rel="stylesheet">
<link href="{{ url_for('static', filename='css/libs/bootstrap-editable.css') }}" rel="stylesheet">
<link href="{{ url_for('static', filename='css/libs/bootstrap-select.min.css') }}" rel="stylesheet" >
{% endblock %}
{% block body %}
<h2 class="{{page}}">{{_(title)}}</h2>
<table class="table table-striped" id="table_user">
<thead>
<tr>
<th>{{_('Name')}}</th>
<th>{{_('Title')}}</th>
<th>{{_('Icon')}}</th>
<th>{{_('Position')}}</th>
<th>{{_('Enabled')}}</th>
<th>{{_('Order')}}</th>
</tr>
</thead>
<tbody>
{% for page in pages %}
<tr>
<td><a class="session" href="{{url_for('editpage.edit_page', file=page.id)}}">{{page.name}}</a></td>
<td>{{page.title}}</td>
<td>{{page.icon}}</td>
<td>{{_('bottom') if page.position == "0" else _('top')}}</td>
<td>
{% if page.is_enabled %}
<span class="glyphicon glyphicon-ok"></span>
{% else %}
<span class="glyphicon glyphicon-remove"></span>
{% endif %}
</td>
<td>{{page.order}}</td>
</tr>
{% endfor %}
</tbody>
</table>
<a class="session" id="new_page" href="{{url_for('editpage.edit_page', file="new")}}">{{_('New Page')}}</a>
{% endblock %}
{% block js %}
<script src="{{ url_for('static', filename='js/libs/bootstrap-table/bootstrap-table.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/libs/bootstrap-table/bootstrap-table-locale-all.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/libs/bootstrap-table/bootstrap-table-editable.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/libs/bootstrap-table/bootstrap-editable.min.js') }}"></script>
{% if not current_user.locale == 'en' %}
<script
src="{{ url_for('static', filename='js/libs/bootstrap-table/locale/bootstrap-table-' + current_user.locale + '.min.js') }}"
charset="UTF-8"></script>
{% endif %}
<script src="{{ url_for('static', filename='js/table.js') }}"></script>
{% endblock %}
4 changes: 4 additions & 0 deletions cps/templates/page.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{% extends "layout.html" %}
{% block body %}
<div>{{body|safe}}</div>
{% endblock %}
13 changes: 12 additions & 1 deletion cps/ub.py
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,16 @@ class Thumbnail(Base):
generated_at = Column(DateTime, default=lambda: datetime.datetime.utcnow())
expiration = Column(DateTime, nullable=True)

class Page(Base):
__tablename__ = 'page'

id = Column(Integer, primary_key=True)
title = Column(String)
name = Column(String)
icon = Column(String)
order = Column(Integer)
position = Column(String)
is_enabled = Column(Boolean, default=True)

# Add missing tables during migration of database
def add_missing_tables(engine, _session):
Expand All @@ -558,7 +568,8 @@ def add_missing_tables(engine, _session):
trans = conn.begin()
conn.execute("insert into registration (domain, allow) values('%.%',1)")
trans.commit()

if not engine.dialect.has_table(engine.connect(), "page"):
Page.__table__.create(bind=engine)

# migrate all settings missing in registration table
def migrate_registration_table(engine, _session):
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ flask-wtf>=0.14.2,<1.3.0
chardet>=3.0.0,<4.1.0
advocate>=1.0.0,<1.1.0
Flask-Limiter>=2.3.0,<3.6.0
markdown>=3.5.1