Commit 37cab03d authored by Taico Aerts's avatar Taico Aerts
Browse files

Merge branch 'development' into 'master'

Project Forum Release v2.8.10 - 27-09-2022

See merge request !871
parents cf3e78f5 c06e5298
......@@ -15,6 +15,7 @@
//= require jquery
//= require jquery-ui
//= require jquery.slick
//= require jquery-ui-touch-punch/jquery.ui.touch-punch
//= require bootstrap-sprockets
//= require zebra-datepicker/core
//= require normal/datepicker_inputs
......
/*!
* jQuery UI Touch Punch 1.0.8 as modified by RWAP Software
* based on original touchpunch v0.2.3 which has not been updated since 2014
*
* Updates by RWAP Software to take account of various suggested changes on the original code issues
*
* Updates by EIP to include more suggested features
*
* Original: https://github.com/furf/jquery-ui-touch-punch
* Copyright 2011–2014, Dave Furfero
* Dual licensed under the MIT or GPL Version 2 licenses.
*
* Fork: https://github.com/RWAP/jquery-ui-touch-punch
* EIP Fork: https://gitlab.ewi.tudelft.nl/eip/projectforum/jquery-ui-touch-punch
*
* Depends:
* jquery.ui.widget.js
* jquery.ui.mouse.js
*/
(function( factory ) {
if ( typeof define === "function" && define.amd ) {
// AMD. Register as an anonymous module.
define([ "jquery", "jquery-ui" ], factory );
} else {
// Browser globals
factory( jQuery );
}
}(function ($) {
// Detect touch support - Windows Surface devices and other touch devices
$.support.mspointer = window.navigator.msPointerEnabled;
$.support.touch = ( 'ontouchstart' in document
|| 'ontouchstart' in window
|| window.TouchEvent
|| (window.DocumentTouch && document instanceof DocumentTouch)
|| navigator.maxTouchPoints > 0
|| navigator.msMaxTouchPoints > 0
);
// Ignore browsers without touch or mouse support
if ((!$.support.touch && !$.support.mspointer) || !$.ui.mouse) {
return;
}
var mouseProto = $.ui.mouse.prototype,
_mouseInit = mouseProto._mouseInit,
_mouseDestroy = mouseProto._mouseDestroy,
touchHandled;
/**
* Cancel the event passed in
* @param {Object} event A touch event
*/
function cancelEvent(e) {
e.preventDefault();
e.stopPropagation();
}
/**
* Get the x,y position of a touch event
* @param {Object} event A touch event
*/
function getTouchCoords (event) {
return {
x: event.originalEvent.changedTouches[0].pageX,
y: event.originalEvent.changedTouches[0].pageY
};
}
/**
* Simulate a mouse event based on a corresponding touch event
* @param {Object} event A touch event
* @param {String} simulatedType The corresponding mouse event
*/
function simulateMouseEvent (event, simulatedType) {
// Ignore multi-touch events
if (event.originalEvent.touches.length > 1) {
return;
}
// Prevent "Ignored attempt to cancel a touchmove event with cancelable=false" errors
if (event.cancelable) {
event.preventDefault();
}
var touch = event.originalEvent.changedTouches[0],
simulatedEvent = document.createEvent('MouseEvents');
// Initialize the simulated mouse event using the touch event's coordinates
simulatedEvent.initMouseEvent(
simulatedType, // type
true, // bubbles
true, // cancelable
window, // view
1, // detail
touch.screenX, // screenX
touch.screenY, // screenY
touch.clientX, // clientX
touch.clientY, // clientY
false, // ctrlKey
false, // altKey
false, // shiftKey
false, // metaKey
0, // button
null // relatedTarget
);
// Dispatch the simulated event to the target element
event.target.dispatchEvent(simulatedEvent);
}
/**
* Handle the jQuery UI widget's touchstart events
* @param {Object} event The widget element's touchstart event
*/
mouseProto._touchStart = function (event) {
var self = this;
// Interaction time
this._startedMove = event.timeStamp;
// Track movement to determine if interaction was a click
self._startPos = getTouchCoords(event);
// Ignore the event if another widget is already being handled
if (touchHandled || !self._mouseCapture(event.originalEvent.changedTouches[0])) {
return;
}
// Track movement to determine if interaction was a click
self._touchMoved = false;
//Prevent right click menu from showing up
window.addEventListener('contextmenu', cancelEvent);
//set timeout based on delay property
self._touchStartTimeout = setTimeout(function() {
// Set the flag to prevent other widgets from inheriting the touch event
touchHandled = true;
//unset timeout variable
self._touchStartTimeout = null;
//only simulate events if touch hasn't moved
if(!self._touchMoved) {
// Simulate the mouseover event
simulateMouseEvent(event, 'mouseover');
// Simulate the mousemove event
simulateMouseEvent(event, 'mousemove');
// Simulate the mousedown event
simulateMouseEvent(event, 'mousedown');
}
}, 250);
};
/**
* Handle the jQuery UI widget's touchmove events
* @param {Object} event The document's touchmove event
*/
mouseProto._touchMove = function (event) {
// Interaction was not a click
this._touchMoved = true;
// Ignore event if not handled
if (!touchHandled) {
return;
}
// Simulate the mousemove event
simulateMouseEvent(event, 'mousemove');
};
/**
* Handle the jQuery UI widget's touchend events
* @param {Object} event The document's touchend event
*/
mouseProto._touchEnd = function (event) {
//clear a pending touchStart timeout
if(this._touchStartTimeout) {
clearTimeout(this._touchStartTimeout);
this._touchStartTimeout = null;
}
//remove listener for right click menu
window.removeEventListener('contextmenu', cancelEvent);
// Ignore event if not handled
if (!touchHandled) {
return;
}
// Simulate the mouseup event
simulateMouseEvent(event, 'mouseup');
// Simulate the mouseout event
simulateMouseEvent(event, 'mouseout');
// If the touch interaction did not move, it should trigger a click
// Check for this in two ways - length of time of simulation and distance moved
// Allow for Apple Stylus to be used also
var timeMoving = event.timeStamp - this._startedMove;
if (!this._touchMoved || timeMoving < 500) {
// Simulate the click event
simulateMouseEvent(event, 'click');
} else {
var endPos = getTouchCoords(event);
if ((Math.abs(endPos.x - this._startPos.x) < 10) && (Math.abs(endPos.y - this._startPos.y) < 10)) {
// If the touch interaction did not move, it should trigger a click
if (!this._touchMoved || event.originalEvent.changedTouches[0].touchType === 'stylus') {
// Simulate the click event
simulateMouseEvent(event, 'click');
}
}
}
// Unset the flag to determine the touch movement stopped
this._touchMoved = false;
// Unset the flag to allow other widgets to inherit the touch event
touchHandled = false;
};
/**
* A duck punch of the $.ui.mouse _mouseInit method to support touch events.
* This method extends the widget with bound touch event handlers that
* translate touch events to mouse events and pass them to the widget's
* original mouse event handling methods.
*/
mouseProto._mouseInit = function () {
var self = this;
// Microsoft Surface Support = remove original touch Action
if ($.support.mspointer) {
self.element[0].style.msTouchAction = 'none';
}
// Delegate the touch handlers to the widget's element
self.element.on({
touchstart: $.proxy(self, '_touchStart'),
touchmove: $.proxy(self, '_touchMove'),
touchend: $.proxy(self, '_touchEnd')
});
// Call the original $.ui.mouse init method
_mouseInit.call(self);
};
/**
* Remove the touch event handlers
*/
mouseProto._mouseDestroy = function () {
var self = this;
// Delegate the touch handlers to the widget's element
self.element.off({
touchstart: $.proxy(self, '_touchStart'),
touchmove: $.proxy(self, '_touchMove'),
touchend: $.proxy(self, '_touchEnd')
});
// Call the original $.ui.mouse destroy method
_mouseDestroy.call(self);
};
}));
......@@ -279,6 +279,17 @@
transition:opacity 300ms;
}
.cursor-grab {
cursor: grab;
}
.prevent-text-selection {
-webkit-user-select: none; // Safari
-webkit-touch-callout: none; // iOS popout
-ms-user-select: none; // Edge
user-select: none;
}
/* --------------
CENTER
-------------- */
......
......@@ -6,7 +6,7 @@ module Admin
load_resource :edition, class: 'CourseEdition'
def create
authorize! :update, Project
authorize! :import_projects_into, @edition
projects = Project.where(id: params[:selected_projects][:ids])
......@@ -29,6 +29,7 @@ module Admin
redirect_to import_projects_admin_course_edition_path(@edition.course,
@edition,
course_edition_ids: params[:importing_editions],
filtered_importable_projects: params[:filtered_importable_projects],
success_projects: success_projects,
failed_projects: failed_projects), status: :see_other # 303
end
......@@ -39,35 +40,41 @@ module Admin
# @param project [Project] the (non specific) project
# @return [Project] the (non specific) duplicated project
def copy_project(project)
authorize! :import_projects_from, project.course_edition
event = nil
new_project = nil
Project.transaction do
new_project = project.specific.dup
new_project.original_project_id = project.original_project_id || project.id
new_project.original_project_id =
if @edition.recurrent_edition_id.nil? &&
!project.course_edition.recurrent &&
project.course_edition.recurrent_edition_id.nil?
(project.original_project_id || project.id)
end
new_project.course_edition_id = @edition.id
new_project.status = :approved if project.status == 'deactivated'
new_project.save!
new_project = new_project.acting_as
project.images.each do |img|
new_project.images.create(title: img.title, file: img.file)
end
project.images.each { |img| new_project.images.create(title: img.title, file: img.file) }
project.tags.filter { |t| @edition.tags.find_by(id: t.id) }.each { |tag| new_project.tags << tag }
new_project.save!
if params[:deactivate_old] == '1'
log_event(
project,
:deactivate
)
log_event(project, :deactivate)
project.status = :deactivated
project.save!
end
copy_course_specific_roles(new_project, project)
if params[:notify] == '1'
event = log_event(
new_project,
:import
).associate(project.course_edition, as: :course_edition_old)
.associate(@edition, as: :course_edition_new)
.associate(project, as: :project_old)
.associate(@edition, as: :course_edition_new)
.associate(project, as: :project_old)
end
end
......@@ -80,6 +87,23 @@ module Admin
new_project
end
def copy_course_specific_roles(new_project, project)
csr_ids_to_copy = (params[:course_specific_roles_to_migrate] || {})[:ids] || []
project.user_course_specific_roles.each do |ucsr|
csr = ucsr.course_specific_role
next unless csr_ids_to_copy.include?(csr.id.to_s)
new_csr = @edition.course_specific_roles.find_by(name: csr.name)
raise ArgumentError, 'Could not migrate role' if new_csr.nil?
new_ucsr = ucsr.dup
new_ucsr.course_specific_role = new_csr
new_ucsr.resource = new_project
new_ucsr.save!
end
end
end
end
end
......@@ -132,13 +132,23 @@ module Admin
end
def import_projects
authorize! :import_projects_into, @course_edition
@current_projects = @course_edition.projects
.preload(offerer: %i[actable], actable: [], original_project: %i[actable])
@importable_projects = @course_edition.importable_projects(params[:course_edition_ids])
.preload(course_edition: [], offerer: %i[actable], actable: [])
@importing_editions = params[:course_edition_ids]
CourseEdition.where(id: @importing_editions).each { |c| authorize! :import_projects_from, c }
@success_projects = params[:success_projects] || []
@failed_projects = params[:failed_projects] || []
@filtered_importable_projects = params[:filtered_importable_projects] || []
@importable_projects = (
if @filtered_importable_projects.any?
Project.where(course_edition_id: params[:course_edition_ids])
.where(id: @filtered_importable_projects)
.where.not(id: @success_projects)
else
@course_edition.importable_projects(params[:course_edition_ids])
end
).preload(course_edition: [], offerer: %i[actable], actable: [])
render 'admin/course_editions/import_projects/overview'
end
......
<div class="panel panel-default mtl">
<div class="panel-heading ptm pbm">
Course-specific roles
</div>
<div class="panel-body">
<p>
Course-specific roles of the source course editions can be mapped to course-specific roles of the target course
edition.
For this linking to occur, the names of those roles must be exactly the same.
</p>
<p>
Here you can see how roles will be copied. Please tick the ones you wish to copy. All other role assignments will
be discarded.
</p>
<% mappings = [] %>
<%
CourseEdition.where(id: @importing_editions).each do |edition|
edition.course_specific_roles.each do |source_csr|
dest_csr = @course_edition.course_specific_roles.find_by(name: source_csr.name)
mappings.append([source_csr.name, source_csr, edition, dest_csr])
end
end
has_missing_required_roles = false
importing_from_optional_to_required_role = false
@course_edition.course_specific_roles.each do |csr|
CourseEdition.where(id: @importing_editions).each do |edition|
mapping = mappings.filter { |m| m.first == csr.name && m.third&.id == edition.id }.first
if mapping
importing_from_optional_to_required_role ||=
mapping.second && mapping.second.assignment_rule != 'required' && csr.assignment_rule == 'required'
else
mappings.append([csr.name, nil, edition, csr])
has_missing_required_roles ||= csr.assignment_rule == 'required'
end
end
end
mappings.sort_by!(&:first)
%>
<% if has_missing_required_roles %>
<div class="alert alert-warning" role="alert">
<%= glyphicon :'warning-sign' %>
Some required roles could not be mapped. The imported projects will have required roles
which are not assigned to any users.
</div>
<% end %>
<% if importing_from_optional_to_required_role %>
<div class="alert alert-warning" role="alert">
<%= glyphicon :'warning-sign' %>
Some non-required roles are mapped to required roles. Some of the imported projects may have required roles
which are not assigned to any users.
</div>
<% end %>
<div class="panel panel-default">
<table class="table table-condensed table--hoverable">
<thead>
<tr>
<th><input type="checkbox" checked="checked"/></th>
<th>Source</th>
<th></th>
<th>Destination</th>
</tr>
</thead>
<tbody>
<% mappings.each do |mapping| %>
<% source_csr, source_edition, dest_csr = mapping.second, mapping.third, mapping.fourth %>
<%
colour = if !source_csr && dest_csr && dest_csr.assignment_rule == 'required'
# missing required role
'danger'
elsif !source_csr && dest_csr && dest_csr.assignment_rule != 'required'
# missing optional/non-specifiable role
'warning'
elsif source_csr && !dest_csr
# assignments will be dropped - existing source role cannot be mapped to destination
'danger'
elsif source_csr && source_csr.assignment_rule != 'required' && dest_csr && dest_csr.assignment_rule == 'required'
# importing from optional/non-specifiable source role to required destination role
'warning'
end
%>
<tr class="<%= colour %>">
<td>
<% if source_csr && dest_csr %>
<input type="checkbox" name="course_specific_roles_to_migrate[ids][]" value="<%= source_csr.id %>" checked="checked">
<% end %>
</td>
<td>
<% if source_csr %>
<%= colored_label (source_csr.role_type == 'coach' ? :primary : :success),
source_csr.human_enum(:role_type) %>
<%= source_csr.name %>
<%= colored_label (source_csr.assignment_rule == 'required' ? :danger : :primary),
source_csr.human_enum(:assignment_rule),
'pull-right' %>
<% else %>
<i class="fa-regular fa-circle-question fa-xl mtm"></i>
<% end %>
<span class="text-muted block">
from <span class="txt-bold"><%= source_edition.display_name %></span>
</span>
</td>
<td class="txt-center">
<% if source_csr && dest_csr %>
<i class="fa-solid fa-right-long fa-xl mtl"></i>
<% else %>
<i class="fa-solid fa-right-long fa-xl mtl text-muted"></i>
<% end %>
</td>
<td>
<% if dest_csr %>
<%= colored_label (dest_csr.role_type == 'coach' ? :primary : :success),
dest_csr.human_enum(:role_type) %>
<%= dest_csr.name %>
<%= colored_label (dest_csr.assignment_rule == 'required' ? :danger : :primary),
dest_csr.human_enum(:assignment_rule),
'pull-right' %>
<span class="text-muted block">
in <span class="txt-bold"><%= @course_edition.display_name %></span>
</span>
<% else %>
<p class="text-center text-muted">
<i class="fa-solid fa-trash fa-xl mtl"></i>
</p>
<% end %>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
</div>
</div>
......@@ -3,7 +3,7 @@
<thead>
<tr>
<% if check_boxes %>
<th><input type="checkbox" /></th>
<th><input type="checkbox" <%= checked ? 'checked="checked"' : '' %> /></th>
<th><%= :id %></th>
<% else %>
<th><%= :id %></th>
......@@ -20,7 +20,7 @@
<tfoot>
<tr>