Commit f169cf1e by Owo Sugiana

Hapus ketergantungan pada database

1 parent d46036b9
Showing 91 changed files with 377 additions and 3943 deletions
......@@ -6,3 +6,4 @@ test-*
test.ini
dist
build
env
0.7 2026-07-18
--------------
- Dalam sebuah IP:PORT bisa lebih dari 1 koneksi. Sebelumnya akan memutus
koneksi lama bila ada yang baru. Ini berkat modul opensipkd-iso8583 versi
0.3.
- Hapus ketergantungan pada database.
0.6 2021-11-4
-------------
- Tambah log_warning
......
include CHANGES.txt production.ini development.ini README.rst
recursive-include iso8583_web *.py *.png *.css *.pt *.tpl *.mako *.pot *.po *.mo *.csv
recursive-include iso8583_web *.py
recursive-include iso8583_web_test *.py
delete from users;
select setval('users_id_seq', 1, false);
drop table external_identities;
drop table users_resources_permissions;
drop table groups_resources_permissions;
drop table groups_permissions;
drop table resources;
drop table users_groups;
drop table users_permissions;
drop table groups;
drop table users;
drop table alembic_version;
# pip install requests
import sys
import os
import json
import traceback
from datetime import datetime
from random import randrange
from time import (
sleep,
time,
)
from threading import Thread
from argparse import ArgumentParser
import requests
threads = dict()
end_threads = list()
durations = dict()
json_responses = dict()
server_info = dict()
default_url = 'http://localhost:6543/rpc'
default_count = 1
default_delay = 0.1
help_url = 'default ' + default_url
help_count = 'default {}'.format(default_count)
help_delay = 'default {} detik saat --count'.format(default_delay)
help_invoice_id = 'wajib saat --payment dan --reversal'
help_amount = 'wajib saat --payment dan --reversal'
help_ntb = 'opsional saat --payment, wajib saat --reversal'
help_stan = 'opsional saat --payment, wajib saat --reversal'
help_bit = 'bit tambahan, contoh: --bit=42:TOKOPEDIA'
help_conf = 'konfigurasi tambahan, contoh untuk multi: --conf=pajak:bphtb'
def error(s):
print('ERROR: {}'.format(s))
sys.exit()
def log_info(s):
t = datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')
t = t[:-3]
msg = '{} {}'.format(t, s)
print(msg)
def get_option(argv):
parser = ArgumentParser()
parser.add_argument('--url', default=default_url, help=help_url)
parser.add_argument(
'--count', type=int, default=default_count, help=help_count)
parser.add_argument(
'--delay', type=float, default=default_delay, help=help_delay)
parser.add_argument('--invoice-id', help=help_invoice_id)
parser.add_argument('--payment', action='store_true')
parser.add_argument('--reversal', action='store_true')
parser.add_argument('--amount', type=int, help=help_amount)
parser.add_argument('--ntb', help=help_ntb)
parser.add_argument('--stan', type=int, help=help_stan)
parser.add_argument('--bit', help=help_bit)
parser.add_argument('--conf', help=help_conf)
return parser.parse_args(argv)
def send(p):
url = server_info['url']
key = p['id']
log_info(f'Request {url} {p}')
start = time()
try:
resp = requests.post(url, json=p)
durations[key] = time() - start
if resp.status_code == 200:
json_resp = resp.json()
log_info('Response: {}'.format(json_resp))
json_responses[key] = json_resp
else:
log_info('Status Code: {}'.format(resp.status_code))
log_info('Body: {}'.format([resp.text]))
json_responses[key] = dict(fatal=resp.text)
except requests.exceptions.ConnectionError as e:
durations[key] = time() - start
log_info('Response: {}'.format(e))
json_responses[key] = dict(fatal=e)
except json.decoder.JSONDecodeError as e:
durations[key] = time() - start
log_info('Body: {}'.format([resp.text]))
log_info('Response: {}'.format(e))
json_responses[key] = dict(fatal=e)
except Exception as e:
durations[key] = time() - start
json_responses[key] = dict(fatal=e)
traceback.print_exc()
finally:
end_threads.append(key)
def stan_from_result(result):
if result['code'] == 0:
return result['data']['stan']
return '-'
def show_errors(errors):
if errors:
for err, count in errors.items():
log_info('{} {}'.format(err, count))
else:
log_info('Tidak ada yang gagal')
def show_durations():
key_fastest = None
key_slowest = None
total_duration = 0
messages = dict()
errors = dict()
for key in durations:
duration = durations[key]
msg = 'thread {} {} detik'.format(key, duration)
resp = json_responses[key]
if 'fatal' in resp:
errors['fatal'] = resp['fatal']
elif 'error' in resp:
result = resp['error']
msg = '{} {}'.format(msg, result['message'])
err = result['message']
if err in errors:
errors[err] += 1
else:
errors[err] = 1
else:
result = resp['result']
stan = stan_from_result(result)
msg = '{} stan {}'.format(msg, stan)
messages[key] = msg
if key_fastest:
if duration < durations[key_fastest]:
key_fastest = key
else:
key_fastest = key
if key_slowest:
if duration > durations[key_slowest]:
key_slowest = key
else:
key_slowest = key
total_duration += duration
log_info(msg)
if key_fastest != key_slowest:
log_info('Tercepat {}'.format(messages[key_fastest]))
log_info('Terlama {}'.format(messages[key_slowest]))
log_info('Rerata {} detik / request'.format(
total_duration/len(durations)))
show_errors(errors)
def create_ntb():
i = randrange(99999999)
return str(i).zfill(8)
class App:
def __init__(self, argv):
self.option = get_option(argv)
server_info['url'] = self.option.url
if self.option.stan:
self.stan = self.option.stan
else:
self.stan = int(datetime.now().strftime('%H%M%S'))
def create_thread(self, data):
thread = Thread(target=send, args=[data])
# Exit the server thread when the main thread terminates
thread.daemon = True
thread_id = data['id']
threads[thread_id] = thread
thread.start()
def get_invoice_ids(self):
if not os.path.exists(self.option.invoice_id):
return [self.option.invoice_id]
r = []
with open(self.option.invoice_id) as f:
for line in f.readlines():
invoice_id = line.rstrip()
r += [invoice_id]
return r
def get_method(self):
if self.option.payment:
return 'payment'
if self.option.reversal:
return 'reversal'
return 'inquiry'
def get_transaction(self, invoice_id):
def required(name, default=None):
value = getattr(self.option, name)
if not value and not default:
error('--{} harus diisi'.format(name))
p[name] = value or default
p = dict(invoice_id=invoice_id)
if self.option.payment or self.option.reversal:
required('amount')
if self.option.payment:
ntb = create_ntb()
required('ntb', ntb)
else:
required('ntb')
required('stan')
if 'stan' not in p:
stan = str(self.stan).zfill(6)
p['stan'] = stan
self.stan += 1
if self.option.bit:
p['bits'] = dict()
for t in self.option.bit.split(','):
bit, value = t.split(':')
p['bits'][bit] = value
if self.option.conf:
conf = dict()
for t in self.option.conf.split(','):
key, val = t.split(':')
conf[key] = val
p['conf'] = conf
return p
def run_transaction(self):
method = self.get_method()
thread_id = 0
for i in range(self.option.count):
for invoice_id in self.get_invoice_ids():
thread_id += 1
p = self.get_transaction(invoice_id)
data = dict(
id=thread_id, method=method, params=[p],
jsonrpc='2.0')
self.create_thread(data)
sleep(self.option.delay)
def run_echo(self):
for thread_id in range(1, self.option.count+1):
p = dict(id=thread_id)
data = dict(id=thread_id, method='echo', params=[p], jsonrpc='2.0')
self.create_thread(data)
def run(self):
p = dict()
if self.option.invoice_id:
self.run_transaction()
else:
self.run_echo()
while threads:
if not end_threads:
continue
i = end_threads[0]
if i in threads:
thread = threads[i]
thread.join()
del threads[i]
index = end_threads.index(i)
del end_threads[index]
show_durations()
def main(argv=sys.argv[1:]):
app = App(argv)
app.run()
if __name__ == '__main__':
main()
###
# app configuration
# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html
###
[app:main]
use = egg:iso8583_web
pyramid.reload_templates = true
pyramid.debug_authorization = false
pyramid.debug_notfound = false
pyramid.debug_routematch = false
pyramid.default_locale_name = id
pyramid.includes =
pyramid_debugtoolbar
pyramid_rpc.jsonrpc
sqlalchemy.url = postgresql://user:pass@localhost/dbname
sqlalchemy.pool_pre_ping = true
timezone = Asia/Jakarta
localization = id_ID.UTF-8
mail.host = localhost
mail.port = 25
mail.username = user@example.com
mail.password = FIXME
mail.sender_name = Example Name
retry.attempts = 3
# By default, the toolbar only appears for clients from IP addresses
# '127.0.0.1' and '::1'.
# debugtoolbar.hosts = 127.0.0.1 ::1
[alembic]
# path to migration scripts
script_location = alembic
file_template = %%(year)d%%(month).2d%%(day).2d_%%(rev)s
# file_template = %%(rev)s_%%(slug)s
###
# wsgi server configuration
###
[server:main]
use = egg:waitress#main
listen = localhost:6543
###
# logging configuration
# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
###
[loggers]
keys = root, iso8583_web
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = INFO
handlers = console
[logger_iso8583_web]
level = DEBUG
handlers =
qualname = iso8583_web
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s
###
# app configuration
# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html
###
[app:main]
use = egg:iso8583_web
pyramid.reload_templates = true
pyramid.debug_authorization = false
pyramid.debug_notfound = false
pyramid.debug_routematch = false
pyramid.default_locale_name = id
pyramid.includes =
pyramid_debugtoolbar
pyramid_rpc.jsonrpc
opensipkd.views.bjb.jsonrpc
sqlalchemy.url = postgresql://user:pass@localhost/db
sqlalchemy.pool_size = 50
sqlalchemy.max_overflow = 100
sqlalchemy.pool_pre_ping = true
timezone = Asia/Jakarta
localization = id_ID.UTF-8
mail.host = localhost
mail.port = 25
mail.username = user@example.com
mail.password = FIXME
mail.sender_name = Example Name
retry.attempts = 3
# By default, the toolbar only appears for clients from IP addresses
# '127.0.0.1' and '::1'.
# debugtoolbar.hosts = 127.0.0.1 ::1
[alembic]
# path to migration scripts
script_location = iso8583_web/alembic
file_template = %%(year)d%%(month).2d%%(day).2d_%%(rev)s
# file_template = %%(rev)s_%%(slug)s
###
# wsgi server configuration
###
[server:main]
use = egg:waitress#main
listen = localhost:6543
###
# logging configuration
# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
###
[loggers]
keys = root, iso8583_web, jsonrpc
[handlers]
keys = console, file
[formatters]
keys = generic
[logger_root]
level = DEBUG
handlers = console, file
[logger_iso8583_web]
level = DEBUG
handlers =
qualname = iso8583_web
[logger_jsonrpc]
level = DEBUG
handlers = console, file
qualname = pyramid_rpc.jsonrpc
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = DEBUG
formatter = generic
[handler_file]
class = FileHandler
args = ('/home/sugiana/log/bank.log', 'a')
level = DEBUG
formatter = generic
[formatter_generic]
format = %(asctime)s %(levelname)s %(message)s
[host_agratek]
ip = 127.0.0.1
port = 10001
listen = true
streamer = bjb
module = opensipkd.iso8583.bjb.pbb.test_aggregator
db_url = postgresql://user:pass@localhost/db
# Routing dari 4 digit awal InvoiceID
host =
3271:kota_bogor
3275:kota_bekasi
valid_bits =
2:622011888888888888
18:6025
22:010
32:015
35:622011888888888888=9912?
41:AGRATEK
42:LINKAJA
43:AGRATEK-LINKAJA
49:360
59:PAY
63:214
[host_kota_bogor]
ip = 127.0.0.1
port = 10002
listen = true
streamer = bjb_with_suffix
module = opensipkd.iso8583.bjb.pbb.test
[host_kota_bekasi]
ip = 10.8.20.134
port = 10002
listen = true
streamer = bjb_with_suffix
module = opensipkd.iso8583.bjb.pbb.test
[module_opensipkd.iso8583.bjb.pbb.test]
db_url = postgresql://user:pass@localhost/db
request_bits =
18:6025
32:110
33:00110
###
# logging configuration
# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
###
[loggers]
keys = root, iso8583_web
[handlers]
keys = console, file
keys = console
[formatters]
keys = generic
[logger_root]
level = DEBUG
handlers = console, file
level = INFO
handlers = console
[logger_iso8583_web]
level = DEBUG
......@@ -27,58 +22,14 @@ args = (sys.stderr,)
level = NOTSET
formatter = generic
[handler_file]
class = FileHandler
args = ('/home/sugiana/log/pemda.log', 'a')
level = NOTSET
formatter = generic
[formatter_generic]
format = %(asctime)s %(levelname)s %(message)s
[module_opensipkd.iso8583.bjb.pbb.bogor_kota]
db_url = postgresql://user:pass@localhost/db
db_pool_size = 50
db_max_overflow = 100
db_pool_pre_ping = true
persen_denda = 2
nip_pencatat = 999999999
#####################
# Tempat pembayaran #
#####################
kd_kanwil = 01
kd_kantor = 01
# Penerjemahan nilai bit NN menjadi kd_tp
# Prioritas sesuai urutan
kd_tp =
bit42:TOKOPEDIA:46
bit42:BUKALAPAK:47
bit43:INDOMARET:48
bit43:ALFAMART:49
bit18:6010:69
bit18:6011:71
bit18:6012:72
bit18:6013:73
bit18:6014:74
bit18:6015:75
bit18:6016:76
bit18:6017:77
bit0:default:20
[host_bjb]
ip = 127.0.0.1
port = 10002
listen = false
port = 8592
listen = true
streamer = bjb_with_suffix
timeout = 60
module = opensipkd.iso8583.bjb.pbb.bogor_kota
[host_mitracomm]
active = false
ip = 127.0.0.1
port = 8583
streamer = mitracomm
listen = true
timeout = 60
module = opensipkd.iso8583.bjb.pbb.bogor_kota
module = opensipkd.iso8583.network
echo = true
__path__ = __import__('pkgutil').extend_path(__path__, __name__)
import os
import csv
import deform
from threading import Thread
from pkg_resources import resource_filename
from pyramid.i18n import get_localizer
from pyramid.threadlocal import get_current_request
from pyramid.config import Configurator
from pyramid_beaker import session_factory_from_settings
from pyramid_mailer import mailer_factory_from_settings
from .tools.this_framework import get_locale_name
from .views import RemoveSlashNotFoundViewFactory
from .iso8583 import (
read_conf,
main_loop as iso8583_main_loop,
)
def translator(term):
return get_localizer(get_current_request()).translate(term)
deform_template_dir = resource_filename('deform', 'templates/')
zpt_renderer = deform.ZPTRendererFactory(
[deform_template_dir], translator=translator)
deform.Form.set_default_renderer(zpt_renderer)
iso8583_threads = []
......@@ -41,20 +21,7 @@ def start_iso8583(conf_file):
def main(global_config, **settings):
""" This function returns a Pyramid WSGI application.
"""
session_factory = session_factory_from_settings(settings)
config = Configurator(
settings=settings,
root_factory='iso8583_web.models.ziggurat.RootFactory',
session_factory=session_factory,
locale_negotiator=get_locale_name)
config.include('.models')
config.include('pyramid_beaker')
config.include('pyramid_chameleon')
config.include('.routes')
config.include('.security')
config.add_notfound_view(RemoveSlashNotFoundViewFactory())
config.add_translation_dirs('locale')
config.registry['mailer'] = mailer_factory_from_settings(settings)
config = Configurator(settings=settings)
config.scan()
start_iso8583(global_config['__file__'])
return config.make_wsgi_app()
Generic single-database configuration.
\ No newline at end of file
"""Pyramid bootstrap environment. """
from alembic import context
from pyramid.paster import get_appsettings, setup_logging
from sqlalchemy import engine_from_config
from iso8583_web.models.meta import Base
config = context.config
setup_logging(config.config_file_name)
settings = get_appsettings(config.config_file_name)
target_metadata = Base.metadata
def run_migrations_offline():
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
context.configure(url=settings['sqlalchemy.url'])
with context.begin_transaction():
context.run_migrations()
def run_migrations_online():
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
engine = engine_from_config(settings, prefix='sqlalchemy.')
connection = engine.connect()
context.configure(
connection=connection,
target_metadata=target_metadata
)
try:
with context.begin_transaction():
context.run_migrations()
finally:
connection.close()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
branch_labels = ${repr(branch_labels)}
depends_on = ${repr(depends_on)}
def upgrade():
${upgrades if upgrades else "pass"}
def downgrade():
${downgrades if downgrades else "pass"}
"""alter users date fields with time zone
Revision ID: 074b33635316
Revises:
Create Date: 2018-10-11 12:00:48.568483
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '074b33635316'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
op.alter_column(
'users', 'last_login_date',
type_=sa.DateTime(timezone=True),
existing_type=sa.DateTime(timezone=False))
op.alter_column(
'users', 'registered_date',
type_=sa.DateTime(timezone=True),
existing_type=sa.DateTime(timezone=False))
op.alter_column(
'users', 'security_code_date',
type_=sa.DateTime(timezone=True),
existing_type=sa.DateTime(timezone=False))
def downgrade():
pass
......@@ -3,7 +3,6 @@ from time import (
time,
sleep,
)
from sqlalchemy import exc
from opensipkd.tcp.connection import join_ip_port
from .iso8583.read_conf import name_conf
from .iso8583.connection import conn_mgr
......@@ -151,17 +150,3 @@ class BaseIsoView(BaseView):
def timeout_error(self):
return Exception('Timeout')
SQL_CHECK = dict(
postgresql='SELECT 1',
oracle='SELECT 1 FROM dual')
def db_check(engine):
sql = SQL_CHECK[engine.dialect.name]
try:
engine.execute(sql)
except exc.DBAPIError as e:
if e.connection_invalidated:
log.debug('Database connection problem')
......@@ -99,7 +99,7 @@ def read_section(conf, section):
cfg['ip'] = conf.get(section, 'ip')
cfg['port'] = conf.getint(section, 'port')
ip_port = validate_ip_port(cfg)
cfg['name'] = section.lstrip(PREFIX_SECTION)
cfg['name'] = section.removeprefix(PREFIX_SECTION)
cfg['streamer'] = get_str(conf, section, 'streamer', 'none')
cfg['streamer_cls'] = get_streamer_class(cfg['streamer'])
cfg['ip'] = conf.get(section, 'ip')
......
# Translations template for colander.
# Copyright (C) 2016 ORGANIZATION
# This file is distributed under the same license as the colander project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2016.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: colander 1.3.1\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2016-09-18 22:57+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.3.4\n"
#: colander/__init__.py:294
msgid "Invalid value"
msgstr ""
#: colander/__init__.py:340
msgid "String does not match expected pattern"
msgstr ""
#: colander/__init__.py:359
msgid "Invalid email address"
msgstr ""
#: colander/__init__.py:387
msgid "${val} is less than minimum value ${min}"
msgstr ""
#: colander/__init__.py:388
msgid "${val} is greater than maximum value ${max}"
msgstr ""
#: colander/__init__.py:435
msgid "Shorter than minimum length ${min}"
msgstr ""
#: colander/__init__.py:436
msgid "Longer than maximum length ${max}"
msgstr ""
#: colander/__init__.py:464
msgid "\"${val}\" is not one of ${choices}"
msgstr ""
#: colander/__init__.py:479
msgid "\"${val}\" must not be one of ${choices}"
msgstr ""
#: colander/__init__.py:501
msgid "One or more of the choices you made was not acceptable"
msgstr ""
#: colander/__init__.py:523 colander/__init__.py:528
msgid "\"${val}\" is not a valid credit card number"
msgstr ""
#: colander/__init__.py:549
msgid "Must be a URL"
msgstr ""
#: colander/__init__.py:553
msgid "Invalid UUID string"
msgstr ""
#: colander/__init__.py:650
msgid "\"${val}\" is not a mapping type: ${err}"
msgstr ""
#: colander/__init__.py:694
msgid "Unrecognized keys in mapping: \"${val}\""
msgstr ""
#: colander/__init__.py:789 colander/__init__.py:1020
msgid "\"${val}\" is not iterable"
msgstr ""
#: colander/__init__.py:797
msgid "\"${val}\" has an incorrect number of elements (expected ${exp}, was ${was})"
msgstr ""
#: colander/__init__.py:936 colander/__init__.py:967
msgid "${cstruct} is not iterable"
msgstr ""
#: colander/__init__.py:1254
msgid "${val} cannot be serialized: ${err}"
msgstr ""
#: colander/__init__.py:1275
msgid "${val} is not a string: ${err}"
msgstr ""
#: colander/__init__.py:1295 colander/__init__.py:1306
msgid "\"${val}\" is not a number"
msgstr ""
#: colander/__init__.py:1454
msgid "${val} is not a string"
msgstr ""
#: colander/__init__.py:1465
msgid "\"${val}\" is neither in (${false_choices}) nor in (${true_choices})"
msgstr ""
#: colander/__init__.py:1525 colander/__init__.py:1542 colander/__init__.py:1552
msgid "relative name \"${val}\" irresolveable without package"
msgstr ""
#: colander/__init__.py:1582
msgid "\"${val}\" has no __name__"
msgstr ""
#: colander/__init__.py:1591
msgid "\"${val}\" is not a string"
msgstr ""
#: colander/__init__.py:1600
msgid "The dotted name \"${name}\" cannot be imported"
msgstr ""
#: colander/__init__.py:1648 colander/__init__.py:1722
msgid "Invalid date"
msgstr ""
#: colander/__init__.py:1662
msgid "\"${val}\" is not a datetime object"
msgstr ""
#: colander/__init__.py:1733
msgid "\"${val}\" is not a date object"
msgstr ""
#: colander/__init__.py:1794
msgid "Invalid time"
msgstr ""
#: colander/__init__.py:1804
msgid "\"${val}\" is not a time object"
msgstr ""
#: colander/tests/test_colander.py:334 colander/tests/test_colander.py:341
msgid "fail ${val}"
msgstr ""
#: colander/tests/test_colander.py:537
msgid "${val}: ${choices}"
msgstr ""
#
# SOME DESCRIPTIVE TITLE
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2016.
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE 1.0\n"
"POT-Creation-Date: 2016-11-19 13:28+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: \n"
"Generated-By: Lingua 4.11\n"
#: ./deform/schema.py:109
#, python-format
msgid "${value} is not a dictionary"
msgstr ""
#: ./deform/schema.py:116
#, python-format
msgid "${value} has no ${key} key"
msgstr ""
#: ./deform/widget.py:718 ./deform/widget.py:1800
msgid "Incomplete date"
msgstr ""
#: ./deform/widget.py:721
msgid "Incomplete time"
msgstr ""
#: ./deform/widget.py:1253
msgid "Fields did not match"
msgstr ""
#: ./deform/widget.py:1254
msgid "Value"
msgstr ""
#: ./deform/widget.py:1255
msgid "Confirm Value"
msgstr ""
#: ./deform/widget.py:1321
msgid "Password did not match confirm"
msgstr ""
#: ./deform/widget.py:1485
#, python-format
msgid "Add ${subitem_title}"
msgstr ""
#: ./deform/templates/checked_password.pt:15
msgid "Password"
msgstr ""
#: ./deform/templates/checked_password.pt:25
msgid "Confirm Password"
msgstr ""
#: ./deform/templates/dateparts.pt:10
msgid "Year"
msgstr ""
#: ./deform/templates/dateparts.pt:18
msgid "Month"
msgstr ""
#: ./deform/templates/dateparts.pt:26
msgid "Day"
msgstr ""
#: ./deform/templates/datetimeinput.pt:9
msgid "Date"
msgstr ""
#: ./deform/templates/datetimeinput.pt:16
msgid "Time"
msgstr ""
#: ./deform/templates/form.pt:35
msgid "There was a problem with your submission"
msgstr ""
#: ./deform/templates/form.pt:37
msgid "Errors have been highlighted below"
msgstr ""
#: ./deform/templates/mapping.pt:13 ./deform/templates/mapping_accordion.pt:29
msgid "There was a problem with this section"
msgstr ""
#. Default: en
#: ./deform/templates/richtext.pt:30
msgid "language-code"
msgstr ""
#: ./deform/templates/sequence_item.pt:26
msgid "Reorder (via drag and drop)"
msgstr ""
#: ./deform/templates/sequence_item.pt:31
msgid "Remove"
msgstr ""
#: ./deform/templates/readonly/checkbox.pt:8
msgid "True"
msgstr ""
#: ./deform/templates/readonly/checkbox.pt:12
msgid "False"
msgstr ""
#: ./deform/templates/readonly/checked_password.pt:4
msgid "Password not displayed."
msgstr ""
#: ./deform/tests/test_widget.py:1752
#, python-format
msgid "Yo ${subitem_title}"
msgstr ""
#
# SOME DESCRIPTIVE TITLE
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2018.
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE 1.0\n"
"POT-Creation-Date: 2018-10-15 20:15+0700\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Lingua 4.13\n"
#. Default: Copyright &copy; Pylons Project
msgid "copyright"
msgstr ""
msgid "welcome"
msgstr ""
msgid "you-are-logged-in"
msgstr ""
msgid "login-link"
msgstr ""
msgid "Home"
msgstr ""
# Translations template for colander.
# Copyright (C) 2016 ORGANIZATION
# This file is distributed under the same license as the colander project.
# Owo Sugiana <sugiana@gmail.com>, 2016.
#
msgid ""
msgstr ""
"Project-Id-Version: colander 1.3.1\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2016-09-18 22:57+0200\n"
"PO-Revision-Date: 2018-10-14 19:52+0700\n"
"Last-Translator: Owo Sugiana <sugiana@gmail.com>\n"
"Language-Team: Indonesian\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.3.4\n"
"Language: id\n"
#: colander/__init__.py:294
msgid "Invalid value"
msgstr ""
#: colander/__init__.py:340
msgid "String does not match expected pattern"
msgstr ""
#: colander/__init__.py:359
msgid "Invalid email address"
msgstr ""
#: colander/__init__.py:387
msgid "${val} is less than minimum value ${min}"
msgstr ""
#: colander/__init__.py:388
msgid "${val} is greater than maximum value ${max}"
msgstr ""
#: colander/__init__.py:435
msgid "Shorter than minimum length ${min}"
msgstr ""
#: colander/__init__.py:436
msgid "Longer than maximum length ${max}"
msgstr ""
#: colander/__init__.py:464
msgid "\"${val}\" is not one of ${choices}"
msgstr ""
#: colander/__init__.py:479
msgid "\"${val}\" must not be one of ${choices}"
msgstr ""
#: colander/__init__.py:501
msgid "One or more of the choices you made was not acceptable"
msgstr ""
#: colander/__init__.py:523 colander/__init__.py:528
msgid "\"${val}\" is not a valid credit card number"
msgstr ""
#: colander/__init__.py:549
msgid "Must be a URL"
msgstr ""
#: colander/__init__.py:553
msgid "Invalid UUID string"
msgstr ""
#: colander/__init__.py:650
msgid "\"${val}\" is not a mapping type: ${err}"
msgstr ""
#: colander/__init__.py:694
msgid "Unrecognized keys in mapping: \"${val}\""
msgstr ""
#: colander/__init__.py:789 colander/__init__.py:1020
msgid "\"${val}\" is not iterable"
msgstr ""
#: colander/__init__.py:797
msgid ""
"\"${val}\" has an incorrect number of elements (expected ${exp}, was ${was})"
msgstr ""
#: colander/__init__.py:936 colander/__init__.py:967
msgid "${cstruct} is not iterable"
msgstr ""
#: colander/__init__.py:1254
msgid "${val} cannot be serialized: ${err}"
msgstr ""
#: colander/__init__.py:1275
msgid "${val} is not a string: ${err}"
msgstr ""
#: colander/__init__.py:1295 colander/__init__.py:1306
msgid "\"${val}\" is not a number"
msgstr ""
#: colander/__init__.py:1454
msgid "${val} is not a string"
msgstr ""
#: colander/__init__.py:1465
msgid "\"${val}\" is neither in (${false_choices}) nor in (${true_choices})"
msgstr ""
#: colander/__init__.py:1525 colander/__init__.py:1542
#: colander/__init__.py:1552
msgid "relative name \"${val}\" irresolveable without package"
msgstr ""
#: colander/__init__.py:1582
msgid "\"${val}\" has no __name__"
msgstr ""
#: colander/__init__.py:1591
msgid "\"${val}\" is not a string"
msgstr ""
#: colander/__init__.py:1600
msgid "The dotted name \"${name}\" cannot be imported"
msgstr ""
#: colander/__init__.py:1648 colander/__init__.py:1722
msgid "Invalid date"
msgstr ""
#: colander/__init__.py:1662
msgid "\"${val}\" is not a datetime object"
msgstr ""
#: colander/__init__.py:1733
msgid "\"${val}\" is not a date object"
msgstr ""
#: colander/__init__.py:1794
msgid "Invalid time"
msgstr ""
#: colander/__init__.py:1804
msgid "\"${val}\" is not a time object"
msgstr ""
#: colander/tests/test_colander.py:334 colander/tests/test_colander.py:341
msgid "fail ${val}"
msgstr ""
#: colander/tests/test_colander.py:537
msgid "${val}: ${choices}"
msgstr ""
msgid "Required"
msgstr "Dibutuhkan"
#
# Indonesian translations for PACKAGE package
# This file is distributed under the same license as the PACKAGE package.
# Owo Sugiana <sugiana@gmail.com>, 2016.
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE 1.0\n"
"POT-Creation-Date: 2016-11-19 13:28+0200\n"
"PO-Revision-Date: 2018-10-14 19:45+0700\n"
"Last-Translator: Owo Sugiana <sugiana@gmail.com>\n"
"Language-Team: Indonesian\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: id\n"
"Generated-By: Lingua 4.11\n"
#: deform/schema.py:109
#, python-format
msgid "${value} is not a dictionary"
msgstr ""
#: deform/schema.py:116
#, python-format
msgid "${value} has no ${key} key"
msgstr ""
#: deform/widget.py:718 deform/widget.py:1800
msgid "Incomplete date"
msgstr ""
#: deform/widget.py:721
msgid "Incomplete time"
msgstr ""
#: deform/widget.py:1253
msgid "Fields did not match"
msgstr ""
#: deform/widget.py:1254
msgid "Value"
msgstr ""
#: deform/widget.py:1255
msgid "Confirm Value"
msgstr ""
#: deform/widget.py:1321
msgid "Password did not match confirm"
msgstr ""
#: deform/widget.py:1485
#, python-format
msgid "Add ${subitem_title}"
msgstr ""
#: deform/templates/checked_password.pt:15
msgid "Password"
msgstr ""
#: deform/templates/checked_password.pt:25
msgid "Confirm Password"
msgstr ""
#: deform/templates/dateparts.pt:10
msgid "Year"
msgstr ""
#: deform/templates/dateparts.pt:18
msgid "Month"
msgstr ""
#: deform/templates/dateparts.pt:26
msgid "Day"
msgstr ""
#: deform/templates/datetimeinput.pt:9
msgid "Date"
msgstr ""
#: deform/templates/datetimeinput.pt:16
msgid "Time"
msgstr ""
#: deform/templates/form.pt:35
msgid "There was a problem with your submission"
msgstr "Ada masalah dengan apa yang Anda kirim"
#: deform/templates/form.pt:37
msgid "Errors have been highlighted below"
msgstr "Pesan kesalahannya ada di bawah ini"
#: deform/templates/mapping.pt:13 deform/templates/mapping_accordion.pt:29
msgid "There was a problem with this section"
msgstr ""
#. Default: en
#: deform/templates/richtext.pt:30
msgid "language-code"
msgstr ""
#: deform/templates/sequence_item.pt:26
msgid "Reorder (via drag and drop)"
msgstr ""
#: deform/templates/sequence_item.pt:31
msgid "Remove"
msgstr ""
#: deform/templates/readonly/checkbox.pt:8
msgid "True"
msgstr ""
#: deform/templates/readonly/checkbox.pt:12
msgid "False"
msgstr ""
#: deform/templates/readonly/checked_password.pt:4
msgid "Password not displayed."
msgstr ""
#: deform/tests/test_widget.py:1752
#, python-format
msgid "Yo ${subitem_title}"
msgstr ""
#
# Indonesian translations for PACKAGE package
# This file is distributed under the same license as the PACKAGE package.
# Owo Sugiana <sugiana@gmail.com>, 2018.
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE 1.0\n"
"POT-Creation-Date: 2018-10-15 20:15+0700\n"
"PO-Revision-Date: 2018-10-19 17:11+0700\n"
"Last-Translator: Owo Sugiana <sugiana@gmail.com>\n"
"Language-Team: Indonesian\n"
"Language: id\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Lingua 4.13\n"
#. Default: Copyright &copy; Pylons Project
msgid "copyright"
msgstr "Hak cipta &copy; Pylons Project"
msgid "welcome"
msgstr "Selamat datang di <span class=\"font-normal\">Web Starter</span>, sebuah aplikasi Pyramid yang dibentuk oleh<br><span class=\"font-normal\">Cookiecutter</span>."
msgid "you-are-logged-in"
msgstr "Anda sedang masuk sebagai ${username}. Klik <a href=\"/logout\">di sini</a> untuk keluar."
msgid "login-link"
msgstr "Silakan masuk"
msgid "Home"
msgstr "Beranda"
#
# Indonesian translations for PACKAGE package
# This file is distributed under the same license as the PACKAGE package.
# Owo Sugiana <sugiana@gmail.com>, 2018.
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE 1.0\n"
"POT-Creation-Date: 2018-10-15 08:41+0700\n"
"PO-Revision-Date: 2018-10-15 08:41+0700\n"
"Last-Translator: Owo Sugiana <sugiana@gmail.com>\n"
"Language-Team: Indonesian\n"
"Language: id\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Lingua 4.13\n"
#. Default: Enter new password for ${name}:
msgid "ask-password-1"
msgstr "Masukkan password baru untuk ${name}: "
#. Default: Retype new password for ${name}:
msgid "ask-password-2"
msgstr "Ulangi password baru untuk ${name}: "
msgid "Sorry, passwords do not match"
msgstr "Maaf, kedua password tidak sama"
#
# Indonesian translations for PACKAGE package
# This file is distributed under the same license as the PACKAGE package.
# Owo Sugiana <sugiana@gmail.com>, 2018.
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE 1.0\n"
"POT-Creation-Date: 2018-10-13 22:28+0700\n"
"PO-Revision-Date: 2018-10-13 22:30+0700\n"
"Last-Translator: Owo Sugiana <sugiana@gmail.com>\n"
"Language-Team: Indonesian\n"
"Language: id\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Lingua 4.13\n"
#. Default: Login failed
#: views/login.py:42
msgid "Login failed"
msgstr "Gagal masuk"
#. Default: Login
#: views/login.py:58
msgid "Login"
msgstr "Masuk"
msgid "Submit"
msgstr "Kirim"
msgid "Username"
msgstr "Nama"
msgid "Password"
msgstr "Kata kunci"
msgid "Reset password"
msgstr "Pemulihan kata kunci"
msgid "email-reset-password"
msgstr "Tulis email Anda dan kami akan mengirimkan tautan untuk penetapan ulang kata kunci"
msgid "Send password reset email"
msgstr "Kirim email"
msgid "reset-password-body"
msgstr "Kami menerima permintaan pemulihan kata sandi. Silakan klik tautan berikut:\n\n${url}\n\nTautan ini akan kedaluwarsa dalam ${minutes} menit. Mohon abaikan jika Anda tidak memintanya."
msgid "reset-password-link-sent"
msgstr "Periksa email Anda untuk tautan pemulihan kata kunci. Jika tidak muncul dalam beberapa menit, periksa di bagian spam."
msgid "Invalid email"
msgstr "Email tidak terdaftar"
msgid "Forgot password"
msgstr "Lupa kata kunci"
msgid "change-password-done"
msgstr "Kata kunci Anda telah diubah"
msgid "Save"
msgstr "Simpan"
msgid "Cancel"
msgstr "Batalkan"
msgid "Invalid old password"
msgstr "Kata kunci yang lama tidak benar"
msgid "Retype mismatch"
msgstr "Pengulangan kata kunci yang baru tidak sama"
msgid "Old password"
msgstr "Kata kunci yang lama"
msgid "New password"
msgstr "Kata kunci yang baru"
msgid "Retype new password"
msgstr "Ulangi kata kunci yang baru"
msgid "Change password"
msgstr "Ganti kata kunci"
msgid "Invalid security code"
msgstr "Kode keamananan tidak benar"
#
# Indonesian translations for PACKAGE package
# This file is distributed under the same license as the PACKAGE package.
# Owo Sugiana <sugiana@gmail.com>, 2018.
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE 1.0\n"
"POT-Creation-Date: 2018-10-28 21:10+0700\n"
"PO-Revision-Date: 2018-10-28 21:11+0700\n"
"Last-Translator: Owo Sugiana <sugiana@gmail.com>\n"
"Language-Team: Indonesian\n"
"Language: id\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Lingua 4.13\n"
#. Default: Home
#: views/templates/layout-menu.pt:40
msgid "Home"
msgstr "Beranda"
#. Default: Admin<dynamic element>
#: views/templates/layout-menu.pt:44
msgid "Admin"
msgstr "Admin"
#. Default: Users
#: views/templates/layout-menu.pt:46
msgid "Users"
msgstr "Pengguna"
#. Default: Add user
#: views/templates/layout-menu.pt:47
msgid "Add user"
msgstr "Tambah pengguna"
#. Default: My account <dynamic element>
#: views/templates/layout-menu.pt:54
msgid "My account"
msgstr "Akun saya"
#. Default: Change password
#: views/templates/layout-menu.pt:56
msgid "Change password"
msgstr "Ubah kata kunci"
#. Default: ${username} logout
#: views/templates/layout-menu.pt:57
msgid "username-logout"
msgstr "${username} keluar"
msgid "Users groups"
msgstr "Kelompok pengguna"
msgid "Add users group"
msgstr "Tambah kelompok pengguna"
#
# Indonesian translations for PACKAGE package
# This file is distributed under the same license as the PACKAGE package.
# Owo Sugiana <sugiana@gmail.com>, 2018.
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE 1.0\n"
"POT-Creation-Date: 2018-10-27 14:20+0700\n"
"PO-Revision-Date: 2018-10-27 14:49+0700\n"
"Last-Translator: Owo Sugiana <sugiana@gmail.com>\n"
"Language-Team: Indonesian\n"
"Language: id\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Lingua 4.13\n"
#: web_starter/views/user.py:43
msgid "Users"
msgstr "Pengguna"
#: web_starter/views/user.py:78
msgid "Invalid email format"
msgstr "Susunan email tidak benar"
#. Default: Email ${email} already used by user ID ${uid}
#: web_starter/views/user.py:87
msgid "email-already-used"
msgstr "Email ${email} sudah digunakan oleh ID ${uid}"
#. Default: Only a-z, 0-9, and - characters are allowed
#: web_starter/views/user.py:107
msgid "username-only-contain"
msgstr "Hanya boleh karakter a-z, 0-9, dan -"
#. Default: Only a-z or 0-9 at the start and end
#: web_starter/views/user.py:113
msgid "username-first-end-alphanumeric"
msgstr "Awal dan akhir hanya boleh a-z atau 0-9"
#. Default: Username ${username} already used by ID ${uid}
#: web_starter/views/user.py:122
msgid "username-already-used"
msgstr "Nama ${username} sudah digunakan ID ${uid}"
#: web_starter/views/user.py:141
msgid "Email"
msgstr "Email"
#: web_starter/views/user.py:143
msgid "Username"
msgstr "Nama"
#: web_starter/views/user.py:146
msgid "Group"
msgstr "Grup"
#: web_starter/views/user.py:154
msgid "Status"
msgstr "Status"
#: web_starter/views/user.py:165
msgid "Active"
msgstr "Aktif"
#: web_starter/views/user.py:166
msgid "Inactive"
msgstr "Tidak aktif"
#: web_starter/views/user.py:176
msgid "Save"
msgstr "Simpan"
#: web_starter/views/user.py:177 web_starter/views/user.py:299
msgid "Cancel"
msgstr "Batalkan"
#: web_starter/views/user.py:202
msgid "Add user"
msgstr "Tambah pengguna"
#: web_starter/views/user.py:219
msgid "user-added"
msgstr "${email} sudah ditambahkan dan email untuknya sudah dikirim"
#: web_starter/views/user.py:266
msgid "Edit user"
msgstr "Ubah pengguna"
#. Default: ${username} profile updated
#: web_starter/views/user.py:282
msgid "user-updated"
msgstr "Profil ${username} sudah diperbarui"
#: web_starter/views/user.py:298
msgid "Delete"
msgstr "Hapus"
#: web_starter/views/user.py:303
msgid "Delete user"
msgstr "Hapus pengguna"
#. Default: User ${email} ID ${uid} has been deleted
#: web_starter/views/user.py:306
msgid "user-deleted"
msgstr "${email} ID ${uid} sudah dihapus"
msgid "Registered date"
msgstr "Tanggal pendaftaran"
msgid "Last login"
msgstr "Terakhir masuk"
msgid "Edit"
msgstr "Ubah"
msgid "System"
msgstr "Sistem"
msgid "You"
msgstr "Anda"
msgid "Finance"
msgstr "Keuangan"
msgid "Warning"
msgstr "Perhatian"
msgid "warning-delete-user"
msgstr "Hapus ${email} ID ${uid} ?"
msgid "Show"
msgstr "Lihat"
msgid "user-result"
msgstr "Ada ${count} baris"
msgid "No result"
msgstr "Tidak ada hasil"
msgid "Groups"
msgstr "Kelompok"
msgid "Group name"
msgstr "Nama kelompok"
msgid "Description"
msgstr "Keterangan"
msgid "Member count"
msgstr "Jumlah anggota"
#
# SOME DESCRIPTIVE TITLE
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2018.
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE 1.0\n"
"POT-Creation-Date: 2018-10-15 08:41+0700\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Lingua 4.13\n"
#. Default: Enter new password for ${name}:
msgid "ask-password-1"
msgstr ""
#. Default: Retype new password for ${name}:
msgid "ask-password-2"
msgstr ""
msgid "Sorry, passwords do not match"
msgstr ""
#
# SOME DESCRIPTIVE TITLE
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2018.
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE 1.0\n"
"POT-Creation-Date: 2018-10-13 22:28+0700\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Lingua 4.13\n"
#. Default: Login failed
#: ./views/login.py:42
msgid "login-failed"
msgstr ""
#. Default: Login
#: ./views/login.py:58
msgid "login"
msgstr ""
msgid "Reset password"
msgstr ""
#. Default: Enter your email address and we will send you a link to reset your password.
msgid "email-reset-password"
msgstr ""
msgid "Send password reset email"
msgstr ""
msgid "reset-password-body"
msgstr ""
msgid "reset-password-link-sent"
msgstr ""
msgid "change-password-done"
msgstr ""
msgid "Save"
msgstr ""
msgid "Cancel"
msgstr ""
msgid "Invalid old password"
msgstr ""
msgid "Retype mismatch"
msgstr ""
msgid "Old password"
msgstr ""
msgid "New password"
msgstr ""
msgid "Retype new password"
msgstr ""
msgid "Change password"
msgstr ""
msgid "Invalid security code"
msgstr ""
#
# SOME DESCRIPTIVE TITLE
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2018.
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE 1.0\n"
"POT-Creation-Date: 2018-10-28 21:10+0700\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Lingua 4.13\n"
#. Default: Home
#: ./views/templates/layout-menu.pt:40
msgid "Home"
msgstr ""
#. Default: Admin<dynamic element>
#: ./views/templates/layout-menu.pt:44
msgid "Admin"
msgstr ""
#. Default: Users
#: ./views/templates/layout-menu.pt:46
msgid "Users"
msgstr ""
#. Default: Add user
#: ./views/templates/layout-menu.pt:47
msgid "Add user"
msgstr ""
#. Default: My account <dynamic element>
#: ./views/templates/layout-menu.pt:54
msgid "My account"
msgstr ""
#. Default: Change password
#: ./views/templates/layout-menu.pt:56
msgid "Change password"
msgstr ""
#. Default: ${username} logout
#: ./views/templates/layout-menu.pt:57
msgid "username-logout"
msgstr ""
msgid "Users groups"
msgstr ""
msgid "Add users group"
msgstr ""
#
# SOME DESCRIPTIVE TITLE
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2018.
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE 1.0\n"
"POT-Creation-Date: 2018-10-27 14:20+0700\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Lingua 4.13\n"
#: ./web_starter/views/user.py:43
msgid "Users"
msgstr ""
#: ./web_starter/views/user.py:78
msgid "Invalid email format"
msgstr ""
#. Default: Email ${email} already used by user ID ${uid}
#: ./web_starter/views/user.py:87
msgid "email-already-used"
msgstr ""
#. Default: Only a-z, 0-9, and - characters are allowed
#: ./web_starter/views/user.py:107
msgid "username-only-contain"
msgstr ""
#. Default: Only a-z or 0-9 at the start and end
#: ./web_starter/views/user.py:113
msgid "username-first-end-alphanumeric"
msgstr ""
#. Default: Username ${username} already used by ID ${uid}
#: ./web_starter/views/user.py:122
msgid "username-already-used"
msgstr ""
#: ./web_starter/views/user.py:141
msgid "Email"
msgstr ""
#: ./web_starter/views/user.py:143
msgid "Username"
msgstr ""
#: ./web_starter/views/user.py:146
msgid "Group"
msgstr ""
#: ./web_starter/views/user.py:154
msgid "Status"
msgstr ""
#: ./web_starter/views/user.py:165
msgid "Active"
msgstr ""
#: ./web_starter/views/user.py:166
msgid "Inactive"
msgstr ""
#: ./web_starter/views/user.py:176
msgid "Save"
msgstr ""
#: ./web_starter/views/user.py:177 ./web_starter/views/user.py:299
msgid "Cancel"
msgstr ""
#: ./web_starter/views/user.py:202
msgid "Add user"
msgstr ""
#: ./web_starter/views/user.py:219
msgid "user-added"
msgstr ""
#: ./web_starter/views/user.py:266
msgid "Edit user"
msgstr ""
#. Default: ${username} profile updated
#: ./web_starter/views/user.py:282
msgid "user-updated"
msgstr ""
#: ./web_starter/views/user.py:298
msgid "Delete"
msgstr ""
#: ./web_starter/views/user.py:303
msgid "Delete user"
msgstr ""
#. Default: User ${email} ID ${uid} has been deleted
#: ./web_starter/views/user.py:306
msgid "user-deleted"
msgstr ""
msgid "Registered date"
msgstr ""
msgid "Last login"
msgstr ""
msgid "Edit"
msgstr ""
msgid "System"
msgstr ""
msgid "You"
msgstr ""
msgid "Finance"
msgstr ""
msgid "Warning"
msgstr ""
msgid "warning-delete-user"
msgstr ""
msgid "Show"
msgstr ""
msgid "user-result"
msgstr ""
msgid "No result"
msgstr ""
msgid "Groups"
msgstr ""
msgid "Group name"
msgstr ""
msgid "Description"
msgstr ""
msgid "Member count"
msgstr ""
from sqlalchemy import engine_from_config
from sqlalchemy.orm import sessionmaker
from sqlalchemy.orm import configure_mappers
import zope.sqlalchemy
# run configure_mappers after defining all of the models to ensure
# all relationships can be setup
configure_mappers()
def get_engine(settings, prefix='sqlalchemy.'):
return engine_from_config(settings, prefix)
def get_session_factory(engine):
factory = sessionmaker()
factory.configure(bind=engine)
return factory
def get_tm_session(session_factory, transaction_manager):
"""
Get a ``sqlalchemy.orm.Session`` instance backed by a transaction.
This function will hook the session to the transaction manager which
will take care of committing any changes.
- When using pyramid_tm it will automatically be committed or aborted
depending on whether an exception is raised.
- When using scripts you should wrap the session in a manager yourself.
For example::
import transaction
engine = get_engine(settings)
session_factory = get_session_factory(engine)
with transaction.manager:
dbsession = get_tm_session(session_factory, transaction.manager)
"""
dbsession = session_factory()
zope.sqlalchemy.register(
dbsession, transaction_manager=transaction_manager)
return dbsession
def includeme(config):
"""
Initialize the model for a Pyramid app.
Activate this setup using ``config.include('.models')``.
"""
settings = config.get_settings()
settings['tm.manager_hook'] = 'pyramid_tm.explicit_manager'
# use pyramid_tm to hook the transaction lifecycle to the request
config.include('pyramid_tm')
# use pyramid_retry to retry a request when transient exceptions occur
config.include('pyramid_retry')
session_factory = get_session_factory(get_engine(settings))
config.registry['dbsession_factory'] = session_factory
# make request.dbsession available for use in Pyramid
config.add_request_method(
# r.tm is the transaction manager used by pyramid_tm
lambda r: get_tm_session(session_factory, r.tm),
'dbsession',
reify=True
)
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.schema import MetaData
# Recommended naming convention used by Alembic, as various different database
# providers will autogenerate vastly different names making migrations more
# difficult. See: http://alembic.zzzcomputing.com/en/latest/naming.html
NAMING_CONVENTION = {
"ix": "ix_%(column_0_label)s",
"uq": "uq_%(table_name)s_%(column_0_name)s",
"ck": "ck_%(table_name)s_%(constraint_name)s",
"fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
"pk": "pk_%(table_name)s"
}
metadata = MetaData(naming_convention=NAMING_CONVENTION)
Base = declarative_base(metadata=metadata)
class CommonModel(object):
def to_dict(self):
values = {}
for column in self.__table__.columns:
values[column.name] = getattr(self, column.name)
return values
def to_dict_without_none(self):
values = {}
for column in self.__table__.columns:
val = getattr(self, column.name)
if val is not None:
values[column.name] = val
return values
from pyramid.security import (
Allow,
Authenticated,
ALL_PERMISSIONS,
)
from sqlalchemy import PrimaryKeyConstraint
import ziggurat_foundations.models
from ziggurat_foundations.models.base import BaseModel
from ziggurat_foundations.models.external_identity import ExternalIdentityMixin
from ziggurat_foundations.models.group import GroupMixin
from ziggurat_foundations.models.group_permission import GroupPermissionMixin
from ziggurat_foundations.models.group_resource_permission \
import GroupResourcePermissionMixin
from ziggurat_foundations.models.resource import ResourceMixin
from ziggurat_foundations.models.user import UserMixin
from ziggurat_foundations.models.user_group import UserGroupMixin
from ziggurat_foundations.models.user_permission import UserPermissionMixin
from ziggurat_foundations.models.user_resource_permission \
import UserResourcePermissionMixin
from ziggurat_foundations import ziggurat_model_init
from .meta import (
Base,
CommonModel,
)
# Base is sqlalchemy's Base = declarative_base() from your project
class Group(GroupMixin, Base, CommonModel):
pass
class GroupPermission(GroupPermissionMixin, Base):
pass
class UserGroup(UserGroupMixin, Base):
pass
class GroupResourcePermission(GroupResourcePermissionMixin, Base):
__table_args__ = (
PrimaryKeyConstraint(
'group_id',
'resource_id',
'perm_name'),)
class Resource(ResourceMixin, Base):
# ... your own properties....
# example implementation of ACLS for pyramid application
@property
def __acl__(self):
acls = []
if self.owner_user_id:
acls.extend([(Allow, self.owner_user_id, ALL_PERMISSIONS,), ])
if self.owner_group_id:
acls.extend([(Allow, 'group:%s' % self.owner_group_id,
ALL_PERMISSIONS,), ])
return acls
class UserPermission(UserPermissionMixin, Base):
pass
class UserResourcePermission(UserResourcePermissionMixin, Base):
pass
class User(UserMixin, Base, CommonModel):
# ... your own properties....
pass
class ExternalIdentity(ExternalIdentityMixin, Base):
pass
# you can define multiple resource derived models to build a complex
# application like CMS, forum or other permission based solution
# class Entry(Resource):
# '''
# Resource of `entry` type
# '''
# __tablename__ = 'entries'
# __mapper_args__ = {'polymorphic_identity': 'entry'}
# resource_id = sa.Column(sa.Integer(),
# sa.ForeignKey('resources.resource_id',
# onupdate='CASCADE',
# ondelete='CASCADE', ),
# primary_key=True, )
# ... your own properties....
# some_property = sa.Column(sa.UnicodeText())
class RootFactory:
def __init__(self, request):
self.__acl__ = [
(Allow, Authenticated, 'view'),
(Allow, 'group:1', ALL_PERMISSIONS),
]
for gp in request.dbsession.query(GroupPermission):
acl_name = 'group:{}'.format(gp.group_id)
self.__acl__.append((Allow, acl_name, gp.perm_name))
ziggurat_model_init(
User, Group, UserGroup, GroupPermission, UserPermission,
UserResourcePermission, GroupResourcePermission, Resource,
ExternalIdentity, passwordmanager=None)
def includeme(config):
config.add_static_view('static', 'static', cache_max_age=3600)
config.add_static_view('deform_static', 'deform:static')
config.add_route('home', '/')
config.add_route('login', '/login')
config.add_route('logout', '/logout')
config.add_route('change-password', '/change-password')
config.add_route('change-password-done', '/change-password-done')
config.add_route('reset-password', '/reset-password')
config.add_route('reset-password-sent', '/reset-password-sent')
config.add_route('login-by-code-failed', '/login-by-code-failed')
config.add_route('user', '/user')
config.add_route('user-add', '/user/add')
config.add_route('user-edit', '/user/{id}')
config.add_route('user-delete', '/user/{id}/delete')
config.add_route('group', '/group')
config.add_route('group-add', '/group/add')
config.add_route('group-edit', '/group/{id}')
config.add_route('group-delete', '/group/{id}/delete')
email,status,user_name
admin@local,1,admin
import os
import sys
import csv
import subprocess
from getpass import getpass
from argparse import ArgumentParser
import transaction
from ziggurat_foundations.models.services.user import UserService
from pyramid.paster import (
get_appsettings,
setup_logging,
)
from pyramid.i18n import (
Localizer,
TranslationStringFactory,
Translations,
)
from ..models import (
get_engine,
get_session_factory,
get_tm_session,
)
from ..models.ziggurat import (
Group,
GroupPermission,
UserGroup,
User,
)
from ..models.meta import (
metadata,
Base,
)
domain = 'initialize_db'
_ = TranslationStringFactory(domain)
my_registry = dict()
class MyLocalizer:
def __init__(self):
settings = my_registry['settings']
locale_name = settings['pyramid.default_locale_name']
here = os.path.abspath(os.path.dirname(__file__))
locale_dir = os.path.join(here, '..', 'locale')
translations = Translations.load(locale_dir, [locale_name], domain)
self.localizer = Localizer(locale_name, translations)
def translate(self, ts):
return self.localizer.translate(ts)
def read_file(filename):
f = open(filename)
s = f.read()
f.close()
return s
def alembic_run(ini_file):
bin_path = os.path.split(sys.executable)[0]
alembic_bin = os.path.join(bin_path, 'alembic')
command = (alembic_bin, '-c', ini_file, 'upgrade', 'head')
if subprocess.call(command) != 0:
sys.exit()
def get_file(filename):
base_dir = os.path.split(__file__)[0]
fullpath = os.path.join(base_dir, 'data', filename)
return open(fullpath)
def ask_password(name):
localizer = MyLocalizer()
data = dict(name=name)
t_msg1 = _(
'ask-password-1', default='Enter new password for ${name}: ',
mapping=data)
t_msg2 = _(
'ask-password-2', default='Retype new password for ${name}: ',
mapping=data)
msg1 = localizer.translate(t_msg1)
msg2 = localizer.translate(t_msg2)
while True:
pass1 = getpass(msg1)
if not pass1:
continue
pass2 = getpass(msg2)
if pass1 == pass2:
return pass1
ts = _('Sorry, passwords do not match')
print(localizer.translate(ts))
def restore_csv(table, filename):
DBSession = my_registry['dbsession']
q = DBSession.query(table)
if q.first():
return
with get_file(filename) as f:
reader = csv.DictReader(f)
for cf in reader:
row = table()
for fieldname in cf:
val = cf[fieldname]
if not val:
continue
setattr(row, fieldname, val)
DBSession.add(row)
return True
def append_csv(table, filename, keys):
DBSession = my_registry['dbsession']
with get_file(filename) as f:
reader = csv.DictReader(f)
filter_ = dict()
for cf in reader:
for key in keys:
filter_[key] = cf[key]
q = DBSession.query(table).filter_by(**filter_)
found = q.first()
if found:
continue
row = table()
for fieldname in cf:
val = cf[fieldname]
if not val:
continue
setattr(row, fieldname, val)
DBSession.add(row)
def setup_models(dbsession):
metadata.create_all(dbsession.bind)
if restore_csv(User, 'users.csv'):
dbsession.flush()
q = dbsession.query(User).filter_by(id=1)
user = q.first()
password = ask_password(user.user_name)
UserService.set_password(user, password)
append_csv(Group, 'groups.csv', ['group_name'])
restore_csv(UserGroup, 'users_groups.csv')
def parse_args(argv):
parser = ArgumentParser()
parser.add_argument(
'config_uri',
help='Configuration file, e.g., development.ini',
)
return parser.parse_args(argv[1:])
def main(argv=sys.argv):
args = parse_args(argv)
setup_logging(args.config_uri)
my_registry['settings'] = settings = get_appsettings(args.config_uri)
engine = get_engine(settings)
Base.metadata.create_all(engine)
alembic_run(args.config_uri)
session_factory = get_session_factory(engine)
with transaction.manager:
my_registry['dbsession'] = dbsession = get_tm_session(
session_factory, transaction.manager)
setup_models(dbsession)
from pyramid.authentication import AuthTktAuthenticationPolicy
from pyramid.authorization import ACLAuthorizationPolicy
from .models.ziggurat import (
User,
UserGroup,
)
def group_finder(login, request):
q = request.dbsession.query(User).filter_by(id=login)
u = q.first()
if not u or not u.status:
return # None means logout
r = []
q = request.dbsession.query(UserGroup).filter_by(user_id=u.id)
for ug in q:
acl_name = f'group:{ug.group_id}'
r.append(acl_name)
return r
def get_user(request):
uid = request.authenticated_userid
if uid:
q = request.dbsession.query(User).filter_by(id=uid)
return q.first()
def includeme(config):
authn_policy = AuthTktAuthenticationPolicy(
'sosecret', callback=group_finder, hashalg='sha512')
config.set_authentication_policy(authn_policy)
authz_policy = ACLAuthorizationPolicy()
config.set_authorization_policy(authz_policy)
config.add_request_method(get_user, 'user', reify=True)
.grid-container {
display: grid;
grid-template-columns: auto auto auto;
}
body {
padding-top: 40px;
padding-bottom: 40px;
background-color: #eee;
}
.form-signin {
max-width: 330px;
padding: 15px;
margin: 0 auto;
}
.form-signin .form-signin-heading,
.form-signin .checkbox {
margin-bottom: 10px;
}
.form-signin .checkbox {
font-weight: normal;
}
.form-signin .form-control {
position: relative;
font-size: 16px;
height: auto;
padding: 10px;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.form-signin .form-control:focus {
z-index: 2;
}
.form-signin input[type="text"] {
margin-bottom: -1px;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
.form-signin input[type="password"] {
margin-bottom: 10px;
border-top-left-radius: 0;
border-top-right-radius: 0;
}
.panel {
margin-top: 20px;
}
.alert {
margin-top: 20px;
}
@import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700);
body {
font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
font-weight: 300;
color: #ffffff;
background: #bc2131;
}
h1,
h2,
h3,
h4,
h5,
h6 {
font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
font-weight: 300;
}
p {
font-weight: 300;
}
.font-normal {
font-weight: 400;
}
.font-semi-bold {
font-weight: 600;
}
.font-bold {
font-weight: 700;
}
.starter-template {
margin-top: 250px;
}
.starter-template .content {
margin-left: 10px;
}
.starter-template .content h1 {
margin-top: 10px;
font-size: 60px;
}
.starter-template .content h1 .smaller {
font-size: 40px;
color: #f2b7bd;
}
.starter-template .content .lead {
font-size: 25px;
color: #f2b7bd;
}
.starter-template .content .lead .font-normal {
color: #ffffff;
}
.starter-template .links {
float: right;
right: 0;
margin-top: 125px;
}
.starter-template .links ul {
display: block;
padding: 0;
margin: 0;
}
.starter-template .links ul li {
list-style: none;
display: inline;
margin: 0 10px;
}
.starter-template .links ul li:first-child {
margin-left: 0;
}
.starter-template .links ul li:last-child {
margin-right: 0;
}
.starter-template .links ul li.current-version {
color: #f2b7bd;
font-weight: 400;
}
.starter-template .links ul li a, a {
color: #f2b7bd;
text-decoration: underline;
}
.starter-template .links ul li a:hover, a:hover {
color: #ffffff;
text-decoration: underline;
}
.starter-template .links ul li .icon-muted {
color: #eb8b95;
margin-right: 5px;
}
.starter-template .links ul li:hover .icon-muted {
color: #ffffff;
}
.starter-template .copyright {
margin-top: 10px;
font-size: 0.9em;
color: #f2b7bd;
text-transform: lowercase;
float: right;
right: 0;
}
@media (max-width: 1199px) {
.starter-template .content h1 {
font-size: 45px;
}
.starter-template .content h1 .smaller {
font-size: 30px;
}
.starter-template .content .lead {
font-size: 20px;
}
}
@media (max-width: 991px) {
.starter-template {
margin-top: 0;
}
.starter-template .logo {
margin: 40px auto;
}
.starter-template .content {
margin-left: 0;
text-align: center;
}
.starter-template .content h1 {
margin-bottom: 20px;
}
.starter-template .links {
float: none;
text-align: center;
margin-top: 60px;
}
.starter-template .copyright {
float: none;
text-align: center;
}
}
@media (max-width: 767px) {
.starter-template .content h1 .smaller {
font-size: 25px;
display: block;
}
.starter-template .content .lead {
font-size: 16px;
}
.starter-template .links {
margin-top: 40px;
}
.starter-template .links ul li {
display: block;
margin: 0;
}
.starter-template .links ul li .icon-muted {
display: none;
}
.starter-template .copyright {
margin-top: 20px;
}
}
# items = request.POST.items()
def to_dict(items):
d = dict()
values = None
for item in items:
print(item)
key, value = item
if key == '__start__':
fieldname = value
values = []
elif key == '__end__':
d[fieldname] = values
values = None
elif isinstance(values, list):
values.append(value)
else:
d[key] = value
return d
import sys
from datetime import (
date,
datetime,
)
from .waktu import (
dmy,
dmyhms,
)
def to_str(v):
if isinstance(v, date):
if isinstance(v, datetime):
return dmyhms(v)
return dmy(v)
if v == 0:
return '0'
if isinstance(v, str):
return v.strip()
elif isinstance(v, bool):
return v and '1' or '0'
return v and str(v) or ''
def dict_to_str(d):
r = {}
for key in d:
val = d[key]
r[key] = to_str(val)
return r
from pyramid.threadlocal import get_current_registry
from pyramid.events import (
subscriber,
BeforeRender,
)
from pyramid.i18n import default_locale_negotiator
LOCALE_NAMES = {
'en': 'English',
'id': 'Indonesia',
}
LOCALE_IDS = list(LOCALE_NAMES.keys())
def get_settings():
return get_current_registry().settings
def get_locale_should_be(request):
if 'HTTP_ACCEPT_LANGUAGE' not in request.environ:
return 'id'
# id-ID,id;q=0.9,en-US;q=0.8,en;q=0.7
vals = request.environ['HTTP_ACCEPT_LANGUAGE'].split(',')
for val in vals:
for locale_id in LOCALE_IDS:
if val.find(locale_id) == 0:
return locale_id
def get_locale_name(request):
return default_locale_negotiator(request) or \
get_locale_should_be(request)
def get_locale_title(name):
if name in LOCALE_NAMES:
return LOCALE_NAMES[name]
return name
def get_locale_title_(request):
name = get_locale_name(request)
return get_locale_title(name)
@subscriber(BeforeRender)
def add_global(event):
event['locale_name'] = get_locale_title_
event['locale_should_be'] = get_locale_should_be
import calendar
from datetime import (
date,
datetime,
)
import pytz
from .this_framework import get_settings
def get_timezone():
settings = get_settings()
return pytz.timezone(settings['timezone'])
def create_datetime(
year, month, day, hour=0, minute=7, second=0, microsecond=0):
tz = get_timezone()
return datetime(
year, month, day, hour, minute, second, microsecond, tzinfo=tz)
def create_date(year, month, day):
return create_datetime(year, month, day)
def as_timezone(tz_date):
localtz = get_timezone()
if not tz_date.tzinfo:
tz_date = create_datetime(tz_date.year, tz_date.month, tz_date.day,
tz_date.hour, tz_date.minute, tz_date.second,
tz_date.microsecond)
return tz_date.astimezone(localtz)
def create_now():
tz = get_timezone()
return datetime.now(tz)
def date_from_str(value):
separator = None
value = value.split()[0] # dd-mm-yyyy HH:MM:SS
for s in ['-', '/']:
if value.find(s) > -1:
separator = s
break
if separator:
t = map(lambda x: int(x), value.split(separator))
y, m, d = t[2], t[1], t[0]
if d > 999: # yyyy-mm-dd
y, d = d, y
else:
y, m, d = int(value[:4]), int(value[4:6]), int(value[6:])
return date(y, m, d)
def split_time(s):
t = s.split(':') # HH:MM:SS
hour = int(t[0])
minutes = seconds = 0
if t[1:]:
minutes = int(t[1])
if t[2:]:
seconds = int(t[2])
return hour, minutes, seconds
# dd-mm-yyyy
# yyyy-mm-dd
# dd/mm/yyyy
# yyyy/mm/dd
# yyyymmdd
def split_date(s):
separator = None
for sep in ['-', '/']:
if s.find(sep) > -1:
separator = sep
break
if separator:
d, m, y = [int(x) for x in s.split(separator)]
if d > 999: # yyyy-mm-dd
y, d = d, y
else:
y, m, d = int(value[:4]), int(value[4:6]), int(value[6:])
return y, m, d
def split_datetime(s):
t = s.split() # dd-mm-yyyy HH:MM:SS
year, month, day = split_date(t[0])
if t[1:]:
hours, minutes, seconds = split_time(t[1])
else:
hours = minutes = seconds = 0
return year, month, day, hours, minutes, seconds
def date_from_str(s):
y, m, d, hh, mm, ss = split_datetime(s)
return date(y, m, d)
def datetime_from_str(s):
y, m, d, hh, mm, ss = split_datetime(s)
return create_datetime(y, m, d, hh, mm, ss)
def dmy(tgl):
return tgl.strftime('%d-%m-%Y')
def dmyhms(t):
return t.strftime('%d-%m-%Y %H:%M:%S')
def next_month(year, month):
if month == 12:
month = 1
year += 1
else:
month += 1
return year, month
def best_date(year, month, day):
try:
return date(year, month, day)
except ValueError:
last_day = calendar.monthrange(year, month)[1]
return date(year, month, last_day)
def next_month_day(year, month, day):
year, month = next_month(year, month)
return best_date(year, month, day)
from pyramid.view import view_config
from pyramid.httpexceptions import (
default_exceptionresponse_view,
HTTPFound,
)
from pyramid.interfaces import IRoutesMapper
from pyramid.response import Response
from pyramid.i18n import TranslationStringFactory
_ = TranslationStringFactory('home')
# http://stackoverflow.com/questions/9845669/pyramid-inverse-to-add-notfound-viewappend-slash-true
class RemoveSlashNotFoundViewFactory:
def __init__(self, notfound_view=None):
if notfound_view is None:
notfound_view = default_exceptionresponse_view
self.notfound_view = notfound_view
def __call__(self, context, request):
if not isinstance(context, Exception):
# backwards compat for an append_notslash_view registered via
# config.set_notfound_view instead of as a proper exception view
context = getattr(request, 'exception', None) or context
path = request.path
registry = request.registry
mapper = registry.queryUtility(IRoutesMapper)
if mapper is not None and path.endswith('/'):
noslash_path = path.rstrip('/')
for route in mapper.get_routes():
if route.match(noslash_path) is not None:
qs = request.query_string
if qs:
noslash_path += '?' + qs
return HTTPFound(location=noslash_path)
return self.notfound_view(context, request)
@view_config(route_name='home', renderer='templates/home.pt')
def my_view(request):
if '_LOCALE_' in request.GET:
resp = Response()
resp.set_cookie('_LOCALE_', request.GET['_LOCALE_'], 31536000)
return HTTPFound(
location=request.route_url('home'), headers=resp.headers)
return dict(project='ISO8583 Web', title=_('Home'))
You have registered on our site. Please click the link below to change the
password.
${url}
This link will expire in ${minutes} minutes.
If you did not request this, please ignore it.
from pyramid.view import view_config
from pyramid.httpexceptions import HTTPFound
from pyramid.i18n import TranslationStringFactory
import colander
from deform import (
Form,
Button,
ValidationFailure,
)
from deform.widget import (
TextAreaWidget,
HiddenWidget,
CheckboxChoiceWidget,
)
from ..models.ziggurat import (
Group,
GroupPermission,
)
_ = TranslationStringFactory('user')
########
# List #
########
@view_config(
route_name='group', renderer='templates/group/list.pt',
permission='user-edit')
def view_list(request):
q = request.dbsession.query(Group).order_by(Group.group_name)
return dict(groups=q, title=_('Groups'))
#######
# Add #
#######
def clean_name(s):
s = s.strip()
while s.find(' ') > -1:
s = s.replace(' ', ' ')
return s
class GroupNameValidator:
def __init__(self, kw):
self.group = kw['group']
self.db_session = kw['request'].dbsession
def __call__(self, node, value):
group_name = clean_name(value)
if self.group and self.group.group_name.lower() == group_name.lower():
return
q = self.db_session.query(Group).filter(
Group.group_name.ilike(group_name))
found = q.first()
if not found:
return
data = dict(group_name=group_name, gid=found.id)
ts = _(
'group-name-already-used',
default='Group name ${group_name} already used by ID ${gid}',
mapping=data)
raise colander.Invalid(node, ts)
@colander.deferred
def deferred_group_name_validator(node, kw):
return GroupNameValidator(kw)
@colander.deferred
def deferred_permissions(node, kw):
values = kw.get('permission_list', [])
return CheckboxChoiceWidget(values=values)
class AddSchema(colander.Schema):
group_name = colander.SchemaNode(
colander.String(), title=_('Group name'),
validator=deferred_group_name_validator)
description = colander.SchemaNode(
colander.String(),
missing=colander.drop,
widget=TextAreaWidget(rows=5),
title=_('Description'))
permissions = colander.SchemaNode(
colander.Set(), widget=deferred_permissions,
title=_('Permissions'))
class EditSchema(AddSchema):
id = colander.SchemaNode(
colander.String(), widget=HiddenWidget(readonly=True))
PERMISSIONS = [
('edit-user', _('User management'))
]
def get_form(request, class_form, group=None):
schema = class_form()
schema = schema.bind(
request=request, permission_list=PERMISSIONS, group=group)
btn_save = Button('save', _('Save'))
btn_cancel = Button('cancel', _('Cance'))
buttons = (btn_save, btn_cancel)
return Form(schema, buttons=buttons)
def insert(db_session, values):
group = Group()
group.group_name = clean_name(values['group_name'])
group.description = values['description']
db_session.add(group)
db_session.flush()
for perm_name in values['permissions']:
gp = GroupPermission()
gp.group_id = group.id
gp.perm_name = perm_name
db_session.add(gp)
return group
@view_config(
route_name='group-add', renderer='templates/group/add.pt',
permission='user-edit')
def view_add(request):
form = get_form(request, AddSchema)
resp = dict(title=_('Add goup'))
if not request.POST:
resp['form'] = form.render()
return resp
if 'save' not in request.POST:
return HTTPFound(location=request.route_url('group'))
items = request.POST.items()
try:
c = form.validate(items)
except ValidationFailure:
resp['form'] = form.render()
return resp
group = insert(request.dbsession, dict(c.items()))
data = dict(group_name=group.group_name)
ts = _(
'group-added',
default='${group_name} group has been added.',
mapping=data)
request.session.flash(ts)
return HTTPFound(location=request.route_url('group'))
########
# Edit #
########
def group_permission_set(db_session, group):
q = db_session.query(GroupPermission).filter_by(group_id=group.id)
r = []
for gp in q:
r.append(gp.perm_name)
return set(r)
def update(db_session, group, values):
group.group_name = clean_name(values['group_name'])
group.description = values['description']
db_session.add(group)
existing = group_permission_set(db_session, group)
unused = existing - values['permissions']
if unused:
q = db_session.query(GroupPermission).filter_by(group_id=group.id).\
filter(GroupPermission.perm_name.in_(unused))
q.delete(synchronize_session=False)
new = values['permissions'] - existing
for perm_name in new:
gp = GroupPermission()
gp.group_id = group.id
gp.perm_name = perm_name
db_session.add(gp)
@view_config(
route_name='group-edit', renderer='templates/group/edit.pt',
permission='user-edit')
def view_edit(request):
q = request.dbsession.query(Group).filter_by(id=request.matchdict['id'])
group = q.first()
if not group:
return HTTPNotFound()
form = get_form(request, EditSchema, group)
resp = dict(title=_('Edit group'))
if not request.POST:
d = group.to_dict_without_none()
d['permissions'] = group_permission_set(request.dbsession, group)
resp['form'] = form.render(appstruct=d)
return resp
if 'save' not in request.POST:
return HTTPFound(location=request.route_url('group'))
resp['form'] = form.render()
items = request.POST.items()
try:
c = form.validate(items)
except ValidationFailure:
resp['form'] = form.render()
return resp
update(request.dbsession, group, dict(c.items()))
data = dict(group_name=group.group_name)
ts = _(
'group-updated', default='${group_name} group profile updated',
mapping=data)
request.session.flash(ts)
return HTTPFound(location=request.route_url('group'))
##########
# Delete #
##########
@view_config(
route_name='group-delete', renderer='templates/group/delete.pt',
permission='user-edit')
def view_delete(request):
q = request.dbsession.query(Group).filter_by(id=request.matchdict['id'])
group = q.first()
if not group:
return HTTPNotFound()
if not request.POST:
btn_delete = Button('delete', _('Delete'))
btn_cancel = Button('cancel', _('Cancel'))
buttons = (btn_delete, btn_cancel)
form = Form(colander.Schema(), buttons=buttons)
return dict(title=_('Delete group'), form=form.render(), group=group)
if 'delete' not in request.POST:
return HTTPFound(location=request.route_url('group'))
data = dict(group_name=group.group_name)
ts = _(
'group-deleted',
default='{group_name} group has been deleted.',
mapping=data)
q.delete()
request.session.flash(ts)
return HTTPFound(location=request.route_url('group'))
import os
from datetime import (
datetime,
timedelta,
)
from urllib.parse import urlparse
from pyramid.response import Response
from pyramid.view import view_config
from pyramid.httpexceptions import (
HTTPFound,
HTTPForbidden,
)
from pyramid.security import (
remember,
forget,
)
from pyramid.i18n import TranslationStringFactory
import colander
from deform import (
Form,
ValidationFailure,
widget,
Button,
)
from ziggurat_foundations.models.services.user import UserService
from pyramid_mailer.message import Message
from ..tools.waktu import create_now
from ..tools.this_framework import get_settings
from ..models.ziggurat import User
_ = TranslationStringFactory('login')
class Login(colander.Schema):
username = colander.SchemaNode(colander.String(), title=_('Username'))
password = colander.SchemaNode(
colander.String(), widget=widget.PasswordWidget(),
title=_('Password'),
description=_('Forgot password'))
# http://deformdemo.repoze.org/interfield/
def login_validator(form, value):
user = form.user
if not user or \
not user.status or \
not user.user_password or \
not UserService.check_password(user, value['password']):
raise colander.Invalid(form, _('Login failed'))
def login_ok(request, user, route='log'):
headers = remember(request, user.id)
user.last_login_date = create_now()
request.dbsession.add(user)
return HTTPFound(location=request.route_url(route), headers=headers)
def get_user_by_identity(request):
identity = request.POST.get('username')
if identity.find('@') > -1:
q = request.dbsession.query(User).filter_by(email=identity)
else:
q = request.dbsession.query(User).filter_by(user_name=identity)
return q.first()
one_hour = timedelta(1/24)
two_minutes = timedelta(1/24/60)
def login_by_code(request):
q = request.dbsession.query(User).filter_by(
security_code=request.GET['code'])
user = q.first()
if not user or \
create_now() - user.security_code_date > one_hour:
return HTTPFound(location=request.route_url('login-by-code-failed'))
user.security_code = None
request.dbsession.add(user)
request.dbsession.flush()
return login_ok(request, user, 'change-password')
def login_default_response():
return dict(title=_('Login'), label_forgot_password=_('Forgot password'))
@view_config(route_name='login', renderer='templates/login.pt')
def view_login(request):
if request.authenticated_userid():
return HTTPFound(location=request.route_url('home'))
if '_LOCALE_' in request.GET:
resp = Response()
resp.set_cookie('_LOCALE_', request.GET['_LOCALE_'], 31536000)
return HTTPFound(
location=request.route_url('login'), headers=resp.headers)
if 'code' in request.GET:
return login_by_code(request)
resp = login_default_response()
schema = Login(validator=login_validator)
btn_submit = Button('submit', _('Submit'))
form = Form(schema, buttons=(btn_submit,))
if 'submit' not in request.POST:
resp['form'] = form.render()
return resp
controls = request.POST.items()
schema.user = user = get_user_by_identity(request)
try:
c = form.validate(controls)
except ValidationFailure:
resp['form'] = form.render()
return resp
return login_ok(request, user)
@view_config(route_name='logout')
def view_logout(request):
headers = forget(request)
return HTTPFound(location=request.route_url('home'), headers=headers)
@view_config(
route_name='login-by-code-failed',
renderer='templates/login-by-code-failed.pt')
def view_login_by_code_failed(request):
return dict(title='Login by code failed')
###################
# Change password #
###################
class ChangePassword(colander.Schema):
new_password = colander.SchemaNode(
colander.String(), title=_('New password'),
widget=widget.PasswordWidget())
retype_password = colander.SchemaNode(
colander.String(), title=_('Retype new password'),
widget=widget.PasswordWidget())
def password_validator(form, value):
if value['new_password'] != value['retype_password']:
raise colander.Invalid(form, _('Retype mismatch'))
@view_config(
route_name='change-password', renderer='templates/change-password.pt',
permission='view')
def view_change_password(request):
schema = ChangePassword(validator=password_validator)
btn_submit = Button('save', _('Save'))
btn_cancel = Button('cancel', _('Cancel'))
form = Form(schema, buttons=(btn_submit, btn_cancel))
resp = dict(title=_('Change password'))
if not request.POST:
resp['form'] = form.render()
return resp
if 'save' not in request.POST:
return HTTPFound(location=request.route_url('home'))
schema.request = request
controls = request.POST.items()
try:
c = form.validate(controls)
except ValidationFailure as e:
resp['form'] = form.render()
return resp
UserService.set_password(request.user, c['new_password'])
request.dbsession.add(request.user)
return HTTPFound(location=request.route_url('change-password-done'))
@view_config(
route_name='change-password-done',
renderer='templates/change-password-done.pt', permission='view')
def view_change_password_done(request):
return dict(title=_('Change password'))
##################
# Reset password #
##################
class ResetPassword(colander.Schema):
email = colander.SchemaNode(
colander.String(), title=_('Email'),
description=_(
'email-reset-password',
default='Enter your email address and we will send you '
'a link to reset your password.')
)
def reset_password_validator(form, value):
user = form.user
if not user or not user.status:
raise colander.Invalid(form, _('Invalid email'))
def security_code_age(user):
return create_now() - user.security_code_date
def send_email_security_code(
request, user, time_remain, subject, body_msg_id, body_default_file):
settings = get_settings()
up = urlparse(request.url)
url = '{}://{}/login?code={}'.format(
up.scheme, up.netloc, user.security_code)
minutes = int(time_remain.seconds / 60)
data = dict(url=url, minutes=minutes)
here = os.path.abspath(os.path.dirname(__file__))
body_file = os.path.join(here, body_default_file)
with open(body_file) as f:
body_tpl = f.read()
body = _(body_msg_id, default=body_tpl, mapping=data)
body = request.localizer.translate(body)
sender = '{} <{}>'.format(
settings['mail.sender_name'], settings['mail.username'])
subject = request.localizer.translate(_(subject))
message = Message(
subject=subject, sender=sender, recipients=[user.email], body=body)
mailer = request.registry['mailer']
mailer.send(message)
def regenerate_security_code(db_session, user):
age = security_code_age(user)
remain = one_hour - age
if user.security_code and age < one_hour and remain > two_minutes:
return remain
UserService.regenerate_security_code(user)
user.security_code_date = create_now()
db_session.add(user)
return one_hour
@view_config(
route_name='reset-password', renderer='templates/reset-password.pt')
def view_reset_password(request):
if request.authenticated_userid():
return HTTPFound(location=request.route_url('home'))
resp = dict(title=_('Reset password'))
schema = ResetPassword(validator=reset_password_validator)
btn_submit = Button('submit', _('Send password reset email'))
form = Form(schema, buttons=(btn_submit,))
if 'submit' in request.POST:
controls = request.POST.items()
identity = request.POST.get('email')
q = request.dbsession.query(User).filter_by(email=identity)
schema.user = user = q.first()
try:
c = form.validate(controls)
except ValidationFailure:
resp['form'] = form.render()
return resp
remain = regenerate_security_code(request.dbsession, user)
send_email_security_code(
request, user, remain, 'Reset password', 'reset-password-body',
'reset-password-body.tpl')
return HTTPFound(location=request.route_url('reset-password-sent'))
resp['form'] = form.render()
return resp
@view_config(
route_name='reset-password-sent',
renderer='templates/reset-password-sent.pt')
def view_reset_password_sent(request):
return dict(title=_('Reset password'))
We accepted password recovery requests. Please click the following link:
${url}
This link will expire in ${minutes} minutes.
If you did not request this, please ignore it.
<div metal:use-macro="load: layout.pt">
<div metal:fill-slot="content" i18n:domain="login">
<p i18n:translate="change-password-done">
Your password has been changed
</p>
</div>
</div>
<div metal:use-macro="load: layout-form.pt">
<div metal:fill-slot="content">
<div tal:content="structure form" />
</div>
</div>
<div metal:use-macro="load: ../layout-menu.pt">
<div metal:fill-slot="content" i18n:domain="user">
<h1 i18n:translate="Add user group">Add user group</h1>
<div tal:content="structure form"/>
</div>
</div>
<div metal:use-macro="load: ../layout-menu.pt">
<div metal:fill-slot="content" i18n:domain="user">
<div class="panel panel-danger">
<div class="panel-heading">
<h3 class="panel-title" i18n:translate="Warning">Warning</h3>
</div>
<div class="panel-body" i18n:translate="warning-delete-group">
Delete
<span tal:replace="group.group_name" i18n:name="group_name"/>
group that has
<span tal:replace="group.member_count" i18n:name="member_count"/>
members ?
</div>
</div>
<div tal:content="structure form"/>
</div>
</div>
<div metal:use-macro="load: ../layout-menu.pt">
<div metal:fill-slot="content" i18n:domain="user">
<h1 i18n:translate="Edit group">Edit group</h1>
<div tal:content="structure form"/>
</div>
</div>
<div metal:use-macro="load: ../layout-menu.pt">
<div metal:fill-slot="content" i18n:domain="user">
<h1 i18n:translate="">Groups</h1>
<table class="table table-striped table-hover">
<thead>
<tr>
<th i18n:translate="">Group name</th>
<th i18n:translate="">Description</th>
<th i18n:translate="">Member count</th>
<th colspan="2"/>
</tr>
</thead>
<tbody>
<tr tal:repeat="group groups">
<td i18n:translate="" tal:content="group.group_name"/>
<td i18n:translate="" tal:content="group.description"/>
<td tal:content="group.member_count" align="center"/>
<td><a href="/group/${group.id}" i18n:translate="">Edit</a></td>
<td><a href="/group/${group.id}/delete" i18n:translate="">Delete</a></td>
</tr>
</tbody>
</table>
</div>
</div>
<div metal:use-macro="load: layout.pt">
<div metal:fill-slot="content" i18n:domain="home">
<div class="content">
<h1><span class="font-semi-bold">Pyramid</span> <span class="smaller">Starter project</span></h1>
<p i18n:translate="welcome" class="lead">Welcome to <span
class="font-normal">Web Starter</span>, a&nbsp;Pyramid
application generated&nbsp;by<br><span class="font-normal">Cookiecutter</span>.</p>
<p tal:condition="not request.user">
<a href="/login" i18n:translate="login-link">Login please</a>
</p>
<p i18n:translate="you-are-logged-in" tal:condition="request.user">You are logged in as
<span tal:replace="request.user.user_name" i18n:name="username" />. Click <a href="/logout">here</a> to
logout.</p>
</div>
</div>
</div>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="">
<meta name="author" content="">
<link rel="shortcut icon" href="/static/pyramid-16x16.png">
<title tal:content="title"/>
<!-- Bootstrap core CSS -->
<link href="/deform_static/css/bootstrap.min.css" rel="stylesheet"/>
<link href="/deform_static/css/form.css" rel="stylesheet"/>
<!-- Custom styles for this template -->
<link href="/static/signin.css" rel="stylesheet">
</head>
<body>
<div class="container">
<div tal:condition="request.session.peek_flash()">
<div class="alert alert-success" tal:repeat="message request.session.pop_flash()">
${message}
</div>
</div>
<div tal:condition="request.session.peek_flash('error')">
<div class="alert alert-danger" tal:repeat="message request.session.pop_flash('error')">
${message}
</div>
</div>
<div metal:define-slot="content"/>
</div> <!-- /container -->
<!-- Bootstrap core JavaScript
================================================== -->
<!-- Placed at the end of the document so the pages load faster -->
<script type="text/javascript" src="/deform_static/scripts/jquery-2.0.3.min.js"></script>
<script type="text/javascript" src="/deform_static/scripts/bootstrap.min.js"></script>
<script type="text/javascript" src="/deform_static/scripts/deform.js"></script>
<div metal:define-slot="content-script"/>
</body>
</html>
<!DOCTYPE html>
<html lang="en" i18n:domain="menu">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="">
<meta name="author" content="">
<link rel="shortcut icon" href="/static/pyramid-16x16.png">
<title tal:content="title"/>
<!-- Bootstrap core CSS -->
<link href="/deform_static/css/bootstrap.min.css" rel="stylesheet"/>
<link href="/deform_static/css/form.css" rel="stylesheet"/>
<!-- Custom styles for this template -->
<link href="/static/signin.css" rel="stylesheet">
<div metal:define-slot="head"/>
</head>
<body>
<!-- Fixed navbar -->
<div class="navbar navbar-default navbar-fixed-top" role="navigation"
i18n:domain="menu">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#"></a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li tal:attributes="class request.path == '/' and 'active'">
<a href="/" i18n:translate="Home">Home</a></li>
<li class="dropdown"
tal:attributes="class request.matched_route.name in ['user',
'user-add', 'user-edit', 'user-delete', 'group', 'group-edit',
'group-delete'] and 'active'">
<a href="#" class="dropdown-toggle" data-toggle="dropdown"
i18n:translate="Admin">Admin<b class="caret"></b></a>
<ul class="dropdown-menu">
<li><a href="/user" i18n:translate="Users">Users</a></li>
<li><a href="/user/add" i18n:translate="Add user">Add user</a></li>
<li><a href="/group" i18n:translate="Users groups">Users groups</a></li>
<li><a href="/group/add" i18n:translate="Add users group">Add users group</a></li>
</ul>
</li>
<li class="dropdown" tal:attributes="class request.path in
['/change-password'] and 'active'">
<a href="#" class="dropdown-toggle" data-toggle="dropdown"
i18n:translate="My account">My account <b class="caret"></b></a>
<ul class="dropdown-menu">
<li><a href="/change-password" i18n:translate="Change password">Change password</a></li>
<li><a href="/logout" i18n:translate="username-logout">
<span tal:replace="request.user.user_name" i18n:name="username"/> logout
</a>
</li>
</ul>
</li>
</ul>
</div><!--/.nav-collapse -->
</div>
</div>
<div class="container">
<div tal:condition="request.session.peek_flash()">
<div class="alert alert-success" tal:repeat="message request.session.pop_flash()">
${message}
</div>
</div>
<div tal:condition="request.session.peek_flash('error')">
<div class="alert alert-danger" tal:repeat="message request.session.pop_flash('error')">
${message}
</div>
</div>
<div metal:define-slot="content"/>
</div> <!-- /container -->
<!-- Bootstrap core JavaScript
================================================== -->
<!-- Placed at the end of the document so the pages load faster -->
<script type="text/javascript" src="/deform_static/scripts/jquery-2.0.3.min.js"></script>
<script type="text/javascript" src="/deform_static/scripts/bootstrap.min.js"></script>
<script type="text/javascript" src="/deform_static/scripts/deform.js"></script>
<div metal:define-slot="content-script"/>
</body>
</html>
<!DOCTYPE html metal:define-macro="layout">
<html lang="${request.locale_name}"
xmlns:i18n="http://xml.zope.org/namespaces/i18n"
i18n:domain="home">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="pyramid web application">
<meta name="author" content="Pylons Project">
<link rel="shortcut icon" href="/static/pyramid-16x16.png">
<title>Web Starter - <span tal:replace="title"/></title>
<!-- Bootstrap core CSS -->
<link href="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet">
<!-- Custom styles for this scaffold -->
<link href="/static/theme.css" rel="stylesheet">
<!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries -->
<!--[if lt IE 9]>
<script src="//oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js" integrity="sha384-0s5Pv64cNZJieYFkXYOTId2HMA2Lfb6q2nAcx2n0RTLUnCAoTTsS0nKEO27XyKcY" crossorigin="anonymous"></script>
<script src="//oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js" integrity="sha384-f1r2UzjsxZ9T4V1f2zBO/evUqSEOpeaUUZcMTz1Up63bl4ruYnFYeM+BxI4NhyI0" crossorigin="anonymous"></script>
<![endif]-->
</head>
<body>
<div class="starter-template">
<div class="container">
<div class="row">
<div class="col-md-2">
<img class="logo img-responsive"
src="/static/pyramid.png" alt="pyramid web framework">
</div>
<div class="col-md-10">
<div metal:define-slot="content">No content</div>
</div>
</div>
<div class="row">
<div class="links">
<ul>
<li><i class="glyphicon glyphicon-cog icon-muted"></i><a href="https://github.com/Pylons/pyramid">Github Project</a></li>
<li><i class="glyphicon glyphicon-globe icon-muted"></i><a href="https://webchat.freenode.net/?channels=pyramid">IRC Channel</a></li>
<li><i class="glyphicon glyphicon-home icon-muted"></i><a href="https://pylonsproject.org">Pylons Project</a></li>
</ul>
</div>
</div>
<div class="row">
<div class="copyright" i18n:translate="copyright">
Copyright &copy; Pylons Project
</div>
</div>
</div>
</div>
<!-- Bootstrap core JavaScript
================================================== -->
<!-- Placed at the end of the document so the pages load faster -->
<script src="//oss.maxcdn.com/libs/jquery/1.10.2/jquery.min.js" integrity="sha384-aBL3Lzi6c9LNDGvpHkZrrm3ZVsIwohDD7CDozL0pk8FwCrfmV7H9w8j3L7ikEv6h" crossorigin="anonymous"></script>
<script src="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/js/bootstrap.min.js" integrity="sha384-s1ITto93iSMDxlp/79qhWHi+LsIi9Gx6yL+cOKDuymvihkfol83TYbLbOw+W/wv4" crossorigin="anonymous"></script>
</body>
</html>
<form
tal:define="style style|field.widget.style;
css_class css_class|string:${field.widget.css_class or field.css_class or ''};
item_template item_template|field.widget.item_template;
autocomplete autocomplete|field.autocomplete;
title title|field.title;
errormsg errormsg|field.errormsg;
description description|field.description;
buttons buttons|field.buttons;
use_ajax use_ajax|field.use_ajax;
ajax_options ajax_options|field.ajax_options;
formid formid|field.formid;
action action|field.action or None;
method method|field.method;"
tal:attributes="autocomplete autocomplete;
style style;
class css_class;
action action;"
id="${formid}"
method="${method}"
enctype="multipart/form-data"
accept-charset="utf-8"
i18n:domain="deform"
>
<fieldset class="deform-form-fieldset">
<legend tal:condition="title">${title}</legend>
<input type="hidden" name="_charset_" />
<input type="hidden" name="__formid__" value="${formid}"/>
<div class="alert alert-danger" tal:condition="field.error">
<div class="error-msg-lbl" i18n:translate=""
>There was a problem with your submission</div>
<div class="error-msg-detail" i18n:translate=""
>Errors have been highlighted below</div>
<p class="error-msg">${field.errormsg}</p>
</div>
<p class="section first" tal:condition="description">
${description}
</p>
<div class="grid-container">
<div tal:repeat="child field"
tal:replace="structure child.render_template(item_template)"/>
</div>
<div class="form-group deform-form-buttons">
<tal:loop tal:repeat="button buttons">
<button
tal:define="btn_disposition repeat.button.start and 'btn-primary' or 'btn-default';"
tal:attributes="disabled button.disabled if button.disabled else None"
id="${formid+button.name}"
name="${button.name}"
type="${button.type}"
class="btn ${button.css_class or btn_disposition}"
value="${button.value}"
tal:condition="button.type != 'link'">
<span tal:condition="button.icon" class="glyphicon glyphicon-${button.icon}"></span>
${button.title}
</button>
<a
tal:define="btn_disposition repeat.button.start and 'btn-primary' or 'btn-default';
btn_href button.value|''"
class="btn ${button.css_class or btn_disposition}"
id="${field.formid + button.name}"
href="${btn_href}"
tal:condition="button.type == 'link'">
<span tal:condition="button.icon" class="glyphicon glyphicon-${button.icon}"></span>
${button.title}
</a>
</tal:loop>
</div>
</fieldset>
<script type="text/javascript" tal:condition="use_ajax">
deform.addCallback(
'${formid}',
function(oid) {
var target = '#' + oid;
var options = {
target: target,
replaceTarget: true,
success: function() {
deform.processCallbacks();
deform.focusFirstInput(target);
},
beforeSerialize: function() {
// See http://bit.ly/1agBs9Z (hack to fix tinymce-related ajax bug)
if ('tinymce' in window) {
$(tinymce.get()).each(
function(i, el) {
var content = el.getContent();
var editor_input = document.getElementById(el.id);
editor_input.value = content;
});
}
}
};
var extra_options = ${ajax_options} || {};
$('#' + oid).ajaxForm($.extend(options, extra_options));
}
);
</script>
</form>
<div metal:use-macro="load: ../layout-menu.pt">
<div metal:fill-slot="head">
<link href="/static/grid.css" rel="stylesheet"/>
</div>
<div metal:fill-slot="content">
<h1>
Log File
</h1>
<div tal:content="structure form"/>
<div tal:condition="request.GET.get('lihat')">
<div tal:condition="not count" class="alert alert-danger">
Tidak ada hasil.
</div>
<div tal:condition="count">
<div class="alert alert-success">
Ada ${count} hasil.
</div>
<table class="table table-striped table-hover">
<thead>
<tr>
<th>
ID
</th>
<th>
Keterangan
</th>
</tr>
</thead>
<tbody>
<tr tal:repeat="row rows">
<td>
${row.id}
</td>
<td tal:condition="row.line.find('\n') < 0">
${row.line}
</td>
<td tal:condition="row.line.find('\n') > -1">
<pre class="errormsg">${row.line}</pre>
</td>
</tr>
</tbody>
</table>
<center tal:content="structure rows.pager(format='$link_previous ~4~ $link_next')"/>
</div><!-- condition count -->
</div><!-- condition lihat -->
</div>
</div>
<div metal:use-macro="load: layout.pt">
<div metal:fill-slot="content" i18n:domain="login">
<p i18n:translate="Invalid security code">
Invalid security code.
</p>
</div>
</div>
<div metal:use-macro="load: layout-form.pt">
<div metal:fill-slot="content">
<div tal:condition="form" tal:content="structure form"/>
<p tal:condition="request.locale_name != 'en'">
<a href="?_LOCALE_=en">English</a>
</p>
<p tal:condition="request.locale_name != 'id'">
<a href="?_LOCALE_=id">Indonesia</a>
</p>
</div>
<div metal:fill-slot="content-script">
<script type="text/javascript">
$(window).on('load', function() {
$("p.help-block").html('<a href="/reset-password">${label_forgot_password}</a>');
});
</script>
</div>
</div>
<div metal:use-macro="load: layout.pt">
<div metal:fill-slot="content" i18n:domain="login">
<p i18n:translate="reset-password-link-sent">
Check your email for a link to reset your password. If it
doesn’t appear within a few minutes, check your spam folder. The
reset password link has been sent. Please check your email.
</p>
</div>
</div>
<div metal:use-macro="load: layout-form.pt">
<div metal:fill-slot="content">
<div tal:content="structure form" />
</div>
</div>
<div metal:use-macro="load: ../layout-menu.pt">
<div metal:fill-slot="content" i18n:domain="user">
<h1 i18n:translate="Add user">Add User</h1>
<div tal:content="structure form"/>
</div>
</div>
<div metal:use-macro="load: ../layout-menu.pt">
<div metal:fill-slot="content" i18n:domain="user">
<div class="panel panel-danger">
<div class="panel-heading">
<h3 class="panel-title" i18n:translate="Warning">Warning</h3>
</div>
<div class="panel-body" i18n:translate="warning-delete-user">
Delete <span tal:replace="user.email" i18n:name="email"/>
ID <span tal:replace="user.id" i18n:name="uid"/> ?
</div>
</div>
<div tal:content="structure form"/>
</div>
</div>
<div metal:use-macro="load: ../layout-menu.pt">
<div metal:fill-slot="content" i18n:domain="user">
<h1 i18n:translate="Edit user">Edit user</h1>
<div tal:content="structure form"/>
</div>
</div>
<div metal:use-macro="load: ../layout-menu.pt">
<div metal:fill-slot="content" i18n:domain="user">
<h1 i18n:translate="">Users</h1>
<form method="post" action="/user">
<table class="table table-striped table-hover">
<thead>
<tr>
<th i18n:translate="">Group</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<select id="gid" name="gid" class="form-control">
<option/>
<option
i18n:translate=""
tal:repeat="group groups"
tal:attributes="value group.id; selected
str(group.id) == request.GET.get('gid')">
${group.group_name}
</option>
</select>
</td>
<td>
<input type="submit" name="submit" value="Show"
class="btn btn-primary"
i18n:attributes="value">
</td>
</tr>
</tbody>
</table>
</form>
<p tal:condition="not count" i18n:translate="">No result</p>
<p tal:condition="count" i18n:translate="user-result">
<span tal:replace="count" i18n:name="count"/> rows
</p>
<table class="table table-striped table-hover" tal:condition="count">
<thead>
<tr>
<th i18n:translate="">Email</th>
<th i18n:translate="">Username</th>
<th i18n:translate="">Status</th>
<th i18n:translate="">Last login</th>
<th i18n:translate="">Registered date</th>
<th colspan="2"/>
</tr>
</thead>
<tbody>
<tr tal:repeat="user users">
<td tal:content="user.email"/>
<td tal:content="user.user_name"/>
<td tal:condition="user.status" i18n:translate="">Active</td>
<td tal:condition="not user.status"/>
<td tal:condition="user.last_login_date"
tal:content="user.last_login_date.strftime('%d-%m-%Y
%H:%M:%S %z')"/>
<td tal:condition="not user.last_login_date"/>
<td tal:content="user.registered_date.strftime('%d-%m-%Y %H:%M:%S %z')"/>
<td>
<a href="/user/${user.id}" i18n:translate="">Edit</a>
</td>
<td tal:condition="user.id > 1 and user.id != request.user.id">
<a href="/user/${user.id}/delete" i18n:translate="">Delete</a>
</td>
<td tal:condition="user.id <= 1" i18n:translate="">System</td>
<td tal:condition="user.id > 1 and user.id == request.user.id" i18n:translate="">You</td>
</tr>
</tbody>
</table>
</div>
</div>
from logging import getLogger
from pyramid_rpc.jsonrpc import jsonrpc_method
from iso8583_web.common import BaseIsoView
from iso8583_web.iso8583.tools import iso_to_dict
ROUTE = 'rpc'
class TestView(BaseIsoView):
def get_iso_conf_name(self): # Override
# Sesuai file pyramid-bank.ini section [host_pemda]
return 'pemda'
@jsonrpc_method(endpoint=ROUTE)
def echo(self, p: dict):
log = getLogger()
self.validate()
ip = self.request.client_addr
log.info(f"Request from {ip} echo {p}")
conn = self.get_connection()
iso_req = conn.job.echo_request()
stan = iso_req.get_stan()
iso_resp = self.send_iso(conn, iso_req)
data = iso_to_dict(iso_resp)
data['stan'] = data.get('11', stan)
r = dict(code=0, message='OK', data=data)
log.info(f"Response to {ip} echo {r}")
return r
def includeme(config):
log = getLogger()
config.add_jsonrpc_endpoint(ROUTE, '/rpc')
log.info(f"JSON-RPC /rpc")
config.scan('.')
NAME="$1"
msgfmt -o web_starter/locale/id/LC_MESSAGES/$NAME.mo web_starter/locale/id/LC_MESSAGES/$NAME.po
[build-system]
requires = ['setuptools >= 64']
[project]
name = 'iso8583-web'
version = '0.7'
dependencies = [
'pyramid_rpc',
'waitress',
'opensipkd-iso8583 @ git+https://git.opensipkd.com/sugiana/opensipkd-iso8583.git@0.3',
'opensipkd-hitung @ git+https://git.opensipkd.com/sugiana/opensipkd-hitung.git',
]
requires-python = '>= 3.9'
authors = [
{name='Owo Sugiana', email='sugiana@gmail.com'},
]
description = 'Daemon ISO8583 yang memiliki web service'
readme = 'README.rst'
license = {text='PostgreSQL License'}
classifiers = [
'Programming Language :: Python :: 3',
'Operating System :: OS Independent',
]
[tool.setuptools.packages.find]
include = ['iso8583_web', 'iso8583_web_test']
[project.scripts]
iso8583 = 'iso8583_web.iso8583:main'
[project.entry-points."paste.app_factory"]
main = 'iso8583_web:main'
......@@ -5,22 +5,10 @@
[app:main]
use = egg:iso8583_web
pyramid.includes =
pyramid_rpc.jsonrpc
iso8583_web_test.view
pyramid.reload_templates = false
pyramid.debug_authorization = false
pyramid.debug_notfound = false
pyramid.debug_routematch = false
pyramid.default_locale_name = id
sqlalchemy.url = postgresql://user:pass@localhost/dbname
timezone = Asia/Jakarta
localization = id_ID.UTF-8
mail.host = localhost
mail.port = 25
mail.username = user@example.com
mail.password = FIXME
mail.sender_name = Example Name
###
# wsgi server configuration
......@@ -45,11 +33,11 @@ keys = console
keys = generic
[logger_root]
level = WARN
level = INFO
handlers = console
[logger_iso8583_web]
level = WARN
level = DEBUG
handlers =
qualname = iso8583_web
......@@ -60,4 +48,12 @@ level = NOTSET
formatter = generic
[formatter_generic]
format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s
format = %(asctime)s %(levelname)s %(message)s
[host_pemda]
ip = 127.0.0.1
port = 8592
listen = false
streamer = bjb_with_suffix
timeout = 60
module = opensipkd.iso8583.network
[pytest]
testpaths = iso8583_web
python_files = *.py
import os
import sys
import subprocess
from setuptools import setup, find_packages
here = os.path.abspath(os.path.dirname(__file__))
with open(os.path.join(here, 'README.rst')) as f:
README = f.read()
with open(os.path.join(here, 'CHANGES.txt')) as f:
CHANGES = f.read()
line = CHANGES.splitlines()[0]
version = line.split()[0]
requires = [
'plaster_pastedeploy',
'pyramid',
'pyramid_chameleon',
'pyramid_debugtoolbar',
'pyramid_retry',
'pyramid_tm',
'waitress',
'zope.sqlalchemy',
'transaction',
'psycopg2-binary',
'pytz',
'ziggurat-foundations',
'alembic',
'colander',
'deform',
'pyramid_beaker',
'pyramid_mailer',
'requests',
'pyramid_rpc',
'opensipkd-iso8583 @ '
'git+https://git.opensipkd.com/sugiana/opensipkd-iso8583.git',
'opensipkd-hitung @ '
'git+https://git.opensipkd.com/sugiana/opensipkd-hitung.git',
]
tests_require = [
'WebTest >= 1.3.1', # py3 compat
'pytest',
'pytest-cov',
]
setup(
name='iso8583_web',
version=version,
description='Daemon ISO8583 yang memiliki web service',
long_description=README + '\n\n' + CHANGES,
classifiers=[
'Programming Language :: Python',
'Framework :: Pyramid',
'Topic :: Internet :: WWW/HTTP',
'Topic :: Internet :: WWW/HTTP :: WSGI :: Application',
],
author='Owo Sugiana',
author_email='sugiana@gmail.com',
url='https://git.opensipkd.com/sugiana/iso8583-web',
keywords='web pyramid pylons',
packages=find_packages(),
include_package_data=True,
zip_safe=False,
extras_require={
'testing': tests_require,
},
install_requires=requires,
entry_points={
'paste.app_factory': [
'main = iso8583_web:main',
],
'console_scripts': [
'iso8583 = iso8583_web.iso8583:main',
'initialize_iso8583_web_db = '
'iso8583_web.scripts.initialize_db:main',
]
},
)
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!