Add backend for tips and enable markdown support

This commit is contained in:
Sebastian Serth
2020-10-08 19:04:10 +02:00
parent dce5998a2a
commit e550828c58
16 changed files with 171 additions and 10 deletions

View File

@ -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')
}
});
},

View File

@ -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();

View File

@ -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);
})

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -0,0 +1,3 @@
h1 = @tip.to_s
= render('form')

View File

@ -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)

View File

@ -0,0 +1,3 @@
h1 = t('shared.new_model', model: Tip.model_name.human)
= render('form')

View File

@ -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) : '')

View File

@ -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