Commit 7ab1fcc0 authored by Hussain Kashef's avatar Hussain Kashef

Merge branch 'develop' into 'add/field_reserved_to_client'

# Conflicts:
#   db/schema.rb
parents 93e21cdc cc379f1d
Pipeline #63956 passed with stage
in 37 minutes and 29 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
......@@ -31,3 +31,20 @@
.billing-expenses.new .table-scrollable {
max-height: 500px;
}
.table-multicolumn {
display: flex;
flex-wrap: wrap;
& > table:first-child {
margin-bottom: 0;
}
@media (min-width: $container-md) {
flex-wrap: nowrap;
& > table:not(:first-child) {
margin-left: 10px;
}
}
}
.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
......@@ -87,11 +87,25 @@ class AssignmentsController < ApplicationController
def find_client
set_volunteer
@q = policy_scope(Client).inactive.ransack(params[:q])
@q = policy_scope(Client).inactive.ransack(include_egal(params[:q]))
@q.sorts = ['created_at desc'] if @q.sorts.empty?
@need_accompanying = @q.result.paginate(page: params[:page])
end
#special method for the Egal use case. Egal should be included and this is a hacking on the ransack search matchers to transform cont and eq to _in that permits the use of OR in the sql statement
def include_egal(params)
parameters = params.deep_dup
if parameters.present? && parameters.key?("age_request_cont") && parameters["age_request_cont"]&.present?
parameters["age_request_in"] = [parameters["age_request_cont"], "age_no_matter"]
parameters.delete("age_request_cont")
end
if parameters.present? && parameters.key?("gender_request_eq")&& parameters["gender_request_eq"]&.present?
parameters["gender_request_in"] = [parameters["gender_request_eq"], "no_matter"]
parameters.delete("gender_request_eq")
end
return parameters
end
def last_submitted_hours_and_feedbacks
@last_submitted_hours = @assignment.hours_since_last_submitted
@last_submitted_feedbacks = @assignment.feedbacks_since_last_submitted
......
......@@ -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
module ProcessedByConcern
extend ActiveSupport::Concern
included do
def register_acceptance_change(resource)
return unless resource.will_save_change_to_attribute?(:acceptance)
if resource.respond_to?("#{resource.acceptance}_by_id".to_sym)
resource["#{resource.acceptance}_by_id".to_sym] = current_user.id
end
end
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
......
class VolunteersController < ApplicationController
include ProcessedByConcern
before_action :set_volunteer, only: [:show, :edit, :update, :terminate, :account, :update_bank_details, :reactivate]
before_action :set_active_and_archived_missions, only: [:show, :edit]
......@@ -62,11 +63,16 @@ class VolunteersController < ApplicationController
def update
@volunteer.attributes = volunteer_params
return render :edit unless @volunteer.valid?
register_acceptance_change(@volunteer)
if @volunteer.will_save_change_to_attribute?(:acceptance, to: 'accepted') &&
@volunteer.internal? && !@volunteer.user && @volunteer.save
auto_assign_department!
redirect_to(edit_volunteer_path(@volunteer),
notice: t('invite_sent', email: @volunteer.primary_email))
elsif @volunteer.save
auto_assign_department! if @volunteer.saved_change_to_attribute?(:acceptance) && @volunteer.invited?
redirect_to edit_volunteer_path(@volunteer), notice: t('volunteer_updated')
else
render :edit
......@@ -81,7 +87,7 @@ class VolunteersController < ApplicationController
def terminate
if @volunteer.terminatable?
@volunteer.terminate!
@volunteer.terminate!(current_user)
redirect_back fallback_location: edit_volunteer_path(@volunteer),
notice: 'Freiwillige/r wurde erfolgreich beendet.'
else
......@@ -120,6 +126,13 @@ class VolunteersController < ApplicationController
private
def auto_assign_department!
return if !current_user.department_manager? || current_user.department.empty? || @volunteer.department.present?
# association
@volunteer.update(department: current_user.department.first)
end
def not_resigned
return if params[:q]
@volunteers = @volunteers.not_resigned
......
......@@ -83,6 +83,14 @@ module ApplicationHelper
@search_parameters ||= (params[:q]&.to_unsafe_hash || {}).except(:all)
end
def request_params_include_egal(params)
parameters = params.deep_dup
if parameters.has_key? 'age_request_cont'
parameters["age_request_in"] = [parameters["age_request_cont"], "age_no_matter"]
parameters.delete("age_request_cont")
end
end
def bootstrap_paginate(paginate_collection)
will_paginate paginate_collection, renderer: WillPaginate::ActionView::Bootstrap4LinkRenderer,
class: 'pagination-lg text-center hidden-print', 'aria-label': 'Pagination'
......@@ -145,11 +153,18 @@ module ApplicationHelper
tag.abbr(abbr.to_s, title: full_term)
end
def show_status_date(record, *args)
tag.ul(class: "list-unstyled") do
def show_status_date(record, include_processing_person, *args)
tag.ul(class: "list-unstyled") do
record.slice(*args).compact.each do |key, value|
concat tag.li(t_attr(key) +' '+ l(value))
if include_processing_person
updated_by_attr = key.include?('_at') ? key.sub('_at', '_by') : nil
concat tag.li([
t_attr(key) +' '+ l(value), record.send(updated_by_attr).to_s
].reject(&:blank?).join(" #{I18n.t('by')} "))
else
concat tag.li(t_attr(key) +' '+ l(value))
end
end
end
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
......
......@@ -82,7 +82,7 @@ class Client < ApplicationRecord
}
def terminatable?
assignments.unterminated.none?
assignments.active_or_not_yet_active.none?
end
def self.acceptences_restricted
......
......@@ -17,6 +17,7 @@ module TerminationScopes
date_between_inclusion(:termination_submitted_at, start_date, end_date)
}
scope :no_active_assignments, -> { joins(:clients).where("period_end < ?", Time.zone.now)}
scope :unterminated, (-> { field_nil(:termination_verified_by_id) })
scope :terminated, (-> { field_not_nil(:termination_verified_by_id) })
......
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