Skip to content

Commit

Permalink
PRO-413: New field user education
Browse files Browse the repository at this point in the history
Added new table: `UserEducation` (M:O)
Custom migrations for `organization` -> `UserEducation`:
```
python manage.py migrate_education
python manage.py reverse_migrate_education
```
The migrations themselves are "safe" because they affect only the new entity, without erasing or editing existing information.

`organization` - the built field for education functionality is fully preserved, the front must be ready for the new format.
Accordingly, to carry out the migration, the front must be ready for the new format of filling in information on education.
After all migrations and when the front is ready, it (the `organization` field) will simply need to be deleted.
  • Loading branch information
pavuchara committed Sep 3, 2024
1 parent e385ff1 commit a4de196
Show file tree
Hide file tree
Showing 8 changed files with 325 additions and 13 deletions.
44 changes: 44 additions & 0 deletions core/management/commands/migrate_education.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from django.db import transaction
from django.core.management.base import BaseCommand
from django.contrib.auth import get_user_model

from users.models import UserEducation


CustomUser = get_user_model()


class Command(BaseCommand):
"""
Use: python manage.py migrate_education.
"""
def handle(self, *args, **kwargs):
self.stdout.write("Start manual migration ...")
try:
total_user_migrate = migrate_organization_to_education()
self.stdout.write(
self.style.SUCCESS(
f"Manual migration complete, users migrated: {total_user_migrate}"
)
)
except Exception as e:
self.stderr.write(
self.style.ERROR(f"Migration failed: {str(e)}")
)


@transaction.atomic
def migrate_organization_to_education() -> int:
"""
Migrate old field `organization` to new model `Education`.
Returns count migrated users.
"""
users_with_irganization = CustomUser.objects.exclude(organization=None).exclude(organization="")
UserEducation.objects.bulk_create([
UserEducation(
user=user,
organization_name=user.organization,
)
for user in users_with_irganization
])
return users_with_irganization.count()
51 changes: 51 additions & 0 deletions core/management/commands/reverse_migrate_education.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
from django.db import transaction
from django.contrib.auth import get_user_model
from django.core.management.base import BaseCommand, CommandParser

from users.models import UserEducation


CustomUser = get_user_model()


class Command(BaseCommand):
"""
Use: python manage.py reverse_migrate_education.
"""

def add_arguments(self, parser: CommandParser) -> None:
parser.add_argument(
"--confirm",
action="store_true",
help="Confirm delete Users educations from UserEducation model"
)

def handle(self, *args, **kwargs):
confirm = kwargs["confirm"]
self.stdout.write(self.style.WARNING(
"You are about to DELETE ALL INSTANCES in the UserEducation model."))

if not confirm:
answer = input("Type 'yes' to continue, or 'no' to cancel: ").lower()
if answer != "yes":
self.stdout.write(self.style.ERROR("Manual migrations canceled."))
return

self.stdout.write("Starting manual migrations...")

try:
deleted_instances = delete_all_instances_usereducation()
self.stdout.write(self.style.SUCCESS("Manual migrations completed successfully."))
self.stdout.write(self.style.SUCCESS(f"Deleted: {deleted_instances}"))
except Exception as e:
self.stderr.write(self.style.ERROR(f"Manual migrations failed: {str(e)}"))


@transaction.atomic
def delete_all_instances_usereducation() -> int:
"""
Destroy all UserEducation instances.
"""
count = UserEducation.objects.count()
UserEducation.objects.all().delete()
return count
18 changes: 17 additions & 1 deletion users/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,21 @@
Expert,
Investor,
UserLink,
UserEducation,
)

from core.admin import SkillToObjectInline

admin.site.register(Permission)


class UserEducationInline(admin.TabularInline):
model = UserEducation
extra = 1
verbose_name = "Образование"
verbose_name_plural = "Образования"


@admin.register(CustomUser)
class CustomUserAdmin(admin.ModelAdmin):
fieldsets = (
Expand Down Expand Up @@ -68,7 +76,7 @@ class CustomUserAdmin(admin.ModelAdmin):
"status",
"city",
"region",
"organization",
"organization", # TODO need to be removed in future.
"speciality",
"v2_speciality",
"key_skills",
Expand Down Expand Up @@ -124,6 +132,7 @@ class CustomUserAdmin(admin.ModelAdmin):

inlines = [
SkillToObjectInline,
UserEducationInline,
]

readonly_fields = ("ordering_score",)
Expand Down Expand Up @@ -315,3 +324,10 @@ class UserLinkAdmin(admin.ModelAdmin):
@admin.register(Expert)
class ExpertAdmin(admin.ModelAdmin):
list_display = ("id", "user")


@admin.register(UserEducation)
class UserEducationAdmin(admin.ModelAdmin):
list_display = ("id", "user", "organization_name", "entry_year")
list_display_links = ("id", "organization_name")
search_fields = ("user__first_name", "user__email")
2 changes: 1 addition & 1 deletion users/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ class Meta:
"patronymic",
"city",
"region",
"organization",
"organization", # TODO need to be removed in future.
"user_type",
"speciality",
)
Expand Down
97 changes: 97 additions & 0 deletions users/migrations/0049_alter_customuser_key_skills_and_more.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# Generated by Django 4.2.11 on 2024-09-02 16:01

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import users.validators


class Migration(migrations.Migration):

dependencies = [
("users", "0048_customuser_dataset_migration_applied"),
]

