Form to swap 2 objects in database with Django - python

I'm new to django and I've been developing a simple application for the past month or so, but I have a problem, something I did not manage to do.
I have a simple model called WeeklyPlaylist (from my models.py):
class WeeklyPlaylist(models.Model):
total_num_entries_in_playlist = 8
get_pos_choices = get_integer_choices(1, total_num_entries_in_playlist)
sched = models.ForeignKey(Schedule)
week = models.IntegerField(choices=get_integer_choices(1, 52))
position = models.IntegerField(choices=get_pos_choices)
where 'position' simply indicates the position of a video in a playlist.
I'd like to provide the admin with the ability to swap the position of one video in a playlist with another video in that same playlist, through the change/update form of the model above (from my admin.py):
class WeeklyPlaylistAdmin(admin.ModelAdmin):
(...)
readonly_fields = ('position',)
form = WeeklyPlaylistAdminForm
def get_changelist_form(self, request, obj=None, **kwargs):
return WeeklyPlaylistAdminForm
and I'm defining my own form for this object (still from admin.py):
class WeeklyPlaylistAdminForm(ModelForm):
class Meta:
model = WeeklyPlaylist
fields = ('position',)
swap_with_position = forms.IntegerField(widget=forms.Select(choices=WeeklyPlaylist.get_pos_choices))
def clean_swap_with_position(self):
swap_with_position = self.cleaned_data['swap_with_position']
instance = getattr(self, 'instance', None)
if instance and instance.id and swap_with_position == self.instance.position:
raise forms.ValidationError("You must specify a different position than the actual one.")
# select the database obj to swap position with
other = WeeklyPlaylist.objects.filter(sched__screen__name=self.instance.sched.screen.name, sched__year__exact=self.instance.sched.year, week=self.instance.week, position=swap_with_position)
if other.count() != 1:
raise forms.ValidationError("The desired position does not correspond to any existing WeeklyPlaylist entry.")
return swap_with_position
What I had in my mind basically was to provide an extra 'select' html tag to the admin in the change/update form of the WeeklyPlaylist model where he could enter the new position in the playlist for the current video, with the necessary checks in the associated clean_ method to make sure the desired playlist position is valid.
So far so good. Now, my problem is the following: when the admin clicks on the 'save' button, how can I simultaneously save the modified object, and the one it is exchanging its position with? I've tried doing this in the save() method of the form, using the following code:
def save(self, commit=True, *args, **kwargs):
m = super(WeeklyPlaylistAdminForm, self).save(commit=False, *args, **kwargs)
cleaned_data = self.cleaned_data
swap_with_position = cleaned_data.get("swap_with_position")
if commit:
# select the database obj to swap position with
other = WeeklyPlaylist.objects.get(sched__screen__name=m.sched.screen.name, sched__year__exact=m.sched.year, week=m.week, position=swap_with_position)
m.position, other.position = other.position, m.position
m.save()
other.save()
return m
That seems perfectly fine, except that for some reason, commit is always false, even though 'm' is saved after the operation is finished, which is something I don't understand. But as a consequence, other.save() is never called, and if I remove the if statement checking the value of commit, I won't be able to do a save(commit=False) on the WeeklyPlaylistAdminForm objects, which can be annoying...
So, any suggestions to help me?
Many thanks in advance!
Cheers!
Adrien

When I've done things similar to this in Django I haven't implemented models that are specifically swapping with another category but that they re-sort the list; so moving element with order 5 to order 0 would shift all the previous elements with order in the range from 0 to 4 up, but swapping with a specifically ordered element should be much easier.
I would store the previous position of your model before any modifications take place and detect if it has changed in the save method before the saving is actually done. If it has changed then I would save the current model then do a lookup for the model that did have that position and that doesn't have the current position and do an update on that one to correct the position; hopefully the following code should help demonstrate what I mean:
class WeeklyPlaylist(models.Model):
def __init__(self, *args, **kwargs):
super(WeeklyPlaylist, self).__init__(*args, **kwargs)
self._position = int(self.position)
def save(self, *args, **kwargs):
super(WeeklyPlaylist, self).save(*args, **kwargs)
# position has changed, so change the position of the element that held the new position now held by this element
if int(self.position) != self._position:
WeeklyPlaylist.objects.exclude(pk=self.pk).filter(
position=self.position
).update(position=self._position)

Related

Can I dynamically generate boolean options on a custom Blender operator?

