Repository navigation
Add the Profile model and SkillProfile models #828
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
f05f7ce
dda007d
e0114a6
a90d37d
7b5bce4
389e17e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,14 +22,15 @@ | |
| CompetencyDomain, | ||
| Event, | ||
| LearningResource, | ||
| Profile, | ||
| Provider, | ||
| Skill, | ||
| SkillLevel, | ||
| SkillProfile, | ||
| Team, | ||
| TeamMembership, | ||
| ToolLanguageMethodology, | ||
| User, | ||
| UserSkill, | ||
| ) | ||
|
|
||
|
|
||
|
|
@@ -159,13 +160,6 @@ class SkillLevelAdmin(ImportExportModelAdmin[SkillLevel]): | |
| resource_classes = (SkillLevelResource,) | ||
|
|
||
|
|
||
| class UserSkillInline(admin.TabularInline[UserSkill, User]): | ||
| """Inline admin class for the UserSkill model.""" | ||
|
|
||
| model = UserSkill | ||
| extra = 1 | ||
|
|
||
|
|
||
| @admin.register(Event) | ||
| class EventAdmin(ImportExportModelAdmin[Event]): | ||
| """Admin class for the Event model.""" | ||
|
|
@@ -176,53 +170,80 @@ class EventAdmin(ImportExportModelAdmin[Event]): | |
| resource_classes = (EventResource,) | ||
|
|
||
|
|
||
| class UserProxy(User): | ||
| """A proxy model of the User model to create a User Skills admin view. | ||
|
|
||
| This is required to make a second admin view tied to the User model. | ||
| """ | ||
| class SkillProfileInline(admin.TabularInline[SkillProfile, Profile]): | ||
| """Inline admin class for the SkillProfile model.""" | ||
|
|
||
| class Meta: | ||
| """Make model a proxy and set verbose names.""" | ||
|
|
||
| proxy = True | ||
| verbose_name = "User skills profile" | ||
| model = SkillProfile | ||
| extra = 1 | ||
|
|
||
|
|
||
| @admin.register(UserProxy) | ||
| class CustomUserSkillsProfilesAdmin(admin.ModelAdmin[UserProxy]): | ||
| """Admin class for adding UserSkills in bulk to individual Users.""" | ||
| @admin.register(Profile) | ||
| class ProfileAdmin(admin.ModelAdmin[Profile]): | ||
| """Admin class for the Profile model.""" | ||
|
|
||
| exclude = ( | ||
| "username", | ||
| "password", | ||
| "first_name", | ||
| "last_name", | ||
| "email", | ||
| "last_login", | ||
| "is_superuser", | ||
| "is_staff", | ||
| "is_active", | ||
| "date_joined", | ||
| "groups", | ||
| "user_permissions", | ||
| "agreed_to_tos", | ||
| "date_agreed", | ||
| fields = ( | ||
| "user", | ||
| "user_first_name", | ||
| "user_last_name", | ||
| "user_email", | ||
| "user_is_active", | ||
| ) | ||
| readonly_fields = ("username", "first_name", "last_name", "email", "is_active") | ||
| list_display = ("username", "is_active") | ||
| search_fields = ("username", "email") | ||
| list_filter = ("is_active",) | ||
| inlines = (UserSkillInline,) | ||
|
|
||
| def has_add_permission(self, request: HttpRequest) -> bool: | ||
| """Do not allow adding new users from this view.""" | ||
| return False | ||
| list_display = ( | ||
| "user", | ||
| "user__first_name", | ||
| "user__last_name", | ||
| "user__email", | ||
| "user__is_active", | ||
| ) | ||
| search_fields = ( | ||
| "user__username", | ||
| "user__first_name", | ||
| "user__last_name", | ||
| "user__email", | ||
| "user__is_active", | ||
| ) | ||
| list_filter = ("user__is_active",) | ||
| readonly_fields = fields | ||
| inlines = (SkillProfileInline,) | ||
|
|
||
| @admin.display(description="First name", ordering="user__first_name") | ||
| def user_first_name(self, obj: Profile) -> str: | ||
| """Render user first name in the detail/edit view.""" | ||
| if obj.user is None: | ||
| return "" | ||
| return obj.user.first_name | ||
|
|
||
| @admin.display(description="Last name", ordering="user__last_name") | ||
| def user_last_name(self, obj: Profile) -> str: | ||
| """Render user last name in the detail/edit view.""" | ||
| if obj.user is None: | ||
| return "" | ||
| return obj.user.last_name | ||
|
|
||
| @admin.display(description="Email", ordering="user__email") | ||
| def user_email(self, obj: Profile) -> str: | ||
| """Render user email in the detail/edit view.""" | ||
| if obj.user is None: | ||
| return "" | ||
| return obj.user.email | ||
|
|
||
| @admin.display(description="Active", boolean=True, ordering="user__is_active") | ||
| def user_is_active(self, obj: Profile) -> bool: | ||
| """Render user is_active status in the detail/edit view.""" | ||
| if obj.user is None: | ||
| return False | ||
| return obj.user.is_active | ||
|
|
||
| def has_delete_permission( | ||
| self, request: HttpRequest, obj: UserProxy | None = None | ||
| self, request: HttpRequest, obj: Profile | None = None | ||
| ) -> bool: | ||
| """Do not allow deleting users from this view.""" | ||
| """Prevent direct deletion of Profiles with associated users.""" | ||
| # Show checkboxes for bulk-deletion | ||
| if obj is None: | ||
| return True | ||
|
Comment on lines
+242
to
+243
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think there might be an issue with this. But I might be completely wrong 😅 So this is here so that the bulk deletion checkboxes appear, that's fine. But, the problem is that Django’s built-in Delete selected profiles action then deletes the selected profiles and I'm not sure it repeats this custom permission check separately for each profile. So a user with the relevant delete permissions can select a user-linked profile and delete it in bulk, even though deleting that same profile from its individual profile page is blocked.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not 100% sure of the exact order of things, but this is what I landed on when trying multiple options. It does appear that this function is called multiple times, at multiple stages, both with and without
But it remains possible to delete profiles with no associated user. |
||
| # Allow deletion via the User admin page and when there is no related user | ||
| if "user" in request.path or obj.user is None: | ||
| return True | ||
| return False | ||
|
|
||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| # Generated by Django 6.1.1 on 2026-10-27 08:16 | ||
|
|
||
| import django.db.models.deletion | ||
| from django.conf import settings | ||
| from django.db import migrations, models | ||
|
|
||
|
|
||
| class Migration(migrations.Migration): | ||
|
|
||
| dependencies = [ | ||
| ('main', '0031_team_teammembership'), | ||
| ] | ||
|
|
||
| operations = [ | ||
| migrations.AlterField( | ||
| model_name='userskill', | ||
| name='user', | ||
| field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), | ||
| ), | ||
| migrations.CreateModel( | ||
| name='Profile', | ||
| fields=[ | ||
| ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||
| ('user', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), | ||
| ], | ||
| ), | ||
| migrations.AddField( | ||
| model_name='userskill', | ||
| name='profile', | ||
| field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='main.profile'), | ||
| ), | ||
| ] |

Uh oh!
There was an error while loading. Please reload this page.