1
0
Fork 0
mirror of https://codeberg.org/Mo8it/AdvLabDB.git synced 2024-11-12 21:40:41 +00:00
AdvLabDB/advlabdb/custom_classes.py

357 lines
12 KiB
Python
Raw Normal View History

2021-07-30 12:20:54 +00:00
from flask import flash, redirect, request, url_for
from flask_admin import AdminIndexView, BaseView, expose
from flask_admin.contrib.sqla import ModelView
2022-05-06 23:06:08 +00:00
from flask_admin.helpers import get_form_data
from flask_admin.model.helpers import get_mdict_item_or_list
2022-08-15 20:22:36 +00:00
from flask_login import current_user
from sqlalchemy import func, select
2021-07-13 15:22:15 +00:00
2022-08-09 23:14:47 +00:00
from .exceptions import DatabaseException, ModelViewException
2022-08-09 12:46:48 +00:00
from .model_independent_funs import reportBadAttempt
from .models import Assistant, ExperimentMark, GroupExperiment, SemesterExperiment, db
def adminViewIsAccessible():
return current_user.has_role("admin")
2021-07-30 00:03:44 +00:00
def assistantViewIsAccessible():
return current_user.has_role("assistant")
2021-07-30 00:03:44 +00:00
2022-06-28 14:57:55 +00:00
def get_url(kwargs):
url = kwargs["url"]
if "/" in url:
raise ModelViewException("url can not contain a slash!")
return url
2021-07-30 00:03:44 +00:00
class CustomIndexView(AdminIndexView):
2021-07-01 12:02:23 +00:00
def inaccessible_callback(self, name, **kwargs):
# Redirect to login page if user doesn't have access
return redirect(url_for("security.login", next=request.url))
2021-07-30 00:03:44 +00:00
class SecureAdminIndexView(CustomIndexView):
def is_accessible(self):
return adminViewIsAccessible()
2022-03-04 02:49:02 +00:00
@expose("/")
def index(self):
assistants_num_missing = db.session.execute(
select(Assistant, func.count())
.join(Assistant.semester_experiments)
.where(SemesterExperiment.semester == current_user.active_semester)
.join(SemesterExperiment.group_experiments)
2023-11-02 17:09:10 +00:00
.where(GroupExperiment.experiment_marks_missing is True)
.join(GroupExperiment.experiment_marks)
2023-11-02 17:09:10 +00:00
.where(ExperimentMark.final_experiment_mark is None)
.group_by(Assistant.id)
.order_by(func.count().desc())
2022-05-16 19:49:34 +00:00
)
return self.render(
2022-09-11 12:55:53 +00:00
"admin_index.jinja.html",
assistants_num_missing=assistants_num_missing,
)
2021-07-30 00:03:44 +00:00
class SecureAssistantIndexView(CustomIndexView):
def is_accessible(self):
return assistantViewIsAccessible()
2022-03-04 02:49:02 +00:00
@expose("/")
def index(self):
number_of_missing_final_experiment_marks = db.session.scalar(
select(func.count())
.select_from(ExperimentMark)
2022-05-16 19:49:34 +00:00
.join(GroupExperiment)
.join(SemesterExperiment)
.where(SemesterExperiment.semester == current_user.active_semester)
2022-05-16 19:49:34 +00:00
.join(SemesterExperiment.assistants)
.where(Assistant.user == current_user)
2023-11-02 17:09:10 +00:00
.where(ExperimentMark.final_experiment_mark is None)
2022-05-16 19:49:34 +00:00
)
return self.render(
2022-09-11 12:55:53 +00:00
"assistant_index.jinja.html",
number_of_missing_final_experiment_marks=number_of_missing_final_experiment_marks,
)
2021-07-30 00:03:44 +00:00
class CustomModelView(ModelView):
2021-07-01 11:12:43 +00:00
create_modal = True
edit_modal = True
details_modal = True
can_view_details = False
2022-05-06 23:44:39 +00:00
refreshFiltersCache = False
# Should not be a copy of column_formatters
# because of link formatting.
2022-07-02 22:48:05 +00:00
column_formatters_export: dict = {}
2022-07-01 16:48:17 +00:00
2022-05-17 00:31:09 +00:00
# Used in the UserView because of create_user
# Should not be touched in other views
_skip_session_addition_on_model_creation = False
2022-05-06 23:44:39 +00:00
@expose("/")
def index_view(self):
if self.refreshFiltersCache:
# Update filter options
self._refresh_filters_cache()
return super().index_view()
2021-07-01 12:02:23 +00:00
def inaccessible_callback(self, name, **kwargs):
# Redirect to login page if user doesn't have access
return redirect(url_for("security.login", next=request.url))
2022-05-16 21:32:31 +00:00
def query_modifier(self, query):
return query
2022-05-16 21:32:31 +00:00
def get_query(self):
return self.query_modifier(super().get_query())
2022-05-06 23:06:08 +00:00
def get_count_query(self):
2022-05-16 21:32:31 +00:00
return self.query_modifier(super().get_count_query())
2022-05-06 23:06:08 +00:00
2021-07-13 15:22:15 +00:00
def handle_view_exception(self, exc):
2022-08-09 23:14:47 +00:00
if type(exc) in (ModelViewException, DatabaseException):
2021-07-13 15:22:15 +00:00
flash(str(exc), "error")
return True
return super().handle_view_exception(exc)
2021-07-29 22:24:10 +00:00
2022-05-17 00:31:09 +00:00
def customCreateModel(self, form):
model = self.build_new_instance()
form.populate_obj(model)
return model
2022-05-06 23:06:08 +00:00
2022-05-17 00:31:09 +00:00
def create_model(self, form):
2022-05-06 23:06:08 +00:00
try:
model = self.customCreateModel(form)
2022-05-17 00:31:09 +00:00
if not self._skip_session_addition_on_model_creation:
self.session.add(model)
2022-05-06 23:06:08 +00:00
self.on_model_change(form, model, True)
self.session.commit()
except Exception as ex:
2022-05-15 17:07:08 +00:00
if not self.handle_view_exception(ex):
flash(str(ex), "error")
2022-05-06 23:06:08 +00:00
self.session.rollback()
2021-07-29 22:24:10 +00:00
else:
2022-05-06 23:06:08 +00:00
self.after_model_change(form, model, True)
return model
2021-07-29 22:24:10 +00:00
2022-05-17 00:31:09 +00:00
def customUpdateModel(self, form, model):
"""
2022-07-02 22:20:13 +00:00
Return True if something changed during update, False otherwise.
"""
2022-05-17 00:31:09 +00:00
form.populate_obj(model)
# No way to know if something changed. Therefore, return True anyway.
return True
2022-05-17 00:31:09 +00:00
def update_model(self, form, model):
try:
if self.customUpdateModel(form, model) is False:
# Nothing changed
return True
2022-05-17 00:31:09 +00:00
self.on_model_change(form, model, False)
self.session.commit()
except Exception as ex:
if not self.handle_view_exception(ex):
flash(str(ex), "error")
self.session.rollback()
return False
else:
self.after_model_change(form, model, False)
return True
2022-05-06 23:06:08 +00:00
def create_form(self, obj=None):
if hasattr(self, "CreateForm"):
formClass = self.CreateForm
elif hasattr(self, "CreateAndEditForm"):
formClass = self.CreateAndEditForm
else:
return super().create_form(obj)
2021-07-29 22:24:10 +00:00
2022-05-06 23:06:08 +00:00
return formClass(get_form_data(), obj=obj)
2021-07-29 22:24:10 +00:00
2022-05-06 23:06:08 +00:00
def edit_form(self, obj=None):
if hasattr(self, "EditForm"):
formClass = self.EditForm
elif hasattr(self, "CreateAndEditForm"):
formClass = self.CreateAndEditForm
else:
return super().edit_form(obj)
2021-07-29 22:24:10 +00:00
2022-05-06 23:06:08 +00:00
return formClass(get_form_data(), obj=obj)
2021-07-30 00:03:44 +00:00
2022-07-01 16:48:17 +00:00
def get_export_columns(self):
# Use column_details_list instead of column_list
only_columns = self.column_export_list or self.column_details_list or self.scaffold_list_columns()
return self.get_column_names(
only_columns=only_columns,
excluded_columns=self.column_export_exclude_list,
)
2021-07-30 00:03:44 +00:00
class SecureAdminModelView(CustomModelView):
2021-11-30 00:36:19 +00:00
can_export = True
can_set_page_size = True
can_create = True
can_edit = True
can_delete = True
column_display_actions = True
2022-06-30 02:06:07 +00:00
can_view_details = True
2021-11-30 00:36:19 +00:00
2022-09-11 12:55:53 +00:00
list_template = "admin_list.jinja.html"
create_template = "admin_create.jinja.html"
edit_template = "admin_edit.jinja.html"
details_template = "admin_details.jinja.html"
2021-07-30 00:03:44 +00:00
2022-06-28 14:57:55 +00:00
def __init__(self, model, **kwargs):
url = get_url(kwargs)
2022-09-11 18:05:20 +00:00
super().__init__(model, db.session, endpoint="admin_" + url, category="Tables", **kwargs)
2022-06-28 14:57:55 +00:00
2021-07-30 00:03:44 +00:00
def is_accessible(self):
return adminViewIsAccessible()
class SecureAssistantModelView(CustomModelView):
2021-11-30 00:36:19 +00:00
can_export = False
can_set_page_size = False
can_edit = False
column_display_actions = False
2022-09-11 12:55:53 +00:00
list_template = "assistant_list.jinja.html"
create_template = "assistant_create.jinja.html"
edit_template = "assistant_edit.jinja.html"
details_template = "assistant_details.jinja.html"
2021-07-30 00:03:44 +00:00
"""
2022-05-06 23:06:08 +00:00
SECURITY NOTES:
- Every variable and method defined below in this class is NOT ALLOWED TO BE (completely) OVERWRITTEN!
You can only extend the predefined methods.
2022-05-16 21:32:31 +00:00
- The method query_modifier(self, query) has to be implemented!
"""
# Assistants are not allowed to create or delete.
can_create = False
can_delete = False
2022-06-28 14:57:55 +00:00
def __init__(self, model, **kwargs):
url = get_url(kwargs)
super().__init__(model, db.session, endpoint="assistant_" + url, **kwargs)
2021-07-30 00:03:44 +00:00
def is_accessible(self):
return assistantViewIsAccessible()
2021-09-11 18:48:14 +00:00
2022-05-16 21:32:31 +00:00
def query_modifier(self, query):
2021-11-30 00:36:19 +00:00
"""
2022-05-16 21:32:31 +00:00
A default query modifier has to be implemented to restrict assistant's read/write access.
2021-11-30 00:36:19 +00:00
See on_model_change!
"""
2022-03-03 02:04:15 +00:00
raise NotImplementedError()
2021-11-30 00:36:19 +00:00
def on_model_change(self, form, model, is_created):
"""
2022-05-16 21:32:31 +00:00
This method uses the modified query returned by query_modifier (which has to be implemented!) to prevent assistants
2022-02-13 19:05:29 +00:00
from modifying models not listed on their view by sending a POST request with a different id.
2021-11-30 00:36:19 +00:00
You can extend this method by implementing a custom on_model_change and then calling super().on_model_change within it.
"""
if is_created:
reportBadAttempt("An assistant tried to create a model!")
raise ModelViewException("Assistants can not create models!")
if model not in self.get_query():
reportBadAttempt("An assistant tried to change a model not in his filter!")
2022-09-21 14:52:15 +00:00
self.handle_view_exception(ModelViewException("Unauthorized action!"))
return redirect(self.url)
2021-11-30 00:36:19 +00:00
2023-11-02 18:24:14 +00:00
return None
2021-11-30 00:36:19 +00:00
def on_model_delete(self, model):
reportBadAttempt("An assistant tried to delete a model!")
raise ModelViewException("Assistants can not delete models!")
@expose("/edit/", methods=("GET", "POST"))
def edit_view(self):
"""
Prevent an assistant from seeing the edit form of a model not in his filter by using the GET method and entering an id.
This is important if can_edit is set to True.
"""
id = get_mdict_item_or_list(request.args, "id")
if id is not None:
model = self.get_one(id)
if model not in self.get_query():
reportBadAttempt("An assistant tried to edit a model not in his filter!")
2022-09-21 14:52:15 +00:00
self.handle_view_exception(ModelViewException("Unauthorized action!"))
return redirect(self.url)
return super().edit_view()
@expose("/details/")
def details_view(self):
"""
Prevent an assistant from seeing the details of a model not in his filter by using the GET method and entering an id.
This is important if can_view_details is set to True.
"""
id = get_mdict_item_or_list(request.args, "id")
if id is not None:
model = self.get_one(id)
if model not in self.get_query():
reportBadAttempt("An assistant tried to see details of a model not in his filter!")
2022-09-21 14:52:15 +00:00
self.handle_view_exception(ModelViewException("Unauthorized action!"))
return redirect(self.url)
return super().details_view()
def get_details_columns(self):
"""
Prevent showing unintended data if can_view_details is set to True and column_details_list was not set (which equals None).
"""
if not self.column_details_list:
self.column_details_list = self.column_list
return super().get_details_columns()
2021-09-11 18:48:14 +00:00
2022-09-19 13:20:18 +00:00
class CustomBaseView(BaseView):
def inaccessible_callback(self, name, **kwargs):
# Redirect to login page if user doesn't have access
return redirect(url_for("security.login", next=request.url))
class SecureAdminBaseView(CustomBaseView):
2022-06-28 14:57:55 +00:00
def __init__(self, **kwargs):
url = get_url(kwargs)
super().__init__(endpoint="admin_" + url, **kwargs)
2021-09-11 18:48:14 +00:00
def is_accessible(self):
return adminViewIsAccessible()
2022-02-23 18:37:09 +00:00
2022-09-19 13:20:18 +00:00
class SecureAssistantBaseView(CustomBaseView):
2022-06-28 14:57:55 +00:00
def __init__(self, **kwargs):
url = get_url(kwargs)
super().__init__(endpoint="assistant_" + url, **kwargs)
2022-02-23 18:37:09 +00:00
def is_accessible(self):
return assistantViewIsAccessible()