Is there any way to make the wagtail body field readonly? - django

I'm developing blog app with wagtail.
main point about blog page:
Writer can write "title", "introduction" and "body".
Firstly, writer can submit title.
After finishing "title task", writer can edit and submit body.
When writer do title task, writer can not edit body field.
Furthermore, when writer do body task, writer can not edit title field.
I want to change permission dynamically for titleField and bodyField(RichTextField) but I could not figure out how to do it.
I thought editing hook about #hooks.register("after_edit_page") in wagtail_hooks.py can reach resolving.
I tried to use PagePermissionHelper, PermissionHelper.

Proposed Solution
The Wagtail documentation regarding Customising generated forms explains the method to override the form that gets generated when editing/creating a new page.
The WagtailAdminPageForm extends the Django ModelForm and you can extend this further to add custom clean/__init__/save etc methods to add essentially any logic you want to both how the form renders and what errors get provided to the user before the save gets applied.
Django ModelForm documentation.
By default you do not have acesss to the request object on form creation, but you do get it on the save method so it would be possible to easily do some user basic logic there.
If you need further customisation, you can dig into Wagtail edit handers (search through the source code) and you can create your own edit handler that can pass in the request to your custom BlogPageForm.
Note: If the eventual goal is to add a full on 'process' based page editing workflow, you may want to look at Wagtails' ModelAdmin and essentially just build the blog workflow completely in isolation of the normal page structure and then restructure permissions so that blog editors cannot access the normal page tree but can only access your custom workflow.
Example Code
This is just a basic example of a custom form for a BlogPage model.
__init__ can be extended to add custom logic to how the form gets generated (e.g. make some fields read only or even 'hide' some fields).
save can be extended to add server side validation to the read only fields and also provide user facing error messaging.
It is possible to add logic for a 'new' page creation along with logic for editing an existing page by checking if the self.instance.pk (primary key) exists.
# other imports
from wagtail.admin.forms import WagtailAdminPageForm
class BlogPageForm(WagtailAdminPageForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
instance = getattr(self, 'instance', None)
if not instance.pk:
# this is a NEW blog entry form - only allow title to be enabled, disable other fields
self.fields['description'].widget.attrs['readonly'] = True
if instance.pk:
# assume title has been entered and saved at this point (required for a new post)
# disable the title field
self.fields['title'].widget.attrs['readonly'] = True
def clean(self):
cleaned_data = super().clean()
instance = getattr(self, 'instance', None)
title = cleaned_data['title']
description = cleaned_data['description']
if not instance.pk:
# this is a NEW blog entry, check that only the title has been entered
if not title:
self.add_error('title', 'title must be edited before all other fields')
return cleaned_data
if description:
self.add_error('description', 'description cannot be entered until title has been completed')
if instance.pk:
# an existing blog entry, do not allow title to be changed
print('title', instance.title, title)
if instance.title != title:
self.add_error('title', 'title cannot be edited after the initial save')
class BlogPage(Page):
# ...fields
base_form_class = BlogPageForm

Can be done easy with additional custom.css file
With insert_global_admin_css Wagtail hook, add path to your custom.css file. Here is link for documentation: https://docs.wagtail.io/en/latest/reference/hooks.html#insert-global-admin-css
Then add classname(eg. "myReadonlyInput") to FieldPanel in Page model. This will add new class to li element with input field.
FieldPanel("field_name", classname="myReadonlyInput"),
In custom.css file add pointer-events:none; to input field that belongs to new class for li element:
li.myReadonlyInput div.input input {
background-color: rgb(239 239 239);
color: rgb(99 99 99);
pointer-events:none;
cursor:text;
}
That way only with adding classname to any field model, input field will be grayed out and not reachable.

Related

Getting user's old image if no image is selected

I have a form that takes in user data like bio, profile picture, gender, etc. and upon submission either creates this new row about the user or updates the existing row. This will only work if the user uploads an image. If no image is uploaded for the profile picture, then the form doesn't submit. How can I make it so that if the user didn't upload a profile picture, then it'll keep the user's previous profile picture and still submit?
Here's my code:
class ProfileSettings(UpdateView):
model = Profile
template_name = 'blog/settings.html'
form_class = ProfileForm
success_url = reverse_lazy('blog:settings')
def post(self, request, *args, **kwargs):
form = self.form_class(request.POST, request.FILES)
if form.is_valid():
bio = form.cleaned_data['bio']
gender = form.cleaned_data['gender']
avatar = form.cleaned_data['avatar']
Profile.objects.update_or_create(user=self.request.user, defaults={'avatar':avatar, 'bio':bio, 'gender':gender})
return HttpResponseRedirect(self.success_url)
I'll give you the quick and dirty - 3 places to solve this:
Javascript - make the form aware of what fields are required and pre-fill if the username already exists (out of scope from your question but just throwing it out there)
In the API endpoint (this seems to be the approach you are going for)
In your model (implement a custom save function that looks to see if new, and compare initial value to subsequent value)
I'll dump options 1 and 3 because they aren't pertinent to your question as asked. I'm assuming user is unique per profile. And I'm assuming that currently the field avatar is required. If you set that to not required then the form post should allow a null value for avatar - How to make FileField in django optional?. You may be thinking, but I don't want that field to be possibly blank - you could always enforce that the first time the post is made that the field is set via the API endpoint itself. If you made that field optional then the form would post but you may want to be more explicit with .update_or_create by actually checking to see if the object already exists and if so require the field or if not confirm that field is set.

Make Django build contenteditable div for TextField instead of textarea

In my Django 1.10 project, I have a model:
class Contact(models.Model):
notes = models.TextField()
...and ModelForm:
class ContactForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(ContactForm, self).__init__(*args, **kwargs)
for field_name, field in self.fields.items():
field.widget.attrs['class'] = 'form-control input-sm plain'
if field.required == True:
field.widget.attrs['required'] = ''
class Meta:
model = Contact
fields = ('notes',)
I have two questions regarding this:
Can I make Django render the notes field as div with contenteditable=true rather than textarea?
If yes, how do I automate the form.save() method?
The second question is a bit vague, so I would be grateful for a hint regarding the first question. I have read through the doc, but couldn't find relevant section :(
Question 1: Render a field with a specific HTML tag
In this case, <div contenteditable="true">...</div>
You can customize the widget used to render a form field. Basically, when you declare the field, you have to pass the argument widget=TheWidgetClass. Django has a lot of builtin widgets you can use. But you can also define your own. To know how, you will find many resources on the Internet. Simply search for "django create custom widget" on a search engine, or on SO. Possible answer: Django: How to build a custom form widget?
Since Django doesn't provide any official documentation on how to create custom widget, the smartest way would be to create a class inheriting Django's TextArea and using a custom html template based on original one.
Question 2: Automate form.save() method with such a custom widget
Basically, you have to make sure the value of the div tag is sent with other inputs values to the target view of your form, in POST data. In other words, you have to make sure the content of the div acts like the content of any form field.
Maybe this SO question can help you: one solution could be using JavaScript to copy the content of your div to a hidden textarea tag in the form when the user click on submit button.
Good luck ;)
First one can be done by using jquery. Since django will load the textarea with id='id_notes'. So after that you can select the element and do whatever you want to.
And in second one you can redefine the save method by writing you own save function in forms.py Here is an example for a url shortener.
Save method basically, defines what you want to execute when something is being committed to database.