operations = [
migrations.AlterField(
model_name="customuser",
name="key_skills",
field=models.CharField(
blank=True,
help_text="Устаревшее поле -> skills",
max_length=512,
null=True,
),
),
migrations.AlterField(
model_name="customuser",
name="organization",
field=models.CharField(
blank=True,
help_text="Устаревшее поле -> UserEducation",
max_length=255,
null=True,
),
),
migrations.AlterField(
model_name="customuser",
name="speciality",
field=models.CharField(
blank=True,
help_text="Устаревшее поле -> v2_speciality",
max_length=255,
null=True,
),
),
migrations.CreateModel(
name="UserEducation",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"organization_name",
models.CharField(
max_length=255, verbose_name="Наименование организации"
),
),
(
"description",
models.CharField(
blank=True,
max_length=255,
null=True,
verbose_name="Краткое описание",
),
),
(
"entry_year",
models.PositiveSmallIntegerField(
blank=True,
null=True,
validators=[users.validators.user_entry_year_education_validator],
verbose_name="Год поступления",
),
),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="education",
to=settings.AUTH_USER_MODEL,
verbose_name="Пользователь",
),
),
],
options={
"verbose_name": "Образование",
"verbose_name_plural": "Образование",
},
),
]
82 changes: 73 additions & 9 deletions users/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@
UserAchievementManager,
LikesOnProjectManager,
)
from users.validators import user_birthday_validator, user_name_validator
from users.validators import (
user_birthday_validator,
user_name_validator,
user_entry_year_education_validator,
)


def get_default_user_type():
Expand Down Expand Up @@ -71,22 +75,24 @@ class CustomUser(AbstractUser):
choices=VERBOSE_USER_TYPES,
default=get_default_user_type,
)

ordering_score = models.PositiveIntegerField(
default=0,
editable=False,
)
patronymic = models.CharField(
max_length=255, validators=[user_name_validator], null=True, blank=True
)
# TODO need to be removed in future `key_skills` -> `skills`.
key_skills = models.CharField(
max_length=512, null=True, blank=True
) # to be deprecated in future
max_length=512,
null=True,
blank=True,
help_text="Устаревшее поле -> skills",
)
skills = GenericRelation(
"core.SkillToObject",
related_query_name="users",
)

avatar = models.URLField(null=True, blank=True)
birthday = models.DateField(
validators=[user_birthday_validator],
Expand All @@ -95,14 +101,26 @@ class CustomUser(AbstractUser):
status = models.CharField(max_length=255, null=True, blank=True)
region = models.CharField(max_length=255, null=True, blank=True)
city = models.CharField(max_length=255, null=True, blank=True)
organization = models.CharField(max_length=255, null=True, blank=True)
# TODO need to be removed in future `organization` -> `education`.
organization = models.CharField(
max_length=255,
null=True,
blank=True,
help_text="Устаревшее поле -> UserEducation",
)
v2_speciality = models.ForeignKey(
on_delete=models.SET_NULL,
null=True,
related_name="users",
to="core.Specialization",
)
speciality = models.CharField(max_length=255, null=True, blank=True) # to be deprecated in future
# TODO need to be removed in future `speciality` -> `v2_speciality`.
speciality = models.CharField(
max_length=255,
null=True,
blank=True,
help_text="Устаревшее поле -> v2_speciality",
)
onboarding_stage = models.PositiveSmallIntegerField(
null=True,
blank=True,
Expand All @@ -119,7 +137,8 @@ class CustomUser(AbstractUser):
)
datetime_updated = models.DateTimeField(auto_now=True)
datetime_created = models.DateTimeField(auto_now_add=True)
dataset_migration_applied = models.BooleanField( # To be deprecated in future.
# TODO need to be removed in future.
dataset_migration_applied = models.BooleanField(
null=True,
blank=True,
default=False,
Expand Down Expand Up @@ -157,7 +176,8 @@ def calculate_ordering_score(self) -> int:
score += 4
if self.city:
score += 4
if self.organization:
# TODO need to be removed in future.
if self.organization or self.education.all().exists():
score += 6
if self.speciality:
score += 7
Expand Down Expand Up @@ -411,3 +431,47 @@ class Meta(TypedModelMeta):
verbose_name = "Ссылка пользователя"
verbose_name_plural = "Ссылки пользователей"
unique_together = ("user", "link")


class UserEducation(models.Model):
"""
User education model
User education information.
Attributes:
user: FK CustomUser.
organization_name: CharField Name of the organization.
description: CharField Organization Description.
entry_year: PositiveSmallIntegerField Year of admission.
"""
user = models.ForeignKey(
to=CustomUser,
on_delete=models.CASCADE,
related_name="education",
verbose_name="Пользователь",
)
organization_name = models.CharField(
max_length=255,
verbose_name="Наименование организации",
)
description = models.CharField(
max_length=255,
null=True,
blank=True,
verbose_name="Краткое описание",
)
entry_year = models.PositiveSmallIntegerField(
null=True,
blank=True,
validators=[user_entry_year_education_validator],
verbose_name="Год поступления",
)

class Meta:
verbose_name = "Образование"
verbose_name_plural = "Образование"

def __str__(self) -> str:
return (f"{self.user.first_name}: {self.organization_name} - "
f"{self.entry_year} (id {self.id})")
Loading

0 comments on commit a4de196

Please sign in to comment.