Skip to content
Snippets Groups Projects
distribution_setup_controller.py 12.2 KiB
Newer Older
from random import shuffle

from PyQt5.QtCore import pyqtSlot, QThread, pyqtSignal
from PyQt5.QtGui import QMovie
from PyQt5.QtWidgets import QWidget, QMessageBox, QLabel
from controllers.base_controller import BaseController
from controllers.requirement_dialog import NewRequirementDialog, RequirementEditDialog
Jakub Štercl's avatar
Jakub Štercl committed
from model.requirements import Requirement
from utils.distribution_maker import DistributionMaker
from utils.widgets.requirements_table import RequirementsTable
class DistributionSetup(BaseController, QWidget, distributionsetup.Ui_Form):
    def __init__(self, main_window, group, parent=None):
        """
        :param main_window: main window controller 
        :param group: (Group) that is being distributed
        if len(group.members) == 0:
            raise EmptyGroupError()

        super(DistributionSetup, self).__init__(parent)
        self.setupUi(self)
        self.main_window = main_window
        self.tableRequirements = RequirementsTable(
            2,
            ({'text': "", 'tooltip': self.tr("Upravit"), 'icon': ":/icons/edit.svg"},
             {'text': "", 'tooltip': self.tr("Smazat omezení"), 'icon': ":/icons/minus.svg"},)
        )
Jakub Štercl's avatar
Jakub Štercl committed
        self.placeTable.insertWidget(1, self.tableRequirements)
        self.tableRequirements.btnClicked.connect(self.onTableRequirementsBtnClicked)
        self.tableRequirements.itemDoubleClicked.connect(self.editRequirement)
        # setup for multithreading
        self.dist_maker = None
        self.lbl_load = QLabel()
        self.lbl_load.setMovie(QMovie(":/icons/loader.gif"))
        self.lbl_load.movie().start()

        # set up header
        self.lblGroupName.setText(self.group.name)
        self.lblMemberCount.setText('(' + str(self.group.member_count) + ' členů)')
Jakub Štercl's avatar
Jakub Štercl committed
        # set up comboPrefer
        self.comboPrefer.setItemData(0, None)
        self.comboPrefer.setItemData(1, DistributionMaker.PREFER_SAME_TEAMS)
        self.comboPrefer.setItemData(2, DistributionMaker.PREFER_DIFFERENT_TEAMS)

        self.spinTeamCount.setMaximum(self.group.member_count)
        self.spinMinTeamSize.setMinimum(0)
        self.spinMinTeamSize.setMaximum(self.group.member_count)
        self.spinMaxTeamSize.setMinimum(1)
        self.spinMaxTeamSize.setMaximum(self.group.member_count)
        self.spinMaxTeamSize.setValue(self.group.member_count)
        # set up checkboxes
        self.cbIgnoreTeamCount.stateChanged.connect(self.spinTeamCount.setDisabled)
        self.cbIgnoreMinSize.stateChanged.connect(self.spinMinTeamSize.setDisabled)
        self.cbIgnoreMaxSize.stateChanged.connect(self.spinMaxTeamSize.setDisabled)

        self.btnAddRequirement.clicked.connect(self.showRequirementDialog)
        self.btnDistribute.clicked.connect(self.distribute)
        """
        display dialog that allows creating of requirement
        then, after user creates the requirement insert it into requirements table
        """
        dialog = NewRequirementDialog(self.group, self.tableRequirements.model.sourceModel().requirements, parent=self)
        dialog.exec_()
        if dialog.result() == dialog.Accepted:
            if dialog.requirement is not None:
                self.tableRequirements.insertRequirement(dialog.requirement)
    @pyqtSlot(Requirement, int)
    def onTableRequirementsBtnClicked(self, requirement, btn_nr):
        """
        check which button was clicked and act accordingly
        :param requirement: selected requirement
        :param btn_nr: column of clicked button (0 is the first column with buttons, not first column in table)
        """
        if btn_nr == 0:
            # it's btn "edit"
            self.editRequirement(requirement)
        elif btn_nr == 1:
            # it's btn "delete"
            self.deleteRequirement(requirement)
        """
        remove requirement from requirement table,
        open dialog that allows editting of requirement 
        then, after the dialog is done, reinsert the requirement into table
        :param requirement: requirement that we're editting
        """
        self.tableRequirements.model.sourceModel().removeRequirement(requirement)
        dialog = RequirementEditDialog(self.group, requirement)
        dialog.exec_()
        if dialog.result() == dialog.Accepted:
            if dialog.requirement is not None:
                self.tableRequirements.insertRequirement(dialog.requirement)
        else:
Jakub Štercl's avatar
Jakub Štercl committed
            self.tableRequirements.insertRequirement(requirement)
        """
        show confirmation dialog, then delete requirement
        :param requirement: requirement to be deleted
        """
        msg = QMessageBox(self)
        msg.setIcon(QMessageBox.Question)
        msg.setWindowTitle("Odebrat omezení?")
        msg.setText("Chcete odebrat toto omezení?")
        msg.setStandardButtons(QMessageBox.Ok | QMessageBox.Cancel)
        if msg.exec() == QMessageBox.Ok:
            self.tableRequirements.model.sourceModel().removeRequirement(requirement)

    def distribute(self):
        """
        create distribution with set parameters and requirements in requirements table
        if successful, move to DistributionOverview, else show error message
        """
            self.tableRequirements.getAllRequirements(),
            Violated,
            self.spinTeamCount.value() if not self.cbIgnoreTeamCount.isChecked() else None,
            self.spinMinTeamSize.value() if not self.cbIgnoreMinSize.isChecked() else None,
            self.spinMaxTeamSize.value() if not self.cbIgnoreMaxSize.isChecked() else None,
            self.comboPrefer.currentData()
        self.dist_maker.start()
        self.dist_maker.result.connect(self.distributionFound)
        # hide button distribute and display 'loading' instead
        self.horizontalLayout.replaceWidget(self.btnDistribute, self.lbl_load)
        self.btnDistribute.hide()
        self.lbl_load.show()

    @pyqtSlot(list, list)
    def distributionFound(self, teams, violated_requirements):
        """
        show found distribution (if it fits all requirements) or show dialog with violated requirements
        :param teams: list of teams in distribution
        :param violated_requirements: list of violated requirements
        """
        self.horizontalLayout.replaceWidget(self.lbl_load, self.btnDistribute)
        self.btnDistribute.show()
        self.lbl_load.hide()
        if len(violated_requirements) > 0:
            msg.setWindowTitle(self.tr("Rozdělení se nepodařilo nalézt"))
            msg.setText(self.tr("Rozdělení s požadovanými parametry se nepodařilo nalézt!"))
            detailed_text = self.createDetailedText(violated_requirements)
            msg.setDetailedText(self.tr("Nalezené rozdělení porušuje následující požadavky:\n") + detailed_text)
            btn_show_anyways = msg.addButton(self.tr("Přesto zobrazit"), QMessageBox.AcceptRole)
            btn_repeat = msg.addButton(self.tr("Zkusit znovu"), QMessageBox.NoRole)
            btn_repeat.setToolTip(self.tr("Pokud si myslíte, že takové rozdělení by mělo existovat, zvolte tuto možnost.\n"
                                          "Aplikace se pokusí rozdělení nalézt znovu (výsledek se může lišit)."))
            btn_back = msg.addButton(self.tr("Upravit požadavky"), QMessageBox.RejectRole)
            if msg.clickedButton() == btn_show_anyways:
                self.main_window.goToDistributionOverview(self.createTeamsDict(teams), self.group)
            if msg.clickedButton() == btn_repeat:
                return self.distribute()
        else:
            self.main_window.goToDistributionOverview(self.createTeamsDict(teams), self.group)

    def createDetailedText(self, requirements):
        """
        create detailed text for dialog with violated requirements
        :param requirements: list of violated requirements, that will show in the detailed text
        """
        res = ''
        for requirement in requirements:
            if isinstance(requirement, Requirement):
                res += requirement.member.name + " " + self.tr(requirement.keyword) + self.tr(" být v týmu s: ")
                for member_with in requirement.target_members:
                    res += member_with.name + ", "
            else:
                if requirement == Violated.TEAM_COUNT:
                    res += self.tr("Počet týmů = ") + str(self.spinTeamCount.value())
                elif requirement == Violated.MIN_TEAM_SIZE:
                    res += self.tr("Minimální velikost týmu = ") + str(self.spinMinTeamSize.value())
                elif requirement == Violated.MAX_TEAM_SIZE:
                    res += self.tr("Maximální velikost týmu = ") + str(self.spinMaxTeamSize.value())
            res += "\n"
        return res

    def createTeamsDict(self, teams):
        """
        transform list of teams into dict
        :param teams: list of lists (or sets or whatever iterable) of members [{Member, Member ...}, {Member, ...}, ...]
        :return: dict of teams {"Team 1": [Member, Member ...], "Team 2": [Member, ...], ...}
        """
        res = {}
        shuffle(teams)
        for team_nr, team in enumerate(teams):
            res[self.tr("Tým") + " " + str(team_nr + 1)] = list(teams[team_nr])
        return res

    def returningToPrevious(self):
        """
        stop searching for valid distribution
        """
        if self.dist_maker is not None:
            self.dist_maker.terminate()
class DistributeThread(QThread):
    # signal with results from DistributionMaker
    # params: list of teams in distribution, list of violated requirements
    result = pyqtSignal(list, list)

    def __init__(self, members, requirements, violated_enum, team_count, min_team_size, max_team_size, prefer):
        """
        QThread for finding distribution
        :param members: list of members to be distributed
        :param requirements: list of requirements for the distribution
        :param violated_enum: enum for values to put in violated requirements instead of:
                team_count, min_team_size and max_team_size
        :param team_count: int for how many teams should there be in distribution, None if not specified 
        :param min_team_size: int for what is the minimum size of team in distribution, None if not specified
        :param max_team_size: int for what is the maximum size of team in distribution, None if not specified
        :param prefer: DistributionMaker.PREFER_SAME_TEAMS, DistributionMaker.PREFER_DIFFERENT_TEAMS or None
        """
        super(DistributeThread, self).__init__()
        self.members = members
        self.requirements = requirements
        self.violated_enum = violated_enum
        self.team_count = team_count
        self.min_team_size = min_team_size
        self.max_team_size = max_team_size
        self.prefer = prefer
        # a little hack, because sqlite doesn't support multithreading well
        # (in python at all: https://docs.python.org/3/library/sqlite3.html#multithreading)
        # initiate all history_count values in member (it's lazy initialization)
        # so that when we ask for history count, it's not taken from db, but already is there
        for member in members:
            member.history_count

    def __del__(self):
        self.wait()

    def run(self):
        dist_maker = DistributionMaker(
            self.members,
            self.requirements,
            self.violated_enum,
            self.team_count,
            self.min_team_size,
            self.max_team_size,
            self.prefer
        )
        teams = dist_maker.distribute()
        violated_requirements = dist_maker.getViolatedRequirements()
        self.result.emit(teams, violated_requirements)


class Violated(Enum):
    """
    Enum for symbolizing which requirement was violated
    """
    TEAM_COUNT = 1
    MIN_TEAM_SIZE = 2
    MAX_TEAM_SIZE = 3