from flask import flash, request, url_for from flask_admin.contrib.sqla.fields import QuerySelectField, QuerySelectMultipleField from flask_admin.contrib.sqla.filters import BaseSQLAFilter from flask_admin.helpers import get_form_data from flask_admin.menu import MenuLink from flask_admin.model.template import EndpointLinkRowAction from flask_security import admin_change_password, current_user, hash_password from sqlalchemy import func, or_, and_ from wtforms import BooleanField, Form, RadioField, SelectField, TextField from wtforms.fields.html5 import DateField from wtforms.validators import URL, DataRequired, Email, NumberRange, Optional from advlabdb import adminSpace, app, assistantSpace, db, user_datastore from advlabdb.configUtils import getConfig from advlabdb.customClasses import SecureAdminModelView from advlabdb.exceptions import DataBaseException, ModelViewException from advlabdb.models import ( Appointment, Assistant, Experiment, ExperimentMark, Group, GroupExperiment, Part, PartStudent, Program, Role, Semester, SemesterExperiment, Student, User, ) from advlabdb.utils import ( initActiveSemesterMenuLinks, randomPassword, setUserActiveSemester, userActiveSemester, ) def semesterExperimentQueryFactory(): return SemesterExperiment.query.filter(SemesterExperiment.semester == userActiveSemester()) class UserView(SecureAdminModelView): class CreateForm(Form): def roleQueryFactory(): return Role.query def semesterQueryFactory(): return Semester.query def activeSemesterDefault(): return userActiveSemester() email = TextField("Email", validators=[DataRequired(), Email()]) roles = QuerySelectMultipleField( "Roles", query_factory=roleQueryFactory, validators=[DataRequired()], default=[Role.query.filter(Role.name == "assistant").first()], ) first_name = TextField("First Name", validators=[DataRequired()]) last_name = TextField("Last Name", validators=[DataRequired()]) phone_number = TextField("Phone Number") mobile_phone_number = TextField("Mobile Phone Number") room = TextField("Room") building = TextField("Building") semester_experiments = QuerySelectMultipleField( "Semester Experiments", query_factory=semesterExperimentQueryFactory, allow_blank=True, blank_text="-", description="Only needed if the user has the assistant role", ) active = BooleanField("Active", default=True) active_semester = QuerySelectField( "Active Semester", query_factory=semesterQueryFactory, allow_blank=True, blank_text="-", default=activeSemesterDefault, description="Not fixed and users (including assistants) can change it.", ) class EditForm(CreateForm): semester_experiments = None generate_new_password = BooleanField("Generate new random password", default=False) form = EditForm can_view_details = True column_list = ["first_name", "last_name", "email", "active", "roles", "assistant", "active_semester"] column_details_list = column_list + [ "phone_number", "mobile_phone_number", "room", "building", "create_datetime", "update_datetime", ] column_searchable_list = ["first_name", "last_name", "email"] column_filters = ["active", "active_semester"] column_editable_list = ["active"] def create_form(self, obj=None): form = self.CreateForm return form(get_form_data(), obj=obj) def flashPassword(password): flash(f"Random password: {password}", category="warning") def create_model(self, form): try: password = randomPassword() hashedPassword = hash_password(password) email = form.email.data.lower() roles = [role.name for role in form.roles.data] model = user_datastore.create_user( email=email, password=hashedPassword, roles=roles, first_name=form.first_name.data, last_name=form.last_name.data, phone_number=form.phone_number.data, mobile_phone_number=form.mobile_phone_number.data, room=form.room.data, building=form.building.data, active=form.active.data, active_semester=form.active_semester.data, ) if "assistant" in roles: assistant = Assistant(user=model, semester_experiments=form.semester_experiments.data) self.session.add(assistant) self.on_model_change(form, model, True) self.session.commit() except Exception as ex: flash(str(ex), "error") self.session.rollback() else: flash( f"{email} registered with roles: {', '.join([role.name for role in form.roles.data])}.", category="success", ) UserView.flashPassword(password) self.after_model_change(form, model, True) return model def on_model_delete(self, model): if model == current_user: raise ModelViewException("Tried to delete yourself as user!") def on_model_change(self, form, model, is_created): if not is_created: if model == current_user and not form.active.data: raise ModelViewException("Tried to deactiavte yourself as user!") if hasattr(form, "generate_new_password") and form.generate_new_password.data: password = randomPassword() UserView.flashPassword(password) admin_change_password( model, password, notify=False ) # Password is automatically hashed with this method class RoleView(SecureAdminModelView): can_create = False can_edit = False can_delete = False column_display_actions = False can_export = False can_set_page_size = False column_list = ["name", "description", "permissions", "update_datetime"] class SemesterView(SecureAdminModelView): class CreateForm(Form): def labelDefault(): if userActiveSemester().label == "WS": return "SS" else: return "WS" def yearDefault(): activeSemester = userActiveSemester() if activeSemester.label == "WS": return activeSemester.year + 1 else: return activeSemester.year label = RadioField("Semester", choices=["WS", "SS"], validators=[DataRequired()], default=labelDefault) year = TextField("Year", validators=[DataRequired()], default=yearDefault) transfer_parts = BooleanField( "Transfer parts", description="This option transfers the parts you have in your current active semester. Make sure that your semester is the last semester before creating a new one (recommended)!", default=True, ) transfer_assistants = BooleanField( "Transfer Assistants", description="This option transfers assistants of your active semester to active experiments in the new semester. Make sure that your semester is the last semester before creating a new one (recommended)! Active experiments are transfered anyway. If you do not want an experiment to be transfered, set it to inactive before creating the new semester. Experiments which are switched to active before creating the new semester will be created in the new semester without assistants. It is important to check the assistants of all experiments after creating a new semester.", default=True, ) form = CreateForm can_edit = False can_delete = False can_view_details = True column_list = ["label", "year", "parts"] column_details_list = column_list + ["semester_experiments", "active_users", "groups"] column_searchable_list = ["label", "year"] def customCreateModel(self, form): return Semester.customInit( label=form.label.data, year=form.year.data, oldSemester=userActiveSemester(), transferParts=form.transfer_parts.data, transferAssistants=form.transfer_assistants.data, ) def addMenuLink(space, newSemester): space.add_link( MenuLink( name=newSemester.repr(), url=url_for("set_semester") + "?semester_id=" + str(newSemester.id), category="Active semester", ) ) def after_model_change(self, form, model, is_created): setUserActiveSemester(model.id) SemesterView.addMenuLink(adminSpace, model) SemesterView.addMenuLink(assistantSpace, model) def programQueryFactory(): return Program.query class PartView(SecureAdminModelView): can_view_details = True column_sortable_list = [] column_list = ["program", "number", "semester"] column_details_list = column_list + ["part_students"] form_columns = ["program", "number"] form_extra_fields = { "program": QuerySelectField( "Program", query_factory=programQueryFactory, validators=[DataRequired()], allow_blank=True, blank_text="-" ) } def queryFilter(self): return Part.semester == userActiveSemester() def customCreateModel(self, form): return Part(program=form.program.data, number=form.number.data, semester=userActiveSemester()) class StudentView(SecureAdminModelView): can_view_details = True column_list = ["student_number", "first_name", "last_name", "uni_email", "contact_email", "part_students"] column_descriptions = {"contact_email": "The preferred contact email address if entered by the student"} column_details_list = column_list + ["bachelor_thesis", "bachelor_thesis_work_group", "note"] column_sortable_list = ["student_number", "first_name", "last_name"] column_searchable_list = column_sortable_list + ["uni_email", "contact_email"] form_excluded_columns = ["part_students"] form_args = { "uni_email": {"validators": [Email()]}, "contact_email": {"validators": [Email()]}, } column_extra_row_actions = [ EndpointLinkRowAction( "glyphicon glyphicon-time", id_arg="flt1_0", title="Experiments history", endpoint="experimentmark.index_view", ) ] def partQueryFactory(): return Part.query.filter(Part.semester == userActiveSemester()) def groupQueryFactory(): return Group.query.filter(Group.semester == userActiveSemester()) class PartStudentView(SecureAdminModelView): class CreateForm(Form): def studentQueryFactory(): return Student.query student = QuerySelectField( "Student", query_factory=studentQueryFactory, validators=[DataRequired()], allow_blank=True, blank_text="-", ) part = QuerySelectField( "Part", query_factory=partQueryFactory, validators=[DataRequired()], allow_blank=True, blank_text="-", ) group = QuerySelectField( "Group", query_factory=groupQueryFactory, allow_blank=True, blank_text="-", ) class EditForm(CreateForm): student = None part = None form = EditForm column_filters = ["part", "student", "group"] def queryFilter(self): return PartStudent.part.has(Part.semester == userActiveSemester()) def create_form(self, obj=None): form = self.CreateForm return form(get_form_data(), obj=obj) def on_model_change(self, form, model, is_created): PartStudent.check(model.group, model.part) def partStudentQueryFactory(): return PartStudent.query.filter(PartStudent.part.has(Part.semester == userActiveSemester())) class GroupView(SecureAdminModelView): def formFactory(is_created, group): if is_created: def query_factory(): return partStudentQueryFactory().filter(PartStudent.group == None) else: def query_factory(): return partStudentQueryFactory().filter( or_( and_(PartStudent.group == None, PartStudent.part.has(Part.program == group.program)), PartStudent.group == group, ) ) class CustomForm(Form): part_students = QuerySelectMultipleField( "Part Students", query_factory=query_factory, validators=[DataRequired()], description="The part students have to be in the same program!", ) return CustomForm column_list = ["number", "semester", "program", "part_students", "group_experiments"] column_filters = ["number", "semester", "program"] def queryFilter(self): return Group.semester == userActiveSemester() def customCreateModel(self, form): return Group.customInit(form.part_students.data) def create_form(self, obj=None): form = GroupView.formFactory(is_created=True, group=None) return form(get_form_data(), obj=obj) def edit_form(self, obj=None): form = GroupView.formFactory(is_created=False, group=obj) return form(get_form_data(), obj=obj) class ExperimentView(SecureAdminModelView): can_view_details = True column_filters = ["active"] column_list = ["number", "program", "title", "active"] column_descriptions = {"active": "Active experiments are present in new semesters"} column_details_list = column_list + [ "description", "wiki_link", "room", "building", "responsibility", "duration_in_days", "oral_weighting", "protocol_weighting", "final_weighting", "semester_experiments", ] form_columns = column_details_list.copy() form_columns.remove("semester_experiments") column_editable_list = ["active"] form_args = { "wiki_link": {"validators": [URL()]}, "oral_weighting": { "description": "Oral and protocol weighting have to add to 1! Both are rounded to 2 decimal digits." }, } form_extra_fields = { "program": QuerySelectField( "Program", query_factory=programQueryFactory, validators=[DataRequired()], allow_blank=True, blank_text="-" ) } def on_model_change(self, form, model, is_created): model.checkWeightings(roundWeightings=True) def assistantQueryFactory(): return Assistant.query.filter(Assistant.user.has(User.active == True)) class SemesterExperimentView(SecureAdminModelView): class CreateForm(Form): def experimentQueryFactory(): return Experiment.query.filter(Experiment.active == True) experiment = QuerySelectField( "Experiment", query_factory=experimentQueryFactory, validators=[DataRequired()], allow_blank=True, blank_text="-", ) assistants = QuerySelectMultipleField("Assistants", query_factory=assistantQueryFactory) form = CreateForm can_view_details = True column_list = ["experiment", "assistants", "semester"] column_details_list = column_list + ["group_experiments"] column_filters = ["experiment"] def queryFilter(self): return SemesterExperiment.semester == userActiveSemester() def customCreateModel(self, form): return SemesterExperiment( semester=userActiveSemester(), experiment=form.experiment.data, assistants=form.assistants.data ) class AssistantView(SecureAdminModelView): def assistantUserQueryFactory(): return User.query.filter(User.roles.any(Role.name == "assistant")) can_view_details = True column_list = ["user.first_name", "user.last_name", "user", "semester_experiments"] column_details_list = column_list + [ "user.phone_number", "user.mobile_phone_number", "user.room", "user.building", "appointments", "experiment_marks", ] column_searchable_list = ["user.first_name", "user.last_name", "user.email"] column_filters = ["user.active"] form_excluded_columns = ["experiment_marks", "appointments"] form_extra_fields = { "user": QuerySelectField( "User", query_factory=assistantUserQueryFactory, validators=[DataRequired()], allow_blank=True, blank_text="-", ), "semester_experiments": QuerySelectMultipleField( "Semester Experiments", query_factory=semesterExperimentQueryFactory, allow_blank=True, blank_text="-", ), } assistantBlankText = "Auto assign if experiment has only one assistant" class GroupExperimentView(SecureAdminModelView): class CreateForm(Form): group = QuerySelectField( "Group", query_factory=groupQueryFactory, validators=[DataRequired()], allow_blank=True, blank_text="-", ) semester_experiment = QuerySelectField( "Semester Experiment", query_factory=semesterExperimentQueryFactory, validators=[DataRequired()], allow_blank=True, blank_text="-", ) appointment1_date = DateField( "Appointment-1 Date", validators=[Optional()], description="Set if you already want to add an appointment. Otherwise, leave it blank and you can do it later under the Appointment tab.", ) appointment1_special = BooleanField("Appointment-1 Special", default=False) appointment1_assistant = QuerySelectField( "Appointment-1 Assistant", query_factory=assistantQueryFactory, allow_blank=True, blank_text=assistantBlankText, ) appointment2_date = DateField( "Appointment-2 Date", validators=[Optional()], description="Add a second appointment (see above)." ) appointment2_special = BooleanField("Appointment-2 Special", default=False) appointment2_assistant = QuerySelectField( "Appointment-2 Assistant", query_factory=assistantQueryFactory, allow_blank=True, blank_text=assistantBlankText, ) form = CreateForm can_edit = False column_list = ["group", "semester_experiment", "appointments", "experiment_marks"] column_filters = ["group", "semester_experiment.experiment", "appointments"] def queryFilter(self): return GroupExperiment.group.has(Group.semester == userActiveSemester()) def customCreateModel(self, form): return GroupExperiment.customInit(semester_experiment=form.semester_experiment.data, group=form.group.data) def on_model_change(self, form, model, is_created): if is_created: for date, special, assistant in zip( [form.appointment1_date.data, form.appointment2_date.data], [form.appointment1_special.data, form.appointment2_special.data], [form.appointment1_assistant.data, form.appointment2_assistant.data], ): if date: appointment = Appointment.customInit(date, special, assistant, model) self.session.add(appointment) def after_model_change(self, form, model, is_created): if is_created: if model.appointments: flash(f"Appointments {model.appointments} added.", "success") def groupExperimentQueryFactory(): return GroupExperiment.query.filter( GroupExperiment.semester_experiment.has(SemesterExperiment.semester == userActiveSemester()) ) class AppointmentView(SecureAdminModelView): class CreateForm(Form): group_experiment = QuerySelectField( "Group Experiment", query_factory=groupExperimentQueryFactory, validators=[DataRequired()], allow_blank=True, blank_text="-", ) date = DateField("Date", validators=[DataRequired()]) special = BooleanField("Special", default=False) assistant = QuerySelectField( "Assistant", query_factory=assistantQueryFactory, allow_blank=True, blank_text=assistantBlankText, ) form = CreateForm column_filters = [ "date", "special", "group_experiment.group", "group_experiment.semester_experiment.experiment", "assistant.user", ] column_editable_list = ["date", "special"] def queryFilter(self): return Appointment.group_experiment.has( GroupExperiment.semester_experiment.has(SemesterExperiment.semester == userActiveSemester()) ) def customCreateModel(self, form): return Appointment.customInit( form.date.data, form.special.data, form.assistant.data, form.group_experiment.data ) def update_model(self, form, model): if not form.date or not form.special: # For editables return super().update_model(form, model) try: model.customUpdate(form.date.data, form.special.data, form.assistant.data, form.group_experiment.data) self.on_model_change(form, model, False) self.session.commit() except Exception as ex: flash(str(ex), "error") self.session.rollback() return False else: self.after_model_change(form, model, False) return True class ExperimentMarkView(SecureAdminModelView): class StudentIdFilter(BaseSQLAFilter): def apply(self, query, value, alias=None): return query.filter(self.column == value) def operation(self): return "equals" def validate(self, value): if Student.query.get(value): return True else: return False class CreateForm(Form): part_student = QuerySelectField( "Part Student", query_factory=partStudentQueryFactory, validators=[DataRequired()], allow_blank=True, blank_text="-", ) group_experiment = QuerySelectField( "Group Experiment", query_factory=groupExperimentQueryFactory, validators=[DataRequired()], allow_blank=True, blank_text="-", ) form = CreateForm can_edit = False column_descriptions = { "oral_mark": "Between 0 and 15", "protocol_mark": "Between 0 and 15", "assistant": "The assistant who assigned the mark", "edited_by_admin": "If the mark was edited by an admin", } column_editable_list = ["oral_mark", "protocol_mark"] form_args = { "oral_mark": {"validators": [NumberRange(0, 15)]}, "protocol_mark": {"validators": [NumberRange(0, 15)]}, } column_filters = [ StudentIdFilter(PartStudent.id, "Student / ID"), "part_student.part.program", "part_student.part", "part_student.student", "group_experiment.group", "group_experiment.semester_experiment.semester", "group_experiment.semester_experiment.experiment", "edited_by_admin", "assistant.user", ] column_default_sort = [("oral_mark", False), ("protocol_mark", False)] """ # Deactivated for the experiments hisotry of a student. def queryFilter(self): return ExperimentMark.group_experiment.has( GroupExperiment.semester_experiment.has(SemesterExperiment.semester == userActiveSemester()) ) """ def customCreateModel(self, form): return ExperimentMark.customInit( part_student=form.part_student.data, group_experiment=form.group_experiment.data ) def update_model(self, form, model): if (form.oral_mark and form.oral_mark.data != model.oral_mark) or ( form.protocol_mark and form.protocol_mark.data != model.protocol_mark ): model.edited_by_admin = True return super().update_model(form, model) class ProgramView(SecureAdminModelView): can_view_details = True column_list = ["label"] form_excluded_columns = ["parts", "experiments", "groups"] column_details_list = column_list + form_excluded_columns adminSpace.add_view(StudentView(Student, db.session)) adminSpace.add_view(PartStudentView(PartStudent, db.session)) adminSpace.add_view(GroupView(Group, db.session)) adminSpace.add_view(GroupExperimentView(GroupExperiment, db.session)) adminSpace.add_view(AppointmentView(Appointment, db.session)) adminSpace.add_view(ExperimentMarkView(ExperimentMark, db.session)) adminSpace.add_view(ExperimentView(Experiment, db.session)) adminSpace.add_view(SemesterExperimentView(SemesterExperiment, db.session)) adminSpace.add_view(SemesterView(Semester, db.session)) adminSpace.add_view(PartView(Part, db.session)) adminSpace.add_view(AssistantView(Assistant, db.session)) adminSpace.add_view(UserView(User, db.session)) adminSpace.add_view(RoleView(Role, db.session)) adminSpace.add_view(ProgramView(Program, db.session)) initActiveSemesterMenuLinks(adminSpace)