I have a model, say 'Article', with a field
published = models.BooleanField(default=True)
and a template with condition:
{% if user.is_staff %}
<li>form.published.label_tag</li>
<li>form.published</li>
{% else %}
<li>form.published.as_hidden</li>
{% endif %}
and I use class-based generic views to add and update for this model.
In this case it is still possible for regular non-staff and malicious user to replace the value of published field.
I think I have to move the condition to views level to prevent this issue, somithing like
class ArticleEdit(UpdateView):
model = Article
form_class = ArticleForm
def form_valid(self, form):
self.object = form.save(commit=False)
if self.request.user.is_staff:
''' How to let the staff change this value? '''
else:
''' How to set previous value? '''
self.object.save()
return HttpResponseRedirect(self.get_success_url())
assuming that I remove this hidden field from template.
I would consider defining two forms, one for staff and one for regular users. You can then override the get_form_class method to select the correct form. If you exclude the published field from the form for non-staff, then they won't be able to change the value.
class ArticleForm(forms.ModelForm):
class Meta:
model = Article
exclude = ('published',)
class ArticleStaffForm(ArticleForm)
class Meta:
model = Article
exclude = ()
class ArticleEdit(UpdateView):
...
def get_form_class(self):
if self.request.user.is_staff:
return ArticleStaffForm
else:
return ArticleForm
you can do something like:
class MyForm(forms.Form):
def __init__(self, user, *args, **kwargs):
super(MyForm, self).__init__(*args, **kwargs)
if not user.is_staff:
del self.fields['published']
and then pass the request.user object to the form when initialising it.
WARNING: Untested pseudo code. But this should give you an idea.
Related
I have the following (simplified) Model:
class Order(models.Model):
is_anonymized = models.BooleanField(default=False)
billing_address = models.ForeignKey('order.BillingAddress', null=True, blank=True)
I want to hide the billing_address for objects where the customer chosen to do so by setting is_anonymized=True.
My best approach so far was to do that in the init:
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.is_anonymized:
self.billing_address = None
self.billing_address_id = None
This works fine in the Admin BUT...
anywhere in the code there are select_related-QuerySets for Orders:
queryset = Order._default_manager.select_related('billing_address')
All places where the select_related-querysets are used, the billing_address is accidentally shown.
Elsewhere (like in the admin), it isn't.
How can I ensure to remove the billing_address everywhere for objects with is_anonymized = True?
I thought about overwriting the queryset in the manager but i couldn't overwrite the billing_address field by condition.
Using the getter-setter pattern was not a good solution because it breaks the admin at multiple places (there are many attributes to cloak like billing_address).
To be more precise:
The goal is to only simulate as if the data would be deleted although persisting in the database.
I would like to start by saying that I do not understand why would you want to hide information from the admin of your system. Unless you have a complex work environment where only the DBA have access to such information, I honestly do not see the point.
To answer your question...
To hide information in the admin page, one option is to disable all links and replace the HTML with the edit link when is_anonymized value is False:
(adapted from answer_1 and answer_2)
admin.py:
from django.utils.html import format_html
class OrderAdmin(admin.ModelAdmin):
list_display = ['anonymous_address']
def anonymous_address(self, obj):
if not obj.is_anonymized:
return format_html(u'{}', obj.id, obj.billing_address.address)
else:
return ("%s" % ('anonymous'))
def __init__(self, *args, **kwargs):
super(OrderAdmin, self).__init__(*args, **kwargs)
self.list_display_links = None
admin.site.register(Order, OrderAdmin)
Note that with this solution admin still has access to BillingAddress model, if you registered it in the admin site. In that case it will be also necessary to override that.
On your queries, you can aggregate values with conditional expressions:
views.py:
from core.models import Order
from django.db.models import When, Case
def anonymous_address(request):
orders = Order.objects.annotate(anonymised_address=Case(
When(is_anonymized=True, then=None),
When(is_anonymized=False, then='billing_address'),
)).values('is_anonymized', 'anonymised_address')
context = {'orders': orders}
return render(request, 'anonymous_address.html', context)
anonymous_address.html:
{% block content %}
{% for order in orders %}
Should be anonymous: {{order.is_anonymized}} <br>
Address: {{order.anonymised_address}}
<hr>
{% endfor %}
{% endblock content %}
And, instead of having this long query in every view, it is possible to replace that by a custom manager:
models.py:
class AnonymousOrdersManager(models.Manager):
def get_queryset(self):
return super().get_queryset().annotate(anonymised_address=Case(
When(is_anonymized=True, then=None),
When(is_anonymized=False, then='billing_address'),
)).values('is_anonymized', 'anonymised_address')
class Order(models.Model):
is_anonymized = models.BooleanField(default=False)
billing_address = models.ForeignKey(BillingAdress, null=True, blank=True, on_delete=models.CASCADE)
objects = models.Manager()
anonymous_orders = AnonymousOrdersManager()
views.py:
def anonymous_address(request):
orders = Order.anonymous_orders.all()
context = {'orders': orders}
return render(request, 'anonymous_address.html', context)
I got two models:
Project:
class Project(Model):
name = CharField(max_length=50)
members = ManyToManyField("accounts.User", through='ProjectUser')
organization = ForeignKey(Organization, related_name="projects", on_delete=CASCADE)
def __str__(self):
return self.name
and Task:
class Task(Model):
task = CharField(max_length=100)
project = ForeignKey(Project, on_delete=CASCADE)
class Meta:
db_table = 'task'
I got a UpdateView class:
class ProjectUpdateView(UpdateView):
form_class = ProjectUpdateForm
template_name = 'projects/project_edit.html'
success_url = reverse_lazy('projects:list')
How can I allow a user to add tasks (through an inline formset) on the same page as where they'd edit a Project instance?
E.g one consolidated form where the user can edit the Project name, and add / remove Task instances, all in one place
Form/Formset:
First, create a form and a formset for your Task model
class TaskForm(ModelForm):
class Meta:
model = Task
fields = ['task']
def __init__(self, *args, **kwargs):
super(TaskForm, self).__init__(*args, **kwargs)
class TaskBaseFormSet(BaseInlineFormSet):
def __init__(self, *args, **kwargs):
super(TaskBaseFormSet, self).__init__(*args, **kwargs)
TaskFormset = inlineformset_factory(
Project, # parent_model
Task, # model
form=TaskForm,
formset=TaskBaseFormSet
)
Or maybe all that you need to do to create a TaskFormset if you dont need a TaskForm class is this
TaskFormset = inlineformset_factory(Project, Task, fields=('task',))
View:
I see you're using a UpdateView class for your view, so you can do this to get a TaskFormset in your context_data, so now you can use the TaskFormset in the template that you declared in the 'template_name' property of your UpdateView class
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
if self.request.POST:
context['task_formset'] = forms.TaskFormset(self.request.POST)
else:
context['task_formset'] = forms.TaskFormset()
return context
# In the form_valid method of your UpdateView class you can validate the data
# and assign the Project instance to all the tasks that were create by the formsets
def form_valid(self, form):
task_formset = context['task_formset']
# you can validate formset data like this
if not task_formset.is_valid():
return self.form_invalid(form)
project = form.save()
# Here you assign the Project instance to the Tasks
task_formset.instance = project
task_formset.save()
return super().form_valid(form)
Template:
Now all that you need to do is to print the management_form and each form from the formset using a loop as you can see in the code below
<form method="post">
<!-- Your ProjectUpdateForm form here... -->
{{ task_formset.management_form }}
<table>
{% for form in task_formset %}
{{ form }}
{% endfor %}
</table>
</form>
Hope this can help! There are some links to the official Django documentation that you may find useful:
https://docs.djangoproject.com/en/3.1/topics/forms/formsets/#using-a-formset-in-views-and-templates
https://docs.djangoproject.com/en/3.1/topics/forms/modelforms/#inline-formsets
https://docs.djangoproject.com/en/3.1/ref/forms/models/#inlineformset-factory
I am trying to allow users to save details of a workout for a specific exercise through submitting a form. My ExerciseDetailView displays the form how I'd like it to:
class ExerciseDetailView(DetailView):
model = Exercise
template_name = 'workouts/types.html'
def get_context_data(self, **kwargs):
context = super(ExerciseDetailView, self).get_context_data(**kwargs)
context['form'] = WorkoutModelForm
return context
But my problem is with saving the inputted data in the database. I have tried making both a FormView and a CreateView but am clearly missing something:
class ExerciseFormView(FormView):
form_class = WorkoutModelForm
success_url = 'workouts:exercise_detail'
def form_valid(self, form):
form.save()
return super(ExerciseFormView, self).form_valid(form)
Here is my referenced WorkoutModelForm:
class WorkoutModelForm(forms.ModelForm):
class Meta:
model = Workout
fields = ['weight', 'reps']
My template:
<form action="{% url 'workouts:workout' exercise.id %}" method="post">
{% csrf_token %}
{{ form }}
<button type="submit">Save</button>
</form>
Urls:
path('exercise/<int:pk>/detail/', ExerciseDetailView.as_view(), name='exercise_detail'),
path('exercise/<int:pk>/detail/', ExerciseFormView.as_view(), name='workout'),
And for context here is my Workout model which contains a get_absolute_url method:
class Workout(models.Model):
weight = models.FloatField(default=0)
reps = models.PositiveIntegerField(default=0)
created = models.DateField(auto_now_add=True)
updated = models.DateField(auto_now=True)
exercise = models.ForeignKey(Exercise, on_delete=models.CASCADE, default=None)
def get_absolute_url(self):
return reverse('exercise_detail', args=[str(self.pk)])
I am not receiving any errors, but when I submit the form my url remains the same, as I hoped, however the page just appears blank and the objects are not recorded. Can anybody please help me see what the problem is?
The problem is not your view, the Django logic will never trigger this view, the URLs are perfectly overlapping, so that means that for a URL, it will always trigger the first view (here the ExerciseDetailView), you should make the paths non-overlapping, for example with:
path('exercise/<int:pk>/detail/', ExerciseDetailView.as_view(), name='exercise_detail'),
path('exercise/<int:pk>/workout/', ExerciseFormView.as_view(), name='workout'),
Triggering the logic will however not be sufficient, since it will not link the Workout to the necessary exercise, you can alter the logic to:
from django.urls import reverse
class ExerciseFormView(CreateView):
form_class = WorkoutModelForm
def form_valid(self, form):
form.instance.exercise_id = self.kwargs['pk']
return super().form_valid(form)
def get_success_url(self):
return reverse('workouts:exercise_detail', kwargs={'pk': self.kwargs['pk']})
Need use CreateView
from django.views.generic.edit import CreateView
class ExerciseFormView(CreateView):
form_class = WorkoutModelForm
...
So I have a model:
class MyThing(models.Model):
my_field = OneToOneField(SomeOtherModel)
... other fields
A form:
class MyThingForm(forms.ModelForm):
class Meta:
model = MyThing
A view:
class MyThingView(views.TemplateView):
template_name = 'thing.html'
def get(self, request, *args, **kwargs):
form = MyThingForm()
return render(self.template_name, {'form': form})
def post(self, request, *args, **kwargs):
... retrieve some_instance
request.POST['my_field'] = some_instance
form = MyThingForm(request.POST)
if form.is_valid():
form.save()
return HttpResponseRedirect(...somewhere else)
return render(self.template_name, {'form': form})
my thing.html template:
{% for field in form %}
{{ field }}
{% endfor %}
What my problem is:
I need to hide the my_field field when rendering the template (but from backend), that implying that when I do the for on form in the template, it shouldn't have the my_field field in the fields set already
This is a creation form, that means that I don't have an existing instance
In the backend the my_field is required, so when doing POST I retrieve the instance for my_field from somewhere, doesn't matter where, and add it to the data for the form in sight. After this the form should be valid and can be saved to database
So the basic question is : How do I make a required field, hidden but saveable?
It very usual use case look at this doc.
In summary you can exclude the field from form and save it after retrieving it from somewhere.
Update code as
class MyThingForm(forms.ModelForm):
class Meta:
model = MyThing
exclude = ['my_field', ]
class MyThingView(views.TemplateView):
...
def post(self, request, *args, **kwargs):
form = MyThingForm(request.POST)
#retrieved_my_field = retrieve the field
if form.is_valid():
inst = form.save(commit=False)
inst.my_field = retrieved_my_field
inst.save()
return HttpResponseRedirect(...somewhere else)
return render(self.template_name, {'form': form})
The problem
I'm trying to modify the class-based view 'CreateView' to handle a formset instead of a form.
When client does a GET request, the formset is displayed to the client correctly.
The problem is when the client submit the form with a POST.
When Django recieve POST, it lands in form_invalid() and the form.errors say 'this field is required' for the length and name field.
class Service(models.Model):
TIME_CHOICES = (
(15, '15 minutes'),
(30, '30 minutes'),
)
length = models.FloatField(choices=TIME_CHOICES,max_length=6)
name = models.CharField(max_length=40)
class ServiceForm(ModelForm):
class Meta:
model = Service
ServiceFormSet = modelformset_factory(Service,form=ServiceForm)
class ServiceEditView(CreateView):
template_name = "service_formset.html"
model = Service
form_class = ServiceForm
success_url = 'works/'
def form_valid(self, form):
context = self.get_context_data()
formset = context['formset']
if formset.is_valid():
self.object = form.save()
return HttpResponseRedirect('works/')
else:
return HttpResponseRedirect('doesnt-work/')
def form_invalid(self, form):
print form.errors
return HttpResponseRedirect('doesnt-work/')
def get_context_data(self, **kwargs):
context = super(ServiceEditView, self).get_context_data(**kwargs)
if self.request.POST:
context['formset'] = ServiceFormSet(self.request.POST)
else:
context['formset'] = ServiceFormSet(queryset=Service.objects.filter(user__exact=self.request.user.id))
return context
My question is
How can I use a createview to handle a formset?
What am I missing to get it do validate correctly?
The tutorial I've taken most of the bits from so far http://haineault.com/blog/155/
In short, what I've done so far
Since the form.errors variable say each field is required, I think it expects a regular form not a formset -> I'm missing some option that tell the CreateView it's a formset.
I've also tried the solution suggested here: http://www.kevinbrolly.com/.
class BaseServiceFormSet(BaseModelFormSet):
def __init__(self, *args, **kwargs):
super(BaseServiceFormSet, self).__init__(*args, **kwargs)
for form in self.forms:
form.empty_permitted = False
But it didnt make any difference.
Solution
pip install django-extra-views
And in view.py:
from extra_views import FormSetView
class ItemFormSetView(ModelFormSetView):
model = Service
template_name = 'service_formset.html'
There's a discussion about getting this into Django core, but the discussions seems to have stagnated.
https://code.djangoproject.com/ticket/16256
Where I found the solution
At this repository https://github.com/AndrewIngram/django-extra-views
there's a view called ModelFormSetView, which does exactly what I needed.
It's a class-based view, that does the same as CreateView, but for formsets.
Django go into form_invalid() and the form.errors say 'this field is required' for the length and name field.
This is normal and due to the required field paramatere:
By default, each Field class assumes the value is required, so if you
pass an empty value -- either None or the empty string ("") -- then
clean() will raise a ValidationError exception:
If you want to inverse that, you can set required=False:
class Service(models.Model):
TIME_CHOICES = (
(15, '15 minutes'),
(30, '30 minutes'),
)
length = models.FloatField(choices=TIME_CHOICES,max_length=6, required=False)
name = models.CharField(max_length=40, required=False)
What am I missing to get it do validate correctly
Did you try to post a form with name and length values ?