<%= yield %>
diff --git a/db/migrate/20170702012742_ensure_indexes_in_place.rb b/db/migrate/20170702012742_ensure_indexes_in_place.rb
index 6bd9410..367ce5f 100644
--- a/db/migrate/20170702012742_ensure_indexes_in_place.rb
+++ b/db/migrate/20170702012742_ensure_indexes_in_place.rb
@@ -1,8 +1,12 @@
class EnsureIndexesInPlace < ActiveRecord::Migration
def change
#users_perms
+ remove_foreign_key :users_perms, :perms
+ remove_foreign_key :users_perms, :users
remove_index :users_perms, name: 'index_users_perms_on_user_id_and_perm_id'
add_index :users_perms, :user_id
+ add_foreign_key :users_perms, :perms
+ add_foreign_key :users_perms, :users
#user_identifiers
add_index :user_identifiers, :user_id
#roles
@@ -27,14 +31,22 @@
#annotations
add_index :annotations, :question_id
#question_themes
+ remove_foreign_key :questions_themes, :questions
+ remove_foreign_key :questions_themes, :themes
remove_index :questions_themes, name: 'question_theme_index'
remove_index :questions_themes, name: 'theme_question_index'
add_index :questions_themes, :question_id
+ add_foreign_key :questions_themes, :questions
+ add_foreign_key :questions_themes, :themes
#question_options
add_index :question_options, :question_id
#answers_question_options
+ remove_foreign_key :answers_question_options, :answers
+ remove_foreign_key :answers_question_options, :question_options
remove_index :answers_question_options, name: 'answer_question_option_index'
remove_index :answers_question_options, name: 'question_option_answer_index'
add_index :answers_question_options, :answer_id
+ add_foreign_key :answers_question_options, :answers
+ add_foreign_key :answers_question_options, :question_options
end
end
diff --git a/db/seeds.rb b/db/seeds.rb
index 6c4aded..fabb9a8 100644
--- a/db/seeds.rb
+++ b/db/seeds.rb
@@ -8,7 +8,7 @@
{name: 'orcid', description: 'ORCID', active: true,
logo_url:'http://orcid.org/sites/default/files/images/orcid_16x16.png',
user_landing_url:'https://orcid.org' },
- {name: 'shibboleth', description: 'your institutional credentials', active: true,
+ {name: 'shibboleth', description: 'Your institutional credentials', active: true,
},
]
identifier_schemes.map{ |is| IdentifierScheme.create!(is) if IdentifierScheme.find_by(name: is[:name]).nil? }
@@ -16,13 +16,13 @@
# Question Formats
# -------------------------------------------------------
question_formats = [
- {title: "Text area", option_based: false},
- {title: "Text field", option_based: false},
- {title: "Radio buttons", option_based: true},
- {title: "Check box", option_based: true},
- {title: "Dropdown", option_based: true},
- {title: "Multi select box", option_based: true},
- {title: "Date", option_based: false}
+ {title: "Text area", option_based: false, formattype: 0},
+ {title: "Text field", option_based: false, formattype: 1},
+ {title: "Radio buttons", option_based: true, formattype: 2},
+ {title: "Check box", option_based: true, formattype: 3},
+ {title: "Dropdown", option_based: true, formattype: 4},
+ {title: "Multi select box", option_based: true, formattype: 5},
+ {title: "Date", option_based: true, formattype: 6}
]
question_formats.map{ |qf| QuestionFormat.create!(qf) if QuestionFormat.find_by(title: qf[:title]).nil? }
@@ -154,7 +154,9 @@
# -------------------------------------------------------
token_permission_types = [
{token_type: 'guidances', text_description: 'allows a user access to the guidances api endpoint'},
- {token_type: 'plans', text_description: 'allows a user access to the plans api endpoint'}
+ {token_type: 'plans', text_description: 'allows a user access to the plans api endpoint'},
+ {token_type: 'templates', text_description: 'allows a user access to the templates api endpoint'},
+ {token_type: 'statistics', text_description: 'allows a user access to the statistics api endpoint'}
]
token_permission_types.map{ |tpt| TokenPermissionType.create!(tpt) if TokenPermissionType.find_by(token_type: tpt[:token_type]).nil? }
diff --git a/lib/assets/.eslintrc.json b/lib/assets/.eslintrc.json
index 95727a0..b2243c8 100644
--- a/lib/assets/.eslintrc.json
+++ b/lib/assets/.eslintrc.json
@@ -1,7 +1,7 @@
{
"extends": "airbnb-base",
"env": {
- "jquery": true,
- "jasmine": true
+ "jasmine": true,
+ "jquery": true
}
}
diff --git a/lib/assets/javascripts/application.js b/lib/assets/javascripts/application.js
index 401088c..9328f23 100644
--- a/lib/assets/javascripts/application.js
+++ b/lib/assets/javascripts/application.js
@@ -2,4 +2,5 @@
import './views/devise/invitations/edit';
import './views/devise/passwords/edit';
import './views/devise/passwords/new';
+import './views/plans/edit_details';
import './views/plans/share';
diff --git a/lib/assets/javascripts/utils/tinymce.js b/lib/assets/javascripts/utils/tinymce.js
new file mode 100644
index 0000000..d0b7e37
--- /dev/null
+++ b/lib/assets/javascripts/utils/tinymce.js
@@ -0,0 +1,108 @@
+// Import TinyMCE
+import tinymce from 'tinymce/tinymce';
+// Import TinyMCE theme
+import 'tinymce/themes/modern/theme';
+// Plugins
+import 'tinymce/plugins/table';
+import 'tinymce/plugins/autoresize';
+import 'tinymce/plugins/link';
+import 'tinymce/plugins/paste';
+import 'tinymce/plugins/advlist';
+// Other dependencies
+import { isObject, isString } from './isType';
+
+// Configuration extracted from https://www.tinymce.com/docs/advanced/usage-with-module-loaders/
+require.context(
+ 'file-loader?name=./javascripts/[path][name].[ext]&context=node_modules/tinymce!tinymce/skins',
+ true,
+ /.*/,
+);
+
+export const defaultOptions = {
+ selector: '.tinymce',
+ statusbar: false,
+ menubar: false,
+ toolbar: 'bold italic | bullist numlist | link | table',
+ plugins: 'table autoresize link paste advlist',
+ advlist_bullet_styles: 'circle,disc,square', // Only disc bullets display on htmltoword
+ target_list: false,
+ autoresize_min_height: 130,
+ autoresize_bottom_margin: 10,
+ extended_valid_elements: 'iframe[tooltip] , a[href|target=_blank]',
+ paste_auto_cleanup_on_paste: true,
+ paste_remove_styles: true,
+ paste_retain_style_properties: 'none',
+ paste_convert_middot_lists: true,
+ paste_remove_styles_if_webkit: true,
+ paste_remove_spans: true,
+ paste_strip_class_attributes: 'all',
+ table_default_attributes: {
+ border: 1,
+ },
+};
+
+export const Tinymce = {
+ /*
+ Initialises a tinymce editor given the object passed. If a non-valid object is passed,
+ the defaultOptions object is used instead
+ @param options - An object with tinyMCE properties
+ */
+ init(options = {}) {
+ if (isObject(options)) {
+ tinymce.init($.extend(true, defaultOptions, options));
+ } else {
+ tinymce.init(defaultOptions);
+ }
+ },
+ /*
+ Finds any tinyMCE editor whose target element/textarea has the className passed
+ @param className - A string representing the class name of the tinyMCE editor
+ target element/textarea to look for
+ @return An Array of tinymce.Editor objects
+ */
+ findEditorsByClassName(className) {
+ if (isString(className)) {
+ return tinymce.editors.reduce((acc, e) => {
+ if ($(e.getElement()).hasClass(className)) {
+ return acc.concat([e]);
+ }
+ return acc;
+ }, []);
+ }
+ return [];
+ },
+ /*
+ Finds a tinyMCE editor whose target element/textarea has the id passed
+ @param id - A string representing the id of the tinyMCE editor target
+ element/textarea to look for
+ @return tinymce.Editor object, otherwise undefined
+ */
+ findEditorById(id) {
+ if (isString(id)) {
+ return tinymce.editors.find(el => el.id === id);
+ }
+ return undefined;
+ },
+ /*
+ Destroy every editor instance whose target element/textarea has the className passed. This
+ method executes for each editor the method defined at tinymce.Editor.destroy (e.g. https://www.tinymce.com/docs/api/tinymce/tinymce.editor/#destroy).
+ @param className - A string representing the class name of the tinyMCE editor
+ target element/textarea to look for
+ @return undefined
+ */
+ destroyEditorsByClassName(className) {
+ const editors = this.findEditorsByClassName(className);
+ editors.forEach(ed => ed.destroy(false));
+ },
+ /*
+ Destroy an editor instance whose target element/textarea has HTML id passed. This method
+ executes tinymce.Editor.destroy (e.g. https://www.tinymce.com/docs/api/tinymce/tinymce.editor/#destroy) for a successfull id found.
+ @return undefined
+ */
+ destroyEditorById(id) {
+ const editor = this.findEditorById(id);
+ if (editor) {
+ editor.destroy(false);
+ }
+ },
+};
diff --git a/lib/assets/javascripts/utils/tinymceSpec.js b/lib/assets/javascripts/utils/tinymceSpec.js
new file mode 100644
index 0000000..2c6439f
--- /dev/null
+++ b/lib/assets/javascripts/utils/tinymceSpec.js
@@ -0,0 +1,50 @@
+import { Tinymce } from './tinymce';
+
+beforeEach(() => {
+ $('body').append('');
+ $('body').append('');
+ Tinymce.init({ selector: '.test' });
+});
+
+describe('findEditorsByClassName test suite', () => {
+ it('expect two editors with class name test', () => expect(Tinymce.findEditorsByClassName('test').length).toBe(2));
+ it('expect zero editors with class name whatever', () => expect(Tinymce.findEditorsByClassName('whatever').length).toBe(0));
+});
+
+describe('findEditorById test suite', () => {
+ it('expect editor with id test1 to be defined', () => expect(Tinymce.findEditorById('test1')).toBeDefined());
+ it('expect editor with id test2 to be defined', () => expect(Tinymce.findEditorById('test2')).toBeDefined());
+ it('expect editor with id whatever to be undefined', () => expect(Tinymce.findEditorById('whatever')).not.toBeDefined());
+});
+
+describe('destroyEditorsByClassName test suite', () => {
+ it('expect remaining two editors', () => {
+ Tinymce.destroyEditorsByClassName('whatever');
+ expect(Tinymce.findEditorsByClassName('test').length).toBe(2);
+ });
+ it('expect remaining zero editors', () => {
+ Tinymce.destroyEditorsByClassName('test');
+ expect(Tinymce.findEditorsByClassName('test').length).toBe(0);
+ });
+});
+
+describe('destroyEditorsById test suite', () => {
+ it('expect remaining two editors', () => {
+ Tinymce.destroyEditorById('test3');
+ expect(Tinymce.findEditorsByClassName('test').length).toBe(2);
+ });
+ it('expect remaining one editor', () => {
+ Tinymce.destroyEditorById('test1');
+ expect(Tinymce.findEditorsByClassName('test').length).toBe(1);
+ });
+ it('expect remaining zero editors', () => {
+ Tinymce.destroyEditorById('test1');
+ Tinymce.destroyEditorById('test2');
+ expect(Tinymce.findEditorsByClassName('test').length).toBe(0);
+ });
+});
+
+afterEach(() => {
+ $('body').html('');
+});
+
diff --git a/lib/assets/javascripts/views/plans/edit_details.js b/lib/assets/javascripts/views/plans/edit_details.js
new file mode 100644
index 0000000..85514c0
--- /dev/null
+++ b/lib/assets/javascripts/views/plans/edit_details.js
@@ -0,0 +1,14 @@
+import 'bootstrap-sass/assets/javascripts/bootstrap/tooltip';
+import 'bootstrap-sass/assets/javascripts/bootstrap/popover';
+import { Tinymce } from '../../utils/tinymce';
+import ariatiseForm from '../../utils/ariatiseForm';
+
+$(() => {
+ $('[data-toggle="tooltip"]').tooltip();
+ $('[data-toggle="popover"]').popover();
+ Tinymce.init();
+ $('#is_test').click(() => {
+ $('#plan_visibility').val($(this).is(':checked') ? 'is_test' : 'privately_visible');
+ });
+ ariatiseForm({ selector: '.edit_plan' });
+});
diff --git a/lib/assets/karma.conf.js b/lib/assets/karma.conf.js
index f94733c..a48ab6e 100644
--- a/lib/assets/karma.conf.js
+++ b/lib/assets/karma.conf.js
@@ -11,7 +11,6 @@
// available frameworks: https://npmjs.org/browse/keyword/karma-adapter
frameworks: ['fixture', 'jquery-3.2.1', 'jasmine-jquery', 'jasmine'],
-
// list of files / patterns to load in the browser
files: [
'./node_modules/phantomjs-polyfill/bind-polyfill.js',
diff --git a/lib/assets/webpack.config.js b/lib/assets/webpack.config.js
index 0011906..71f1ec7 100644
--- a/lib/assets/webpack.config.js
+++ b/lib/assets/webpack.config.js
@@ -15,7 +15,7 @@
context: __dirname,
entry: {
- vendor: ['jquery', 'tinymce/tinymce', 'tinymce/themes/modern/theme'],
+ vendor: ['jquery'],
application: ['./javascripts/application.js', './stylesheets/application.scss'],
},
@@ -47,7 +47,7 @@
}),
},
{
- test: /\.woff2?$|\.ttf$|\.eot$|\.svg$/,
+ test: /(?:fonts\/bootstrap\/.*)|(?:font-awesome\/fonts\/.*)(?:\.woff2?$|\.ttf$|\.eot$|\.svg$)/,
use: [
{
loader: 'file-loader',
@@ -64,14 +64,14 @@
plugins: [
extractSass,
+ new webpack.ProvidePlugin({ // Load jquery module automatically instead of import everywhere
+ jQuery: 'jquery',
+ $: 'jquery',
+ }),
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
minChunks: Infinity,
}),
- new webpack.ProvidePlugin({ // Load jquery module automatically instead of import everywhere
- $: 'jquery',
- jQuery: 'jquery',
- }),
new CopyWebPackPlugin([ // Copies every file under images or videos
{ from: './images/**/*', to: `${destPath}/` },
{ from: './videos/**/*', to: `${destPath}/` },
diff --git a/lib/tasks/bugfix.rake b/lib/tasks/bugfix.rake
new file mode 100644
index 0000000..5fc6bf1
--- /dev/null
+++ b/lib/tasks/bugfix.rake
@@ -0,0 +1,49 @@
+namespace :bugfix do
+
+ desc "Bug fixes for version v0.3.3"
+ task v0_3_3: :environment do
+ Rake::Task['bugfix:fix_question_formats'].execute
+ Rake::Task['bugfix:add_missing_token_permission_types'].execute
+ end
+
+ desc "Add the missing formattype to the question_formats table"
+ task fix_question_formats: :environment do
+ QuestionFormat.all.each do |qf|
+ case qf.title.downcase
+ when 'text area'
+ qf.formattype = :textarea
+ when 'text field'
+ qf.formattype = :textfield
+ when 'radio buttons'
+ qf.formattype = :radiobuttons
+ when 'check box'
+ qf.formattype = :checkbox
+ when 'dropdown'
+ qf.formattype = :dropdown
+ when 'multi select box'
+ qf.formattype = :multiselectbox
+ when 'date'
+ qf.formattype = :date
+ end
+
+ qf.save!
+ end
+
+ if QuestionFormat.find_by(formattype: :date).nil?
+ QuestionFormat.create!({title: "Date", option_based: true, formattype: 6})
+ end
+ end
+
+ desc "Add the missing token_permission_types"
+ task add_missing_token_permission_types: :environment do
+ if TokenPermissionType.find_by(token_type: 'templates').nil?
+ TokenPermissionType.create!({token_type: 'templates',
+ text_description: 'allows a user access to the templates api endpoint'})
+ end
+ if TokenPermissionType.find_by(token_type: 'statistics').nil?
+ TokenPermissionType.create!({token_type: 'statistics',
+ text_description: 'allows a user access to the statistics api endpoint'})
+ end
+ end
+
+end
\ No newline at end of file
diff --git a/test/functional/roles_controller_test.rb b/test/functional/roles_controller_test.rb
index 6a61b79..7d2f9f0 100644
--- a/test/functional/roles_controller_test.rb
+++ b/test/functional/roles_controller_test.rb
@@ -7,10 +7,10 @@
setup do
scaffold_plan
scaffold_org_admin(@plan.template.org)
-
+
# This should NOT be unnecessary! Owner should have full access
@plan.roles << Role.create(user: @user, plan: @plan, access: 15)
-
+
end
# TODO: Cleanup routes for this one. The controller currently only responds to create, update, destroy
@@ -21,19 +21,19 @@
# role PATCH /roles/:id roles#update
# PUT /roles/:id roles#update
# DELETE /roles/:id roles#destroy
-
+
# POST /roles (roles_path)
# ----------------------------------------------------------
test "create a new role" do
params = {plan_id: @plan.id, access_level: 4}
-
+
# Should redirect user to the root path if they are not logged in!
post roles_path, {role: params}
assert_unauthorized_redirect_to_root_path
sign_in @user
-
+
# Known user
@invitee = User.where.not(id: [@plan.owner.id, @user.id]).first
post roles_path, {user: @invitee.email, role: params}
@@ -50,15 +50,15 @@
assert_redirected_to share_plan_path(@plan)
assert_equal @invitee.id, Role.last.user_id, "expected no record to have been created!"
assert assigns(:role)
-
+
# Unknown user
post roles_path, {user: 'unknown_user@org.org', role: params}
- assert_equal _('Invitation to unknown_user@org.org issued successfully.'), flash[:notice]
+ assert_equal _('Invitation to unknown_user@org.org issued successfully. \nPlan shared with unknown_user@org.org.'), flash[:notice]
assert_response :redirect
assert_redirected_to share_plan_path(@plan)
assert_equal User.find_by(email:'unknown_user@org.org').id, Role.last.user_id, "expected the record to have been created!"
assert assigns(:role)
-
+
# Invite owner
@invitee = User.find_by(id: @plan.owner.id)
post roles_path, {user: @invitee.email, role: params}
@@ -67,32 +67,32 @@
assert_redirected_to share_plan_path(@plan)
assert_not_equal @invitee.id, Role.last.user_id, "expected no record to have been created!"
assert assigns(:role)
-
+
# Missing email
post roles_path, {role: {plan_id: @plan.id, access_level: 4}}
assert_equal _('Please enter an email address'), flash[:notice]
assert_response :redirect
assert_redirected_to share_plan_path(@plan)
assert assigns(:role)
- end
-
+ end
+
# PUT /role/:id (role_path)
# ----------------------------------------------------------
test "update the role" do
@invitee = User.last
role = Role.create(user: @invitee, plan: @plan, access: 1)
params = {access_level: 2}
-
+
# Should redirect user to the root path if they are not logged in!
put role_path(role), {role: params}
assert_unauthorized_redirect_to_root_path
-
+
sign_in @user
# Valid save
put role_path(role, format: :json), {role: params}
assert_equal 13, role.reload.access, "expected the record to have been updated"
-
+
# TODO: Role should require a user, plan and an access level :/
# Invalid save
# put role_path(role), {role: {user: nil}}
@@ -101,26 +101,26 @@
# assert_redirected_to share_plan_path(@plan)
# assert assigns(:role)
end
-
+
# DELETE /role/:id (role_path)
# ----------------------------------------------------------
test "delete the section" do
@invitee = User.last
role = Role.create(user: @invitee, plan: @plan, access: 1)
-
+
# Should redirect user to the root path if they are not logged in!
delete role_path(role)
assert_unauthorized_redirect_to_root_path
-
+
sign_in @user
-
+
delete role_path(role)
assert_equal _('Access removed'), flash[:notice]
assert_response :redirect
assert_redirected_to share_plan_path(@plan)
- assert_raise ActiveRecord::RecordNotFound do
+ assert_raise ActiveRecord::RecordNotFound do
Role.find(role.id).nil?
end
end
-
+
end