'): + lines.append(line.replace('
', '\n') + .replace('', '**') + .replace('', '**') + .replace('', '`')
+ .replace('
', '`'))
+
+ return '\n'.join(lines)
+
+ def generate_files(self):
+ """Generate all the stored files for a question."""
+ # YAML for structure file
+ with open(f'./programming/review/structure/{self.object.slug}.yaml', 'w') as file:
+ yaml.dump(self.generate_YAML(), file)
+
+ # Question text
+ with open(f'./programming/review/en/{self.object.slug}/question.md', 'w') as file:
+ file.write(self.generate_markdown())
+
+ # Question solution
+ with open(f'./programming/review/en/{self.object.slug}/solution.py', 'w') as file:
+ # Add a newline to the end of the file if needed
+ solution = self.object.solution
+ solution_lines = self.object.solution.split('/n')
+ if solution_lines[-1] != '':
+ solution += '\n'
+
+ file.write(solution)
+
+ # Initial code (for debugging)
+ if self.object.question_type == 'debugging':
+ with open(f'./programming/review/en/{self.object.slug}/initial.py', 'w') as file:
+ file.write(self.object.initial_code)
+
+ # Iterate through test cases
+ test_case_suffix = 'input' if self.object.question_type == 'program' else 'code'
+ for test_case in list(self.object.draft_test_cases.values()):
+ test_case_file_prefix = f'./programming/review/en/{self.object.slug}/test-case-{test_case["number"]}'
+ with open(f'{test_case_file_prefix}-{test_case_suffix}.txt', 'w') as file:
+ file.write(test_case["test_code"])
+ with open(f'{test_case_file_prefix}-output.txt', 'w') as file:
+ file.write(test_case["expected_output"])
+
+ # Create YAML file to store the macros
+ macros = []
+ for macro in DraftMacro.objects.filter(draft=self.object):
+ values = []
+ for value in list(macro.macro_values.values()):
+ values.append(value['value'])
+ macros.append({macro.placeholder: values})
+ if len(macros) > 0:
+ with open(f'./programming/review/en/{self.object.slug}/macros.yaml', 'w') as file:
+ yaml.dump(macros, file)
+
+ def post(self, request, *args, **kwargs):
+ """Handle post request to submit a draft."""
+ if not request.user.is_authenticated:
+ return HttpResponseForbidden()
+
+ self.object = self.get_object()
+
+ if self.is_valid_question(request):
+ # Generate files
+ self.generate_files()
+
+ # Delete from database
+ self.object.delete()
+
+ # Send user to question list page
+ messages.info(request, 'Question submitted successfully.')
+ return redirect(reverse('programming:draft_list'))
+
+ # Question was invalid, send user to question creation form to fix the issues
+ return redirect(reverse('programming:edit_draft', kwargs={'pk': self.kwargs['pk']}))
+
+
+class DraftQuestionView(LoginRequiredMixin, generic.CreateView, generic.UpdateView):
+ """Display the form for editing a draft question."""
+
+ template_name = 'programming/add_question.html'
+ template_name_suffix = ''
+ form_class = NewQuestionForm
+ model = Draft
+
+ def get_object(self, **kwargs):
+ """Get question object for view."""
+ try:
+ if 'pk' in self.kwargs:
+ draft = Draft.objects.get_subclass(
+ pk=self.kwargs['pk']
+ )
+ else:
+ # Here, we create a new question
+ draft = None
+ except Draft.DoesNotExist:
+ raise Http404("No draft question matches the given ID.")
+
+ return draft
+
+ def get_context_data(self, **kwargs):
+ """
+ Provide the context data for the create/edit question view.
+
+ Returns: Dictionary of context data.
+ """
+ context = super().get_context_data(**kwargs)
+ context['question'] = self.object
+ if context['question'] is not None:
+ test_cases = self.object.draft_test_cases.values()
+ context['test_cases'] = test_cases
+ context['test_cases_json'] = json.dumps(list(test_cases))
+
+ fetched_macros = DraftMacro.objects.filter(draft=self.object)
+ macros = []
+ for macro in fetched_macros:
+ macro_values = macro.macro_values.values()
+ macros.append({
+ 'placeholder': macro.placeholder,
+ 'values': [macro_val['value'] for macro_val in macro_values]
+ })
+
+ context['macros_json'] = json.dumps(list(macros))
+ context['forms'] = {
+ "main_form": NewQuestionForm(instance=context['question']),
+ "macro_form": MacroForm(),
+ "test_case_form": TestCaseForm(),
+ }
+
+ return context
+
+ def _custom_split(self, string):
+ """Split on commas, but allow backslashes to escape splitting."""
+ parts = string.split(',')
+ i = 1
+ output = [parts[0]]
+
+ while i < len(parts):
+ if output[-1].endswith('\\'):
+ output[-1] = output[-1][:-1] + ',' + parts[i]
+ else:
+ output.append(parts[i])
+ i += 1
+
+ return output
+
+ def form_valid(self, form, *args, **kwargs):
+ """Save the draft when a valid form is submitted."""
+ if not self.request.user.is_authenticated:
+ return HttpResponseForbidden()
+ self.object = self.get_object()
+
+ # First save the draft
+ draft = form.save(commit=False)
+ if self.object is None:
+ # New draft
+ draft.languages = ['en']
+ draft.author_id = self.request.user.id
+ draft.slug = self._generate_slug(form.cleaned_data)
+ draft.save()
+
+ # Then fetch/save many-to-many fields
+ # Concepts
+ concept_names = form.cleaned_data.get('concepts')
+ if 'conditionals' in concept_names:
+ concept_names.remove('conditionals')
+ concept_names += [form.cleaned_data.get('concept_conditionals')]
+ if 'loops' in concept_names:
+ concept_names.remove('loops')
+ concept_names += [form.cleaned_data.get('concept_loops')]
+
+ # Contexts
+ context_names = form.cleaned_data.get('contexts')
+ if 'mathematics' in context_names:
+ context_names.remove('mathematics')
+ context_names += [form.cleaned_data.get('context_mathematics')]
+ context_names.remove('') # Handle the case where only geometry is selected
+ if form.cleaned_data.get('context_has_geometry'):
+ context_names += [form.cleaned_data.get('context_geometry')]
+
+ # Test cases
+ test_case_lines = form.cleaned_data.get('test_cases').split('\n')
+ saved_test_cases = DraftTestCase.objects.filter(draft=draft)
+ for i in range(len(test_case_lines)):
+ parts = test_case_lines[i].split('@@')
+ if len(parts) != 3:
+ continue
+ given_type = parts[0]
+ code = parts[1]
+ expected_output = parts[2]
+
+ if i < len(saved_test_cases):
+ test_case = saved_test_cases[i]
+ else:
+ test_case = DraftTestCase()
+
+ # Fill data
+ test_case.number = i + 1
+ test_case.type = given_type
+ test_case.test_code = code
+ test_case.expected_output = expected_output
+ test_case.draft = draft
+
+ test_case.save()
+ # Remove test cases that have been deleted
+ for j in range(i + 1, len(saved_test_cases)):
+ saved_test_cases[j].delete()
+
+ # Macros
+ macro_lines = form.cleaned_data.get('macros').split('\n')
+ saved_macros = DraftMacro.objects.filter(draft=draft)
+ for i in range(len(macro_lines)):
+ parts = macro_lines[i].split('@@')
+ if len(parts) != 2:
+ continue
+ name = parts[0]
+ values = self._custom_split(parts[1])
+
+ if i < len(saved_macros):
+ macro = saved_macros[i]
+ else:
+ macro = DraftMacro()
+
+ macro.placeholder = name
+ macro.draft = draft
+ macro.save()
+
+ saved_values = DraftMacroValue.objects.filter(macro=macro)
+ for j in range(len(values)):
+ if j < len(saved_values):
+ possible_value = saved_values[j]
+ else:
+ possible_value = DraftMacroValue()
+ possible_value.macro = macro
+ possible_value.value = values[j]
+ possible_value.save()
+
+ # Remove values that have been deleted
+ for k in range(j + 1, len(saved_values)):
+ # saved_values[k].delete()
+ print("Delete value")
+ # Remove macros that have been deleted
+ for j in range(i + 1, len(saved_macros)):
+ # saved_macros[j].delete()
+ print("Delete macro")
+
+ # Apply many-to-many fields to question
+ for name in concept_names:
+ concept_obj = ProgrammingConcepts.objects.get(slug=name)
+ draft.concepts.add(concept_obj)
+
+ for name in context_names:
+ context_obj = QuestionContexts.objects.get(slug=name)
+ draft.contexts.add(context_obj)
+
+ return redirect(self.get_success_url())
+
+ def _generate_slug(self, cleaned):
+ """
+ Create a slug for new questions in a similar style to existing questions.
+
+ Make title lowercase, replace spaces with hyphens, and add -', + '</p>': '
', + '<code>': '',
+ '</code>': '
',
+ '<sup>': '',
+ '</sup>': '',
+ '<strong>': '',
+ '</strong>': '',
+}
+
+// Setup
+$(document).ready(function() {
+ // Perform initial formatting
+ // Go to preview tab to render visibility-dependent items
+ $('#preview-select-tab a[href="#preview"]').tab('show');
+ preview_editor = base.create_new_editor("code");
+
+ // Return to details tab
+ $('#preview-select-tab a[href="#details"]').tab('show');
+ solution_editor = base.create_new_editor("id_solution");
+ solution_editor.on('blur', function() {
+ let example_code = solution_editor.getValue();
+ run_code(null, example_code);
+ for (var number in test_cases) {
+ update_test_case_tables(number, example_code);
+ }
+ update_test_cases();
+ });
+ initial_editor = base.create_new_editor("id_initial_code");
+
+ // Hide form fields which are displayed differently
+ $("#id_test_cases").hide();
+ $("#id_macros").hide();
+
+ setup_concepts_contexts();
+ setup_drag_and_drop()
+ setup_form()
+
+ // Bind event-driven functions
+ $('#preview-tab-button').on('shown.bs.tab', function (event) {
+ see_preview();
+ });
+ $('#id_question_type').change(function () {
+ update_form();
+ });
+
+ // Run once initially as this is the first active tab
+ load_test_cases();
+ load_macros();
+ update_macro_table();
+
+ initial_fill_form();
+});
+
+function load_test_cases() {
+ // Load any initial test cases
+ if (typeof test_cases_list === 'undefined') {
+ return;
+ }
+
+ for (let i = 0; i < test_cases_list.length; i++) {
+ let data = test_cases_list[i];
+ test_cases[data.number] = data;
+ test_cases[data.number]['saved_input'] = test_cases[data.number].test_code;
+ // Test code is set by default
+ if (question_type == 'program') {
+ test_cases[data.number].test_input = test_cases[data.number]['saved_input'];
+ delete test_cases[data.number].test_code;
+ }
+ }
+}
+
+function load_macros() {
+ // Load any initial macros
+ if (typeof macros_list === 'undefined') {
+ return;
+ }
+
+ let value_to_store = "";
+
+ for (let i = 0; i < macros_list.length; i++) {
+ let macro = macros_list[i];
+ macros.aliases.push(macro['placeholder']);
+ macros.substitutes.push(macro['values']);
+ value_to_store += `${macro['placeholder']}@@${macro['values'].join(',')}\n`;
+ }
+
+ // Update the form field
+ $('#id_macros').val(value_to_store);
+}
+
+function setup_form() {
+ // Define button actions
+ $('#btn_new_macro').on("click", function() {
+ create_edit_sub_form(MACRO_ATTR);
+ });
+ $('#btn_new_test_case').on("click", function() {
+ create_edit_sub_form(TEST_CASE_ATTR);
+ });
+ $('#btn_save_concepts').on("click", function() {
+ save_tags('concept');
+ });
+ $('#btn_save_contexts').on("click", function() {
+ save_tags('context');
+ });
+ $('#btn-macro-decrease').on("click", function() {
+ step_macros(-1);
+ });
+ $('#btn-macro-increase').on("click", function() {
+ step_macros(1);
+ });
+ $('.btn-edit-test-case').on("click", function() {
+ edit_sub_form($(this));
+ });
+ $('.btn-edit-macro').on("click", function() {
+ edit_sub_form($(this));
+ });
+}
+
+function setup_drag_and_drop(setup_target=null) {
+ if(setup_target == null) {
+ setup_target = $('.dnd-interactable');
+ }
+
+ // Drag-and-drop table control, from
+ // https://www.therogerlab.com/sandbox/pages/how-to-reorder-table-rows-in-javascript?s=0ea4985d74a189e8b7b547976e7192ae.4122809346f6a15e41c9a43f6fcb5fd5
+ setup_target.on('dragstart', function (event){
+ let selected_item = event.target.tagName;
+ if (selected_item == "TR") {
+ drag_and_drop_row = event.target;
+ } else if (selected_item == "TD") {
+ drag_and_drop_row = event.target.parentNode;
+ } else {
+ return;
+ }
+ drag_and_drop_row.style.backgroundColor = "lightgrey";
+ });
+ setup_target.on('dragover', function (event){
+ event.preventDefault();
+
+ if (drag_and_drop_row != null && event.target.tagName == "TD") {
+ // Make sure only the relevant table can be targeted
+ if (!event.target.parentNode.classList.contains('dnd-interactable')) {
+ return;
+ }
+ // Sort the list to force consistency
+ let children = Array.from(event.target.parentNode.parentNode.children).sort(function(a, b) {
+ // Comparing id of the output-help-text element because it is the most uniquely identifiable
+ return parseInt(a.querySelector('p').id.slice(10,11)) - parseInt(b.querySelector('p').id.slice(10,11));
+ });
+ if (children.indexOf(event.target.parentNode)${code}
An error has occurred once our test code has been added to the end of yours. You may not have terminated your strings correctly (is there a closing quote for every opening quote?)
`; + let output_cell = `${escapeHtml(parts[1])}
${parts[2]}
Currently all questions are based in Python 3.
+ + {% include "programming/question_creation/configurable_delete_modal.html" %} + ++ Sorry! + No questions found matching the selected filters. +
+ {% endif %} + +{% endblock content %} + +{% block scripts %} + + + +{% endblock scripts %} diff --git a/codewof/templates/programming/question.html b/codewof/templates/programming/question.html index d080dc6b4..42fa632b3 100644 --- a/codewof/templates/programming/question.html +++ b/codewof/templates/programming/question.html @@ -52,9 +52,7 @@