<commit_before>from collections import Counter
from itertools import dropwhile
import logging
from random import shuffle

from django.utils import timezone

from flindt.feedback.models import (Feedback, FeedbackOnIndividual, FeedbackOnRole)
from flindt.user.models import User

logger = logging.getLogger(__name__)


class IntegrationError(Exception):
    pass


class MatchNotFoundError(Exception):
    pass


class NoSolutionFound(Exception):
    pass


class NoSolutionPossible(Exception):
    pass


class RoundManager:
    """
    Manager that makes starting a round possible.

    When initializing the class, a Round object is needed.
    When you want to create a round, use `start_round`. This will try to
    create feedback objects for every receiver and sender associated with the round.
    It will raise NoSolutionFound if no solution could be found in a reasonable amount of tries.
    It will raise NoSolutionPossible if there is no solution possible (eg. when
    a user has no roles within a circle).
    It will raise IntegrationError when messenging integrations fail.
    """
    def __init__(self, _round):
        # Current depth of solution.
        self.counter = 0
        # Total number of solutions tried.
        self.tries = 0
        # Max solution depth achieved.
        self.max_depth = 0
        # The Round object.
        self.round = _round
        # All role feedback objects created.
        self.role_feedback = []
        # All individual feedback created.
        self.individual_feedback_to_be_received = []
        # The users giving feedback.
        self.users_giving_feedback = set(_round.participants_senders.all().values_list('id', flat=True))
        # Counter that keeps track how often a user has given feedback on roles.
        self.users_have_given_feedback_on_role = Counter()
        # Counter that keeps track how often a user has given feedback on individuals.
        self.users_have_given_feedback_on_individual = Counter()
        # Caching of users in particular circle.
        self.users_in_circle = {}
        # Keeps track on what the maximum number of feedback a user is allowed to give.
        # Will increase if no solution can be found.
        self.max_reviews_per_user = 0

    def start_round(self):
        """
        This will try to
        create feedback objects for every receiver and sender associated with the round.
        It will raise NoSolutionFound if no solution could be found in a reasonable amount of tries.
        It will raise NoSolutionPossible if there is no solution possible (eg. when
        a user has no roles within a circle).
        """
        # We do a maximum of tries that is equal to number of users giving feedback.
        logger.info('starting a new round: {}'.format(self.round))

        for i in range(self.round.min_feedback_sent + 100):
            # After every 3th try to fix the round, we increment the maximum number of reviews that needs to be given.
            self.max_reviews_per_user = self.round.roles_to_review + i // 3
            try:
                logger.info('Trying to create role feedback {}th try, max number of reviews: {}'.format(
                    i + 1, self.max_reviews_per_user)
                )
                self._create_role_feedback_for_participants()
                self._sort_feedback_on_circle_size()
                self._match_role_feedback_to_senders(self.role_feedback)
                logger.info('succesfully matched all role feedback on the {}th try'.format(i + 1))
                break
            except (NoSolutionFound, MatchNotFoundError):
                # reset the round
                self.counter = 0
                self.tries = 0
                self.max_depth = 0
                self.users_have_given_feedback_on_role = Counter()

        for i in range(10000):
            self.max_reviews_per_user = self.round.individuals_to_review + i // 3
            try:
                logger.info('Trying to create individual feedback Solution {}th try, max number of reviews: {}'.format(
                    i + 1, self.max_reviews_per_user)
                )
                self._create_individual_feedback_for_participants()
                self._match_individual_feedback_to_senders(self.individual_feedback_to_be_received)
                logger.info('succesfully matched all individual feedback on the {}th try'.format(i + 1))
                break
            except (NoSolutionFound, MatchNotFoundError):
                # reset the round
                self.counter = 0
                self.tries = 0
                self.max_depth = 0
                self.users_have_given_feedback_on_individual = Counter()

        logger.info('New round started.')

    def _create_role_feedback_for_participants(self):
        """
        This will create Feedback + FeedbackOnRole objects for every participant.
        """
        self.role_feedback = []
        for participant in self.round.participants_receivers.all():
            # TODO: FEED-60: Make sure all roles are different.
            for _ in range(self.round.roles_to_review):
                feedback = self._create_role_feedback_for_participant(participant)
                if feedback:
                    self.role_feedback.append(feedback)

    def _create_role_feedback_for_participant(self, participant):
        """
        This will create a Feedback + FeedbackOnRole object for the participant.
        Args:
            participant: User object

        Returns:
            Feedback object with FeedbackOnRole object.

        Raises:
            NoSolutionPossible: If no roles can be found for the user.
        """
        logger.info('Creating role feedback for user: {}'.format(participant))
        role = participant.role_set.order_by('?').exclude(parent_id=None).exclude(archived=True).first()
        if not role:
            raise NoSolutionPossible

        feedback_on_role = FeedbackOnRole(role=role)
        feedback = Feedback(
            actionable=False,
            round=self.round,
            recipient=participant,
            date=timezone.now(),
            role=feedback_on_role,
        )
        return feedback

    def _create_individual_feedback_for_participants(self):
        """
        This will create a Feedback + FeedbackForIndividual object for the
        participants and store it in the list
        `self.individual_feedback_to_be_received`.
        """
        self.individual_feedback_to_be_received = []

        for i in range(self.round.individuals_to_review):
            for participant in self.round.participants_receivers.all():
                question = self.round.question_for_individual_feedback
                individual = FeedbackOnIndividual(question=question)
                feedback = Feedback(
                    actionable=False,
                    round=self.round,
                    recipient=participant,
                    date=timezone.now(),
                    individual=individual,
                )
                self.individual_feedback_to_be_received.append(feedback)

    def _sort_feedback_on_circle_size(self):
        """
        This will sort the feedback objects in order of the circle size.

        This makes finding a solution easier, because finding a sender in a small circle is
        less easy than finding them in big circles, so putting small circles first in the
        matching should make finding the solution simpler.
        """
        for feedback in self.role_feedback:
            feedback._circle_size = len(self._get_senders_for_user_in_role(feedback.role.role.parent))
        sorted(
            self.role_feedback,
            key=lambda feedback: feedback._circle_size,
            reverse=True,
        )

    def _get_senders_for_user_in_role(self, circle):
        """
        Caching function for Users in circles.

        To hit the DB all little less, we cache the users in a circle.<commit_msg>set the actionable property False --> True to make it the default<commit_after>from collections import Counter
from itertools import dropwhile
import logging
from random import shuffle

from django.utils import timezone

from flindt.feedback.models import (Feedback, FeedbackOnIndividual, FeedbackOnRole)
from flindt.user.models import User

logger = logging.getLogger(__name__)


class IntegrationError(Exception):
    pass


class MatchNotFoundError(Exception):
    pass


class NoSolutionFound(Exception):
    pass


class NoSolutionPossible(Exception):
    pass


class RoundManager:
    """
    Manager that makes starting a round possible.

    When initializing the class, a Round object is needed.
    When you want to create a round, use `start_round`. This will try to
    create feedback objects for every receiver and sender associated with the round.
    It will raise NoSolutionFound if no solution could be found in a reasonable amount of tries.
    It will raise NoSolutionPossible if there is no solution possible (eg. when
    a user has no roles within a circle).
    It will raise IntegrationError when messenging integrations fail.
    """
    def __init__(self, _round):
        # Current depth of solution.
        self.counter = 0
        # Total number of solutions tried.
        self.tries = 0
        # Max solution depth achieved.
        self.max_depth = 0
        # The Round object.
        self.round = _round
        # All role feedback objects created.
        self.role_feedback = []
        # All individual feedback created.
        self.individual_feedback_to_be_received = []
        # The users giving feedback.
        self.users_giving_feedback = set(_round.participants_senders.all().values_list('id', flat=True))
        # Counter that keeps track how often a user has given feedback on roles.
        self.users_have_given_feedback_on_role = Counter()
        # Counter that keeps track how often a user has given feedback on individuals.
        self.users_have_given_feedback_on_individual = Counter()
        # Caching of users in particular circle.
        self.users_in_circle = {}
        # Keeps track on what the maximum number of feedback a user is allowed to give.
        # Will increase if no solution can be found.
        self.max_reviews_per_user = 0

    def start_round(self):
        """
        This will try to
        create feedback objects for every receiver and sender associated with the round.
        It will raise NoSolutionFound if no solution could be found in a reasonable amount of tries.
        It will raise NoSolutionPossible if there is no solution possible (eg. when
        a user has no roles within a circle).
        """
        # We do a maximum of tries that is equal to number of users giving feedback.
        logger.info('starting a new round: {}'.format(self.round))

        for i in range(self.round.min_feedback_sent + 100):
            # After every 3th try to fix the round, we increment the maximum number of reviews that needs to be given.
            self.max_reviews_per_user = self.round.roles_to_review + i // 3
            try:
                logger.info('Trying to create role feedback {}th try, max number of reviews: {}'.format(
                    i + 1, self.max_reviews_per_user)
                )
                self._create_role_feedback_for_participants()
                self._sort_feedback_on_circle_size()
                self._match_role_feedback_to_senders(self.role_feedback)
                logger.info('succesfully matched all role feedback on the {}th try'.format(i + 1))
                break
            except (NoSolutionFound, MatchNotFoundError):
                # reset the round
                self.counter = 0
                self.tries = 0
                self.max_depth = 0
                self.users_have_given_feedback_on_role = Counter()

        for i in range(10000):
            self.max_reviews_per_user = self.round.individuals_to_review + i // 3
            try:
                logger.info('Trying to create individual feedback Solution {}th try, max number of reviews: {}'.format(
                    i + 1, self.max_reviews_per_user)
                )
                self._create_individual_feedback_for_participants()
                self._match_individual_feedback_to_senders(self.individual_feedback_to_be_received)
                logger.info('succesfully matched all individual feedback on the {}th try'.format(i + 1))
                break
            except (NoSolutionFound, MatchNotFoundError):
                # reset the round
                self.counter = 0
                self.tries = 0
                self.max_depth = 0
                self.users_have_given_feedback_on_individual = Counter()

        logger.info('New round started.')

    def _create_role_feedback_for_participants(self):
        """
        This will create Feedback + FeedbackOnRole objects for every participant.
        """
        self.role_feedback = []
        for participant in self.round.participants_receivers.all():
            # TODO: FEED-60: Make sure all roles are different.
            for _ in range(self.round.roles_to_review):
                feedback = self._create_role_feedback_for_participant(participant)
                if feedback:
                    self.role_feedback.append(feedback)

    def _create_role_feedback_for_participant(self, participant):
        """
        This will create a Feedback + FeedbackOnRole object for the participant.
        Args:
            participant: User object

        Returns:
            Feedback object with FeedbackOnRole object.

        Raises:
            NoSolutionPossible: If no roles can be found for the user.
        """
        logger.info('Creating role feedback for user: {}'.format(participant))
        role = participant.role_set.order_by('?').exclude(parent_id=None).exclude(archived=True).first()
        if not role:
            raise NoSolutionPossible

        feedback_on_role = FeedbackOnRole(role=role)
        feedback = Feedback(
            actionable=True,
            round=self.round,
            recipient=participant,
            date=timezone.now(),
            role=feedback_on_role,
        )
        return feedback

    def _create_individual_feedback_for_participants(self):
        """
        This will create a Feedback + FeedbackForIndividual object for the
        participants and store it in the list
        `self.individual_feedback_to_be_received`.
        """
        self.individual_feedback_to_be_received = []

        for i in range(self.round.individuals_to_review):
            for participant in self.round.participants_receivers.all():
                question = self.round.question_for_individual_feedback
                individual = FeedbackOnIndividual(question=question)
                feedback = Feedback(
                    actionable=True,
                    round=self.round,
                    recipient=participant,
                    date=timezone.now(),
                    individual=individual,
                )
                self.individual_feedback_to_be_received.append(feedback)

    def _sort_feedback_on_circle_size(self):
        """
        This will sort the feedback objects in order of the circle size.

        This makes finding a solution easier, because finding a sender in a small circle is
        less easy than finding them in big circles, so putting small circles first in the
        matching should make finding the solution simpler.
        """
        for feedback in self.role_feedback:
            feedback._circle_size = len(self._get_senders_for_user_in_role(feedback.role.role.parent))
        sorted(
            self.role_feedback,
            key=lambda feedback: feedback._circle_size,
            reverse=True,
        )

    def _get_senders_for_user_in_role(self, circle):
        """
        Caching function for Users in circles.

        To hit the DB all little less, we cache the users in a circle.
{'fraction_non_alphanumeric': 0.0628125, 'fraction_numerical': 0.003375, 'mean_word_length': 2.2156350482315115, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 4, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5426006', 'n_tokens_mistral': 4366, 'n_tokens_neox': 3940, 'n_words': 1317}