Combining unrelated Models in Formset and saving results - django

These are the models related to my problem:
Models.py
class SequenceDiagram(models.Model):
name = models.TextField(blank=True)
attributeMappingName = models.TextField(blank=True)
class AttributeFilter(models.Model):
seqDiagram = models.ForeignKey(SequenceDiagram)
attributeName = models.TextField(blank=True)
protocol = models.TextField()
isDisplayed = models.BooleanField(default=False)
class AttributeMapping(models.Model):
mappingName = models.TextField()
protocol = models.TextField(blank=True)
nativeName = models.TextField(blank=True)
customName = models.TextField(blank=True
Filters are specific to each SequenceDiagram but mappings are generic and applicable to different Diagrams.
I want a Formset with all AttributeFilters and AttributeMappings linked with the SequenceDiagram.
These are to be displayed in a table where isDisplayed and customName can be edited and then saved to the database.
How can I combine them to a Formset and then save the users changes?
Can a many-to-many relationship help solve my problem? If so, in which end should it be defined?
Please tell me if anything needs to be clarified.
edit
The resulting table should look like this:
Protocol|Native|Custom|Display
prot1 | Nat1 | Cus1 | Chkbx1
prot2 | Nat2 | Cus2 | Chkbx2
.......
So that matching customNames and isDisplayed are aligned.
I have tried using objects.extra() but I can't seem to save the changes to the 'other' model, I also don't know how to get the queryset to a Formfield and back.
AttributeFilter.objects.extra(
select={"protocol":"protocol", "sd":"sdAttributeName"},
where=["customName=nativeName"],
tables=["project_attributemapping"])

You can display both forms in the template and the process the forms separately in the view.
Here, we will be using ModelForms. We will create ModelForm for each model and then save all the models in a single view.
forms.py
from django import forms
from my_app.models import SequenceDiagram, AttributeFilter, AttributeMapping
class SequenceDiagramForm(forms.ModelForm):
class Meta:
model = SequenceDiagram
class AttributeFilterForm(forms.ModelForm):
class Meta:
model = AttributeFilter
exclude = (seqDiagram,)
class AttributeMappingForm(forms.ModelForm):
class Meta:
model = AttributeMapping
views.py
from django.views.generic import View
class MyView(View):
def post(self, request, *args, **kwargs):
sequence_diagram_form = SequenceDiagramForm(request.POST) # create form instance and populate with data
attribute_filter_form = AttributeFilterForm(request.POST) # create form instance and populate with data
attribute_mapping_form = AttributeMappingForm(request.POST) # create form instance and populate with data
sequence_diagram_form_valid = sequence_diagram_form.is_valid() # check if 'SequenceDiagramForm' is valid
attribute_filter_form_valid = attribute_filter_form.is_valid() # check if 'AttributeFilterForm' is valid
attribute_mapping_form_valid = attribute_mapping_form.is_valid() # check if 'AttributeMappingForm' is valid
# Check if all the forms are valid
if sequence_diagram_form_valid and attribute_filter_form_valid and attribute_mapping_form_valid:
sequence_diagram_obj = sequence_diagram_form.save() # save the SequenceDiagram object
attribute_filter_obj = attribute_filter_form.save(commit=False) # not save but get the instance
attribute_mapping_obj = attribute_mapping_form.save() # save the AttributeMapping object
attribute_filter_obj.seqDiagram = sequence_diagram_obj # set the `seqDiagram` to `sequence_diagram_obj`
attribute_filter_obj.save() # Now save the AttributeFilter object
...
# redirect to success page on all all forms being valid
...
# render the page again with errors if any of the form is invalid
In our view, we check if all the forms are valid and then only save all the 3 objects into db. If any of the form is invalid, then we don't save any of them.
For the case when any of the form is invalid, you can add the code to render the page again with the form errors.

Related

Django - build form fields from database dynamically

I hope the title of this question is as it should be based on this explanation below.
I have a model as below:
class Setting(models.Model):
TYPE_CHOICES = (
('CONFIG', 'Config'),
('PREFS', 'Prefs'),
)
attribute = models.CharField(max_length=100, unique=True)
value = models.CharField(max_length=200)
description = models.CharField(max_length=300)
type = models.CharField(max_length=30, choices=TYPE_CHOICES)
is_active = models.BooleanField(_('Active'), default=True)
I use this to save settings. I have don't know all settings in advance and they can change in future. So I decided to save attributes and their values in this model instead of creating columns for each setting(attribute in the model).
Now the problem I am facing is how do I present form with all attributes as fields so that a user can fill in appropriate values.
Right now, as you can see, form shows columns 'Attribute' and "Value" as labels. I would like it to show value of column 'Attribute' as label and column 'Value' as field input.
For example, in Setting model I have this:
Attribute ------------ Value
'Accept Cash' ---------- 'No'
I would like to appear this on form as
<Label>: <Input>
'Accept Cash': 'No'
I think I will have to build form fields from the database(Setting model). I am new to this and have no idea how to begin with it any example or link to tutorial that would help me get started will be much appreciated.
Thank you
you can define a model form based on your Settings model. Check the django documentation on Django Model Forms. The basic definition of the model form should be something like this
Define a forms.py file in your current django app and put the following code in it.
from django import forms
from .models import Settings
class SettingsForm(forms.ModelForm):
class Meta:
model = Settings
fields = ['the fields you want to add'] # or use '__all__' without the parentheses for all fields
Then in your views.py file navigate to the function which renders the page containing the form and add this to it
from .forms import SettingsForm
def your_function(request):
....
context = {
....
'form':SettingsForm()
}
return render(request, 'template_name.html', context)
Now in your template add the form using
........
{{ form }}
.......

django-autocomplete-light initial data using Select2 widget

I have a form field with autocomplete (using django-autocomplete-light app and Select2 widget), which serve as a search filter and it works as expected.
When I submit the form and search results are listed, I would like to set this form field initial value to previously submitted value - so the user can adjust some of the search parameters instead of setting up all search filters from scratch.
This form field will be used to choose one of the ~10000 values, so I need it to load values on-demand. As the form field is not prepopulated with any values, I have no idea how it would be possible to set initial value.
models.py
class Location(models.Model):
place = models.CharField(max_length=50)
postal_code = models.CharField(max_length=5)
views.py
class LocationAutocomplete(autocomplete.Select2QuerySetView):
def get_queryset(self):
qs = Location.objects.all()
if self.q:
qs = qs.filter(place__istartswith=self.q) | qs.filter(postal_code__istartswith=self.q)
return qs
forms.py
class LocationForm(forms.ModelForm):
class Meta:
model = Location
fields = ('place',)
widgets = {
'place': autocomplete.Select2(url='location_autocomplete')
}
Any suggestions?
Thanks!
In your views.py if you pass place value like this:
LocationForm(initial={'place': place })
It will be pre-populated in your form.
Docs: https://docs.djangoproject.com/en/1.11/ref/forms/fields/#initial

Lazy loading a model field's choices

I'm building a Django app to pull in data via an API to track live results of an event with the added ability to override that data before it is displayed.
The first task of the app is to make a request and store the response in the database so I've setup a model;
class ApiData(models.Model):
event = models.CharField(
_("Event"),
max_length=100,
)
key = models.CharField(
_("Data identifier"),
max_length=255,
help_text=_("Something to identify the json stored.")
)
json = JSONField(
load_kwargs={'object_pairs_hook': collections.OrderedDict},
blank=True,
null=True,
)
created = models.DateTimeField()
Ideally I would like it so that objects are created in the admin and the save method populates the ApiData.json field after creating an API request based on the other options in the object.
Because these fields would have choices based on data returned from the API I wanted to lazy load the choices but at the moment I'm just getting a standard Charfield() in my form.
Is this the correct approach for lazy loading model field choices? Or should I just create a custom ModelForm and load the choices there? (That's probably the more typical approach I guess)
def get_event_choices():
events = get_events()
choices = []
for event in events['events']:
choices.append((event['name'], event['title']),)
return choices
class ApiData(models.Model):
# Fields as seen above
def __init__(self, *args, **kwargs):
super(ApiData, self).__init__(*args, **kwargs)
self._meta.get_field_by_name('event')[0]._choices = lazy(
get_event_choices, list
)()
So I went for a typical approach to get this working by simply defining a form for the model admin to use;
# forms.py
from django import forms
from ..models import get_event_choices, ApiData
from ..utils.api import JsonApi
EVENT_CHOICES = get_event_choices()
class ApiDataForm(forms.ModelForm):
"""
Form for collecting the field choices.
The Event field is populated based on the events returned from the API.
"""
event = forms.ChoiceField(choices=EVENT_CHOICES)
class Meta:
model = ApiData
# admin.py
from django.contrib import admin
from .forms.apidata import ApiDataForm
from .models import ApiData
class ApiDataAdmin(admin.ModelAdmin):
form = ApiDataForm
admin.site.register(ApiData, ApiDataAdmin)

Using an instance's fields to filter the choices of a manytomany selection in a Django admin view

I have a Django model with a ManyToManyField.
1) When adding a new instance of this model via admin view, I would like to not see the M2M field at all.
2) When editing an existing instance I would like to be able to select multiple options for the M2M field, but display only a subset of the M2M options, depending on another field in the model. Because of the dependence on another field's actual value, I can't just use formfield_for_manytomany
I can do both of the things using a custom ModelForm, but I can't reliably tell whether that form is being used to edit an existing model instance, or if it's being used to create a new instance. Even MyModel.objects.filter(pk=self.instance.pk).exists() in the custom ModelForm doesn't cut it. How can I accomplish this, or just tell whether the form is being displayed in an "add" or an "edit" context?
EDIT: my relevant code is as follows:
models.py
class LimitedClassForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(LimitedClassForm, self).__init__(*args, **kwargs)
if not self.instance._adding:
# Edit form
clas = self.instance
sheets_in_course = Sheet.objects.filter(course__pk=clas.course.pk)
self.Meta.exclude = ['course']
widget = self.fields['active_sheets'].widget
sheet_choices = []
for sheet in sheets_in_course:
sheet_choices.append((sheet.id, sheet.name))
widget.choices = sheet_choices
else:
# Add form
self.Meta.exclude = ['active_sheets']
class Meta:
exclude = []
admin.py
class ClassAdmin(admin.ModelAdmin):
formfield_overrides = {models.ManyToManyField: {
'widget': CheckboxSelectMultiple}, }
form = LimitedClassForm
admin.site.register(Class, ClassAdmin)
models.py
class Course(models.Model):
name = models.CharField(max_length=255)
class Sheet(models.Model):
name = models.CharField(max_length=255)
course = models.ForeignKey(Course)
file = models.FileField(upload_to=getSheetLocation)
class Class(models.model):
name = models.CharField(max_length=255)
course = models.ForeignKey(Course)
active_sheets = models.ManyToManyField(Sheet)
You can see that both Sheets and Classes have course fields. You shouldn't be able to put a sheet into active_sheets if the sheet's course doesn't match the class's course.

