Browse Source

crud for consumables

php2python
Holger Frey 7 years ago
parent
commit
abb0a6afc1
  1. 8
      ordr2/resources/__init__.py
  2. 6
      ordr2/resources/admin.py
  3. 70
      ordr2/schemas/orders.py
  4. 1
      ordr2/static/css/style.css
  5. 70
      ordr2/templates/admin/consumable_delete.jinja2
  6. 24
      ordr2/templates/admin/consumable_edit.jinja2
  7. 72
      ordr2/templates/admin/consumable_list.jinja2
  8. 24
      ordr2/templates/admin/consumable_new.jinja2
  9. 2
      ordr2/templates/admin/user_edit.jinja2
  10. 2
      ordr2/templates/admin/users_change_roles.jinja2
  11. 2
      ordr2/templates/admin/users_delete.jinja2
  12. 7
      ordr2/templates/layout.jinja2
  13. 3
      ordr2/templates/macros.jinja2
  14. 166
      ordr2/views/admin.py

8
ordr2/resources/__init__.py

@ -1,7 +1,13 @@
from pyramid.security import Allow, Everyone from pyramid.security import Allow, Everyone
from .account import Account, PasswordResetAccount from .account import Account, PasswordResetAccount
from .admin import Admin, UserList, UserAccount from .admin import (
Admin,
ConsumableList,
ConsumableResource,
UserList,
UserAccount
)
from .base import BaseResource from .base import BaseResource

6
ordr2/resources/admin.py

