Merge pull request #2685 from SchrodingersGat/date-format

Adds a user-configurable setting to configure how dates are displayed
This commit is contained in:
Oliver 2022-03-01 08:45:30 +11:00 committed by GitHub
commit a30e478412
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 201 additions and 43 deletions

View File

@ -151,7 +151,7 @@ src="{% static 'img/blank_image.png' %}"
{% trans "Target Date" %}
</td>
<td>
{{ build.target_date }}
{% render_date build.target_date %}
{% if build.is_overdue %}
<span title='{% blocktrans with target=build.target_date %}This build was due on {{target}}{% endblocktrans %}' class='badge badge-right rounded-pill bg-danger'>{% trans "Overdue" %}</span>
{% endif %}

View File

@ -18,7 +18,7 @@
<div class='panel-content'>
<div class='row'>
<div class='col-sm-6'>
<table class='table table-striped'>
<table class='table table-striped table-condensed'>
<col width='25'>
<tr>
<td><span class='fas fa-info'></span></td>
@ -120,19 +120,19 @@
</table>
</div>
<div class='col-sm-6'>
<table class='table table-striped'>
<table class='table table-striped table-condensed'>
<col width='25'>
<tr>
<td><span class='fas fa-calendar-alt'></span></td>
<td>{% trans "Created" %}</td>
<td>{{ build.creation_date }}</td>
<td>{% render_date build.creation_date %}</td>
</tr>
<tr>
<td><span class='fas fa-calendar-alt'></span></td>
<td>{% trans "Target Date" %}</td>
{% if build.target_date %}
<td>
{{ build.target_date }}{% if build.is_overdue %} <span class='fas fa-calendar-times icon-red'></span>{% endif %}
{% render_date build.target_date %}{% if build.is_overdue %} <span class='fas fa-calendar-times icon-red'></span>{% endif %}
</td>
{% else %}
<td><em>{% trans "No target date set" %}</em></td>
@ -142,7 +142,7 @@
<td><span class='fas fa-calendar-alt'></span></td>
<td>{% trans "Completed" %}</td>
{% if build.completion_date %}
<td>{{ build.completion_date }}{% if build.completed_by %}<span class='badge badge-right rounded-pill bg-dark'>{{ build.completed_by }}</span>{% endif %}</td>
<td>{% render_date build.completion_date %}{% if build.completed_by %}<span class='badge badge-right rounded-pill bg-dark'>{{ build.completed_by }}</span>{% endif %}</td>
{% else %}
<td><em>{% trans "Build not complete" %}</em></td>
{% endif %}

View File

@ -443,12 +443,12 @@ class BaseInvenTreeSetting(models.Model):
except self.DoesNotExist:
pass
def choices(self):
def choices(self, **kwargs):
"""
Return the available choices for this setting (or None if no choices are defined)
"""
return self.__class__.get_setting_choices(self.key)
return self.__class__.get_setting_choices(self.key, **kwargs)
def valid_options(self):
"""
@ -462,6 +462,33 @@ class BaseInvenTreeSetting(models.Model):
return [opt[0] for opt in choices]
def is_choice(self, **kwargs):
"""
Check if this setting is a "choice" field
"""
return self.__class__.get_setting_choices(self.key, **kwargs) is not None
def as_choice(self, **kwargs):
"""
Render this setting as the "display" value of a choice field,
e.g. if the choices are:
[('A4', 'A4 paper'), ('A3', 'A3 paper')],
and the value is 'A4',
then display 'A4 paper'
"""
choices = self.get_setting_choices(self.key, **kwargs)
if not choices:
return self.value
for value, display in choices:
if value == self.value:
return display
return self.value
def is_bool(self, **kwargs):
"""
Check if this setting is required to be a boolean value
@ -1212,6 +1239,21 @@ class InvenTreeUserSetting(BaseInvenTreeSetting):
'default': False,
'validator': bool,
},
'DATE_DISPLAY_FORMAT': {
'name': _('Date Format'),
'description': _('Preferred format for displaying dates'),
'default': 'YYYY-MM-DD',
'choices': [
('YYYY-MM-DD', '2022-02-22'),
('YYYY/MM/DD', '2022/22/22'),
('DD-MM-YYYY', '22-02-2022'),
('DD/MM/YYYY', '22/02/2022'),
('MM-DD-YYYY', '02-22-2022'),
('MM/DD/YYYY', '02/22/2022'),
('MMM DD YYYY', 'Feb 22 2022'),
]
}
}
class Meta:

