Commit d286170d by Owo Sugiana

Add user group management

1 parent b29c9cc3
0.1.3 2018-11-04
- Add user group management.
0.1.2 2018-10-29
----------------
......
......@@ -49,7 +49,7 @@ if sys.argv[1:] and sys.argv[1] == 'develop-use-pip':
setup(
name='web_starter',
version='0.1.2',
version='0.1.3',
description='Web Starter',
long_description=README + '\n\n' + CHANGES,
classifiers=[
......
......@@ -49,3 +49,9 @@ msgstr "Ubah kata kunci"
#: 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"
......@@ -128,3 +128,24 @@ 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"
......@@ -50,3 +50,9 @@ msgstr ""
#: ./views/templates/layout-menu.pt:57
msgid "username-logout"
msgstr ""
msgid "Users groups"
msgstr ""
msgid "Add users group"
msgstr ""
......@@ -129,3 +129,24 @@ 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 ""
......@@ -17,3 +17,11 @@ class CommonModel(object):
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
......@@ -28,7 +28,7 @@ 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):
class Group(GroupMixin, Base, CommonModel):
pass
class GroupPermission(GroupPermissionMixin, Base):
......
......@@ -11,3 +11,7 @@ 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
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'))
]
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'))
<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>
......@@ -39,12 +39,17 @@
<li tal:attributes="class request.path == '/' and 'active'">
<a href="/" i18n:translate="Home">Home</a></li>
<li class="dropdown" tal:attributes="class request.matched_route.name in ['user', 'user-add', 'user-edit', 'user-delete'] and 'active'">
<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>
......
<div metal:use-macro="load: ../layout-menu.pt">
<div metal:fill-slot="content" i18n:domain="user">
<h1 i18n:translate="Users">Users</h1>
<h1 i18n:translate="">Users</h1>
<form method="post" action="/user">
<table class="table table-striped table-hover">
<thead>
<tr>
<th i18n:translate="Email">Email</th>
<th i18n:translate="Username">Username</th>
<th i18n:translate="Status">Status</th>
<th i18n:translate="Last login">Last login</th>
<th i18n:translate="Registered date">Registered date</th>
<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>
......@@ -18,7 +55,7 @@
<tr tal:repeat="user users">
<td tal:content="user.email"/>
<td tal:content="user.user_name"/>
<td tal:condition="user.status" i18n:translate="Active">Active</td>
<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
......@@ -26,13 +63,13 @@
<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">Edit</a>
<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">Delete</a>
<a href="/user/${user.id}/delete" i18n:translate="">Delete</a>
</td>
<td tal:condition="user.id <= 1" i18n:translate="System">System</td>
<td tal:condition="user.id > 1 and user.id == request.user.id" i18n:translate="You">You</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>
......
import re
from email.utils import parseaddr
from sqlalchemy import func
from pyramid.view import view_config
from pyramid.httpexceptions import HTTPFound
from pyramid.httpexceptions import (
HTTPFound,
HTTPNotFound,
)
from pyramid.i18n import TranslationStringFactory
import colander
from deform import (
......@@ -35,12 +39,41 @@ _ = 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):
q = DBSession.query(User).order_by(User.email)
return dict(title=_('Users'), users=q)
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
#######
......@@ -59,19 +92,14 @@ def deferred_group(node, kw):
class Validator:
def __init__(self, request):
self.request = request
if 'id' in self.request.matchdict:
q = DBSession.query(User).filter_by(id=self.request.matchdict['id'])
self.user = q.first()
else:
self.user = None
def __init__(self, user):
self.user = user
class EmailValidator(colander.Email, Validator):
def __init__(self, request):
def __init__(self, user):
colander.Email.__init__(self)
Validator.__init__(self, request)
Validator.__init__(self, user)
def __call__(self, node, value):
if self.match_object.match(value) is None:
......@@ -95,15 +123,12 @@ REGEX_ONLY_CONTAIN = re.compile('([a-z0-9-]*)')
REGEX_BEGIN_END_ALPHANUMERIC = re.compile('^[a-z0-9]+(?:[-][a-z0-9]+)*$')
class UsernameValidator(Validator):
def __init__(self, request):
Validator.__init__(self, request)
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:
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')
......@@ -128,12 +153,12 @@ class UsernameValidator(Validator):
@colander.deferred
def deferred_email_validator(node, kw):
return EmailValidator(kw['request'])
return EmailValidator(kw['user'])
@colander.deferred
def deferred_username_validator(node, kw):
return UsernameValidator(kw['request'])
return UsernameValidator(kw['user'])
class AddSchema(colander.Schema):
......@@ -160,7 +185,7 @@ class MyEditSchema(AddSchema):
widget=HiddenWidget(readonly=True))
def get_form(request, class_form):
def get_form(request, class_form, user=None):
status_list = (
(1, _('Active')),
(0, _('Inactive')))
......@@ -171,13 +196,26 @@ def get_form(request, class_form):
group_list.append(group)
schema = class_form()
schema = schema.bind(
status_list=status_list, group_list=group_list, request=request)
schema.request = request
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()
......@@ -189,6 +227,7 @@ def insert(request, values):
for gid in values['groups']:
ug = UserGroup(user_id=user.id, group_id=gid)
DBSession.add(ug)
add_member_count(gid)
return user, remain
......@@ -233,7 +272,7 @@ def user_group_set(user):
return set(r)
def update(request, values, user):
def update(request, user, values):
fnames = ['email', 'user_name']
user.email = values['email'].lower()
user.user_name = values['user_name'].lower()
......@@ -244,12 +283,15 @@ def update(request, values, user):
unused = existing - values['groups']
if unused:
q = DBSession.query(UserGroup).filter_by(user_id=user.id).filter(
UserGroup.group_id.in_([2]))
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(
......@@ -261,9 +303,9 @@ def view_edit(request):
if not user:
return HTTPNotFound()
if user.id == request.user.id:
form = get_form(request, MyEditSchema)
form = get_form(request, MyEditSchema, user)
else:
form = get_form(request, EditSchema)
form = get_form(request, EditSchema, user)
resp = dict(title=_('Edit user'))
if not request.POST:
d = user.to_dict()
......@@ -278,7 +320,7 @@ def view_edit(request):
except ValidationFailure:
resp['form'] = form.render()
return resp
update(request, dict(c.items()), user)
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)
......@@ -296,13 +338,17 @@ def view_delete(request):
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)
if not request.POST:
return dict(title=_('Delete user'), user=user, form=form.render())
if 'delete' in request.POST:
if 'delete' not in request.POST:
return HTTPFound(location=request.route_url('user'))
gid_list = user_group_set(user)
for gid in gid_list:
reduce_member_count(gid)
data = dict(uid=user.id, email=user.email)
ts = _(
'user-deleted',
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!