Skip to content

Commit

Permalink
[UI] adds Hedy dashboard & common error detection (#4166)
Browse files Browse the repository at this point in the history
**Description**

* Adds a button to the `for-teachers/class/{{class_info.id}}` page to open the live statistics dashboard.
* Adds a live statistics dashboards page at `/live_stats/class/{{class_info.id}}`.
  • Loading branch information
karlijnbok authored Sep 13, 2023
1 parent 138d7ce commit 7e25197
Show file tree
Hide file tree
Showing 67 changed files with 4,870 additions and 100 deletions.
3 changes: 2 additions & 1 deletion app.py
Original file line number Diff line number Diff line change
Expand Up @@ -527,7 +527,7 @@ def parse():
response['Location'] = ex.error_location
transpile_result = ex.fixed_result
exception = ex
except hedy.exceptions.UnquotedEqualityCheck as ex:
except hedy.exceptions.UnquotedEqualityCheckException as ex:
response['Error'] = translate_error(ex.error_code, ex.arguments, keyword_lang)
response['Location'] = ex.error_location
exception = ex
Expand Down Expand Up @@ -2223,6 +2223,7 @@ def current_user_allowed_to_see_program(program):
app.register_blueprint(quiz.QuizModule(DATABASE, ACHIEVEMENTS, QUIZZES))
app.register_blueprint(parsons.ParsonsModule(PARSONS))
app.register_blueprint(statistics.StatisticsModule(DATABASE))
app.register_blueprint(statistics.LiveStatisticsModule(DATABASE))


# *** START SERVER ***
Expand Down
11 changes: 8 additions & 3 deletions build-tools/heroku/tailwind/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -203,14 +203,19 @@ a.green-btn:hover {
@apply bg-blue-500 text-white border-blue-700;
}

.blue-btn:hover {
@apply bg-blue-400 border-blue-500;
@apply active:bg-blue-700 active:border-blue-700;
}

.gray-btn {
@apply btn-shape;
@apply bg-gray-500 text-white border-gray-700;
}

.blue-btn:hover {
@apply bg-blue-400 border-blue-500;
@apply active:bg-blue-700 active:border-blue-700;
.gray-btn:hover {
@apply bg-gray-400 border-gray-500;
@apply active:bg-gray-700 active:border-gray-700;
}

/* Styles only for use in MarkDown blocks */
Expand Down
17 changes: 17 additions & 0 deletions content/error-messages.txt
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,20 @@ gettext('float')
gettext('list')
gettext('input')
gettext('Invalid At Command')
# Exceptions groups used in the Live Statistics Module. If you add a new Exception Group here,
# you should use the same name when you assign this exception group to a exception in that module
# in the exception_types dictionary
gettext('program_too_large_exception')
gettext('use_of_blanks_exception')
gettext('use_of_nested_functions_exception')
gettext('incorrect_use_of_types_exception')
gettext('invalid_command_exception')
gettext('incomplete_command_exception')
gettext('command_unavailable_exception')
gettext('command_not_available_yet_exception')
gettext('incorrect_use_of_variable_exception')
gettext('indentation_exception')
gettext('echo_and_ask_mismatch_exception')
gettext('incorrect_handling_of_quotes_exception')
gettext('cant_parse_exception')

15 changes: 13 additions & 2 deletions exceptions.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
import inspect
"""
Any exception added in this file must be also added to error-messages.txt
So we can translate the error message. The exception must also be assigned
an Exception Type in the exception_types dictionary in statistics.py
"""


class HedyException(Exception):
def __init__(self, error_code, **arguments):
"""Create a new HedyException.
Expand Down Expand Up @@ -72,14 +80,14 @@ def __init__(self, level, location, found, fixed_code=None):
self.fixed_code = fixed_code


class UnquotedEqualityCheck(HedyException):
class UnquotedEqualityCheckException(HedyException):
def __init__(self, line_number):
super().__init__('Unquoted Equality Check',
line_number=line_number)
self.location = [line_number]


class AccessBeforeAssign(HedyException):
class AccessBeforeAssignException(HedyException):
def __init__(self, name, access_line_number, definition_line_number):
super().__init__('Access Before Assign',
name=name,
Expand Down Expand Up @@ -284,3 +292,6 @@ def __init__(self, command, level, line_number):
class NestedFunctionException(HedyException):
def __init__(self):
super().__init__('Nested Function')


HEDY_EXCEPTIONS = {name: cls for name, cls in globals().items() if inspect.isclass(cls)}
6 changes: 3 additions & 3 deletions hedy.py
Original file line number Diff line number Diff line change
Expand Up @@ -1316,7 +1316,7 @@ def is_variable(self, variable_name, access_line_number=100):
if variable_name in all_names and variable_name not in all_names_before_access_line:
# referenced before assignment!
definition_line_number = [a.linenumber for a in self.lookup if a.name == variable_name][0]
raise hedy.exceptions.AccessBeforeAssign(
raise hedy.exceptions.AccessBeforeAssignException(
name=variable_name,
access_line_number=access_line_number,
definition_line_number=definition_line_number)
Expand Down Expand Up @@ -3084,7 +3084,7 @@ def parse_input(input_string, level, lang):
except lark.UnexpectedEOF:
lines = input_string.split('\n')
last_line = len(lines)
raise exceptions.UnquotedEqualityCheck(line_number=last_line)
raise exceptions.UnquotedEqualityCheckException(line_number=last_line)
except UnexpectedCharacters as e:
try:
location = e.line, e.column
Expand Down Expand Up @@ -3140,7 +3140,7 @@ def is_program_valid(program_root, input_string, level, lang):
raise exceptions.InvalidSpaceException(
level=level, line_number=line, fixed_code=fixed_code, fixed_result=result)
elif invalid_info.error_type == 'invalid condition':
raise exceptions.UnquotedEqualityCheck(line_number=line)
raise exceptions.UnquotedEqualityCheckException(line_number=line)
elif invalid_info.error_type == 'invalid repeat':
raise exceptions.MissingInnerCommandException(command='repeat', level=level, line_number=line)
elif invalid_info.error_type == 'repeat missing print':
Expand Down
2 changes: 1 addition & 1 deletion static/css/generated.css

Large diffs are not rendered by default.

145 changes: 145 additions & 0 deletions static/js/statistics.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { Chart, registerables } from 'chart.js';
import {modal} from "./modal";
import {showAchievements} from "./app";
Chart.register(...registerables);


Expand Down Expand Up @@ -491,3 +493,146 @@ function updateChart(elementId: string, datasets: any[] ) {
ch.data.datasets = datasets;
ch.update();
}

export function resolve_student(class_id: string, error_id: string, prompt: string) {
modal.confirm(prompt, function(){
$.ajax({
type: 'DELETE',
url: '/live_stats/class/' + class_id + '/error/' + error_id,
contentType: 'application/json',
dataType: 'json'
}).done(function(response) {
if (response.achievement) {
showAchievements(response.achievement, true, "");
} else {
location.reload();
}
}).fail(function(err) {
modal.notifyError(err.responseText);
});
});
}

export function InitLineChart(data: any[], labels: any[]){
const ctx = document.getElementById("runsOverTime") as HTMLCanvasElement;
new Chart(ctx, {
type: 'line',
data: {
labels: labels.map(String),
datasets: [{
data: data,
fill: false,
pointBackgroundColor: function(context) {
var index = context.dataIndex;
var value = context.dataset.data[index];
if (value === 0) {
return 'red'
}
else if (value === 1){
return 'green'
}
return 'blue'
},
// backgroundColor: 'rgba(0, 0, 255, 1)',
borderColor: 'rgba(0, 0, 255, 0.6)',
borderWidth: 1
}]
},
options: {
scales: {
y: {
ticks: {
callback: function(index) {
// Hide every 2nd tick label
if (index === 0) {
return 'Fail'
}
else if (index === 1){
return 'Success'
}
return ''
},
}
},
},
plugins: {
legend: {
display: false
}
}
}
});
}

export function enable_level_class_overview(level: string) {
if ($('#level_button_' + level).hasClass('gray-btn')) {
$('#level_button_' + level).removeClass('gray-btn');
$('#level_button_' + level).addClass('green-btn');
} else {
$('#level_button_' + level).removeClass('green-btn');
$('#level_button_' + level).addClass('gray-btn');
}
}

export function select_levels_class_overview(class_id: string) {
let levels: (string | undefined)[] = [];
$('.level-select-button').each(function() {
if ($(this).hasClass("green-btn")) {
levels.push(<string>$(this).val());
}
});

$.ajax({
type: 'POST',
url: '/live_stats/class/' + class_id,
data: JSON.stringify({
levels: levels
}),
contentType: 'application/json',
dataType: 'json'
}).done(function () {
location.reload();
}).fail(function (err) {
modal.notifyError(err.responseText);
});
}

export function toggle_show_students_class_overview(adventure: string) {
var adventure_panel = "div[id='adventure_panel_" + adventure + "']";
if ($(adventure_panel).hasClass('hidden')) {
$(adventure_panel).removeClass('hidden');
$(adventure_panel).addClass('block');
} else {
$(adventure_panel).removeClass('block');
$(adventure_panel).addClass('hidden');
}
}

export function getRunsOverTime(data: any[], labels: any[]) {
const chart = Chart.getChart("runsOverTime")!;
chart.data.labels = labels.map(String);

var datasets = [{
data: data,
fill: false,
pointBackgroundColor: function(context:any) {
var index = context.dataIndex;
var value = context.dataset.data[index];
if (value === 0) {
return 'red'
}
else if (value === 1){
return 'green'
}
return 'blue'
},
// backgroundColor: 'rgba(0, 0, 255, 1)',
borderColor: 'rgba(0, 0, 255, 0.6)',
borderWidth: 1
}]

chart.data.datasets = datasets;
chart.update();
}

export const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
8 changes: 4 additions & 4 deletions static_babel_content.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

COUNTRIES = {'AF': 'افغانستان', 'AX': 'Åland', 'AL': 'Shqipëri', 'DZ': 'الجزائر', 'AS': 'American Samoa', 'AD': 'Andorra', 'AO': 'Angola', 'AI': 'Anguilla', 'AQ': 'Antarctica', 'AG': 'Antigua & Barbuda', 'AR': 'Argentina', 'AM': 'Հայաստան', 'AW': 'Aruba', 'AU': 'Australia', 'AT': 'Österreich', 'AZ': 'Azərbaycan', 'BS': 'Bahamas', 'BH': 'البحرين', 'BD': 'বাংলাদেশ', 'BB': 'Barbados', 'BY': 'Беларусь', 'BE': 'Belgium', 'BZ': 'Belize', 'BJ': 'Bénin', 'BM': 'Bermuda', 'BT': 'འབྲུག', 'BO': 'Bolivia', 'BQ': 'Caribisch Nederland', 'BA': 'Bosna i Hercegovina', 'BW': 'Botswana', 'BV': 'Bouvet Island', 'BR': 'Brasil', 'IO': 'British Indian Ocean Territory', 'BN': 'Brunei', 'BG': 'България', 'BF': 'Burkina Faso', 'BI': 'Uburundi', 'KH': 'កម្ពុជា', 'CM': 'Cameroun', 'CA': 'Canada', 'CV': 'Kabu Verdi', 'KY': 'Cayman Islands', 'CF': 'République centrafricaine', 'TD': 'Tchad', 'CL': 'Chile', 'CN': '中国', 'CX': 'Christmas Island', 'CC': 'Cocos (Keeling) Islands', 'CO': 'Colombia', 'KM': 'جزر القمر', 'CG': 'Congo-Brazzaville', 'CD': 'Jamhuri ya Kidemokrasia ya Kongo', 'CK': 'Cook Islands', 'CR': 'Costa Rica', 'CI': 'Côte d’Ivoire', 'HR': 'Hrvatska', 'CU': 'Cuba', 'CW': 'Curaçao', 'CY': 'Κύπρος', 'CZ': 'Česko', 'DK': 'Danmark', 'DJ': 'Jabuuti', 'DM': 'Dominica', 'DO': 'República Dominicana', 'EC': 'Ecuador', 'EG': 'مصر', 'SV': 'El Salvador', 'GQ': 'Guinea Ecuatorial', 'ER': 'ኤርትራ', 'EE': 'Eesti', 'ET': 'ኢትዮጵያ', 'FK': 'Falkland Islands', 'FO': 'Føroyar', 'FJ': 'Fiji', 'FI': 'Suomi', 'FR': 'France', 'GF': 'Guyane française', 'PF': 'Polynésie française', 'TF': 'French Southern Territories', 'GA': 'Gabon', 'GM': 'Gambia', 'GE': 'საქართველო', 'DE': 'Deutschland', 'GH': 'Gaana', 'GI': 'Gibraltar', 'GR': 'Ελλάδα', 'GL': 'Kalaallit Nunaat', 'GD': 'Grenada', 'GP': 'Guadeloupe', 'GU': 'Guam', 'GT': 'Guatemala', 'GG': 'Guernsey', 'GN': 'Guinée', 'GW': 'Guiné-Bissau', 'GY': 'Guyana', 'HT': 'Haïti', 'HM': 'Heard Island and McDonald Islands', 'VA': 'Città del Vaticano', 'HN': 'Honduras', 'HK': '中國香港', 'HU': 'Magyarország', 'IS': 'Ísland', 'IN': 'भारत', 'ID': 'Indonesia', 'IR': 'ایران', 'IQ': 'العراق', 'IE': 'Ireland', 'IM': 'Isle of Man', 'IL': 'ישראל', 'IT': 'Italia', 'JM': 'Jamaica', 'JP': '日本', 'JE': 'Jersey', 'JO': 'الأردن', 'KZ': 'Казахстан', 'KE': 'Kenya', 'KI': 'Kiribati', 'KP': '조선민주주의인민공화국', 'KR': '대한민국', 'XK': 'Kosovë', 'KW': 'الكويت', 'KG': 'Кыргызстан', 'LA': 'ລາວ', 'LV': 'Latvija', 'LB': 'لبنان', 'LS': 'Lesotho', 'LR': 'Liberia', 'LY': 'ليبيا', 'LI': 'Liechtenstein',
'LT': 'Lietuva', 'LU': 'Luxembourg', 'MO': '中國澳門', 'MK': 'Северна Македонија', 'MG': 'Madagasikara', 'MW': 'Malawi', 'MY': 'Malaysia', 'MV': 'Maldives', 'ML': 'Mali', 'MT': 'Malta', 'MH': 'Marshall Islands', 'MQ': 'Martinique', 'MR': 'موريتانيا', 'MU': 'Moris', 'YT': 'Mayotte', 'MX': 'México', 'FM': 'Micronesia', 'MD': 'Republica Moldova', 'MC': 'Monaco', 'MN': 'Монгол', 'ME': 'Crna Gora', 'MS': 'Montserrat', 'MA': 'المغرب', 'MZ': 'Moçambique', 'MM': 'မြန်မာ', 'NA': 'Namibië', 'NR': 'Nauru', 'NP': 'नेपाल', 'NL': 'Nederland', 'NC': 'Nouvelle-Calédonie', 'NZ': 'New Zealand', 'NI': 'Nicaragua', 'NE': 'Nijar', 'NG': 'Nigeria', 'NU': 'Niue', 'NF': 'Norfolk Island', 'MP': 'Northern Mariana Islands', 'NO': 'Norge', 'OM': 'عُمان', 'PK': 'پاکستان', 'PW': 'Palau', 'PS': 'الأراضي الفلسطينية', 'PA': 'Panamá', 'PG': 'Papua New Guinea', 'PY': 'Paraguay', 'PE': 'Perú', 'PH': 'Philippines', 'PN': 'Pitcairn Islands', 'PL': 'Polska', 'PT': 'Portugal', 'PR': 'Puerto Rico', 'QA': 'قطر', 'RE': 'La Réunion', 'RO': 'România', 'RU': 'Россия', 'RW': 'U Rwanda', 'BL': 'Saint-Barthélemy', 'SH': 'St. Helena', 'KN': 'St. Kitts & Nevis', 'LC': 'St. Lucia', 'MF': 'Saint-Martin', 'PM': 'Saint-Pierre-et-Miquelon', 'VC': 'St. Vincent & Grenadines', 'WS': 'Samoa', 'SM': 'San Marino', 'ST': 'São Tomé e Príncipe', 'SA': 'المملكة العربية السعودية', 'SN': 'Senegaal', 'RS': 'Србија', 'SC': 'Seychelles', 'SL': 'Sierra Leone', 'SG': 'Singapore', 'SX': 'Sint Maarten', 'SK': 'Slovensko', 'SI': 'Slovenija', 'SB': 'Solomon Islands', 'SO': 'Soomaaliya', 'ZA': 'South Africa', 'GS': 'South Georgia and the South Sandwich Islands', 'SS': 'جنوب السودان', 'ES': 'España', 'LK': 'ශ්\u200dරී ලංකාව', 'SD': 'السودان', 'SR': 'Suriname', 'SJ': 'Svalbard og Jan Mayen', 'SZ': 'Eswatini', 'SE': 'Sverige', 'CH': 'Schweiz', 'SY': 'سوريا', 'TW': '台灣', 'TJ': 'Тоҷикистон', 'TZ': 'Tanzania', 'TH': 'ไทย', 'TL': 'Timor-Leste', 'TG': 'Togo', 'TK': 'Tokelau', 'TO': 'Tonga', 'TT': 'Trinidad & Tobago', 'TN': 'تونس', 'TR': 'Türkiye', 'TM': 'Türkmenistan', 'TC': 'Turks & Caicos Islands', 'TV': 'Tuvalu', 'UG': 'Uganda', 'UA': 'Україна', 'AE': 'الإمارات العربية المتحدة', 'GB': 'United Kingdom', 'US': 'United States', 'UM': 'U.S. Outlying Islands', 'UY': 'Uruguay', 'UZ': 'Oʻzbekiston', 'VU': 'Vanuatu', 'VE': 'Venezuela', 'VN': 'Việt Nam', 'VG': 'British Virgin Islands', 'VI': 'U.S. Virgin Islands', 'WF': 'Wallis-et-Futuna', 'EH': 'الصحراء الغربية', 'YE': 'اليمن', 'ZM': 'Zambia', 'ZW': 'Zimbabwe'}
LANGUAGE_NAMES = {'ur': 'اردو', 'kmr': 'Kurdî (Tirkiye)', 'sw': 'Kiswahili', 'pl': 'Polski', 'vi': 'Tiếng Việt', 'sq': 'Shqip', 'sv': 'Svenska', 'he': 'עברית', 'da': 'Dansk', 'pt_BR': 'Português (Brasil)', 'ja': '日本語', 'el': 'Ελληνικά', 'fy': 'Frysk', 'it': 'Italiano', 'ca': 'Català', 'nb_NO': 'Norsk Bokmål (Norge)', 'cs': 'Čeština', 'pa_PK': 'پنجابی (عربی, پاکستان)', 'te': 'తెలుగు', 'pt_PT': 'Português (Portugal)', 'zh_Hans': '中文 (简体)',
'ru': 'Русский', 'tl': 'Filipino (Pilipinas)', 'zh_Hant': '中文 (繁體)', 'ro': 'Română', 'uk': 'Українська', 'sr': 'Српски', 'ar': 'العربية', 'hu': 'Magyar', 'nl': 'Nederlands', 'bg': 'Български', 'bn': 'বাংলা', 'hi': 'हिन्दी', 'de': 'Deutsch', 'ko': '한국어', 'fi': 'Suomi', 'eo': 'Esperanto', 'id': 'Indonesia', 'fr': 'Français', 'es': 'Español', 'et': 'Eesti', 'en': 'English', 'fa': 'فارسی', 'cy': 'Cymraeg', 'th': 'ไทย', 'tr': 'Türkçe'}
TEXT_DIRECTIONS = {'ur': 'rtl', 'kmr': 'ltr', 'sw': 'ltr', 'pl': 'ltr', 'vi': 'ltr', 'sq': 'ltr', 'sv': 'ltr', 'he': 'rtl', 'da': 'ltr', 'pt_BR': 'ltr', 'ja': 'ltr', 'el': 'ltr', 'fy': 'ltr', 'it': 'ltr', 'ca': 'ltr', 'nb_NO': 'ltr', 'cs': 'ltr', 'pa_PK': 'rtl', 'te': 'ltr', 'pt_PT': 'ltr', 'zh_Hans': 'ltr', 'ru': 'ltr',
'tl': 'ltr', 'zh_Hant': 'ltr', 'ro': 'ltr', 'uk': 'ltr', 'sr': 'ltr', 'ar': 'rtl', 'hu': 'ltr', 'nl': 'ltr', 'bg': 'ltr', 'bn': 'ltr', 'hi': 'ltr', 'de': 'ltr', 'ko': 'ltr', 'fi': 'ltr', 'eo': 'ltr', 'id': 'ltr', 'fr': 'ltr', 'es': 'ltr', 'et': 'ltr', 'en': 'ltr', 'fa': 'rtl', 'cy': 'ltr', 'th': 'ltr', 'tr': 'ltr'}
LANGUAGE_NAMES = {'ca': 'Català', 'da': 'Dansk', 'sw': 'Kiswahili', 'de': 'Deutsch', 'sv': 'Svenska', 'pt_BR': 'Português (Brasil)', 'uk': 'Українська', 'cy': 'Cymraeg', 'zh_Hans': '中文 (简体)', 'fr': 'Français', 'hu': 'Magyar', 'he': 'עברית', 'eo': 'Esperanto', 'es': 'Español', 'tl': 'Filipino (Pilipinas)', 'bg': 'Български', 'it': 'Italiano', 'th': 'ไทย', 'ar': 'العربية', 'sq': 'Shqip', 'ja': '日本語', 'ru': 'Русский', 'fy': 'Frysk',
'zh_Hant': '中文 (繁體)', 'en': 'English', 'nl': 'Nederlands', 'nb_NO': 'Norsk Bokmål (Norge)', 'pa_PK': 'پنجابی (عربی, پاکستان)', 'tr': 'Türkçe', 'pl': 'Polski', 'fi': 'Suomi', 'el': 'Ελληνικά', 'kmr': 'Kurdî (Tirkiye)', 'et': 'Eesti', 'ko': '한국어', 'vi': 'Tiếng Việt', 'ro': 'Română', 'bn': 'বাংলা', 'te': 'తెలుగు', 'fa': 'فارسی', 'cs': 'Čeština', 'pt_PT': 'Português (Portugal)', 'hi': 'हिन्दी', 'id': 'Indonesia', 'ur': 'اردو', 'sr': 'Српски'}
TEXT_DIRECTIONS = {'ca': 'ltr', 'da': 'ltr', 'sw': 'ltr', 'de': 'ltr', 'sv': 'ltr', 'pt_BR': 'ltr', 'uk': 'ltr', 'cy': 'ltr', 'zh_Hans': 'ltr', 'fr': 'ltr', 'hu': 'ltr', 'he': 'rtl', 'eo': 'ltr', 'es': 'ltr', 'tl': 'ltr', 'bg': 'ltr', 'it': 'ltr', 'th': 'ltr', 'ar': 'rtl', 'sq': 'ltr', 'ja': 'ltr', 'ru': 'ltr', 'fy': 'ltr',
'zh_Hant': 'ltr', 'en': 'ltr', 'nl': 'ltr', 'nb_NO': 'ltr', 'pa_PK': 'rtl', 'tr': 'ltr', 'pl': 'ltr', 'fi': 'ltr', 'el': 'ltr', 'kmr': 'ltr', 'et': 'ltr', 'ko': 'ltr', 'vi': 'ltr', 'ro': 'ltr', 'bn': 'ltr', 'te': 'ltr', 'fa': 'rtl', 'cs': 'ltr', 'pt_PT': 'ltr', 'hi': 'ltr', 'id': 'ltr', 'ur': 'rtl', 'sr': 'ltr'}
19 changes: 19 additions & 0 deletions templates/class-live-stats.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{% extends "auth.html" %}

{% block regular_content %}
{# This template holds the outer parts of the page, while the partial is the one we will be rendering trtough HTMX calls #}
{{
render_partial('partial-class-live-stats.html',
class_info=class_info,
class_overview=class_overview,
dashboard_options=dashboard_options,
attempted_adventures=attempted_adventures,
dashboard_options_args=dashboard_options_args,
adventures=adventures,
max_level=max_level,
current_page=current_page,
page_title=page_title
)
}}

{% endblock %}
Loading

0 comments on commit 7e25197

Please sign in to comment.