I have the following form:
class TutorForm(SignupForm):
subjects = forms.ModelMultipleChoiceField(queryset=Subject.objects.all(),
widget=forms.CheckboxSelectMultiple())
I have a child form called TutorUpdateFormthat inherits from TutorForm and it sets initial values in init method.
self.fields['subjects'].initial = current_user.subjects.all()
In my template however the values are not checked (in views the values are there, so setting initial values works). How can I enforce checked inputs in the template?
EDIT (init code)
def __init__(self, *args, **kwargs):
current_user = None
try:
current_user = kwargs.pop('user')
except Exception:
pass
super(TutorUpdateForm, self).__init__(*args, **kwargs)
for field in _update_exclude:
self.fields.pop(field)
if current_user:
self.fields['subjects'].initial = current_user.subjects.all()
You should pass the initial values into the call to super, you can also set a default value for dict.pop rather than using try/except
def __init__(self, *args, **kwargs):
current_user = kwargs.pop('user', None)
initial = kwargs.get('initial', {})
if current_user:
initial.update({'subjects': current_user.subjects.all()})
kwargs['initial'] = initial
super(TutorUpdateForm, self).__init__(*args, **kwargs)
for field in _update_exclude:
self.fields.pop(field)
Here is a link to the documentation on dynamic initial values for forms
Related
I'm new to Django and I'm having a hard time understanding forms when the data to choose from are not taken from the database nor user input that they're generated on the go.
I currently have a template with a single ChoiceField. The data inside this field aren't fixed and they're calculated on the go once the page is requested. To calculate it I need the username of the User who is logged in. Basically, the calculation returns a list of lists in the form of ((title, id),(title,id),(title,id)), etc. that I need to put into the ChoiceField to make the User choose from one of the options.
Now, I'm not understanding how to pass the calculated list of lists to the form. I've tried to add the calculations inside the form as below but it is clearly the wrong way.
The main issue is that, to calculate my list of lists, I need the request value, and I don't know how to access it from the form.
Another idea was to add the generate_selection function inside the init but then I don't know how to pass main_playlist to being able to add it to ChoiceField
Below my not working forms.py
forms.py
class ChoosePlaylistForm(forms.Form):
playlists = forms.ChoiceField(choices=HERE_SHOULD_GO_main_playlist)
def generate_selection(self):
sp_auth, cache_handler = spotify_oauth2(self.request)
spotify = spotipy.Spotify(oauth_manager=sp_auth)
user_playlists = spotify.current_user_playlists(limit=10)
main_playlist = []
for playlists in user_playlists["items"]:
playlists_list = []
playlists_list.append(playlists['name'])
playlists_list.append(playlists['id'])
main_playlist.append(playlists_list)
return main_playlist
def __init__(self, *args, **kwargs):
self.request = kwargs.pop('request', None)
super(ChoosePlaylistForm, self).__init__(*args, **kwargs)
class Meta:
model = User
fields = ('playlists',)
The views should be something like below so I'm able to pass the request
views.py
form = ChoosePlaylistForm(request=request)
Maybe overriding the field choices in the form constructor would work:
class ChoosePlaylistForm(forms.Form):
playlists = forms.ChoiceField(choices=())
class Meta:
model = User
fields = ('playlists',)
def __init__(self, *args, request=None, **kwargs):
super(ChoosePlaylistForm, self).__init__(*args, **kwargs)
self.request = request
self.fields['playlists'].choices = self.generate_selection()
def generate_selection(self):
sp_auth, cache_handler = spotify_oauth2(self.request)
spotify = spotipy.Spotify(oauth_manager=sp_auth)
user_playlists = spotify.current_user_playlists(limit=10)
choices = []
for playlist in user_playlists["items"]:
playlist_choice = (playlist["name"], playlist["id"])
choices.append(playlist_choice)
return choices
I'm unit testing a view and I am attempting to patch the .data property on my serializer but it looks like it behaves differently when the many=True kwarg is passed to the serializer constructor and thus not properly patching. Here is a generalized example of my code.
# myapp/serializers.py
class MySerializer(serializers.Serializer):
some_field = serializers.CharField()
# myapp/views.py
class MyView(View):
def get(self, request):
# ..stuff
some_data = []
serializer = MySerializer(some_data, many=True)
print(type(serializer)) # <class 'rest_framework.serializers.ListSerializer'>
print(type(serializer.data)) # <class 'rest_framework.utils.serializer_helpers.ReturnList'>
return Response({"data": seralizer.data, status=200})
# in tests
def test_view_case_one(mocker):
# setup other mocks
serialized_data = mocker.patch("myapp.views.MySerializer.data", new_callable=mocker.PropertyMock)
# invoke view
response = MyView().get(fake_request)
# run assertions
serialized_data.assert_called_once() # this says it's never called
Earlier I had ran into issues attempting to patch rest_framework.serializers.ListSerializer.data. Must of been a typo. Reattempted and was able to successfully patch. Given the case many=True recreates the serializer as a ListSerializer I simply needed to patch the property on the underlying class.
serialized_data = mocker.patch(
"rest_framework.serializers.ListSerializer.data",
new_callable=mocker.PropertyMock
)
Edit: A more in depth answer
When many=True is used the __new__ method on BaseSerializer grabs you class and constructs a ListSerializer from it and that is why my object showed up as a ListSerializer. Since we are actually receiving a ListSerializer instead of our defined class the patch is not applied to ListSerializer.data method. The relevant parts of the source code for BaseSerializer is below
class BaseSerializer(Field):
def __new__(cls, *args, **kwargs):
# We override this method in order to automagically create
# `ListSerializer` classes instead when `many=True` is set.
if kwargs.pop('many', False):
return cls.many_init(*args, **kwargs)
return super(BaseSerializer, cls).__new__(cls, *args, **kwargs)
#classmethod
def many_init(cls, *args, **kwargs):
"""
This method implements the creation of a `ListSerializer` parent
class when `many=True` is used. You can customize it if you need to
control which keyword arguments are passed to the parent, and
which are passed to the child.
Note that we're over-cautious in passing most arguments to both parent
and child classes in order to try to cover the general case. If you're
overriding this method you'll probably want something much simpler, eg:
#classmethod
def many_init(cls, *args, **kwargs):
kwargs['child'] = cls()
return CustomListSerializer(*args, **kwargs)
"""
allow_empty = kwargs.pop('allow_empty', None)
child_serializer = cls(*args, **kwargs)
list_kwargs = {
'child': child_serializer,
}
if allow_empty is not None:
list_kwargs['allow_empty'] = allow_empty
list_kwargs.update({
key: value for key, value in kwargs.items()
if key in LIST_SERIALIZER_KWARGS
})
meta = getattr(cls, 'Meta', None)
list_serializer_class = getattr(meta, 'list_serializer_class', ListSerializer)
return list_serializer_class(*args, **list_kwargs)
Having already looked at this answer, I'm having trouble getting my custom WTForm validator to accept another arg.
How my code is structures is that I have a form_create() method that is called via the following:
form = other_file.form_create(
form='page_settings',
data=data,
all_applications=all_applications,
)
all_applications being a dict I need to index through for one of my validators. form_create looks like the following:
def get_form(form=None, *args, **kwargs):
class Form(wtf.Form):
if form == 'page_settings':
my_host_name = HostName('Hostname', validators=[wtf.validators.DataRequired(), validate_hostname, validate_config])
I want validate_config to use data from the field and from all_applications, but even after putting it in HostName's init method as such:
class HostName(wtf.StringField):
def __init__(self, *args, **kwargs):
kwargs.setdefault('validators', [
wtf.validators.DataRequired(),
])
all_applications = kwargs.setdefault('all_applications')
super(HostNameField, self).__init__(*args, **kwargs)
But I keep getting errors like ‘Form’ object has no attribute ‘all_applications’
The custom validator in question looks like this:
def validate_config(form, field, all_applications):
if (some logic here is true):
raise wtf.ValidationError(
'some error string here'
)
Any ideas as to how I can get a custom validator to accept another container like all_applications?
You can set all_applications as an attribute of the field when the form is being initialised, then access it via the field in the validator.
def validate_config(form, field):
if field.data and field.all_applications is not None:
if field.data not in field.all_applications:
raise ValidationError('Invalid config')
class F(Form):
foo = StringField(validators=[validate_config])
def __init__(self, *args, **kwargs):
all_applications = kwargs.pop('all_applications', None)
super().__init__(*args, **kwargs)
# Make all_applications an attribute of field foo
self.foo.all_applications = all_applications
>>> from webob.multidict import MultiDict
>>> f = F(formdata=MultiDict(foo='a'), all_applications={'a': 1})
>>> f.validate()
True
>>> f = F(formdata=MultiDict(foo='b'), all_applications={'a': 1})
>>> f.validate()
False
>>> f.errors
{'foo': ['Invalid config']}
Model field links as foreign_key to another model which has big amount of entries.
I decided to replace default select for foreign_keys with simple link.
And it works grate besides the fact that field becomes hidden!
What should I do to avoid that?
admin.py
class SeriesAdmin(ModelLinkAdminFields, admin.ModelAdmin):
modellink = ['video',]
wdiget_file.py
class ModelLinkWidget(forms.HiddenInput):
def __init__(self, admin_site, original_object):
self.admin_site = admin_site
self.original_object = original_object
super(ModelLinkWidget,self).__init__()
def render(self, name, value, attrs=None):
if self.original_object is not None:
change_url = urlresolvers.reverse('admin:%s_%s_change' %
(type(self.original_object)._meta.app_label,
type(self.original_object)._meta.object_name.lower()),
args=(self.original_object.id,))
return mark_safe('<a id="%s" name="{name}" href="%s">%s</a>' %
(attrs['id'], change_url , escape(self.original_object)))
else:
return None
class ModelLinkAdminFields(object):
def get_form(self, request, obj=None, **kwargs):
form = super(ModelLinkAdminFields, self).get_form(request, obj, **kwargs)
if hasattr(self, 'modellink'):
for field_name in self.modellink:
if field_name in form.base_fields:
form.base_fields[field_name].widget = ModelLinkWidget(self.admin_site, getattr(obj, field_name, ''))
return form
Your widget overrides forms.HiddenInput
You should use the correct widget, I presume this should be Select but there are other options available
If there a way to detect if information in a model is being added or changed.
If there is can this information be used to exclude fields.
Some pseudocode to illustrate what I'm talking about.
class SubSectionAdmin(admin.ModelAdmin):
if something.change_or_add = 'change':
exclude = ('field',)
...
Thanks
orwellian's answer will make the whole SubSectionAdmin singleton change its exclude property.
A way to ensure that fields are excluded on a per-request basis is to do something like:
class SubSectionAdmin(admin.ModelAdmin):
# ...
def get_form(self, request, obj=None, **kwargs):
"""Override the get_form and extend the 'exclude' keyword arg"""
if obj:
kwargs.update({
'exclude': getattr(kwargs, 'exclude', tuple()) + ('field',),
})
return super(SubSectionAdmin, self).get_form(request, obj, **kwargs)
which will just inform the Form to exclude those extra fields.
Not sure how this will behave given a required field being excluded...
Setting self.exclude does as #steve-pike mentions, make the whole SubSectionAdmin singleton change its exclude property.
A singleton is a class that will reuse the same instance every time the class is instantiated, so an instance is only created on the first use of the constructor, and subsequent use of the constructor will return the same instance. See the wiki page for a more indept description.
This means that if you write code to exclude the field on change it will have the implication that if you first add an item, the field will be there, but if you open an item for change, the field will be excluded for your following visits to the add page.
The simplest way to achieve a per request behaviour, is to use get_fields and test on the obj argument, which is None if we are adding an object, and an instance of an object if we are changing an object. The get_fields method is available from Django 1.7.
class SubSectionAdmin(admin.ModelAdmin):
def get_fields(self, request, obj=None):
fields = super(SubSectionAdmin, self).get_fields(request, obj)
if obj: # obj will be None on the add page, and something on change pages
fields.remove('field')
return fields
Update:
Please note that get_fields may return a tuple, so you may need to convert fields into a list to remove elements.
You may also encounter an error if the field name you try to remove is not in the list. Therefore it may, in some cases where you have other factors that exclude fields, be better to build a set of excludes and remove using a list comprehension:
class SubSectionAdmin(admin.ModelAdmin):
def get_fields(self, request, obj=None):
fields = list(super(SubSectionAdmin, self).get_fields(request, obj))
exclude_set = set()
if obj: # obj will be None on the add page, and something on change pages
exclude_set.add('field')
return [f for f in fields if f not in exclude_set]
Alternatively you can also make a deepcopy of the result in the get_fieldsets method, which in other use cases may give you access to better context for excluding stuff. Most obviously this will be useful if you need to act on the fieldset name. Also, this is the only way to go if you actually use fieldsets since that will omit the call to get_fields.
from copy import deepcopy
class SubSectionAdmin(admin.ModelAdmin):
def get_fieldsets(self, request, obj=None):
"""Custom override to exclude fields"""
fieldsets = deepcopy(super(SubSectionAdmin, self).get_fieldsets(request, obj))
# Append excludes here instead of using self.exclude.
# When fieldsets are defined for the user admin, so self.exclude is ignored.
exclude = ()
if not request.user.is_superuser:
exclude += ('accepted_error_margin_alert', 'accepted_error_margin_warning')
# Iterate fieldsets
for fieldset in fieldsets:
fieldset_fields = fieldset[1]['fields']
# Remove excluded fields from the fieldset
for exclude_field in exclude:
if exclude_field in fieldset_fields:
fieldset_fields = tuple(field for field in fieldset_fields if field != exclude_field) # Filter
fieldset[1]['fields'] = fieldset_fields # Store new tuple
return fieldsets
class SubSectionAdmin(admin.ModelAdmin):
# ...
def change_view(self, request, object_id, extra_context=None):
self.exclude = ('field', )
return super(SubSectionAdmin, self).change_view(request, object_id, extra_context)
The approach below has the advantage of not overriding the object wide exclude property; instead it is reset based on each type of request
class SubSectionAdmin(admin.ModelAdmin):
add_exclude = ('field1', 'field2')
edit_exclude = ('field2',)
def add_view(self, *args, **kwargs):
self.exclude = getattr(self, 'add_exclude', ())
return super(SubSectionAdmin, self).add_view(*args, **kwargs)
def change_view(self, *args, **kwargs):
self.exclude = getattr(self, 'edit_exclude', ())
return super(SubSectionAdmin, self).change_view(*args, **kwargs)
I believe you can override get_fieldsets method of ModeAdmin class. See the example below, in the code example below, I only want to display country field in the form when adding a new country, In order to check if object is being added, we simply need to check if obj == None, I am specifying the fields I need. Now otherwise obj != None means existing object is being changed, so you can specify which fields you want to exclude from the change form.
def get_fieldsets(self, request: HttpRequest, obj=None):
fieldset = super().get_fieldsets(request, obj=obj)
if obj == None: # obj is None when you are adding new object.
fieldset[0][1]["fields"] = ["country"]
else:
fieldset[0][1]["fields"] = [
f.name
for f in self.model._meta.fields
if f.name not in ["id", "country"]
]
return fieldset
You can override the get_exclude method of the admin.ModelAdmin class:
def get_exclude(self, request, obj):
if "change" in request.path.split("/"):
return [
"fields",
"to",
"exclude",
]
return super().get_exclude(request, obj)
I think this is cleaner than the provided answers. It doesn't override the exclude field of the Class explicitly, but rather only contextually provides the fields you wish to exclude depending on what view you're on.