diff --git a/astros/static/astros/css/style.css b/astros/static/astros/css/style.css index c3abd6e9..db413703 100644 --- a/astros/static/astros/css/style.css +++ b/astros/static/astros/css/style.css @@ -265,12 +265,12 @@ li{ height: 60vh; padding: 20px; border-radius: 5px; - /*box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);*/ background-color: white; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); - position:relative; - + display: flex; + flex-direction: column; + } /* Assignment displays need less height */ .assignment-li { @@ -279,14 +279,13 @@ li{ div.course-section { + display: flex; + flex-direction: column; position: relative; height: 70%; } /* Set the height of .image-container to 50% of the height of .course-li */ div.image-container { - position: absolute; - top: 0; - left: 0; height: 85%; width: 100%; /* Make the image container full-width within .course-li */ overflow: hidden; /* Ensure the image doesn't overflow the container */ @@ -295,22 +294,16 @@ div.image-container { /* Style the image to fill the .image-container */ div.image-container img.course-image { height: 100%; - width: 100%; - /*max-height: 100%; Ensure the image height fits within the container */ - /*max-width: 100%; Ensure the image width fits within the container */ + width: 100%; object-fit: cover; /* Maintain aspect ratio and cover the container */ - border-radius: 10px 10px 10px 10px; /* Optional: Add border radius to the top corners */ + border-radius: 10px; /* Optional: Add border radius to the top corners */ } -div.c-text { - position: absolute; - top: 87%; - height: 30%; +.c-text { + height: 15%; } - -.a-text.c-text { - top: 25%; +.c-description { + overflow: hidden; } - .edit-section { bottom: 5%; position: absolute; diff --git a/astros/templates/astros/prof_course_display.html b/astros/templates/astros/prof_course_display.html index c2e1ffae..9cc41ccc 100644 --- a/astros/templates/astros/prof_course_display.html +++ b/astros/templates/astros/prof_course_display.html @@ -8,12 +8,8 @@
{{course.name}} -

{% if course.description|length > 90 %} - {{ course.description|slice:":90" }}... - {% else %} - {{ course.description }} - {% endif %}

- +

+ {{ course.description }}

diff --git a/db.sqlite3 b/db.sqlite3 index 81293796..5a37e7e8 100644 Binary files a/db.sqlite3 and b/db.sqlite3 differ diff --git a/deimos/__pycache__/models.cpython-311.pyc b/deimos/__pycache__/models.cpython-311.pyc index 4bfeeebc..77be7577 100644 Binary files a/deimos/__pycache__/models.cpython-311.pyc and b/deimos/__pycache__/models.cpython-311.pyc differ diff --git a/deimos/__pycache__/views.cpython-311.pyc b/deimos/__pycache__/views.cpython-311.pyc index 5683b692..bb809168 100644 Binary files a/deimos/__pycache__/views.cpython-311.pyc and b/deimos/__pycache__/views.cpython-311.pyc differ diff --git a/deimos/migrations/0020_alter_questionstudent_num_units_attempts.py b/deimos/migrations/0020_alter_questionstudent_num_units_attempts.py new file mode 100644 index 00000000..7778f4ed --- /dev/null +++ b/deimos/migrations/0020_alter_questionstudent_num_units_attempts.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.3 on 2023-11-10 23:36 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('deimos', '0019_note_title_notetemporary_title'), + ] + + operations = [ + migrations.AlterField( + model_name='questionstudent', + name='num_units_attempts', + field=models.IntegerField(blank=True, default=0, null=True, validators=[django.core.validators.MinValueValidator(0)]), + ), + ] diff --git a/deimos/migrations/0021_questionstudent_is_complete.py b/deimos/migrations/0021_questionstudent_is_complete.py new file mode 100644 index 00000000..47b31b31 --- /dev/null +++ b/deimos/migrations/0021_questionstudent_is_complete.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.3 on 2023-11-11 22:19 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('deimos', '0020_alter_questionstudent_num_units_attempts'), + ] + + operations = [ + migrations.AddField( + model_name='questionstudent', + name='is_complete', + field=models.BooleanField(default=False), + ), + ] diff --git a/deimos/migrations/__pycache__/0020_alter_questionstudent_num_units_attempts.cpython-311.pyc b/deimos/migrations/__pycache__/0020_alter_questionstudent_num_units_attempts.cpython-311.pyc new file mode 100644 index 00000000..ee9c189c Binary files /dev/null and b/deimos/migrations/__pycache__/0020_alter_questionstudent_num_units_attempts.cpython-311.pyc differ diff --git a/deimos/migrations/__pycache__/0021_questionstudent_is_complete.cpython-311.pyc b/deimos/migrations/__pycache__/0021_questionstudent_is_complete.cpython-311.pyc new file mode 100644 index 00000000..74dfbff1 Binary files /dev/null and b/deimos/migrations/__pycache__/0021_questionstudent_is_complete.cpython-311.pyc differ diff --git a/deimos/models.py b/deimos/models.py index 1d5b33ae..e3f8d213 100644 --- a/deimos/models.py +++ b/deimos/models.py @@ -2,6 +2,7 @@ from phobos.models import Course, Question, User, Assignment, VariableInstance, QuestionChoices from django.core.validators import MaxValueValidator, MinValueValidator from .utils import * +from datetime import date class Student(User): """ Student class to handle students in the platform. @@ -115,7 +116,8 @@ class QuestionStudent(models.Model): success = models.BooleanField(default=False) var_instances = models.ManyToManyField(VariableInstance, related_name='question_students') instances_created = models.BooleanField(default=False) - num_units_attempts = models.IntegerField(null=True, blank=True, validators=[MinValueValidator(0)]) + num_units_attempts = models.IntegerField(default=0, null=True, blank=True, validators=[MinValueValidator(0)]) + is_complete = models.BooleanField(default=False) def create_instances(self): """ Get variable instances from the variables associated to the question. @@ -231,6 +233,62 @@ def get_num_attempts(self): Calculates and returns the number of attempts on a question by a user. """ return self.attempts.count() + def get_too_many_attempts(self): + """ + Returns True if no attempts left for this question + """ + if self.question.answer_type.startswith('STRUCT'): + return self.get_num_attempts() >= self.question.struct_settings.max_num_attempts + else: + return self.get_num_attempts() >= self.question.mcq_settings.mcq_max_num_attempts + def get_units_too_many_atempts(self): + """ + Returns True if no attempts left for units in this question. + """ + if not self.question.answer_type.startswith('STRUCT'): + return True + return self.num_units_attempts >= self.question.struct_settings.units_num_attempts + def get_status(self): + """ + Returns True if the question (including sub questions) have been completed, + Otherwise False + """ + parent_question = self.question.parent_question if self.question.parent_question else self.question + question_students = QuestionStudent.objects.filter(question__parent_question=parent_question) + for qs in question_students: + if not qs.is_complete: + return False + return True + + def get_potential(self, no_unit = False): + """ + Returns the fraction of number of points the student can get for that question. + If no_unit, then we don't add the units points in the potential + """ + try: + days_overdue = max(0, (date.today() - self.question.assignment.due_date.date()).days) + overall_percentage = max(self.question.assignment.grading_scheme.floor_percentage, \ + 1 - days_overdue * self.question.assignment.grading_scheme.late_sub_deduct) + except: + overall_percentage = 1 + # overall_percentage is the possible percentage of points a student can get. + # it is used to reduce the points in case of late submissions. + t = int(not self.get_too_many_attempts()) + t_u = int(not self.get_units_too_many_atempts()) + n_a = self.get_num_attempts() # number of attempts + if self.question.answer_type.startswith('STRUCT'): + p_u = self.question.struct_settings.percentage_pts_units + d = self.question.struct_settings.deduct_per_attempt # percentage deduct per attempt + if no_unit: + potential = t * (1 - p_u + ((d * n_a) * (p_u - 1))) + else: + potential = t * (1 - p_u + ((d * n_a) * (p_u - 1))) + p_u * t_u # This is the most use and general + # case for structural questions. + else: + d = self.question.mcq_settings.mcq_deduct_per_attempt + potential = t * (1 - (d * n_a)) + return potential * overall_percentage + def __str__(self): return f"Question-Student:{self.question} {self.student.username}" diff --git a/deimos/static/deimos/css/style.css b/deimos/static/deimos/css/style.css index f51c0b4e..5f3734ef 100644 --- a/deimos/static/deimos/css/style.css +++ b/deimos/static/deimos/css/style.css @@ -374,16 +374,27 @@ li{ .course-li { /*font-family: 'Courier New', Courier, monospace;*/ + display: flex; font-family:'Nebula', 'Times New Roman', Times, serif; flex: 1 0 calc(33.33% - 20px); /* Distribute the items equally in three columns */ height: 60vh; padding: 20px; border-radius: 5px; - position:relative; background-color: white; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); + position:relative; + overflow: hidden; + color:black; } +.course-li a { + color: inherit; + width: 100%; +} +.course-li a:hover { + text-decoration: none; + color: inherit; +} /* Assignment displays need less height */ .assignment-li { height: 25vh; @@ -391,53 +402,31 @@ li{ div.course-section { - position: relative; - height: 70%; + max-height: 100%; + height: 100%; + width: 100%; + display: flex; + flex-direction: column; } /* Set the height of .image-container to 50% of the height of .course-li */ div.image-container { - position: absolute; - top: 0; - left: 0; - height: 85%; + height: 70%; width: 100%; /* Make the image container full-width within .course-li */ - overflow: hidden; /* Ensure the image doesn't overflow the container */ + /* overflow: hidden; Ensure the image doesn't overflow the container */ +} +.c-text { + height: 30%; + overflow: hidden; } /* Style the image to fill the .image-container */ div.image-container img.course-image { height: 100%; width: 100%; - /*max-height: 100%; Ensure the image height fits within the container */ - /*max-width: 100%; Ensure the image width fits within the container */ - object-fit: cover; /* Maintain aspect ratio and cover the container */ - border-radius: 10px 10px 10px 10px; /* Optional: Add border radius to the top corners */ -} -div.c-text { - position: absolute; - top: 87%; - height: 30%; -} - -.a-text.c-text { - top: 25%; -} - -.edit-section { - bottom: 5%; - position: absolute; + border-radius: 10px; + object-fit: cover; } -.edit-btn { - background-color: rgba(23, 137, 252, 1) !important; - transition: background-color 0.2s !important; - color: rgba(0,0,0,0.95) !important; - font-weight:400 !important; - font-family: 'Aileron', sans-serif; -} -.edit-btn:hover { - background-color: rgba(23, 137, 252, 0.7) !important; -} @@ -595,7 +584,24 @@ div.c-text { justify-content:space-between; align-items: center; } +.question-block { + width:90%; +} +.status-container { + border-radius: 8px; + box-shadow: 13px 13px 20px #cbced1; + display: flex; /* Enable Flexbox */ + flex-direction: row; /* Align children in a row */ + justify-content: space-evenly; /* Distribute space around items */ + align-items: center; /* Align items vertically at the center */ + font-size:x-small; + width:fit-content; +} +.status-container > div { + margin: 0 10px; /* Optional: Add some space between the divs */ + color: gray; +} .image-in-formatted { max-width: 90%; /* Limit image width to 100% of parent div's width */ max-height: 300px; /* Limit image height to 100% of parent div's height */ @@ -854,9 +860,6 @@ ion-icon:hover{ .question-li { height: 30vh; } - .a-text.c-text { - top: 15%; - } .sf-container { display: flex; flex-direction: row; @@ -872,6 +875,9 @@ ion-icon:hover{ } @media screen and (max-width: 450px){ + .status-container { + display: none; + } div.message { top: 14%; left:30%; diff --git a/deimos/static/deimos/js/answer_question.js b/deimos/static/deimos/js/answer_question.js index 614c7345..c1cb9eca 100644 --- a/deimos/static/deimos/js/answer_question.js +++ b/deimos/static/deimos/js/answer_question.js @@ -486,7 +486,13 @@ document.addEventListener('DOMContentLoaded', ()=> { calculatorDiv.querySelector('.units-screen').disabled = true; displayFeedback(feedbackContainerDiv, 'You exhausted your units attempts for this question'); } - }); + + setTimeout(()=>{ + updateQuestionStatus(form, result); + }, toggleTime) + + + }); } else if( question_type ==='mcq'){ // TODO make sure some mcqs are selected as true. const ids_ = getSelectedTrueAnswerIds(form) @@ -515,7 +521,10 @@ document.addEventListener('DOMContentLoaded', ()=> { submitBtn.style.display = 'none'; }, toggleTime) } - }); + setTimeout(()=>{ + updateQuestionStatus(form, result); + }, toggleTime) + }); }else if(question_type ==='mp'){ fetch(`${baseUrl}/validate_answer/${qid}`, { method: 'POST', @@ -573,7 +582,9 @@ document.addEventListener('DOMContentLoaded', ()=> { setLights(redLight, yellowLight, greenLight,null, index=0); }, toggleTime) // set the lights to red } - + setTimeout(()=>{ + updateQuestionStatus(form, result); + }, toggleTime) }) }else { alert('Something went wrong'); @@ -703,7 +714,7 @@ document.addEventListener('DOMContentLoaded', ()=> { yellowLight.classList.remove('activated'); yellowLight.classList.remove('blinking'); greenLight.classList.add('activated'); - }, 1000); + }, toggleTime); } else { setTimeout(function () { @@ -743,7 +754,7 @@ document.addEventListener('DOMContentLoaded', ()=> { } } - }, 1000); + }, toggleTime); } @@ -835,16 +846,36 @@ document.addEventListener('DOMContentLoaded', ()=> { return selectedAnswerIds; } - function feedback_message(message){ - if(message != 'None'){ - alert(message); - } + function updateQuestionStatus(form, result){ + // Define an object mapping keys to their respective class names + const statusMappings = { + grade: '.status-grade', + numAttempts: '.status-num-attempts', + potential: '.status-potential', + unitsNumAttempts:'.status-units-num-attempts' + }; + + // Loop through each key in the statusMappings object + for (const key in statusMappings) { + if (statusMappings.hasOwnProperty(key)) { + // Construct the selector using the mapping + const selector = statusMappings[key] + ' .status-value'; + const qb = form.closest('.question-block'); + // Update the innerHTML of the element matching the selector + const element = qb.querySelector(selector); + if(element != null){ + element.innerHTML = result[key]; + } + if(result.success){ + qb.querySelector('.status-potential').style.display = 'none'; + } + } + } + } - - // Detecting latex in questions and displaying. const questionContentPs = document.querySelectorAll(".question-content"); MathJax.typesetPromise().then(() => { diff --git a/deimos/static/deimos/js/search_generic.js b/deimos/static/deimos/js/search_generic.js index d14cbf52..718dd702 100644 --- a/deimos/static/deimos/js/search_generic.js +++ b/deimos/static/deimos/js/search_generic.js @@ -7,7 +7,7 @@ document.addEventListener('DOMContentLoaded', ()=>{ parentDivName = 'course-li'; // most pages will have course-li } if(displayProperty == null){ - displayProperty = 'block'; + displayProperty = 'flex'; } // corresponding to searchField.dataset.name if(searchField != null){ // Add an event listener to the search field for the 'keydown' event diff --git a/deimos/templates/deimos/answer_question.html b/deimos/templates/deimos/answer_question.html index 4656b32f..19295fcd 100644 --- a/deimos/templates/deimos/answer_question.html +++ b/deimos/templates/deimos/answer_question.html @@ -24,7 +24,59 @@
{% for index, question_dict in questions_dict.items %} +

Question {{question_dict.question.number}}

+
+ + {% if question_dict.questtype == 'struct' %} +
+ Grade: {{question_dict.num_points}}/{{question_dict.question.struct_settings.num_points}} +
+ +
+ attempts: {{question_dict.question_student.get_num_attempts}}/{{question_dict.question.struct_settings.max_num_attempts}} +
+ +
+ {% if not question_dict.question_student.success %} + Potential: {{question_dict.potential}}% + {% endif %} +
+ +
+ deduct per attempt: {{question_dict.question.struct_settings.deduct_per_attempt|multiply:"100" }}% +
+ +
+ {% if question_dict.answer.answer_unit %} + units attempt: {{question_dict.question_student.num_units_attempts}}/{{question_dict.question.struct_settings.units_num_attempts}} + {% endif %} +
+ + {% else %} +
+ Grade: {{question_dict.num_points}}/{{question_dict.question.mcq_settings.num_points}} +
+ +
+ attempts: {{question_dict.question_student.get_num_attempts}}/{{question_dict.question.mcq_settings.mcq_max_num_attempts}} +
+ +
+ {% if not question_dict.question_student.success %} + Potential: {{question_dict.potential}}% + {% endif %} +
+ +
+ deduct per attempt: {{question_dict.question.mcq_settings.mcq_deduct_per_attempt|multiply:"100" }}% +
+ {% endif %} +

{% csrf_token %} @@ -177,7 +229,11 @@

Question {{question_dict.question.number}}

{% elif question_dict.questtype == 'mp' %} -
Match the following pairs
+ {% if not question_dict.question_student.success %} +
Match the following pairs
+ {% else %} +
You correctly matched the pairs.
+ {% endif %}
@@ -298,6 +354,7 @@

Question {{question_dict.question.number}}

{% endif %}
+
{% endfor %}