apluslms / a-plus

A+ frontend portal - A+ LMS documentation:
https://apluslms.github.io/
Other
66 stars 73 forks source link

Interactive points counter and performance prediction #419

Open markkuriekkinen opened 4 years ago

markkuriekkinen commented 4 years ago

A+ should have a grade calculator. The course configuration would define grade limits and then students could see which grade they have earned based on their points.

Students should also be able to enter points manually so that they can check which grade they would get with certain points.

A+ should be able to predict students' future performance based on past performance so that teachers can see which students are in danger of dropping out.

Currently, A+ has a hacky JavaScript grade calculator that can be added to the course sidebar by an admin. It shows the grade the student has earned with their points thus far. The grade limits are hardcoded in the JS code.

Mikael-Lenander commented 1 year ago

Where should the grade limits be stored, exactly? In the CourseInstance model? Does this require a database migration? Where can I find the "hacky JavaScript grade calculator"? It could be a good starting point. On which page should the students be able to enter points manually?

markkuriekkinen commented 1 year ago

The existing hacky JavaScript grade calculator

How to add a grade calculator to A+ (based on exercise difficulty levels A-B-C) Arvosanalaskurin lisääminen A+-kurssille (tehtävän vaikeusasteen (difficulty) mukaan)

This requires an admin user in A+. Django administration (URL path /admin) -> Apps -> Html plugins -> Add new Html plugin

Container type: course_instance Object ID: course instance ID (number), can be found in the course instances list in Django admin Title: Pistelaskuri / Grade calculator Views: results Content: Copy the HTML and JavaScript code below. (you must update the grade limits and course-specific links) (there are variations of this code in different courses)

O1

<div class="well total-score onlyfi">
<h4>Kerätyt pisteet</h4>
<ul class="list-unstyled">
<li>A: <strong class="A-unconfirmed">0</strong></li>
<li>B: <strong class="B-unconfirmed">0</strong></li>
<li>C: <strong class="C-unconfirmed">0</strong></li>
</ul>
<p>Arvosana: <strong class="grade-unconfirmed">0</strong></p>
<p><small>
<a href="https://plus.cs.aalto.fi/o1/2019/w01/ch01/#tehtavapisteet-ja-arvosana">Arvosteluperusteet</a>
</small></p>
</div>

<div class="well total-score onlyen">
<h4>Points collected</h4>
<ul class="list-unstyled">
<li>A: <strong class="A-unconfirmed">0</strong></li>
<li>B: <strong class="B-unconfirmed">0</strong></li>
<li>C: <strong class="C-unconfirmed">0</strong></li>
</ul>
<p>Grade: <strong class="grade-unconfirmed">0</strong></p>
<p><small>
<a href="https://plus.cs.aalto.fi/o1/2019/w01/ch01/#from-assignment-points-to-a-course-grade">Grade limits</a>
</small></p>
</div>

<script>
$(function() {
var maxPoints = aplusPointsTotal.max_points_by_difficulty;
var unconfirmedPoints = aplusPointsTotal.unconfirmed_points_by_difficulty;
var points = aplusPointsTotal.points_by_difficulty;
var wrap = $('.total-score');

var levels = ['A','B','C'];
for (var i in levels) {
  var key = levels[i];
  if (!unconfirmedPoints[key]) { unconfirmedPoints[key] = 0; }
  if (!points[key]) { points[key] = 0; }
  unconfirmedPoints[key] += points[key];
  wrap.find('.'+key+'-unconfirmed').text(unconfirmedPoints[key]);
  //wrap.find('.'+key+'-points').text(points[key]);
}

var o1grade = function(pointsObject) {
  var points = [pointsObject.A, pointsObject.B, pointsObject.C];
  var boundaries = [
    [1900, 0, 0],
    [2100, 400, 0],
    [2100, 750, 0],
    [2100, 750, 500],
    [2100, 750, 750]
  ];
  var grade = 0;
  for (var gr=0; gr < boundaries.length; gr++) {
    var bound = boundaries[gr];
    for (var cl=0; cl < bound.length; cl++) {
      var p = bound[cl];
      if (points[cl] < p) {
        for (var i=cl+1; i<points.length; i++) {
          if (points[i] > (p-points[cl])) {
            points[i] -= (p-points[cl]);
            points[cl] = p;
            break;
          } else {
            points[cl] += points[i];
            points[i] = 0;
          }
        }
        if(points[cl] >= p) {
          continue; /* passed current class for current grade. */
        }
        return grade;
      }
    }
    grade++;
  }
  return grade;
};
wrap.find('.grade-unconfirmed').text(o1grade(unconfirmedPoints));
//wrap.find('.grade').text(o1grade(points));
});
</script>

