Refactory class certificates

This commit is contained in:
Mauricio Baeza 2020-12-30 22:45:57 -06:00
parent 56e52782f4
commit 85c5a37798
24 changed files with 513 additions and 1128 deletions

View File

@ -13,6 +13,7 @@ pypng
reportlab
psycopg2-binary
cryptography
xmlsec
# escpos
# pyusb

View File

@ -0,0 +1,256 @@
#!/usr/bin/env python3
import argparse
import base64
import datetime
import getpass
from pathlib import Path
import xmlsec
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
from cryptography import x509
from cryptography.x509.oid import NameOID
from cryptography.x509.oid import ExtensionOID
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding
from .conf import TOKEN
class SATCertificate(object):
def __init__(self, cer=b'', key=b'', password=''):
self._error = ''
self._init_values()
self._get_data_cer(cer)
self._get_data_key(key, password)
def _init_values(self):
self._rfc = ''
self._serial_number = ''
self._not_before = None
self._not_after = None
self._is_fiel = False
self._are_couple = False
self._is_valid_time = False
self._cer_pem = ''
self._cer_txt = ''
self._key_enc = b''
self._p12 = b''
self._cer_modulus = 0
self._key_modulus = 0
return
def __str__(self):
msg = '\tRFC: {}\n'.format(self.rfc)
msg += '\tNo de Serie: {}\n'.format(self.serial_number)
msg += '\tVálido desde: {}\n'.format(self.not_before)
msg += '\tVálido hasta: {}\n'.format(self.not_after)
msg += '\tEs vigente: {}\n'.format(self.is_valid_time)
msg += '\tSon pareja: {}\n'.format(self.are_couple)
msg += '\tEs FIEL: {}\n'.format(self.is_fiel)
return msg
def __bool__(self):
return self.is_valid
def _get_hash(self):
digest = hashes.Hash(hashes.SHA512(), default_backend())
digest.update(self._rfc.encode())
digest.update(self._serial_number.encode())
digest.update(TOKEN.encode())
return digest.finalize()
def _get_data_cer(self, cer):
obj = x509.load_der_x509_certificate(cer, default_backend())
self._rfc = obj.subject.get_attributes_for_oid(
NameOID.X500_UNIQUE_IDENTIFIER)[0].value.split(' ')[0]
self._serial_number = '{0:x}'.format(obj.serial_number)[1::2]
self._not_before = obj.not_valid_before
self._not_after = obj.not_valid_after
now = datetime.datetime.utcnow()
self._is_valid_time = (now > self.not_before) and (now < self.not_after)
if not self._is_valid_time:
msg = 'El certificado no es vigente'
self._error = msg
self._is_fiel = obj.extensions.get_extension_for_oid(
ExtensionOID.KEY_USAGE).value.key_agreement
self._cer_pem = obj.public_bytes(serialization.Encoding.PEM).decode()
self._cer_txt = ''.join(self._cer_pem.split('\n')[1:-2])
self._cer_modulus = obj.public_key().public_numbers().n
return
def _get_data_key(self, key, password):
self._key_enc = key
if not key or not password:
return
try:
obj = serialization.load_der_private_key(
key, password.encode(), default_backend())
except ValueError:
msg = 'La contraseña es incorrecta'
self._error = msg
return
p = self._get_hash()
self._key_enc = obj.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.PKCS8,
encryption_algorithm=serialization.BestAvailableEncryption(p)
)
self._key_modulus = obj.public_key().public_numbers().n
self._are_couple = self._cer_modulus == self._key_modulus
if not self._are_couple:
msg = 'El CER y el KEY no son pareja'
self._error = msg
return
def _get_key(self, password):
if not password:
password = self._get_hash()
private_key = serialization.load_pem_private_key(
self._key_enc, password=password, backend=default_backend())
return private_key
def _get_key_pem(self):
obj = self._get_key('')
key_pem = obj.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.PKCS8,
encryption_algorithm=serialization.NoEncryption()
)
return key_pem
# Not work
def _get_p12(self):
obj = serialization.pkcs12.serialize_key_and_certificates('test',
self.key_pem, self.cer_pem, None,
encryption_algorithm=serialization.NoEncryption()
)
return obj
def sign(self, data, password=''):
private_key = self._get_key(password)
firma = private_key.sign(data, padding.PKCS1v15(), hashes.SHA256())
return base64.b64encode(firma).decode()
def sign_xml(self, tree):
node = xmlsec.tree.find_node(tree, xmlsec.constants.NodeSignature)
ctx = xmlsec.SignatureContext()
key = xmlsec.Key.from_memory(self.key_pem, xmlsec.constants.KeyDataFormatPem)
ctx.key = key
ctx.sign(node)
node = xmlsec.tree.find_node(tree, 'X509Certificate')
node.text = self.cer_txt
return tree
@property
def rfc(self):
return self._rfc
@property
def serial_number(self):
return self._serial_number
@property
def not_before(self):
return self._not_before
@property
def not_after(self):
return self._not_after
@property
def is_fiel(self):
return self._is_fiel
@property
def are_couple(self):
return self._are_couple
@property
def is_valid(self):
return not bool(self.error)
@property
def is_valid_time(self):
return self._is_valid_time
@property
def cer_pem(self):
return self._cer_pem.encode()
@property
def cer_txt(self):
return self._cer_txt
@property
def key_pem(self):
return self._get_key_pem()
@property
def key_enc(self):
return self._key_enc
@property
def p12(self):
return self._get_p12()
@property
def error(self):
return self._error
def main(args):
# ~ contra = getpass.getpass('Introduce la contraseña del archivo KEY: ')
contra = '12345678a'
if not contra.strip():
msg = 'La contraseña es requerida'
print(msg)
return
path_cer = Path(args.cer)
path_key = Path(args.key)
if not path_cer.is_file():
msg = 'El archivo CER es necesario'
print(msg)
return
if not path_key.is_file():
msg = 'El archivo KEY es necesario'
print(msg)
return
cer = path_cer.read_bytes()
key = path_key.read_bytes()
cert = SATCertificate(cer, key, contra)
if cert.error:
print(cert.error)
else:
print(cert)
return
def _process_command_line_arguments():
parser = argparse.ArgumentParser(description='CFDI Certificados')
help = 'Archivo CER'
parser.add_argument('-c', '--cer', help=help, default='')
help = 'Archivo KEY'
parser.add_argument('-k', '--key', help=help, default='')
args = parser.parse_args()
return args
if __name__ == '__main__':
args = _process_command_line_arguments()
main(args)

