Commit 324d8d62 by aa.gusti

perbaikan template upload

1 parent 8783308f
...@@ -2,13 +2,14 @@ import locale ...@@ -2,13 +2,14 @@ import locale
import logging import logging
import re import re
# from opensipkd.tools.captcha import get_captcha_url
from .routes import routes from .routes import routes
try: try:
from urllib import (urlencode, quote, quote_plus, ) from urllib import (urlencode, quote, quote_plus, )
except ImportError: except ImportError:
from urllib.parse import (urlencode, quote, quote_plus, ) from urllib.parse import (urlencode, quote, quote_plus, )
from collections import OrderedDict
from pyramid.events import NewRequest from pyramid.events import NewRequest
from pyramid.config import Configurator from pyramid.config import Configurator
from pyramid_beaker import session_factory_from_settings from pyramid_beaker import session_factory_from_settings
...@@ -498,7 +499,7 @@ def get_module_submenus(parent_id): ...@@ -498,7 +499,7 @@ def get_module_submenus(parent_id):
q = DBSession.query(Route) \ q = DBSession.query(Route) \
.filter(Route.parent_id == parent_id) \ .filter(Route.parent_id == parent_id) \
.order_bY(Route.order_id) .order_bY(Route.order_id)
return [r.kode for r in query.all()] return [r.kode for r in q.all()]
partner_idcard_url = 'partner/idcard' partner_idcard_url = 'partner/idcard'
...@@ -561,6 +562,7 @@ def main(global_config, **settings): ...@@ -561,6 +562,7 @@ def main(global_config, **settings):
config.add_request_method(is_devel, 'devel', reify=True) config.add_request_method(is_devel, 'devel', reify=True)
config.add_request_method(get_host, '_host', reify=True) config.add_request_method(get_host, '_host', reify=True)
config.add_request_method(get_host, 'home', reify=True) config.add_request_method(get_host, 'home', reify=True)
# config.add_request_method(get_captcha_url, 'captcha', reify=True)
# config.add_request_method(get_urls, 'route_urls', reify=True) # config.add_request_method(get_urls, 'route_urls', reify=True)
config.add_request_method(google_signin_client_id, config.add_request_method(google_signin_client_id,
'google_signin_client_id', reify=True) 'google_signin_client_id', reify=True)
...@@ -592,6 +594,10 @@ def main(global_config, **settings): ...@@ -592,6 +594,10 @@ def main(global_config, **settings):
os.makedirs(captcha_files) os.makedirs(captcha_files)
config.add_static_view('captcha', captcha_files) config.add_static_view('captcha', captcha_files)
config.add_static_view('partner/files',
get_params("partner_files", settings=settings,
alternate="/tmp/partner"))
config.add_renderer('csv', 'opensipkd.tools.CSVRenderer') config.add_renderer('csv', 'opensipkd.tools.CSVRenderer')
config.add_renderer('json', json_renderer()) config.add_renderer('json', json_renderer())
config.add_renderer('json_rpc', json_rpc()) config.add_renderer('json_rpc', json_rpc())
......
...@@ -3,32 +3,25 @@ from datetime import timedelta ...@@ -3,32 +3,25 @@ from datetime import timedelta
import colander import colander
from deform import ( from deform import (
Form, ValidationFailure, widget, Button, ) Form, ValidationFailure, widget, Button, FileData)
from opensipkd.tools.api import JsonRpcInvalidLoginError from opensipkd.base import get_params, get_urls
from opensipkd.models import (
DBSession, UserService, )
from opensipkd.tools import mem_tmp_store
from pyramid.httpexceptions import ( from pyramid.httpexceptions import (
HTTPFound, HTTPForbidden, HTTPNotFound, HTTPInternalServerError, HTTPFound, HTTPForbidden, HTTPNotFound, HTTPInternalServerError,
HTTPSeeOther) HTTPSeeOther)
from pyramid.i18n import TranslationStringFactory from pyramid.i18n import TranslationStringFactory
from pyramid.interfaces import IRoutesMapper from pyramid.interfaces import IRoutesMapper
from pyramid.renderers import render_to_response from pyramid.renderers import render_to_response
from pyramid.response import Response
from pyramid.security import remember
from pyramid.view import view_config from pyramid.view import view_config
from opensipkd.base import get_params, get_urls from .base_views import BaseView, DataTables, ColumnDT
from opensipkd.base.tools.api import rpc_auth
from .base_views import BaseView
from opensipkd.models import (
DBSession, UserService, )
from .common import DataTables, ColumnDT
from pyramid.csrf import new_csrf_token
_ = TranslationStringFactory('login') _ = TranslationStringFactory('login')
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@view_config(context=HTTPNotFound, renderer='templates/404.pt') @view_config(context=HTTPNotFound, renderer='templates/404.pt')
def not_found(request): def not_found(request):
path = request.path path = request.path
...@@ -55,6 +48,33 @@ def internal_server_error(request): ...@@ -55,6 +48,33 @@ def internal_server_error(request):
# return response # return response
class Validator(object):
def __init__(self, row):
self.row = row
class FileSchema(colander.Schema):
file_name = colander.SchemaNode(
FileData(),
widget=widget.FileUploadWidget(mem_tmp_store, size=104857600),
missing=colander.drop,
title="File"
)
description = colander.SchemaNode(
colander.String(),
missing=colander.drop,
validator=colander.Length(max=256),
)
class FilesSchema(colander.SequenceSchema):
file_name = FileSchema()
def after_bin(self, node, kw):
self["file_name"].title = ""
######## ########
# Home # # Home #
######## ########
...@@ -106,14 +126,14 @@ class Password(colander.Schema): ...@@ -106,14 +126,14 @@ class Password(colander.Schema):
new_password = colander.SchemaNode( new_password = colander.SchemaNode(
colander.String(), widget=widget.CheckedPasswordWidget()) colander.String(), widget=widget.CheckedPasswordWidget())
# retype_password = colander.SchemaNode( # retype_password = colander.SchemaNode(
# colander.String(), widget=widget.PasswordWidget()) # colander.String(), widget=widget.PasswordWidget())
def password_validator(form, value): def password_validator(form, value):
if not UserService.check_password(form.request.user, value['old_password']): if not UserService.check_password(form.request.user, value['old_password']):
raise colander.Invalid(form, 'Invalid old password.') raise colander.Invalid(form, 'Invalid old password.')
# if value['new_password'] != value['retype_password']: # if value['new_password'] != value['retype_password']:
# raise colander.Invalid(form, 'Retype mismatch.') # raise colander.Invalid(form, 'Retype mismatch.')
@view_config( @view_config(
...@@ -151,6 +171,4 @@ two_minutes = timedelta(1.0 / 24 / 60) ...@@ -151,6 +171,4 @@ two_minutes = timedelta(1.0 / 24 / 60)
@colander.deferred @colander.deferred
def deferred_jenis(node, kw): def deferred_jenis(node, kw):
values = kw.get('daftar_jenis', []) values = kw.get('daftar_jenis', [])
return widget.RadioChoiceWidget(values=values) return widget.RadioChoiceWidget(values=values)
\ No newline at end of file \ No newline at end of file
...@@ -9,7 +9,6 @@ from datatables import ColumnDT ...@@ -9,7 +9,6 @@ from datatables import ColumnDT
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
from deform import (widget, Form, ValidationFailure, FileData, ) from deform import (widget, Form, ValidationFailure, FileData, )
from deform.widget import SelectWidget from deform.widget import SelectWidget
from opensipkd.base.views.upload import tmpstore
from opensipkd.tools import dmy, get_settings, get_ext, \ from opensipkd.tools import dmy, get_settings, get_ext, \
date_from_str, get_random_string date_from_str, get_random_string
from opensipkd.tools.buttons import btn_save, btn_cancel, btn_close, btn_delete, \ from opensipkd.tools.buttons import btn_save, btn_cancel, btn_close, btn_delete, \
...@@ -19,6 +18,7 @@ from opensipkd.tools.captcha import get_captcha ...@@ -19,6 +18,7 @@ from opensipkd.tools.captcha import get_captcha
from opensipkd.tools.report import csv_response, file_response from opensipkd.tools.report import csv_response, file_response
from pyramid.httpexceptions import HTTPFound, HTTPNotFound from pyramid.httpexceptions import HTTPFound, HTTPNotFound
from opensipkd.base.views.upload import tmpstore
from .common import DataTables from .common import DataTables
from .. import DBSession, get_params, get_urls from .. import DBSession, get_params, get_urls
from ..scripts.initializedb import append_csv from ..scripts.initializedb import append_csv
...@@ -149,6 +149,9 @@ class BaseView(object): ...@@ -149,6 +149,9 @@ class BaseView(object):
self.edit_schema = "" self.edit_schema = ""
self.add_schema = "" self.add_schema = ""
self.upload_schema = UploadSchema self.upload_schema = UploadSchema
self.upload_exts = (".csv", ".tsv")
self.upload_keys = ["kode"]
self.table = "" self.table = ""
self.home = self.req._host self.home = self.req._host
self.buttons = None self.buttons = None
...@@ -156,7 +159,6 @@ class BaseView(object): ...@@ -156,7 +159,6 @@ class BaseView(object):
self.bindings = {} self.bindings = {}
self.autocomplete = 'on' self.autocomplete = 'on'
self.action_suffix = "/grid/act" self.action_suffix = "/grid/act"
self.upload_keys = ["kode"]
self.report_file = "" self.report_file = ""
self.new_buttons = {} self.new_buttons = {}
self.is_object = False self.is_object = False
...@@ -263,7 +265,7 @@ class BaseView(object): ...@@ -263,7 +265,7 @@ class BaseView(object):
return arg return arg
def get_bindings(self, row=None): def get_bindings(self, row=None):
return {} return {"row": row}
def next_view(self, form, **kwargs): def next_view(self, form, **kwargs):
return return
...@@ -296,11 +298,20 @@ class BaseView(object): ...@@ -296,11 +298,20 @@ class BaseView(object):
result = (btn_close,) result = (btn_close,)
return result return result
def before_view(self, **kw):
return False
def view_view(self, **kwargs): # row = query_id(request).first() def view_view(self, **kwargs): # row = query_id(request).first()
request = self.req request = self.req
row = self.query_id().first() row = self.query_id().first()
if not row: if not row:
return self.id_not_found() return self.id_not_found()
before_view = self.before_view(row=row)
if before_view:
return before_view
bindings = self.get_bindings(row) bindings = self.get_bindings(row)
buttons = kwargs.get("buttons", None) buttons = kwargs.get("buttons", None)
if not buttons: if not buttons:
...@@ -339,7 +350,11 @@ class BaseView(object): ...@@ -339,7 +350,11 @@ class BaseView(object):
buttons = (btn_post, btn_close) buttons = (btn_post, btn_close)
return self.view_view(buttons=buttons) return self.view_view(buttons=buttons)
def view_upload(self, exts=('.png', '.ico'), **args): def view_upload(self, **kw):
exts = kw.get("exts")
if not exts:
exts = self.upload_exts
delimiter = args.get("delimiter") delimiter = args.get("delimiter")
bindings = self.get_bindings() bindings = self.get_bindings()
form = self.get_form(self.upload_schema, bindings=bindings) form = self.get_form(self.upload_schema, bindings=bindings)
...@@ -516,6 +531,9 @@ class BaseView(object): ...@@ -516,6 +531,9 @@ class BaseView(object):
else: else:
return self.next_act(**kwargs) return self.next_act(**kwargs)
def get_captcha_url(self):
return get_urls("/captcha/") + get_captcha(self.req)
def view_add(self, **kwargs): def view_add(self, **kwargs):
# bindings = self.get_bindings() # bindings = self.get_bindings()
form = self.get_form(self.add_schema, **kwargs) form = self.get_form(self.add_schema, **kwargs)
...@@ -538,6 +556,8 @@ class BaseView(object): ...@@ -538,6 +556,8 @@ class BaseView(object):
if isinstance(f.typ, colander.Date): if isinstance(f.typ, colander.Date):
e.cstruct[f.name] = date_from_str( e.cstruct[f.name] = date_from_str(
e.cstruct[f.name]) e.cstruct[f.name])
if f.name == "captcha":
e.cstruct[f.name] = self.get_captcha_url()
form.set_appstruct(e.cstruct) form.set_appstruct(e.cstruct)
return self.returned_form(form, table, **kwargs) return self.returned_form(form, table, **kwargs)
...@@ -703,8 +723,9 @@ class BaseView(object): ...@@ -703,8 +723,9 @@ class BaseView(object):
def query_id(self): def query_id(self):
q = self.db_session.query(self.table).filter_by( q = self.db_session.query(self.table).filter_by(
id=self.req.matchdict['id']) id=self.req.matchdict['id'])
if hasattr(self.table, 'company_id') and self.req.user.company_id: if self.req.user:
q = q.filter_by(company_id=self.req.user.company_id) if hasattr(self.table, 'company_id') and self.req.user.company_id:
q = q.filter_by(company_id=self.req.user.company_id)
return q return q
def filter_company(self, query): def filter_company(self, query):
......
...@@ -2,17 +2,17 @@ import colander ...@@ -2,17 +2,17 @@ import colander
from deform import ( from deform import (
widget, Button, widget, Button,
) )
from pyramid.i18n import TranslationStringFactory
from pyramid.view import (
view_config,
)
from opensipkd.base import get_id_card_folder from opensipkd.base import get_id_card_folder
from opensipkd.models import DBSession, Partner from opensipkd.models import DBSession, Partner
from opensipkd.models import ( from opensipkd.models import (
ResProvinsi, ResDati2, ResKecamatan, ResDesa) ResProvinsi, ResDati2, ResKecamatan, ResDesa)
from opensipkd.models.common import ResCompany from opensipkd.models.common import ResCompany
from opensipkd.tools import Upload, img_exts from opensipkd.tools import Upload, img_exts
from pyramid.i18n import TranslationStringFactory
from pyramid.view import (
view_config,
)
from .company import company_widget from .company import company_widget
from .partner_base import PartnerSchema from .partner_base import PartnerSchema
# from .. import partner_idcard_url # from .. import partner_idcard_url
...@@ -243,6 +243,7 @@ class ViewPartner(BaseView): ...@@ -243,6 +243,7 @@ class ViewPartner(BaseView):
value["status"] = 'status' in value and value['status'] and 1 or 0 value["status"] = 'status' in value and value['status'] and 1 or 0
def get_bindings(self, row=None): def get_bindings(self, row=None):
result = super().get_bindings(row)
provinsi_list = ResProvinsi.get_list() provinsi_list = ResProvinsi.get_list()
dati2_list = row and row.provinsi_id and ResDati2.get_list( dati2_list = row and row.provinsi_id and ResDati2.get_list(
row.provinsi_id) or [] row.provinsi_id) or []
...@@ -250,17 +251,19 @@ class ViewPartner(BaseView): ...@@ -250,17 +251,19 @@ class ViewPartner(BaseView):
row.dati2_id) or [] row.dati2_id) or []
desa_list = row and row.kecamatan_id and ResDesa.get_list( desa_list = row and row.kecamatan_id and ResDesa.get_list(
row.kecamatan_id) or [] row.kecamatan_id) or []
return dict( result.update(dict(
provinsi_list=provinsi_list, provinsi_list=provinsi_list,
dati2_list=dati2_list, dati2_list=dati2_list,
kecamatan_list=kecamatan_list, kecamatan_list=kecamatan_list,
desa_list=desa_list, desa_list=desa_list,
company_list=ResCompany.get_list() company_list=ResCompany.get_list()
) ))
return result
def save_request(self, values, row=None): def save_request(self, values, row=None):
if "idcard" in values and values["idcard"]: if "idcard" in values and values["idcard"]:
if str(self.req.POST['upload'].decode('utf-8')) != "": if str(self.req.POST['upload']) != "":
folder = self.get_params("idcard_folder", '/tmp/idcard') folder = self.get_params("idcard_folder", '/tmp/idcard')
upload = Upload(folder) upload = Upload(folder)
file_name = upload.save(self.req, 'upload', img_exts) file_name = upload.save(self.req, 'upload', img_exts)
...@@ -288,4 +291,4 @@ class ViewPartner(BaseView): ...@@ -288,4 +291,4 @@ class ViewPartner(BaseView):
@colander.deferred @colander.deferred
def partner_widget(node, kw): def partner_widget(node, kw):
values = kw.get('partner_list', []) values = kw.get('partner_list', [])
return widget.Select2Widget(values=values) return widget.Select2Widget(values=values)
\ No newline at end of file \ No newline at end of file
...@@ -5,15 +5,70 @@ from opensipkd.base.views.dati2 import dati2_widget ...@@ -5,15 +5,70 @@ from opensipkd.base.views.dati2 import dati2_widget
from opensipkd.base.views.desa import desa_widget from opensipkd.base.views.desa import desa_widget
from opensipkd.base.views.kecamatan import kecamatan_widget from opensipkd.base.views.kecamatan import kecamatan_widget
from opensipkd.base.views.provinsi import provinsi_widget from opensipkd.base.views.provinsi import provinsi_widget
from opensipkd.models import Partner
from opensipkd.tools import mem_tmp_store from opensipkd.tools import mem_tmp_store
from translationstring import TranslationStringFactory
from .. import get_urls from .. import get_urls
from . import Validator
_ = TranslationStringFactory('partner')
class PartnerEmailValidator(colander.Email, Validator):
def __init__(self, row):
Validator.__init__(self, row)
colander.Email.__init__(self)
def __call__(self, node, value):
def email_found():
data = dict(email=email, rid=found.id, rname=found.nama)
ts = _(
'email-already-used',
default='Email ${email} already used by Partner ID ${rid}: ${rname}',
mapping=data)
raise colander.Invalid(node, ts)
if self.match_object.match(value) is None:
raise colander.Invalid(node, _('Invalid email format'))
email = value.lower()
q = Partner.query().filter_by(email=email)
found = q.first()
if found and (not self.row or self.row.email != found.email):
email_found()
@colander.deferred
def partner_email_validator(node, kw):
return PartnerEmailValidator(kw['row'])
class PartnerKodeValidator(Validator):
def __init__(self, row):
Validator.__init__(self, row)
def __call__(self, node, value):
def err_found():
data = dict(kode=val, rid=found.id, rnama=found.nama)
ts = _(
'kode-already-used',
default='Kode ${kode} already used by Partner ID ${rid}: ${rnama}',
mapping=data)
raise colander.Invalid(node, ts)
val = value
q = Partner.query().filter_by(kode=val)
found = q.first()
if found and (not self.row or self.row.kode != found.kode):
err_found()
@colander.deferred
def partner_kode_validator(node, kw):
return PartnerKodeValidator(kw['row'])
class NamaSchema(colander.Schema): class NamaSchema(colander.Schema):
kode = colander.SchemaNode( kode = colander.SchemaNode(
colander.String(), colander.String(),
validator=colander.Length(max=32), validator=partner_kode_validator,
oid="kode", oid="kode",
title="Kode", title="Kode",
width="100pt") width="100pt")
...@@ -21,7 +76,10 @@ class NamaSchema(colander.Schema): ...@@ -21,7 +76,10 @@ class NamaSchema(colander.Schema):
colander.String(), colander.String(),
validator=colander.Length(max=64), validator=colander.Length(max=64),
oid="nama") oid="nama")
email = colander.SchemaNode(
colander.String(),
validator=partner_email_validator,
oid="email")
class PartnerSchema(NamaSchema): class PartnerSchema(NamaSchema):
nip = colander.SchemaNode( nip = colander.SchemaNode(
...@@ -103,10 +161,7 @@ class PartnerSchema(NamaSchema): ...@@ -103,10 +161,7 @@ class PartnerSchema(NamaSchema):
missing=colander.drop, missing=colander.drop,
title="Desa/Kelurahan", title="Desa/Kelurahan",
oid="desa_id") oid="desa_id")
email = colander.SchemaNode(
colander.String(),
validator=colander.Length(max=128),
oid="email")
phone = colander.SchemaNode( phone = colander.SchemaNode(
colander.String(), colander.String(),
validator=colander.Length(max=16), validator=colander.Length(max=16),
...@@ -136,4 +191,4 @@ class PartnerSchema(NamaSchema): ...@@ -136,4 +191,4 @@ class PartnerSchema(NamaSchema):
prefix = get_urls(request.route_url("home")) prefix = get_urls(request.route_url("home"))
self["provinsi_id"].slave_url = f"{prefix}/dati2/select/act?provinsi_id=" self["provinsi_id"].slave_url = f"{prefix}/dati2/select/act?provinsi_id="
self["dati2_id"].slave_url = f"{prefix}/kecamatan/select/act?dati2_id=" self["dati2_id"].slave_url = f"{prefix}/kecamatan/select/act?dati2_id="
self["kecamatan_id"].slave_url = f"{prefix}/desa/select/act?kecamatan_id=" self["kecamatan_id"].slave_url = f"{prefix}/desa/select/act?kecamatan_id="
\ No newline at end of file \ No newline at end of file
...@@ -22,4 +22,4 @@ ...@@ -22,4 +22,4 @@
}); });
</script> </script>
</span> </span>
\ No newline at end of file \ No newline at end of file
<tal:block tal:define="oid oid|field.oid; <tal:block tal:define="oid oid|field.oid;
css_class css_class|field.widget.css_class; css_class css_class|field.widget.css_class;
style style|field.widget.style; style style|field.widget.style;
preview_url cstruct.get('preview_url')|''; preview_url cstruct.get('preview_url')|'';
ext str(cstruct.get('filename').split('.')[-1:][0]).lower()|[]; ext str(cstruct.get('filename').split('.')[-1:][0]).lower()|[];
fname str(cstruct.get('filename'))|''; fname str(cstruct.get('filename'))|'';
delete cstruct.get('delete')|''; delete cstruct.get('delete')|'';
maxsize field.widget.size|5242880;"> maxsize field.widget.size|5242880;
${field.start_mapping()} img ['jpg', 'jpeg', 'gif', 'png', 'svg', 'eps', 'psd'];
<img id="preview-${oid}" alt="" src="${preview_url}" style="width:100px;height:auto;display:block;" video ['avi', 'mov', 'mpv']
onload="window.URL.revokeObjectURL(this.src);"></img> ">
<a id="label-${oid}" tal:condition="preview_url" class="label label-default" href="${preview_url}" ${field.start_mapping()}
target="_blank"><i class="fa fa-search"></i> View</a> <img tal:condition="ext in img" id="preview-${oid}" alt="" src="${preview_url}"
<input type="file" name="upload" id="${oid}" style="width:100px;height:auto;display:block;"
tal:attributes="style style; onload="window.URL.revokeObjectURL(this.src);"></img>
accept accept|field.widget.accept; <video tal:condition="ext in video" width="100%" height="100%" controls
data-filename fname; src="${preview_url}" type="video/${ext}">
attributes|field.widget.attributes|{};"/> Your browser does not support the video tag.
<input tal:define="uid cstruct.get('uid')" </video>
tal:condition="uid"
type="hidden" name="uid" value="${uid}"/> <video tal:condition="ext=='mp4'" width="100%" height="100%" controls
${field.end_mapping()} src="${preview_url}" type="video/mp4">
<script type="text/javascript"> Your browser does not support the video tag.
deform.addCallback('${oid}', function (oid) { </video>
$('#' + oid).upload();
}); <embed tal:condition="ext=='pdf'" src="${preview_url}" width="100%" height="400">
document.getElementById("${oid}").onchange = function() {
if(this.files[0].size > ${maxsize}){ <a id="label-${oid}" tal:condition="preview_url" class="label label-default" href="${preview_url}"
alert("File is too big!"); target="_blank"><i class="fa fa-search"></i> View</a>
this.value = "";
document.getElementById('preview-'+this.id).src = ''; <input type="file" name="upload" id="${oid}"
} tal:attributes="style style;
document.getElementById('preview-'+this.id).src = window.URL.createObjectURL(this.files[0]); accept accept|field.widget.accept;
document.getElementById('labeldelete-'+this.id).remove(); data-filename fname;
document.getElementById('label-'+this.id).remove(); attributes|field.widget.attributes|{};"/>
}; <input tal:define="uid cstruct.get('uid')"
</script> tal:condition="uid"
</tal:block> type="hidden" name="uid" value="${uid}"/>
${field.end_mapping()}
<script type="text/javascript">
deform.addCallback('${oid}', function (oid) {
$('#' + oid).upload();
});
document.getElementById("${oid}").onchange = function () {
if (this.files[0].size > ${maxsize}) {
alert("File is too big!");
this.value = "";
document.getElementById('preview-' + this.id).src = '';
}
document.getElementById('preview-' + this.id).src = window.URL.createObjectURL(this.files[0]);
document.getElementById('labeldelete-' + this.id).remove();
document.getElementById('label-' + this.id).remove();
};
</script>
</tal:block>
\ No newline at end of file \ No newline at end of file
...@@ -41,6 +41,7 @@ ...@@ -41,6 +41,7 @@
<div tal:repeat="child field" <div tal:repeat="child field"
tal:replace="structure child.render_template(item_template)"/> tal:replace="structure child.render_template(item_template)"/>
</div> </div>
<div class="row"> <div class="row">
<div class="form-group deform-form-buttons"> <div class="form-group deform-form-buttons">
<tal:loop tal:repeat="button buttons"> <tal:loop tal:repeat="button buttons">
...@@ -105,4 +106,4 @@ ...@@ -105,4 +106,4 @@
); );
</script> </script>
</form> </form>
\ No newline at end of file \ No newline at end of file
...@@ -5,11 +5,12 @@ from sqlalchemy import ( ...@@ -5,11 +5,12 @@ from sqlalchemy import (
SmallInteger, SmallInteger,
DateTime, ForeignKey DateTime, ForeignKey
) )
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.orm import backref
from .common import NamaModel from .base import NamaModel, StandarModel
from .wilayah import ResProvinsi, ResDesa, ResKecamatan, ResDati2
from .meta import (Base) from .meta import (Base)
from sqlalchemy.orm import relationship, backref from .wilayah import ResProvinsi, ResDesa, ResKecamatan, ResDati2
class PartnerModel(NamaModel): class PartnerModel(NamaModel):
...@@ -76,6 +77,7 @@ class Partner(Base, PartnerModel): ...@@ -76,6 +77,7 @@ class Partner(Base, PartnerModel):
"ResKecamatan", backref=backref('partner')) "ResKecamatan", backref=backref('partner'))
res_desa = relationship( res_desa = relationship(
"ResDesa", backref=backref('partner')) "ResDesa", backref=backref('partner'))
partner_files: Mapped["PartnerFiles"] = relationship(back_populates="partner")
# npwp = Column(String(16)) # npwp = Column(String(16))
# npwpd = Column(String(16)) # npwpd = Column(String(16))
...@@ -97,7 +99,15 @@ class Partner(Base, PartnerModel): ...@@ -97,7 +99,15 @@ class Partner(Base, PartnerModel):
row = cls.query().filter_by(mobile=ident).first() row = cls.query().filter_by(mobile=ident).first()
return row return row
class PartnerFiles(Base, StandarModel):
__tablename__ = 'partner_files'
partner_id: Mapped[int] = mapped_column(ForeignKey(Partner.id))
file_name: Mapped[str] = mapped_column(String(256))
description: Mapped[str] = mapped_column(String(256), nullable=True)
partner: Mapped["Partner"] = relationship(back_populates="partner_files")
# class PartnerUserModel(Base, DefaultModel): # class PartnerUserModel(Base, DefaultModel):
# __tablename__ = 'partner_user' # __tablename__ = 'partner_user'
# partner_id = Column(Integer, ForeignKey(Partner.id)) # partner_id = Column(Integer, ForeignKey(Partner.id))
# user_id = Column(Integer, ForeignKey(User.id)) # user_id = Column(Integer, ForeignKey(User.id))
\ No newline at end of file \ No newline at end of file
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!