multiple: use object level permissions

object-level-permissions
Christian Merten 3 years ago
parent 1b06aff1a1
commit 7255190153
Signed by: christian.merten
GPG Key ID: D953D69721B948B3

@ -8,8 +8,12 @@ from django.utils.translation import gettext_lazy as _
from django.shortcuts import render
from django.conf import settings
from contrib.admin import CommonAdminInlineMixin, CommonAdminMixin
from rules.contrib.admin import ObjectPermissionsModelAdmin
from .models import Ledger, Statement, Receipt, Transaction, Bill, StatementSubmitted, StatementConfirmed,\
StatementUnSubmitted
StatementUnSubmitted, BillOnStatementProxy
@admin.register(Ledger)
@ -17,8 +21,8 @@ class LedgerAdmin(admin.ModelAdmin):
search_fields = ('name', )
class BillOnStatementInline(admin.TabularInline):
model = Bill
class BillOnStatementInline(CommonAdminInlineMixin, admin.TabularInline):
model = BillOnStatementProxy
extra = 0
sortable_options = []
fields = ['short_description', 'explanation', 'amount', 'paid_by', 'proof']
@ -26,16 +30,11 @@ class BillOnStatementInline(admin.TabularInline):
TextField: {'widget': Textarea(attrs={'rows': 1, 'cols': 40})}
}
def get_readonly_fields(self, request, obj=None):
if obj is not None and obj.submitted:
return self.fields
return super(BillOnStatementInline, self).get_readonly_fields(request, obj)
@admin.register(StatementUnSubmitted)
class StatementUnSubmittedAdmin(admin.ModelAdmin):
class StatementUnSubmittedAdmin(CommonAdminMixin, admin.ModelAdmin):
fields = ['short_description', 'explanation', 'excursion', 'submitted']
list_display = ['__str__', 'excursion']
list_display = ['__str__', 'excursion', 'created_by']
inlines = [BillOnStatementInline]
def save_model(self, request, obj, form, change):
@ -43,16 +42,6 @@ class StatementUnSubmittedAdmin(admin.ModelAdmin):
obj.created_by = request.user.member
super().save_model(request, obj, form, change)
def get_queryset(self, request):
queryset = super().get_queryset(request)
if request.user.has_perm('members.may_list_all_statements'):
return queryset
if not hasattr(request.user, 'member'):
return Member.objects.none()
return queryset.filter(Q(created_by=request.user.member) | Q(excursion__jugendleiter=request.user.member))
def get_readonly_fields(self, request, obj=None):
readonly_fields = ['submitted', 'excursion']
if obj is not None and obj.submitted:
@ -109,7 +98,7 @@ class TransactionOnSubmittedStatementInline(admin.TabularInline):
class BillOnSubmittedStatementInline(BillOnStatementInline):
model = Bill
model = BillOnStatementProxy
extra = 0
sortable_options = []
fields = ['short_description', 'explanation', 'amount', 'paid_by', 'proof', 'costs_covered']

@ -0,0 +1,49 @@
# Generated by Django 4.0.1 on 2023-04-03 20:34
from django.db import migrations
class Migration(migrations.Migration):
#replaces = [('finance', '0002_billonexcursionproxy_billonstatementproxy_and_more'), ('finance', '0003_alter_statementunsubmitted_options'), ('finance', '0004_alter_billonexcursionproxy_options'), ('finance', '0005_alter_billonstatementproxy_options'), ('finance', '0006_alter_statementsubmitted_options'), ('finance', '0007_alter_billonexcursionproxy_options_and_more')]
dependencies = [
('finance', '0001_initial'),
]
operations = [
migrations.AlterModelOptions(
name='statementsubmitted',
options={'permissions': [('process_statementsubmitted', 'Can manage submitted statements.')], 'verbose_name': 'Submitted statement', 'verbose_name_plural': 'Submitted statements'},
),
migrations.CreateModel(
name='BillOnExcursionProxy',
fields=[
],
options={
'verbose_name': 'Bill',
'verbose_name_plural': 'Bills',
'proxy': True,
'indexes': [],
'constraints': [],
},
bases=('finance.bill',),
),
migrations.CreateModel(
name='BillOnStatementProxy',
fields=[
],
options={
'verbose_name': 'Bill',
'verbose_name_plural': 'Bills',
'proxy': True,
'indexes': [],
'constraints': [],
},
bases=('finance.bill',),
),
migrations.AlterModelOptions(
name='statementunsubmitted',
options={'verbose_name': 'Statement in preparation', 'verbose_name_plural': 'Statements in preparation'},
),
]

@ -0,0 +1,41 @@
# Generated by Django 4.0.1 on 2023-04-04 12:37
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('finance', '0002_alter_permissions'),
]
operations = [
migrations.AlterModelOptions(
name='bill',
options={'default_permissions': ('add_global', 'change_global', 'view_global', 'delete_global', 'list_global', 'view'), 'verbose_name': 'Bill', 'verbose_name_plural': 'Bills'},
),
migrations.AlterModelOptions(
name='billonexcursionproxy',
options={'default_permissions': ('add_global', 'change_global', 'view_global', 'delete_global', 'list_global', 'view'), 'verbose_name': 'Bill', 'verbose_name_plural': 'Bills'},
),
migrations.AlterModelOptions(
name='billonstatementproxy',
options={'default_permissions': ('add_global', 'change_global', 'view_global', 'delete_global', 'list_global', 'view'), 'verbose_name': 'Bill', 'verbose_name_plural': 'Bills'},
),
migrations.AlterModelOptions(
name='statement',
options={'default_permissions': ('add_global', 'change_global', 'view_global', 'delete_global', 'list_global', 'view'), 'permissions': [('may_edit_submitted_statements', 'Is allowed to edit submitted statements')], 'verbose_name': 'Statement', 'verbose_name_plural': 'Statements'},
),
migrations.AlterModelOptions(
name='statementconfirmed',
options={'default_permissions': ('add_global', 'change_global', 'view_global', 'delete_global', 'list_global', 'view'), 'permissions': [('may_manage_confirmed_statements', 'Can view and manage confirmed statements.')], 'verbose_name': 'Paid statement', 'verbose_name_plural': 'Paid statements'},
),
migrations.AlterModelOptions(
name='statementsubmitted',
options={'default_permissions': ('add_global', 'change_global', 'view_global', 'delete_global', 'list_global', 'view'), 'permissions': [('process_statementsubmitted', 'Can manage submitted statements.')], 'verbose_name': 'Submitted statement', 'verbose_name_plural': 'Submitted statements'},
),
migrations.AlterModelOptions(
name='statementunsubmitted',
options={'default_permissions': ('add_global', 'change_global', 'view_global', 'delete_global', 'list_global', 'view'), 'verbose_name': 'Statement in preparation', 'verbose_name_plural': 'Statements in preparation'},
),
]

