can I use duck-typing with class-based views - django

I may be completely off the reservation here. (Feel free to tell me if I am.)
My use case is that I have a list of schools. The school model is pretty simple:
class School(models.Model):
name = models.CharField(max_length=100)
mascot = models.CharField(max_length=100, null=True, blank=True)
When my user wants to edit one of these schools, I don't want them editing the master copy. Instead, I want to give them their own copy which they can play with. When they are done editing their copy, they can submit their change, and someone else will approve it. So I have another class for the user's copy of the school:
class UserSchool(models.Model):
name = models.CharField(max_length=100)
mascot = models.CharField(max_length=100, null=True, blank=True)
master_school = models.ForeignKey(School)
user = models.ForeignKey(settings.AUTH_USER_MODEL)
So I set up a form to handle the editing of the UserSchool:
class UserSchoolForm(forms.ModelForm):
class Meta:
model = UserSchool
fields = ['name','mascot']
And now I have my EditSchool form:
class EditSchool(UpdateView):
model = School
success_url = reverse_lazy('list_schools')
form_class = UserSchoolForm
def get(self, request, *args, **kwargs):
school = self.get_object()
# make a copy of the school for this user
user_school, created = UserSchool.objects.get_or_create(
master_school=school, user=request.user,
defaults={'name' : school.name, 'mascot' : school.mascot})
self.object = user_school
form = UserSchoolForm()
context = self.get_context_data(form=form)
return self.render_to_response(context)
I know that get() is making the copy correctly, but when the form displays, there are no values listed in the "name" or "default" fields. My suspicion is that the problem is with the fact that cls.model = School, but self.object is an instance of UserSchool.
Am I close but missing something? Am I completely on the wrong path? Is there a better model for this (like having a single School instance with a special user for "master")?
(And one small complication -- since I'm an old hand at Django, but new a class-based views, I'm trying to use Vanilla Views because I find it easier to figure out what's going on.)

Just to rule out the obvious - you're not passing anything to the form constructor. Have you tried it with instance=user_school? There might be more that needs work but I'd start there.
To expand on this a bit - in your view, you're completely overriding the built in get method. That's fine, but it means that you're bypassing some of the automated behavior of your view superclass. Specifically, the get method of ProcessFormView (one of your ancestor classes) instantiates the form using the get_form method of the view class. FormMixin, another ancestor, defines get_form:
return form_class(**self.get_form_kwargs())
And get_form_kwargs on ModelFormMixin adds self.object to the form's kwargs:
kwargs.update({'instance': self.object})
Because your overridden get method does not call get_form, it also doesn't call get_form_kwargs and therefore doesn't go through the whole path that provides an initial binding for the form.
I personally would try to handle this by modifying the get_object method of your custom view and leaving the rest alone:
class EditSchool(UpdateView):
model = School
success_url = reverse_lazy('list_schools')
form_class = UserSchoolForm
def get_object(self, queryset=None):
school = super(EditSchool, self).get_object(queryset=queryset)
user_school, created = UserSchool.objects.get_or_create(
master_school=school, user=self.request.user,
defaults={'name' : school.name, 'mascot' : school.mascot})
return user_school
There may be more changes needed - I haven't tested this - but both the get and set methods use get_object, and bind it to the form as appropriate.

Related

Django rest framework updating nested serializers

I have two models, one with a foreignkey relationship to the other. I am wanting to be able to create or update multiple entries at the same time. I would not be creating and updating simultaneously.
I keep getting a bit confused about what is going on with people's examples. I was hoping someone could not only show me what I need to do, but also give a bit of an explanation about what is going on at each step. I'm specifically not understanding what the validated_data.get() method is doing, or what exactly is an instance, and what is being done with them.
class ExtraFieldsSerializer(serializers.ModelSerializer):
id = serializers.ReadOnlyField()
class Meta:
model = ExtraFields
fields = [
'id',
'job',
'custom_data',
]
class JobWriteSerializer(serializers.ModelSerializer):
extra_fields = ExtraFieldsSerializer(many=True)
class Meta:
model = Job
fields = [
"extra_fields",
"custom_data_fields",
]
# Make JobSerializer able to create custom fields.
def create(self, validated_data):
extra_fields = validated_data.pop("extra_fields")
user = User.objects.get(id=self.context['request'].user.id)
client = Client.objects.get(user=self.context['request'].user)
new_job = Job.objects.create(user=user, client=client, **validated_data)
new_job.save()
for field in extra_fields:
new_field = ExtraFields.objects.create(job=new_job, custom_data=field['custom_data'])
new_field.save()
return new_job
# Make JobSerializer able to update custom fields
def update(self, instance, validated_data):
pass
First of all, you could probably use drf-writeable-nested for that, as it pretty much does exactly what you want.
But it never hurts to understand what's going on:
def create(self, validated_data):
extra_fields = validated_data.pop("extra_fields")
user = User.objects.get(id=self.context['request'].user.id)
client = Client.objects.get(user=self.context['request'].user)
new_job = Job.objects.create(user=user, client=client, **validated_data)
new_job.save()
for field in extra_fields:
new_field = ExtraFields.objects.create(job=new_job, custom_data=field['custom_data'])
new_field.save()
return new_job
Since JobWriteSerializer is the parent serializer, we will start with that.
the validated_data argument that is passed to your create function contains, well, as the name suggests, the validated data. This means, you can assume that all the constraints you defined in your serializer (required fields, max_length, min_length etc) hold, and you don't need to check them.
The code looks fine, it seems you are popping all the extra_fields from your serializer and create ExtraField objects from them.
You specifically ask what's going on when using validated_data.get but you are using validated_data.pop. the difference is, that when using get, the retrieved data stays in the dictionary, while pop removes it.
This is especially handy for cases where you also have to create nested objects, consider this (some things omitted as they are not relevant):
class MyModel(models.Model):
text = models.CharField(max_length=10, related_name='children')
class MyChildModel(models.Model):
someVal = models.BooleanField()
model = models.ForeignKey(MyModel)
class MyChildSerializer(serializers.ModelSerializer):
someVal = serializers.BooleanField()
class MyModelSerializer(serializers.ModelSerializer):
text = serializers.TextField(...)
childen = ChildrenSerializer(many=True)
def create(self, validated_data):
children = validated_data.pop('children', []) #POP!
instance = super().create(validated_data)
for c in children:
MyChildSerializer.objects.create(model=instance, **c)
return instance
You can test this yourself, if you use get instead of pop here, your serializer will rightfully complain that there are children objects inside the validated_data object, and drf cannot create nested relations out of the box. When you pop them, the serializer does not have those fields anymore and it works.
Note that this approach would not be efficient for your case, as you manually pass data to your Job object (like the user and the client) which you do not get via the passed data, but from your request. You can, if you want, get around that by using get_serializer_context, but lets just say that this is out of the questions scope.
On to your update method, I suggest something like that (not tested, but you get the idea):
def update(self, instance, validated_data):
extra_fields = validated_data.pop('extra_fields', []) # POP the extra fields
instance = super().update(instance, validated_data) # !!!!!
for extra in extra_fields:
#retrieve your extra fields and update them
myExtra = ExtraFields.objects.get(id=extra['id])
....
The passed argument instance is actually the already existing instance of your Job model. This is the object you want to update. Note that again, I popped the extra_fields, precisely for doing what I described above: I used drf itself to modify/update the object, since I only have to implement the update for the child elements.

Django validate answer for specific question

Overview:
I want to build a Question-Answer website, where the user has to enter the correct answer for each question. I have made 3 models for this:
class ProblemSet(models.Model):
id = models.IntegerField(primary_key=True)
class Problem(models.Model):
id = models.IntegerField(primary_key=True)
problem_set = models.ForeignKey(ProblemSet, on_delete=models.CASCADE)
question = models.TextField()
solution = models.TextField()
class Solve(models.Model):
username = models.ForeignKey(User, on_delete=models.CASCADE)
problem_set = models.ForeignKey(ProblemSet, on_delete=models.CASCADE)
problem_id = models.ForeignKey(Problem, on_delete= models.CASCADE)
In solve model, if there is any entry that means that particular user has solved that problem_id.
So, I have utilized the generic Form View:
class IndexView(FormView):
form_class = ProblemForm
template_name = 'home/index.html'
success_url = reverse_lazy('index')
def get_context_data(self, **kwargs):
context = super(IndexView, self).get_context_data(**kwargs)
if self.request.user.is_authenticated:
inner_qs = "fetch ids that are solved from Solve model"
problem_obj = Problem.objects\
.exclude(id__in=inner_qs)\
.order_by('id').first()
else:
#do something
context['question'] = problem_obj.question
return context
The problem form is:
from django import forms
class ProblemForm(forms.Form):
solution = forms.CharField(widget=forms.TextInput())
How do I validate that the user is inputting the correct answer? I do get the value of solution field in def form_valid(self, form) function but how should I deal with it? Should i pass question_id in context and query the database in form_valid, or should i pass solution itself to context and access context data in form_valid() method to prevent double query but in this method I am not sure if this is secure as I don't want solution to be passed to client.
Is there any elegant way of doing this?
P.S. After user entered solution is compared to the one in database for that question, I add an entry in the Solve table denoting that this particular user has solved the question id.
The FormView is processing two separate requests: First the GET request when the student fetches the form with a question to answer. Then the POST request when the student submits her answer to the question.
Now HTTP is stateless so somehow you need to keep track of the question that was presented in the first request so you know which question was answered when receiving the POST request.
The easiest way I would say is to actually include the question_id in the form itself, as a hidden input field. There's not real security issue here: The question_id can be manipulated by the student even though it's hidden, but what's the point?
So this is what I would do:
Add problem as a ModelChoiceField with a HiddenInput widget to your ProblemForm.
problem = forms.ModelChoiceField(queryset=Problem.objects.all(), widget=forms.HiddenInput())
Set an initial value for the problem in the get_inital() method of your IndexView:
def get_problem(self): # use also in get_context_data() to add the question
if hasattr(self, 'problem'):
return self.problem
if self.request.user.is_authenticated:
inner_qs = "fetch ids that are solved from Solve model"
self.problem = Problem.objects\
.exclude(id__in=inner_qs)\
.order_by('id').first()
return self.problem
def get_initial(self):
initial = super().get_initial()
initial['problem'] = self.get_problem()}
return initial
When the form gets submitted and is valid, you'll see that form.cleaned_data['problem'] is the submitted problem. So you can use that in the form_valid() method:
def form_valid(self, form):
problem = form.cleaned_data['problem']
# check that it hasn't been solved by the user already
if problem.answer == form.cleaned_data['solution']:
# create solve object for the user
return redirect(...)
The alternative would be to not include it in the form but refetch the problem in form_valid (note that the problem is fetched in the above method as well, when the form maps the submitted problem_id to the actual problem instance to populate its cleaned_data).

Modify Django Rest Framework ModelViewSet behavior

I basically have the following model in my project:
class ShellMessage(TimeStampedModel):
# There is a hidden created and modified field in this model.
ACTION_TYPE = (
('1' , 'Action 1'),
('2' , 'Action 2')
)
type = models.CharField(max_length=2,choices=ACTION_TYPE,default='1')
action = models.CharField(max_length=100)
result = models.CharField(max_length=300, blank=True)
creator = models.ForeignKey(User)
I created a serializer:
class ShellMessageSerializer(serializers.ModelSerializer):
class Meta:
model = ShellMessage
fields = ('action', 'type', 'result', 'creator')
And a ModelViewSet:
class ShellListViewSet(viewsets.ModelViewSet):
serializer_class = ShellMessageSerializer
queryset = ShellMessage.objects.all()
My issue is the following:
When I create a new ShellMessage with a POST to my API, I don't want to provide the foreignKey of 'creator' but instead just the username of the guy and then process it in my ViewSet to find the user associated with this username and save it in my ShellMessage object.
How can I achieve this using Django rest Framework? I wanted to supercharge create() or pre_save() methods but I'm stuck as all my changes overwrite 'normal' framework behavior and cause unexpected errors.
Thank you.
I finally find my solution just after posting my question :)
So I did the following:
class ShellListViewSet(viewsets.ModelViewSet):
serializer_class = ShellMessageSerializer
queryset = ShellMessage.objects.all()
def pre_save(self, obj):
obj.creator = self.request.user
return super(ShellListViewSet, self).pre_save(obj)
This is working as expected. I hope I did well.
UPDATE: This topic seems to be a duplicate to Editing django-rest-framework serializer object before save
If you intend to intercept and perform some processing before the object gets saved in the model database, then what you're looking for is overriding the method "perform_create" (for POST) or "perform_update" (for PUT/PATCH) which is present within the viewsets.ModelViewSet class.
This reference http://www.cdrf.co/3.1/rest_framework.viewsets/ModelViewSet.html lists all available methods within viewsets.ModelViewSet where you can see that the "create" method calls "perform_create" which in turn performs the actual saving through the serializer object (the object that has access to the model):
def perform_create(self, serializer):
serializer.save()
We can override this functionality that is present in the base class (viewsets.ModelViewSet) through the derived class (the ShellListViewSet in this example) and modify the model attribute(s) that you want to be changed upon saving:
class ShellListViewSet(viewsets.ModelViewSet):
serializer_class = ShellMessageSerializer
queryset = ShellMessage.objects.all()
def findCreator(self):
# You can perform additional processing here to find proper creator
return self.request.user
def perform_create(self, serializer):
# Save with the new value for the target model fields
serializer.save(creator = self.findCreator())
You can also opt to modify the model fields separately and then save (probably not advisable but is possible):
serializer.validated_data['creator'] = self.findCreator()
serializer.save()
Later if the object is already created and you also want to apply the same logic during an update (PUT, PATCH), then within "perform_update" you can either do the same as above through the "serializer.validated_data['creator']" or you could also change it directly through the instance:
serializer.instance.creator = self.findCreator()
serializer.save()
But beware with such updating directly through the instance as from https://www.django-rest-framework.org/api-guide/serializers/ :
class MyModelSerializer(serializers.Serializer):
field_name = serializers.CharField(max_length=200)
def create(self, validated_data):
return MyModel(**validated_data)
def update(self, instance, validated_data):
instance.field_name = validated_data.get('field_name', instance.field_name)
return instance
This means that whatever you assign to the "instance.field_name" object could be overriden if there is a "field_name" data set within the "validated_data" (so in other terms, if the HTTP Body of the PUT/PATCH Request contains that particular "field_name" resulting to it being present in the "validated_data" and thus overriding whatever value you set to the "instance.field_name").

django cbv dynamically exclude field from form based on is_staff / is_superuser

Been trying to determine the "most" elegant solution to dropping a field from a from if the user is not is_staff/is_superuser. Found one that works, with a minimal amount of code. Originally I though to add 'close' to the 'exclude' meta or use two different forms. But this seems to document what's going on. The logic is in the 'views.py' which is where I feel it blongs.
My question: Is this safe? I've not seen forms manipulated in this fashion, it works.
models.py
class Update(models.Model):
denial = models.ForeignKey(Denial)
user = models.ForeignKey(User)
action = models.CharField(max_length=1, choices=ACTION_CHOICES)
notes = models.TextField(blank=True, null=True)
timestamp = models.DateTimeField(default=datetime.datetime.utcnow().replace(tzinfo=utc))
close = models.BooleanField(default=False)
forms.py
class UpdateForm(ModelForm):
class Meta:
model = Update
exclude = ['user', 'timestamp', 'denial', ]
views.py
class UpdateView(CreateView):
model = Update
form_class = UpdateForm
success_url = '/denials/'
template_name = 'denials/update_detail.html'
def get_form(self, form_class):
form = super(UpdateView, self).get_form(form_class)
if not self.request.user.is_staff:
form.fields.pop('close') # ordinary users cannot close tickets.
return form
Yes, your approach is perfectly valid. The FormMixin was designed so you can override methods related to managing the form in the view and it is straightforward to test.
However, should yours or someone else's dynamic modifications of the resulting form object become too extensive, it would probably be best to define several form classes and use get_form_class() to pick the correct form class to instantiate the form object from.

Django Forms - Proper way to validate user?

I'm having a bit of trouble finding an answer on this:
Using Django forms, what's the proper way to provide the current/requesting user?
My form looks like this:
class NewProjectForm(forms.Form):
project_name = forms.CharField(max_length=100)
def save(self):
project = Project(name=self.cleaned_data[project_name])
project.status = 'Working'
#TODO project.created_by = ???
project.save()
return project
Do I need to pass a second argument of user into the save function and I get that from the request or?
Yes, you do. However, you could probably save yourself some time by turning your form into a ModelForm and then handling the model save in your view:
class NewProjectForm(forms.ModelForm):
class Meta:
model = Project
fields = ['name',]
And then in your view:
if form.is_valid():
new_project = form.save(commit=False)
new_project.created_by = request.user
new_project.save()
That way you don't have to worry about passing around your user object, and the form itself will take care of setting the other properties for you (for project.status, you might try a default argument in your field definition).