pull/2948/merge
apollo1220 3 months ago committed by GitHub
commit 4d66f8c31f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

2
.gitignore vendored

@ -35,3 +35,5 @@ gdrive_credentials
client_secrets.json client_secrets.json
gmail.json gmail.json
/.key /.key
pages/

@ -68,6 +68,7 @@ ROLE_ANONYMOUS = 1 << 5
ROLE_EDIT_SHELFS = 1 << 6 ROLE_EDIT_SHELFS = 1 << 6
ROLE_DELETE_BOOKS = 1 << 7 ROLE_DELETE_BOOKS = 1 << 7
ROLE_VIEWER = 1 << 8 ROLE_VIEWER = 1 << 8
ROLE_SEND_TO_EREADER = 1 << 9
ALL_ROLES = { ALL_ROLES = {
"admin_role": ROLE_ADMIN, "admin_role": ROLE_ADMIN,
@ -78,6 +79,7 @@ ALL_ROLES = {
"edit_shelf_role": ROLE_EDIT_SHELFS, "edit_shelf_role": ROLE_EDIT_SHELFS,
"delete_role": ROLE_DELETE_BOOKS, "delete_role": ROLE_DELETE_BOOKS,
"viewer_role": ROLE_VIEWER, "viewer_role": ROLE_VIEWER,
"send_to_ereader": ROLE_SEND_TO_EREADER,
} }
DETAIL_RANDOM = 1 << 0 DETAIL_RANDOM = 1 << 0

@ -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
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)

@ -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)

@ -36,6 +36,9 @@ def main():
from .gdrive import gdrive from .gdrive import gdrive
from .editbooks import editbook from .editbooks import editbook
from .about import about from .about import about
from .page import page
from .listpages import listpages
from .editpage import editpage
from .search import search from .search import search
from .search_metadata import meta from .search_metadata import meta
from .shelf import shelf from .shelf import shelf
@ -65,6 +68,9 @@ def main():
limiter.limit("3/minute",key_func=request_username)(opds) limiter.limit("3/minute",key_func=request_username)(opds)
app.register_blueprint(jinjia) app.register_blueprint(jinjia)
app.register_blueprint(about) app.register_blueprint(about)
app.register_blueprint(page)
app.register_blueprint(listpages)
app.register_blueprint(editpage)
app.register_blueprint(shelf) app.register_blueprint(shelf)
app.register_blueprint(admi) app.register_blueprint(admi)
app.register_blueprint(remotelogin) app.register_blueprint(remotelogin)

@ -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)

@ -104,14 +104,24 @@ def get_sidebar_config(kwargs=None):
g.shelves_access = ub.session.query(ub.Shelf).filter( 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() 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 # Returns the template for rendering and includes the instance name
def render_title_template(*args, **kwargs): def render_title_template(*args, **kwargs):
sidebar, simple = get_sidebar_config(kwargs) sidebar, simple, top_pages, bottom_pages = get_sidebar_config(kwargs)
try: try:
return render_template(instance=config.config_calibre_web_title, sidebar=sidebar, simple=simple, 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, accept=constants.EXTENSIONS_UPLOAD,
*args, **kwargs) *args, **kwargs)
except PermissionError: except PermissionError:

