basic support for time spend of tikets and follow-ups

This commit is contained in:
Jachym Cepicky 2019-02-06 14:24:43 +01:00
parent 8c4e094705
commit 6ceb89a5cb
11 changed files with 89 additions and 5 deletions

View File

@ -14,7 +14,8 @@ class QueueAdmin(admin.ModelAdmin):
@admin.register(Ticket) @admin.register(Ticket)
class TicketAdmin(admin.ModelAdmin): class TicketAdmin(admin.ModelAdmin):
list_display = ('title', 'status', 'assigned_to', 'queue', 'hidden_submitter_email',) list_display = ('title', 'status', 'assigned_to', 'queue',
'hidden_submitter_email', 'time_spent')
date_hierarchy = 'created' date_hierarchy = 'created'
list_filter = ('queue', 'assigned_to', 'status') list_filter = ('queue', 'assigned_to', 'status')
@ -28,6 +29,9 @@ class TicketAdmin(admin.ModelAdmin):
return ticket.submitter_email return ticket.submitter_email
hidden_submitter_email.short_description = _('Submitter E-Mail') hidden_submitter_email.short_description = _('Submitter E-Mail')
def time_spent(self, ticket):
return ticket.time_spent
class TicketChangeInline(admin.StackedInline): class TicketChangeInline(admin.StackedInline):
model = TicketChange model = TicketChange
@ -40,7 +44,8 @@ class AttachmentInline(admin.StackedInline):
@admin.register(FollowUp) @admin.register(FollowUp)
class FollowUpAdmin(admin.ModelAdmin): class FollowUpAdmin(admin.ModelAdmin):
inlines = [TicketChangeInline, AttachmentInline] inlines = [TicketChangeInline, AttachmentInline]
list_display = ('ticket_get_ticket_for_url', 'title', 'date', 'ticket', 'user', 'new_status') list_display = ('ticket_get_ticket_for_url', 'title', 'date', 'ticket',
'user', 'new_status', 'time_spent')
list_filter = ('user', 'date', 'new_status') list_filter = ('user', 'date', 'new_status')
def ticket_get_ticket_for_url(self, obj): def ticket_get_ticket_for_url(self, obj):

View File

@ -0,0 +1,18 @@
# Generated by Django 2.0.5 on 2019-02-06 13:24
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('helpdesk', '0023_add_enable_notifications_on_email_events_to_ticket'),
]
operations = [
migrations.AddField(
model_name='followup',
name='time_spent',
field=models.DurationField(blank=True, help_text='Time spent on this follow up', null=True),
),
]

View File

@ -17,6 +17,7 @@ from django.utils import timezone
from django.utils.translation import ugettext_lazy as _, ugettext from django.utils.translation import ugettext_lazy as _, ugettext
from io import StringIO from io import StringIO
import re import re
import datetime
import uuid import uuid
@ -301,6 +302,17 @@ class Queue(models.Model):
return u'%s <%s>' % (self.title, self.email_address) return u'%s <%s>' % (self.title, self.email_address)
from_address = property(_from_address) from_address = property(_from_address)
@property
def time_spent(self):
"""Return back total time spent on the ticket. This is calculated value
based on total sum from all FollowUps
"""
total = datetime.timedelta(0)
for val in self.ticket_set.all():
if val.time_spent:
total = total + val.time_spent
return total
def prepare_permission_name(self): def prepare_permission_name(self):
"""Prepare internally the codename for the permission and store it in permission_name. """Prepare internally the codename for the permission and store it in permission_name.
:return: The codename that can be used to create a new Permission object. :return: The codename that can be used to create a new Permission object.
@ -497,6 +509,17 @@ class Ticket(models.Model):
default=mk_secret, default=mk_secret,
) )
@property
def time_spent(self):
"""Return back total time spent on the ticket. This is calculated value
based on total sum from all FollowUps
"""
total = datetime.timedelta(0)
for val in self.followup_set.all():
if val.time_spent:
total = total + val.time_spent
return total
def send(self, roles, dont_send_to=None, **kwargs): def send(self, roles, dont_send_to=None, **kwargs):
""" """
Send notifications to everyone interested in this ticket. Send notifications to everyone interested in this ticket.
@ -771,6 +794,11 @@ class FollowUp(models.Model):
objects = FollowUpManager() objects = FollowUpManager()
time_spent = models.DurationField(
help_text=_("Time spent on this follow up"),
blank=True, null=True
)
class Meta: class Meta:
ordering = ('date',) ordering = ('date',)
verbose_name = _('Follow-up') verbose_name = _('Follow-up')

