This is an oversimplified example of what am I trying to achieve. Let's say I have these two models:
class Question(models.Model):
text = models.CharField(max_length=255)
class Answer(models.Model):
text = models.CharField(max_length=255)
question = models.ForeignKey(Question)
And let's say I have thousands of questions in the database, but I want each user to answer only a few random ones. So, my idea was to create a dynamic form. Something like this:
class QuestionnaireForm(forms.Form):
def __init__(self, *args, **kwargs):
super(QuestionnaireForm, self).__init__(*args, **kwargs)
questions = list(Question.objects.all())
random.shuffle(questions) # it successfully validates without this line
questions = questions[:3]
for q in questions:
self.fields[q.text] = forms.CharField()
When I do this, I get my three random questions as desired, but the form won't validate. If I comment out shuffling, everything works fine, but then I get the same questions every time, obviously.
From what I can see, it seems like Django is calling the __init__ method again on form submission and thus repeating the shuffling and getting different questions. I tried reading through the documentation, but I'm not managing to wrap my head around why is this the way it is.
Random ordering:
questions = Question.objects.all().order_by('?')[:3]
To validate form, it hink, you need restore same queryset with something like:
questions = Question.objects.filter(pk__in=request.POST.getlist('ids'))
and put it to form. Think you need also save same ordering — then you can sort out this list in form.
Update:
You should save state between requests some way. You can add hidden field, add URL parameters, set cookie, save info in user's profile — is up to your choice. Tipical way is set hidden inputs (generally it's default django ModelForm behavior).
So on first request, when you show form — you get queryset, sort it by pk for example, put to form and add hidden fields with IDs. When user made POST request with answer — you'll restore your queryset with this IDs, sort it again same way and put to form to validate.
Related
Background: I would like to enhance a page instance during an admin page view with some admin request related information (some pre-population in general). Basically I would need some function like "get_queryset", but not for list view, just for edit view.
In my older question related to a similar problem: Wagtail - how to preopulate fields in admin form? I was provided with instructions to use something called
CreatePageView
However, I cannot import it. Furthermore, I cannot even found any mention about that in google if I search:
Wagtail +CreatePageView
The closest thing I found is https://docs.wagtail.io/en/v2.1.1/reference/contrib/modeladmin/create_edit_delete_views.html but the page also states:
NOTE: modeladmin only provides ‘create’, ‘edit’ and ‘delete’
functionality for non page type models (i.e. models that do not extend
wagtailcore.models.Page). If your model is a ‘page type’ model,
customising any of the following will not have any effect
I am quite confused. What should I do if I need to customize the admin view for Page model extension?
I studied the wagtail source codes for Model.admin and Page and I have not found any way. Any ideas?
The related code simplified:
wagtail hooks:
class ItemAdmin(ModelAdmin):
pass
# some function override here maybe?
models:
class ItemPage(Page):
pass
# override for a function that gives data to the admin view maybe here?
Edit
As suggested in comments, it is possible to modify the admin page form during creation:
from wagtail.admin.forms import WagtailAdminPageForm
class ItemPageForm(WagtailAdminPageForm):
def __init__(self, data=None, files=None, parent_page=None, *args, **kwargs):
super().__init__(data, files, *args, **kwargs)
class ItemPage(Page):
base_form_class = ItemPageForm
however, acquiring the "request" in the WagtailAdminPageForm constructor does not seem possible.
This question is a bit ambiguous, so it is not super clear exactly what you need.
Interpreted question: When crediting (or editing) a page, I need access to the request to modify the initial values of some fields in the page form.
Potential Approach
Note: This may not be best practice and could be fragile depending on future changes to Wagtail.
First, we need a custom EditHandler, these are the way Wagtail builds up forms and even Panels within the editing interface. An EditHandler's job is to manage the form to return based on the model and even the current request.
As a first step, it would be good to get your page create form showing correctly by following the instructions on using a custom tabbed interface. From here, you can replace the TabbedInterface with your custom class (e.g. CustomTabbedInterface) and add some functionality to this which will allow for a dynamic form_class to be returned.
get_form_class should return the form_class, however, we can modify it to return a function that, when called, will instantiate the class with custom information based on the request.
There may be some issues with this approach below in edit views or scenarios not considered by this example so validate this fully before using.
Example Code
from wagtail.admin.edit_handlers import TabbedInterface, ObjectList
from wagtail.core.models import Page
class CustomTabbedInterface(TabbedInterface):
def get_form_class(self):
form_class = super().get_form_class()
request = self.request
if request and request.method != 'POST':
# check request is available to ensure this instance has been bound to it
user = self.request.user
def initiate_class(**kwargs):
# instead of returning the class, return a function that returns the instantiated class
# here we can inject a kwarg `initial` into the generated form
# important: this gets called for edit view also and initial will override the instance data
# kwarg['instance'] will be the `Page` instance and can be inspected as needed
kwargs['initial'] = {'introduction': user.first_name}
return form_class(**kwargs)
return initiate_class
return form_class
class StandardPage(Page):
# ... field etc
edit_handler = CustomTabbedInterface([
ObjectList(content_panels, heading='Content'),
ObjectList(Page.promote_panels, heading='Promote'),
ObjectList(Page.settings_panels, heading='Settings', classname="settings"),
])
Explanation
wagtail/admin/views/pages.py contains the create view, which will use the edit_handler, bind it to the model and the request and then call its get_form_class.
The form_class is used for the response here form = form_class(instance=page, parent_page=parent_page)
It gets called with the instance and the parent_page kwargs
Our custom get_form_class response takes those kwargs and injects an additional initial kwarg.
initial is used by Django forms to add any initial data - https://docs.djangoproject.com/en/3.0/ref/forms/api/#dynamic-initial-values
Finally, the Django form will merge the instance field values with the intial kwarg to generate the final pre-filled data for the form. You can see how this works in Django's BaseModelForm.
Be careful to consider what will happen on an update view, you likely do not want to override existing values with your initial values when a user has already entered something in the field.
Having a bit of trouble trying to bulk add a list of items to a many to many field and though having tried various things have no clue on how to approach this. I've looked at the Django documentation and cant seem to find what I'm looking for.
Here is the code for my models:
class Subject(models.Model):
noun = models.CharField(max_length=30, null=True, blank=True)
class Knowledge(models.Model):
item_text = models.TextField()
item_subjects = models.ManyToManyField(Subject, null=True, blank=True)
def add_subjects(sender, instance, *args, **kwargs):
if instance.item_info:
item_subjects = classifier.predict_subjects(instance.item_info)
if item_subjects:
....
post_save.connect(add_subjects, sender=Knowledge)
The list is being generated by the classifer.predict_subjects function.
I have tried using the m2m_changed connector and the pre_save and post_save connect. I'm not even sure the many to many field is the right option would it be better to do make a foreign key relationship.
in place of the '...' I have tried this but it doesn't create the relationship between and only saves the last one.
for sub in item_subjects:
subject = Subject(id=instance.id, noun=sub)
subject.save()
I've also tried
instance.item_subjects = item_subjects
and a load more things that I can't really remember, I don't really think I'm in the right ballpark to be honest. Any suggestions?
edit:
ok, so I have got it adding all of the list items but still haven't managed to link these items to the many to many field.
for sub in item_subjects:
subject = Subject.objects.get_or_create(noun=sub)
edit 2:
So doing pretty much exactly the same thing outside of the loop in the Django shell seems to be working and saves the entry but it doesn't inside the function.
>>> k[0].item_subjects.all()
<QuerySet []>
>>> d, b = Subject.objects.get_or_create(noun="cats")
<Subject: cats>
>>> k[0].item_subjects.add(d)
>>> k[0].item_subjects.all()
<QuerySet [<Subject: cats>]>
edit 3
So I took what Robert suggested and it works in the shell just like above just not when using it in the admin interface. The print statements in my code show the array item being updated but it just dosen't persist. I read around and this seems to be a problem to do with the admin form clearing items before saving.
def sub_related_changed(sender, instance, *args, **kwargs):
print instance.item_subjects.all()
if instance.item_info:
item_subjects = classifier.predict_subjects(instance.item_info)
if item_subjects:
for sub in item_subjects:
subject, created = Subject.objects.get_or_create(noun=sub)
instance.item_subjects.add(subject)
print instance.item_subjects.all()
post_save.connect(sub_related_changed, sender=Knowledge)
I have tried using the function as m2m_changed signal as follows:
m2m_changed.connect(model_saved, sender=Knowledge.item_subjects.through)
But this either generates a recursive loop or doesn't fire.
Once you have the subject objects (as you have in your edit), you can add them with
for sub in item_subjects:
subject, created = Subject.objects.get_or_create(noun=sub)
instance.item_subjects.add(subject)
The "item_subjects" attribute is a way of managing the related items. The through relationships are created via the "add" method.
Once you've done this, you can do things like instance.item_subjects.filter(noun='foo') or instance.item_subjects.all().delete() and so on
Documentation Reference: https://docs.djangoproject.com/en/1.11/topics/db/examples/many_to_many/
EDIT
Ahh I didn't realize that this was taking place in the Django Admin. I think you're right that that's the issue. Upon save, the admin calls two methods: The first is model_save() which calls the model's save() method (where I assume this code lives). The second method it calls is "save_related" which first clears out ManyToMany relationships and then saves them based on the submitted form data. In your case, there is no valid form data because you're creating the objeccts on save.
If you put the relevant parts of this code into the save_related() method of the admin, the changes should persist.
I can be more specific about where it should go if you'll post both your < app >/models.py and your < app >/admin.py files.
Reference from another SO question:
Issue with ManyToMany Relationships not updating inmediatly after save
Is there any way to make a django forms class that actually holds an array of fields? I have a database that will pull up a variable number of questions to ask the user and each question will know how to define it's widget...etc, I just can't seem to hook this up to django forms.
I tried this:
class MyForm(forms.Form):
question = []
questions = Question.objects.all()
for q in questions:
question.append(forms.CharField(max_length=100, label=q.questionText))
But this doesn't seem to expose my questions list when I create a new instance of MyForm. Is there any way to get a variable number of form fields using django forms, or is this beyond the scope of what it can do?
You may be able to use formsets if your forms are identical (including their labels). e.g.
Question: __________________
Question: __________________
Question: __________________
etc. I'm assuming each form contains only one field here (the 'Question' field). There are three forms in this example.
If you need a dynamic number of fields in a single form, then you can use __init__ to achieve what you want (note: untested code!):
class MyForm(forms.Form):
def __init__(self, *args, **kwargs):
questions = kwargs.pop('questions')
super(MyForm, self).__init__(*args, **kwargs)
counter = 1
for q in questions:
self.fields['question-' + str(counter)] = forms.CharField(label=question)
counter += 1
And you'd create the form with something like:
form = MyForm(questions=your_list_of_questions)
You'll find this article useful: http://jacobian.org/writing/dynamic-form-generation/
Of course you can!
class MyForm(forms.Form):
def __init__(self, *args, **kwargs):
super(MyForm, self).__init__(*args, **kwargs)
for i, q in enumerate(Question.objects.all()):
self.fields['%s_field' % i] = forms.CharField(max_length=100, label=q.questionText)
Note: make sure your questions are ordered between calls.. as the field list will be repopulated upon form submission, receipt, etc.
If the data is ordered and static, it won't be a problem.
Also you may want to look into FormSets, a list of forms which may be more fitting in your case.
I need to generate several textareas to be filled by users and submitted back to the model. However, I think they need all to have different names (am i correct?)
How can I generate random names for each textarea and how can the model get the data within one it gets the POST request?
Thanks!
EDIT: I was hoping to use the randomly generated name as a way to identify the content and to save them in the database
It's hard to give you a good answer here because you haven't indicated what you're actually trying to achieve. You could add a 1000 text fields to your form, but if they don't correlate somehow to data on your model, there's not much point, and you've neglected that crucial piece of information.
Still, on a very basic level, you can add the additional textareas to your form like so:
class MyModelForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(MyModelForm, self).__init__(*args, **kwargs)
for i in range(1, 11):
self.fields['mytextarea%d' % i] = forms.CharField(label='Textarea %d' % i, widget=forms.Textarea)
UPDATE
Based on your comment, and the fact that you're intending to actually store and retrieve the textarea fields at a later point, you should create a separate model for them and link it to your main model through a one-to-many relationship. For example:
class PrimaryModel(models.Model):
... # Stuff here
class TextareaModel(models.Model):
myprimarymodel = models.ForeignKey(PrimaryModel)
mytextarea = models.TextField()
It's hard to give a good example since you haven't indicated anything about what your models look like, but the basic idea is that you have a model that contains nothing but a foreign key to the main model and a textarea field. You can then treat these textareas as inlines with via a model formset.
So I have a model that is shown in inline form.
That model have ManyToManyField.
Imagine that there are several inline-objects that are already created.
The problem is how to show different querysets of available objects in my m2m-field based on original inline-object.
One more time:)
I mean that in each inline-object must by m2m-field with different available variants.
Variants will of course include all that is actually set for this inline-object + they must include only variants that are not present at the moment anywhere else.
Thanks.
Question is very poorly written, so it's hard to be sure exactly what you're looking for, but my best guess is that you're want to limit the queryset for the ManyToManyField to items that are not assigned to anything else? If that's correct:
(You also didn't post an example model, so I'll make one up to illustrate)
class SomeModel(models.Model):
my_m2m_field = models.ManyToManyField(OtherModel)
And, here's the code to limit the field based on that:
class SomeModelInlineAdminForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(MyInlineAdminForm, self).__init__(*args, **kwargs)
self.fields['my_m2m_field'].queryset = OtherModel.objects.filter(somemodel__isnull=True)
class SomeModelInlineAdmin(admin.TabularInline):
model = SomeModel
form = SomeModelInlineAdminForm