1
0
Fork 0
mirror of https://codeberg.org/Mo8it/AdvLabDB.git synced 2024-12-02 22:33:05 +00:00

Compare commits

...

8 commits

12 changed files with 205 additions and 39 deletions

3
.ignore Normal file
View file

@ -0,0 +1,3 @@
/advlabdb/static/
/LICENSE.txt
/poetry.lock

View file

@ -31,7 +31,7 @@ def backup(backup_prefix):
flash(f"Created a database backup at the path {dest}", "success")
else:
flash("Failed to create a database backup!", "danger")
except:
except Exception:
flash("Failed to create a database backup! Make sure that `sqlite3` is installed on your system!", "danger")
@ -43,9 +43,9 @@ def deactivate_assistants():
user_ids_to_deactivate = db.session.scalars(
select(Assistant.user_id)
.join(User)
.where(User.active is True)
.where(User.active == True)
.except_(
select(Assistant.user_id).join(Assistant.semester_experiments).join(Semester).where(Semester.done is False)
select(Assistant.user_id).join(Assistant.semester_experiments).join(Semester).where(Semester.done == False)
)
)

View file

@ -397,7 +397,7 @@ class SemesterView(SecureAdminModelView):
form_args = {
"done": {
"description": "Setting a semester as done prevents assistants from changing or even seeing marks in this semester. Setting a semester as done sets older semesters as done, too. Only set the semester as done if all marks in the semester and all previous semesters are already set."
"description": "Setting a semester as done prevents assistants from changing or even seeing marks in this semester. Setting a semester as done sets older semesters as done, too. Only set a semester as done if all marks in that semester and all previous semesters are already set."
},
}
@ -636,14 +636,14 @@ class GroupView(SecureAdminModelView):
if is_created:
def query_factory():
return partStudentQueryFactory().where(PartStudent.group is None)
return partStudentQueryFactory().where(PartStudent.group == None)
else:
def query_factory():
return partStudentQueryFactory().where(
or_(
and_(PartStudent.group is None, Part.program == group.program),
and_(PartStudent.group == None, Part.program == group.program),
PartStudent.group == group,
)
)
@ -754,7 +754,7 @@ class ExperimentView(SecureAdminModelView):
def assistantQueryFactory():
return Assistant.query.join(User).where(User.active is True)
return Assistant.query.join(User).where(User.active == True)
def weighting_field(label: str, default: float):
@ -776,7 +776,7 @@ class SemesterExperimentView(SecureAdminModelView):
class CreateForm(FlaskForm):
@staticmethod
def experimentQueryFactory():
return Experiment.query.where(Experiment.active is True)
return Experiment.query.where(Experiment.active == True)
experiment = QuerySelectField(
"Experiment",
@ -937,7 +937,7 @@ class ExperimentRowFilter(FilterEqual):
if not has_request_context():
return ()
activeExperiments = db.session.scalars(select(Experiment).where(Experiment.active is True))
activeExperiments = db.session.scalars(select(Experiment).where(Experiment.active == True))
return tuple(
(
f"{activeExperiment.number},{activeExperiment.program_id}",
@ -1193,7 +1193,7 @@ class ExperimentMarkView(SecureAdminModelView):
if not has_request_context():
return ()
admins = db.session.scalars(select(Admin).join(User).where(User.active is True))
admins = db.session.scalars(select(Admin).join(User).where(User.active == True))
return tuple((admin.id, str(admin)) for admin in admins)
def apply(self, query, value, alias=None):

View file

@ -88,7 +88,7 @@ def email_formatter(view, context, model, name: str):
def experiment_marks_missing_formatter(view, context, model, name: str):
experiment_marks_missing = getattr(model, name)
if experiment_marks_missing is True:
if experiment_marks_missing == True:
return Markup("<span style='color: red;'>Yes</span>")
return "No"

View file

@ -77,7 +77,7 @@ def mark_hists(markType, active_assistants):
def assistant_marks_analysis(cls):
active_assistants = db.session.scalars(select(Assistant).join(User).where(User.active is True)).all()
active_assistants = db.session.scalars(select(Assistant).join(User).where(User.active == True)).all()
if not active_assistants:
flash("No active assistants!", "warning")

View file

@ -211,7 +211,7 @@ class AssistantUserView(SecureAssistantModelView):
@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)
return Semester.query.order_by(Semester.id.desc()).where(Semester.done == False).limit(2)
active_semester = QuerySelectField(
"Active Semester",

View file

@ -44,9 +44,9 @@ class SecureAdminIndexView(CustomIndexView):
.join(Assistant.semester_experiments)
.where(SemesterExperiment.semester == current_user.active_semester)
.join(SemesterExperiment.group_experiments)
.where(GroupExperiment.experiment_marks_missing is True)
.where(GroupExperiment.experiment_marks_missing == True)
.join(GroupExperiment.experiment_marks)
.where(ExperimentMark.final_experiment_mark is None)
.where(ExperimentMark.final_experiment_mark == None)
.group_by(Assistant.id)
.order_by(func.count().desc())
)
@ -71,7 +71,7 @@ class SecureAssistantIndexView(CustomIndexView):
.where(SemesterExperiment.semester == current_user.active_semester)
.join(SemesterExperiment.assistants)
.where(Assistant.user == current_user)
.where(ExperimentMark.final_experiment_mark is None)
.where(ExperimentMark.final_experiment_mark == None)
)
return self.render(
@ -158,7 +158,7 @@ class CustomModelView(ModelView):
def update_model(self, form, model):
try:
if self.customUpdateModel(form, model) is False:
if self.customUpdateModel(form, model) == False:
# Nothing changed
return True

View file

@ -537,7 +537,7 @@ class Semester(db.Model):
if transferParts:
semester.transferPartsFrom(oldSemester)
for experiment in db.session.scalars(select(Experiment).where(Experiment.active is True)):
for experiment in db.session.scalars(select(Experiment).where(Experiment.active == True)):
newSemesterExperiment = SemesterExperiment(experiment=experiment, semester=semester)
if transferAssistants:
@ -575,7 +575,7 @@ class Semester(db.Model):
.join(GroupExperiment)
.join(SemesterExperiment)
.where(SemesterExperiment.semester == self)
.where(ExperimentMark.final_experiment_mark is None)
.where(ExperimentMark.final_experiment_mark == None)
)
def set_done(self, next_semester=None):

View file

@ -82,7 +82,7 @@
<p>
Every part student has a final part mark shown in this table.
The final part mark is calculated as explained in <a href="#experiment">Experiment</a>.
The final part mark is calculated as explained in <a href="#semester_experiment">Semester Experiment</a>.
Normally, this final part mark is the one relevant for the transcript of records of students.
</p>
@ -193,40 +193,161 @@
This mark is calculated with respect to the weights of the related <a href="#semester_experiment">semester experiment</a>.
</p>
<!-- TODO
<h4 id="experiment">Experiment</h4>
<p></p>
<p>This table shows all experiments, not only in your active semester.</p>
<p>
Experiments that are marked as "active" will be present in new semesters.
Make sure to active/deactivate experiments before creating a new semester.
</p>
<h4 id="semester_experiment">Semester Experiment</h4>
<p></p>
<p>This table shows the semester experiments in your active semester.</p>
<p>
A semester experiment is an <a href="#experiment">experiment</a> that takes place in a specific <a href="#semester">semester</a>.
</p>
<p>
Assistants and mark weightings are assigned to these semester experiments.
These assignments are transferred to the next semester if the related <a href="#experiment">experiment</a> was active before creating the new semester.
</p>
<p>Mark weightings consist of oral, protocol and final weightings.</p>
<p>
The oral weighting and protocol weightings are between 0 and 1 and have to add up to 1.
By default, these weightings should both be 0.5, but you can for example have an oral weighting of 0.25 and a protocol weighting of 0.75.
</p>
<p>
The final weighting has to be a value between 0 and 1.
By default, its value should be 1.
But you can choose a value lower than 1 in case an experiment is easier than others.
</p>
<p>
The final part mark for a student is calculated by summing the following expression over <code>i</code> and then dividing through the sum of <code>f_i</code>:
</p>
<p>
<code>f_i * (ow_i * o_i + pw_i * p_i)</code>
</p>
<ul>
<li>
<code>i</code> is the index of a semester experiment.
The sum over <code>i</code> is done only over the semester experiments that a student has in that part.
</li>
<li>
<code>f_i</code> is the final part mark for the semester experiment <code>i</code>.
</li>
<li>
<code>ow_i</code> is the oral weighting.
</li>
<li>
<code>o_i</code> is the oral mark.
</li>
<li>
<code>pw_i</code> is the protocol weighting.
</li>
<li>
<code>p_i</code> is the protocol mark.
</li>
</ul>
<p>
The result after the division is rounded to an integer using the "round half up" strategy.
This means that <code>x.5</code> is always rounded up to <code>x+1</code>.
Values between <code>x.5</code> and <code>x+1</code> (closed interval) are rounded up to <code>x+1</code>.
Values higher than <code>x</code> and lower than <code>x.5</code> are rounded down to <code>x</code>.
</p>
<p>
In case you change the weightings during a semester (some marks are already set), all part and experiment marks will be automatically updated.
</p>
<h4 id="semester">Semester</h4>
<p></p>
<p>This table shows all semesters.</p>
<p>
Before creating a new semester, make sure to read all notes in the form that shows up after pressing the "Create" button.
</p>
<p>
You can and should set a semester as "done" by clicking on the edit button near that semester, then clicking the "done" checkbox and then "Save".
</p>
<p>
Setting a semester as done prevents assistants from changing or even seeing marks in this semester.
Setting a semester as done sets older semesters as done, too.
Only set a semester as done if all marks in that semester and all previous semesters are already set.
</p>
<h4 id="part">Part</h4>
<p></p>
<p>This table shows the parts in your active semester.</p>
<p>
A part consists of a <a href="#program">program</a>, a <a href="#semester">semester</a> and a number.
For example, if students in the bachelor of science program (BS) must absolve two lab courses, then you would have the parts BS1 and BS2.
But maybe the university only offers part BS2 every second semester.
Therefore, a part is coupled to a semester.
</p>
<h4 id="assistant">Assistant</h4>
<p></p>
<p>This table shows all assistants.</p>
<p>
To deactivate an assistant, you have to deactivate their <a href="#user">user</a> account.
</p>
<p>
You can assign <a href="#semester_experiment">semester experiments</a> to assistants either in this table or in the semester experiments table.
</p>
<h4 id="admin">Admin</h4>
<p></p>
<p>This is a read only table that shows current admins.</p>
<h4 id="user">User</h4>
<p></p>
<p>This table shows all users.</p>
<p>A user can have either an admin or assistant role (or both).</p>
<p>
Make sure to deactivate users if they don't require access to the database anymore.
Deactivated users can't login anymore.
</p>
<p>
Further details can be read under <a href="#user_settings">User settings</a>.
</p>
<h4 id="program">Program</h4>
<p></p>
<p>This table shows all programs.</p>
<p>
A program is a university program.
You can for example have the programs BS for bachelor of science, BE for bachelor of education, MS for master of science and so on.
It is recommended to choose short labels for programs.
</p>
<h3 id="import">Import</h3>
<p></p>
<p>
Allows you to import data for your active semester from a file.
Visit the importing page for more information.
</p>
<h3 id="actions">Actions</h3>
<p></p>
<p>
Run one or multiple actions like creating a database backup.
The available actions should be explained on the page.
</p>
<h3 id="analysis">Analysis</h3>
<p></p>
-->
<p>
Run an analysis and plot the results.
More information on the page itself.
</p>
<h3 id="docs">Docs</h3>
<p>This is a link which leads you to this page.</p>
@ -263,11 +384,51 @@
</p>
<p>If you forget your password, you can reset it using the CLI on the server.</p>
<!-- TODO
<h2>How-Tos</h2>
<h3>Start a new semester</h3>
<p></p>
-->
<p>Here is a list of tables that one has to visit when creating a new semester:</p>
<ol>
<li>
Program: Make sure that the programs are up to date. Usually, you will only create new programs when first creating the database for the first time.
</li>
<li>
User: Create a user for every new assistants.
Old assistants should already be in the database, just make sure that their users are activated.
</li>
<li>
Experiment: Create possibly new experiments.
Old experiments should already be in the database, just make sure that they are activated.
</li>
<li>
Semester: Create a new semester.
Read the notes in the form for creating a semester.
Possibly set old semester as done.
</li>
<li>
Part: The parts from the last semester should be duplicated in the new semester (if you don't uncheck the checkbox "Transfer parts" on creating a new semester).
You can add or remove a part from the new semester if required.
</li>
<li>
Semester Experiment: Semester experiments should be duplicated in the new semester (if you don't uncheck the checkbox "Transfer experiments" on creating a new semester).
Make sure that all are present.
Possibly create new ones.
Check the assigned assistants and possibly assign new ones or reassign old ones.
</li>
<li>
Import: Skip the next steps if you want to use the import functionality.
In that case, create an import file and import it.
Check that everything is imported correctly afterwards.
</li>
<li>Student: Add students that are new to the database.</li>
<li>Part Student: Add part students for all students that are doing a part in this semester.</li>
<li>Group: Create groups for part students.</li>
<li>
Group Experiment: Create new group experiments to assign semester experiments to groups.
The form allows you to directly add up to two appointments.
</li>
<li>Appointment: Check appointments and possibly add missing ones.</li>
</ol>
<h3>Tables</h3>

View file

@ -13,7 +13,7 @@ def _reset_admin_password(manage):
app = create_app(create_for_server=False)
with app.app_context(), db.session.begin():
admins = db.session.scalars(select(Admin).join(User).where(User.active is True)).all()
admins = db.session.scalars(select(Admin).join(User).where(User.active == True)).all()
activate_user = False
if len(admins) == 0:

View file

@ -1,6 +1,6 @@
[tool.poetry]
name = "advlabdb"
version = "0.9.0"
version = "1.0.0"
description = "Database with a web interface for labs."
authors = ["Mo Bitar <mo8it@proton.me>"]
readme = "README.md"
@ -48,7 +48,9 @@ select = [
"RUF",
]
ignore = [
"E5",
"E501",
"E711",
"E712",
"S3",
"S6",
"RET504",