View File

@ -22,7 +22,9 @@ class TicketSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Ticket model = Ticket
# fields = '__all__' # fields = '__all__'
fields = ('ticket', 'id', 'priority', 'title', 'queue', 'status', 'created', 'due_date', 'assigned_to', 'row_class') fields = ('ticket', 'id', 'priority', 'title', 'queue', 'status',
'created', 'due_date', 'assigned_to', 'row_class',
'time_spent')
def get_ticket(self, obj): def get_ticket(self, obj):
return (str(obj.id) + " " + obj.ticket) return (str(obj.id) + " " + obj.ticket)

View File

@ -46,6 +46,8 @@
<dt><label for="id_new_status">New Status:</label></dt> <dt><label for="id_new_status">New Status:</label></dt>
<dd>{{ form.new_status }}</dd> <dd>{{ form.new_status }}</dd>
<p>If the status was changed, what was it changed to?</p> <p>If the status was changed, what was it changed to?</p>
<dt><label for="id_time_spent">Time spent:</label></dt>
<dd>{{ form.time_spent }}</dd>
</dl> </dl>
</fieldset> </fieldset>
<p><input class="btn btn-primary" type="submit" value="Submit"></p>{% csrf_token %} <p><input class="btn btn-primary" type="submit" value="Submit"></p>{% csrf_token %}

View File

@ -45,6 +45,7 @@
<th>{% trans "Open" %}</th> <th>{% trans "Open" %}</th>
<th>{% trans "Resolved" %}</th> <th>{% trans "Resolved" %}</th>
<th>{% trans "Closed" %}</th> <th>{% trans "Closed" %}</th>
<th>{% trans "Time spent" %}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -54,6 +55,7 @@
<td>{% if queue.open %}<a href='{{ hdlist }}?queue={{ queue.queue }}&status=1&status=2'>{% endif %}{{ queue.open }}{% if queue.open %}</a>{% endif %}</td> <td>{% if queue.open %}<a href='{{ hdlist }}?queue={{ queue.queue }}&status=1&status=2'>{% endif %}{{ queue.open }}{% if queue.open %}</a>{% endif %}</td>
<td>{% if queue.resolved %}<a href='{{ hdlist }}?queue={{ queue.queue }}&status=3'>{% endif %}{{ queue.resolved }}{% if queue.resolved %}</a>{% endif %}</td> <td>{% if queue.resolved %}<a href='{{ hdlist }}?queue={{ queue.queue }}&status=3'>{% endif %}{{ queue.resolved }}{% if queue.resolved %}</a>{% endif %}</td>
<td>{% if queue.closed %}<a href='{{ hdlist }}?queue={{ queue.queue }}&status=4'>{% endif %}{{ queue.closed }}{% if queue.closed %}</a>{% endif %}</td> <td>{% if queue.closed %}<a href='{{ hdlist }}?queue={{ queue.queue }}&status=4'>{% endif %}{{ queue.closed }}{% if queue.closed %}</a>{% endif %}</td>
<td>{{ queue.time_spent }}</td>
</tr> </tr>
{% empty %} {% empty %}
<tr><td colspan='6'>{% trans "There are no unassigned tickets." %}</td></tr> <tr><td colspan='6'>{% trans "There are no unassigned tickets." %}</td></tr>

View File

