[IMPROVED] Agregar packs devs al ambiente de desarrollo odoo

This commit is contained in:
Mauro Rosero P. 2025-01-27 10:41:26 -05:00
parent c522ebad8f
commit d04ef587dc
Signed by: mrosero
GPG key ID: 83BD2A5F674B7E26
268 changed files with 45303 additions and 0 deletions

2
.gitignore vendored
View file

@ -9,6 +9,7 @@
!bin/
!bin/config/
!packs
!packs/devs/
# Permitir el seguimiento de archivos específicos
# !archivo1.txt
@ -20,3 +21,4 @@
# !carpeta1/*.txt
!bin/*
!bin/config/*
!packs/devs/**

View file

@ -0,0 +1,41 @@
Odoo Code🔨
==================
* Odoo Code🔨 help you build Odoo application onlione, super speedup development process🚀
============
Features
==========
✍️ View & Write Odoo code through Web Code Editor in the same Odoo runtime environment;
☕️ Hot reload, quickly and easily experiment, build UIs, add features, fix bugs & more;
👉 Locate the Templates Script source code quickly, save time & speed up development process;
⚡︎ Write Odoo code anytime anywhere, free mind & happy coding!
Warning
==========
⚠️ Only use for development phase! Production Environment is not recommend❗
Team
---------
ixkit.com
Artificer@ixkit.com
Maintainer
---------
This module was maintained by ixkit Team.
For support and more information, please visit https://www.ixkit.com/odookit
Further information
---------
HTML Description: `<static/description/index.html>`
Github
---------
https://www.github.com/ixkit/odookit

View file

@ -0,0 +1,4 @@
{
}

View file

@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
import os
import sys
from . import land
from . import controllers
from . import models
from .__support__ import *
from .land import config
from .land.trace import Tracer
from .hooks import module_pre_init_hook, module_post_init_hook,module_uninstall_hook
def main_init():
config_data = config.load(get_config_file())
#-- main ---
main_init()

View file

@ -0,0 +1,71 @@
# -*- coding: utf-8 -*-
#
# ixkit - odoo code🔨
#
#@team : ixkit
#@author : Artificer@ixkit.com
#@date : 2024-4-25
#@version : 1.1.0
#
#-----------------------------------------------------------------------
{
"name": "Odoo Code🔨",
"version": "18.0",
"module_version": "1.1.0",
"summary": """
Help you build Odoo application online, super speed development process🚀
""",
"description":"Odoo Code🔨 build application in new way!",
'category': 'Extra Tools/ixkit',
'author': 'ixkit',
'company': 'ixkit',
'maintainer': 'ixkit',
'website': "http://www.ixkit.com/odookit",
"live_test_url": "http://www.ixkit.com/odookit",
"license": "OPL-1",
"support": "odoo@ixkit.com",
"depends": [
'base', 'web'
],
"data": [
'security/ir.model.access.csv',
'views/code_server/forms.xml',
'views/code_server/lists.xml',
'views/code_server/actions.xml',
'views/code_server/menus.xml',
'views/console/console.xml',
'views/page/web.xml',
'views/page/effect.xml',
],
'assets': {
'web.assets_backend': [
'kit_code/static/src/xml/code_dashboard.xml',
'kit_code/static/src/js/code_dashboard.js',
'kit_code/static/src/css/code_dashboard.css',
'kit_code/static/image/**/*',
'kit_code/static/jslib/termynal/**/*',
'kit_code/static/jslib/jsontable/**/*',
'kit_code/static/src/js/effect.js',
],
'web.assets_frontend': [
],
},
'images': [
'static/description/banner.png',
],
'pre_init_hook': 'module_pre_init_hook',
'post_init_hook': 'module_post_init_hook',
'uninstall_hook': 'module_uninstall_hook',
'installable': True,
'application': True,
'auto_install': False,
}

View file

@ -0,0 +1,57 @@
# -*- coding: utf-8 -*-
import os
import sys
from .land import config
from .land.trace import Tracer
def get_config_file():
my_dir = os.path.dirname(os.path.abspath(__file__))
return my_dir + "/__config__.py"
def get_module_path():
my_dir = os.path.dirname(os.path.abspath(__file__))
return my_dir
def get_mainifset_file():
my_dir = os.path.dirname(os.path.abspath(__file__))
return my_dir + "/__manifest__.py"
def get_mainifset():
mainifset_data = config.load(get_mainifset_file())
return mainifset_data
def get_mainifset_version():
mainifset_data = get_mainifset()
version = mainifset_data.get('version')
first_dot_index = version.index(".")
big_ver = version[0:first_dot_index]
return big_ver
#TODO
def get_support_major_version():
return '17'
def setup_tracer(level=None):
trace_level = level
while trace_level is None:
runtime_options = sys.modules['runtime_options']
if runtime_options is None:
break
trace_level = runtime_options.config.get('trace_level')
if trace_level is None:
break
break
Tracer.set_trace_level(trace_level)
#--
setup_tracer(False)
#setup_tracer('DEBUG')

View file

@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
from . import main
from . import intent

View file

@ -0,0 +1,38 @@
# -*- coding: utf-8 -*-
"""
"url":"/web/static/src/core/dropdown/dropdown.xml",
"file":"/odoo-17/odoo/addons/web/static/src/core/dropdown/dropdown.xml"
=>
dir: /odoo-17/odoo/addons/web
file: static/src/core/dropdown/dropdown.xml
"""
def to_dir_file(url,full_file_name):
dir = full_file_name.replace(url,'')
list = url.split('/' );
module_name = list[1];
dir = dir + "/" + module_name
r_file_name = None
for i in range(2,len(list)):
if r_file_name is None:
r_file_name = list[i]
else:
r_file_name = r_file_name + "/" + list[i]
return dir, r_file_name
def _test():
url = "/web/static/src/core/dropdown/dropdown.xml"
file ="/odoo-17/odoo/addons/web/static/src/core/dropdown/dropdown.xml"
#dir: /odoo-17/odoo/addons/web
#file: static/src/core/dropdown/dropdown.xml
dir, sfile = to_dir_file(url,file)
print("\r\n-----------\r\n")
print("dir:" +dir)
print("file:" + sfile)
if __name__ == '__main__':
_test()

View file

@ -0,0 +1,180 @@
# -*- coding: utf-8 -*-
import json
import logging
from odoo import http
from odoo.tools import date_utils
from odoo.http import request
from odoo import api, SUPERUSER_ID
from werkzeug.wrappers import Request, Response
from urllib.parse import quote
from ..land.web import rpc_result
from .intent import to_dir_file
_logger = logging.getLogger(__name__)
def kws2Dict(**kwargs):
result = {}
# Iterating over the Python kwargs dictionary
for key, value in kwargs.items():
if key.startswith('amp;'):
key = key.replace("amp;",'')
result [key]= value
return result
def dictToQueryParamsStr(dict):
result = ""
for key, value in dict.items():
result = result + "&{}={}".format(key,value)
return result
ROOT = "/code🔨"
def entry(val=None):
if val is None:
return ROOT
return "{}{}".format(ROOT,val)
class Main(http.Controller):
@http.route(entry('/api/ping') , auth='user', type='json',website=True )
def code_server_ping(self, **kwargs):
headers = {'Content-Type': 'application/json'}
body = rpc_result.success()
return body #Response(json.dumps(body), headers=headers)
"""
"name:"web.Dropdown",
"url":"/web/static/src/core/dropdown/dropdown.xml",
"file":"/odoo-17/odoo/addons/web/static/src/core/dropdown/dropdown.xml"
"""
@http.route(entry(), auth='user', website=True )
def code_server(self, **kwargs):
params = kws2Dict(**kwargs)
_logger.debug('/code🔨, input params =%s',params)
action = params.get('action')
if action == 'start':
return self.start_then_reload_current_page(params)
#@case
next_url = params.get('nextUrl')
_logger.debug('/code🔨, next_url=%s',next_url)
if not next_url is None:
started = params.get('started')
module_name = params.get('moduleName')
return self.open_file(next_url,module_name,started)
#@case
url = params.get('url')
file_full_path = params.get('file')
name = params.get('name')
try:
dir,file = to_dir_file(url,file_full_path)
except Exception as ex:
render_values = {
'error': 'invalidate parameters'
}
return self.render_self_home_page(render_values)
params ={
'dir' : dir,
'file':file,
'keywords': name
}
# params_str = urlencode(params)
# _logger.debug('🧐 try ->open_file, urlencode params_str:%s',params_str)
"""
http://localhost:3030/folder?dir={}&file={}&keywords={}
"""
# next_url = 'http://localhost:3030/folder?{}'.format(params_str)
# _logger.debug('🧐 try ->open_file, next_url:%s',next_url)
next_url = "http://localhost:3030/folder?dir={}&file={}&keywords={}".format(dir,file,name)
_logger.debug('🧐 try ->open_file, next_url:%s',next_url)
next_url = quote(next_url)
module_name = name
return self.open_file(next_url,module_name)
def open_file(self,next_url,module_name,started=False):
_logger.debug('entry->open_file, next_url:%s,',next_url)
code_server = self.get_code_server()
if not code_server.get('status') == 'running':
if started:
code_server['status'] = 'running'
code_server['loading'] = True
render_values = { 'nextUrl':next_url,
'codeServer': code_server,
'moduleName': module_name,
}
_logger.debug('->open_file, render_values:%s',render_values)
return self.render_self_home_page(render_values,True)
def render_self_home_page(self,render_values,need_start=True):
if need_start:
next_url = render_values.get('nextUrl')
start_url = entry("?action=start&nextUrl={}".format(next_url))
render_values['startUrl'] = start_url
return request.render('kit_code.home_page',render_values )
def get_code_server(self):
server_info = request.env['kit.code.server'].fetch_server_info()
"""
{
"success":
"data":{
url:
}
}
"""
if server_info is None or server_info.get('success') is False :
return {
'status':'ready'
}
#info = server_info.get('data')
result = {
'status': 'running',
# 'url' : info.url,
'detail': server_info
}
return result
def start_then_reload_current_page(self, params):
_logger.debug('->start_then_reload_current_page, params:%s',params)
if not params.get("start") is None:
del params['start']
codeServer = request.env['kit.code.server']
result = codeServer.ctrl_server('start')
if result and result.get('success'):
next_url = params.get('nextUrl')
file = params.get('file')
keywords = params.get('keywords')
if file and not file in next_url:
next_url = "{}&file={}&keywords={}".format(file,keywords)
next_url = quote(next_url)
url = entry("?started=true&nextUrl={}".format(next_url))
return request.redirect(url)
else:
# failed start the reback to original state
paramsStr = dictToQueryParamsStr(params)
url = entry("?{}".format(paramsStr))
return request.redirect(url)

View file

@ -0,0 +1,18 @@
# -*- coding: utf-8 -*-
import sys
def module_pre_init_hook(env):
pass
def module_post_init_hook(env):
pass
def module_uninstall_hook(env):
pass

View file

@ -0,0 +1,7 @@
# -*- coding: utf-8 -*-
from . import lang
from . import trace
from . import jsons
def smart_json(val):
jsons.SmartJson(val)

View file

@ -0,0 +1,8 @@
# -*- coding: utf-8 -*-
import os
from ..jsons import load_py_json
def load(path):
return load_py_json(path)

View file

@ -0,0 +1,50 @@
# -*- coding: utf-8 -*-
import json
from .__smart_json__ import SmartJson
# remove comment
def load_py_to_json_str(file_name):
buf = None
with open(file_name, 'r') as file:
Lines = file.readlines()
for line in Lines:
if line.startswith("#"):
continue
else:
if buf is None:
buf = line
else:
buf = buf + line
return buf
def load_py_json(file_name):
buf = load_py_to_json_str(file_name)
if not buf is None:
data = json.loads(buf)
return data
return None
def load_json(file_name):
with open(file_name, 'r') as fcc_file:
data = json.load(fcc_file)
return data
return None
def to_json(obj):
data = json.load(obj)
return data
def set_to_list(obj):
if isinstance(obj, set):
return list(obj)
raise TypeError(f"Object of type {type(obj)} is not JSON serializable")
def json_dumps(obj):
obj
json_str = json.dumps(obj, default=set_to_list)
return json_str

View file

@ -0,0 +1,368 @@
"""
Author : J. Koffi ONIPOH
Version : 2.0.2
email: jolli644@gmail.com
"""
import datetime
import json
import os
from collections import OrderedDict
from copy import deepcopy
class SmartJson:
def __init__(self, cls=None):
"""
:param cls:
"""
self.__copy = cls
self.__classe = deepcopy(cls)
self.___obj = None
if cls:
self.classname = cls.__class__.__name__
def serialize(self, pretty=True):
"""
:param pretty:
:return:
"""
try:
if isinstance(self.__classe, dict):
return SmartJson._DictConversion(self.__classe).serialize(pretty)
elif isinstance(self.__classe, list):
return SmartJson._ListConversion(self.__classe).serialize(pretty)
elif isinstance(self.__classe, (int, float, bool, str, type(None))):
if pretty:
return json.dumps(self.__classe, indent=2, sort_keys=True)
else:
return json.dumps(self.__classe, sort_keys=True)
elif isinstance(self.__classe, (tuple, complex, datetime.date, datetime.datetime, OrderedDict)):
if pretty:
return json.dumps(self._JsonConvert().json_convert(self.__classe), indent=2, sort_keys=True)
else:
return json.dumps(self._JsonConvert().json_convert(self.__classe), sort_keys=True)
elif isinstance(self.__classe, object):
if SmartJson._JsonConvert().get_class_name(self.__classe) == "enum.EnumMeta":
return SmartJson._EnumConversion(self.__classe).serialize(pretty)
else:
self.___obj = SmartJson._DataTypeConversion(self.__classe).next()
if pretty:
return json.dumps({'' + self.classname: self._serialize(self.___obj).__dict__}, indent=2,
sort_keys=True)
else:
return json.dumps({'' + self.classname: self._serialize(self.___obj).__dict__}, sort_keys=True)
except TypeError as e:
SmartJson._UnsupportedClass((type(self.___obj).__name__), e)
def serializeToJsonFile(self, directory="output", filename="smart.json"):
"""
:param pretty:
:return:
"""
try:
os.makedirs(directory)
except OSError:
if not os.path.isdir(directory):
raise
try:
if isinstance(self.__classe, (
int, float, bool, str, type(None), dict, tuple, complex, datetime.date, datetime.datetime,
OrderedDict)) or SmartJson._JsonConvert().get_class_name(self.__classe) == "enum.EnumMeta":
with open(directory + "/" + filename, 'w') as outfile:
json.dump(json.loads(self.serialize(pretty=True)), outfile, indent=2, sort_keys=True)
elif isinstance(self.__classe, object):
self.___obj = SmartJson._DataTypeConversion(self.__classe).next()
if filename != "smart.json":
with open(directory + "/" + filename, 'w') as outfile:
json.dump(json.loads(
json.dumps({'' + self.classname: self._serialize(self.___obj).__dict__}, indent=2,
sort_keys=True)), outfile, indent=2, sort_keys=True)
else:
with open(directory + "/" + self.classname + ".json", 'w') as outfile:
json.dump(json.loads(
json.dumps({'' + self.classname: self._serialize(self.___obj).__dict__}, indent=2,
sort_keys=True)), outfile, indent=2, sort_keys=True)
except TypeError as e:
SmartJson._UnsupportedClass((type(self.___obj).__name__), e)
def toObjectFromFile(self, jsonFile):
"""
:param jsonFile:
:param obj_name:
:return:
"""
with open(jsonFile) as outfile:
dic = json.load(outfile)
return SmartJson._KObject(dic)
def toObject(self, _json):
"""
:param _json:
:param obj_name:
:return:
"""
dic = None
if isinstance(_json, str):
dic = json.loads(_json)
elif isinstance(_json, dict):
dic = _json
return SmartJson._KObject(dic)
def getClass(self):
return self.__copy
def _serialize(self, obj):
for attr, value in vars(obj).items():
if hasattr(value, "__class__"):
if isinstance(value, (
int, float, bool, complex, list, tuple, str, OrderedDict, dict,
datetime.datetime, datetime.date, bytes, type(None))):
continue
elif SmartJson._JsonConvert().get_class_name(value) == "builtins.dict":
continue
else:
obj.__setattr__(attr, value.__dict__)
self._serialize(value)
return obj
class _DataTypeConversion:
def __init__(self, cls):
self.___cls = cls
self._json_cvt = SmartJson._JsonConvert()
def next(self):
try:
return self.__next(self.___cls)
except TypeError as e:
SmartJson._UnsupportedClass((type(self.___cls).__name__), e)
def __next(self, cls):
for attr, value in vars(cls).items():
if hasattr(value, "__class__"):
if isinstance(value, (datetime.datetime, datetime.date, complex)):
cls.__setattr__(attr, self._json_cvt.json_convert(value))
elif isinstance(value, (int, float, bool, str)):
continue
elif isinstance(value, (list, tuple)):
cls.__setattr__(attr, list((self._json_cvt.json_convert(v) for v in value)))
elif self._json_cvt.get_class_name(value) == "collections.deque":
cls.__setattr__(attr, self._json_cvt.json_convert(OrderedDict(value)))
elif self._json_cvt.get_class_name(value) == "enum.EnumMeta":
cls.__setattr__(attr, [SmartJson._EnumConversion(value).convert()])
elif isinstance(value, type(None)):
cls.__setattr__(attr, "")
elif isinstance(value, bytes):
cls.__setattr__(attr, value.decode("utf-8"))
elif isinstance(value, dict):
obj = [SmartJson._DictConversion(value).convert()]
cls.__setattr__(attr, obj)
else:
self.__next(value)
return cls
class _UnsupportedClass(Exception):
def __init__(self, message, errors):
# Call the base class constructor with the parameters it needs
super().__init__(message)
# Now for your custom code...
self.errors = errors
print("Error : 228 , UnsupportedClass : %s " % (message), errors)
class _KObject(object):
def __init__(self, d):
for a, b in d.items():
if isinstance(b, (list, tuple)):
setattr(self, a, [self.__class__(x) if isinstance(x, dict) else x for x in b])
elif isinstance(b, str):
try:
setattr(self, a, datetime.datetime.strptime(b, "%Y-%m-%d %H:%M:%S.%f"))
except ValueError:
setattr(self, a, b)
else:
setattr(self, a, self.__class__(b) if isinstance(b, dict) else b)
class _EnumConversion:
def __init__(self, myEnum):
self.__myEnum = deepcopy(myEnum)
self._json_cvt = SmartJson._JsonConvert()
def serialize(self, pretty):
if pretty:
return json.dumps(self.convert(), indent=2, sort_keys=True)
else:
return json.dumps(self.convert())
def convert(self):
if self._json_cvt.get_class_name(self.__myEnum) == "enum.EnumMeta":
converts = {}
for attr, value in vars(self.__myEnum).items():
if "_member_names_" == attr:
for member in value:
# var = self.__myEnum.__getattribute__(self.__myEnum, member).value
converts[member] = self.__myEnum[member].value
return SmartJson._DictConversion(converts).convert()
else:
SmartJson._UnsupportedClass((type(self.__myEnum).__name__), "This type of enum not support")
class _ListConversion:
def __init__(self, myList):
self.__myList = deepcopy(myList)
self._json_cvt = SmartJson._JsonConvert()
def serialize(self, pretty):
if pretty:
return json.dumps(self.convert(), indent=2, sort_keys=True)
else:
return json.dumps(self.convert())
def convert(self):
convert_result = []
for attr in self.__myList:
if isinstance(attr, (datetime.date, datetime.datetime)):
convert_result.append(str(attr))
elif isinstance(attr, bytes):
convert_result.append(attr.decode("utf-8"))
elif isinstance(attr, (int, float, bool, str, type(None))):
convert_result.append(attr)
elif isinstance(attr, (list, tuple, set)):
convert_result.append([self._json_cvt.json_convert(item) for item in attr])
elif isinstance(attr, OrderedDict):
convert_result.append(self._json_cvt.json_convert(attr))
elif isinstance(attr, complex):
convert_result.append(self._json_cvt.json_convert(attr))
elif isinstance(attr, dict):
obj = [SmartJson._DictConversion(attr).convert()]
convert_result.append(obj)
elif self._json_cvt.get_class_name(attr) == "collections.deque":
convert_result.append(self._json_cvt.json_convert(OrderedDict(attr)))
elif self._json_cvt.get_class_name(attr) == "enum.EnumMeta":
convert_result.append([SmartJson._EnumConversion(attr).convert()])
elif hasattr(attr, "__class__"):
if isinstance(attr, (int, float, bool, str, type(None))):
convert_result.append(attr)
elif isinstance(attr, (list, tuple, set)):
convert_result.append({[self._json_cvt.json_convert(item) for item in attr]})
else:
cls = attr
serialize = SmartJson._DataTypeConversion(cls).next().__dict__
convert_result.append(serialize)
return convert_result
class _DictConversion:
def __init__(self, dict):
self.__dict = deepcopy(dict)
self._json_cvt = SmartJson._JsonConvert()
def serialize(self, pretty):
if pretty:
return json.dumps(self.convert(), indent=2, sort_keys=True)
else:
return json.dumps(self.convert())
def convert(self):
for attr in self.__dict:
if isinstance(self.__dict[attr], (datetime.date, datetime.datetime)):
self.__dict[attr] = str(self.__dict[attr])
elif isinstance(self.__dict[attr], bytes):
self.__dict[attr] = self.__dict[attr].decode("utf-8")
elif isinstance(self.__dict[attr], (int, float, bool, str, type(None))):
continue
elif isinstance(self.__dict[attr], (list, tuple, set)):
self.__dict[attr] = [self._json_cvt.json_convert(item) for item in self.__dict[attr]]
elif isinstance(self.__dict[attr], OrderedDict):
self.__dict[attr] = self._json_cvt.json_convert(self.__dict[attr])
elif isinstance(self.__dict[attr], complex):
self.__dict[attr] = self._json_cvt.json_convert(self.__dict[attr])
elif self._json_cvt.get_class_name(self.__dict[attr]) == "collections.deque":
self.__dict[attr] = self._json_cvt.json_convert(OrderedDict(self.__dict[attr]))
elif self._json_cvt.get_class_name(self.__dict[attr]) == "enum.EnumMeta":
self.__dict[attr] = [SmartJson._EnumConversion(self.__dict[attr]).convert()]
elif hasattr(self.__dict[attr], "__class__"):
if isinstance(self.__dict[attr], (int, float, bool, str, type(None))):
continue
elif isinstance(self.__dict[attr], (list, tuple, set)):
self.__dict[attr] = {[self._json_cvt.json_convert(item) for item in self.__dict[attr]]}
elif isinstance(self.__dict[attr], dict):
cls = self.__dict[attr]
serialize = SmartJson._DictConversion(cls).convert()
self.__dict[attr] = serialize
else:
cls = self.__dict[attr]
serialize = SmartJson._DataTypeConversion(cls).next().__dict__
self.__dict[attr] = serialize
return self.__dict
class _JsonConvert:
self_dumpers = dict()
self_loaders = dict()
def self_dump(self, obj):
class_name = self.get_class_name(obj)
if class_name in self.self_dumpers:
return self.self_dumpers[class_name](self, obj)
raise TypeError("%r is not JSON serializable" % obj)
def json_convert(self, obj):
if isinstance(obj, OrderedDict):
try:
return self.self_dump(obj)
except TypeError:
return {k: self.json_convert(v) for k, v in self.iter_items(obj)}
# nested dict
elif isinstance(obj, dict):
return {k: self.json_convert(v) for k, v in self.iter_items(obj)}
# list or tuple
elif isinstance(obj, (list, tuple)):
return list((self.json_convert(v) for v in obj))
elif isinstance(obj, (datetime.datetime, datetime.date)):
return str(obj)
elif isinstance(obj, complex):
return [{'expression': str(obj), 'real': obj.real, 'imag': obj.imag}]
elif hasattr(obj, "__class__"):
if isinstance(obj, (int, float, bool, str, type(None))):
pass
elif isinstance(obj, complex):
return [{'expression': str(obj), 'real': obj.real, 'imag': obj.imag}]
else:
return SmartJson._DataTypeConversion(obj).next().__dict__
# single object
try:
return self.self_dump(obj)
except TypeError:
return obj
def iter_items(self, d, **kw):
return iter(d.items(**kw))
def get_class_name(self, obj):
return obj.__class__.__module__ + "." + obj.__class__.__name__

