├── .gitignore ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── README.md ├── custom_field ├── __init__.py ├── admin.py ├── custom_field.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_auto_20150119_2032.py │ ├── 0003_customfield_mask.py │ ├── 0004_django2_upgrade.py │ └── __init__.py ├── models.py ├── templates │ └── admin │ │ └── includes │ │ └── custom_field_fieldset.html └── tests.py ├── manage.py ├── screencaps ├── customfield1.png ├── customfield2.png ├── customfield3.png ├── customfield4.png └── customfield5.png ├── setup.py ├── test_settings.py ├── test_urls.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | 3 | # Packages 4 | *.egg 5 | *.egg-info 6 | dist 7 | build 8 | eggs 9 | parts 10 | bin 11 | var 12 | sdist 13 | develop-eggs 14 | .installed.cfg 15 | 16 | # Installer logs 17 | pip-log.txt 18 | 19 | # Unit test / coverage reports 20 | .coverage 21 | .tox 22 | 23 | #Translations 24 | *.mo 25 | 26 | #Mr Developer 27 | .mr.developer.cfg 28 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | sudo: false 4 | 5 | 6 | env: 7 | - TOX_ENV=py27-django111 8 | - TOX_ENV=py36-django111 9 | - TOX_ENV=py36-django20 10 | - TOX_ENV=py36-django21 11 | - TOX_ENV=py36-django22 12 | 13 | 14 | install: 15 | - pip install tox 16 | 17 | script: 18 | - tox -e $TOX_ENV 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Wills Ward 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include custom_field/templates * 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | django-custom-field 2 | =================== 3 | 4 | Allow end users to create easy (but slower to work with) fields attached to any Django model. Includes support so fields show up in the admin interface and helper functions that make it easy to access any custom fields programmatically. 5 | 6 | Does not alter sql when adding a new field. Does not allow you use to custom fields in django ORM mymodel.objects.filter(custom_field_x=Whatever) 7 | 8 | [![Build Status](https://travis-ci.org/willseward/django-custom-field.png?branch=master)](https://travis-ci.org/willseward/django-custom-field) 9 | 10 | # Installation 11 | 12 | 1. pip install django-custom-field 13 | 2. Add 'custom_field' to settings.INSTALLED_APPS 14 | 3. Optional: For shortcut methods to get and set custom fields, extend models you want to use it with like: 15 | ``` 16 | from custom_field.custom_field import CustomFieldModel 17 | class MyModel(CustomFieldModel): 18 | ``` 19 | 4. Optional: To have admin work with custom fields include this class: 20 | ``` 21 | from custom_field.custom_field import CustomFieldAdmin 22 | class MyModelAdmin(CustomFieldAdmin): 23 | ``` 24 | 25 | #Schooldriver Usage 26 | The custom fields option allows schools additional flexibility with regards to storing information to a particular model (student, applicants, student worker, etc.). 27 | 28 | Under Admin > Custom Fields, the custom fields creation screen displays: 29 | ![Alt text](https://raw.github.com/willseward/django-custom-field/master/screencaps/customfield1.png) 30 | 31 | Required fields: 32 | 33 | Name- Refers to the name of the custom field. Note: this name will be visible to other users 34 | 35 | Content Type- Designates which model to affix the custom field to. (Student, Alumni, Applicant, Faculty, etc.) 36 | 37 | Field Type- Text, Integer, and Boolean- select the type of custom field. 38 | 39 | NB: Boolean refers to a simple checkbox. The box can be checked or unchecked when created based on preference. Leave blank for unchecked and enter "1" for checked under the "Default Value" in the creation screen shown above. 40 | 41 | #Example 42 | This example will demonstrate custom fields for a school that would like to attach additional information to the incoming applicants page. 43 | 44 | In sum, the school wants three additional items of information: who an applicant was referred by, whether or not financial aid is needed, and how many times the applicant has visited the school. 45 | 46 | Accordingly, three custom fields will need to be generated under Admin > Custom Fields > Add, formatted as follows: 47 | 48 | Who the applicant was referred by: 49 | 50 | 51 | ![Alt text](https://raw.github.com/willseward/django-custom-field/master/screencaps/customfield2.png) 52 | 53 | Number of visits to the school: 54 | 55 | 56 | ![Alt text](https://raw.github.com/willseward/django-custom-field/master/screencaps/customfield3.png) 57 | 58 | Whether or not financial aid is needed: 59 | 60 | 61 | ![Alt text](https://raw.github.com/willseward/django-custom-field/master/screencaps/customfield4.png) 62 | 63 | The end result will look like the image below where end users will see the custom fields attached to the Applicant model and can then enter the appropiate information 64 | 65 | 66 | ![Alt text](https://raw.github.com/willseward/django-custom-field/master/screencaps/customfield5.png) 67 | 68 | # Credit 69 | 70 | This project was originally designed and maintained by [burke-software](https://github.com/burke-software). After a period of stagnation, it was graciously handed over to me for upkeep. Please give due respect and gratitude for the inception of this project to David. 71 | -------------------------------------------------------------------------------- /custom_field/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willseward/django-custom-field/c49361cd38ee20b4f3fea7365dbc3e16d29a4f4b/custom_field/__init__.py -------------------------------------------------------------------------------- /custom_field/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import CustomField 4 | from .custom_field import CustomFieldAdmin 5 | 6 | 7 | class CFAdmin(CustomFieldAdmin): 8 | list_display = ("content_type", "name") 9 | list_filter = ("content_type",) 10 | search_fields = ("content_type__name", "name") 11 | 12 | 13 | admin.site.register(CustomField, CFAdmin) 14 | -------------------------------------------------------------------------------- /custom_field/custom_field.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.db import IntegrityError 3 | from django.contrib.contenttypes.models import ContentType 4 | from django.contrib.admin import ModelAdmin 5 | from django.core.exceptions import ObjectDoesNotExist 6 | from django.contrib.contenttypes import admin 7 | 8 | from .models import CustomField, CustomFieldValue 9 | 10 | 11 | class Callable: 12 | def __init__(self, anycallable): 13 | self.__call__ = anycallable 14 | 15 | 16 | class CustomFieldModel(object): 17 | """ 18 | Abstract class adds some helper functions a Model 19 | """ 20 | 21 | @property 22 | def get_custom_fields(self): 23 | """ Return a list of custom fields for this model """ 24 | return CustomField.objects.filter( 25 | content_type=ContentType.objects.get_for_model(self) 26 | ) 27 | 28 | def get_model_custom_fields(self): 29 | """ Return a list of custom fields for this model, directly callable 30 | without an instance. Use like Foo.get_model_custom_fields(Foo) 31 | """ 32 | return CustomField.objects.filter( 33 | content_type=ContentType.objects.get_for_model(self) 34 | ) 35 | 36 | get_model_custom_fields = Callable(get_model_custom_fields) 37 | 38 | def get_custom_field(self, field_name): 39 | """ Get a custom field object for this model 40 | field_name - Name of the custom field you want. 41 | """ 42 | content_type = ContentType.objects.get_for_model(self) 43 | return CustomField.objects.get(content_type=content_type, name=field_name) 44 | 45 | def get_custom_value(self, field_name): 46 | """ Get a value for a specified custom field 47 | field_name - Name of the custom field you want. 48 | """ 49 | custom_field = self.get_custom_field(field_name) 50 | return CustomFieldValue.objects.get_or_create( 51 | field=custom_field, object_id=self.id 52 | )[0].value 53 | 54 | def set_custom_value(self, field_name, value): 55 | """ Set a value for a specified custom field 56 | field_name - Name of the custom field you want. 57 | value - Value to set it to 58 | """ 59 | custom_field = self.get_custom_field(field_name) 60 | custom_value = CustomFieldValue.objects.get_or_create( 61 | field=custom_field, object_id=self.id 62 | )[0] 63 | custom_value.value = value 64 | custom_value.save() 65 | 66 | 67 | class CustomFieldValueForm(forms.ModelForm): 68 | def __init__(self, *args, **kwargs): 69 | super(CustomFieldValueForm, self).__init__(*args, **kwargs) 70 | if self.instance: 71 | try: 72 | self.fields["value"] = self.instance.get_form_field() 73 | except ObjectDoesNotExist: 74 | pass 75 | 76 | 77 | class CustomInline(admin.GenericTabularInline): 78 | model = CustomFieldValue 79 | form = CustomFieldValueForm 80 | can_delete = False 81 | readonly_fields = ("field",) 82 | fields = ("field", "value") 83 | extra = 0 84 | max_num = 0 85 | 86 | def has_change_permission(self, request, obj=None): 87 | """ Need to always allow changing custom values """ 88 | return True 89 | 90 | 91 | class CustomFieldAdmin(ModelAdmin): 92 | """ Abstract class addes functionality to deal with custom fields in 93 | Django admin. 94 | """ 95 | 96 | inlines = () 97 | 98 | def change_view(self, request, object_id, *args, **kwargs): 99 | inlines = list(self.inlines) 100 | if CustomInline not in inlines: 101 | inlines.append(CustomInline) 102 | self.inlines = inlines 103 | return super(CustomFieldAdmin, self).change_view( 104 | request, object_id, *args, **kwargs 105 | ) 106 | 107 | def get_form(self, request, obj=None, **kwargs): 108 | if obj: 109 | content_type = ContentType.objects.get_for_model(obj) 110 | custom_fields = CustomField.objects.filter(content_type=content_type) 111 | for custom_field in custom_fields: 112 | try: 113 | field_value, created = CustomFieldValue.objects.get_or_create( 114 | content_type=content_type, object_id=obj.id, field=custom_field, 115 | ) 116 | except IntegrityError: 117 | # This can happen because content_type is really a 118 | # cache field and didn't always exist 119 | field_value, created = CustomFieldValue.objects.get_or_create( 120 | object_id=obj.id, field=custom_field, 121 | ) 122 | field_value.content_type = content_type 123 | field_value.save() 124 | if created: 125 | if field_value.field.default_value: 126 | field_value.value = field_value.field.default_value 127 | field_value.save() 128 | return super(CustomFieldAdmin, self).get_form(request, obj, **kwargs) 129 | -------------------------------------------------------------------------------- /custom_field/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ("contenttypes", "0001_initial"), 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name="CustomField", 16 | fields=[ 17 | ( 18 | "id", 19 | models.AutoField( 20 | verbose_name="ID", 21 | serialize=False, 22 | auto_created=True, 23 | primary_key=True, 24 | ), 25 | ), 26 | ("name", models.CharField(max_length=75)), 27 | ( 28 | "field_type", 29 | models.CharField( 30 | default=b"t", 31 | max_length=1, 32 | choices=[ 33 | (b"t", b"Text"), 34 | (b"i", b"Integer"), 35 | (b"b", b"Boolean (Yes/No)"), 36 | ], 37 | ), 38 | ), 39 | ( 40 | "default_value", 41 | models.CharField( 42 | help_text=b"You may leave blank. For Boolean use True or False", 43 | max_length=255, 44 | blank=True, 45 | ), 46 | ), 47 | ( 48 | "content_type", 49 | models.ForeignKey( 50 | to="contenttypes.ContentType", on_delete=models.CASCADE 51 | ), 52 | ), 53 | ], 54 | options={}, 55 | bases=(models.Model,), 56 | ), 57 | migrations.CreateModel( 58 | name="CustomFieldValue", 59 | fields=[ 60 | ( 61 | "id", 62 | models.AutoField( 63 | verbose_name="ID", 64 | serialize=False, 65 | auto_created=True, 66 | primary_key=True, 67 | ), 68 | ), 69 | ("value", models.CharField(max_length=255, null=True, blank=True)), 70 | ("object_id", models.PositiveIntegerField()), 71 | ( 72 | "content_type", 73 | models.ForeignKey( 74 | blank=True, 75 | to="contenttypes.ContentType", 76 | null=True, 77 | on_delete=models.CASCADE, 78 | ), 79 | ), 80 | ( 81 | "field", 82 | models.ForeignKey( 83 | related_name="instance", 84 | to="custom_field.CustomField", 85 | on_delete=models.CASCADE, 86 | ), 87 | ), 88 | ], 89 | options={}, 90 | bases=(models.Model,), 91 | ), 92 | migrations.AlterUniqueTogether( 93 | name="customfieldvalue", unique_together=set([("field", "object_id")]), 94 | ), 95 | migrations.AlterUniqueTogether( 96 | name="customfield", unique_together=set([("name", "content_type")]), 97 | ), 98 | ] 99 | -------------------------------------------------------------------------------- /custom_field/migrations/0002_auto_20150119_2032.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ("custom_field", "0001_initial"), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name="customfield", 16 | name="field_choices", 17 | field=models.CharField( 18 | help_text=b"List the choices you want displayed, seperated by commas. This is only valid for Dropdown, Multiple, and Checkbox field types", 19 | max_length=2000, 20 | blank=True, 21 | ), 22 | preserve_default=True, 23 | ), 24 | migrations.AddField( 25 | model_name="customfield", 26 | name="is_required", 27 | field=models.BooleanField(default=False), 28 | preserve_default=True, 29 | ), 30 | migrations.AlterField( 31 | model_name="customfield", 32 | name="default_value", 33 | field=models.CharField( 34 | help_text=b"You may leave blank. For Boolean use True or False", 35 | max_length=5000, 36 | blank=True, 37 | ), 38 | preserve_default=True, 39 | ), 40 | migrations.AlterField( 41 | model_name="customfield", 42 | name="field_type", 43 | field=models.CharField( 44 | default=b"t", 45 | max_length=1, 46 | choices=[ 47 | (b"t", b"Text"), 48 | (b"a", b"Large Text Field"), 49 | (b"i", b"Integer"), 50 | (b"f", b"Floating point decimal"), 51 | (b"b", b"Boolean (Yes/No)"), 52 | (b"m", b"Dropdown Choices"), 53 | (b"d", b"Date"), 54 | ], 55 | ), 56 | preserve_default=True, 57 | ), 58 | migrations.AlterField( 59 | model_name="customfield", 60 | name="name", 61 | field=models.CharField(max_length=150), 62 | preserve_default=True, 63 | ), 64 | migrations.AlterField( 65 | model_name="customfieldvalue", 66 | name="value", 67 | field=models.CharField(max_length=5000, null=True, blank=True), 68 | preserve_default=True, 69 | ), 70 | ] 71 | -------------------------------------------------------------------------------- /custom_field/migrations/0003_customfield_mask.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ("custom_field", "0002_auto_20150119_2032"), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name="customfield", 16 | name="mask", 17 | field=models.CharField( 18 | help_text=b"You may leave blank. For user Jquery Mask, ex: '00/00/0000' for date.", 19 | max_length=5000, 20 | blank=True, 21 | ), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /custom_field/migrations/0004_django2_upgrade.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.1 on 2019-06-11 13:35 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("custom_field", "0003_customfield_mask"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="customfield", 15 | name="default_value", 16 | field=models.CharField( 17 | blank=True, 18 | help_text="You may leave blank. For Boolean use True or False", 19 | max_length=5000, 20 | ), 21 | ), 22 | migrations.AlterField( 23 | model_name="customfield", 24 | name="field_choices", 25 | field=models.CharField( 26 | blank=True, 27 | help_text="List the choices you want displayed, seperated by commas. This is only valid for Dropdown, Multiple, and Checkbox field types", 28 | max_length=2000, 29 | ), 30 | ), 31 | migrations.AlterField( 32 | model_name="customfield", 33 | name="field_type", 34 | field=models.CharField( 35 | choices=[ 36 | ("t", "Text"), 37 | ("a", "Large Text Field"), 38 | ("i", "Integer"), 39 | ("f", "Floating point decimal"), 40 | ("b", "Boolean (Yes/No)"), 41 | ("m", "Dropdown Choices"), 42 | ("d", "Date"), 43 | ("h", "Date Time"), 44 | ], 45 | default="t", 46 | max_length=1, 47 | ), 48 | ), 49 | migrations.AlterField( 50 | model_name="customfield", 51 | name="mask", 52 | field=models.CharField( 53 | blank=True, 54 | help_text="You may leave blank. For user Jquery Mask, ex: '00/00/0000' for date.", 55 | max_length=5000, 56 | ), 57 | ), 58 | ] 59 | -------------------------------------------------------------------------------- /custom_field/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willseward/django-custom-field/c49361cd38ee20b4f3fea7365dbc3e16d29a4f4b/custom_field/migrations/__init__.py -------------------------------------------------------------------------------- /custom_field/models.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from django import forms 4 | from django.contrib.contenttypes import fields 5 | from django.contrib.contenttypes.models import ContentType 6 | from django.db import models 7 | from django.utils.encoding import python_2_unicode_compatible 8 | 9 | if sys.version < "3": 10 | text_type = unicode 11 | else: 12 | text_type = str 13 | 14 | 15 | @python_2_unicode_compatible 16 | class CustomField(models.Model): 17 | """ 18 | A field abstract -- it describe what the field is. There are one of these 19 | for each custom field the user configures. 20 | """ 21 | 22 | name = models.CharField(max_length=150) 23 | content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) 24 | field_type = models.CharField( 25 | max_length=1, 26 | choices=( 27 | ("t", "Text"), 28 | ("a", "Large Text Field"), 29 | ("i", "Integer"), 30 | ("f", "Floating point decimal"), 31 | ("b", "Boolean (Yes/No)"), 32 | ("m", "Dropdown Choices"), 33 | ("d", "Date"), 34 | ("h", "Date Time"), 35 | ), 36 | default="t", 37 | ) 38 | default_value = models.CharField( 39 | max_length=5000, 40 | blank=True, 41 | help_text="You may leave blank. For Boolean use True or False", 42 | ) 43 | is_required = models.BooleanField(default=False) 44 | mask = models.CharField( 45 | max_length=5000, 46 | blank=True, 47 | help_text="You may leave blank. For user Jquery Mask, ex: '00/00/0000' for date.", 48 | ) 49 | field_choices = models.CharField( 50 | max_length=2000, 51 | blank=True, 52 | help_text="List the choices you want displayed, seperated by commas. " 53 | "This is only valid for Dropdown, Multiple, and Checkbox field types", 54 | ) 55 | 56 | def get_value_for_object(self, obj): 57 | return CustomFieldValue.objects.get_or_create(field=self, object_id=obj.id)[0] 58 | 59 | def __str__(self): 60 | return self.name 61 | 62 | def get_form_field(self): 63 | universal_kwargs = { 64 | "initial": self.default_value, 65 | "required": self.is_required, 66 | } 67 | if self.field_type == "b": 68 | return forms.BooleanField(**universal_kwargs) 69 | elif self.field_type == "i": 70 | return forms.IntegerField(**universal_kwargs) 71 | elif self.field_type == "f": 72 | return forms.FloatField(**universal_kwargs) 73 | elif self.field_type == "a": 74 | return forms.CharField(widget=forms.Textarea, **universal_kwargs) 75 | elif self.field_type == "m": 76 | choices = self.field_choices.split(",") 77 | if self.is_required is True: 78 | select_choices = () 79 | else: 80 | select_choices = (("", "---------"),) 81 | for choice in choices: 82 | select_choices = select_choices + ((choice, choice),) 83 | return forms.ChoiceField(choices=select_choices, **universal_kwargs) 84 | elif self.field_type == "d": 85 | return forms.DateField(**universal_kwargs) 86 | elif self.field_type == "h": 87 | return forms.DateTimeField(**universal_kwargs) 88 | return forms.CharField(**universal_kwargs) 89 | 90 | class Meta: 91 | unique_together = ("name", "content_type") 92 | 93 | 94 | @python_2_unicode_compatible 95 | class CustomFieldValue(models.Model): 96 | """ 97 | A field instance -- contains the actual data. There are many of these, for 98 | each value that corresponds to a CustomField for a given model. 99 | """ 100 | 101 | field = models.ForeignKey( 102 | CustomField, related_name="instance", on_delete=models.CASCADE 103 | ) 104 | value = models.CharField(max_length=5000, blank=True, null=True) 105 | object_id = models.PositiveIntegerField() 106 | content_type = models.ForeignKey( 107 | ContentType, blank=True, null=True, on_delete=models.CASCADE 108 | ) 109 | content_object = fields.GenericForeignKey("content_type", "object_id") 110 | 111 | def __str__(self): 112 | return text_type(self.value) 113 | 114 | def save(self, *args, **kwargs): 115 | super(CustomFieldValue, self).save(*args, **kwargs) 116 | if not self.content_type: 117 | self.content_type = self.field.content_type 118 | self.save() 119 | 120 | def clean(self): 121 | form_field = self.get_form_field() 122 | form_field.clean(self.value) 123 | return super(CustomFieldValue, self).clean() 124 | 125 | def get_form_field(self): 126 | return self.field.get_form_field() 127 | 128 | class Meta: 129 | unique_together = ("field", "object_id") 130 | -------------------------------------------------------------------------------- /custom_field/templates/admin/includes/custom_field_fieldset.html: -------------------------------------------------------------------------------- 1 | {% spaceless %} 2 | {% if custom_form.fields %} 3 | 4 |
5 |

