Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions assets/locales/en_US.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
{
"Calculation mode": "Calculation mode",
"Weighted average": "Weighted average",
"Points sum": "Points sum",
"Each item weight is treated as its maximum points; the grade is the sum of points, not normalized.": "Each item weight is treated as its maximum points; the grade is the sum of points, not normalized.",
"The grade is the weighted average of items, normalized by the sum of weights.": "The grade is the weighted average of items, normalized by the sum of weights.",
"Forum thread": "Forum thread",
"Points for one message": "Points for one message",
"Points for two or more messages": "Points for two or more messages",
"Agenda": "Agenda",
"Documents": "Documents",
"Groups": "Groups",
Expand Down
69 changes: 69 additions & 0 deletions assets/vue/components/gradebook/CalculationModeSelector.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<template>
<div class="flex flex-col gap-1">
<BaseSelect
id="gradebook-calculation-mode"
v-model="selectedMode"
name="calculationMode"
:label="t('Calculation mode')"
:options="modeOptions"
:disabled="disabled"
@change="onChange"
/>
<p class="text-sm text-gray-50 flex items-start gap-1">
<BaseIcon icon="information" />
<span>{{ helpText }}</span>
</p>
</div>
</template>

<script setup>
import { computed, ref, watch } from "vue"
import { useI18n } from "vue-i18n"
import BaseSelect from "../basecomponents/BaseSelect.vue"
import BaseIcon from "../basecomponents/BaseIcon.vue"

const props = defineProps({
modelValue: {
type: String,
required: false,
default: "weighted_average",
},
disabled: {
type: Boolean,
required: false,
default: false,
},
})

const emit = defineEmits(["update:modelValue", "change"])

const { t } = useI18n()

const selectedMode = ref(props.modelValue)

watch(
() => props.modelValue,
(value) => {
selectedMode.value = value
},
)

const modeOptions = computed(() => [
{ label: t("Weighted average"), value: "weighted_average" },
{ label: t("Points sum"), value: "points_sum" },
])

const helpText = computed(() =>
"points_sum" === selectedMode.value
? t("Each item weight is treated as its maximum points; the grade is the sum of points, not normalized.")
: t("The grade is the weighted average of items, normalized by the sum of weights."),
)

/**
* Propagates the selected calculation mode to the parent component.
*/
function onChange() {
emit("update:modelValue", selectedMode.value)
emit("change", selectedMode.value)
}
</script>
122 changes: 122 additions & 0 deletions assets/vue/components/gradebook/ForumParticipationItemForm.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
<template>
<form
class="flex flex-col gap-4"
@submit.prevent="submit"
>
<BaseSelect
id="forum-participation-thread"
v-model="form.threadId"
name="threadId"
:label="t('Forum thread')"
:options="threadOptions"
option-label="title"
option-value="id"
:disabled="isEdit"
/>

<div class="flex gap-4 items-end">
<BaseInputNumber
id="forum-participation-points-one"
v-model="form.pointsOne"
name="pointsOne"
:label="t('Points for one message')"
:min="0"
/>
<BaseInputNumber
id="forum-participation-points-many"
v-model="form.pointsMany"
name="pointsMany"
:label="t('Points for two or more messages')"
:min="0"
/>
</div>

<div class="flex gap-2 justify-end">
<BaseButton
type="plain"
:label="t('Cancel')"
@click="$emit('cancel')"
/>
<BaseButton
type="success"
icon="save"
:label="t('Save')"
:disabled="!isValid"
@click="submit"
/>
</div>
</form>
</template>

<script setup>
import { computed, reactive } from "vue"
import { useI18n } from "vue-i18n"
import BaseSelect from "../basecomponents/BaseSelect.vue"
import BaseInputNumber from "../basecomponents/BaseInputNumber.vue"
import BaseButton from "../basecomponents/BaseButton.vue"
import gradebookService from "../../services/gradebookService"

const props = defineProps({
courseId: {
type: Number,
required: true,
},
categoryId: {
type: [Number, String],
required: true,
},
threads: {
type: Array,
required: true,
},
link: {
type: Object,
required: false,
default: null,
},
})

const emit = defineEmits(["saved", "cancel"])

const { t } = useI18n()

const isEdit = computed(() => null !== props.link)

const form = reactive({
threadId: props.link?.refId ?? null,
pointsOne: Number(props.link?.pointsOne ?? 0),
pointsMany: Number(props.link?.pointsMany ?? 0),
})

const threadOptions = computed(() => props.threads)

const isValid = computed(() => null !== form.threadId && form.pointsOne >= 0 && form.pointsMany >= 0)

/**
* Creates or updates the forum participation gradebook item via the API.
*/
async function submit() {
if (!isValid.value) {
return
}

if (isEdit.value) {
await gradebookService.updateForumParticipationLink(props.link.id, {
pointsOne: String(form.pointsOne),
pointsMany: String(form.pointsMany),
// Keep weight in sync with pointsMany (the item's max points).
weight: Number(form.pointsMany),
})
} else {
await gradebookService.createForumParticipationLink({
threadId: form.threadId,
courseId: props.courseId,
categoryId: props.categoryId,
pointsOne: form.pointsOne,
pointsMany: form.pointsMany,
})
}

emit("saved")
}
</script>
64 changes: 64 additions & 0 deletions assets/vue/services/gradebookService.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,68 @@ export default {
async getDefaultCertificate(courseId) {
return await baseService.get(`${API_BASE}/default_certificate/${courseId}`)
},

/**
* Updates the calculation mode (weighted_average | points_sum) of a gradebook category.
* @param {number|string} categoryId The numeric id of the gradebook category.
* @param {string} calculationMode The target calculation mode.
* @returns {Promise<Object>} The updated category resource.
*/
async updateCalculationMode(categoryId, calculationMode) {
return await baseService.put(`/api/gradebook_categories/${categoryId}`, { calculationMode })
},

/**
* Fetches the gradebook links of a category.
* @param {number|string} categoryId The numeric id of the gradebook category.
* @returns {Promise<Array>} The list of gradebook links.
*/
async getLinks(categoryId) {
return await baseService.getCollection("/api/gradebook_links", {
category: `/api/gradebook_categories/${categoryId}`,
})
},

/**
* Creates a forum participation gradebook item.
* @param {Object} payload The link payload.
* @param {number} payload.threadId The forum thread id used as ref_id.
* @param {number} payload.courseId The course id.
* @param {number|string} payload.categoryId The gradebook category id.
* @param {number} payload.pointsOne Points awarded for exactly one message.
* @param {number} payload.pointsMany Points awarded for two or more messages.
* @returns {Promise<Object>} The created gradebook link resource.
*/
async createForumParticipationLink({ threadId, courseId, categoryId, pointsOne, pointsMany }) {
// 11 = LINK_FORUM_PARTICIPATION. Weight equals pointsMany (the item's max points) so in
// points_sum the contribution equals the earned points.
return await baseService.post("/api/gradebook_links", {
type: 11,
refId: threadId,
course: `/api/courses/${courseId}`,
category: `/api/gradebook_categories/${categoryId}`,
weight: Number(pointsMany),
pointsOne: String(pointsOne),
pointsMany: String(pointsMany),
})
},

/**
* Updates a forum participation gradebook item.
* @param {number|string} linkId The numeric id of the gradebook link.
* @param {Object} payload The fields to update (pointsOne, pointsMany, refId).
* @returns {Promise<Object>} The updated gradebook link resource.
*/
async updateForumParticipationLink(linkId, payload) {
return await baseService.put(`/api/gradebook_links/${linkId}`, payload)
},

/**
* Deletes a gradebook link.
* @param {number|string} linkId The numeric id of the gradebook link.
* @returns {Promise<Object>}
*/
async deleteLink(linkId) {
return await baseService.delete(`/api/gradebook_links/${linkId}`)
},
}
4 changes: 4 additions & 0 deletions public/main/gradebook/gradebook_add_cat.php
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@
$cat->set_parent_id($values['hid_parent_id']);
$cat->set_weight($values['weight']);

if (isset($values['calculation_mode'])) {
$cat->setCalculationMode($values['calculation_mode']);
}

if (isset($values['generate_certificates'])) {
$cat->setGenerateCertificates(true);
} else {
Expand Down
15 changes: 14 additions & 1 deletion public/main/gradebook/gradebook_add_link.php
Original file line number Diff line number Diff line change
Expand Up @@ -93,12 +93,25 @@

$parent_cat = Category::load($addvalues['select_gradebook']);
$global_weight = $category[0]->get_weight();
$link->set_weight($addvalues['weight_mask']);

if (isset($addvalues['min_score']) && $addvalues['min_score'] !== '') {
$link->set_min_score(api_float_val($addvalues['min_score']));
}

if (LINK_FORUM_PARTICIPATION == $link->get_type()) {
$pointsOne = isset($addvalues['points_one']) && '' !== $addvalues['points_one']
? api_float_val($addvalues['points_one'])
: null;
$pointsMany = isset($addvalues['points_many']) && '' !== $addvalues['points_many']
? api_float_val($addvalues['points_many'])
: null;
$link->set_points_one($pointsOne);
$link->set_points_many($pointsMany);
// Weight is derived from the points by ForumParticipationLink::get_weight().
} else {
$link->set_weight($addvalues['weight_mask']);
}

if ($link->needs_max()) {
$link->set_max($addvalues['max']);
}
Expand Down
4 changes: 4 additions & 0 deletions public/main/gradebook/gradebook_edit_cat.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@
$cat->set_parent_id($values['hid_parent_id']);
$cat->set_weight($values['weight']);

if (isset($values['calculation_mode'])) {
$cat->setCalculationMode($values['calculation_mode']);
}

if (isset($values['generate_certificates'])) {
$cat->setGenerateCertificates($values['generate_certificates']);
} else {
Expand Down
18 changes: 16 additions & 2 deletions public/main/gradebook/gradebook_edit_link.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,6 @@
if ($form->validate()) {
$values = $form->exportValues();
$parent_cat = Category::load($values['select_gradebook']);
$final_weight = $values['weight_mask'];
$link->set_weight($final_weight);

if (!empty($values['select_gradebook'])) {
$link->set_category_id($values['select_gradebook']);
Expand All @@ -63,6 +61,22 @@
if (isset($values['min_score']) && $values['min_score'] !== '') {
$link->set_min_score(api_float_val($values['min_score']));
}

if (LINK_FORUM_PARTICIPATION == $link->get_type()) {
$pointsOne = isset($values['points_one']) && '' !== $values['points_one']
? api_float_val($values['points_one'])
: null;
$pointsMany = isset($values['points_many']) && '' !== $values['points_many']
? api_float_val($values['points_many'])
: null;
$link->set_points_one($pointsOne);
$link->set_points_many($pointsMany);
// Weight is derived from the points by ForumParticipationLink::get_weight().
$final_weight = $link->get_weight();
} else {
$final_weight = $values['weight_mask'];
}
$link->set_weight($final_weight);
$link->save();

//Update weight for attendance
Expand Down
Loading
Loading