View file

@ -0,0 +1,121 @@
# -*- coding: utf-8 -*-
from . import pattern
from . import timeout
import json
import html
from ...land.trace import Tracer
_logger = Tracer
def is_primitive(x):
if isinstance(x, (int, float, bool, str, type(None))):
return True
else:
return False
def model_to_print_data(obj):
fields_dict = {}
for key in obj.fields_get():
val = obj[key]
if (is_primitive(val)):
val = str(val)
else:
try:
# val = model_to_print_data(val)
val = str(val)
except Exception as ex:
print(ex)
val = str(val)
finally:
pass
fields_dict[key] = val
return fields_dict
def to_str(val):
try:
return json.dumps(val)
except Exception as ex:
print(ex)
pass
finally:
return str(val)
def _format_template_code(value):
_logger.debug("👀->👽 ._format_template_code,template_code:{},", value)
if not value:
return ""
if value:
buf = '<t t-name="web_editor.snippet_options_image_optimization_widgets"><t t-set="filter_label">Filter</t>'
buf = value
buf = buf.replace("\"", "&?quote") # avoid json dump issue in js
buf = html.escape(buf, True)
return buf
"""
UIView
"""
def view_as_dict(view,deep=True):
_logger.debug("👀->👽 try view_as_dict, view:{},", view)
if not view or not view.id:
return None
result = {
"id": view.id,
"name": html.escape(view.name,True),
"model": view.model,
"key": view.key,
"type": view.type,
"mode": view.mode,
"arch_fs": view.arch_fs,
# "arch_db": view.arch_db, # should use this one ?
}
if not deep:
return result
# if view.arch_prev :
# result["arch_prev_formatted"] = _format_template_code(view.arch_prev)
# if view.arch :
# result["arch"] = _format_template_code(view.arch)
# step
parent_dict = None
if not view.inherit_id.id is None:
parent_dict = view_as_dict(view.inherit_id,deep=False)
if not parent_dict is None:
result["parent"] = parent_dict
# step
children_views = None
if not view.inherit_children_ids is None:
children_views = []
for x in view.inherit_children_ids:
item = view_as_dict(x,deep=False)
children_views.append(item)
if not children_views is None:
# import time
# mock_item = {
# "id": "111-" + str(time.time()),
# "name": "xxx",
# }
# children_views.append(mock_item)
result["children_views"] = children_views
_logger.debug("👀->👽 view_as_dict, result:{},", result)
return result

View file

@ -0,0 +1,11 @@
# -*- coding: utf-8 -*-
def singleton(cls):
_instance = {}
def inner():
if cls not in _instance:
_instance[cls] = cls()
return _instance[cls]
return inner

View file

@ -0,0 +1,81 @@
# -*- coding: utf-8 -*-
"""
clone from https://github.com/johejo/inputimeout
"""
import sys
DEFAULT_TIMEOUT = 30.0
INTERVAL = 0.05
SP = ' '
CR = '\r'
LF = '\n'
CRLF = CR + LF
class TimeoutOccurred(Exception):
pass
def echo(string):
sys.stdout.write(string)
sys.stdout.flush()
def posix_inputimeout(prompt='', timeout=DEFAULT_TIMEOUT):
echo(prompt)
sel = selectors.DefaultSelector()
sel.register(sys.stdin, selectors.EVENT_READ)
events = sel.select(timeout)
if events:
key, _ = events[0]
return key.fileobj.readline().rstrip(LF)
else:
echo(LF)
termios.tcflush(sys.stdin, termios.TCIFLUSH)
raise TimeoutOccurred
def win_inputimeout(prompt='', timeout=DEFAULT_TIMEOUT):
echo(prompt)
begin = time.monotonic()
end = begin + timeout
line = ''
while time.monotonic() < end:
if msvcrt.kbhit():
c = msvcrt.getwche()
if c in (CR, LF):
echo(CRLF)
return line
if c == '\003':
raise KeyboardInterrupt
if c == '\b':
line = line[:-1]
cover = SP * len(prompt + line + SP)
echo(''.join([CR, cover, CR, prompt, line]))
else:
line += c
time.sleep(INTERVAL)
echo(CRLF)
raise TimeoutOccurred
inputimeout = None
try:
import msvcrt
except ImportError:
import selectors
import termios
inputimeout = posix_inputimeout
else:
import time
inputimeout = win_inputimeout

View file

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
from .tracer import Tracer

View file

@ -0,0 +1,118 @@
# -*- coding: utf-8 -*-
#
# ixkit - odoo spy
#
#@purpose : logger as Tracer with step feature
#@author : Artificer@ixkit.com
#@date : 2024-2-15
#@version : 1.0.0
#
#-----------------------------------------------------------------------
import sys
import inspect
from ...land import jsons
from ..lang.timeout import inputimeout
from string import Template
from string import Formatter as StrFormatter
def _wait_input(prompt,timeout):
result = None
try:
result = inputimeout(prompt=prompt, timeout=timeout)
except Exception as ex:
pass
finally:
return result
def _smart_json(val):
return jsons.SmartJson(val)
def to_dic(obj):
dic = {}
for fieldkey in dir(obj):
fieldvaule = getattr(obj, fieldkey)
if not fieldkey.startswith("__") and not callable(fieldvaule) and not fieldkey.startswith("_"):
dic[fieldkey] = fieldvaule
return dic
#---------------------------------------------------------------------------
# Tracer classes and functions
#---------------------------------------------------------------------------
class Tracer():
trace_level = True # True|False, 'None','DEBUG'|'INFO'...
def __init__(self ):
pass
@staticmethod
def set_trace_level(level=None):
Tracer.trace_level = level
@staticmethod
def get_trace_level():
if Tracer.trace_level is None:
return None
if Tracer.trace_level == "None":
return None
if Tracer.trace_level == False or Tracer.trace_level == "False":
return None
return Tracer.trace_level
@staticmethod
def smart_json(val):
return to_dic(val)
"""
# named
msg = "xxxx {age} xxxx {name}".format(age=18, name="hangman")
print(msg) # xxxx 18 xxxx hangman
# order
msg = "xxxx {1} xxx{0}".format(value1,value2)
print(msg) # xxxx [9, 0] xxx(7, 8)
# mix
msg = "xxxx {} XXX {name} xxx {}".format(value2,value1,name="s4")
print(msg) # xxxx [9, 0] XXX s4 xxx (7, 8)
"""
@staticmethod
def debug( msg, *args, sender=None, step=None, wait_second=-1, **kwargs ):
trace_level = Tracer.get_trace_level()
if trace_level is None:
return
line = "line?"
if sender is None:
stack_frame = inspect.stack()[1]
stack_module = inspect.getmodule(stack_frame[0])
sender = stack_module.__name__
line = inspect.getlineno(stack_frame[0])
Tracer._log(sender,line, msg, *args, **kwargs)
if step == True :
prompt = "💡" + msg +' 👉 press any key to continue...'
if wait_second > 0 :
# wait limit time for input using inputimeout() function
prompt = prompt + " wait second:" + str(wait_second) + "\r\n"
input_val = _wait_input(prompt=prompt, timeout=wait_second)
if not input_val is None:
print(input_val)
else:
prompt = prompt + "\r\n"
input_val = str(input(prompt))
print(input_val)
@staticmethod
def _log(sender,line, msg, /, *args, **kwargs):
prefix = "{},{}:".format(sender,line)
buf = msg.format(*args, **kwargs)
buf = prefix + buf
print("🔍->" + buf)

View file

@ -0,0 +1,17 @@
# -*- coding: utf-8 -*-
import time
def as_result(success, data=None):
result = {
"success": success,
"ts": int(time.time())
}
if not data is None:
result['data'] = data
return result
def success(data = None):
return as_result(True,data)
def error(data = None):
return as_result(False,data)

View file

@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
from . import ir_http
from . import code_server
from . import notifyer

View file

@ -0,0 +1,431 @@
#-*- coding: utf-8 -*-
import logging
import threading
import time
import json
import os
import re
import sys
import html
from array import array
import requests
import odoo
from odoo import models, fields, api, tools
from odoo.modules.module import get_manifest
from os.path import join as opj
from datetime import datetime
from .server_master import ServerMaster
from ..land.jsons import json_dumps
from ..land.lang.pattern import singleton
from ..land.web import rpc_result
from ..shell.sheller import async_run
from ..__support__ import get_module_path
__author__ = odoo.release.author
__version__ = odoo.release.version
_logger = logging.getLogger(__name__)
def simple_manifest(manifest):
result ={}
try:
keys = ["name","version","description","summary","author","website","license","category","depends","application"]
#keys = ["name","version","category","description"]
for x in keys:
val = manifest.get(x)
if not val is None:
result[x] = val
# buf = json_dumps(result)
# buf = html.escape(buf)
# _logger.debug(" simple_manifest, buf:%s",buf)
# return buf
except Exception as ex:
_logger.error("Failed simple_manifest, ex:%s",ex)
pass
return result
def concise_folder(folder):
result ={
'name': folder["name"],
"path": folder["path"],
}
data = folder.get('data')
if not data is None:
result["data"] = data
return result
@singleton
class CodeServerCahce():
_cacheSet = {}
def get(self, key):
return self._cacheSet.get(key)
def set(self, key, val):
self._cacheSet[key] = val
# ready to use
theCache = CodeServerCahce()
#
class CodeServer(models.Model):
_name = 'kit.code.server'
_description = 'kit code server'
name = fields.Char(default="",required=True)
ts = fields.Datetime( string="Time",required=True)
message = fields.Text( string="Message")
message_type = fields.Char()
_order = "ts desc"
def on_echo(self,**kw):
message = kw['message']
if message is None or message == "":
return
_logger.debug("on_echo:%s",message)
value ={
"message":json.dumps(message),
"ts": datetime.now(),
}
# if self.env.user._is_public():
#self.ensure_one()
#self.env['kit.code.server'].sudo().create(value)
with self.pool.cursor() as new_cr:
self = self.with_env(self.env(cr=new_cr))
self.env['kit.code.server'].sudo().create(value)
self.env["bus.bus"].push( value)
def write(self, vals):
write_res = super(CodeServer, self).write(vals)
return write_res
def _echo_server_op(self,op):
if "start" == op:
op = "🚀 Start"
if "stop" == op:
op = "❌ Stop"
msg = {"stdout":[op + " Code Server..."]}
self.on_echo(message = msg)
def _get_server_handler(self):
return theCache.get('server_handler')
def _set_server_handler(self,val):
theCache.set('server_handler',val)
# op = 'start'|'stop'
@api.model
def ctrl_server(self,op):
#raise Warning(('Test action!'))
args = {
"on_echo": self.on_echo
}
result = rpc_result.error( {'msg':"Invalidate operation:{}".format(op)})
success = None
while True:
if "start" == op:
server_handler = ServerMaster.startServer(args)
success = not server_handler is None
if success:
self._set_server_handler(server_handler)
#self._start_submit_space_info()
result = rpc_result.success(server_handler)
break
if "stop" == op:
success = ServerMaster.stopServer(args)
self._set_server_handler(None)
result = rpc_result.success()
break
break
if success is None:
result = rpc_result.error( {'msg':"Invalidate operation:{}".format(op)})
return result
self._echo_server_op(op);
return result
def _start_submit_space_info(self):
thread = threading.Thread(target=self._try_submit_odoo_space_info)
thread.start()
#thread.join();
return thread
def _try_submit_odoo_space_info(self, timeout=30):
time.sleep(timeout) # second
go = True
wait_count = 0
wait_unit = 3
while(go):
_logger.debug("_try_submit_odoo_space_info,loop:%s", wait_count)
server_running = self._server_is_running()
if server_running:
go = False
break
# not running yet then wait it
time.sleep(wait_unit) # second
wait_count = wait_count + 1
if wait_count * wait_unit > timeout :
go = False
server_running = self._server_is_running()
_logger.debug("_try_submit_odoo_space_info,server_running:%s", server_running)
if server_running :
self.push_space_to_code_server()
else:
_logger.warn("❌ server is NOT running ")
@api.model
def fetch_server_info(self):
server_handler = self._get_server_handler()
if server_handler is None:
return rpc_result.error()
servers = ServerMaster.getServers()
if len(servers)<1:
return rpc_result.error()
return rpc_result.success(server_handler)
def _server_is_running(self):
servers = ServerMaster.getServers()
if servers is None or len(servers)<1:
return False
server_info = ServerMaster.get_server_info()
if server_info is None:
return False
if server_info.get('success'):
return True
return False
"""
@copy: from server.py
@purpose: output odoo runtime environment information
"""
@api.model
def report_configuration(self):
""" Log the server version and some configuration values.
This function assumes the configuration has been initialized.
"""
result = {}
result["version"] = __version__
result["release"] = vars(odoo.release)
result["addons"] = self.get_addons_modules() #odoo.addons.__path__
config = tools.config
_logger.debug("Odoo version %s", __version__)
if os.path.isfile(config.rcfile):
_logger.debug("Using configuration file at " + config.rcfile)
_logger.info('addons paths: %s', odoo.addons.__path__)
if config.get('upgrade_path'):
_logger.debug('upgrade path: %s', config['upgrade_path'])
host = config['db_host'] or os.environ.get('PGHOST', 'default')
port = config['db_port'] or os.environ.get('PGPORT', 'default')
user = config['db_user'] or os.environ.get('PGUSER', 'default')
_logger.debug('database: %s@%s:%s', user, host, port)
#json_config = json.dumps(config.options, indent = 4)
result["config"] = config.options
return result
def get_addons_modules(self):
#app=odoo.http.root
mod2path = {}
for addons_path in odoo.addons.__path__:
addons_item = {}
for module in os.listdir(addons_path):
manifest = get_manifest(module)
module_path = opj(addons_path, module)
if (manifest
#and (manifest['installable'])
and os.path.isdir(module_path)):
addons_item[module] = module_path
mod2path[addons_path] = addons_item
return mod2path
def addons_to_folders(self):
#app=odoo.http.root
folders = []
for addons_path in odoo.addons.__path__:
folder = {
"name":addons_path,
"path": addons_path
}
items = []
for module in os.listdir(addons_path):
manifest = get_manifest(module)
module_path = opj(addons_path, module)
if (manifest
and (manifest['installable'])
and os.path.isdir(module_path)):
subFolder ={
"name" : module,
"path" : module_path,
"data" : simple_manifest(manifest)
}
items.append(subFolder)
if len(items)>0:
folder["items"] = items
folders.append(folder)
return folders
"""
let params = {
folders:[
{
name:"group-name" + ts,
path: "group-path" + ts,
data: {},
items:[ {
name:"item-name" + ts,
path: 'item-path' + ts,
data:{}
}]
}
]
}
"""
@api.model
def push_space_to_code_server(self):
_logger.debug("try push_space_to_code_server")
folders = self.addons_to_folders()
for x in folders:
self.post_addons_folder(x)
# split one big post task to mullti-tasks avoid Big Data issue on Node server
def post_addons_folder(self,folder):
simple_folder = concise_folder(folder)
folder_rep = self.call_post_folders([simple_folder])
if not folder_rep.get('success'):
return False
one = folder_rep.get("data")[0]
folder_id = one.get("id")
items = folder.get('items')
if items is None:
return
count = 0
folders =[]
for item in items:
simple_folder = concise_folder(item)
simple_folder['parentId'] = folder_id
folders.append(simple_folder)
count = count + 1
if count >30:
self.call_post_folders(folders)
# reset
count = 0
folders =[]
if len(folders) > 0:
self.call_post_folders(folders)
def call_post_folders(self,folders):
return ServerMaster.post_folders(folders)
def _get_cached_addons(self):
addons_modules = theCache.get("addons_modules")
if addons_modules is None:
addons_modules = self.get_addons_modules()
theCache.set("addons_modules",addons_modules)
return addons_modules
"""
const intent = {
"module" : "kit_code",
"file" : "__manifest__.py",
"lineNo": 1
}
"""
@api.model
def get_home_link(self):
dir = get_module_path()
server = ServerMaster._server
end_point = server.get("url")
file = "__manifest__.py"
url = "{}/folder?dir={}&file={}".format(end_point,dir,file)
result = {
'navigateTo': url
}
return result
"""
name:web.NavBar.DropdownItem
class:sub template
type:owl
url:/web/static/src/webclient/navbar/navbar.xml
file:/odoo-17/odoo/addons/web/static/src/webclient/navbar/navbar.xml
"""
@api.model
def get_editable_file(self,intent):
if (intent is None):
intent = {
"name":"CodeDashboard",
"url" : "",
"file" : "full path",
"lineNo": 99
}
file_name = intent["file"]
addons_modules = self._get_cached_addons()
module_dir = None
module_name = None
for addons in addons_modules:
if file_name.startswith(addons + os.sep):
modules = addons_modules[addons]
for module in modules:
module_path = modules[module]
if file_name.startswith(module_path + os.sep):
module_dir = module_path
module_name = module
break
if module_dir is None:
return {"status":False,"error":"No matched module!"}
module ={
"name":module_name,
"path":module_dir
}
short_file_name = file_name.replace(module_dir + os.sep,"")
args ={
"dir":module_dir,
"file": short_file_name
}
navigate_to_url = ServerMaster.get_open_file_url(args)
result ={
"module":module,
"file":short_file_name,
"navigateTo": navigate_to_url
}
return result

View file

@ -0,0 +1,15 @@
# -*- coding: utf-8 -*-
from odoo import models
from odoo.http import request
class Http(models.AbstractModel):
_inherit = 'ir.http'
@classmethod
def _pre_dispatch(cls, rule, args):
super()._pre_dispatch(rule, args)
url = request.httprequest.url
for key in args:
pass

View file

@ -0,0 +1,19 @@
#-*- coding: utf-8 -*-
import logging
from odoo import models, fields, api
import threading
import time
import json
class Notifyer(models.AbstractModel):
_inherit = "bus.bus"
@api.model
def push(self,data, channel="eventChannel"):
message = {
"data": data,
"channel": channel
}
self._sendone(channel, "notification", message)

View file