View File

@ -1,3 +0,0 @@
#!/usr/bin/env python3
from .comercio import PACComercioDigital

View File

@ -1,342 +0,0 @@
#!/usr/bin/env python
# ~
# ~ PAC
# ~ Copyright (C) 2018-2019 Mauricio Baeza Servin - public [AT] elmau [DOT] net
# ~
# ~ This program is free software: you can redistribute it and/or modify
# ~ it under the terms of the GNU General Public License as published by
# ~ the Free Software Foundation, either version 3 of the License, or
# ~ (at your option) any later version.
# ~
# ~ This program is distributed in the hope that it will be useful,
# ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
# ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# ~ GNU General Public License for more details.
# ~
# ~ You should have received a copy of the GNU General Public License
# ~ along with this program. If not, see <http://www.gnu.org/licenses/>.
import logging
import lxml.etree as ET
import requests
from requests.exceptions import ConnectionError
LOG_FORMAT = '%(asctime)s - %(levelname)s - %(message)s'
LOG_DATE = '%d/%m/%Y %H:%M:%S'
logging.addLevelName(logging.ERROR, '\033[1;41mERROR\033[1;0m')
logging.addLevelName(logging.DEBUG, '\x1b[33mDEBUG\033[1;0m')
logging.addLevelName(logging.INFO, '\x1b[32mINFO\033[1;0m')
logging.basicConfig(level=logging.DEBUG, format=LOG_FORMAT, datefmt=LOG_DATE)
log = logging.getLogger(__name__)
logging.getLogger('requests').setLevel(logging.ERROR)
try:
from .conf import DEBUG, AUTH
except ImportError:
DEBUG = False
log.debug('Need make conf.py')
TIMEOUT = 10
class PACComercioDigital(object):
ws = 'https://{}.comercio-digital.mx/{}'
api = 'https://app2.comercio-digital.mx/{}'
URL = {
'timbra': ws.format('ws', 'timbre/timbrarV5.aspx'),
'cancel': ws.format('cancela', 'cancela3/cancelarUuid'),
'cancelxml': ws.format('cancela', 'cancela3/cancelarXml'),
'client': api.format('x3/altaEmpresa'),
'saldo': api.format('x3/saldo'),
'timbres': api.format('x3/altaTimbres'),
}
CODES = {
'000': '000 Exitoso',
'004': '004 RFC {} ya esta dado de alta con Estatus=A',
'704': '704 Usuario Invalido',
}
NS_CFDI = {
'cfdi': 'http://www.sat.gob.mx/cfd/3',
'tdf': 'http://www.sat.gob.mx/TimbreFiscalDigital',
}
if DEBUG:
ws = 'https://pruebas.comercio-digital.mx/{}'
URL = {
'timbra': ws.format('timbre/timbrarV5.aspx'),
'cancel': ws.format('cancela3/cancelarUuid'),
'cancelxml': ws.format('cancela3/cancelarXml'),
'client': api.format('x3/altaEmpresa'),
'saldo': api.format('x3/saldo'),
'timbres': api.format('x3/altaTimbres'),
}
def __init__(self):
self.error = ''
self.cfdi_uuid = ''
self.date_stamped = ''
def _error(self, msg):
self.error = str(msg)
log.error(msg)
return
def _post(self, url, data, headers={}):
result = None
headers['host'] = url.split('/')[2]
headers['Content-type'] = 'text/plain'
headers['Connection'] = 'Keep-Alive'
try:
result = requests.post(url, data=data, headers=headers, timeout=TIMEOUT)
except ConnectionError as e:
self._error(e)
return result
def _validate_cfdi(self, xml):
"""
Comercio Digital solo soporta la declaración con doble comilla
"""
tree = ET.fromstring(xml.encode())
xml = ET.tostring(tree,
pretty_print=True, doctype='<?xml version="1.0" encoding="utf-8"?>')
return xml
def stamp(self, cfdi, auth={}):
if DEBUG or not auth:
auth = AUTH
url = self.URL['timbra']
headers = {
'usrws': auth['user'],
'pwdws': auth['pass'],
'tipo': 'XML',
}
cfdi = self._validate_cfdi(cfdi)
result = self._post(url, cfdi, headers)
if result is None:
return ''
if result.status_code != 200:
return ''
if 'errmsg' in result.headers:
self._error(result.headers['errmsg'])
return ''
xml = result.content
tree = ET.fromstring(xml)
self.cfdi_uuid = tree.xpath(
'string(//cfdi:Complemento/tdf:TimbreFiscalDigital/@UUID)',
namespaces=self.NS_CFDI)
self.date_stamped = tree.xpath(
'string(//cfdi:Complemento/tdf:TimbreFiscalDigital/@FechaTimbrado)',
namespaces=self.NS_CFDI)
return xml.decode()
def _get_data_cancel(self, cfdi, info, auth):
NS_CFDI = {
'cfdi': 'http://www.sat.gob.mx/cfd/3',
'tdf': 'http://www.sat.gob.mx/TimbreFiscalDigital',
}
tree = ET.fromstring(cfdi)
tipo = tree.xpath(
'string(//cfdi:Comprobante/@TipoDeComprobante)',
namespaces=NS_CFDI)
total = tree.xpath(
'string(//cfdi:Comprobante/@Total)',
namespaces=NS_CFDI)
rfc_emisor = tree.xpath(
'string(//cfdi:Comprobante/cfdi:Emisor/@Rfc)',
namespaces=NS_CFDI)
rfc_receptor = tree.xpath(
'string(//cfdi:Comprobante/cfdi:Receptor/@Rfc)',
namespaces=NS_CFDI)
uid = tree.xpath(
'string(//cfdi:Complemento/tdf:TimbreFiscalDigital/@UUID)',
namespaces=NS_CFDI)
data = (
f"USER={auth['user']}",
f"PWDW={auth['pass']}",
f"RFCE={rfc_emisor}",
f"UUID={uid}",
f"PWDK={info['pass']}",
f"KEYF={info['key']}",
f"CERT={info['cer']}",
f"TIPO={info['tipo']}",
f"ACUS=SI",
f"RFCR={rfc_receptor}",
f"TIPOC={tipo}",
f"TOTAL={total}",
)
return '\n'.join(data)
def cancel(self, cfdi, info, auth={}):
if not auth:
auth = AUTH
url = self.URL['cancel']
data = self._get_data_cancel(cfdi, info, auth)
result = self._post(url, data)
if result is None:
return ''
if result.status_code != 200:
return ''
if result.headers['codigo'] != '000':
self._error(result.headers['errmsg'])
return ''
return result.content
def _get_headers_cancel_xml(self, cfdi, info, auth):
NS_CFDI = {
'cfdi': 'http://www.sat.gob.mx/cfd/3',
'tdf': 'http://www.sat.gob.mx/TimbreFiscalDigital',
}
tree = ET.fromstring(cfdi)
tipo = tree.xpath(
'string(//cfdi:Comprobante/@TipoDeComprobante)',
namespaces=NS_CFDI)
total = tree.xpath(
'string(//cfdi:Comprobante/@Total)',
namespaces=NS_CFDI)
rfc_receptor = tree.xpath(
'string(//cfdi:Comprobante/cfdi:Receptor/@Rfc)',
namespaces=NS_CFDI)
headers = {
'usrws': auth['user'],
'pwdws': auth['pass'],
'rfcr': rfc_receptor,
'total': total,
'tipocfdi': tipo,
}
headers.update(info)
return headers
def cancel_xml(self, cfdi, xml, info, auth={}):
if not auth:
auth = AUTH
url = self.URL['cancelxml']
headers = self._get_headers_cancel_xml(cfdi, info, auth)
result = self._post(url, xml, headers)
if result is None:
return ''
if result.status_code != 200:
return ''
if result.headers['codigo'] != '000':
self._error(result.headers['errmsg'])
return ''
return result.content
def _get_data_client(self, auth, values):
data = [f"usr_ws={auth['user']}", f"pwd_ws={auth['pass']}"]
fields = (
'rfc_contribuyente',
'nombre_contribuyente',
'calle',
'noExterior',
'noInterior',
'colonia',
'localidad',
'municipio',
'estado',
'pais',
'cp',
'contacto',
'telefono',
'email',
'rep_nom',
'rep_rfc',
'email_fact',
'pwd_asignado',
)
data += [f"{k}={values[k]}" for k in fields]
return '\n'.join(data)
def client_add(self, data):
auth = AUTH
url = self.URL['client']
data = self._get_data_client(auth, data)
result = self._post(url, data)
if result is None:
return False
if result.status_code != 200:
self._error(f'Code: {result.status_code}')
return False
if result.text != self.CODES['000']:
self._error(result.text)
return False
return True
def client_balance(self, data):
url = self.URL['saldo']
host = url.split('/')[2]
headers = {
'Content-type': 'text/plain',
'Host': host,
'Connection' : 'Keep-Alive',
}
try:
result = requests.get(url, params=data, headers=headers, timeout=TIMEOUT)
except ConnectionError as e:
self._error(e)
return ''
if result.status_code != 200:
return ''
if result.text == self.CODES['704']:
self._error(result.text)
return ''
return result.text
def client_add_timbres(self, data, auth={}):
if not auth:
auth = AUTH
url = self.URL['timbres']
data = '\n'.join((
f"usr_ws={auth['user']}",
f"pwd_ws={auth['pass']}",
f"rfc_recibir={data['rfc']}",
f"num_timbres={data['timbres']}"
))
result = self._post(url, data)
if result is None:
return False
if result.status_code != 200:
self._error(f'Code: {result.status_code}')
return False
if result.text != self.CODES['000']:
self._error(result.text)
return False
return True

