Add backend for tips and enable markdown support
This commit is contained in:
@ -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')
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -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 ($('#editor-edit').isPresent()) {
|
||||
configureEditors();
|
||||
|
@ -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);
|
||||
})
|
||||
|
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
|
||||
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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
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.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)
|
||||
|
@ -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)
|
||||
|
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 :tips
|
||||
|
||||
resources :user_exercise_feedbacks, except: [:show, :index]
|
||||
|
||||
resources :external_users, only: [:index, :show], concerns: :statistics do
|
||||
|
Reference in New Issue
Block a user