@ -0,0 +1,242 @@
# -*- coding: utf-8 -*-
# 1 : imports of python lib
from pathlib import Path
import json
import logging
import urllib
import os
import subprocess
import base64
import sys
import requests
# 2 : imports of odoo
from odoo import _, api, exceptions, fields, http, models, tools
from ..shell.sheller import async_run
from ..land.jsons import json_dumps
# 4 : variable declarations
_logger = logging.getLogger(__name__)
def log_it(msg):
_logger.debug(str(msg))
def get_current_path():
#dir_path = os.path.dirname(os.path.realpath(__file__))
dir_path = Path(__file__).resolve().parent
return dir_path
def get_value(data, key,default=None):
result = data.get(key)
if result is None:
result = default
return result
def contains_error(buf):
if "\"error\":" in buf:
return True
return False
def contains_success(buf):
if "'result': True" in buf:
return True
return False
# duplicate
def _quote_(value):
#return "\"{}\"".format(value)
return "\\\"{}\\\"".format(value)
def _key_str_value(key,value):
result = _quote_(key) +":" + _quote_(value)
return result
def _key_obj_value(key,value):
result = _quote_(key) +":" + "{ }" if value is None else value
return result
# sys.platform => 'darwin', 'linux', 'win32'
def _get_server_exe_name():
bin_app_fmt = "code-server-{}"
os = sys.platform
if 'win32' == os :
return bin_app_fmt.format(".exe")
if 'darwin' == os :
return bin_app_fmt.format("macos")
return bin_app_fmt.format("linux")
def _get_server_path():
path = get_current_path()
result = str(path.resolve().parent) + "/vendors/code-server/" + _get_server_exe_name()
return result
def _get_module_path():
path = get_current_path()
result = str(path.resolve().parent)
return result
def on_shell_echo(process, msg):
log_it("------------- on_shell_echo -------------")
log_it(msg)
ServerMaster.put_proc(str(process.pid), process)
# Class
class ServerMaster():
proc_set ={}
args = None
_server={}
@staticmethod
def put_proc(key,proc):
cached_proc = ServerMaster.proc_set.get(key)
if cached_proc is None:
ServerMaster.proc_set[key] = proc
@staticmethod
def get_proc(key):
cached_proc = ServerMaster.proc_set.get(key)
return cached_proc
@staticmethod
def get_proc_set():
return ServerMaster.proc_set
@staticmethod
def getTargetServerPath():
return _get_server_path()
"""
"""
@staticmethod
def startServer(args):
ServerMaster.args = args;
port = get_value(args,"port",3030)
doc_path = _get_module_path()
server = ServerMaster.getTargetServerPath()
cmd = "{} start --port={} --space={} --slient=true --focus=README.rst,__manifest__.py ".format(server,port, doc_path)
log_it(cmd)
# return json string that print out by RPC server
async_run(cmd,ServerMaster.on_shell_echo)
return ServerMaster.warp_server(port)
# on_echo: procss, message ={stdout,stderr,exitcode}
@staticmethod
def on_shell_echo(process, message):
log_it("------------- on_shell_echo -------------")
log_it(message)
ServerMaster.put_proc(str(process.pid), process)
args = ServerMaster.args
if args is None or args.get('on_echo') is None:
return True
on_echo = args.get('on_echo')
on_echo(message=message)
return True
@staticmethod
def getServers():
proc_set = ServerMaster.get_proc_set()
result =[]
if len(proc_set) <= 0:
return result
for key in list(proc_set.keys()):
try:
x = proc_set[key]
exit_code = x.poll()
if not exit_code is None:
del proc_set[key]
continue
item = {
"pid": x.pid,
"args": x.args,
}
result.append(item)
except Exception as ex:
pass
return result
@staticmethod
def stopServer(args):
proc_set = ServerMaster.get_proc_set()
#Popen.terminate()
for x in proc_set.values():
try:
x.terminate()
except Exception as ex:
pass
return True
#----------------- biz logic -----------------
@staticmethod
def warp_server(port):
result = {
"url": "http://localhost:{}".format(port)
}
ServerMaster._server = result;
return result
@staticmethod
def get_open_file_url(args):
dir = args.get("dir")
file = args.get("file")
server = ServerMaster._server
end_point = server.get("url")
url = "{}/folder?dir={}&file={}".format(end_point,dir,file)
return url
@staticmethod
def get_server_info():
server = ServerMaster._server
headers = {"content-type":"application/json"}
#url = "http://localhost:3030/api/echo"
end_point = server.get("url")
url = "{}/api/info".format(end_point)
_logger.debug("get_server_info, url:%s",url)
response = requests.get(url, headers=headers)
json_response = response.json()
buf = json_dumps(json_response)
_logger.debug('server_is_running response: %s',buf)
return json_response
@staticmethod
def post_folders(folders):
try:
return ServerMaster._post_folders(folders)
except Exception as e:
_logger.error('failed post folders,error:%s', e)
return {}
@staticmethod
def _post_folders(folders):
payload ={
"folders":folders
}
data_buf = json_dumps(payload)
headers = {"content-type":"application/json"}
#url = "http://localhost:3030/api/folders"
end_point = ServerMaster._server.get("url")
url = "{}/api/folders".format(end_point)
response = requests.post(url, data=data_buf,headers=headers)
if response is None:
return {}
json_response = response.json()
buf = json_dumps(json_response)
_logger.debug('call_post_folders response: %s',buf)
return json_response

View file

@ -0,0 +1,4 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_kit_code_server,kit_code_server,model_kit_code_server,base.group_user,1,1,1,1
bot_kit_code_server,bot_kit_code_server,model_kit_code_server,base.group_public,1,1,1,1
sys_kit_code_server,sys_kit_code_server,model_kit_code_server,base.group_system,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_kit_code_server kit_code_server model_kit_code_server base.group_user 1 1 1 1
3 bot_kit_code_server bot_kit_code_server model_kit_code_server base.group_public 1 1 1 1
4 sys_kit_code_server sys_kit_code_server model_kit_code_server base.group_system 1 1 1 1

View file

@ -0,0 +1,69 @@
# -*- coding: utf-8 -*-
import subprocess
from time import sleep
import threading
# command: ["python", "timer.py", "5"]
# on_echo: procss, message ={stdout,stderr,exitCode}
"""
procss is Popen instance
Popen.terminate()
Stop the child. On POSIX OSs the method sends SIGTERM to the child. On Windows the Win32 API function TerminateProcess() is called to stop the child.
Popen.kill()
"""
def run(command,on_echo):
if isinstance(command, str):
command = command.split()
with subprocess.Popen(
command, stdout=subprocess.PIPE, stderr=subprocess.PIPE
) as process:
go = not on_echo is None
def poll_and_read():
# print(f"Output from poll: {process.poll()}")
# print(f"Output from stdout: {process.stdout.read1().decode('utf-8')}")
exit_code = process.poll()
if not exit_code is None:
on_echo(process,{'process exitcode':exit_code})
return False
#stdout, stderr = process.communicate()
stdout = process.stdout.readline()
#stderr = process.stderr.readline()
# stdout = process.stdout.read1().decode('utf-8')
# stderr = process.stderr.read1().decode('utf-8')
stderr = ""
message ={}
if not stdout == '':
# Split output into lines.
stdout_buf = [s.rstrip("\n\r") for s in str(stdout, "utf8").splitlines()]
message['stdout']=stdout_buf
if not stderr == '':
#stderr_buf = [s.rstrip("\n\r") for s in str(stderr, "utf8").splitlines()]
message['stderr']=stderr
if len(message) == 0:
return True
return on_echo(process,message)
while go:
go = poll_and_read()
sleep(3)
def async_run(command,on_echo):
thread = threading.Thread(target=run,args=(command,on_echo))
thread.start()
return thread

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 354 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 456 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View file

@ -0,0 +1,262 @@
<section class="oe_container">
<div class="oe_row oe_spaced">
<div class="oe_span12">
<h2 class="text-center oe_slogan"
style="font-family: Montserrat, 'sans-serif' !important; font-weight: bolder !important; color: hsl(0, 0%, 13%);">
Odoo Code
</h2>
<div class="text-center " style="font-weight: bolder !important; margin-top: -20px; color:#6f6060;" >Third-Party</div>
<h3 class="oe_slogan" style="text-shadow: 1px 1px 2px rgb(40, 244, 88);color: #cd0909e0;">Help you build Odoo application code online!</h3>
</div>
</div>
</section>
<section class="oe_container">
<div class="oe_row oe_spaced">
<div class="oe_span12">
<h4 class="oe_slogan"><b>Features</b></h4>
<div class="col-lg-12 d-flex justify-content-center align-items-center" style="margin: 2rem 0;font-size: 1.5rem !important; ">
<ul >
<li class="fa fa-star " style="color: rgb(217, 47, 9);"> <span style="color: #4a0404; margin-left: 6px;">View & Write Odoo code through web Code Editor in the same Odoo runtime environment;</span>
</li> <br><br>
<li class="fa fa-star" style="color: rgb(217, 47, 9);"> <span style="color: #4a0404; margin-left: 6px;">Hot reload, quickly and easily experiment, build UIs, add features, fix bugs & more;</span>
</li> <br><br>
<li class="fa fa-star" style="color: rgb(217, 47, 9);"> <span style="color: #4a0404; margin-left: 6px;">Locate the Templates Script source code quickly, save time & speed up development process;</span>
</li> <br><br>
<li class="fa fa-star" style="color: rgb(217, 47, 9);"> <span style="color: #4a0404; margin-left: 6px;">Write Odoo code anytime anywhere, free mind & happy coding! </span>
</li>
</ul>
</div>
</div>
</div>
</section>
<!-- <section class="oe_container oe_dark">
<div class="oe_row oe_spaced">
<div class="oe_row_img oe_centered" align="center">
<img class="oe_demo oe_picture oe_screenshot" src="assets/frontend.gif"/>
</div>
</div>
</section> -->
<!-- Browse gif -->
<div class=" text-left">
<p class="oe_mt32">
<font style="font-size: 120%;font-weight: bolder !important;color: #6f6060;">Browse Modules:</font>
</p>
</div>
<section class="oe_container oe_dark">
<div class="oe_row oe_spaced">
<div class="oe_screenshot" align="center">
<img style="max-width: 80%" src="assets/code-browse.gif"/>
</div>
</div>
</section>
<div class="text-center">
<p class="oe_mt32" style="font-size: 120%;">
Launch Code Server, browse Odoo modules source code online.
</p>
</div>
<!-- Spy-Code gif -->
<div class=" text-left">
<p class="oe_mt32">
<font style="font-size: 120%;font-weight: bolder !important;color: #6f6060;">Spy Element & Edit:</font>
</p>
</div>
<section class="oe_container oe_dark">
<div class="oe_row oe_spaced">
<div class="oe_screenshot" align="center">
<img style="max-width: 80%" src="assets/spy-code.gif"/>
</div>
</div>
</section>
<div class="text-center">
<p class="oe_mt32" style="font-size: 120%;">
Insight into the Odoo web page structure, super easy locate the feature source code line, The Spy Element feature should install the <a href="https://apps.odoo.com/apps/modules/17.0/kit_spy">Odoo Spy</a> module.
</p>
</div>
<h4 class="oe_slogan"><b>Screenshots</b></h4>
<div class=" text-left">
<p class="oe_mt32">
<font style="font-size: 120%;font-weight: bolder !important;color: #6f6060;">Home Space:</font>
</p>
</div>
<section class="oe_container oe_dark">
<div class="oe_row oe_spaced">
<div class="oe_screenshot" align="center">
<img style="max-width: 80%" src="assets/screenshots/editor.png"/>
</div>
</div>
</section>
<div class=" text-left">
<p class="oe_mt32">
<font style="font-size: 120%;font-weight: bolder !important;color: #6f6060;">Browse Modules:</font>
</p>
</div>
<section class="oe_container oe_dark">
<div class="oe_row oe_spaced">
<div class="oe_screenshot" align="center">
<img style="max-width: 80%" src="assets/screenshots/browse.png"/>
</div>
</div>
</section>
<div class="text-center">
<p class="oe_mt32" style="font-size: 120%;">
All Odoo modules can be load in online code editor, write code, hot-reload frontend UIs, easily and quickly!
</p>
</div>
<!-- <section class="oe_container oe_dark">
<div class="oe_row oe_spaced">
<div class="oe_span6">
<div class="oe_row_img oe_centered" align="center">
<img class="oe_demo oe_picture oe_screenshot" src="assets/screenshots/backend-app-studio-1.png"/>
</div>
</div>
<div class="oe_span6">
<div class="oe_row_img oe_centered" align="center">
<img class="oe_demo oe_picture oe_screenshot" src="assets/screenshots/backend-app-studio-2.png"/>
</div>
</div>
</div>
</section> -->
<h4 class="oe_slogan"><b>Usage</b></h4>
<section class="oe_container text-left">
<div class="oe_row " >
<p class="oe_mt0" style="font-size: 120%;margin-left: 16px; margin-right: 16px; ">
&nbsp;&nbsp;&nbsp;&nbsp;Start Code Server from the Code Console page of this module, once start success, browser auto navigate to new window that address is :<span>
<a style="font-weight: bolder !important; color: #827d7d !important; cursor: pointer !important;" href="http://localhost:3030">http://localhost:3030/
</a>
</span>; The core features of code editor server was provide by the open source <span> <a style=" cursor: pointer !important;" href="https://github.com/ixkit/code-server">Code Server
</a> </span>
</p>
</div>
</div>
<div class="oe_row oe_spaced oe_mt0" style="text-align: center; width: 100%;">
<div>
<a href="https://github.com/ixkit/code-server"
target="_blank"
class="btn btn-block mb-2 deep_hover"
style="text-decoration: none; background-color: #413e3eaf; color: #FFF; border-radius: 4px;">
<i class="fa fa-github mr-2"></i>
<span style="margin-left: 6px;">CodeServer</span>
</a>
</div>
<div>
<a style="font-size: 120%; color: #827d7d; cursor: pointer !important;" href="https://github.com/ixkit/code-server">https://github.com/ixkit/code-server
</a>
</div>
</div>
<div class="oe_row oe_spaced oe_mt0">
<p class=" text-left" style="font-size: 120%;">
<font style="font-size: 100%;font-weight: bold !important;color: #6f6060;">Additional Step:</font> Manual install the Code Server
</p>
<div class="oe_span8" style="width: 100%; " >
<ul style="margin-left: -10px;" >
<li >
<p class="oe_mt8" style="font-size: 120%;">
Download the Code Server program from the Code Server<span> <a style=" cursor: pointer !important;" href="https://github.com/ixkit/code-server/releases">repository
</a> </span> or clone the source code and build the Code Sever binary package.
</p>
</li>
<li >
<p class="oe_mt8" style="font-size: 120%;">
Put the Code Sever binary package into the moudule directory './kit_code/vendors/',
</p>
<p class="oe_mt8" style="font-size: 120%;">
eg:<br>
mac platform :
it should be './kit_code/venders/code_server/code-server-macos'
<br>
windows:
it should be './kit_code/venders/code_server/code-server-win.exe'
</p>
</li>
</ul>
</div>
</div>
</section>
<div class="oe_row " style="margin-top: -20px;">
<div class="text-left">
<p class=" text-left" style="font-size: 120%;">
<font style="font-size: 100%;font-weight: bold !important;color: #6f6060;">Notice:</font>
</p>
<p class="oe_mt16 oe_mb16" style="font-size: 120%; ">
<i class="fa fa-exclamation-triangle" style="color: red" aria-hidden="true"></i> This module should only use for development phase! Production Environment is not recommend!
</p>
</div>
</div>
<div class="text-center">
<p class="oe_mb48 oe_mt32" style="font-size: 120%; color: #827d7d;">
Enjoy & Happy Coding!&nbsp;&nbsp;<i class="fa fa-smile-o" aria-hidden="true"></i>
</p>
</div>
<h4 class="oe_slogan"><b>Support</b></h4>
<section class="oe_container oe_dark">
<div class="oe_row oe_spaced">
<div class="oe_span6">
<div class="oe_row_img oe_centered" align="center">
<a href="mailto:odoo@ixkit.com" target="_blank"
class="btn btn-block mb-2 deep_hover"
style="text-decoration: none; background-color: #b94646af; color: #FFF; border-radius: 4px;"><i
class="fa fa-envelope mr-2"></i>&nbsp;odoo@ixkit.com</a>
</div>
</div>
<div class="oe_span6">
<div class="oe_row_img oe_centered" align="center">
<a href="skype:live:.cid.c5d5ecc7e54128e2?call"
class="btn btn-block mb-2 deep_hover"
style="text-decoration: none; background-color: #1b9beabd; color: #FFF; border-radius: 4px;">
<i class="fa fa-skype mr-2"></i>&nbsp;&nbsp;ixkit&nbsp;&nbsp;
</a>
</div>
</div>
</div>
</section>
<section class="oe_container oe_dark oe_mt64 oe_mb64"></section>
<h4 class="oe_slogan "><i class="fa fa-heart" aria-hidden="true" style="font-size: 90%;color: rgb(217, 47, 9);"></i>
<span class="font-sans-serif" style="margin-left: 6px;font-size: 90%; cursor: pointer !important; ">
<a style="font-weight: bolder !important; color: #827d7d !important; cursor: pointer !important;" href="http://ixkit.com/odookit">by ixkit team
</a>
</span>
</h4>
</section>
<section class="oe_container oe_separator">
</section>

View file

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg fill="#000000" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 503.607 503.607" xml:space="preserve">
<g>
<g>
<path d="M503.607,125.902V25.18c0-13.43-10.911-25.18-25.18-25.18H25.18C10.911,0,0,10.911,0,25.18v100.721
c0,13.429,10.911,25.18,25.18,25.18v25.18c-13.43,0-25.18,10.911-25.18,25.18v100.721c0,13.43,10.911,25.18,25.18,25.18v25.18
c-13.43,0-25.18,10.911-25.18,25.18v100.721c0,13.43,10.911,25.18,25.18,25.18h453.246c13.43,0,25.18-10.911,25.18-25.18V377.705
c0-13.429-10.911-25.18-25.18-25.18v-25.18c13.43,0,25.18-10.911,25.18-25.18V201.443c0-13.43-10.911-25.18-25.18-25.18v-25.18
C491.856,151.082,503.607,140.171,503.607,125.902z M41.967,151.082h419.672v25.18H41.967V151.082z M33.574,276.984v-50.361
c0-5.036,3.357-8.393,8.393-8.393h100.721c5.036,0,8.393,3.357,8.393,8.393v50.361c0,5.036-3.357,8.393-8.393,8.393H41.967
C36.931,285.377,33.574,281.18,33.574,276.984z M461.639,352.525H41.967v-25.18h419.672V352.525z M176.262,276.984
c0-5.036,3.357-8.393,8.393-8.393s8.393,3.357,8.393,8.393c0,5.036-3.357,8.393-8.393,8.393S176.262,281.18,176.262,276.984z
M201.443,260.197c-5.036,0-8.393-3.357-8.393-8.393s3.357-8.393,8.393-8.393s8.393,3.357,8.393,8.393
S206.479,260.197,201.443,260.197z M218.229,268.59c5.036,0,8.393,3.357,8.393,8.393c0,5.036-3.357,8.393-8.393,8.393
s-8.393-3.357-8.393-8.393C209.836,271.948,213.193,268.59,218.229,268.59z M235.016,260.197c-5.036,0-8.393-3.357-8.393-8.393
s3.357-8.393,8.393-8.393s8.393,3.357,8.393,8.393S240.052,260.197,235.016,260.197z M251.803,268.59
c5.036,0,8.393,3.357,8.393,8.393c0,5.036-3.357,8.393-8.393,8.393c-5.036,0-8.393-3.357-8.393-8.393
C243.41,271.948,246.767,268.59,251.803,268.59z M268.59,260.197c-5.036,0-8.393-3.357-8.393-8.393s3.357-8.393,8.393-8.393
s8.393,3.357,8.393,8.393S273.626,260.197,268.59,260.197z M285.377,268.59c5.036,0,8.393,3.357,8.393,8.393
c0,5.036-3.357,8.393-8.393,8.393c-5.036,0-8.393-3.357-8.393-8.393C276.984,271.948,280.341,268.59,285.377,268.59z
M302.164,260.197c-5.036,0-8.393-3.357-8.393-8.393s3.357-8.393,8.393-8.393s8.393,3.357,8.393,8.393
S307.2,260.197,302.164,260.197z M318.951,268.59c5.036,0,8.393,3.357,8.393,8.393c0,5.036-3.357,8.393-8.393,8.393
c-5.036,0-8.393-3.357-8.393-8.393C310.557,271.948,313.915,268.59,318.951,268.59z M335.738,260.197
c-5.036,0-8.393-3.357-8.393-8.393s3.357-8.393,8.393-8.393c5.036,0,8.393,3.357,8.393,8.393S340.774,260.197,335.738,260.197z
M352.525,268.59c5.036,0,8.393,3.357,8.393,8.393c0,5.036-3.357,8.393-8.393,8.393c-5.036,0-8.393-3.357-8.393-8.393
C344.131,271.948,347.489,268.59,352.525,268.59z M369.311,260.197c-5.036,0-8.393-3.357-8.393-8.393s3.357-8.393,8.393-8.393
s8.393,3.357,8.393,8.393S374.348,260.197,369.311,260.197z M386.098,268.59c5.036,0,8.393,3.357,8.393,8.393
c0,5.036-3.357,8.393-8.393,8.393c-5.036,0-8.393-3.357-8.393-8.393C377.705,271.948,381.062,268.59,386.098,268.59z
M402.885,260.197c-5.036,0-8.393-3.357-8.393-8.393s3.357-8.393,8.393-8.393c5.036,0,8.393,3.357,8.393,8.393
S407.921,260.197,402.885,260.197z M419.672,268.59c5.036,0,8.393,3.357,8.393,8.393c0,5.036-3.357,8.393-8.393,8.393
s-8.393-3.357-8.393-8.393C411.279,271.948,414.636,268.59,419.672,268.59z M428.066,251.803c0-5.036,3.357-8.393,8.393-8.393
c5.036,0,8.393,3.357,8.393,8.393s-3.357,8.393-8.393,8.393C431.423,260.197,428.066,256,428.066,251.803z M453.246,285.377
c-5.036,0-8.393-3.357-8.393-8.393c0-5.036,3.357-8.393,8.393-8.393c5.036,0,8.393,3.357,8.393,8.393
C461.639,282.02,458.282,285.377,453.246,285.377z M461.639,235.016H176.262c-5.036,0-8.393-3.357-8.393-8.393
s3.357-8.393,8.393-8.393h285.377c5.036,0,8.393,3.357,8.393,8.393S466.675,235.016,461.639,235.016z"/>
</g>
</g>
<g>
<g>
<rect x="50.361" y="235.016" width="83.934" height="33.574"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.9 KiB

View file