View File

@ -1,40 +0,0 @@
#!/usr/bin/env python
# ~
# ~ PAC
# ~ Copyright (C) 2018-2019 Mauricio Baeza Servin - public [AT] elmau [DOT] net
# ~
# ~ This program is free software: you can redistribute it and/or modify
# ~ it under the terms of the GNU General Public License as published by
# ~ the Free Software Foundation, either version 3 of the License, or
# ~ (at your option) any later version.
# ~
# ~ This program is distributed in the hope that it will be useful,
# ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
# ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# ~ GNU General Public License for more details.
# ~
# ~ You should have received a copy of the GNU General Public License
# ~ along with this program. If not, see <http://www.gnu.org/licenses/>.
# ~ Siempre consulta la documentación de Finkok
# ~ AUTH = Puedes usar credenciales genericas para timbrar, o exclusivas para
# ~ cada emisor
# ~ RESELLER = Algunos procesos como agregar emisores, solo pueden ser usadas
# ~ con una cuenta de reseller
DEBUG = False
AUTH = {
'user': '',
'pass': '',
}
if DEBUG:
AUTH = {
'user': 'AAA010101AAA',
'pass': 'PWD',
}

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python3
from .conf import DEBUG, ID_INTEGRADOR, FINKOK
from .conf import DEBUG, FINKOK
DEBUG = DEBUG
TIMEOUT = 10
@ -11,34 +11,6 @@ TIMEOUT = 10
PAC = 'finkok'
def ecodex(debug):
NEW_SERVER = True
auth = {'ID': ID_INTEGRADOR}
if debug:
#~ No cambies este ID de pruebas
auth = {'ID': '2b3a8764-d586-4543-9b7e-82834443f219'}
base_url = 'https://servicios.ecodex.com.mx:4043/Servicio{}.svc?wsdl'
if NEW_SERVER:
base_url = 'https://serviciosnominas.ecodex.com.mx:4043/Servicio{}.svc?wsdl'
base_api = 'https://api.ecodex.com.mx/{}'
if debug:
base_url = 'https://wsdev.ecodex.com.mx:2045/Servicio{}.svc?wsdl'
base_api = 'https://pruebasapi.ecodex.com.mx/{}'
url = {
'seguridad': base_url.format('Seguridad'),
'clients': base_url.format('Clientes'),
'timbra': base_url.format('Timbrado'),
'token': base_api.format('token?version=2'),
'docs': base_api.format('api/documentos'),
'hash': base_api.format('api/Documentos/{}'),
'codes': {
'HASH': 'DUPLICIDAD EN HASH',
}
}
return auth, url
#~ IMPORTANTE: Si quieres hacer pruebas, con tu propio correo de usuario y
#~ contraseña, ponte en contacto con Finkok para que te asignen tus datos de
#~ acceso, consulta su documentación para ver las diferentes opciones de acceso.

View File

