Browse Source

added custom jinja filters

funding-tag
Holger Frey 5 years ago
parent
commit
cf121be9cd
  1. 10
      development.ini
  2. 4
      ordr3/__init__.py
  3. 7
      ordr3/resources.py
  4. 14
      ordr3/services.py
  5. 4
      ordr3/templates/orders/batch_delete.jinja2
  6. 6
      ordr3/templates/orders/delete.jinja2
  7. 6
      ordr3/templates/orders/list_content.jinja2
  8. 55
      ordr3/templates/orders/view.jinja2
  9. 38
      ordr3/views/__init__.py
  10. 11
      ordr3/views/orders.py
  11. 8
      ordr3/views/root.py
  12. 19
      tests/test_models.py
  13. 46
      tests/test_repo.py
  14. 54
      tests/test_services.py

10
development.ini

@ -31,6 +31,16 @@ mail.host = localhost @@ -31,6 +31,16 @@ mail.host = localhost
mail.port = 2525
mail.default_sender = ordr@example.com
# custom jinja2 filters:
jinja2.filters =
resource_url = pyramid_jinja2.filters:resource_url_filter
as_date = ordr3.views:jinja_date
as_time = ordr3.views:jinja_time
as_datetime = ordr3.views:jinja_datetime
view_comment = ordr3.views:jinja_view_comment
extract_links = ordr3.views:jinja_extract_links
nl2br = ordr3.views:jinja_nl2br
# By default, the toolbar only appears for clients from IP addresses
# '127.0.0.1' and '::1'.
# debugtoolbar.hosts = 127.0.0.1 ::1

4
ordr3/__init__.py

@ -27,6 +27,8 @@ def main(global_config, **settings): @@ -27,6 +27,8 @@ def main(global_config, **settings):
config.set_root_factory(resources.Root)
config.include("pyramid_jinja2")
config.include(".adapters")
config.include(".events")
config.include(".resources")
@ -35,8 +37,6 @@ def main(global_config, **settings): @@ -35,8 +37,6 @@ def main(global_config, **settings):
config.include(".schemas")
config.include(".views")
config.include("pyramid_jinja2")
config.scan()
return config.make_wsgi_app()

7
ordr3/resources.py

@ -2,7 +2,7 @@ @@ -2,7 +2,7 @@
import abc
from pyramid.security import Allow, Everyone, Authenticated
from pyramid.security import DENY_ALL, Allow, Everyone, Authenticated
class BaseResource(abc.ABC):
@ -37,6 +37,7 @@ class User(BaseResource): @@ -37,6 +37,7 @@ class User(BaseResource):
acl = [
(Allow, "role:admin", "view"),
(Allow, "role:admin", "edit"),
DENY_ALL,
]
if not self.model.is_active:
acl.append((Allow, "role:admin", "delete"))
@ -51,7 +52,7 @@ class User(BaseResource): @@ -51,7 +52,7 @@ class User(BaseResource):
class UserList(BaseResource):
def __acl__(self):
""" access controll list """
return [(Allow, "role:admin", "view")]
return [(Allow, "role:admin", "view"), DENY_ALL]
def __getitem__(self, key):
""" returns child resources """
@ -76,6 +77,7 @@ class Order(BaseResource): @@ -76,6 +77,7 @@ class Order(BaseResource):
else:
acl.append((Allow, f"user:{self.model.created_by}", "edit"))
acl.append((Allow, f"user:{self.model.created_by}", "delete"))
acl.append(DENY_ALL)
return acl
@classmethod
@ -92,6 +94,7 @@ class OrderList(BaseResource): @@ -92,6 +94,7 @@ class OrderList(BaseResource):
(Allow, "role:user", "add"),
(Allow, "role:purchaser", "batch-edit"),
(Allow, "role:purchaser", "batch-delete"),
DENY_ALL,
]
def __getitem__(self, key):

14
ordr3/services.py

@ -58,11 +58,15 @@ def _find_consumables(repo, repeat=3, days=365 * 2): @@ -58,11 +58,15 @@ def _find_consumables(repo, repeat=3, days=365 * 2):
def create_log_entry(order, status, user):
old_status = order.status
log_entry = models.LogEntry(order.id, status, user.username)
order.add_to_log(log_entry)
# is this noteworthy?
return (old_status != status) and (order.created_by != user.username)
if old_status != status:
# only change to a new status
log_entry = models.LogEntry(order.id, status, user.username)
order.add_to_log(log_entry)
# is this noteworthy?
return order.created_by != user.username
# it's not noteworthy
return False
def verify_credentials(repo, pass_ctx, username, password):

4
ordr3/templates/orders/batch_delete.jinja2

