Commit bf1c61fe by Owo Sugiana

Kali pertama

0 parents
Showing 98 changed files with 5453 additions and 0 deletions
0.1 2018-11-16
--------------
- Kali pertama.
include *.txt *.ini *.md
recursive-include web_imgw *.ico *.png *.css *.gif *.jpg *.pt *.txt *.js *.csv *.mo *.po *.pt
Web Instant Messaging Gateway
=============================
Ini adalah web untuk paket Debian bernama `im-gw`. Pastikan Anda telah
memasangnya sebelum menggunakan aplikasi ini, meski tidak harus berada di mesin
yang sama.
Masuklah ke direktorinya:
cd web-imgw
Buat Python virtual environment:
python3 -m venv ../env
Upgrade packaging tools:
../env/bin/pip install --upgrade pip setuptools
Pasang paket yang dibutuhkan:
../env/bin/python setup.py develop-use-pip
Ubahlah file `development.ini` pada baris `sqlalchemy.url` sesuai dengan yang
ada di `/etc/im/gw/config.py`. Lalu buatlah tabel terkait web ini:
../env/bin/initialize_web_imgw_db development.ini
Kemudian cobalah:
../env/bin/pserve --reload development.ini
Web Instant Messaging Gateway
=============================
Getting Started
---------------
- Change directory into your newly created project.
cd web-imgw
- Create a Python virtual environment.
python3 -m venv ../env
- Upgrade packaging tools.
../env/bin/pip install --upgrade pip setuptools
- Install required package.
../env/bin/python setup.py develop-use-pip
- Set sqlalchemy.url on development.ini and create tables.
../env/bin/initialize_web_imgw_db development.ini
- Run your project.
../env/bin/pserve --reload development.ini
# A generic, single database configuration.
[alembic]
# path to migration scripts
script_location = alembic
# template used to generate migration files
# file_template = %%(rev)s_%%(slug)s
# timezone to use when rendering the date
# within the migration file as well as the filename.
# string value is passed to dateutil.tz.gettz()
# leave blank for localtime
# timezone =
# max length of characters to apply to the
# "slug" field
#truncate_slug_length = 40
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# set to 'true' to allow .pyc and .pyo files without
# a source .py file to be detected as revisions in the
# versions/ directory
# sourceless = false
# version location specification; this defaults
# to alembic/versions. When using multiple version
# directories, initial revisions must be specified with --version-path
# version_locations = %(here)s/bar %(here)s/bat alembic/versions
# the output encoding used when revision files
# are written from script.py.mako
# output_encoding = utf-8
sqlalchemy.url = {db_url}
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S
Generic single-database configuration.
\ No newline at end of file \ No newline at end of file
from __future__ import with_statement
from alembic import context
from sqlalchemy import engine_from_config, pool
from logging.config import fileConfig
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
fileConfig(config.config_file_name)
# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
target_metadata = None
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
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.
"""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url, target_metadata=target_metadata, literal_binds=True)
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.
"""
connectable = engine_from_config(
config.get_section(config.config_ini_section),
prefix='sqlalchemy.',
poolclass=pool.NullPool)
with connectable.connect() as connection:
context.configure(
connection=connection,
target_metadata=target_metadata
)
with context.begin_transaction():
context.run_migrations()
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
delete from produk where nama in ('CERIA', 'STARONE', 'ESIA', 'HEPI', 'FREN', 'FLEXI');
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;
###
# app configuration
# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html
###
[app:main]
use = egg:web_imgw
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
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
# By default, the toolbar only appears for clients from IP addresses
# '127.0.0.1' and '::1'.
# debugtoolbar.hosts = 127.0.0.1 ::1
###
# 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, web_imgw
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = INFO
handlers = console
[logger_web_imgw]
level = DEBUG
handlers =
qualname = web_imgw
[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
NAME="$1"
msgfmt -o web_imgw/locale/id/LC_MESSAGES/$NAME.mo web_imgw/locale/id/LC_MESSAGES/$NAME.po
###
# app configuration
# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html
###
[app:main]
use = egg:web_imgw
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
###
[server:main]
use = egg:waitress#main
listen = *:6543
###
# logging configuration
# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
###
[loggers]
keys = root, web_imgw
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
[logger_web_imgw]
level = WARN
handlers =
qualname = web_imgw
[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
[pytest]
testpaths = web_imgw
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.txt')) as f:
README = f.read()
with open(os.path.join(here, 'CHANGES.txt')) as f:
CHANGES = f.read()
requires = [
'plaster_pastedeploy',
'pyramid',
'pyramid_chameleon',
'pyramid_debugtoolbar',
'pyramid_tm',
'waitress',
'zope.sqlalchemy',
'psycopg2-binary',
'pytz',
'ziggurat-foundations',
'alembic',
'colander',
'deform',
'pyramid_beaker',
'pyramid_mailer',
'paginate_sqlalchemy',
]
tests_require = [
'WebTest >= 1.3.1', # py3 compat
'pytest',
'pytest-cov',
]
if sys.argv[1:] and sys.argv[1] == 'develop-use-pip':
bin_ = os.path.split(sys.executable)[0]
pip = os.path.join(bin_, 'pip')
for package in requires:
if sys.argv[2:]:
cmd = [pip, 'install', sys.argv[2], package]
else:
cmd = [pip, 'install', package]
subprocess.call(cmd)
cmd = [sys.executable, sys.argv[0], 'develop']
subprocess.call(cmd)
sys.exit()
setup(
name='web_imgw',
version='0.1',
description='Web Instant Messaging Gateway',
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='',
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 = web_imgw:main',
],
'console_scripts': [
'initialize_web_imgw_db = web_imgw.scripts.initialize_db:main',
]
},
)
import os
import csv
import deform
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.authentication import AuthTktAuthenticationPolicy
from pyramid.authorization import ACLAuthorizationPolicy
from pyramid_mailer import mailer_factory_from_settings
from sqlalchemy import engine_from_config
from .models import (
DBSession,
Base,
)
from .security import (
group_finder,
get_user,
)
from .tools.this_framework import get_locale_name
from .views import RemoveSlashNotFoundViewFactory
here = os.path.abspath(os.path.dirname(__file__))
routes_file = os.path.join(here, 'routes.csv')
def set_paths(config):
with open(routes_file) as f:
c = csv.DictReader(f)
for row in c:
path = row['path'] or '/' + row['name']
config.add_route(row['name'], path)
config.scan()
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)
def main(global_config, **settings):
""" This function returns a Pyramid WSGI application.
"""
engine = engine_from_config(settings, 'sqlalchemy.')
DBSession.configure(bind=engine)
Base.metadata.bind = engine
session_factory = session_factory_from_settings(settings)
config = Configurator(
settings=settings,
root_factory='web_imgw.models.ziggurat.RootFactory',
session_factory=session_factory,
locale_negotiator=get_locale_name)
config.include('pyramid_tm')
config.include('pyramid_beaker')
config.include('pyramid_chameleon')
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)
config.add_notfound_view(RemoveSlashNotFoundViewFactory())
config.add_static_view('static', 'static', cache_max_age=3600)
config.add_static_view('deform_static', 'deform:static')
config.add_translation_dirs('locale')
config.registry['mailer'] = mailer_factory_from_settings(settings)
set_paths(config)
return config.make_wsgi_app()
# 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 ""
No preview for this file type
# 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"
No preview for this file type
#
# 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 ""
No preview for this file type
#
# 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\">Instant Messaging Gateway</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"
No preview for this file type
#
# 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"
No preview for this file type
#
# 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"
msgid "Messages"
msgstr "Pesan"
msgid "message-list"
msgstr "Pesan terkirim / masuk"
msgid "message-add"
msgstr "Kirim pesan"
msgid "agent-list"
msgstr "Agen"
msgid "agent-add"
msgstr "Tambah agen"
No preview for this file type
#
# 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-11-16 04:58+0700\n"
"PO-Revision-Date: 2018-11-16 04:59+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: Kirim / terima
#: web_imgw/views/message.py:72
msgid "direction"
msgstr ""
#. Default: Pengirim / penerima
#: web_imgw/views/message.py:75
msgid "sender-recipient-identity"
msgstr ""
#: web_imgw/views/message.py:86
msgid "Lihat"
msgstr ""
#: web_imgw/views/message.py:89
msgid "SMS"
msgstr ""
#: web_imgw/views/message.py:90
msgid "USSD"
msgstr ""
#: web_imgw/views/message.py:91
msgid "Mail"
msgstr ""
#. Default: Penerimaan
#: web_imgw/views/message.py:95
msgid "msg-received"
msgstr ""
#. Default: Pengiriman
#: web_imgw/views/message.py:96
msgid "msg-sent"
msgstr ""
#. Default: Pengiriman berhasil
#: web_imgw/views/message.py:97
msgid "msg-sent-ok"
msgstr ""
#. Default: Pengiriman gagal
#: web_imgw/views/message.py:98
msgid "msg-sent-failed"
msgstr ""
#. Default: Pengiriman masih proses
#: web_imgw/views/message.py:99
msgid "msg-sent-process"
msgstr ""
#: web_imgw/views/message.py:176
msgid "Pesan"
msgstr ""
#. Default: Pesan USSD harus ada pengirimnya.
#: web_imgw/views/message.py:207
msgid "missing-ussd-sender"
msgstr ""
#. Default: Nomor penerima tidak benar.
#: web_imgw/views/message.py:216
msgid "invalid-recipient-phone-number"
msgstr ""
#. Default: Nomor penerima tidak benar.
#: web_imgw/views/message.py:225
msgid "invalid-recipient-msisdn"
msgstr ""
#. Default: Bisa nomor HP atau email, sesuai Pengirim. Kosongkan bila mengirim
#. USSD.
#: web_imgw/views/message.py:241
msgid "desc-penerima"
msgstr ""
#. Default: Pesan sudah masuk antrian.
#: web_imgw/views/message.py:265
msgid "message-sent"
msgstr ""
#. Default: Kirim pesan
#: web_imgw/views/message.py:305
msgid "add-message"
msgstr "Kirim pesan"
msgid "Resend"
msgstr "Kirim ulang"
msgid "Cancel"
msgstr "Batalkan"
No preview for this file type
#
# 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 ""
msgid "Add message"
msgstr ""
msgid "Messages"
msgstr ""
msgid "message-add"
msgstr ""
msgid "message-list"
msgstr ""
msgid "agent-add"
msgstr ""
msgid "agent-list"
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-11-16 04:58+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: Kirim / terima
#: ./web_imgw/views/message.py:72
msgid "direction"
msgstr ""
#. Default: Pengirim / penerima
#: ./web_imgw/views/message.py:75
msgid "sender-recipient-identity"
msgstr ""
#: ./web_imgw/views/message.py:86
msgid "Lihat"
msgstr ""
#: ./web_imgw/views/message.py:89
msgid "SMS"
msgstr ""
#: ./web_imgw/views/message.py:90
msgid "USSD"
msgstr ""
#: ./web_imgw/views/message.py:91
msgid "Mail"
msgstr ""
#. Default: Penerimaan
#: ./web_imgw/views/message.py:95
msgid "msg-received"
msgstr ""
#. Default: Pengiriman
#: ./web_imgw/views/message.py:96
msgid "msg-sent"
msgstr ""
#. Default: Pengiriman berhasil
#: ./web_imgw/views/message.py:97
msgid "msg-sent-ok"
msgstr ""
#. Default: Pengiriman gagal
#: ./web_imgw/views/message.py:98
msgid "msg-sent-failed"
msgstr ""
#. Default: Pengiriman masih proses
#: ./web_imgw/views/message.py:99
msgid "msg-sent-process"
msgstr ""
#: ./web_imgw/views/message.py:176
msgid "Pesan"
msgstr ""
#. Default: Pesan USSD harus ada pengirimnya.
#: ./web_imgw/views/message.py:207
msgid "missing-ussd-sender"
msgstr ""
#. Default: Nomor penerima tidak benar.
#: ./web_imgw/views/message.py:216
msgid "invalid-recipient-phone-number"
msgstr ""
#. Default: Nomor penerima tidak benar.
#: ./web_imgw/views/message.py:225
msgid "invalid-recipient-msisdn"
msgstr ""
#. Default: Bisa nomor HP atau email, sesuai Pengirim. Kosongkan bila mengirim
#. USSD.
#: ./web_imgw/views/message.py:241
msgid "desc-penerima"
msgstr ""
#. Default: Pesan sudah masuk antrian.
#: ./web_imgw/views/message.py:265
msgid "message-sent"
msgstr ""
#. Default: Kirim pesan
#: ./web_imgw/views/message.py:305
msgid "add-message"
msgstr ""
msgid "Resend"
msgstr ""
msgid "Cancel"
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 (
Column,
Integer,
)
from sqlalchemy.orm import (
sessionmaker,
scoped_session,
)
from sqlalchemy.ext.declarative import declarative_base
from zope.sqlalchemy import ZopeTransactionExtension
DBSession = scoped_session(
sessionmaker(extension=ZopeTransactionExtension()))
Base = declarative_base()
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
def from_dict(self, d):
for column in self.__table__.columns:
if column.name in d:
setattr(self, column.name, d[column.name])
class DefaultModel(CommonModel):
id = Column(Integer, primary_key=True)
from datetime import datetime
from sqlalchemy import (
Column,
Integer,
Float,
Text,
String,
Boolean,
DateTime,
Date,
ForeignKey,
Sequence,
func,
text,
)
from sqlalchemy.orm import relationship
from . import (
Base,
DBSession,
DefaultModel,
CommonModel,
)
class Jalur(Base, DefaultModel):
__tablename__ = 'jalur'
nama = Column(String(15), unique=True, nullable=False)
class Status(Base, DefaultModel):
__tablename__ = 'status'
__table_args__ = dict(schema='im')
ket = Column(String(100), unique=True, nullable=False)
class Antrian(Base, DefaultModel):
__tablename__ = 'antrian'
__table_args__ = dict(schema='im')
jalur = Column(
Integer, ForeignKey(Jalur.id), nullable=False, server_default='1')
kirim = Column(Boolean, nullable=False, server_default='true')
tgl = Column(
DateTime(timezone=True), server_default=func.now(), nullable=False)
tgl_operator = Column(DateTime(timezone=True))
pengirim = Column(String(64))
penerima = Column(String(64))
pesan = Column(Text, nullable=False)
jawaban = Column(Text)
parser = Column(String(64))
status = Column(
Integer, ForeignKey(Status.id), server_default='1', nullable=False)
class Selesai(Base, CommonModel):
__tablename__ = 'selesai'
__table_args__ = dict(schema='im')
id = Column(
Integer, primary_key=True,
server_default=text("nextval('im.antrian_id_seq')"))
jalur = Column(
Integer, ForeignKey(Jalur.id), nullable=False, server_default='1')
kirim = Column(Boolean, nullable=False)
tgl = Column(
DateTime(timezone=True), server_default=func.now(), nullable=False)
tgl_operator = Column(DateTime(timezone=True))
pengirim = Column(String(64))
penerima = Column(String(64))
pesan = Column(Text, nullable=False)
jawaban = Column(Text)
parser = Column(String(64))
status = Column(Integer, ForeignKey(Status.id), nullable=False)
#########
# Agent #
#########
class StatusAgent(Base, DefaultModel):
__tablename__ = 'status_agent'
__table_args__ = dict(schema='im')
ket = Column(String(100), unique=True, nullable=False)
class Agent(Base, CommonModel):
__tablename__ = 'agent'
__table_args__ = dict(schema='im')
id = Column(String(64), primary_key=True)
jalur = Column(Integer, ForeignKey(Jalur.id), nullable=False)
#jalur_ref = relationship('Jalur', foreign_keys='Agent.jalur')
jalur_ref = relationship(Jalur)
status = Column(
Integer, ForeignKey(StatusAgent.id), nullable=False,
server_default='0')
#status_ref = relationship('StatusAgent', foreign_keys='Agent.status')
status_ref = relationship(StatusAgent)
job = Column(Integer, nullable=False, server_default='0')
lastjob = Column(
DateTime(timezone=True), nullable=False, server_default=func.now())
startup = Column(
DateTime(timezone=True), nullable=False, server_default=func.now())
ket = Column(Text)
lasterr = Column(Text)
url = Column(String(64))
modem = relationship('Modem', backref='im.agent', uselist=False)
class Produk(Base, CommonModel):
__tablename__ = 'produk'
nama = Column(String(20), primary_key=True)
class MSISDN(Base, CommonModel):
__tablename__ = 'msisdn'
awalan = Column(String(10), primary_key=True)
produk = Column(String(20), ForeignKey(Produk.nama), nullable=False)
wilayah = Column(String(30))
class Modem(Base, CommonModel):
__tablename__ = 'modem'
__table_args__ = dict(schema='im')
msisdn = Column(String(64), nullable=False, primary_key=True)
imei = Column(String(64), ForeignKey(Agent.id, ondelete='cascade'), nullable=False)
device = Column(String(20))
sn = Column(String(20))
merk = Column(String(64))
signal = Column(Integer, nullable=False, server_default='0')
produk = Column(String(20), ForeignKey(Produk.nama), nullable=False)
wilayah = Column(String(30))
pulsa = relationship('Pulsa', backref='im.modem', uselist=False)
reply_for = relationship('ModemPengirim', backref='im.modem_pengirim', order_by='ModemPengirim.produk')
class Pulsa(Base, CommonModel):
__tablename__ = 'pulsa'
__table_args__ = dict(schema='im')
msisdn = Column(
String(20), ForeignKey(Modem.msisdn, ondelete='cascade'),
primary_key=True)
request = Column(String(10), nullable=False)
response = Column(Text)
tgl = Column(DateTime(timezone=True))
class ModemPengirim(Base, CommonModel):
__tablename__ = 'modem_pengirim'
__table_args__ = dict(schema='im')
produk = Column(String(20), ForeignKey(Produk.nama), primary_key=True)
msisdn = Column(String(20), ForeignKey(Modem.msisdn, ondelete='cascade'), primary_key=True)
msisdn_produk = Column(
String(20), ForeignKey(Produk.nama), nullable=False)
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 . import (
Base,
DBSession,
CommonModel,
)
# this is needed for scoped session approach like in pylons 1.0
ziggurat_foundations.models.DBSession = DBSession
# optional for folks who pass request.db to model methods
# 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 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, passwordmanager=None)
ziggurat_model_init(User, Group, UserGroup, GroupPermission, UserPermission,
UserResourcePermission, GroupResourcePermission, Resource,
ExternalIdentity, passwordmanager=None)
name,path
home,/
login
logout
change-password
change-password-done
reset-password
reset-password-sent
login-by-code-failed
user
user-add,/user/add
user-edit,/user/{id}
user-delete,/user/{id}/delete
group
group-add,/group/add
group-edit,/group/{id}
group-delete,/group/{id}/delete
message
message-add,/message/add
message-resend,/message/{id}/resend
agent
agent-edit,/agent/{id}
agent-delete,/agent/{id}/delete
group_name
Superuser
Finance
This diff could not be displayed because it is too large.
nama
AS
AXIS
HALO
IM3
MENTARI
SIMPATI
SMART
THREE
XL
email,status,user_name
admin@local,1,admin
import os
import sys
import csv
import subprocess
import transaction
from getpass import getpass
from sqlalchemy import engine_from_config
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 (
DBSession,
Base,
)
from ..models.ziggurat import (
Group,
GroupPermission,
UserGroup,
User,
)
from ..models.imgw import (
Produk,
Status,
StatusAgent,
Jalur,
Agent,
Modem,
ModemPengirim,
Pulsa,
Antrian,
Selesai,
MSISDN,
)
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 usage(argv):
cmd = os.path.basename(argv[0])
print('usage: %s <config_uri>\n'
'(example: "%s development.ini")' % (cmd, cmd))
sys.exit(1)
def read_file(filename):
f = open(filename)
s = f.read()
f.close()
return s
def alembic_run(ini_file, url):
bin_path = os.path.split(sys.executable)[0]
alembic_bin = os.path.join(bin_path, 'alembic')
command = (alembic_bin, 'upgrade', 'head')
s = read_file(ini_file)
s = s.replace('{db_url}', url)
f = open('alembic.ini', 'w')
f.write(s)
f.close()
subprocess.call(command)
os.remove('alembic.ini')
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):
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):
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 create_schema(name):
return 'CREATE SCHEMA IF NOT EXISTS {}'.format(name)
def main(argv=sys.argv):
if len(argv) != 2:
usage(argv)
config_uri = argv[1]
setup_logging(config_uri)
settings = get_appsettings(config_uri)
my_registry['settings'] = settings
engine = engine_from_config(settings, 'sqlalchemy.')
Base.metadata.bind = engine
engine.execute(create_schema('im'))
Base.metadata.create_all()
alembic_run('alembic.ini.tpl', settings['sqlalchemy.url'])
with transaction.manager:
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')
append_csv(Produk, 'produk.csv', ['nama'])
append_csv(MSISDN, 'msisdn.csv', ['awalan'])
from .models import DBSession
from .models.ziggurat import (
User,
UserGroup,
)
def group_finder(login, request):
q = DBSession.query(User).filter_by(id=login)
u = q.first()
if not u or not u.status:
return # None means logout
r = []
q = DBSession.query(UserGroup).filter_by(user_id=u.id)
for ug in q:
acl_name = 'group:{gid}'.format(gid=ug.group_id)
r.append(acl_name)
return r
def get_user(request):
uid = request.authenticated_userid
if uid:
q = DBSession.query(User).filter_by(id=uid)
return q.first()
.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;
}
.just-separator {
padding-top: 15px;
}
@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;
}
}
import unittest
from pyramid import testing
class ViewTests(unittest.TestCase):
def setUp(self):
self.config = testing.setUp()
def tearDown(self):
testing.tearDown()
def test_my_view(self):
from .views import my_view
request = testing.DummyRequest()
info = my_view(request)
self.assertEqual(info['project'], 'Web Instant Messaging Gateway')
class FunctionalTests(unittest.TestCase):
def setUp(self):
from web_imgw import main
app = main({})
from webtest import TestApp
self.testapp = TestApp(app)
def test_root(self):
res = self.testapp.get('/', status=200)
self.assertTrue(b'Pyramid' in res.body)
import re
REGEX_ONLY_CONTAIN = re.compile('([0-9\+\- ]*)')
def get_msisdn(msisdn, country='+62'):
match = REGEX_ONLY_CONTAIN.search(msisdn)
if not match or match.group(1) != msisdn:
return
clean = msisdn.replace(' ', '').replace('-', '')
length = len(clean)
if length < 7 or length > 16:
return
try:
int(clean)
except ValueError:
return
if clean[0] == '+':
return clean
return country + clean.lstrip('0')
def one_space(s):
s = s.strip()
while s.find(' ') > -1:
s = s.replace(' ', ' ')
return s
def to_str(v):
if isinstance(v, date):
return dmy(v)
if isinstance(v, datetime):
return dmyhms(v)
if v == 0:
return '0'
if isinstance(v, str):
return v.strip()
if 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 = [int(x) for x in 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 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='Web Instant Messaging Gateway', title=_('Home'))
from sqlalchemy import func
from pyramid.view import view_config
from pyramid.httpexceptions import (
HTTPFound,
HTTPNotFound,
)
from pyramid.i18n import TranslationStringFactory
import colander
from deform import (
Form,
ValidationFailure,
Button,
)
from deform.widget import (
TextInputWidget,
CheckboxChoiceWidget,
)
from ..models import DBSession
from ..models.imgw import (
Agent,
Modem,
ModemPengirim,
Produk,
)
from ..tools.msisdn import get_msisdn
_ = TranslationStringFactory('agent')
########
# List #
########
@view_config(
route_name='agent', renderer='templates/agent/list.pt',
permission='edit-agent')
def view_list(request):
q = DBSession.query(func.count(Agent.id))
count = q.scalar()
q = DBSession.query(Agent).order_by(Agent.jalur, Agent.id)
return dict(title=_('Agent'), agents=q, count=count)
########
# Edit #
########
def get_produk_list():
r = []
for row in DBSession.query(Produk).order_by(Produk.nama):
r.append((row.nama, row.nama))
return r
def get_produk_set_by_agent(agent):
r = []
for row in agent.modem.reply_for:
r.append(row.produk)
return set(r)
def get_form(request, class_form):
schema = class_form()
#schema = class_form(validator=form_validator)
schema = schema.bind(produk_list=get_produk_list())
#schema.request = request
btn_save = Button('save', _('Simpan'))
btn_cancel = Button('cancel', _('Batalkan'))
buttons = (btn_save, btn_cancel)
return Form(schema, buttons=buttons)
def update(agent, values):
agent.from_dict(values)
DBSession.add(agent)
if 'msisdn' not in values:
return
q = DBSession.query(Modem).filter_by(imei=agent.id)
modem = q.first()
if not modem:
modem = Modem()
modem.imei = row.id
modem.msisdn = get_msisdn(values['msisdn'])
DBSession.add(modem)
existing = get_produk_set_by_agent(agent)
unused = existing - values['pengirim_sms']
if unused:
q = DBSession.query(ModemPengirim).filter_by(msisdn=modem.msisdn).\
filter(ModemPengirim.produk.in_(unused))
q.delete(synchronize_session=False)
new = values['pengirim_sms'] - existing
for produk in new:
mp = ModemPengirim(produk=produk, msisdn=modem.msisdn)
DBSession.add(mp)
def save_request(values, request, row=None):
if 'id' in request.matchdict:
values['id'] = request.matchdict['id']
row = save_agent(values, request.user, row)
request.session.flash('%s %s berhasil disimpan.' % (
row.jalur_ref.nama.title(), row.id))
class EditSchema(colander.Schema):
id = colander.SchemaNode(colander.String(),
missing=colander.drop,
widget=TextInputWidget(readonly=True),
title=_('title-id', default='ID'))
jenis = colander.SchemaNode(colander.String(),
missing=colander.drop,
widget=TextInputWidget(readonly=True),
title=_('title-jenis', default='Jenis'))
ket = colander.SchemaNode(colander.String(),
missing=colander.drop,
title=_('title-ket', default='Keterangan'),
description=_(
'desc-ket', default='Informasi tambahan, misalnya: untuk SMS'))
url = colander.SchemaNode(colander.String(),
missing=colander.drop,
title=_('title-url', default='URL'),
description=_(
'desc-url', default='URL im-agent atau im-gw forwarder'))
@colander.deferred
def deferred_pengirim_sms(node, kw):
values = kw.get('produk_list', [])
return CheckboxChoiceWidget(values=values, inline=True)
class ModemSchema(colander.Schema):
msisdn = colander.SchemaNode(
colander.String(), title=_('MSISDN'),
description=_('Nomor HP SIM card'))
pengirim_sms = colander.SchemaNode(
colander.Set(), missing=colander.drop,
widget=deferred_pengirim_sms,
title=_(
'title-pengirim-sms', default='Pengirim SMS untuk'),
description=_(
'desc-pengirim-sms',
default='Sistem akan menggunakan informasi ini untuk '\
'menentukan modem mana yang digunakan untuk mengirim SMS. '\
'Tujuannya efisiensi pulsa.'))
ket = colander.SchemaNode(colander.String(),
missing=colander.drop,
title=_('title-ket', default='Keterangan'),
description=_(
'desc-ket', default='Informasi tambahan, misalnya: untuk SMS'))
url = colander.SchemaNode(colander.String(),
missing=colander.drop,
title=_('title-url', default='URL'),
description=_(
'desc-url', default='URL im-agent atau im-gw forwarder'))
def pengirim_sms(agent):
r = []
for mp in agent.modem.reply_for:
r.append(mp.produk)
return set(r)
@view_config(
route_name='agent-edit', renderer='templates/agent/edit.pt',
permission='edit-agent')
def view_edit(request):
q = DBSession.query(Agent).filter_by(id=request.matchdict['id'])
agent = q.first()
if not agent:
return HTTPNotFound()
cls = agent.jalur == 1 and ModemSchema or EditSchema
form = get_form(request, cls)
resp = dict(title=_('Edit agent'))
if not request.POST:
d = agent.to_dict_without_none()
if agent.modem:
d['msisdn'] = agent.modem.msisdn
d['pengirim_sms'] = pengirim_sms(agent)
else:
d['jenis'] = agent.jalur_ref.nama.title()
resp['form'] = form.render(appstruct=d)
return resp
if 'save' not in request.POST:
return HTTPFound(location=request.route_url('agent'))
items = request.POST.items()
try:
c = form.validate(items)
except ValidationFailure:
resp['form'] = form.render()
return resp
update(agent, dict(c.items()))
if agent.modem:
data = dict(msisdn=agent.modem.msisdn)
ts = _(
'modem-apdated', default='Modem ${msisdn} updated',
mapping=data)
else:
data = dict(agent_id=agent.id)
ts = _('agent-updated', default='Agent ID ${agent_id} updated', mapping=data)
request.session.flash(ts)
return HTTPFound(location=request.route_url('agent'))
##########
# Delete #
##########
@view_config(
route_name='agent-delete', renderer='templates/agent/delete.pt',
permission='edit-agent')
def view_delete(request):
q = DBSession.query(Agent).filter_by(id=request.matchdict['id'])
agent = q.first()
if not agent:
return HTTPNotFound()
btn_delete = Button('delete', _('Delete'))
btn_cancel = Button('cancel', _('Cancel'))
buttons = (btn_delete, btn_cancel)
form = Form(colander.Schema(), buttons=buttons)
resp = dict(title=_('Delete agent'))
if not request.POST:
resp['form'] = form.render()
return resp
if 'delete' not in request.POST:
return HTTPFound(location=request.route_url('agent'))
agent_id = agent.modem and agent.modem.msisdn or agent.id
data = dict(agent_id=agent_id)
ts = _('agent-deleted', default='${agent_id} sudah dihapus', mapping=data)
q.delete()
request.session.flash(ts)
return HTTPFound(location=request.route_url('agent'))
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 import DBSession
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 = 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, group):
self.group = group
def __call__(self, node, value):
group_name = clean_name(value)
if self.group and self.group.group_name.lower() == group_name.lower():
return
q = DBSession.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['group'])
@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')),
('edit-agent', _('IM Agent management')),
('add-message', _('Send message')),
]
def get_form(request, class_form, group=None):
schema = class_form()
schema = schema.bind(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(values):
group = Group()
group.group_name = clean_name(values['group_name'])
group.description = values['description']
DBSession.add(group)
DBSession.flush()
for perm_name in values['permissions']:
gp = GroupPermission()
gp.group_id = group.id
gp.perm_name = perm_name
DBSession.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(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(group):
q = DBSession.query(GroupPermission).filter_by(group_id=group.id)
r = []
for gp in q:
r.append(gp.perm_name)
return set(r)
def update(group, values):
group.group_name = clean_name(values['group_name'])
group.description = values['description']
DBSession.add(group)
existing = group_permission_set(group)
unused = existing - values['permissions']
if unused:
q = DBSession.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
DBSession.add(gp)
@view_config(
route_name='group-edit', renderer='templates/group/edit.pt',
permission='user-edit')
def view_edit(request):
q = 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(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(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 = 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,
authenticated_userid,
)
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 import DBSession
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='home'):
headers = remember(request, user.id)
user.last_login_date = create_now()
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 = DBSession.query(User).filter_by(email=identity)
else:
q = 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 = 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
DBSession.add(user)
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 authenticated_userid(request):
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'])
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(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()
DBSession.add(user)
return one_hour
@view_config(route_name='reset-password', renderer='templates/reset-password.pt')
def view_reset_password(request):
if authenticated_userid(request):
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 = 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(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'))
import os
from datetime import (
date,
datetime,
timedelta,
)
from base64 import (
b64encode,
b64decode,
)
from sqlalchemy import (
or_,
not_,
)
from pkg_resources import resource_filename
from pyramid.view import view_config
from pyramid.httpexceptions import (
HTTPFound,
HTTPNotFound,
)
from pyramid.i18n import TranslationStringFactory
import colander
from deform import (
Form,
widget,
ValidationFailure,
Button,
ZPTRendererFactory,
)
from paginate_sqlalchemy import SqlalchemyOrmPage
from ..models import DBSession
from ..models.ziggurat import User
from ..models.imgw import (
Jalur,
Selesai,
Antrian,
Agent,
Modem,
ModemPengirim,
)
from ..tools.msisdn import get_msisdn
from ..tools.waktu import (
date_from_str,
create_datetime,
)
_ = TranslationStringFactory('message')
########
# List #
########
@colander.deferred
def jalur_widget(node, kw):
values = kw.get('jalur_list')
return widget.SelectWidget(values=values)
@colander.deferred
def kirim_widget(node, kw):
values = kw.get('kirim_list')
return widget.SelectWidget(values=values)
class FilterSchema(colander.Schema):
awal = colander.SchemaNode(colander.Date())
akhir = colander.SchemaNode(colander.Date())
pesan = colander.SchemaNode(colander.String(), missing=colander.drop)
jalur = colander.SchemaNode(
colander.String(), missing=colander.drop, widget=jalur_widget)
kirim = colander.SchemaNode(
colander.String(), missing=colander.drop, widget=kirim_widget,
title=_('direction', default='Kirim / terima'))
identitas = colander.SchemaNode(
colander.String(), missing=colander.drop,
title=_('sender-recipient-identity', default='Pengirim / penerima'))
deform_templates = resource_filename('deform', 'templates')
here = os.path.abspath(os.path.dirname(__file__))
my_templates = os.path.join(here, 'templates', 'message')
search_path = [my_templates, deform_templates]
my_renderer = ZPTRendererFactory(search_path)
def get_filter_form():
btn_show = Button('lihat', _('Lihat'))
jalur_list = [
('', ''),
('1', _('SMS')),
('1 ussd', _('USSD')),
('6', _('Mail')),
]
kirim_list = [
('', ''),
('0', _('msg-received', default='Penerimaan')),
('1', _('msg-sent', default='Pengiriman')),
('1 0', _('msg-sent-ok', default='Pengiriman berhasil')),
('1 -1', _('msg-sent-failed', default='Pengiriman gagal')),
('1 1', _('msg-sent-process', default='Pengiriman masih proses')),
]
schema = FilterSchema()
schema = schema.bind(jalur_list=jalur_list, kirim_list=kirim_list)
return Form(
schema, buttons=(btn_show,), renderer=my_renderer)
def route_list(request, p={}):
return HTTPFound(location=request.route_url('message', _query=p))
def get_query(p):
d = p['awal']
awal = create_datetime(d.year, d.month, d.day)
d = p['akhir'] + timedelta(1)
akhir = create_datetime(d.year, d.month, d.day)
q = DBSession.query(Selesai).filter(
Selesai.tgl >= awal, Selesai.tgl < akhir)
if 'jalur' in p:
t = p['jalur'].split()
jalur_id = int(t[0])
q = q.filter_by(jalur=jalur_id)
if t[1:]:
q = q.filter(Selesai.penerima == None) # USSD
else:
q = q.filter(Selesai.penerima != None) # SMS
if 'kirim' in p:
t = p['kirim'].split()
kirim = t[0] == '1'
q = q.filter_by(kirim=kirim)
if t[1:]:
status = t[1]
if status == '0':
q = q.filter_by(status=0)
elif status == '-1':
q = q.filter(or_(Selesai.status < 0, Selesai.status == 4))
else:
q = q.filter(Selesai.status > 0)
if 'identitas' in p:
q = q.filter(
or_(
Selesai.pengirim.ilike('%' + p['identitas'] + '%'),
Selesai.pengirim.ilike('%' + p['identitas'].lstrip('0') + '%'),
Selesai.penerima.ilike('%' + p['identitas'] + '%'),
Selesai.penerima.ilike('%' + p['identitas'].lstrip('0') + '%')))
if 'pesan' in p:
q = q.filter(
or_(
Selesai.pesan.ilike('%' + p['pesan'] + '%'),
Selesai.jawaban.ilike('%' + p['pesan'] + '%')))
return q
@view_config(
route_name='message', renderer='templates/message/list.pt',
permission='view')
def view_list(request):
def url_maker(page):
p['page'] = page
return request.route_url('message', _query=p)
if not request.GET and not request.POST:
p = dict(awal=date.today())
p['akhir'] = p['awal']
return route_list(request, p)
if request.POST:
form = get_filter_form()
items = request.POST.items()
try:
c = form.validate(items)
except ValidationFailure as e:
resp['form'] = e.render()
return resp
p = dict(c.items())
p['lihat'] = 1
return route_list(request, p)
resp = dict(title=_('Pesan'))
form = get_filter_form()
items = request.GET.items()
p = dict(items)
p['awal'] = date_from_str(p['awal'])
p['akhir'] = date_from_str(p['akhir'])
resp['form'] = form.render(appstruct=p)
if 'lihat' not in request.GET:
return resp
q = get_query(p)
resp['count'] = count = q.count()
if not count:
return resp
d = dict(request.GET)
del d['lihat']
from_url = request.route_url('message', _query=d)
resp['from_url'] = b64encode(from_url.encode('utf-8'))
q = q.order_by(Selesai.id.desc())
page = request.GET.get('page')
page = page and int(page) or 1
resp['rows'] = SqlalchemyOrmPage(
q, page=page, items_per_page=10, item_count=count,
url_maker=url_maker)
return resp
########
# Send #
########
def form_validator(form, value):
t = value['pengirim'].split()
jalur_id = int(t[0])
if jalur_id != 1:
return
if 'penerima' not in value:
if not t[1:]: # USSD tapi tidak ada pengirim ?
raise colander.Invalid(
form, _(
'missing-ussd-sender',
default='Pesan USSD harus ada pengirimnya.'))
return
msisdn = value['penerima']
if msisdn[0] in ['0', '+']:
msisdn = get_msisdn(msisdn)
if not msisdn:
raise colander.Invalid(
form, _(
'invalid-recipient-phone-number',
default='Nomor penerima tidak benar.'))
return
# Siapa tahu shortcode
try:
int(msisdn)
except ValueError:
raise colander.Invalid(
form, _(
'invalid-recipient-msisdn',
default='Nomor penerima tidak benar.'))
@colander.deferred
def pengirim_widget(node, kw):
values = kw.get('pengirim_list')
return widget.SelectWidget(values=values)
class MessageSchema(colander.Schema):
pengirim = colander.SchemaNode(
colander.String(), widget=pengirim_widget)
penerima = colander.SchemaNode(
colander.String(), missing=colander.drop,
description=_(
'desc-penerima',
default='Bisa nomor HP atau email, sesuai Pengirim. '\
'Kosongkan bila mengirim USSD.'))
pesan = colander.SchemaNode(
colander.String(),
widget=widget.TextAreaWidget(rows=5, cols=60))
def save(request, values):
p = dict(pesan=values['pesan'],
kirim=True)
t = values['pengirim'].split()
p['jalur'] = int(t[0])
if p['jalur'] == 1 and values['penerima'].find('0') == 0:
p['penerima'] = get_msisdn(values['penerima'])
elif values['penerima']:
p['penerima'] = values['penerima']
if t[1:]:
p['pengirim'] = t[1]
outbox = Antrian()
outbox.from_dict(p)
DBSession.add(outbox)
request.session.flash(
_('message-sent', default='Pesan sudah masuk antrian.'))
def get_pengirim_list():
r = []
q = DBSession.query(Jalur).filter(Jalur.id.in_([1, 4, 6]))
for row in q.order_by(Jalur.id):
r.append((row.id, row.nama))
q = DBSession.query(Agent)
for row in q.order_by(Agent.jalur, Agent.id):
if row.jalur == 1 and row.modem:
agent_id = row.modem.msisdn
else:
agent_id = row.id
key = '{} {}'.format(row.jalur, agent_id)
if row.status == 0:
if row.jalur == 1:
status = '{}%'.format(row.modem.signal)
else:
status = 'ON'
else:
status = 'OFF'
value = '{} {} {}'.format(row.jalur_ref.nama, agent_id, status)
r.append((key, value))
return r
def get_add_form():
schema = MessageSchema(validator=form_validator)
schema = schema.bind(pengirim_list=get_pengirim_list())
btn_kirim = Button('kirim', 'Kirim')
buttons = (btn_kirim,)
return Form(schema, buttons=buttons)
@view_config(
route_name='message-add', renderer='templates/message/add.pt',
permission='add-message')
def view_add(request):
form = get_add_form()
resp = dict(title=_('add-message', default='Kirim pesan'))
form = get_add_form()
if not request.POST:
resp['form'] = form.render()
return resp
items = request.POST.items()
try:
c = form.validate(items)
except ValidationFailure as e:
resp['form'] = e.render()
return resp
save(request, dict(c.items()))
return route_list(request)
##########
# Resend #
##########
def resender_list(jalur):
r = [('', _('auto-resender', default='Auto'))]
q = DBSession.query(Agent).filter_by(jalur=jalur)
for row in q.order_by(Agent.jalur, Agent.id):
if row.jalur == 1 and row.modem:
agent_id = row.modem.msisdn
else:
agent_id = row.id
if row.status == 0:
if row.jalur == 1:
status = '{}%'.format(row.modem.signal)
else:
status = 'ON'
else:
status = 'OFF'
value = '{} {} {}'.format(row.jalur_ref.nama, agent_id, status)
r.append((row.id, value))
return r
@colander.deferred
def resender_widget(node, kw):
values = kw.get('resender_list')
return widget.SelectWidget(values=values)
class ResendSchema(colander.Schema):
pengirim = colander.SchemaNode(
colander.String(), missing=colander.drop, widget=resender_widget)
tgl = colander.SchemaNode(
colander.String(), missing=colander.drop,
widget=widget.TextInputWidget(readonly=True))
penerima = colander.SchemaNode(
colander.String(), missing=colander.drop,
widget=widget.TextInputWidget(readonly=True))
pesan = colander.SchemaNode(
colander.String(), missing=colander.drop,
widget=widget.TextAreaWidget(readonly=True))
def get_resend_form(row):
schema = ResendSchema()
schema = schema.bind(resender_list=resender_list(row.jalur))
btn_submit = Button('submit', _('Resend'))
btn_cancel = Button('cancel', _('Cancel'))
buttons = (btn_submit, btn_cancel)
return Form(schema, buttons=buttons)
def save_resend(request, row, values):
outbox = Antrian()
outbox.jalur = row.jalur
outbox.penerima = row.penerima
outbox.pesan = row.pesan
outbox.kirim = row.kirim
outbox.parser = row.parser
if 'pengirim' in values:
outbox.pengirim = values['pengirim']
DBSession.add(outbox)
row.status = 4
DBSession.add(row)
data = dict(penerima=row.penerima)
ts = _(
'resend-done',
default='Pesan untuk ${penerima} sudah dikirim ulang.',
mapping=data)
request.session.flash(ts)
@view_config(
route_name='message-resend', renderer='templates/message/resend.pt',
permission='add-message')
def view_resend(request):
q = DBSession.query(Selesai).filter_by(id=request.matchdict['id'])
row = q.first()
if not row or not row.kirim:
return HTTPNotFound()
resp = dict(title=_('Resend'))
form = get_resend_form(row)
if not request.POST:
p = row.to_dict()
p['tgl'] = p['tgl'].strftime('%-d %B %Y %H:%M')
resp['form'] = form.render(appstruct=p)
return resp
if 'submit' in request.POST:
items = request.POST.items()
try:
c = form.validate(items)
except ValidationFailure as e:
resp['form'] = e.render()
return resp
save_resend(request, row, dict(c.items()))
if 'from_url' in request.GET:
from_url = b64decode(request.GET['from_url']).decode('utf-8')
return HTTPFound(location=from_url)
return route_list(request)
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.
<html metal:use-macro="load: ../../../templates/base.pt">
<div metal:fill-slot="content">
<div class="panel panel-danger">
<div class="panel-heading">
<h3 class="panel-title">Perhatian</h3>
</div>
<div class="panel-body">
Hapus ${row.jalur_ref.nama}
<span tal:condition="row.modem">${row.modem.msisdn}</span>
<span tal:condition="not row.modem">${row.id}</span> ?
</div>
</div>
<div tal:content="structure form"/>
</div>
</html>
<div metal:use-macro="load: ../layout-menu.pt">
<div metal:fill-slot="content">
<h1>Ubah agent</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="">Agent</h1>
<p tal:condition="not count" i18n:translate="">No agent found.</p>
<table class="table table-striped table-hover" tal:condition="count">
<thead>
<tr>
<th i18n:translate="">No.</th>
<th i18n:translate="">ID</th>
<th i18n:translate="">Status</th>
<th i18n:translate="">Description</th>
<th i18n:translate="">Job</th>
<th i18n:translate="">Startup</th>
<th colspan="2"/>
</tr>
</thead>
<tbody>
<tr tal:repeat="agent agents">
<td tal:content="repeat.agent.number"/>
<td>
<span tal:replace="agent.id"/>
<span tal:replace="agent.jalur_ref.nama"/>
<span tal:condition="agent.modem">
<br/>
<span tal:replace="agent.modem.msisdn"/>
</span>
</td>
<td>
<span tal:replace="agent.status_ref.ket"/>
<span tal:condition="agent.modem">
<br/>
<span tal:replace="agent.modem.signal"/> %
</span>
</td>
<td>
<div tal:condition="agent.modem">
<span tal:condition="agent.modem.pulsa">
<span tal:replace="agent.modem.pulsa.request"/>
<span tal:condition="agent.modem.pulsa.tgl">
<span tal:replace="agent.modem.pulsa.tgl.strftime('%-d %B %Y %H:%M')"/>
<span tal:replace="agent.modem.pulsa.response">
</span>
<br/>
</span>
<span tal:condition="agent.modem.reply_for">
Pengirim SMS untuk
<span tal:repeat="mp agent.modem.reply_for">
<span tal:replace="mp.produk"/>
</span>
<br/>
</span>
</div>
<span tal:condition="agent.ket">
<span tal:replace="agent.ket"/>
<br/>
</span>
</td>
<td>
<span tal:replace="agent.lastjob.strftime('%-d %B %Y %H:%M:%S')"/>
</td>
<td>
<span tal:replace="agent.startup.strftime('%-d %B %Y %H:%M:%S')"/>
</td>
<td>
<a href="/agent/${agent.id}">Ubah</a>
<br/>
<a href="/agent/${agent.id}/delete">Hapus</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<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">Web</span> <span
class="smaller">Instant Messaging Gateway</span></h1>
<p i18n:translate="welcome" class="lead">Welcome to <span
class="font-normal">Instant Messaging Gateway</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</a></li>
<li class="dropdown" tal:attributes="class
request.matched_route.name in ['message', 'agent'] and 'active'">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
<span i18n:translate="">Messages</span><b class="caret"></b>
</a>
<ul class="dropdown-menu">
<li>
<a href="/message" i18n:translate="message-list">
Message list
</a>
</li>
<li>
<a href="/message/add" i18n:translate="message-add">
Send a message
</a>
</li>
<li>
<a href="/agent" i18n:translate="agent-list">
Agent list
</a>
</li>
<li>
<a href="/agent/add" i18n:translate="agent-add">
Add agent
</a>
</li>
</ul>
</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="just-separator"/>
<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="${request.static_url('web_imgw:static/pyramid-16x16.png')}">
<title>Web IMGW - <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="${request.static_url('web_imgw: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="${request.static_url('web_imgw: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>
<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-menu.pt">
<div metal:fill-slot="content" i18n:domain="message">
<h1 i18n:translate="add-message">Send a message</h1>
<div tal:content="structure form"/>
</div>
</div>
<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>Pesan</h1>
<div tal:content="structure form"/>
<div tal:condition="request.GET.get('lihat')">
<div tal:condition="not count">
<div class="alert alert-danger">Tidak ada hasil.</div>
</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>Waktu Catat &amp;<br/>Operator</th>
<th>Pengirim &amp;<br/>Penerima</th>
<th>Pesan &amp;<br/>Jawaban</th>
<th>Status</th>
<th>Parser</th>
</tr>
</thead>
<tbody>
<tr tal:repeat="row rows">
<td>${row.id}</td>
<td>
${row.tgl.strftime('%-d %B %Y %H:%M:%S')}<br/>
${row.tgl_operator and row.tgl_operator.strftime('%-d %B %Y %H:%M:%S')}
</td>
<td>
<span tal:condition="row.pengirim" tal:replace="row.pengirim"/>
<span tal:condition="not row.pengirim">
Tidak ada pengirim
</span>
<br/>
<span tal:condition="row.penerima" tal:replace="row.penerima"/>
<span tal:condition="not row.penerima">
USSD
</span>
</td>
<td>
${row.pesan}<br/>
<i>${row.jawaban}</i>
</td>
<td tal:condition="row.kirim">
<span tal:condition="row.status == 0">Terkirim</span>
<span tal:condition="row.status > 0 and row.status != 4">Sedang dikirim</span>
<span tal:condition="row.status < 0">Gagal terkirim</span>
<span tal:condition="row.status == 4">Sudah dikirim ulang</span>
<br/>
<a tal:condition="row.status not in [0, 4]"
href="/message/${row.id}/resend?from_url=${from_url}">
Kirim ulang
</a>
</td>
<td tal:condition="not row.kirim">Pesan masuk</td>
<td>${row.parser}</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-menu.pt">
<div metal:fill-slot="content" i18n:domain="message">
<h1 i18n:translate="Resend">Resend</h1>
<div tal:content="structure form"/>
</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>
import re
from email.utils import parseaddr
from sqlalchemy import func
from pyramid.view import view_config
from pyramid.httpexceptions import (
HTTPFound,
HTTPNotFound,
)
from pyramid.i18n import TranslationStringFactory
import colander
from deform import (
Form,
ValidationFailure,
Button,
)
from deform.widget import (
SelectWidget,
CheckboxChoiceWidget,
PasswordWidget,
HiddenWidget,
)
from ..models import DBSession
from ..models.ziggurat import (
User,
Group,
UserGroup,
)
from ..tools.waktu import create_now
from .login import (
regenerate_security_code,
send_email_security_code,
)
_ = TranslationStringFactory('user')
########
# List #
########
def query_filter(request, q):
return q.filter(
User.id==UserGroup.user_id,
UserGroup.group_id==request.GET['gid'])
@view_config(
route_name='user', renderer='templates/user/list.pt',
permission='user-edit')
def view_list(request):
if request.POST:
p = dict(gid=request.POST['gid'])
return HTTPFound(location=request.route_url('user', _query=p))
if 'gid' in request.GET and request.GET['gid']:
try:
int(request.GET['gid'])
except ValueError:
return HTTPNotFound()
q_count = DBSession.query(func.count())
q_count = query_filter(request, q_count)
count = q_count.scalar()
if count:
q_user = DBSession.query(User)
q_user = query_filter(request, q_user)
else:
q_count = DBSession.query(func.count(User.id))
count = q_count.scalar()
if count:
q_user = DBSession.query(User)
q_group = DBSession.query(Group).order_by(Group.group_name)
resp = dict(title=_('Users'), count=count, groups=q_group)
if count:
resp['users'] = q_user.order_by(User.email)
return resp
#######
# Add #
#######
@colander.deferred
def status_widget(node, kw):
values = kw.get('status_list', [])
return SelectWidget(values=values)
@colander.deferred
def group_widget(node, kw):
values = kw.get('group_list', [])
return CheckboxChoiceWidget(values=values)
class Validator:
def __init__(self, user):
self.user = user
class EmailValidator(colander.Email, Validator):
def __init__(self, user):
colander.Email.__init__(self)
Validator.__init__(self, user)
def __call__(self, node, value):
if self.match_object.match(value) is None:
raise colander.Invalid(node, _('Invalid email format'))
email = value.lower()
if self.user and self.user.email == email:
return
q = DBSession.query(User).filter_by(email=email)
found = q.first()
if not found:
return
data = dict(email=email, uid=found.id)
ts = _(
'email-already-used',
default='Email ${email} already used by user ID ${uid}',
mapping=data)
raise colander.Invalid(node, ts)
REGEX_ONLY_CONTAIN = re.compile('([a-z0-9-]*)')
REGEX_BEGIN_END_ALPHANUMERIC = re.compile('^[a-z0-9]+(?:[-][a-z0-9]+)*$')
class UsernameValidator(Validator):
def __call__(self, node, value):
username = value.lower()
if self.user and self.user.user_name == username:
return
match = REGEX_ONLY_CONTAIN.search(username)
if not match or match.group(1) != username or username != value:
ts = _(
'username-only-contain',
default='Only a-z, 0-9, and - characters are allowed')
raise colander.Invalid(node, ts)
match = REGEX_BEGIN_END_ALPHANUMERIC.search(username)
if not match:
ts = _(
'username-first-end-alphanumeric',
default='Only a-z or 0-9 at the start and end')
raise colander.Invalid(node, ts)
q = DBSession.query(User).filter_by(user_name=username)
found = q.first()
if not found:
return
data = dict(username=username, uid=found.id)
ts = _(
'username-already-used',
default='Username ${username} already used by ID ${uid}',
mapping=data)
raise colander.Invalid(node, ts)
@colander.deferred
def email_validator(node, kw):
return EmailValidator(kw['user'])
@colander.deferred
def username_validator(node, kw):
return UsernameValidator(kw['user'])
class AddSchema(colander.Schema):
email = colander.SchemaNode(
colander.String(), title=_('Email'), validator=email_validator)
user_name = colander.SchemaNode(colander.String(), title=_('Username'),
validator=username_validator)
groups = colander.SchemaNode(
colander.Set(), widget=group_widget, title=_('Group'))
class EditSchema(AddSchema):
id = colander.SchemaNode(
colander.String(), missing=colander.drop,
widget=HiddenWidget(readonly=True))
status = colander.SchemaNode(
colander.String(), widget=status_widget, title=_('Status'))
class MyEditSchema(AddSchema):
id = colander.SchemaNode(
colander.String(), missing=colander.drop,
widget=HiddenWidget(readonly=True))
def get_form(request, class_form, user=None):
status_list = (
(1, _('Active')),
(0, _('Inactive')))
group_list = []
q = DBSession.query(Group).order_by(Group.group_name)
for row in q:
group = (str(row.id), _(row.group_name))
group_list.append(group)
schema = class_form()
schema = schema.bind(
status_list=status_list, group_list=group_list, user=user)
btn_save = Button('save', _('Save'))
btn_cancel = Button('cancel', _('Cancel'))
return Form(schema, buttons=(btn_save, btn_cancel))
def add_member_count(gid):
q = DBSession.query(Group).filter_by(id=gid)
group = q.first()
group.member_count += 1
DBSession.add(group)
def reduce_member_count(gid):
q = DBSession.query(Group).filter_by(id=gid)
group = q.first()
group.member_count -= 1
DBSession.add(group)
def insert(request, values):
user = User()
user.email = values['email'].lower()
user.user_name = values['user_name'].lower()
user.security_code_date = create_now()
remain = regenerate_security_code(user)
DBSession.add(user)
DBSession.flush()
for gid in values['groups']:
ug = UserGroup(user_id=user.id, group_id=gid)
DBSession.add(ug)
add_member_count(gid)
return user, remain
@view_config(
route_name='user-add', renderer='templates/user/add.pt',
permission='user-edit')
def view_add(request):
form = get_form(request, AddSchema)
resp = dict(title=_('Add user'))
if not request.POST:
resp['form'] = form.render()
return resp
if 'save' not in request.POST:
return HTTPFound(location=request.route_url('user'))
items = request.POST.items()
try:
c = form.validate(items)
except ValidationFailure:
resp['form'] = form.render()
return resp
user, remain = insert(request, dict(c.items()))
send_email_security_code(
request, user, remain, 'Welcome new user', 'email-new-user',
'email-new-user.tpl')
data = dict(email=user.email)
ts = _(
'user-added',
default='${email} has been added and the email has been sent.',
mapping=data)
request.session.flash(ts)
return HTTPFound(location=request.route_url('user'))
########
# Edit #
########
def user_group_list(user):
q = DBSession.query(UserGroup).filter_by(user_id=user.id)
r = []
for ug in q:
r.append(str(ug.group_id))
return r
def update(request, user, values):
fnames = ['email', 'user_name']
user.email = values['email'].lower()
user.user_name = values['user_name'].lower()
if user.id != request.user.id:
user.status = values['status']
DBSession.add(user)
existing = set(user_group_list(user))
unused = existing - values['groups']
if unused:
q = DBSession.query(UserGroup).filter_by(user_id=user.id).filter(
UserGroup.group_id.in_(unused))
q.delete(synchronize_session=False)
for gid in unused:
reduce_member_count(gid)
new = values['groups'] - existing
for gid in new:
ug = UserGroup(user_id=user.id, group_id=gid)
DBSession.add(ug)
add_member_count(gid)
@view_config(
route_name='user-edit', renderer='templates/user/edit.pt',
permission='user-edit')
def view_edit(request):
q = DBSession.query(User).filter_by(id=request.matchdict['id'])
user = q.first()
if not user:
return HTTPNotFound()
cls = user.id == request.user.id and MyEditSchema or EditSchema
form = get_form(request, cls, user)
resp = dict(title=_('Edit user'))
if not request.POST:
d = user.to_dict()
d['groups'] = user_group_list(user)
resp['form'] = form.render(appstruct=d)
return resp
if 'save' not in request.POST:
return HTTPFound(location=request.route_url('user'))
items = request.POST.items()
try:
c = form.validate(items)
except ValidationFailure:
resp['form'] = form.render()
return resp
update(request, user, dict(c.items()))
data = dict(username=user.user_name)
ts = _('user-updated', default='${username} profile updated', mapping=data)
request.session.flash(ts)
return HTTPFound(location=request.route_url('user'))
##########
# Delete #
##########
@view_config(
route_name='user-delete', renderer='templates/user/delete.pt',
permission='user-edit')
def view_delete(request):
q = DBSession.query(User).filter_by(id=request.matchdict['id'])
user = q.first()
if not user:
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 user'), user=user, form=form.render())
if 'delete' not in request.POST:
return HTTPFound(location=request.route_url('user'))
gid_list = user_group_list(user)
for gid, x in gid_list:
reduce_member_count(gid)
data = dict(uid=user.id, email=user.email)
ts = _(
'user-deleted',
default='User ${email} ID ${uid} has been deleted',
mapping=data)
q.delete()
request.session.flash(ts)
return HTTPFound(location=request.route_url('user'))
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!