from django.conf import settings from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin from django.core.exceptions import PermissionDenied from django.http import Http404, HttpRequest, HttpResponse from django.shortcuts import redirect from django.urls import reverse, reverse_lazy from django.utils.translation import gettext_lazy as _ from django.views.generic.detail import SingleObjectMixin from rules.contrib.views import PermissionRequiredMixin as RulesPermissionRequiredMixin from core.models.assemblies import Assembly from core.models.badges import Badge from core.models.conference import Conference, ConferenceMember from core.models.rooms import Room from core.models.sso import Application class ConferenceRequiredMixinBase: login_url = reverse_lazy('backoffice:login') require_conference = False _conference: Conference | None = None _conferencemember: ConferenceMember | None = None def __init__(self, *args, **kwargs): self._conference = kwargs.pop('conference', None) super().__init__(*args, **kwargs) @property def conference(self): if self._conference is None: conference_id = self.request.session.get('conference') if conference_id is None: try: self._conference = Conference.objects.filter(is_public=True).first() except Conference.DoesNotExist: try: if self.request.user.is_staff: self._conference = Conference.objects.first() except Conference.DoesNotExist: pass if self._conference is not None: self.request.session['conference'] = self._conference.slug else: try: self._conference = Conference.objects.accessible_by_user(self.request.user).get(slug=conference_id) except Conference.DoesNotExist: self.request.session['conference'] = None return self._conference @property def conferencemember(self): if self._conferencemember is None: self._conferencemember = ConferenceMember.get_member( conference=self.conference, user=self.request.user, prefetch_user=True, ) return self._conferencemember @property def is_assembly_team(self): return self.conferencemember.user.is_authenticated and self.conferencemember.has_perms('core.assembly_team', require_staff=True) def dispatch(self, request, *args, **kwargs): if self.require_conference and self.conference is None: return redirect('backoffice:conferences') return super().dispatch(request, *args, **kwargs) def get_context_data(self, **kwargs): # super() does not have to have get_context_data(), e.g. if it's a plain View if hasattr(super(), 'get_context_data'): context = super().get_context_data(**kwargs) else: context = {} conference_list = Conference.objects.accessible_by_user(self.request.user).all() context.update( { 'LANGUAGES': settings.LANGUAGES, 'conference': self.conference, 'conferencemember': self.conferencemember, 'conferences': conference_list, } ) if self.request.user.is_authenticated: context.update( { 'has_sos': self.conferencemember is not None, 'has_assemblies': self.is_assembly_team, 'has_pages': self.conferencemember.has_perms('core.static_pages', require_staff=True), 'has_map': self.conferencemember.has_perms('core.map_edit', require_staff=True), 'has_assembly_registration_admin': self.conferencemember.has_perms('core.assembly_registration_admin', require_staff=True), 'has_conference_admin': self.conferencemember.has_perms('core.conference_admin', require_staff=True), 'has_moderation': self.conferencemember.has_perms('core.moderation', require_staff=True), 'has_schedules': self.conferencemember.has_perms('core.scheduleadmin', require_staff=True), } ) else: context.update( { 'has_assemblies': False, 'has_pages': False, 'has_map': False, 'has_assembly_registration_admin': False, 'has_conference_admin': False, 'has_moderation': False, 'has_schedules': False, } ) return context class ConferenceRequiredMixin(ConferenceRequiredMixinBase, PermissionRequiredMixin): permission_required = [] require_all_permissions = True require_staff = False def has_permission(self): perms = self.get_permission_required() if not perms: return True cm = self.conferencemember return ( cm.has_perms( *perms, require_all=self.require_all_permissions, require_staff=self.require_staff, ) if cm is not None else False ) class ConferenceLoginRequiredMixin(LoginRequiredMixin, ConferenceRequiredMixin): pass class ConferenceRuleRequiredMixin(ConferenceRequiredMixinBase, RulesPermissionRequiredMixin): pass class ConferenceRuleLoginRequiredMixin(LoginRequiredMixin, ConferenceRuleRequiredMixin): pass class AssemblyMixinBase: assembly_url_param = 'pk' def __init__(self, *args, **kwargs): self._assembly = kwargs.pop('assembly', None) super().__init__(*args, **kwargs) # if _can_manage is set, user can edit assembly. This is a cache variable for the can_manage property self._can_manage = None # if _staff_access is set, extra staff fields can be modified (internal comment, is_official) self._staff_access = False # if _assembly_staff_access is set, the field state can be modified self._assembly_staff_access = False # configures the staff warning self._staff_mode = False def _get_assembly(self): if self._assembly is not None: return self._assembly assembly = Assembly.objects.get(conference=self.conference, pk=self.request.resolver_match.kwargs.get(self.assembly_url_param)) # check if it's the assembly team if self.conferencemember.has_perms('core.assembly_team', 'core.change_assembly', require_staff=True): self._assembly_staff_access = True self._staff_access = self._staff_access self._staff_mode = True # neither owner/manager nor assembly team? go away if not assembly.has_user(self.request.user) and not self._assembly_staff_access: raise PermissionDenied self._assembly = assembly return assembly @property def assembly(self): return self._get_assembly() @property def can_manage(self): if self._can_manage is None: if not self.request.user.is_authenticated: # guests are not allowed to manage anything self._can_manage = False return False self._can_manage = self.assembly.user_can_manage(self.request.user, staff_can_manage=True) return self._can_manage @property def staff_access(self): return self._staff_access @property def assembly_staff_access(self): return self._assembly_staff_access @property def staff_mode(self): return self._staff_mode def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['assembly'] = assembly = self.assembly context['can_manage'] = can_manage = self.can_manage context['staff_access'] = self.staff_access context['staff_mode'] = self.staff_mode sidebar = [] context['sidebar'] = { 'back_link': {'link': reverse('backoffice:index'), 'caption': _('my_assemblies')}, 'title': assembly.slug, 'title_link': reverse('backoffice:assembly', kwargs={'pk': assembly.id}), 'items': sidebar, } # load backlink from the session if it set. Set by Assemblyteam / Channelsteam pages as there are a bunch of # pages that will link to assembly views if 'assembly_back' in self.request.session: assembly_back = self.request.session['assembly_back'] if 'link' in assembly_back and 'title' in assembly_back: context['sidebar']['back_link'] = {'link': assembly_back['link'], 'caption': assembly_back['title']} organisation = [] sidebar.append({'caption': _('backoffice:assembly-organizational-data'), 'children': organisation}) organisation.append( { 'caption': _('backoffice:assembly-basic-data'), 'link': reverse('backoffice:assembly-edit', kwargs={'pk': assembly.id}), } ) if assembly.is_cluster: organisation.append( { 'caption': 'Sub-Assemblies', 'link': reverse('backoffice:assembly-editchildren', kwargs={'pk': assembly.id}), } ) organisation.append( { 'caption': 'Links', 'link': reverse('backoffice:assembly-editlinks', kwargs={'pk': assembly.id}), } ) organisation.append( { 'caption': _('Assembly__members'), 'link': reverse('backoffice:assembly-members', kwargs={'pk': assembly.id}), 'count': assembly.members.count(), } ) if can_manage: apps = [ { 'caption': f'{app["name"]}', 'link': reverse('backoffice:assembly-auth-app', kwargs={'assembly': assembly.id, 'pk': app['id']}), 'classes': [], } for app in Application.objects.filter(assembly=self.assembly).values('id', 'name') ] sidebar.append( { 'caption': _('Assembly__authentication'), 'children': apps, 'count': len(apps), 'link': reverse('backoffice:assembly-auth', kwargs={'assembly': assembly.id}), } ) if (voucher_count := assembly.get_voucher_count(with_always_public=True)) is not None: organisation.append( { 'caption': _('Vouchers'), 'count': voucher_count, 'link': reverse('backoffice:assembly-vouchers', kwargs={'pk': assembly.id}), } ) rooms = [ { 'caption': f'{room["name"]} ({room["room_type"]})', 'link': reverse('backoffice:assembly-room', kwargs={'assembly': assembly.id, 'pk': room['id']}), 'classes': ['blocked'] if room['blocked'] else [], } for room in assembly.rooms.exclude(room_type=Room.RoomType.PROJECT).values('id', 'name', 'room_type', 'blocked') ] sidebar.append( { 'caption': _('backoffice:assembly-rooms'), 'children': rooms, 'count': len(rooms), 'add_link': reverse('backoffice:assembly-create-room', kwargs={'assembly': assembly.id}) if can_manage else None, } ) projects = [ { 'caption': prj['name'], 'link': reverse('backoffice:assembly-project', kwargs={'assembly': assembly.id, 'pk': prj['id']}), 'classes': ['blocked'] if prj['blocked'] else [], } for prj in assembly.projects.all().values('id', 'name', 'is_public', 'blocked') ] sidebar.append( { 'caption': _('backoffice:assembly-projects'), 'link': reverse('backoffice:assembly-projects', kwargs={'assembly': assembly.id}), 'children': projects, 'count': len(projects), 'add_link': reverse('backoffice:assembly-create-project', kwargs={'assembly': assembly.id}) if can_manage else None, } ) events = [ { 'caption': ev['name'], 'link': reverse('backoffice:assembly-event', kwargs={'assembly': assembly.id, 'pk': ev['id']}), 'classes': ['blocked'] if not ev['is_public'] else [], } for ev in assembly.events.values('id', 'name', 'is_public') ] sidebar.append( { 'caption': 'Events', 'link': reverse('backoffice:assembly-events', kwargs={'assembly': assembly.id}), 'children': events, 'count': len(events), 'add_link': reverse('backoffice:assembly-create-event', kwargs={'assembly': assembly.id}) if can_manage else None, } ) badges = [ { 'caption': b['name'], 'link': reverse('backoffice:assembly-badge', kwargs={'assembly': assembly.id, 'pk': b['id']}), 'classes': ['blocked'] if b['state'] == Badge.State.PLANNED else [], } for b in assembly.badges.values('id', 'name', 'state') ] sidebar.append( { 'caption': 'Badges', 'link': reverse('backoffice:assembly-badges', kwargs={'assembly': assembly.id}), 'children': badges, 'count': len(badges), 'add_link': reverse('backoffice:assembly-create-badge', kwargs={'assembly': assembly.id}) if can_manage else None, } ) # try to guess 'active' sidebar item guess_active_sidebar_item(self.request, context['sidebar']['items']) return context class AssemblyMixin(AssemblyMixinBase, ConferenceLoginRequiredMixin): assembly_management = False def dispatch(self, request, *args, **kwargs): # for authenticated users check if they have assembly management permissions in case those are required try: if self.request.user.is_authenticated and self.assembly_management and not self.can_manage: return HttpResponse('You do not have assembly management permissions.', status=401) except PermissionDenied: return HttpResponse('You do not have assembly management permissions.', status=401) return super().dispatch(request, *args, **kwargs) class AssemblyRuleMixin(AssemblyMixinBase, ConferenceRuleLoginRequiredMixin): pass def guess_active_sidebar_item(request: HttpRequest, sidebar_items: dict, with_query_string: bool = True) -> None: query_string = '' if with_query_string and (qs := request.META.get('QUERY_STRING')): query_string = '?' + qs request_url = request.META.get('PATH_INFO') + query_string for sidebar_item in sidebar_items: if request_url == sidebar_item.get('link'): sidebar_item['active'] = True sidebar_item['expanded'] = True continue if request_url == sidebar_item.get('add_link'): sidebar_item['active'] = True sidebar_item['expanded'] = False continue if 'children' in sidebar_item: for sidebar_child in sidebar_item.get('children') or []: if 'link' in sidebar_child and sidebar_child['link'] == request_url: sidebar_child['active'] = True sidebar_item['child_active'] = True sidebar_item['expanded'] = True else: sidebar_item['children'] = None class PasswordMixin: def get_context_data(self, *args, **kwargs): try: context = super().get_context_data(*args, **kwargs) except AttributeError: # super() does not have .get_context_data(), e.g. if it's a plain View context = {} context.update( { 'LANGUAGES': settings.LANGUAGES, } ) return context class SingleUUIDObjectMixin(SingleObjectMixin): uuid_url_kwarg = 'uuid' def get_object(self, queryset=None): """ Return the object the view is displaying. Require `self.queryset` and a `uuid` argument in the URLconf. Subclasses can override this to return any object. """ # Use a custom queryset if provided; this is required for subclasses # like DateDetailView if queryset is None: queryset = self.get_queryset() uuid = self.kwargs.get(self.uuid_url_kwarg) if uuid is not None: queryset = queryset.filter(uuid=uuid) else: raise AttributeError(f'Generic detail view {self.__class__.__name__} must be called with an object uuid in the URLconf.') try: # Get the single item from the filtered queryset obj = queryset.get() except queryset.model.DoesNotExist as exc: raise Http404(_('No %(verbose_name)s found matching the query') % {'verbose_name': queryset.model._meta.verbose_name}) from exc return obj