Merge pull request #946 from alligatorbait/custom_datetime

CustomField datetime type formats updated to fixed string formats
This commit is contained in:
Garret Wassermann 2021-02-10 20:14:46 -05:00 committed by GitHub
commit db6202aac0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 100 additions and 31 deletions

View File

@ -7,6 +7,7 @@ forms.py - Definitions of newforms-based forms for creating and maintaining
tickets. tickets.
""" """
import logging import logging
from datetime import datetime, date, time
from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django import forms from django import forms
@ -35,6 +36,10 @@ CUSTOMFIELD_TO_FIELD_DICT = {
'slug': forms.SlugField, 'slug': forms.SlugField,
} }
CUSTOMFIELD_DATE_FORMAT = "%Y-%m-%d"
CUSTOMFIELD_TIME_FORMAT = "%H:%M:%S"
CUSTOMFIELD_DATETIME_FORMAT = f"{CUSTOMFIELD_DATE_FORMAT} {CUSTOMFIELD_TIME_FORMAT}"
class CustomFieldMixin(object): class CustomFieldMixin(object):
""" """
@ -71,8 +76,14 @@ class CustomFieldMixin(object):
# Try to use the immediate equivalences dictionary # Try to use the immediate equivalences dictionary
try: try:
fieldclass = CUSTOMFIELD_TO_FIELD_DICT[field.data_type] fieldclass = CUSTOMFIELD_TO_FIELD_DICT[field.data_type]
# Change widget in case it is a boolean # Change widgets for the following classes
if fieldclass == forms.BooleanField: if fieldclass == forms.DateField:
instanceargs['widget'] = forms.DateInput(attrs={'class': 'form-control date-field'})
elif fieldclass == forms.DateTimeField:
instanceargs['widget'] = forms.DateTimeInput(attrs={'class': 'form-control datetime-field'})
elif fieldclass == forms.TimeField:
instanceargs['widget'] = forms.TimeInput(attrs={'class': 'form-control time-field'})
elif fieldclass == forms.BooleanField:
instanceargs['widget'] = forms.CheckboxInput(attrs={'class': 'form-control'}) instanceargs['widget'] = forms.CheckboxInput(attrs={'class': 'form-control'})
except KeyError: except KeyError:
@ -88,6 +99,9 @@ class EditTicketForm(CustomFieldMixin, forms.ModelForm):
model = Ticket model = Ticket
exclude = ('created', 'modified', 'status', 'on_hold', 'resolution', 'last_escalation', 'assigned_to') exclude = ('created', 'modified', 'status', 'on_hold', 'resolution', 'last_escalation', 'assigned_to')
class Media:
js = ('helpdesk/js/init_due_date.js', 'helpdesk/js/init_datetime_classes.js')
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
""" """
Add any custom fields that are defined to the form Add any custom fields that are defined to the form
@ -99,14 +113,24 @@ class EditTicketForm(CustomFieldMixin, forms.ModelForm):
self.fields['merged_to'].help_text = _('This ticket is merged into the selected ticket.') self.fields['merged_to'].help_text = _('This ticket is merged into the selected ticket.')
for field in CustomField.objects.all(): for field in CustomField.objects.all():
initial_value = None
try: try:
current_value = TicketCustomFieldValue.objects.get(ticket=self.instance, field=field) current_value = TicketCustomFieldValue.objects.get(ticket=self.instance, field=field)
initial_value = current_value.value initial_value = current_value.value
# Attempt to convert from fixed format string to date/time data type
if 'datetime' == current_value.field.data_type:
initial_value = datetime.strptime(initial_value, CUSTOMFIELD_DATETIME_FORMAT)
elif 'date' == current_value.field.data_type:
initial_value = datetime.strptime(initial_value, CUSTOMFIELD_DATE_FORMAT)
elif 'time' == current_value.field.data_type:
initial_value = datetime.strptime(initial_value, CUSTOMFIELD_TIME_FORMAT)
# If it is boolean field, transform the value to a real boolean instead of a string # If it is boolean field, transform the value to a real boolean instead of a string
if current_value.field.data_type == 'boolean': elif 'boolean' == current_value.field.data_type:
initial_value = initial_value == 'True' initial_value = 'True' == initial_value
except TicketCustomFieldValue.DoesNotExist: except (TicketCustomFieldValue.DoesNotExist, ValueError, TypeError):
initial_value = None # ValueError error if parsing fails, using initial_value = current_value.value
# TypeError if parsing None type
pass
instanceargs = { instanceargs = {
'label': field.label, 'label': field.label,
'help_text': field.help_text, 'help_text': field.help_text,
@ -126,7 +150,16 @@ class EditTicketForm(CustomFieldMixin, forms.ModelForm):
cfv = TicketCustomFieldValue.objects.get(ticket=self.instance, field=customfield) cfv = TicketCustomFieldValue.objects.get(ticket=self.instance, field=customfield)
except ObjectDoesNotExist: except ObjectDoesNotExist:
cfv = TicketCustomFieldValue(ticket=self.instance, field=customfield) cfv = TicketCustomFieldValue(ticket=self.instance, field=customfield)
cfv.value = value
# Convert date/time data type to known fixed format string.
if datetime is type(value):
cfv.value = value.strftime(CUSTOMFIELD_DATETIME_FORMAT)
elif date is type(value):
cfv.value = value.strftime(CUSTOMFIELD_DATE_FORMAT)
elif time is type(value):
cfv.value = value.strftime(CUSTOMFIELD_TIME_FORMAT)
else:
cfv.value = value
cfv.save() cfv.save()
return super(EditTicketForm, self).save(*args, **kwargs) return super(EditTicketForm, self).save(*args, **kwargs)
@ -182,7 +215,7 @@ class AbstractTicketForm(CustomFieldMixin, forms.Form):
due_date = forms.DateTimeField( due_date = forms.DateTimeField(
widget=forms.TextInput(attrs={'class': 'form-control', 'autocomplete': 'off'}), widget=forms.TextInput(attrs={'class': 'form-control', 'autocomplete': 'off'}),
required=False, required=False,
input_formats=['%d/%m/%Y', '%m/%d/%Y', "%d.%m.%Y"], input_formats=[CUSTOMFIELD_DATE_FORMAT, CUSTOMFIELD_DATETIME_FORMAT, '%d/%m/%Y', '%m/%d/%Y', "%d.%m.%Y"],
label=_('Due on'), label=_('Due on'),
) )
@ -194,7 +227,7 @@ class AbstractTicketForm(CustomFieldMixin, forms.Form):
) )
class Media: class Media:
js = ('helpdesk/js/init_due_date.js',) js = ('helpdesk/js/init_due_date.js', 'helpdesk/js/init_datetime_classes.js')
def __init__(self, kbcategory=None, *args, **kwargs): def __init__(self, kbcategory=None, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)

