From c0a1a88ea85901f3d8632fb0dc45f0a34f002472 Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Sun, 1 Apr 2018 18:17:12 -0400 Subject: [PATCH 1/3] [#83] Login: fix option hiden menu login when user is logging --- src/web/partials/_base.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/web/partials/_base.html b/src/web/partials/_base.html index c1d9373f..adccce29 100644 --- a/src/web/partials/_base.html +++ b/src/web/partials/_base.html @@ -81,7 +81,7 @@
  • Admin
  • {% end %} - {% if not disable_login and not hide_menu_login %} + {% if not disable_login and (not hide_menu_login or current_user) %} {% if current_user %}
  • {{current_user.get("username")}}
  • Déconnexion
  • From f23a56e74bf58330059904cdf86a9a26f87d3cb6 Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Sun, 1 Apr 2018 16:01:28 -0400 Subject: [PATCH 2/3] [#79] Doc Generator: support google drive spreadsheet to generate doc - Implement config file to add the url of the spreadsheet to read - Add google API key to connect to google API - Add documentation for how to use it - Generate Manual and Lore documentation from spreadsheet - Add spreadsheet validation format - Add editor admin page - Add button to share document with writting permission to actual user - Add verification when connect to remote document - Persistente remote connection file - Add generator button in editor page - Validate email not exist before shared document permission - Show status and error message for editor execution - Reload and save manual documentation after generation - Update url of doc from Client - Inform user when missing permission for the software - show email to invite when missing permission --- INSTALL.md | 19 +- database/example_config.json | 5 + doc/developer/google_api.md | 21 + src/web/__main__.py | 12 +- src/web/base_handler.py | 4 + src/web/handlers.py | 182 +++++++ src/web/partials/_base.html | 1 + src/web/partials/admin/_base.html | 5 +- src/web/partials/admin/editor.html | 60 +++ src/web/partials/admin/news.html | 4 +- src/web/py_class/config.py | 77 +++ src/web/py_class/doc_generator/__init__.py | 0 .../doc_generator/doc_connector_gspread.py | 492 ++++++++++++++++++ .../doc_generator/doc_generator_gspread.py | 240 +++++++++ src/web/py_class/lore.py | 18 +- src/web/py_class/manual.py | 18 +- .../js/tl_module/editor_ctrl/editor_ctrl.js | 230 ++++++++ .../js/tl_module/profile_ctrl/profile_ctrl.js | 4 +- src/web/resources/js/tool.js | 4 + src/web/web.py | 26 +- 20 files changed, 1398 insertions(+), 24 deletions(-) create mode 100644 database/example_config.json create mode 100644 doc/developer/google_api.md create mode 100644 src/web/partials/admin/editor.html create mode 100644 src/web/py_class/config.py create mode 100644 src/web/py_class/doc_generator/__init__.py create mode 100644 src/web/py_class/doc_generator/doc_connector_gspread.py create mode 100644 src/web/py_class/doc_generator/doc_generator_gspread.py create mode 100644 src/web/resources/js/tl_module/editor_ctrl/editor_ctrl.js diff --git a/INSTALL.md b/INSTALL.md index 3e33430b..a4d3a103 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -18,27 +18,36 @@ Or download it from here : https://github.com/AdminTL/gestion_personnage_TL/arch Dependencies ------------ -You need python3, python3-tornado and python3-sockjs-tornado +You need python3.5 Arch Linux ------ ```{r, engine='bash', count_lines} sudo pacman -S python python-pip -sudo pip install tornado sockjs-tornado tinydb bcrypt +sudo pip install tornado sockjs-tornado tinydb bcrypt PyOpenSSL oauth2client gspread ``` Mac OSX ------- ```{r, engine='bash', count_lines} brew install python3 -sudo pip3 install tornado sockjs-tornado tinydb bcrypt +sudo pip3 install tornado sockjs-tornado tinydb bcrypt PyOpenSSL oauth2client gspread ``` Ubuntu / Debian --------------- ```{r, engine='bash', count_lines} sudo apt-get install python3 python3-pip -sudo pip3 install tornado sockjs-tornado tinydb bcrypt +sudo pip3 install tornado sockjs-tornado tinydb bcrypt PyOpenSSL oauth2client gspread +``` + +If you have problem with oauth2client, maybe you need to update pyasn. +```{r, engine='bash', count_lines} +sudo apt-get --reinstall install python-pyasn1 python-pyasn1-modules +``` +or +```{r, engine='bash', count_lines} +sudo pip4 install --upgrade google-auth-oauthlib ``` Windows @@ -47,7 +56,7 @@ Install python 3 from https://www.python.org/downloads/ using the installer Install nodejs if not done already https://nodejs.org/en/download/ Start a cmd prompt with admin privileges by right-clicking->run as administrator (git-bash works well) ``` -pip3 install tornado sockjs-tornado tinydb +pip3 install tornado sockjs-tornado tinydb PyOpenSSL oauth2client gspread ``` Bower diff --git a/database/example_config.json b/database/example_config.json new file mode 100644 index 00000000..ac0a9862 --- /dev/null +++ b/database/example_config.json @@ -0,0 +1,5 @@ +{ + "google_spreadsheet": { + "file_url": null + } +} \ No newline at end of file diff --git a/doc/developer/google_api.md b/doc/developer/google_api.md new file mode 100644 index 00000000..af89f25a --- /dev/null +++ b/doc/developer/google_api.md @@ -0,0 +1,21 @@ +Google API +========== + +* The Google OAuth2 is used, [read file authentication.md](./authentication.md). +* The Google Spreadsheet is used to update the database documentation for manual and other document. + +Google Spreadsheet +------------------ + +Good documentation: http://gspread.readthedocs.io/en/latest/oauth2.html + +To resume : +1. Create signed credentials "Service account key" for Drive API in format JSON. +2. Move the file to "database/client_secret.json" + +Manual generator +---------------- + +To enable the option to generate the manual from a spreadsheet: +1. Copy the file ./database/example_config.json to ./database/config.json +2. Fill information in key "google_spreadsheet". \ No newline at end of file diff --git a/src/web/__main__.py b/src/web/__main__.py index 594a1380..d3c02927 100644 --- a/src/web/__main__.py +++ b/src/web/__main__.py @@ -6,6 +6,7 @@ import argparse import os import web +from py_class.config import Config WEB_ROOT_DIR = os.path.dirname(os.path.realpath(__file__)) WEB_DEFAULT_STATIC_DIR = os.path.join(WEB_ROOT_DIR) @@ -15,6 +16,8 @@ DB_MANUAL_PATH = os.path.join(WEB_ROOT_DIR, "..", "..", "database", "tl_manual.json") DB_LORE_PATH = os.path.join(WEB_ROOT_DIR, "..", "..", "database", "tl_lore.json") DB_AUTH_PATH = os.path.join(WEB_ROOT_DIR, "..", "..", "database", "auth.json") +GOOGLE_API_SECRET_PATH = os.path.join(WEB_ROOT_DIR, "..", "..", "database", "client_secret.json") +CONFIG_PATH = os.path.join(WEB_ROOT_DIR, "..", "..", "database", "config.json") def main(): @@ -85,15 +88,20 @@ def parse_args(): _parser.db_manual_path = DB_MANUAL_PATH _parser.db_lore_path = DB_LORE_PATH _parser.db_auth_keys_path = DB_AUTH_PATH + _parser.db_google_API_path = GOOGLE_API_SECRET_PATH + _parser.db_config_path = CONFIG_PATH - # apply condition + # Apply condition if not _parser.ssl and _parser.redirect_http_to_https: - # cannot redirect http to https if ssl is not enable + # Cannot redirect http to https if ssl is not enable _parser.redirect_http_to_https = False if _parser.disable_character: _parser.disable_user_character = True + # Add general configuration in parser + _parser.config = Config(_parser) + return _parser diff --git a/src/web/base_handler.py b/src/web/base_handler.py index 14205ceb..c24a510e 100644 --- a/src/web/base_handler.py +++ b/src/web/base_handler.py @@ -13,6 +13,8 @@ class BaseHandler(tornado.web.RequestHandler): _db = None _invalid_login = None _redirect_http_to_https = None + _config = None + _doc_generator_gspread = None _global_arg = {} def initialize(self, **kwargs): @@ -23,6 +25,8 @@ def initialize(self, **kwargs): self._invalid_login = self.get_argument("invalid", default="disable_login" if kwargs.get("disable_login") else None) self._redirect_http_to_https = kwargs.get("redirect_http_to_https") + self._config = kwargs.get("config") + self._doc_generator_gspread = kwargs.get("doc_generator_gspread") self._global_arg = { "debug": self._debug, diff --git a/src/web/handlers.py b/src/web/handlers.py index 09e3bf22..7ea75c98 100644 --- a/src/web/handlers.py +++ b/src/web/handlers.py @@ -443,6 +443,25 @@ def get(self): raise tornado.web.Finish() +class AdminEditorHandler(base_handler.BaseHandler): + @tornado.web.asynchronous + @tornado.web.authenticated + def get(self): + if self._global_arg["disable_admin"]: + # Not Found + self.set_status(404) + self.send_error(404) + raise tornado.web.Finish() + if self.is_permission_admin(): + self.render('admin/editor.html', **self._global_arg) + else: + print("Insufficient permissions from %s" % self.request.remote_ip, file=sys.stderr) + # Forbidden + self.set_status(403) + self.send_error(403) + raise tornado.web.Finish() + + class ProfileHandler(base_handler.BaseHandler): @tornado.web.asynchronous @tornado.web.authenticated @@ -686,6 +705,169 @@ def get(self): self.finish() +class EditorCmdInfoHandler(jsonhandler.JsonHandler): + @tornado.web.asynchronous + @tornado.web.authenticated + def get(self): + if not self.is_permission_admin(): + print("Insufficient permissions from %s" % self.request.remote_ip, file=sys.stderr) + # Forbidden + self.set_status(403) + self.send_error(403) + raise tornado.web.Finish() + + current_user = self.get_current_user() + + # Do get_instance first + doc_generator = self._doc_generator_gspread.get_instance() + + # Fetch information + if doc_generator: + has_access_perm = doc_generator.check_has_permission() + has_user_writer_perm = doc_generator.has_user_write_permission(current_user.get("email")) + else: + has_user_writer_perm = False + has_access_perm = False + + file_url = self._doc_generator_gspread.get_url() + email_google_service = self._doc_generator_gspread.get_email_service() + is_auth = self._doc_generator_gspread.is_auth() + can_generate = bool(doc_generator and + not self._doc_generator_gspread.has_error() and + not doc_generator.has_error() and + has_access_perm and is_auth + ) + + info = { + "file_url": file_url, + "is_auth": is_auth, + "user_has_writer_perm": has_user_writer_perm, + "has_access_perm": has_access_perm, + "email_google_service": email_google_service, + "can_generate": can_generate + } + + if self._doc_generator_gspread.has_error(): + error = self._doc_generator_gspread.get_error() + info["error"] = error + + self.write(info) + self.finish() + + +class EditorCmdAddGeneratorShareHandler(jsonhandler.JsonHandler): + @tornado.web.asynchronous + @tornado.web.authenticated + def post(self): + if not self.is_permission_admin(): + print("Insufficient permissions from %s" % self.request.remote_ip, file=sys.stderr) + # Forbidden + self.set_status(403) + self.send_error(403) + raise tornado.web.Finish() + + current_user = self.get_current_user() + + doc_generator = self._doc_generator_gspread.get_instance() + if not doc_generator: + status = self._doc_generator_gspread.get_error() + self.write(status) + self.finish() + return + + email = current_user.get("email") + has_writer_perm = doc_generator.has_user_write_permission(email) + if not has_writer_perm: + status = doc_generator.share_document(current_user.get("email")) + + if status: + data = {"status": "Document shared."} + else: + data = {"error": "Cannot share the document."} + else: + data = {"status": "Document already shared to user %s." % email} + + self.write(data) + self.finish() + + +class EditorCmdUpdateFileUrlHandler(jsonhandler.JsonHandler): + @tornado.web.asynchronous + @tornado.web.authenticated + def post(self): + if not self.is_permission_admin(): + print("Insufficient permissions from %s" % self.request.remote_ip, file=sys.stderr) + # Forbidden + self.set_status(403) + self.send_error(403) + raise tornado.web.Finish() + + self.prepare_json() + + file_url = self.get_argument("file_url") + if not file_url: + status = {"error": "The url is empty."} + self.write(status) + self.finish() + return + + # Validate is not the same link + actual_file_url = self._doc_generator_gspread.get_url() + if actual_file_url == file_url: + status = {"error": "The url is already open."} + self.write(status) + self.finish() + return + + # Update and save the new link + if self._doc_generator_gspread.connect(): + self._doc_generator_gspread.update_url(url=file_url, save=True) + + # Return data + if self._doc_generator_gspread.has_error(): + data = self._doc_generator_gspread.get_error() + else: + data = {"status": "Document url is updated."} + + self.write(data) + self.finish() + + +class EditorCmdGenerateAndSaveHandler(jsonhandler.JsonHandler): + @tornado.web.asynchronous + @tornado.web.authenticated + def post(self): + if not self.is_permission_admin(): + print("Insufficient permissions from %s" % self.request.remote_ip, file=sys.stderr) + # Forbidden + self.set_status(403) + self.send_error(403) + raise tornado.web.Finish() + + # Generate the document. An error is returned if status is not True + doc_generator = self._doc_generator_gspread.get_instance() + if not doc_generator: + status = self._doc_generator_gspread.get_error() + self.write(status) + self.finish() + return + status = doc_generator.generate_doc() + if status: + document = doc_generator.get_generated_doc() + if "manual" in document: + doc_part = document.get("manual") + self._manual.update({"manual": doc_part}, save=True) + if "lore" in document: + doc_part = document.get("lore") + self._lore.update({"lore": doc_part}, save=True) + status = {"status": "Generated with success. Database updated."} + else: + status = doc_generator.get_error(force_error=True) + + self.write(status) + self.finish() + + class StatSeasonPass(jsonhandler.JsonHandler): @tornado.web.asynchronous def get(self): diff --git a/src/web/partials/_base.html b/src/web/partials/_base.html index adccce29..a0913cc4 100644 --- a/src/web/partials/_base.html +++ b/src/web/partials/_base.html @@ -181,6 +181,7 @@

    + diff --git a/src/web/partials/admin/_base.html b/src/web/partials/admin/_base.html index 87713700..c60d6532 100644 --- a/src/web/partials/admin/_base.html +++ b/src/web/partials/admin/_base.html @@ -67,6 +67,7 @@ {% if not disable_character %}
  • Personnage
  • {% end %} +
  • Éditeur
  • {% if not disable_custom_css %} {% end %} -
  • Quitter
  • +
  • Quitter
  • {% if not disable_login %} {% if current_user %} @@ -178,6 +179,8 @@

    + + diff --git a/src/web/partials/admin/editor.html b/src/web/partials/admin/editor.html new file mode 100644 index 00000000..c8246b6e --- /dev/null +++ b/src/web/partials/admin/editor.html @@ -0,0 +1,60 @@ +{% extends "_base.html" %} + +{% block content %} + +
    +

    Gestionnaire de documentation

    +

    Générateur de documentation à partir de Google Drive Spreadsheet

    +
    + Mise à jour des informations. +
    +
    + Cet outil permet d'ouvrir un fichier sur Google Drive Spreadsheet, d'itérer dans le document pour extraire les données, valider le formatage du document et générer la base de donnée des documents. +
    + + +
    + + {{! model_editor.generated_doc.status.text }} + +
    + +{% end %} diff --git a/src/web/partials/admin/news.html b/src/web/partials/admin/news.html index 0437fc13..4c66f6c5 100644 --- a/src/web/partials/admin/news.html +++ b/src/web/partials/admin/news.html @@ -1,7 +1,7 @@ {% extends "_base.html" %} {% block content %} - -Section Admin +

    Accueil des administrateurs de Traître-Lame.

    +Il n'y a pas de nouvelle à publier. {% end %} diff --git a/src/web/py_class/config.py b/src/web/py_class/config.py new file mode 100644 index 00000000..a7861a1b --- /dev/null +++ b/src/web/py_class/config.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import json +from sys import stderr + + +class Config(object): + """Contains general configuration.""" + + def __init__(self, parser): + self._db_config_path = parser.db_config_path + self._keys = {} + try: + with open(self._db_config_path, encoding='utf-8') as keys_file: + self._keys = json.load(keys_file) + except json.decoder.JSONDecodeError as exception: + print("ERROR: %s isn't formatted properly. \nDetails: %s" % (self._db_config_path, exception), + file=stderr) + except FileNotFoundError: + print("ERROR: file %s not exist. Please create it or read installation file." % self._db_config_path) + + def get(self, key): + """ + Get the value of the key. + + Use dot to generate a key to navigate in the dictionary. + Example: test1.test2.test3 + + :return: Return the value or None if cannot find the key. + """ + lst_key = key.split(".") + first_run = True + result = None + for a_key in lst_key: + if first_run: + result = self._keys.get(a_key) + first_run = False + elif type(result) is not dict: + print("Error to get key %s in file %s" % (key, self._db_config_path), file=stderr) + return + else: + result = result.get(a_key) + + return result + + def update(self, key, value, save=False): + """ + Update set of key with value. + :param key: string of value separate by dot + :param value: The value to insert. + :param save: Option to save on file + :return: + """ + # Search and replace value for key + lst_key = key.split(".") + result = None + for i in range(len(lst_key)): + a_key = lst_key[i] + if i == 0: + if a_key in self._keys: + result = self._keys.get(a_key) + else: + result = {} + self._keys[a_key] = result + elif type(result) is not dict: + print("Error to get key %s in file %s" % (key, self._db_config_path), file=stderr) + return + elif i == len(lst_key) - 1: + result[a_key] = value + else: + result = result.get(a_key) + + # Save on file + if save: + with open(self._db_config_path, mode="w", encoding='utf-8') as txt_file: + json.dump(self._keys, txt_file, indent=2) diff --git a/src/web/py_class/doc_generator/__init__.py b/src/web/py_class/doc_generator/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/web/py_class/doc_generator/doc_connector_gspread.py b/src/web/py_class/doc_generator/doc_connector_gspread.py new file mode 100644 index 00000000..5a03c758 --- /dev/null +++ b/src/web/py_class/doc_generator/doc_connector_gspread.py @@ -0,0 +1,492 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import sys +import gspread + + +class DocConnectorGSpread: + """ + DocConnectorGSpread manage doc generation parsing and Google spreadsheet functionality. + + Use DocGeneratorGSpread to get instance of DocGeneratorGSpread. + This is more secure for multi-thread execution + """ + + def __init__(self, gc, gc_doc, msg_share_invite): + self._gc = gc + self._g_file = gc_doc + + self._generated_doc = None + self._error = None + self._connector_is_valid = True + self._msg_share_invite = msg_share_invite + + self._info_sheet_name = ["manual", "lore"] + self._info_header = [ + "Title H1", "Title H1 HTML", "Description H1", "Bullet Description H1", "Second Bullet Description H1", + "Under Level Color H1", + "Title H2", "Title H2 HTML", "Description H2", "Bullet Description H2", "Second Bullet Description H2", + "Under Level Color H2", + "Title H3", "Title H3 HTML", "Description H3", "Bullet Description H3", "Second Bullet Description H3", + "Under Level Color H3", + "Title H4", "Title H4 HTML", "Description H4", "Bullet Description H4", "Second Bullet Description H4", + "Under Level Color H4", + "Title H5", "Title H5 HTML", "Description H5", "Bullet Description H5", "Second Bullet Description H5", + "Under Level Color H5" + ] + + def has_error(self): + """ + + :return: If instance of _DocGenerator contain error. + """ + return bool(self._error) + + def get_error(self, create_object=True, force_error=False): + """ + + :param create_object: if return dict with key "error" or return the message in string + :param force_error: if activate, generate an unknown error message. + :return: information about error. + """ + msg = self._error + if not msg and force_error: + msg = "Unknown error." + + if create_object: + return {"error": msg} + return msg + + def is_auth_valid(self): + """ + + :return: True if authentication is valid, else need to create a new instance of the document + """ + return self._connector_is_valid + + def get_permission_document(self): + """ + Get permission of all user. + :return: Formatted list of user with permission + """ + if not self._g_file: + return False + + all_info = self._g_file.list_permissions() + lst_info = [] + for perm in all_info: + info = {"name": perm.get('name'), "email": perm.get("emailAddress"), "role": perm.get("role"), + "type": perm.get("type")} + lst_info.append(info) + return lst_info + + def check_has_permission(self): + """ + Return bool if success or fail. Return dict when got error. + :return: + """ + # To know if permission error, get worksheets + try: + self._g_file.worksheets() + except gspread.v4.exceptions.APIError as e: + if e.response.status_code == 403: + return False + if e.response.status_code == 401: + self._connector_is_valid = False + # Need to reopen the file and ask user to refresh + self._error = "Refresh the page. Got error %s" % e + print(self._error, file=sys.stderr) + return False + + self._error = "Got error %s" % e + print(self._error, file=sys.stderr) + return False + return True + + def has_user_write_permission(self, email): + """ + Check if the user from email has writing permission on the document. + :param email: email in string + :return: success or fail + """ + if not self._g_file: + return False + + has_permission = self.check_has_permission() + if type(has_permission) is dict or not has_permission: + return False + + lst_permission = self._g_file.list_permissions() + for perm in lst_permission: + if perm.get("emailAddress") == email and perm.get("role") in ["owner", "writer"]: + return True + return False + + def share_document(self, email): + """ + Share a document and send email for invitation. + :param email: email of user. + :return: succeed or failed + """ + if not self._g_file: + return False + + msg = self._msg_share_invite + self._g_file.share(email, "user", "writer", notify=True, email_message=msg) + return True + + def generate_doc(self): + """ + Generate documentation. + :return: True when success, else False + """ + if not self._g_file: + self._error = "Remote file is not open." + print(self._error, file=sys.stderr) + return False + + # Create spreadsheet variable + sh = self._g_file + worksheet_list = sh.worksheets() + dct_doc = {} + + for doc_sheet_name in self._info_sheet_name: + # Find working sheet + for sheet in worksheet_list: + if sheet.title == doc_sheet_name: + manual_sheet = sheet + break + else: + lst_str_worksheet = [sheet.title for sheet in worksheet_list] + self._error = "Sheet '%s' not exist. Existing sheet: %s" % (doc_sheet_name, lst_str_worksheet) + print(self._error, file=sys.stderr) + return False + + # Validate the header + header_row = manual_sheet.row_values(1) + if self._info_header != header_row: + self._error = "Header of sheet %s is %s, and expected is %s" % ( + doc_sheet_name, header_row, self._info_header) + print(self._error, file=sys.stderr) + return False + + # Fetch all line + all_values = manual_sheet.get_all_values() + info = self._parse_doc(doc_sheet_name, all_values) + if info is None: + return False + + dct_doc[doc_sheet_name] = info + + self._generated_doc = dct_doc + return True + + def get_generated_doc(self): + """ + Property of generated_doc + :return: return False if the document is not generated, else return the dict + """ + if self._generated_doc: + return self._generated_doc + return False + + def _parse_doc(self, doc_sheet_name, all_values): + """ + Read each line of the doc from the spreadsheet and generate the structure. + :param doc_sheet_name: Sheet name + :param all_values: List of all row from the spreadsheet + :return: List of section to the doc or None when got error + """ + lst_doc_section = [] + line_number = 1 + first_section = None + second_section = None + third_section = None + status = False + + lst_value = all_values[1:] + if not lst_value: + # List is empty + status = True + + for row in lst_value: + line_number += 1 + is_first_section = any(row[0:5]) + is_second_section = any(row[6:11]) + is_third_section = any(row[12:17]) + is_fourth_section = any(row[18:23]) + is_fifth_section = any(row[24:29]) + + # Check error + sum_section = sum( + (is_first_section, is_second_section, is_third_section, is_fourth_section, is_fifth_section)) + + if sum_section == 0: + # Ignore empty line + continue + + if sum_section > 1: + self._error = "L.%s S.%s: Cannot contain more than 1 section at time. " \ + "H1: %s, H2: %s, H3: %s, H4: %s, H5: %s." % ( + line_number, doc_sheet_name, is_first_section, is_second_section, is_third_section, + is_fourth_section, + is_fifth_section) + print(self._error, file=sys.stderr) + return + + if is_first_section: + status = self._extract_section(0, row, line_number, doc_sheet_name, lst_doc_section) + + elif is_second_section: + second_section = None + third_section = None + first_section = lst_doc_section[-1] + + # Get section from last section + if "section" in first_section: + lst_section = first_section.get("section") + else: + lst_section = [] + first_section["section"] = lst_section + + status = self._extract_section(1, row, line_number, doc_sheet_name, lst_section) + + elif is_third_section: + third_section = None + if not first_section: + self._error = "L.%s S.%s: Missing section H1 to insert section H3." % (line_number, doc_sheet_name) + print(self._error, file=sys.stderr) + return + + lst_section = first_section.get("section") + if not lst_section: + self._error = "L.%s S.%s: Missing section H2 to insert section H3." % (line_number, doc_sheet_name) + print(self._error, file=sys.stderr) + return + + second_section = lst_section[-1] + + # Get section from last section + if "section" in second_section: + lst_section = second_section.get("section") + else: + lst_section = [] + second_section["section"] = lst_section + + status = self._extract_section(2, row, line_number, doc_sheet_name, lst_section) + + elif is_fourth_section: + if not first_section: + self._error = "L.%s S.%s: Missing section H1 to insert section H4." % (line_number, doc_sheet_name) + print(self._error, file=sys.stderr) + return + + if not second_section: + self._error = "L.%s S.%s: Missing section H2 to insert section H4." % (line_number, doc_sheet_name) + print(self._error, file=sys.stderr) + return + + # Create third_section + lst_section = second_section.get("section") + if not lst_section: + self._error = "L.%s S.%s: Missing section H3 to insert section H4." % (line_number, doc_sheet_name) + print(self._error, file=sys.stderr) + return + + third_section = lst_section[-1] + + # Get section from last section + if "section" in third_section: + lst_section = third_section.get("section") + else: + lst_section = [] + third_section["section"] = lst_section + + status = self._extract_section(3, row, line_number, doc_sheet_name, lst_section) + + elif is_fifth_section: + if not first_section: + self._error = "L.%s S.%s: Missing section H1 to insert section H5." % (line_number, doc_sheet_name) + print(self._error, file=sys.stderr) + return + + if not second_section: + self._error = "L.%s S.%s: Missing section H2 to insert section H5." % (line_number, doc_sheet_name) + print(self._error, file=sys.stderr) + return + + if not third_section: + self._error = "L.%s S.%s: Missing section H3 to insert section H5." % (line_number, doc_sheet_name) + print(self._error, file=sys.stderr) + return + + # Create third_section + lst_section = third_section.get("section") + if not lst_section: + self._error = "L.%s S.%s: Missing section H4 to insert section H5." % (line_number, doc_sheet_name) + print(self._error, file=sys.stderr) + return + + fourth_section = lst_section[-1] + + # Get section from last section + if "section" in fourth_section: + lst_section = fourth_section.get("section") + else: + lst_section = [] + fourth_section["section"] = lst_section + + status = self._extract_section(4, row, line_number, doc_sheet_name, lst_section) + + if not status: + return + + return lst_doc_section + + def _extract_section(self, level, row, line_number, doc_sheet_name, lst_section): + """ + Fill the recent section when read the spreadsheet row. + :param level: The level of section, 0 to 4. + :param row: The spreadsheet row. + :param line_number: The row's index of spreadsheet. + :param lst_section: list of parent section, to append new section. + :param doc_sheet_name: Sheet name + :return: True if success, else False + """ + if not (0 <= level <= 4): + self._error = "L.%s S.%s: Internal error, support only level 1 to 5 and got: %s" % ( + line_number, doc_sheet_name, level + 1) + print(self._error, file=sys.stderr) + return False + + nb_column = 6 + i_column = level * nb_column + + title = row[i_column] + title_html = row[i_column + 1] + description = row[i_column + 2] + bullet_description = row[i_column + 3] + second_bullet_description = row[i_column + 4] + under_level_color = row[i_column + 5] + + # Check error + if title_html and not title: + self._error = "L.%s S.%s: Need title when fill title html for H%s." % ( + line_number, doc_sheet_name, i_column) + print(self._error, file=sys.stderr) + return False + + if description and bullet_description: + self._error = "L.%s S.%s: Cannot have a description and a bullet description " \ + "on same line for H%s." % (line_number, doc_sheet_name, i_column) + print(self._error, file=sys.stderr) + return False + + if description and second_bullet_description: + self._error = "L.%s S.%s: Cannot have a description and a second bullet description " \ + "on same line for H%s." % (line_number, doc_sheet_name, i_column) + print(self._error, file=sys.stderr) + return False + + if bullet_description and second_bullet_description: + self._error = "L.%s S.%s: Cannot have a bullet description and a second bullet description " \ + "on same line for H%s." % (line_number, doc_sheet_name, i_column) + print(self._error, file=sys.stderr) + return False + + # Begin to fill this section + # If contain title, it's a new section. Else, take the last on the list. + if title: + # New section + section = {"title": title} + lst_section.append(section) + else: + section = lst_section[-1] + # Cannot continue if contain child section, because the data will be append to parent + # this will cause a view error + if "section" in section: + self._error = "L.%s S.%s: Cannot add information on this section when contain sub header " \ + "on same line for H%s." % (line_number, doc_sheet_name, i_column) + print(self._error, file=sys.stderr) + return False + + # Special title, contain html to improve view + if title_html: + if "title_html" in section: + self._error = "L.%s S.%s: Cannot manage many title_html for H%s." % ( + line_number, doc_sheet_name, i_column) + print(self._error, file=sys.stderr) + return False + section["title_html"] = title_html + + # Description can be append for the same section + if description: + # Check if new description + if "description" not in section: + lst_description = [] + section["description"] = lst_description + else: + lst_description = section.get("description") + + lst_description.append(description) + + # Bullet description can be append for the same description + if bullet_description: + # Can create a bullet description if no paragraph description + if "description" not in section: + lst_description = [] + section["description"] = lst_description + else: + lst_description = section.get("description") + + # Check if last item is a bullet description + if not (lst_description and type(lst_description[-1]) is list): + lst_bullet_description = [] + lst_description.append(lst_bullet_description) + else: + lst_bullet_description = lst_description[-1] + + # Add the bullet_description + lst_bullet_description.append(bullet_description) + + if second_bullet_description: + if "description" not in section: + self._error = "L.%s S.%s: Cannot create second-bullet description missing description " \ + "for H%s." % (line_number, doc_sheet_name, i_column) + print(self._error, file=sys.stderr) + return False + lst_description = section.get("description") + + # Check if last item is a bullet description + if lst_description: + lst_bullet_description = lst_description[-1] + else: + self._error = "L.%s S.%s: Cannot create second-bullet description when not precede to bullet " \ + "description for H%s." % (line_number, doc_sheet_name, i_column) + print(self._error, file=sys.stderr) + return False + + # Create second-bullet description list + if not lst_bullet_description: + lst_second_bullet_description = [] + lst_bullet_description.append(lst_second_bullet_description) + else: + lst_second_bullet_description = lst_bullet_description[-1] + # Validate last bullet description is a sub-bullet + if type(lst_second_bullet_description) is not lst_second_bullet_description: + lst_second_bullet_description = [] + lst_bullet_description.append(lst_second_bullet_description) + + # First second bullet description insertion + lst_second_bullet_description.append(second_bullet_description) + + if under_level_color: + # Add color for header + if "under_level_color" in section: + self._error = "L.%s S.%s: Already contain value of 'Under Level Color'for H%s." % ( + line_number, doc_sheet_name, i_column) + print(self._error, file=sys.stderr) + return False + section["under_level_color"] = under_level_color + + return True diff --git a/src/web/py_class/doc_generator/doc_generator_gspread.py b/src/web/py_class/doc_generator/doc_generator_gspread.py new file mode 100644 index 00000000..d6c5d3c1 --- /dev/null +++ b/src/web/py_class/doc_generator/doc_generator_gspread.py @@ -0,0 +1,240 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import gspread +from oauth2client.service_account import ServiceAccountCredentials +import sys +from .doc_connector_gspread import DocConnectorGSpread + + +class DocGeneratorGSpread(object): + """ + Generate documentation with google drive spreadsheet. + + Manage file configuration and Google authentication. + + This class generate instance of DocConnectorGSpread. + """ + + def __init__(self, parser): + self._parser = parser + + self._gc = None + self._gc_email = "" + self._url = "" + self._google_file = None + self._doc_connector = None + + self._error = None + + self._msg_invite_share = self._parser.config.get("msg_email_share_document") + + def get_instance(self): + """ + Return DocConnectorGSpread instance. + :return: instance of DocConnectorGSpread + """ + self._error = None + + # Return doc connector if is valid, else generate a new one + if self._doc_connector: + # Need to check if need to reload document + self._doc_connector.check_has_permission() + if self._doc_connector.is_auth_valid(): + return self._doc_connector + + status = self.connect(force_connect=True) + if not status: + return + + status = self.update_url(ignore_error=True) + if not status: + return + + obj = DocConnectorGSpread(self._gc, self._google_file, self._msg_invite_share) + self._doc_connector = obj + return obj + + def update_url(self, url=None, save=False, ignore_error=False): + """ + Validate the url, open a new document and can save to configuration file. + :param url: New URL to update. + :param save: If True, save the url to configuration file if valid. + :param ignore_error: Can update url without generate error. + :return: True if success else False + """ + has_open_file = False + status = False + + if url: + status = self._open_file_by_url(url) + if status: + has_open_file = True + self._url = url + self._doc_connector = None + elif not ignore_error: + return + else: + info = self._fetch_config() + if info: + status = self._open_file_by_url(info) + self._url = info + has_open_file = True + self._doc_connector = None + elif not ignore_error: + self._error = "Cannot open file from empty config." + print(self._error, file=sys.stderr) + return False + + if not has_open_file and not ignore_error: + self._error = "Missing url to open the remote file." + print(self._error, file=sys.stderr) + return False + + if has_open_file and save: + # Open config file + self._parser.config.update("google_spreadsheet.file_url", url, save=True) + + return status + + def is_auth(self): + """ + + :return: If auth with oauth2 + """ + return bool(self._gc) + + def is_file_open(self): + """ + + :return: If auth with oauth2 + """ + return bool(self._google_file) + + def has_error(self): + """ + + :return: If instance of DocGeneratorGSpread contain error. + """ + return bool(self._error) + + def get_error(self, create_object=True, force_error=False): + """ + + :param create_object: if return dict with key "error" or return the message in string + :param force_error: if activate, generate an unknown error message. + :return: information about error. + """ + msg = self._error + if not msg and force_error: + msg = "Unknown error." + + if create_object: + return {"error": msg} + return msg + + def get_url(self): + """ + Get the url of the remote document. + :return: String of URL + """ + return self._url + + def get_email_service(self): + """ + Get email to communicate with google service. + :return: string of email + """ + return self._gc_email + + def connect(self, force_connect=False): + """ + Do authentication with oauth2 of Google + :return: Success if True else Fail + """ + if self._gc is None or force_connect: + scope = ['https://spreadsheets.google.com/feeds', 'https://www.googleapis.com/auth/drive'] + try: + # Get credentials about oauth2 Service + credentials = ServiceAccountCredentials.from_json_keyfile_name(self._parser.db_google_API_path, scope) + except FileNotFoundError: + self._error = "Missing file %s to configure Google Drive Spreadsheets." % \ + self._parser.db_google_API_path + print(self._error, file=sys.stderr) + return False + + # Send http request to get authorization + self._gc = gspread.authorize(credentials) + if not self._gc: + self._error = "Cannot connect to Google API Drive." + print(self._error, file=sys.stderr) + return False + + # Store useful information about account + self._gc_email = credentials.service_account_email + + # Reinitialize error + self._error = None + return True + + def _open_file_by_name(self, name): + """ + Open remote file by name. + :param name: type String name to open + :return: bool True if success else False if fail + """ + try: + google_file = self._gc.open(name) + except gspread.SpreadsheetNotFound: + self._error = "Cannot open google file from name : %s" % name + print(self._error, file=sys.stderr) + return False + + self._google_file = google_file + return True + + def _open_file_by_key(self, key): + """ + Open remote file by key. + :param key: type String key to open + :return: bool True if success else False if fail + """ + try: + google_file = self._gc.open_by_key(key) + except gspread.SpreadsheetNotFound: + self._error = "Cannot open google file from key : %s" % key + print(self._error, file=sys.stderr) + return False + + self._google_file = google_file + return True + + def _open_file_by_url(self, url): + """ + Open remote file by url. + :param url: type String url to open + :return: bool True if success else False if fail + """ + try: + google_file = self._gc.open_by_url(url) + except gspread.SpreadsheetNotFound: + self._error = "Cannot open google file from url : %s" % url + print(self._error, file=sys.stderr) + return False + except gspread.NoValidUrlKeyFound: + self._error = "Cannot open google file from invalid url : %s" % url + print(self._error, file=sys.stderr) + return False + + self._google_file = google_file + return True + + def _fetch_config(self): + """ + Search file information to fetch from remote in config file. + Enable type of way to access to the remote file. + :return: String information if contain data, else None + """ + # Get by url + info = self._parser.config.get("google_spreadsheet.file_url") + return info diff --git a/src/web/py_class/lore.py b/src/web/py_class/lore.py index 94bc2944..e6ccc899 100644 --- a/src/web/py_class/lore.py +++ b/src/web/py_class/lore.py @@ -8,9 +8,19 @@ class Lore(object): """Contain knowledge who not necessary for the play.""" def __init__(self, parser): - self.str_lore = "" - with open(parser.db_lore_path, encoding='utf-8') as lore_file: - self.str_lore = json.load(lore_file) + self._str_lore = "" + self._lore_path = parser.db_lore_path + with open(self._lore_path, encoding='utf-8') as lore_file: + self._str_lore = json.load(lore_file) + + def update(self, dct_lore, save=False): + # Transform the object in json string + self._str_lore = json.dumps(dct_lore) + + # Save on file + if save: + with open(self._lore_path, mode="w", encoding='utf-8') as lore_file: + json.dump(dct_lore, lore_file, indent=2) def get_str_all(self): - return self.str_lore + return self._str_lore diff --git a/src/web/py_class/manual.py b/src/web/py_class/manual.py index 627191c8..fbe1d322 100644 --- a/src/web/py_class/manual.py +++ b/src/web/py_class/manual.py @@ -8,9 +8,19 @@ class Manual(object): """Contain all gaming rule.""" def __init__(self, parser): - self.str_manual = "" - with open(parser.db_manual_path, encoding='utf-8') as manual_file: - self.str_manual = json.load(manual_file) + self._str_manual = "" + self._manual_path = parser.db_manual_path + with open(self._manual_path, encoding='utf-8') as manual_file: + self._str_manual = json.load(manual_file) + + def update(self, dct_manual, save=False): + # Transform the object in json string + self._str_manual = json.dumps(dct_manual) + + # Save on file + if save: + with open(self._manual_path, mode="w", encoding='utf-8') as manual_file: + json.dump(dct_manual, manual_file, indent=2) def get_str_all(self): - return self.str_manual + return self._str_manual diff --git a/src/web/resources/js/tl_module/editor_ctrl/editor_ctrl.js b/src/web/resources/js/tl_module/editor_ctrl/editor_ctrl.js new file mode 100644 index 00000000..4a11e98b --- /dev/null +++ b/src/web/resources/js/tl_module/editor_ctrl/editor_ctrl.js @@ -0,0 +1,230 @@ +// Formulaire de Traitre-Lame +"use strict"; + +characterApp.controller("editor_ctrl", ["$scope", "$q", "$http", "$window", /*"$timeout",*/ function ($scope, $q, $http, $window) { + $scope.init_model = function (e) { + $scope.model_editor = { + is_ctrl_ready: false, + + modulestate: { + has_error: false, + error: "" + }, + + info: { + file_url: "", + is_auth: false, + user_has_writer_perm: false, + has_access_perm: false, + email_google_service: "", + can_generate: false + }, + + is_updating_file_url: false, + update_file_url: { + status: { + enabled: false, + is_error: false, + text: "" + }, + url: "" + }, + + is_generating_doc: false, + generated_doc: { + status: { + enabled: false, + is_error: false, + text: "" + } + }, + + is_sharing_doc: false, + sharing_doc: { + status: { + enabled: false, + is_error: false, + text: "" + } + } + + }; + }; + $scope.init_model(); + + // Get editor info + $scope.update_editor = function (e) { + $scope.init_model(); + + $http({ + method: "get", + url: "/cmd/editor/get_info", + headers: {"Content-Type": "application/json; charset=UTF-8"}, + timeout: 5000 + }).then(function (response/*, status, headers, config*/) { + $scope.model_editor.info = response.data; + $scope.model_editor.is_ctrl_ready = true; + + if ("error" in $scope.model_editor.info) { + $scope.model_editor.modulestate.has_error = true; + $scope.model_editor.modulestate.error = $scope.model_editor.info.error; + } + }, function errorCallback(response) { + console.error(response); + + $scope.model_editor.generated_doc.status.enabled = true; + if (response.status == -1) { + // Timeout + $scope.model_editor.generated_doc.status.is_error = true; + $scope.model_editor.generated_doc.status.text = "Timeout request."; + } else { + // Error from server + $scope.model_editor.generated_doc.status.is_error = true; + $scope.model_editor.generated_doc.status.text = "Error from server : " + response.status; + } + }); + + }; + $scope.update_editor(); + + // Send request to receive writer permission + $scope.send_writingpermission = function (e) { + if ($scope.model_editor.is_sharing_doc) { + return; + } + $scope.model_editor.sharing_doc.status.enabled = false; + $scope.model_editor.is_sharing_doc = true; + + $http({ + method: "post", + url: "/cmd/editor/add_generator_share", + headers: {"Content-Type": "application/json; charset=UTF-8"}, + timeout: 5000 + }).then(function (response/*, status, headers, config*/) { + console.info(response); + $scope.model_editor.info.user_has_writer_perm = true; + $scope.model_editor.is_sharing_doc = false; + + $scope.model_editor.sharing_doc.status.enabled = true; + if ("error" in response.data) { + $scope.model_editor.sharing_doc.status.is_error = true; + $scope.model_editor.sharing_doc.status.text = response.data.error; + } else if ("status" in response.data) { + $scope.model_editor.sharing_doc.status.is_error = false; + $scope.model_editor.sharing_doc.status.text = response.data.status; + // Add password in info + $scope.model_editor.info.password = true; + } else { + $scope.model_editor.sharing_doc.status.is_error = true; + $scope.model_editor.sharing_doc.status.text = "Unknown error"; + } + }, function errorCallback(response) { + console.error(response); + $scope.model_editor.is_sharing_doc = false; + + $scope.model_editor.sharing_doc.status.enabled = true; + if (response.status == -1) { + // Timeout + $scope.model_editor.sharing_doc.status.is_error = true; + $scope.model_editor.sharing_doc.status.text = "Timeout request."; + } else { + // Error from server + $scope.model_editor.sharing_doc.status.is_error = true; + $scope.model_editor.sharing_doc.status.text = "Error from server : " + response.status; + } + }); + }; + + // Send request to generate documentation + $scope.generate_doc = function (e) { + if ($scope.model_editor.is_generating_doc) { + return; + } + $scope.model_editor.generated_doc.status.enabled = false; + $scope.model_editor.is_generating_doc = true; + $http({ + method: "post", + url: "/cmd/editor/generate_and_save", + headers: {"Content-Type": "application/json; charset=UTF-8"}, + timeout: 10000 + }).then(function (response/*, status, headers, config*/) { + console.info(response); + $scope.model_editor.is_generating_doc = false; + + $scope.model_editor.generated_doc.status.enabled = true; + if ("error" in response.data) { + $scope.model_editor.generated_doc.status.is_error = true; + $scope.model_editor.generated_doc.status.text = response.data.error; + } else if ("status" in response.data) { + $scope.model_editor.generated_doc.status.is_error = false; + $scope.model_editor.generated_doc.status.text = response.data.status; + // Add password in info + $scope.model_editor.info.password = true; + } else { + $scope.model_editor.generated_doc.status.is_error = true; + $scope.model_editor.generated_doc.status.text = "Unknown error"; + } + }, function errorCallback(response) { + console.error(response); + $scope.model_editor.is_generating_doc = false; + + $scope.model_editor.generated_doc.status.enabled = true; + if (response.status == -1) { + // Timeout + $scope.model_editor.generated_doc.status.is_error = true; + $scope.model_editor.generated_doc.status.text = "Timeout request."; + } else { + // Error from server + $scope.model_editor.generated_doc.status.is_error = true; + $scope.model_editor.generated_doc.status.text = "Error from server : " + response.status; + } + }); + }; + + // Send request to update file url + $scope.update_file_url = function (e) { + if ($scope.model_editor.is_updating_file_url) { + return; + } + $scope.model_editor.is_updating_file_url = true; + var data = {"file_url": $scope.model_editor.update_file_url.url} + $http({ + method: "post", + url: "/cmd/editor/update_file_url", + data: data, + headers: {"Content-Type": "application/json; charset=UTF-8"}, + timeout: 5000 + }).then(function (response/*, status, headers, config*/) { + console.info(response); + $scope.model_editor.is_updating_file_url = false; + + $scope.update_editor(); + $scope.model_editor.update_file_url.status.enabled = true; + if ("error" in response.data) { + $scope.model_editor.update_file_url.status.is_error = true; + $scope.model_editor.update_file_url.status.text = response.data.error; + } else if ("status" in response.data) { + $scope.model_editor.update_file_url.status.is_error = false; + $scope.model_editor.update_file_url.status.text = response.data.status; + } else { + $scope.model_editor.update_file_url.status.is_error = true; + $scope.model_editor.update_file_url.status.text = "Unknown error"; + } + }, function errorCallback(response) { + console.error(response); + $scope.model_editor.is_updating_file_url = false; + + $scope.model_editor.update_file_url.status.enabled = true; + if (response.status == -1) { + // Timeout + $scope.model_editor.update_file_url.status.is_error = true; + $scope.model_editor.update_file_url.status.text = "Timeout request."; + } else { + // Error from server + $scope.model_editor.update_file_url.status.is_error = true; + $scope.model_editor.update_file_url.status.text = "Error from server : " + response.status; + } + }); + }; + +}]); diff --git a/src/web/resources/js/tl_module/profile_ctrl/profile_ctrl.js b/src/web/resources/js/tl_module/profile_ctrl/profile_ctrl.js index b227f953..0b92de69 100644 --- a/src/web/resources/js/tl_module/profile_ctrl/profile_ctrl.js +++ b/src/web/resources/js/tl_module/profile_ctrl/profile_ctrl.js @@ -93,7 +93,7 @@ characterApp.controller("profile_ctrl", ["$scope", "$q", "$http", "$window", /*" // Timeout $scope.model_profile.update_password.loading = false; $scope.model_profile.status_password.is_error = true; - $scope.model_profile.status_password.text = "Timeout resquest."; + $scope.model_profile.status_password.text = "Timeout request."; } else { // Error from server $scope.model_profile.update_password.loading = false; @@ -160,7 +160,7 @@ characterApp.controller("profile_ctrl", ["$scope", "$q", "$http", "$window", /*" // Timeout $scope.model_profile.update_password.loading = false; $scope.model_profile.status_password.is_error = true; - $scope.model_profile.status_password.text = "Timeout resquest."; + $scope.model_profile.status_password.text = "Timeout request."; } else { // Error from server $scope.model_profile.update_password.loading = false; diff --git a/src/web/resources/js/tool.js b/src/web/resources/js/tool.js index fb520d14..1576660c 100644 --- a/src/web/resources/js/tool.js +++ b/src/web/resources/js/tool.js @@ -75,3 +75,7 @@ function hashSha256(secret, salt) { shaObj.update(secret); return shaObj.getHash('HEX'); } + +function isObjEmpty(obj) { + return Object.keys(obj).length === 0; +} diff --git a/src/web/web.py b/src/web/web.py index 69e78196..c2312753 100644 --- a/src/web/web.py +++ b/src/web/web.py @@ -16,6 +16,7 @@ from py_class.db import DB from py_class.manual import Manual from py_class.lore import Lore +from py_class.doc_generator.doc_generator_gspread import DocGeneratorGSpread from py_class.auth_keys import AuthKeys WEB_ROOT_DIR = os.path.dirname(os.path.realpath(__file__)) @@ -78,10 +79,12 @@ def main(parse_arg): "db": DB(parse_arg), "manual": Manual(parse_arg), "lore": Lore(parse_arg), + "doc_generator_gspread": DocGeneratorGSpread(parse_arg), "disable_character": parse_arg.disable_character, "disable_user_character": parse_arg.disable_user_character, "disable_admin": parse_arg.disable_admin, "disable_login": parse_arg.disable_login, + "config": parse_arg.config, "hide_menu_login": parse_arg.hide_menu_login, "disable_custom_css": parse_arg.disable_custom_css, "url": url, @@ -104,24 +107,29 @@ def main(parse_arg): # To create parameters: /(?P[^\/]+)/?(?P[^\/]+)/? # Add ? after ) to make a parameter optional - # pages + # Web page tornado.web.url(r"/?", handlers.IndexHandler, name='index', kwargs=settings), tornado.web.url(r"/login/?", handlers.LoginHandler, name='login', kwargs=settings), tornado.web.url(r"/logout/?", handlers.LogoutHandler, name='logout', kwargs=settings), tornado.web.url(r"/admin/?", handlers.AdminHandler, name='admin', kwargs=settings), - tornado.web.url(r"/admin/character?", handlers.AdminCharacterHandler, name='admin character', kwargs=settings), tornado.web.url(r"/profile/?(?P[^\/]+)?/?", handlers.ProfileHandler, name='profile', kwargs=settings), tornado.web.url(r"/character/?", handlers.CharacterHandler, name='character', kwargs=settings), tornado.web.url(r"/manual/?", handlers.ManualPageHandler, name='manual', kwargs=settings), tornado.web.url(r"/lore/?", handlers.LorePageHandler, name='lore', kwargs=settings), - # command + # Admin web page + tornado.web.url(r"/admin/character?", handlers.AdminCharacterHandler, name='admin character', kwargs=settings), + tornado.web.url(r"/admin/editor?", handlers.AdminEditorHandler, name='admin editor', kwargs=settings), + + # Command tornado.web.url(r"/cmd/character_view/?", handlers.CharacterViewHandler, name='character_view', kwargs=settings), tornado.web.url(r"/cmd/manual/?", handlers.ManualHandler, name='cmd_manual', kwargs=settings), tornado.web.url(r"/cmd/lore/?", handlers.LoreHandler, name='cmd_lore', kwargs=settings), tornado.web.url(r"/cmd/stat/total_season_pass/?", handlers.StatSeasonPass, name='cmd_stat_total_season_pass', kwargs=settings), + + # Profile tornado.web.url(r"/cmd/profile/update_password/?", handlers.ProfileCmdUpdatePasswordHandler, name='cmd_profile_update_password', kwargs=settings), tornado.web.url(r"/cmd/profile/add_new_password/?", handlers.ProfileCmdAddNewPasswordHandler, @@ -129,7 +137,17 @@ def main(parse_arg): tornado.web.url(r"/cmd/profile/get_info/?", handlers.ProfileCmdInfoHandler, name='cmd_profile_get_info', kwargs=settings), - # auto ssl + # Editor + tornado.web.url(r"/cmd/editor/get_info/?", handlers.EditorCmdInfoHandler, + name='cmd_editor_get_info', kwargs=settings), + tornado.web.url(r"/cmd/editor/add_generator_share/?", handlers.EditorCmdAddGeneratorShareHandler, + name='cmd_editor_add_generator_share', kwargs=settings), + tornado.web.url(r"/cmd/editor/generate_and_save/?", handlers.EditorCmdGenerateAndSaveHandler, + name='cmd_editor_generate_and_save', kwargs=settings), + tornado.web.url(r"/cmd/editor/update_file_url/?", handlers.EditorCmdUpdateFileUrlHandler, + name='cmd_editor_update_file_url', kwargs=settings), + + # Auto ssl tornado.web.url(r"/.well-known/acme-challenge.*", handlers.AutoSSLHandler, name="auto_ssl") ] From a5a2e2c6742584f992e7a90a03205be594f2b6f8 Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Sun, 8 Apr 2018 00:26:58 -0400 Subject: [PATCH 3/3] Base html: update fontawesome path from bower update --- src/web/partials/_base.html | 2 +- src/web/partials/admin/_base.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/web/partials/_base.html b/src/web/partials/_base.html index a0913cc4..1c3ae2c4 100644 --- a/src/web/partials/_base.html +++ b/src/web/partials/_base.html @@ -35,7 +35,7 @@ {% else %} - + {% end %} diff --git a/src/web/partials/admin/_base.html b/src/web/partials/admin/_base.html index c60d6532..e739b39e 100644 --- a/src/web/partials/admin/_base.html +++ b/src/web/partials/admin/_base.html @@ -35,7 +35,7 @@ {% else %} - + {% end %}