Skip to content
GitLab
Menu
Projects
Groups
Snippets
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in
Toggle navigation
Menu
Open sidebar
EIP
Project Forum
Project Forum
Commits
37cab03d
Commit
37cab03d
authored
Sep 27, 2022
by
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
Changes
22
Hide whitespace changes
Inline
Side-by-side
app/assets/javascripts/application.js
View file @
37cab03d
...
...
@@ -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
...
...
app/assets/javascripts/jquery-ui-touch-punch/jquery.ui.touch-punch.js
0 → 100644
View file @
37cab03d
/*!
* 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
);
};
}));
app/assets/stylesheets/normal/helpers.scss
View file @
37cab03d
...
...
@@ -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
-------------- */
...
...
app/controllers/admin/course_editions/project_imports_controller.rb
View file @
37cab03d
...
...
@@ -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
.
im
ag
e
s
.
create
(
title:
img
.
title
,
file:
img
.
file
)
end
project
.
images
.
each
{
|
img
|
new_project
.
images
.
create
(
title:
img
.
title
,
file:
img
.
file
)
}
project
.
t
ags
.
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
app/controllers/admin/course_editions_controller.rb
View file @
37cab03d
...
...
@@ -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
...
...
app/views/admin/course_editions/import_projects/_course_specific_role_mapping.html.erb
0 → 100644
View file @
37cab03d
<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>
app/views/admin/course_editions/import_projects/_table.html.erb
View file @
37cab03d
...
...
@@ -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>