Browse Source

completed usermanagement

funding-tag
Holger Frey 5 years ago
parent
commit
26a8105748
  1. 8
      ordr3/repo.py
  2. 7
      ordr3/schemas/account.py
  3. 15
      ordr3/static/script.js
  4. 25
      ordr3/static/style.css
  5. 22
      ordr3/templates/layout_full.jinja2
  6. 2
      ordr3/templates/macros.jinja2
  7. 32
      ordr3/templates/users/delete.jinja2
  8. 3
      ordr3/templates/users/edit.jinja2
  9. 58
      ordr3/views/users.py
  10. 4
      tests/test_services.py

8
ordr3/repo.py

@ -36,6 +36,10 @@ class AbstractOrderRepository(abc.ABC): @@ -36,6 +36,10 @@ class AbstractOrderRepository(abc.ABC):
def add_user(self, user):
""" add a user to the datastore """
@abc.abstractmethod
def delete_user(self, user):
""" removes a user from the datastore """
@abc.abstractmethod
def get_user(self, reference):
""" get a user from the datastore by primary key """
@ -113,6 +117,10 @@ class SqlAlchemyRepository(AbstractOrderRepository): @@ -113,6 +117,10 @@ class SqlAlchemyRepository(AbstractOrderRepository):
""" add a user to the database """
self._add_item_to_db(user)
def delete_user(self, user):
""" removes a user from the datastore """
self._delete_item_from_db(user)
def get_user(self, reference):
""" get a user from the database by primary key """
try:

7
ordr3/schemas/account.py

@ -139,7 +139,7 @@ class MyAccountSchema(CSRFSchema): @@ -139,7 +139,7 @@ class MyAccountSchema(CSRFSchema):
@classmethod
def as_form(cls, request, **override):
settings = {
"buttons": ("Save", "Cancel"),
"buttons": ("Save Changes", "Cancel"),
"css_class": "form-horizontal registration",
}
settings.update(override)
@ -156,7 +156,10 @@ class EditAccountSchema(CSRFSchema): @@ -156,7 +156,10 @@ class EditAccountSchema(CSRFSchema):
),
)
role = colander.SchemaNode(
colander.String(), widget=deform.widget.SelectWidget(values=ROLES)
colander.String(),
widget=deform.widget.SelectWidget(
values=ROLES, css_class="custom-select"
),
)
first_name = colander.SchemaNode(colander.String(),)
last_name = colander.SchemaNode(colander.String(),)

15
ordr3/static/script.js

