mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge remote-tracking branch 'inventree/master'
This commit is contained in:
commit
b5409c88be
@ -6,7 +6,7 @@ from InvenTree.settings import *
|
||||
|
||||
# Override the 'test' database
|
||||
if 'test' in sys.argv:
|
||||
eprint('InvenTree: Running tests - Using MySQL test database')
|
||||
eprint('InvenTree: Running tests - Using PostGreSQL test database')
|
||||
|
||||
DATABASES['default'] = {
|
||||
# Ensure postgresql backend is being used
|
||||
|
@ -9,8 +9,9 @@ from django.utils.translation import ugettext as _
|
||||
from django import forms
|
||||
from crispy_forms.helper import FormHelper
|
||||
from crispy_forms.layout import Layout, Field
|
||||
from crispy_forms.bootstrap import PrependedText, AppendedText, PrependedAppendedText
|
||||
from crispy_forms.bootstrap import PrependedText, AppendedText, PrependedAppendedText, StrictButton, Div
|
||||
from django.contrib.auth.models import User
|
||||
from common.models import ColorTheme
|
||||
|
||||
|
||||
class HelperForm(forms.ModelForm):
|
||||
@ -161,3 +162,36 @@ class SetPasswordForm(HelperForm):
|
||||
'enter_password',
|
||||
'confirm_password'
|
||||
]
|
||||
|
||||
|
||||
class ColorThemeSelectForm(forms.ModelForm):
|
||||
""" Form for setting color theme """
|
||||
|
||||
name = forms.ChoiceField(choices=(), required=False)
|
||||
|
||||
class Meta:
|
||||
model = ColorTheme
|
||||
fields = [
|
||||
'name'
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ColorThemeSelectForm, self).__init__(*args, **kwargs)
|
||||
|
||||
# Populate color themes choices
|
||||
self.fields['name'].choices = ColorTheme.get_color_themes_choices()
|
||||
|
||||
self.helper = FormHelper()
|
||||
# Form rendering
|
||||
self.helper.form_show_labels = False
|
||||
self.helper.layout = Layout(
|
||||
Div(
|
||||
Div(Field('name'),
|
||||
css_class='col-sm-6',
|
||||
style='width: 200px;'),
|
||||
Div(StrictButton(_('Apply Theme'), css_class='btn btn-primary', type='submit'),
|
||||
css_class='col-sm-6',
|
||||
style='width: auto;'),
|
||||
css_class='row',
|
||||
),
|
||||
)
|
||||
|
@ -82,6 +82,9 @@ STATICFILES_DIRS = [
|
||||
os.path.join(BASE_DIR, 'InvenTree', 'static'),
|
||||
]
|
||||
|
||||
# Color Themes Directory
|
||||
STATIC_COLOR_THEMES_DIR = os.path.join(STATIC_ROOT, 'css', 'color-themes')
|
||||
|
||||
# Web URL endpoint for served media files
|
||||
MEDIA_URL = '/media/'
|
||||
|
||||
|
2793
InvenTree/InvenTree/static/css/color-themes/dark-reader.css
Normal file
2793
InvenTree/InvenTree/static/css/color-themes/dark-reader.css
Normal file
File diff suppressed because it is too large
Load Diff
42
InvenTree/InvenTree/static/css/color-themes/darker.css
Normal file
42
InvenTree/InvenTree/static/css/color-themes/darker.css
Normal file
@ -0,0 +1,42 @@
|
||||
/* Color Theme: "Darker" by Radek Hladik */
|
||||
|
||||
.navbar-nav > li {
|
||||
border-color: rgb(179, 179, 179);
|
||||
}
|
||||
|
||||
.navbar-nav > li > a {
|
||||
color:#0b2a62 !important;
|
||||
}
|
||||
|
||||
.navbar-nav > li > a:hover {
|
||||
color:#202020 !important;
|
||||
}
|
||||
|
||||
.navbar-nav > .open > a {
|
||||
color:#202020 !important;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
background-color: rgb(189, 189, 189);
|
||||
}
|
||||
|
||||
.table-condensed > tbody > tr > td {
|
||||
border-top: 1px solid #062152 !important ;
|
||||
}
|
||||
|
||||
.table-striped > tbody > tr > td {
|
||||
border-top: 1px solid #92b3f1 ;
|
||||
}
|
||||
|
||||
.table-bordered, .table-bordered > tbody > tr > td {
|
||||
border: 1px solid rgb(182,182,182);
|
||||
}
|
||||
|
||||
.table-bordered > thead > tr > th {
|
||||
border: 1px solid rgb(182, 182, 182);
|
||||
background-color: rgb(235, 235, 235);
|
||||
}
|
||||
|
||||
h3 {
|
||||
color:#06255d;
|
||||
}
|
1
InvenTree/InvenTree/static/css/color-themes/default.css
Normal file
1
InvenTree/InvenTree/static/css/color-themes/default.css
Normal file
@ -0,0 +1 @@
|
||||
/* Color Theme: "Default" */
|
@ -36,7 +36,7 @@ from django.views.generic.base import RedirectView
|
||||
from rest_framework.documentation import include_docs_urls
|
||||
|
||||
from .views import IndexView, SearchView, DatabaseStatsView
|
||||
from .views import SettingsView, EditUserView, SetPasswordView
|
||||
from .views import SettingsView, EditUserView, SetPasswordView, ColorThemeSelectView
|
||||
from .views import DynamicJsView
|
||||
|
||||
from .api import InfoView
|
||||
@ -71,6 +71,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'^theme/?', ColorThemeSelectView.as_view(), name='settings-theme'),
|
||||
url(r'^other/?', SettingsView.as_view(template_name='InvenTree/settings/other.html'), name='settings-other'),
|
||||
|
||||
# Catch any other urls
|
||||
|
@ -11,16 +11,17 @@ from __future__ import unicode_literals
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.template.loader import render_to_string
|
||||
from django.http import JsonResponse, HttpResponseRedirect
|
||||
from django.urls import reverse_lazy
|
||||
|
||||
from django.views import View
|
||||
from django.views.generic import UpdateView, CreateView
|
||||
from django.views.generic import UpdateView, CreateView, FormView
|
||||
from django.views.generic.base import TemplateView
|
||||
|
||||
from part.models import Part, PartCategory
|
||||
from stock.models import StockLocation, StockItem
|
||||
from common.models import InvenTreeSetting
|
||||
from common.models import InvenTreeSetting, ColorTheme
|
||||
|
||||
from .forms import DeleteForm, EditUserForm, SetPasswordForm
|
||||
from .forms import DeleteForm, EditUserForm, SetPasswordForm, ColorThemeSelectForm
|
||||
from .helpers import str2bool
|
||||
|
||||
from rest_framework import views
|
||||
@ -556,6 +557,81 @@ class SettingsView(TemplateView):
|
||||
return ctx
|
||||
|
||||
|
||||
class ColorThemeSelectView(FormView):
|
||||
""" View for selecting a color theme """
|
||||
|
||||
form_class = ColorThemeSelectForm
|
||||
success_url = reverse_lazy('settings-theme')
|
||||
template_name = "InvenTree/settings/theme.html"
|
||||
|
||||
def get_user_theme(self):
|
||||
""" Get current user color theme """
|
||||
try:
|
||||
user_theme = ColorTheme.objects.filter(user=self.request.user).get()
|
||||
except ColorTheme.DoesNotExist:
|
||||
user_theme = None
|
||||
|
||||
return user_theme
|
||||
|
||||
def get_initial(self):
|
||||
""" Select current user color theme as initial choice """
|
||||
|
||||
initial = super(ColorThemeSelectView, self).get_initial()
|
||||
|
||||
user_theme = self.get_user_theme()
|
||||
if user_theme:
|
||||
initial['name'] = user_theme.name
|
||||
return initial
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
""" Check if current color theme exists, else display alert box """
|
||||
|
||||
context = {}
|
||||
|
||||
form = self.get_form()
|
||||
context['form'] = form
|
||||
|
||||
user_theme = self.get_user_theme()
|
||||
if user_theme:
|
||||
# Check color theme is a valid choice
|
||||
if not ColorTheme.is_valid_choice(user_theme):
|
||||
user_color_theme_name = user_theme.name
|
||||
if not user_color_theme_name:
|
||||
user_color_theme_name = 'default'
|
||||
|
||||
context['invalid_color_theme'] = user_color_theme_name
|
||||
|
||||
return self.render_to_response(context)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
""" Save user color theme selection """
|
||||
|
||||
form = self.get_form()
|
||||
|
||||
# Get current user theme
|
||||
user_theme = self.get_user_theme()
|
||||
|
||||
# Create theme entry if user did not select one yet
|
||||
if not user_theme:
|
||||
user_theme = ColorTheme()
|
||||
user_theme.user = request.user
|
||||
|
||||
if form.is_valid():
|
||||
theme_selected = form.cleaned_data['name']
|
||||
|
||||
# Set color theme to form selection
|
||||
user_theme.name = theme_selected
|
||||
user_theme.save()
|
||||
|
||||
return self.form_valid(form)
|
||||
else:
|
||||
# Set color theme to default
|
||||
user_theme.name = ColorTheme.default_color_theme[0]
|
||||
user_theme.save()
|
||||
|
||||
return self.form_invalid(form)
|
||||
|
||||
|
||||
class DatabaseStatsView(AjaxView):
|
||||
""" View for displaying database statistics """
|
||||
|
||||
|
21
InvenTree/common/migrations/0007_colortheme.py
Normal file
21
InvenTree/common/migrations/0007_colortheme.py
Normal file
@ -0,0 +1,21 @@
|
||||
# Generated by Django 3.0.7 on 2020-09-09 19:50
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('common', '0006_auto_20200203_0951'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ColorTheme',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(blank=True, default='', max_length=20)),
|
||||
('user', models.CharField(max_length=150, unique=True)),
|
||||
],
|
||||
),
|
||||
]
|
@ -6,7 +6,10 @@ These models are 'generic' and do not fit a particular business logic object.
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import os
|
||||
|
||||
from django.db import models
|
||||
from django.conf import settings
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.core.validators import MinValueValidator, MaxValueValidator
|
||||
from django.core.exceptions import ValidationError
|
||||
@ -154,3 +157,49 @@ class Currency(models.Model):
|
||||
self.value = 1.0
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class ColorTheme(models.Model):
|
||||
""" Color Theme Setting """
|
||||
|
||||
default_color_theme = ('', _('Default'))
|
||||
|
||||
name = models.CharField(max_length=20,
|
||||
default='',
|
||||
blank=True)
|
||||
|
||||
user = models.CharField(max_length=150,
|
||||
unique=True)
|
||||
|
||||
@classmethod
|
||||
def get_color_themes_choices(cls):
|
||||
""" Get all color themes from static folder """
|
||||
|
||||
# Get files list from css/color-themes/ folder
|
||||
files_list = []
|
||||
for file in os.listdir(settings.STATIC_COLOR_THEMES_DIR):
|
||||
files_list.append(os.path.splitext(file))
|
||||
|
||||
# Get color themes choices (CSS sheets)
|
||||
choices = [(file_name.lower(), _(file_name.replace('-', ' ').title()))
|
||||
for file_name, file_ext in files_list
|
||||
if file_ext == '.css' and file_name.lower() != 'default']
|
||||
|
||||
# Add default option as empty option
|
||||
choices.insert(0, cls.default_color_theme)
|
||||
|
||||
return choices
|
||||
|
||||
@classmethod
|
||||
def is_valid_choice(cls, user_color_theme):
|
||||
""" Check if color theme is valid choice """
|
||||
try:
|
||||
user_color_theme_name = user_color_theme.name
|
||||
except AttributeError:
|
||||
return False
|
||||
|
||||
for color_theme in cls.get_color_themes_choices():
|
||||
if user_color_theme_name == color_theme[0]:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
@ -1,12 +1,13 @@
|
||||
""" This module provides template tags for extra functionality
|
||||
over and above the built-in Django tags.
|
||||
"""
|
||||
import os
|
||||
|
||||
from django import template
|
||||
from InvenTree import version
|
||||
from InvenTree import version, settings
|
||||
from InvenTree.helpers import decimal2string
|
||||
|
||||
from common.models import InvenTreeSetting
|
||||
from common.models import InvenTreeSetting, ColorTheme
|
||||
|
||||
register = template.Library()
|
||||
|
||||
@ -88,3 +89,22 @@ def inventree_docs_url(*args, **kwargs):
|
||||
@register.simple_tag()
|
||||
def inventree_setting(key, *args, **kwargs):
|
||||
return InvenTreeSetting.get_setting(key)
|
||||
|
||||
|
||||
@register.simple_tag()
|
||||
def get_color_theme_css(username):
|
||||
try:
|
||||
user_theme = ColorTheme.objects.filter(user=username).get()
|
||||
user_theme_name = user_theme.name
|
||||
if not user_theme_name or not ColorTheme.is_valid_choice(user_theme):
|
||||
user_theme_name = 'default'
|
||||
except ColorTheme.DoesNotExist:
|
||||
user_theme_name = 'default'
|
||||
|
||||
# Build path to CSS sheet
|
||||
inventree_css_sheet = os.path.join('css', 'color-themes', user_theme_name + '.css')
|
||||
|
||||
# Build static URL
|
||||
inventree_css_static_url = os.path.join(settings.STATIC_URL, inventree_css_sheet)
|
||||
|
||||
return inventree_css_static_url
|
||||
|
@ -78,6 +78,56 @@ class PartDetailTest(PartViewTestCase):
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTrue(response.context['editing_enabled'])
|
||||
|
||||
def test_part_detail_from_ipn(self):
|
||||
"""
|
||||
Test that we can retrieve a part detail page from part IPN:
|
||||
- if no part with matching IPN -> return part index
|
||||
- if unique IPN match -> return part detail page
|
||||
- if multiple IPN matches -> return part index
|
||||
"""
|
||||
ipn_test = 'PART-000000-AA'
|
||||
pk = 1
|
||||
|
||||
def test_ipn_match(index_result=False, detail_result=False):
|
||||
index_redirect = False
|
||||
detail_redirect = False
|
||||
|
||||
response = self.client.get(reverse('part-detail-from-ipn', args=(ipn_test,)))
|
||||
|
||||
# Check for PartIndex redirect
|
||||
try:
|
||||
if response.url == '/part/':
|
||||
index_redirect = True
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
# Check for PartDetail redirect
|
||||
try:
|
||||
if response.context['part'].pk == pk:
|
||||
detail_redirect = True
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
self.assertEqual(index_result, index_redirect)
|
||||
self.assertEqual(detail_result, detail_redirect)
|
||||
|
||||
# Test no match
|
||||
test_ipn_match(index_result=True, detail_result=False)
|
||||
|
||||
# Test unique match
|
||||
part = Part.objects.get(pk=pk)
|
||||
part.IPN = ipn_test
|
||||
part.save()
|
||||
|
||||
test_ipn_match(index_result=False, detail_result=True)
|
||||
|
||||
# Test multiple matches
|
||||
part = Part.objects.get(pk=pk + 1)
|
||||
part.IPN = ipn_test
|
||||
part.save()
|
||||
|
||||
test_ipn_match(index_result=True, detail_result=False)
|
||||
|
||||
def test_bom_download(self):
|
||||
""" Test downloading a BOM for a valid part """
|
||||
|
||||
|
@ -99,7 +99,7 @@ part_urls = [
|
||||
# Export data for multiple parts
|
||||
url(r'^export/', views.PartExport.as_view(), name='part-export'),
|
||||
|
||||
# Individual part
|
||||
# Individual part using pk
|
||||
url(r'^(?P<pk>\d+)/', include(part_detail_urls)),
|
||||
|
||||
# Part category
|
||||
@ -124,6 +124,9 @@ part_urls = [
|
||||
# Bom Items
|
||||
url(r'^bom/(?P<pk>\d+)/', include(part_bom_urls)),
|
||||
|
||||
# Individual part using IPN as slug
|
||||
url(r'^(?P<slug>[-\w]+)/', views.PartDetailFromIPN.as_view(), name='part-detail-from-ipn'),
|
||||
|
||||
# Top level part list (display top level parts and categories)
|
||||
url(r'^.*$', views.PartIndex.as_view(), name='part-index'),
|
||||
]
|
||||
|
@ -663,6 +663,43 @@ class PartDetail(DetailView):
|
||||
return context
|
||||
|
||||
|
||||
class PartDetailFromIPN(PartDetail):
|
||||
slug_field = 'IPN'
|
||||
slug_url_kwarg = 'slug'
|
||||
|
||||
def get_object(self):
|
||||
""" Return Part object which IPN field matches the slug value """
|
||||
queryset = self.get_queryset()
|
||||
# Get slug
|
||||
slug = self.kwargs.get(self.slug_url_kwarg)
|
||||
|
||||
if slug is not None:
|
||||
slug_field = self.get_slug_field()
|
||||
# Filter by the slug value
|
||||
queryset = queryset.filter(**{slug_field: slug})
|
||||
|
||||
try:
|
||||
# Get unique part from queryset
|
||||
part = queryset.get()
|
||||
# Return Part object
|
||||
return part
|
||||
except queryset.model.MultipleObjectsReturned:
|
||||
pass
|
||||
except queryset.model.DoesNotExist:
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
""" Attempt to match slug to a Part, else redirect to PartIndex view """
|
||||
self.object = self.get_object()
|
||||
|
||||
if not self.object:
|
||||
return HttpResponseRedirect(reverse('part-index'))
|
||||
|
||||
return super(PartDetailFromIPN, self).get(request, *args, **kwargs)
|
||||
|
||||
|
||||
class PartQRCode(QRCodeView):
|
||||
""" View for displaying a QR code for a Part object """
|
||||
|
||||
|
@ -8,6 +8,9 @@
|
||||
<li{% ifequal tab 'part' %} class='active'{% endifequal %}>
|
||||
<a href="{% url 'settings-part' %}"><span class='fas fa-shapes'></span> Part</a>
|
||||
</li>
|
||||
<li{% ifequal tab 'theme' %} class='active'{% endifequal %}>
|
||||
<a href="{% url 'settings-theme' %}"><span class='fas fa-fill'></span> Theme</a>
|
||||
</li>
|
||||
{% if user.is_staff %}
|
||||
<li{% ifequal tab 'other' %} class='active'{% endifequal %}>
|
||||
<a href="{% url 'settings-other' %}"><span class='fas fa-cogs'></span> Other</a>
|
||||
|
32
InvenTree/templates/InvenTree/settings/theme.html
Normal file
32
InvenTree/templates/InvenTree/settings/theme.html
Normal file
@ -0,0 +1,32 @@
|
||||
{% extends "InvenTree/settings/settings.html" %}
|
||||
{% load i18n %}
|
||||
{% load inventree_extras %}
|
||||
|
||||
{% block tabs %}
|
||||
{% include "InvenTree/settings/tabs.html" with tab='theme' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block settings %}
|
||||
|
||||
<div class='row'>
|
||||
<div class='col-sm-6'>
|
||||
<h4>Color Themes</h4>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form action="{% url 'settings-theme' %}" method="post">
|
||||
{% csrf_token %}
|
||||
{% load crispy_forms_tags %}
|
||||
{% crispy form %}
|
||||
</form>
|
||||
|
||||
{% if invalid_color_theme %}
|
||||
<div class="alert alert-danger alert-block" role="alert" style="display: inline-block;">
|
||||
{% blocktrans %}
|
||||
The CSS sheet "{{invalid_color_theme}}.css" for the currently selected color theme was not found.<br>
|
||||
Please select another color theme :)
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
@ -1,5 +1,6 @@
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load inventree_extras %}
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
@ -39,6 +40,7 @@
|
||||
<link rel="stylesheet" href="{% static 'css/select2-bootstrap.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/bootstrap-toggle.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/inventree.css' %}">
|
||||
<link rel="stylesheet" href="{% get_color_theme_css user.get_username %}">
|
||||
|
||||
{% block css %}
|
||||
{% endblock %}
|
||||
|
Loading…
Reference in New Issue
Block a user