Simple app to add configuration options to a Django project.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge branch 'master' into develop

+282 -52
+3 -1
.gitignore
··· 45 45 # Pycharm 46 46 .idea 47 47 48 - # Vim 48 + # Visual Code 49 + .vscode 49 50 51 + # Vim 50 52 *~ 51 53 *.swp 52 54 *.swo
+10
HISTORY.rst
··· 3 3 History 4 4 ------- 5 5 6 + 1.0 (2018-10-2) 7 + +++++++++++++++++ 8 + 9 + * Added model for user's custom options. 10 + 11 + 1.0a3 (2018-8-29) 12 + +++++++++++++++++ 13 + 14 + * Fixed dependency with GeoDejango. 15 + 6 16 1.0a2 (2017-2-20) 7 17 +++++++++++++++++ 8 18
+16
Pipfile
··· 1 + [[source]] 2 + url = "https://pypi.org/simple" 3 + verify_ssl = true 4 + name = "pypi" 5 + 6 + [dev-packages] 7 + setuptools = "*" 8 + twine = "*" 9 + wheel = "*" 10 + 11 + [packages] 12 + six = ">=1.0" 13 + Django = ">=1.9" 14 + 15 + [requires] 16 + python_version = "3.6"
+119
Pipfile.lock
··· 1 + { 2 + "_meta": { 3 + "hash": { 4 + "sha256": "e93d0dd2d7b77e1f50699ba8c9c4b7125496ad3c7ed038db73cf5799192354cc" 5 + }, 6 + "pipfile-spec": 6, 7 + "requires": { 8 + "python_version": "3.6" 9 + }, 10 + "sources": [ 11 + { 12 + "name": "pypi", 13 + "url": "https://pypi.org/simple", 14 + "verify_ssl": true 15 + } 16 + ] 17 + }, 18 + "default": { 19 + "django": { 20 + "hashes": [ 21 + "sha256:7f246078d5a546f63c28fc03ce71f4d7a23677ce42109219c24c9ffb28416137", 22 + "sha256:ea50d85709708621d956187c6b61d9f9ce155007b496dd914fdb35db8d790aec" 23 + ], 24 + "index": "pypi", 25 + "version": "==2.1" 26 + }, 27 + "pytz": { 28 + "hashes": [ 29 + "sha256:a061aa0a9e06881eb8b3b2b43f05b9439d6583c206d0a6c340ff72a7b6669053", 30 + "sha256:ffb9ef1de172603304d9d2819af6f5ece76f2e85ec10692a524dd876e72bf277" 31 + ], 32 + "version": "==2018.5" 33 + }, 34 + "six": { 35 + "hashes": [ 36 + "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9", 37 + "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb" 38 + ], 39 + "index": "pypi", 40 + "version": "==1.11.0" 41 + } 42 + }, 43 + "develop": { 44 + "certifi": { 45 + "hashes": [ 46 + "sha256:376690d6f16d32f9d1fe8932551d80b23e9d393a8578c5633a2ed39a64861638", 47 + "sha256:456048c7e371c089d0a77a5212fb37a2c2dce1e24146e3b7e0261736aaeaa22a" 48 + ], 49 + "version": "==2018.8.24" 50 + }, 51 + "chardet": { 52 + "hashes": [ 53 + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", 54 + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" 55 + ], 56 + "version": "==3.0.4" 57 + }, 58 + "idna": { 59 + "hashes": [ 60 + "sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e", 61 + "sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16" 62 + ], 63 + "version": "==2.7" 64 + }, 65 + "pkginfo": { 66 + "hashes": [ 67 + "sha256:5878d542a4b3f237e359926384f1dde4e099c9f5525d236b1840cf704fa8d474", 68 + "sha256:a39076cb3eb34c333a0dd390b568e9e1e881c7bf2cc0aee12120636816f55aee" 69 + ], 70 + "version": "==1.4.2" 71 + }, 72 + "requests": { 73 + "hashes": [ 74 + "sha256:63b52e3c866428a224f97cab011de738c36aec0185aa91cfacd418b5d58911d1", 75 + "sha256:ec22d826a36ed72a7358ff3fe56cbd4ba69dd7a6718ffd450ff0e9df7a47ce6a" 76 + ], 77 + "version": "==2.19.1" 78 + }, 79 + "requests-toolbelt": { 80 + "hashes": [ 81 + "sha256:42c9c170abc2cacb78b8ab23ac957945c7716249206f90874651971a4acff237", 82 + "sha256:f6a531936c6fa4c6cfce1b9c10d5c4f498d16528d2a54a22ca00011205a187b5" 83 + ], 84 + "markers": "python_version != '3.1.*' and python_version < '4' and python_version != '3.3.*' and python_version != '3.2.*' and python_version >= '2.6' and python_version != '3.0.*'", 85 + "version": "==0.8.0" 86 + }, 87 + "tqdm": { 88 + "hashes": [ 89 + "sha256:5ef526702c0d265d5a960a3b27f3971fac13c26cf0fb819294bfa71fc6026c88", 90 + "sha256:a3364bd83ce4777320b862e3c8a93d7da91e20a95f06ef79bed7dd71c654cafa" 91 + ], 92 + "markers": "python_version >= '2.6' and python_version != '3.1.*' and python_version != '3.0.*'", 93 + "version": "==4.25.0" 94 + }, 95 + "twine": { 96 + "hashes": [ 97 + "sha256:08eb132bbaec40c6d25b358f546ec1dc96ebd2638a86eea68769d9e67fe2b129", 98 + "sha256:2fd9a4d9ff0bcacf41fdc40c8cb0cfaef1f1859457c9653fd1b92237cc4e9f25" 99 + ], 100 + "index": "pypi", 101 + "version": "==1.11.0" 102 + }, 103 + "urllib3": { 104 + "hashes": [ 105 + "sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf", 106 + "sha256:b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5" 107 + ], 108 + "version": "==1.23" 109 + }, 110 + "wheel": { 111 + "hashes": [ 112 + "sha256:0a2e54558a0628f2145d2fc822137e322412115173e8a2ddbe1c9024338ae83c", 113 + "sha256:80044e51ec5bbf6c894ba0bc48d26a8c20a9ba629f4ca19ea26ecfcf87685f5f" 114 + ], 115 + "index": "pypi", 116 + "version": "==0.31.1" 117 + } 118 + } 119 + }
+3 -8
options/__init__.py
··· 1 1 # -*- coding: utf-8 -*- 2 - from __future__ import unicode_literals, print_function, division, absolute_import 3 2 4 3 from django import get_version 5 4 from django.utils.translation import ugettext_lazy as _ 6 5 7 6 8 7 FLOAT, INT, STRING = (0, 1, 2) 9 - TYPE_CHOICES = ( 10 - (FLOAT, _("Float")), 11 - (INT, _("Integer")), 12 - (STRING, _("String")), 13 - ) 8 + TYPE_CHOICES = ((FLOAT, _("Float")), (INT, _("Integer")), (STRING, _("String"))) 14 9 15 - default_app_config = 'options.apps.ConfigurationsConfig' 10 + default_app_config = "options.apps.ConfigurationsConfig" 16 11 17 - VERSION = (1, 0, 0, 'alpha', 2) 12 + VERSION = (1, 1, 0, "final", 0) 18 13 19 14 __version__ = get_version(VERSION)
+2 -4
options/admin.py
··· 1 1 # -*- coding: utf-8 -*- 2 - from __future__ import unicode_literals, absolute_import 3 2 4 3 from django.contrib import admin 5 - 6 4 from options.models import Option 7 5 8 6 ··· 10 8 class OptionAdmin(admin.ModelAdmin): 11 9 """Manage configuration options.""" 12 10 13 - list_display = ['public_name', 'value'] 14 - search_fields = ['public_name', 'name'] 11 + list_display = ["public_name", "value"] 12 + search_fields = ["public_name", "name"]
+1 -2
options/apps.py
··· 1 1 # -*- coding: utf-8 -*- 2 - from __future__ import unicode_literals, print_function, division, absolute_import 3 - 4 2 import logging 5 3 import six 6 4 from django.apps import AppConfig ··· 15 13 def create_default_options(sender, **kwargs): 16 14 """Creates the defaults configuration options if they don't exists.""" 17 15 from options.models import Option 16 + 18 17 for key, data in six.iteritems(DEFAULT_OPTIONS): 19 18 if not Option.objects.filter(name=key).exists(): 20 19 try:
-2
options/context_processors.py
··· 1 1 # -*- coding: utf-8 -*- 2 - from __future__ import unicode_literals, print_function, absolute_import 3 - 4 2 from options.models import Option 5 3 6 4
+1 -3
options/management/commands/export_options.py
··· 1 1 # -*- coding: utf-8 -*- 2 - from __future__ import unicode_literals, print_function, division, absolute_import 3 - 4 2 import json 5 3 6 4 from django.core.management import BaseCommand ··· 17 15 export[option.name] = { 18 16 "value": option.value, 19 17 "type": option.type, 20 - "public_name": option.public_name 18 + "public_name": option.public_name, 21 19 } 22 20 self.stdout.write(json.dumps(export, indent=4, sort_keys=True))
+21 -1
options/managers.py
··· 1 1 # -*- coding: utf-8 -*- 2 - from __future__ import unicode_literals, print_function, division, absolute_import 2 + 3 3 4 4 from django.db import models 5 + from options.settings import DEFAULT_EXCLUDE_USER_OPTIONS 5 6 6 7 7 8 class OptionManager(models.Manager): ··· 15 16 except self.model.DoesNotExist: 16 17 return default 17 18 19 + 20 + class UserOptionManager(models.Manager): 21 + """Manager to handle user's custom options.""" 22 + 23 + def filter_user_customizable(self): 24 + """Returns option that the user can customize himself.""" 25 + return self.exclude(name__in=DEFAULT_EXCLUDE_USER_OPTIONS) 26 + 27 + def get_value(self, name, user=None, default=None): 28 + """Gets the value with the proper type.""" 29 + from options.models import Option 30 + 31 + if user is None: 32 + return Option.objects.get_value(name=name, default=default) 33 + try: 34 + option = self.model.objects.get(user=user, name=name) 35 + return option.get_value() 36 + except self.model.DoesNotExist: 37 + return Option.objects.get_value(name=name, default=default)
+39
options/migrations/0002_auto_20181002_0254.py
··· 1 + # Generated by Django 2.1 on 2018-10-02 07:54 2 + 3 + from django.conf import settings 4 + from django.db import migrations, models 5 + import django.db.models.deletion 6 + 7 + 8 + class Migration(migrations.Migration): 9 + 10 + dependencies = [ 11 + migrations.swappable_dependency(settings.AUTH_USER_MODEL), 12 + ('options', '0001_initial'), 13 + ] 14 + 15 + operations = [ 16 + migrations.CreateModel( 17 + name='UserOption', 18 + fields=[ 19 + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 + ('public_name', models.CharField(db_index=True, max_length=255, verbose_name='Public name of the parameter')), 21 + ('type', models.PositiveIntegerField(choices=[(0, 'Float'), (1, 'Integer'), (2, 'String')], default=2)), 22 + ('value', models.CharField(blank=True, default=None, max_length=256, null=True, verbose_name='Value')), 23 + ('is_list', models.BooleanField(default=False)), 24 + ('name', models.CharField(max_length=255, verbose_name='Parameter')), 25 + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='options', to=settings.AUTH_USER_MODEL)), 26 + ], 27 + options={ 28 + 'ordering': ['public_name'], 29 + }, 30 + ), 31 + migrations.AlterModelOptions( 32 + name='option', 33 + options={'ordering': ['public_name']}, 34 + ), 35 + migrations.AlterUniqueTogether( 36 + name='useroption', 37 + unique_together={('user', 'name')}, 38 + ), 39 + ]
+64 -31
options/models.py
··· 1 1 # -*- coding: utf-8 -*- 2 - from __future__ import unicode_literals, print_function, division, absolute_import 2 + 3 3 4 4 import six 5 - from django.contrib.gis.db import models 6 - from django.utils.encoding import python_2_unicode_compatible 5 + from django.conf import settings 6 + from django.db import models 7 7 from django.utils.translation import ugettext_lazy as _ 8 8 9 9 from options import STRING, TYPE_CHOICES, INT, FLOAT 10 - from options.managers import OptionManager 10 + from options.managers import OptionManager, UserOptionManager 11 11 12 12 13 - @python_2_unicode_compatible 14 - class Option(models.Model): 15 - """System options and configurations.""" 13 + class BaseOption(models.Model): 14 + """Base model for system options and configurations.""" 16 15 17 16 name = models.CharField( 18 - verbose_name=_("Parameter"), 19 - max_length=255, 20 - unique=True, 21 - db_index=True 17 + verbose_name=_("Parameter"), max_length=255, unique=True, db_index=True 22 18 ) 23 19 public_name = models.CharField( 24 20 verbose_name=_("Public name of the parameter"), 25 21 max_length=255, 26 22 unique=False, 27 - db_index=True 28 - ) 29 - type = models.PositiveIntegerField( 30 - choices=TYPE_CHOICES, 31 - default=STRING 23 + db_index=True, 32 24 ) 25 + type = models.PositiveIntegerField(choices=TYPE_CHOICES, default=STRING) 33 26 value = models.CharField( 34 - null=True, 35 - blank=True, 36 - default=None, 37 - max_length=256, 38 - verbose_name=_("Value") 27 + null=True, blank=True, default=None, max_length=256, verbose_name=_("Value") 39 28 ) 40 - 41 29 is_list = models.BooleanField(default=False) 42 30 43 - objects = OptionManager() 31 + class Meta: 32 + abstract = True 44 33 45 34 def __str__(self): 46 35 return "%s" % self.public_name 47 36 37 + def _convert_value(self, value, type): 38 + converter = {INT: int, FLOAT: float, STRING: six.text_type} 39 + default_values = {INT: 0, FLOAT: 1.0, STRING: ""} 40 + try: 41 + option_value = converter.get(self.type, six.text_type)(self.value) 42 + except ValueError: 43 + option_value = default_values.get(self.type) 44 + return option_value 45 + 48 46 def get_value(self): 49 - """Gets the value with the proper type.""" 50 - converter = { 51 - INT: int, 52 - FLOAT: float, 53 - STRING: six.text_type 54 - } 47 + """Gets the value with the proper type. If the type is not 48 + valid it would return the default value for the field, to avoid 49 + problems with manual database modifications""" 50 + 55 51 if not self.is_list: 56 - return converter.get(self.type, six.text_type)(self.value) 52 + return self._convert_value(self.value, self.type) 57 53 else: 58 54 values = self.value.split(",") 59 - return list(map(lambda item: converter.get(self.type, six.text_type)(item), values)) 55 + return [self._convert_value(self.type, item) for item in values] 56 + 57 + def clean(self): 58 + from django.core.exceptions import ValidationError 59 + 60 + converter = {INT: int, FLOAT: float, STRING: six.text_type} 61 + try: 62 + converter.get(self.type, six.text_type)(self.value) 63 + except ValueError: 64 + raise ValidationError(_("Invalid value for this type.")) 65 + 66 + def save(self, *args, **kwargs): 67 + self.clean() 68 + super().save(*args, **kwargs) 69 + 70 + 71 + class Option(BaseOption): 72 + """System options and configurations.""" 73 + 74 + objects = OptionManager() 75 + 76 + class Meta: 77 + ordering = ["public_name"] 78 + 79 + 80 + class UserOption(BaseOption): 81 + """Custom option for a user.""" 82 + 83 + user = models.ForeignKey( 84 + settings.AUTH_USER_MODEL, related_name="options", on_delete=models.CASCADE 85 + ) 86 + name = models.CharField(verbose_name=_("Parameter"), max_length=255) 87 + 88 + objects = UserOptionManager() 89 + 90 + class Meta: 91 + unique_together = ["user", "name"] 92 + ordering = ["public_name"]
+3
options/settings.py
··· 17 17 # } 18 18 # 19 19 DEFAULT_OPTIONS = getattr(settings, "CONFIGURATION_DEFAULT_OPTIONS", {}) 20 + 21 + # Set the list of options that the user can't customize. 22 + DEFAULT_EXCLUDE_USER_OPTIONS = getattr(settings, "EXCLUDE_USER_OPTIONS", tuple())