@ -20,6 +20,7 @@
<th class="hidden-xs hidden-sm">{{_('Upload')}}</th> <th class="hidden-xs hidden-sm">{{_('Upload')}}</th>
{% endif %} {% endif %}
<th class="hidden-xs hidden-sm">{{_('Download')}}</th> <th class="hidden-xs hidden-sm">{{_('Download')}}</th>
<th class="hidden-xs hidden-sm">{{_('Send to eReader')}}</th>
<th class="hidden-xs hidden-sm hidden-md">{{_('View Books')}}</th> <th class="hidden-xs hidden-sm hidden-md">{{_('View Books')}}</th>
<th class="hidden-xs hidden-sm hidden-md">{{_('Edit')}}</th> <th class="hidden-xs hidden-sm hidden-md">{{_('Edit')}}</th>
<th class="hidden-xs hidden-sm hidden-md">{{_('Delete')}}</th> <th class="hidden-xs hidden-sm hidden-md">{{_('Delete')}}</th>
@ -38,6 +39,7 @@
<td class="hidden-xs hidden-sm">{{ display_bool_setting(user.role_upload()) }}</td> <td class="hidden-xs hidden-sm">{{ display_bool_setting(user.role_upload()) }}</td>
{% endif %} {% endif %}
<td class="hidden-xs hidden-sm">{{ display_bool_setting(user.role_download()) }}</td> <td class="hidden-xs hidden-sm">{{ display_bool_setting(user.role_download()) }}</td>
<td class="hidden-xs hidden-sm">{{ display_bool_setting(user.role_send_to_ereader()) }}</td>
<td class="hidden-xs hidden-sm hidden-md">{{ display_bool_setting(user.role_viewer()) }}</td> <td class="hidden-xs hidden-sm hidden-md">{{ display_bool_setting(user.role_viewer()) }}</td>
<td class="hidden-xs hidden-sm hidden-md">{{ display_bool_setting(user.role_edit()) }}</td> <td class="hidden-xs hidden-sm hidden-md">{{ display_bool_setting(user.role_edit()) }}</td>
<td class="hidden-xs hidden-sm hidden-md">{{ display_bool_setting(user.role_delete_books()) }}</td> <td class="hidden-xs hidden-sm hidden-md">{{ display_bool_setting(user.role_delete_books()) }}</td>
@ -159,6 +161,7 @@
<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="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="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="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>
</div> </div>
{% if feature_support['scheduler'] %} {% if feature_support['scheduler'] %}

@ -43,6 +43,8 @@
{% endif %} {% endif %}
</div> </div>
{% endif %} {% endif %}
{% endif %}
{% if current_user.role_send_to_ereader() %}
{% if current_user.kindle_mail and entry.email_share_list %} {% if current_user.kindle_mail and entry.email_share_list %}
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"> <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
{% if entry.email_share_list.__len__() == 1 %} {% if entry.email_share_list.__len__() == 1 %}

@ -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 %}

@ -142,6 +142,9 @@
<div class="col-sm-2"> <div class="col-sm-2">
<nav class="navigation"> <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"> <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> <li class="nav-head hidden-xs">{{_('Browse')}}</li>
{% for element in sidebar %} {% for element in sidebar %}
{% if current_user.check_visibility(element['visibility']) and element['public'] %} {% if current_user.check_visibility(element['visibility']) and element['public'] %}
@ -157,6 +160,9 @@
<li id="nav_createshelf" class="create-shelf"><a href="{{url_for('shelf.create_shelf')}}">{{_('Create a Shelf')}}</a></li> <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> <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 %} {% 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 %} {% endif %}
</ul> </ul>

@ -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 %}

@ -0,0 +1,4 @@
{% extends "layout.html" %}
{% block body %}
<div>{{body|safe}}</div>
{% endblock %}

@ -160,6 +160,9 @@ class UserBase:
def role_viewer(self): def role_viewer(self):
return self._has_role(constants.ROLE_VIEWER) return self._has_role(constants.ROLE_VIEWER)
def role_send_to_ereader(self):
return self._has_role(constants.ROLE_SEND_TO_EREADER)
@property @property
def is_active(self): def is_active(self):
return True return True
@ -535,6 +538,16 @@ class Thumbnail(Base):
generated_at = Column(DateTime, default=lambda: datetime.datetime.utcnow()) generated_at = Column(DateTime, default=lambda: datetime.datetime.utcnow())
expiration = Column(DateTime, nullable=True) 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 # Add missing tables during migration of database
def add_missing_tables(engine, _session): def add_missing_tables(engine, _session):
@ -558,7 +571,8 @@ def add_missing_tables(engine, _session):
trans = conn.begin() trans = conn.begin()
conn.execute("insert into registration (domain, allow) values('%.%',1)") conn.execute("insert into registration (domain, allow) values('%.%',1)")
trans.commit() trans.commit()
if not engine.dialect.has_table(engine.connect(), "page"):
Page.__table__.create(bind=engine)
# migrate all settings missing in registration table # migrate all settings missing in registration table
def migrate_registration_table(engine, _session): def migrate_registration_table(engine, _session):

@ -18,3 +18,4 @@ flask-wtf>=0.14.2,<1.3.0
chardet>=3.0.0,<4.1.0 chardet>=3.0.0,<4.1.0
advocate>=1.0.0,<1.1.0 advocate>=1.0.0,<1.1.0
Flask-Limiter>=2.3.0,<3.6.0 Flask-Limiter>=2.3.0,<3.6.0
markdown>=3.5.1

Loading…
Cancel
Save