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

6
ordr2/resources/admin.py

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

70
ordr2/schemas/orders.py

@ -0,0 +1,70 @@ @@ -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; } @@ -732,3 +732,4 @@ input[value="new_password:mapping"] + div { margin-bottom:10px; }
margin-bottom: 20px;
border-bottom: 1px solid #aaa;}
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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -1,7 +1,7 @@
{% extends "ordr2:templates/layout.jinja2" %}
{% 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 %}
<div class="content controls">

2
ordr2/templates/admin/users_change_roles.jinja2

@ -1,7 +1,7 @@ @@ -1,7 +1,7 @@
{% extends "ordr2:templates/layout.jinja2" %}
{% 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 %}
<div class="content controls">

2
ordr2/templates/admin/users_delete.jinja2

@ -1,7 +1,7 @@ @@ -1,7 +1,7 @@
{% extends "ordr2:templates/layout.jinja2" %}
{% 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 %}
<div class="content controls">

7
ordr2/templates/layout.jinja2

@ -18,7 +18,9 @@ @@ -18,7 +18,9 @@
#wrap {display:table;height:100%}
</style>
<![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>
<body class="{{ ''.join(request.traversed)}} {{request.view_name}}">
<header class="navbar navbar-fixed-top">
@ -71,8 +73,7 @@ @@ -71,8 +73,7 @@
</div>
</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-modal.js')}}"></script>
<script src="{{request.static_url('ordr2:static/js/bootstrap-alert.js')}}"></script>

3
ordr2/templates/macros.jinja2

@ -47,10 +47,11 @@ @@ -47,10 +47,11 @@
{% endif %}
{%- endmacro %}
{% macro sortable_table_header(title, sort_by, column_class=None) -%}
{% 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 %} ">
{% 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 %}
<a href="{{ context.url( (context.query_key_sorting, new_sort), (context.query_key_current_page, 1) ) }}">{{ title }}</a>
</th>

166
ordr2/views/admin.py

@ -6,12 +6,13 @@ from pyramid.security import remember, forget @@ -6,12 +6,13 @@ from pyramid.security import remember, forget
from pyramid.view import view_config
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.orders import ConsumableSchema
from . import update_column_display
# user log in and log out
# admin section
@view_config(
context='ordr2:resources.Admin',
@ -36,6 +37,8 @@ def admin_section(context, request): @@ -36,6 +37,8 @@ def admin_section(context, request):
return {}
# user list and user editing
@view_config(
context='ordr2:resources.UserList',
permission='view',
@ -253,3 +256,162 @@ def user_delete_form_processing(context, request): @@ -253,3 +256,162 @@ def user_delete_form_processing(context, request):
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())