@ -58,11 +58,11 @@ @@ -58,11 +58,11 @@
{% endfor %}
</tbody>
</table>
<p class="font-weight-bold mt-4 mb-4">This action is permanent and cannot be undone!</p>
<p class="font-weight-bold mt-4 mb-4">Deleting the orders permanent and cannot be undone!</p>
<p class="mt-4">
<div class="custom-control custom-switch o3-confirmation">
<input type="checkbox" class="custom-control-input" id="confirmation" value="confirmed" name="confirmation">
<label class="custom-control-label" for="confirmation">I confirm that I want to delete this order.</label>
<label class="custom-control-label" for="confirmation">I confirm that I want to delete these orders.</label>
</div>
</p>
<p class="mt-4">

6
ordr3/templates/orders/delete.jinja2

@ -18,16 +18,16 @@ @@ -18,16 +18,16 @@
<dt class="col-sm-4">Package Size</dt>
<dd class="col-sm-8">{{ context.model.package_size }}</dd>
<dt class="col-sm-4">Amount, Price</dt>
<dd class="col-sm-8">{{ context.model.amount }} * {{ context.model.unit_price }} {{ context.model.currency }}</dd>
<dd class="col-sm-8">{{ context.model.amount }} * {{ "%.2f"|format(context.model.unit_price) }} {{ context.model.currency }}</dd>
<dt class="col-sm-4">Ordered By</dt>
<dd class="col-sm-8">{{ context.model.created_by }}</dd>
<dt class="col-sm-4">Ordered On</dt>
<dd class="col-sm-8">{{ context.model.created_on.strftime("%Y-%m-%d %H:%I") }}</dd>
<dt class="col-sm-4">Status</dt>
<dd class="col-sm-8">{{ macros.status_label(context.model.status) }}</dd>
</dl>
</dl>
<p class="font-weight-bold mt-4 mb-4">This action is permanent and cannot be undone!</p>
<p class="font-weight-bold mt-4 mb-4">Deleting this order is permanent and cannot be undone!</p>
<form action="{{request.resource_url(context, 'delete')}}" method="POST">
<p class="mt-4">
<div class="custom-control custom-switch o3-confirmation">

6
ordr3/templates/orders/list_content.jinja2

@ -5,8 +5,8 @@ @@ -5,8 +5,8 @@
<td><input type="checkbox" name="selection" value="{{order.model.id}}" class="o3-multiple-selection"></td>
{% endif %}
<td class="o3-dont-wrap">
{{ order.model.created_on.strftime("%Y-%m-%d") }}
<div class="text-secondary small">{{ order.model.created_on.strftime("%H:%I:%S") }}</div>
{{ order.model.created_on|as_date }}
<div class="text-secondary small">{{ order.model.created_on|as_time }}</div>
</td>
<td>
<a class="o3-copy" title="copy to clipboard">{{ order.model.cas_description }}</a>
@ -34,7 +34,7 @@ @@ -34,7 +34,7 @@
</td>
<td class="o3-actions">
{% if request.has_permission("edit", order) %}
<a href="{{ request.resource_url(order, 'edit') }}" title="Edit Order">{{ macros.icon("pencil")}}</a>
<a href="{{ order|resource_url('edit') }}" title="Edit Order">{{ macros.icon("pencil")}}</a>
{% elif request.has_permission("view", order) %}
<a href="{{ request.resource_url(order) }}" title="View Order">{{ macros.icon("eye")}}</a>
{% endif %}

55
ordr3/templates/orders/view.jinja2

