Skip to content

Commit

Permalink
Merge pull request #332 from reduxionist/per-queue-staff-membership
Browse files Browse the repository at this point in the history
Per queue staff membership
  • Loading branch information
rossp committed Jun 15, 2015
2 parents 9807356 + 42f478b commit 84330ee
Show file tree
Hide file tree
Showing 9 changed files with 361 additions and 17 deletions.
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ If a user is a staff member, they get general helpdesk access, including:
6. Assign tickets to themselves or other staff members
7. Resolve tickets

Optionally, their access to view tickets, both on the dashboard and through searches and reports, may be restricted by a list of queues to which they have been granted membership. Create and update permissions for individual tickets are not limited by this optional restriction.

Licensing
---------
Expand Down
8 changes: 8 additions & 0 deletions docs/settings.rst
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,14 @@ Staff Ticket Creation Settings
**Default:** ``HELPDESK_CREATE_TICKET_HIDE_ASSIGNED_TO = False``


Staff Ticket View Settings
------------------------------

- **HELPDESK_ENABLE_PER_QUEUE_MEMBERSHIP** If ``True``, logged in staff users only see queues and tickets to which they have specifically been granted access - this holds for the dashboard, ticket query, and ticket report views. User assignment can be modified in the ``User`` section of the standard ``django.contrib.admin`` app. *Note*: This setting does not prevent staff users from creating tickets for all queues or editing tickets in any queue, should they know the ticket ID or editing URL. It is meant to keep work loads segregated for staff convenience, not to prevent malicious behavior. Also, superuser accounts have full access to all queues, regardless of whatever queue memberships they have been granted.

**Default:** ``HELPDESK_ENABLE_PER_QUEUE_MEMBERSHIP = False``



Default E-Mail Settings
-----------------------
Expand Down
21 changes: 20 additions & 1 deletion helpdesk/admin.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
from django.contrib import admin
from django.contrib.auth import get_user_model
from django.contrib.auth.admin import UserAdmin
from helpdesk.models import Queue, Ticket, FollowUp, PreSetReply, KBCategory
from helpdesk.models import EscalationExclusion, EmailTemplate, KBItem
from helpdesk.models import TicketChange, Attachment, IgnoreEmail
from helpdesk.models import CustomField
from helpdesk.models import QueueMembership
from helpdesk import settings as helpdesk_settings

class QueueAdmin(admin.ModelAdmin):
list_display = ('title', 'slug', 'email_address', 'locale')
Expand All @@ -24,14 +28,26 @@ class FollowUpAdmin(admin.ModelAdmin):
class KBItemAdmin(admin.ModelAdmin):
list_display = ('category', 'title', 'last_updated',)
list_display_links = ('title',)

class CustomFieldAdmin(admin.ModelAdmin):
list_display = ('name', 'label', 'data_type')

class EmailTemplateAdmin(admin.ModelAdmin):
list_display = ('template_name', 'heading', 'locale')
list_filter = ('locale', )

class QueueMembershipInline(admin.StackedInline):
model = QueueMembership

class UserAdminWithQueueMemberships(UserAdmin):

def change_view(self, request, object_id, form_url='', extra_context=None):
self.inlines = (QueueMembershipInline,)

return super(UserAdminWithQueueMemberships, self).change_view(
request, object_id, form_url=form_url, extra_context=extra_context)


admin.site.register(Ticket, TicketAdmin)
admin.site.register(Queue, QueueAdmin)
admin.site.register(FollowUp, FollowUpAdmin)
Expand All @@ -42,3 +58,6 @@ class EmailTemplateAdmin(admin.ModelAdmin):
admin.site.register(KBItem, KBItemAdmin)
admin.site.register(IgnoreEmail)
admin.site.register(CustomField, CustomFieldAdmin)
if helpdesk_settings.HELPDESK_ENABLE_PER_QUEUE_STAFF_MEMBERSHIP:
admin.site.unregister(get_user_model())
admin.site.register(get_user_model(), UserAdminWithQueueMemberships)
29 changes: 29 additions & 0 deletions helpdesk/migrations/0004_add_per_queue_staff_membership.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django.db import models, migrations
from django.conf import settings


class Migration(migrations.Migration):

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('helpdesk', '0003_initial_data_import'),
]

