Skip to content

Commit

Permalink
[FEATURE] Add opening dates for class levels (hedyorg#2014)
Browse files Browse the repository at this point in the history
  • Loading branch information
TiBiBa authored Feb 18, 2022
1 parent d6c2b97 commit 2735274
Showing 14 changed files with 155 additions and 59 deletions.
14 changes: 13 additions & 1 deletion app.py
Original file line number Diff line number Diff line change
@@ -876,7 +876,14 @@ def index(level, step):
customizations = DATABASE.get_student_class_customizations(current_user()['username'])
level_defaults_for_lang = LEVEL_DEFAULTS[g.lang]

if level not in level_defaults_for_lang.levels or ('levels' in customizations and level not in customizations['levels']):
if 'levels' in customizations:
available_levels = customizations['levels']
now = timems()
for current_level, timestamp in customizations.get('opening_dates', {}).items():
if utils.datetotimeordate(timestamp) > utils.datetotimeordate(utils.mstoisostring(now)):
available_levels.remove(int(current_level))

if level not in level_defaults_for_lang.levels or ('levels' in customizations and level not in available_levels):
return utils.error_page(error=404, ui_message='no_such_level')
defaults = level_defaults_for_lang.get_defaults_for_level(level)
max_level = level_defaults_for_lang.max_level()
@@ -885,13 +892,18 @@ def index(level, step):
for adventure in customizations.get('teacher_adventures', []):
teacher_adventures.append(DATABASE.get_adventure(adventure))

enforce_developers_mode = False
if 'other_settings' in customizations and 'developers_mode' in customizations['other_settings']:
enforce_developers_mode = True

return hedyweb.render_code_editor_with_tabs(
level_defaults=defaults,
max_level=max_level,
level_number=level,
version=version(),
adventures=adventures,
customizations=customizations,
enforce_developers_mode=enforce_developers_mode,
teacher_adventures=teacher_adventures,
loaded_program=loaded_program,
adventure_name=adventure_name)
3 changes: 2 additions & 1 deletion content/pages/customize-class/en.yaml
Original file line number Diff line number Diff line change
@@ -5,7 +5,8 @@ explanation_1: |
steps:
["Select levels for your class by pressing the \"level buttons\"", "\"Checkboxes\" will appear for the adventures available for the chosen levels",
"Select the adventures you want to make available", "Click the name of an adventure to (de)select for all levels",
"Add personal adventures", "Choose \"Save\" -> You're done!"]
"Add personal adventures", "Selecting an opening date for each level (you can also leave it empty)",
"Selection other settings", "Choose \"Save\" -> You're done!"]
explanation_2: |
You can always change these settings later on. For example, you can make specific adventures or levels available while teaching a class.
This way it's easy for you to determine which level and adventures your students will be working on.
3 changes: 2 additions & 1 deletion content/pages/customize-class/nl.yaml
Original file line number Diff line number Diff line change
@@ -4,7 +4,8 @@ explanation_1: |
steps:
["Selecteer de levels voor jouw klas door op een \"level knop\" te drukken", "Er verschijnen \"selectievinkjes\" voor de beschikbare avonturen voor deze levels",
"Selecteer de avonturen die je beschikbaar wilt maken", "Klik op de naam van een avontuur om deze voor alle levels te (de)selecteren",
"Voeg eventueel persoonlijke avonturen toe", "Kies \"Opslaan\" -> je bent helemaal klaar!"]
"Voeg eventueel persoonlijke avonturen toe", "Selecteer een openingsdatum per level (leeg laten mag ook)",
"Kies andere instellingen", "Kies \"Opslaan\" -> je bent helemaal klaar!"]
explanation_2: |
Je kunt deze instellingen altijd op een later moment wijzigen. Zo kun je bijvoorbeeld tijdens het les geven nieuwe avonturen of levels beschikbaar maken.
Hierdoor is het voor leerlingen (en voor de leraar!) makkelijker om te kiezen waar aan gewerkt kan worden en wat er geleerd wordt.
10 changes: 8 additions & 2 deletions content/texts/en.yaml
Original file line number Diff line number Diff line change
@@ -330,7 +330,13 @@ Auth:
reset_adventure_prompt: "Are you sure you want to reset all selected adventures?"
reset_adventures: "Reset selected adventures"
remove_customization: "Remove customization"

opening_date: "Opening date"
opening_dates: "Opening dates"
directly_available: "Directly open"
other_settings: "Other settings"
option: "Option"
mandatory_mode: "Mandatory developer's mode"

