diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 027d3641cb..ebb9d4a4fc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,6 +4,10 @@ Contributions to InvenTree are welcomed - please follow the guidelines below. No pushing to master! New featues must be submitted in a separate branch (one branch per feature). +## Include Migration Files + +Any required migration files **must** be included in the commit, or the pull-request will be rejected. If you change the underlying database schema, make sure you run `make migrate` and commit the migration files before submitting the PR. + ## Testing Any new code should be covered by unit tests - a submitted PR may not be accepted if the code coverage is decreased. diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index d811c0165e..033261d9f8 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -61,6 +61,7 @@ settings_urls = [ url(r'^user/?', SettingsView.as_view(template_name='InvenTree/settings/user.html'), name='settings-user'), url(r'^currency/?', SettingsView.as_view(template_name='InvenTree/settings/currency.html'), name='settings-currency'), url(r'^part/?', SettingsView.as_view(template_name='InvenTree/settings/part.html'), name='settings-part'), + url(r'^other/?', SettingsView.as_view(template_name='InvenTree/settings/other.html'), name='settings-other'), # Catch any other urls url(r'^.*$', SettingsView.as_view(template_name='InvenTree/settings/user.html'), name='settings'), diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py index 610f3c0c00..ad0d70bfe8 100644 --- a/InvenTree/InvenTree/views.py +++ b/InvenTree/InvenTree/views.py @@ -16,6 +16,7 @@ from django.views.generic import UpdateView, CreateView from django.views.generic.base import TemplateView from part.models import Part +from common.models import InvenTreeSetting from .forms import DeleteForm, EditUserForm, SetPasswordForm from .helpers import str2bool @@ -511,3 +512,11 @@ class SettingsView(TemplateView): """ template_name = "InvenTree/settings.html" + + def get_context_data(self, **kwargs): + + ctx = super().get_context_data(**kwargs).copy() + + ctx['settings'] = InvenTreeSetting.objects.all().order_by('key') + + return ctx diff --git a/InvenTree/common/admin.py b/InvenTree/common/admin.py index e0db0a7136..cb643deca4 100644 --- a/InvenTree/common/admin.py +++ b/InvenTree/common/admin.py @@ -5,11 +5,17 @@ from django.contrib import admin from import_export.admin import ImportExportModelAdmin -from .models import Currency +from .models import Currency, InvenTreeSetting class CurrencyAdmin(ImportExportModelAdmin): list_display = ('symbol', 'suffix', 'description', 'value', 'base') +class SettingsAdmin(ImportExportModelAdmin): + + list_display = ('key', 'value', 'description') + + admin.site.register(Currency, CurrencyAdmin) +admin.site.register(InvenTreeSetting, SettingsAdmin) diff --git a/InvenTree/common/migrations/0004_inventreesetting.py b/InvenTree/common/migrations/0004_inventreesetting.py new file mode 100644 index 0000000000..64fc9cd486 --- /dev/null +++ b/InvenTree/common/migrations/0004_inventreesetting.py @@ -0,0 +1,21 @@ +# Generated by Django 2.2.5 on 2019-09-15 12:44 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('common', '0003_auto_20190902_2310'), + ] + + operations = [ + migrations.CreateModel( + name='InvenTreeSetting', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('key', models.CharField(help_text='Settings key', max_length=50, unique=True)), + ('value', models.CharField(blank=True, help_text='Settings value', max_length=200)), + ], + ), + ] diff --git a/InvenTree/common/migrations/0005_auto_20190915_1256.py b/InvenTree/common/migrations/0005_auto_20190915_1256.py new file mode 100644 index 0000000000..5b3205128d --- /dev/null +++ b/InvenTree/common/migrations/0005_auto_20190915_1256.py @@ -0,0 +1,23 @@ +# Generated by Django 2.2.5 on 2019-09-15 12:56 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('common', '0004_inventreesetting'), + ] + + operations = [ + migrations.AddField( + model_name='inventreesetting', + name='description', + field=models.CharField(blank=True, help_text='Settings description', max_length=200), + ), + migrations.AlterField( + model_name='inventreesetting', + name='key', + field=models.CharField(help_text='Settings key (must be unique - case insensitive', max_length=50, unique=True), + ), + ] diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index 61bb9ec138..0b8795484b 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -9,6 +9,79 @@ from __future__ import unicode_literals from django.db import models from django.utils.translation import ugettext as _ from django.core.validators import MinValueValidator, MaxValueValidator +from django.core.exceptions import ValidationError + + +class InvenTreeSetting(models.Model): + """ + An InvenTreeSetting object is a key:value pair used for storing + single values (e.g. one-off settings values). + + The class provides a way of retrieving the value for a particular key, + even if that key does not exist. + """ + + @classmethod + def get_setting(cls, key, backup_value=None): + """ + Get the value of a particular setting. + If it does not exist, return the backup value (default = None) + """ + + try: + setting = InvenTreeSetting.objects.get(key__iexact=key) + return setting.value + except InvenTreeSetting.DoesNotExist: + return backup_value + + @classmethod + def set_setting(cls, key, value, user, create=True): + """ + Set the value of a particular setting. + If it does not exist, option to create it. + + Args: + key: settings key + value: New value + user: User object (must be staff member to update a core setting) + create: If True, create a new setting if the specified key does not exist. + """ + + if not user.is_staff: + return + + try: + setting = InvenTreeSetting.objects.get(key__iexact=key) + except InvenTreeSetting.DoesNotExist: + + if create: + setting = InvenTreeSetting(key=key) + else: + return + + setting.value = value + setting.save() + + key = models.CharField(max_length=50, blank=False, unique=True, help_text=_('Settings key (must be unique - case insensitive')) + + value = models.CharField(max_length=200, blank=True, unique=False, help_text=_('Settings value')) + + description = models.CharField(max_length=200, blank=True, unique=False, help_text=_('Settings description')) + + def validate_unique(self, exclude=None): + """ Ensure that the key:value pair is unique. + In addition to the base validators, this ensures that the 'key' + is unique, using a case-insensitive comparison. + """ + + super().validate_unique(exclude) + + try: + setting = InvenTreeSetting.objects.exclude(id=self.id).filter(key__iexact=self.key) + if setting.exists(): + raise ValidationError({'key': _('Key string must be unique')}) + except InvenTreeSetting.DoesNotExist: + pass class Currency(models.Model): diff --git a/InvenTree/templates/InvenTree/settings/other.html b/InvenTree/templates/InvenTree/settings/other.html new file mode 100644 index 0000000000..2f6207b170 --- /dev/null +++ b/InvenTree/templates/InvenTree/settings/other.html @@ -0,0 +1,37 @@ +{% extends "InvenTree/settings/settings.html" %} + +{% block tabs %} +{% include "InvenTree/settings/tabs.html" with tab='other' %} +{% endblock %} + +{% block settings %} + +

InvenTree Settings

+ + + + + + + + + + + {% for setting in settings %} + + + + + + {% endfor %} + +
SettingValueDescription
{{ setting.key }}{{ setting.value }}{{ setting.description }}
+ +{% endblock %} + +{% block js_ready %} +{{ block.super }} + + $("#other-table").bootstrapTable(); + +{% endblock %} \ No newline at end of file diff --git a/InvenTree/templates/InvenTree/settings/tabs.html b/InvenTree/templates/InvenTree/settings/tabs.html index 78d15dbcfe..c33f823be2 100644 --- a/InvenTree/templates/InvenTree/settings/tabs.html +++ b/InvenTree/templates/InvenTree/settings/tabs.html @@ -8,4 +8,9 @@ Part + {% if user.is_staff %} + + Other + + {% endif %} \ No newline at end of file