Hi all,
Context : I’m not the most experiences django developer, but I have go a fair amount of experience (8ish years) with other forms of web development and some databasing.
Im interested to know two things, firstly, am I approaching this all wrong, and secondly, how do I work with models within models within models when creating forms.
Lets take a snippet of one of my models
class EmailEnqueueData(models.Model):
#// competencies = ArrayField(models.CharField(max_length=20), blank=True, null=True)
competencies = models.JSONField(default=list, blank=True)
priority = models.IntegerField(blank=True, null=True)
queue = models.CharField(max_length=abs_models.NAME_LENGTH)
def json_model():
return {
"Skills": "[competencies]",
"Priority": "priority",
"QueueName": "queue"
}
class EmailReply(models.Model):
include_original = models.BooleanField(default=False)
from_address = models.CharField(max_length=abs_models.EMAIL_LENGTH)
subject = models.CharField(max_length=abs_models.NAME_LENGTH)
body = models.TextField()
def json_model():
return {
"IncludeOriginal": "include_original",
"From": "from_address",
"Subject": "subject",
"Body": "body"
}
class EmailBehaviour(models.Model):
"""Email behaviour settings for an Email, used for both Open and Close behaviours
Args:
models (Model): Django Model class
Params:
behaviour_type (CharField): The type of behaviour
competencies (JSONField): The competencies for the behaviour
priority (IntegerField): The priority of the behaviour
queue (CharField): The queue the behaviour belongs to
include_original (BooleanField): Whether to include the original email
from_address (CharField): The from address for the email
subject (CharField): The subject for the email
body (TextField): The body for the email
enable_reply (BooleanField): Whether to enable reply
enqueue_data (OneToOneField): The enqueue data for the behaviour (on_delete=models.CASCADE)
reply (OneToOneField): The reply settings for the behaviour (on_delete=models.CASCADE)
"""
behaviour_type = models.CharField(choices=Choices.Email_BehaviourChoices, blank=True, null=True, max_length=50)
enable_reply = models.BooleanField(default=False)
enqueue_data = models.OneToOneField(EmailEnqueueData, on_delete=models.CASCADE, blank=True, null=True)
reply = models.OneToOneField(EmailReply, on_delete=models.CASCADE, blank=True, null=True)
def json_model():
return {
"Behavior": "behaviour_type",
"EnableReply": "enable_reply",
"EnqueueData": "{enqueue_data}",
"Reply": "{reply}"
}
class EmailBusinessHours(models.Model):
"""Business hours settings for an Email
Args:
models (Model): Django Model class
Params:
buisness_hours_profile (CharField): The business hours profile
buisness_hours_timezone (CharField): The timezone for the business hours
"""
buisness_hours_profile = models.CharField(choices=Choices.BuisnessHourProfile_Choices, max_length=abs_models.TZ_LENGTH)
buisness_hours_timezone = models.CharField(choices=Choices.Timezone_Choices, max_length=abs_models.TZ_LENGTH)
def json_model():
return {
"Timezone": "buisness_hours_timezone",
"BusinessHoursProfile": "buisness_hours_profile"
}
class EmailBlockedAddress(models.Model):
"""Blocked email addresses for an Email
Args:
models (Model): Django Model class
Params:
type (CharField): The type of address
email (EmailField): The email address
reason (CharField): The reason for blocking the email
"""
type = models.CharField(choices=[('Email Address', 'Email Address')], blank=True, null=True, max_length=50)
email = models.EmailField(max_length=abs_models.EMAIL_LENGTH)
reason = models.CharField(max_length=abs_models.DESCRIPTION_LENGTH)
def json_model():
return {
"Type": "type",
"EmailAddress": "email",
"Description": "reason"
}
class EmailConfiguration(models.Model):
queue_behaviour = models.CharField(choices=Choices.EmailConfiguration_Choices, blank=True, null=True, max_length=50)
open_behaviour = models.OneToOneField(EmailBehaviour, on_delete=models.CASCADE, related_name='open_behaviour', blank=True, null=True)
close_behaviour = models.OneToOneField(EmailBehaviour, on_delete=models.CASCADE, related_name='close_behaviour', blank=True, null=True)
auto_reply = models.BooleanField(default=False)
business_hours = models.OneToOneField(EmailBusinessHours, on_delete=models.CASCADE, blank=True, null=True)
class Email(abs_models.AbstractEmail):
"""Email Configurations, or Email Touchpoints, define the behaviour, handling, and responses to inbound emails into ECX.
Args:
abs_models (AbstractEmail): An Abstract Model, Email, from abstracts.py. Django Model class under the hood.
Params:
cdw (ForeignKey): The CDW the email belongs to (on_delete=models.CASCADE)
email (EmailField): The email address
description (CharField): The description of the email
buisness_hours_timezone (CharField): The timezone for the business hours
buisness_hours_profile (CharField): The business hours profile
open_behaviour (OneToOneField): The EmailBehaviour for when the email is opened (on_delete=models.CASCADE)
close_behaviour (OneToOneField): The EmailBehaviour for when the email is closed (on_delete=models.CASCADE)
auto_reply (BooleanField): Whether to auto reply to the email
blocked_addresses (ManyToManyField): The EmailBlockedAddress for the email
"""
cdw = models.ForeignKey(CDW, on_delete=models.CASCADE, blank=True, null=True)
config = models.OneToOneField(EmailConfiguration, on_delete=models.CASCADE, blank=True, null=True)
blocked_addresses = models.ManyToManyField(EmailBlockedAddress, blank=True)
#competencies = models.ManyToManyField(Competencies, blank=True)
#priority = models.IntegerField(blank=True, null=True)
def json_model():
return {
"Address": "email_address",
"Description": "description",
"Configuration": "{config}",
"BlockedAddresses": "[{blocked_addresses}]"
}
The way I’ve got this laid out, is each level of the one-to-one field layer of the Email model represents a new layer in the JSON that itll be used to create for another platform (unfortunately I dont get to choose the project or the tool here, Im much more comfortable else where).
I have a generic CRUD function that rolls that takes a given models and its form, walks through, finds instances of the one-to-one fields, saves them, then searches them also. This is currently untested and new to the code base so excuse any major issues.
def addData(request, cdw_id, cdw_model, cdw_form, render_page, *, name_key='name', form_fn=None) -> HttpResponse:
"""A generic function to add data to a CDW model
Args:
request (HttpRequest): The request object
cdw_id (str): A unique identifier for the CDW
cdw_model (AbstractCDW): The CDW model class
cdw_form (forms.Form): The form class associated with the model provided
render_page (str): The page to render inside the current apps path
name_key (str, optional): The key which contains the identifiable string to add to Action Logs. Defaults to 'name'.
form_fn (Callable, optional): A function, with arguments request, cdw_form, and cdw_object, to do before the form is saved. Defaults to None.
Returns:
HttpResponse: The rendered page or a redirect to the CDW page
"""
cdw = get_object_or_404(cdw_model, id=cdw_id)
# If this isnt a post request, just send them to the render page with the provided cdw form and model
if request.method != 'POST':
return render(request, f'CDWApp/{render_page}', {'form': cdw_form(cdw=cdw), 'post': cdw})
form = cdw_form(request.POST, cdw=cdw)
if form.is_valid():
cdw_object = form.save(commit=False)
# Add all one-to-one fields in a Cascading manor, dont commit since we might have additonal save code to run
cdw_object = addOne2OneData(cdw_object, cdw_form, commit=False)
cdw_object.cdw = cdw
# if a form function has been provided, then run that
if form_fn is not None:
form_fn(request, form, cdw_object)
# commit a final save
cdw_object.save()
form.save_m2m()
# Update the logs for the current CDW to reflect changes
ActionLogNew.objects.create(
user=request.user,
action='create',
description=f'Created a {str(type(cdw_object))}: {cdw_object[name_key]}',
cdw=cdw
)
else:
print("Form is not valid:", form.errors)
return render(request, f'CDWApp/{render_page}', {'form' : form, 'cdw' : cdw})
def addOne2OneData(model_instance: models.Model, form_instance: forms.ModelForm, *, commit=False) -> models.Model:
# Get all one-to-one fields
one2ones = [field for field in model_instance._meta.fields if isinstance(field, models.OneToOneField)]
for field in one2ones:
# Get the model associated with the one to one field
related_model = field.remote_field.model
# Then we create a modelform from that model, assuming one doesnt exist
related_form_class = forms.modelform_factory(related_model, fields='__all__')
related_instance = None
related_form = None
# We check for a private key and if we find one, we set the instance and form to that one-to-one fields model
if form_instance.instance.pk:
related_instance = getattr(form_instance.instance, field.name, None)
related_form = related_form_class(instance=related_instance)
else:
related_form = related_form_class()
if all(form_instance.is_valid(), related_form.is_valid()):
# if both forms are valid then we save the related_form to update the instance
related_instance = related_form.save(commit=False)
# then we want to check the current model for its own one to one fields and return the updated model
related_instance = addOne2OneData(related_instance, related_form)
# then we set the original reference to the updated one to one model
setattr(form_instance.instance, field.name, related_instance)
# once returned, the current instance and all child one-to-one fields should be up to date.
return form_instance.save(commit)
However I then don’t have a single form (or easy way to represent those forms) on a webpage for someone to add information too (and test the code above).
So I guess to narrow down my question. Given my models structure, how would I create a form to accurate represent the full scope of the form? You can make the assumption that there are no many-to-many fields, while thats false, i can handle those myself.
If im in too deep and should just make it less general, thats also an option, so please let me know.
Also I’m happy to answer any questions and provide extra examples/missing context. Im relatively technical and know how to use google so please don’t leave out any details.
Thank you in advanced,
Oakley
p.s. the def json_model(self)
functions are how im converting the models to json, and that part of my code is already sorted so any major changes will likely cause me to rewrite a fair chuck of the code base again