I have a policy_category model with 16 categories.
I have a Response model to record survey answers. The model includes a field to record the respondent's 1-5 rating for each of the policy categories. (policy_1_rank..policy_16_rank, etc.)
I can't figure out how to display the CHOICES I created for each field:
Form Template
<div><b>How important are each of the following issues or policy areas to you, when you're selecting a candidate for president?</b></div>
<div><br></div>
<ul><div>{{ policy_category.object(pk=1) }}</div></ul>
# show POLICY_1_CHOICES below:
<div>{{ form.policy_1_rank }}</div>
<div><br></div>
<ul><div>{{ policy_category.object(pk=2) }}</div></ul>
# show POLICY_2_CHOICES below:
<div>{{ form.policy_2_rank }}</div>
...
Responses Model:
# Temp Survey Response
class Temporaryresponse(models.Model):
# Healthcare
POLICY_1_CHOICES = [
(1, '1: Extremely supportive of a healthcare system with ONLY private insurance'),
(2, '2: Somewhat supportive of a healthcare system with ONLY private insurance'),
(3, '3: Somewhat supportive of a healthcare system with BOTH private insurance and a public insurance option'),
(4, '4: Extremely supportive of a healthcare system with BOTH private insurance and a public insurance option'),
(5, '5: Somewhat supportive of a healthcare system with ONLY public insurance (commonly referred to as "universal healthcare")'),
(6, '6: Extremely supportive of a healthcare system with ONLY public insurance (commonly referred to as "universal healthcare")'),
]
...
# Healthcare
policy_1_rank = models.IntegerField(blank=True, default=0, choices=POLICY_1_CHOICES)
Forms.py
class NextresponseForm(ModelForm):
policy_1_rank = forms.IntegerField()
policy_2_rank = forms.IntegerField()
...
class Meta:
model = Temporaryresponse
fields = ['policy_1_rank', 'policy_2_rank',...]
EDIT: Maybe the answer below isn't working because there is a problem with my view. I'm trying to navigate from a first form page "tr.html", save the data, and send the pk of the response to "nr.html" for a second part of the survey. This is working. But is my view of "nr.html" incorrect?
views.py
# Create temporary response view - this saves and goes to nr perfectly. pk produced from saving the response shows in nr link in web browser (myapp/nr/pk# shows here perfectly.)
def tr(request):
if request.method == "POST":
form = TemporaryresponseForm(request.POST)
if form.is_valid():
tempresponse = form.save()
tempresponse.save()
return redirect('nr', pk=tempresponse.pk)
else:
form = TemporaryresponseForm()
return render(request, 'politicalexperimentpollapp/tr.html', {'form': form})
def nr(request, pk):
tempresponse = get_object_or_404(Temporaryresponse, pk=pk)
instance = Temporaryresponse.objects.get(pk=pk)
if request.method == "POST":
form = NextresponseForm(request.POST, instance=instance)
if form.is_valid():
nextresponse = form.save()
nextresponse.save()
return redirect('fr', pk=nextresponse.pk)
else:
form = NextresponseForm(instance=instance)
return render(request, 'politicalexperimentpollapp/nr.html', {'tempresponse': tempresponse}, {'form': form})
You can override your form's fields in __init__ to include a forms.ChoiceField:
class NextresponseForm(ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['policy_1_rank'] = forms.ChoiceField(
choices=self._meta.model.POLICY_1_CHOICES)
class Meta:
model = Temporaryresponse
fields = ['policy_1_rank', 'policy_2_rank',...]
Update
The easiest way to render a form object in django is to pass it as context in your view:
def some_view(request):
form = NextresponseForm(request.POST or None)
context = {'form':form}
return render(request, 'some_template.html', context)
Then you can access the form by calling form.as_ul in your template:
<ul>
{{ form.as_ul }}
</ul>
Update 2
The last line in your views is incorrect:
return render(request, 'politicalexperimentpollapp/nr.html', {'tempresponse': tempresponse}, {'form': form})
Instead this should be:
return render(request, 'politicalexperimentpollapp/nr.html', {'tempresponse': tempresponse, 'form': form})
context is a dictionary. If you need to pass multiple items to your template, you need to include multiple keys in your context dictionary, not multiple arguments.
Lastly, I'd recommend always initializing forms with request.POST or None, to avoid invalidating your form:
form = TemporaryresponseForm(request.POST or None)
This a textbook answer to a commonly asked question "Why is form.is_valid() always invalid?".
Related
I have a django.db.models.Model A whose instances are created in a rest_framework.serializers.ModelSerializer from POST requests.
Depending on the data being sent in the POST, I would like to create one of several other "addon" models, let's say B or C, which I link to the original through a django.db.models.OneToOneField:
from django.db import models
class A(models.Model):
some_field = models.CharField()
class B(models.Model):
a = models.OneToOneField(A, related_name='addon', on_delete=models.CASCADE)
class C(models.Model):
a = models.OneToOneField(A, related_name='addon', on_delete=models.CASCADE)
What I would like to is to have a serializer which validates the incoming data, including some string indicating which addon to use. The serializer then creates the model instance of A and based on this creates the addon model.
I do not want to create a utility field in model A used to determine which addon to use, I would like to create the model directly using the instance of model A and information from the POST itself.
At the same time when accessing the data through a get, I would like to return the original string used to determine which addon to use.
What I have come up with so far:
from rest_framework import serializers
str2model = {'b': B, 'c': C}
class AddonField(serializers.Field):
def to_representation(self, value):
# I completely ignore "value" as no "internal value" is set in "to_internal_value"
myvalue = self.parent.instance.addon
for addon_name, addon_class in str2model.items():
if isinstance(myvalue, addon_class):
return addon_name
def to_internal_value(self, data):
# I create the "internal value" after "A" instance is created, thus here I do nothing?
return data
class ASerializer(serializers.ModelSerializer):
some_field = serializers.CharField()
the_addon = AddonField()
def validate_the_addon(self, value): # here addon is a string
if value in str2model.keys():
return value
def create(self, validated_data):
addon_name = validated_data.pop('the_addon')
addon_class = str2model[addon]
a = super(ASerializer, self).create(validated_data)
addon_class.objects.create(a=a)
return a
class Meta:
model = A
fields = ["some_field", "the_addon"]
When testing this I get:
AttributeError: Got AttributeError when attempting to get a value for field `the_addon` on serializer `ASerializer`.
The serializer field might be named incorrectly and not match any attribute or key on the `A` instance.
Original exception text was: 'A' object has no attribute 'the_addon'.
How can I temporarily store the_addon in the serializer until the A instance has been created?
This is how I would typically approach it
# Serializer
class ASerializer(serializers.Serializer):
some_field = serializers.CharField()
addon_b = serializers.CharField(required=False, allow_null=True)
addon_c = serializers.CharField(required=False, allow_null=True)
def create(self, validated_data):
addon_b = validated_data.pop('addon_b')
addon_c = validated_data.pop('addon_c')
a = A.objects.create(some_field=validated_data['some_field'])
if addon_b:
B.objects.create(a=a)
if addon_c:
C.objects.create(a=a)
return a
You can do other validations if necessary.
class TestAPIView01(generics.CreateAPIView):
permission_classes = {}
serializer_class = serializers.ASerializer
queryset = A.objects.all()
Also, look at the related_name on B and C you may want to consider making them different, as that might throw an error in the future. Cheers
This question or many like it has been asked multiple times but for some reason I am unable to find the answer.
I do have this working to an extent in the way that if you go on the api pages, it renders, creates and updates without problem. The issue is displaying a field (title) from the nested object instead of just the primary key on the front end.
Some background before getting into the code:
Races is a finite list (e.g. Race1, Race2, Race3) and the front end does not have the ability to add more.
Cards is not finite, but each card must link to an existing Race (this currently does so by Primary Key).
The front end should display the card_text and race title of the linked race.
It also has the ability to add a new card but this works fine.
I have had this working with separate serializers for read and create/update where the read has a 'depth = 1' to pull through the entire object but the create/update doesn't and you then parse the object and send the primary key back (I couldn't find a way of doing this in the serializer, is it possible?).
So basically my question is, are you meant to pass the entire object through and parse it on a POST method, or do you pass the primary key and pull in the linked objects (Races) and use the primary key as an index (e.g. Races[card_race]). Also, why is 'linked_race' not coming through to the front end?
I realise I've almost answered my own question but as I'm new to Django I'm looking for the correct conventions and who knows, it may save someone else time when searching for the same answer.
urls.py
from .api import CardViewSet, RaceViewSet
from rest_framework.routers import DefaultRouter
from django.conf.urls import url, include
from .views import landing
router = DefaultRouter()
router.register(r'cards', CardViewSet)
router.register(r'races', RaceViewSet)
urlpatterns = [
url(r'^$', landing),
url(r'^api/', include(router.urls)),
]
api.py
from rest_framework.viewsets import ModelViewSet
from .serializers import CardSerializer, RaceSerializer
from .models import Card, Race
class CardViewSet(ModelViewSet):
queryset = Card.objects.filter(active=True)
def get_serializer_class(self):
return CardSerializer
def perform_create(self, serializer):
serializer.save(creator=self.request.user)
class RaceViewSet(ModelViewSet):
queryset = Race.objects.filter(active=True)
serializer_class = RaceSerializer
models.py
from django.db import models
from django.conf import settings
User = settings.AUTH_USER_MODEL
class Race(models.Model):
id = models.IntegerField(primary_key=True)
title = models.CharField(max_length=30, blank=False)
active = models.BooleanField(default=True)
def __str__(self):
return "{}".format(self.title)
def __unicode__(self):
return self.title
class Card(models.Model):
card_text = models.CharField(max_length=100, blank=False)
card_description = models.CharField(max_length=100, blank=True)
card_race = models.ForeignKey(Race, related_name='linked_race', on_delete=models.CASCADE)
creator = models.ForeignKey('auth.User', on_delete=models.CASCADE)
created = models.DateTimeField(auto_now_add=True)
active = models.BooleanField(default=True)
def __str__(self):
return self.card_text
class Meta:
ordering = ('created',)
serializers.py
from rest_framework import serializers
from .models import Card, Race
class RaceSerializer(serializers.ModelSerializer):
class Meta:
model = Race
fields = '__all__'
class CardSerializer(serializers.ModelSerializer):
linked_race = RaceSerializer(read_only=True, many=True)
class Meta:
model = Card
fields = 'id', 'card_text', 'card_description', 'card_race', 'linked_race',
Javascript extract (AngularJS)
$http.get('/api/races/').then(function (response) {
$scope.races = response.data;
$scope.selectedOption = $scope.races[0];
});
$scope.cards = [];
$http.get('/api/cards/').then(function (response) {
$scope.cards = orderBy(response.data, 'created', true);
});
html extract (AngularJS)
<div class="races--row" ng-repeat="c in cards | filter : card_filter |
orderBy : sortVal : sortDir" ng-class-odd="'odd'" ng-click="openModal(c)">
<div class="races--cell race">{{ c.card_race.title }}</div>
<div class="races--cell card-text">{{ c.card_text }}</div>
</div>
Your first "problem" is with the Card model (I say problem because I don't think you intended to do this). You're defining related_name='linked_race' for the card_race field. This related_name is the name you use to refer to a card FROM a race.
I would suggest you leave it out and use the default that Django already gives us (i.e. my_race.card_set.all() in this case). So change change that field in the Card model to:
class Card(models.Model):
...
card_race = models.ForeignKey(Race, on_delete=models.CASCADE)
...
And let's change the card serializer to:
class CardSerializer(serializers.ModelSerializer):
# no more linked_race
class Meta:
model = Card
fields = ('id', 'card_text', 'card_description', 'card_race')
Alright, this is a vary basic model serializer and you won't see details of a race yet. So now let's get to your main problem which was that you wanted to:
see the details of the associated race of a card
perform create/get/update/delete operations using the same serializer
For this, let's further change the CardSerializer to include another field called race_detail:
class CardSerializer(serializers.ModelSerializer):
race_detail = RaceSerializer(source='card_race', read_only=True)
class Meta:
model = Card
fields = ('id', 'card_text', 'card_description', 'card_race', 'race_detail')
We have defined two serializer fields for the same model field. Note the source and read_only attributes. This makes this field available when you GET a card (which is what we want), but not when you're performing POSTs or PUTs (which avoids the problem of sending the whole race object and parsing and stuff). You can just send the race id for the card_race field and it should work.
DRF will use the editable=False on a field to default the Serializer to read-only. This is a very helpful / safe default that I take advantage of (ie I won't forget to set the Serializer to read-only). That being said once I have set editable=False is there any way to then force the Django admin to allow editing one of those fields?
Presumably the admin is a super user and I do want him to be able to change the fields value but fore safety I want the default Serializer logic to be read only.
UPDATE
I don't actually need to be able to edit the field as much as "set-it" when I create the object.
You are going about this the wrong way.
Your models should be the most pure implementation of the things you are modelling. If something about a model is fixed (for example a creation date) it shouldn't be editable in the model, if its mutable, then leave as editable in the model.
Otherwise, in the future you (or someone else) might be stuck wondering why a field which is set to editable=False is some how being changed. Especially as the documentation states:
If False, the field will not be displayed in the admin or any other ModelForm. They are also skipped during model validation.
If you have one view in which it shouldn't be editable (such as in the API), then override it there.
If you have multiple serilaizers for a model, instead make an abstract serializer with a read_only_fields set and then subclass that. For example:
class AbstractFooSerializer(serializers.ModelSerializer):
class Meta:
model = Foo
read_only_fields = ('bar',)
class MainFooSerializer(AbstractFooSerializer):
pass
class DifferentFooSerializer(AbstractFooSerializer):
pass
If you really, really want to use editable=False, but allow the item to be edited in the Admin site only on creation you have an up hill battle.
Probably the best approach would be to reimplement the AdminForm you are using for the Admin
So instead of:
class FooAdmin(admin.ModelAdmin):
Use:
class FooAdmin(admin.ModelAdmin):
form = MySpecialForm
Then declare the form:
class MySpecialForm(forms.Model):
def __init__(self, *args, **kwargs):
self.is_new = False
if kwargs.get('instance',None) is None:
# There is no instance, thus its a new item
self.is_new = True
self.fields['one_time_field'] = forms.CharField() # Or what have you.
super(MySpecialForm, self).__init__(*args, **kwargs)
def save(self, commit=True):
instance = super(MySpecialForm, self).save(commit)
if self.is_new:
instance.your_one_time_only_field = self.one_time_field
instance.save()
return instance
Note: you will need to manually add a field and save each readonly field that you want to do this for. This may or may not be 100% functional.
For those who want to allow editing of a non-editabled field only during creation (no instance.pk, yet):
# models.py
class Entity(Model):
name = CharField(max_length=200, unique=True, null=False, blank=False, editable=False)
# admin.py
#register(Entity)
class EntityAdmin(ModelAdmin):
def get_readonly_fields(self, request, obj=None):
if obj: # This is the case when obj is already created i.e. it's an edit
return ['id', 'name']
else:
return []
# this override prevents that the new_name field shows up in the change form if it's not a creation
def get_form(self, request, obj=None, **kwargs):
orig_self_form = self.form
if not obj:
self.form = CreateEntityForm
result = super().get_form(request, obj=obj, **kwargs)
self.form = orig_self_form
return result
# forms.py
class CreateEntityForm(ModelForm):
new_name = CharField(max_length=200, min_length=2, label='Name', required=True)
def clean_new_name(self):
code = self.cleaned_data['new_name']
# validate uniqueness - if you need
exists = Entity.objects.filter(name=code).first()
if exists:
raise ValidationError('Entity with this name already exists: {}', exists)
return name
def save(self, commit=True):
if self.instance.pk:
raise NotImplementedError('Editing of existing Entity is not allowed!')
self.instance.name = self.cleaned_data['new_name'].upper()
return super().save(commit)
class Meta:
model = Entity
fields = ['new_name']
exclude = ['id', 'name']
I'm experimenting with django-nonrel on appengine and trying to use a djangotoolbox.fields.ListField to implement a many-to-many relation. As I read in the documentation a ListField is something that you can use to make a workaround for djamgo-nonrel not supporting many-to-many relations.
This is an excerpt from my model:
class MyClass(models.Model):
field = ListField(models.ForeignKey(AnotherClass))
So if I am getting this right I am creating a list of foreign keys to another class to show a relationship with multiple instances of another class
With this approach everything works fine ... No Exceptions. I can create `MyClass' objects in code and views. But when I try to use the admin interface I get the following error
No form field implemented for <class 'djangotoolbox.fields.ListField'>
So I though I would try something that I haven't done before. Create my own field. Well actually my own form for editing MyClass instances in the admin interface. Here is what I did:
class MyClassForm(ModelForm):
field = fields.MultipleChoiceField(choices=AnotherClass.objects.all(), widget=FilteredSelectMultiple("verbose_name", is_stacked=False))
class Meta:
model = MyClass
then I pass MyClassForm as the form to use to the admin interface
class MyClassAdmin(admin.ModelAdmin):
form = MyClassForm
admin.site.register(MyClass, MyClassAdmin)
I though that this would work but It doesn't. When I go to the admin interface I get the same error as before. Can anyone tell what I am doing wrong here ... or if you have any other suggestions or success stories of using the ListField, SetField, etc. from djangotoolbox.fields in the admin interface it would be very much appreciated.
OK, here is what I did to get this all working ...
I'll start from the beginning
This is what what my model looked like
class MyClass(models.Model):
field = ListField(models.ForeignKey(AnotherClass))
I wanted to be able to use the admin interface to create/edit instances of this model using a multiple select widget for the list field. Therefore, I created some custom classes as follows
class ModelListField(ListField):
def formfield(self, **kwargs):
return FormListField(**kwargs)
class ListFieldWidget(SelectMultiple):
pass
class FormListField(MultipleChoiceField):
"""
This is a custom form field that can display a ModelListField as a Multiple Select GUI element.
"""
widget = ListFieldWidget
def clean(self, value):
#TODO: clean your data in whatever way is correct in your case and return cleaned data instead of just the value
return value
These classes allow the listfield to be used in the admin. Then I created a form to use in the admin site
class MyClassForm(ModelForm):
def __init__(self, *args, **kwargs):
super(MyClasstForm,self).__init__(*args, **kwargs)
self.fields['field'].widget.choices = [(i.pk, i) for i in AnotherClass.objects.all()]
if self.instance.pk:
self.fields['field'].initial = self.instance.field
class Meta:
model = MyClass
After having done this I created a admin model and registered it with the admin site
class MyClassAdmin(admin.ModelAdmin):
form = MyClassForm
def __init__(self, model, admin_site):
super(MyClassAdmin,self).__init__(model, admin_site)
admin.site.register(MyClass, MyClassAdmin)
This is now working in my code. Keep in mind that this approach might not at all be well suited for google_appengine as I am not very adept at how it works and it might create inefficient queries an such.
As far as I understand, you're trying to have a M2M relationship in django-nonrel, which is not an out-of-the-box functionality. For starters, if you want a quick hack, you can go with this simple class and use a CharField to enter foreign keys manually:
class ListFormField(forms.Field):
""" A form field for being able to display a djangotoolbox.fields.ListField. """
widget = ListWidget
def clean(self, value):
return [v.strip() for v in value.split(',') if len(v.strip()) > 0]
But if you want to have a multiple selection from a list of models normally you'd have to use ModelMultipleChoiceField, which is also not functional in django-nonrel. Here's what I've done to emulate a M2M relationship using a MultipleSelectField:
Let's say you have a M2M relationship between 2 classes, SomeClass and AnotherClass respectively. You want to select the relationship on the form for SomeClass. Also I assume you want to hold the references as a ListField in SomeClass. (Naturally you want to create M2M relationships as they're explained here, to prevent exploding indexes if you're working on App Engine).
So you have your models like:
class SomeClass(models.Model):
another_class_ids = ListField(models.PositiveIntegerField(), null=True, blank=True)
#fields go here
class AnotherClass(models.Model):
#fields go here
And in your form:
class SomeClassForm(forms.ModelForm):
#Empty field, will be populated after form is initialized
#Otherwise selection list is not refreshed after new entities are created.
another_class = forms.MultipleChoiceField(required=False)
def __init__(self, *args, **kwargs):
super(SomeClassForm,self).__init__(*args, **kwargs)
self.fields['another_class'].choices = [(item.pk,item) for item in AnotherClass.objects.all()]
if self.instance.pk: #If class is saved, highlight the instances that are related
self.fields['another_class'].initial = self.instance.another_class_ids
def save(self, *args, **kwargs):
self.instance.another_class_ids = self.cleaned_data['another_class']
return super(SomeClassForm, self).save()
class Meta:
model = SomeClass
Hopefully this should get you going for the start, I implemented this functionality for normal forms, adjust it for admin panel shouldn't be that hard.
This could be unrelated but for the admin interface, be sure you have djangotoolbox listed after django.contrib.admin in the settings.. INSTALLED_APPS
You could avoid a custom form class for such usage by inquiring for the model object
class ModelListField(ListField):
def __init__(self, embedded_model=None, *args, **kwargs):
super(ModelListField, self).__init__(*args, **kwargs)
self._model = embedded_model.embedded_model
def formfield(self, **kwargs):
return FormListField(model=self._model, **kwargs)
class ListFieldWidget(SelectMultiple):
pass
class FormListField(MultipleChoiceField):
widget = ListFieldWidget
def __init__(self, model=None, *args, **kwargs):
self._model = model
super(FormListField, self).__init__(*args, **kwargs)
self.widget.choices = [(unicode(i.pk), i) for i in self._model.objects.all()]
def to_python(self, value):
return [self._model.objects.get(pk=key) for key in value]
def clean(self, value):
return value
I have the follow code example, which is a simplified abstraction of a real world project I'm working on:
from django.db import models
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes import generic
class FeatureSet(models.Model):
"""
Feature Set
"""
name = models.CharField(max_length=255)
def __unicode__(self):
return u"%s" % self.name
class GenericObjectAlpha(models.Model):
title = models.CharField(max_length=255)
feature_sets = models.ManyToManyField(FeatureSet, through='Feature')
def __unicode__(self):
return u"%s" % self.title
class GenericObjectBeta(models.Model):
title = models.CharField(max_length=255)
feature_sets = models.ManyToManyField(FeatureSet, through='Feature')
def __unicode__(self):
return u"%s" % self.title
class Feature(models.Model):
"""
Feature
"""
# FK to feature set
feature_set = models.ForeignKey(FeatureSet)
# FK to generic object, Generic object alpha or beta... or others
content_type = models.ForeignKey(
ContentType,
default='article',
limit_choices_to={ 'model__in': ('genericobjectalpha', 'genericobjectbeta') },
related_name="play__feature_set__feature")
object_id = models.PositiveIntegerField(
"Feature object lookup")
content_object = generic.GenericForeignKey(
'content_type',
'object_id')
# Extra fields on a m2m relationship
active = models.BooleanField()
order = models.PositiveIntegerField()
def __unicode__(self):
return u"%s::%s" % (self.feature_set, self.content_object)
This line causes an error:
feature_sets = models.ManyToManyField(FeatureSet, through='Feature')
Obviously because the 'through' model lacks a corresponding FK to each side of the m2m. What I'd like to achieve here, is that one side of the m2m relationship is generic, and, that I can specify my own intermediary join table, to do the usual adding of custom fields etc.
What are my options for accomplishing this?
Note, its currently an important requirement to include the feature_sets = models.ManyToManyField(FeatureSet, through='Feature') line in the generic model, mostly for admin UI purposes. The reason why its generic is that its not yet determined how many models this line will be placed upon.