Newer
Older
from enum import Enum
from random import shuffle
Jakub Štercl
committed
from PyQt5.QtCore import pyqtSlot, QThread, pyqtSignal
from PyQt5.QtGui import QMovie
from PyQt5.QtWidgets import QWidget, QMessageBox, QLabel
Jakub Štercl
committed
from controllers.base_controller import BaseController
Jakub Štercl
committed
from controllers.requirement_dialog import NewRequirementDialog, RequirementEditDialog
from model.requirements import Requirement
from utils.distribution_maker import DistributionMaker
from utils.widgets.requirements_table import RequirementsTable
Jakub Štercl
committed
from windows import distributionsetup
Jakub Štercl
committed
class EmptyGroupError(Exception):
pass
Jakub Štercl
committed
class DistributionSetup(BaseController, QWidget, distributionsetup.Ui_Form):
Jakub Štercl
committed
def __init__(self, main_window, group, parent=None):
"""
:param main_window: main window controller
:param group: (Group) that is being distributed
Jakub Štercl
committed
:param parent: parent widget for qt
Jakub Štercl
committed
"""
Jakub Štercl
committed
if len(group.members) == 0:
raise EmptyGroupError()
Jakub Štercl
committed
super(DistributionSetup, self).__init__(parent)
self.setupUi(self)
self.tableRequirements = RequirementsTable(
2,
({'text': "", 'tooltip': self.tr("Upravit"), 'icon': ":/icons/edit.svg"},
{'text': "", 'tooltip': self.tr("Smazat omezení"), 'icon': ":/icons/minus.svg"},)
)
self.placeTable.insertWidget(1, self.tableRequirements)
Jakub Štercl
committed
self.tableRequirements.btnClicked.connect(self.onTableRequirementsBtnClicked)
self.tableRequirements.itemDoubleClicked.connect(self.editRequirement)
Jakub Štercl
committed
self.group = group
Jakub Štercl
committed
# setup for multithreading
self.dist_maker = None
self.lbl_load = QLabel()
self.lbl_load.setMovie(QMovie(":/icons/loader.gif"))
self.lbl_load.movie().start()
Jakub Štercl
committed
# set up header
self.lblGroupName.setText(self.group.name)
Jakub Štercl
committed
self.lblMemberCount.setText('(' + str(self.group.member_count) + ' členů)')
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)
Jakub Štercl
committed
# set up spinners
Jakub Štercl
committed
self.spinTeamCount.setMinimum(1)
Jakub Štercl
committed
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)
Jakub Štercl
committed
Jakub Štercl
committed
# set up checkboxes
self.cbIgnoreTeamCount.stateChanged.connect(self.spinTeamCount.setDisabled)
self.cbIgnoreMinSize.stateChanged.connect(self.spinMinTeamSize.setDisabled)
self.cbIgnoreMaxSize.stateChanged.connect(self.spinMaxTeamSize.setDisabled)
Jakub Štercl
committed
self.btnAddRequirement.clicked.connect(self.showRequirementDialog)
Jakub Štercl
committed
self.btnDistribute.clicked.connect(self.distribute)
Jakub Štercl
committed
@pyqtSlot()
Jakub Štercl
committed
def showRequirementDialog(self):
Jakub Štercl
committed
"""
display dialog that allows creating of requirement
then, after user creates the requirement insert it into requirements table
"""
Jakub Štercl
committed
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)
Jakub Štercl
committed
Jakub Štercl
committed
@pyqtSlot(Requirement, int)
def onTableRequirementsBtnClicked(self, requirement, btn_nr):
Jakub Štercl
committed
"""
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)
"""
Jakub Štercl
committed
if btn_nr == 0:
# it's btn "edit"
self.editRequirement(requirement)
elif btn_nr == 1:
# it's btn "delete"
self.deleteRequirement(requirement)
Jakub Štercl
committed
Jakub Štercl
committed
def editRequirement(self, requirement):
Jakub Štercl
committed
"""
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
"""
Jakub Štercl
committed
self.tableRequirements.model.sourceModel().removeRequirement(requirement)
dialog = RequirementEditDialog(self.group, requirement)
Jakub Štercl
committed
dialog.exec_()
if dialog.result() == dialog.Accepted:
Jakub Štercl
committed
if dialog.requirement is not None:
self.tableRequirements.insertRequirement(dialog.requirement)
else:
self.tableRequirements.insertRequirement(requirement)
Jakub Štercl
committed
def deleteRequirement(self, requirement):
Jakub Štercl
committed
"""
show confirmation dialog, then delete requirement
:param requirement: requirement to be deleted
"""
msg = QMessageBox(self)
msg.setIcon(QMessageBox.Question)
Jakub Štercl
committed
msg.setWindowTitle("Odebrat omezení?")
msg.setText("Chcete odebrat toto omezení?")
Jakub Štercl
committed
msg.setStandardButtons(QMessageBox.Ok | QMessageBox.Cancel)
if msg.exec() == QMessageBox.Ok:
Jakub Štercl
committed
self.tableRequirements.model.sourceModel().removeRequirement(requirement)
def distribute(self):
Jakub Štercl
committed
"""
create distribution with set parameters and requirements in requirements table
if successful, move to DistributionOverview, else show error message
"""
Jakub Štercl
committed
self.dist_maker = DistributeThread(
self.group.members,
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()
Jakub Štercl
committed
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.dist_maker = None
Jakub Štercl
committed
self.horizontalLayout.replaceWidget(self.lbl_load, self.btnDistribute)
self.btnDistribute.show()
self.lbl_load.hide()
if len(violated_requirements) > 0:
Jakub Štercl
committed
msg = QMessageBox(self)
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)."))
Jakub Štercl
committed
btn_back = msg.addButton(self.tr("Upravit požadavky"), QMessageBox.RejectRole)
Jakub Štercl
committed
msg.exec_()
if msg.clickedButton() == btn_show_anyways:
self.main_window.goToDistributionOverview(self.createTeamsDict(teams), self.group)
if msg.clickedButton() == btn_repeat:
return self.distribute()
Jakub Štercl
committed
else:
self.main_window.goToDistributionOverview(self.createTeamsDict(teams), self.group)
def createDetailedText(self, requirements):
Jakub Štercl
committed
"""
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
Jakub Štercl
committed
def returningToPrevious(self):
"""
stop searching for valid distribution
"""
if self.dist_maker is not None:
self.dist_maker.terminate()
Jakub Štercl
committed
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
Jakub Štercl
committed
# 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
Jakub Štercl
committed
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