TabbycatDebate/tabbycat

View on GitHub
tabbycat/users/permissions.py

Summary

Maintainability
A
2 hrs
Test Coverage
from itertools import groupby
from typing import List, TYPE_CHECKING, Union

from django.core.cache import cache
from django.db.models import TextChoices
from django.utils.translation import gettext_lazy as _

if TYPE_CHECKING:
    from django.conf import settings
    from tournaments.models import Tournament

PERM_CACHE_KEY = "user_%d_%s_%s_permission"


class Permission(TextChoices):
    VIEW_ADJ_TEAM_CONFLICTS = 'view.adjudicatorteamconflict', _("view adjudicator-team conflicts")
    EDIT_ADJ_TEAM_CONFLICTS = 'edit.adjudicatorteamconflict', _("edit adjudicator-team conflicts")
    VIEW_ADJ_ADJ_CONFLICTS = 'view.adjudicatoradjudicatorconflict', _("view adjudicator-adjudicator conflicts")
    EDIT_ADJ_ADJ_CONFLICTS = 'edit.adjudicatoradjudicatorconflict', _("edit adjudicator-adjudicator conflicts")
    VIEW_ADJ_INST_CONFLICTS = 'view.adjudicatorinstitutionconflict', _("view adjudicator-institution conflicts")
    EDIT_ADJ_INST_CONFLICTS = 'edit.adjudicatorinstitutionconflict', _("edit adjudicator-institution conflicts")
    VIEW_TEAM_INST_CONFLICTS = 'view.teaminstitutionconflict', _("view team-institution conflicts")
    EDIT_TEAM_INST_CONFLICTS = 'edit.teaminstitutionconflict', _("edit team-institution conflicts")

    VIEW_ACTIONLOGENTRIES = 'view.actionlogentry', _("view action log entries")
    # EDIT_ACTIONLOGENTRIES omitted as pre-supposed when taking an action

    VIEW_TEAMS = 'view.team', _("view teams")
    ADD_TEAMS = 'add.team', _("add teams")
    VIEW_DECODED_TEAMS = 'view.teamname', _("view decoded team names")
    VIEW_ANONYMOUS = 'view.anonymous', _("View names of anonymized participants")
    VIEW_ADJUDICATORS = 'view.adj', _("view adjudicators")
    ADD_ADJUDICATORS = 'add.adj', _("add adjudicators")
    VIEW_ROOMS = 'view.room', _("view rooms")
    ADD_ROOMS = 'add.room', _("add rooms")
    VIEW_INSTITUTIONS = 'view.inst', _("view institutions")
    ADD_INSTITUTIONS = 'add.inst', _("add institutions")
    VIEW_PARTICIPANTS = 'view.particpants', _("view participants")
    VIEW_PARTICIPANT_GENDER = 'view.participants.gender', _("view participants' gender information")
    VIEW_PARTICIPANT_CONTACT = 'view.participants.contact', _("view participants' contact information")
    VIEW_PARTICIPANT_DECODED = 'view.participants.decoded', _("view participants' real names")
    VIEW_PARTICIPANT_INST = 'view.participants.inst', _("view participants' institution")

    VIEW_ROUNDAVAILABILITIES_TEAM = 'view.roundavailability.team', _("view round availabilities for teams")
    VIEW_ROUNDAVAILABILITIES_ADJ = 'view.roundavailability.adjudicator', _("view round availabilities for adjudicators")
    VIEW_ROUNDAVAILABILITIES_VENUE = 'view.roundavailability.venue', _("view round availabilities for rooms")
    EDIT_ROUNDAVAILABILITIES_TEAM = 'edit.roundavailability.team', _("edit round availabilities for teams")
    EDIT_ROUNDAVAILABILITIES_ADJ = 'edit.roundavailability.adjudicator', _("edit round availabilities for adjudicators")
    EDIT_ROUNDAVAILABILITIES_VENUE = 'edit.roundavailability.venue', _("edit round availabilities for rooms")
    VIEW_ROUNDAVAILABILITIES = 'view.roundavailability', _("view round availabilities")
    EDIT_ROUNDAVAILABILITIES = 'edit.roundavailability', _("edit round availabilities")

    VIEW_ROOMCONSTRAINTS = 'view.roomconstraints', _("view room constraints")
    VIEW_ROOMCATEGORIES = 'view.roomcategories', _("view room categories")
    EDIT_ROOMCONSTRAINTS = 'edit.roomconstraints', _("edit room constraints")
    EDIT_ROOMCATEGORIES = 'edit.roomcategories', _("edit room categories")

    VIEW_DEBATE = 'view.debate', _("view debates (draw)")
    VIEW_ADMIN_DRAW = 'view.debate.admin', _("view debates (detailed draw)")
    GENERATE_DEBATE = 'generate.debate', _("generate debates (draw)")
    DELETE_DEBATE = 'delete.debate', _("delete debates (draw)")
    EDIT_DEBATETEAMS = 'edit.debateteam', _("edit debate teams (pairings)")
    VIEW_DEBATEADJUDICATORS = 'view.debateadjudicator', _("view debate adjudicators (allocations)")
    EDIT_DEBATEADJUDICATORS = 'edit.debateadjudicator', _("edit debate adjudicators (allocations)")
    VIEW_ROOMALLOCATIONS = 'view.roomallocations', _("view room allocations")
    EDIT_ROOMALLOCATIONS = 'edit.roomallocations', _("edit room allocations")
    EDIT_ALLOCATESIDES = 'edit.allocatesides', _("edit and confirm outround team positions")

    # Logic behind the ballotsub permissions:
    # Confirmed ballots are more prominent than old ones, but are more sensitive to changes.
    # Then, assistants may confirm others' ballots but not their own.
    VIEW_NEW_BALLOTSUBMISSIONS = 'view.ballotsubmission.new', _("view confirmed ballots")
    EDIT_OLD_BALLOTSUBMISSIONS = 'edit.ballotsubmission.old', _("edit non-confirmed ballots")
    VIEW_BALLOTSUBMISSIONS = 'view.ballotsubmission', _("view any ballot")
    EDIT_BALLOTSUBMISSIONS = 'edit.ballotsubmission', _("edit any ballot")
    ADD_BALLOTSUBMISSIONS = 'add.ballotsubmission', _("create ballots")
    MARK_BALLOTSUBMISSIONS = 'mark.ballotsubmission', _("confirm/discard any ballot")
    MARK_OTHERS_BALLOTSUBMISSIONS = 'mark.ballotsubmission.others', _("confirm/discard others' ballots")
    VIEW_BALLOTSUBMISSION_GRAPH = 'view.ballotsubmission.graph', _("view ballot graph")
    VIEW_RESULTS = 'view.results', _("view results entry page")

    VIEW_MOTION = 'view.roundmotion', _("view motion per round")
    EDIT_MOTION = 'edit.roundmotion', _("edit motion per round")
    RELEASE_DRAW = 'release.draw', _("release draw to public")
    RELEASE_MOTION = 'release.motion', _("release motion to public")
    UNRELEASE_DRAW = 'unrelease.draw', _("unrelease draw to public")
    UNRELEASE_MOTION = 'unrelease.motion', _("unrelease motion to public")
    EDIT_STARTTIME = 'edit.starttime', _("add debate start time")
    VIEW_DRAW = 'view.draw', _("view draws")

    VIEW_BRIEFING_DRAW = 'view.briefingdraw', _("view draws (for the briefing room)")
    DISPLAY_MOTION = 'display.motion', _("display motion (for the briefing room)")

    VIEW_TOURNAMENTPREFERENCEMODEL = 'view.tournamentpreferencemodel', _("view tournament configuration")
    EDIT_TOURNAMENTPREFERENCEMODEL = 'edit.tournamentpreferencemodel', _("edit tournament configuration")

    VIEW_PREFORMEDPANELS = 'view.preformedpanels', _("view existing preformed panels")
    EDIT_PREFORMEDPANELS = 'edit.preformedpanels', _("edit preformed panels")

    # standings tab
    VIEW_STANDINGS_OVERVIEW = 'view.standingsoverview', _("view the overviews of standings")
    VIEW_TEAMSTANDINGS = 'view.teamstandings', _("view the most recent team standings")
    VIEW_SPEAKERSSTANDINGS = 'view.speakersstandings', _("view the most recent speaker standings")
    VIEW_REPLIESSTANDINGS = 'view.repliesstandings', _("view the most recent replies standings")
    VIEW_MOTIONSTAB = 'view.motionstab', _("view the most recent motions tab")
    VIEW_DIVERSITYTAB = 'view.diversitytab', _("view the diversity tab")

    # Feedback tab
    VIEW_FEEDBACK_OVERVIEW = 'view.feedbackoverview', _("view overview of judge feedback")
    EDIT_JUDGESCORES_BULK = 'edit.judgescoresbulk', _("bulk update judge scores")
    EDIT_BASEJUDGESCORES_IND = 'edit.judgescoresind', _("edit base scores of judges")
    VIEW_FEEDBACK = 'view.feedback', _("view feedback")
    EDIT_FEEDBACK_IGNORE = 'edit.feedbackignore', _("toggle ignore feedback")
    EDIT_FEEDBACK_CONFIRM = 'edit.feedbackconfirm', _("toggle confirm feedback")
    VIEW_FEEDBACK_UNSUBMITTED = 'view.feedbackunsubmitted', _("view feedback unsubmitted tab")
    ADD_FEEDBACK = 'add.feedback', _("add feedback")
    VIEW_ADJ_BREAK = 'view.adj.break', _("view adjudicator break")
    EDIT_ADJ_BREAK = 'edit.adj.break', _("edit adjudicator break")
    # idk if its possible for them to add feedback everywhere, considering there is add feedback on multiple pages

    EDIT_FEEDBACKQUESTION = 'edit.feedbackquestion', _("edit feedback questions")

    # breaks
    EDIT_BREAK_ELIGIBILITY = 'edit.breakeligibility', _("edit break eligibility")
    VIEW_BREAK_ELIGIBILITY = 'view.breakeligibility', _("view break eligibility")
    EDIT_BREAK_CATEGORIES = 'edit.breakcategories', _("edit break categories")
    VIEW_BREAK_CATEGORIES = 'view.breakcategories', _("view break categories")
    VIEW_SPEAKER_CATEGORIES = 'view.speakercategories', _("view speaker categories")
    EDIT_SPEAKER_CATEGORIES = 'edit.speakercategories', _("edit speaker categories")
    VIEW_SPEAKER_ELIGIBILITY = 'view.speakereligibility', _("view speaker eligibility")
    EDIT_SPEAKER_ELIGIBILITY = 'edit.speakereligibility', _("edit speaker eligibility")
    VIEW_BREAK_OVERVIEW = 'view.break.overview', _("view break overview")
    VIEW_BREAK = 'view.break', _("view breaks")
    GENERATE_BREAK = 'generate.break', _("generate all breaks")

    VIEW_PRIVATE_URLS = 'view.privateurls', _("view private urls")
    VIEW_PRIVATE_URLS_EMAIL_LIST = 'view.privateurls.emaillist', _("view private urls email list")
    GENERATE_PRIVATE_URLS = 'generate.privateurls', _("generate private URLs")
    # need to get rid of generate private urls soons
    SEND_PRIVATE_URLS = 'send.privateurls', _("send private URLs")

    VIEW_CHECKIN = 'view.checkin', _("view checkins")
    EDIT_PARTICIPANT_CHECKIN = 'edit.participantcheckin', _("edit participant check-in")
    EDIT_ROOM_CHECKIN = 'edit.roomcheckin', _("edit room check-in")

    EDIT_ROUND = 'edit.round', _("edit round attributes")
    DELETE_ROUND = 'delete.round', _("delete rounds")
    CREATE_ROUND = 'add.round', _("create rounds")
    CONFIRM_ROUND = 'confirm.round', _("confirm rounds")
    SILENCE_ROUND = 'silence.round', _("silence rounds")

    VIEW_EMAIL_STATUSES = 'view.emails', _("view email statuses")
    SEND_EMAILS = 'send.emails', _("send participants email messages")

    EXPORT_XML = 'export.xml', _("export DebateXML")

    VIEW_SETTINGS = 'view.settings', _("view settings")
    EDIT_SETTINGS = 'edit.settings', _("edit settings")