Flask-admin; To add custom logic during row insertion

I'm new to the flask-admin library so please forgive me if this is trivial. When I click 'Save' to create a new row for a model, I also want to do some custom things. In my case, I'll create a table dynamically whose name is the string entered in the form. This will be in addition to what flask-admin does for me i.e. add a new row to the model table. So where will I put the custom logic to do what I want to do? I saw this post on so: Customize (override) Flask-Admin's Submit method from edit view with Joe's answer about overriding on_model_change but I'll like some more explanation. From the docs, it says that on_model_change is called from update_model and create_model. When I click on the source link to the right, I get to: http://flask-admin.readthedocs.org/en/latest/_modules/flask_admin/model/base/#BaseModelView.create_model . It doesn't show the code. So I don't know how it is implemented.
Can someone please illustrate what I'm trying to in a simple sample code? Thanks.
The right way of doing this (as you have mentioned) is through after_model_change function. Quoting Flask-Admin source code
def after_model_change(self, form, model, is_created):
"""
Perform some actions after a model was created or updated and
committed to the database.
Called from create_model after successful database commit.
By default does nothing.
:param form:
Form used to create/update model
:param model:
Model that was created/updated
:param is_created:
True if model was created, False if model was updated
"""
pass
So basically, in your case, you need to perform the table creation inside this function on your model. Namely
class MyModelView(BaseModelView):
column_list = ('fieldX', 'fieldY')
def after_model_change(self, form, model, is_created):
tablename = form.tablename
if is_created: # create the table just once
perform_dynamic_table_creation(conn,tablename)

How to render django form differently based on what user selects?

