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)
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, ))
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)
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,))
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)
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)