View File

@ -0,0 +1,10 @@
$(() => {
$(".date-field").datepicker({dateFormat: 'yy-mm-dd'});
});
$(() => {
$(".datetime-field").datepicker({dateFormat: 'yy-mm-dd 00:00:00'});
});
$(() => {
// TODO: This does not work as written, need to make functional
$(".time-field").tooltip="Time format 24hr: 00:00:00";
});

View File

@ -1,3 +1,3 @@
$(() => { $(() => {
$("#id_due_date").datepicker(); $("#id_due_date").datepicker({dateFormat: 'yy-mm-dd 00:00:00'});
}); });

View File

@ -26,7 +26,7 @@
<strong>{% trans "Note" %}:</strong> <strong>{% trans "Note" %}:</strong>
{% blocktrans %}Editing a ticket does <em>not</em> send an e-mail to the ticket owner or submitter. No new details should be entered, this form should only be used to fix incorrect details or clean up the submission.{% endblocktrans %} {% blocktrans %}Editing a ticket does <em>not</em> send an e-mail to the ticket owner or submitter. No new details should be entered, this form should only be used to fix incorrect details or clean up the submission.{% endblocktrans %}
</p> </p>
{% if errors %}{% for error in errors %}{% trans "Error: " %}{{ error }}{% endfor %}{% endif %} {% if errors %}<p class="text-danger">{% for error in errors %}{% trans "Error: " %}{{ error }}<br>{% endfor %}</p>{% endif %}
<form method='post'> <form method='post'>
{% csrf_token %} {% csrf_token %}
<fieldset> <fieldset>
@ -45,9 +45,5 @@
{% endblock %} {% endblock %}
{% block helpdesk_js %} {% block helpdesk_js %}
<script> {{ form.media.js }}
$(() => {
$("#id_due_date").datepicker({dateFormat: 'yy-mm-dd'});
})
</script>
{% endblock %} {% endblock %}

View File