I have a model and a form like this:
class MyModel(models.Model):
param = models.CharField()
param1 = models.CharField()
param2 = models.CharField()
class MyForm(forms.ModelForm):
class Meta:
model = MyModel
fields = ('param', 'param1', 'param2')
Then I have one drop down menu with different values and based on what value is selected I'm hiding and showing fields of MyForm. Now I have to take one step further and render param2 as a CheckboxInput widget if user selects a certain value from a drop down but in other cases it should be standard text field. So how would I do that?
I know this post is almost a year old, but it took me multiple hours to even find a post related to this topic (this is the only one I found, which came up as related when submitting my own question), so I felt the need to share my solution.
I wanted to have a form that would show and require a text field if an option from a dropdown menu matched a value stored in another model. I had a foreignKey relation between two models and I passed an instance of Model1 into the ModelForm for Model2. If a value chosen for a variable in Model2 matched a variable already set in Model1, I wanted to show and require a textfield. It was basically a "choose Other and then enter your own description" scenario.
I did not want the page to reload (I was trying to have this work in both mobile and desktop browsers with the least delay/reloads and using the same code for both), so I could not use the mentioned multiple forms loading in a view option. I started trying to do it with AJAX as suggested above when I realized I was over thinking the problem.
The answer was using JS and clean methods in the form. I added a non-required field (field1) that was not in Model2 to my Model2Form. I then hid this using jQuery and only displayed it (using jQuery) if the value of another field (field2) matched the value of the variable from Model1. To make that work, I did decide to have a hidden < span > in my template with the pk of the variable so I could easily grab it with jQuery. This jQuery worked perfectly for hiding and showing the field correctly so the user could choose the "other" value and then decided to choose a different one instead (and go back and forth endlessly).
I then used a clean method in my Model2Form for field1 that raised a ValidationError if no value was entered when the value in field2 matched my Model1 variable. I accessed that variable by using "self.other = Model1.variable" in my __ init __ method and then referencing that in the clean_field1 method.
I would have liked to have been able to accomplish this without having to hide and show a field with JS, but I think the only solutions for doing so with views or ajax caused delays/reloads that I did not want. Also, I liked the general simplicity of the method I used, rather than having to figure out how to pass partial forms back and forth through the HTTPRequest.
Update:
In my situation, I was creating entries for lost and found items and if the location where the item was found was not a provided option, then I wanted to show a textbox for the user to enter the location. I created a location object that was set as the "other" location and then displayed the textbox when that object was selected as the "found" location.
In forms.py, I added an extra CharField and use a clean method to check if the field is required and then throw a ValidationError if it wasn't filled in:
class Model2Form(forms.ModelForm):
def __init__(self, Model1, *args, **kwargs):
self.other = Model1.otherLocation
super(Model2Form, self).__init__(*args, **kwargs)
...
otherLocation = forms.CharField(
label="Location Description",
max_length=255,
required=False
)
def clean_otherLocation(self):
if self.cleaned_data['locationFound'] == self.other and not self.cleaned_data['otherLocation']:
raise ValidationError("Must describe the location.")
return self.cleaned_data['otherLocation']
Then in my JavaScript, I checked if the value of the "found" location was the "other" location (the value of which I had in a hidden span on my html page). I then used .show() and .hide() on the textbox's parent element as necessary:
$("#id_locationFound").change( function(){
if ($("#id_locationFound").val() == $("#otherLocation").attr("value")){ //if matches "other" location, display textbox; otherwise, hide textbox
$("#id_otherLocation").parent().show();
}else
$("#id_otherLocation").parent().hide();
});
Your best guess would be to trigger a "POST" request when you select something from your drop down menu.
The Value of that "POST" has to correspond your values you use to determine which field you would like to output.
Now you will actually need two forms:
class MyBaseForm(forms.ModelForm):
class Meta:
model = MyModel
fields = ('param', 'param1', 'param2')
class MyDropDownForm(MyBaseForm):
class Meta:
widgets = {
'param2': Select(attrs={...}),
}
So as you can see the DropDownForm has been derived from MyBaseForm to make sure it will have all the same properties. But we have modified the widget of one of the fields.
Now you can update your view. Please note, this is untested Python + Pseudocode
views.py
def myFormView(request):
if request.method == 'POST': # If the form has been submitted...
form = MyBaseForm(request.POST)
#submit button has not been pressed, so the dropdown has triggered the submission.
#Hence we won't safe the form, but reload it
if 'my_real_submitbotton' not in form.data:
if 'param1' == "Dropdown":
form = MyDropDownForm(request.POST)
else:
#do your normal form saving procedure
else:
form = ContactForm() # An unbound form
return render(request, 'yourTemplate.html', {
'form': form,
})
This mechanism does the following:
When the form is submitted it checks if you have pressed the "submit" button or have used a dropdown onChange to trigger a submission. My solution doesn't contain the javascript code you need to trigger the submission with an onChange. I just like to provide a way to solve it.
To use the 'my_real_submitbutton' in form.data construct you will be required to name your submit button:
<input type="submit" name="my_real_submitbutton" value="Submit" />
Of course you can choose any string as Name. :-)
In case of a submit by your dropdown field you must check which value has been selected in this drop down menu. If this value satisfies the condition you want to return a Dropdown Menu you create an instance of DropDownForm(request.POST) otherwise you can leave everything as it is and rerender your template.
On the downside this will refresh your page.
On the upside it will keep all the already entered field values. So no harm done here.
If you would like to avoid the page refresh you can keep my proposed idea but you need to render the new form via AJAX.