Custom Fields

6 | 7 | {% for field in custom_form %} 8 |
9 |
10 | {{ field.label_tag }} 11 |
12 |
13 | {{ field }} 14 |
15 |
16 | {% endfor %} 17 |
18 | 19 | {% endif %} 20 | {% endspaceless %} 21 | -------------------------------------------------------------------------------- /custom_field/tests.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import User 2 | from django.contrib.contenttypes.models import ContentType 3 | from django.core.exceptions import ValidationError 4 | from django.test import TestCase 5 | from django.urls import reverse 6 | 7 | from .models import CustomField, CustomFieldValue 8 | 9 | 10 | class CustomFieldTest(TestCase): 11 | def setUp(self): 12 | custom_field_ct = ContentType.objects.get( 13 | app_label="custom_field", model="customfield" 14 | ) 15 | self.custom_field = CustomField.objects.create( 16 | name="test_field", content_type=custom_field_ct, field_type="i", 17 | ) 18 | self.user_custom_field = CustomField.objects.create( 19 | name="test_user_field", 20 | content_type=custom_field_ct, 21 | field_type="i", 22 | default_value=42, 23 | ) 24 | user = User.objects.create_user("temporary", "temporary@gmail.com", "temporary") 25 | user.is_staff = True 26 | user.is_superuser = True 27 | user.save() 28 | self.client.login(username="temporary", password="temporary") 29 | 30 | def test_validation(self): 31 | custom_value = CustomFieldValue.objects.create( 32 | field=self.custom_field, value="5", object_id=self.custom_field.id, 33 | ) 34 | custom_value.clean() 35 | custom_value.save() 36 | self.assertEquals(custom_value.value, "5") 37 | custom_value.value = "fdsf" 38 | try: 39 | custom_value.clean() 40 | self.fail("Was able to save string as custom integer field!") 41 | except ValidationError: 42 | pass 43 | 44 | def test_admin(self): 45 | change_url = reverse("admin:custom_field_customfield_change", args=[1]) 46 | response = self.client.get(change_url) 47 | self.assertContains(response, "42") 48 | response = self.client.get(change_url) 49 | # Make sure we aren't adding it on each get 50 | self.assertContains(response, "42") 51 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /screencaps/customfield1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willseward/django-custom-field/c49361cd38ee20b4f3fea7365dbc3e16d29a4f4b/screencaps/customfield1.png -------------------------------------------------------------------------------- /screencaps/customfield2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willseward/django-custom-field/c49361cd38ee20b4f3fea7365dbc3e16d29a4f4b/screencaps/customfield2.png -------------------------------------------------------------------------------- /screencaps/customfield3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willseward/django-custom-field/c49361cd38ee20b4f3fea7365dbc3e16d29a4f4b/screencaps/customfield3.png -------------------------------------------------------------------------------- /screencaps/customfield4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willseward/django-custom-field/c49361cd38ee20b4f3fea7365dbc3e16d29a4f4b/screencaps/customfield4.png -------------------------------------------------------------------------------- /screencaps/customfield5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willseward/django-custom-field/c49361cd38ee20b4f3fea7365dbc3e16d29a4f4b/screencaps/customfield5.png -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import find_packages, setup 2 | 3 | setup( 4 | name="django-custom-field", 5 | version="3.0", 6 | author="David Burke", 7 | author_email="wward@warddevelopment.com", 8 | description=("End user custom fields for Django including contrib.admin support"), 9 | license="MIT", 10 | keywords="django admin", 11 | url="http://github.com/willseward/django-custom-field", 12 | packages=find_packages(), 13 | include_package_data=True, 14 | install_requires=["django"], 15 | classifiers=[ 16 | "Development Status :: 5 - Production/Stable", 17 | "Environment :: Web Environment", 18 | "Framework :: Django", 19 | "Programming Language :: Python", 20 | "Programming Language :: Python :: 2.7", 21 | "Programming Language :: Python :: 3.4", 22 | "Programming Language :: Python :: 3.5", 23 | "Programming Language :: Python :: 3.6", 24 | "Programming Language :: Python :: 3.7", 25 | "Programming Language :: Python :: 3.8", 26 | "Intended Audience :: Developers", 27 | "Intended Audience :: System Administrators", 28 | "License :: OSI Approved :: MIT License", 29 | ], 30 | ) 31 | -------------------------------------------------------------------------------- /test_settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | PROJECT_DIR = os.path.dirname(__file__) 4 | 5 | STATIC_URL = PROJECT_DIR + "/static/" 6 | 7 | SECRET_KEY = "thisisatestingkey" 8 | 9 | DATABASES = {"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": "testdb"}} 10 | 11 | INSTALLED_APPS = ( 12 | "custom_field", 13 | "django.contrib.auth", 14 | "django.contrib.contenttypes", 15 | "django.contrib.messages", 16 | "django.contrib.sessions", 17 | "django.contrib.sites", 18 | "django.contrib.staticfiles", 19 | "django.contrib.admin", 20 | ) 21 | 22 | TEMPLATES = [ 23 | { 24 | "BACKEND": "django.template.backends.django.DjangoTemplates", 25 | "DIRS": [], 26 | "APP_DIRS": True, 27 | "OPTIONS": { 28 | "context_processors": [ 29 | "django.contrib.auth.context_processors.auth", 30 | "django.contrib.messages.context_processors.messages", 31 | "django.template.context_processors.i18n", 32 | "django.template.context_processors.request", 33 | "django.template.context_processors.media", 34 | "django.template.context_processors.static", 35 | "django.template.context_processors.request", 36 | ] 37 | }, 38 | }, 39 | ] 40 | 41 | MIDDLEWARE = ( 42 | "django.contrib.sessions.middleware.SessionMiddleware", 43 | "django.middleware.common.CommonMiddleware", 44 | "django.middleware.csrf.CsrfViewMiddleware", 45 | "django.contrib.auth.middleware.AuthenticationMiddleware", 46 | "django.contrib.messages.middleware.MessageMiddleware", 47 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 48 | ) 49 | 50 | SITE_ID = 1 51 | 52 | ROOT_URLCONF = "test_urls" 53 | -------------------------------------------------------------------------------- /test_urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | from django.contrib import admin 3 | 4 | admin.autodiscover() 5 | 6 | urlpatterns = [ 7 | url(r"^admin/", admin.site.urls), 8 | ] 9 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27-django111, 3 | py{35,36,37}-django{20,21,22} 4 | 5 | [testenv] 6 | commands = 7 | python manage.py test 8 | 9 | deps = 10 | django111: Django>=1.11, <1.12 11 | django20: Django>=2.0,<2.1 12 | django21: Django>=2.1,<2.2 13 | django22: Django>=2.2,<2.3 14 | --------------------------------------------------------------------------------