@ -2,11 +2,16 @@ import math
from itertools import groupby
from decimal import Decimal, ROUND_HALF_DOWN
from django.utils import timezone
from .rules import is_creator, not_submitted, leads_excursion
from members.rules import is_leader, statement_not_submitted
from django.db import models
from django.utils.translation import gettext_lazy as _
from members.models import Member, Freizeit, OEFFENTLICHE_ANREISE, MUSKELKRAFT_ANREISE
from django.conf import settings
import rules
from contrib.models import CommonModel
from contrib.rules import has_global_perm
# Create your models here.
@ -35,7 +40,7 @@ class StatementManager(models.Manager):
return super().get_queryset().filter(submitted=False, confirmed=False)
class Statement(models.Model):
class Statement(CommonModel):
MISSING_LEDGER, NON_MATCHING_TRANSACTIONS, VALID = 0, 1, 2
short_description = models.CharField(verbose_name=_('Short description'),
@ -71,10 +76,20 @@ class Statement(models.Model):
on_delete=models.SET_NULL,
related_name='confirmed_statements')
class Meta:
class Meta(CommonModel.Meta):
verbose_name = _('Statement')
verbose_name_plural = _('Statements')
permissions = [('may_edit_submitted_statements', 'Is allowed to edit submitted statements')]
permissions = [
('may_edit_submitted_statements', 'Is allowed to edit submitted statements')
]
rules_permissions = {
# this is suboptimal, but Statement is only ever used as an inline on Freizeit
# so we check for excursion permissions
'add_obj': is_leader,
'view_obj': is_leader | has_global_perm('members.view_global_freizeit'),
'change_obj': is_leader & statement_not_submitted,
'delete_obj': is_leader & statement_not_submitted,
}
def __str__(self):
if self.excursion is not None:
@ -306,10 +321,16 @@ class StatementUnSubmittedManager(models.Manager):
class StatementUnSubmitted(Statement):
objects = StatementUnSubmittedManager()
class Meta:
class Meta(CommonModel.Meta):
proxy = True
verbose_name = _('Statement in preparation')
verbose_name_plural = _('Statements in preparation')
rules_permissions = {
'add_obj': rules.is_staff,
'view_obj': is_creator | leads_excursion | has_global_perm('finance.view_global_statementunsubmitted'),
'change_obj': is_creator | leads_excursion,
'delete_obj': is_creator | leads_excursion,
}
class StatementSubmittedManager(models.Manager):
@ -320,11 +341,13 @@ class StatementSubmittedManager(models.Manager):
class StatementSubmitted(Statement):
objects = StatementSubmittedManager()
class Meta:
class Meta(CommonModel.Meta):
proxy = True
verbose_name = _('Submitted statement')
verbose_name_plural = _('Submitted statements')
permissions = (('may_manage_submitted_statements', 'Can view and manage submitted statements.'),)
permissions = [
('process_statementsubmitted', 'Can manage submitted statements.'),
]
class StatementConfirmedManager(models.Manager):
@ -335,14 +358,16 @@ class StatementConfirmedManager(models.Manager):
class StatementConfirmed(Statement):
objects = StatementConfirmedManager()
class Meta:
class Meta(CommonModel.Meta):
proxy = True
verbose_name = _('Paid statement')
verbose_name_plural = _('Paid statements')
permissions = (('may_manage_confirmed_statements', 'Can view and manage confirmed statements.'),)
permissions = [
('may_manage_confirmed_statements', 'Can view and manage confirmed statements.'),
]
class Bill(models.Model):
class Bill(CommonModel):
statement = models.ForeignKey(Statement, verbose_name=_('Statement'), on_delete=models.CASCADE)
short_description = models.CharField(verbose_name=_('Short description'), max_length=30)
explanation = models.TextField(verbose_name=_('Explanation'), blank=True)
@ -363,9 +388,37 @@ class Bill(models.Model):
pretty_amount.admin_order_field = 'amount'
pretty_amount.short_description = _('Amount')
class Meta:
class Meta(CommonModel.Meta):
verbose_name = _('Bill')
verbose_name_plural = _('Bills')
class BillOnExcursionProxy(Bill):
class Meta(CommonModel.Meta):
proxy = True
verbose_name = _('Bill')
verbose_name_plural = _('Bills')
rules_permissions = {
'add_obj': leads_excursion & not_submitted,
'view_obj': leads_excursion | has_global_perm('finance.view_global_billonexcursionproxy'),
'change_obj': (leads_excursion | has_global_perm('finance.change_global_billonexcursionproxy')) & not_submitted,
'delete_obj': (leads_excursion | has_global_perm('finance.delete_global_billonexcursionproxy')) & not_submitted,
}
class BillOnStatementProxy(Bill):
class Meta(CommonModel.Meta):
proxy = True
verbose_name = _('Bill')
verbose_name_plural = _('Bills')
rules_permissions = {
'add_obj': (is_creator | leads_excursion) & not_submitted,
'view_obj': is_creator | leads_excursion | has_global_perm('finance.view_global_billonstatementproxy'),
'change_obj': (is_creator | leads_excursion | has_global_perm('finance.change_global_billonstatementproxy'))
& (not_submitted | has_global_perm('finance.process_statementsubmitted')),
'delete_obj': (is_creator | leads_excursion | has_global_perm('finance.delete_global_billonstatementproxy'))
& not_submitted,
}
class Transaction(models.Model):