@ -632,3 +632,19 @@ class AppSociosCuentasBanco(object):
req.context['result'] = self._db.partners_accounts_bank(values)
resp.status = falcon.HTTP_200
class AppCert(object):
def __init__(self, db):
self._db = db
def on_get(self, req, resp):
values = req.params
req.context['result'] = self._db.cert_get(values)
resp.status = falcon.HTTP_200
def on_post(self, req, resp):
values = req.params
req.context['result'] = self._db.cert_post(values)
resp.status = falcon.HTTP_200

View File

@ -68,7 +68,7 @@ from settings import DEBUG, MV, log, template_lookup, COMPANIES, DB_SAT, \
PATH_XSLT, PATH_XSLTPROC, PATH_OPENSSL, PATH_TEMPLATES, PATH_MEDIA, PRE, \
PATH_XMLSEC, TEMPLATE_CANCEL, DEFAULT_SAT_PRODUCTO, DECIMALES, DIR_FACTURAS
from settings import SEAFILE_SERVER, USAR_TOKEN, API, DECIMALES_TAX
from settings import USAR_TOKEN, API, DECIMALES_TAX
from .configpac import AUTH

View File

@ -48,9 +48,9 @@ from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from dateutil import parser
import seafileapi
from settings import DEBUG, DB_COMPANIES, PATHS
from .cfdi_cert import SATCertificate
from .pacs import PACComercioDigital
from .pac import Finkok as PACFinkok
# ~ from .finkok import PACFinkok
@ -492,29 +492,6 @@ def _backup_db(rfc, is_mv, url_seafile):
shutil.copy(path, path_target)
else:
log.error('\tNo existe la carpeta compartida...')
# ~ sql = 'select correo_timbrado, token_soporte from emisor;'
# ~ args = 'psql -U postgres -d {} -Atc "{}"'.format(data['name'], sql)
# ~ result = _call(args)
# ~ if not result:
# ~ log.error('\tSin datos para backup remoto')
# ~ return
# ~ data = result.strip().split('|')
# ~ if not data[1]:
# ~ log.error('\tSin token de soporte')
# ~ return
# ~ email = data[0]
# ~ uuid = data[1]
# ~ email = 'hola@elmau.net'
# ~ uuid = 'cc42c591-cf66-499a-ae70-c09df5646be9'
# ~ log.debug(url_seafile, email, _get_pass(rfc))
# ~ client = seafileapi.connect(url_seafile, email, _get_pass(rfc))
# ~ repo = client.repos.get_repo(uuid)
# ~ print(repo)
return
@ -629,14 +606,30 @@ def xml_cancel(xml, auth, cert, name):
return data, result
def get_client_balance(auth, name):
def get_client_balance(auth):
if DEBUG:
return '-d'
pac = PACS[name]()
auth = {'usr': auth['USER'], 'pwd': auth['PASS']}
pac = PACS[auth['pac']]()
balance = pac.client_balance(auth)
if pac.error:
balance = '-e'
return balance
def get_cert(args):
p1 = '/home/mau/Desktop/Pruebas_EKU9003173C9/file.cer'
cer = args['cer']
# ~ cer = cer.encode()
# ~ cer = base64.b64decode(args['cer'].encode())
with open(p1, 'w') as f:
f.write(cer)
# ~ cer = base64.b64decode(args['cer'].encode())
print('TYPE', type(cer))
# ~ print(cer)
# ~ key = base64.b64decode(args['key'].encode())
# ~ cert = SATCertificate(cer, key, args['contra'])
return

View File

