Loading api/views.py +7 −1 Changes for api/views.py: 7 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -125,13 +125,19 @@ class VoteAPIView(APIView): class ResultsViewPermission(BasePermission): def has_permission(self, request, view): return request.user and request.user.is_authenticated and request.user.is_active if not request.user or not request.user.is_authenticated or not request.user.is_active: return False return True class ResultsView(APIView): permission_classes = [ResultsViewPermission] def get(self, request, election_id): election = get_object_or_404(Election, pk=election_id) if not request.user.is_superuser and not request.user == election.owner and not election.contributors.filter(user=request.user, can_view_stats=True).exists(): raise PermissionDenied options = list(election.options.values('name', 'votes')) if election.allow_invalid: Loading elections/admin.py +102 −3 Changes for elections/admin.py: 102 added lines, 3 removed lines. Original line number Diff line number Diff line Loading @@ -2,24 +2,123 @@ from django.contrib import admin from django.contrib.admin import AdminSite from django.utils.html import format_html from django.urls import reverse from django.db.models import Q from .models import Election, Option from .models import Election, Option, ElectionContributor class OptionInline(admin.TabularInline): model = Option exclude = ['votes'] extra = 1 class ElectionContributorInline(admin.TabularInline): model = ElectionContributor classes = ['collapse'] extra = 0 # def get_readonly_fields(self, request, obj = None): # if obj is None: # return super().get_readonly_fields(request, obj) # if obj.owner == request.user: # return [] # return ['can_manage_permissions'] def get_formset(self, request, obj = None, **kwargs): formset = super().get_formset(request, obj, **kwargs) old_init = formset.form.__init__ def new_init(form, *args, **kwargs): old_init(form, *args, **kwargs) if obj is None: return if form.instance.user_id == obj.owner.id or request.user.is_superuser: return elif form.instance.user_id == request.user.id: for field_name in form.fields: form.fields[field_name].disabled = True else: form.fields["can_manage_permissions"].disabled = True formset.form.__init__ = new_init return formset def has_add_permission(self, request, obj=None): if obj is None: return super().has_add_permission(request, obj) if obj.owner == request.user: return True return ElectionContributor.objects.filter(election=obj, user=request.user, can_manage_permissions=True).exists() def has_change_permission(self, request, obj=None): if obj is None: return super().has_change_permission(request, obj) if obj.owner == request.user: return True return ElectionContributor.objects.filter(election=obj, user_id=request.user.id, can_manage_permissions=True).exists() def has_delete_permission(self, request, obj = None): if obj is None: return super().has_delete_permission(request, obj) if obj.owner == request.user: return True return ElectionContributor.objects.filter(election=obj, user_id=request.user.id, can_manage_permissions=True).exists() def has_view_permission(self, request, obj = None): return True class ElectionAdmin(admin.ModelAdmin): list_display = ['name', 'start_date', 'end_date', 'is_active_status'] ordering = ['-created_at', '-updated_at'] exclude = ['invalid_votes', 'local_votes_entered'] inlines = (OptionInline, ) exclude = ['invalid_votes', 'local_votes_entered', 'owner'] inlines = (OptionInline, ElectionContributorInline) @admin.display(description="Aktywne", boolean=True) def is_active_status(self, obj): return obj.is_active def save_model(self, request, obj, form, change): if not change: obj.owner = request.user super().save_model(request, obj, form, change) def get_queryset(self, request): queryset = super().get_queryset(request) if request.user.is_superuser: return queryset return queryset.filter(Q(owner=request.user) | Q(contributors__user_id=request.user.id)) def has_add_permission(self, request): return super().has_add_permission(request) def has_change_permission(self, request, obj = None): if obj is None: return True contributor = ElectionContributor.objects.filter(election=obj, user_id=request.user.id).first() if contributor is not None and contributor.can_edit: return True return request.user.is_superuser or request.user == obj.owner def has_delete_permission(self, request, obj = None): if obj is None: return True contributor = ElectionContributor.objects.filter(election=obj, user_id=request.user.id).first() if contributor is not None and contributor.can_delete: return True return request.user.is_superuser or request.user == obj.owner def has_view_permission(self, request, obj = None): if obj is None: return True contributor = ElectionContributor.objects.filter(election=obj, user_id=request.user.id).first() if contributor is not None and contributor.can_view: return True return request.user.is_superuser or request.user == obj.owner # Register your models here. admin.site.register(Election, ElectionAdmin) No newline at end of file elections/migrations/0004_election_owner.py 0 → 100644 +22 −0 Changes for elections/migrations/0004_election_owner.py: 22 added lines, 0 removed lines. Original line number Diff line number Diff line # Generated by Django 6.0.3 on 2026-05-31 15:10 import django.db.models.deletion from django.conf import settings from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('elections', '0003_alter_option_unique_together'), migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ migrations.AddField( model_name='election', name='owner', field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.PROTECT, related_name='owned_elections', to=settings.AUTH_USER_MODEL), preserve_default=False, ), ] elections/migrations/0005_electioncontributor.py 0 → 100644 +30 −0 Changes for elections/migrations/0005_electioncontributor.py: 30 added lines, 0 removed lines. Original line number Diff line number Diff line # Generated by Django 6.0.3 on 2026-05-31 15:23 import django.db.models.deletion from django.conf import settings from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('elections', '0004_election_owner'), migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ migrations.CreateModel( name='ElectionContributor', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('can_view', models.BooleanField(default=True)), ('can_edit', models.BooleanField(default=True)), ('can_delete', models.BooleanField(default=True)), ('election', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='contributors', to='elections.election')), ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), ], options={ 'unique_together': {('election', 'user')}, }, ), ] elections/migrations/0006_alter_electioncontributor_options_and_more.py 0 → 100644 +60 −0 Changes for elections/migrations/0006_alter_electioncontributor_options_and_more.py: 60 added lines, 0 removed lines. Original line number Diff line number Diff line # Generated by Django 6.0.3 on 2026-05-31 15:51 import django.db.models.deletion from django.conf import settings from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('elections', '0005_electioncontributor'), migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ migrations.AlterModelOptions( name='electioncontributor', options={'verbose_name': 'Współpracownik', 'verbose_name_plural': 'Współpracownicy'}, ), migrations.AddField( model_name='electioncontributor', name='can_enter_local_votes', field=models.BooleanField(default=False, verbose_name='wprowadzanie głosów stacjonarnych'), ), migrations.AddField( model_name='electioncontributor', name='can_issue_cards', field=models.BooleanField(default=False, verbose_name='wydawanie kart'), ), migrations.AddField( model_name='electioncontributor', name='can_manage_permissions', field=models.BooleanField(default=False, verbose_name='zarządzanie uprawnieniami'), ), migrations.AddField( model_name='electioncontributor', name='can_view_stats', field=models.BooleanField(default=False, verbose_name='przeglądanie wyników'), ), migrations.AlterField( model_name='electioncontributor', name='can_delete', field=models.BooleanField(default=False, verbose_name='usuwanie'), ), migrations.AlterField( model_name='electioncontributor', name='can_edit', field=models.BooleanField(default=False, verbose_name='edycja'), ), migrations.AlterField( model_name='electioncontributor', name='can_view', field=models.BooleanField(default=True, verbose_name='wyświetlanie'), ), migrations.AlterField( model_name='electioncontributor', name='user', field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='użytkownik'), ), ] Loading
api/views.py +7 −1 Changes for api/views.py: 7 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -125,13 +125,19 @@ class VoteAPIView(APIView): class ResultsViewPermission(BasePermission): def has_permission(self, request, view): return request.user and request.user.is_authenticated and request.user.is_active if not request.user or not request.user.is_authenticated or not request.user.is_active: return False return True class ResultsView(APIView): permission_classes = [ResultsViewPermission] def get(self, request, election_id): election = get_object_or_404(Election, pk=election_id) if not request.user.is_superuser and not request.user == election.owner and not election.contributors.filter(user=request.user, can_view_stats=True).exists(): raise PermissionDenied options = list(election.options.values('name', 'votes')) if election.allow_invalid: Loading
elections/admin.py +102 −3 Changes for elections/admin.py: 102 added lines, 3 removed lines. Original line number Diff line number Diff line Loading @@ -2,24 +2,123 @@ from django.contrib import admin from django.contrib.admin import AdminSite from django.utils.html import format_html from django.urls import reverse from django.db.models import Q from .models import Election, Option from .models import Election, Option, ElectionContributor class OptionInline(admin.TabularInline): model = Option exclude = ['votes'] extra = 1 class ElectionContributorInline(admin.TabularInline): model = ElectionContributor classes = ['collapse'] extra = 0 # def get_readonly_fields(self, request, obj = None): # if obj is None: # return super().get_readonly_fields(request, obj) # if obj.owner == request.user: # return [] # return ['can_manage_permissions'] def get_formset(self, request, obj = None, **kwargs): formset = super().get_formset(request, obj, **kwargs) old_init = formset.form.__init__ def new_init(form, *args, **kwargs): old_init(form, *args, **kwargs) if obj is None: return if form.instance.user_id == obj.owner.id or request.user.is_superuser: return elif form.instance.user_id == request.user.id: for field_name in form.fields: form.fields[field_name].disabled = True else: form.fields["can_manage_permissions"].disabled = True formset.form.__init__ = new_init return formset def has_add_permission(self, request, obj=None): if obj is None: return super().has_add_permission(request, obj) if obj.owner == request.user: return True return ElectionContributor.objects.filter(election=obj, user=request.user, can_manage_permissions=True).exists() def has_change_permission(self, request, obj=None): if obj is None: return super().has_change_permission(request, obj) if obj.owner == request.user: return True return ElectionContributor.objects.filter(election=obj, user_id=request.user.id, can_manage_permissions=True).exists() def has_delete_permission(self, request, obj = None): if obj is None: return super().has_delete_permission(request, obj) if obj.owner == request.user: return True return ElectionContributor.objects.filter(election=obj, user_id=request.user.id, can_manage_permissions=True).exists() def has_view_permission(self, request, obj = None): return True class ElectionAdmin(admin.ModelAdmin): list_display = ['name', 'start_date', 'end_date', 'is_active_status'] ordering = ['-created_at', '-updated_at'] exclude = ['invalid_votes', 'local_votes_entered'] inlines = (OptionInline, ) exclude = ['invalid_votes', 'local_votes_entered', 'owner'] inlines = (OptionInline, ElectionContributorInline) @admin.display(description="Aktywne", boolean=True) def is_active_status(self, obj): return obj.is_active def save_model(self, request, obj, form, change): if not change: obj.owner = request.user super().save_model(request, obj, form, change) def get_queryset(self, request): queryset = super().get_queryset(request) if request.user.is_superuser: return queryset return queryset.filter(Q(owner=request.user) | Q(contributors__user_id=request.user.id)) def has_add_permission(self, request): return super().has_add_permission(request) def has_change_permission(self, request, obj = None): if obj is None: return True contributor = ElectionContributor.objects.filter(election=obj, user_id=request.user.id).first() if contributor is not None and contributor.can_edit: return True return request.user.is_superuser or request.user == obj.owner def has_delete_permission(self, request, obj = None): if obj is None: return True contributor = ElectionContributor.objects.filter(election=obj, user_id=request.user.id).first() if contributor is not None and contributor.can_delete: return True return request.user.is_superuser or request.user == obj.owner def has_view_permission(self, request, obj = None): if obj is None: return True contributor = ElectionContributor.objects.filter(election=obj, user_id=request.user.id).first() if contributor is not None and contributor.can_view: return True return request.user.is_superuser or request.user == obj.owner # Register your models here. admin.site.register(Election, ElectionAdmin) No newline at end of file
elections/migrations/0004_election_owner.py 0 → 100644 +22 −0 Changes for elections/migrations/0004_election_owner.py: 22 added lines, 0 removed lines. Original line number Diff line number Diff line # Generated by Django 6.0.3 on 2026-05-31 15:10 import django.db.models.deletion from django.conf import settings from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('elections', '0003_alter_option_unique_together'), migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ migrations.AddField( model_name='election', name='owner', field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.PROTECT, related_name='owned_elections', to=settings.AUTH_USER_MODEL), preserve_default=False, ), ]
elections/migrations/0005_electioncontributor.py 0 → 100644 +30 −0 Changes for elections/migrations/0005_electioncontributor.py: 30 added lines, 0 removed lines. Original line number Diff line number Diff line # Generated by Django 6.0.3 on 2026-05-31 15:23 import django.db.models.deletion from django.conf import settings from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('elections', '0004_election_owner'), migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ migrations.CreateModel( name='ElectionContributor', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('can_view', models.BooleanField(default=True)), ('can_edit', models.BooleanField(default=True)), ('can_delete', models.BooleanField(default=True)), ('election', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='contributors', to='elections.election')), ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), ], options={ 'unique_together': {('election', 'user')}, }, ), ]
elections/migrations/0006_alter_electioncontributor_options_and_more.py 0 → 100644 +60 −0 Changes for elections/migrations/0006_alter_electioncontributor_options_and_more.py: 60 added lines, 0 removed lines. Original line number Diff line number Diff line # Generated by Django 6.0.3 on 2026-05-31 15:51 import django.db.models.deletion from django.conf import settings from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('elections', '0005_electioncontributor'), migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ migrations.AlterModelOptions( name='electioncontributor', options={'verbose_name': 'Współpracownik', 'verbose_name_plural': 'Współpracownicy'}, ), migrations.AddField( model_name='electioncontributor', name='can_enter_local_votes', field=models.BooleanField(default=False, verbose_name='wprowadzanie głosów stacjonarnych'), ), migrations.AddField( model_name='electioncontributor', name='can_issue_cards', field=models.BooleanField(default=False, verbose_name='wydawanie kart'), ), migrations.AddField( model_name='electioncontributor', name='can_manage_permissions', field=models.BooleanField(default=False, verbose_name='zarządzanie uprawnieniami'), ), migrations.AddField( model_name='electioncontributor', name='can_view_stats', field=models.BooleanField(default=False, verbose_name='przeglądanie wyników'), ), migrations.AlterField( model_name='electioncontributor', name='can_delete', field=models.BooleanField(default=False, verbose_name='usuwanie'), ), migrations.AlterField( model_name='electioncontributor', name='can_edit', field=models.BooleanField(default=False, verbose_name='edycja'), ), migrations.AlterField( model_name='electioncontributor', name='can_view', field=models.BooleanField(default=True, verbose_name='wyświetlanie'), ), migrations.AlterField( model_name='electioncontributor', name='user', field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='użytkownik'), ), ]