I'm trying to write a custom operator for Blender that:
Gets the objects currently in the scene (may be lots)
Filters them based on some criteria
Prompts the user to select/deselect any of the filtered objects (using checkboxes or similar)
Does something with the final selection.
I'm stuck on number 3. I'd like to show the user a window with checkboxes beside each of the filtered options, but to do that I'd have to be able to generate the properties dynamically.
The closest thing I've found so far is a bpy.props.EnumProperty, which takes callable to set its items. But it only supports 1 selection, whereas I need the user to be able to select multiple options.
Example:
def filter_objects(self, context):
return [obj for obj in bpy.data.objects if obj.name.startswith('A')]
class TurnObjectsBlue(bpy.types.Operator):
'TurnObjectsBlue'
bl_idname = 'object.turnobjectsblue'
bl_label = 'TurnObjectsBlue'
bl_options = {'REGISTER'}
# MultiSelectCheckboxes doesn't exist :(
chosen_objects: bpy.props.MultiSelectCheckboxes(
name='Select Objects',
)
def execute(self, context):
from coolmodule import turn_blue
for obj in self.user_selected_objects:
turn_blue(obj)
return {'FINISHED'}
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
It looks like you want a CollectionProperty. These are basically native blender lists that can store blender properties, and can have as many items as you like.
To set one up, start by creating a new class that will represent one of the items in the CollectionProperty.
It should inherit from bpy.types.PropertyGroup, and in it you can define any properties that you want to be able to access (also make sure to register that class):
class MyCollectionItem(bpy.types.PropertyGroup):
# This will tell us which object this item is for
object: bpy.props.PointerProperty(type=bpy.types.Object)
# This is whether this item (and by extension the object) is enabled/disabled
is_item_selected: bpy.props.BoolProperty()
def register():
# Don't forget to register it!
bpy.utils.register_class(MyCollectionItem)
Then you can rewrite your operator to do something like this:
class TurnObjectsBlue(bpy.types.Operator):
'TurnObjectsBlue'
bl_idname = 'object.turnobjectsblue'
bl_label = 'TurnObjectsBlue'
bl_options = {'REGISTER'}
# This acts as a list, where each item is an instance of MyCollectionItem
chosen_objects: bpy.props.CollectionProperty(
type=MyCollectionItem,
)
def execute(self, context):
from coolmodule import turn_blue
# Loop through all items in the CollectionProperty, and if they are selected, do something
for item in self.chosen_objects:
if item.is_item_selected:
turn_blue(item.object)
return {'FINISHED'}
def invoke(self, context, event):
objects = filter_objects(context)
# For each object, add a new item to the CollectionProperty, and set it's object property
for object in objects:
# Note how items are created with CollectionProperty.add()
obj_item = self.chosen_objects.add()
obj_item.object = object
return context.window_manager.invoke_props_dialog(self)
def draw(self, context):
layout = self.layout
# Loop through all of the CollectionProperty items and draw the "is_item_selected" property
for item in self.chosen_objects:
layout.prop(item, "is_item_selected")
While collection properties can act like Blender lists, they work fairly differently to python lists, so I'd suggest reading a bit about them to try and understand how they work:
https://docs.blender.org/api/current/bpy.props.html#collection-example
Hope that helps!

ModelChoicesField returns Non-Valid-Choice error, although form is valid

If have a date picker form that filters a set of models (Sonde) and populates a ModelChoicesField. This works correctly in terms of date choice in my app, but on my canvas I constantly get the error:
Select a valid choice. That choice is not one of the available choices.
I do the init, to filter the available instances of Sonde and populate the choices of the ModelChoiceField.
From my forms.py
class date_choice(forms.Form):
avSonden = forms.ModelChoiceField(queryset = Sonde.objects.none())
def __init__(self, *args, **kwargs):
currentUserID = kwargs.pop('currentUserID', None)
super(date_choice, self).__init__(*args, **kwargs)
if currentUserID:
self.fields['avSonden'].queryset = Sonde.objects.filter(owned_by__Kundennummer = currentUserID).values_list("Serial",flat=True).distinct()
start = forms.DateField(input_formats=['%Y-%m-%d'])
end = forms.DateField(input_formats=['%Y-%m-%d'])
I had to force the clean() to ignore my change from PK to other identifier:
def clean_status(self):
#valid if a value has been selected
if self["avSonden"].value()!="":
del self._errors["avSonden"]
return self["avSonden"].value()

django 1.3 forms validation using clean method to retrieve getlist

