Compare commits

...
This repo is archived. You can view files and clone it, but cannot push or open issues/pull-requests.

100 Commits

Author SHA1 Message Date
Holger Frey 62a25301a8 Bump version: 0.1.3 → 0.1.4 7 years ago
Holger Frey 947a4798f1 only notify on order change if not own order 7 years ago
Holger Frey 20e18bed57 status in order change also recorded for multiple selections 7 years ago
Holger Frey 880416ade6 added date formatting 7 years ago
Holger Frey 31f22e3128 Bump version: 0.1.2 → 0.1.3 7 years ago
Holger Frey c17e15ee6d multiple selection in order list only for purchasers and admins 7 years ago
Holger Frey df7f0a29e2 google fonts loaded via https 7 years ago
Holger Frey bc78ffcdde updated bootstrap-typahead 7 years ago
Holger Frey 4ba8214262 added update-order-app script to gitignore 7 years ago
Holger Frey 6c76c8d76e redirecting from old orders list view to new url 7 years ago
Holger Frey 14aa56d111 Bump version: 0.1.1 → 0.1.2 7 years ago
Holger Frey f7f45bbdd8 send admin email on exception 7 years ago
Holger Frey 7a757a0ef7 made exceptions templates independent from layout.jinja2 7 years ago
Holger Frey 1585964dc5 added new import data 7 years ago
Holger Frey 0f6d51e22a Bump version: 0.1.0 → 0.1.1 7 years ago
Holger Frey de71b6fa9d moved production.ini 7 years ago
Holger Frey 71c1b5fd76 updated production.ini 7 years ago
Holger Frey 648454fffc added more documentation 7 years ago
Holger Frey a1d9d30b68 Bump version: 0.0.1 → 0.1.0 7 years ago
Holger Frey b5da0d3411 added lots of code documentation 7 years ago
Holger Frey f74918b058 added more custom error pages 7 years ago
Holger Frey d9d630a2af added search for orders, users and consumables 7 years ago
Holger Frey 7e601040b9 added order view 7 years ago
Holger Frey ed9d9c22e1 order status not changable for normal users 7 years ago
Holger Frey 59bc6a8f6c added new order splash screen 7 years ago
Holger Frey b9e3be30ce added editing orders and placing custom orders 7 years ago
Holger Frey 76e0d4e034 added order schemas 7 years ago
Holger Frey 8a2c072c35 added custom money input schema 7 years ago
Holger Frey 2178f37d6a deletion of orders working 7 years ago
Holger Frey e8305b97a3 status change on multiple orders 7 years ago
Holger Frey bfc782560d added colored status macro 7 years ago
Holger Frey 26f7ea962c added viewing order list 7 years ago
Holger Frey 8c4adaad1f modified filter box macro to add multiple boxes on one page 7 years ago
Holger Frey a5a13585d6 added order resources 7 years ago
Holger Frey 2add325e5a changed user principal from id to username 7 years ago
Holger Frey 1d2f5ab0dd added import of order data 7 years ago
Holger Frey f6f609957c added order model 7 years ago
Holger Frey abb0a6afc1 crud for consumables 7 years ago
Holger Frey 627826ab03 added consumable resources 7 years ago
Holger Frey db474eec20 added models for consumables and categories 7 years ago
Holger Frey 30df8627db passwort reset links are working 7 years ago
Holger Frey c638ede5a3 multiple user edits and deltes working 7 years ago
Holger Frey ecf938353e deleting user accounts works 7 years ago
Holger Frey 5e5fe19af3 editing single user works 7 years ago
Holger Frey f6b1b127d6 moved subject for user notification email to events class 7 years ago
Holger Frey 437872da91 generalized user notifications 7 years ago
Holger Frey f0ee6c205e added user notifications 7 years ago
Holger Frey 99bf420f51 added requirement for pyramid mailer 7 years ago
Holger Frey fe6e13aa09 refactored column display selection 7 years ago
Holger Frey e82416882b bugfix in deferred_unique_email_validator 7 years ago
Holger Frey 27ea681279 added user edit form 7 years ago
Holger Frey bafb16453a added child resource lookup to pagination base resource 7 years ago
Holger Frey c754b23daa added showing and hiding of column tables 7 years ago
Holger Frey 914b17e193 added events system 7 years ago
Holger Frey f216996d89 modified role filter as query parameter 7 years ago
Holger Frey ad73f427db fixed default sorting for user list resource 7 years ago
Holger Frey bd0b8fd0a2 added user list view 7 years ago
Holger Frey b01bf05d82 added admin user resources 7 years ago
Holger Frey d12374d25c added base resources for paginated queries 7 years ago
Holger Frey da4db69146 added migration for users 7 years ago
Holger Frey 018531f163 added tqdm to setup requirements 7 years ago
Holger Frey d5dd56e254 added PyYaml to setup requirements 7 years ago
Holger Frey 149dfbc19e moved child nodes to base resource class 7 years ago
Holger Frey cbd6329142 added admin section 7 years ago
Holger Frey 35537e1ae2 added account settings 7 years ago
Holger Frey 6ed06eff82 added deform widgets for deactivated fields 7 years ago
Holger Frey 15de311063 cache_max_age for static views configurable in ini file 7 years ago
Holger Frey e06acbb26b tweaked look of flash messages 7 years ago
Holger Frey 738e4b90b4 added check for unique username in registration 7 years ago
Holger Frey ae6cbac58d initializing database will delete existing sqlite file 7 years ago
Holger Frey ee9df92675 finished first version of registration form 7 years ago
Holger Frey d15523e8a2 adapter javascript to new registration form 7 years ago
Holger Frey 7a919daacf changed templates regarding conrols section 7 years ago
Holger Frey 1285202483 added first view for user registration 7 years ago
Holger Frey d641266b0d template for user registration 7 years ago
Holger Frey db53f7e7c1 default config for registration form added 7 years ago
Holger Frey 44d5e26ca6 simplified form generation from schema 7 years ago
Holger Frey 19de23927e overriding deform templates, missing config 7 years ago
Holger Frey d41d541eb0 overriding deform templates 7 years ago
Holger Frey df13a65727 moved static view config to views package 7 years ago
Holger Frey 5b0709c8cb added account registration schema 7 years ago
Holger Frey 0cb5057c00 added deform to requirements 7 years ago
Holger Frey 6f334852eb flash messages allow a dismissable parameter 7 years ago
Holger Frey ca578303f1 added session flash messages 7 years ago
Holger Frey 725a23979b added basic user login / logout 7 years ago
Holger Frey 4c8e5c69da 'about' navigation highlight set explicitly 7 years ago
Holger Frey 3bfe633260 added Account resource 7 years ago
Holger Frey a1291e5774 redirect default view depending on user login 7 years ago
Holger Frey 77d58da148 reworked error views 7 years ago
Holger Frey 553e01f119 templates referenced by package name 7 years ago
Holger Frey 3d1f52ff8f forgot to add security settings to configurator 7 years ago
Holger Frey 779be58392 adjusted templates to acl setup 7 years ago
Holger Frey 0ae93d5514 added acls for root resource 7 years ago
Holger Frey 3e51a8cda8 configured app security 7 years ago
Holger Frey 9906795d6d added user model 7 years ago
Holger Frey 3f5b98aaef Root resource stores request 7 years ago
Holger Frey ac703eee12 added pyramid.session 7 years ago
Holger Frey e2c872a9f5 imported php faq page 7 years ago
Holger Frey 8ac004bb4a moved layout template from php to jinja 7 years ago
Holger Frey cbd851e4a1 import of original static files 7 years ago
  1. 6
      .gitignore
  2. 7
      AUTHORS.rst
  3. 8
      HISTORY.rst
  4. 43
      README.rst
  5. 18
      development.ini
  6. 18
      ordr2/__init__.py
  7. 77
      ordr2/events.py
  8. 10
      ordr2/models/__init__.py
  9. 2
      ordr2/models/meta.py
  10. 18
      ordr2/models/mymodel.py
  11. 135
      ordr2/models/orders.py
  12. 95
      ordr2/models/user.py
  13. 36
      ordr2/resources/__init__.py
  14. 64
      ordr2/resources/account.py
  15. 195
      ordr2/resources/admin.py
  16. 303
      ordr2/resources/base.py
  17. 114
      ordr2/resources/orders.py
  18. 72
      ordr2/schemas/__init__.py
  19. 155
      ordr2/schemas/account.py
  20. 62
      ordr2/schemas/helpers.py
  21. 211
      ordr2/schemas/orders.py
  22. 2
      ordr2/scripts/__init__.py
  23. 4792
      ordr2/scripts/export_consumables.yml
  24. 100590
      ordr2/scripts/export_orders.yml
  25. 715
      ordr2/scripts/export_users.yml
  26. 119
      ordr2/scripts/initializedb.py
  27. 55
      ordr2/security.py
  28. 567
      ordr2/static/css/bootstrap-responsive.css
  29. 3365
      ordr2/static/css/bootstrap.css
  30. 51
      ordr2/static/css/email.css
  31. 753
      ordr2/static/css/style.css
  32. BIN
      ordr2/static/img/bg.png
  33. BIN
      ordr2/static/img/favicon.ico
  34. BIN
      ordr2/static/img/sprite.png
  35. 91
      ordr2/static/js/bootstrap-alert.js
  36. 136
      ordr2/static/js/bootstrap-collapse.js
  37. 92
      ordr2/static/js/bootstrap-dropdown.js
  38. 209
      ordr2/static/js/bootstrap-modal.js
  39. 270
      ordr2/static/js/bootstrap-tooltip.js
  40. 51
      ordr2/static/js/bootstrap-transition.js
  41. 335
      ordr2/static/js/bootstrap-typeahead.js
  42. 118
      ordr2/static/js/functions.js
  43. 9252
      ordr2/static/js/jquery.js
  44. BIN
      ordr2/static/pyramid-16x16.png
  45. BIN
      ordr2/static/pyramid.png
  46. 154
      ordr2/static/theme.css
  47. 8
      ordr2/templates/404.jinja2
  48. 39
      ordr2/templates/account/login.jinja2
  49. 24
      ordr2/templates/account/password_reset.jinja2
  50. 23
      ordr2/templates/account/register.jinja2
  51. 32
      ordr2/templates/account/register_sucessful.jinja2
  52. 24
      ordr2/templates/account/settings.jinja2
  53. 36
      ordr2/templates/admin/admin_section.jinja2
  54. 70
      ordr2/templates/admin/consumable_delete.jinja2
  55. 24
      ordr2/templates/admin/consumable_edit.jinja2
  56. 81
      ordr2/templates/admin/consumable_list.jinja2
  57. 24
      ordr2/templates/admin/consumable_new.jinja2
  58. 24
      ordr2/templates/admin/user_edit.jinja2
  59. 135
      ordr2/templates/admin/user_list.jinja2
  60. 79
      ordr2/templates/admin/users_change_roles.jinja2
  61. 66
      ordr2/templates/admin/users_delete.jinja2
  62. 97
      ordr2/templates/deform/form.pt
  63. 51
      ordr2/templates/deform/mapping_item.pt
  64. 36
      ordr2/templates/deform/money_mapping.pt
  65. 36
      ordr2/templates/deform/money_mapping_disabled.pt
  66. 25
      ordr2/templates/deform/money_mapping_item.pt
  67. 25
      ordr2/templates/deform/money_mapping_item_diabled.pt
  68. 71
      ordr2/templates/deform/order_info_mapping.pt
  69. 42
      ordr2/templates/deform/select_disabled.pt
  70. 22
      ordr2/templates/deform/textinput_disabled.pt
  71. 25
      ordr2/templates/emails/activation.jinja2
  72. 36
      ordr2/templates/emails/order.jinja2
  73. 25
      ordr2/templates/emails/password_reset.jinja2
  74. 21
      ordr2/templates/errors/bad_csrf_token.jinja2
  75. 65
      ordr2/templates/errors/exception.jinja2
  76. 20
      ordr2/templates/errors/forbidden.jinja2
  77. 20
      ordr2/templates/errors/not_found.jinja2
  78. 129
      ordr2/templates/layout.jinja2
  79. 110
      ordr2/templates/macros.jinja2
  80. 8
      ordr2/templates/mytemplate.jinja2
  81. 66
      ordr2/templates/orders/delete.jinja2
  82. 24
      ordr2/templates/orders/edit.jinja2
  83. 77
      ordr2/templates/orders/edit_multiple_stati.jinja2
  84. 194
      ordr2/templates/orders/list.jinja2
  85. 24
      ordr2/templates/orders/new.jinja2
  86. 69
      ordr2/templates/orders/splash.jinja2
  87. 156
      ordr2/templates/orders/view.jinja2
  88. 76
      ordr2/templates/pages/faq.jinja2
  89. 42
      ordr2/templates/pages/welcome.jinja2
  90. 7
      ordr2/templates/tests.py
  91. 64
      ordr2/views/__init__.py
  92. 273
      ordr2/views/account.py
  93. 469
      ordr2/views/admin.py
  94. 33
      ordr2/views/default.py
  95. 78
      ordr2/views/errors.py
  96. 7
      ordr2/views/notfound.py
  97. 579
      ordr2/views/orders.py
  98. 36
      ordr2/views/pages.py
  99. 20
      production.ini.template
  100. 4
      setup.cfg
  101. Some files were not shown because too many files have changed in this diff Show More

6
.gitignore vendored

@ -1,5 +1,11 @@ @@ -1,5 +1,11 @@
# ignore update script on production server
update-order-app
# ignore sqlite database
ordr2.sqlite
# ignore pyramid_mailer.debug folder
mail/
# ignore production.ini
production.ini
# Byte-compiled / optimized / DLL files
__pycache__/

7
AUTHORS.rst

@ -7,6 +7,13 @@ Development Lead @@ -7,6 +7,13 @@ Development Lead
* Holger Frey <frey@imtek.de>
Original PHP Version
--------------------
* Sebastian Sebald (@sebald on github)
Contributors
------------

8
HISTORY.rst

@ -2,7 +2,13 @@ @@ -2,7 +2,13 @@
History
=======
0.1.0 (2017-10-05, branch php2python)
-------------------------------------
* first working version of the PHP to Python rewrite
0.0.1 (2017-09-26)
------------------
* First release on PyPI.
* setup of the project

43
README.rst

@ -1,10 +1,43 @@ @@ -1,10 +1,43 @@
===========================
Ordr2 - CPI Ordering System
===========================
==============================================
Ordr2 - CPI Ordering System, php2python branch
==============================================
This is a rewrite in Python of the original Ordr system by Sebastian Sebald
that can still be found here: https://github.com/sebald/Ordr
Features
Installation
------------
Installation consists of three steps:
1. clone the project and checkout the php2python branch
> git clone https://git.cpi.imtek.uni-freiburg.de/holgi/ordr2
> cd ordr2
> git checkout php2pyton
2. create a python virtual environment and activate it
> python3 -m venv ordr-venv
> source ordr-venv/bin/activate
3. install the cloned package and deactivate the environment
(ordr-venv) > pip install .
> deactivate
Updating
--------
* TODO
updating consists of three steps:
1. Update the source code
> cd ordr2
> git pull origin php2python
2. activate the python virtual environment
> source ordr-venv/bin/activate
3. install the new version and deactivate the environment
(ordr-venv) > pip install .
> deactivate

18
development.ini

@ -12,6 +12,7 @@ pyramid.debug_notfound = false @@ -12,6 +12,7 @@ pyramid.debug_notfound = false
pyramid.debug_routematch = false
pyramid.default_locale_name = en
pyramid.includes =
pyramid_mailer.debug
pyramid_debugtoolbar
sqlalchemy.url = sqlite:///%(here)s/ordr2.sqlite
@ -20,6 +21,23 @@ sqlalchemy.url = sqlite:///%(here)s/ordr2.sqlite @@ -20,6 +21,23 @@ sqlalchemy.url = sqlite:///%(here)s/ordr2.sqlite
# '127.0.0.1' and '::1'.
# debugtoolbar.hosts = 127.0.0.1 ::1
# email delivery
mail.host = localhost
mail.port = 2525
mail.default_sender = ordr@example.com
# custom settings
auth.secret = 'Change Me 1'
session.secret = 'Change Me 2'
session.auto_csrf = true
static_views.cache_max_age = 0
# custom jinja filters and tests
jinja2.filters =
are_extras_active = ordr2.templates.tests:are_extras_active
###
# wsgi server configuration
###

18
ordr2/__init__.py

@ -1,20 +1,28 @@ @@ -1,20 +1,28 @@
# -*- coding: utf-8 -*-
''' Top-level package for Ordr2. '''
__author__ = 'Holger Frey'
__email__ = 'frey@imtek.de'
__version__ = '0.0.1'
__version__ = '0.1.4'
from pyramid.config import Configurator
from pyramid.session import SignedCookieSessionFactory
def main(global_config, **settings):
''' This function returns a Pyramid WSGI application. '''
config = Configurator(settings=settings)
config.include('pyramid_jinja2')
config.include('.models')
session_factory = SignedCookieSessionFactory(settings['session.secret'])
config.set_session_factory(session_factory)
config.set_default_csrf_options(require_csrf=settings['session.auto_csrf'])
config.include('.resources')
config.include('.models')
config.include('.security')
config.include('.views')
config.include('pyramid_jinja2')
config.scan()
return config.make_wsgi_app()

77
ordr2/events.py

@ -0,0 +1,77 @@ @@ -0,0 +1,77 @@
''' custom events and event subsribers '''
from pyramid.events import NewRequest, subscriber
from pyramid.renderers import render
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):
self.request = request
self.user = user
self.data = data
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},
event.request
)
message = Message(
subject=event.subject,
sender=event.request.registry.settings['mail.default_sender'],
recipients=[event.user.email],
html=body
)
event.request.mailer.send(message)

10
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
@ -5,7 +7,8 @@ import zope.sqlalchemy @@ -5,7 +7,8 @@ import zope.sqlalchemy
# import or define all models here to ensure they are attached to the
# Base.metadata prior to any initialization routines
from .mymodel import MyModel # flake8: noqa
from .user import User, Role # flake8: noqa
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
@ -13,18 +16,19 @@ configure_mappers() @@ -13,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

18
ordr2/models/mymodel.py

@ -1,18 +0,0 @@ @@ -1,18 +0,0 @@
from sqlalchemy import (
Column,
Index,
Integer,
Text,
)
from .meta import Base
class MyModel(Base):
__tablename__ = 'models'
id = Column(Integer, primary_key=True)
name = Column(Text)
value = Column(Integer)
Index('my_index', MyModel.name, unique=True, mysql_length=255)

135
ordr2/models/orders.py

@ -0,0 +1,135 @@ @@ -0,0 +1,135 @@
''' Consumables, Categories, Orders and Order Status Database Models '''
import bcrypt
import enum
import uuid
from collections import namedtuple
from datetime import datetime
from sqlalchemy import (
Column,
DateTime,
Enum,
Float,
Integer,
Text,
)
from .meta import Base
class Category(enum.Enum):
''' Categories of consumables and orders '''
CHEMICAL = 'chemical'
DISPOSABLE = 'disposable'
SOLVENT = 'solvent'
BIOLAB = 'biolab'
class OrderStatus(enum.Enum):
''' status of the order '''
OPEN = 'open'
APPROVAL = 'approval'
ORDERED = 'ordered'
COMPLETED = 'completed'
class Consumable(Base):
''' A consumable '''
__tablename__ = 'consumables'
id = Column(Integer, primary_key=True)
cas_description = Column(Text, nullable=False)
category = Column(Enum(Category), nullable=False)
catalog_nr = Column(Text, nullable=False)
vendor = Column(Text, nullable=False)
package_size = Column(Text, nullable=False)
unit_price = Column(Float, nullable=False)
currency = Column(Text, nullable=False, default='EUR')
comment = Column(Text, nullable=False, default='')
date_created = Column(DateTime, nullable=False, default=datetime.utcnow)
date_modified = Column(
DateTime,
nullable=False,
default=datetime.utcnow,
onupdate=datetime.utcnow
)
def __str__(self):
''' string representation '''
return '{!s} ({!s})'.format(self.cas_description, self.vendor)
class Order(Base):
''' An order '''
__tablename__ = 'orders'
id = Column(Integer, primary_key=True)
status = Column(Enum(OrderStatus), nullable=False)
cas_description = Column(Text, nullable=False)
category = Column(Enum(Category), nullable=False)
catalog_nr = Column(Text, nullable=False)
vendor = Column(Text, nullable=False)
package_size = Column(Text, nullable=False)
unit_price = Column(Float, nullable=False)
currency = Column(Text, nullable=False, default='EUR')
amount = Column(Integer, nullable=False)
total_price = Column(Float, nullable=False)
account = Column(Text, nullable=False, default='')
comment = Column(Text, nullable=False, default='')
created_date = Column(DateTime, nullable=False, default=datetime.utcnow)
created_by = Column(Text, nullable=False)
approval_date = Column(DateTime, nullable=True)
approval_by = Column(Text, nullable=False, default='')
ordered_date = Column(DateTime, nullable=True)
ordered_by = Column(Text, nullable=False, default='')
completed_date = Column(DateTime, nullable=True)
completed_by = Column(Text, nullable=False, default='')
def __str__(self):
''' string representation '''
return '{!s} ({!s})'.format(self.cas_description, self.vendor)
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.strftime('%Y-%m-%d %H:%M'),
some_one
)
# historical data does not have a purchaser associated with a date
return '{!s}'.format(some_date.strftime('%Y-%m-%d %H:%M'))
@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)

95
ordr2/models/user.py

@ -0,0 +1,95 @@ @@ -0,0 +1,95 @@
''' User Account and Roles Models '''
import bcrypt
import enum
import uuid
from collections import namedtuple
from datetime import datetime
from sqlalchemy import (
Column,
Date,
Enum,
Integer,
Text,
)
from .meta import Base
class Role(enum.Enum):
''' roles of user accounts '''
NEW = 'new'
USER = 'user'
PURCHASER = 'purchaser'
ADMIN = 'admin'
INACTIVE = 'inactive'
@property
def principal(self):
''' returns the principal identifier of the role '''
return 'role:' + self.value.lower()
class User(Base):
''' A user of the application '''
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
user_name = Column(Text, nullable=False, unique=True)
first_name = Column(Text, nullable=False)
last_name = Column(Text, nullable=False)
email = Column(Text, nullable=False, unique=True)
password_hash = Column(Text, nullable=False)
role = Column(Enum(Role), nullable=False)
password_reset = Column(Text, nullable=False, default='')
date_created = Column(Date, nullable=False, default=datetime.utcnow)
@property
def principal(self):
''' returns the principal identifier for the user '''
return 'user:' + self.user_name
@property
def role_principals(self):
''' 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
@property
def is_active(self):
''' check if it is an active user account '''
return self.role in (Role.USER, Role.PURCHASER, Role.ADMIN)
def set_password(self, password):
''' hashes a new password '''
pwhash = bcrypt.hashpw(password.encode('utf8'), bcrypt.gensalt())
self.password_hash = pwhash.decode('utf8')
def check_password(self, password):
''' compares a password with a stored password hash '''
if self.password_hash:
expected_hash = self.password_hash.encode('utf8')
return bcrypt.checkpw(password.encode('utf8'), expected_hash)
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
def __str__(self):
''' string representation '''
return '{!s}'.format(self.user_name)

36
ordr2/resources/__init__.py

@ -1,10 +1,37 @@ @@ -1,10 +1,37 @@
class Root(object):
''' base resource and resource root factory '''
from pyramid.security import Allow, Everyone
from .account import Account, PasswordResetAccount
from .admin import (
Admin,
ConsumableList,
ConsumableResource,
UserList,
UserAccount
)
from .base import BaseResource
from .orders import OrderList, OrderResource
class Root(BaseResource):
''' Root resource '''
__name__ = None
__parent__ = None
nodes = {
'account': Account,
'admin': Admin,
'orders': OrderList
}
def __init__(self, request):
self.request = request
def root_factory(request):
return Root()
def __acl__(self):
''' access controll list '''
return [ (Allow, Everyone, 'view') ]
def includeme(config):
@ -14,5 +41,4 @@ def includeme(config): @@ -14,5 +41,4 @@ def includeme(config):
Activate this setup using ``config.include('ordr2.resources')``.
'''
config.set_root_factory(root_factory)
config.add_static_view('static', 'ordr2:static', cache_max_age=3600)
config.set_root_factory(Root)

64
ordr2/resources/account.py

@ -0,0 +1,64 @@ @@ -0,0 +1,64 @@
''' Resources for User Accounts '''
from pyramid.security import Allow, Authenticated, Deny, DENY_ALL, Everyone
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
]
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.\
query(User).\
filter_by(password_reset=key).\
first()
if account:
return PasswordResetAccount(key, self, account)
raise KeyError
class Account(BaseResource):
''' User Account and Settings '''
nodes = {'reset': PasswordReset}
def __init__(self, name, parent):
super().__init__(name, parent)
self.model = self.request.user
def __acl__(self):
''' access controll list '''
return [
(Allow, Everyone, 'login'),
(Allow, Everyone, 'logout'),
(Deny, Authenticated, 'register'),
(Allow, Everyone, 'register'),
(Allow, Authenticated, 'settings'),
(Allow, Everyone, 'reset'),
DENY_ALL
]

195
ordr2/resources/admin.py