operations = [
migrations.CreateModel(
name='QueueMembership',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('queues', models.ManyToManyField(to='helpdesk.Queue', verbose_name='Authorized Queues')),
('user', models.OneToOneField(verbose_name='User', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Queue Membership',
'verbose_name_plural': 'Queue Memberships',
},
bases=(models.Model,),
),
]
22 changes: 22 additions & 0 deletions helpdesk/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1359,3 +1359,25 @@ class Meta:
unique_together = ('ticket', 'depends_on')
verbose_name = _('Ticket dependency')
verbose_name_plural = _('Ticket dependencies')


class QueueMembership(models.Model):
"""
Used to restrict staff members to certain queues only
"""
user = models.OneToOneField(
settings.AUTH_USER_MODEL,
verbose_name=_('User'),
)

queues = models.ManyToManyField(
Queue,
verbose_name=_('Authorized Queues'),
)

def __unicode__(self):
return '%s authorized for queues %s' % (self.user, ", ".join(self.queues.values_list('title', flat=True)))

class Meta:
verbose_name = _('Queue Membership')
verbose_name_plural = _('Queue Memberships')
10 changes: 7 additions & 3 deletions helpdesk/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@


''' options for update_ticket views '''
# allow non-staff users to interact with tickets? this will also change how 'staff_member_required'
# allow non-staff users to interact with tickets? this will also change how 'staff_member_required'
# in staff.py will be defined.
HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE = getattr(settings, 'HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE', False)

Expand All @@ -73,10 +73,10 @@
# make all updates public by default? this will hide the 'is this update public' checkbox
HELPDESK_UPDATE_PUBLIC_DEFAULT = getattr(settings, 'HELPDESK_UPDATE_PUBLIC_DEFAULT', False)

# only show staff users in ticket owner drop-downs
# only show staff users in ticket owner drop-downs
HELPDESK_STAFF_ONLY_TICKET_OWNERS = getattr(settings, 'HELPDESK_STAFF_ONLY_TICKET_OWNERS', False)

# only show staff users in ticket cc drop-down
# only show staff users in ticket cc drop-down
HELPDESK_STAFF_ONLY_TICKET_CC = getattr(settings, 'HELPDESK_STAFF_ONLY_TICKET_CC', False)


Expand All @@ -97,3 +97,7 @@
QUEUE_EMAIL_BOX_HOST = getattr(settings, 'QUEUE_EMAIL_BOX_HOST', None)
QUEUE_EMAIL_BOX_USER = getattr(settings, 'QUEUE_EMAIL_BOX_USER', None)
QUEUE_EMAIL_BOX_PASSWORD = getattr(settings, 'QUEUE_EMAIL_BOX_PASSWORD', None)


# only allow users to access queues that they are members of?
HELPDESK_ENABLE_PER_QUEUE_STAFF_MEMBERSHIP = getattr(settings, 'HELPDESK_ENABLE_PER_QUEUE_STAFF_MEMBERSHIP', False)
3 changes: 2 additions & 1 deletion helpdesk/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from helpdesk.tests.ticket_submission import *
from helpdesk.tests.public_actions import *
from helpdesk.tests.navigation import *
from helpdesk.tests.navigation import *
from helpdesk.tests.per_queue_staff_membership import *
221 changes: 221 additions & 0 deletions helpdesk/tests/per_queue_staff_membership.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
from django.contrib.auth import get_user_model
from django.core.urlresolvers import reverse
from django.test import TestCase
from django.test.client import Client

from helpdesk.models import Queue, Ticket, QueueMembership
from helpdesk import settings


class PerQueueStaffMembershipTestCase(TestCase):

IDENTIFIERS = (1, 2)

def setUp(self):
"""
Create user_1 with access to queue_1 containing 1 ticket
and user_2 with access to queue_2 containing 2 tickets
and superuser who should be able to access both queues
"""
settings.HELPDESK_ENABLE_PER_QUEUE_STAFF_MEMBERSHIP = True
self.client = Client()
User = get_user_model()

self.superuser = User.objects.create(
username='superuser',
is_staff=True,
is_superuser=True,
)
self.superuser.set_password('superuser')
self.superuser.save()

for identifier in self.IDENTIFIERS:
queue = self.__dict__['queue_%d' % identifier] = Queue.objects.create(
title='Queue %d' % identifier,
slug='q%d' % identifier,
)

user = self.__dict__['user_%d' % identifier] = User.objects.create(
username='User_%d' % identifier,
is_staff=True,
)
user.set_password(identifier)
user.save()

queue_membership = self.__dict__['queue_membership_%d' % identifier] = QueueMembership.objects.create(
user=user,
)
queue_membership.queues = (queue,)
queue_membership.save()

for ticket_number in range(1, identifier + 1):
Ticket.objects.create(
title='Unassigned Ticket %d in Queue %d' % (ticket_number, identifier),
queue=queue,
)
Ticket.objects.create(
title='Ticket %d in Queue %d Assigned to User_%d' % (ticket_number, identifier, identifier),
queue=queue,
assigned_to=user,
)

def test_dashboard_ticket_counts(self):
"""
Check that the regular users' dashboard only shows 1 of the 2 queues,
that user_1 only sees a total of 1 ticket, that user_2 sees a total of 2
tickets, but that the superuser's dashboard shows all queues and tickets.
"""

# Regular users
for identifier in self.IDENTIFIERS:
self.client.login(username='User_%d' % identifier, password=identifier)
response = self.client.get(reverse('helpdesk_dashboard'))
self.assertEqual(
len(response.context['unassigned_tickets']),
identifier,
'Unassigned tickets were not properly limited by queue membership'
)
self.assertEqual(
len(response.context['dash_tickets']),
1,
'The queues in dash_tickets were not properly limited by queue membership'
)
self.assertEqual(
response.context['dash_tickets'][0]['open'],
identifier * 2,
'The tickets in dash_tickets were not properly limited by queue membership'
)
self.assertEqual(
response.context['basic_ticket_stats']['open_ticket_stats'][0][1],
identifier * 2,
'Basic ticket stats were not properly limited by queue membership'
)

# Superuser
self.client.login(username='superuser', password='superuser')
response = self.client.get(reverse('helpdesk_dashboard'))
self.assertEqual(
len(response.context['unassigned_tickets']),
3,
'Unassigned tickets were limited by queue membership for a superuser'
)
self.assertEqual(
len(response.context['dash_tickets']),
2,
'The queues in dash_tickets were limited by queue membership for a superuser'
)
self.assertEqual(
response.context['dash_tickets'][0]['open'] +
response.context['dash_tickets'][1]['open'],
6,
'The tickets in dash_tickets were limited by queue membership for a superuser'
)
self.assertEqual(
response.context['basic_ticket_stats']['open_ticket_stats'][0][1] +
response.context['basic_ticket_stats']['open_ticket_stats'][1][1],
6,
'Basic ticket stats were limited by queue membership for a superuser'
)

def test_ticket_list_per_queue_user_restrictions(self):
"""
Ensure that while the superuser can list all tickets, user_1 can only
list the 1 ticket in his queue and user_2 can list only the 2 tickets
in his queue.
"""
# Regular users
for identifier in self.IDENTIFIERS:
self.client.login(username='User_%d' % identifier, password=identifier)
response = self.client.get(reverse('helpdesk_list'))
self.assertEqual(
len(response.context['tickets']),
identifier * 2,
'Ticket list was not properly limited by queue membership'
)
self.assertEqual(
len(response.context['queue_choices']),
1,
'Queue choices were not properly limited by queue membership'
)
self.assertEqual(
response.context['queue_choices'][0],
Queue.objects.get(title="Queue %d" % identifier),
'Queue choices were not properly limited by queue membership'
)

# Superuser
self.client.login(username='superuser', password='superuser')
response = self.client.get(reverse('helpdesk_list'))
self.assertEqual(
len(response.context['tickets']),
6,
'Ticket list was limited by queue membership for a superuser'
)

def test_ticket_reports_per_queue_user_restrictions(self):
"""
Ensure that while the superuser can generate reports on all queues and
tickets, user_1 can only generate reports for queue 1 and user_2 can
only do so for queue 2
"""
# Regular users
for identifier in self.IDENTIFIERS:
self.client.login(username='User_%d' % identifier, password=identifier)
response = self.client.get(
reverse('helpdesk_run_report', kwargs={'report': 'userqueue'})
)
# Only two columns of data should be present: ticket counts for
# unassigned and this user only
self.assertEqual(
len(response.context['data']),
2,
'Queues in report were not properly limited by queue membership'
)
# Each user should see a total number of tickets equal to twice their ID
self.assertEqual(
sum([sum(user_tickets[1:]) for user_tickets in response.context['data']]),
identifier * 2,
'Tickets in report were not properly limited by queue membership'
)
# Each user should only be able to pick 1 queue
self.assertEqual(
len(response.context['headings']),
2,
'Queue choices were not properly limited by queue membership'
)
# The queue each user can pick should be the queue named after their ID
self.assertEqual(
response.context['headings'][1],
"Queue %d" % identifier,
'Queue choices were not properly limited by queue membership'
)

# Superuser
self.client.login(username='superuser', password='superuser')
response = self.client.get(
reverse('helpdesk_run_report', kwargs={'report': 'userqueue'})
)
# Superuser should see ticket counts for all two queues, which includes
# three columns: unassigned and both user 1 and user 2
self.assertEqual(
len(response.context['data'][0]),
3,
'Queues in report were improperly limited by queue membership for a superuser'
)
# Superuser should see the total ticket count of three tickets
self.assertEqual(
sum([sum(user_tickets[1:]) for user_tickets in response.context['data']]),
6,
'Tickets in report were improperly limited by queue membership for a superuser'
)
self.assertEqual(
len(response.context['headings']),
3,
'Queue choices were improperly limited by queue membership for a superuser'
)

def tearDown(self):
"""
Don't interfere with subsequent tests that do not expect this setting
"""
settings.HELPDESK_ENABLE_PER_QUEUE_STAFF_MEMBERSHIP = False

0 comments on commit 84330ee

Please sign in to comment.