diff --git a/parameciofast/fast.py b/parameciofast/fast.py index 1a6323e..32b03be 100644 --- a/parameciofast/fast.py +++ b/parameciofast/fast.py @@ -28,6 +28,8 @@ for name_module, module_app in config.apps.items(): base_path=controller_mod.__file__.replace('__init__.py', '') + app_basedir=os.path.basename(base_path[0:len(base_path)-1]) + media_path=base_path+'media/' dir_controllers=os.listdir(base_path) @@ -39,9 +41,10 @@ for name_module, module_app in config.apps.items(): subcontroller_path=module+'.'+controller.replace('.py', '') subcontroller_mod=import_module(subcontroller_path) - if os.path.isfile(media_path): + if os.path.isdir(media_path): if yes_static: - app.mount("/mediafrom/"+name_app, StaticFiles(directory=media_path), name="static_"+name_app) + + app.mount("/mediafrom/"+app_basedir, StaticFiles(directory=media_path), name="static_"+app_basedir) fast_app=getattr(controller_mod, app_module) diff --git a/parameciofast/libraries/config_admin.py b/parameciofast/libraries/config_admin.py new file mode 100644 index 0000000..a688564 --- /dev/null +++ b/parameciofast/libraries/config_admin.py @@ -0,0 +1,27 @@ +""" +Paramecio2fm is a series of wrappers for Flask, mako and others and construct a simple headless cms. + +Copyright (C) 2023 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 . +""" + +# Variable base for admin modules + +"""Variables for control admin modules for admin module +""" + +config_admin=[] +"""list: List for save the differente admin configurations from paramecio2 modules +""" diff --git a/parameciofast/libraries/i18n.py b/parameciofast/libraries/i18n.py new file mode 100644 index 0000000..52aaf8d --- /dev/null +++ b/parameciofast/libraries/i18n.py @@ -0,0 +1,228 @@ +#!/usr/bin/env python3 + +""" +Parameciofast is a series of wrappers for Fastapi, mako and others and construct a simple headless cms. + +Copyright (C) 2023 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 +import gettext +#from paramecio.citoplasma.sessions import get_session +import json +#from flask import session, has_request_context +import os +#from fastapi import Request + +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, default_lang=None): + + self.module=module + self.default_lang=I18n.default_lang + + 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) + + #@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 + + #if has_request_context(): + #lang=session.get('lang', lang) + + return lang + + @staticmethod + def lang(module, symbol, text_default): + """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() + + 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): + """Static method for return a language dict in JSON + + Static method used to get the string of the selected language in JSON format. If there are not 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 + """ + + arr_final={} + + for l in I18n.dict_i18n: + arr_final[l]=I18n.lang(module, symbol, text_default, l) + + return json.dumps(arr_final) + + @staticmethod + def session_lang(request): + + return request.session.get('lang', I18n.get_default_lang()) + +common_pgettext=PGetText(__file__) diff --git a/parameciofast/libraries/mtemplates.py b/parameciofast/libraries/mtemplates.py new file mode 100644 index 0000000..5c17342 --- /dev/null +++ b/parameciofast/libraries/mtemplates.py @@ -0,0 +1,287 @@ +""" +Paramecio2fm is a series of wrappers for Fastapi, 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 . +""" + +# Template frontend from mako. + +#import gettext +from mako.template import Template +#from flask import url_for as url_for_flask +from mako.lookup import TemplateLookup +from os import path +try: + from settings import config +except: + class config: + theme='default' + reloader=False + +#import gettext +import sys +from parameciofast.libraries.i18n import I18n, PGetText +from parameciofast.libraries.urls import make_url, make_media_url, add_get_parameters +#from parameciofast.libraries.formsutils import csrf_token + +framework='flask' + +if hasattr(config, 'framework'): + framework=config.framework + + +""" +def _(text): + + return gettext.gettext(text) +""" + +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': + + module=path.dirname(module) + + standard_templates=path.dirname(__file__)+'/templates' + + standard_languages=path.dirname(__file__)+'/languages' + + module_directory+='/'+module + + module_templates=module+'/templates' + + theme_templates='themes/'+config.theme+'/templates' + + search_folders=[theme_templates, module_templates, standard_templates] + + #if self.inject_folder is not None: + #search_folders.insert(1, self.inject_folder+'/templates') + + #Standard templates + #print(standard_templates) + return TemplateLookup(directories=search_folders, default_filters=['h'], input_encoding='utf-8', encoding_errors='replace', cache_enabled=cache_enabled, cache_impl=cache_impl, cache_args=cache_args, module_directory=module_directory, filesystem_checks=config.reloader) + +class PTemplate: + """A class used how shortcuts for Mako template functions. + """ + + templates_loaded={} + + def __init__(self, environment, url_for): + """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. + + """ + + self.autoescape_ext=('html', 'htm', 'xml', 'phtml', 'js') + + self.env=environment + + self.filters={} + + self.js={} + + base_name=path.dirname(path.realpath(__file__)) + + #self.i18n=I18n(base_name, default_lang) + + #self.add_filter(self.i18n.lang) + + #self.add_filter(make_url) + + self.add_filter(make_media_url) + """ + if not url_for_function: + + url_for=url_for_flask + + self.add_filter(url_for) + + else: + + url_for=url_for_function + + self.add_filter(url_for) + + """ + + self.add_filter(url_for) + + #self.add_filter(csrf_token) + + #self.add_filter(add_get_parameters) + + self.add_filter(self.add_js) + + self.add_filter(self.load_js) + + # Loading language domain for gettext in templates + + module_env=self.env.directories[1].replace('/templates', '') + + #print(path.basename(module_env)+' '+base_name+'/languages/') + + self.l=PGetText(module_env+'/app.py') + + self.add_filter(self._) + + #self.add_filter(self.i18n.slang) + + #self.add_filter(self.i18n.tlang) + + def _(self, text): + + return self.l.gettext(text) + + def add_js(self, js, module=''): + """Function for add js to self.js attribute + + Add a js file name to an attribute list called self.js with the '.format(make_media_url('js/'+js, module)) + + return "" + + def load_js(self): + + return "\n".join(self.js.values()) + + + """ + def gettext(self, text): + return gettext.dgettext(self.domain_gettext, text) + """ + + 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 + """ + + """ + if self.prepare_gettext: + + module_lang_dir=self.env.directories[1].replace('/templates', '/locales') + + module_name=path.basename(module_lang_dir.replace('/locales', '')) + + en = gettext.translation(module_name, localedir=module_lang_dir, languages=['en']) + en.install() + _=en.gettext + + arguments.update({'_': _}) + """ + template = self.env.get_template(template_file) + + arguments.update(self.filters) + + return template.render(**arguments) + + 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 + + ext = template_name.rsplit('.', 1)[1] + + return ext in self.autoescape_ext + + 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) + + arguments.update(self.filters) + + return PTemplate.templates_loaded[str(self.env.directories)+'/'+template_file].render(**arguments) + + def add_filter(self, filter_name): + """Method for add filters to self.filters attributes + + Method for add filters to self.filters attributes for use in templates + + Args: + filter_name (function): Filter function + """ + + self.filters[filter_name.__name__]=filter_name + +def add_css_home_local(file_css, module): + + pass + +#env=env_theme(__file__) + +#standard_t=PTemplate(env) + diff --git a/parameciofast/libraries/session.py b/parameciofast/libraries/session.py new file mode 100644 index 0000000..4fd6411 --- /dev/null +++ b/parameciofast/libraries/session.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 + +from settings import config +from itsdangerous.url_safe import URLSafeTimedSerializer + +import os +try: + import ujson as json +except: + import json + +# Cookie session +# This save the session in a cookie for maximum performance. In next version i can use memcached or something for session +# In next versions have two secret_keys for more security. + +class ParamecioSession: + + def __init__(self, session_cookie): + + self.serializer=URLSafeTimedSerializer(config.secret_key) + + try: + + self.session=self.serializer.loads(session_cookie) + pass + + except: + self.session={} + + def get(self, name, default_value): + + if not name in self.session: + self.session[name]=default_value + + return self.session[name] + + def __getitem__(self, key): + + return self.session[key] + + def __setitem__(self, key, value): + + self.session[key]=value + + def __delitem__(self, key): + + if key!='token': + del self.session[key] + + def __contains__(self, key): + + if key in self.session: + return True + else: + return False + + def __iter__(self): + return self.session + + def __str__(self): + return self.session.__str__() + + def keys(self): + return self.session.keys() + """ + def remove(self): + response.delete_cookie(config.cookie_name, path="/") + + def delete(self): + self.remove() + + def save(self): + + # Here get the function for load session + + #save_session(self.session['token'], self.session) + pass + """ + + def save(self): + + return self.serializer.dumps(session_cookie) + + def reset(self): + + #token=self.session['token'] + self.session={} + #self.save() + diff --git a/parameciofast/libraries/urls.py b/parameciofast/libraries/urls.py new file mode 100644 index 0000000..4f621e5 --- /dev/null +++ b/parameciofast/libraries/urls.py @@ -0,0 +1,133 @@ +""" +Paramecio2fm is a series of wrappers for Flask, mako and others and construct a simple headless cms. + +Copyright (C) 2023 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 . +""" + +try: + from settings import config +except: + + class config: + + application_root='/' + domain_url='http://localhost/' + yes_static=False + +import urllib.parse + +def make_url(path, query_args={}): + + """ + This is a method for create urls for the system + + Keyword arguments: + path -- The path to the module + query_args -- a ser of get variables for add to url + + """ + + get_query='' + + if len(query_args)>0: + + get_query='?'+urllib.parse.urlencode(query_args) + + path=urllib.parse.quote_plus(path, safe='/') + + return config.application_root+path+get_query + +def make_url_domain(path, query_args={}): + + """ + This is a method for create urls for the system, using the domain_url config variable how prefix + + Keyword arguments: + path -- The path to the module + query_args -- a ser of get variables for add to url + + """ + + return config.domain_url+make_url(path, query_args) + +def make_external_url(path, query_args={}): + + """ + This is a method for create urls for external systems + + Keyword arguments: + path -- The base url of the url + query_args -- a ser of get variables for add to url + + """ + + get_query='' + + if len(query_args)>0: + + get_query='?'+urllib.parse.urlencode(query_args) + + return path+get_query + +def add_get_parameters(url, **args): + + """ + This is a method for add args to existent url + + Keyword arguments: + url -- The url + args -- a ser of get variables for add to url + + """ + + added_url='&' + + if url.find('?')==-1: + added_url='?' + + get_query=urllib.parse.urlencode(args) + + return url+added_url+get_query + + +if config.yes_static==True: + + def make_media_url(file_path, module): + + """ + This is a method for create urls for media resources. + + Keyword arguments: + file_path -- The relative path of module + module -- the module where you can find the resource + """ + + return make_url('mediafrom/'+module+'/'+file_path) + #config.media_url+'mediafrom/'+module+'/'+file_path +else: + + def make_media_url(file_path, module): + + """ + This is a method for create urls for media resources if config.yes_static is disabled.. + + Keyword arguments: + file_path -- The relative path of module + module -- the module where you can find the resource + + """ + + return config.media_url+urllib.parse.quote_plus(module)+'/'+urllib.parse.quote_plus(file_path, safe='/') diff --git a/parameciofast/modules/fastadmin/app.py b/parameciofast/modules/fastadmin/app.py index 646d993..6567f65 100644 --- a/parameciofast/modules/fastadmin/app.py +++ b/parameciofast/modules/fastadmin/app.py @@ -3,6 +3,11 @@ from fastapi.responses import HTMLResponse, RedirectResponse from parameciofast.modules.fastadmin import admin_app from typing import Annotated from parameciofast.fast import app +from parameciofast.libraries.i18n import I18n +from parameciofast.libraries.mtemplates import env_theme, PTemplate + +env=env_theme(__file__) +t=PTemplate(env, app.url_path_for) @admin_app.get('/', response_class=HTMLResponse) def home_admin(request: Request, paramecio_session: Annotated[str | None, Cookie(description='Cookie for validate into the admin site. The cookie name can change in you settings/config.py')] = None): @@ -17,8 +22,10 @@ def home_admin(request: Request, paramecio_session: Annotated[str | None, Cookie @admin_app.get('/login', response_class=HTMLResponse) -def login_admin(): +def login_admin(request: Request): + #session=request.session + i18n=I18n('admin', I18n.session_lang(request)) - return "Login" + return t.load_template('login.phtml', title=i18n.tlang('Login'), tlang=i18n.tlang, url_for=app.url_path_for) diff --git a/parameciofast/modules/fastadmin/media/css/layout.css b/parameciofast/modules/fastadmin/media/css/layout.css new file mode 100644 index 0000000..0df334a --- /dev/null +++ b/parameciofast/modules/fastadmin/media/css/layout.css @@ -0,0 +1,78 @@ +/* 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/parameciofast/modules/fastadmin/templates/login.phtml b/parameciofast/modules/fastadmin/templates/login.phtml new file mode 100644 index 0000000..18fef6b --- /dev/null +++ b/parameciofast/modules/fastadmin/templates/login.phtml @@ -0,0 +1,71 @@ + + + + + + ${title} + + + <%block name="css"> + + <%block name="header_js"> + + + + +
+
+
+
+
+
+ ${tlang('Login')} +
+
+
+
+ + +
+ ${tlang('You need a valid username and password')} +
+
+
+ + +
+
+ + +
+ +
+
+
+
+
+
+
+ + + + + +