...
 
Commits (2)
......@@ -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); }
});
}
......@@ -8,6 +8,7 @@
@import 'jquery-ui/theme';
@import 'selectize';
@import 'selectize.bootstrap3';
@import 'boostrap-treeview.css';
@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
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
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
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
......@@ -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'
......
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
= render 'form'
\ No newline at end of file
h1.m-b-20 Dokumente
.documents-page
.sidebar
.panel.panel-default
.panel-heading
h4= t('actions')
.list-group
- if policy(Document).new?
= link_to t_title(:new, Document), new_document_path, class: 'list-group-item'
= link_to t('expand_all'), '#', id: 'tree_expand_all', class: 'list-group-item'
= link_to t('collapse_all'), '#', id: 'tree_collapse_all', class: 'list-group-item'
- if policy(Document).edit?
= link_to t_title(:edit, Document), documents_path, id: 'tree_edit_node', class: 'list-group-item'
- if policy(Document).destroy?
= link_to t_title(:destroy, Document), documents_path, id: 'tree_delete_node', class: 'list-group-item'
.content
#tree(data-tree="#{@documents_json.to_json}")
\ No newline at end of file
= render 'form'
\ No newline at end of file
---
de:
# to be injected into later groups
actions: Aktionen
expand_all: Alle anzeigen
collapse_all: Alle einklappen
generic_keys: &id-generic_keys
salutation: Anrede
first_name: Vorname
......@@ -500,6 +503,9 @@ de:
clients: Kunden/innen
profile: Profil
role: Rolle
document:
title: Titel
file: Datei
volunteer: &id-volunteer_attributes
<<: [*id-availability, *id-generic_keys, *id-volunteer-acceptance-keys]
acceptance: Prozess
......@@ -626,6 +632,7 @@ de:
user: Benutzer/in
volunteer: Freiwillige/n
volunteer_application: Freiwilligen Anmeldung
document: Dokument
# model name plural for controller name shortcut => t('.key')
notification_mailer:
......@@ -809,6 +816,9 @@ de:
necessary_volunteers: 'Benötigt %{amount} Freiwillige'
offer_of: Angebot von
show: *id-help-texts
documents:
submit:
title: 'Dokument erfassen'
hours:
new:
report_hours: "Stunden erfassen"
......
......@@ -96,6 +96,8 @@ Rails.application.routes.draw do
get :send_trial_period, on: :member
end
resources :documents
resources :semester_process_volunteers do
get :review_semester, on: :member
put :take_responsibility, on: :member
......
class CreateDocuments < ActiveRecord::Migration[5.1]
def change
create_table :documents do |t|
t.attachment :file
t.string :title, default: '', null: false
t.string :category1
t.string :category2
t.string :category3
t.string :category4
t.timestamps
end
end
end
class AddDeletedAtToDocument < ActiveRecord::Migration[5.1]
def change
add_column :documents, :deleted_at, :datetime
add_index :documents, :deleted_at
end
end
......@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20190806075109) do
ActiveRecord::Schema.define(version: 20191106160032) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
......@@ -269,6 +269,22 @@ ActiveRecord::Schema.define(version: 20190806075109) do
t.index ["department_id", "user_id"], name: "index_departments_users_on_department_id_and_user_id"
end
create_table "documents", force: :cascade do |t|
t.string "file_file_name"
t.string "file_content_type"
t.bigint "file_file_size"
t.datetime "file_updated_at"
t.string "title", default: "", null: false
t.string "category1"
t.string "category2"
t.string "category3"
t.string "category4"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.datetime "deleted_at"
t.index ["deleted_at"], name: "index_documents_on_deleted_at"
end
create_table "email_templates", force: :cascade do |t|
t.string "subject"
t.text "body"
......
require 'test_helper'
class DocumentsControllerPath < ActionDispatch::IntegrationTest
setup do
@superadmin = create :user, :with_clients,
:with_department, role: 'superadmin'
@social_worker = create :user, :with_clients,
:with_department, role: 'social_worker'
@department_manager = create :department_manager
end
test 'superadmin can submit new document' do
login_as @superadmin
path = File.join(Rails.root, 'test/fixtures/sample.pdf')
file = fixture_file_upload(path, 'application/pdf')
params = { document: { title: 't', category1: 'c', file: file } }
assert_difference 'Document.count', 1 do
post documents_path, params: params
end
end
test 'superadmin can edit document' do
document = create :document
login_as @superadmin
params = { document: { title: 'new', category1: 'c', category2: 'a' } }
patch document_path(document), params: params
assert Document.find(document.id).category2, 'c'
assert Document.find(document.id).title, 'new'
end
test 'superadmin can destroy document' do
document = create :document
login_as @superadmin
assert_difference 'Document.count', -1 do
delete document_path(document)
end
end
test 'others cannot destroy document' do
document = create :document
count = Document.count
login_as @social_worker
delete document_path(document)
assert count, Document.count
end
end
FactoryBot.define do
factory :document do
title { FFaker::Name }
category1 { FFaker::Name }
category2 { FFaker::Name }
category3 { FFaker::Name }
category4 { FFaker::Name }
file { File.new(File.join(Rails.root, 'test/fixtures/sample.pdf')) }
end
end
require 'test_helper'
class DocumentPolicyTest < PolicyAssertions::Test
def setup
@superadmin = create :user, :with_clients, :with_department, role: 'superadmin'
@social_worker = create :user, :with_clients, role: 'social_worker'
@department_manager = create :department_manager
@document = create :document
@file = File.new(File.join(Rails.root, 'test/fixtures/sample.pdf'))
end
test 'only superadmin can create document' do
document_params = {
title: 't', category1: 'c', file: @file
}
assert_permit @superadmin, Document.new(document_params), 'create?', 'new?', 'destroy?', 'edit?'
end
test 'others can only view' do
refute_permit(
@social_worker, @document,
'new?', 'edit?', 'create?', 'update?', 'destroy?'
)
refute_permit(
@department_manager, @document,
'new?', 'edit?', 'create?', 'update?', 'destroy?'
)
assert_permit(
@department_manager, @document,
'show?', 'index?'
)
assert_permit(
@social_worker, @document,
'show?', 'index?'
)
end
end