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:
Gideon Thomas 2019-06-13 16:43:11 -04:00 коммит произвёл GitHub
Родитель 065dd3196b
Коммит fa5e510c08
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
8 изменённых файлов: 274 добавлений и 17 удалений

Просмотреть файл

@ -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
)