From ac645b1fc70a0c20ec2ab8529276ca424c0611aa Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 26 Nov 2021 22:05:27 +1100 Subject: [PATCH 01/17] Adds "responsible" row to purchase order table --- InvenTree/order/serializers.py | 11 +++++++---- InvenTree/templates/js/translated/order.js | 17 +++++++++++++++++ 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/InvenTree/order/serializers.py b/InvenTree/order/serializers.py index 9c65c2b038..983cd01a63 100644 --- a/InvenTree/order/serializers.py +++ b/InvenTree/order/serializers.py @@ -17,16 +17,16 @@ from rest_framework.serializers import ValidationError from sql_util.utils import SubqueryCount +from common.settings import currency_code_mappings +from company.serializers import CompanyBriefSerializer, SupplierPartSerializer + from InvenTree.serializers import InvenTreeAttachmentSerializer from InvenTree.serializers import InvenTreeModelSerializer from InvenTree.serializers import InvenTreeDecimalField from InvenTree.serializers import InvenTreeMoneySerializer from InvenTree.serializers import InvenTreeAttachmentSerializerField - from InvenTree.status_codes import StockStatus -from company.serializers import CompanyBriefSerializer, SupplierPartSerializer - from part.serializers import PartBriefSerializer import stock.models @@ -37,7 +37,7 @@ from .models import PurchaseOrderAttachment, SalesOrderAttachment from .models import SalesOrder, SalesOrderLineItem from .models import SalesOrderAllocation -from common.settings import currency_code_mappings +from users.serializers import OwnerSerializer class POSerializer(InvenTreeModelSerializer): @@ -86,6 +86,8 @@ class POSerializer(InvenTreeModelSerializer): reference = serializers.CharField(required=True) + responsible_detail = OwnerSerializer(source='responsible', read_only=True, many=False) + class Meta: model = PurchaseOrder @@ -100,6 +102,7 @@ class POSerializer(InvenTreeModelSerializer): 'overdue', 'reference', 'responsible', + 'responsible_detail', 'supplier', 'supplier_detail', 'supplier_reference', diff --git a/InvenTree/templates/js/translated/order.js b/InvenTree/templates/js/translated/order.js index b0e3009720..c61722858d 100644 --- a/InvenTree/templates/js/translated/order.js +++ b/InvenTree/templates/js/translated/order.js @@ -695,6 +695,23 @@ function loadPurchaseOrderTable(table, options) { title: '{% trans "Items" %}', sortable: true, }, + { + field: 'responsible', + title: '{% trans "Responsible" %}', + switchable: true, + sortable: false, + formatter: function(value, row) { + var html = row.responsible_detail.name; + + if (row.responsible_detail.label == 'group') { + html += ``; + } else { + html += ``; + } + + return html; + } + }, ], }); } From ef7a9b5152096d693871820083a7e31481d716d9 Mon Sep 17 00:00:00 2001 From: Oliver Date: Sat, 27 Nov 2021 00:11:18 +1100 Subject: [PATCH 02/17] Adds a simple endpoint for accessing serial number information for a Part instance - This is not included by default in the "part detail" endpoint as it must be calculated! --- InvenTree/part/api.py | 38 ++++++++++++++++++- .../stock/templates/stock/item_base.html | 1 + InvenTree/templates/js/translated/api.js | 1 + InvenTree/templates/js/translated/stock.js | 14 +++++++ 4 files changed, 52 insertions(+), 2 deletions(-) diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index 00b5dfa7de..403934d3d9 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -42,7 +42,7 @@ from build.models import Build from . import serializers as part_serializers -from InvenTree.helpers import str2bool, isNull +from InvenTree.helpers import str2bool, isNull, increment from InvenTree.api import AttachmentMixin from InvenTree.status_codes import BuildStatus @@ -410,6 +410,33 @@ class PartThumbsUpdate(generics.RetrieveUpdateAPIView): ] +class PartSerialNumberDetail(generics.RetrieveAPIView): + """ + API endpoint for returning extra serial number information about a particular part + """ + + queryset = Part.objects.all() + + def retrieve(self, request, *args, **kwargs): + + part = self.get_object() + + # Calculate the "latest" serial number + latest = part.getLatestSerialNumber() + + data = { + 'latest': latest, + } + + if latest is not None: + next = increment(latest) + + if next != increment: + data['next'] = next + + return Response(data) + + class PartDetail(generics.RetrieveUpdateDestroyAPIView): """ API endpoint for detail view of a single Part object """ @@ -1532,7 +1559,14 @@ part_api_urls = [ url(r'^(?P\d+)/?', PartThumbsUpdate.as_view(), name='api-part-thumbs-update'), ])), - url(r'^(?P\d+)/', PartDetail.as_view(), name='api-part-detail'), + url(r'^(?P\d+)/', include([ + + # Endpoint for extra serial number information + url(r'^serial-numbers/', PartSerialNumberDetail.as_view(), name='api-part-serial-number-detail'), + + # Part detail endpoint + url(r'^.*$', PartDetail.as_view(), name='api-part-detail'), + ])), url(r'^.*$', PartList.as_view(), name='api-part-list'), ] diff --git a/InvenTree/stock/templates/stock/item_base.html b/InvenTree/stock/templates/stock/item_base.html index 0f8d81203a..111007cd71 100644 --- a/InvenTree/stock/templates/stock/item_base.html +++ b/InvenTree/stock/templates/stock/item_base.html @@ -433,6 +433,7 @@ $("#stock-serialize").click(function() { serializeStockItem({{ item.pk }}, { + part: {{ item.part.pk }}, reload: true, data: { quantity: {{ item.quantity }}, diff --git a/InvenTree/templates/js/translated/api.js b/InvenTree/templates/js/translated/api.js index 735ce0a676..d9c23f035f 100644 --- a/InvenTree/templates/js/translated/api.js +++ b/InvenTree/templates/js/translated/api.js @@ -54,6 +54,7 @@ function inventreeGet(url, filters={}, options={}) { data: filters, dataType: 'json', contentType: 'application/json', + async: (options.async == false) ? false : true, success: function(response) { if (options.success) { options.success(response); diff --git a/InvenTree/templates/js/translated/stock.js b/InvenTree/templates/js/translated/stock.js index c624278c93..e5a59d7767 100644 --- a/InvenTree/templates/js/translated/stock.js +++ b/InvenTree/templates/js/translated/stock.js @@ -80,6 +80,20 @@ function serializeStockItem(pk, options={}) { notes: {}, }; + if (options.part) { + // Work out the next available serial number + inventreeGet(`/api/part/${options.part}/serial-numbers/`, {}, { + success: function(data) { + if (data.next) { + options.fields.serial_numbers.placeholder = `{% trans "Next available serial number" %}: ${data.next}`; + } else if (data.latest) { + options.fields.serial_numbers.placeholder = `{% trans "Latest serial number" %}: ${data.latest}`; + } + }, + async: false, + }); + } + constructForm(url, options); } From ad6e34953b16d8c7983b2011f7477d2bcfcb787f Mon Sep 17 00:00:00 2001 From: Oliver Date: Sat, 27 Nov 2021 00:17:31 +1100 Subject: [PATCH 03/17] Update placeholders for serial numbers when creating a new stock item --- InvenTree/templates/js/translated/forms.js | 6 ++++++ InvenTree/templates/js/translated/stock.js | 18 +++++++++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/InvenTree/templates/js/translated/forms.js b/InvenTree/templates/js/translated/forms.js index fd1668cc77..5af85d382e 100644 --- a/InvenTree/templates/js/translated/forms.js +++ b/InvenTree/templates/js/translated/forms.js @@ -28,6 +28,7 @@ disableFormInput, enableFormInput, hideFormInput, + setFormInputPlaceholder, setFormGroupVisibility, showFormInput, */ @@ -1276,6 +1277,11 @@ function initializeGroups(fields, options) { } } +// Set the placeholder value for a field +function setFormInputPlaceholder(name, placeholder, options) { + $(options.modal).find(`#id_${name}`).attr('placeholder', placeholder); +} + // Clear a form input function clearFormInput(name, options) { updateFieldValue(name, null, {}, options); diff --git a/InvenTree/templates/js/translated/stock.js b/InvenTree/templates/js/translated/stock.js index e5a59d7767..5e92299f03 100644 --- a/InvenTree/templates/js/translated/stock.js +++ b/InvenTree/templates/js/translated/stock.js @@ -158,10 +158,26 @@ function stockItemFields(options={}) { // If a "trackable" part is selected, enable serial number field if (data.trackable) { enableFormInput('serial_numbers', opts); - // showFormInput('serial_numbers', opts); + + // Request part serial number information from the server + inventreeGet(`/api/part/${data.pk}/serial-numbers/`, {}, { + success: function(data) { + var placeholder = ''; + if (data.next) { + placeholder = `{% trans "Next available serial number" %}: ${data.next}`; + } else if (data.latest) { + placeholder = `{% trans "Latest serial number" %}: ${data.latest}`; + } + + setFormInputPlaceholder('serial_numbers', placeholder, opts); + } + }); + } else { clearFormInput('serial_numbers', opts); disableFormInput('serial_numbers', opts); + + setFormInputPlaceholder('serial_numbers', '{% trans "This part cannot be serialized" %}', opts); } // Enable / disable fields based on purchaseable status From db1a434f81524a98cf5cce2312f544db65e60116 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 26 Nov 2021 23:56:24 +0100 Subject: [PATCH 04/17] [FR] User sessions Fixes #2327 --- InvenTree/InvenTree/settings.py | 10 ++++- InvenTree/InvenTree/urls.py | 5 +++ InvenTree/InvenTree/views.py | 20 +++++++++ .../templates/InvenTree/settings/user.html | 42 +++++++++++++++++++ requirements.txt | 1 + 5 files changed, 76 insertions(+), 2 deletions(-) diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index 038d07600f..df84ba315e 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -257,7 +257,7 @@ INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', - 'django.contrib.sessions', + 'user_sessions', # db user sessions 'django.contrib.messages', 'django.contrib.staticfiles', 'django.contrib.sites', @@ -299,7 +299,7 @@ INSTALLED_APPS = [ MIDDLEWARE = CONFIG.get('middleware', [ 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', + 'user_sessions.middleware.SessionMiddleware', # db user sessions 'django.middleware.locale.LocaleMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', @@ -626,6 +626,12 @@ if _cache_host: # as well Q_CLUSTER["django_redis"] = "worker" +# database user sessions +SESSION_ENGINE = 'user_sessions.backends.db' +LOGOUT_REDIRECT_URL = 'index' +SILENCED_SYSTEM_CHECKS = [ + 'admin.E410', +] # Password validation # https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index 584403fc84..15ed8d5478 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -38,6 +38,7 @@ from rest_framework.documentation import include_docs_urls from .views import auth_request from .views import IndexView, SearchView, DatabaseStatsView from .views import SettingsView, EditUserView, SetPasswordView, CustomEmailView, CustomConnectionsView, CustomPasswordResetFromKeyView +from .views import CustomSessionDeleteView, CustomSessionDeleteOtherView from .views import CurrencyRefreshView from .views import AppearanceSelectView, SettingCategorySelectView from .views import DynamicJsView @@ -156,6 +157,10 @@ urlpatterns = [ url(r'^markdownx/', include('markdownx.urls')), + # DB user sessions + url(r'^accounts/sessions/other/delete/$', view=CustomSessionDeleteOtherView.as_view(), name='session_delete_other', ), + url(r'^accounts/sessions/(?P\w+)/delete/$', view=CustomSessionDeleteView.as_view(), name='session_delete', ), + # Single Sign On / allauth # overrides of urlpatterns url(r'^accounts/email/', CustomEmailView.as_view(), name='account_email'), diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py index 989fb1bc9d..a5c4da48b6 100644 --- a/InvenTree/InvenTree/views.py +++ b/InvenTree/InvenTree/views.py @@ -14,6 +14,7 @@ from django.utils.translation import gettext_lazy as _ from django.template.loader import render_to_string from django.http import HttpResponse, JsonResponse, HttpResponseRedirect from django.urls import reverse_lazy +from django.utils.timezone import now from django.shortcuts import redirect from django.conf import settings @@ -29,6 +30,7 @@ from allauth.socialaccount.forms import DisconnectForm from allauth.account.models import EmailAddress from allauth.account.views import EmailView, PasswordResetFromKeyView from allauth.socialaccount.views import ConnectionsView +from user_sessions.views import SessionDeleteView, SessionDeleteOtherView from common.settings import currency_code_default, currency_codes @@ -733,6 +735,10 @@ class SettingsView(TemplateView): ctx["request"] = self.request ctx['social_form'] = DisconnectForm(request=self.request) + # user db sessions + ctx['session_key'] = self.request.session.session_key + ctx['session_list'] = self.request.user.session_set.filter(expire_date__gt=now()).order_by('-last_activity') + return ctx @@ -766,6 +772,20 @@ class CustomPasswordResetFromKeyView(PasswordResetFromKeyView): success_url = reverse_lazy("account_login") +class UserSessionOverride(): + """overrides sucessurl to lead to settings""" + def get_success_url(self): + return str(reverse_lazy('settings')) + + +class CustomSessionDeleteView(UserSessionOverride, SessionDeleteView): + pass + + +class CustomSessionDeleteOtherView(UserSessionOverride, SessionDeleteOtherView): + pass + + class CurrencyRefreshView(RedirectView): """ POST endpoint to refresh / update exchange rates diff --git a/InvenTree/templates/InvenTree/settings/user.html b/InvenTree/templates/InvenTree/settings/user.html index a4c12a9bb0..43dee386be 100644 --- a/InvenTree/templates/InvenTree/settings/user.html +++ b/InvenTree/templates/InvenTree/settings/user.html @@ -4,6 +4,7 @@ {% load inventree_extras %} {% load socialaccount %} {% load crispy_forms_tags %} +{% load user_sessions i18n %} {% block label %}account{% endblock %} @@ -173,6 +174,47 @@ +
+