How to prevent self (recursive) selection for FK / MTM fields in the Django Admin

Given a model with ForeignKeyField (FKF) or ManyToManyField (MTMF) fields with a foreignkey to 'self' how can I prevent self (recursive) selection within the Django Admin (admin).
In short, it should be possible to prevent self (recursive) selection of a model instance in the admin. This applies when editing existing instances of a model, not creating new instances.
For example, take the following model for an article in a news app;
class Article(models.Model):
title = models.CharField(max_length=100)
slug = models.SlugField()
related_articles = models.ManyToManyField('self')
If there are 3 Article instances (title: a1-3), when editing an existing Article instance via the admin the related_articles field is represented by default by a html (multiple)select box which provides a list of ALL articles (Article.objects.all()). The user should only see and be able to select Article instances other than itself, e.g. When editing Article a1, related_articles available to select = a2, a3.
I can currently see 3 potential to ways to do this, in order of decreasing preference;
Provide a way to set the queryset providing available choices in the admin form field for the related_articles (via an exclude query filter, e.g. Article.objects.filter(~Q(id__iexact=self.id)) to exclude the current instance being edited from the list of related_articles a user can see and select from. Creation/setting of the queryset to use could occur within the constructor (__init__) of a custom Article ModelForm, or, via some kind of dynamic limit_choices_to Model option. This would require a way to grab the instance being edited to use for filtering.
Override the save_model function of the Article Model or ModelAdmin class to check for and remove itself from the related_articles before saving the instance. This still means that admin users can see and select all articles including the instance being edited (for existing articles).
Filter out self references when required for use outside the admin, e.g. templates.
The ideal solution (1) is currently possible to do via custom model forms outside of the admin as it's possible to pass in a filtered queryset variable for the instance being edited to the model form constructor. Question is, can you get at the Article instance, i.e. 'self' being edited the admin before the form is created to do the same thing.
It could be I am going about this the wrong way, but if your allowed to define a FKF / MTMF to the same model then there should be a way to have the admin - do the right thing - and prevent a user from selecting itself by excluding it in the list of available choices.
Note: Solution 2 and 3 are possible to do now and are provided to try and avoid getting these as answers, ideally i'd like to get an answer to solution 1.
Carl is correct, here's a cut and paste code sample that would go in admin.py
I find navigating the Django relationships can be tricky if you don't have a solid grasp, and a living example can be worth 1000 time more than a "go read this" (not that you don't need to understand what is happening).
class MyForm(forms.ModelForm):
class Meta:
model = MyModel
def __init__(self, *args, **kwargs):
super(MyForm, self).__init__(*args, **kwargs)
self.fields['myManyToManyField'].queryset = MyModel.objects.exclude(
id__exact=self.instance.id)
You can use a custom ModelForm in the admin (by setting the "form" attribute of your ModelAdmin subclass). So you do it the same way in the admin as you would anywhere else.
You can also override the get_form method of the ModelAdmin like so:
def get_form(self, request, obj=None, **kwargs):
"""
Modify the fields in the form that are self-referential by
removing self instance from queryset
"""
form = super().get_form(request, obj=None, **kwargs)
# obj won't exist yet for create page
if obj:
# Finds fieldnames of related fields whose model is self
rmself_fields = [f.name for f in self.model._meta.get_fields() if (
f.concrete and f.is_relation and f.related_model is self.model)]
for fieldname in rmself_fields:
form.base_fields[fieldname]._queryset =
form.base_fields[fieldname]._queryset.exclude(id=obj.id)
return form
Note that this is a on-size-fits-all solution that automatically finds self-referencing model fields and removes self from all of them :-)
I like the solution of checking at save() time:
def save(self, *args, **kwargs):
# call full_clean() that in turn will call clean()
self.full_clean()
return super().save(*args, **kwargs)
def clean(self):
obj = self
parents = set()
while obj is not None:
if obj in parents:
raise ValidationError('Loop error', code='infinite_loop')
parents.add(obj)
obj = obj.parent