Commit e1cc62f3 authored by Hussain Kashef's avatar Hussain Kashef

Merge branch 'add/field_reserved_to_client' of...

Merge branch 'add/field_reserved_to_client' of git.panter.ch:open-source/aoz-003 into add/field_reserved_to_client

* 'add/field_reserved_to_client' of git.panter.ch:open-source/aoz-003: (38 commits)
  Automatically add department for invited
  Feat/cancellation of clients
  Multicolumn for clients/volunteers
  Auto assign department to volunteer
  on volunteer track who did the process step / wrong links in mails to admins after ending
  Fixed feedbacks
  Department now deletable
  Added contact to header
  Sorting by department
  Added other kind and conditional rendering
  All volunteers appear on events
  Fixed policy scopes
  Fixed spec
  Zoom of PDFs
  Another typo fixed
  Type in name of file
  Changed treeview extension
  Added library
  Fixed rendering of PDFs
  Fixed incorrect email & spelling on PDFs
  ...
parents 2e60a310 7ab1fcc0
Pipeline #63985 passed with stage
in 35 minutes and 25 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