Projects Update
* Conversion script to new configuration layout. This is to prevent from secrets being loaded into memory aside from when they are needed. * Updated code throughout the project to account for the new methods. * New classes and code for new configuration layout * New Javascript for backend to control all the dialogues * Moved modals to separate files * Smaller database model update * Removed redundant code * Added database upgrade path via the dashboard * Admin password is now properly hashed * You can now export all locations manually in event of an error
This commit is contained in:
parent
194ffc754b
commit
ad8f68cdd8
7
.gitignore
vendored
7
.gitignore
vendored
@ -4,8 +4,11 @@ venv
|
||||
db/*.db
|
||||
db/*.db-shm
|
||||
db/*.db-wal
|
||||
output
|
||||
/output/
|
||||
*.old
|
||||
*.server
|
||||
/backup/
|
||||
labertasche.yaml
|
||||
/.secret
|
||||
/credentials.yaml
|
||||
*.bak
|
||||
smileys.yaml
|
||||
|
@ -15,6 +15,7 @@
|
||||
// post-internal-server-error
|
||||
// post-success
|
||||
// post-before-fetch
|
||||
// post-project-not-found
|
||||
function labertasche_callback(state)
|
||||
{
|
||||
if (state === "post-before-fetch"){
|
||||
|
@ -15,8 +15,8 @@ from sqlalchemy import exc
|
||||
from labertasche.database import labertasche_db as db
|
||||
from labertasche.helper import is_valid_json, default_timestamp, check_gravatar, export_location
|
||||
from labertasche.mail import mail
|
||||
from labertasche.models import TComments, TLocation, TEmail
|
||||
from labertasche.settings import Settings
|
||||
from labertasche.models import TComments, TLocation, TEmail, TProjects
|
||||
from labertasche.settings import Smileys
|
||||
from secrets import compare_digest
|
||||
|
||||
|
||||
@ -25,19 +25,17 @@ bp_comments = Blueprint("bp_comments", __name__, url_prefix='/comments')
|
||||
|
||||
|
||||
# Route for adding new comments
|
||||
@bp_comments.route("/new", methods=['POST'])
|
||||
@bp_comments.route("/<name>/new", methods=['POST'])
|
||||
@cross_origin()
|
||||
def check_and_insert_new_comment():
|
||||
def check_and_insert_new_comment(name):
|
||||
if request.method == 'POST':
|
||||
settings = Settings()
|
||||
smileys = settings.smileys
|
||||
addons = settings.addons
|
||||
smileys = Smileys()
|
||||
sender = mail()
|
||||
|
||||
# Check length of content and abort if too long or too short
|
||||
if request.content_length > 2048:
|
||||
return make_response(jsonify(status="post-max-length"), 400)
|
||||
if request.content_length <= 0:
|
||||
if request.content_length == 0:
|
||||
return make_response(jsonify(status="post-min-length"), 400)
|
||||
|
||||
# get json from request
|
||||
@ -59,9 +57,14 @@ def check_and_insert_new_comment():
|
||||
content = re.sub(tags, '', new_comment['content']).strip()
|
||||
content = re.sub(special, '', content).strip()
|
||||
|
||||
# Convert smileys
|
||||
if addons['smileys']:
|
||||
for key, value in smileys.items():
|
||||
# Get project
|
||||
project = db.session.query(TProjects).filter(TProjects.name == name).first()
|
||||
if not project:
|
||||
return make_response(jsonify(status="post-project-not-found"), 400)
|
||||
|
||||
# Convert smileys if enabled
|
||||
if project.addon_smileys:
|
||||
for key, value in smileys.smileys.items():
|
||||
content = content.replace(key, value)
|
||||
|
||||
# Validate replied_to field is integer
|
||||
@ -119,7 +122,8 @@ def check_and_insert_new_comment():
|
||||
else:
|
||||
# Insert new location
|
||||
loc_table = {
|
||||
'location': new_comment['location']
|
||||
'location': new_comment['location'],
|
||||
'project_id': project.id_project
|
||||
}
|
||||
new_loc = TLocation(**loc_table)
|
||||
db.session.add(new_loc)
|
||||
@ -133,11 +137,15 @@ def check_and_insert_new_comment():
|
||||
# insert comment
|
||||
# noinspection PyBroadException
|
||||
try:
|
||||
new_comment.update({"is_published": False})
|
||||
if project.sendotp:
|
||||
new_comment.update({"is_published": False})
|
||||
else:
|
||||
new_comment.update({"is_published": True})
|
||||
new_comment.update({"created_on": default_timestamp()})
|
||||
new_comment.update({"is_spam": is_spam})
|
||||
new_comment.update({"spam_score": has_score})
|
||||
new_comment.update({"gravatar": check_gravatar(new_comment['email'])})
|
||||
new_comment.update({"gravatar": check_gravatar(new_comment['email'], project.name)})
|
||||
new_comment.update({"project_id": project.id_project})
|
||||
t_comment = TComments(**new_comment)
|
||||
db.session.add(t_comment)
|
||||
db.session.commit()
|
||||
@ -145,10 +153,11 @@ def check_and_insert_new_comment():
|
||||
db.session.refresh(t_comment)
|
||||
|
||||
# Send confirmation link and store returned value
|
||||
hashes = sender.send_confirmation_link(new_comment['email'])
|
||||
setattr(t_comment, "confirmation", hashes[0])
|
||||
setattr(t_comment, "deletion", hashes[1])
|
||||
db.session.commit()
|
||||
if project.sendotp:
|
||||
hashes = sender.send_confirmation_link(new_comment['email'], project.name)
|
||||
setattr(t_comment, "confirmation", hashes[0])
|
||||
setattr(t_comment, "deletion", hashes[1])
|
||||
db.session.commit()
|
||||
|
||||
except exc.IntegrityError as e:
|
||||
# Comment body exists, because content is unique
|
||||
@ -163,11 +172,12 @@ def check_and_insert_new_comment():
|
||||
|
||||
|
||||
# Route for confirming comments
|
||||
@bp_comments.route("/confirm/<email_hash>", methods=['GET'])
|
||||
@bp_comments.route("/confirm/<name>/<email_hash>", methods=['GET'])
|
||||
@cross_origin()
|
||||
def check_confirmation_link(email_hash):
|
||||
settings = Settings()
|
||||
def check_confirmation_link(name, email_hash):
|
||||
comment = db.session.query(TComments).filter(TComments.confirmation == email_hash).first()
|
||||
project = db.session.query(TProjects).filter(TProjects.name == name).first()
|
||||
|
||||
if comment:
|
||||
location = db.session.query(TLocation).filter(TLocation.id_location == comment.location_id).first()
|
||||
if compare_digest(comment.confirmation, email_hash):
|
||||
@ -175,27 +185,27 @@ def check_confirmation_link(email_hash):
|
||||
if not comment.is_spam:
|
||||
setattr(comment, "is_published", True)
|
||||
db.session.commit()
|
||||
url = f"{settings.system['blog_url']}{location.location}#comment_{comment.comments_id}"
|
||||
url = f"{project.blogurl}{location.location}#comment_{comment.comments_id}"
|
||||
export_location(location.id_location)
|
||||
return redirect(url)
|
||||
|
||||
return redirect(f"{settings.system['blog_url']}?cnf=true")
|
||||
return redirect(f"{project.blogurl}?cnf=true")
|
||||
|
||||
|
||||
# Route for deleting comments
|
||||
@bp_comments.route("/delete/<email_hash>", methods=['GET'])
|
||||
@bp_comments.route("/delete/<name>/<email_hash>", methods=['GET'])
|
||||
@cross_origin()
|
||||
def check_deletion_link(email_hash):
|
||||
settings = Settings()
|
||||
query = db.session.query(TComments).filter(TComments.deletion == email_hash)
|
||||
comment = query.first()
|
||||
def check_deletion_link(name, email_hash):
|
||||
project = db.session.query(TProjects).filter(TProjects.name == name).first()
|
||||
comment = db.session.query(TComments).filter(TComments.deletion == email_hash).first()
|
||||
|
||||
if comment:
|
||||
location = db.session.query(TLocation).filter(TLocation.id_location == comment.location_id).first()
|
||||
if compare_digest(comment.deletion, email_hash):
|
||||
query.delete()
|
||||
comment.delete()
|
||||
db.session.commit()
|
||||
url = f"{settings.system['blog_url']}?deleted=true"
|
||||
url = f"{project.blogurl}?deleted=true"
|
||||
export_location(location.id_location)
|
||||
return redirect(url)
|
||||
|
||||
return redirect(f"{settings.system['blog_url']}?cnf=true")
|
||||
return redirect(f"{project.blogurl}?cnf=true")
|
||||
|
@ -7,12 +7,12 @@
|
||||
# * _license : This project is under MIT License
|
||||
# *********************************************************************************/
|
||||
from . import bp_jsconnector
|
||||
from flask import request, redirect
|
||||
from flask import request, redirect, make_response, jsonify
|
||||
from flask_login import login_required
|
||||
from flask_cors import cross_origin
|
||||
from labertasche.database import labertasche_db as db
|
||||
from labertasche.helper import export_location
|
||||
from labertasche.models import TComments, TEmail
|
||||
from labertasche.helper import export_location, get_id_from_project_name
|
||||
from labertasche.models import TComments, TEmail, TLocation
|
||||
|
||||
# This file contains the routes for the manage comments menu point.
|
||||
# They are called via GET
|
||||
@ -112,3 +112,19 @@ def api_comment_block_mail(comment_id):
|
||||
return redirect(request.referrer)
|
||||
|
||||
|
||||
@cross_origin()
|
||||
@bp_jsconnector.route('/comment-export-all/<name>', methods=["GET"])
|
||||
@login_required
|
||||
def api_export_all_by_project(name):
|
||||
proj_id = get_id_from_project_name(name)
|
||||
if proj_id == -1:
|
||||
return make_response(jsonify(status='not-found'), 400)
|
||||
|
||||
try:
|
||||
locations = db.session.query(TLocation).all()
|
||||
for each in locations:
|
||||
export_location(each.id_location)
|
||||
except Exception as e:
|
||||
return make_response(jsonify(status='sql-error', msg=str(e)), 400)
|
||||
|
||||
return make_response(jsonify(status='ok'), 200)
|
||||
|
@ -13,9 +13,57 @@ from flask_cors import cross_origin
|
||||
from labertasche.database import labertasche_db as db
|
||||
from labertasche.helper import get_id_from_project_name
|
||||
from labertasche.models import TProjects, TComments, TEmail, TLocation
|
||||
from validators import url as validate_url
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
|
||||
def validate_project(project):
|
||||
"""
|
||||
Validates important bits of a project database entry
|
||||
|
||||
:param project: The json from the request, containing the data for a project.
|
||||
:return: A response with the error or None if the project is valid.
|
||||
"""
|
||||
|
||||
# Validate length
|
||||
if not len(project['name']) and \
|
||||
not len(project['blogurl']) and \
|
||||
not len(project['output']):
|
||||
return make_response(jsonify(status='too-short'), 400)
|
||||
|
||||
# Validate project name
|
||||
if not re.match('^\\w+$', project['name']):
|
||||
print(project['name'])
|
||||
return make_response(jsonify(status='invalid-project-name'), 400)
|
||||
|
||||
# Check if project name already exists
|
||||
name_check = db.session.query(TProjects.name).filter(TProjects.name == project['name']).first()
|
||||
if name_check:
|
||||
return make_response(jsonify(status='project-exists'), 400)
|
||||
|
||||
# Validate url
|
||||
url_exists = db.session.query(TProjects.blogurl).filter(TProjects.blogurl == project['blogurl']).first()
|
||||
if not validate_url(project['blogurl']) or url_exists:
|
||||
return make_response(jsonify(status='invalid-blog-url'), 400)
|
||||
|
||||
# Validate output path
|
||||
output = Path(project['output']).absolute()
|
||||
# The second check is needed, since javascript is passing an empty string instead of
|
||||
# null. For some reason, this makes SQLAlchemy accept the data and commit it to the db
|
||||
# without exception. This check prevents this issue from happening.
|
||||
if not output.exists() or len(project['output'].strip()) == 0:
|
||||
return make_response(jsonify(status='invalid-path-output'), 400)
|
||||
|
||||
# Validate cache path, if caching is enabled
|
||||
if project['gravatar_cache']:
|
||||
cache = Path(project['gravatar_cache_dir']).absolute()
|
||||
if not cache.exists() or len(project['gravatar_cache_dir'].strip()) == 0:
|
||||
return make_response(jsonify(status='invalid-path-cache'), 400)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
@cross_origin()
|
||||
@bp_jsconnector.route("/project/new", methods=['POST'])
|
||||
@login_required
|
||||
@ -25,58 +73,64 @@ def api_create_project():
|
||||
|
||||
:return: A string with an error code and 'ok' as string on success.
|
||||
"""
|
||||
# TODO: Project name exists?
|
||||
name = request.json['name']
|
||||
response = validate_project(request.json)
|
||||
if response is not None:
|
||||
return response
|
||||
|
||||
if not len(name):
|
||||
return make_response(jsonify(status='too-short'), 400)
|
||||
if not re.match('^\\w+$', name):
|
||||
return make_response(jsonify(status='invalid-name'), 400)
|
||||
|
||||
proj = TProjects(name=name)
|
||||
db.session.add(proj)
|
||||
db.session.commit()
|
||||
try:
|
||||
db.session.add(TProjects(**request.json))
|
||||
db.session.commit()
|
||||
except Exception as e:
|
||||
print(str(e))
|
||||
db.session.rollback()
|
||||
return make_response(jsonify(status='exception', msg=str(e)), 500)
|
||||
|
||||
return make_response(jsonify(status='ok'), 200)
|
||||
|
||||
|
||||
@cross_origin()
|
||||
@bp_jsconnector.route('project/edit/<name>', methods=['POST'])
|
||||
@bp_jsconnector.route('/project/edit/<name>', methods=['POST'])
|
||||
@login_required
|
||||
def api_edit_project_name(name: str):
|
||||
"""
|
||||
Renames the project.
|
||||
:param name:
|
||||
:param name: The previous name of the project to edit, must exist
|
||||
:return: A string with an error code and 'ok' as string on success.
|
||||
"""
|
||||
# TODO: Project name exists?
|
||||
new_name = request.json['name']
|
||||
response = validate_project(request.json)
|
||||
if response is not None:
|
||||
return response
|
||||
|
||||
if not len(new_name):
|
||||
return make_response(jsonify(status='too-short'), 400)
|
||||
if not re.match('^\\w+$', new_name):
|
||||
return make_response(jsonify(status='invalid-name'), 400)
|
||||
|
||||
proj_id = get_id_from_project_name(name)
|
||||
project = db.session.query(TProjects).filter(TProjects.id_project == proj_id)
|
||||
setattr(project, 'name', new_name)
|
||||
db.session.upate(project)
|
||||
db.session.commit()
|
||||
try:
|
||||
project = db.session.query(TProjects).filter(TProjects.name == name).first()
|
||||
setattr(project, "id_project", project.id_project)
|
||||
setattr(project, "name", request.json['name'])
|
||||
setattr(project, "blogurl", request.json['blogurl'].strip())
|
||||
setattr(project, "output", request.json['output'].strip())
|
||||
setattr(project, "sendotp", request.json['sendotp'])
|
||||
setattr(project, "gravatar_cache", request.json['gravatar_cache'])
|
||||
setattr(project, "gravatar_cache_dir", request.json['gravatar_cache_dir'])
|
||||
setattr(project, "gravatar_size", request.json['gravatar_size'])
|
||||
setattr(project, "addon_smileys", request.json['addon_smileys'])
|
||||
db.session.commit()
|
||||
except Exception as e:
|
||||
print(str(e))
|
||||
return make_response(jsonify(status='exception', msg=str(e)), 500)
|
||||
|
||||
return make_response(jsonify(status='ok'), 200)
|
||||
|
||||
|
||||
@cross_origin()
|
||||
@bp_jsconnector.route('project/delete/<project>', methods=['GET'])
|
||||
@bp_jsconnector.route('/project/delete/<name>', methods=['GET'])
|
||||
@login_required
|
||||
def api_delete_project(project: str):
|
||||
def api_delete_project(name: str):
|
||||
"""
|
||||
Deletes a project from the database and all associated data
|
||||
|
||||
:param project: The name of the project
|
||||
:param name: The name of the project
|
||||
:return: A string with an error code and 'ok' as string on success.
|
||||
"""
|
||||
proj_id = get_id_from_project_name(project)
|
||||
proj_id = get_id_from_project_name(name)
|
||||
if proj_id == -1:
|
||||
return make_response(jsonify(status='not-found'), 400)
|
||||
|
||||
@ -92,3 +146,34 @@ def api_delete_project(project: str):
|
||||
return make_response(jsonify(status='exception'), 400)
|
||||
|
||||
return make_response(jsonify(status='ok'), 200)
|
||||
|
||||
|
||||
@cross_origin()
|
||||
@bp_jsconnector.route('/project/exists/<name>', methods=['GET'])
|
||||
@login_required
|
||||
def api_project_exists(name: str):
|
||||
proj_id = get_id_from_project_name(name)
|
||||
if proj_id == -1:
|
||||
return make_response(jsonify(status='not-found'), 200)
|
||||
return make_response(jsonify(status='ok'), 200)
|
||||
|
||||
|
||||
@cross_origin()
|
||||
@bp_jsconnector.route('/project/get/<name>', methods=['GET'])
|
||||
@login_required
|
||||
def api_project_get_data(name: str):
|
||||
project = db.session.query(TProjects).filter(TProjects.name == name).first()
|
||||
if project:
|
||||
return make_response(jsonify(status='ok',
|
||||
id_project=project.id_project,
|
||||
name=project.name,
|
||||
blogurl=project.blogurl,
|
||||
output=project.output,
|
||||
sendotp=project.sendotp,
|
||||
gravatar_cache=project.gravatar_cache,
|
||||
gravatar_cache_dir=project.gravatar_cache_dir,
|
||||
gravatar_size=project.gravatar_size,
|
||||
addon_smileys=project.addon_smileys), 200)
|
||||
else:
|
||||
print('400')
|
||||
return make_response(jsonify(status='not-found'), 400)
|
||||
|
@ -8,7 +8,9 @@
|
||||
# *********************************************************************************/
|
||||
from flask import Blueprint, render_template, request, redirect, url_for
|
||||
from flask_cors import cross_origin
|
||||
from labertasche.helper import check_auth, User
|
||||
from labertasche.helper import User
|
||||
from labertasche.settings import Credentials
|
||||
from secrets import compare_digest
|
||||
from flask_login import login_user, current_user, logout_user
|
||||
|
||||
# Blueprint
|
||||
@ -30,7 +32,9 @@ def login():
|
||||
username = request.form['username']
|
||||
password = request.form['password']
|
||||
|
||||
if check_auth(username, password):
|
||||
credentials = Credentials()
|
||||
if compare_digest(username.encode('utf8'), credentials.username.encode('utf8')) and \
|
||||
credentials.compare_password(password):
|
||||
login_user(User(0), remember=True)
|
||||
return redirect(url_for('bp_dashboard.dashboard_project_list'))
|
||||
|
||||
|
@ -13,7 +13,7 @@ from flask import render_template, jsonify, make_response
|
||||
from pathlib import Path
|
||||
from labertasche.database import labertasche_db as db
|
||||
from labertasche.models import TProjects, TComments, TLocation, TEmail, TVersion
|
||||
from labertasche.helper import Settings
|
||||
from labertasche.settings import LegacySettings
|
||||
from json import dump, load
|
||||
from shutil import copy, make_archive
|
||||
from re import search
|
||||
@ -128,7 +128,7 @@ def upgrade_db_to_v2_export():
|
||||
|
||||
# Copy database
|
||||
try:
|
||||
settings = Settings()
|
||||
settings = LegacySettings(True)
|
||||
db_uri = settings.system['database_uri']
|
||||
if compare_digest(db_uri[0:6], "sqlite"):
|
||||
m = search("([/]{3})(.*)", db_uri)
|
||||
@ -163,7 +163,7 @@ def upgrade_db_to_v2_recreate():
|
||||
@login_required
|
||||
def upgrade_db_to_v2_import():
|
||||
path = get_backup_folder()
|
||||
settings = Settings()
|
||||
settings = LegacySettings(True)
|
||||
|
||||
try:
|
||||
# load location
|
||||
|
@ -8,21 +8,19 @@
|
||||
# *********************************************************************************/
|
||||
import datetime
|
||||
import json
|
||||
from labertasche.models import TLocation, TComments, TProjects
|
||||
from labertasche.settings import Settings
|
||||
from labertasche.database import labertasche_db as db
|
||||
from functools import wraps
|
||||
from hashlib import md5
|
||||
from flask import request
|
||||
from flask_login import UserMixin
|
||||
from secrets import compare_digest
|
||||
from pathlib import Path
|
||||
from sys import stderr
|
||||
from re import match as re_match
|
||||
import requests
|
||||
from labertasche.models import TLocation, TComments, TProjects
|
||||
from labertasche.database import labertasche_db as db
|
||||
|
||||
|
||||
class User(UserMixin):
|
||||
"""
|
||||
Class for flask-login, which represents a user
|
||||
"""
|
||||
def __init__(self, user_id):
|
||||
self.id = user_id
|
||||
|
||||
@ -37,7 +35,7 @@ def is_valid_json(j):
|
||||
try:
|
||||
json.dumps(j)
|
||||
return True
|
||||
except json.JSONDecodeError as e:
|
||||
except json.JSONDecodeError:
|
||||
print("not valid json")
|
||||
return False
|
||||
|
||||
@ -77,60 +75,30 @@ def alchemy_query_to_dict(obj):
|
||||
|
||||
# Come on, it's a mail hash, don't complain
|
||||
# noinspection InsecureHash
|
||||
def check_gravatar(email: str):
|
||||
def check_gravatar(email: str, name: str):
|
||||
"""
|
||||
Builds the gravatar email hash, which uses md5.
|
||||
You may use ?size=128 for example to dictate size in the final template.
|
||||
:param email: the email to use for the hash
|
||||
:param name: The project name
|
||||
:return: the gravatar url of the image
|
||||
"""
|
||||
settings = Settings()
|
||||
options = settings.gravatar
|
||||
from requests import get
|
||||
project = db.session.query(TProjects).filter(TProjects.name == name).first()
|
||||
gravatar_hash = md5(email.strip().lower().encode("utf8")).hexdigest()
|
||||
if options['cache']:
|
||||
url = f"https://www.gravatar.com/avatar/{gravatar_hash}?s={options['size']}"
|
||||
response = requests.get(url)
|
||||
|
||||
if project.gravatar_cache:
|
||||
url = f"https://www.gravatar.com/avatar/{gravatar_hash}?s={project.gravatar_size}"
|
||||
response = get(url)
|
||||
if response.ok:
|
||||
outfile = Path(f"{options['static_dir']}/{gravatar_hash}.jpg")
|
||||
outfile = Path(f"{project.gravatar_cache_dir}/{gravatar_hash}.jpg")
|
||||
with outfile.open('wb') as fp:
|
||||
response.raw.decode_content = True
|
||||
for chunk in response:
|
||||
fp.write(chunk)
|
||||
|
||||
return gravatar_hash
|
||||
|
||||
|
||||
def check_auth(username: str, password: str):
|
||||
"""
|
||||
Compares username and password from the settings file in a safe way.
|
||||
Direct string comparison is vulnerable to timing attacks
|
||||
https://sqreen.github.io/DevelopersSecurityBestPractices/timing-attack/python
|
||||
:param username: username entered by the user
|
||||
:param password: password entered by the user
|
||||
:return: True if equal, False if not
|
||||
"""
|
||||
settings = Settings()
|
||||
if compare_digest(username, settings.dashboard['username']) and \
|
||||
compare_digest(password, settings.dashboard['password']):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def basic_login_required(f):
|
||||
"""
|
||||
Decorator for basic auth
|
||||
"""
|
||||
@wraps(f)
|
||||
def wrapped_view(**kwargs):
|
||||
auth = request.authorization
|
||||
if not (auth and check_auth(auth.username, auth.password)):
|
||||
return ('Unauthorized', 401, {
|
||||
'WWW-Authenticate': 'Basic realm="Login Required"'
|
||||
})
|
||||
return f(**kwargs)
|
||||
return wrapped_view
|
||||
|
||||
|
||||
def export_location(location_id: int) -> bool:
|
||||
"""
|
||||
Exports the comments for the location after the comment was accepted
|
||||
@ -141,12 +109,28 @@ def export_location(location_id: int) -> bool:
|
||||
db.session.flush()
|
||||
|
||||
# Query
|
||||
loc_query = db.session.query(TLocation).filter(TLocation.id_location == location_id).first()
|
||||
location = db.session.query(TLocation).filter(TLocation.id_location == location_id).first()
|
||||
|
||||
if loc_query:
|
||||
if location:
|
||||
comments = db.session.query(TComments).filter(TComments.is_spam != True) \
|
||||
.filter(TComments.is_published == True) \
|
||||
.filter(TComments.location_id == loc_query.id_location)
|
||||
.filter(TComments.location_id == location.id_location) \
|
||||
.filter(TProjects.id_project == location.project_id) \
|
||||
.all()
|
||||
project = db.session.query(TProjects).filter(TProjects.id_project == location.project_id).first()
|
||||
|
||||
# Removes the last slash
|
||||
path_loc = re_match(".*(?=/)", location.location)[0]
|
||||
|
||||
# Construct export path
|
||||
jsonfile = Path(f"{project.output}/{path_loc}.json").absolute()
|
||||
folder = jsonfile.parents[0]
|
||||
|
||||
# If there are no comments, do not export and remove empty file.
|
||||
# The database is the point of trust.
|
||||
if len(comments) == 0:
|
||||
jsonfile.unlink(missing_ok=True)
|
||||
return True
|
||||
|
||||
bundle = {
|
||||
"comments": [],
|
||||
@ -158,20 +142,14 @@ def export_location(location_id: int) -> bool:
|
||||
continue
|
||||
bundle['comments'].append(alchemy_query_to_dict(comment))
|
||||
|
||||
path_loc = re_match(".*(?=/)", loc_query.location)[0]
|
||||
|
||||
system = Settings().system
|
||||
out = Path(f"{system['output']}/{path_loc}.json")
|
||||
out = out.absolute()
|
||||
folder = out.parents[0]
|
||||
# Create folder if not exists and write file
|
||||
folder.mkdir(parents=True, exist_ok=True)
|
||||
with out.open('w') as fp:
|
||||
with jsonfile.open('w') as fp:
|
||||
json.dump(bundle, fp)
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
# mail(f"export_comments has thrown an error: {str(e)}")
|
||||
print(e, file=stderr)
|
||||
return False
|
||||
|
||||
|
@ -13,10 +13,11 @@ from pathlib import Path
|
||||
from platform import system
|
||||
from smtplib import SMTP_SSL, SMTPHeloError, SMTPAuthenticationError, SMTPException
|
||||
from ssl import create_default_context
|
||||
from labertasche.settings import Settings
|
||||
from validate_email import validate_email_or_fail
|
||||
from secrets import token_urlsafe
|
||||
|
||||
from labertasche.models import TProjects
|
||||
from labertasche.database import labertasche_db as db
|
||||
from labertasche.settings import Settings
|
||||
|
||||
class mail:
|
||||
|
||||
@ -61,24 +62,30 @@ class mail:
|
||||
except SMTPException as e:
|
||||
print(f"SMTPException: {e}")
|
||||
|
||||
def send_confirmation_link(self, email):
|
||||
def send_confirmation_link(self, email: str, name: str) -> tuple:
|
||||
"""
|
||||
Send confirmation link after entering a comment
|
||||
:param email: The address to send the mail to
|
||||
:param name: The name of the project
|
||||
:return: A tuple with the confirmation token and the deletion token, in this order
|
||||
"""
|
||||
project = db.session.query(TProjects).filter(TProjects.name == name).first()
|
||||
if not project:
|
||||
return None, None
|
||||
|
||||
settings = Settings()
|
||||
|
||||
confirm_digest = token_urlsafe(48)
|
||||
delete_digest = token_urlsafe(48)
|
||||
|
||||
confirm_url = f"{settings.system['web_url']}/comments/confirm/{confirm_digest}"
|
||||
delete_url = f"{settings.system['web_url']}/comments/delete/{delete_digest}"
|
||||
confirm_url = f"{settings.weburl}/comments/confirm/{confirm_digest}"
|
||||
delete_url = f"{settings.weburl}/comments/delete/{delete_digest}"
|
||||
|
||||
txt_what = f"Hey there. You have made a comment on {settings.system['blog_url']}. Please confirm it by " \
|
||||
txt_what = f"Hey there. You have made a comment on {project.blogurl}. Please confirm it by " \
|
||||
f"copying this link into your browser:\n{confirm_url}\nIf you want to delete your comment for,"\
|
||||
f"whatever reason, please use this link:\n{delete_url}"
|
||||
|
||||
html_what = f"Hey there. You have made a comment on {settings.system['blog_url']}.<br>Please confirm it by " \
|
||||
html_what = f"Hey there. You have made a comment on {project.blogurl}.<br>Please confirm it by " \
|
||||
f"clicking on this <a href='{confirm_url}'>link</a>.<br>"\
|
||||
f"In case you want to delete your comment later, please click <a href='{delete_url}'>here</a>."\
|
||||
f"<br><br>If you think this is in error or someone made this comment in your name, please "\
|
||||
|
@ -19,8 +19,8 @@ class TProjects(db.Model):
|
||||
|
||||
# data
|
||||
name = db.Column(db.Text, nullable=False, unique=True)
|
||||
blogurl = db.Column(db.Text, nullable=False)
|
||||
output = db.Column(db.Text, nullable=False)
|
||||
blogurl = db.Column(db.Text, nullable=False, unique=False)
|
||||
output = db.Column(db.Text, nullable=False, unique=True)
|
||||
sendotp = db.Column(db.Boolean, nullable=False)
|
||||
|
||||
gravatar_cache = db.Column(db.Boolean, nullable=False)
|
||||
|
@ -9,12 +9,29 @@
|
||||
import yaml
|
||||
from pathlib import Path
|
||||
from platform import system
|
||||
from shutil import copy
|
||||
from hashlib import pbkdf2_hmac
|
||||
from secrets import compare_digest
|
||||
|
||||
|
||||
def hash_password(password, secret=None):
|
||||
"""
|
||||
Hashes the administrator password
|
||||
:param password: The password to hash
|
||||
:param secret: The site secret
|
||||
:return: The hashed value as a hexadecimal string
|
||||
"""
|
||||
if not secret:
|
||||
secret = Secret()
|
||||
h = pbkdf2_hmac('sha512',
|
||||
password=password.encode('utf8'),
|
||||
salt=secret.key.encode('utf8'),
|
||||
iterations=250000)
|
||||
return h.hex()
|
||||
|
||||
|
||||
class Settings:
|
||||
"""
|
||||
Automatically loads the settings from /etc/ on Linux and same directory on other OS
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
file = Path("labertasche.yaml")
|
||||
if system().lower() == "linux":
|
||||
@ -23,9 +40,133 @@ class Settings:
|
||||
with file.open('r') as fp:
|
||||
conf = yaml.safe_load(fp)
|
||||
|
||||
self.weburl = conf['system']['weburl']
|
||||
self.cookie_domain = conf['system']['cookie_domain']
|
||||
self.database_uri = conf['system']['database_uri']
|
||||
self.debug = conf['system']['debug']
|
||||
self.cookie_secure = conf['system']['cookie_secure']
|
||||
|
||||
|
||||
class Secret:
|
||||
|
||||
def __init__(self):
|
||||
file = Path(".secret")
|
||||
if system().lower() == "linux":
|
||||
file = Path("/etc/labertasche/.secret")
|
||||
|
||||
with file.open('r') as fp:
|
||||
self.key = fp.readline()
|
||||
|
||||
|
||||
class Smileys:
|
||||
|
||||
def __init__(self):
|
||||
file = Path("smileys.yaml")
|
||||
if system().lower() == "linux":
|
||||
file = Path("/etc/labertasche/smileys.yaml")
|
||||
|
||||
with file.open('r') as fp:
|
||||
conf = yaml.safe_load(fp)
|
||||
|
||||
self.smileys = conf['smileys']
|
||||
|
||||
|
||||
class Credentials:
|
||||
def __init__(self):
|
||||
file = Path("credentials.yaml")
|
||||
if system().lower() == "linux":
|
||||
file = Path("/etc/labertasche/credentials.yaml")
|
||||
|
||||
with file.open('r') as fp:
|
||||
conf = yaml.safe_load(fp)
|
||||
|
||||
self.username = conf['credentials']['username']
|
||||
self.password = conf['credentials']['password']
|
||||
|
||||
def compare_password(self, userinput):
|
||||
"""
|
||||
Compares 2 passwords with one another
|
||||
|
||||
:param userinput: The input on the login page
|
||||
:return: True if the passwords match, otherwise False
|
||||
"""
|
||||
secret = Secret()
|
||||
hashed = pbkdf2_hmac('sha512',
|
||||
password=userinput.encode('utf8'),
|
||||
salt=secret.key.encode('utf8'),
|
||||
iterations=250000)
|
||||
return compare_digest(self.password, hashed.hex())
|
||||
|
||||
|
||||
# deprecated, leave as is
|
||||
class LegacySettings:
|
||||
"""
|
||||
Automatically loads the settings from /etc/ on Linux and same directory on other OS
|
||||
"""
|
||||
def __init__(self, use_backup: bool = False):
|
||||
file = Path("labertasche.yaml")
|
||||
if system().lower() == "linux":
|
||||
file = Path("/etc/labertasche/labertasche.yaml")
|
||||
|
||||
# Use backup when conversion is done, this is used in db_v2.py
|
||||
if use_backup:
|
||||
file = file.with_suffix('.bak')
|
||||
|
||||
with file.open('r') as fp:
|
||||
conf = yaml.safe_load(fp)
|
||||
|
||||
self.system = conf['system']
|
||||
self.smileys = conf['smileys']
|
||||
self.dashboard = conf['dashboard']
|
||||
self.gravatar = conf['gravatar']
|
||||
self.addons = conf['addons']
|
||||
self.smileys = conf['smileys']
|
||||
|
||||
def convert_to_v2(self):
|
||||
old = Path("labertasche.yaml")
|
||||
if system().lower() == "linux":
|
||||
old = Path("/etc/labertasche/labertasche.yaml")
|
||||
|
||||
systemvars = {
|
||||
'system': {
|
||||
'weburl': self.system['web_url'],
|
||||
'cookie_domain': self.system['cookie_domain'],
|
||||
'database_uri': self.system['database_uri'],
|
||||
'debug': self.system['debug'],
|
||||
'cookie_secure': self.system['cookie_secure']
|
||||
}
|
||||
}
|
||||
|
||||
credentials = {
|
||||
'credentials': {
|
||||
'username': self.dashboard['username'],
|
||||
'password': hash_password(self.dashboard['password'], self.system['secret'])
|
||||
}
|
||||
}
|
||||
|
||||
smileys = {
|
||||
'smileys': self.smileys
|
||||
}
|
||||
|
||||
# backup old config
|
||||
copy(old, old.with_suffix('.bak'))
|
||||
|
||||
# Write new config files
|
||||
p_sys = Path('labertasche.yaml')
|
||||
p_credentials = Path('credentials.yaml')
|
||||
p_smileys = Path('smileys.yaml')
|
||||
p_secret = Path('.secret')
|
||||
|
||||
if system().lower() == 'linux':
|
||||
p_sys = '/etc/labertasche/' / p_sys
|
||||
p_credentials = '/etc/labertasche/' / p_credentials
|
||||
p_smileys = '/etc/labertasche/' / p_smileys
|
||||
p_secret = '/etc/labertasche/' / p_secret
|
||||
|
||||
with p_sys.open('w') as fp:
|
||||
yaml.dump(systemvars, fp)
|
||||
with p_credentials.open('w') as fp:
|
||||
yaml.dump(credentials, fp)
|
||||
with p_smileys.open('w') as fp:
|
||||
yaml.dump(smileys, fp)
|
||||
with p_secret.open('w') as fp:
|
||||
fp.write(self.system['secret'])
|
||||
|
37
server.py
37
server.py
@ -6,13 +6,13 @@
|
||||
# * _repo : https://git.tuxstash.de/gothseidank/labertasche
|
||||
# * _license : This project is under MIT License
|
||||
# *********************************************************************************/
|
||||
import logging
|
||||
# noinspection PyProtectedMember
|
||||
from sqlalchemy.engine import Engine
|
||||
from logging import getLogger, ERROR as LOGGING_ERROR
|
||||
from flask import Flask, redirect, url_for
|
||||
from flask_cors import CORS
|
||||
from sqlalchemy import event, inspect
|
||||
# noinspection PyProtectedMember
|
||||
from sqlalchemy.engine import Engine
|
||||
from labertasche.settings import Settings
|
||||
from labertasche.settings import Settings, Secret
|
||||
from labertasche.database import labertasche_db
|
||||
from labertasche.blueprints import bp_comments, bp_login, bp_dashboard, bp_jsconnector, bp_dbupgrades
|
||||
from labertasche.helper import User
|
||||
@ -21,28 +21,29 @@ from datetime import timedelta
|
||||
|
||||
# Load settings
|
||||
settings = Settings()
|
||||
secret = Secret()
|
||||
|
||||
# Flask App
|
||||
laberflask = Flask(__name__)
|
||||
laberflask.config.update(dict(
|
||||
SESSION_COOKIE_DOMAIN=settings.system['cookie_domain'],
|
||||
SESSION_COOKIE_SECURE=settings.system['cookie_secure'],
|
||||
REMEMBER_COOKIE_SECURE=settings.system['cookie_secure'],
|
||||
SESSION_COOKIE_DOMAIN=settings.cookie_domain,
|
||||
SESSION_COOKIE_SECURE=settings.cookie_secure,
|
||||
REMEMBER_COOKIE_SECURE=settings.cookie_secure,
|
||||
REMEMBER_COOKIE_DURATION=timedelta(days=7),
|
||||
REMEMBER_COOKIE_HTTPONLY=True,
|
||||
REMEMBER_COOKIE_REFRESH_EACH_REQUEST=True,
|
||||
DEBUG=settings.system['debug'],
|
||||
SECRET_KEY=settings.system['secret'],
|
||||
TEMPLATES_AUTO_RELOAD=settings.system['debug'],
|
||||
SQLALCHEMY_DATABASE_URI=settings.system['database_uri'],
|
||||
DEBUG=settings.debug,
|
||||
SECRET_KEY=secret.key,
|
||||
TEMPLATES_AUTO_RELOAD=settings.debug,
|
||||
SQLALCHEMY_DATABASE_URI=settings.database_uri,
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS=False
|
||||
))
|
||||
|
||||
# Mark secret for deletion
|
||||
del secret
|
||||
|
||||
# CORS
|
||||
CORS(laberflask, resources={r"/comments": {"origins": settings.system['blog_url']},
|
||||
r"/api": {"origins": settings.system['web_url']},
|
||||
r"/dashboard": {"origins": settings.system['web_url']},
|
||||
})
|
||||
cors = CORS(laberflask)
|
||||
|
||||
# Import blueprints
|
||||
laberflask.register_blueprint(bp_comments)
|
||||
@ -52,8 +53,8 @@ laberflask.register_blueprint(bp_jsconnector)
|
||||
laberflask.register_blueprint(bp_dbupgrades)
|
||||
|
||||
# Disable Werkzeug's verbosity during development
|
||||
log = logging.getLogger('werkzeug')
|
||||
log.setLevel(logging.ERROR)
|
||||
log = getLogger('werkzeug')
|
||||
log.setLevel(LOGGING_ERROR)
|
||||
|
||||
# Set up login manager
|
||||
loginmgr = LoginManager(laberflask)
|
||||
@ -87,7 +88,7 @@ def login_invalid():
|
||||
# noinspection PyUnusedLocal
|
||||
@event.listens_for(Engine, "connect")
|
||||
def set_sqlite_pragma(dbapi_connection, connection_record):
|
||||
if settings.system["database_uri"][0:6] == 'sqlite':
|
||||
if settings.database_uri[0:6] == 'sqlite':
|
||||
cursor = dbapi_connection.cursor()
|
||||
cursor.execute("PRAGMA journal_mode=WAL;")
|
||||
cursor.close()
|
||||
|
@ -5,6 +5,48 @@
|
||||
// # * _license : This project is under MIT License
|
||||
// # *********************************************************************************/
|
||||
|
||||
async function get(partial, callback) {
|
||||
await fetch(window.location.protocol + "//" + window.location.host + partial,
|
||||
{
|
||||
mode: "cors",
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': window.location.host,
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
method: "GET"
|
||||
})
|
||||
.then(async function (response) {
|
||||
let result = await response.json();
|
||||
callback(result);
|
||||
})
|
||||
.catch(function (exc) {
|
||||
console.log(exc);
|
||||
return null;
|
||||
})
|
||||
}
|
||||
|
||||
async function post(partial, stringified_json, callback) {
|
||||
await fetch(window.location.protocol + "//" + window.location.host + partial,
|
||||
{
|
||||
mode: "cors",
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': window.location.host,
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
method: "POST",
|
||||
body: stringified_json
|
||||
})
|
||||
.then(async function (response) {
|
||||
let result = await response.json();
|
||||
callback(result);
|
||||
})
|
||||
.catch(function (exc) {
|
||||
console.log(exc);
|
||||
})
|
||||
}
|
||||
|
||||
// ------------------------------------------------------
|
||||
// Called when search for mail addresses in manage mail
|
||||
// ------------------------------------------------------
|
||||
@ -24,74 +66,15 @@ function dashboard_mailsearch(search_txt)
|
||||
}
|
||||
|
||||
// ------------------------------------------------------
|
||||
// Called when a new project is created,
|
||||
// posts it to the server
|
||||
// Deletes a project from the db
|
||||
// ------------------------------------------------------
|
||||
function new_project_save() {
|
||||
let modal_ok = document.getElementById('modal-ok');
|
||||
let modal_cancel = document.getElementById('modal-cancel');
|
||||
let short_help_short = document.getElementById('new-project-too-short');
|
||||
let short_help_invalid = document.getElementById('new-project-invalid-name');
|
||||
let name = document.getElementById('project-name').value
|
||||
|
||||
short_help_short.classList.add('is-hidden');
|
||||
short_help_invalid.classList.add('is-hidden');
|
||||
|
||||
// Validate input
|
||||
if (name.length === 0) {
|
||||
short_help_short.classList.remove('is-hidden');
|
||||
return false;
|
||||
}
|
||||
if (/^\w+$/.test(name) === false){
|
||||
short_help_invalid.classList.remove('is-hidden');
|
||||
return false;
|
||||
}
|
||||
|
||||
modal_ok.classList.add('is-loading');
|
||||
modal_cancel.classList.add('is-hidden');
|
||||
fetch(window.location.protocol + "//" + window.location.host + '/api/project/new',
|
||||
{
|
||||
mode: "cors",
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
method: "POST",
|
||||
// use real location
|
||||
body: JSON.stringify({
|
||||
"name": name
|
||||
})
|
||||
})
|
||||
.then(async function (response) {
|
||||
let result = await response.json();
|
||||
result = result['status'];
|
||||
modal_ok.classList.remove('is-loading');
|
||||
modal_cancel.classList.remove('is-hidden');
|
||||
if (result === "ok"){
|
||||
hide_modal('modal-new-project');
|
||||
window.location.reload(true);
|
||||
}
|
||||
if (result === "too-short"){
|
||||
short_help_short.classList.remove('is-hidden');
|
||||
}
|
||||
if (result === "invalid-name"){
|
||||
short_help_invalid.classList.remove('is-hidden');
|
||||
}
|
||||
})
|
||||
.catch(function (exc) {
|
||||
console.log(exc);
|
||||
})
|
||||
}
|
||||
|
||||
function project_delete()
|
||||
async function project_delete()
|
||||
{
|
||||
let modal = document.getElementById('modal-project-delete');
|
||||
let modal_ok = document.getElementById('modal-delete-ok');
|
||||
let modal_cancel = document.getElementById('modal-delete-cancel');
|
||||
|
||||
const project = modal.dataset.project;
|
||||
console.log("Project: " + project);
|
||||
const project = modal.dataset.name;
|
||||
if (project === null || project.length === 0){
|
||||
console.log("Couldn't find a valid dataset");
|
||||
return;
|
||||
@ -99,58 +82,253 @@ function project_delete()
|
||||
|
||||
modal_ok.classList.add('is-loading');
|
||||
modal_cancel.classList.add('is-hidden');
|
||||
fetch(window.location.protocol + "//" + window.location.host + '/api/project/delete/' + project,
|
||||
{
|
||||
mode: "cors",
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
method: "GET"
|
||||
})
|
||||
.then(async function (response) {
|
||||
let result = await response.json();
|
||||
result = result['status'];
|
||||
modal_ok.classList.remove('is-loading');
|
||||
modal_cancel.classList.remove('is-hidden');
|
||||
if (result === "ok") {
|
||||
hide_modal('modal-project-delete');
|
||||
window.location.reload(true);
|
||||
}
|
||||
|
||||
await get('/api/project/delete/' + project, function(result){
|
||||
modal_ok.classList.remove('is-loading');
|
||||
modal_cancel.classList.remove('is-hidden');
|
||||
if (result === "ok") {
|
||||
console.log(result);
|
||||
})
|
||||
.catch(function (exc) {
|
||||
console.log(exc);
|
||||
})
|
||||
}
|
||||
})
|
||||
modal.classList.remove('is-active');
|
||||
window.location.reload(true);
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ------------------------------------------------------
|
||||
// Hides any modal
|
||||
// ------------------------------------------------------
|
||||
function hide_modal(id_name)
|
||||
function hide_modal(id_name, redirect=null)
|
||||
{
|
||||
let el = document.getElementById(id_name);
|
||||
el.classList.remove("is-active");
|
||||
|
||||
if(redirect != null){
|
||||
window.location = redirect;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ------------------------------------------------------
|
||||
// Shows any modal
|
||||
// Shows any modal and attach project name
|
||||
// ------------------------------------------------------
|
||||
function show_modal(id_name)
|
||||
function show_modal(id_name, proj_name)
|
||||
{
|
||||
let el = document.getElementById(id_name);
|
||||
el.classList.add("is-active");
|
||||
el.setAttribute('data-name', proj_name);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------
|
||||
// Shows any modal
|
||||
// Shows edit/new modal
|
||||
// ------------------------------------------------------
|
||||
function show_modal_with_project(id_name, proj_name)
|
||||
async function show_modal_with_project(id_name, proj_name)
|
||||
{
|
||||
let el = document.getElementById(id_name);
|
||||
el.classList.add("is-active");
|
||||
el.setAttribute('data-project', proj_name)
|
||||
// Get title element
|
||||
let title = document.getElementById('modal-title');
|
||||
|
||||
// Get Dialog
|
||||
let modal = document.getElementById(id_name);
|
||||
|
||||
if (proj_name){
|
||||
// Get Data
|
||||
await get('/api/project/get/' + proj_name,
|
||||
function(r){
|
||||
document.getElementById('edit-project-blog-url').value = r['blogurl'];
|
||||
document.getElementById('edit-project-output').value = r['output'];
|
||||
document.getElementById('edit-project-gravatar-cache').checked = r['gravatar_cache'];
|
||||
document.getElementById('edit-project-gravatar-cache-dir').value = r['gravatar_cache_dir'];
|
||||
document.getElementById('edit-project-gravatar-size').value = r['gravatar_size'];
|
||||
document.getElementById('edit-project-send-otp').checked = r['sendotp'];
|
||||
document.getElementById('edit-project-addons-smileys').checked = r['addon_smileys'];
|
||||
});
|
||||
|
||||
// Set project name
|
||||
let proj_el = document.getElementById('edit-project-name');
|
||||
proj_el.value = proj_name
|
||||
|
||||
// Set project name
|
||||
title.innerText = "Edit project '" + proj_name + "'";
|
||||
|
||||
// Make active
|
||||
modal.classList.add("is-active");
|
||||
|
||||
// Edit mode
|
||||
modal.setAttribute('data-mode', 'edit');
|
||||
modal.setAttribute('data-name', proj_name);
|
||||
}
|
||||
if (proj_name == null){
|
||||
// Set project name
|
||||
title.innerText = "New Project";
|
||||
|
||||
// Reset fields, needed when user pressed cancel on edit modal
|
||||
document.getElementById('edit-project-name').value = "";
|
||||
document.getElementById('edit-project-blog-url').value = "";
|
||||
document.getElementById('edit-project-output').value = "";
|
||||
document.getElementById('edit-project-gravatar-cache').checked = true;
|
||||
document.getElementById('edit-project-gravatar-cache-dir').value = "";
|
||||
document.getElementById('edit-project-gravatar-size').value = 256;
|
||||
document.getElementById('edit-project-send-otp').checked = true;
|
||||
document.getElementById('edit-project-addons-smileys').checked = true;
|
||||
|
||||
// Edit mode
|
||||
modal.setAttribute('data-mode', 'new');
|
||||
|
||||
// Make active
|
||||
modal.classList.add("is-active");
|
||||
}
|
||||
}
|
||||
|
||||
async function save_project_settings(id_name)
|
||||
{
|
||||
// Spin the tea cups
|
||||
let btn = document.getElementById('modal-save-ok');
|
||||
btn.classList.add('is-loading');
|
||||
|
||||
// Get modal
|
||||
let modal = document.getElementById(id_name);
|
||||
|
||||
// Get field data
|
||||
let json_data = {
|
||||
"name": document.getElementById('edit-project-name').value,
|
||||
"blogurl": document.getElementById('edit-project-blog-url').value,
|
||||
"output": document.getElementById('edit-project-output').value,
|
||||
"gravatar_cache": document.getElementById('edit-project-gravatar-cache').checked,
|
||||
"gravatar_cache_dir": document.getElementById('edit-project-gravatar-cache-dir').value,
|
||||
"gravatar_size": document.getElementById('edit-project-gravatar-size').value,
|
||||
"sendotp": document.getElementById('edit-project-send-otp').checked,
|
||||
"addon_smileys": document.getElementById('edit-project-addons-smileys').checked
|
||||
}
|
||||
|
||||
if (modal.dataset.mode === "edit"){
|
||||
let old_name = modal.dataset.name;
|
||||
|
||||
await post('/api/project/edit/' + old_name, JSON.stringify(json_data), function(result){
|
||||
let error = document.getElementById('modal-edit-error-messages')
|
||||
error.innerText = ''
|
||||
|
||||
if (result['status'] === 'too-short'){
|
||||
error.innerText = "A required field has been left empty!"
|
||||
return;
|
||||
}
|
||||
if (result['status'] === 'invalid-project-name') {
|
||||
error.innerText = "The project name is not valid. Please only use alphanumeric characters!"
|
||||
return;
|
||||
}
|
||||
if (result['status'] === 'project-exists') {
|
||||
error.innerText = "A project with this name already exists!"
|
||||
return;
|
||||
}
|
||||
if (result['status'] === 'invalid-blog-url') {
|
||||
error.innerText = "The blog-url is invalid!"
|
||||
return;
|
||||
}
|
||||
if (result['status'] === 'invalid-path-output') {
|
||||
error.innerText = "This output path does not exist!"
|
||||
return;
|
||||
}
|
||||
if (result['status'] === 'invalid-path-cache') {
|
||||
error.innerText = "The cache path does not exist!"
|
||||
return;
|
||||
}
|
||||
if (result['status'] === 'exception') {
|
||||
error.innerText = "There was an unexpected exception. Please report this to contact@tuxstash.de:"
|
||||
error.innerText += result['msg']
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset button
|
||||
btn.classList.remove('is-loading');
|
||||
window.location.reload(true);
|
||||
})
|
||||
}
|
||||
if (modal.dataset.mode === 'new'){
|
||||
await post('/api/project/new', JSON.stringify(json_data), function(result){
|
||||
let error = document.getElementById('modal-edit-error-messages')
|
||||
error.innerText = '';
|
||||
|
||||
console.log(result['status']);
|
||||
if (result['status'] === 'too-short'){
|
||||
error.innerText = "A required field has been left empty!";
|
||||
btn.classList.remove('is-loading');
|
||||
return;
|
||||
}
|
||||
if (result['status'] === 'invalid-project-name') {
|
||||
error.innerText = "The project name is not valid. Please only use alphanumeric characters!";
|
||||
btn.classList.remove('is-loading');
|
||||
return;
|
||||
}
|
||||
if (result['status'] === 'project-exists') {
|
||||
error.innerText = "A project with this name already exists!";
|
||||
btn.classList.remove('is-loading');
|
||||
return;
|
||||
}
|
||||
if (result['status'] === 'invalid-blog-url') {
|
||||
error.innerText = "The blog-url is invalid!";
|
||||
btn.classList.remove('is-loading');
|
||||
return;
|
||||
}
|
||||
if (result['status'] === 'invalid-path-output') {
|
||||
error.innerText = "This output path does not exist!";
|
||||
btn.classList.remove('is-loading');
|
||||
return;
|
||||
}
|
||||
if (result['status'] === 'invalid-path-cache') {
|
||||
error.innerText = "The cache path does not exist!";
|
||||
btn.classList.remove('is-loading');
|
||||
return;
|
||||
}
|
||||
if (result['status'] === 'exception') {
|
||||
error.innerText = "There was an unexpected exception. Please report this to contact@tuxstash.de:";
|
||||
error.innerText += result['msg'];
|
||||
btn.classList.remove('is-loading');
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset button
|
||||
btn.classList.remove('is-loading');
|
||||
window.location.reload(true);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------
|
||||
// Disables inputs when gravatar caching is disabled.
|
||||
// ------------------------------------------------------
|
||||
function toggle_gravatar_settings(chkbx)
|
||||
{
|
||||
let cache = document.getElementById('edit-project-gravatar-cache-dir');
|
||||
let size = document.getElementById('edit-project-gravatar-size');
|
||||
|
||||
if(!chkbx.checked){
|
||||
cache.setAttribute('disabled', '');
|
||||
size.setAttribute('disabled', '');
|
||||
cache.value = "disabled";
|
||||
size.value = "disabled";
|
||||
}
|
||||
else{
|
||||
cache.removeAttribute('disabled');
|
||||
size.removeAttribute('disabled');
|
||||
cache.value = "";
|
||||
size.value = "256";
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------
|
||||
// Exports all comments
|
||||
// ------------------------------------------------------
|
||||
async function export_all_comments(btn)
|
||||
{
|
||||
btn.classList.add('is-loading');
|
||||
let proj_name = document.getElementById('modal-comments-export').dataset.name;
|
||||
|
||||
await get('/api/comment-export-all/' + proj_name, function(result){
|
||||
if (result['status'] === 'ok'){
|
||||
hide_modal('modal-comments-export');
|
||||
}
|
||||
if (result['status'] === 'not-found'){
|
||||
// Redirect to error
|
||||
hide_modal('modal-comments-export', '?error=404');
|
||||
}
|
||||
btn.classList.remove('is-loading');
|
||||
})
|
||||
}
|
||||
|
@ -4,14 +4,6 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0, user-scalable=yes">
|
||||
<meta name="description" content="labertasche comment system dashboard">
|
||||
|
||||
<!-- Preload -->
|
||||
<link rel="preload" href="/static/css/open-sans-v18-latin-regular.woff2" as="font" type="font/woff2" crossorigin="anonymous" media="all">
|
||||
<link rel="preload" href="/static/css/materialdesignicons-webfont.woff2" as="font" type="font/woff2" crossorigin="anonymous" media="all">
|
||||
<link rel="preload" href="/static/css/labertasche.css" as="style" crossorigin="anonymous" media="screen">
|
||||
<link rel="preload" href="/static/css/materialdesignicons.min.css" as="style" crossorigin="anonymous" media="screen">
|
||||
<link rel="preload" href="/static/css/Chart.min.css" as="style" media="screen">
|
||||
|
||||
<link rel="stylesheet" href="/static/css/labertasche.css" media="screen">
|
||||
<link rel="stylesheet" href="/static/css/materialdesignicons.min.css" media="screen">
|
||||
<link rel="stylesheet" href="/static/css/Chart.min.css" media="screen">
|
||||
|
@ -4,15 +4,9 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0, user-scalable=yes">
|
||||
<meta name="description" content="labertasche comment system dashboard">
|
||||
<link rel="preconnect" href="https://cdn.materialdesignicons.com/">
|
||||
|
||||
<!-- Preload -->
|
||||
<link rel="preload" href="/static/css/open-sans-v18-latin-regular.woff2" as="font" type="font/woff2" crossorigin="anonymous" media="all">
|
||||
<link rel="preload" href="https://cdn.materialdesignicons.com/5.4.55/css/materialdesignicons.min.css" as="style">
|
||||
<link rel="preload" as="style" href="/static/css/labertasche.css">
|
||||
|
||||
<link rel="stylesheet" href="https://cdn.materialdesignicons.com/5.4.55/css/materialdesignicons.min.css">
|
||||
<link rel="stylesheet" href="/static/css/labertasche.css" media="screen">
|
||||
<link rel="stylesheet" href="/static/css/materialdesignicons.min.css" media="screen">
|
||||
<link rel="stylesheet" href="/static/css/Chart.min.css" media="screen">
|
||||
|
||||
<title>labertasche Dashboard</title>
|
||||
</head>
|
||||
|
22
templates/modals/comments-export-all.html
Normal file
22
templates/modals/comments-export-all.html
Normal file
@ -0,0 +1,22 @@
|
||||
<div class="modal" id="modal-comments-export">
|
||||
<div class="modal-background"></div>
|
||||
<div class="modal-card">
|
||||
<header class="modal-card-head has-background-warning">
|
||||
<p class="modal-card-title has-text-black is-uppercase">Export all comments</p>
|
||||
<button onclick="hide_modal('modal-comments-export')" class="delete" aria-label="close"></button>
|
||||
</header>
|
||||
<section class="modal-card-body has-text-white bg-deepmatte">
|
||||
<p class="block">This will export all comments of this project to all locations.
|
||||
Usually this is not needed, but can be helpful, if you have imported backups or similar.</p>
|
||||
<p class="block has-text-danger">Do you wish to proceed?</p>
|
||||
</section>
|
||||
<footer class="modal-card-foot bg-deepmatte">
|
||||
<button id="modal-ok" onclick="export_all_comments(this);" class="button is-success">
|
||||
OK
|
||||
</button>
|
||||
<button id="modal-cancel" onclick="hide_modal('modal-comments-export')" class="button is-info">
|
||||
CANCEL
|
||||
</button>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
21
templates/modals/project-delete.html
Normal file
21
templates/modals/project-delete.html
Normal file
@ -0,0 +1,21 @@
|
||||
<div class="modal" id="modal-project-delete">
|
||||
<div class="modal-background"></div>
|
||||
<div class="modal-card">
|
||||
<header class="modal-card-head has-background-warning">
|
||||
<p class="modal-card-title has-text-black">WARNING!</p>
|
||||
<button onclick="hide_modal('modal-project-delete')" class="delete" aria-label="close"></button>
|
||||
</header>
|
||||
<section class="modal-card-body has-text-black">
|
||||
You are about to delete a project. All associated data will be unrecoverably lost!
|
||||
Please perform a manual sql dump if you would like to retain that data.
|
||||
</section>
|
||||
<footer class="modal-card-foot">
|
||||
<button id="modal-delete-ok" onclick="project_delete()" class="button is-danger">
|
||||
OK
|
||||
</button>
|
||||
<button id="modal-delete-cancel" onclick="hide_modal('modal-project-delete')" class="button is-success">
|
||||
CANCEL
|
||||
</button>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
109
templates/modals/project_edit.html
Normal file
109
templates/modals/project_edit.html
Normal file
@ -0,0 +1,109 @@
|
||||
<div class="modal" id="modal-project-edit">
|
||||
<div class="modal-background"></div>
|
||||
<div class="modal-card">
|
||||
<header class="modal-card-head has-background-warning">
|
||||
<p id="modal-title" class="modal-card-title has-text-black">Edit Project</p>
|
||||
<button onclick="hide_modal('modal-project-edit')" class="delete" aria-label="close"></button>
|
||||
</header>
|
||||
<section class="modal-card-body has-text-black bg-deepmatte">
|
||||
<form>
|
||||
<div class="field">
|
||||
<div class="control has-icons-left"
|
||||
data-tippy-content="Please select an unique name for your project.">
|
||||
<input class="input" id="edit-project-name"
|
||||
name="edit-project-name" type="text">
|
||||
<span class="icon is-small is-left">
|
||||
<i class="mdi mdi-24px mdi-help-rhombus-outline"></i>
|
||||
</span>
|
||||
</div>
|
||||
<label class="label help has-text-white" for="edit-project-name">
|
||||
Project Name
|
||||
</label>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="control has-icons-left"
|
||||
data-tippy-content="URL of your hugo site, e.g. https://example.com">
|
||||
<input class="input" id="edit-project-blog-url" name="edit-project-blog-url" type="text">
|
||||
<span class="icon is-small is-left">
|
||||
<i class="mdi mdi-24px mdi-help-rhombus-outline"></i>
|
||||
</span>
|
||||
</div>
|
||||
<label class="label help has-text-white" for="edit-project-blog-url">
|
||||
URL of your Hugo site for this project
|
||||
</label>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="control has-icons-left"
|
||||
data-tippy-content="The path to the data directory of your Hugo installation.">
|
||||
<input class="input" id="edit-project-output" name="edit-project-output" type="text">
|
||||
<span class="icon is-small is-left">
|
||||
<i class="mdi mdi-24px mdi-help-rhombus-outline"></i>
|
||||
</span>
|
||||
</div>
|
||||
<label class="label help has-text-white" for="edit-project-output">Output Directory</label>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<label class="checkbox help has-text-white" for="edit-project-gravatar-cache">
|
||||
<input id="edit-project-gravatar-cache" class="checkbox" type="checkbox"
|
||||
name="edit-project-gravatar-cache" checked onclick="toggle_gravatar_settings(this);">
|
||||
Cache Gravatar images?
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="control has-icons-left"
|
||||
data-tippy-content="The directory where to save the gravatar images. Should be a full path.">
|
||||
<input class="input" id="edit-project-gravatar-cache-dir" name="edit-project-gravatar-cache-dir" type="text">
|
||||
<span class="icon is-small is-left">
|
||||
<i class="mdi mdi-24px mdi-help-rhombus-outline"></i>
|
||||
</span>
|
||||
</div>
|
||||
<label class="label help has-text-white" for="edit-project-gravatar-cache-dir">
|
||||
Gravatar Cache Directory
|
||||
</label>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="control has-icons-left"
|
||||
data-tippy-content="The numeric size of the images to download. Must be a power of 2.">
|
||||
<input class="input" id="edit-project-gravatar-size" name="edit-project-gravatar-size" type="text" value="256">
|
||||
<span class="icon is-small is-left">
|
||||
<i class="mdi mdi-24px mdi-help-rhombus-outline"></i>
|
||||
</span>
|
||||
</div>
|
||||
<label class="label help has-text-white" for="edit-project-gravatar-size">
|
||||
Gravatar image size
|
||||
</label>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<label class="checkbox help has-text-white" for="edit-project-send-otp">
|
||||
<input id="edit-project-send-otp" class="checkbox" type="checkbox"
|
||||
name="edit-project-send-otp" checked>
|
||||
Send OTP to publish?
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<label class="checkbox help has-text-white" for="edit-project-addons-smileys">
|
||||
<input id="edit-project-addons-smileys" class="checkbox" type="checkbox"
|
||||
name="edit-project-addons-smileys" checked>
|
||||
Enable Smiley Addon?
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="label has-text-danger" id="modal-edit-error-messages">
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
<footer class="modal-card-foot bg-deepmatte">
|
||||
<button id="modal-save-ok" onclick="save_project_settings('modal-project-edit');" class="button is-success">
|
||||
OK
|
||||
</button>
|
||||
<button onclick="hide_modal('modal-project-edit')" class="button is-info">
|
||||
CANCEL
|
||||
</button>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
24
templates/modals/project_not_found.html
Normal file
24
templates/modals/project_not_found.html
Normal file
@ -0,0 +1,24 @@
|
||||
<div class="modal" id="modal-project-not-found">
|
||||
<div class="modal-background"></div>
|
||||
<div class="modal-card">
|
||||
<header class="modal-card-head has-background-danger">
|
||||
<p class="modal-card-title has-text-white">ERROR</p>
|
||||
<button onclick="hide_modal('modal-project-not-found')" class="delete" aria-label="close"></button>
|
||||
</header>
|
||||
<section class="modal-card-body has-text-black">
|
||||
The specified project was not found! Did you delete it? Try refreshing the page.<br>
|
||||
If you believe this to be a bug, please report it
|
||||
<a class="has-text-info"
|
||||
href="https://github.com/domeniko-gentner/labertasche/issues"
|
||||
target="_blank" rel="nofollow noopener norefferer">
|
||||
here
|
||||
</a>.
|
||||
</section>
|
||||
<footer class="modal-card-foot">
|
||||
<button id="modal-ok" onclick="hide_modal('modal-project-not-found', {{ url_for('bp_dashboard.dashboard_project_list') }})"
|
||||
class="button is-success">
|
||||
OK
|
||||
</button>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
@ -36,10 +36,11 @@
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<div class="card-footer-item">
|
||||
<!-- TODO: onclick -->
|
||||
<a class="has-text-weight-bold has-text-black is-uppercase"
|
||||
onclick="show_modal('modal-new-project');"
|
||||
data-tippy-content="Create a new project"
|
||||
href="#">NEW</a>
|
||||
onclick="show_modal_with_project('modal-project-edit', null)"
|
||||
data-tippy-content="Create a new project" href="#"
|
||||
>NEW</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -80,22 +81,24 @@
|
||||
<div class="card-footer-item has-background-danger-dark">
|
||||
<a class="has-text-weight-bold has-text-white is-uppercase"
|
||||
data-tippy-content="Delete the project and all of its content"
|
||||
onclick="show_modal_with_project('modal-project-delete', '{{ each['name'] }}');">DELETE</a>
|
||||
onclick="show_modal('modal-project-delete', '{{ each['name'] }}');">DELETE</a>
|
||||
</div>
|
||||
<div class="card-footer-item">
|
||||
<a class="has-text-weight-bold has-text-black is-uppercase"
|
||||
onclick="show_modal_with_project('modal-project-edit', '{{ each['name'] }}');"
|
||||
data-tippy-content="Edit the name of the project and it's properties"
|
||||
href="#">EDIT</a>
|
||||
</div>
|
||||
<div class="card-footer-item">
|
||||
<a class="has-text-weight-bold has-text-black is-uppercase"
|
||||
onclick="show_modal('modal-comments-export', '{{ each['name'] }}')"
|
||||
data-tippy-content="Export all comments to Hugo.<br>This is normally not needed."
|
||||
href="#">EXPORT</a>
|
||||
</div>
|
||||
<div class="card-footer-item">
|
||||
<a class="has-text-weight-bold has-text-black is-uppercase"
|
||||
data-tippy-content="Manage this project"
|
||||
href="{{ url_for('bp_dashboard.dashboard_project_stats', project=each['name']) }}">VIEW</a>
|
||||
href="{{ url_for('bp_dashboard.dashboard_project_stats', project=each['name']) }}">manage</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -103,142 +106,14 @@
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal" id="modal-new-project">
|
||||
<div class="modal-background"></div>
|
||||
<div class="modal-card">
|
||||
<header class="modal-card-head">
|
||||
<p class="modal-card-title">New Project</p>
|
||||
<button onclick="hide_modal('modal-new-project')" class="delete" aria-label="close"></button>
|
||||
</header>
|
||||
<section class="modal-card-body">
|
||||
<label for="project-name" class="has-text-black">PROJECT NAME
|
||||
<input class="input is-success"
|
||||
type="text"
|
||||
name="project-name"
|
||||
id="project-name"
|
||||
placeholder="Type a name without special characters.."
|
||||
>
|
||||
</label>
|
||||
<p id="new-project-too-short" class="is-hidden help has-text-danger">Input too short. Needs at least 1 character!</p>
|
||||
<p id="new-project-invalid-name" class="is-hidden help has-text-danger">Input is invalid. Please use only a-z and 0-9.</p>
|
||||
</section>
|
||||
<footer class="modal-card-foot">
|
||||
<button id="modal-ok" onclick="new_project_save()" class="button is-success">Save</button>
|
||||
<button id="modal-cancel" onclick="hide_modal('modal-new-project')" class="button is-danger">Cancel</button>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal" id="modal-project-not-found">
|
||||
<div class="modal-background"></div>
|
||||
<div class="modal-card">
|
||||
<header class="modal-card-head has-background-danger">
|
||||
<p class="modal-card-title has-text-white">ERROR</p>
|
||||
<button onclick="hide_modal('modal-project-not-found')" class="delete" aria-label="close"></button>
|
||||
</header>
|
||||
<section class="modal-card-body has-text-black">
|
||||
The specified project was not found! Did you delete it? Try refreshing the page.<br>
|
||||
If you believe this to be a bug, please report it
|
||||
<a class="has-text-info"
|
||||
href="https://github.com/domeniko-gentner/labertasche/issues"
|
||||
target="_blank" rel="nofollow noopener norefferer">
|
||||
here
|
||||
</a>.
|
||||
</section>
|
||||
<footer class="modal-card-foot">
|
||||
<button id="modal-ok" onclick="hide_modal('modal-project-not-found')" class="button is-success">
|
||||
OK
|
||||
</button>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal" id="modal-project-delete">
|
||||
<div class="modal-background"></div>
|
||||
<div class="modal-card">
|
||||
<header class="modal-card-head has-background-warning">
|
||||
<p class="modal-card-title has-text-black">WARNING!</p>
|
||||
<button onclick="hide_modal('modal-project-delete')" class="delete" aria-label="close"></button>
|
||||
</header>
|
||||
<section class="modal-card-body has-text-black">
|
||||
You are about to delete a project. All associated data will be unrecoverably lost!
|
||||
Please perform a manual sql dump if you would like to retain that data.
|
||||
</section>
|
||||
<footer class="modal-card-foot">
|
||||
<button id="modal-delete-ok" onclick="project_delete()" class="button is-danger">
|
||||
OK
|
||||
</button>
|
||||
<button id="modal-delete-cancel" onclick="hide_modal('modal-project-delete')" class="button is-success">
|
||||
CANCEL
|
||||
</button>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal is-active" id="modal-project-edit">
|
||||
<div class="modal-background"></div>
|
||||
<div class="modal-card">
|
||||
<header class="modal-card-head has-background-warning">
|
||||
<p class="modal-card-title has-text-black">Edit Project</p>
|
||||
<button onclick="hide_modal('modal-project-edit')" class="delete" aria-label="close"></button>
|
||||
</header>
|
||||
<section class="modal-card-body has-text-black bg-deepmatte">
|
||||
<form>
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<input class="input" id="edit-project-name" name="edit-project-name" type="text">
|
||||
</div>
|
||||
<label class="label help has-text-white" for="edit-project-name">
|
||||
Project Name
|
||||
</label>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<input class="input" id="edit-project-web-url" name="edit-project-web-url" type="text">
|
||||
</div>
|
||||
<label class="label help has-text-white" for="edit-project-web-url">
|
||||
URI of the comment system
|
||||
</label>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<input class="input" id="edit-project-blog-url" name="edit-project-blog-url" type="text">
|
||||
</div>
|
||||
<label class="label help has-text-white" for="edit-project-blog-url">
|
||||
URL of your Hugo site for this project
|
||||
</label>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<input class="input" id="edit-project-output" name="edit-project-output" type="text">
|
||||
</div>
|
||||
<label class="label help has-text-white" for="edit-project-output">Output Directory</label>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<label class="checkbox help has-text-white" for="edit-project-send-otp">
|
||||
<input id="edit-project-send-otp" class="checkbox" type="checkbox"
|
||||
name="edit-project-send-otp" checked>
|
||||
Send OTP to publish?
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
<footer class="modal-card-foot bg-deepmatte">
|
||||
<button id="modal-delete-ok" onclick="project_delete()" class="button is-danger">
|
||||
OK
|
||||
</button>
|
||||
<button id="modal-delete-cancel" onclick="hide_modal('modal-project-edit')" class="button is-success">
|
||||
CANCEL
|
||||
</button>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
{% include "modals/project_not_found.html" %}
|
||||
{% include "modals/project-delete.html" %}
|
||||
{% include "modals/project_edit.html" %}
|
||||
{% include "modals/comments-export-all.html"%}
|
||||
{% endblock %}
|
||||
{% block javascript %}
|
||||
tippy('[data-tippy-content]', {
|
||||
allowHTML: true,
|
||||
delay: 500
|
||||
});
|
||||
|
||||
weburl = document.getElementById('edit-project-web-url')
|
||||
weburl.value = window.location.host;
|
||||
{% endblock %}
|
||||
|
Loading…
x
Reference in New Issue
Block a user