{% trans "Active Sessions" %}

+
+ +
+ {% trans "unknown on unknown" as unknown_on_unknown %} + {% trans "unknown" as unknown %} + + + + + + + + + {% for object in session_list %} + + + + + + {% endfor %} +
{% trans "IP Address" %}{% trans "Device" %}{% trans "Last Activity" %}
{{ object.ip }}{{ object.user_agent|device|default_if_none:unknown_on_unknown|safe }} + {% if object.session_key == session_key %} + {% blocktrans with time=object.last_activity|timesince %}{{ time }} ago (this session){% endblocktrans %} + {% else %} + {% blocktrans with time=object.last_activity|timesince %}{{ time }} ago{% endblocktrans %} + {% endif %} +
+ + {% if session_list.count > 1 %} +
+ {% csrf_token %} +

{% blocktrans %}You can also end all other sessions but the current. + This will log you out on all other devices.{% endblocktrans %} +

+
+ {% endif %} +
+ +

{% trans "Language Settings" %}

diff --git a/requirements.txt b/requirements.txt index 139942fd80..5f5695ed3d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -23,6 +23,7 @@ django-q==1.3.4 # Background task scheduling django-sql-utils==0.5.0 # Advanced query annotation / aggregation django-stdimage==5.1.1 # Advanced ImageField management django-test-migrations==1.1.0 # Unit testing for database migrations +django-user-sessions==1.7.1 # user sessions in DB django-weasyprint==1.0.1 # django weasyprint integration djangorestframework==3.12.4 # DRF framework flake8==3.8.3 # PEP checking From 84f0966e4fd8a3c5d6a6850e9441bef6bb69e2d5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 27 Nov 2021 00:02:39 +0100 Subject: [PATCH 05/17] movelanguage to display settings --- .../templates/InvenTree/settings/user.html | 55 ------------------- .../InvenTree/settings/user_display.html | 53 ++++++++++++++++++ 2 files changed, 53 insertions(+), 55 deletions(-) diff --git a/InvenTree/templates/InvenTree/settings/user.html b/InvenTree/templates/InvenTree/settings/user.html index 43dee386be..97ed4805c9 100644 --- a/InvenTree/templates/InvenTree/settings/user.html +++ b/InvenTree/templates/InvenTree/settings/user.html @@ -213,61 +213,6 @@ {% endif %} - - -
-