@ -0,0 +1,36 @@
from members.models import Freizeit
from contrib.rules import memberize_user
from rules import predicate
from members.rules import _is_leader
@predicate
@memberize_user
def is_creator(self, statement):
assert statement is not None
return statement.created_by == self
@predicate
@memberize_user
def not_submitted(self, statement):
assert statement is not None
if isinstance(statement, Freizeit):
if hasattr(statement, 'statement'):
return not statement.statement.submitted
else:
return True
return not statement.submitted
@predicate
@memberize_user
def leads_excursion(self, statement):
assert statement is not None
if isinstance(statement, Freizeit):
return _is_leader(self, statement)
if not hasattr(statement, 'excursion'):
return False
if statement.excursion is None:
return False
return _is_leader(self, statement.excursion)

@ -7,12 +7,15 @@ from django import forms
#from easy_select2 import apply_select2
import json
from rules.contrib.admin import ObjectPermissionsModelAdmin
from .models import Message, Attachment, MessageForm, EmailAddress, EmailAddressForm
from .mailutils import NOT_SENT, PARTLY_SENT
from members.models import Member
from contrib.admin import CommonAdminMixin, CommonAdminInlineMixin
class AttachmentInline(admin.TabularInline):
class AttachmentInline(CommonAdminInlineMixin, admin.TabularInline):
model = Attachment
extra = 0
@ -27,8 +30,9 @@ class EmailAddressAdmin(admin.ModelAdmin):
form = EmailAddressForm
class MessageAdmin(admin.ModelAdmin):
class MessageAdmin(CommonAdminMixin, ObjectPermissionsModelAdmin):
"""Message creation view"""
exclude = ('created_by',)
list_display = ('subject', 'get_recipients', 'sent')
search_fields = ('subject',)
change_form_template = "mailer/change_form.html"
@ -42,6 +46,11 @@ class MessageAdmin(admin.ModelAdmin):
form = MessageForm
filter_horizontal = ('to_members','reply_to')
def save_model(self, request, obj, form, change):
if not change and hasattr(request.user, 'member'):
obj.created_by = request.user.member
super().save_model(request, obj, form, change)
def send_message(self, request, queryset):
if request.POST.get('confirmed'):
for msg in queryset:

@ -0,0 +1,20 @@
# Generated by Django 4.0.1 on 2023-04-02 12:06
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('members', '0006_rename_permissions'),
('mailer', '0001_initial_squashed_0006_auto_20210924_1155'),
]
operations = [
migrations.AddField(
model_name='message',
name='created_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_messages', to='members.member', verbose_name='Created by'),
),
]

@ -0,0 +1,17 @@
# Generated by Django 4.0.1 on 2023-04-04 12:35
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('mailer', '0002_message_created_by'),
]
operations = [
migrations.AlterModelOptions(
name='message',
options={'default_permissions': ('add_global', 'change_global', 'view_global', 'delete_global', 'list_global', 'view'), 'permissions': (('submit_mails', 'Can submit mails'),), 'verbose_name': 'message', 'verbose_name_plural': 'messages'},
),
]

@ -9,6 +9,9 @@ from jdav_web.celery import app
from django.core.validators import RegexValidator
from django.conf import settings
from contrib.models import CommonModel
from .rules import is_creator
import os
@ -59,7 +62,7 @@ class EmailAddressForm(forms.ModelForm):
# Create your models here.
class Message(models.Model):
class Message(CommonModel):
"""Represents a message that can be sent to some members"""
subject = models.CharField(_('subject'), max_length=50)
content = models.TextField(_('content'))
@ -88,6 +91,11 @@ class Message(models.Model):
blank=True,
related_name='reply_to_email_addr')
sent = models.BooleanField(_('sent'), default=False)
created_by = models.ForeignKey('members.Member', verbose_name=_('Created by'),
blank=True,
null=True,
on_delete=models.SET_NULL,
related_name='created_messages')
def __str__(self):
return self.subject
@ -175,12 +183,17 @@ class Message(models.Model):
self.save()
return success
class Meta:
class Meta(CommonModel.Meta):
verbose_name = _('message')
verbose_name_plural = _('messages')
permissions = (
("submit_mails", _("Can submit mails")),
)
rules_permissions = {
"view_obj": is_creator,
"change_obj": is_creator,
"delete_obj": is_creator,
}
class MessageForm(forms.ModelForm):
@ -203,7 +216,7 @@ class MessageForm(forms.ModelForm):
raise ValidationError(_('At least one reply-to recipient is required. '
'Use the info mail if you really want no reply-to recipient.'))
class Attachment(models.Model):
class Attachment(CommonModel):
"""Represents an attachment to an email"""
msg = models.ForeignKey(Message, on_delete=models.CASCADE)
# file (not naming it file because of builtin)
@ -218,3 +231,10 @@ class Attachment(models.Model):
class Meta:
verbose_name = _('attachment')
verbose_name_plural = _('attachments')
rules_permissions = {
"add_obj": is_creator,
"view_obj": is_creator,
"change_obj": is_creator,
"delete_obj": is_creator,
}

@ -0,0 +1,10 @@
from contrib.rules import memberize_user
from rules import predicate
@predicate
@memberize_user
def is_creator(self, message):
if message is None:
return False
return message.created_by == self