@ -46,6 +46,9 @@
{% if followup.comment %} {% if followup.comment %}
<p>{{ followup.comment|force_escape|urlizetrunc:50|num_to_link|linebreaksbr }}</p> <p>{{ followup.comment|force_escape|urlizetrunc:50|num_to_link|linebreaksbr }}</p>
{% endif %} {% endif %}
{% if followup.time_spent %}
<small>{% trans "Time spent" %}: {{ followup.time_spent }}</small></p>
{% endif %}
{% for change in followup.ticketchange_set.all %} {% for change in followup.ticketchange_set.all %}
{% if forloop.first %}<div class='changes'><ul>{% endif %} {% if forloop.first %}<div class='changes'><ul>{% endif %}
<li>{% blocktrans with change.field as field and change.old_value as old_value and change.new_value as new_value %}Changed {{ field }} from {{ old_value }} to {{ new_value }}.{% endblocktrans %}</li> <li>{% blocktrans with change.field as field and change.old_value as old_value and change.new_value as new_value %}Changed {{ field }} from {{ old_value }} to {{ new_value }}.{% endblocktrans %}</li>
@ -152,6 +155,11 @@
<dd><input type='checkbox' name='public' value='1' checked='checked' />&nbsp; {% trans 'Yes, make this update public.' %}</dd> <dd><input type='checkbox' name='public' value='1' checked='checked' />&nbsp; {% trans 'Yes, make this update public.' %}</dd>
<dd class='form_help_text'>{% trans "If this is public, the submitter will be e-mailed your comment or resolution." %}</dd> <dd class='form_help_text'>{% trans "If this is public, the submitter will be e-mailed your comment or resolution." %}</dd>
{% endif %} {% endif %}
<dt>
<label for='id_time_spent'>{% trans "Time spent" %}</label> <span class='form_optional'>{% trans "(Optional)" %}</span>
</dt>
<dd><input name='time_spent' type="time" /></dd>
</dl> </dl>
<p id='ShowFurtherOptPara'><button class="btn btn-warning btn-sm" id='ShowFurtherEditOptions'>{% trans "Change Further Details &raquo;" %}</button></p> <p id='ShowFurtherOptPara'><button class="btn btn-warning btn-sm" id='ShowFurtherEditOptions'>{% trans "Change Further Details &raquo;" %}</button></p>

View File

@ -89,6 +89,10 @@
<p><a data-toggle='tooltip' href='{% url 'helpdesk:ticket_dependency_add' ticket.id %}' title="{% trans "Click on 'Add Dependency', if you want to make this ticket dependent on another ticket. A ticket may not be closed until all tickets it depends on are closed." %}"><button type="button" class="btn btn-primary btn-sm"><i class="fas fa-child"></i>&nbsp;{% trans "Add Dependency" %}</button></a></p> <p><a data-toggle='tooltip' href='{% url 'helpdesk:ticket_dependency_add' ticket.id %}' title="{% trans "Click on 'Add Dependency', if you want to make this ticket dependent on another ticket. A ticket may not be closed until all tickets it depends on are closed." %}"><button type="button" class="btn btn-primary btn-sm"><i class="fas fa-child"></i>&nbsp;{% trans "Add Dependency" %}</button></a></p>
</td> </td>
</tr> </tr>
<tr>
<th>{% trans "Total time spent" %}</th>
<td>{{ ticket.time_spent }}</td>
</tr>
</tbody> </tbody>
</table> </table>
</div> </div>

View File

@ -222,6 +222,7 @@
<th>{% trans "Created" %}</th> <th>{% trans "Created" %}</th>
<th>{% trans "Due Date" %}</th> <th>{% trans "Due Date" %}</th>
<th>{% trans "Owner" %}</th> <th>{% trans "Owner" %}</th>
<th>{% trans "Time Spent" %}</th>
</tr> </tr>
</thead> </thead>
{% if not server_side %} {% if not server_side %}
@ -339,6 +340,7 @@
{"data": "created"}, {"data": "created"},
{"data": "due_date"}, {"data": "due_date"},
{"data": "assigned_to"}, {"data": "assigned_to"},
{"data": "time_spent"},
] ]
}); });
}) })

View File

