diff --git a/ordr3/repo.py b/ordr3/repo.py index 9ae00f0..fedd5e3 100644 --- a/ordr3/repo.py +++ b/ordr3/repo.py @@ -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): """ 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: diff --git a/ordr3/schemas/account.py b/ordr3/schemas/account.py index 92d9cde..65d7a99 100644 --- a/ordr3/schemas/account.py +++ b/ordr3/schemas/account.py @@ -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): ), ) 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(),) diff --git a/ordr3/static/script.js b/ordr3/static/script.js index 3f4d08c..d49c41a 100644 --- a/ordr3/static/script.js +++ b/ordr3/static/script.js @@ -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] }); diff --git a/ordr3/static/style.css b/ordr3/static/style.css index e8b9ce9..4aa6b5f 100644 --- a/ordr3/static/style.css +++ b/ordr3/static/style.css @@ -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 +} diff --git a/ordr3/templates/layout_full.jinja2 b/ordr3/templates/layout_full.jinja2 index 9730b9f..aac59a9 100644 --- a/ordr3/templates/layout_full.jinja2 +++ b/ordr3/templates/layout_full.jinja2 @@ -44,22 +44,22 @@ {{ macros.icon("eye-slash") }} - {% if request.user.role.name == "ADMIN" %} -
{{message[0]}}
+{{message[0]}}
{% if message[1] %}{{message[1]|safe}}
{% endif %} diff --git a/ordr3/templates/users/delete.jinja2 b/ordr3/templates/users/delete.jinja2 new file mode 100644 index 0000000..c1ad716 --- /dev/null +++ b/ordr3/templates/users/delete.jinja2 @@ -0,0 +1,32 @@ +{% extends "ordr3:templates/layout_full.jinja2" %} + +{% block subtitle %} Edit User {{context.model.user_name}} {% endblock subtitle %} + +{% block content %} + +You are about to delete the user {{ context.model.first_name }} {{ context.model.last_name }}.
+This action is permanent and cannot be undone!
+ +Reset Password - {% if not request.has_permission("delete", user) %} + {% if request.has_permission("delete", context) %} Delete User {% endif %}
@@ -18,7 +18,6 @@ - {% endblock content %} diff --git a/ordr3/views/users.py b/ordr3/views/users.py index 985b208..4caca9e 100644 --- a/ordr3/views/users.py +++ b/ordr3/views/users.py @@ -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): 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): 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__)) diff --git a/tests/test_services.py b/tests/test_services.py index 9b6b2da..51630be 100644 --- a/tests/test_services.py +++ b/tests/test_services.py @@ -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)