@ -0,0 +1,55 @@ @@ -0,0 +1,55 @@
{% extends "ordr3:templates/layout_full.jinja2" %}
{% block subtitle %} View Order {{context.model.cas_description}} {% endblock subtitle %}
{% block content %}
<div class="col-5">
<h4 class="mb-2 text-muted mb-4 text-truncate">Details for Order <span class="text-dark">{{ context.model.cas_description }}</span></h4>
<dl class="row mt-4">
<dt class="col-sm-4">Cas / Description</dt>
<dd class="col-sm-8">{{ context.model.cas_description }}</dd>
<dt class="col-sm-4">Vendor</dt>
<dd class="col-sm-8">{{ context.model.vendor }}</dd>
<dt class="col-sm-4">Catalog Nr.</dt>
<dd class="col-sm-8">{{ context.model.catalog_nr }}</dd>
<dt class="col-sm-4">Package Size</dt>
<dd class="col-sm-8">{{ context.model.package_size }}</dd>
<dt class="col-sm-4">Amount, Price</dt>
<dd class="col-sm-8">{{ context.model.amount }} * {{ "%.2f"|format(context.model.unit_price) }} {{ context.model.currency }}</dd>
<dt class="col-sm-4">Price, total</dt>
<dd class="col-sm-8">{{ "%.2f"|format(context.model.total_price) }} {{ context.model.currency }}</dd>
<dt class="col-sm-4">Ordered By</dt>
<dd class="col-sm-8">{{ context.model.created_by }}</dd>
<dt class="col-sm-4">Ordered On</dt>
<dd class="col-sm-8">{{ context.model.created_on.strftime("%Y-%m-%d %H:%I") }}</dd>
<dt class="col-sm-4">Account</dt>
<dd class="col-sm-8">{{ context.model.account|default("-", True) }}</dd>
<dt class="col-sm-4">Comment</dt>
<dd class="col-sm-8">{{ context.model.comment|default("-", True) }}</dd>
<dt class="col-sm-4">Status</dt>
<dd class="col-sm-8">{{ macros.status_label(context.model.status) }}</dd>
</dl>
<p class="font-weight-bold mt-4 mb-4">Deleting this order is permanent and cannot be undone!</p>
<form action="{{request.resource_url(context, 'delete')}}" method="POST">
<p class="mt-4">
<div class="custom-control custom-switch o3-confirmation">
<input type="checkbox" class="custom-control-input" id="confirmation" value="confirmed" name="confirmation">
<label class="custom-control-label" for="confirmation">I confirm that I want to delete this order.</label>
</div>
</p>
<p class="mt-4">
<input type="hidden" name="csrf_token" value="{{csrf_token}}">
<button class="btn btn-outline-danger o3-confirm-button" type="submit" name="delete" disabled="disabled">Delete Order</button>
<button class="btn btn-outline-secondary" type="submit" name="cancel">Cancel</button>
</p>
</form>
</div>
<div class="col-5"></div>
{% endblock content %}

38
ordr3/views/__init__.py

@ -3,7 +3,10 @@ @@ -3,7 +3,10 @@
some view helpers are defined here
"""
from collections import UserDict
import re
from collections import UserDict, namedtuple
RE_SIMPLE_URL = re.compile("((?:www|http)\\S+)")
def get_offset(request):
@ -21,6 +24,39 @@ class DefaultQueryParams(UserDict): @@ -21,6 +24,39 @@ class DefaultQueryParams(UserDict):
return {k: v for k, v in copied.items() if v is not None}
def jinja_date(some_date):
return some_date.strftime("%Y-%m-%d")
def jinja_time(some_date):
return some_date.strftime("%H:%I")
def jinja_datetime(some_date):
return some_date.strftime("%Y-%m-%d %H:%I")
def _as_html_link(url, css_class):
return f'<a href="{url}" class="{css_class}">{url}</a>'
def jinja_extract_links(text, css_class=""):
links = RE_SIMPLE_URL.findall(text)
return [_as_html_link(url, css_class) for url in links]
def jinja_view_comment(text, css_class):
links = RE_SIMPLE_URL.findall(text)
for url in links:
html = _as_html_link(url, css_class)
text = text.replace(url, html)
return text
def jinja_nl2br(text, replacement="<br>"):
return replacement.join(text.splitlines())
def includeme(config):
""" adding request helpers

11
ordr3/views/orders.py

