Example #1
0
class ManageLeadersView(CreateRatingView):
    """ A view to update the rating of any leader across all ratings. """
    form_class = forms.LeaderForm
    template_name = 'chair/leaders.html'
    success_url = reverse_lazy('manage_leaders')

    @method_decorator(chairs_only())
    def dispatch(self, request, *args, **kwargs):
        return super().dispatch(request, *args, **kwargs)
Example #2
0
class OnlyForActivityChair(View):
    @property
    def activity(self):
        """ The activity, should be verified by the dispatch method. """
        return self.kwargs['activity']

    @method_decorator(chairs_only())
    def dispatch(self, request, *args, **kwargs):
        activity = kwargs.get('activity')
        if not perm_utils.chair_or_admin(request.user, activity):
            raise PermissionDenied
        return super().dispatch(request, *args, **kwargs)

    def get_success_url(self):
        return reverse('activity_leaders', args=(self.activity, ))
Example #3
0
class ApproveTripView(SingleObjectMixin, View):
    model = models.Trip

    def post(self, request, *args, **kwargs):
        postdata = json.loads(self.request.body)
        trip = self.get_object()
        trip.chair_approved = bool(postdata.get('approved'))
        trip.save()
        return JsonResponse({'approved': trip.chair_approved})

    @method_decorator(chairs_only())
    def dispatch(self, request, *args, **kwargs):
        trip = self.get_object()
        if not perm_utils.is_chair(request.user, trip.activity, False):
            raise PermissionDenied
        return super(ApproveTripView, self).dispatch(request, *args, **kwargs)
Example #4
0
class OnlyForActivityChair(View):
    @property
    def activity(self):
        """The activity, should be verified by the dispatch method."""
        return self.kwargs['activity']

    @property
    def activity_enum(self):
        return enums.Activity(self.kwargs['activity'])

    @method_decorator(chairs_only())
    def dispatch(self, request, *args, **kwargs):
        try:
            activity_enum = enums.Activity(kwargs.get('activity'))
        except ValueError:
            raise Http404  # pylint: disable=raise-missing-from

        if not perm_utils.chair_or_admin(request.user, activity_enum):
            raise PermissionDenied
        return super().dispatch(request, *args, **kwargs)

    def get_success_url(self):
        return reverse('activity_leaders', args=(self.activity,))
