diff --git a/ordr3/resources.py b/ordr3/resources.py index 7ea69b5..9c6b7ce 100644 --- a/ordr3/resources.py +++ b/ordr3/resources.py @@ -2,7 +2,7 @@ import abc -from pyramid.security import Allow, Everyone +from pyramid.security import Allow, Everyone, Authenticated from sqlalchemy.inspection import inspect @@ -61,6 +61,7 @@ class Root(BaseResource): (Allow, Everyone, "logout"), (Allow, Everyone, "registration"), (Allow, Everyone, "view"), + (Allow, Authenticated, "account"), ] diff --git a/ordr3/schemas/account.py b/ordr3/schemas/account.py index 3942889..2556064 100644 --- a/ordr3/schemas/account.py +++ b/ordr3/schemas/account.py @@ -114,3 +114,30 @@ class ResetPasswordSchema(CSRFSchema): } settings.update(override) return super().as_form(request, **settings) + + +class MyAccountSchema(CSRFSchema): + """ edit an account """ + + user_name = colander.SchemaNode( + colander.String(), + widget=deform.widget.TextInputWidget( + template="textinput_disabled.pt", css_class="o3-reg-username" + ), + ) + 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", "Cancel"), + "css_class": "form-horizontal registration", + } + settings.update(override) + return super().as_form(request, **settings) diff --git a/ordr3/templates/account/myaccount.jinja2 b/ordr3/templates/account/myaccount.jinja2 new file mode 100644 index 0000000..94bd92f --- /dev/null +++ b/ordr3/templates/account/myaccount.jinja2 @@ -0,0 +1,29 @@ +{% extends "ordr3:templates/layout_small.jinja2" %} + +{% block subtitle %} My Account {% endblock subtitle %} + +{% block content %} + +
+
+
+
+
+
+

Ordr

+
+
+
Edit your account
+ {{form.render()|safe}} +
+

+ Change Password +

