How to let wagtail's TimeInput to accept seconds? - wagtail

I'm using TimeField in my model but I'm not able to fill seconds in wagtail's wagtail.contrib.modeladmin.options.ModelAdmin.
My current ModelAdmins code is:
#modeladmin_register
class ScheduleAdmin(ModelAdmin):
model = ScheduleCell
menu_label = _("Schedule")
menu_icon = 'date'
menu_order = 200
add_to_settings_menu = False
exclude_from_explorer = False
list_display = ('start_time', 'end_time', 'page', 'output_devices')
search_fields = ('page__title', )
current result is
When I'm trying to write seconds to the input manually - wagtail does not allow it.
How to resolve it?

The date chooser widget doesn't support adding seconds, so you'll need to override this to use a basic text input widget instead. You can do this by adding a panels definition to your model (in the next Wagtail release, Wagtail 2.5, it will be possible to define this on the ModelAdmin class too), and specifying the widget there:
from django import forms
from wagtail.admin.edit_handlers import FieldPanel
class ScheduleCell(models.Model):
# ... field definitions here ...
panels = [
# ...
FieldPanel('start_time', widget=forms.TextInput),
FieldPanel('end_time', widget=forms.TextInput),
# ...
]

Related

How can I set the number of initial empty (orderable) items in an InlinePanel?

I have an Orderable model called SetListItem with a ParentalKey on a ClusterableModel called FloorWithSets. The parent FloorWithSets model defines using an InlinePanel to control adding/ordering/removing of the SetListItems. The issue I have is that the admin form automatically renders three empty SetListItems for each FloorWithSets, and I cannot find any way to control this setting.
The InlinePanel class takes parameters to, e.g. set the minimum and maximum number of items, but nothing to set the number of initial empty items rendered.
I cannot find any information about this in the Wagtail docs. I've also dug into the source for InlinePanel and EditHandler but cannot find anything I could override.
I do see from the InlinePanel template file that there is a hidden input with id ending -INITIAL_FORMS which is being rendered via self.formset.management_form. The value of this field is consistently lower than a neighbouring hidden input with id ending -TOTAL_FORMS, which makes sense. I just don't understand where the value is coming from or how to control it.
The only information I can find about this INITIAL_FORMS all seems to relate to testing, (e.g. this documentation) and I cannot see how to relate what that says to what I need.
class FloorWithSets(ClusterableModel):
page = ParentalKey(EventPage, on_delete=models.CASCADE, related_name='floor_with_sets')
FLOOR_CHOICES = [
('1', 'X'),
('2', 'Y'),
('3', 'Z'),
]
floor = models.CharField(
max_length=1,
choices=FLOOR_CHOICES,
default='1',
)
panels = [
FieldPanel('floor'),
InlinePanel('set_list', label=_("set")),
]
class SetListItem(Orderable):
floor = ParentalKey(FloorWithSets, on_delete=models.CASCADE, related_name='set_list')
artist = models.CharField(max_length=255, blank=True, verbose_name=_('artist'))
label = models.CharField(max_length=255, blank=True, verbose_name=_('label'))
start_time = models.TimeField(blank=True, null=True, verbose_name=_('start time'))
end_time = models.TimeField(blank=True, null=True, verbose_name=_('end time'))
set_list_item = FieldRowPanel([
FieldPanel('artist', classname="col6"),
FieldPanel('label', classname="col6")
])
set_list_item_details = FieldRowPanel([
FieldPanel('start_time', classname="col6"),
FieldPanel('end_time', classname="col6")
])
panels = [set_list_item, set_list_item_details]
I think I've found a solution. Try creating a custom Form class for your EventPage model with a custom metaclass like so:
class EventPageFormMetaclass(WagtailAdminModelFormMetaclass):
#classmethod
def child_form(cls):
return EventPageForm
class EventPageForm(WagtailAdminPageForm, metaclass=EventPageFormMetaclass):
pass
class EventPage(Page):
# Whatever you have in your model
base_form_class = EventPageForm
I believe the problem stems from the fact that the ClusterFormMetaclass is hard-coded to create instances of ClusterForm for child models. So your EventPage gets a WagtailAdminPageForm, but the FloorWithSets models gets a ClusterForm. If you stop there, it's fine, but when FloorWithSets generates it's inline panels, it does so as a ClusterForm, whose metaclass has extra_form_count set to 3, as opposed to the WagtailAdminPageForm whose metaclass has it set to 0.
So the solution above creates a new Form class, whose metaclass overrides the child_form class method to return a Form class with extra_form_count set to 0.
Whew.

