diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..5309500 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "paramecio/modules/admin/media/js/jsutils"] + path = paramecio/modules/admin/media/js/jsutils + url = git@git.cuchulu.com:paramecio/jsutils.git diff --git a/LICENSE.txt b/LICENSE.txt index 10926e8..be3f7b2 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,23 +1,21 @@ - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 - Copyright (C) 2007 Free Software Foundation, Inc. + Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble - The GNU General Public License is a free, copyleft license for -software and other kinds of works. + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to +our General Public Licenses are intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. You can apply it to -your programs, too. +software for all its users. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you @@ -26,44 +24,34 @@ them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. - To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. The precise terms and conditions for copying, distribution and modification follow. @@ -72,7 +60,7 @@ modification follow. 0. Definitions. - "This License" refers to version 3 of the GNU General Public License. + "This License" refers to version 3 of the GNU Affero General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. @@ -549,35 +537,45 @@ to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. - 13. Use with the GNU Affero General Public License. + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single +under version 3 of the GNU General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of -the GNU General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU General +Program specifies that a certain numbered version of the GNU Affero General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published +GNU Affero General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's +versions of the GNU Affero General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. @@ -635,41 +633,29 @@ the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by + it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. + GNU Affero General Public License for more details. - You should have received a copy of the GNU General Public License - along with this program. If not, see . + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. - If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - Copyright (C) - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, your program's commands -might be different; for a GUI interface, you would use an "about box". + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU GPL, see -. - - The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -. - +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/MANIFEST.in b/MANIFEST.in index 22fb9d9..8497f71 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,8 +2,7 @@ include paramecio/*.py include README.md include LICENSE include REQUIREMENTS -recursive-include paramecio/citoplasma * +recursive-include paramecio/libraries * recursive-include paramecio/frontend * -recursive-include paramecio/cromosoma * recursive-include paramecio/settings * -recursive-include paramecio/modules * \ No newline at end of file +recursive-include paramecio/modules * diff --git a/README.md b/README.md index 3af5b9d..7d67b25 100644 --- a/README.md +++ b/README.md @@ -12,22 +12,19 @@ Also, you need the next software installed in your os: ### Python 3.4 or later. -Paramecio should work fine in 3.3 but is tested in 3.4 and 3.5 python 3 versions. +Paramecio should work fine since python 3.6 but is tested in 3.12 and 3.13 python 3 versions. In Debian and Ubuntu you can install Python 3 using the next command: `apt-get install python3`. -In Fedora and other Red Hat derived distros you can use `yum install python3`. In RedHat/Centos 6 or 7 you need install [Ius Repos](https://ius.io/GettingStarted/) for get sane versions of python3. In new Fedora versions you can use dnf, a drop-in replacement for yum: `dnf install python3`. +In Fedora and other Red Hat derived distros you can use `dnf install python3`. ### MySQL or MariaDB database servers. -MariaDB 10.0 and later are recommended. +MariaDB 10.6 and later are recommended. In Debian and Ubuntu you can install MariaDB using the next command: `apt-get install mariadb-server`. -In Fedora and other Red Hat derived distros you can use `yum install mariadb-server`. -In RedHat/Centos 6 probably you need install adittional repositories for get latest versions of mariadb, but with MySQL 5.5, Paramecio should work fine. - -When you will install the mysql server, you should create a new user and database for Paramecio. +In Fedora and other Red Hat derived distros you can use `dnf install mariadb-server`. ### Pip @@ -35,7 +32,7 @@ Pip is the package manager of python. You can use the package manager of your os In Debian and Ubuntu you can install pip using the next command: `apt-get install python3-pip`. -In Fedora and other Red Hat derived distros you can use `yum install python3-pip`. Of course, the command can change if you use Centos 6/7 with **Ius repos**. +In Fedora and other Red Hat derived distros you can use `dnf install python3-pip`. ### Git @@ -43,7 +40,7 @@ In Fedora and other Red Hat derived distros you can use `yum install python3-pip In Debian and Ubuntu you can install git using the next command: `apt-get install git`. -In Fedora and other Red Hat derived distros you can use `yum install git` or `dnf install git` in last fedora versions. +In Fedora and other Red Hat derived distros you can use `dnf install git`. ## Install Paramecio Framework @@ -59,8 +56,6 @@ This command will install in your server paramecio framework with its dependenci When Paramecio finish the installing, you can create your first paramecio site with `paramecio` command. -> If you install passlib and bcrypt python modules, your paramecio install will use bcrypt algorithm for crypt system passwords. If not, default system implementation crypt algorithm (normally the more strong algorithm available in the system) will be used. - ### Tipical errors If you get an error in your installation of dependencies how MarkupSafe or SqlAlchemy, please install gcc or install manually mako and sqlalchemy with your system package manager. For example, for debian and ubuntu: diff --git a/paramecio/index.py b/paramecio/app.py similarity index 83% rename from paramecio/index.py rename to paramecio/app.py index f57610a..9792fdf 100644 --- a/paramecio/index.py +++ b/paramecio/app.py @@ -4,18 +4,18 @@ from bottle import route, get, post, run, default_app, abort, request, response, from settings import config #from beaker.middleware import SessionMiddleware from mimetypes import guess_type -from paramecio.cromosoma.webmodel import WebModel -from paramecio.citoplasma.datetime import set_timezone -from itsdangerous import JSONWebSignatureSerializer -from paramecio.citoplasma.keyutils import create_key_encrypt, create_key_encrypt_256, create_key +from paramecio.libraries.db.webmodel import WebModel +from paramecio.libraries.datetime import set_timezone +#from itsdangerous import JSONWebSignatureSerializer +from paramecio.libraries.keyutils import create_key_encrypt, create_key_encrypt_256, create_key from paramecio.wsgiapp import app -#from paramecio.citoplasma.sessions import after_session +#from paramecio.libraries.sessions import after_session modules_pass=False #app.reset() -#from paramecio.citoplasma.sessions import generate_session +#from paramecio.libraries.sessions import generate_session #Prepare links for static. #WARNING: only use this feature in development, not in production. @@ -43,6 +43,10 @@ if hasattr(config, 'error_reporting'): error_reporting=config.error_reporting def prepare_app(): + + # In mod_wsgi, make strange thing with reloading. + + app.reset() def print_memory(): print(resource.getrusage(resource.RUSAGE_SELF).ru_maxrss) @@ -52,7 +56,7 @@ def prepare_app(): module_loaded=None #Getting paths for loaded modules for use in media load files - + """ for module in config.modules: #controller_path=sys.modules[module] @@ -62,6 +66,7 @@ def prepare_app(): base_module=module.split('.')[-1] arr_module_path[base_module]=controller_base + """ #app.add_hook('before_request', print_memory) """ @@ -80,11 +85,11 @@ def prepare_app(): app_mounts={} for key_app, added_app in config.apps.items(): - - controller_path=import_module(added_app[0]) - - controller_base=os.path.dirname(controller_path.__file__) + controller_path=import_module(added_app[0]) + + controller_base=os.path.dirname(controller_path.__file__) + print(controller_base) dir_controllers=os.listdir(controller_base) for controller in dir_controllers: @@ -97,23 +102,25 @@ def prepare_app(): a=import_module(module_app) - app_name=getattr(a, added_app[1]) + if added_app[1]!='': + + app_name=getattr(a, added_app[1]) - #app.register_blueprint(app_name, url_prefix=added_app[2]) - - #app.mount(added_app[2], app_name) - app_mounts[added_app[2]]=app_name + app_mounts[added_app[2]]=app_name arr_module_path[key_app]=os.path.dirname(sys.modules[module_app].__file__) for k_app,v_app in app_mounts.items(): - - app.mount(k_app, v_app) + #print(k_app) + if k_app!='/': + app.mount(k_app, v_app) + elif k_app!='': + app.merge(v_app) set_timezone() if error_reporting: - from paramecio.citoplasma.error_reporting import ErrorReportingPlugin + from paramecio.libraries.error_reporting import ErrorReportingPlugin app.install(ErrorReportingPlugin()) @@ -144,20 +151,21 @@ application=app try: - from settings import modules - + #from settings import modules prepare_app() except: - + + app.reset() + @app.route('/') def catch_errors(all='/'): try: from pathlib import Path - from settings import modules + #from settings import modules import time prepare_app() - p=Path('index.py') + p=Path('app.py') p.touch() time.sleep(1) except: diff --git a/paramecio/citoplasma/i18n.py b/paramecio/citoplasma/i18n.py deleted file mode 100644 index a205934..0000000 --- a/paramecio/citoplasma/i18n.py +++ /dev/null @@ -1,95 +0,0 @@ -#!/usr/bin/env python3 - -from importlib import import_module -from paramecio.citoplasma.sessions import get_session -import json -from bottle import request - -yes_session=False - -i18n_module={} - -def load_lang(*args): - - for module in args: - - lang_path=module[0]+'.i18n.'+module[1] - - try: - - i18n_module[lang_path]=import_module(lang_path) - - pass - - except: - pass - - # here load the language - - -class I18n: - - default_lang='en-US' - - dict_i18n=['en-US', 'es-ES'] - - l={} - - #@staticmethod - #def set_lang(code_lang): - # if default_lang - - - @staticmethod - def get_default_lang(): - - lang=I18n.default_lang - - s=get_session() - - lang=s.get('lang', lang) - - return lang - - @staticmethod - def lang(module, symbol, text_default, lang=None): - - if not lang: - lang=I18n.get_default_lang() - - I18n.l[lang]=I18n.l.get(lang, {}) - - I18n.l[lang][module]=I18n.l[lang].get(module, {}) - - I18n.l[lang][module][symbol]=I18n.l[lang][module].get(symbol, text_default) - - return I18n.l[lang][module][symbol] - - @staticmethod - def extract_value(value): - - value=json.loads(value) - - lang=I18n.get_default_lang() - - if value[lang]!='': - - return value[lang] - - return value[I18n.default_lang] - - @staticmethod - def get_browser_lang(): - - return request.headers.get('Accept-Language', 'en-US') - - @staticmethod - def lang_json(module, symbol, text_default): - - arr_final={} - - for l in I18n.dict_i18n: - arr_final[l]=I18n.lang(module, symbol, text_default, l) - - return json.dumps(arr_final) - diff --git a/paramecio/citoplasma/keyutils.py b/paramecio/citoplasma/keyutils.py deleted file mode 100644 index 4cb4b8a..0000000 --- a/paramecio/citoplasma/keyutils.py +++ /dev/null @@ -1,19 +0,0 @@ -from hashlib import sha512, sha256 -from base64 import b64encode -from os import urandom - -# Functions for create random strings usando urandom - -def create_key_encrypt(n=10): - - return sha512(urandom(n)).hexdigest() - -def create_key_encrypt_256(n=10): - - return sha256(urandom(n)).hexdigest() - -def create_key(n=10): - - rand_bytes=urandom(n) - - return b64encode(rand_bytes).decode('utf-8')[0:-2] diff --git a/paramecio/console.py b/paramecio/console.py index 2e949a7..1f45d49 100644 --- a/paramecio/console.py +++ b/paramecio/console.py @@ -7,8 +7,8 @@ import getpass import re from pathlib import Path from base64 import b64encode -from paramecio.cromosoma.webmodel import WebModel -from paramecio.modules.admin.models.admin import UserAdmin +from paramecio.libraries.db.webmodel import WebModel +from paramecio.modules.admin2.models.admin import UserAdmin2, LoginTries2 from subprocess import call from urllib.parse import urlparse @@ -52,7 +52,7 @@ def start(): print('Error: cannot create the directory. Check if exists and if you have permissions') exit() - # Create folder settings and copy index.py, admin.py + # Create folder settings and copy app.py, admin.py path_settings=args.path+'/settings' @@ -74,11 +74,11 @@ def start(): try: - shutil.copy(workdir+'/frontend/index.py', args.path+'/index.py') + shutil.copy(workdir+'/frontend/app.py', args.path+'/app.py') except: - print('Error: cannot copy the file index.py. Check if exists and if you have permissions for this task') + print('Error: cannot copy the file app.py. Check if exists and if you have permissions for this task') try: @@ -112,7 +112,7 @@ def start(): print('Error: cannot copy the file create_module.py. Check if exists and if you have permissions for this task') - + """ try: shutil.copy(workdir+'/settings/modules.py', path_settings+'/modules.py') @@ -120,6 +120,7 @@ def start(): except: print('Error: cannot copy the file modules.py. Check if exists and if you have permissions for this task') + """ if args.symlink==True: try: @@ -221,7 +222,9 @@ def start(): conn=WebModel.connection() - useradmin=UserAdmin(conn) + useradmin=UserAdmin2(conn) + + logintries=LoginTries2(conn) # Check if db exists @@ -248,13 +251,15 @@ def start(): try: - shutil.copy(workdir+'/settings/modules.py.admin', path_settings+'/modules.py') + #shutil.copy(workdir+'/settings/modules.py.admin', path_settings+'/modules.py') - shutil.copy(workdir+'/settings/config_admin.py.sample', path_settings+'/config_admin.py') + #shutil.copy(workdir+'/settings/config_admin.py.sample', path_settings+'/config_admin.py') sql=useradmin.create_table() - if not useradmin.query(sql): + tries_sql=logintries.create_table() + + if not useradmin.query(sql) or not useradmin.query(tries_sql) : print('Error: cannot create table admin, you can create this table with padmin.py') else: @@ -265,7 +270,8 @@ def start(): f.close() - config_text=config_text.replace("modules=['paramecio.modules.welcome']", "modules=['paramecio.modules.welcome', 'paramecio.modules.admin', 'paramecio.modules.lang']") + #config_text=config_text.replace("modules=['paramecio.modules.welcome']", "modules=['paramecio.modules.welcome', 'paramecio.modules.lang']") + config_text=config_text.replace("apps={'welcome': ['paramecio.modules.welcome', 'welcome_app', '/'], 'lang': ['paramecio.modules.lang', '', '']}", "apps={'welcome': ['paramecio.modules.welcome', 'welcome_app', '/'], 'lang': ['paramecio.modules.lang', '', ''], 'admin2': ['paramecio.modules.admin2', 'admin_app', '/admin/']}") with open(path_settings+'/config.py', 'w') as f: @@ -360,7 +366,7 @@ def start(): os.chdir(args.path) #Regenerating modules.py - + """ regenerate='regenerate.py' os.chmod(regenerate, 0o755) @@ -370,7 +376,7 @@ def start(): exit(1) else: print('Regeneration of modules.py finished') - + """ # Installing models padmin='padmin.py' @@ -385,7 +391,7 @@ def start(): models_files=os.listdir(models_path) - m=re.compile(".*\.py$") + m=re.compile(r".*\.py$") underscore=re.compile("^__.*") diff --git a/paramecio/create_module.py b/paramecio/create_module.py index 96e7c87..5f8e68f 100644 --- a/paramecio/create_module.py +++ b/paramecio/create_module.py @@ -8,8 +8,11 @@ import getpass from pathlib import Path from settings import config from importlib import import_module +import re def start(): + """Module for create new modules for paramecio + """ parser=argparse.ArgumentParser(description='A tool for create new modules for paramecio') @@ -37,17 +40,68 @@ def start(): #f=open('modules/'+args.path+'/index.py', 'w') + name_module=os.path.basename(args.path) + try: - shutil.copy(workdir+'/examples/index.py', 'modules/'+args.path) + shutil.copy(workdir+'/examples/app.py', 'modules/'+args.path) + + with open('modules/'+args.path+'/app.py') as f: + + app_file=f.read() + + app_file=app_file.replace('/example', '/'+name_module) + + with open('modules/'+args.path+'/app.py', 'w') as f: + + f.write(app_file) + + pass except: print('Error: cannot copy controller example. Check if you have permissions') exit(1) + # Edit config.py + + #module_final='modules.'+name_module + module_final=f"'{name_module}': ['modules.{name_module}', '', '']" + + try: + + with open('./settings/config.py') as f: + + #modules_final='\''+'\', \''.join(final_modules)+'\'' + + p=re.compile(r"^apps=\{(.*)\}$") + + #config_file=p.sub(r"modules=[\1, "+modules_final+"]", "modules=['paramecio.modules.welcome', 'paramecio.modules.admin', 'paramecio.modules.lang', 'modules.pastafari', 'modules.monit', 'modules.example']") + + final_config='' + + for line in f: + if p.match(line): + line=p.sub(r"apps={\1, "+module_final+"}", line) + final_config+=line + + with open('./settings/config.py', 'w') as f: + + f.write(final_config) + + print('Updated configuration for add new modules...') + + + except: + + print('Cannot update configuration, you need add the new module by hand') + + # Reload config + + #config.modules.append(module_final) + # Regenerate modules - regenerate_modules_config() + #regenerate_modules_config() def regenerate_modules_config(): @@ -76,7 +130,7 @@ def regenerate_modules_config(): for controller in dir_controllers: - if controller.find('.py')!=-1 and controller.find('__init__')==-1: + if controller.find('.py')!=-1 and controller.find('_')==-1: controller_py=controller.replace('.py', '') @@ -99,11 +153,8 @@ def regenerate_modules_config(): print("-"*60) exit(1) - f=open('./settings/modules.py', 'w') - - f.write("".join(modules)) - - f.close() + with open('./settings/modules.py', 'w') as f: + f.write("".join(modules)) if __name__=="__main__": start() diff --git a/paramecio/cromosoma/corefields.py b/paramecio/cromosoma/corefields.py deleted file mode 100644 index 4c24ba3..0000000 --- a/paramecio/cromosoma/corefields.py +++ /dev/null @@ -1,262 +0,0 @@ -from paramecio.cromosoma.webmodel import PhangoField -from paramecio.cromosoma import coreforms -from paramecio.citoplasma.i18n import I18n - -class IntegerField(PhangoField): - - """Class that figure an integer sql type field. - - Args: - name (str): The name of new field - size (int): The size of the new field in database. By default 11. - required (bool): Boolean for define if - - """ - - def __init__(self, name, size=11, required=False): - super(IntegerField, self).__init__(name, size, required) - self.default_value=0 - - def check(self, value): - - """Method for check if value is integer - - Args: - value (int): The value to check - - """ - - self.error=False - self.txt_error='' - - try: - - value=str(int(value)) - - if value=="0" and self.required==True: - self.txt_error="The value is zero" - self.error=True - except: - - value="0" - self.txt_error="The value is zero" - self.error=True - - return value - - def get_type_sql(self): - - """Method for return the sql code for this type - - """ - - return 'INT('+str(self.size)+') NOT NULL DEFAULT "0"' - -class BigIntegerField(IntegerField): - - """Class that figure an big integer sql type field. - - Only change the sql type with respect to IntegerField - - """ - - def get_type_sql(self): - - """Method for return the sql code for this type - - """ - - return 'BIGINT('+str(self.size)+') NOT NULL DEFAULT "0"' - - -class FloatField(PhangoField): - - """Class that figure an float sql type field. - - Args: - name (str): The name of new field - size (int): The size of the new field in database. By default 11. - required (bool): Boolean for define if - - """ - - def __init__(self, name, size=11, required=False): - super(FloatField, self).__init__(name, size, required) - - self.error_default="The value is zero" - self.default_value=0 - - def check(self, value): - - """Method for check if value is integer - - Args: - value (float): The value to check - - """ - - self.error=False - self.txt_error='' - - try: - - value=str(value) - - if value.find(',')!=-1: - value=value.replace(',', '.') - - value=str(float(value)) - - if value==0 and self.required==True: - self.txt_error=self.error_default - self.error=True - except: - - value="0" - self.txt_error=self.error_default - self.error=True - - return value - - def get_type_sql(self): - - return 'FLOAT NOT NULL DEFAULT "0"' - -class DecimalField(FloatField): - - def get_type_sql(self): - - return 'DECIMAL(20, 2) NOT NULL DEFAULT "0"' - -class DoubleField(FloatField): - - def get_type_sql(self): - - return 'DOUBLE NOT NULL DEFAULT "0"' - -class CharField(PhangoField): - - pass - -class TextField(PhangoField): - - def __init__(self, name, required=False): - super().__init__(name, 11, required) - - self.set_default='NOT NULL' - - def get_type_sql(self): - - """Method for return the sql code for this type - - """ - - return 'TEXT '+self.set_default - -class HTMLField(TextField): - - def __init__(self, name, required=False): - super().__init__(name, required) - - def check(self, value): - - return re.sub('<.*?script?>', '', value) - - -class ForeignKeyField(IntegerField): - - def __init__(self, name, related_table, size=11, required=False, identifier_field='id', named_field="id", select_fields=[]): - - super(ForeignKeyField, self).__init__(name, size, required) - - self.table_id=related_table.name_field_id - - self.table_name=related_table.name - - self.related_model=related_table - - self.identifier_field=identifier_field - - self.named_field=named_field - - self.select_fields=select_fields - - self.foreignkey=True - - self.change_form(coreforms.SelectModelForm, [related_table, self.named_field, self.identifier_field]) - - self.default_value=None - - def check(self, value): - - value=super().check(value) - - if value=='0' or value==0: - value='NULL' - - return value - - def get_type_sql(self): - - """Method for return the sql code for this type - - """ - - return 'INT NULL' - - -class BooleanField(IntegerField): - - def __init__(self, name, size=1): - - required=False - - self.yes_text=I18n.lang('common', 'yes', 'Yes') - self.no_text=I18n.lang('common', 'no', 'No') - - super(IntegerField, self).__init__(name, size, required) - - self.default_error="Need 0 or 1 value" - self.default_value=0 - - def check(self, value): - - self.error=False - self.txt_error='' - - try: - - value=int(value) - - if value<0 or value>1: - self.txt_error=self.default_error - self.error=True - - except: - - self.error=True - self.txt_error=self.default_error - value=0 - - value=str(value) - - return value - - def get_type_sql(self): - - """Method for return the sql code for this type - - """ - - return 'BOOLEAN NOT NULL DEFAULT "0"' - - def show_formatted(self, value): - - value=int(value) - - if value==0: - value=self.no_text - else: - value=self.yes_text - - return str(value) diff --git a/paramecio/cromosoma/extrafields/arrayfield.py b/paramecio/cromosoma/extrafields/arrayfield.py deleted file mode 100644 index 93d26b6..0000000 --- a/paramecio/cromosoma/extrafields/arrayfield.py +++ /dev/null @@ -1,59 +0,0 @@ -from paramecio.cromosoma.webmodel import PhangoField,WebModel -import json - -class ArrayField(PhangoField): - - def __init__(self, name, field_type, required=False): - - super().__init__(name, required) - - self.field_type=field_type - - self.error_default='Sorry, the json array is invalid' - - self.set_default='NOT NULL' - - def check(self, value): - - if type(value).__name__=='str': - try: - value=json.loads(value) - except json.JSONDecodeError: - - value=[] - self.error=True - self.txt_error=self.error_default - - elif type(value).__name__!='list': - - value=[] - self.error=True - self.txt_error='Sorry, the json array is invalid' - - if type(self.field_type).__name__!='ArrayField': - for k,v in enumerate(value): - - value[k]=self.field_type.check(v) - - final_value=json.dumps(value) - - final_value=WebModel.escape_sql(final_value) - - return final_value - - def get_type_sql(self): - - return 'TEXT '+self.set_default - - def show_formatted(self, value): - - return ", ".join(value) - - def loads(self, value): - - try: - - return json.loads(value) - except: - - return False diff --git a/paramecio/cromosoma/extrafields/colorfield.py b/paramecio/cromosoma/extrafields/colorfield.py deleted file mode 100644 index 673698e..0000000 --- a/paramecio/cromosoma/extrafields/colorfield.py +++ /dev/null @@ -1,47 +0,0 @@ -from paramecio.cromosoma.corefields import IntegerField -from paramecio.cromosoma.extraforms.colorform import ColorForm - -class ColorField(IntegerField): - - def __init__(self, name, size=11, required=False): - super().__init__(name, size, required) - - self.name_form=ColorForm - - def check(self, value): - - value=str(value).replace('#', '0x') - - value=int(value, 16) - - if value<0 or value>0xffffff: - value=0 - - return value - def get_hex_color(self, value): - - value=str(hex(int(value))).replace('0x', '') - - c=len(value) - - if(c<6): - repeat=6-c - value=('0'*repeat)+value - - value='#'+value - - return value - - def show_formatted(self, value): - - value=str(hex(int(value))).replace('0x', '') - - c=len(value) - - if(c<6): - repeat=6-c - value=('0'*repeat)+value - - value='#'+value - - return '
' % value; diff --git a/paramecio/cromosoma/extrafields/datefield.py b/paramecio/cromosoma/extrafields/datefield.py deleted file mode 100644 index 0ca8995..0000000 --- a/paramecio/cromosoma/extrafields/datefield.py +++ /dev/null @@ -1,39 +0,0 @@ -from paramecio.cromosoma.corefields import PhangoField -from paramecio.citoplasma import datetime -from paramecio.cromosoma.extraforms.dateform import DateForm - -class DateField(PhangoField): - - def __init__(self, name, size=255, required=False): - - super().__init__(name, size, required) - - self.name_form=DateForm - - self.utc=True - - self.error_default='Error: Date format invalid' - - def check(self, value): - - if self.utc: - - value=datetime.local_to_gmt(value) - - elif not datetime.obtain_timestamp(value): - - self.error=True - self.txt_error=self.error_default - return '' - - if value==False: - - self.error=True - self.txt_error=self.error_default - return '' - - return value - - def show_formatted(self, value): - - return datetime.format_date(value) diff --git a/paramecio/cromosoma/extrafields/datetimefield.py b/paramecio/cromosoma/extrafields/datetimefield.py deleted file mode 100644 index 4a5c57c..0000000 --- a/paramecio/cromosoma/extrafields/datetimefield.py +++ /dev/null @@ -1,64 +0,0 @@ -from paramecio.cromosoma.corefields import PhangoField -from paramecio.citoplasma import datetime -from paramecio.cromosoma.extraforms.dateform import DateForm - -class DateTimeField(PhangoField): - - def __init__(self, name, size=255, required=False): - - super().__init__(name, size, required) - - self.name_form=DateForm - - self.utc=False - - self.error_default='Error: Date format invalid' - - def check(self, value): - - if self.utc: - - value=datetime.local_to_gmt(value) - - elif not datetime.obtain_timestamp(value): - - self.error=True - self.txt_error=self.error_default - return None - - if value==False: - - self.error=True - self.txt_error=self.error_default - return None - else: - - """ - format_date_txt="YYYY/MM/DD" - - format_time_txt="HH:mm:ss" - """ - - value=datetime.format_local_strtime('YYYY-MM-DD HH:mm:ss', value) - - return value - - def show_formatted(self, value): - - # Convert to paramecio value - if value!=None: - value=str(value) - value=value.replace('-', '').replace(':', '').replace(' ', '') - - return datetime.format_date(value) - - else: - return '' - - def get_type_sql(self): - - """Method for return the sql code for this type - - """ - - return 'DATETIME NULL' diff --git a/paramecio/cromosoma/extrafields/dictfield.py b/paramecio/cromosoma/extrafields/dictfield.py deleted file mode 100644 index 1ef816e..0000000 --- a/paramecio/cromosoma/extrafields/dictfield.py +++ /dev/null @@ -1,54 +0,0 @@ -from paramecio.cromosoma.webmodel import WebModel, PhangoField - -try: - import ujson as json -except: - import json - -class DictField(PhangoField): - - def __init__(self, name, field_type, required=False): - - super().__init__(name, required) - - self.field_type=field_type - - self.error_default='Sorry, the json dict is invalid' - - self.set_default='NOT NULL' - - def check(self, value): - - if type(value).__name__=='str': - try: - value=json.loads(value) - except json.JSONDecodeError: - - value={} - self.error=True - self.txt_error=self.error_default - - elif type(value).__name__!='dict': - - value={} - self.error=True - self.txt_error=self.error_default - - for k,v in value.items(): - - value[k]=self.field_type.check(v) - - final_value=json.dumps(value) - - #final_value=WebModel.escape_sql(final_value) - - return final_value - - def get_type_sql(self): - - return 'TEXT '+self.set_default - - def show_formatted(self, value): - - return ", ".join(value) - diff --git a/paramecio/cromosoma/extrafields/emailfield.py b/paramecio/cromosoma/extrafields/emailfield.py deleted file mode 100644 index bd35b5c..0000000 --- a/paramecio/cromosoma/extrafields/emailfield.py +++ /dev/null @@ -1,27 +0,0 @@ -from paramecio.cromosoma.corefields import CharField -import re - -mail_pattern=re.compile("\w[\w\.-]*@\w[\w\.-]+\.\w+") - -class EmailField(CharField): - - def __init__(self, name, size=1024, required=False): - - super().__init__(name, size, required) - - self.error_default='Error: No valid format' - - def check(self, value): - - value=super().check(value) - - self.error=False - self.txt_error='' - - if not mail_pattern.match(value): - - self.error=True - value="" - self.txt_error=self.error_default - - return value diff --git a/paramecio/cromosoma/extraforms/colorform.py b/paramecio/cromosoma/extraforms/colorform.py deleted file mode 100644 index 8b7872f..0000000 --- a/paramecio/cromosoma/extraforms/colorform.py +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env python3 - -from paramecio.cromosoma.coreforms import BaseForm -from paramecio.citoplasma.mtemplates import standard_t - -class ColorForm(BaseForm): - - def __init__(self, name, value): - - super().__init__(name, value) - - self.t=standard_t - - def form(self): - - - return self.t.load_template('forms/colorform.phtml', name_form=self.name_field_id, default_value=self.default_value, form=self) diff --git a/paramecio/examples/app.py b/paramecio/examples/app.py new file mode 100644 index 0000000..a6de3f5 --- /dev/null +++ b/paramecio/examples/app.py @@ -0,0 +1,14 @@ +from paramecio.libraries.mtemplates import env_theme, PTemplate +from paramecio.libraries.urls import make_url +from bottle import request +from settings import config +from paramecio.wsgiapp import app + +env=env_theme(__file__) +t=PTemplate(env) + +@app.route('/example') +def home(): + + return "Hello World!!" + diff --git a/paramecio/examples/index.py b/paramecio/examples/index.py deleted file mode 100644 index 0237aa8..0000000 --- a/paramecio/examples/index.py +++ /dev/null @@ -1,14 +0,0 @@ -from paramecio.citoplasma.mtemplates import env_theme, PTemplate -from paramecio.citoplasma.urls import make_url -from bottle import route, request -from settings import config - -env=env_theme(__file__) - -@route('/example') -def home(): - - t=PTemplate(env) - - return "Hello World!!" - diff --git a/paramecio/frontend/index.py b/paramecio/frontend/app.py similarity index 76% rename from paramecio/frontend/index.py rename to paramecio/frontend/app.py index 96233db..0acc1a7 100644 --- a/paramecio/frontend/index.py +++ b/paramecio/frontend/app.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 from paramecio.wsgiapp import app -from paramecio.index import run_app +from paramecio.app import run_app application=app diff --git a/paramecio/frontend/i18nadmin.py b/paramecio/frontend/i18nadmin.py index 7b215f9..6eed6a7 100644 --- a/paramecio/frontend/i18nadmin.py +++ b/paramecio/frontend/i18nadmin.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -from paramecio.citoplasma.check_i18n import start +from paramecio.libraries.check_i18n import start start() diff --git a/paramecio/frontend/padmin.py b/paramecio/frontend/padmin.py index 0ecf3de..307fea2 100644 --- a/paramecio/frontend/padmin.py +++ b/paramecio/frontend/padmin.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -from paramecio.cromosoma.dbadmin import start +from paramecio.libraries.db.dbadmin import start start() diff --git a/paramecio/i18n/admin.py b/paramecio/i18n/admin.py index 8c545d9..a899a71 100644 --- a/paramecio/i18n/admin.py +++ b/paramecio/i18n/admin.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -from paramecio.citoplasma.i18n import I18n +from paramecio.libraries.i18n import I18n I18n.l['en-US']=I18n.l.get('en-US', {}) diff --git a/paramecio/i18n/common.py b/paramecio/i18n/common.py index d260ca6..3cb8913 100644 --- a/paramecio/i18n/common.py +++ b/paramecio/i18n/common.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -from paramecio.citoplasma.i18n import I18n +from paramecio.libraries.i18n import I18n I18n.l['en-US']=I18n.l.get('en-US', {}) diff --git a/paramecio/citoplasma/__init__.py b/paramecio/libraries/__init__.py similarity index 100% rename from paramecio/citoplasma/__init__.py rename to paramecio/libraries/__init__.py diff --git a/paramecio/citoplasma/adminutils.py b/paramecio/libraries/adminutils.py similarity index 89% rename from paramecio/citoplasma/adminutils.py rename to paramecio/libraries/adminutils.py index c890ab4..7698755 100644 --- a/paramecio/citoplasma/adminutils.py +++ b/paramecio/libraries/adminutils.py @@ -1,14 +1,14 @@ #!/usr/bin/env python3 from collections import OrderedDict -from paramecio.citoplasma.sessions import get_session -from paramecio.citoplasma.urls import make_url -from paramecio.citoplasma.i18n import I18n -from paramecio.citoplasma.httputils import GetPostFiles -from paramecio.citoplasma.keyutils import create_key_encrypt, create_key_encrypt_256, create_key -from paramecio.cromosoma.formsutils import generate_csrf +from paramecio.libraries.sessions import get_session +from paramecio.libraries.urls import make_url +from paramecio.libraries.i18n import I18n +from paramecio.libraries.httputils import GetPostFiles +from paramecio.libraries.keyutils import create_key_encrypt, create_key_encrypt_256, create_key +from paramecio.libraries.db.formsutils import generate_csrf from bottle import response,request -from paramecio.cromosoma.webmodel import WebModel +from paramecio.libraries.db.webmodel import WebModel from time import time try: @@ -98,7 +98,8 @@ def get_menu(modules_admin): if len(menu[mod[2]])<4: menu[mod[2]].append('') - + else: + menu[mod[2]][3]=''.format(menu[mod[2]][3]) else: menu[mod[2]]=mod[0] @@ -114,6 +115,8 @@ def get_menu(modules_admin): if len(menu[submod[2]])<4: menu[submod[2]].append('') + else: + menu[submod[2]][3]=''.format(menu[submod[2]][3]) return menu @@ -158,7 +161,7 @@ def login_model(ModelLogin, session='', enable_tries=False): user_admin.conditions=['WHERE username=%s', [username]] - arr_user=user_admin.select_a_row_where(['id', 'username', 'password', 'privileges', 'lang', 'num_tries', 'email']) + arr_user=user_admin.select_a_row_where(['id', 'username', 'password', 'privileges', 'lang', 'num_tries', 'email', 'theme']) if arr_user==False: @@ -180,6 +183,7 @@ def login_model(ModelLogin, session='', enable_tries=False): s[session+'lang']=arr_user['lang'] s[session+'email']=arr_user['email'] s[session+'username']=arr_user['username'] + s[session+'theme']=str(arr_user['theme']) if s['lang']=='': s['lang']=I18n.default_lang diff --git a/paramecio/citoplasma/base_admin.py b/paramecio/libraries/base_admin.py similarity index 79% rename from paramecio/citoplasma/base_admin.py rename to paramecio/libraries/base_admin.py index 06e9673..4e40cfe 100644 --- a/paramecio/citoplasma/base_admin.py +++ b/paramecio/libraries/base_admin.py @@ -1,10 +1,10 @@ #!/usr/bin/env python3 -from paramecio.citoplasma.mtemplates import PTemplate -from paramecio.citoplasma.adminutils import check_login, get_language, get_menu -from paramecio.cromosoma.webmodel import WebModel -from paramecio.citoplasma.sessions import get_session -from paramecio.citoplasma.i18n import I18n +from paramecio.libraries.mtemplates import PTemplate +from paramecio.libraries.adminutils import check_login, get_language, get_menu +from paramecio.libraries.db.webmodel import WebModel +from paramecio.libraries.sessions import get_session +from paramecio.libraries.i18n import I18n try: diff --git a/paramecio/citoplasma/check_i18n.py b/paramecio/libraries/check_i18n.py similarity index 97% rename from paramecio/citoplasma/check_i18n.py rename to paramecio/libraries/check_i18n.py index 96ccba4..7d5832b 100644 --- a/paramecio/citoplasma/check_i18n.py +++ b/paramecio/libraries/check_i18n.py @@ -5,7 +5,7 @@ import os import re from pathlib import Path from importlib import import_module -from paramecio.citoplasma.i18n import I18n +from paramecio.libraries.i18n import I18n from settings import config pattern=re.compile('^\w+\.(py|html|phtml|js)$') @@ -48,6 +48,8 @@ def start(): #lang_t=re.compile("\${lang\('("+module_base+"?)',\s+'(.*?)',\s+'(.*?)'\)\}") lang_t=re.compile("lang\('("+module_base+"?)',\s+'(.*?)',\s+'(.*?)'\)") + lang_s=re.compile("slang\('(.*?)',\s+'(.*?)'\)" + if not os.path.isdir(path): print("Error: directory to scan doesn't exists") @@ -91,7 +93,7 @@ def start(): file_lang="#!/usr/bin/env python3\n\n" - file_lang+="from paramecio.citoplasma.i18n import I18n\n\n" + file_lang+="from paramecio.libraries.i18n import I18n\n\n" for lang in I18n.dict_i18n: diff --git a/paramecio/citoplasma/datetime.py b/paramecio/libraries/datetime.py similarity index 99% rename from paramecio/citoplasma/datetime.py rename to paramecio/libraries/datetime.py index 9ad9703..4287712 100644 --- a/paramecio/citoplasma/datetime.py +++ b/paramecio/libraries/datetime.py @@ -6,7 +6,7 @@ try: from settings import config except: config={} -#from paramecio.citoplasma.sessions import get_session +#from paramecio.libraries.sessions import get_session from os import environ """Simple hook for timedate functions from Arrow datetime module. Maybe in the future use native python datetime functions or other libraries. Is simply an abstraction for not depend of particular library. diff --git a/paramecio/cromosoma/.gitignore b/paramecio/libraries/db/.gitignore similarity index 100% rename from paramecio/cromosoma/.gitignore rename to paramecio/libraries/db/.gitignore diff --git a/paramecio/cromosoma/LICENSE b/paramecio/libraries/db/LICENSE similarity index 100% rename from paramecio/cromosoma/LICENSE rename to paramecio/libraries/db/LICENSE diff --git a/paramecio/cromosoma/README.md b/paramecio/libraries/db/README.md similarity index 100% rename from paramecio/cromosoma/README.md rename to paramecio/libraries/db/README.md diff --git a/paramecio/cromosoma/__init__.py b/paramecio/libraries/db/__init__.py similarity index 100% rename from paramecio/cromosoma/__init__.py rename to paramecio/libraries/db/__init__.py diff --git a/paramecio/libraries/db/corefields.py b/paramecio/libraries/db/corefields.py new file mode 100644 index 0000000..fbe71a9 --- /dev/null +++ b/paramecio/libraries/db/corefields.py @@ -0,0 +1,426 @@ +""" +Parameciofm is a series of wrappers for Bottle.py, mako and others and construct a simple headless cms. + +Copyright (C) 2024 Antonio de la Rosa Caballero + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +""" + +from paramecio.libraries.db.webmodel import PhangoField +from paramecio.libraries.db import coreforms +from paramecio.libraries.i18n import I18n +#from bs4 import BeautifulSoup +import bleach + +class IntegerField(PhangoField): + + """Class that figure an integer sql type field. + """ + + def __init__(self, name, size=11, required=False): + """ + Args: + name (str): The name of field + size (int): The size of the new field in database. By default 11. + required (bool): Boolean for define if field is required or not + """ + + super(IntegerField, self).__init__(name, size, required) + self.default_value=0 + self.jtype='integer' + self.jformat='int64' + self.jexample='12345' + + self.type_sql='int({})'.format(self.size) + + def check(self, value): + + """Method for check if value is integer + + Args: + value (int): The value to check + + """ + + self.error=False + self.txt_error='' + + try: + + value=str(int(value)) + + if value=="0" and self.required==True: + self.txt_error="The value is zero" + self.error=True + except: + + value="0" + self.txt_error="The value is zero" + self.error=True + + return value + + def get_type_sql(self): + + """Method for return the sql code for this type + + """ + + return 'INT('+str(self.size)+') NOT NULL DEFAULT "0"' + +class BigIntegerField(IntegerField): + + """Class that figure an big integer sql type field. + + Only change the sql type with respect to IntegerField + + """ + + def __init__(self, name, size=11, required=False): + + super().__init__(name, size, required) + self.type_sql='bigint({})'.format(self.size) + + def get_type_sql(self): + + """Method for return the sql code for this type + + """ + + return 'BIGINT('+str(self.size)+') NOT NULL DEFAULT "0"' + + +class FloatField(PhangoField): + + """Class that figure an float sql type field. + + Args: + name (str): The name of new field + size (int): The size of the new field in database. By default 11. + required (bool): Boolean for define if + + """ + + def __init__(self, name, size=11, required=False): + super(FloatField, self).__init__(name, size, required) + + self.error_default="The value is zero" + self.default_value=0 + self.type_sql='float'.format(self.size) + + self.jtype='float' + + def check(self, value): + + """Method for check if value is integer + + Args: + value (float): The value to check + + """ + + self.error=False + self.txt_error='' + + try: + + value=str(value) + + if value.find(',')!=-1: + value=value.replace(',', '.') + + value=str(float(value)) + + if value==0 and self.required==True: + self.txt_error=self.error_default + self.error=True + except: + + value="0" + self.txt_error=self.error_default + self.error=True + + return value + + def get_type_sql(self): + + return 'FLOAT NOT NULL DEFAULT "0"' + +class DecimalField(FloatField): + """PhangoField field for Decimals fields.""" + + def __init__(self, name, size=11, required=False): + + super().__init__(name, size, required) + self.type_sql='decimal(20,2)' + self.jtype='number' + + def get_type_sql(self): + + return 'DECIMAL(20, 2) NOT NULL DEFAULT "0"' + +class DoubleField(FloatField): + """PhangoField field for Double fields.""" + + def __init__(self, name, size=11, required=False): + + super().__init__(name, size, required) + self.type_sql='double' + self.jtype='float' + + def get_type_sql(self): + + return 'DOUBLE NOT NULL DEFAULT "0"' + +class CharField(PhangoField): + """Simple alias for PhangoField""" + + pass + +class TextField(PhangoField): + """Class used for text fields + + Class used for text fields, use TEXT sql type for the this field. + """ + + def __init__(self, name, required=False): + """Init TextField class different to standard PhangoField + + Args: + name (str): The name of new field + required (bool): Boolean for define if field is required or not + + Attributes: + set_default (str): Set if the value es NOT NULL or not + """ + + super().__init__(name, 11, required) + + self.type_sql='text' + + self.set_default='NOT NULL' + + def get_type_sql(self): + + """Method for return the sql code for this type + + """ + + return 'TEXT '+self.set_default + +class LongTextField(TextField): + """Class used for long text fields (32 bits size, 4G) + + Class used for text fields, use LONGTEXT sql type for the this field. + """ + + def __init__(self, name, required=False): + """Init TextField class different to standard PhangoField + + Args: + name (str): The name of new field + required (bool): Boolean for define if field is required or not + + Attributes: + set_default (str): Set if the value es NOT NULL or not + """ + + super().__init__(name, required) + self.type_sql='longtext' + + def get_type_sql(self): + + """Method for return the sql code for this type + + """ + + return 'LONGTEXT '+self.set_default + +class HTMLField(TextField): + """Class used for html fields + + Class used for html fields, use TEXT sql type for the this field because is children of TextField. In this method self.escape is used for convert " to " + """ + + def __init__(self, name, required=False): + """Init HTMLField class different to standard PhangoField + + Args: + name (str): The name of new field + required (bool): Boolean for define if field is required or not + + Attributes: + trusted_tags (list): A list with safe tags. + """ + + super().__init__(name, required) + self.trusted_tags=[] + + def check(self, value): + """Check method for html values + + This check method use beautifulsoap for clean and format html code + """ + + # leach.clean('

"trial"

', tags=('p')) + """ + soup=BeautifulSoup(value, features='html.parser') + + for tag in soup.findAll(True): + + if tag.name not in self.trusted_tags: + tag.hidden=True + + value=soup.renderContents().decode('utf-8') + + if self.escape: + + return value.replace('"', '"') + else: + + return value + + """ + + value=bleach.clean('

"trial"

', tags=self.trusted_tags) + + if self.escape: + + return value.replace('"', '"') + else: + + return value + + +class ForeignKeyField(IntegerField): + """Subclass of IntegerField for create Foreign keys + + A subclass of IntegerField used for create foreign keys in related tables. + """ + + def __init__(self, name, related_table, size=11, required=False, identifier_field='id', named_field="id", select_fields=[]): + """ + Args: + name (str): Name of field + related_table (WebModel): The table-model related with this foreign key + size (int): The size of the new field in database. By default 11. + required (bool): Boolean for define if field is required or not + identifier_field (str): The Id field name from related table + named_field (str): The field from related table used for identify the row seleted from related table + select_fields (list): A series of fields names from related + """ + + super(ForeignKeyField, self).__init__(name, size, required) + + self.table_id=related_table.name_field_id + + self.table_name=related_table.name + + self.related_model=related_table + + self.identifier_field=identifier_field + + self.named_field=named_field + + self.select_fields=select_fields + + self.foreignkey=True + + self.change_form(coreforms.SelectModelForm, [related_table, self.named_field, self.identifier_field]) + + self.default_value=None + + def check(self, value): + + value=super().check(value) + + if value=='0' or value==0: + value=None + + return value + + def get_type_sql(self): + + """Method for return the sql code for this type + + """ + + return 'INT NULL' + + +class BooleanField(IntegerField): + """Field for boolean values + """ + + def __init__(self, name, size=1): + """ + Args: + name (str): Name of field + size (int): The size of the new field in database. By default 11. + """ + + required=False + + self.yes_text=I18n.lang('common', 'yes', 'Yes') + self.no_text=I18n.lang('common', 'no', 'No') + + super(IntegerField, self).__init__(name, size, required) + + self.default_error="Need 0 or 1 value" + self.default_value=0 + + self.type_sql='tinyint(1)' + + def check(self, value): + + self.error=False + self.txt_error='' + + try: + + value=int(value) + + if value<0 or value>1: + self.txt_error=self.default_error + self.error=True + value=0 + + except: + + self.error=True + self.txt_error=self.default_error + value=0 + + value=str(value) + + return value + + def get_type_sql(self): + + """Method for return the sql code for this type + + """ + + return 'BOOLEAN NOT NULL DEFAULT "0"' + + def show_formatted(self, value): + + value=int(value) + + if value==0: + value=self.no_text + else: + value=self.yes_text + + return str(value) diff --git a/paramecio/cromosoma/coreforms.py b/paramecio/libraries/db/coreforms.py similarity index 52% rename from paramecio/cromosoma/coreforms.py rename to paramecio/libraries/db/coreforms.py index 3247ddc..76d8a7a 100644 --- a/paramecio/cromosoma/coreforms.py +++ b/paramecio/libraries/db/coreforms.py @@ -1,13 +1,55 @@ #!/usr/bin/env python3 +""" +Parameciofm is a series of wrappers for Bottle.py, mako and others and construct a simple headless cms. + +Copyright (C) 2024 Antonio de la Rosa Caballero + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +""" + + from collections import OrderedDict from html import escape #Forms para python3 class BaseForm: + """The class used by all forms classes + + BaseForm is the base class used for all form classes. + + A form class is used for generate simple html forms, how input type, text type, hidden type, etc. PhangoField classes use this forms for generate automatically forms using GenerateAdminClass and others. + """ def __init__(self, name, value): + """ + Args: + name (str): The html name for this form + value (str): The default value of this html form. + + Attributes: + label (str): Label used in functions how show_form that generate a complete html form from a form class list + name (str): Name of the html form. + default_value (mixed): The default value of the form. Equal to value in typical html form. + css (str): Used for add css classes to the html form + type (str): Variable used for conventional html forms with html tag + field (PhangoField): Field related with this form. Used in PhangoField. + required (boolean): If form is required or not, used in functions that generate forms. + name_field_id (str): The html id for the html form. Used for html things. + help (str): A string with help text, used in functions that generate forms. + """ self.label=name self.name=name @@ -17,26 +59,45 @@ class BaseForm: self.field=None self.required=False self.txt_error='' + self.error=False self.name_field_id=self.name+'_form' self.help='' + self.placeholder='' def form(self): + """Method for returm the html code of the form + """ - return '' + return '' def show_formatted(self, value): + """Method for show the value of form formatted + + Args: + value (mixed): The value of field form + """ return value #Method for escape value for html input. DON'T CHANGE IF YOU DON'T KNOWN WHAT ARE YOU DOING def setform(self, value): + """A method for set the value in the form for escape and other things + + Args: + value (mixed): The value of field form for set + """ value=str(value) return value.replace('"', '"').replace("'", ''') def change_name(self, new_name): + """A method for change the default form html name of the field form + + Args: + new_name (str): The new name of the form. Always is finished with _form suffix + """ self.name=new_name @@ -45,6 +106,8 @@ class BaseForm: return "" class SimpleTextForm(BaseForm): + """Form for simple text + """ def __init__(self, name, value): super().__init__(name, value) @@ -56,6 +119,8 @@ class SimpleTextForm(BaseForm): return super().form()+' '+self.after_text class TextForm(BaseForm): + """Form for simple text form + """ def __init__(self, name, value): super(TextForm, self).__init__(name, value) @@ -65,6 +130,8 @@ class TextForm(BaseForm): return '' class PasswordForm(BaseForm): + """Form for password forms + """ def __init__(self, name, value, show_password=False): super(PasswordForm, self).__init__(name, value) @@ -79,6 +146,8 @@ class PasswordForm(BaseForm): return value class HiddenForm(BaseForm): + """Form for hidden forms + """ def __init__(self, name, value): super(HiddenForm, self).__init__(name, value) @@ -86,8 +155,16 @@ class HiddenForm(BaseForm): class SelectForm(BaseForm): + """Form for select html form + """ def __init__(self, name, value, elements=OrderedDict()): + """ + Args: + name (str): The html name for this form + value (str): The default value of this html form + elements (OrderedDict): An ordered dict with the keys(the form value) and text label. Example, if you have a OrderedDict how {'0': 'Value selected'} in a html select form you have the next result: ' @@ -135,7 +136,7 @@ def generate_csrf(): if not 'csrf_token' in s: s['csrf_token']=create_key_encrypt() - s.save() + #s.save() return s['csrf_token'] diff --git a/paramecio/cromosoma/querybuilder.py b/paramecio/libraries/db/querybuilder.py similarity index 100% rename from paramecio/cromosoma/querybuilder.py rename to paramecio/libraries/db/querybuilder.py diff --git a/paramecio/libraries/db/simplequery.py b/paramecio/libraries/db/simplequery.py new file mode 100644 index 0000000..7c8aaad --- /dev/null +++ b/paramecio/libraries/db/simplequery.py @@ -0,0 +1,44 @@ + +# A more simple set for make queries + +def insert(model, db, dict_values): + + final_values={} + + for k in model.fields.keys(): + final_values[k]=model.fields[k].check(dict_values.get(k, '')) + + del final_values[model.name_field_id] + + str_fields="`"+"`, `".join(final_values.keys())+"`" + + str_query='insert into {} ({}) VALUES ({})'.format(model.name, str_fields, ", ".join(['%s']*len(final_values))) + + success=False + + with db.query(str_query, list(final_values.values())) as cursor: + + if cursor.rowcount>0: + + model.last_id=cursor.lastrowid + success=True + + return success + + +def select(model, db, dict_fields=[], where_sql='', limit='', dict_values=[]): + + if len(dict_fields)==0: + dict_fields=['`'+field+'`' for field in model.fields.keys()] + + str_fields=", ".join(dict_fields) + + str_query='select {} from {} {} limit 1'.format(str_fields, model.name, where_sql) + + arr_result=[] + + with db.query(str_query, dict_values) as cursor: + + arr_result=cursor.fetchall() + + return arr_result diff --git a/paramecio/cromosoma/usermodel.py b/paramecio/libraries/db/usermodel.py similarity index 96% rename from paramecio/cromosoma/usermodel.py rename to paramecio/libraries/db/usermodel.py index 99b77d8..a2ead69 100644 --- a/paramecio/cromosoma/usermodel.py +++ b/paramecio/libraries/db/usermodel.py @@ -1,9 +1,9 @@ #!/usr/bin/env python3 -from paramecio.cromosoma.webmodel import WebModel -from paramecio.cromosoma.coreforms import PasswordForm -from paramecio.citoplasma.i18n import I18n -from paramecio.citoplasma.httputils import GetPostFiles +from paramecio.libraries.db.webmodel import WebModel +from paramecio.libraries.db.coreforms import PasswordForm +from paramecio.libraries.i18n import I18n +from paramecio.libraries.httputils import GetPostFiles class UserModel(WebModel): diff --git a/paramecio/cromosoma/webmodel.py b/paramecio/libraries/db/webmodel.py similarity index 59% rename from paramecio/cromosoma/webmodel.py rename to paramecio/libraries/db/webmodel.py index c4da46f..2f16db2 100644 --- a/paramecio/cromosoma/webmodel.py +++ b/paramecio/libraries/db/webmodel.py @@ -1,23 +1,64 @@ #!/usr/bin/env python3 +from paramecio.libraries.db.databases.sqlalchemy import SqlClass +from paramecio.libraries.db.coreforms import BaseForm, HiddenForm import sys import re import uuid from importlib import import_module, reload from collections import OrderedDict -from paramecio.cromosoma.databases.sqlalchemy import SqlClass -from paramecio.cromosoma.coreforms import BaseForm, HiddenForm import copy import traceback class PhangoField: + """Base class for fields used in WebModel classes + + PhangoField is a class with all elements and variables that you can imagine for a database mysql field in a table. You have many types similar to mysql field types. + """ def __init__(self, name, size=255, required=False): + """ + Args: + name (str): The name of the field + size (int): The size of sql field. + required (bool): If the field is required or not. + + Attributes: + name (str): The name of the field + jtype(Python type): The type of value in python + label (str): A label or generic name for use in text labels used for representate the field + required (bool): If the field is required or not. + size (int): The size of sql field. + protected (bool): If the field can be updated or not in WebModel update method. + quote_close (str): In old versions was used for get more protection for sql sentences + error (bool): If error in query, set to True. + txt_error (str): Variable where the basic text error is saved + model (str): The model where this component or field live + indexed (bool): Property used for set this field how indexed in the database table. + unique (bool): Property used for set this field how unique value in the database table. + foreignkey (bool): Simple property for make more easy identify foreignkeyfields. + default_value (str): Property that define the default value for this field + update (bool): Property that define if this field is in an update operation or insert operation + check_blank (bool): Property used for check if this value cannot change if is in blank and is filled + name_form(BaseForm): Define the form, when is created forms with create_forms you can change the properties of this class + escape (bool): Property that define if make escape in show_formatted. This property control the html transformation of <>" characters in html entities.If false, convert. + file_related (bool): File related: if the field have a file related, delete the file + extra_parameters (list): Extra parameters for the form related with this field + t (PTemplate): Template manager for the form if needed + error_default (str): Error text by default + show_formatted_value (bool): Show this value formatted + help (str): Value used for help strings in tooltips in forms + + """ # The name of the field in database table self.name=name + # The type of the field in javascript. Util for api documentation + + self.jtype='string' + # The label for the Field self.label=name.replace('_', ' ').title() @@ -50,7 +91,7 @@ class PhangoField: self.txt_error="" - # Themodel where this component or field live + # The model where this component or field live self.model=None @@ -100,7 +141,7 @@ class PhangoField: # Error by default - self.error_default='Error: field required' + self.error_default='Error: '+self.name+' field required' # Show this value formatted @@ -109,28 +150,29 @@ class PhangoField: # Value used for help strings in tooltips in forms self.help='' - - # This method is used for describe the new field in a sql language format. - + + self.type_sql='varchar({})'.format(self.size) def get_type_sql(self): + """This method is used for describe the new field in a sql language format.""" return 'VARCHAR('+str(self.size)+') NOT NULL DEFAULT "'+self.default_value+'"' def show_formatted(self, value): + """Method for format the value to show in html or others text outputs""" return value - - # This method for check the value - def check(self, value): + """Method for check if value is valid for this type field""" self.error=False self.txt_error='' value=str(value).strip() + #Minimal escape for prevent basic js injection. + if self.escape==False: value=value.replace('<', '<') @@ -150,6 +192,7 @@ class PhangoField: pass def create_form(self): + """Create a BaseForm object for use in forms functions and methods""" #self.name, self.default_value, final_parameters=copy.copy(self.extra_parameters) @@ -165,6 +208,7 @@ class PhangoField: return form def change_form(self, new_form, parameters): + """Change the base form of the field and its parameters""" self.name_form=new_form @@ -174,6 +218,10 @@ class PhangoField: pass class PrimaryKeyField(PhangoField): + """Primary key field based in PhangoField. + + This field is used for create a typical id field in a mariadb/mysql table + """ def __init__(self, name, size=11, required=False): super(PrimaryKeyField, self).__init__(name, size, required) @@ -181,6 +229,7 @@ class PrimaryKeyField(PhangoField): self.name_form=HiddenForm self.required=False self.error_default="The value is zero" + self.type_sql='int({})'.format(self.size) def check(self, value): @@ -217,6 +266,24 @@ class PrimaryKeyField(PhangoField): class WebModel: + """The most important class for the framework + + Webmodel is a class for create objects that represent models. This models are a mirage of SQL tables. You can create fields, add indexes, foreign keys, and more. + + Attributes: + arr_sql_index (dict): Internal dict used for generate mysql index in fields + arr_sql_set_index (dict): Internal dict used for generate mysql index in fields + arr_sql_unique (dict): Internal dict used for generate mysql unique values in fields + arr_sql_set_unique (dict): Internal dict used for generate mysql unique values in fields + last_query (str): The last query execute by WebModel + connection_pool (list): A list used in older versions, deprecated + first_primary_key (PrimaryKeyField): Field used for primary field and create models in database. + model (OrderedDict): Dict used for internal things and create tables. + connections (dict): A dict with the configuration of the mysql connection. You can use this element in config.py. You set elements, normally "default" with typical elements how: + host: Database host, user: The username of mysql db, password: The password of user, db: The name of the db, charset: The charset of database, normally utf8, db_type: The db_type, possible values are mysqldb or pymysql, by default, pymysql. + connection_id (str): The id by default of the selected connection from connections. + + """ __slots__=('sqlclass', 'fields', 'forms') @@ -248,12 +315,54 @@ class WebModel: @staticmethod def connection(): + """Static method for make a connection using SqlClass + + Returns: Return a SqlClass connection for mysql db. + """ return SqlClass(WebModel.connections['default']) # Init the class def __init__(self, sqlclass=None, name_field_id="id"): + """ + Args: + sqlclass (SqlClass): The SqlClass connection used for the mysql db + name_field_id (str): The name of field id of this model/mysql table + + Attributes: + name (str): The name of this model correspondient to the sql table name with lower string. + label (str): Descriptive name, first is used self.name how default. + label_general (str): Descriptive general name, first is used self.name how default. + name_field_id (str): The name of field id of this model/mysql table + fields (OrderedDict): A dict with the fields of model/table based in PhangoField objects. + fields_error (OrderedDict): A dict where the errors when check data fields are saved + related (list): A list where related fields are saved. + forms (OrderedDict): A dict where forms related with fields using how base BaseForm class are saved if you use self.create_forms() method. + errors (dict): A dict where generic errors are saved. + num_errors (int): Number of errors generated by the model on query methods. + query_error (str): If error in query, saved here. + values_query (list): Where the values for a sql query for filtering are saved. + conditions (list): A list used for define the sql conditions. + First element is the sql condition, Example: 'WHERE id=%s', and second element is the variable to substitute %s, example [1]. Complete example: ['WHERE id=%s', 1] + order_by (str): Internal variable used for set the sql order str. You don't shoud change this variable if yo don't know what are you doing. + limit (str): Internal variable used for set the sql limit str. + related_models_deleted (list): Internal variable used for delete tables from db. + required_save (str): Internal variable used for required fields defined in self.fields + primary_key (str): Default name of primary key field + yes_reset_conditions (bool): If True, methods how select and update reset self.conditions. If False, self.conditions is used in next select and update executions. + updated (bool): True if the model is used for update, False if the model is used for insert or other operations. + valid_fields (list): List with the fields validated for insert or update + last_id (int): The id of last inserted row. + distinct (str): Add DISTINCT keyword to self.select method. + post (dict): A simple dictionary where post values are saved for use of fields classes + files_delete (dict): A simple dictionary that save the fields that have files related. If i delete the row in database i need delete the files related + sqlclass (SqlClass): A sql_class used for connect to db. + show_formatted (bool): If True, by default all fields are showed with formatted value using show_formatted method of PhangoField classes and children in select method. If False, raw value is showed. + enctype (bool): If True, forms generated using this model are prepared for enctype=multipart/form-data A.K.A. upload files. + model_id (int): Variable where the actual row from model selected can be saved for different things. + field_quote(str): The field delimiter. In Mariadb is `, in PostgreSQL in the future is \" + """ self.cached=WebModel.global_cached @@ -277,7 +386,7 @@ class WebModel: # Errors of fields of the table, for safe thread reasons. - self.fields_error=OrderedDict() + #self.fields_error=OrderedDict() #The tables related with foreignkeyfield to this table @@ -357,12 +466,20 @@ class WebModel: self.enctype=False + self.model_id=0 + self.dummy=0 + + self.field_quote='`' # A method for add the connection def conn(self, sqlclass): + """ Method for get the SqlClass object and prepare sql variables + Args: + sqlclass (SqlClass): A SqlClass object that present the db connection + """ self.sqlclass=sqlclass # Reset conditions @@ -371,21 +488,35 @@ class WebModel: self.conditions=["WHERE 1=1", []] - self.order_by="ORDER BY `"+self.name+"`.`id` ASC" + name_table=self.field_quote+self.name+self.field_quote + + field_id=self.field_quote+"id"+self.field_quote + + self.order_by="ORDER BY "+name_table+"."+field_id+" ASC" self.limit="" # A method for change the name of table def change_name(self, name): + """ A method for change the name of table + + Args; + name (str): The new name of table + """ self.name=name + + name_table=self.field_quote+self.name+self.field_quote + + field_id=self.field_quote+"id"+self.field_quote - self.order_by="ORDER BY `"+self.name+"`.`id` ASC" + self.order_by="ORDER BY "+name_table+"."+field_id+" ASC" # A method where create the new fields of this model def create_fields(self): + """Dummy method for use in children classes for add fields""" #print([i for i in dir(self.__class__) if i[:1] != '_']) #print(dir(self)) @@ -395,6 +526,14 @@ class WebModel: # A method for register the fields def register(self, field_model, required=False): + """A method for register the fields in model class + + With this method, your register your fields in the model, inside self.fields attribute. Fields are used for build the query for get the data from the sql table. + + Args: + field_model (PhangoField): PhangoField object for add to model + required (bool): If True, the field is required when you insert or update a item row in table model. If False, the field is not required. If field is not required and checking fail, the model update/insert ignore it. + """ #self.fields_required[field_model]=field_model.required @@ -436,6 +575,13 @@ class WebModel: # Method for make queries def query(self, str_query, args=[], connection_id='default'): + """Method for make typical sql query to db + + Args: + str_query (str): The str query. Use the typical format of sql python drivers, example: select * from my_table WHERE id=%s. + args (list): The arguments to substitute %s characters of the strings. The list must sequential with %s characters in the string. + connection_id (str): The connection data used for this connection, by default is "default". + """ self.connect_to_db() return self.sqlclass.query(str_query, args, connection_id) @@ -443,6 +589,8 @@ class WebModel: # Method for clean fields def clean_fields(self): + """Method for delete fields from self.fields dict""" + clean=self.fields_to_clean for field in self.fields_to_clean: del self.fields[field] @@ -451,6 +599,14 @@ class WebModel: # External agent define if the update is in code or from external source, how a form. def insert(self, dict_values, external_agent=True): + """Insert method, for insert a row in database using a dictionary + + This method is a shortcut for typical sql insert sentence. + + Args: + dict_values (dict): A dict with the name of the fields how defined in PhangoField for the keys, and values for the values designed for every field. + external_agent (bool): External agent define if the update is in code or from external source, how a form. + """ self.clean_fields() @@ -466,7 +622,6 @@ class WebModel: try: - #fields, values, update_values=self.check_all_fields(dict_values, external_agent) arr_return=self.check_all_fields(dict_values, external_agent) if arr_return: @@ -476,17 +631,20 @@ class WebModel: else: return False - except: - self.query_error='Cannot insert the new row' - print(sys.exc_info()[0]) - raise - #return False + except Exception as e: + self.query_error='Cannot insert the new row '+str(e) + #print(sys.exc_info()[0]) + return False c=len(values) arr_str=['%s' for x in range(c)] - sql="insert into `"+self.name+"` (`"+"`, `".join(fields)+"`) VALUES ("+", ".join(arr_str)+")" + join_fields="`, `".join(fields) + + join_values=", ".join(arr_str) + + sql="insert into `"+self.name+self.field_quote+" ("+self.field_quote+join_fields+self.field_quote+") VALUES ("+join_values+")" cursor=self.query(sql, values, self.connection_id) @@ -509,6 +667,15 @@ class WebModel: # Update method. For update one or many rows. def update(self, dict_values, external_agent=True): + """Upate method, for update a row in database using a dictionary + + This method is a shortcut for typical sql update sentence. + + Args: + dict_values (dict): A dict with the name of the fields how defined in PhangoField for the keys, and values for the values designed for every field. + external_agent (bool): External agent define if the update is in code or from external source, how a form. + """ + self.clean_fields() @@ -545,7 +712,9 @@ class WebModel: #print(traceback.format_exc()) return False - sql="update `"+self.name+"` SET "+", ".join(update_values)+" "+self.conditions[0] + field_name=self.field_quote+self.name+self.field_quote + + sql="update "+field_name+" SET "+", ".join(update_values)+" "+self.conditions[0] cursor=self.query(sql, values+self.conditions[1], self.connection_id) @@ -583,7 +752,7 @@ class WebModel: """ def reset_conditions(self): - + """Method for reset self.conditions to default values""" self.conditions=["WHERE 1=1", []] self.limit='' @@ -591,6 +760,19 @@ class WebModel: #Type assoc can be assoc for return dictionaries def select(self, arr_select=[], raw_query=False): + """A method for select fields from a table in db. Support for foreignkeys. + + This method is a shortcut for typical sql select sentence. You can select multiple tables using ForeignKeyField class. + + Args: + arr_select (dict): A list with the name of the fields how defined in PhangoField. + raw_query (bool): If True, if foreignkeyfields exists, are not selected. If False, foreignkeyfields are selected too if foreignkeyfield are in arr_select. + + Returns: + false (bool): If return false, the db connection is down. + sql_cursor (cursor): Return cursor db for get data using loops or other if operation is successful, if not, return False. + + """ self.clean_fields() @@ -608,7 +790,7 @@ class WebModel: #First table selecction - tables_to_select=['`'+self.name+'`'] + tables_to_select=[self.field_quote+self.name+self.field_quote] keys=list(self.fields.keys()) @@ -641,21 +823,21 @@ class WebModel: else: arr_repeat_field[self.fields[field].table_name]=0 - table_name=self.fields[field].table_name+'` as `'+self.fields[field].table_name+str(arr_repeat_field[self.fields[field].table_name]) + table_name=self.fields[field].table_name+self.field_quote+' as '+self.field_quote+self.fields[field].table_name+str(arr_repeat_field[self.fields[field].table_name]) final_table_name=self.fields[field].table_name+str(arr_repeat_field[self.fields[field].table_name]) # The name with its alias of this related table model - tables_to_select.append('`'+table_name+'`') + tables_to_select.append(self.field_quote+table_name+self.field_quote) # Add field from related table # as "+table_name+"_"+self.fields[field].named_field - extra_fields.append("`"+final_table_name+"`.`"+self.fields[field].named_field+"` as "+field) + extra_fields.append(self.field_quote+final_table_name+self.field_quote+"."+self.field_quote+self.fields[field].named_field+self.field_quote+" as "+field) # Add a condition to sql query for join the two tables. - conditions[0]+=" AND `"+final_table_name+"`.`"+self.fields[field].identifier_field+"`=`"+self.name+"`.`"+field+"`" + conditions[0]+=" AND "+self.field_quote+final_table_name+self.field_quote+"."+self.field_quote+self.fields[field].identifier_field+self.field_quote+"="+self.field_quote+self.name+self.field_quote+"."+self.field_quote+field+self.field_quote # Add extra fields from related table from select_fields ForeignKeyField class member @@ -668,11 +850,11 @@ class WebModel: # Check if extra_field is ForeignKeyField, if yes, call this function recursively. - extra_fields.append("`"+final_table_name+"`.`"+extra_field+"` as `"+field+"_"+extra_field+"`") + extra_fields.append(self.field_quote+final_table_name+self.field_quote+"."+self.field_quote+extra_field+self.field_quote+" as "+self.field_quote+field+"_"+extra_field+self.field_quote) else: # Add normal field to sql query - final_fields.append("`"+self.name+"`.`"+field+"`") + final_fields.append(self.field_quote+self.name+self.field_quote+"."+self.field_quote+field+self.field_quote) #if len(new_fields)>0: #self.fields.update(new_fields) @@ -706,16 +888,31 @@ class WebModel: # Show results in a dictionary def fetch(self, cursor): + """ Simple method for get a row from db using cursor + + Args: + cursor (Db cursor): A typical db cursor of python sql interface standard. + + Returns: + row (dict): Return a dictionary with the row selected. + + """ return cursor.fetchone() def insert_id(self): + """Method for get the id from last row inserted in table""" return self.last_id def element_exists(self, id): + """Check if exist row with id in db - self.conditions=['WHERE `'+self.name_field_id+'`=%s', [id]] + Args: + id (int): The id of the row to search. + """ + + self.conditions=['WHERE '+self.field_quote+self.name_field_id+self.field_quote+'=%s', [id]] count=self.select_count(self.name_field_id) @@ -731,8 +928,17 @@ class WebModel: pass def select_a_row(self, id, fields_selected=[], raw_query=0): + """Shortcut for get a simple row from a query - self.conditions=['WHERE `'+self.name+'`.`'+self.name_field_id+'`=%s', [id]] + Args: + fields_selected (dict): A list with the name of the fields how defined in PhangoField. + raw_query (bool): If True, if foreignkeyfields exists, are not selected. If False, foreignkeyfields are selected too if foreignkeyfield are in arr_select. + + Returns: + row (dict): Returns dict with the row values. + """ + + self.conditions=['WHERE '+self.field_quote+self.name+self.field_quote+'.'+self.field_quote+self.name_field_id+self.field_quote+'=%s', [id]] self.limit="limit 1" @@ -752,6 +958,15 @@ class WebModel: return row def select_a_row_where(self, fields_selected=[], raw_query=0, begin=0): + """Shortcut for get a simple row from a query using self.conditions + + Args: + fields_selected (dict): A list with the name of the fields how defined in PhangoField. + raw_query (bool): If True, if foreignkeyfields exists, are not selected. If False, foreignkeyfields are selected too if foreignkeyfield are in arr_select. + + Returns: + row (dict): Returns dict with the row values. + """ self.limit="limit "+str(begin)+", 1" @@ -770,6 +985,16 @@ class WebModel: def select_to_array(self, fields_selected=[], raw_query=0): + """Shortcut for get a a list of rows from select sql query + + Args: + fields_selected (dict): A list with the name of the fields how defined in PhangoField. + raw_query (bool): If True, if foreignkeyfields exists, are not selected. If False, foreignkeyfields are selected too if foreignkeyfield are in arr_select. + + Returns: + row_values (dict): Returns dict with the row values. + """ + if len(fields_selected)==0: fields_selected=list(self.fields.keys()) @@ -810,6 +1035,16 @@ class WebModel: def select_to_dict(self, fields_selected=[], raw_query=0, integer=True): + """Shortcut for get a dict of rows from select sql query + + Args: + fields_selected (dict): A list with the name of the fields how defined in PhangoField. + raw_query (bool): If True, if foreignkeyfields exists, are not selected. If False, foreignkeyfields are selected too if foreignkeyfield are in arr_select. + + Returns: + row (dict): Returns dict with the row values. + """ + if integer: def check_index(index): return index @@ -855,6 +1090,15 @@ class WebModel: # A method por count num rows affected for sql conditions def select_count(self, field_to_count='id', raw_query=True): + """Method for get a typical sql count using conditions + + Args: + field_to_count (str): The field + raw_query (bool): If True, if foreignkeyfields exists, are not selected. If False, foreignkeyfields are selected too if foreignkeyfield are in arr_select. + + Returns: + num_elecments (int): Returns the number of elements selected. + """ # Connect to db @@ -864,7 +1108,7 @@ class WebModel: #First table selecction - tables_to_select=['`'+self.name+'`'] + tables_to_select=[self.field_quote+self.name+self.field_quote] fields=list(self.fields.keys()) @@ -878,13 +1122,13 @@ class WebModel: table_name=self.fields[field].table_name - tables_to_select.append('`'+table_name+'`') + tables_to_select.append(self.field_quote+table_name+self.field_quote) # Add a condition to sql query for join the two tables. - conditions[0]+=" AND `"+table_name+"`.`"+self.fields[field].identifier_field+"`=`"+self.name+"`.`"+field+"`" + conditions[0]+=" AND "+self.field_quote+table_name+self.field_quote+"."+self.field_quote+self.fields[field].identifier_field+self.field_quote+"="+self.field_quote+self.name+self.field_quote+"."+self.field_quote+field+self.field_quote - sql= "select count(`"+field_to_count+"`) from "+", ".join(tables_to_select)+' '+conditions[0] + sql= "select count("+self.field_quote+field_to_count+self.field_quote+") from "+", ".join(tables_to_select)+' '+conditions[0] count=0 @@ -901,12 +1145,19 @@ class WebModel: # A method for delete rows using sql conditions def delete(self): + """Method for delete a series of rows using conditions + + Returns: + bool (bool): If delete is successfully, return True, if not, return False. + """ #self.connect_to_db() #Need delete rows from other related tables save in self.related_models_deleted - sql=("delete from `"+self.name+"` "+self.conditions[0]+' '+self.order_by+' '+self.limit).strip() + #+' '+self.order_by+' '+self.limit + + sql=("delete from "+self.field_quote+self.name+self.field_quote+" "+self.conditions[0]).strip() result=self.query(sql, self.conditions[1], self.connection_id) @@ -923,6 +1174,15 @@ class WebModel: return False def set_conditions(self, sql_text, values:list) -> object: + """Method for set conditions for a typical sql query + + Args: + sql_text (str): The sql text with the conditions, Example: WHERE id=%s + values (list): A list with values for substitute %s characters for the real values filtered for not allow sql injections. + + Returns: + Return the same object with self.conditions modified. + """ self.conditions=[sql_text, values] @@ -930,6 +1190,15 @@ class WebModel: @staticmethod def check_in_list(in_list): + """Method for convert values to int for use in IN (1,2,3) sql sentences. + + Args: + in_list (list): List with numbers items. + + Returns: + sql_filtered (str): with (1,2,3) sql sentence filtered. + + """ for x in range(0, len(in_list)): try: @@ -939,6 +1208,15 @@ class WebModel: return '('+', '.join(in_list)+')' def check_in_list_str(self, field, in_list): + """Method for convert values to int for use in IN (value1, value2, value3) sql sentences. + + Args: + field (PhangoField): The PhangoField used for check the values of in_list + in_list (list): List with value items. + + Returns: + sql_filtered (str): (value1, value2, value3) sql sentence filtered. + """ for x in range(0, len(in_list)): in_list[x]=str(self.fields[field].check(in_list[x])) @@ -946,6 +1224,15 @@ class WebModel: return '("'+'", "'.join(in_list)+'")' def set_order(self, order:dict) -> object: + """ Method for set and complete the query with "order by" sentences. + + Args: + order (dict): A dict with a field name how key, and 0 or 1 how values. 0 define order how ASC, 1 define order how DESC. + + Returns: + + Returns the same object for execute a query after set_order declaration. + """ arr_order=[] arr_order.append('ASC') @@ -964,6 +1251,15 @@ class WebModel: return self def set_limit(self, limit: tuple) -> None: + """ Method for set and complete the query with "limit" sentences. + + Args: + limit (tuple): A tuple with one or two elements. If one element, example (1), the result is "LIMIT first_element", if two elements, example (1,2), the result is "LIMIT first_element, two_element" + + Returns: + + Returns the same object for execute a query after set_order declaration. + """ limit[0]=int(limit[0]) @@ -979,6 +1275,7 @@ class WebModel: # Method for create sql tables def create_table(self): + """Method for create a table from this model object""" #self.connect_to_db() @@ -996,13 +1293,13 @@ class WebModel: fields=self.fields for field, data in fields.items(): - table_fields.append('`'+field+'` '+data.get_type_sql()) + table_fields.append(self.field_quote+field+self.field_quote+' '+data.get_type_sql()) #Check if indexed if fields[field].indexed==True: - self.arr_sql_index[self.name][field]='CREATE INDEX `index_'+self.name+'_'+field+'` ON '+self.name+'(`'+field+'`);' + self.arr_sql_index[self.name][field]='CREATE INDEX '+self.field_quote+'index_'+self.name+'_'+field+self.field_quote+' ON '+self.name+'('+self.field_quote+field+self.field_quote+');' self.arr_sql_set_index[self.name][field]="" @@ -1010,20 +1307,20 @@ class WebModel: if fields[field].unique==True: - self.arr_sql_unique[self.name][field]='ALTER TABLE `'+self.name+'` ADD UNIQUE (`'+field+'`)' + self.arr_sql_unique[self.name][field]='ALTER TABLE '+self.field_quote+self.name+self.field_quote+' ADD UNIQUE ('+self.field_quote+field+self.field_quote+')' self.arr_sql_set_unique[self.name][field]="" if type(fields[field]).__name__=="ForeignKeyField": - self.arr_sql_index[self.name][field]='CREATE INDEX `index_'+self.name+'_'+field+'` ON '+self.name+'(`'+field+'`);' + self.arr_sql_index[self.name][field]='CREATE INDEX '+self.field_quote+'index_'+self.name+'_'+field+self.field_quote+' ON '+self.name+'('+self.field_quote+field+self.field_quote+');' table_related=fields[field].table_name id_table_related=fields[field].table_id - self.arr_sql_set_index[self.name][field]='ALTER TABLE `'+self.name+'` ADD CONSTRAINT `'+field+'_'+self.name+'IDX` FOREIGN KEY ( `'+field+'` ) REFERENCES `'+table_related+'` (`'+id_table_related+'`) ON DELETE CASCADE ON UPDATE CASCADE;' + self.arr_sql_set_index[self.name][field]='ALTER TABLE '+self.field_quote+self.name+self.field_quote+' ADD CONSTRAINT '+self.field_quote+field+'_'+self.name+'IDX'+self.field_quote+' FOREIGN KEY ( '+self.field_quote+field+self.field_quote+' ) REFERENCES '+self.field_quote+table_related+self.field_quote+' ('+self.field_quote+id_table_related+self.field_quote+') ON DELETE CASCADE ON UPDATE CASCADE;' - return "create table `"+self.name+"` (\n"+",\n".join(table_fields)+"\n) DEFAULT CHARSET=utf8;"; + return "create table "+self.field_quote+self.name+self.field_quote+" (\n"+",\n".join(table_fields)+"\n) DEFAULT CHARSET=utf8;"; def update_table(self, fields_to_add, fields_to_modify, fields_to_add_index, fields_to_add_constraint, fields_to_add_unique, fields_to_delete_index, fields_to_delete_unique, fields_to_delete_constraint, fields_to_delete): @@ -1033,40 +1330,40 @@ class WebModel: print("---Deleting index from "+field+" in "+self.name) - self.query('DROP INDEX `index_'+self.name+'_'+field+'` ON '+self.name, [], self.connection_id) + self.query('DROP INDEX '+self.field_quote+'index_'+self.name+'_'+field+self.field_quote+' ON '+self.name, [], self.connection_id) for field in fields_to_delete_unique: print("---Deleting unique from "+field+" in "+self.name) - self.query('DROP INDEX `'+field+'` ON '+self.name, [], self.connection_id) + self.query('DROP INDEX '+self.field_quote+field+self.field_quote+' ON '+self.field_quote+self.name+self.field_quote, [], self.connection_id) for field in fields_to_delete_constraint: print("---Deleting foreignkey from "+field+" in "+self.name) - self.query('ALTER TABLE `'+self.name+'` DROP FOREIGN KEY '+field+'_'+self.name+'IDX', [], self.connection_id) + self.query('ALTER TABLE '+self.field_quote+self.name+self.field_quote+' DROP FOREIGN KEY '+field+'_'+self.name+'IDX', [], self.connection_id) for field in fields_to_delete: print("---Deleting "+field+" from "+self.name) - self.query('ALTER TABLE `'+self.name+'` DROP `'+field+'`', [], self.connection_id) + self.query('ALTER TABLE '+self.field_quote+self.name+self.field_quote+' DROP '+self.field_quote+field+self.field_quote, [], self.connection_id) #Deleting indexes and constraints. #Obtain new fields for field in fields_to_modify: print("---Updating "+field+" in "+self.name) - self.query('ALTER TABLE `'+self.name+'` MODIFY `'+field+'` '+self.fields[field].get_type_sql(), [], self.connection_id) + self.query('ALTER TABLE '+self.field_quote+self.name+self.field_quote+' MODIFY '+self.field_quote+field+self.field_quote+' '+self.fields[field].get_type_sql(), [], self.connection_id) for field in fields_to_add: print("---Adding "+field+" in "+self.name) - self.query('ALTER TABLE `'+self.name+'` ADD `'+field+'` '+self.fields[field].get_type_sql(), [], self.connection_id) + self.query('ALTER TABLE '+self.field_quote+self.name+self.field_quote+' ADD '+self.field_quote+field+self.field_quote+' '+self.fields[field].get_type_sql(), [], self.connection_id) for field in fields_to_add_index: print("---Adding index to "+field+" in "+self.name) - self.query('CREATE INDEX `index_'+self.name+'_'+field+'` ON '+self.name+' (`'+field+'`);', [], self.connection_id) + self.query('CREATE INDEX '+self.field_quote+'index_'+self.name+'_'+field+self.field_quote+' ON '+self.name+' ('+self.field_quote+field+self.field_quote+');', [], self.connection_id) for field in fields_to_add_constraint: @@ -1076,20 +1373,26 @@ class WebModel: id_table_related=self.fields[field].table_id - self.query('ALTER TABLE `'+self.name+'` ADD CONSTRAINT `'+field+'_'+self.name+'IDX` FOREIGN KEY ( `'+field+'` ) REFERENCES `'+table_related+'` (`'+id_table_related+'`) ON DELETE CASCADE ON UPDATE CASCADE;', [], self.connection_id) + self.query('ALTER TABLE '+self.field_quote+self.name+self.field_quote+' ADD CONSTRAINT '+self.field_quote+field+'_'+self.name+'IDX'+self.field_quote+' FOREIGN KEY ( '+self.field_quote+field+self.field_quote+' ) REFERENCES '+self.field_quote+table_related+self.field_quote+' ('+self.field_quote+id_table_related+self.field_quote+') ON DELETE CASCADE ON UPDATE CASCADE;', [], self.connection_id) for field in fields_to_add_unique: print("---Adding unique to "+field+" in "+self.name) - self.query('ALTER TABLE `'+self.name+'` ADD UNIQUE (`'+field+'`)', [], self.connection_id) + self.query('ALTER TABLE '+self.field_quote+self.name+self.field_quote+' ADD UNIQUE ('+self.field_quote+field+self.field_quote+')', [], self.connection_id) # Method for drop sql tables and related def drop(self): - return self.query('DROP TABLE '+self.name, [], self.connection_id) + """Method for drop a table based in this model + + Returns: + sql_str (str): Return the sql query for drop the table represented by this model + """ + + return self.query('DROP TABLE '+self.field_quote+self.name+self.field_quote, [], self.connection_id) #Return an array with all fields @@ -1099,6 +1402,20 @@ class WebModel: #Check of all fields in table. def check_all_fields(self, dict_values, external_agent, yes_update=False, errors_set="insert"): + """Method for check all fields of a model for insert or update a row in table db. + + Args: + dict_values (dict): The dict of values to check + external_agent (bool): If True, the query is considered manipulated by external agent and the checks are stricts, if not, checks are not stricts + yes_update (bool): If True, the check need be done for update sql sentence, if False, the check is done for insert sql sentence + errors_set (str): If insert value, the errors are set for insert sql statement, if update value, then the errors are set for update sql statement. + + Returns: + wrong (bool): Return False if checking is wrong. If not False returns a tuple with fields filtered, original values as values and values filtered how update_values + fields (list): list with fields + values (dict): dict with values + update_values (dict): dict with updated values with checking + """ fields=[] values=[] @@ -1115,7 +1432,7 @@ class WebModel: error=False if yes_update==True: - f_update=lambda field, value: "`"+field+"`=%s" + f_update=lambda field, value: self.field_quote+field+self.field_quote+"=%s" else: f_update=lambda field, value: "" @@ -1227,6 +1544,7 @@ class WebModel: #Reset the require field in fields def reset_require(self): + """Reset the require attribute in fields""" for k, v in self.fields.items(): @@ -1237,6 +1555,7 @@ class WebModel: #Reload the require field in fields def reload_require(self): + """Reload the require field in fields""" for k,r in self.fields.items(): self.fields[k].required=r @@ -1244,6 +1563,7 @@ class WebModel: #Choose all fields to updated def set_valid_fields(self, fields={}): + """Choose all fields to updated""" if len(fields)==0: fields=self.fields.keys() @@ -1253,6 +1573,7 @@ class WebModel: #Create a form based in table. def create_forms(self, arr_fields=[]): + """Create a form based in table.""" self.forms=OrderedDict() @@ -1270,6 +1591,12 @@ class WebModel: return arr_fields def create_form_after(self, form_after, new_form): + """Create form after other form + + Args: + form_after (str): The name of the form where the new form is located next + new_form (BaseForm): The BaseForm or derivated class used for create the new form. + """ new_dict=OrderedDict() @@ -1281,6 +1608,11 @@ class WebModel: self.forms=new_dict def show_errors(self): + """Get all errors of model last operation. + + Returns: + error_txt (str): A string with all errors. + """ arr_error=[] error_txt='' @@ -1301,6 +1633,11 @@ class WebModel: return error_txt def collect_errors(self): + """Get all errors and save in dictionary + + Returns: + errors (dict): Return a dict where the key is the field where the error exists and value is the error text. + """ arr_error= {} error_txt='' @@ -1318,12 +1655,15 @@ class WebModel: return arr_error def safe_query(self): + """Method for reset require for fields. + With this method you can make queries without real checks, except mysql injection safe variables.""" self.create_forms() self.reset_require() def close(self): + """Method for close sqlclass db connection""" self.sqlclass.close() @@ -1341,6 +1681,7 @@ class WebModel: #del sqlclass.connection[key] @staticmethod def escape_sql(value): + """Manual escape for sql, you shouldn't use it""" value=str(value) @@ -1354,6 +1695,11 @@ class WebModel: # Set post values from a post array def set_post_values(self, post): + """Prepare a dict with values using fields keys how base + + Returns: + post (dict): Return a dict with values without checking anything. + """ for k in self.fields.keys(): @@ -1373,6 +1719,6 @@ class QueryModel(WebModel): self.label_general=self.name - self.order_by="ORDER BY `"+self.name+"`.`id` ASC" + self.order_by="ORDER BY "+self.field_quote+self.name+self.field_quote+"."+self.field_quote+"id"+self.field_quote+" ASC" diff --git a/paramecio/citoplasma/error_reporting.py b/paramecio/libraries/error_reporting.py similarity index 95% rename from paramecio/citoplasma/error_reporting.py rename to paramecio/libraries/error_reporting.py index 04b75de..f6fc2ae 100644 --- a/paramecio/citoplasma/error_reporting.py +++ b/paramecio/libraries/error_reporting.py @@ -3,7 +3,7 @@ # A bottle plugin for send emails if from settings import config -from paramecio.citoplasma.sendmail import SendMail +from paramecio.libraries.sendmail import SendMail import sys, traceback email_failed='' diff --git a/paramecio/citoplasma/filesize.py b/paramecio/libraries/filesize.py similarity index 100% rename from paramecio/citoplasma/filesize.py rename to paramecio/libraries/filesize.py diff --git a/paramecio/citoplasma/filterip.py b/paramecio/libraries/filterip.py similarity index 100% rename from paramecio/citoplasma/filterip.py rename to paramecio/libraries/filterip.py diff --git a/paramecio/citoplasma/generate_admin_class.py b/paramecio/libraries/generate_admin_class.py similarity index 94% rename from paramecio/citoplasma/generate_admin_class.py rename to paramecio/libraries/generate_admin_class.py index f0a46dc..ec2e8da 100644 --- a/paramecio/citoplasma/generate_admin_class.py +++ b/paramecio/libraries/generate_admin_class.py @@ -1,10 +1,10 @@ -from paramecio.citoplasma.lists import SimpleList +from paramecio.libraries.lists import SimpleList from bottle import request -from paramecio.citoplasma.urls import add_get_parameters, redirect -from paramecio.citoplasma.mtemplates import set_flash_message -from paramecio.cromosoma.formsutils import show_form -from paramecio.citoplasma.i18n import I18n -from paramecio.citoplasma.httputils import GetPostFiles +from paramecio.libraries.urls import add_get_parameters, redirect +from paramecio.libraries.mtemplates import set_flash_message +from paramecio.libraries.db.formsutils import show_form +from paramecio.libraries.i18n import I18n +from paramecio.libraries.httputils import GetPostFiles from collections import OrderedDict class GenerateAdminClass: @@ -49,6 +49,8 @@ class GenerateAdminClass: self.url_redirect=self.url + self.pre_update=None + self.post_update=None self.text_home=I18n.lang('common', 'home', 'Home') @@ -126,6 +128,9 @@ class GenerateAdminClass: title_edit=I18n.lang('common', 'edit_new_item', 'Edit item') self.model.conditions=['WHERE `'+self.model.name+'`.`'+self.model.name_field_id+'`=%s', [getpostfiles.get['id']]] + if self.pre_update: + getpostfiles.post=self.pre_update(self, getpostfiles.post) + if insert_row(getpostfiles.post): set_flash_message(I18n.lang('common', 'task_successful', 'Task successful')) @@ -209,6 +214,8 @@ class GenerateConfigClass: self.template_insert='utils/insertform.phtml' + self.pre_update=None + self.post_update=None self.text_home=I18n.lang('common', 'home', 'Home') diff --git a/paramecio/citoplasma/gunicornssl.py b/paramecio/libraries/gunicornssl.py similarity index 100% rename from paramecio/citoplasma/gunicornssl.py rename to paramecio/libraries/gunicornssl.py diff --git a/paramecio/citoplasma/hierarchy_links.py b/paramecio/libraries/hierarchy_links.py similarity index 98% rename from paramecio/citoplasma/hierarchy_links.py rename to paramecio/libraries/hierarchy_links.py index 569b2bb..f24f997 100644 --- a/paramecio/citoplasma/hierarchy_links.py +++ b/paramecio/libraries/hierarchy_links.py @@ -1,6 +1,6 @@ #/usr/bin/env python3 -from paramecio.citoplasma.urls import add_get_parameters +from paramecio.libraries.urls import add_get_parameters class HierarchyLinks: diff --git a/paramecio/citoplasma/httputils.py b/paramecio/libraries/httputils.py similarity index 83% rename from paramecio/citoplasma/httputils.py rename to paramecio/libraries/httputils.py index 93ab337..4a9769f 100644 --- a/paramecio/citoplasma/httputils.py +++ b/paramecio/libraries/httputils.py @@ -2,12 +2,12 @@ import json, re from bottle import request, response -from paramecio.citoplasma.sessions import get_session -from paramecio.citoplasma.keyutils import create_key_encrypt +from paramecio.libraries.sessionplugin import get_session +from paramecio.libraries.keyutils import create_key_encrypt from bottle import HTTPResponse no_csrf=False -change_csrf=False +change_csrf=True try: @@ -84,15 +84,22 @@ class GetPostFiles: self.post={} - try: + if not request.json: - self.post=request.forms.decode('utf-8') + try: - except: - - request.forms.recode_unicode=False - self.post=request.forms.decode('utf-8') + self.post=request.forms.decode('utf-8') + + except: + + request.forms.recode_unicode=False + self.post=request.forms.decode('utf-8') + + else: + self.post=request.json + + #print(self.post.keys()) if len(required_post)==0: required_post=self.post.keys() @@ -100,20 +107,20 @@ class GetPostFiles: self.post[post]=self.post.get(post, '') s=get_session() - + #print('s', s) if ignore_csrf_token==False and no_csrf==False: if 'csrf_token' in s: self.post['csrf_token']=self.post.get('csrf_token', '') - + if self.post['csrf_token']!=s['csrf_token'] or self.post['csrf_token'].strip()=="": # Clean csrf_token - del s['csrf_token'] + #del s['csrf_token'] - s.save() + #s.save() #raise NameError('Error: you need a valid csrf_token') raise HTTPResponse(body=json.dumps({'error_csrf': 1, 'error': 1, 'token_invalid': 1}), status=200, headers={'Content-type': 'application/json'}) @@ -124,7 +131,7 @@ class GetPostFiles: del s['csrf_token'] - s.save() + #s.save() else: @@ -160,8 +167,8 @@ def check_csrf(post): del s['csrf_token'] - s.save() + #s.save() else: #raise NameError('Error: you don\'t send any valid csrf_token') - raise HTTPResponse(body=json.dumps({'error_csrf': 1, 'error': 1, 'token_invalid': 0}), status=200, headers={'Content-type': 'application/json'}) + raise HTTPResponse(body=json.dumps({'error_csrf': 1, 'error': 1, 'message': 'Error: csrf token invalid', 'token_invalid': 0}), status=200, headers={'Content-type': 'application/json'}) diff --git a/paramecio/libraries/i18n.py b/paramecio/libraries/i18n.py new file mode 100644 index 0000000..7733b17 --- /dev/null +++ b/paramecio/libraries/i18n.py @@ -0,0 +1,212 @@ +#!/usr/bin/env python3 + +""" +Parameciofm is a series of wrappers for bottle.py, mako and others and construct a simple headless cms. + +Copyright (C) 2024 Antonio de la Rosa Caballero + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +""" + +from importlib import import_module +from paramecio.libraries.sessionplugin import get_session +import json +from bottle import request +import gettext +import os + +yes_session=False + +i18n_module={} + +def load_lang(*args): + """A function for load the lang module dinamically + """ + + for module in args: + + lang_path=module[0]+'.i18n.'+module[1] + + try: + + i18n_module[lang_path]=import_module(lang_path) + + pass + + except: + pass + + # here load the language + +class PGetText: + + # Dict where all gettext domain are saved -> domain=name, example, admin, libraries, pastafari2, etc... + + l={} + + def __init__(self, module_file): + + module_dir=os.path.dirname(os.path.realpath(module_file)) + + module_name=os.path.basename(module_dir) + + if module_name not in PGetText.l: + + PGetText.l[module_name]={} + + for i in I18n.dict_i18n: + + if i not in PGetText.l[module_name]: + + PGetText.l[module_name][i]=gettext.translation(module_name, module_dir+'/languages/', languages=[i], fallback=True) + PGetText.l[module_name][i].install() + + self.module=module_name + + def gettext(self, text): + + return PGetText.l[self.module][I18n.get_default_lang()].gettext(text) + +def pgettext(module_file): + + module=os.path.dirname(os.path.realpath(module_file)) + + base_name=os.path.dirname(os.path.realpath(module)) + + l=gettext.translation(os.path.basename(base_name), module+'/languages/', languages=I18n.get_default_lang(), fallback=True) + + return l.gettext + +class I18n: + + """Class for i18n tasks + + Class for i18n tasks, how, strings for every language supported, for now are en-US and es-ES. You can add more languages adding + + Attributes: + default_lang (str): The default string lang used when get someone + dict_i18n (list): The list with default languages. You can add more calling it static variable in settings/config.py + + """ + + default_lang='en-US' + + dict_i18n=['en-US', 'es-ES'] + + l={} + + def __init__(self, module): + + self.module=module + + def slang(self, symbol, text_default, lang=None): + """Method for get a string from selected language but object oriented + + Method for get a string from selected language but object oriented + + Args: + symbol (str): The symbol used for identify the text string. + text_default (str): The text default used. You have use how base for translations. + """ + return I18n.lang(self.module, symbol, text_default, lang) + + def tlang(self, text_default, lang=None): + """Method for get a string from selected language but object oriented and using module and symbol by default + + Method for get a string from selected language but object oriented and using module and symbol by default + + Args: + symbol (str): The symbol used for identify the text string. + text_default (str): The text default used. You have use how base for translations. + """ + + symbol=text_default[:60] + + return I18n.lang(self.module, symbol, text_default, lang) + + + #@staticmethod + #def set_lang(code_lang): + # if default_lang + + + @staticmethod + def get_default_lang(): + """Static method for get the default lang""" + + lang=I18n.default_lang + + s=get_session() + + lang=s.get('lang', lang) + + return lang + + @staticmethod + def lang(module, symbol, text_default, lang=None): + """Static method for get a string from selected language + + Static method used to get the string of the selected language. If there is no string in the selected language, it returns text_default. + + Args: + module (str): The module to which the translation string belongs + symbol (str): Simple symbol that is useful for identify the string + text_default (str): The text used by default when there are not translation in the selected language + """ + + if not lang: + lang=I18n.get_default_lang() + + I18n.l[lang]=I18n.l.get(lang, {}) + + I18n.l[lang][module]=I18n.l[lang].get(module, {}) + + I18n.l[lang][module][symbol]=I18n.l[lang][module].get(symbol, text_default) + + return I18n.l[lang][module][symbol] + + @staticmethod + def extract_value(value): + """Static method for get values from json lang array + + Args: + value (json): Lang dict in json format + """ + + value=json.loads(value) + + lang=I18n.get_default_lang() + + if value[lang]!='': + + return value[lang] + + return value[I18n.default_lang] + + @staticmethod + def get_browser_lang(): + + return request.headers.get('Accept-Language', 'en-US') + + @staticmethod + def lang_json(module, symbol, text_default): + + arr_final={} + + for l in I18n.dict_i18n: + arr_final[l]=I18n.lang(module, symbol, text_default, l) + + return json.dumps(arr_final) + +common_pgettext=PGetText(__file__) diff --git a/paramecio/citoplasma/js.py b/paramecio/libraries/js.py similarity index 71% rename from paramecio/citoplasma/js.py rename to paramecio/libraries/js.py index 04b3ed7..230ef17 100644 --- a/paramecio/citoplasma/js.py +++ b/paramecio/libraries/js.py @@ -1,5 +1,5 @@ -from paramecio.citoplasma.sessions import get_session -from paramecio.citoplasma.i18n import I18n +from paramecio.libraries.sessions import get_session +from paramecio.libraries.i18n import I18n from settings import config def make_js_url(file_path, module): diff --git a/paramecio/libraries/keyutils.py b/paramecio/libraries/keyutils.py new file mode 100644 index 0000000..5bc3c66 --- /dev/null +++ b/paramecio/libraries/keyutils.py @@ -0,0 +1,86 @@ +""" +Parameciofm is a series of wrappers for bottlepy, mako and others and construct a simple headless cms. + +Copyright (C) 2024 Antonio de la Rosa Caballero + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +""" + +from hashlib import sha512, sha256 +from base64 import b64encode +from os import urandom +import string +import secrets + +# Functions for create random strings usando urandom + +def create_key_encrypt(n=10): + """ Simple function for create a random string + + Simple function for create a random string based in sha512 + + Args: + n (int): size of string random bytes (view urandom function in Python3 Help) + """ + + return sha512(urandom(n)).hexdigest() + +def create_key_encrypt_256(n=10): + + """ Simple function for create a random string + + Simple function for create a random string based in sha256 + + Args: + n (int): size of string random bytes (view urandom function in Python3 Help) + """ + + return sha256(urandom(n)).hexdigest() + +def create_key(n=10): + + """ Simple function for create a random string + + Simple function for create a random string based in urandom function and base64 encoding + + Args: + n (int): size of string random bytes (view urandom function in Python3 Help) + """ + + rand_bytes=urandom(n) + + return b64encode(rand_bytes).decode('utf-8')[0:-2] + +def create_simple_password(n=14): + + """ Based in python3 documentation for create passwords using secrets module + + https://docs.python.org/3/library/secrets.html + + Args: + n (int): Number of random elements of the password + + """ + + password='' + + alphabet=string.ascii_letters+string.digits+string.punctuation + + while True: + password=''.join(secrets.choice(alphabet) for i in range(n)) + if (any(c.islower() for c in password) and any(c.isupper() for c in password) and sum(c.isdigit() for c in password) >= 3): + break + + return password + diff --git a/paramecio/citoplasma/lists.py b/paramecio/libraries/lists.py similarity index 97% rename from paramecio/citoplasma/lists.py rename to paramecio/libraries/lists.py index dc51a19..93cf2aa 100644 --- a/paramecio/citoplasma/lists.py +++ b/paramecio/libraries/lists.py @@ -1,14 +1,14 @@ #By default id is not showed -from paramecio.citoplasma.pages import Pages -from paramecio.citoplasma.urls import add_get_parameters -from paramecio.citoplasma.sessions import get_session -from paramecio.citoplasma.i18n import I18n -from paramecio.citoplasma.httputils import GetPostFiles +from paramecio.libraries.pages import Pages +from paramecio.libraries.urls import add_get_parameters +from paramecio.libraries.sessions import get_session +from paramecio.libraries.i18n import I18n +from paramecio.libraries.httputils import GetPostFiles from bottle import request import sys import re -from paramecio.citoplasma.pages import Pages +from paramecio.libraries.pages import Pages class SimpleList: diff --git a/paramecio/citoplasma/mtemplates.py b/paramecio/libraries/mtemplates.py similarity index 66% rename from paramecio/citoplasma/mtemplates.py rename to paramecio/libraries/mtemplates.py index 8327d20..fc054ae 100644 --- a/paramecio/citoplasma/mtemplates.py +++ b/paramecio/libraries/mtemplates.py @@ -1,22 +1,58 @@ #!/usr/bin/python +""" +Parameciofm is a series of wrappers for Bottle.py, mako and others and construct a simple headless cms. + +Copyright (C) 2024 Antonio de la Rosa Caballero + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +""" + from bottle import hook from mako.template import Template from mako.lookup import TemplateLookup -from paramecio.citoplasma.urls import make_url, make_url_domain, make_media_url, make_media_url_module, add_get_parameters -from paramecio.citoplasma.i18n import I18n -from paramecio.citoplasma.sessions import get_session -from paramecio.citoplasma.adminutils import make_admin_url -from paramecio.cromosoma.formsutils import csrf_token -from paramecio.citoplasma.js import make_js_url +from paramecio.libraries.urls import make_url, make_url_domain, make_media_url, make_media_url_module, add_get_parameters +from paramecio.libraries.i18n import I18n, PGetText +from paramecio.libraries.sessionplugin import get_session +from paramecio.libraries.adminutils import make_admin_url +from paramecio.libraries.db.formsutils import csrf_token +from paramecio.libraries.js import make_js_url from settings import config from os import path from collections import OrderedDict +from paramecio.wsgiapp import app # Preparing envs for views of modules, and views of def env_theme(module, cache_enabled=True, cache_impl='', cache_args={}, module_directory="./tmp/modules"): + """Function for create an environment for mako templates + + Function for create an environment for mako templates. Really is a shortcut for TemplateLookup mako function. You can define cache options and module_directory for templates compiled + + Args: + module (str): The module where the templates can be founded + cache_enabled (boolean): If True then mako template cache is enabled, is False, mako template cache is disabled. + cache_args (dict): Cache Args dict parameter for TemplateLookup function from Mako templates. View Mako Templates documentation. + module_directory (str): Module directory parameter for TemplateLookup function from Mako templates. View Mako Templates documentation. + + Returns: + + template (TemplateLookup): Return TemplateLookup object + + """ + ext=module[len(module)-3:] if ext=='.py': @@ -50,12 +86,33 @@ def preload_templates(template_files, env): return templates +def url_for(name, **kwargs): + + return app.get_url(name, **kwargs) + class PTemplate: + """A class used how shortcuts for Mako template functions. + """ templates_loaded={} def __init__(self, environment): + """A class used how shortcuts for Mako template functions. + + This class is used to have a set of shortcuts and hooks to Mako templates functions and methods over a series of default options. + + Args: + environment (TemplateLookup): A TemplateLookup object generated with env_theme function + + Attributes: + autoescape_ext (set): A set of extensions file where automatic autoescape is used + environment (TemplateLookup): A TemplateLookup object generated with env_theme function + filters (list): A list of functions used for add filters to your templates. + js (list): A list of javascript sources for generate js html load tags. + + """ + # A simple method used in internal things of paramecio self.show_basic_template=True @@ -130,6 +187,32 @@ class PTemplate: self.env=environment + # Loading language domain for gettext in templates + + base_name=path.dirname(path.realpath(__file__)) + + module_env=self.env.directories[1].replace('/templates', '') + + self.l=PGetText(module_env+'/app.py') + + self.add_filter(self._) + + self.i18n=I18n(base_name) + + self.add_filter(self.i18n.slang) + + self.add_filter(self.i18n.tlang) + + #self.url_for=lambda name: app.get_url(name) + #x = lambda a : a + 10 + #print(self.url_for) + + self.add_filter(url_for) + + def _(self, text): + + return self.l.gettext(text) + #self.auto_reload=True # Clean HeaderHTML @@ -144,6 +227,12 @@ class PTemplate: def guess_autoescape(self, template_name): + """Simple helper method for get an extension from filename + + Args: + template_name (str): The template name + """ + if template_name is None or '.' not in template_name: return False @@ -180,6 +269,18 @@ class PTemplate: def load_template(self, template_file, **arguments): + """Load a mako template and return the result + + Load a mako template and return the results with different arguments applied + + Args: + template_file (str): The name of template file. The template is searched using configuration defined in self.env + **arguments (mixed): Extra arguments with variables passed to template + + Returns: + template (str): Return a template rendered using mako class from self.env + """ + template = self.env.get_template(template_file) arguments.update(self.filters) @@ -190,6 +291,18 @@ class PTemplate: def render_template(self, template_file, **arguments): + """Experimental method for parse a template + + Experimental method for parse a template, similar to load_template but try cache the template loaded + + Args: + template_file (str): The name of template file. The template is searched using configuration defined in self.env + **arguments (mixed): Extra arguments with variables passed to template + + Returns: + dummy (str): Dummy return necessary because mako expect return something + """ + if not str(self.env.directories)+'/'+template_file in PTemplate.templates_loaded: PTemplate.templates_loaded[str(self.env.directories)+'/'+template_file]=self.env.get_template(template_file) @@ -335,7 +448,7 @@ class HeaderHTML: s['flash']='' - s.save() + #s.save() return message @@ -345,7 +458,7 @@ def set_flash_message(message): s['flash']=message - s.save() + #s.save() def qf(text): diff --git a/paramecio/citoplasma/pages.py b/paramecio/libraries/pages.py similarity index 93% rename from paramecio/citoplasma/pages.py rename to paramecio/libraries/pages.py index d0f34d8..33a68a6 100644 --- a/paramecio/citoplasma/pages.py +++ b/paramecio/libraries/pages.py @@ -1,8 +1,8 @@ #!/usr/bin/env python3 from math import ceil, floor -from paramecio.citoplasma.urls import add_get_parameters -from paramecio.citoplasma.i18n import I18n +from paramecio.libraries.urls import add_get_parameters +from paramecio.libraries.i18n import I18n class Pages: diff --git a/paramecio/citoplasma/plugins.py b/paramecio/libraries/plugins.py similarity index 95% rename from paramecio/citoplasma/plugins.py rename to paramecio/libraries/plugins.py index 8573ace..c5c5756 100644 --- a/paramecio/citoplasma/plugins.py +++ b/paramecio/libraries/plugins.py @@ -1,8 +1,8 @@ from paramecio.modules.admin.models.admin import UserAdmin from bottle import request -from paramecio.citoplasma.sessions import get_session -from paramecio.citoplasma.urls import redirect, make_url -from paramecio.cromosoma.webmodel import WebModel +from paramecio.libraries.sessions import get_session +from paramecio.libraries.urls import redirect, make_url +from paramecio.libraries.db.webmodel import WebModel import inspect class LoginPlugin(object): diff --git a/paramecio/citoplasma/sendmail.py b/paramecio/libraries/sendmail.py similarity index 70% rename from paramecio/citoplasma/sendmail.py rename to paramecio/libraries/sendmail.py index 84f7374..588e7ae 100644 --- a/paramecio/citoplasma/sendmail.py +++ b/paramecio/libraries/sendmail.py @@ -1,4 +1,23 @@ #!/usr/bin/env python3 + +""" +Parameciofm is a series of wrappers for bottle.py, mako and others and construct a simple headless cms. + +Copyright (C) 2024 Antonio de la Rosa Caballero + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +""" import os import smtplib import mimetypes @@ -13,6 +32,8 @@ import ssl as ssl_module import sys class SendMail: + """Class for send email + """ port=587 @@ -25,6 +46,20 @@ class SendMail: #ssl=True def __init__(self, ssl=True): + """Class for send email + + Class for send email using standard python library + + Attributes: + port (int): The port used for send email, by default 587 + host (str): The hostname of mail server used for send the email + username (str): The username for login in mail server + password (str): The password for login in mail server + smtp (smtplib.SMTP): The python SMTP object used for send emails + txt_error: (str): If error, is saved in this attribute + + """ + self.smtp=None #smtplib.SMTP(host=self.host, port=self.port) self.txt_error='' @@ -79,9 +114,9 @@ class SendMail: return False - except smtplib.SMTPAuthenticationError: + except smtplib.SMTPAuthenticationError as eauth: - self.txt_error='Error: cannot login. Wrong username or password' + self.txt_error='Error: cannot login. Wrong username or password '+eauth.__str__() return False @@ -94,6 +129,18 @@ class SendMail: return True def send(self, from_address, to_address: list, subject, message, content_type='plain', attachments=[]): + """ Method that send email + + With this method you can send email to multiple address, html, and add attachments to email + + Args: + from_address (str): The adress used for send the email + to_address (list): A list of emails where the email will be sended. + subject (str): The subject of the email + message (str): The content of the message + content_type (str): The type of mail content, can be plain or html. + attachments (list): A list with a serie of file paths for attach to the email. + """ if self.smtp==None: if not self.connect(): @@ -174,12 +221,14 @@ class SendMail: return True def quit(self): + """Function used when you need quit connection for any reason""" if self.smtp!=None: self.smtp.quit() self.smtp=None def __del__(self): + """Method for clean the connection when the object is closed""" if self.smtp!=None: diff --git a/paramecio/libraries/sessionplugin.py b/paramecio/libraries/sessionplugin.py new file mode 100644 index 0000000..ba4ea7d --- /dev/null +++ b/paramecio/libraries/sessionplugin.py @@ -0,0 +1,159 @@ +from bottle import request, response +from itsdangerous.url_safe import URLSafeTimedSerializer +from paramecio.libraries.keyutils import create_key_encrypt, create_key_encrypt_256, create_key + +try: + + from settings import config + +except: + + class config: + cookie_name='paramecio.session' + key_encrypt=create_key_encrypt_256(30) + session_opts={'session.data_dir': 'sessions', 'session.type': 'file', 'session.path': 'paramecio'} + +import inspect + + + +class Session(dict): + + def __init__(self, *args, **kwargs): + + self.update(*args, **kwargs) + + self.changed=False + + def __setitem__(self, item, value): + + super(Session, self).__setitem__(item, value) + self.changed=True + +def get_session(): + + return request.environ.get('session', {}) + +def session_plugin(callback): + + def wrapper(*args, **kwargs): + + cookie=request.get_cookie(config.cookie_name) + + safe=None + + if not cookie: + session=Session() + else: + + safe=URLSafeTimedSerializer(config.key_encrypt) + try: + session=Session(safe.loads(cookie)) + + if type(session).__name__!='Session': + session=Session() + + except: + session=Session() + + if 'session' in kwargs: + kwargs['session']=session + + #For compatibility with old sessions server-side style. + + request.environ['session']=session + + rv=callback(*args, **kwargs) + + if session.changed: + #print('changed') + if not safe: + safe=URLSafeTimedSerializer(config.key_encrypt) + + #if not max_age: + response.set_cookie(config.cookie_name, safe.dumps(session), path=config.session_opts['session.path'], httponly=True) + + return rv + + return wrapper + +class SessionPlugin(object): + + name = 'session' + api = 2 + + def __init__(self, keyword='session'): + + self.keyword=keyword + + + def setup(self, app): + ''' Make sure that other installed plugins don't affect the same keyword argument.''' + for other in app.plugins: + if not isinstance(other, SessionPlugin): continue + if other.keyword == self.keyword: + raise PluginError("Found another login plugin with "\ + "conflicting settings (non-unique keyword).") + + def apply(self, callback, context): + + # Test if the original callback accepts a 'session' keyword. + # Ignore it if it does not need a login handle. + + conf = context.config.get('session') or {} + + keyword = conf.get('keyword', self.keyword) + + args = inspect.getfullargspec(context.callback)[0] + + """ + if keyword not in args: + return callback + """ + + def wrapper(*args, **kwargs): + + cookie=request.get_cookie(config.cookie_name) + + safe=None + + if not cookie: + session=Session() + else: + + safe=URLSafeTimedSerializer(config.key_encrypt) + try: + session=Session(safe.loads(cookie)) + + if type(session).__name__!='Session': + session=Session() + + except: + session=Session() + + #if 'session' in kwargs: + kwargs['session']=session + + #For compatibility with old sessions server-side style. + + request.environ['session']=session + + rv=callback(*args, **kwargs) + + if session.changed: + #print('changed') + if not safe: + safe=URLSafeTimedSerializer(config.key_encrypt) + + #if not max_age: + response.set_cookie(config.cookie_name, safe.dumps(session), path=config.session_opts['session.path'], httponly=True) + + return rv + + return wrapper + + + + + + diff --git a/paramecio/citoplasma/sessions.py b/paramecio/libraries/sessions.py similarity index 76% rename from paramecio/citoplasma/sessions.py rename to paramecio/libraries/sessions.py index 3d41064..bf9938f 100644 --- a/paramecio/citoplasma/sessions.py +++ b/paramecio/libraries/sessions.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 -from paramecio.citoplasma.keyutils import create_key_encrypt, create_key_encrypt_256, create_key -from oslo_concurrency import lockutils +from paramecio.libraries.keyutils import create_key_encrypt, create_key_encrypt_256, create_key +#from oslo_concurrency import lockutils try: @@ -15,7 +15,7 @@ except: key_encrypt=create_key_encrypt_256(30) session_opts={'session.data_dir': 'sessions', 'session.type': 'file', 'session.path': 'paramecio'} -from itsdangerous import JSONWebSignatureSerializer +#from itsdangerous import JSONWebSignatureSerializer from bottle import request, response import os try: @@ -142,40 +142,8 @@ elif config.session_opts['session.type']=='redis': import redis - def load_session(token): - - s={} - - r=redis.StrictRedis(host=config.session_opts['session.host'], port=config.session_opts['session.port'], db=config.session_opts['session.db']) - - value=r.get(token) - - if not value: - s={'token': token} - else: - try: - s=json.loads(value.decode('utf-8')) - except: - s={'token': token} - return s - - def save_session(token, session): - - r=redis.StrictRedis(host=config.session_opts['session.host'], port=config.session_opts['session.port'], db=config.session_opts['session.db']) - - r.set(token, json.dumps(session)) - - def after_session(): - pass - -else: - def generate_session(session={}, max_age=None): - #secret=URLSafeSerializer(config.key_encrypt) - - #session=secret.dumps(session) - token=create_key(30).replace('/', '#') s={'token': token} @@ -183,9 +151,76 @@ else: # Bug in python 3.6, if you put max_age how None, is passed to header cookie. if not max_age: - response.set_cookie(config.cookie_name, token, path=config.session_opts['session.path']) + response.set_cookie(config.cookie_name, token, path=config.session_opts['session.path'], httponly=True) else: - response.set_cookie(config.cookie_name, token, path=config.session_opts['session.path'], max_age=max_age) + response.set_cookie(config.cookie_name, token, path=config.session_opts['session.path'], max_age=max_age, httponly=True) + + request.environ['session']=s + + save_session(token, s) + + request.environ['session']=s + + return s + + def regenerate_session(): + + token=create_key(30).replace('/', '#') + + s={'token': token} + + response.set_cookie(config.cookie_name, token, path=config.session_opts['session.path'], httponly=True) + + save_session(token, s) + + request.environ['session']=s + + return ParamecioSession(s) + + def load_session(token): + + s={} + + r=redis.Redis(host=config.session_opts['session.host'], port=config.session_opts['session.port'], decode_responses=True) + + value=r.get(token) + + r.close() + + if not value: + s={'token': token} + else: + try: + s=json.loads(value) + except: + s={'token': token} + return s + + def save_session(token, session): + + r=redis.Redis(host=config.session_opts['session.host'], port=config.session_opts['session.port']) + + r.set(token, json.dumps(session)) + + r.close() + + def after_session(): + pass + +else: + + def generate_session(session={}, max_age=None): + + token=create_key(30).replace('/', '#') + + s={'token': token} + + # Bug in python 3.6, if you put max_age how None, is passed to header cookie. + + if not max_age: + response.set_cookie(config.cookie_name, token, path=config.session_opts['session.path'], httponly=True) + else: + response.set_cookie(config.cookie_name, token, path=config.session_opts['session.path'], max_age=max_age, httponly=True) #Set-Cookie: phango_session=n2ro4lghim75p8vjseb5v3lhap; path=/experiment2/ #response.set_header('Set-Cookie', '%s=%s; path=%s' % (config.cookie_name, token, config.session_opts['session.path']) ) @@ -205,7 +240,7 @@ else: s={'token': token} - response.set_cookie(config.cookie_name, token, path=config.session_opts['session.path']) + response.set_cookie(config.cookie_name, token, path=config.session_opts['session.path'], httponly=True) file_session=config.session_opts['session.data_dir']+'/'+token+'_session' @@ -237,7 +272,7 @@ else: return s - @lockutils.synchronized('not_thread_safe') + #@lockutils.synchronized('not_thread_safe') def save_session(token, session, create_file=False): file_session=config.session_opts['session.data_dir']+'/'+token+'_session' diff --git a/paramecio/citoplasma/show_links.py b/paramecio/libraries/show_links.py similarity index 100% rename from paramecio/citoplasma/show_links.py rename to paramecio/libraries/show_links.py diff --git a/paramecio/citoplasma/slugify.py b/paramecio/libraries/slugify.py similarity index 100% rename from paramecio/citoplasma/slugify.py rename to paramecio/libraries/slugify.py diff --git a/paramecio/citoplasma/templates.py b/paramecio/libraries/templates.py similarity index 95% rename from paramecio/citoplasma/templates.py rename to paramecio/libraries/templates.py index 44138d4..0056670 100644 --- a/paramecio/citoplasma/templates.py +++ b/paramecio/libraries/templates.py @@ -1,9 +1,9 @@ #!/usr/bin/python from jinja2 import Template, Environment, FileSystemLoader -from paramecio.citoplasma.urls import make_url, make_media_url, make_media_url_module, add_get_parameters -from paramecio.citoplasma.i18n import I18n -from paramecio.citoplasma.sessions import get_session +from paramecio.libraries.urls import make_url, make_media_url, make_media_url_module, add_get_parameters +from paramecio.libraries.i18n import I18n +from paramecio.libraries.sessions import get_session from settings import config # Preparing envs for views of modules, and views of diff --git a/paramecio/citoplasma/templates/forms/colorform.phtml b/paramecio/libraries/templates/forms/colorform.phtml similarity index 94% rename from paramecio/citoplasma/templates/forms/colorform.phtml rename to paramecio/libraries/templates/forms/colorform.phtml index bb21e6d..9c2cbad 100644 --- a/paramecio/citoplasma/templates/forms/colorform.phtml +++ b/paramecio/libraries/templates/forms/colorform.phtml @@ -1,4 +1,3 @@ -${add_js_home_local('jquery.min.js', 'admin')} ${add_js_home_local('spectrum.js', 'admin')} ${add_css_home_local('spectrum.css', 'admin')} <% diff --git a/paramecio/citoplasma/templates/forms/dateform.phtml b/paramecio/libraries/templates/forms/dateform.phtml similarity index 100% rename from paramecio/citoplasma/templates/forms/dateform.phtml rename to paramecio/libraries/templates/forms/dateform.phtml diff --git a/paramecio/citoplasma/templates/forms/fileform.phtml b/paramecio/libraries/templates/forms/fileform.phtml similarity index 100% rename from paramecio/citoplasma/templates/forms/fileform.phtml rename to paramecio/libraries/templates/forms/fileform.phtml diff --git a/paramecio/citoplasma/templates/forms/i18nform.phtml b/paramecio/libraries/templates/forms/i18nform.phtml similarity index 100% rename from paramecio/citoplasma/templates/forms/i18nform.phtml rename to paramecio/libraries/templates/forms/i18nform.phtml diff --git a/paramecio/citoplasma/templates/forms/modelform.html b/paramecio/libraries/templates/forms/modelform.html similarity index 100% rename from paramecio/citoplasma/templates/forms/modelform.html rename to paramecio/libraries/templates/forms/modelform.html diff --git a/paramecio/citoplasma/templates/forms/modelform.phtml b/paramecio/libraries/templates/forms/modelform.phtml similarity index 100% rename from paramecio/citoplasma/templates/forms/modelform.phtml rename to paramecio/libraries/templates/forms/modelform.phtml diff --git a/paramecio/citoplasma/templates/forms/texthtmlform.phtml b/paramecio/libraries/templates/forms/texthtmlform.phtml similarity index 100% rename from paramecio/citoplasma/templates/forms/texthtmlform.phtml rename to paramecio/libraries/templates/forms/texthtmlform.phtml diff --git a/paramecio/citoplasma/templates/utils/admin.phtml b/paramecio/libraries/templates/utils/admin.phtml similarity index 100% rename from paramecio/citoplasma/templates/utils/admin.phtml rename to paramecio/libraries/templates/utils/admin.phtml diff --git a/paramecio/citoplasma/templates/utils/insertform.phtml b/paramecio/libraries/templates/utils/insertform.phtml similarity index 100% rename from paramecio/citoplasma/templates/utils/insertform.phtml rename to paramecio/libraries/templates/utils/insertform.phtml diff --git a/paramecio/citoplasma/templates/utils/list.phtml b/paramecio/libraries/templates/utils/list.phtml similarity index 100% rename from paramecio/citoplasma/templates/utils/list.phtml rename to paramecio/libraries/templates/utils/list.phtml diff --git a/paramecio/citoplasma/templates/utils/translations.phtml b/paramecio/libraries/templates/utils/translations.phtml similarity index 100% rename from paramecio/citoplasma/templates/utils/translations.phtml rename to paramecio/libraries/templates/utils/translations.phtml diff --git a/paramecio/citoplasma/templates/utils/verify_delete.phtml b/paramecio/libraries/templates/utils/verify_delete.phtml similarity index 100% rename from paramecio/citoplasma/templates/utils/verify_delete.phtml rename to paramecio/libraries/templates/utils/verify_delete.phtml diff --git a/paramecio/citoplasma/urls.py b/paramecio/libraries/urls.py similarity index 100% rename from paramecio/citoplasma/urls.py rename to paramecio/libraries/urls.py diff --git a/paramecio/modules/admin/admin/ausers.py b/paramecio/modules/admin/admin/ausers.py index f4dcb43..c713e19 100644 --- a/paramecio/modules/admin/admin/ausers.py +++ b/paramecio/modules/admin/admin/ausers.py @@ -1,11 +1,17 @@ #!/usr/bin/env python3 from paramecio.modules.admin.models.admin import UserAdmin -from paramecio.citoplasma.urls import make_url -from paramecio.citoplasma.generate_admin_class import GenerateAdminClass -from paramecio.citoplasma.i18n import I18n -from paramecio.cromosoma.coreforms import SelectForm +from paramecio.libraries.urls import make_url +from paramecio.libraries.generate_admin_class import GenerateAdminClass +from paramecio.libraries.i18n import I18n, PGetText +from paramecio.libraries.db.coreforms import SelectForm +from paramecio.libraries.sessions import get_session from settings import config +from bottle import request + +pgettext=PGetText(__file__+'/../') + +_=pgettext.gettext def admin(**args): @@ -17,10 +23,14 @@ def admin(**args): user_admin.fields['privileges'].name_form=SelectForm - user_admin.create_forms(['username', 'password', 'email', 'privileges', 'lang']) + user_admin.fields['theme'].name_form=SelectForm + + user_admin.create_forms(['username', 'password', 'email', 'privileges', 'lang', 'theme']) user_admin.forms['privileges'].arr_select={0: I18n.lang('admin', 'without_privileges', 'Without privileges'), 1: I18n.lang('admin', 'selected_privileges', 'Selected privileges'), 2: I18n.lang('admin', 'administrator', 'Administrator')} + user_admin.forms['theme'].arr_select={0: _('Light theme'), 1: _('Dark theme')} + user_admin.fields['password'].protected=False url=make_url('admin/ausers', {}) @@ -31,7 +41,20 @@ def admin(**args): admin.list.search_fields=['username'] - admin.arr_fields_edit=['username', 'password', 'repeat_password', 'email', 'privileges', 'lang'] + admin.arr_fields_edit=['username', 'password', 'repeat_password', 'email', 'privileges', 'lang', 'theme'] + + #if reqes + if request.environ.get('REQUEST_METHOD', 'POST')=='POST': + s=get_session() + theme=request.forms.get('theme', '0') + + if theme!='0' and theme!='1': + theme='0' + + s['theme']=str(theme) + + s.save() + #admin.list.limit_pages=5 diff --git a/paramecio/modules/admin/index.py b/paramecio/modules/admin/index.py index 47d1de7..a70e57e 100644 --- a/paramecio/modules/admin/index.py +++ b/paramecio/modules/admin/index.py @@ -1,30 +1,32 @@ #!/usr/bin/env python3 import traceback, sys -from paramecio.citoplasma.mtemplates import env_theme, PTemplate +from paramecio.libraries.mtemplates import env_theme, PTemplate from paramecio.modules.admin.models.admin import UserAdmin -from paramecio.citoplasma.i18n import load_lang, I18n -from paramecio.citoplasma.urls import make_url, add_get_parameters, redirect -from paramecio.citoplasma.sessions import get_session, generate_session -from bottle import get,post,response,request +from paramecio.libraries.i18n import load_lang, I18n +from paramecio.libraries.urls import make_url, add_get_parameters, redirect +from paramecio.libraries.sessions import get_session, generate_session +from bottle import get,post,response,request, Bottle from settings import config from settings import config_admin -from paramecio.citoplasma.adminutils import get_menu, get_language, make_admin_url, check_login, login_model -from paramecio.citoplasma.httputils import GetPostFiles -from paramecio.cromosoma.formsutils import show_form, pass_values_to_form, set_extra_forms_user -from paramecio.cromosoma.coreforms import PasswordForm -from paramecio.cromosoma.webmodel import WebModel +from paramecio.libraries.adminutils import get_menu, get_language, make_admin_url, check_login, login_model +from paramecio.libraries.httputils import GetPostFiles +from paramecio.libraries.db.formsutils import show_form, pass_values_to_form, set_extra_forms_user +from paramecio.libraries.db.coreforms import PasswordForm +from paramecio.libraries.db.webmodel import WebModel from importlib import import_module, reload from collections import OrderedDict from time import time -from paramecio.citoplasma.keyutils import create_key_encrypt, create_key_encrypt_256, create_key -from paramecio.citoplasma.sendmail import SendMail +from paramecio.libraries.keyutils import create_key_encrypt, create_key_encrypt_256, create_key +from paramecio.libraries.sendmail import SendMail from os import path from paramecio.wsgiapp import app import copy from paramecio.i18n import admin -#from citoplasma.login import LoginClass +admin_app=Bottle() + +#from libraries.login import LoginClass # Check login yes_recovery_login=False @@ -70,11 +72,11 @@ if hasattr(config, 'admin_templates_index'): num_template+=1 -@app.get('/'+config.admin_folder) -@app.get('/'+config.admin_folder+'/') -@app.post('/'+config.admin_folder+'/') -@app.get('/'+config.admin_folder+'//') -@app.post('/'+config.admin_folder+'//') +@app.get('/admin') +@admin_app.get('/') +@admin_app.post('/') +@admin_app.get('//') +@admin_app.post('//') def home(module='', submodule='', t=t): # A simple boolean used for show or not the code of admin module in standard template @@ -149,7 +151,7 @@ def home(module='', submodule='', t=t): #args={'t': t, 'connection': connection} content_index=module_imported[module].admin(t=t, connection=connection) - + if t.show_basic_template==True: title_module=menu[module][0] @@ -158,7 +160,7 @@ def home(module='', submodule='', t=t): title_module=content_index[0] content_index=content_index[1] connection.close() - return t.load_template('admin/content.html', title=title_module, content_index=content_index, menu=menu, lang_selected=lang_selected, arr_i18n=I18n.dict_i18n) + return t.load_template('admin/content.html', title=title_module, content_index=content_index, menu=menu, lang_selected=lang_selected, arr_i18n=I18n.dict_i18n, module_name=module) else: connection.close() @@ -168,7 +170,7 @@ def home(module='', submodule='', t=t): else: connection.close() - return t.load_template('admin/index.html', title=I18n.lang('admin', 'welcome_to_paramecio', 'Welcome to Paramecio Admin!!!'), menu=menu, lang_selected=lang_selected, arr_i18n=I18n.dict_i18n) + return t.load_template('admin/index.html', title=I18n.lang('admin', 'welcome_to_paramecio', 'Welcome to Paramecio Admin!!!'), menu=menu, lang_selected=lang_selected, arr_i18n=I18n.dict_i18n, module_name='home') connection.close() return "" @@ -183,6 +185,8 @@ def home(module='', submodule='', t=t): else: + c=0 + user_admin.conditions=['WHERE privileges=%s', [2]] c=user_admin.select_count() @@ -197,7 +201,7 @@ def home(module='', submodule='', t=t): user_admin.conditions=['WHERE token_login=%s', [token_login]] - arr_user=user_admin.select_a_row_where(['id', 'privileges']) + arr_user=user_admin.select_a_row_where(['id', 'privileges', 'theme']) if arr_user==False: # delete cookioe @@ -209,6 +213,7 @@ def home(module='', submodule='', t=t): s['id']=arr_user['id'] s['login']=1 s['privileges']=arr_user['privileges'] + s['theme']=str(arr_user['theme']) s.save() @@ -248,121 +253,12 @@ def home(module='', submodule='', t=t): return "" -@app.post('/'+config.admin_folder+'/login') +@admin_app.post('/login') def login(): return login_model(UserAdmin) - - """ - connection=WebModel.connection() - - user_admin=UserAdmin(connection) - - getpostfiles=GetPostFiles() - - getpostfiles.obtain_post() - - getpostfiles.post['username']=getpostfiles.post.get('username', '') - getpostfiles.post['password']=getpostfiles.post.get('password', '') - - username=user_admin.fields['username'].check(getpostfiles.post['username']) - - password=getpostfiles.post['password'].strip() - - user_admin.conditions=['WHERE username=%s', [username]] - - arr_user=user_admin.select_a_row_where(['id', 'password', 'privileges', 'lang', 'num_tries', 'email']) - - if arr_user==False: - - s=get_session() - - s['csrf_token']=create_key_encrypt() - - s.save() - connection.close() - return {'error': 1, 'csrf_token': s['csrf_token']} - else: - - num_tries=int(arr_user['num_tries']) - - if arr_user['num_tries']<3: - - if user_admin.fields['password'].verify(password, arr_user['password']): - - s=get_session() - - s['id']=arr_user['id'] - s['login']=1 - s['privileges']=arr_user['privileges'] - s['lang']=arr_user['lang'] - s['email']=arr_user['email'] - - if s['lang']=='': - s['lang']=I18n.default_lang - - remember_login=getpostfiles.post.get('remember_login', '0') - - if remember_login=='1': - - timestamp=time()+315360000 - - random_text=create_key_encrypt() - - #Update user with autologin token - - user_admin.check_user=False - - user_admin.conditions=['WHERE username=%s', [username]] - - user_admin.valid_fields=['token_login'] - - user_admin.reset_require() - - if user_admin.update({'token_login': random_text}): - - response.set_cookie('remember_login', random_text, path=config.session_opts['session.path'], expires=timestamp, secret=key_encrypt) - #else: - #print(user_admin.query_error) - s.save() - - connection.close() - - return {'error': 0} - else: - - user_admin.check_user=False - - user_admin.conditions=['WHERE username=%s', [username]] - - user_admin.valid_fields=['num_tries'] - - user_admin.reset_require() - - user_admin.update({'num_tries': arr_user['num_tries']+1}) - - s=get_session() - - s['csrf_token']=create_key_encrypt() - - s.save() - - connection.close() - - return {'error': 1, 'csrf_token': s['csrf_token']} - else: - s=get_session() - - s['csrf_token']=create_key_encrypt() - - s.save() - - connection.close() - - return {'error': 1, 'csrf_token': s['csrf_token']} - """ -@app.post('/'+config.admin_folder+'/register') +@admin_app.post('/register') def register(): getpostfiles=GetPostFiles() @@ -423,7 +319,7 @@ def register(): return {'error': 1} -@app.get('/'+config.admin_folder+'/logout') +@admin_app.get('/logout') def logout(): s=get_session() @@ -446,7 +342,7 @@ def logout(): redirect(make_url(config.admin_folder)) -@app.get('/'+config.admin_folder+'/recovery_password') +@admin_app.get('/recovery_password') def recovery_password(): t=PTemplate(env) @@ -465,7 +361,7 @@ def recovery_password(): connection.close() return t.load_template('admin/recovery.phtml', forms=forms) -@app.post('/'+config.admin_folder+'/recovery_password') +@admin_app.post('/recovery_password') def send_password(): connection=WebModel.connection() @@ -522,13 +418,13 @@ def send_password(): return {'email': '', 'error': 0} -@app.get('/'+config.admin_folder+'/check_token') +@admin_app.get('/check_token') def check_token(): t=PTemplate(env) return t.load_template('admin/check_token.phtml') -@app.post('/'+config.admin_folder+'/check_token') +@admin_app.post('/check_token') def check_code_token(): t=PTemplate(env) @@ -583,3 +479,30 @@ def check_code_token(): s.save() return {'token': 'Error: token is not valid', 'error': 1, 'csrf_token': s['csrf_token']} + +@admin_app.get('/change_theme') +def change_theme(): + + error=1 + + if check_login(): + + theme_selected=str(request.query.get('theme', '0')) + + s=get_session() + + s['id']=s.get('id', 0) + + connection=WebModel.connection() + + s['theme']=theme_selected + + s.save() + + connection.query('update useradmin set theme=%s WHERE id=%s', [s['theme'], s['id']]) + + error=0 + + return {'error': error} + +app.mount('/'+config.admin_folder+'/', admin_app) diff --git a/paramecio/modules/admin/media/css/admin.css b/paramecio/modules/admin/media/css/admin.css index f741fa6..cee2169 100644 --- a/paramecio/modules/admin/media/css/admin.css +++ b/paramecio/modules/admin/media/css/admin.css @@ -1,28 +1,54 @@ +/** { + margin: 0; + padding: 0; + -webkit-box-sizing: border-box; + box-sizing: border-box; +}*/ + body { margin:0px; - background-color:#f7f6f1; - font-family: "Roboto", sans, sans-serif, serif; + background-color:#f4f6f9; + font-family: sans, sans-serif; font-size: 16px; + /*-webkit-transition: all 0.5s ease-in-out; + transition: all 0.5s ease-in-out; + -webkit-transition-property: background-color, color; + transition-property: background-color, color;*/ } -a -{ +body.dark { + background-color: #232834; + + color: #fbfbfb; + /*-webkit-transition: all 0.5s ease-in-out; + transition: all 0.5s ease-in-out; + -webkit-transition-property: background-color, color; + transition-property: background-color, color;*/ + +} + +a { color: #1c6280; } -a:hover -{ +a:hover { color: #d54e21; } +.dark a { + + color: #5fa6c4; + +} + #header { @@ -30,7 +56,7 @@ a:hover height:52px; color:#000; font-size:22px; - font-family:"Roboto", sans, serif; + font-family:sans, sans-serif; /*background-image:url('../images/background.png'); background-position:top; background-repeat:repeat-x;*/ @@ -39,12 +65,20 @@ a:hover } +.dark #header { + + + + background-color:#1e1412; + +} + #title_phango { font-size:28px; padding-left:15px; color: #555555; - font-family:"Roboto", sans, serif; + font-family: sans, serif; /*font-style:italic;*/ /*text-shadow:#000000 1px 1px 1px; filter: progid:DXImageTransform.Microsoft.Shadow(color='#000000', Direction=130, Strength=4);*/ @@ -65,7 +99,7 @@ a:hover h1, h2 { - font-family:"Roboto", sans, serif; + font-family: sans, sans-serif; border: solid #cbcbcb; border-width: 0px 0px 1px 1px; font-size:26px; @@ -99,6 +133,20 @@ h1 { } +.dark h1 { + + background-color:#000a22; + color: #fbfbfb; + border-color: #000; + +} + +.dark h2 { + + color: #fbfbfb; + +} + p { border: solid #cbcbcb; @@ -134,7 +182,7 @@ p { { float:left; - width:20%; + width:18%; margin-right:0px; position:relative; /* border:solid #cccccc; @@ -142,6 +190,16 @@ p { box-sizing: border-box; /* padding-bottom: 100px;*/ overflow:hidden; + height:100%; + -webkit-box-shadow: 1px 1px 5px 0px rgba(0,0,0,0.75); + -moz-box-shadow: 1px 1px 5px 0px rgba(0,0,0,0.75); + box-shadow: 1px 1px 5px 0px rgba(0,0,0,0.75); +} + +.dark #menu { + + + } #menu a @@ -161,7 +219,7 @@ p { background: transparent; } -#menu a:hover +#menu a:hover, #menu a.selected_menu { text-decoration:underline; @@ -185,7 +243,7 @@ p { padding:5px; font-size:18px; - font-family:"Roboto", sans, serif; + font-family: sans, sans-serif; font-weight:bold; /*background-image:url('../images/background_title.png'); background-position:top; @@ -202,7 +260,7 @@ p { .father_admin { - font-family:"Roboto", sans, serif; + font-family:sans, sans-serif; padding:5px; font-size:18px; display: block; @@ -240,13 +298,13 @@ p { margin:auto; background: rgba(18,47,59,1); - background: -moz-linear-gradient(left, rgba(18,47,59,1) 0%, rgba(18,47,59,1) 51%, rgba(18,47,59,1) 74%, rgba(8,59,77,1) 100%); + /*background: -moz-linear-gradient(left, rgba(18,47,59,1) 0%, rgba(18,47,59,1) 51%, rgba(18,47,59,1) 74%, rgba(8,59,77,1) 100%); background: -webkit-gradient(left top, right top, color-stop(0%, rgba(18,47,59,1)), color-stop(51%, rgba(18,47,59,1)), color-stop(74%, rgba(18,47,59,1)), color-stop(100%, rgba(8,59,77,1))); background: -webkit-linear-gradient(left, rgba(18,47,59,1) 0%, rgba(18,47,59,1) 51%, rgba(18,47,59,1) 74%, rgba(8,59,77,1) 100%); background: -o-linear-gradient(left, rgba(18,47,59,1) 0%, rgba(18,47,59,1) 51%, rgba(18,47,59,1) 74%, rgba(8,59,77,1) 100%); - background: -ms-linear-gradient(left, rgba(18,47,59,1) 0%, rgba(18,47,59,1) 51%, rgba(18,47,59,1) 74%, rgba(8,59,77,1) 100%); + background: -ms-linear-gradient(left, rgba(18,47,59,1) 0%, rgba(18,47,59,1) 51%, rgba(18,47,59,1) 74%, rgba(8,59,77,1) 100%);*/ background: linear-gradient(to right, rgba(18,47,59,1) 0%, rgba(18,47,59,1) 51%, rgba(18,47,59,1) 74%, rgba(8,59,77,1) 100%); - background-size: 20% 100%; + background-size: 18% 100%; background-repeat:no-repeat; padding:0px; border:solid #bcbcbc; @@ -257,8 +315,23 @@ p { border: solid #cbcbcb; border-width: 0px 0px 1px 0px; min-height: 90vh; + /*-webkit-transition: all 0.5s ease-in-out; + transition: all 0.5s ease-in-out;*/ + + } +.dark .content_admin { + + background-color: #1e202a; + /*-webkit-transition: all 0.5s ease-in-out; + transition: all 0.5s ease-in-out;*/ + border: solid #323232; + border-width: 0px 0px 1px 0px; + +} + + .content_admin i { margin-right: 10px; @@ -268,11 +341,13 @@ p { .contents { float:right; - width:80%; + width:82%; padding:0px 0px 0px 0px; box-sizing: border-box; + } + .content { padding:0px 10px 5px 10px; @@ -313,9 +388,20 @@ p { padding:10px; margin:10px 0px 10px 0px; border-radius:5px; + background: #fbfbfb; + -webkit-box-shadow: 1px 1px 5px 0px rgba(0,0,0,0.75); + -moz-box-shadow: 1px 1px 5px 0px rgba(0,0,0,0.75); + box-shadow: 1px 1px 5px 0px rgba(0,0,0,0.75); } +.dark .form { + + background-color: #1e202a; + border-color: #323232; + +} + .form textarea { width:100%; @@ -386,6 +472,14 @@ margin-right:auto; } +table { + + -webkit-box-shadow: 1px 1px 5px 0px rgba(0,0,0,0.75); + -moz-box-shadow: 1px 1px 5px 0px rgba(0,0,0,0.75); + box-shadow: 1px 1px 5px 0px rgba(0,0,0,0.75); + +} + .table_list { width:100%; @@ -396,6 +490,12 @@ margin-right:auto; } +.dark .table_list { + + border: solid #323232 1px; + +} + .title_list td, .fields_span_title { margin:0px; @@ -411,6 +511,13 @@ margin-right:auto; } +.dark .title_list td { + + background-color:#000a22; + + +} + .fields_span_title, div.options_td , div.fields_span_table_data { box-sizing: border-box; @@ -433,6 +540,14 @@ margin-right:auto; /*border:solid #cccccc; border-width:1px;*/ padding:4px; + background: #fbfbfb; + +} + +.dark .row_list td { + + background-color: #2d313b; + color: #fbfbfb; } @@ -459,6 +574,13 @@ div.fields_span_table_data { } +.dark .cont_text, .dark .cont { + + background-color: #2d313b; + border-color: #343434; + +} + .cont_top { border-width: 1px 1px 1px 1px; @@ -505,6 +627,12 @@ div.fields_span_table_data { } +.dark .error { + + color: #ff3232; + +} + .hidden_form { @@ -564,9 +692,11 @@ a:hover.no_choose_flag #center_body input { border: solid #bcbcbc; - border-width:1px; + border-width:2px; border-radius:5px; background: #eeeeee; + padding: 2px 4px; + font-size:16px; } @@ -576,7 +706,14 @@ a:hover.no_choose_flag } -#center_body input[type="submit"], #center_body input[type="button"] +.dark #center_body input { + + background-color: #2d313b; + color: #fbfbfb; + +} + +#center_body input[type="submit"], #center_body input[type="button"], #center_body input[type="button"].button_blue { font-family: helvetica, arial,sans; @@ -598,7 +735,13 @@ a:hover.no_choose_flag } -#center_body input:hover[type="submit"] +.dark #center_body input[type="button"] { + + background: #d08b2c; + +} + +#center_body input:hover[type="submit"], #center_body input[type="button"].button_blue { background: #104070; @@ -614,10 +757,19 @@ a:hover.no_choose_flag } -#center_body input[type="text"] +#center_body input:hover[type="button"].button_blue +{ + + background: #215181; + cursor: pointer; + +} + + +#center_body input[type="text"], #center_body input[type="password"] { - width:500px; + min-width:500px; } @@ -634,6 +786,16 @@ a:hover.no_choose_flag border: solid #bcbcbc; border-width:1px; border-radius:5px; + padding: 2px 5px; + border: solid #cbcbcb 2px; + font-size: 16px; + +} + +.dark #center_body select { + + background-color: #2d313b; + color: #fbfbfb; } @@ -649,6 +811,12 @@ a:hover.no_choose_flag } +#logout { + + z-index:99999; + +} + #languages_general { top:8px; @@ -801,6 +969,341 @@ a.form_button_tab:hover } +/* Loading */ +/*
*/ + +.lds-dual-ring { + display: inline-block; + width: 80px; + height: 80px; +} +.lds-dual-ring:after { + content: " "; + display: block; + width: 64px; + height: 64px; + margin: 8px; + border-radius: 50%; + border: 6px solid #000; + border-color: #000 transparent #000 transparent; + animation: lds-dual-ring 1.2s linear infinite; +} +@keyframes lds-dual-ring { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + +/* Block screen while loading */ + +#layer_loading { + + z-index:50000; + /*background-color:rgba(0,0,0,0.4);*/ + /*opacity:0.5;*/ + position:absolute; + width:100%; + height:100%; + overflow:auto; + + +} + +#container_loading { + + z-index:50001; + overflow:auto; + /* border: solid #fbfbfb 4px;*/ + position:absolute; + overflow:visible; + width:100%; + height:100%; + display: flex; + align-items: center; + justify-content: center; + +} + +/* Simple grid */ + +.row_4x { + + width: 32%; + box-sizing: border-box; +/* border: solid #fff 1px;*/ + float:left; + overflow:hidden; + +} + +.row_5x { + + width: 44%; + box-sizing: border-box; +/* border: solid #fff 1px;*/ + float:left; + +} + + +.row_7x { + + width: 56%; + box-sizing: border-box; + /*border: solid #f00 1px;*/ + float:left; + +} + +.row_8x { + + width: 68%; + box-sizing: border-box; + /*border: solid #f00 1px;*/ + float:left; + +} + +.row_margin_left { + + padding-left: 20px; + padding-right: 10px; + +} + +.row_margin_right { + + padding-left: 10px; + padding-right: 20px; + +} + +.row_margin_right_left { + + + padding-left: 10px; + padding-right: 10px; + +} + +/* special boxes */ + +.container_block { + + padding-top:0px; + padding-bottom:10px; + +} + +.container_block_no_padding { + + padding-bottom:0px; + +} + +.container_warning { + +} + +h2.title_container { + + display:block; + padding: 10px 15px 10px 15px; + font-size:1.2rem; + margin:0px; + font-weight:400; + color:#fbfbfb; + border-radius: 5px 5px 0px 0px; + border: solid #dfe1e4 1px; + border-width: 1px 1px 0px 1px; + position:relative; + /*-webkit-box-shadow: 3px 0px 2px -1px rgba(0,0,0,0.20); + box-shadow: 3px 0px 2px -1px rgba(0,0,0,0.20);*/ + +} + +.dark h2.title_container { + + background-color: #2d313b; + border-color: #343434; + color: #fbfbfb; + +} + +.container_warning h2.title_container { + + background:#dc3545; + +} + +.container_info h2.title_container { + + background: #000458; + +} + +.container_soft h2.title_container { + + background: #0faa32; + +} + +.container_content { + + display:block; + background:#fbfbfb; + padding: 10px 15px 10px 15px; + color:#212529; + border-radius: 0px 0px 5px 5px; + /*-webkit-box-shadow: 3px 2px 1px -1px rgba(0,0,0,0.20); + box-shadow: 3px 2px 1px -1px rgba(0,0,0,0.20);*/ + border: solid #dfe1e4 1px; + overflow:hidden; + +} + +.dark .container_content { + + background-color: #2d313b; + border-color: #343434; + color: #fbfbfb; + +} + +/* Switch for dark mode */ + +/* switches css */ + + /* The switch - the box around the slider */ + +.switch { + position: relative; + display: inline-block; + width: 40px; + height: 16px; + +} + +/* Hide default HTML checkbox */ +.switch input {display:none;} + +/* The slider */ +.slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #ccc; + -webkit-transition: .4s; + transition: .4s; + height: 16px; + width:38px; + box-sizing: border-box; + -webkit-box-sizing:border-box; + -moz-box-sizing: border-box; +} + +/* before is the checkbox */ + +.slider:before { + position: absolute; + content: ""; + height: 22px; + width: 22px; + left: -1px; + top: -3px; + background-color: #fbfbfb; + -webkit-transition: .4s; + transition: .4s; +} + +input:checked + .slider { + background-color: #447211; +} + +input:focus + .slider { + box-shadow: 0 0 1px #447211; +} + +input:checked + .slider:before { + -webkit-transform: translateX(21px); + -ms-transform: translateX(21px); + transform: translateX(21px); + background-color: #7ed321; +} + +input:checked + .slider_grey { + background-color: #ccc; +} + +input:focus + .slider_grey { + box-shadow: 0 0 1px #ccc; +} + +input:checked + .slider_grey:before { + -webkit-transform: translateX(21px); + -ms-transform: translateX(21px); + transform: translateX(21px); + background-color: #fbfbfb; +} + + +/* Rounded sliders */ +.slider.round { + border-radius: 34px; +} + +.slider.round:before { + border-radius: 50%; +} + +.container_switch { + + margin-top:6px; + display:inline-block; + float:right; + +} + +.switch-btn { + + position:absolute; + /*border: solid #f00 1px;*/ + right:4px; + top:8px; + z-index:9999; + +} + +.switch-slider { + + text-align:center; + display: inline-block; + +} + +.switch-text { + + display: inline-block; + font-size: 12px; + position:relative; + top: -4px; + left:-2px; + color: #000; + +} + +.dark .switch-text { + + color: #fbfbfb; + +} + /* Media queries */ @media only screen and (max-width: 800px) { @@ -868,7 +1371,7 @@ a.form_button_tab:hover } - .ip_td { + /*.ip_td { display:none; @@ -878,7 +1381,7 @@ a.form_button_tab:hover display:none; - } + }*/ .first_canvas { @@ -895,5 +1398,44 @@ a.form_button_tab:hover } + /*Grid 100% */ + + .row_4x { + + width: 100%; + box-sizing: border-box; + /* border: solid #fff 1px;*/ + float:auto; + + } + + .row_5x { + + width: 100%; + box-sizing: border-box; + /* border: solid #fff 1px;*/ + float:auto; + + } + + + .row_7x { + + width: 100%; + box-sizing: border-box; + /*border: solid #f00 1px;*/ + float:auto; + + } + + .row_8x { + + width: 100%; + box-sizing: border-box; + /*border: solid #f00 1px;*/ + float:auto; + + } + } diff --git a/paramecio/modules/admin/media/js/jsutils b/paramecio/modules/admin/media/js/jsutils new file mode 160000 index 0000000..353a80d --- /dev/null +++ b/paramecio/modules/admin/media/js/jsutils @@ -0,0 +1 @@ +Subproject commit 353a80d8437aaf449ecdd7e459c1fcf7575f41bc diff --git a/paramecio/modules/admin/models/admin.py b/paramecio/modules/admin/models/admin.py index 3c27276..9fdf718 100644 --- a/paramecio/modules/admin/models/admin.py +++ b/paramecio/modules/admin/models/admin.py @@ -1,12 +1,12 @@ #!/usr/bin/env python3 -from paramecio.citoplasma.i18n import I18n -from paramecio.cromosoma.webmodel import WebModel -from paramecio.cromosoma.usermodel import UserModel -from paramecio.cromosoma import corefields -from paramecio.cromosoma.extrafields.emailfield import EmailField -from paramecio.cromosoma.extrafields.passwordfield import PasswordField -from paramecio.cromosoma.extrafields.langfield import LangField +from paramecio.libraries.i18n import I18n +from paramecio.libraries.db.webmodel import WebModel +from paramecio.libraries.db.usermodel import UserModel +from paramecio.libraries.db import corefields +from paramecio.libraries.db.extrafields.emailfield import EmailField +from paramecio.libraries.db.extrafields.passwordfield import PasswordField +from paramecio.libraries.db.extrafields.langfield import LangField class PrivilegesField(corefields.IntegerField): @@ -89,6 +89,8 @@ class UserAdmin(UserModel): self.register(corefields.IntegerField('num_tries', 1)) + self.register(corefields.IntegerField('theme')) + """ user_admin=WebModel('user_admin') diff --git a/paramecio/modules/admin/templates/admin/home.html b/paramecio/modules/admin/templates/admin/home.html index 4922219..6b9ffbc 100644 --- a/paramecio/modules/admin/templates/admin/home.html +++ b/paramecio/modules/admin/templates/admin/home.html @@ -1,3 +1,17 @@ +<% + +from paramecio.libraries.sessions import get_session + +s=get_session() + +dark_checked="" +dark_css="" + +if s.get('theme', '0')=='1': + dark_checked='checked' + dark_css='dark' + +%> <%def name="check_menu(module)">\ % if module[:1]=='/': ${make_url(module[1:])}\ @@ -9,13 +23,13 @@ ${make_url('admin/'+module)}\ - + ${title} - + ${HeaderHTML.css_home()|n} <%block name="extra_css"> @@ -29,7 +43,8 @@ ${HeaderHTML.header_home()|n} <%block name="extra_header"> - + +
@@ -43,18 +58,11 @@ ${HeaderHTML.header_home()|n} <%block name="languages">
- <%def name="select_lang(i18n, lang_selected)"> - % if i18n==lang_selected: - choose_flag - % else: - no_choose_flag - % endif - + <%def name="select_lang(i18n, lang_selected)">${'choose_flag' if i18n==lang_selected else 'no_choose_flag'} % if lang_selected!=None: % for i18n in arr_i18n: ${i18n} - % endfor % endif
@@ -66,7 +74,7 @@ ${HeaderHTML.header_home()|n} % for module in menu: % if type(menu[module]).__name__=='list': % if menu[module][0]!="": -
  • ${menu[module][3]|n}${menu[module][0]}
  • +
  • ${menu[module][3]|n}${menu[module][0]}
  • % endif % else: % if menu[module]!="": @@ -78,6 +86,17 @@ ${HeaderHTML.header_home()|n}

    ${title}

    +
    +
    + ${_('Dark theme')} +
    +
    + +
    +
    ${show_flash_message()|n} <%block name="content"> @@ -96,6 +115,65 @@ ${HeaderHTML.header_home()|n} trigger: 'click' }); + const slider = document.querySelector('input[name="theme"]'); + + slider.addEventListener("change", function () { + + //Block button while send to ajax. + + $(this).prop("disabled",true); + + var dark=''; + + if (this.checked) { + document.body.classList.add("dark"); + + dark='1'; + + } else { + document.body.classList.remove("dark"); + + dark='0'; + } + + $.ajax({ + url: "${make_admin_url('change_theme')}?theme="+dark, + type: 'GET', + data: {}, + success: function (data) { + + if(!data.error) { + + console.log('Changed to dark in all pages'); + + } + else { + + console.log('Cannot set dark theme in all pages!'); + + } + + $(slider).prop("disabled",false); + + }, + error: function (data) { + + alert('Error: '+data.status+' '+data.statusText); + + $(slider).prop("disabled", false); + + }, + dataType: 'json' + }); + + }); + + $(document).ready(function () { + + $('#layer_loading').hide(); + + }); + <%block name="jscript_block"> diff --git a/paramecio/modules/admin/templates/admin/login.phtml b/paramecio/modules/admin/templates/admin/login.phtml index c40f55d..6448c8f 100644 --- a/paramecio/modules/admin/templates/admin/login.phtml +++ b/paramecio/modules/admin/templates/admin/login.phtml @@ -35,8 +35,8 @@ if(data.error==0) { - //window.location.href="${make_url('admin')}"; - location.reload() + window.location.href="${make_url('admin')}"; + //location.reload() } else diff --git a/paramecio/modules/admin2/__init__.py b/paramecio/modules/admin2/__init__.py new file mode 100644 index 0000000..f693521 --- /dev/null +++ b/paramecio/modules/admin2/__init__.py @@ -0,0 +1,7 @@ +from bottle import request, redirect, Bottle +from paramecio.modules.admin2.libraries.loginplugin import check_login +from paramecio.libraries.sessionplugin import SessionPlugin + +admin_app=Bottle() +admin_app.install(SessionPlugin()) +admin_app.install(check_login) diff --git a/paramecio/modules/admin2/app.py b/paramecio/modules/admin2/app.py new file mode 100644 index 0000000..2daaaca --- /dev/null +++ b/paramecio/modules/admin2/app.py @@ -0,0 +1,313 @@ + +#from paramecio import wsgi_app +from paramecio.libraries.i18n import I18n +from paramecio.libraries.mtemplates import env_theme, PTemplate +from paramecio.modules.admin2.models.admin import UserAdmin2, LoginTries2 +from paramecio.libraries.db.webmodel import WebModel +from paramecio.libraries.db import simplequery +from settings import config +from paramecio.libraries.datetime import now, format_local_strtime, timestamp_to_datetime, obtain_timestamp +from paramecio.libraries.keyutils import create_key_encrypt, create_key +from time import time +from paramecio.wsgiapp import app +#from paramecio.modules.admin2 import admin_app +from bottle import request, redirect, Bottle +from paramecio.modules.admin2.libraries.loginplugin import check_login +from paramecio.libraries.sessionplugin import SessionPlugin +from paramecio.libraries.httputils import GetPostFiles +from paramecio.libraries.db.formsutils import check_form, csrf_token +from paramecio.libraries.db.coreforms import PasswordForm + +admin_app=Bottle() +admin_app.install(SessionPlugin()) +admin_app.install(check_login) + +env=env_theme(__file__) +t=PTemplate(env) + +usermodel=UserAdmin2() + +usermodel.create_forms() + +login_tries=5 + +if hasattr(config, 'login_tries'): + login_tries=config.login_tries + +seconds_login=300 + +if hasattr(config, 'seconds_login'): + seconds_login=config.seconds_login + +cookie_name='paramecio_session' + +if hasattr(config, 'cookie_name'): + cookie_name=config.cookie_name + +admin_folder='/admin' + +if hasattr(config, 'admin_folder'): + admin_folder=config.admin_folder + +@app.get(admin_folder) +def redirect_home(): + + redirect(app.get_url('admin_app.home_admin')) + +@admin_app.get('/home', name="admin_app.home_admin") +def home_admin(session={}): + + #s=get_session() + + i18n=I18n('admin2') + + return t.load_template('layout.phtml', title=i18n.tlang('Admin'), module_selected='home_admin', session=session) + #return {} + + +@admin_app.get('/login', name="admin_app.login_admin", skip=[check_login]) +def login_admin(session={}): + + db=WebModel.connection() + + if cookie_name+'_remember' in request.cookies: + + arr_user=simplequery.select(usermodel, db, dict_fields=['id', 'username'], where_sql='WHERE token_login=%s', dict_values=[request.cookies[cookie_name+'_remember']]) + + if len(arr_user)>0: + now_str=now() + date_now=format_local_strtime('YYYY-MM-DD HH:mm:ss', now_str) + + db.query('update useradmin2 set last_login=%s WHERE id=%s', [date_now, arr_user[0]['id']]) + + request.session['login_admin']=True + + db.close() + + redirect(app.get_url('app_admin.home_admin')) + + with db.query('select count(id) as num_users from useradmin2', []) as cursor: + num_users=cursor.fetchone()['num_users'] + + if not num_users: + redirect(app.get_url('admin_app.signup_admin')) + + db.close() + + i18n=I18n('admin2') + + return t.load_template('login.phtml', title=i18n.tlang('Login')) + +@admin_app.get('/signup', skip=[check_login], name='admin_app.signup_admin') +def signup_admin(session={}): + + db=WebModel.connection() + + with db.query('select count(id) as num_users from useradmin2', []) as cursor: + num_users=cursor.fetchone()['num_users'] + + if num_users>0: + redirect(app.get_url('admin_app.login_admin')) + + db.close() + + i18n=I18n('admin2') + + return t.load_template('signup.phtml', title=i18n.tlang('Signup')) + +@admin_app.post('/login', skip=[check_login], name='admin_app.check_login_admin') +def check_login_admin(session={}): + + db=WebModel.connection() + + i18n=I18n('admin2') + + error=1 + + message=i18n.tlang('Invalid user and password') + + no_login=check_login_tries(request, db) + + #username=request.json.get('username', '') + #password=request.json.get('password', '') + #remember_login=request.json.get('remember_login', '') + getpost=GetPostFiles() + + getpost.obtain_post() + + username=getpost.post.get('username') + password=getpost.post.get('password') + remember_login=getpost.post.get('remember_login') + + if username!='' and password!='' and not no_login: + + with db.query('select * from useradmin2 WHERE username=%s', [username]) as cursor: + result=cursor.fetchone() + + if result: + + if usermodel.fields['password'].verify(password, result['password']): + + remember_key='' + + if remember_login==True: + # Send cookies + + remember_key=create_key_encrypt() + + timestamp=int(time())+315360000 + + response.set_cookie(key=cookie_name+'_remember', value=remember_key, expires=timestamp, max_age=315360000, httponly=True, path=config.application_root) + + now_str=now() + date_now=format_local_strtime('YYYY-MM-DD HH:mm:ss', now_str) + + db.query('update useradmin2 set token_login=%s, last_login=%s WHERE id=%s', [remember_key, date_now, result['id']]) + + session['login_admin']=True + session['user_id']=result['id'] + session['theme']=result['dark_theme'] + error=0 + message='' + + db.close() + + return {'error': error, 'message': message, 'no_login': no_login} + +@admin_app.post('/signup', skip=[check_login], name='admin_app.signup_insert_admin') +def signup_insert_admin(session={}): + + i18n=I18n('admin2') + + error=1 + + message='' + + db=WebModel.connection() + + arr_form={'username': usermodel.forms['username'], 'password': usermodel.forms['password'], 'repeat_password': PasswordForm('repeat_password', ''), 'email': usermodel.forms['email']} + + getpost=GetPostFiles() + + getpost.obtain_post() + + #Only can exist and user + + final_password=getpost.post.get('password', '') + + (error, error_form, post, arr_form)=check_form(getpost.post, arr_form, sufix_form='_error') + + post['password']=final_password + + post['privileges']=2 + + with db.query('select count(id) as num_users from useradmin2', []) as cursor: + num_users=cursor.fetchone()['num_users'] + + if num_users: + message="You cannot add new users from here" + else: + error=0 + + if not error: + + if simplequery.insert(usermodel, db, dict(post)): + #if 1: + error=0 + message="User added!" + else: + message=i18n.tlang('Sorry, cannot create the new user') + + db.close() + + return {'error': error, 'message': message} + +@admin_app.get('/logout', name="admin_app.logout_admin", skip=[check_login]) +def logout_admin(session={}): + + if login_admin in session: + del session['login_admin'] + + if cookie_name+'_remember' in request.cookies: + response.delete_cookie(cookie_name+'_remember', path=session_opts['session.path']) + + redirect(app.get_url('admin_app.login_admin')) + +@admin_app.get('/change_lang') +def change_lang(): + + error=0 + + message='' + + return {'error': 0, 'message': ''} + +@admin_app.get('/change_theme', name="admin_app.change_theme") +def change_theme(session={}): + + try: + theme=int(request.query.get('theme', 0)) + except: + theme=0 + + error=1 + + db=WebModel.connection() + + session['theme']=theme + + db.query('update useradmin2 set dark_theme=%s WHERE id=%s', [theme, session['user_id']]) + + error=0 + + db.close() + + return {'error': error, 'message': ''} + + +def check_login_tries(request, db): + + logintries=LoginTries2(db) + + logintries.safe_query() + + ip=request.environ.get('HTTP_X_FORWARDED_FOR') or request.environ.get('REMOTE_ADDR') + + """ + if 'x-real-ip' in request.headers: + ip=request.headers['x-real-ip'] + elif 'x-forwarded-for' in request.headers: + ip=request.headers['x-forwarded-for'] + else: + ip=request.client.host + """ + + you_cannot_login=0 + + now_str=now() + date_now=format_local_strtime('YYYY-MM-DD HH:mm:ss', now_str) + + date_check=format_local_strtime('YYYY-MM-DD HH:mm:ss', timestamp_to_datetime(obtain_timestamp(now_str)-seconds_login)) + + logintries.query('delete from logintries2 where last_login<%s', [date_check]) + + arr_try=logintries.set_conditions('WHERE ip=%s', [ip]).select_a_row_where() + + if arr_try: + + if arr_try['num_tries']') + +@admin_app.get('/ausers', name="admin_app.admin_users") +@admin_app.post('/ausers', name="admin_app.admin_users") +def admin_users(session={}): + + i18n=I18n('admin2') + + db=WebModel.connection() + + user_admin=UserAdmin2(db) + + user_admin.fields['privileges'].name_form=SelectForm + + user_admin.fields['dark_theme'].name_form=SelectForm + + user_admin.create_forms(['username', 'password', 'email', 'privileges', 'lang', 'dark_theme']) + + user_admin.forms['privileges'].arr_select={0: I18n.lang('admin', 'without_privileges', 'Without privileges'), 1: I18n.lang('admin', 'selected_privileges', 'Selected privileges'), 2: I18n.lang('admin', 'administrator', 'Administrator')} + + user_admin.forms['dark_theme'].arr_select={0: _('Light theme'), 1: _('Dark theme')} + + user_admin.fields['password'].protected=False + + url=app.get_url('admin_app.admin_users') + + admin=GenerateAdminClass(user_admin, url, t) + + admin.list.fields_showed=['username', 'email', 'double_auth', 'privileges', 'last_login'] + + admin.list.search_fields=['username'] + + admin.arr_fields_edit=['username', 'password', 'repeat_password', 'email', 'privileges', 'lang', 'dark_theme'] + + slist=admin.show() + + db.close() + + return t.load_template('users.phtml', title=i18n.tlang('Admin users'), tlang=i18n.tlang, module_selected='admin_app.admin_users', slist=slist, session=session) + + diff --git a/paramecio/modules/admin2/libraries/config.py b/paramecio/modules/admin2/libraries/config.py new file mode 100644 index 0000000..b362ec3 --- /dev/null +++ b/paramecio/modules/admin2/libraries/config.py @@ -0,0 +1,10 @@ +#!/usr/bin/python3 + +# Every element + +# ['Name of module admin', 'name_function_for_url_for', 'xml-icon'] + +modules_admin=[] + +modules_admin_icons=[] + diff --git a/paramecio/modules/admin2/libraries/i18n.py b/paramecio/modules/admin2/libraries/i18n.py new file mode 100644 index 0000000..f5288ab --- /dev/null +++ b/paramecio/modules/admin2/libraries/i18n.py @@ -0,0 +1,19 @@ +from parameciofast.libraries.i18n import I18n + +""" +I18n.l['en-US']=I18n.l.get('en-US', {}) + +I18n.l['en-US']['fastadmin']=I18n.l['en-US'].get('fastadmin', {}) + +I18n.l['en-US']['fastadmin']['fastadmin_ausers']='Admin users' + +I18n.l['es-ES']=I18n.l.get('es-ES', {}) + +I18n.l['es-ES']['fastadmin']=I18n.l['en-US'].get('fastadmin', {}) + +I18n.l['es-ES']['fastadmin']['fastadmin_ausers']='Usuarios admin' +""" + +I18n.add_lang('en-US', 'admin2', 'admin_app.admin_users', 'Admin users') +I18n.add_lang('es-ES', 'admin2', 'admin_app.admin_users', 'Usuarios admin') + diff --git a/paramecio/modules/admin2/libraries/loginplugin.py b/paramecio/modules/admin2/libraries/loginplugin.py new file mode 100644 index 0000000..085351f --- /dev/null +++ b/paramecio/modules/admin2/libraries/loginplugin.py @@ -0,0 +1,66 @@ +from bottle import request, response, redirect +#from settings import config +import inspect +from paramecio.wsgiapp import app + +def check_login(callback): + def wrapper(*args, **kwargs): + + if 'session' in request.environ: + + if request.environ['session'].get('login_admin', 0): + + result = callback(*args, **kwargs) + + return result + + redirect(app.get_url('admin_app.login_admin')) + + return wrapper +""" +class CheckLoginPlugin(object): + + name = 'checklogin' + api = 2 + + def __init__(self, keyword='session'): + + self.keyword=keyword + + + def setup(self, app): + ''' Make sure that other installed plugins don't affect the same keyword argument.''' + for other in app.plugins: + if not isinstance(other, SessionPlugin): continue + if other.keyword == self.keyword: + raise PluginError("Found another login plugin with "\ + "conflicting settings (non-unique keyword).") + + def apply(self, callback, context): + + # Test if the original callback accepts a 'session' keyword. + # Ignore it if it does not need a login handle. + + conf = context.config.get('checklogin') or {} + + keyword = conf.get('keyword', self.keyword) + + args = inspect.getfullargspec(context.callback)[0] + + if keyword not in args: + return callback + + def wrapper(*args, **kwargs): + + if 'session' in request.environ: + + if request.environ['session'].get('login_admin', 1): + + rv=callback(*args, **kwargs) + + return rv + + redirect(app.get_url('admin_app.home_admin')) + + return wrapper +""" diff --git a/paramecio/modules/admin2/media/css/admin.css b/paramecio/modules/admin2/media/css/admin.css new file mode 100644 index 0000000..cee2169 --- /dev/null +++ b/paramecio/modules/admin2/media/css/admin.css @@ -0,0 +1,1441 @@ + +/** { + margin: 0; + padding: 0; + -webkit-box-sizing: border-box; + box-sizing: border-box; +}*/ + +body +{ + + margin:0px; + background-color:#f4f6f9; + font-family: sans, sans-serif; + font-size: 16px; + /*-webkit-transition: all 0.5s ease-in-out; + transition: all 0.5s ease-in-out; + -webkit-transition-property: background-color, color; + transition-property: background-color, color;*/ + +} + +body.dark { + background-color: #232834; + + color: #fbfbfb; + /*-webkit-transition: all 0.5s ease-in-out; + transition: all 0.5s ease-in-out; + -webkit-transition-property: background-color, color; + transition-property: background-color, color;*/ + +} + +a { + + color: #1c6280; + +} + +a:hover { + + color: #d54e21; + +} + +.dark a { + + color: #5fa6c4; + +} + +#header +{ + + width:100%; + height:52px; + color:#000; + font-size:22px; + font-family:sans, sans-serif; + /*background-image:url('../images/background.png'); + background-position:top; + background-repeat:repeat-x;*/ + background-color:#e1ebed; + line-height: 47px; + +} + +.dark #header { + + + + background-color:#1e1412; + +} + +#title_phango +{ + font-size:28px; + padding-left:15px; + color: #555555; + font-family: sans, serif; + /*font-style:italic;*/ + /*text-shadow:#000000 1px 1px 1px; + filter: progid:DXImageTransform.Microsoft.Shadow(color='#000000', Direction=130, Strength=4);*/ + +} + +#title_framework +{ + + font-size:28px; + padding-right:15px; + color:#ff9b2f; + /*text-shadow:#000000 1px 1px 1px; + filter: progid:DXImageTransform.Microsoft.Shadow(color='#000000', Direction=130, Strength=4);*/ + +} + +h1, h2 +{ + + font-family: sans, sans-serif; + border: solid #cbcbcb; + border-width: 0px 0px 1px 1px; + font-size:26px; + color:#333333; + margin:0px 0px 20px 0px; + padding:5px 10px; + font-weight:normal; + background: #e6e6e6; + +} + +h2 { + + font-size:20px; + background: transparent; + color: #000; + padding: 0px; + border-width:0px; + margin: 4px 0px; + +} + +h1 { + + -webkit-box-shadow: 1px 1px 5px 0px rgba(0,0,0,0.75); + -moz-box-shadow: 1px 1px 5px 0px rgba(0,0,0,0.75); + box-shadow: 1px 1px 5px 0px rgba(0,0,0,0.75); + font-size:20px; + padding:3px 10px; + font-weight: 600; + +} + +.dark h1 { + + background-color:#000a22; + color: #fbfbfb; + border-color: #000; + +} + +.dark h2 { + + color: #fbfbfb; + +} + +p { + + border: solid #cbcbcb; + margin-top:4px; + border-width:0px; + +} + +#logo_header +{ + + + +} + +#center_body { + + background-position:left; + background-repeat:repeat-y; + height: auto !important; + width:100%; + margin-left:auto; + margin-right:auto; + overflow:auto; + height: auto !important; + min-height:300px; + position:relative; + + +} + +#menu +{ + + float:left; + width:18%; + margin-right:0px; + position:relative; +/* border:solid #cccccc; + border-width:0px 1px 1px 0px;*/ + box-sizing: border-box; +/* padding-bottom: 100px;*/ + overflow:hidden; + height:100%; + -webkit-box-shadow: 1px 1px 5px 0px rgba(0,0,0,0.75); + -moz-box-shadow: 1px 1px 5px 0px rgba(0,0,0,0.75); + box-shadow: 1px 1px 5px 0px rgba(0,0,0,0.75); +} + +.dark #menu { + + + +} + +#menu a +{ + + font-size:16px; + /*font-family:arial, sans;*/ + text-decoration:none; + color:#ededed; + display:block; + border:solid #cccccc; + /*border-width:0px 1px 1px 1px;*/ + border-width:0px; + padding:5px; + padding-top:10px; + padding-bottom:10px; + background: transparent; +} + +#menu a:hover, #menu a.selected_menu +{ + + text-decoration:underline; + color:#fbfbfb; + background: #0fb1dd; + text-decoration:none; + +} + +#menu a.sub_module +{ + + color:#6183b1; + background-color: #fbfbfb; + text-decoration:none; + +} + +.menu_title +{ + + padding:5px; + font-size:18px; + font-family: sans, sans-serif; + font-weight:bold; + /*background-image:url('../images/background_title.png'); + background-position:top; + background-repeat: repeat-x;*/ + background-color: #003c95; + /*border:solid #cccccc; + border-width:0px 1px 1px 1px;*/ + color: #fbfbfb; + overflow: hidden; + box-sizing: border-box; + + +} + +.father_admin { + + font-family:sans, sans-serif; + padding:5px; + font-size:18px; + display: block; + color: #fbfbfb; + font-weight:bold; + background: #000; + /* + background: rgba(76,76,76,1); + background: -moz-linear-gradient(top, rgba(76,76,76,1) 0%, rgba(89,89,89,1) 12%, rgba(102,102,102,1) 25%, rgba(71,71,71,1) 39%, rgba(44,44,44,1) 50%, rgba(0,0,0,1) 51%, rgba(17,17,17,1) 60%, rgba(43,43,43,1) 76%, rgba(28,28,28,1) 91%, rgba(19,19,19,1) 100%); + background: -webkit-gradient(left top, left bottom, color-stop(0%, rgba(76,76,76,1)), color-stop(12%, rgba(89,89,89,1)), color-stop(25%, rgba(102,102,102,1)), color-stop(39%, rgba(71,71,71,1)), color-stop(50%, rgba(44,44,44,1)), color-stop(51%, rgba(0,0,0,1)), color-stop(60%, rgba(17,17,17,1)), color-stop(76%, rgba(43,43,43,1)), color-stop(91%, rgba(28,28,28,1)), color-stop(100%, rgba(19,19,19,1))); + background: -webkit-linear-gradient(top, rgba(76,76,76,1) 0%, rgba(89,89,89,1) 12%, rgba(102,102,102,1) 25%, rgba(71,71,71,1) 39%, rgba(44,44,44,1) 50%, rgba(0,0,0,1) 51%, rgba(17,17,17,1) 60%, rgba(43,43,43,1) 76%, rgba(28,28,28,1) 91%, rgba(19,19,19,1) 100%); + background: -o-linear-gradient(top, rgba(76,76,76,1) 0%, rgba(89,89,89,1) 12%, rgba(102,102,102,1) 25%, rgba(71,71,71,1) 39%, rgba(44,44,44,1) 50%, rgba(0,0,0,1) 51%, rgba(17,17,17,1) 60%, rgba(43,43,43,1) 76%, rgba(28,28,28,1) 91%, rgba(19,19,19,1) 100%); + background: -ms-linear-gradient(top, rgba(76,76,76,1) 0%, rgba(89,89,89,1) 12%, rgba(102,102,102,1) 25%, rgba(71,71,71,1) 39%, rgba(44,44,44,1) 50%, rgba(0,0,0,1) 51%, rgba(17,17,17,1) 60%, rgba(43,43,43,1) 76%, rgba(28,28,28,1) 91%, rgba(19,19,19,1) 100%); + background: linear-gradient(to bottom, rgba(76,76,76,1) 0%, rgba(89,89,89,1) 12%, rgba(102,102,102,1) 25%, rgba(71,71,71,1) 39%, rgba(44,44,44,1) 50%, rgba(0,0,0,1) 51%, rgba(17,17,17,1) 60%, rgba(43,43,43,1) 76%, rgba(28,28,28,1) 91%, rgba(19,19,19,1) 100%); + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#4c4c4c', endColorstr='#131313', GradientType=0 );*/ + + +} + +#menu ul { + + margin:0px; + /*border: solid #000 1px;*/ + padding:0px; +} + +#menu li { + + border-width: 0px; + +} + +.content_admin +{ + + margin:auto; + background: rgba(18,47,59,1); + /*background: -moz-linear-gradient(left, rgba(18,47,59,1) 0%, rgba(18,47,59,1) 51%, rgba(18,47,59,1) 74%, rgba(8,59,77,1) 100%); + background: -webkit-gradient(left top, right top, color-stop(0%, rgba(18,47,59,1)), color-stop(51%, rgba(18,47,59,1)), color-stop(74%, rgba(18,47,59,1)), color-stop(100%, rgba(8,59,77,1))); + background: -webkit-linear-gradient(left, rgba(18,47,59,1) 0%, rgba(18,47,59,1) 51%, rgba(18,47,59,1) 74%, rgba(8,59,77,1) 100%); + background: -o-linear-gradient(left, rgba(18,47,59,1) 0%, rgba(18,47,59,1) 51%, rgba(18,47,59,1) 74%, rgba(8,59,77,1) 100%); + background: -ms-linear-gradient(left, rgba(18,47,59,1) 0%, rgba(18,47,59,1) 51%, rgba(18,47,59,1) 74%, rgba(8,59,77,1) 100%);*/ + background: linear-gradient(to right, rgba(18,47,59,1) 0%, rgba(18,47,59,1) 51%, rgba(18,47,59,1) 74%, rgba(8,59,77,1) 100%); + background-size: 18% 100%; + background-repeat:no-repeat; + padding:0px; + border:solid #bcbcbc; + border-width: 0px; + font-size:16px; + position:relative; + overflow:auto; + border: solid #cbcbcb; + border-width: 0px 0px 1px 0px; + min-height: 90vh; + /*-webkit-transition: all 0.5s ease-in-out; + transition: all 0.5s ease-in-out;*/ + + +} + +.dark .content_admin { + + background-color: #1e202a; + /*-webkit-transition: all 0.5s ease-in-out; + transition: all 0.5s ease-in-out;*/ + border: solid #323232; + border-width: 0px 0px 1px 0px; + +} + + +.content_admin i { + + margin-right: 10px; + +} + +.contents +{ + float:right; + width:82%; + padding:0px 0px 0px 0px; + box-sizing: border-box; + +} + + +.content +{ + padding:0px 10px 5px 10px; + /*border: solid #000 1px;*/ + +} + +.title +{ + + font-size:18px; + font-weight:bold; + font-family:helvetica, sans; + text-decoration:none; + color:#fbfbfb; + padding:5px; + /*background-image:url('../images/background_title.png'); + background-position:top; + background-repeat: repeat-x;*/ + background-color: #337ab7; + border-radius: 8px 8px 0px 0px; + border:solid #337ab7; + border-width:1px 0px 1px 0px; + margin-top:8px; +} + +#inform { + + margin:auto; + +} + +/*Styles for common templates...*/ + +.form { + + border: solid #cbcbcb 1px; + padding:10px; + margin:10px 0px 10px 0px; + border-radius:5px; + background: #fbfbfb; + -webkit-box-shadow: 1px 1px 5px 0px rgba(0,0,0,0.75); + -moz-box-shadow: 1px 1px 5px 0px rgba(0,0,0,0.75); + box-shadow: 1px 1px 5px 0px rgba(0,0,0,0.75); + +} + +.dark .form { + + background-color: #1e202a; + border-color: #323232; + +} + +.form textarea { + + width:100%; + height:200px; + +} + +.form label { + display: block; + /*width: 150px;*/ + float: left; + margin-bottom: 20px; + text-align: left; + width: 350px; + padding-right: 20px; + /*border: #000000 solid; + border-width:1px;*/ + +} + + + +.form p { + clear: left; +} + +img { + +display:block; + +} + +.panel { + +display:block; +float:left; +/*clear:left;*/ +text-align:center; +border: #000000 solid; +border-width:0px; +min-width:200px; +width:200px !important; +padding:0px; +margin:0px; +min-height:100px; +height:100px !important; +position:relative; + + +} + +.panel p { + +position:absolute; +bottom:0px; +/*padding-bottom:0px;*/ +text-align:center; + +margin-left:auto; +margin-right:auto; + +} + +.panel img { + +margin-left:auto; +margin-right:auto; + +} + +table { + + -webkit-box-shadow: 1px 1px 5px 0px rgba(0,0,0,0.75); + -moz-box-shadow: 1px 1px 5px 0px rgba(0,0,0,0.75); + box-shadow: 1px 1px 5px 0px rgba(0,0,0,0.75); + +} + +.table_list { + + width:100%; + border:solid #cccccc; + border-width:1px; + margin:0px; + box-sizing: border-box; + +} + +.dark .table_list { + + border: solid #323232 1px; + +} + +.title_list td, .fields_span_title { + + margin:0px; + padding:4px; + /*background-image:url('../images/background_title.png'); + background-position:top; + background-repeat: repeat-x;*/ + background: #ccc; + font-size:14px; + font-weight:bold; + box-sizing: border-box; + /*border: solid #000 1px;*/ + +} + +.dark .title_list td { + + background-color:#000a22; + + +} + +.fields_span_title, div.options_td , div.fields_span_table_data { + + box-sizing: border-box; + display:inline-block; + float:left; + /*margin-top:4px;*/ + /*border: solid #000 1px;*/ +} + +.tr_list_div , div.fields_span_table_data { + + margin-top:4px; + margin-left: 4px; + margin-right: 2px; + +} + +.row_list td, div.fields_span_table_data { + + /*border:solid #cccccc; + border-width:1px;*/ + padding:4px; + background: #fbfbfb; + +} + +.dark .row_list td { + + background-color: #2d313b; + color: #fbfbfb; + +} + +div.fields_span_table_data { + + box-sizing: border-box; + margin: 0px; + +} + +.cont_text, .cont +{ + + margin:auto; + background-color:#ffffff; + padding:10px 10px 10px 10px; + border:solid #cbcbcb; + border-width: 0px 1px 1px 1px; + font-size:14px; + position:relative; + overflow:auto; + margin-bottom:10px; + border-radius: 0px 0px 5px 5px; + +} + +.dark .cont_text, .dark .cont { + + background-color: #2d313b; + border-color: #343434; + +} + +.cont_top { + + border-width: 1px 1px 1px 1px; + border-radius: 5px 5px 5px 5px; +} + +.search { + + border-width: 1px; + border-radius:5px; + margin-top:5px; + +} + +.right_cont +{ + + width:78%; + +} + +.none_cont +{ + + /*width:100%;*/ + +} + +.contview +{ + + border:solid #cccccc; + border-width:1px 1px 1px 1px; + padding:15px; + margin: 0px 0px 5px 0px; + +} + +.error +{ + + color: #ff0000; + font-weight:bold; + +} + +.dark .error { + + color: #ff3232; + +} + +.hidden_form +{ + + display: none; + /*clear:left;*/ + +} + +.no_hidden_form +{ + + display: inline; + /*float:left; + clear:left right;*/ + +} + +a.no_choose_flag +{ + + filter: Alpha(opacity=50); + -moz-opacity: .5; + opacity: .5; + display:inline-block; + margin: 0px; +} + +a.choose_flag +{ + + filter: Alpha(opacity=100); + -moz-opacity: 100; + opacity: 1; + display:inline-block; + margin: 0px; + +} + +a:hover.no_choose_flag +{ + + filter: Alpha(opacity=100); + -moz-opacity: 100; + opacity: 1; + +} + +#languages +{ + width:100%; + clear:left; + +} + +/** input **/ + +#center_body input { + + border: solid #bcbcbc; + border-width:2px; + border-radius:5px; + background: #eeeeee; + padding: 2px 4px; + font-size:16px; + +} + +#center_body input:hover { + + background: #fafafa; + +} + +.dark #center_body input { + + background-color: #2d313b; + color: #fbfbfb; + +} + +#center_body input[type="submit"], #center_body input[type="button"], #center_body input[type="button"].button_blue +{ + + font-family: helvetica, arial,sans; + font-size:16px; + border-radius:5px; + border: solid #205081 1px; + background: #205081; + padding: 5px 10px 5px 10px; + font-weight:bold; + color: #fbfbfb; + +} + +#center_body input[type="button"] +{ + + background: #f0ad4e; + border: solid #f0ad4e 1px; + +} + +.dark #center_body input[type="button"] { + + background: #d08b2c; + +} + +#center_body input:hover[type="submit"], #center_body input[type="button"].button_blue +{ + + background: #104070; + cursor: pointer; + +} + +#center_body input:hover[type="button"] +{ + + background: #d09c3d; + cursor: pointer; + +} + +#center_body input:hover[type="button"].button_blue +{ + + background: #215181; + cursor: pointer; + +} + + +#center_body input[type="text"], #center_body input[type="password"] +{ + + min-width:500px; + +} + +#center_body textarea { + + border: solid #bcbcbc; + border-width:1px; + border-radius:5px; + +} + +#center_body select { + + border: solid #bcbcbc; + border-width:1px; + border-radius:5px; + padding: 2px 5px; + border: solid #cbcbcb 2px; + font-size: 16px; + +} + +.dark #center_body select { + + background-color: #2d313b; + color: #fbfbfb; + +} + +#languages_general, #logout +{ + + position:absolute; + margin: 0px 0px 0px 0px; + top:54px; + z-index:3; + right:0px; + width:50px; + +} + +#logout { + + z-index:99999; + +} + +#languages_general { + + top:8px; + right:5px; + height:10px; + +} + +#logout +{ + + top:8px; + right:10px; + width:55px; + font-weight:bold; + font-size:10px; + text-decoration: none; + /*border: solid #000 1px;*/ +} + +#logout a { + + text-decoration: none; + +} + +a.no_choose_flag_general +{ + + filter: Alpha(opacity=50); + -moz-opacity: .5; + opacity: .5; + float:left; + margin: 2px; + +} + +a.choose_flag_general +{ + + filter: Alpha(opacity=100); + -moz-opacity: 100; + opacity: 100; + float:left; + margin: 2px; + +} + +a:hover.no_choose_flag_general +{ + + filter: Alpha(opacity=100); + -moz-opacity: 100; + opacity: 100; + float:left; + margin: 2px; + +} + +a.form_button_tab, a.form_button_tab_selected +{ + + border: solid #121212 1px; + border-radius:10px; + padding:10px; + display:inline-block; + text-decoration:none; + color:#fbfbfb; + background: #000; + +} + +a.form_button_tab_selected +{ + + background:#fbfbfb; + color: #000; + +} + +a.form_button_tab:hover +{ + + + color:#000; + background:#fbfbfb; + +} + +.flash +{ + border-radius:15px; + margin-top:15px; + padding:25px; + color:#fbfbfb; + text-shadow:#000000 1px 1px 1px; + filter: progid:DXImageTransform.Microsoft.Shadow(color='#000000', Direction=130, Strength=4); + + background: #ff3019; /* Old browsers */ + background: -moz-linear-gradient(top, #ff3019 0%, #cf0404 100%); /* FF3.6+ */ + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#ff3019), color-stop(100%,#cf0404)); /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(top, #ff3019 0%,#cf0404 100%); /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(top, #ff3019 0%,#cf0404 100%); /* Opera 11.10+ */ + background: -ms-linear-gradient(top, #ff3019 0%,#cf0404 100%); /* IE10+ */ + background: linear-gradient(to bottom, #ff3019 0%,#cf0404 100%); /* W3C */ + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#ff3019', endColorstr='#cf0404',GradientType=0 ); /* IE6-9 */ + +} + +#toggle { + + position:absolute; + left:10px; + top:0px; + z-index:1; + display:none; + + +} + +#toggle span { + + display:none; + +} + +.first_canvas { + + width:48%; + float:right; + +} + +.other_canvas { + + width:48%; + float:left; + +} + +.my_arrow_right { + + position: absolute; + +} + +.little_textarea { + + height: 50px !important; + +} + +/* Loading */ +/*
    */ + +.lds-dual-ring { + display: inline-block; + width: 80px; + height: 80px; +} +.lds-dual-ring:after { + content: " "; + display: block; + width: 64px; + height: 64px; + margin: 8px; + border-radius: 50%; + border: 6px solid #000; + border-color: #000 transparent #000 transparent; + animation: lds-dual-ring 1.2s linear infinite; +} +@keyframes lds-dual-ring { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + +/* Block screen while loading */ + +#layer_loading { + + z-index:50000; + /*background-color:rgba(0,0,0,0.4);*/ + /*opacity:0.5;*/ + position:absolute; + width:100%; + height:100%; + overflow:auto; + + +} + +#container_loading { + + z-index:50001; + overflow:auto; + /* border: solid #fbfbfb 4px;*/ + position:absolute; + overflow:visible; + width:100%; + height:100%; + display: flex; + align-items: center; + justify-content: center; + +} + +/* Simple grid */ + +.row_4x { + + width: 32%; + box-sizing: border-box; +/* border: solid #fff 1px;*/ + float:left; + overflow:hidden; + +} + +.row_5x { + + width: 44%; + box-sizing: border-box; +/* border: solid #fff 1px;*/ + float:left; + +} + + +.row_7x { + + width: 56%; + box-sizing: border-box; + /*border: solid #f00 1px;*/ + float:left; + +} + +.row_8x { + + width: 68%; + box-sizing: border-box; + /*border: solid #f00 1px;*/ + float:left; + +} + +.row_margin_left { + + padding-left: 20px; + padding-right: 10px; + +} + +.row_margin_right { + + padding-left: 10px; + padding-right: 20px; + +} + +.row_margin_right_left { + + + padding-left: 10px; + padding-right: 10px; + +} + +/* special boxes */ + +.container_block { + + padding-top:0px; + padding-bottom:10px; + +} + +.container_block_no_padding { + + padding-bottom:0px; + +} + +.container_warning { + +} + +h2.title_container { + + display:block; + padding: 10px 15px 10px 15px; + font-size:1.2rem; + margin:0px; + font-weight:400; + color:#fbfbfb; + border-radius: 5px 5px 0px 0px; + border: solid #dfe1e4 1px; + border-width: 1px 1px 0px 1px; + position:relative; + /*-webkit-box-shadow: 3px 0px 2px -1px rgba(0,0,0,0.20); + box-shadow: 3px 0px 2px -1px rgba(0,0,0,0.20);*/ + +} + +.dark h2.title_container { + + background-color: #2d313b; + border-color: #343434; + color: #fbfbfb; + +} + +.container_warning h2.title_container { + + background:#dc3545; + +} + +.container_info h2.title_container { + + background: #000458; + +} + +.container_soft h2.title_container { + + background: #0faa32; + +} + +.container_content { + + display:block; + background:#fbfbfb; + padding: 10px 15px 10px 15px; + color:#212529; + border-radius: 0px 0px 5px 5px; + /*-webkit-box-shadow: 3px 2px 1px -1px rgba(0,0,0,0.20); + box-shadow: 3px 2px 1px -1px rgba(0,0,0,0.20);*/ + border: solid #dfe1e4 1px; + overflow:hidden; + +} + +.dark .container_content { + + background-color: #2d313b; + border-color: #343434; + color: #fbfbfb; + +} + +/* Switch for dark mode */ + +/* switches css */ + + /* The switch - the box around the slider */ + +.switch { + position: relative; + display: inline-block; + width: 40px; + height: 16px; + +} + +/* Hide default HTML checkbox */ +.switch input {display:none;} + +/* The slider */ +.slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #ccc; + -webkit-transition: .4s; + transition: .4s; + height: 16px; + width:38px; + box-sizing: border-box; + -webkit-box-sizing:border-box; + -moz-box-sizing: border-box; +} + +/* before is the checkbox */ + +.slider:before { + position: absolute; + content: ""; + height: 22px; + width: 22px; + left: -1px; + top: -3px; + background-color: #fbfbfb; + -webkit-transition: .4s; + transition: .4s; +} + +input:checked + .slider { + background-color: #447211; +} + +input:focus + .slider { + box-shadow: 0 0 1px #447211; +} + +input:checked + .slider:before { + -webkit-transform: translateX(21px); + -ms-transform: translateX(21px); + transform: translateX(21px); + background-color: #7ed321; +} + +input:checked + .slider_grey { + background-color: #ccc; +} + +input:focus + .slider_grey { + box-shadow: 0 0 1px #ccc; +} + +input:checked + .slider_grey:before { + -webkit-transform: translateX(21px); + -ms-transform: translateX(21px); + transform: translateX(21px); + background-color: #fbfbfb; +} + + +/* Rounded sliders */ +.slider.round { + border-radius: 34px; +} + +.slider.round:before { + border-radius: 50%; +} + +.container_switch { + + margin-top:6px; + display:inline-block; + float:right; + +} + +.switch-btn { + + position:absolute; + /*border: solid #f00 1px;*/ + right:4px; + top:8px; + z-index:9999; + +} + +.switch-slider { + + text-align:center; + display: inline-block; + +} + +.switch-text { + + display: inline-block; + font-size: 12px; + position:relative; + top: -4px; + left:-2px; + color: #000; + +} + +.dark .switch-text { + + color: #fbfbfb; + +} + +/* Media queries */ + +@media only screen and (max-width: 800px) { + + .content_admin { + + background: transparent; + + } + + #toggle { + + display:block; + + } + + #toggle span { + + display:none; + + } + + .contents { + + width:100%; + + } + + #menu { + + width:100%; + + } + + #menu a { + + background: rgba(18,47,59,1); + background: -moz-linear-gradient(left, rgba(18,47,59,1) 0%, rgba(18,47,59,1) 51%, rgba(18,47,59,1) 74%, rgba(8,59,77,1) 100%); + background: -webkit-gradient(left top, right top, color-stop(0%, rgba(18,47,59,1)), color-stop(51%, rgba(18,47,59,1)), color-stop(74%, rgba(18,47,59,1)), color-stop(100%, rgba(8,59,77,1))); + background: -webkit-linear-gradient(left, rgba(18,47,59,1) 0%, rgba(18,47,59,1) 51%, rgba(18,47,59,1) 74%, rgba(8,59,77,1) 100%); + background: -o-linear-gradient(left, rgba(18,47,59,1) 0%, rgba(18,47,59,1) 51%, rgba(18,47,59,1) 74%, rgba(8,59,77,1) 100%); + background: -ms-linear-gradient(left, rgba(18,47,59,1) 0%, rgba(18,47,59,1) 51%, rgba(18,47,59,1) 74%, rgba(8,59,77,1) 100%); + background: linear-gradient(to right, rgba(18,47,59,1) 0%, rgba(18,47,59,1) 51%, rgba(18,47,59,1) 74%, rgba(8,59,77,1) 100%); + + } + + #title_phango + { + font-size:18px; + text-align:center; + color: #555555; + border: solid #000 0px; + } + + #title_framework + { + + display:none; + + } + + #header { + + text-align:center; + + } + + /*.ip_td { + + display:none; + + } + + .num_updates_td { + + display:none; + + }*/ + + .first_canvas { + + width:100%; + clear:both; + + } + + + .other_canvas { + + width:100%; + clear:both; + + } + + /*Grid 100% */ + + .row_4x { + + width: 100%; + box-sizing: border-box; + /* border: solid #fff 1px;*/ + float:auto; + + } + + .row_5x { + + width: 100%; + box-sizing: border-box; + /* border: solid #fff 1px;*/ + float:auto; + + } + + + .row_7x { + + width: 100%; + box-sizing: border-box; + /*border: solid #f00 1px;*/ + float:auto; + + } + + .row_8x { + + width: 100%; + box-sizing: border-box; + /*border: solid #f00 1px;*/ + float:auto; + + } + +} + diff --git a/paramecio/modules/admin2/media/css/layout.css b/paramecio/modules/admin2/media/css/layout.css new file mode 100644 index 0000000..d6d1d97 --- /dev/null +++ b/paramecio/modules/admin2/media/css/layout.css @@ -0,0 +1,126 @@ + +body { + + background: #121c27; + min-height: 100vh; + min-height: -webkit-fill-available; + +} + +main { + display: flex; + flex-wrap: nowrap; + height: 100vh; + height: -webkit-fill-available; + max-height: 100vh; + overflow-x: auto; + /*overflow-y: hidden;*/ +} + + +html { + height: -webkit-fill-available; +} + + +#header_dashboard { + + background: #1d2939; + -webkit-box-shadow: 0px 3px 5px 0px rgba(0,0,0,0.75); + -moz-box-shadow: 0px 3px 5px 0px rgba(0,0,0,0.75); + box-shadow: 0px 3px 5px 0px rgba(0,0,0,0.75); + z-index:99999; + +} + +.bi { + vertical-align: -.125em; + pointer-events: none; + fill: currentColor; +} + +.hands { + + vertical-align: -.140em !important; + +} + + +/* Loader page */ + +.loader-div { + position: fixed; + top: 0; + left: 0; + height: 100vh; + width: 100%; + display: flex; + align-items: center; + justify-content: center; + z-index:999999; + /*background: url('../images/caledonian.png') no-repeat center 40%; + background-color: #ec1c24;*/ + background: transparent; +} + +.loader { + position: relative; + width: 10vw; + height: 5vw; + padding: 1.5vw; + display: flex; + align-items: center; + justify-content: center; +} + +.loader span { + position: absolute; + height: 0.8vw; + width: 0.8vw; + border-radius: 50%; + background-color: #fff; +} + +.loader span:nth-child(1) { + animation: loading-dotsA 0.5s infinite linear; +} + +.loader span:nth-child(2) { + animation: loading-dotsB 0.5s infinite linear; +} + +@keyframes loading-dotsA { + 0% { + transform: none; + } + 25% { + transform: translateX(2vw); + } + 50% { + transform: none; + } + 75% { + transform: translateY(2vw); + } + 100% { + transform: none; + } +} + +@keyframes loading-dotsB { + 0% { + transform: none; + } + 25% { + transform: translateX(-2vw); + } + 50% { + transform: none; + } + 75% { + transform: translateY(-2vw); + } + 100% { + transform: none; + } +} diff --git a/paramecio/modules/admin2/media/css/responsive-nav.css b/paramecio/modules/admin2/media/css/responsive-nav.css new file mode 100644 index 0000000..9bdbd6f --- /dev/null +++ b/paramecio/modules/admin2/media/css/responsive-nav.css @@ -0,0 +1,49 @@ +/*! responsive-nav.js 1.0.39 by @viljamis */ + +.nav-collapse ul { + margin: 0; + padding: 0; + width: 100%; + display: block; + list-style: none; +} + +.nav-collapse li { + width: 100%; + display: block; +} + +.js .nav-collapse { + clip: rect(0 0 0 0); + max-height: 0; + position: absolute; + display: block; + overflow: hidden; + zoom: 1; +} + +.nav-collapse.opened { + max-height: 9999px; +} + +.nav-toggle { + -webkit-tap-highlight-color: rgba(0,0,0,0); + -webkit-touch-callout: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + -o-user-select: none; + user-select: none; +} + +@media screen and (min-width: 40em) { + .js .nav-collapse { + position: relative; + } + .js .nav-collapse.closed { + max-height: none; + } + .nav-toggle { + display: none; + } +} diff --git a/paramecio/modules/admin2/media/css/tooltipster.bundle.min.css b/paramecio/modules/admin2/media/css/tooltipster.bundle.min.css new file mode 100644 index 0000000..d8f30fe --- /dev/null +++ b/paramecio/modules/admin2/media/css/tooltipster.bundle.min.css @@ -0,0 +1 @@ +.tooltipster-fall,.tooltipster-grow.tooltipster-show{-webkit-transition-timing-function:cubic-bezier(.175,.885,.32,1);-moz-transition-timing-function:cubic-bezier(.175,.885,.32,1.15);-ms-transition-timing-function:cubic-bezier(.175,.885,.32,1.15);-o-transition-timing-function:cubic-bezier(.175,.885,.32,1.15)}.tooltipster-base{display:flex;pointer-events:none;position:absolute}.tooltipster-box{flex:1 1 auto}.tooltipster-content{box-sizing:border-box;max-height:100%;max-width:100%;overflow:auto}.tooltipster-ruler{bottom:0;left:0;overflow:hidden;position:fixed;right:0;top:0;visibility:hidden}.tooltipster-fade{opacity:0;-webkit-transition-property:opacity;-moz-transition-property:opacity;-o-transition-property:opacity;-ms-transition-property:opacity;transition-property:opacity}.tooltipster-fade.tooltipster-show{opacity:1}.tooltipster-grow{-webkit-transform:scale(0,0);-moz-transform:scale(0,0);-o-transform:scale(0,0);-ms-transform:scale(0,0);transform:scale(0,0);-webkit-transition-property:-webkit-transform;-moz-transition-property:-moz-transform;-o-transition-property:-o-transform;-ms-transition-property:-ms-transform;transition-property:transform;-webkit-backface-visibility:hidden}.tooltipster-grow.tooltipster-show{-webkit-transform:scale(1,1);-moz-transform:scale(1,1);-o-transform:scale(1,1);-ms-transform:scale(1,1);transform:scale(1,1);-webkit-transition-timing-function:cubic-bezier(.175,.885,.32,1.15);transition-timing-function:cubic-bezier(.175,.885,.32,1.15)}.tooltipster-swing{opacity:0;-webkit-transform:rotateZ(4deg);-moz-transform:rotateZ(4deg);-o-transform:rotateZ(4deg);-ms-transform:rotateZ(4deg);transform:rotateZ(4deg);-webkit-transition-property:-webkit-transform,opacity;-moz-transition-property:-moz-transform;-o-transition-property:-o-transform;-ms-transition-property:-ms-transform;transition-property:transform}.tooltipster-swing.tooltipster-show{opacity:1;-webkit-transform:rotateZ(0);-moz-transform:rotateZ(0);-o-transform:rotateZ(0);-ms-transform:rotateZ(0);transform:rotateZ(0);-webkit-transition-timing-function:cubic-bezier(.23,.635,.495,1);-webkit-transition-timing-function:cubic-bezier(.23,.635,.495,2.4);-moz-transition-timing-function:cubic-bezier(.23,.635,.495,2.4);-ms-transition-timing-function:cubic-bezier(.23,.635,.495,2.4);-o-transition-timing-function:cubic-bezier(.23,.635,.495,2.4);transition-timing-function:cubic-bezier(.23,.635,.495,2.4)}.tooltipster-fall{-webkit-transition-property:top;-moz-transition-property:top;-o-transition-property:top;-ms-transition-property:top;transition-property:top;-webkit-transition-timing-function:cubic-bezier(.175,.885,.32,1.15);transition-timing-function:cubic-bezier(.175,.885,.32,1.15)}.tooltipster-fall.tooltipster-initial{top:0!important}.tooltipster-fall.tooltipster-dying{-webkit-transition-property:all;-moz-transition-property:all;-o-transition-property:all;-ms-transition-property:all;transition-property:all;top:0!important;opacity:0}.tooltipster-slide{-webkit-transition-property:left;-moz-transition-property:left;-o-transition-property:left;-ms-transition-property:left;transition-property:left;-webkit-transition-timing-function:cubic-bezier(.175,.885,.32,1);-webkit-transition-timing-function:cubic-bezier(.175,.885,.32,1.15);-moz-transition-timing-function:cubic-bezier(.175,.885,.32,1.15);-ms-transition-timing-function:cubic-bezier(.175,.885,.32,1.15);-o-transition-timing-function:cubic-bezier(.175,.885,.32,1.15);transition-timing-function:cubic-bezier(.175,.885,.32,1.15)}.tooltipster-slide.tooltipster-initial{left:-40px!important}.tooltipster-slide.tooltipster-dying{-webkit-transition-property:all;-moz-transition-property:all;-o-transition-property:all;-ms-transition-property:all;transition-property:all;left:0!important;opacity:0}@keyframes tooltipster-fading{0%{opacity:0}100%{opacity:1}}.tooltipster-update-fade{animation:tooltipster-fading .4s}@keyframes tooltipster-rotating{25%{transform:rotate(-2deg)}75%{transform:rotate(2deg)}100%{transform:rotate(0)}}.tooltipster-update-rotate{animation:tooltipster-rotating .6s}@keyframes tooltipster-scaling{50%{transform:scale(1.1)}100%{transform:scale(1)}}.tooltipster-update-scale{animation:tooltipster-scaling .6s}.tooltipster-sidetip .tooltipster-box{background:#565656;border:2px solid #000;border-radius:4px}.tooltipster-sidetip.tooltipster-bottom .tooltipster-box{margin-top:8px}.tooltipster-sidetip.tooltipster-left .tooltipster-box{margin-right:8px}.tooltipster-sidetip.tooltipster-right .tooltipster-box{margin-left:8px}.tooltipster-sidetip.tooltipster-top .tooltipster-box{margin-bottom:8px}.tooltipster-sidetip .tooltipster-content{color:#fff;line-height:18px;padding:6px 14px}.tooltipster-sidetip .tooltipster-arrow{overflow:hidden;position:absolute}.tooltipster-sidetip.tooltipster-bottom .tooltipster-arrow{height:10px;margin-left:-10px;top:0;width:20px}.tooltipster-sidetip.tooltipster-left .tooltipster-arrow{height:20px;margin-top:-10px;right:0;top:0;width:10px}.tooltipster-sidetip.tooltipster-right .tooltipster-arrow{height:20px;margin-top:-10px;left:0;top:0;width:10px}.tooltipster-sidetip.tooltipster-top .tooltipster-arrow{bottom:0;height:10px;margin-left:-10px;width:20px}.tooltipster-sidetip .tooltipster-arrow-background,.tooltipster-sidetip .tooltipster-arrow-border{height:0;position:absolute;width:0}.tooltipster-sidetip .tooltipster-arrow-background{border:10px solid transparent}.tooltipster-sidetip.tooltipster-bottom .tooltipster-arrow-background{border-bottom-color:#565656;left:0;top:3px}.tooltipster-sidetip.tooltipster-left .tooltipster-arrow-background{border-left-color:#565656;left:-3px;top:0}.tooltipster-sidetip.tooltipster-right .tooltipster-arrow-background{border-right-color:#565656;left:3px;top:0}.tooltipster-sidetip.tooltipster-top .tooltipster-arrow-background{border-top-color:#565656;left:0;top:-3px}.tooltipster-sidetip .tooltipster-arrow-border{border:10px solid transparent;left:0;top:0}.tooltipster-sidetip.tooltipster-bottom .tooltipster-arrow-border{border-bottom-color:#000}.tooltipster-sidetip.tooltipster-left .tooltipster-arrow-border{border-left-color:#000}.tooltipster-sidetip.tooltipster-right .tooltipster-arrow-border{border-right-color:#000}.tooltipster-sidetip.tooltipster-top .tooltipster-arrow-border{border-top-color:#000}.tooltipster-sidetip .tooltipster-arrow-uncropped{position:relative}.tooltipster-sidetip.tooltipster-bottom .tooltipster-arrow-uncropped{top:-10px}.tooltipster-sidetip.tooltipster-right .tooltipster-arrow-uncropped{left:-10px} \ No newline at end of file diff --git a/paramecio/modules/admin2/media/images/ajax-loader.gif b/paramecio/modules/admin2/media/images/ajax-loader.gif new file mode 100644 index 0000000..5516707 Binary files /dev/null and b/paramecio/modules/admin2/media/images/ajax-loader.gif differ diff --git a/paramecio/modules/admin2/media/images/background.png b/paramecio/modules/admin2/media/images/background.png new file mode 100644 index 0000000..5660f22 Binary files /dev/null and b/paramecio/modules/admin2/media/images/background.png differ diff --git a/paramecio/modules/admin2/media/images/background_title.png b/paramecio/modules/admin2/media/images/background_title.png new file mode 100644 index 0000000..c716ff3 Binary files /dev/null and b/paramecio/modules/admin2/media/images/background_title.png differ diff --git a/paramecio/modules/admin2/media/images/background_title_login.png b/paramecio/modules/admin2/media/images/background_title_login.png new file mode 100644 index 0000000..99166e1 Binary files /dev/null and b/paramecio/modules/admin2/media/images/background_title_login.png differ diff --git a/paramecio/modules/admin2/media/images/languages/en-US.png b/paramecio/modules/admin2/media/images/languages/en-US.png new file mode 100644 index 0000000..6b89b61 Binary files /dev/null and b/paramecio/modules/admin2/media/images/languages/en-US.png differ diff --git a/paramecio/modules/admin2/media/images/languages/es-ES.png b/paramecio/modules/admin2/media/images/languages/es-ES.png new file mode 100644 index 0000000..1c12d91 Binary files /dev/null and b/paramecio/modules/admin2/media/images/languages/es-ES.png differ diff --git a/paramecio/modules/admin2/media/images/logo.png b/paramecio/modules/admin2/media/images/logo.png new file mode 100644 index 0000000..8999282 Binary files /dev/null and b/paramecio/modules/admin2/media/images/logo.png differ diff --git a/paramecio/modules/admin2/media/js/jquery-3.7.1.min.js b/paramecio/modules/admin2/media/js/jquery-3.7.1.min.js new file mode 100644 index 0000000..7f37b5d --- /dev/null +++ b/paramecio/modules/admin2/media/js/jquery-3.7.1.min.js @@ -0,0 +1,2 @@ +/*! jQuery v3.7.1 | (c) OpenJS Foundation and other contributors | jquery.org/license */ +!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(ie,e){"use strict";var oe=[],r=Object.getPrototypeOf,ae=oe.slice,g=oe.flat?function(e){return oe.flat.call(e)}:function(e){return oe.concat.apply([],e)},s=oe.push,se=oe.indexOf,n={},i=n.toString,ue=n.hasOwnProperty,o=ue.toString,a=o.call(Object),le={},v=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},y=function(e){return null!=e&&e===e.window},C=ie.document,u={type:!0,src:!0,nonce:!0,noModule:!0};function m(e,t,n){var r,i,o=(n=n||C).createElement("script");if(o.text=e,t)for(r in u)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function x(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[i.call(e)]||"object":typeof e}var t="3.7.1",l=/HTML$/i,ce=function(e,t){return new ce.fn.init(e,t)};function c(e){var t=!!e&&"length"in e&&e.length,n=x(e);return!v(e)&&!y(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+ge+")"+ge+"*"),x=new RegExp(ge+"|>"),j=new RegExp(g),A=new RegExp("^"+t+"$"),D={ID:new RegExp("^#("+t+")"),CLASS:new RegExp("^\\.("+t+")"),TAG:new RegExp("^("+t+"|[*])"),ATTR:new RegExp("^"+p),PSEUDO:new RegExp("^"+g),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+ge+"*(even|odd|(([+-]|)(\\d*)n|)"+ge+"*(?:([+-]|)"+ge+"*(\\d+)|))"+ge+"*\\)|)","i"),bool:new RegExp("^(?:"+f+")$","i"),needsContext:new RegExp("^"+ge+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+ge+"*((?:-\\d)?\\d*)"+ge+"*\\)|)(?=[^-]|$)","i")},N=/^(?:input|select|textarea|button)$/i,q=/^h\d$/i,L=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,H=/[+~]/,O=new RegExp("\\\\[\\da-fA-F]{1,6}"+ge+"?|\\\\([^\\r\\n\\f])","g"),P=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},M=function(){V()},R=J(function(e){return!0===e.disabled&&fe(e,"fieldset")},{dir:"parentNode",next:"legend"});try{k.apply(oe=ae.call(ye.childNodes),ye.childNodes),oe[ye.childNodes.length].nodeType}catch(e){k={apply:function(e,t){me.apply(e,ae.call(t))},call:function(e){me.apply(e,ae.call(arguments,1))}}}function I(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(V(e),e=e||T,C)){if(11!==p&&(u=L.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return k.call(n,a),n}else if(f&&(a=f.getElementById(i))&&I.contains(e,a)&&a.id===i)return k.call(n,a),n}else{if(u[2])return k.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&e.getElementsByClassName)return k.apply(n,e.getElementsByClassName(i)),n}if(!(h[t+" "]||d&&d.test(t))){if(c=t,f=e,1===p&&(x.test(t)||m.test(t))){(f=H.test(t)&&U(e.parentNode)||e)==e&&le.scope||((s=e.getAttribute("id"))?s=ce.escapeSelector(s):e.setAttribute("id",s=S)),o=(l=Y(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+Q(l[o]);c=l.join(",")}try{return k.apply(n,f.querySelectorAll(c)),n}catch(e){h(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return re(t.replace(ve,"$1"),e,n,r)}function W(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function F(e){return e[S]=!0,e}function $(e){var t=T.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function B(t){return function(e){return fe(e,"input")&&e.type===t}}function _(t){return function(e){return(fe(e,"input")||fe(e,"button"))&&e.type===t}}function z(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&R(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function X(a){return F(function(o){return o=+o,F(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function U(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}function V(e){var t,n=e?e.ownerDocument||e:ye;return n!=T&&9===n.nodeType&&n.documentElement&&(r=(T=n).documentElement,C=!ce.isXMLDoc(T),i=r.matches||r.webkitMatchesSelector||r.msMatchesSelector,r.msMatchesSelector&&ye!=T&&(t=T.defaultView)&&t.top!==t&&t.addEventListener("unload",M),le.getById=$(function(e){return r.appendChild(e).id=ce.expando,!T.getElementsByName||!T.getElementsByName(ce.expando).length}),le.disconnectedMatch=$(function(e){return i.call(e,"*")}),le.scope=$(function(){return T.querySelectorAll(":scope")}),le.cssHas=$(function(){try{return T.querySelector(":has(*,:jqfake)"),!1}catch(e){return!0}}),le.getById?(b.filter.ID=function(e){var t=e.replace(O,P);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&C){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(O,P);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&C){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):t.querySelectorAll(e)},b.find.CLASS=function(e,t){if("undefined"!=typeof t.getElementsByClassName&&C)return t.getElementsByClassName(e)},d=[],$(function(e){var t;r.appendChild(e).innerHTML="",e.querySelectorAll("[selected]").length||d.push("\\["+ge+"*(?:value|"+f+")"),e.querySelectorAll("[id~="+S+"-]").length||d.push("~="),e.querySelectorAll("a#"+S+"+*").length||d.push(".#.+[+~]"),e.querySelectorAll(":checked").length||d.push(":checked"),(t=T.createElement("input")).setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),r.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&d.push(":enabled",":disabled"),(t=T.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||d.push("\\["+ge+"*name"+ge+"*="+ge+"*(?:''|\"\")")}),le.cssHas||d.push(":has"),d=d.length&&new RegExp(d.join("|")),l=function(e,t){if(e===t)return a=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!le.sortDetached&&t.compareDocumentPosition(e)===n?e===T||e.ownerDocument==ye&&I.contains(ye,e)?-1:t===T||t.ownerDocument==ye&&I.contains(ye,t)?1:o?se.call(o,e)-se.call(o,t):0:4&n?-1:1)}),T}for(e in I.matches=function(e,t){return I(e,null,null,t)},I.matchesSelector=function(e,t){if(V(e),C&&!h[t+" "]&&(!d||!d.test(t)))try{var n=i.call(e,t);if(n||le.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){h(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(O,P),e[3]=(e[3]||e[4]||e[5]||"").replace(O,P),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||I.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&I.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return D.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&j.test(n)&&(t=Y(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(O,P).toLowerCase();return"*"===e?function(){return!0}:function(e){return fe(e,t)}},CLASS:function(e){var t=s[e+" "];return t||(t=new RegExp("(^|"+ge+")"+e+"("+ge+"|$)"))&&s(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=I.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function T(e,n,r){return v(n)?ce.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?ce.grep(e,function(e){return e===n!==r}):"string"!=typeof n?ce.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(ce.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||k,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:S.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof ce?t[0]:t,ce.merge(this,ce.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:C,!0)),w.test(r[1])&&ce.isPlainObject(t))for(r in t)v(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=C.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):v(e)?void 0!==n.ready?n.ready(e):e(ce):ce.makeArray(e,this)}).prototype=ce.fn,k=ce(C);var E=/^(?:parents|prev(?:Until|All))/,j={children:!0,contents:!0,next:!0,prev:!0};function A(e,t){while((e=e[t])&&1!==e.nodeType);return e}ce.fn.extend({has:function(e){var t=ce(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,Ce=/^$|^module$|\/(?:java|ecma)script/i;xe=C.createDocumentFragment().appendChild(C.createElement("div")),(be=C.createElement("input")).setAttribute("type","radio"),be.setAttribute("checked","checked"),be.setAttribute("name","t"),xe.appendChild(be),le.checkClone=xe.cloneNode(!0).cloneNode(!0).lastChild.checked,xe.innerHTML="",le.noCloneChecked=!!xe.cloneNode(!0).lastChild.defaultValue,xe.innerHTML="",le.option=!!xe.lastChild;var ke={thead:[1,"","
    "],col:[2,"","
    "],tr:[2,"","
    "],td:[3,"","
    "],_default:[0,"",""]};function Se(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&fe(e,t)?ce.merge([e],n):n}function Ee(e,t){for(var n=0,r=e.length;n",""]);var je=/<|&#?\w+;/;function Ae(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function Re(e,t){return fe(e,"table")&&fe(11!==t.nodeType?t:t.firstChild,"tr")&&ce(e).children("tbody")[0]||e}function Ie(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function We(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Fe(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(_.hasData(e)&&(s=_.get(e).events))for(i in _.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),C.head.appendChild(r[0])},abort:function(){i&&i()}}});var Jt,Kt=[],Zt=/(=)\?(?=&|$)|\?\?/;ce.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Kt.pop()||ce.expando+"_"+jt.guid++;return this[e]=!0,e}}),ce.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Zt.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Zt.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=v(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Zt,"$1"+r):!1!==e.jsonp&&(e.url+=(At.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||ce.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=ie[r],ie[r]=function(){o=arguments},n.always(function(){void 0===i?ce(ie).removeProp(r):ie[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,Kt.push(r)),o&&v(i)&&i(o[0]),o=i=void 0}),"script"}),le.createHTMLDocument=((Jt=C.implementation.createHTMLDocument("").body).innerHTML="
    ",2===Jt.childNodes.length),ce.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(le.createHTMLDocument?((r=(t=C.implementation.createHTMLDocument("")).createElement("base")).href=C.location.href,t.head.appendChild(r)):t=C),o=!n&&[],(i=w.exec(e))?[t.createElement(i[1])]:(i=Ae([e],t,o),o&&o.length&&ce(o).remove(),ce.merge([],i.childNodes)));var r,i,o},ce.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(ce.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},ce.expr.pseudos.animated=function(t){return ce.grep(ce.timers,function(e){return t===e.elem}).length},ce.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=ce.css(e,"position"),c=ce(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=ce.css(e,"top"),u=ce.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),v(t)&&(t=t.call(e,n,ce.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},ce.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){ce.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===ce.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===ce.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=ce(e).offset()).top+=ce.css(e,"borderTopWidth",!0),i.left+=ce.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-ce.css(r,"marginTop",!0),left:t.left-i.left-ce.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===ce.css(e,"position"))e=e.offsetParent;return e||J})}}),ce.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;ce.fn[t]=function(e){return M(this,function(e,t,n){var r;if(y(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),ce.each(["top","left"],function(e,n){ce.cssHooks[n]=Ye(le.pixelPosition,function(e,t){if(t)return t=Ge(e,n),_e.test(t)?ce(e).position()[n]+"px":t})}),ce.each({Height:"height",Width:"width"},function(a,s){ce.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){ce.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return M(this,function(e,t,n){var r;return y(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?ce.css(e,t,i):ce.style(e,t,n,i)},s,n?e:void 0,n)}})}),ce.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){ce.fn[t]=function(e){return this.on(t,e)}}),ce.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.on("mouseenter",e).on("mouseleave",t||e)}}),ce.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){ce.fn[n]=function(e,t){return 010||Math.abs(a.touches[0].clientY-this.startY)>10)&&(this.touchHasMoved=!0)},_onTouchEnd:function(a){if(this._preventDefault(a),k&&!this.touchHasMoved){if("touchend"===a.type)return void this.toggle();var c=a||b.event;3!==c.which&&2!==c.button&&this.toggle()}},_onKeyUp:function(a){var c=a||b.event;13===c.keyCode&&this.toggle()},_transitions:function(){if(h.animate){var a=g.style,b="max-height "+h.transition+"ms";a.WebkitTransition=a.MozTransition=a.OTransition=a.transition=b}},_calcHeight:function(){for(var a=0,b=0;b0?e=c.__plugins[d]:a.each(c.__plugins,function(a,b){return b.name.substring(b.name.length-d.length-1)=="."+d?(e=b,!1):void 0}),e}if(b.name.indexOf(".")<0)throw new Error("Plugins must be namespaced");return c.__plugins[b.name]=b,b.core&&c.__bridge(b.core,c,b.name),this},_trigger:function(){var a=Array.prototype.slice.apply(arguments);return"string"==typeof a[0]&&(a[0]={type:a[0]}),this.__$emitterPrivate.trigger.apply(this.__$emitterPrivate,a),this.__$emitterPublic.trigger.apply(this.__$emitterPublic,a),this},instances:function(b){var c=[],d=b||".tooltipstered";return a(d).each(function(){var b=a(this),d=b.data("tooltipster-ns");d&&a.each(d,function(a,d){c.push(b.data(d))})}),c},instancesLatest:function(){return this.__instancesLatestArr},off:function(){return this.__$emitterPublic.off.apply(this.__$emitterPublic,Array.prototype.slice.apply(arguments)),this},on:function(){return this.__$emitterPublic.on.apply(this.__$emitterPublic,Array.prototype.slice.apply(arguments)),this},one:function(){return this.__$emitterPublic.one.apply(this.__$emitterPublic,Array.prototype.slice.apply(arguments)),this},origins:function(b){var c=b?b+" ":"";return a(c+".tooltipstered").toArray()},setDefaults:function(b){return a.extend(f,b),this},triggerHandler:function(){return this.__$emitterPublic.triggerHandler.apply(this.__$emitterPublic,Array.prototype.slice.apply(arguments)),this}},a.tooltipster=new i,a.Tooltipster=function(b,c){this.__callbacks={close:[],open:[]},this.__closingTime,this.__Content,this.__contentBcr,this.__destroyed=!1,this.__$emitterPrivate=a({}),this.__$emitterPublic=a({}),this.__enabled=!0,this.__garbageCollector,this.__Geometry,this.__lastPosition,this.__namespace="tooltipster-"+Math.round(1e6*Math.random()),this.__options,this.__$originParents,this.__pointerIsOverOrigin=!1,this.__previousThemes=[],this.__state="closed",this.__timeouts={close:[],open:null},this.__touchEvents=[],this.__tracker=null,this._$origin,this._$tooltip,this.__init(b,c)},a.Tooltipster.prototype={__init:function(b,c){var d=this;if(d._$origin=a(b),d.__options=a.extend(!0,{},f,c),d.__optionsFormat(),!h.IE||h.IE>=d.__options.IEmin){var e=null;if(void 0===d._$origin.data("tooltipster-initialTitle")&&(e=d._$origin.attr("title"),void 0===e&&(e=null),d._$origin.data("tooltipster-initialTitle",e)),null!==d.__options.content)d.__contentSet(d.__options.content);else{var g,i=d._$origin.attr("data-tooltip-content");i&&(g=a(i)),g&&g[0]?d.__contentSet(g.first()):d.__contentSet(e)}d._$origin.removeAttr("title").addClass("tooltipstered"),d.__prepareOrigin(),d.__prepareGC(),a.each(d.__options.plugins,function(a,b){d._plug(b)}),h.hasTouchCapability&&a(h.window.document.body).on("touchmove."+d.__namespace+"-triggerOpen",function(a){d._touchRecordEvent(a)}),d._on("created",function(){d.__prepareTooltip()})._on("repositioned",function(a){d.__lastPosition=a.position})}else d.__options.disabled=!0},__contentInsert:function(){var a=this,b=a._$tooltip.find(".tooltipster-content"),c=a.__Content,d=function(a){c=a};return a._trigger({type:"format",content:a.__Content,format:d}),a.__options.functionFormat&&(c=a.__options.functionFormat.call(a,a,{origin:a._$origin[0]},a.__Content)),"string"!=typeof c||a.__options.contentAsHTML?b.empty().append(c):b.text(c),a},__contentSet:function(b){return b instanceof a&&this.__options.contentCloning&&(b=b.clone(!0)),this.__Content=b,this._trigger({type:"updated",content:b}),this},__destroyError:function(){throw new Error("This tooltip has been destroyed and cannot execute your method call.")},__geometry:function(){var b=this,c=b._$origin,d=b._$origin.is("area");if(d){var e=b._$origin.parent().attr("name");c=a('img[usemap="#'+e+'"]')}var f=c[0].getBoundingClientRect(),g=a(h.window.document),i=a(h.window),j=c,k={available:{document:null,window:null},document:{size:{height:g.height(),width:g.width()}},window:{scroll:{left:h.window.scrollX||h.window.document.documentElement.scrollLeft,top:h.window.scrollY||h.window.document.documentElement.scrollTop},size:{height:i.height(),width:i.width()}},origin:{fixedLineage:!1,offset:{},size:{height:f.bottom-f.top,width:f.right-f.left},usemapImage:d?c[0]:null,windowOffset:{bottom:f.bottom,left:f.left,right:f.right,top:f.top}}};if(d){var l=b._$origin.attr("shape"),m=b._$origin.attr("coords");if(m&&(m=m.split(","),a.map(m,function(a,b){m[b]=parseInt(a)})),"default"!=l)switch(l){case"circle":var n=m[0],o=m[1],p=m[2],q=o-p,r=n-p;k.origin.size.height=2*p,k.origin.size.width=k.origin.size.height,k.origin.windowOffset.left+=r,k.origin.windowOffset.top+=q;break;case"rect":var s=m[0],t=m[1],u=m[2],v=m[3];k.origin.size.height=v-t,k.origin.size.width=u-s,k.origin.windowOffset.left+=s,k.origin.windowOffset.top+=t;break;case"poly":for(var w=0,x=0,y=0,z=0,A="even",B=0;By&&(y=C,0===B&&(w=y)),w>C&&(w=C),A="odd"):(C>z&&(z=C,1==B&&(x=z)),x>C&&(x=C),A="even")}k.origin.size.height=z-x,k.origin.size.width=y-w,k.origin.windowOffset.left+=w,k.origin.windowOffset.top+=x}}var D=function(a){k.origin.size.height=a.height,k.origin.windowOffset.left=a.left,k.origin.windowOffset.top=a.top,k.origin.size.width=a.width};for(b._trigger({type:"geometry",edit:D,geometry:{height:k.origin.size.height,left:k.origin.windowOffset.left,top:k.origin.windowOffset.top,width:k.origin.size.width}}),k.origin.windowOffset.right=k.origin.windowOffset.left+k.origin.size.width,k.origin.windowOffset.bottom=k.origin.windowOffset.top+k.origin.size.height,k.origin.offset.left=k.origin.windowOffset.left+k.window.scroll.left,k.origin.offset.top=k.origin.windowOffset.top+k.window.scroll.top,k.origin.offset.bottom=k.origin.offset.top+k.origin.size.height,k.origin.offset.right=k.origin.offset.left+k.origin.size.width,k.available.document={bottom:{height:k.document.size.height-k.origin.offset.bottom,width:k.document.size.width},left:{height:k.document.size.height,width:k.origin.offset.left},right:{height:k.document.size.height,width:k.document.size.width-k.origin.offset.right},top:{height:k.origin.offset.top,width:k.document.size.width}},k.available.window={bottom:{height:Math.max(k.window.size.height-Math.max(k.origin.windowOffset.bottom,0),0),width:k.window.size.width},left:{height:k.window.size.height,width:Math.max(k.origin.windowOffset.left,0)},right:{height:k.window.size.height,width:Math.max(k.window.size.width-Math.max(k.origin.windowOffset.right,0),0)},top:{height:Math.max(k.origin.windowOffset.top,0),width:k.window.size.width}};"html"!=j[0].tagName.toLowerCase();){if("fixed"==j.css("position")){k.origin.fixedLineage=!0;break}j=j.parent()}return k},__optionsFormat:function(){return"number"==typeof this.__options.animationDuration&&(this.__options.animationDuration=[this.__options.animationDuration,this.__options.animationDuration]),"number"==typeof this.__options.delay&&(this.__options.delay=[this.__options.delay,this.__options.delay]),"number"==typeof this.__options.delayTouch&&(this.__options.delayTouch=[this.__options.delayTouch,this.__options.delayTouch]),"string"==typeof this.__options.theme&&(this.__options.theme=[this.__options.theme]),null===this.__options.parent?this.__options.parent=a(h.window.document.body):"string"==typeof this.__options.parent&&(this.__options.parent=a(this.__options.parent)),"hover"==this.__options.trigger?(this.__options.triggerOpen={mouseenter:!0,touchstart:!0},this.__options.triggerClose={mouseleave:!0,originClick:!0,touchleave:!0}):"click"==this.__options.trigger&&(this.__options.triggerOpen={click:!0,tap:!0},this.__options.triggerClose={click:!0,tap:!0}),this._trigger("options"),this},__prepareGC:function(){var b=this;return b.__options.selfDestruction?b.__garbageCollector=setInterval(function(){var c=(new Date).getTime();b.__touchEvents=a.grep(b.__touchEvents,function(a,b){return c-a.time>6e4}),d(b._$origin)||b.close(function(){b.destroy()})},2e4):clearInterval(b.__garbageCollector),b},__prepareOrigin:function(){var a=this;if(a._$origin.off("."+a.__namespace+"-triggerOpen"),h.hasTouchCapability&&a._$origin.on("touchstart."+a.__namespace+"-triggerOpen touchend."+a.__namespace+"-triggerOpen touchcancel."+a.__namespace+"-triggerOpen",function(b){a._touchRecordEvent(b)}),a.__options.triggerOpen.click||a.__options.triggerOpen.tap&&h.hasTouchCapability){var b="";a.__options.triggerOpen.click&&(b+="click."+a.__namespace+"-triggerOpen "),a.__options.triggerOpen.tap&&h.hasTouchCapability&&(b+="touchend."+a.__namespace+"-triggerOpen"),a._$origin.on(b,function(b){a._touchIsMeaningfulEvent(b)&&a._open(b)})}if(a.__options.triggerOpen.mouseenter||a.__options.triggerOpen.touchstart&&h.hasTouchCapability){var b="";a.__options.triggerOpen.mouseenter&&(b+="mouseenter."+a.__namespace+"-triggerOpen "),a.__options.triggerOpen.touchstart&&h.hasTouchCapability&&(b+="touchstart."+a.__namespace+"-triggerOpen"),a._$origin.on(b,function(b){!a._touchIsTouchEvent(b)&&a._touchIsEmulatedEvent(b)||(a.__pointerIsOverOrigin=!0,a._openShortly(b))})}if(a.__options.triggerClose.mouseleave||a.__options.triggerClose.touchleave&&h.hasTouchCapability){var b="";a.__options.triggerClose.mouseleave&&(b+="mouseleave."+a.__namespace+"-triggerOpen "),a.__options.triggerClose.touchleave&&h.hasTouchCapability&&(b+="touchend."+a.__namespace+"-triggerOpen touchcancel."+a.__namespace+"-triggerOpen"),a._$origin.on(b,function(b){a._touchIsMeaningfulEvent(b)&&(a.__pointerIsOverOrigin=!1)})}return a},__prepareTooltip:function(){var b=this,c=b.__options.interactive?"auto":"";return b._$tooltip.attr("id",b.__namespace).css({"pointer-events":c,zIndex:b.__options.zIndex}),a.each(b.__previousThemes,function(a,c){b._$tooltip.removeClass(c)}),a.each(b.__options.theme,function(a,c){b._$tooltip.addClass(c)}),b.__previousThemes=a.merge([],b.__options.theme),b},__scrollHandler:function(b){var c=this;if(c.__options.triggerClose.scroll)c._close(b);else if(d(c._$origin)&&d(c._$tooltip)){var e=null;if(b.target===h.window.document)c.__Geometry.origin.fixedLineage||c.__options.repositionOnScroll&&c.reposition(b);else{e=c.__geometry();var f=!1;if("fixed"!=c._$origin.css("position")&&c.__$originParents.each(function(b,c){var d=a(c),g=d.css("overflow-x"),h=d.css("overflow-y");if("visible"!=g||"visible"!=h){var i=c.getBoundingClientRect();if("visible"!=g&&(e.origin.windowOffset.lefti.right))return f=!0,!1;if("visible"!=h&&(e.origin.windowOffset.topi.bottom))return f=!0,!1}return"fixed"==d.css("position")?!1:void 0}),f)c._$tooltip.css("visibility","hidden");else if(c._$tooltip.css("visibility","visible"),c.__options.repositionOnScroll)c.reposition(b);else{var g=e.origin.offset.left-c.__Geometry.origin.offset.left,i=e.origin.offset.top-c.__Geometry.origin.offset.top;c._$tooltip.css({left:c.__lastPosition.coord.left+g,top:c.__lastPosition.coord.top+i})}}c._trigger({type:"scroll",event:b,geo:e})}return c},__stateSet:function(a){return this.__state=a,this._trigger({type:"state",state:a}),this},__timeoutsClear:function(){return clearTimeout(this.__timeouts.open),this.__timeouts.open=null,a.each(this.__timeouts.close,function(a,b){clearTimeout(b)}),this.__timeouts.close=[],this},__trackerStart:function(){var a=this,b=a._$tooltip.find(".tooltipster-content");return a.__options.trackTooltip&&(a.__contentBcr=b[0].getBoundingClientRect()),a.__tracker=setInterval(function(){if(d(a._$origin)&&d(a._$tooltip)){if(a.__options.trackOrigin){var e=a.__geometry(),f=!1;c(e.origin.size,a.__Geometry.origin.size)&&(a.__Geometry.origin.fixedLineage?c(e.origin.windowOffset,a.__Geometry.origin.windowOffset)&&(f=!0):c(e.origin.offset,a.__Geometry.origin.offset)&&(f=!0)),f||(a.__options.triggerClose.mouseleave?a._close():a.reposition())}if(a.__options.trackTooltip){var g=b[0].getBoundingClientRect();g.height===a.__contentBcr.height&&g.width===a.__contentBcr.width||(a.reposition(),a.__contentBcr=g)}}else a._close()},a.__options.trackerInterval),a},_close:function(b,c,d){var e=this,f=!0;if(e._trigger({type:"close",event:b,stop:function(){f=!1}}),f||d){c&&e.__callbacks.close.push(c),e.__callbacks.open=[],e.__timeoutsClear();var g=function(){a.each(e.__callbacks.close,function(a,c){c.call(e,e,{event:b,origin:e._$origin[0]})}),e.__callbacks.close=[]};if("closed"!=e.__state){var i=!0,j=new Date,k=j.getTime(),l=k+e.__options.animationDuration[1];if("disappearing"==e.__state&&l>e.__closingTime&&e.__options.animationDuration[1]>0&&(i=!1),i){e.__closingTime=l,"disappearing"!=e.__state&&e.__stateSet("disappearing");var m=function(){clearInterval(e.__tracker),e._trigger({type:"closing",event:b}),e._$tooltip.off("."+e.__namespace+"-triggerClose").removeClass("tooltipster-dying"),a(h.window).off("."+e.__namespace+"-triggerClose"),e.__$originParents.each(function(b,c){a(c).off("scroll."+e.__namespace+"-triggerClose")}),e.__$originParents=null,a(h.window.document.body).off("."+e.__namespace+"-triggerClose"),e._$origin.off("."+e.__namespace+"-triggerClose"),e._off("dismissable"),e.__stateSet("closed"),e._trigger({type:"after",event:b}),e.__options.functionAfter&&e.__options.functionAfter.call(e,e,{event:b,origin:e._$origin[0]}),g()};h.hasTransitions?(e._$tooltip.css({"-moz-animation-duration":e.__options.animationDuration[1]+"ms","-ms-animation-duration":e.__options.animationDuration[1]+"ms","-o-animation-duration":e.__options.animationDuration[1]+"ms","-webkit-animation-duration":e.__options.animationDuration[1]+"ms","animation-duration":e.__options.animationDuration[1]+"ms","transition-duration":e.__options.animationDuration[1]+"ms"}),e._$tooltip.clearQueue().removeClass("tooltipster-show").addClass("tooltipster-dying"),e.__options.animationDuration[1]>0&&e._$tooltip.delay(e.__options.animationDuration[1]),e._$tooltip.queue(m)):e._$tooltip.stop().fadeOut(e.__options.animationDuration[1],m)}}else g()}return e},_off:function(){return this.__$emitterPrivate.off.apply(this.__$emitterPrivate,Array.prototype.slice.apply(arguments)),this},_on:function(){return this.__$emitterPrivate.on.apply(this.__$emitterPrivate,Array.prototype.slice.apply(arguments)),this},_one:function(){return this.__$emitterPrivate.one.apply(this.__$emitterPrivate,Array.prototype.slice.apply(arguments)),this},_open:function(b,c){var e=this;if(!e.__destroying&&d(e._$origin)&&e.__enabled){var f=!0;if("closed"==e.__state&&(e._trigger({type:"before",event:b,stop:function(){f=!1}}),f&&e.__options.functionBefore&&(f=e.__options.functionBefore.call(e,e,{event:b,origin:e._$origin[0]}))),f!==!1&&null!==e.__Content){c&&e.__callbacks.open.push(c),e.__callbacks.close=[],e.__timeoutsClear();var g,i=function(){"stable"!=e.__state&&e.__stateSet("stable"),a.each(e.__callbacks.open,function(a,b){b.call(e,e,{origin:e._$origin[0],tooltip:e._$tooltip[0]})}),e.__callbacks.open=[]};if("closed"!==e.__state)g=0,"disappearing"===e.__state?(e.__stateSet("appearing"),h.hasTransitions?(e._$tooltip.clearQueue().removeClass("tooltipster-dying").addClass("tooltipster-show"),e.__options.animationDuration[0]>0&&e._$tooltip.delay(e.__options.animationDuration[0]),e._$tooltip.queue(i)):e._$tooltip.stop().fadeIn(i)):"stable"==e.__state&&i();else{if(e.__stateSet("appearing"),g=e.__options.animationDuration[0],e.__contentInsert(),e.reposition(b,!0),h.hasTransitions?(e._$tooltip.addClass("tooltipster-"+e.__options.animation).addClass("tooltipster-initial").css({"-moz-animation-duration":e.__options.animationDuration[0]+"ms","-ms-animation-duration":e.__options.animationDuration[0]+"ms","-o-animation-duration":e.__options.animationDuration[0]+"ms","-webkit-animation-duration":e.__options.animationDuration[0]+"ms","animation-duration":e.__options.animationDuration[0]+"ms","transition-duration":e.__options.animationDuration[0]+"ms"}),setTimeout(function(){"closed"!=e.__state&&(e._$tooltip.addClass("tooltipster-show").removeClass("tooltipster-initial"),e.__options.animationDuration[0]>0&&e._$tooltip.delay(e.__options.animationDuration[0]),e._$tooltip.queue(i))},0)):e._$tooltip.css("display","none").fadeIn(e.__options.animationDuration[0],i),e.__trackerStart(),a(h.window).on("resize."+e.__namespace+"-triggerClose",function(b){var c=a(document.activeElement);(c.is("input")||c.is("textarea"))&&a.contains(e._$tooltip[0],c[0])||e.reposition(b)}).on("scroll."+e.__namespace+"-triggerClose",function(a){e.__scrollHandler(a)}),e.__$originParents=e._$origin.parents(),e.__$originParents.each(function(b,c){a(c).on("scroll."+e.__namespace+"-triggerClose",function(a){e.__scrollHandler(a)})}),e.__options.triggerClose.mouseleave||e.__options.triggerClose.touchleave&&h.hasTouchCapability){e._on("dismissable",function(a){a.dismissable?a.delay?(m=setTimeout(function(){e._close(a.event)},a.delay),e.__timeouts.close.push(m)):e._close(a):clearTimeout(m)});var j=e._$origin,k="",l="",m=null;e.__options.interactive&&(j=j.add(e._$tooltip)),e.__options.triggerClose.mouseleave&&(k+="mouseenter."+e.__namespace+"-triggerClose ",l+="mouseleave."+e.__namespace+"-triggerClose "),e.__options.triggerClose.touchleave&&h.hasTouchCapability&&(k+="touchstart."+e.__namespace+"-triggerClose",l+="touchend."+e.__namespace+"-triggerClose touchcancel."+e.__namespace+"-triggerClose"),j.on(l,function(a){if(e._touchIsTouchEvent(a)||!e._touchIsEmulatedEvent(a)){var b="mouseleave"==a.type?e.__options.delay:e.__options.delayTouch;e._trigger({delay:b[1],dismissable:!0,event:a,type:"dismissable"})}}).on(k,function(a){!e._touchIsTouchEvent(a)&&e._touchIsEmulatedEvent(a)||e._trigger({dismissable:!1,event:a,type:"dismissable"})})}e.__options.triggerClose.originClick&&e._$origin.on("click."+e.__namespace+"-triggerClose",function(a){e._touchIsTouchEvent(a)||e._touchIsEmulatedEvent(a)||e._close(a)}),(e.__options.triggerClose.click||e.__options.triggerClose.tap&&h.hasTouchCapability)&&setTimeout(function(){if("closed"!=e.__state){var b="",c=a(h.window.document.body);e.__options.triggerClose.click&&(b+="click."+e.__namespace+"-triggerClose "),e.__options.triggerClose.tap&&h.hasTouchCapability&&(b+="touchend."+e.__namespace+"-triggerClose"),c.on(b,function(b){e._touchIsMeaningfulEvent(b)&&(e._touchRecordEvent(b),e.__options.interactive&&a.contains(e._$tooltip[0],b.target)||e._close(b))}),e.__options.triggerClose.tap&&h.hasTouchCapability&&c.on("touchstart."+e.__namespace+"-triggerClose",function(a){e._touchRecordEvent(a)})}},0),e._trigger("ready"),e.__options.functionReady&&e.__options.functionReady.call(e,e,{origin:e._$origin[0],tooltip:e._$tooltip[0]})}if(e.__options.timer>0){var m=setTimeout(function(){e._close()},e.__options.timer+g);e.__timeouts.close.push(m)}}}return e},_openShortly:function(a){var b=this,c=!0;if("stable"!=b.__state&&"appearing"!=b.__state&&!b.__timeouts.open&&(b._trigger({type:"start",event:a,stop:function(){c=!1}}),c)){var d=0==a.type.indexOf("touch")?b.__options.delayTouch:b.__options.delay;d[0]?b.__timeouts.open=setTimeout(function(){b.__timeouts.open=null,b.__pointerIsOverOrigin&&b._touchIsMeaningfulEvent(a)?(b._trigger("startend"),b._open(a)):b._trigger("startcancel")},d[0]):(b._trigger("startend"),b._open(a))}return b},_optionsExtract:function(b,c){var d=this,e=a.extend(!0,{},c),f=d.__options[b];return f||(f={},a.each(c,function(a,b){var c=d.__options[a];void 0!==c&&(f[a]=c)})),a.each(e,function(b,c){void 0!==f[b]&&("object"!=typeof c||c instanceof Array||null==c||"object"!=typeof f[b]||f[b]instanceof Array||null==f[b]?e[b]=f[b]:a.extend(e[b],f[b]))}),e},_plug:function(b){var c=a.tooltipster._plugin(b);if(!c)throw new Error('The "'+b+'" plugin is not defined');return c.instance&&a.tooltipster.__bridge(c.instance,this,c.name),this},_touchIsEmulatedEvent:function(a){for(var b=!1,c=(new Date).getTime(),d=this.__touchEvents.length-1;d>=0;d--){var e=this.__touchEvents[d];if(!(c-e.time<500))break;e.target===a.target&&(b=!0)}return b},_touchIsMeaningfulEvent:function(a){return this._touchIsTouchEvent(a)&&!this._touchSwiped(a.target)||!this._touchIsTouchEvent(a)&&!this._touchIsEmulatedEvent(a)},_touchIsTouchEvent:function(a){return 0==a.type.indexOf("touch")},_touchRecordEvent:function(a){return this._touchIsTouchEvent(a)&&(a.time=(new Date).getTime(),this.__touchEvents.push(a)),this},_touchSwiped:function(a){for(var b=!1,c=this.__touchEvents.length-1;c>=0;c--){var d=this.__touchEvents[c];if("touchmove"==d.type){b=!0;break}if("touchstart"==d.type&&a===d.target)break}return b},_trigger:function(){var b=Array.prototype.slice.apply(arguments);return"string"==typeof b[0]&&(b[0]={type:b[0]}),b[0].instance=this,b[0].origin=this._$origin?this._$origin[0]:null,b[0].tooltip=this._$tooltip?this._$tooltip[0]:null,this.__$emitterPrivate.trigger.apply(this.__$emitterPrivate,b),a.tooltipster._trigger.apply(a.tooltipster,b),this.__$emitterPublic.trigger.apply(this.__$emitterPublic,b),this},_unplug:function(b){var c=this;if(c[b]){var d=a.tooltipster._plugin(b);d.instance&&a.each(d.instance,function(a,d){c[a]&&c[a].bridged===c[b]&&delete c[a]}),c[b].__destroy&&c[b].__destroy(),delete c[b]}return c},close:function(a){return this.__destroyed?this.__destroyError():this._close(null,a),this},content:function(a){var b=this;if(void 0===a)return b.__Content;if(b.__destroyed)b.__destroyError();else if(b.__contentSet(a),null!==b.__Content){if("closed"!==b.__state&&(b.__contentInsert(),b.reposition(),b.__options.updateAnimation))if(h.hasTransitions){var c=b.__options.updateAnimation;b._$tooltip.addClass("tooltipster-update-"+c),setTimeout(function(){"closed"!=b.__state&&b._$tooltip.removeClass("tooltipster-update-"+c)},1e3)}else b._$tooltip.fadeTo(200,.5,function(){"closed"!=b.__state&&b._$tooltip.fadeTo(200,1)})}else b._close();return b},destroy:function(){var b=this;if(b.__destroyed)b.__destroyError();else{"closed"!=b.__state?b.option("animationDuration",0)._close(null,null,!0):b.__timeoutsClear(),b._trigger("destroy"),b.__destroyed=!0,b._$origin.removeData(b.__namespace).off("."+b.__namespace+"-triggerOpen"),a(h.window.document.body).off("."+b.__namespace+"-triggerOpen");var c=b._$origin.data("tooltipster-ns");if(c)if(1===c.length){var d=null;"previous"==b.__options.restoration?d=b._$origin.data("tooltipster-initialTitle"):"current"==b.__options.restoration&&(d="string"==typeof b.__Content?b.__Content:a("
    ").append(b.__Content).html()),d&&b._$origin.attr("title",d),b._$origin.removeClass("tooltipstered"),b._$origin.removeData("tooltipster-ns").removeData("tooltipster-initialTitle")}else c=a.grep(c,function(a,c){return a!==b.__namespace}),b._$origin.data("tooltipster-ns",c);b._trigger("destroyed"),b._off(),b.off(),b.__Content=null,b.__$emitterPrivate=null,b.__$emitterPublic=null,b.__options.parent=null,b._$origin=null,b._$tooltip=null,a.tooltipster.__instancesLatestArr=a.grep(a.tooltipster.__instancesLatestArr,function(a,c){return b!==a}),clearInterval(b.__garbageCollector)}return b},disable:function(){return this.__destroyed?(this.__destroyError(),this):(this._close(),this.__enabled=!1,this)},elementOrigin:function(){return this.__destroyed?void this.__destroyError():this._$origin[0]},elementTooltip:function(){return this._$tooltip?this._$tooltip[0]:null},enable:function(){return this.__enabled=!0,this},hide:function(a){return this.close(a)},instance:function(){return this},off:function(){return this.__destroyed||this.__$emitterPublic.off.apply(this.__$emitterPublic,Array.prototype.slice.apply(arguments)),this},on:function(){return this.__destroyed?this.__destroyError():this.__$emitterPublic.on.apply(this.__$emitterPublic,Array.prototype.slice.apply(arguments)),this},one:function(){return this.__destroyed?this.__destroyError():this.__$emitterPublic.one.apply(this.__$emitterPublic,Array.prototype.slice.apply(arguments)),this},open:function(a){return this.__destroyed?this.__destroyError():this._open(null,a),this},option:function(b,c){return void 0===c?this.__options[b]:(this.__destroyed?this.__destroyError():(this.__options[b]=c,this.__optionsFormat(),a.inArray(b,["trigger","triggerClose","triggerOpen"])>=0&&this.__prepareOrigin(),"selfDestruction"===b&&this.__prepareGC()),this)},reposition:function(a,b){var c=this;return c.__destroyed?c.__destroyError():"closed"!=c.__state&&d(c._$origin)&&(b||d(c._$tooltip))&&(b||c._$tooltip.detach(),c.__Geometry=c.__geometry(),c._trigger({type:"reposition",event:a,helper:{geo:c.__Geometry}})),c},show:function(a){return this.open(a)},status:function(){return{destroyed:this.__destroyed,enabled:this.__enabled,open:"closed"!==this.__state,state:this.__state}},triggerHandler:function(){return this.__destroyed?this.__destroyError():this.__$emitterPublic.triggerHandler.apply(this.__$emitterPublic,Array.prototype.slice.apply(arguments)),this}},a.fn.tooltipster=function(){var b=Array.prototype.slice.apply(arguments),c="You are using a single HTML element as content for several tooltips. You probably want to set the contentCloning option to TRUE.";if(0===this.length)return this;if("string"==typeof b[0]){var d="#*$~&";return this.each(function(){var e=a(this).data("tooltipster-ns"),f=e?a(this).data(e[0]):null;if(!f)throw new Error("You called Tooltipster's \""+b[0]+'" method on an uninitialized element');if("function"!=typeof f[b[0]])throw new Error('Unknown method "'+b[0]+'"');this.length>1&&"content"==b[0]&&(b[1]instanceof a||"object"==typeof b[1]&&null!=b[1]&&b[1].tagName)&&!f.__options.contentCloning&&f.__options.debug&&console.log(c);var g=f[b[0]](b[1],b[2]);return g!==f||"instance"===b[0]?(d=g,!1):void 0}),"#*$~&"!==d?d:this}a.tooltipster.__instancesLatestArr=[];var e=b[0]&&void 0!==b[0].multiple,g=e&&b[0].multiple||!e&&f.multiple,h=b[0]&&void 0!==b[0].content,i=h&&b[0].content||!h&&f.content,j=b[0]&&void 0!==b[0].contentCloning,k=j&&b[0].contentCloning||!j&&f.contentCloning,l=b[0]&&void 0!==b[0].debug,m=l&&b[0].debug||!l&&f.debug;return this.length>1&&(i instanceof a||"object"==typeof i&&null!=i&&i.tagName)&&!k&&m&&console.log(c),this.each(function(){var c=!1,d=a(this),e=d.data("tooltipster-ns"),f=null;e?g?c=!0:m&&(console.log("Tooltipster: one or more tooltips are already attached to the element below. Ignoring."),console.log(this)):c=!0,c&&(f=new a.Tooltipster(this,b[0]),e||(e=[]),e.push(f.__namespace),d.data("tooltipster-ns",e),d.data(f.__namespace,f),f.__options.functionInit&&f.__options.functionInit.call(f,f,{origin:this}),f._trigger("init")),a.tooltipster.__instancesLatestArr.push(f)}),this},b.prototype={__init:function(b){this.__$tooltip=b,this.__$tooltip.css({left:0,overflow:"hidden",position:"absolute",top:0}).find(".tooltipster-content").css("overflow","auto"),this.$container=a('
    ').append(this.__$tooltip).appendTo(h.window.document.body)},__forceRedraw:function(){var a=this.__$tooltip.parent();this.__$tooltip.detach(),this.__$tooltip.appendTo(a)},constrain:function(a,b){return this.constraints={width:a,height:b},this.__$tooltip.css({display:"block",height:"",overflow:"auto",width:a}),this},destroy:function(){this.__$tooltip.detach().find(".tooltipster-content").css({display:"",overflow:""}),this.$container.remove()},free:function(){return this.constraints=null,this.__$tooltip.css({display:"",height:"",overflow:"visible",width:""}),this},measure:function(){this.__forceRedraw();var a=this.__$tooltip[0].getBoundingClientRect(),b={size:{height:a.height||a.bottom-a.top,width:a.width||a.right-a.left}};if(this.constraints){var c=this.__$tooltip.find(".tooltipster-content"),d=this.__$tooltip.outerHeight(),e=c[0].getBoundingClientRect(),f={height:d<=this.constraints.height,width:a.width<=this.constraints.width&&e.width>=c[0].scrollWidth-1};b.fits=f.height&&f.width}return h.IE&&h.IE<=11&&b.size.width!==h.window.document.documentElement.clientWidth&&(b.size.width=Math.ceil(b.size.width)+1),b}};var j=navigator.userAgent.toLowerCase();-1!=j.indexOf("msie")?h.IE=parseInt(j.split("msie")[1]):-1!==j.toLowerCase().indexOf("trident")&&-1!==j.indexOf(" rv:11")?h.IE=11:-1!=j.toLowerCase().indexOf("edge/")&&(h.IE=parseInt(j.toLowerCase().split("edge/")[1]));var k="tooltipster.sideTip";return a.tooltipster._plugin({name:k,instance:{__defaults:function(){return{arrow:!0,distance:6,functionPosition:null,maxWidth:null,minIntersection:16,minWidth:0,position:null,side:"top",viewportAware:!0}},__init:function(a){var b=this;b.__instance=a,b.__namespace="tooltipster-sideTip-"+Math.round(1e6*Math.random()),b.__previousState="closed",b.__options,b.__optionsFormat(),b.__instance._on("state."+b.__namespace,function(a){"closed"==a.state?b.__close():"appearing"==a.state&&"closed"==b.__previousState&&b.__create(),b.__previousState=a.state}),b.__instance._on("options."+b.__namespace,function(){b.__optionsFormat()}),b.__instance._on("reposition."+b.__namespace,function(a){b.__reposition(a.event,a.helper)})},__close:function(){this.__instance.content()instanceof a&&this.__instance.content().detach(),this.__instance._$tooltip.remove(),this.__instance._$tooltip=null},__create:function(){var b=a('
    ');this.__options.arrow||b.find(".tooltipster-box").css("margin",0).end().find(".tooltipster-arrow").hide(),this.__options.minWidth&&b.css("min-width",this.__options.minWidth+"px"),this.__options.maxWidth&&b.css("max-width",this.__options.maxWidth+"px"), +this.__instance._$tooltip=b,this.__instance._trigger("created")},__destroy:function(){this.__instance._off("."+self.__namespace)},__optionsFormat:function(){var b=this;if(b.__options=b.__instance._optionsExtract(k,b.__defaults()),b.__options.position&&(b.__options.side=b.__options.position),"object"!=typeof b.__options.distance&&(b.__options.distance=[b.__options.distance]),b.__options.distance.length<4&&(void 0===b.__options.distance[1]&&(b.__options.distance[1]=b.__options.distance[0]),void 0===b.__options.distance[2]&&(b.__options.distance[2]=b.__options.distance[0]),void 0===b.__options.distance[3]&&(b.__options.distance[3]=b.__options.distance[1]),b.__options.distance={top:b.__options.distance[0],right:b.__options.distance[1],bottom:b.__options.distance[2],left:b.__options.distance[3]}),"string"==typeof b.__options.side){var c={top:"bottom",right:"left",bottom:"top",left:"right"};b.__options.side=[b.__options.side,c[b.__options.side]],"left"==b.__options.side[0]||"right"==b.__options.side[0]?b.__options.side.push("top","bottom"):b.__options.side.push("right","left")}6===a.tooltipster._env.IE&&b.__options.arrow!==!0&&(b.__options.arrow=!1)},__reposition:function(b,c){var d,e=this,f=e.__targetFind(c),g=[];e.__instance._$tooltip.detach();var h=e.__instance._$tooltip.clone(),i=a.tooltipster._getRuler(h),j=!1,k=e.__instance.option("animation");switch(k&&h.removeClass("tooltipster-"+k),a.each(["window","document"],function(d,k){var l=null;if(e.__instance._trigger({container:k,helper:c,satisfied:j,takeTest:function(a){l=a},results:g,type:"positionTest"}),1==l||0!=l&&0==j&&("window"!=k||e.__options.viewportAware))for(var d=0;d=h.outerSize.width&&c.geo.available[k][n].height>=h.outerSize.height?h.fits=!0:h.fits=!1:h.fits=p.fits,"window"==k&&(h.fits?"top"==n||"bottom"==n?h.whole=c.geo.origin.windowOffset.right>=e.__options.minIntersection&&c.geo.window.size.width-c.geo.origin.windowOffset.left>=e.__options.minIntersection:h.whole=c.geo.origin.windowOffset.bottom>=e.__options.minIntersection&&c.geo.window.size.height-c.geo.origin.windowOffset.top>=e.__options.minIntersection:h.whole=!1),g.push(h),h.whole)j=!0;else if("natural"==h.mode&&(h.fits||h.size.width<=c.geo.available[k][n].width))return!1}})}}),e.__instance._trigger({edit:function(a){g=a},event:b,helper:c,results:g,type:"positionTested"}),g.sort(function(a,b){if(a.whole&&!b.whole)return-1;if(!a.whole&&b.whole)return 1;if(a.whole&&b.whole){var c=e.__options.side.indexOf(a.side),d=e.__options.side.indexOf(b.side);return d>c?-1:c>d?1:"natural"==a.mode?-1:1}if(a.fits&&!b.fits)return-1;if(!a.fits&&b.fits)return 1;if(a.fits&&b.fits){var c=e.__options.side.indexOf(a.side),d=e.__options.side.indexOf(b.side);return d>c?-1:c>d?1:"natural"==a.mode?-1:1}return"document"==a.container&&"bottom"==a.side&&"natural"==a.mode?-1:1}),d=g[0],d.coord={},d.side){case"left":case"right":d.coord.top=Math.floor(d.target-d.size.height/2);break;case"bottom":case"top":d.coord.left=Math.floor(d.target-d.size.width/2)}switch(d.side){case"left":d.coord.left=c.geo.origin.windowOffset.left-d.outerSize.width;break;case"right":d.coord.left=c.geo.origin.windowOffset.right+d.distance.horizontal;break;case"top":d.coord.top=c.geo.origin.windowOffset.top-d.outerSize.height;break;case"bottom":d.coord.top=c.geo.origin.windowOffset.bottom+d.distance.vertical}"window"==d.container?"top"==d.side||"bottom"==d.side?d.coord.left<0?c.geo.origin.windowOffset.right-this.__options.minIntersection>=0?d.coord.left=0:d.coord.left=c.geo.origin.windowOffset.right-this.__options.minIntersection-1:d.coord.left>c.geo.window.size.width-d.size.width&&(c.geo.origin.windowOffset.left+this.__options.minIntersection<=c.geo.window.size.width?d.coord.left=c.geo.window.size.width-d.size.width:d.coord.left=c.geo.origin.windowOffset.left+this.__options.minIntersection+1-d.size.width):d.coord.top<0?c.geo.origin.windowOffset.bottom-this.__options.minIntersection>=0?d.coord.top=0:d.coord.top=c.geo.origin.windowOffset.bottom-this.__options.minIntersection-1:d.coord.top>c.geo.window.size.height-d.size.height&&(c.geo.origin.windowOffset.top+this.__options.minIntersection<=c.geo.window.size.height?d.coord.top=c.geo.window.size.height-d.size.height:d.coord.top=c.geo.origin.windowOffset.top+this.__options.minIntersection+1-d.size.height):(d.coord.left>c.geo.window.size.width-d.size.width&&(d.coord.left=c.geo.window.size.width-d.size.width),d.coord.left<0&&(d.coord.left=0)),e.__sideChange(h,d.side),c.tooltipClone=h[0],c.tooltipParent=e.__instance.option("parent").parent[0],c.mode=d.mode,c.whole=d.whole,c.origin=e.__instance._$origin[0],c.tooltip=e.__instance._$tooltip[0],delete d.container,delete d.fits,delete d.mode,delete d.outerSize,delete d.whole,d.distance=d.distance.horizontal||d.distance.vertical;var l=a.extend(!0,{},d);if(e.__instance._trigger({edit:function(a){d=a},event:b,helper:c,position:l,type:"position"}),e.__options.functionPosition){var m=e.__options.functionPosition.call(e,e.__instance,c,l);m&&(d=m)}i.destroy();var n,o;"top"==d.side||"bottom"==d.side?(n={prop:"left",val:d.target-d.coord.left},o=d.size.width-this.__options.minIntersection):(n={prop:"top",val:d.target-d.coord.top},o=d.size.height-this.__options.minIntersection),n.valo&&(n.val=o);var p;p=c.geo.origin.fixedLineage?c.geo.origin.windowOffset:{left:c.geo.origin.windowOffset.left+c.geo.window.scroll.left,top:c.geo.origin.windowOffset.top+c.geo.window.scroll.top},d.coord={left:p.left+(d.coord.left-c.geo.origin.windowOffset.left),top:p.top+(d.coord.top-c.geo.origin.windowOffset.top)},e.__sideChange(e.__instance._$tooltip,d.side),c.geo.origin.fixedLineage?e.__instance._$tooltip.css("position","fixed"):e.__instance._$tooltip.css("position",""),e.__instance._$tooltip.css({left:d.coord.left,top:d.coord.top,height:d.size.height,width:d.size.width}).find(".tooltipster-arrow").css({left:"",top:""}).css(n.prop,n.val),e.__instance._$tooltip.appendTo(e.__instance.option("parent")),e.__instance._trigger({type:"repositioned",event:b,position:d})},__sideChange:function(a,b){a.removeClass("tooltipster-bottom").removeClass("tooltipster-left").removeClass("tooltipster-right").removeClass("tooltipster-top").addClass("tooltipster-"+b)},__targetFind:function(a){var b={},c=this.__instance._$origin[0].getClientRects();if(c.length>1){var d=this.__instance._$origin.css("opacity");1==d&&(this.__instance._$origin.css("opacity",.99),c=this.__instance._$origin[0].getClientRects(),this.__instance._$origin.css("opacity",1))}if(c.length<2)b.top=Math.floor(a.geo.origin.windowOffset.left+a.geo.origin.size.width/2),b.bottom=b.top,b.left=Math.floor(a.geo.origin.windowOffset.top+a.geo.origin.size.height/2),b.right=b.left;else{var e=c[0];b.top=Math.floor(e.left+(e.right-e.left)/2),e=c.length>2?c[Math.ceil(c.length/2)-1]:c[0],b.right=Math.floor(e.top+(e.bottom-e.top)/2),e=c[c.length-1],b.bottom=Math.floor(e.left+(e.right-e.left)/2),e=c.length>2?c[Math.ceil((c.length+1)/2)-1]:c[c.length-1],b.left=Math.floor(e.top+(e.bottom-e.top)/2)}return b}}}),a}); \ No newline at end of file diff --git a/paramecio/modules/admin2/models/__init__.py b/paramecio/modules/admin2/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/paramecio/modules/admin2/models/admin.py b/paramecio/modules/admin2/models/admin.py new file mode 100644 index 0000000..a7654e1 --- /dev/null +++ b/paramecio/modules/admin2/models/admin.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 + +from paramecio.libraries.i18n import I18n +from paramecio.libraries.db.webmodel import WebModel +#from paramecio.libraries.db.usermodel import UserModel +from paramecio.libraries.db import corefields +from paramecio.libraries.db.extrafields.emailfield import EmailField +from paramecio.libraries.db.extrafields.passwordfield import PasswordField +from paramecio.libraries.db.extrafields.langfield import LangField +from paramecio.libraries.db.extrafields.ipfield import IpField +from paramecio.libraries.db.extrafields.datetimefield import DateTimeField +from paramecio.libraries.db.usermodel import UserModel + +class PrivilegesField2(corefields.IntegerField): + + def show_formatted(self, value): + + value=int(value) + + if value==0: + return I18n.lang('admin', 'without_privileges', 'Without privileges') + elif value==1: + return I18n.lang('admin', 'selected_privileges', 'Selected privileges') + elif value==2: + return I18n.lang('admin', 'administrator', 'Administrator') + +class UserAdmin2(UserModel): + + #def create_fields(self): + def __init__(self, connection=None): + + super().__init__(connection) + + # I can change other fields here, how the name. + + self.register(corefields.CharField('username')) + + self.fields['username'].required=True + + self.register(PasswordField('password')) + + self.fields['password'].required=True + + self.register(EmailField('email')) + + self.fields['email'].required=True + + self.register(corefields.CharField('token_recovery')) + + self.register(corefields.CharField('token_login')) + + #self.register(PasswordField('token_auth')) + + self.register(PrivilegesField2('privileges')) + + self.register(LangField('lang', 20)) + + self.register(corefields.BooleanField('disabled')) + + self.register(corefields.BooleanField('double_auth')) + + self.register(corefields.BooleanField('dark_theme')) + + #self.register(corefields.IntegerField('num_tries', 1)) + + self.register(DateTimeField('last_login')) + +class LoginTries2(WebModel): + + def __init__(self, connection=None): + + super().__init__(connection) + self.register(IpField('ip')) + self.register(corefields.IntegerField('num_tries', 1)) + self.register(DateTimeField('last_login')) + diff --git a/paramecio/modules/admin2/templates/layout.phtml b/paramecio/modules/admin2/templates/layout.phtml new file mode 100644 index 0000000..d6bfb78 --- /dev/null +++ b/paramecio/modules/admin2/templates/layout.phtml @@ -0,0 +1,176 @@ +<% + +from paramecio.modules.admin2.libraries.config import modules_admin, modules_admin_icons +from parameciofast.libraries.i18n import I18n + +i18n=I18n('admin2') + +dark_checked="" +dark_css="" + +if session.get('theme', '0')==True: + dark_checked='checked' + dark_css='dark' + + +%> + + + + + +${title} + + + + +<%block name="extra_css"> + + + + +<%block name="extra_js"> + +<%block name="extra_header"> + + + +
    +
    +
    +
    +<%block name="logout"> Logout +
    + +
    + +
    + +
    +

    ${title}

    +
    +
    + ${_('Dark theme')} +
    +
    + +
    +
    +
    + + <%block name="content"> + +
    +
    +
    +
    +
    + +<%block name="jscript_block"> + + + diff --git a/paramecio/modules/admin2/templates/layout_bs.phtml b/paramecio/modules/admin2/templates/layout_bs.phtml new file mode 100644 index 0000000..0cab79d --- /dev/null +++ b/paramecio/modules/admin2/templates/layout_bs.phtml @@ -0,0 +1,123 @@ +<% + +from parameciofast.modules.fastadmin.libraries.config import modules_admin, modules_admin_icons +from parameciofast.libraries.i18n import I18n + +i18n=I18n('fastadmin') + +%> + + + + + + ${title} + + + <%block name="css"> + + <%block name="header_js"> + + + + + + + + %for module_icon in modules_admin_icons: + + ${module_icon|n} + + %endfor + + +
    +
    +
    +
    +

    ${tlang('Dashboard')}

    +
    +
    +
    +
    + +
    + + % for module in modules_admin: + + % endfor +
    + +
    +
    + <%block name="content"> +
    +
    + ${tlang('Welcome to admin')} +
    +
    +

    ${tlang('This is the admin section of your site.')}

    +
    +
    + +
    +
    +
    +
    +
    + +
    + + + + + <%block name="js"> + + + diff --git a/paramecio/modules/admin2/templates/login.phtml b/paramecio/modules/admin2/templates/login.phtml new file mode 100644 index 0000000..0ad1045 --- /dev/null +++ b/paramecio/modules/admin2/templates/login.phtml @@ -0,0 +1,171 @@ + + + + + + ${title} + + + <%block name="css"> + + <%block name="header_js"> + + + + +
    +
    +
    +
    + <%block name="content"> +
    +
    + ${tlang('Login')} +
    +
    +
    +
    + + +
    + +
    +
    +
    + + +
    + ${tlang('Error: username or password invalid')} +
    +
    +
    + + +
    + + ${csrf_token()|n} +
    +
    + +
    +
    +
    +
    +
    + + + + + <%block name="jscript"> + + + + diff --git a/paramecio/modules/admin2/templates/signup.phtml b/paramecio/modules/admin2/templates/signup.phtml new file mode 100644 index 0000000..2434e78 --- /dev/null +++ b/paramecio/modules/admin2/templates/signup.phtml @@ -0,0 +1,186 @@ +<%inherit file="login.phtml"/> +<%block name="content"> +
    +
    + ${tlang('Signup')} +
    +
    +
    +
    + + +
    + ${tlang('You need a valid username')} +
    +
    +
    + + +
    + ${tlang('You need an email')} +
    +
    +
    + + +
    + ${tlang('You need a password')} +
    +
    +
    + + +
    + ${tlang('You need the same password in this field and not empty')} +
    +
    + + ${csrf_token()|n} +
    +
    + +<%block name="jscript"> + + diff --git a/paramecio/modules/admin2/templates/users.phtml b/paramecio/modules/admin2/templates/users.phtml new file mode 100644 index 0000000..afc5e33 --- /dev/null +++ b/paramecio/modules/admin2/templates/users.phtml @@ -0,0 +1,4 @@ +<%inherit file="layout.phtml"/> +<%block name="content"> +${slist|n} + diff --git a/paramecio/modules/javascript/load_js.py b/paramecio/modules/javascript/load_js.py index 1d85909..5869e26 100644 --- a/paramecio/modules/javascript/load_js.py +++ b/paramecio/modules/javascript/load_js.py @@ -1,10 +1,10 @@ #!/usr/bin/python3 from paramecio.wsgiapp import app -from paramecio.citoplasma.mtemplates import env_theme, PTemplate +from paramecio.libraries.mtemplates import env_theme, PTemplate from settings import config -from paramecio.citoplasma.sessions import get_session -from paramecio.citoplasma.i18n import I18n +from paramecio.libraries.sessions import get_session +from paramecio.libraries.i18n import I18n from bottle import response import os diff --git a/paramecio/modules/lang/admin/translations.py b/paramecio/modules/lang/admin/translations.py index 5b4b7b2..5490c23 100644 --- a/paramecio/modules/lang/admin/translations.py +++ b/paramecio/modules/lang/admin/translations.py @@ -1,15 +1,15 @@ #from modules.pastafari.models.servers import OsServer -from paramecio.citoplasma.generate_admin_class import GenerateConfigClass -from paramecio.citoplasma.lists import SimpleList -from paramecio.citoplasma.adminutils import make_admin_url -#from paramecio.citoplasma.urls import make_url -from paramecio.citoplasma.i18n import I18n -from paramecio.citoplasma.urls import add_get_parameters +from paramecio.libraries.generate_admin_class import GenerateConfigClass +from paramecio.libraries.lists import SimpleList +from paramecio.libraries.adminutils import make_admin_url +#from paramecio.libraries.urls import make_url +from paramecio.libraries.i18n import I18n +from paramecio.libraries.urls import add_get_parameters from settings import config -from paramecio.citoplasma.httputils import GetPostFiles -from paramecio.cromosoma.coreforms import SelectForm, BaseForm -from paramecio.cromosoma.extraforms.i18nform import I18nForm +from paramecio.libraries.httputils import GetPostFiles +from paramecio.libraries.db.coreforms import SelectForm, BaseForm +from paramecio.libraries.db.extraforms.i18nform import I18nForm import re, json from collections import OrderedDict from importlib import import_module @@ -94,7 +94,7 @@ def admin(**args): file_lang="#!/usr/bin/env python3\n\n" - file_lang+="from paramecio.citoplasma.i18n import I18n\n\n" + file_lang+="from paramecio.libraries.i18n import I18n\n\n" z=0 diff --git a/paramecio/modules/lang/index.py b/paramecio/modules/lang/index.py index f814488..9849b47 100644 --- a/paramecio/modules/lang/index.py +++ b/paramecio/modules/lang/index.py @@ -1,10 +1,12 @@ -from paramecio.citoplasma.i18n import I18n +from paramecio.libraries.i18n import I18n from bottle import get,response,request -from paramecio.citoplasma.sessions import get_session -from paramecio.citoplasma.urls import redirect +#from paramecio.libraries.sessions import get_session +from paramecio.libraries.sessionplugin import get_session, session_plugin +from paramecio.libraries.urls import redirect import re @get('/change_lang/') +@session_plugin def index(lang): if lang in I18n.dict_i18n: @@ -12,8 +14,7 @@ def index(lang): s=get_session() s['lang']=lang - - s.save() + #s.save() redirect_url=request.headers.get('Referer') @@ -23,5 +24,7 @@ def index(lang): redirect(redirect_url) - return "" + return {'error': 1, 'message': 'No referer for redirect'} + + diff --git a/paramecio/modules/welcome/__init__.py b/paramecio/modules/welcome/__init__.py index e69de29..e39e1d4 100644 --- a/paramecio/modules/welcome/__init__.py +++ b/paramecio/modules/welcome/__init__.py @@ -0,0 +1,3 @@ +from bottle import Bottle + +welcome_app=Bottle() diff --git a/paramecio/modules/welcome/index.py b/paramecio/modules/welcome/index.py index 64b0274..9783d81 100644 --- a/paramecio/modules/welcome/index.py +++ b/paramecio/modules/welcome/index.py @@ -1,28 +1,30 @@ #!/usr/bin/env python3 -from paramecio.citoplasma.mtemplates import PTemplate, env_theme -from paramecio.citoplasma.urls import make_url +from paramecio.libraries.mtemplates import PTemplate, env_theme +from paramecio.libraries.urls import make_url from paramecio.wsgiapp import app from settings import config +from paramecio.modules.welcome import welcome_app +from bottle import request #t=ptemplate(__file__) env=env_theme(__file__) t=PTemplate(env) -@app.route('/welcome') +@welcome_app.route('/welcome') def home(): return t.render_template('welcome.html', title="Welcome to Paramecio!!!", content="The simple web framework writed in Python3!!!") -@app.route('/welcome/') +@welcome_app.route('/welcome/') def page(id): return t.render_template('index.html', title="A simple example of a page", id=id, value=request.query.value) -@app.route('/welcome/test/') +@welcome_app.route('/welcome/test/') def test(int_id): - return make_url('welcome/test/'+int_id, {'ohmygod': 'This is gooood', 'shutup':'Shut up!!'}) + return make_url(f'welcome/test/{int_id}', {'ohmygod': 'This is gooood', 'shutup':'Shut up!!'}) if config.default_module=="welcome": diff --git a/paramecio/modules/welcome/templates/index.html b/paramecio/modules/welcome/templates/index.html index 590b0ed..7047306 100644 --- a/paramecio/modules/welcome/templates/index.html +++ b/paramecio/modules/welcome/templates/index.html @@ -3,7 +3,7 @@ Paramecio WebFramework - +