Browse Source

added user editing

funding-tag
Holger Frey 5 years ago
parent
commit
a2b67939b0
  1. 31
      ordr3/resources.py
  2. 35
      ordr3/schemas/account.py
  3. 4
      ordr3/static/style.css
  4. 1
      ordr3/templates/account/myaccount.jinja2
  5. 11
      ordr3/templates/emails/activation.jinja2
  6. 65
      ordr3/templates/layout_full.jinja2
  7. 19
      ordr3/templates/macros.jinja2
  8. 24
      ordr3/templates/users/edit.jinja2
  9. 24
      ordr3/templates/users/list.jinja2
  10. 18
      ordr3/templates/users/list_content.jinja2
  11. 61
      ordr3/views/users.py

31
ordr3/resources.py

@ -3,7 +3,6 @@ @@ -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): @@ -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): @@ -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 """

35
ordr3/schemas/account.py

@ -4,6 +4,9 @@ import deform @@ -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): @@ -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): @@ -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)

4
ordr3/static/style.css

@ -136,3 +136,7 @@ td.o3-actions a:hover { @@ -136,3 +136,7 @@ td.o3-actions a:hover {
.infinite-more-link td {
text-align:center;
}
.o3-alerts {
max-width: 16.666667%;
}

1
ordr3/templates/account/myaccount.jinja2

@ -4,7 +4,6 @@ @@ -4,7 +4,6 @@
{% block content %}
<div class="col-2 o3-sidebar"></div>
<div class="col-5">
<h4 class="mb-2 text-muted mb-4">Edit your account</h4>
{{form.render()|safe}}

11
ordr3/templates/emails/activation.jinja2

@ -0,0 +1,11 @@ @@ -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.

65
ordr3/templates/layout_full.jinja2

@ -24,32 +24,40 @@ @@ -24,32 +24,40 @@
<a href="{{request.resource_url(request.root)}}" class="navbar-brand col-2 mr-0">ordr</a>
<div class="col-7">
<ul class="navbar-nav">
<li class="nav-item mr-3">
<a href="{{request.resource_url(request.root, 'orders', 'add') }}" class="btn btn-outline-light pt-1" title="Create a new order">
{{ macros.icon("plus-circle") }}
</a>
</li>
<li class="nav-item btn-group b3-on-select mr-3">
<a href="{{request.resource_url(request.root, 'orders', 'add') }}" class="btn btn-outline-light pt-1" title="Edit selected orders">
{{ macros.icon("pencil-square") }}
</a>
<a href="{{request.resource_url(request.root, 'orders', 'add') }}" class="btn btn-outline-light pt-1" title="Delete selected orders">
{{ macros.icon("trash") }}
</a>
</li>
<li class="nav-item mr-3">
<a href="{{request.resource_url(request.root, 'orders', 'add') }}" class="btn btn-outline-light pt-1 o3-details o3-show-details" title="View order details">
{{ macros.icon("eye") }}
{{ macros.icon("eye-slash") }}
</a>
</li>
{% if request.user.role.name == "ADMIN" %}
{% if is_order_list %}
<li class="nav-item mr-3">
<a href="{{request.resource_url(request.root, 'users') }}" class="btn btn-outline-light pt-1 o3-users-link" title="User Management{% if new_user_badge >0 %}, {{ new_user_badge }} new users{% endif %}">
{{ macros.icon("people") }}
{% if new_user_badge >0 %}
<span class="badge badge-pill badge-light">{{ new_user_badge }}</span>
{% endif %}
<a href="{{request.resource_url(request.root, 'orders', 'add') }}" class="btn btn-outline-light pt-1" title="Create a new order">
{{ macros.icon("plus-circle") }}
</a>
</li>
<li class="nav-item btn-group b3-on-select mr-3">
<a href="{{request.resource_url(request.root, 'orders', 'add') }}" class="btn btn-outline-light pt-1" title="Edit selected orders">
{{ macros.icon("pencil-square") }}
</a>
<a href="{{request.resource_url(request.root, 'orders', 'add') }}" class="btn btn-outline-light pt-1" title="Delete selected orders">
{{ macros.icon("trash") }}
</a>
</li>
<li class="nav-item mr-3">
<a href="{{request.resource_url(request.root, 'orders', 'add') }}" class="btn btn-outline-light pt-1 o3-details o3-show-details" title="View order details">
{{ macros.icon("eye") }}
{{ macros.icon("eye-slash") }}
</a>
</li>
{% if request.user.role.name == "ADMIN" %}
<li class="nav-item mr-3">
<a href="{{request.resource_url(request.root, 'users') }}" class="btn btn-outline-light pt-1 o3-users-link" title="User Management{% if new_user_badge >0 %}, {{ new_user_badge }} new users{% endif %}">
{{ macros.icon("people") }}
{% if new_user_badge >0 %}
<span class="badge badge-pill badge-light">{{ new_user_badge }}</span>
{% endif %}
</a>
</li>
{% endif %}
{% elif is_user_list %}
<li class="nav-item mr-3">
<a href="{{request.resource_url(request.root) }}" class="btn btn-outline-light pt-1" title="Show orders">
{{ macros.icon("bag") }}
</a>
</li>
{% endif %}
@ -59,7 +67,7 @@ @@ -59,7 +67,7 @@
<ul class="navbar-nav">
<li class="nav-item mr-3">
<form class="form-inline mt-1" method="GET" action="{{request.resource_url(request.root, 'orders') }}">
<input class="form-control form-control-sm" name="search" type="search" placeholder="Search" aria-label="Search">
<input class="form-control form-control-sm" name="search" type="search" placeholder="Search Orders" aria-label="Search">
</form>
</li>
<li class="nav-item dropdown">
@ -76,9 +84,14 @@ @@ -76,9 +84,14 @@
</header>
<div class="container-fluid o3-head-space o3-content">
<div class="row">
<div class="col-2 o3-sidebar">
{% block sidebar %}{% endblock sidebar %}
{{ macros.alerts() }}
</div>
{% block content %}
<p>No content</p>
{% endblock content %}
</div>
</div>

19
ordr3/templates/macros.jinja2

@ -1,3 +1,22 @@ @@ -1,3 +1,22 @@
{% macro icon(name) -%}
{% include 'ordr3:templates/icons/%s.svg' % name %}
{%- endmacro %}
{% macro alerts() -%}
<div class="o3-alerts fixed-bottom">
{% for channel in ("info", "warning") %}
{% for message in request.session.pop_flash(channel) %}
<div class="alert alert-{{channel}} alert-dismissible fade show" role="alert">
<p>{{message[0]}}</p>
{% if message[1] %}
<p class="small">{{message[1]|safe}}</p>
{% endif %}
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
{% endfor %}
{% endfor %}
</div>
{%- endmacro %}

24
ordr3/templates/users/edit.jinja2

@ -0,0 +1,24 @@ @@ -0,0 +1,24 @@
{% extends "ordr3:templates/layout_full.jinja2" %}
{% block subtitle %} Edit User {{context.model.user_name}} {% endblock subtitle %}
{% block content %}
<div class="col-5">
<h4 class="mb-2 text-muted mb-4">Edit User</h4>
{{form.render()|safe}}
<hr>
<p class="mt-4">
<a href="{{request.resource_url(request.context, 'password')}}" class="btn btn-outline-secondary">Reset Password</a>
{% if not request.has_permission("delete", user) %}
<a href="{{request.resource_url(request.context, 'delete')}}" class="btn btn-outline-danger">Delete User</a>
{% endif %}
</p>
</div>
<div class="col-5"></div>
{% endblock content %}

24
ordr3/templates/users/list.jinja2

@ -2,19 +2,22 @@ @@ -2,19 +2,22 @@
{% block subtitle %} Manage Users {% endblock subtitle %}
{% block content %}
<div class="col-2 o3-sidebar">
{% block sidebar %}
<nav class="nav nav-pills flex-column">
<div class="nav-link disabled text-small" tabindex="-1" aria-disabled="true">Role</div>
<a class="nav-link {% if filter_role == 'all' %}active{% endif %}" href="{{ request.resource_url(context) }}">All</a>
{% for role in roles %}
<a class="nav-link {% if filter_role == role.name.lower() %}active{% endif %}" href="{{ request.resource_url(context, query={'role':role.name.lower()}) }}">{{role.name.lower()}}</a>
{% endfor %}
</nav>
<nav class="nav nav-pills flex-column">
<div class="nav-link disabled text-small" tabindex="-1" aria-disabled="true">Role</div>
<a class="nav-link {% if filter_role == 'all' %}active{% endif %}" href="{{ request.resource_url(context) }}">All</a>
{% for role in roles %}
<a class="nav-link {% if filter_role == role.name.lower() %}active{% endif %}" href="{{ request.resource_url(context, query={'role':role.name.lower()}) }}">{{role.name.lower()}}</a>
{% endfor %}
</nav>
{% endblock sidebar %}
{% block content %}
</div>
<div class="col-10">
<table class="table table-hover o3-data-table">
<thead>
@ -36,5 +39,4 @@ @@ -36,5 +39,4 @@
{% endif %}
</div>
{% endblock content %}

18
ordr3/templates/users/list_content.jinja2

@ -1,14 +1,18 @@ @@ -1,14 +1,18 @@
{% import 'ordr3:templates/macros.jinja2' as macros with context %}
{% for user in users %}
<tr class="infinite-item">
<td><a href="{{request.resource_url(request.root, 'orders', query={'user':user.username})}}" title="show orders">{{ user.username }}</a></td>
<td>{{ user.first_name }}</td>
<td>{{ user.last_name }}</td>
<td><a class="o3-copy" title="copy to clipboard">{{ user.email }}</a></td>
<td>{{ user.role.name.capitalize() }}</td>
<td><a href="{{request.resource_url(request.root, 'orders', query={'user':user.model.username})}}" title="show orders">{{ user.model.username }}</a></td>
<td>{{ user.model.first_name }}</td>
<td>{{ user.model.last_name }}</td>
<td><a class="o3-copy" title="copy to clipboard">{{ user.model.email }}</a></td>
<td>{{ user.model.role.name.capitalize() }}</td>
<td class="o3-actions">
<a href="{{ request.resource_url(context, user.username, 'edit') }}" title="Edit user">{{ macros.icon("pencil")}}</a>
<a href="{{ request.resource_url(context, user.username, 'delete') }}" title="Delete user">{{ macros.icon("trash")}}</a>
{% if request.has_permission("edit", user) %}
<a href="{{ request.resource_url(context, user.model.username, 'edit') }}" title="Edit user">{{ macros.icon("pencil")}}</a>
{% endif %}
{% if request.has_permission("delete", user) %}
<a href="{{ request.resource_url(context, user.model.username, 'delete') }}" title="Delete user">{{ macros.icon("trash")}}</a>
{% endif %}
</td>
</tr>
{% endfor %}

61
ordr3/views/users.py

@ -1,7 +1,10 @@ @@ -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): @@ -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__))

Loading…
Cancel
Save