Browse Source

added lots of code documentation

php2python
Holger Frey 7 years ago
parent
commit
b5da0d3411
  1. 2
      ordr2/__init__.py
  2. 15
      ordr2/events.py
  3. 9
      ordr2/models/__init__.py
  4. 2
      ordr2/models/meta.py
  5. 14
      ordr2/models/orders.py
  6. 11
      ordr2/models/user.py
  7. 3
      ordr2/resources/__init__.py
  8. 10
      ordr2/resources/account.py
  9. 31
      ordr2/resources/admin.py
  10. 85
      ordr2/resources/base.py
  11. 27
      ordr2/resources/orders.py
  12. 12
      ordr2/schemas/__init__.py
  13. 31
      ordr2/schemas/orders.py
  14. 2
      ordr2/scripts/__init__.py
  15. 41
      ordr2/scripts/initializedb.py
  16. 9
      ordr2/security.py
  17. 3
      ordr2/templates/tests.py
  18. 15
      ordr2/views/__init__.py
  19. 19
      ordr2/views/account.py
  20. 44
      ordr2/views/admin.py
  21. 42
      ordr2/views/orders.py
  22. 2
      ordr2/views/pages.py

2
ordr2/__init__.py

@ -1,5 +1,3 @@ @@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
''' Top-level package for Ordr2. '''
__author__ = 'Holger Frey'

15
ordr2/events.py

