mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
[FR] Add Feature flags (#4982)
* make currency choices independend
* Remove check for field, just try to get rid of it
* Add IF EXISTS to avoid error (works in postgres)
* Look for operational error, not programming error
* Use variants, depending on errors caused
* [FR] Add Feature flags
Fixes #4965
* Add option to define custom flags
* Revert "make currency choices independend"
This reverts commit ab84a7ff83
.
* try fixing mysql
* more safeguards
* fix executioner call
* a fck
* use migrations. syntax
* and another round for mysql
* revert print change
* use UTC for datetime
* Update part.migrations.0112
- Add custom migration class which handles errors
* Add unit test for migration
- Ensure that the new fields are added to the model
* Update reference to PR
* fix ruleset for missing_models
* fix ruleset for flags_flagstate
* add API endpoints for flags
* add tests for new API endpoints
* fix tests
* fix merge
* fix tests
---------
Co-authored-by: martin <martin@iggland.com>
Co-authored-by: Oliver Walters <oliver.henry.walters@gmail.com>
This commit is contained in:
parent
15ab911da6
commit
9f56ee1023
@ -234,6 +234,7 @@ INSTALLED_APPS = [
|
||||
'formtools', # Form wizard tools
|
||||
'dbbackup', # Backups - django-dbbackup
|
||||
'taggit', # Tagging
|
||||
'flags', # Flagging - django-flags
|
||||
|
||||
'allauth', # Base app for SSO
|
||||
'allauth.account', # Extend user with accounts
|
||||
@ -944,3 +945,23 @@ if DEBUG:
|
||||
|
||||
logger.info(f"MEDIA_ROOT: '{MEDIA_ROOT}'")
|
||||
logger.info(f"STATIC_ROOT: '{STATIC_ROOT}'")
|
||||
|
||||
# Flags
|
||||
FLAGS = {
|
||||
'EXPERIMENTAL': [
|
||||
{'condition': 'boolean', 'value': DEBUG},
|
||||
{'condition': 'parameter', 'value': 'experimental='},
|
||||
], # Should experimental features be turned on?
|
||||
'NEXT_GEN': [
|
||||
{'condition': 'parameter', 'value': 'ngen='},
|
||||
], # Should next-gen features be turned on?
|
||||
}
|
||||
|
||||
# Get custom flags from environment/yaml
|
||||
CUSTOM_FLAGS = get_setting('INVENTREE_FLAGS', 'flags', None, typecast=dict)
|
||||
if CUSTOM_FLAGS:
|
||||
if not isinstance(CUSTOM_FLAGS, dict):
|
||||
logger.error(f"Invalid custom flags, must be valid dict: {CUSTOM_FLAGS}")
|
||||
else:
|
||||
logger.info(f"Custom flags: {CUSTOM_FLAGS}")
|
||||
FLAGS.update(CUSTOM_FLAGS)
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
import json
|
||||
|
||||
from django.conf import settings
|
||||
from django.http.response import HttpResponse
|
||||
from django.urls import include, path, re_path
|
||||
from django.utils.decorators import method_decorator
|
||||
@ -480,6 +481,29 @@ class ProjectCodeDetail(RetrieveUpdateDestroyAPI):
|
||||
permission_classes = [permissions.IsAuthenticated, IsStaffOrReadOnly]
|
||||
|
||||
|
||||
class FlagList(ListAPI):
|
||||
"""List view for feature flags."""
|
||||
|
||||
queryset = settings.FLAGS
|
||||
serializer_class = common.serializers.FlagSerializer
|
||||
permission_classes = [permissions.AllowAny, ]
|
||||
|
||||
|
||||
class FlagDetail(RetrieveAPI):
|
||||
"""Detail view for an individual feature flag."""
|
||||
|
||||
serializer_class = common.serializers.FlagSerializer
|
||||
permission_classes = [permissions.AllowAny, ]
|
||||
|
||||
def get_object(self):
|
||||
"""Attempt to find a config object with the provided key."""
|
||||
key = self.kwargs['key']
|
||||
value = settings.FLAGS.get(key, None)
|
||||
if not value:
|
||||
raise NotFound()
|
||||
return {key: value}
|
||||
|
||||
|
||||
settings_api_urls = [
|
||||
# User settings
|
||||
re_path(r'^user/', include([
|
||||
@ -552,6 +576,11 @@ common_api_urls = [
|
||||
re_path(r'^.*$', NewsFeedEntryList.as_view(), name='api-news-list'),
|
||||
])),
|
||||
|
||||
# Flags
|
||||
path('flags/', include([
|
||||
path('<str:key>/', FlagDetail.as_view(), name='api-flag-detail'),
|
||||
re_path(r'^.*$', FlagList.as_view(), name='api-flag-list'),
|
||||
])),
|
||||
]
|
||||
|
||||
admin_api_urls = [
|
||||
|
@ -1,7 +1,9 @@
|
||||
"""JSON serializers for common components."""
|
||||
|
||||
|
||||
from django.urls import reverse
|
||||
|
||||
from flags.state import flag_state
|
||||
from rest_framework import serializers
|
||||
|
||||
from common.models import (InvenTreeSetting, InvenTreeUserSetting,
|
||||
@ -269,3 +271,19 @@ class ProjectCodeSerializer(InvenTreeModelSerializer):
|
||||
'code',
|
||||
'description'
|
||||
]
|
||||
|
||||
|
||||
class FlagSerializer(serializers.Serializer):
|
||||
"""Serializer for feature flags."""
|
||||
|
||||
def to_representation(self, instance):
|
||||
"""Return the configuration data as a dictionary."""
|
||||
request = self.context.get('request')
|
||||
if not isinstance(instance, str):
|
||||
instance = list(instance.keys())[0]
|
||||
data = {'key': instance, 'state': flag_state(instance, request=request)}
|
||||
|
||||
if request and request.user.is_superuser:
|
||||
data['conditions'] = self.instance[instance]
|
||||
|
||||
return data
|
||||
|
@ -875,6 +875,43 @@ class CommonTest(InvenTreeAPITestCase):
|
||||
self.user.is_superuser = False
|
||||
self.user.save()
|
||||
|
||||
def test_flag_api(self):
|
||||
"""Test flag URLs."""
|
||||
# Not superuser
|
||||
response = self.get(reverse('api-flag-list'), expected_code=200)
|
||||
self.assertEqual(len(response.data), 2)
|
||||
self.assertEqual(response.data[0]['key'], 'EXPERIMENTAL')
|
||||
|
||||
# Turn into superuser
|
||||
self.user.is_superuser = True
|
||||
self.user.save()
|
||||
|
||||
# Successful checks
|
||||
response = self.get(reverse('api-flag-list'), expected_code=200)
|
||||
self.assertEqual(len(response.data), 2)
|
||||
self.assertEqual(response.data[0]['key'], 'EXPERIMENTAL')
|
||||
self.assertTrue(response.data[0]['conditions'])
|
||||
|
||||
response = self.get(reverse('api-flag-detail', kwargs={'key': 'EXPERIMENTAL'}), expected_code=200)
|
||||
self.assertEqual(len(response.data), 3)
|
||||
self.assertEqual(response.data['key'], 'EXPERIMENTAL')
|
||||
self.assertTrue(response.data['conditions'])
|
||||
|
||||
# Try without param -> false
|
||||
response = self.get(reverse('api-flag-detail', kwargs={'key': 'NEXT_GEN'}), expected_code=200)
|
||||
self.assertFalse(response.data['state'])
|
||||
|
||||
# Try with param -> true
|
||||
response = self.get(reverse('api-flag-detail', kwargs={'key': 'NEXT_GEN'}), {'ngen': ''}, expected_code=200)
|
||||
self.assertTrue(response.data['state'])
|
||||
|
||||
# Try non existent flag
|
||||
response = self.get(reverse('api-flag-detail', kwargs={'key': 'NON_EXISTENT'}), expected_code=404)
|
||||
|
||||
# Turn into normal user again
|
||||
self.user.is_superuser = False
|
||||
self.user.save()
|
||||
|
||||
|
||||
class ColorThemeTest(TestCase):
|
||||
"""Tests for ColorTheme."""
|
||||
|
@ -228,3 +228,11 @@ remote_login_header: HTTP_REMOTE_USER
|
||||
# splash: splash_screen.jpg
|
||||
# hide_admin_link: true
|
||||
# hide_password_reset: true
|
||||
|
||||
# Custom flags
|
||||
# InvenTree uses django-flags; read more in their docs at https://cfpb.github.io/django-flags/conditions/
|
||||
# Use environment variable INVENTREE_FLAGS or the settings below
|
||||
# flags:
|
||||
# MY_FLAG:
|
||||
# - condition: 'parameter'
|
||||
# value: 'my_flag_param1'
|
||||
|
@ -81,6 +81,7 @@ class RuleSet(models.Model):
|
||||
'common_newsfeedentry',
|
||||
'taggit_tag',
|
||||
'taggit_taggeditem',
|
||||
'flags_flagstate',
|
||||
],
|
||||
'part_category': [
|
||||
'part_partcategory',
|
||||
|
@ -10,6 +10,7 @@ django-crispy-forms<2.0 # Form helpers # FIXED 2023-02-18 due to
|
||||
django-dbbackup # Backup / restore of database and media files
|
||||
django-error-report # Error report viewer for the admin interface
|
||||
django-filter # Extended filtering options
|
||||
django-flags # Feature flags
|
||||
django-formtools # Form wizard tools
|
||||
django-ical # iCal export for calendar views
|
||||
django-import-export==2.5.0 # Data import / export for admin interface
|
||||
|
@ -53,6 +53,7 @@ django==3.2.19
|
||||
# django-dbbackup
|
||||
# django-error-report
|
||||
# django-filter
|
||||
# django-flags
|
||||
# django-formtools
|
||||
# django-ical
|
||||
# django-import-export
|
||||
@ -92,6 +93,8 @@ django-error-report==0.2.0
|
||||
# via -r requirements.in
|
||||
django-filter==23.2
|
||||
# via -r requirements.in
|
||||
django-flags==5.0.12
|
||||
# via -r requirements.in
|
||||
django-formtools==2.4.1
|
||||
# via -r requirements.in
|
||||
django-ical==1.9.1
|
||||
|
Loading…
Reference in New Issue
Block a user