Skip to content
Snippets Groups Projects
Commit 9f52b3b1 authored by florimondmanca's avatar florimondmanca
Browse files

add visit organizers group system : 1 group automatically created for each visit

parent ad59c526
Branches
No related tags found
No related merge requests found
......@@ -39,12 +39,15 @@ THIRD_PARTY_APPS = [
'rest_framework',
'rest_framework.authtoken',
# DRY REST permissions (rules-based API permissions)
# See: https://github.com/dbkaplan/dry-rest-permissions
# https://github.com/dbkaplan/dry-rest-permissions
'dry_rest_permissions',
# Frontend integration
# CORS headers for Frontend integration
'corsheaders',
# Sortable models in Admin
'adminsortable2',
# Django Guardian: per object permissions
# https://github.com/django-guardian/django-guardian
'guardian',
]
PROJECT_APPS = [
'core.apps.CoreConfig',
......@@ -148,6 +151,10 @@ DATABASES = {
# Authentication
AUTH_USER_MODEL = 'users.User'
AUTHENTICATION_BACKENDS = [
'django.contrib.auth.backends.ModelBackend', # default
'guardian.backends.ObjectPermissionBackend',
]
# Password validation
# https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators
......
......@@ -30,7 +30,7 @@ class TestProfile(ModelTestCase):
user__last_name='Dylan')
def test_one_to_one_relationship(self):
user = User.objects.get()
user = User.objects.get(first_name='Bob')
self.assertEqual(user, self.obj.user)
# user.profile_object is the raw Profile object
self.assertEqual(user.profile_object, self.obj)
......@@ -71,9 +71,9 @@ class TestCreateProfileForUserSignal(TestCase):
"""Test a new user instance gets a profile based on the profile_type."""
def test_create_user_assigns_a_profile_according_to_profile_type(self):
UserFactory.create(profile_type='student')
UserFactory.create(first_name='Bob', profile_type='student')
# A signal was triggered and user received a Student profile
user = User.objects.get()
user = User.objects.get(first_name='Bob')
# user.profile is a property that dynamically finds the Profile
# object corresponding to (profile.user == user)
self.assertIsInstance(user.profile, Student)
......
"""Test visits signals."""
from django.test import TestCase
from guardian.shortcuts import get_group_perms
from django.contrib.auth.models import Group
from tests.factory import VisitFactory
class CreateOrganizersGroupTest(TestCase):
"""Test the post_save create_organizers_group signal."""
def test_creates_group_using_organizers_group_name_property(self):
visit = VisitFactory.create()
self.assertTrue(Group.objects
.filter(name=visit.organizers_group_name)
.exists())
def test_organizers_group_can_manage_visit(self):
visit = VisitFactory.create()
group = visit.organizers_group
self.assertTrue('manage_visit' in get_group_perms(group, visit))
def test_visit_save_updates_group_name(self):
visit = VisitFactory.create()
initial_name = visit.organizers_group.name
visit.title = 'Look, another title!'
visit.save() # should trigger update of organizers_group.name
self.assertNotEqual(visit.organizers_group_name, initial_name)
self.assertEqual(visit.organizers_group.name,
visit.organizers_group_name)
......@@ -68,6 +68,12 @@ class VisitTest(ModelTestCase):
visit = VisitWithClosedRegistrationsFactory.create()
self.assertFalse(visit.registrations_open)
def test_organizers_group_name_contains_visit_id(self):
self.assertIn(str(self.obj.id), self.obj.organizers_group_name)
def test_organizers_group_name_contains_visit_title(self):
self.assertIn(self.obj.title, self.obj.organizers_group_name)
def test_str_is_title(self):
self.assertEqual(str(self.obj), str(self.obj.title))
......
......@@ -3,6 +3,7 @@
from django.contrib import admin
from django.utils.translation import ugettext_lazy as _
from django.contrib.auth.admin import UserAdmin
from guardian.admin import GuardedModelAdminMixin
from .models import User, Tutor, Student, SchoolStaffMember
from tutoring.admin import TutoringGroupMembershipInline
......@@ -13,7 +14,7 @@ from visits.admin import VisitParticipantInline
@admin.register(User)
class CustomUserAdmin(UserAdmin):
class CustomUserAdmin(GuardedModelAdminMixin, UserAdmin):
"""Customized user admin panel."""
# The fields to be used in displaying the User model.
......
......@@ -2,6 +2,8 @@
from django import forms
from django.contrib import admin
from guardian.admin import GuardedModelAdmin
from guardian.shortcuts import get_objects_for_user
from .models import Visit, Place
# Register your models here.
......@@ -61,7 +63,7 @@ class VisitParticipantInline(admin.TabularInline):
@admin.register(Visit)
class VisitAdmin(admin.ModelAdmin):
class VisitAdmin(GuardedModelAdmin):
"""Admin panel for visits."""
# IDEA create a dashboard using:
......@@ -73,7 +75,13 @@ class VisitAdmin(admin.ModelAdmin):
'_registrations_open', 'num_participants')
list_filter = ('date', RegistrationsOpenFilter)
search_fields = ('title', 'place',)
exclude = ('participants',)
exclude = ('participants', 'organizers_group',)
def get_queryset(self, request):
if request.user.is_superuser:
return super().get_queryset(request)
# show only visits that user can manage
return get_objects_for_user(request.user, 'visits.manage_visit')
def num_participants(self, obj):
return obj.participants.count()
......
......@@ -4,3 +4,6 @@ from django.apps import AppConfig
class VisitsConfig(AppConfig):
name = 'visits'
verbose_name = 'Sorties'
def ready(self):
from . import signals
......@@ -132,6 +132,10 @@ class Visit(models.Model):
help_text="Formats supportés : PDF")
participants = models.ManyToManyField('users.Student',
through='VisitParticipant')
organizers_group = models.OneToOneField('auth.Group',
on_delete=models.CASCADE,
null=True,
verbose_name='organisateurs')
def _registrations_open(self):
return now() < self.deadline
......@@ -141,9 +145,16 @@ class Visit(models.Model):
registrations_open = property(_registrations_open)
registrations_open.fget.short_description = 'Inscriptions ouvertes'
@property
def organizers_group_name(self):
return 'Organisateurs - {} : {}'.format(self.id, self.title)
class Meta: # noqa
ordering = ('date',)
verbose_name = 'sortie'
permissions = (
('manage_visit', 'Can manage visit'),
)
# Read-only permissions
......
"""Visits signals."""
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.contrib.auth.models import Group
from django.db import transaction
from guardian.shortcuts import assign_perm
from visits.models import Visit
@receiver(post_save, sender=Visit)
def sync_organizers_group(sender, instance, **kwargs):
"""Sync visit and its organizers group.
Create the group if visit has none, updates the group name otherwise.
"""
visit = instance
if not visit.organizers_group:
with transaction.atomic():
group_name = visit.organizers_group_name
group = Group.objects.create(name=group_name)
group.save()
visit.organizers_group = group
assign_perm('manage_visit', group, visit)
elif visit.organizers_group.name != visit.organizers_group_name:
with transaction.atomic():
# update group name
group = visit.organizers_group
old_name = group.name
group.name = visit.organizers_group_name
group.save()
# delete old group
Group.objects.filter(name=old_name).delete()
......@@ -10,3 +10,4 @@ django-cors-headers
Pillow
django-markdownx
dry_rest_permissions>=0.1.10
django-guardian>=1.4.9
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment