Commit 51d26cb1 by Ari Agung Prasetiyo

Merge branch '5.0.3' of https://git.opensipkd.com/aa.gusti/opensipkd-base into 5.0.3

2 parents 266eb772 5be4c97a
5.0.4 2026-06-01
1. Penambahan parameter
- ws_print_url
- is_pylpr
2. Penambahan Table
- tabel text_printer
3. Penambahan URL
- printer
- printer-add
- printer-edit
- printer-edit
- printer-delete
5.0.3 2026-04-28 5.0.3 2026-04-28
- Penambahan field session_id pada table users digunakan untuk membatasi jumlah - Penambahan field session_id pada table users digunakan untuk membatasi jumlah
login hanya bisa ada pada satu device login hanya bisa ada pada satu device
......
...@@ -520,14 +520,18 @@ class BaseApp(): ...@@ -520,14 +520,18 @@ class BaseApp():
self.reg_nip = 0 self.reg_nip = 0
self.single_device = "false" self.single_device = "false"
self.is_pylpr = False self.is_pylpr = False
self.wsp_url = ""
self.wsp_client_id = ""
self.wsp_client_key = ""
def get_route_file(self, filename="routes.csv"): def get_route_file(self, filename="routes.csv"):
fullpath = os.path.join(self.base_dir, 'scripts', 'data', filename) fullpath = os.path.join(self.base_dir, 'scripts', 'data', filename)
return open(fullpath) return open(fullpath, encoding="utf-8")
def static_view(self, config, settings=None): def static_view(self, config, settings=None):
if not settings: settings = settings or config.registry.settings
settings = get_settings() # if not settings:
# settings = get_settings()
self.temp_files = settings.get("temp_files") self.temp_files = settings.get("temp_files")
if not os.path.exists(self.temp_files): if not os.path.exists(self.temp_files):
os.makedirs(self.temp_files) os.makedirs(self.temp_files)
...@@ -564,6 +568,9 @@ class BaseApp(): ...@@ -564,6 +568,9 @@ class BaseApp():
self.login_tpl = get_params("login_tpl", "", settings=settings) self.login_tpl = get_params("login_tpl", "", settings=settings)
self.login_captcha = int(get_params( self.login_captcha = int(get_params(
"login_captcha", 0, settings=settings)) "login_captcha", 0, settings=settings))
self.wsp_url = settings.get("wsp_url", "")
self.wsp_client_id = settings.get("wsp_client_id", "")
self.wsp_client_key = settings.get("wsp_client_key", "")
def add_menu(self, config, route_menus, parent=None, paket="opensipkd.base.views", def add_menu(self, config, route_menus, parent=None, paket="opensipkd.base.views",
template_path="views/templates/"): template_path="views/templates/"):
...@@ -784,18 +791,33 @@ def set_routes(config, app_id=None): ...@@ -784,18 +791,33 @@ def set_routes(config, app_id=None):
from pyramid.httpexceptions import HTTPBadRequest, HTTPFound from pyramid.httpexceptions import HTTPBadRequest, HTTPFound
from pyramid.security import forget from pyramid.security import forget
from pyramid.view import exception_view_config from pyramid.view import exception_view_config
from pyramid.response import Response
import json
@exception_view_config(HTTPBadRequest) @exception_view_config(HTTPBadRequest)
def bad_request_view(exc, request): def bad_request_view(exc, request):
# Bersihkan sesi autentikasi (logout) if request.matched_route.name=='base-login' :
headers = forget(request) # Bersihkan sesi autentikasi (logout)
headers = forget(request)
# Arahkan ulang ke halaman login (misalnya route 'login')
request.session.flash("Permintaan tidak valid. Silakan ulangi kembali. atau origin tidak diizinkan.", "error") # Arahkan ulang ke halaman login (misalnya route 'login')
_logging.debug(f"Bad Request: {exc} from {request.url}") request.session.flash("Permintaan tidak valid. Silakan ulangi kembali. atau origin tidak diizinkan.", "error")
referrer = request.route_url('base-home') _logging.debug(f"Bad Request: {exc} from {request.url}")
response = HTTPFound(location=referrer) referrer = request.route_url('base-login')
response.headers.extend(headers) response = HTTPFound(location=referrer)
return response response.headers.extend(headers)
return response
elif request.is_xhr:
payload = {
"status": "error",
"message": exc.detail or str(exc)
}
return Response(
json_body=payload,
status=400,
content_type='application/json'
)
return exc
from .depreciated_base import * from .depreciated_base import *
...@@ -15,7 +15,6 @@ from pyramid.request import Response ...@@ -15,7 +15,6 @@ from pyramid.request import Response
from sqlalchemy import Table from sqlalchemy import Table
# from opensipkd.base.views.upload import tmpstore # from opensipkd.base.views.upload import tmpstore
from opensipkd.tools.captcha import img_captcha
from opensipkd.tools import dmy, get_settings, get_ext, \ from opensipkd.tools import dmy, get_settings, get_ext, \
date_from_str, get_random_string, Upload, InvalidExtension, mem_tmp_store date_from_str, get_random_string, Upload, InvalidExtension, mem_tmp_store
from opensipkd.tools.buttons import ( from opensipkd.tools.buttons import (
...@@ -77,8 +76,6 @@ class CSRFSchema(colander.Schema): ...@@ -77,8 +76,6 @@ class CSRFSchema(colander.Schema):
import re import re
from pyramid.interfaces import IRoutesMapper from pyramid.interfaces import IRoutesMapper
from pyramid.threadlocal import get_current_registry from pyramid.threadlocal import get_current_registry
import io
import xlsxwriter
class BaseView(object): class BaseView(object):
def __init__(self, request): def __init__(self, request):
...@@ -107,7 +104,9 @@ class BaseView(object): ...@@ -107,7 +104,9 @@ class BaseView(object):
self.list_route = 'home' self.list_route = 'home'
# self.list_col_defs = "" # self.list_col_defs = ""
# self.list_cols = "" # self.list_cols = ""
self.list_report = (btn_csv, btn_pdf) self.list_report = (btn_csv, btn_xls)
self.list_csvpdf = (btn_csv, btn_pdf)
self.list_pdf = (btn_pdf,)
self.list_buttons = (btn_add,) self.list_buttons = (btn_add,)
self.list_upload = (btn_upload,) self.list_upload = (btn_upload,)
self.list_view_field = None self.list_view_field = None
...@@ -556,7 +555,8 @@ class BaseView(object): ...@@ -556,7 +555,8 @@ class BaseView(object):
elif url_dict['act'] == 'csv': elif url_dict['act'] == 'csv':
return self.csv_response(**kwargs) return self.csv_response(**kwargs)
elif url_dict['act'] == 'csv1':
return self.csv1_response(**kwargs)
elif url_dict['act'] == 'pdf': elif url_dict['act'] == 'pdf':
return self.pdf_response(**kwargs) return self.pdf_response(**kwargs)
...@@ -671,7 +671,7 @@ class BaseView(object): ...@@ -671,7 +671,7 @@ class BaseView(object):
filename = jasper_export(self.report_file) filename = jasper_export(self.report_file)
return file_response(self.req, filename=filename[0]) return file_response(self.req, filename=filename[0])
def csv_response(self, **kwargs): def csv1_response(self, **kwargs):
query = self.table.query_register() query = self.table.query_register()
row = query.first() row = query.first()
header = row._mapping.keys() header = row._mapping.keys()
...@@ -725,6 +725,12 @@ class BaseView(object): ...@@ -725,6 +725,12 @@ class BaseView(object):
for i, value in enumerate(row): for i, value in enumerate(row):
if isinstance(value, str): if isinstance(value, str):
row[i] = self.remove_tags(value) row[i] = self.remove_tags(value)
elif isinstance(value, datetime):
is_aware_utc = value.tzinfo is not None \
and value.tzinfo.utcoffset(value) is not None
if is_aware_utc:
value = value.astimezone().replace(tzinfo=None)
row[i] = value
value = { value = {
'header': header, 'header': header,
...@@ -732,11 +738,14 @@ class BaseView(object): ...@@ -732,11 +738,14 @@ class BaseView(object):
} }
return value return value
def xls_response(self, **kwargs): def xls_response(self, **kwargs):
value = self.xls_data() value = self.xls_data()
return xls_response(self.req, value) return xls_response(self.req, value)
def csv_response(self, **kwargs):
value = self.xls_data()
return csv_response(self.req, value)
def get_bindings(self, row=None): def get_bindings(self, row=None):
return {"row": row} return {"row": row}
......
...@@ -30,13 +30,16 @@ class AddSchema(colander.Schema): ...@@ -30,13 +30,16 @@ class AddSchema(colander.Schema):
validator=colander.Length(max=32), validator=colander.Length(max=32),
oid="kode") oid="kode")
kode = colander.SchemaNode(colander.String(), kode = colander.SchemaNode(colander.String(),
validator=colander.Length(max=32), oid="kode") validator=colander.Length(max=32), oid="kode",
global_search=True,)
kategori = colander.SchemaNode(colander.String(), kategori = colander.SchemaNode(colander.String(),
widget=widget.SelectWidget( widget=widget.SelectWidget(
values=kategori_dati2), values=kategori_dati2),
validator=colander.Length(max=32), validator=colander.Length(max=32),
oid="kode") oid="kode",
nama = colander.SchemaNode(colander.String(), oid="nama") global_search=True,)
nama = colander.SchemaNode(colander.String(), oid="nama",
global_search=True,)
class EditSchema(AddSchema): class EditSchema(AddSchema):
......
...@@ -86,8 +86,10 @@ class EditSchema(AddSchema): ...@@ -86,8 +86,10 @@ class EditSchema(AddSchema):
class ListSchema(colander.Schema): class ListSchema(colander.Schema):
id = colander.SchemaNode(colander.String(), title="Action", visible=False) id = colander.SchemaNode(colander.String(), title="Action", visible=False)
kode = colander.SchemaNode(colander.String(), title="Kode") kode = colander.SchemaNode(colander.String(), title="Kode",
nama = colander.SchemaNode(colander.String(), title="Nama") global_search=True,)
nama = colander.SchemaNode(colander.String(), title="Nama",
global_search=True,)
status = colander.SchemaNode(colander.Boolean(), title="Status", width='50pt', status = colander.SchemaNode(colander.Boolean(), title="Status", width='50pt',
widget=widget.CheckboxWidget()) widget=widget.CheckboxWidget())
level_id = colander.SchemaNode( level_id = colander.SchemaNode(
......
...@@ -43,12 +43,15 @@ class AddSchema(colander.Schema): ...@@ -43,12 +43,15 @@ class AddSchema(colander.Schema):
oid="kecamatan_id", oid="kecamatan_id",
title="Kecamatan") title="Kecamatan")
kode = colander.SchemaNode(colander.String(), kode = colander.SchemaNode(colander.String(),
validator=colander.Length(max=32), oid="kode") validator=colander.Length(max=32), oid="kode",
global_search=True,)
kategori = colander.SchemaNode(colander.String(), kategori = colander.SchemaNode(colander.String(),
widget=widget.SelectWidget( widget=widget.SelectWidget(
values=kategori_desa), values=kategori_desa),
validator=colander.Length(max=32), oid="kode") validator=colander.Length(max=32), oid="kode",
nama = colander.SchemaNode(colander.String(), oid="nama") global_search=True,)
nama = colander.SchemaNode(colander.String(), oid="nama",
global_search=True,)
class EditSchema(AddSchema): class EditSchema(AddSchema):
......
...@@ -42,8 +42,10 @@ class EditSchema(AddSchema): ...@@ -42,8 +42,10 @@ class EditSchema(AddSchema):
class ListSchema(colander.Schema): class ListSchema(colander.Schema):
id = colander.SchemaNode(colander.Integer(), visible=False, title="Action") id = colander.SchemaNode(colander.Integer(), visible=False, title="Action")
group_name = colander.SchemaNode( group_name = colander.SchemaNode(
colander.String(), ) colander.String(),
description = colander.SchemaNode(colander.String()) global_search=True, )
description = colander.SchemaNode(colander.String(),
global_search=True,)
class Views(BaseView): class Views(BaseView):
......
...@@ -32,17 +32,21 @@ class AddSchema(colander.Schema): ...@@ -32,17 +32,21 @@ class AddSchema(colander.Schema):
kode = colander.SchemaNode( kode = colander.SchemaNode(
colander.String(), colander.String(),
validator=colander.Length(max=32), validator=colander.Length(max=32),
oid="kode") oid="kode",
global_search=True,)
nama = colander.SchemaNode( nama = colander.SchemaNode(
colander.String(), colander.String(),
oid="nama") oid="nama",
global_search=True,)
nama_pendek = colander.SchemaNode( nama_pendek = colander.SchemaNode(
colander.String(), colander.String(),
oid="nama_pendek", oid="nama_pendek",
global_search=True,
missing=colander.drop) missing=colander.drop)
nama_lain = colander.SchemaNode( nama_lain = colander.SchemaNode(
colander.String(), colander.String(),
oid="nama_lain", oid="nama_lain",
global_search=True,
missing=colander.drop) missing=colander.drop)
jenis = colander.SchemaNode( jenis = colander.SchemaNode(
colander.Integer(), colander.Integer(),
......
$(function () {
$('#deformcheck').click(function (e) {
var url = '%s';
e.preventDefault();
console.log('Check button clicked: ' + url);
$.get(url, function (data) {
console.log("Data loaded:", data);
$('#errors').hide();
$('#success').text(data.message).show();
}).fail(function (jqXHR, textStatus, errorThrown,) {
var err = ("Error printing: " + errorThrown + " " + jqXHR.responseJSON.message);
$('#success').hide();
$('#errors').text(err).show();
});
});
});
\ No newline at end of file \ No newline at end of file
...@@ -41,8 +41,10 @@ class AddSchema(colander.Schema): ...@@ -41,8 +41,10 @@ class AddSchema(colander.Schema):
oid="dati2_id") oid="dati2_id")
kode = colander.SchemaNode( kode = colander.SchemaNode(
colander.String(), colander.String(),
validator=colander.Length(max=32), oid="kode") validator=colander.Length(max=32), oid="kode",
nama = colander.SchemaNode(colander.String(), oid="nama") global_search=True,)
nama = colander.SchemaNode(colander.String(), oid="nama",
global_search=True,)
class EditSchema(AddSchema): class EditSchema(AddSchema):
......
...@@ -70,15 +70,18 @@ class ListSchema(colander.Schema): ...@@ -70,15 +70,18 @@ class ListSchema(colander.Schema):
validator=colander.Length(max=32), validator=colander.Length(max=32),
oid="kode", oid="kode",
title="Kode", title="Kode",
width="100pt") width="100pt",
global_search=True)
nama = colander.SchemaNode( nama = colander.SchemaNode(
colander.String(), colander.String(),
validator=colander.Length(max=64), validator=colander.Length(max=64),
oid="nama") oid="nama",
global_search=True)
email = colander.SchemaNode( email = colander.SchemaNode(
colander.String(), colander.String(),
oid="email") oid="email",
global_search=True)
idcard = colander.SchemaNode( idcard = colander.SchemaNode(
colander.String(), colander.String(),
oid="idcard", oid="idcard",
......
...@@ -18,12 +18,14 @@ class ListSchema(colander.Schema): ...@@ -18,12 +18,14 @@ class ListSchema(colander.Schema):
perm_name = colander.SchemaNode( perm_name = colander.SchemaNode(
colander.String(), colander.String(),
oid="perm_name", oid="perm_name",
title="Nama") title="Nama",
global_search=True)
description = colander.SchemaNode( description = colander.SchemaNode(
colander.String(), colander.String(),
missing=colander.drop, missing=colander.drop,
oid="description", oid="description",
title="Diskripsi") title="Diskripsi",
global_search=True)
class EditSchema(ListSchema): class EditSchema(ListSchema):
pass pass
......
from pyramid.httpexceptions import HTTPBadRequest
import logging import logging
import re import re
import colander import colander
from deform import (widget,) from deform import (widget,)
from pyramid.i18n import TranslationStringFactory from pyramid.i18n import TranslationStringFactory
from ..models import TextPrinters import opensipkd
import platform
from . import BaseView, NamaSchema import websockets
import json
import os
import asyncio
from opensipkd.base import BASE_CLASS
from opensipkd.tools.buttons import btn_check
from opensipkd.tools import get_random_string
from ..models import TextPrinters, User
from . import BaseView
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -14,148 +24,61 @@ _ = TranslationStringFactory("opensipkd") ...@@ -14,148 +24,61 @@ _ = TranslationStringFactory("opensipkd")
SESS_ADD_FAILED = 'Tambah partner gagal' SESS_ADD_FAILED = 'Tambah partner gagal'
SESS_EDIT_FAILED = 'Edit partner gagal' SESS_EDIT_FAILED = 'Edit partner gagal'
TEST_PRINT_DATA = \
"""*** TEST PRINT FROM WEB SERVER ***
This is a test print message sent from the server to the client.
def print_text(user_id, text=None, filename=None): If you see this on your printer, the connection and printing functionality are working
from opensipkd.base.models import TextPrinters correctly."""
printer = TextPrinters.query().filter_by(user_id=user_id, status=1).first()
if not printer:
log.error(
f"User {user_id} does not have an active printer configured.")
raise Exception("No active printer configured for the user.")
# Replace with your printer's IP
ip = printer.kode if printer else "127.0.0.1"
ips = ip.split(".") # Support multiple IPs separated by commas
ips = [int(ip.strip()) for ip in ips if ip.strip()] # Clean up whitespace and empty entries
hostname = ".".join(map(str, ips)) if ips else "127.0.0.1"
queue = printer.queue if printer else "lp" # Common default queue name
port = printer.port if printer else 515 # Default LPR port
timeout = printer.timeout if printer else 10 # Timeout in seconds
# Set to True if the printer is an Epson model that requires specific control codes
is_epson = bool(printer.is_epson) if printer else False
from opensipkd.base import BASE_CLASS
from opensipkd.tools import get_random_string
import os
if not BASE_CLASS.is_pylpr:
import platform
current_os = platform.system().lower()
if current_os == "windows":
cmd = 'lpr'
else: # Darwin is macOS
cmd = 'rlpr'
if text and not filename:
filename = os.path.join(BASE_CLASS.temp_files,
get_random_string(16) + ".txt")
with open(filename, "w", encoding="utf-8") as f:
f.write(text)
try:
import subprocess
subprocess.run(
[cmd, f"--printer={queue}@{hostname}:{port}", filename], check=True)
except subprocess.CalledProcessError as e:
log.error(f"Failed to print using {cmd}: {e}")
raise Exception(f"Failed to print the document. {e}, {hostname}, {port}, {queue}") from e
else:
from pylpr import LprClient
with LprClient(
hostname=hostname,
port=port,
timeout=timeout,
queue=queue,
# recv_buffer=args.recv_buffer,
# username=args.username,
# job_name=args.job_name,
# file_name=getattr(args.print_file, 'name', None) if hasattr(
# args, 'print_file') and args.print_file else None,
# label=args.label,
# use_reserved_port=args.use_reserved_port
) as lpr:
# if args.print_queue:
# # Send the 'Print any waiting jobs' command (0x01) to the specified queue
# lpr._send_lpr_command(1, queue.encode('ascii'))
# print(
# f"Sent 'Print any waiting jobs' command to queue '{args.queue}'")
# return
# if args.status:
# lpr._send_lpr_command(
# 3, (args.queue + " " + args.status).encode('ascii'), noreceive=True)
# print(
# f"Sent 'Send queue state (short)' command with attributes '{args.status}' to queue '{args.queue}'")
# # Receive and print the response from the printer
# response = lpr.receive()
# print("Received queue state (short) response:")
# print(response.decode('utf-8', errors='replace'))
# return
# if args.longstatus:
# lpr._send_lpr_command(
# 4, (args.queue + " " + args.longstatus).encode('ascii'), noreceive=True)
# print(
# f"Sent 'Send queue state (long)' command with attributes '{args.longstatus}' to queue '{args.queue}'")
# # Receive and print the response from the printer
# response = lpr.receive()
# print("Received queue state (long) response:")
# print(response.decode('utf-8', errors='replace'))
# return
# if args.remove:
# # Send the 'Remove jobs' command (0x05) to the specified queue
# lpr._send_lpr_command(
# 5, (args.queue + " " + args.remove).encode('ascii'))
# print(
# f"Sent 'Remove jobs' command with attributes '{args.remove}' to queue '{args.queue}'")
# return
if is_epson:
lpr.send(
lpr.EXIT_PACKET_MODE
+ lpr.INITIALIZE_PRINTER
+ text.encode('utf-8')
+ lpr.FF
)
else:
try:
lpr.send(text.encode('utf-8'))
except Exception as e:
log.error(f"Failed to send print job: {e}")
raise Exception(
f"Failed to print the document. {e}, {hostname}, {port}, {queue}") from e
# lpr.send(text.encode('utf-8'))
class AddSchema(colander.Schema):
class AddSchema(NamaSchema): user_id = colander.SchemaNode(
colander.Integer(),
widget=widget.SelectWidget(),
title="User ID",
global_search=False,
searchable=True,
search_method="numeric",
default=10)
nama = colander.SchemaNode( nama = colander.SchemaNode(
colander.String(), colander.String(),
title="Nama Printer", title="Nama Printer",
validator=colander.Length(max=64) validator=colander.Length(max=64),
global_search=True,
) )
is_epson = colander.SchemaNode(
colander.Integer(),
widget=widget.SelectWidget(),
title="Printer/Network Type",
default=0)
kode = colander.SchemaNode( kode = colander.SchemaNode(
colander.String(), colander.String(),
title="IP Address", title="IP Address",
widget=widget.TextInputWidget(mask="999.999.999.999",)) widget=widget.TextInputWidget(mask="999.999.999.999",),
missing="",
global_search=True,
)
port = colander.SchemaNode( port = colander.SchemaNode(
colander.Integer(), colander.Integer(),
title="Port", title="Port",
missing=0,
default=515) default=515)
is_epson = colander.SchemaNode(
colander.Integer(),
widget=widget.CheckboxWidget(true_val="1", false_val="0"),
title="Epson Printer",
default=0)
queue = colander.SchemaNode( queue = colander.SchemaNode(
colander.String(), colander.String(),
validator=colander.Length(max=16), validator=colander.Length(max=16),
title="Queue Name", title="Queue Name",
global_search=True,
missing="",
default='lp') default='lp')
timeout = colander.SchemaNode( timeout = colander.SchemaNode(
colander.Integer(), colander.Integer(),
title="Timeout", title="Timeout",
missing=0,
default=10) default=10)
user_id = colander.SchemaNode(
colander.Integer(),
widget=widget.SelectWidget(),
title="User ID",
default=10)
status = colander.SchemaNode( status = colander.SchemaNode(
colander.Integer(), colander.Integer(),
widget=widget.CheckboxWidget(true_val="1", false_val="0"), widget=widget.CheckboxWidget(true_val="1", false_val="0"),
...@@ -167,12 +90,16 @@ class AddSchema(NamaSchema): ...@@ -167,12 +90,16 @@ class AddSchema(NamaSchema):
mask="999.999.999.999", # Use specialized mask format mask="999.999.999.999", # Use specialized mask format
mask_mapping={'Z': {'pattern': '[0-9]', 'optional': True}}, mask_mapping={'Z': {'pattern': '[0-9]', 'optional': True}},
) )
from opensipkd.base.models import User # Use a simple text input for user_id
schema["user_id"].widget.values = User.get_list() # Use a simple text input for user_id schema["user_id"].widget.values = User.get_list()
request = kw.get('request') schema["user_id"].aligned = "text-left"
if request and not request.has_permission('admin'): schema["is_epson"].aligned = "text-left"
schema["user_id"].widget = widget.HiddenWidget() epson_values = [
schema["user_id"].default = request.user.id ("0", "Other"),
("1", "Epson Type"),]
if BASE_CLASS.wsp_url:
epson_values.append(("2", "Web Socket"))
schema["is_epson"].widget.values = epson_values
class EditSchema(AddSchema): class EditSchema(AddSchema):
...@@ -183,7 +110,12 @@ class EditSchema(AddSchema): ...@@ -183,7 +110,12 @@ class EditSchema(AddSchema):
class ListSchema(EditSchema): class ListSchema(EditSchema):
pass def after_bind(self, schema, kw):
super().after_bind(schema, kw)
request = kw.get('request')
if request and not request.has_permission('admin'):
schema["user_id"].widget = widget.HiddenWidget()
schema["user_id"].default = request.user.id
class Views(BaseView): class Views(BaseView):
...@@ -199,6 +131,18 @@ class Views(BaseView): ...@@ -199,6 +131,18 @@ class Views(BaseView):
self.save_state = True self.save_state = True
self.allow_check = True self.allow_check = True
self.list_view_field = 'nama' self.list_view_field = 'nama'
self.filter_columns = True
self.allow_delete = True
route_name = request.matched_route.name
id_ = request.matchdict.get('id')
if id_ and route_name.endswith('view'):
import os
folder = os.path.abspath(opensipkd.base.views.__file__)
filename = os.path.join(os.path.dirname(
folder), "js", "printer_form.js")
url = self.req.route_url(self.list_route)+f'/check/act?id={id_}'
with open(filename, "r", encoding="utf-8") as f:
self.form_scripts = f.read() % url
def list_filter(self, query, **kwargs): def list_filter(self, query, **kwargs):
if not self.req.has_permission('admin'): if not self.req.has_permission('admin'):
...@@ -206,29 +150,51 @@ class Views(BaseView): ...@@ -206,29 +150,51 @@ class Views(BaseView):
return query return query
def form_validator(self, form, value): def form_validator(self, form, value):
exc = colander.Invalid(form, None) err = {}
ip = value['kode'].split('.') err_format = 'Format IP harus 123.123.123.123'
err = [] found = TextPrinters.query().filter(
err_format = 'Format IP salah' TextPrinters.nama == value['nama'],
if not re.match(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}', value['kode']): TextPrinters.user_id == value['user_id']
err.append(err_format+' 123.123.123.123') ).first()
if len(ip) != 4: if found:
err.append(err_format+' harus 4 oktet') if str(found.id) != self.req.matchdict.get('id'):
for i in ip: err["nama"] = 'Nama printer sudah digunakan untuk user ini'
if not i.isdigit(): is_epson = value['is_epson']
err.append(err_format) if is_epson < 2:
if int(i) < 0 or int(i) > 255: if not value.get('port'):
err.append(err_format+' setiap oktet harus antara 0-255') err["port"] = 'Port harus diisi untuk tipe Web Socket'
if ip[0] == '0': if not value.get('queue'):
err.append(err_format+' tidak boleh diawali 0') err["queue"] = 'Queue harus diisi untuk tipe Web Socket'
if (not value.get('kode')
or not re.match(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}', value['kode'])):
err["kode"] = 'Kode harus diisi untuk tipe Web Socket'
else:
ip = value['kode'].split('.')
if len(ip) != 4:
err["kode"] = err_format+' terdiri dari 4 oktet'
else:
for i in ip:
if not i.isdigit():
err["kode"] = err_format + \
' setiap oktet harus angka'
break
if int(i) < 0 or int(i) > 255:
err["kode"] = err_format + \
' setiap oktet harus antara 0-255'
break
if ip[0] == '000':
err["kode"] = err_format+' tidak boleh diawali 000'
break
if err: if err:
exc['kode'] = '\n'.join(err) exc = colander.Invalid(form, None)
for key, val in err.items():
exc[key] = val
raise exc raise exc
def after_save(self, values, row): def after_save(self, values, row):
self.db_session.flush() self.db_session.flush()
if values['status'] == 1: if values['status'] == 1:
printers = TextPrinters.query().filter_by(create_uid=row.create_uid).all() printers = TextPrinters.query().filter_by(user_id=row.user_id).all()
for printer in printers: for printer in printers:
if printer.id == row.id: if printer.id == row.id:
continue continue
...@@ -236,3 +202,164 @@ class Views(BaseView): ...@@ -236,3 +202,164 @@ class Views(BaseView):
self.db_session.add(printer) self.db_session.add(printer)
self.db_session.flush() self.db_session.flush()
return row return row
def view_buttons(self, row):
buttons = super().view_buttons(row)
buttons += (btn_check,)
return buttons
def next_act(self, **kwargs):
id_ = self.req.GET.get('id')
try:
return asyncio.run(print_text(print_id=id_, text="Test Print"))
# return {'success': True,
# 'message': "Print job sent successfully."}
except Exception as e:
raise HTTPBadRequest(explanation=str(e)) from e
async def print_text(user_id=None, print_id=None, text=None, filename=None,
ws_url=BASE_CLASS.wsp_url):
log.debug(f"Starting print_text with user_id={user_id}, print_id={print_id}, filename={filename}")
if not user_id and not print_id:
log.error("User ID or Print ID must be provided.")
raise Exception("User ID or Print ID must be provided.")
if user_id:
printer = TextPrinters.query().filter_by(user_id=user_id, status=1).first()
else:
printer = TextPrinters.query().filter_by(id=print_id, status=1).first()
if not printer:
log.error(
f"User {user_id} or Print ID {print_id} does not have an active printer configured.")
raise Exception("No active printer configured for the user.")
is_ws = printer.is_epson == 2 if printer else False
timeout = printer.timeout if printer else 10 # Timeout in seconds
if is_ws:
printer_name = printer.nama if printer else "unknown"
printer_name = BASE_CLASS.wsp_client_id+'_'+ printer_name
ws_url = ws_url or BASE_CLASS.wsp_url
if not ws_url:
log.error("WebSocket URL is not configured.")
raise Exception("WebSocket URL is not configured.")
try:
async with websockets.connect(ws_url) as websocket:
# Send user on connect
log.info(f"Connected to server at {ws_url} as {printer_name}")
# data = {"action": "print",
# "printer": self.printer,
# "message": message
# }
data = {
"action": "login",
"client_id": BASE_CLASS.wsp_client_id,
"api_key": BASE_CLASS.wsp_client_key,
"printer": BASE_CLASS.wsp_client_id
}
await websocket.send(json.dumps(data))
text = await websocket.recv()
log.info(f"Received from server: {text}")
try:
resp = json.loads(text)
except json.JSONDecodeError as e:
log.error(f"Failed to decode server response: {e}")
raise Exception(f"Invalid response from server: {text}") from e
if resp.get("status") != True:
log.error(
f"Login failed: {resp.get('message', 'No message provided')}")
raise Exception(f"Login failed: {resp.get('message', 'No message provided')}")
# Wait for server to process login
if filename and os.path.isfile(filename):
ext = os.path.splitext(filename)[1].lower()
if ext in {".prn", ".txt"}:
with open(filename, "r", encoding="utf-8") as f:
message = f.read()
else:
with open(filename, "rb") as file:
message = file.read()
else:
message = TEST_PRINT_DATA
data = {"action": "print",
"printer": printer_name,
"filename": os.path.basename(filename) if filename else "test_print.txt",
"message": message
}
await websocket.send(json.dumps(data))
resp = await websocket.recv()
data = json.loads(resp)
if not data.get("status"):
log.error(
f"Server returned an error: {data.get('message')}")
raise Exception(f"Server error: {data.get('message')}")
# Wait for any response from server
log.info(f"Received from server: {resp}")
return data
except (websockets.ConnectionClosed, ConnectionRefusedError, OSError) as e:
msg = f"Failed to connect to WebSocket server at {ws_url}: {e}"
log.error(msg)
raise Exception(msg) from e
# Replace with your printer's IP
ip = printer.kode if printer else "127.0.0.1"
ips = ip.split(".") # Support multiple IPs separated by commas
# Clean up whitespace and empty entries
ips = [int(ip.strip()) for ip in ips if ip.strip()]
hostname = ".".join(map(str, ips)) if ips else "127.0.0.1"
queue = printer.queue if printer else "lp" # Common default queue name
port = printer.port if printer else 515 # Default LPR port
# Set to True if the printer is an Epson model that requires specific control codes
is_epson = printer.is_epson == 1 if printer else False
if not BASE_CLASS.is_pylpr:
current_os = platform.system().lower()
if current_os == "windows":
cmd = 'lpr'
else: # Darwin is macOS
cmd = 'rlpr'
if text and not filename:
filename = os.path.join(BASE_CLASS.temp_files,
get_random_string(16) + ".txt")
with open(filename, "w", encoding="utf-8") as f:
f.write(text)
try:
import subprocess
subprocess.run(
[cmd, f"--printer={queue}@{hostname}:{port}", filename], check=True)
except subprocess.CalledProcessError as e:
log.error(f"Failed to print using {cmd}: {e}")
raise Exception(
f"Failed to print the document. {e}, {hostname}, {port}, {queue}") from e
finally:
if filename and os.path.isfile(filename):
try:
os.remove(filename)
except Exception as e:
log.warning(f"Failed to delete temp file {filename}: {e}")
else:
from pylpr import LprClient
with LprClient(
hostname=hostname,
port=port,
timeout=timeout,
queue=queue,
) as lpr:
if is_epson:
lpr.send(
lpr.EXIT_PACKET_MODE
+ lpr.INITIALIZE_PRINTER
+ text.encode('utf-8')
+ lpr.FF
)
else:
try:
lpr.send(text.encode('utf-8'))
except Exception as e:
log.error(f"Failed to send print job: {e}")
raise Exception(
f"Failed to print the document. {e}, {hostname}, {port}, {queue}") from e
lpr.send(text.encode('utf-8'))
...@@ -27,14 +27,17 @@ def provinsi_widget(node, kw): ...@@ -27,14 +27,17 @@ def provinsi_widget(node, kw):
class AddSchema(colander.Schema): class AddSchema(colander.Schema):
kode = colander.SchemaNode(colander.String(), kode = colander.SchemaNode(colander.String(),
oid="kode", oid="kode",
validator=colander.Length(max=32), ) validator=colander.Length(max=32),
global_search=True, )
kategori = colander.SchemaNode(colander.String(), kategori = colander.SchemaNode(colander.String(),
widget=widget.SelectWidget( widget=widget.SelectWidget(
values=kategori_provinsi), values=kategori_provinsi),
validator=colander.Length(max=32), oid="kode") validator=colander.Length(max=32), oid="kode")
nama = colander.SchemaNode(colander.String(), oid="nama") nama = colander.SchemaNode(colander.String(), oid="nama",
global_search=True,)
ibu_kota = colander.SchemaNode( ibu_kota = colander.SchemaNode(
colander.String(), oid="nama", missing=colander.drop) colander.String(), oid="nama", missing=colander.drop,
global_search=True,)
class EditSchema(AddSchema): class EditSchema(AddSchema):
......
import os import os
import re import re
from shapely import node
import colander import colander
from deform import (widget, ) from deform import (widget, )
from opensipkd.base import BASE_CLASS from opensipkd.base import BASE_CLASS
from pyramid.i18n import TranslationStringFactory from pyramid.i18n import TranslationStringFactory
from sqlalchemy import (func, true, ) from sqlalchemy import (func )
from ziggurat_foundations.models.services.user import UserService from ziggurat_foundations.models.services.user import UserService
from opensipkd.tools import create_now from opensipkd.tools import create_now
from opensipkd.tools.buttons import btn_delete from opensipkd.tools.buttons import btn_delete
from . import BaseView from . import BaseView
# from .company import company_widget
from .user_login import ( from .user_login import (
regenerate_security_code, send_email_security_code, generate_api_key, ) regenerate_security_code, send_email_security_code, generate_api_key, )
from ..models.users import (DBSession, User, Group, UserGroup,) from ..models.users import (DBSession, User, Group, UserGroup,)
...@@ -25,9 +23,11 @@ class ListSchema(colander.Schema): ...@@ -25,9 +23,11 @@ class ListSchema(colander.Schema):
id = colander.SchemaNode(colander.String(), id = colander.SchemaNode(colander.String(),
title="Action", title="Action",
searchable=False) searchable=False)
email = colander.SchemaNode(colander.String()) email = colander.SchemaNode(colander.String(),
global_search=True)
user_name = colander.SchemaNode(colander.String(), user_name = colander.SchemaNode(colander.String(),
title=_("user-name", default="User Name")) title=_("user-name", default="User Name"),
global_search=True)
status = colander.SchemaNode(colander.Integer(), status = colander.SchemaNode(colander.Integer(),
widget=widget.CheckboxWidget(), widget=widget.CheckboxWidget(),
width=50, searchable=False) width=50, searchable=False)
......
...@@ -14,14 +14,16 @@ class ListSchema(colander.Schema): ...@@ -14,14 +14,16 @@ class ListSchema(colander.Schema):
user_name = colander.SchemaNode( user_name = colander.SchemaNode(
colander.String(), colander.String(),
field=User.user_name, field=User.user_name,
title="User" title="User",
global_search=True,
) )
desa_kd = colander.SchemaNode( desa_kd = colander.SchemaNode(
colander.String(), colander.String(),
field=ResDesa.kode) field=ResDesa.kode)
desa_name = colander.SchemaNode( desa_name = colander.SchemaNode(
colander.String(), colander.String(),
field=ResDesa.nama) field=ResDesa.nama,
global_search=True,)
class AddSchema(colander.Schema): class AddSchema(colander.Schema):
......
...@@ -190,6 +190,14 @@ class DeTable(field.Field): ...@@ -190,6 +190,14 @@ class DeTable(field.Field):
window.location = o%sUri+'/csv/act%s&'+paramString;} window.location = o%sUri+'/csv/act%s&'+paramString;}
""" % ( """ % (
tableid, tableid, params), tableid, tableid, params),
"csv2": """{
params = o%s.ajax.params();
paramString = $.param(params);
console.log(params);
window.location = o%sUri+'/csv2/act%s&'+paramString;}
""" % (
tableid, tableid, params),
"xls": """{ "xls": """{
params = o%s.ajax.params(); params = o%s.ajax.params();
paramString = $.param(params); paramString = $.param(params);
......
...@@ -38,6 +38,8 @@ dependencies = [ ...@@ -38,6 +38,8 @@ dependencies = [
'wheezy.captcha', 'wheezy.captcha',
'ziggurat-foundations', 'ziggurat-foundations',
'zope.sqlalchemy', 'zope.sqlalchemy',
'websockets',
"asyncio",
] ]
requires-python = '>= 3.8' requires-python = '>= 3.8'
authors = [ authors = [
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!