View File

@ -141,27 +141,27 @@ src="{% static 'img/blank_image.png' %}"
<tr>
<td><span class='fas fa-calendar-alt'></span></td>
<td>{% trans "Created" %}</td>
<td>{{ order.creation_date }}<span class='badge badge-right rounded-pill bg-dark'>{{ order.created_by }}</span></td>
<td>{% render_date order.creation_date %}<span class='badge badge-right rounded-pill bg-dark'>{{ order.created_by }}</span></td>
</tr>
{% if order.issue_date %}
<tr>
<td><span class='fas fa-calendar-alt'></span></td>
<td>{% trans "Issued" %}</td>
<td>{{ order.issue_date }}</td>
<td>{% render_date order.issue_date %}</td>
</tr>
{% endif %}
{% if order.target_date %}
<tr>
<td><span class='fas fa-calendar-alt'></span></td>
<td>{% trans "Target Date" %}</td>
<td>{{ order.target_date }}</td>
<td>{% render_date order.target_date %}</td>
</tr>
{% endif %}
{% if order.status == PurchaseOrderStatus.COMPLETE %}
<tr>
<td><span class='fas fa-calendar-alt'></span></td>
<td>{% trans "Received" %}</td>
<td>{{ order.complete_date }}<span class='badge badge-right rounded-pill bg-dark'>{{ order.received_by }}</span></td>
<td>{% render_date order.complete_date %}<span class='badge badge-right rounded-pill bg-dark'>{{ order.received_by }}</span></td>
</tr>
{% endif %}
{% if order.responsible %}

View File

@ -155,13 +155,13 @@ src="{% static 'img/blank_image.png' %}"
<tr>
<td><span class='fas fa-calendar-alt'></span></td>
<td>{% trans "Created" %}</td>
<td>{{ order.creation_date }}<span class='badge badge-right rounded-pill bg-dark'>{{ order.created_by }}</span></td>
<td>{% render_date order.creation_date %}<span class='badge badge-right rounded-pill bg-dark'>{{ order.created_by }}</span></td>
</tr>
{% if order.target_date %}
<tr>
<td><span class='fas fa-calendar-alt'></span></td>
<td>{% trans "Target Date" %}</td>
<td>{{ order.target_date }}</td>
<td>{% render_date order.target_date %}</td>
</tr>
{% endif %}
{% if order.shipment_date %}
@ -169,7 +169,7 @@ src="{% static 'img/blank_image.png' %}"
<td><span class='fas fa-truck'></span></td>
<td>{% trans "Completed" %}</td>
<td>
{{ order.shipment_date }}
{% render_date order.shipment_date %}
{% if order.shipped_by %}
<span class='badge badge-right rounded-pill bg-dark'>{{ order.shipped_by }}</span>
{% endif %}

View File