I have a form with checkboxes, the form functioning well, in my view i can use request.POST.getlist('list') to retrieve the list of values.
At the moment am trying to do some form validation inside the clean method and when i try to use self.cleaned_data['list'] I get the last value. I cannot retrieve the list of items.
Any idea how i could do that?
forms.py
class SelectList_Form(forms.Form):
list = forms.CharField(required=False)
def clean(self):
super(SelectList_Form, self).clean()
cleaned_data = self.cleaned_data
try:
# TODO: list validation
if cleaned_data['list'].__len__() is 0:
raise forms.ValidationError(_('Must select at least one of the lists below'),)
if cleaned_data['list'].__len__() > 1:
try:
# In here when i print list it only shows me the last value. It doesn't show me the list of values when the box is checked
print cleaned_data['list']
except Main.DoesNotExist:
raise Http404
except forms.ValidationError:
raise
class Posting_Wizard(FormWizard):
def render_template(self, request, form, previous_fields, step, context=None):
if step == 0:
obj = MainI18n.objects.filter(main__is_active=True, language=request.LANGUAGE_CODE).\
exclude(main__parent=None).order_by('main__parent').select_related(depth=1)
category_choices=dict(['%s,%s' % (i.main.slug, i.main.parent.slug), '%s - %s' % (i.main.parent,i.label)] for i in obj)
form.fields['categories'] = forms.CharField(widget=forms.RadioSelect(choices=category_choices.items()))
if step == 1:
category = request.POST.get('0-categories')
pobj = Main.objects.filter(slug=category.split(',')[1], parent=None).get()
cobj = Main.objects.filter(slug=category.split(',')[0], parent=pobj.id).get()
lobj = ListI18n.objects.filter(list__is_active=True, language=request.LANGUAGE_CODE, list__main__slug=category.split(',')[0], list__main__parent=pobj.id).select_related()
list_choices = dict([i.id, i.title] for i in lobj)
if cobj.mainproperties.relation == 'M':
# Here i generate the checkboxes
form.fields['list']=forms.CharField(widget=forms.CheckboxSelectMultiple(choices=list_choices.items()),label="Pick the list",)
else:
form.fields['list']=forms.CharField(widget=forms.RadioSelect(choices=list_choices.items()),label="Pick the list",)
return super(Posting_Wizard, self).render_template(request, form, previous_fields, step, context)
def done(self, request, form_list):
return HttpResponseRedirect(reverse('accounts-registration-wizard-done'))
def get_template(self, step):
return 'listing/post/wizard/wizard_%s.html' % step
First, there are a number of basic Python errors here. There is almost never a need to access the double-underscore functions - they are internal implementation details. Always use the normal len() function instead. And, never use is for comparisons: it's for identity, so should only be used with things you know have the same identity, which basically just means None. So your code should read:
if len(cleaned_data['list']) == 0:
etc.
Now, secondly, I don't understand why you think there could ever be more than one 'element' in list. You've defined it as a CharField, which is a single field containing many characters. Your len is testing the number of characters entered into that field, not the number of fields, however you think you've defined them.

python: Using the choice box to get data besides getString()

I have defined an object that has several attribute..
class thing(object):
def __init__(self, type, name, attrA, attrB, attrC):
self.type = type
self.name = name
self.attrA = attrA
self.attrB = attrB
self.attrC = attrC
lets say then I have a list of things
self.things=[thing('car','fred',1,2,3),
thing('car','george',a,b,c),
thing('truck','bob',6,7,8),
thing('truck','tom',x,y,z)
]
I then populate a choice box with SOME of the items from that list
for each in self.things:
if each.type == 'car':
self.choiceCar.Append(item=each.name)
When the user selects Bob from the dropdown I have an event for that
def EvtChoice(self,event):
self.Name = event.GetString()
This captures the name of the selection, but how do I get the other attributes? What I am currently doing is
for each in self.items:
if self.Name == each.name
#Get other things here
My thought is that if my list grows large then this loop through my entire list will become very inefficient and really unneeded since the user has already selected the specific item I want. What I think I should be able to do is to get the index of the selected item, but im not sure how to do that, or even if that is the correct way to go about it.
Associating data or objects with wx.Choice or wx.ComboBox is pretty easy. You can see an example using the latter here:
http://www.blog.pythonlibrary.org/2010/12/16/wxpython-storing-object-in-combobox-or-listbox-widgets/
The basic idea is to pass an empty list to the control's constructor and then iterate over the objects and Append them to the control. So something like this:
for obj in self.things:
self.choiceCar.Append(obj.name, obj)
Then in the event handler for the widget, you can get the object back by doing this:
obj = self.choiceCar.GetClientData(self.choiceCar.GetSelection())

