from flask import flash, redirect, request, url_for from flask_admin import Admin as FlaskAdmin from flask_admin import expose from flask_admin.contrib.sqla.fields import QuerySelectField from flask_admin.menu import MenuLink from flask_admin.model.template import EndpointLinkRowAction from flask_login import current_user from flask_security.changeable import admin_change_password from flask_wtf import FlaskForm from markupsafe import Markup from wtforms.validators import DataRequired from .advlabdb_independent_funs import ( deep_getattr, experiment_marks_missing_formatter, flashRandomPassword, str_formatter, ) from .custom_classes import ( SecureAssistantBaseView, SecureAssistantIndexView, SecureAssistantModelView, ) from .exceptions import ModelViewException from .forms import assistant_group_experiment_form_factory from .model_dependent_funs import ( generate_new_password_field, parse_selection_mark_field, user_info_fields, ) from .model_independent_funs import randomPassword, reportBadAttempt from .models import Assistant, GroupExperiment, Semester, SemesterExperiment, User, db assistantSpace = FlaskAdmin( name="Assistant@AdvLabDB", url="/assistant", template_mode="bootstrap4", index_view=SecureAssistantIndexView(name="Home", url="/assistant", endpoint="assistant"), ) class AssistantGroupExperimentView(SecureAssistantModelView): def is_accessible(self): if not super().is_accessible(): return False active_semester = current_user.active_semester if active_semester.done: semester_changed = current_user.set_last_semester_as_active() if not semester_changed: flash( Markup( f"Active semester {active_semester} is set as done. Therefore, you are not allowed to view or edit any marks in this semester. You should change your active semester in user settings if possible." ), "danger", ) return False return True column_display_actions = True column_list = [ "semester_experiment.experiment", "group.number", "group.part_students", "appointments", "experiment_marks_missing", "note", ] column_labels = { "semester_experiment.experiment": "Experiment", "group.number": "Group number", "group.part_students": "Students", } column_default_sort = ("experiment_marks_missing", True) column_extra_row_actions = [ EndpointLinkRowAction( icon_class="fa fa-pencil", endpoint="assistant_group_experiment.form_view", title="Edit", ) ] @staticmethod def part_students_formatter(view, context, model, name): part_students = deep_getattr(model, name) if part_students is not None: return ", ".join(str(part_student.student) for part_student in part_students) return "" @staticmethod def appointments_formatter(view, context, model, name): appointments = deep_getattr(model, name) if appointments is not None: return ", ".join(str(appointment.date) for appointment in appointments) return "" column_formatters = { "semester_experiment.experiment": str_formatter, "group.part_students": part_students_formatter, "appointments": appointments_formatter, "experiment_marks_missing": experiment_marks_missing_formatter, } def query_modifier(self, query): return ( query.join(SemesterExperiment) .where(SemesterExperiment.semester == current_user.active_semester) .join(SemesterExperiment.assistants) .where(Assistant.user == current_user) ) @expose("/form/", methods=("GET", "POST")) def form_view(self): group_experiment_id_str = request.args.get("id") try: group_experiment = db.session.get(GroupExperiment, int(group_experiment_id_str)) except Exception: red = url_for("main.index") + "assistant/group_experiment" flash("No valid group experiment id") return redirect(red) if group_experiment not in self.get_query(): reportBadAttempt("Assistant {current_user} tried to edit {group_experiment}") self.handle_view_exception(ModelViewException("Unauthorized action!")) return redirect(self.url) form, appointments, experiment_marks = assistant_group_experiment_form_factory(current_user, group_experiment) num_appointments = len(appointments) appointment_fields = [ getattr(form, f"appointment_{appointment_num}") for appointment_num in range(1, num_appointments + 1) ] num_experiment_marks = len(experiment_marks) experiment_mark_students = [experiment_mark.part_student.student for experiment_mark in experiment_marks] oral_experiment_mark_fields = [ getattr(form, f"oral_experiment_mark_{experiment_mark_num}") for experiment_mark_num in range(1, num_experiment_marks + 1) ] protocol_experiment_mark_fields = [ getattr(form, f"protocol_experiment_mark_{experiment_mark_num}") for experiment_mark_num in range(1, num_experiment_marks + 1) ] if form.validate_on_submit(): any_final_experiment_mark_changed = False try: for ind, appointment in enumerate(appointments): appointment.date = appointment_fields[ind].data for ind, experiment_mark in enumerate(experiment_marks): form_oral_mark = parse_selection_mark_field(oral_experiment_mark_fields[ind]) form_protocol_mark = parse_selection_mark_field(protocol_experiment_mark_fields[ind]) if ( form_oral_mark != experiment_mark.oral_mark or form_protocol_mark != experiment_mark.protocol_mark ): final_experiment_mark_changed = experiment_mark.set_oral_protocol_mark( form_oral_mark, form_protocol_mark, call_update_experiment_marks_missing=False, ) if final_experiment_mark_changed: any_final_experiment_mark_changed = True experiment_mark.assistant = current_user.assistant if any_final_experiment_mark_changed: group_experiment.update_experiment_marks_missing() group_experiment.note = form.note.data db.session.commit() red = url_for("main.index") + "assistant/group_experiment" return redirect(red) except Exception as ex: flash(str(ex), "error") db.session.rollback() final_experiment_marks = [experiment_mark.final_experiment_mark for experiment_mark in experiment_marks] return self.render( "assistant_group_experiment_form.jinja.html", form=form, semester_experiment=group_experiment.semester_experiment, group_number=group_experiment.group.number, appointment_fields=appointment_fields, experiment_mark_zip=zip( experiment_mark_students, oral_experiment_mark_fields, protocol_experiment_mark_fields, final_experiment_marks, ), ) class AssistantUserView(SecureAssistantModelView): class EditForm(FlaskForm): @staticmethod def semesterQueryFactory(): # Show only last two semesters to assistants return Semester.query.order_by(Semester.id.desc()).where(Semester.done is False).limit(2) active_semester = QuerySelectField( "Active Semester", query_factory=semesterQueryFactory, validators=[DataRequired()], default=Semester.lastSemester, description="You should change the active semester to the last semester. Do not forget to click save! Only last two semesters are shown that are not set as done.", ) phone_number, mobile_phone_number, building, room = user_info_fields() generate_new_password = generate_new_password_field() can_edit = True can_view_details = True column_display_actions = True column_sortable_list: list[str] = [] column_list = [ "email", "phone_number", "mobile_phone_number", "building", "room", "assistant.semester_experiments", ] column_labels = { "assistant.semester_experiments": "Semester Experiments", } def query_modifier(self, query): return query.where(User.id == current_user.id) def on_model_change(self, form, model, is_created): if form.generate_new_password.data: password = randomPassword() flashRandomPassword(model.email, password) admin_change_password(model, password, notify=False) # Password is automatically hashed with this function class AssistantDocsView(SecureAssistantBaseView): @expose("/") def index(self): return self.render("docs/docs.jinja.html", role="assistant") def init_assistant_model_views(app): assistantSpace.add_view(AssistantGroupExperimentView(GroupExperiment, url="group_experiment")) assistantSpace.add_view(AssistantDocsView(name="Docs", url="docs")) # Don't add to menu # Has to be placed before assistantSpace.init_app assistantSpace._views.append(AssistantUserView(User, url="user")) assistantSpace.add_link(MenuLink(name="User settings", url="/user-settings")) assistantSpace.add_link(MenuLink(name="Logout", url="/logout")) assistantSpace.init_app(app)