Wagtail - how to preopulate fields in admin form? - django

I would like to pre-populate fields in wagtail page admin. Particularly I would like to take username of currently logged admin/editor user and fill it in the form as a string. A simplified version of my page looks like this:
class ItemPage(Page):
author = models.CharField(max_length=255, default="")
content_panels = Page.content_panels + [
FieldPanel('author'),
]
I do not want to set a default value in the author field in the model - it should be user specific.
I do not want to use the save method or signal after the model is saved/altered. The user should see what is there and should have the ability to change it. Also, the pages will be generated automatically without the admin interface.
I think that I need something like https://stackoverflow.com/a/14322706/3960850 but not in Django, but with the Wagtail ModelAdmin.
How to achieve this in Wagtail?

Here is an example based on gasmans comment and the documentation that accompanies the new code they linked:
from wagtail.admin.views.pages import CreatePageView, register_create_page_view
from myapp.models import ItemPage
class ItemPageCreateView(CreatePageView):
def get_page_instance(self):
page = super().get_page_instance()
page.author = 'Some default value'
return page
register_create_page_view(ItemPage, ItemPageCreateView.as_view())
You could also do this by overriding the models init method, but the above is much nicer
class ItemPage(Page):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
try:
author = kwargs['owner'].username
except (AttributeError, KeyError):
pass
else:
self.author = author

Related

Reload choices dynamically when using MultipleChoiceFilter

