Add backend for tips and enable markdown support
This commit is contained in:
@ -315,6 +315,9 @@ var CodeOceanEditor = {
|
|||||||
}).fail(_.noop)
|
}).fail(_.noop)
|
||||||
.always(function () {
|
.always(function () {
|
||||||
ace.edit(editor).session.setMode(newMode);
|
ace.edit(editor).session.setMode(newMode);
|
||||||
|
if (newMode === 'ace/mode/python') {
|
||||||
|
ace.edit(editor).setTheme('ace/theme/tomorrow')
|
||||||
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -30,7 +30,11 @@ $(document).on('turbolinks:load', function () {
|
|||||||
editor.setShowPrintMargin(false);
|
editor.setShowPrintMargin(false);
|
||||||
editor.setTheme(THEME);
|
editor.setTheme(THEME);
|
||||||
|
|
||||||
|
// For creating / editing an exercise
|
||||||
var textarea = $('textarea[id="exercise_files_attributes_' + index + '_content"]');
|
var textarea = $('textarea[id="exercise_files_attributes_' + index + '_content"]');
|
||||||
|
if ($('.edit_tip, .new_tip').isPresent()) {
|
||||||
|
textarea = $('textarea[id="tip_example"]')
|
||||||
|
}
|
||||||
var content = textarea.val();
|
var content = textarea.val();
|
||||||
|
|
||||||
if (content != undefined) {
|
if (content != undefined) {
|
||||||
@ -383,11 +387,11 @@ $(document).on('turbolinks:load', function () {
|
|||||||
observeExportButtons();
|
observeExportButtons();
|
||||||
}
|
}
|
||||||
toggleCodeHeight();
|
toggleCodeHeight();
|
||||||
if (window.hljs) {
|
|
||||||
highlightCode();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (window.hljs) {
|
||||||
|
highlightCode();
|
||||||
|
}
|
||||||
|
|
||||||
if ($('#editor-edit').isPresent()) {
|
if ($('#editor-edit').isPresent()) {
|
||||||
configureEditors();
|
configureEditors();
|
||||||
|
@ -31,12 +31,16 @@ $(document).on('turbolinks:load', function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// enable chosen hook when editing an exercise to update ace code highlighting
|
// 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(){
|
chosen_inputs.filter(function(){
|
||||||
return $(this).attr('id').includes('file_type_id');
|
return $(this).attr('id').includes('file_type_id');
|
||||||
}).on('change chosen:ready', function(event, parameter) {
|
}).on('change chosen:ready', function(event, parameter) {
|
||||||
// Set ACE editor mode (for code highlighting) on change of file type and after initialization
|
// Set ACE editor mode (for code highlighting) on change of file type and after initialization
|
||||||
editorInstance = $(event.target).closest('.card-body').find('.editor')[0];
|
editorInstance = $(event.target).closest('.card-body').find('.editor')[0];
|
||||||
|
if (editorInstance === undefined) {
|
||||||
|
editorInstance = $(event.target).closest('.container').find('.editor')[0];
|
||||||
|
}
|
||||||
selectedFileType = event.target.value;
|
selectedFileType = event.target.value;
|
||||||
CodeOceanEditor.updateEditorModeToFileTypeID(editorInstance, selectedFileType);
|
CodeOceanEditor.updateEditorModeToFileTypeID(editorInstance, selectedFileType);
|
||||||
})
|
})
|
||||||
|
62
app/controllers/tips_controller.rb
Normal file
62
app/controllers/tips_controller.rb
Normal 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
|
@ -5,10 +5,10 @@ module ApplicationHelper
|
|||||||
APPLICATION_NAME
|
APPLICATION_NAME
|
||||||
end
|
end
|
||||||
|
|
||||||
def code_tag(code)
|
def code_tag(code, language = nil)
|
||||||
if code.present?
|
if code.present?
|
||||||
content_tag(:pre) do
|
content_tag(:pre) do
|
||||||
content_tag(:code, code)
|
content_tag(:code, code, class: "language-#{language}")
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
empty
|
empty
|
||||||
|
@ -32,6 +32,10 @@ class FileType < ApplicationRecord
|
|||||||
end
|
end
|
||||||
private :set_default_values
|
private :set_default_values
|
||||||
|
|
||||||
|
def programming_language
|
||||||
|
editor_mode.gsub('ace/mode/', '')
|
||||||
|
end
|
||||||
|
|
||||||
def to_s
|
def to_s
|
||||||
name
|
name
|
||||||
end
|
end
|
||||||
|
@ -20,4 +20,9 @@ class Tip < ApplicationRecord
|
|||||||
"#{I18n.t('activerecord.models.tip.one')} #{id}"
|
"#{I18n.t('activerecord.models.tip.one')} #{id}"
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
13
app/policies/tip_policy.rb
Normal file
13
app/policies/tip_policy.rb
Normal 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
|
@ -10,7 +10,7 @@
|
|||||||
li = link_to(t('breadcrumbs.statistics.show'), statistics_path, class: 'dropdown-item') if policy(:statistics).show?
|
li = link_to(t('breadcrumbs.statistics.show'), statistics_path, class: 'dropdown-item') if policy(:statistics).show?
|
||||||
li.dropdown-divider role='separator'
|
li.dropdown-divider role='separator'
|
||||||
= render('navigation_submenu', title: t('activerecord.models.exercise.other'),
|
= 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],
|
= render('navigation_submenu', title: t('navigation.sections.users'), models: [InternalUser, ExternalUser],
|
||||||
cached: true)
|
cached: true)
|
||||||
= render('navigation_collection_link', model: StudyGroup, cached: true)
|
= render('navigation_collection_link', model: StudyGroup, cached: true)
|
||||||
|
@ -13,11 +13,11 @@
|
|||||||
.card-body.p-3
|
.card-body.p-3
|
||||||
h5
|
h5
|
||||||
= t('exercises.implement.tips.description')
|
= t('exercises.implement.tips.description')
|
||||||
= tip.description
|
= render_markdown(tip.description)
|
||||||
- if tip.example?
|
- if tip.example?
|
||||||
h5.mt-2
|
h5
|
||||||
= t('exercises.implement.tips.example')
|
= t('exercises.implement.tips.example')
|
||||||
pre
|
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
|
= tip.example
|
||||||
= render(partial: 'tips/collapsed_card', collection: exercise_tip.children, as: :exercise_tip)
|
= render(partial: 'tips/collapsed_card', collection: exercise_tip.children, as: :exercise_tip)
|
||||||
|
21
app/views/tips/_form.html.slim
Normal file
21
app/views/tips/_form.html.slim
Normal 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
|
3
app/views/tips/edit.html.slim
Normal file
3
app/views/tips/edit.html.slim
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
h1 = @tip.to_s
|
||||||
|
|
||||||
|
= render('form')
|
20
app/views/tips/index.html.slim
Normal file
20
app/views/tips/index.html.slim
Normal 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)
|
3
app/views/tips/new.html.slim
Normal file
3
app/views/tips/new.html.slim
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
h1 = t('shared.new_model', model: Tip.model_name.human)
|
||||||
|
|
||||||
|
= render('form')
|
17
app/views/tips/show.html.slim
Normal file
17
app/views/tips/show.html.slim
Normal 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) : '')
|
@ -107,6 +107,8 @@ Rails.application.routes.draw do
|
|||||||
|
|
||||||
resources :tags
|
resources :tags
|
||||||
|
|
||||||
|
resources :tips
|
||||||
|
|
||||||
resources :user_exercise_feedbacks, except: [:show, :index]
|
resources :user_exercise_feedbacks, except: [:show, :index]
|
||||||
|
|
||||||
resources :external_users, only: [:index, :show], concerns: :statistics do
|
resources :external_users, only: [:index, :show], concerns: :statistics do
|
||||||
|
Reference in New Issue
Block a user