Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Features/assessment tracker #59

Open
wants to merge 6 commits into
base: open-release/quince.master
Choose a base branch
from
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
120 changes: 119 additions & 1 deletion common/djangoapps/student/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
from lms.djangoapps.verify_student.models import VerificationDeadline
from lms.djangoapps.verify_student.services import IDVerificationService
from lms.djangoapps.verify_student.utils import is_verification_expiring_soon, verification_for_datetime
from lms.djangoapps.courseware.courses import get_course_date_blocks, get_course_with_access
from lms.djangoapps.courseware.courses import get_course_date_blocks, get_course_with_access, get_first_component_of_block
from lms.djangoapps.courseware.date_summary import TodaysDate
from lms.djangoapps.courseware.context_processor import user_timezone_locale_prefs
from lms.djangoapps.course_home_api.dates.serializers import DateSummarySerializer
Expand All @@ -61,6 +61,13 @@
from openedx.core.lib.time_zone_utils import get_time_zone_offset
from xmodule.data import CertificatesDisplayBehaviors # lint-amnesty, pylint: disable=wrong-import-order

from lms.djangoapps.courseware.models import StudentModule
from common.djangoapps.student.models.user import anonymous_id_for_user
from submissions.models import StudentItem, Submission
from submissions import api as submissions_api
from xmodule.modulestore.django import modulestore
from openedx.core.djangoapps.enrollments.data import get_course_enrollments

# Enumeration of per-course verification statuses
# we display on the student dashboard.
VERIFY_STATUS_NEED_TO_VERIFY = "verify_need_to_verify"
Expand Down Expand Up @@ -916,3 +923,114 @@ def get_course_dates_for_email(user, course_id, request):

course_date_list = list(map(_remove_date_key_from_course_dates, course_date_list))
return course_date_list


def get_assessments_for_courses(request):
user = User.objects.get(email = request.user.email)
user_courses = list(get_course_enrollments(user.username))
all_blocks_data = []
for i, user_course in enumerate(user_courses):
course_key_string = user_course["course_details"]["course_id"]
course_key = CourseKey.from_string(course_key_string)

course_usage_key = modulestore().make_course_usage_key(course_key)
block_data = get_course_blocks(user, course_usage_key, allow_start_dates_in_future=True, include_completion=True)
for section_key in block_data.get_children(course_usage_key):
for subsection_key in block_data.get_children(section_key):
start = block_data.get_xblock_field(subsection_key, 'start')
due = block_data.get_xblock_field(subsection_key, 'due')
showNotSubmitted = True if due is not None and datetime.now() > due.replace(tzinfo=None) else False
ignoreUnit = False
temp = {"course_name" : user_course["course_details"]["course_name"], "title" : block_data.get_xblock_field(subsection_key, 'display_name'), "start_date" : start, "date" : due, "link" : reverse('jump_to', args=[course_key, subsection_key])}
units = block_data.get_children(subsection_key)
if not units:
ignoreUnit = True
continue
while units:
unit = units.pop()

components = block_data.get_children(unit)
problemSubmissionStatus, problemType = [], False
for component in components:
category = block_data.get_xblock_field(component, 'category')
if category not in ["edx_sga", "openassessment", "problem", "freetextresponse"]:
ignoreUnit = True
continue

block_id = get_first_component_of_block(component, block_data)
student_module_info = StudentModule.get_state_by_params(course_key_string, [block_id], user.id).first()
student_item = {"student_id" : anonymous_id_for_user(request.user, course_key_string), "course_id" : course_key_string, "item_id" : block_id, "item_type" : "sga" if category == "edx_sga" else category}
score = submissions_api.get_score(student_item)
temp["is_graded"] = "Graded" if score and (score["points_earned"] or score["points_earned"] == 0) else "Not Graded"
if not student_module_info:
temp["submission_status"] = "Not Submitted" if showNotSubmitted else "-"
temp["is_graded"] = "-"
problemSubmissionStatus.append("Not Submitted")
submission_state = {}
else:
submission_state = json.loads(student_module_info.state)

if category in ["freetextresponse"]:
if submission_state.get("student_answer", None) and not submission_state.get("count_attempts", None):
temp["submission_status"] = "In Progress"
temp["is_graded"] = "Not Graded" if showNotSubmitted else "-"
elif submission_state.get("student_answer", None) and submission_state.get("count_attempts", None):
temp["submission_status"] = "Submitted"



