class Controller(SafeClass): inner_name = Checker.inner_name required_data_cols = oDict([("email", "email"), ("fam", "Фамилия"), ("name", "Имя"), ("otch", "Отчество"), ("town", "Город"), ("school", "Школа"), ("team", "Команда"), ("klass", "Класс")]) _default_full_dict = required_data_cols.copy() _default_full_dict.update([("aud", "Ауд."), ("row", "Ряд"), ("col", "Место"), ("arrived", "Отметка о прибытии"), ("key", "Ключ")]) _mini_out = ["fam", "name", "otch", "aud", "row", "col"] _razdatka_cols = ["fam", "name", "otch", "klass", "row", "col", "Пришел?", "Справка?"] max_iter = 20 CHECK = ["cl7_8", "cl7_9", "cl7_10", "cl7_11", "cl8_9", "cl8_10", "cl8_11", "cl9_10", # Для виджета "cl9_11", "cl10_11", "one_school", "one_town", "com_in_one", "debug_mode"] SCALE = [{"name": "max_compart", "var": (0, 1)}] @property def settings(self): return self.checker.settings def __getitem__(self, item): return self.auds[item] def __init__(self, file, from_pickle=False): if from_pickle: data = pickle.load(file) Checker.clean_global_init(data["checker_meta"]) Seat.counters = data["seats_meta"] self.__dict__.update(data["controller"].__dict__) return self.email_handle = list() self.mode = {"people": "None"} self.last_change = None self.people = pd.DataFrame() self.auds = dict() self.inds = list() self.teams = list() self.seed = 1 found_main_settings = False excel_file = ExcelFile(file) for name in excel_file.sheet_names: raw_frame = excel_file.parse(name, index_col=None, header=None) unresolved_dict = splitter(raw_frame, named=True) if "main_settings" in unresolved_dict.keys(): if found_main_settings: raise ControllerException("Две страницы с общими настройками!") found_main_settings = True Checker.raw_global_init(unresolved_dict) self.checker = Checker() if not found_main_settings: raise TypeError("Настройки не найдены, на странице с настройками нужен ключ main_settings") for name in excel_file.sheet_names: raw_frame = excel_file.parse(name, index_col=None, header=None) unresolved_dict = splitter(raw_frame, named=True) if "main_settings" not in unresolved_dict.keys(): tmp = Auditory(unresolved_dict, outer_name=name) if tmp.inner_name in self.auds.keys(): del tmp raise TypeError("Есть одинаковые аудитории") else: self.auds[tmp.inner_name] = tmp self._message_upd() def coords_by_email(self, email) -> dict: """ Ищет место, на котором сидит участник с указанным email :param email: :return: """ for aud in self.auds.values(): try: result = aud.coords_by_email_in_aud(email) return result except KeyError: continue raise KeyError(email) def _rand_loop_insert(self, data, available): """ Рекурсивная часть :param dict data: чел :param set available: оставшиеся аудитрии на проверку :return: """ if not available: raise NoFreeAuditory("Нет свободных аудиторий") for_check = random.sample(available, 1)[0] available.remove(for_check) try: for_check.rand_insert(data) except EndLoopException: self._rand_loop_insert(data, available) def _rand_loop_team_insert(self, data, available): """ Рекурсивная часть :param dict data: :param set available: :return: """ if not available: raise NoFreeAuditory("Нет свободных аудиторий") for_check = random.sample(available, 1)[0] available.remove(for_check) try: for_check.rand_insert_team(data) except EndLoopException: self._rand_loop_team_insert(data, available) def _split_people(self): """ Блок разбивает загруженных людей в пачки команд и индивидуалов в соответствии с настройками """ self.inds = list() self.teams = list() tmp = pd.DataFrame(self.people.drop(["aud", "row", "col"], errors="ignore", axis=1)) self.inds = tmp.query("team == 'и'").to_dict(orient="records") print(self.checker.settings["com_in_one"]) if not self.checker.settings["com_in_one"]: self.inds.extend(tmp.query("team != 'и'").to_dict(orient="records")) else: for team_number in list(np.unique(tmp.query("team != 'и'")["team"])): query = "team == " + str(team_number) self.teams.append(tmp.query(query).to_dict(orient="records")) @mutable def switch_on_aud(self, audname): self.auds[audname].switch_on() @mutable def switch_off_aud(self, audname): self.auds[audname].switch_off() @mutable def load_auditory(self, file): """ Повторяющиеся загружены не будут :param file: :return: """ excel_file = ExcelFile(file) for name in excel_file.sheet_names: raw_frame = excel_file.parse(name, index_col=None, header=None) unresolved_dict = splitter(raw_frame, named=True) if "settings" in unresolved_dict.keys(): tmp = Auditory(unresolved_dict, outer_name=name) if tmp.inner_name in self.auds.keys(): del tmp else: self.auds[tmp.inner_name] = tmp @mutable def delete_auditory(self, audname): try: del self.auds[str(audname)] except KeyError: raise ControllerException("Такой аудитории не существует: {}".format(audname)) @mutable def load_emails(self, file): table = pd.read_excel(file, sheet_name=0).applymap(clr) if not self._check_settings(fact=set(table.columns), req={"email"}, way=">="): raise NotEnoughSettings(fact=set(table.columns), req={"email"}, way=">=") self.email_handle = list(table.to_dict()["email"].values()) @mutable def load_people(self, file): """ Требования к входным данным: 1) см required_data_cols 2) если с целью получить права editor, то надо дополнительно указать все места Аудитория, Ряд, Место """ people = pd.read_excel(file, sheet_name=0).applymap(clr) if not self._check_settings(fact=set(people.columns), req=set(self.required_data_cols.values()), way=">="): raise NotEnoughSettings(fact=set(people.columns), req=set(self.required_data_cols.values()), way=">=") people = people.rename(columns=swap(self._default_full_dict)) if (any([item in people.columns for item in ["aud", "row", "col"]]) and not all([item in people.columns for item in ["aud", "row", "col"]])): raise ControllerException("Некорректно заданы столбцы с местами") # Присвоение уровня доступа if len(people) == 0: self.mode["people"] = "None" # Нельзя ничего делать с рассаженными участниками return elif "aud" in people.columns: people['aud'] = np.vectorize(str)(people['aud']) self.mode["people"] = "input/edit" # Можно менять информацию, изымать и добавлять else: self.mode["people"] = "input" # Можно только добавлять self.people = people self._split_people() def rand_aud_insert_team(self, data): not_visited = set(self.auds.values()) self._rand_loop_team_insert(data=data, available=not_visited) def rand_aud_insert(self, data): not_visited = set(self.auds.values()) self._rand_loop_insert(data=data, available=not_visited) @mutable def clean_seated(self): """ Изымает абсолютно всех незаблокированных(!) участников из аудиторий :return: """ for aud in self.auds.values(): aud.clean_all() @mutable def lock_seated_on_key_by_email(self, key: str): """ Блокироует участников по ключу. Это позволяет получать определенного рода спокойствие за исполняемые опасные действия над рассаженными участниками :param key: Ключ :return: """ if not key: raise ControllerException("Некорректный ключ") if not self.email_handle: raise ControllerException("Список email-ов пуст") for email in self.email_handle: coords = self.coords_by_email(email) self.auds[coords["aud"]].lock_by_coords((coords["row"], coords["col"]), key) @mutable def unlock_seated_by_email(self): """ Разблокировывает участников по email игнорирует ключи """ if not self.email_handle: raise ControllerException("Список email-ов пуст") for email in self.email_handle: coords = self.coords_by_email(email) self.auds[coords["aud"]].unlock_by_coords((coords["row"], coords["col"])) @mutable def lock_seated_on_key(self, key: str): """ Блокироует участников по ключу. Это позволяет получать определенного рода спокойствие за исполняемые опасные действия над рассаженными участниками :param key: Ключ :return: """ if not key: raise ControllerException(key) for aud in self.auds.values(): aud.lock_all(key) @mutable def unlock_seated_by_key(self, key): """ Разблокировывает участников по ключу :param str key: Ключ :return: """ if key in self.key_holder: for aud in self.auds.values(): aud.unlock_all(key) else: raise ControllerException("Key Error %s" % key) @mutable def mark_arrival_by_email(self): """ Ставит отметку о прибытии для участников, чьи email подгружены """ if not self.email_handle: raise ControllerException("Список email-ов пуст") for email in self.email_handle: try: seat = self.coords_by_email(email) self.auds[str(seat["aud"])].mark_arrival_by_coords((seat["row"], seat["col"])) except KeyError: continue @mutable def update_seated_by_coords(self, forced=False): """ Меняет всю информацию на актуальную. Необходимо иметь места, где менять и загруженных людей(что менять) :param bool forced: менять ли людей на местах, которые заблокированы? """ if "edit" not in self.mode["people"].split("/"): # Проверка уровня доступа raise ControllerException("PermissionError") for new_data in self.people.to_dict(orient="records"): for_insert = new_data.copy() del for_insert["aud"], for_insert["row"], for_insert["col"] self.auds[new_data["aud"]].update_by_coords((new_data["row"], new_data["col"]), for_insert, forced=forced) @mutable def update_seated_by_email(self, forced=False): """ Меняет всю информацию на актуальную. Необходимо иметь обычную допустимую входную загрузку :param bool forced: менять ли людей на местах, которые заблокированы? """ if "input" not in self.mode["people"].split("/"): # Проверка уровня доступа raise ControllerException("PermissionError") for new_data in self.people.to_dict(orient="records"): for_insert = new_data.copy() coords = self.coords_by_email(for_insert["email"]) if "edit" in self.mode["people"].split("/"): del for_insert["aud"], for_insert["row"], for_insert["col"] self.auds[coords["aud"]].update_by_coords((coords["row"], coords["col"]), for_insert, forced=forced) @mutable def remove_seated_by_coords(self): """ Изымает всех людей по указанным местам. Необходимо иметь места. Не изымает людей с заблокированных мест. """ if "edit" not in self.mode["people"].split("/"): # Проверка уровня доступа raise ControllerException("PermissionError") for remove_data in self.people.to_dict(orient="records"): self.auds[remove_data["aud"]].remove_by_coords((remove_data["row"], remove_data["col"])) @mutable def remove_seated_by_email(self): """ Изымает всех людей по указанным email. Необходимо иметь подгруженные email. Не изымает с заблокированных мест. """ if not bool(self.email_handle): raise ControllerException("Список email-ов пуст") for email in self.email_handle: try: seat = self.coords_by_email(email) self.auds[seat["aud"]].remove_by_coords((seat["row"], seat["col"])) except KeyError: continue @mutable def clear_buffer(self): """ Очистить подгруженных людей и emails. Обнуляет уровень доступа. """ self.people = pd.DataFrame(columns=self.required_data_cols.keys()) self.inds = list() self.teams = list() self.email_handle = list() self.mode["people"] = "None" @mutable def place_loaded_people(self): """ Рассаживает подгруженных участников """ if "input" not in self.mode["people"].split("/"): raise ControllerException("PermissionError") if len(self.people) and len(self.seated_people): if (set(self.people["email"])) & set(self.seated_people["email"]): message = "{} загруженных участников рассажены!" message = message.format(len(set(self.people["email"]) & set(self.seated_people["email"]))) raise ControllerException(message) random.seed(self.seed) for team in self.teams: self.rand_aud_insert_team(team) for individual in self.inds: self.rand_aud_insert(individual) @mutable def refresh(self, new_settings): self.checker.refresh(new_settings) self._split_people() @property def seated_people(self) -> pd.DataFrame: """ Собирает в общую табличку всех рассаженных участников """ seated = list() for aud in sorted(self.auds.values()): seated.extend(aud.get_all_seated()) frame = pd.DataFrame.from_records(seated) return frame def comparison(self): if len(self.people) and len(self.seated_people): other = self.people.set_index("email", drop=False) seated = self.seated_people.set_index("email", drop=False) emails = set(seated.index.tolist()) & set(other.index.tolist()) not_seated = set(other.index.tolist()) - set(seated.index.tolist()) result = {"there": list(), "here": list()} for email in emails: here = seated.loc[email].to_dict() there = other.loc[email].to_dict() for key in self.required_data_cols.keys(): if clr(here[key]) != clr(there[key]): result["there"].append(there) result["here"].append(here) break result["not_seated"] = other.loc[not_seated].rename(columns=self._default_full_dict) else: result = {"there": list(), "here": list(), "not_seated": pd.DataFrame()} if len(self.email_handle) and len(self.seated_people): seated = self.seated_people.set_index("email", drop=False) result["emails_not_seated"] = pd.DataFrame(list(set(self.email_handle) - set(seated.index.tolist()))) else: result["emails_not_seated"] = pd.DataFrame() return {"here": pd.DataFrame.from_records(result["here"]).rename(columns=self._default_full_dict), "there": pd.DataFrame.from_records(result["there"]).rename(columns=self._default_full_dict), "not_seated": result["not_seated"], "emails_not_seated": result["emails_not_seated"]} @property def not_seated(self): if not len(self.people) or not len(self.seated_people): raise ControllerException("Нету людей для сравнения") other = self.people.set_index("email", drop=False) seated = self.seated_people.set_index("email", drop=False) emails = set(other.index.tolist()) - set(seated.index.tolist()) return other.loc[emails] def save_summary_to_txt(self, file): """ <Статистика по аудиториям> :param file: :return: """ message = "" for aud in sorted(self.auds.values()): message += aud.summary + "\n" file.write(message) def save_summary_to_excel(self, file): """ <Статистика по аудиториям> :param file: :return: """ with pd.ExcelWriter(file) as writer: summary = list() for aud in sorted(self.auds.values()): summary.append(aud.info) table = pd.DataFrame.from_records(summary, index="name") table.ix[:, Auditory.export_names.keys()].rename(columns=Auditory.export_names).to_excel(writer) def save_seated_to_excel(self, file, full=False): """ <Участники> [True, False] Выводит в эксель инфу по участникам, с их местами и тд :param file: куда выводить :param bool full: всю ли инфу выводить или только на стенд? :return: """ with pd.ExcelWriter(file) as writer: if not full: select = self._mini_out else: select = list(self._default_full_dict.keys()) frame = self.seated_people frame.ix[:, select].sort_values("fam", ascending=True).rename( columns=self._default_full_dict).reset_index(drop=True).to_excel(writer, "На стенд") sheet = writer.sheets["На стенд"] sheet.set_column("B:D", 15) sheet.repeat_rows(0) sheet.hide_gridlines(0) sheet.set_paper(9) def save_maps_with_data_to_excel(self, file, data): """ <Карта мест с> [email, fam, name, otch, town, school, team, klass, arrived, key] Выводит карту рассадки в эксель с необходимой информацией :param file: куда выводим :param data: какая информация нужна """ with xlsxwriter.Workbook(file) as workbook: form = workbook.add_format() form.set_align('center') form.set_bold() for aud in sorted(self.auds.values()): sheet = workbook.add_worksheet(aud.inner_name) aud.map_with_data_to_writer(sheet, form, data) sheet.set_header("&L&30 " + aud.inner_name) sheet.set_column(0, aud.shape[1], 6.5) sheet.hide_gridlines(0) if aud.inner_name.startswith("П"): sheet.set_paper(8) sheet.set_print_scale(75) else: sheet.set_paper(9) sheet.set_landscape() sheet.fit_to_pages(1, 1) sheet.set_page_view() def save_maps_with_status_to_excel(self, file): """ <Карта с местами> Выводит карту рассадка с пропечатанными местами в эксель :param file: куда выводим """ with xlsxwriter.Workbook(file) as workbook: form = workbook.add_format() form.set_align('center') form.set_bold() for aud in sorted(self.auds.values()): sheet = workbook.add_worksheet(aud.inner_name) aud.map_with_status_to_writer(sheet, form) sheet.set_header("&L&30 " + aud.inner_name) sheet.hide_gridlines(0) sheet.set_column(0, aud.shape[1], 6.5) if aud.inner_name.startswith("П"): sheet.set_paper(8) sheet.set_print_scale(75) else: sheet.set_paper(9) sheet.set_landscape() sheet.fit_to_pages(1, 1) sheet.set_page_view() def save_razdatka_to_excel(self, file): """ <Раздатка> Выводит списки с участниками в каждой аудитории в ексель :param file: куда выводим :return: """ with pd.ExcelWriter(file) as writer: for aud in sorted(self.auds.values()): aud.people_table.ix[:, self._razdatka_cols].sort_values("fam", ascending=True).rename( columns=self._default_full_dict).to_excel( writer, aud.inner_name, index=False) sheet = writer.sheets[aud.inner_name] sheet.set_column("A:C", 15) sheet.set_column("D:H", 9) sheet.set_margins(left=0.2, right=0.2) sheet.repeat_rows(0) sheet.hide_gridlines(0) sheet.set_paper(9) sheet.set_header("&L&30 " + aud.inner_name) sheet.set_page_view() def to_pickle(self, file): """ Сохраняемся вместе с классовой инфой :param file: куда сохраняемся """ prepared = dict([("checker_meta", Checker.settings), ("seats_meta", Seat.counters), ("controller", self)]) pickle.dump(prepared, file) @property def key_holder(self): return np.unique(reduce(list.__add__, [aud.keys for aud in self.auds.values()], [])) @property def info(self): def s(x, subset, condition=None): if not len(x): return set() else: if not condition: return set(x.ix[:, subset]) else: return set(x.query(condition).ix[:, subset]) info = dict() info["last_change"] = self.last_change info["n_auds"] = len(self.auds) info["n_used_auds"] = sum([aud.settings["available"] for aud in self.auds.values()]) info["seated_teams"] = len(reduce(lambda x, y: x.union(y), [aud.teams_set for aud in self.auds.values()])) info["arrived_teams"] = len(reduce(lambda x, y: x.union(y), [aud.teams_arrived_set for aud in self.auds.values()])) info["seats_available"] = sum([aud.capacity for aud in self.auds.values() if aud.settings["available"]]) info["seated"] = Seat.total_seated() info["seats_total"] = sum([aud.capacity for aud in self.auds.values()]) info["arrived"] = Seat.total_arrived() info["mode"] = self.mode["people"] info["emails"] = len(self.email_handle) info["people"] = len(self.people) info["intersect_teams"] = len(s(self.people, "team", condition="team != 'и'") & s(self.seated_people, "team")) info["intersect_people"] = len(s(self.people, "email") & s(self.seated_people, "email")) info["intersect_emails"] = len(set(self.email_handle) & s(self.seated_people, "email")) info["n_teams"] = len(self.teams) all_keys = reduce(list.__add__, [aud.keys for aud in self.auds.values()], []) key, frequency = np.unique(all_keys, return_counts=True) info["keys"] = dict(zip(key, frequency)) return info def __str__(self): return self._message def update(self): self.last_change = datetime.datetime.now().strftime("%A, %d. %B %Y %I:%M%p") self._message_upd() def _message_upd(self): self._message = """ Последнее изменение {last_change} Режим -{mode}- Временные списки для взаимодействия с рассаженными Загружено(сидит) человек {people:<5}({intersect_people}) команд {n_teams:<5}({intersect_teams}) emails {emails:<5}({intersect_emails}) -------------------------- Доступно(всего) аудиторий {n_used_auds:<5}({n_auds}) мест {seats_available:<5}({seats_total}) Посажено(пришло) человек {seated:<5}({arrived}) команд {seated_teams:<5}({arrived_teams}) Ключи блокировки мест {{ключ: количество}} {keys}""".format(**self.info)