diff --git a/Gemfile b/Gemfile index e77ae876..5c6bbd31 100644 --- a/Gemfile +++ b/Gemfile @@ -35,6 +35,7 @@ gem 'uglifier', '>= 1.3.0' gem 'will_paginate', '~> 3.0' gem 'tubesock' gem 'faye-websocket' +gem 'nokogiri' group :development do gem 'better_errors', platform: :ruby diff --git a/Gemfile.lock b/Gemfile.lock index 10284f6f..5feafdc7 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -362,6 +362,7 @@ DEPENDENCIES jquery-turbolinks kramdown newrelic_rpm + nokogiri nyan-cat-formatter pg pry diff --git a/app/assets/stylesheets/code_harbor_links.css.scss b/app/assets/stylesheets/code_harbor_links.css.scss new file mode 100644 index 00000000..5ca7b3ce --- /dev/null +++ b/app/assets/stylesheets/code_harbor_links.css.scss @@ -0,0 +1,3 @@ +// Place all the styles related to the CodeHarborLinks controller here. +// They will automatically be included in application.css. +// You can use Sass (SCSS) here: http://sass-lang.com/ diff --git a/app/assets/stylesheets/scaffolds.css.scss b/app/assets/stylesheets/scaffolds.css.scss new file mode 100644 index 00000000..6ec6a8ff --- /dev/null +++ b/app/assets/stylesheets/scaffolds.css.scss @@ -0,0 +1,69 @@ +body { + background-color: #fff; + color: #333; + font-family: verdana, arial, helvetica, sans-serif; + font-size: 13px; + line-height: 18px; +} + +p, ol, ul, td { + font-family: verdana, arial, helvetica, sans-serif; + font-size: 13px; + line-height: 18px; +} + +pre { + background-color: #eee; + padding: 10px; + font-size: 11px; +} + +a { + color: #000; + &:visited { + color: #666; + } + &:hover { + color: #fff; + background-color: #000; + } +} + +div { + &.field, &.actions { + margin-bottom: 10px; + } +} + +#notice { + color: green; +} + +.field_with_errors { + padding: 2px; + background-color: red; + display: table; +} + +#error_explanation { + width: 450px; + border: 2px solid red; + padding: 7px; + padding-bottom: 0; + margin-bottom: 20px; + background-color: #f0f0f0; + h2 { + text-align: left; + font-weight: bold; + padding: 5px 5px 5px 15px; + font-size: 12px; + margin: -7px; + margin-bottom: 0px; + background-color: #c00; + color: #fff; + } + ul li { + font-size: 12px; + list-style: square; + } +} diff --git a/app/controllers/code_harbor_links_controller.rb b/app/controllers/code_harbor_links_controller.rb new file mode 100644 index 00000000..a4736f26 --- /dev/null +++ b/app/controllers/code_harbor_links_controller.rb @@ -0,0 +1,68 @@ +class CodeHarborLinksController < ApplicationController + include CommonBehavior + before_action :set_code_harbor_link, only: [:show, :edit, :update, :destroy] + + def authorize! + authorize(@code_harbor_link || @code_harbor_links) + end + private :authorize! + + # GET /code_harbor_links + # GET /code_harbor_links.json + def index + @code_harbor_links = CodeHarborLink.where(user_id: current_user.id).paginate(page: params[:page]) + authorize! + end + + # GET /code_harbor_links/1 + # GET /code_harbor_links/1.json + def show + authorize! + end + + # GET /code_harbor_links/new + def new + @code_harbor_link = CodeHarborLink.new + authorize! + end + + # GET /code_harbor_links/1/edit + def edit + authorize! + end + + # POST /code_harbor_links + # POST /code_harbor_links.json + def create + @code_harbor_link = CodeHarborLink.new(code_harbor_link_params) + @code_harbor_link.user = current_user + authorize! + create_and_respond(object: @code_harbor_link) + end + + # PATCH/PUT /code_harbor_links/1 + # PATCH/PUT /code_harbor_links/1.json + def update + update_and_respond(object: @code_harbor_link, params: code_harbor_link_params) + authorize! + end + + # DELETE /code_harbor_links/1 + # DELETE /code_harbor_links/1.json + def destroy + destroy_and_respond(object: @code_harbor_link) + end + + private + # Use callbacks to share common setup or constraints between actions. + def set_code_harbor_link + @code_harbor_link = CodeHarborLink.find(params[:id]) + @code_harbor_link.user = current_user + authorize! + end + + # Never trust parameters from the scary internet, only allow the white list through. + def code_harbor_link_params + params.require(:code_harbor_link).permit(:oauth2token) + end +end diff --git a/app/controllers/exercises_controller.rb b/app/controllers/exercises_controller.rb index cfe4d155..69d1f46f 100644 --- a/app/controllers/exercises_controller.rb +++ b/app/controllers/exercises_controller.rb @@ -11,6 +11,10 @@ class ExercisesController < ApplicationController before_action :set_file_types, only: [:create, :edit, :new, :update] before_action :set_teams, only: [:create, :edit, :new, :update] + skip_before_filter :verify_authenticity_token, only: [:import_proforma_xml] + skip_after_action :verify_authorized, only: [:import_proforma_xml] + skip_after_action :verify_policy_scoped, only: [:import_proforma_xml] + def authorize! authorize(@exercise || @exercises) end @@ -62,6 +66,58 @@ class ExercisesController < ApplicationController def edit end + def import_proforma_xml + begin + user = user_for_oauth2_request() + exercise = Exercise.new + request_body = request.body.read + exercise.from_proforma_xml(request_body) + exercise.user = user + saved = exercise.save + if saved + render :text => 'SUCCESS', :status => 200 + else + logger.info(exercise.errors.full_messages) + render :text => 'Invalid exercise', :status => 400 + end + rescue => error + if error.class == Hash + render :text => error.message, :status => error.status + else + raise error + render :text => '', :status => 500 + end + end + end + + def user_for_oauth2_request + authorizationHeader = request.headers['Authorization'] + if authorizationHeader == nil + raise ({status: 401, message: 'No Authorization header'}) + end + + oauth2Token = authorizationHeader.split(' ')[1] + if oauth2Token == nil || oauth2Token.size == 0 + raise ({status: 401, message: 'No token in Authorization header'}) + end + + user = user_by_code_harbor_token(oauth2Token) + if user == nil + raise ({status: 401, message: 'Unknown OAuth2 token'}) + end + + return user + end + private :user_for_oauth2_request + + def user_by_code_harbor_token(oauth2Token) + link = CodeHarborLink.where(:oauth2token => oauth2Token)[0] + if link != nil + return link.user + end + end + private :user_by_code_harbor_token + def exercise_params params[:exercise].permit(:description, :execution_environment_id, :file_id, :instructions, :public, :hide_file_tree, :team_id, :title, files_attributes: file_attributes).merge(user_id: current_user.id, user_type: current_user.class.name) end diff --git a/app/helpers/code_harbor_links_helper.rb b/app/helpers/code_harbor_links_helper.rb new file mode 100644 index 00000000..d8e92ddf --- /dev/null +++ b/app/helpers/code_harbor_links_helper.rb @@ -0,0 +1,2 @@ +module CodeHarborLinksHelper +end diff --git a/app/models/code_harbor_link.rb b/app/models/code_harbor_link.rb new file mode 100644 index 00000000..2e219aa9 --- /dev/null +++ b/app/models/code_harbor_link.rb @@ -0,0 +1,13 @@ +class CodeHarborLink < ActiveRecord::Base + validates :oauth2token, presence: true + validates :user_id, presence: true + + belongs_to :internal_user, foreign_key: :user_id + alias_method :user, :internal_user + alias_method :user=, :internal_user= + + def to_s + oauth2token + end + +end diff --git a/app/models/exercise.rb b/app/models/exercise.rb index f86a084c..4a1e0486 100644 --- a/app/models/exercise.rb +++ b/app/models/exercise.rb @@ -1,3 +1,4 @@ +require 'nokogiri' require File.expand_path('../../../lib/active_model/validations/boolean_presence_validator', __FILE__) class Exercise < ActiveRecord::Base @@ -109,6 +110,54 @@ class Exercise < ActiveRecord::Base exercise end + def determine_file_role_from_proforma_file(task_node, file_node) + file_id = file_node.xpath('@id') + file_class = file_node.xpath('@class').first.value + comment = file_node.xpath('@comment').first.value + is_referenced_by_test = task_node.xpath("p:tests/p:test/p:filerefs/p:fileref[@id=#{file_id}]") + is_referenced_by_model_solution = task_node.xpath("p:model-solutions/p:model-solution/p:filerefs/p:fileref[@id=#{file_id}]") + if is_referenced_by_test && (file_class == 'internal') + return 'teacher_defined_test' + elsif is_referenced_by_model_solution && (file_class == 'internal') + return 'reference_implementation' + elsif (file_class == 'template') && (comment == 'main') + return 'main_file' + elsif (file_class == 'internal') && (comment == 'main') + end + return 'regular_file' + end + + def from_proforma_xml(xml_string) + # how to extract the proforma functionality into a different module in rails? + xml = Nokogiri::XML(xml_string) + xml.collect_namespaces + task_node = xml.xpath('/root/p:task') + description = task_node.xpath('p:description/text()')[0].content + self.attributes = { + title: task_node.xpath('p:meta-data/p:title/text()')[0].content, + description: description, + instructions: description + } + task_node.xpath('p:files/p:file').all? { |file| + file_name_split = file.xpath('@filename').first.value.split('.') + file_class = file.xpath('@class').first.value + role = determine_file_role_from_proforma_file(task_node, file) + feedback_message_nodes = task_node.xpath("p:tests/p:test/p:test-configuration/c:feedback-message/text()") + files.build({ + name: file_name_split.first, + content: file.xpath('text()').first.content, + read_only: false, + hidden: file_class == 'internal', + role: role, + feedback_message: (role == 'teacher_defined_test') ? feedback_message_nodes.first.content : nil, + file_type: FileType.where( + file_extension: ".#{file_name_split.second}" + ).take + }) + } + self.execution_environment_id = 1 + end + def generate_token self.token ||= SecureRandom.hex(4) end @@ -133,4 +182,5 @@ class Exercise < ActiveRecord::Base end end private :valid_main_file? + end diff --git a/app/policies/code_harbor_link_policy.rb b/app/policies/code_harbor_link_policy.rb new file mode 100644 index 00000000..8726c22a --- /dev/null +++ b/app/policies/code_harbor_link_policy.rb @@ -0,0 +1,3 @@ +class CodeHarborLinkPolicy < AdminOnlyPolicy + +end diff --git a/app/views/application/_navigation.html.slim b/app/views/application/_navigation.html.slim index 276723bf..4ab39e30 100644 --- a/app/views/application/_navigation.html.slim +++ b/app/views/application/_navigation.html.slim @@ -8,7 +8,7 @@ - if current_user.admin? li = link_to(t('breadcrumbs.dashboard.show'), admin_dashboard_path) li.divider - - models = [ExecutionEnvironment, Exercise, Consumer, ExternalUser, FileType, InternalUser, Submission, Team].sort_by { |model| model.model_name.human(count: 2) } + - models = [ExecutionEnvironment, Exercise, Consumer, CodeHarborLink, ExternalUser, FileType, InternalUser, Submission, Team].sort_by { |model| model.model_name.human(count: 2) } - models.each do |model| - if policy(model).index? li = link_to(model.model_name.human(count: 2), send(:"#{model.model_name.collection}_path")) diff --git a/app/views/code_harbor_links/_form.html.slim b/app/views/code_harbor_links/_form.html.slim new file mode 100644 index 00000000..f7b449ee --- /dev/null +++ b/app/views/code_harbor_links/_form.html.slim @@ -0,0 +1,6 @@ += form_for(@code_harbor_link) do |f| + = render('shared/form_errors', object: @code_harbor_link) + .form-group + = f.label(:oauth2token) + = f.text_field(:oauth2token, class: 'form-control', required: true) + .actions = render('shared/submit_button', f: f, object: @code_harbor_link) diff --git a/app/views/code_harbor_links/edit.html.slim b/app/views/code_harbor_links/edit.html.slim new file mode 100644 index 00000000..d1c7ea8f --- /dev/null +++ b/app/views/code_harbor_links/edit.html.slim @@ -0,0 +1,3 @@ +h1 = @code_harbor_link + += render('form') diff --git a/app/views/code_harbor_links/index.html.slim b/app/views/code_harbor_links/index.html.slim new file mode 100644 index 00000000..953985c4 --- /dev/null +++ b/app/views/code_harbor_links/index.html.slim @@ -0,0 +1,18 @@ +h1 = CodeHarborLink.model_name.human(count: 2) + +.table-responsive + table.table + thead + tr + th = t('activerecord.attributes.code_harbor_link.oauth2token') + th colspan=3 = t('shared.actions') + tbody + - @code_harbor_links.each do |code_harbor_link| + tr + td = code_harbor_link.oauth2token + td = link_to(t('shared.show'), code_harbor_link) + td = link_to(t('shared.edit'), edit_code_harbor_link_path(code_harbor_link)) + td = link_to(t('shared.destroy'), code_harbor_link, data: {confirm: t('shared.confirm_destroy')}, method: :delete) + += render('shared/pagination', collection: @code_harbor_links) +p = render('shared/new_button', model: CodeHarborLink) diff --git a/app/views/code_harbor_links/new.html.slim b/app/views/code_harbor_links/new.html.slim new file mode 100644 index 00000000..ef19a3e6 --- /dev/null +++ b/app/views/code_harbor_links/new.html.slim @@ -0,0 +1,3 @@ +h1 = t('shared.new_model', model: CodeHarborLink.model_name.human) + += render('form') diff --git a/app/views/code_harbor_links/show.html.slim b/app/views/code_harbor_links/show.html.slim new file mode 100644 index 00000000..b2d95342 --- /dev/null +++ b/app/views/code_harbor_links/show.html.slim @@ -0,0 +1,7 @@ +h1 + = @code_harbor_link + = render('shared/edit_button', object: @code_harbor_link) if policy(@code_harbor_link).edit? + +- %w[oauth2token].each do |attribute| + = row(label: "code_harbor_link.#{attribute}") do + = content_tag(:input, nil, class: 'form-control', readonly: true, value: @code_harbor_link.send(attribute)) diff --git a/config/locales/de.yml b/config/locales/de.yml index 9fd2dce0..01e1bf7c 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -1,6 +1,8 @@ de: activerecord: attributes: + code_harbor_link: + oauth2token: OAuth2 Token consumer: name: Name oauth_key: OAuth Key @@ -90,6 +92,9 @@ de: internal_user_ids: Mitglieder name: Name models: + code_harbor_link: + one: CodeHarbor-Link + other: CodeHarbor-Links consumer: one: Konsument other: Konsumenten diff --git a/config/locales/en.yml b/config/locales/en.yml index f005eea2..56fd4fc8 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1,6 +1,8 @@ en: activerecord: attributes: + code_harbor_link: + oauth2token: OAuth2 Token consumer: name: Name oauth_key: OAuth Key @@ -90,6 +92,9 @@ en: internal_user_ids: Members name: Name models: + code_harbor_link: + one: CodeHarbor Link + other: CodeHarbor Links consumer: one: Consumer other: Consumers diff --git a/config/routes.rb b/config/routes.rb index 24f6a0d9..72ccb489 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,6 +1,7 @@ FILENAME_REGEXP = /[\w\.]+/ unless Kernel.const_defined?(:FILENAME_REGEXP) Rails.application.routes.draw do + resources :code_harbor_links resources :request_for_comments get '/my_request_for_comments', as: 'my_request_for_comments', to: 'request_for_comments#get_my_comment_requests' resources :comments, except: [:destroy] do @@ -40,6 +41,8 @@ Rails.application.routes.draw do resources :hints end + post '/import_proforma_xml' => 'exercises#import_proforma_xml' + resources :exercises do collection do match '', to: 'exercises#batch_update', via: [:patch, :put] diff --git a/db/migrate/20160204094409_create_code_harbor_links.rb b/db/migrate/20160204094409_create_code_harbor_links.rb new file mode 100644 index 00000000..b87d5c9d --- /dev/null +++ b/db/migrate/20160204094409_create_code_harbor_links.rb @@ -0,0 +1,9 @@ +class CreateCodeHarborLinks < ActiveRecord::Migration + def change + create_table :code_harbor_links do |t| + t.string :oauth2token + + t.timestamps + end + end +end diff --git a/db/migrate/20160204111716_add_user_to_code_harbor_link.rb b/db/migrate/20160204111716_add_user_to_code_harbor_link.rb new file mode 100644 index 00000000..8fa36ed1 --- /dev/null +++ b/db/migrate/20160204111716_add_user_to_code_harbor_link.rb @@ -0,0 +1,5 @@ +class AddUserToCodeHarborLink < ActiveRecord::Migration + def change + add_reference :code_harbor_links, :user, index: true, foreign_key: true + end +end diff --git a/db/schema.rb b/db/schema.rb index d895da3f..793c021a 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,11 +11,20 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20150922125415) do +ActiveRecord::Schema.define(version: 20160204111716) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" + create_table "code_harbor_links", force: true do |t| + t.string "oauth2token" + t.datetime "created_at" + t.datetime "updated_at" + t.integer "user_id" + end + + add_index "code_harbor_links", ["user_id"], name: "index_code_harbor_links_on_user_id", using: :btree + create_table "comments", force: true do |t| t.integer "user_id" t.integer "file_id"