Skip to content

Commit

Permalink
[frontend/course_admin] add additional course fields (#825)
Browse files Browse the repository at this point in the history
  • Loading branch information
Drumor committed Oct 17, 2022
1 parent 0a882b1 commit b8523f7
Show file tree
Hide file tree
Showing 6 changed files with 165 additions and 5 deletions.
4 changes: 2 additions & 2 deletions doc/admin_doc/install_doc/installation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ INGInious needs:
.. _Python: https://www.python.org/
.. _MongoDB: http://www.mongodb.org/

RHEL/Cent OS 7+, Fedora 32+
RHEL/Rocky 8, Fedora 32+
`````````````````````````````

.. DANGER::
Expand All @@ -37,7 +37,7 @@ The previously mentioned dependencies can be installed, for Cent OS 7+ :
.. code-block:: bash
# yum install -y epel-release
# yum install -y git gcc libtidy python3 python3-devel python3-pip zeromq-devel yum-utils
# yum install -y git gcc libtidy python38 python38-devel python38-pip zeromq-devel yum-utils
# yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
# yum install -y docker-ce docker-ce-cli
# cat <<EOF > /etc/yum.repos.d/mongodb-org-4.4.repo
Expand Down
53 changes: 53 additions & 0 deletions inginious/common/additional_field.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
"""
Module definition for AdditionalField class
-*- coding: utf-8 -*-
This file is part of INGInious. See the LICENSE and the COPYRIGHTS files for
more information about the licensing of this file.
"""
from inginious.common.field_types import FieldTypes


class AdditionalField:
"""
This class represent extra field that can be added to a course.
"""

def __init__(self, field_id, description, field_type):
self._id = field_id
self._description = description
if field_type in [field.value for field in FieldTypes]:
self._type = field_type
else:
raise Exception("Field type not correct")

def __eq__(self, other):
return self._id == other._id

def __hash__(self):
return hash(self._id)

def get_id(self):
"""
:return: id of the additional field
"""
return self._id

def get_description(self):
"""
:return: description of the additional field
"""
return self._description

def get_type_name(self):
""""
:return: type name of the additional field
"""
return FieldTypes(self._type).name

def get_type(self):
"""
:return: type of the additional field
"""
return self._type
18 changes: 18 additions & 0 deletions inginious/common/field_types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"""
Module definition for FieldTypes class
-*- coding: utf-8 -*-
This file is part of INGInious. See the LICENSE and the COPYRIGHTS files for
more information about the licensing of this file.
"""
from enum import Enum


class FieldTypes(Enum):
"""
A class used to represent a field type. Based on Enums.
"""
INTEGER = 1
STRING = 2
BOOLEAN = 3
11 changes: 11 additions & 0 deletions inginious/frontend/courses.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from typing import List
from collections import OrderedDict

from inginious.common.additional_field import AdditionalField
from inginious.common.tags import Tag
from inginious.frontend.accessible_time import AccessibleTime
from inginious.frontend.parsable_text import ParsableText
Expand Down Expand Up @@ -80,6 +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()}
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 @@ -238,6 +243,12 @@ def get_description(self, language):
def get_tags(self):
return self._tags

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

def get_task_dispenser(self):
"""
:return: the structure of the course
Expand Down
28 changes: 26 additions & 2 deletions inginious/frontend/pages/course_admin/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import flask

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

Expand Down Expand Up @@ -96,7 +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
if len(errors) == 0:
self.course_factory.update_course_descriptor_content(courseid, course_content)
errors = None
Expand All @@ -106,7 +110,8 @@ def POST_AUTH(self, courseid): # pylint: disable=arguments-differ

def page(self, course, errors=None, saved=False):
""" Get all data and display the page """
return self.template_helper.render("course_admin/settings.html", course=course, errors=errors, saved=saved)
return self.template_helper.render("course_admin/settings.html", course=course, errors=errors, saved=saved,
field_types=FieldTypes)

def define_tags(self, course, data, course_content):
tags = self.prepare_datas(data, "tags")
Expand All @@ -131,6 +136,25 @@ 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"""
fields = self.prepare_datas(data, "field")
if not isinstance(fields, dict):
# prepare_datas returned an error
return fields

# Repair fields
for field in fields.values():
try:
field["type"] = FieldTypes(int(field["type"])).value
except:
return _("Invalid type value: {}").format(field["type"])
if not id_checker(field["id"]):
return _("Invalid id: {}").format(field["id"])

del field["id"]
return fields

def prepare_datas(self, data, prefix: str):
# prepare dict
datas = dict_from_prefix(prefix, data)
Expand Down
56 changes: 55 additions & 1 deletion inginious/frontend/templates/course_admin/settings.html
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ <h2>{{_("Course settings")}}</h2>
<li class="nav-item">
<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>
</li>
</ul>
</div>

Expand Down Expand Up @@ -425,12 +428,63 @@ <h2>{{_("Course settings")}}</h2>
{% endfor %}
</tbody>
</table>
</div>
<div class="tab-pane fade" id="additional_fields">
{% if error %}
<div class="alert alert-danger" role="alert">
{{error}}
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
{% endif %}
<div class="btn-group btn-group-sm pull-right">
<button type="button" class="mt-3 btn btn-info btn-block" onclick="studio_add_table_line(-1,'new_fields_table','NEW_FIELD')"><i class="fa fa-plus fa-lg"></i> {{ _("New field") }}</button>
</div>
<table id="new_fields_table" class="table">
<thead class="table-borderless">
<tr>
<th>{{_("id")}}</th>
<th>{{_("description")}}</th>
<th>{{_("type ")}}</th>
</tr>
</thead>
<tbody>

<!-- An empty row so that we can copy it to add new rows. All 'disabled' are important to not send this row to INGInious when saving the task-->
<tr id="NEW_FIELD" style="display:none;">
<td><input disabled type="text" ID_STOP id="id_field_NEW" class="form-control" name="field[NEW][id]" value="ID_REPLACE"></td>
<td><textarea disabled class="form-control" onfocus="studio_expand_description(this);" onblur="studio_expand_description_not(this);" name="field[NEW][description]" rows="1" style="resize:none">DESCRIPTION_REPLACE</textarea></td>
<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>
{% endfor %}
</select>
</td>
</tr>

{% for key,field in course.get_descriptor().get("fields", {}).items() %}
{% set description = field["description"] if "description" in field else "" %}
<tr id="{{loop.index}}">
<td><input type="text" id="id_field_{{loop.index}}" class="form-control" name="field[{{loop.index}}][id]" value="{{ key }}"/></td>
<td><textarea class="form-control" onfocus="studio_expand_description(this);" onblur="studio_expand_description_not(this);" name="field[{{loop.index}}][description]" rows="1" style="resize:none">{{description}}</textarea></td>
<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>
{% endfor %}
</select>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<button class="btn btn-primary btn-block mt-3" type="submit"><i class="fa fa-download"></i> {{ _("Save changes") }}</button>

</form>

<script type="text/javascript">
Expand Down

0 comments on commit b8523f7

Please sign in to comment.