@ -0,0 +1,81 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg height="800px" width="800px" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 512 512" xml:space="preserve">
<g transform="translate(1 1)">
<path style="fill:#FCC309;" d="M58.733,186.733h392.533v-42.667H58.733V186.733z"/>
<path style="fill:#FFFFFF;" d="M33.133,186.733h25.6v-42.667h-25.6V186.733z"/>
<path style="fill:#FD9808;" d="M451.267,186.733h25.6v-42.667h-25.6V186.733z"/>
<path style="fill:#FCC309;" d="M58.733,365.933h392.533v-42.667H58.733V365.933z"/>
<path style="fill:#FFFFFF;" d="M33.133,365.933h25.6v-42.667h-25.6V365.933z"/>
<path style="fill:#FD9808;" d="M451.267,365.933h25.6v-42.667h-25.6V365.933z"/>
<path style="fill:#FFDD09;" d="M459.8,323.267H50.2c-9.387,0-17.067-7.68-17.067-17.067V203.8c0-9.387,7.68-17.067,17.067-17.067
h409.6c9.387,0,17.067,7.68,17.067,17.067v102.4C476.867,315.587,469.187,323.267,459.8,323.267"/>
<path style="fill:#1CD759;" d="M41.667,280.6h102.4v-51.2h-102.4V280.6z"/>
<path style="fill:#FFDD09;" d="M459.8,144.067H50.2c-9.387,0-17.067-7.68-17.067-17.067V24.6c0-9.387,7.68-17.067,17.067-17.067
h409.6c9.387,0,17.067,7.68,17.067,17.067V127C476.867,136.387,469.187,144.067,459.8,144.067"/>
<path style="fill:#FFFFFF;" d="M33.133,127V24.6c0-9.387,7.68-17.067,17.067-17.067H24.6c-9.387,0-17.067,7.68-17.067,17.067V127
c0,9.387,7.68,17.067,17.067,17.067h25.6C40.813,144.067,33.133,136.387,33.133,127"/>
<path style="fill:#FD9808;" d="M485.4,7.533h-25.6c9.387,0,17.067,7.68,17.067,17.067V127c0,9.387-7.68,17.067-17.067,17.067h25.6
c9.387,0,17.067-7.68,17.067-17.067V24.6C502.467,15.213,494.787,7.533,485.4,7.533"/>
<path style="fill:#FFFFFF;" d="M33.133,306.2V203.8c0-9.387,7.68-17.067,17.067-17.067H24.6c-9.387,0-17.067,7.68-17.067,17.067
v102.4c0,9.387,7.68,17.067,17.067,17.067h25.6C40.813,323.267,33.133,315.587,33.133,306.2"/>
<path style="fill:#FD9808;" d="M485.4,186.733h-25.6c9.387,0,17.067,7.68,17.067,17.067v102.4c0,9.387-7.68,17.067-17.067,17.067
h25.6c9.387,0,17.067-7.68,17.067-17.067V203.8C502.467,194.413,494.787,186.733,485.4,186.733"/>
<path style="fill:#FFDD09;" d="M459.8,502.467H50.2c-9.387,0-17.067-7.68-17.067-17.067V383c0-9.387,7.68-17.067,17.067-17.067
h409.6c9.387,0,17.067,7.68,17.067,17.067v102.4C476.867,494.787,469.187,502.467,459.8,502.467"/>
<path style="fill:#FFFFFF;" d="M33.133,485.4V383c0-9.387,7.68-17.067,17.067-17.067H24.6c-9.387,0-17.067,7.68-17.067,17.067
v102.4c0,9.387,7.68,17.067,17.067,17.067h25.6C40.813,502.467,33.133,494.787,33.133,485.4"/>
<path style="fill:#FD9808;" d="M485.4,365.933h-25.6c9.387,0,17.067,7.68,17.067,17.067v102.4c0,9.387-7.68,17.067-17.067,17.067
h25.6c9.387,0,17.067-7.68,17.067-17.067V383C502.467,373.613,494.787,365.933,485.4,365.933"/>
<path d="M485.4,152.6H24.6C10.947,152.6-1,141.507-1,127V24.6C-1,10.947,10.093-1,24.6-1h460.8C499.053-1,511,10.093,511,24.6V127
C511,141.507,499.907,152.6,485.4,152.6z M24.6,16.067c-5.12,0-8.533,3.413-8.533,8.533V127c0,4.267,3.413,8.533,8.533,8.533h460.8
c4.267,0,8.533-3.413,8.533-8.533V24.6c0-4.267-3.413-8.533-8.533-8.533H24.6z"/>
<path d="M485.4,511H24.6C10.947,511-1,499.907-1,485.4V383c0-13.653,11.093-25.6,25.6-25.6h460.8c13.653,0,25.6,11.093,25.6,25.6
v102.4C511,499.907,499.907,511,485.4,511z M24.6,374.467c-4.267,0-8.533,3.413-8.533,8.533v102.4c0,4.267,3.413,8.533,8.533,8.533
h460.8c4.267,0,8.533-3.413,8.533-8.533V383c0-4.267-3.413-8.533-8.533-8.533H24.6z"/>
<path d="M485.4,331.8H24.6C10.947,331.8-1,320.707-1,306.2V203.8c0-13.653,11.093-25.6,25.6-25.6h460.8
c13.653,0,25.6,11.093,25.6,25.6v102.4C511,320.707,499.907,331.8,485.4,331.8z M24.6,195.267c-4.267,0-8.533,3.413-8.533,8.533
v102.4c0,4.267,3.413,8.533,8.533,8.533h460.8c4.267,0,8.533-3.413,8.533-8.533V203.8c0-4.267-3.413-8.533-8.533-8.533H24.6z"/>
<path d="M365.933,280.6c0,5.12-3.413,8.533-8.533,8.533s-8.533-3.413-8.533-8.533s3.413-8.533,8.533-8.533
S365.933,275.48,365.933,280.6"/>
<path d="M400.067,280.6c0,5.12-3.413,8.533-8.533,8.533S383,285.72,383,280.6s3.413-8.533,8.533-8.533
S400.067,275.48,400.067,280.6"/>
<path d="M297.667,280.6c0,5.12-3.413,8.533-8.533,8.533c-5.12,0-8.533-3.413-8.533-8.533s3.413-8.533,8.533-8.533
C294.253,272.067,297.667,275.48,297.667,280.6"/>
<path d="M331.8,280.6c0,5.12-3.413,8.533-8.533,8.533c-5.12,0-8.533-3.413-8.533-8.533s3.413-8.533,8.533-8.533
C328.387,272.067,331.8,275.48,331.8,280.6"/>
<path d="M229.4,280.6c0,5.12-3.413,8.533-8.533,8.533c-5.12,0-8.533-3.413-8.533-8.533s3.413-8.533,8.533-8.533
C225.987,272.067,229.4,275.48,229.4,280.6"/>
<path d="M263.533,280.6c0,5.12-3.413,8.533-8.533,8.533s-8.533-3.413-8.533-8.533s3.413-8.533,8.533-8.533
S263.533,275.48,263.533,280.6"/>
<path d="M195.267,280.6c0,5.12-3.413,8.533-8.533,8.533c-5.12,0-8.533-3.413-8.533-8.533s3.413-8.533,8.533-8.533
C191.853,272.067,195.267,275.48,195.267,280.6"/>
<path d="M434.2,280.6c0,5.12-3.413,8.533-8.533,8.533s-8.533-3.413-8.533-8.533s3.413-8.533,8.533-8.533S434.2,275.48,434.2,280.6"
/>
<path d="M468.333,280.6c0,5.12-3.413,8.533-8.533,8.533s-8.533-3.413-8.533-8.533s3.413-8.533,8.533-8.533
S468.333,275.48,468.333,280.6"/>
<path d="M383,255c0,5.12-3.413,8.533-8.533,8.533c-5.12,0-8.533-3.413-8.533-8.533s3.413-8.533,8.533-8.533
C379.587,246.467,383,249.88,383,255"/>
<path d="M417.133,255c0,5.12-3.413,8.533-8.533,8.533c-5.12,0-8.533-3.413-8.533-8.533s3.413-8.533,8.533-8.533
C413.72,246.467,417.133,249.88,417.133,255"/>
<path d="M314.733,255c0,5.12-3.413,8.533-8.533,8.533s-8.533-3.413-8.533-8.533s3.413-8.533,8.533-8.533
S314.733,249.88,314.733,255"/>
<path d="M348.867,255c0,5.12-3.413,8.533-8.533,8.533c-5.12,0-8.533-3.413-8.533-8.533s3.413-8.533,8.533-8.533
C345.453,246.467,348.867,249.88,348.867,255"/>
<path d="M246.467,255c0,5.12-3.413,8.533-8.533,8.533c-5.12,0-8.533-3.413-8.533-8.533s3.413-8.533,8.533-8.533
C243.053,246.467,246.467,249.88,246.467,255"/>
<path d="M280.6,255c0,5.12-3.413,8.533-8.533,8.533s-8.533-3.413-8.533-8.533s3.413-8.533,8.533-8.533S280.6,249.88,280.6,255"/>
<path d="M212.333,255c0,5.12-3.413,8.533-8.533,8.533c-5.12,0-8.533-3.413-8.533-8.533s3.413-8.533,8.533-8.533
C208.92,246.467,212.333,249.88,212.333,255"/>
<path d="M451.267,255c0,5.12-3.413,8.533-8.533,8.533c-5.12,0-8.533-3.413-8.533-8.533s3.413-8.533,8.533-8.533
C447.853,246.467,451.267,249.88,451.267,255"/>
<path d="M468.333,237.933H178.2c-5.12,0-8.533-3.413-8.533-8.533c0-5.12,3.413-8.533,8.533-8.533h290.133
c5.12,0,8.533,3.413,8.533,8.533C476.867,234.52,473.453,237.933,468.333,237.933z"/>
<path d="M144.067,289.133h-102.4c-5.12,0-8.533-3.413-8.533-8.533v-51.2c0-5.12,3.413-8.533,8.533-8.533h102.4
c5.12,0,8.533,3.413,8.533,8.533v51.2C152.6,285.72,149.187,289.133,144.067,289.133z M50.2,272.067h85.333v-34.133H50.2V272.067z"
/>
<path d="M485.4,195.267H24.6v-59.733h460.8V195.267z M41.667,178.2h426.667v-25.6H41.667V178.2z"/>
<path d="M485.4,374.467H24.6v-59.733h460.8V374.467z M41.667,357.4h426.667v-25.6H41.667V357.4z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 7 KiB

View file

@ -0,0 +1,60 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg fill="#000000" height="800px" width="800px" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 512 512" xml:space="preserve">
<g transform="translate(1 1)">
<g>
<g>
<path d="M511,127V24.6C511,10.093,499.053-1,485.4-1H24.6C10.093-1-1,10.947-1,24.6V127c0,14.507,11.947,25.6,25.6,25.6v25.6
C10.093,178.2-1,190.147-1,203.8v102.4c0,14.507,11.947,25.6,25.6,25.6v25.6C10.093,357.4-1,369.347-1,383v102.4
c0,14.507,11.947,25.6,25.6,25.6h460.8c14.507,0,25.6-11.093,25.6-25.6V383c0-14.507-11.947-25.6-25.6-25.6v-25.6
c14.507,0,25.6-11.093,25.6-25.6V203.8c0-14.507-11.947-25.6-25.6-25.6v-25.6C499.907,152.6,511,141.507,511,127z M493.933,383
v102.4c0,5.12-4.267,8.533-8.533,8.533H24.6c-5.12,0-8.533-4.267-8.533-8.533V383c0-5.12,4.267-8.533,8.533-8.533h460.8
C490.52,374.467,493.933,378.733,493.933,383z M468.333,357.4H41.667v-25.6h426.667V357.4z M493.933,203.8v102.4
c0,5.12-4.267,8.533-8.533,8.533H24.6c-5.12,0-8.533-4.267-8.533-8.533V203.8c0-5.12,4.267-8.533,8.533-8.533h460.8
C490.52,195.267,493.933,199.533,493.933,203.8z M468.333,178.2H41.667v-25.6h426.667V178.2z M24.6,135.533
c-5.12,0-8.533-4.267-8.533-8.533V24.6c0-5.12,3.413-8.533,8.533-8.533h460.8c5.12,0,8.533,4.267,8.533,8.533V127
c0,5.12-4.267,8.533-8.533,8.533H24.6z"/>
<path d="M357.4,272.067c-5.12,0-8.533,3.413-8.533,8.533s3.413,8.533,8.533,8.533s8.533-3.413,8.533-8.533
S362.52,272.067,357.4,272.067z"/>
<path d="M391.533,272.067c-5.12,0-8.533,3.413-8.533,8.533s3.413,8.533,8.533,8.533c5.12,0,8.533-3.413,8.533-8.533
S396.653,272.067,391.533,272.067z"/>
<path d="M289.133,272.067c-5.12,0-8.533,3.413-8.533,8.533s3.413,8.533,8.533,8.533s8.533-3.413,8.533-8.533
S294.253,272.067,289.133,272.067z"/>
<path d="M323.267,272.067c-5.12,0-8.533,3.413-8.533,8.533s3.413,8.533,8.533,8.533s8.533-3.413,8.533-8.533
S328.387,272.067,323.267,272.067z"/>
<path d="M220.867,272.067c-5.12,0-8.533,3.413-8.533,8.533s3.413,8.533,8.533,8.533s8.533-3.413,8.533-8.533
S225.987,272.067,220.867,272.067z"/>
<path d="M255,272.067c-5.12,0-8.533,3.413-8.533,8.533s3.413,8.533,8.533,8.533s8.533-3.413,8.533-8.533
S260.12,272.067,255,272.067z"/>
<path d="M186.733,272.067c-5.12,0-8.533,3.413-8.533,8.533s3.413,8.533,8.533,8.533s8.533-3.413,8.533-8.533
S191.853,272.067,186.733,272.067z"/>
<path d="M425.667,272.067c-5.12,0-8.533,3.413-8.533,8.533s3.413,8.533,8.533,8.533s8.533-3.413,8.533-8.533
S430.787,272.067,425.667,272.067z"/>
<path d="M459.8,272.067c-5.12,0-8.533,3.413-8.533,8.533s3.413,8.533,8.533,8.533c5.12,0,8.533-3.413,8.533-8.533
S464.92,272.067,459.8,272.067z"/>
<path d="M374.467,246.467c-5.12,0-8.533,3.413-8.533,8.533s3.413,8.533,8.533,8.533S383,260.12,383,255
S379.587,246.467,374.467,246.467z"/>
<path d="M408.6,246.467c-5.12,0-8.533,3.413-8.533,8.533s3.413,8.533,8.533,8.533s8.533-3.413,8.533-8.533
S413.72,246.467,408.6,246.467z"/>
<path d="M306.2,246.467c-5.12,0-8.533,3.413-8.533,8.533s3.413,8.533,8.533,8.533s8.533-3.413,8.533-8.533
S311.32,246.467,306.2,246.467z"/>
<path d="M340.333,246.467c-5.12,0-8.533,3.413-8.533,8.533s3.413,8.533,8.533,8.533c5.12,0,8.533-3.413,8.533-8.533
S345.453,246.467,340.333,246.467z"/>
<path d="M237.933,246.467c-5.12,0-8.533,3.413-8.533,8.533s3.413,8.533,8.533,8.533s8.533-3.413,8.533-8.533
S243.053,246.467,237.933,246.467z"/>
<path d="M272.067,246.467c-5.12,0-8.533,3.413-8.533,8.533s3.413,8.533,8.533,8.533c5.12,0,8.533-3.413,8.533-8.533
S277.187,246.467,272.067,246.467z"/>
<path d="M203.8,246.467c-5.12,0-8.533,3.413-8.533,8.533s3.413,8.533,8.533,8.533c5.12,0,8.533-3.413,8.533-8.533
S208.92,246.467,203.8,246.467z"/>
<path d="M442.733,246.467c-5.12,0-8.533,3.413-8.533,8.533s3.413,8.533,8.533,8.533s8.533-3.413,8.533-8.533
S447.853,246.467,442.733,246.467z"/>
<path d="M178.2,237.933h290.133c5.12,0,8.533-3.413,8.533-8.533s-3.413-8.533-8.533-8.533H178.2c-5.12,0-8.533,3.413-8.533,8.533
S173.08,237.933,178.2,237.933z"/>
<path d="M41.667,289.133h102.4c5.12,0,8.533-3.413,8.533-8.533v-51.2c0-5.12-3.413-8.533-8.533-8.533h-102.4
c-5.12,0-8.533,3.413-8.533,8.533v51.2C33.133,285.72,36.547,289.133,41.667,289.133z M50.2,237.933h85.333v34.133H50.2V237.933z
"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.4 KiB

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (C) 2017 Ines Montani
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View file

@ -0,0 +1,101 @@
/**
* termynal.js
*
* @author Ines Montani <ines@ines.io>
* @version 0.0.1
* @license MIT
*/
:root {
--color-bg: #252a33;
--color-text: #eee;
--color-text-subtle: #a2a2a2;
}
[data-termynal] {
width: 750px;
max-width: 100%;
background: var(--color-bg);
color: var(--color-text);
font-size: 18px;
font-family: 'Fira Mono', Consolas, Menlo, Monaco, 'Courier New', Courier, monospace;
border-radius: 4px;
padding: 75px 45px 35px;
position: relative;
-webkit-box-sizing: border-box;
box-sizing: border-box;
}
[data-termynal]:before {
content: '';
position: absolute;
top: 15px;
left: 15px;
display: inline-block;
width: 15px;
height: 15px;
border-radius: 50%;
/* A little hack to display the window buttons in one pseudo element. */
background: #d9515d;
-webkit-box-shadow: 25px 0 0 #f4c025, 50px 0 0 #3ec930;
box-shadow: 25px 0 0 #f4c025, 50px 0 0 #3ec930;
}
[data-termynal]:after {
content: 'bash';
position: absolute;
color: var(--color-text-subtle);
top: 5px;
left: 0;
width: 100%;
text-align: center;
}
[data-ty] {
display: block;
line-height: 2;
}
[data-ty]:before {
/* Set up defaults and ensure empty lines are displayed. */
content: '';
display: inline-block;
vertical-align: middle;
}
[data-ty="input"]:before,
[data-ty-prompt]:before {
margin-right: 0.75em;
color: var(--color-text-subtle);
}
[data-ty="input"]:before {
content: '$';
}
[data-ty][data-ty-prompt]:before {
content: attr(data-ty-prompt);
}
[data-ty-cursor]:after {
content: attr(data-ty-cursor);
font-family: monospace;
margin-left: 0.5em;
-webkit-animation: blink 1s infinite;
animation: blink 1s infinite;
}
/* Cursor animation */
@-webkit-keyframes blink {
50% {
opacity: 0;
}
}
@keyframes blink {
50% {
opacity: 0;
}
}

View file

@ -0,0 +1,231 @@
/**
* termynal.js
* A lightweight, modern and extensible animated terminal window, using
* async/await.
*
* @author Ines Montani <ines@ines.io>
* @version 0.0.1
* @license MIT
*/
'use strict';
/** Generate a terminal widget. */
class Termynal {
/**
* Construct the widget's settings.
* @param {(string|Node)=} container - Query selector or container element.
* @param {Object=} options - Custom settings.
* @param {string} options.prefix - Prefix to use for data attributes.
* @param {number} options.startDelay - Delay before animation, in ms.
* @param {number} options.typeDelay - Delay between each typed character, in ms.
* @param {number} options.lineDelay - Delay between each line, in ms.
* @param {number} options.progressLength - Number of characters displayed as progress bar.
* @param {string} options.progressChar Character to use for progress bar, defaults to .
* @param {number} options.progressPercent - Max percent of progress.
* @param {string} options.cursor Character to use for cursor, defaults to .
* @param {Object[]} lineData - Dynamically loaded line data objects.
* @param {boolean} options.noInit - Don't initialise the animation.
*/
constructor(container = '#termynal', options = {}) {
this.container = (typeof container === 'string') ? document.querySelector(container) : container;
this.pfx = `data-${options.prefix || 'ty'}`;
this.maxLines = options.maxLines
|| parseInt(this.container.getAttribute(`${this.pfx}-maxLines`)) || 100;
this.startDelay = options.startDelay
|| parseFloat(this.container.getAttribute(`${this.pfx}-startDelay`)) || 600;
this.typeDelay = options.typeDelay
|| parseFloat(this.container.getAttribute(`${this.pfx}-typeDelay`)) || 90;
this.lineDelay = options.lineDelay
|| parseFloat(this.container.getAttribute(`${this.pfx}-lineDelay`)) || 1500;
this.progressLength = options.progressLength
|| parseFloat(this.container.getAttribute(`${this.pfx}-progressLength`)) || 40;
this.progressChar = options.progressChar
|| this.container.getAttribute(`${this.pfx}-progressChar`) || '█';
this.progressPercent = options.progressPercent
|| parseFloat(this.container.getAttribute(`${this.pfx}-progressPercent`)) || 100;
this.cursor = options.cursor
|| this.container.getAttribute(`${this.pfx}-cursor`) || '▋';
this.lineData = this.lineDataToElements(options.lineData || []);
if (!options.noInit) this.init()
}
/**
* Initialise the widget, get lines, clear container and start animation.
*/
init() {
// Appends dynamically loaded lines to existing line elements.
this.lines = [...this.container.querySelectorAll(`[${this.pfx}]`)].concat(this.lineData);
/**
* Calculates width and height of Termynal container.
* If container is empty and lines are dynamically loaded, defaults to browser `auto` or CSS.
*/
const containerStyle = getComputedStyle(this.container);
this.container.style.width = containerStyle.width !== '0px' ?
containerStyle.width : undefined;
this.container.style.minHeight = containerStyle.height !== '0px' ?
containerStyle.height : undefined;
//this.container.setAttribute('data-termynal', '');
this.container.innerHTML = '';
this.start();
}
/**
* Start the animation and rener the lines depending on their data attributes.
*/
async start() {
await this._wait(this.startDelay);
for (let line of this.lines) {
const type = line.getAttribute(this.pfx);
const delay = line.getAttribute(`${this.pfx}-delay`) || this.lineDelay;
if (type == 'input') {
line.setAttribute(`${this.pfx}-cursor`, this.cursor);
await this.type(line);
await this._wait(delay);
}
else if (type == 'progress') {
await this.progress(line);
await this._wait(delay);
}
else {
this.container.appendChild(line);
await this._wait(delay);
}
line.removeAttribute(`${this.pfx}-cursor`);
}
}
// <span data-ty="input">[('Hello', 'INTJ'), ('world', 'NOUN')]</span>
async appendLine(msg){
const lineNode = document.createElement("span");
lineNode.setAttribute('data-ty',"input");
lineNode.textContent = msg;
this.type(lineNode)
}
async removeFirstLine(){
const container = this.container;
const node = container.firstElementChild;
if (!node){
return false
}
container.removeChild(node);
return true
}
getLineCount(){
const container = this.container;
const size = container.childNodes.length;
return size;
}
async removeLineIfNeeds(){
const size = this.getLineCount();
if (size <= this.maxLines){
return false
}
return this.removeFirstLine()
}
/**
* Animate a typed line.
* @param {Node} line - The line element to render.
*/
async type(line) {
const chars = [...line.textContent];
const delay = line.getAttribute(`${this.pfx}-typeDelay`) || this.typeDelay;
line.textContent = '';
this.container.appendChild(line);
for (let char of chars) {
await this._wait(delay);
line.textContent += char;
}
this.removeLineIfNeeds();
}
/**
* Animate a progress bar.
* @param {Node} line - The line element to render.
*/
async progress(line) {
const progressLength = line.getAttribute(`${this.pfx}-progressLength`)
|| this.progressLength;
const progressChar = line.getAttribute(`${this.pfx}-progressChar`)
|| this.progressChar;
const chars = progressChar.repeat(progressLength);
const progressPercent = line.getAttribute(`${this.pfx}-progressPercent`)
|| this.progressPercent;
line.textContent = '';
this.container.appendChild(line);
for (let i = 1; i < chars.length + 1; i++) {
await this._wait(this.typeDelay);
const percent = Math.round(i / chars.length * 100);
line.textContent = `${chars.slice(0, i)} ${percent}%`;
if (percent>progressPercent) {
break;
}
}
}
/**
* Helper function for animation delays, called with `await`.
* @param {number} time - Timeout, in ms.
*/
_wait(time) {
return new Promise(resolve => setTimeout(resolve, time));
}
/**
* Converts line data objects into line elements.
*
* @param {Object[]} lineData - Dynamically loaded lines.
* @param {Object} line - Line data object.
* @returns {Element[]} - Array of line elements.
*/
lineDataToElements(lineData) {
return lineData.map(line => {
let div = document.createElement('div');
div.innerHTML = `<span ${this._attributes(line)}>${line.value || ''}</span>`;
return div.firstElementChild;
});
}
/**
* Helper function for generating attributes string.
*
* @param {Object} line - Line data object.
* @returns {string} - String of attributes.
*/
_attributes(line) {
let attrs = '';
for (let prop in line) {
attrs += this.pfx;
if (prop === 'type') {
attrs += `="${line[prop]}" `
} else if (prop !== 'value') {
attrs += `-${prop}="${line[prop]}" `
}
}
return attrs;
}
}
/**
* HTML API: If current script has container(s) specified, initialise Termynal.
*/
// if (document.currentScript.hasAttribute('data-termynal-container')) {
// const containers = 'termynal';// document.currentScript.getAttribute('data-termynal-container');
// containers.split('|')
// .forEach(container => new Termynal(container))
// }

