I'm working on enhancing a single-level menu that my site is currently using to great effect. The code is as follows (with irrelevant parts clipped out):
from wagtail.contrib.settings.models import BaseSetting
from modelcluster.models import ClusterableModel
class AbstractCustomMenuItem(models.Model):
"""
Derive from this model to define one or more CustomMenus.
"""
url = models.CharField('URL', max_length=200, blank=True,
help_text='This must be either a fully qualified URL, e.g. https://www.google.com or a local absolute URL, '
'e.g. /admin/login'
)
page = models.ForeignKey('wagtailcore.Page', null=True, blank=True, on_delete=models.CASCADE, related_name='+',
help_text='If a Page is selected, the URL field is ignored. The title of the selected Page will be displayed '
'if the Link Text field is blank.'
)
link_text = models.CharField('Link Text', max_length=50, blank=True)
is_separator = models.BooleanField( 'Separator', default=False,
help_text='Separators are used to visually distinguish different sections of a menu.'
)
panels = [
MultiFieldPanel([
FieldPanel('url', classname='col8 url'), FieldPanel('link_text', classname='col4 link-text'),
], classname='url-and-link-text'),
# This is a bit gnarly, but it was the best way I could find to render the form in a pretty
# way. I'm using MultiFieldPanel and classname='col8' entirely for formatting, rather than organization.
MultiFieldPanel([PageChooserPanel('page')], classname='col8 page-chooser'),
MultiFieldPanel([FieldPanel('is_separator', classname='separator')]),
]
class Meta:
abstract = True
#register_setting(order=1000)
class Settings(BaseSetting, ClusterableModel):
####### FIELD CODE #######
...
####### FORM CODE #######
...
theme_and_menu_panels = [
InlinePanel( 'header_menu_items', label='Header Menu Item',
help_text='You can optionally add a Header Menu to your site, which will appear in the ribbon at the top '
'of the page.'
),
InlinePanel('footer_menu_items', label='Footer Menu Item',
help_text='You can optionally add a Footer Menu to your site, which will appear in the footer.'
),
]
...
edit_handler = TabbedInterface(
[
...
ObjectList(theme_and_menu_panels, heading='Theme and Menus', classname='theme-and-menus'),
...
]
)
class HeaderMenuItem(Orderable, AbstractCustomMenuItem):
"""
This class provides the model for the Header Menu Items that can be added to a Site's settings.
"""
settings = ParentalKey('www.Settings', related_name='header_menu_items', on_delete=models.CASCADE)
class FooterMenuItem(Orderable, AbstractCustomMenuItem):
"""
This class provides the model for the Footer Menu Items that can be added to a Site's settings.
"""
settings = ParentalKey('www.Settings', related_name='footer_menu_items', on_delete=models.CASCADE)
This gives my code the ability to assign separate, custom Header and Footer menus.
Now, however, I need to upgrade this code to allow the Header menu items to optionally have their own submenu beneath them. And I figured that I could do basically the same thing I did to create the MenuItems in the first place, and just parent them beneath the HeaderMenuItem class, rather than beneath Settings.
So I changed the HeaderMenuItem class, and added HeaderMenuDropdownItem:
class HeaderMenuItem(Orderable, ClusterableModel, AbstractCustomMenuItem):
"""
This class provides the model for the Header Menu Items that can be added to a Site's settings.
"""
settings = ParentalKey('www.Settings', related_name='header_menu_items', on_delete=models.CASCADE)
panels = [
MultiFieldPanel([
FieldPanel('url', classname='col8 url'), FieldPanel('link_text', classname='col4 link-text'),
], classname='url-and-link-text'),
# This is a bit gnarly, but it was the best way I could find to render the form in a pretty
# way. I'm using MultiFieldPanel and classname='col8' entirely for formatting, rather than organization.
MultiFieldPanel([PageChooserPanel('page')], classname='col8 page-chooser'),
MultiFieldPanel([FieldPanel('is_separator', classname='separator')]),
InlinePanel('dropdown_items', classname='dropdown-items'),
]
class HeaderMenuDropdownItem(Orderable, AbstractCustomMenuItem):
"""
This class provides the model for the Header Menu Dropdown items.
"""
header_menu_item = ParentalKey('www.HeaderMenuItem', related_name='dropdown_items', on_delete=models.CASCADE)
Unfortunately, I now get the following exception when I load the Wagtail admin page for editing the Settings class:
File "/.../django/core/handlers/exception.py" in inner
35. response = get_response(request)
File "/.../django/core/handlers/base.py" in _get_response
128. response = self.process_exception_by_middleware(e, request)
File "/.../django/core/handlers/base.py" in _get_response
126. response = wrapped_callback(request, *callback_args, **callback_kwargs)
File "/.../django/views/decorators/cache.py" in _cache_controlled
31. response = viewfunc(request, *args, **kw)
File "/.../wagtail/admin/urls/__init__.py" in wrapper
102. return view_func(request, *args, **kwargs)
File "/.../wagtail/admin/decorators.py" in decorated_view
34. return view_func(request, *args, **kwargs)
File "/.../wagtail/contrib/settings/views.py" in edit
83. instance=instance, form=form, request=request)
File "/.../wagtail/admin/edit_handlers.py" in bind_to_instance
152. new.on_instance_bound()
File "/.../wagtail/admin/edit_handlers.py" in on_instance_bound
294. request=self.request))
File "/.../wagtail/admin/edit_handlers.py" in bind_to_instance
152. new.on_instance_bound()
File "/.../wagtail/admin/edit_handlers.py" in on_instance_bound
294. request=self.request))
File "/.../wagtail/admin/edit_handlers.py" in bind_to_instance
152. new.on_instance_bound()
File "/.../wagtail/admin/edit_handlers.py" in on_instance_bound
708. request=self.request))
File "/.../wagtail/admin/edit_handlers.py" in bind_to_instance
152. new.on_instance_bound()
File "/.../wagtail/admin/edit_handlers.py" in on_instance_bound
294. request=self.request))
File "/.../wagtail/admin/edit_handlers.py" in bind_to_instance
152. new.on_instance_bound()
File "/.../wagtail/admin/edit_handlers.py" in on_instance_bound
693. self.formset = self.form.formsets[self.relation_name]
Exception Type: AttributeError at /admin/settings/www/settings/3/
Exception Value: 'HeaderMenuItemForm' object has no attribute 'formsets'
What am I doing wrong? Can a ParentalKey simply not be nested inside another ParentalKey? If not, how could I implement this multi-level menu? Maybe I'm going about this all wrong from the start?
Have you looked into wagtailmenus? It might save you some development time and effort.
Related
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.
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),
# ...
]
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
I am trying to add inlines to my template but continue to get a Database error:
more than one row returned by a subquery used as an expression
I have 3 objects in my models.py that relate to each other. The user will be able to see which Teacher is selected and have all Owners under that Teacher listed (Teacher and Owner will only appear as an uneditable list). I'd like to have all the Pets under the Owner listed and editable. Any ideas on why I am receiving this error? And how I may be able to accomplish my goal?
models.py
class Teacher(models.Model):
teacher = models.CharField(max_length=300)
class Owner(models.Model):
relevantteacher = models.ForeignKey(Teacher)
owner = models.CharField(max_length=300)
class PetName(models.Model):
relevantowner = models.ForeignKey(Owner)
pet_name = models.CharField(max_length=50)
forms.py
class OwnerForm(forms.ModelForm):
class Meta:
model = Owner
PetNameFormSet = inlineformset_factory(Owner,
PetName,
can_delete=False,
extra=3,
form=OwnerForm)
views.py
def petname(request, teacher_id):
teacher = get_object_or_404(Teacher, pk=teacher_id)
owners = Owner.objects.filter(relevantteacher=teacher_id)
if request.method == "POST":
petNameInlineFormSet = PetNameFormSet(request.POST, request.FILES, instance=owners)
if petNameInlineFormSet.is_valid():
petNameInlineFormSet.save()
return HttpResponseRedirect(reverse('success'))
else:
petNameInlineFormSet = PetNameFormSet(instance=owners) //error might be here?
context = {'teacher': teacher, 'owners': owners, 'petNameInlineFormSet' : petNameInlineFormSet}
return render(request, 'petname.html', context)
Update:
Here is the traceback:
File "hde/lib/python2.7/site-packages/django/core/handlers/base.py" in get_response
111. response = callback(request, *callback_args, **callback_kwargs)
File "/views.py" in petname
60. petNameInlineFormSet = PetNameFormSet(instance=owners)
File "lib/python2.7/site-packages/django/forms/models.py" in __init__
697. queryset=qs, **kwargs)
File "lib/python2.7/site-packages/django/forms/models.py" in __init__
424. super(BaseModelFormSet, self).__init__(**defaults)
Needed to pass only 1 object to the instance
owner = owners[0]
then
instance=owner
However, I am only able to add/edit pet names 1 owner at a time.
Thanks aamir for the help!
I believe your error is in the second line of the views.py file. I believe it is the call to the get_object_or_404 method causing the error when you try to specify teacher.id in your template. The call to the get_object_or_404 method is returning more than one row from the database, so calling teacher.id is not possible on it from more than one row.
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:]]