@ -1,3 +1,5 @@ @@ -1,3 +1,5 @@
''' custom events and event subsribers '''
from pyramid.events import NewRequest, subscriber
from pyramid.renderers import render
from pyramid_mailer.message import Message
@ -5,14 +7,19 @@ from pyramid_mailer.message import Message @@ -5,14 +7,19 @@ from pyramid_mailer.message import Message
from ordr2.views import set_display_defaults
# custom events
class UserLogIn(object):
''' notify on user log in '''
def __init__(self, request, user):
self.request = request
self.user = user
class UserNotification(object):
''' base class for user notifications '''
subject = None
template = None
def __init__(self, request, user, data=None):
@ -22,32 +29,40 @@ class UserNotification(object): @@ -22,32 +29,40 @@ class UserNotification(object):
class AccountActivation(UserNotification):
''' user notification for account activation '''
subject='[ordr] Your account was activated'
template = 'ordr2:templates/emails/activation.jinja2'
class PasswordReset(UserNotification):
''' user notification for password reset link '''
subject='[ordr] Password Reset'
template = 'ordr2:templates/emails/password_reset.jinja2'
class OrderStatusChange(UserNotification):
''' user notification for order status change '''
subject='[ordr] Order Status Change'
template = 'ordr2:templates/emails/order.jinja2'
# subsribers for events
@subscriber(UserLogIn)
def set_display_defaults_on_log_in(event):
''' set column display defaults at every login '''
set_display_defaults(event.request)
@subscriber(NewRequest)
def check_display_defaults(event):
''' check if column display preferences are set in sesssion '''
if event.request.user and 'display' not in event.request.session:
set_display_defaults(event.request)
@subscriber(UserNotification)
def notify_user(event):
''' notify a user about an event '''
body = render(
event.template,
{'user': event.user, 'data': event.data},

9
ordr2/models/__init__.py

@ -1,3 +1,5 @@ @@ -1,3 +1,5 @@
''' Database models and setup '''
from sqlalchemy import engine_from_config
from sqlalchemy.orm import sessionmaker
from sqlalchemy.orm import configure_mappers
@ -6,7 +8,7 @@ import zope.sqlalchemy @@ -6,7 +8,7 @@ import zope.sqlalchemy
# import or define all models here to ensure they are attached to the
# Base.metadata prior to any initialization routines
from .user import User, Role # flake8: noqa
from .orders import Category, Consumable, Order, OrderStatus
from .orders import Category, Consumable, Order, OrderStatus # flake8: noqa
# run configure_mappers after defining all of the models to ensure
# all relationships can be setup
@ -14,18 +16,19 @@ configure_mappers() @@ -14,18 +16,19 @@ configure_mappers()
def get_engine(settings, prefix='sqlalchemy.'):
''' returns a sqlalchemy engine from the application configuration '''
return engine_from_config(settings, prefix)
def get_session_factory(engine):
''' returns a database session '''
factory = sessionmaker()
factory.configure(bind=engine)
return factory
def get_tm_session(session_factory, transaction_manager):
'''
Get a ``sqlalchemy.orm.Session`` instance backed by a transaction.
''' Get a sqlalchemy.orm.Session instance backed by a transaction.
This function will hook the session to the transaction manager which
will take care of committing any changes.

2
ordr2/models/meta.py

@ -1,3 +1,5 @@ @@ -1,3 +1,5 @@
''' sqlalchemy metadata configuration '''
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.schema import MetaData

14
ordr2/models/orders.py

@ -1,3 +1,5 @@ @@ -1,3 +1,5 @@
''' Consumables, Categories, Orders and Order Status Database Models '''
import bcrypt
import enum
import uuid
@ -99,26 +101,32 @@ class Order(Base): @@ -99,26 +101,32 @@ class Order(Base):
def _date_info(self, some_date, some_one):
''' string representaton of date and user '''
if not some_date:
# no date, no string
return ''
if some_one:
# in the new system a status change also stores the purchaser
return '{!s} by {!s}'.format(some_date, some_one)
else:
return '{!s}'.format(some_date)
# historical data does not have a purchaser associated with a date
return '{!s}'.format(some_date)
@property
def placed(self):
''' string representation for placed on / by '''
return self._date_info(self.created_date, self.created_by)
@property
def approved(self):
''' string representation for approval on / by '''
return self._date_info(self.approval_date, self.approval_by)
@property
def ordered(self):
''' string representation for ordered on / by '''
return self._date_info(self.ordered_date, self.ordered_by)
@property
def completed(self):
''' string representation for completed on / by '''
return self._date_info(self.completed_date, self.completed_by)

11
ordr2/models/user.py

@ -1,3 +1,5 @@ @@ -1,3 +1,5 @@
''' User Account and Roles Models '''
import bcrypt
import enum
import uuid
@ -16,7 +18,8 @@ from .meta import Base @@ -16,7 +18,8 @@ from .meta import Base
class Role(enum.Enum):
''' roles of the user '''
''' roles of user accounts '''
NEW = 'new'
USER = 'user'
PURCHASER = 'purchaser'
@ -25,7 +28,8 @@ class Role(enum.Enum): @@ -25,7 +28,8 @@ class Role(enum.Enum):
@property
def principal(self):
return 'role:' + self.value
''' returns the principal identifier of the role '''
return 'role:' + self.value.lower()
class User(Base):
@ -54,8 +58,10 @@ class User(Base): @@ -54,8 +58,10 @@ class User(Base):
''' returns the principal identifiers for the user's role '''
principals = [self.role.principal]
if self.role is Role.PURCHASER:
# a purchaser is also a user
principals.append(Role.USER.principal)
elif self.role is Role.ADMIN:
# an admin is also a purchaser and a user
principals.append(Role.USER.principal)
principals.append(Role.PURCHASER.principal)
return principals
@ -78,6 +84,7 @@ class User(Base): @@ -78,6 +84,7 @@ class User(Base):
return False
def generate_password_token(self):
''' generates a token for a password reset link '''
token = uuid.uuid4()
self.password_reset = token.hex
return token.hex

3
ordr2/resources/__init__.py

@ -1,3 +1,5 @@ @@ -1,3 +1,5 @@
''' base resource and resource root factory '''
from pyramid.security import Allow, Everyone
from .account import Account, PasswordResetAccount
@ -28,6 +30,7 @@ class Root(BaseResource): @@ -28,6 +30,7 @@ class Root(BaseResource):
self.request = request
def __acl__(self):
''' access controll list '''
return [ (Allow, Everyone, 'view') ]

10
ordr2/resources/account.py

@ -1,3 +1,5 @@ @@ -1,3 +1,5 @@
''' Resources for User Accounts '''
from pyramid.security import Allow, Authenticated, Deny, DENY_ALL, Everyone
from ordr2.models import User
@ -5,10 +7,11 @@ from ordr2.models import User @@ -5,10 +7,11 @@ from ordr2.models import User
from .base import BaseResource
class PasswordResetAccount(BaseResource):
''' resource for passwort change representing a reset token '''
def __acl__(self):
''' access controll list '''
return [
(Allow, Everyone, 'reset'),
DENY_ALL
@ -16,14 +19,17 @@ class PasswordResetAccount(BaseResource): @@ -16,14 +19,17 @@ class PasswordResetAccount(BaseResource):
class PasswordReset(BaseResource):
''' resource for passwort reset link '''
def __acl__(self):
''' access controll list '''
return [
(Allow, Everyone, 'reset'),
DENY_ALL
]
def __getitem__(self, key):
''' queries the database for a password reset token '''
key = key.strip()
if key:
account = self.request.dbsession.\
@ -36,6 +42,7 @@ class PasswordReset(BaseResource): @@ -36,6 +42,7 @@ class PasswordReset(BaseResource):
class Account(BaseResource):
''' User Account and Settings '''
nodes = {'reset': PasswordReset}
@ -45,6 +52,7 @@ class Account(BaseResource): @@ -45,6 +52,7 @@ class Account(BaseResource):
def __acl__(self):
''' access controll list '''
return [
(Allow, Everyone, 'login'),
(Allow, Everyone, 'logout'),

31
ordr2/resources/admin.py

@ -1,3 +1,5 @@ @@ -1,3 +1,5 @@
''' Resources for the Admin Section '''
from sqlalchemy import or_
from pyramid.security import Allow, Authenticated, Deny, DENY_ALL, Everyone
@ -9,7 +11,9 @@ from ordr2.models import Category, Consumable, User, Role @@ -9,7 +11,9 @@ from ordr2.models import Category, Consumable, User, Role
# user accounr resources
class UserAccount(BaseResource):
''' Resource for a user account '''
def __acl__(self):
''' Access Controll List '''
return [
(Allow, 'role:admin', 'view'),
(Allow, 'role:admin', 'edit'),
@ -19,12 +23,15 @@ class UserAccount(BaseResource): @@ -19,12 +23,15 @@ class UserAccount(BaseResource):
class UserList(BaseResource, PaginationResourceMixin):
''' Resource for a list of users '''
sql_model_class = User
child_resource_class = UserAccount
default_sorting = 'user.asc'
default_items_per_page = 12
def __acl__(self):
''' Access Controll List '''
return [
(Allow, 'role:admin', 'view'),
(Allow, 'role:admin', 'edit'),
@ -32,11 +39,11 @@ class UserList(BaseResource, PaginationResourceMixin): @@ -32,11 +39,11 @@ class UserList(BaseResource, PaginationResourceMixin):
DENY_ALL
]
def prepare_filtered_query(self, dbsession, filter_params):
''' setup the base filtered query '''
query = dbsession.query(self.sql_model_class)
# filter by role
role_name = filter_params.get('role', None)
try:
role_name = role_name.lower()
@ -46,6 +53,7 @@ class UserList(BaseResource, PaginationResourceMixin): @@ -46,6 +53,7 @@ class UserList(BaseResource, PaginationResourceMixin):
role_name = None
self.filters['role'] = role_name
# filter by search term
search = filter_params.get('search', None)
if search:
term = '%{}%'.format(search)
@ -61,9 +69,8 @@ class UserList(BaseResource, PaginationResourceMixin): @@ -61,9 +69,8 @@ class UserList(BaseResource, PaginationResourceMixin):
return query
def prepare_sorted_query(self, query, sorting):
''' setup the base filtered query '''
''' add sorting to the base query '''
available_fields = {
'user': 'user_name',
'first': 'first_name',
@ -76,10 +83,13 @@ class UserList(BaseResource, PaginationResourceMixin): @@ -76,10 +83,13 @@ class UserList(BaseResource, PaginationResourceMixin):
if model_field:
sort_func = sorting.func(model_field)
query = query.order_by(sort_func)
# add default sorting
default_sort = self.parse_sort_parameters(self.default_sorting)
if sorting.field != default_sort.field:
default_sort = self.parse_sort_parameters(self.default_sorting)
query = self.prepare_sorted_query(query, default_sort)
return query
@ -87,7 +97,9 @@ class UserList(BaseResource, PaginationResourceMixin): @@ -87,7 +97,9 @@ class UserList(BaseResource, PaginationResourceMixin):
# consumables resources
class ConsumableResource(BaseResource):
''' Resource for one consumable '''
def __acl__(self):
''' Access Controll List '''
return [
(Allow, 'role:admin', 'view'),
(Allow, 'role:admin', 'edit'),
@ -97,12 +109,15 @@ class ConsumableResource(BaseResource): @@ -97,12 +109,15 @@ class ConsumableResource(BaseResource):
class ConsumableList(BaseResource, PaginationResourceMixin):
''' Resource for a list of consumables '''
sql_model_class = Consumable
child_resource_class = ConsumableResource
default_sorting = 'cas.asc'
default_items_per_page = 12
def __acl__(self):
''' Access Controll List '''
return [
(Allow, 'role:admin', 'view'),
(Allow, 'role:admin', 'create'),
@ -116,6 +131,7 @@ class ConsumableList(BaseResource, PaginationResourceMixin): @@ -116,6 +131,7 @@ class ConsumableList(BaseResource, PaginationResourceMixin):
''' setup the base filtered query '''
query = dbsession.query(self.sql_model_class)
# filter by category
category_name = filter_params.get('category', None)
try:
category_name = category_name.lower()
@ -125,6 +141,7 @@ class ConsumableList(BaseResource, PaginationResourceMixin): @@ -125,6 +141,7 @@ class ConsumableList(BaseResource, PaginationResourceMixin):
category_name = None
self.filters['category'] = category_name
# filter by search term
search = filter_params.get('search', None)
if search:
term = '%{}%'.format(search)
@ -139,9 +156,8 @@ class ConsumableList(BaseResource, PaginationResourceMixin): @@ -139,9 +156,8 @@ class ConsumableList(BaseResource, PaginationResourceMixin):
return query
def prepare_sorted_query(self, query, sorting):
''' setup the base filtered query '''
''' add sorting to the base query '''
available_fields = {
'cas': 'cas_description',
'category': 'category',
@ -156,14 +172,18 @@ class ConsumableList(BaseResource, PaginationResourceMixin): @@ -156,14 +172,18 @@ class ConsumableList(BaseResource, PaginationResourceMixin):
if model_field:
sort_func = sorting.func(model_field)
query = query.order_by(sort_func)
# add default sorting
default_sort = self.parse_sort_parameters(self.default_sorting)
if sorting.field != default_sort.field:
default_sort = self.parse_sort_parameters(self.default_sorting)
query = self.prepare_sorted_query(query, default_sort)
return query
class Admin(BaseResource):
''' Resource for the admin section '''
nodes = {
'users': UserList,
@ -171,4 +191,5 @@ class Admin(BaseResource): @@ -171,4 +191,5 @@ class Admin(BaseResource):
}
def __acl__(self):
''' Access Controll List '''
return [ (Allow, 'role:admin', 'view') ]

85
ordr2/resources/base.py

@ -1,11 +1,14 @@ @@ -1,11 +1,14 @@
''' Base Resource and Mixin classes, not to be used directly '''
from collections import namedtuple
from pyramid.security import DENY_ALL
from pyramid.security import DENY_ALL
from sqlalchemy import asc, desc
from sqlalchemy.inspection import inspect
class BaseResource(object):
''' Base Resource for all other resources '''
__parent__ = None
__name__ = None
@ -26,10 +29,12 @@ class BaseResource(object): @@ -26,10 +29,12 @@ class BaseResource(object):
super().__init__()
def __acl__(self):
''' Access controll list '''
return [ DENY_ALL ]
def __getitem__(self, key):
''' returns child resources '''
klass = self.nodes.get(key, None)
if klass:
return klass(key, self)
@ -52,19 +57,45 @@ class BaseResource(object): @@ -52,19 +57,45 @@ class BaseResource(object):
class Pagination(object):
''' calculates pagination information
Available instance attributes
count: total number of items
items: number of items displayed per page, aliased by items_per_page
first: first page number
last: last page number
current: current page number
previous: previous page number
next: next page number
window: page window, e.g:
lets assume the current page is 10 and window size is 7
self.window = [7, 8, 9, 10, 11, 12, 13]
'''
default_items = 25
default_window_size = 7
def __init__(self, current, count, items=None, window_size=None):
''' calculates pagination information
Parameters:
current: current pages
count: total number of items
items: number of items displayed per pages
window_size: size of pagination window
'''
# ensure values are integers
current = self._ensure_int(current, 1)
count = self._ensure_int(count, 0)
items = self._ensure_int(items, self.default_items)
window_size = self._ensure_int(window_size, self.default_window_size)
# set the simples values that won't change
self.count = count
self.items = self.items_per_page = items
# calculate number of pages
pages = (count - 1) // items + 1
self.first = 1
self.last = max(self.first, pages)
@ -88,33 +119,57 @@ class Pagination(object): @@ -88,33 +119,57 @@ class Pagination(object):
return default
def _ensure_int(self, value, default):
''' converts the value to integer, returns default if it fails '''
try:
return int(value)
except Exception:
return default
# named tuple for parameters used in sorting
SortParameter = namedtuple('SortParameter', 'text field direction func')
class PaginationResourceMixin(object):
''' mixin providing pagination information for simple sql models
class attributes that must be defined in child classes:
sql_model_class: sqlalchemy model class
child_resource_class: resource representing a sqlalchemy model
default_sorting: string for default soring behaviour,
e.g. 'created_on.desc'
default_items_per_page: default number of items displayed per page
available instance attributes:
pages: pagination information, see Pagination class
sorting: sorting parameter, processed from request.GET
filters: filter parameters, processed from request.GET
'''
# class attributes that must be defined in child classes
sql_model_class = None
child_resource_class = None
default_sorting = None
default_items_per_page = 25
# attributes set by processing request.GET
pages = None
sorting = None
filters = {}
# keys for request.GET processing
query_key_current_page = 'p'
query_key_items_per_page = 'n'
query_key_sorting = 'o'
# base sqlalchemy query object
_base_query = None
def __init__(self):
''' sets parameters from request.GET '''
# first we need to remove non-filter parameters from GET
params = dict(self.request.GET)
page = params.pop(self.query_key_current_page, 1)
@ -145,13 +200,16 @@ class PaginationResourceMixin(object): @@ -145,13 +200,16 @@ class PaginationResourceMixin(object):
''' setup the base filtered query
An example:
def prepare_filtered_query(self, dbsession, filter_params):
query = dbsession.query(self.sql_model_class)
by_username = filter_params.get('username', None)
if by_username is not None:
query = query.filter_by(username=by_username)
# don't forget to remember the filter
self.filters['username'] = by_username
return query
'''
msg = 'Query setup must be implemented in child class'
@ -159,9 +217,10 @@ class PaginationResourceMixin(object): @@ -159,9 +217,10 @@ class PaginationResourceMixin(object):
def prepare_sorted_query(self, query, sorting):
''' setup the base filtered query
''' add sorting to the base query
An example:
def prepare_sorted_query(self, query, sorting):
model_field = getattr(self.sql_model_class, sorting.field)
sort_func = sorting.func(model_field)
@ -172,6 +231,7 @@ class PaginationResourceMixin(object): @@ -172,6 +231,7 @@ class PaginationResourceMixin(object):
def parse_sort_parameters(self, sort_param):
''' parses a string that might contain sorting information '''
sort_functions = { 'asc': asc, 'desc': desc}
try:
sort_param = sort_param.lower()
@ -185,12 +245,16 @@ class PaginationResourceMixin(object): @@ -185,12 +245,16 @@ class PaginationResourceMixin(object):
def items(self):
''' returns the items of the current page as resources'''
if not self.pages.count:
return
return []
# calculate the offset from the paging information
offset = (self.pages.current - 1) * self.pages.items_per_page
# prepare the query including sorting, offset and limit
query = self.prepare_sorted_query(self._base_query, self.sorting)
query = query.offset(offset).limit(self.pages.items_per_page)
# return a list of resources representing the items found by the query
return [
self.child_resource_class.from_sqla(item, self)
for item
@ -199,6 +263,14 @@ class PaginationResourceMixin(object): @@ -199,6 +263,14 @@ class PaginationResourceMixin(object):
def query_params(self, *args, **kwargs):
''' returns a dict with query parameters for a new request
The entries are set by the parsed request.GET parameters used to
construct the queries. If a new request comes in with the parameters
provided, it will return the same child resources in the same order.
the query parameters can be overridden by providing tuples or
keyword arguments with new values.
'''
params = {
self.query_key_current_page: self.pages.current,
self.query_key_items_per_page: self.pages.items,
@ -207,16 +279,23 @@ class PaginationResourceMixin(object): @@ -207,16 +279,23 @@ class PaginationResourceMixin(object):
params.update(self.filters)
params.update(args)
params.update(kwargs)
# remove items that have None as a value
filtered = {k: v for k, v in params.items() if v is not None}
return filtered
def url(self, *args, **kwargs):
''' shortcut for creating a url pointing to this resource
shortcut for:
request.resource_url(self, query=self.query_params(*args, **kwargs))
'''
params = self.query_params(*args, **kwargs)
return self.request.resource_url(self, query=params)
def __getitem__(self, key):
''' returns a child resource representing a sqlalchemy model '''
model = self.request.dbsession.query(self.sql_model_class).get(key)
if not model:
raise KeyError()

27
ordr2/resources/orders.py

@ -8,27 +8,38 @@ from ordr2.models import Category, Order, OrderStatus @@ -8,27 +8,38 @@ from ordr2.models import Category, Order, OrderStatus
class OrderResource(BaseResource):
''' Resource representing one order '''
def __acl__(self):
''' Access controll list '''
acl = [
(Allow, 'role:user', 'view'),
(Allow, 'role:user', 'create'),
(Allow, 'role:purchaser', 'edit'),
(Allow, 'role:purchaser', 'delete'),
]
# open orders may be edited and deleted by the user that placed them
if self.model.status == OrderStatus.OPEN:
acl.append( (Allow, 'user:' + str(self.model.created_by), 'edit') )
acl.append( (Allow, 'user:' + str(self.model.created_by), 'delete') )
acl.append(
(Allow, 'user:' + str(self.model.created_by), 'edit')
)
acl.append(
(Allow, 'user:' + str(self.model.created_by), 'delete')
)
acl.append(DENY_ALL)
return acl
class OrderList(BaseResource, PaginationResourceMixin):
''' Resource representing a list of orders '''
sql_model_class = Order
child_resource_class = OrderResource
default_sorting = 'created.desc'
default_items_per_page = 12
def __acl__(self):
''' Access controll list '''
return [
(Allow, 'role:user', 'view'),
(Allow, 'role:user', 'create'),
@ -37,13 +48,13 @@ class OrderList(BaseResource, PaginationResourceMixin): @@ -37,13 +48,13 @@ class OrderList(BaseResource, PaginationResourceMixin):
DENY_ALL
]
def prepare_filtered_query(self, dbsession, filter_params):
''' setup the base filtered query '''
query = dbsession.query(self.sql_model_class)
# filter by status
status_name = filter_params.get('status', None)
try:
status_name = filter_params.get('status', None)
status_name = status_name.lower()
status = OrderStatus(status_name)
query = query.filter_by(status=status)
@ -51,11 +62,13 @@ class OrderList(BaseResource, PaginationResourceMixin): @@ -51,11 +62,13 @@ class OrderList(BaseResource, PaginationResourceMixin):
status_name = None
self.filters['status'] = status_name
# filter by user
user_name = filter_params.get('user', None)
if user_name:
query = query.filter_by(created_by=user_name)
self.filters['user'] = user_name
# filter by search term
search = filter_params.get('search', None)
if search:
term = '%{}%'.format(search)
@ -72,9 +85,8 @@ class OrderList(BaseResource, PaginationResourceMixin): @@ -72,9 +85,8 @@ class OrderList(BaseResource, PaginationResourceMixin):
return query
def prepare_sorted_query(self, query, sorting):
''' setup the base filtered query '''
''' add sorting to the base query '''
available_fields = {
'cas': 'cas_description',
'category': 'category',
@ -92,8 +104,11 @@ class OrderList(BaseResource, PaginationResourceMixin): @@ -92,8 +104,11 @@ class OrderList(BaseResource, PaginationResourceMixin):
if model_field:
sort_func = sorting.func(model_field)
query = query.order_by(sort_func)
# add default sorting
default_sort = self.parse_sort_parameters(self.default_sorting)
if sorting.field != default_sort.field:
default_sort = self.parse_sort_parameters(self.default_sorting)
query = self.prepare_sorted_query(query, default_sort)
return query

12
ordr2/schemas/__init__.py

@ -1,16 +1,19 @@ @@ -1,16 +1,19 @@
''' Schemas for form input and validation '''
import colander
import deform
from deform.renderer import configure_zpt_renderer
from .helpers import (
deferred_csrf_default,
deferred_csrf_validator
)
from deform.renderer import configure_zpt_renderer
# Make Deform widgets aware of our widget template paths
configure_zpt_renderer(['ordr2:templates/deform'])
# Base Schema
class CSRFSchema(colander.Schema):
@ -25,7 +28,8 @@ class CSRFSchema(colander.Schema): @@ -25,7 +28,8 @@ class CSRFSchema(colander.Schema):
@classmethod
def as_form(cls, request, **kwargs):
url = kwargs.get('url', None)
''' returns the schema as a form '''
url = kwargs.pop('url', None)
if not url:
url = request.resource_url(request.context, request.view_name)
schema = cls().bind(request=request)
@ -35,6 +39,7 @@ class CSRFSchema(colander.Schema): @@ -35,6 +39,7 @@ class CSRFSchema(colander.Schema):
class MoneyInputSchema(colander.Schema):
''' custom schema for structured money and currency input '''
amount = colander.SchemaNode(
colander.Decimal(),
@ -53,6 +58,7 @@ class MoneyInputSchema(colander.Schema): @@ -53,6 +58,7 @@ class MoneyInputSchema(colander.Schema):
)
def __init__(self, *args, **kwargs):
''' define the custom schema templates '''
if 'widget' not in kwargs:
readonly = kwargs.pop('readonly', False)
kwargs['widget'] = deform.widget.MappingWidget(

31
ordr2/schemas/orders.py

@ -1,3 +1,5 @@ @@ -1,3 +1,5 @@
''' schemas for creating and editing orders and consumables'''
import colander
import deform
@ -5,10 +7,12 @@ from ordr2.models import Category, OrderStatus @@ -5,10 +7,12 @@ from ordr2.models import Category, OrderStatus
from . import CSRFSchema, MoneyInputSchema
# key / value pairs for select fields
CATEGORIES = [(c.name, c.value.capitalize()) for c in Category]
STATI = [(s.name, s.value.capitalize()) for s in OrderStatus]
# schema for user registration
class ConsumableSchema(CSRFSchema):
''' edit or add consumable '''
@ -40,6 +44,8 @@ class ConsumableSchema(CSRFSchema): @@ -40,6 +44,8 @@ class ConsumableSchema(CSRFSchema):
@classmethod
def as_form(cls, request, **override):
''' returns the schema as a form '''
# define buttons separately for a new consumable and one to edit
is_new_consumable = override.pop('is_new_consumable', False)
if is_new_consumable:
settings = {
@ -67,6 +73,10 @@ class ConsumableSchema(CSRFSchema): @@ -67,6 +73,10 @@ class ConsumableSchema(CSRFSchema):
class OrderInformation(colander.Schema):
''' schema for editing order status
parital schema, used in EditOrderSchema
'''
status = colander.SchemaNode(
colander.String(),
@ -75,6 +85,10 @@ class OrderInformation(colander.Schema): @@ -75,6 +85,10 @@ class OrderInformation(colander.Schema):
class OrderItem(colander.Schema):
''' schema for editing item information
parital schema, used in NewOrderSchema and EditOrderSchema
'''
cas_description = colander.SchemaNode(
colander.String()
@ -95,6 +109,10 @@ class OrderItem(colander.Schema): @@ -95,6 +109,10 @@ class OrderItem(colander.Schema):
class OrderPricing(colander.Schema):
''' schema for editing price information
parital schema, used in NewOrderSchema and EditOrderSchema
'''
unit_price = MoneyInputSchema(
readonly=False
@ -113,7 +131,10 @@ class OrderPricing(colander.Schema): @@ -113,7 +131,10 @@ class OrderPricing(colander.Schema):
class OrderOptionals(colander.Schema):
''' schema for editing optional order information
parital schema, used in NewOrderSchema and EditOrderSchema
'''
account = colander.SchemaNode(
colander.String(),
missing=''
@ -126,7 +147,7 @@ class OrderOptionals(colander.Schema): @@ -126,7 +147,7 @@ class OrderOptionals(colander.Schema):
class NewOrderSchema(CSRFSchema):
''' edit or add an order '''
''' schema for a new order '''
item_information = OrderItem()
pricing = OrderPricing()
@ -134,6 +155,7 @@ class NewOrderSchema(CSRFSchema): @@ -134,6 +155,7 @@ class NewOrderSchema(CSRFSchema):
@classmethod
def as_form(cls, request, **override):
''' returns the schema as a form '''
settings = {
'buttons': (
deform.Button(name='save', title='Place Order'),
@ -146,7 +168,7 @@ class NewOrderSchema(CSRFSchema): @@ -146,7 +168,7 @@ class NewOrderSchema(CSRFSchema):
class EditOrderSchema(CSRFSchema):
''' edit or add an order '''
''' schema for editing an order '''
order_information = OrderInformation(
widget=deform.widget.MappingWidget(
@ -159,6 +181,7 @@ class EditOrderSchema(CSRFSchema): @@ -159,6 +181,7 @@ class EditOrderSchema(CSRFSchema):
@classmethod
def as_form(cls, request, **override):
''' returns the schema as a form '''
settings = {
'buttons': (
deform.Button(name='save', title='Edit Order'),
@ -178,6 +201,8 @@ class EditOrderSchema(CSRFSchema): @@ -178,6 +201,8 @@ class EditOrderSchema(CSRFSchema):
}
settings.update(override)
form = super().as_form(request, **settings)
# disable the status field, if the current user is not a purchaser
if not 'role:purchaser' in request.user.role_principals:
form['order_information']['status'].widget = \
deform.widget.TextInputWidget(

2
ordr2/scripts/__init__.py

@ -1 +1 @@ @@ -1 +1 @@
# package
''' command line scripts '''

41
ordr2/scripts/initializedb.py

@ -1,37 +1,36 @@ @@ -1,37 +1,36 @@
''' initializes a new data base and migrates old data '''
import os
import sys
import transaction
import yaml
from datetime import datetime
from tqdm import tqdm
from urllib.parse import urlparse
from pyramid.paster import (
get_appsettings,
setup_logging,
)
from pyramid.paster import get_appsettings, setup_logging
from pyramid.scripts.common import parse_vars
from ..models.meta import Base
from ..models import (
from ordr2.models.meta import Base
from ordr2.models import (
get_engine,
get_session_factory,
get_tm_session,
)
from ..models import Category, Consumable, Order, OrderStatus, User, Role
from ordr2.models import Category, Consumable, Order, OrderStatus, User, Role
def usage(argv):
''' display the command line help message '''
cmd = os.path.basename(argv[0])
print('usage: %s <config_uri> [var=value]\n'
'(example: "%s development.ini")' % (cmd, cmd))
sys.exit(1)
def read_exported_file(*paths):
def read_yaml_file(*paths):
''' read and parse a yaml file '''
path = os.path.join(*paths)
with open(path, 'r') as filehandle:
items = yaml.load(filehandle)
@ -39,12 +38,18 @@ def read_exported_file(*paths): @@ -39,12 +38,18 @@ def read_exported_file(*paths):
def parse_dt(value, default=False):
''' parse a datetime value or return a default
for a default value Null is explicitly allowed.
'''
if not value or value.startswith('0000'):
return datetime.utcnow() if default is False else default
return datetime.strptime(value + ' -0000', '%Y-%m-%d %H:%M:%S %z')
def main(argv=sys.argv):
''' setup the database and migrate the data '''
# load the settings from the provided config file
if len(argv) < 2:
usage(argv)
config_uri = argv[1]
@ -57,16 +62,17 @@ def main(argv=sys.argv): @@ -57,16 +62,17 @@ def main(argv=sys.argv):
if database_url.scheme == 'sqlite' and os.path.isfile(database_url.path):
os.remove(database_url.path)
# setup the database connection
engine = get_engine(settings)
Base.metadata.create_all(engine)
session_factory = get_session_factory(engine)
with transaction.manager:
dbsession = get_tm_session(session_factory, transaction.manager)
base_dir = os.path.dirname(__file__)
user_list = read_exported_file(base_dir, 'export_users.yml')
# migrate user data
user_list = read_yaml_file(base_dir, 'export_users.yml')
for data in tqdm(user_list, desc='Users'):
user = User(
id=data['id'],
@ -83,8 +89,9 @@ def main(argv=sys.argv): @@ -83,8 +89,9 @@ def main(argv=sys.argv):
dbsession.add(user)
dbsession.flush()
cat_list = read_exported_file(base_dir, 'export_consumables.yml')
for data in tqdm(cat_list, desc='Consumables'):
# migrate consumables
con_list = read_yaml_file(base_dir, 'export_consumables.yml')
for data in tqdm(con_list, desc='Consumables'):
category = Category[data['category'].upper()]
date_created = parse_dt(data['date_created'])
consumable = Consumable(
@ -103,8 +110,8 @@ def main(argv=sys.argv): @@ -103,8 +110,8 @@ def main(argv=sys.argv):
dbsession.add(consumable)
dbsession.flush()
order_list = read_exported_file(base_dir, 'export_orders.yml')
# migrate orders
order_list = read_yaml_file(base_dir, 'export_orders.yml')
for data in tqdm(order_list, desc='Orders'):
work_status = data.get('work_status', 'approval')
status = OrderStatus[work_status.upper()]

9
ordr2/security.py

@ -1,3 +1,5 @@ @@ -1,3 +1,5 @@
''' User Authentication and Authorization '''
from pyramid.authentication import AuthTktAuthenticationPolicy
from pyramid.authorization import ACLAuthorizationPolicy
from pyramid.security import Authenticated, Everyone
@ -10,7 +12,7 @@ class AuthenticationPolicy(AuthTktAuthenticationPolicy): @@ -10,7 +12,7 @@ class AuthenticationPolicy(AuthTktAuthenticationPolicy):
def authenticated_userid(self, request):
''' returns the id of an authenticated user
heavy lifting done in get_user() attached to request
'''
user = request.user
@ -39,7 +41,10 @@ def get_user(request): @@ -39,7 +41,10 @@ def get_user(request):
def includeme(config):
''' initializing authentication and authorization for the Pyramid app '''
''' initializing authentication and authorization for the Pyramid app
Activate this setup using ``config.include('ordr2.security')``.
'''
settings = config.get_settings()
authn_policy = AuthenticationPolicy(
settings['auth.secret'],

3
ordr2/templates/tests.py

@ -1,4 +1,7 @@ @@ -1,4 +1,7 @@
''' custom jinja2 tests and filters '''
def are_extras_active(context, extras):
''' checks if the filters are active in a PaginationResource '''
if extras is None:
return True
return all(context.filters.get(k) == v for k, v in extras.items())

15
ordr2/views/__init__.py

@ -1,6 +1,11 @@ @@ -1,6 +1,11 @@
from collections import namedtuple
''' views package
some view helpers are defined here
'''
from collections import namedtuple
# a message for session.flash()
FlashMessage = namedtuple('FlashMessage', 'message description dismissable')
@ -11,7 +16,7 @@ def flash(request, channel, message, description='', dismissable=True): @@ -11,7 +16,7 @@ def flash(request, channel, message, description='', dismissable=True):
def update_column_display(request, section):
''' update the session values for wich columns to display '''
''' update the session values for which columns to display '''
if section not in request.session['display']:
return
display_keys = request.session['display'][section].keys()
@ -23,6 +28,7 @@ def update_column_display(request, section): @@ -23,6 +28,7 @@ def update_column_display(request, section):
def set_display_defaults(request):
''' sets the coulumn display default '''
defaults = {
'users': {
'first': True,
@ -43,7 +49,10 @@ def set_display_defaults(request): @@ -43,7 +49,10 @@ def set_display_defaults(request):
def includeme(config):
''' adding request helpers and static views '''
''' adding request helpers and static views
Activate this setup using ``config.include('ordr2.views')``.
'''
config.add_request_method(flash, 'flash')
settings = config.get_settings()

19
ordr2/views/account.py

@ -1,3 +1,5 @@ @@ -1,3 +1,5 @@
''' Account Registration and Settings '''
import deform
from pyramid.httpexceptions import HTTPFound
@ -13,6 +15,9 @@ from ordr2.schemas.account import ( @@ -13,6 +15,9 @@ from ordr2.schemas.account import (
SettingsSchema
)
# below this password length a warning is displayed
MIN_PW_LENGTH = 12
# user log in and log out
@view_config(
@ -90,7 +95,6 @@ def logout(context, request): @@ -90,7 +95,6 @@ def logout(context, request):
)
def registration_form(context, request):
''' display a registration form '''
context.nav_highlight = 'register'
form = RegistrationSchema.as_form(request)
return {'form': form}
@ -133,7 +137,7 @@ def registration_form_processing(context, request): @@ -133,7 +137,7 @@ def registration_form_processing(context, request):
'Your account <em>{}</em> has been created.'.format(account.user_name),
dismissable=False
)
if len(appstruct['password']) < 8:
if len(appstruct['password']) < MIN_PW_LENGTH:
request.flash(
'warning',
'You should really consider using a longer password.',
@ -187,7 +191,7 @@ def settings_form(context, request): @@ -187,7 +191,7 @@ def settings_form(context, request):
renderer='ordr2:templates/account/settings.jinja2'
)
def settings_form_processing(context, request):
''' display the user settings form '''
''' process the user settings form '''
if 'Cancel' in request.POST:
return HTTPFound(request.resource_url(request.root))
@ -205,7 +209,7 @@ def settings_form_processing(context, request): @@ -205,7 +209,7 @@ def settings_form_processing(context, request):
request.user.email = appstruct['general']['email']
if appstruct['change_password']['new_password']:
request.user.set_password(appstruct['change_password']['new_password'])
if len(appstruct['change_password']['new_password']) < 8:
if len(appstruct['change_password']['new_password']) < MIN_PW_LENGTH:
request.flash(
'warning',
'You should really consider using a longer password.'
@ -249,6 +253,7 @@ def reset_password_form_processing(context, request): @@ -249,6 +253,7 @@ def reset_password_form_processing(context, request):
except deform.ValidationFailure as e:
return {'form': form}
# form validation sucessful, change password and remove reset token
context.model.set_password(appstruct['new_password'])
context.model.password_reset = ''
@ -257,6 +262,12 @@ def reset_password_form_processing(context, request): @@ -257,6 +262,12 @@ def reset_password_form_processing(context, request):
'Password reset successful',
'Please Log In with your new password',
)
if len(appstruct['new_password']) < MIN_PW_LENGTH:
request.flash(
'warning',
'You should really consider using a longer password.'
)
return HTTPFound(request.resource_url(request.root, 'account', 'login'))

44
ordr2/views/admin.py

@ -1,3 +1,5 @@ @@ -1,3 +1,5 @@
''' views for the admin section '''
import deform
from pyramid.httpexceptions import HTTPFound
@ -37,7 +39,7 @@ def admin_section(context, request): @@ -37,7 +39,7 @@ def admin_section(context, request):
return {}
# user list and user editing
# user list
@view_config(
context='ordr2:resources.UserList',
@ -71,6 +73,7 @@ def change_column_view(context, request): @@ -71,6 +73,7 @@ def change_column_view(context, request):
request_method='POST'
)
def user_search(context, request):
''' process the search user form '''
term = request.POST.get('search', '')
term = term.strip()
if term:
@ -87,6 +90,7 @@ def user_search(context, request): @@ -87,6 +90,7 @@ def user_search(context, request):
renderer='ordr2:templates/admin/users_delete.jinja2'
)
def delete_multiple_accounts_form(context, request):
''' show confirmation page for deleting users '''
account_ids = [v for k, v in request.POST.items() if k == 'marked']
accounts = request.dbsession.\
query(User).\
@ -107,6 +111,7 @@ def delete_multiple_accounts_form(context, request): @@ -107,6 +111,7 @@ def delete_multiple_accounts_form(context, request):
renderer='ordr2:templates/admin/users_change_roles.jinja2'
)
def edit_multiple_roles_form(context, request):
''' show form for editing multiple user roles '''
account_ids = [v for k, v in request.POST.items() if k == 'marked']
accounts = request.dbsession.\
query(User).\
@ -126,6 +131,7 @@ def edit_multiple_roles_form(context, request): @@ -126,6 +131,7 @@ def edit_multiple_roles_form(context, request):
request_method='POST'
)
def edit_multiple_roles_form_processing(context, request):
''' form processing for editing multiple user roles '''
if 'change' in request.POST:
count = 0
@ -155,6 +161,8 @@ def edit_multiple_roles_form_processing(context, request): @@ -155,6 +161,8 @@ def edit_multiple_roles_form_processing(context, request):
return HTTPFound(context.url())
# editing one user account
@view_config(
context='ordr2:resources.UserAccount',
permission='edit',
@ -186,9 +194,19 @@ def user_account_form_processing(context, request): @@ -186,9 +194,19 @@ def user_account_form_processing(context, request):
form = UserSchema.as_form(request)
data = request.POST.items()
if 'delete' in request.POST:
# redirect to delete user confirmation page
return HTTPFound(request.resource_url(context, 'delete'))
elif 'reset' in request.POST:
# create a password reset token and notify user
token = context.model.generate_password_token()
event = PasswordReset(request, context.model, token)
request.registry.notify(event)
msg = 'Password reset mail sent to {}.'.format(context.model.email)
request.flash('success', msg)
elif 'save' in request.POST:
try:
appstruct = form.validate(data)
@ -217,16 +235,6 @@ def user_account_form_processing(context, request): @@ -217,16 +235,6 @@ def user_account_form_processing(context, request):
)
request.flash('success', msg, text)
elif 'reset' in request.POST:
token = context.model.generate_password_token()
event = PasswordReset(request, context.model, token)
request.registry.notify(event)
msg = 'Password reset mail sent to {}.'.format(context.model.email)
request.flash('success', msg)
elif 'delete' in request.POST:
return HTTPFound(context, 'delete')
return HTTPFound(context.__parent__.url())
@ -238,6 +246,7 @@ def user_account_form_processing(context, request): @@ -238,6 +246,7 @@ def user_account_form_processing(context, request):
renderer='ordr2:templates/admin/users_delete.jinja2'
)
def user_delete_form(context, request):
''' delete user confirmation page '''
return {'accounts': [context.model]}
@ -254,6 +263,7 @@ def user_delete_form(context, request): @@ -254,6 +263,7 @@ def user_delete_form(context, request):
request_method='POST'
)
def user_delete_form_processing(context, request):
''' delete one or multiple users after confirmation '''
if 'delete' in request.POST:
account_ids = [v for k, v in request.POST.items() if k == 'account']
accounts = request.dbsession.\
@ -272,7 +282,7 @@ def user_delete_form_processing(context, request): @@ -272,7 +282,7 @@ def user_delete_form_processing(context, request):
return HTTPFound(request.resource_url(request.root, 'admin', 'users'))
# consumables
# consumables list
@view_config(
context='ordr2:resources.ConsumableList',
@ -294,6 +304,7 @@ def consumable_list(context, request): @@ -294,6 +304,7 @@ def consumable_list(context, request):
request_method='POST'
)
def consumable_search(context, request):
''' process the search consumable form '''
term = request.POST.get('search', '')
term = term.strip()
if term:
@ -301,6 +312,8 @@ def consumable_search(context, request): @@ -301,6 +312,8 @@ def consumable_search(context, request):
return HTTPFound(context.url())
# adding a consumable
@view_config(
context='ordr2:resources.ConsumableList',
name='new',
@ -351,6 +364,8 @@ def consumable_new_form_processing(context, request): @@ -351,6 +364,8 @@ def consumable_new_form_processing(context, request):
return HTTPFound(context.url())
# edit a consumable
@view_config(
context='ordr2:resources.ConsumableResource',
permission='edit',
@ -407,11 +422,14 @@ def consumable_edit_form_processing(context, request): @@ -407,11 +422,14 @@ def consumable_edit_form_processing(context, request):
request.flash('success', msg)
elif 'delete' in request.POST and context.model:
# redirect to delete consumable confirmation page
return HTTPFound(request.resource_url(context, 'delete'))
return HTTPFound(context.__parent__.url())
# delete consumable
@view_config(
context='ordr2:resources.ConsumableResource',
name='delete',
@ -420,6 +438,7 @@ def consumable_edit_form_processing(context, request): @@ -420,6 +438,7 @@ def consumable_edit_form_processing(context, request):
renderer='ordr2:templates/admin/consumable_delete.jinja2'
)
def consumable_delete_form(context, request):
''' delete consumable confirmation page '''
return {'consumables': [context.model]}
@ -430,6 +449,7 @@ def consumable_delete_form(context, request): @@ -430,6 +449,7 @@ def consumable_delete_form(context, request):
request_method='POST'
)
def consumable_delete_form_processing(context, request):
''' delete consumable after confirmation '''
if 'delete' in request.POST:
c_ids = [v for k, v in request.POST.items() if k == 'consumable']
consumables = request.dbsession.\

42
ordr2/views/orders.py

@ -1,9 +1,11 @@ @@ -1,9 +1,11 @@
''' views for creating and editing orders '''
import deform
import io
import xlsxwriter
from datetime import datetime
from collections import OrderedDict
from datetime import datetime
from pyramid.httpexceptions import HTTPFound
from pyramid.renderers import render
@ -20,6 +22,7 @@ from . import update_column_display @@ -20,6 +22,7 @@ from . import update_column_display
# helper method
def change_in_order_status(request, order, old):
''' notifies a user if a noteworthy change in a order occured '''
noteworthy = False
if old != OrderStatus.ORDERED and order.status == OrderStatus.ORDERED:
noteworthy = True
@ -35,7 +38,7 @@ def change_in_order_status(request, order, old): @@ -35,7 +38,7 @@ def change_in_order_status(request, order, old):
request.registry.notify(event)
# oder list and multiple editing
# oder list
@view_config(
context='ordr2:resources.OrderList',
@ -70,7 +73,9 @@ def change_column_view(context, request): @@ -70,7 +73,9 @@ def change_column_view(context, request):
renderer='ordr2:templates/orders/edit_multiple_stati.jinja2'
)
def download_view(context, request):
''' see https://xlsxwriter.readthedocs.io/example_http_server3.html '''
''' downloads the displayed order list as an excel file
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
@ -134,6 +139,7 @@ def download_view(context, request): @@ -134,6 +139,7 @@ def download_view(context, request):
request_method='POST'
)
def search(context, request):
''' process the search order form '''
term = request.POST.get('search', '')
term = term.strip()
if term:
@ -150,6 +156,7 @@ def search(context, request): @@ -150,6 +156,7 @@ def search(context, request):
renderer='ordr2:templates/orders/edit_multiple_stati.jinja2'
)
def edit_multiple_stati_form(context, request):
''' form for editing the stati of multiple orders '''
order_ids = [v for k, v in request.POST.items() if k == 'marked']
orders = request.dbsession.\
query(Order).\
@ -169,6 +176,7 @@ def edit_multiple_stati_form(context, request): @@ -169,6 +176,7 @@ def edit_multiple_stati_form(context, request):
request_method='POST'
)
def edit_multiple_stati_form_processing(context, request):
''' change the stati of multiple orders '''
if 'change' in request.POST:
count = 0
@ -205,6 +213,7 @@ def edit_multiple_stati_form_processing(context, request): @@ -205,6 +213,7 @@ def edit_multiple_stati_form_processing(context, request):
renderer='ordr2:templates/orders/delete.jinja2'
)
def delete_multiple_orders_form(context, request):
''' show confirmation page for deleting multiple orders '''
order_ids = [v for k, v in request.POST.items() if k == 'marked']
orders = request.dbsession.\
query(Order).\
@ -229,6 +238,7 @@ def delete_multiple_orders_form(context, request): @@ -229,6 +238,7 @@ def delete_multiple_orders_form(context, request):
request_method='POST'
)
def order_delete_form_processing(context, request):
''' delete one or multiple orders after confirmation '''
if 'delete' in request.POST:
order_ids = [v for k, v in request.POST.items() if k == 'order']
orders = request.dbsession.\
@ -247,7 +257,7 @@ def order_delete_form_processing(context, request): @@ -247,7 +257,7 @@ def order_delete_form_processing(context, request):
return HTTPFound(request.resource_url(request.root, 'orders'))
# single order processing
# Single Order views
@view_config(
context='ordr2:resources.OrderResource',
@ -256,8 +266,10 @@ def order_delete_form_processing(context, request): @@ -256,8 +266,10 @@ def order_delete_form_processing(context, request):
renderer='ordr2:templates/orders/view.jinja2'
)
def order_view(context, request):
''' show the order information '''
return {}
@view_config(
context='ordr2:resources.OrderResource',
name='delete',
@ -266,6 +278,7 @@ def order_view(context, request): @@ -266,6 +278,7 @@ def order_view(context, request):
renderer='ordr2:templates/orders/delete.jinja2'
)
def order_delete_form(context, request):
''' show the confirmation page for deleting one order '''
return {'orders': [context.model]}
@ -277,6 +290,7 @@ def order_delete_form(context, request): @@ -277,6 +290,7 @@ def order_delete_form(context, request):
renderer='ordr2:templates/orders/edit.jinja2'
)
def order_edit_form(context, request):
''' show the edit order form '''
form = EditOrderSchema.as_form(request)
order = context.model
info = {
@ -323,7 +337,7 @@ def order_edit_form(context, request): @@ -323,7 +337,7 @@ def order_edit_form(context, request):
renderer='ordr2:templates/orders/edit.jinja2'
)
def order_edit_form_processing(context, request):
''' process the consumable edit form '''
''' process the edit order form '''
form = EditOrderSchema.as_form(request)
data = request.POST.items()
@ -375,9 +389,11 @@ def order_edit_form_processing(context, request): @@ -375,9 +389,11 @@ def order_edit_form_processing(context, request):
request.flash('success', msg)
elif 'delete' in request.POST and context.model:
# redirect to delete order confirmation page
return HTTPFound(request.resource_url(context, 'delete'))
elif 'reorder' in request.POST and context.model:
# redirect to create new order form
return HTTPFound(
request.resource_url(
context.__parent__,
@ -397,8 +413,11 @@ def order_edit_form_processing(context, request): @@ -397,8 +413,11 @@ def order_edit_form_processing(context, request):
renderer='ordr2:templates/orders/new.jinja2'
)
def order_new_form(context, request):
''' create a new order '''
form = NewOrderSchema.as_form(request)
# check if the form should be prefilled with some data,
# either from selecting a consumable or from reordering an item
consumable_id = request.GET.get('consumable', None)
order_id = request.GET.get('reorder', None)
prefill = None
@ -408,7 +427,9 @@ def order_new_form(context, request): @@ -408,7 +427,9 @@ def order_new_form(context, request):
elif consumable_id:
prefill = request.dbsession.query(Consumable).get(consumable_id)
# prefill the form data
if prefill:
# some fields depend on if the prefill was a reorder or consumable
quantity = prefill.amount if order_id else 1
total_price = prefill.total_price if order_id else prefill.unit_price
account = prefill.account if order_id else ''
@ -453,6 +474,7 @@ def order_new_form(context, request): @@ -453,6 +474,7 @@ def order_new_form(context, request):
renderer='ordr2:templates/orders/splash.jinja2'
)
def order_splash(context, request):
''' splash screen for a new order where consumables can be selected '''
structured = OrderedDict()
for cat in Category:
structured[cat] = []
@ -475,6 +497,7 @@ def order_splash(context, request): @@ -475,6 +497,7 @@ def order_splash(context, request):
renderer='ordr2:templates/orders/splash.jinja2'
)
def order_splash_processing(context, request):
''' process the splash screen selection '''
name = request.POST.get('search')
consumable = request.dbsession.\
query(Consumable).\
@ -501,7 +524,7 @@ def order_splash_processing(context, request): @@ -501,7 +524,7 @@ def order_splash_processing(context, request):
renderer='ordr2:templates/orders/new.jinja2'
)
def order_new_form_processing(context, request):
''' process the consumable edit form '''
''' process the new order form '''
form = NewOrderSchema.as_form(request)
data = request.POST.items()
@ -543,10 +566,3 @@ def order_new_form_processing(context, request): @@ -543,10 +566,3 @@ def order_new_form_processing(context, request):
return HTTPFound(context.url())

2
ordr2/views/pages.py

@ -1,3 +1,5 @@ @@ -1,3 +1,5 @@
''' views for static pages '''
from pyramid.httpexceptions import HTTPFound
from pyramid.view import view_config