Skip to content

Commit

Permalink
[frontend/course] Additional fields for student (#832)
Browse files Browse the repository at this point in the history
  • Loading branch information
Drumor committed Mar 29, 2023
1 parent 2b054c4 commit cebfaad
Show file tree
Hide file tree
Showing 14 changed files with 301 additions and 76 deletions.
53 changes: 0 additions & 53 deletions inginious/common/additional_field.py

This file was deleted.

16 changes: 8 additions & 8 deletions inginious/frontend/courses.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from typing import List
from collections import OrderedDict

from inginious.common.additional_field import AdditionalField
from inginious.frontend.user_settings.course_user_setting import CourseUserSetting
from inginious.common.tags import Tag
from inginious.frontend.accessible_time import AccessibleTime
from inginious.frontend.parsable_text import ParsableText
Expand Down Expand Up @@ -81,10 +81,10 @@ def __init__(self, courseid, content, course_fs, task_factory, plugin_manager, t
self._lti_keys = self._content.get('lti_keys', {})
self._lti_send_back_grade = self._content.get('lti_send_back_grade', False)
self._tags = {key: Tag(key, tag_dict, self.gettext) for key, tag_dict in self._content.get("tags", {}).items()}
self._additional_fields = {key: AdditionalField(key,
field_dict["description"],
field_dict["type"])
for key, field_dict in self._content.get("fields", {}).items()}
self._course_user_setting = {key: CourseUserSetting(key,
field_dict["description"],
field_dict["type"])
for key, field_dict in self._content.get("fields", {}).items()}
task_dispenser_class = task_dispensers.get(self._content.get('task_dispenser', 'toc'), TableOfContents)
# Here we use a lambda to encourage the task dispenser to pass by the task_factory to fetch course tasks
# to avoid them to be cached along with the course object. Passing the task factory as argument
Expand Down Expand Up @@ -243,11 +243,11 @@ def get_description(self, language):
def get_tags(self):
return self._tags

def get_additional_fields(self):
def get_course_user_settings(self):
"""
:return: The additional fields dictionary for a course.
:return: The course user settings dictionary for a course.
"""
return self._additional_fields
return self._course_user_setting

def get_task_dispenser(self):
"""
Expand Down
2 changes: 2 additions & 0 deletions inginious/frontend/flask/mapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from inginious.frontend.pages.admin.admin import AdministrationUsersPage, \
AdministrationUserActionPage
from inginious.frontend.pages.course_user_settings import CourseUserSettingPage
from inginious.frontend.pages.maintenance import MaintenancePage
from inginious.frontend.pages.utils import INGIniousStaticPage
from inginious.frontend.pages.index import IndexPage
Expand Down Expand Up @@ -84,6 +85,7 @@ def init_flask_mapping(flask_app):
flask_app.add_url_rule('/<cookieless:sessionid>course/<courseid>/<taskid>/<path:path>',
view_func=TaskPageStaticDownload.as_view('taskpagestaticdownload'))
flask_app.add_url_rule('/<cookieless:sessionid>group/<courseid>', view_func=GroupPage.as_view('grouppage'))
flask_app.add_url_rule('/<cookieless:sessionid>user_settings/<courseid>', view_func=CourseUserSettingPage.as_view('courseusersettingpage'))
flask_app.add_url_rule('/<cookieless:sessionid>auth/signin/<auth_id>',
view_func=AuthenticationPage.as_view('authenticationpage'))
flask_app.add_url_rule('/<cookieless:sessionid>auth/callback/<auth_id>',
Expand Down
16 changes: 8 additions & 8 deletions inginious/frontend/pages/course_admin/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import flask

from inginious.common.base import dict_from_prefix, id_checker
from inginious.common.field_types import FieldTypes
from inginious.frontend.user_settings.field_types import FieldTypes
from inginious.frontend.accessible_time import AccessibleTime
from inginious.frontend.pages.course_admin.utils import INGIniousAdminPage

Expand Down Expand Up @@ -97,10 +97,10 @@ def POST_AUTH(self, courseid): # pylint: disable=arguments-differ
if tag_error is not None:
errors.append(tag_error)

additional_fields = self.define_additional_fields(data)
if additional_fields is not None and not isinstance(additional_fields, dict):
errors.append(additional_fields)
course_content["fields"] = additional_fields
course_user_settings = self.define_course_user_settings(data)
if course_user_settings is not None and not isinstance(course_user_settings, dict):
errors.append(course_user_settings)
course_content["fields"] = course_user_settings
if len(errors) == 0:
self.course_factory.update_course_descriptor_content(courseid, course_content)
errors = None
Expand Down Expand Up @@ -136,8 +136,8 @@ def define_tags(self, course, data, course_content):
course_content["tags"] = tags
self.course_factory.update_course_descriptor_content(course.get_id(), course_content)

def define_additional_fields(self, data):
"""Additional field definition method"""
def define_course_user_settings(self, data):
"""Course user settings definition method"""
fields = self.prepare_datas(data, "field")
if not isinstance(fields, dict):
# prepare_datas returned an error
Expand All @@ -146,7 +146,7 @@ def define_additional_fields(self, data):
# Repair fields
for field in fields.values():
try:
field["type"] = FieldTypes(int(field["type"])).value
field["type"] = int(field["type"])
except:
return _("Invalid type value: {}").format(field["type"])
if not id_checker(field["id"]):
Expand Down
105 changes: 105 additions & 0 deletions inginious/frontend/pages/course_user_settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# -*- coding: utf-8 -*-
"""
Module definition for CourseUserSettingPage class
This file is part of INGInious. See the LICENSE and the COPYRIGHTS files for
more information about the licensing of this file.
"""

import flask
from inginious.frontend.user_settings.field_types import FieldTypes
from inginious.frontend.pages.utils import INGIniousAuthPage


class CourseUserSettingPage(INGIniousAuthPage):
"""
Class definition for CourseUserSettingPage
"""

def GET_AUTH(self, courseid):
""" GET request """
username = self.user_manager.session_username()
if not self._is_accessible(courseid, username):
return self.template_helper.render("course_unavailable.html")
current_user = self.database.users.find_one(
{"username": username})
course_user_settings = current_user.get("course_settings", {})
return self.show_page(courseid, course_user_settings.get(courseid, {}), None)

def POST_AUTH(self, courseid):
""" POST request """
username = self.user_manager.session_username()
if not self._is_accessible(courseid, username):
return self.template_helper.render("course_unavailable.html")
try:
course_user_settings = self._sanitize_content(flask.request.form, courseid)
except Exception as e:
feedback = ("danger", e)
current_user = self.database.users.find_one(
{"username": username})
course_user_settings = current_user.get("course_settings", {})
return self.show_page(courseid, course_user_settings.get(courseid, {}), feedback)

self.database.users.update_one({"username": username},
{"$set": {"course_settings." + courseid: course_user_settings}})

return self.show_page(courseid, course_user_settings, ("success", "Course settings successfully updated."))

def show_page(self, courseid, course_user_settings, feedback):
"""
Definition of the show page method.
:param: courseid: the id of the course.
:param: course_user_settings: The dict with the settings values.
:param: feedback: a tuple with the type of feedback and the feedback. None if there is no feedback.
:return:
"""
try:
course = self.course_factory.get_course(courseid)
course_user_setting_fields = course.get_course_user_settings()
return self.template_helper.render("course_user_settings.html", course=course,
course_user_setting_fields=course_user_setting_fields,
course_user_settings=course_user_settings, fieldtypes=FieldTypes,
feedback=feedback)
except Exception:
return self.template_helper.render("course_unavailable.html")

def _is_accessible(self, courseid, username):
"""
Verify if course is accessible and that courseid match an existing course.
:param: courseid - The id of the course.
:param: username - The username of the logged user.
:return: A boolean if the course is accessible for the given user.
"""
try:
course = self.course_factory.get_course(courseid)
return self.user_manager.course_is_user_registered(course, username) \
and self.user_manager.course_is_open_to_user(course, lti=False)
except Exception:
return False

def _sanitize_content(self, data, courseid):
"""
Sanitize received data
:param: data - A dictionary that normally contains user settings for a course.
:param: courseid - Id of the course.
:return: A sanitized dict.
"""
if not isinstance(data, dict):
raise TypeError("Incorrect type of data.")
copied_data = {}
course = self.course_factory.get_course(courseid)
add_fields = course.get_course_user_settings()
for field in add_fields:
if field not in data:
# setup default value.
copied_data[field] = add_fields[field].get_cast_type()()
else:
try:
value = data[field]
if value is None or value == "":
value = add_fields[field].get_default_value()
# try to cast given value to be sure that we match expected type.
copied_data[field] = add_fields[field].get_cast_type()(value)
except ValueError:
raise ValueError("Wrong value for field: " + str(field))
return copied_data
7 changes: 7 additions & 0 deletions inginious/frontend/templates/course.html
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ <h3> {{_("Last tried exercises") }}</h3>
<a class="list-group-item list-group-item-action disabled submission-empty">{{ _("No submissions") }}</a>
{% endif %}
</div>

{% endif %}
{%if staff %}
{% if not course.is_open_to_non_staff() %}
Expand All @@ -48,6 +49,9 @@ <h3> {{_("Last tried exercises") }}</h3>
</div>
{% endif %}
<div class="list-group">
<a class="list-group-item list-group-item-action list-group-item-info" href="{{ get_homepath() }}/user_settings/{{ course.get_id() }}">
<i class="fa fa-cogs fa-fw"></i>&nbsp; {{ _("Course Settings") }}
</a>
<a class="list-group-item list-group-item-action list-group-item-info" href="{{ get_homepath() }}/admin/{{ course.get_id() }}">
<i class="fa fa-user-secret fa-fw"></i>&nbsp; {{ _("Course administration") }}
</a>
Expand All @@ -63,6 +67,9 @@ <h3> {{_("Last tried exercises") }}</h3>
{{ _("Group management") }}
{% endif %}
</a>
<a class="list-group-item list-group-item-action list-group-item-info" href="{{ get_homepath() }}/user_settings/{{ course.get_id() }}">
<i class="fa fa-cogs fa-fw"></i>&nbsp; {{ _("Course Settings") }}
</a>
{% if course.allow_unregister() %}
<a href="#" data-toggle="modal" data-target="#unregister_modal" class="list-group-item list-group-item-action list-group-item-info">
<i class="fa fa-user-times fa-fw"></i>&nbsp; {{ _("Unregister from this course") }}
Expand Down
8 changes: 4 additions & 4 deletions inginious/frontend/templates/course_admin/settings.html
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ <h2>{{_("Course settings")}}</h2>
<a class="nav-link" data-toggle="tab" href="#tags_tab">{{ _("Tags") }}</a>
</li>
<li class="nav-item">
<a class="nav-link" data-toggle="tab" href="#additional_fields">{{ _("Additional fields") }}</a>
<a class="nav-link" data-toggle="tab" href="#course_user_settings">{{ _("User Settings") }}</a>
</li>
</ul>
</div>
Expand Down Expand Up @@ -429,7 +429,7 @@ <h2>{{_("Course settings")}}</h2>
</tbody>
</table>
</div>
<div class="tab-pane fade" id="additional_fields">
<div class="tab-pane fade" id="course_user_settings">
{% if error %}
<div class="alert alert-danger" role="alert">
{{error}}
Expand Down Expand Up @@ -458,7 +458,7 @@ <h2>{{_("Course settings")}}</h2>
<td>
<select disabled class="form-control" name="field[NEW][type]">
{% for elem in field_types %}
<option value={{ elem.value}} TYPE_REPLACE_{{ elem.value }}>{{_(elem.name)}}</option>
<option value={{ elem.value.id}} TYPE_REPLACE_{{ elem.value.id }}>{{_(elem.name)}}</option>
{% endfor %}
</select>
</td>
Expand All @@ -472,7 +472,7 @@ <h2>{{_("Course settings")}}</h2>
<td>
<select class="form-control" name="field[{{loop.index}}][type]">
{% for elem in field_types %}
<option value={{ elem.value}} {% if elem.value == field["type"] %}selected="selected"{% endif %}>{{_(elem.name)}}</option>
<option value={{ elem.value.id}} {% if elem.value.id == field["type"] %}selected="selected"{% endif %}>{{_(elem.name)}}</option>
{% endfor %}
</select>
</td>
Expand Down
73 changes: 73 additions & 0 deletions inginious/frontend/templates/course_user_settings.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
{# This file is part of INGInious. See the LICENSE and the COPYRIGHTS files for #}
{# more information about the licensing of this file. #}

{% extends "layout.html" %}
{% block title %}{{ course.get_name(user_manager.session_language()) }}{% endblock %}
{% set registered = user_manager.course_is_user_registered(course) %}
{% set staff = user_manager.has_staff_rights_on_course(course) %}
{% block column %}
{{ template_helper.call('course_menu', course=course, template_helper=template_helper) | safe }}
{%if staff %}
{% if not course.is_open_to_non_staff() %}
<div class="alert alert-warning" role="alert">
{{ _("This course is currently invisible for students. You can change this by modifying the \"accessibility\" option in the configuration of the course.") }}
</div>
{% endif %}
<div class="list-group">
<a class="list-group-item list-group-item-action list-group-item-info" href="{{ get_homepath() }}/admin/{{ course.get_id() }}">
<i class="fa fa-user-secret fa-fw"></i>&nbsp; {{ _("Course administration") }}
</a>
</div>
{% elif registered %}
<div class="list-group">
<a class="list-group-item list-group-item-action list-group-item-info" href="{{ get_homepath() }}/group/{{ course.get_id() }}">
<i class="fa fa-group fa-fw"></i>&nbsp;
{% set mygroup = user_manager.get_course_user_group(course) %}
{% if mygroup and user_manager.session_username() in mygroup['students'] %}
{{ _("Group : {}").format(mygroup['description']) }}
{% else %}
{{ _("Group management") }}
{% endif %}
</a>
<a class="list-group-item list-group-item-action list-group-item-info" href="{{ get_homepath() }}/user_settings/{{ course.get_id() }}">
<i class="fa fa-cogs fa-fw"></i>&nbsp; {{ _("Course Settings") }}
</a>
</div>
{%endif %}
{% endblock %}
{% block navbar %}
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
{% if registered %}
<li class="breadcrumb-item"><a href="{{ get_homepath() }}/mycourses" title="{{ _('My courses') }}" data-toggle="tooltip" data-placement="bottom"><i class="fa fa-th-list"></i></a></li>
{% else %}
<li class="breadcrumb-item"><a href="{{get_homepath() }}/courselist" title="{{ _('Course list') }}" data-toggle="tooltip" data-placement="bottom"><i class="fa fa-th-list"></i></a></li>
{% endif %}
<li class="breadcrumb-item"><a href="{{get_homepath() }}/course/{{ course.get_id() }}">{{ course.get_name(user_manager.session_language()) }} <span class="sr-only">{{ _("(current)") }}</span></a></li>
<li class="breadcrumb-item active"><a href="#"><i class="fa fa-cogs"></i> {{ _("Course settings") }}</a></li>
</ol>
</nav>
{% endblock %}
{% block content %}
{% if feedback is not none %}
<div class="alert alert-{{ feedback[0] }}" role="alert">
{{ _(feedback[1] ) }}
</div>
{% endif %}
<h2>{{_("Course settings")}}</h2>
{% if course_user_setting_fields | length ==0 %}
<div class="alert alert-info" role="alert">{{ _("There is no customizable option for this course") }}</div>
{% else %}
<form method="post" action="">
{% for key in course_user_setting_fields %}
<div class="form-group row">
<label class="col-sm-2 " for="{{ key }}" class="control-label">{{ course_user_setting_fields[key].get_description() }}</label>
<div class="col-sm-10">
{{ course_user_setting_fields[key].render(template_helper, course_user_settings[key]) | safe }}
</div>
</div>
{% endfor %}
<button type="submit" class="btn btn-block btn-primary">{{ _("Save data") }}</button>
</form>
{% endif %}
{% endblock %}

0 comments on commit cebfaad

Please sign in to comment.