@ -28,6 +28,8 @@ from django.shortcuts import render
from django.core.exceptions import PermissionDenied
from .pdf import render_tex
from contrib.admin import CommonAdminInlineMixin, CommonAdminMixin
import nested_admin
from .models import (Member, Group, Freizeit, MemberNoteList, NewMemberOnList, Klettertreff,
@ -35,7 +37,7 @@ from .models import (Member, Group, Freizeit, MemberNoteList, NewMemberOnList, K
PermissionGroup, MemberTraining, TrainingCategory,
KlettertreffAttendee, ActivityCategory,
annotate_activity_score, RegistrationPassword, MemberUnconfirmedProxy)
from finance.models import Statement, Bill
from finance.models import Statement, BillOnExcursionProxy
from mailer.mailutils import send as send_mail, get_echo_link
from django.conf import settings
#from easy_select2 import apply_select2
@ -57,7 +59,7 @@ class FilteredMemberFieldMixin:
elif not hasattr(request.user, 'member'):
field.queryset = Member.objects.none()
else:
field.queryset = request.user.member.filter_queryset_by_permissions()
field.queryset = request.user.member.filter_queryset_by_permissions(model=Member)
return field
def formfield_for_manytomany(self, db_field, request=None, **kwargs):
@ -75,7 +77,7 @@ class FilteredMemberFieldMixin:
elif not hasattr(request.user, 'member'):
field.queryset = Member.objects.none()
else:
field.queryset = request.user.member.filter_queryset_by_permissions()
field.queryset = request.user.member.filter_queryset_by_permissions(model=Member)
return field
@ -91,7 +93,7 @@ class PermissionOnMemberInline(admin.StackedInline):
can_delete = False
class TrainingOnMemberInline(admin.TabularInline):
class TrainingOnMemberInline(CommonAdminInlineMixin, admin.TabularInline):
model = MemberTraining
formfield_overrides = {
TextField: {'widget': Textarea(attrs={'rows': 1, 'cols': 40})}
@ -145,7 +147,7 @@ class RegistrationFilter(admin.SimpleListFilter):
# Register your models here.
class MemberAdmin(admin.ModelAdmin):
class MemberAdmin(CommonAdminMixin, admin.ModelAdmin):
fields = ['prename', 'lastname', 'email', 'email_parents', 'cc_email_parents', 'street', 'plz',
'town', 'address_extra', 'country', 'nationality',
'phone_number_private', 'phone_number_mobile',
@ -156,7 +158,8 @@ class MemberAdmin(admin.ModelAdmin):
'medication', 'tetanus_vaccination', 'photos_may_be_taken', 'legal_guardians',
'good_conduct_certificate_presented_date', 'good_conduct_certificate_presentation_needed',
'iban', 'has_key', 'has_free_ticket_gym', 'gets_newsletter', 'registered', 'registration_form',
'active', 'echoed', 'join_date', 'leave_date', 'comments', 'technical_comments']
'active', 'echoed', 'join_date', 'leave_date', 'comments', 'technical_comments',
'user']
list_display = ('name_text_or_link', 'birth_date', 'age', 'get_group', 'gets_newsletter',
'registered', 'active', 'echoed', 'comments', 'activity_score')
search_fields = ('prename', 'lastname', 'email')
@ -174,60 +177,13 @@ class MemberAdmin(admin.ModelAdmin):
sensitive_fields = ['iban', 'registration_form', 'comments']
def has_view_permission(self, request, obj=None):
user = request.user
if request.user.has_perm('members.may_view_everyone'):
return True
if not hasattr(user, 'member'):
return False
if obj is None:
return True
return request.user.member.may_view(obj)
def has_change_permission(self, request, obj=None):
user = request.user
if request.user.has_perm('members.may_change_everyone'):
return True
if not hasattr(user, 'member'):
return False
if obj is None:
return True
return request.user.member.may_change(obj)
def has_delete_permission(self, request, obj=None):
user = request.user
if request.user.has_perm('members.may_delete_everyone'):
return True
if not hasattr(user, 'member'):
return False
if obj is None:
return True
return request.user.member.may_delete(obj)
def get_fields(self, request, obj=None):
if request.user.has_perm('members.may_set_auth_user'):
if 'user' not in self.fields:
self.fields.append('user')
else:
if 'user' in self.fields:
self.fields.remove('user')
return super(MemberAdmin, self).get_fields(request, obj)
field_permissions = {
'user': 'members.may_set_auth_user',
'group': 'members.may_change_group'
}
def get_queryset(self, request):
queryset = super().get_queryset(request)
if request.user.has_perm('members.may_list_everyone'):
return annotate_activity_score(queryset.prefetch_related('group'))
if not hasattr(request.user, 'member'):
return Member.objects.none()
queryset = request.user.member.filter_queryset_by_permissions(queryset, annotate=True)
return annotate_activity_score(queryset.prefetch_related('group'))
def change_view(self, request, object_id, form_url="", extra_context=None):
@ -242,11 +198,6 @@ class MemberAdmin(admin.ModelAdmin):
extra_context=extra_context)
except Member.DoesNotExist:
return super().change_view(request, object_id)
except PermissionDenied:
member = Member.objects.get(pk=object_id)
messages.error(request,
_("You are not allowed to view %(name)s.") % {'name': member.name})
return HttpResponseRedirect(reverse('admin:%s_%s_changelist' % (self.opts.app_label, self.opts.model_name)))
def send_mail_to(self, request, queryset):
member_pks = [m.pk for m in queryset]
@ -297,7 +248,7 @@ class MemberAdmin(admin.ModelAdmin):
class MemberUnconfirmedAdmin(admin.ModelAdmin):
fields = ['prename', 'lastname', 'email', 'email_parents', 'cc_email_parents', 'street', 'plz',
'town', 'phone_number', 'phone_number_parents', 'birth_date', 'group',
'town', 'phone_number_mobile', 'phone_number_private','phone_number_parents', 'birth_date', 'group',
'registered', 'registration_form', 'active', 'comments']
list_display = ('name', 'birth_date', 'age', 'get_group', 'confirmed_mail', 'confirmed_mail_parents')
search_fields = ('prename', 'lastname', 'email')
@ -380,7 +331,7 @@ class WaiterInviteForm(forms.Form):
label=_('Group'))
class MemberWaitingListAdmin(admin.ModelAdmin):
class MemberWaitingListAdmin(CommonAdminMixin, admin.ModelAdmin):
fields = ['prename', 'lastname', 'email', 'email_parents', 'birth_date', 'comments', 'invited_for_group']
list_display = ('name', 'birth_date', 'age', 'confirmed_mail', 'confirmed_mail_parents',
'waiting_confirmed')
@ -501,7 +452,7 @@ class GroupAdminForm(forms.ModelForm):
self.fields['leiters'].queryset = Member.objects.filter(group__name='Jugendleiter')
class GroupAdmin(admin.ModelAdmin):
class GroupAdmin(CommonAdminMixin, admin.ModelAdmin):
fields = ['name', 'year_from', 'year_to', 'leiters']
form = GroupAdminForm
list_display = ('name', 'year_from', 'year_to')
@ -529,13 +480,13 @@ class FreizeitAdminForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(FreizeitAdminForm, self).__init__(*args, **kwargs)
q = self.fields['jugendleiter'].queryset
self.fields['jugendleiter'].queryset = q.filter(group__name='Jugendleiter')
#self.fields['add_member'].queryset = Member.objects.filter(prename__startswith='F')
if 'jugendleiter' in self.fields:
q = self.fields['jugendleiter'].queryset
self.fields['jugendleiter'].queryset = q.filter(group__name='Jugendleiter')
class BillOnStatementInline(FilteredMemberFieldMixin, admin.TabularInline):
model = Bill
class BillOnExcursionInline(FilteredMemberFieldMixin, CommonAdminInlineMixin, admin.TabularInline):
model = BillOnExcursionProxy
extra = 0
sortable_options = []
fields = ['short_description', 'explanation', 'amount', 'paid_by', 'proof']
@ -543,31 +494,16 @@ class BillOnStatementInline(FilteredMemberFieldMixin, admin.TabularInline):
TextField: {'widget': Textarea(attrs={'rows': 1, 'cols': 40})}
}
def get_readonly_fields(self, request, obj=None):
if obj is not None and obj.submitted:
return self.fields
return super(BillOnStatementInline, self).get_readonly_fields(request, obj)
class StatementOnListInline(nested_admin.NestedStackedInline):
class StatementOnListInline(CommonAdminInlineMixin, nested_admin.NestedStackedInline):
model = Statement
extra = 1
sortable_options = []
fields = ['night_cost']
inlines = [BillOnStatementInline]
def get_readonly_fields(self, request, obj=None):
if obj is not None and hasattr(obj, 'statement') and obj.statement.submitted:
return self.fields
return super(StatementOnListInline, self).get_readonly_fields(request, obj)
def has_delete_permission(self, request, obj=None):
if obj is not None and hasattr(obj, 'statement') and obj.statement.submitted:
return False
return True
inlines = [BillOnExcursionInline]
class InterventionOnLJPInline(admin.TabularInline):
class InterventionOnLJPInline(CommonAdminInlineMixin, admin.TabularInline):
model = Intervention
extra = 0
sortable_options = []
@ -576,14 +512,14 @@ class InterventionOnLJPInline(admin.TabularInline):
}
class LJPOnListInline(nested_admin.NestedStackedInline):
class LJPOnListInline(CommonAdminInlineMixin, nested_admin.NestedStackedInline):
model = LJPProposal
extra = 1
sortable_options = []
inlines = [InterventionOnLJPInline]
class MemberOnListInline(FilteredMemberFieldMixin, GenericTabularInline):
class MemberOnListInline(FilteredMemberFieldMixin, CommonAdminInlineMixin, GenericTabularInline):
model = NewMemberOnList
extra = 0
formfield_overrides = {
@ -669,8 +605,8 @@ class MemberNoteListAdmin(admin.ModelAdmin):
generate_summary.short_description = "PDF Übersicht erstellen"
class FreizeitAdmin(FilteredMemberFieldMixin, nested_admin.NestedModelAdmin):
inlines = [MemberOnListInline, LJPOnListInline, StatementOnListInline]
class FreizeitAdmin(CommonAdminMixin, nested_admin.NestedModelAdmin):
#inlines = [MemberOnListInline, LJPOnListInline, StatementOnListInline]
form = FreizeitAdminForm
list_display = ['__str__', 'date']
search_fields = ('name',)
@ -682,6 +618,12 @@ class FreizeitAdmin(FilteredMemberFieldMixin, nested_admin.NestedModelAdmin):
# ForeignKey: {'widget': apply_select2(forms.Select)}
#}
def get_inlines(self, request, obj=None):
if obj:
return [MemberOnListInline, LJPOnListInline, StatementOnListInline]
else:
return [MemberOnListInline]
class Media:
css = {'all': ('admin/css/tabular_hide_original.css',)}
@ -689,23 +631,11 @@ class FreizeitAdmin(FilteredMemberFieldMixin, nested_admin.NestedModelAdmin):
super(FreizeitAdmin, self).__init__(*args, **kwargs)
def save_model(self, request, obj, form, change):
print("saving model")
if not change and hasattr(request.user, 'member') and hasattr(obj, 'statement'):
print("setting obj statement created")
obj.statement.created_by = request.user.member
obj.statement.save()
super().save_model(request, obj, form, change)
def get_queryset(self, request):
queryset = super().get_queryset(request)
if request.user.has_perm('members.may_list_all_excursions'):
return queryset
if not hasattr(request.user, 'member'):
return Member.objects.none()
return Freizeit.filter_queryset_by_permissions(request.user.member, queryset)
def may_view_excursion(self, request, memberlist):
return request.user.has_perm('members.may_view_everyone') or \
( hasattr(request.user, 'member') and \

@ -0,0 +1,25 @@
# Generated by Django 4.0.1 on 2023-04-03 20:33
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('members', '0006_rename_permissions'),
]
operations = [
migrations.AlterModelOptions(
name='freizeit',
options={'verbose_name': 'Freizeit', 'verbose_name_plural': 'Freizeiten'},
),
migrations.AlterModelOptions(
name='member',
options={'permissions': (('may_see_qualities', 'Is allowed to see the quality overview'), ('may_set_auth_user', 'Is allowed to set auth user member connections.'), ('change_member_group', 'Can change the group field')), 'verbose_name': 'member', 'verbose_name_plural': 'members'},
),
migrations.AlterModelOptions(
name='membertraining',
options={'verbose_name': 'Training', 'verbose_name_plural': 'Trainings'},
),
]

@ -0,0 +1,38 @@
# Generated by Django 4.0.1 on 2023-04-03 21:20
from django.db import migrations
import django.db.migrations.operations.special
class Migration(migrations.Migration):
dependencies = [
('members', '0007_alter_permission_options'),
]
operations = [
migrations.AlterModelOptions(
name='member',
options={'default_permissions': ('add_global', 'change_global', 'delete_global', 'view_global'), 'permissions': (('may_see_qualities', 'Is allowed to see the quality overview'), ('may_set_auth_user', 'Is allowed to set auth user member connections.'), ('change_member_group', 'Can change the group field')), 'verbose_name': 'member', 'verbose_name_plural': 'members'},
),
migrations.AlterModelOptions(
name='freizeit',
options={'default_permissions': ('add_global', 'change_global', 'view_global', 'delete_global', 'list_global'), 'verbose_name': 'Freizeit', 'verbose_name_plural': 'Freizeiten'},
),
migrations.AlterModelOptions(
name='ljpproposal',
options={'default_permissions': ('add_global', 'change_global', 'view_global', 'delete_global', 'list_global'), 'verbose_name': 'LJP Proposal', 'verbose_name_plural': 'LJP Proposals'},
),
migrations.AlterModelOptions(
name='membertraining',
options={'default_permissions': ('add_global', 'change_global', 'view_global', 'delete_global', 'list_global'), 'verbose_name': 'Training', 'verbose_name_plural': 'Trainings'},
),
migrations.AlterModelOptions(
name='newmemberonlist',
options={'default_permissions': ('add_global', 'change_global', 'view_global', 'delete_global', 'list_global'), 'verbose_name': 'Member', 'verbose_name_plural': 'Members'},
),
migrations.AlterModelOptions(
name='member',
options={'default_permissions': ('add_global', 'change_global', 'view_global', 'delete_global', 'list_global'), 'permissions': (('may_see_qualities', 'Is allowed to see the quality overview'), ('may_set_auth_user', 'Is allowed to set auth user member connections.'), ('change_member_group', 'Can change the group field')), 'verbose_name': 'member', 'verbose_name_plural': 'members'},
),
]

@ -0,0 +1,37 @@
# Generated by Django 4.0.1 on 2023-04-04 12:20
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('members', '0008_change_default_permissions'),
]
operations = [
migrations.AlterModelOptions(
name='freizeit',
options={'default_permissions': ('add_global', 'change_global', 'view_global', 'delete_global', 'list_global', 'view'), 'verbose_name': 'Freizeit', 'verbose_name_plural': 'Freizeiten'},
),
migrations.AlterModelOptions(
name='ljpproposal',
options={'default_permissions': ('add_global', 'change_global', 'view_global', 'delete_global', 'list_global', 'view'), 'verbose_name': 'LJP Proposal', 'verbose_name_plural': 'LJP Proposals'},
),
migrations.AlterModelOptions(
name='member',
options={'default_permissions': ('add_global', 'change_global', 'view_global', 'delete_global', 'list_global', 'view'), 'permissions': (('may_see_qualities', 'Is allowed to see the quality overview'), ('may_set_auth_user', 'Is allowed to set auth user member connections.'), ('change_member_group', 'Can change the group field')), 'verbose_name': 'member', 'verbose_name_plural': 'members'},
),
migrations.AlterModelOptions(
name='membertraining',
options={'default_permissions': ('add_global', 'change_global', 'view_global', 'delete_global', 'list_global', 'view'), 'verbose_name': 'Training', 'verbose_name_plural': 'Trainings'},
),
migrations.AlterModelOptions(
name='memberwaitinglist',
options={'default_permissions': ('add_global', 'change_global', 'view_global', 'delete_global', 'list_global', 'view'), 'permissions': (('may_manage_waiting_list', 'Can view and manage the waiting list.'),), 'verbose_name': 'Waiter', 'verbose_name_plural': 'Waiters'},
),
migrations.AlterModelOptions(
name='newmemberonlist',
options={'default_permissions': ('add_global', 'change_global', 'view_global', 'delete_global', 'list_global', 'view'), 'verbose_name': 'Member', 'verbose_name_plural': 'Members'},
),
]

