Commit 0acf5321 by Owo Sugiana

user/list.pt filter form menggunakan Schema

1 parent d286170d
*.egg
*.egg-info *.egg-info
*.pyc *.pyc
*$py.class
*~ *~
.coverage
coverage.xml
build/
dist/
.tox/
nosetests.xml
env*/ env*/
tmp/ test*
Data.fs*
*.sublime-project
*.sublime-workspace
.*.sw?
.sw?
.DS_Store
coverage
test
0.1.3 2018-11-04 0.1.4 2019-02-01
----------------
- Versi otomatis sesuai CHANGES.txt.
- user/list.pt untuk filter kini menggunakan Schema.
0.1.3 2018-11-04
----------------
- Add user group management. - Add user group management.
0.1.2 2018-10-29 0.1.2 2018-10-29
---------------- ----------------
- Add user management. - Add user management.
- Add menu. - Add menu.
- Additional argument for setup.py develop-use-pip (ex. --proxy). - Additional argument for setup.py develop-use-pip (ex. --proxy).
0.1.1 0.1.1
----- -----
- Forgot password. - Forgot password.
0.1 0.1
--- ---
- Initial version. - Initial version.
Web Starter
===========
Change directory into your newly created project:
cd web-starter
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_starter_db development.ini
Run your project:
../env/bin/pserve --reload development.ini
Web Starter
===========
Change directory into your newly created project::
$ cd web-starter
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
Copy configuration file::
$ cp development.ini test.ini
Set ``sqlalchemy.url`` on ``test.ini`` and create tables::
$ ../env/bin/initialize_web_starter_db test.ini
Run your project::
$ ../env/bin/pserve --reload test.ini
Web Baru (Indonesian)
---------------------
Web Starter dirancang untuk disalin menjadi direktori baru. Misalkan Anda ingin
membuat aplikasi SMS dengan bekal ini maka salinlah direktorinya::
$ cd ..
$ cp -r web-starter web-sms
$ cd web-sms
$ rm -rf .git
$ rm .gitignore
$ mv web_starter web_sms
Carilah kata ``web_starter`` di seluruh file Python::
$ find -name "*.py" | xargs grep web_starter
Ubahlah filenya dan ganti ``web_starter`` menjadi ``web_sms``. Lakukan hal yang
sama pada file konfigurasi ``*.ini``.
Web Starter
===========
Getting Started
---------------
- Change directory into your newly created project.
cd web-starter
- 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_starter_db development.ini
- Run your project.
../env/bin/pserve --reload development.ini
...@@ -42,7 +42,7 @@ listen = localhost:6543 ...@@ -42,7 +42,7 @@ listen = localhost:6543
### ###
[loggers] [loggers]
keys = root, web_starter keys = root, sqlalchemy, web_starter
[handlers] [handlers]
keys = console keys = console
...@@ -54,6 +54,11 @@ keys = generic ...@@ -54,6 +54,11 @@ keys = generic
level = INFO level = INFO
handlers = console handlers = console
[logger_sqlalchemy]
level = INFO
handlers =
qualname = sqlalchemy.engine
[logger_web_starter] [logger_web_starter]
level = DEBUG level = DEBUG
handlers = handlers =
......
import os import os
import sys import sys
import subprocess import subprocess
from setuptools import setup, find_packages from setuptools import (
setup,
find_packages,
)
here = os.path.abspath(os.path.dirname(__file__)) here = os.path.abspath(os.path.dirname(__file__))
with open(os.path.join(here, 'README.txt')) as f: with open(os.path.join(here, 'README.rst')) as f:
README = f.read() README = f.read()
with open(os.path.join(here, 'CHANGES.txt')) as f: with open(os.path.join(here, 'CHANGES.txt')) as f:
CHANGES = f.read() CHANGES = f.read()
line = CHANGES.splitlines()[0]
version = line.split()[0]
requires = [ requires = [
'plaster_pastedeploy', 'plaster_pastedeploy',
'pyramid', 'pyramid',
...@@ -27,6 +34,10 @@ requires = [ ...@@ -27,6 +34,10 @@ requires = [
'pyramid_mailer', 'pyramid_mailer',
] ]
customs_require = [
# 'http://vpn.opensipkd.com/2019/01/opensipkd-jsonrpc-0.1.tgz',
]
tests_require = [ tests_require = [
'WebTest >= 1.3.1', # py3 compat 'WebTest >= 1.3.1', # py3 compat
'pytest', 'pytest',
...@@ -34,22 +45,41 @@ tests_require = [ ...@@ -34,22 +45,41 @@ tests_require = [
] ]
def run(cmd):
if subprocess.call(cmd) != 0:
sys.exit()
def pip_install(package, upgrade=False):
cmd = [pip, 'install']
if upgrade:
cmd += ['--upgrade']
if sys.argv[2:]:
option = sys.argv[2] # Bisa untuk proxy
cmd += [option]
cmd += [package]
run(cmd)
if sys.argv[1:] and sys.argv[1] == 'develop-use-pip': if sys.argv[1:] and sys.argv[1] == 'develop-use-pip':
bin_ = os.path.split(sys.executable)[0] bin_ = os.path.split(sys.executable)[0]
pip = os.path.join(bin_, 'pip') pip = os.path.join(bin_, 'pip')
for package in requires: pip_install('pip', True)
pip_install('setuptools', True)
requires_ = requires + customs_require
for package in requires_:
if sys.argv[2:]: if sys.argv[2:]:
cmd = [pip, 'install', sys.argv[2], package] cmd = [pip, 'install', sys.argv[2], package]
else: else:
cmd = [pip, 'install', package] cmd = [pip, 'install', package]
subprocess.call(cmd) run(cmd)
cmd = [sys.executable, sys.argv[0], 'develop'] cmd = [sys.executable, sys.argv[0], 'develop']
subprocess.call(cmd) subprocess.call(cmd)
sys.exit() sys.exit()
setup( setup(
name='web_starter', name='web_starter',
version='0.1.3', version=version,
description='Web Starter', description='Web Starter',
long_description=README + '\n\n' + CHANGES, long_description=README + '\n\n' + CHANGES,
classifiers=[ classifiers=[
......
.grid-container {
display: grid;
grid-template-columns: auto auto auto;
}
...@@ -17,6 +17,7 @@ ...@@ -17,6 +17,7 @@
<!-- Custom styles for this template --> <!-- Custom styles for this template -->
<link href="/static/signin.css" rel="stylesheet"> <link href="/static/signin.css" rel="stylesheet">
<div metal:define-slot="head"/>
</head> </head>
<body> <body>
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="pyramid web application"> <meta name="description" content="pyramid web application">
<meta name="author" content="Pylons Project"> <meta name="author" content="Pylons Project">
<link rel="shortcut icon" href="${request.static_url('web_starter:static/pyramid-16x16.png')}"> <link rel="shortcut icon" href="/static/pyramid-16x16.png">
<title>Web Starter - <span tal:replace="title"/></title> <title>Web Starter - <span tal:replace="title"/></title>
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
<link href="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet"> <link href="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet">
<!-- Custom styles for this scaffold --> <!-- Custom styles for this scaffold -->
<link href="${request.static_url('web_starter:static/theme.css')}" rel="stylesheet"> <link href="/static/theme.css" rel="stylesheet">
<!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries --> <!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries -->
<!--[if lt IE 9]> <!--[if lt IE 9]>
...@@ -32,7 +32,7 @@ ...@@ -32,7 +32,7 @@
<div class="row"> <div class="row">
<div class="col-md-2"> <div class="col-md-2">
<img class="logo img-responsive" <img class="logo img-responsive"
src="${request.static_url('web_starter:static/pyramid.png')}" alt="pyramid web framework"> src="/static/pyramid.png" alt="pyramid web framework">
</div> </div>
<div class="col-md-10"> <div class="col-md-10">
<div metal:define-slot="content">No content</div> <div metal:define-slot="content">No content</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:use-macro="load: ../layout-menu.pt">
<div metal:fill-slot="head">
<link href="/static/grid.css" rel="stylesheet"/>
</div>
<div metal:fill-slot="content" i18n:domain="user"> <div metal:fill-slot="content" i18n:domain="user">
<h1 i18n:translate="">Users</h1> <h1 i18n:translate="">Users</h1>
<div tal:content="structure form"/>
<form method="post" action="/user"> <div tal:condition="request.GET.get('show')">
<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="not count" i18n:translate="">No result</p>
<p tal:condition="count" i18n:translate="user-result"> <p tal:condition="count" i18n:translate="user-result">
<span tal:replace="count" i18n:name="count"/> rows <span tal:replace="count" i18n:name="count"/> rows
</p> </p>
<table class="table table-striped table-hover" tal:condition="count"> <table class="table table-striped table-hover" tal:condition="count">
<thead> <thead>
<tr> <tr>
...@@ -58,8 +28,7 @@ ...@@ -58,8 +28,7 @@
<td tal:condition="user.status" i18n:translate="">Active</td> <td tal:condition="user.status" i18n:translate="">Active</td>
<td tal:condition="not user.status"/> <td tal:condition="not user.status"/>
<td tal:condition="user.last_login_date" <td tal:condition="user.last_login_date"
tal:content="user.last_login_date.strftime('%d-%m-%Y tal:content="user.last_login_date.strftime('%d-%m-%Y %H:%M:%S %z')"/>
%H:%M:%S %z')"/>
<td tal:condition="not user.last_login_date"/> <td tal:condition="not user.last_login_date"/>
<td tal:content="user.registered_date.strftime('%d-%m-%Y %H:%M:%S %z')"/> <td tal:content="user.registered_date.strftime('%d-%m-%Y %H:%M:%S %z')"/>
<td> <td>
...@@ -74,5 +43,5 @@ ...@@ -74,5 +43,5 @@
</tbody> </tbody>
</table> </table>
</div> </div>
</div>
</div> </div>
import os
import re import re
from email.utils import parseaddr from email.utils import parseaddr
from sqlalchemy import func from pkg_resources import resource_filename
from sqlalchemy import (
func,
or_,
)
from pyramid.view import view_config from pyramid.view import view_config
from pyramid.httpexceptions import ( from pyramid.httpexceptions import (
HTTPFound, HTTPFound,
...@@ -12,6 +17,7 @@ from deform import ( ...@@ -12,6 +17,7 @@ from deform import (
Form, Form,
ValidationFailure, ValidationFailure,
Button, Button,
ZPTRendererFactory,
) )
from deform.widget import ( from deform.widget import (
SelectWidget, SelectWidget,
...@@ -39,39 +45,95 @@ _ = TranslationStringFactory('user') ...@@ -39,39 +45,95 @@ _ = TranslationStringFactory('user')
######## ########
# List # # List #
######## ########
@colander.deferred
def group_widget(node, kw):
return SelectWidget(values=kw['group_list'])
class FilterSchema(colander.Schema):
group = colander.SchemaNode(
colander.String(), missing=colander.drop, widget=group_widget)
name = colander.SchemaNode(colander.String(), missing=colander.drop)
def get_group_list():
r = []
q = DBSession.query(Group).order_by(Group.group_name)
for row in q:
group = (str(row.id), _(row.group_name))
r.append(group)
return r
deform_templates = resource_filename('deform', 'templates')
here = os.path.abspath(os.path.dirname(__file__))
my_templates = os.path.join(here, 'templates', 'user')
search_path = [my_templates, deform_templates]
my_renderer = ZPTRendererFactory(search_path)
def query_filter(request, q):
return q.filter( def get_filter_form():
group_list = get_group_list()
group_list = [('', '')] + group_list
schema = FilterSchema()
schema = schema.bind(group_list=group_list)
btn_show = Button('show', _('Show'))
buttons = (btn_show,)
return Form(schema, buttons=buttons, renderer=my_renderer)
def query_filter(q, p):
if 'group' in p:
q = q.filter(
User.id==UserGroup.user_id, User.id==UserGroup.user_id,
UserGroup.group_id==request.GET['gid']) UserGroup.group_id==p['group'])
if 'name' in p:
pattern = '%{}%'.format(p['name'])
q = q.filter(
or_(
User.email.ilike(pattern),
User.user_name.ilike(pattern)))
return q
def get_filter(request):
p = dict()
if request.GET.get('group'):
try:
p['group'] = int(request.GET.get('group'))
except ValueError:
pass
if request.GET.get('name'):
p['name'] = request.GET.get('name')
return p
@view_config( @view_config(
route_name='user', renderer='templates/user/list.pt', route_name='user', renderer='templates/user/list.pt',
permission='user-edit') permission='user-edit')
def view_list(request): def view_list(request):
form = get_filter_form()
resp = dict(title=_('Users'))
if request.POST: if request.POST:
p = dict(gid=request.POST['gid']) items = request.POST.items()
return HTTPFound(location=request.route_url('user', _query=p))
if 'gid' in request.GET and request.GET['gid']:
try: try:
int(request.GET['gid']) c = form.validate(items)
except ValueError: except ValidationFailure as e:
return HTTPNotFound() resp['form'] = e.render()
return resp
p = dict(c.items())
p['show'] = 1
return HTTPFound(location=request.route_url('user', _query=p))
p = get_filter(request)
resp['form'] = form.render(appstruct=p)
if 'show' not in request.GET:
return resp
q_count = DBSession.query(func.count()) q_count = DBSession.query(func.count())
q_count = query_filter(request, q_count) q_count = query_filter(q_count, p)
count = q_count.scalar() resp['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: if count:
q_user = DBSession.query(User) q_user = DBSession.query(User)
q_group = DBSession.query(Group).order_by(Group.group_name) q_user = query_filter(q_user, p)
resp = dict(title=_('Users'), count=count, groups=q_group)
if count:
resp['users'] = q_user.order_by(User.email) resp['users'] = q_user.order_by(User.email)
return resp return resp
...@@ -189,11 +251,7 @@ def get_form(request, class_form, user=None): ...@@ -189,11 +251,7 @@ def get_form(request, class_form, user=None):
status_list = ( status_list = (
(1, _('Active')), (1, _('Active')),
(0, _('Inactive'))) (0, _('Inactive')))
group_list = [] group_list = get_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 = class_form()
schema = schema.bind( schema = schema.bind(
status_list=status_list, group_list=group_list, user=user) status_list=status_list, group_list=group_list, user=user)
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!