Example #5
0
class LeaderApplicationView(ApplicationManager, FormMixin,
                            DetailView):  # type: ignore[misc]
    """Handle applications by participants to become leaders."""

    form_class = forms.ApplicationLeaderForm
    context_object_name = 'application'
    template_name = 'chair/applications/view.html'

    def get_success_url(self):
        """Get the next application in this queue.

        (i.e. if this was an application needing a recommendation,
        move to the next application without a recommendation)
        """
        if self.next_app:  # Set before we saved this object
            app_args = (self.activity, self.next_app.pk)
            return reverse('view_application', args=app_args)
        return reverse('manage_applications', args=(self.activity, ))

    def get_other_apps(self):
        """Get the applications that come before and after this in the queue.

        Each "queue" is of applications that need recommendations or ratings.
        """
        ordered_apps = iter(self.pending_applications())
        prev_app = None
        for app in ordered_apps:
            if app.pk == self.object.pk:
                try:
                    next_app = next(ordered_apps)
                except StopIteration:
                    next_app = None
                break
            prev_app = app
        else:
            return None, None  # Could be from another (past) year
        last_app = app  # pylint: disable=undefined-loop-variable

        def if_valid(other_app):
            mismatch = (
                not other_app
                or bool(other_app.num_recs) != bool(last_app.num_recs)
                or bool(other_app.num_ratings) != bool(last_app.num_ratings))
            return None if mismatch else other_app

        return if_valid(prev_app), if_valid(next_app)

    @property
    def par_ratings(self) -> QuerySet[models.LeaderRating]:
        return models.LeaderRating.objects.filter(
            participant=self.object.participant,
            activity=self.activity,
        )

    def existing_rating(self) -> Optional[models.LeaderRating]:
        return self.par_ratings.filter(active=True).first()

    def existing_rec(self) -> Optional[models.LeaderRecommendation]:
        """Load an existing recommendation for the viewing participant."""
        return models.LeaderRecommendation.objects.filter(
            creator=self.chair,
            participant=self.object.participant,
            activity=self.activity,
            time_created__gte=self.object.time_created,
        ).first()

    def _rating_to_prefill(self) -> Optional[str]:
        """Return the rating that we should prefill the form with (if applicable).

        Possible return values:
        - None: We're not ready for the rating step yet.
        - Empty string: We should rate, but we have nothing to pre-fill.
        - Non-empty string: We should rate and we have a pre-filled value!
        """
        # Note that the below logic technically allows admins to weigh in with a rec.
        # Admins can break consensus, but they're not required to *declare* consensus.
        all_recs = self.get_recommendations()

        proposed_ratings = {rec.rating for rec in all_recs}

        chairs = self.activity_chairs()
        users_making_recs = {rec.creator.user for rec in all_recs}

        if set(chairs).difference(users_making_recs):
            if self.activity != enums.Activity.WINTER_SCHOOL.value:
                return None

            # As always, Winter School is special.
            # We regard both the WS chair(s) *and* the WSC as the chairs.
            # However, only the WSC gives ratings.
            # Thus, we can be missing ratings from some "chairs" but really have WSC consensus.
            assert len(chairs) >= 3, "WSC + WS chairs fewer than 3 people!?"
            if len(users_making_recs.intersection(chairs)) < 3:
                # We're definitely missing recommendations from some of the WSC.
                return None

        if len(proposed_ratings) != 1:
            return ''  # No consensus
        return proposed_ratings.pop()

    def get_initial(self) -> Dict[str, Union[str, bool]]:
        """Pre-populate the rating/recommendation form.

        This method tries to provide convenience for common scenarios:

        - activity has only 1 chair, so default to a plain rating
        - all activity chairs have made recommendations, and they agree!
        - the viewing chair wishes to revise their recommendation
        - activity chairs have different recommendations

        Each of the above scenarios has different behaviors.
        """
        # Allow for editing a given rating simply by loading an old form.
        rating = self.existing_rating()
        if rating:
            return {
                'rating': rating.rating,
                'notes': rating.notes,
                'is_recommendation': False,
            }

        rec = self.existing_rec()

        # No recommendation or rating from the viewer? Blank form.
        if not rec:
            # Recommendations only make sense with multiple chairs.
            return {'is_recommendation': len(self.activity_chairs()) > 1}

        # We may be ready to assign a rating (and we may even have a pre-fillable one)
        prefill_rating = self._rating_to_prefill()
        if prefill_rating is not None:
            return {
                'is_recommendation': False,
                'rating': prefill_rating,
                'notes': ''
            }

        # Viewer has given a recommendation, but we're still waiting on others.
        # Prefill their present recommendation, in case they want to edit it.
        return {
            'is_recommendation': True,
            'rating': rec.rating,
            'notes': rec.notes
        }

    @property
    def assigned_rating(self):
        """Return any rating given in response to this application."""
        in_future = Q(
            participant=self.object.participant,
            activity=self.activity,
            time_created__gte=self.object.time_created,
        )
        if not hasattr(self, '_assigned_rating'):
            ratings = models.LeaderRating.objects.filter(in_future)
            self._assigned_rating = ratings.order_by('time_created').first()
        return self._assigned_rating

    @property
    def before_rating(self):
        if self.assigned_rating:
            return Q(time_created__lte=self.assigned_rating.time_created)
        return Q()

    def get_recommendations(self) -> QuerySet[models.LeaderRecommendation]:
        """Get recommendations made by leaders/chairs for this application.

        Only show recommendations that were made for this application. That is,
        don't show recommendations made before the application was created (they must
        have pertained to a previous application), or those created after a
        rating was assigned (those belong to a future application).
        """
        match = Q(participant=self.object.participant, activity=self.activity)
        rec_after_creation = Q(time_created__gte=self.object.time_created)
        find_recs = match & self.before_rating & rec_after_creation
        recs = models.LeaderRecommendation.objects.filter(find_recs)
        return recs.select_related('creator__user')  # (User used for WSC)

    def get_feedback(self):
        """Return all feedback for the participant.

        Activity chairs see the complete history of feedback (without the normal
        "clean slate" period). The only exception is that activity chairs cannot
        see their own feedback.
        """
        return (models.Feedback.everything.filter(
            participant=self.object.
            participant).exclude(participant=self.chair).select_related(
                'leader',
                'trip').prefetch_related('leader__leaderrating_set').annotate(
                    display_date=Least('trip__trip_date',
                                       Cast('time_created', DateField()))).
                order_by('-display_date'))

    def get_context_data(self, **kwargs):
        # Super calls DetailView's `get_context_data` so we'll manually add form
        context = super().get_context_data(**kwargs)
        assigned_rating = self.assigned_rating
        context['assigned_rating'] = assigned_rating
        context['recommendations'] = self.get_recommendations()
        context['leader_form'] = self.get_form()
        context['all_feedback'] = self.get_feedback()
        context['prev_app'], context['next_app'] = self.get_other_apps()

        participant = self.object.participant
        context['active_ratings'] = list(
            participant.ratings(must_be_active=True))
        participant_chair_activities = set(
            perm_utils.chair_activities(participant.user))
        context['chair_activities'] = [
            label for (activity, label) in models.LeaderRating.ACTIVITY_CHOICES
            if activity in participant_chair_activities
        ]
        context['existing_rating'] = self.existing_rating()
        context['existing_rec'] = self.existing_rec()
        context['hide_recs'] = not (assigned_rating or context['existing_rec'])

        all_trips_led = self.object.participant.trips_led
        trips_led = all_trips_led.filter(self.before_rating,
                                         activity=self.activity)
        context['trips_led'] = trips_led.prefetch_related(
            'leaders__leaderrating_set')
        return context

    def form_valid(self, form):
        """Save the rating as a recommendation or a binding rating."""
        # After saving, the order of applications changes
        _, self.next_app = self.get_other_apps(
        )  # Obtain next in current order

        rating = form.save(commit=False)
        rating.creator = self.chair
        rating.participant = self.object.participant
        rating.activity = self.object.activity

        is_rec = form.cleaned_data['is_recommendation']
        if is_rec:
            # Hack to convert the (unsaved) rating to a recommendation
            # (Both models have the exact same fields)
            rec = forms.LeaderRecommendationForm(model_to_dict(rating),
                                                 instance=self.existing_rec())
            rec.save()
        else:
            ratings_utils.deactivate_ratings(rating.participant,
                                             rating.activity)
            rating.save()

        verb = "Recommended" if is_rec else "Created"
        msg = f"{verb} {rating.rating} rating for {rating.participant.name}"
        messages.success(self.request, msg)

        return super().form_valid(form)

    def post(self, request, *args, **kwargs):
        """Create the leader's rating, redirect to other applications."""
        self.object = self.get_object()
        form = self.get_form()

        if form.is_valid():
            return self.form_valid(form)
        return self.form_invalid(form)

    @method_decorator(chairs_only())
    def dispatch(self, request, *args, **kwargs):
        """Redirect if anonymous, but deny permission if not a chair."""
        try:
            activity_enum = enums.Activity(self.activity)
        except ValueError:
            raise Http404  # pylint: disable=raise-missing-from

        if not perm_utils.chair_or_admin(request.user, activity_enum):
            raise PermissionDenied
        return super().dispatch(request, *args, **kwargs)