Filtering on PageQuerySet (which is retuned by specific()) is returning FieldError in Wagtail

I am using subclassed model of Wagtail Page.
In below code you can see that PhoenixPage is base page which subclasses Wagtail Page model.
PhoenixArticlePage & PhoenixMealPrepPage subclasses PhoenixPage
PhoenixArticleIndexPage subclasses PhoenixBaseIndexPage which in turn subclasses PhoenixPage
Idea is to use PhoenixArticleIndexPage for all other article pages.
Problem is even after using the specific() method on queryset i am unable to use filter or any other operation on the queryset.
i tried using order_by() as well as filter()
Can someone share some insights here ? what might be wrong ?
Here is a model example:
class PhoenixPage(Page):
"""
General use page with caching, templating, and SEO functionality.
All pages should inherit from this.
"""
class Meta:
verbose_name = _("Phoenix Page")
# Do not allow this page type to be created in wagtail admin
is_creatable = False
tags = ClusterTaggableManager(
through=PhoenixBaseTag,
verbose_name="Tags",
blank=True,
related_name="phoenixpage_tags",
)
class PhoenixBaseIndexPage(PaginatedListPageMixin, PhoenixPage):
class meta:
verbose_name = "Phoenix Base Index Page"
app_label = "v1"
index_show_subpages_default = True
is_creatable = False
class PhoenixArticleIndexPage(PhoenixBaseIndexPage):
class Meta:
verbose_name = "Phoenix Article Index Page"
app_label = "v1"
class PhoenixArticlePage(PhoenixPage):
class Meta:
verbose_name = "Phoenix Article Page"
app_label = "v1"
subpage_types = []
parent_page_types = ["v1.PhoenixArticleIndexPage"]
class PhoenixMealPrepPage(PhoenixPage):
class Meta:
verbose_name = "Phoenix Meal Prep Page"
app_label = "v1"
subpage_types = []
parent_page_types = ["v1.PhoenixArticleIndexPage"]
Here are shell queries i tried.
Index page
In [4]: a = PhoenixArticleIndexPage.objects.all()[0]
In [5]: a
Out[5]: <PhoenixArticleIndexPage: articles>
As expected, get_children returning all instances of Wagtail Page.
In [6]: a.get_children()
Out[6]: <PageQuerySet [<Page: article title>, <Page: article title2>, <Page: Our 30-Day Reset Recipes Are So Easy AND Delicious>]>
Getting specific children from the Index page.
In [7]: a.get_children().specific()
Out[7]: <PageQuerySet [<PhoenixArticlePage: article title>, <PhoenixArticlePage: article title2>, <PhoenixMealPrepPage: Our 30-Day Reset Recipes Are So Easy AND Delicious>]>
Get Tag and try to filter the queryset
In [8]: q = a.get_children().specific()
In [12]: m = PhoenixTag.objects.get(slug='meal')
In [16]: k={"tags":m}
In [19]: q.filter(**k)
***FieldError: Cannot resolve keyword 'tags' into field. Choices are ...***
But if i go to particular entry in queryset then i can see tags field on it.
In [15]: q[2]
Out[15]: <PhoenixMealPrepPage: Our 30-Day Reset Recipes Are So Easy AND Delicious>
In [16]: q[2].tags
Out[16]: <modelcluster.contrib.taggit._ClusterTaggableManager at 0x1060832b0>
Could be different question all together but for reference adding it here.
Found the corner case of using difference() and specific() method on a queryset.
In [87]: q = PhoenixPage.objects.child_of(a).live()
In [89]: f = q.filter(featured=True)[:3]
In [91]: l = q.difference(f)
In [93]: l.order_by(a.index_order_by).specific() . <-- does not work
DatabaseError: ORDER BY term does not match any column in the result set.
The specific() method on PageQuerySet works by running the initial query on the basic Page model as normal, then running additional queries - one for each distinct page type found in the results - to retrieve the information from the specific page models. This means it's not possible to use fields from the specific model in filter or order_by clauses, because those have to be part of the initial query, and at that point Django has no way to know which page models are involved.
However, if you know that your query should only ever return pages of one particular type (PhoenixPage in this case) containing the field you want to filter/order on, you can reorganise your query expression so that the query happens on that model instead:
PhoenixPage.objects.child_of(a).filter(tags=m).specific()