Class-based views for M2M relationship with intermediate model

I have a M2M relationship between two Models which uses an intermediate model. For the sake of discussion, let's use the example from the manual:
class Person(models.Model):
name = models.CharField(max_length=128)
def __unicode__(self):
return self.name
class Group(models.Model):
name = models.CharField(max_length=128)
members = models.ManyToManyField(Person, through='Membership')
def __unicode__(self):
return self.name
class Membership(models.Model):
person = models.ForeignKey(Person)
group = models.ForeignKey(Group)
date_joined = models.DateField()
invite_reason = models.CharField(max_length=64)
I'd like to make use of Django's Class-based views, to avoid writing CRUD-handling views. However, if I try to use the default CreateView, it doesn't work:
class GroupCreate(CreateView):
model=Group
This renders a form with all of the fields on the Group object, and gives a multi-select box for the members field, which would be correct for a simple M2M relationship. However, there is no way to specify the date_joined or invite_reason, and submitting the form gives the following AttributeError:
"Cannot set values on a ManyToManyField which specifies an intermediary model. Use Membership's Manager instead."
Is there a neat way to override part of the generic CreateView, or compose my own custom view to do this with mixins? It feels like this should be part of the framework, as the Admin interface atomatically handles M2M relationships with intermediates using inlines.
You must extend CreateView:
from django.views.generic import CreateView
class GroupCreate(CreateView):
model=Group
and override the form_valid():
from django.views.generic.edit import ModelFormMixin
from django.views.generic import CreateView
class GroupCreate(CreateView):
model = Group
def form_valid(self, form):
self.object = form.save(commit=False)
for person in form.cleaned_data['members']:
membership = Membership()
membership.group = self.object
membership.person = person
membership.save()
return super(ModelFormMixin, self).form_valid(form)
As the documentation says, you must create new memberships for each relation between group and person.
I saw the form_valid override here:
Using class-based UpdateView on a m-t-m with an intermediary model
class GroupCreate(CreateView):
model = Group
def form_valid(self, form):
self.object = form.save(commit=False)
### delete current mappings
Membership.objects.filter(group=self.object).delete()
### find or create (find if using soft delete)
for member in form.cleaned_data['members']:
x, created = Membership.objects.get_or_create(group=self.object, person=member)
x.group = self.object
x.person = member
#x.alive = True # if using soft delete
x.save()
return super(ModelFormMixin, self).form_valid(form)
'For reference, I didn't end up using a class-based view, instead I did something like this:
def group_create(request):
group_form = GroupForm(request.POST or None)
if request.POST and group_form.is_valid():
group = group_form.save(commit=False)
membership_formset = MembershipFormSet(request.POST, instance=group)
if membership_formset.is_valid():
group.save()
membership_formset.save()
return redirect('success_page.html')
else:
# Instantiate formset with POST data if this was a POST with an invalid from,
# or with no bound data (use existing) if this is a GET request for the edit page.
membership_formset = MembershipFormSet(request.POST or None, instance=Group())
return render_to_response(
'group_create.html',
{
'group_form': recipe_form,
'membership_formset': membership_formset,
},
context_instance=RequestContext(request),
)
This may be a starting point for a Class-based implementation, but it's simple enough that it's not been worth my while to try to shoehorn this into the Class-based paradigm.
I was facing pretty the same problem just a few days ago. Django has problems to process intermediary m2m relationships.
This is the solutions what I have found useful:
1. Define new CreateView
class GroupCreateView(CreateView):
form_class = GroupCreateForm
model = Group
template_name = 'forms/group_add.html'
success_url = '/thanks'
Then alter the save method of defined form - GroupCreateForm. Save is responsible for making changes permanent to DB. I wasn't able to make this work just through ORM, so I've used raw SQL too:
1. Define new CreateView
class GroupCreateView(CreateView):
class GroupCreateForm(ModelForm):
def save(self):
# get data from the form
data = self.cleaned_data
cursor = connection.cursor()
# use raw SQL to insert the object (in your case Group)
cursor.execute("""INSERT INTO group(group_id, name)
VALUES (%s, %s);""" (data['group_id'],data['name'],))
#commit changes to DB
transaction.commit_unless_managed()
# create m2m relationships (using classical object approach)
new_group = get_object_or_404(Group, klient_id = data['group_id'])
#for each relationship create new object in m2m entity
for el in data['members']:
Membership.objects.create(group = new_group, membership = el)
# return an object Group, not boolean!
return new_group
Note:I've changed the model a little bit, as you can see (i have own unique IntegerField for primary key, not using serial. That's how it got into get_object_or_404
Just one comment, when using CBV you need to save the form with commit=True, so the group is created and an id is given that can be used to create the memberships.
Otherwise, with commit=False, the group object has no id yet and an error is risen.