elif category in ["edx_sga"]:
try:
submission_id = StudentItem.objects.get(**student_item)
sga_submissions = Submission.objects.filter(student_item=submission_id).first()
if sga_submissions.answer.get("finalized"):
temp["submission_status"] = "Submitted"

elif not sga_submissions.answer.get("finalized"):
temp["submission_status"] = "In Progress"
temp["is_graded"] = "Not Graded" if showNotSubmitted else "-"
except Exception as err:
log.info(err)
temp["submission_status"] = "Not Submitted" if showNotSubmitted else "-"
temp["is_graded"] = "-"

elif category in ["openassessment"]:

if "submission_uuid" in submission_state:
temp["submission_status"] = "Submitted"

elif submission_state and "has_saved" in submission_state:
temp["submission_status"] = "In Progress"
temp["is_graded"] = "Not Graded" if showNotSubmitted else "-"
else:
temp["submission_status"] = "Not Submitted" if showNotSubmitted else "-"
temp["is_graded"] = "-"

elif category in ["problem"]:
if (student_module_info and student_module_info.state and "last_submission_time" in student_module_info.state):
problemSubmissionStatus.append("Submitted")
elif ("score" in submission_state and "raw_earned" in submission_state["score"] and submission_state["score"]["raw_earned"] == 0):
problemSubmissionStatus.append("In Progress")
problemType = True

if not ignoreUnit:
if problemType:
if all([status == "Submitted" for status in problemSubmissionStatus ]):
temp["submission_status"] = "Submitted"
temp["is_graded"] = "Graded"
elif all([status == "Not Submitted" for status in problemSubmissionStatus ]):
temp["submission_status"] = "Not Submitted" if showNotSubmitted else "-"
temp["is_graded"] = "-"
else:
temp["submission_status"] = "Submitted" if showNotSubmitted else "In Progress"
temp["is_graded"] = "Graded" if showNotSubmitted else "-"

all_blocks_data.append(temp)


filtered_sorted_date_blocks = sorted(all_blocks_data, key=lambda x: x['start_date'])
user_local_timezone = user_timezone_locale_prefs(request)
return {
'date_blocks': filtered_sorted_date_blocks,
"user_timezone" : user_local_timezone if user_local_timezone["user_timezone"] is not None else {"user_timezone" : ""}
}
1 change: 1 addition & 0 deletions common/djangoapps/student/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,4 @@


urlpatterns += [re_path(r'^assessment_tracker', views.user_tracker_link, name='user_tracker_link')]
re_path(r'^extras/new_assessment_tracker', views.user_assessments_tracker_link, name='user_assessments_tracker_link')
8 changes: 7 additions & 1 deletion common/djangoapps/student/views/management.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
from django.core.validators import ValidationError, validate_email
from django.core.cache import cache
from openedx.core.djangoapps.enrollments.api import add_enrollment
from common.djangoapps.student.helpers import DISABLE_UNENROLL_CERT_STATES, cert_info, do_create_account
from common.djangoapps.student.helpers import DISABLE_UNENROLL_CERT_STATES, cert_info, do_create_account, get_assessments_for_courses
from django.core.exceptions import ObjectDoesNotExist
from openedx.core.djangoapps.user_authn.views.registration_form import AccountCreationForm
from django.shortcuts import redirect, render
Expand Down Expand Up @@ -1464,3 +1464,9 @@ def extras_update_user_details(request):
old_user.last_name = lastName
old_user.save()
return HttpResponse("Saved")