{% trans "Language Settings" %}

-
- -
-
-
- {% csrf_token %} - - -
- -
- -
-
-

{% trans "Some languages are not complete" %} - {% if ALL_LANG %} - . {% trans "Show only sufficent" %} - {% else %} - {% trans "and hidden." %} {% trans "Show them too" %} - {% endif %} -

-
-
-
-

{% trans "Help the translation efforts!" %}

-

{% blocktrans with link="https://crowdin.com/project/inventree" %}Native language translation of the InvenTree web application is community contributed via crowdin. Contributions are welcomed and encouraged.{% endblocktrans %}

-
-
- {% endblock %} {% block js_ready %} diff --git a/InvenTree/templates/InvenTree/settings/user_display.html b/InvenTree/templates/InvenTree/settings/user_display.html index ae7843df9c..9f5c22f991 100644 --- a/InvenTree/templates/InvenTree/settings/user_display.html +++ b/InvenTree/templates/InvenTree/settings/user_display.html @@ -50,4 +50,57 @@ +
+

{% trans "Language Settings" %}

+
+ +
+
+
+ {% csrf_token %} + + +
+ +
+ +
+
+

{% trans "Some languages are not complete" %} + {% if ALL_LANG %} + . {% trans "Show only sufficent" %} + {% else %} + {% trans "and hidden." %} {% trans "Show them too" %} + {% endif %} +

+
+
+
+

{% trans "Help the translation efforts!" %}

+

{% blocktrans with link="https://crowdin.com/project/inventree" %}Native language translation of the InvenTree web application is community contributed via crowdin. Contributions are welcomed and encouraged.{% endblocktrans %}