View file

@ -0,0 +1,236 @@
.oh_dashboards{
padding-top :3px;
background-color: #f8faff !important;
}
.oh-card h4 {
font-size: 1.1rem;
}
.breadcrumbs {
margin-top: 0;
}
.buttons button {
margin: 2px 0; }
/* Button Reset */
.btn, .button {
display: inline-block;
font-weight: 400;
text-align: center;
white-space: nowrap;
vertical-align: middle;
transition: all .15s ease-in-out;
border-radius: 0;
cursor: pointer; }
/* Widget One
---------------------------*/
.stat-content {
display: inline-block;
width: 66%;
}
.stat-icon{
display: inline-block;
}
.status-widget-image-ready {
margin: auto;
max-width: 60px;
max-height: 110px;
content: url("../../image/server-white.svg");
}
.status-widget-image-running {
margin: auto;
max-width: 60px;
max-height: 110px;
content: url("../../image/server-running.svg");
}
/* HTML: <div class="status-widget-image-loading"></div> */
.status-widget-image-loading {
width: 50px;
aspect-ratio: 1;
display: grid;
color: #854f1d;
background: radial-gradient(farthest-side, currentColor calc(100% - 6px),#0000 calc(100% - 5px) 0);
-webkit-mask: radial-gradient(farthest-side,#0000 calc(100% - 13px),#000 calc(100% - 12px));
border-radius: 50%;
animation: l19 2s infinite linear;
}
.status-widget-image-loading::before,
.status-widget-image-loading::after {
content: "";
grid-area: 1/1;
background:
linear-gradient(currentColor 0 0) center,
linear-gradient(currentColor 0 0) center;
background-size: 100% 10px,10px 100%;
background-repeat: no-repeat;
}
.status-widget-image-loading::after {
transform: rotate(45deg);
}
@keyframes l19 {
100%{transform: rotate(1turn)}
}
.stat-widget-one .stat-icon {
vertical-align: top;
margin: auto;
width: 100%;
color: #01c490;
}
.stat-widget-one .stat-icon i {
font-size: 30px;
font-weight: 900;
display: inline-block;
color: #01c490;}
.stat-widget-one .stat-text {
font-size: 14px;
color: #868e96;
font-weight: bold;
}
.stat-widget-one .stat-digit {
font-size: 24px;
color: #02448b; }
.stat-count {
font-size: 20px;
text-align: center;
color: #00438b;}
.stat-title {
font-size: 17px;
text-align: center;
color: #00438b; }
.mb-0 .dash-title {
font-size: 20px;
text-align: center;
color: rgba(255, 255, 255, 0.81);
}
.hr_birthday {
font-size: 28px;
text-align: center;
padding: 20px 0;
color: #00438b;
font-weight: 600;
}
body .text-color {
color: #00438b;
}
.slice {
stroke: #fff;
stroke-width: 0px;
}
/*.dummy{
height:130vh;
}*/
.oh-card {
padding-top: 0px;
padding: 0px;
margin-bottom: 1.5rem;
border-radius: 0px;
box-shadow: none;
background: none;
transition: transform 0.2s ease, box-shadow 0.2s ease;
will-change: transform, box-shadow;
}
.oh-card:hover {
transform: translateY(-2px) translateZ(0) !important;
box-shadow: 0 10px 10px 0 rgba(62, 57, 107, 0.12), 0 0 0 transparent !important;
}
.oh-payslip {
margin-top: 10px;
}
.oh-payslip .stat-icon {
width: 30%;
height: 85px;
text-align: center;
background: #ff8762;
color: #fff;
width: 32%;
padding-top: 2%;
font-size: xxx-large;
}
.oh-payslip .oh-card {
transition: transform 0.2s ease, box-shadow 0.2s ease;
will-change: transform, box-shadow;
box-shadow: 0 10px 40px 0 rgba(62,57,107,0.07), 0 2px 9px 0 rgba(62,57,107,0.06);
}
.stat-widget-one .stat-text {
font-size: 14px;
color: #ff8762;
margin-top: 2.3rem;
margin-left: 1rem;
}
.stat-widget-one .stat-digit {
font-size: 26px;
color:#993232;
margin-left: 1rem;
margin-top: -1px;
font-family: initial
}
.stat-widget-one .stat-icon i {
font-size: 25px;
font-weight: 900;
display: inline-block;
color: #fff;
}
.stat-widget-one {
background-color: white;
text-align: left;
}
.stat-widget-one {
width: 100%;
}
.oh-payslip .stat-icon {
width: 30%;
height: 85px;
text-align: center;
padding-top: 2%;
}
.row.main-section {
margin-right: 0px; !important;
}
.oh-card-body {
display: flex;
justify-content: space-between;
align-items: center;
}

View file

@ -0,0 +1,388 @@
/** @odoo-module **/
import { registry } from "@web/core/registry";
import { session } from "@web/session";
import { _t } from "@web/core/l10n/translation";
import { Component } from "@odoo/owl";
import { onWillStart, onMounted,onWillUnmount, onWillDestroy, useState } from "@odoo/owl";
import { useService } from "@web/core/utils/hooks";
import { ConfirmationDialog, AlertDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
const actionRegistry = registry.category("actions");
export class CodeDashboard extends Component {
//Initializes the CodeDashboard component,
setup() {
super.setup(...arguments);
this.dialog = useService("dialog");
//message bus
const listenBus = (payload) => {
this.onMessage(payload)
}
this.busService = this.env.services.bus_service;
this.channel = "eventChannel";
this.busService.addChannel(this.channel);
// this.busService.addEventListener("notification", this.onMessage.bind(this))
this.busService.subscribe("notification", listenBus);
this.orm = useService("orm");
//this.user = useService("user");
this.actionService = useService("action");
const that = this;
this.state = useState({
serverStatus: "ready",
coderServer: null,
codeServerTip: null,
runtimeEnv: {}, //odoo runtime env
});
// When the component is about to start, fetch data in tiles
onWillStart(async () => {
await this.fetch_data();
});
//When the component is mounted, render various charts
onMounted(async () => {
await this.render_server_status();
this._initConsole();
//this.mockInput();
this.getOdooEnv();
});
onWillUnmount(() => {
this.setTimerHandler(null);
});
onWillDestroy(() => {
if (that.mockTyping) {
window.clearTimeout(this.mockTyping);
}
//this.busService.removeEventListener("notification", this.onMessage);
this.busService.unsubscribe("notification", listenBus);
this.busService.deleteChannel('eventChannel')
});
}
_initConsole() {
const container = "#termynal";
const termynal = new Termynal(container);
this.termynal = termynal;
}
async mockInput() {
// await this.onclick_pos_sales('Hourly');
if (!this.termynal) {
return;
}
const termynal = this.termynal;
const that = this;
function output() {
termynal.appendLine("Hello line @" + Date.now());
that.mockTyping = setTimeout(output, 3000);
try {
that.gotoLastLineIfNeeds();
} catch (ex) {
console.error("failed mockInput, ex?", ex);
}
}
setTimeout(output, 3000);
}
// array ?
async pathStatus(strOrArray) {
if (!strOrArray) return;
var out;
if (Array.isArray(strOrArray)) {
out = strOrArray[0];
} else {
out = strOrArray;
}
if (!out) return;
// Stop Code Server
// Start Code Server
let refresh = false;
if (out.includes("Stop Code Server")) {
refresh = true;
this.setServerStatus("ready");
}
if (out.includes("Start Code Server")) {
refresh = true;
this.setServerStatus("running");
}
return;
if (!refresh) return;
const that = this;
setTimeout(() => {
that.setServerStatus();
that.render_server_status();
}, 1000);
}
/*
{
"data": {
"message": "{\"stdout\": [\"2024-10-22T04:21:58.341Z root INFO Changed application state from 'ready' to 'closing_window'.\"]}",
"ts": "2024-10-22 04:21:58"
},
"channel": "eventChannel"
}
*/
onMessage(playload) {
const that = this;
const data = playload.data;
const message = JSON.parse(data.message);
let out = message.stdout;
if (!out) {
out = message.stderr;
}
if (!out) {
out = JSON.stringify(message);
}
try {
that.echo(out);
that.gotoLastLineIfNeeds();
} catch (ex) {}
}
onMessage_17({ detail: notifications }) {
console.debug("onMessage, detail?", detail);
notifications = notifications.filter(
(item) => item.payload.channel === this.channel
);
const that = this;
notifications.forEach((item) => {
const data = item.payload.data;
const message = JSON.parse(data.message);
let out = message.stdout;
if (!out) {
out = message.stderr;
}
if (!out) {
out = JSON.stringify(message);
}
try {
that.echo(out);
that.gotoLastLineIfNeeds();
} catch (ex) {}
});
}
async echo(msg) {
const termynal = this.termynal;
termynal.appendLine(msg);
}
async gotoLastLineIfNeeds() {
const container = "#termynal";
try {
const elt = document.getElementById(container); // $( container).get(0);
if (!elt) return;
elt.animate({ scrollTop: elt.scrollHeight }, 5000);
} catch (ex) {
console.error(ex);
}
}
async fetch_data() {}
on_ctrl_server() {
const op = this.state.serverStatus === "ready" ? "start" : "stop";
const opTip = op === "start" ? "🚀 Start" : "❌ Stop";
const prompt = `${opTip} Code Server?`;
const that = this;
this.dialog.add(ConfirmationDialog, {
title: _t("Confirm"),
body: _t(prompt),
confirm: async () => {
await this._do_ctrl_server();
},
confirmLabel: _t("OK"),
cancel: () => {},
cancelLabel: _t("Cancel"),
});
}
getCodeServerUrl() {
const coderServer = this.state.coderServer;
if (!coderServer) {
return null;
}
return coderServer.url;
return this.state.codeServerUrl;
const port = "3030";
const o = window.location.origin;
const list = o.split(":");
return `${list[0]}:${list[1]}:${port}`;
}
toCodeServerTip(status) {
let op = status === "ready" ? "start" : "stop";
op = op.charAt(0).toUpperCase() + op.slice(1);
let result = `Click the server icon to ${op} the Code Server`;
return result;
}
async _do_ctrl_server() {
const that = this;
const op = this.state.serverStatus === "ready" ? "start" : "stop";
this.orm
.call("kit.code.server", "ctrl_server", [op])
.then(function (result) {
//anyway refresh the UI
that.startCheckStatus();
});
}
async startCheckStatus(delay = 2000) {
const that = this;
const timerHandler = setTimeout(() => {
that.render_server_status((x) => {
if (x === "running") {
try {
if (that) {
that.onCodeServerStarted();
}
} catch (ex) {}
return;
}
if (that) {
that.startCheckStatus(3000);
}
});
}, delay);
this.setTimerHandler(timerHandler);
}
setTimerHandler(timer) {
const existTimer = this._timerHandler;
if (existTimer) {
clearTimeout(existTimer);
}
this._timerHandler = timer;
}
async onCodeServerStarted() {
const that = this;
const size = 1;
setTimeout(() => {
if (1 > 10) {
const url = that.getCodeServerUrl();
that._openCodeServerPage(url);
} else {
that._openHome();
}
// should do this job in server internal
that.pushSpaceToCodeServer();
}, 1000 * size);
}
async handleCickCodeServer() {
const that = this;
const url = that.getCodeServerUrl();
if (!url || "" === url) return;
that._openCodeServerPage(url);
}
async _openCodeServerPage(url) {
const that = this;
const tab = window.open(url, "modal=false,alwaysRaised=yes");
}
//status = "running" | "ready"
setServerStatus(status) {
try {
this.state.serverStatus = status;
this.state.codeServerTip = this.toCodeServerTip(status);
} catch (ex) {}
}
fetchServerInfo(onResult) {
const that = this;
this.orm
.call("kit.code.server", "fetch_server_info")
.then(function (rpcResult) {
if (rpcResult) {
if (rpcResult.success) {
const server_info = rpcResult.data;
that.state.coderServer = server_info;
onResult.apply(null, ["running"]);
return;
}
}
that.state.coderServer = null;
onResult.apply(null, ["ready"]);
});
}
async render_server_status(onStatusResult) {
const that = this;
this.fetchServerInfo((x) => {
that.setServerStatus(x);
if (!onStatusResult) {
return;
}
onStatusResult.apply(null, [x]);
});
}
async getOdooEnv(onResult) {
const that = this;
this.orm
.call("kit.code.server", "report_configuration")
.then(function (data) {
that.state.runtimeEnv = data;
that.renderOdooEnvData(data);
});
}
renderOdooEnvData(data) {
this.jsonTable = new JsonViewer({
value: data,
}).render("#jsonTable");
}
async pushSpaceToCodeServer() {
const that = this;
this.orm
.call("kit.code.server", "push_space_to_code_server")
.then(function (data) {});
}
async _openHome() {
const intent = {
module: "kit_code",
file: "__manifest__.py",
lineNo: 1,
};
const that = this;
this.orm.call("kit.code.server", "get_home_link", []).then(function (data) {
const url = data.navigateTo;
that._openCodeServerPage(url);
});
}
/*
name:web.NavBar.DropdownItem
class:sub template
type:owl
url:/web/static/src/webclient/navbar/navbar.xml
file:/odoo/addons/web/static/src/webclient/navbar/navbar.xml
*/
async _openFile() {
const intent = {
name: "web.NavBar.DropdownItem",
url: "/web/static/src/webclient/navbar/navbar.xml",
file: "/odoo/addons/web/static/src/webclient/navbar/navbar.xml",
lineNo: 99,
};
const that = this;
this.orm
.call("kit.code.server", "get_editable_file", [intent])
.then(function (data) {
const url = data.navigateTo;
that._openCodeServerPage(url);
});
}
}
CodeDashboard.template = 'CodeDashboard'
registry.category("actions").add("open_code_dashboard", CodeDashboard)

View file

@ -0,0 +1,71 @@
const getAnimateCanvas =()=>{
const mycanvas = document.getElementById('codeStreamCanvas');
return mycanvas ;
}
const animate_coding_effect =()=> {
var H = window.innerHeight;
var W = window.innerWidth;
var mycanvas = getAnimateCanvas()
var ctx = mycanvas.getContext('2d');
mycanvas.height = H;
mycanvas.width = W;
var fontsize = 18;
var text = [];
var lie = Math.floor(W / fontsize);
var str = '01'
for (var i = 0; i < lie; i++) {
text.push(0);
}
ctx.font = fontsize + 'px ';
function draw() {
ctx.fillStyle = 'rgba(0,0,0,0.08)'
ctx.fillRect(0, 0, W, H);
ctx.fillStyle = randColor();
for (var i = 0; i < lie; i++) {
var index = Math.floor(Math.random() * str.length);
var x = Math.floor(fontsize * i)
var y = text[i] * fontsize
ctx.fillText(str[index], x, y);
if (y > H && Math.random() > 0.99) {
text[i] = 0
}
text[i]++;
}
}
function randColor() {
var r = Math.ceil(Math.random() * 155) + 100;
var g = Math.ceil(Math.random() * 155) + 100;
var b = Math.ceil(Math.random() * 155) + 100;
return 'rgb(' + r + ',' + g + ',' + b + ')'
}
var timer = setInterval(function() {
draw();
}, 1000 / 30)
}
const listenWindowSize = ()=>{
window.οnresize=function(){
var mycanvas = document.getElementById('mycanvas');
if (!mycanvas) return ;
var H = window.innerHeight;
var W = window.innerWidth;
mycanvas.height = H;
mycanvas.width = W;
}
}
window.onload=function(){
//console.log('🚀 effect.js,window.onload')
if (getAnimateCanvas()){
animate_coding_effect()
listenWindowSize();
}
}
//console.log('🚀 effect.js')

View file

@ -0,0 +1,91 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="kit_code.code_dashboard" xml:space="preserve">
<!-- Template for Code Dashboard -->
<t t-name="CodeDashboard">
<div class="oh_dashboards" style="height:100%;" >
<!-- <div class="debug-area" style="display:block">
<button t-on-click="pushSpaceToCodeServer" >pushSpaceToCodeServer</button>
<button t-on-click="_openFile" >openFile</button>
</div> -->
<div class="container-fluid o_dashboard" style="height:100%;">
<div class="row main-section" align="center" >
<div class=" oh-payslip " >
<div xclass="oh-card" >
<div class="oh-card-body" style="justify-content: center;">
<div style="cursor:pointer" class = 'button-grow-shadow'
t-on-click="on_ctrl_server" t-att-title='state.codeServerTip' >
<img t-att-class="state.serverStatus =='ready' ? 'status-widget-image-ready button ': 'status-widget-image-running button ' " alt="code server" />
</div>
<span style="margin-left:20px;font-weight: bold;">Code Server </span>
<span style="margin-left:5px;">is <t t-esc="state.serverStatus"/></span>
<div style="margin-left:6px; cursor: -webkit-grab; cursor: grab;" title="Open Code Editor" t-if="state.serverStatus ==='running'" t-on-click="handleCickCodeServer" >
<span > at <t t-esc="state.coderServer.url"/></span>
</div>
<div style='font-size:12px;margin-left:10px;' >
<button name="" type="object"
class="btn-primary" icon="fa-star" t-on-click="on_ctrl_server">
<div>
<t t-if="state.serverStatus ==='running'">Stop</t>
<t t-else="">
Start
</t>
</div>
</button>
</div>
</div>
<!-- <div class="status-widget-image-loading"></div>
<div style="text-align: center; margin-top:3px; color:gray; "><t t-esc="state.codeServerTip" /></div>
-->
</div>
</div>
</div>
<div style=" margin-top:-20px;" classx="col-md-4 col-sm-6" align="center">
<div style="height:100%;" >
<ul class="nav nav-tabs" id="nav-tab" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="console-tab" data-bs-toggle="tab" data-bs-target="#console" type="button" role="tab" aria-controls="console" aria-selected="true">Console</button>
</li>
<li class="nav-item">
<button class="nav-link" id="odooEnv-tab" data-bs-toggle="tab" data-bs-target="#odooEnv" type="button" role="tab" aria-controls="odooEnv" aria-selected="false">Odoo Env</button>
</li>
</ul>
<div class="tab-content" id="myTabContent" style=" height:100%;">
<div class="tab-pane fade show active" id="console" role="tabpanel" aria-labelledby="console-tab" style=" height:100%;">
<!-- the termynal container max-height:30vh;-->
<div data-termynal='' style=" padding:75px 45px 35px !important; width:98%; max-height:80vh " >
<div style="min-height:70vh;max-height:70vh;overflow: auto;">
<div id="termynal" data-ty-maxLines="50" style=" margin: 0px 10px 10px 10px;text-align: left; width:98%; ">
</div>
</div>
</div>
</div>
<div class="tab-pane fade" id="odooEnv" role="tabpanel" aria-labelledby="odooEnv-tab" >
<div style="padding:5px 5px 5px !important; width:98%; height:100%;" >
<div><span style="font-weight: bold;">Odoo Runtime Environment</span>
</div>
<div id="jsonTable" style=" margin: 10px 10px 10px 10px;text-align: left;overflow: scroll; width:98%;max-height:75vh ">
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</t>
</templates>

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="act_window@code_server" model="ir.actions.act_window">
<field name="name">Message</field>
<field name="res_model">kit.code.server</field>
<field name="view_mode">list,form</field>
</record>
</data>
</odoo>

View file

@ -0,0 +1,21 @@
<odoo>
<data>
<record id="viewForm@code_server" model="ir.ui.view">
<field name="name">Message Form</field>
<field name="model">kit.code.server</field>
<field name="arch" type="xml">
<form string="">
<sheet >
<notebook groups="base.group_user">
<page string="Message" >
<div > Time: <field name="ts" readonly="1" /> </div>
<div> Message: <field name="message" readonly="1" /></div>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
</data>
</odoo>

View file

@ -0,0 +1,16 @@
<odoo>
<data>
<record id="viewList@code_server" model="ir.ui.view" >
<field name="name">Message List</field>
<field name="model">kit.code.server</field>
<field name="arch" type="xml">
<list>
<!-- <field name="name" eval="time.strftime('%Y-12-31')"/> -->
<field name="ts"/>
<field name="message"/>
</list>
</field>
</record>
</data>
</odoo>

View file

@ -0,0 +1,11 @@
<odoo>
<data>
<menuitem id="menu@root" name="Code🔨"
web_icon="kit_code,static/description/icon.png"
/>
<menuitem id="menu@root_log" name="Log" parent="menu@root" action="act_window@code_server"/>
</data>
</odoo>

View file

@ -0,0 +1,86 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>代码雨炫酷效果:公众号AlbertYang</title>
<style>
* {
margin: 0;
padding: 0;
}
html,
body {
height: 100%;
width: 100%;
overflow: hidden;
}
</style>
<script>
window.onload = function() {
var H = window.innerHeight;
var W = window.innerWidth;
var mycanvas = document.getElementById('mycanvas');
var ctx = mycanvas.getContext('2d');
mycanvas.height = H;
mycanvas.width = W;
var fontsize = 18;
var text = [];
var lie = Math.floor(W / fontsize);
var str = '01'
for (var i = 0; i < lie; i++) {
text.push(0);
}
ctx.font = fontsize + 'px 微雅软黑';
function draw() {
ctx.fillStyle = 'rgba(0,0,0,0.08)'
ctx.fillRect(0, 0, W, H);
ctx.fillStyle = randColor();
for (var i = 0; i < lie; i++) {
var index = Math.floor(Math.random() * str.length);
var x = Math.floor(fontsize * i)
var y = text[i] * fontsize
ctx.fillText(str[index], x, y);
if (y > H && Math.random() > 0.99) {
text[i] = 0
}
text[i]++;
}
}
function randColor() {
var r = Math.ceil(Math.random() * 155) + 100;
var g = Math.ceil(Math.random() * 155) + 100;
var b = Math.ceil(Math.random() * 155) + 100;
return 'rgb(' + r + ',' + g + ',' + b + ')'
}
console.log(randColor())
var timer = setInterval(function() {
draw();
}, 1000 / 30)
}
</script>
</head>
<body data-gr-c-s-loaded="true">
<div class="" style="display:relative;">
<div id="contentDiv"style="margin: 0;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
height:100px;width:100px; color: aqua;
z-index:99999;">
fdsafdsa
</div>
<div>
<canvas id="mycanvas" style="position:absolute;z-index:-1;">
</div>
</div>
</body>
</html>

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<!-- Action for model, tag was point by js call -->
<record id="act_client@console" model="ir.actions.client">
<field name="name">Console</field>
<field name="tag">open_code_dashboard</field>
</record>
<menuitem name="Console" id="menu@root_console" parent="menu@root" sequence="0"
action="act_client@console"/>
</odoo>

View file

@ -0,0 +1,11 @@
<?xml version="1.0" ?>
<odoo><data>
<template id="kit_code.code_effect_js" >
<script src="/kit_code/static/src/js/effect.js"></script>
<script type="text/javascript">
<!-- console.debug('❌ effect.xml') -->
</script>
</template>
</data></odoo>

View file

@ -0,0 +1,90 @@
<?xml version="1.0" ?>
<odoo><data>
<template id = "kit_code.home_page" >
<div class="" style="display:relative;">
<div id="contentDiv" style="margin: 0;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: aqua;
z-index:99999;">
<t t-call='kit_code.home_page_feature'></t>
</div>
<div>
<canvas id="codeStreamCanvas" style="position:absolute;z-index:-1;"/>
</div>
</div>
<t t-call='kit_code.code_effect_js'></t>
</template>
<template id="kit_code.home_page_feature" >
<t t-set="tracer" t-value='not true'></t>
<t t-set='codeServerIsRunning' t-value="(not codeServer is None and codeServer.get('status') == 'running') "/>
<div style=' display: inline-flex; height:100%;width:100%;flex-direction: column; justify-content: center;align-items: center; font-size:25px; '>
<div t-if="error" style="color:red; ">
<div><t t-out="error"/></div>
<div><t t-out="codeServer"/></div>
<div><t t-out="nextUrl"/></div>
</div>
<div t-if="not error">
<div style=" justify-content: center;align-items: center; " >
<div t-if="nextUrl" style="display:none">
<!-- debug -->
<span id='nextUrl' ><t t-out="nextUrl"/></span>
</div>
<div t-if="codeServerIsRunning" >
Loading <span style="font-weight: bold; "><t t-out="moduleName"/></span> ...
</div>
</div>
<div t-if=" not codeServerIsRunning" >
Code Server not running, launch now?
<div align='center' style="margin-top:10px;">
<span style="cursor:pointer; font-weight: bold; color:yellow; font-size:25px;" onclick='onClickStart()' >Start⚡</span>
</div>
</div>
</div>
<div t-if="tracer" style="background:blue;color:white">
<div>error:<t t-out="error"/></div>
<div>codeServer:<t t-out="codeServer"/></div>
<div>nextUrl:<t t-out="nextUrl"/></div>
<div>startUrl:<t t-out="startUrl"/></div>
</div>
</div>
<script t-if="codeServerIsRunning and not nextUrl is None">
function go(){
let forwardUrl = '<t t-out="nextUrl"/>'
//console.log('forwardUrl?', forwardUrl)
if (! forwardUrl || '' === forwardUrl){
return ;
}
forwardUrl = decodeURIComponent(forwardUrl)
forwardUrl = forwardUrl.replace('amp;','')
window.location.href = forwardUrl
}
setTimeout(()=>{
go();
},1000*1)
</script>
<script t-if="not codeServerIsRunning">
function onClickStart(){
let forwardUrl = '<t t-out="startUrl"/>'
//forwardUrl = decodeURIComponent(forwardUrl)
location.href = forwardUrl;
}
//console.debug('☁️ start....')
</script>
<!-- <t t-call='kit_code.code_effect_js'></t> -->
</template>
</data></odoo>

View file

@ -0,0 +1,21 @@
Odoo Spy🕵
==================
* Odoo Spy🕵 help you insight into the rendering templates of the Odoo web page,speed up development process🚀
============
Team
-------
ixkit.com
Artificer@ixkit.com
Maintainer
==========
This module was maintained by ixkit Team.
For support and more information, please visit https://www.ixkit.com/odookit
Further information
===================
HTML Description: `<static/description/index.html>`

View file

@ -0,0 +1,6 @@
{
"hook_paths_test" : ["/web#","/odookit","/about-us","/contactus"],
"hook_paths" : ["*"]
}

View file

@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
import os
import sys
from . import land
from . import controllers
from . import models
from .__support__ import get_config_file,setup_tracer
from .land import config
from .land.trace import Tracer
from .odo import runtime
from .hooks import module_pre_init_hook, module_post_init_hook,module_uninstall_hook,set_hook_url_paths
from .odo import start_spy
def main_init():
config_data = config.load(get_config_file())
set_hook_url_paths(config_data.get('hook_paths'))
start_spy()
#-- main ---
main_init()

View file

@ -0,0 +1,98 @@
# -*- coding: utf-8 -*-
#
# ixkit - odoo spy
#
#@team : ixkit
#@author : Artificer@ixkit.com
#@date : 2024-3-15
#@version : 1.0.0
#
#-----------------------------------------------------------------------
{
"name": "Odoo Spy🕵",
"version": "18.0.1.0.0",
"summary": """
Help you insight into the rendering Templates of the Odoo web page,light up the template snippets,speed up development process🚀
""",
"description":"Spy the rendering Templates structure",
'category': 'Extra Tools/ixkit',
'author': 'ixkit',
'company': 'ixkit',
'maintainer': 'ixkit',
'website': "http://www.ixkit.com/odookit",
"live_test_url": "http://www.ixkit.com/odookit",
"license": "OPL-1",
"support": "odoo@ixkit.com",
"depends": [
'base', 'web', 'website'
],
"data": [
],
"assets": {
"kit_spy._assets_extends_owl": [
"kit_spy/static/src/owl/call_tracer.js",
"kit_spy/static/src/owl/spy.js",
"kit_spy/static/src/owl/owl_track_xml.js",
# ("replace", "web/static/lib/owl/owl.js", "kit_spy/static/src/owl/17/owl.js"),
"kit_spy/static/src/owl/owl_meta_builder.js",
"kit_spy/static/src/owl/owl_override.js",
],
"web.assets_backend": [
("include", "kit_spy._assets_extends_owl"),
"kit_spy/static/jslib/underscore/underscore.js",
"kit_spy/static/src/tooltip/spy_popover.js",
"kit_spy/static/src/tooltip/spy_tooltip.v17.js",
"kit_spy/static/src/tooltip/service/tooltip.js",
"kit_spy/static/src/tooltip/service/tooltip_service.js",
"kit_spy/static/src/tooltip/service/spy_tooltip.scss",
"kit_spy/static/src/tooltip/xml/spy_tooltip.xml",
# extends website editor
("before","website/static/src/systray_items/edit_website.js", "kit_spy/static/src/website/systray_items/spy.website.systray.js")
],
'web.assets_frontend_lazy': [
"kit_spy/static/src/website/spy.snippets.animation.v17.js",
],
"web.assets_frontend": [
("include", "kit_spy._assets_extends_owl"),
"kit_spy/static/src/website/spy.snippets.animation.v17.js",
"kit_spy/static/jslib/underscore/underscore.js",
"kit_spy/static/src/tooltip/spy_popover.js",
"kit_spy/static/src/tooltip/spy_tooltip.v17.js",
"kit_spy/static/src/tooltip/service/tooltip.js",
"kit_spy/static/src/tooltip/service/tooltip_service.js",
"kit_spy/static/src/tooltip/service/spy_tooltip.scss",
"kit_spy/static/src/tooltip/xml/spy_tooltip.xml",
],
},
'images': [
'static/description/banner.png'
],
'pre_init_hook': 'module_pre_init_hook',
'post_init_hook': 'module_post_init_hook',
'uninstall_hook': 'module_uninstall_hook',
'installable': True,
'application': True,
'auto_install': False,
}

View file

@ -0,0 +1,53 @@
# -*- coding: utf-8 -*-
import os
import sys
from .land import config
from .land.trace import Tracer
def get_config_file():
my_dir = os.path.dirname(os.path.abspath(__file__))
return my_dir + "/__config__.py"
def get_mainifset_file():
my_dir = os.path.dirname(os.path.abspath(__file__))
return my_dir + "/__manifest__.py"
def get_mainifset():
mainifset_data = config.load(get_mainifset_file())
return mainifset_data
def get_mainifset_version():
mainifset_data = get_mainifset()
version = mainifset_data.get('version')
first_dot_index = version.index(".")
big_ver = version[0:first_dot_index]
return big_ver
#TODO
def get_support_major_version():
return '17'
def setup_tracer(level=None):
trace_level = level
while trace_level is None:
runtime_options = sys.modules['runtime_options']
if runtime_options is None:
break
trace_level = runtime_options.config.get('trace_level')
if trace_level is None:
break
break
Tracer.set_trace_level(trace_level)
#--
setup_tracer(False)
#setup_tracer('DEBUG')

View file

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
from . import main

View file

@ -0,0 +1,42 @@
# -*- coding: utf-8 -*-
import json
import logging
from odoo import http
from odoo.tools import date_utils
from odoo.http import request
from odoo import api, SUPERUSER_ID
from odoo.addons.web.controllers.utils import ensure_db
from ..odo import clean_cache
_logger = logging.getLogger(__name__)
def cleanQWebCache(env):
if True:
new_cr = env.registry.cursor()
env = api.Environment(new_cr, SUPERUSER_ID, {})
#env["ir.qweb"].clear_caches()
clean_cache(env)
class Main(http.Controller):
@http.route(
"/api/v1/spy/reset",
type="http",
auth="none",
csrf=False,
)
def reset(self, **kwargs):
ensure_db()
cleanQWebCache(http.request.env)
data = {
"success" : True
}
return request.make_json_response(data, status=200)

View file

@ -0,0 +1,39 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
* {
margin: 0px ;
padding: 0px;
width: 100px;
}
#menu-checkbox{
display: none;
}
#menu-checkbox:checked ~ .tag-list{
display: none;
}
li{
float: left;
margin: 2px;
list-style-type: none;
}
a {
text-decoration: none;
}
</style>
</head>
<body>
<label for="menu-checkbox">主菜单</label>
<input id="menu-checkbox" type="checkbox" name="" value="">
<div class="tag-list">
<ul>
<li><a href="#">首页</a> </li>
<li><a href="#">正文</a> </li>
<li><a href="#">设置</a> </li>
</ul>
</div>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

View file

@ -0,0 +1,90 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
/* .tab-content can be styled as you like */
main {
max-width: 400px;
margin: 0 auto;
}
p {
font-family: monospace;
font-size: 13px;
}
summary {
font-size: 1rem;
font-weight: 600;
background-color: #f3f3f3;
color: #000;
padding: 1rem;
margin-bottom: 1rem;
outline: none;
border-radius: 0.25rem;
cursor: pointer;
position: relative;
}
details[open] summary~* {
animation: sweep .5s ease-in-out;
}
@keyframes sweep {
0% {
opacity: 0;
margin-top: -10px
}
100% {
opacity: 1;
margin-top: 0px
}
}
details>summary::after {
position: absolute;
content: "+";
right: 20px;
}
details[open]>summary::after {
position: absolute;
content: "-";
right: 20px;
}
details>summary::-webkit-details-marker {
display: none;
}
</style>
</head>
<body>
<main>
<details open>
<summary>HTML</summary>
<div class="tab-content">
<p>your text goes here</p>
</div>
</details>
<details>
<summary>CSS</summary>
<div class="tab-content">
<p>your text goes here</p>
</div>
</details>
<details>
<summary>javascript</summary>
<div class="tab-content">
<p>your text goes here</p>
</div>
</details>
</main>
</body>
</html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,12 @@
root(ComponentNode)
app(owl.App){
Root: WebClient
env:{
bus: EventBus
services:[
tooltip: //register js service
]
}
rawTemplates:
}

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,182 @@
<t name="Products" t-name="website_sale.products">
<t t-call="website.layout">
<t t-set="additional_title">Shop</t>
<t t-set="grid_block_name">Grid</t>
<t t-set="product_block_name">Product</t>
<!-- Qweb variable defining the class suffix for navbar items.
Change accordingly to the derired visual result (eg. `primary`, `dark`...)-->
<t t-set="navClass" t-valuef="light"/>
<!-- Check for active options: the stored value may be used in sub-templates too -->
<t t-set="opt_wsale_categories" t-value="is_view_active('website_sale.products_categories')"/>
<t t-set="opt_wsale_attributes" t-value="is_view_active('website_sale.products_attributes')"/>
<t t-set="opt_wsale_filter_price" t-value="is_view_active('website_sale.filter_products_price')"/>
<t t-set="opt_wsale_filter_tags" t-value="is_view_active('website_sale.filter_products_tags')"/>
<t t-set="opt_wsale_categories_top" t-value="is_view_active('website_sale.products_categories_top')"/>
<t t-set="opt_wsale_attributes_top" t-value="is_view_active('website_sale.products_attributes_top')"/>
<t t-set="website_sale_pricelists" t-value="website.get_pricelist_available(show_visible=True)"/>
<t t-set="website_sale_sortable" t-value="website._get_product_sort_mapping()"/>
<t t-set="hasLeftColumn" t-value="opt_wsale_categories or opt_wsale_attributes"/>
<t t-set="isFilteringByPrice" t-if="opt_wsale_filter_price" t-value="float_round(available_min_price, 2) != float_round(min_price, 2) or float_round(available_max_price, 2) != float_round(max_price, 2)"/>
<t t-set="hasPricelistDropdown" t-value="website_sale_pricelists and len(website_sale_pricelists)&gt;1"/>
<t t-set="isSortingBy" t-value="[sort for sort in website_sale_sortable if sort[0]==request.params.get('order', '')]"/>
<div id="wrap" class="js_sale o_wsale_products_page">
<div class="oe_structure oe_empty oe_structure_not_nearest" id="oe_structure_website_sale_products_1"/>
<div class="container oe_website_sale pt-2">
<div class="row o_wsale_products_main_row align-items-start flex-nowrap">
<aside t-if="hasLeftColumn" id="products_grid_before" class="d-none d-lg-block position-sticky col-3 px-3 clearfix">
<div class="o_wsale_products_grid_before_rail vh-100 ms-n2 mt-n2 pt-2 pe-lg-2 pb-lg-5 ps-2 overflow-y-scroll">
<div t-if="opt_wsale_categories" class="products_categories mb-3">
<t t-call="website_sale.products_categories_list"/>
</div>
<div class="products_attributes_filters">
<div id="wsale_products_attributes_collapse" class=" position-relative">
<form t-if="attributes or all_tags" class="js_attributes position-relative mb-2" method="get">
<input t-if="category" type="hidden" name="category" t-att-value="category.id"/>
<input type="hidden" name="search" t-att-value="search"/>
<input type="hidden" name="order" t-att-value="order"/>
<a t-if="attrib_values or tags" t-att-href="keep('/shop'+ ('/category/'+slug(category)) if category else None, attrib=0, tags=0)" t-attf-class="btn btn-{{navClass}} d-flex align-items-center py-1 mb-2">
<small class="mx-auto"><b>Clear Filters</b></small>
<i class="oi oi-close"/>
</a>
<t t-foreach="attributes" t-as="a">
<t t-cache="a,attrib_set">
<div class="accordion-item nav-item mb-1 border-0" t-if="a.value_ids and len(a.value_ids) &gt; 1">
<h6 class="mb-3">
<b class="o_products_attributes_title d-none d-lg-block" t-field="a.name"/>
</h6>
<div t-attf-id="o_products_attributes_{{a.id}}" class="">
<t t-if="a.display_type == 'select'">
<select class="form-select css_attribute_select mb-2" name="attrib">
<option value="" selected="true">-</option>
<t t-foreach="a.value_ids" t-as="v">
<option t-att-value="'%s-%s' % (a.id,v.id)" t-esc="v.name" t-att-selected="v.id in attrib_set"/>
</t>
</select>
</t>
<div t-elif="a.display_type == 'color'" class="mb-3">
<t t-call="website_sale.o_wsale_offcanvas_color_attribute"/>
</div>
<div t-elif="a.display_type in ('radio', 'pills', 'multi')" class="flex-column mb-3">
<t t-foreach="a.value_ids" t-as="v">
<div class="form-check mb-1">
<input type="checkbox" name="attrib" class="form-check-input" t-att-id="'%s-%s' % (a.id,v.id)" t-att-value="'%s-%s' % (a.id,v.id)" t-att-checked="'checked' if v.id in attrib_set else None"/>
<label class="form-check-label fw-normal" t-att-for="'%s-%s' % (a.id,v.id)" t-field="v.name"/>
</div>
</t>
</div>
</div>
</div>
</t>
</t>
<t t-if="opt_wsale_filter_tags and opt_wsale_attributes" t-call="website_sale.filter_products_tags">
<t t-set="all_tags" t-value="all_tags"/>
</t>
</form>
</div></div>
<t t-if="opt_wsale_filter_price and opt_wsale_attributes" t-call="website_sale.filter_products_price"/>
</div>
</aside>
<div id="products_grid" t-attf-class="#{'o_wsale_layout_list' if layout_mode == 'list' else ''} {{'col-lg-9' if hasLeftColumn else 'col-12'}}">
<t t-call="website_sale.products_breadcrumb">
<t t-set="_classes" t-valuef="d-none d-lg-flex w-100 p-0 small"/>
</t>
<div class="products_header btn-toolbar flex-nowrap align-items-center justify-content-between gap-3 mb-3">
<t t-if="is_view_active('website_sale.search')" t-call="website_sale.search">
<t t-set="search" t-value="original_search or search"/>
<t t-set="_form_classes" t-valuef="d-lg-inline {{'d-inline' if not category else 'd-none'}}"/>
</t>
<t t-call="website_sale.pricelist_list" t-cache="pricelist">
<t t-set="_classes" t-valuef="d-none d-lg-inline"/>
</t>
<t t-if="is_view_active('website_sale.sort')" t-call="website_sale.sort">
<t t-set="_classes" t-valuef="d-none d-lg-inline-block"/>
</t>
<div t-if="category" class="d-flex align-items-center d-lg-none me-auto">
<t t-if="not category.parent_id" t-set="backUrl" t-valuef="/shop"/>
<t t-else="" t-set="backUrl" t-value="keep('/shop/category/' + slug(category.parent_id), category=0)"/>
<a t-attf-class="btn btn-{{navClass}} me-2" t-att-href="category.parent_id and keep('/shop/category/' + slug(category.parent_id), category=0) or '/shop'">
<i class="fa fa-angle-left"/>
</a>
<h4 t-out="category.name" class="mb-0 me-auto"/>
</div>
<t t-if="is_view_active('website_sale.add_grid_or_list_option')" t-call="website_sale.add_grid_or_list_option">
<t t-set="_classes" t-valuef="d-flex"/>
</t>
<button t-if="is_view_active('website_sale.sort') or opt_wsale_categories or opt_wsale_attributes or opt_wsale_attributes_top" t-attf-class="btn btn-{{navClass}} position-relative {{not opt_wsale_attributes_top and 'd-lg-none'}}" data-bs-toggle="offcanvas" data-bs-target="#o_wsale_offcanvas">
<i class="fa fa-sliders"/>
<span t-if="isFilteringByPrice or attrib_set or tags" t-attf-class="position-absolute top-0 start-100 translate-middle border border-{{navClass}} rounded-circle bg-danger p-1"><span class="visually-hidden">filters active</span></span>
</button>
</div>
<t t-if="opt_wsale_categories_top" t-call="website_sale.filmstrip_categories"/>
<div t-if="original_search and products" class="alert alert-warning mt8">
No results found for '<span t-esc="original_search"/>'. Showing results for '<span t-esc="search"/>'.
</div>
<t t-if="category">
<t t-set="editor_msg">Drag building blocks here to customize the header for "<t t-esc="category.name"/>" category.</t>
<div class="mb16" id="category_header" t-att-data-editor-message="editor_msg" t-field="category.website_description"/>
</t>
<div t-if="products" class="o_wsale_products_grid_table_wrapper pt-3 pt-lg-0">
<table class="table table-borderless h-100 m-0 o_wsale_context_thumb_cover" t-att-data-ppg="ppg" t-att-data-ppr="ppr" t-att-data-default-sort="website.shop_default_sort" t-att-data-name="grid_block_name">
<colgroup t-ignore="true">
<!-- Force the number of columns (useful when only one row of (x < ppr) products) -->
<col t-foreach="ppr" t-as="p"/>
</colgroup>
<tbody>
<tr t-foreach="bins" t-as="tr_product">
<t t-foreach="tr_product" t-as="td_product">
<t t-if="td_product">
<!-- We use t-attf-class here to allow easier customization -->
<td t-att-colspan="td_product['x'] != 1 and td_product['x']" t-att-rowspan="td_product['y'] != 1 and td_product['y']" t-attf-class="oe_product" t-att-data-ribbon-id="td_product['ribbon'].id" t-att-data-name="product_block_name">
<div t-attf-class="o_wsale_product_grid_wrapper position-relative h-100 o_wsale_product_grid_wrapper_#{td_product['x']}_#{td_product['y']}">
<t t-call="website_sale.products_item">
<t t-set="product" t-value="td_product['product']"/>
</t>
</div>
</td>
</t>
<td t-else=""/>
</t>
</tr>
</tbody>
</table>
</div>
<div t-nocache="get the actual search" t-else="" class="text-center text-muted mt128 mb256">
<t t-if="not search">
<h3 class="mt8">No product defined</h3>
<p t-if="category">No product defined in this category.</p>
</t>
<t t-else="">
<h3 class="mt8">No results</h3>
<p>No results for "<strong t-esc="search"/>"<t t-if="category"> in category "<strong t-esc="category.display_name"/>"</t>.</p>
</t>
<p t-ignore="true" groups="sales_team.group_sale_manager">Click <i>'New'</i> in the top-right corner to create your first product.</p>
</div>
<div class="products_pager d-flex justify-content-center pt-5 pb-3">
<t t-call="website.pager"/>
</div>
</div>
</div>
<t t-call="website_sale.o_wsale_offcanvas"/>
</div>
<div class="oe_structure oe_empty oe_structure_not_nearest" id="oe_structure_website_sale_products_2"/>
</div>
</t>
</t>

View file

@ -0,0 +1,4 @@
_request_stack = werkzeug.local.LocalStack()
request = _request_stack()
debug = request and request.session.debug

View file

@ -0,0 +1 @@
<div role="tooltip" class="o_popover popover mw-100 shadow-sm bs-popover-bottom o-popover-bottom o-popover--be" popover-id="13" style="position: fixed; top: 40px; left: 634.444px;"><div class="popover-arrow start-auto"></div><div class="o-tooltip px-2 py-1"><div class="o-tooltip"><div class="o-tooltip--string" role="tooltip"> Template : web.Dropdown</div><ul class="o-tooltip--technical" role="tooltip"><li data-item="object"><span class="o-tooltip--technical--title">name:</span>web.Dropdown</li><li data-item="object"><span class="o-tooltip--technical--title">class:</span>sub x template</li><li data-item="view_type"><span class="o-tooltip--technical--title">type:</span>owl</li><li data-item="special"><span style="cursor:pointer"><span class="o-tooltip--technical--title">url:</span>/web/static/src/core/dropdown/dropdown.xml</span></li><li data-item="special"><span style="cursor:pointer"><span class="o-tooltip--technical--title">file:</span>/Users/icoco/WorkSpace/2022/prj/python/odoo/odoo-16.0/addons/web/static/src/core/dropdown/dropdown.xml</span></li></ul></div></div></div>

View file

@ -0,0 +1,44 @@
{
"id": 181,
"is_seo_optimized": false,
"website_meta_title": false,
"website_meta_description": false,
"website_meta_keywords": false,
"website_meta_og_img": false,
"seo_name": false,
"name": "Brand Promotion Message",
"model": false,
"key": "web.brand_promotion_message",
"priority": 16,
"type": "qweb",
"arch": "<t name=&quot;Brand Promotion Message&quot; t-name=&quot;web.brand_promotion_message&quot;>\n <t t-set=&quot;odoo_logo&quot;>\n <a target=&quot;_blank&quot; t-attf-href=&quot;http://www.odoo.com?utm_source=db&amp;utm_medium=#{_utm_medium}&quot; class=&quot;badge text-bg-light&quot;>\n <img alt=&quot;Odoo&quot; src=&quot;/web/static/img/odoo_logo_tiny.png&quot; width=&quot;62&quot; height=&quot;20&quot; style=&quot;width: auto; height: 1em; vertical-align: baseline;&quot;/>\n </a>\n </t>\n <t t-set=&quot;final_message&quot;>Powered by %s%s</t>\n <t t-out=&quot;final_message % (odoo_logo, _message and (\"- \" + _message) or \"\")&quot;/>\n </t>\n ",
"arch_base": "<t name=&quot;Brand Promotion Message&quot; t-name=&quot;web.brand_promotion_message&quot;>\n <t t-set=&quot;odoo_logo&quot;>\n <a target=&quot;_blank&quot; t-attf-href=&quot;http://www.odoo.com?utm_source=db&amp;utm_medium=#{_utm_medium}&quot; class=&quot;badge text-bg-light&quot;>\n <img alt=&quot;Odoo&quot; src=&quot;/web/static/img/odoo_logo_tiny.png&quot; width=&quot;62&quot; height=&quot;20&quot; style=&quot;width: auto; height: 1em; vertical-align: baseline;&quot;/>\n </a>\n </t>\n <t t-set=&quot;final_message&quot;>Powered by %s%s</t>\n <t t-out=&quot;final_message % (odoo_logo, _message and (\"- \" + _message) or \"\")&quot;/>\n </t>\n ",
"arch_db": "<t name=&quot;Brand Promotion Message&quot; t-name=&quot;web.brand_promotion_message&quot;>\n <t t-set=&quot;odoo_logo&quot;>\n <a target=&quot;_blank&quot; t-attf-href=&quot;http://www.odoo.com?utm_source=db&amp;utm_medium=#{_utm_medium}&quot; class=&quot;badge text-bg-light&quot;>\n <img alt=&quot;Odoo&quot; src=&quot;/web/static/img/odoo_logo_tiny.png&quot; width=&quot;62&quot; height=&quot;20&quot; style=&quot;width: auto; height: 1em; vertical-align: baseline;&quot;/>\n </a>\n </t>\n <t t-set=&quot;final_message&quot;>Powered by %s%s</t>\n <t t-out=&quot;final_message % (odoo_logo, _message and (\"- \" + _message) or \"\")&quot;/>\n </t>",
"arch_fs": "web/views/webclient_templates.xml",
"arch_updated": false,
"arch_prev": "<t name=&quot;Brand Promotion Message&quot; t-name=&quot;web.brand_promotion_message&quot;>\n <t t-set=&quot;odoo_logo&quot;>\n <a target=&quot;_blank&quot; t-attf-href=&quot;http://www.odoo.com?utm_source=db&amp;utm_medium=#{_utm_medium}&quot; class=&quot;badge text-bg-light&quot;>\n <img alt=&quot;Odoo&quot; src=&quot;/web/static/img/odoo_logo_tiny.png&quot; width=&quot;62&quot; height=&quot;20&quot; style=&quot;width: auto; height: 1em; vertical-align: baseline;&quot;/>\n </a>\n </t>\n <t t-set=&quot;final_message&quot;>Powered by %s%s</t>\n <t t-out=&quot;final_message % (odoo_logo, _message and (\"- \" + _message) or \"\")&quot;/>\n </t>",
"inherit_id": "ir.ui.view()",
"inherit_children_ids": "ir.ui.view(2573,)",
"field_parent": false,
"model_data_id": "ir.model.data(5280,)",
"xml_id": "web.brand_promotion_message",
"groups_id": "res.groups()",
"mode": "primary",
"active": true,
"__last_update": "datetime.datetime(2024, 2, 27, 9, 23, 51, 656358)",
"display_name": "Brand Promotion Message",
"create_uid": "res.users(1,)",
"create_date": "datetime.datetime(2024, 1, 26, 15, 12, 26, 758238)",
"write_uid": "res.users(1,)",
"write_date": "datetime.datetime(2024, 2, 27, 9, 23, 51, 656358)",
"customize_show": false,
"website_id": "website()",
"page_ids": "website.page()",
"first_page_id": "website.page()",
"track": false,
"visibility": false,
"visibility_password": false,
"visibility_password_display": "",
"theme_template_id": "theme.ir.ui.view()"
}

View file

@ -0,0 +1,230 @@
POST: http://localhost:8120/web/action/load
request: {"id":0,"jsonrpc":"2.0","method":"call","params":{"action_id":609,"additional_context":{}}}
response: {
"jsonrpc": "2.0",
"id": 0,
"result": {
"id": 609,
"name": "Base",
"type": "ir.actions.act_window",
"xml_id": "anvil_base.action_window_anvil_base_regular",
"help": false,
"binding_model_id": false,
"binding_type": "action",
"binding_view_types": "list,form",
"display_name": "Base",
"view_id": false,
"domain": false,
"context": "{}",
"res_id": 0,
"res_model": "anvil.base",
"target": "current",
"view_mode": "list,kanban,form",
"mobile_view_mode": "kanban",
"views": [
[
false,
"list"
],
[
false,
"kanban"
],
[
false,
"form"
]
],
"limit": 80,
"groups_id": [],
"search_view_id": false,
"filter": false
}
}
POST: http://localhost:8120/web/dataset/call_kw/anvil.base/get_views
request:
{"id":3,"jsonrpc":"2.0","method":"call","params":{"model":"anvil.base","method":"get_views","args":[],"kwargs":{"context":{"lang":"en_US","tz":"Europe/Brussels","uid":2,"allowed_company_ids":[1]},"views":[[false,"list"],[false,"kanban"],[false,"form"],[false,"search"]],"options":{"action_id":609,"load_filters":true,"toolbar":true}}}}
response:
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"views": {
"list": {
"arch": "<tree>\n <field name=\"name\"/>\n <field name=\"value\" on_change=\"1\"/> \n <field name=\"value2\"/>\n </tree>",
"id": 2545,
"model": "anvil.base",
"toolbar": {}
},
"kanban": {
"arch": "<kanban string=\"anvil.base\"><templates><t t-name=\"kanban-box\"><div t-attf-class=\"oe_kanban_card oe_kanban_global_click\"><div class=\"o_kanban_card_content\"><field name=\"name\"/></div></div></t></templates></kanban>",
"id": false,
"model": "anvil.base",
"toolbar": {}
},
"form": {
"arch": "<form string=\"AnvilBase\">\n <sheet style=\"background-color:red\">\n <!-- <group col=\"2\" string=\"Page Details\">\n <group colspan=\"1\" string=\"Page\">\n <field name=\"name\" />\n <field name=\"value\"/>\n <label for=\"name_slugified\" string=\"URL\"/>\n <div>\n <span>/value2/</span>\n <field name=\"value2\" nolabel=\"1\" />\n \n </div>\n \n </group>\n \n </group> -->\n <notebook>\n <page string=\"View\">\n <group> \n <div style=\"background-color:red\">\n <field name=\"name\"/>\n </div> \n </group>\n </page>\n <page string=\"Value\">\n <div style=\"background-color:yellow\">\n <field name=\"value\"/>\n </div> \n </page>\n </notebook>\n </sheet>\n </form>",
"id": 2554,
"model": "anvil.base",
"toolbar": {}
},
"search": {
"arch": "<search string=\"anvil.base\"><field name=\"name\"/></search>",
"id": false,
"model": "anvil.base",
"toolbar": {},
"filters": []
}
},
"models": {
"anvil.base": {
"name": {
"change_default": false,
"name": "name",
"readonly": false,
"required": false,
"searchable": true,
"sortable": true,
"store": true,
"string": "Name",
"translate": false,
"trim": true,
"type": "char"
},
"value": {
"change_default": false,
"group_operator": "sum",
"name": "value",
"readonly": false,
"required": false,
"searchable": true,
"sortable": true,
"store": true,
"string": "Value",
"type": "integer"
},
"value2": {
"change_default": false,
"group_operator": "sum",
"name": "value2",
"readonly": true,
"required": false,
"searchable": true,
"sortable": true,
"store": true,
"string": "Value2",
"type": "float"
},
"description": {
"change_default": false,
"name": "description",
"readonly": false,
"required": false,
"searchable": true,
"sortable": true,
"store": true,
"string": "Description",
"translate": false,
"type": "text"
},
"id": {
"change_default": false,
"name": "id",
"readonly": true,
"required": false,
"searchable": true,
"sortable": true,
"store": true,
"string": "ID",
"type": "integer"
},
"display_name": {
"change_default": false,
"name": "display_name",
"readonly": true,
"required": false,
"searchable": false,
"sortable": false,
"store": false,
"string": "Display Name",
"translate": false,
"trim": true,
"type": "char"
},
"create_uid": {
"change_default": false,
"context": {},
"domain": [],
"name": "create_uid",
"readonly": true,
"relation": "res.users",
"required": false,
"searchable": true,
"sortable": true,
"store": true,
"string": "Created by",
"type": "many2one"
},
"create_date": {
"change_default": false,
"name": "create_date",
"readonly": true,
"required": false,
"searchable": true,
"sortable": true,
"store": true,
"string": "Created on",
"type": "datetime"
},
"write_uid": {
"change_default": false,
"context": {},
"domain": [],
"name": "write_uid",
"readonly": true,
"relation": "res.users",
"required": false,
"searchable": true,
"sortable": true,
"store": true,
"string": "Last Updated by",
"type": "many2one"
},
"write_date": {
"change_default": false,
"name": "write_date",
"readonly": true,
"required": false,
"searchable": true,
"sortable": true,
"store": true,
"string": "Last Updated on",
"type": "datetime"
}
}
}
}
}
POST:
http://localhost:8120/web/dataset/call_kw/anvil.base/web_read
request:
{"id":5,"jsonrpc":"2.0","method":"call","params":{"model":"anvil.base","method":"web_read","args":[[2]],"kwargs":{"context":{"lang":"en_US","tz":"Europe/Brussels","uid":2,"allowed_company_ids":[1],"bin_size":true,"params":{"id":2,"cids":1,"menu_id":413,"action":609,"model":"anvil.base","view_type":"form"}},"specification":{"name":{},"value":{},"display_name":{}}}}}
response:
{
"jsonrpc": "2.0",
"id": 5,
"result": [
{
"id": 2,
"name": "xxxxxx",
"value": 0,
"display_name": "xxxxxx"
}
]
}

View file

@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
import sys
from .odo.runtime import get_config,set_config
from .odo import start_spy, end_spy, clean_cache
def module_pre_init_hook(env):
start_spy()
def module_post_init_hook(env):
clean_cache(env)
def module_uninstall_hook(env):
clean_cache(env)
end_spy()
def set_hook_url_paths(paths=None):
hook_url_paths = get_config('hook_url_paths')
if hook_url_paths is None:
hook_url_paths = []
if not paths is None:
hook_url_paths = hook_url_paths + paths
set_config('hook_url_paths',hook_url_paths)
return True

View file

@ -0,0 +1,7 @@
# -*- coding: utf-8 -*-
from . import lang
from . import trace
from . import jsons
def smart_json(val):
jsons.SmartJson(val)

View file

@ -0,0 +1,8 @@
# -*- coding: utf-8 -*-
import os
from ..jsons import load_py_json
def load(path):
return load_py_json(path)

View file

@ -0,0 +1,38 @@
# -*- coding: utf-8 -*-
import json
from .__smart_json__ import SmartJson
# remove comment
def load_py_to_json_str(file_name):
buf = None
with open(file_name, 'r') as file:
Lines = file.readlines()
for line in Lines:
if line.startswith("#"):
continue
else:
if buf is None:
buf = line
else:
buf = buf + line
return buf
def load_py_json(file_name):
buf = load_py_to_json_str(file_name)
if not buf is None:
data = json.loads(buf)
return data
return None
def load_json(file_name):
with open(file_name, 'r') as fcc_file:
data = json.load(fcc_file)
return data
return None
def to_json(obj):
data = json.load(obj)
return data

View file

@ -0,0 +1,368 @@
"""
Author : J. Koffi ONIPOH
Version : 2.0.2
email: jolli644@gmail.com
"""
import datetime
import json
import os
from collections import OrderedDict
from copy import deepcopy
class SmartJson:
def __init__(self, cls=None):
"""
:param cls:
"""
self.__copy = cls
self.__classe = deepcopy(cls)
self.___obj = None
if cls:
self.classname = cls.__class__.__name__
def serialize(self, pretty=True):
"""
:param pretty:
:return:
"""
try:
if isinstance(self.__classe, dict):
return SmartJson._DictConversion(self.__classe).serialize(pretty)
elif isinstance(self.__classe, list):
return SmartJson._ListConversion(self.__classe).serialize(pretty)
elif isinstance(self.__classe, (int, float, bool, str, type(None))):
if pretty:
return json.dumps(self.__classe, indent=2, sort_keys=True)
else:
return json.dumps(self.__classe, sort_keys=True)
elif isinstance(self.__classe, (tuple, complex, datetime.date, datetime.datetime, OrderedDict)):
if pretty:
return json.dumps(self._JsonConvert().json_convert(self.__classe), indent=2, sort_keys=True)
else:
return json.dumps(self._JsonConvert().json_convert(self.__classe), sort_keys=True)
elif isinstance(self.__classe, object):
if SmartJson._JsonConvert().get_class_name(self.__classe) == "enum.EnumMeta":
return SmartJson._EnumConversion(self.__classe).serialize(pretty)
else:
self.___obj = SmartJson._DataTypeConversion(self.__classe).next()
if pretty:
return json.dumps({'' + self.classname: self._serialize(self.___obj).__dict__}, indent=2,
sort_keys=True)
else:
return json.dumps({'' + self.classname: self._serialize(self.___obj).__dict__}, sort_keys=True)
except TypeError as e:
SmartJson._UnsupportedClass((type(self.___obj).__name__), e)
def serializeToJsonFile(self, directory="output", filename="smart.json"):
"""
:param pretty:
:return:
"""
try:
os.makedirs(directory)
except OSError:
if not os.path.isdir(directory):
raise
try:
if isinstance(self.__classe, (
int, float, bool, str, type(None), dict, tuple, complex, datetime.date, datetime.datetime,
OrderedDict)) or SmartJson._JsonConvert().get_class_name(self.__classe) == "enum.EnumMeta":
with open(directory + "/" + filename, 'w') as outfile:
json.dump(json.loads(self.serialize(pretty=True)), outfile, indent=2, sort_keys=True)
elif isinstance(self.__classe, object):
self.___obj = SmartJson._DataTypeConversion(self.__classe).next()
if filename != "smart.json":
with open(directory + "/" + filename, 'w') as outfile:
json.dump(json.loads(
json.dumps({'' + self.classname: self._serialize(self.___obj).__dict__}, indent=2,
sort_keys=True)), outfile, indent=2, sort_keys=True)
else:
with open(directory + "/" + self.classname + ".json", 'w') as outfile:
json.dump(json.loads(
json.dumps({'' + self.classname: self._serialize(self.___obj).__dict__}, indent=2,
sort_keys=True)), outfile, indent=2, sort_keys=True)
except TypeError as e:
SmartJson._UnsupportedClass((type(self.___obj).__name__), e)
def toObjectFromFile(self, jsonFile):
"""
:param jsonFile:
:param obj_name:
:return:
"""
with open(jsonFile) as outfile:
dic = json.load(outfile)
return SmartJson._KObject(dic)
def toObject(self, _json):
"""
:param _json:
:param obj_name:
:return:
"""
dic = None
if isinstance(_json, str):
dic = json.loads(_json)
elif isinstance(_json, dict):
dic = _json
return SmartJson._KObject(dic)
def getClass(self):
return self.__copy
def _serialize(self, obj):
for attr, value in vars(obj).items():
if hasattr(value, "__class__"):
if isinstance(value, (
int, float, bool, complex, list, tuple, str, OrderedDict, dict,
datetime.datetime, datetime.date, bytes, type(None))):
continue
elif SmartJson._JsonConvert().get_class_name(value) == "builtins.dict":
continue
else:
obj.__setattr__(attr, value.__dict__)
self._serialize(value)
return obj
class _DataTypeConversion:
def __init__(self, cls):
self.___cls = cls
self._json_cvt = SmartJson._JsonConvert()
def next(self):
try:
return self.__next(self.___cls)
except TypeError as e:
SmartJson._UnsupportedClass((type(self.___cls).__name__), e)
def __next(self, cls):
for attr, value in vars(cls).items():
if hasattr(value, "__class__"):
if isinstance(value, (datetime.datetime, datetime.date, complex)):
cls.__setattr__(attr, self._json_cvt.json_convert(value))
elif isinstance(value, (int, float, bool, str)):
continue
elif isinstance(value, (list, tuple)):
cls.__setattr__(attr, list((self._json_cvt.json_convert(v) for v in value)))
elif self._json_cvt.get_class_name(value) == "collections.deque":
cls.__setattr__(attr, self._json_cvt.json_convert(OrderedDict(value)))
elif self._json_cvt.get_class_name(value) == "enum.EnumMeta":
cls.__setattr__(attr, [SmartJson._EnumConversion(value).convert()])
elif isinstance(value, type(None)):
cls.__setattr__(attr, "")
elif isinstance(value, bytes):
cls.__setattr__(attr, value.decode("utf-8"))
elif isinstance(value, dict):
obj = [SmartJson._DictConversion(value).convert()]
cls.__setattr__(attr, obj)
else:
self.__next(value)
return cls
class _UnsupportedClass(Exception):
def __init__(self, message, errors):
# Call the base class constructor with the parameters it needs
super().__init__(message)
# Now for your custom code...
self.errors = errors
print("Error : 228 , UnsupportedClass : %s " % (message), errors)
class _KObject(object):
def __init__(self, d):
for a, b in d.items():
if isinstance(b, (list, tuple)):
setattr(self, a, [self.__class__(x) if isinstance(x, dict) else x for x in b])
elif isinstance(b, str):
try:
setattr(self, a, datetime.datetime.strptime(b, "%Y-%m-%d %H:%M:%S.%f"))
except ValueError:
setattr(self, a, b)
else:
setattr(self, a, self.__class__(b) if isinstance(b, dict) else b)
class _EnumConversion:
def __init__(self, myEnum):
self.__myEnum = deepcopy(myEnum)
self._json_cvt = SmartJson._JsonConvert()
def serialize(self, pretty):
if pretty:
return json.dumps(self.convert(), indent=2, sort_keys=True)
else:
return json.dumps(self.convert())
def convert(self):
if self._json_cvt.get_class_name(self.__myEnum) == "enum.EnumMeta":
converts = {}
for attr, value in vars(self.__myEnum).items():
if "_member_names_" == attr:
for member in value:
# var = self.__myEnum.__getattribute__(self.__myEnum, member).value
converts[member] = self.__myEnum[member].value
return SmartJson._DictConversion(converts).convert()
else:
SmartJson._UnsupportedClass((type(self.__myEnum).__name__), "This type of enum not support")
class _ListConversion:
def __init__(self, myList):
self.__myList = deepcopy(myList)
self._json_cvt = SmartJson._JsonConvert()
def serialize(self, pretty):
if pretty:
return json.dumps(self.convert(), indent=2, sort_keys=True)
else:
return json.dumps(self.convert())
def convert(self):
convert_result = []
for attr in self.__myList:
if isinstance(attr, (datetime.date, datetime.datetime)):
convert_result.append(str(attr))
elif isinstance(attr, bytes):
convert_result.append(attr.decode("utf-8"))
elif isinstance(attr, (int, float, bool, str, type(None))):
convert_result.append(attr)
elif isinstance(attr, (list, tuple, set)):
convert_result.append([self._json_cvt.json_convert(item) for item in attr])
elif isinstance(attr, OrderedDict):
convert_result.append(self._json_cvt.json_convert(attr))
elif isinstance(attr, complex):
convert_result.append(self._json_cvt.json_convert(attr))
elif isinstance(attr, dict):
obj = [SmartJson._DictConversion(attr).convert()]
convert_result.append(obj)
elif self._json_cvt.get_class_name(attr) == "collections.deque":
convert_result.append(self._json_cvt.json_convert(OrderedDict(attr)))
elif self._json_cvt.get_class_name(attr) == "enum.EnumMeta":
convert_result.append([SmartJson._EnumConversion(attr).convert()])
elif hasattr(attr, "__class__"):
if isinstance(attr, (int, float, bool, str, type(None))):
convert_result.append(attr)
elif isinstance(attr, (list, tuple, set)):
convert_result.append({[self._json_cvt.json_convert(item) for item in attr]})
else:
cls = attr
serialize = SmartJson._DataTypeConversion(cls).next().__dict__
convert_result.append(serialize)
return convert_result
class _DictConversion:
def __init__(self, dict):
self.__dict = deepcopy(dict)
self._json_cvt = SmartJson._JsonConvert()
def serialize(self, pretty):
if pretty:
return json.dumps(self.convert(), indent=2, sort_keys=True)
else:
return json.dumps(self.convert())
def convert(self):
for attr in self.__dict:
if isinstance(self.__dict[attr], (datetime.date, datetime.datetime)):
self.__dict[attr] = str(self.__dict[attr])
elif isinstance(self.__dict[attr], bytes):
self.__dict[attr] = self.__dict[attr].decode("utf-8")
elif isinstance(self.__dict[attr], (int, float, bool, str, type(None))):
continue
elif isinstance(self.__dict[attr], (list, tuple, set)):
self.__dict[attr] = [self._json_cvt.json_convert(item) for item in self.__dict[attr]]
elif isinstance(self.__dict[attr], OrderedDict):
self.__dict[attr] = self._json_cvt.json_convert(self.__dict[attr])
elif isinstance(self.__dict[attr], complex):
self.__dict[attr] = self._json_cvt.json_convert(self.__dict[attr])
elif self._json_cvt.get_class_name(self.__dict[attr]) == "collections.deque":
self.__dict[attr] = self._json_cvt.json_convert(OrderedDict(self.__dict[attr]))
elif self._json_cvt.get_class_name(self.__dict[attr]) == "enum.EnumMeta":
self.__dict[attr] = [SmartJson._EnumConversion(self.__dict[attr]).convert()]
elif hasattr(self.__dict[attr], "__class__"):
if isinstance(self.__dict[attr], (int, float, bool, str, type(None))):
continue
elif isinstance(self.__dict[attr], (list, tuple, set)):
self.__dict[attr] = {[self._json_cvt.json_convert(item) for item in self.__dict[attr]]}
elif isinstance(self.__dict[attr], dict):
cls = self.__dict[attr]
serialize = SmartJson._DictConversion(cls).convert()
self.__dict[attr] = serialize
else:
cls = self.__dict[attr]
serialize = SmartJson._DataTypeConversion(cls).next().__dict__
self.__dict[attr] = serialize
return self.__dict
class _JsonConvert:
self_dumpers = dict()
self_loaders = dict()
def self_dump(self, obj):
class_name = self.get_class_name(obj)
if class_name in self.self_dumpers:
return self.self_dumpers[class_name](self, obj)
raise TypeError("%r is not JSON serializable" % obj)
def json_convert(self, obj):
if isinstance(obj, OrderedDict):
try:
return self.self_dump(obj)
except TypeError:
return {k: self.json_convert(v) for k, v in self.iter_items(obj)}
# nested dict
elif isinstance(obj, dict):
return {k: self.json_convert(v) for k, v in self.iter_items(obj)}
# list or tuple
elif isinstance(obj, (list, tuple)):
return list((self.json_convert(v) for v in obj))
elif isinstance(obj, (datetime.datetime, datetime.date)):
return str(obj)
elif isinstance(obj, complex):
return [{'expression': str(obj), 'real': obj.real, 'imag': obj.imag}]
elif hasattr(obj, "__class__"):
if isinstance(obj, (int, float, bool, str, type(None))):
pass
elif isinstance(obj, complex):
return [{'expression': str(obj), 'real': obj.real, 'imag': obj.imag}]
else:
return SmartJson._DataTypeConversion(obj).next().__dict__
# single object
try:
return self.self_dump(obj)
except TypeError:
return obj
def iter_items(self, d, **kw):
return iter(d.items(**kw))
def get_class_name(self, obj):
return obj.__class__.__module__ + "." + obj.__class__.__name__

View file

@ -0,0 +1,121 @@
# -*- coding: utf-8 -*-
from . import pattern
from . import class_helper
from . import timeout
import json
import html
from ...land.trace import Tracer
_logger = Tracer
def is_primitive(x):
if isinstance(x, (int, float, bool, str, type(None))):
return True
else:
return False
def model_to_print_data(obj):
fields_dict = {}
for key in obj.fields_get():
val = obj[key]
if (is_primitive(val)):
val = str(val)
else:
try:
# val = model_to_print_data(val)
val = str(val)
except Exception as ex:
print(ex)
val = str(val)
finally:
pass
fields_dict[key] = val
return fields_dict
def to_str(val):
try:
return json.dumps(val)
except Exception as ex:
print(ex)
pass
finally:
return str(val)
def _format_template_code(value):
_logger.debug("👀->👽 ._format_template_code,template_code:{},", value)
if not value:
return ""
if value:
buf = '<t t-name="web_editor.snippet_options_image_optimization_widgets"><t t-set="filter_label">Filter</t>'
buf = value
buf = buf.replace("\"", "&?quote") # avoid json dump issue in js
buf = html.escape(buf, True)
return buf
"""
UIView
"""
def view_as_dict(view,deep=True):
_logger.debug("👀->👽 try view_as_dict, view:{},", view)
if not view or not view.id:
return None
result = {
"id": view.id,
"name": html.escape(view.name,True),
"model": view.model,
"key": view.key,
"type": view.type,
"mode": view.mode,
"arch_fs": view.arch_fs,
# "arch_db": view.arch_db, # should use this one ?
}
if not deep:
return result
# if view.arch_prev :
# result["arch_prev_formatted"] = _format_template_code(view.arch_prev)
# if view.arch :
# result["arch"] = _format_template_code(view.arch)
# step
parent_dict = None
if not view.inherit_id.id is None:
parent_dict = view_as_dict(view.inherit_id,deep=False)
if not parent_dict is None:
result["parent"] = parent_dict
# step
children_views = None
if not view.inherit_children_ids is None:
children_views = []
for x in view.inherit_children_ids:
item = view_as_dict(x,deep=False)
children_views.append(item)
if not children_views is None:
# import time
# mock_item = {
# "id": "111-" + str(time.time()),
# "name": "xxx",
# }
# children_views.append(mock_item)
result["children_views"] = children_views
_logger.debug("👀->👽 view_as_dict, result:{},", result)
return result

View file

@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
from types import MethodType
"""
replace a class method with new function dynamic
@clazz: target class
@func: could be global function that do not need to consider class style
@method_name: if special then replace it
"""
def set_class_method(clazz, func, method_name=None, back_orginal=False):
original_method = None
if (back_orginal):
original_method = getattr(clazz,method_name or func.__name__)
setattr(clazz, method_name or func.__name__, func)
return original_method
"""
not modify clazz definition only apply on the object instance
"""
def set_instance_method(target_instance, func, method_name):
new_method = MethodType(func, target_instance)
setattr(target_instance, method_name, new_method)

View file

@ -0,0 +1,11 @@
# -*- coding: utf-8 -*-
def singleton(cls):
_instance = {}
def inner():
if cls not in _instance:
_instance[cls] = cls()
return _instance[cls]
return inner

View file

@ -0,0 +1,81 @@
# -*- coding: utf-8 -*-
"""
clone from https://github.com/johejo/inputimeout
"""
import sys
DEFAULT_TIMEOUT = 30.0
INTERVAL = 0.05
SP = ' '
CR = '\r'
LF = '\n'
CRLF = CR + LF
class TimeoutOccurred(Exception):
pass
def echo(string):
sys.stdout.write(string)
sys.stdout.flush()
def posix_inputimeout(prompt='', timeout=DEFAULT_TIMEOUT):
echo(prompt)
sel = selectors.DefaultSelector()
sel.register(sys.stdin, selectors.EVENT_READ)
events = sel.select(timeout)
if events:
key, _ = events[0]
return key.fileobj.readline().rstrip(LF)
else:
echo(LF)
termios.tcflush(sys.stdin, termios.TCIFLUSH)
raise TimeoutOccurred
def win_inputimeout(prompt='', timeout=DEFAULT_TIMEOUT):
echo(prompt)
begin = time.monotonic()
end = begin + timeout
line = ''
while time.monotonic() < end:
if msvcrt.kbhit():
c = msvcrt.getwche()
if c in (CR, LF):
echo(CRLF)
return line
if c == '\003':
raise KeyboardInterrupt
if c == '\b':
line = line[:-1]
cover = SP * len(prompt + line + SP)
echo(''.join([CR, cover, CR, prompt, line]))
else:
line += c
time.sleep(INTERVAL)
echo(CRLF)
raise TimeoutOccurred
inputimeout = None
try:
import msvcrt
except ImportError:
import selectors
import termios
inputimeout = posix_inputimeout
else:
import time
inputimeout = win_inputimeout

View file

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
from .tracer import Tracer

View file

@ -0,0 +1,118 @@
# -*- coding: utf-8 -*-
#
# ixkit - odoo spy
#
#@purpose : logger as Tracer with step feature
#@author : Artificer@ixkit.com
#@date : 2024-2-15
#@version : 1.0.0
#
#-----------------------------------------------------------------------
import sys
import inspect
from ...land import jsons
from ..lang.timeout import inputimeout
from string import Template
from string import Formatter as StrFormatter
def _wait_input(prompt,timeout):
result = None
try:
result = inputimeout(prompt=prompt, timeout=timeout)
except Exception as ex:
pass
finally:
return result
def _smart_json(val):
return jsons.SmartJson(val)
def to_dic(obj):
dic = {}
for fieldkey in dir(obj):
fieldvaule = getattr(obj, fieldkey)
if not fieldkey.startswith("__") and not callable(fieldvaule) and not fieldkey.startswith("_"):
dic[fieldkey] = fieldvaule
return dic
#---------------------------------------------------------------------------
# Tracer classes and functions
#---------------------------------------------------------------------------
class Tracer():
trace_level = True # True|False, 'None','DEBUG'|'INFO'...
def __init__(self ):
pass
@staticmethod
def set_trace_level(level=None):
Tracer.trace_level = level
@staticmethod
def get_trace_level():
if Tracer.trace_level is None:
return None
if Tracer.trace_level == "None":
return None
if Tracer.trace_level == False or Tracer.trace_level == "False":
return None
return Tracer.trace_level
@staticmethod
def smart_json(val):
return to_dic(val)
"""
# named
msg = "xxxx {age} xxxx {name}".format(age=18, name="hangman")
print(msg) # xxxx 18 xxxx hangman
# order
msg = "xxxx {1} xxx{0}".format(value1,value2)
print(msg) # xxxx [9, 0] xxx(7, 8)
# mix
msg = "xxxx {} XXX {name} xxx {}".format(value2,value1,name="s4")
print(msg) # xxxx [9, 0] XXX s4 xxx (7, 8)
"""
@staticmethod
def debug( msg, *args, sender=None, step=None, wait_second=-1, **kwargs ):
trace_level = Tracer.get_trace_level()
if trace_level is None:
return
line = "line?"
if sender is None:
stack_frame = inspect.stack()[1]
stack_module = inspect.getmodule(stack_frame[0])
sender = stack_module.__name__
line = inspect.getlineno(stack_frame[0])
Tracer._log(sender,line, msg, *args, **kwargs)
if step == True :
prompt = "💡" + msg +' 👉 press any key to continue...'
if wait_second > 0 :
# wait limit time for input using inputimeout() function
prompt = prompt + " wait second:" + str(wait_second) + "\r\n"
input_val = _wait_input(prompt=prompt, timeout=wait_second)
if not input_val is None:
print(input_val)
else:
prompt = prompt + "\r\n"
input_val = str(input(prompt))
print(input_val)
@staticmethod
def _log(sender,line, msg, /, *args, **kwargs):
prefix = "{},{}:".format(sender,line)
buf = msg.format(*args, **kwargs)
buf = prefix + buf
print("🔍->" + buf)

View file

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
from . import ir_http

View file

@ -0,0 +1,15 @@
# -*- coding: utf-8 -*-
from odoo import models
from odoo.http import request
class Http(models.AbstractModel):
_inherit = 'ir.http'
@classmethod
def _pre_dispatch(cls, rule, args):
super()._pre_dispatch(rule, args)
url = request.httprequest.url
for key in args:
pass

View file

@ -0,0 +1,55 @@
# -*- coding: utf-8 -*-
from datetime import datetime
from . import runtime
from . import session
from . import spy
from ..__support__ import get_support_major_version
from ..land.trace import Tracer
_logger = Tracer
def now_times():
now = datetime.now()
current_time = now.strftime("%Y-%m-%d %H:%M:%S")
return current_time
def start_spy():
_logger.debug("start_spy 🚀🔥...{}", now_times())
try:
result = spy.hook()
_logger.debug("hook result:{}",str(result))
except Exception as ex:
print(ex)
finally:
_logger.debug("start_spy done,{}",now_times(),step=False)
def end_spy():
_logger.debug("end_spy ...{}", now_times())
try:
result = spy.unhook()
_logger.debug("unhook result:{}",str(result))
except Exception as ex:
print(ex)
finally:
_logger.debug("end_spy done,{}",now_times(),step=False)
def clean_cache(env):
ver = get_support_major_version()
if '17' == ver :
# env.registry.clear_cache('assets')
# env.registry.clear_cache('templates')
# env.registry.clear_cache('routing')
env.registry.clear_all_caches()
return ver
# 16
env["ir.qweb"].clear_caches()
env["ir.ui.view"].clear_caches()

View file

@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
from . import ir_qweb_up

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,139 @@
# -*- coding: utf-8 -*-
#
# ixkit - odoo spy
#
#@purpose : inject new logic code make asset pass meta information ( url,file name etc) to js layer
#@author : Artificer@ixkit.com
#@date : 2024-3-4
#@version : 1.0.0
#
#-----------------------------------------------------------------------
from contextlib import closing
from collections import OrderedDict
from datetime import datetime
from lxml import etree
from subprocess import Popen, PIPE
import base64
import copy
import hashlib
import io
import itertools
import json
import logging
import os
import re
import textwrap
import uuid
import sys
from odoo import release, SUPERUSER_ID, _
from odoo.http import request
from odoo.modules.module import get_resource_path
# from odoo.tools import (func, misc, transpile_javascript,
# is_odoo_module, SourceMapGenerator, profiler,
# apply_inheritance_specs)
from odoo.tools.misc import file_open, html_escape as escape
from odoo.tools.pycompat import to_text
from odoo.addons.base.models.assetsbundle import WebAsset, XMLAsset, AssetError,AssetNotFound
_logger = logging.getLogger(__name__)
EXTENSIONS = (".js", ".css", ".scss", ".sass", ".less", ".xml")
"""
class Asset{
self._filename = filename
self.url = url
}
"""
def _to_meta_json_str(asset):
meta = {
"url" : asset.url,
"file" : asset._filename,
"asset_time": datetime.now().timestamp()
# "content": content
}
result = json.dumps(meta)
return result
def _mark_json_str(val):
result = val.replace("{", "[")
result = result.replace("}", "]")
return result
def _to_meta_str(asset):
meta = {
"url" : asset.url,
"file" : asset._filename,
# "content": content
}
result = "\"{}\"=\"{}\"".format("url",asset.url)
result = result + ";\"{}\"=\"{}\"".format("file",asset._filename)
return result
def _super_fetch_content(self):
""" Fetch content from file or database"""
try:
self.stat()
if self._filename:
with closing(file_open(self._filename, 'rb', filter_ext=EXTENSIONS)) as fp:
return fp.read().decode('utf-8')
else:
return self._ir_attach.raw.decode()
except UnicodeDecodeError:
raise AssetError('%s is not utf-8 encoded.' % self.name)
except IOError:
raise AssetNotFound('File %s does not exist.' % self.name)
except:
raise AssetError('Could not get content for %s.' % self.name)
#class MyXMLAsset(XMLAsset):
def _fetch_content(self):
"""
_content = None
_filename = None
_ir_attach = None
_id = None
"""
tracer = sys.modules['tracer']
try:
tracer.debug("🧐👽 try _fetch_content, self:{}", self, step=False)
#content = super()._fetch_content()
content = _super_fetch_content(self)
except AssetError as e:
_logger.error("Error when _fetch_content,self:%s",self, exc_info=True)
return f'<error data-asset-bundle={self.bundle.name!r} data-asset-version={self.bundle.version!r}>{json.dumps(to_text(e))}</error>'
parser = etree.XMLParser(ns_clean=True, recover=True, remove_comments=True)
root = etree.parse(io.BytesIO(content.encode('utf-8')), parser=parser).getroot()
#@@ original logic
# if root.tag in ('templates', 'template'):
# return ''.join(etree.tostring(el, encoding='unicode') for el in root)
# return etree.tostring(root, encoding='unicode')
"""
apply new logic, pass the meta information
"""
if root.tag in ('templates', 'template'):
buf = ""
for el in root:
if 't-name' in el.attrib:
meta = _to_meta_json_str(self)
el.attrib['t-meta']= meta
buf = buf + etree.tostring(el, encoding='unicode')
tracer.debug("🧐 XMLAsset->_fetch_content,template?? buf:{}", buf ,step=False)
return buf
result = etree.tostring(root, encoding='unicode')
tracer.debug("🧐👽 XMLAsset->_fetch_content,no template? result:{}", result ,step=False)
return result

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,53 @@
# -*- coding: utf-8 -*-
#
# ixkit - odoo spy
#
#@purpose : jnject new logic replace exist methods, append template meta information (name,url,file, db row_id, etc), render by qweb
#@author : Artificer@ixkit.com
#@date : 2024-2-15
#@version : 1.0.0
#
#-----------------------------------------------------------------------
import sys
from lxml import etree
#class IrQWeb(models.AbstractModel):
def _load(self, ref):
"""
Load the template referenced by ``ref``.
:returns: The loaded template (as string or etree) and its
identifier
:rtype: Tuple[Union[etree, str], Optional[str, int]]
"""
IrUIView = self.env['ir.ui.view'].sudo()
view = IrUIView._get(ref)
template = IrUIView._read_template(view.id)
#@@ inject spy code
template = sys.modules['spy'].inject_spy_template(template,view,ref)
#@@ end
etree_view = etree.fromstring(template)
xmlid = view.key or ref
if isinstance(ref, int):
domain = [('model', '=', 'ir.ui.view'), ('res_id', '=', view.id)]
model_data = self.env['ir.model.data'].sudo().search_read(domain, ['module', 'name'], limit=1)
if model_data:
xmlid = f"{model_data[0]['module']}.{model_data[0]['name']}"
# QWeb's ``_read_template`` will check if one of the first children of
# what we send to it has a "t-name" attribute having ``ref`` as value
# to consider it has found it. As it'll never be the case when working
# with view ids or children view or children primary views, force it here.
if view.inherit_id is not None:
for node in etree_view:
if node.get('t-name') == str(ref) or node.get('t-name') == str(view.key):
node.attrib.pop('name', None)
node.attrib.pop('id', None)
etree_view = node
break
etree_view.set('t-name', str(xmlid))
return (etree_view, view.id)

View file

@ -0,0 +1,6 @@
"""
@purpose: Never execute; copy from odoo framework folder, only for read easy
"""
files: ir_qweb.py, assetsbundle.py

View file

@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
from . import http_up

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,61 @@
# -*- coding: utf-8 -*-
"""
hook the class Session, track the 'debug' state that identify the spy mode
"""
import sys
from datetime import datetime
from odoo.http import Session
from ...land.trace import Tracer
def _log_spy(spy):
Tracer.debug("🪝🧐 _log_spy,spy:{}",spy)
def _set_debug_mode(val):
spy = sys.modules['spy']
if spy is None:
return False
if val is None or '' == val:
# if not spy.get('debug') is None:
# del spy.debug
spy.debug = None
_log_spy(spy)
return True
spy.debug = {
'value': val,
'ts': datetime.timestamp(datetime.now())
}
_log_spy(spy)
return True
#@ref from odoo/http.py
#class SessionUp(collections.abc.MutableMapping):
def __setattr__(self, key, val):
#print('🪝🧐 __setattr__' + str(key) + "=>" + str(val))
if key in self.__slots__:
#super().__setattr__(key, val)
super(Session, self).__setattr__(key,val)
else:
self[key] = val
if 'debug' == key:
_set_debug_mode(val)
def clear(self):
if not self.__data is None:
self.__data.clear()
self.is_dirty = True
_set_debug_mode(None)

View file

@ -0,0 +1,18 @@
# -*- coding: utf-8 -*-
import sys
from . import runtime_options
from . import sys_register
from ...land.trace import Tracer
def get_config(key):
if runtime_options.config.get(key):
return runtime_options.config[key]
return None
def set_config(key,val):
sys.modules['runtime_options'].set(key,val)
return True
sys_register.register_instance('tracer',Tracer)

View file

@ -0,0 +1,35 @@
# -*- coding: utf-8 -*-
import sys
from odoo.tools import config
from ...land.lang.pattern import singleton
@singleton
class RuntimeOptions(object):
def __init__(self):
pass
class ConstError(TypeError): pass
def __setattr__(self, key, value):
# self.__dict__.update()
if key in self.__dict__:
raise self.ConstError("constant reassignment error!, key:{},value:{},dict:{} ".format(key,value, self.__dict__ ))
self.__dict__[key] = value
return True
def set(self, key,val):
return self.__setattr__(key,val)
def get(self,key):
return self.__dict__.get(key)
sys.modules['runtime_options'] = RuntimeOptions()
RuntimeOptions().config = config

View file

@ -0,0 +1,18 @@
# -*- coding: utf-8 -*-
import sys
import os
import logging
from odoo.tools import config
_logger = logging.getLogger(__name__)
def register_instance(name,value):
sys.modules[name] = value
_logger.debug('register_instance, infrastructure name:%s, value:%s',name,value)
def unregister_instance(name):
del sys.modules[name]
_logger.debug('unregister_instance, infrastructure name:%s',name)

View file

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
from . import request

Some files were not shown because too many files have changed in this diff Show more