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 %}
+
+
+
+
+
+
+
+
+
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}