Robertas
Robertas

Reputation: 129

How to add different model to ModelForm as extra field?

So I'm trying to add an extra field to a ModelForm class in the __init__ func, so that on the Create/Update class views I could grab this model and create a new model. Since the Model doesn't have this field, how could I add it in the __init__ func or is there some other way I could add this field?

I've tried overriding __init__ method to include this field in the ModelForm class, but this still throws me an error that the argument is unexpected

class MeterInstallationForm(ModelForm):

    class Meta:
        model = MeterInstallation
        fields = METER_INSTALLATION_DEFAULT_FIELDS

    def __init__(self, *args, **kwargs):
        instance = kwargs.get("instance")
        super(MeterInstallationForm, self).__init__(*args, **kwargs)
        if instance:
            #  get tariff info in the form
            # self.fields["tariff"] = instance.tariff
            self.fields["tariff"] = TariffApplication.objects.filter(meter_installation__pk=instance.meter_installation.pk).values_list("tariff", flat=True)

class MeterInstallationCreateView(MeterInstallationsViewMixin, LoginRequiredMixin, CreateView):
    template_name = "meter_installations/meter_installation_create.html"
    fields = (
        "name",
        "meter_type",
        "parent",
        "meter",
        "building",
        "initial_reading",
        "final_reading",
        "active_after",
        "active_until",
        "comment",
    )
    form_class = MeterInstallationForm


meter_installation_create_view = MeterInstallationCreateView.as_view()

class MeterInstallation(ActiveAfterUntilModel, DateTrackedModel, MPTTModel, NamedModel):  # type: ignore
    meter_type = models.ForeignKey(
        MeterType,
        on_delete=models.PROTECT,
        null=False,
        blank=False,
        related_name="installations",
        verbose_name=_("Meter Installation type"),
    )
    parent = TreeForeignKey(
        "self", on_delete=models.CASCADE, null=True, blank=True, related_name="children", db_index=True
    )
    meter = models.ForeignKey(
        Meter, on_delete=models.PROTECT, related_name="installations", null=False, blank=False, verbose_name=_("Meter")
    )
    building = models.ForeignKey(
        Building,
        on_delete=models.PROTECT,
        related_name="meter_installations",
        null=True,
        blank=False,
        verbose_name=_("Building"),
    )
    places = models.ManyToManyField(Place, related_name="meter_installations", blank=False, verbose_name=_("Places"))
    initial_reading = models.DecimalField(
        decimal_places=4, max_digits=10, null=False, blank=False, default=0, verbose_name=_("Initial reading")
    )
    final_reading = models.DecimalField(
        decimal_places=4, max_digits=10, null=True, blank=True, default=0, verbose_name=_("Final reading")
    )

    class MPTTMeta:
        order_insertion_by = ["meter"]

    def get_absolute_url(self):
        return reverse("meter-installations:meter-installation-detail", kwargs={"pk": self.pk})

    def delete(self, *args, **kwargs):
        first_lvl_children = self.get_children().filter(level=1)
        for first_lvl_child in first_lvl_children:
            first_lvl_child.parent = None
            first_lvl_child.save()
            for leaf in first_lvl_child.get_children():
                leaf.parent = first_lvl_child
                leaf.save()
            tree_id = first_lvl_child.tree_id

            MeterInstallation.objects.partial_rebuild(tree_id)

        super(MeterInstallation, self).delete(*args, **kwargs)

    def __str__(self):
        return f"[{self.pk}] type: {self.meter_type_id}, meter: {self.meter_id}"

class Tariff(ActiveAfterUntilModel, NamedModel, DateTrackedModel):
    tariff_type = models.ForeignKey(
        MeterType,
        on_delete=models.PROTECT,
        null=False,
        blank=False,
        related_name="tariffs",
        verbose_name=_("Tariff type"),
    )
    building = models.ForeignKey(
        Building, on_delete=models.PROTECT, related_name="tariffs", null=True, blank=False, verbose_name=_("Building")
    )
    unit_name = models.CharField(max_length=100, null=False, blank=True, unique=False, verbose_name=_("Unit name"))
    unit_price = models.DecimalField(
        decimal_places=4, max_digits=10, null=False, blank=False, default=0.0, verbose_name=_("Unit price")
    )
    VAT = models.DecimalField(decimal_places=2, max_digits=10, null=True, blank=True, verbose_name=_("VAT"))

    class Meta:
        unique_together = ("name", "tariff_type", "active_after", "active_until")

    def get_absolute_url(self):
        return reverse("tariffs:tariff-detail", kwargs={"pk": self.pk})

    def __str__(self) -> str:
        return (
            f"[{self.pk}] "
            f"type: {self.tariff_type_id}, "
            f"building: {self.building_id}, "
            f"price: {self.unit_price}, "
            f"VAT: {self.VAT}, "
            f"active_until: {self.active_until}"
        )

class TariffApplication(ActiveAfterUntilModel, DateTrackedModel):  # type: ignore
    tariff = models.ForeignKey(
        Tariff,
        on_delete=models.PROTECT,
        null=False,
        blank=False,
        related_name="tariff_applications",
        verbose_name=_("Tariff Applications"),
    )
    meter_installation = models.ForeignKey(
        "MeterInstallation",
        on_delete=models.PROTECT,
        null=False,
        blank=False,
        related_name="tariff_applications",
        verbose_name=_("Meter Installation"),
    )

    def __str__(self) -> str:
        return f"[{self.pk}] tariff: {self.tariff_id}, meter installation: {self.meter_installation_id}"

I would love to know how to make this work, so that in my CreateView I could start creating a third model by the given Tariff

Upvotes: 2

Views: 139

Answers (2)

Alasdair
Alasdair

Reputation: 308889

You can use a ModelChoiceField so that you can select a tariff on the form.

class MeterInstallationForm(ModelForm):

    class Meta:
        model = MeterInstallation
        fields = METER_INSTALLATION_DEFAULT_FIELDS

    def __init__(self, *args, **kwargs):
        super(MeterInstallationForm, self).__init__(*args, **kwargs)
        self.fields["tariff"] = forms.ModelChoiceField(queryset=Tariff.objects.all()) 

Then in your form_valid() method, you can retrieve the tariff from cleaned_data and create the related TariffApplication.

def form_valid(self, form):
    instance = form.save()
    TariffApplication.objects.create(tariff=form.cleaned_data['tariff'], meter_installation=instance)
    return HttpResponseRedirect(self.get_success_url())

You may need to change the queryset if you need to filter the list of available tariffs. In your original question, I don't think it makes sense to have if instance in the form's __init__ method, because an instance won't be passed to the form for a CreateView.

Upvotes: 1

cagrias
cagrias

Reputation: 1847

You can include extra field in your ModelForm and set that in your view such as:

# forms.py
class MeterInstallationForm(ModelForm):    
    class Meta:
        model = MeterInstallation
        fields = METER_INSTALLATION_DEFAULT_FIELDS + ('tariff',)
# views.py
class MeterInstallationCreateView(MeterInstallationsViewMixin, LoginRequiredMixin, CreateView):
    template_name = "meter_installations/meter_installation_create.html"
    fields = (
        "name",
        "meter_type",
        "parent",
        "meter",
        "building",
        "initial_reading",
        "final_reading",
        "active_after",
        "active_until",
        "comment",
    )
    form_class = MeterInstallationForm

    def form_valid(self, form):
        form.instance.tariff = forms.TariffApplication(form.data)
        return super().form_valid(form)

Upvotes: 1

Related Questions