misc: integration of journal in manager (#47155)
This commit is contained in:
parent
1cc04e3ad7
commit
8487d33cff
|
@ -78,13 +78,17 @@ class UserQuerySet(models.QuerySet):
|
|||
self = self.distinct()
|
||||
return self
|
||||
|
||||
def find_duplicates(self, first_name, last_name, birthdate=None):
|
||||
def find_duplicates(self, first_name=None, last_name=None, fullname=None, birthdate=None):
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(
|
||||
"SET pg_trgm.similarity_threshold = %f" % app_settings.A2_DUPLICATES_THRESHOLD
|
||||
)
|
||||
|
||||
name = '%s %s' % (first_name, last_name)
|
||||
if fullname is not None:
|
||||
name = fullname
|
||||
else:
|
||||
assert first_name is not None and last_name is not None
|
||||
name = '%s %s' % (first_name, last_name)
|
||||
name = unicodedata.normalize('NFKD', name).encode('ascii', 'ignore').decode('ascii').lower()
|
||||
|
||||
qs = self.filter(deleted__isnull=True)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# authentic2 - versatile identity manager
|
||||
# Copyright (C) 2010-2019 Entr'ouvert
|
||||
# Copyright (C) 2010-2020 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
|
|
|
@ -0,0 +1,503 @@
|
|||
# authentic2 - versatile identity manager
|
||||
# Copyright (C) 2010-2020 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from authentic2.journal_event_types import get_attributes_label, EventTypeWithService
|
||||
from authentic2.apps.journal.models import EventTypeDefinition
|
||||
from authentic2.apps.journal.utils import form_to_old_new
|
||||
|
||||
|
||||
from django_rbac.utils import get_role_model
|
||||
|
||||
User = get_user_model()
|
||||
Role = get_role_model()
|
||||
|
||||
|
||||
class ManagerUserCreation(EventTypeDefinition):
|
||||
name = 'manager.user.creation'
|
||||
label = _('user creation')
|
||||
|
||||
@classmethod
|
||||
def record(cls, user, session, form):
|
||||
super().record(user=user, session=session, references=[form.instance])
|
||||
|
||||
@classmethod
|
||||
def get_message(cls, event, context):
|
||||
(user,) = event.get_typed_references(User)
|
||||
# user journal page
|
||||
if context and context == user:
|
||||
return _('creation by administrator')
|
||||
elif user:
|
||||
# manager gloabal journal page
|
||||
return _('creation of user "%s"') % user.get_full_name()
|
||||
return super().get_message(event, context)
|
||||
|
||||
|
||||
class ManagerUserProfileEdit(EventTypeDefinition):
|
||||
name = 'manager.user.profile.edit'
|
||||
label = _('user profile edit')
|
||||
|
||||
@classmethod
|
||||
def record(cls, user, session, form):
|
||||
super().record(user=user, session=session, references=[form.instance], data=form_to_old_new(form))
|
||||
|
||||
@classmethod
|
||||
def get_message(cls, event, context):
|
||||
(user,) = event.get_typed_references(User)
|
||||
new = event.get_data('new') or {}
|
||||
edited_attributes = ', '.join(get_attributes_label(new)) or ''
|
||||
if context and context == user:
|
||||
return _('edit by administrator (%s)') % edited_attributes
|
||||
elif user:
|
||||
user_full_name = user.get_full_name()
|
||||
return _('edit of user "{0}" ({1})').format(user_full_name, edited_attributes)
|
||||
return super().get_message(event, context)
|
||||
|
||||
|
||||
class ManagerUserEmailChangeRequest(EventTypeDefinition):
|
||||
name = 'manager.user.email.change.request'
|
||||
label = _('email change request')
|
||||
|
||||
@classmethod
|
||||
def record(cls, user, session, form):
|
||||
data = {
|
||||
'old_email': form.instance.email,
|
||||
'email': form.cleaned_data.get('new_email'),
|
||||
}
|
||||
super().record(user=user, session=session, references=[form.instance], data=data)
|
||||
|
||||
@classmethod
|
||||
def get_message(cls, event, context):
|
||||
(user,) = event.get_typed_references(User)
|
||||
new_email = event.get_data('email')
|
||||
if context and context == user:
|
||||
return _('email change for email address "%s" requested by administrator') % new_email
|
||||
elif user:
|
||||
user_full_name = user.get_full_name()
|
||||
return _('email change of user "{0}" for email address "{1}"').format(user_full_name, new_email)
|
||||
return super().get_message(event, context)
|
||||
|
||||
|
||||
class ManagerUserPasswordChange(EventTypeDefinition):
|
||||
name = 'manager.user.password.change'
|
||||
label = _('user password change')
|
||||
|
||||
@classmethod
|
||||
def record(cls, user, session, form):
|
||||
data = {
|
||||
'generate_password': form.cleaned_data['generate_password'],
|
||||
'send_mail': form.cleaned_data['send_mail'],
|
||||
}
|
||||
super().record(user=user, session=session, references=[form.instance], data=data)
|
||||
|
||||
@classmethod
|
||||
def get_message(cls, event, context):
|
||||
(user,) = event.get_typed_references(User)
|
||||
send_mail = event.get_data('send_mail')
|
||||
if context and context == user:
|
||||
if send_mail:
|
||||
return _('password change by administrator and notification by mail')
|
||||
else:
|
||||
return _('password change by administrator')
|
||||
elif user:
|
||||
user_full_name = user.get_full_name()
|
||||
if send_mail:
|
||||
return _('password change of user "%s" and notification by mail') % user_full_name
|
||||
else:
|
||||
return _('password change of user "%s"') % user_full_name
|
||||
return super().get_message(event, context)
|
||||
|
||||
|
||||
class ManagerUserPasswordResetRequest(EventTypeDefinition):
|
||||
name = 'manager.user.password.reset.request'
|
||||
label = _('user password reset request')
|
||||
|
||||
@classmethod
|
||||
def record(cls, user, session, target_user):
|
||||
super().record(
|
||||
user=user, session=session, references=[target_user], data={'email': target_user.email}
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_message(cls, event, context):
|
||||
(user,) = event.get_typed_references(User)
|
||||
email = event.get_data('email')
|
||||
if context and context == user:
|
||||
return _('password reset request by administrator sent to "%s"') % email
|
||||
elif user:
|
||||
return _('password reset request of "{0}" sent to "{1}"').format(user.get_full_name(), email)
|
||||
return super().get_message(event, context)
|
||||
|
||||
|
||||
class ManagerUserPasswordChangeForce(EventTypeDefinition):
|
||||
name = 'manager.user.password.change.force'
|
||||
label = _('mandatory password change at next login set')
|
||||
|
||||
@classmethod
|
||||
def record(cls, user, session, target_user):
|
||||
super().record(user=user, session=session, references=[target_user])
|
||||
|
||||
@classmethod
|
||||
def get_message(cls, event, context):
|
||||
(user,) = event.get_typed_references(User)
|
||||
if context and context == user:
|
||||
return _('mandatory password change at next login set by administrator')
|
||||
elif user:
|
||||
return _('mandatory password change at next login set for user "%s"') % user.get_full_name()
|
||||
return super().get_message(event, context)
|
||||
|
||||
|
||||
class ManagerUserPasswordChangeUnforce(EventTypeDefinition):
|
||||
name = 'manager.user.password.change.unforce'
|
||||
label = _('mandatory password change at next login unset')
|
||||
|
||||
@classmethod
|
||||
def record(cls, user, session, target_user):
|
||||
super().record(user=user, session=session, references=[target_user])
|
||||
|
||||
@classmethod
|
||||
def get_message(cls, event, context):
|
||||
(user,) = event.get_typed_references(User)
|
||||
if context and context == user:
|
||||
return _('mandatory password change at next login unset by administrator')
|
||||
elif user:
|
||||
return _('mandatory password change at next login unset for user "%s"') % user.get_full_name()
|
||||
return super().get_message(event, context)
|
||||
|
||||
|
||||
class ManagerUserActivation(EventTypeDefinition):
|
||||
name = 'manager.user.activation'
|
||||
label = _('user activation')
|
||||
|
||||
@classmethod
|
||||
def record(cls, user, session, target_user):
|
||||
super().record(user=user, session=session, references=[target_user])
|
||||
|
||||
@classmethod
|
||||
def get_message(cls, event, context):
|
||||
(user,) = event.get_typed_references(User)
|
||||
if context and context == user:
|
||||
return _('activation by administrator')
|
||||
elif user:
|
||||
return _('activation of user "%s"') % user.get_full_name()
|
||||
return super().get_message(event, context)
|
||||
|
||||
|
||||
class ManagerUserDeactivation(EventTypeDefinition):
|
||||
name = 'manager.user.deactivation'
|
||||
label = _('user deactivation')
|
||||
|
||||
@classmethod
|
||||
def record(cls, user, session, target_user):
|
||||
super().record(user=user, session=session, references=[target_user])
|
||||
|
||||
@classmethod
|
||||
def get_message(cls, event, context):
|
||||
(user,) = event.get_typed_references(User)
|
||||
if context and context == user:
|
||||
return _('deactivation by administrator')
|
||||
elif user:
|
||||
return _('deactivation of user "%s"') % user.get_full_name()
|
||||
return super().get_message(event, context)
|
||||
|
||||
|
||||
class ManagerUserDeletion(EventTypeDefinition):
|
||||
name = 'manager.user.deletion'
|
||||
label = _('user deletion')
|
||||
|
||||
@classmethod
|
||||
def record(cls, user, session, target_user):
|
||||
super().record(user=user, session=session, references=[target_user])
|
||||
|
||||
@classmethod
|
||||
def get_message(cls, event, context):
|
||||
(user,) = event.get_typed_references(User)
|
||||
if context and context == user:
|
||||
return _('deletion by administrator')
|
||||
elif user:
|
||||
return _('deletion of user "%s"') % user.get_full_name()
|
||||
return super().get_message(event, context)
|
||||
|
||||
|
||||
class ManagerUserSSOAuthorizationDeletion(EventTypeWithService):
|
||||
name = 'manager.user.sso.authorization.deletion'
|
||||
label = _('delete authorization')
|
||||
|
||||
@classmethod
|
||||
def record(cls, user, session, service, target_user):
|
||||
super().record(user=user, session=session, service=service, references=[target_user])
|
||||
|
||||
@classmethod
|
||||
def get_message(cls, event, context):
|
||||
# first reference is to the service
|
||||
__, user = event.get_typed_references(None, User)
|
||||
service_name = cls.get_service_name(event)
|
||||
if context and context == user:
|
||||
return _('deletion of authorization of single sign on with "%s" by administrator') % service_name
|
||||
elif user:
|
||||
return _('deletion of authorization of single sign on with "%s" of user "%s"') % (
|
||||
service_name,
|
||||
user.get_full_name(),
|
||||
)
|
||||
return super().get_message(event, context)
|
||||
|
||||
|
||||
class RoleEventsMixin(EventTypeDefinition):
|
||||
@classmethod
|
||||
def record(self, user, session, role, references=None, data=None):
|
||||
references = references or []
|
||||
references = [role] + references
|
||||
data = data or {}
|
||||
data.update(
|
||||
{'role_name': str(role), 'role_uuid': role.uuid,}
|
||||
)
|
||||
super().record(
|
||||
user=user, session=session, references=references, data=data,
|
||||
)
|
||||
|
||||
|
||||
class ManagerRoleCreation(RoleEventsMixin):
|
||||
name = 'manager.role.creation'
|
||||
label = _('role creation')
|
||||
|
||||
@classmethod
|
||||
def get_message(cls, event, context):
|
||||
(role,) = event.get_typed_references(Role)
|
||||
role = role or event.get_data('role_name')
|
||||
if context != role:
|
||||
return _('creation of role "%s"') % role
|
||||
else:
|
||||
return _('creation')
|
||||
|
||||
|
||||
class ManagerRoleEdit(RoleEventsMixin):
|
||||
name = 'manager.role.edit'
|
||||
label = _('role edit')
|
||||
|
||||
@classmethod
|
||||
def record(cls, user, session, role, form):
|
||||
super().record(user=user, session=session, role=role, data=form_to_old_new(form))
|
||||
|
||||
@classmethod
|
||||
def get_message(cls, event, context):
|
||||
(role,) = event.get_typed_references(Role)
|
||||
role = role or event.get_data('role_name')
|
||||
new = event.get_data('new')
|
||||
edited_attributes = ', '.join(get_attributes_label(new)) or ''
|
||||
if context != role:
|
||||
return _('edit of role "%s" (%s)') % (role, edited_attributes)
|
||||
else:
|
||||
return _('edit (%s)') % edited_attributes
|
||||
|
||||
|
||||
class ManagerRoleDeletion(RoleEventsMixin):
|
||||
name = 'manager.role.deletion'
|
||||
label = _('role deletion')
|
||||
|
||||
@classmethod
|
||||
def get_message(cls, event, context):
|
||||
(role,) = event.get_typed_references(Role)
|
||||
role = role or event.get_data('role_name')
|
||||
if context != role:
|
||||
return _('deletion of role "%s"') % role
|
||||
else:
|
||||
return _('deletion')
|
||||
|
||||
|
||||
class ManagerRoleMembershipGrant(RoleEventsMixin):
|
||||
name = 'manager.role.membership.grant'
|
||||
label = _('role membership grant')
|
||||
|
||||
@classmethod
|
||||
def record(cls, user, session, role, member):
|
||||
data = {'member_name': member.get_full_name()}
|
||||
super().record(user=user, session=session, role=role, references=[member], data=data)
|
||||
|
||||
@classmethod
|
||||
def get_message(cls, event, context):
|
||||
role, member = event.get_typed_references(Role, User)
|
||||
role = role or event.get_data('role_name')
|
||||
member = member or event.get_data('member_name')
|
||||
if context == member:
|
||||
return _('membership grant in role "%s"') % role
|
||||
elif context == role:
|
||||
return _('membership grant to user "%s"') % member
|
||||
else:
|
||||
return _('membership grant to user "{member}" in role "{role}"').format(member=member, role=role)
|
||||
|
||||
|
||||
class ManagerRoleMembershipRemoval(RoleEventsMixin):
|
||||
name = 'manager.role.membership.removal'
|
||||
label = _('role membership removal')
|
||||
|
||||
@classmethod
|
||||
def record(cls, user, session, role, member):
|
||||
data = {'member_name': member.get_full_name()}
|
||||
super().record(user=user, session=session, role=role, references=[member], data=data)
|
||||
|
||||
@classmethod
|
||||
def get_message(cls, event, context):
|
||||
role, member = event.get_typed_references(Role, User)
|
||||
role = role or event.get_data('role_name')
|
||||
member = member or event.get_data('member_name')
|
||||
if context == member:
|
||||
return _('membership removal from role "%s"') % role
|
||||
elif context == role:
|
||||
return _('membership removal of user "%s"') % member
|
||||
else:
|
||||
return _('membership removal of user "{member}" from role "{role}"').format(
|
||||
member=member, role=role
|
||||
)
|
||||
|
||||
|
||||
class ManagerRoleInheritanceAddition(RoleEventsMixin):
|
||||
name = 'manager.role.inheritance.addition'
|
||||
label = _('role inheritance addition')
|
||||
|
||||
@classmethod
|
||||
def record(cls, user, session, parent, child):
|
||||
data = {
|
||||
'child_name': str(child),
|
||||
'child_uuid': child.uuid,
|
||||
}
|
||||
super().record(user=user, session=session, role=parent, references=[child], data=data)
|
||||
|
||||
@classmethod
|
||||
def get_message(cls, event, context):
|
||||
parent, child = event.get_typed_references(Role, Role)
|
||||
parent = parent or event.get_data('role_name')
|
||||
child = child or event.get_data('child_name')
|
||||
if context == child:
|
||||
return _('inheritance addition from parent role "%s"') % parent
|
||||
elif context == parent:
|
||||
return _('inheritance addition to child role "%s"') % child
|
||||
else:
|
||||
return _('inheritance addition from parent role "{parent}" to child role "{child}"').format(
|
||||
parent=parent, child=child
|
||||
)
|
||||
|
||||
|
||||
class ManagerRoleInheritanceRemoval(ManagerRoleInheritanceAddition):
|
||||
name = 'manager.role.inheritance.removal'
|
||||
label = _('role inheritance removal')
|
||||
|
||||
@classmethod
|
||||
def get_message(cls, event, context):
|
||||
parent, child = event.get_typed_references(Role, Role)
|
||||
parent = parent or event.get_data('role_name')
|
||||
child = child or event.get_data('child_name')
|
||||
if context == child:
|
||||
return _('inheritance removal from parent role "%s"') % parent
|
||||
elif context == parent:
|
||||
return _('inheritance removal to child role "%s"') % child
|
||||
else:
|
||||
return _('inheritance removal from parent role "{parent}" to child role "{child}"').format(
|
||||
parent=parent, child=child
|
||||
)
|
||||
|
||||
|
||||
class ManagerRoleAdministratorRoleAddition(RoleEventsMixin):
|
||||
name = 'manager.role.administrator.role.addition'
|
||||
label = _('role administrator role addition')
|
||||
|
||||
@classmethod
|
||||
def record(cls, user, session, role, admin_role):
|
||||
data = {
|
||||
'admin_role_name': str(admin_role),
|
||||
'admin_role_uuid': admin_role.uuid,
|
||||
}
|
||||
super().record(user=user, session=session, role=role, references=[admin_role], data=data)
|
||||
|
||||
@classmethod
|
||||
def get_message(cls, event, context):
|
||||
role, admin_role = event.get_typed_references(Role, Role)
|
||||
role = role or event.get_data('role_name')
|
||||
admin_role = admin_role or event.get('admin_role_name')
|
||||
if context == role:
|
||||
return _('addition of role "%s" as administrator') % admin_role
|
||||
elif context == admin_role:
|
||||
return _('addition as administrator of role "%s"') % role
|
||||
else:
|
||||
return _('addition of role "{admin_role}" as administrator of role "{role}"').format(
|
||||
admin_role=admin_role, role=role
|
||||
)
|
||||
|
||||
|
||||
class ManagerRoleAdministratorRoleRemoval(ManagerRoleAdministratorRoleAddition):
|
||||
name = 'manager.role.administrator.role.removal'
|
||||
label = _('role administrator role removal')
|
||||
|
||||
@classmethod
|
||||
def get_message(cls, event, context):
|
||||
role, admin_role = event.get_typed_references(Role, Role)
|
||||
role = role or event.get_data('role_name')
|
||||
admin_role = admin_role or event.get('admin_role_name')
|
||||
if context == role:
|
||||
return _('removal of role "%s" as administrator') % admin_role
|
||||
elif context == admin_role:
|
||||
return _('removal as administrator of role "%s"') % role
|
||||
else:
|
||||
return _('removal of role "{admin_role}" as administrator of role "{role}"').format(
|
||||
admin_role=admin_role, role=role
|
||||
)
|
||||
|
||||
|
||||
class ManagerRoleAdministratorUserAddition(RoleEventsMixin):
|
||||
name = 'manager.role.administrator.user.addition'
|
||||
label = _('role administrator user addition')
|
||||
|
||||
@classmethod
|
||||
def record(cls, user, session, role, admin_user):
|
||||
data = {
|
||||
'admin_user_name': admin_user.get_full_name(),
|
||||
'admin_user_uuid': admin_user.uuid,
|
||||
}
|
||||
super().record(user=user, session=session, role=role, references=[admin_user], data=data)
|
||||
|
||||
@classmethod
|
||||
def get_message(cls, event, context):
|
||||
role, admin_user = event.get_typed_references(Role, User)
|
||||
role = role or event.get_data('role_name')
|
||||
admin_user = admin_user or event.get_data('admin_user_name')
|
||||
if context == role:
|
||||
return _('addition of user "%s" as administrator') % admin_user
|
||||
elif context == admin_user:
|
||||
return _('addition as administrator of role "%s"') % role
|
||||
else:
|
||||
return _('addition of user "{admin_user}" as administrator of role "{role}"').format(
|
||||
admin_user=admin_user, role=role
|
||||
)
|
||||
|
||||
|
||||
class ManagerRoleAdministratorUserRemoval(ManagerRoleAdministratorUserAddition):
|
||||
name = 'manager.role.administrator.user.removal'
|
||||
label = _('role administrator user removal')
|
||||
|
||||
@classmethod
|
||||
def get_message(cls, event, context):
|
||||
role, admin_user = event.get_typed_references(Role, User)
|
||||
role = role or event.get_data('role_name')
|
||||
admin_user = admin_user or event.get_data('admin_user_name')
|
||||
if context == role:
|
||||
return _('removal of user "%s" as administrator') % admin_user
|
||||
elif context == admin_user:
|
||||
return _('removal as administrator of role "%s"') % role
|
||||
else:
|
||||
return _('removal of user "{admin_user}" as administrator of role "{role}"').format(
|
||||
admin_user=admin_user, role=role
|
||||
)
|
|
@ -0,0 +1,98 @@
|
|||
# authentic2 - versatile identity manager
|
||||
# Copyright (C) 2010-2020 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import uuid
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.exceptions import PermissionDenied, ValidationError
|
||||
from django.core.validators import EmailValidator
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from authentic2.apps.journal.forms import JournalForm
|
||||
from authentic2.apps.journal.search_engine import JournalSearchEngine
|
||||
from authentic2.apps.journal.views import JournalView
|
||||
|
||||
from . import views
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class JournalSearchEngine(JournalSearchEngine):
|
||||
def search_by_uuid(self, lexem):
|
||||
# by user uuid
|
||||
try:
|
||||
user_uuid = uuid.UUID(lexem)
|
||||
except ValueError:
|
||||
yield self.q_false
|
||||
else:
|
||||
yield Q(user__uuid=user_uuid.hex)
|
||||
|
||||
search_by_uuid.documentation = _(
|
||||
'''\
|
||||
You can use <tt>uuid:1234</tt> to find all events related \
|
||||
to user whose UUID is <tt>1234</tt>.'''
|
||||
)
|
||||
unmatched = None
|
||||
|
||||
def lexem_queries(self, lexem):
|
||||
queries = list(super().lexem_queries(lexem))
|
||||
if queries:
|
||||
yield from queries
|
||||
elif '@' in lexem:
|
||||
# fallback for raw email
|
||||
try:
|
||||
EmailValidator(lexem)
|
||||
except ValidationError:
|
||||
pass
|
||||
else:
|
||||
yield from super().lexem_queries('email:' + lexem)
|
||||
yield from super().lexem_queries('username:' + lexem)
|
||||
|
||||
def unmatched_lexems_query(self, lexems):
|
||||
fullname = ' '.join(lexem.strip() for lexem in lexems if lexem.strip())
|
||||
if fullname:
|
||||
users = User.objects.find_duplicates(fullname=fullname)
|
||||
return self.query_for_users(users)
|
||||
|
||||
|
||||
class JournalForm(JournalForm):
|
||||
search_engine_class = JournalSearchEngine
|
||||
|
||||
|
||||
class BaseJournalView(views.TitleMixin, views.MediaMixin, views.MultipleOUMixin, JournalView):
|
||||
template_name = 'authentic2/manager/journal.html'
|
||||
title = _('Journal')
|
||||
form_class = JournalForm
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
date_hierarchy = ctx['date_hierarchy']
|
||||
if date_hierarchy.title:
|
||||
ctx['title'] = _('Journal of %s') % date_hierarchy.title
|
||||
return ctx
|
||||
|
||||
|
||||
class GlobalJournalView(BaseJournalView):
|
||||
template_name = 'authentic2/manager/journal.html'
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
if not request.user.is_superuser:
|
||||
raise PermissionDenied
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
|
||||
journal = GlobalJournalView.as_view()
|
|
@ -17,6 +17,7 @@
|
|||
import json
|
||||
|
||||
from django.core.exceptions import PermissionDenied, ValidationError
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.urls import reverse
|
||||
from django.views.generic import FormView, TemplateView
|
||||
|
@ -27,15 +28,20 @@ from django.db import transaction
|
|||
from django.db.models.query import Q, Prefetch
|
||||
from django.db.models import Count, F
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.shortcuts import get_object_or_404
|
||||
|
||||
from django_rbac.utils import get_role_model, get_permission_model, get_ou_model
|
||||
|
||||
from authentic2.forms.profile import modelform_factory
|
||||
from authentic2.utils import redirect
|
||||
from authentic2 import hooks, data_transfer
|
||||
from authentic2.apps.journal.views import JournalViewWithContext
|
||||
|
||||
from . import tables, views, resources, forms, app_settings
|
||||
from .utils import has_show_username
|
||||
from .journal_views import BaseJournalView
|
||||
|
||||
OU = get_ou_model()
|
||||
|
||||
|
||||
class RolesMixin(object):
|
||||
|
@ -48,7 +54,7 @@ class RolesMixin(object):
|
|||
Permission = get_permission_model()
|
||||
permission_ct = ContentType.objects.get_for_model(Permission)
|
||||
ct_ct = ContentType.objects.get_for_model(ContentType)
|
||||
ou_ct = ContentType.objects.get_for_model(get_ou_model())
|
||||
ou_ct = ContentType.objects.get_for_model(OU)
|
||||
permission_qs = Permission.objects.filter(target_ct_id__in=[ct_ct.id, ou_ct.id]) \
|
||||
.values_list('id', flat=True)
|
||||
# only non role-admin roles, they are accessed through the
|
||||
|
@ -61,7 +67,8 @@ class RolesMixin(object):
|
|||
return qs
|
||||
|
||||
|
||||
class RolesView(views.HideOUColumnMixin, RolesMixin, views.BaseTableView):
|
||||
class RolesView(views.SearchOUMixin, views.HideOUColumnMixin, RolesMixin,
|
||||
views.BaseTableView):
|
||||
template_name = 'authentic2/manager/roles.html'
|
||||
model = get_role_model()
|
||||
table_class = tables.RoleTable
|
||||
|
@ -105,6 +112,7 @@ class RoleAddView(views.BaseAddView):
|
|||
response = super(RoleAddView, self).form_valid(form)
|
||||
hooks.call_hooks('event', name='manager-add-role', user=self.request.user,
|
||||
instance=form.instance, form=form)
|
||||
self.request.journal.record('manager.role.creation', role=form.instance)
|
||||
return response
|
||||
|
||||
|
||||
|
@ -145,6 +153,7 @@ class RoleEditView(RoleViewMixin, views.BaseEditView):
|
|||
response = super(RoleEditView, self).form_valid(form)
|
||||
hooks.call_hooks('event', name='manager-edit-role', user=self.request.user,
|
||||
instance=form.instance, form=form)
|
||||
self.request.journal.record('manager.role.edit', role=form.instance, form=form)
|
||||
return response
|
||||
|
||||
edit = RoleEditView.as_view()
|
||||
|
@ -186,6 +195,7 @@ class RoleMembersView(views.HideOUColumnMixin, RoleViewMixin, views.BaseSubTable
|
|||
self.object.members.add(user)
|
||||
hooks.call_hooks('event', name='manager-add-role-member',
|
||||
user=self.request.user, role=self.object, member=user)
|
||||
self.request.journal.record('manager.role.membership.grant', role=self.object, member=user)
|
||||
elif action == 'remove':
|
||||
if not self.object.members.filter(pk=user.pk).exists():
|
||||
messages.warning(self.request, _('User was not in this role.'))
|
||||
|
@ -193,6 +203,7 @@ class RoleMembersView(views.HideOUColumnMixin, RoleViewMixin, views.BaseSubTable
|
|||
self.object.members.remove(user)
|
||||
hooks.call_hooks('event', name='manager-remove-role-member',
|
||||
user=self.request.user, role=self.object, member=user)
|
||||
self.request.journal.record('manager.role.membership.removal', role=self.object, member=user)
|
||||
else:
|
||||
messages.warning(self.request, _('You are not authorized'))
|
||||
return super(RoleMembersView, self).form_valid(form)
|
||||
|
@ -210,7 +221,7 @@ class RoleMembersView(views.HideOUColumnMixin, RoleViewMixin, views.BaseSubTable
|
|||
self.object.children(include_self=False, annotate=True))
|
||||
ctx['parents'] = views.filter_view(self.request, self.object.parents(
|
||||
include_self=False, annotate=True).order_by(F('ou').asc(nulls_first=True), 'name'))
|
||||
ctx['has_multiple_ou'] = get_ou_model().objects.count() > 1
|
||||
ctx['has_multiple_ou'] = OU.objects.count() > 1
|
||||
ctx['admin_roles'] = views.filter_view(self.request,
|
||||
self.object.get_admin_role().children(include_self=False,
|
||||
annotate=True))
|
||||
|
@ -247,11 +258,12 @@ class RoleDeleteView(RoleViewMixin, views.BaseDeleteView):
|
|||
def get_success_url(self):
|
||||
return reverse('a2-manager-roles')
|
||||
|
||||
def form_valid(self, form):
|
||||
response = super(RoleDeleteView, self).form_valid(form)
|
||||
hooks.call_hooks('event', name='manager-delete-role', user=self.request.user,
|
||||
role=form.instance)
|
||||
return response
|
||||
def delete(self, request, *args, **kwargs):
|
||||
role = self.get_object()
|
||||
|
||||
hooks.call_hooks('event', name='manager-delete-role', user=request.user, role=role)
|
||||
self.request.journal.record('manager.role.deletion', role=role)
|
||||
return super(RoleDeleteView, self).delete(request, *args, **kwargs)
|
||||
|
||||
delete = RoleDeleteView.as_view()
|
||||
|
||||
|
@ -331,6 +343,7 @@ class RoleAddChildView(views.AjaxFormViewMixin, views.TitleMixin,
|
|||
parent.add_child(role)
|
||||
hooks.call_hooks('event', name='manager-add-child-role', user=self.request.user,
|
||||
parent=parent, child=role)
|
||||
self.request.journal.record('manager.role.inheritance.addition', parent=parent, child=role)
|
||||
return super(RoleAddChildView, self).form_valid(form)
|
||||
|
||||
add_child = RoleAddChildView.as_view()
|
||||
|
@ -356,6 +369,7 @@ class RoleAddParentView(views.AjaxFormViewMixin, views.TitleMixin,
|
|||
child.add_parent(role)
|
||||
hooks.call_hooks('event', name='manager-add-child-role', user=self.request.user,
|
||||
parent=role, child=child)
|
||||
self.request.journal.record('manager.role.inheritance.addition', parent=role, child=child)
|
||||
return super(RoleAddParentView, self).form_valid(form)
|
||||
|
||||
add_parent = RoleAddParentView.as_view()
|
||||
|
@ -383,6 +397,7 @@ class RoleRemoveChildView(views.AjaxFormViewMixin, SingleObjectMixin,
|
|||
self.object.remove_child(self.child)
|
||||
hooks.call_hooks('event', name='manager-remove-child-role', user=self.request.user,
|
||||
parent=self.object, child=self.child)
|
||||
self.request.journal.record('manager.role.inheritance.removal', parent=self.object, child=self.child)
|
||||
return redirect(self.request, self.success_url)
|
||||
|
||||
remove_child = RoleRemoveChildView.as_view()
|
||||
|
@ -413,6 +428,7 @@ class RoleRemoveParentView(views.AjaxFormViewMixin, SingleObjectMixin,
|
|||
self.object.remove_parent(self.parent)
|
||||
hooks.call_hooks('event', name='manager-remove-child-role', user=self.request.user,
|
||||
parent=self.parent, child=self.object)
|
||||
self.request.journal.record('manager.role.inheritance.removal', parent=self.parent, child=self.object)
|
||||
return redirect(self.request, self.success_url)
|
||||
|
||||
remove_parent = RoleRemoveParentView.as_view()
|
||||
|
@ -438,6 +454,8 @@ class RoleAddAdminRoleView(views.AjaxFormViewMixin, views.TitleMixin,
|
|||
administered_role.get_admin_role().add_child(role)
|
||||
hooks.call_hooks('event', name='manager-add-admin-role', user=self.request.user,
|
||||
role=administered_role, admin_role=role)
|
||||
self.request.journal.record('manager.role.administrator.role.addition',
|
||||
role=administered_role, admin_role=role)
|
||||
return super(RoleAddAdminRoleView, self).form_valid(form)
|
||||
|
||||
add_admin_role = RoleAddAdminRoleView.as_view()
|
||||
|
@ -466,6 +484,8 @@ class RoleRemoveAdminRoleView(views.TitleMixin, views.AjaxFormViewMixin,
|
|||
self.object.get_admin_role().remove_child(self.child)
|
||||
hooks.call_hooks('event', name='manager-remove-admin-role',
|
||||
user=self.request.user, role=self.object, admin_role=self.child)
|
||||
self.request.journal.record('manager.role.administrator.role.removal',
|
||||
role=self.object, admin_role=self.child)
|
||||
return redirect(self.request, self.success_url)
|
||||
|
||||
remove_admin_role = RoleRemoveAdminRoleView.as_view()
|
||||
|
@ -491,6 +511,8 @@ class RoleAddAdminUserView(views.AjaxFormViewMixin, views.TitleMixin,
|
|||
administered_role.get_admin_role().members.add(user)
|
||||
hooks.call_hooks('event', name='manager-add-admin-role-user', user=self.request.user,
|
||||
role=administered_role, admin=user)
|
||||
self.request.journal.record('manager.role.administrator.user.addition',
|
||||
role=administered_role, admin_user=user)
|
||||
return super(RoleAddAdminUserView, self).form_valid(form)
|
||||
|
||||
add_admin_user = RoleAddAdminUserView.as_view()
|
||||
|
@ -519,6 +541,8 @@ class RoleRemoveAdminUserView(views.TitleMixin, views.AjaxFormViewMixin,
|
|||
self.object.get_admin_role().members.remove(self.user)
|
||||
hooks.call_hooks('event', name='remove-remove-admin-role-user', user=self.request.user,
|
||||
role=self.object, admin=self.user)
|
||||
self.request.journal.record('manager.role.administrator.user.removal',
|
||||
role=self.object, admin_user=self.user)
|
||||
return redirect(self.request, self.success_url)
|
||||
|
||||
remove_admin_user = RoleRemoveAdminUserView.as_view()
|
||||
|
@ -564,3 +588,33 @@ class RolesImportView(views.PermissionMixin, views.TitleMixin, views.MediaMixin,
|
|||
|
||||
|
||||
roles_import = RolesImportView.as_view()
|
||||
|
||||
|
||||
class RoleJournal(views.PermissionMixin, JournalViewWithContext, BaseJournalView):
|
||||
template_name = 'authentic2/manager/role_journal.html'
|
||||
permissions = ['a2_rbac.view_role']
|
||||
title = _('Journal')
|
||||
|
||||
@cached_property
|
||||
def context(self):
|
||||
return get_object_or_404(get_role_model(), pk=self.kwargs['pk'])
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['object'] = self.context
|
||||
return ctx
|
||||
|
||||
journal = RoleJournal.as_view()
|
||||
|
||||
|
||||
class RolesJournal(views.SearchOUMixin, views.PermissionMixin, JournalViewWithContext, BaseJournalView):
|
||||
template_name = 'authentic2/manager/roles_journal.html'
|
||||
permissions = ['a2_rbac.view_role']
|
||||
title = _('Journal')
|
||||
|
||||
@cached_property
|
||||
def context(self):
|
||||
return get_role_model()
|
||||
|
||||
|
||||
roles_journal = RolesJournal.as_view()
|
||||
|
|
|
@ -219,5 +219,20 @@
|
|||
window.history.replaceState({'form': '#search-form', 'values': $('#search-form').values()}, window.document.title, window.location.href)
|
||||
}
|
||||
$(window.document).trigger('gadjo:content-update');
|
||||
function FitToContent(id, maxHeight)
|
||||
{
|
||||
var text = id && id.style ? id : document.getElementById(id);
|
||||
if ( !text )
|
||||
return;
|
||||
|
||||
}
|
||||
$('textarea.js-autoresize').each(function () {
|
||||
this.setAttribute('style', 'height:' + (this.scrollHeight) + 'px;overflow-y:hidden;');
|
||||
})
|
||||
$(document).on('input', 'textarea.js-autoresize', function () {
|
||||
this.style.height = 'auto';
|
||||
this.style.height = (this.scrollHeight) + 'px';
|
||||
return true;
|
||||
});
|
||||
});
|
||||
})(jQuery, window)
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
{% extends "authentic2/manager/base.html" %}
|
||||
{% load i18n gadjo %}
|
||||
|
||||
{% block page-title %}{{ block.super }} - {% trans "Journal" %}{% endblock %}
|
||||
|
||||
{% block breadcrumb %}
|
||||
{{ block.super }}
|
||||
{% block breadcrumb-before-title %}
|
||||
{% endblock %}
|
||||
<a href="#">{% trans 'Journal' %}</a>
|
||||
{% for caption, url in date_hierarchy.back_urls %}
|
||||
<a href="{{ url }}">{{ caption }}</a>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
<aside id="sidebar">
|
||||
<h3>{% trans "Search" context "title" %}</h3>
|
||||
<div class="journal-list--search-form">
|
||||
<form action="{{ form.url }}">
|
||||
{{ form|with_template }}
|
||||
<button>{% trans "Search" %}</button>
|
||||
</form>
|
||||
</div>
|
||||
{% if date_hierarchy.choice_name %}
|
||||
<h4>{{ date_hierarchy.choice_name }}</h4>
|
||||
<p>
|
||||
{% for caption, url in date_hierarchy.choice_urls %}
|
||||
<a href="{{ url }}">{{ caption }}</a>
|
||||
{% endfor %}
|
||||
</p>
|
||||
{% endif %}
|
||||
<div class="documentation">
|
||||
{% for documentation in form.search_engine_class.documentation %}
|
||||
<p>{{ documentation|safe }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</aside>
|
||||
{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
{% include "journal/event_list.html" %}
|
||||
{% endblock %}
|
|
@ -6,4 +6,8 @@
|
|||
{% block breadcrumb %}
|
||||
{{ block.super }}
|
||||
<a href="{% url 'a2-manager-roles' %}">{% trans 'Roles' %}</a>
|
||||
{% firstof ou object.ou as current_ou %}
|
||||
{% if multiple_ou and current_ou %}
|
||||
<a href="{% url 'a2-manager-roles' %}?search-ou={{ current_ou.id }}">{{ current_ou }}</a>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
{% extends "authentic2/manager/journal.html" %}
|
||||
{% load i18n gadjo %}
|
||||
|
||||
{% block breadcrumb-before-title %}
|
||||
<a href="{% url 'a2-manager-roles' %}">{% trans 'Roles' %}</a>
|
||||
{% if multiple_ou and object.ou %}
|
||||
<a href="../?search-ou={{ object.ou.pk }}">{{ object.ou }}</a>
|
||||
{% endif %}
|
||||
<a href="../">{{ object }}</a>
|
||||
{% endblock %}
|
|
@ -3,9 +3,6 @@
|
|||
|
||||
{% block breadcrumb %}
|
||||
{{ block.super }}
|
||||
{% if multiple_ou and object.ou %}
|
||||
<a href="../?search-ou={{ object.ou.pk }}">{{ object.ou }}</a>
|
||||
{% endif %}
|
||||
<a href="#">{{ object }}</a>
|
||||
{% endblock %}
|
||||
|
||||
|
@ -19,6 +16,7 @@
|
|||
{% block appbar %}
|
||||
{{ block.super }}
|
||||
<span class="actions">
|
||||
<a class="extra-actions-menu-opener"></a>
|
||||
{% if not object.is_internal and view.can_delete %}
|
||||
<a rel="popup" href="{% url "a2-manager-role-delete" pk=object.pk %}">{% trans "Delete" %}</a>
|
||||
{% else %}
|
||||
|
@ -36,6 +34,9 @@
|
|||
{% if perms.a2_rbac.admin_permission %}
|
||||
<a href="{% url "a2-manager-role-permissions" pk=object.pk %}">{% trans "Permissions" %}</a>
|
||||
{% endif %}
|
||||
<ul class="extra-actions-menu">
|
||||
<li><a href="{% url "a2-manager-role-journal" pk=object.pk %}">{% trans "Journal" %}</a></li>
|
||||
</ul>
|
||||
</span>
|
||||
{% endblock %}
|
||||
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
|
||||
{% block page-title %}{{ block.super }} - {% trans "Roles" %}{% endblock %}
|
||||
|
||||
{% block page_title %}{% if multiple_ou and ou %}{{ ou }}{% else %}{{ block.super }}{% endif %}{% endblock %}
|
||||
|
||||
{% block appbar %}
|
||||
{{ block.super }}
|
||||
<span class="actions">
|
||||
|
@ -13,6 +15,7 @@
|
|||
<a href="#" class="disabled" rel="popup">{% trans "Add role" %}</a>
|
||||
{% endif %}
|
||||
<ul class="extra-actions-menu">
|
||||
<li><a href="{% url "a2-manager-roles-journal" %}{% if multiple_ou and ou %}?search-ou={{ ou.id }}{% endif %}">{% trans "Journal" %}</a></li>
|
||||
<li><a download href="{% url 'a2-manager-roles-export' format="json" %}?{{ request.GET.urlencode }}">{% trans 'Export' %}</a></li>
|
||||
{% if view.can_add %}
|
||||
<li><a href="{% url 'a2-manager-roles-import' %}?{{ request.GET.urlencode }}" rel="popup">{% trans 'Import' %}</a></li>
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
{% extends "authentic2/manager/journal.html" %}
|
||||
{% load i18n gadjo %}
|
||||
|
||||
{% block breadcrumb-before-title %}
|
||||
<a href="{% url 'a2-manager-roles' %}">{% trans 'Roles' %}</a>
|
||||
{% if multiple_ou and ou %}
|
||||
<a href="../?search-ou={{ ou.id }}">{{ ou }}</a>
|
||||
{% endif %}
|
||||
{% endblock %}
|
|
@ -22,6 +22,7 @@
|
|||
{% if view.is_oidc_services %}
|
||||
<li><a href="{% url "a2-manager-user-authorizations" pk=object.pk %}">{% trans "Consents" %}</a></li>
|
||||
{% endif %}
|
||||
<li><a href="{% url "a2-manager-user-journal" pk=object.pk %}">{% trans "Journal" %}</a></li>
|
||||
</ul>
|
||||
</span>
|
||||
{% endblock %}
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
{% extends "authentic2/manager/journal.html" %}
|
||||
{% load i18n gadjo %}
|
||||
|
||||
{% block breadcrumb-before-title %}
|
||||
<a href="{% url 'a2-manager-users' %}">{% trans 'Users' %}</a>
|
||||
{% if multiple_ou and object.ou %}
|
||||
<a href="../?search-ou={{ object.ou.pk }}">{{ object.ou }}</a>
|
||||
{% endif %}
|
||||
<a href="{% url 'a2-manager-user-detail' pk=object.pk %}">{{ object.get_full_name }}</a>
|
||||
{% endblock %}
|
|
@ -19,7 +19,7 @@ from django.conf.urls import url
|
|||
from django.views.i18n import JavaScriptCatalog
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.utils.functional import lazy
|
||||
from . import views, role_views, ou_views, user_views, service_views
|
||||
from . import views, role_views, ou_views, user_views, service_views, journal_views
|
||||
from ..decorators import required
|
||||
from authentic2 import utils
|
||||
|
||||
|
@ -67,6 +67,12 @@ urlpatterns = required(
|
|||
name='a2-manager-user-change-email'),
|
||||
url(r'^users/(?P<pk>\d+)/su/$', user_views.su,
|
||||
name='a2-manager-user-su'),
|
||||
url(r'^users/(?P<pk>\d+)/authorizations/$',
|
||||
user_views.user_authorizations,
|
||||
name='a2-manager-user-authorizations'),
|
||||
url(r'^users/(?P<pk>\d+)/journal/$',
|
||||
user_views.user_journal,
|
||||
name='a2-manager-user-journal'),
|
||||
# by uuid
|
||||
url(r'^users/uuid:(?P<slug>[a-z0-9]+)/$', user_views.user_detail,
|
||||
name='a2-manager-user-by-uuid-detail'),
|
||||
|
@ -81,9 +87,9 @@ urlpatterns = required(
|
|||
url(r'^users/uuid:(?P<slug>[a-z0-9]+)/change-email/$',
|
||||
user_views.user_change_email,
|
||||
name='a2-manager-user-by-uuid-change-email'),
|
||||
url(r'^users/(?P<pk>\d+)/authorizations/$',
|
||||
user_views.user_authorizations,
|
||||
name='a2-manager-user-authorizations'),
|
||||
url(r'^users/uuid:(?P<slug>[a-z0-9]+)/journal/$',
|
||||
user_views.user_journal,
|
||||
name='a2-manager-user-journal'),
|
||||
|
||||
# Authentic2 roles
|
||||
url(r'^roles/$', role_views.listing,
|
||||
|
@ -94,6 +100,8 @@ urlpatterns = required(
|
|||
name='a2-manager-role-add'),
|
||||
url(r'^roles/export/(?P<format>csv|json)/$',
|
||||
role_views.export, name='a2-manager-roles-export'),
|
||||
url(r'^roles/journal/$', role_views.roles_journal,
|
||||
name='a2-manager-roles-journal'),
|
||||
url(r'^roles/(?P<pk>\d+)/$', role_views.members,
|
||||
name='a2-manager-role-members'),
|
||||
url(r'^roles/(?P<pk>\d+)/add-child/$', role_views.add_child,
|
||||
|
@ -124,6 +132,8 @@ urlpatterns = required(
|
|||
name='a2-manager-role-edit'),
|
||||
url(r'^roles/(?P<pk>\d+)/permissions/$', role_views.permissions,
|
||||
name='a2-manager-role-permissions'),
|
||||
url(r'^roles/(?P<pk>\d+)/journal/$', role_views.journal,
|
||||
name='a2-manager-role-journal'),
|
||||
|
||||
|
||||
# Authentic2 organizational units
|
||||
|
@ -152,6 +162,10 @@ urlpatterns = required(
|
|||
url(r'^services/(?P<service_pk>\d+)/edit/$', service_views.edit,
|
||||
name='a2-manager-service-edit'),
|
||||
|
||||
# Journal
|
||||
url(r'^journal/$', journal_views.journal,
|
||||
name='a2-manager-journal'),
|
||||
|
||||
# backoffice menu as json
|
||||
url(r'^menu.json$', views.menu_json),
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ import collections
|
|||
import operator
|
||||
|
||||
from django.db import models
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import ugettext_lazy as _, pgettext_lazy, ugettext
|
||||
from django.utils.html import format_html
|
||||
from django.urls import reverse
|
||||
|
@ -33,6 +34,7 @@ from django.views.generic import FormView, TemplateView, DetailView
|
|||
from django.views.generic.edit import BaseFormView
|
||||
from django.views.generic.detail import SingleObjectMixin
|
||||
from django.http import Http404, FileResponse, HttpResponseRedirect
|
||||
from django.shortcuts import get_object_or_404
|
||||
|
||||
import tablib
|
||||
|
||||
|
@ -41,8 +43,9 @@ from authentic2.utils import send_password_reset_mail, redirect, select_next_url
|
|||
from authentic2.a2_rbac.utils import get_default_ou
|
||||
from authentic2 import hooks
|
||||
from authentic2_idp_oidc.models import OIDCAuthorization, OIDCClient
|
||||
from django_rbac.utils import get_role_model, get_role_parenting_model, get_ou_model
|
||||
from authentic2.apps.journal.views import JournalViewWithContext
|
||||
|
||||
from django_rbac.utils import get_role_model, get_role_parenting_model, get_ou_model
|
||||
|
||||
from .views import (BaseTableView, BaseAddView, BaseEditView, ActionMixin,
|
||||
OtherActionsMixin, Action, ExportMixin, BaseSubTableView,
|
||||
|
@ -55,6 +58,7 @@ from .forms import (UserSearchForm, UserAddForm, UserEditForm,
|
|||
UserEditImportForm, ChooseUserAuthorizationsForm)
|
||||
from .resources import UserResource
|
||||
from .utils import get_ou_count, has_show_username
|
||||
from .journal_views import BaseJournalView
|
||||
from . import app_settings
|
||||
|
||||
User = get_user_model()
|
||||
|
@ -215,6 +219,7 @@ class UserAddView(BaseAddView):
|
|||
response = super(UserAddView, self).form_valid(form)
|
||||
hooks.call_hooks('event', name='manager-add-user', user=self.request.user,
|
||||
instance=form.instance, form=form)
|
||||
self.request.journal.record('manager.user.creation', form=form)
|
||||
return response
|
||||
|
||||
def get_initial(self, *args, **kwargs):
|
||||
|
@ -286,10 +291,12 @@ class UserDetailView(OtherActionsMixin, BaseDetailView):
|
|||
|
||||
def action_force_password_change(self, request, *args, **kwargs):
|
||||
PasswordReset.objects.get_or_create(user=self.object)
|
||||
request.journal.record('manager.user.password.change.force', target_user=self.object)
|
||||
|
||||
def action_activate(self, request, *args, **kwargs):
|
||||
self.object.is_active = True
|
||||
self.object.save()
|
||||
request.journal.record('manager.user.activation', target_user=self.object)
|
||||
|
||||
def action_deactivate(self, request, *args, **kwargs):
|
||||
if request.user == self.object:
|
||||
|
@ -298,6 +305,7 @@ class UserDetailView(OtherActionsMixin, BaseDetailView):
|
|||
else:
|
||||
self.object.is_active = False
|
||||
self.object.save()
|
||||
request.journal.record('manager.user.deactivation', target_user=self.object)
|
||||
|
||||
def action_password_reset(self, request, *args, **kwargs):
|
||||
user = self.object
|
||||
|
@ -308,9 +316,11 @@ class UserDetailView(OtherActionsMixin, BaseDetailView):
|
|||
return
|
||||
send_password_reset_mail(user, request=request)
|
||||
messages.info(request, _('A mail was sent to %s') % self.object.email)
|
||||
request.journal.record('manager.user.password.reset.request', target_user=self.object)
|
||||
|
||||
def action_delete_password_reset(self, request, *args, **kwargs):
|
||||
PasswordReset.objects.filter(user=self.object).delete()
|
||||
request.journal.record('manager.user.password.change.unforce', target_user=self.object)
|
||||
|
||||
def action_su(self, request, *args, **kwargs):
|
||||
return redirect(request, 'auth_logout',
|
||||
|
@ -430,8 +440,10 @@ class UserEditView(OtherActionsMixin, ActionMixin, BaseEditView):
|
|||
self.object.email_verified = False
|
||||
self.object.save()
|
||||
response = super(UserEditView, self).form_valid(form)
|
||||
hooks.call_hooks('event', name='manager-edit-user', user=self.request.user,
|
||||
instance=form.instance, form=form)
|
||||
if form.has_changed():
|
||||
hooks.call_hooks('event', name='manager-edit-user', user=self.request.user,
|
||||
instance=form.instance, form=form)
|
||||
self.request.journal.record('manager.user.profile.edit', form=form)
|
||||
return response
|
||||
|
||||
user_edit = UserEditView.as_view()
|
||||
|
@ -518,6 +530,7 @@ class UserChangePasswordView(BaseEditView):
|
|||
response = super(UserChangePasswordView, self).form_valid(form)
|
||||
hooks.call_hooks('event', name='manager-change-password', user=self.request.user,
|
||||
instance=form.instance, form=form)
|
||||
self.request.journal.record('manager.user.password.change', form=form)
|
||||
return response
|
||||
|
||||
|
||||
|
@ -633,10 +646,13 @@ class UserRolesView(HideOUColumnMixin, BaseSubTableView):
|
|||
user.roles.add(role)
|
||||
hooks.call_hooks('event', name='manager-add-role-member',
|
||||
user=self.request.user, role=role, member=user)
|
||||
self.request.journal.record('manager.role.membership.grant', member=user, role=role)
|
||||
elif action == 'remove':
|
||||
user.roles.remove(role)
|
||||
hooks.call_hooks('event', name='manager-remove-role-member', user=self.request.user,
|
||||
role=role, member=user)
|
||||
if user.roles.filter(pk=role.pk).exists():
|
||||
user.roles.remove(role)
|
||||
hooks.call_hooks('event', name='manager-remove-role-member', user=self.request.user,
|
||||
role=role, member=user)
|
||||
self.request.journal.record('manager.role.membership.removal', member=user, role=role)
|
||||
return super(UserRolesView, self).form_valid(form)
|
||||
|
||||
def get_search_form_kwargs(self):
|
||||
|
@ -674,6 +690,7 @@ class UserDeleteView(BaseDeleteView):
|
|||
self.get_object().mark_as_deleted()
|
||||
hooks.call_hooks('event', name='manager-delete-user', user=request.user,
|
||||
instance=self.object)
|
||||
request.journal.record('manager.user.deletion', target_user=self.object)
|
||||
return HttpResponseRedirect(self.get_success_url())
|
||||
|
||||
|
||||
|
@ -898,8 +915,32 @@ class UserAuthorizationsView(FormNeedsRequest, BaseFormView, SingleObjectMixin,
|
|||
if self.can_manage_authorizations:
|
||||
qs = OIDCAuthorization.objects.filter(user=self.get_object())
|
||||
qs = qs.filter(id=auth_id.pk)
|
||||
qs.delete()
|
||||
oidc_authorization = qs.first()
|
||||
count, cascade = qs.delete()
|
||||
if count:
|
||||
self.request.journal.record(
|
||||
'manager.user.sso.authorization.deletion',
|
||||
service=oidc_authorization.client,
|
||||
target_user=self.object)
|
||||
return response
|
||||
|
||||
|
||||
user_authorizations = UserAuthorizationsView.as_view()
|
||||
|
||||
|
||||
class UserJournal(PermissionMixin, JournalViewWithContext, BaseJournalView):
|
||||
template_name = 'authentic2/manager/user_journal.html'
|
||||
permissions = ['custom_user.view_user']
|
||||
title = _('Journal')
|
||||
|
||||
@cached_property
|
||||
def context(self):
|
||||
return get_object_or_404(User, pk=self.kwargs['pk'])
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['object'] = self.context
|
||||
return ctx
|
||||
|
||||
|
||||
user_journal = UserJournal.as_view()
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
import base64
|
||||
import json
|
||||
import inspect
|
||||
import itertools
|
||||
import pickle
|
||||
|
||||
from django.core import signing
|
||||
|
@ -30,6 +30,7 @@ from django.views.generic.edit import FormMixin
|
|||
from django.http import HttpResponse, Http404
|
||||
from django.utils.encoding import force_text
|
||||
from django.utils import six
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.timezone import now
|
||||
from django.urls import reverse
|
||||
|
@ -45,6 +46,7 @@ from gadjo.templatetags.gadjo import xstatic
|
|||
|
||||
from django_rbac.utils import get_ou_model
|
||||
|
||||
from authentic2.a2_rbac.models import OrganizationalUnit as OU
|
||||
from authentic2.data_transfer import export_site, import_site, ImportContext
|
||||
from authentic2.forms.profile import modelform_factory
|
||||
from authentic2.utils import redirect, batch_queryset
|
||||
|
@ -597,6 +599,14 @@ class HomepageView(TitleMixin, PermissionMixin, MediaMixin, TemplateView):
|
|||
'permission': 'authentic2.search_service',
|
||||
'slug': 'services',
|
||||
},
|
||||
{
|
||||
'class': 'icon-journal',
|
||||
'href': reverse_lazy('a2-manager-journal'),
|
||||
'label': _('Journal'),
|
||||
'order': -1,
|
||||
'permission': 'superuser',
|
||||
'slug': 'journal',
|
||||
},
|
||||
]
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
|
@ -606,15 +616,16 @@ class HomepageView(TitleMixin, PermissionMixin, MediaMixin, TemplateView):
|
|||
|
||||
def get_homepage_entries(self):
|
||||
entries = []
|
||||
for entry in self.default_entries:
|
||||
if 'permission' in entry and not self.request.user.has_perm_any(entry['permission']):
|
||||
continue
|
||||
entries.append(entry)
|
||||
for hook_entries in hooks.call_hooks('manager_homepage_entries', self):
|
||||
for hook_entries in itertools.chain(
|
||||
self.default_entries,
|
||||
hooks.call_hooks('manager_homepage_entries', self)):
|
||||
if not hasattr(hook_entries, 'append'):
|
||||
hook_entries = [hook_entries]
|
||||
for entry in hook_entries:
|
||||
if 'permission' in entry and not self.request.user.has_perm_any(entry['permission']):
|
||||
permission = entry.get('permission')
|
||||
if permission == 'superuser' and not self.request.user.is_superuser:
|
||||
continue
|
||||
elif permission and not self.request.user.has_perm_any(permission):
|
||||
continue
|
||||
entries.append(entry)
|
||||
# use possible key order to sort
|
||||
|
@ -730,3 +741,17 @@ class SiteImportView(MediaMixin, TitleMixin, FormView):
|
|||
|
||||
|
||||
site_import = SiteImportView.as_view()
|
||||
|
||||
|
||||
class SearchOUMixin:
|
||||
@cached_property
|
||||
def ou(self):
|
||||
try:
|
||||
ou_id = int(self.request.GET['search-ou'])
|
||||
except (ValueError, KeyError):
|
||||
return None
|
||||
else:
|
||||
return OU.objects.filter(pk=ou_id).first()
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
return super().get_context_data(ou=self.ou, **kwargs)
|
||||
|
|
|
@ -0,0 +1,892 @@
|
|||
# authentic2 - versatile identity manager
|
||||
# Copyright (C) 2010-2020 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import datetime
|
||||
import mock
|
||||
|
||||
from authentic2.custom_user.models import User
|
||||
from authentic2.a2_rbac.utils import get_default_ou
|
||||
from authentic2.a2_rbac.models import Role
|
||||
from authentic2.models import Service
|
||||
from authentic2.apps.journal.models import Event, _registry
|
||||
from authentic2.journal import journal
|
||||
|
||||
from django.contrib.sessions.models import Session
|
||||
from django.utils.timezone import make_aware
|
||||
|
||||
import pytest
|
||||
|
||||
from .utils import login, text_content
|
||||
|
||||
|
||||
def test_journal_authorization(app, db, admin):
|
||||
response = login(app, admin, path='/manage/')
|
||||
assert 'Journal' not in response
|
||||
app.get('/manage/journal/', status=403)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def events(db, freezer):
|
||||
session1 = Session(session_key="1234")
|
||||
session2 = Session(session_key="abcd")
|
||||
|
||||
ou = get_default_ou()
|
||||
user = User.objects.create(
|
||||
username="user", email="user@example.com", ou=ou, uuid="1" * 32, first_name='Johnny', last_name='doe'
|
||||
)
|
||||
agent = User.objects.create(username="agent", email="agent@example.com", ou=ou, uuid="2" * 32)
|
||||
role_user = Role.objects.create(name="role1", ou=ou)
|
||||
role_agent = Role.objects.create(name="role2", ou=ou)
|
||||
service = Service.objects.create(name="service")
|
||||
|
||||
class EventFactory:
|
||||
date = make_aware(datetime.datetime(2020, 1, 1))
|
||||
|
||||
def __call__(self, name, **kwargs):
|
||||
freezer.move_to(self.date)
|
||||
journal.record(name, **kwargs)
|
||||
assert Event.objects.latest("timestamp").type.name == name
|
||||
self.date += datetime.timedelta(hours=1)
|
||||
|
||||
make = EventFactory()
|
||||
make("user.registration.request", email=user.email)
|
||||
make(
|
||||
"user.registration", user=user, session=session1, service=service, how="franceconnect",
|
||||
)
|
||||
make("user.logout", user=user, session=session1)
|
||||
|
||||
make("user.login.failure", username="user")
|
||||
make("user.login.failure", username="agent")
|
||||
make("user.login", user=user, session=session1, how="password")
|
||||
make("user.password.change", user=user, session=session1)
|
||||
edit_profile_form = mock.Mock()
|
||||
edit_profile_form.initial = {'email': "user@example.com", 'first_name': "John"}
|
||||
edit_profile_form.changed_data = ["first_name"]
|
||||
edit_profile_form.cleaned_data = {'first_name': "Jane"}
|
||||
make("user.profile.edit", user=user, session=session1, form=edit_profile_form)
|
||||
make("user.service.sso.authorization", user=user, session=session1, service=service)
|
||||
make("user.service.sso", user=user, session=session1, service=service, how="password")
|
||||
make("user.service.sso.unauthorization", user=user, session=session1, service=service)
|
||||
make("user.deletion", user=user, session=session1, service=service)
|
||||
|
||||
make("user.password.reset.request", email="USER@example.com", user=user)
|
||||
make("user.password.reset.failure", email="USER@example.com")
|
||||
make("user.password.reset", user=user)
|
||||
|
||||
make("user.login", user=agent, session=session2, how="saml")
|
||||
|
||||
create_form = mock.Mock(spec=["instance"])
|
||||
create_form.instance = user
|
||||
make("manager.user.creation", user=agent, session=session2, form=create_form)
|
||||
|
||||
edit_form = mock.Mock(spec=["instance", "initial", "changed_data", "cleaned_data"])
|
||||
edit_form.instance = user
|
||||
edit_form.initial = {'email': "user@example.com", 'first_name': "John"}
|
||||
edit_form.changed_data = ["first_name"]
|
||||
edit_form.cleaned_data = {'first_name': "Jane"}
|
||||
make("manager.user.profile.edit", user=agent, session=session2, form=edit_form)
|
||||
|
||||
change_email_form = mock.Mock(spec=["instance", "cleaned_data"])
|
||||
change_email_form.instance = user
|
||||
change_email_form.cleaned_data = {'new_email': "jane@example.com"}
|
||||
make(
|
||||
"manager.user.email.change.request", user=agent, session=session2, form=change_email_form,
|
||||
)
|
||||
|
||||
password_change_form = mock.Mock(spec=["instance", "cleaned_data"])
|
||||
password_change_form.instance = user
|
||||
password_change_form.cleaned_data = {'generate_password': False, 'send_mail': False}
|
||||
make(
|
||||
"manager.user.password.change", user=agent, session=session2, form=password_change_form,
|
||||
)
|
||||
|
||||
password_change_form.cleaned_data["send_mail"] = True
|
||||
make(
|
||||
"manager.user.password.change", user=agent, session=session2, form=password_change_form,
|
||||
)
|
||||
|
||||
make(
|
||||
"manager.user.password.reset.request", user=agent, session=session2, target_user=user,
|
||||
)
|
||||
|
||||
make(
|
||||
"manager.user.password.change.force", user=agent, session=session2, target_user=user,
|
||||
)
|
||||
make(
|
||||
"manager.user.password.change.unforce", user=agent, session=session2, target_user=user,
|
||||
)
|
||||
|
||||
make("manager.user.activation", user=agent, session=session2, target_user=user)
|
||||
make("manager.user.deactivation", user=agent, session=session2, target_user=user)
|
||||
make("manager.user.deletion", user=agent, session=session2, target_user=user)
|
||||
make(
|
||||
"manager.user.sso.authorization.deletion",
|
||||
user=agent,
|
||||
session=session2,
|
||||
service=service,
|
||||
target_user=user,
|
||||
)
|
||||
|
||||
make("manager.role.creation", user=agent, session=session2, role=role_user)
|
||||
role_edit_form = mock.Mock(spec=["instance", "initial", "changed_data", "cleaned_data"])
|
||||
role_edit_form.instance = role_user
|
||||
role_edit_form.initial = {'name': role_user.name}
|
||||
role_edit_form.changed_data = ["name"]
|
||||
role_edit_form.cleaned_data = {'name': "changed role name"}
|
||||
make(
|
||||
"manager.role.edit", user=agent, session=session2, role=role_user, form=role_edit_form,
|
||||
)
|
||||
make("manager.role.deletion", user=agent, session=session2, role=role_user)
|
||||
make(
|
||||
"manager.role.membership.grant", user=agent, session=session2, role=role_user, member=user,
|
||||
)
|
||||
make(
|
||||
"manager.role.membership.removal", user=agent, session=session2, role=role_user, member=user,
|
||||
)
|
||||
|
||||
make(
|
||||
"manager.role.inheritance.addition", user=agent, session=session2, parent=role_agent, child=role_user,
|
||||
)
|
||||
make(
|
||||
"manager.role.inheritance.removal", user=agent, session=session2, parent=role_agent, child=role_user,
|
||||
)
|
||||
|
||||
make(
|
||||
"manager.role.administrator.role.addition",
|
||||
user=agent,
|
||||
session=session2,
|
||||
role=role_user,
|
||||
admin_role=role_agent,
|
||||
)
|
||||
make(
|
||||
"manager.role.administrator.role.removal",
|
||||
user=agent,
|
||||
session=session2,
|
||||
role=role_user,
|
||||
admin_role=role_agent,
|
||||
)
|
||||
|
||||
make(
|
||||
"manager.role.administrator.user.addition",
|
||||
user=agent,
|
||||
session=session2,
|
||||
role=role_user,
|
||||
admin_user=user,
|
||||
)
|
||||
make(
|
||||
"manager.role.administrator.user.removal",
|
||||
user=agent,
|
||||
session=session2,
|
||||
role=role_user,
|
||||
admin_user=user,
|
||||
)
|
||||
|
||||
# verify we created at least one event for each type
|
||||
assert set(Event.objects.values_list("type__name", flat=True)) == set(_registry)
|
||||
|
||||
return locals()
|
||||
|
||||
|
||||
def extract_journal(response):
|
||||
rows = []
|
||||
seen_event_ids = set()
|
||||
while True:
|
||||
for tr in response.pyquery("tr[data-event-type]"):
|
||||
# page can overlap when they contain less than 20 items (to prevent orphan rows)
|
||||
event_id = tr.attrib["data-event-id"]
|
||||
if event_id not in seen_event_ids:
|
||||
rows.append(response.pyquery(tr))
|
||||
seen_event_ids.add(event_id)
|
||||
if "Previous page" not in response:
|
||||
break
|
||||
response = response.click("Previous page", index=0)
|
||||
|
||||
rows.reverse()
|
||||
content = [
|
||||
{
|
||||
'timestamp': text_content(row.find(".journal-list--timestamp-column")[0]).strip(),
|
||||
'type': row[0].attrib["data-event-type"],
|
||||
'user': text_content(row.find(".journal-list--user-column")[0]).strip(),
|
||||
'message': text_content(row.find(".journal-list--message-column")[0]),
|
||||
}
|
||||
for row in rows
|
||||
]
|
||||
return content
|
||||
|
||||
|
||||
def test_global_journal(app, superuser, events):
|
||||
response = login(app, user=superuser, path="/manage/")
|
||||
|
||||
# remove event about admin login
|
||||
Event.objects.filter(user=superuser).delete()
|
||||
|
||||
response = response.click(href="journal")
|
||||
|
||||
content = extract_journal(response)
|
||||
|
||||
assert content == [
|
||||
{
|
||||
'message': 'registration request with email "user@example.com"',
|
||||
'timestamp': 'Jan. 1, 2020, midnight',
|
||||
'type': 'user.registration.request',
|
||||
'user': '-',
|
||||
},
|
||||
{
|
||||
'message': 'registration using franceconnect',
|
||||
'timestamp': 'Jan. 1, 2020, 1 a.m.',
|
||||
'type': 'user.registration',
|
||||
'user': 'Johnny doe',
|
||||
},
|
||||
{
|
||||
'message': 'logout',
|
||||
'timestamp': 'Jan. 1, 2020, 2 a.m.',
|
||||
'type': 'user.logout',
|
||||
'user': 'Johnny doe',
|
||||
},
|
||||
{
|
||||
'message': 'login failure with username "user"',
|
||||
'timestamp': 'Jan. 1, 2020, 3 a.m.',
|
||||
'type': 'user.login.failure',
|
||||
'user': '-',
|
||||
},
|
||||
{
|
||||
'message': 'login failure with username "agent"',
|
||||
'timestamp': 'Jan. 1, 2020, 4 a.m.',
|
||||
'type': 'user.login.failure',
|
||||
'user': '-',
|
||||
},
|
||||
{
|
||||
'message': 'login using password',
|
||||
'timestamp': 'Jan. 1, 2020, 5 a.m.',
|
||||
'type': 'user.login',
|
||||
'user': 'Johnny doe',
|
||||
},
|
||||
{
|
||||
'message': 'password change',
|
||||
'timestamp': 'Jan. 1, 2020, 6 a.m.',
|
||||
'type': 'user.password.change',
|
||||
'user': 'Johnny doe',
|
||||
},
|
||||
{
|
||||
'message': 'profile edit (first name)',
|
||||
'timestamp': 'Jan. 1, 2020, 7 a.m.',
|
||||
'type': 'user.profile.edit',
|
||||
'user': 'Johnny doe',
|
||||
},
|
||||
{
|
||||
'message': 'authorization of single sign on with "service"',
|
||||
'timestamp': 'Jan. 1, 2020, 8 a.m.',
|
||||
'type': 'user.service.sso.authorization',
|
||||
'user': 'Johnny doe',
|
||||
},
|
||||
{
|
||||
'message': 'service single sign on with "service"',
|
||||
'timestamp': 'Jan. 1, 2020, 9 a.m.',
|
||||
'type': 'user.service.sso',
|
||||
'user': 'Johnny doe',
|
||||
},
|
||||
{
|
||||
'message': 'unauthorization of single sign on with "service"',
|
||||
'timestamp': 'Jan. 1, 2020, 10 a.m.',
|
||||
'type': 'user.service.sso.unauthorization',
|
||||
'user': 'Johnny doe',
|
||||
},
|
||||
{
|
||||
'message': 'deletion',
|
||||
'timestamp': 'Jan. 1, 2020, 11 a.m.',
|
||||
'type': 'user.deletion',
|
||||
'user': 'Johnny doe',
|
||||
},
|
||||
{
|
||||
'message': 'password reset request with email "user@example.com"',
|
||||
'timestamp': 'Jan. 1, 2020, noon',
|
||||
'type': 'user.password.reset.request',
|
||||
'user': 'Johnny doe',
|
||||
},
|
||||
{
|
||||
'message': 'password reset failure with email "USER@example.com"',
|
||||
'timestamp': 'Jan. 1, 2020, 1 p.m.',
|
||||
'type': 'user.password.reset.failure',
|
||||
'user': '-',
|
||||
},
|
||||
{
|
||||
'message': 'password reset',
|
||||
'timestamp': 'Jan. 1, 2020, 2 p.m.',
|
||||
'type': 'user.password.reset',
|
||||
'user': 'Johnny doe',
|
||||
},
|
||||
{
|
||||
'message': 'login using SAML',
|
||||
'timestamp': 'Jan. 1, 2020, 3 p.m.',
|
||||
'type': 'user.login',
|
||||
'user': 'agent',
|
||||
},
|
||||
{
|
||||
'message': 'creation of user "Johnny doe"',
|
||||
'timestamp': 'Jan. 1, 2020, 4 p.m.',
|
||||
'type': 'manager.user.creation',
|
||||
'user': 'agent',
|
||||
},
|
||||
{
|
||||
'message': 'edit of user "Johnny doe" (first name)',
|
||||
'timestamp': 'Jan. 1, 2020, 5 p.m.',
|
||||
'type': 'manager.user.profile.edit',
|
||||
'user': 'agent',
|
||||
},
|
||||
{
|
||||
'message': 'email change of user "Johnny doe" for email address "jane@example.com"',
|
||||
'timestamp': 'Jan. 1, 2020, 6 p.m.',
|
||||
'type': 'manager.user.email.change.request',
|
||||
'user': 'agent',
|
||||
},
|
||||
{
|
||||
'message': 'password change of user "Johnny doe"',
|
||||
'timestamp': 'Jan. 1, 2020, 7 p.m.',
|
||||
'type': 'manager.user.password.change',
|
||||
'user': 'agent',
|
||||
},
|
||||
{
|
||||
'message': 'password change of user "Johnny doe" and notification by mail',
|
||||
'timestamp': 'Jan. 1, 2020, 8 p.m.',
|
||||
'type': 'manager.user.password.change',
|
||||
'user': 'agent',
|
||||
},
|
||||
{
|
||||
'message': 'password reset request of "Johnny doe" sent to "user@example.com"',
|
||||
'timestamp': 'Jan. 1, 2020, 9 p.m.',
|
||||
'type': 'manager.user.password.reset.request',
|
||||
'user': 'agent',
|
||||
},
|
||||
{
|
||||
'message': 'mandatory password change at next login set for user "Johnny doe"',
|
||||
'timestamp': 'Jan. 1, 2020, 10 p.m.',
|
||||
'type': 'manager.user.password.change.force',
|
||||
'user': 'agent',
|
||||
},
|
||||
{
|
||||
'message': 'mandatory password change at next login unset for user "Johnny doe"',
|
||||
'timestamp': 'Jan. 1, 2020, 11 p.m.',
|
||||
'type': 'manager.user.password.change.unforce',
|
||||
'user': 'agent',
|
||||
},
|
||||
{
|
||||
'message': 'activation of user "Johnny doe"',
|
||||
'timestamp': 'Jan. 2, 2020, midnight',
|
||||
'type': 'manager.user.activation',
|
||||
'user': 'agent',
|
||||
},
|
||||
{
|
||||
'message': 'deactivation of user "Johnny doe"',
|
||||
'timestamp': 'Jan. 2, 2020, 1 a.m.',
|
||||
'type': 'manager.user.deactivation',
|
||||
'user': 'agent',
|
||||
},
|
||||
{
|
||||
'message': 'deletion of user "Johnny doe"',
|
||||
'timestamp': 'Jan. 2, 2020, 2 a.m.',
|
||||
'type': 'manager.user.deletion',
|
||||
'user': 'agent',
|
||||
},
|
||||
{
|
||||
'message': 'deletion of authorization of single sign on with "service" of ' 'user "Johnny doe"',
|
||||
'timestamp': 'Jan. 2, 2020, 3 a.m.',
|
||||
'type': 'manager.user.sso.authorization.deletion',
|
||||
'user': 'agent',
|
||||
},
|
||||
{
|
||||
'message': 'creation of role "role1"',
|
||||
'timestamp': 'Jan. 2, 2020, 4 a.m.',
|
||||
'type': 'manager.role.creation',
|
||||
'user': 'agent',
|
||||
},
|
||||
{
|
||||
'message': 'edit of role "role1" (name)',
|
||||
'timestamp': 'Jan. 2, 2020, 5 a.m.',
|
||||
'type': 'manager.role.edit',
|
||||
'user': 'agent',
|
||||
},
|
||||
{
|
||||
'message': 'deletion of role "role1"',
|
||||
'timestamp': 'Jan. 2, 2020, 6 a.m.',
|
||||
'type': 'manager.role.deletion',
|
||||
'user': 'agent',
|
||||
},
|
||||
{
|
||||
'message': 'membership grant to user "user (111111)" in role "role1"',
|
||||
'timestamp': 'Jan. 2, 2020, 7 a.m.',
|
||||
'type': 'manager.role.membership.grant',
|
||||
'user': 'agent',
|
||||
},
|
||||
{
|
||||
'message': 'membership removal of user "user (111111)" from role "role1"',
|
||||
'timestamp': 'Jan. 2, 2020, 8 a.m.',
|
||||
'type': 'manager.role.membership.removal',
|
||||
'user': 'agent',
|
||||
},
|
||||
{
|
||||
'message': 'inheritance addition from parent role "role2" to child role ' '"role1"',
|
||||
'timestamp': 'Jan. 2, 2020, 9 a.m.',
|
||||
'type': 'manager.role.inheritance.addition',
|
||||
'user': 'agent',
|
||||
},
|
||||
{
|
||||
'message': 'inheritance removal from parent role "role2" to child role ' '"role1"',
|
||||
'timestamp': 'Jan. 2, 2020, 10 a.m.',
|
||||
'type': 'manager.role.inheritance.removal',
|
||||
'user': 'agent',
|
||||
},
|
||||
{
|
||||
'message': 'addition of role "role2" as administrator of role "role1"',
|
||||
'timestamp': 'Jan. 2, 2020, 11 a.m.',
|
||||
'type': 'manager.role.administrator.role.addition',
|
||||
'user': 'agent',
|
||||
},
|
||||
{
|
||||
'message': 'removal of role "role2" as administrator of role "role1"',
|
||||
'timestamp': 'Jan. 2, 2020, noon',
|
||||
'type': 'manager.role.administrator.role.removal',
|
||||
'user': 'agent',
|
||||
},
|
||||
{
|
||||
'message': 'addition of user "user (111111)" as administrator of role ' '"role1"',
|
||||
'timestamp': 'Jan. 2, 2020, 1 p.m.',
|
||||
'type': 'manager.role.administrator.user.addition',
|
||||
'user': 'agent',
|
||||
},
|
||||
{
|
||||
'message': 'removal of user "user (111111)" as administrator of role "role1"',
|
||||
'timestamp': 'Jan. 2, 2020, 2 p.m.',
|
||||
'type': 'manager.role.administrator.user.removal',
|
||||
'user': 'agent',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def test_user_journal(app, superuser, events):
|
||||
response = login(app, user=superuser, path="/manage/")
|
||||
user = User.objects.get(username="user")
|
||||
|
||||
response = app.get("/manage/users/%s/journal/" % user.id)
|
||||
content = extract_journal(response)
|
||||
|
||||
assert content == [
|
||||
{
|
||||
'message': 'registration using franceconnect',
|
||||
'timestamp': 'Jan. 1, 2020, 1 a.m.',
|
||||
'type': 'user.registration',
|
||||
'user': 'Johnny doe',
|
||||
},
|
||||
{
|
||||
'message': 'logout',
|
||||
'timestamp': 'Jan. 1, 2020, 2 a.m.',
|
||||
'type': 'user.logout',
|
||||
'user': 'Johnny doe',
|
||||
},
|
||||
{
|
||||
'message': 'login using password',
|
||||
'timestamp': 'Jan. 1, 2020, 5 a.m.',
|
||||
'type': 'user.login',
|
||||
'user': 'Johnny doe',
|
||||
},
|
||||
{
|
||||
'message': 'password change',
|
||||
'timestamp': 'Jan. 1, 2020, 6 a.m.',
|
||||
'type': 'user.password.change',
|
||||
'user': 'Johnny doe',
|
||||
},
|
||||
{
|
||||
'message': 'profile edit (first name)',
|
||||
'timestamp': 'Jan. 1, 2020, 7 a.m.',
|
||||
'type': 'user.profile.edit',
|
||||
'user': 'Johnny doe',
|
||||
},
|
||||
{
|
||||
'message': 'authorization of single sign on with "service"',
|
||||
'timestamp': 'Jan. 1, 2020, 8 a.m.',
|
||||
'type': 'user.service.sso.authorization',
|
||||
'user': 'Johnny doe',
|
||||
},
|
||||
{
|
||||
'message': 'service single sign on with "service"',
|
||||
'timestamp': 'Jan. 1, 2020, 9 a.m.',
|
||||
'type': 'user.service.sso',
|
||||
'user': 'Johnny doe',
|
||||
},
|
||||
{
|
||||
'message': 'unauthorization of single sign on with "service"',
|
||||
'timestamp': 'Jan. 1, 2020, 10 a.m.',
|
||||
'type': 'user.service.sso.unauthorization',
|
||||
'user': 'Johnny doe',
|
||||
},
|
||||
{
|
||||
'message': 'deletion',
|
||||
'timestamp': 'Jan. 1, 2020, 11 a.m.',
|
||||
'type': 'user.deletion',
|
||||
'user': 'Johnny doe',
|
||||
},
|
||||
{
|
||||
'message': 'password reset request with email "user@example.com"',
|
||||
'timestamp': 'Jan. 1, 2020, noon',
|
||||
'type': 'user.password.reset.request',
|
||||
'user': 'Johnny doe',
|
||||
},
|
||||
{
|
||||
'message': 'password reset',
|
||||
'timestamp': 'Jan. 1, 2020, 2 p.m.',
|
||||
'type': 'user.password.reset',
|
||||
'user': 'Johnny doe',
|
||||
},
|
||||
{
|
||||
'message': 'creation by administrator',
|
||||
'timestamp': 'Jan. 1, 2020, 4 p.m.',
|
||||
'type': 'manager.user.creation',
|
||||
'user': 'agent',
|
||||
},
|
||||
{
|
||||
'message': 'edit by administrator (first name)',
|
||||
'timestamp': 'Jan. 1, 2020, 5 p.m.',
|
||||
'type': 'manager.user.profile.edit',
|
||||
'user': 'agent',
|
||||
},
|
||||
{
|
||||
'message': 'email change for email address "jane@example.com" requested by administrator',
|
||||
'timestamp': 'Jan. 1, 2020, 6 p.m.',
|
||||
'type': 'manager.user.email.change.request',
|
||||
'user': 'agent',
|
||||
},
|
||||
{
|
||||
'message': 'password change by administrator',
|
||||
'timestamp': 'Jan. 1, 2020, 7 p.m.',
|
||||
'type': 'manager.user.password.change',
|
||||
'user': 'agent',
|
||||
},
|
||||
{
|
||||
'message': 'password change by administrator and notification by mail',
|
||||
'timestamp': 'Jan. 1, 2020, 8 p.m.',
|
||||
'type': 'manager.user.password.change',
|
||||
'user': 'agent',
|
||||
},
|
||||
{
|
||||
'message': "password reset request by administrator sent to " '"user@example.com"',
|
||||
'timestamp': 'Jan. 1, 2020, 9 p.m.',
|
||||
'type': 'manager.user.password.reset.request',
|
||||
'user': 'agent',
|
||||
},
|
||||
{
|
||||
'message': 'mandatory password change at next login set by administrator',
|
||||
'timestamp': 'Jan. 1, 2020, 10 p.m.',
|
||||
'type': 'manager.user.password.change.force',
|
||||
'user': 'agent',
|
||||
},
|
||||
{
|
||||
'message': 'mandatory password change at next login unset by administrator',
|
||||
'timestamp': 'Jan. 1, 2020, 11 p.m.',
|
||||
'type': 'manager.user.password.change.unforce',
|
||||
'user': 'agent',
|
||||
},
|
||||
{
|
||||
'message': 'activation by administrator',
|
||||
'timestamp': 'Jan. 2, 2020, midnight',
|
||||
'type': 'manager.user.activation',
|
||||
'user': 'agent',
|
||||
},
|
||||
{
|
||||
'message': 'deactivation by administrator',
|
||||
'timestamp': 'Jan. 2, 2020, 1 a.m.',
|
||||
'type': 'manager.user.deactivation',
|
||||
'user': 'agent',
|
||||
},
|
||||
{
|
||||
'message': 'deletion by administrator',
|
||||
'timestamp': 'Jan. 2, 2020, 2 a.m.',
|
||||
'type': 'manager.user.deletion',
|
||||
'user': 'agent',
|
||||
},
|
||||
{
|
||||
'message': 'deletion of authorization of single sign on with "service" by ' "administrator",
|
||||
'timestamp': 'Jan. 2, 2020, 3 a.m.',
|
||||
'type': 'manager.user.sso.authorization.deletion',
|
||||
'user': 'agent',
|
||||
},
|
||||
{
|
||||
'message': 'membership grant in role "role1"',
|
||||
'timestamp': 'Jan. 2, 2020, 7 a.m.',
|
||||
'type': 'manager.role.membership.grant',
|
||||
'user': 'agent',
|
||||
},
|
||||
{
|
||||
'message': 'membership removal from role "role1"',
|
||||
'timestamp': 'Jan. 2, 2020, 8 a.m.',
|
||||
'type': 'manager.role.membership.removal',
|
||||
'user': 'agent',
|
||||
},
|
||||
{
|
||||
'message': 'addition as administrator of role "role1"',
|
||||
'timestamp': 'Jan. 2, 2020, 1 p.m.',
|
||||
'type': 'manager.role.administrator.user.addition',
|
||||
'user': 'agent',
|
||||
},
|
||||
{
|
||||
'message': 'removal as administrator of role "role1"',
|
||||
'timestamp': 'Jan. 2, 2020, 2 p.m.',
|
||||
'type': 'manager.role.administrator.user.removal',
|
||||
'user': 'agent',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def test_role_journal(app, superuser, events):
|
||||
response = login(app, user=superuser, path="/manage/")
|
||||
role1 = Role.objects.get(name="role1")
|
||||
role2 = Role.objects.get(name="role2")
|
||||
|
||||
response = app.get("/manage/roles/%s/journal/" % role1.id)
|
||||
content = extract_journal(response)
|
||||
|
||||
assert content == [
|
||||
{
|
||||
'message': 'creation',
|
||||
'timestamp': 'Jan. 2, 2020, 4 a.m.',
|
||||
'type': 'manager.role.creation',
|
||||
'user': 'agent',
|
||||
},
|
||||
{
|
||||
'message': 'edit (name)',
|
||||
'timestamp': 'Jan. 2, 2020, 5 a.m.',
|
||||
'type': 'manager.role.edit',
|
||||
'user': 'agent',
|
||||
},
|
||||
{
|
||||
'message': 'deletion',
|
||||
'timestamp': 'Jan. 2, 2020, 6 a.m.',
|
||||
'type': 'manager.role.deletion',
|
||||
'user': 'agent',
|
||||
},
|
||||
{
|
||||
'message': 'membership grant to user "user (111111)"',
|
||||
'timestamp': 'Jan. 2, 2020, 7 a.m.',
|
||||
'type': 'manager.role.membership.grant',
|
||||
'user': 'agent',
|
||||
},
|
||||
{
|
||||
'message': 'membership removal of user "user (111111)"',
|
||||
'timestamp': 'Jan. 2, 2020, 8 a.m.',
|
||||
'type': 'manager.role.membership.removal',
|
||||
'user': 'agent',
|
||||
},
|
||||
{
|
||||
'message': 'inheritance addition from parent role "role2"',
|
||||
'timestamp': 'Jan. 2, 2020, 9 a.m.',
|
||||
'type': 'manager.role.inheritance.addition',
|
||||
'user': 'agent',
|
||||
},
|
||||
{
|
||||
'message': 'inheritance removal from parent role "role2"',
|
||||
'timestamp': 'Jan. 2, 2020, 10 a.m.',
|
||||
'type': 'manager.role.inheritance.removal',
|
||||
'user': 'agent',
|
||||
},
|
||||
{
|
||||
'message': 'addition of role "role2" as administrator',
|
||||
'timestamp': 'Jan. 2, 2020, 11 a.m.',
|
||||
'type': 'manager.role.administrator.role.addition',
|
||||
'user': 'agent',
|
||||
},
|
||||
{
|
||||
'message': 'removal of role "role2" as administrator',
|
||||
'timestamp': 'Jan. 2, 2020, noon',
|
||||
'type': 'manager.role.administrator.role.removal',
|
||||
'user': 'agent',
|
||||
},
|
||||
{
|
||||
'message': 'addition of user "user (111111)" as administrator',
|
||||
'timestamp': 'Jan. 2, 2020, 1 p.m.',
|
||||
'type': 'manager.role.administrator.user.addition',
|
||||
'user': 'agent',
|
||||
},
|
||||
{
|
||||
'message': 'removal of user "user (111111)" as administrator',
|
||||
'timestamp': 'Jan. 2, 2020, 2 p.m.',
|
||||
'type': 'manager.role.administrator.user.removal',
|
||||
'user': 'agent',
|
||||
},
|
||||
]
|
||||
|
||||
response = app.get("/manage/roles/%s/journal/" % role2.id)
|
||||
content = extract_journal(response)
|
||||
|
||||
assert content == [
|
||||
{
|
||||
'message': 'inheritance addition to child role "role1"',
|
||||
'timestamp': 'Jan. 2, 2020, 9 a.m.',
|
||||
'type': 'manager.role.inheritance.addition',
|
||||
'user': 'agent',
|
||||
},
|
||||
{
|
||||
'message': 'inheritance removal to child role "role1"',
|
||||
'timestamp': 'Jan. 2, 2020, 10 a.m.',
|
||||
'type': 'manager.role.inheritance.removal',
|
||||
'user': 'agent',
|
||||
},
|
||||
{
|
||||
'message': 'addition as administrator of role "role1"',
|
||||
'timestamp': 'Jan. 2, 2020, 11 a.m.',
|
||||
'type': 'manager.role.administrator.role.addition',
|
||||
'user': 'agent',
|
||||
},
|
||||
{
|
||||
'message': 'removal as administrator of role "role1"',
|
||||
'timestamp': 'Jan. 2, 2020, noon',
|
||||
'type': 'manager.role.administrator.role.removal',
|
||||
'user': 'agent',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def test_roles_journal(app, superuser, events):
|
||||
response = login(app, user=superuser, path='/manage/')
|
||||
response = response.click('Role')
|
||||
response = response.click('Journal')
|
||||
|
||||
content = extract_journal(response)
|
||||
|
||||
assert content == [
|
||||
{
|
||||
'message': 'creation of role "role1"',
|
||||
'timestamp': 'Jan. 2, 2020, 4 a.m.',
|
||||
'type': 'manager.role.creation',
|
||||
'user': 'agent',
|
||||
},
|
||||
{
|
||||
'message': 'edit of role "role1" (name)',
|
||||
'timestamp': 'Jan. 2, 2020, 5 a.m.',
|
||||
'type': 'manager.role.edit',
|
||||
'user': 'agent',
|
||||
},
|
||||
{
|
||||
'message': 'deletion of role "role1"',
|
||||
'timestamp': 'Jan. 2, 2020, 6 a.m.',
|
||||
'type': 'manager.role.deletion',
|
||||
'user': 'agent',
|
||||
},
|
||||
{
|
||||
'message': 'membership grant to user "user (111111)" in role "role1"',
|
||||
'timestamp': 'Jan. 2, 2020, 7 a.m.',
|
||||
'type': 'manager.role.membership.grant',
|
||||
'user': 'agent',
|
||||
},
|
||||
{
|
||||
'message': 'membership removal of user "user (111111)" from role "role1"',
|
||||
'timestamp': 'Jan. 2, 2020, 8 a.m.',
|
||||
'type': 'manager.role.membership.removal',
|
||||
'user': 'agent',
|
||||
},
|
||||
{
|
||||
'message': 'inheritance addition from parent role "role2" to child role ' '"role1"',
|
||||
'timestamp': 'Jan. 2, 2020, 9 a.m.',
|
||||
'type': 'manager.role.inheritance.addition',
|
||||
'user': 'agent',
|
||||
},
|
||||
{
|
||||
'message': 'inheritance removal from parent role "role2" to child role ' '"role1"',
|
||||
'timestamp': 'Jan. 2, 2020, 10 a.m.',
|
||||
'type': 'manager.role.inheritance.removal',
|
||||
'user': 'agent',
|
||||
},
|
||||
{
|
||||
'message': 'addition of role "role2" as administrator of role "role1"',
|
||||
'timestamp': 'Jan. 2, 2020, 11 a.m.',
|
||||
'type': 'manager.role.administrator.role.addition',
|
||||
'user': 'agent',
|
||||
},
|
||||
{
|
||||
'message': 'removal of role "role2" as administrator of role "role1"',
|
||||
'timestamp': 'Jan. 2, 2020, noon',
|
||||
'type': 'manager.role.administrator.role.removal',
|
||||
'user': 'agent',
|
||||
},
|
||||
{
|
||||
'message': 'addition of user "user (111111)" as administrator of role ' '"role1"',
|
||||
'timestamp': 'Jan. 2, 2020, 1 p.m.',
|
||||
'type': 'manager.role.administrator.user.addition',
|
||||
'user': 'agent',
|
||||
},
|
||||
{
|
||||
'message': 'removal of user "user (111111)" as administrator of role "role1"',
|
||||
'timestamp': 'Jan. 2, 2020, 2 p.m.',
|
||||
'type': 'manager.role.administrator.user.removal',
|
||||
'user': 'agent',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def test_date_navigation(app, superuser, events):
|
||||
response = login(app, user=superuser, path="/manage/journal/")
|
||||
response = response.click('2020')
|
||||
response = response.click('January')
|
||||
response = response.click('1')
|
||||
response = response.click('January 2020')
|
||||
response = response.click('2020')
|
||||
response = response.click('All dates')
|
||||
|
||||
|
||||
def test_search(app, superuser, events):
|
||||
response = login(app, user=superuser, path="/manage/journal/")
|
||||
response.form.set('search', 'event:registration')
|
||||
response = response.form.submit()
|
||||
assert len(response.pyquery('tbody tr')) == 1
|
||||
|
||||
response.form.set('search', 'username:agent event:login')
|
||||
response = response.form.submit()
|
||||
assert len(response.pyquery('tbody tr')) == 1
|
||||
assert all(
|
||||
'agent' == text_content(node) for node in response.pyquery('tbody tr td.journal-list--user-column')
|
||||
)
|
||||
|
||||
response.form.set('search', 'uuid:%s event:reset' % events['user'].uuid)
|
||||
response = response.form.submit()
|
||||
assert len(response.pyquery('tbody tr')) == 1
|
||||
|
||||
response.form.set('search', 'session:1234')
|
||||
response = response.form.submit()
|
||||
assert len(response.pyquery('tbody tr')) == 9
|
||||
assert all(
|
||||
text_content(node) == 'Johnny doe'
|
||||
for node in response.pyquery('tbody tr td.journal-list--user-column')
|
||||
)
|
||||
|
||||
response.form.set('search', 'email:jane@example.com')
|
||||
response = response.form.submit()
|
||||
assert (
|
||||
text_content(response.pyquery('tbody tr td.journal-list--message-column')[0]).strip()
|
||||
== 'email change of user "Johnny doe" for email address "jane@example.com"'
|
||||
)
|
||||
|
||||
response.form.set('search', 'jane@example.com')
|
||||
response = response.form.submit()
|
||||
assert (
|
||||
text_content(response.pyquery('tbody tr td.journal-list--message-column')[0]).strip()
|
||||
== 'email change of user "Johnny doe" for email address "jane@example.com"'
|
||||
)
|
||||
|
||||
response.form.set('search', 'johny doe event:login')
|
||||
response = response.form.submit()
|
||||
pq = response.pyquery
|
||||
|
||||
assert [
|
||||
list(map(text_content, p))
|
||||
for p in zip(pq('tbody td.journal-list--user-column'), pq('tbody td.journal-list--message-column'))
|
||||
] == [['Johnny doe', 'login using password']]
|
Loading…
Reference in New Issue