@ -952,7 +952,7 @@
{% if price_history %}
var purchasepricedata = {
labels: [
{% for line in price_history %}'{{ line.date }}',{% endfor %}
{% for line in price_history %}'{% render_date line.date %}',{% endfor %}
],
datasets: [{
label: '{% blocktrans %}Purchase Unit Price - {{currency}}{% endblocktrans %}',
@ -1065,7 +1065,7 @@
{% if sale_history %}
var salepricedata = {
labels: [
{% for line in sale_history %}'{{ line.date }}',{% endfor %}
{% for line in sale_history %}'{% render_date line.date %}',{% endfor %}
],
datasets: [{
label: '{% blocktrans %}Unit Price - {{currency}}{% endblocktrans %}',

View File

@ -312,7 +312,7 @@
<td><span class='fas fa-calendar-alt'></span></td>
<td>{% trans "Creation Date" %}</td>
<td>
{{ part.creation_date }}
{% render_date part.creation_date %}
{% if part.creation_user %}
<span class='badge badge-right rounded-pill bg-dark'>{{ part.creation_user }}</span>
{% endif %}

View File

@ -5,6 +5,7 @@ This module provides template tags for extra functionality,
over and above the built-in Django tags.
"""
from datetime import date
import os
import sys
@ -43,6 +44,52 @@ def define(value, *args, **kwargs):
return value
@register.simple_tag(takes_context=True)
def render_date(context, date_object):
"""
Renders a date according to the preference of the provided user
Note that the user preference is stored using the formatting adopted by moment.js,
which differs from the python formatting!
"""
if date_object is None:
return None
if type(date_object) == str:
# If a string is passed, first convert it to a datetime
date_object = date.fromisoformat(date_object)
# We may have already pre-cached the date format by calling this already!
user_date_format = context.get('user_date_format', None)
if user_date_format is None:
user = context.get('user', None)
if user:
# User is specified - look for their date display preference
user_date_format = InvenTreeUserSetting.get_setting('DATE_DISPLAY_FORMAT', user=user)
else:
user_date_format = 'YYYY-MM-DD'
# Convert the format string to Pythonic equivalent
replacements = [
('YYYY', '%Y'),
('MMM', '%b'),
('MM', '%m'),
('DD', '%d'),
]
for o, n in replacements:
user_date_format = user_date_format.replace(o, n)
# Update the context cache
context['user_date_format'] = user_date_format
return date_object.strftime(user_date_format)
@register.simple_tag()
def decimal(x, *args, **kwargs):
""" Simplified rendering of a decimal number """

View File

@ -120,13 +120,13 @@ content: "v{{report_revision}} - {{ date.isoformat }}";
</tr>
<tr>
<th>{% trans "Issued" %}</th>
<td>{{ build.creation_date }}</td>
<td>{% render_date build.creation_date %}</td>
</tr>
<tr>
<th>{% trans "Target Date" %}</th>
<td>
{% if build.target_date %}
{{ build.target_date }}
{% render_date build.target_date %}
{% else %}
<em>Not specified</em>
{% endif %}

View File

@ -187,7 +187,7 @@
<td><span class='fas fa-calendar-alt{% if item.is_expired %} icon-red{% endif %}'></span></td>
<td>{% trans "Expiry Date" %}</td>
<td>
{{ item.expiry_date }}
{% render_date item.expiry_date %}
{% if item.is_expired %}
<span title='{% blocktrans %}This StockItem expired on {{ item.expiry_date }}{% endblocktrans %}' class='badge rounded-pill bg-danger badge-right'>{% trans "Expired" %}</span>
{% elif item.is_stale %}
@ -205,7 +205,7 @@
<td><span class='fas fa-calendar-alt'></span></td>
<td>{% trans "Last Stocktake" %}</td>
{% if item.stocktake_date %}
<td>{{ item.stocktake_date }} <span class='badge badge-right rounded-pill bg-dark'>{{ item.stocktake_user }}</span></td>
<td>{% render_date item.stocktake_date %} <span class='badge badge-right rounded-pill bg-dark'>{{ item.stocktake_user }}</span></td>
{% else %}
<td><em>{% trans "No stocktake performed" %}</em></td>
{% endif %}

View File

@ -81,7 +81,7 @@
{% endif %}
</td>
<td>{{ plugin.author }}</td>
<td>{{ plugin.pub_date }}</td>
<td>{% render_date plugin.pub_date %}</td>
<td>{% if plugin.version %}{{ plugin.version }}{% endif %}</td>
</tr>
{% endfor %}

View File

@ -36,7 +36,7 @@
<tr>
<td><span class='fas fa-calendar-alt'></span></td>
<td>{% trans "Date" %}</td>
<td>{{ plugin.pub_date }}{% include "clip.html" %}</td>
<td>{% render_date plugin.pub_date %}{% include "clip.html" %}</td>
</tr>
<tr>
<td><span class='fas fa-hashtag'></span></td>
@ -101,7 +101,7 @@
</tr>
<tr>
<td><span class='fas fa-calendar-alt'></span></td>
<td>{% trans "Commit Date" %}</td><td>{{ plugin.package.date }}{% include "clip.html" %}</td>
<td>{% trans "Commit Date" %}</td><td>{% render_date plugin.package.date %}{% include "clip.html" %}</td>
</tr>
<tr>
<td><span class='fas fa-code-branch'></span></td>

View File

@ -28,7 +28,11 @@
<div id='setting-{{ setting.pk }}'>
<span id='setting-value-{{ setting.key.upper }}' fieldname='{{ setting.key.upper }}'>
{% if setting.value %}
{% if setting.is_choice %}
<strong>{{ setting.as_choice }}</strong>
{% else %}
<strong>{{ setting.value }}</strong>
{% endif %}
{% else %}
<em style='color: #855;'>{% trans "No value set" %}</em>
{% endif %}

View File

@ -15,6 +15,7 @@
<table class='table table-striped table-condensed'>
<tbody>
{% include "InvenTree/settings/setting.html" with key="STICKY_HEADER" icon="fa-bars" user_setting=True %}
{% include "InvenTree/settings/setting.html" with key="DATE_DISPLAY_FORMAT" icon="fa-calendar-alt" user_setting=True %}
{% include "InvenTree/settings/setting.html" with key="FORMS_CLOSE_USING_ESCAPE" icon="fa-window-close" user_setting=True %}
{% include "InvenTree/settings/setting.html" with key="PART_SHOW_QUANTITY_IN_FORMS" icon="fa-hashtag" user_setting=True %}
</tbody>

View File

@ -44,7 +44,7 @@
{% if commit_date %}
<tr>
<td><span class='fas fa-calendar-alt'></span></td>
<td>{% trans "Commit Date" %}</td><td>{{ commit_date }}{% include "clip.html" %}</td>
<td>{% trans "Commit Date" %}</td><td>{% render_date commit_date %}{% include "clip.html" %}</td>
</tr>
{% endif %}
{% endif %}

View File

@ -7,6 +7,7 @@
clearEvents,
endDate,
startDate,
renderDate,
*/
/**
@ -32,3 +33,33 @@ function clearEvents(calendar) {
event.remove();
});
}
/*
* Render the provided date in the user-specified format.
*
* The provided "date" variable is a string, nominally ISO format e.g. 2022-02-22
* The user-configured setting DATE_DISPLAY_FORMAT determines how the date should be displayed.
*/
function renderDate(date, options={}) {
if (!date) {
return null;
}
var fmt = user_settings.DATE_DISPLAY_FORMAT || 'YYYY-MM-DD';
if (options.showTime) {
fmt += ' HH:mm';
}
var m = moment(date);
if (m.isValid()) {
return m.format(fmt);
} else {
// Invalid input string, simply return provided value
return date;
}
}

View File

@ -40,12 +40,15 @@ function editSetting(pk, options={}) {
url = `/api/settings/user/${pk}/`;
}
var reload_required = false;
// First, read the settings object from the server
inventreeGet(url, {}, {
success: function(response) {
if (response.choices && response.choices.length > 0) {
response.type = 'choice';
reload_required = true;
}
// Construct the field
@ -89,7 +92,9 @@ function editSetting(pk, options={}) {
var setting = response.key;
if (response.type == 'boolean') {
if (reload_required) {
location.reload();
} else if (response.type == 'boolean') {
var enabled = response.value.toString().toLowerCase() == 'true';
$(`#setting-value-${setting}`).prop('checked', enabled);
} else {

View File

@ -165,6 +165,9 @@ function loadAttachmentTable(url, options) {
{
field: 'upload_date',
title: '{% trans "Upload Date" %}',
formatter: function(value) {
return renderDate(value);
}
},
{
field: 'actions',

View File

@ -1961,6 +1961,9 @@ function loadBuildTable(table, options) {
field: 'creation_date',
title: '{% trans "Created" %}',
sortable: true,
formatter: function(value) {
return renderDate(value);
}
},
{
field: 'issued_by',
@ -1990,11 +1993,17 @@ function loadBuildTable(table, options) {
field: 'target_date',
title: '{% trans "Target Date" %}',
sortable: true,
formatter: function(value) {
return renderDate(value);
}
},
{
field: 'completion_date',
title: '{% trans "Completion Date" %}',
sortable: true,
formatter: function(value) {
return renderDate(value);
}
},
],
});

View File

@ -923,11 +923,17 @@ function loadPurchaseOrderTable(table, options) {
field: 'creation_date',
title: '{% trans "Date" %}',
sortable: true,
formatter: function(value) {
return renderDate(value);
}
},
{
field: 'target_date',
title: '{% trans "Target Date" %}',
sortable: true,
formatter: function(value) {
return renderDate(value);
}
},
{
field: 'line_items',
@ -1198,7 +1204,7 @@ function loadPurchaseOrderLineItemTable(table, options={}) {
title: '{% trans "Target Date" %}',
formatter: function(value, row) {
if (row.target_date) {
var html = row.target_date;
var html = renderDate(row.target_date);
if (row.overdue) {
html += `<span class='fas fa-calendar-alt icon-red float-right' title='{% trans "This line item is overdue" %}'></span>`;
@ -1207,7 +1213,7 @@ function loadPurchaseOrderLineItemTable(table, options={}) {
return html;
} else if (row.order_detail && row.order_detail.target_date) {
return `<em>${row.order_detail.target_date}</em>`;
return `<em>${renderDate(row.order_detail.target_date)}</em>`;
} else {
return '-';
}
@ -1371,16 +1377,25 @@ function loadSalesOrderTable(table, options) {
sortable: true,
field: 'creation_date',
title: '{% trans "Creation Date" %}',
formatter: function(value) {
return renderDate(value);
}
},
{
sortable: true,
field: 'target_date',
title: '{% trans "Target Date" %}',
formatter: function(value) {
return renderDate(value);
}
},
{
sortable: true,
field: 'shipment_date',
title: '{% trans "Shipment Date" %}',
formatter: function(value) {
return renderDate(value);
}
},
{
sortable: true,
@ -1532,9 +1547,9 @@ function loadSalesOrderShipmentTable(table, options={}) {
sortable: true,
formatter: function(value, row) {
if (value) {
return value;
return renderDate(value);
} else {
return '{% trans "Not shipped" %}';
return '<em>{% trans "Not shipped" %}</em>';
}
}
},
@ -2317,7 +2332,7 @@ function loadSalesOrderLineItemTable(table, options={}) {
switchable: true,
formatter: function(value, row) {
if (row.target_date) {
var html = row.target_date;
var html = renderDate(row.target_date);
if (row.overdue) {
html += `<span class='fas fa-calendar-alt icon-red float-right' title='{% trans "This line item is overdue" %}'></span>`;
@ -2326,7 +2341,7 @@ function loadSalesOrderLineItemTable(table, options={}) {
return html;
} else if (row.order_detail && row.order_detail.target_date) {
return `<em>${row.order_detail.target_date}</em>`;
return `<em>${renderDate(row.order_detail.target_date)}</em>`;
} else {
return '-';
}

View File

@ -25,7 +25,6 @@
modalSetContent,
modalSetTitle,
modalSubmit,
moment,
openModal,
printStockItemLabels,
printTestReports,
@ -1820,6 +1819,9 @@ function loadStockTable(table, options) {
col = {
field: 'stocktake_date',
title: '{% trans "Stocktake" %}',
formatter: function(value) {
return renderDate(value);
}
};
if (!options.params.ordering) {
@ -1833,6 +1835,9 @@ function loadStockTable(table, options) {
title: '{% trans "Expiry Date" %}',
visible: global_settings.STOCK_ENABLE_EXPIRY,
switchable: global_settings.STOCK_ENABLE_EXPIRY,
formatter: function(value) {
return renderDate(value);
}
};
if (!options.params.ordering) {
@ -1844,6 +1849,9 @@ function loadStockTable(table, options) {
col = {
field: 'updated',
title: '{% trans "Last Updated" %}',
formatter: function(value) {
return renderDate(value);
}
};
if (!options.params.ordering) {
@ -2649,14 +2657,7 @@ function loadStockTrackingTable(table, options) {
title: '{% trans "Date" %}',
sortable: true,
formatter: function(value) {
var m = moment(value);
if (m.isValid()) {
var html = m.format('dddd MMMM Do YYYY'); // + '<br>' + m.format('h:mm a');
return html;
}
return '<i>{% trans "Invalid date" %}</i>';
return renderDate(value, {showTime: true});
}
});

View File

@ -2,7 +2,7 @@
InvenTree-Version: {% inventree_version %}
Django Version: {% django_version %}
{% inventree_commit_hash as hash %}{% if hash %}Commit Hash: {{ hash }}{% endif %}
{% inventree_commit_date as commit_date %}{% if commit_date %}Commit Date: {{ commit_date }}{% endif %}
{% inventree_commit_date as commit_date %}{% if commit_date %}Commit Date: {% render_date commit_date %}{% endif %}
Database: {% inventree_db_engine %}
Debug-Mode: {% inventree_in_debug_mode %}
Deployed using Docker: {% inventree_docker_mode %}