Commit 24c9f63f by aa.gusti

Merge branch '5.0.3-oracle' into '5.0.3'

# Conflicts:
#   opensipkd/base/views/base_views.py
2 parents 05cbbc00 4c69bd02
5.0.5 2027-07-11
Penyesuaian ke deform 3.x
5.0.4 2026-06-01
1. Penambahan parameter
- ws_print_url
......
......@@ -14,7 +14,7 @@ pyramid.debug_notfound = true
pyramid.debug_routematch = true
pyramid.debug_templates = true
default_locale_name = id
sqlalchemy.url = postgresql://aagusti:a@localhost:5432/demos
sqlalchemy.url = postgresql://aagusti:a@localhost:5432/webrs
pyramid.includes =
pyramid_tm
pyramid_beaker
......@@ -64,13 +64,13 @@ reg_captcha = 1
reg_verify = 1
# MAIL
mail.host = smtp.gmail.com
mail.port = 465
mail.ssl = True
mail.tls = True
mail.username = opensipkd@gmail.com
mail.password = ajmyoksxeiprmtyc
mail.sender_name = "openSIPKD"
; mail.host = smtp.gmail.com
; mail.port = 465
; mail.ssl = True
; mail.tls = True
; mail.username = opensipkd@gmail.com
; mail.password = ajmyoksxeiprmtyc
; mail.sender_name = "openSIPKD"
;PBB
pbb.url = oracle://PBB:A@10.8.50.62/simpbb
......
......@@ -27,7 +27,7 @@ from .models.handlers import LogDBSession
from .models.meta import Base
from .models.users import init_model
from .models import Route
# from .models import TABLE_ARGS
# from deform import ZPTRendererFactory, Form
# from deform.widget import default_resource_registry
......@@ -302,10 +302,25 @@ def get_config(settings):
return config
# def get_schema_for_dialect(engine_dialect_name: str) -> str | None:
# """Returns 'public' for PostgreSQL or None for Oracle to match native defaults."""
# if "postgresql" in engine_dialect_name.lower():
# return "public"
# return None # Triggers default user-schema fallback on Oracle
def init_db(settings):
engine = engine_from_config(
settings, 'sqlalchemy.', client_encoding='utf8',
settings, 'sqlalchemy.',
# client_encoding='utf8',
max_identifier_length=30) # , convert_unicode=True
# global TABLE_ARGS
# # Resolve the target schema namespace dynamically based on active engine
# TABLE_ARGS = dict(extend_existing=True,
# schema=get_schema_for_dialect(engine.dialect.name))
DBSession.configure(bind=engine)
LogDBSession.configure(bind=engine)
Base.metadata.bind = engine
......@@ -314,6 +329,13 @@ def init_db(settings):
def datetime_output_handler(cursor, name, default_type, size, precision, scale):
"""Intercepts Oracle TSTZ data types and returns them with tzinfo intact."""
# DB_TYPE_TIMESTAMP_TZ handles Oracle's 'TIMESTAMP WITH TIME ZONE'
import oracledb
if default_type == oracledb.DB_TYPE_TIMESTAMP_TZ:
return cursor.var(oracledb.DB_TYPE_TIMESTAMP_TZ, arraysize=cursor.arraysize, outconverter=lambda v: v)
def main(global_config, **settings):
""" This function returns a Pyramid WSGI application.
"""
......@@ -322,6 +344,21 @@ def main(global_config, **settings):
if not settings.get('localization', ''):
settings['localization'] = 'id_ID.UTF-8'
if settings.get("lib_dir"):
sqlalchemy_url = settings.get("sqlalchemy.url")
if sqlalchemy_url and sqlalchemy_url.find("oracledb") > -1:
try:
import oracledb
oracledb.init_oracle_client(lib_dir=settings.get("lib_dir"))
# Apply the global configuration handler to your connection pool
oracledb.defaults.outputtypehandler = datetime_output_handler
_logging.debug("oracledb initialized")
except:
pass
locale.setlocale(locale.LC_ALL, settings['localization'])
if 'timezone' not in settings:
settings['timezone'] = DefaultTimeZone
......
import logging
from logging.config import fileConfig
from sqlalchemy import engine_from_config
......@@ -11,6 +12,19 @@ from opensipkd.models import Base
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
log = logging.getLogger(__name__)
url = config.get_main_option("sqlalchemy.url")
if url.find("oracledb") > -1:
log.error("OracleDB used: %s", url)
try:
import oracledb
lib_dir = config.get_main_option("lib_dir")
if lib_dir:
oracledb.init_oracle_client(lib_dir=lib_dir)
except Exception as e:
log.error(f"An error occurred: {str(e)}")
log.error("Oracle not initialize")
if config.config_file_name is not None:
fileConfig(config.config_file_name)
......
......@@ -18,6 +18,7 @@ depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
try:
op.create_table('eselon',
sa.Column('pangkat', sa.String(length=32), nullable=True),
sa.Column('ruang', sa.String(length=1), nullable=True),
......@@ -29,10 +30,14 @@ def upgrade():
sa.Column('updated', sa.DateTime(), nullable=True),
sa.Column('create_uid', sa.Integer(), nullable=True),
sa.Column('update_uid', sa.Integer(), nullable=True),
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('id', sa.Integer(), sa.Identity(always=True),
primary_key=True, nullable=False),
sa.PrimaryKeyConstraint('id', name=op.f('pk_eselon')),
schema='public'
# schema='public'
)
except Exception as e:
print(str(e))
try:
op.create_table('pangkat',
sa.Column('pangkat', sa.String(length=32), nullable=True),
sa.Column('ruang', sa.String(length=1), nullable=True),
......@@ -43,10 +48,14 @@ def upgrade():
sa.Column('updated', sa.DateTime(), nullable=True),
sa.Column('create_uid', sa.Integer(), nullable=True),
sa.Column('update_uid', sa.Integer(), nullable=True),
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('id', sa.Integer(), sa.Identity(always=True),
primary_key=True, nullable=False),
sa.PrimaryKeyConstraint('id', name=op.f('pk_pangkat')),
schema='public'
# schema='public'
)
except Exception as e:
print(str(e))
try:
op.create_table('jabatan',
sa.Column('jenis', sa.SmallInteger(), nullable=True),
sa.Column('nama_lain', sa.String(
......@@ -61,22 +70,27 @@ def upgrade():
sa.Column('updated', sa.DateTime(), nullable=True),
sa.Column('create_uid', sa.Integer(), nullable=True),
sa.Column('update_uid', sa.Integer(), nullable=True),
sa.Column('id', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['eselon_id'], ['public.eselon.id'], name=op.f(
sa.Column('id', sa.Integer(), sa.Identity(always=True),
primary_key=True, nullable=False),
sa.ForeignKeyConstraint(['eselon_id'], ['eselon.id'], name=op.f(
'fk_jabatan_eselon_id_eselon')),
sa.PrimaryKeyConstraint('id', name=op.f('pk_jabatan')),
schema='public'
# schema='public'
)
except Exception as e:
print(str(e))
try:
op.create_table('partner_departemen',
sa.Column('partner_id', sa.Integer(), nullable=True),
sa.Column('departemen_id', sa.Integer(), nullable=True),
sa.Column('jabatan_id', sa.SmallInteger(), nullable=True),
sa.Column('mulai', sa.DateTime(), nullable=True),
sa.Column('selesai', sa.DateTime(), nullable=True),
sa.Column('id', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['departemen_id'], ['public.departemen.id'], name=op.f(
sa.Column('id', sa.Integer(), sa.Identity(always=True),
primary_key=True, nullable=False),
sa.ForeignKeyConstraint(['departemen_id'], ['departemen.id'], name=op.f(
'fk_partner_departemen_departemen_id_departemen')),
sa.ForeignKeyConstraint(['jabatan_id'], ['public.jabatan.id'], name=op.f(
sa.ForeignKeyConstraint(['jabatan_id'], ['jabatan.id'], name=op.f(
'fk_partner_departemen_jabatan_id_jabatan')),
sa.ForeignKeyConstraint(['partner_id'], ['partner.id'], name=op.f(
'fk_partner_departemen_partner_id_partner')),
......@@ -84,15 +98,18 @@ def upgrade():
'id', name=op.f('pk_partner_departemen')),
sa.UniqueConstraint('partner_id', 'departemen_id',
'jabatan_id', 'mulai', name='partner_dept_uq'),
schema='public'
# schema='public'
)
except Exception as e:
print(str(e))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('partner_departemen', schema='public')
op.drop_table('jabatan', schema='public')
op.drop_table('pangkat', schema='public')
op.drop_table('eselon', schema='public')
op.drop_table('partner_departemen',)# schema='public')
op.drop_table('jabatan', )#schema='public')
op.drop_table('pangkat', ) # schema='public')
op.drop_table('eselon', ) # schema='public')
# ### end Alembic commands ###
......@@ -30,14 +30,15 @@ def upgrade():
sa.Column('updated', sa.DateTime(), nullable=True),
sa.Column('create_uid', sa.Integer(), nullable=True),
sa.Column('update_uid', sa.Integer(), nullable=True),
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('id', sa.Integer(), sa.Identity(always=True),
primary_key=True , nullable=False),
sa.PrimaryKeyConstraint('id', name=op.f('pk_text_printers')),
schema='public'
# schema='public'
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('text_printers', schema='public')
op.drop_table('text_printers', ) #schema='public')
# ### end Alembic commands ###
from datetime import datetime, timezone
import sqlalchemy as sa
class ForceUTCDatetime(sa.TypeDecorator):
"""Ensures datetimes are timezone-aware UTC objects across both DBs."""
impl = sa.TIMESTAMP(timezone=True)
cache_ok = True
def process_bind_param(self, value, dialect):
if value is not None:
if value.tzinfo is None:
# Interpret naive datetime as UTC or handle according to business logic
value = value.replace(tzinfo=timezone.utc)
else:
# Convert any other timezone directly to UTC
value = value.astimezone(timezone.utc)
return value
def process_result_value(self, value, dialect):
if value is not None and value.tzinfo is None:
# Force the Python object to be aware if Oracle stripped the offset
return value.replace(tzinfo=timezone.utc)
return value
from .meta import *
from .common import *
from .base import *
......
......@@ -23,7 +23,8 @@ DBSession = scoped_session(session_factory)
register(DBSession)
ziggurat_foundations.models.DBSession = DBSession
TABLE_ARGS = dict(extend_existing=True, schema="public")
TABLE_ARGS = dict(extend_existing=True,)
#schema="public")
def flush(row, db_session=DBSession):
......
from sqlalchemy import (Column, Integer, ForeignKey, String, SmallInteger, text)
from sqlalchemy import (Column, Integer, ForeignKey,
String, SmallInteger, text)
from sqlalchemy.orm import (relationship, backref, declared_attr)
from ..models import DBSession, Base
from ..models import (NamaModel, TABLE_ARGS)
class _Departemen(NamaModel):
__table_args__ = (TABLE_ARGS,)
# __table_args__ = (TABLE_ARGS,)
id = Column(Integer, primary_key=True)
parent_id = Column(Integer, ForeignKey('public.departemen.id'))
# parent_id = Column(Integer, ForeignKey('public.departemen.id'))
parent_id = Column(Integer, ForeignKey('departemen.id'))
kategori = Column(String(32))
alamat = Column(String(255))
singkat = Column(String(32))
......@@ -54,57 +56,72 @@ class _Departemen(NamaModel):
parent_id = kwargs.get('id', None)
str_where = parent_id and " parent_id={} ".format(
parent_id) or " parent_id IS NULL"
eng = cls.db_session.get_bind()
if eng.dialect.name.lower() == "oracle":
sql = """
WITH RECURSIVE dep_tree AS (
SELECT
--LPAD(' ', (LEVEL - 1) * 2) || nama AS hierarchy,
SYS_CONNECT_BY_PATH(nama, '/') AS hierarchy,
id,
kode,
nama,
parent_id,
status,
0 AS level,
ARRAY[id] AS path
(LEVEL - 1) AS lvl,
SYS_CONNECT_BY_PATH(id, ',') AS path
FROM
public.departemen
WHERE
{str_where}
departemen
START WITH
parent_id IS NULL
CONNECT BY
PRIOR id = parent_id
ORDER SIBLINGS BY
id """
else:
sql = """
WITH RECURSIVE dep_tree AS (
SELECT
id,
kode,
nama::text AS nama,
parent_id,
status,
0 AS lvl,
ARRAY[id] AS path
FROM departemen
UNION ALL
SELECT
c.id,
c.kode,
c.nama,
ct.nama || '/' || c.nama::text AS nama,
c.parent_id,
c.status,
ct.level + 1,
ct.lvl + 1,
ct.path || c.id
FROM
public.departemen c
JOIN
dep_tree ct ON c.parent_id = ct.id
)
SELECT
REPEAT(' ', level) || nama AS hierarchy, -- Indent names based on level
FROM departemen c
JOIN dep_tree ct ON c.parent_id = ct.id
)
SELECT DISTINCT ON (kode)
id,
kode,
nama,
nama as hierarchy,
parent_id,
status,
level,
lvl,
path
FROM
dep_tree
""".format(str_where=str_where)
FROM dep_tree
"""
#.format(str_where=str_where)
if search:
sql = f"{sql} WHERE nama ILIKE '%{search}%' "
sql=sql+"""ORDER BY path;"""
sql = sql+"""ORDER BY kode, path """
return cls.db_session.execute(text(sql)).fetchall()
class Departemen(_Departemen, Base):
db_session = DBSession
from sqlalchemy import (Column, Integer, String, DateTime, func, )
from sqlalchemy import (Column, Integer, String, DateTime, func, Text)
from sqlalchemy.orm import (scoped_session, sessionmaker, )
from ..models.base import CommonModel
from ..models.meta import Base
......@@ -11,9 +11,9 @@ class Log(Base, CommonModel):
__tablename__ = 'logs'
id = Column(Integer, primary_key=True) # auto incrementing
line_id = Column(String(32), nullable=False, unique=True)
logger = Column(String) # the name of the logger. (e.g. myapp.views)
level = Column(String) # info, debug, or error?
trace = Column(String) # the full traceback printout
logger = Column(Text) # the name of the logger. (e.g. myapp.views)
level = Column(Text) # info, debug, or error?
trace = Column(Text) # the full traceback printout
msg = Column(String, nullable=False)
created_at = Column(
DateTime(timezone=True),
......
from datetime import datetime
import logging
from math import log
from requests import session
import pytz
import sqlalchemy as sa
from pyramid.authorization import (Allow, Authenticated, ALL_PERMISSIONS)
# from sqlalchemy import TIMESTAMP
from sqlalchemy import (
Column, Integer, DateTime, SmallInteger, String)
from sqlalchemy.orm import (relationship, backref)
......@@ -28,7 +26,7 @@ from opensipkd.tools import as_timezone
from .base import CommonModel, DBSession, DefaultModel
from .meta import Base
from .base import TABLE_ARGS
from . import ForceUTCDatetime
log = logging.getLogger(__name__)
class _GroupPermission(GroupPermissionMixin):
pass
......@@ -100,12 +98,14 @@ class _User(UserMixin, BaseModel):
registered_date = Column(DateTime(timezone=True),
nullable=False,
default=datetime.utcnow)
security_code_date = Column(DateTime(timezone=True),
default=datetime(2000, 1, 1,
tzinfo=pytz.timezone(
'Asia/Jakarta')),
server_default="2000-01-01 01:01+7",
)
# security_code_date = Column(DateTime(timezone=True),
# default=datetime(2000, 1, 1,
# tzinfo=pytz.timezone(
# 'Asia/Jakarta')),
# server_default="2000-01-01 01:01+7",
# )
# security_code_date = Column(TIMESTAMP(timezone=True))
security_code_date = Column(ForceUTCDatetime)
api_key = Column(String(256))
partner_id = Column(Integer) # , ForeignKey(Partner.id))
company_id = Column(Integer) # , ForeignKey(Partner.id))
......
user_id/users.user_name,group_id/groups.group_name
admin,Superuser
\ No newline at end of file
......@@ -5,6 +5,14 @@ import os
import subprocess
import sys
from getpass import getpass
from pyramid.paster import (get_appsettings, setup_logging, )
from sqlalchemy import (engine_from_config, select, Table, inspect)
from sqlalchemy import text
from sqlalchemy.schema import CreateSchema
from sqlalchemy.sql.sqltypes import BOOLEAN
from ziggurat_foundations.models.services.user import UserService
from opensipkd.tools import get_ext
import transaction
from ..models.users import (
......@@ -18,15 +26,6 @@ from ..models.meta import Base
# Route, Eselon, Jabatan, ResProvinsi, ResDati2, ResKecamatan, ResDesa,
# Menus, Pangkat
from pyramid.paster import (get_appsettings, setup_logging, )
from sqlalchemy import (desc, engine_from_config, select, Table, inspect)
from sqlalchemy import text
from sqlalchemy.dialects import oracle
from sqlalchemy.schema import CreateSchema
from sqlalchemy.sql.sqltypes import BOOLEAN
from ziggurat_foundations.models.services.user import UserService
from opensipkd.tools import get_ext
log = logging.getLogger(__name__)
......@@ -115,6 +114,7 @@ def restore_csv(table, filename, get_file_func=get_file, db_session=DBSession):
eng = db_session.get_bind()
q = db_session.query(table)
if q.first():
log.error("Restore discarded")
return
with get_file_func(filename) as f:
reader = csv.DictReader(f)
......@@ -135,7 +135,7 @@ def restore_csv(table, filename, get_file_func=get_file, db_session=DBSession):
raise e
fname_orig = t[0]
schema = "public"
schema = None # "public"
if t[1:]:
t_array = t[1].split('.')
if len(t_array) == 2:
......@@ -218,7 +218,7 @@ def append_csv(table, filename, keys, get_file_func=get_file,
# print(dir(table.__table__))
# print("____")
schema = hasattr(
table.__table__, "schema") and table.__table__.schema or "public"
table.__table__, "schema") and table.__table__.schema or None # "public"
columns_table = insp.get_columns(table.__tablename__, schema)
fields = {}
for c in columns_table:
......@@ -245,7 +245,7 @@ def append_csv(table, filename, keys, get_file_func=get_file,
raise e
fname_orig = t[0]
schema = "public"
schema = None # "public"
if t[1:]:
t_array = t[1].split('.')
if len(t_array) == 2:
......@@ -255,7 +255,7 @@ def append_csv(table, filename, keys, get_file_func=get_file,
schema = t_array[0]
foreign_table = t_array[1]
foreign_field = t_array[2]
log.debug("%s.%s", schema, foreign_table)
foreign_table = Table(foreign_table, base.metadata,
# autoload=True, # merubah v1.4 ke v.2
autoload_with=eng,
......@@ -286,8 +286,8 @@ def append_csv(table, filename, keys, get_file_func=get_file,
with eng.connect() as conn:
q = conn.execute(sql)
row = q.fetchone()
if not row:
raise Exception(f"Foreign key value '{value}' not found in table '{foreign_table.name}' for field '{fname}'")
# if not row:
# raise Exception(f"Foreign key value '{value}' not found in table '{foreign_table.name}' for field '{fname}'")
value = row and row.id or None
q.close()
# connection.close()
......@@ -377,8 +377,9 @@ def reset_sequence_(cls, seq):
def reset_sequences():
reset_sequence_(User, 'users_id_seq')
reset_sequence_(Group, 'groups_id_seq')
pass
# reset_sequence_(User, 'users_id_seq')
# reset_sequence_(Group, 'groups_id_seq')
def alembic_run(ini_file, name=None):
......@@ -430,6 +431,7 @@ def main(argv=sys.argv):
q = DBSession.query(User).filter_by(id=1)
user = q.first()
init_model()
if user:
password = ask_password(user.user_name)
UserService.set_password(user, password)
append_csv(Group, 'groups.csv', ['group_name'])
......
......@@ -48,6 +48,7 @@ class SearchMethods(enum.Enum):
yadcf_range_number = 10
yadcf_range_number_slider = 11
yadcf_range_date = 12
# SEARCH_METHODS = {
# 'none': lambda expr, value: None,
# 'string_contains': lambda expr, value: expr.ilike('%' + value + '%'),
......@@ -157,6 +158,17 @@ class BaseView(object):
self.action_suffix = "/grid/act"
self.html_buttons = {}
self.new_buttons = {}
"""
Additional button for list
{
"name":{
"obj": BtObject,
"js": JavaScript},
"name..."{
}
}
"""
self.list_form = None # List dam Form
self.form_list = None # Form kemudian detail list
......@@ -755,7 +767,7 @@ class BaseView(object):
for d in list_schema:
title = hasattr(d, "title") and d.title or d.name
values = hasattr(d, "widget") \
and isinstance(d.widget, (widget.SelectWidget, widget.Select2Widget)) \
and isinstance(d.widget , (widget.SelectWidget, widget.Select2Widget)) \
and d.widget.values or None
selects[d.name] = {"title": title,
......@@ -771,11 +783,12 @@ class BaseView(object):
if selects[k]["values"]:
select = isinstance(selects[k]["values"], dict) \
and selects[k]["values"].items() or dict(selects[k]["values"])
d[k] = select.get(v, v)
d[k] = select.get(v,v)
row = list(d.values())
rows.append(row)
# for i, h in enumerate(header):
# for d in list_schema:
# if d.name == h and hasattr(d, "title"):
......@@ -795,6 +808,7 @@ class BaseView(object):
# value = value.astimezone().replace(tzinfo=None)
# row[i] = value
value = {
'header': header,
'rows': rows,
......
......@@ -175,14 +175,18 @@ class Views(BaseView):
return row
def view_act(self):
request = self.req
params = request.params
url_dict = request.matchdict
if url_dict['act'] == 'grid':
query = Departemen.cte_get()
data = [{"id": d.id, "kode": d.kode, "nama": d.nama, "status": d.status,
"level_id":d.level, "parent_id": d.parent_id} for d in query]
data = [{"id": d.id, "kode": d.kode,
"nama": d.hierarchy.startswith(
'/') and d.hierarchy[1:] or d.hierarchy,
"status": d.status,
"level_id": d.lvl, "parent_id": d.parent_id} for d in query]
return {
"draw": "1",
"recordsTotal": len(data),
"recordsFiltered": len(data),
"data": data}
else:
return self.next_act()
......
......@@ -759,6 +759,8 @@ def reset_password_validator(form, value):
def security_code_age(user):
now = create_now()
if user.security_code_date:
if not user.security_code_date.tzinfo:
return now - user.security_code_date.replace(tzinfo=now.tzinfo)
return now - user.security_code_date
return timedelta(minutes=1)
......
from pyramid.csrf import new_csrf_token, get_csrf_token
from iso8601.iso8601 import ISO8601_REGEX
# from deform.widget import string_types
# from deform.widget import str
import json
import logging
from pyramid.csrf import new_csrf_token, get_csrf_token
from iso8601.iso8601 import ISO8601_REGEX
from colander import SchemaNode, null, Mapping, Invalid # , string_types
from colander import SchemaNode, null, Mapping, Invalid # , str
# from colander import compat # tidak ada di colander 2.0
from deform import widget
# from deform.compat import sequence_types, text_type, text_
......@@ -16,7 +19,7 @@ from deform.widget import (
from opensipkd.tools.captcha import img_captcha
_logging = logging.getLogger(__name__)
sequence_types = (list, range, tuple)
class DokumenWidget(Widget):
template = "opensipkd.base:/widgets/templates/dokumen.pt"
readonly_template = "opensipkd.base:/widgets/templates/readonly/dokumen.pt"
......@@ -285,7 +288,7 @@ class AutocompleteMsInputWidget(AutocompleteInputWidget):
try:
validated = self._pstruct_schema.deserialize(pstruct)
except Invalid as exc:
raise Invalid(field.schema, text_("Invalid pstruct: %s" % exc))
raise Invalid(field.schema, "Invalid pstruct: %s" % exc)
auto_id = validated["auto_id"]
auto_value = validated["auto_value"]
......@@ -893,10 +896,10 @@ class FilterWidget(Widget):
"""
if self.multiple:
if value in map(text_type, cstruct):
if value in map(str, cstruct): # text_type
return "selected"
else:
if value == text_type(cstruct):
if value == str(cstruct): #text_type
return "selected"
return None
......
......@@ -223,7 +223,7 @@ class DeTable(field.Field):
_scripts = []
# buttons = Params Buttons
for button in buttons:
if isinstance(button, str):
if isinstance(button, str): #compat.string_types
button = Button(button)
obj_buttons.append(button)
header_buttons = []
......
......@@ -15,7 +15,7 @@ show_error_codes = true
[project]
name = 'opensipkd_base'
version = '5.0.1'
version = '5.0.4'
dependencies = [
'deform',
'colander',
......@@ -40,6 +40,7 @@ dependencies = [
'zope.sqlalchemy',
'websockets',
"asyncio",
"pdfkit",
]
requires-python = '>= 3.8'
authors = [
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!