Why is the SnippetChooserPanel not opening in Wagtail?

Some time ago I stopped using #register_snippet to decorate Snippets. This takes the Snippet out of the snippets section of admin.
Instead I used the wagtail_hooks.py to show the snippet directly in the left admin panel for user convenience. See below. This works nicely as the user can go directly to the snippet and you can also alter the displayed fields and ordering of fields - nice.
So in the below example I removed the line that says #register_snippet. What's the catch? The SnippetChooserPanel does not work! Later I was building a complex model and the SnippetChooserPanel did not work. I wasted quite a bit of time thinking the problem was in the complexity of my model. I want to save others' time!
wagtail_hooks.py:
from wagtail.contrib.modeladmin.options import ModelAdmin, modeladmin_register
from wagtail.wagtailsnippets.models import register_snippet
from demo.models import Advert
class AdvertAdmin(ModelAdmin):
model = Advert
modeladmin_register(AdvertAdmin)
Here is the snippet example from Wagtail: snippets
#register_snippet #<------- Source of issue (I removed this line!)
#python_2_unicode_compatible # provide equivalent __unicode__ and __str__ methods on Python 2
class Advert(models.Model):
url = models.URLField(null=True, blank=True)
text = models.CharField(max_length=255)
panels = [
FieldPanel('url'),
FieldPanel('text'),
]
def __str__(self):
return self.text
class BookPage(Page):
advert = models.ForeignKey(
'demo.Advert',
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name='+'
)
content_panels = Page.content_panels + [
SnippetChooserPanel('advert'),
# ...
]
If you make your Snippets editable via modelAdmin you still need to apply the decorator #register_snippet. Otherwise the chooser panel route/view won't be available. This view is requested by the ajax request fired on SnippetChooser modal open. Missing #register snippet will trow a 404.
You can register menu items via construct_main_menu hook. You can use the same hook to remove exiting menu-items. If you do not want the 'Snippets' menu item remove it. In wagtail_hooks.py:
#hooks.register('construct_main_menu')
def hide_snippet(request, menu_items):
menu_items[:] = [item for item in menu_items if item.name != 'snippets']
The solution is always use #register_snippet decorator otherwise the SnippetChooserPanel doesn't work!
#register_snippet
#python_2_unicode_compatible
class Advert(models.Model):
url = models.URLField(null=True, blank=True)
text = models.CharField(max_length=255)
panels = [
FieldPanel('url'),
FieldPanel('text'),
]
def __str__(self):
return self.text

How to show all fields of model in admin page?

