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 'develop'

+367 -236
+10
HISTORY.rst
··· 3 3 History 4 4 ------- 5 5 6 + 2.0 (2019-08-27) 7 + +++++++++++++++++ 8 + 9 + * Drop support for Python 2. 10 + * ``Option`` model and ``UserOption`` model are now swappable. 11 + * Added option for file options. 12 + * Changed names of settings variables. 13 + * Added ``pyptoject.toml`` to distribute. 14 + * Tests running with pytest. 15 + 6 16 1.2 (2019-07-26) 7 17 +++++++++++++++++ 8 18
-3
MANIFEST.in
··· 1 - include LICENSE 2 - include README.rst 3 - include HISTORY.rst
+2 -4
Pipfile
··· 4 4 name = "pypi" 5 5 6 6 [packages] 7 - six = ">=1.0" 8 7 Django = ">=1.9" 9 8 pytest-django = "*" 10 9 pytest-cov = "*" 11 10 django-rest-framework = "*" 12 - setuptools = "*" 13 - twine = "*" 14 - wheel = "*" 15 11 factory-boy = "*" 12 + flit = "*" 13 + pytest = "*"
+27 -76
Pipfile.lock
··· 1 1 { 2 2 "_meta": { 3 3 "hash": { 4 - "sha256": "576c91ff28cf0ab747a29bdfa9a5d6b857cc3621b66faea43be2052c43e8bfeb" 4 + "sha256": "5ff4dd46a3be6eb20b2e877d1ba9808959ace19525fc40dda1e9eadf46ba6d49" 5 5 }, 6 6 "pipfile-spec": 6, 7 7 "requires": {}, ··· 27 27 "sha256:f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399" 28 28 ], 29 29 "version": "==19.1.0" 30 - }, 31 - "bleach": { 32 - "hashes": [ 33 - "sha256:213336e49e102af26d9cde77dd2d0397afabc5a6bf2fed985dc35b5d1e285a16", 34 - "sha256:3fdf7f77adcf649c9911387df51254b813185e32b2c6619f690b593a617e19fa" 35 - ], 36 - "version": "==3.1.0" 37 30 }, 38 31 "certifi": { 39 32 "hashes": [ ··· 126 119 }, 127 120 "faker": { 128 121 "hashes": [ 129 - "sha256:96ad7902706f2409a2d0c3de5132f69b413555a419bacec99d3f16e657895b47", 130 - "sha256:b3bb64aff9571510de6812df45122b633dbc6227e870edae3ed9430f94698521" 122 + "sha256:1d3f700e8dfcefd6e657118d71405d53e86974448aba78884f119bbd84c0cddf", 123 + "sha256:d5366e120191c5610fceeebfe1c298dc46da0277096f639c6dd7e2eaee0fa547" 124 + ], 125 + "version": "==2.0.1" 126 + }, 127 + "flit": { 128 + "hashes": [ 129 + "sha256:1d93f7a833ed8a6e120ddc40db5c4763bc39bccc75c05081ec8285ece718aefb", 130 + "sha256:6f6f0fb83c51ffa3a150fa41b5ac118df9ea4a87c2c06dff4ebf9adbe7b52b36" 131 131 ], 132 - "version": "==2.0.0" 132 + "index": "pypi", 133 + "version": "==1.3" 133 134 }, 134 135 "idna": { 135 136 "hashes": [ ··· 143 144 "sha256:23d3d873e008a513952355379d93cbcab874c58f4f034ff657c7a87422fa64e8", 144 145 "sha256:80d2de76188eabfbfcf27e6a37342c2827801e59c4cc14b0371c56fed43820e3" 145 146 ], 147 + "markers": "python_version < '3.8'", 146 148 "version": "==0.19" 147 149 }, 148 150 "more-itertools": { ··· 159 161 ], 160 162 "version": "==19.1" 161 163 }, 162 - "pkginfo": { 163 - "hashes": [ 164 - "sha256:7424f2c8511c186cd5424bbf31045b77435b37a8d604990b79d4e70d741148bb", 165 - "sha256:a6d9e40ca61ad3ebd0b72fbadd4fba16e4c0e4df0428c041e01e06eb6ee71f32" 166 - ], 167 - "version": "==1.5.0.1" 168 - }, 169 164 "pluggy": { 170 165 "hashes": [ 171 166 "sha256:0825a152ac059776623854c1543d65a4ad408eb3d33ee114dff91e57ec6ae6fc", ··· 180 175 ], 181 176 "version": "==1.8.0" 182 177 }, 183 - "pygments": { 184 - "hashes": [ 185 - "sha256:71e430bc85c88a430f000ac1d9b331d2407f681d6f6aec95e8bcfbc3df5b0127", 186 - "sha256:881c4c157e45f30af185c1ffe8d549d48ac9127433f2c380c24b84572ad66297" 187 - ], 188 - "version": "==2.4.2" 189 - }, 190 178 "pyparsing": { 191 179 "hashes": [ 192 180 "sha256:6f98a7b9397e206d78cc01df10131398f1c8b8510a2f4d97d9abd82e1aacdd80", ··· 196 184 }, 197 185 "pytest": { 198 186 "hashes": [ 199 - "sha256:6ef6d06de77ce2961156013e9dff62f1b2688aa04d0dc244299fe7d67e09370d", 200 - "sha256:a736fed91c12681a7b34617c8fcefe39ea04599ca72c608751c31d89579a3f77" 187 + "sha256:95b1f6db806e5b1b5b443efeb58984c24945508f93a866c1719e1a507a957d7c", 188 + "sha256:c3d5020755f70c82eceda3feaf556af9a341334414a8eca521a18f463bcead88" 201 189 ], 202 - "version": "==5.0.1" 190 + "index": "pypi", 191 + "version": "==5.1.1" 203 192 }, 204 193 "pytest-cov": { 205 194 "hashes": [ ··· 223 212 "sha256:c89805f6f4d64db21ed966fda138f8a5ed7a4fdbc1a8ee329ce1b74e3c74da9e" 224 213 ], 225 214 "version": "==2.8.0" 215 + }, 216 + "pytoml": { 217 + "hashes": [ 218 + "sha256:57a21e6347049f73bfb62011ff34cd72774c031b9828cb628a752225136dfc33", 219 + "sha256:8eecf7c8d0adcff3b375b09fe403407aa9b645c499e5ab8cac670ac4a35f61e7" 220 + ], 221 + "version": "==0.1.21" 226 222 }, 227 223 "pytz": { 228 224 "hashes": [ ··· 231 227 ], 232 228 "version": "==2019.2" 233 229 }, 234 - "readme-renderer": { 235 - "hashes": [ 236 - "sha256:bb16f55b259f27f75f640acf5e00cf897845a8b3e4731b5c1a436e4b8529202f", 237 - "sha256:c8532b79afc0375a85f10433eca157d6b50f7d6990f337fa498c96cd4bfc203d" 238 - ], 239 - "version": "==24.0" 240 - }, 241 230 "requests": { 242 231 "hashes": [ 243 232 "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4", ··· 245 234 ], 246 235 "version": "==2.22.0" 247 236 }, 248 - "requests-toolbelt": { 249 - "hashes": [ 250 - "sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f", 251 - "sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0" 252 - ], 253 - "version": "==0.9.1" 254 - }, 255 237 "six": { 256 238 "hashes": [ 257 239 "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", 258 240 "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" 259 241 ], 260 - "index": "pypi", 261 242 "version": "==1.12.0" 262 243 }, 263 244 "sqlparse": { ··· 274 255 ], 275 256 "version": "==1.2" 276 257 }, 277 - "tqdm": { 278 - "hashes": [ 279 - "sha256:1dc82f87a8726602fa7177a091b5e8691d6523138a8f7acd08e58088f51e389f", 280 - "sha256:47220a4f2aeebbc74b0ab317584264ea44c745e1fd5ff316b675cd0aff8afad8" 281 - ], 282 - "version": "==4.33.0" 283 - }, 284 - "twine": { 285 - "hashes": [ 286 - "sha256:0fb0bfa3df4f62076cab5def36b1a71a2e4acb4d1fa5c97475b048117b1a6446", 287 - "sha256:d6c29c933ecfc74e9b1d9fa13aa1f87c5d5770e119f5a4ce032092f0ff5b14dc" 288 - ], 289 - "index": "pypi", 290 - "version": "==1.13.0" 291 - }, 292 258 "urllib3": { 293 259 "hashes": [ 294 260 "sha256:b246607a25ac80bedac05c6f282e3cdaf3afb65420fd024ac94435cabe6e18d1", ··· 303 269 ], 304 270 "version": "==0.1.7" 305 271 }, 306 - "webencodings": { 307 - "hashes": [ 308 - "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", 309 - "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923" 310 - ], 311 - "version": "==0.5.1" 312 - }, 313 - "wheel": { 314 - "hashes": [ 315 - "sha256:5e79117472686ac0c4aef5bad5172ea73a1c2d1646b808c35926bd26bdfb0c08", 316 - "sha256:62fcfa03d45b5b722539ccbc07b190e4bfff4bb9e3a4d470dd9f6a0981002565" 317 - ], 318 - "index": "pypi", 319 - "version": "==0.33.4" 320 - }, 321 272 "zipp": { 322 273 "hashes": [ 323 - "sha256:4970c3758f4e89a7857a973b1e2a5d75bcdc47794442f2e2dd4fe8e0466e809a", 324 - "sha256:8a5712cfd3bb4248015eb3b0b3c54a5f6ee3f2425963ef2a0125b8bc40aafaec" 274 + "sha256:3718b1cbcd963c7d4c5511a8240812904164b7f381b647143a89d3b98f9bcd8e", 275 + "sha256:f06903e9f1f43b12d371004b4ac7b06ab39a44adc747266928ae6debfa7b3335" 325 276 ], 326 - "version": "==0.5.2" 277 + "version": "==0.6.0" 327 278 } 328 279 }, 329 280 "develop": {}
+2 -2
README.rst
··· 26 26 Settings options 27 27 ---------------- 28 28 29 - Use ``CONFIGURATION_DEFAULT_OPTIONS`` to set the default options:: 29 + Use ``SIMPLE_OPTIONS_CONFIGURATION_DEFAULT`` to set the default options:: 30 30 31 - CONFIGURATION_DEFAULT_OPTIONS = { 31 + SIMPLE_OPTIONS_CONFIGURATION_DEFAULT = { 32 32 "sold_out": { 33 33 "value": 0, 34 34 "type": INT,
+14 -11
options/__init__.py
··· 1 - import six 2 - from django import get_version 3 - from django.utils.translation import ugettext_lazy as _ 4 - 5 - 6 - FLOAT, INT, STRING = (0, 1, 2) 7 - TYPE_CHOICES = ((FLOAT, _("Float")), (INT, _("Integer")), (STRING, _("String"))) 8 - CONVERTER = {INT: int, FLOAT: float, STRING: six.text_type} 1 + """Simple app to add configuration options to a Django project.""" 2 + from options.constants import FLOAT, INT, STRING, FILE, TYPE_CHOICES, CONVERTER 3 + from options.helpers import get_option_model, get_user_option_model 9 4 10 5 default_app_config = "options.apps.ConfigurationsConfig" 11 6 12 - VERSION = (1, 2, 0, "final", 0) 13 - 14 - __version__ = get_version(VERSION) 7 + __all__ = [ 8 + "get_option_model", 9 + "get_user_option_model", 10 + "FLOAT", 11 + "INT", 12 + "STRING", 13 + "FILE", 14 + "TYPE_CHOICES", 15 + "CONVERTER", 16 + ] 17 + __version__ = "2.0.0"
+5 -1
options/admin.py
··· 1 1 from django.contrib import admin 2 - from options.models import Option, UserOption 2 + 3 + from options import get_option_model, get_user_option_model 4 + 5 + Option = get_option_model() 6 + UserOption = get_user_option_model() 3 7 4 8 5 9 @admin.register(Option)
+7 -4
options/apps.py
··· 1 1 import logging 2 - import six 2 + 3 3 from django.apps import AppConfig 4 4 from django.db.models.signals import post_migrate 5 5 from django.db.utils import IntegrityError 6 + from django.utils.translation import ugettext_lazy as _ 6 7 7 - from options.settings import DEFAULT_OPTIONS 8 + from options import get_option_model 9 + from options.settings import DEFAULT_CONFIGURATION 8 10 9 11 logger = logging.getLogger(__name__) 10 12 11 13 12 14 def create_default_options(sender, **kwargs): 13 15 """Creates the defaults configuration options if they don't exists.""" 14 - from options.models import Option 16 + Option = get_option_model() 15 17 16 - for key, data in six.iteritems(DEFAULT_OPTIONS): 18 + for key, data in DEFAULT_CONFIGURATION.items(): 17 19 if not Option.objects.filter(name=key).exists(): 18 20 try: 19 21 Option.objects.create(name=key, **data) ··· 23 25 24 26 class ConfigurationsConfig(AppConfig): 25 27 name = "options" 28 + verbose_name = _("Options") 26 29 27 30 def ready(self): 28 31 """Connects signals with their managers."""
+10
options/constants.py
··· 1 + from django.utils.translation import ugettext_lazy as _ 2 + 3 + FLOAT, INT, STRING, FILE = (0, 1, 2, 3) 4 + TYPE_CHOICES = ( 5 + (FLOAT, _("Float")), 6 + (INT, _("Integer")), 7 + (STRING, _("String")), 8 + (FILE, _("File")), 9 + ) 10 + CONVERTER = {INT: int, FLOAT: float, STRING: str, FILE: str}
+77 -3
options/helpers.py
··· 1 - import six 1 + import hashlib 2 + import os 3 + import random 4 + import time 2 5 3 - from options import INT, FLOAT, STRING, CONVERTER 6 + from django.apps import apps as django_apps 7 + from django.core.exceptions import ImproperlyConfigured 8 + from django.utils.deconstruct import deconstructible 9 + from django.utils.text import slugify 10 + 11 + from options.constants import INT, FLOAT, STRING, CONVERTER 12 + from options.settings import DEFAULT_OPTION_MODEL, DEFAULT_USER_OPTION_MODEL 13 + 14 + 15 + def get_option_model(): 16 + """Return the Notification model that is active in this project.""" 17 + try: 18 + return django_apps.get_model(DEFAULT_OPTION_MODEL, require_ready=False) 19 + except ValueError: 20 + raise ImproperlyConfigured( 21 + "SIMPLE_OPTIONS_OPTION_MODEL must be of the form 'app_label.model_name'" 22 + ) 23 + except LookupError: 24 + raise ImproperlyConfigured( 25 + "SIMPLE_OPTIONS_OPTION_MODEL refers to model '%s' that has not " 26 + "been installed" % DEFAULT_OPTION_MODEL 27 + ) 28 + 29 + 30 + def get_user_option_model(): 31 + """Return the Notification model that is active in this project.""" 32 + try: 33 + return django_apps.get_model(DEFAULT_USER_OPTION_MODEL, require_ready=False) 34 + except ValueError: 35 + raise ImproperlyConfigured( 36 + "SIMPLE_OPTIONS_USER_OPTION_MODEL must be of the form " 37 + "'app_label.model_name'" 38 + ) 39 + except LookupError: 40 + raise ImproperlyConfigured( 41 + "SIMPLE_OPTIONS_USER_OPTION_MODEL refers to model '%s' that has not " 42 + "been installed" % DEFAULT_OPTION_MODEL 43 + ) 4 44 5 45 6 46 def convert_value(value, value_type): 7 47 """Converts the given value to the given type.""" 8 48 default_values = {INT: 0, FLOAT: 1.0, STRING: ""} 9 49 try: 10 - option_value = CONVERTER.get(value_type, six.text_type)(value) 50 + option_value = CONVERTER.get(value_type, str)(value) 11 51 except ValueError: 12 52 option_value = default_values.get(value_type) 13 53 return option_value 54 + 55 + 56 + @deconstructible 57 + class UploadToDir(object): 58 + """Generates a function to give to ``upload_to`` parameter in 59 + models.Fields, that generates an name for uploaded files based on ``populate_from`` 60 + attribute. 61 + """ 62 + 63 + def __init__(self, path, populate_from=None, prefix=None, random_name=False): 64 + self.path = path 65 + self.populate_from = populate_from 66 + self.random_name = random_name 67 + self.prefix = prefix 68 + 69 + def __call__(self, instance, filename): 70 + """Generates an name for an uploaded file.""" 71 + if self.populate_from is not None and not hasattr(instance, self.populate_from): 72 + raise AttributeError( 73 + "Instance hasn't {} attribute".format(self.populate_from) 74 + ) 75 + ext = filename.split(".")[-1] 76 + readable_name = slugify(filename.split(".")[0]) 77 + if self.populate_from: 78 + readable_name = slugify(getattr(instance, self.populate_from)) 79 + if self.random_name: 80 + random_name = hashlib.sha256( 81 + "{}--{}".format(time.time(), random.random()).encode("utf-8") 82 + ) 83 + readable_name = random_name.hexdigest() 84 + elif self.prefix is not None: 85 + readable_name = f"{self.prefix}{readable_name}" 86 + file_name = "{}.{}".format(readable_name, ext) 87 + return os.path.join(self.path, file_name)
+5 -3
options/managers.py
··· 1 1 from django.db import models 2 - from options.settings import DEFAULT_EXCLUDE_USER_OPTIONS 2 + 3 + from options import get_option_model 4 + from options.settings import DEFAULT_EXCLUDE_USER 3 5 4 6 5 7 class OptionManager(models.Manager): ··· 19 21 20 22 def filter_user_customizable(self): 21 23 """Returns option that the user can customize himself.""" 22 - return self.exclude(name__in=DEFAULT_EXCLUDE_USER_OPTIONS) 24 + return self.exclude(name__in=DEFAULT_EXCLUDE_USER) 23 25 24 26 def get_value(self, name, user=None, default=None): 25 27 """Gets the value with the proper type.""" 26 - from options.models import Option 28 + Option = get_option_model() 27 29 28 30 if user is None: 29 31 return Option.objects.get_value(name=name, default=default)
+96
options/migrations/0003_auto_20190827_0605.py
··· 1 + # Generated by Django 2.2.4 on 2019-08-27 11:05 2 + 3 + from django.db import migrations, models 4 + import options.helpers 5 + 6 + 7 + class Migration(migrations.Migration): 8 + 9 + dependencies = [("options", "0002_auto_20181002_0254")] 10 + 11 + operations = [ 12 + migrations.AddField( 13 + model_name="option", 14 + name="file", 15 + field=models.FileField( 16 + blank=True, 17 + null=True, 18 + upload_to=options.helpers.UploadToDir("options", random_name=True), 19 + ), 20 + ), 21 + migrations.AddField( 22 + model_name="useroption", 23 + name="file", 24 + field=models.FileField( 25 + blank=True, 26 + null=True, 27 + upload_to=options.helpers.UploadToDir("options", random_name=True), 28 + ), 29 + ), 30 + migrations.AlterField( 31 + model_name="option", 32 + name="name", 33 + field=models.CharField( 34 + db_index=True, 35 + max_length=255, 36 + unique=True, 37 + verbose_name="parameter name", 38 + ), 39 + ), 40 + migrations.AlterField( 41 + model_name="option", 42 + name="public_name", 43 + field=models.CharField( 44 + db_index=True, 45 + max_length=255, 46 + verbose_name="public name of the parameter", 47 + ), 48 + ), 49 + migrations.AlterField( 50 + model_name="option", 51 + name="type", 52 + field=models.PositiveIntegerField( 53 + choices=[(0, "Float"), (1, "Integer"), (2, "String"), (3, "File")], 54 + default=2, 55 + ), 56 + ), 57 + migrations.AlterField( 58 + model_name="option", 59 + name="value", 60 + field=models.CharField( 61 + blank=True, 62 + default=None, 63 + max_length=256, 64 + null=True, 65 + verbose_name="value", 66 + ), 67 + ), 68 + migrations.AlterField( 69 + model_name="useroption", 70 + name="public_name", 71 + field=models.CharField( 72 + db_index=True, 73 + max_length=255, 74 + verbose_name="public name of the parameter", 75 + ), 76 + ), 77 + migrations.AlterField( 78 + model_name="useroption", 79 + name="type", 80 + field=models.PositiveIntegerField( 81 + choices=[(0, "Float"), (1, "Integer"), (2, "String"), (3, "File")], 82 + default=2, 83 + ), 84 + ), 85 + migrations.AlterField( 86 + model_name="useroption", 87 + name="value", 88 + field=models.CharField( 89 + blank=True, 90 + default=None, 91 + max_length=256, 92 + null=True, 93 + verbose_name="value", 94 + ), 95 + ), 96 + ]
+20 -12
options/models.py
··· 1 - import six 2 1 from django.conf import settings 3 2 from django.core.exceptions import ValidationError 4 3 from django.db import models 5 4 from django.utils.translation import ugettext_lazy as _ 6 5 7 - from options import STRING, TYPE_CHOICES, CONVERTER 8 - from options.helpers import convert_value 6 + from options.constants import STRING, TYPE_CHOICES, CONVERTER, FILE 7 + from options.helpers import convert_value, UploadToDir 9 8 from options.managers import OptionManager, UserOptionManager 10 9 11 10 ··· 13 12 """Base model for system options and configurations.""" 14 13 15 14 name = models.CharField( 16 - verbose_name=_("Parameter"), max_length=255, unique=True, db_index=True 15 + verbose_name=_("parameter name"), max_length=255, unique=True, db_index=True 17 16 ) 18 17 public_name = models.CharField( 19 - verbose_name=_("Public name of the parameter"), 18 + verbose_name=_("public name of the parameter"), 20 19 max_length=255, 21 20 unique=False, 22 21 db_index=True, 23 22 ) 24 23 type = models.PositiveIntegerField(choices=TYPE_CHOICES, default=STRING) 25 24 value = models.CharField( 26 - null=True, blank=True, default=None, max_length=256, verbose_name=_("Value") 25 + null=True, blank=True, default=None, max_length=256, verbose_name=_("value") 26 + ) 27 + file = models.FileField( 28 + upload_to=UploadToDir("options", random_name=True), null=True, blank=True 27 29 ) 28 30 is_list = models.BooleanField(default=False) 29 31 ··· 31 33 abstract = True 32 34 33 35 def __str__(self): 34 - return "%s" % self.public_name 36 + return f"{self.public_name}" 35 37 36 38 def get_value(self): 37 39 """Gets the value with the proper type. If the type is not 38 40 valid it would return the default value for the field, to avoid 39 - problems with manual database modifications""" 40 - 41 + problems with manual database modifications. 42 + """ 43 + # If the option is a file, returns the URL of the file 44 + if self.type == FILE and self.file is not None: 45 + return self.file.url 41 46 if not self.is_list: 42 47 return convert_value(self.value, self.type) 43 48 else: 44 49 values = self.value.split(",") 45 - return [convert_value(self.type, value) for value in values] 50 + return [convert_value(type, value) for value in values] 46 51 47 52 def clean(self): 48 53 """Calls to the converter to check the type conversion. Added exception 49 - for lists, to check all values.""" 54 + for lists, to check all values. 55 + """ 50 56 try: 51 57 values = [self.value] if not self.is_list else self.value.split(",") 52 - [CONVERTER.get(self.type, six.text_type)(value) for value in values] 58 + [CONVERTER.get(self.type, str)(value) for value in values] 53 59 except ValueError: 54 60 raise ValidationError(_("Invalid value for this type.")) 55 61 ··· 65 71 66 72 class Meta: 67 73 ordering = ["public_name"] 74 + swappable = "SIMPLE_OPTIONS_OPTION_MODEL" 68 75 69 76 70 77 class UserOption(BaseOption): ··· 80 87 class Meta: 81 88 unique_together = ["user", "name"] 82 89 ordering = ["public_name"] 90 + swappable = "SIMPLE_OPTIONS_USER_OPTION_MODEL"
+7 -4
options/rest_framework/serializers.py
··· 1 1 from django.utils.translation import ugettext_lazy as _ 2 2 from rest_framework import serializers 3 3 4 - from options.settings import DEFAULT_EXCLUDE_USER_OPTIONS 5 - from options.models import Option, UserOption 4 + from options import get_option_model, get_user_option_model 5 + from options.settings import DEFAULT_EXCLUDE_USER 6 + 7 + Option = get_option_model() 8 + UserOption = get_user_option_model() 6 9 7 10 8 11 class OptionSerializer(serializers.ModelSerializer): 9 12 class Meta: 10 13 model = Option 11 - fields = ["id", "name", "public_name", "type", "value", "is_list"] 14 + fields = ["id", "name", "public_name", "type", "value", "file", "is_list"] 12 15 13 16 14 17 class UserOptionSerializer(OptionSerializer): ··· 17 20 18 21 def validate_name(self, value): 19 22 """Checks if the name is in DEFAULT_EXCLUDE_USER_OPTIONS.""" 20 - if value in DEFAULT_EXCLUDE_USER_OPTIONS: 23 + if value in DEFAULT_EXCLUDE_USER: 21 24 raise serializers.ValidationError( 22 25 _("The name in the option can't be handle by the user.") 23 26 )
+4 -1
options/rest_framework/viewsets.py
··· 1 1 from rest_framework import viewsets 2 2 from rest_framework.permissions import IsAdminUser, IsAuthenticated 3 3 4 - from options.models import Option, UserOption 4 + from options import get_option_model, get_user_option_model 5 5 from options.rest_framework.serializers import OptionSerializer, UserOptionSerializer 6 + 7 + Option = get_option_model() 8 + UserOption = get_user_option_model() 6 9 7 10 8 11 class OptionViewSet(viewsets.ModelViewSet):
+19 -3
options/settings.py
··· 1 1 from django.conf import settings 2 2 3 + # Needed to build and publish with Flit 4 + # ------------------------------------------------------------------------------ 5 + SECRET_KEY = "snitch" 6 + 7 + # Specific project configuration 8 + # ------------------------------------------------------------------------------ 3 9 # Set on settings the default options for this project. These will be created 4 10 # on the post migrate signal handler. 5 11 # Sample: 6 12 # 7 - # CONFIGURATION_DEFAULT_OPTIONS = { 13 + # SIMPLE_OPTIONS_CONFIGURATION = { 8 14 # "sold_out": { 9 15 # "value": 0, 10 16 # "type": INT, ··· 12 18 # }, 13 19 # } 14 20 # 15 - DEFAULT_OPTIONS = getattr(settings, "CONFIGURATION_DEFAULT_OPTIONS", {}) 21 + DEFAULT_CONFIGURATION = getattr(settings, "SIMPLE_OPTIONS_CONFIGURATION", {}) 16 22 17 23 # Set the list of options that the user can't customize. 18 - DEFAULT_EXCLUDE_USER_OPTIONS = getattr(settings, "EXCLUDE_USER_OPTIONS", tuple()) 24 + DEFAULT_EXCLUDE_USER = getattr(settings, "SIMPLE_OPTIONS_EXCLUDE_USER", tuple()) 25 + 26 + # Swappable Option model 27 + DEFAULT_OPTION_MODEL = getattr( 28 + settings, "SIMPLE_OPTIONS_OPTION_MODEL", "options.Option" 29 + ) 30 + 31 + # Swappable UserOption model 32 + DEFAULT_USER_OPTION_MODEL = getattr( 33 + settings, "SIMPLE_OPTIONS_USER_OPTION_MODEL", "options.UserOption" 34 + )
+49
pyproject.toml
··· 1 + [build-system] 2 + requires = ["flit"] 3 + build-backend = "flit.buildapi" 4 + 5 + [tool.flit.metadata] 6 + dist-name = "django-simple-options" 7 + module = "options" 8 + author = "Marcos Gabarda" 9 + author-email = "hey@marcosgabarda.com" 10 + home-page = "https://github.com/marcosgabarda/django-simple-options" 11 + classifiers = [ 12 + "Environment :: Web Environment", 13 + "Framework :: Django", 14 + "Intended Audience :: Developers", 15 + "License :: OSI Approved :: MIT License", 16 + "Operating System :: OS Independent", 17 + "Programming Language :: Python :: 3", 18 + "Topic :: Utilities", 19 + ] 20 + requires = [ 21 + "django" 22 + ] 23 + description-file = "README.rst" 24 + requires-python=">=3.6" 25 + 26 + [tool.flit.metadata.requires-extra] 27 + test = [ 28 + "pytest", 29 + "pytest-django", 30 + "pytest-cov", 31 + "factory_boy", 32 + ] 33 + doc = ["sphinx"] 34 + 35 + [tool.tox] 36 + legacy_tox_ini = """ 37 + [tox] 38 + skipsdist=True 39 + envlist = py36,py37 40 + 41 + [testenv] 42 + deps = 43 + django 44 + pytest 45 + pytest-django 46 + pytest-cov 47 + factory_boy 48 + commands = pytest 49 + """
-2
requirements.txt
··· 1 - django>=1.9 2 - six>=1.0
-6
requirements_test.txt
··· 1 - coverage==4.3.4 2 - mock>=1.0.1 3 - flake8>=2.1.0 4 - tox>=1.7.0 5 - codecov>=2.0.0 6 - six>=1.0
-22
runtests.py
··· 1 - import os 2 - import sys 3 - 4 - import django 5 - from django.conf import settings 6 - from django.test.utils import get_runner 7 - 8 - 9 - def run_tests(*test_args): 10 - if not test_args: 11 - test_args = ["tests"] 12 - 13 - os.environ["DJANGO_SETTINGS_MODULE"] = "tests.settings" 14 - django.setup() 15 - TestRunner = get_runner(settings) 16 - test_runner = TestRunner() 17 - failures = test_runner.run_tests(test_args) 18 - sys.exit(bool(failures)) 19 - 20 - 21 - if __name__ == "__main__": 22 - run_tests(*sys.argv[1:])
-5
setup.cfg
··· 1 - [metadata] 2 - description-file = README.rst 3 - 4 - [wheel] 5 - universal=1
-47
setup.py
··· 1 - import os 2 - 3 - from setuptools import setup 4 - 5 - with open(os.path.join(os.path.dirname(__file__), "README.rst")) as readme: 6 - README = readme.read() 7 - 8 - with open(os.path.join(os.path.dirname(__file__), "HISTORY.rst")) as history: 9 - HISTORY = history.read().replace(".. :changelog:", "") 10 - 11 - # Allow setup.py to be run from any path 12 - os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir))) 13 - 14 - # Dynamically calculate the version based on belt.VERSION. 15 - version = __import__("options").__version__ 16 - 17 - 18 - setup( 19 - name="django-simple-options", 20 - version=version, 21 - packages=[ 22 - "options.management.commands", 23 - "options.migrations", 24 - "options.rest_framework", 25 - "options", 26 - ], 27 - include_package_data=True, 28 - license="MIT License", 29 - description="Simple app to add configuration options to a Django project", 30 - long_description=README + "\n\n" + HISTORY, 31 - url="https://github.com/marcosgabarda/django-options", 32 - author="Marcos Gabarda", 33 - author_email="hey@marcosgabarda.com", 34 - classifiers=[ 35 - "Development Status :: 3 - Alpha", 36 - "Environment :: Web Environment", 37 - "Framework :: Django", 38 - "Intended Audience :: Developers", 39 - "License :: OSI Approved :: MIT License", 40 - "Operating System :: OS Independent", 41 - "Programming Language :: Python", 42 - "Programming Language :: Python :: 2", 43 - "Programming Language :: Python :: 3", 44 - "Topic :: Utilities", 45 - ], 46 - install_requires=["django>=1.9", "six>=1.0"], 47 - )
+3 -9
tests/settings.py
··· 31 31 ) 32 32 } 33 33 34 - from options import STRING 35 - 36 - CONFIGURATION_DEFAULT_OPTIONS = { 37 - "default_option": { 38 - "public_name": "Default Option", 39 - "type": STRING, 40 - "value": "default", 41 - } 34 + SIMPLE_OPTIONS_CONFIGURATION = { 35 + "default_option": {"public_name": "Default Option", "type": 2, "value": "default"} 42 36 } 43 - EXCLUDE_USER_OPTIONS = ["secret_option"] 37 + SIMPLE_OPTIONS_EXCLUDE_USER = ["secret_option"]
+10 -13
tests/test_options.py
··· 1 - from django.test import override_settings, TestCase 2 - from django.contrib.auth.models import User 3 - from rest_framework.test import APITestCase 1 + from django.test import TestCase 4 2 from rest_framework import status 3 + from rest_framework.test import APITestCase 5 4 6 - from tests.test_settings import TEST_SETTINGS 5 + from options import INT, FLOAT, STRING, get_option_model, get_user_option_model 7 6 from tests.factories import UserFactory, UserOptionFactory, OptionFactory 8 - from options import INT, FLOAT, STRING 9 - from options.models import Option, UserOption 7 + 8 + Option = get_option_model() 9 + UserOption = get_user_option_model() 10 10 11 11 12 - @override_settings(**TEST_SETTINGS) 13 12 class OptionTests(TestCase): 14 13 def test_default_options(self): 15 - value = Option.objects.get_value("default_option", default="ohter") 14 + value = Option.objects.get_value("default_option", default="other") 16 15 self.assertEqual("default", value) 17 16 18 17 def test_int_conversion_options(self): 19 18 name = "int_option" 20 - option = OptionFactory(name=name, value="42", type=INT) 19 + OptionFactory(name=name, value="42", type=INT) 21 20 value = Option.objects.get_value(name) 22 21 self.assertIsInstance(value, int) 23 22 self.assertEqual(42, value) 24 23 25 24 def test_str_conversion_options(self): 26 25 name = "string_option" 27 - option = OptionFactory(name=name, value="42") 26 + OptionFactory(name=name, value="42") 28 27 value = Option.objects.get_value(name) 29 28 self.assertIsInstance(value, str) 30 29 self.assertEqual("42", value) 31 30 32 31 def test_float_conversion_options(self): 33 32 name = "string_option" 34 - option = OptionFactory(name=name, value="42.5", type=FLOAT) 33 + OptionFactory(name=name, value="42.5", type=FLOAT) 35 34 value = Option.objects.get_value(name) 36 35 self.assertIsInstance(value, float) 37 36 self.assertAlmostEqual(42.5, value) 38 37 39 38 40 - @override_settings(**TEST_SETTINGS) 41 39 class UserOptionTests(TestCase): 42 40 def test_custom_user_options(self): 43 41 user = UserFactory() ··· 50 48 self.assertEqual(expected_value, value) 51 49 52 50 53 - @override_settings(**TEST_SETTINGS) 54 51 class OptionAPITests(APITestCase): 55 52 def test_list_options(self): 56 53 admin = UserFactory(is_staff=True)
-5
tests/test_settings.py
··· 1 - from tests import settings as _settings 2 - 3 - TEST_SETTINGS = dict( 4 - (k, getattr(_settings, k)) for k in dir(_settings) if k == k.upper() 5 - )