@ -12,6 +12,7 @@
<td data-order='{{ ticket.created|date:"U" }}'><span title='{{ ticket.created|date:"r" }}'>{{ ticket.created|naturaltime }}</span></td> <td data-order='{{ ticket.created|date:"U" }}'><span title='{{ ticket.created|date:"r" }}'>{{ ticket.created|naturaltime }}</span></td>
<td data-order='{{ ticket.due_date|date:"U" }}'><span title='{{ ticket.due_date|date:"r" }}'>{{ ticket.due_date|naturaltime }}</span></td> <td data-order='{{ ticket.due_date|date:"U" }}'><span title='{{ ticket.due_date|date:"r" }}'>{{ ticket.due_date|naturaltime }}</span></td>
<td>{{ ticket.get_assigned_to }}</td> <td>{{ ticket.get_assigned_to }}</td>
<td>{{ ticket.time_spent }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>

View File

@ -237,6 +237,7 @@ def followup_edit(request, ticket_id, followup_id):
'comment': escape(followup.comment), 'comment': escape(followup.comment),
'public': followup.public, 'public': followup.public,
'new_status': followup.new_status, 'new_status': followup.new_status,
'time_spent': followup.time_spent,
}) })
ticketcc_string, show_subscribe = \ ticketcc_string, show_subscribe = \
@ -256,9 +257,13 @@ def followup_edit(request, ticket_id, followup_id):
comment = form.cleaned_data['comment'] comment = form.cleaned_data['comment']
public = form.cleaned_data['public'] public = form.cleaned_data['public']
new_status = form.cleaned_data['new_status'] new_status = form.cleaned_data['new_status']
time_spent = form.cleaned_data['time_spent']
# will save previous date # will save previous date
old_date = followup.date old_date = followup.date
new_followup = FollowUp(title=title, date=old_date, ticket=_ticket, comment=comment, public=public, new_status=new_status, ) new_followup = FollowUp(title=title, date=old_date, ticket=_ticket,
comment=comment, public=public,
new_status=new_status,
time_spent=time_spent)
# keep old user if one did exist before. # keep old user if one did exist before.
if followup.user: if followup.user:
new_followup.user = followup.user new_followup.user = followup.user
@ -469,6 +474,11 @@ def update_ticket(request, ticket_id, public=False):
due_date_year = int(request.POST.get('due_date_year', 0)) due_date_year = int(request.POST.get('due_date_year', 0))
due_date_month = int(request.POST.get('due_date_month', 0)) due_date_month = int(request.POST.get('due_date_month', 0))
due_date_day = int(request.POST.get('due_date_day', 0)) due_date_day = int(request.POST.get('due_date_day', 0))
if request.POST.get("time_spent"):
(hours, minutes) = [int(f) for f in request.POST.get("time_spent").split(":")]
time_spent = timedelta(hours=hours, minutes=minutes)
else:
time_spent = None
# NOTE: jQuery's default for dates is mm/dd/yy # NOTE: jQuery's default for dates is mm/dd/yy
# very US-centric but for now that's the only format supported # very US-centric but for now that's the only format supported
# until we clean up code to internationalize a little more # until we clean up code to internationalize a little more
@ -523,7 +533,8 @@ def update_ticket(request, ticket_id, public=False):
if owner is -1 and ticket.assigned_to: if owner is -1 and ticket.assigned_to:
owner = ticket.assigned_to.id owner = ticket.assigned_to.id
f = FollowUp(ticket=ticket, date=timezone.now(), comment=comment) f = FollowUp(ticket=ticket, date=timezone.now(), comment=comment,
time_spent=time_spent)
if is_helpdesk_staff(request.user): if is_helpdesk_staff(request.user):
f.user = request.user f.user = request.user
@ -1161,6 +1172,7 @@ def report_index(request):
'open': queue.ticket_set.filter(status__in=[1, 2]).count(), 'open': queue.ticket_set.filter(status__in=[1, 2]).count(),
'resolved': queue.ticket_set.filter(status=3).count(), 'resolved': queue.ticket_set.filter(status=3).count(),
'closed': queue.ticket_set.filter(status=4).count(), 'closed': queue.ticket_set.filter(status=4).count(),
'time_spent': queue.time_spent
} }
dash_tickets.append(dash_ticket) dash_tickets.append(dash_ticket)