#Multi-accounts adding
create_student_accounts: "Create student accounts"
create_accounts: "Create multiple accounts"
@@ -352,7 +358,7 @@ Auth:
On this page you can create accounts for multiple students at the same time. It is also possible to directly add them to one of your classes.
By pressing the green + on the bottom right of the page you can add extra rows. You can delete a row by pressing the corresponding red cross.
Make sure no rows are empty when you press "Create accounts". Please keep in mind that every username and mail address needs to be unique and the password needs to be <b>at least</b> 6 characters.
#Adventure customizations
customize_adventure: "Customize adventure"
general_settings: "General settings"
6 changes: 6 additions & 0 deletions content/texts/nl.yaml
Original file line number Diff line number Diff line change
@@ -324,6 +324,12 @@ Auth:
reset_adventure_prompt: "Weet je zeker dat je alle geselecteerde avonturen wilt wissen?"
reset_adventures: "Wis gekozen avonturen"
remove_customization: "Verwijder Personalisatie"
opening_date: "Openingsdatum"
opening_dates: "Openingsdata"
directly_available: "Gelijk open"
other_settings: "Andere instellingen"
option: "Optie"
mandatory_mode: "Verplichte programmeursmodus"

#Multi-accounts adding
create_student_accounts: "Maak leerling accounts"
3 changes: 2 additions & 1 deletion hedyweb.py
Original file line number Diff line number Diff line change
@@ -76,7 +76,7 @@ def get_page_title(current_page):
return page_titles_json['start'].get("en")


def render_code_editor_with_tabs(level_defaults, max_level, level_number, version, loaded_program, adventures, customizations, teacher_adventures, adventure_name):
def render_code_editor_with_tabs(level_defaults, max_level, level_number, version, loaded_program, adventures, customizations, enforce_developers_mode, teacher_adventures, adventure_name):
user = current_user()

if not level_defaults:
@@ -91,6 +91,7 @@ def render_code_editor_with_tabs(level_defaults, max_level, level_number, versio
arguments_dict['prev_level'] = int(level_number) - 1 if int(level_number) > 1 else None
arguments_dict['next_level'] = int(level_number) + 1 if int(level_number) < max_level else None
arguments_dict['customizations'] = customizations
arguments_dict['enforce_developers_mode'] = enforce_developers_mode
arguments_dict['teacher_adventures'] = teacher_adventures
arguments_dict['menu'] = True
arguments_dict['latest'] = version
2 changes: 1 addition & 1 deletion static/css/generated.css

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions static/js/app.ts
Original file line number Diff line number Diff line change
@@ -1285,8 +1285,8 @@ export function turnIntoAceEditor(element: HTMLElement, isReadOnly: boolean): Ac
});
return editor;
}
export function toggle_developers_mode() {
if ($('#developers_toggle').is(":checked")) {
export function toggle_developers_mode(enforced: boolean) {
if ($('#developers_toggle').is(":checked") || enforced) {
$('#adventures').hide();
pushAchievement("lets_focus");
} else {
23 changes: 0 additions & 23 deletions static/js/appbundle.js

Large diffs are not rendered by default.

7 changes: 0 additions & 7 deletions static/js/appbundle.js.map

Large diffs are not rendered by default.

30 changes: 28 additions & 2 deletions static/js/teachers.ts
Original file line number Diff line number Diff line change
@@ -351,14 +351,30 @@ export function save_customizations(class_id: string) {
teacher_adventures.push(<string>$(this).attr('id'));
}
});

let opening_dates = {};
$('.opening_date_container').each(function() {
if ($(this).is(":visible")) {
$(this).find(':input').each(function () {
// @ts-ignore
opening_dates[<string>$(this).attr('level')] = $(this).val();
});
}
});
let other_settings: string[] = [];
$('.other_settings_checkbox').each(function() {
if ($(this).prop("checked")) {
other_settings.push(<string>$(this).attr('id'));
}
});
$.ajax({
type: 'POST',
url: '/for-teachers/customize-class/' + class_id,
data: JSON.stringify({
levels: levels,
opening_dates: opening_dates,
adventures: adventures,
teacher_adventures: teacher_adventures
teacher_adventures: teacher_adventures,
other_settings: other_settings
}),
contentType: 'application/json',
dataType: 'json'
@@ -384,8 +400,10 @@ export function remove_customizations(class_id: string) {
$('#customizations_alert').removeClass('hidden');
$('.adventure_level_input').prop('checked', false);
$('.teacher_adventures_checkbox').prop('checked', false);
$('.other_settings_checkbox').prop('checked', false);
$('.level-select-button').removeClass('green-btn');
$('.level-select-button').addClass('blue-btn');
$('.opening_date_container').addClass('hidden');
}).fail(function (err) {
modal.alert(err.responseText, 3000, true);
});
@@ -418,13 +436,21 @@ export function select_all_level_adventures(level: string) {
});
$('#level_button_' + level).removeClass('blue-btn');
$('#level_button_' + level).addClass('green-btn');

// We also have to add this level to the "Opening dates" section
$('#opening_date_level_' + level).removeClass('hidden');
$('#opening_date_level_' + level).find('input').val('');
$('#opening_date_level_' + level).find('input').prop({type:"text"});
} else {
$('.adventure_level_' + level).each(function () {
$(this).prop("checked", false);
$(this).addClass('hidden');
});
$('#level_button_' + level).removeClass('green-btn');
$('#level_button_' + level).addClass('blue-btn');

// We also have to remove this level from the "Opening dates" section
$('#opening_date_level_' + level).addClass('hidden');
}
}