I am trying to construct a MultipleChoiceFilter where the choices are the set of possible dates that exist on a related model (DatedResource).
Here is what I am working with so far...
resource_date = filters.MultipleChoiceFilter(
field_name='dated_resource__date',
choices=[
(d, d.strftime('%Y-%m-%d')) for d in
sorted(resource_models.DatedResource.objects.all().values_list('date', flat=True).distinct())
],
label="Resource Date"
)
When this is displayed in a html view...
This works fine at first, however if I create new DatedResource objects with new distinct date values I need to re-launch my webserver in order for them to get picked up as a valid choice in this filter. I believe this is because the choices list is evaluated once when the webserver starts up, not every time my page loads.
Is there any way to get around this? Maybe through some creative use of a ModelMultipleChoiceFilter?
Thanks!
Edit:
I tried some simple ModelMultipleChoice usage, but hitting some issues.
resource_date = filters.ModelMultipleChoiceFilter(
field_name='dated_resource__date',
queryset=resource_models.DatedResource.objects.all().values_list('date', flat=True).order_by('date').distinct(),
label="Resource Date"
)
The HTML form is showing up just fine, however the choices are not accepted values to the filter. I get "2019-04-03" is not a valid value. validation errors, I am assuming because this filter is expecting datetime.date objects. I thought about using the coerce parameter, however those are not accepted in ModelMultipleChoice filters.
Per dirkgroten's comment, I tried to use what was suggested in the linked question. This ends up being something like
resource_date = filters.ModelMultipleChoiceFilter(
field_name='dated_resource__date',
to_field_name='date',
queryset=resource_models.DatedResource.objects.all(),
label="Resource Date"
)
This also isnt what I want, as the HTML now form is now a) displaying the str representation of each DatedResource, instead of the DatedResource.date field and b) they are not unique (ex if I have two DatedResource objects with the same date, both of their str representations appear in the list. This also isnt sustainable because I have 200k+ DatedResources, and the page hangs when attempting to load them all (as compared to the values_list filter, which is able to pull all distinct dates out in seconds.
One of the easy solutions will be overriding the __init__() method of the filterset class.
from django_filters import filters, filterset
class FooFilter(filterset.FilterSet):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
try:
self.filters['user'].extra['choices'] = [(d, d.strftime('%Y-%m-%d')) for d in sorted(
resource_models.DatedResource.objects.all().values_list('date', flat=True).distinct())]
except (KeyError, AttributeError):
pass
resource_date = filters.MultipleChoiceFilter(field_name='dated_resource__date', choices=[], label="Resource Date")
NOTE: provide choices=[] in your field definition of filterset class
Results
I tested and verified this solution with following dependencies
1. Python 3.6
2. Django 2.1
3. DRF 3.8.2
4. django-filter 2.0.0
I used following code to reproduce the behaviour
# models.py
from django.db import models
class Musician(models.Model):
name = models.CharField(max_length=50)
def __str__(self):
return f'{self.name}'
class Album(models.Model):
artist = models.ForeignKey(Musician, on_delete=models.CASCADE)
name = models.CharField(max_length=100)
release_date = models.DateField()
def __str__(self):
return f'{self.name} : {self.artist}'
# serializers.py
from rest_framework import serializers
class AlbumSerializer(serializers.ModelSerializer):
artist = serializers.StringRelatedField()
class Meta:
fields = '__all__'
model = Album
# filters.py
from django_filters import rest_framework as filters
class AlbumFilter(filters.FilterSet):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.filters['release_date'].extra['choices'] = self.get_album_filter_choices()
def get_album_filter_choices(self):
release_date_list = Album.objects.values_list('release_date', flat=True).distinct()
return [(date, date) for date in release_date_list]
release_date = filters.MultipleChoiceFilter(choices=[])
class Meta:
model = Album
fields = ('release_date',)
# views.py
from rest_framework.viewsets import ModelViewSet
from django_filters import rest_framework as filters
class AlbumViewset(ModelViewSet):
serializer_class = AlbumSerializer
queryset = Album.objects.all()
filter_backends = (filters.DjangoFilterBackend,)
filter_class = AlbumFilter
Here I've used the django-filter with DRF.
Now, I populated some data through Django Admin console. After that, the album api become as below,
and I got the release_date as
Then, I added new entry through Django admin -- (Screenshot) and I refresh the DRF API endpoint and the possible choices became as below,
I have looked into your problem and I have following suggestions
The Problem
You have got the problem right. Choices for your MultipleChoiceFilter are calculated statically whenever you run server.Thats why they don't get updated dynamically whenever you insert new instance in DatedResource.
To get it working correctly, you have to provide choices dynamically to MultipleChoiceFilter. I searched in documentation but did not find anything regarding this. So here is my solution.
The solution
You have to extend MultipleChoiceFilter and create your own filter class. I have created this and here it is.
from typing import Callable
from django_filters.conf import settings
import django_filters
class LazyMultipleChoiceFilter(django_filters.MultipleChoiceFilter):
def get_field_choices(self):
choices = self.extra.get('choices', [])
if isinstance(choices, Callable):
choices = choices()
return choices
#property
def field(self):
if not hasattr(self, '_field'):
field_kwargs = self.extra.copy()
if settings.DISABLE_HELP_TEXT:
field_kwargs.pop('help_text', None)
field_kwargs.update(choices=self.get_field_choices())
self._field = self.field_class(label=self.label, **field_kwargs)
return self._field
Now you can use this class as replacement and pass choices as lambda function like this.
resource_date = LazyMultipleChoiceFilter(
field_name='dated_resource__date',
choices=lambda: [
(d, d.strftime('%Y-%m-%d')) for d in
sorted(resource_models.DatedResource.objects.all().values_list('date', flat=True).distinct())
],
label="Resource Date"
)
Whenever instance of filter will be created choices will be updated dynamically. You can also pass choices statically (without lambda function) to this field if want default behavior.

Add method doesnt work when trying to establish m2m relationships using post_save in Django

My Content model has a many-to-many relationship to the Tag model. When I save a Content object, I want to add the relationships dynamically. I'm doing this the following way.
def tag_content(obj):
for tag in Tag.objects.all():
print tag
obj.tags.add(tag)
obj.is_tagged = True
obj.save()
class Tag(models.Model):
name = models.CharField(max_length=255)
class Content(models.Model):
title = models.CharField(max_length=255)
is_tagged = models.BooleanField(default=False)
tags = models.ManyToManyField(Tag, blank=True)
def save(self, *args, **kwargs):
super(Content, self).save(*args, **kwargs)
#receiver(post_save, sender = Content)
def update_m2m_relationships_on_save(sender, **kwargs):
if not kwargs['instance'].is_tagged:
tag_content(kwargs['instance'])
The tag_content function runs, however, the m2m relationships are not established. Im using Django 1.9.8 btw. This makes no sense. What am I missing? Moreover, if I do something like tag_content(content_instance) in shell, then the tags are set, so the function is ok. I guess the problem is in the receiver. Any help?
Edit
My question has nothing to do with m2m_changed, as I have said, creating a Content object in shell works perfectly. Therefore, the problem lies in the admin panel's setup.
Ok so I solved the problem. Basically, this has something to do with how Django handles its form in the admin panel. When trying to add the Contents from admin, I kept the tags field empty, thinking the tag_content function would handle it. However, that is exactly where the problem was, as creating a Content from shell tagged it just fine. In other words, changing the admin panel to something like this solved my problem :
from django.contrib import admin
from myapp.models import *
from django import forms
class ContentCreationForm(forms.ModelForm):
class Meta:
model = Content
fields = ('title',)
class ContentChangeForm(forms.ModelForm):
class Meta:
model = Content
fields = ('title', 'is_tagged', 'tags')
class ContentAdmin(admin.ModelAdmin):
def get_form(self, request, obj=None, **kwargs):
if obj is None:
return ContentCreationForm
else:
return ContentChangeForm
admin.site.register(Tag)
admin.site.register(Content, ContentAdmin)
When trying to create a new Content, only the 'title' field is presented. This solves the problem.

ForeignKey field will not appear in Django admin site

A foreign key on a model is not appearing in the Django admin site. This is irrespective of whether the field is explicitly specified in a ModelAdmin instance (fields = ('title', 'field-that-does-not-show-up')) or not.
I realize there are many variables that could be causing this behavior.
class AdvertiserAdmin(admin.ModelAdmin):
search_fields = ['company_name', 'website']
list_display = ['company_name', 'website', 'user']
class AdBaseAdmin(admin.ModelAdmin):
list_display = ['title', 'url', 'advertiser', 'since', 'updated', 'enabled']
list_filter = ['updated', 'enabled', 'since', 'updated', 'zone']
search_fields = ['title', 'url']
The problem is the advertiser foreign key is not showing up in the admin for AdBase
class Advertiser(models.Model):
""" A Model for our Advertiser
"""
company_name = models.CharField(max_length=255)
website = models.URLField(verify_exists=True)
user = models.ForeignKey(User)
def __unicode__(self):
return "%s" % self.company_name
def get_website_url(self):
return "%s" % self.website
class AdBase(models.Model):
"""
This is our base model, from which all ads will inherit.
The manager methods for this model will determine which ads to
display return etc.
"""
title = models.CharField(max_length=255)
url = models.URLField(verify_exists=True)
enabled = models.BooleanField(default=False)
since = models.DateTimeField(default=datetime.now)
expires_on=models.DateTimeField(_('Expires on'), blank=True, null=True)
updated = models.DateTimeField(editable=False)
# Relations
advertiser = models.ForeignKey(Advertiser)
category = models.ForeignKey(AdCategory)
zone = models.ForeignKey(AdZone)
# Our Custom Manager
objects = AdManager()
def __unicode__(self):
return "%s" % self.title
#models.permalink
def get_absolute_url(self):
return ('adzone_ad_view', [self.id])
def save(self, *args, **kwargs):
self.updated = datetime.now()
super(AdBase, self).save(*args, **kwargs)
def impressions(self, start=None, end=None):
if start is not None:
start_q=models.Q(impression_date__gte=start)
else:
start_q=models.Q()
if end is not None:
end_q=models.Q(impression_date__lte=end)
else:
end_q=models.Q()
return self.adimpression_set.filter(start_q & end_q).count()
def clicks(self, start=None, end=None):
if start is not None:
start_q=models.Q(click_date__gte=start)
else:
start_q=models.Q()
if end is not None:
end_q=models.Q(click_date__lte=end)
else:
end_q=models.Q()
return self.adclick_set.filter(start_q & end_q).count()
class BannerAd(AdBase):
""" A standard banner Ad """
content = models.ImageField(upload_to="adzone/bannerads/")
The mystery deepens. I just tried to create a ModelForm object for both AdBase and BannerAd, and both generated fields for the advertiser. Some crazy admin things going on here...
I believe I've just run into exactly the same problem, but was able to debug it thanks to the help of persistent co-workers. :)
In short, if you look in the raw HTML source you'll find the field was always there - it's just that:
Django tries to be clever and put the form field inside a div with CSS class="form-row $FIELD_NAME",
The field's name was "advertiser", so the CSS class was "form-row advertiser",
...Adblock Plus.
Adblock Plus will hide anything with the CSS class "advertiser", along with a hell of a lot of other CSS classes.
I consider this a bug in Django.
maybe it is an encode error. I had the same problem, but when i added # -- coding: UTF-8 -- in the models.py, all fine.
Another very dumb cause of the same problem:
If there is only one instance of the related model, then the filter simply won't show. There is a has_output() method in RelatedFieldListFilter class that returns False in this case.
It's a strange problem for sure. On the AdBase model if you change
advertiser = models.ForeignKey(Advertiser)
to
adver = models.ForeignKey(Advertiser)
then I believe it'll show up.
Powellc, do you have the models registered with their respective ModelAdmin class?
admin.site.register(Advertiser, AdvertiserAdmin) after the ModelAdmin definitions.
You are talking about the list_display option, right?
Is the unicode-method for your related model set?
If the field is a ForeignKey, Django
will display the unicode() of the
related object
Also check this thread for some hints: Can "list_display" in a Django ModelAdmin display attributes of ForeignKey fields?
Try disabling your ad blocker. No, this is not a joke. I just ran into this exact problem.
We just ran into this problem.
It seems that if you call you field advertiser the in the admin the gets given an 'advertiser' class.
Then is then hidden by standard ad blocking plugins. If you view source your field will be there.

In django, how to limit choices of a foreignfield based on another field in the same model?

I have these models (I have limited the number of fields to just those needed)
class unit(models.Model):
name = models.CharField(max_length=200)
class project(models.Model):
name = models.CharField(max_length=200)
class location(address):
project = models.ForeignKey(project)
class project_unit(models.Model):
project = models.ForeignKey(project)
unit = models.ForeignKey(unit)
class location_unit(models.Model):
project = models.ForeignKey(project)
#Limit the selection of locations based on which project has been selected
location = models.ForeignKey(location)
#The same here for unit. But I have no idea how.
unit = models.ForeignKey(project_unit)
My newbie head just cannot grasp how to limit the two fields, location and unit, in the location_unit model to only show the choices which refers to the selected project in location_unit. Should I override the modelform and make a query there or can I use the limit_choices_to. Either way I have failed trying both
Edit: Just to clarify, I want this to happen in the Django Admin. I have also tried formfield_for_foreignkey, but still a no go for me.
EDIT 2:
def formfield_for_foreignkey(self, db_field, request, **kwargs):
if db_field.name == "unit":
kwargs["queryset"] = project_unit.objects.filter(project=1)
return db_field.formfield(**kwargs)
return super(location_unit_admin, self).formfield_for_foreignkey(db_field, request, **kwargs)
The above code snippet works. But of course I don't want the project to point to 1. How do I reference to the models project_id?
I tried this:
kwargs["queryset"] = project_unit.objects.filter(project=self.model.project.project_id)
But that doesn't work (actually I have tried a lot of variations, yes I am a django newbie)
This is the answer, it is brilliant: https://github.com/digi604/django-smart-selects
Your formfield_for_foreignkey looks like it might be a good direction, but you have to realize that the ModelAdmin (self) won't give you a specific instance. You'll have to derive that from the request (possibly a combination of django.core.urlresolvers.resolve and request.path)
If you only want this functionality in the admin (and not model validation in general), you can use a custom form with the model admin class:
forms.py:
from django import forms
from models import location_unit, location, project_unit
class LocationUnitForm(forms.ModelForm):
class Meta:
model = location_unit
def __init__(self, *args, **kwargs):
inst = kwargs.get('instance')
super(LocationUnitForm, self).__init__(*args, **kwargs)
if inst:
self.fields['location'].queryset = location.objects.filter(project=inst.project)
self.fields['unit'].queryset = project_unit.objects.filter(project=inst.project)
admin.py:
from django.contrib import admin
from models import location_unit
from forms import LocationUnitForm
class LocationUnitAdmin(admin.ModelAdmin):
form = LocationUnitForm
admin.site.register(location_unit, LocationUnitAdmin)
(Just wrote these on the fly with no testing, so no guarantee they'll work, but it should be close.)

Django: MultipleChoiceField in admin to carry over previously saved values

I am having troubles to carry over previously selected items in a ModelForm in the admin.
I want to use the forms.CheckboxSelectMultiple widget since that is the most straightforward UI in this usecase. It works as far that when saving, the values are stored. But when editing the previously saved item, the values previously saved in this field are not reflected in the widget.
UI Example:
After posting (editing that item, returns it blank):
However, when not using the widget but a regular CharField when editing the item it looks like:
So for some reason the values are not represented by the checkbox widget?
Here's my simplified setup, models.py
POST_TYPES = (
('blog', 'Blog'),
('portfolio', 'Portfolio'),
('beeldbank', 'Beeldbank'),
)
class Module(models.Model):
title = models.CharField(max_length=100, verbose_name='title')
entriesFrom = models.CharField(max_length=100)
def __unicode__(self):
return self.title
forms.py:
class ModuleForm(forms.ModelForm):
entriesFrom = forms.MultipleChoiceField(
choices=POST_TYPES,
widget=CheckboxSelectMultiple,
label="Pull content from",
required=False,
show_hidden_initial=True)
class Meta:
model = Module
def __init__(self, *args, **kwargs):
super(ModuleForm, self).__init__(*args, **kwargs)
if kwargs.has_key('instance'):
instance = kwargs['instance']
self.fields['entriesFrom'].initial = instance.entriesFrom
logging.debug(instance.entriesFrom)
admin.py
class ModuleAdmin(admin.ModelAdmin):
form = ModuleForm
So when editing a previously saved item with say 'blog' selected, debugging on init returns me the correct values on self.fields['entriesFrom'] ([u'blog',]), but its not reflected in the checkboxes (nothing is shown as selected) in the admin.
edit
updated the ModuleForm class to pass on initial values, but nothing still gets pre-populated whilst there are a few values in the initial value ("[u'blog']").
Solution:
Setting the choices by a integer, instead of a string.
POST_TYPES = (
(1, 'Blog'),
(2, 'Portfolio'),
(3, 'Beeldbank'),
)
Damn, that wasn't worth breaking my skull over.
Might not be correct, but for my usecase, I did not want to replace the values with integers (as per the accepted answer). This was arrived at by a smidge of trial-and-error, and a lot of stepping through Django internals. Works for me, but YMMV:
from django.forms.widgets import (
CheckboxSelectMultiple as OriginalCheckboxSelectMultiple,
)
class CheckboxSelectMultiple(OriginalCheckboxSelectMultiple):
def optgroups(self, name, value, attrs=None):
# values come back as e.g. `['foo,bar']`, which we don't want when inferring "selected"
return super().optgroups(name, value[0].split(","), attrs)
Maybe I'm not understanding your question completely, but it seems like you could simplify a little. Using ModelForms, I don't think any of your overriding the _init_ in your form is necessary. Try this and see if you get your desired behavior.
models.py
class Module(models.Model):
POST_TYPES = (
('blog', 'Blog'),
('portfolio', 'Portfolio'),
)
title = models.CharField(max_length=100, verbose_name='title')
entriesFrom = models.CharField(max_length=100, verbose_name='Pull content from', choices=POST_TYPES, blank=True)
def __unicode__(self):
return self.title
forms.py
class ModuleForm(forms.ModelForm):
class Meta:
model = Module
admin.py
from django.contrib import admin
admin.site.register(models.Module)
If my answer isn't what you're looking for, try clarifying your question and I'll see if I can help you out.
you can use this function to remove string mark
from ast import literal_eval
literal_eval(value)
I faced this issue, my changes haven't affected on save.
I use CharField in model, but in forms.py;
class ModuleForm(forms.ModelForm):
my_field = forms.MultipleChoiceField(
choices=POST_TYPES,
widget=CheckboxSelectMultiple,
required=False,)
def __init__(self, *args, **kwargs):
super(ModuleForm, self).__init__(*args, **kwargs)
if kwargs.get('instance'):
self.initial['my_field'] = eval(self.initial['my_field'])
This form solution worked for me on MultipleChoiceField on Django Admin.