@ -63,6 +63,21 @@ $(function() { @@ -63,6 +63,21 @@ $(function() {
$(target).fadeOut( 100 ).delay( 100 ).fadeIn( 100 );
});
$(".o3-confirmation").on("click", function(event) {
var target = $(event.delegateTarget);
var checkbox = $(target).find(".custom-control-input");
var button = $(".o3-confirm-button");
if (checkbox.is(':checked')) {
button.prop('disabled', false);
button.addClass("btn-danger")
button.removeClass("btn-outline-danger")
} else {
button.prop('disabled', true);
button.removeClass("btn-danger")
button.addClass("btn-outline-danger")
}
});
var infinite = new Waypoint.Infinite({
element: $('.infinite-container')[0]
});

25
ordr3/static/style.css

@ -140,3 +140,28 @@ td.o3-actions a:hover { @@ -140,3 +140,28 @@ td.o3-actions a:hover {
.o3-alerts {
max-width: 16.666667%;
}
.o3-delete-warning {
padding:1rem;
}
.o3-delete-warning .custom-control-input:checked ~ .custom-control-label::before {
color: #fff;
border-color: #dc3545;
background-color:#dc3545;
}
.o3-delete-warning .custom-control-input:focus ~ .custom-control-label::before {
box-shadow:0 0 0 .2rem rgba(0, 123, 255, .25)
}
.o3-delete-warning .custom-control-input:focus:not(:checked) ~ .custom-control-label::before {
border-color:#80bdff
}
.o3-delete-warning .custom-control-input:not(:disabled):active ~ .custom-control-label::before {
color: #fff;
background-color: #b3d7ff;
border-color:#b3d7ff
}

22
ordr3/templates/layout_full.jinja2

@ -44,22 +44,22 @@ @@ -44,22 +44,22 @@
{{ 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 %}
{% endif %}
{% if 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>
{% elif 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 %}
</ul>
</div>

2
ordr3/templates/macros.jinja2

@ -8,7 +8,7 @@ @@ -8,7 +8,7 @@
{% 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>
<p class="small">{{message[0]}}</p>
{% if message[1] %}
<p class="small">{{message[1]|safe}}</p>
{% endif %}

32
ordr3/templates/users/delete.jinja2

@ -0,0 +1,32 @@ @@ -0,0 +1,32 @@
{% extends "ordr3:templates/layout_full.jinja2" %}
{% block subtitle %} Edit User {{context.model.user_name}} {% endblock subtitle %}
{% block content %}
<div class="col-5">
<div class="border border-danger rounded o3-delete-warning">
<h4 class="mb-2 text-muted mb-4">Delete User <span class="text-danger">{{ context.model.username }}</span></h4>
<p>You are about to delete the user {{ context.model.first_name }} {{ context.model.last_name }}.</p>
<p class="font-weight-bold mt-4 mb-4">This action 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 user.</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 User</button>
<button class="btn btn-outline-secondary" type="submit" name="cancel">Cancel</button>
</p>
</form>
</div>
</div>
<div class="col-5"></div>
{% endblock content %}

3
ordr3/templates/users/edit.jinja2

@ -10,7 +10,7 @@ @@ -10,7 +10,7 @@
<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) %}
{% if request.has_permission("delete", context) %}
<a href="{{request.resource_url(request.context, 'delete')}}" class="btn btn-outline-danger">Delete User</a>
{% endif %}
</p>
@ -18,7 +18,6 @@ @@ -18,7 +18,6 @@
<div class="col-5"></div>
{% endblock content %}

58
ordr3/views/users.py

@ -1,9 +1,10 @@ @@ -1,9 +1,10 @@
import deform
from sqlalchemy import func
from pyramid.csrf import get_csrf_token
from pyramid.view import view_config
from pyramid.httpexceptions import HTTPFound
from .. import events, models, resources
from .. import events, models, services, resources
from ..schemas import account
@ -87,7 +88,7 @@ def edit_user(context, request): @@ -87,7 +88,7 @@ def edit_user(context, request):
request_method="POST",
renderer="ordr3:templates/users/edit.jinja2",
)
def save_edit_user(context, request):
def save_edits(context, request):
if "Cancel" in request.POST:
return HTTPFound(request.resource_path(context.__parent__))
@ -110,3 +111,56 @@ def save_edit_user(context, request): @@ -110,3 +111,56 @@ def save_edit_user(context, request):
request.emit(events.FlashMessage.info(f"User {user.username} updated."))
return HTTPFound(request.resource_path(context.__parent__))
@view_config(
context="ordr3:resources.User",
permission="edit",
name="password",
request_method="GET",
)
def user_reset_password(context, request):
user = context.model
token = services.create_token_for_user(request.repo, user)
request.emit(events.PasswordResetEmail(user, token.token))
request.emit(
events.FlashMessage.info(
f"A password reset link has been sent to {user.email}."
)
)
return HTTPFound(request.resource_path(context.__parent__))
@view_config(
context="ordr3:resources.User",
permission="delete",
name="delete",
request_method="GET",
renderer="ordr3:templates/users/delete.jinja2",
)
def delete_user(context, request):
return {"csrf_token": get_csrf_token(request)}
@view_config(
context="ordr3:resources.User",
permission="delete",
name="delete",
request_method="POST",
)
def delete_confirmed(context, request):
if "delete" not in request.POST:
return HTTPFound(request.resource_path(context.__parent__))
if request.POST.get("confirmation", "") != "confirmed":
return HTTPFound(request.resource_path(context.__parent__))
user = context.model
request.emit(
events.FlashMessage.warning(
f"The user {user.first_name} {user.last_name} is deleted."
)
)
request.repo.delete_user(user)
return HTTPFound(request.resource_path(context.__parent__))

4
tests/test_services.py

@ -30,6 +30,10 @@ class FakeOrderRepository(AbstractOrderRepository): @@ -30,6 +30,10 @@ class FakeOrderRepository(AbstractOrderRepository):
""" add a user to the datastore """
self._users.add(user)
def delete_user(self, user):
""" removes a user from the datastore """
self._users.remove(user)
def get_user(self, reference):
""" retrieve a user from the datastore """
return next(o for o in self._users if o.id == reference)

Loading…
Cancel
Save