@ -0,0 +1,195 @@ @@ -0,0 +1,195 @@
''' Resources for the Admin Section '''
from sqlalchemy import or_
from pyramid.security import Allow, Authenticated, Deny, DENY_ALL, Everyone
from .base import BaseResource, PaginationResourceMixin
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'),
(Allow, 'role:admin', 'delete'),
DENY_ALL
]
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'),
(Allow, 'role:admin', 'delete'),
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()
role = Role(role_name)
query = query.filter_by(role=role)
except (AttributeError, ValueError):
role_name = None
self.filters['role'] = role_name
# filter by search term
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
def prepare_sorted_query(self, query, sorting):
''' add sorting to the base query '''
available_fields = {
'user': 'user_name',
'first': 'first_name',
'last': 'last_name',
'email': 'email',
'role': 'role'
}
name = available_fields.get(sorting.field, None)
model_field = getattr(self.sql_model_class, name, None)
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
# consumables resources
class ConsumableResource(BaseResource):
''' Resource for one consumable '''
def __acl__(self):
''' Access Controll List '''
return [
(Allow, 'role:admin', 'view'),
(Allow, 'role:admin', 'edit'),
(Allow, 'role:admin', 'delete'),
DENY_ALL
]
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'),
(Allow, 'role:admin', 'edit'),
(Allow, 'role:admin', 'delete'),
DENY_ALL
]
def prepare_filtered_query(self, dbsession, filter_params):
''' 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()
category = Category(category_name)
query = query.filter_by(category=category)
except (AttributeError, ValueError):
category_name = None
self.filters['category'] = category_name
# filter by search term
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
def prepare_sorted_query(self, query, sorting):
''' add sorting to the base query '''
available_fields = {
'cas': 'cas_description',
'category': 'category',
'catalog': 'catalog_nr',
'vendor': 'vendor',
'pkg': 'package_size',
'price': 'unit_price',
'currency': 'currency'
}
name = available_fields.get(sorting.field, None)
model_field = getattr(self.sql_model_class, name, None)
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,
'consumables': ConsumableList,
}
def __acl__(self):
''' Access Controll List '''
return [ (Allow, 'role:admin', 'view') ]

303
ordr2/resources/base.py

@ -0,0 +1,303 @@ @@ -0,0 +1,303 @@
''' Base Resource and Mixin classes, not to be used directly '''
from collections import namedtuple
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
request = None
model = None
nodes = dict()
nav_highlight = None
def __init__(self, name, parent, sql_model_instance=None):
self.__name__ = name
self.__parent__ = parent
self.request = parent.request
self.model = sql_model_instance
# call to super().__init_() needed to set up PaginationMixin
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)
try:
return super().__getitem__(key)
except AttributeError:
raise KeyError()
@classmethod
def from_sqla(cls, sql_model_instance, parent):
''' initializes a resource from an SQLalchemy object '''
primary_keys = inspect(sql_model_instance).identity
if primary_keys is None:
raise ValueError('Cannot init resource for primary key: None')
elif len(primary_keys) != 1:
raise ValueError('Cannot init resource for composite primary key')
primary_key = str(primary_keys[0])
return cls(primary_key, parent, sql_model_instance)
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)
self.current = self._is_valid(current, default=self.first)
self.previous = self._is_valid(self.current - 1)
self.next = self._is_valid(self.current + 1)
# window calculations
# example: lets assume the current page is 10 and window size is 7
# self.window = [7, 8, 9, 10, 11, 12, 13]
half_window = window_size // 2
start = self.current - half_window
end = self.current + half_window
calculated_window = range(start, end + 1)
self.window = [p for p in calculated_window if self._is_valid(p)]
def _is_valid(self, page, default=None):
''' checks if the given page is valid, returns default if not '''
if self.count and self.first <= page <= self.last:
return page
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)
items = params.pop(
self.query_key_items_per_page,
self.default_items_per_page
)
sort = params.pop(self.query_key_sorting, self.default_sorting)
# we can now setup a base query with applied filters
self._base_query = self.prepare_filtered_query(
self.request.dbsession,
params
)
# with this base query, the pagination can be calculated:
count = self._base_query.count()
self.pages = Pagination(page, count, items)
# and we should check that we can sort results later
self.sorting = self.parse_sort_parameters(sort)
if self.sorting is None:
msg = 'Error in default sorting {}'.format(self.default_sorting)
raise ValueError(msg)
def prepare_filtered_query(self, dbsession, filter_params):
''' 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'
raise NotImplementedError(msg)
def prepare_sorted_query(self, query, sorting):
''' 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)
return query.order_by(sort_func)
'''
msg = 'Query setup must be implemented in child class'
raise NotImplementedError(msg)
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()
field, direction = sort_param.split('.', 1)
func = sort_functions[direction]
return SortParameter(sort_param, field, direction, func)
except (AttributeError, IndexError, KeyError, ValueError):
return None
def items(self):
''' returns the items of the current page as resources'''
if not self.pages.count:
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
in query.all()
]
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,
self.query_key_sorting: self.sorting.text
}
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()
return self.child_resource_class.from_sqla(model, self)

114
ordr2/resources/orders.py

@ -0,0 +1,114 @@ @@ -0,0 +1,114 @@
from sqlalchemy import or_
from pyramid.security import Allow, Authenticated, Deny, DENY_ALL, Everyone
from .base import BaseResource, PaginationResourceMixin
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(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'),
(Allow, 'role:purchaser', 'edit'),
(Allow, 'role:purchaser', 'delete'),
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 = status_name.lower()
status = OrderStatus(status_name)
query = query.filter_by(status=status)
except (AttributeError, ValueError):
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)
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
def prepare_sorted_query(self, query, sorting):
''' add sorting to the base query '''
available_fields = {
'cas': 'cas_description',
'category': 'category',
'catalog': 'catalog_nr',
'vendor': 'vendor',
'price': 'unit_price',
'amount': 'amount',
'total': 'total_price',
'created': 'created_date',
'user': 'created_by',
'status': 'status',
}
name = available_fields.get(sorting.field, None)
model_field = getattr(self.sql_model_class, name, None)
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

72
ordr2/schemas/__init__.py

@ -0,0 +1,72 @@ @@ -0,0 +1,72 @@
''' 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
)
# Make Deform widgets aware of our widget template paths
configure_zpt_renderer(['ordr2:templates/deform'])
# Base Schema
class CSRFSchema(colander.Schema):
''' base class for schemas with csrf validation '''
csrf_token = colander.SchemaNode(
colander.String(),
default=deferred_csrf_default,
validator=deferred_csrf_validator,
widget=deform.widget.HiddenWidget(),
)
@classmethod
def as_form(cls, request, **kwargs):
''' 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)
form = deform.Form(schema, action=url, **kwargs)
return form
class MoneyInputSchema(colander.Schema):
''' custom schema for structured money and currency input '''
amount = colander.SchemaNode(
colander.Decimal(),
widget=deform.widget.MoneyInputWidget(
readonly_template='textinput_disabled.pt',
css_class='moneyinput amount'
),
)
currency = colander.SchemaNode(
colander.String(),
default='EUR',
widget=deform.widget.TextInputWidget(
readonly_template='textinput_disabled.pt',
css_class='moneyinput currency'
)
)
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(
category='default',
template='money_mapping.pt',
readonly_template='money_mapping_disabled.pt',
item_template='money_mapping_item.pt',
item_readonly_template='money_mapping_item_diabled.pt',
readonly=readonly,
)
super().__init__(*args, **kwargs)

155
ordr2/schemas/account.py

@ -0,0 +1,155 @@ @@ -0,0 +1,155 @@
import colander
import deform
from ordr2.models import Role
from . import CSRFSchema
from .helpers import (
deferred_unique_email_validator,
deferred_unique_username_validator,
deferred_password_validator
)
ROLES = [(role.name, role.value.capitalize()) for role in Role]
# schema for user registration
class RegistrationSchema(CSRFSchema):
''' new user registration '''
user_name = colander.SchemaNode(
colander.String(),
widget=deform.widget.TextInputWidget(
template='textinput_disabled.pt'
),
description='automagically generated for you',
validator = deferred_unique_username_validator,
)
first_name = colander.SchemaNode(
colander.String()
)
last_name = colander.SchemaNode(
colander.String()
)
email = colander.SchemaNode(
colander.String(),
validator=deferred_unique_email_validator
)
password = colander.SchemaNode(
colander.String(),
widget=deform.widget.CheckedPasswordWidget()
)
@classmethod
def as_form(cls, request, **override):
settings = {
'buttons': ('Create Account', 'Cancel'),
'css_class': 'form-horizontal registration'
}
settings.update(override)
return super().as_form(request, **settings)
# schema for user settings
class UserSchema(CSRFSchema):
''' user settings schema '''
user_name = colander.SchemaNode(
colander.String(),
widget=deform.widget.TextInputWidget(
template='textinput_disabled.pt'
),
)
first_name = colander.SchemaNode(
colander.String()
)
last_name = colander.SchemaNode(
colander.String()
)
email = colander.SchemaNode(
colander.String(),
validator=deferred_unique_email_validator
)
role = colander.SchemaNode(
colander.String(),
widget=deform.widget.SelectWidget(values=ROLES)
)
@classmethod
def as_form(cls, request, **override):
settings = {
'buttons': (
deform.Button(name='save', title='Save changes'),
deform.Button(
name='delete',
title='Delete user',
css_class='btn-danger'
),
deform.Button(name='reset', title='Reset password'),
deform.Button(name='cancel', title='Cancel')
),
'css_class': 'form-horizontal',
}
settings.update(override)
return super().as_form(request, **settings)
class ChangePasswordSchema(CSRFSchema):
''' change password of an account '''
new_password = colander.SchemaNode(
colander.String(),
widget=deform.widget.CheckedPasswordWidget(),
missing=''
)
class ConfirmSettingsSchema(CSRFSchema):
''' confirm changes with current password '''
current_password = colander.SchemaNode(
colander.String(),
widget=deform.widget.PasswordWidget(),
description='Enter your current password to confirm changes',
validator=deferred_password_validator
)
class SettingsSchema(CSRFSchema):
general = UserSchema()
change_password = ChangePasswordSchema()
confirm_changes = ConfirmSettingsSchema()
@classmethod
def as_form(cls, request, **override):
settings = {
'buttons': ('Save Settings', 'Cancel'),
'css_class': 'form-horizontal user-settings'
}
settings.update(override)
form = super().as_form(request, **settings)
# disable the role field for user settings
form['general']['role'].widget = deform.widget.TextInputWidget(
template='textinput_disabled.pt'
)
return form
class ResetPasswordSchema(CSRFSchema):
''' reset password of an account '''
new_password = colander.SchemaNode(
colander.String(),
widget=deform.widget.CheckedPasswordWidget()
)
@classmethod
def as_form(cls, request, **override):
settings = {
'buttons': ('Change Password', 'Cancel'),
'css_class': 'form-horizontal'
}
settings.update(override)
return super().as_form(request, **settings)

62
ordr2/schemas/helpers.py

@ -0,0 +1,62 @@ @@ -0,0 +1,62 @@
import colander
import deform
from pyramid.csrf import get_csrf_token, check_csrf_token
from ordr2.models import User
@colander.deferred
def deferred_csrf_default(node, kw):
''' sets the current csrf token '''
request = kw.get('request')
return get_csrf_token(request)
@colander.deferred
def deferred_csrf_validator(node, kw):
''' validates a submitted csrf token '''
def validate_csrf(node, value):
request = kw.get('request')
if not check_csrf_token(request, raises=False):
raise colander.Invalid(node, 'Bad CSRF token')
return validate_csrf
@colander.deferred
def deferred_unique_username_validator(node, kw):
''' checks if an username is not registered already '''
def validate_unique_username(node, value):
request = kw.get('request')
user = request.dbsession.query(User).filter_by(user_name=value).first()
if user is not None:
raise colander.Invalid(node, 'User name already registered')
return validate_unique_username
@colander.deferred
def deferred_unique_email_validator(node, kw):
''' checks if an email is not registered already '''
email_validator = colander.Email()
def validate_unique_email(node, value):
email_validator(node, value) # raises exception on invalid address
request = kw.get('request')
user = request.dbsession.query(User).filter_by(email=value).first()
if user not in (None, request.context.model):
# allow existing email addresses if
# it belongs to the user that is currently edited
raise colander.Invalid(node, 'Email address in use')
return validate_unique_email
@colander.deferred
def deferred_password_validator(node, kw):
''' checks password confirmation for settings '''
def validate_password_confirmation(node, value):
request = kw.get('request')
if request.user is None or not request.user.check_password(value):
raise colander.Invalid(node, 'Wrong password')
return validate_password_confirmation

211
ordr2/schemas/orders.py

@ -0,0 +1,211 @@ @@ -0,0 +1,211 @@
''' schemas for creating and editing orders and consumables'''
import colander
import deform
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]
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 = MoneyInputSchema(
readonly=False
)
comment = colander.SchemaNode(
colander.String(),
widget=deform.widget.TextAreaWidget(rows=5),
missing=''
)
@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 = {
'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)
class OrderInformation(colander.Schema):
''' schema for editing order status
parital schema, used in EditOrderSchema
'''
status = colander.SchemaNode(
colander.String(),
widget=deform.widget.SelectWidget(values=STATI)
)
class OrderItem(colander.Schema):
''' schema for editing item information
parital schema, used in NewOrderSchema and EditOrderSchema
'''
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()
)
class OrderPricing(colander.Schema):
''' schema for editing price information
parital schema, used in NewOrderSchema and EditOrderSchema
'''
unit_price = MoneyInputSchema(
readonly=False
)
quantity = colander.SchemaNode(
colander.Integer(),
validator=colander.Range(min=1),
widget=deform.widget.TextInputWidget(
css_class='number'
),
default=1
)
total_price = MoneyInputSchema(
readonly=True
)
class OrderOptionals(colander.Schema):
''' schema for editing optional order information
parital schema, used in NewOrderSchema and EditOrderSchema
'''
account = colander.SchemaNode(
colander.String(),
missing=''
)
comment = colander.SchemaNode(
colander.String(),
widget=deform.widget.TextAreaWidget(rows=5),
missing=''
)
class NewOrderSchema(CSRFSchema):
''' schema for a new order '''
item_information = OrderItem()
pricing = OrderPricing()
optional_information = OrderOptionals()
@classmethod
def as_form(cls, request, **override):
''' returns the schema as a form '''
settings = {
'buttons': (
deform.Button(name='save', title='Place Order'),
deform.Button(name='cancel', title='Cancel')
),
'css_class': 'form-horizontal'
}
settings.update(override)
return super().as_form(request, **settings)
class EditOrderSchema(CSRFSchema):
''' schema for editing an order '''
order_information = OrderInformation(
widget=deform.widget.MappingWidget(
template='order_info_mapping.pt'
)
)
item_information = OrderItem()
pricing = OrderPricing()
optional_information = OrderOptionals()
@classmethod
def as_form(cls, request, **override):
''' returns the schema as a form '''
settings = {
'buttons': (
deform.Button(name='save', title='Edit Order'),
deform.Button(
name='reorder',
title='Reorder',
css_class='btn-success'
),
deform.Button(
name='delete',
title='Delete Order',
css_class='btn-danger'
),
deform.Button(name='cancel', title='Cancel')
),
'css_class': 'form-horizontal'
}
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(
template='textinput_disabled.pt'
)
return form

2
ordr2/scripts/__init__.py

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

4792
ordr2/scripts/export_consumables.yml

File diff suppressed because it is too large Load Diff

100590
ordr2/scripts/export_orders.yml

File diff suppressed because it is too large Load Diff

715
ordr2/scripts/export_users.yml

@ -0,0 +1,715 @@ @@ -0,0 +1,715 @@
%YAML 1.1
---
# imtekordr.users
-
id: 99
username: "MartinSchoenstein"
first_name: "Martin"
last_name: "Schoenstein"
password: "ea8458396a0c17954f9a9bc5b835f81148a39bcb"
email: "schoenst@imtek.de"
role: "Purchaser"
date_created: "2012-04-05 14:29:21"
-
id: 98
username: "DanielaMoessner"
first_name: "Daniela"
last_name: "Moessner"
password: "5ee566ad84396a2a26d7f6fcdf589a261f202569"
email: "moessner@imtek.de"
role: "Purchaser"
date_created: "2012-03-30 09:35:40"
-
id: 97
username: "AlexanderDietz"
first_name: "Alexander"
last_name: "Dietz"
password: "621d0004efe268724a86473649ae3ced2871e1c9"
email: "alexander.dietz@imtek.uni-freiburg.de"
role: "Purchaser"
date_created: "2012-03-29 16:39:29"
-
id: 94
username: "ASasdasdgase"
first_name: "ASasd"
last_name: "asdgase"
password: "7c4a8d09ca3762af61e59520943dc26494f8941b"
email: "axsd@sdasd.com"
role: "Inactive"
date_created: "2012-02-15 15:27:34"
-
id: 82
username: "SebastianSebald"
first_name: "Sebastian"
last_name: "Sebald"
password: "9b3e1fa230b050fb324086e2fcc94b1b9aa2ac35"
email: "sebastian.sebald@gmail.com"
role: "Admin"
date_created: "2012-01-20 13:12:53"
-
id: 89
username: "HolgerFrey"
first_name: "Holger"
last_name: "Frey"
password: "9999fc0774280169f52232de4500e0077a314d0c"
email: "holger.frey@imtek.uni-freiburg.de"
role: "Admin"
date_created: "2012-01-20 21:35:40"
-
id: 100
username: "MaxMustermann"
first_name: "Max"
last_name: "Mustermann"
password: "af0ba23d48fe7db684d9a9d13eb5ec0f5183081c"
email: "oswaldprucker@web.de"
role: "Inactive"
date_created: "2012-05-16 08:40:37"
-
id: 91
username: "NataliaSchatz"
first_name: "Natalia"
last_name: "Schatz"
password: "5f89fc9b6033aa210658489a14bf865a096dd702"
email: "schatz@imtek.de"
role: "Admin"
date_created: "2012-01-20 21:37:32"
-
id: 92
username: "OswaldPrucker"
first_name: "Oswald"
last_name: "Prucker"
password: "4aa75e8bc73f4bc072133a10acb728633d83eb20"
email: "prucker@imtek.de"
role: "Admin"
date_created: "2012-01-20 22:22:27"
-
id: 96
username: "MalwinaPajestka"
first_name: "Malwina"
last_name: "Pajestka"
password: "cf8abbfad4ea9a10200e6b0b6008ac55dcbc499e"
email: "malwina.pajestka@imtek.de"
role: "Purchaser"
date_created: "2012-03-29 14:47:44"
-
id: 155
username: "AlexandraSchneider"
first_name: "Alexandra"
last_name: "Schneider"
password: "f3d4facdde3f40ad5abaf8b8bb17f2da7b08af32"
email: "alexandra.schneider@jupiter.uni-freiburg"
role: "User"
date_created: "2015-08-04 16:45:44"
-
id: 102
username: "MichaelHenze"
first_name: "Michael"
last_name: "Henze"
password: "2dfa7ae9d549ae3e152c1621b111d92029972749"
email: "michael.henze@imtek.uni-freiburg.de"
role: "User"
date_created: "2012-05-22 17:00:41"
-
id: 154
username: "VaniaWidyaya"
first_name: "Vania"
last_name: "Widyaya"
password: "93b8e920dce9eb632b4698a77ca63a1dd6f3d14c"
email: "vania.widyaya@imtek.uni-freiburg.de"
role: "User"
date_created: "2015-05-06 09:47:09"
-
id: 104
username: "RomanErath"
first_name: "Roman"
last_name: "Erath"
password: "452f3991d26d97c62cdf6bbcb31ef2be9b42df1d"
email: "roman.erath@imtek.uni-freiburg.de"
role: "User"
date_created: "2012-05-29 13:42:51"
-
id: 106
username: "WibkeHartleb"
first_name: "Wibke"
last_name: "Hartleb"
password: "aeea4f54de35d5abe4d0fcbb8116df4842bc7304"
email: "hartleb@imtek.de"
role: "Inactive"
date_created: "2012-06-04 16:19:55"
-
id: 107
username: "FranziskaDorner"
first_name: "Franziska"
last_name: "Dorner"
password: "0320f2762d397cb4225f36bcad23a1d6ec9a5dd5"
email: "franziska.dorner@imtek.de"
role: "Inactive"
date_created: "2012-06-04 16:28:24"
-
id: 156
username: "ChinnawutPipatpanukul"
first_name: "Chinnawut"
last_name: "Pipatpanukul"
password: "fc0b55c7a97cb2b16d94844dd3b260435cb69acd"
email: "polymer_chin@hotmail.com"
role: "User"
date_created: "2015-09-07 17:03:11"
-
id: 110
username: "SimonSchuster"
first_name: "Simon"
last_name: "Schuster"
password: "5cd79755dcc701a0a72bbeacd5a45aa932c3d63f"
email: "schuster@imtek.uni-freiburg.de"
role: "Inactive"
date_created: "2012-06-18 15:27:14"
-
id: 111
username: "JonGreen"
first_name: "Jon"
last_name: "Green"
password: "871f35fb0bc758f97dcc9dc484b9002014322c1b"
email: "jon.green@imtek.uni-freiburg.de"
role: "Inactive"
date_created: "2012-06-27 15:30:31"
-
id: 113
username: "PengZou"
first_name: "Peng"
last_name: "Zou"
password: "50d1199385e3b7c39d382017e281e37782d84026"
email: "peng.zou@imtek.uni-freiburg.de"
role: "User"
date_created: "2012-07-30 10:38:40"
-
id: 114
username: "HolgerKlapproth"
first_name: "Holger"
last_name: "Klapproth"
password: "ec2ec7fad8702059c81783d7cb720d288ffb6786"
email: "holger.klapproth@imtek.de"
role: "User"
date_created: "2012-07-31 12:39:58"
-
id: 115
username: "TobiasHeitzler"
first_name: "Tobias"
last_name: "Heitzler"
password: "1d119fcf4eb869e99de044125cbe54960e84b1de"
email: "tobias.heitzler@jupiter.uni-freiburg.de"
role: "Inactive"
date_created: "2012-08-20 10:27:36"
-
id: 116
username: "VitaliyKondrashov"
first_name: "Vitaliy"
last_name: "Kondrashov"
password: "a194f7e6584fd78fc6fd87aea8244d7d28cecdfb"
email: "vitaliy.kondrashov@imtek.de"
role: "Inactive"
date_created: "2012-09-12 12:37:03"
-
id: 117
username: "MaraFlorea"
first_name: "Mara"
last_name: "Florea"
password: "2c7d26e5a650f1452596c5d6859f1de70e9dfda0"
email: "mara.florea@fmf-uni-freiburg.de"
role: "User"
date_created: "2012-09-24 09:57:37"
-
id: 118
username: "NicoleBirsner"
first_name: "Nicole"
last_name: "Birsner"
password: "205e21a7eb6a9475d252cc34ec5652a48cd40a1b"
email: "birsner@imtek.de"
role: "User"
date_created: "2012-09-26 16:11:50"
-
id: 119
username: "KarenLienkamp"
first_name: "Karen"
last_name: "Lienkamp"
password: "810616773a6895260bb6b1f06436bf5919b9ac7e"
email: "lienkamp@imtek.uni-freiburg.de"
role: "Purchaser"
date_created: "2012-10-17 13:38:47"
-
id: 120
username: "ChristophScheibelein"
first_name: "Christoph"
last_name: "Scheibelein"
password: "d52b154b64b76cb7d41c900b6dc04075568f5881"
email: "christoph.scheibelein@imtek.de"
role: "User"
date_created: "2012-10-30 17:00:57"
-
id: 151
username: "CrispinAmiriNaini"
first_name: "Crispin"
last_name: "AmiriNaini"
password: "a5e0427ef5e4b47165950f2233767158c6305d72"
email: "crispin.amiri@imtek.de"
role: "User"
date_created: "2015-01-16 15:17:00"
-
id: 121
username: "DavidBoschert"
first_name: "David"
last_name: "Boschert"
password: "219ca82924d6388be4be803104e721c28c7bd8ed"
email: "david.boschert@frias.uni-freiburg.de"
role: "User"
date_created: "2013-01-22 12:01:21"
-
id: 122
username: "NilsKorf"
first_name: "Nils"
last_name: "Korf"
password: "2785030b6e08e0c1b5ff80080382efbe11efd644"
email: "nils.korf@bcf.uni-freiburg.de"
role: "Inactive"
date_created: "2013-01-29 13:26:11"
-
id: 123
username: "FrankScherag"
first_name: "Frank"
last_name: "Scherag"
password: "acd383d906d92779723d121ac3399aa0d84dde00"
email: "Frank.Scherag@imtek.uni-freiburg.de"
role: "User"
date_created: "2013-03-07 10:49:57"
-
id: 124
username: "AnnaSchuler"
first_name: "Anna"
last_name: "Schuler"
password: "e546a402c7ee629029bad23803b14768207fd61a"
email: "anne-katrin.schuler@imtek.uni-freiburg.d"
role: "User"
date_created: "2013-03-12 13:07:07"
-
id: 157
username: "SebastianAnders"
first_name: "Sebastian"
last_name: "Anders"
password: "3d48159e1c5a08a06aa8149208791f686effaba6"
email: "sebastian.anders@imtek.de"
role: "User"
date_created: "2015-09-24 10:34:21"
-
id: 126
username: "MarcelRothfelder"
first_name: "Marcel"
last_name: "Rothfelder"
password: "cb70c818d0a3ab0d595fabbe7eadcf96d7e003f9"
email: "marcel.rothfelder@fmf.uni-freiburg.de"
role: "User"
date_created: "2013-03-28 09:42:44"
-
id: 127
username: "AnneBuderer"
first_name: "Anne"
last_name: "Buderer"
password: "574f939d78617aef36d6240e3cd5c90eb390aa73"
email: "anne.buderer@imtek.uni-freiburg.de"
role: "User"
date_created: "2013-04-02 10:20:27"
-
id: 153
username: "SureshBanda"
first_name: "Suresh"
last_name: "Banda"
password: "91b33a824a0aaac74bfd465ab0ad253885896b20"
email: "suresh.banda@imtek.de"
role: "User"
date_created: "2015-02-19 14:43:41"
-
id: 152
username: "RolandHoenes"
first_name: "Roland"
last_name: "Hoenes"
password: "ad972ff6f6c8e9c1b9fd44a1b9774877482d0974"
email: "roland.hoenes@imtek.de"
role: "User"
date_created: "2015-02-03 09:42:28"
-
id: 129
username: "AnselmHoppmann"
first_name: "Anselm"
last_name: "Hoppmann"
password: "98fe5b48a33f8618fe08f9ca4b8dfb3256fa7e00"
email: "anselm.hoppmann@gmx.de"
role: "Inactive"
date_created: "2013-06-04 10:27:08"
-
id: 130
username: "ShararehAsiaee"
first_name: "Sharareh"
last_name: "Asiaee"
password: "d01d59fb35a1fbbba731f81f28fb43baf3eeb5e5"
email: "sharareh.sahneh@imtek.de"
role: "User"
date_created: "2013-07-04 11:58:37"
-
id: 132
username: "MarcZinggeler"
first_name: "Marc"
last_name: "Zinggeler"
password: "2aead214ed9af75a0dbd8a9c50341ba2a5945451"
email: "marc.zinggeler@imtek.uni-freiburg.de"
role: "User"
date_created: "2013-07-05 12:23:34"
-
id: 133
username: "XiaoqiangHou"
first_name: "Xiaoqiang"
last_name: "Hou"
password: "f717b7dc2f620d074151d544bec5328481d7faac"
email: "xiaoqiang.hou@imtek.de"
role: "User"
date_created: "2013-07-05 16:44:39"
-
id: 134
username: "SamarKazan"
first_name: "Samar"
last_name: "Kazan"
password: "334ef431feb818a657a5d413c341bce0d28cd407"
email: "samar.kazan@imtek.uni-freiburg.de"
role: "User"
date_created: "2013-07-10 14:30:57"
-
id: 136
username: "SaschaEngel"
first_name: "Sascha"
last_name: "Engel"
password: "94299c5dc1b3e954f0d7500d2bc3770a66bbd325"
email: "sascha.engel@imtek.uni-freiburg.de"
role: "Inactive"
date_created: "2013-08-09 13:47:01"
-
id: 137
username: "MartinKoerner"
first_name: "Martin"
last_name: "Koerner"
password: "115e67def35ce67ede63c93d7d81479d828d705c"
email: "martin.koerner@imtek.de"
role: "User"
date_created: "2013-08-26 15:02:45"
-
id: 138
username: "UrmilShah"
first_name: "Urmil"
last_name: "Shah"
password: "27bf49a7ec0d428a4e65ac5fbe88bd0bd8d55643"
email: "shahurmil86@yahoo.com"
role: "User"
date_created: "2013-10-29 14:28:59"
-
id: 139
username: "DavidSchwaerzle"
first_name: "David"
last_name: "Schwaerzle"
password: "938791e4907d0673c3baf5278b574f11b45c2fbb"
email: "david.schwaerzle@gmx.de"
role: "User"
date_created: "2013-10-29 14:29:02"
-
id: 147
username: "MonikaKurowska"
first_name: "Monika"
last_name: "Kurowska"
password: "a5fcbf21e6919599279dbe9c4adfea9878686321"
email: "monikkurowska@gmail.com"
role: "User"
date_created: "2014-10-02 09:21:18"
-
id: 140
username: "PhilipKotrade"
first_name: "Philip"
last_name: "Kotrade"
password: "8200057b2e20c4e5b801336841e6849de8bf3716"
email: "Philip.Kotrade@imtek.uni-freiburg.de"
role: "User"
date_created: "2013-11-07 11:19:22"
-
id: 141
username: "NiklasSchoenberg"
first_name: "Niklas"
last_name: "Schoenberg"
password: "6a2bf1397988b6da3c2857c2466a2396365880a0"
email: "schoenberg@imtek.de"
role: "User"
date_created: "2013-11-07 15:35:13"
-
id: 142
username: "MatthiasMenzel"
first_name: "Matthias"
last_name: "Menzel"
password: "8b1a8e1c534016c16396dc2308dbb9410221df20"
email: "matthias.menzel@imtek.uni-freiburg.de"
role: "User"
date_created: "2013-12-10 12:40:26"
-
id: 144
username: "MostafaMahmoud"
first_name: "Mostafa"
last_name: "Mahmoud"
password: "1dc1feb8629b3292065654e2a50160177b6e5aab"
email: "mostafasafwat@live.com"
role: "Inactive"
date_created: "2014-02-28 15:49:50"
-
id: 145
username: "HeidiPerez"
first_name: "Heidi"
last_name: "Perez"
password: "b3223810838aee0d0d08bef11ae6ab8139ead98d"
email: "heidi.perez@frias.uni-freiburg.de"
role: "Inactive"
date_created: "2014-04-23 13:32:24"
-
id: 146
username: "EstherRiga"
first_name: "Esther"
last_name: "Riga"
password: "f58b22fe282c539d9504e4f5023f30a5e694dae4"
email: "esther.riga@imtek.uni-freiburg.de"
role: "User"
date_created: "2014-07-08 13:34:35"
-
id: 148
username: "thomasseery"
first_name: "thomas"
last_name: "seery"
password: "808b3bf707af545878b2a14fe8d29649f6d5c227"
email: "seery@mail.ims.uconn.edu"
role: "Inactive"
date_created: "2014-10-16 16:24:24"
-
id: 149
username: "VanessaWeiss"
first_name: "Vanessa"
last_name: "Weiss"
password: "cdab1dc399f2db48b9f0d7edf241ffd5163ccf97"
email: "vanessa.weiss@imtek.de"
role: "User"
date_created: "2014-11-21 13:47:35"
-
id: 150
username: "AndreasMader"
first_name: "Andreas"
last_name: "Mader"
password: "ef18eaebcdc6ec5cc07f1d60f07ff0056ff6fc9d"
email: "andreas.mader@imtek.de"
role: "Inactive"
date_created: "2014-12-02 15:34:58"
-
id: 158
username: "JuliaSaar"
first_name: "Julia"
last_name: "Saar"
password: "c931d353713d309431819e824d6b8d5ffcc91660"
email: "Julia.Saar91@googlemail.com"
role: "User"
date_created: "2015-10-26 09:35:14"
-
id: 159
username: "MariaVoehringer"
first_name: "Maria"
last_name: "Voehringer"
password: "4538f730a9e9ad282753a447376a7bb4537ab24c"
email: "maria.voehringer@gmx.de"
role: "User"
date_created: "2015-11-23 11:00:08"
-
id: 160
username: "JessicaBean"
first_name: "Jessica"
last_name: "Bean"
password: "61095da8cef256650a98ad2941ba71892f15c958"
email: "jessica.bean@fmf.uni-freiburg.de"
role: "User"
date_created: "2015-11-24 15:22:32"
-
id: 161
username: "SimonZunker"
first_name: "Simon"
last_name: "Zunker"
password: "f19814fbf8f2cbdfee3fff746553869c30d3828b"
email: "simon.zunker@imtek.uni-freiburg.de"
role: "User"
date_created: "2016-01-26 16:42:14"
-
id: 162
username: "WeiChen"
first_name: "Wei"
last_name: "Chen"
password: "cb0aed09cd8838e6a4a319d284315ff51069ad75"
email: "wei.chen@imtek.uni-freiburg.de"
role: "User"
date_created: "2016-01-27 09:53:00"
-
id: 163
username: "AndreasWalder"
first_name: "Andreas"
last_name: "Walder"
password: "19dc4b581986466ef062d3e9d4eb2ae862e88a39"
email: "andreas.walder@imtek.uni-freiburg.de"
role: "User"
date_created: "2016-02-12 14:47:11"
-
id: 164
username: "KrisdaSudprasert"
first_name: "Krisda"
last_name: "Sudprasert"
password: "8520af56684c336d8403d0436b59010ea427a602"
email: "srati_wonnuch@hotmail.com"
role: "Inactive"
date_created: "2016-03-02 14:26:40"
-
id: 165
username: "ThananthornKanokwijitsilp"
first_name: "Thananthorn"
last_name: "Kanokwijitsilp"
password: "bf7b831ec66bdba869dbed4bbf203661ab5a2a6e"
email: "kanokwijitsilp@imtek.de"
role: "User"
date_created: "2016-03-09 10:23:26"
-
id: 166
username: "AlexanderStraub"
first_name: "Alexander"
last_name: "Straub"
password: "0cabd9350fe909cedb21776957fcf72f00a3210e"
email: "alexander.straub@jupiter.uni-freiburg.de"
role: "User"
date_created: "2016-05-25 09:48:39"
-
id: 167
username: "TaisukeKojima"
first_name: "Taisuke"
last_name: "Kojima"
password: "da53b8d3a7cbee5aaa7985a6330d8e83206aba5e"
email: "taisuke.kojima@imtek.uni-freiburg.de"
role: "User"
date_created: "2016-06-03 13:09:43"
-
id: 168
username: "StefanMuellers"
first_name: "Stefan"
last_name: "Muellers"
password: "653a9b2a076040f8246a5143901b0c1628176b27"
email: "stefan.muellers@imtek.uni-freiburg.de"
role: "User"
date_created: "2016-06-20 09:00:46"
-
id: 169
username: "JonasKost"
first_name: "Jonas"
last_name: "Kost"
password: "7bfa62293f70e1d6ee017642223cdbf7ae227f2d"
email: "jonas.kost@imtek.uni-freiburg.de"
role: "User"
date_created: "2016-07-11 09:57:57"
-
id: 170
username: "SimonSchoelch"
first_name: "Simon"
last_name: "Schoelch"
password: "fb747c7e015579361c416d4a59fbffe11a21be2f"
email: "simon.schoelch@gmail.com"
role: "User"
date_created: "2016-07-11 14:44:54"
-
id: 171
username: "ZhuolingDeng"
first_name: "Zhuoling"
last_name: "Deng"
password: "9374fdfaf8bc080b30233819f7f59bdb97b7a292"
email: "zhuoling.deng@merkur.uni-freiburg.de"
role: "User"
date_created: "2016-08-03 10:33:55"
-
id: 172
username: "TaoZou"
first_name: "Tao"
last_name: "Zou"
password: "0982e0a944c95b1b8d479e9635a4d05c656dd2e1"
email: "zout1219@msn.cn"
role: "User"
date_created: "2016-08-11 10:49:50"
-
id: 173
username: "StefanBritz"
first_name: "Stefan"
last_name: "Britz"
password: "c0836a94f6b666febedda96600fc8bec2999e086"
email: "stefan.britz@imtek.uni-freiburg.de"
role: "User"
date_created: "2016-11-02 13:18:23"
-
id: 174
username: "AliceEickenscheidt"
first_name: "Alice"
last_name: "Eickenscheidt"
password: "eb1db0df34b947382e2edeab0300f34f660f2fc9"
email: "alice.eickenscheidt@imtek.uni-freiburg.d"
role: "User"
date_created: "2016-11-03 13:18:08"
-
id: 175
username: "SarahFontaine"
first_name: "Sarah"
last_name: "Fontaine"
password: "5ace8b390b394d6438d4f691f06613996f61f919"
email: "sarah.fontaine@uranus.uni-freiburg.de"
role: "User"
date_created: "2016-11-09 13:24:59"
-
id: 176
username: "BidhariPidhatika"
first_name: "Bidhari"
last_name: "Pidhatika"
password: "9a68eb1ffa40cf74e8498d5641d16de456ac37ad"
email: "bpidhatika@gmail.com"
role: "User"
date_created: "2016-12-06 10:18:04"
-
id: 177
username: "LukasMetzler"
first_name: "Lukas"
last_name: "Metzler"
password: "0778b41d0d82cda54ba3f7176c566272cac16f88"
email: "lukas.metzler@imtek.de"
role: "User"
date_created: "2016-12-22 14:19:15"
-
id: 179
username: "KatrinTuecking"
first_name: "Katrin"
last_name: "Tuecking"
password: "9fb28ff3449a1436cd964aef6d5aa695ddcc1636"
email: "katrintuecking@web.de"
role: "User"
date_created: "2017-02-07 15:01:30"
-
id: 180
username: "CarmenEger"
first_name: "Carmen"
last_name: "Eger"
password: "f3bbfcf8a5f5430d020afdaae6e8f2745366bd0e"
email: "carmen.eger@pluto.uni-freiburg.de"
role: "User"
date_created: "2017-02-20 10:32:28"
-
id: 181
username: "DeepaPantulu"
first_name: "Deepa"
last_name: "Pantulu"
password: "641c92fce59bfa5ad4d80c47d0f932f853ced64c"
email: "deepa.pantulu@imtek.uni-freiburg.de"
role: "User"
date_created: "2017-04-06 10:22:54"
...