here is the models page
In this picture, only the title shows up on here, I used:
def __unicode__(self):
return self.title;
here is the each individual objects
How do I show all these fields?
How do I show all the fields in each Model page?
If you want to include all fields without typing all fieldnames, you can use
list_display = BookAdmin._meta.get_all_field_names()
The drawback is, the fields are in sorted order.
Edit:
This method has been deprecated in Django 1.10 See Migrating from old API for reference. Following should work instead for Django >= 1.9 for most cases -
list_display = [field.name for field in Book._meta.get_fields()]
By default, the admin layout only shows what is returned from the object's unicode function. To display something else you need to create a custom admin form in app_dir/admin.py.
See here: https://docs.djangoproject.com/en/dev/ref/contrib/admin/#django.contrib.admin.ModelAdmin.list_display
You need to add an admin form, and setting the list_display field.
In your specific example (admin.py):
class BookAdmin(admin.ModelAdmin):
list_display = ('title', 'author', 'price')
admin.site.register(Book, BookAdmin)
If you want to include all but the ManyToManyField field names, and have them in the same order as in the models.py file, you can use:
list_display = [field.name for field in Book._meta.fields if field.name != "id"]
As you can see, I also excluded the id.
If you find yourself doing this a lot, you could create a subclass of ModelAdmin:
class CustomModelAdmin(admin.ModelAdmin):
def __init__(self, model, admin_site):
self.list_display = [field.name for field in model._meta.fields if field.name != "id"]
super(CustomModelAdmin, self).__init__(model, admin_site)
and then just inherit from that:
class BookAdmin(CustomModelAdmin):
pass
or you can do it as a mixin:
class CustomModelAdminMixin(object):
def __init__(self, model, admin_site):
self.list_display = [field.name for field in model._meta.fields if field.name != "id"]
super(CustomModelAdminMixin, self).__init__(model, admin_site)
class TradeAdmin(CustomModelAdminMixin, admin.ModelAdmin):
pass
The mixin is useful if you want to inherit from something other than admin.ModelAdmin.
I found OBu's answer here to be very useful for me. He mentions:
The drawback is, the fields are in sorted order.
A small adjustment to his method solves this problem as well:
list_display = [f.name for f in Book._meta.fields]
Worked for me.
The problem with most of these answers is that they will break if your model contains ManyToManyField or ForeignKey fields.
For the truly lazy, you can do this in your admin.py:
from django.contrib import admin
from my_app.models import Model1, Model2, Model3
#admin.register(Model1, Model2, Model3)
class UniversalAdmin(admin.ModelAdmin):
def get_list_display(self, request):
return [field.name for field in self.model._meta.concrete_fields]
Many of the answers are broken by Django 1.10. For version 1.10 or above, this should be
list_display = [f.name for f in Book._meta.get_fields()]
Docs
Here is my approach, will work with any model class:
MySpecialAdmin = lambda model: type('SubClass'+model.__name__, (admin.ModelAdmin,), {
'list_display': [x.name for x in model._meta.fields],
'list_select_related': [x.name for x in model._meta.fields if isinstance(x, (ManyToOneRel, ForeignKey, OneToOneField,))]
})
This will do two things:
Add all fields to model admin
Makes sure that there is only a single database call for each related object (instead of one per instance)
Then to register you model:
admin.site.register(MyModel, MySpecialAdmin(MyModel))
Note: if you are using a different default model admin, replace 'admin.ModelAdmin' with your admin base class
Show all fields:
list_display = [field.attname for field in BookModel._meta.fields]
Every solution found here raises an error like this
The value of 'list_display[n]' must not be a ManyToManyField.
If the model contains a Many to Many field.
A possible solution that worked for me is:
list_display = [field.name for field in MyModel._meta.get_fields() if not x.many_to_many]
I like this answer and thought I'd post the complete admin.py code (in this case, I wanted all the User model fields to appear in admin)
from django.contrib import admin
from django.contrib.auth.models import User
from django.db.models import ManyToOneRel, ForeignKey, OneToOneField
MySpecialAdmin = lambda model: type('SubClass'+model.__name__, (admin.ModelAdmin,), {
'list_display': [x.name for x in model._meta.fields],
'list_select_related': [x.name for x in model._meta.fields if isinstance(x, (ManyToOneRel, ForeignKey, OneToOneField,))]
})
admin.site.unregister(User)
admin.site.register(User, MySpecialAdmin(User))
I'm using Django 3.1.4 and here is my solution.
I have a model Qualification
model.py
from django.db import models
TRUE_FALSE_CHOICES = (
(1, 'Yes'),
(0, 'No')
)
class Qualification(models.Model):
qual_key = models.CharField(unique=True, max_length=20)
qual_desc = models.CharField(max_length=255)
is_active = models.IntegerField(choices=TRUE_FALSE_CHOICES)
created_at = models.DateTimeField()
created_by = models.CharField(max_length=255)
updated_at = models.DateTimeField()
updated_by = models.CharField(max_length=255)
class Meta:
managed = False
db_table = 'qualification'
admin.py
from django.contrib import admin
from models import Qualification
#admin.register(Qualification)
class QualificationAdmin(admin.ModelAdmin):
list_display = [field.name for field in Qualification._meta.fields if field.name not in ('id', 'qual_key', 'qual_desc')]
list_display.insert(0, '__str__')
here i am showing all fields in list_display excluding 'id', 'qual_key', 'qual_desc' and inserting '__str__' at the beginning.
This answer is helpful when you have large number of modal fields, though i suggest write all fields one by one for better functionality.
list_display = [field.name for field in Book._meta.get_fields()]
This should work even with python 3.9
happy coding
I needed to show all fields for multiple models, I did using the following style:
#admin.register(
AnalyticsData,
TechnologyData,
TradingData,
)
class CommonAdmin(admin.ModelAdmin):
search_fields = ()
def get_list_display(self, request):
return [
field.name
for field in self.model._meta.concrete_fields
if field.name != "id" and not field.many_to_many
]
But you can also do this by creating a mixin, if your models have different fields.