@ -1,5 +1,6 @@
{% load i18n humanize ticket_to_link %} {% load i18n humanize ticket_to_link %}
{% load static %} {% load static %}
{% load helpdesk_util %}
<div class="card mb-3"> <div class="card mb-3">
<!--div class="card-header"> <!--div class="card-header">
@ -9,7 +10,7 @@
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-sm table-border"> <table class="table table-sm table-border">
<thead class="thead-light"> <thead class="thead-light">
<tr class=''><th colspan='4'><h3>{{ ticket.queue.slug }}-{{ ticket.id }}. {{ ticket.title }} [{{ ticket.get_status }}]</h3> <tr class=''><th colspan='4'><h3>{{ ticket.queue.slug }}-{{ ticket.id }}. {{ ticket.title }} [{{ ticket.get_status }}]</h3>
{% blocktrans with ticket.queue as queue %}Queue: {{ queue }}{% endblocktrans %} {% blocktrans with ticket.queue as queue %}Queue: {{ queue }}{% endblocktrans %}
<span class='ticket_toolbar float-right'> <span class='ticket_toolbar float-right'>
<a href="{% url 'helpdesk:edit' ticket.id %}" class="ticket-edit"><button class="btn btn-warning btn-sm"><i class="fas fa-pencil-alt"></i> {% trans "Edit" %}</button></a> <a href="{% url 'helpdesk:edit' ticket.id %}" class="ticket-edit"><button class="btn btn-warning btn-sm"><i class="fas fa-pencil-alt"></i> {% trans "Edit" %}</button></a>
@ -21,20 +22,25 @@
{% for customfield in ticket.ticketcustomfieldvalue_set.all %} {% for customfield in ticket.ticketcustomfieldvalue_set.all %}
<tr> <tr>
<th class="table-secondary">{{ customfield.field.label }}</th> <th class="table-secondary">{{ customfield.field.label }}</th>
<td>{% ifequal customfield.field.data_type "url" %}<a href='{{ customfield.value }}'>{{ customfield.value }}</a>{% else %}{{ customfield.value|default:"" }}{% endifequal %}</td> <td>{% spaceless %}{% if "url" == customfield.field.data_type %}<a href='{{ customfield.value }}'>{{ customfield.value }}</a>
{% elif "datetime" == customfield.field.data_type %}{{ customfield.value|datetime_string_format }}
{% elif "date" == customfield.field.data_type %}{{ customfield.value|datetime_string_format }}
{% elif "time" == customfield.field.data_type %}{{ customfield.value|datetime_string_format }}
{% else %}{{ customfield.value|default:"" }}
{% endif %}{% endspaceless %}</td>
</tr>{% endfor %} </tr>{% endfor %}
<tr> <tr>
<th class="table-active">{% trans "Due Date" %}</th> <th class="table-active">{% trans "Due Date" %}</th>
<td>{{ ticket.due_date|date }} {% if ticket.due_date %}({{ ticket.due_date|naturaltime }}){% endif %} <td>{{ ticket.due_date|date:"DATETIME_FORMAT" }} {% if ticket.due_date %}({{ ticket.due_date|naturaltime }}){% endif %}
</td> </td>
<th class="table-active">{% trans "Submitted On" %}</th> <th class="table-active">{% trans "Submitted On" %}</th>
<td>{{ ticket.created|date }} ({{ ticket.created|naturaltime }})</td> <td>{{ ticket.created|date:"DATETIME_FORMAT" }} ({{ ticket.created|naturaltime }})</td>
</tr> </tr>
<tr> <tr>
<th class="table-active">{% trans "Assigned To" %}</th> <th class="table-active">{% trans "Assigned To" %}</th>
<td>{{ ticket.get_assigned_to }}{% ifequal ticket.get_assigned_to _('Unassigned') %} <strong> <td>{{ ticket.get_assigned_to }}{% if _('Unassigned') == ticket.get_assigned_to %} <strong>
<a data-toggle="tooltip" href='?take' title='{% trans "Assign this ticket to " %}{{ request.user.email }}'><button type="button" class="btn btn-primary btn-sm float-right"><i class="fas fa-hand-paper"></i></button></a> <a data-toggle="tooltip" href='?take' title='{% trans "Assign this ticket to " %}{{ request.user.email }}'><button type="button" class="btn btn-primary btn-sm float-right"><i class="fas fa-hand-paper"></i></button></a>
</strong>{% endifequal %} </strong>{% endif %}
</td> </td>
<th class="table-active">{% trans "Submitter E-Mail" %}</th> <th class="table-active">{% trans "Submitter E-Mail" %}</th>
<td> {{ ticket.submitter_email }} <td> {{ ticket.submitter_email }}
@ -97,7 +103,7 @@
</tr> </tr>
{% if ticket.resolution %}<tr> {% if ticket.resolution %}<tr>
<th colspan='2'>{% trans "Resolution" %}{% ifequal ticket.get_status_display "Resolved" %} <a href='?close'><button type="button" class="btn btn-warning btn-sm">{% trans "Accept and Close" %}</button></a>{% endifequal %}</th> <th colspan='2'>{% trans "Resolution" %}{% if "Resolved" == ticket.get_status_display %} <a href='?close'><button type="button" class="btn btn-warning btn-sm">{% trans "Accept and Close" %}</button></a>{% endif %}</th>
</tr> </tr>
<tr> <tr>
<td colspan='2'>{{ ticket.get_resolution_markdown|urlizetrunc:50|linebreaksbr }}</td> <td colspan='2'>{{ ticket.get_resolution_markdown|urlizetrunc:50|linebreaksbr }}</td>

