mirror of
https://gitea.mueller.network/extern/django-helpdesk.git
synced 2024-11-25 01:13:31 +01:00
Merge pull request #803 from auto-mat/teams
Add Teams functionality, using pinax-teams
This commit is contained in:
commit
0e9358e61b
@ -15,6 +15,8 @@ Contents
|
||||
settings
|
||||
spam
|
||||
custom_fields
|
||||
integration
|
||||
teams
|
||||
contributing
|
||||
license
|
||||
|
||||
|
@ -58,7 +58,11 @@ errors with trying to create User settings.
|
||||
'django.contrib.admin', # Required for helpdesk admin/maintenance
|
||||
'django.contrib.humanize', # Required for elapsed time formatting
|
||||
'bootstrap4form', # Required for nicer formatting of forms with the default templates
|
||||
'account', # Required by pinax-teams
|
||||
'pinax.inviations', # required by pinax-teams
|
||||
'pinax.teams', # team support
|
||||
'helpdesk', # This is us!
|
||||
'reversion', # required by pinax-teams
|
||||
)
|
||||
|
||||
Your ``settings.py`` file should also define a ``SITE_ID`` that allows multiple projects to share
|
||||
|
14
docs/teams.rst
Normal file
14
docs/teams.rst
Normal file
@ -0,0 +1,14 @@
|
||||
Working with teams and larger organizations
|
||||
===========================================
|
||||
|
||||
If you only have one or two people working on tickets, basic Queue setup is enough to get you going. You can now assign tickets to teams for better ticket filtering, reducing noise and improving organization efficiency.
|
||||
|
||||
Rather than assigning tickets to teams directly, django-helpdesk allows you assign tickets to knowledge-base items and then assign knowledge base items to teams.
|
||||
|
||||
Knowledge-base items can be in either public or private knowledge-base categories, so this organizational structure need not have any influence on the external appearance of your public helpdesk web portal.
|
||||
|
||||
You can visit the 'Pinax Teams' page in your django admin in order to create a team and add team members.
|
||||
|
||||
You can assign a knowledge-base item to a team on the Helpdesk admin page.
|
||||
|
||||
Once you have set up teams. Unassigned tickets which are associated with a knowledge-base item will only be shown on the dashboard to those users who are members of the team which is associated with that knowledge-base item.
|
@ -70,7 +70,7 @@ class FollowUpAdmin(admin.ModelAdmin):
|
||||
|
||||
@admin.register(KBItem)
|
||||
class KBItemAdmin(admin.ModelAdmin):
|
||||
list_display = ('category', 'title', 'last_updated',)
|
||||
list_display = ('category', 'title', 'last_updated', 'team', 'order', 'enabled')
|
||||
inlines = [KBIAttachmentInline]
|
||||
readonly_fields = ('voted_by', 'downvoted_by')
|
||||
|
||||
@ -93,6 +93,10 @@ class IgnoreEmailAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'queue_list', 'email_address', 'keep_in_mailbox')
|
||||
|
||||
|
||||
@admin.register(KBCategory)
|
||||
class KBCategoryAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'title', 'slug', 'public')
|
||||
|
||||
|
||||
admin.site.register(PreSetReply)
|
||||
admin.site.register(EscalationExclusion)
|
||||
admin.site.register(KBCategory)
|
||||
|
@ -183,8 +183,8 @@ class AbstractTicketForm(CustomFieldMixin, forms.Form):
|
||||
self.fields['kbitem'] = forms.ChoiceField(
|
||||
widget=forms.Select(attrs={'class': 'form-control'}),
|
||||
required=False,
|
||||
label=_('Knowedge Base Item'),
|
||||
choices=[(kbi.pk, kbi.title) for kbi in KBItem.objects.filter(category=kbcategory.pk)],
|
||||
label=_('Knowledge Base Item'),
|
||||
choices=[(kbi.pk, kbi.title) for kbi in KBItem.objects.filter(category=kbcategory.pk, enabled=True)],
|
||||
)
|
||||
|
||||
def _add_form_custom_fields(self, staff_only_filter=None):
|
||||
|
20
helpdesk/migrations/0028_kbitem_team.py
Normal file
20
helpdesk/migrations/0028_kbitem_team.py
Normal file
@ -0,0 +1,20 @@
|
||||
# Generated by Django 2.2.9 on 2020-01-27 15:01
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pinax_teams', '0004_auto_20170511_0856'),
|
||||
('helpdesk', '0027_auto_20200107_1221'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='kbitem',
|
||||
name='team',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='pinax_teams.Team', verbose_name='Team'),
|
||||
),
|
||||
]
|
18
helpdesk/migrations/0029_kbcategory_public.py
Normal file
18
helpdesk/migrations/0029_kbcategory_public.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 2.2.9 on 2020-01-27 16:00
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('helpdesk', '0028_kbitem_team'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='kbcategory',
|
||||
name='public',
|
||||
field=models.BooleanField(default=True, verbose_name='Is KBCategory publicly visible?'),
|
||||
),
|
||||
]
|
33
helpdesk/migrations/0030_add_kbcategory_name.py
Normal file
33
helpdesk/migrations/0030_add_kbcategory_name.py
Normal file
@ -0,0 +1,33 @@
|
||||
# Generated by Django 2.2.10 on 2020-02-25 11:21
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
def copy_title(apps, schema_editor):
|
||||
KBCategory = apps.get_model("helpdesk", "KBCategory")
|
||||
KBCategory.objects.update(name=models.F('title'))
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('helpdesk', '0029_kbcategory_public'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='kbcategory',
|
||||
name='name',
|
||||
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Name of the category'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='kbcategory',
|
||||
name='title',
|
||||
field=models.CharField(max_length=100, verbose_name='Title on knowledgebase page'),
|
||||
),
|
||||
migrations.RunPython(copy_title, migrations.RunPython.noop),
|
||||
migrations.AlterField(
|
||||
model_name='kbcategory',
|
||||
name='name',
|
||||
field=models.CharField(blank=False, max_length=100, null=False, verbose_name='Name of the category'),
|
||||
),
|
||||
]
|
22
helpdesk/migrations/0031_auto_20200225_1440.py
Normal file
22
helpdesk/migrations/0031_auto_20200225_1440.py
Normal file
@ -0,0 +1,22 @@
|
||||
# Generated by Django 2.2.10 on 2020-02-25 13:40
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('helpdesk', '0030_add_kbcategory_name'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='kbitem',
|
||||
options={'ordering': ('order', 'title'), 'verbose_name': 'Knowledge base item', 'verbose_name_plural': 'Knowledge base items'},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='kbitem',
|
||||
name='order',
|
||||
field=models.PositiveIntegerField(blank=True, null=True, verbose_name='Order'),
|
||||
),
|
||||
]
|
18
helpdesk/migrations/0032_kbitem_enabled.py
Normal file
18
helpdesk/migrations/0032_kbitem_enabled.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 2.2.10 on 2020-02-25 13:44
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('helpdesk', '0031_auto_20200225_1440'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='kbitem',
|
||||
name='enabled',
|
||||
field=models.BooleanField(default=True, verbose_name='Enabled to display to users'),
|
||||
),
|
||||
]
|
@ -25,6 +25,8 @@ from django.utils.safestring import mark_safe
|
||||
from markdown import markdown
|
||||
from markdown.extensions import Extension
|
||||
|
||||
import pinax.teams.models
|
||||
|
||||
|
||||
import uuid
|
||||
|
||||
@ -1213,8 +1215,13 @@ class KBCategory(models.Model):
|
||||
listing of questions & answers.
|
||||
"""
|
||||
|
||||
name = models.CharField(
|
||||
_('Name of the category'),
|
||||
max_length=100,
|
||||
)
|
||||
|
||||
title = models.CharField(
|
||||
_('Title'),
|
||||
_('Title on knowledgebase page'),
|
||||
max_length=100,
|
||||
)
|
||||
|
||||
@ -1234,8 +1241,13 @@ class KBCategory(models.Model):
|
||||
verbose_name=_('Default queue when creating a ticket after viewing this category.'),
|
||||
)
|
||||
|
||||
public = models.BooleanField(
|
||||
default=True,
|
||||
verbose_name=_("Is KBCategory publicly visible?")
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return '%s' % self.title
|
||||
return '%s' % self.name
|
||||
|
||||
class Meta:
|
||||
ordering = ('title',)
|
||||
@ -1297,6 +1309,25 @@ class KBItem(models.Model):
|
||||
blank=True,
|
||||
)
|
||||
|
||||
team = models.ForeignKey(
|
||||
pinax.teams.models.Team,
|
||||
on_delete=models.CASCADE,
|
||||
verbose_name=_('Team'),
|
||||
blank=True,
|
||||
null=True,
|
||||
)
|
||||
|
||||
order = models.PositiveIntegerField(
|
||||
_('Order'),
|
||||
blank=True,
|
||||
null=True,
|
||||
)
|
||||
|
||||
enabled = models.BooleanField(
|
||||
_('Enabled to display to users'),
|
||||
default=True,
|
||||
)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.last_updated:
|
||||
self.last_updated = timezone.now()
|
||||
@ -1310,10 +1341,10 @@ class KBItem(models.Model):
|
||||
score = property(_score)
|
||||
|
||||
def __str__(self):
|
||||
return '%s' % self.title
|
||||
return '%s: %s' % (self.category.title, self.title)
|
||||
|
||||
class Meta:
|
||||
ordering = ('title',)
|
||||
ordering = ('order', 'title',)
|
||||
verbose_name = _('Knowledge base item')
|
||||
verbose_name_plural = _('Knowledge base items')
|
||||
|
||||
@ -1328,6 +1359,9 @@ class KBItem(models.Model):
|
||||
def num_open_tickets(self):
|
||||
return Ticket.objects.filter(kbitem=self, status__in=(1, 2)).count()
|
||||
|
||||
def unassigned_tickets(self):
|
||||
return Ticket.objects.filter(kbitem=self, status__in=(1, 2), assigned_to__isnull=True)
|
||||
|
||||
def get_markdown(self):
|
||||
return get_markdown(self.answer)
|
||||
|
||||
|
@ -77,13 +77,16 @@ def get_search_filter_args(search):
|
||||
|
||||
DATATABLES_ORDER_COLUMN_CHOICES = Choices(
|
||||
('0', 'id'),
|
||||
('1', 'title'),
|
||||
('2', 'priority'),
|
||||
('3', 'title'),
|
||||
('4', 'queue'),
|
||||
('5', 'status'),
|
||||
('6', 'created'),
|
||||
('7', 'due_date'),
|
||||
('8', 'assigned_to')
|
||||
('3', 'queue'),
|
||||
('4', 'status'),
|
||||
('5', 'created'),
|
||||
('6', 'due_date'),
|
||||
('7', 'assigned_to'),
|
||||
('8', 'submitter_email'),
|
||||
# ('9', 'time_spent'),
|
||||
('10', 'kbitem'),
|
||||
)
|
||||
|
||||
|
||||
@ -123,10 +126,9 @@ class __Query__:
|
||||
|
||||
sorting: The name of the column to sort by
|
||||
"""
|
||||
for key in self.params.get('filtering', {}).keys():
|
||||
filter = {key: self.params['filtering'][key]}
|
||||
queryset = queryset.filter(**filter)
|
||||
queryset = queryset.filter(self.get_search_filter_args())
|
||||
filter = self.params.get('filtering', {})
|
||||
filter_or = self.params.get('filtering_or', {})
|
||||
queryset = queryset.filter((Q(**filter) | Q(**filter_or)) & self.get_search_filter_args())
|
||||
sorting = self.params.get('sorting', None)
|
||||
if sorting:
|
||||
sortreverse = self.params.get('sortreverse', None)
|
||||
@ -162,12 +164,12 @@ class __Query__:
|
||||
"""
|
||||
objects = self.get()
|
||||
order_by = '-date_created'
|
||||
draw = int(kwargs.get('draw', None)[0])
|
||||
length = int(kwargs.get('length', None)[0])
|
||||
start = int(kwargs.get('start', None)[0])
|
||||
search_value = kwargs.get('search[value]', None)[0]
|
||||
order_column = kwargs.get('order[0][column]', None)[0]
|
||||
order = kwargs.get('order[0][dir]', None)[0]
|
||||
draw = int(kwargs.get('draw', [0])[0])
|
||||
length = int(kwargs.get('length', [25])[0])
|
||||
start = int(kwargs.get('start', [0])[0])
|
||||
search_value = kwargs.get('search[value]', [""])[0]
|
||||
order_column = kwargs.get('order[0][column]', ['5'])[0]
|
||||
order = kwargs.get('order[0][dir]', ["asc"])[0]
|
||||
|
||||
order_column = DATATABLES_ORDER_COLUMN_CHOICES[order_column]
|
||||
# django orm '-' -> desc
|
||||
@ -177,7 +179,7 @@ class __Query__:
|
||||
queryset = objects.all().order_by(order_by)
|
||||
total = queryset.count()
|
||||
|
||||
if search_value:
|
||||
if search_value: # Dead code currently
|
||||
queryset = queryset.filter(get_search_filter_args(search_value))
|
||||
|
||||
count = queryset.count()
|
||||
|
@ -22,13 +22,14 @@ class DatatablesTicketSerializer(serializers.ModelSerializer):
|
||||
row_class = serializers.SerializerMethodField()
|
||||
time_spent = serializers.SerializerMethodField()
|
||||
queue = serializers.SerializerMethodField()
|
||||
kbitem = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Ticket
|
||||
# fields = '__all__'
|
||||
fields = ('ticket', 'id', 'priority', 'title', 'queue', 'status',
|
||||
'created', 'due_date', 'assigned_to', 'submitter', 'row_class',
|
||||
'time_spent')
|
||||
'time_spent', 'kbitem')
|
||||
|
||||
def get_queue(self, obj):
|
||||
return ({"title": obj.queue.title, "id": obj.queue.id})
|
||||
@ -62,3 +63,6 @@ class DatatablesTicketSerializer(serializers.ModelSerializer):
|
||||
|
||||
def get_row_class(self, obj):
|
||||
return (obj.get_priority_css_class)
|
||||
|
||||
def get_kbitem(self, obj):
|
||||
return obj.kbitem.title if obj.kbitem else ""
|
||||
|
@ -242,6 +242,10 @@ body.fixed-nav.sidebar-toggled #content-wrapper {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.card-text {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.card-body-icon {
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
|
380
helpdesk/static/helpdesk/vendor/datatables/css/buttons.dataTables.css
vendored
Normal file
380
helpdesk/static/helpdesk/vendor/datatables/css/buttons.dataTables.css
vendored
Normal file
@ -0,0 +1,380 @@
|
||||
@keyframes dtb-spinner {
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
@-o-keyframes dtb-spinner {
|
||||
100% {
|
||||
-o-transform: rotate(360deg);
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
@-ms-keyframes dtb-spinner {
|
||||
100% {
|
||||
-ms-transform: rotate(360deg);
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
@-webkit-keyframes dtb-spinner {
|
||||
100% {
|
||||
-webkit-transform: rotate(360deg);
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
@-moz-keyframes dtb-spinner {
|
||||
100% {
|
||||
-moz-transform: rotate(360deg);
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
div.dt-button-info {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 400px;
|
||||
margin-top: -100px;
|
||||
margin-left: -200px;
|
||||
background-color: white;
|
||||
border: 2px solid #111;
|
||||
box-shadow: 3px 3px 8px rgba(0, 0, 0, 0.3);
|
||||
border-radius: 3px;
|
||||
text-align: center;
|
||||
z-index: 21;
|
||||
}
|
||||
div.dt-button-info h2 {
|
||||
padding: 0.5em;
|
||||
margin: 0;
|
||||
font-weight: normal;
|
||||
border-bottom: 1px solid #ddd;
|
||||
background-color: #f3f3f3;
|
||||
}
|
||||
div.dt-button-info > div {
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
div.dt-button-collection-title {
|
||||
text-align: center;
|
||||
padding: 0.3em 0 0.5em;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
div.dt-button-collection-title:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
button.dt-button,
|
||||
div.dt-button,
|
||||
a.dt-button {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
box-sizing: border-box;
|
||||
margin-right: 0.333em;
|
||||
margin-bottom: 0.333em;
|
||||
padding: 0.5em 1em;
|
||||
border: 1px solid #999;
|
||||
border-radius: 2px;
|
||||
cursor: pointer;
|
||||
font-size: 0.88em;
|
||||
line-height: 1.6em;
|
||||
color: black;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
background-color: #e9e9e9;
|
||||
/* Fallback */
|
||||
background-image: -webkit-linear-gradient(top, white 0%, #e9e9e9 100%);
|
||||
/* Chrome 10+, Saf5.1+, iOS 5+ */
|
||||
background-image: -moz-linear-gradient(top, white 0%, #e9e9e9 100%);
|
||||
/* FF3.6 */
|
||||
background-image: -ms-linear-gradient(top, white 0%, #e9e9e9 100%);
|
||||
/* IE10 */
|
||||
background-image: -o-linear-gradient(top, white 0%, #e9e9e9 100%);
|
||||
/* Opera 11.10+ */
|
||||
background-image: linear-gradient(to bottom, white 0%, #e9e9e9 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0,StartColorStr='white', EndColorStr='#e9e9e9');
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
text-decoration: none;
|
||||
outline: none;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
button.dt-button.disabled,
|
||||
div.dt-button.disabled,
|
||||
a.dt-button.disabled {
|
||||
color: #999;
|
||||
border: 1px solid #d0d0d0;
|
||||
cursor: default;
|
||||
background-color: #f9f9f9;
|
||||
/* Fallback */
|
||||
background-image: -webkit-linear-gradient(top, #ffffff 0%, #f9f9f9 100%);
|
||||
/* Chrome 10+, Saf5.1+, iOS 5+ */
|
||||
background-image: -moz-linear-gradient(top, #ffffff 0%, #f9f9f9 100%);
|
||||
/* FF3.6 */
|
||||
background-image: -ms-linear-gradient(top, #ffffff 0%, #f9f9f9 100%);
|
||||
/* IE10 */
|
||||
background-image: -o-linear-gradient(top, #ffffff 0%, #f9f9f9 100%);
|
||||
/* Opera 11.10+ */
|
||||
background-image: linear-gradient(to bottom, #ffffff 0%, #f9f9f9 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0,StartColorStr='#ffffff', EndColorStr='#f9f9f9');
|
||||
}
|
||||
button.dt-button:active:not(.disabled), button.dt-button.active:not(.disabled),
|
||||
div.dt-button:active:not(.disabled),
|
||||
div.dt-button.active:not(.disabled),
|
||||
a.dt-button:active:not(.disabled),
|
||||
a.dt-button.active:not(.disabled) {
|
||||
background-color: #e2e2e2;
|
||||
/* Fallback */
|
||||
background-image: -webkit-linear-gradient(top, #f3f3f3 0%, #e2e2e2 100%);
|
||||
/* Chrome 10+, Saf5.1+, iOS 5+ */
|
||||
background-image: -moz-linear-gradient(top, #f3f3f3 0%, #e2e2e2 100%);
|
||||
/* FF3.6 */
|
||||
background-image: -ms-linear-gradient(top, #f3f3f3 0%, #e2e2e2 100%);
|
||||
/* IE10 */
|
||||
background-image: -o-linear-gradient(top, #f3f3f3 0%, #e2e2e2 100%);
|
||||
/* Opera 11.10+ */
|
||||
background-image: linear-gradient(to bottom, #f3f3f3 0%, #e2e2e2 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0,StartColorStr='#f3f3f3', EndColorStr='#e2e2e2');
|
||||
box-shadow: inset 1px 1px 3px #999999;
|
||||
}
|
||||
button.dt-button:active:not(.disabled):hover:not(.disabled), button.dt-button.active:not(.disabled):hover:not(.disabled),
|
||||
div.dt-button:active:not(.disabled):hover:not(.disabled),
|
||||
div.dt-button.active:not(.disabled):hover:not(.disabled),
|
||||
a.dt-button:active:not(.disabled):hover:not(.disabled),
|
||||
a.dt-button.active:not(.disabled):hover:not(.disabled) {
|
||||
box-shadow: inset 1px 1px 3px #999999;
|
||||
background-color: #cccccc;
|
||||
/* Fallback */
|
||||
background-image: -webkit-linear-gradient(top, #eaeaea 0%, #cccccc 100%);
|
||||
/* Chrome 10+, Saf5.1+, iOS 5+ */
|
||||
background-image: -moz-linear-gradient(top, #eaeaea 0%, #cccccc 100%);
|
||||
/* FF3.6 */
|
||||
background-image: -ms-linear-gradient(top, #eaeaea 0%, #cccccc 100%);
|
||||
/* IE10 */
|
||||
background-image: -o-linear-gradient(top, #eaeaea 0%, #cccccc 100%);
|
||||
/* Opera 11.10+ */
|
||||
background-image: linear-gradient(to bottom, #eaeaea 0%, #cccccc 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0,StartColorStr='#eaeaea', EndColorStr='#cccccc');
|
||||
}
|
||||
button.dt-button:hover,
|
||||
div.dt-button:hover,
|
||||
a.dt-button:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
button.dt-button:hover:not(.disabled),
|
||||
div.dt-button:hover:not(.disabled),
|
||||
a.dt-button:hover:not(.disabled) {
|
||||
border: 1px solid #666;
|
||||
background-color: #e0e0e0;
|
||||
/* Fallback */
|
||||
background-image: -webkit-linear-gradient(top, #f9f9f9 0%, #e0e0e0 100%);
|
||||
/* Chrome 10+, Saf5.1+, iOS 5+ */
|
||||
background-image: -moz-linear-gradient(top, #f9f9f9 0%, #e0e0e0 100%);
|
||||
/* FF3.6 */
|
||||
background-image: -ms-linear-gradient(top, #f9f9f9 0%, #e0e0e0 100%);
|
||||
/* IE10 */
|
||||
background-image: -o-linear-gradient(top, #f9f9f9 0%, #e0e0e0 100%);
|
||||
/* Opera 11.10+ */
|
||||
background-image: linear-gradient(to bottom, #f9f9f9 0%, #e0e0e0 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0,StartColorStr='#f9f9f9', EndColorStr='#e0e0e0');
|
||||
}
|
||||
button.dt-button:focus:not(.disabled),
|
||||
div.dt-button:focus:not(.disabled),
|
||||
a.dt-button:focus:not(.disabled) {
|
||||
border: 1px solid #426c9e;
|
||||
text-shadow: 0 1px 0 #c4def1;
|
||||
outline: none;
|
||||
background-color: #79ace9;
|
||||
/* Fallback */
|
||||
background-image: -webkit-linear-gradient(top, #bddef4 0%, #79ace9 100%);
|
||||
/* Chrome 10+, Saf5.1+, iOS 5+ */
|
||||
background-image: -moz-linear-gradient(top, #bddef4 0%, #79ace9 100%);
|
||||
/* FF3.6 */
|
||||
background-image: -ms-linear-gradient(top, #bddef4 0%, #79ace9 100%);
|
||||
/* IE10 */
|
||||
background-image: -o-linear-gradient(top, #bddef4 0%, #79ace9 100%);
|
||||
/* Opera 11.10+ */
|
||||
background-image: linear-gradient(to bottom, #bddef4 0%, #79ace9 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0,StartColorStr='#bddef4', EndColorStr='#79ace9');
|
||||
}
|
||||
|
||||
.dt-button embed {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
div.dt-buttons {
|
||||
position: relative;
|
||||
float: left;
|
||||
}
|
||||
div.dt-buttons.buttons-right {
|
||||
float: right;
|
||||
}
|
||||
|
||||
div.dt-button-collection {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 150px;
|
||||
margin-top: 3px;
|
||||
padding: 8px 8px 4px 8px;
|
||||
border: 1px solid #ccc;
|
||||
border: 1px solid rgba(0, 0, 0, 0.4);
|
||||
background-color: white;
|
||||
overflow: hidden;
|
||||
z-index: 2002;
|
||||
border-radius: 5px;
|
||||
box-shadow: 3px 3px 5px rgba(0, 0, 0, 0.3);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
div.dt-button-collection button.dt-button,
|
||||
div.dt-button-collection div.dt-button,
|
||||
div.dt-button-collection a.dt-button {
|
||||
position: relative;
|
||||
left: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
display: block;
|
||||
float: none;
|
||||
margin-bottom: 4px;
|
||||
margin-right: 0;
|
||||
}
|
||||
div.dt-button-collection button.dt-button:active:not(.disabled), div.dt-button-collection button.dt-button.active:not(.disabled),
|
||||
div.dt-button-collection div.dt-button:active:not(.disabled),
|
||||
div.dt-button-collection div.dt-button.active:not(.disabled),
|
||||
div.dt-button-collection a.dt-button:active:not(.disabled),
|
||||
div.dt-button-collection a.dt-button.active:not(.disabled) {
|
||||
background-color: #dadada;
|
||||
/* Fallback */
|
||||
background-image: -webkit-linear-gradient(top, #f0f0f0 0%, #dadada 100%);
|
||||
/* Chrome 10+, Saf5.1+, iOS 5+ */
|
||||
background-image: -moz-linear-gradient(top, #f0f0f0 0%, #dadada 100%);
|
||||
/* FF3.6 */
|
||||
background-image: -ms-linear-gradient(top, #f0f0f0 0%, #dadada 100%);
|
||||
/* IE10 */
|
||||
background-image: -o-linear-gradient(top, #f0f0f0 0%, #dadada 100%);
|
||||
/* Opera 11.10+ */
|
||||
background-image: linear-gradient(to bottom, #f0f0f0 0%, #dadada 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0,StartColorStr='#f0f0f0', EndColorStr='#dadada');
|
||||
box-shadow: inset 1px 1px 3px #666;
|
||||
}
|
||||
div.dt-button-collection.fixed {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
margin-left: -75px;
|
||||
border-radius: 0;
|
||||
}
|
||||
div.dt-button-collection.fixed.two-column {
|
||||
margin-left: -200px;
|
||||
}
|
||||
div.dt-button-collection.fixed.three-column {
|
||||
margin-left: -225px;
|
||||
}
|
||||
div.dt-button-collection.fixed.four-column {
|
||||
margin-left: -300px;
|
||||
}
|
||||
div.dt-button-collection > :last-child {
|
||||
display: block !important;
|
||||
-webkit-column-gap: 8px;
|
||||
-moz-column-gap: 8px;
|
||||
-ms-column-gap: 8px;
|
||||
-o-column-gap: 8px;
|
||||
column-gap: 8px;
|
||||
}
|
||||
div.dt-button-collection > :last-child > * {
|
||||
-webkit-column-break-inside: avoid;
|
||||
break-inside: avoid;
|
||||
}
|
||||
div.dt-button-collection.two-column {
|
||||
width: 400px;
|
||||
}
|
||||
div.dt-button-collection.two-column > :last-child {
|
||||
padding-bottom: 1px;
|
||||
-webkit-column-count: 2;
|
||||
-moz-column-count: 2;
|
||||
-ms-column-count: 2;
|
||||
-o-column-count: 2;
|
||||
column-count: 2;
|
||||
}
|
||||
div.dt-button-collection.three-column {
|
||||
width: 450px;
|
||||
}
|
||||
div.dt-button-collection.three-column > :last-child {
|
||||
padding-bottom: 1px;
|
||||
-webkit-column-count: 3;
|
||||
-moz-column-count: 3;
|
||||
-ms-column-count: 3;
|
||||
-o-column-count: 3;
|
||||
column-count: 3;
|
||||
}
|
||||
div.dt-button-collection.four-column {
|
||||
width: 600px;
|
||||
}
|
||||
div.dt-button-collection.four-column > :last-child {
|
||||
padding-bottom: 1px;
|
||||
-webkit-column-count: 4;
|
||||
-moz-column-count: 4;
|
||||
-ms-column-count: 4;
|
||||
-o-column-count: 4;
|
||||
column-count: 4;
|
||||
}
|
||||
div.dt-button-collection .dt-button {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
div.dt-button-background {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
/* Fallback */
|
||||
background: -ms-radial-gradient(center, ellipse farthest-corner, rgba(0, 0, 0, 0.3) 0%, rgba(0, 0, 0, 0.7) 100%);
|
||||
/* IE10 Consumer Preview */
|
||||
background: -moz-radial-gradient(center, ellipse farthest-corner, rgba(0, 0, 0, 0.3) 0%, rgba(0, 0, 0, 0.7) 100%);
|
||||
/* Firefox */
|
||||
background: -o-radial-gradient(center, ellipse farthest-corner, rgba(0, 0, 0, 0.3) 0%, rgba(0, 0, 0, 0.7) 100%);
|
||||
/* Opera */
|
||||
background: -webkit-gradient(radial, center center, 0, center center, 497, color-stop(0, rgba(0, 0, 0, 0.3)), color-stop(1, rgba(0, 0, 0, 0.7)));
|
||||
/* Webkit (Safari/Chrome 10) */
|
||||
background: -webkit-radial-gradient(center, ellipse farthest-corner, rgba(0, 0, 0, 0.3) 0%, rgba(0, 0, 0, 0.7) 100%);
|
||||
/* Webkit (Chrome 11+) */
|
||||
background: radial-gradient(ellipse farthest-corner at center, rgba(0, 0, 0, 0.3) 0%, rgba(0, 0, 0, 0.7) 100%);
|
||||
/* W3C Markup, IE10 Release Preview */
|
||||
z-index: 2001;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 640px) {
|
||||
div.dt-buttons {
|
||||
float: none !important;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
button.dt-button.processing,
|
||||
div.dt-button.processing,
|
||||
a.dt-button.processing {
|
||||
color: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
button.dt-button.processing:after,
|
||||
div.dt-button.processing:after,
|
||||
a.dt-button.processing:after {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin: -8px 0 0 -8px;
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
content: ' ';
|
||||
border: 2px solid #282828;
|
||||
border-radius: 50%;
|
||||
border-left-color: transparent;
|
||||
border-right-color: transparent;
|
||||
animation: dtb-spinner 1500ms infinite linear;
|
||||
-o-animation: dtb-spinner 1500ms infinite linear;
|
||||
-ms-animation: dtb-spinner 1500ms infinite linear;
|
||||
-webkit-animation: dtb-spinner 1500ms infinite linear;
|
||||
-moz-animation: dtb-spinner 1500ms infinite linear;
|
||||
}
|
206
helpdesk/static/helpdesk/vendor/datatables/js/buttons.colVis.js
vendored
Normal file
206
helpdesk/static/helpdesk/vendor/datatables/js/buttons.colVis.js
vendored
Normal file
@ -0,0 +1,206 @@
|
||||
/*!
|
||||
* Column visibility buttons for Buttons and DataTables.
|
||||
* 2016 SpryMedia Ltd - datatables.net/license
|
||||
*/
|
||||
|
||||
(function( factory ){
|
||||
if ( typeof define === 'function' && define.amd ) {
|
||||
// AMD
|
||||
define( ['jquery', 'datatables.net', 'datatables.net-buttons'], function ( $ ) {
|
||||
return factory( $, window, document );
|
||||
} );
|
||||
}
|
||||
else if ( typeof exports === 'object' ) {
|
||||
// CommonJS
|
||||
module.exports = function (root, $) {
|
||||
if ( ! root ) {
|
||||
root = window;
|
||||
}
|
||||
|
||||
if ( ! $ || ! $.fn.dataTable ) {
|
||||
$ = require('datatables.net')(root, $).$;
|
||||
}
|
||||
|
||||
if ( ! $.fn.dataTable.Buttons ) {
|
||||
require('datatables.net-buttons')(root, $);
|
||||
}
|
||||
|
||||
return factory( $, root, root.document );
|
||||
};
|
||||
}
|
||||
else {
|
||||
// Browser
|
||||
factory( jQuery, window, document );
|
||||
}
|
||||
}(function( $, window, document, undefined ) {
|
||||
'use strict';
|
||||
var DataTable = $.fn.dataTable;
|
||||
|
||||
|
||||
$.extend( DataTable.ext.buttons, {
|
||||
// A collection of column visibility buttons
|
||||
colvis: function ( dt, conf ) {
|
||||
return {
|
||||
extend: 'collection',
|
||||
text: function ( dt ) {
|
||||
return dt.i18n( 'buttons.colvis', 'Column visibility' );
|
||||
},
|
||||
className: 'buttons-colvis',
|
||||
buttons: [ {
|
||||
extend: 'columnsToggle',
|
||||
columns: conf.columns,
|
||||
columnText: conf.columnText
|
||||
} ]
|
||||
};
|
||||
},
|
||||
|
||||
// Selected columns with individual buttons - toggle column visibility
|
||||
columnsToggle: function ( dt, conf ) {
|
||||
var columns = dt.columns( conf.columns ).indexes().map( function ( idx ) {
|
||||
return {
|
||||
extend: 'columnToggle',
|
||||
columns: idx,
|
||||
columnText: conf.columnText
|
||||
};
|
||||
} ).toArray();
|
||||
|
||||
return columns;
|
||||
},
|
||||
|
||||
// Single button to toggle column visibility
|
||||
columnToggle: function ( dt, conf ) {
|
||||
return {
|
||||
extend: 'columnVisibility',
|
||||
columns: conf.columns,
|
||||
columnText: conf.columnText
|
||||
};
|
||||
},
|
||||
|
||||
// Selected columns with individual buttons - set column visibility
|
||||
columnsVisibility: function ( dt, conf ) {
|
||||
var columns = dt.columns( conf.columns ).indexes().map( function ( idx ) {
|
||||
return {
|
||||
extend: 'columnVisibility',
|
||||
columns: idx,
|
||||
visibility: conf.visibility,
|
||||
columnText: conf.columnText
|
||||
};
|
||||
} ).toArray();
|
||||
|
||||
return columns;
|
||||
},
|
||||
|
||||
// Single button to set column visibility
|
||||
columnVisibility: {
|
||||
columns: undefined, // column selector
|
||||
text: function ( dt, button, conf ) {
|
||||
return conf._columnText( dt, conf );
|
||||
},
|
||||
className: 'buttons-columnVisibility',
|
||||
action: function ( e, dt, button, conf ) {
|
||||
var col = dt.columns( conf.columns );
|
||||
var curr = col.visible();
|
||||
|
||||
col.visible( conf.visibility !== undefined ?
|
||||
conf.visibility :
|
||||
! (curr.length ? curr[0] : false )
|
||||
);
|
||||
},
|
||||
init: function ( dt, button, conf ) {
|
||||
var that = this;
|
||||
button.attr( 'data-cv-idx', conf.columns );
|
||||
|
||||
dt
|
||||
.on( 'column-visibility.dt'+conf.namespace, function (e, settings) {
|
||||
if ( ! settings.bDestroying && settings.nTable == dt.settings()[0].nTable ) {
|
||||
that.active( dt.column( conf.columns ).visible() );
|
||||
}
|
||||
} )
|
||||
.on( 'column-reorder.dt'+conf.namespace, function (e, settings, details) {
|
||||
if ( dt.columns( conf.columns ).count() !== 1 ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// This button controls the same column index but the text for the column has
|
||||
// changed
|
||||
button.text( conf._columnText( dt, conf ) );
|
||||
|
||||
// Since its a different column, we need to check its visibility
|
||||
that.active( dt.column( conf.columns ).visible() );
|
||||
} );
|
||||
|
||||
this.active( dt.column( conf.columns ).visible() );
|
||||
},
|
||||
destroy: function ( dt, button, conf ) {
|
||||
dt
|
||||
.off( 'column-visibility.dt'+conf.namespace )
|
||||
.off( 'column-reorder.dt'+conf.namespace );
|
||||
},
|
||||
|
||||
_columnText: function ( dt, conf ) {
|
||||
// Use DataTables' internal data structure until this is presented
|
||||
// is a public API. The other option is to use
|
||||
// `$( column(col).node() ).text()` but the node might not have been
|
||||
// populated when Buttons is constructed.
|
||||
var idx = dt.column( conf.columns ).index();
|
||||
var title = dt.settings()[0].aoColumns[ idx ].sTitle
|
||||
.replace(/\n/g," ") // remove new lines
|
||||
.replace(/<br\s*\/?>/gi, " ") // replace line breaks with spaces
|
||||
.replace(/<select(.*?)<\/select>/g, "") // remove select tags, including options text
|
||||
.replace(/<!\-\-.*?\-\->/g, "") // strip HTML comments
|
||||
.replace(/<.*?>/g, "") // strip HTML
|
||||
.replace(/^\s+|\s+$/g,""); // trim
|
||||
|
||||
return conf.columnText ?
|
||||
conf.columnText( dt, idx, title ) :
|
||||
title;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
colvisRestore: {
|
||||
className: 'buttons-colvisRestore',
|
||||
|
||||
text: function ( dt ) {
|
||||
return dt.i18n( 'buttons.colvisRestore', 'Restore visibility' );
|
||||
},
|
||||
|
||||
init: function ( dt, button, conf ) {
|
||||
conf._visOriginal = dt.columns().indexes().map( function ( idx ) {
|
||||
return dt.column( idx ).visible();
|
||||
} ).toArray();
|
||||
},
|
||||
|
||||
action: function ( e, dt, button, conf ) {
|
||||
dt.columns().every( function ( i ) {
|
||||
// Take into account that ColReorder might have disrupted our
|
||||
// indexes
|
||||
var idx = dt.colReorder && dt.colReorder.transpose ?
|
||||
dt.colReorder.transpose( i, 'toOriginal' ) :
|
||||
i;
|
||||
|
||||
this.visible( conf._visOriginal[ idx ] );
|
||||
} );
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
colvisGroup: {
|
||||
className: 'buttons-colvisGroup',
|
||||
|
||||
action: function ( e, dt, button, conf ) {
|
||||
dt.columns( conf.show ).visible( true, false );
|
||||
dt.columns( conf.hide ).visible( false, false );
|
||||
|
||||
dt.columns.adjust();
|
||||
},
|
||||
|
||||
show: [],
|
||||
|
||||
hide: []
|
||||
}
|
||||
} );
|
||||
|
||||
|
||||
return DataTable.Buttons;
|
||||
}));
|
2015
helpdesk/static/helpdesk/vendor/datatables/js/dataTables.buttons.js
vendored
Normal file
2015
helpdesk/static/helpdesk/vendor/datatables/js/dataTables.buttons.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
@ -26,6 +26,7 @@
|
||||
|
||||
<!-- DataTables CSS-->
|
||||
<link href="{% static 'helpdesk/vendor/datatables/css/dataTables.bootstrap4.css' %}" rel="stylesheet">
|
||||
<link href="{% static 'helpdesk/vendor/datatables/css/buttons.dataTables.css' %}" rel="stylesheet">
|
||||
|
||||
<!-- MetisMenu CSS -->
|
||||
<link href="{% static 'helpdesk/vendor/metisMenu/metisMenu.min.css' %}" rel="stylesheet">
|
||||
|
@ -15,6 +15,8 @@
|
||||
<script src="{% static 'helpdesk/vendor/chart.js/Chart.min.js' %}"></script>
|
||||
<script src="{% static 'helpdesk/vendor/datatables/js/jquery.dataTables.js' %}"></script>
|
||||
<script src="{% static 'helpdesk/vendor/datatables/js/dataTables.bootstrap4.js' %}"></script>
|
||||
<script src="{% static 'helpdesk/vendor/datatables/js/dataTables.buttons.js' %}"></script>
|
||||
<script src="{% static 'helpdesk/vendor/datatables/js/buttons.colVis.js' %}"></script>
|
||||
|
||||
<!-- jQuery UI DatePicker -->
|
||||
<script src='{% static "helpdesk/vendor/jquery-ui/jquery-ui.min.js" %}' type='text/javascript' language='javascript'></script>
|
||||
@ -25,3 +27,6 @@
|
||||
|
||||
<!-- Custom Theme JavaScript -->
|
||||
<script src="{% static 'helpdesk/js/sb-admin.js' %}"></script>
|
||||
|
||||
{% block js_bottom %}
|
||||
{% endblock %}
|
||||
|
@ -6,7 +6,16 @@
|
||||
<label for='id_statuses'>{% trans "Knowledge base item(s)" %}:</label>
|
||||
</div>
|
||||
<div class="col col-sm-3">
|
||||
<select id='id_kbitems' name='kbitem' multiple='selected' size='5'>{% for s in kbitem_choices %}<option value='{{ s.0 }}'{% if s.0|in_list:query_params.filtering.kbitem__in %} selected='selected'{% endif %}>{{ s.1 }}</option>{% endfor %}</select>
|
||||
<select id='id_kbitems' name='kbitem' multiple='selected' size='5'>
|
||||
{% with magic_number=-1 %}
|
||||
<option value='{{magic_number}}'{% if magic_number|in_list:query_params.filtering.kbitem__in %} selected='selected'{% endif %}>
|
||||
{% trans "Uncategorized" %}
|
||||
</option>
|
||||
{% endwith %}
|
||||
{% for s in kbitem_choices %}
|
||||
<option value='{{ s.0 }}'{% if s.0|in_list:query_params.filtering.kbitem__in %} selected='selected'{% endif %}>{{ s.1 }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col col-sm-6">
|
||||
<button class="filterBuilderRemove btn btn-danger btn-sm float-right"><i class="fas fa-trash-alt"></i></button>
|
||||
|
@ -7,6 +7,11 @@
|
||||
</div>
|
||||
<div class="col col-sm-3">
|
||||
<select id='id_owners' name='assigned_to' multiple='selected' size='5'>
|
||||
{% with magic_number=-1 %}
|
||||
<option value='{{magic_number}}'{% if magic_number|in_list:query_params.filtering.assigned_to__id__in %} selected='selected'{% endif %}>
|
||||
{% trans "Unassigned" %}
|
||||
</option>
|
||||
{% endwith %}
|
||||
{% for u in user_choices %}
|
||||
<option value='{{ u.id }}'{% if u.id|in_list:query_params.filtering.assigned_to__id__in %} selected='selected'{% endif %}>
|
||||
{{ u.get_username }}{% ifequal u user %} {% trans "(ME)" %}{% endifequal %}
|
||||
|
@ -1,6 +1,5 @@
|
||||
{% load i18n humanize %}
|
||||
|
||||
<!-- DataTables Example -->
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<i class="fas fa-table"></i>
|
||||
@ -40,3 +39,43 @@
|
||||
<div class="card-footer small text-muted">Listing {{ unassigned_tickets|length }} ticket(s).</div>
|
||||
</div>
|
||||
|
||||
{% for kbitem in kbitems %}
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<i class="fas fa-table"></i>
|
||||
{% trans "KBItem:" %} {{kbitem.title}} {% trans "Team:" %} {{kbitem.team.name}} {% trans "(pick up a ticket if you start to work on it)" %}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-sm table-striped" id="dataTable" width="100%" cellspacing="0">
|
||||
<thead class="thead-light">
|
||||
<tr>
|
||||
<th>{% trans "Ticket" %}</th>
|
||||
<th>{% trans "Prority" %}</th>
|
||||
<th>{% trans "Queue" %}</th>
|
||||
<th>{% trans "Created" %}</th>
|
||||
<th>{% trans "Actions" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for ticket in kbitem.unassigned_tickets %}
|
||||
<tr class="{{ ticket.get_priority_css_class }}">
|
||||
<td class="tickettitle"><a href='{{ ticket.get_absolute_url }}'>{{ ticket.id }}. {{ ticket.title }} </a></td>
|
||||
<td>{{ ticket.priority }}</td>
|
||||
<td>{{ ticket.queue }}</td>
|
||||
<td><span title='{{ ticket.created|date:"r" }}'>{{ ticket.created|naturaltime }}</span></td>
|
||||
<td class="text-center">
|
||||
<a href='{{ ticket.get_absolute_url }}?take'><button class='btn btn-primary btn-sm'><i class="fas fa-hand-paper"></i> {% trans "Take" %}</button></a>
|
||||
<a href='{% url 'helpdesk:delete' ticket.id %}'><button class='btn btn-danger btn-sm'><i class="fas fa-trash"></i> {% trans "Delete" %}</button></a>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr><td colspan='6'>{% trans "There are no unassigned tickets." %}</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer small text-muted">Listing {{ kbitem.unassigned_tickets|length }} ticket(s).</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
@ -1,48 +1,63 @@
|
||||
{% load i18n %}
|
||||
<h2>{% trans 'Knowledgebase Category' %}:{% blocktrans with category.title as kbcat %}{{ kbcat }}{% endblocktrans %}</h2>
|
||||
<p>{{ category.description }}</p>
|
||||
{% block header %}
|
||||
<h2>{% blocktrans with category.title as kbcat %}{{ kbcat }}{% endblocktrans %}</h2>
|
||||
{{ category.description|linebreaks }}
|
||||
{% endblock %}
|
||||
|
||||
{% block item_list %}
|
||||
<div id="accordion">
|
||||
{% for item in items %}
|
||||
<div class="card mb-3">
|
||||
<button class="btn btn-link" data-toggle="collapse" data-target="#collapse{{item.id}}" aria-expanded="true" aria-controls="collapse{{item.id}}">
|
||||
<div class="btn btn-link" data-toggle="collapse" data-target="#collapse{{item.id}}" role="region" aria-expanded="true" aria-controls="collapse{{item.id}}">
|
||||
<div class="card-header" id="header{{item.id}}">
|
||||
<h5 class="mb-0">
|
||||
{{ item.title }}
|
||||
</h5>
|
||||
</div>
|
||||
</button>
|
||||
<div id="collapse{{item.id}}" class="collapse {% if item.id == selected_item %}show{% endif %}" aria-labelledby="header{{item.id}}" data-parent="#accordion">
|
||||
</div>
|
||||
<div id="collapse{{item.id}}" class="collapse {% if item.id == selected_item %}show{% endif %}" role="region" aria-labelledby="header{{item.id}}" data-parent="#accordion">
|
||||
{% block card_body %}
|
||||
<div class="card-body">
|
||||
<p class="card-text">{{ item.question }}</p>
|
||||
<p>{{ item.get_markdown }}</p>
|
||||
<div class="card-answer">
|
||||
{{ item.get_markdown }}
|
||||
</div>
|
||||
<div class="row">
|
||||
{% if request.user.pk %}
|
||||
<div class="col-sm">
|
||||
<a href='{% url "helpdesk:kb_vote" item.pk %}?vote=up'><button type="button" class="btn btn-success btn-circle btn-xl"><i class="fa fa-thumbs-up fa-lg"></i></button></a>
|
||||
<a href='{% url "helpdesk:kb_vote" item.pk %}?vote=down'><button type="button" class="btn btn-danger btn-circle btn-xl"><i class="fa fa-thumbs-down fa-lg"></i></button></a>
|
||||
<a href='{% url "helpdesk:kb_vote" item.pk %}?vote=up'><div class="btn btn-success btn-circle btn-xl"><i class="fa fa-thumbs-up fa-lg"></i></div></a>
|
||||
<a href='{% url "helpdesk:kb_vote" item.pk %}?vote=down'><div class="btn btn-danger btn-circle btn-xl"><i class="fa fa-thumbs-down fa-lg"></i></div></a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if staff %}
|
||||
<a href='{% url 'helpdesk:list' %}?kbitem={{item.id}}' class="col-sm">
|
||||
<button type="button" class="btn btn-success btn-circle btn-xl float-right"><i class="fa fa-search fa-lg"></i> {{item.num_open_tickets}} {% trans 'open tickets' %}</button>
|
||||
<div class="btn btn-success btn-circle btn-xl float-right"><i class="fa fa-search fa-lg"></i> {{item.num_open_tickets}} {% trans 'open tickets' %}</div>
|
||||
</a>
|
||||
|
||||
{% endif %}
|
||||
<a href='{% if iframe %}{% url 'helpdesk:submit_iframe' %}{% else %}{% url 'helpdesk:submit' %}{%endif%}?{% if category.queue %}queue={{category.queue.pk}};_readonly_fields_=queue;{%endif%}kbitem={{item.id}};{{query_param_string}}' class="col-sm">
|
||||
<button type="button" class="btn btn-success btn-circle btn-xl float-right"><i class="fa fa-envelope fa-lg"></i> {% trans 'Contact a human' %}</button>
|
||||
<div class="btn btn-success btn-circle btn-xl float-right"><i class="fa fa-envelope fa-lg"></i> {% trans 'Contact a human' %}</div>
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
{% if item.votes != 0 %}
|
||||
{% blocktrans with recommendations=item.recommendations votes=item.votes %}{{ recommendations }} people found this answer useful of {{votes}}{% endblocktrans %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block footer %}
|
||||
{% endblock %}
|
||||
{% if category.queue %}
|
||||
<a href='{% if iframe %}{% url 'helpdesk:submit_iframe' %}{% else %}{% url 'helpdesk:submit' %}{%endif%}?queue={{category.queue.pk}};_readonly_fields_=queue;{{query_param_string}}'>
|
||||
<button type="button" class="btn btn-danger btn-circle btn-xl float-right"><i class="fa fa-envelope fa-lg"></i> {% trans 'Contact a human' %}</button>
|
||||
{% block submit_button %}
|
||||
<div class="btn btn-danger btn-circle btn-xl float-right"><i class="fa fa-envelope fa-lg"></i> {% trans 'Contact a human' %}</div>
|
||||
{% endblock %}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
@ -1,5 +1,6 @@
|
||||
{% load i18n %}
|
||||
{% load saved_queries %}
|
||||
<!DOCTYPE html>
|
||||
<head>
|
||||
{% include 'helpdesk/base-head.html' %}
|
||||
{% block helpdesk_head %}{% endblock %}
|
||||
|
@ -3,7 +3,9 @@
|
||||
{% with request|load_helpdesk_settings as helpdesk_settings %}
|
||||
|
||||
{% if helpdesk_settings.HELPDESK_SUBMIT_A_TICKET_PUBLIC %}
|
||||
{% block form_header %}
|
||||
<p>{% trans "Unless otherwise stated, all fields are required." %} {% trans "Please provide as descriptive a title and description as possible." %}</p>
|
||||
{% endblock %}
|
||||
<form method='post' enctype='multipart/form-data'>
|
||||
{{ form|bootstrap4form }}
|
||||
<button type="submit" class="btn btn-primary btn-lg btn-block"><i class="fa fa-send"></i> {% trans "Submit Ticket" %}</button>
|
||||
|
@ -1,5 +1,6 @@
|
||||
{% load i18n %}
|
||||
{% load saved_queries %}
|
||||
<!DOCTYPE html>
|
||||
<head>
|
||||
{% include 'helpdesk/base-head.html' %}
|
||||
</head>
|
||||
|
@ -71,7 +71,7 @@
|
||||
{% if ticket.kbitem %}
|
||||
<tr>
|
||||
<th class="table-active">{% trans "Knowlegebase item" %}</th>
|
||||
<td> <a href ="{{ticket.kbitem.query_url}}"> {{ticket.kbitem.title}} </a> </td>
|
||||
<td> <a href ="{{ticket.kbitem.query_url}}"> {{ticket.kbitem}} </a> </td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
|
@ -68,6 +68,7 @@
|
||||
<th>{% trans "Owner" %}</th>
|
||||
<th>{% trans "Submitter" %}</th>
|
||||
<th>{% trans "Time Spent" %}</th>
|
||||
<th>{% trans "KB item" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
@ -96,6 +97,10 @@
|
||||
<option value='unassign'>{% trans "Nobody (Unassign)" %}</option>
|
||||
{% for u in user_choices %}<option value='assign_{{ u.id }}'>{{ u.get_username }}</option>{% endfor %}
|
||||
</optgroup>
|
||||
<optgroup label='{% trans "Set KB Item" %}'>
|
||||
<option value='kbitem_none'>{% trans "No KB Item" %}</option>
|
||||
{% for kbi in kb_items %}<option value='kbitem_{{ kbi.id }}'>{{kbi.category.title}}: {{ kbi.title }}</option>{% endfor %}
|
||||
</optgroup>
|
||||
</select>
|
||||
<button type="submit" class="btn btn-primary btn-sm"><i class="fas fa-arrow-circle-right"></i> {% trans "Go" %}</button>
|
||||
</p>
|
||||
@ -303,6 +308,8 @@
|
||||
{
|
||||
$( row ).addClass(data.row_class);
|
||||
},
|
||||
dom: 'ltBp',
|
||||
buttons: ["colvis"],
|
||||
|
||||
"columns": [
|
||||
{"data": "id",
|
||||
@ -339,16 +346,18 @@
|
||||
priority = "danger";
|
||||
}
|
||||
return '<p class="text-'+priority+'">'+data+'</p>';
|
||||
}
|
||||
},
|
||||
"visible": false,
|
||||
},
|
||||
{"data": "queue",
|
||||
"render": function(data, type, row, meta) {
|
||||
return data.title;
|
||||
}
|
||||
},
|
||||
"visible": false,
|
||||
},
|
||||
{"data": "status"},
|
||||
{"data": "created"},
|
||||
{"data": "due_date"},
|
||||
{"data": "due_date", "visible": false},
|
||||
{"data": "assigned_to",
|
||||
"render": function(data, type, row, meta) {
|
||||
if (data != "None") {
|
||||
@ -360,7 +369,8 @@
|
||||
}
|
||||
},
|
||||
{"data": "submitter"},
|
||||
{"data": "time_spent"},
|
||||
{"data": "time_spent", "visible": false},
|
||||
{"data": "kbitem"},
|
||||
]
|
||||
});
|
||||
})
|
||||
|
106
helpdesk/tests/test_query.py
Normal file
106
helpdesk/tests/test_query.py
Normal file
@ -0,0 +1,106 @@
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from helpdesk.models import KBCategory, KBItem, Queue, Ticket
|
||||
from helpdesk.query import query_to_base64
|
||||
|
||||
from helpdesk.tests.helpers import (get_staff_user, reload_urlconf, User, create_ticket, print_response)
|
||||
|
||||
|
||||
class QueryTests(TestCase):
|
||||
def setUp(self):
|
||||
self.queue = Queue.objects.create(
|
||||
title="Test queue",
|
||||
slug="test_queue",
|
||||
allow_public_submission=True,
|
||||
)
|
||||
self.queue.save()
|
||||
cat = KBCategory.objects.create(
|
||||
title="Test Cat",
|
||||
slug="test_cat",
|
||||
description="This is a test category",
|
||||
queue=self.queue,
|
||||
)
|
||||
cat.save()
|
||||
self.kbitem1 = KBItem.objects.create(
|
||||
category=cat,
|
||||
title="KBItem 1",
|
||||
question="What?",
|
||||
answer="A KB Item",
|
||||
)
|
||||
self.user = get_staff_user()
|
||||
self.ticket1 = Ticket.objects.create(
|
||||
title="unassigned to kbitem",
|
||||
queue=self.queue,
|
||||
description="lol",
|
||||
)
|
||||
self.ticket1.save()
|
||||
self.ticket2 = Ticket.objects.create(
|
||||
title="assigned to kbitem",
|
||||
queue=self.queue,
|
||||
description="lol",
|
||||
kbitem=self.kbitem1,
|
||||
)
|
||||
self.ticket2.save()
|
||||
|
||||
def loginUser(self, is_staff=True):
|
||||
"""Create a staff user and login"""
|
||||
User = get_user_model()
|
||||
self.user = User.objects.create(
|
||||
username='User_1',
|
||||
is_staff=is_staff,
|
||||
)
|
||||
self.user.set_password('pass')
|
||||
self.user.save()
|
||||
self.client.login(username='User_1', password='pass')
|
||||
|
||||
def test_query_basic(self):
|
||||
self.loginUser()
|
||||
query = query_to_base64({})
|
||||
response = self.client.get(reverse('helpdesk:datatables_ticket_list', args=[query]))
|
||||
self.assertEqual(
|
||||
response.json(),
|
||||
{
|
||||
"data":
|
||||
[{"ticket": "1 [test_queue-1]", "id": 1, "priority": 3, "title": "unassigned to kbitem", "queue": {"title": "Test queue", "id": 1}, "status": "Open", "created": "now", "due_date": None, "assigned_to": "None", "submitter": None, "row_class": "", "time_spent": "", "kbitem": ""},
|
||||
{"ticket": "2 [test_queue-2]", "id": 2, "priority": 3, "title": "assigned to kbitem", "queue": {"title": "Test queue", "id": 1}, "status": "Open", "created": "now", "due_date": None, "assigned_to": "None", "submitter": None, "row_class": "", "time_spent": "", "kbitem": "KBItem 1"}],
|
||||
"recordsFiltered": 2,
|
||||
"recordsTotal": 2,
|
||||
"draw": 0,
|
||||
},
|
||||
)
|
||||
|
||||
def test_query_by_kbitem(self):
|
||||
self.loginUser()
|
||||
query = query_to_base64(
|
||||
{'filtering': {'kbitem__in': [self.kbitem1.pk]}}
|
||||
)
|
||||
response = self.client.get(reverse('helpdesk:datatables_ticket_list', args=[query]))
|
||||
self.assertEqual(
|
||||
response.json(),
|
||||
{
|
||||
"data":
|
||||
[{"ticket": "2 [test_queue-2]", "id": 2, "priority": 3, "title": "assigned to kbitem", "queue": {"title": "Test queue", "id": 1}, "status": "Open", "created": "now", "due_date": None, "assigned_to": "None", "submitter": None, "row_class": "", "time_spent": "", "kbitem": "KBItem 1"}],
|
||||
"recordsFiltered": 1,
|
||||
"recordsTotal": 1,
|
||||
"draw": 0,
|
||||
},
|
||||
)
|
||||
|
||||
def test_query_by_no_kbitem(self):
|
||||
self.loginUser()
|
||||
query = query_to_base64(
|
||||
{'filtering_or': {'kbitem__in': [self.kbitem1.pk]}}
|
||||
)
|
||||
response = self.client.get(reverse('helpdesk:datatables_ticket_list', args=[query]))
|
||||
self.assertEqual(
|
||||
response.json(),
|
||||
{
|
||||
"data":
|
||||
[{"ticket": "2 [test_queue-2]", "id": 2, "priority": 3, "title": "assigned to kbitem", "queue": {"title": "Test queue", "id": 1}, "status": "Open", "created": "now", "due_date": None, "assigned_to": "None", "submitter": None, "row_class": "", "time_spent": "", "kbitem": "KBItem 1"}],
|
||||
"recordsFiltered": 1,
|
||||
"recordsTotal": 1,
|
||||
"draw": 0,
|
||||
},
|
||||
)
|
@ -1,11 +1,17 @@
|
||||
from helpdesk.models import (
|
||||
Ticket,
|
||||
Queue
|
||||
Queue,
|
||||
KBCategory,
|
||||
KBItem,
|
||||
)
|
||||
|
||||
from helpdesk import settings as helpdesk_settings
|
||||
|
||||
|
||||
def huser_from_request(req):
|
||||
return HelpdeskUser(req.user)
|
||||
|
||||
|
||||
class HelpdeskUser:
|
||||
def __init__(self, user):
|
||||
self.user = user
|
||||
@ -30,9 +36,26 @@ class HelpdeskUser:
|
||||
else:
|
||||
return all_queues
|
||||
|
||||
def get_allowed_kb_categories(self):
|
||||
categories = []
|
||||
for cat in KBCategory.objects.all():
|
||||
if self.can_access_kbcategory(cat):
|
||||
categories.append(cat)
|
||||
return categories
|
||||
|
||||
def get_assigned_kb_items(self):
|
||||
kbitems = []
|
||||
for item in KBItem.objects.all():
|
||||
if item.team and item.team.is_member(self.user):
|
||||
kbitems.append(item)
|
||||
return kbitems
|
||||
|
||||
def get_tickets_in_queues(self):
|
||||
return Ticket.objects.filter(queue__in=self.get_queues())
|
||||
|
||||
def has_full_access(self):
|
||||
return self.user.is_superuser or self.user.is_staff
|
||||
|
||||
def can_access_queue(self, queue):
|
||||
"""Check if a certain user can access a certain queue.
|
||||
|
||||
@ -40,11 +63,10 @@ class HelpdeskUser:
|
||||
:param queue: The django-helpdesk Queue instance
|
||||
:return: True if the user has permission (either by default or explicitly), false otherwise
|
||||
"""
|
||||
user = self.user
|
||||
if user.is_superuser or not helpdesk_settings.HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION:
|
||||
if self.has_full_access():
|
||||
return True
|
||||
else:
|
||||
return user.has_perm(queue.permission_name)
|
||||
return helpdesk_settings.HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION and self.user.has_perm(queue.permission_name)
|
||||
|
||||
def can_access_ticket(self, ticket):
|
||||
"""Check to see if the user has permission to access
|
||||
@ -52,8 +74,13 @@ class HelpdeskUser:
|
||||
user = self.user
|
||||
if self.can_access_queue(ticket.queue):
|
||||
return True
|
||||
elif user.is_superuser or user.is_staff or \
|
||||
elif self.has_full_access() or \
|
||||
(ticket.assigned_to and user.id == ticket.assigned_to.id):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def can_access_kbcategory(self, category):
|
||||
if category.public:
|
||||
return True
|
||||
return self.has_full_access() or (category.queue and self.can_access_queue(category.queue))
|
||||
|
@ -8,26 +8,29 @@ views/kb.py - Public-facing knowledgebase views. The knowledgebase is a
|
||||
resolutions to common problems.
|
||||
"""
|
||||
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.http import HttpResponseRedirect, Http404
|
||||
from django.shortcuts import render, get_object_or_404
|
||||
from django.views.decorators.clickjacking import xframe_options_exempt
|
||||
|
||||
from helpdesk import settings as helpdesk_settings
|
||||
from helpdesk import user
|
||||
from helpdesk.models import KBCategory, KBItem
|
||||
|
||||
|
||||
def index(request):
|
||||
category_list = KBCategory.objects.all()
|
||||
huser = user.huser_from_request(request)
|
||||
# TODO: It'd be great to have a list of most popular items here.
|
||||
return render(request, 'helpdesk/kb_index.html', {
|
||||
'kb_categories': category_list,
|
||||
'kb_categories': huser.get_allowed_kb_categories(),
|
||||
'helpdesk_settings': helpdesk_settings,
|
||||
})
|
||||
|
||||
|
||||
def category(request, slug, iframe=False):
|
||||
category = get_object_or_404(KBCategory, slug__iexact=slug)
|
||||
items = category.kbitem_set.all()
|
||||
if not user.huser_from_request(request).can_access_kbcategory(category):
|
||||
raise Http404
|
||||
items = category.kbitem_set.filter(enabled=True)
|
||||
selected_item = request.GET.get('kbitem', None)
|
||||
try:
|
||||
selected_item = int(selected_item)
|
||||
|
@ -29,6 +29,7 @@ import helpdesk.views.abstract_views as abstract_views
|
||||
from helpdesk.forms import PublicTicketForm
|
||||
from helpdesk.lib import text_is_spam
|
||||
from helpdesk.models import CustomField, Ticket, Queue, UserSettings, KBCategory, KBItem
|
||||
from helpdesk.user import huser_from_request
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -142,7 +143,7 @@ class Homepage(CreateTicketView):
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['kb_categories'] = KBCategory.objects.all()
|
||||
context['kb_categories'] = huser_from_request(self.request).get_allowed_kb_categories()
|
||||
return context
|
||||
|
||||
|
||||
|
@ -103,6 +103,7 @@ def dashboard(request):
|
||||
showing ticket counts by queue/status, and a list of unassigned tickets
|
||||
with options for them to 'Take' ownership of said tickets.
|
||||
"""
|
||||
huser = HelpdeskUser(request.user)
|
||||
active_tickets = Ticket.objects.select_related('queue').exclude(
|
||||
status__in=[Ticket.CLOSED_STATUS, Ticket.RESOLVED_STATUS],
|
||||
)
|
||||
@ -117,13 +118,16 @@ def dashboard(request):
|
||||
assigned_to=request.user,
|
||||
status__in=[Ticket.CLOSED_STATUS, Ticket.RESOLVED_STATUS])
|
||||
|
||||
user_queues = HelpdeskUser(request.user).get_queues()
|
||||
user_queues = huser.get_queues()
|
||||
|
||||
unassigned_tickets = active_tickets.filter(
|
||||
assigned_to__isnull=True,
|
||||
kbitem__isnull=True,
|
||||
queue__in=user_queues
|
||||
)
|
||||
|
||||
kbitems = huser.get_assigned_kb_items()
|
||||
|
||||
# all tickets, reported by current user
|
||||
all_tickets_reported_by_current_user = ''
|
||||
email_current_user = request.user.email
|
||||
@ -157,6 +161,7 @@ def dashboard(request):
|
||||
'user_tickets': tickets,
|
||||
'user_tickets_closed_resolved': tickets_closed_resolved,
|
||||
'unassigned_tickets': unassigned_tickets,
|
||||
'kbitems': kbitems,
|
||||
'all_tickets_reported_by_current_user': all_tickets_reported_by_current_user,
|
||||
'basic_ticket_stats': basic_ticket_stats,
|
||||
})
|
||||
@ -706,6 +711,13 @@ def mass_update(request):
|
||||
parts = action.split('_')
|
||||
user = User.objects.get(id=parts[1])
|
||||
action = 'assign'
|
||||
if action == 'kbitem_none':
|
||||
kbitem = None
|
||||
action = 'set_kbitem'
|
||||
if action.startswith('kbitem_'):
|
||||
parts = action.split('_')
|
||||
kbitem = KBItem.objects.get(id=parts[1])
|
||||
action = 'set_kbitem'
|
||||
elif action == 'take':
|
||||
user = request.user
|
||||
action = 'assign'
|
||||
@ -735,6 +747,15 @@ def mass_update(request):
|
||||
public=True,
|
||||
user=request.user)
|
||||
f.save()
|
||||
elif action == 'set_kbitem':
|
||||
t.kbitem = kbitem
|
||||
t.save()
|
||||
f = FollowUp(ticket=t,
|
||||
date=timezone.now(),
|
||||
title=_('KBItem set in bulk update'),
|
||||
public=False,
|
||||
user=request.user)
|
||||
f.save()
|
||||
elif action == 'close' and t.status != Ticket.CLOSED_STATUS:
|
||||
t.status = Ticket.CLOSED_STATUS
|
||||
t.save()
|
||||
@ -798,13 +819,14 @@ def ticket_list(request):
|
||||
# a query, to be saved if needed:
|
||||
query_params = {
|
||||
'filtering': {},
|
||||
'filtering_or': {},
|
||||
'sorting': None,
|
||||
'sortreverse': False,
|
||||
'search_string': '',
|
||||
}
|
||||
default_query_params = {
|
||||
'filtering': {
|
||||
'status__in': [1, 2, 3],
|
||||
'status__in': [1, 2],
|
||||
},
|
||||
'sorting': 'created',
|
||||
'search_string': '',
|
||||
@ -863,12 +885,21 @@ def ticket_list(request):
|
||||
('status', 'status__in'),
|
||||
('kbitem', 'kbitem__in'),
|
||||
]
|
||||
|
||||
filter_null_params = dict([
|
||||
('queue', 'queue__id__isnull'),
|
||||
('assigned_to', 'assigned_to__id__isnull'),
|
||||
('status', 'status__isnull'),
|
||||
('kbitem', 'kbitem__isnull'),
|
||||
])
|
||||
for param, filter_command in filter_in_params:
|
||||
patterns = request.GET.getlist(param)
|
||||
if patterns:
|
||||
if not request.GET.get(param) is None:
|
||||
patterns = request.GET.getlist(param)
|
||||
try:
|
||||
pattern_pks = [int(pattern) for pattern in patterns]
|
||||
if -1 in pattern_pks:
|
||||
query_params['filtering_or'][filter_null_params[param]] = True
|
||||
else:
|
||||
query_params['filtering_or'][filter_command] = pattern_pks
|
||||
query_params['filtering'][filter_command] = pattern_pks
|
||||
except ValueError:
|
||||
pass
|
||||
@ -911,12 +942,13 @@ def ticket_list(request):
|
||||
'<a href="http://docs.djangoproject.com/en/dev/ref/databases/#sqlite-string-matching">'
|
||||
'Django Documentation on string matching in SQLite</a>.')
|
||||
|
||||
kbitem_choices = [(item.pk, item.title) for item in KBItem.objects.all()]
|
||||
kbitem_choices = [(item.pk, str(item)) for item in KBItem.objects.all()]
|
||||
|
||||
return render(request, 'helpdesk/ticket_list.html', dict(
|
||||
context,
|
||||
default_tickets_per_page=request.user.usersettings_helpdesk.tickets_per_page,
|
||||
user_choices=User.objects.filter(is_active=True, is_staff=True),
|
||||
kb_items=KBItem.objects.all(),
|
||||
queue_choices=huser.get_queues(),
|
||||
status_choices=Ticket.STATUS_CHOICES,
|
||||
kbitem_choices=kbitem_choices,
|
||||
|
@ -28,7 +28,11 @@ class QuickDjangoTest(object):
|
||||
'django.contrib.sites',
|
||||
'django.contrib.staticfiles',
|
||||
'bootstrap4form',
|
||||
'account',
|
||||
'pinax.invitations',
|
||||
'pinax.teams',
|
||||
'helpdesk',
|
||||
'reversion',
|
||||
)
|
||||
MIDDLEWARE = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
|
@ -12,3 +12,4 @@ pytz
|
||||
six
|
||||
djangorestframework
|
||||
django-model-utils
|
||||
pinax-teams @ git+https://github.com/auto-mat/pinax-teams.git@slugify#egg=pinax-teams
|
||||
|
Loading…
Reference in New Issue
Block a user