+
+
+ {% endblock %} \ No newline at end of file From 6511a774d141d54c7e83854abef778e9f0cc5eb7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 27 Nov 2021 00:04:43 +0100 Subject: [PATCH 06/17] exclude sessions from imort / exports --- tasks.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tasks.py b/tasks.py index 21f7616d76..a8f9bcaf84 100644 --- a/tasks.py +++ b/tasks.py @@ -291,6 +291,7 @@ def content_excludes(): "exchange.rate", "exchange.exchangebackend", "common.notificationentry", + "sessions.session", ] output = "" From e9ed7bffc974cd988614c79867320b8c7b8fb81d Mon Sep 17 00:00:00 2001 From: Oliver Date: Sun, 28 Nov 2021 12:53:01 +1100 Subject: [PATCH 07/17] adds a "link" field to the InvenTree attachment model - Allows attachments to also serve as links to external files --- InvenTree/InvenTree/models.py | 15 ++++++-- .../migrations/0033_auto_20211128_0151.py | 25 +++++++++++++ .../migrations/0053_auto_20211128_0151.py | 35 +++++++++++++++++++ .../migrations/0075_auto_20211128_0151.py | 25 +++++++++++++ .../migrations/0070_auto_20211128_0151.py | 25 +++++++++++++ 5 files changed, 123 insertions(+), 2 deletions(-) create mode 100644 InvenTree/build/migrations/0033_auto_20211128_0151.py create mode 100644 InvenTree/order/migrations/0053_auto_20211128_0151.py create mode 100644 InvenTree/part/migrations/0075_auto_20211128_0151.py create mode 100644 InvenTree/stock/migrations/0070_auto_20211128_0151.py diff --git a/InvenTree/InvenTree/models.py b/InvenTree/InvenTree/models.py index 0f8350f84f..4e7ed89e63 100644 --- a/InvenTree/InvenTree/models.py +++ b/InvenTree/InvenTree/models.py @@ -21,7 +21,8 @@ from django.dispatch import receiver from mptt.models import MPTTModel, TreeForeignKey from mptt.exceptions import InvalidMove -from .validators import validate_tree_name +from InvenTree.fields import InvenTreeURLField +from InvenTree.validators import validate_tree_name logger = logging.getLogger('inventree') @@ -89,6 +90,8 @@ class ReferenceIndexingMixin(models.Model): class InvenTreeAttachment(models.Model): """ Provides an abstracted class for managing file attachments. + An attachment can be either an uploaded file, or an external URL + Attributes: attachment: File comment: String descriptor for the attachment @@ -107,7 +110,15 @@ class InvenTreeAttachment(models.Model): return os.path.basename(self.attachment.name) attachment = models.FileField(upload_to=rename_attachment, verbose_name=_('Attachment'), - help_text=_('Select file to attach')) + help_text=_('Select file to attach'), + blank=True, null=True + ) + + link = InvenTreeURLField( + blank=True, null=True, + verbose_name=_('Link'), + help_text=_('Link to external URL') + ) comment = models.CharField(blank=True, max_length=100, verbose_name=_('Comment'), help_text=_('File comment')) diff --git a/InvenTree/build/migrations/0033_auto_20211128_0151.py b/InvenTree/build/migrations/0033_auto_20211128_0151.py new file mode 100644 index 0000000000..db8df848ce --- /dev/null +++ b/InvenTree/build/migrations/0033_auto_20211128_0151.py @@ -0,0 +1,25 @@ +# Generated by Django 3.2.5 on 2021-11-28 01:51 + +import InvenTree.fields +import InvenTree.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('build', '0032_auto_20211014_0632'), + ] + + operations = [ + migrations.AddField( + model_name='buildorderattachment', + name='link', + field=InvenTree.fields.InvenTreeURLField(blank=True, help_text='Link to external URL', null=True, verbose_name='Link'), + ), + migrations.AlterField( + model_name='buildorderattachment', + name='attachment', + field=models.FileField(blank=True, help_text='Select file to attach', null=True, upload_to=InvenTree.models.rename_attachment, verbose_name='Attachment'), + ), + ] diff --git a/InvenTree/order/migrations/0053_auto_20211128_0151.py b/InvenTree/order/migrations/0053_auto_20211128_0151.py new file mode 100644 index 0000000000..bbe029b4af --- /dev/null +++ b/InvenTree/order/migrations/0053_auto_20211128_0151.py @@ -0,0 +1,35 @@ +# Generated by Django 3.2.5 on 2021-11-28 01:51 + +import InvenTree.fields +import InvenTree.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('order', '0052_auto_20211014_0631'), + ] + + operations = [ + migrations.AddField( + model_name='purchaseorderattachment', + name='link', + field=InvenTree.fields.InvenTreeURLField(blank=True, help_text='Link to external URL', null=True, verbose_name='Link'), + ), + migrations.AddField( + model_name='salesorderattachment', + name='link', + field=InvenTree.fields.InvenTreeURLField(blank=True, help_text='Link to external URL', null=True, verbose_name='Link'), + ), + migrations.AlterField( + model_name='purchaseorderattachment', + name='attachment', + field=models.FileField(blank=True, help_text='Select file to attach', null=True, upload_to=InvenTree.models.rename_attachment, verbose_name='Attachment'), + ), + migrations.AlterField( + model_name='salesorderattachment', + name='attachment', + field=models.FileField(blank=True, help_text='Select file to attach', null=True, upload_to=InvenTree.models.rename_attachment, verbose_name='Attachment'), + ), + ] diff --git a/InvenTree/part/migrations/0075_auto_20211128_0151.py b/InvenTree/part/migrations/0075_auto_20211128_0151.py new file mode 100644 index 0000000000..d484a7adce --- /dev/null +++ b/InvenTree/part/migrations/0075_auto_20211128_0151.py @@ -0,0 +1,25 @@ +# Generated by Django 3.2.5 on 2021-11-28 01:51 + +import InvenTree.fields +import InvenTree.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('part', '0074_partcategorystar'), + ] + + operations = [ + migrations.AddField( + model_name='partattachment', + name='link', + field=InvenTree.fields.InvenTreeURLField(blank=True, help_text='Link to external URL', null=True, verbose_name='Link'), + ), + migrations.AlterField( + model_name='partattachment', + name='attachment', + field=models.FileField(blank=True, help_text='Select file to attach', null=True, upload_to=InvenTree.models.rename_attachment, verbose_name='Attachment'), + ), + ] diff --git a/InvenTree/stock/migrations/0070_auto_20211128_0151.py b/InvenTree/stock/migrations/0070_auto_20211128_0151.py new file mode 100644 index 0000000000..a2f6ef322d --- /dev/null +++ b/InvenTree/stock/migrations/0070_auto_20211128_0151.py @@ -0,0 +1,25 @@ +# Generated by Django 3.2.5 on 2021-11-28 01:51 + +import InvenTree.fields +import InvenTree.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('stock', '0069_auto_20211109_2347'), + ] + + operations = [ + migrations.AddField( + model_name='stockitemattachment', + name='link', + field=InvenTree.fields.InvenTreeURLField(blank=True, help_text='Link to external URL', null=True, verbose_name='Link'), + ), + migrations.AlterField( + model_name='stockitemattachment', + name='attachment', + field=models.FileField(blank=True, help_text='Select file to attach', null=True, upload_to=InvenTree.models.rename_attachment, verbose_name='Attachment'), + ), + ] From 6582fd3d04392bc301816fe514cbb64e0915b061 Mon Sep 17 00:00:00 2001 From: Oliver Date: Sun, 28 Nov 2021 12:57:37 +1100 Subject: [PATCH 08/17] Add 'link' field to attachment serializer --- InvenTree/build/serializers.py | 1 + InvenTree/order/serializers.py | 2 ++ InvenTree/part/serializers.py | 1 + InvenTree/stock/serializers.py | 1 + 4 files changed, 5 insertions(+) diff --git a/InvenTree/build/serializers.py b/InvenTree/build/serializers.py index 4d3680ea98..12f57f980a 100644 --- a/InvenTree/build/serializers.py +++ b/InvenTree/build/serializers.py @@ -525,6 +525,7 @@ class BuildAttachmentSerializer(InvenTreeAttachmentSerializer): 'pk', 'build', 'attachment', + 'link', 'filename', 'comment', 'upload_date', diff --git a/InvenTree/order/serializers.py b/InvenTree/order/serializers.py index 983cd01a63..a3674afb59 100644 --- a/InvenTree/order/serializers.py +++ b/InvenTree/order/serializers.py @@ -386,6 +386,7 @@ class POAttachmentSerializer(InvenTreeAttachmentSerializer): 'pk', 'order', 'attachment', + 'link', 'filename', 'comment', 'upload_date', @@ -607,6 +608,7 @@ class SOAttachmentSerializer(InvenTreeAttachmentSerializer): 'order', 'attachment', 'filename', + 'link', 'comment', 'upload_date', ] diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index 388faf1ca2..189665fb32 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -85,6 +85,7 @@ class PartAttachmentSerializer(InvenTreeAttachmentSerializer): 'part', 'attachment', 'filename', + 'link', 'comment', 'upload_date', ] diff --git a/InvenTree/stock/serializers.py b/InvenTree/stock/serializers.py index 840eb4793e..39ebd13acb 100644 --- a/InvenTree/stock/serializers.py +++ b/InvenTree/stock/serializers.py @@ -432,6 +432,7 @@ class StockItemAttachmentSerializer(InvenTree.serializers.InvenTreeAttachmentSer 'stock_item', 'attachment', 'filename', + 'link', 'comment', 'upload_date', 'user', From 0949bac175a1763404c59f688a677f2655b03dc3 Mon Sep 17 00:00:00 2001 From: Oliver Date: Sun, 28 Nov 2021 14:21:09 +1100 Subject: [PATCH 09/17] serializer fixes --- InvenTree/InvenTree/models.py | 10 ++++-- InvenTree/InvenTree/serializers.py | 56 +++++++++++++++++++++--------- InvenTree/build/serializers.py | 2 -- InvenTree/order/serializers.py | 4 --- InvenTree/part/serializers.py | 2 -- InvenTree/stock/serializers.py | 2 -- 6 files changed, 48 insertions(+), 28 deletions(-) diff --git a/InvenTree/InvenTree/models.py b/InvenTree/InvenTree/models.py index 4e7ed89e63..52c5384b63 100644 --- a/InvenTree/InvenTree/models.py +++ b/InvenTree/InvenTree/models.py @@ -107,7 +107,10 @@ class InvenTreeAttachment(models.Model): return "attachments" def __str__(self): - return os.path.basename(self.attachment.name) + if self.attachment is not None: + return os.path.basename(self.attachment.name) + else: + return str(self.link) attachment = models.FileField(upload_to=rename_attachment, verbose_name=_('Attachment'), help_text=_('Select file to attach'), @@ -134,7 +137,10 @@ class InvenTreeAttachment(models.Model): @property def basename(self): - return os.path.basename(self.attachment.name) + if self.attachment: + return os.path.basename(self.attachment.name) + else: + return None @basename.setter def basename(self, fn): diff --git a/InvenTree/InvenTree/serializers.py b/InvenTree/InvenTree/serializers.py index 90cc857cdd..c236bad603 100644 --- a/InvenTree/InvenTree/serializers.py +++ b/InvenTree/InvenTree/serializers.py @@ -239,22 +239,6 @@ class InvenTreeModelSerializer(serializers.ModelSerializer): return data -class InvenTreeAttachmentSerializer(InvenTreeModelSerializer): - """ - Special case of an InvenTreeModelSerializer, which handles an "attachment" model. - - The only real addition here is that we support "renaming" of the attachment file. - """ - - # The 'filename' field must be present in the serializer - filename = serializers.CharField( - label=_('Filename'), - required=False, - source='basename', - allow_blank=False, - ) - - class InvenTreeAttachmentSerializerField(serializers.FileField): """ Override the DRF native FileField serializer, @@ -284,6 +268,46 @@ class InvenTreeAttachmentSerializerField(serializers.FileField): return os.path.join(str(settings.MEDIA_URL), str(value)) +class InvenTreeAttachmentSerializer(InvenTreeModelSerializer): + """ + Special case of an InvenTreeModelSerializer, which handles an "attachment" model. + + The only real addition here is that we support "renaming" of the attachment file. + """ + + def validate(self, data): + """ + Validation for an attachment. Either a file or external link must be provided + """ + + data = super().validate(data) + + attachment = data.get('attachment', None) + link = data.get('link', None) + + if not attachment and not link: + raise ValidationError({ + 'attachment': _('Missing file'), + 'link': _('Missing external link'), + }) + + return data + + attachment = InvenTreeAttachmentSerializerField( + required=False, + allow_null=False, + ) + + # The 'filename' field must be present in the serializer + filename = serializers.CharField( + label=_('Filename'), + required=False, + source='basename', + allow_blank=False, + ) + + + class InvenTreeImageSerializerField(serializers.ImageField): """ Custom image serializer. diff --git a/InvenTree/build/serializers.py b/InvenTree/build/serializers.py index 12f57f980a..338e4a38b3 100644 --- a/InvenTree/build/serializers.py +++ b/InvenTree/build/serializers.py @@ -516,8 +516,6 @@ class BuildAttachmentSerializer(InvenTreeAttachmentSerializer): Serializer for a BuildAttachment """ - attachment = InvenTreeAttachmentSerializerField(required=True) - class Meta: model = BuildOrderAttachment diff --git a/InvenTree/order/serializers.py b/InvenTree/order/serializers.py index a3674afb59..c1356d78c9 100644 --- a/InvenTree/order/serializers.py +++ b/InvenTree/order/serializers.py @@ -377,8 +377,6 @@ class POAttachmentSerializer(InvenTreeAttachmentSerializer): Serializers for the PurchaseOrderAttachment model """ - attachment = InvenTreeAttachmentSerializerField(required=True) - class Meta: model = PurchaseOrderAttachment @@ -598,8 +596,6 @@ class SOAttachmentSerializer(InvenTreeAttachmentSerializer): Serializers for the SalesOrderAttachment model """ - attachment = InvenTreeAttachmentSerializerField(required=True) - class Meta: model = SalesOrderAttachment diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index 189665fb32..1be81c16ba 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -75,8 +75,6 @@ class PartAttachmentSerializer(InvenTreeAttachmentSerializer): Serializer for the PartAttachment class """ - attachment = InvenTreeAttachmentSerializerField(required=True) - class Meta: model = PartAttachment diff --git a/InvenTree/stock/serializers.py b/InvenTree/stock/serializers.py index 39ebd13acb..c74d674275 100644 --- a/InvenTree/stock/serializers.py +++ b/InvenTree/stock/serializers.py @@ -420,8 +420,6 @@ class StockItemAttachmentSerializer(InvenTree.serializers.InvenTreeAttachmentSer user_detail = InvenTree.serializers.UserSerializerBrief(source='user', read_only=True) - attachment = InvenTree.serializers.InvenTreeAttachmentSerializerField(required=True) - # TODO: Record the uploading user when creating or updating an attachment! class Meta: From 4ee55847f12030e5179fd6bd517b4164fefa34a2 Mon Sep 17 00:00:00 2001 From: Oliver Date: Sun, 28 Nov 2021 14:21:20 +1100 Subject: [PATCH 10/17] Refactor attachment tables --- InvenTree/build/templates/build/detail.html | 54 ++------ .../order/purchase_order_detail.html | 51 ++----- .../templates/order/sales_order_detail.html | 52 ++----- InvenTree/part/templates/part/detail.html | 57 ++------ InvenTree/stock/templates/stock/item.html | 55 ++------ InvenTree/templates/attachment_button.html | 3 + .../templates/js/translated/attachment.js | 131 +++++++++++++----- 7 files changed, 143 insertions(+), 260 deletions(-) diff --git a/InvenTree/build/templates/build/detail.html b/InvenTree/build/templates/build/detail.html index 31e9f38080..8479c2819f 100644 --- a/InvenTree/build/templates/build/detail.html +++ b/InvenTree/build/templates/build/detail.html @@ -431,53 +431,17 @@ enableDragAndDrop( } ); -// Callback for creating a new attachment -$('#new-attachment').click(function() { - - constructForm('{% url "api-build-attachment-list" %}', { - fields: { - attachment: {}, - comment: {}, - build: { - value: {{ build.pk }}, - hidden: true, - } - }, - method: 'POST', - onSuccess: reloadAttachmentTable, - title: '{% trans "Add Attachment" %}', - }); -}); - -loadAttachmentTable( - '{% url "api-build-attachment-list" %}', - { - filters: { - build: {{ build.pk }}, - }, - onEdit: function(pk) { - var url = `/api/build/attachment/${pk}/`; - - constructForm(url, { - fields: { - filename: {}, - comment: {}, - }, - onSuccess: reloadAttachmentTable, - title: '{% trans "Edit Attachment" %}', - }); - }, - onDelete: function(pk) { - - constructForm(`/api/build/attachment/${pk}/`, { - method: 'DELETE', - confirmMessage: '{% trans "Confirm Delete Operation" %}', - title: '{% trans "Delete Attachment" %}', - onSuccess: reloadAttachmentTable, - }); +loadAttachmentTable('{% url "api-build-attachment-list" %}', { + filters: { + build: {{ build.pk }}, + }, + fields: { + build: { + value: {{ build.pk }}, + hidden: true, } } -); +}); $('#edit-notes').click(function() { constructForm('{% url "api-build-detail" build.pk %}', { diff --git a/InvenTree/order/templates/order/purchase_order_detail.html b/InvenTree/order/templates/order/purchase_order_detail.html index 257707347a..3a6ea090d5 100644 --- a/InvenTree/order/templates/order/purchase_order_detail.html +++ b/InvenTree/order/templates/order/purchase_order_detail.html @@ -124,51 +124,16 @@ } ); - loadAttachmentTable( - '{% url "api-po-attachment-list" %}', - { - filters: { - order: {{ order.pk }}, - }, - onEdit: function(pk) { - var url = `/api/order/po/attachment/${pk}/`; - - constructForm(url, { - fields: { - filename: {}, - comment: {}, - }, - onSuccess: reloadAttachmentTable, - title: '{% trans "Edit Attachment" %}', - }); - }, - onDelete: function(pk) { - - constructForm(`/api/order/po/attachment/${pk}/`, { - method: 'DELETE', - confirmMessage: '{% trans "Confirm Delete Operation" %}', - title: '{% trans "Delete Attachment" %}', - onSuccess: reloadAttachmentTable, - }); + loadAttachmentTable('{% url "api-po-attachment-list" %}', { + filters: { + order: {{ order.pk }}, + }, + fields: { + order: { + value: {{ order.pk }}, + hidden: true, } } - ); - - $("#new-attachment").click(function() { - - constructForm('{% url "api-po-attachment-list" %}', { - method: 'POST', - fields: { - attachment: {}, - comment: {}, - order: { - value: {{ order.pk }}, - hidden: true, - }, - }, - reload: true, - title: '{% trans "Add Attachment" %}', - }); }); loadStockTable($("#stock-table"), { diff --git a/InvenTree/order/templates/order/sales_order_detail.html b/InvenTree/order/templates/order/sales_order_detail.html index 887ecd390c..1cf2ce06cc 100644 --- a/InvenTree/order/templates/order/sales_order_detail.html +++ b/InvenTree/order/templates/order/sales_order_detail.html @@ -110,55 +110,21 @@ }, label: 'attachment', success: function(data, status, xhr) { - location.reload(); + reloadAttachmentTable(); } } ); - loadAttachmentTable( - '{% url "api-so-attachment-list" %}', - { - filters: { - order: {{ order.pk }}, + loadAttachmentTable('{% url "api-so-attachment-list" %}', { + filters: { + order: {{ order.pk }}, + }, + fields: { + order: { + value: {{ order.pk }}, + hidden: true, }, - onEdit: function(pk) { - var url = `/api/order/so/attachment/${pk}/`; - - constructForm(url, { - fields: { - filename: {}, - comment: {}, - }, - onSuccess: reloadAttachmentTable, - title: '{% trans "Edit Attachment" %}', - }); - }, - onDelete: function(pk) { - constructForm(`/api/order/so/attachment/${pk}/`, { - method: 'DELETE', - confirmMessage: '{% trans "Confirm Delete Operation" %}', - title: '{% trans "Delete Attachment" %}', - onSuccess: reloadAttachmentTable, - }); - } } - ); - - $("#new-attachment").click(function() { - - constructForm('{% url "api-so-attachment-list" %}', { - method: 'POST', - fields: { - attachment: {}, - comment: {}, - order: { - value: {{ order.pk }}, - hidden: true - } - }, - onSuccess: reloadAttachmentTable, - title: '{% trans "Add Attachment" %}' - }); }); loadBuildTable($("#builds-table"), { diff --git a/InvenTree/part/templates/part/detail.html b/InvenTree/part/templates/part/detail.html index a737bfa6fc..4cf6f5e824 100644 --- a/InvenTree/part/templates/part/detail.html +++ b/InvenTree/part/templates/part/detail.html @@ -999,36 +999,17 @@ }); onPanelLoad("part-attachments", function() { - loadAttachmentTable( - '{% url "api-part-attachment-list" %}', - { - filters: { - part: {{ part.pk }}, - }, - onEdit: function(pk) { - var url = `/api/part/attachment/${pk}/`; - - constructForm(url, { - fields: { - filename: {}, - comment: {}, - }, - title: '{% trans "Edit Attachment" %}', - onSuccess: reloadAttachmentTable, - }); - }, - onDelete: function(pk) { - var url = `/api/part/attachment/${pk}/`; - - constructForm(url, { - method: 'DELETE', - confirmMessage: '{% trans "Confirm Delete Operation" %}', - title: '{% trans "Delete Attachment" %}', - onSuccess: reloadAttachmentTable, - }); + loadAttachmentTable('{% url "api-part-attachment-list" %}', { + filters: { + part: {{ part.pk }}, + }, + fields: { + part: { + value: {{ part.pk }}, + hidden: true } } - ); + }); enableDragAndDrop( '#attachment-dropzone', @@ -1043,26 +1024,6 @@ } } ); - - $("#new-attachment").click(function() { - - constructForm( - '{% url "api-part-attachment-list" %}', - { - method: 'POST', - fields: { - attachment: {}, - comment: {}, - part: { - value: {{ part.pk }}, - hidden: true, - } - }, - onSuccess: reloadAttachmentTable, - title: '{% trans "Add Attachment" %}', - } - ) - }); }); diff --git a/InvenTree/stock/templates/stock/item.html b/InvenTree/stock/templates/stock/item.html index 9bafc2633c..9cc6d85aeb 100644 --- a/InvenTree/stock/templates/stock/item.html +++ b/InvenTree/stock/templates/stock/item.html @@ -221,55 +221,16 @@ } ); - loadAttachmentTable( - '{% url "api-stock-attachment-list" %}', - { - filters: { - stock_item: {{ item.pk }}, - }, - onEdit: function(pk) { - var url = `/api/stock/attachment/${pk}/`; - - constructForm(url, { - fields: { - filename: {}, - comment: {}, - }, - title: '{% trans "Edit Attachment" %}', - onSuccess: reloadAttachmentTable - }); - }, - onDelete: function(pk) { - var url = `/api/stock/attachment/${pk}/`; - - constructForm(url, { - method: 'DELETE', - confirmMessage: '{% trans "Confirm Delete Operation" %}', - title: '{% trans "Delete Attachment" %}', - onSuccess: reloadAttachmentTable, - }); + loadAttachmentTable('{% url "api-stock-attachment-list" %}', { + filters: { + stock_item: {{ item.pk }}, + }, + fields: { + stock_item: { + value: {{ item.pk }}, + hidden: true, } } - ); - - $("#new-attachment").click(function() { - - constructForm( - '{% url "api-stock-attachment-list" %}', - { - method: 'POST', - fields: { - attachment: {}, - comment: {}, - stock_item: { - value: {{ item.pk }}, - hidden: true, - }, - }, - reload: true, - title: '{% trans "Add Attachment" %}', - } - ); }); loadStockTestResultsTable( diff --git a/InvenTree/templates/attachment_button.html b/InvenTree/templates/attachment_button.html index e1561010c0..d220f4829d 100644 --- a/InvenTree/templates/attachment_button.html +++ b/InvenTree/templates/attachment_button.html @@ -1,5 +1,8 @@ {% load i18n %} + \ No newline at end of file diff --git a/InvenTree/templates/js/translated/attachment.js b/InvenTree/templates/js/translated/attachment.js index 5ff5786588..87f2fbdcc0 100644 --- a/InvenTree/templates/js/translated/attachment.js +++ b/InvenTree/templates/js/translated/attachment.js @@ -6,10 +6,57 @@ */ /* exported + addAttachmentButtonCallbacks, loadAttachmentTable, reloadAttachmentTable, */ + +/* + * Add callbacks to buttons for creating new attachments. + * + * Note: Attachments can also be external links! + */ +function addAttachmentButtonCallbacks(url, fields={}) { + + // Callback for 'new attachment' button + $('#new-attachment').click(function() { + + var file_fields = { + attachment: {}, + comment: {}, + }; + + Object.assign(file_fields, fields); + + constructForm(url, { + fields: file_fields, + method: 'POST', + onSuccess: reloadAttachmentTable, + title: '{% trans "Add Attachment" %}', + }); + }); + + // Callback for 'new link' button + $('#new-attachment-link').click(function() { + + var link_fields = { + link: {}, + comment: {}, + }; + + Object.assign(link_fields, fields); + + constructForm(url, { + fields: link_fields, + method: 'POST', + onSuccess: reloadAttachmentTable, + title: '{% trans "Add Link" %}', + }); + }); +} + + function reloadAttachmentTable() { $('#attachment-table').bootstrapTable('refresh'); @@ -20,6 +67,8 @@ function loadAttachmentTable(url, options) { var table = options.table || '#attachment-table'; + addAttachmentButtonCallbacks(url, options.fields || {}); + $(table).inventreeTable({ url: url, name: options.name || 'attachments', @@ -34,56 +83,70 @@ function loadAttachmentTable(url, options) { $(table).find('.button-attachment-edit').click(function() { var pk = $(this).attr('pk'); - if (options.onEdit) { - options.onEdit(pk); - } + constructForm(`${url}${pk}/`, { + fields: { + link: {}, + comment: {}, + }, + onSuccess: reloadAttachmentTable, + title: '{% trans "Edit Attachment" %}', + }); }); // Add callback for 'delete' button $(table).find('.button-attachment-delete').click(function() { var pk = $(this).attr('pk'); - if (options.onDelete) { - options.onDelete(pk); - } + constructForm(`${url}${pk}/`, { + method: 'DELETE', + confirmMessage: '{% trans "Confirm Delete" %}', + title: '{% trans "Delete Attachment" %}', + onSuccess: reloadAttachmentTable, + }); }); }, columns: [ { field: 'attachment', - title: '{% trans "File" %}', - formatter: function(value) { + title: '{% trans "Attachment" %}', + formatter: function(value, row) { - var icon = 'fa-file-alt'; + if (row.attachment) { + var icon = 'fa-file-alt'; - var fn = value.toLowerCase(); + var fn = value.toLowerCase(); - if (fn.endsWith('.csv')) { - icon = 'fa-file-csv'; - } else if (fn.endsWith('.pdf')) { - icon = 'fa-file-pdf'; - } else if (fn.endsWith('.xls') || fn.endsWith('.xlsx')) { - icon = 'fa-file-excel'; - } else if (fn.endsWith('.doc') || fn.endsWith('.docx')) { - icon = 'fa-file-word'; - } else if (fn.endsWith('.zip') || fn.endsWith('.7z')) { - icon = 'fa-file-archive'; + if (fn.endsWith('.csv')) { + icon = 'fa-file-csv'; + } else if (fn.endsWith('.pdf')) { + icon = 'fa-file-pdf'; + } else if (fn.endsWith('.xls') || fn.endsWith('.xlsx')) { + icon = 'fa-file-excel'; + } else if (fn.endsWith('.doc') || fn.endsWith('.docx')) { + icon = 'fa-file-word'; + } else if (fn.endsWith('.zip') || fn.endsWith('.7z')) { + icon = 'fa-file-archive'; + } else { + var images = ['.png', '.jpg', '.bmp', '.gif', '.svg', '.tif']; + + images.forEach(function(suffix) { + if (fn.endsWith(suffix)) { + icon = 'fa-file-image'; + } + }); + } + + var split = value.split('/'); + var filename = split[split.length - 1]; + + var html = ` ${filename}`; + + return renderLink(html, value); + } else if (row.link) { + return renderLink(row.link, row.link); } else { - var images = ['.png', '.jpg', '.bmp', '.gif', '.svg', '.tif']; - - images.forEach(function(suffix) { - if (fn.endsWith(suffix)) { - icon = 'fa-file-image'; - } - }); + return '-'; } - - var split = value.split('/'); - var filename = split[split.length - 1]; - - var html = ` ${filename}`; - - return renderLink(html, value); } }, { From 78309c19156609b4bb3462333231155d7b60755b Mon Sep 17 00:00:00 2001 From: Oliver Date: Sun, 28 Nov 2021 22:29:01 +1100 Subject: [PATCH 11/17] Move validation to the model class --- InvenTree/InvenTree/models.py | 11 +++++++++++ InvenTree/InvenTree/serializers.py | 18 ------------------ .../templates/js/translated/attachment.js | 6 ++++++ 3 files changed, 17 insertions(+), 18 deletions(-) diff --git a/InvenTree/InvenTree/models.py b/InvenTree/InvenTree/models.py index 52c5384b63..1b130cf599 100644 --- a/InvenTree/InvenTree/models.py +++ b/InvenTree/InvenTree/models.py @@ -98,6 +98,7 @@ class InvenTreeAttachment(models.Model): user: User associated with file upload upload_date: Date the file was uploaded """ + def getSubdir(self): """ Return the subdirectory under which attachments should be stored. @@ -106,6 +107,16 @@ class InvenTreeAttachment(models.Model): return "attachments" + def save(self, *args, **kwargs): + # Either 'attachment' or 'link' must be specified! + if not self.attachment and not self.link: + raise ValidationError({ + 'attachment': _('Missing file'), + 'link': _('Missing external link'), + }) + + super().save(*args, **kwargs) + def __str__(self): if self.attachment is not None: return os.path.basename(self.attachment.name) diff --git a/InvenTree/InvenTree/serializers.py b/InvenTree/InvenTree/serializers.py index c236bad603..36d344c867 100644 --- a/InvenTree/InvenTree/serializers.py +++ b/InvenTree/InvenTree/serializers.py @@ -275,24 +275,6 @@ class InvenTreeAttachmentSerializer(InvenTreeModelSerializer): The only real addition here is that we support "renaming" of the attachment file. """ - def validate(self, data): - """ - Validation for an attachment. Either a file or external link must be provided - """ - - data = super().validate(data) - - attachment = data.get('attachment', None) - link = data.get('link', None) - - if not attachment and not link: - raise ValidationError({ - 'attachment': _('Missing file'), - 'link': _('Missing external link'), - }) - - return data - attachment = InvenTreeAttachmentSerializerField( required=False, allow_null=False, diff --git a/InvenTree/templates/js/translated/attachment.js b/InvenTree/templates/js/translated/attachment.js index 87f2fbdcc0..fe8a0580c1 100644 --- a/InvenTree/templates/js/translated/attachment.js +++ b/InvenTree/templates/js/translated/attachment.js @@ -88,6 +88,12 @@ function loadAttachmentTable(url, options) { link: {}, comment: {}, }, + processResults: function(data, fields, opts) { + // Remove the "link" field if the attachment is a file! + if (data.attachment) { + delete opts.fields.link; + } + }, onSuccess: reloadAttachmentTable, title: '{% trans "Edit Attachment" %}', }); From 32be39774ff75e2c011f7f9a0b85a12950d0d994 Mon Sep 17 00:00:00 2001 From: Oliver Date: Sun, 28 Nov 2021 22:36:47 +1100 Subject: [PATCH 12/17] PEP fixes --- InvenTree/InvenTree/models.py | 2 +- InvenTree/InvenTree/serializers.py | 1 - InvenTree/build/serializers.py | 2 +- InvenTree/order/serializers.py | 1 - InvenTree/templates/js/translated/attachment.js | 3 ++- 5 files changed, 4 insertions(+), 5 deletions(-) diff --git a/InvenTree/InvenTree/models.py b/InvenTree/InvenTree/models.py index 1b130cf599..0448a69781 100644 --- a/InvenTree/InvenTree/models.py +++ b/InvenTree/InvenTree/models.py @@ -116,7 +116,7 @@ class InvenTreeAttachment(models.Model): }) super().save(*args, **kwargs) - + def __str__(self): if self.attachment is not None: return os.path.basename(self.attachment.name) diff --git a/InvenTree/InvenTree/serializers.py b/InvenTree/InvenTree/serializers.py index 36d344c867..3785cfb292 100644 --- a/InvenTree/InvenTree/serializers.py +++ b/InvenTree/InvenTree/serializers.py @@ -289,7 +289,6 @@ class InvenTreeAttachmentSerializer(InvenTreeModelSerializer): ) - class InvenTreeImageSerializerField(serializers.ImageField): """ Custom image serializer. diff --git a/InvenTree/build/serializers.py b/InvenTree/build/serializers.py index 338e4a38b3..35a3bb3baa 100644 --- a/InvenTree/build/serializers.py +++ b/InvenTree/build/serializers.py @@ -16,7 +16,7 @@ from rest_framework import serializers from rest_framework.serializers import ValidationError from InvenTree.serializers import InvenTreeModelSerializer, InvenTreeAttachmentSerializer -from InvenTree.serializers import InvenTreeAttachmentSerializerField, UserSerializerBrief +from InvenTree.serializers import UserSerializerBrief import InvenTree.helpers from InvenTree.serializers import InvenTreeDecimalField diff --git a/InvenTree/order/serializers.py b/InvenTree/order/serializers.py index c1356d78c9..0528d57596 100644 --- a/InvenTree/order/serializers.py +++ b/InvenTree/order/serializers.py @@ -24,7 +24,6 @@ from InvenTree.serializers import InvenTreeAttachmentSerializer from InvenTree.serializers import InvenTreeModelSerializer from InvenTree.serializers import InvenTreeDecimalField from InvenTree.serializers import InvenTreeMoneySerializer -from InvenTree.serializers import InvenTreeAttachmentSerializerField from InvenTree.status_codes import StockStatus from part.serializers import PartBriefSerializer diff --git a/InvenTree/templates/js/translated/attachment.js b/InvenTree/templates/js/translated/attachment.js index fe8a0580c1..5c5af5682f 100644 --- a/InvenTree/templates/js/translated/attachment.js +++ b/InvenTree/templates/js/translated/attachment.js @@ -149,7 +149,8 @@ function loadAttachmentTable(url, options) { return renderLink(html, value); } else if (row.link) { - return renderLink(row.link, row.link); + var html = ` ${row.link}`; + return renderLink(html, row.link); } else { return '-'; } From ace4370ee9773292b9493a15780cd5c9caddbe0f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 28 Nov 2021 16:50:34 +0100 Subject: [PATCH 13/17] add ruleset --- InvenTree/users/models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/InvenTree/users/models.py b/InvenTree/users/models.py index 4d1b46ae5d..c9ce20e567 100644 --- a/InvenTree/users/models.py +++ b/InvenTree/users/models.py @@ -159,6 +159,7 @@ class RuleSet(models.Model): 'error_report_error', 'exchange_rate', 'exchange_exchangebackend', + 'django_session', # Django-q 'django_q_ormq', From 5f685f3c2a283604bcefae382754b5c1d7b63a45 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 28 Nov 2021 16:54:11 +0100 Subject: [PATCH 14/17] fix db sessions in import / export --- tasks.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tasks.py b/tasks.py index a8f9bcaf84..f56ed44bc8 100644 --- a/tasks.py +++ b/tasks.py @@ -279,7 +279,6 @@ def content_excludes(): excludes = [ "contenttypes", - "sessions.session", "auth.permission", "authtoken.token", "error_report.error", @@ -291,7 +290,7 @@ def content_excludes(): "exchange.rate", "exchange.exchangebackend", "common.notificationentry", - "sessions.session", + "django_session", ] output = "" From 9454b51866bcaec85446416743b25352aace6521 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 28 Nov 2021 17:18:46 +0100 Subject: [PATCH 15/17] fix rules --- InvenTree/users/models.py | 3 +-- tasks.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/InvenTree/users/models.py b/InvenTree/users/models.py index c9ce20e567..48644e1889 100644 --- a/InvenTree/users/models.py +++ b/InvenTree/users/models.py @@ -145,7 +145,6 @@ class RuleSet(models.Model): # Core django models (not user configurable) 'admin_logentry', 'contenttypes_contenttype', - 'sessions_session', # Models which currently do not require permissions 'common_colortheme', @@ -159,7 +158,7 @@ class RuleSet(models.Model): 'error_report_error', 'exchange_rate', 'exchange_exchangebackend', - 'django_session', + 'user_sessions_session', # Django-q 'django_q_ormq', diff --git a/tasks.py b/tasks.py index f56ed44bc8..51df809149 100644 --- a/tasks.py +++ b/tasks.py @@ -290,7 +290,7 @@ def content_excludes(): "exchange.rate", "exchange.exchangebackend", "common.notificationentry", - "django_session", + "user_sessions_session", ] output = "" From a06d91d9a3218d5a63b700ddc2a58b81159539a8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 28 Nov 2021 17:21:54 +0100 Subject: [PATCH 16/17] fix reference name --- tasks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tasks.py b/tasks.py index 51df809149..7408bb40b5 100644 --- a/tasks.py +++ b/tasks.py @@ -290,7 +290,7 @@ def content_excludes(): "exchange.rate", "exchange.exchangebackend", "common.notificationentry", - "user_sessions_session", + "user_sessions.session", ] output = "" From 7b339b35ccbef37446117af9dd800361ab9428a2 Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 29 Nov 2021 07:46:37 +1100 Subject: [PATCH 17/17] Small UI tweaks for "sessions" view --- .../templates/InvenTree/settings/user.html | 30 +++++++++++-------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/InvenTree/templates/InvenTree/settings/user.html b/InvenTree/templates/InvenTree/settings/user.html index 97ed4805c9..cfc5b421e3 100644 --- a/InvenTree/templates/InvenTree/settings/user.html +++ b/InvenTree/templates/InvenTree/settings/user.html @@ -15,12 +15,12 @@ {% block actions %} {% inventree_demo_mode as demo %} {% if not demo %} +
+ {% trans "Set Password" %} +
{% trans "Edit" %}
-
- {% trans "Set Password" %} -
{% endif %} {% endblock %} @@ -175,7 +175,20 @@
-

{% trans "Active Sessions" %}

+
+

{% trans "Active Sessions" %}

+ {% include "spacer.html" %} +
+ {% if session_list.count > 1 %} +
+ {% csrf_token %} + +
+ {% endif %} +
+
@@ -203,15 +216,6 @@ {% endfor %} - - {% if session_list.count > 1 %} -
- {% csrf_token %} -

{% blocktrans %}You can also end all other sessions but the current. - This will log you out on all other devices.{% endblocktrans %} -

-
- {% endif %}
{% endblock %}