@ -17,7 +17,7 @@ from controllers.main import (AppEmpresas,
AppDocumentos, AppFiles, AppPreInvoices, AppCuentasBanco,
AppMovimientosBanco, AppTickets, AppStudents, AppEmployees, AppNomina,
AppInvoicePay, AppCfdiPay, AppSATBancos, AppSociosCuentasBanco,
AppSATFormaPago, AppSATLeyendaFiscales
AppSATFormaPago, AppSATLeyendaFiscales, AppCert
)
@ -62,6 +62,7 @@ api.add_route('/satbancos', AppSATBancos(db))
api.add_route('/satformapago', AppSATFormaPago(db))
api.add_route('/socioscb', AppSociosCuentasBanco(db))
api.add_route('/leyendasfiscales', AppSATLeyendaFiscales(db))
api.add_route('/cert', AppCert(db))
session_options = {

View File

@ -471,6 +471,13 @@ class StorageEngine(object):
def sat_leyendas_fiscales_delete(self, values):
return main.SATLeyendasFiscales.remove(values)
# ~ v2
def cert_get(self, values):
return main.Certificado.get_data(values)
def cert_post(self, values):
return main.Certificado.post(values)
# Companies only in MV
def _get_empresas(self, values):
return main.companies_get()

View File

@ -1165,19 +1165,35 @@ class Certificado(BaseModel):
return self.serie
@classmethod
def get_cert(cls, is_fiel=False):
return Certificado.get(Certificado.es_fiel==is_fiel)
@classmethod
def get_data(cls):
obj = cls.get_(cls)
row = {
def _get_cert(cls, args):
obj = Certificado.get(Certificado.es_fiel==False)
data = {
'cert_rfc': obj.rfc,
'cert_serie': obj.serie,
'cert_desde': obj.desde,
'cert_hasta': obj.hasta,
}
return row
return data
@classmethod
def get_data(cls, values):
opt = values['opt']
return getattr(cls, f'_get_{opt}')(values)
@classmethod
def _validate(cls, args):
cert = utils.get_cert(args)
print(cert)
return {'ok': False, 'msg': 'error', 'data': {}}
@classmethod
def post(cls, values):
opt = values['opt']
return getattr(cls, f'_{opt}')(values)
# ~ @classmethod
# ~ def get_cert(cls, is_fiel=False):
# ~ return Certificado.get(Certificado.es_fiel==is_fiel)
def get_(cls):
return Certificado.select()[0]

View File

@ -1,5 +0,0 @@
from seafileapi.client import SeafileApiClient
def connect(server, username, password):
client = SeafileApiClient(server, username, password)
return client

View File

@ -1,7 +0,0 @@
class SeafileAdmin(object):
def lists_users(self, maxcount=100):
pass
def list_user_repos(self, username):
pass

View File

@ -1,77 +0,0 @@
import requests
from seafileapi.utils import urljoin
from seafileapi.exceptions import ClientHttpError
from seafileapi.repos import Repos
class SeafileApiClient(object):
"""Wraps seafile web api"""
def __init__(self, server, username=None, password=None, token=None):
"""Wraps various basic operations to interact with seahub http api.
"""
self.server = server
self.username = username
self.password = password
self._token = token
self.repos = Repos(self)
self.groups = Groups(self)
if token is None:
self._get_token()
def _get_token(self):
data = {
'username': self.username,
'password': self.password,
}
url = urljoin(self.server, '/api2/auth-token/')
res = requests.post(url, data=data)
if res.status_code != 200:
raise ClientHttpError(res.status_code, res.content)
token = res.json()['token']
assert len(token) == 40, 'The length of seahub api auth token should be 40'
self._token = token
def __str__(self):
return 'SeafileApiClient[server=%s, user=%s]' % (self.server, self.username)
__repr__ = __str__
def get(self, *args, **kwargs):
return self._send_request('GET', *args, **kwargs)
def post(self, *args, **kwargs):
return self._send_request('POST', *args, **kwargs)
def put(self, *args, **kwargs):
return self._send_request('PUT', *args, **kwargs)
def delete(self, *args, **kwargs):
return self._send_request('delete', *args, **kwargs)
def _send_request(self, method, url, *args, **kwargs):
if not url.startswith('http'):
url = urljoin(self.server, url)
headers = kwargs.get('headers', {})
headers.setdefault('Authorization', 'Token ' + self._token)
kwargs['headers'] = headers
expected = kwargs.pop('expected', 200)
if not hasattr(expected, '__iter__'):
expected = (expected, )
resp = requests.request(method, url, *args, **kwargs)
if resp.status_code not in expected:
msg = 'Expected %s, but get %s' % \
(' or '.join(map(str, expected)), resp.status_code)
raise ClientHttpError(resp.status_code, msg)
return resp
class Groups(object):
def __init__(self, client):
pass
def create_group(self, name):
pass

View File

@ -1,25 +0,0 @@
class ClientHttpError(Exception):
"""This exception is raised if the returned http response is not as
expected"""
def __init__(self, code, message):
super(ClientHttpError, self).__init__()
self.code = code
self.message = message
def __str__(self):
return 'ClientHttpError[%s: %s]' % (self.code, self.message)
class OperationError(Exception):
"""Expcetion to raise when an opeartion is failed"""
pass
class DoesNotExist(Exception):
"""Raised when not matching resource can be found."""
def __init__(self, msg):
super(DoesNotExist, self).__init__()
self.msg = msg
def __str__(self):
return 'DoesNotExist: %s' % self.msg

View File

@ -1,250 +0,0 @@
import io
import os
import posixpath
import re
from seafileapi.utils import querystr
ZERO_OBJ_ID = '0000000000000000000000000000000000000000'
class _SeafDirentBase(object):
"""Base class for :class:`SeafFile` and :class:`SeafDir`.
It provides implementation of their common operations.
"""
isdir = None
def __init__(self, repo, path, object_id, size=0):
"""
:param:`path` the full path of this entry within its repo, like
"/documents/example.md"
:param:`size` The size of a file. It should be zero for a dir.
"""
self.client = repo.client
self.repo = repo
self.path = path
self.id = object_id
self.size = size
@property
def name(self):
return posixpath.basename(self.path)
def list_revisions(self):
pass
def delete(self):
suffix = 'dir' if self.isdir else 'file'
url = '/api2/repos/%s/%s/' % (self.repo.id, suffix) + querystr(p=self.path)
resp = self.client.delete(url)
return resp
def rename(self, newname):
"""Change file/folder name to newname
"""
suffix = 'dir' if self.isdir else 'file'
url = '/api2/repos/%s/%s/' % (self.repo.id, suffix) + querystr(p=self.path, reloaddir='true')
postdata = {'operation': 'rename', 'newname': newname}
resp = self.client.post(url, data=postdata)
succeeded = resp.status_code == 200
if succeeded:
if self.isdir:
new_dirent = self.repo.get_dir(os.path.join(os.path.dirname(self.path), newname))
else:
new_dirent = self.repo.get_file(os.path.join(os.path.dirname(self.path), newname))
for key in list(self.__dict__.keys()):
self.__dict__[key] = new_dirent.__dict__[key]
return succeeded
def _copy_move_task(self, operation, dirent_type, dst_dir, dst_repo_id=None):
url = '/api/v2.1/copy-move-task/'
src_repo_id = self.repo.id
src_parent_dir = os.path.dirname(self.path)
src_dirent_name = os.path.basename(self.path)
dst_repo_id = dst_repo_id
dst_parent_dir = dst_dir
operation = operation
dirent_type = dirent_type
postdata = {'src_repo_id': src_repo_id, 'src_parent_dir': src_parent_dir,
'src_dirent_name': src_dirent_name, 'dst_repo_id': dst_repo_id,
'dst_parent_dir': dst_parent_dir, 'operation': operation,
'dirent_type': dirent_type}
return self.client.post(url, data=postdata)
def copyTo(self, dst_dir, dst_repo_id=None):
"""Copy file/folder to other directory (also to a different repo)
"""
if dst_repo_id is None:
dst_repo_id = self.repo.id
dirent_type = 'dir' if self.isdir else 'file'
resp = self._copy_move_task('copy', dirent_type, dst_dir, dst_repo_id)
return resp.status_code == 200
def moveTo(self, dst_dir, dst_repo_id=None):
"""Move file/folder to other directory (also to a different repo)
"""
if dst_repo_id is None:
dst_repo_id = self.repo.id
dirent_type = 'dir' if self.isdir else 'file'
resp = self._copy_move_task('move', dirent_type, dst_dir, dst_repo_id)
succeeded = resp.status_code == 200
if succeeded:
new_repo = self.client.repos.get_repo(dst_repo_id)
dst_path = os.path.join(dst_dir, os.path.basename(self.path))
if self.isdir:
new_dirent = new_repo.get_dir(dst_path)
else:
new_dirent = new_repo.get_file(dst_path)
for key in list(self.__dict__.keys()):
self.__dict__[key] = new_dirent.__dict__[key]
return succeeded
def get_share_link(self):
pass
class SeafDir(_SeafDirentBase):
isdir = True
def __init__(self, *args, **kwargs):
super(SeafDir, self).__init__(*args, **kwargs)
self.entries = None
self.entries = kwargs.pop('entries', None)
def ls(self, force_refresh=False):
"""List the entries in this dir.
Return a list of objects of class :class:`SeafFile` or :class:`SeafDir`.
"""
if self.entries is None or force_refresh:
self.load_entries()
return self.entries
def share_to_user(self, email, permission):
url = '/api2/repos/%s/dir/shared_items/' % self.repo.id + querystr(p=self.path)
putdata = {
'share_type': 'user',
'username': email,
'permission': permission
}
resp = self.client.put(url, data=putdata)
return resp.status_code == 200
def create_empty_file(self, name):
"""Create a new empty file in this dir.
Return a :class:`SeafFile` object of the newly created file.
"""
# TODO: file name validation
path = posixpath.join(self.path, name)
url = '/api2/repos/%s/file/' % self.repo.id + querystr(p=path, reloaddir='true')
postdata = {'operation': 'create'}
resp = self.client.post(url, data=postdata)
self.id = resp.headers['oid']
self.load_entries(resp.json())
return SeafFile(self.repo, path, ZERO_OBJ_ID, 0)
def mkdir(self, name):
"""Create a new sub folder right under this dir.
Return a :class:`SeafDir` object of the newly created sub folder.
"""
path = posixpath.join(self.path, name)
url = '/api2/repos/%s/dir/' % self.repo.id + querystr(p=path, reloaddir='true')
postdata = {'operation': 'mkdir'}
resp = self.client.post(url, data=postdata)
self.id = resp.headers['oid']
self.load_entries(resp.json())
return SeafDir(self.repo, path, ZERO_OBJ_ID)
def upload(self, fileobj, filename):
"""Upload a file to this folder.
:param:fileobj :class:`File` like object
:param:filename The name of the file
Return a :class:`SeafFile` object of the newly uploaded file.
"""
if isinstance(fileobj, str):
fileobj = io.BytesIO(fileobj)
upload_url = self._get_upload_link()
files = {
'file': (filename, fileobj),
'parent_dir': self.path,
}
self.client.post(upload_url, files=files)
return self.repo.get_file(posixpath.join(self.path, filename))
def upload_local_file(self, filepath, name=None):
"""Upload a file to this folder.
:param:filepath The path to the local file
:param:name The name of this new file. If None, the name of the local file would be used.
Return a :class:`SeafFile` object of the newly uploaded file.
"""
name = name or os.path.basename(filepath)
with open(filepath, 'r') as fp:
return self.upload(fp, name)
def _get_upload_link(self):
url = '/api2/repos/%s/upload-link/' % self.repo.id
resp = self.client.get(url)
return re.match(r'"(.*)"', resp.text).group(1)
def get_uploadable_sharelink(self):
"""Generate a uploadable shared link to this dir.
Return the url of this link.
"""
pass
def load_entries(self, dirents_json=None):
if dirents_json is None:
url = '/api2/repos/%s/dir/' % self.repo.id + querystr(p=self.path)
dirents_json = self.client.get(url).json()
self.entries = [self._load_dirent(entry_json) for entry_json in dirents_json]
def _load_dirent(self, dirent_json):
path = posixpath.join(self.path, dirent_json['name'])
if dirent_json['type'] == 'file':
return SeafFile(self.repo, path, dirent_json['id'], dirent_json['size'])
else:
return SeafDir(self.repo, path, dirent_json['id'], 0)
@property
def num_entries(self):
if self.entries is None:
self.load_entries()
return len(self.entries) if self.entries is not None else 0
def __str__(self):
return 'SeafDir[repo=%s,path=%s,entries=%s]' % \
(self.repo.id[:6], self.path, self.num_entries)
__repr__ = __str__
class SeafFile(_SeafDirentBase):
isdir = False
def update(self, fileobj):
"""Update the content of this file"""
pass
def __str__(self):
return 'SeafFile[repo=%s,path=%s,size=%s]' % \
(self.repo.id[:6], self.path, self.size)
def _get_download_link(self):
url = '/api2/repos/%s/file/' % self.repo.id + querystr(p=self.path)
resp = self.client.get(url)
return re.match(r'"(.*)"', resp.text).group(1)
def get_content(self):
"""Get the content of the file"""
url = self._get_download_link()
return self.client.get(url).content
__repr__ = __str__

View File

@ -1,22 +0,0 @@
class Group(object):
def __init__(self, client, group_id, group_name):
self.client = client
self.group_id = group_id
self.group_name = group_name
def list_memebers(self):
pass
def delete(self):
pass
def add_member(self, username):
pass
def remove_member(self, username):
pass
def list_group_repos(self):
pass

View File

@ -1,99 +0,0 @@
from urllib.parse import urlencode
from seafileapi.files import SeafDir, SeafFile
from seafileapi.utils import raise_does_not_exist
class Repo(object):
"""
A seafile library
"""
def __init__(self, client, repo_id, repo_name,
encrypted, owner, perm):
self.client = client
self.id = repo_id
self.name = repo_name
self.encrypted = encrypted
self.owner = owner
self.perm = perm
@classmethod
def from_json(cls, client, repo_json):
repo_id = repo_json['id']
repo_name = repo_json['name']
encrypted = repo_json['encrypted']
perm = repo_json['permission']
owner = repo_json['owner']
return cls(client, repo_id, repo_name, encrypted, owner, perm)
def is_readonly(self):
return 'w' not in self.perm
@raise_does_not_exist('The requested file does not exist')
def get_file(self, path):
"""Get the file object located in `path` in this repo.
Return a :class:`SeafFile` object
"""
assert path.startswith('/')
url = '/api2/repos/%s/file/detail/' % self.id
query = '?' + urlencode(dict(p=path))
file_json = self.client.get(url + query).json()
return SeafFile(self, path, file_json['id'], file_json['size'])
@raise_does_not_exist('The requested dir does not exist')
def get_dir(self, path):
"""Get the dir object located in `path` in this repo.
Return a :class:`SeafDir` object
"""
assert path.startswith('/')
url = '/api2/repos/%s/dir/' % self.id
query = '?' + urlencode(dict(p=path))
resp = self.client.get(url + query)
dir_id = resp.headers['oid']
dir_json = resp.json()
dir = SeafDir(self, path, dir_id)
dir.load_entries(dir_json)
return dir
def delete(self):
"""Remove this repo. Only the repo owner can do this"""
self.client.delete('/api2/repos/' + self.id)
def list_history(self):
"""List the history of this repo
Returns a list of :class:`RepoRevision` object.
"""
pass
## Operations only the repo owner can do:
def update(self, name=None):
"""Update the name of this repo. Only the repo owner can do
this.
"""
pass
def get_settings(self):
"""Get the settings of this repo. Returns a dict containing the following
keys:
`history_limit`: How many days of repo history to keep.
"""
pass
def restore(self, commit_id):
pass
class RepoRevision(object):
def __init__(self, client, repo, commit_id):
self.client = client
self.repo = repo
self.commit_id = commit_id
def restore(self):
"""Restore the repo to this revision"""
self.repo.revert(self.commit_id)

View File

@ -1,26 +0,0 @@
from seafileapi.repo import Repo
from seafileapi.utils import raise_does_not_exist
class Repos(object):
def __init__(self, client):
self.client = client
def create_repo(self, name, password=None):
data = {'name': name}
if password:
data['passwd'] = password
repo_json = self.client.post('/api2/repos/', data=data).json()
return self.get_repo(repo_json['repo_id'])
@raise_does_not_exist('The requested library does not exist')
def get_repo(self, repo_id):
"""Get the repo which has the id `repo_id`.
Raises :exc:`DoesNotExist` if no such repo exists.
"""
repo_json = self.client.get('/api2/repos/' + repo_id).json()
return Repo.from_json(self.client, repo_json)
def list_repos(self):
repos_json = self.client.get('/api2/repos/').json()
return [Repo.from_json(self.client, j) for j in repos_json]

View File

@ -1,57 +0,0 @@
import string
import random
from functools import wraps
from urllib.parse import urlencode
from seafileapi.exceptions import ClientHttpError, DoesNotExist
def randstring(length=0):
if length == 0:
length = random.randint(1, 30)
return ''.join(random.choice(string.lowercase) for i in range(length))
def urljoin(base, *args):
url = base
if url[-1] != '/':
url += '/'
for arg in args:
arg = arg.strip('/')
url += arg + '/'
if '?' in url:
url = url[:-1]
return url
def raise_does_not_exist(msg):
"""Decorator to turn a function that get a http 404 response to a
:exc:`DoesNotExist` exception."""
def decorator(func):
@wraps(func)
def wrapped(*args, **kwargs):
try:
return func(*args, **kwargs)
except ClientHttpError as e:
if e.code == 404:
raise DoesNotExist(msg)
else:
raise
return wrapped
return decorator
def to_utf8(obj):
if isinstance(obj, str):
return obj.encode('utf-8')
return obj
def querystr(**kwargs):
return '?' + urlencode(kwargs)
def utf8lize(obj):
if isinstance(obj, dict):
return {k: to_utf8(v) for k, v in obj.items()}
if isinstance(obj, list):
return [to_utf8(x) for x in ob]
if instance(obj, str):
return obj.encode('utf-8')
return obj

View File

@ -30,11 +30,6 @@ try:
except ImportError:
DEFAULT_PASSWORD = 'salgueiro3.3'
try:
from conf import SEAFILE_SERVER
except ImportError:
SEAFILE_SERVER = {}
try:
from conf import TITLE_APP
except ImportError:

View File

@ -19,6 +19,9 @@ var msg = ''
var tb_options = null
var tb_sat = null
var file_cer = null
var file_key = null
var controllers = {
init: function(){
@ -32,7 +35,9 @@ var controllers = {
$$('chk_escuela').attachEvent('onChange', chk_escuela_change)
$$('chk_ong').attachEvent('onChange', chk_ong_change)
$$('cmd_subir_certificado').attachEvent('onItemClick', cmd_subir_certificado_click)
$$('up_cert').attachEvent('onUploadComplete', up_cert_upload_complete)
//~ $$('up_cert').attachEvent('onUploadComplete', up_cert_upload_complete)
//~ $$('up_cert').attachEvent('onAfterFileAdd', up_cert_after_file_add)
$$('up_cert').attachEvent('onBeforeFileAdd', up_cert_before_file_add)
$$('cmd_agregar_serie').attachEvent('onItemClick', cmd_agregar_serie_click)
$$('grid_folios').attachEvent('onItemClick', grid_folios_click)
$$('chk_folio_custom').attachEvent('onItemClick', chk_config_item_click)
@ -280,7 +285,7 @@ function get_emisor(){
function get_certificado(){
var form = $$('form_cert')
webix.ajax().get("/values/cert", {}, {
webix.ajax().get("/cert", {'opt': 'cert'}, {
error: function(text, data, xhr) {
msg = 'Error al consultar'
msg_error(msg)
@ -602,105 +607,6 @@ function chk_ong_change(new_value, old_value){
}
function cmd_subir_certificado_click(){
var form = $$('form_upload')
if (!form.validate()){
msg = 'Valores inválidos'
msg_error(msg)
return
}
var values = form.getValues()
if(!values.contra.trim()){
msg = 'La contraseña no puede estar vacía'
msg_error(msg)
return
}
if($$('lst_cert').count() < 2){
msg = 'Selecciona al menos dos archivos: CER y KEY del certificado.'
msg_error(msg)
return
}
if($$('lst_cert').count() > 2){
msg = 'Selecciona solo dos archivos: CER y KEY del certificado.'
msg_error(msg)
return
}
var fo1 = $$('up_cert').files.getItem($$('up_cert').files.getFirstId())
var fo2 = $$('up_cert').files.getItem($$('up_cert').files.getLastId())
var ext = ['key', 'cer']
if(ext.indexOf(fo1.type.toLowerCase()) == -1 || ext.indexOf(fo2.type.toLowerCase()) == -1){
msg = 'Archivos inválidos, se requiere un archivo CER y un KEY.'
msg_error(msg)
return
}
if(fo1.type == fo2.type && fo1.size == fo2.size){
msg = 'Selecciona archivos diferentes: un archivo CER y un KEY.'
msg_error(msg)
return
}
var serie = $$('form_cert').getValues()['cert_serie']
if(serie){
msg = 'Ya existe un certificado guardado<BR><BR>¿Deseas reemplazarlo?'
webix.confirm({
title: 'Certificado Existente',
ok: 'Si',
cancel: 'No',
type: 'confirm-error',
text: msg,
callback:function(result){
if(result){
$$('up_cert').send()
}
}
})
}else{
$$('up_cert').send()
}
}
function up_cert_upload_complete(response){
if(response.status != 'server'){
msg = 'Ocurrio un error al subir los archivos'
msg_error(msg)
return
}
msg = 'Archivos subidos correctamente. Esperando validación'
msg_ok(msg)
var values = $$('form_upload').getValues()
$$('form_upload').setValues({})
$$('up_cert').files.data.clearAll()
webix.ajax().post('/values/cert', values, {
error:function(text, data, XmlHttpRequest){
msg = 'Ocurrio un error, consulta a soporte técnico'
msg_error(msg)
},
success:function(text, data, XmlHttpRequest){
var values = data.json()
if(values.ok){
$$('form_cert').setValues(values.data)
msg_ok(values.msg)
}else{
msg_error(values.msg)
}
}
})
}
function cmd_agregar_serie_click(){
var form = $$('form_folios')
var grid = $$('grid_folios')
@ -2726,3 +2632,175 @@ function cmd_save_pac_click(){
}
})
}
function cmd_subir_certificado_click(){
var form = $$('form_upload')
if (!form.validate()){
msg = 'Valores inválidos'
msg_error(msg)
return
}
var values = form.getValues()
if(!values.contra.trim()){
msg = 'La contraseña no puede estar vacía'
msg_error(msg)
return
}
//~ if($$('lst_cert').count() < 2){
//~ msg = 'Selecciona al menos dos archivos: CER y KEY del certificado.'
//~ msg_error(msg)
//~ return
//~ }
//~ if($$('lst_cert').count() > 2){
//~ msg = 'Selecciona solo dos archivos: CER y KEY del certificado.'
//~ msg_error(msg)
//~ return
//~ }
//~ var fo1 = $$('up_cert').files.getItem($$('up_cert').files.getFirstId())
//~ var fo2 = $$('up_cert').files.getItem($$('up_cert').files.getLastId())
//~ var ext = ['key', 'cer']
//~ if(ext.indexOf(fo1.type.toLowerCase()) == -1 || ext.indexOf(fo2.type.toLowerCase()) == -1){
//~ msg = 'Archivos inválidos, se requiere un archivo CER y un KEY.'
//~ msg_error(msg)
//~ return
//~ }
//~ if(fo1.type == fo2.type && fo1.size == fo2.size){
//~ msg = 'Selecciona archivos diferentes: un archivo CER y un KEY.'
//~ msg_error(msg)
//~ return
//~ }
var serie = $$('form_cert').getValues()['cert_serie']
if(serie){
msg = 'Ya existe un certificado guardado<BR><BR>¿Deseas reemplazarlo?'
webix.confirm({
title: 'Certificado Existente',
ok: 'Si',
cancel: 'No',
type: 'confirm-error',
text: msg,
callback:function(result){
if(!result){
return
}
}
})
}
//~ if (fo1.type.toLowerCase()=='cer'){
//~ values['cer'] = fo1.file
//~ values['key'] = fo2.file
//~ } else {
//~ values['key'] = fo1.file
//~ values['cer'] = fo2.file
//~ }
$$('form_upload').setValues({})
$$('up_cert').files.data.clearAll()
values['cer'] = file_cer
values['key'] = file_key
validate_cert(values)
}
function up_cert_before_file_add(file){
if (file.type.toLowerCase() != 'cer' && file.type.toLowerCase() != 'key'){
msg_error('Selecciona un archivo CER o KEY')
return false
}
var count = $$('lst_cert').count()
if (count > 1){
msg = 'Selecciona solo dos archivos: CER y KEY del certificado.'
msg_error(msg)
return false
}
if (count > 0){
var f = $$('up_cert').files.getItem($$('up_cert').files.getFirstId())
if (f.type.toLowerCase() == file.type.toLowerCase()){
msg = 'Selecciona archivos diferentes: un archivo CER y un KEY.'
msg_error(msg)
return false
}
}
var reader = new FileReader();
if (file.type.toLowerCase() == 'cer'){
reader.addEventListener('load', (event) => {
file_cer = event.target.result;
});
reader.readAsBinaryString(file.file);
} else {
reader.addEventListener('load', (event) => {
file_key = event.target.result;
});
reader.readAsBinaryString(file.file);
}
}
function validate_cert(values){
msg = 'Archivos recibidos correctamente. Esperando validación'
msg_ok(msg)
values['opt'] = 'validate'
webix.ajax().post('/cert', values, {
error:function(text, data, XmlHttpRequest){
msg = 'Ocurrio un error, consulta a soporte técnico'
msg_error(msg)
},
success:function(text, data, XmlHttpRequest){
var values = data.json()
if(values.ok){
$$('form_cert').setValues(values.data)
msg_ok(values.msg)
}else{
msg_error(values.msg)
}
}
})
}
//~ function up_cert_upload_complete(response){
//~ if(response.status != 'server'){
//~ msg = 'Ocurrio un error al subir los archivos'
//~ msg_error(msg)
//~ return
//~ }
//~ msg = 'Archivos subidos correctamente. Esperando validación'
//~ msg_ok(msg)
//~ var values = $$('form_upload').getValues()
//~ $$('form_upload').setValues({})
//~ $$('up_cert').files.data.clearAll()
//~ values['opt'] = 'validate'
//~ webix.ajax().post('/cert', values, {
//~ error:function(text, data, XmlHttpRequest){
//~ msg = 'Ocurrio un error, consulta a soporte técnico'
//~ msg_error(msg)
//~ },
//~ success:function(text, data, XmlHttpRequest){
//~ var values = data.json()
//~ if(values.ok){
//~ $$('form_cert').setValues(values.data)
//~ msg_ok(values.msg)
//~ }else{
//~ msg_error(values.msg)
//~ }
//~ }
//~ })
//~ }

View File

@ -278,18 +278,21 @@ var col_fiel = {rows: [
]}
//~ {view: 'uploader', id: 'up_cert', autosend: false, link: 'lst_cert',
//~ value: 'Seleccionar certificado', upload: '/values/files'}, {}]},
var emisor_certificado = [
{cols: [col_sello, col_fiel]},
{template: 'Cargar Certificado', type: 'section'},
{view: 'form', id: 'form_upload', rows: [
{cols: [{},
{view: 'uploader', id: 'up_cert', autosend: false, link: 'lst_cert',
value: 'Seleccionar certificado', upload: '/values/files'}, {}]},
value: 'Seleccionar certificado'}, {}]},
{cols: [{},
{view: 'list', id: 'lst_cert', name: 'certificado',
type: 'uploader', autoheight:true, borderless: true}, {}]},
{cols: [{},
{view: 'text', id: 'txt_contra', name: 'contra',
{view: 'text', id: 'txt_contra', name: 'contra', value: '12345678a',
label: 'Contraseña KEY', labelPosition: 'top',
labelAlign: 'center', type: 'password', required: true}, {}]},
{cols: [{}, {view: 'button', id: 'cmd_subir_certificado',