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.shortcuts import render
from django.conf import settings 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,\ from .models import Ledger, Statement, Receipt, Transaction, Bill, StatementSubmitted, StatementConfirmed,\
StatementUnSubmitted StatementUnSubmitted, BillOnStatementProxy
@admin.register(Ledger) @admin.register(Ledger)
@ -17,8 +21,8 @@ class LedgerAdmin(admin.ModelAdmin):
search_fields = ('name', ) search_fields = ('name', )
class BillOnStatementInline(admin.TabularInline): class BillOnStatementInline(CommonAdminInlineMixin, admin.TabularInline):
model = Bill model = BillOnStatementProxy
extra = 0 extra = 0
sortable_options = [] sortable_options = []
fields = ['short_description', 'explanation', 'amount', 'paid_by', 'proof'] fields = ['short_description', 'explanation', 'amount', 'paid_by', 'proof']
@ -26,16 +30,11 @@ class BillOnStatementInline(admin.TabularInline):
TextField: {'widget': Textarea(attrs={'rows': 1, 'cols': 40})} 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) @admin.register(StatementUnSubmitted)
class StatementUnSubmittedAdmin(admin.ModelAdmin): class StatementUnSubmittedAdmin(CommonAdminMixin, admin.ModelAdmin):
fields = ['short_description', 'explanation', 'excursion', 'submitted'] fields = ['short_description', 'explanation', 'excursion', 'submitted']
list_display = ['__str__', 'excursion'] list_display = ['__str__', 'excursion', 'created_by']
inlines = [BillOnStatementInline] inlines = [BillOnStatementInline]
def save_model(self, request, obj, form, change): def save_model(self, request, obj, form, change):
@ -43,16 +42,6 @@ class StatementUnSubmittedAdmin(admin.ModelAdmin):
obj.created_by = request.user.member obj.created_by = request.user.member
super().save_model(request, obj, form, change) 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): def get_readonly_fields(self, request, obj=None):
readonly_fields = ['submitted', 'excursion'] readonly_fields = ['submitted', 'excursion']
if obj is not None and obj.submitted: if obj is not None and obj.submitted:
@ -109,7 +98,7 @@ class TransactionOnSubmittedStatementInline(admin.TabularInline):
class BillOnSubmittedStatementInline(BillOnStatementInline): class BillOnSubmittedStatementInline(BillOnStatementInline):
model = Bill model = BillOnStatementProxy
extra = 0 extra = 0
sortable_options = [] sortable_options = []
fields = ['short_description', 'explanation', 'amount', 'paid_by', 'proof', 'costs_covered'] 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 itertools import groupby
from decimal import Decimal, ROUND_HALF_DOWN from decimal import Decimal, ROUND_HALF_DOWN
from django.utils import timezone 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.db import models
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from members.models import Member, Freizeit, OEFFENTLICHE_ANREISE, MUSKELKRAFT_ANREISE from members.models import Member, Freizeit, OEFFENTLICHE_ANREISE, MUSKELKRAFT_ANREISE
from django.conf import settings from django.conf import settings
import rules
from contrib.models import CommonModel
from contrib.rules import has_global_perm
# Create your models here. # Create your models here.
@ -35,7 +40,7 @@ class StatementManager(models.Manager):
return super().get_queryset().filter(submitted=False, confirmed=False) 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 MISSING_LEDGER, NON_MATCHING_TRANSACTIONS, VALID = 0, 1, 2
short_description = models.CharField(verbose_name=_('Short description'), short_description = models.CharField(verbose_name=_('Short description'),
@ -71,10 +76,20 @@ class Statement(models.Model):
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
related_name='confirmed_statements') related_name='confirmed_statements')
class Meta: class Meta(CommonModel.Meta):
verbose_name = _('Statement') verbose_name = _('Statement')
verbose_name_plural = _('Statements') 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): def __str__(self):
if self.excursion is not None: if self.excursion is not None:
@ -306,10 +321,16 @@ class StatementUnSubmittedManager(models.Manager):
class StatementUnSubmitted(Statement): class StatementUnSubmitted(Statement):
objects = StatementUnSubmittedManager() objects = StatementUnSubmittedManager()
class Meta: class Meta(CommonModel.Meta):
proxy = True proxy = True
verbose_name = _('Statement in preparation') verbose_name = _('Statement in preparation')
verbose_name_plural = _('Statements 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): class StatementSubmittedManager(models.Manager):
@ -320,11 +341,13 @@ class StatementSubmittedManager(models.Manager):
class StatementSubmitted(Statement): class StatementSubmitted(Statement):
objects = StatementSubmittedManager() objects = StatementSubmittedManager()
class Meta: class Meta(CommonModel.Meta):
proxy = True proxy = True
verbose_name = _('Submitted statement') verbose_name = _('Submitted statement')
verbose_name_plural = _('Submitted statements') 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): class StatementConfirmedManager(models.Manager):
@ -335,14 +358,16 @@ class StatementConfirmedManager(models.Manager):
class StatementConfirmed(Statement): class StatementConfirmed(Statement):
objects = StatementConfirmedManager() objects = StatementConfirmedManager()
class Meta: class Meta(CommonModel.Meta):
proxy = True proxy = True
verbose_name = _('Paid statement') verbose_name = _('Paid statement')
verbose_name_plural = _('Paid statements') 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) statement = models.ForeignKey(Statement, verbose_name=_('Statement'), on_delete=models.CASCADE)
short_description = models.CharField(verbose_name=_('Short description'), max_length=30) short_description = models.CharField(verbose_name=_('Short description'), max_length=30)
explanation = models.TextField(verbose_name=_('Explanation'), blank=True) explanation = models.TextField(verbose_name=_('Explanation'), blank=True)
@ -363,9 +388,37 @@ class Bill(models.Model):
pretty_amount.admin_order_field = 'amount' pretty_amount.admin_order_field = 'amount'
pretty_amount.short_description = _('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 = _('Bill')
verbose_name_plural = _('Bills') 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): 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 #from easy_select2 import apply_select2
import json import json
from rules.contrib.admin import ObjectPermissionsModelAdmin
from .models import Message, Attachment, MessageForm, EmailAddress, EmailAddressForm from .models import Message, Attachment, MessageForm, EmailAddress, EmailAddressForm
from .mailutils import NOT_SENT, PARTLY_SENT from .mailutils import NOT_SENT, PARTLY_SENT
from members.models import Member from members.models import Member
from contrib.admin import CommonAdminMixin, CommonAdminInlineMixin
class AttachmentInline(admin.TabularInline): class AttachmentInline(CommonAdminInlineMixin, admin.TabularInline):
model = Attachment model = Attachment
extra = 0 extra = 0
@ -27,8 +30,9 @@ class EmailAddressAdmin(admin.ModelAdmin):
form = EmailAddressForm form = EmailAddressForm
class MessageAdmin(admin.ModelAdmin): class MessageAdmin(CommonAdminMixin, ObjectPermissionsModelAdmin):
"""Message creation view""" """Message creation view"""
exclude = ('created_by',)
list_display = ('subject', 'get_recipients', 'sent') list_display = ('subject', 'get_recipients', 'sent')
search_fields = ('subject',) search_fields = ('subject',)
change_form_template = "mailer/change_form.html" change_form_template = "mailer/change_form.html"
@ -42,6 +46,11 @@ class MessageAdmin(admin.ModelAdmin):
form = MessageForm form = MessageForm
filter_horizontal = ('to_members','reply_to') 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): def send_message(self, request, queryset):
if request.POST.get('confirmed'): if request.POST.get('confirmed'):
for msg in queryset: 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.core.validators import RegexValidator
from django.conf import settings from django.conf import settings
from contrib.models import CommonModel
from .rules import is_creator
import os import os
@ -59,7 +62,7 @@ class EmailAddressForm(forms.ModelForm):
# Create your models here. # Create your models here.
class Message(models.Model): class Message(CommonModel):
"""Represents a message that can be sent to some members""" """Represents a message that can be sent to some members"""
subject = models.CharField(_('subject'), max_length=50) subject = models.CharField(_('subject'), max_length=50)
content = models.TextField(_('content')) content = models.TextField(_('content'))
@ -88,6 +91,11 @@ class Message(models.Model):
blank=True, blank=True,
related_name='reply_to_email_addr') related_name='reply_to_email_addr')
sent = models.BooleanField(_('sent'), default=False) 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): def __str__(self):
return self.subject return self.subject
@ -175,12 +183,17 @@ class Message(models.Model):
self.save() self.save()
return success return success
class Meta: class Meta(CommonModel.Meta):
verbose_name = _('message') verbose_name = _('message')
verbose_name_plural = _('messages') verbose_name_plural = _('messages')
permissions = ( permissions = (
("submit_mails", _("Can submit mails")), ("submit_mails", _("Can submit mails")),
) )
rules_permissions = {
"view_obj": is_creator,
"change_obj": is_creator,
"delete_obj": is_creator,
}
class MessageForm(forms.ModelForm): class MessageForm(forms.ModelForm):
@ -203,7 +216,7 @@ class MessageForm(forms.ModelForm):
raise ValidationError(_('At least one reply-to recipient is required. ' raise ValidationError(_('At least one reply-to recipient is required. '
'Use the info mail if you really want no reply-to recipient.')) '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""" """Represents an attachment to an email"""
msg = models.ForeignKey(Message, on_delete=models.CASCADE) msg = models.ForeignKey(Message, on_delete=models.CASCADE)
# file (not naming it file because of builtin) # file (not naming it file because of builtin)
@ -218,3 +231,10 @@ class Attachment(models.Model):
class Meta: class Meta:
verbose_name = _('attachment') verbose_name = _('attachment')
verbose_name_plural = _('attachments') 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 django.core.exceptions import PermissionDenied
from .pdf import render_tex from .pdf import render_tex
from contrib.admin import CommonAdminInlineMixin, CommonAdminMixin
import nested_admin import nested_admin
from .models import (Member, Group, Freizeit, MemberNoteList, NewMemberOnList, Klettertreff, 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, PermissionGroup, MemberTraining, TrainingCategory,
KlettertreffAttendee, ActivityCategory, KlettertreffAttendee, ActivityCategory,
annotate_activity_score, RegistrationPassword, MemberUnconfirmedProxy) 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 mailer.mailutils import send as send_mail, get_echo_link
from django.conf import settings from django.conf import settings
#from easy_select2 import apply_select2 #from easy_select2 import apply_select2
@ -57,7 +59,7 @@ class FilteredMemberFieldMixin:
elif not hasattr(request.user, 'member'): elif not hasattr(request.user, 'member'):
field.queryset = Member.objects.none() field.queryset = Member.objects.none()
else: else:
field.queryset = request.user.member.filter_queryset_by_permissions() field.queryset = request.user.member.filter_queryset_by_permissions(model=Member)
return field return field
def formfield_for_manytomany(self, db_field, request=None, **kwargs): def formfield_for_manytomany(self, db_field, request=None, **kwargs):
@ -75,7 +77,7 @@ class FilteredMemberFieldMixin:
elif not hasattr(request.user, 'member'): elif not hasattr(request.user, 'member'):
field.queryset = Member.objects.none() field.queryset = Member.objects.none()
else: else:
field.queryset = request.user.member.filter_queryset_by_permissions() field.queryset = request.user.member.filter_queryset_by_permissions(model=Member)
return field return field
@ -91,7 +93,7 @@ class PermissionOnMemberInline(admin.StackedInline):
can_delete = False can_delete = False
class TrainingOnMemberInline(admin.TabularInline): class TrainingOnMemberInline(CommonAdminInlineMixin, admin.TabularInline):
model = MemberTraining model = MemberTraining
formfield_overrides = { formfield_overrides = {
TextField: {'widget': Textarea(attrs={'rows': 1, 'cols': 40})} TextField: {'widget': Textarea(attrs={'rows': 1, 'cols': 40})}
@ -145,7 +147,7 @@ class RegistrationFilter(admin.SimpleListFilter):
# Register your models here. # Register your models here.
class MemberAdmin(admin.ModelAdmin): class MemberAdmin(CommonAdminMixin, admin.ModelAdmin):
fields = ['prename', 'lastname', 'email', 'email_parents', 'cc_email_parents', 'street', 'plz', fields = ['prename', 'lastname', 'email', 'email_parents', 'cc_email_parents', 'street', 'plz',
'town', 'address_extra', 'country', 'nationality', 'town', 'address_extra', 'country', 'nationality',
'phone_number_private', 'phone_number_mobile', 'phone_number_private', 'phone_number_mobile',
@ -156,7 +158,8 @@ class MemberAdmin(admin.ModelAdmin):
'medication', 'tetanus_vaccination', 'photos_may_be_taken', 'legal_guardians', 'medication', 'tetanus_vaccination', 'photos_may_be_taken', 'legal_guardians',
'good_conduct_certificate_presented_date', 'good_conduct_certificate_presentation_needed', 'good_conduct_certificate_presented_date', 'good_conduct_certificate_presentation_needed',
'iban', 'has_key', 'has_free_ticket_gym', 'gets_newsletter', 'registered', 'registration_form', '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', list_display = ('name_text_or_link', 'birth_date', 'age', 'get_group', 'gets_newsletter',
'registered', 'active', 'echoed', 'comments', 'activity_score') 'registered', 'active', 'echoed', 'comments', 'activity_score')
search_fields = ('prename', 'lastname', 'email') search_fields = ('prename', 'lastname', 'email')
@ -174,60 +177,13 @@ class MemberAdmin(admin.ModelAdmin):
sensitive_fields = ['iban', 'registration_form', 'comments'] sensitive_fields = ['iban', 'registration_form', 'comments']
def has_view_permission(self, request, obj=None): field_permissions = {
user = request.user 'user': 'members.may_set_auth_user',
if request.user.has_perm('members.may_view_everyone'): 'group': 'members.may_change_group'
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)
def get_queryset(self, request): def get_queryset(self, request):
queryset = super().get_queryset(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')) return annotate_activity_score(queryset.prefetch_related('group'))
def change_view(self, request, object_id, form_url="", extra_context=None): def change_view(self, request, object_id, form_url="", extra_context=None):
@ -242,11 +198,6 @@ class MemberAdmin(admin.ModelAdmin):
extra_context=extra_context) extra_context=extra_context)
except Member.DoesNotExist: except Member.DoesNotExist:
return super().change_view(request, object_id) 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): def send_mail_to(self, request, queryset):
member_pks = [m.pk for m in queryset] member_pks = [m.pk for m in queryset]
@ -297,7 +248,7 @@ class MemberAdmin(admin.ModelAdmin):
class MemberUnconfirmedAdmin(admin.ModelAdmin): class MemberUnconfirmedAdmin(admin.ModelAdmin):
fields = ['prename', 'lastname', 'email', 'email_parents', 'cc_email_parents', 'street', 'plz', 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'] 'registered', 'registration_form', 'active', 'comments']
list_display = ('name', 'birth_date', 'age', 'get_group', 'confirmed_mail', 'confirmed_mail_parents') list_display = ('name', 'birth_date', 'age', 'get_group', 'confirmed_mail', 'confirmed_mail_parents')
search_fields = ('prename', 'lastname', 'email') search_fields = ('prename', 'lastname', 'email')
@ -380,7 +331,7 @@ class WaiterInviteForm(forms.Form):
label=_('Group')) label=_('Group'))
class MemberWaitingListAdmin(admin.ModelAdmin): class MemberWaitingListAdmin(CommonAdminMixin, admin.ModelAdmin):
fields = ['prename', 'lastname', 'email', 'email_parents', 'birth_date', 'comments', 'invited_for_group'] fields = ['prename', 'lastname', 'email', 'email_parents', 'birth_date', 'comments', 'invited_for_group']
list_display = ('name', 'birth_date', 'age', 'confirmed_mail', 'confirmed_mail_parents', list_display = ('name', 'birth_date', 'age', 'confirmed_mail', 'confirmed_mail_parents',
'waiting_confirmed') 'waiting_confirmed')
@ -501,7 +452,7 @@ class GroupAdminForm(forms.ModelForm):
self.fields['leiters'].queryset = Member.objects.filter(group__name='Jugendleiter') 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'] fields = ['name', 'year_from', 'year_to', 'leiters']
form = GroupAdminForm form = GroupAdminForm
list_display = ('name', 'year_from', 'year_to') list_display = ('name', 'year_from', 'year_to')
@ -529,13 +480,13 @@ class FreizeitAdminForm(forms.ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(FreizeitAdminForm, self).__init__(*args, **kwargs) super(FreizeitAdminForm, self).__init__(*args, **kwargs)
if 'jugendleiter' in self.fields:
q = self.fields['jugendleiter'].queryset q = self.fields['jugendleiter'].queryset
self.fields['jugendleiter'].queryset = q.filter(group__name='Jugendleiter') self.fields['jugendleiter'].queryset = q.filter(group__name='Jugendleiter')
#self.fields['add_member'].queryset = Member.objects.filter(prename__startswith='F')
class BillOnStatementInline(FilteredMemberFieldMixin, admin.TabularInline): class BillOnExcursionInline(FilteredMemberFieldMixin, CommonAdminInlineMixin, admin.TabularInline):
model = Bill model = BillOnExcursionProxy
extra = 0 extra = 0
sortable_options = [] sortable_options = []
fields = ['short_description', 'explanation', 'amount', 'paid_by', 'proof'] 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})} 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 model = Statement
extra = 1 extra = 1
sortable_options = [] sortable_options = []
fields = ['night_cost'] fields = ['night_cost']
inlines = [BillOnStatementInline] inlines = [BillOnExcursionInline]
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): class InterventionOnLJPInline(CommonAdminInlineMixin, admin.TabularInline):
if obj is not None and hasattr(obj, 'statement') and obj.statement.submitted:
return False
return True
class InterventionOnLJPInline(admin.TabularInline):
model = Intervention model = Intervention
extra = 0 extra = 0
sortable_options = [] sortable_options = []
@ -576,14 +512,14 @@ class InterventionOnLJPInline(admin.TabularInline):
} }
class LJPOnListInline(nested_admin.NestedStackedInline): class LJPOnListInline(CommonAdminInlineMixin, nested_admin.NestedStackedInline):
model = LJPProposal model = LJPProposal
extra = 1 extra = 1
sortable_options = [] sortable_options = []
inlines = [InterventionOnLJPInline] inlines = [InterventionOnLJPInline]
class MemberOnListInline(FilteredMemberFieldMixin, GenericTabularInline): class MemberOnListInline(FilteredMemberFieldMixin, CommonAdminInlineMixin, GenericTabularInline):
model = NewMemberOnList model = NewMemberOnList
extra = 0 extra = 0
formfield_overrides = { formfield_overrides = {
@ -669,8 +605,8 @@ class MemberNoteListAdmin(admin.ModelAdmin):
generate_summary.short_description = "PDF Übersicht erstellen" generate_summary.short_description = "PDF Übersicht erstellen"
class FreizeitAdmin(FilteredMemberFieldMixin, nested_admin.NestedModelAdmin): class FreizeitAdmin(CommonAdminMixin, nested_admin.NestedModelAdmin):
inlines = [MemberOnListInline, LJPOnListInline, StatementOnListInline] #inlines = [MemberOnListInline, LJPOnListInline, StatementOnListInline]
form = FreizeitAdminForm form = FreizeitAdminForm
list_display = ['__str__', 'date'] list_display = ['__str__', 'date']
search_fields = ('name',) search_fields = ('name',)
@ -682,6 +618,12 @@ class FreizeitAdmin(FilteredMemberFieldMixin, nested_admin.NestedModelAdmin):
# ForeignKey: {'widget': apply_select2(forms.Select)} # ForeignKey: {'widget': apply_select2(forms.Select)}
#} #}
def get_inlines(self, request, obj=None):
if obj:
return [MemberOnListInline, LJPOnListInline, StatementOnListInline]
else:
return [MemberOnListInline]
class Media: class Media:
css = {'all': ('admin/css/tabular_hide_original.css',)} css = {'all': ('admin/css/tabular_hide_original.css',)}
@ -689,23 +631,11 @@ class FreizeitAdmin(FilteredMemberFieldMixin, nested_admin.NestedModelAdmin):
super(FreizeitAdmin, self).__init__(*args, **kwargs) super(FreizeitAdmin, self).__init__(*args, **kwargs)
def save_model(self, request, obj, form, change): def save_model(self, request, obj, form, change):
print("saving model")
if not change and hasattr(request.user, 'member') and hasattr(obj, 'statement'): 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.created_by = request.user.member
obj.statement.save() obj.statement.save()
super().save_model(request, obj, form, change) 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): def may_view_excursion(self, request, memberlist):
return request.user.has_perm('members.may_view_everyone') or \ return request.user.has_perm('members.may_view_everyone') or \
( hasattr(request.user, 'member') and \ ( 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.conf import settings
from django.core.validators import MinValueValidator 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(): def generate_random_key():
return uuid.uuid4().hex return uuid.uuid4().hex
@ -29,6 +33,7 @@ GEMEINSCHAFTS_TOUR = MUSKELKRAFT_ANREISE = 0
FUEHRUNGS_TOUR = OEFFENTLICHE_ANREISE = 1 FUEHRUNGS_TOUR = OEFFENTLICHE_ANREISE = 1
AUSBILDUNGS_TOUR = FAHRGEMEINSCHAFT_ANREISE = 2 AUSBILDUNGS_TOUR = FAHRGEMEINSCHAFT_ANREISE = 2
class ActivityCategory(models.Model): class ActivityCategory(models.Model):
""" """
Describes one kind of activity Describes one kind of activity
@ -69,7 +74,7 @@ class MemberManager(models.Manager):
return super().get_queryset().filter(confirmed=True) 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. 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_key = models.CharField(max_length=32, default="")
confirm_mail_parents_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 abstract = True
def __str__(self): def __str__(self):
@ -123,7 +128,7 @@ class Person(models.Model):
self.confirmed_mail_parents = False self.confirmed_mail_parents = False
self.confirm_mail_parents_key = uuid.uuid4().hex self.confirm_mail_parents_key = uuid.uuid4().hex
send_mail(_('Email confirmation needed'), send_mail(_('Email confirmation needed'),
CONFIRM_MAIL_TEXT.format(name=self.prename, settings.CONFIRM_MAIL_TEXT.format(name=self.prename,
link=get_mail_confirmation_link(self.confirm_mail_parents_key), link=get_mail_confirmation_link(self.confirm_mail_parents_key),
whattoconfirm='der Emailadresse deiner Eltern'), whattoconfirm='der Emailadresse deiner Eltern'),
settings.DEFAULT_SENDING_MAIL, settings.DEFAULT_SENDING_MAIL,
@ -293,11 +298,21 @@ class Member(Person):
return groupstring return groupstring
get_group.short_description = _('Group') get_group.short_description = _('Group')
class Meta: class Meta(CommonModel.Meta):
verbose_name = _('member') verbose_name = _('member')
verbose_name_plural = _('members') verbose_name_plural = _('members')
permissions = (('may_see_qualities', 'Is allowed to see the quality overview'), permissions = (
('may_set_auth_user', 'Is allowed to set auth user member connections.')) ('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): def get_skills(self):
# get skills by summing up all the activities taken part in # get skills by summing up all the activities taken part in
@ -333,13 +348,51 @@ class Member(Person):
settings.DEFAULT_SENDING_MAIL, settings.DEFAULT_SENDING_MAIL,
jl.email) 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: if queryset is None:
queryset = Member.objects.all() 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] pks = [self.pk]
view_pks = [self.pk] view_pks = [self.pk]
if hasattr(self, 'permissions'): if hasattr(self, 'permissions'):
pks += [ m.pk for m in self.permissions.list_members.all() ] pks += [ m.pk for m in self.permissions.list_members.all() ]
view_pks += [ m.pk for m in self.permissions.view_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())) 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): def may_list(self, other):
if self.pk == other.pk: if self.pk == other.pk:
return True return True
@ -493,10 +561,16 @@ class MemberWaitingList(Person):
default=None, default=None,
verbose_name=_('Invited for group'), verbose_name=_('Invited for group'),
on_delete=models.SET_NULL) on_delete=models.SET_NULL)
class Meta: class Meta(CommonModel.Meta):
verbose_name = _('Waiter') verbose_name = _('Waiter')
verbose_name_plural = _('Waiters') verbose_name_plural = _('Waiters')
permissions = (('may_manage_waiting_list', 'Can view and manage the waiting list.'),) 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 @property
def waiting_confirmation_needed(self): def waiting_confirmation_needed(self):
@ -565,7 +639,7 @@ class MemberWaitingList(Person):
else self.email) else self.email)
class NewMemberOnList(models.Model): class NewMemberOnList(CommonModel):
""" """
Connects members to a list of members. Connects members to a list of members.
""" """
@ -579,9 +653,15 @@ class NewMemberOnList(models.Model):
def __str__(self): def __str__(self):
return str(self.member) return str(self.member)
class Meta: class Meta(CommonModel.Meta):
verbose_name = _('Member') verbose_name = _('Member')
verbose_name_plural = _('Members') 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 @property
def comments_tex(self): def comments_tex(self):
@ -604,7 +684,7 @@ class NewMemberOnList(models.Model):
return ", ".join(qualities) return ", ".join(qualities)
class Freizeit(models.Model): class Freizeit(CommonModel):
"""Lets the user create a 'Freizeit' and generate a members overview in pdf format. """ """Lets the user create a 'Freizeit' and generate a members overview in pdf format. """
name = models.CharField(verbose_name=_('Activity'), default='', name = models.CharField(verbose_name=_('Activity'), default='',
@ -641,9 +721,15 @@ class Freizeit(models.Model):
"""String represenation""" """String represenation"""
return self.name return self.name
class Meta: class Meta(CommonModel.Meta):
verbose_name = "Freizeit" verbose_name = "Freizeit"
verbose_name_plural = "Freizeiten" 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): def get_tour_type(self):
if self.tour_type == FUEHRUNGS_TOUR: if self.tour_type == FUEHRUNGS_TOUR:
@ -827,7 +913,7 @@ class RegistrationPassword(models.Model):
verbose_name_plural = _('registration passwords') verbose_name_plural = _('registration passwords')
class LJPProposal(models.Model): class LJPProposal(CommonModel):
"""A proposal for LJP""" """A proposal for LJP"""
title = models.CharField(verbose_name=_('Title'), max_length=30) title = models.CharField(verbose_name=_('Title'), max_length=30)
@ -843,14 +929,20 @@ class LJPProposal(models.Model):
null=True, null=True,
on_delete=models.SET_NULL) on_delete=models.SET_NULL)
class Meta: class Meta(CommonModel.Meta):
verbose_name = _('LJP Proposal') verbose_name = _('LJP Proposal')
verbose_name_plural = _('LJP Proposals') 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): def __str__(self):
return self.title return self.title
class Intervention(models.Model): class Intervention(CommonModel):
"""An intervention during a seminar as part of a LJP proposal""" """An intervention during a seminar as part of a LJP proposal"""
date_start = models.DateTimeField(verbose_name=_('Starting time')) date_start = models.DateTimeField(verbose_name=_('Starting time'))
duration = models.DecimalField(verbose_name=_('Duration in hours'), duration = models.DecimalField(verbose_name=_('Duration in hours'),
@ -866,6 +958,12 @@ class Intervention(models.Model):
class Meta: class Meta:
verbose_name = _('Intervention') verbose_name = _('Intervention')
verbose_name_plural = _('Interventions') 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): def annotate_activity_score(queryset):
@ -1030,7 +1128,7 @@ class TrainingCategory(models.Model):
return self.name return self.name
class MemberTraining(models.Model): class MemberTraining(CommonModel):
"""Represents a training planned or attended by a member.""" """Represents a training planned or attended by a member."""
member = models.ForeignKey(Member, on_delete=models.CASCADE, related_name='traininigs') member = models.ForeignKey(Member, on_delete=models.CASCADE, related_name='traininigs')
title = models.CharField(verbose_name=_('Title'), max_length=30) title = models.CharField(verbose_name=_('Title'), max_length=30)
@ -1040,9 +1138,17 @@ class MemberTraining(models.Model):
participated = models.BooleanField(verbose_name=_('Participated')) participated = models.BooleanField(verbose_name=_('Participated'))
passed = models.BooleanField(verbose_name=_('Passed')) passed = models.BooleanField(verbose_name=_('Passed'))
class Meta: class Meta(CommonModel.Meta):
verbose_name = _('Training') verbose_name = _('Training')
verbose_name_plural = _('Trainings') 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): def import_from_csv(path):
@ -1179,3 +1285,4 @@ CLUBDESK_TO_KOMPASS = {
'Erziehungsberechtigte': 'legal_guardians', 'Erziehungsberechtigte': 'legal_guardians',
'Mobil Eltern': 'phone_number_parents', '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