Commit 9f9f23a8 authored by Jiri Strojil's avatar Jiri Strojil

Merge branch 'develop' into 'master'

Develop

See merge request !998
parents 7d9b2d5f c8c15741
Pipeline #62740 failed with stage
in 33 minutes and 43 seconds
......@@ -18,7 +18,9 @@
//= require bootstrap-sprockets
//= require bootstrap-datepicker/core
//= require bootstrap-datepicker/locales/bootstrap-datepicker.de.js
//= require bootstrap-treeview
//= require cocoon
//= require selectize
//
//= require_tree .
//= require global
This diff is collapsed.
"use strict";
$(function () {
register_freetext_toggler();
register_treeview();
register_sidebar_submit();
});
function register_sidebar_submit() {
$('#sidebar-submit').on('click', function(e) {
e.preventDefault();
$('input[type=submit]').each(function() {
$(this).click();
});
});
}
// document view allows users to change input from select field to freetext input
// markup is prepared by rendering both field types, this listener toggles between
// both
function register_freetext_toggler() {
$('a.freetext').click(function() {
$(this).parents('.form-group').find(':input').each(function() {
var $input = $(this);
$input.toggle();
// assure that only the visible field has the name attribute
// set to avoid double submits
if ($input.is(':visible')) {
$input.attr('name', $input.data('name'));
} else {
$input.data('name', $input.attr('name'));
$input.removeAttr('name');
};
});
});
$('a.freetext').click();
}
/* controls the treeview plugin to display the documents tree */
function register_treeview() {
$('#tree').treeview({
data: $('#tree').data('tree'),
enableLinks: true,
levels: 1
});
$('#tree_expand_all').click(function() {
$('#tree').treeview('expandAll');
});
$('#tree_collapse_all').click(function() {
$('#tree').treeview('collapseAll');
});
$('#tree_delete_node').click(function(event) {
event.preventDefault();
event.stopPropagation();
var nodes = $('#tree').treeview('getSelected');
if (1 > nodes.length) {
alert('Bitte wählen Sie ein Dokument zum löschen.')
return;
}
if (!window.confirm("Wirklich löschen?")) {
return;
}
var href = $(this).attr("href") + '/' + nodes[0].documentId;
$.ajax(href, {method: "DELETE", async: false})
location.reload();
});
$('#tree_edit_node').click(function(event) {
event.preventDefault();
event.stopPropagation();
var nodes = $('#tree').treeview('getSelected');
if (1 > nodes.length) {
alert('Bitte wählen Sie ein Dokument zum bearbeiten.')
return;
}
var href = $(this).attr("href") + '/' + nodes[0].documentId+ '/edit';
window.location = href;
});
$( "#tree" ).on( "click", "a", function(event) {
event.preventDefault();
var href = $(this).attr("href");
if ('#' != href) { window.open(href); }
});
}
function volunteerForm() {
show_rejection();
toggleOtherInput($('#other-offer label input').length > 0 ? $('#other-offer label input')[0] : null)
$('#volunteer_acceptance').on('change', ({target}) => show_rejection(target));
$('#volunteer_acceptance').on('change load', ({target}) => show_rejection(target));
$('#other-offer label input').on('change', ({ target }) => { toggleOtherInput(target)});
$('.volunteer-active-checkbox-changes').on('change', ({target}) => {
const data = $(target).data();
......@@ -22,6 +25,13 @@ function volunteerForm() {
$('.checkbox-toggle-collapse').trigger('change');
}
const toggleOtherInput = (target) => {
if (!target) return;
const checked = $(target).is(':checked');
$('#volunteer_other_offer_desc').parent().toggle(checked);
}
const hideFormRegions = (hide) => {
hide.forEach(cssClass => $(`.${cssClass}`).hide());
}
......@@ -30,7 +40,7 @@ const showFormRegions = (hide) => {
hide.forEach(cssClass => $('.' + cssClass).show());
}
const show_rejection = (target) => {
const show_rejection = (target = '#volunteer_acceptance') => {
if($(target).val() == 'rejected') {
return $('.volunteer_rejection_type, .volunteer_rejection_text').show();
}
......
......@@ -8,6 +8,7 @@
@import 'jquery-ui/theme';
@import 'selectize';
@import 'selectize.bootstrap3';
@import 'bootstrap-treeview';
@import 'layout/*';
@import 'pages/*';
/* =========================================================
* bootstrap-treeview.css v1.2.0
* =========================================================
* Copyright 2013 Jonathan Miles
* Project URL : http://www.jondmiles.com/bootstrap-treeview
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* ========================================================= */
.treeview .list-group-item {
cursor: pointer;
}
.treeview span.indent {
margin-left: 10px;
margin-right: 10px;
}
.treeview span.icon {
width: 12px;
margin-right: 5px;
}
.treeview .node-disabled {
color: silver;
cursor: not-allowed;
}
\ No newline at end of file
.documents-page {
display: flex;
flex-direction: column;
margin-bottom: 30px;
@media (min-width: $container-md) {
flex-direction: row;
}
.sidebar {
width: 100%;
flex: 1;
margin-right: 20px;
@media (min-width: $container-md) {
flex: 0 0 $sidebar-width;
width: $sidebar-width;
}
}
.content {
flex: 1 1 auto;
overflow-x: hidden;
}
.document_category {
margin-left: -10px;
margin-right: -10px;
}
.freetext {
margin-left: 5px;
}
}
\ No newline at end of file
......@@ -57,22 +57,9 @@ class CertificatesController < ApplicationController
def prepare_params
certificate_params
.except(:assignment_kinds).merge(volunteer: @volunteer, user_id: current_user.id,
assignment_kinds: { done: kinds_done_filter, available: kinds_available_filter })
.except(:assignment_kinds).merge(volunteer: @volunteer, user_id: current_user.id)
end
def kinds_available_filter
@kinds_available ||= GroupOfferCategory.where.not(id: kinds_done_filter
.map { |done| done[1] }).map { |goc| [goc.category_name, goc.id] }
end
def kinds_done_filter
@kinds_done ||= @volunteer.assignment_categories_done.select do |_, id|
certificate_params[:assignment_kinds].reject(&:blank?).map(&:to_i).include? id
end + @volunteer.assignment_categories_available.select do |_, id|
certificate_params[:assignment_kinds].reject(&:blank?).map(&:to_i).include? id
end
end
def set_certificate
@certificate = Certificate.find(params[:id])
......
......@@ -4,9 +4,9 @@ module PdfHelpers
"#{record.model_name.human}-#{record.id}-#{date.strftime '%F'}.pdf"
end
def render_to_pdf(action = "#{action_name}.html")
html = render_to_string(action: action, layout: WickedPdf.config[:layout])
WickedPdf.new.pdf_from_string(html)
def render_to_pdf(action = "#{action_name}.html", options = {})
html = render_to_string({action: action}.merge(options))
WickedPdf.new.pdf_from_string(html, options)
end
def render_pdf_attachment(record)
......@@ -19,8 +19,15 @@ module PdfHelpers
filename: pdf_file_name(record)
end
def save_with_pdf(record, action = 'show.html')
record.pdf = StringIO.new(render_to_pdf(action)) if record.generate_pdf
def save_with_pdf(record, action = 'show.html', options = {})
{ layout: 'pdf_layout.pdf.slim', zoom: 1.15,
dpi: 600, margin: { top: 10, bottom: 10, left: 0, right: 0 }
}.each do |k,v|
next if options.key?(k)
options[k] = v
end
record.pdf = StringIO.new(render_to_pdf(action, options)) if record.generate_pdf
record.save
end
end
class DocumentsController < ApplicationController
before_action :find_document, only: %i[update edit destroy]
def new
@document = Document.new
authorize @document
end
def index
authorize Document
@documents_json = DocumentTreeview.new.document_js_nodes
end
def create
@document = Document.create(document_params)
authorize @document
if @document.valid?
redirect_to documents_path
else
render :new
end
end
def edit; end
def update
@document.update(document_params)
if @document.valid?
redirect_to documents_path
else
render :edit
end
end
def destroy
@document.destroy
redirect_to documents_path
end
private
def find_document
@document = Document.find(params[:id])
authorize @document
end
def document_params
params.require(:document).permit(
:category1, :category2, :category3, :category4, :title, :file
)
end
end
\ No newline at end of file
......@@ -9,7 +9,7 @@ class EventsController < ApplicationController
end
def show
@volunteers = @event.intro_course? ? Volunteer.needs_intro_course : Volunteer.accepted.internal
@volunteers = Volunteer.all
@volunteers -= @event.event_volunteers.map(&:volunteer)
@event_volunteer = EventVolunteer.new(event: @event)
respond_to do |format|
......
# this is obsolete, only used for the index action. we should move the action and the view to semester_feedbacks
class FeedbacksController < ApplicationController
def index
semester_feedback = SemesterFeedback.joins(:volunteer).where(:volunteers => { id: params[:volunteer_id] })
@feedbacks = if params[:assignment_id]
SemesterFeedback.where(assignment_id: params[:assignment_id])
semester_feedback.where(assignment_id: params[:assignment_id])
elsif params[:group_offer_id]
SemesterFeedback.where(group_assignment_id: GroupAssignment.where(group_offer_id: params[:group_offer_id]).ids)
semester_feedback.where(group_assignment_id: GroupAssignment.where(group_offer_id: params[:group_offer_id]).ids)
else
[]
end
......
......@@ -20,6 +20,7 @@ class GroupAssignmentsController < ApplicationController
def create
@group_assignment = GroupAssignment.new(group_assignment_params)
@group_assignment.created_by = current_user
@group_assignment.default_values
authorize @group_assignment
if save_with_pdf @group_assignment, 'show.pdf'
......@@ -35,6 +36,7 @@ class GroupAssignmentsController < ApplicationController
def update
@group_assignment.assign_attributes(group_assignment_params)
@group_assignment.created_by = current_user
period_end_set_notice, redirect_path = handle_period_end
if save_with_pdf @group_assignment, 'show.pdf'
create_redirect period_end_set_notice, redirect_path
......
......@@ -28,7 +28,12 @@ class GroupOffersController < ApplicationController
respond_to do |format|
format.html
format.pdf do
render pdf: pdf_file_name(@group_offer)
render pdf: pdf_file_name(@group_offer),
layout: 'pdf_layout.pdf.slim',
zoom: 1.5,
dpi: 600,
margin: { top: 10, bottom: 10, left: 0, right: 0 },
template: 'group_offers/show.pdf.slim'
end
end
end
......
module ContactHelper
def self.address_for_pdf(contact)
[
[contact&.street, contact&.extended].reject(&:blank?).join(', '),
[contact&.postal_code, contact&.city].reject(&:blank?).join(' '),
"www.aoz.ch/freiwilligenarbeit"
].compact.join('<br>')
end
end
......@@ -10,4 +10,15 @@ class NotificationMailer < ApplicationMailer
)
)
end
def volunteer_added_to_group_offer(group_assignment)
@group_assignment = group_assignment
mail(
to: User.superadmins.collect(&:email),
subject: I18n.t(
'notification_mailer.volunteer_added_to_group_offer.subject'
)
)
end
end
......@@ -29,7 +29,7 @@ class Certificate < ApplicationRecord
end
DEFAULT_INSTITUTION = "**AOZ** Zürich, Flüelastrasse 32, 8047 Zürich \r\n"\
'info@aoz-freiwillige.ch'.freeze
'freiwillige@aoz.ch'.freeze
DEFAULT_FUNCTION = 'Förderung der sozialen und beruflichen Integration von Asylsuchenden, '\
'Geflüchteten und Migrant/innen'.freeze
......
class Document < ApplicationRecord
has_attached_file :file
validates :title, presence: true
validates_attachment_presence :file
validates_attachment_content_type :file, content_type: ['application/pdf', 'application/vnd.ms-excel', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet']
def self.categories(level = 1)
Document.pluck('category'+level.to_i.to_s).compact.uniq.sort
end
end
# helps building the json format that is used by the treeview plugin
class DocumentTreeview
# access the database to identify all the (flat) stored categories and bring
# them into a tree structure
def categories_tree
tree = {}
category_keys = Document.order(:category1).pluck(:category1, :category2, :category3, :category4)
category_keys.each do |keys|
next if keys.first.blank?
tree = a_to_h(keys, tree)
end
tree
end
# transform the format of categories tree to a format used by the
# js to display trees in the view
def category_js_nodes(cat_tree = categories_tree)
nodes = []
cat_tree.keys.sort.each do |key|
node = { text: key, selectable: false, nodes: [] }
unless cat_tree[key].empty?
node[:nodes] = category_js_nodes(cat_tree[key])
end
nodes << node
end
nodes
end
# add all documents / links to the document efficient to the js formatted category
def document_js_nodes
js_nodes = category_js_nodes
Document.all.each do |d|
categories = [ d.category1, d.category2, d.category3, d.category4 ].compact
nodes = js_nodes
categories.each do |category|
nodes.each do |node|
if category == node[:text]
nodes = node[:nodes]
break
end
end
end
nodes << {
text: d.title,
href: d.file.url,
documentId: d.id,
icon: 'glyphicon glyphicon-book'
}
nodes.sort_by! do |node|
key = node[:nodes] ? '0folder' : '1doc' # prefer folders over documents
key += node[:text]
key
end
end
js_nodes
end
# helper method to transform category arrays to an array
def a_to_h(arr, hash)
return {} if arr.empty? || arr.first.blank?
cur_key = arr.shift
cur_hash = hash[cur_key]
hash[cur_key] = cur_hash = {} if cur_hash.nil?
a_to_h(arr, cur_hash)
return hash
end
end
\ No newline at end of file
......@@ -16,6 +16,9 @@ class GroupAssignment < ApplicationRecord
}
after_save :update_group_offer_search_field
after_create :mail_superadmins
attr_accessor :created_by
scope :running, (-> { no_end.have_start })
......@@ -50,6 +53,11 @@ class GroupAssignment < ApplicationRecord
group_offer
end
def mail_superadmins
return unless created_by
NotificationMailer.volunteer_added_to_group_offer(self).deliver_now unless created_by.superadmin?
end
def assignment?
false
end
......
......@@ -22,7 +22,8 @@ class GroupOffer < ApplicationRecord
has_many :group_assignment_logs
has_many :hours, as: :hourable, dependent: :destroy
has_many :feedbacks, as: :feedbackable, dependent: :destroy
# obsolete?
# has_many :feedbacks, as: :feedbackable, dependent: :destroy
has_many :trial_feedbacks, as: :trial_feedbackable, dependent: :destroy
has_many :volunteers, through: :group_assignments
......
......@@ -12,6 +12,8 @@ class GroupOfferCategory < ApplicationRecord
scope :house_moving, -> { where('category_name LIKE ?', '%Zürich%') }
scope :without_house_moving, -> { where.not('category_name LIKE ?', '%Zürich%') }
scope :in_group_offer, (-> { joins(:group_offers) })
scope :without_other, -> { where.not('category_name = ?', 'Anderes / auf Inserat:')}
scope :other, -> { where('category_name = ? ', 'Anderes / auf Inserat:')}
def self.available_categories(exclude_ids)
active.where.not(id: exclude_ids).map { |goc| [goc.category_name, goc.id] }
......
class DocumentPolicy < ApplicationPolicy
alias_method :new?, :superadmin?
alias_method :create?, :superadmin?
alias_method :index?, :allow_all!
alias_method :show?, :allow_all!
alias_method :edit?, :superadmin?
alias_method :update?, :superadmin?
alias_method :destroy?, :superadmin?
end
class HourPolicy < ApplicationPolicy
class Scope < ApplicationScope
def resolve
return all if superadmin?
if department_manager?
return scope.in_department_or_secondary_department(user.department).or(scope.assignable_to_department)
end
none
end
end
def index?
superadmin? || volunteer? && handle_record_or_class
superadmin? || department_manager? || volunteer? && handle_record_or_class
end
def handle_record_or_class
......@@ -10,7 +20,7 @@ class HourPolicy < ApplicationPolicy
alias_method :supervisor?, :superadmin?
# Actions
alias_method :show?, :superadmin_or_volunteer_related?
alias_method :show?, :superadmin_or_department_manager_or_volunteer_related?
alias_method :new?, :superadmin_or_volunteer_related?
alias_method :edit?, :superadmin_or_volunteer_related?
alias_method :create?, :superadmin_or_volunteer_related?
......
......@@ -35,6 +35,8 @@ nav.navbar.navbar-top.hidden-print
li= link_to 'Standorte', departments_path
- elsif policy(Department).manager_with_department?
li= link_to t_model(Department), department_path(current_user.department.first.id)
- if policy(Document).index?
li= link_to 'Dokumente', documents_path
ul.nav.navbar-nav.navbar-right
li.dropdown#menu
a.dropdown-toggle aria-expanded='false' aria-haspopup='true' data-toggle='dropdown' href='#' role='button'
......
......@@ -8,30 +8,16 @@
button.btn.btn-default(data-toggle='collapse' data-target='#additionalEdit' type='button' aria-expanded='false' aria-controls='additionalEdit')
= t('.edit_more_fields')
hr
#additionalEdit.collapse
fieldset
legend Einsatz Arten
= f.label :assignment_kinds, class: 'sr-only'
= f.input :assignment_kinds, as: :check_boxes, collection: @certificate.assignment_kinds['done'].uniq, include_blank: false, label: false, input_html: { checked: true }
h4 Kontaktangaben des Freiwilligen
= f.simple_fields_for :volunteer_contact do |fvc|
= fvc.input :name, label: t_attr(:name), input_html: { value: @certificate.volunteer_contact['name'] }
= fvc.input :street, label: t_attr(:street), input_html: { value: @certificate.volunteer_contact['street'] }
= fvc.input :city, label: t_attr(:city), input_html: { value: @certificate.volunteer_contact['city'] }
button.btn.btn-default(data-toggle='collapse' data-target='#additional-kinds' type='button' aria-expanded='false' aria-controls='additionalEdit')
'Manuell Einsatzkategorien hinzufügen
#additional-kinds.collapse
h4 Zusätzliche Einsatzarten
= f.label :assignment_kinds, class: 'sr-only'
= f.input :assignment_kinds, as: :check_boxes, collection: @certificate.collection_for_additional_kinds,
include_blank: false, label: false, input_html: { checked: false }
h4 Kontaktangaben des Freiwilligen
= f.simple_fields_for :volunteer_contact do |fvc|
= fvc.input :name, label: t_attr(:name), input_html: { value: @certificate.volunteer_contact['name'] }
= fvc.input :street, label: t_attr(:street), input_html: { value: @certificate.volunteer_contact['street'] }
= fvc.input :city, label: t_attr(:city), input_html: { value: @certificate.volunteer_contact['city'] }
= f.input :function
= f.input :institution
= f.input :creator_name
= f.input :creator_function
= f.input :function
= f.input :institution
= f.input :creator_name
= f.input :creator_function
ul.list-inline
li= f.button :submit
......
......@@ -28,13 +28,6 @@
dt= t_attr(:function)
dd= @certificate.function
dl
dt Einsatzkategorien
dd
- @certificate.assignment_kinds['done'].uniq.each do |kind_name, _|
span.kind-list
input*{ type: 'checkbox', checked: true } onclick='return false;'
= kind_name
dl
dt= t_attr(:duration)
dd= "#{l(@certificate.duration_start, default: '')} #{t('to')} #{l(@certificate.duration_end, default: '')}"
......
......@@ -17,13 +17,6 @@
.col-xs-3= t_attr(:function)
.col-xs-7= @certificate.function
.row
.col-xs-3= t_attr(:assignment)
.col-xs-7
- @certificate.assignment_kinds['done'].uniq.each do |kind_name, _|
span.kind-list
== File.read('app/assets/images/pdf_checkbox_checked.svg')
= kind_name
.row
.col-xs-3= t_attr(:duration)
.col-xs-7= "#{l(@certificate.duration_start, default: '')} #{t('to')} #{l(@certificate.duration_end, default: '')}"
......
h1.m-b-20= t_title(:new)
.documents-page
.sidebar
.panel.panel-default
.panel-heading
h4= t('actions')
.list-group
= link_to t_title(:submit, Document), '', class: 'list-group-item', id: 'sidebar-submit'
= link_to t_title(:index, Document), documents_path, class: 'list-group-item'
.content
= simple_form_for @document do |form|
= form.input :title
- (1..4).each do |i|
.row.form-group.string.optional.document_category
label.string.optional.col-sm-2
= Document.human_attribute_name("category#{i}")
a(class='freetext glyphicon glyphicon-edit' href="#")
.col-sm-10
= form.select "category#{i}", Document.categories(i), {}, class: 'select optional form-control', style: 'display: none;'
= form.text_field "category#{i}", class: 'string optional form-control'
= form.input :file
.row
.col-sm-3
= form_navigation_btn :back, with_row: false
.col-sm-offset-1.col-sm-3
= form.button :submit