I have 3 different kinds of users in my application.
- Customer that goes on, finds appointments, and books appointments.
- Individual that can create appointments for customers to sign up for, and collect payments for the appointments
- Organization that can create appointments for customers to sign up for, collect payments for appointments, and provide links to individual providers that are employed in the organization (i.e. users in group 2 above)
I see types 1 and 2 having some overlap in that they are both individuals, and have fields like gender and date of birth that 3 does not, whereas 2 and 3 have overlap in that they are able to create appointments and collect payments.
I have structured my model classes as such:
class BaseProfileModel(models.Model):
user = models.OneToOneField(User, related_name="profile", primary_key=True)
phone = PhoneNumberField(verbose_name="Phone Number")
pic = models.ImageField(upload_to=get_upload_file_name,
width_field="width_field",
height_field="height_field",
null=True,
blank=True,
verbose_name="Profile Picture"
)
height_field = models.PositiveIntegerField(null=True, default=0)
width_field = models.PositiveIntegerField(null=True, default=0)
thumbnail = ImageSpecField(source='pic',
processors=[ResizeToFill(180,180)],
format='JPEG',
options={'quality': 100})
bio = models.TextField(
verbose_name="About",
default="",
blank=True,
max_length=800
)
is_provider=False
class Meta:
abstract = True
def __str__(self):
if self.user.email:
return self.user.email
else:
return self.user.username
@property
def thumbnail_url(self):
"""
Returns the URL of the image associated with this Object.
If an image hasn't been uploaded yet, it returns a stock image
:returns: str -- the image url
"""
if self.pic and hasattr(self.pic, 'url'):
return self.thumbnail.url
else:
# Return url for default thumbnail
# Make it the size of a thumbnail
return '/media/StockImage.png'
@property
def image_url(self):
if self.pic and hasattr(self.pic, 'url'):
return self.pic.url
else:
# Return url for full sized stock image
return '/media/StockImage.png'
def get_absolute_url(self):
return reverse_lazy(self.profile_url_name, kwargs={'pk': self.pk})
class BaseHumanUserModel(BaseProfileModel):
birth_date = models.DateField(verbose_name="Date of Birth", null=True, blank=True)
GENDER_CHOICES = (
('M', 'Male'),
('F', 'Female'),
('N', 'Not Specified'),
)
gender = models.CharField(
max_length=1, choices=GENDER_CHOICES, blank=False, default='N', verbose_name='Gender')
class Meta:
abstract = True
class BaseProviderModel(models.Model):
stripe_access_token = models.TextField(blank=True, default='')
is_provider=True
class Meta:
abstract = True
def rating(self):
avg = self.reviews.aggregate(Avg('rating'))
return avg['rating__avg']
def rounded_rating(self):
avg = self.rating()
return round(avg * 2) / 2
# More methods...
class IndividualProviderProfile(BaseProviderModel, BaseHumanUserModel):
locations = models.ManyToManyField(Location, null=True, blank=True, related_name='providers')
specialties = models.CharField(
verbose_name = "Specialties",
max_length=200,
blank=True,
)
certifications = models.CharField(
verbose_name = "Certifications", max_length=200,
blank=True, null=True
)
self.profile_url_name = 'profiles:individual_provider_profile'
def certifications_as_list(self):
return ''.join(self.certifications.split()).split(',')
def specialties_as_list(self):
return ''.join(self.specialties.split()).split(',')
class CustomerProfile(BaseHumanUserModel):
home_location = models.OneToOneField(
Location,
related_name='customer',
null=True,
blank=True,
on_delete=models.SET_NULL
)
self.profile_url_name = 'profiles:customer_profile'
# More methods...
class OrganizationProviderProfile(BaseProviderModel):
website = models.URLField(blank=True)
location = models.ForeignKey(Location)
employees = models.ManyToManyField(IndividualProviderProfile)
self.profile_url_name = 'profiles:organization_provider_profile'
# More methods
I am wondering a few things:
Does this separation of models into different classes make sense? Or would it be better to do something like making providers into one model, individual or not, and just leaving some fields as blank and a field specifying provider type? This just seems like a mess to me.
However, I'm seeing an issue with the way that I want to do things when it comes to ForeignKey relationships. I want users to be able to leave reviews on providers, which would require a foreign key to the provider. If they are different model classes then one ForeignKey will not cut it, unless I use the django contenttypes framework, which I haven't really looked into much. GenericForeignKeys seem like the way to go, unless this is bad practice to use a GenericForeignKey that is really only meant for two classes. So my question, for someone who has worked with the contenttypes framework before (or someone who has had a similar predicament), it be bad practice, and/or could my code end up getting messy, if I set up my models like this and use generic foreign keys to assign relationships to providers?
EDIT
Upon reconsidering, maybe this would be a better structure: Let me know what you think vs the above:
Leave the BaseProfileModel, BaseHumanUserModel, and CustomerProfileModel the same as above, and change the following to have OneToOne relationships
class ProviderDetails(models.Model):
stripe_access_token = models.TextField(blank=True, default='')
def rating(self):
avg = self.reviews.aggregate(Avg('rating'))
return avg['rating__avg']
def rounded_rating(self):
avg = self.rating()
return round(avg * 2) / 2
# More methods...
class IndividualProviderProfile(BaseHumanUserModel):
provider_details = models.OneToOneField(ProviderDetails, related_name='profile')
locations = models.ManyToManyField(Location, null=True, blank=True, related_name='providers')
specialties = models.CharField(
verbose_name = "Specialties",
max_length=200,
blank=True,
)
certifications = models.CharField(
verbose_name = "Certifications", max_length=200,
blank=True, null=True
)
self.profile_url_name = 'profiles:individual_provider_profile'
def certifications_as_list(self):
return ''.join(self.certifications.split()).split(',')
def specialties_as_list(self):
return ''.join(self.specialties.split()).split(',')
class OrganizationProviderProfile(BaseProfileModel):
provider_details = models.OneToOneField(ProviderDetails, related_name='profile')
website = models.URLField(blank=True)
location = models.ForeignKey(Location)
employees = models.ManyToManyField(IndividualProviderProfile)
self.profile_url_name = 'profiles:organization_provider_profile'
# More methods