diff --git a/parameciofast/libraries/datetime.py b/parameciofast/libraries/datetime.py new file mode 100644 index 0000000..4287712 --- /dev/null +++ b/parameciofast/libraries/datetime.py @@ -0,0 +1,782 @@ +import time +from datetime import date, datetime, tzinfo +import arrow +# from babel.dates import format_date, format_datetime, format_time, get_timezone, UTC +try: + from settings import config +except: + config={} +#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. +""" + +sql_format_time='YYYYMMDDHHmmss' +"""str: variable for define basic string for format dates + + +By default, datetime module use YYYYMMDDHHmmss string for define dates and time. Tipically is used for sql operations in paramecio2 framework. +""" + +format_date_txt="YYYY/MM/DD" +"""str: variable for define basic formatted date string +""" + +format_time_txt="HH:mm:ss" +"""str: variable for define basic formatted time string +""" + +timezone='Europe/Madrid' +"""str: basic timezone for dates, by default, Europe/Madrid +""" + +"""If default are changed in settings/config, change variables +""" + +if hasattr(config, 'format_date'): + format_date_txt=config.format_date + +if hasattr(config, 'format_time'): + format_time_txt=config.format_time + +if hasattr(config, 'timezone'): + timezone=config.timezone + +def set_timezone(): + """Simple function for change the timezone in general environment of python + """ + + environ['TZ']=environ.get('TZ', timezone) + + if environ['TZ']!=timezone: + environ['TZ']=timezone + time.tzset() + +""" +def set_timezone_session(): + + s=get_session() + + timezone_local=timezone + + if s!=None: + if 'timezone' in s: + timezone_local=s['timezone'] + #timezone_local=s.get('timezone', timezone) + + environ['TZ']=environ.get('TZ', timezone_local) + + if environ['TZ']!=timezone_local: + environ['TZ']=timezone_local + time.tzset() + + #request.environ['TIMEZONE'] = request.environ['PATH_INFO'].rstrip('/') +""" + +def format_timedata(time): + """Function for get separated year, month, day, hour, minute and second from sql_format_time string + + Args: + time (str): A YYYYMMDDHHmmss string for get datetime components from there. + + Returns: + list: A dict with datetime components (year, month, day, hour, minute, second). + + """ + + year=0 + month=0 + day=0 + hour=0 + minute=0 + second=0 + ampm='' + + try: + + year=int(time[:4]) + + month=int(time[4:6]) + + day=int(time[6:8]) + + hour=int(time[8:10]) + + minute=int(time[10:12]) + + second=int(time[12:14]) + + ampm=int(time[14:16]) + + except: + pass + + if ampm=='PM' or ampm=='pm': + + if hour>0: + hour+=12 + + return (year, month, day, hour, minute, second) + +def checkdatetime(y, m, d, h, mi, s): + """Check if a series of datetime separated elements are correct, the datetime values are type int + + Args: + y (int): Year of datetime + m (int): month + d (int): day + h (int): hour + mi (int): minute + s (int): seconds + + Returns: + bool: If values are correct, return True, otherwise return False + + """ + + try: + #test=datetime.strptime(str(y)+'-'+str(m)+'-'+str(d)+' '+str(h)+'-'+str(mi)+'-'+str(s), '%Y-%m-%d %H-%M-%S') + test=arrow.arrow.Arrow(y, m, d, h, mi, s) + return True + except: + return False + +# Get the localtime + +def now(utc=False, tz=''): + + """Returns the actual datetime in YYYYMMDDHHmmss format. + + Args: + utc (bool): If True, the datetime is returned in UTC timezone + tz (str, optional): Timezone name, example: Europe/Madrid. If set the datetime is returned in the timezone selected + + Returns: + str: Return actual datetime + + """ + + if not utc: + if tz=='': + actual=arrow.now().format(sql_format_time) + else: + #actual=arrow.to(tz).now().format(sql_format_time) + utc=arrow.utcnow() + actual=utc.to(tz).format(sql_format_time) + else: + actual=arrow.utcnow().format(sql_format_time) + + return actual + +def today(utc=False,tz=''): + + """Returns the actual date in YYYYMMDDHHmmss format. + + Is different from (now) function because return the date to 00:00:00 time + + Args: + utc (bool): If True, the date is returned in UTC timezone + tz (str, optional): Timezone name, example: Europe/Madrid. If set the date is returned in the timezone selected + + Returns: + str: Return actual date with 00:00:00 how time + + """ + + return now(utc, tz)[:8]+'000000' + +# Get actual timestamp + +def obtain_timestamp(timeform): + + """Get the timestamp from datetime in YYYYMMDDHHmmss format. + + Args: + timeform (str): Datetime in YYYYMMDDHHmmss format. + + Returns: + + int: datetime in timestamp format + + """ + + y, m, d, h, mi, s=format_timedata(timeform) + + if checkdatetime(y, m, d, h, mi, s): + + #timestamp=int(time.mktime((y, m, d, h, mi, s, 0, 0, -1))) + + timestamp=arrow.arrow.Arrow(y, m, d, h, mi, s).timestamp() + + return timestamp + + #return mktime($h, $mi, $s, $m, $d, $y); + else: + return False + +# timestamp is gmt time, convert in normal time + +def timestamp_to_datetime(timestamp, sql_format_time=sql_format_time): + + """Turn datetime in YYYYMMDDHHmmss format. + + Args: + timestamp (int): The timestamp for convert + + Returns: + str: Datetime in YYYYMMDDHHmmss format + + """ + + return arrow.get(timestamp).format(sql_format_time) + +# Get a utc timestamp and convert to local + +def timestamp_to_datetime_local(timestamp, tz='', sql_format_time=sql_format_time): + """Get a utc timestamp and convert to timezone datetime in YYYYMMDDHHmmss format. + + Args: + timestamp (int): The timestamp for convert in datetime + tz (str, optional): If you want convert to other timezone, set it. + + Returns: + + str: Datetime in YYYYMMDDHHmmss format in selected timezone datetime + + """ + + t=arrow.get(timestamp) + + if tz=='': + tz=environ['TZ'] + + return t.to(tz).format(sql_format_time) + + +def format_datetime(format_time, timeform, func_utc_return): + + """Get a datetime in YYYYMMDDHHmmss format and convert in other str datetime (normally, same YYYYMMDDHHmmss format). Is a primitive function for other high level datetime functions. + + Args: + format_time (str): The strtime string used for format the datetime + timeform (str): datetime in YYYYMMDDHHmmss format to convert to new format + func_utc_return (function): A function used for get the datetime. + + Returns: + If timestamp is False, return False, if timestamp is valid, return the datetime formatted + + """ + + timestamp=obtain_timestamp(timeform) + + if timestamp: + + t=func_utc_return(timestamp) + + return t.format(format_time) + + else: + + return False + +# This method parse local time to gmt + +def local_to_gmt(timeform, sql_format_time=sql_format_time): + + """Get a datetime in YYYYMMDDHHmmss format and convert in other str datetime. Is a primitive function for other high level datetime functions. + + Expects that timeform was in time not gmt and convert to gmt + + Args: + timeform (str): datetime in YYYYMMDDHHmmss format to convert to new format + sql_format_time (str, optional): by default, the format is YYYYMMDDHHmmss, you can put other formatted str formats for date, here. + + Returns: + If timeform is False, return False, if timeform is valid, return the datetime formatted + + """ + + return format_datetime(sql_format_time, timeform, substract_utc) + +# time.localtime is useless, you need sum the time offset to the date + +def gmt_to_local(timeform, sql_format_time=sql_format_time): + + """Get a datetime in YYYYMMDDHHmmss format in UTC and convert in other str datetime. Is a primitive function for other high level datetime functions. + + Expects that timeform was in time gmt and convert to localtime + + Args: + timeform (str): datetime in YYYYMMDDHHmmss format to convert to new format + sql_format_time (str, optional): by default, the format is YYYYMMDDHHmmss, you can put other formatted str formats for date, here. + + Returns: + If timeform is False, return False, if timeform is valid, return the datetime formatted + + """ + + return format_datetime(sql_format_time, timeform, sum_utc) + +def format_time(timeform): + + """Get a datetime in YYYYMMDDHHmmss format and convert in HH:mm:ss UTC format. Is a primitive function for other high level datetime functions. + + Args: + timeform (str): datetime in YYYYMMDDHHmmss format to convert to new format + + Returns: + If timeform is False, return False, if timeform is valid, return the datetime formatted in UTC + + """ + + return format_datetime(format_time_txt, timeform, sum_utc) + +def format_date(timeform): + + """Get a datetime in YYYYMMDDHHmmss format and convert in YYYY/MM/DD UTC format. Is a primitive function for other high level datetime functions. + + Args: + timeform (str): datetime in YYYYMMDDHHmmss format to convert to new format + + Returns: + If timeform is False, return False, if timeform is valid, return the datetime formatted in UTC + """ + + return format_datetime(format_date_txt, timeform, sum_utc) + +def format_fulldate(timeform): + + """Get a datetime in YYYYMMDDHHmmss format and convert in YYYY/MM/DD HH:mm:ss UTC format. Is a primitive function for other high level datetime functions. + + Args: + timeform (str): datetime in YYYYMMDDHHmmss format to convert to new format + + Returns: + If timeform is False, return False, if timeform is valid, return the datetime formatted in UTC + """ + + return format_datetime(format_date_txt+' '+format_time_txt, timeform, sum_utc) + +def format_local_time(timeform): + + """Get a datetime in YYYYMMDDHHmmss format and convert in HH:mm:ss format. Is a primitive function for other high level datetime functions. + + Args: + timeform (str): datetime in YYYYMMDDHHmmss format to convert to new format + + Returns: + If timeform is False, return False, if timeform is valid, return the datetime formatted + """ + + return format_datetime(format_time_txt, timeform, no_utc) + +def format_local_date(timeform): + + """Get a datetime in YYYYMMDDHHmmss format and convert in YYYY/MM/DD format. Is a primitive function for other high level datetime functions. + + Args: + timeform (str): datetime in YYYYMMDDHHmmss format to convert to new format + + Returns: + If timeform is False, return False, if timeform is valid, return the datetime formatted + """ + + return format_datetime(format_date_txt, timeform, no_utc) + +def format_local_fulldate(timeform): + + """Get a datetime in YYYYMMDDHHmmss format and convert in YYYY/MM/DD HH:mm:ss format. Is a primitive function for other high level datetime functions. + + Args: + timeform (str): datetime in YYYYMMDDHHmmss format to convert to new format + + Returns: + If timeform is False, return False, if timeform is valid, return the datetime formatted + """ + + return format_datetime(format_date_txt+' '+format_time_txt, timeform, no_utc) + +def format_strtime(strtime, timeform): + + """Get a datetime in YYYYMMDDHHmmss format and convert in strtime string UTC format. Is a primitive function for other high level datetime functions. + + Args: + timeform (str): datetime in YYYYMMDDHHmmss format to convert to new format + + Returns: + If timeform is False, return False, if timeform is valid, return the datetime formatted in UTC + """ + + return format_datetime(strtime, timeform, sum_utc) + +def format_local_strtime(strtime, timeform): + + """Get a datetime in YYYYMMDDHHmmss format and convert in strtime string format. Is a primitive function for other high level datetime functions. + + Args: + timeform (str): datetime in YYYYMMDDHHmmss format to convert to new format + + Returns: + If timeform is False, return False, if timeform is valid, return the datetime formatted + """ + + return format_datetime(strtime, timeform, no_utc) + +#Input is utc timestamp, return local arrow object + +def sum_utc(timestamp, tz=''): + + """Get timestamp in UTC and convert in arrow date object with timezone datetime + + Args: + timestamp (int): The timestamp for convert in other timezone + tz (str): Timezone of timestamp + + Returns: + Return arrow object with new timezone selected + """ + + #offset=time.altzone + + #return time.localtime(timestamp-offset) + + t=arrow.get(timestamp) + + if tz=='': + tz=environ['TZ'] + + return t.to(tz) + +#Input is local timestamp, return utc arrow object + +def substract_utc(timestamp, tz=''): + + """Get local timestamp and convert in arrow date object with UTC datetime + + Args: + timestamp (int): The timestamp for convert in UTC timezone + tz (str): Timezone of timestamp + + Returns: + Return arrow object with UTC timezone selected + """ + + #offset=time.altzone + + #return time.localtime(timestamp+offset) + + #t=arrow.get(timestamp).to('UTC') + + timeform=timestamp_to_datetime(timestamp) + + y, m, d, h, mi, s=format_timedata(timeform) + + if tz=='': + tz=environ['TZ'] + + t=arrow.get(datetime(y, m, d, h, mi, s), tz).to('UTC') + + return t + +def no_utc(timestamp): + + """Return an arrow object based in timestamp value + + Args: + timestamp (int): The timestamp for convert in UTC timezone + + Returns: + Return arrow object based in timestamp value + + """ + + return arrow.get(timestamp) + +class TimeClass: + """Simple abstraction of arrow class, in future i can change arrow class by others + + Args: + timestamp (int, str, optional): You can set the initial arrow object with timestamp date or YYYYMMDDHHmmss date format + tz (str): Timezone + + Attributes: + utc (bool): If True, the default timezone is UTC, if False, timezone is system default + format_time (str): The default datetime format, YYYYMMDDHHmmss + format_time_txt (str): Time text format, usually HH:mm:ss + format_date_txt (str): Date text format, usually YYYY/MM/DD + format_date_full (str): Full DateTime text format, usually YYYY/MM/DD HH:mm:ss + tz (str): Default timezone for arrow object + + """ + + def __init__(self, timestamp=None, tz=''): + + self.utc=False + + self.format_time=sql_format_time + self.format_time_txt=format_time_txt + + self.format_date_txt=format_date_txt + + self.format_date_full=format_date_txt+' '+format_time_txt + + self.tz=environ.get('TZ', 'utc') + + if tz: + self.tz=tz + + if type(timestamp).__name__=='int': + + self.datetime=timestamp_to_datetime(timestamp) + + else: + + if not timestamp: + + self.datetime=now(self.utc, tz) + + else: + self.datetime=timestamp + + y, m, d, h, mi, s=format_timedata(self.datetime) + + self.t=arrow.get(datetime(y, m, d, h, mi, s), self.tz) + + def add_month(self, num_months): + """Method for add months to datetime + + Args: + num_months (int): Number of months to add + + Returns: + New added datetime + """ + + m=self.t.shift(months=+num_months) + + return m.format(self.format_time) + + def substract_month(self, num_months): + """Method for substract months to datetime + + Args: + num_months (int): Number of months to substract + + Returns: + New substracted datetime + """ + + m=self.t.shift(months=-num_months) + + return m.format(self.format_time) + + def add_day(self, num_days): + """Method for add days to datetime + + Args: + num_days (int): Number of days to add + + Returns: + New added datetime + """ + + m=self.t.shift(days=+num_days) + + return m.format(self.format_time) + + def substract_day(self, num_days): + """Method for substract days to datetime + + Args: + num_days (int): Number of days to substract + + Returns: + New substracted datetime + + """ + + m=self.t.shift(days=-num_days) + + return m.format(self.format_time) + + def add_year(self, num_years): + """Method for add years to datetime + + Args: + num_years (int): Number of years to add + + Returns: + New added datetime + """ + + m=self.t.shift(years=+num_years) + + return m.format(self.format_time) + + def substract_year(self, num_years): + """Method for substract years to datetime + + Args: + num_years (int): Number of years to substract + + Returns: + New substracted datetime + """ + + m=self.t.shift(years=-num_years) + + return m.format(self.format_time) + + def add_hour(self, num_hours): + """Method for add hours to datetime + + Args: + num_hours (int): Number of hours to add + + Returns: + New added datetime + """ + + m=self.t.shift(hours=+num_hours) + + return m.format(self.format_time) + + def substract_hour(self, num_hours): + """Method for substract hours to datetime + + Args: + num_hours (int): Number of hours to substract + + Returns: + New substracted datetime + """ + + m=self.t.shift(hours=-num_hours) + + return m.format(self.format_time) + + def format(self): + """Method for get datetime formatted using format_date_full attribute + + Returns: + Datetime formatted with format_date_full attribute + """ + + return self.t.format(self.format_date_full) + + def local_to_utc(self): + """Method for convert datetime from actual timezone to UTC""" + + self.t=self.t.to('utc') + + # Only use + + def utc_to_local(self): + """Method for convert datetime from actual timezone from UTC to actual timezone""" + + self.t=self.t.to(self.tz) + + def local_to_tz(self, tz): + """Method for convert actual timezone to other timezone""" + + self.t=self.t.to(tz) + + def now(self, utc=False): + """Method for get actual datetime. + + Args: + utc (bool): If True, then get actual datetime in UTC datetime, if False, get actual datetime in selected timezone in tz attribute + + Returns: + + Actual datetime formatted in YYYYMMDDHHmmss format. + """ + + if not utc: + + actual=arrow.now(self.tz).format(sql_format_time) + else: + actual=arrow.utcnow().format(sql_format_time) + + return actual + + def today(self, utc=False): + + """Method for get today datetime. Get now datetime with 00:00:00 time. + + Args: + utc (bool): If True, then get actual datetime in UTC datetime, if False, get actual datetime in selected timezone in tz attribute + + Returns: + + Actual datetime formatted in YYYYMMDD000000 format. + """ + + if utc: + + return arrow.utcnow()[:8]+'000000' + else: + return arrow.now(self.tz)[:8]+'000000' + + def timestamp_to_datetime(self, timestamp): + + """Method for convert a timestamp in YYYYMMDDHHmmss format. + + Args: + + timestamp (int): datetime in timestamp format. + + Returns: + + Datetime in YYYYMMDDHHmmss format. + """ + + return arrow.get(timestamp).format(sql_format_time) + + def obtain_timestamp(self, timeform): + + """Method for get timestamp from a datetime in YYYYMMDDHHmmss format. + + Args: + timeform (str): Datetime in YYYYMMDDHHmmss format. + + Returns: + + Datetime in YYYYMMDDHHmmss format.If timeform is incorrect, return False + """ + + y, m, d, h, mi, s=format_timedata(timeform) + + if checkdatetime(y, m, d, h, mi, s): + + timestamp=arrow.arrow.Arrow(y, m, d, h, mi, s).timestamp + + return timestamp + + else: + return False + + def format_strtime(self, strtime, timeform): + + """Method for get datetime formatted in strtime format + + Args: + strtime (str): The string used for format the datetime + timeform (str): Datetime in YYYYMMDDHHmmss format. + + Returns: + + Datetime in strtime format.If timeform is incorrect, return False + + """ + + + try: + y, m, d, h, mi, s=format_timedata(timeform) + + return arrow.get(datetime(y, m, d, h, mi, s), self.tz).format(strtime) + + except: + + return False diff --git a/parameciofast/libraries/db/.gitignore b/parameciofast/libraries/db/.gitignore new file mode 100644 index 0000000..bdd28c8 --- /dev/null +++ b/parameciofast/libraries/db/.gitignore @@ -0,0 +1,4 @@ +*~ +*.pyc +__pycache__ + diff --git a/parameciofast/libraries/db/README.md b/parameciofast/libraries/db/README.md new file mode 100644 index 0000000..7531f86 --- /dev/null +++ b/parameciofast/libraries/db/README.md @@ -0,0 +1 @@ +# A very simple ORM for Python 3 diff --git a/parameciofast/libraries/db/__init__.py b/parameciofast/libraries/db/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/parameciofast/libraries/db/corefields.py b/parameciofast/libraries/db/corefields.py new file mode 100644 index 0000000..f2954c3 --- /dev/null +++ b/parameciofast/libraries/db/corefields.py @@ -0,0 +1,420 @@ +""" +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 parameciofast.libraries.db.webmodel import PhangoField +from parameciofast.libraries.db import coreforms +from parameciofast.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.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) + + 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)' + + 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' + + 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/parameciofast/libraries/db/coreforms.py b/parameciofast/libraries/db/coreforms.py new file mode 100644 index 0000000..76d8a7a --- /dev/null +++ b/parameciofast/libraries/db/coreforms.py @@ -0,0 +1,297 @@ +#!/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 + self.default_value=value + self.css='' + self.type='text' + 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 '' + + 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 + + self.name_field_id=self.name+'_form' + + return "" + +class SimpleTextForm(BaseForm): + """Form for simple text + """ + + def __init__(self, name, value): + super().__init__(name, value) + + self.after_text='' + + def form(self): + + 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) + + def form(self): + + return '' + +class PasswordForm(BaseForm): + """Form for password forms + """ + + def __init__(self, name, value, show_password=False): + super(PasswordForm, self).__init__(name, value) + self.type='password' + self.show_password=show_password + + def setform(self, value): + if not self.show_password: + return "" + + else: + return value + +class HiddenForm(BaseForm): + """Form for hidden forms + """ + + def __init__(self, name, value): + super(HiddenForm, self).__init__(name, value) + self.type='hidden' + + +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: \n' + + arr_selected={self.default_value: 'selected'} + + for k,v in self.arr_select.items(): + arr_selected[k]=arr_selected.get(k, '') + + the_form+="" + + the_form+='\n' + + return the_form + +class SelectModelForm(SelectForm): + """Form for select html using a webmodel how base for get the data + """ + + def __init__(self, name, value, model, field_name, field_value, field_parent=None): + """ + Args: + name (str): The html name for this form + value (str): The default value of this html form. + model (WebModel): The webmodel used for get the data for arr_select + field_name (str): The field used for get the name of every option in select + field_value (str): The field used for get the value of every option in select + field_parent (int): If the model have parents or children, the value of this argument + """ + super(SelectModelForm, self).__init__(name, value) + + try: + self.default_value=int(self.default_value) + except: + self.default_value=0 + + self.arr_select=OrderedDict() + self.model=model + self.field_name=field_name + self.field_value=field_value + self.field_parent=field_parent + + self.form=self.normal_form + + if self.field_parent!=None: + self.form=self.parent_form + + + def normal_form(self): + """Method for prepare the form hierated from SelectForm class, without parents + + Method for prepare the form hierated from SelectForm class getting data from database using model attribute. + """ + + self.arr_select['']='' + + with self.model.select([self.field_name, self.field_value], True) as cur: + for arr_value in cur: + #print(self.model.fields[self.field_name]) + self.arr_select[arr_value[self.field_value]]=self.model.fields[self.field_name].show_formatted(arr_value[self.field_name]) + + try: + + self.default_value=int(self.default_value) + + except: + self.default_value=0 + + return super().form() + + def parent_form(self): + """Method for prepare the form hierated from SelectForm class, with parents + + Method for prepare the form hierated from SelectForm class getting data from database using model attribute. + """ + + self.arr_select['']='' + + arr_son={} + + old_conditions=self.model.conditions + old_limit=self.model.limit + + self.model.limit='' + + self.model.set_conditions('WHERE 1=1', []) + + + with self.model.select([self.field_name, self.field_value, self.field_parent], True) as cur: + + for arr_value in cur: + + if not arr_value[self.field_parent] in arr_son: + + arr_son[arr_value[self.field_parent]]=[] + + if arr_value[self.field_value]!=self.model.model_id: + arr_son[arr_value[self.field_parent]].append([arr_value[self.field_value], self.model.fields[self.field_name].show_formatted(arr_value[self.field_name])]) + + self.create_son(0, arr_son) + + self.model.conditions=old_conditions + self.model.limit=old_limit + + try: + + self.default_value=int(self.default_value) + + except: + self.default_value=0 + + return super().form() + + + def create_son(self, parent_id, arr_son, separator=''): + """Recursive method for generate parents and children dictionary + """ + + if parent_id in arr_son: + for son in arr_son[parent_id]: + self.arr_select[son[0]]=separator+son[1] + + son_separator=separator + + if son[0] in arr_son: + son_separator+='--' + self.create_son(son[0],arr_son, son_separator) diff --git a/parameciofast/libraries/db/databases/__init__.py b/parameciofast/libraries/db/databases/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/parameciofast/libraries/db/databases/mysql.py b/parameciofast/libraries/db/databases/mysql.py new file mode 100644 index 0000000..7ea17f2 --- /dev/null +++ b/parameciofast/libraries/db/databases/mysql.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 + +import sys +import pymysql +pymysql.install_as_MySQLdb() + +class SqlClass: + + connection={} + connection_method='' + + def __init__(self): + + self.error_connection="" + self.connected=False + SqlClass.connection_method=self.connect_to_db_sql + + def dummy_connect(self, connection, name_connection="default"): + pass + + def connect_to_db(self, connection, name_connection="default"): + + SqlClass.connection_method(connection, name_connection) + + SqlClass.connection_method=self.dummy_connect + + def connect_to_db_sql(self, connection, name_connection="default"): + + try: + + self.connection[name_connection] = pymysql.connect(connection['host'], + user=connection['user'], + passwd=connection['password'], + db=connection['db'], + charset='utf8mb4', + cursorclass=pymysql.cursors.DictCursor) + + self.connected=True + + except: + e = sys.exc_info()[0] + v = sys.exc_info()[1] + + self.error_connection="Error in connection: %s %s" % (e, v) + + #return False + raise NameError(self.error_connection) + + #Make def query more simple if not debugging. + + def query(self, sql_query, arguments=[], name_connection="default"): + + #if fetch_type=="ASSOC": + #fetch_type=pymysql.cursors.DictCursor + + with self.connection[name_connection].cursor(pymysql.cursors.DictCursor) as cursor: + + try: + + cursor.execute(sql_query, arguments) + self.connection[name_connection].commit() + + return cursor + + except: + e = sys.exc_info()[0] + v = sys.exc_info()[1] + + if hasattr(cursor, '_last_executed'): + sql_query=cursor._last_executed + + self.error_connection="Error in query ||"+sql_query+"||: %s %s" % (e, v) + + #return False + raise NameError(self.error_connection) + + #Fetcho row return dictionary if is defined in query. + + #def fetch(self, cursor): + + #return cursor.fetchone() + """ + def __del__(self): + + for key in self.connection: + + self.close(self.connection) + """ + """ + def close(self, name_connection="default"): + + if self.connection[name_connection]: + + self.connection[name_connection].close() + self.connection[name_connection]=False + + pass + """ + diff --git a/parameciofast/libraries/db/databases/mysqldb.py b/parameciofast/libraries/db/databases/mysqldb.py new file mode 100644 index 0000000..d09cec5 --- /dev/null +++ b/parameciofast/libraries/db/databases/mysqldb.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 + +import sys +import MySQLdb.cursors +import sqlalchemy.pool as pool +import traceback + +class SqlClass: + + def __init__(self, connection): + + self.max_overflow=-1 + self.pool_size=15 + self.error_connection="" + # Data of connection + self.connection=connection + # Sql connection + self.conn=None + self.connected=False + self.pool_recycle=3600 + self.connect() + + def connect(self): + + """ + if self.conn==None: + """ + try: + + def getconn(): + return MySQLdb.connect(self.connection['host'], + user=self.connection['user'], + passwd=self.connection['password'], + db=self.connection['db'], + charset='utf8mb4', + cursorclass=MySQLdb.cursors.DictCursor) + """ + if mypool==None: + + mypool=pool.QueuePool(getconn, max_overflow=self.max_overflow, pool_size=self.pool_size, recycle=self.pool_recycle, use_threadlocal=False) + """ + self.conn=getconn() #mypool.connect() + + self.conn.ping(True) + + """ + while not self.conn.open: + self.conn=SqlClass.mypool.connect() + """ + + self.connected=True + + except: + e = sys.exc_info()[0] + v = sys.exc_info()[1] + + self.error_connection="Error in connection: %s %s" % (e, v) + + self.conn.close() + + raise NameError(self.error_connection) + + + #Make def query more simple if not debugging. + + def query(self, sql_query, arguments=[], name_connection="default"): + + #if fetch_type=="ASSOC": + #fetch_type=MySQLdb.cursors.DictCursor + + #with self.conn.cursor(MySQLdb.cursors.DictCursor) as cursor: + cursor=self.conn.cursor(MySQLdb.cursors.DictCursor) + + try: + + cursor.execute(sql_query, arguments) + self.conn.commit() + + return cursor + + + except: + e = sys.exc_info()[0] + v = sys.exc_info()[1] + + if hasattr(cursor, '_last_executed'): + sql_query=cursor._last_executed + #, traceback.format_exc() + self.error_connection="Error in query ||%s||Values: %s" % (sql_query, str(arguments)) + + self.conn.close() + + #return False + raise NameError(self.error_connection) + + #Fetcho row return dictionary if is defined in query. + + #def fetch(self, cursor): + + #return cursor.fetchone() + + def __del__(self): + + if self.conn: + + self.conn.close() + + def close(self, name_connection="default"): + + if self.conn: + + self.conn.close() + self.conn=None + + pass + + diff --git a/parameciofast/libraries/db/databases/pymysql.py b/parameciofast/libraries/db/databases/pymysql.py new file mode 100644 index 0000000..83c1967 --- /dev/null +++ b/parameciofast/libraries/db/databases/pymysql.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 + +import sys +import pymysql.cursors +pymysql.install_as_MySQLdb +import sqlalchemy.pool as pool +import traceback + +mypool=None + +class SqlClass: + + def __init__(self, connection): + + self.max_overflow=-1 + self.pool_size=0 + self.error_connection="" + # Data of connection + self.connection=connection + # Sql connection + self.conn=None + self.connected=False + self.pool_recycle=3600 + self.connect() + + def connect(self): + + global mypool + + if self.conn==None: + try: + def getconn(): + return pymysql.connect(self.connection['host'], + user=self.connection['user'], + passwd=self.connection['password'], + db=self.connection['db'], + charset='utf8mb4', + cursorclass=pymysql.cursors.DictCursor) + + if mypool==None: + + mypool=pool.QueuePool(getconn, max_overflow=self.max_overflow, pool_size=self.pool_size, recycle=self.pool_recycle, use_threadlocal=True) + + self.conn=mypool.connect() + + self.conn.ping(True) + + """ + while not self.conn.open: + self.conn=SqlClass.mypool.connect() + """ + + self.connected=True + + except: + e = sys.exc_info()[0] + v = sys.exc_info()[1] + + self.error_connection="Error in connection: %s %s" % (e, v) + + self.conn.close() + + raise NameError(self.error_connection) + + + #Make def query more simple if not debugging. + + def query(self, sql_query, arguments=[], name_connection="default"): + + #self.connect() + + #if fetch_type=="ASSOC": + #fetch_type=MySQLdb.cursors.DictCursor + + #with self.conn.cursor(MySQLdb.cursors.DictCursor) as cursor: + cursor=self.conn.cursor(pymysql.cursors.DictCursor) + + try: + + cursor.execute(sql_query, arguments) + self.conn.commit() + + return cursor + + + except: + e = sys.exc_info()[0] + v = sys.exc_info()[1] + + if hasattr(cursor, '_last_executed'): + sql_query=cursor._last_executed + #, traceback.format_exc() + self.error_connection="Error in query ||%s||Values: %s" % (sql_query, str(arguments)) + + self.conn.close() + + #return False + raise NameError(self.error_connection) + + #Fetcho row return dictionary if is defined in query. + + #def fetch(self, cursor): + + #return cursor.fetchone() + + def __del__(self): + + if self.conn: + + self.conn.close() + + def close(self, name_connection="default"): + + if self.conn: + + self.conn.close() + self.conn=None + + pass + + diff --git a/parameciofast/libraries/db/databases/pymysql2.py b/parameciofast/libraries/db/databases/pymysql2.py new file mode 100644 index 0000000..6bc765d --- /dev/null +++ b/parameciofast/libraries/db/databases/pymysql2.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 + +import sys +import pymysql.cursors +pymysql.install_as_MySQLdb +import sqlalchemy.pool as pool +import traceback + +class SqlClass: + + mypool=None + + def __init__(self, connection): + + self.max_overflow=-1 + self.pool_size=0 + self.error_connection="" + # Data of connection + self.connection=connection + # Sql connection + self.conn=None + self.connected=False + self.pool_recycle=3600 + + def connect(self): + + if self.conn==None: + try: + def getconn(): + return pymysql.connect(self.connection['host'], + user=self.connection['user'], + passwd=self.connection['password'], + db=self.connection['db'], + charset='utf8mb4', + cursorclass=pymysql.cursors.DictCursor) + + if SqlClass.mypool==None: + SqlClass.mypool=pool.QueuePool(getconn, max_overflow=self.max_overflow, pool_size=self.pool_size, recycle=self.pool_recycle) + + self.conn=SqlClass.mypool.connect() + + while not self.conn.open: + self.conn=SqlClass.mypool.connect() + + self.conn.ping(True) + + self.connected=True + + except: + e = sys.exc_info()[0] + v = sys.exc_info()[1] + + self.error_connection="Error in connection: %s %s" % (e, v) + + self.conn.close() + + raise NameError(self.error_connection) + + + #Make def query more simple if not debugging. + + def query(self, sql_query, arguments=[], name_connection="default"): + + self.connect() + + #if fetch_type=="ASSOC": + #fetch_type=pymysql.cursors.DictCursor + + #with self.conn.cursor(pymysql.cursors.DictCursor) as cursor: + cursor=self.conn.cursor(pymysql.cursors.DictCursor) + + try: + + cursor.execute(sql_query, arguments) + self.conn.commit() + + return cursor + + + except: + e = sys.exc_info()[0] + v = sys.exc_info()[1] + + if hasattr(cursor, '_last_executed'): + sql_query=cursor._last_executed + #, traceback.format_exc() + self.error_connection="Error in query ||%s||Values: %s" % (sql_query, str(arguments)) + + #return False + raise NameError(self.error_connection) + + #Fetcho row return dictionary if is defined in query. + + #def fetch(self, cursor): + + #return cursor.fetchone() + + def __del__(self): + + if self.conn: + + self.conn.close() + + def close(self, name_connection="default"): + + if self.conn: + + self.conn.close() + self.conn=None + + pass + + diff --git a/parameciofast/libraries/db/databases/sqlalchemy.py b/parameciofast/libraries/db/databases/sqlalchemy.py new file mode 100644 index 0000000..9dcf592 --- /dev/null +++ b/parameciofast/libraries/db/databases/sqlalchemy.py @@ -0,0 +1,253 @@ +#!/usr/bin/env python3 + +import sys +#import pymysql.cursors +#pymysql.install_as_MySQLdb +#import sqlalchemy.pool as pool +from sqlalchemy import create_engine +import traceback + +#mypool=None +#engine = create_engine('sqlite:///:memory:', echo=True) +engine=None + +class SqlClass: + """Class used how interface to sqlalchemy for connect to mysql engine + + Attributes: + cursors_connect (pymysql.cursors.DictCursor): Cursor dict connection to database + disable_pool (boolean): If True then not exists mysql pool, if False, use sql pool for better connections. + pymsql_install (boolean): If True, pymysql is used how mysqldb classic python module. + pool_size (int): The size of the mysql pool. + """ + + cursors_connect=None + disable_pool=False + pymysql_install=False + pool_size=15 + + def __init__(self, connection): + """ + Args: + connection (dict): A dict with the configurations of SqlClass connection + Attributes: + error_connection (str): A string where errors are saved + connection (dict): A dict with the configurations of SqlClass connection + conn (MySQL Connection): A PyMySQL or Mysqldb connection + connected (bool): Simple bool for check if was connected to mysql + pool_recycle (int): Time limite for recycle the pool by inactivity + """ + + #self.max_overflow=-1 + self.error_connection="" + # Data of connection + self.connection=connection + # Sql connection + self.conn=None + self.connected=False + self.pool_recycle=3600 + self.last_query='' + self.connect() + + + def connect(self): + """Method for connect to mysql db using pymysql or mysqldb + """ + + global engine + + if not SqlClass.disable_pool: + + if not engine: + + try: + + if self.connection['db_type']=='pymysql': + + import pymysql.cursors + pymysql.install_as_MySQLdb + SqlClass.pymysql_install=True + SqlClass.cursors_connect=pymysql.cursors.DictCursor + else: + import MySQLdb.cursors + SqlClass.cursors_connect=MySQLdb.cursors.DictCursor + + engine=create_engine("mysql+%s://%s:%s@%s/%s?charset=utf8mb4" % (self.connection['db_type'], self.connection['user'], self.connection['password'], self.connection['host'], self.connection['db']), pool_recycle=self.pool_recycle, echo_pool=True, pool_size=self.pool_size, pool_pre_ping=True) + + #Postgre + #engine = create_engine("postgresql+psycopg2://scott:tiger@localhost:5432/mydatabase") + + except: + e = sys.exc_info()[0] + v = sys.exc_info()[1] + + self.error_connection="Error in connection: %s %s" % (e, v) + + #self.conn.close() + + raise NameError(self.error_connection) + + self.conn=engine.raw_connection() + + #self.conn.ping(True) + + else: + + if self.connection['db_type']=='pymysql': + + import pymysql.cursors + + if not SqlClass.pymysql_install: + pymysql.install_as_MySQLdb + SqlClass.pymysql_install=True + + """ + connection = pymysql.connect(host='localhost', + user='user', + password='passwd', + database='db', + charset='utf8mb4', + cursorclass=pymysql.cursors.DictCursor) + """ + + self.conn=pymysql.connect(host=self.connection['host'], user=self.connection['user'], passwd=self.connection['password'], db=self.connection['db'], charset='utf8mb4', cursorclass=pymysql.cursors.DictCursor) + + else: + + import MySQLdb.cursors + + self.conn=MySQLdb.connect(host=self.connection['host'], + user=self.connection['user'], + passwd=self.connection['password'], + db=self.connection['db'], + charset='utf8mb4', + cursorclass=MySQLdb.cursors.DictCursor) + + pass + + """ + if self.conn==None: + try: + def getconn(): + return pymysql.connect(self.connection['host'], + user=self.connection['user'], + passwd=self.connection['password'], + db=self.connection['db'], + charset='utf8mb4', + cursorclass=pymysql.cursors.DictCursor) + + if mypool==None: + + mypool=pool.QueuePool(getconn, max_overflow=self.max_overflow, pool_size=self.pool_size, recycle=self.pool_recycle, use_threadlocal=True) + + self.conn=mypool.connect() + + self.conn.ping(True) + + self.connected=True + + except: + e = sys.exc_info()[0] + v = sys.exc_info()[1] + + self.error_connection="Error in connection: %s %s" % (e, v) + + self.conn.close() + + raise NameError(self.error_connection) + """ + + #Make def query more simple if not debugging. + + def query(self, sql_query, arguments=[], name_connection="default"): + """Method for send a sql query to mysql server + + Args: + sql_query (str): The sql sentence to execute. For data you should use %s character. + arguments (list): The data used in sql sentence. This data substitute the %s characters. + name_connection (str): The name of dict element with the configuration of connection, without used in this moment. + """ + + cursor=self.conn.cursor(SqlClass.cursors_connect) + + try: + + cursor.execute(sql_query, arguments) + self.conn.commit() + + #if hasattr(cursor, '_executed'): + # self.last_query=cursor._executed + + return cursor + + except: + e = sys.exc_info()[0] + v = sys.exc_info()[1] + + #if hasattr(cursor, '_executed'): + # self.last_query=cursor._executed + + self.error_connection="Error in query ||%s||Values: %s" % (self.last_query, str(arguments)) + + self.conn.close() + + #return False + raise NameError(self.error_connection) + + + #self.connect() + + #if fetch_type=="ASSOC": + #fetch_type=MySQLdb.cursors.DictCursor + + #with self.conn.cursor(MySQLdb.cursors.DictCursor) as cursor: + """ + cursor=self.conn.cursor(pymysql.cursors.DictCursor) + + try: + + cursor.execute(sql_query, arguments) + self.conn.commit() + + return cursor + + + except: + e = sys.exc_info()[0] + v = sys.exc_info()[1] + + if hasattr(cursor, '_last_executed'): + sql_query=cursor._last_executed + #, traceback.format_exc() + self.error_connection="Error in query ||%s||Values: %s" % (sql_query, str(arguments)) + + #return False + raise NameError(self.error_connection) + """ + + #Fetcho row return dictionary if is defined in query. + + #def fetch(self, cursor): + + #return cursor.fetchone() + + def __del__(self): + """Typical method used when class is deleted from memory. Close the connextion if exists. + """ + + if self.conn: + + self.conn.close() + + def close(self, name_connection="default"): + """Method used for close the connection if you want close connection and open other. + """ + + if self.conn: + + self.conn.close() + self.conn=None + + pass + + diff --git a/parameciofast/libraries/db/dbadmin.py b/parameciofast/libraries/db/dbadmin.py new file mode 100644 index 0000000..7f7fb27 --- /dev/null +++ b/parameciofast/libraries/db/dbadmin.py @@ -0,0 +1,487 @@ +#!/usr/bin/env python3 + +""" +Parameciofast 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 . +""" + + +import argparse +import os,traceback +import sys, inspect +import shutil +import re +from datetime import date +from pathlib import Path +from colorama import init, Fore, Back, Style +from importlib import import_module, reload +from parameciofast.libraries.db.webmodel import WebModel +sys.path.insert(0, os.path.realpath('.')) + +#try: +from settings import config +#except: +#print('You need a settings directory with a parameciofast configuration') +#sys.exit(1) + + +def start(): + """Function for create and update mysql tables using webmodel classes and fields how source. + """ + + connection=WebModel.connection() + + #connection.connect_to_db(WebModel.connections['default']) + + parser = argparse.ArgumentParser(description='A tool for create tables in databases using models from Cromosoma') + + parser.add_argument('--model', help='Model python path', required=True) + + parser.add_argument('--config', help='The config file', required=False) + + args = parser.parse_args() + + init() + + #Import config + + config_file='config' + + if args.config!=None: + config_file=args.config + + try: + + config=import_module('settings.'+config_file) + + except: + e = sys.exc_info()[0] + v = sys.exc_info()[1] + + print(Fore.WHITE+Back.RED+Style.BRIGHT+"Config file not found: %s %s" % (e, v)) + + exit(1) + + #print(WebModel.connections) + + if '/' in args.model: + + args.model=args.model.replace('/', '.')[:-3] #.replace('.py', '') + + try: + + model=import_module(args.model) + + for name, obj in inspect.getmembers(sys.modules[model.__name__]): + if inspect.isclass(obj): + if obj.__module__==args.model and hasattr(obj, 'webmodel'): + + WebModel.model[name.lower()]=obj(connection) + + + #WebModel.modelobj + + except: + """ + e = sys.exc_info()[0] + v = sys.exc_info()[1] + + print(Fore.WHITE+Back.RED+Style.BRIGHT +"Error, file with model not found: %s %s" % (e, v)) + """ + print("Exception in user code:") + print("-"*60) + traceback.print_exc(file=sys.stdout) + print("-"*60) + + exit(1) + + #load the table of databases + + cursor=connection.query("show tables") + + table_exists=[] + + for row in cursor: + table=list(row.values())[0] + + if table in WebModel.model: + table_exists.append(table) + + #If don't want order + #set([1,2,3,4]) - set([2,5]) + + tables=list(WebModel.model.keys()) + + #Array diff ordered + + new_tables=[x for x in tables if x not in table_exists] + + # Get foreignkeys + + # SELECT * FROM information_schema.TABLE_CONSTRAINTS WHERE CONSTRAINT_SCHEMA='catalogdev_db' AND information_schema.TABLE_CONSTRAINTS.CONSTRAINT_TYPE = 'FOREIGN KEY' ; + + foreignkey_fields={} + + #| CONSTRAINT_CATALOG | CONSTRAINT_SCHEMA | CONSTRAINT_NAME | TABLE_SCHEMA | TABLE_NAME | CONSTRAINT_TYPE | + #+--------------------+-------------------+-----------------------------------------+---------------+-------------------+-----------------+ + #| def | catalogdev_db | product_id_attributesIDX | catalogdev_db | attributes | FOREIGN KEY | + + + #WebModel.connections + + db_name=WebModel.connections['default']['db'] + + with connection.query('SELECT * FROM information_schema.TABLE_CONSTRAINTS WHERE CONSTRAINT_SCHEMA=%s AND information_schema.TABLE_CONSTRAINTS.CONSTRAINT_TYPE = %s', [db_name, 'FOREIGN KEY']) as cursor: + + for row in cursor: + if not row['TABLE_NAME'] in foreignkey_fields: + foreignkey_fields[row['TABLE_NAME']]=[] + + foreignkey_fields[row['TABLE_NAME']]=row['CONSTRAINT_NAME'].replace('_{}IDX'.format(row['TABLE_NAME']), '') + + pass + + + #If don't want order + #new_tables=set(tables)-set(table_exists) + + #Need order new_tables + + changes=0 + + #Create new tables + + if len(new_tables)>0: + print(Style.BRIGHT+"Creating new tables...") + + changes+=1 + + for table in new_tables: + print(Style.NORMAL+"--Creating table "+table+"...") + connection.query(WebModel.model[table].create_table()) + + for table in new_tables: + + print("--Adding indexes and constraints for the new table "+table) + + if table in WebModel.arr_sql_index: + + for k_field, index in WebModel.arr_sql_index[table].items(): + print("---Added index to "+k_field) + connection.query(index) + + if table in WebModel.arr_sql_set_index: + + for k_set, index_set in WebModel.arr_sql_set_index[table].items(): + + if index_set!="": + connection.query(index_set) + print("---Added constraint to "+k_set) + + print("--Adding uniques elements for the new table") + + if table in WebModel.arr_sql_unique: + + for k_field, unique_set in WebModel.arr_sql_unique[table].items(): + + if unique_set!="": + connection.query(unique_set) + print("---Added unique to "+unique_set) + + #See if changes exists + + #Check if created tables are modified. + + try: + + model_old=import_module('backups.'+args.model) + + for name, obj in inspect.getmembers(sys.modules[model_old.__name__]): + if inspect.isclass(obj): + if obj.__module__=='backups.'+args.model and hasattr(obj, 'webmodel'): + + WebModel.model['old_'+name.lower()]=obj(connection) + + print(Style.BRIGHT+"Checking old versions of model for find changes...") + + for table in tables: + + #print(table) + + table_fields={table: {}} + + # Field | Type | Null | Key | Default | Extra | + #| id | int(11) | NO | PRI | NULL | auto_increment | + + with connection.query('describe %s' % table) as cursor: + #all_fields=cursor.fetchall() + #print(all_fields) + + for row in cursor: + table_fields[table][row['Field']]={'type': row['Type'], 'key': row['Key']} + pass + #print(table_fields) + + #connection.query("") + #Check if new table + + #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 + + 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=[] + + old_table='old_'+table + + if not old_table in WebModel.model: + WebModel.model[old_table]=WebModel.model[table] + + for f, v in WebModel.model[table].fields.items(): + + #if not f in WebModel.model[old_table].fields: + if not f in table_fields[table]: + + fields_to_add.append(f) + + #Add index + + if v.indexed==True: + + fields_to_add_index.append(f) + + changes+=1 + + #Add unique + + if v.unique==True: + + fields_to_add_unique.append(f) + + changes+=1 + + #Add constraint + + if v.foreignkey==True: + + fields_to_add_constraint.append(f) + + changes+=1 + + changes+=1 + + #If exists field in old webmodel and new + + else: + + v_old=WebModel.model[old_table].fields[f] + + if v.get_type_sql()!=v_old.get_type_sql(): + + fields_to_modify.append(f) + + changes+=1 + + #Add index + + #if v.indexed==True and v_old.indexed==False: + if v.indexed==True and table_fields[table][f]['key']!='MUL': + + fields_to_add_index.append(f) + + changes+=1 + + #if v.indexed==False and v_old.indexed==True: + if v.indexed==False and table_fields[table][f]['key']=='MUL' and v.foreignkey==False: + + fields_to_delete_index.append(f) + + changes+=1 + + #Add unique + + #if v.unique==True and v_old.unique==False: + if v.unique==True and table_fields[table][f]['key']!='UNI': + + fields_to_add_unique.append(f) + + changes+=1 + + #if v.unique==False and v_old.unique==True: + if v.unique==False and table_fields[table][f]['key']=='UNI': + + fields_to_delete_unique.append(f) + + changes+=1 + + #Add constraint + + #if v.foreignkey==True and v_old.foreignkey==False: + if v.foreignkey==True and table_fields[table][f]['key']!='MUL': + + fields_to_add_constraint.append(f) + + changes+=1 + + #if v.foreignkey==False and v_old.foreignkey==True: + if v.foreignkey==False and table_fields[table][f]['key']=='MUL': + + if table in foreignkey_fields: + + if f in foreignkey_fields[table]: + + fields_to_delete_constraint.append(f) + + changes+=1 + + # Clean fields + + #for f, v in WebModel.model[old_table].fields.items(): + + for f, v in table_fields[table].items(): + + if not f in WebModel.model[table].fields: + + #Add constraint + + #if v.foreignkey==True: + + if table in foreignkey_fields: + + if f in foreignkey_fields[table]: + + fields_to_delete_constraint.append(f) + + changes+=1 + + fields_to_delete.append(f) + + changes+=1 + + WebModel.model[table].update_table(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) + + #for field_update in arr_update: + + + #Make a for in fields, if the field not exist in old model, create, if is not same type, recreate. If no have index now, delete index, if is a new index, create, same thing with uniques + + #for field in WebModel.model + + except ImportError: + + pass + + except: + + print("Exception in user code:") + print("-"*60) + traceback.print_exc(file=sys.stdout) + print("-"*60) + + exit(1) + + original_file_path=args.model.replace('.', '/')+'.py' + + backup_path='backups/'+original_file_path + + if changes>0: + print(Style.BRIGHT+"Creating backup of the model. WARNING: DON'T DELETE BACKUPS DIRECTORY IF YOU WANT MAKE CHANGES IN THE FUTURE WITHOUT MODIFY DIRECTLY THE DATABASE") + + create_backup(original_file_path, backup_path) + + else: + if not os.path.isfile(backup_path): + create_backup(original_file_path, backup_path) + + # Execute script + + arr_script_model=args.model.split('.') + + arr_script_model.pop() + + script_model='.'.join(arr_script_model)+'.scripts.install' + + script_py=script_model.replace('.', '/')+'.py' + + if os.path.isfile(script_py): + + locked_file='/'.join(arr_script_model)+'/scripts/locked' + + if not os.path.isfile(locked_file): + + script_install=import_module(script_model) + + script_install.run() + + f=open(locked_file, 'w') + + f.write('OK') + + f.close() + + + connection.close() + + #script_model=args.model+'' + + print(Style.BRIGHT+"All tasks finished") + +def create_backup(original_file_path, file_path): + + #Create copy + + path=os.path.dirname(file_path) + + p=Path(path) + + if not p.is_dir(): + p.mkdir(0o755, True) + with open(path+'/__init__.py', 'w') as f: + f.write("#!/usr/bin/env python3\n") + + #Create path + + if os.path.isfile(file_path): + today = date.today() + shutil.copy(file_path, file_path+'.'+today.strftime("%Y%M%d%H%M%S")) + + new_file="" + + f=open(original_file_path) + + for line in f: + """ + new_line=line.replace("model[\"", "model[\"old_") + new_line=new_line.replace("model['", "model['old_") + + new_line=new_line.replace("WebModel(\"", "WebModel(\"old_") + new_line=new_line.replace("WebModel('", "WebModel('old_") + """ + new_file+=line + + f.close() + + f=open(file_path, 'w') + + f.write(new_file) + + f.close() diff --git a/parameciofast/libraries/db/extrafields/__init__.py b/parameciofast/libraries/db/extrafields/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/parameciofast/libraries/db/extrafields/arrayfield.py b/parameciofast/libraries/db/extrafields/arrayfield.py new file mode 100644 index 0000000..a0523f0 --- /dev/null +++ b/parameciofast/libraries/db/extrafields/arrayfield.py @@ -0,0 +1,94 @@ +""" +Parameciofm is a series of wrappers for Flask, 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 parameciofast.libraries.db.webmodel import PhangoField,WebModel +import json + +class ArrayField(PhangoField): + """Field for save json arrays with determined values""" + + def __init__(self, name, field_type, required=False): + """ + Args: + name (str): The name of new field + field_type (PhangoField): The type of PhangoField for save in ArrayField + required (bool): Boolean for define if field is required or not + """ + + super().__init__(name, required) + + self.field_type=field_type + + self.error_default='Sorry, the json array is invalid' + + self.set_default='' + + self.type_sql='text' + + 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' + + error=0 + + if type(self.field_type).__name__!='ArrayField': + for k,v in enumerate(value): + + value[k]=self.field_type.check(v) + if self.field_type.error: + error+=1 + + if error>0: + self.error=True + + final_value=json.dumps(value) + + final_value=WebModel.escape_sql(final_value) + + return final_value + + def get_type_sql(self): + + return 'JSON' + + def show_formatted(self, value): + + return ", ".join(value) + + def loads(self, value): + + try: + + return json.loads(value) + except: + + return False diff --git a/parameciofast/libraries/db/extrafields/colorfield.py b/parameciofast/libraries/db/extrafields/colorfield.py new file mode 100644 index 0000000..dee5b4b --- /dev/null +++ b/parameciofast/libraries/db/extrafields/colorfield.py @@ -0,0 +1,71 @@ +""" +Parameciofm is a series of wrappers for Bottle, 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 parameciofast.libraries.db.corefields import IntegerField +try: + from parameciofast.libraries.db.extraforms.colorform import ColorForm +except: + class ColorForm: + pass + +class ColorField(IntegerField): + """Simple field for save colors in hexadecimal format.""" + + 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/parameciofast/libraries/db/extrafields/datefield.py b/parameciofast/libraries/db/extrafields/datefield.py new file mode 100644 index 0000000..0b20649 --- /dev/null +++ b/parameciofast/libraries/db/extrafields/datefield.py @@ -0,0 +1,64 @@ +""" +Parameciofm is a series of wrappers for Bottle, 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 parameciofast.libraries.db.corefields import PhangoField +from parameciofast.libraries import datetime +try: + from parameciofast.libraries.db.extraforms.dateform import DateForm +except: + + class DateForm: + pass + +class DateField(PhangoField): + """Field for use and save dates in YYYYMMDDHHSS format""" + + 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/parameciofast/libraries/db/extrafields/datetimefield.py b/parameciofast/libraries/db/extrafields/datetimefield.py new file mode 100644 index 0000000..b33ac12 --- /dev/null +++ b/parameciofast/libraries/db/extrafields/datetimefield.py @@ -0,0 +1,87 @@ +""" +Parameciofm is a series of wrappers for Bottle, 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 parameciofast.libraries.db.corefields import PhangoField +from parameciofast.libraries import datetime +try: + from parameciofast.libraries.db.extraforms.dateform import DateForm +except: + class DateForm: + pass + +class DateTimeField(PhangoField): + """Field for use and save dates in MySQL date format""" + + 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 in %s' % self.name + + self.type_sql='datetime' + + 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+' '+value + + return '0000-00-00 00:00:00' + + if value==False: + + self.error=True + self.txt_error=self.error_default+' '+value + return '0000-00-00 00:00:00' + 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 + value=str(value) + value=value.replace('-', '').replace(':', '').replace(' ', '') + + return datetime.format_date(value) + + def get_type_sql(self): + + """Method for return the sql code for this type + + """ + + return 'DATETIME NOT NULL' diff --git a/parameciofast/libraries/db/extrafields/dictfield.py b/parameciofast/libraries/db/extrafields/dictfield.py new file mode 100644 index 0000000..acfaa7a --- /dev/null +++ b/parameciofast/libraries/db/extrafields/dictfield.py @@ -0,0 +1,83 @@ +""" +Parameciofm is a series of wrappers for Bottle, 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 parameciofast.libraries.db.webmodel import WebModel, PhangoField +import json + +class DictField(PhangoField): + """Field for save json dicts with determined values""" + + def __init__(self, name, field_type, required=False): + """ + Args: + name (str): The name of field + field_type (PhangoField): The type of PhangoField for save in ArrayField + required (bool): Boolean for define if field is required or not + """ + + super().__init__(name, required) + + self.field_type=field_type + + self.error_default='Sorry, the json dict is invalid' + + self.set_default='NOT NULL' + + self.type_sql='longtext' + + 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 + error=0 + for k,v in value.items(): + + value[k]=self.field_type.check(v) + if self.field_type.error: + error+=1 + + final_value=json.dumps(value) + + if error>0: + self.error=True + + #final_value=WebModel.escape_sql(final_value) + + return final_value + + def get_type_sql(self): + + return 'JSON '+self.set_default + + def show_formatted(self, value): + + return ", ".join(value) + diff --git a/parameciofast/libraries/db/extrafields/emailfield.py b/parameciofast/libraries/db/extrafields/emailfield.py new file mode 100644 index 0000000..f7a63fc --- /dev/null +++ b/parameciofast/libraries/db/extrafields/emailfield.py @@ -0,0 +1,47 @@ +""" +Parameciofm is a series of wrappers for bottle, 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 parameciofast.libraries.db.corefields import CharField +import re + +mail_pattern=re.compile(r"\w[\w\.-]*@\w[\w\.-]+\.\w+") + +class EmailField(CharField): + """Field for save and check email addreses""" + + def __init__(self, name, size=1024, required=False): + + super().__init__(name, size, required) + + self.error_default='Error: No valid format in '+self.name + + 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+' Email:'+value + + return value diff --git a/parameciofast/libraries/db/extrafields/filefield.py b/parameciofast/libraries/db/extrafields/filefield.py new file mode 100644 index 0000000..cd7f2e3 --- /dev/null +++ b/parameciofast/libraries/db/extrafields/filefield.py @@ -0,0 +1,176 @@ +import os +import sys +from pathlib import Path +from parameciofast.libraries.db.corefields import CharField +from parameciofast.libraries.db.extraforms.fileform import FileForm +from parameciofast.libraries import httputils +from parameciofast.libraries.keyutils import create_key +import traceback + +from bottle import request + + +from uuid import uuid4 +#from parameciofast.libraries.db.extraforms.fileform import FileForm + +class FileField(CharField): + + def __init__(self, name, save_folder='media/upload/files', sizes=None, module=None, size=255, required=False): + + super().__init__(name, size, required) + + self.yes_prefix=True + + self.suffix='' + + # Is relative to media folder of paramecio + + #if module!=None: + + self.save_folder=save_folder + + self.file_related=True + + self.sizes=sizes + + self.name_form=FileForm + self.extra_parameters=[self.save_folder] + + + def change_folder(self, folder): + + pass + + def check(self, value): + + files_uploaded=request.files + + field_file=self.name+'_file' + + #if not change + + if not field_file in files_uploaded: + + if value=='': + + if self.model: + + if self.model.updated: + + old_reset=self.model.yes_reset_conditions + + self.model.yes_reset_conditions=False + + with self.model.select([self.name]) as cur: + + for arr_image in cur: + + if arr_image[self.name]!='': + try: + os.remove(arr_image[self.name]) + except: + pass + + #if arr_image[self.name]!=save_file and arr_image[self.name]!='': + + #value=arr_image[self.name] + + self.model.yes_reset_conditions=old_reset + self.txt_error='Field is empty' + self.error=True + + return '' + + else: + + value=os.path.basename(value) + + return self.save_folder+'/'+value + + + # Load image file + + file_bytecode=files_uploaded[field_file].file + + filename=files_uploaded[field_file].filename + + realfilename, ext = os.path.splitext(filename) + + prefix='' + + if self.yes_prefix==True: + #prefix=uuid4().hex+'_' + prefix=create_key(5).replace('/', '-').replace('#', '-')+self.suffix+'_' + + filename=prefix+filename + + save_file=self.save_folder+'/'+filename + + # Save file + + try: + + #Check if directory exists + + if not os.path.isdir(self.save_folder): + + # Try create if not + + try: + + p=Path(self.save_folder) + + p.mkdir(mode=0o755, parents=True) + + except: + self.error=True + + self.txt_error='Error: cannot create the directory where save the image.Check permissions,' + return "" + + #files_uploaded[field_file].save(self.save_folder, overwrite=True) + + if os.path.isfile(save_file): + + os.remove(save_file) + + # Delete old files + + if self.model!=None: + + if self.model.updated: + + #old_conditions=self.model.conditions + + old_reset=self.model.yes_reset_conditions + + self.model.yes_reset_conditions=False + + with self.model.select([self.name]) as cur: + + for arr_file in cur: + + if arr_file[self.name]!=save_file and arr_file[self.name]!='': + + if os.path.isfile(arr_file[self.name]): + + os.remove(arr_file[self.name]) + + self.model.yes_reset_conditions=old_reset + + + #self.model.conditions=old_conditions + + return save_file + + except: + + self.error=True + self.txt_error='Error: cannot save the image file, Exists directory for save the file? '+traceback.format_exc() + print(traceback.format_exc()) + return "" + + def show_formatted(self, value): + + return os.path.basename(value) + diff --git a/parameciofast/libraries/db/extrafields/i18nfield.py b/parameciofast/libraries/db/extrafields/i18nfield.py new file mode 100644 index 0000000..7a48d25 --- /dev/null +++ b/parameciofast/libraries/db/extrafields/i18nfield.py @@ -0,0 +1,157 @@ +#!/usr/bin/env python3 + +import json +from parameciofast.libraries.db.webmodel import PhangoField +from parameciofast.libraries.db.coreforms import BaseForm +from parameciofast.libraries.db.extraforms.i18nform import I18nForm +from parameciofast.libraries.i18n import I18n +from parameciofast.libraries.httputils import GetPostFiles +import json +import re + +class I18nField(PhangoField): + + def __init__(self, name, form=None): + + super().__init__(name) + + if form==None: + form=BaseForm(name, '') + + self.name_form=I18nForm + self.extra_parameters=[form] + self.show_formatted_value=True + self.show_blank=False + + arr_i18n={i:'' for i in I18n.dict_i18n} + self.default_value=json.dumps(arr_i18n) + + def change_form(self, form): + self.extra_parameters=[form] + + def check_value(self, value): + + return super().check(value) + + def check(self, value): + + self.error=False + self.txt_error='' + + arr_values={} + + try: + arr_values=json.loads(value) + + if not arr_values: + arr_values={} + + except: + arr_values={} + + arr_real_values={} + + error_values=0 + + for lang in I18n.dict_i18n: + arr_real_values[lang]=arr_values.get(lang, '') + arr_real_values[lang]=self.check_value(arr_real_values[lang]) + + + if not arr_real_values[lang] or arr_real_values[lang]=='None': + arr_real_values[lang]='' + error_values+=1 + + self.error=False + + arr_values=arr_real_values + + if error_values: + if error_values==len(arr_values): + self.error=True + self.txt_error='Sorry, You field language is empty' + return json.dumps(arr_values) + + + """ + if arr_values[I18n.default_lang]=='': + self.error=True + self.txt_error='Sorry, You need default language '+I18n.default_lang + return json.dumps(arr_values) + """ + + return json.dumps(arr_values) + + def get_type_sql(self): + + return 'TEXT NOT NULL' + + def obtain_lang_value(self, lang, value): + + return value.get(self.name+'_'+lang, '') + + def obtain_lang_from_post(self, lang, value): + + #getpost=GetPostFiles() + + #getpost.obtain_post() + + return "" #GetPostFiles.post.get(self.name+'_'+lang, '') + + def show_formatted(self, value): + + if value=='': + value='{"en-US": "", "es-ES": ""}' + + value=json.loads(value) + + lang=I18n.get_default_lang() + + if value[lang]!='' or self.show_blank: + + return value[lang] + + lang_value=value[I18n.default_lang] + + if value[I18n.default_lang]=='': + for l in value: + + if value[l]!='': + lang_value=value[l] + break + + return lang_value + + + @staticmethod + def get_value(value): + + value=json.loads(value) + + lang=I18n.get_default_lang() + + if value[lang]!='': + + return value[lang] + + return value[I18n.default_lang] + +class I18nHTMLField(I18nField): + + def check_value(self, value): + + return re.sub('<.*?script?>', '', value) + +class I18nPhangoField(I18nField): + + def __init__(self, name, field_class, form=None): + + super().__init__(name, form) + + self.field_class=field_class + + def check_value(self, value): + + f=self.field_class + + return f.check(value) diff --git a/parameciofast/libraries/db/extrafields/imagefield.py b/parameciofast/libraries/db/extrafields/imagefield.py new file mode 100644 index 0000000..b91ee6f --- /dev/null +++ b/parameciofast/libraries/db/extrafields/imagefield.py @@ -0,0 +1,276 @@ +import os +import sys +from pathlib import Path +from parameciofast.libraries.db.corefields import CharField +from parameciofast.libraries.db.extraforms.fileform import FileForm +from parameciofast.libraries import httputils +from parameciofast.libraries.keyutils import create_key +import traceback + +from bottle import request +try: + from PIL import Image +except: + print("Unexpected error:", sys.exc_info()[0]) + raise + + +from uuid import uuid4 +#from parameciofast.libraries.db.extraforms.fileform import FileForm + +class ImageField(CharField): + + def __init__(self, name, save_folder='media/upload/images', sizes=None, module=None, size=255, required=False): + + super().__init__(name, size, required) + + self.yes_prefix=True + + #self.name_form=FileForm + + self.thumbnail={'mini_': 150} + + self.yes_thumbnail=False + + self.default_quality_thumb=95 + + self.suffix='' + + # Is relative to media folder of paramecio + + #if module!=None: + + self.save_folder=save_folder + + self.file_related=True + + self.sizes=sizes + + self.name_form=FileForm + self.extra_parameters=[self.save_folder] + + + def change_folder(self, folder): + + pass + + def check(self, value): + + files_uploaded=request.files + + field_file=self.name+'_file' + + #if not change + + if not field_file in files_uploaded: + + if value=='': + + if self.model: + + if self.model.updated: + + old_reset=self.model.yes_reset_conditions + + self.model.yes_reset_conditions=False + + with self.model.select([self.name]) as cur: + + for arr_image in cur: + + if arr_image[self.name]!='': + try: + os.remove(arr_image[self.name]) + except: + pass + + #if arr_image[self.name]!=save_file and arr_image[self.name]!='': + + #value=arr_image[self.name] + + self.model.yes_reset_conditions=old_reset + self.txt_error='Field is empty' + self.error=True + + return '' + + else: + + value=os.path.basename(value) + + return self.save_folder+'/'+value + + + # Load image file + + file_bytecode=files_uploaded[field_file].file + + filename=files_uploaded[field_file].filename + + try: + + im=Image.open(file_bytecode) + + except IOError: + + self.error=True + + self.txt_error='Error, file not have a valid format' + return "" + + real_width=im.size[0] + real_height=im.size[1] + + if self.sizes: + + if 'maximum' in self.sizes: + if self.sizes['maximum'][0]real_width or self.sizes['minimum'][1]>real_height: + + self.error=True + self.txt_error='Wrong size. Minimum size is '+str(self.sizes['minimum'][0])+'x'+str(self.sizes['minimum'][1]) + im.close() + return "" + + if 'resize' in self.sizes: + + height_t=0 + width_t=0 + + if real_height<=self.sizes['resize'][1]: + height_t=self.sizes['resize'][1] + + if real_width>self.sizes['resize'][0]: + + width_t=self.sizes['resize'][0] + + if height_t==0: + ratio=(real_width/width_t) + height_t=round(real_height/ratio) + + size=(width_t, height_t) + + if width_t>0 and height_t>0: + im.thumbnail(size, 3) + + format_image=im.format + + if format_image!='JPEG' and format_image!='GIF' and format_image!='PNG': + + self.error=True + self.txt_error='Format is wrong. Requires JPEG or PNG formats' + im.close() + return "" + + # Create thumbnails and move file + + realfilename, ext = os.path.splitext(filename) + + prefix='' + + if self.yes_prefix==True: + #prefix=uuid4().hex+'_' + prefix=create_key(5).replace('/', '-').replace('#', '-')+self.suffix+'_' + + filename=prefix+filename + + save_file=self.save_folder+'/'+filename + + if self.yes_thumbnail: + + for name, width_t in self.thumbnail.items(): + + im_thumb=im.copy() + + ratio=(real_width/width_t) + height_t=round(real_height/ratio) + + size=(width_t, height_t) + + save_file_thumb=self.save_folder+'/'+name+filename + + im_thumb.thumbnail(size, Image.ANTIALIAS) + im_thumb.save(save_file_thumb, "JPEG", quality=self.default_quality_thumb) + + im_thumb.close() + + # Save file + + try: + + #Check if directory exists + + if not os.path.isdir(self.save_folder): + + # Try create if not + + try: + + p=Path(self.save_folder) + + p.mkdir(mode=0o755, parents=True) + + except: + im.close() + self.error=True + + self.txt_error='Error: cannot create the directory where save the image.Check permissions,' + return "" + + #files_uploaded[field_file].save(self.save_folder, overwrite=True) + + if os.path.isfile(save_file): + + os.remove(save_file) + + im.save(save_file) + + # Delete old files + + if self.model!=None: + + if self.model.updated: + + #old_conditions=self.model.conditions + + old_reset=self.model.yes_reset_conditions + + self.model.yes_reset_conditions=False + + with self.model.select([self.name]) as cur: + + for arr_image in cur: + + if arr_image[self.name]!=save_file and arr_image[self.name]!='': + + if os.path.isfile(arr_image[self.name]): + + os.remove(arr_image[self.name]) + + self.model.yes_reset_conditions=old_reset + + + #self.model.conditions=old_conditions + + im.close() + + return save_file + + except: + + im.close() + self.error=True + self.txt_error='Error: cannot save the image file, Exists directory for save the file? '+traceback.format_exc() + print(traceback.format_exc()) + return "" + + def show_formatted(self, value): + + return os.path.basename(value) + diff --git a/parameciofast/libraries/db/extrafields/ipfield.py b/parameciofast/libraries/db/extrafields/ipfield.py new file mode 100644 index 0000000..902d3e9 --- /dev/null +++ b/parameciofast/libraries/db/extrafields/ipfield.py @@ -0,0 +1,19 @@ +from parameciofast.libraries.db.corefields import CharField +import ipaddress + +class IpField(CharField): + + def check(self, value): + + try: + + value=str(ipaddress.ip_address(value)) + + except: + + self.error=True + self.txt_error='No Valid IP' + value="" + + + return value diff --git a/parameciofast/libraries/db/extrafields/jsonfield.py b/parameciofast/libraries/db/extrafields/jsonfield.py new file mode 100644 index 0000000..3425a1f --- /dev/null +++ b/parameciofast/libraries/db/extrafields/jsonfield.py @@ -0,0 +1,115 @@ +""" +Parameciofm is a series of wrappers for Bottle, 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 parameciofast.libraries.db.webmodel import WebModel, PhangoField +import sys +try: + + import ujson as json +except: + import json + +class JsonField(PhangoField): + """Field for save json datatype values""" + + def __init__(self, name, field_type, required=False): + """ + Args: + name (str): The name of field + field_type (PhangoField): The type of PhangoField for save in JsonField + required (bool): Boolean for define if field is required or not + """ + + super().__init__(name, required) + + self.field_type=field_type + + self.error_default='Sorry, the json dict is invalid' + + self.set_default='NOT NULL' + + self.type_sql='longtext' + + 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 'JSON '+self.set_default + + def show_formatted(self, value): + + return ", ".join(value) + +# You need check the values previously. + +class JsonValueField(PhangoField): + """Field for save json mixed values. You need check the values previously, the field only check values for prevent sql injections.""" + + def __init__(self, name, required=False): + + super().__init__(name, required) + + self.error_default='Sorry, the json dict is invalid' + self.default_value={} + + #self.set_default='NOT NULL' + + def get_type_sql(self): + + return 'JSON' + + def check(self, value): + + try: + final_value=json.dumps(value) + + except json.JSONDecodeError: + + final_value='{}' + self.error=True + self.txt_error=self.error_default + + return final_value diff --git a/parameciofast/libraries/db/extrafields/langfield.py b/parameciofast/libraries/db/extrafields/langfield.py new file mode 100644 index 0000000..a67488c --- /dev/null +++ b/parameciofast/libraries/db/extrafields/langfield.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 + +from parameciofast.libraries.db.corefields import CharField +from parameciofast.libraries.db import coreforms +from parameciofast.libraries.i18n import I18n + +class LangField(CharField): + + def __init__(self, name, size=255, required=False): + + super(CharField, self).__init__(name, size, required) + + select_lang={} + + for lang in I18n.dict_i18n: + select_lang[lang]=lang + + self.change_form(coreforms.SelectForm, [select_lang]) + self.default_value=I18n.default_lang + + def check(self, value): + + if value not in I18n.dict_i18n: + + value=I18n.default_lang + + return value diff --git a/parameciofast/libraries/db/extrafields/moneyfield.py b/parameciofast/libraries/db/extrafields/moneyfield.py new file mode 100644 index 0000000..782403d --- /dev/null +++ b/parameciofast/libraries/db/extrafields/moneyfield.py @@ -0,0 +1,26 @@ +from parameciofast.libraries.db.corefields import DecimalField +from decimal import Decimal, getcontext +from locale import format_string + +getcontext().prec=2 + +class MoneyField(DecimalField): + + def __init__(self, name, required=False): + + super().__init__(name, 11, required) + + def check(self, value): + + value=Decimal(value) + + return value + + def show_formatted(self, value): + + return format_string('%.2f', Decimal(value), grouping=True) + + @staticmethod + def format_money(value): + return format_string('%.2f', Decimal(value), grouping=True) + diff --git a/parameciofast/libraries/db/extrafields/parentfield.py b/parameciofast/libraries/db/extrafields/parentfield.py new file mode 100644 index 0000000..432874b --- /dev/null +++ b/parameciofast/libraries/db/extrafields/parentfield.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 + +#from parameciofast.libraries.db.webmodel import PhangoField +from parameciofast.libraries.db.corefields import IntegerField +from parameciofast.libraries.db.coreforms import SelectModelForm +from parameciofast.libraries.httputils import GetPostFiles + +class ParentField(IntegerField): + + def __init__(self, name, size=11, required=False, field_name='name'): + + super().__init__(name, size, required) + + #self.foreignkey=True + self.indexed=True + self.field_name=field_name + + def post_register(self): + + if self.model!=None: + self.change_form(SelectModelForm, [self.model, self.field_name, self.model.name_field_id, self.name]) + + def check(self, value): + + value=super().check(value) + + if self.model!=None: + if self.model.updated==True: + if self.model.name_field_id in self.model.post: + GetPostFiles.obtain_get() + + model_id=GetPostFiles.get.get(self.model.name_field_id, '0') + + if model_id==value: + self.error=True + self.txt_error='A field cannot be its own father' + self.required=True + value=0 + return value + + + return value diff --git a/parameciofast/libraries/db/extrafields/passwordfield.py b/parameciofast/libraries/db/extrafields/passwordfield.py new file mode 100644 index 0000000..5f8d3cd --- /dev/null +++ b/parameciofast/libraries/db/extrafields/passwordfield.py @@ -0,0 +1,157 @@ +#!/usr/bin/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 parameciofast.libraries.db.corefields import PhangoField +from parameciofast.libraries.db.coreforms import PasswordForm +from hmac import compare_digest as compare_hash +from hmac import compare_digest as compare_hash +#try: +# import crypt +#except: +# pass + +#import bcrypt +from argon2 import PasswordHasher + +class PasswordField(PhangoField): + """Field for check and save passwords""" + + def __init__(self, name, size=1024, required=False): + + super(PasswordField, self).__init__(name, size, required) + self.protected=True + self.name_form=PasswordForm + self.default_value='' + self.encrypt_password=True + + def check(self, value): + + self.txt_error='' + self.error=False + + value.strip() + + if value=='': + + if self.model!=None: + + if self.model.updated==True: + self.required=False + self.check_blank=True + return "" + else: + + self.txt_error=self.error_default + self.error=True + + else: + self.txt_error=self.error_default + self.error=True + + else: + + #if crypt.METHOD_SHA512 in crypt.methods: + + #salt=crypt.mksalt(crypt.METHOD_SHA512) + if self.encrypt_password: + #value=crypt.crypt(value) + ph=PasswordHasher() + final_value=ph.hash(value) + return final_value + + """ + else: + + self.txt_error="You need the SHA512 method" + self.error=True + return "" + """ + + return value + + @staticmethod + def verify( password, h): + """Static method used for verify a password save using PasswordField""" + #return bcrypt_sha256.verify(password, h) + #return compare_hash(h, crypt.crypt(password, h)) + ph=PasswordHasher() + try: + return ph.verify(h, password) + except: + return False + +# Old function bcrypt + +""" +try: + + from passlib.hash import bcrypt + from passlib.hash import bcrypt_sha256 + + class PasswordField(PhangoField): + + def __init__(self, name, size=1024, required=False): + + super(PasswordField, self).__init__(name, size, required) + self.protected=True + self.name_form=PasswordForm + self.default_value='' + + def check(self, value): + + self.txt_error='' + self.error=False + + value.strip() + + if value=='': + + if self.model!=None: + + if self.model.updated==True: + self.required=False + self.check_blank=True + return "" + else: + + self.txt_error="The field is empty" + self.error=True + + else: + self.txt_error="The field is empty" + self.error=True + + else: + + #if crypt.METHOD_SHA512 in crypt.methods: + + #value = bcrypt_sha256.encrypt(value) + value = bcrypt_sha256.hash(value) + + return value + + @staticmethod + def verify( password, h): + + return bcrypt_sha256.verify(password, h) + +except: +""" diff --git a/parameciofast/libraries/db/extrafields/percentfield.py b/parameciofast/libraries/db/extrafields/percentfield.py new file mode 100644 index 0000000..6a5eb5e --- /dev/null +++ b/parameciofast/libraries/db/extrafields/percentfield.py @@ -0,0 +1,22 @@ +from parameciofast.libraries.db.corefields import IntegerField + +class PercentField(IntegerField): + + def __init__(self, name, required=False): + + super().__init__(name, 2, required) + + def check(self, value): + + try: + value=int(value) + + if value<0: + value=0 + if value>100: + value=100 + + except: + value=0 + + return value diff --git a/parameciofast/libraries/db/extrafields/slugifyfield.py b/parameciofast/libraries/db/extrafields/slugifyfield.py new file mode 100644 index 0000000..7127ee5 --- /dev/null +++ b/parameciofast/libraries/db/extrafields/slugifyfield.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python3 + +from parameciofast.libraries.db.corefields import CharField +from parameciofast.libraries.slugify import slugify +from parameciofast.libraries.db.coreforms import HiddenForm + +class SlugifyField(CharField): + + def __init__(self, name, size=255, field_related=None, required=False): + + super(SlugifyField, self).__init__(name, size, required) + + self.name_form=HiddenForm + + self.field_related=field_related + + def check(self, value): + + value=slugify(value) + + if value=='': + + if self.model!=None and self.field_related!=None: + + self.model.post[self.field_related]=self.model.post.get(self.field_related, '') + + value=slugify(self.model.post[self.field_related]) + + if value=='': + + self.error=True + self.error_txt='Value is empty' + + return '' + return value + + diff --git a/parameciofast/libraries/db/extrafields/urlfield.py b/parameciofast/libraries/db/extrafields/urlfield.py new file mode 100644 index 0000000..c75f45d --- /dev/null +++ b/parameciofast/libraries/db/extrafields/urlfield.py @@ -0,0 +1,42 @@ +from parameciofast.libraries.db.corefields import CharField +import re + +check_url = re.compile( + r'^(?:http|ftp)s?://' # http:// or https:// + r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' #domain... + r'localhost|' #localhost... + r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' # ...or ip + r'(?::\d+)?' # optional port + r'(?:/?|[/?]\S+)$', re.IGNORECASE) + +class UrlField(CharField): + + def check(self, value): + + self.error=False + self.txt_error='' + + if not check_url.match(value): + + self.error=True + value="" + self.txt_error='No valid URL format' + + return value + +check_domain=re.compile('^(([a-zA-Z]{1})|([a-zA-Z]{1}[a-zA-Z]{1})|([a-zA-Z]{1}[0-9]{1})|([0-9]{1}[a-zA-Z]{1})|([a-zA-Z0-9][a-zA-Z0-9-_]{1,61}[a-zA-Z0-9]))\.([a-zA-Z]{2,6}|[a-zA-Z0-9-]{2,30}\.[a-zA-Z]{2,3})$') + +class DomainField(CharField): + + def check(self, value): + + self.error=False + self.txt_error='' + + if not check_domain.match(value): + + self.error=True + value="" + self.txt_error='No valid domain format' + + return value diff --git a/parameciofast/libraries/db/extrafields/usernamefield.py b/parameciofast/libraries/db/extrafields/usernamefield.py new file mode 100644 index 0000000..0e03016 --- /dev/null +++ b/parameciofast/libraries/db/extrafields/usernamefield.py @@ -0,0 +1,16 @@ +from parameciofast.libraries.db.corefields import PhangoField +from parameciofast.libraries.db.coreforms import PasswordForm +from hmac import compare_digest as compare_hash +import crypt +import re + +class UserNameField(PhangoField): + + def check(self, value): + + if not re.match("^[A-Za-z0-9_-]+$", value): + self.txt_error='Error: use only letters, numbers, underscores and dashes for this field' + self.error=1 + value='' + + return value diff --git a/parameciofast/libraries/db/extraforms/__init__.py b/parameciofast/libraries/db/extraforms/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/parameciofast/libraries/db/extraforms/checkform.py b/parameciofast/libraries/db/extraforms/checkform.py new file mode 100644 index 0000000..5e96541 --- /dev/null +++ b/parameciofast/libraries/db/extraforms/checkform.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python3 + +from parameciofast.libraries.db.coreforms import BaseForm + +class CheckForm(BaseForm): + + def __init__(self, name, value, real_value=1): + super(CheckForm, self).__init__(name, value) + + self.real_value=real_value + + def form(self): + + arr_value={} + + arr_value[self.setform(self.default_value)]='' + + arr_value[self.setform(self.real_value)]='checked' + + return '' diff --git a/parameciofast/libraries/db/extraforms/colorform.py b/parameciofast/libraries/db/extraforms/colorform.py new file mode 100644 index 0000000..39f4f7c --- /dev/null +++ b/parameciofast/libraries/db/extraforms/colorform.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 + +""" +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 parameciofast.libraries.db.coreforms import BaseForm +from parameciofast.libraries.mtemplates import standard_t + +class ColorForm(BaseForm): + + def __init__(self, name, value): + """Form for get colors from a picker""" + + 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/parameciofast/libraries/db/extraforms/dateform.py b/parameciofast/libraries/db/extraforms/dateform.py new file mode 100644 index 0000000..b00cc22 --- /dev/null +++ b/parameciofast/libraries/db/extraforms/dateform.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 + +from parameciofast.libraries.db.coreforms import BaseForm +from parameciofast.libraries.mtemplates import standard_t +from parameciofast.libraries.datetime import format_timedata + +class DateForm(BaseForm): + + def __init__(self, name, value): + + super().__init__(name, value) + + self.yes_time=False + self.t=standard_t + + def form(self): + + y='' + m='' + d='' + h='' + min='' + s='' + min_time='' + + time=format_timedata(self.default_value) + + if time[0]: + y=int(time[0]) + m=int(time[1]) + d=int(time[2]) + h=int(time[3]) + min_time=int(time[4]) + s=int(time[5]) + + return self.t.load_template('forms/dateform.phtml', yes_time=self.yes_time, form=self.name, y=y, m=m, d=d, h=h, min=min_time, s=s) + + #def + diff --git a/parameciofast/libraries/db/extraforms/fileform.py b/parameciofast/libraries/db/extraforms/fileform.py new file mode 100644 index 0000000..9448c39 --- /dev/null +++ b/parameciofast/libraries/db/extraforms/fileform.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 + +from parameciofast.libraries.db.coreforms import BaseForm +from parameciofast.libraries.mtemplates import env_theme, PTemplate + +env=env_theme(__file__) + +t=PTemplate(env) + +class FileForm(BaseForm): + + def __init__(self, name, value, path): + + super().__init__(name, value) + + self.t=t + self.enctype=True + + def form(self): + + return self.t.load_template('forms/fileform.phtml', form=self) diff --git a/parameciofast/libraries/db/extraforms/i18nform.py b/parameciofast/libraries/db/extraforms/i18nform.py new file mode 100644 index 0000000..7f576f8 --- /dev/null +++ b/parameciofast/libraries/db/extraforms/i18nform.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 + +from parameciofast.libraries.db.coreforms import BaseForm +from parameciofast.libraries.i18n import I18n +from parameciofast.libraries.mtemplates import standard_t +import json + +class I18nForm(BaseForm): + + def __init__(self, name, value, form): + + super().__init__(name, value) + + self.form_child=form + self.t=standard_t + + def form(self): + + lang_selected=I18n.get_default_lang() + + try: + self.default_value=json.loads(self.default_value) + except: + self.default_value={} + + if type(self.default_value).__name__!='dict': + self.default_value={} + + for lang in I18n.dict_i18n: + self.default_value[lang]=self.default_value.get(lang, '') + + return standard_t.load_template('forms/i18nform.phtml', name_form=self.name_field_id, real_name_form=self.name, form=self.form_child, arr_i18n=I18n.dict_i18n, lang_selected=lang_selected, default_value=self.default_value) diff --git a/parameciofast/libraries/db/extraforms/texthtmlform.py b/parameciofast/libraries/db/extraforms/texthtmlform.py new file mode 100644 index 0000000..9d2d0ab --- /dev/null +++ b/parameciofast/libraries/db/extraforms/texthtmlform.py @@ -0,0 +1,22 @@ + +from parameciofast.libraries.db.coreforms import BaseForm +from parameciofast.libraries.mtemplates import env_theme, PTemplate + +env=env_theme(__file__) + +t=PTemplate(env) + +class TextHTMLForm(BaseForm): + + def __init__(self, name, value, t_add=None): + + super().__init__(name, value) + + self.t=t_add + + if t_add==None: + self.t=t + + def form(self): + + return self.t.load_template('forms/texthtmlform.phtml', form=self) diff --git a/parameciofast/libraries/db/extraforms/usernameform.py b/parameciofast/libraries/db/extraforms/usernameform.py new file mode 100644 index 0000000..9d2d0ab --- /dev/null +++ b/parameciofast/libraries/db/extraforms/usernameform.py @@ -0,0 +1,22 @@ + +from parameciofast.libraries.db.coreforms import BaseForm +from parameciofast.libraries.mtemplates import env_theme, PTemplate + +env=env_theme(__file__) + +t=PTemplate(env) + +class TextHTMLForm(BaseForm): + + def __init__(self, name, value, t_add=None): + + super().__init__(name, value) + + self.t=t_add + + if t_add==None: + self.t=t + + def form(self): + + return self.t.load_template('forms/texthtmlform.phtml', form=self) diff --git a/parameciofast/libraries/db/formsutils.py b/parameciofast/libraries/db/formsutils.py new file mode 100644 index 0000000..dff65f1 --- /dev/null +++ b/parameciofast/libraries/db/formsutils.py @@ -0,0 +1,145 @@ +#!/usr/bin/env python3 + +from parameciofast.libraries.db import corefields +from parameciofast.libraries.db.coreforms import PasswordForm +from parameciofast.libraries.i18n import I18n +from parameciofast.libraries.sessions import get_session +from parameciofast.libraries.keyutils import create_key_encrypt +from bottle import request + +# Need unittest + +def pass_values_to_form(post, arr_form, yes_error=True, pass_values=True): + + if pass_values: + def get_value(key): + return post[key] + + else: + def get_value(key): + return arr_form[key].default_value + + for key, value in arr_form.items(): + + post[key]=post.get(key, '') + + #if arr_form[key].default_value=='': + arr_form[key].default_value=get_value(key) + + if arr_form[key].field==None: + arr_form[key].field=corefields.CharField(key, 255, required=False) + + # Recheck value if no set error field + if arr_form[key].field.error == None: + arr_form[key].field.check(post[key]) + + #arr_form[key].txt_error="" + + if arr_form[key].required==True and arr_form[key].field.error==True and yes_error==True: + arr_form[key].txt_error=arr_form[key].field.txt_error + + # Reset error on field. + + arr_form[key].field.error=None + + return arr_form + +class CheckForm(): + + def __init__(self): + + self.error=0 + + def check(self, post, arr_form): + + for k in arr_form.keys(): + + post[k]=post.get(k, '') + + if arr_form[k].field==None: + arr_form[k].field=corefields.CharField(k, 255, required=False) + + post[k]=arr_form[k].field.check(post[k]) + arr_form[k].txt_error=arr_form[k].field.txt_error + + if arr_form[k].field.error==True and arr_form[k].required==True: + self.error+=1 + + return post, arr_form + +def check_form(post, arr_form, sufix_form='_error'): + + error=0 + error_form={} + + for k in arr_form.keys(): + + post[k]=post.get(k, '') + + if arr_form[k].field==None: + arr_form[k].field=corefields.CharField(k, 255, required=False) + + post[k]=arr_form[k].field.check(post[k]) + arr_form[k].txt_error=arr_form[k].field.txt_error + + if arr_form[k].field.error==True and arr_form[k].required==True: + error_form['#'+k+sufix_form]=arr_form[k].txt_error + error+=1 + + return error, error_form, post, arr_form + +def show_form(post, arr_form, t, yes_error=True, pass_values=True, modelform_tpl='forms/modelform.phtml'): + + # Create csrf_token in session + + generate_csrf() + + if pass_values==True: + pass_values_to_form(post, arr_form, yes_error, pass_values) + + return t.load_template(modelform_tpl, forms=arr_form) + +#Simple Function for add repeat_password form to user model + +def set_extra_forms_user(user_admin): + + user_admin.fields['password'].required=True + user_admin.fields['email'].required=True + + user_admin.create_forms(['username', 'email', 'password']) + + user_admin.forms['repeat_password']=PasswordForm('repeat_password', '') + + user_admin.forms['repeat_password'].required=True + + user_admin.forms['repeat_password'].label=I18n.lang('common', 'repeat_password', 'Repeat Password') + +#Function for initial values for necessary fields. + +def ini_fields(fields): + pass + +def csrf_token(token_id='csrf_token'): + + s=get_session() + + if not 'csrf_token' in s: + s['csrf_token']=create_key_encrypt() + s.save() + + return '' + +def generate_csrf(): + + s=get_session() + + if not 'csrf_token' in s: + s['csrf_token']=create_key_encrypt() + s.save() + + return s['csrf_token'] + +def request_type(): + + return request.environ['REQUEST_METHOD'] + diff --git a/parameciofast/libraries/db/querybuilder.py b/parameciofast/libraries/db/querybuilder.py new file mode 100644 index 0000000..a45454f --- /dev/null +++ b/parameciofast/libraries/db/querybuilder.py @@ -0,0 +1,328 @@ +#!/usr/bin/env python3 + +from collections import OrderedDict +import sys + +def query(model, str_query, args=[], connection_id='default'): + + model.connect_to_db() + + return model.sqlclass.query(str_query, args, connection_id) + +# Insert method, for insert a row in database.using a dictionary +# External agent define if the update is in code or from external source, how a form. + +def insert(model, dict_values, external_agent=True): + + model.clean_fields() + + # Connect to db + + model.post=dict_values + + #model.connect_to_db() + + query_error=False + last_sql='' + + #model.fields[model.name_field_id].required=False + + if model.name_field_id in dict_values: + del dict_values[model.name_field_id] + + try: + + fields, values, update_values=model.check_all_fields(dict_values, external_agent) + + except: + + query_error=(model.sqlclass.error_connection+' '+sys.exc_info()[0], '') + #cursor.close() + return (query_error, False) + + + c=len(values) + + arr_str=['%s' for x in range(c)] + + sql="insert into `"+model.name+"` (`"+"`, `".join(fields)+"`) VALUES ("+", ".join(arr_str)+")" + + last_sql=sql + + cursor=model.query(sql, values, model.connection_id) + + if cursor.rowcount>0: + + model.last_id=cursor.lastrowid + + cursor.close() + + # Delete cache for this table. + + return (False, True) + else: + query_error=('Cannot insert the new row', last_sql) + + + return (query_error, False) + +class QueryBuilderException(Exception): + pass + +def select(model, conditions=['', []], arr_select=[], raw_query=False): + + model.clean_fields() + + final_fields=[] + + extra_fields=[] + + #model.query_error='' + query_error=False + last_query='' + + #First table selecction + + tables_to_select=['`'+model.name+'`'] + + keys=list(model.fields.keys()) + + if len(arr_select)==0: + arr_select=keys + + # Array intersect for obtain the valid fields + + fields = list(set(keys) & set(arr_select)) + + #Creating the fields + arr_repeat_field={} + + new_fields=OrderedDict() + for field in fields: + + #Check if foreignkeyfield + + if type(model.fields[field]).__name__=="ForeignKeyField" and raw_query==False: + + if model.fields[field].table_name in arr_repeat_field: + arr_repeat_field[model.fields[field].table_name]+=1 + + else: + arr_repeat_field[model.fields[field].table_name]=0 + + table_name=model.fields[field].table_name+'` as `'+model.fields[field].table_name+str(arr_repeat_field[model.fields[field].table_name]) + + final_table_name=model.fields[field].table_name+str(arr_repeat_field[model.fields[field].table_name]) + + # The name with its alias of this related table model + + tables_to_select.append('`'+table_name+'`') + + # Add field from related table + # as "+table_name+"_"+model.fields[field].named_field + extra_fields.append("`"+final_table_name+"`.`"+model.fields[field].named_field+"` as "+field) + + # Add a condition to sql query for join the two tables. + + conditions[0]+=" AND `"+final_table_name+"`.`"+model.fields[field].identifier_field+"`=`"+model.name+"`.`"+field+"`" + + # Add extra fields from related table from select_fields ForeignKeyField class member + + select_fields=model.fields[field].select_fields + + for extra_field in select_fields: + + model.fields[field+'_'+extra_field]=model.fields[field].related_model.fields[extra_field] + model.fields_to_clean.append(field+'_'+extra_field) + + # Check if extra_field is ForeignKeyField, if yes, call this function recursively. + + extra_fields.append("`"+final_table_name+"`.`"+extra_field+"` as `"+field+"_"+extra_field+"`") + else: + # Add normal field to sql query + + final_fields.append("`"+model.name+"`.`"+field+"`") + + #if len(new_fields)>0: + #model.fields.update(new_fields) + + extra_sql_field="" + + if len(extra_fields)>0: + + extra_sql_field=", "+", ".join(extra_fields) + + if len(final_fields)==0: + query_error=("Error: without fields to search", '') + #return (query_error, False) + raise QueryBuilderException("Error: without fields to search") + + sql= ("select "+" "+model.distinct+", ".join(final_fields)+extra_sql_field+" from "+", ".join(tables_to_select)+' '+conditions[0]).strip() + + last_query=sql + + cursor=model.query(sql, conditions[1], model.connection_id) + + if cursor==False: + #query_error=(model.sqlclass.error_connection, last_query) + #cursor.close() + #return (query_error, False) + raise QueryBuilderException(model.sqlclass.error_connection+last_query) + else: + return cursor + +def select_to_array(model, conditions=['', []], fields_selected=[], raw_query=0): + + if len(fields_selected)==0: + fields_selected=model.fields.keys() + + if (model.name_field_id not in fields_selected): + fields_selected.append(model.name_field_id) + def del_row_id(row): + + try: + + index_id=row.index(model.name_field_id) + + del row[index_id] + + except: + + pass + else: + def del_row_id(row): + pass + + results=[] #OrderedDict() + + with select(model, conditions, fields_selected, raw_query) as cursor: + for row in cursor: + + if model.show_formatted and row: + for k, col in row.items(): + if model.fields[k].show_formatted_value: + row[k]=self.fields[k].show_formatted(col) + + results.append(row) + + del_row_id(results) + + return results + +def select_to_dict(model, conditions=['', []], fields_selected=[], raw_query=0, integer_dict=False): + + if not integer_dict: + def conv_int(i): + return str(i) + else: + def conv_int(i): + return i + + if len(fields_selected)==0: + fields_selected=model.fields.keys() + + if (model.name_field_id not in fields_selected): + fields_selected.append(model.name_field_id) + def del_row_id(row): + + try: + + index_id=row.index(model.name_field_id) + + del row[index_id] + + except: + + pass + else: + def del_row_id(row): + pass + + results=OrderedDict() + + with select(model, conditions, fields_selected, raw_query) as cursor: + for row in cursor: + + if model.show_formatted and row: + for k, col in row.items(): + row[k]=model.fields[k].show_formatted(col) + + results[conv_int(row[model.name_field_id])]=row + + del_row_id(results) + + return results + +def select_a_row_where(model, conditions=['', []], fields_selected=[], raw_query=0, begin=0): + + limit="limit "+str(begin)+", 1" + + with select(model, conditions, fields_selected, raw_query) as cursor: + + row=cursor.fetchone() + + if row==None: + row=False + else: + if model.show_formatted: + for k, col in row.items(): + row[k]=model.fields[k].show_formatted(col) + + return row + +def select_a_row(model, id, fields_selected=[], raw_query=0): + + conditions=['WHERE `'+model.name+'`.`'+model.name_field_id+'`=%s', [id]] + + with select(model, conditions, fields_selected, raw_query) as cursor: + + row=cursor.fetchone() + + if row==None: + row=False + else: + if model.show_formatted: + for k, col in row.items(): + row[k]=model.fields[k].show_formatted(col) + return row + + +# A method por count num rows affected for sql conditions + +def select_count(model, conditions=['', []], field_to_count='id', raw_query=True): + + print(model.dummy) + #First table selecction + + tables_to_select=['`'+model.name+'`'] + + fields=list(model.fields.keys()) + + #Creating the fields + + for field in fields: + + #Check if foreignkeyfield + + if type(model.fields[field]).__name__=="ForeignKeyField" and raw_query==False: + + table_name=model.fields[field].table_name + + tables_to_select.append('`'+table_name+'`') + + # Add a condition to sql query for join the two tables. + + conditions[0]+=" AND `"+table_name+"`.`"+model.fields[field].identifier_field+"`=`"+model.name+"`.`"+field+"`" + + sql= "select count(`"+field_to_count+"`) from "+", ".join(tables_to_select)+' '+conditions[0] + + count=0 + + with model.query(sql, conditions[1], model.connection_id) as cursor: + count=list(cursor.fetchone().values())[0] + + if model.yes_reset_conditions: + model.reset_conditions() + + return count + diff --git a/parameciofast/libraries/db/usermodel.py b/parameciofast/libraries/db/usermodel.py new file mode 100644 index 0000000..d3a59b9 --- /dev/null +++ b/parameciofast/libraries/db/usermodel.py @@ -0,0 +1,153 @@ +#!/usr/bin/env python3 + +from parameciofast.libraries.db.webmodel import WebModel +from parameciofast.libraries.db.coreforms import PasswordForm +from parameciofast.libraries.i18n import I18n +from parameciofast.libraries.httputils import GetPostFiles + +class UserModel(WebModel): + + def __init__(self, name_field_id="id"): + + super().__init__(name_field_id) + + self.password_field='password' + self.email_field='email' + self.username_field='username' + self.yes_repeat_password=True + self.check_user=True + self.check_email=True + + def create_forms(self, arr_fields=[]): + + # Add password_repeat to forms from the model + + arr_fields=super().create_forms(arr_fields) + + if self.password_field in arr_fields and self.yes_repeat_password: + + repeat_password=PasswordForm('repeat_password', '') + + repeat_password.required=1 + + repeat_password.label=I18n.lang('common', 'repeat_password', 'Repeat Password') + + repeat_password.field=self.fields['password'] + + self.create_form_after(self.password_field, repeat_password) + + return arr_fields + + """ + def insert(self, dict_values, external_agent=True): + + if 'password' in dict_values: + + dict_values['repeat_password']=dict_values.get('repeat_password', '') + + if dict_values['repeat_password']!=dict_values[self.password_field]: + self.fields[self.password_field].error=True + self.fields['password'].txt_error=I18n.lang('common', 'error_passwords_no_match', 'Error: passwords doesn\'t match') + + return super().insert(dict_values, external_agent) + """ + + def check_all_fields(self, dict_values, external_agent, yes_update=False, errors_set="insert"): + + error=0 + + try: + + fields, values, update_values=super().check_all_fields(dict_values, external_agent, yes_update, errors_set) + except: + + error+=1 + + if self.check_user==True: + + # Check if passwords matches + + if self.password_field in dict_values: + + dict_values['repeat_password']=dict_values.get('repeat_password', '') + + if dict_values[self.password_field].strip()!="": + + if dict_values['repeat_password']!=dict_values[self.password_field]: + + self.fields[self.password_field].error=True + self.fields[self.password_field].txt_error=I18n.lang('common', 'error_passwords_no_match', 'Error: passwords doesn\'t match') + + error+=1 + + # Check if exists user with same email or password + + get_id=0 + + if self.updated: + # Need the id + #GetPostFiles.obtain_get() + #GetPostFiles.obtain_post() + + getpostfiles=GetPostFiles() + + getpostfiles.obtain_get() + + get_id=getpostfiles.get.get(self.name_field_id, '0') + + post_id=getpostfiles.post.get(self.name_field_id, '0') + + if get_id!='0': + get_id=int(get_id) + + if post_id!='0': + get_id=int(post_id) + + pass + + get_id=int(get_id) + + sql_id='' + + original_conditions=self.conditions + + self.reset_conditions() + + if self.username_field in dict_values: + + self.conditions=['WHERE username=%s AND '+self.name_field_id+'!=%s', [dict_values[self.username_field], get_id]] + + if self.select_count()>0: + + self.fields[self.username_field].error=True + self.fields[self.username_field].txt_error=I18n.lang('common', 'error_username_exists', 'Error: username already exists') + self.fields_errors[self.username_field].append(self.fields[self.username_field].txt_error) + error+=1 + + + if self.check_email: + + if self.email_field in dict_values: + + self.conditions=['WHERE email=%s AND '+self.name_field_id+'!=%s', [dict_values[self.email_field], get_id]] + + if self.select_count()>0: + + self.fields[self.email_field].error=True + self.fields[self.email_field].txt_error=I18n.lang('common', 'error_email_exists', 'Error: this email is already being used') + self.fields_errors[self.email_field].append(self.fields[self.email_field].txt_error) + + error+=1 + + self.conditions=original_conditions + + if error>0: + self.query_error+='Error:if is not expected, please, check that you disabled the special checkings of this model' + return False + + return fields, values, update_values + + + + + diff --git a/parameciofast/libraries/db/webmodel.py b/parameciofast/libraries/db/webmodel.py new file mode 100644 index 0000000..3acf67c --- /dev/null +++ b/parameciofast/libraries/db/webmodel.py @@ -0,0 +1,1719 @@ +#!/usr/bin/env python3 + +from parameciofast.libraries.db.databases.sqlalchemy import SqlClass +from parameciofast.libraries.db.coreforms import BaseForm, HiddenForm +import sys +import re +import uuid +from importlib import import_module, reload +from collections import OrderedDict +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 + 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 label for the Field + + self.label=name.replace('_', ' ').title() + + # If field is required, self.required is True + + self.required=required + + # The size of field in database + + self.size=size + + # Protected, if this value != False, cannot use it in insert or update. + + self.protected=False + + # $quote_open is used if you need a more flexible sql sentence, + # @warning USE THIS FUNCTION IF YOU KNOW WHAT YOU ARE DOING + + self.quot_open='\'' + + # $quote_close is used if you need a more flexible sql sentence, + # @warning USE THIS PROPERTY IF YOU KNOW WHAT YOU ARE DOING + + self.quot_close='\'' + + # Variable where the basic text error is saved + + self.error=None + + self.txt_error="" + + # The model where this component or field live + + self.model=None + + # Property used for set this field how indexed in the database table. + + self.indexed=False + + # Property used for set this field how unique value in the database table. + + self.unique=False + + # Simple property for make more easy identify foreignkeyfields. + + self.foreignkey=False + + # Property that define the default value for this field + + self.default_value="" + + # Property that define if this field is in an update operation or insert operation + + self.update=False + + # Property used for check if this value cannot change if is in blank and is filled + + self.check_blank=False + + # Define the form, when is created forms with create_forms you can change the properties of this class + + self.name_form=BaseForm + + # Property that define if make escape in show_formatted. This property control the html transformation of <>" characters in html entities.If false, convert. + + self.escape=False + + # File related: if the field have a file related, delete the file + + self.file_related=False + + # Extra parameters for the form + + self.extra_parameters=[] + + # Template manager for the form if needed + + self.t=None + + # Error by default + + self.error_default='Error: '+self.name+' field required' + + # Show this value formatted + + self.show_formatted_value=False + + # Value used for help strings in tooltips in forms + + self.help='' + + 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 + + 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('<', '<') + + value=value.replace('>', '>') + + value=value.replace('"', '"') + + #value=WebModel.escape_sql(value) + + if value=="": + self.txt_error=self.error_default + self.error=True + + return value + + def set_relationships(self): + 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) + + final_parameters.insert(0, self.name) + final_parameters.insert(1, self.default_value) + form=self.name_form(*final_parameters) + form.default_value=self.default_value + form.required=self.required + form.label=self.label + form.field=self + form.help=self.help + return form + + def change_form(self, new_form, parameters): + """Change the base form of the field and its parameters""" + + self.name_form=new_form + + self.extra_parameters=parameters + + def post_register(self): + 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) + self.protected=True + 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): + + self.error=None + self.txt_error=self.error_default + + if value=='': + value='0' + + try: + + value=str(int(value)) + + except: + + value=0 + + if value==0: + self.txt_error=self.error_default + self.error=True + + + return value + + def get_type_sql(self): + + return 'INT NOT NULL PRIMARY KEY AUTO_INCREMENT' + +# 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. +# +# + + +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') + + #Globals class variables for internal tasks + + arr_sql_index={} + arr_sql_set_index={} + arr_sql_unique={} + arr_sql_set_unique={} + last_query="" + connection_pool=[] + first_primary_key=PrimaryKeyField('id') + + #A dictionary for add models here + + model=OrderedDict() + + connections={'default': {'host': 'localhost', 'user': 'user', 'password': '', 'db': 'default', 'charset': 'utf8', 'set_connection': False, 'db_type': 'pymysql'} } + + connection_id="default" + + webmodel=True + + global_cached=False + + #sqlclass=SqlClass() + + #make_connection=sqlclass.connect_to_db + + @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 + + self.cached_runquery=WebModel.global_cached + + self.type_cache='file' + + #The name of the table + + self.name=type(self).__name__.lower() + + self.label=self.name + + self.label_general=self.name + + self.name_field_id=name_field_id + + #Fields of the table, inserted with register method + + self.fields=OrderedDict() + + # Errors of fields of the table, for safe thread reasons. + + #self.fields_error=OrderedDict() + + #The tables related with foreignkeyfield to this table + + self.related=[] + + #A dictionary where forms of this model are saved + + self.forms=OrderedDict() + + self.cache_method='' + + # A dictionary with the errors in fields. + + self.fields_errors={} + + self.errors={} + + self.num_errors=0 + + self.query_error="" + + self.values_query=[] + + self.conditions=["WHERE 1=1", []] + + self.order_by="ORDER BY `"+self.name+"`.`id` ASC" + + self.limit="" + + self.related_models_deleted=[] + + self.required_save={} + + #Create id field + + primary_key=WebModel.first_primary_key + + primary_key.name=self.name_field_id + + self.register(primary_key) + + #self.register(PrimaryKeyField(self.name_field_id)) + + #self.model[name]=self + + self.yes_reset_conditions=True + + #self.create_fields() + + self.updated=False + + self.valid_fields=[] + + self.last_id=0 + + self.distinct='' + + # A simple dictionary where post values are saved for use of fields classes + + self.post={} + + # A simple dictionary that save the fields that have files related. If i delete the row in database i need delete the files related + + self.files_delete={} + + self.sqlclass=sqlclass + + self.fields_to_clean=[] + + self.create_fields() + + # property for use show_formatted property if needed + + self.show_formatted=False + + # property for def if the model have enctype + + 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 + + self.yes_reset_conditions=True + + self.conditions=["WHERE 1=1", []] + + 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 "+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)) + + pass + + # 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 + + self.fields[field_model.name]=field_model + + self.fields[field_model.name].model=self + + if required: + self.fields[field_model.name].required=required + + self.fields[field_model.name].post_register() + + #self.files_delete[field_model.name]=field_model.file_related + + # A method for create the id field. + + def create_id_field(self, field_name="id"): + pass + + # A method for connect to database + + def connect_to_db(self): + + #if WebModel.make_connection(self.connections[self.connection_id])==False: + #raise NameError(sqlclass.error_connection) + + #self.connection_pool.append(True) + + + #if self.sqlclass.connect_to_db(self.connections[self.connection_id])==False: + # raise NameError(sqlclass.error_connection) + + #WebModel.make_connection=sqlclass.dummy_connect + pass + + def dummy_connect(self, connection): + return True + + # 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) + + # 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] + + # Insert method, for insert a row in database.using a dictionary + # 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() + + # Connect to db + + self.post=dict_values + + #self.connect_to_db() + + self.query_error='' + + #self.fields[self.name_field_id].required=False + + try: + + arr_return=self.check_all_fields(dict_values, external_agent) + + if arr_return: + + fields, values, update_values=arr_return + + else: + 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)] + + 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) + + if cursor.rowcount>0: + + self.last_id=cursor.lastrowid + + cursor.close() + + # Delete cache for this table. + + return True + else: + self.query_error='Cannot insert the new row' + + cursor.close() + + return False + + # 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() + + self.post=dict_values + + # Connect to db + + #self.fields[self.name_field_id].required=False + + if self.name_field_id in dict_values: + del dict_values[self.name_field_id] + + #self.connect_to_db() + + self.query_error='' + + #try: + self.updated=True + + try: + + arr_return=self.check_all_fields(dict_values, external_agent, True, 'update') + + if arr_return: + + fields, values, update_values=arr_return + + else: + return False + + except: + + self.query_error+="\n"+traceback.format_exc() + #print(traceback.format_exc()) + return False + + 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) + + if self.yes_reset_conditions: + self.reset_conditions() + + cursor.close() + + return True + + """ + if cursor.rowcount>0: + + if self.yes_reset_conditions: + self.reset_conditions() + + return True + + else: + + self.query_error='Cannot update the row' + + return False + """ + """ + except: + + #self.query_error=sqlclass.error_connection + e = sys.exc_info()[0] + v = sys.exc_info()[1] + + self.error_connection="Error in query: %s %s" % (e, v) + + return False + """ + + def reset_conditions(self): + """Method for reset self.conditions to default values""" + self.conditions=["WHERE 1=1", []] + self.limit='' + + # A method for select fields from a table in db. Support for foreignkeys. + #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() + + # Connect to db + + #self.connect_to_db() + + conditions=self.conditions + + final_fields=[] + + extra_fields=[] + + self.query_error='' + + #First table selecction + + tables_to_select=[self.field_quote+self.name+self.field_quote] + + keys=list(self.fields.keys()) + + if len(arr_select)==0: + arr_select=keys + + # Array intersect for obtain the valid fields + + #fields = list(set(keys) & set(arr_select)) + fields=[ select for select in arr_select if select in keys ] + """ + for select in arr_select: + if select in keys: + fields.append(select) + """ + + #Creating the fields + arr_repeat_field={} + + new_fields=OrderedDict() + for field in fields: + + #Check if foreignkeyfield + + if type(self.fields[field]).__name__=="ForeignKeyField" and raw_query==False: + + if self.fields[field].table_name in arr_repeat_field: + arr_repeat_field[self.fields[field].table_name]+=1 + + else: + arr_repeat_field[self.fields[field].table_name]=0 + + 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(self.field_quote+table_name+self.field_quote) + + # Add field from related table + # as "+table_name+"_"+self.fields[field].named_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 "+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 + + select_fields=self.fields[field].select_fields + + for extra_field in select_fields: + + self.fields[field+'_'+extra_field]=self.fields[field].related_model.fields[extra_field] + self.fields_to_clean.append(field+'_'+extra_field) + + # Check if extra_field is ForeignKeyField, if yes, call this function recursively. + + 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.field_quote+self.name+self.field_quote+"."+self.field_quote+field+self.field_quote) + + #if len(new_fields)>0: + #self.fields.update(new_fields) + + extra_sql_field="" + + if len(extra_fields)>0: + + extra_sql_field=", "+", ".join(extra_fields) + + if len(final_fields)==0: + self.query_error="Error: without fields to search" + return False + + sql= ("select "+" "+self.distinct+", ".join(final_fields)+extra_sql_field+" from "+", ".join(tables_to_select)+' '+conditions[0]+' '+self.order_by+' '+self.limit).strip() + + self.last_query=sql + + if self.yes_reset_conditions: + self.reset_conditions() + + cursor=self.query(sql, conditions[1], self.connection_id) + + if not cursor: + self.query_error=self.sqlclass.error_connection + + return False + else: + return cursor + + # 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 + + 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) + + if self.yes_reset_conditions: + self.reset_conditions() + + if count>0: + return True + + return False + + def select_a_field(self, field): + pass + + def select_a_row(self, id, fields_selected=[], raw_query=0): + """Shortcut for get a simple row from a 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. + """ + + 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" + + with self.select(fields_selected, raw_query) as cursor: + self.reset_conditions() + + row=cursor.fetchone() + + if row==None: + row=False + else: + if self.show_formatted: + #for k, col in row.items(): + row[k]={k:self.fields[k].show_formatted(col) for k,col in row.items()} + + + 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" + + with self.select(fields_selected, raw_query) as cursor: + + row=cursor.fetchone() + + if row==None: + row=False + else: + if self.show_formatted: + for k, col in row.items(): + row[k]=self.fields[k].show_formatted(col) + + return row + + + 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()) + + if (self.name_field_id not in fields_selected): + fields_selected.append(self.name_field_id) + def del_row_id(row): + + try: + + index_id=row.index(self.name_field_id) + + del row[index_id] + + except: + + pass + else: + def del_row_id(row): + pass + + results=[] #OrderedDict() + + with self.select(fields_selected, raw_query) as cursor: + for row in cursor: + + if self.show_formatted and row: + for k, col in row.items(): + if self.fields[k].show_formatted_value: + row[k]=self.fields[k].show_formatted(col) + + results.append(row) + + del_row_id(results) + + return results + + + 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 + else: + def check_index(index): + return str(index) + + if len(fields_selected)==0: + fields_selected=list(self.fields.keys()) + + if (self.name_field_id not in fields_selected): + fields_selected.append(self.name_field_id) + def del_row_id(row): + + try: + + index_id=row.index(self.name_field_id) + + del row[index_id] + + except: + + pass + else: + def del_row_id(row): + pass + + results=OrderedDict() + + with self.select(fields_selected, raw_query) as cursor: + for row in cursor: + + if self.show_formatted and row: + for k, col in row.items(): + row[k]=self.fields[k].show_formatted(col) + + results[check_index(row[self.name_field_id])]=row + + del_row_id(results) + + return results + + # 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 + + self.connect_to_db() + + conditions=self.conditions + + #First table selecction + + tables_to_select=[self.field_quote+self.name+self.field_quote] + + fields=list(self.fields.keys()) + + #Creating the fields + + for field in fields: + + #Check if foreignkeyfield + + if type(self.fields[field]).__name__=="ForeignKeyField" and raw_query==False: + + table_name=self.fields[field].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 "+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("+self.field_quote+field_to_count+self.field_quote+") from "+", ".join(tables_to_select)+' '+conditions[0] + + count=0 + + with self.query(sql, conditions[1], self.connection_id) as cursor: + count=list(cursor.fetchone().values())[0] + + if self.yes_reset_conditions: + self.reset_conditions() + + return count + + #+' ORDER BY '+self.order_by+' '+self.limit).strip() + + # 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 + + #+' '+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) + + if self.yes_reset_conditions: + self.reset_conditions() + + if result.rowcount>0: + + result.close() + return True + else: + + result.close() + 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] + + return self + + @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: + in_list[x]=str(int(in_list[x])) + except: + in_list[x]='0' + 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])) + + 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') + arr_order.append('DESC') + + final_order=[] + + for o,v in order.items(): + + if o in self.fields: + + final_order.append(o+' '+arr_order[v]) + + self.order_by='order by '+", ".join(final_order) + + 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]) + + sql_limit=str(limit[0]) + + if len(limit)>1: + sql_limit+=', '+str(limit[1]) + + self.limit='limit '+sql_limit + + return self + + # Method for create sql tables + + def create_table(self): + """Method for create a table from this model object""" + + #self.connect_to_db() + + self.arr_sql_index[self.name]={} + self.arr_sql_set_index[self.name]={} + self.arr_sql_unique[self.name]={} + self.arr_sql_set_unique[self.name]={} + + #foreach($this->components as $field => $data) + table_fields=[] + + #Create id field + #Not neccesary + #table_fields.append('`'+self.name_field_id+"` INT NOT NULL PRIMARY KEY AUTO_INCREMENT") + fields=self.fields + for field, data in fields.items(): + + 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 '+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]="" + + + #Check if unique + + if fields[field].unique==True: + + 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 '+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.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.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): + + # First delete fields + + for field in fields_to_delete_index: + + print("---Deleting index from "+field+" in "+self.name) + + 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 '+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.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.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.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.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 '+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: + + print("---Adding foreign key to "+field+" in "+self.name) + + table_related=self.fields[field].table_name + + id_table_related=self.fields[field].table_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.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): + """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 + + def all_fields(): + pass + + #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=[] + update_values=[] + self.errors[errors_set]=[] + self.num_errors=0 + #A dictionary that define if update property is added + + updated_field={} + updated_field['insert']=0 + updated_field['update']=1 + + + error=False + + if yes_update==True: + f_update=lambda field, value: self.field_quote+field+self.field_quote+"=%s" + else: + f_update=lambda field, value: "" + + # I can optimize this later + + for k, v in self.fields.items(): + + #List where the errors are saved + + self.fields_errors[k]=[] + + if k in dict_values: + + # If fields is protected, but external_agent =0, then insert + # If fields is not protected always insert if not error checking + + value=dict_values[k] + + # Cleaning the error + + self.fields[k].error=False + + if (self.fields[k].protected==None or self.fields[k].protected==False or external_agent==False): + + if k in self.valid_fields: + + self.fields[k].update=updated_field[errors_set] + + value=self.fields[k].check(value) + + if self.fields[k].check_blank==False or self.updated==False: + + # If error checking, value=False + + if self.fields[k].error==True and self.fields[k].required==True: + + #Error, need this fields. + self.num_errors+=1 + + if self.fields[k].txt_error=='': + + self.fields_errors[k].append("Error: "+v.label+" field required") + + else: + self.fields_errors[k].append(self.fields[k].txt_error) + + error=True + + else: + + fields.append(k) + + #final_value=self.fields[k].quot_open+value+self.fields[k].quot_close + #final_value=self.fields[k].quot_open+value+self.fields[k].quot_close + + values.append(value) + + update_values.append(f_update(k, value)) + else: + self.num_errors+=1 + + self.fields_errors[k].append("Error: "+self.fields[k].label+" is not in valid fields") + self.fields[k].error=True + self.fields[k].txt_error="Error: "+self.fields[k].label+" is not in valid fields" + error=True + + else: + self.num_errors+=1 + + self.fields_errors[k].append("Error: "+self.fields[k].label+" is protected field") + self.fields[k].error=True + self.fields[k].txt_error="Error: "+self.fields[k].label+" is protected field" + error=True + + elif v.required==True: + + self.num_errors+=1 + + self.fields_errors[k].append("Error: "+v.label+" field required") + error=True + + elif v.required==False and k!=self.name_field_id and not yes_update: + + fields.append(k) + + values.append(self.fields[k].default_value) + + update_values.append(f_update(k, self.fields[k].default_value)) + + if len(fields)==0: + + self.num_errors+=1 + + self.errors[errors_set].append("Error: no elements to insert in table") + + error=True + + if error==True: + + self.num_errors+=1 + + self.errors[errors_set].append("Error: error checking the values of the table") + + return False + + return (fields, values, update_values) + + + #Reset the require field in fields + + def reset_require(self): + """Reset the require attribute in fields""" + + for k, v in self.fields.items(): + + self.required_save[k]=self.fields[k].required + self.fields[k].required=False + + + #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 + + #Choose all fields to updated + + def set_valid_fields(self, fields={}): + """Choose all fields to updated""" + + if len(fields)==0: + fields=self.fields.keys() + + self.valid_fields=fields + + #Create a form based in table. + + def create_forms(self, arr_fields=[]): + """Create a form based in table.""" + + self.forms=OrderedDict() + + if len(arr_fields)==0: + arr_fields=list(self.fields.keys()) + + if self.name_field_id in arr_fields: + del arr_fields[arr_fields.index(self.name_field_id)] + + #for name_field, field in self.fields.items(): + for name_field in arr_fields: + self.valid_fields.append(name_field) + self.forms[name_field]=self.fields[name_field].create_form() + + 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() + + for name_form, form in self.forms.items(): + new_dict[name_form]=form + if name_form==form_after: + new_dict[new_form.name]=new_form + + 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='' + + for k_error in self.fields_errors.values(): + + for error in k_error: + arr_error.append(error) + + for type_error in self.errors.values(): + for error in type_error: + arr_error.append(error) + + arr_error.append(self.query_error) + + error_txt="\n".join(arr_error) + + 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='' + + for field_error, k_error in self.fields_errors.items(): + + for error in k_error: + arr_error[field_error]=error + """ + for type_error in self.errors.values(): + for error in type_error: + arr_error[field_error]=error + """ + + 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() + + #connection_to_delete=[] + + #WebModel.make_connection=self.sqlclass.connect_to_db + + #for key in self.sqlclass.connection: + #self.sqlclass.close(key) + #connection_to_delete.append(key) + + #self.sqlclass.connection={} + + #for key in connection_to_delete: + #del sqlclass.connection[key] + @staticmethod + def escape_sql(value): + """Manual escape for sql, you shouldn't use it""" + + value=str(value) + + return value.replace("'","\\'").strip() + """ + def __del__(self): + + self.close() + """ + + # 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(): + + post[k]=post.get(k, '') + + return post + +class QueryModel(WebModel): + + def __init__(self, model_name, sqlclass=None, name_field_id="id"): + + super().__init__(sqlclass, name_field_id) + + self.name=model_name.lower() + + self.label=self.name + + self.label_general=self.name + + self.order_by="ORDER BY "+self.field_quote+self.name+self.field_quote+"."+self.field_quote+"id"+self.field_quote+" ASC" + + diff --git a/parameciofast/libraries/fastutils.py b/parameciofast/libraries/fastutils.py new file mode 100644 index 0000000..2de6bb2 --- /dev/null +++ b/parameciofast/libraries/fastutils.py @@ -0,0 +1,9 @@ +from pydantic import BaseModel + +class ResponseData(BaseModel): + + error: bool + message: str + code_error: int | None = None + + diff --git a/parameciofast/libraries/i18n.py b/parameciofast/libraries/i18n.py index 52aaf8d..bed3815 100644 --- a/parameciofast/libraries/i18n.py +++ b/parameciofast/libraries/i18n.py @@ -221,8 +221,8 @@ class I18n: return json.dumps(arr_final) @staticmethod - def session_lang(request): + def session_lang(session): - return request.session.get('lang', I18n.get_default_lang()) + return session.get('lang', I18n.get_default_lang()) common_pgettext=PGetText(__file__) diff --git a/parameciofast/modules/fastadmin/__init__.py b/parameciofast/modules/fastadmin/__init__.py index 3f870d2..6e657f8 100644 --- a/parameciofast/modules/fastadmin/__init__.py +++ b/parameciofast/modules/fastadmin/__init__.py @@ -2,7 +2,7 @@ from fastapi import FastAPI, Cookie, Request from fastapi.responses import HTMLResponse, RedirectResponse from typing import Annotated from settings import config -from parameciofast.fast import app +#from parameciofast.fast import app from parameciofast.libraries.session import ParamecioSession from starlette.middleware.sessions import SessionMiddleware diff --git a/parameciofast/modules/fastadmin/app.py b/parameciofast/modules/fastadmin/app.py index 6567f65..a13699b 100644 --- a/parameciofast/modules/fastadmin/app.py +++ b/parameciofast/modules/fastadmin/app.py @@ -1,20 +1,23 @@ -from fastapi import FastAPI, Cookie, Request +from fastapi import FastAPI, Cookie, Request, Response 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 +from pydantic import BaseModel, Field +from parameciofast.modules.fastadmin.models.admin import UserAdmin +from parameciofast.libraries.db.webmodel import WebModel +from parameciofast.libraries.fastutils import ResponseData env=env_theme(__file__) t=PTemplate(env, app.url_path_for) +useradmin=UserAdmin() + @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): - """ - if not paramecio_session: - return RedirectResponse(app.url_path_for('login_admin')) - """ + if not request.session.get('login_admin', None): return RedirectResponse(app.url_path_for('login_admin')) @@ -24,8 +27,75 @@ def home_admin(request: Request, paramecio_session: Annotated[str | None, Cookie @admin_app.get('/login', response_class=HTMLResponse) def login_admin(request: Request): - #session=request.session + db=WebModel.connection() - i18n=I18n('admin', I18n.session_lang(request)) + with db.query('select count(id) as num_users from useradmin', []) as cursor: + num_users=cursor.fetchone()['num_users'] + + if not num_users: + return RedirectResponse(app.url_path_for('signup_admin')) + + db.close() + + i18n=I18n('admin', I18n.session_lang(request.session)) return t.load_template('login.phtml', title=i18n.tlang('Login'), tlang=i18n.tlang, url_for=app.url_path_for) + +@admin_app.get('/signup', response_class=HTMLResponse) +def signup_admin(request: Request): + + db=WebModel.connection() + + with db.query('select count(id) as num_users from useradmin', []) as cursor: + num_users=cursor.fetchone()['num_users'] + + if num_users>0: + return RedirectResponse(app.url_path_for('signup_admin')) + + db.close() + + i18n=I18n('admin', I18n.session_lang(request.session)) + + return t.load_template('signup.phtml', title=i18n.tlang('Signup'), tlang=i18n.tlang, url_for=app.url_path_for) + +class UserAdmin(BaseModel): + username: str = Field(description="The username of user") + password: str = Field(description="The password of user") + remember_login: bool | None = None + +@admin_app.post('/login') +def check_login_admin(user: UserAdmin, request: Request) -> ResponseData: + + db=WebModel.connection() + + i18n=I18n('admin', I18n.session_lang(request.session)) + + error=1 + + message=i18n.tlang('Invalid user and password') + + if user.username!='' and user.password!='': + + with db.query('select * from useradmin WHERE username=%s', [user.username]) as cursor: + result=cursor.fetchone() + + if result: + + if useradmin.fields['password'].verify(user.password, result['password']): + + request.session['login_admin']=True + error=0 + message='' + + db.close() + + return {'error': error, 'message': message} + +@admin_app.get('/logout') +def logout_admin(request: Request) -> RedirectResponse: + + if 'login_admin' in request.session: + del request.session['login_admin'] + + return RedirectResponse(app.url_path_for('login_admin')) + diff --git a/parameciofast/modules/fastadmin/models/__init__.py b/parameciofast/modules/fastadmin/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/parameciofast/modules/fastadmin/models/admin.py b/parameciofast/modules/fastadmin/models/admin.py new file mode 100644 index 0000000..183674f --- /dev/null +++ b/parameciofast/modules/fastadmin/models/admin.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 + +from parameciofast.libraries.i18n import I18n +from parameciofast.libraries.db.webmodel import WebModel +#from parameciofast.libraries.db.usermodel import UserModel +from parameciofast.libraries.db import corefields +from parameciofast.libraries.db.extrafields.emailfield import EmailField +from parameciofast.libraries.db.extrafields.passwordfield import PasswordField +from parameciofast.libraries.db.extrafields.langfield import LangField +from parameciofast.libraries.db.extrafields.ipfield import IpField +from parameciofast.libraries.db.extrafields.datetimefield import DateTimeField + +class PrivilegesField(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 UserAdmin(WebModel): + + #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(PrivilegesField('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 LoginTries(WebModel): + + #def create_fields(self): + def __init__(self, connection=None): + + super().__init__(connection) + self.register(IpField('ip')) + self.register(corefields.IntegerField('num_tries', 1)) + self.register(DateTimeField('last_login')) + + +""" + +user_admin=WebModel('user_admin') + +user_admin.register(corefields.CharField('username')) + +user_admin.fields['username'].required=True + +user_admin.register(corefields.CharField('password')) + +user_admin.fields['password'].required=True + +user_admin.register(EmailField('email')) + +user_admin.fields['email'].required=True + +user_admin.register(corefields.CharField('token_recovery')) + +user_admin.register(corefields.BooleanField('privileges')) + +#user_admin.register(corefields.CharField('prueba')) + +""" diff --git a/parameciofast/modules/fastadmin/templates/login.phtml b/parameciofast/modules/fastadmin/templates/login.phtml index 18fef6b..c3e10ea 100644 --- a/parameciofast/modules/fastadmin/templates/login.phtml +++ b/parameciofast/modules/fastadmin/templates/login.phtml @@ -22,6 +22,7 @@
+ <%block name="content">
${tlang('Login')} @@ -43,9 +44,10 @@
- +
+
@@ -56,6 +58,9 @@ + <%block name="jscript"> + + diff --git a/parameciofast/modules/fastadmin/templates/signup.phtml b/parameciofast/modules/fastadmin/templates/signup.phtml new file mode 100644 index 0000000..5519f93 --- /dev/null +++ b/parameciofast/modules/fastadmin/templates/signup.phtml @@ -0,0 +1,113 @@ +<%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('Password not equal')} +
+
+ +
+
+ +<%block name="jscript"> +$(document).ready( function () { + + $('#login_form').submit( function () { + + $('#loader-div').show(); + + $('#login_submit').prop('disabled', true); + + data_form={'username': $('#username_form').val(), 'email': $('#email_form').val(), 'password': $('#password_form').val(), 'repeat_password': $('#repeat_password_form').val(), 'csrf_token': $("#csrf_token").val(), 'remember_login': 0}; + + $.ajax({ + url: "${url_for('check_login_admin')}", + method: "POST", + dataType: "json", + contentType : 'application/json', + data: JSON.stringify(data_form) + }).done(function(data) { + + if(data.error==0) + { + + //location.reload() + location.href="${url_for('home_admin')}"; + + } + else + { + + $('#login_submit').prop('disabled', false); + + $('#loader-div').hide(); + + // Firefox have a horrible and stupid bug and you need attr for set de new csrf_token + + /*$('#csrf_token').attr('value', data.csrf_token); + + $('#loading').hide('slow'); + + if(data.hasOwnProperty('disable')) { + + $('#username_error').html("${_('Error, your user is disabled, you need support of web administration')}"); + + } if(data.hasOwnProperty('you_cannot_login')) { + + if(data.you_cannot_login) { + + $('#username_error').html("${_('Error, excessive tries, wait some minutes for login again')}"); + + } + else { + + $('#username_error').html("${_('Error, wrong username or password')}"); + + } + + } + else { + + $('#username_error').html("${_('Error, wrong username or password')}"); + + }*/ + + } + + }); + + return false; + + }); + +}); +