Browse Source

added search for orders, users and consumables

php2python
Holger Frey 7 years ago
parent
commit
d9d630a2af
  1. 14
      ordr2/models/orders.py
  2. 31
      ordr2/resources/admin.py
  3. 16
      ordr2/resources/orders.py
  4. 11
      ordr2/templates/admin/consumable_list.jinja2
  5. 4
      ordr2/templates/admin/user_list.jinja2
  6. 8
      ordr2/templates/orders/list.jinja2
  7. 30
      ordr2/views/admin.py
  8. 83
      ordr2/views/orders.py
  9. 1
      setup.py

14
ordr2/models/orders.py

@ -6,7 +6,7 @@ from collections import namedtuple @@ -6,7 +6,7 @@ from collections import namedtuple
from datetime import datetime
from sqlalchemy import (
Column,
Date,
DateTime,
Enum,
Float,
Integer,
@ -47,9 +47,9 @@ class Consumable(Base): @@ -47,9 +47,9 @@ class Consumable(Base):
unit_price = Column(Float, nullable=False)
currency = Column(Text, nullable=False, default='EUR')
comment = Column(Text, nullable=False, default='')
date_created = Column(Date, nullable=False, default=datetime.utcnow)
date_created = Column(DateTime, nullable=False, default=datetime.utcnow)
date_modified = Column(
Date,
DateTime,
nullable=False,
default=datetime.utcnow,
onupdate=datetime.utcnow
@ -83,13 +83,13 @@ class Order(Base): @@ -83,13 +83,13 @@ class Order(Base):
account = Column(Text, nullable=False, default='')
comment = Column(Text, nullable=False, default='')
created_date = Column(Date, nullable=False, default=datetime.utcnow)
created_date = Column(DateTime, nullable=False, default=datetime.utcnow)
created_by = Column(Text, nullable=False)
approval_date = Column(Date, nullable=True)
approval_date = Column(DateTime, nullable=True)
approval_by = Column(Text, nullable=False, default='')
ordered_date = Column(Date, nullable=True)
ordered_date = Column(DateTime, nullable=True)
ordered_by = Column(Text, nullable=False, default='')
completed_date = Column(Date, nullable=True)
completed_date = Column(DateTime, nullable=True)
completed_by = Column(Text, nullable=False, default='')

31
ordr2/resources/admin.py

@ -1,3 +1,5 @@ @@ -1,3 +1,5 @@
from sqlalchemy import or_
from pyramid.security import Allow, Authenticated, Deny, DENY_ALL, Everyone
from .base import BaseResource, PaginationResourceMixin
@ -34,6 +36,7 @@ class UserList(BaseResource, PaginationResourceMixin): @@ -34,6 +36,7 @@ class UserList(BaseResource, PaginationResourceMixin):
def prepare_filtered_query(self, dbsession, filter_params):
''' setup the base filtered query '''
query = dbsession.query(self.sql_model_class)
role_name = filter_params.get('role', None)
try:
role_name = role_name.lower()
@ -42,6 +45,20 @@ class UserList(BaseResource, PaginationResourceMixin): @@ -42,6 +45,20 @@ class UserList(BaseResource, PaginationResourceMixin):
except (AttributeError, ValueError):
role_name = None
self.filters['role'] = role_name
search = filter_params.get('search', None)
if search:
term = '%{}%'.format(search)
query = query.filter(
or_(
self.sql_model_class.user_name.ilike(term),
self.sql_model_class.first_name.ilike(term),
self.sql_model_class.last_name.ilike(term),
self.sql_model_class.email.ilike(term)
)
)
self.filters['search'] = search
return query
@ -98,6 +115,7 @@ class ConsumableList(BaseResource, PaginationResourceMixin): @@ -98,6 +115,7 @@ class ConsumableList(BaseResource, PaginationResourceMixin):
def prepare_filtered_query(self, dbsession, filter_params):
''' setup the base filtered query '''
query = dbsession.query(self.sql_model_class)
category_name = filter_params.get('category', None)
try:
category_name = category_name.lower()
@ -106,6 +124,19 @@ class ConsumableList(BaseResource, PaginationResourceMixin): @@ -106,6 +124,19 @@ class ConsumableList(BaseResource, PaginationResourceMixin):
except (AttributeError, ValueError):
category_name = None
self.filters['category'] = category_name
search = filter_params.get('search', None)
if search:
term = '%{}%'.format(search)
query = query.filter(
or_(
self.sql_model_class.cas_description.ilike(term),
self.sql_model_class.vendor.ilike(term),
self.sql_model_class.catalog_nr.ilike(term)
)
)
self.filters['search'] = search
return query

16
ordr2/resources/orders.py

@ -1,3 +1,5 @@ @@ -1,3 +1,5 @@
from sqlalchemy import or_
from pyramid.security import Allow, Authenticated, Deny, DENY_ALL, Everyone
from .base import BaseResource, PaginationResourceMixin
@ -54,6 +56,20 @@ class OrderList(BaseResource, PaginationResourceMixin): @@ -54,6 +56,20 @@ class OrderList(BaseResource, PaginationResourceMixin):
query = query.filter_by(created_by=user_name)
self.filters['user'] = user_name
search = filter_params.get('search', None)
if search:
term = '%{}%'.format(search)
query = query.filter(
or_(
self.sql_model_class.cas_description.ilike(term),
self.sql_model_class.vendor.ilike(term),
self.sql_model_class.catalog_nr.ilike(term),
self.sql_model_class.account.ilike(term),
self.sql_model_class.created_by.ilike(term)
)
)
self.filters['search'] = search
return query

11
ordr2/templates/admin/consumable_list.jinja2

@ -14,11 +14,20 @@ @@ -14,11 +14,20 @@
Consumables
</h1>
</div>
{{ macros.filter_box('Category', 'category', categories) }}
{{ macros.filter_box('Category', 'category', categories, {'search':None}) }}
</div>
<div class="span10">
<div class="page-controls">
<form action="{{ request.resource_url(context, 'actions') }}" method="POST">
<input type="hidden" name="csrf_token" value="{{get_csrf_token()}}">
<div class="input-append search">
<input type="search" name="search" size="30" placeholder="Search" value="{{ context.filters.get('search', None) or '' }}">
<label class="add-on">
<button type="submit" class="search" name="action" value="search">Search</button>
</label>
</div>
</form>
<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>

4
ordr2/templates/admin/user_list.jinja2

@ -14,7 +14,7 @@ @@ -14,7 +14,7 @@
Users
</h1>
</div>
{{ macros.filter_box('Role', 'role', roles) }}
{{ macros.filter_box('Role', 'role', roles, {'search':None}) }}
</div>
<div class="span10">
@ -22,7 +22,7 @@ @@ -22,7 +22,7 @@
<input type="hidden" name="csrf_token" value="{{get_csrf_token()}}">
<div class="page-controls">
<div class="input-append search">
<input type="search" name="search" size="30" placeholder="Search">
<input type="search" name="search" size="30" placeholder="Search" value="{{ context.filters.get('search', None) or '' }}">
<label class="add-on">
<button type="submit" class="search" name="action" value="search">Search</button>
</label>

8
ordr2/templates/orders/list.jinja2

@ -11,11 +11,11 @@ @@ -11,11 +11,11 @@
<div class="span2">
<div class="page-controls">
<h1>
Users
Orders
</h1>
</div>
{{ macros.filter_box('All Orders', 'status', stati, {'user': None} ) }}
{{ macros.filter_box('My Orders', 'status', stati, {'user': request.user.user_name} ) }}
{{ macros.filter_box('All Orders', 'status', stati, {'user': None, 'search':None} ) }}
{{ macros.filter_box('My Orders', 'status', stati, {'user': request.user.user_name, 'search':None} ) }}
</div>
<div class="span10">
@ -23,7 +23,7 @@ @@ -23,7 +23,7 @@
<input type="hidden" name="csrf_token" value="{{get_csrf_token()}}">
<div class="page-controls">
<div class="input-append search">
<input type="search" name="search" size="30" placeholder="Search">
<input type="search" name="search" size="30" placeholder="Search" value="{{ context.filters.get('search', None) or '' }}">
<label class="add-on">
<button type="submit" class="search" name="action" value="search">Search</button>
</label>

30
ordr2/views/admin.py

@ -63,6 +63,21 @@ def change_column_view(context, request): @@ -63,6 +63,21 @@ def change_column_view(context, request):
return HTTPFound(context.url())
@view_config(
context='ordr2:resources.UserList',
name='actions',
request_param='action=search',
permission='view',
request_method='POST'
)
def user_search(context, request):
term = request.POST.get('search', '')
term = term.strip()
if term:
return HTTPFound(context.url(search=term, role=None, p=1))
return HTTPFound(context.url())
@view_config(
context='ordr2:resources.UserList',
name='actions',
@ -271,6 +286,21 @@ def consumable_list(context, request): @@ -271,6 +286,21 @@ def consumable_list(context, request):
return {'consumables': consumables, 'categories': categories}
@view_config(
context='ordr2:resources.ConsumableList',
name='actions',
request_param='action=search',
permission='view',
request_method='POST'
)
def consumable_search(context, request):
term = request.POST.get('search', '')
term = term.strip()
if term:
return HTTPFound(context.url(search=term, category=None, p=1))
return HTTPFound(context.url())
@view_config(
context='ordr2:resources.ConsumableList',
name='new',

83
ordr2/views/orders.py

@ -1,10 +1,13 @@ @@ -1,10 +1,13 @@
import deform
import io
import xlsxwriter
from datetime import datetime
from collections import OrderedDict
from pyramid.httpexceptions import HTTPFound
from pyramid.renderers import render
from pyramid.response import FileIter
from pyramid.view import view_config
from ordr2.events import OrderStatusChange
@ -58,6 +61,86 @@ def change_column_view(context, request): @@ -58,6 +61,86 @@ def change_column_view(context, request):
return HTTPFound(context.url())
@view_config(
context='ordr2:resources.OrderList',
name='actions',
request_param='action=export',
permission='view',
request_method='POST',
renderer='ordr2:templates/orders/edit_multiple_stati.jinja2'
)
def download_view(context, request):
''' see https://xlsxwriter.readthedocs.io/example_http_server3.html '''
# Create an in-memory output file for the new workbook.
output = io.BytesIO()
# Even though the final file will be in memory the module uses temp
# files during assembly for efficiency. To avoid this on servers that
# don't allow temp files, for example the Google APP Engine, set the
# 'in_memory' constructor option to True:
workbook = xlsxwriter.Workbook(output, {'in_memory': True})
worksheet = workbook.add_worksheet()
# formatting
bold = workbook.add_format({'bold': 1})
# Add a number format for cells with money.
money_format = workbook.add_format({'num_format': '0.00'})
# Add an Excel date format.
date_format = workbook.add_format({'num_format': 'yy-mm-dd hh:mm'})
# Write the column headers
headers = [
'Placed On', 'CAS / Description', 'Vendor', 'Catalog Nr',
'Package Size', 'Unit Price', 'Quantity', 'Total Price', 'Currency',
'Account', 'Status', 'Category', 'Placed By'
]
for col, header in enumerate(headers):
worksheet.write(0, col, header, bold)
for row, resource in enumerate(context.items()):
order = resource.model
worksheet.write(row + 1, 0, order.created_date, date_format)
worksheet.write_string(row + 1, 1, order.cas_description)
worksheet.write_string(row + 1, 2, order.vendor)
worksheet.write_string(row + 1, 3, order.catalog_nr)
worksheet.write_string(row + 1, 4, order.package_size)
worksheet.write(row + 1, 5, order.unit_price, money_format)
worksheet.write(row + 1, 6, order.amount)
worksheet.write(row + 1, 7, order.total_price, money_format)
worksheet.write_string(row + 1, 8, order.currency)
worksheet.write_string(row + 1, 9, order.account)
worksheet.write_string(row + 1, 10, order.status.value.capitalize())
worksheet.write_string(row + 1, 11, order.category.value.capitalize())
worksheet.write_string(row + 1, 12, order.created_by)
# Close the workbook before streaming the data.
workbook.close()
# Rewind the buffer.
output.seek(0)
response = request.response
response.app_iter = FileIter(output)
headers = response.headers
headers['Content-Type'] = 'application/download'
headers['Accept-Ranges'] = 'bite'
headers['Content-Disposition'] = 'attachment;filename=order-list.xlsx'
return response
@view_config(
context='ordr2:resources.OrderList',
name='actions',
request_param='action=search',
permission='view',
request_method='POST'
)
def search(context, request):
term = request.POST.get('search', '')
term = term.strip()
if term:
return HTTPFound(context.url(search=term, user=None, status=None, p=1))
return HTTPFound(context.url())
@view_config(
context='ordr2:resources.OrderList',
name='actions',

1
setup.py

@ -25,6 +25,7 @@ requirements = [ @@ -25,6 +25,7 @@ requirements = [
'deform',
'PyYAML',
'tqdm',
'XlsxWriter',
]
setup_requirements = [