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 1 commit
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/
105 changes: 105 additions & 0 deletions cps/editpage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import os
import flask
from flask import Flask, abort, request
from functools import wraps
from pathlib import Path
from flask_login import current_user, login_required

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

log = logger.create()

editpage = flask.Blueprint('editpage', __name__)
silenceway marked this conversation as resolved.
Show resolved Hide resolved

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()
if page:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you could also do

Suggested change
page = ub.session.query(ub.Page).filter(ub.Page.id == file).first()
if page:
try:
page = ub.session.query(ub.Page).get_or_404(file)

And then check my comment after line 51
https://flask-sqlalchemy.palletsprojects.com/en/2.x/api/#flask_sqlalchemy.BaseQuery.get_or_404

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It shows an error with get_or_404, added 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

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 = to_save.get("order", 0)
order = int(order)
silenceway marked this conversation as resolved.
Show resolved Hide resolved
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
is_path = os.path.exists(dir_config_path)
if not is_path:
try:
os.makedirs(dir_config_path)
except Exception as ex:
log.error(ex)
silenceway marked this conversation as resolved.
Show resolved Hide resolved
try:
with open(file_path, 'w') as f:
f.write(content)
f.close()
except Exception as ex:
log.error(ex)

if file != "new":
dir_config_path = os.path.join(_CONFIG_DIR, 'pages')
file_name = Path(name + '.md')
file_path = dir_config_path / file_name
if file_path.is_file():
with open(file_path, 'r') as f:
doc = f.read()
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)
40 changes: 40 additions & 0 deletions cps/listpages.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import flask
import json
from flask import 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 = flask.Blueprint('listpages', __name__)
silenceway marked this conversation as resolved.
Show resolved Hide resolved

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)

@listpages.route("/ajax/listpages")
@login_required
@edit_required
def list_pages():
pages = ub.session.query(ub.Page).order_by(ub.Page.position).order_by(ub.Page.order).all()
table_entries = {'totalNotFiltered': len(pages), 'total': len(pages), "rows": pages}
js_list = json.dumps(table_entries, cls=db.AlchemyEncoder)
response = make_response(js_list)
silenceway marked this conversation as resolved.
Show resolved Hide resolved
response.headers["Content-Type"] = "application/json; charset=utf-8"

return response
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
39 changes: 39 additions & 0 deletions cps/page.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import os
import flask
import markdown
from flask import abort
from pathlib import Path
from flask_babel import gettext as _

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 page:
dir_config_path = os.path.join(_CONFIG_DIR, 'pages')
file_name = Path(file + '.md')
file_path = dir_config_path / file_name
silenceway marked this conversation as resolved.
Show resolved Hide resolved

if file_path.is_file():
silenceway marked this conversation as resolved.
Show resolved Hide resolved
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)
else:
log.error("'%s' was accessed but file doesn't exists." % file)
abort(404)
else:
log.error("'%s' was accessed but is not enabled or it's not in database." % file)
abort(404)
silenceway marked this conversation as resolved.
Show resolved Hide resolved
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="text" class="form-control" name="order" id="order" value="{{ order }}" required>
silenceway marked this conversation as resolved.
Show resolved Hide resolved
</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 %}
silenceway marked this conversation as resolved.
Show resolved Hide resolved
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
48 changes: 48 additions & 0 deletions cps/templates/list_pages.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
{% 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">
<tr>
<th>{{_('Name')}}</th>
<th>{{_('Title')}}</th>
<th>{{_('Icon')}}</th>
<th>{{_('Position')}}</th>
<th>{{_('Enabled')}}</th>
<th>{{_('Order')}}</th>
</tr>
{% for page in pages %}
<tr>
<td><a class="session" href="{{url_for('editpage.edit_page', file=page.id)}}">{{page.name}}</a></td>
silenceway marked this conversation as resolved.
Show resolved Hide resolved
<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 %}
</table>
<a class="session" 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 %}
silenceway marked this conversation as resolved.
Show resolved Hide resolved
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 %}
silenceway marked this conversation as resolved.
Show resolved Hide resolved
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