@ -239,6 +239,17 @@ def batch_edit_confirm(context, request): @@ -239,6 +239,17 @@ def batch_edit_confirm(context, request):
return HTTPFound(request.resource_path(context))
@view_config(
context="ordr3:resources.Order",
permission="view",
name="view",
request_method="GET",
renderer="ordr3:templates/orders/view.jinja2",
)
def view_order(context, request):
return {"csrf_token": get_csrf_token(request)}
@view_config(
context="ordr3:resources.Order",
permission="delete",

8
ordr3/views/root.py

@ -1,10 +1,16 @@ @@ -1,10 +1,16 @@
""" static and login pages """
from pyramid.view import view_config
from pyramid.view import (
view_config,
notfound_view_config,
forbidden_view_config,
)
from pyramid.httpexceptions import HTTPFound
# @forbidden_view_config()
# @notfound_view_config()
@view_config(
context="ordr3:resources.Root", permission="view",
)

19
tests/test_models.py

@ -64,6 +64,25 @@ def test_orderitem_add_to_log_non_empty_log(): @@ -64,6 +64,25 @@ def test_orderitem_add_to_log_non_empty_log():
assert order.status == log_entry_2.status
@pytest.mark.parametrize(
"status,expected",
[
("OPEN", False),
("APPROVAL", True),
("ORDERED", True),
("COMPLETED", True),
("HOLD", False),
],
)
def test_orderitem_in_process(status, expected):
from ordr3.models import OrderItem, OrderStatus
order = OrderItem(*list("ABCDEFGHIJK"))
order.status = OrderStatus[status]
assert order.in_process == expected
def test_LogEntry_init():
from ordr3.models import LogEntry

46
tests/test_repo.py

@ -79,6 +79,26 @@ def test_sql_repo_add_order(session, example_orders): @@ -79,6 +79,26 @@ def test_sql_repo_add_order(session, example_orders):
assert order == example_orders[0]
def test_sql_repo_delete_order(session, example_orders):
from ordr3.repo import SqlAlchemyRepository
from ordr3.services import create_log_entry
from ordr3.models import LogEntry, OrderStatus, User
repo = SqlAlchemyRepository(session)
repo.add_order(example_orders[0])
repo.add_order(example_orders[1])
user = User(*list("ABCDEFG"))
create_log_entry(example_orders[0], OrderStatus.APPROVAL, user)
create_log_entry(example_orders[1], OrderStatus.APPROVAL, user)
session.flush()
repo.delete_order(example_orders[0])
session.flush()
assert repo.list_orders() == [example_orders[1]]
assert session.query(LogEntry).all() == example_orders[1].log
def test_sql_repo_get_order(session, example_orders):
from ordr3.repo import SqlAlchemyRepository
@ -126,6 +146,19 @@ def test_sql_repo_add_user(session, example_users): @@ -126,6 +146,19 @@ def test_sql_repo_add_user(session, example_users):
assert user == example_users[0]
def test_sql_repo_delte_user(session, example_users):
from ordr3.repo import SqlAlchemyRepository
repo = SqlAlchemyRepository(session)
repo.add_user(example_users[0])
repo.add_user(example_users[1])
session.flush()
repo.delete_user(example_users[0])
assert repo.list_users() == [example_users[1]]
def test_sql_repo_get_user(session, example_users):
from ordr3.repo import SqlAlchemyRepository
@ -204,6 +237,19 @@ def test_sql_repo_list_users(session, example_users): @@ -204,6 +237,19 @@ def test_sql_repo_list_users(session, example_users):
assert repo.list_users() == [earlier, later]
def test_sql_repo_count_new_users(session, example_users):
from ordr3.repo import SqlAlchemyRepository
from ordr3.models import UserRole
repo = SqlAlchemyRepository(session)
example_users[0].role = UserRole.NEW
repo.add_user(example_users[0])
repo.add_user(example_users[1])
session.flush()
assert repo.count_new_users() == 1
def test_sql_search_vendor(session, example_users):
from ordr3.repo import SqlAlchemyRepository
from ordr3.models import Vendor

54
tests/test_services.py

@ -149,24 +149,54 @@ def test_service_find_consumables(prefilled_repo): @@ -149,24 +149,54 @@ def test_service_find_consumables(prefilled_repo):
assert [o.id for o in result] == [3, 0]
def test_create_log_entry(prefilled_repo):
def test_create_log_entry_noteworthy(prefilled_repo):
from ordr3.services import create_log_entry
from ordr3.models import OrderStatus, User
order = prefilled_repo.get_order(1)
user_1 = User(*list("ABCDEFG"))
user_2 = User(*list("AXCDEFG"))
# first log entry, order.created_by is automatically set
create_log_entry(order, OrderStatus.OPEN, user_1)
# second log entry
result = create_log_entry(order, OrderStatus.APPROVAL, user_2)
assert result is True
assert len(order.log) == 2
first_entry, second_entry = order.log
assert first_entry.order_id == order.id
assert first_entry.status == OrderStatus.OPEN
assert first_entry.by == "B"
assert isinstance(first_entry.date, datetime)
assert order.created_by == first_entry.by
assert order.created_on == first_entry.date
assert order.status == second_entry.status
def test_create_log_entry_not_noteworthy_same_user(prefilled_repo):
from ordr3.services import create_log_entry
from ordr3.models import OrderStatus, User
order = prefilled_repo.get_order(1)
user = User(*list("ABCDEFG"))
order.created_by = user.username
result = create_log_entry(order, OrderStatus.APPROVAL, user)
assert result is False
def test_create_log_entry_not_noteworthy_same_status(prefilled_repo):
from ordr3.services import create_log_entry
from ordr3.models import OrderStatus, User
order = prefilled_repo.get_order(1)
user = User(*list("ABCDEFG"))
create_log_entry(order, OrderStatus.APPROVAL, user)
assert len(order.log) == 1
log_entry = order.log[0]
assert log_entry.order_id == order.id
assert log_entry.status == OrderStatus.APPROVAL
assert log_entry.by == "B"
assert isinstance(log_entry.date, datetime)
assert order.status == log_entry.status
assert order.created_by == log_entry.by
assert order.created_on == log_entry.date
result = create_log_entry(order, OrderStatus.ORDERED, user)
assert result is False
@pytest.mark.parametrize(

Loading…
Cancel
Save