76 changes: 63 additions & 13 deletions templates/customize-class.html
Original file line number Diff line number Diff line change
@@ -53,16 +53,44 @@ <h3 class="px-4">{{ auth.select_adventures }}</h3>
</div>
</div>
<div class="flex flex-row">
{% if teacher_adventures %}
<div class="flex flex-col">
<div class="flex flex-col ltr:mr-4 rtl:ml-4">
<h3 class="px-4">{{ auth.opening_dates }}</h3>
<div class="border border-gray-400 w-3/4 py-2 px-4 rounded-lg">
<table class="table-auto text-center opening_dates_overview">
<thead>
<tr>
<th class="w-40 border-r border-gray-400 text-left px-4">{{ ui.level_title }}</th>
<th class="w-64">{{ auth.opening_date }}</th>
</tr>
</thead>
<tbody>
{% for i in range(1, max_level + 1) %}
<tr class="{% if i not in customizations['levels'] %}hidden{% endif %} opening_date_container" id="opening_date_level_{{ i }}">
<td class="border-r border-t border-gray-400">{{ ui.level_title }} {{ i }}</td>
<td class="border-t border-gray-400 ltr:pl-2 rtl:pr-2">
<input level="{{ i }}"
{% if customizations and 'opening_dates' in customizations and i|string in customizations['opening_dates'] %}
type="date" value="{{ customizations['opening_dates'][i|string] }}"
{% else %}
type="text" onfocus="(this.type='date')" placeholder="{{ auth.directly_available }}"
{% endif %}
class="inline-block text-center appearance-none bg-gray-200 border border-gray-200 w-full h-10 my-1 text-gray-700 py-2 px-4 rounded">
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="flex flex-col ltr:ml-4 rtl:mr-4">
<h3 class="px-4">{{ auth.select_own_adventures }}</h3>
<div class="border border-gray-400 py-2 px-4 rounded-lg">
<table class="table-auto text-center w-full customize_own_adventure_overview">
<thead>
<tr>
<th class="w-64 text-left border-r border-gray-400">Avontuur</th>
<th class="w-64 border-r border-gray-400">Level</th>
<th class="w-64">Selecteren?</th>
<th class="w-64 text-left border-r border-gray-400">{{ ui.adventure }}</th>
<th class="w-64 border-r border-gray-400">{{ ui.level_title }}</th>
<th class="w-64">{{ auth.select }}?</th>
</tr>
</thead>
<tbody>
@@ -79,16 +107,38 @@ <h3 class="px-4">{{ auth.select_own_adventures }}</h3>
</table>
</div>
</div>
{% endif %}
<div class="flex flex-col mt-8 ltr:ml-6 rtl:mr-6 items-start">
</div>
<div class="flex flex-row mt-4">
<div class="flex flex-col ltr:mr-4 rtl:ml-4">
<h3 class="px-4">{{ auth.other_settings}}</h3>
<div class="border border-gray-400 py-2 px-4 rounded-lg">
<table class="table-auto text-center w-full customize_other_settings">
<thead>
<tr>
<th class="w-64 text-left border-r border-gray-400">{{ auth.option }}</th>
<th class="w-40">{{ auth.select }}?</th>
</tr>
</thead>
<tbody>
<tr>
<td class="text-left border-t border-r border-gray-400">{{ auth.mandatory_mode }}</td>
<td class="border-t border-gray-400">
<input class="other_settings_checkbox" id="developers_mode" type="checkbox" {% if "developers_mode" in customizations['other_settings'] %}checked{% endif %}>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="flex flex-col">
<div class="flex flex-row ml-auto">
<button class="yellow-btn ltr:mr-4 rtl:ml-4 text-white" onclick="hedyApp.modal.confirm('{{ auth.reset_adventure_prompt }}', function(){$('.adventure_level_input').prop('checked', false);$('.teacher_adventures_checkbox').prop('checked', false);});">{{ auth.reset_adventures }}</button>
<button class="blue-btn" onclick="hedyApp.save_customizations('{{class_info.id}}')">{{ auth.save }}</button>
<button class="yellow-btn text-white" onclick="hedyApp.modal.confirm('{{ auth.reset_adventure_prompt }}', function(){$('.adventure_level_input').prop('checked', false);$('.teacher_adventures_checkbox').prop('checked', false);});">{{ auth.reset_adventures }}</button>
<button class="blue-btn ltr:ml-2 rtl:mr-2" onclick="hedyApp.save_customizations('{{class_info.id}}')">{{ auth.save }}</button>
</div>
<div class="flex flex-row ml-auto mt-4">
<button class="red-btn ltr:mr-4 rtl:ml-4 {% if not customizations %}hidden{% endif %}" id="remove_customizations_button" onclick="hedyApp.remove_customizations('{{class_info.id}}')">
{{ auth.remove_customization }}</button>
<button class="green-btn" onclick="window.location.href = '/for-teachers/class/{{class_info.id}}'">{{auth.back_to_class}}</button>
<div class="flex flex-row mt-2 ml-auto">
<button class="red-btn {% if not customizations %}hidden{% endif %}" id="remove_customizations_button" onclick="hedyApp.remove_customizations('{{class_info.id}}')">
{{ auth.remove_customization }}</button>
<button class="green-btn ltr:ml-2 rtl:mr-2" onclick="window.location.href = '/for-teachers/class/{{class_info.id}}'">{{auth.back_to_class}}</button>
</div>
</div>
</div>
14 changes: 11 additions & 3 deletions templates/incl-editor-and-output.html
Original file line number Diff line number Diff line change
@@ -131,13 +131,21 @@
</div>
</div>
<!-- developer mode toggle -->
<div class="mx-auto mt-4 w-64 border-gray-400 border-2 rounded-full py-2 px-4{% if editor_readonly %} hidden{% endif %}" id="developers_toggle_container">
<div class="mx-auto mt-4 w-64 border-gray-400 border-2 rounded-full py-2 px-4{% if editor_readonly or enforce_developers_mode %} hidden{% endif %}" id="developers_toggle_container">
<label for="developers_toggle" class="flex flex-row items-center justify-center cursor-pointer">
<div class="relative">
<input id="developers_toggle" type="checkbox" onclick="hedyApp.toggle_developers_mode()" class="sr-only" />
<input id="developers_toggle" type="checkbox" onclick="hedyApp.toggle_developers_mode(false)" class="sr-only" />
<div class="w-10 h-4 bg-gray-400 rounded-full shadow-inner"></div>
<div class="toggle-circle absolute w-6 h-6 bg-white rounded-full shadow ltr:-left-1 rtl:-right-1 -top-1 transition" style="top: -5px;"></div>
</div>
<div class="ltr:ml-3 rtl:mr-3">{{ ui.developers_mode }}</div>
</label>
</div>
</div>
<!-- If we enforce the developer's mode in customizations -> hide adventures, increase editor and output height -->
{% if enforce_developers_mode %}
<script>
$(document).ready(function () {
hedyApp.toggle_developers_mode(true);
});
</script>
{% endif %}
19 changes: 17 additions & 2 deletions website/teacher.py
Original file line number Diff line number Diff line change
@@ -252,19 +252,34 @@ def update_customizations(user, class_id):
if not isinstance(body.get('levels'), list):
return "Levels must be a list", 400
if not isinstance(body.get('adventures'), dict):
return 'adventures must be a dict', 400
return 'Adventures must be a dict', 400
if not isinstance(body.get('opening_dates'), dict):
return 'Opening dates must be a dict', 400

#Values are always strings from the front-end -> convert to numbers
levels = [int(i) for i in body['levels']]

opening_dates = body['opening_dates'].copy()
for level, timestamp in body.get('opening_dates').items():
if len(timestamp) < 1:
opening_dates.pop(level)
else:
try:
opening_dates[level] = utils.datetotimeordate(timestamp)
except:
return 'One or more of your opening dates is invalid', 400

adventures = {}
for name, adventure_levels in body['adventures'].items():
adventures[name] = [int(i) for i in adventure_levels]

customizations = {
'id': class_id,
'levels': levels,
'opening_dates': opening_dates,
'adventures': adventures,
'teacher_adventures': body['teacher_adventures']
'teacher_adventures': body['teacher_adventures'],
'other_settings': body['other_settings']
}

DATABASE.update_class_customizations(customizations)

0 comments on commit 2735274

Please sign in to comment.