diff --git a/ordr3/resources.py b/ordr3/resources.py
index c95c8d4..c67d063 100644
--- a/ordr3/resources.py
+++ b/ordr3/resources.py
@@ -3,7 +3,6 @@
import abc
from pyramid.security import Allow, Everyone, Authenticated
-from sqlalchemy.inspection import inspect
class BaseResource(abc.ABC):
@@ -31,16 +30,22 @@ class BaseResource(abc.ABC):
child_node_class = self.nodes[key]
return child_node_class(key, self)
+
+class User(BaseResource):
+ def __acl__(self):
+ """ access controll list """
+ acl = [
+ (Allow, "role:admin", "view"),
+ (Allow, "role:admin", "edit"),
+ ]
+ if not self.model.is_active:
+ acl.append((Allow, "role:admin", "delete"))
+ return acl
+
@classmethod
- def from_sqla(cls, sql_model_instance, parent):
+ def from_model(cls, model, 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)
+ return cls(model.username, parent, model)
class UserList(BaseResource):
@@ -48,6 +53,14 @@ class UserList(BaseResource):
""" access controll list """
return [(Allow, "role:admin", "view")]
+ def __getitem__(self, key):
+ """ returns child resources """
+ try:
+ user = self.request.repo.get_user_by_username(key)
+ return User.from_model(user, self)
+ except StopIteration as e:
+ raise KeyError from e
+
class Root(BaseResource):
""" Root resource """
diff --git a/ordr3/schemas/account.py b/ordr3/schemas/account.py
index 2556064..92d9cde 100644
--- a/ordr3/schemas/account.py
+++ b/ordr3/schemas/account.py
@@ -4,6 +4,9 @@ import deform
import colander
from .base import CSRFSchema
+from ..models import UserRole
+
+ROLES = [(role.name, role.name.capitalize()) for role in UserRole]
@colander.deferred
@@ -117,7 +120,7 @@ class ResetPasswordSchema(CSRFSchema):
class MyAccountSchema(CSRFSchema):
- """ edit an account """
+ """ edit the own account """
user_name = colander.SchemaNode(
colander.String(),
@@ -141,3 +144,33 @@ class MyAccountSchema(CSRFSchema):
}
settings.update(override)
return super().as_form(request, **settings)
+
+
+class EditAccountSchema(CSRFSchema):
+ """ edit an account """
+
+ user_name = colander.SchemaNode(
+ colander.String(),
+ widget=deform.widget.TextInputWidget(
+ template="textinput_disabled.pt", css_class="o3-reg-username"
+ ),
+ )
+ role = colander.SchemaNode(
+ colander.String(), widget=deform.widget.SelectWidget(values=ROLES)
+ )
+ first_name = colander.SchemaNode(colander.String(),)
+ last_name = colander.SchemaNode(colander.String(),)
+ email = colander.SchemaNode(
+ colander.String(),
+ validator=colander.Email(),
+ widget=deform.widget.TextInputWidget(template="email.pt",),
+ )
+
+ @classmethod
+ def as_form(cls, request, **override):
+ settings = {
+ "buttons": ("Save Changes", "Cancel"),
+ "css_class": "form-horizontal registration",
+ }
+ settings.update(override)
+ return super().as_form(request, **settings)
diff --git a/ordr3/static/style.css b/ordr3/static/style.css
index 783b8ff..e8b9ce9 100644
--- a/ordr3/static/style.css
+++ b/ordr3/static/style.css
@@ -136,3 +136,7 @@ td.o3-actions a:hover {
.infinite-more-link td {
text-align:center;
}
+
+.o3-alerts {
+ max-width: 16.666667%;
+ }
diff --git a/ordr3/templates/account/myaccount.jinja2 b/ordr3/templates/account/myaccount.jinja2
index 0ee4b14..69be72a 100644
--- a/ordr3/templates/account/myaccount.jinja2
+++ b/ordr3/templates/account/myaccount.jinja2
@@ -4,7 +4,6 @@
{% block content %}
-
Edit your account
{{form.render()|safe}}
diff --git a/ordr3/templates/emails/activation.jinja2 b/ordr3/templates/emails/activation.jinja2
new file mode 100644
index 0000000..dc9bc0b
--- /dev/null
+++ b/ordr3/templates/emails/activation.jinja2
@@ -0,0 +1,11 @@
+Dear {{ user.first_name }},
+
+Your account was activated by {{ request.user.first_name }}.
+
+You can now log in at {{ request.resource_url(request.root) }} and order some stuff.
+
+Regards,
+The Ordr System
+
+--
+Please don't respont to this email! This is an automatically generated notification.
diff --git a/ordr3/templates/layout_full.jinja2 b/ordr3/templates/layout_full.jinja2
index 24975d5..9730b9f 100644
--- a/ordr3/templates/layout_full.jinja2
+++ b/ordr3/templates/layout_full.jinja2
@@ -24,32 +24,40 @@
ordr
-
-
- {{ macros.icon("plus-circle") }}
-
-
-
-
- {{ macros.icon("pencil-square") }}
-
-
- {{ macros.icon("trash") }}
-
-
-
-
- {{ macros.icon("eye") }}
- {{ macros.icon("eye-slash") }}
-
-
- {% if request.user.role.name == "ADMIN" %}
+ {% if is_order_list %}
-
- {{ macros.icon("people") }}
- {% if new_user_badge >0 %}
- {{ new_user_badge }}
- {% endif %}
+
+ {{ macros.icon("plus-circle") }}
+
+
+
+
+ {{ macros.icon("pencil-square") }}
+
+
+ {{ macros.icon("trash") }}
+
+
+
+
+ {{ macros.icon("eye") }}
+ {{ macros.icon("eye-slash") }}
+
+
+ {% if request.user.role.name == "ADMIN" %}
+
+
+ {{ macros.icon("people") }}
+ {% if new_user_badge >0 %}
+ {{ new_user_badge }}
+ {% endif %}
+
+
+ {% endif %}
+ {% elif is_user_list %}
+
+
+ {{ macros.icon("bag") }}
{% endif %}
@@ -59,7 +67,7 @@
@@ -76,9 +84,14 @@
+
{% block content %}
No content
{% endblock content %}
+
diff --git a/ordr3/templates/macros.jinja2 b/ordr3/templates/macros.jinja2
index 2cf5f5c..d6c3a52 100644
--- a/ordr3/templates/macros.jinja2
+++ b/ordr3/templates/macros.jinja2
@@ -1,3 +1,22 @@
{% macro icon(name) -%}
{% include 'ordr3:templates/icons/%s.svg' % name %}
{%- endmacro %}
+
+
+{% macro alerts() -%}
+
+ {% for channel in ("info", "warning") %}
+ {% for message in request.session.pop_flash(channel) %}
+
+
{{message[0]}}
+ {% if message[1] %}
+
{{message[1]|safe}}
+ {% endif %}
+
+ ×
+
+
+ {% endfor %}
+ {% endfor %}
+
+{%- endmacro %}
diff --git a/ordr3/templates/users/edit.jinja2 b/ordr3/templates/users/edit.jinja2
new file mode 100644
index 0000000..cc10031
--- /dev/null
+++ b/ordr3/templates/users/edit.jinja2
@@ -0,0 +1,24 @@
+{% extends "ordr3:templates/layout_full.jinja2" %}
+
+{% block subtitle %} Edit User {{context.model.user_name}} {% endblock subtitle %}
+
+{% block content %}
+
+
+
Edit User
+ {{form.render()|safe}}
+
+
+ Reset Password
+ {% if not request.has_permission("delete", user) %}
+ Delete User
+ {% endif %}
+
+
+
+
+
+
+{% endblock content %}
+
+
diff --git a/ordr3/templates/users/list.jinja2 b/ordr3/templates/users/list.jinja2
index dd373dd..da22a27 100644
--- a/ordr3/templates/users/list.jinja2
+++ b/ordr3/templates/users/list.jinja2
@@ -2,19 +2,22 @@
{% block subtitle %} Manage Users {% endblock subtitle %}
-{% block content %}
-
@@ -36,5 +39,4 @@
{% endif %}
-
{% endblock content %}
diff --git a/ordr3/templates/users/list_content.jinja2 b/ordr3/templates/users/list_content.jinja2
index 41aa927..e9430b5 100644
--- a/ordr3/templates/users/list_content.jinja2
+++ b/ordr3/templates/users/list_content.jinja2
@@ -1,14 +1,18 @@
{% import 'ordr3:templates/macros.jinja2' as macros with context %}
{% for user in users %}
- {{ user.username }}
- {{ user.first_name }}
- {{ user.last_name }}
- {{ user.email }}
- {{ user.role.name.capitalize() }}
+ {{ user.model.username }}
+ {{ user.model.first_name }}
+ {{ user.model.last_name }}
+ {{ user.model.email }}
+ {{ user.model.role.name.capitalize() }}
- {{ macros.icon("pencil")}}
- {{ macros.icon("trash")}}
+ {% if request.has_permission("edit", user) %}
+ {{ macros.icon("pencil")}}
+ {% endif %}
+ {% if request.has_permission("delete", user) %}
+ {{ macros.icon("trash")}}
+ {% endif %}
{% endfor %}
diff --git a/ordr3/views/users.py b/ordr3/views/users.py
index 12f1027..985b208 100644
--- a/ordr3/views/users.py
+++ b/ordr3/views/users.py
@@ -1,7 +1,10 @@
+import deform
from sqlalchemy import func
from pyramid.view import view_config
+from pyramid.httpexceptions import HTTPFound
-from .. import models
+from .. import events, models, resources
+from ..schemas import account
def _get_role(request):
@@ -46,10 +49,64 @@ def list(context, request):
next_offset = None if limit != len(users) else (offset + limit)
filter_role = "all" if role is None else role.name.lower()
+ user_resources = [resources.User.from_model(u, context) for u in users]
return {
"filter_role": filter_role,
"roles": models.UserRole,
- "users": users,
+ "users": user_resources,
"next_offset": next_offset,
+ "is_user_list": True,
}
+
+
+@view_config(
+ context="ordr3:resources.User",
+ permission="edit",
+ name="edit",
+ request_method="GET",
+ renderer="ordr3:templates/users/edit.jinja2",
+)
+def edit_user(context, request):
+ form = account.EditAccountSchema.as_form(request)
+ form_data = {
+ "user_name": context.model.username,
+ "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="ordr3:resources.User",
+ permission="edit",
+ name="edit",
+ request_method="POST",
+ renderer="ordr3:templates/users/edit.jinja2",
+)
+def save_edit_user(context, request):
+ if "Cancel" in request.POST:
+ return HTTPFound(request.resource_path(context.__parent__))
+
+ form = account.EditAccountSchema.as_form(request)
+ data = request.POST.items()
+ try:
+ appstruct = form.validate(data)
+ except deform.ValidationFailure:
+ return {"form": form}
+
+ user = context.model
+ was_active = user.is_active
+ user.first_name = appstruct["first_name"]
+ user.last_name = appstruct["last_name"]
+ user.email = appstruct["email"]
+ user.role = models.UserRole[appstruct["role"]]
+
+ if not was_active and user.is_active:
+ request.emit(events.AccountActivationEmail(user,))
+
+ request.emit(events.FlashMessage.info(f"User {user.username} updated."))
+ return HTTPFound(request.resource_path(context.__parent__))