+
+
+
+
+
+
+ +{% endblock content %} diff --git a/ordr3/views/account.py b/ordr3/views/account.py index ce551fb..7d4d766 100644 --- a/ordr3/views/account.py +++ b/ordr3/views/account.py @@ -8,7 +8,7 @@ from pyramid.httpexceptions import HTTPFound from .. import models, security, services from ..repo import RepoItemNotFound -from ..events import PasswordResetEmail +from ..events import FlashMessage, PasswordResetEmail from ..schemas import account @@ -225,4 +225,62 @@ def reset_password(context, request): ) def password_reseted(context, request): return {} - # http://localhost:6543/reset?t=69e24c08-1cb2-4656-987a-4791175f3368 + + +@view_config( + context="ordr3:resources.Root", + name="myaccount", + permission="account", + request_method="GET", + renderer="ordr3:templates/account/myaccount.jinja2", +) +def myaccount(context, request): + form = account.MyAccountSchema.as_form(request) + form_data = { + "user_name": request.user.username, + "first_name": request.user.first_name, + "last_name": request.user.last_name, + "email": request.user.email, + } + form.set_appstruct(form_data) + return {"form": form} + + +@view_config( + context="ordr3:resources.Root", + name="myaccount", + permission="account", + request_method="POST", + renderer="ordr3:templates/account/myaccount.jinja2", +) +def edit_myaccount(context, request): + if "Cancel" in request.POST: + return HTTPFound(request.resource_path(request.root)) + + form = account.MyAccountSchema.as_form(request) + data = request.POST.items() + try: + appstruct = form.validate(data) + except deform.ValidationFailure: + return {"form": form} + + request.user.first_name = appstruct["first_name"] + request.user.last_name = appstruct["last_name"] + request.user.email = appstruct["email"] + + return HTTPFound(request.resource_path(request.root)) + + +@view_config( + context="ordr3:resources.Root", name="mypassword", permission="account", +) +def myaccount_reset_link(context, request): + token = services.create_token_for_user(request.repo, request.user) + request.emit(PasswordResetEmail(request.user, token.token)) + request.emit( + FlashMessage.info( + f"A password reset link has been sent to {request.user.email}." + ) + ) + + return HTTPFound(request.resource_path(request.root)) diff --git a/tests/test_repo.py b/tests/test_repo.py index 5a4c483..eb0e3dc 100644 --- a/tests/test_repo.py +++ b/tests/test_repo.py @@ -52,6 +52,20 @@ def example_users(): ] +@pytest.fixture() +def example_tokens(): + from ordr3.models import PasswordResetToken + from datetime import datetime, timedelta + + valid = datetime.utcnow() + timedelta(days=2) + invalid = datetime.utcnow() - timedelta(days=2) + + return [ + PasswordResetToken("valid_token", 1, valid), + PasswordResetToken("invalid_token", 2, invalid), + ] + + def test_sql_repo_add_order(session, example_orders): from ordr3.repo import SqlAlchemyRepository from ordr3.models import OrderItem @@ -76,6 +90,17 @@ def test_sql_repo_get_order(session, example_orders): assert example_orders[1] == repo.get_order(2) +def test_sql_repo_get_order_raises_exception(session, example_orders): + from ordr3.repo import SqlAlchemyRepository, RepoItemNotFound + + repo = SqlAlchemyRepository(session) + repo.add_order(example_orders[0]) + session.flush() + + with pytest.raises(RepoItemNotFound): + repo.get_order(2) + + def test_sql_repo_list_orders(session, example_orders): from ordr3.repo import SqlAlchemyRepository @@ -112,6 +137,17 @@ def test_sql_repo_get_user(session, example_users): assert example_users[1] == repo.get_user(2) +def test_sql_repo_get_user_raises_exception(session, example_users): + from ordr3.repo import SqlAlchemyRepository, RepoItemNotFound + + repo = SqlAlchemyRepository(session) + repo.add_user(example_users[0]) + session.flush() + + with pytest.raises(RepoItemNotFound): + repo.get_user(2) + + def test_sql_repo_get_user_by_username(session, example_users): from ordr3.repo import SqlAlchemyRepository @@ -123,6 +159,17 @@ def test_sql_repo_get_user_by_username(session, example_users): assert example_users[1] == repo.get_user_by_username("Me") +def test_sql_repo_get_user_by_username_exception(session, example_users): + from ordr3.repo import SqlAlchemyRepository, RepoItemNotFound + + repo = SqlAlchemyRepository(session) + repo.add_user(example_users[0]) + session.flush() + + with pytest.raises(RepoItemNotFound): + repo.get_user_by_username("unknown user name") + + def test_sql_repo_get_user_by_email(session, example_users): from ordr3.repo import SqlAlchemyRepository @@ -134,6 +181,17 @@ def test_sql_repo_get_user_by_email(session, example_users): assert example_users[1] == repo.get_user_by_email("jane.doe") +def test_sql_repo_get_user_by_email_exception(session, example_users): + from ordr3.repo import SqlAlchemyRepository, RepoItemNotFound + + repo = SqlAlchemyRepository(session) + repo.add_user(example_users[0]) + session.flush() + + with pytest.raises(RepoItemNotFound): + repo.get_user_by_email("unknown email") + + def test_sql_repo_list_users(session, example_users): from ordr3.repo import SqlAlchemyRepository @@ -157,3 +215,70 @@ def test_sql_search_vendor(session, example_users): assert repo.search_vendor("sa") == "Sigma Aldrich" assert repo.search_vendor("unknown") is None + + +def test_sql_repo_add_reset_token(session, example_tokens): + from ordr3.repo import SqlAlchemyRepository + from ordr3.models import PasswordResetToken + + repo = SqlAlchemyRepository(session) + repo.add_reset_token(example_tokens[0]) + session.flush() + + token = session.query(PasswordResetToken).first() + + assert token == example_tokens[0] + + +def test_sql_repo_delete_reset_token(session, example_tokens): + from ordr3.repo import SqlAlchemyRepository + from ordr3.models import PasswordResetToken + + repo = SqlAlchemyRepository(session) + repo.add_reset_token(example_tokens[0]) + repo.add_reset_token(example_tokens[1]) + session.flush() + + repo.delete_reset_token(example_tokens[0]) + + tokens = session.query(PasswordResetToken).all() + assert tokens == [example_tokens[1]] + + +def test_sql_repo_get_reset_token(session, example_tokens): + from ordr3.repo import SqlAlchemyRepository + + repo = SqlAlchemyRepository(session) + repo.add_reset_token(example_tokens[0]) + repo.add_reset_token(example_tokens[1]) + session.flush() + + token = repo.get_reset_token("valid_token") + + assert token == example_tokens[0] + + +def test_sql_repo_get_reset_token_raises_exception(session, example_tokens): + from ordr3.repo import SqlAlchemyRepository, RepoItemNotFound + + repo = SqlAlchemyRepository(session) + repo.add_reset_token(example_tokens[0]) + session.flush() + + with pytest.raises(RepoItemNotFound): + repo.get_reset_token("unknown token") + + +def test_clear_stale_reset_tokens(session, example_tokens): + from ordr3.repo import SqlAlchemyRepository + from ordr3.models import PasswordResetToken + + repo = SqlAlchemyRepository(session) + repo.add_reset_token(example_tokens[0]) + repo.add_reset_token(example_tokens[1]) + session.flush() + + repo.clear_stale_reset_tokens() + + tokens = session.query(PasswordResetToken).all() + assert tokens == [example_tokens[0]] diff --git a/tests/test_services.py b/tests/test_services.py index 3fc517f..9b6b2da 100644 --- a/tests/test_services.py +++ b/tests/test_services.py @@ -403,3 +403,17 @@ def test_get_user_from_reset_token_unknown_user(): result = services.get_user_from_reset_token(repo, "identifier") assert result is None + + +def test_create_token_for_user(): + from ordr3.services import create_token_for_user + from ordr3.models import PasswordResetToken, User + + repo = FakeOrderRepository(None) + user = User(*list("ABCDEFG")) + + result = create_token_for_user(repo, user) + + assert isinstance(result, PasswordResetToken) + assert result.user_id == "A" + assert repo._tokens == {result}