Does django's Form class maintain state?

I'm building my first form with django, and I'm seeing some behavior that I really did not expect at all. I defined a form class:
class AssignmentFilterForm(forms.Form):
filters = []
filter = forms.ChoiceField()
def __init__(self, *args, **kwargs):
super(forms.Form, self).__init__(*args, **kwargs)
self.filters.append(PatientFilter('All'))
self.filters.append(PatientFilter('Assigned', 'service__isnull', False))
self.filters.append(PatientFilter('Unassigned', 'service__isnull', True))
for i, f in enumerate(self.filters):
self.fields["filter"].choices.append((i, f.name))
When I output this form to a template using:
{{ form.as_p }}
I see the correct choices. However, after refreshing the page, I see the list three times in the select box. Hitting refresh again results in the list showing 10 times in the select box!
Here is my view:
#login_required
def assign_test(request):
pg = PhysicianGroup.objects.get(pk=physician_group)
if request.method == 'POST':
form = AssignmentFilterForm(request.POST)
if form.is_valid():
yes = False
else:
form = AssignmentFilterForm()
patients = pg.allPatients().order_by('bed__room__unit', 'bed__room__order', 'bed__order' )
return render_to_response('hospitalists/assign_test.html', RequestContext(request, {'patients': patients, 'form': form,}))
What am I doing wrong?
Thanks, Pete
This is actually a feature of Python that catches a lot of people.
When you define variables on the class as you have with filters = [] the right half of the expression is evaluated when the class is initially defined. So when your code is first run it will create a new list in memory and return a reference to this list. As a result, each AssignmentFilterForm instance will have its own filters variable, but they will all point to this same list in memory. To solve this just move the initialization of self.filters into your __init__ method.
Most of the time you don't run into this issue because the types you are using aren't stored as a reference. Numbers, booleans, etc are stored as their value. Strings are stored by reference, but strings are immutable meaning a new string must be created in memory every time it is changed and a new reference returned.
Pointers don't present themselves often in scripting language, so it's often confusing at first when they do.
Here's a simple IDLE session example to show what's happening
>>> class Test():
myList = []
def __init__( self ):
self.myList.append( "a" )
>>> Test.myList
[]
>>> test1 = Test()
>>> Test.myList
['a']
>>> test1.myList
['a']
>>> test2 = Test()
>>> test2.myList
['a', 'a']
>>> test1.myList
['a', 'a']
>>> Test.myList
['a', 'a']
I picked up the book Pro Django which answers this question. It's a great book by the way, and I highly recommend it!
The solution is to make BOTH the choice field and my helper var both instance variables:
class AssignmentFilterForm(forms.Form):
def __init__(self, pg, request = None):
super(forms.Form, self).__init__(request)
self.filters = []
self.filters.append(PatientFilter('All'))
self.filters.append(PatientFilter('Assigned', 'service__isnull', False))
self.filters.append(PatientFilter('Unassigned', 'service__isnull', True))
self.addPhysicians(pg)
self.fields['filter'] = forms.ChoiceField()
for i, f in enumerate(self.filters):
self.fields['filter'].choices.append((i, f.name))
Clearing out the choices works but would surely result in threading issues.
You're appending to the PER-CLASS variable self.filters. Make it into a PER-INSTANCE variable instead, by doing self.filters = [] at the start of __init__.
To clarify from some of the other answers:
The fields are, and must be, class variables. They get all sorts of things done to them by the metaclass, and this is the correct way to define them.
However, your filters variable does not need to be a class var. It can quite easily be an instance var - just remove the definition from the class and put it in __init__. Or, perhaps even better, don't make it a property at all - just a local var within __init__. Then, instead of appending to filters.choices, just reassign it.
def __init__(self, *args, **kwargs):
super(forms.Form, self).__init__(*args, **kwargs)
filters = []
filters.append(PatientFilter('All'))
filters.append(PatientFilter('Assigned', 'service__isnull', False))
filters.append(PatientFilter('Unassigned', 'service__isnull', True))
self.fields["filter"].choices = [(i, f.name) for i, f in enumerate(filters)]
As answered above, you need to initialize filters as an instance variable:
def __init__(...):
self.filters = []
self.filters.append(...)
# ...
If you want to know more about how the Form class works, you should read this page in the Django wiki:
Model Creation and Initialization
It talks about the internals of the Model class, but you'll find the general setup of fields is somewhat similar to the Form (minus the database stuff). It's a bit dated (2006), but I think the basic principles still apply. The metaclass stuff can be a bit confusing if you're new though.

Categories

Resources