diff --git a/app/assets/javascripts/editor/editor.js.erb b/app/assets/javascripts/editor/editor.js.erb index cfb48fad..c72d2fac 100644 --- a/app/assets/javascripts/editor/editor.js.erb +++ b/app/assets/javascripts/editor/editor.js.erb @@ -315,6 +315,9 @@ var CodeOceanEditor = { }).fail(_.noop) .always(function () { ace.edit(editor).session.setMode(newMode); + if (newMode === 'ace/mode/python') { + ace.edit(editor).setTheme('ace/theme/tomorrow') + } }); }, diff --git a/app/assets/javascripts/exercises.js.erb b/app/assets/javascripts/exercises.js.erb index 000637e6..a82b590f 100644 --- a/app/assets/javascripts/exercises.js.erb +++ b/app/assets/javascripts/exercises.js.erb @@ -30,7 +30,11 @@ $(document).on('turbolinks:load', function () { editor.setShowPrintMargin(false); editor.setTheme(THEME); + // For creating / editing an exercise var textarea = $('textarea[id="exercise_files_attributes_' + index + '_content"]'); + if ($('.edit_tip, .new_tip').isPresent()) { + textarea = $('textarea[id="tip_example"]') + } var content = textarea.val(); if (content != undefined) { @@ -383,11 +387,11 @@ $(document).on('turbolinks:load', function () { observeExportButtons(); } toggleCodeHeight(); - if (window.hljs) { - highlightCode(); - } } + if (window.hljs) { + highlightCode(); + } if ($('#editor-edit').isPresent()) { configureEditors(); diff --git a/app/assets/javascripts/forms.js b/app/assets/javascripts/forms.js index b74840bb..91d19f17 100644 --- a/app/assets/javascripts/forms.js +++ b/app/assets/javascripts/forms.js @@ -31,12 +31,16 @@ $(document).on('turbolinks:load', function() { }); // enable chosen hook when editing an exercise to update ace code highlighting - if ($.isController('exercises') && $('.edit_exercise, .new_exercise').isPresent()) { + if ($.isController('exercises') && $('.edit_exercise, .new_exercise').isPresent() || + $.isController('tips') && $('.edit_tip, .new_tip').isPresent() ) { chosen_inputs.filter(function(){ return $(this).attr('id').includes('file_type_id'); }).on('change chosen:ready', function(event, parameter) { // Set ACE editor mode (for code highlighting) on change of file type and after initialization editorInstance = $(event.target).closest('.card-body').find('.editor')[0]; + if (editorInstance === undefined) { + editorInstance = $(event.target).closest('.container').find('.editor')[0]; + } selectedFileType = event.target.value; CodeOceanEditor.updateEditorModeToFileTypeID(editorInstance, selectedFileType); }) diff --git a/app/controllers/tips_controller.rb b/app/controllers/tips_controller.rb new file mode 100644 index 00000000..9fa5170c --- /dev/null +++ b/app/controllers/tips_controller.rb @@ -0,0 +1,62 @@ +class TipsController < ApplicationController + include CommonBehavior + + before_action :set_tip, only: MEMBER_ACTIONS + before_action :set_file_types, only: %i[create edit new update] + + def authorize! + authorize(@tip || @tips) + end + private :authorize! + + def create + @tip = Tip.new(tip_params) + authorize! + create_and_respond(object: @tip) + end + + def destroy + destroy_and_respond(object: @tip) + end + + def edit + end + + def tip_params + return unless params[:tip].present? + + params[:tip] + .permit(:title, :description, :example, :file_type_id) + .each { |_key, value| value.strip! unless value.is_a?(Array) } + .merge(user_id: current_user.id, user_type: current_user.class.name) + end + private :tip_params + + def index + @tips = Tip.all.paginate(page: params[:page]) + authorize! + end + + def new + @tip = Tip.new + authorize! + end + + def set_tip + @tip = Tip.find(params[:id]) + authorize! + end + private :set_tip + + def show + end + + def update + update_and_respond(object: @tip, params: tip_params) + end + + def set_file_types + @file_types = FileType.all.order(:name) + end + private :set_file_types +end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 844a87c4..20b89244 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -5,10 +5,10 @@ module ApplicationHelper APPLICATION_NAME end - def code_tag(code) + def code_tag(code, language = nil) if code.present? content_tag(:pre) do - content_tag(:code, code) + content_tag(:code, code, class: "language-#{language}") end else empty diff --git a/app/models/file_type.rb b/app/models/file_type.rb index ee777092..cf128a62 100644 --- a/app/models/file_type.rb +++ b/app/models/file_type.rb @@ -32,6 +32,10 @@ class FileType < ApplicationRecord end private :set_default_values + def programming_language + editor_mode.gsub('ace/mode/', '') + end + def to_s name end diff --git a/app/models/tip.rb b/app/models/tip.rb index 5fb82d39..85427f41 100644 --- a/app/models/tip.rb +++ b/app/models/tip.rb @@ -20,4 +20,9 @@ class Tip < ApplicationRecord "#{I18n.t('activerecord.models.tip.one')} #{id}" end end + + def can_be_destroyed? + # This tip can only be destroyed if it is no parent to any other exercise tip + ExerciseTip.where(parent_exercise_tip: exercise_tips).none? + end end diff --git a/app/policies/tip_policy.rb b/app/policies/tip_policy.rb new file mode 100644 index 00000000..b9a313ad --- /dev/null +++ b/app/policies/tip_policy.rb @@ -0,0 +1,13 @@ +class TipPolicy < AdminOnlyPolicy + + class Scope < Scope + def resolve + if @user.admin? || @user.teacher? + @scope.all + else + @scope.none + end + end + end + +end diff --git a/app/views/application/_navigation.html.slim b/app/views/application/_navigation.html.slim index 738dfc9d..5921a208 100644 --- a/app/views/application/_navigation.html.slim +++ b/app/views/application/_navigation.html.slim @@ -10,7 +10,7 @@ li = link_to(t('breadcrumbs.statistics.show'), statistics_path, class: 'dropdown-item') if policy(:statistics).show? li.dropdown-divider role='separator' = render('navigation_submenu', title: t('activerecord.models.exercise.other'), - models: [Exercise, ExerciseCollection, ProxyExercise, Tag, Submission], link: exercises_path, cached: true) + models: [Exercise, ExerciseCollection, ProxyExercise, Tag, Tip, Submission], link: exercises_path, cached: true) = render('navigation_submenu', title: t('navigation.sections.users'), models: [InternalUser, ExternalUser], cached: true) = render('navigation_collection_link', model: StudyGroup, cached: true) diff --git a/app/views/tips/_collapsed_card.html.slim b/app/views/tips/_collapsed_card.html.slim index fd581350..262831e4 100644 --- a/app/views/tips/_collapsed_card.html.slim +++ b/app/views/tips/_collapsed_card.html.slim @@ -13,11 +13,11 @@ .card-body.p-3 h5 = t('exercises.implement.tips.description') - = tip.description + = render_markdown(tip.description) - if tip.example? - h5.mt-2 + h5 = t('exercises.implement.tips.example') pre - code.mh-100 class="language-#{tip.file_type.editor_mode.gsub("ace/mode/", "")}" + code.mh-100 class="language-#{tip.file_type.programming_language}" = tip.example = render(partial: 'tips/collapsed_card', collection: exercise_tip.children, as: :exercise_tip) diff --git a/app/views/tips/_form.html.slim b/app/views/tips/_form.html.slim new file mode 100644 index 00000000..d5f317d3 --- /dev/null +++ b/app/views/tips/_form.html.slim @@ -0,0 +1,21 @@ += form_for(@tip, builder: PagedownFormBuilder) do |f| + = render('shared/form_errors', object: @tip) + .form-group + = f.label(:title) + = f.text_field(:title, class: 'form-control', required: false) + .form-group + = f.label(:description) + = f.pagedown :description, input_html: { preview: true, rows: 5 } + .form-group + = f.label(:file_type_id, t('activerecord.attributes.file.file_type_id')) + = f.collection_select(:file_type_id, @file_types, :id, :name, {include_blank: true}, class: 'form-control') + .form-group + = f.label(:example) + = f.text_area(:example, class: 'code-field form-control', rows: 5, style: "display:none;", required: false) + #editor-edit.original-input data-file-id=@tip.id + #frames + .edit-frame + .editor-content.d-none + .editor.allow_ace_tooltip + .actions = render('shared/submit_button', f: f, object: @tip) + .editor \ No newline at end of file diff --git a/app/views/tips/edit.html.slim b/app/views/tips/edit.html.slim new file mode 100644 index 00000000..cf442c6f --- /dev/null +++ b/app/views/tips/edit.html.slim @@ -0,0 +1,3 @@ +h1 = @tip.to_s + += render('form') diff --git a/app/views/tips/index.html.slim b/app/views/tips/index.html.slim new file mode 100644 index 00000000..01edc1ad --- /dev/null +++ b/app/views/tips/index.html.slim @@ -0,0 +1,20 @@ +h1 = Tip.model_name.human(count: 2) + +.table-responsive + table.sortable.table + thead + tr + th = t('activerecord.attributes.tip.title') + th = t('activerecord.attributes.file.file_type') + th colspan=3 = t('shared.actions') + tbody + - @tips.each do |tip| + tr + td = link_to_if(policy(tip).show?, tip.title || tip.to_s, tip) + td = tip.file_type ? link_to_if(policy(tip.file_type).show?, tip.file_type.name, tip.file_type) : '' + td = link_to(t('shared.show'), tip) if policy(tip).show? + td = link_to(t('shared.edit'), edit_tip_path(tip)) if policy(tip).edit? + td = link_to(t('shared.destroy'), tip, data: {confirm: t('shared.confirm_destroy')}, method: :delete) if tip.can_be_destroyed? && policy(tip).destroy? + += render('shared/pagination', collection: @tips) +p = render('shared/new_button', model: Tip, path: new_tip_path) diff --git a/app/views/tips/new.html.slim b/app/views/tips/new.html.slim new file mode 100644 index 00000000..b8ad65c4 --- /dev/null +++ b/app/views/tips/new.html.slim @@ -0,0 +1,3 @@ +h1 = t('shared.new_model', model: Tip.model_name.human) + += render('form') diff --git a/app/views/tips/show.html.slim b/app/views/tips/show.html.slim new file mode 100644 index 00000000..a4528da4 --- /dev/null +++ b/app/views/tips/show.html.slim @@ -0,0 +1,17 @@ +- content_for :head do + // Force a full page reload, see https://github.com/turbolinks/turbolinks/issues/326. + Otherwise, code might not be highlighted correctly (race condition) + meta name='turbolinks-visit-control' content='reload' + = javascript_pack_tag('highlight', 'data-turbolinks-track': true) + = stylesheet_pack_tag('highlight', media: 'all', 'data-turbolinks-track': true) + +h1 + = @tip.to_s + = render('shared/edit_button', object: @tip) + += row(label: 'tip.title', value: @tip.title) += row(label: 'tip.description', value: render_markdown(@tip.description), class: 'm-0') += row(label: 'file.file_type', value: @tip.file_type_id? ? \ + link_to_if(policy(@tip.file_type).show?, @tip.file_type.name, @tip.file_type) : '') += row(label: 'tip.example', value: @tip.file_type_id? ? \ + code_tag(@tip.example, @tip.file_type.programming_language) : '') diff --git a/config/routes.rb b/config/routes.rb index e6d8464e..e13c3b52 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -107,6 +107,8 @@ Rails.application.routes.draw do resources :tags + resources :tips + resources :user_exercise_feedbacks, except: [:show, :index] resources :external_users, only: [:index, :show], concerns: :statistics do