TRAK Y

<div class="well total-score">
<h4>Kerätyt pisteet</h4>
<ul class="list-unstyled">
<li>A: <strong class="A-unconfirmed">0</strong></li>
<li>B: <strong class="B-unconfirmed">0</strong></li>
<li>C: <strong class="C-unconfirmed">0</strong></li>
</ul>
<div>
<p>Arvosana: <strong class="grade-unconfirmed">0</strong></p>
<p><small><a href="https://plus.cs.aalto.fi/a1141/2019/johdanto/kurssin_esittely/#harjoitusten-arvosanarajat">Arvosteluperusteet</a></small></p>
</div>
</div>
<script>
$(function() {
var maxPoints = aplusPointsTotal.max_points_by_difficulty;
var unconfirmedPoints = aplusPointsTotal.unconfirmed_points_by_difficulty;
var points = aplusPointsTotal.points_by_difficulty;
var wrap = $('.total-score');

var levels = ['A','B','C'];
for (var i in levels) {
  var key = levels[i];
  if (!unconfirmedPoints[key]) { unconfirmedPoints[key] = 0; }
  if (!points[key]) { points[key] = 0; }
  unconfirmedPoints[key] += points[key];
  wrap.find('.'+key+'-unconfirmed').text(unconfirmedPoints[key]);
  //wrap.find('.'+key+'-points').text(points[key]);
}

var o1grade = function(pointsObject) {
  var points = [pointsObject.A, pointsObject.B, pointsObject.C];
  var boundaries = [
    [9025, 350, 0],
    [9500, 3500, 0],
    [9500, 7000, 0],
    [9500, 7000, 2000],
    [9500, 7000, 4000]
  ];
  var grade = 0;
  for (var gr=0; gr < boundaries.length; gr++) {
    var bound = boundaries[gr];
    for (var cl=0; cl < bound.length; cl++) {
      var p = bound[cl];
      if (points[cl] < p) {
        for (var i=cl+1; i<points.length; i++) {
          if (points[i] > (p-points[cl])) {
            points[i] -= (p-points[cl]);
            points[cl] = p;
            break;
          } else {
            points[cl] += points[i];
            points[i] = 0;
          }
        }
        if(points[cl] >= p) {
          continue; /* passed current class for current grade. */
        }
        return grade;
      }
    }
    grade++;
  }
  return grade;
};
wrap.find('.grade-unconfirmed').text(o1grade(unconfirmedPoints));
//wrap.find('.grade').text(o1grade(points));
});
</script>
markkuriekkinen commented 1 year ago

Where should the grade limits be stored, exactly? In the CourseInstance model? Does this require a database migration? Where can I find the "hacky JavaScript grade calculator"? It could be a good starting point. On which page should the students be able to enter points manually?

@Mikael-Lenander The grade limits have to be stored in the database. The CourseInstance model would be one choice, but I am afraid we are starting to have too bloated models that have over a dozen attributes and several large TEXT attributes. It might be better to make a separate model for the course grade limits and use a ForeignKey from CourseInstance (or a OneToOne relation depending on the overall structure). Though, we don't have any hard evidence if a separate table performs better than placing all attributes in the same CourseInstance model. However, not all courses are going to use these grade limits, thus it should be better that we use a separate table and the ForeignKey. Then, we don't have several CourseInstances with an empty TEXT (or whatever the type is) attribute for the unused grade limits.

This requires a database migration since we need to change the database schema in order to store the grade limits somewhere. We don't have any existing attributes in the tables that could be used for the grade limits.

On which page should the students be able to enter points manually? You will need to think about the user interface and usability first so that we can figure out the best solution. Perhaps one option is to make the calculator itself do this, that is, the calculator would first show the expected grade based on the real points, but you could then enter points manually there to see how the grade changes.