@ -18,8 +18,12 @@ from django.contrib.auth.models import User
from django.conf import settings
from django.core.validators import MinValueValidator
from dateutil.relativedelta import relativedelta
from .rules import may_view, may_change, may_delete, is_own_training, is_oneself, is_leader, is_leader_of_excursion
import rules
from contrib.models import CommonModel
from contrib.rules import memberize_user, has_global_perm
from dateutil.relativedelta import relativedelta
def generate_random_key():
return uuid.uuid4().hex
@ -29,6 +33,7 @@ GEMEINSCHAFTS_TOUR = MUSKELKRAFT_ANREISE = 0
FUEHRUNGS_TOUR = OEFFENTLICHE_ANREISE = 1
AUSBILDUNGS_TOUR = FAHRGEMEINSCHAFT_ANREISE = 2
class ActivityCategory(models.Model):
"""
Describes one kind of activity
@ -69,7 +74,7 @@ class MemberManager(models.Manager):
return super().get_queryset().filter(confirmed=True)
class Person(models.Model):
class Person(CommonModel):
"""
Represents an abstract person. Not necessarily a member of any group.
"""
@ -89,7 +94,7 @@ class Person(models.Model):
confirm_mail_key = models.CharField(max_length=32, default="")
confirm_mail_parents_key = models.CharField(max_length=32, default="")
class Meta:
class Meta(CommonModel.Meta):
abstract = True
def __str__(self):
@ -123,9 +128,9 @@ class Person(models.Model):
self.confirmed_mail_parents = False
self.confirm_mail_parents_key = uuid.uuid4().hex
send_mail(_('Email confirmation needed'),
CONFIRM_MAIL_TEXT.format(name=self.prename,
link=get_mail_confirmation_link(self.confirm_mail_parents_key),
whattoconfirm='der Emailadresse deiner Eltern'),
settings.CONFIRM_MAIL_TEXT.format(name=self.prename,
link=get_mail_confirmation_link(self.confirm_mail_parents_key),
whattoconfirm='der Emailadresse deiner Eltern'),
settings.DEFAULT_SENDING_MAIL,
self.email_parents)
else:
@ -293,11 +298,21 @@ class Member(Person):
return groupstring
get_group.short_description = _('Group')
class Meta:
class Meta(CommonModel.Meta):
verbose_name = _('member')
verbose_name_plural = _('members')
permissions = (('may_see_qualities', 'Is allowed to see the quality overview'),
('may_set_auth_user', 'Is allowed to set auth user member connections.'))
permissions = (
('may_see_qualities', 'Is allowed to see the quality overview'),
('may_set_auth_user', 'Is allowed to set auth user member connections.'),
('change_member_group', 'Can change the group field'),
)
rules_permissions = {
'members': rules.always_allow,
'add_obj': has_global_perm('members.add_global_member'),
'view_obj': may_view | has_global_perm('members.view_global_member'),
'change_obj': may_change | has_global_perm('members.change_global_member'),
'delete_obj': may_delete | has_global_perm('members.delete_global_member'),
}
def get_skills(self):
# get skills by summing up all the activities taken part in
@ -333,13 +348,51 @@ class Member(Person):
settings.DEFAULT_SENDING_MAIL,
jl.email)
def filter_queryset_by_permissions(self, queryset=None, annotate=False):
def filter_queryset_by_permissions(self, queryset=None, annotate=False, model=None):
name = model._meta.object_name
if queryset is None:
queryset = Member.objects.all()
# every member may list themself
if name == "Message":
return self.filter_messages_by_permissions(queryset, annotate)
elif name == "Member":
return self.filter_members_by_permissions(queryset, annotate)
elif name == "StatementUnSubmitted":
return self.filter_statements_by_permissions(queryset, annotate)
elif name == "Freizeit":
return self.filter_excursions_by_permissions(queryset, annotate)
elif name == "LJPProposal":
return queryset
elif name == "MemberTraining":
return queryset
elif name == "NewMemberOnList":
return queryset
elif name == "Statement":
return queryset
elif name == "BillOnExcursionProxy":
return queryset
elif name == "Intervention":
return queryset
elif name == "BillOnStatementProxy":
return queryset
elif name == "Attachment":
return queryset
elif name == "Group":
return queryset
else:
raise ValueError(name)
def filter_members_by_permissions(self, queryset, annotate=False):
#mems = Member.objects.all().prefetch_related('group')
#list_pks = [ m.pk for m in mems if self.may_list(m) ]
#view_pks = [ m.pk for m in mems if self.may_view(m) ]
## every member may list themself
pks = [self.pk]
view_pks = [self.pk]
if hasattr(self, 'permissions'):
pks += [ m.pk for m in self.permissions.list_members.all() ]
view_pks += [ m.pk for m in self.permissions.view_members.all() ]
@ -367,6 +420,21 @@ class Member(Person):
return filtered.annotate(_viewable=Case(When(pk__in=view_pks, then=Value(True)), default=Value(False), output_field=models.BooleanField()))
def filter_messages_by_permissions(self, queryset, annotate=False):
# ignores annotate
return queryset.filter(created_by=self)
def filter_statements_by_permissions(self, queryset, annotate=False):
# ignores annotate
return queryset.filter(Q(created_by=self) | Q(excursion__jugendleiter=self))
def filter_excursions_by_permissions(self, queryset, annotate=False):
# ignores annotate
groups = self.leited_groups.all()
# one may view all excursions by leited groups and leited excursions
queryset = queryset.filter(Q(groups__in=groups) | Q(jugendleiter=self)).distinct()
return queryset
def may_list(self, other):
if self.pk == other.pk:
return True
@ -493,10 +561,16 @@ class MemberWaitingList(Person):
default=None,
verbose_name=_('Invited for group'),
on_delete=models.SET_NULL)
class Meta:
class Meta(CommonModel.Meta):
verbose_name = _('Waiter')
verbose_name_plural = _('Waiters')
permissions = (('may_manage_waiting_list', 'Can view and manage the waiting list.'),)
rules_permissions = {
'add_obj': has_global_perm('members.add_global_memberwaitinglist'),
'view_obj': has_global_perm('members.view_global_memberwaitinglist'),
'change_obj': has_global_perm('members.change_global_memberwaitinglist'),
'delete_obj': has_global_perm('members.delete_global_memberwaitinglist'),
}
@property
def waiting_confirmation_needed(self):
@ -565,7 +639,7 @@ class MemberWaitingList(Person):
else self.email)
class NewMemberOnList(models.Model):
class NewMemberOnList(CommonModel):
"""
Connects members to a list of members.
"""
@ -579,9 +653,15 @@ class NewMemberOnList(models.Model):
def __str__(self):
return str(self.member)
class Meta:
class Meta(CommonModel.Meta):
verbose_name = _('Member')
verbose_name_plural = _('Members')
rules_permissions = {
'add_obj': is_leader,
'view_obj': is_leader | has_global_perm('members.view_global_freizeit'),
'change_obj': is_leader,
'delete_obj': is_leader,
}
@property
def comments_tex(self):
@ -604,7 +684,7 @@ class NewMemberOnList(models.Model):
return ", ".join(qualities)
class Freizeit(models.Model):
class Freizeit(CommonModel):
"""Lets the user create a 'Freizeit' and generate a members overview in pdf format. """
name = models.CharField(verbose_name=_('Activity'), default='',
@ -641,9 +721,15 @@ class Freizeit(models.Model):
"""String represenation"""
return self.name
class Meta:
class Meta(CommonModel.Meta):
verbose_name = "Freizeit"
verbose_name_plural = "Freizeiten"
rules_permissions = {
'add_obj': has_global_perm('members.add_global_freizeit'),
'view_obj': is_leader | has_global_perm('members.view_global_freizeit'),
'change_obj': is_leader | has_global_perm('members.change_global_freizeit'),
'delete_obj': is_leader | has_global_perm('members.delete_global_freizeit'),
}
def get_tour_type(self):
if self.tour_type == FUEHRUNGS_TOUR:
@ -827,7 +913,7 @@ class RegistrationPassword(models.Model):
verbose_name_plural = _('registration passwords')
class LJPProposal(models.Model):
class LJPProposal(CommonModel):
"""A proposal for LJP"""
title = models.CharField(verbose_name=_('Title'), max_length=30)
@ -843,14 +929,20 @@ class LJPProposal(models.Model):
null=True,
on_delete=models.SET_NULL)
class Meta:
class Meta(CommonModel.Meta):
verbose_name = _('LJP Proposal')
verbose_name_plural = _('LJP Proposals')
rules_permissions = {
'add_obj': is_leader,
'view_obj': is_leader | has_global_perm('members.view_global_freizeit'),
'change_obj': is_leader,
'delete_obj': is_leader,
}
def __str__(self):
return self.title
class Intervention(models.Model):
class Intervention(CommonModel):
"""An intervention during a seminar as part of a LJP proposal"""
date_start = models.DateTimeField(verbose_name=_('Starting time'))
duration = models.DecimalField(verbose_name=_('Duration in hours'),
@ -866,6 +958,12 @@ class Intervention(models.Model):
class Meta:
verbose_name = _('Intervention')
verbose_name_plural = _('Interventions')
rules_permissions = {
'add_obj': is_leader_of_excursion,
'view_obj': is_leader_of_excursion | has_global_perm('members.view_global_freizeit'),
'change_obj': is_leader_of_excursion,
'delete_obj': is_leader_of_excursion,
}
def annotate_activity_score(queryset):
@ -1030,7 +1128,7 @@ class TrainingCategory(models.Model):
return self.name
class MemberTraining(models.Model):
class MemberTraining(CommonModel):
"""Represents a training planned or attended by a member."""
member = models.ForeignKey(Member, on_delete=models.CASCADE, related_name='traininigs')
title = models.CharField(verbose_name=_('Title'), max_length=30)
@ -1040,9 +1138,17 @@ class MemberTraining(models.Model):
participated = models.BooleanField(verbose_name=_('Participated'))
passed = models.BooleanField(verbose_name=_('Passed'))
class Meta:
class Meta(CommonModel.Meta):
verbose_name = _('Training')
verbose_name_plural = _('Trainings')
rules_permissions = {
# sine this is used in an inline, the member and not the training is passed
'add_obj': is_oneself | has_global_perm('members.add_global_membertraining'),
'view_obj': is_oneself | has_global_perm('members.view_global_membertraining'),
'change_obj': is_oneself | has_global_perm('members.change_global_membertraining'),
'delete_obj': is_oneself | has_global_perm('members.delete_global_membertraining'),
}
def import_from_csv(path):
@ -1179,3 +1285,4 @@ CLUBDESK_TO_KOMPASS = {
'Erziehungsberechtigte': 'legal_guardians',
'Mobil Eltern': 'phone_number_parents',
}

@ -0,0 +1,75 @@
from contrib.rules import memberize_user
from rules import predicate
@predicate
@memberize_user
def is_oneself(self, other):
assert other is not None
return self.pk == other.pk
@predicate
@memberize_user
def may_view(self, other):
assert other is not None
return self.may_view(other)
@predicate
@memberize_user
def may_change(self, other):
assert other is not None
return self.may_change(other)
@predicate
@memberize_user
def may_delete(self, other):
assert other is not None
return self.may_delete(other)
@predicate
@memberize_user
def is_own_training(self, training):
assert training is not None
return training.member == self
@predicate
@memberize_user
def is_leader_of_excursion(self, ljpproposal):
assert ljpproposal is not None
if not hasattr(ljpproposal, 'excursion'):
return _is_leader(self, ljpproposal)
return _is_leader(self, ljpproposal.excursion)
@predicate
@memberize_user
def is_leader(self, excursion):
assert excursion is not None
return _is_leader(self, excursion)
def _is_leader(member, excursion):
if not hasattr(member, 'pk'):
return False
if member.pk is None:
return False
if member in excursion.jugendleiter.all():
return True
yl = [ yl for group in member.group.all() for yl in group.leiters.all() ]
return member in yl
@predicate
@memberize_user
def statement_not_submitted(self, excursion):
assert excursion is not None
if not hasattr(excursion, 'statement'):
return False
if excursion.statement is None:
return False
return not excursion.statement.submitted
Loading…
Cancel
Save