diff --git a/.gitignore b/.gitignore index 7292bf9..4da0b88 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ # ignore sqlite database ordr2.sqlite +# ignor pyramid_mailer.debug folder +mail/ # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/ordr2/events.py b/ordr2/events.py index 1f7635f..3218f93 100644 --- a/ordr2/events.py +++ b/ordr2/events.py @@ -19,12 +19,12 @@ class UserNotification(object): class AccountActivation(UserNotification): - subject='[ordr] Your account was activated', + subject='[ordr] Your account was activated' template = 'ordr2:templates/emails/activation.jinja2' class PasswordReset(UserNotification): - subject='[ordr] Password Reset', + subject='[ordr] Password Reset' template = 'ordr2:templates/emails/password_reset.jinja2' @@ -50,7 +50,7 @@ def notify_user(event): event.request ) message = Message( - subject=event.subject + subject=event.subject, sender=event.request.registry.settings['mail.default_sender'], recipients=[event.user.email], html=body diff --git a/ordr2/models/user.py b/ordr2/models/user.py index 400e675..8c25394 100644 --- a/ordr2/models/user.py +++ b/ordr2/models/user.py @@ -1,5 +1,6 @@ import bcrypt import enum +import uuid from collections import namedtuple from datetime import datetime @@ -40,6 +41,7 @@ class User(Base): email = Column(Text, nullable=False, unique=True) password_hash = Column(Text, nullable=False) role = Column(Enum(Role), nullable=False) + password_reset = Column(Text, nullable=False, default='') date_created = Column(Date, nullable=False, default=datetime.utcnow) @property @@ -75,6 +77,11 @@ class User(Base): return bcrypt.checkpw(password.encode('utf8'), expected_hash) return False + def generate_password_token(self): + token = uuid.uuid4() + self.password_reset = token.hex + return token.hex + def __str__(self): ''' string representation ''' return '{!s}'.format(self.user_name) diff --git a/ordr2/schemas/account.py b/ordr2/schemas/account.py index d013933..0f06534 100644 --- a/ordr2/schemas/account.py +++ b/ordr2/schemas/account.py @@ -81,9 +81,14 @@ class UserSchema(CSRFSchema): def as_form(cls, request, **override): settings = { 'buttons': ( - deform.Button('Save changes'), - deform.Button('Reset password', css_class='btn-danger'), - deform.Button('Cancel') + deform.Button(name='save', title='Save changes'), + deform.Button( + name='delete', + title='Delete user', + css_class='btn-danger' + ), + deform.Button(name='reset', title='Reset password'), + deform.Button(name='cancel', title='Cancel') ), 'css_class': 'form-horizontal', } diff --git a/ordr2/views/admin.py b/ordr2/views/admin.py index 4234e6c..7f2ce53 100644 --- a/ordr2/views/admin.py +++ b/ordr2/views/admin.py @@ -5,6 +5,7 @@ from pyramid.renderers import render from pyramid.security import remember, forget from pyramid.view import view_config +from ordr2.events import AccountActivation, PasswordReset from ordr2.models import User, Role from ordr2.schemas.account import UserSchema @@ -66,6 +67,7 @@ def change_column_view(context, request): renderer='ordr2:templates/admin/user_edit.jinja2' ) def user_account_form(context, request): + ''' display the user edit form ''' form = UserSchema.as_form(request) form_data = { 'user_name': context.model.user_name, @@ -79,34 +81,55 @@ def user_account_form(context, request): @view_config( - context='ordr2:resources.Account', - name='settingsx', - permission='settings', + context='ordr2:resources.UserAccount', + permission='edit', request_method='POST', - renderer='ordr2:templates/account/settings.jinja2' + renderer='ordr2:templates/admin/user_edit.jinja2' ) -def settingsx_form_processing(context, request): - ''' display the user settings form ''' +def user_account_form_processing(context, request): + ''' process the user edit form ''' - form = SettingsSchema.as_form(request) + form = UserSchema.as_form(request) data = request.POST.items() - try: - appstruct = form.validate(data) - except deform.ValidationFailure as e: - return {'form': form} - - # form validation sucessful, change settings - request.user.first_name = appstruct['general']['first_name'] - request.user.last_name = appstruct['general']['last_name'] - request.user.email = appstruct['general']['email'] - if appstruct['change_password']['new_password']: - request.user.set_password(appstruct['change_password']['new_password']) - if len(appstruct['change_password']['new_password']) < 8: - request.flash( - 'warning', - 'You should really consider using a longer password.' + if 'delete' in request.POST: + return HTTPFound(request.resource_url(context, 'delete')) + + elif 'save' in request.POST: + try: + appstruct = form.validate(data) + except deform.ValidationFailure as e: + return {'form': form} + + # form validation sucessful, change settings + was_active = context.model.is_active + context.model.first_name = appstruct['first_name'] + context.model.last_name = appstruct['last_name'] + context.model.email = appstruct['email'] + context.model.role = Role[appstruct['role']] + + if not was_active and context.model.is_active: + # user account was activated, notify user + event = AccountActivation(request, context.model) + request.registry.notify(event) + text = 'An activation email was sent to {}'.format( + appstruct['email'] ) + else: + text = '' - request.flash('success', 'Your account information has been updated.') + msg = 'User account {} updated.'.format( + context.model.user_name + ) + request.flash('success', msg, text) - return {'form': form} + elif 'reset' in request.POST: + token = context.model.generate_password_token() + event = PasswordReset(request, context.model, token) + request.registry.notify(event) + msg = 'Password reset mail sent to {}.'.format(context.model.email) + request.flash('success', msg) + + elif 'delete' in request.POST: + return HTTPFound(context, 'delete') + + return HTTPFound(context.__parent__.url())