permission_type = Union[Permission, bool]


def has_permission(user: 'settings.AUTH_USER_MODEL', permission: permission_type, tournament: 'Tournament') -> bool:
    if user.is_anonymous:
        return False
    if user.is_superuser:
        return True

    if isinstance(permission, bool):
        return permission

    if not hasattr(user, '_permissions'):
        user._permissions = {}

    if tournament.slug in user._permissions:
        if permission in user._permissions[tournament.slug]:
            return True
    else:
        user._permissions[tournament.slug] = set()

    cached_perm = cache.get(PERM_CACHE_KEY % (user.pk, tournament.slug, str(permission)))
    if cached_perm is not None:
        if cached_perm:
            user._permissions[tournament.slug].add(permission)
        return cached_perm

    perm = (
        user.userpermission_set.filter(permission=permission, tournament=tournament).exists() or
        user.membership_set.filter(group__permissions__contains=[permission], group__tournament=tournament).exists()
    )
    if perm:
        user._permissions[tournament.slug].add(permission)
        cache.set(PERM_CACHE_KEY % (user.pk, tournament.slug, str(permission)), perm)
    return perm


def get_permissions(user: 'settings.AUTH_USER_MODEL') -> List['Tournament']:
    user_perms = {}
    for t, groups in groupby(user.membership_set.select_related('group', 'group__tournament').order_by('group__tournament').all(), key=lambda m: m.group.tournament):
        tournament = user_perms.setdefault(t.id, t)
        tournament.permissions = set()
        tournament.groups = [m.group for m in groups]
        for g in tournament.groups:
            tournament.permissions |= set(g.permissions)
    for t, perms in groupby(user.userpermission_set.select_related('tournament').order_by('tournament').all(), key=lambda p: p.tournament):
        tournament = user_perms.setdefault(t.id, t)
        tournament.permissions = getattr(tournament, 'permissions', set()) | {p.permission for p in perms}

    return list(user_perms.values())