119
ordr2/scripts/initializedb.py

@ -1,31 +1,55 @@ @@ -1,31 +1,55 @@
''' initializes a new data base and migrates old data '''
import os
import sys
import transaction
import yaml
from pyramid.paster import (
get_appsettings,
setup_logging,
)
from datetime import datetime
from tqdm import tqdm
from urllib.parse import urlparse
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 MyModel
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_yaml_file(*paths):
''' read and parse a yaml file '''
path = os.path.join(*paths)
with open(path, 'r') as filehandle:
items = yaml.load(filehandle)
return items
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]
@ -33,13 +57,88 @@ def main(argv=sys.argv): @@ -33,13 +57,88 @@ def main(argv=sys.argv):
setup_logging(config_uri)
settings = get_appsettings(config_uri, options=options)
# remove an existing sqlite database to issue a restart
database_url = urlparse(settings['sqlalchemy.url'])
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__)
# 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'],
user_name=data['username'],
first_name=data['first_name'],
last_name=data['last_name'],
email=data['email'],
role=Role(data['role'].lower())
)
if user.user_name == 'HolgerFrey':
user.set_password('holgi')
else:
user.set_password(data['password'])
dbsession.add(user)
dbsession.flush()
# 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(
id=data['id'],
vendor=data['vendor'],
catalog_nr=data['catalog_number'],
cas_description=data['CAS_description'],
package_size=data['package_size'],
unit_price=data['price_unit'],
currency=data['currency'],
comment=data['comment'],
date_created=date_created,
date_modified=parse_dt(data['date_modified'], date_created),
category=category
)
dbsession.add(consumable)
dbsession.flush()
model = MyModel(name='one', value=1)
dbsession.add(model)
# 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()]
category = Category[data['category'].upper()]
date_created = parse_dt(data['date_created'])
order = Order(
id=data['id'],
vendor=data['vendor'],
catalog_nr=data['catalog_number'],
cas_description=data['CAS_description'],
package_size=data['package_size'],
unit_price=data['price_unit'],
amount=data['quantity'],
total_price=data['price_total'],
account=data['account'],
currency=data['currency'],
comment=data['comment'],
created_date=date_created,
created_by=data['username'],
approval_date=parse_dt(data['date_modified'], None),
approval_by='',
ordered_date=parse_dt(data['date_ordered'], None),
ordered_by='',
completed_date=parse_dt(data['date_completed'], None),
completed_by='',
category=category,
status=status
)
dbsession.add(order)
dbsession.flush()

55
ordr2/security.py