@login_required
def user_assessments_tracker_link(request):
output_dict = get_assessments_for_courses(request)
return render(request, 'user_assessment_tracker_link.html', {'data': output_dict, 'program_image_url': configuration_helpers.get_value("MKTG_URLS", True)["HEADER_LOGO"]})
217 changes: 217 additions & 0 deletions lms/templates/user_assessment_tracker_link.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
{% load static %}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<link href="https://static.talentsprint.com/extras/table_template/css/bootstrap.min.css" rel="stylesheet">
<link href="https://code.jquery.com/ui/1.13.2/themes/base/jquery-ui.css" rel="stylesheet">
<link href="https://cdn.datatables.net/1.13.6/css/dataTables.jqueryui.min.css" rel="stylesheet">
<link href="https://cdn.datatables.net/scroller/2.2.0/css/scroller.jqueryui.min.css" rel="stylesheet">
<link rel="shortcut icon" href="https://static.talentsprint.com/favicon.ico" type="image/x-icon" >
<style>
table.dataTable>thead>tr>th{background-color:#0075bb;color: white;}
h1{text-align: center;margin-top: 20px;font-size: 30px;font-weight: 700;margin-bottom: 50px;}
h5{text-align: center;text-decoration: underline;font-weight: 700;color: #2b78e4;}
img.logo{height: 60px;display: block;margin-top: 20px;margin-left: auto;margin-right: auto;}
.dt-buttons {margin-bottom: 10px;}
.dt-buttons.btn-group{float: left;margin-right: 2%;}
.dataTables_filter {float: right!important;margin-top: 4px;margin-right: 0%;text-align: left;}
.dataTables_wrapper .ui-toolbar {padding-right: 0px;}
.dataTables_info {float: right;}
.dataTables_length{float: right;margin-top: 4px;margin-left: 2%;}
#example_info{display: none;}
.ui-widget-header {border-bottom: none;background: none;color: black;border-left: none;border-top: none;border-right: none;}
.table-striped>tbody>tr:nth-of-type(odd) {--bs-table-accent-bg: none;}
.table-striped>tbody>tr:nth-of-type(odd) {background-color: white;}
.table-striped>tbody>tr:nth-of-type(even){background-color: #eee;}
.dataTables_wrapper .dataTables_filter input{outline-offset: 0px;width: 220px;border: 2px solid #aaa;}
.dataTables_wrapper .dataTables_filter input {border-radius: 6px;}
.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody{max-height: 360px!important;}
.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody::-webkit-scrollbar {display: none;}
div.dataTables_scrollHead table {border-top-left-radius: 10px;}
div.dataTables_scrollHead th:first-child {border-top-left-radius: 10px;}
div.dataTables_scrollHead th:last-child {border-top-right-radius: 10px;}
.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody{border-bottom-left-radius: 10px;border-bottom-right-radius: 10px;}
table th, table td{text-align: center!important;}
table th {vertical-align: middle;font-size: 17px}
table td {font-size: 15px}
button.btn.btn-success.dues{font-size: 12px;}
td span svg{height: 20px;color: green;}
table td:nth-child(4), table th:nth-child(4), table td:nth-child(5), table th:nth-child(5){
width: 169px;
}
.dataTables_scroll{
border-right: 1px solid #ccc;
border-bottom: 1px solid #ccc;
border-left: 1px solid #ccc;
border-radius: 10px;
}
@media only screen and (max-width: 600px) {
img.logo {height: 37px;display: block;margin-top: 20px;margin-left: auto;margin-right: auto;}
div.container {width: 100%;max-width: 100%;}
#myTable th{padding: 12px;}
#myTable td {padding: 12px;}
.table>thead {vertical-align: middle;}
.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody{border-bottom-left-radius: 10px;border-bottom-right-radius: 10px;border-bottom: none}
table.dataTable>thead>tr>th, table.dataTable>thead>tr>td {padding: 8px 30px;font-size: 15px;}
table.dataTable tbody th, table.dataTable tbody td {padding: 8px 5px;font-size: small;}
h1{font-size: 20px;margin-bottom: 20px;}
}
#tableData td:nth-child(4), #tableData td:nth-child(5){
white-space: nowrap !important;
}
#tableData td:nth-child(1){
white-space: wrap !important;
}
</style>
</head>
<body>
<div class="container">
<img class="logo" src="{% block program_image_url %}{{ program_image_url }}{% endblock program_image_url %}">
<h1>Assessment Tracker</h1>
{% block table %}
<div class="table-responsive table-responsive-xs table-responsive-sm table-responsive-md table-responsive-lg table-responsive-xl tableFixHead">
<table id="example" class="table table-striped table-bordered" cellspacing="0" width="100%">
<thead>
<tr class="header">
<th>Module</th>
<th>Assessment</th>
<th>Release Date</th>
<th>Due Date</th>
<th>Submission Status</th>
<th>Evaluation Status</th>
</tr>
</thead>
<tbody id="tableData">
{% if data and data.date_blocks %}
{% for item in data.date_blocks %}
{% if "Open Response Assessment" in item.title and "Submission" in item.title or "Open Response Assessment" not in item.title %}
<tr>
<td>{{ item.course_name }}</td>
<td>
{% if item.link == '' %}
{{ item.title }}
{% else %}
<a href="{{ item.link }}" target = "_blank">{{ item.title }}</a>
{% endif %}
</td>

<td style="text-wrap: nowrap;">{{item.start_date|date:'Y-m-d H:i'}}</td>
{% if item.date != "None" %}
<td>{{ item.date|date:'Y-m-d H:i' }}</td>
{%else%}
<td>-</td>
{%endif%}
<td>{{item.submission_status}}</td>
<td>{{ item.is_graded }}</td>
</tr>
{%endif%}
{% endfor %}
{% endif %}
</tbody>
</table>

</div>
{% endblock table %}
</div>
<script src="https://code.jquery.com/jquery-3.7.0.js"></script>
<script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.min.js"></script>
<script src="https://cdn.datatables.net/1.13.6/js/dataTables.jqueryui.min.js"></script>
<script src="https://cdn.datatables.net/scroller/2.2.0/js/dataTables.scroller.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.1/moment.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment-timezone/0.5.11/moment-timezone-with-data-2010-2020.min.js"></script>

<script>
$(document).ready(function() {
// Uncommented custom search functionality
$.fn.dataTable.ext.search.push(function(settings, searchData, index, rowData, counter) {
var searchValue = settings.oPreviousSearch.sSearch.toLowerCase();

if (searchValue.includes(" ")) {
return searchData.join(' ').toLowerCase().includes(searchValue);
}

var searchTerms = searchValue.split(" ");
for (var i = 0; i < searchTerms.length; i++) {
var termFound = false;
for (var j = 0; j < searchData.length; j++) {
if (searchData[j].toLowerCase().includes(searchTerms[i])) {
termFound = true;
break;
}
}
if (!termFound) {
return false;
}
}
return true;
});

// DataTable initialization
var table = new DataTable('#example', {
fixedHeader: true,
paging: false,
responsive: true,
deferRender: true,
scrollCollapse: false,
scroller: false,
scrollX: true,
language: {
search: "",
searchPlaceholder: "Search..."
},
order: [[3, 'desc']]
});
// Search functionality on keyup
$('#example_filter input').on('keyup', function() {
table.search(this.value).draw();
});
});
var maxCharacters = 28;
$('table td:nth-child(1)').each(function () {
var text = $(this).text();
var words = text.split(' ');
for (var i = 0; i < words.length; i++) {
if (words[i].length > maxCharacters) {
words[i] = words[i].substring(0, maxCharacters) + words[i].substring(maxCharacters);
// Add a line break after every 28 characters
words[i] = words[i].replace(/(.{28})/g, "$1<br>");
}
}
var wrappedText = words.join(' ');
$(this).html(wrappedText);
});
get_user_timezone = {{data.user_timezone | safe }}
user_set_zone = get_user_timezone["user_timezone"]
$('table td:nth-child(3)').each(function () {
var text = $(this).text();
//console.log(moment.utc(text, 'MMM. D, YYYY [at] h:mm A z').local().format('YYYY-MM-DD HH:mm:ss'))
utcCutoff = moment.utc(text, 'YYYY-MM-DD HH:mm');
console.log(utcCutoff.format("YYYY-MM-DD HH:mm"))
if (user_set_zone != "") {
$(this).text(utcCutoff.tz(user_set_zone).format("YYYY-MM-DD hh:mm A"))
}else {
console.log(moment.utc(text, 'YYYY-MM-DD HH:mm').local().toDate())
$(this).text(moment.utc(text, 'YYYY-MM-DD HH:mm').local().format('YYYY-MM-DD hh:mm A'))
}
});
$('table td:nth-child(4)').each(function () {
var text = $(this).text();
if(text != "") {
utcCutoff = moment.utc(text, 'YYYY-MM-DD HH:mm');
if (user_set_zone != "") {
$(this).text(utcCutoff.tz(user_set_zone).format("YYYY-MM-DD hh:mm A"))
}else {
$(this).text(moment.utc(text, 'YYYY-MM-DD HH:mm').local().format('YYYY-MM-DD hh:mm A'))
}
}
else {
$(this).text("-");
}
});

</script>
</body>
</html>


Loading