View File

@ -1,9 +1,34 @@
from django import template from django.template import Library
from django.template.defaultfilters import date as date_filter
from django.conf import settings
register = template.Library() from datetime import datetime
from helpdesk.forms import CUSTOMFIELD_DATE_FORMAT, CUSTOMFIELD_TIME_FORMAT, CUSTOMFIELD_DATETIME_FORMAT
register = Library()
@register.filter @register.filter
def get(value, arg, default=None): def get(value, arg, default=None):
""" Call the dictionary get function """ """ Call the dictionary get function """
return value.get(arg, default) return value.get(arg, default)
@register.filter(expects_localtime=True)
def datetime_string_format(value):
"""
:param value: String - Expected to be a datetime, date, or time in specific format
:return: String - reformatted to default datetime, date, or time string if received in one of the expected formats
"""
try:
new_value = date_filter(datetime.strptime(value, CUSTOMFIELD_DATETIME_FORMAT), settings.DATETIME_FORMAT)
except ValueError:
try:
new_value = date_filter(datetime.strptime(value, CUSTOMFIELD_DATE_FORMAT), settings.DATE_FORMAT)
except ValueError:
try:
new_value = date_filter(datetime.strptime(value, CUSTOMFIELD_TIME_FORMAT), settings.TIME_FORMAT)
except ValueError:
new_value = value
return new_value

View File

@ -47,7 +47,7 @@ class KBTests(TestCase):
self.assertContains(response, 'This is a test category') self.assertContains(response, 'This is a test category')
self.assertContains(response, 'KBItem 1') self.assertContains(response, 'KBItem 1')
self.assertContains(response, 'KBItem 2') self.assertContains(response, 'KBItem 2')
self.assertContains(response, 'Contact a human') self.assertContains(response, 'Create New Ticket Queue:')
self.client.login(username=self.user.get_username(), password='password') self.client.login(username=self.user.get_username(), password='password')
response = self.client.get(reverse('helpdesk:kb_category', args=("test_cat", ))) response = self.client.get(reverse('helpdesk:kb_category', args=("test_cat", )))
self.assertContains(response, '<i class="fa fa-thumbs-up fa-lg"></i>') self.assertContains(response, '<i class="fa fa-thumbs-up fa-lg"></i>')

View File

@ -24,6 +24,7 @@ from django.utils.html import escape
from django.utils import timezone from django.utils import timezone
from django.views.generic.edit import FormView, UpdateView from django.views.generic.edit import FormView, UpdateView
from helpdesk.forms import CUSTOMFIELD_DATE_FORMAT
from helpdesk.query import ( from helpdesk.query import (
get_query_class, get_query_class,
query_to_base64, query_to_base64,
@ -75,9 +76,6 @@ else:
lambda u: u.is_authenticated and u.is_active and u.is_staff) lambda u: u.is_authenticated and u.is_active and u.is_staff)
User = get_user_model()
def _get_queue_choices(queues): def _get_queue_choices(queues):
"""Return list of `choices` array for html form for given queues """Return list of `choices` array for html form for given queues
@ -382,6 +380,7 @@ def view_ticket(request, ticket_id):
) )
else: else:
submitter_userprofile_url = None submitter_userprofile_url = None
return render(request, 'helpdesk/ticket.html', { return render(request, 'helpdesk/ticket.html', {
'ticket': ticket, 'ticket': ticket,
'submitter_userprofile_url': submitter_userprofile_url, 'submitter_userprofile_url': submitter_userprofile_url,
@ -1774,8 +1773,8 @@ def calc_basic_ticket_stats(Tickets):
date_30 = date_rel_to_today(today, 30) date_30 = date_rel_to_today(today, 30)
date_60 = date_rel_to_today(today, 60) date_60 = date_rel_to_today(today, 60)
date_30_str = date_30.strftime('%Y-%m-%d') date_30_str = date_30.strftime(CUSTOMFIELD_DATE_FORMAT)
date_60_str = date_60.strftime('%Y-%m-%d') date_60_str = date_60.strftime(CUSTOMFIELD_DATE_FORMAT)
# > 0 & <= 30 # > 0 & <= 30
ota_le_30 = all_open_tickets.filter(created__gte=date_30_str) ota_le_30 = all_open_tickets.filter(created__gte=date_30_str)