@ -0,0 +1,55 @@ @@ -0,0 +1,55 @@
''' User Authentication and Authorization '''
from pyramid.authentication import AuthTktAuthenticationPolicy
from pyramid.authorization import ACLAuthorizationPolicy
from pyramid.security import Authenticated, Everyone
from .models import User
class AuthenticationPolicy(AuthTktAuthenticationPolicy):
''' How to authenticate users '''
def authenticated_userid(self, request):
''' returns the id of an authenticated user
heavy lifting done in get_user() attached to request
'''
user = request.user
if user is not None:
return user.id
def effective_principals(self, request):
''' returns a list of principals for the user '''
principals = [Everyone]
user = request.user
if user is not None:
principals.append(Authenticated)
principals.append(user.principal)
principals.extend(user.role_principals)
return principals
def get_user(request):
''' retrieves the user object by the unauthenticated user id '''
user_id = request.unauthenticated_userid
if user_id is not None:
user = request.dbsession.query(User).filter_by(id=user_id).first()
if user and user.is_active:
return user
return None
def includeme(config):
''' 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'],
hashalg='sha512',
)
config.set_authentication_policy(authn_policy)
config.set_authorization_policy(ACLAuthorizationPolicy())
config.add_request_method(get_user, 'user', reify=True)

567
ordr2/static/css/bootstrap-responsive.css vendored

@ -0,0 +1,567 @@ @@ -0,0 +1,567 @@
/*!
* Bootstrap Responsive v2.0.0
*
* Copyright 2012 Twitter, Inc
* Licensed under the Apache License v2.0
* http://www.apache.org/licenses/LICENSE-2.0
*
* Designed and built with all the love in the world @twitter by @mdo and @fat.
*/
.hidden {
display: none;
visibility: hidden;
}
@media (max-width: 480px) {
.nav-collapse {
-webkit-transform: translate3d(0, 0, 0);
}
.page-header h1 small {
display: block;
line-height: 18px;
}
input[class*="span"],
select[class*="span"],
textarea[class*="span"],
.uneditable-input {
display: block;
width: 100%;
height: 28px;
/* Make inputs at least the height of their button counterpart */
/* Makes inputs behave like true block-level elements */
-webkit-box-sizing: border-box;
/* Older Webkit */
-moz-box-sizing: border-box;
/* Older FF */
-ms-box-sizing: border-box;
/* IE8 */
box-sizing: border-box;
/* CSS3 spec*/
}
.input-prepend input[class*="span"], .input-append input[class*="span"] {
width: auto;
}
input[type="checkbox"], input[type="radio"] {
border: 1px solid #ccc;
}
.form-horizontal .control-group > label {
float: none;
width: auto;
padding-top: 0;
text-align: left;
}
.form-horizontal .controls {
margin-left: 0;
}
.form-horizontal .control-list {
padding-top: 0;
}
.form-horizontal .form-actions {
padding-left: 10px;
padding-right: 10px;
}
.modal {
position: absolute;
top: 10px;
left: 10px;
right: 10px;
width: auto;
margin: 0;
}
.modal.fade.in {
top: auto;
}
.modal-header .close {
padding: 10px;
margin: -10px;
}
.carousel-caption {
position: static;
}
}
@media (max-width: 768px) {
.container {
width: auto;
padding: 0 20px;
}
.row-fluid {
width: 100%;
}
.row {
margin-left: 0;
}
.row > [class*="span"], .row-fluid > [class*="span"] {
float: none;
display: block;
width: auto;
margin: 0;
}
}
@media (min-width: 768px) and (max-width: 980px) {
.row {
margin-left: -20px;
*zoom: 1;
}
.row:before, .row:after {
display: table;
content: "";
}
.row:after {
clear: both;
}
[class*="span"] {
float: left;
margin-left: 20px;
}
.span1 {
width: 42px;
}
.span2 {
width: 104px;
}
.span3 {
width: 166px;
}
.span4 {
width: 228px;
}
.span5 {
width: 290px;
}
.span6 {
width: 352px;
}
.span7 {
width: 414px;
}
.span8 {
width: 476px;
}
.span9 {
width: 538px;
}
.span10 {
width: 600px;
}
.span11 {
width: 662px;
}
.span12, .container {
width: 724px;
}
.offset1 {
margin-left: 82px;
}
.offset2 {
margin-left: 144px;
}
.offset3 {
margin-left: 206px;
}
.offset4 {
margin-left: 268px;
}
.offset5 {
margin-left: 330px;
}
.offset6 {
margin-left: 392px;
}
.offset7 {
margin-left: 454px;
}
.offset8 {
margin-left: 516px;
}
.offset9 {
margin-left: 578px;
}
.offset10 {
margin-left: 640px;
}
.offset11 {
margin-left: 702px;
}
.row-fluid {
width: 100%;
*zoom: 1;
}
.row-fluid:before, .row-fluid:after {
display: table;
content: "";
}
.row-fluid:after {
clear: both;
}
.row-fluid > [class*="span"] {
float: left;
margin-left: 2.762430939%;
}
.row-fluid > [class*="span"]:first-child {
margin-left: 0;
}
.row-fluid .span1 {
width: 5.801104972%;
}
.row-fluid .span2 {
width: 14.364640883%;
}
.row-fluid .span3 {
width: 22.928176794%;
}
.row-fluid .span4 {
width: 31.491712705%;
}
.row-fluid .span5 {
width: 40.055248616%;
}
.row-fluid .span6 {
width: 48.618784527%;
}
.row-fluid .span7 {
width: 57.182320438000005%;
}
.row-fluid .span8 {
width: 65.74585634900001%;
}
.row-fluid .span9 {
width: 74.30939226%;
}
.row-fluid .span10 {
width: 82.87292817100001%;
}
.row-fluid .span11 {
width: 91.436464082%;
}
.row-fluid .span12 {
width: 99.999999993%;
}
input.span1, textarea.span1, .uneditable-input.span1 {
width: 32px;
}
input.span2, textarea.span2, .uneditable-input.span2 {
width: 94px;
}
input.span3, textarea.span3, .uneditable-input.span3 {
width: 156px;
}
input.span4, textarea.span4, .uneditable-input.span4 {
width: 218px;
}
input.span5, textarea.span5, .uneditable-input.span5 {
width: 280px;
}
input.span6, textarea.span6, .uneditable-input.span6 {
width: 342px;
}
input.span7, textarea.span7, .uneditable-input.span7 {
width: 404px;
}
input.span8, textarea.span8, .uneditable-input.span8 {
width: 466px;
}
input.span9, textarea.span9, .uneditable-input.span9 {
width: 528px;
}
input.span10, textarea.span10, .uneditable-input.span10 {
width: 590px;
}
input.span11, textarea.span11, .uneditable-input.span11 {
width: 652px;
}
input.span12, textarea.span12, .uneditable-input.span12 {
width: 714px;
}
}
@media (max-width: 980px) {
body {
padding-top: 0;
}
.navbar-fixed-top {
position: static;
margin-bottom: 18px;
}
.navbar-fixed-top .navbar-inner {
padding: 5px;
}
.navbar .container {
width: auto;
padding: 0;
}
.navbar .brand {
padding-left: 10px;
padding-right: 10px;
margin: 0 0 0 -5px;
}
.navbar .nav-collapse {
clear: left;
}
.navbar .nav {
float: none;
margin: 0 0 9px;
}
.navbar .nav > li {
float: none;
}
.navbar .nav > li > a {
margin-bottom: 2px;
}
.navbar .nav > .divider-vertical {
display: none;
}
.navbar .nav > li > a, .navbar .dropdown-menu a {
padding: 6px 15px;
font-weight: bold;
color: #999999;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
border-radius: 3px;
}
.navbar .dropdown-menu li + li a {
margin-bottom: 2px;
}
.navbar .nav > li > a:hover, .navbar .dropdown-menu a:hover {
background-color: #222222;
}
.navbar .dropdown-menu {
position: static;
top: auto;
left: auto;
float: none;
display: block;
max-width: none;
margin: 0 15px;
padding: 0;
background-color: transparent;
border: none;
-webkit-border-radius: 0;
-moz-border-radius: 0;
border-radius: 0;
-webkit-box-shadow: none;
-moz-box-shadow: none;
box-shadow: none;
}
.navbar .dropdown-menu:before, .navbar .dropdown-menu:after {
display: none;
}
.navbar .dropdown-menu .divider {
display: none;
}
.navbar-form, .navbar-search {
float: none;
padding: 9px 15px;
margin: 9px 0;
border-top: 1px solid #222222;
border-bottom: 1px solid #222222;
-webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1);
-moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1);
}
.navbar .nav.pull-right {
float: none;
margin-left: 0;
}
.navbar-static .navbar-inner {
padding-left: 10px;
padding-right: 10px;
}
.btn-navbar {
display: block;
}
.nav-collapse {
overflow: hidden;
height: 0;
}
}
@media (min-width: 980px) {
.nav-collapse.collapse {
height: auto !important;
}
}
@media (min-width: 1200px) {
.row {
margin-left: -30px;
*zoom: 1;
}
.row:before, .row:after {
display: table;
content: "";
}
.row:after {
clear: both;
}
[class*="span"] {
float: left;
margin-left: 30px;
}
.span1 {
width: 70px;
}
.span2 {
width: 170px;
}
.span3 {
width: 270px;
}
.span4 {
width: 370px;
}
.span5 {
width: 470px;
}
.span6 {
width: 570px;
}
.span7 {
width: 670px;
}
.span8 {
width: 770px;
}
.span9 {
width: 870px;
}
.span10 {
width: 970px;
}
.span11 {
width: 1070px;
}
.span12, .container {
width: 1170px;
}
.offset1 {
margin-left: 130px;
}
.offset2 {
margin-left: 230px;
}
.offset3 {
margin-left: 330px;
}
.offset4 {
margin-left: 430px;
}
.offset5 {
margin-left: 530px;
}
.offset6 {
margin-left: 630px;
}
.offset7 {
margin-left: 730px;
}
.offset8 {
margin-left: 830px;
}
.offset9 {
margin-left: 930px;
}
.offset10 {
margin-left: 1030px;
}
.offset11 {
margin-left: 1130px;
}
.row-fluid {
width: 100%;
*zoom: 1;
}
.row-fluid:before, .row-fluid:after {
display: table;
content: "";
}
.row-fluid:after {
clear: both;
}
.row-fluid > [class*="span"] {
float: left;
margin-left: 2.564102564%;
}
.row-fluid > [class*="span"]:first-child {
margin-left: 0;
}
.row-fluid .span1 {
width: 5.982905983%;
}
.row-fluid .span2 {
width: 14.529914530000001%;
}
.row-fluid .span3 {
width: 23.076923077%;
}
.row-fluid .span4 {
width: 31.623931624%;
}
.row-fluid .span5 {
width: 40.170940171000005%;
}
.row-fluid .span6 {
width: 48.717948718%;
}
.row-fluid .span7 {
width: 57.264957265%;
}
.row-fluid .span8 {
width: 65.81196581200001%;
}
.row-fluid .span9 {
width: 74.358974359%;
}
.row-fluid .span10 {
width: 82.905982906%;
}
.row-fluid .span11 {
width: 91.45299145300001%;
}
.row-fluid .span12 {
width: 100%;
}
input.span1, textarea.span1, .uneditable-input.span1 {
width: 60px;
}
input.span2, textarea.span2, .uneditable-input.span2 {
width: 160px;
}
input.span3, textarea.span3, .uneditable-input.span3 {
width: 260px;
}
input.span4, textarea.span4, .uneditable-input.span4 {
width: 360px;
}
input.span5, textarea.span5, .uneditable-input.span5 {
width: 460px;
}
input.span6, textarea.span6, .uneditable-input.span6 {
width: 560px;
}
input.span7, textarea.span7, .uneditable-input.span7 {
width: 660px;
}
input.span8, textarea.span8, .uneditable-input.span8 {
width: 760px;
}
input.span9, textarea.span9, .uneditable-input.span9 {
width: 860px;
}
input.span10, textarea.span10, .uneditable-input.span10 {
width: 960px;
}
input.span11, textarea.span11, .uneditable-input.span11 {
width: 1060px;
}
input.span12, textarea.span12, .uneditable-input.span12 {
width: 1160px;
}
.thumbnails {
margin-left: -30px;
}
.thumbnails > li {
margin-left: 30px;
}
}

3365
ordr2/static/css/bootstrap.css vendored

File diff suppressed because it is too large Load Diff

51
ordr2/static/css/email.css

@ -0,0 +1,51 @@ @@ -0,0 +1,51 @@
* {
margin: 0;
}
body {
background: url(../img/bg.png) repeat-x scroll 0 0 #FCFCFC;
margin: 10px 20px;
}
h1 {
font-family: 'Anton', sans-serif;
font-size: 45px;
margin-bottom: 25px;
}
p {
font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;
font-size: 16px;
line-height: 24px;
margin-bottom: 9px;
}
a {
color: #0088CC;
text-decoration: none;
}
small {
color: #888888;
font-size: 10px;
}
strong {
display: block;
margin: 15px 0 35px;
font-size: 25px;
}
.signature {
margin-top: 20px;
}
.signature .brand {
font-family: 'Anton', sans-serif;
font-size: 20px;
}
.footprint {
border-top: 1px solid #E5E5E5;
margin-top: 20px;
}
.footprint .icon-dbs {
background-image: url(../img/sprite.png);
background-position: 0px -214px;
width: 174px;
display: block;
height: 26px;
opacity: 0.5;
margin: 5px 0 0 120px;;
}

753
ordr2/static/css/style.css

@ -0,0 +1,753 @@ @@ -0,0 +1,753 @@
html, body {
height: 100%;
}
body {
background-color: #FCFCFC;
}
/*Opera Fix*/
body:before {/* thanks to Maleika (Kohoutec)*/
content:"";
height:100%;
float:left;
width:0;
margin-top:-32767px;/* thank you Erik J - negate effect of float*/
}
.rounded-box {
-webkit-border-radius: 5px;
-moz-border-radius: 5px;
border-radius: 5px;
background-color: #EEEEEE;
padding: 10px;
}
.brand {
font-family: 'Anton', sans-serif;
color: #049CDB;
}
.right {
float: right;
}
.left {
float: left;
}
.clear:after {
content: ".";
visibility: hidden;
display: block;
height: 0;
clear: both;
}
.bordered {
border-top: 1px solid #EEEEEE;
border-bottom: 1px solid #EEEEEE;
padding: 7px 0;
margin: 35px 0 15px;
}
/*================================ BT FIXES ================================*/
.sidebar-nav {
padding: 9px 0;
}
.tooltip {
z-index: 2000
}
/*================================= LAYOUT =================================*/
.content {
min-height: 100%;
}
.content:before {
display: table;
content: "";
height: 40px;
zoom: 1;
}
.content.controls:before {
background: none repeat scroll 0 0 #F1F1F1;
border-bottom: 1px solid #D2D2D2;
content: "";
display: table;
height: 86px;
margin-bottom: -47px;
width: 100%;
}
.content .container-fluid, .content .container {
overflow:auto;
padding-bottom: 53px;
}
footer {
position: relative;
margin: -53px 20px 0; /* negative value of footer height */
height: 53px;
clear:both;
text-align: center;
}
footer a {
display: inline-block;
height: 26px;
opacity: 0.5;
-webkit-transition: all 0.5s ease 0s;
-moz-transition: all 0.5s ease 0s;
-o-transition: all 0.5s ease 0s;
transition: all 0.5s ease 0s;
}
footer a:hover {
opacity: 1;
}
footer .copy{
border-top: 1px solid #E5E5E5;
padding-top: 15px;
}
footer .icon-html {
background-image: url(../img/sprite.png);
background-position: -174px -214px;
width: 60px;
margin-left: 114px;
}
footer .icon-dbs {
background-image: url(../img/sprite.png);
background-position: 0px -214px;
height: 26px;
width: 174px;
}
/*================================ BUTTONS ================================*/
.btn-flat {
text-decoration: none;
text-shadow: 0 1px 0 #fff;
font: bold 14px Helvetica, Arial, sans-serif;
color: #444;
line-height: 17px;
display: inline-block;
margin: 0 15px 0 0;
padding: 5px 15px;
background: #F3F3F3;
border: solid 1px #D9D9D9;
border-radius: 4px;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
-webkit-transition: border-color .20s;
-moz-transition: border-color .20s;
-o-transition: border-color .20s;
transition: border-color .20s;
}
a.btn-flat {
cursor: pointer;
height: 18px;
}
button.btn-flat {
height: 30px;
}
.btn-flat:hover {
text-decoration: none;
background: #F4F4F4;
border-color: #C0C0C0;
color: #333;
}
.btn-flat:active {
border-color: #0069D6;
color: #0069D6;
-moz-box-shadow: inset 0 0 10px #D4D4D4;
-webkit-box-shadow: inset 0 0 10px #D4D4D4;
box-shadow: inset 0 0 10px #D4D4D4;
}
.btn-flat i {
background-image: url(../img/sprite.png);
display: inline-block;
height: 18px;
width: 18px;
}
.btn-flat i.pencil {
background-position: -72px 0px;
}
.btn-flat:hover i.pencil {
background-position: -72px -18px;
}
.btn-flat i.trash {
background-position: -90px 0px;
}
.btn-flat:hover i.trash {
background-position: -90px -18px;
}
.btn-flat i.add {
background-position: -108px 0px;
}
.btn-flat:hover i.add {
background-position: -108px -18px;
}
.btn-flat i.abacus {
background-position: -126px 0px;
}
.btn-flat:hover i.abacus {
background-position: -126px -18px;
}
.btn-flat i.eye {
background-position: -144px 0px;
}
.btn-flat:hover i.eye {
background-position: -144px -18px;
}
.btn-flat i.group {
background-position: -54px 0;
}
.btn-flat:hover i.group {
background-position: -54px -18px;
}
.btn-flat i.reset {
background-position: -162px 0;
}
.btn-flat:hover i.reset {
background-position: -162px -18px;
}
.btn-flat i.clip {
background-position: -180px 0;
}
.btn-flat:hover i.clip {
background-position: -180px -18px;
}
.btn-flat i.download {
background-position: -198px 0;
}
.btn-flat:hover i.download {
background-position: -198px -18px;
}
.page-controls .btn-group, .page-controls .actions, .page-controls .btn-flat {
float: left;
display: block;
}
.page-controls .btn-group {
margin-right: 15px;
}
.btn-group + .btn-group {
margin-left: 0;
}
.btn-group .btn-flat {
position: relative;
float: left;
margin-left: -1px;
margin-right: 0px;
-webkit-border-radius: 0;
-moz-border-radius: 0;
border-radius: 0;
}
.btn-group .btn-flat:first-child {
margin-left: 0;
-webkit-border-top-left-radius: 4px;
-moz-border-radius-topleft: 4px;
border-top-left-radius: 4px;
-webkit-border-bottom-left-radius: 4px;
-moz-border-radius-bottomleft: 4px;
border-bottom-left-radius: 4px;
}
.btn-group .btn-flat:last-child {
-webkit-border-top-right-radius: 4px;
-moz-border-radius-topright: 4px;
border-top-right-radius: 4px;
-webkit-border-bottom-right-radius: 4px;
-moz-border-radius-bottomright: 4px;
border-bottom-right-radius: 4px;
}
.search [type="search"] {
padding: 5px 4px;
}
.search .add-on {
cursor: pointer;
padding: 5px;
}
.search [type="submit"] {
background-image: url(../img/sprite.png);
background-position: 0px -68px;
background-color: #F5F5F5;
border: none;
height: 18px;
width: 18px;
display: inline-block;
overflow: hidden;
text-indent: -9999px;
}
.search .autocomplete {
background-image: url(../img/sprite.png);
background-position: -18px -68px;
background-color: #F5F5F5;
height: 18px;
width: 18px;
display: inline-block;
overflow: hidden;
text-indent: -9999px;
}
.search .add-on:active {
border-color: #0069D6;
color: #0069D6;
-moz-box-shadow: inset 0 0 3px #D4D4D4;
-webkit-box-shadow: inset 0 0 3px #D4D4D4;
box-shadow: inset 0 0 3px #D4D4D4;
}
.typeahead.dropdown-menu {
overflow: hidden;
}
.btn-group.marking-needed, .btn-flat.marking-needed {
display: none;
}
/*================================ NAVIGATION ================================*/
.navbar .brand, .navbar .brand:hover {
color: #049CDB;
}
#user-options a {
color: #555555;
text-shadow: none;
}
#user-options .user-name {
color: #0088CC;
}
#user-options:hover .user-name {
text-decoration: underline;
}
.page-controls {
display: block;
height: 30px;
margin-bottom: 25px;
padding: 8px 0;
}
.page-controls:after {
content: ".";
visibility: hidden;
display: block;
height: 0;
clear: both;
}
.page-controls h1 {
line-height: 30px;
}
.page-controls form {
margin-bottom: 0;
}
.page-controls .input-append.search {
float: right;
}
/*================================ TABLES ================================*/
thead {
background-color: #E0F3FF;
border-bottom: 2px solid #0064CD;
}
th {
border-top: none;
}
th.sortable {
cursor: pointer;
background-color: #E0F3FF;
}
th.sortable.headerSortDown, th.sortable.headerSortUp {
background-color: #C7E9FF;
}
th, th a {
color: #0064CD;
}
th a:hover {
text-decoration: none;
color: #0064CD;
}
tbody tr:hover {
background-color: #FAFAFA;
}
td.center, th.center {
text-align: center;
vertical-align: middle;
}
table .header:hover:after {
border-width: 4px 4px 0;
}
table .action {
background-image: url(../img/sprite.png);
margin-bottom: -5px;
margin-right: 2px;
height: 16px;
width: 16px;
display: inline-block;
overflow: hidden;
text-indent: -9999px;
background-repeat: no-repeat;
-webkit-transition: background-image 0.20s linear;
-moz-transition: background-image 0.20s linear;
-o-transition: background-image 0.20s linear;
transition: background-image 0.20s linear;
zoom: 1;
filter: alpha(opacity=40);
opacity: 0.4;
}
table .action:hover {
filter: alpha(opacity=100);
opacity: 1;
}
table .action.delete {
background-position: 0px -36px;
}
table .action.delete:hover {
background-position: 0px -52px;
}
table .action.edit {
background-position: -16px -36px;
}
table .action.edit:hover {
background-position: -16px -52px;
}
table .action.eye {
background-position: -145px 0px;
}
table .action.eye:hover {
background-position: -145px -18px;
}
/*================================ MODALS ================================*/
.modal form {
margin: 0;
}
.modal-body .option {
font-size: 15px;
}
.modal-body .option a {
font-weight: bold;
}
.modal-body .checklist {
margin: 0 20px;
}
.modal-body .checklist:after {
content: ".";
visibility: hidden;
display: block;
height: 0;
clear: both;
}
.modal-body .checklist .checkbox {
margin-bottom: 10px;
}
.modal-body .checklist .left {
float: left;
width: 50%;
}
.modal-body .checklist .right {
float: right;
width: 50%;
}
.modal-body .checklist .checkbox input {
margin-left: -20px;
}
.modal-body .help-block {
margin-bottom: 15px;
}
/*============================= WELCOME PAGE =============================*/
.welcome .content {
background: url(../img/bg.png) repeat-x scroll 0 0 #FCFCFC;
}
#welcome {
margin: 90px 0 80px;
text-align: center;
}
#welcome h1 {
font-family: 'Anton',sans-serif;
font-size: 120px;
line-height: 1;
margin-bottom: 5px;
}
#welcome .brand {
text-transform: none;
}
#welcome .quote {
color: #878787;
font-size: 16px;
}
/*============================= ACCOUNT =============================*/
#register-successful, #access-denied {
margin: 90px 0 50px;
text-align: center;
}
#register-successful h1, #access-denied h1 {
font-family: 'Anton',sans-serif;
font-size: 80px;
line-height: 1;
margin-bottom: 5px;
}
#access-denied {
margin-top: 150px;
}
#access-denied h1 {
color: #B94A48;
}
hgroup .info {
color: #878787;
font-size: 16px;
line-height: 24px;
}
.account.login h1 {
border-bottom: 1px solid #EEEEEE;
font-family: 'Anton',sans-serif;
font-size: 50px;
line-height: 50px;
margin: 80px 0 20px;
padding-bottom: 5px;
}
/*============================== ADMIN AREA ==============================*/
.admin-options a:hover [class*="span"] {
background-color: #EEEEEE;
-webkit-transition: background-color 0.50s linear;
-moz-transition: background-color 0.50s linear;
-o-transition: background-color 0.50s linear;
transition: background-color 0.50s linear;
}
.admin-options .option {
background-image: url(../img/sprite.png);
background-position: 0px -86px;
margin: 0 auto;
width: 128px;
height: 128px;
}
.admin-options .option.shop {
background-position: -128px -86px;
}
.admin-options h2 {
margin-top: 5px;
text-align: center;
}
.admin-options [class*="span"] {
padding: 20px 0;
background-color: #F7F7F7;
}
/*============================== FAQ ==============================*/
.faq h1 {
font-family: 'Anton',sans-serif;
font-size: 60px;
line-height: 1;
margin: 30px 0;
text-align: center;
}
.faq h2 {
color: #FCFCFC;
font-size: 28px;
line-height: 1;
background-color: #049CDB;
padding: 10px;
margin-bottom: 5px;
-webkit-border-radius: 5px;
-moz-border-radius: 5px;
border-radius: 5px;
}
.faq section {
margin-bottom: 35px;
}
/*============================= ACTIONS =============================*/
.action-header {
margin-bottom: 15px;
}
/*============================== FORMS ==============================*/
.control-group.information {
margin: 0;
}
.control-group.information p {
padding: 5px 0;
color: #AAAAAA;
}
/*=========================== ORDER SPLASH ===========================*/
.accordion {
margin-top: 5px;
}
.accordion li {
background-color: #EAEAEA;
cursor: pointer;
font-size: 16px;
margin: 5px 0;
padding: 12px 10px;
-webkit-border-radius: 5px;
-moz-border-radius: 5px;
border-radius: 5px;
-webkit-transition: background-color 0.1s ease 0s;
-moz-transition: background-color 0.1s ease 0s;
-o-transition: background-color 0.1s ease 0s;
transition: background-color 0.1s ease 0s;
}
.accordion li:hover {
background-color: #DDDDDD;
}
.accordion li span {
font-size: 13px;
color: #A0A0A0;
}
.accordion-heading {
background-color: #333333;
padding: 0;
-webkit-border-radius: 5px;
-moz-border-radius: 5px;
border-radius: 5px;
-webkit-transition: background-color 0.1s ease 0s;
-moz-transition: background-color 0.1s ease 0s;
-o-transition: background-color 0.1s ease 0s;
transition: background-color 0.1s ease 0s;
}
.accordion-heading a {
color: #EEEEEE;
display: block;
font-size: 18px;
font-weight: bold;
line-height: 27px;
padding: 8px 15px;
text-decoration: none;
outline: none;
-webkit-transition: color 0.1s ease 0s;
-moz-transition: color 0.1s ease 0s;
-o-transition: color 0.1s ease 0s;
transition: color 0.1s ease 0s;
}
.accordion-heading a:hover {
color: #049CDB;
}
/*========================= RESPONSIVE =========================*/
@media (max-width: 1200px) {
.page-controls h1 { font-size: 25px; }
}
/*================ STYLES FOR php2python BRANCH ================*/
input[value="password:mapping"] + div { margin-bottom:10px; }
input[value="new_password:mapping"] + div { margin-bottom:10px; }
.form-horizontal.user-settings fieldset > .controls { margin-left:0; }
.user-settings .panel-heading,
.edit-order .panel-heading {
font-size:150%;
padding-top: 20px;
padding-bottom: 20px;
margin-bottom: 20px;
border-bottom: 1px solid #aaa;}
div.alert a { color:inherit; text-decoration:underline; }
td.column-pkg, td.column-price, td.column-total, td.column-amount {
text-align:right;}
input.number { text-align:right; }
.moneyinput .amount { width:167px; text-align:right;}
.moneyinput .currency { width:30px; text-align:center;}
.controls .form-control-static { padding-top:5px; }
.form-like-display.form-horizontal .control-group { margin-bottom:0px; }

BIN
ordr2/static/img/bg.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

BIN
ordr2/static/img/favicon.ico

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
ordr2/static/img/sprite.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

91
ordr2/static/js/bootstrap-alert.js vendored

@ -0,0 +1,91 @@ @@ -0,0 +1,91 @@
/* ==========================================================
* bootstrap-alert.js v2.0.0
* http://twitter.github.com/bootstrap/javascript.html#alerts
* ==========================================================
* Copyright 2012 Twitter, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* ========================================================== */
!function( $ ){
"use strict"
/* ALERT CLASS DEFINITION
* ====================== */
var dismiss = '[data-dismiss="alert"]'
, Alert = function ( el ) {
$(el).on('click', dismiss, this.close)
}
Alert.prototype = {
constructor: Alert
, close: function ( e ) {
var $this = $(this)
, selector = $this.attr('data-target')
, $parent
if (!selector) {
selector = $this.attr('href')
selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7
}
$parent = $(selector)
$parent.trigger('close')
e && e.preventDefault()
$parent.length || ($parent = $this.hasClass('alert') ? $this : $this.parent())
$parent.removeClass('in')
function removeElement() {
$parent.remove()
$parent.trigger('closed')
}
$.support.transition && $parent.hasClass('fade') ?
$parent.on($.support.transition.end, removeElement) :
removeElement()
}
}
/* ALERT PLUGIN DEFINITION
* ======================= */
$.fn.alert = function ( option ) {
return this.each(function () {
var $this = $(this)
, data = $this.data('alert')
if (!data) $this.data('alert', (data = new Alert(this)))
if (typeof option == 'string') data[option].call($this)
})
}
$.fn.alert.Constructor = Alert
/* ALERT DATA-API
* ============== */
$(function () {
$('body').on('click.alert.data-api', dismiss, Alert.prototype.close)
})
}( window.jQuery )

136
ordr2/static/js/bootstrap-collapse.js vendored

@ -0,0 +1,136 @@ @@ -0,0 +1,136 @@
/* =============================================================
* bootstrap-collapse.js v2.0.0
* http://twitter.github.com/bootstrap/javascript.html#collapse
* =============================================================
* Copyright 2012 Twitter, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* ============================================================ */
!function( $ ){
"use strict"
var Collapse = function ( element, options ) {
this.$element = $(element)
this.options = $.extend({}, $.fn.collapse.defaults, options)
if (this.options["parent"]) {
this.$parent = $(this.options["parent"])
}
this.options.toggle && this.toggle()
}
Collapse.prototype = {
constructor: Collapse
, dimension: function () {
var hasWidth = this.$element.hasClass('width')
return hasWidth ? 'width' : 'height'
}
, show: function () {
var dimension = this.dimension()
, scroll = $.camelCase(['scroll', dimension].join('-'))
, actives = this.$parent && this.$parent.find('.in')
, hasData
if (actives && actives.length) {
hasData = actives.data('collapse')
actives.collapse('hide')
hasData || actives.data('collapse', null)
}
this.$element[dimension](0)
this.transition('addClass', 'show', 'shown')
this.$element[dimension](this.$element[0][scroll])
}
, hide: function () {
var dimension = this.dimension()
this.reset(this.$element[dimension]())
this.transition('removeClass', 'hide', 'hidden')
this.$element[dimension](0)
}
, reset: function ( size ) {
var dimension = this.dimension()
this.$element
.removeClass('collapse')
[dimension](size || 'auto')
[0].offsetWidth
this.$element.addClass('collapse')
}
, transition: function ( method, startEvent, completeEvent ) {
var that = this
, complete = function () {
if (startEvent == 'show') that.reset()
that.$element.trigger(completeEvent)
}
this.$element
.trigger(startEvent)
[method]('in')
$.support.transition && this.$element.hasClass('collapse') ?
this.$element.one($.support.transition.end, complete) :
complete()
}
, toggle: function () {
this[this.$element.hasClass('in') ? 'hide' : 'show']()
}
}
/* COLLAPSIBLE PLUGIN DEFINITION
* ============================== */
$.fn.collapse = function ( option ) {
return this.each(function () {
var $this = $(this)
, data = $this.data('collapse')
, options = typeof option == 'object' && option
if (!data) $this.data('collapse', (data = new Collapse(this, options)))
if (typeof option == 'string') data[option]()
})
}
$.fn.collapse.defaults = {
toggle: true
}
$.fn.collapse.Constructor = Collapse
/* COLLAPSIBLE DATA-API
* ==================== */
$(function () {
$('body').on('click.collapse.data-api', '[data-toggle=collapse]', function ( e ) {
var $this = $(this), href
, target = $this.attr('data-target')
|| e.preventDefault()
|| (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') //strip for ie7
, option = $(target).data('collapse') ? 'toggle' : $this.data()
$(target).collapse(option)
})
})
}( window.jQuery )

92
ordr2/static/js/bootstrap-dropdown.js vendored

@ -0,0 +1,92 @@ @@ -0,0 +1,92 @@
/* ============================================================
* bootstrap-dropdown.js v2.0.0
* http://twitter.github.com/bootstrap/javascript.html#dropdowns
* ============================================================
* Copyright 2012 Twitter, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* ============================================================ */
!function( $ ){
"use strict"
/* DROPDOWN CLASS DEFINITION
* ========================= */
var toggle = '[data-toggle="dropdown"]'
, Dropdown = function ( element ) {
var $el = $(element).on('click.dropdown.data-api', this.toggle)
$('html').on('click.dropdown.data-api', function () {
$el.parent().removeClass('open')
})
}
Dropdown.prototype = {
constructor: Dropdown
, toggle: function ( e ) {
var $this = $(this)
, selector = $this.attr('data-target')
, $parent
, isActive
if (!selector) {
selector = $this.attr('href')
selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7
}
$parent = $(selector)
$parent.length || ($parent = $this.parent())
isActive = $parent.hasClass('open')
clearMenus()
!isActive && $parent.toggleClass('open')
return false
}
}
function clearMenus() {
$(toggle).parent().removeClass('open')
}
/* DROPDOWN PLUGIN DEFINITION
* ========================== */
$.fn.dropdown = function ( option ) {
return this.each(function () {
var $this = $(this)
, data = $this.data('dropdown')
if (!data) $this.data('dropdown', (data = new Dropdown(this)))
if (typeof option == 'string') data[option].call($this)
})
}
$.fn.dropdown.Constructor = Dropdown
/* APPLY TO STANDARD DROPDOWN ELEMENTS
* =================================== */
$(function () {
$('html').on('click.dropdown.data-api', clearMenus)
$('body').on('click.dropdown.data-api', toggle, Dropdown.prototype.toggle)
})
}( window.jQuery )

209
ordr2/static/js/bootstrap-modal.js vendored

@ -0,0 +1,209 @@ @@ -0,0 +1,209 @@
/* =========================================================
* bootstrap-modal.js v2.0.0
* http://twitter.github.com/bootstrap/javascript.html#modals
* =========================================================
* Copyright 2012 Twitter, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* ========================================================= */
!function( $ ){
"use strict"
/* MODAL CLASS DEFINITION
* ====================== */
var Modal = function ( content, options ) {
this.options = $.extend({}, $.fn.modal.defaults, options)
this.$element = $(content)
.delegate('[data-dismiss="modal"]', 'click.dismiss.modal', $.proxy(this.hide, this))
}
Modal.prototype = {
constructor: Modal
, toggle: function () {
return this[!this.isShown ? 'show' : 'hide']()
}
, show: function () {
var that = this
if (this.isShown) return
$('body').addClass('modal-open')
this.isShown = true
this.$element.trigger('show')
escape.call(this)
backdrop.call(this, function () {
var transition = $.support.transition && that.$element.hasClass('fade')
!that.$element.parent().length && that.$element.appendTo(document.body) //don't move modals dom position
that.$element
.show()
if (transition) {
that.$element[0].offsetWidth // force reflow
}
that.$element.addClass('in')
transition ?
that.$element.one($.support.transition.end, function () { that.$element.trigger('shown') }) :
that.$element.trigger('shown')
})
}
, hide: function ( e ) {
e && e.preventDefault()
if (!this.isShown) return
var that = this
this.isShown = false
$('body').removeClass('modal-open')
escape.call(this)
this.$element
.trigger('hide')
.removeClass('in')
$.support.transition && this.$element.hasClass('fade') ?
hideWithTransition.call(this) :
hideModal.call(this)
}
}
/* MODAL PRIVATE METHODS
* ===================== */
function hideWithTransition() {
var that = this
, timeout = setTimeout(function () {
that.$element.off($.support.transition.end)
hideModal.call(that)
}, 500)
this.$element.one($.support.transition.end, function () {
clearTimeout(timeout)
hideModal.call(that)
})
}
function hideModal( that ) {
this.$element
.hide()
.trigger('hidden')
backdrop.call(this)
}
function backdrop( callback ) {
var that = this
, animate = this.$element.hasClass('fade') ? 'fade' : ''
if (this.isShown && this.options.backdrop) {
var doAnimate = $.support.transition && animate
this.$backdrop = $('<div class="modal-backdrop ' + animate + '" />')
.appendTo(document.body)
if (this.options.backdrop != 'static') {
this.$backdrop.click($.proxy(this.hide, this))
}
if (doAnimate) this.$backdrop[0].offsetWidth // force reflow
this.$backdrop.addClass('in')
doAnimate ?
this.$backdrop.one($.support.transition.end, callback) :
callback()
} else if (!this.isShown && this.$backdrop) {
this.$backdrop.removeClass('in')
$.support.transition && this.$element.hasClass('fade')?
this.$backdrop.one($.support.transition.end, $.proxy(removeBackdrop, this)) :
removeBackdrop.call(this)
} else if (callback) {
callback()
}
}
function removeBackdrop() {
this.$backdrop.remove()
this.$backdrop = null
}
function escape() {
var that = this
if (this.isShown && this.options.keyboard) {
$(document).on('keyup.dismiss.modal', function ( e ) {
e.which == 27 && that.hide()
})
} else if (!this.isShown) {
$(document).off('keyup.dismiss.modal')
}
}
/* MODAL PLUGIN DEFINITION
* ======================= */
$.fn.modal = function ( option ) {
return this.each(function () {
var $this = $(this)
, data = $this.data('modal')
, options = typeof option == 'object' && option
if (!data) $this.data('modal', (data = new Modal(this, options)))
if (typeof option == 'string') data[option]()
else data.show()
})
}
$.fn.modal.defaults = {
backdrop: true
, keyboard: true
}
$.fn.modal.Constructor = Modal
/* MODAL DATA-API
* ============== */
$(function () {
$('body').on('click.modal.data-api', '[data-toggle="modal"]', function ( e ) {
var $this = $(this), href
, $target = $($this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) //strip for ie7
, option = $target.data('modal') ? 'toggle' : $.extend({}, $target.data(), $this.data())
e.preventDefault()
$target.modal(option)
})
})
}( window.jQuery )

270
ordr2/static/js/bootstrap-tooltip.js vendored

@ -0,0 +1,270 @@ @@ -0,0 +1,270 @@
/* ===========================================================
* bootstrap-tooltip.js v2.0.0
* http://twitter.github.com/bootstrap/javascript.html#tooltips
* Inspired by the original jQuery.tipsy by Jason Frame
* ===========================================================
* Copyright 2012 Twitter, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* ========================================================== */
!function( $ ) {
"use strict"
/* TOOLTIP PUBLIC CLASS DEFINITION
* =============================== */
var Tooltip = function ( element, options ) {
this.init('tooltip', element, options)
}
Tooltip.prototype = {
constructor: Tooltip
, init: function ( type, element, options ) {
var eventIn
, eventOut
this.type = type
this.$element = $(element)
this.options = this.getOptions(options)
this.enabled = true
if (this.options.trigger != 'manual') {
eventIn = this.options.trigger == 'hover' ? 'mouseenter' : 'focus'
eventOut = this.options.trigger == 'hover' ? 'mouseleave' : 'blur'
this.$element.on(eventIn, this.options.selector, $.proxy(this.enter, this))
this.$element.on(eventOut, this.options.selector, $.proxy(this.leave, this))
}
this.options.selector ?
(this._options = $.extend({}, this.options, { trigger: 'manual', selector: '' })) :
this.fixTitle()
}
, getOptions: function ( options ) {
options = $.extend({}, $.fn[this.type].defaults, options, this.$element.data())
if (options.delay && typeof options.delay == 'number') {
options.delay = {
show: options.delay
, hide: options.delay
}
}
return options
}
, enter: function ( e ) {
var self = $(e.currentTarget)[this.type](this._options).data(this.type)
if (!self.options.delay || !self.options.delay.show) {
self.show()
} else {
self.hoverState = 'in'
setTimeout(function() {
if (self.hoverState == 'in') {
self.show()
}
}, self.options.delay.show)
}
}
, leave: function ( e ) {
var self = $(e.currentTarget)[this.type](this._options).data(this.type)
if (!self.options.delay || !self.options.delay.hide) {
self.hide()
} else {
self.hoverState = 'out'
setTimeout(function() {
if (self.hoverState == 'out') {
self.hide()
}
}, self.options.delay.hide)
}
}
, show: function () {
var $tip
, inside
, pos
, actualWidth
, actualHeight
, placement
, tp
if (this.hasContent() && this.enabled) {
$tip = this.tip()
this.setContent()
if (this.options.animation) {
$tip.addClass('fade')
}
placement = typeof this.options.placement == 'function' ?
this.options.placement.call(this, $tip[0], this.$element[0]) :
this.options.placement
inside = /in/.test(placement)
$tip
.remove()
.css({ top: 0, left: 0, display: 'block' })
.appendTo(inside ? this.$element : document.body)
pos = this.getPosition(inside)
actualWidth = $tip[0].offsetWidth
actualHeight = $tip[0].offsetHeight
switch (inside ? placement.split(' ')[1] : placement) {
case 'bottom':
tp = {top: pos.top + pos.height, left: pos.left + pos.width / 2 - actualWidth / 2}
break
case 'top':
tp = {top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2}
break
case 'left':
tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth}
break
case 'right':
tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width}
break
}
$tip
.css(tp)
.addClass(placement)
.addClass('in')
}
}
, setContent: function () {
var $tip = this.tip()
$tip.find('.tooltip-inner').html(this.getTitle())
$tip.removeClass('fade in top bottom left right')
}
, hide: function () {
var that = this
, $tip = this.tip()
$tip.removeClass('in')
function removeWithAnimation() {
var timeout = setTimeout(function () {
$tip.off($.support.transition.end).remove()
}, 500)
$tip.one($.support.transition.end, function () {
clearTimeout(timeout)
$tip.remove()
})
}
$.support.transition && this.$tip.hasClass('fade') ?
removeWithAnimation() :
$tip.remove()
}
, fixTitle: function () {
var $e = this.$element
if ($e.attr('title') || typeof($e.attr('data-original-title')) != 'string') {
$e.attr('data-original-title', $e.attr('title') || '').removeAttr('title')
}
}
, hasContent: function () {
return this.getTitle()
}
, getPosition: function (inside) {
return $.extend({}, (inside ? {top: 0, left: 0} : this.$element.offset()), {
width: this.$element[0].offsetWidth
, height: this.$element[0].offsetHeight
})
}
, getTitle: function () {
var title
, $e = this.$element
, o = this.options
title = $e.attr('data-original-title')
|| (typeof o.title == 'function' ? o.title.call($e[0]) : o.title)
title = title.toString().replace(/(^\s*|\s*$)/, "")
return title
}
, tip: function () {
return this.$tip = this.$tip || $(this.options.template)
}
, validate: function () {
if (!this.$element[0].parentNode) {
this.hide()
this.$element = null
this.options = null
}
}
, enable: function () {
this.enabled = true
}
, disable: function () {
this.enabled = false
}
, toggleEnabled: function () {
this.enabled = !this.enabled
}
, toggle: function () {
this[this.tip().hasClass('in') ? 'hide' : 'show']()
}
}
/* TOOLTIP PLUGIN DEFINITION
* ========================= */
$.fn.tooltip = function ( option ) {
return this.each(function () {
var $this = $(this)
, data = $this.data('tooltip')
, options = typeof option == 'object' && option
if (!data) $this.data('tooltip', (data = new Tooltip(this, options)))
if (typeof option == 'string') data[option]()
})
}
$.fn.tooltip.Constructor = Tooltip
$.fn.tooltip.defaults = {
animation: true
, delay: 0
, selector: false
, placement: 'top'
, trigger: 'hover'
, title: ''
, template: '<div class="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>'
}
}( window.jQuery )

51
ordr2/static/js/bootstrap-transition.js vendored

@ -0,0 +1,51 @@ @@ -0,0 +1,51 @@
/* ===================================================
* bootstrap-transition.js v2.0.0
* http://twitter.github.com/bootstrap/javascript.html#transitions
* ===================================================
* Copyright 2012 Twitter, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* ========================================================== */
!function( $ ) {
$(function () {
"use strict"
/* CSS TRANSITION SUPPORT (https://gist.github.com/373874)
* ======================================================= */
$.support.transition = (function () {
var thisBody = document.body || document.documentElement
, thisStyle = thisBody.style
, support = thisStyle.transition !== undefined || thisStyle.WebkitTransition !== undefined || thisStyle.MozTransition !== undefined || thisStyle.MsTransition !== undefined || thisStyle.OTransition !== undefined
return support && {
end: (function () {
var transitionEnd = "TransitionEnd"
if ( $.browser.webkit ) {
transitionEnd = "webkitTransitionEnd"
} else if ( $.browser.mozilla ) {
transitionEnd = "transitionend"
} else if ( $.browser.opera ) {
transitionEnd = "oTransitionEnd"
}
return transitionEnd
}())
}
})()
})
}( window.jQuery )

335
ordr2/static/js/bootstrap-typeahead.js vendored

@ -0,0 +1,335 @@ @@ -0,0 +1,335 @@
/* =============================================================
* bootstrap-typeahead.js v2.3.2
* http://getbootstrap.com/2.3.2/javascript.html#typeahead
* =============================================================
* Copyright 2013 Twitter, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* ============================================================ */
!function($){
"use strict"; // jshint ;_;
/* TYPEAHEAD PUBLIC CLASS DEFINITION
* ================================= */
var Typeahead = function (element, options) {
this.$element = $(element)
this.options = $.extend({}, $.fn.typeahead.defaults, options)
this.matcher = this.options.matcher || this.matcher
this.sorter = this.options.sorter || this.sorter
this.highlighter = this.options.highlighter || this.highlighter
this.updater = this.options.updater || this.updater
this.source = this.options.source
this.$menu = $(this.options.menu)
this.shown = false
this.listen()
}
Typeahead.prototype = {
constructor: Typeahead
, select: function () {
var val = this.$menu.find('.active').attr('data-value')
this.$element
.val(this.updater(val))
.change()
return this.hide()
}
, updater: function (item) {
return item
}
, show: function () {
var pos = $.extend({}, this.$element.position(), {
height: this.$element[0].offsetHeight
})
this.$menu
.insertAfter(this.$element)
.css({
top: pos.top + pos.height
, left: pos.left
})
.show()
this.shown = true
return this
}
, hide: function () {
this.$menu.hide()
this.shown = false
return this
}
, lookup: function (event) {
var items
this.query = this.$element.val()
if (!this.query || this.query.length < this.options.minLength) {
return this.shown ? this.hide() : this
}
items = $.isFunction(this.source) ? this.source(this.query, $.proxy(this.process, this)) : this.source
return items ? this.process(items) : this
}
, process: function (items) {
var that = this
items = $.grep(items, function (item) {
return that.matcher(item)
})
items = this.sorter(items)
if (!items.length) {
return this.shown ? this.hide() : this
}
return this.render(items.slice(0, this.options.items)).show()
}
, matcher: function (item) {
return ~item.toLowerCase().indexOf(this.query.toLowerCase())
}
, sorter: function (items) {
var beginswith = []
, caseSensitive = []
, caseInsensitive = []
, item
while (item = items.shift()) {
if (!item.toLowerCase().indexOf(this.query.toLowerCase())) beginswith.push(item)
else if (~item.indexOf(this.query)) caseSensitive.push(item)
else caseInsensitive.push(item)
}
return beginswith.concat(caseSensitive, caseInsensitive)
}
, highlighter: function (item) {
var query = this.query.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, '\\$&')
return item.replace(new RegExp('(' + query + ')', 'ig'), function ($1, match) {
return '<strong>' + match + '</strong>'
})
}
, render: function (items) {
var that = this
items = $(items).map(function (i, item) {
i = $(that.options.item).attr('data-value', item)
i.find('a').html(that.highlighter(item))
return i[0]
})
items.first().addClass('active')
this.$menu.html(items)
return this
}
, next: function (event) {
var active = this.$menu.find('.active').removeClass('active')
, next = active.next()
if (!next.length) {
next = $(this.$menu.find('li')[0])
}
next.addClass('active')
}
, prev: function (event) {
var active = this.$menu.find('.active').removeClass('active')
, prev = active.prev()
if (!prev.length) {
prev = this.$menu.find('li').last()
}
prev.addClass('active')
}
, listen: function () {
this.$element
.on('focus', $.proxy(this.focus, this))
.on('blur', $.proxy(this.blur, this))
.on('keypress', $.proxy(this.keypress, this))
.on('keyup', $.proxy(this.keyup, this))
if (this.eventSupported('keydown')) {
this.$element.on('keydown', $.proxy(this.keydown, this))
}
this.$menu
.on('click', $.proxy(this.click, this))
.on('mouseenter', 'li', $.proxy(this.mouseenter, this))
.on('mouseleave', 'li', $.proxy(this.mouseleave, this))
}
, eventSupported: function(eventName) {
var isSupported = eventName in this.$element
if (!isSupported) {
this.$element.setAttribute(eventName, 'return;')
isSupported = typeof this.$element[eventName] === 'function'
}
return isSupported
}
, move: function (e) {
if (!this.shown) return
switch(e.keyCode) {
case 9: // tab
case 13: // enter
case 27: // escape
e.preventDefault()
break
case 38: // up arrow
e.preventDefault()
this.prev()
break
case 40: // down arrow
e.preventDefault()
this.next()
break
}
e.stopPropagation()
}
, keydown: function (e) {
this.suppressKeyPressRepeat = ~$.inArray(e.keyCode, [40,38,9,13,27])
this.move(e)
}
, keypress: function (e) {
if (this.suppressKeyPressRepeat) return
this.move(e)
}
, keyup: function (e) {
switch(e.keyCode) {
case 40: // down arrow
case 38: // up arrow
case 16: // shift
case 17: // ctrl
case 18: // alt
break
case 9: // tab
case 13: // enter
if (!this.shown) return
this.select()
break
case 27: // escape
if (!this.shown) return
this.hide()
break
default:
this.lookup()
}
e.stopPropagation()
e.preventDefault()
}
, focus: function (e) {
this.focused = true
}
, blur: function (e) {
this.focused = false
if (!this.mousedover && this.shown) this.hide()
}
, click: function (e) {
e.stopPropagation()
e.preventDefault()
this.select()
this.$element.focus()
}
, mouseenter: function (e) {
this.mousedover = true
this.$menu.find('.active').removeClass('active')
$(e.currentTarget).addClass('active')
}
, mouseleave: function (e) {
this.mousedover = false
if (!this.focused && this.shown) this.hide()
}
}
/* TYPEAHEAD PLUGIN DEFINITION
* =========================== */
var old = $.fn.typeahead
$.fn.typeahead = function (option) {
return this.each(function () {
var $this = $(this)
, data = $this.data('typeahead')
, options = typeof option == 'object' && option
if (!data) $this.data('typeahead', (data = new Typeahead(this, options)))
if (typeof option == 'string') data[option]()
})
}
$.fn.typeahead.defaults = {
source: []
, items: 8
, menu: '<ul class="typeahead dropdown-menu"></ul>'
, item: '<li><a href="#"></a></li>'
, minLength: 1
}
$.fn.typeahead.Constructor = Typeahead
/* TYPEAHEAD NO CONFLICT
* =================== */
$.fn.typeahead.noConflict = function () {
$.fn.typeahead = old
return this
}
/* TYPEAHEAD DATA-API
* ================== */
$(document).on('focus.typeahead.data-api', '[data-provide="typeahead"]', function (e) {
var $this = $(this)
if ($this.data('typeahead')) return
$this.typeahead($this.data())
})
}(window.jQuery);

118
ordr2/static/js/functions.js

@ -0,0 +1,118 @@ @@ -0,0 +1,118 @@
$(document).ready(function() {
function capitalize(s){
return s.replace( /\b./g, function(a){ return a.toUpperCase(); } );
};
function generate_user_name() {
var first_name = $('.registration .item-first_name input').val();
var last_name = $('.registration .item-last_name input').val();
var user_name = capitalize(first_name) + capitalize(last_name);
return user_name.replace( /\s/g, '')
};
// autocomplete of the username (registration form)
//$('.registration .item-user_name input').val( generate_user_name() );
$('.registration .item-first_name input').keyup(function() {
$('.registration .item-user_name input').val( generate_user_name() );
});
$('.registration .item-last_name input').keyup(function() {
$('.registration .item-user_name input').val( generate_user_name() );
});
// "dispatch" clicking a th to the corresponding anchor
$('th.sortable').click( function() {
window.location = $(this).children('a').attr('href');
});
// "dispatch" clicking list item in collapse to the corresponding anchor
$('.accordion li').click( function() {
window.location = $(this).children('a').attr('href');
});
// the mark all
$('input[name="mark_all"]').click(function() {
var checked_status = this.checked;
$('input[name*="marked"]').each(function() {
this.checked = checked_status;
});
if( $('input[name="mark_all"]').is(':checked') && $('.marking-needed').is(':hidden') ) {
$('.marking-needed').fadeIn("fast");
} else if( $('input[name="mark_all"]').is(':checked') && $('.marking-needed').is(':visible') ) {
// do nothing
} else {
$('.marking-needed').fadeOut("fast");
}
});
// show actions only if some data is marked
$('input[name="marked"]').change(function() {
if( $('input[name*="marked"]').is(':checked') ) {
if( $('.marking-needed').is(':hidden') )
$('.marking-needed').fadeIn("slow");
} else {
$('.marking-needed').fadeOut("slow");
}
});
// quick-actions
$('.quick-action a[data-value]').click(function(event) {
event.preventDefault();
var value = $(this).attr('data-value');
var action = $(this).closest('div').attr('data-action');
$('select[name*='+action+']').each(function() {
$(this).val(value);
});
});
// submit search
$('.search-form input[type="search"]').keypress(function(event) {
if( event.keyCode == 13 && $('.typeahead.dropdown-menu').is(':hidden') )
$('.search-form').submit();
});
// aside filter
$('.filter input[type="checkbox"]').click( function() {
if( $(this).is(':checked') ) {
$(this).parents('li').addClass('active');
} else {
$(this).parents('li').removeClass('active');
}
});
// tooltips
$('.page-controls').tooltip({
selector: "[rel=tooltip]"
});
// calculator
if ( $('.item-unit_price input[name="amount"]').length ) {
if( $('.item-unit_price input[name="amount"]').val() != '' && $('input[name="quantity"]').val() != '' ){
var total = $('.item-unit_price input[name="amount"]').val().replace(",", "") * $('input[name="quantity"]').val();
total = Math.round(total*100)/100;
$('.item-total_price input[name="amount"]').attr( 'value', total );
}
$('.item-unit_price input[name="amount"], input[name="quantity"]').keyup(function() {
var total = $('.item-unit_price input[name="amount"]').val().replace(",", "") * $('input[name="quantity"]').val();
total = Math.round(total*100)/100;
$('.item-total_price input[name="amount"]').attr( 'value', total );
});
}
if ( $('.item-unit_price input[name="currency"]').length ) {
// added currency to total price
if( $('.item-unit_price input[name="currency"]').val() != '' ){
$('.item-total_price input[name="currency"]').attr( 'value', $('.item-unit_price input[name="currency"]').val() );
}
$('.item-unit_price input[name="currency"]').keyup(function() {
$('.item-total_price input[name="currency"]').attr( 'value', $('.item-unit_price input[name="currency"]').val() );
});
$('.item-unit_price input[name="currency"]').change(function() {
$('.item-total_price input[name="currency"]').attr( 'value', $('.item-unit_price input[name="currency"]').val() );
});
}
});

9252
ordr2/static/js/jquery.js vendored

File diff suppressed because it is too large Load Diff

BIN
ordr2/static/pyramid-16x16.png

Binary file not shown.

BIN
ordr2/static/pyramid.png

Binary file not shown.

154
ordr2/static/theme.css

@ -1,154 +0,0 @@ @@ -1,154 +0,0 @@
@import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700);
body {
font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
font-weight: 300;
color: #ffffff;
background: #bc2131;
}
h1,
h2,
h3,
h4,
h5,
h6 {
font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
font-weight: 300;
}
p {
font-weight: 300;
}
.font-normal {
font-weight: 400;
}
.font-semi-bold {
font-weight: 600;
}
.font-bold {
font-weight: 700;
}
.starter-template {
margin-top: 250px;
}
.starter-template .content {
margin-left: 10px;
}
.starter-template .content h1 {
margin-top: 10px;
font-size: 60px;
}
.starter-template .content h1 .smaller {
font-size: 40px;
color: #f2b7bd;
}
.starter-template .content .lead {
font-size: 25px;
color: #f2b7bd;
}
.starter-template .content .lead .font-normal {
color: #ffffff;
}
.starter-template .links {
float: right;
right: 0;
margin-top: 125px;
}
.starter-template .links ul {
display: block;
padding: 0;
margin: 0;
}
.starter-template .links ul li {
list-style: none;
display: inline;
margin: 0 10px;
}
.starter-template .links ul li:first-child {
margin-left: 0;
}
.starter-template .links ul li:last-child {
margin-right: 0;
}
.starter-template .links ul li.current-version {
color: #f2b7bd;
font-weight: 400;
}
.starter-template .links ul li a, a {
color: #f2b7bd;
text-decoration: underline;
}
.starter-template .links ul li a:hover, a:hover {
color: #ffffff;
text-decoration: underline;
}
.starter-template .links ul li .icon-muted {
color: #eb8b95;
margin-right: 5px;
}
.starter-template .links ul li:hover .icon-muted {
color: #ffffff;
}
.starter-template .copyright {
margin-top: 10px;
font-size: 0.9em;
color: #f2b7bd;
text-transform: lowercase;
float: right;
right: 0;
}
@media (max-width: 1199px) {
.starter-template .content h1 {
font-size: 45px;
}
.starter-template .content h1 .smaller {
font-size: 30px;
}
.starter-template .content .lead {
font-size: 20px;
}
}
@media (max-width: 991px) {
.starter-template {
margin-top: 0;
}
.starter-template .logo {
margin: 40px auto;
}
.starter-template .content {
margin-left: 0;
text-align: center;
}
.starter-template .content h1 {
margin-bottom: 20px;
}
.starter-template .links {
float: none;
text-align: center;
margin-top: 60px;
}
.starter-template .copyright {
float: none;
text-align: center;
}
}
@media (max-width: 767px) {
.starter-template .content h1 .smaller {
font-size: 25px;
display: block;
}
.starter-template .content .lead {
font-size: 16px;
}
.starter-template .links {
margin-top: 40px;
}
.starter-template .links ul li {
display: block;
margin: 0;
}
.starter-template .links ul li .icon-muted {
display: none;
}
.starter-template .copyright {
margin-top: 20px;
}
}

8
ordr2/templates/404.jinja2

@ -1,8 +0,0 @@ @@ -1,8 +0,0 @@
{% extends "layout.jinja2" %}
{% block content %}
<div class="content">
<h1><span class="font-semi-bold">Pyramid</span> <span class="smaller">Alchemy scaffold</span></h1>
<p class="lead"><span class="font-semi-bold">404</span> Page Not Found</p>
</div>
{% endblock content %}

39
ordr2/templates/account/login.jinja2

@ -0,0 +1,39 @@ @@ -0,0 +1,39 @@
{% extends "ordr2:templates/layout.jinja2" %}
{% import 'ordr2:templates/macros.jinja2' as macros with context %}
{% block subtitle %} Login {% endblock subtitle %}
{% block content %}
<div class="content">
<div class="container">
<div class="row">
<div class="span6 offset3">
<h1>Log in</h1>
{{ macros.flash_messages() }}
</div>
</div>
<div class="row">
<div class="span6 offset3">
<form action="{{request.resource_url(request.root, 'account', 'login')}}" method="post" class="form-horizontal">
<fieldset class="control-group">
<label for="input01" class="control-label">Username</label>
<div class="controls">
<input type="text" name="username" class="span3" size="30">
</div>
</fieldset>
<fieldset class="control-group">
<label for="password" class="control-label">Password</label>
<div class="controls">
<input type="password" name="password" class="span3" size="30">
</div>
</fieldset>
<fieldset class="form-actions">
<input type="hidden" name="csrf_token" value="{{get_csrf_token()}}">
<button class="btn primary large" type="submit">Log in</button>
</fieldset>
</form>
</div>
</div>
</div>
</div>
{% endblock content %}

24
ordr2/templates/account/password_reset.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 %} Account | Reset Password {% endblock subtitle %}
{% block content %}
<div class="content controls">
<div class="container-fluid">
<div class="row-fluid">
<div class="page-controls">
<h1>Reset your Password</h1>
</div>
</div>
<div class="row">
<div class="span8">
{{ macros.flash_messages() }}
{{form.render()|safe}}
</div>
</div>
</div>
</div>
{% endblock content %}

23
ordr2/templates/account/register.jinja2

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

32
ordr2/templates/account/register_sucessful.jinja2

@ -0,0 +1,32 @@ @@ -0,0 +1,32 @@
{% extends "ordr2:templates/layout.jinja2" %}
{% import 'ordr2:templates/macros.jinja2' as macros with context %}
{% block subtitle %} Register {% endblock subtitle %}
{% block content %}
<div class="content">
<div class="container">
<div class="row">
<div class="span12">
<hgroup id="register-successful">
<h1>Registration successful!!</h1>
</hgroup>
</div>
<div class="span10 offset1">
{{ macros.flash_messages() }}
<div class="alert alert-info">
<h4 class="alert-heading">Not so fast!</h4>
<p>
Before you can log in your account has first to be activated by an admin.
So lean back and read through the <a href="{{ request.resource_url(request.root, 'faq') }}">FAQ Page</a>.
Maybe the information will help you use this software better.
</p>
</div>
</div>
</div>
</div>
</div>
{% endblock content %}

24
ordr2/templates/account/settings.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 %} Account | Settings {% endblock subtitle %}
{% block content %}
<div class="content controls">
<div class="container-fluid">
<div class="row-fluid">
<div class="page-controls">
<h1>Account Settings</h1>
</div>
</div>
<div class="row">
<div class="span8">
{{ macros.flash_messages() }}
{{form.render()|safe}}
</div>
</div>
</div>
</div>
{% endblock content %}

36
ordr2/templates/admin/admin_section.jinja2

@ -0,0 +1,36 @@ @@ -0,0 +1,36 @@
{% extends "ordr2:templates/layout.jinja2" %}
{% import 'ordr2:templates/macros.jinja2' as macros with context %}
{% block subtitle %} Admin {% endblock subtitle %}
{% block content %}
<div class="content controls">
<div class="container-fluid">
<div class="row-fluid">
<div class="page-controls">
<h1>Register</h1>
</div>
</div>
<div class="row-fluid">{{ macros.flash_messages() }}</div>
<div class="row admin-options">
<a href="{{ request.resource_url(context, 'users') }}">
<div class="span4 rounded-box">
<div class="option user"></div>
<h2>Mangage Users</h2>
</div>
</a>
<a href="{{ request.resource_url(context, 'consumables') }}">
<div class="span4 rounded-box">
<div class="option shop"></div>
<h2>Manage Consumables</h2>
</div>
</a>
</div>
</div>
</div>
{% endblock content %}

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 %}

81
ordr2/templates/admin/consumable_list.jinja2

@ -0,0 +1,81 @@ @@ -0,0 +1,81 @@
{% 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, {'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>
</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 %}

24
ordr2/templates/admin/user_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 | User | {{ context.model.user_name }} {% endblock subtitle %}
{% block content %}
<div class="content controls">
<div class="container-fluid">
<div class="row-fluid">
<div class="page-controls">
<h1>Edit User: {{ context.model.user_name }}</h1>
</div>
</div>
<div class="row">
<div class="span8">
{{ macros.flash_messages() }}
{{form.render()|safe}}
</div>
</div>
</div>
</div>
{% endblock content %}

135
ordr2/templates/admin/user_list.jinja2

@ -0,0 +1,135 @@ @@ -0,0 +1,135 @@
{% extends "ordr2:templates/layout.jinja2" %}
{% import 'ordr2:templates/macros.jinja2' as macros with context %}
{% block subtitle %} Admin | Users {% 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>
Users
</h1>
</div>
{{ macros.filter_box('Role', 'role', roles, {'search':None}) }}
</div>
<div class="span10">
<form action="{{ request.resource_url(context, 'actions') }}" method="POST">
<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" value="{{ context.filters.get('search', None) or '' }}">
<label class="add-on">
<button type="submit" class="search" name="action" value="search">Search</button>
</label>
</div>
<div class="actions">
<div class="btn-group marking-needed">
<button rel="tooltip" data-original-title="Change Role" class="btn-flat group" name="action" type="submit" value="role"><i class="group"></i></button>
<button rel="tooltip" data-original-title="Delete" class="btn-flat delete" name="action" type="submit" value="delete"><i class="trash"></i></button>
</div>
<a href="#modal-display" rel="tooltip" data-original-title="Display Options" class="btn-flat single" data-toggle="modal"><i class="eye"></i></a>
</div>
</div>
{{ macros.flash_messages() }}
{% if users %}
{{ macros.show_or_hide_columns('users') }}
<table class="table">
<thead>
<th class="center">
<input type="checkbox" value="all" name="mark_all" id="mark_all">
</th>
{{ macros.sortable_table_header('Username', 'user') }}
{{ macros.sortable_table_header('First Name', 'first') }}
{{ macros.sortable_table_header('Last Name', 'last') }}
{{ macros.sortable_table_header('Email', 'email') }}
{{ macros.sortable_table_header('Role', 'role') }}
<th>Actions</th>
</thead>
<tbody>
{% for user in users %}
<tr>
<td class="center">
<input type="checkbox" name="marked" value="{{ user.model.id }}">
</td>
<td class="column-user">
<a href="{{ request.resource_url(request.root, 'orders', query={'user': user.model.user_name}) }}" title="click to view all orders from user">{{ user.model.user_name }}</a>
</td>
<td class="column-first">{{ user.model.first_name }} </td>
<td class="column-last">{{ user.model.last_name }} </td>
<td class="column-email">{{ user.model.email }} </td>
<td class="column-role">{{ user.model.role.value.capitalize() }} </td>
<td>
<a href="{{ request.resource_url(user) }}" class="action edit" title="Edit User">edit</a>
<a href="{{ request.resource_url(user, 'delete') }}" class="action delete" title="Delete User">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 %}
</form>
</div>
</div>
</div>
<div id="modal-display" class="modal hide fade">
<form action="{{ request.resource_url(context, 'changeview', query=context.query_params()) }}" method="POST" class="checkslist">
<input type="hidden" name="csrf_token" value="{{get_csrf_token()}}">
{% set display = request.session.get('display', dict()) %}
{% set settings = display.get('users', dict()) %}
<div class="modal-header">
<a href="#" class="close" data-dismiss="modal">×</a>
<h3>Display Options</h3>
</div>
<div class="modal-body">
<p class="help-block"><span class="label notice">Notice</span> If the displayed information is too cluttered, deselect some fields below. This will temporaly remove them from your view and should help you stay on top of things.</p>
<div class="checklist">
<fieldset class="left">
<label class="checkbox">
<input type="checkbox" checked="checked" value="user" name="display" disabled="disabled" >
Username
</label>
<label class="checkbox">
<input type="checkbox" {{ 'checked="checked"' if settings.get('first') }} value="first" name="display">
First Name
</label>
<label class="checkbox">
<input type="checkbox" {{ 'checked="checked"' if settings.get('last') }} value="last" name="display">
Last Name
</label>
</fieldset>
<fieldset class="right">
<label class="checkbox">
<input type="checkbox" {{ 'checked="checked"' if settings.get('email') }} value="email" name="display">
Email
</label>
<label class="checkbox">
<input type="checkbox" checked="checked" value="role" name="display" disabled="disabled" >
Role
</label>
</fieldset>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-primary" type="submit">Apply Changes</button>
<a data-dismiss="modal" class="btn" href="#">Close</a>
</div>
</form>
</div>
</div>
{% endblock content %}

79
ordr2/templates/admin/users_change_roles.jinja2

@ -0,0 +1,79 @@ @@ -0,0 +1,79 @@
{% extends "ordr2:templates/layout.jinja2" %}
{% import 'ordr2:templates/macros.jinja2' as macros with context %}
{% block subtitle %} Admin | Users | Change Roles {% endblock subtitle %}
{% block content %}
<div class="content controls">
<div class="container-fluid">
<div class="row-fluid">
<div class="page-controls">
<h1>Change Role of User{{ 's' if accounts|length > 1 }}</h1>
</div>
</div>
<div class="row">
<div class="span10">
<div class="action-header">
<h3>The role of the following user{{ 's' if accounts|length > 1 }} will be changed:</h3>
</div>
<form action="{{ request.resource_url(context, 'roles') }}" method="POST" class="action">
<input type="hidden" name="csrf_token" value="{{get_csrf_token()}}">
<table class="table">
<thead>
<th>Username</th>
<th>First Name</th>
<th>Last Name</th>
<th>Email</th>
<th>Role</th>
</thead>
<tbody>
{% for account in accounts %}
<tr>
<td class="column-user">
{{ account.user_name }}
</td>
<td>{{ account.first_name }} </td>
<td>{{ account.last_name }} </td>
<td>{{ account.email }} </td>
<td>
<select name="account-{{ account.id }}" class="select-role span2">
{% for value, display in roles %}
<option value="{{ value }}" {{ 'selected="selected"' if value == account.role.name }}>{{ display }}</option>
{% endfor %}
</select>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<fieldset class="form-actions">
<div class="right">
<button name="change" type="submit" value="change" class="btn btn-large btn-danger">Change Role{{ 's' if accounts|length > 1 }}</button>
<button name="cancel" type="submit" value="cancel" class="btn btn-large">Cancel</button>
</div>
<div class="btn-group quick-action left" data-action="account">
<a data-value="USER" href="#" class="btn btn-large btn-primary">Set all to User</a>
<a href="#" data-toggle="dropdown" class="btn btn-large btn-primary dropdown-toggle"><span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a data-value="INACTIVE" href="#">Set all to Inactive</a></li>
<li><a data-value="PURCHASER" href="#">Set all to Purchaser</a></li>
</ul>
</div>
</fieldset>
</form>
</div>
</div>
</div>
</div>
{% endblock content %}

66
ordr2/templates/admin/users_delete.jinja2

@ -0,0 +1,66 @@ @@ -0,0 +1,66 @@
{% extends "ordr2:templates/layout.jinja2" %}
{% import 'ordr2:templates/macros.jinja2' as macros with context %}
{% block subtitle %} Admin | Users | Confirm Delete {% endblock subtitle %}
{% block content %}
<div class="content controls">
<div class="container-fluid">
<div class="row-fluid">
<div class="page-controls">
<h1>Delete User{{ 's' if accounts|length > 1 }}</h1>
</div>
</div>
<div class="row">
<div class="span10">
<div class="action-header">
<h3>The following user{{ 's' if accounts|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>Username</th>
<th>First Name</th>
<th>Last Name</th>
<th>Email</th>
<th>Role</th>
</thead>
<tbody>
{% for account in accounts %}
<tr>
<td class="column-user">
<input type="hidden" name="account" value="{{ account.id }}">
{{ account.user_name }}
</td>
<td>{{ account.first_name }} </td>
<td>{{ account.last_name }} </td>
<td>{{ account.email }} </td>
<td>{{ account.role.value.capitalize() }} </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 User{{ 's' if accounts|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 %}

97
ordr2/templates/deform/form.pt

@ -0,0 +1,97 @@ @@ -0,0 +1,97 @@
<form
tal:define="style style|field.widget.style;
css_class css_class|string:${field.widget.css_class or field.css_class or ''};
item_template item_template|field.widget.item_template;
autocomplete autocomplete|field.autocomplete;
title title|field.title;
errormsg errormsg|field.errormsg;
description description|field.description;
buttons buttons|field.buttons;
use_ajax use_ajax|field.use_ajax;
ajax_options ajax_options|field.ajax_options;
formid formid|field.formid;
action action|field.action or None;
method method|field.method;"
tal:attributes="autocomplete autocomplete;
style style;
class css_class;
action action;"
id="${formid}"
method="${method}"
enctype="multipart/form-data"
accept-charset="utf-8"
i18n:domain="deform"
>
<fieldset class="deform-form-fieldset">
<legend tal:condition="title">${title}</legend>
<input type="hidden" name="_charset_" />
<input type="hidden" name="__formid__" value="${formid}"/>
<div class="alert alert-danger" tal:condition="field.error">
<div class="error-msg-lbl" i18n:translate=""
>There was a problem with your submission</div>
<div class="error-msg-detail" i18n:translate=""
>Errors have been highlighted below</div>
<p class="error-msg">${field.errormsg}</p>
</div>
<p class="section first" tal:condition="description">
${description}
</p>
<div tal:repeat="child field"
tal:replace="structure child.render_template(item_template)"/>
<div class="form-actions deform-form-buttons">
<tal:loop tal:repeat="button buttons">
<button
tal:define="btn_disposition repeat.button.start and 'btn-primary' or 'btn-default';
btn_icon button.icon|None"
tal:attributes="disabled button.disabled if button.disabled else None"
id="${formid+button.name}"
name="${button.name}"
type="${button.type}"
class="btn ${button.css_class or btn_disposition}"
value="${button.value}">
<i tal:condition="btn_icon" class="${btn_icon}"> </i>
${button.title}
</button>
</tal:loop>
</div>
</fieldset>
<script type="text/javascript" tal:condition="use_ajax">
deform.addCallback(
'${formid}',
function(oid) {
var target = '#' + oid;
var options = {
target: target,
replaceTarget: true,
success: function() {
deform.processCallbacks();
deform.focusFirstInput(target);
},
beforeSerialize: function() {
// See http://bit.ly/1agBs9Z (hack to fix tinymce-related ajax bug)
if ('tinymce' in window) {
$(tinymce.get()).each(
function(i, el) {
var content = el.getContent();
var editor_input = document.getElementById(el.id);
editor_input.value = content;
});
}
}
};
var extra_options = ${ajax_options} || {};
$('#' + oid).ajaxForm($.extend(options, extra_options));
}
);
</script>
</form>

51
ordr2/templates/deform/mapping_item.pt

@ -0,0 +1,51 @@ @@ -0,0 +1,51 @@
<div tal:define="error_class error_class|field.widget.error_class;
description description|field.description;
title title|field.title;
oid oid|field.oid;
hidden hidden|field.widget.hidden;
category category|field.widget.category;
structural hidden or category == 'structural';
required required|field.required;"
class="control-group ${field.error and 'error' or ''} ${field.widget.item_css_class or ''} ${field.default_item_css_class()}"
title="${description}"
id="item-${oid}"
tal:omit-tag="structural"
i18n:domain="deform">
<label for="${oid}"
class="control-label ${required and 'required' or ''}"
tal:condition="not structural"
id="req-${oid}"
>
${title}
</label>
<div class="controls">
<div tal:define="input_prepend field.widget.input_prepend | None;
input_append field.widget.input_append | None"
tal:omit-tag="not (input_prepend or input_append)"
class="input-group">
<span class="input-group-addon"
tal:condition="input_prepend">${input_prepend}</span
><span tal:replace="structure field.serialize(cstruct).strip()"
/><span class="input-group-addon"
tal:condition="input_append">${input_append}</span>
</div>
<p class="help-inline"
tal:define="errstr 'error-%s' % field.oid"
tal:repeat="msg field.error.messages()"
i18n:translate=""
tal:attributes="id repeat.msg.index==0 and errstr or
('%s-%s' % (errstr, repeat.msg.index))"
tal:condition="field.error and not field.widget.hidden and not field.typ.__class__.__name__=='Mapping'">
${msg}
</p>
<p tal:condition="field.description and not field.widget.hidden"
class="help-inline" >
${field.description}
</p>
</div>
</div>

36
ordr2/templates/deform/money_mapping.pt

@ -0,0 +1,36 @@ @@ -0,0 +1,36 @@
<span tal:define="error_class error_class|field.widget.error_class;
description description|field.description;
title title|field.title;
hidden hidden|field.widget.hidden;
category category|field.widget.category;
item_template item_template|field.widget.item_template;
amount_field field.children[0];
currency_field field.children[1];
oid oid|amount_field.oid;
required required|amount_field.required;"
i18n:domain="deform"
class="moneyinput">
${field.start_mapping()}
<div tal:repeat="child field.children"
tal:replace="structure child.render_template(item_template)" >
</div>
${field.end_mapping()}
<p class="help-inline"
tal:define="errstr 'error-%s' % field.oid"
tal:repeat="msg amount_field.error.messages()|currency_field.error.messages()"
i18n:translate=""
tal:attributes="id repeat.msg.index==0 and errstr or
('%s-%s' % (errstr, repeat.msg.index))"
tal:condition="(amount_field.error or currency_field.error) and not field.widget.hidden">
${msg}
</p>
<p tal:condition="field.description and not field.widget.hidden"
class="help-inline" >
${field.description}
</p>
</span>

36
ordr2/templates/deform/money_mapping_disabled.pt

@ -0,0 +1,36 @@ @@ -0,0 +1,36 @@
<span tal:define="error_class error_class|field.widget.error_class;
description description|field.description;
title title|field.title;
hidden hidden|field.widget.hidden;
category category|field.widget.category;
item_template item_template|field.widget.item_readonly_template;
amount_field field.children[0];
currency_field field.children[1];
oid oid|amount_field.oid;
required required|amount_field.required;"
i18n:domain="deform"
class="moneyinput">
${field.start_mapping()}
<div tal:repeat="child field.children"
tal:replace="structure child.render_template(item_template)" >
</div>
${field.end_mapping()}
<p class="help-inline"
tal:define="errstr 'error-%s' % field.oid"
tal:repeat="msg amount_field.error.messages()|currency_field.error.messages()"
i18n:translate=""
tal:attributes="id repeat.msg.index==0 and errstr or
('%s-%s' % (errstr, repeat.msg.index))"
tal:condition="(amount_field.error or currency_field.error) and not field.widget.hidden">
${msg}
</p>
<p tal:condition="field.description and not field.widget.hidden"
class="help-inline" >
${field.description}
</p>
</span>

25
ordr2/templates/deform/money_mapping_item.pt

@ -0,0 +1,25 @@ @@ -0,0 +1,25 @@
<span tal:define="error_class error_class|field.widget.error_class;
description description|field.description;
title title|field.title;
oid oid|field.oid;
hidden hidden|field.widget.hidden;
category category|field.widget.category;
structural hidden or category == 'structural';
required required|field.required;"
class="${field.error and 'error' or ''} ${field.widget.item_css_class or ''} ${field.default_item_css_class()}"
title="${description}"
id="item-${oid}"
tal:omit-tag="structural"
i18n:domain="deform">
<div tal:define="input_prepend field.widget.input_prepend | None;
input_append field.widget.input_append | None"
tal:omit-tag="not (input_prepend or input_append)"
class="input-group">
<span class="input-group-addon"
tal:condition="input_prepend">${input_prepend}</span
><span tal:replace="structure field.serialize(cstruct).strip()"
/><span class="input-group-addon"
tal:condition="input_append">${input_append}</span>
</div>
</span>

25
ordr2/templates/deform/money_mapping_item_diabled.pt

@ -0,0 +1,25 @@ @@ -0,0 +1,25 @@
<span tal:define="error_class error_class|field.widget.error_class;
description description|field.description;
title title|field.title;
oid oid|field.oid;
hidden hidden|field.widget.hidden;
category category|field.widget.category;
structural hidden or category == 'structural';
required required|field.required;"
class="${field.error and 'error' or ''} ${field.widget.item_css_class or ''} ${field.default_item_css_class()}"
title="${description}"
id="item-${oid}"
tal:omit-tag="structural"
i18n:domain="deform">
<div tal:define="input_prepend field.widget.input_prepend | None;
input_append field.widget.input_append | None"
tal:omit-tag="not (input_prepend or input_append)"
class="input-group">
<span class="input-group-addon"
tal:condition="input_prepend">${input_prepend}</span
><span tal:replace="structure field.serialize(cstruct, readonly=True).strip()"
/><span class="input-group-addon"
tal:condition="input_append">${input_append}</span>
</div>
</span>

71
ordr2/templates/deform/order_info_mapping.pt

@ -0,0 +1,71 @@ @@ -0,0 +1,71 @@
<tal:def tal:define="title title|field.title;
description description|field.description;
errormsg errormsg|field.errormsg;
item_template item_template|field.widget.item_template;
request field.schema.bindings['request']"
i18n:domain="deform">
<div class="panel panel-default" title="${description}">
<div class="panel-heading">${title}</div>
<div class="panel-body">
<div tal:condition="errormsg"
class="clearfix alert alert-danger">
<p i18n:translate="">
There was a problem with this section
</p>
<p>${errormsg}</p>
</div>
<div tal:condition="description">
${description}
</div>
${field.start_mapping()}
<div tal:repeat="child field.children"
tal:replace="structure child.render_template(item_template)" >
</div>
<div class="control-group">
<label class="control-label"> Placed </label>
<div class="controls">
<p class="form-control-static">
${request.context.model.placed}
</p>
</div>
</div>
<div class="control-group">
<label class="control-label"> Approved </label>
<div class="controls">
<p class="form-control-static">
${request.context.model.approved}
</p>
</div>
</div>
<div class="control-group">
<label class="control-label"> Ordered </label>
<div class="controls">
<p class="form-control-static">
${request.context.model.ordered}
</p>
</div>
</div>
<div class="control-group">
<label class="control-label"> Completed </label>
<div class="controls">
<p class="form-control-static">
${request.context.model.completed}
</p>
</div>
</div>
${field.end_mapping()}
</div>
</div>
</tal:def>

42
ordr2/templates/deform/select_disabled.pt

@ -0,0 +1,42 @@ @@ -0,0 +1,42 @@
<div tal:define="
name name|field.name;
oid oid|field.oid;
style style|field.widget.style;
size size|field.widget.size;
css_class css_class|field.widget.css_class;
unicode unicode|str;
optgroup_class optgroup_class|field.widget.optgroup_class;
multiple multiple|field.widget.multiple;"
tal:omit-tag="">
<input type="hidden" name="__start__" value="${name}:sequence"
tal:condition="multiple" />
<select tal:attributes="
name name;
id oid;
class string: form-control ${css_class or ''};
multiple multiple;
size size;
style style;"
readonly="readonly">
<tal:loop tal:repeat="item values">
<optgroup tal:condition="isinstance(item, optgroup_class)"
tal:attributes="label item.label">
<option tal:repeat="(value, description) item.options"
tal:attributes="
selected python:field.widget.get_select_value(cstruct, value);
class css_class;
label field.widget.long_label_generator and description;
value value"
tal:content="field.widget.long_label_generator and field.widget.long_label_generator(item.label, description) or description"/>
</optgroup>
<option tal:condition="not isinstance(item, optgroup_class)"
tal:attributes="
selected python:field.widget.get_select_value(cstruct, item[0]);
class css_class;
value item[0]">${item[1]}</option>
</tal:loop>
</select>
<input type="hidden" name="__end__" value="${name}:sequence"
tal:condition="multiple" />
</div>

22
ordr2/templates/deform/textinput_disabled.pt

@ -0,0 +1,22 @@ @@ -0,0 +1,22 @@
<span tal:define="name name|field.name;
css_class css_class|field.widget.css_class;
oid oid|field.oid;
mask mask|field.widget.mask|None;
mask_placeholder mask_placeholder|field.widget.mask_placeholder|'_';
style style|field.widget.style;
"
tal:omit-tag="">
<input type="text" name="${name}" value="${cstruct}"
tal:attributes="class string: form-control ${css_class or ''};
style style"
id="${oid}"
readonly="readonly"/>
<script tal:condition="mask" type="text/javascript">
deform.addCallback(
'${oid}',
function (oid) {
$("#" + oid).mask("${mask}",
{placeholder:"${mask_placeholder}"});
});
</script>
</span>

25
ordr2/templates/emails/activation.jinja2

@ -0,0 +1,25 @@ @@ -0,0 +1,25 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>ordr Account Activation</title>
<link href='http://fonts.googleapis.com/css?family=Anton&subset=latin,latin-ext' rel='stylesheet' type='text/css'>
<link rel="stylesheet" href="{{request.static_url('ordr2:static/css/email.css')}}" type="text/css" media="screen">
</head>
<body>
<h1>Hi there!</h1>
<p>
Your account {{ user.user_name }} has been activated.
<a href="{{ request.resource_url(request.root) }}">Log in and start ordering.</a>
</p>
<p class="signature">
Regards,
<br/>
<span class="brand">ordr</span>
</p>
<p class="footprint">
<small>Please don't respont to this email! This is an automatically generated notification by the system.</small>
<a href="http://distractedbysquirrels.com/" target="_blank" title="This software was originally written by Sebastian Sebald." class="icon-dbs"></a>
</p>
</body>
</html>

36
ordr2/templates/emails/order.jinja2

@ -0,0 +1,36 @@ @@ -0,0 +1,36 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>ordr Notification</title>
<link href='http://fonts.googleapis.com/css?family=Anton&subset=latin,latin-ext' rel='stylesheet' type='text/css'>
<link rel="stylesheet" href="{{request.static_url('ordr2:static/css/email.css')}}" type="text/css" media="screen">
</head>
<body>
<h1>Hi there!</h1>
<p>
Your purchase of the following item
{% if data.status.name == 'ORDERED' %}
has <u>been orderd</u>:
{% else %}
has <u>arrived</u>:
{% endif %}
<strong>{{ data }}</strong>
</p>
<p>
If you want to check details about the purchase go here: <a href="{{ request.resource_url(request.root, 'orders', data.id) }}">{{ request.resource_url(request.root, 'orders', data.id) }}</a>
</p>
<p>
</p>
<p class="signature">
Regards,
<br/>
<span class="brand">ordr</span>
</p>
<p class="footprint">
<small>Please don't respont to this email! This is an automatically generated notification by the system.</small>
<a href="http://distractedbysquirrels.com/" target="_blank" title="This software was originally written by Sebastian Sebald." class="icon-dbs"></a>
</p>
</body>
</html>

25
ordr2/templates/emails/password_reset.jinja2

@ -0,0 +1,25 @@ @@ -0,0 +1,25 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>ordr Password Reset</title>
<link href='http://fonts.googleapis.com/css?family=Anton&subset=latin,latin-ext' rel='stylesheet' type='text/css'>
<link rel="stylesheet" href="{{request.static_url('ordr2:static/css/email.css')}}" type="text/css" media="screen">
</head>
<body>
<h1>Hi there!</h1>
<p>
If you forgot your password, you can set a new one by
<a href="{{ request.resource_url(request.root, 'account', 'reset', data) }}">clicking this link.</a>
</p>
<p class="signature">
Regards,
<br/>
<span class="brand">ordr</span>
</p>
<p class="footprint">
<small>Please don't respont to this email! This is an automatically generated notification by the system.</small>
<a href="http://distractedbysquirrels.com/" target="_blank" title="This software was originally written by Sebastian Sebald." class="icon-dbs"></a>
</p>
</body>
</html>

21
ordr2/templates/errors/bad_csrf_token.jinja2

@ -0,0 +1,21 @@ @@ -0,0 +1,21 @@
{% extends "ordr2:templates/layout.jinja2" %}
{% block subtitle %} Whoops! {% endblock subtitle %}
{% block content %}
<div class="content">
<div class="container">
<div class="row">
<div class="span12">
<hgroup id="access-denied">
<h1>Please try again.</h1>
<p class="info">There was a problem with your form submission.</p>
<p class="info">Maybe it took you too long to fill out the form.</p>
</hgroup>
</div>
</div>
</div>
</div>
{% endblock content %}

65
ordr2/templates/errors/exception.jinja2

@ -0,0 +1,65 @@ @@ -0,0 +1,65 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Ordr | Whoops!</title>
<link href="{{request.static_url('ordr2:static/img/favicon.ico')}}" type="image/x-icon" rel="shortcut icon">
<link href='http://fonts.googleapis.com/css?family=Anton&subset=latin,latin-ext' rel='stylesheet' type='text/css'>
<link rel="stylesheet" href="{{request.static_url('ordr2:static/css/bootstrap.css')}}" type="text/css" media="screen">
<link rel="stylesheet" href="{{request.static_url('ordr2:static/css/bootstrap-responsive.css')}}" type="text/css" media="screen">
<link rel="stylesheet" href="{{request.static_url('ordr2:static/css/style.css')}}" type="text/css" media="screen" />
<link rel="stylesheet" href="{{request.static_url('deform:static/css/form.css')}}" type="text/css" media="screen" />
<!--[if !IE 7]>
<style type="text/css">
#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">
<div class="navbar-inner">
<div class="container-fluid">
<a href="{{request.resource_url(request.root)}}" class="brand">ordr</a>
</div>
</div>
</header>
<div class="content">
<div class="container">
<div class="row">
<div class="span12">
<hgroup id="access-denied">
<h1>Whoops!</h1>
<p class="info">This really shouldn't happen - You encountered a bug.</p>
</hgroup>
</div>
</div>
</div>
</div>
<footer>
<div class="copy">
<a class="icon-dbs" title="This software was orignially written by Sebastian Sebald." target="_blank" href="http://distractedbysquirrels.com/"></a>
</div>
</footer>
<!--<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>
<script src="{{request.static_url('ordr2:static/js/bootstrap-tooltip.js')}}"></script>
<script src="{{request.static_url('ordr2:static/js/bootstrap-typeahead.js')}}"></script>
<script src="{{request.static_url('ordr2:static/js/bootstrap-collapse.js')}}"></script>
<script src="{{request.static_url('ordr2:static/js/functions.js')}}"></script>
</body>
</html>

20
ordr2/templates/errors/forbidden.jinja2

@ -0,0 +1,20 @@ @@ -0,0 +1,20 @@
{% extends "ordr2:templates/layout.jinja2" %}
{% block subtitle %} Access Denied {% endblock subtitle %}
{% block content %}
<div class="content">
<div class="container">
<div class="row">
<div class="span12">
<hgroup id="access-denied">
<h1>Access Denied!!!</h1>
<p class="info">You do not have the permission to access this page.</p>
</hgroup>
</div>
</div>
</div>
</div>
{% endblock content %}

20
ordr2/templates/errors/not_found.jinja2

@ -0,0 +1,20 @@ @@ -0,0 +1,20 @@
{% extends "ordr2:templates/layout.jinja2" %}
{% block subtitle %} Not Found {% endblock subtitle %}
{% block content %}
<div class="content">
<div class="container">
<div class="row">
<div class="span12">
<hgroup id="access-denied">
<h1>These are not the droids<br>you are looking for</h1>
<p class="info">Whatever you wanted to see was not found on this server.</p>
</hgroup>
</div>
</div>
</div>
</div>
{% endblock content %}

129
ordr2/templates/layout.jinja2

@ -1,64 +1,85 @@ @@ -1,64 +1,85 @@
<!DOCTYPE html>
<html lang="{{request.locale_name}}">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="pyramid web application">
<meta name="author" content="Pylons Project">
<link rel="shortcut icon" href="{{request.static_url('ordr2:static/pyramid-16x16.png')}}">
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Cookiecutter Alchemy project for the Pyramid Web Framework</title>
<title>Ordr | {% block subtitle %} Subtitle {% endblock subtitle %}</title>
<!-- Bootstrap core CSS -->
<link href="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet">
<link href="{{request.static_url('ordr2:static/img/favicon.ico')}}" type="image/x-icon" rel="shortcut icon">
<!-- Custom styles for this scaffold -->
<link href="{{request.static_url('ordr2:static/theme.css')}}" rel="stylesheet">
<link href='https://fonts.googleapis.com/css?family=Anton&subset=latin,latin-ext' rel='stylesheet' type='text/css'>
<!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries -->
<!--[if lt IE 9]>
<script src="//oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
<script src="//oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script>
<![endif]-->
</head>
<body>
<div class="starter-template">
<div class="container">
<div class="row">
<div class="col-md-2">
<img class="logo img-responsive" src="{{request.static_url('ordr2:static/pyramid.png') }}" alt="pyramid web framework">
</div>
<div class="col-md-10">
{% block content %}
<p>No content</p>
{% endblock content %}
</div>
</div>
<div class="row">
<div class="links">
<ul>
<li><i class="glyphicon glyphicon-cog icon-muted"></i><a href="https://github.com/Pylons/pyramid">Github Project</a></li>
<li><i class="glyphicon glyphicon-globe icon-muted"></i><a href="https://webchat.freenode.net/?channels=pyramid">IRC Channel</a></li>
<li><i class="glyphicon glyphicon-home icon-muted"></i><a href="http://pylonsproject.org">Pylons Project</a></li>
</ul>
</div>
</div>
<div class="row">
<div class="copyright">
Copyright &copy; Pylons Project
</div>
<link rel="stylesheet" href="{{request.static_url('ordr2:static/css/bootstrap.css')}}" type="text/css" media="screen">
<link rel="stylesheet" href="{{request.static_url('ordr2:static/css/bootstrap-responsive.css')}}" type="text/css" media="screen">
<link rel="stylesheet" href="{{request.static_url('ordr2:static/css/style.css')}}" type="text/css" media="screen" />
<link rel="stylesheet" href="{{request.static_url('deform:static/css/form.css')}}" type="text/css" media="screen" />
<!--[if !IE 7]>
<style type="text/css">
#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">
<div class="navbar-inner">
<div class="container-fluid">
<a href="{{request.resource_url(request.root)}}" class="brand">ordr</a>
{% if request.user %}
<ul class="nav">
<li {% if context.nav_highlight == 'orders' %} class="active" {% endif %}><a href="{{request.resource_url(request.root, 'orders')}}">Orders</a></li>
<li {% if context.nav_highlight == 'faq' %} class="active" {% endif %}><a href="{{request.resource_url(request.root, 'faq')}}">FAQs</a></li>
{% if request.user.role.name == 'ADMIN' %}
<li {% if context.nav_highlight == 'admin' %} class="active" {% endif %}><a href="{{request.resource_url(request.root, 'admin')}}">Admin</a></li>
{% endif %}
</ul>
<ul class="nav pull-right">
<li class="dropdown" id="user-options">
<a data-toggle="dropdown" class="dropdown-toggle" href="#">Logged in as <span class="user-name">{{request.user.user_name}}</span></a>
<ul class="dropdown-menu">
<li><a href="{{request.resource_url(request.root, 'account', 'settings')}}">Settings</a></li>
<li><a href="https://git.cpi.imtek.uni-freiburg.de/holgi/ordr2/issues">Submit an Issue</a></li>
<li class="divider"></li>
<li><a href="{{request.resource_url(request.root, 'account', 'logout')}}">Logout</a></li>
</ul>
</li>
</ul>
{% else %}
<ul class="nav">
<li {% if context.nav_highlight == 'register' %} class="active" {% endif %}><a href="{{request.resource_url(request.root, 'account', 'register')}}">Register</a></li>
<li {% if context.nav_highlight == 'about' %} class="active" {% endif %}><a href="{{request.resource_url(request.root, 'about')}}">About</a></li>
</ul>
<form action="{{request.resource_url(request.root, 'account', 'login')}}" method="post" class="navbar-form pull-right">
<input type="hidden" name="csrf_token" value="{{get_csrf_token()}}">
<input name="username" type="text" placeholder="Username" class="input-small">
<input name="password" type="password" placeholder="Password" class="input-small">
<button type="submit" class="btn">Log in</button>
</form>
{% endif %}
</div>
</div>
</div>
</header>
{% block content %}
<p>No content</p>
{% endblock content %}
<footer>
<div class="copy">
<a class="icon-dbs" title="This software was orignially written by Sebastian Sebald." target="_blank" href="http://distractedbysquirrels.com/"></a>
</div>
<!-- Bootstrap core JavaScript
================================================== -->
<!-- Placed at the end of the document so the pages load faster -->
<script src="//oss.maxcdn.com/libs/jquery/1.10.2/jquery.min.js"></script>
<script src="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/js/bootstrap.min.js"></script>
</body>
</footer>
<!--<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>
<script src="{{request.static_url('ordr2:static/js/bootstrap-tooltip.js')}}"></script>
<script src="{{request.static_url('ordr2:static/js/bootstrap-typeahead.js')}}"></script>
<script src="{{request.static_url('ordr2:static/js/bootstrap-collapse.js')}}"></script>
<script src="{{request.static_url('ordr2:static/js/functions.js')}}"></script>
</body>
</html>

110
ordr2/templates/macros.jinja2

@ -0,0 +1,110 @@ @@ -0,0 +1,110 @@
{% macro flash_messages() -%}
{% for queue in ('success', 'info', 'warning', 'error') %}
{% for message in request.session.pop_flash(queue) %}
{% set css_class = 'danger' if queue == 'error' else queue %}
<div class="alert alert-{{ css_class }} {% if message.dismissable %}alert-dismissible{% endif %}" role="alert">
{% if message.dismissable %}
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
{% endif %}
<h4 class="alert-heading">{{message.message|safe}}</h4>
{% if message.description %}
<p>{{message.description|safe}}</p>
{% endif %}
</div>
{% endfor %}
{% endfor %}
{%- endmacro %}
{% macro filter_box(title, query_key, items, extras=None) -%}
{% if not extras %}
{% set extras = dict() %}
{% endif %}
{% set extras_active = context|are_extras_active(extras) %}
<div class="well sidebar-nav">
<ul class="nav nav-list">
<li class="nav-header">{{ title }}</li>
<li {% if not context.filters.get(query_key) and extras_active %}class="active"{% endif %}>
<a href="{{ context.url( (query_key, None), (context.query_key_current_page, 1), *extras.items() ) }}">All</a>
</li>
{% for filter, display in items %}
<li {% if context.filters.get(query_key) == filter and extras_active %}class="active"{% endif %}>
<a href="{{ context.url( (query_key, filter), (context.query_key_current_page, 1), *extras.items() ) }}">{{ display }}</a>
</li>
{% endfor %}
</ul>
</div>
{%- endmacro %}
{% macro show_or_hide_columns(name) -%}
{% set display = request.session.get('display', dict()) %}
{% set section = display.get(name, None) %}
{% if section %}
<style type="text/css">
{% for column, show in section.items() %}
{% if not show %}
.column-{{ column }} { display: none; }
{% endif %}
{% endfor %}
</style>
{% 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' 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>
{%- endmacro %}
{% macro colored_status(status) -%}
{% if status.name == 'APPROVAL' %}
<span class="label label-info">
{% elif status.name == 'ORDERED' %}
<span class="label label-warning">
{% elif status.name == 'COMPLETED' %}
<span class="label label-success">
{% else %}
<span class="label label-important">
{% endif %}
{{ status.value|lower }}
</span>
{%- endmacro %}
{% macro pagination_helper(page, text=None, css_class='') -%}
{% set is_active = 'active' if page == context.pages.current %}
{% set is_disabled = 'disabled' if not page %}
{% set url = context.url( (context.query_key_current_page, page) ) if page %}
<li class="{{ css_class }} {{ is_active }} {{ is_disabled }}">
<a href="{{ url }}" >{{ text if text else page }}</a>
</li>
{%- endmacro %}}
{% macro pagination() -%}
{% if context.pages and context.pages.last > 1 %}
<div class="pagination pagination-centered">
<ul>
{{ pagination_helper(context.pages.previous, '\u2190 Previous', 'prev') }}
{% if context.pages.first not in context.pages.window %}
{{ pagination_helper(None, '...') }}
{% endif %}
{% for page in context.pages.window %}
{{ pagination_helper(page) }}
{% endfor %}
{% if context.pages.last not in context.pages.window %}
{{ pagination_helper(None, '...') }}
{% endif %}
{{ pagination_helper(context.pages.next, 'Next \u2192', 'next') }}
</ul>
</div>
{% endif %}
{%- endmacro %}

8
ordr2/templates/mytemplate.jinja2

@ -1,8 +0,0 @@ @@ -1,8 +0,0 @@
{% extends "layout.jinja2" %}
{% block content %}
<div class="content">
<h1><span class="font-semi-bold">Pyramid</span> <span class="smaller">Alchemy project</span></h1>
<p class="lead">Welcome to <span class="font-normal">Ordr2</span>, a&nbsp;Pyramid application generated&nbsp;by<br><span class="font-normal">Cookiecutter</span>.</p>
</div>
{% endblock content %}

66
ordr2/templates/orders/delete.jinja2

@ -0,0 +1,66 @@ @@ -0,0 +1,66 @@
{% extends "ordr2:templates/layout.jinja2" %}
{% import 'ordr2:templates/macros.jinja2' as macros with context %}
{% block subtitle %} Orders | Confirm Delete {% endblock subtitle %}
{% block content %}
<div class="content controls">
<div class="container-fluid">
<div class="row-fluid">
<div class="page-controls">
<h1>Delete Order{{ 's' if orders|length > 1 }}</h1>
</div>
</div>
<div class="row">
<div class="span10">
<div class="action-header">
<h3>The following order{{ 's' if orders|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>Date Created</th>
<th>CAS / Description</th>
<th>Vendor</th>
<th>Placed by</th>
<th>Status</th>
</thead>
<tbody>
{% for order in orders %}
<tr>
<td>
<input type="hidden" name="order" value="{{ order.id }}">
{{ order.created_date.strftime('%Y-%m-%d %H:%M') }}
</td>
<td>{{ order.cas_description }} </td>
<td>{{ order.vendor }} </td>
<td>{{ order.created_by }} </td>
<td>{{ macros.colored_status(order.status) }} </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 Order{{ 's' if orders|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/orders/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 %} Order | {{ 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 Order: {{ context.model.cas_description }}</h1>
</div>
</div>
<div class="row edit-order">
<div class="span8">
{{ macros.flash_messages() }}
{{form.render()|safe}}
</div>
</div>
</div>
</div>
{% endblock content %}

77
ordr2/templates/orders/edit_multiple_stati.jinja2

@ -0,0 +1,77 @@ @@ -0,0 +1,77 @@
{% extends "ordr2:templates/layout.jinja2" %}
{% import 'ordr2:templates/macros.jinja2' as macros with context %}
{% block subtitle %} Oders | Change Status {% endblock subtitle %}
{% block content %}
<div class="content controls">
<div class="container-fluid">
<div class="row-fluid">
<div class="page-controls">
<h1>Change Status of Order{{ 's' if orders|length > 1 }}</h1>
</div>
</div>
<div class="row">
<div class="span10">
<div class="action-header">
<h3>The status of the following order{{ 's' if orders|length > 1 }} will be changed:</h3>
</div>
<form action="{{ request.resource_url(context, 'stati') }}" method="POST" class="action">
<input type="hidden" name="csrf_token" value="{{get_csrf_token()}}">
<table class="table">
<thead>
<th>Date Created</th>
<th>CAS / Description</th>
<th>Vendor</th>
<th>Placed by</th>
<th>Status</th>
</thead>
<tbody>
{% for order in orders %}
<tr>
<td>{{ order.created_date.strftime('%Y-%m-%d %H:%M') }}</td>
<td>{{ order.cas_description }} </td>
<td>{{ order.vendor }} </td>
<td>{{ order.created_by }} </td>
<td>
<select name="order-{{ order.id }}" class="select-status span2">
{% for value, display in stati %}
<option value="{{ value }}" {{ 'selected="selected"' if value == order.status.name }}>{{ display }}</option>
{% endfor %}
</select>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<fieldset class="form-actions">
<div class="right">
<button name="change" type="submit" value="change" class="btn btn-large btn-danger">Change {{ 'Status' if orders|length > 1 else 'Stati'}}</button>
<button name="cancel" type="submit" value="cancel" class="btn btn-large">Cancel</button>
</div>
<div class="btn-group quick-action left" data-action="order">
<a data-value="APPROVAL" href="#" class="btn btn-large btn-primary">Set all to Approval</a>
<a href="#" data-toggle="dropdown" class="btn btn-large btn-primary dropdown-toggle"><span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a data-value="ORDERED" href="#">Set all to Ordered</a></li>
<li><a data-value="COMPLETED" href="#">Set all to Completed</a></li>
</ul>
</div>
</fieldset>
</form>
</div>
</div>
</div>
</div>
{% endblock content %}

194
ordr2/templates/orders/list.jinja2

@ -0,0 +1,194 @@ @@ -0,0 +1,194 @@
{% extends "ordr2:templates/layout.jinja2" %}
{% import 'ordr2:templates/macros.jinja2' as macros with context %}
{% block subtitle %} Orders {% 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>
Orders
</h1>
</div>
{{ 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">
<form action="{{ request.resource_url(context, 'actions', query=context.query_params()) }}" method="POST">
<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" value="{{ context.filters.get('search', None) or '' }}">
<label class="add-on">
<button type="submit" class="search" name="action" value="search">Search</button>
</label>
</div>
<div class="actions">
{% if request.has_permission('create', context) %}
<a href="{{ request.resource_url(context, 'splash') }}" rel="tooltip" data-original-title="New" class="btn-flat single"><i class="add"></i></a>
{% endif %}
{% if request.has_permission('edit', context) or request.has_permission('delete', context) %}
<div class="btn-group marking-needed">
{% if request.has_permission('edit', context) %}
<button rel="tooltip" data-original-title="Change Status" value="status" type="submit" name="action" class="btn-flat"><i class="clip"></i></button>
{% endif %}
{% if request.has_permission('delete', context) %}
<button rel="tooltip" data-original-title="Delete" value="delete" type="submit" name="action" class="btn-flat"><i class="trash"></i></button>
{% endif %}
</div>
{% endif %}
<a href="#modal-display" rel="tooltip" data-original-title="Display Options" class="btn-flat single" data-toggle="modal"><i class="eye"></i></a>
<button rel="tooltip" data-original-title="Download View" value="export" type="submit" name="action" class="btn-flat"><i class="download"></i></button>
</div>
</div>
{{ macros.flash_messages() }}
{% if orders %}
{{ macros.show_or_hide_columns('orders') }}
<table class="table">
<thead>
{% if request.has_permission('edit', context) or request.has_permission('delete', context) %}
<th class="center">
<input type="checkbox" value="all" name="mark_all" id="mark_all">
</th>
{% endif %}
{{ macros.sortable_table_header('Date Created', 'created') }}
{{ macros.sortable_table_header('CAS / Description', 'cas') }}
{{ macros.sortable_table_header('Vendor', 'vendor') }}
{{ macros.sortable_table_header('Catalog Nr.', 'catalog') }}
{{ macros.sortable_table_header('Unit Price', 'price') }}
{{ macros.sortable_table_header('Quantity', 'amount') }}
{{ macros.sortable_table_header('Total Price', 'total') }}
{{ macros.sortable_table_header('Account', 'account') }}
{{ macros.sortable_table_header('Category', 'category') }}
{{ macros.sortable_table_header('Status', 'status') }}
{{ macros.sortable_table_header('Ordered By', 'user') }}
<th>Actions</th>
</thead>
<tbody>
{% for order in orders %}
<tr>
{% if request.has_permission('edit', context) or request.has_permission('delete', context) %}
<td class="center">
<input type="checkbox" name="marked" value="{{ order.model.id }}">
</td>
{% endif %}
<td class="column-created">{{ order.model.created_date.strftime('%Y-%m-%d %H:%M') }}</td>
<td class="column-cas">{{ order.model.cas_description }}</td>
<td class="column-vendor">{{ order.model.vendor }}</td>
<td class="column-catalog">{{ order.model.catalog_nr }}</td>
<td class="column-price">{{ '%.2f'|format(order.model.unit_price) }} {{ order.model.currency }}</td>
<td class="column-amount">{{ order.model.amount }}</td>
<td class="column-total">{{ '%.2f'|format(order.model.total_price) }} {{ order.model.currency }}</td>
<td class="column-account">{{ order.model.account }}</td>
<td class="column-category">{{ order.model.category.value|capitalize }}</td>
<td class="column-status">{{ macros.colored_status(order.model.status) }}</td>
<td class="column-user">
<a href="{{ request.resource_url(context, query={'user': order.model.created_by}) }}" title="click to view all orders from user">{{ order.model.created_by }}</a>
</td>
<td>
{% if request.has_permission('edit', order) %}
<a href="{{ request.resource_url(order, 'edit') }}" class="action edit" title="Edit Order">edit</a>
{% else %}
<a href="{{ request.resource_url(order) }}" class="action eye" title="View Order">edit</a>
{% endif %}
{% if request.has_permission('delete', order) %}
<a href="{{ request.resource_url(order, 'delete') }}" class="action delete" title="Delete Order">delete</a>
{% endif %}
</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 %}
</form>
</div>
</div>
</div>
<div id="modal-display" class="modal hide fade">
<form action="{{ request.resource_url(context, 'changeview', query=context.query_params()) }}" method="POST" class="checkslist">
<input type="hidden" name="csrf_token" value="{{get_csrf_token()}}">
{% set display = request.session.get('display', dict()) %}
{% set settings = display.get('orders', dict()) %}
<div class="modal-header">
<a href="#" class="close" data-dismiss="modal">×</a>
<h3>Display Options</h3>
</div>
<div class="modal-body">
<p class="help-block"><span class="label notice">Notice</span> If the displayed information is too cluttered, deselect some fields below. This will temporaly remove them from your view and should help you stay on top of things.</p>
<div class="checklist">
<fieldset class="left">
<label class="checkbox">
<input type="checkbox" value="created" name="display" checked="checked" disabled="disabled">
Date Created
</label>
<label class="checkbox">
<input type="checkbox" value="cas" name="display" {{ 'checked="checked"' if settings.get('cas') }}>
CAS / Description
</label>
<label class="checkbox">
<input type="checkbox" value="vendor" name="display" {{ 'checked="checked"' if settings.get('vendor') }}>
Vendor
</label>
<label class="checkbox">
<input type="checkbox" value="catalog" name="display" {{ 'checked="checked"' if settings.get('catalog') }}>
Catalog Number
</label>
<label class="checkbox">
<input type="checkbox" value="price" name="display" {{ 'checked="checked"' if settings.get('price') }}>
Unit Price
</label>
<label class="checkbox">
<input type="checkbox" value="amount" name="display" {{ 'checked="checked"' if settings.get('amount') }}>
Quantity
</label>
</fieldset>
<fieldset class="right">
<label class="checkbox">
<input type="checkbox" value="total" name="display" {{ 'checked="checked"' if settings.get('total') }}>
Total Price
</label>
<label class="checkbox">
<input type="checkbox" value="account" name="display" {{ 'checked="checked"' if settings.get('account') }}>
Account
</label>
<label class="checkbox">
<input type="checkbox" value="category" name="display" checked="checked" disabled="disabled">
Category
</label>
<label class="checkbox">
<input type="checkbox" value="status" name="display" checked="checked" disabled="disabled">
Work Status
</label>
<label class="checkbox">
<input type="checkbox" value="user" name="display" checked="checked" disabled="disabled">
Ordered by
</label>
</fieldset>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-primary" type="submit">Apply Changes</button>
<a data-dismiss="modal" class="btn" href="#">Close</a>
</div>
</form>
</div>
</div>
{% endblock content %}

24
ordr2/templates/orders/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 %} Order | Create New Order {% endblock subtitle %}
{% block content %}
<div class="content controls">
<div class="container-fluid">
<div class="row-fluid">
<div class="page-controls">
<h1>Create New Order</h1>
</div>
</div>
<div class="row edit-order">
<div class="span8">
{{ macros.flash_messages() }}
{{form.render()|safe}}
</div>
</div>
</div>
</div>
{% endblock content %}

69
ordr2/templates/orders/splash.jinja2

@ -0,0 +1,69 @@ @@ -0,0 +1,69 @@
{% extends "ordr2:templates/layout.jinja2" %}
{% import 'ordr2:templates/macros.jinja2' as macros with context %}
{% block subtitle %} Order | Place Order {% endblock subtitle %}
{% block content %}
<div class="content controls">
<div class="container-fluid">
<div class="page-controls">
<h1>Place an Order</h1>
</div>
<div class="row">
<div class="span10">
<div class="alert alert-block alert-info">
<h3 class="alert-heading">You can choose from a number of options to place a new order.</h3>
<ol>
<li>Place a custom order (empty, not prepopulated order form)</li>
<li>Use the search field to find a consumable and use a prepopulated order form.</li>
<li>Choose one of the consumables from a list to use a prepopulated order form.</li>
</ol>
</div>
{{ macros.flash_messages() }}
<div class="bordered">
<form action="{{ request.resource_url(context, 'splash')}}" method="POST" class="form-inline right" autocomplete="off">
<input type="hidden" name="csrf_token" value="{{get_csrf_token()}}">
<input type="search" data-provide="typeahead" data-source='{{ consumable_names|tojson }}' placeholder="Search Consumables" size="50" name="search">
<button class="btn" type="submit" name="search_consumable">Create Order</button>
</form>
<a class="btn" href="{{ request.resource_url(context, 'new') }}">Place a custom order</a>
</div>
<div id="common-consumables-accordion" class="accordion">
{% for category, items in consumables.items() %}
<div class="accordion-group">
<div class="accordion-heading">
<a href="#{{ category.name }}" data-parent="#common-consumables-accordion" data-toggle="collapse">
{{ category.value|capitalize }}
</a>
</div>
<div class="accordion-body collapse" id="{{ category.name }}">
<div class="accordion-inner">
<ul class="unstyled">
{% for item in items %}
<li><a href="{{ request.resource_url(context, 'new', query={'consumable': item.id}) }}">{{ item.cas_description }}</a> <span>({{ item.package_size }})</span></li>
{% endfor %}
</ul>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
{% endblock content %}

156
ordr2/templates/orders/view.jinja2

@ -0,0 +1,156 @@ @@ -0,0 +1,156 @@
{% extends "ordr2:templates/layout.jinja2" %}
{% import 'ordr2:templates/macros.jinja2' as macros with context %}
{% block subtitle %} Order | View | {{ context.model.cas_desctiption }} {% endblock subtitle %}
{% block content %}
<div class="content controls">
<div class="container-fluid">
<div class="row-fluid">
<div class="page-controls">
<h1>
View Order: {{ context.model.cas_desctiption }}
</h1>
</div>
</div>
<div class="row edit-order">
<div class="span8 form-horizontal form-like-display">
<div class="controls">
<div class="panel panel-default">
<div class="panel-heading">
Order Information
</div>
<div class="panel-body">
<div class="control-group">
<label class="control-label"> Status </label>
<div class="controls">
{{ macros.colored_status(context.model.status) }}
</div>
</div>
<div class="control-group">
<label class="control-label"> Placed </label>
<div class="controls">
<p class="form-control-static">{{ context.model.placed }}</p>
</div>
</div>
<div class="control-group">
<label class="control-label"> Approved </label>
<div class="controls">
<p class="form-control-static">{{ context.model.approved }}</p>
</div>
</div>
<div class="control-group">
<label class="control-label"> Ordered </label>
<div class="controls">
<p class="form-control-static">{{ context.model.ordered }}</p>
</div>
</div>
<div class="control-group">
<label class="control-label"> Completed </label>
<div class="controls">
<p class="form-control-static">{{ context.model.completed }}</p>
</div>
</div>
</div>
</div>
</div>
<div class="controls">
<div class="panel panel-default">
<div class="panel-heading">
Item Information
</div>
<div class="panel-body">
<div class="control-group" >
<label class="control-label"> Cas Description </label>
<div class="controls">
<p class="form-control-static">{{ context.model.cas_description }}</p>
</div>
</div>
<div class="control-group" >
<label class="control-label"> Category </label>
<div class="controls">
<p class="form-control-static">{{ context.model.category.value|capitalize }}</p>
</div>
</div>
<div class="control-group" >
<label class="control-label"> Catalog Nr </label>
<div class="controls">
<p class="form-control-static">{{ context.model.catalog_nr }}</p>
</div>
</div>
<div class="control-group" >
<label class="control-label"> Vendor </label>
<div class="controls">
<p class="form-control-static">{{ context.model.vendor }}</p>
</div>
</div>
<div class="control-group" >
<label class="control-label"> Package Size </label>
<div class="controls">
<p class="form-control-static">{{ context.model.package_size }}</p>
</div>
</div>
</div>
</div>
</div>
<div class="controls">
<div class="panel panel-default">
<div class="panel-heading">
Pricing
</div>
<div class="panel-body">
<div class="control-group" >
<label class="control-label"> Unit Price </label>
<div class="controls">
<p class="form-control-static">{{ context.model.unit_price }} {{ context.model.currency }}</p>
</div>
</div>
<div class="control-group" >
<label class="control-label"> Quantity </label>
<div class="controls">
<p class="form-control-static">{{ context.model.amount }}</p>
</div>
</div>
<div class="control-group" >
<label class="control-label"> Total Price </label>
<div class="controls">
<p class="form-control-static">{{ context.model.total_price }} {{ context.model.currency }}</p>
</div>
</div>
</div>
</div>
</div>
<div class="controls">
<div class="panel panel-default">
<div class="panel-heading">
Optional Information
</div>
<div class="panel-body">
<div class="control-group" >
<label class="control-label "> Account </label>
<div class="controls">
<p class="form-control-static">{{ context.model.account }}</p>
</div>
</div>
<div class="control-group" >
<label class="control-label "> Comment </label>
<div class="controls">
<p class="form-control-static">{{ context.model.comment.replace('\n', '<br>') }}</p>
</div>
</div>
</div>
</div>
</div>
<div class="form-actions deform-form-buttons">
<a href="{{ request.resource_url(context.__parent__, 'new', query={'reorder':context.model.id}) }}" class="btn btn-success"> Reorder </a>
<a href="{{ context.__parent__.url() }}" class="btn btn-default"> cancel </a>
</div>
</div>
</div>
</div>
</div>
{% endblock content %}

76
ordr2/templates/pages/faq.jinja2

@ -0,0 +1,76 @@ @@ -0,0 +1,76 @@
{% extends "ordr2:templates/layout.jinja2" %}
{% block subtitle %} FAQ {% endblock subtitle %}
{% block content %}
<div class="content">
<div class="container">
<h1>Frequently Asked Questions</h1>
<section>
<h2>Account</h2>
<div class="well">
<h3>
Why can't I log in? It always says: &quot;You entered the wrong unsername and/or password.&quot;
</h3>
<p>
First of all, just because <em>ordr</em> tells you that you have entered a wrong username or password doesn't mean that's necesseraily true. Sound dumb? But actually this is a security measure, which helps against brood force attacks because the adversary doesn't know if the username he guessed was correct or not.
</p>
<p>
Usually there are two common reasons why you can't log in. There are:
</p>
<ol>
<li><em>Your account hasn't been activated by an admin yet.</em> &ndash; In this case ask a admin if he already has activated your account.</li>
<li><em>You mistyped your password or username.</em> &ndash; Just try it another time and make sure caps log isn't activated.</li>
</ol>
</div>
<div class="well">
<h3>
Why can't I choose my own username?
</h3>
<p>
We want to enforce a specific guideline for the username. Namely <em>first name</em> followed immediately by the <em>last name</em>. This way everyone can easily identify the person, who placed an order. So please enter your real name into the registration form. Otherwise your account may not been activated because no one knows who you are.
</p>
</div>
<div class="well">
<h3>
Why can I only edit my email after I registered?
</h3>
<p>
Your username depends on your first and last name. Changing the username is not allowed, therefore you are also not allowed to change your first and last name afterwards. Even admins can not change your first/last name and your username.
</p>
</div>
</section>
<section>
<h2>Ordering</h2>
<div class="well">
<h3>
I have an consumable, which I am ordering regulary but isn't in the databse. Can I add it?
</h3>
<p>
Unfortunately you can not add consumables to the databse yourself. But ask an admin or purchaser. They're certainly to help you out.
</p>
</div>
</section>
<section>
<h2>Miscellaneous</h2>
<div class="well">
<h3>
I have found a bug where can I report it?
</h3>
<p>
If you have found a bug or an issue with the software please use <a href="https://git.cpi.imtek.uni-freiburg.de/holgi/ordr2/issues">this page here</a>.
You need an <a href="https://wiki.cpi.imtek.uni-freiburg.de/CPIvServerDocumentation/GogsGitServer">gogs</a> account to submit an issue. Please be as specific as you can be.
</p>
</div>
</section>
</div>
</div>
{% endblock content %}

42
ordr2/templates/pages/welcome.jinja2

@ -0,0 +1,42 @@ @@ -0,0 +1,42 @@
{% extends "ordr2:templates/layout.jinja2" %}
{% block subtitle %} Welcome {% endblock subtitle %}
{% block content %}
<div class="content">
<div class="container">
<div class="row">
<div class="span12">
<hgroup id="welcome">
<h1>Welcome to <span class="brand">ordr</span>!</h1>
<p class="quote">An order management system to simplify your shopping for laboratory supplies.</p>
</hgroup>
</div>
</div>
<div class="row">
<div class="span12">
<p>
<strong>What can order do for you?</strong> It will simplify the process of ordering laboratory supplies by using the power of the newest web technologies. Interested? Just follow the three steps below!
</p>
</div>
</div>
<div class="row">
<div class="span4">
<h2>1. Register</h2>
<p>Registration is easy as 1-2-3. Just fill out the form on <?php echo anchor('account/register', 'this page');?> and as soon as an admin has activated your account the shopping can begin!</p>
</div>
<div class="span4">
<h2>2. Place an Order</h2>
<p>A lot of the chemicals, supllies and so forth are already stored in the database, so you don't have to fill out the order form your self!</p>
</div>
<div class="span4">
<h2>3. Get notified</h2>
<p>As soons as your purchase has arrived you will automatically get notified. Or you can use the orders overview to check what the working status is.</p>
</div>
</div>
</div>
</div>
{% endblock content %}

7
ordr2/templates/tests.py

@ -0,0 +1,7 @@ @@ -0,0 +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())

64
ordr2/views/__init__.py

@ -1 +1,63 @@ @@ -1 +1,63 @@
# package
''' views package
some view helpers are defined here
'''
from collections import namedtuple
# a message for session.flash()
FlashMessage = namedtuple('FlashMessage', 'message description dismissable')
def flash(request, channel, message, description='', dismissable=True):
''' small wrapper around request.session.flash '''
msg = FlashMessage(message, description, dismissable)
request.session.flash(msg, channel, allow_duplicate=False)
def update_column_display(request, section):
''' update the session values for which columns to display '''
if section not in request.session['display']:
return
display_keys = request.session['display'][section].keys()
display = dict.fromkeys(display_keys, False)
for column in request.POST.values():
if column in display:
display[column] = True
request.session['display'][section] = display
def set_display_defaults(request):
''' sets the coulumn display default '''
defaults = {
'users': {
'first': True,
'last': True,
'email': True,
},
'orders': {
'account': False,
'cas': True,
'catalog': False,
'vendor': True,
'price': False,
'amount': False,
'total': True,
}
}
request.session['display'] = defaults
def includeme(config):
''' adding request helpers and static views
Activate this setup using ``config.include('ordr2.views')``.
'''
config.add_request_method(flash, 'flash')
settings = config.get_settings()
age = int(settings.get('static_views.cache_max_age', 3600))
config.add_static_view('static', 'ordr2:static', cache_max_age=age)
config.add_static_view('deform', 'deform:static', cache_max_age=age)

273
ordr2/views/account.py

@ -0,0 +1,273 @@ @@ -0,0 +1,273 @@
''' Account Registration and Settings '''
import deform
from pyramid.httpexceptions import HTTPFound
from pyramid.renderers import render
from pyramid.security import remember, forget
from pyramid.view import view_config
from ordr2.events import UserLogIn
from ordr2.models import User, Role
from ordr2.schemas.account import (
ResetPasswordSchema,
RegistrationSchema,
SettingsSchema
)
# below this password length a warning is displayed
MIN_PW_LENGTH = 12
# user log in and log out
@view_config(
context='ordr2:resources.Account',
name='login',
permission='login',
request_method='GET',
renderer='ordr2:templates/account/login.jinja2'
)
def login_form(context, request):
''' display a login form '''
return {}
@view_config(
context='ordr2:resources.Account',
name='login',
permission='login',
request_method='POST'
)
def login(context, request):
''' loging in a user '''
username = request.POST.get('username')
password = request.POST.get('password')
# Form validation is not done for login forms,
# either the data represents a user or not.
user = request.dbsession.query(User).filter_by(user_name=username).first()
if user is not None:
if user.is_active and user.check_password(password):
headers = remember(request, user.id)
event = UserLogIn(request, user)
request.registry.notify(event)
return HTTPFound(
request.resource_path(request.root, 'orders'),
headers=headers
)
request.flash(
'error',
'Oh snap! You entered the wrong unsername and/or password.',
'''Please try it again. If you still can not log in make sure that
your account is activated and you haven't enabled caps lock on
your keyboard.''',
dismissable=False
)
return HTTPFound(request.resource_path(context, 'login'))
@view_config(
context='ordr2:resources.Account',
name='logout',
permission='logout'
)
def logout(context, request):
''' logout of a user '''
if request.user:
pass
# request.session.flash(MSG_LOGOUT, 'success')
headers = forget(request)
return HTTPFound(
request.resource_path(request.root, 'about'),
headers=headers
)
# user registration
@view_config(
context='ordr2:resources.Account',
name='register',
permission='register',
request_method='GET',
renderer='ordr2:templates/account/register.jinja2'
)
def registration_form(context, request):
''' display a registration form '''
context.nav_highlight = 'register'
form = RegistrationSchema.as_form(request)
return {'form': form}
@view_config(
context='ordr2:resources.Account',
name='register',
permission='register',
request_method='POST',
renderer='ordr2:templates/account/register.jinja2'
)
def registration_form_processing(context, request):
''' process a submitted registration form '''
if 'Cancel' in request.POST:
return HTTPFound(request.resource_path(request.root))
form = RegistrationSchema.as_form(request)
data = request.POST.items()
try:
appstruct = form.validate(data)
except deform.ValidationFailure as e:
context.nav_highlight = 'register'
return {'form': form}
# form validation successfull, create user
account = User(
user_name=appstruct['user_name'],
first_name=appstruct['first_name'],
last_name=appstruct['last_name'],
email=appstruct['email'],
role=Role.NEW
)
account.set_password(appstruct['password'])
request.dbsession.add(account)
request.flash(
'success',
'Your account <em>{}</em> has been created.'.format(account.user_name),
dismissable=False
)
if len(appstruct['password']) < MIN_PW_LENGTH:
request.flash(
'warning',
'You should really consider using a longer password.',
dismissable=False
)
return HTTPFound(request.resource_path(context, 'registered'))
@view_config(
context='ordr2:resources.Account',
name='registered',
permission='register',
renderer='ordr2:templates/account/register_sucessful.jinja2'
)
def registration_sucessful(context, request):
''' registration was sucessfull '''
return {}
# user settings
@view_config(
context='ordr2:resources.Account',
name='settings',
permission='settings',
request_method='GET',
renderer='ordr2:templates/account/settings.jinja2'
)
def settings_form(context, request):
''' display the user settings form '''
form = SettingsSchema.as_form(request)
form_data = {
'general': {
'user_name': request.user.user_name,
'first_name': request.user.first_name,
'last_name': request.user.last_name,
'email': request.user.email,
'role': request.user.role.value.capitalize()
}
}
form.set_appstruct(form_data)
return {'form': form}
@view_config(
context='ordr2:resources.Account',
name='settings',
permission='settings',
request_method='POST',
renderer='ordr2:templates/account/settings.jinja2'
)
def settings_form_processing(context, request):
''' process the user settings form '''
if 'Cancel' in request.POST:
return HTTPFound(request.resource_url(request.root))
form = SettingsSchema.as_form(request)
data = request.POST.items()
try:
appstruct = form.validate(data)
except deform.ValidationFailure as e:
return {'form': form}
# form validation sucessful, change settings
request.user.first_name = appstruct['general']['first_name']
request.user.last_name = appstruct['general']['last_name']
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']) < MIN_PW_LENGTH:
request.flash(
'warning',
'You should really consider using a longer password.'
)
request.flash('success', 'Your account information has been updated.')
return {'form': form}
# passwort reset links
@view_config(
context='ordr2:resources.PasswordResetAccount',
permission='reset',
request_method='GET',
renderer='ordr2:templates/account/password_reset.jinja2'
)
def reset_password_form(context, request):
''' display the password reset form '''
form = ResetPasswordSchema.as_form(request)
return {'form': form}
@view_config(
context='ordr2:resources.PasswordResetAccount',
permission='reset',
request_method='POST',
renderer='ordr2:templates/account/password_reset.jinja2'
)
def reset_password_form_processing(context, request):
''' process the password reset form '''
if 'Cancel' in request.POST:
return HTTPFound(request.resource_url(request.root))
form = ResetPasswordSchema.as_form(request)
data = request.POST.items()
try:
appstruct = form.validate(data)
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 = ''
request.flash(
'success',
'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'))

469
ordr2/views/admin.py

@ -0,0 +1,469 @@ @@ -0,0 +1,469 @@
''' views for the admin section '''
import deform
from pyramid.httpexceptions import HTTPFound
from pyramid.renderers import render
from pyramid.security import remember, forget
from pyramid.view import view_config
from ordr2.events import AccountActivation, PasswordReset
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
# admin section
@view_config(
context='ordr2:resources.Admin',
permission='view',
renderer='ordr2:templates/admin/admin_section.jinja2'
)
def admin_section(context, request):
''' display the admin section '''
new_users = request.dbsession.query(User).filter_by(role=Role.NEW).count()
if new_users:
plural = 's' if new_users > 1 else ''
request.flash(
'info',
'{} new user{} have registered.'.format(new_users, plural),
'''Please <a href="{}">take a look at them</a> and confirm or
reject the registration by setting the role accordingly. This
message will disappear when all new registrations have been
processed.'''.format(
request.resource_url(context, 'users', query={'role': 'new'})
)
)
return {}
# user list
@view_config(
context='ordr2:resources.UserList',
permission='view',
renderer='ordr2:templates/admin/user_list.jinja2'
)
def user_list(context, request):
''' display the user list '''
users = context.items()
roles = [(role.value.lower(), role.value.capitalize()) for role in Role]
return {'users':users, 'roles':roles}
@view_config(
context='ordr2:resources.UserList',
name = 'changeview',
permission='view',
request_method='POST'
)
def change_column_view(context, request):
''' changes the columns to display '''
update_column_display(request, 'users')
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):
''' process the search user form '''
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',
request_param='action=delete',
permission='delete',
request_method='POST',
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).\
filter(User.id.in_(account_ids)).\
order_by(User.user_name).\
all()
if len(accounts) == 0:
return HTTPFound(context.url())
return {'accounts': accounts}
@view_config(
context='ordr2:resources.UserList',
name='actions',
request_param='action=role',
permission='edit',
request_method='POST',
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).\
filter(User.id.in_(account_ids)).\
order_by(User.user_name).\
all()
if len(accounts) == 0:
return HTTPFound(context.url())
roles = [(role.name, role.value.capitalize()) for role in Role]
return {'accounts': accounts, 'roles': roles}
@view_config(
context='ordr2:resources.UserList',
name='roles',
permission='edit',
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
for key, value in request.POST.items():
if not key.startswith('account-'):
continue
_, account_id = key.split('-', 1)
account = request.dbsession.query(User).get(account_id)
if account:
was_active = account.is_active
try:
account.role = Role[value]
except ValueError:
pass
if not was_active and account.is_active:
# user account was activated, notify user
event = AccountActivation(request, account)
request.registry.notify(event)
count += 1
if count == 1:
request.flash('success', 'One user account was updated')
elif count > 1:
msg = '{} user accounts were updated.'.format(count)
request.flash('success', msg)
return HTTPFound(context.url())
# editing one user account
@view_config(
context='ordr2:resources.UserAccount',
permission='edit',
request_method='GET',
renderer='ordr2:templates/admin/user_edit.jinja2'
)
def user_account_form(context, request):
''' display the user edit form '''
form = UserSchema.as_form(request)
form_data = {
'user_name': context.model.user_name,
'first_name': context.model.first_name,
'last_name': context.model.last_name,
'email': context.model.email,
'role': context.model.role.name
}
form.set_appstruct(form_data)
return {'form': form}
@view_config(
context='ordr2:resources.UserAccount',
permission='edit',
request_method='POST',
renderer='ordr2:templates/admin/user_edit.jinja2'
)
def user_account_form_processing(context, request):
''' process the user edit form '''
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)
except deform.ValidationFailure as e:
return {'form': form}
# form validation sucessful, change settings
was_active = context.model.is_active
context.model.first_name = appstruct['first_name']
context.model.last_name = appstruct['last_name']
context.model.email = appstruct['email']
context.model.role = Role[appstruct['role']]
if not was_active and context.model.is_active:
# user account was activated, notify user
event = AccountActivation(request, context.model)
request.registry.notify(event)
text = 'An activation email was sent to <em>{}</em>'.format(
appstruct['email']
)
else:
text = ''
msg = 'User account <em>{}</em> updated.'.format(
context.model.user_name
)
request.flash('success', msg, text)
return HTTPFound(context.__parent__.url())
@view_config(
context='ordr2:resources.UserAccount',
name='delete',
permission='delete',
request_method='GET',
renderer='ordr2:templates/admin/users_delete.jinja2'
)
def user_delete_form(context, request):
''' delete user confirmation page '''
return {'accounts': [context.model]}
@view_config(
context='ordr2:resources.UserList',
name='delete',
permission='delete',
request_method='POST'
)
@view_config(
context='ordr2:resources.UserAccount',
name='delete',
permission='delete',
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.\
query(User).\
filter(User.id.in_(account_ids)).\
all()
for account in accounts:
request.dbsession.delete(account)
if len(accounts) == 1:
request.flash('success', 'One user account was deleted')
elif len(accounts) > 1:
msg = '{} user accounts were deleted.'.format(len(accounts))
request.flash('success', msg)
return HTTPFound(request.resource_url(request.root, 'admin', 'users'))
# consumables list
@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='actions',
request_param='action=search',
permission='view',
request_method='POST'
)
def consumable_search(context, request):
''' process the search consumable form '''
term = request.POST.get('search', '')
term = term.strip()
if term:
return HTTPFound(context.url(search=term, category=None, p=1))
return HTTPFound(context.url())
# adding a consumable
@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']['amount'],
currency=appstruct['unit_price']['currency'],
comment=appstruct['comment']
)
request.dbsession.add(consumable)
msg = 'Consumable <em>{!s}</em> added.'.format(consumable)
request.flash('success', msg)
return HTTPFound(context.url())
# edit a consumable
@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': {
'amount': 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']['amount']
context.model.currency = appstruct['unit_price']['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:
# 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',
permission='delete',
request_method='GET',
renderer='ordr2:templates/admin/consumable_delete.jinja2'
)
def consumable_delete_form(context, request):
''' delete consumable confirmation page '''
return {'consumables': [context.model]}
@view_config(
context='ordr2:resources.ConsumableResource',
name='delete',
permission='delete',
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.\
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())

33
ordr2/views/default.py

@ -1,33 +0,0 @@ @@ -1,33 +0,0 @@
from pyramid.response import Response
from pyramid.view import view_config
from sqlalchemy.exc import DBAPIError
from ..models import MyModel
@view_config(context='ordr2.resources.Root', renderer='../templates/mytemplate.jinja2')
def my_view(context, request):
try:
query = request.dbsession.query(MyModel)
one = query.filter(MyModel.name == 'one').first()
except DBAPIError:
return Response(db_err_msg, content_type='text/plain', status=500)
return {'one': one, 'project': 'Ordr2'}
db_err_msg = '''\
Pyramid is having a problem using your SQL database. The problem
might be caused by one of the following things:
1. You may need to run the 'initialize_ordr2_db' script
to initialize your database tables. Check your virtual
environment's 'bin' directory for this script and try to run it.
2. Your database server may not be running. Check that the
database server referred to by the 'sqlalchemy.url' setting in
your 'development.ini' file is running.
After you fix the problem, please restart the Pyramid application to
try it again.
'''

78
ordr2/views/errors.py

@ -0,0 +1,78 @@ @@ -0,0 +1,78 @@
''' display error pages '''
import io
import pprint
import traceback
import sys
from pyramid.exceptions import BadCSRFToken
from pyramid.view import (
notfound_view_config,
forbidden_view_config,
view_config
)
from pyramid_mailer.message import Message
@notfound_view_config(renderer='ordr2:templates/errors/not_found.jinja2')
def notfound_view(context, request):
context.nav_highlight = 'errors'
request.response.status = 404
return {}
@forbidden_view_config(renderer='ordr2:templates/errors/forbidden.jinja2')
def forbidden_view(context, request):
context.nav_highlight = 'errors'
request.response.status = 403
return {}
@view_config(
context=BadCSRFToken,
renderer='ordr2:templates/errors/bad_csrf_token.jinja2'
)
def bad_csrf_view(context, request):
context.nav_highlight = 'errors'
request.response.status = 400
return {}
@view_config(
context=Exception,
renderer='ordr2:templates/errors/exception.jinja2'
)
def exception_view(context, request):
exc_type, exc_value, exc_traceback = sys.exc_info()
traceback_output = io.StringIO()
traceback.print_exc(file=traceback_output)
request_output = io.StringIO()
pprint.pprint(request.__dict__, stream=request_output)
settings = request.registry.settings
default_sender = settings['mail.default_sender']
recipient = settings.get('admin_email', None) or default_sender
body = '\n'.join([
traceback_output.getvalue(),
'',
'',
'Request:',
request_output.getvalue()
])
message = Message(
subject='[ordr] Exception occured',
sender=default_sender,
recipients=[recipient],
body=body
)
request.mailer.send_immediately(message)
context.nav_highlight = 'errors'
request.response.status = 500
return {}

7
ordr2/views/notfound.py

@ -1,7 +0,0 @@ @@ -1,7 +0,0 @@
from pyramid.view import notfound_view_config
@notfound_view_config(renderer='../templates/404.jinja2')
def notfound_view(context, request):
request.response.status = 404
return {}

579
ordr2/views/orders.py

@ -0,0 +1,579 @@ @@ -0,0 +1,579 @@
''' views for creating and editing orders '''
import deform
import io
import xlsxwriter
from collections import OrderedDict
from datetime import datetime
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
from ordr2.models import Category, Consumable, Order, OrderStatus, User
from ordr2.schemas.orders import NewOrderSchema, EditOrderSchema
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 order.status == OrderStatus.APPROVAL and order.status != old:
order.approval_date = datetime.utcnow()
order.approval_by = request.user.user_name
elif order.status == OrderStatus.ORDERED and order.status != old:
noteworthy = True
order.ordered_date = datetime.utcnow()
order.ordered_by = request.user.user_name
elif order.status == OrderStatus.COMPLETED and order.status != old:
noteworthy = True
order.completed_date = datetime.utcnow()
order.completed_by = request.user.user_name
if noteworthy and order.created_by != request.user.user_name:
# only notify if the user who changed the order is not the one who
# created it in the first place
account = request.dbsession.\
query(User).\
filter_by(user_name=order.created_by).\
first()
if account:
event = OrderStatusChange(request, account, order)
request.registry.notify(event)
@view_config(
context='ordr2:resources.OrderList',
name='view',
permission='view'
)
def old_list_redirect(context, request):
''' redirect the old /orders/view/ path to /orders/ '''
return HTTPFound(context.url())
# oder list
@view_config(
context='ordr2:resources.OrderList',
permission='view',
renderer='ordr2:templates/orders/list.jinja2'
)
def order_list(context, request):
''' display the order list '''
orders = context.items()
stati = [(s.value.lower(), s.value.capitalize()) for s in OrderStatus]
return {'orders':orders, 'stati':stati}
@view_config(
context='ordr2:resources.OrderList',
name = 'changeview',
permission='view',
request_method='POST'
)
def change_column_view(context, request):
''' changes the columns to display '''
update_column_display(request, 'orders')
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):
''' 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
# 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):
''' process the search order form '''
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',
request_param='action=status',
permission='edit',
request_method='POST',
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).\
filter(Order.id.in_(order_ids)).\
order_by(Order.created_date).\
all()
if len(orders) == 0:
return HTTPFound(context.url())
stati = [(s.name, s.value.capitalize()) for s in OrderStatus]
return {'orders': orders, 'stati': stati}
@view_config(
context='ordr2:resources.OrderList',
name='stati',
permission='edit',
request_method='POST'
)
def edit_multiple_stati_form_processing(context, request):
''' change the stati of multiple orders '''
if 'change' in request.POST:
count = 0
for key, value in request.POST.items():
if not key.startswith('order-'):
continue
_, order_id = key.split('-', 1)
order = request.dbsession.query(Order).get(order_id)
if order:
old_status = order.status
try:
order.status = OrderStatus[value]
except ValueError:
pass
if old_status != order.status:
change_in_order_status(request, order, old_status)
count += 1
if count == 1:
request.flash('success', 'One order was updated')
elif count > 1:
msg = '{} orders were updated.'.format(count)
request.flash('success', msg)
return HTTPFound(context.url())
@view_config(
context='ordr2:resources.OrderList',
name='actions',
request_param='action=delete',
permission='delete',
request_method='POST',
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).\
filter(Order.id.in_(order_ids)).\
order_by(Order.created_date).\
all()
if len(orders) == 0:
return HTTPFound(context.url())
return {'orders': orders}
@view_config(
context='ordr2:resources.OrderList',
name='delete',
permission='delete',
request_method='POST'
)
@view_config(
context='ordr2:resources.OrderResource',
name='delete',
permission='delete',
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.\
query(Order).\
filter(Order.id.in_(order_ids)).\
all()
for order in orders:
request.dbsession.delete(order)
if len(orders) == 1:
request.flash('success', 'One order was deleted')
elif len(orders) > 1:
msg = '{} orders were deleted.'.format(len(orders))
request.flash('success', msg)
return HTTPFound(request.resource_url(request.root, 'orders'))
# Single Order views
@view_config(
context='ordr2:resources.OrderResource',
permission='view',
request_method='GET',
renderer='ordr2:templates/orders/view.jinja2'
)
def order_view(context, request):
''' show the order information '''
return {}
@view_config(
context='ordr2:resources.OrderResource',
name='delete',
permission='delete',
request_method='GET',
renderer='ordr2:templates/orders/delete.jinja2'
)
def order_delete_form(context, request):
''' show the confirmation page for deleting one order '''
return {'orders': [context.model]}
@view_config(
context='ordr2:resources.OrderResource',
name='edit',
permission='edit',
request_method='GET',
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 = {
'status': order.status.name
}
item = {
'cas_description': order.cas_description,
'category': order.category.name,
'vendor': order.vendor,
'catalog_nr': order.catalog_nr,
'package_size': order.package_size
}
pricing = {
'unit_price': {
'amount': '%.2f' % order.unit_price,
'currency': order.currency
},
'quantity': order.amount,
'total_price': {
'amount': '%.2f' % order.total_price,
'currency': order.currency
},
}
optional = {
'account': order.account,
'comment': order.comment
}
form_data = {
'order_information': info,
'item_information': item,
'pricing': pricing,
'optional_information': optional
}
form.set_appstruct(form_data)
return {'form': form}
@view_config(
context='ordr2:resources.OrderResource',
name='edit',
permission='edit',
request_method='POST',
renderer='ordr2:templates/orders/edit.jinja2'
)
def order_edit_form_processing(context, request):
''' process the edit order form '''
form = EditOrderSchema.as_form(request)
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 order
order = context.model
info = appstruct['order_information']
item = appstruct['item_information']
pricing = appstruct['pricing']
optional = appstruct['optional_information']
old_status = order.status
order.status = OrderStatus[info['status']]
order.cas_description = item['cas_description']
order.category = item['category']
order.vendor = item['vendor']
order.catalog_nr = item['catalog_nr']
order.package_size = item['package_size']
order.unit_price = pricing['unit_price']['amount']
order.currency = pricing['unit_price']['currency']
order.amount = pricing['quantity']
order.total_price = order.unit_price * order.amount
order.account = optional['account']
order.comment = optional['comment']
if old_status != order.status:
change_in_order_status(request, order, old_status)
msg = 'Order <em>{!s}</em> updated.'.format(context.model)
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__,
'new',
query={'reorder': context.model.id}
)
)
return HTTPFound(context.__parent__.url())
@view_config(
context='ordr2:resources.OrderList',
name='new',
permission='create',
request_method='GET',
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
if order_id:
prefill = request.dbsession.query(Order).get(order_id)
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 ''
item = {
'cas_description': prefill.cas_description,
'category': prefill.category.name,
'vendor': prefill.vendor,
'catalog_nr': prefill.catalog_nr,
'package_size': prefill.package_size
}
pricing = {
'unit_price': {
'amount': '%.2f' % prefill.unit_price,
'currency': prefill.currency
},
'quantity': quantity,
'total_price': {
'amount': '%.2f' % total_price,
'currency': prefill.currency
},
}
optional = {
'account': account,
'comment': prefill.comment
}
form_data = {
'item_information': item,
'pricing': pricing,
'optional_information': optional
}
form.set_appstruct(form_data)
return {'form': form}
@view_config(
context='ordr2:resources.OrderList',
name='splash',
permission='create',
request_method='GET',
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] = []
available = request.dbsession.\
query(Consumable).\
order_by(Consumable.cas_description).\
all()
names = []
for consumable in available:
names.append(consumable.cas_description)
structured[consumable.category].append(consumable)
return {'consumable_names': names, 'consumables': structured}
@view_config(
context='ordr2:resources.OrderList',
name='splash',
permission='create',
request_method='POST',
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).\
filter_by(cas_description=name).\
first()
if consumable:
return HTTPFound(
request.resource_url(
context,
'new',
query={'consumable': consumable.id}
)
)
request.flash('error', 'No consumable selected')
return HTTPFound(request.resource_url(context, 'splash'))
@view_config(
context='ordr2:resources.OrderList',
name='new',
permission='create',
request_method='POST',
renderer='ordr2:templates/orders/new.jinja2'
)
def order_new_form_processing(context, request):
''' process the new order form '''
form = NewOrderSchema.as_form(request)
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 order
order = Order(
status=OrderStatus.OPEN,
created_by=request.user.user_name
)
item = appstruct['item_information']
pricing = appstruct['pricing']
optional = appstruct['optional_information']
order.cas_description = item['cas_description']
order.category = item['category']
order.vendor = item['vendor']
order.catalog_nr = item['catalog_nr']
order.package_size = item['package_size']
order.unit_price = pricing['unit_price']['amount']
order.currency = pricing['unit_price']['currency']
order.amount = pricing['quantity']
order.total_price = order.unit_price * order.amount
order.account = optional['account']
order.comment = optional['comment']
request.dbsession.add(order)
msg = 'Order <em>{!s}</em> created.'.format(context.model)
request.flash('success', msg)
return HTTPFound(context.url())

36
ordr2/views/pages.py

@ -0,0 +1,36 @@ @@ -0,0 +1,36 @@
''' views for static pages '''
from pyramid.httpexceptions import HTTPFound
from pyramid.view import view_config
@view_config(
context='ordr2.resources.Root',
permission='view'
)
def welcome(context, request):
next = 'orders' if request.user else 'about'
redirect_to = request.resource_url(context, next)
return HTTPFound(redirect_to)
@view_config(
context='ordr2.resources.Root',
name='about',
permission='view',
renderer='ordr2:templates/pages/welcome.jinja2'
)
def about(context, request):
context.nav_highlight = 'about'
return {}
@view_config(
context='ordr2.resources.Root',
name='faq',
permission='view',
renderer='ordr2:templates/pages/faq.jinja2'
)
def faqs(context, request):
context.nav_highlight = 'faq'
return {}

20
production.ini → production.ini.template

@ -11,9 +11,29 @@ pyramid.debug_authorization = false @@ -11,9 +11,29 @@ pyramid.debug_authorization = false
pyramid.debug_notfound = false
pyramid.debug_routematch = false
pyramid.default_locale_name = en
pyramid.includes =
pyramid_mailer
sqlalchemy.url = sqlite:///%(here)s/ordr2.sqlite
# email delivery
mail.host = localhost
mail.port = 2525
mail.default_sender = ordr@example.com
# custom settings
auth.secret = 'Change Me 1'
session.secret = 'Change Me 2'
session.auto_csrf = true
static_views.cache_max_age = 0
# custom jinja filters and tests
jinja2.filters =
are_extras_active = ordr2.templates.tests:are_extras_active
###
# wsgi server configuration
###

4
setup.cfg

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
[bumpversion]
current_version = 0.0.1
current_version = 0.1.4
commit = True
tag = True
@ -19,4 +19,4 @@ exclude = docs @@ -19,4 +19,4 @@ exclude = docs
[aliases]
test = pytest
# Define setup.py command aliases here

Some files were not shown because too many files have changed in this diff Show More