Example #6
0
class LeaderApplicationView(ApplicationManager, FormMixin, DetailView):
    """ Handle applications by participants to become leaders. """
    form_class = forms.ApplicationLeaderForm
    context_object_name = 'application'
    template_name = 'chair/applications/view.html'

    def get_success_url(self):
        """ Get the next application in this queue.

        (i.e. if this was an application needing a recommendation,
        move to the next application without a recommendation)
        """
        if self.next_app:  # Set before we saved this object
            app_args = (self.activity, self.next_app.pk)
            return reverse('view_application', args=app_args)
        else:
            return reverse('manage_applications', args=(self.activity, ))

    def get_other_apps(self):
        """ Get the applications that come before and after this in the queue.

        Each "queue" is of applications that need recommendations or ratings.
        """
        ordered_apps = iter(self.sorted_applications(just_this_year=True))
        prev_app = None
        for app in ordered_apps:
            if app.pk == self.object.pk:
                try:
                    next_app = next(ordered_apps)
                except StopIteration:
                    next_app = None
                break
            prev_app = app
        else:
            return None, None  # Could be from another (past) year
        last_app = app  # pylint: disable=undefined-loop-variable

        def if_valid(other_app):
            mismatch = (
                not other_app
                or bool(other_app.num_recs) != bool(last_app.num_recs)
                or bool(other_app.num_ratings) != bool(last_app.num_ratings))
            return None if mismatch else other_app

        return if_valid(prev_app), if_valid(next_app)

    @property
    def par_ratings(self):
        find_ratings = Q(participant=self.object.participant,
                         activity=self.activity)
        return models.LeaderRating.objects.filter(find_ratings)

    @property
    def existing_rating(self):
        return self.par_ratings.filter(active=True).first()

    @property
    def existing_rec(self):
        """ Load an existing recommendation for the viewing participant. """
        if hasattr(self, '_existing_rec'):
            # pylint: disable=access-member-before-definition
            return self._existing_rec
        find_rec = Q(creator=self.chair,
                     participant=self.object.participant,
                     activity=self.activity,
                     time_created__gte=self.object.time_created)
        self._existing_rec = models.LeaderRecommendation.objects.filter(
            find_rec).first()
        return self._existing_rec

    def default_to_recommendation(self):
        """ Whether to default the form to a recommendation or not. """
        return False if self.num_chairs < 2 else not self.existing_rec

    def get_initial(self):
        """ Load an existing rating if one exists.

        Because these applications are supposed to be done with leaders that
        have no active rating in the activity, this should almost always be
        blank.
        """
        initial = {'recommendation': self.default_to_recommendation()}
        existing = self.existing_rating or self.existing_rec
        if existing:
            initial['rating'] = existing.rating
            initial['notes'] = existing.notes
        return initial

    @property
    def assigned_rating(self):
        """ Return any rating given in response to this application. """
        in_future = Q(participant=self.object.participant,
                      activity=self.activity,
                      time_created__gte=self.object.time_created)
        if not hasattr(self, '_assigned_rating'):
            ratings = models.LeaderRating.objects.filter(in_future)
            self._assigned_rating = ratings.order_by('time_created').first()
        return self._assigned_rating

    @property
    def before_rating(self):
        if self.assigned_rating:
            return Q(time_created__lte=self.assigned_rating.time_created)
        else:
            return Q()

    def get_recommendations(self, assigned_rating=None):
        """ Get recommendations made by leaders/chairs for this application.

        Only show recommendations that were made for this application. That is,
        don't show recommendations made before the application was created (they must
        have pertained to a previous application), or those created after a
        rating was assigned (those belong to a future application).
        """
        match = Q(participant=self.object.participant, activity=self.activity)
        rec_after_creation = Q(time_created__gte=self.object.time_created)
        find_recs = match & self.before_rating & rec_after_creation
        recs = models.LeaderRecommendation.objects.filter(find_recs)
        return recs.select_related('creator')

    def get_feedback(self):
        """ Return all feedback for the participant.

        Activity chairs see the complete history of feedback (without the normal
        "clean slate" period). The only exception is that activity chairs cannot
        see their own feedback.
        """
        return (models.Feedback.everything.filter(
            participant=self.object.
            participant).exclude(participant=self.chair).select_related(
                'leader',
                'trip').prefetch_related('leader__leaderrating_set').annotate(
                    display_date=Least('trip__trip_date',
                                       Cast('time_created', DateField()))).
                order_by('-display_date'))

    def get_context_data(self, **kwargs):
        # Super calls DetailView's `get_context_data` so we'll manually add form
        context = super().get_context_data(**kwargs)
        assigned_rating = self.assigned_rating
        context['assigned_rating'] = assigned_rating
        context['recommendations'] = self.get_recommendations(assigned_rating)
        context['leader_form'] = self.get_form()
        context['all_feedback'] = self.get_feedback()
        context['prev_app'], context['next_app'] = self.get_other_apps()

        participant = self.object.participant
        context['active_ratings'] = list(
            participant.ratings(rating_active=True))
        participant_chair_activities = set(
            perm_utils.chair_activities(participant.user))
        context['chair_activities'] = [
            label for (activity, label) in models.LeaderRating.ACTIVITY_CHOICES
            if activity in participant_chair_activities
        ]
        context['existing_rating'] = self.existing_rating
        context['existing_rec'] = self.existing_rec

        all_trips_led = self.object.participant.trips_led
        trips_led = all_trips_led.filter(self.before_rating,
                                         activity=self.activity)
        context['trips_led'] = trips_led.prefetch_related(
            'leaders__leaderrating_set')
        return context

    def form_valid(self, form):
        """ Save the rating as a recommendation or a binding rating. """
        # After saving, the order of applications changes
        _, self.next_app = self.get_other_apps(
        )  # Obtain next in current order

        rating = form.save(commit=False)
        rating.creator = self.chair
        rating.participant = self.object.participant
        rating.activity = self.object.activity

        is_rec = form.cleaned_data['recommendation']
        if is_rec:
            # Hack to convert the (unsaved) rating to a recommendation
            # (Both models have the exact same fields)
            rec = forms.LeaderRecommendationForm(model_to_dict(rating),
                                                 instance=self.existing_rec)
            rec.save()
        else:
            ratings_utils.deactivate_ratings(rating.participant,
                                             rating.activity)
            rating.save()

        fmt = {
            'verb': "Recommended" if is_rec else "Created",
            'rating': rating.rating,
            'participant': rating.participant.name
        }
        msg = "{verb} {rating} rating for {participant}".format(**fmt)
        messages.success(self.request, msg)

        return super().form_valid(form)

    def post(self, request, *args, **kwargs):
        """ Create the leader's rating, redirect to other applications. """
        self.object = self.get_object()
        form = self.get_form()

        if form.is_valid():
            return self.form_valid(form)
        else:
            return self.form_invalid(form)

    @method_decorator(chairs_only())
    def dispatch(self, request, *args, **kwargs):
        """ Redirect if anonymous, but deny permission if not a chair. """
        if not perm_utils.chair_or_admin(request.user, self.activity):
            raise PermissionDenied
        return super().dispatch(request, *args, **kwargs)