django model/modelForm - How to get dynamic choices in choiceField?

i'm experimenting with django and the builtin admin interface.
I basically want to have a field that is a drop down in the admin UI. The drop down choices should be all the directories available in a specified directory.
If i define a field like this:
test_folder_list = models.FilePathField(path=/some/file/path)
it shows me all the files in the directory, but not the directories.
Does anyone know how i can display the folders?
also i tried doing
test_folder_list = models.charField(max_length=100, choices=SOME_LIST)
where SOME_LIST is a list i populate using some custom code to read the folders in a directory. This works but it doesn't refresh. i.e. the choice list is limited to a snapshot of whatever was there when running the app for the first time.
thanks in advance.
update:
after some thinking and research i discovered what i want may be to either
1. create my own widget that is based on forms.ChoiceField
or
2. pass my list of folders to the choice list when it is rendered to the client
for 1. i tried a custom widget.
my model looks like
class Test1(models.Model):
test_folder_ddl = models.CharField(max_length=100)
then this is my custom widget:
class FolderListDropDown(forms.Select):
def __init__(self, attrs=None, target_path):
target_folder = '/some/file/path'
dir_contents = os.listdir(target_folder)
directories = []
for item in dir_contents:
if os.path.isdir(''.join((target_folder,item,))):
directories.append((item, item),)
folder_list = tuple(directories)
super(FolderListDropDown, self).__init__(attrs=attrs, choices=folder_list)
then i did this in my modelForm
class test1Form(ModelForm):
test_folder_ddl = forms.CharField(widget=FolderListDropDown())
and it didn't seem to work.What i mean by that is django didn't want to use my widget and instead rendered the default textinput you get when you use a CharField.
for 2. I tried this in my ModelForm
class test1Form(ModelForm):
test_folder_ddl = forms.CharField(widget=FolderListDropDown())
test_folder_ddl.choices = {some list}
I also tried
class test1Form(ModelForm):
test_folder_ddl = forms.ChoiceField(choices={some list})
and it would still render the default char field widget.
Anyone know what i'm doing wrong?
Yay solved. after beating my head all day and going through all sorts of examples by people i got this to work.
basically i had the right idea with #2. The steps are
- Create a ModelForm of our model
- override the default form field user for a models.CharField. i.e. we want to explcitly say use a choiceField.
- Then we have to override how the form is instantiated so that we call the thing we want to use to generate our dynamic list of choices
- then in our ModelAdmin make sure we explicitly tell the admin to use our ModelForm
class Test1(models.Model):
test_folder_ddl = models.CharField(max_length=100)
class Test1Form(ModelForm):
test_folder_ddl = forms.choiceField()
def __init__(self, *args, **kwargs):
super(Test1Form, self).__init__(*args, **kwargs)
self.fields['test_folder_ddl'].choices = utility.get_folder_list()
class Test1Admin(admin.ModelAdmin):
form = Test1Form
I use a generator:
see git://gist.github.com/1118279.git
import pysvn
class SVNChoices(DynamicChoice):
"""
Generate a choice from somes files in a svn repo
""""
SVNPATH = 'http://xxxxx.com/svn/project/trunk/choices/'
def generate(self):
def get_login( realm, username, may_save ):
return True, 'XXX', 'xxxxx', True
client = pysvn.Client()
client.callback_get_login = get_login
return [os.path.basename(sql[0].repos_path) for sql in client.list(self.SVNPATH)[1:]]

Resources