@ -87,6 +87,7 @@ class ConsumableList(BaseResource, PaginationResourceMixin):
def __acl__(self): def __acl__(self):
return [ return [
(Allow, 'role:admin', 'view'), (Allow, 'role:admin', 'view'),
(Allow, 'role:admin', 'create'),
(Allow, 'role:admin', 'edit'), (Allow, 'role:admin', 'edit'),
(Allow, 'role:admin', 'delete'), (Allow, 'role:admin', 'delete'),
DENY_ALL DENY_ALL
@ -98,7 +99,7 @@ class ConsumableList(BaseResource, PaginationResourceMixin):
query = dbsession.query(self.sql_model_class) query = dbsession.query(self.sql_model_class)
category_name = filter_params.get('category', None) category_name = filter_params.get('category', None)
try: try:
category_name = cat_name.lower() category_name = category_name.lower()
category = Category(category_name) category = Category(category_name)
query = query.filter_by(category=category) query = query.filter_by(category=category)
except (AttributeError, ValueError): except (AttributeError, ValueError):
@ -132,7 +133,8 @@ class ConsumableList(BaseResource, PaginationResourceMixin):
class Admin(BaseResource): class Admin(BaseResource):
nodes = { nodes = {
'users': UserList 'users': UserList,
'consumables': ConsumableList,
} }
def __acl__(self): def __acl__(self):

70
ordr2/schemas/orders.py

@ -0,0 +1,70 @@
import colander
import deform
from ordr2.models import Category
from . import CSRFSchema
CATEGORIES = [(c.name, c.value.capitalize()) for c in Category]
# schema for user registration
class ConsumableSchema(CSRFSchema):
''' edit or add consumable '''
cas_description = colander.SchemaNode(
colander.String()
)
category = colander.SchemaNode(
colander.String(),
widget=deform.widget.SelectWidget(values=CATEGORIES)
)
catalog_nr = colander.SchemaNode(
colander.String()
)
vendor = colander.SchemaNode(
colander.String()
)
package_size = colander.SchemaNode(
colander.String()
)
unit_price = colander.SchemaNode(
colander.Decimal(),
widget=deform.widget.MoneyInputWidget()
)
currency = colander.SchemaNode(
colander.String(),
default='EUR'
)
comment = colander.SchemaNode(
colander.String(),
widget=deform.widget.TextAreaWidget(rows=5),
missing=''
)
@classmethod
def as_form(cls, request, **override):
is_new_consumable = override.pop('is_new_consumable', False)
if is_new_consumable:
settings = {
'buttons': (
deform.Button(name='save', title='Add Consumable'),
deform.Button(name='cancel', title='Cancel')
),
'css_class': 'form-horizontal',
}
else:
settings = {
'buttons': (
deform.Button(name='save', title='Save changes'),
deform.Button(
name='delete',
title='Delete Consumable',
css_class='btn-danger'
),
deform.Button(name='cancel', title='Cancel')
),
'css_class': 'form-horizontal',
}
settings.update(override)
return super().as_form(request, **settings)

1
ordr2/static/css/style.css

@ -732,3 +732,4 @@ input[value="new_password:mapping"] + div { margin-bottom:10px; }
margin-bottom: 20px; margin-bottom: 20px;
border-bottom: 1px solid #aaa;} border-bottom: 1px solid #aaa;}
div.alert a { color:inherit; text-decoration:underline; } div.alert a { color:inherit; text-decoration:underline; }
td.column-pkg, td.column-price { text-align:right; }

70
ordr2/templates/admin/consumable_delete.jinja2

@ -0,0 +1,70 @@
{% extends "ordr2:templates/layout.jinja2" %}
{% import 'ordr2:templates/macros.jinja2' as macros with context %}
{% block subtitle %} Admin | Consumable | Confirm Delete {% endblock subtitle %}
{% block content %}
<div class="content controls">
<div class="container-fluid">
<div class="row-fluid">
<div class="page-controls">
<h1>Delete Consumable{{ 's' if consumables|length > 1 }}</h1>
</div>
</div>
<div class="row">
<div class="span10">
<div class="action-header">
<h3>The following consumable{{ 's' if consumables|length > 1 }} will be deleted:</h3>
</div>
<form action="{{ request.resource_url(context, 'delete') }}" method="POST" class="action">
<input type="hidden" name="csrf_token" value="{{get_csrf_token()}}">
<table class="table">
<thead>
<th>Cas / Description</th>
<th>Category</th>
<th>Catalog Nr</th>
<th>Vendor</th>
<th>Package Size</th>
<th>Unit Prize</th>
<th>Currency</th>
</thead>
<tbody>
{% for consumable in consumables %}
<tr>
<td class="column-user">
<input type="hidden" name="consumable" value="{{ consumable.id }}">
{{ consumable.cas_description }}
</td>
<td class="column-category">{{ consumable.category.name|capitalize }}</td>
<td class="column-catalog">{{ consumable.catalog_nr }}</td>
<td class="column-vendor">{{ consumable.vendor }}</td>
<td class="column-pkg">{{ consumable.package_size }}</td>
<td class="column-price">{{ '%.2f'|format(consumable.unit_price) }}</td>
<td class="column-currency">{{ consumable.currency }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<fieldset class="form-actions">
<div class="right">
<button name="delete" type="submit" value="submit" class="btn btn-large btn-danger">Delete Consumable{{ 's' if consumables|length > 1 }}</button>
<button name="cancel" type="submit" value="cancel" class="btn btn-large">Cancel</button>
</div>
</fieldset>
</form>
</div>
</div>
</div>
</div>
{% endblock content %}

24
ordr2/templates/admin/consumable_edit.jinja2

@ -0,0 +1,24 @@
{% extends "ordr2:templates/layout.jinja2" %}
{% import 'ordr2:templates/macros.jinja2' as macros with context %}
{% block subtitle %} Admin | Consumable | {{ context.model.cas_description }} {% endblock subtitle %}
{% block content %}
<div class="content controls">
<div class="container-fluid">
<div class="row-fluid">
<div class="page-controls">
<h1>Edit Consumable: {{ context.model.cas_description }}</h1>
</div>
</div>
<div class="row">
<div class="span8">
{{ macros.flash_messages() }}
{{form.render()|safe}}
</div>
</div>
</div>
</div>
{% endblock content %}

72
ordr2/templates/admin/consumable_list.jinja2

@ -0,0 +1,72 @@
{% extends "ordr2:templates/layout.jinja2" %}
{% import 'ordr2:templates/macros.jinja2' as macros with context %}
{% block subtitle %} Admin | Consumables {% endblock subtitle %}
{% block content %}
<div class="content controls">
<div class="container-fluid controls">
<div class="row-fluid">
<div class="span2">
<div class="page-controls">
<h1>
Consumables
</h1>
</div>
{{ macros.filter_box('Category', 'category', categories) }}
</div>
<div class="span10">
<div class="page-controls">
<div class="actions">
<a href="{{ request.resource_url(context, 'new') }}" rel="tooltip" data-original-title="New" class="btn-flat single"><i class="add"></i></a>
</div>
</div>
{{ macros.flash_messages() }}
{% if consumables %}
<table class="table">
<thead>
{{ macros.sortable_table_header('Cas / Description', 'cas') }}
{{ macros.sortable_table_header('Category', 'category') }}
{{ macros.sortable_table_header('Catalog Nr', 'catalog') }}
{{ macros.sortable_table_header('Vendor', 'vendor') }}
{{ macros.sortable_table_header('Package Size', 'pkg') }}
{{ macros.sortable_table_header('Unit Prize', 'price') }}
{{ macros.sortable_table_header('Currency', 'currency') }}
<th>Actions</th>
</thead>
<tbody>
{% for consumable in consumables %}
<tr>
<td class="column-cas">{{ consumable.model.cas_description }}</td>
<td class="column-category">{{ consumable.model.category.name|capitalize }}</td>
<td class="column-catalog">{{ consumable.model.catalog_nr }}</td>
<td class="column-vendor">{{ consumable.model.vendor }}</td>
<td class="column-pkg">{{ consumable.model.package_size }}</td>
<td class="column-price">{{ '%.2f'|format(consumable.model.unit_price) }}</td>
<td class="column-currency">{{ consumable.model.currency }}</td>
<td>
<a href="{{ request.resource_url(consumable) }}" class="action edit" title="Edit Consumable">edit</a>
<a href="{{ request.resource_url(consumable, 'delete') }}" class="action delete" title="Delete Consumable">delete</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{{ macros.pagination() }}
{% else %}
<div class="alert alert-block alert-error">
<h4 class="alert-heading">Oh snap! Nothing to display!</h4>
<p>Your query didn't return any data.</p>
</div>
{% endif %}
</div>
</div>
</div>
</div>
{% endblock content %}

24
ordr2/templates/admin/consumable_new.jinja2

@ -0,0 +1,24 @@
{% extends "ordr2:templates/layout.jinja2" %}
{% import 'ordr2:templates/macros.jinja2' as macros with context %}
{% block subtitle %} Admin | Consumable | Add {% endblock subtitle %}
{% block content %}
<div class="content controls">
<div class="container-fluid">
<div class="row-fluid">
<div class="page-controls">
<h1>Add Consumable</h1>
</div>
</div>
<div class="row">
<div class="span8">
{{ macros.flash_messages() }}
{{form.render()|safe}}
</div>
</div>
</div>
</div>
{% endblock content %}

2
ordr2/templates/admin/user_edit.jinja2

@ -1,7 +1,7 @@
{% extends "ordr2:templates/layout.jinja2" %} {% extends "ordr2:templates/layout.jinja2" %}
{% import 'ordr2:templates/macros.jinja2' as macros with context %} {% import 'ordr2:templates/macros.jinja2' as macros with context %}
{% block subtitle %} Account | Admin | User | {{ context.model.user_name }} {% endblock subtitle %} {% block subtitle %} Admin | User | {{ context.model.user_name }} {% endblock subtitle %}
{% block content %} {% block content %}
<div class="content controls"> <div class="content controls">

2
ordr2/templates/admin/users_change_roles.jinja2

@ -1,7 +1,7 @@
{% extends "ordr2:templates/layout.jinja2" %} {% extends "ordr2:templates/layout.jinja2" %}
{% import 'ordr2:templates/macros.jinja2' as macros with context %} {% import 'ordr2:templates/macros.jinja2' as macros with context %}
{% block subtitle %} Account | Admin | Users | Change Roles {% endblock subtitle %} {% block subtitle %} Admin | Users | Change Roles {% endblock subtitle %}
{% block content %} {% block content %}
<div class="content controls"> <div class="content controls">

2
ordr2/templates/admin/users_delete.jinja2

@ -1,7 +1,7 @@
{% extends "ordr2:templates/layout.jinja2" %} {% extends "ordr2:templates/layout.jinja2" %}
{% import 'ordr2:templates/macros.jinja2' as macros with context %} {% import 'ordr2:templates/macros.jinja2' as macros with context %}
{% block subtitle %} Account | Admin | Users | Confirm Delete {% endblock subtitle %} {% block subtitle %} Admin | Users | Confirm Delete {% endblock subtitle %}
{% block content %} {% block content %}
<div class="content controls"> <div class="content controls">

7
ordr2/templates/layout.jinja2

@ -18,7 +18,9 @@
#wrap {display:table;height:100%} #wrap {display:table;height:100%}
</style> </style>
<![endif]--> <![endif]-->
<script src="{{request.static_url('deform:static/scripts/jquery-2.0.3.min.js')}}"></script>
<script src="{{request.static_url('deform:static/scripts/deform.js')}}"></script>
<script src="{{request.static_url('deform:static/scripts/jquery.maskMoney-1.4.1.js')}}"></script>
</head> </head>
<body class="{{ ''.join(request.traversed)}} {{request.view_name}}"> <body class="{{ ''.join(request.traversed)}} {{request.view_name}}">
<header class="navbar navbar-fixed-top"> <header class="navbar navbar-fixed-top">
@ -71,8 +73,7 @@
</div> </div>
</footer> </footer>
<script src="{{request.static_url('ordr2:static/js/jquery.js')}}"></script> <!--<script src="{{request.static_url('ordr2:static/js/bootstrap-transition.js')}}"></script>-->
<script src="{{request.static_url('ordr2:static/js/bootstrap-transition.js')}}"></script>
<script src="{{request.static_url('ordr2:static/js/bootstrap-dropdown.js')}}"></script> <script src="{{request.static_url('ordr2:static/js/bootstrap-dropdown.js')}}"></script>
<script src="{{request.static_url('ordr2:static/js/bootstrap-modal.js')}}"></script> <script src="{{request.static_url('ordr2:static/js/bootstrap-modal.js')}}"></script>
<script src="{{request.static_url('ordr2:static/js/bootstrap-alert.js')}}"></script> <script src="{{request.static_url('ordr2:static/js/bootstrap-alert.js')}}"></script>

3
ordr2/templates/macros.jinja2

@ -47,10 +47,11 @@
{% endif %} {% endif %}
{%- endmacro %} {%- endmacro %}
{% macro sortable_table_header(title, sort_by, column_class=None) -%} {% macro sortable_table_header(title, sort_by, column_class=None) -%}
{% set column_class = column_class or 'column-' + sort_by %} {% set column_class = column_class or 'column-' + sort_by %}
<th class="sortable blue header {{ column_class }} {% if sort_by == context.sorting.field %} active {{ 'headerSortUp' if context.sorting.direction == 'asc' else 'headerSortDown' }} {% endif %} "> <th class="sortable blue header {{ column_class }} {% if sort_by == context.sorting.field %} active {{ 'headerSortUp' if context.sorting.direction == 'asc' else 'headerSortDown' }} {% endif %} ">
{% set new_direction = 'desc' if context.sorting.direction == 'asc' else 'asc' %} {% set new_direction = 'desc' if context.sorting.direction == 'asc' and sort_by == context.sorting.field else 'asc' %}
{% set new_sort = sort_by + '.' + new_direction %} {% set new_sort = sort_by + '.' + new_direction %}
<a href="{{ context.url( (context.query_key_sorting, new_sort), (context.query_key_current_page, 1) ) }}">{{ title }}</a> <a href="{{ context.url( (context.query_key_sorting, new_sort), (context.query_key_current_page, 1) ) }}">{{ title }}</a>
</th> </th>

166
ordr2/views/admin.py

@ -6,12 +6,13 @@ from pyramid.security import remember, forget
from pyramid.view import view_config from pyramid.view import view_config
from ordr2.events import AccountActivation, PasswordReset from ordr2.events import AccountActivation, PasswordReset
from ordr2.models import User, Role from ordr2.models import Category, Consumable, User, Role
from ordr2.schemas.account import UserSchema from ordr2.schemas.account import UserSchema
from ordr2.schemas.orders import ConsumableSchema
from . import update_column_display from . import update_column_display
# user log in and log out # admin section
@view_config( @view_config(
context='ordr2:resources.Admin', context='ordr2:resources.Admin',
@ -36,6 +37,8 @@ def admin_section(context, request):
return {} return {}
# user list and user editing
@view_config( @view_config(
context='ordr2:resources.UserList', context='ordr2:resources.UserList',
permission='view', permission='view',
@ -253,3 +256,162 @@ def user_delete_form_processing(context, request):
return HTTPFound(request.resource_url(request.root, 'admin', 'users')) return HTTPFound(request.resource_url(request.root, 'admin', 'users'))
# consumables
@view_config(
context='ordr2:resources.ConsumableList',
permission='view',
renderer='ordr2:templates/admin/consumable_list.jinja2'
)
def consumable_list(context, request):
''' display the consumable list '''
consumables = context.items()
categories = [(c.value.lower(), c.value.capitalize()) for c in Category]
return {'consumables': consumables, 'categories': categories}
@view_config(
context='ordr2:resources.ConsumableList',
name='new',
permission='create',
request_method='GET',
renderer='ordr2:templates/admin/consumable_new.jinja2'
)
def consumable_new_form(context, request):
''' display the new consumable form '''
form = ConsumableSchema.as_form(request, is_new_consumable=True)
return {'form': form}
@view_config(
context='ordr2:resources.ConsumableList',
name='new',
permission='create',
request_method='POST',
renderer='ordr2:templates/admin/consumable_new.jinja2'
)
def consumable_new_form_processing(context, request):
''' process the new consumable form '''
form = ConsumableSchema.as_form(request, is_new_consumable=True)
data = request.POST.items()
if 'save' in request.POST:
try:
appstruct = form.validate(data)
except deform.ValidationFailure as e:
return {'form': form}
# form validation sucessful, change consumable
consumable = Consumable(
cas_description=appstruct['cas_description'],
category=Category[appstruct['category']],
vendor=appstruct['vendor'],
catalog_nr=appstruct['catalog_nr'],
package_size=appstruct['package_size'],
unit_price=appstruct['unit_price'],
currency=appstruct['currency'],
comment=appstruct['comment']
)
request.dbsession.add(consumable)
msg = 'Consumable <em>{!s}</em> added.'.format(consumable)
request.flash('success', msg)
return HTTPFound(context.url())
@view_config(
context='ordr2:resources.ConsumableResource',
permission='edit',
request_method='GET',
renderer='ordr2:templates/admin/consumable_edit.jinja2'
)
def consumable_edit_form(context, request):
''' display the consumable edit form '''
form = ConsumableSchema.as_form(request, is_new_consumable=False)
form_data = {
'cas_description': context.model.cas_description,
'category': context.model.category.name,
'vendor': context.model.vendor,
'catalog_nr': context.model.catalog_nr,
'package_size': context.model.package_size,
'unit_price': context.model.unit_price,
'currency': context.model.currency,
'comment': context.model.comment
}
form.set_appstruct(form_data)
return {'form': form}
@view_config(
context='ordr2:resources.ConsumableResource',
permission='edit',
request_method='POST',
renderer='ordr2:templates/admin/consumable_edit.jinja2'
)
def consumable_edit_form_processing(context, request):
''' process the consumable edit form '''
form = ConsumableSchema.as_form(request, is_new_consumable=False)
data = request.POST.items()
if 'save' in request.POST:
try:
appstruct = form.validate(data)
except deform.ValidationFailure as e:
return {'form': form}
# form validation sucessful, change consumable
context.model.cas_description = appstruct['cas_description']
context.model.category = Category[appstruct['category']]
context.model.vendor = appstruct['vendor']
context.model.catalog_nr = appstruct['catalog_nr']
context.model.package_size = appstruct['package_size']
context.model.unit_price = appstruct['unit_price']
context.model.currency = appstruct['currency']
context.model.comment = appstruct['comment']
msg = 'Consumable <em>{!s}</em> updated.'.format(context.model)
request.flash('success', msg)
elif 'delete' in request.POST and context.model:
return HTTPFound(request.resource_url(context, 'delete'))
return HTTPFound(context.__parent__.url())
@view_config(
context='ordr2:resources.ConsumableResource',
name='delete',
permission='delete',
request_method='GET',
renderer='ordr2:templates/admin/consumable_delete.jinja2'
)
def consumable_delete_form(context, request):
return {'consumables': [context.model]}
@view_config(
context='ordr2:resources.ConsumableResource',
name='delete',
permission='delete',
request_method='POST'
)
def consumable_delete_form_processing(context, request):
if 'delete' in request.POST:
c_ids = [v for k, v in request.POST.items() if k == 'consumable']
consumables = request.dbsession.\
query(Consumable).\
filter(Consumable.id.in_(c_ids)).\
all()
for consumable in consumables:
request.dbsession.delete(consumable)
if len(consumables) == 1:
request.flash('success', 'One consumable was deleted')
elif len(consumables) > 1:
msg = '{} consumables were deleted.'.format(len(accounts))
request.flash('success', msg)
return HTTPFound(context.__parent__.url())