diff --git a/app/assets/javascripts/course_offerings.js b/app/assets/javascripts/course_offerings.js index 7bc01fca7..cfd1926e0 100644 --- a/app/assets/javascripts/course_offerings.js +++ b/app/assets/javascripts/course_offerings.js @@ -1,5 +1,5 @@ (function () { - var check_completeness, form_alert, handle_submit, init, reset_alert_area, valid_token; + var check_completeness, form_alert, handle_submit, handle_generate_textbook, init, reset_alert_area, valid_token; $(document).ready(function () { @@ -21,7 +21,12 @@ $(this).prop('disabled', true); return handle_submit(); }); - + + $('#btn-gen-textbook').click(function () { + $(this).prop('disabled', true); + return handle_generate_textbook(); + }); + $('#display').click(function () { return handle_select_student(); //return handle_display(); @@ -123,18 +128,22 @@ return $('#alerts').append(alert_box); }; - check_completeness = function () { + check_completeness = function (isTextbook) { var messages; messages = []; - if ($('#lms-instance-select').val() === '') { - messages.push('One of the LMS instances has to be selected.'); - } - if (!valid_token) { - messages.push('You have to provide an access token for the selected Canvas instance.'); - } - if ($('#lms-course-num').val() === '') { - messages.push('You have to write LMS course Id.'); + + if (!isTextbook){ + if (!valid_token) { + messages.push('You have to provide an access token for the selected Canvas instance.'); + } + if ($('#lms-course-num').val() === '') { + messages.push('You have to write LMS course Id.'); + } + if ($('#lms-instance-select').val() === '') { + messages.push('One of the LMS instances has to be selected.'); + } } + // if ($('#lms-course-code').val() === '') { // messages.push('You have to write LMS course name.'); // } @@ -175,10 +184,43 @@ console.error(error); }); }; - + + handle_generate_textbook = function () { + var organization_id, course_id, term_id, label, inst_book_id, fd, messages, url; + messages = check_completeness(true); + if (messages.length !== 0) { + form_alert(messages); + $('#btn-submit-co').prop('disabled', false); + return; + } + organization_id = $('#organization-select').val(); + course_id = $('#course-select').val(); + term_id = $('#term-select').val(); + label = $('#label').val(); + inst_book_id = $('#inst-book-select').val(); + fd = new FormData; + fd.append('organization_id', organization_id); + fd.append('course_id', course_id); + fd.append('term_id', term_id); + fd.append('label', label); + fd.append('inst_book_id', inst_book_id); + url = '/textbooks' + return $.ajax({ + url: url, + type: 'post', + data: fd, + processData: false, + contentType: false, + success: function (data) { + return window.location.href = data['url']; + } + }); + }; + + handle_submit = function () { var lms_instance_id, lms_course_num, lms_course_code, organization_id, course_id, term_id, label, late_policy_id, inst_book_id, fd, messages, url; - messages = check_completeness(); + messages = check_completeness(false); if (messages.length !== 0) { form_alert(messages); $('#btn-submit-co').prop('disabled', false); diff --git a/app/controllers/course_offerings_controller.rb b/app/controllers/course_offerings_controller.rb index 0acfdb973..ddcbf05f5 100644 --- a/app/controllers/course_offerings_controller.rb +++ b/app/controllers/course_offerings_controller.rb @@ -308,6 +308,7 @@ def create else err_string = 'There was a problem while creating the course offering.' url = url_for new_course_offerings_path(notice: err_string) + end end diff --git a/app/controllers/textbooks_controller.rb b/app/controllers/textbooks_controller.rb new file mode 100644 index 000000000..0dfc48139 --- /dev/null +++ b/app/controllers/textbooks_controller.rb @@ -0,0 +1,56 @@ +class TextbooksController < ApplicationController + + def create + # Textbooks are CourseOffering with no LMS interaction + # Hence the LMS instance TEXTBOOK is used for all CourseOfferings + # that are TEXTBOOKS + lms_instance = LmsInstance.find_by(url: "TEXTBOOK") + course = Course.find_by(id: params[:course_id]) + term = Term.find_by(id: params[:term_id]) + inst_book = InstBook.find_by(id: params[:inst_book_id]) + + course_offering = CourseOffering.where( + "course_id=? and term_id=? and label=? and lms_instance_id=?", + params[:course_id], params[:term_id], params[:label], lms_instance.id + ).first + + if course_offering.blank? + course_offering = CourseOffering.new( + course: course, + term: term, + label: params[:label], + lms_instance: lms_instance, + lms_course_num: 9999999 + ) + + cloned_book = inst_book.get_clone(current_user) + + if course_offering.save! + cloned_book.course_offering_id = course_offering.id + cloned_book.save! + + enrollment = CourseEnrollment.new + enrollment.course_offering_id = course_offering.id + enrollment.user_id = current_user.id + enrollment.course_role_id = CourseRole.instructor.id + enrollment.save! + else + err_string = 'There was a problem while creating the course offering.' + url = url_for new_course_offerings_path(notice: err_string) + end + end + + if !url + url = url_for(organization_course_path( + course_offering.course.organization, + course_offering.course, + course_offering.term + )) + end + + respond_to do |format| + format.json { render json: {url: url} } + end + end + +end \ No newline at end of file diff --git a/app/jobs/compile_book_job.rb b/app/jobs/compile_book_job.rb index 416421733..43b202eab 100644 --- a/app/jobs/compile_book_job.rb +++ b/app/jobs/compile_book_job.rb @@ -28,7 +28,8 @@ def perform config_path = config_file_path[15..-1] # without the public/OpenDSA require 'net/http' uri = URI(ENV["config_api_link"]) - res = Net::HTTP.post_form(uri, 'config_file_path' => config_path, 'build_path' => build_path, 'rake' => false) + + res = Net::HTTP.post_form(uri, 'config_file_path' => config_path, 'build_path' => build_path, 'rake' => is_textbook(@inst_book)) unless res.kind_of? Net::HTTPSuccess Rails.logger.info(res['stderr_compressed']) end @@ -54,4 +55,17 @@ def book_path(inst_book) sanitize_filename(term.slug) + "/" + sanitize_filename(course_offering.label) end + + + def is_textbook(inst_book) + course_offering = CourseOffering.where(:id => inst_book.course_offering_id).first + + textbook_instance = LmsInstance.find_by(url: "TEXTBOOK") + if course_offering.lms_instance_id == textbook_instance.id + Rails.logger.info("Compiling Standalone Book") + end + + course_offering.lms_instance_id == textbook_instance.id + end + end diff --git a/app/models/inst_book.rb b/app/models/inst_book.rb index f990a3e43..e3ebb3412 100644 --- a/app/models/inst_book.rb +++ b/app/models/inst_book.rb @@ -141,12 +141,11 @@ def extract_av_data_from_rst # Check if rst_folder to prevent Dir.glob from failing return av_data unless Dir.exist?(rst_folder) - + Dir.glob("#{rst_folder}/**/*.rst").each do |rst_file_path| module_name = File.basename(rst_file_path, ".rst") av_data[module_name] = { avmetadata: {}, inlineav: [], avembed: [] } in_metadata_block = false - File.foreach(rst_file_path) do |line| if line.strip == '.. avmetadata::' in_metadata_block = true @@ -164,12 +163,32 @@ def extract_av_data_from_rst end end end - av_data - end + end + + def get_clone(currentUser) + return clone(currentUser) + end + private + + def extract_metadata_from_line(line) + key, value = line.strip.split(': ', 2) + [key[1..].to_sym, value] if key && value + end + + def extract_inlineav_name_from_line(line) + match = line.match(/\.\. inlineav:: (\w+)/) + match[1] if match # Returns the inlineav short name or nil if no match + end + + def extract_avembed_data_from_line(line) + match = line.match(/\.\. avembed:: Exercises\/\w+\/(\w+)\.html/) + match[1] if match # Returns the 3rd level word or nil if no match + end + + # -------------------------------------------------------------------------------- - # FIXME: shouldn't this method be removed? It appears to be out-dated? # FIXME: the real code is now in views/inst_books/show.json.builder def to_builder @@ -271,6 +290,7 @@ def clone(current_user) inst_chapters.each do |chapter| inst_chapter = chapter.clone(b) end + return b end diff --git a/app/views/course_offerings/_form.html.haml b/app/views/course_offerings/_form.html.haml index 07cf33253..122676352 100644 --- a/app/views/course_offerings/_form.html.haml +++ b/app/views/course_offerings/_form.html.haml @@ -8,7 +8,8 @@ %a{href: home_guide_path} instructor's guide for more information. - %h4.text-danger All fields are required. + %h4.text-danger All fields are required + %h4.text-danger Note: LMS fields are not required to create textbooks / .form-group / = label_tag :name, 'Canvas course Name', class: 'control-label col-xs-2' @@ -79,35 +80,43 @@ / %i.fa.fa-info-circle.action{ data: { toggle: 'modal', target: '#inst-book-help-modal' } } .form-group - = label_tag :lms_instance_select, 'Canvas Instance:', class: 'control-label col-xs-1' + = label_tag :lms_instance_select, 'LMS Instance:', class: 'control-label col-xs-1' .col-xs-3 = collection_select nil, nil, LmsInstance.all, :id, :url, { prompt: 'Select', selected: nil }, { id: 'lms-instance-select', class: 'form-control' } #lms-access-token-group.form-group - = label_tag :name, 'Canvas access token', class: 'control-label col-xs-2' + = label_tag :name, 'LMS access token', class: 'control-label col-xs-2' .col-lg-4.col-md-4.col-xs-4 = text_field_tag :lms_access_token, nil, id: 'lms-access-token', class: 'form-control', maxlength: 100, disabled: 'true' %small.col-xs-11.text.text-warning#lms-access-token-desc - Your access token allows the OpenDSA application to generate the book instance in your Canvas course on your behalf. First, you need to generate Canvas access token by following + Your access token allows the OpenDSA application to generate the book instance in your LMS course on your behalf. First, you need to generate LMS access token by following the instructions here. Second, go to the OpenDSA = link_to "LMS Accesses", admin_lms_accesses_path, :target => "_blank" page to add or update your access token. #lms-access-update-btn.col-xs-1 - = link_to admin_lms_accesses_path, title: "Update your access token for the selected Canvas instance", class: 'btn btn-default', :target => "_blank" do + = link_to admin_lms_accesses_path, title: "Update your access token for the selected LMS instance", class: 'btn btn-default', :target => "_blank" do %i.glyphicon.glyphicon-new-window #lms-access-token-check.col-xs-1.fa.fa-check .form-group - = label_tag :name, 'Canvas course Id', class: 'control-label col-xs-2' + = label_tag :name, 'LMS course Id', class: 'control-label col-xs-2' .col-lg-4.col-md-4.col-xs-4 = text_field_tag :lms_course_id, nil, id: 'lms-course-num', class: 'form-control', maxlength: 25 %small.col-xs-11.text.text-warning - Create a new course at the selected canvas instance and copy the course Id here (e.g. Course Id of https://canvas.instructure.com/courses/1076903 is 1076903). + Create a new course at the selected LMS instance and copy the course Id here (e.g. Course Id of https://canvas.instructure.com/courses/1076903 is 1076903). .form-group .col-xs-offset-2.col-xs-2 - %button#btn-submit-co.btn.btn-primary Submit + %button#btn-submit-co.btn.btn-primary Create New Course Offering + + .form-group + .col-xs-offset-2.col-xs-2 + - textbook_lms_instance = LmsInstance.find_by(url: "TEXTBOOK") + %button#btn-gen-textbook.btn.btn-primary{disabled:textbook_lms_instance.blank?} Create Non LMS Textbook + %small.col-xs-11.text.text-warning + = textbook_lms_instance.blank? ? 'Textbook LMS Instance not found' : '' + #lms-instance-help-modal.modal.fade{role: 'dialog', tabindex: '-1' } .modal-dialog.modal-md{ style: 'overflow-y: scroll; max-height:85% margin-top: 50px; margin-bottom:50px;' } diff --git a/app/views/course_offerings/show.html.haml b/app/views/course_offerings/show.html.haml index f92c81047..9d4c350cb 100644 --- a/app/views/course_offerings/show.html.haml +++ b/app/views/course_offerings/show.html.haml @@ -113,141 +113,141 @@ .addClass( "custom-combobox" ) .addClass( "custom-comb" ) .insertAfter( this.element ); - - this.element.hide(); - this._createAutocomplete(); - this._createShowAllButton(); - }, - - _createAutocomplete: function() { - var selected = this.element.children( ":selected" ), - value = selected.val() ? selected.text() : ""; - - this.input = $( "" ) - .appendTo( this.wrapper ) - .val( value ) - .attr( "title", "" ) - .addClass( "custom-combobox-input ui-widget ui-widget-content ui-state-default ui-corner-left" ) - .addClass( "custom-comb-input ui-widget ui-widget-content ui-state-default ui-corner-left" ) - .autocomplete({ - delay: 0, - minLength: 0, - source: $.proxy( this, "_source" ) - }) - .tooltip({ - classes: { - "ui-tooltip": "ui-state-highlight" - } - }); - - this._on( this.input, { - autocompleteselect: function( event, ui ) { - ui.item.option.selected = true; - this._trigger( "select", event, { - item: ui.item.option - }); - }, - - autocompletechange: "_removeIfInvalid" + + this.element.hide(); + this._createAutocomplete(); + this._createShowAllButton(); + }, + + _createAutocomplete: function() { + var selected = this.element.children( ":selected" ), + value = selected.val() ? selected.text() : ""; + + this.input = $( "" ) + .appendTo( this.wrapper ) + .val( value ) + .attr( "title", "" ) + .addClass( "custom-combobox-input ui-widget ui-widget-content ui-state-default ui-corner-left" ) + .addClass( "custom-comb-input ui-widget ui-widget-content ui-state-default ui-corner-left" ) + .autocomplete({ + delay: 0, + minLength: 0, + source: $.proxy( this, "_source" ) + }) + .tooltip({ + classes: { + "ui-tooltip": "ui-state-highlight" + } + }); + + this._on( this.input, { + autocompleteselect: function( event, ui ) { + ui.item.option.selected = true; + this._trigger( "select", event, { + item: ui.item.option }); }, - - _createShowAllButton: function() { - var input = this.input, - wasOpen = false; - - $( "" ) - .attr( "tabIndex", -1 ) - .attr( "title", "Show All Items" ) - .tooltip() - .appendTo( this.wrapper ) - .button({ - icons: { - primary: "ui-icon-triangle-1-s" - }, - text: false - }) - .removeClass( "ui-corner-all" ) - .addClass( "custom-combobox-toggle ui-corner-right" ) - .addClass( "custom-comb-toggle ui-corner-right" ) - .on( "mousedown", function() { - wasOpen = input.autocomplete( "widget" ).is( ":visible" ); - }) - .on( "click", function() { - input.trigger( "focus" ); - + + autocompletechange: "_removeIfInvalid" + }); + }, + + _createShowAllButton: function() { + var input = this.input, + wasOpen = false; + + $( "" ) + .attr( "tabIndex", -1 ) + .attr( "title", "Show All Items" ) + .tooltip() + .appendTo( this.wrapper ) + .button({ + icons: { + primary: "ui-icon-triangle-1-s" + }, + text: false + }) + .removeClass( "ui-corner-all" ) + .addClass( "custom-combobox-toggle ui-corner-right" ) + .addClass( "custom-comb-toggle ui-corner-right" ) + .on( "mousedown", function() { + wasOpen = input.autocomplete( "widget" ).is( ":visible" ); + }) + .on( "click", function() { + input.trigger( "focus" ); + // Close if already visible - if ( wasOpen ) { - return; - } - - // Pass empty string as value to search for, displaying all results - input.autocomplete( "search", "" ); - }); - }, - - _source: function( request, response ) { - var matcher = new RegExp( $.ui.autocomplete.escapeRegex(request.term), "i" ); - response( this.element.children( "option" ).map(function() { - var text = $( this ).text(); - if ( this.value && ( !request.term || matcher.test(text) ) ) - return { - label: text, - value: text, - option: this - }; - }) ); - }, - - _removeIfInvalid: function( event, ui ) { - - // Selected an item, nothing to do - if ( ui.item ) { + if ( wasOpen ) { return; } - + + // Pass empty string as value to search for, displaying all results + input.autocomplete( "search", "" ); + }); + }, + + _source: function( request, response ) { + var matcher = new RegExp( $.ui.autocomplete.escapeRegex(request.term), "i" ); + response( this.element.children( "option" ).map(function() { + var text = $( this ).text(); + if ( this.value && ( !request.term || matcher.test(text) ) ) + return { + label: text, + value: text, + option: this + }; + }) ); + }, + + _removeIfInvalid: function( event, ui ) { + + // Selected an item, nothing to do + if ( ui.item ) { + return; + } + // Search for a match (case-insensitive) - var value = this.input.val(), - valueLowerCase = value.toLowerCase(), - valid = false; - this.element.children( "option" ).each(function() { - if ( $( this ).text().toLowerCase() === valueLowerCase ) { - this.selected = valid = true; - return false; - } - }); - - // Found a match, nothing to do - if ( valid ) { - return; - } - - // Remove invalid value - this.input - .val( "" ) - .attr( "title", value + " didn't match any item" ) - .tooltip( "open" ); - this.element.val( "" ); - this._delay(function() { - this.input.tooltip( "close" ).attr( "title", "" ); - }, 2500 ); - this.input.autocomplete( "instance" ).term = ""; - }, - - _destroy: function() { - this.wrapper.remove(); - this.element.show(); + var value = this.input.val(), + valueLowerCase = value.toLowerCase(), + valid = false; + this.element.children( "option" ).each(function() { + if ( $( this ).text().toLowerCase() === valueLowerCase ) { + this.selected = valid = true; + return false; } }); - - $( "#combobox" ).combobox(); - $( "#toggle" ).on( "click", function() { - $( "#combobox" ).toggle(); - }); - $( "#comb" ).combobox(); - $( "#toggle" ).on( "click", function() { - $( "#comb" ).toggle(); - }); + + // Found a match, nothing to do + if ( valid ) { + return; + } + + // Remove invalid value + this.input + .val( "" ) + .attr( "title", value + " didn't match any item" ) + .tooltip( "open" ); + this.element.val( "" ); + this._delay(function() { + this.input.tooltip( "close" ).attr( "title", "" ); + }, 2500 ); + this.input.autocomplete( "instance" ).term = ""; + }, + + _destroy: function() { + this.wrapper.remove(); + this.element.show(); + } + }); + + $( "#combobox" ).combobox(); + $( "#toggle" ).on( "click", function() { + $( "#combobox" ).toggle(); + }); + $( "#comb" ).combobox(); + $( "#toggle" ).on( "click", function() { + $( "#comb" ).toggle(); + }); } ); %body .ui-widget @@ -269,9 +269,3 @@ %br/ #log - - - - - - diff --git a/app/views/courses/show.html.haml b/app/views/courses/show.html.haml index 48e35f3c8..770371980 100644 --- a/app/views/courses/show.html.haml +++ b/app/views/courses/show.html.haml @@ -48,14 +48,37 @@ %tr %th Book Title %th Canvas Course - %th - %th - %th + %th Textbook %tbody#odsa - - @inst_book = offering.inst_books - = render @inst_book - - + -# - @inst_book = offering.inst_books + -# = render @inst_book + %td= offering.display_name + %td + %td + - course = Course.find_by(id:offering.course_id) + - organization = Organization.find_by(id:course.organization_id) + - term = Term.find_by(id:offering.term_id) + = ENV["config_api_link"].gsub("/api/configure/","/Books/#{organization.slug}/#{course.slug}/#{term.slug}/#{offering.label}/html/index.html") + %td + - inst_book = InstBook.find_by(course_offering_id:offering.id) + - form_name = 'compile_book_' + inst_book.id.to_s + = form_tag '/inst_books/'+ inst_book.id.to_s , data: {type: "script"}, format: 'js', remote: true, onsubmit: "#{form_name}.disabled = true; #{form_name}.value = 'Please wait...'; Window.ODSA = Window.ODSA || {}; Window.ODSA.inst_book_id = #{inst_book.id}; Window.ODSA.action_type = 'compile_book'; return true;" do + = submit_tag "Compile Textbook", name: form_name, class: "btn btn-primary", title: 'Compile book confirmation.', data: {confirm: "The book compilation process will regenerate the book pages on the OpenDSA server. Assignments due dates and points in Canvas course won't be affected by this action. Do you want to continue?"} + .col-xs-1 + %i.fa.fa-info-circle.action{ data: { toggle: 'modal', target: '#generate-course-modal' } } + #generate-course-modal.modal.fade{role: 'dialog', tabindex: '-1' } + .modal-dialog.modal-md{ style: 'overflow-y: scroll; max-height:85% margin-top: 50px; margin-bottom:50px;' } + .modal-content + .modal-header + %p.lead Compile Textbook + .modal-body + %p + When you click this button the OpenDSA book will be generated as an independent resource. + This book will have no LMS ties such as Canvas integration. + -# If this is the first time the course is being generated, all OpenDSA chapters and modules + -# will be created in the course. If you are re-generating the Canvas course, this process + -# will update the assignments points and due dates as well as create new Canvas assignments + -# and modules for any new chapters and modules added to your course offering's OpenDSA book configuration. -else %h2 Offerings @@ -89,5 +112,4 @@ / user_path(i) }.to_sentence / %td / = render partial: 'course_offerings/self_enrollment', - / locals: { o: o } - + / locals: { o: o } \ No newline at end of file diff --git a/app/views/layouts/_navbar.html.haml b/app/views/layouts/_navbar.html.haml index e0954f5e3..21457a991 100644 --- a/app/views/layouts/_navbar.html.haml +++ b/app/views/layouts/_navbar.html.haml @@ -72,4 +72,4 @@ .yamm-content = render '/devise/sessions/new' -#%li= link_to 'Sign Up', new_user_registration_path - -#%li= link_to 'Sign In', new_user_session_path + -#%li= link_to 'Sign In', new_user_session_path \ No newline at end of file diff --git a/app/views/organizations/index.html.haml b/app/views/organizations/index.html.haml index 0f3e06de7..28c404602 100644 --- a/app/views/organizations/index.html.haml +++ b/app/views/organizations/index.html.haml @@ -28,7 +28,7 @@ %th Term %th Section - if user_signed_in? && current_user.global_role.is_admin? - %th Canvas course + %th LMS Course %th OpenDSA Book %th Instructor(s) / %th Enroll @@ -46,7 +46,10 @@ %td= link_to o.display_name, organization_course_path(o.course.organization,o.course, o.term) - if user_signed_in? && current_user.global_role.is_admin? - %td= link_to lms_course_code, course_offering_url, :target => "_blank" + -if lms_url != "TEXTBOOK" + %td= link_to lms_course_code, course_offering_url, :target => "_blank" + -else + %td LMS Course Not Available - if o.odsa_books.many? %td %table.table.table-striped diff --git a/config/routes.rb b/config/routes.rb index b74f26975..04bb953c7 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -109,6 +109,12 @@ get '/course_offerings/indAssigment/assignmentList/student/exercise' => 'course_offerings#ind_assigment', as: :ind_assigment + + #textbook routes + post '/textbooks' => 'textbooks#create', as: :create_textbooks + post '/textbooks/compile/:course_offering_id' => 'textbooks#compile', as: :compile_textbooks + + # All of the routes anchored at /gym scope :gym do # The top-level gym route diff --git a/lib/tasks/sample_data.rake b/lib/tasks/sample_data.rake index 67be51bf3..e2f738cbc 100644 --- a/lib/tasks/sample_data.rake +++ b/lib/tasks/sample_data.rake @@ -32,6 +32,13 @@ namespace :db do last_name: 'Researcher', email: 'example-01@railstutorial.org') + textbook_lms_type = FactoryBot.create(:lms_type, + name: "TEXTBOOK") + + FactoryBot.create(:lms_instance, + url: "TEXTBOOK", + lms_type_id: textbook_lms_type.id, + ) students = [] 50.times do |n|