Create models for storing multiple profile/program types (#492)
* Create models for storing multiple profile/program types * Flake8 fixes * Review fixes - naming mostly * Update cohort model name * Update var name * Add tests for profile category models * Add comments explaining uk prefix for indexes
This commit is contained in:
Родитель
065dd3196b
Коммит
fa5e510c08
|
@ -73,3 +73,10 @@ class ExtendedUserProfileFactory(BasicUserProfileFactory):
|
|||
profile_type = Iterator(ProfileType.objects.all())
|
||||
program_type = Iterator(ProgramType.objects.all())
|
||||
program_year = Iterator(ProgramYear.objects.all())
|
||||
|
||||
|
||||
class ProgramTypeFactory(DjangoModelFactory):
|
||||
value = Faker('job')
|
||||
|
||||
class Meta:
|
||||
model = ProgramType
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.16 on 2019-05-08 18:57
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import pulseapi.utility.validators
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('profiles', '0019_auto_20180625_1744'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='CohortRecord',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('year', models.PositiveSmallIntegerField(blank=True, null=True, validators=[pulseapi.utility.validators.YearValidator(max_offset=2)])),
|
||||
('cohort_name', models.CharField(blank=True, max_length=200, null=True)),
|
||||
('profile', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='cohort_records', to='profiles.UserProfile')),
|
||||
('program', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='profile_cohort_records', to='profiles.ProgramType')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'cohort record',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ProfileRole',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('is_current', models.BooleanField(default=True)),
|
||||
('profile', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='related_types', to='profiles.UserProfile')),
|
||||
('profile_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='related_profiles', to='profiles.ProfileType')),
|
||||
],
|
||||
),
|
||||
migrations.AlterOrderWithRespectTo(
|
||||
name='profilerole',
|
||||
order_with_respect_to='profile',
|
||||
),
|
||||
migrations.AlterOrderWithRespectTo(
|
||||
name='cohortrecord',
|
||||
order_with_respect_to='profile',
|
||||
),
|
||||
]
|
|
@ -0,0 +1,27 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.16 on 2019-05-08 18:58
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('profiles', '0020_auto_20190508_1857'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddIndex(
|
||||
model_name='cohortrecord',
|
||||
index=models.Index(fields=['profile', '_order'], name='uk_membership_profile_order'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='profilerole',
|
||||
index=models.Index(fields=['profile', 'is_current', '_order'], name='uk_role_profile_current_order'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='profilerole',
|
||||
index=models.Index(fields=['profile', 'is_current', 'profile_type'], name='uk_role_profile_current_type'),
|
||||
),
|
||||
]
|
|
@ -2,6 +2,8 @@ from pulseapi.profiles.models.categories import (
|
|||
ProfileType,
|
||||
ProgramType,
|
||||
ProgramYear,
|
||||
CohortRecord,
|
||||
ProfileRole
|
||||
)
|
||||
from pulseapi.profiles.models.bookmarks import UserBookmarks
|
||||
from pulseapi.profiles.models.profiles import UserProfile, entry_thumbnail_path
|
||||
|
@ -10,6 +12,8 @@ __all__ = [
|
|||
'ProfileType',
|
||||
'ProgramType',
|
||||
'ProgramYear',
|
||||
'CohortRecord',
|
||||
'ProfileRole',
|
||||
'UserProfile',
|
||||
'UserBookmarks',
|
||||
'entry_thumbnail_path',
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
from django.db import models
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from pulseapi.utility.validators import YearValidator
|
||||
|
||||
|
||||
class ProfileType(models.Model):
|
||||
|
@ -59,3 +63,87 @@ class ProgramYear(models.Model):
|
|||
|
||||
def __str__(self):
|
||||
return self.value
|
||||
|
||||
|
||||
class CohortRecord(models.Model):
|
||||
profile = models.ForeignKey(
|
||||
'profiles.UserProfile',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='cohort_records',
|
||||
)
|
||||
|
||||
program = models.ForeignKey(
|
||||
'profiles.ProgramType',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='profile_cohort_records',
|
||||
)
|
||||
|
||||
year = models.PositiveSmallIntegerField(
|
||||
# TODO: Change to MaxValueValidator with callable when
|
||||
# we update to Django > v2.2
|
||||
validators=[YearValidator(max_offset=2)],
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
cohort_name = models.CharField(
|
||||
null=True,
|
||||
blank=True,
|
||||
max_length=200,
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.profile.name} - {self.program} {str(self.year)} {self.cohort_name}'
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Don't allow both the cohort and year to be empty
|
||||
if self.year is None and not self.cohort_name:
|
||||
raise ValidationError(
|
||||
_('Either the year or cohort must have a value')
|
||||
)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.full_clean()
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
class Meta:
|
||||
verbose_name = 'cohort record'
|
||||
# This meta option creates an _order column in the table
|
||||
# See https://docs.djangoproject.com/en/1.11/ref/models/options/#order-with-respect-to for more details
|
||||
order_with_respect_to = 'profile'
|
||||
# We prefix the index name in the database with uk to indicate that the constraint is a unique key
|
||||
indexes = [
|
||||
models.Index(fields=['profile', '_order'], name='uk_membership_profile_order'),
|
||||
]
|
||||
|
||||
|
||||
class ProfileRole(models.Model):
|
||||
profile = models.ForeignKey(
|
||||
'profiles.UserProfile',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='related_types',
|
||||
)
|
||||
|
||||
profile_type = models.ForeignKey(
|
||||
'profiles.ProfileType',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='related_profiles',
|
||||
)
|
||||
|
||||
is_current = models.BooleanField(default=True)
|
||||
|
||||
def __str__(self):
|
||||
copular_verb = 'is' if self.is_current else 'was'
|
||||
return f'{self.profile.name} {copular_verb} a {self.role}'
|
||||
|
||||
class Meta:
|
||||
# This meta option creates an _order column in the table
|
||||
# See https://docs.djangoproject.com/en/1.11/ref/models/options/#order-with-respect-to for more details
|
||||
order_with_respect_to = 'profile'
|
||||
# We prefix the index name in the database with uk to indicate that the constraint is a unique key
|
||||
indexes = [
|
||||
models.Index(fields=['profile', 'is_current', '_order'], name='uk_role_profile_current_order'),
|
||||
models.Index(fields=['profile', 'is_current', 'profile_type'], name='uk_role_profile_current_type'),
|
||||
]
|
||||
|
|
|
@ -182,6 +182,7 @@ class UserProfile(models.Model):
|
|||
default=False
|
||||
)
|
||||
|
||||
# TODO: Deprecate
|
||||
profile_type = models.ForeignKey(
|
||||
'profiles.ProfileType',
|
||||
null=True,
|
||||
|
@ -190,6 +191,7 @@ class UserProfile(models.Model):
|
|||
# default is handled in save()
|
||||
)
|
||||
|
||||
# TODO: Deprecate
|
||||
program_type = models.ForeignKey(
|
||||
'profiles.ProgramType',
|
||||
null=True,
|
||||
|
@ -197,6 +199,7 @@ class UserProfile(models.Model):
|
|||
on_delete=models.SET_NULL
|
||||
)
|
||||
|
||||
# TODO: Deprecate
|
||||
program_year = models.ForeignKey(
|
||||
'profiles.ProgramYear',
|
||||
null=True,
|
||||
|
|
|
@ -1,21 +1,55 @@
|
|||
"""UserProfile Model Generator"""
|
||||
from pulseapi.profiles.models import UserProfile
|
||||
from datetime import date
|
||||
from django.test import TestCase
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
import factory
|
||||
from faker import Factory
|
||||
|
||||
fake = Factory.create()
|
||||
from pulseapi.profiles.models import CohortRecord
|
||||
from pulseapi.utility.validators import YearValidator
|
||||
from pulseapi.profiles.factory import (
|
||||
BasicUserProfileFactory,
|
||||
ProgramTypeFactory,
|
||||
)
|
||||
|
||||
|
||||
class UserProfileFactory(factory.Factory):
|
||||
"""Generate UserProfiles for tests"""
|
||||
is_active = True
|
||||
user_bio = factory.LazyAttribute(
|
||||
lambda o: 'user_bio {fake_bio}'.format(
|
||||
fake_bio=''.join(fake.text(max_nb_chars=130))
|
||||
)
|
||||
)
|
||||
class TestProfileCategories(TestCase):
|
||||
def setUp(self):
|
||||
self.profile = BasicUserProfileFactory()
|
||||
self.programs = [ProgramTypeFactory() for i in range(3)]
|
||||
|
||||
class Meta:
|
||||
"""Tell factory that it should be generating UserProfiles"""
|
||||
model = UserProfile
|
||||
def test_cohortrecord_year_under_valid_range(self):
|
||||
current_year = date.today().year
|
||||
expected_message = ValidationError(
|
||||
YearValidator.message,
|
||||
YearValidator.code,
|
||||
params={'min_year': 2000, 'max_year': current_year + 2}
|
||||
).messages[0]
|
||||
|
||||
with self.assertRaisesMessage(ValidationError, expected_message):
|
||||
CohortRecord.objects.create(
|
||||
profile=self.profile,
|
||||
program=self.programs[0],
|
||||
year=1999
|
||||
)
|
||||
|
||||
def test_cohortrecord_year_over_valid_range(self):
|
||||
current_year = date.today().year
|
||||
expected_message = ValidationError(
|
||||
YearValidator.message,
|
||||
YearValidator.code,
|
||||
params={'min_year': 2000, 'max_year': current_year + 2}
|
||||
).messages[0]
|
||||
|
||||
with self.assertRaisesMessage(ValidationError, expected_message):
|
||||
CohortRecord.objects.create(
|
||||
profile=self.profile,
|
||||
program=self.programs[0],
|
||||
year=current_year + 10
|
||||
)
|
||||
|
||||
def test_cohortrecord_missing_year_and_cohort_name(self):
|
||||
expected_message = 'Either the year or cohort must have a value'
|
||||
|
||||
with self.assertRaisesMessage(ValidationError, expected_message):
|
||||
CohortRecord.objects.create(
|
||||
profile=self.profile,
|
||||
program=self.programs[0],
|
||||
)
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
from datetime import date
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.deconstruct import deconstructible
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
@deconstructible
|
||||
class YearValidator:
|
||||
"""Validates that a year is within a specified range of years
|
||||
|
||||
Keyword arguments:
|
||||
min_year -- the lower limit of the year range (default 2000)
|
||||
max_offset -- the number of years to offset from the current year to
|
||||
determine the upper limit of the year range (default 10)
|
||||
"""
|
||||
message = _('The year must be between %(min_year)s and %(max_year)s.')
|
||||
code = 'year_value'
|
||||
|
||||
def __init__(self, min_year=2000, max_offset=10):
|
||||
current_year = date.today().year
|
||||
|
||||
if min_year > current_year:
|
||||
raise ValueError(f'The min_year passed ({min_year}) cannot be after the current year ({current_year})')
|
||||
|
||||
self.min_year = min_year
|
||||
self.max_offset = max_offset
|
||||
|
||||
def __call__(self, value):
|
||||
current_year = date.today().year
|
||||
max_year = current_year + self.max_offset
|
||||
|
||||
if value < self.min_year or value > max_year:
|
||||
raise ValidationError(
|
||||
self.message,
|
||||
code=self.code,
|
||||
params={'min_year': self.min_year, 'max_year': max_year}
|
||||
)
|
||||
|
||||
def __eq__(self, other):
|
||||
return (
|
||||
isinstance(other, self.__class__) and
|
||||
self.min_year == other.min_year and
|
||||
self.max_offset == other.max_offset and
|
||||
self.message == other.message and
|
||||
self.code == other.code
|
||||
)
|
Загрузка…
Ссылка в новой задаче