Skip to content

Commit

Permalink
Merge pull request #432 from PROCOLLAB-github/feature/user_education_…
Browse files Browse the repository at this point in the history
…full_info

PRO-413: New field user education
  • Loading branch information
pavuchara authored Sep 5, 2024
2 parents 12431a6 + a4de196 commit d0733ec
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 d0733ec

Please sign in to comment.