Import the first public release
Signed-off-by: OKTET Labs Ltd. <diary-maint@oktetlabs.ru>master
commit
a8df72ef9e
|
@ -0,0 +1,2 @@
|
|||
.reviewboardrc
|
||||
diary_env.rb
|
|
@ -0,0 +1,73 @@
|
|||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
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.
|
|
@ -0,0 +1,2 @@
|
|||
# SPDX-License-Identifier: Apache-2.0
|
||||
# Copyright (C) 2021 OKTET Labs Ltd. All rights reserved.
|
|
@ -0,0 +1,4 @@
|
|||
##############################################
|
||||
## OKTET Labs. Diary Management Application ##
|
||||
##############################################
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
# SPDX-License-Identifier: Apache-2.0
|
||||
# Copyright (C) 2021 OKTET Labs Ltd. All rights reserved.
|
||||
|
||||
# vim: syntax=apache ts=4 sw=4 sts=4 sr noet
|
||||
|
||||
ScriptAlias /cgi-bin/ /var/www/cgi-bin/
|
||||
<Directory "/var/www/cgi-bin">
|
||||
AllowOverride None
|
||||
Options ExecCGI FollowSymLinks
|
||||
</Directory>
|
||||
|
||||
#Settings fo Diary
|
||||
Alias /main/diary/css /var/www/cgi-bin/diary/css
|
||||
Alias /main/diary/js /var/www/cgi-bin/diary/js
|
||||
Alias /main/diary/diary.js /var/www/cgi-bin/diary/diary.js
|
||||
Alias /main/diary/diary.css /var/www/cgi-bin/diary/diary.css
|
||||
Alias /main/diary/favicon.css /var/www/cgi-bin/diary/favicon.css
|
||||
Alias /main/diary/diary_next.png /var/www/cgi-bin/diary/diary_next.png
|
||||
Alias /main/diary/diary_prev.png /var/www/cgi-bin/diary/diary_prev.png
|
||||
Alias /public/logo-small.gif /var/www/cgi-bin/diary/logo-small.gif
|
||||
<Location "/cgi-bin/diary">
|
||||
Require ldap-filter o=People
|
||||
</Location>
|
|
@ -0,0 +1,134 @@
|
|||
#!/usr/bin/ruby -w
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# Copyright (C) 2021 OKTET Labs Ltd. All rights reserved.
|
||||
|
||||
#
|
||||
# Cgi for Diary Management Application
|
||||
#
|
||||
|
||||
# "." is removed from LOAD_PATH in Ruby 1.9
|
||||
# Backporting require_relative to Ruby 1.8
|
||||
# See http://steveklabnik.github.io/require_relative/require_relative.html
|
||||
#
|
||||
unless Object.new.respond_to?(:require_relative, true)
|
||||
def require_relative(relative_feature)
|
||||
file = caller.first.split(/:\d/,2).first
|
||||
raise LoadError, "require_relative is called in #{$1}" if
|
||||
/\A\((.*)\)/ =~ file
|
||||
require File.expand_path(relative_feature, File.dirname(file))
|
||||
end
|
||||
end
|
||||
|
||||
require 'cgi'
|
||||
require 'ldap'
|
||||
require_relative 'diary_env'
|
||||
require 'dbi'
|
||||
require 'amrita/template'
|
||||
include Amrita
|
||||
|
||||
class AmritaTemplate < Amrita::TemplateText
|
||||
def initialize(filename)
|
||||
# Use TemplateText instead of TemplateFile to avoid problem
|
||||
# in amrita/node.rb with tainted template strings, because
|
||||
# they have been read from external file
|
||||
super(IO.read(filename).untaint)
|
||||
self.amrita_id = :_id
|
||||
self.asxml = true
|
||||
end
|
||||
end
|
||||
|
||||
require_relative 'sql_cache'
|
||||
require_relative 'project'
|
||||
require_relative 'diary'
|
||||
require_relative 'mycgi'
|
||||
|
||||
$SAFE = 1
|
||||
$DEBUG = 1
|
||||
|
||||
class NeedConfirm < Exception
|
||||
def initialize(code = nil)
|
||||
@code = code
|
||||
end
|
||||
attr :code
|
||||
end
|
||||
|
||||
#
|
||||
# Initialize global variables
|
||||
#
|
||||
|
||||
# CGI default parameters
|
||||
cgi_defaults = {
|
||||
"do" => "diary",
|
||||
"prj" => {
|
||||
"list" => {
|
||||
"customer" => ["*"],
|
||||
"prj_status" => [], #Project::STATUS_DEFAULT,
|
||||
"edit" => []
|
||||
},
|
||||
"action" => {}
|
||||
},
|
||||
"diary" => {
|
||||
"list" => {
|
||||
"start" => MyCGI::date2hash(Date.today),
|
||||
"end" => MyCGI::date2hash(Date.today),
|
||||
"prj_id" => ["*"],
|
||||
"customer" => ["*"],
|
||||
"who" => ["*"],
|
||||
"newest_first" => ["off"],
|
||||
"just_stats" => ["off"],
|
||||
"edit" => []
|
||||
},
|
||||
"action" => {}
|
||||
},
|
||||
"confirm" => []
|
||||
}
|
||||
|
||||
env = DiaryEnv.instance
|
||||
env.user = ENV["REMOTE_USER"]
|
||||
|
||||
# Customize default parameters
|
||||
# TODO: make this user-defined
|
||||
cgi_defaults["diary"]["list"]["start"]["day"] = 1
|
||||
cgi_defaults["diary"]["list"]["who"] = [env.user.uid] if env.user.local?
|
||||
|
||||
# CGI object
|
||||
cgi = MyCGI.new("post", cgi_defaults, ["list", "confirm"], ["descr"], "html4Tr")
|
||||
prj = ProjectUI.new(cgi)
|
||||
diary = DiaryUI.new(cgi)
|
||||
|
||||
|
||||
cgi.out("charset" => "utf-8") do begin
|
||||
env.confirmation = cgi.tree_params["confirm"]
|
||||
|
||||
# Process cgi parameters
|
||||
prj.action
|
||||
diary.action
|
||||
|
||||
# Output html data
|
||||
if cgi.tree_params["do"] == "project"
|
||||
prj.show
|
||||
elsif cgi.tree_params["do"] == "diary"
|
||||
diary.show
|
||||
else
|
||||
""
|
||||
end
|
||||
|
||||
rescue NeedConfirm => detail
|
||||
tmpl = AmritaTemplate.new("confirm.html")
|
||||
s = String.new
|
||||
tmpl.expand(s, {
|
||||
:message => noescape{$!},
|
||||
:hiddens => cgi.hiddens(cgi.tree_flatten([])),
|
||||
:code => cgi.hiddens([["confirm", detail.code]])
|
||||
})
|
||||
|
||||
rescue RuntimeError
|
||||
tmpl = AmritaTemplate.new("error.html")
|
||||
s = String.new
|
||||
tmpl.expand(s, {
|
||||
:message => noescape{$!},
|
||||
:debug => $!.backtrace.join("\n")
|
||||
})
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
<?xml version="1.0"?>
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<meta http-equiv="Content-Style-Type" content="text/css" />
|
||||
<link rel="stylesheet" type="text/css" href="/main/diary/diary.css"/>
|
||||
<title>Diary Management</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Diary Management</h1>
|
||||
<h2 class="warning">Confirmation is required!</h2>
|
||||
<p _id="message">Message text</p>
|
||||
<form method="post" action="#">
|
||||
<input type="hidden" _id="hiddens">
|
||||
<input type="hidden" _id="code">
|
||||
<input type="submit" value="Confirm">
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,77 @@
|
|||
# SPDX-License-Identifier: Apache-2.0
|
||||
# Copyright (C) 2021 OKTET Labs Ltd. All rights reserved.
|
||||
|
||||
#
|
||||
# Usage: mysql diary <create.mysql
|
||||
#
|
||||
|
||||
CREATE TABLE `director` (
|
||||
`nick` varchar(30) NOT NULL
|
||||
);
|
||||
INSERT INTO director SET `nick`='director';
|
||||
|
||||
CREATE TABLE `project` (
|
||||
`id` mediumint(9) unsigned NOT NULL auto_increment,
|
||||
`tag` varchar(30) NOT NULL,
|
||||
`name` varchar(70) NOT NULL,
|
||||
`prj_desc` mediumtext NOT NULL,
|
||||
`prj_status` enum('NEW','ACTIVE','TERMINATED') default 'ACTIVE' not NULL,
|
||||
`status_desc` mediumtext,
|
||||
`issues` mediumtext,
|
||||
`manager` varchar(30) NOT NULL,
|
||||
`leader` varchar(30),
|
||||
`customer` varchar(30) NOT NULL,
|
||||
`cust_man` varchar(30) NOT NULL,
|
||||
`cust_tech` varchar(30),
|
||||
`maillists` varchar(120),
|
||||
`svndir` varchar(120),
|
||||
`storagedir` varchar(120),
|
||||
`bugzilla_prod` varchar(64),
|
||||
`extra_rights` varchar(255),
|
||||
`hide_hrs` tinyint(1) default NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE `name` (`name`),
|
||||
UNIQUE `tag` (`customer`, `tag`)
|
||||
);
|
||||
|
||||
CREATE TABLE `cc_list` (
|
||||
`prj_id` mediumint(9) unsigned NOT NULL default 0,
|
||||
`person` varchar(30) NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE `diary` (
|
||||
`id` mediumint(9) unsigned NOT NULL auto_increment,
|
||||
`who` varchar(30) NOT NULL,
|
||||
`prj_id` mediumint(9) unsigned NOT NULL default 0,
|
||||
`hours` tinyint unsigned NOT NULL default 0,
|
||||
`ddate` date NOT NULL default '0000-00-00',
|
||||
`descr` mediumtext NOT NULL,
|
||||
`private` tinyint(1) NOT NULL default 0,
|
||||
`bugid` mediumint(9),
|
||||
`modified` datetime,
|
||||
`created` datetime,
|
||||
`state` tinyint unsigned,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `who` (`who`),
|
||||
KEY `prj_id` (`prj_id`),
|
||||
KEY `ddate` (`ddate`)
|
||||
);
|
||||
|
||||
CREATE TABLE `sandbox` (
|
||||
`id` int(11) default NULL,
|
||||
`modified` datetime default NULL
|
||||
);
|
||||
|
||||
CREATE TABLE `approval` (
|
||||
`who` varchar(30) NOT NULL,
|
||||
`prj_id` mediumint(9) unsigned NOT NULL,
|
||||
`approver` varchar(30) NOT NULL,
|
||||
PRIMARY KEY (`who`, `prj_id`)
|
||||
);
|
||||
|
||||
CREATE TABLE `approval_note` (
|
||||
`id` mediumint(9) unsigned NOT NULL,
|
||||
`created` datetime NOT NULL,
|
||||
`author` varchar(30) NOT NULL,
|
||||
`note` mediumtext
|
||||
);
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,330 @@
|
|||
/*!
|
||||
* Bootstrap Reboot v4.0.0 (https://getbootstrap.com)
|
||||
* Copyright 2011-2018 The Bootstrap Authors
|
||||
* Copyright 2011-2018 Twitter, Inc.
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
|
||||
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
|
||||
*/
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
font-family: sans-serif;
|
||||
line-height: 1.15;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-ms-text-size-adjust: 100%;
|
||||
-ms-overflow-style: scrollbar;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
@-ms-viewport {
|
||||
width: device-width;
|
||||
}
|
||||
|
||||
article, aside, dialog, figcaption, figure, footer, header, hgroup, main, nav, section {
|
||||
display: block;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
color: #212529;
|
||||
text-align: left;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
[tabindex="-1"]:focus {
|
||||
outline: 0 !important;
|
||||
}
|
||||
|
||||
hr {
|
||||
box-sizing: content-box;
|
||||
height: 0;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
abbr[title],
|
||||
abbr[data-original-title] {
|
||||
text-decoration: underline;
|
||||
-webkit-text-decoration: underline dotted;
|
||||
text-decoration: underline dotted;
|
||||
cursor: help;
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
address {
|
||||
margin-bottom: 1rem;
|
||||
font-style: normal;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul,
|
||||
dl {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
ol ol,
|
||||
ul ul,
|
||||
ol ul,
|
||||
ul ol {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
dt {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin-bottom: .5rem;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
dfn {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
small {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
sub,
|
||||
sup {
|
||||
position: relative;
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -.5em;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #007bff;
|
||||
text-decoration: none;
|
||||
background-color: transparent;
|
||||
-webkit-text-decoration-skip: objects;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #0056b3;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
a:not([href]):not([tabindex]) {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:not([href]):not([tabindex]):hover, a:not([href]):not([tabindex]):focus {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:not([href]):not([tabindex]):focus {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
pre,
|
||||
code,
|
||||
kbd,
|
||||
samp {
|
||||
font-family: monospace, monospace;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
pre {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
overflow: auto;
|
||||
-ms-overflow-style: scrollbar;
|
||||
}
|
||||
|
||||
figure {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
img {
|
||||
vertical-align: middle;
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
svg:not(:root) {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
caption {
|
||||
padding-top: 0.75rem;
|
||||
padding-bottom: 0.75rem;
|
||||
color: #6c757d;
|
||||
text-align: left;
|
||||
caption-side: bottom;
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: inherit;
|
||||
}
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
margin-bottom: .5rem;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
button:focus {
|
||||
outline: 1px dotted;
|
||||
outline: 5px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
input,
|
||||
button,
|
||||
select,
|
||||
optgroup,
|
||||
textarea {
|
||||
margin: 0;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
button,
|
||||
input {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
button,
|
||||
select {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
button,
|
||||
html [type="button"],
|
||||
[type="reset"],
|
||||
[type="submit"] {
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
button::-moz-focus-inner,
|
||||
[type="button"]::-moz-focus-inner,
|
||||
[type="reset"]::-moz-focus-inner,
|
||||
[type="submit"]::-moz-focus-inner {
|
||||
padding: 0;
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
input[type="radio"],
|
||||
input[type="checkbox"] {
|
||||
box-sizing: border-box;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
input[type="date"],
|
||||
input[type="time"],
|
||||
input[type="datetime-local"],
|
||||
input[type="month"] {
|
||||
-webkit-appearance: listbox;
|
||||
}
|
||||
|
||||
textarea {
|
||||
overflow: auto;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
legend {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
padding: 0;
|
||||
margin-bottom: .5rem;
|
||||
font-size: 1.5rem;
|
||||
line-height: inherit;
|
||||
color: inherit;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
[type="number"]::-webkit-inner-spin-button,
|
||||
[type="number"]::-webkit-outer-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
[type="search"] {
|
||||
outline-offset: -2px;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
[type="search"]::-webkit-search-cancel-button,
|
||||
[type="search"]::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
font: inherit;
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
output {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
template {
|
||||
display: none;
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
/*# sourceMappingURL=bootstrap-reboot.css.map */
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,8 @@
|
|||
/*!
|
||||
* Bootstrap Reboot v4.0.0 (https://getbootstrap.com)
|
||||
* Copyright 2011-2018 The Bootstrap Authors
|
||||
* Copyright 2011-2018 Twitter, Inc.
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
|
||||
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
|
||||
*/*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;-ms-overflow-style:scrollbar;-webkit-tap-highlight-color:transparent}@-ms-viewport{width:device-width}article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus{outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}dfn{font-style:italic}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent;-webkit-text-decoration-skip:objects}a:hover{color:#0056b3;text-decoration:underline}a:not([href]):not([tabindex]){color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus,a:not([href]):not([tabindex]):hover{color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus{outline:0}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto;-ms-overflow-style:scrollbar}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg:not(:root){overflow:hidden}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}[type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=date],input[type=datetime-local],input[type=month],input[type=time]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important}
|
||||
/*# sourceMappingURL=bootstrap-reboot.min.css.map */
|
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,116 @@
|
|||
body { font-family: sans-serif }
|
||||
textarea, pre { font-family: monospace }
|
||||
#header {
|
||||
height: 72px;
|
||||
border-bottom: #333333 solid 1px;
|
||||
}
|
||||
#footer {
|
||||
border-top: #333333 solid 1px;
|
||||
padding-top: 10px;
|
||||
margin-top: 10px;
|
||||
font-size: 80%;
|
||||
}
|
||||
#logo {
|
||||
float: right;
|
||||
}
|
||||
h1 { font-size: 200%;
|
||||
color: blue
|
||||
}
|
||||
h2 { font-size: 130% }
|
||||
|
||||
img {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
/* http://www.longren.org/wrapping-text-inside-pre-tags/ */
|
||||
pre {
|
||||
overflow-x: auto; /* Use horizontal scroller if needed */
|
||||
white-space: pre-wrap; /* CSS3 */
|
||||
white-space: -moz-pre-wrap !important; /* Mozilla */
|
||||
white-space: -pre-wrap; /* Opera 4-6 */
|
||||
white-space: -o-prep-wrap; /* Opera 7 */
|
||||
word-wrap: break-word; /* IE 5.5+ */
|
||||
width: 99%;
|
||||
}
|
||||
|
||||
.mark { color: red }
|
||||
.error { color: red }
|
||||
.warning { color: #ff3333 }
|
||||
#debug { color: #cccccc; font-size: 80% }
|
||||
|
||||
h4 {
|
||||
color: #000033;
|
||||
background-color: #ccccff;
|
||||
border: 1px dashed gray;
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
.edit h4 {
|
||||
background-color: #cccccc;
|
||||
}
|
||||
|
||||
.edit h5 {
|
||||
border: 1px dotted gray;
|
||||
padding: 2px;
|
||||
background-color: #eeeeee;
|
||||
}
|
||||
|
||||
.state, .timestamp {
|
||||
color: #cccccc;
|
||||
font-size: 70%;
|
||||
}
|
||||
|
||||
.appnote {
|
||||
color: #888888;
|
||||
font-size: 70%;
|
||||
}
|
||||
|
||||
.approval {
|
||||
background-color: #eeeeee;
|
||||
border: 1px dotted gray;
|
||||
padding-left: 5px;
|
||||
padding-bottom: 5px;
|
||||
font-size: 70%;
|
||||
}
|
||||
|
||||
.approval_header {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.state-rejected {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.state-approved {
|
||||
color: green;
|
||||
}
|
||||
|
||||
.date_picker {
|
||||
padding: 3px;
|
||||
margin-right: 10px;
|
||||
cursor: default;
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
/* -moz-user-select: none; */
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.arrow_next {
|
||||
background-image: url('/main/diary/diary_next.png');
|
||||
background-repeat: no-repeat;
|
||||
display: inline-block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.arrow_prev {
|
||||
background-image: url('/main/diary/diary_prev.png');
|
||||
background-repeat: no-repeat;
|
||||
display: inline-block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
cursor: pointer;
|
||||
}
|
|
@ -0,0 +1,427 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
|
||||
<head>
|
||||
<link rel="stylesheet" type="text/css" href="/main/diary/css/bootstrap.min.css" />
|
||||
<link rel="stylesheet" type="text/css" href="/main/diary/favicon.css" />
|
||||
<title>Diary Management
|
||||
</title>
|
||||
<script type="text/javascript" src="/main/diary/diary.js">
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container mx-auto" style="width: 80%; margin-left: ($spacer * .25) !important">
|
||||
<header _id="header">
|
||||
</header>
|
||||
<!-- Add new entry -->
|
||||
<div id="new" _id="new" style="border-width: .2rem; border: solid #f7f7f9; padding: 1.5rem; background-color: #e0eefa;">
|
||||
<form method="post" action="#" _id="edit">
|
||||
<h3>New diary record:
|
||||
</h3>
|
||||
<div class="form-row">
|
||||
<input type="hidden" name="do" value="diary" />
|
||||
<input type="hidden" name="diary:edit:id" _id="id" />
|
||||
<input type="hidden" _id="hiddens" />
|
||||
|
||||
<div class="form-group col-xs-3">
|
||||
<label for="date">When</label>
|
||||
<span class="arrow_prev" onclick="change_date('diary:edit:ddate', -1)">
|
||||
</span>
|
||||
<span class="form-inline" _id="date">
|
||||
</span>
|
||||
<span class="arrow_next" onclick="change_date('diary:edit:ddate', 1)">
|
||||
</span>
|
||||
</div>
|
||||
<div class="from-group col-xs-2">
|
||||
<label for="person">Engineer</label>
|
||||
<select name="diary:edit:who" class="custom-select">
|
||||
<option _id="person">Engineer
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="from-group col-xs-3">
|
||||
<label for="hours">Hours</label>
|
||||
<input _id="hours" autocomplete="off" type="text" size="2" name="diary:edit:hours" class="form-control" />
|
||||
</div>
|
||||
<div class="form-group col-xs-3">
|
||||
<label for="project">Project</label>
|
||||
<select name="diary:edit:prj_id" class="custom-select">
|
||||
<option class="btn btn-primary" _id="project">Project
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div style="width: 800px; overflow: auto; ">
|
||||
<textarea name="diary:edit:descr" cols="70" rows="10" class="form-control"></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit" value="Submit" class="btn btn-primary btn-sm">Submit / Ctrl+Enter</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div>
|
||||
<form method="post" action="#">
|
||||
<div id="massapproval" _id="massapproval">
|
||||
<button type="submit" name="diary:action:verb" value="ApproveAll" class="btn btn-primary btn-sm" _id="edit">Approve All
|
||||
</button>
|
||||
<button type="submit" name="diary:action:verb" value="DenyAll" class="btn btn-warning btn-sm" _id="edit">Deny All
|
||||
</button>
|
||||
<button type="submit" name="diary:action:verb" value="RejectAll" class="btn btn-danger btn-sm" _id="edit">Reject All
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Entries to be approved -->
|
||||
<div id="approvals" _id="approvals">
|
||||
<h3 class="display-4">Records for approval
|
||||
</h3>
|
||||
<hr class="my-4">
|
||||
<div _id="entries">
|
||||
<div class="input-group mb-3">
|
||||
<form method="post" _id="view" action="#">
|
||||
|
||||
<input type="hidden" name="do" value="diary" />
|
||||
<input type="hidden" name="diary:action:id" _id="edit_id" />
|
||||
<input type="hidden" _id="hiddens" />
|
||||
<div class="alert alert-info">
|
||||
<span _id="date">01 Jan 1970
|
||||
</span>
|
||||
<span _id="person">Engineer name
|
||||
</span>
|
||||
<strong><span _id="hours">100
|
||||
</span></strong> hour(s);
|
||||
Project
|
||||
<span _id="project_name">Project name
|
||||
</span>
|
||||
<span class="badge badge-primary badge-pill" _id="project">customer/tag
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div style="width: 800px; overflow: auto; margin-left: 10px;">
|
||||
<pre _id="descr">Work description</pre>
|
||||
</div>
|
||||
<div class="timestamp badge">Status:
|
||||
<span class="badge badge-warning badge-pill" _id="state">valid
|
||||
</span> created
|
||||
<span _id="created">2006-10-23 10:20:00
|
||||
</span>, modified
|
||||
<span _id="modified">2006-10-24 15:17:00
|
||||
</span>
|
||||
</div>
|
||||
<div _id="appnote" class="form-control">
|
||||
<span _id="created">01 10 1970
|
||||
</span>
|
||||
<span _id="author">Author
|
||||
</span>:
|
||||
<span _id="note">Approval note
|
||||
</span>
|
||||
</div>
|
||||
<p>
|
||||
<textarea name="diary:edit:approval" cols="70" rows="5" class="form-control"></textarea>
|
||||
</p>
|
||||
<p>
|
||||
<button type="submit" name="diary:action:verb" value="Approve" class="btn btn-success btn-sm" _id="edit">Approve
|
||||
</button>
|
||||
<button type="submit" name="diary:action:verb" value="Deny" class="btn btn-warning btn-sm" _id="edit">Deny
|
||||
</button>
|
||||
<button type="submit" name="diary:action:verb" value="Reject" class="btn btn-danger btn-sm" _id="edit">Reject
|
||||
</button>
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Selection menu -->
|
||||
<div id="menu" _id="menu" style="border-width: .2rem; border: solid #f7f7f9; padding: 1.5rem; background-color: #e0eefa;">
|
||||
<form method="get" action="#">
|
||||
<h3>Select diary records to list:
|
||||
</h3>
|
||||
<input type="hidden" name="do" value="diary">
|
||||
<div class="form-row">
|
||||
<div class="form-group col-xs-3">
|
||||
<label for="start_date">From</label>
|
||||
<span class="form-inline" _id="start_date">
|
||||
</span>
|
||||
<small class="form-text text-muted"># - current day or year</small>
|
||||
</div>
|
||||
<div class="form-group col-xs-3">
|
||||
<label for="end_date">To</label>
|
||||
<span class="form-inline" _id="end_date">
|
||||
</span>
|
||||
<small class="form-text text-muted">$ - end of month</small>
|
||||
</div>
|
||||
<div class="form-group col-xs-3">
|
||||
<label for="project">Project</label>
|
||||
<select name="diary:list:prj_id" class="custom-select">
|
||||
<option class="btn btn-primary" _id="project">Project
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-xs-3">
|
||||
<select name="diary:list:customer" class="custom-select">
|
||||
<option _id="customer">Customer
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group col-xs-3">
|
||||
<select name="diary:list:who" class="custom-select">
|
||||
<option _id="who">Engineer
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-check form-group">
|
||||
<span _id="newest_first"></span>
|
||||
<label class=" form-check-label" for="newest_first">Newest first</label>
|
||||
</div>
|
||||
<div class="form-check form-group">
|
||||
<span _id="just_stats"></span>
|
||||
<label class="form-check-label" for="just_stats">Just stats</label>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary btn-sm">Search
|
||||
</button>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div _id="nodata" id="nodata">
|
||||
No entries
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- Summary -->
|
||||
<div _id="detailed_totals" id="detailed_totals">
|
||||
<h3>Summary
|
||||
</h3>
|
||||
<p>Total hours:
|
||||
<span _id="hours">100
|
||||
</span>
|
||||
</p>
|
||||
<h4>By projects:
|
||||
</h4>
|
||||
<ul>
|
||||
<li _id="customer">
|
||||
<span _id="name">Name
|
||||
</span>,
|
||||
<span _id="hours">1000
|
||||
</span> hour(s)
|
||||
<ul>
|
||||
<li _id="project">
|
||||
<span _id="name">Name</span> (<span _id="customer">customer</span>/<span _id="tag">tag</span>),
|
||||
<span _id="hours">100</span> hour(s)
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<h4>By engineers:
|
||||
</h4>
|
||||
<table class="table table-striped table-hover table-sm">
|
||||
<caption>Lag: days it took to submit the records, Mon,Tue etc. : average number of hours per day in the selected period (vacations are not handled)</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Engineer</th>
|
||||
<th scope="col">Hours</th>
|
||||
<th scope="col">Lag</th>
|
||||
<th scope="col">Mon</th>
|
||||
<th scope="col">Tue</th>
|
||||
<th scope="col">Wed</th>
|
||||
<th scope="col">Thu</th>
|
||||
<th scope="col">Fri</th>
|
||||
<th scope="col">Sat</th>
|
||||
<th scope="col">Sun</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr _id="person">
|
||||
<th scope="row" _id="name"> </th>
|
||||
<td _id="hours"> </td>
|
||||
<td _id="delay"> </td>
|
||||
<td _id="mon_stat"> </td>
|
||||
<td _id="tue_stat"> </td>
|
||||
<td _id="wed_stat"> </td>
|
||||
<td _id="thu_stat"> </td>
|
||||
<td _id="fri_stat"> </td>
|
||||
<td _id="sat_stat"> </td>
|
||||
<td _id="sun_stat"> </td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div _id="totals" id="totals">
|
||||
<h3>Summary
|
||||
</h3>
|
||||
<p>Total hours:
|
||||
<span _id="hours">100
|
||||
</span>
|
||||
</p>
|
||||
<h4>By projects:
|
||||
</h4>
|
||||
<ul>
|
||||
<li _id="customer">
|
||||
<span _id="name">Name
|
||||
</span>,
|
||||
<span _id="hours">1000
|
||||
</span> hour(s)
|
||||
<ul>
|
||||
<li _id="project">
|
||||
<span _id="name">Name</span> (<span _id="customer">customer</span>/<span _id="tag">tag</span>),
|
||||
<span _id="hours">100</span> hour(s)
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<h4>By engineers:
|
||||
</h4>
|
||||
<ul>
|
||||
<li _id="person">
|
||||
<span _id="name">Person
|
||||
</span>,
|
||||
<span _id="hours">100
|
||||
</span> hour(s)
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- Details -->
|
||||
<div _id="details" id="details">
|
||||
<h3>Details
|
||||
</h3>
|
||||
<div _id="entries">
|
||||
<form _id="view_only" style="margin-top: 3px;" class="card">
|
||||
<div class="card-body">
|
||||
<div class="card-title">
|
||||
<span _id="date">01 Jan 1970
|
||||
</span>:
|
||||
<span _id="person">Engineer name
|
||||
</span>
|
||||
<span _id="hours">100 hour(s)
|
||||
</span>;
|
||||
Project
|
||||
<span _id="project_name">Project name
|
||||
</span>
|
||||
<span class="badge badge-success badge-pill" _id="project">customer/tag
|
||||
</span>
|
||||
</div>
|
||||
<div style="width: 900px; overflow: auto;" class="card-body">
|
||||
<pre _id="descr">Work description</pre>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<form method="post" _id="view" style="margin-top: 3px;" class="card">
|
||||
<div class="card-body">
|
||||
<div class="card-title">
|
||||
<input type="hidden" name="do" value="diary" />
|
||||
<input type="hidden" name="diary:list:edit" _id="edit_id" />
|
||||
<input type="hidden" name="diary:action:id" _id="edit_id" />
|
||||
<input type="hidden" _id="hiddens" />
|
||||
<span _id="date">01 Jan 1970
|
||||
</span>
|
||||
<span _id="person">Engineer name
|
||||
</span>
|
||||
<span _id="hours">100 h
|
||||
</span>
|
||||
<span _id="project_name">Project name
|
||||
</span>
|
||||
<span class="badge badge-success badge-pill" _id="project">customer/tag
|
||||
</span>
|
||||
<button type="submit" name="diary:action:verb" value="Edit" class="btn btn-outline-primary btn-sm" _id="edit">Edit
|
||||
</button>
|
||||
<button type="submit" name="diary:action:verb" value="Delete" class="btn btn-outline-secondary btn-sm" _id="edit">Delete
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body" style="width: 900px; overflow: auto;">
|
||||
<pre _id="descr">Work description</pre>
|
||||
</div>
|
||||
<div _id="appnote" class="appnote">
|
||||
<p>
|
||||
<span _id="created">01 10 1970
|
||||
</span>
|
||||
<span _id="author">Author
|
||||
</span>:
|
||||
<span _id="note">Approval note
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="timestamp card-subtitle"><small>Status:
|
||||
<span _id="state">valid
|
||||
</span>; created
|
||||
<span _id="created">2006-10-23 10:20:00
|
||||
</span>, modified
|
||||
<span _id="modified">2006-10-24 15:17:00
|
||||
</span>
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<form method="post" _id="edit" style="margin-top: 3px;" class="card">
|
||||
<div class="edit">
|
||||
<h5>Edit diary record:
|
||||
</h5>
|
||||
<div class="form-row">
|
||||
<input type="hidden" name="do" value="diary" />
|
||||
<input type="hidden" name="diary:edit:id" _id="id" />
|
||||
<input type="hidden" _id="hiddens" />
|
||||
|
||||
<div class="form-group col-xs-3">
|
||||
<span class="form-inline" _id="date">
|
||||
</span>
|
||||
</div>
|
||||
<div class="form-group col-xs-2">
|
||||
<select name="diary:edit:who" class="custom-select">
|
||||
<option _id="person">
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group col-xs-3">
|
||||
<input _id="hours" type="text" size="2" name="diary:edit:hours" class="form-control" />
|
||||
</div>
|
||||
<div class="form-group col-xs-3">
|
||||
<select name="diary:edit:prj_id" class="custom-select">
|
||||
<option _id="project">Project 1
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div style="width: 800px; overflow: auto; ">
|
||||
<textarea name="diary:edit:descr" cols="70" rows="10" _id="descr" class="form-control">Work description</textarea>
|
||||
</div>
|
||||
<div _id="approval">
|
||||
<div _id="appnote" class="form-control form-inline">
|
||||
<span _id="created">01 10 1970
|
||||
</span>
|
||||
<span _id="author">Author
|
||||
</span>:
|
||||
<span _id="note">Approval note
|
||||
</span>
|
||||
</div>
|
||||
<p>
|
||||
<textarea name="diary:edit:approval" cols="70" rows="10">Approval note</textarea>
|
||||
</p>
|
||||
</div>
|
||||
<p>
|
||||
<button type="submit" value="Submit" class="btn btn-primary btn-sm">Submit / Ctrl+Enter</button>
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<span _id="footer">
|
||||
</span>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,32 @@
|
|||
/* $Id$ */
|
||||
|
||||
/**
|
||||
* Update data in input elements for date by shifting it by @p days days.
|
||||
*
|
||||
* @param prefix Common prefix in input names, e.g. 'diary:edit:ddate'
|
||||
* @param days Size of value shift, in days
|
||||
*/
|
||||
function change_date(prefix, days) {
|
||||
var d = document.getElementsByName(prefix + ':day')[0];
|
||||
var m = document.getElementsByName(prefix + ':month')[0];
|
||||
var y = document.getElementsByName(prefix + ':year')[0];
|
||||
var z = new Date(y.value, m.value - 1, d.value);
|
||||
z.setTime(z.getTime() + 86400000 * days);
|
||||
d.value = z.getDate();
|
||||
m.value = z.getMonth() + 1;
|
||||
y.value = z.getFullYear();
|
||||
}
|
||||
|
||||
document.addEventListener('keypress', logKey);
|
||||
|
||||
function logKey(e) {
|
||||
if (e.ctrlKey && e.keyCode == 10) {
|
||||
var target = e.target
|
||||
if (target.form)
|
||||
target.form.submit()
|
||||
} else if (e.ctrlKey && e.keyCode == 13) {
|
||||
var target = e.target
|
||||
if (target.form)
|
||||
target.form.submit()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,773 @@
|
|||
# SPDX-License-Identifier: Apache-2.0
|
||||
# Copyright (C) 2021 OKTET Labs Ltd. All rights reserved.
|
||||
|
||||
#
|
||||
# Class Diary for Diary Management Application.
|
||||
#
|
||||
|
||||
require_relative 'ldap_record'
|
||||
require_relative 'project'
|
||||
require_relative 'diary_policy'
|
||||
require_relative 'diary_datamapper'
|
||||
|
||||
DIARY_TEMPLATE = AmritaTemplate.new("diary.html")
|
||||
|
||||
def debug_log(msg)
|
||||
STDERR.puts "DEBUG: " + msg
|
||||
end
|
||||
|
||||
def show_action_button(value, name)
|
||||
button = "<button type=\"submit\" name=\"diary:action:verb\" "
|
||||
button += "value=\"" + value + "\""
|
||||
button += " class=\"btn btn-primary btn-sm\" _id=\"edit\">" + name + "</button>"
|
||||
|
||||
return button
|
||||
end
|
||||
|
||||
def show_checkbox(name, checked)
|
||||
button = "<input type=\"checkbox\" name=\"diary:list:" + name + "\" class=\"form-check-input\" _id=\"" + name + "\" "
|
||||
button += "checked" if checked[0] == "on"
|
||||
button += ">"
|
||||
|
||||
return noescape { button }
|
||||
end
|
||||
|
||||
def transform_links(prj, text)
|
||||
if prj and text
|
||||
text = prj.transform_links(text)
|
||||
end
|
||||
|
||||
return noescape { text }
|
||||
end
|
||||
|
||||
# All timestamps in database are in UTC, so we convert them to localtime
|
||||
# before printing if they are valid
|
||||
class DbTime
|
||||
@@offset = DateTime.now.offset
|
||||
|
||||
def DbTime.now
|
||||
# DBI used previously DBI::Timestamp for mapping SQL DATETIME,
|
||||
# now it is deprecated, DateTime is used
|
||||
if DBI::VERSION >= "0.4"
|
||||
DateTime.now.new_offset(0)
|
||||
else
|
||||
Time.now.utc
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(t)
|
||||
@time = t
|
||||
end
|
||||
|
||||
def to_s
|
||||
if @time.is_a? DBI::Timestamp
|
||||
@time.to_time.localtime.strftime("%Y-%m-%d %H:%M:%S")
|
||||
elsif @time.is_a? DateTime and @time.mjd > 0
|
||||
@time.new_offset(@@offset).strftime("%Y-%m-%d %H:%M:%S")
|
||||
else
|
||||
"<N/A>"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class DiaryState
|
||||
NORMAL = 0
|
||||
REQ_APPROVAL = 1
|
||||
APPROVED = 2
|
||||
REJECTED = 3
|
||||
DENIED = 4
|
||||
|
||||
def DiaryState.valid(value)
|
||||
value.to_i == NORMAL || value.to_i == APPROVED
|
||||
end
|
||||
|
||||
def initialize(x)
|
||||
@value = x
|
||||
end
|
||||
|
||||
def to_s
|
||||
case @value
|
||||
when NORMAL then "regular"
|
||||
when REQ_APPROVAL then "requires approval"
|
||||
when APPROVED then "approved"
|
||||
when REJECTED then "rejected"
|
||||
when DENIED then "denied"
|
||||
else "unknown"
|
||||
end
|
||||
end
|
||||
|
||||
def to_html
|
||||
state_name = to_s
|
||||
@value == NORMAL ? state_name :
|
||||
Amrita.e(:span, :class =>
|
||||
"state-" + state_name.tr_s(' ','-')){ state_name }
|
||||
end
|
||||
end
|
||||
|
||||
class DiaryMapper < DataMapper
|
||||
def initialize
|
||||
super("diary")
|
||||
end
|
||||
|
||||
def instantiate(id, attributes)
|
||||
Diary.new(id, attributes)
|
||||
end
|
||||
end
|
||||
|
||||
class DiaryPerson
|
||||
attr_reader :person
|
||||
attr_accessor :hours
|
||||
attr_accessor :records
|
||||
attr_accessor :lag
|
||||
attr_accessor :week
|
||||
|
||||
def initialize(person)
|
||||
@person = person
|
||||
@hours = 0
|
||||
@records = 0
|
||||
@lag = 0
|
||||
@week = Hash.new
|
||||
Date::ABBR_DAYNAMES.each { |x| @week[x] = {:hours => 0, :records => 0} }
|
||||
end
|
||||
|
||||
def register(hours, ddate, created)
|
||||
@hours += hours.to_i
|
||||
@records += 1
|
||||
|
||||
# We assume that 20:00 is the rough time people submit the diary
|
||||
@lag += (created - (ddate + Rational(20, 24)))
|
||||
|
||||
week_day_name = Date::ABBR_DAYNAMES[ddate.wday]
|
||||
@week[week_day_name][:hours] += hours.to_i
|
||||
@week[week_day_name][:records] += 1
|
||||
end
|
||||
|
||||
def lag_to_html
|
||||
average_lag = (@lag / @records).round(1).to_f
|
||||
average_lag = 0 if average_lag < 0
|
||||
|
||||
if average_lag > 5
|
||||
css_style = 'text-danger'
|
||||
elsif average_lag > 2
|
||||
css_style = 'text-warning'
|
||||
elsif average_lag < 0.3
|
||||
css_style = 'text-muted'
|
||||
else
|
||||
css_style = 'text-body'
|
||||
end
|
||||
|
||||
noescape { "<span class=\"" + css_style + "\">" + average_lag.to_s + "</span>" }
|
||||
end
|
||||
|
||||
def week_to_html
|
||||
out = ""
|
||||
@week.each { |key, record|
|
||||
if record[:hours] > 0
|
||||
out = out + key + ": " + record[:hours].to_s + " "
|
||||
end
|
||||
}
|
||||
out
|
||||
end
|
||||
|
||||
def day_to_html(day, d_start, d_end)
|
||||
# calculate number of days between s_date and e_date
|
||||
day_number = DateTime.parse(day).wday
|
||||
days_list = (d_start..d_end).to_a.select { |d| day_number == d.wday }
|
||||
number_of_days = days_list.count
|
||||
|
||||
if not @week.has_key?(day) or @week[day][:records] == 0
|
||||
""
|
||||
else
|
||||
Rational(@week[day][:hours], number_of_days).round(+1).to_f.to_s
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class Diary
|
||||
@@mapper = nil
|
||||
def self.mapper
|
||||
return @@mapper unless @@mapper.nil?
|
||||
@@mapper = DiaryMapper.new
|
||||
end
|
||||
|
||||
def initialize(id = nil, values = nil)
|
||||
@id = id
|
||||
@attributes = values ? values : Hash.new
|
||||
@approval_note = nil
|
||||
end
|
||||
|
||||
def self.get(id)
|
||||
mapper.find(id)
|
||||
end
|
||||
|
||||
def project
|
||||
@attributes.has_key?("prj_id") ?
|
||||
Project.get(@attributes["prj_id"]) : nil
|
||||
end
|
||||
|
||||
def who
|
||||
Person.find_or_create(@attributes["who"])
|
||||
end
|
||||
|
||||
def approval_note
|
||||
policy = DiaryEnv.instance.policy
|
||||
return [] if (not policy.can_edit?(project, who) and
|
||||
not policy.can_approve?(project, who)) or
|
||||
DiaryState.valid(@attributes["state"].to_i)
|
||||
|
||||
@approval_note = DataMapper.database.select_all(
|
||||
"select * from approval_note where id = ?", @id
|
||||
).collect { |row| row.to_h }
|
||||
return @approval_note
|
||||
end
|
||||
|
||||
def self.approve_all(prj, who)
|
||||
DataMapper.database.do("update diary set state = ? where prj_id = ? " +
|
||||
"AND who = ? AND state IN (?,?)", DiaryState::APPROVED,
|
||||
prj.id, who, DiaryState::REQ_APPROVAL, DiaryState::REJECTED)
|
||||
end
|
||||
|
||||
def destroy
|
||||
end
|
||||
|
||||
def [](tag)
|
||||
@attributes[tag]
|
||||
end
|
||||
|
||||
def []=(tag, value)
|
||||
@attributes[tag] = value
|
||||
end
|
||||
|
||||
def self.predict_project(who)
|
||||
# TODO: select the most used of N last entries
|
||||
last_diary = DataMapper.database.select_one(
|
||||
"SELECT prj_id FROM diary " +
|
||||
"WHERE who=? ORDER BY ddate DESC " +
|
||||
"LIMIT 1", who.uid)
|
||||
return last_diary ? last_diary[0].to_i : nil
|
||||
end
|
||||
|
||||
def self.find(args)
|
||||
who = args[:who]
|
||||
prj_id = args[:prj_id]
|
||||
customer = args[:customer]
|
||||
policy = DiaryEnv.instance.policy
|
||||
|
||||
dates = args[:ddate]
|
||||
raise ArgumentError unless dates.is_a? Range
|
||||
query_clause = " where ddate >= ? and ddate <= ?"
|
||||
query_args = [dates.first, dates.last]
|
||||
|
||||
if policy.restriction.include?(:project)
|
||||
prj_list = policy.project_list.collect {|prj| prj.id }
|
||||
prj_list = prj_list & [prj_id] if prj_id
|
||||
return [] if prj_list.empty?
|
||||
query_clause += " AND prj_id IN (" +
|
||||
prj_list.collect { "?" }.join(",") + ")"
|
||||
query_args += prj_list
|
||||
elsif prj_id
|
||||
query_clause += " AND prj_id = ?"
|
||||
query_args = query_args << prj_id
|
||||
end
|
||||
|
||||
if policy.restriction.include?(:engineer)
|
||||
eng_list = policy.engineer_list.collect {|eng| eng.uid }
|
||||
eng_list = eng_list & [who] if who
|
||||
return [] if eng_list.empty?
|
||||
query_clause += " AND who IN (" +
|
||||
eng_list.collect { "?" }.join(",") + ")"
|
||||
query_args += eng_list
|
||||
elsif who
|
||||
query_clause += " AND who = ?"
|
||||
query_args = query_args << who
|
||||
end
|
||||
|
||||
if customer
|
||||
query_clause += " AND customer = ?"
|
||||
query_args = query_args << customer
|
||||
end
|
||||
|
||||
DataMapper.database.select_all("select diary.*, project.customer, " +
|
||||
" project.leader, project.manager, diary.id as id" +
|
||||
", diary.state as state" +
|
||||
" from diary left join project on diary.prj_id = project.id" +
|
||||
query_clause + " order by ddate, prj_id, who",
|
||||
*query_args).collect do |row|
|
||||
self.new(row["id"], row.to_h)
|
||||
end
|
||||
end
|
||||
|
||||
def self.for_approve(who)
|
||||
DataMapper.database.select_all("select *, diary.id as id from" +
|
||||
" diary inner join approval on approval.who = diary.who and" +
|
||||
" approval.prj_id = diary.prj_id left join project on" +
|
||||
" diary.prj_id = project.id where approval.approver = ?" +
|
||||
" and diary.state = ? order by diary.ddate",
|
||||
who.uid, DiaryState::REQ_APPROVAL).collect do |row|
|
||||
self.new(row["id"], row.to_h)
|
||||
end
|
||||
end
|
||||
|
||||
def self.add_approval_note(id, message)
|
||||
if message.is_a?(String) and message.length > 0
|
||||
DataMapper.database.do(
|
||||
"insert into approval_note(id,created,author,note) " +
|
||||
"values (?,?,?,?)", id, DbTime.now,
|
||||
DiaryEnv.instance.user.uid, message)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class DiaryUI
|
||||
def initialize(cgi)
|
||||
@cgi = cgi
|
||||
@user = DiaryEnv.instance.user
|
||||
@policy = DiaryEnv.instance.policy
|
||||
@diary_table = SQL_Cache.new(DataMapper.database, "diary", "")
|
||||
@projects = nil
|
||||
end
|
||||
|
||||
# Array of projects suitable for diary entries
|
||||
def projects
|
||||
return @projects unless @projects.nil?
|
||||
@projects = Project.all(:prj_status => 'ACTIVE')
|
||||
end
|
||||
|
||||
def project_list
|
||||
projects.sort.collect do |prj|
|
||||
[prj.id, prj.to_s]
|
||||
end
|
||||
end
|
||||
|
||||
def customer_list
|
||||
projects.collect {|prj| prj.customer}.uniq.sort.collect do |org|
|
||||
[org.uid, org.to_s]
|
||||
end
|
||||
end
|
||||
|
||||
def show_total(list, d_start, d_end, detailed)
|
||||
totals = Hash.new
|
||||
totals[:customer] = Array.new
|
||||
totals[:person] = Array.new
|
||||
totals[:hours] = 0
|
||||
|
||||
persons = Hash.new
|
||||
projects = Hash.new
|
||||
customers = Hash.new
|
||||
list.each do |x|
|
||||
who = Person.find_or_create(x["who"])
|
||||
prj = Project.get(x["prj_id"])
|
||||
next if not @policy.can_account?(prj)
|
||||
if DiaryState.valid(x["state"].to_i)
|
||||
projects[prj] =
|
||||
projects[prj].to_i + x["hours"].to_i
|
||||
if not persons[who]
|
||||
persons[who] = DiaryPerson.new(who)
|
||||
end
|
||||
persons[who].register(x["hours"], x["ddate"], x["created"])
|
||||
totals[:hours] += x["hours"].to_i
|
||||
end
|
||||
end
|
||||
projects.sort.each do |item|
|
||||
prj = item[0]
|
||||
org = Organization.find(prj["customer"])
|
||||
customers[org] = Array.new unless customers[org]
|
||||
customers[org].push({:name => prj["name"],
|
||||
:customer => prj["customer"],
|
||||
:tag => prj["tag"],
|
||||
:hours => item[1]})
|
||||
end
|
||||
customers.sort.each do |item|
|
||||
hours = item[1].inject(0) { |sum, value| sum += value[:hours] }
|
||||
totals[:customer].push({:name => item[0].to_s,
|
||||
:hours => hours,
|
||||
:project => item[1]})
|
||||
end
|
||||
if detailed
|
||||
persons.sort.each do |item|
|
||||
dp = item[1]
|
||||
totals[:person].push({:name => item[0].to_html,
|
||||
:hours => dp.hours,
|
||||
:delay => dp.lag_to_html,
|
||||
:mon_stat => dp.day_to_html('Mon', d_start, d_end),
|
||||
:tue_stat => dp.day_to_html('Tue', d_start, d_end),
|
||||
:wed_stat => dp.day_to_html('Wed', d_start, d_end),
|
||||
:thu_stat => dp.day_to_html('Thu', d_start, d_end),
|
||||
:fri_stat => dp.day_to_html('Fri', d_start, d_end),
|
||||
:sat_stat => dp.day_to_html('Sat', d_start, d_end),
|
||||
:sun_stat => dp.day_to_html('Sun', d_start, d_end),
|
||||
})
|
||||
end
|
||||
else
|
||||
persons.sort.each do |item|
|
||||
totals[:person].push({:name => item[0].to_html,
|
||||
:hours => item[1].hours,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
return totals[:hours] > 0 ? totals : nil
|
||||
end
|
||||
private :show_total
|
||||
|
||||
def show_approval_notes(entry)
|
||||
return nil if entry.approval_note.empty?
|
||||
entry.approval_note.collect do |note|
|
||||
{
|
||||
:created => DbTime.new(note["created"]).to_s,
|
||||
:author => note["author"],
|
||||
:note => note["note"]
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def show_one(values = nil, edit = false, approve = false)
|
||||
if (values == nil)
|
||||
edit = add = true
|
||||
values = Diary.new
|
||||
else
|
||||
add = false
|
||||
edit = false if values["state"].to_i == DiaryState::DENIED
|
||||
end
|
||||
edit ||= add
|
||||
|
||||
# Set project to the last edited one (for new entries)
|
||||
if not values.project
|
||||
values["prj_id"] = Diary.predict_project(@user)
|
||||
end
|
||||
|
||||
prj = Project.get(values["prj_id"].to_i)
|
||||
if edit
|
||||
edit_data = {
|
||||
:hiddens => @cgi.hiddens(@cgi.tree_flatten(["diary",
|
||||
"list"]).delete_if do |x|
|
||||
x == ["diary:list:edit", values["id"].to_s]
|
||||
end),
|
||||
:date => MyCGI::select_date_e("diary:edit:ddate",
|
||||
values["ddate"] || Date.today, edit),
|
||||
:person => MyCGI::list_options(
|
||||
@policy.engineer_list.collect {|x| [x.uid, x.to_s]},
|
||||
(values["who"] || @user.uid)),
|
||||
:hours => a(:value => values["hours"]),
|
||||
:project => MyCGI::list_options(project_list,
|
||||
prj ? prj.id : ""),
|
||||
:descr => values["descr"]
|
||||
}
|
||||
edit_data[:id] = a(:value => values["id"]) if values["id"]
|
||||
if prj and prj.approvals.has_key?(values["who"])
|
||||
edit_data[:approval] = Hash.new
|
||||
edit_data[:approval][:appnote] = show_approval_notes(values)
|
||||
end
|
||||
return { :edit => edit_data }
|
||||
else
|
||||
view = {
|
||||
:id => a(:name => values["id"]),
|
||||
:hiddens => @cgi.hiddens(@cgi.tree_flatten(["diary", "list"])),
|
||||
:date => MyCGI::select_date_e("diary:edit:ddate",
|
||||
values["ddate"] || Date.today, edit),
|
||||
:person => Person.find_or_create(values["who"]).to_html,
|
||||
:project_name => prj["name"],
|
||||
:project => prj["customer"]+ "/" + prj["tag"],
|
||||
:descr => transform_links(prj, values["descr"]),
|
||||
:edit_id => a(:value => values["id"]),
|
||||
:modified => DbTime.new(values["modified"]).to_s,
|
||||
:created => DbTime.new(values["created"]).to_s,
|
||||
:state => DiaryState.new(values["state"].to_i).to_html
|
||||
}
|
||||
view[:hours] = values["hours"].to_s + " h" if
|
||||
@policy.can_account?(prj)
|
||||
view[:appnote] = show_approval_notes(values) if
|
||||
not values.approval_note.empty?
|
||||
if @policy.can_edit?(prj, Person.find(values["who"])) or approve
|
||||
view[:edit] = { }
|
||||
return { :view => a(:action => "#" +
|
||||
MyCGI::element_id(values["id"])){ view }}
|
||||
else
|
||||
return { :view_only => view }
|
||||
end
|
||||
end
|
||||
end
|
||||
private :show_one
|
||||
|
||||
def menu
|
||||
{
|
||||
:start_date => MyCGI::select_date_e("diary:list:start",
|
||||
@cgi.tree_params["diary"]["list"]["start"]),
|
||||
:end_date => MyCGI::select_date_e("diary:list:end",
|
||||
@cgi.tree_params["diary"]["list"]["end"]),
|
||||
:project => MyCGI::list_options([["*", "- All -"]] +
|
||||
project_list,
|
||||
@cgi.tree_params["diary"]["list"]["prj_id"]),
|
||||
:customer => MyCGI::list_options([["*", "Customer: all"]] +
|
||||
customer_list,
|
||||
@cgi.tree_params["diary"]["list"]["customer"]),
|
||||
:who => MyCGI::list_options([["*", "Engineer: all"]] +
|
||||
@policy.engineer_list.collect {|x| [x.uid, x.to_s]},
|
||||
@cgi.tree_params["diary"]["list"]["who"]),
|
||||
:newest_first => show_checkbox("newest_first", @cgi.tree_params["diary"]["list"]["newest_first"]),
|
||||
:just_stats => show_checkbox("just_stats", @cgi.tree_params["diary"]["list"]["just_stats"])
|
||||
}
|
||||
end
|
||||
|
||||
def show
|
||||
diary_data = Hash.new
|
||||
MyCGI::add_commons(diary_data)
|
||||
diary_data[:menu] = menu
|
||||
diary_data[:new] = show_one() if @user.local?
|
||||
|
||||
if @cgi.tree_params["do"] == "diary"
|
||||
d_start = MyCGI::hash2date(
|
||||
@cgi.tree_params["diary"]["list"]["start"])
|
||||
d_end = MyCGI::hash2date(
|
||||
@cgi.tree_params["diary"]["list"]["end"])
|
||||
prj_id = @cgi.tree_params["diary"]["list"]["prj_id"][0].to_i
|
||||
prj_id = nil if prj_id <= 0
|
||||
customer = @cgi.tree_params["diary"]["list"]["customer"][0]
|
||||
customer = nil if not Organization.exists?(customer)
|
||||
who = @cgi.tree_params["diary"]["list"]["who"][0].strip.downcase
|
||||
who = nil if who == "" or who == "*"
|
||||
|
||||
list_approval = Diary.for_approve(@user)
|
||||
if list_approval.length > 0
|
||||
diary_data[:massapproval] = a(:__id__)
|
||||
|
||||
diary_data[:approvals] = { :entries =>
|
||||
list_approval.inject([]) do |data, item|
|
||||
data << a(:__id__ => MyCGI::element_id(item["id"])){
|
||||
show_one(item, false, true)}
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
list = Diary.find(:who => who,
|
||||
:prj_id => prj_id,
|
||||
:customer => customer,
|
||||
:ddate => (d_start..d_end))
|
||||
list.delete_if do |entry|
|
||||
not DiaryState.valid(entry["state"]) and
|
||||
not @policy.can_edit?(entry.project, entry.who)
|
||||
end
|
||||
if list.length > 0
|
||||
if @policy.is_director?
|
||||
diary_data[:detailed_totals] = show_total(list, d_start, d_end, @policy.is_director?)
|
||||
else
|
||||
diary_data[:totals] = show_total(list, d_start, d_end, false)
|
||||
end
|
||||
|
||||
if @cgi.tree_params["diary"]["list"]["just_stats"][0] != "on"
|
||||
if @cgi.tree_params["diary"]["list"]["newest_first"][0] == "on"
|
||||
list.reverse!
|
||||
end
|
||||
diary_data[:details] = {
|
||||
:entries =>
|
||||
list.inject([]) do |data, item|
|
||||
data << a(:__id__ => MyCGI::element_id(item["id"])){
|
||||
show_one(item,
|
||||
@cgi.tree_params["diary"]["list"]["edit"].
|
||||
include?(item["id"].to_s)) }
|
||||
end
|
||||
}
|
||||
else
|
||||
diary_data[:nodata] = {}
|
||||
end
|
||||
else
|
||||
diary_data[:nodata] = {}
|
||||
end
|
||||
end
|
||||
s = String.new
|
||||
DIARY_TEMPLATE.expand(s, diary_data)
|
||||
s
|
||||
end
|
||||
|
||||
def approve_item(id, action)
|
||||
entry = @diary_table[id].dup
|
||||
raise "Entry does not exist" if not entry
|
||||
raise "You have no rights to approve diary entries for " +
|
||||
"#{Person.find(entry["who"]).to_s} in this project." if
|
||||
not @policy.can_approve?(Project.get(entry["prj_id"]),
|
||||
Person.find_or_create(entry["who"]))
|
||||
|
||||
entry["state"] = case action
|
||||
when "Approve" then DiaryState::APPROVED
|
||||
when "Deny" then DiaryState::DENIED
|
||||
when "Reject" then DiaryState::REJECTED
|
||||
end
|
||||
@diary_table.modify(id, entry)
|
||||
|
||||
return if not @cgi.tree_params["diary"].include?("edit")
|
||||
data = @cgi.tree_params["diary"]["edit"]
|
||||
Diary.add_approval_note(id, data["approval"])
|
||||
end
|
||||
|
||||
def approval_action
|
||||
id = @cgi.tree_params["diary"]["action"]["id"].to_i
|
||||
action = @cgi.tree_params["diary"]["action"]["verb"]
|
||||
|
||||
approve_item(id, action)
|
||||
end
|
||||
|
||||
def list_approval_action
|
||||
list_action = @cgi.tree_params["diary"]["action"]["verb"]
|
||||
action = case list_action
|
||||
when "ApproveAll" then "Approve"
|
||||
when "DenyAll" then "Deny"
|
||||
when "RejectAll" then "Reject"
|
||||
end
|
||||
|
||||
list_approval = Diary.for_approve(@user)
|
||||
if list_approval.length > 0
|
||||
list_approval.each do |item|
|
||||
approve_item(item["id"], action)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def action
|
||||
debug_log(@cgi.tree_params.inspect)
|
||||
|
||||
# Delete an entry
|
||||
if @cgi.tree_params["diary"]["action"]["verb"] == "Delete"
|
||||
id = @cgi.tree_params["diary"]["action"]["id"].to_i
|
||||
entry = Diary.get(id)
|
||||
raise "You are trying to delete diary entry #{id} " +
|
||||
"which does not exist." if not entry
|
||||
|
||||
raise "You have no rights to delete diary entries for " +
|
||||
"#{entry.who.to_s} in this project." if
|
||||
not @policy.can_edit?(entry.project, entry.who)
|
||||
|
||||
raise NeedConfirm.new("delete"),
|
||||
"You are deleting diary entry of " +
|
||||
"#{entry.who.to_s} for #{entry["ddate"]}!" if
|
||||
not DiaryEnv.confirmed?("delete")
|
||||
|
||||
@diary_table.delete(id)
|
||||
return
|
||||
end
|
||||
|
||||
if @cgi.tree_params["diary"]["action"]["verb"] == "ApproveAll" or
|
||||
@cgi.tree_params["diary"]["action"]["verb"] == "RejectAll" or
|
||||
@cgi.tree_params["diary"]["action"]["verb"] == "DenyAll"
|
||||
list_approval_action
|
||||
return
|
||||
end
|
||||
|
||||
|
||||
if @cgi.tree_params["diary"]["action"]["verb"] == "Approve" or
|
||||
@cgi.tree_params["diary"]["action"]["verb"] == "Reject" or
|
||||
@cgi.tree_params["diary"]["action"]["verb"] == "Deny"
|
||||
approval_action
|
||||
return
|
||||
end
|
||||
|
||||
# After adding/editing entry
|
||||
return if not @cgi.tree_params["diary"].include?("edit")
|
||||
data = @cgi.tree_params["diary"]["edit"].dup
|
||||
# normalize input
|
||||
data["who"].strip!
|
||||
data["who"].downcase!
|
||||
raise "No such person #{data["who"]} " if not Person.exists?(data["who"])
|
||||
data["prj_id"] = data["prj_id"].to_i
|
||||
prj = Project.get(data["prj_id"])
|
||||
who = Person.find(data["who"])
|
||||
raise "No such project #{data["prj_id"]}" unless prj
|
||||
|
||||
raise "Project #{prj.to_s} is terminated" if
|
||||
prj["prj_status"] == "TERMINATED"
|
||||
|
||||
raise "You have no rights to add/change diary records " +
|
||||
"for #{who.to_s}." if not @policy.can_edit?(prj, who)
|
||||
|
||||
data["hours"] = data["hours"].to_i
|
||||
data["ddate"] = MyCGI::hash2date(data["ddate"])
|
||||
|
||||
raise NeedConfirm.new("future"),
|
||||
"You're posting diary for the future" if
|
||||
data["ddate"] > Date.today and not DiaryEnv.confirmed?("future")
|
||||
|
||||
raise "Don't use non-English symbols - not all customers can read Russian" if
|
||||
not data["descr"].ascii_only?
|
||||
|
||||
raise "Please use at least 10 characters in diary entry description" if
|
||||
data["descr"].length < 10
|
||||
|
||||
# Check total hours
|
||||
total = DataMapper.database.select_one(
|
||||
"SELECT SUM(hours) FROM diary " +
|
||||
"WHERE who=? AND ddate=?" +
|
||||
(data.include?("id") ?
|
||||
" AND id<>#{data["id"]}" : ""),
|
||||
*([data["who"], data["ddate"]]))[0].to_i +
|
||||
data["hours"]
|
||||
raise "You have more than 24 hours " +
|
||||
"for #{data["ddate"]}" if total > 24
|
||||
|
||||
raise NeedConfirm.new("overwork"),
|
||||
"You have more (#{total}) than 15 hours " +
|
||||
"for #{data["ddate"]}" if
|
||||
total > 15 and not DiaryEnv.confirmed?("overwork")
|
||||
|
||||
if @policy.needs_confirmation?(prj, who)
|
||||
raise NeedConfirm.new("past"), "You are " +
|
||||
(data.include?("id") ? "editing" : "adding") +
|
||||
" a diary record for #{data["ddate"]} that is in the past" if
|
||||
data["ddate"] < Date.today and not DiaryEnv.confirmed?("past")
|
||||
end
|
||||
|
||||
raise NeedConfirm.new("progressive"),
|
||||
"You are trying to add #{total} hours " +
|
||||
"for today, while it is just #{Time.now.hour} hours now" if
|
||||
data["ddate"] == Date.today and total > Time.now.hour and
|
||||
not DiaryEnv.confirmed?("progressive")
|
||||
|
||||
data["state"] = (prj.approvals.has_key?(data["who"]) ?
|
||||
DiaryState::REQ_APPROVAL : DiaryState::NORMAL)
|
||||
|
||||
# Automatically reject creating (not editing) too old entry
|
||||
if not data.include?("id") and
|
||||
data["state"] == DiaryState::REQ_APPROVAL and
|
||||
Date.today - data["ddate"] > DiaryEnv::DIARY_MAX_AGE
|
||||
if DiaryEnv.confirmed?("too_old")
|
||||
data["state"] = DiaryState::REJECTED
|
||||
else
|
||||
raise NeedConfirm.new("too_old"),
|
||||
"Date is too old, entry will be rejected"
|
||||
end
|
||||
end
|
||||
|
||||
# Insert/replace DB entry
|
||||
data["modified"] = DbTime.now
|
||||
if data.include?("id")
|
||||
data.delete("id")
|
||||
if data["hours"].to_i == 0
|
||||
@diary_table.delete(@cgi.tree_params["diary"]["edit"]["id"])
|
||||
else
|
||||
@diary_table.modify(@cgi.tree_params["diary"]["edit"]["id"],
|
||||
data)
|
||||
end
|
||||
else
|
||||
raise "You should specify at least 1 hour" if data["hours"] < 1
|
||||
raise "You are inserting a duplicate diary record!" if
|
||||
DataMapper.database.select_one(
|
||||
"SELECT COUNT(*) FROM diary WHERE " +
|
||||
"who = ? and descr = ? and hours = ? " +
|
||||
"and ddate =?",
|
||||
*([data["who"], data["descr"],
|
||||
data["hours"], data["ddate"]]))[0].to_i > 0
|
||||
data["created"] = data["modified"]
|
||||
@diary_table.create(data)
|
||||
end
|
||||
|
||||
Diary.add_approval_note(@cgi.tree_params["diary"]["edit"]["id"].to_i,
|
||||
data["approval"]) if
|
||||
data["approval"] and data["approval"].length > 0
|
||||
|
||||
# Adjust start/end day of the list to make new record visible
|
||||
@cgi.tree_params["diary"]["list"]["start"] =
|
||||
MyCGI::date2hash(data["ddate"]) if data["ddate"] <
|
||||
MyCGI::hash2date(@cgi.tree_params["diary"]["list"]["start"])
|
||||
@cgi.tree_params["diary"]["list"]["end"] =
|
||||
MyCGI::date2hash(data["ddate"]) if data["ddate"] >
|
||||
MyCGI::hash2date(@cgi.tree_params["diary"]["list"]["end"])
|
||||
end
|
||||
end
|
|
@ -0,0 +1,108 @@
|
|||
# SPDX-License-Identifier: Apache-2.0
|
||||
# Copyright (C) 2021 OKTET Labs Ltd. All rights reserved.
|
||||
|
||||
#
|
||||
# Class DataMapper for Diary Management Application.
|
||||
#
|
||||
|
||||
require 'dbi'
|
||||
|
||||
class DataMapper
|
||||
@@mapper = Hash.new
|
||||
@@database = nil
|
||||
|
||||
def initialize(table, key = "id")
|
||||
@key = key
|
||||
@table = table
|
||||
@@mapper[@table] = self
|
||||
@cache = Hash.new
|
||||
@columns = @@database.columns(@table).collect { |col| col["name"] } - [@key]
|
||||
end
|
||||
def self.setup(args)
|
||||
raise "Database parameters is not a hash" unless args.is_a? Hash
|
||||
@@database = DBI.connect("dbi:#{args[:adapter]}:#{args[:database]}:" +
|
||||
"#{args[:host]}",
|
||||
args[:username], args[:password])
|
||||
end
|
||||
def self.database
|
||||
@@database
|
||||
end
|
||||
|
||||
def pick(row)
|
||||
row_data = row.to_h
|
||||
id = row_data[@key]
|
||||
return @cache[id] if @cache.has_key?(id)
|
||||
@cache[id] = instantiate(id, row_data)
|
||||
end
|
||||
|
||||
def find(id)
|
||||
return @cache[id] if @cache.has_key?(id)
|
||||
row = @@database.select_one("select * from #{@table} where #{@key} = ?", id)
|
||||
return nil unless row
|
||||
pick(row)
|
||||
end
|
||||
|
||||
def where(clause, *args)
|
||||
@@database.select_all("select * from #{@table} where " + clause,
|
||||
*args).collect { |row| pick(row) }
|
||||
end
|
||||
|
||||
def all(args)
|
||||
# TODO: args == nil
|
||||
raise ArgumentError unless args.is_a?(Hash)
|
||||
query_clause = Array.new
|
||||
query_args = Array.new
|
||||
args.each do |tag, value|
|
||||
next unless @columns.include?(tag.to_s) or tag.to_s == @key
|
||||
if value.is_a?(Array)
|
||||
if value.length > 0
|
||||
query_clause << "#{tag.to_s} " +
|
||||
"IN (#{value.collect {"?"}.join(",")})"
|
||||
query_args = query_args + value
|
||||
end
|
||||
elsif value.is_a?(Range)
|
||||
query_clause << "#{tag.to_s} >= ? AND #{tag.to_s} <= ?"
|
||||
query_args << value.first << value.last
|
||||
else
|
||||
query_clause << "#{tag.to_s} = ?"
|
||||
query_args << value
|
||||
end
|
||||
end
|
||||
# raise "#{MyCGI.dump(query_clause)} : #{MyCGI.dump(query_args)}"
|
||||
@@database.select_all("select * from #{@table}" +
|
||||
(query_clause.empty? ? "" :
|
||||
" where " + query_clause.join(" and ")),
|
||||
*query_args).collect { |row| pick(row) }
|
||||
end
|
||||
|
||||
def insert(obj)
|
||||
raise ArgumentError unless obj.id.nil?
|
||||
fields = @columns & obj.attributes.keys
|
||||
@@database.do("insert into #{@table} (" + fields.join(",") +
|
||||
") values (" + fields.collect {"?"}.join(",") + ")",
|
||||
*(fields.collect {|field| obj[field]}))
|
||||
obj.id = @@database.select_one("select last_insert_id()")[0]
|
||||
@@cache[obj.id] = obj
|
||||
end
|
||||
|
||||
def update(obj)
|
||||
raise ArgumentError if obj.id.nil?
|
||||
fields = @columns & obj.attributes.keys
|
||||
@@database.do("update #{@table} set " +
|
||||
fields.collect {|field| field + " = ?"}.join(",") +
|
||||
" where #{@key} = ?",
|
||||
*(fields.collect {|field| obj[field]} << obj.id))
|
||||
end
|
||||
|
||||
def delete(obj)
|
||||
raise ArgumentError if obj.id.nil?
|
||||
@@database.do("delete from #{@table} where #{@key} = ?", obj.id)
|
||||
@@cache.delete(obj.id)
|
||||
obj.id = nil
|
||||
end
|
||||
|
||||
def self.[](table)
|
||||
@@mapper[table]
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
# SPDX-License-Identifier: Apache-2.0
|
||||
# Copyright (C) 2021 OKTET Labs Ltd. All rights reserved.
|
||||
|
||||
#
|
||||
# Class DiaryEnv for Diary Management Application.
|
||||
#
|
||||
|
||||
require_relative 'ldap_record'
|
||||
require_relative 'diary_datamapper'
|
||||
|
||||
class DiaryEnv
|
||||
include Singleton
|
||||
|
||||
DB_HOST = "localhost"
|
||||
DB_USERNAME = "diary"
|
||||
DB_PASSWORD = "diary_pass"
|
||||
DB_DATABASE = "diary"
|
||||
|
||||
LDAP_HOST = 'ldap.example.com'
|
||||
LDAP_ROOT = "ou=People,dc=example,dc=com"
|
||||
|
||||
HOME_ORGANIZATION = "Example ORG"
|
||||
HOME_OU = "Employees"
|
||||
|
||||
SMTP_HOST = 'mail.example.com'
|
||||
SMTP_FROM = 'diary@example.com'
|
||||
|
||||
# Maximum age of diary entry (in days) to accept (approval mode only)
|
||||
DIARY_MAX_AGE = 2
|
||||
|
||||
def initialize
|
||||
@confirmation = Array.new
|
||||
Person.setup(:host => LDAP_HOST,
|
||||
:root => LDAP_ROOT,
|
||||
:key => "uid")
|
||||
Person.set_local(HOME_ORGANIZATION, HOME_OU)
|
||||
DataMapper.setup(:adapter => "Mysql",
|
||||
:database => DB_DATABASE,
|
||||
:host => DB_HOST,
|
||||
:username => DB_USERNAME,
|
||||
:password => DB_PASSWORD)
|
||||
DataMapper.database.execute("SELECT nick FROM director") do |q|
|
||||
raise "'Director' table is not filled" unless
|
||||
q and name = q.fetch[0]
|
||||
@director = [ Person.new('director') ]
|
||||
end
|
||||
end
|
||||
|
||||
def user=(name)
|
||||
@user = Person.find(name)
|
||||
raise "Invalid user #{name}" unless @user
|
||||
@policy = @user.employee? ? EmployeePolicy.new(@user) :
|
||||
CustomerPolicy.new(@user)
|
||||
end
|
||||
|
||||
def confirmation=(value)
|
||||
if value.is_a? Array
|
||||
@confirmation = value
|
||||
end
|
||||
end
|
||||
|
||||
attr_reader :director, :confirmation, :policy, :user
|
||||
def self.director
|
||||
instance.director
|
||||
end
|
||||
def self.confirmed?(token)
|
||||
instance.confirmation.include?(token)
|
||||
end
|
||||
end
|
Binary file not shown.
After Width: | Height: | Size: 252 B |
|
@ -0,0 +1,154 @@
|
|||
# SPDX-License-Identifier: Apache-2.0
|
||||
# Copyright (C) 2021 OKTET Labs Ltd. All rights reserved.
|
||||
|
||||
#
|
||||
# Class Policy for Diary Management Application.
|
||||
#
|
||||
|
||||
require_relative 'ldap_record'
|
||||
#require_relative 'project'
|
||||
require_relative 'diary_env'
|
||||
require_relative 'diary_datamapper'
|
||||
|
||||
# Policy for full employees
|
||||
class EmployeePolicy
|
||||
def initialize(user)
|
||||
@user = user
|
||||
end
|
||||
|
||||
def customer_list
|
||||
Organization.all.sort
|
||||
end
|
||||
|
||||
def project_list
|
||||
Project.where("1").sort
|
||||
end
|
||||
|
||||
def engineer_list
|
||||
Person.find_by_org(DiaryEnv::HOME_ORGANIZATION).sort
|
||||
end
|
||||
|
||||
# Check whether current user can edit entries for user {who}
|
||||
# in project {prj}
|
||||
def can_edit?(prj, who)
|
||||
@user == who or
|
||||
@user == self.is_director? or (prj != nil and
|
||||
(@user.uid == prj["leader"] or @user.uid == prj["manager"]))
|
||||
end
|
||||
|
||||
# Check whether current user needs confirmations when editing things in
|
||||
# the past
|
||||
def needs_confirmation?(prj, who)
|
||||
not (self.is_director? or
|
||||
(prj != nil and
|
||||
(@user.uid == prj["leader"] or @user.uid == prj["manager"])))
|
||||
end
|
||||
|
||||
def can_approve?(prj, who)
|
||||
prj.approvals[who.uid] == @user.uid
|
||||
end
|
||||
|
||||
def can_account?(prj)
|
||||
true
|
||||
end
|
||||
|
||||
def can_edit_project?(prj = nil)
|
||||
true
|
||||
end
|
||||
|
||||
def is_director?
|
||||
DiaryEnv.director.include?(@user)
|
||||
end
|
||||
|
||||
def can_edit_approval?(prj)
|
||||
#@user.director? or @user.uid == prj["leader"]
|
||||
true
|
||||
end
|
||||
|
||||
def restriction
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
# Policy for customers and contractors
|
||||
class CustomerPolicy
|
||||
def initialize(user)
|
||||
@user = user
|
||||
@extra_prj = nil
|
||||
end
|
||||
|
||||
def extra_project_list
|
||||
return @extra_prj if @extra_prj
|
||||
@extra_prj = Array.new
|
||||
Project.where("extra_rights IS NOT NULL").each do |prj|
|
||||
if not Person.find_by_filter("(&(uid=#{@user.uid})#{prj["extra_rights"]})").empty?
|
||||
@extra_prj.push(prj)
|
||||
end
|
||||
|
||||
end
|
||||
@extra_prj
|
||||
end
|
||||
|
||||
def customer_list
|
||||
([@user.organization] +
|
||||
extra_project_list.collect {|prj| prj.customer }).uniq.sort
|
||||
end
|
||||
|
||||
def project_list
|
||||
(Project.where("customer=? AND extra_rights IS NULL",
|
||||
@user.organization.uid) +
|
||||
extra_project_list).uniq.sort
|
||||
end
|
||||
|
||||
def engineer_list
|
||||
return [@user] if @user.local? # For contractors
|
||||
engs = Person.find_by_org(DiaryEnv::HOME_ORGANIZATION)
|
||||
if @user.customer?
|
||||
engs_active = Array.new
|
||||
DataMapper.database.select_all("SELECT DISTINCT who " +
|
||||
"FROM diary INNER JOIN project " +
|
||||
"ON diary.prj_id = project.id " +
|
||||
"WHERE project.id IN " +
|
||||
"(#{project_list.collect {"?"}.join(",")})",
|
||||
*(project_list.collect { |prj| prj.id })
|
||||
) do |row|
|
||||
engs_active.push(row["who"])
|
||||
end
|
||||
engs.delete_if { |x| not engs_active.include?(x.uid) }
|
||||
end
|
||||
engs.sort
|
||||
end
|
||||
|
||||
def can_account?(prj)
|
||||
not prj["hide_hrs"]
|
||||
end
|
||||
|
||||
def can_edit?(prj, who)
|
||||
@user.local? ? who == @user : false
|
||||
end
|
||||
|
||||
def needs_confirmation?(prj, who)
|
||||
return true
|
||||
end
|
||||
|
||||
def can_approve?(prj, who)
|
||||
false
|
||||
end
|
||||
|
||||
def can_edit_project?(prj = nil)
|
||||
false
|
||||
end
|
||||
|
||||
def can_edit_approval?(prj)
|
||||
false
|
||||
end
|
||||
|
||||
def is_director?
|
||||
false
|
||||
end
|
||||
|
||||
def restriction
|
||||
@user.local? ? [:project, :engineer] : [:project]
|
||||
end
|
||||
end
|
||||
|
Binary file not shown.
After Width: | Height: | Size: 256 B |
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0"?>
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<meta http-equiv="Content-Style-Type" content="text/css" />
|
||||
<link rel="stylesheet" type="text/css" href="/main/diary/css/bootstrap.min.css"/>
|
||||
<title>Diary Management</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Diary Management</h1>
|
||||
<h2 class="error">Error</h2>
|
||||
<p _id="message">Message text</p>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,17 @@
|
|||
.arrow_next {
|
||||
background - image: url('/main/diary/diary_next.png');
|
||||
background - repeat: no - repeat;
|
||||
display: inline - block;
|
||||
width: 16 px;
|
||||
height: 16 px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.arrow_prev {
|
||||
background - image: url('/main/diary/diary_prev.png');
|
||||
background - repeat: no - repeat;
|
||||
display: inline - block;
|
||||
width: 16 px;
|
||||
height: 16 px;
|
||||
cursor: pointer;
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
<div id="footer">
|
||||
With problems, contact with
|
||||
<a href="mailto:webmaster@example.com">webmaster</a>
|
||||
</div>
|
|
@ -0,0 +1,26 @@
|
|||
<nav id="header" class="nav navbar-expand bg-light nav-pills">
|
||||
<a class="navbar-brand" id="logo" href="/cgi-bin/diary/cgi.rb">
|
||||
<img src="/public/logo-small.gif" width="80" height="38" alt="Org-logo" class="d-inline-block align-top" />
|
||||
</a>
|
||||
<div class="collapse navbar-collapse">
|
||||
<ul class="navbar-nav mr-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="?do=diary#new">New record</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#approvals">Approvals</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#menu">Select</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#totals">Summary</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#details">Records</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="?do=project">Projects</a>
|
||||
</li>
|
||||
</div>
|
||||
</nav>
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,67 @@
|
|||
# SPDX-License-Identifier: Apache-2.0
|
||||
# Copyright (C) 2021 OKTET Labs Ltd. All rights reserved.
|
||||
|
||||
#
|
||||
# Class LDAP::Cache for Diary Management Application.
|
||||
#
|
||||
|
||||
require 'ldap'
|
||||
|
||||
class LDAP::Cache < Hash
|
||||
def initialize(ldap, base_dn, scope, field)
|
||||
@ldap = ldap
|
||||
@base_dn = base_dn
|
||||
@scope = scope
|
||||
@field = field
|
||||
@lists = Hash.new
|
||||
super(0)
|
||||
end
|
||||
|
||||
attr_reader :ldap, :lists
|
||||
attr_accessor :base_dn, :scope, :field
|
||||
|
||||
def search(filter)
|
||||
list = Array.new
|
||||
begin
|
||||
@ldap.search(@base_dn, @scope, filter) do |entry|
|
||||
hash = Hash.new
|
||||
entry.to_hash.each do |key, value|
|
||||
hash[key] = value[0]
|
||||
end
|
||||
self[hash[@field]] = hash
|
||||
list.push(hash)
|
||||
end
|
||||
rescue
|
||||
$stderr.printf "Failed to search in LDAP\n"
|
||||
exit (-1)
|
||||
end
|
||||
list
|
||||
end
|
||||
private :search
|
||||
|
||||
def list(filter, main_value)
|
||||
if not @lists[filter]
|
||||
@lists[filter] = search(filter).sort_by do |entry|
|
||||
entry[main_value]
|
||||
end.collect do |entry|
|
||||
[entry[@field], entry[main_value]]
|
||||
end
|
||||
|
||||
end
|
||||
@lists[filter]
|
||||
end
|
||||
|
||||
def [](key)
|
||||
if super(key) == 0
|
||||
list = search("(#{@field}=#{key})")
|
||||
if list.length != 1
|
||||
self[key] = nil
|
||||
else
|
||||
self[key] = list[0]
|
||||
end
|
||||
end
|
||||
super(key)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -0,0 +1,176 @@
|
|||
# SPDX-License-Identifier: Apache-2.0
|
||||
# Copyright (C) 2021 OKTET Labs Ltd. All rights reserved.
|
||||
|
||||
#
|
||||
# Class LdapRecord for Diary Management Application.
|
||||
#
|
||||
|
||||
require 'ldap_cache'
|
||||
require 'net/smtp'
|
||||
|
||||
class LdapRecord
|
||||
@@connection = nil
|
||||
@@cache = {}
|
||||
attr_reader :id
|
||||
|
||||
def LdapRecord.setup(args)
|
||||
raise "Invalid parameters" unless args.is_a? Hash
|
||||
raise "LDAP server is not specified" unless args[:host]
|
||||
@@ldap = LDAP::Conn.new(args[:host], args[:port] || LDAP::LDAP_PORT)
|
||||
#@@ldap.bind # Bind is optional for LDAPv3
|
||||
|
||||
raise "LDAP tree root is not specified" unless args[:root]
|
||||
raise "LDAP key attribute is not specified" unless args[:key]
|
||||
@@connection = LDAP::Cache.new(@@ldap, args[:root],
|
||||
LDAP::LDAP_SCOPE_SUBTREE, args[:key])
|
||||
end
|
||||
|
||||
def initialize(id)
|
||||
raise "LDAP connection is not established" if not @@connection
|
||||
@id = id
|
||||
@@cache[id] = self
|
||||
@attributes = @@connection[id]
|
||||
end
|
||||
|
||||
def LdapRecord.find(id)
|
||||
# entry = @@cache[id] || (exists?(id) ? new(id) : nil)
|
||||
# entry = nil if (entry && !entry.valid?)
|
||||
# entry
|
||||
return @@cache[id] if valid?(@@cache[id])
|
||||
exists?(id) ? new(id) : nil
|
||||
end
|
||||
|
||||
def LdapRecord.valid?(entry)
|
||||
entry != nil
|
||||
end
|
||||
|
||||
def LdapRecord.find_or_create(id)
|
||||
@@cache[id] || new(id)
|
||||
end
|
||||
|
||||
def LdapRecord.exists?(id)
|
||||
@@connection[id] != nil
|
||||
end
|
||||
|
||||
def LdapRecord.find_by_filter(filter)
|
||||
@@connection.list(filter, "uid").collect { |x| find(x[0]) }.find_all { |x| x }
|
||||
end
|
||||
|
||||
def method_missing(method, *args)
|
||||
@attributes ? @attributes[method.to_s] : nil
|
||||
end
|
||||
end
|
||||
|
||||
class Person < LdapRecord
|
||||
MAX_LENGTH = 30
|
||||
|
||||
attr_reader :organization
|
||||
def initialize(id)
|
||||
super(id)
|
||||
@organization = Organization.find_by_name(self.o)
|
||||
end
|
||||
|
||||
def Person.exists?(id)
|
||||
super(id) && @@connection[id]["objectClass"] == "inetOrgPerson"
|
||||
end
|
||||
|
||||
def Person.valid?(entry)
|
||||
super(entry) && entry.objectClass == "inetOrgPerson"
|
||||
end
|
||||
|
||||
def Person.set_local(o_name, ou_name)
|
||||
@@local_o = o_name
|
||||
@@local_ou = ou_name
|
||||
end
|
||||
|
||||
def to_s
|
||||
self.cn || @id
|
||||
end
|
||||
|
||||
def to_html
|
||||
if not self.cn
|
||||
e(:span, :style => "color:red"){ @id }
|
||||
elsif self.mail
|
||||
e(:a, :href => "mailto:" + self.mail){ self.cn }
|
||||
else
|
||||
e(:span){ self.cn }
|
||||
end
|
||||
end
|
||||
|
||||
def local?
|
||||
self.o == @@local_o
|
||||
end
|
||||
|
||||
def employee?
|
||||
local? and self.ou == @@local_ou
|
||||
end
|
||||
|
||||
def customer?
|
||||
self.o != nil and not local?
|
||||
end
|
||||
|
||||
def Person.find_by_org(org_name)
|
||||
find_by_filter("(&(o=#{org_name})(objectClass=inetOrgPerson))")
|
||||
end
|
||||
|
||||
def <=>(rhs)
|
||||
if self.cn
|
||||
rhs.cn ? self.cn <=> rhs.cn : -1
|
||||
else
|
||||
rhs.cn ? 1 : @id <=> rhs.id
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class Organization < LdapRecord
|
||||
def to_s
|
||||
self.o || @id
|
||||
end
|
||||
|
||||
def to_html
|
||||
if not self.o
|
||||
e(:span, :style => "color:red"){ @id }
|
||||
else
|
||||
e(:span){ self.o }
|
||||
end
|
||||
end
|
||||
|
||||
def Organization.exists?(id)
|
||||
super(id) && @@connection[id]["objectClass"] == "organization"
|
||||
end
|
||||
|
||||
def Organization.valid?(entry)
|
||||
super(entry) && entry.objectClass == "organization"
|
||||
end
|
||||
|
||||
def Organization.all
|
||||
find_by_filter("(objectClass=organization)")
|
||||
end
|
||||
|
||||
def Organization.find_by_name(name)
|
||||
return nil unless name.is_a? String
|
||||
res = find_by_filter("(&(o=#{name})(objectClass=organization))")
|
||||
res ? res[0] : nil
|
||||
end
|
||||
|
||||
def <=>(rhs)
|
||||
if self.o
|
||||
rhs.o ? self.o <=> rhs.o : -1
|
||||
else
|
||||
rhs.o ? 1 : @id <=> rhs.id
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class Notifier
|
||||
def notify(who, message)
|
||||
raise "No such person \"#{who.to_s}\"" if not who.is_a? Person
|
||||
msg = "From: Diary Management Application <#{DiaryEnv::SMTP_FROM}>\n" +
|
||||
"To: #{who.cn} <#{who.mail}>\n" +
|
||||
"Subject: Diary Management\n" +
|
||||
"\n" + message
|
||||
Net::SMTP.start(DiaryEnv::SMTP_HOST) do |smtp|
|
||||
smtp.send_message msg, DiaryEnv::SMTP_FROM, who.mail.untaint
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,304 @@
|
|||
# SPDX-License-Identifier: Apache-2.0
|
||||
# Copyright (C) 2021 OKTET Labs Ltd. All rights reserved.
|
||||
|
||||
#
|
||||
# General CGI helpers
|
||||
#
|
||||
|
||||
HEADER_FILE = "header.html"
|
||||
FOOTER_FILE = "footer.html"
|
||||
|
||||
CURRENT_MONTH = "- Current -"
|
||||
# Where to place it in the months list
|
||||
CURRENT_MONTH_NUMBER = 0
|
||||
|
||||
def cgi_debug_log(msg)
|
||||
STDERR.puts "DEBUG: " + msg
|
||||
end
|
||||
|
||||
class MyCGI < CGI
|
||||
|
||||
# http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/23950?help-en
|
||||
def MyCGI::pretty(string, shift = " ")
|
||||
i = 0; preserve = {}
|
||||
lines = string.gsub(/\n*<pre.*<\/pre>\n*/inm) {
|
||||
t = "%PRE#{i}"; i += 1
|
||||
preserve[t] = $&
|
||||
t
|
||||
}
|
||||
lines = lines.gsub(/\n*<textarea.*<\/textarea>\n*/inm) {
|
||||
t = "%TEXTAREA#{i}"; i += 1
|
||||
preserve[t] = $&
|
||||
t
|
||||
}
|
||||
lines = lines.gsub(/(?!\A)<(?:.|\n)*?>/n,
|
||||
"\n\\0").gsub(/<(?:.|\n)*?>(?!\n)/n, "\\0\n")
|
||||
end_pos = 0
|
||||
while end_pos = lines.index(/^<\/(\w+)/n, end_pos)
|
||||
element = $1.dup
|
||||
start_pos = lines.rindex(/^\s*<#{element}/ni, end_pos)
|
||||
lines[start_pos ... end_pos] = "__" +
|
||||
lines[start_pos ... end_pos].
|
||||
gsub(/\n(?!\z)/n, "\n" + shift) +
|
||||
"__"
|
||||
end
|
||||
lines = lines.gsub(/^((?:#{Regexp::quote(shift)})*)__(?=<\/?\w)/n,
|
||||
'\1')
|
||||
preserve.each { |t, s|
|
||||
lines.sub!(/#{Regexp::quote(t)}/, s)
|
||||
}
|
||||
lines
|
||||
end
|
||||
|
||||
def MyCGI::list_select(list, param)
|
||||
param = Array.new if not param
|
||||
list.collect do |entry|
|
||||
param.include?(entry[0]) ? entry.dup.push(true) : entry
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(form_type, default_params, multiple_key, multiline_key, *args)
|
||||
super(*args)
|
||||
@form_type = form_type
|
||||
@tree_params = default_params
|
||||
@multiple_key = multiple_key
|
||||
@multiline_key = multiline_key
|
||||
self.params.each do |key, value|
|
||||
@tree_params = add_value(@tree_params, key, value, @multiple_key.include?(key))
|
||||
end
|
||||
end
|
||||
|
||||
def add_value(hash, key, value, multiple)
|
||||
hash = Hash.new if not hash
|
||||
if /^(.*?):(.*)$/ =~ key
|
||||
hash[$1] = add_value(hash[$1], $2, value, @multiple_key.include?($1))
|
||||
elsif @multiline_key.include?(key)
|
||||
hash[key] = value.join
|
||||
else
|
||||
hash[key] = (multiple or value.length > 1) ? value : value[0]
|
||||
end
|
||||
hash
|
||||
end
|
||||
|
||||
def hiddens(value)
|
||||
value.inject([]) do |res, item|
|
||||
res + [Amrita.a(:name => item[0], :value => item[1])]
|
||||
end
|
||||
end
|
||||
|
||||
# Create array of pairs [name, value] from the parameters subtree
|
||||
def tree_flatten(keys)
|
||||
hash = @tree_params
|
||||
keys.each do |key|
|
||||
return [] if not hash[key]
|
||||
hash = hash[key]
|
||||
end
|
||||
add_flatten(keys[0..-2], keys[-1], hash)
|
||||
end
|
||||
|
||||
def add_flatten(prefix, field, data)
|
||||
names = field ? prefix + [field] : prefix
|
||||
if data.is_a? Hash
|
||||
data.inject([]) do |res, pair|
|
||||
res + add_flatten(names, pair[0], pair[1])
|
||||
end
|
||||
elsif data.is_a? Array
|
||||
data.inject([]) do |res, value|
|
||||
res << [names.join(":"), value.to_s]
|
||||
end
|
||||
else
|
||||
# single value
|
||||
[[names.join(":"), data.to_s]]
|
||||
end
|
||||
end
|
||||
|
||||
def tree_hidden(keys)
|
||||
hash = @tree_params
|
||||
keys.each do |key|
|
||||
return "" if not hash[key]
|
||||
hash = hash[key]
|
||||
end
|
||||
add_hidden(keys[0..-2], keys[-1], hash)
|
||||
end
|
||||
|
||||
def MyCGI::list_options(data, selections = nil)
|
||||
selection = ((selections.is_a? Array) ? selections[0] : selections).to_s
|
||||
data.collect do |pair|
|
||||
item = Amrita.a(:value => pair[0]){ pair[1] }
|
||||
item << Amrita.a(:selected => "selected")[0] if
|
||||
pair[0].to_s == selection
|
||||
item
|
||||
end
|
||||
end
|
||||
|
||||
def tree_hidden_e(keys)
|
||||
hash = @tree_params
|
||||
keys.each do |key|
|
||||
return [] if not hash[key]
|
||||
hash = hash[key]
|
||||
end
|
||||
add_hidden_e(keys[0..-2], keys[-1], hash)
|
||||
end
|
||||
|
||||
def add_hidden_e(prefix, field, value)
|
||||
if value.is_a? Hash
|
||||
value.inject([]) do |res, pair|
|
||||
res + add_hidden_e(prefix + [field], pair[0], pair[1])
|
||||
end
|
||||
else
|
||||
value.inject([]) do |res, value1|
|
||||
res + [ Amrita.a(:name => (prefix + [field]).join(":"),
|
||||
:value => value1) ]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def add_hidden(prefix, field, value)
|
||||
if value.is_a?Hash
|
||||
value.inject("") do |res, pair|
|
||||
res + add_hidden(prefix + [field], pair[0], pair[1])
|
||||
end
|
||||
else
|
||||
value.inject("") do |res, value1|
|
||||
res + hidden((prefix + [field]).join(":"), value1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def MyCGI::hash2date(hash)
|
||||
begin
|
||||
value = %w(year month day).collect do |p|
|
||||
hash[p].is_a?(Array) ? hash[p][0] : hash[p]
|
||||
end
|
||||
value[0] = case value[0]
|
||||
when '$' then 9999
|
||||
when '#' then Date.today.year
|
||||
else value[0].to_i
|
||||
end
|
||||
value[1] = case value[1]
|
||||
when '$' then Date::MONTHNAMES.length - 1
|
||||
when CURRENT_MONTH then Date.today.month
|
||||
when CURRENT_MONTH_NUMBER.to_s then Date.today.month
|
||||
else value[1].to_i
|
||||
end
|
||||
|
||||
if value[2] == '$'
|
||||
value[2] = 1
|
||||
(Date.new(*value) >> 1) - 1
|
||||
elsif value[2] == '#'
|
||||
value[2] = Date.today.day
|
||||
Date.new(*value)
|
||||
else
|
||||
value[2] = value[2].to_i
|
||||
Date.new(*value)
|
||||
end
|
||||
rescue ArgumentError
|
||||
raise "Invalid date #{hash["year"]}-#{hash["month"]}-#{hash["day"]}"
|
||||
end
|
||||
end
|
||||
def MyCGI::date2hash(date)
|
||||
{
|
||||
"year" => [date.year.to_s],
|
||||
"month" => [date.mon.to_s],
|
||||
"day" => [date.day.to_s]
|
||||
}
|
||||
end
|
||||
|
||||
def MyCGI::select_date(prefix = "", value = nil, edit = false)
|
||||
if value == nil
|
||||
edit = true
|
||||
value = MyCGI::date2hash(Date.today)
|
||||
elsif value.is_a?Hash
|
||||
# TODO: !!!!
|
||||
value = { "year" => [value["year"]],
|
||||
"month" => [value["month"]],
|
||||
"day" => [value["day"]] } unless value["year"].is_a?Array
|
||||
edit = true
|
||||
else
|
||||
value = MyCGI::date2hash(value)
|
||||
end
|
||||
if edit
|
||||
template = Amrita::TemplateText.new(
|
||||
"<input class='form-control' id='day' size='2' type='text'>
|
||||
<select class='form-control' id='month'><option id='item'></select>
|
||||
<input class='form-control' id='year' size='4' type='text'>")
|
||||
template.asxml = true
|
||||
s = String.new
|
||||
template.expand(s,
|
||||
{:day => Amrita.a(
|
||||
:name => prefix + ":day",
|
||||
:value => value["day"][0]),
|
||||
:month => Amrita.a(
|
||||
:name => prefix + ":month"){
|
||||
{:item => list_options(
|
||||
(0..(Date::MONTHNAMES.length - 1)).collect do |m|
|
||||
case m
|
||||
when CURRENT_MONTH_NUMBER then [m.to_s, CURRENT_MONTH]
|
||||
else [m.to_s, Date::MONTHNAMES[m]]
|
||||
end
|
||||
end, value["month"][0])
|
||||
}
|
||||
},
|
||||
:year => Amrita.a(
|
||||
:name => prefix + ":year",
|
||||
:value => value["year"][0])
|
||||
})
|
||||
s
|
||||
else
|
||||
d = Date.new(value["year"][0].to_i, value["month"][0].to_i,
|
||||
value["day"][0].to_i)
|
||||
sprintf("%s %s %s (%s)", value["day"][0],
|
||||
Date::MONTHNAMES[value["month"][0].to_i],
|
||||
value["year"][0],
|
||||
Date::ABBR_DAYNAMES[d.wday])
|
||||
end
|
||||
end
|
||||
|
||||
def MyCGI::select_date_e(prefix = "", value = nil, edit = false)
|
||||
noescape{ select_date(prefix, value, edit) }
|
||||
end
|
||||
|
||||
# Assign values to the all-site common parts of the page
|
||||
#
|
||||
# @param data Hash of data for amrita template of the page
|
||||
def MyCGI::add_commons(data)
|
||||
data[:header] = noescape{ File.open(HEADER_FILE) { |f| f.read }}
|
||||
data[:footer] = noescape{ File.open(FOOTER_FILE) { |f| f.read }}
|
||||
end
|
||||
|
||||
def MyCGI::optional(data)
|
||||
{ :value => data }
|
||||
end
|
||||
|
||||
# Create string from the 'value' that can be used
|
||||
# as a value of HTML DOM attribute 'id'
|
||||
#
|
||||
# @return String of proposed attribute value
|
||||
def MyCGI::element_id(value)
|
||||
id = value.to_s
|
||||
if id.empty? or id !~ /[A-Za-z].*/
|
||||
"s" + id
|
||||
else
|
||||
id
|
||||
end
|
||||
end
|
||||
|
||||
def MyCGI.dump(value)
|
||||
case value.class.to_s
|
||||
when "Hash" then
|
||||
"{" + value.collect do |key, x|
|
||||
"#{key} => #{dump(x)}"
|
||||
end.join(", ") + "}"
|
||||
when "String" then
|
||||
"\"#{value}\""
|
||||
when "Array" then
|
||||
"[#{value.collect {|x| dump(x)}.join(",")}]"
|
||||
else
|
||||
"#{value.class}(#{value.to_s})"
|
||||
end
|
||||
end
|
||||
|
||||
attr_accessor :form_type, :tree_params, :multiple_key
|
||||
private :add_value
|
||||
end
|
||||
|
|
@ -0,0 +1,212 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<link rel="stylesheet" type="text/css" href="/main/diary/diary.css"/>
|
||||
<link rel="stylesheet" type="text/css" href="/main/diary/css/bootstrap.min.css"/>
|
||||
<title>Diary Management</title>
|
||||
</head>
|
||||
<body>
|
||||
<span _id="header"></span>
|
||||
<p><a href="?do=diary">View/edit diaries</a></p>
|
||||
|
||||
<!-- Selection menu -->
|
||||
<div id="menu" _id="menu">
|
||||
<form method="post" action="#">
|
||||
<h4>Select projects to list:</h4>
|
||||
<p>
|
||||
<input type="hidden" name="do" value="project" />
|
||||
Status of the project:
|
||||
<select name="prj:list:prj_status" multiple="multiple">
|
||||
<option _id="status">Active</option>
|
||||
</select>
|
||||
Customer organization
|
||||
<select name="prj:list:customer">
|
||||
<option _id="customer">Customer</option>
|
||||
</select>
|
||||
<input type="submit" />
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Project list -->
|
||||
<div id="list">
|
||||
<div _id="nodata" id="nodata">
|
||||
No projects found.
|
||||
</div>
|
||||
|
||||
<div _id="list" class="prj_item">
|
||||
<form method="post" _id="view">
|
||||
<h4 _id="name">Project Name</h4>
|
||||
<p>
|
||||
<input type="hidden" name="do" value="project">
|
||||
<input type="hidden" name="prj:list:edit" _id="edit_id">
|
||||
<input type="hidden" name="prj:action:id" _id="edit_id">
|
||||
<input type="hidden" _id="hiddens">
|
||||
<input type="submit" _id="edit" name="prj:action:verb" value="Edit"></p>
|
||||
<p>Project tag: <span _id="tag">tag</span></p>
|
||||
<p>Customer organization: <span _id="customer">customer</span></p>
|
||||
<p>Administrative customer contact: <span _id="cust_man">uid</span>
|
||||
<span _id="cust_tech">Technical customer contact:
|
||||
<span _id="value">uid</span>
|
||||
</span></p>
|
||||
<p>Internal project manager: <span _id="manager">uid</span>
|
||||
<span _id="leader">Internal project leader:
|
||||
<span _id="value">uid</span>
|
||||
</span></p>
|
||||
<p>Long description of the project:</p>
|
||||
<pre _id="prj_desc">Description</pre>
|
||||
<p>Current status of the project: <span _id="prj_status">Active</span></p>
|
||||
<span _id="status_desc"><p>Description of project status:</p>
|
||||
<pre _id="value">Status description</pre></span>
|
||||
<span _id="issues"><p>Issues:</p>
|
||||
<pre _id="value">Issues</pre></span>
|
||||
<span _id="maillists"><p>Mailing lists: <span _id="value">maillist</span></p></span>
|
||||
<span _id="storagedir"><p>Project directory in <a href="/storage/">library</a>:
|
||||
<a _id="value"></a></p></span>
|
||||
<span _id="svndir"><p>Project directory in <a href="/svnroot/">SubVersion</a>:
|
||||
<a _id="value"></a></p></span>
|
||||
<span _id="extra_rights"><p>Extra user rights (in LDAP filter syntax):
|
||||
<span _id="value"></span></p></span>
|
||||
<span _id="bugzilla_prod"><p>Bugzilla product: - not implemented -</p></span>
|
||||
<div _id="approval" class="approval">
|
||||
<p><span class="approval_header">Approval mode</span></p>
|
||||
<p _id="approval_list"><select name="prj:approve:list" multiple="multiple">
|
||||
<option _id="approval_item">User</option>
|
||||
</select>
|
||||
<input type="submit" name="prj:action:verb" value="Delete"></p>
|
||||
Approve <select name="prj:approve:who">
|
||||
<option _id="person">Engineer</option>
|
||||
</select> by
|
||||
<select name="prj:approve:approver">
|
||||
<option _id="person">Engineer</option>
|
||||
</select>
|
||||
<input type="submit" name="prj:action:verb" value="Add">
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form method="post" action="#" _id="edit">
|
||||
<div class="edit">
|
||||
<h4>Editing project</h4>
|
||||
<p>
|
||||
<input type="hidden" name="do" value="project">
|
||||
<input type="hidden" name="prj:edit:id" _id="id">
|
||||
<input type="hidden" _id="hiddens">
|
||||
Project name<span class="mark"/>*</span>:
|
||||
<input _id="name" type="text" name="prj:edit:name">
|
||||
Project tag<span class="mark">*</span>:
|
||||
<input _id="tag" type="text" name="prj:edit:tag"></p>
|
||||
<p>Customer organization<span class="mark">*</span>:
|
||||
<select name="prj:edit:customer">
|
||||
<option _id="customer">Customer</option>
|
||||
</select></p>
|
||||
<p>Administrative customer contact<span class="mark">*</span>:
|
||||
<input _id="cust_man" type="text" name="prj:edit:cust_man">
|
||||
Technical customer contact:
|
||||
<input _id="cust_tech" type="text" name="prj:edit:cust_tech">
|
||||
</p>
|
||||
<p>Internal project manager<span class="mark">*</span>:
|
||||
<input _id="manager" type="text" name="prj:edit:manager">
|
||||
Internal project leader:
|
||||
<input _id="leader" type="text" name="prj:edit:leader"></p>
|
||||
<p>Long description of the project<span class="mark">*</span>:</p>
|
||||
<p><textarea _id="prj_desc" name="prj:edit:prj_desc"
|
||||
cols="70" rows="10">Project description</textarea></p>
|
||||
<p><input _id="hide_hrs" type="checkbox" name="prj:edit:hide_hrs" />
|
||||
Hide number of hours from customer
|
||||
</p>
|
||||
<p>Current status of the project:
|
||||
<select name="prj:edit:prj_status">
|
||||
<option _id="prj_status"></option>
|
||||
</select>
|
||||
<p>Description of project status:</p>
|
||||
<p><textarea _id="status_desc" name="prj:edit:status_desc"
|
||||
cols="70" rows="10">Project status description</textarea>
|
||||
</p>
|
||||
<p>Issues:</p>
|
||||
<p><textarea _id="issues" name="prj:edit:issues"
|
||||
cols="70" rows="10">Issues</textarea></p>
|
||||
<p>Mailing lists:
|
||||
<input _id="maillists" type="text" name="prj:edit:maillists"></p>
|
||||
<p>Project directory in <a href="/storage/">library</a>:
|
||||
<input _id="storagedir" type="text" name="prj:edit:storagedir"></p>
|
||||
<p>Project directory in <a href="/svnroot/">SubVersion</a>:
|
||||
<input _id="svndir" type="text" name="prj:edit:svndir"></p>
|
||||
<p>Extra user rights (in LDAP filter syntax):
|
||||
<input _id="extra_rights" type="text" name="prj:edit:extra_rights"></p>
|
||||
<p>
|
||||
<a href="/cgi-bin/bugzilla/editproducts.cgi">Bugzilla product</a>:
|
||||
-- not implemented --</p>
|
||||
<!--
|
||||
<p>Add new CC address: <input type="text" name="prj:edit:ccadd" /></p>
|
||||
<select name="prj:edit:cclist" multiple="multiple">
|
||||
<option _id="cclist">user@domain.org</option>
|
||||
</select>
|
||||
<p>Remove selected CC <input type="checkbox" name="prj:edit:ccdel" /></p>
|
||||
-->
|
||||
<p><input type="submit" value="Submit changes" /></p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add new project -->
|
||||
<div id="new" _id="new">
|
||||
<h4>Add new project</h4>
|
||||
<form method="post" action="#" _id="edit">
|
||||
<p>
|
||||
<input type="hidden" name="do" value="project">
|
||||
<input type="hidden" name="prj:edit:id" _id="id">
|
||||
<input type="hidden" _id="hiddens">
|
||||
Project name<span class="mark"/>*</span>:
|
||||
<input type="text" name="prj:edit:name" />
|
||||
Project tag<span class="mark">*</span>:
|
||||
<input type="text" name="prj:edit:tag" /></p>
|
||||
<p>Customer organization<span class="mark">*</span>:
|
||||
<select name="prj:edit:customer">
|
||||
<option _id="customer">Customer</option>
|
||||
</select></p>
|
||||
<p>Administrative customer contact<span class="mark">*</span>:
|
||||
<input type="text" name="prj:edit:cust_man" />
|
||||
Technical customer contact:
|
||||
<input type="text" name="prj:edit:cust_tech" /></p>
|
||||
<p>Internal project manager<span class="mark">*</span>:
|
||||
<input type="text" name="prj:edit:manager" _id="manager">
|
||||
Internal project leader:
|
||||
<input type="text" name="prj:edit:leader" /></p>
|
||||
<p>Long description of the project<span class="mark">*</span>:</p>
|
||||
<p><textarea name="prj:edit:prj_desc" cols="70" rows="10"></textarea></p>
|
||||
<p><input type="checkbox" name="prj:edit:make_active" />
|
||||
Mark the project as "ACTIVE"
|
||||
</p>
|
||||
<p><input type="checkbox" name="prj:edit:hide_hrs" />
|
||||
Hide number of hours from customer
|
||||
</p>
|
||||
<p>Description of project status:</p>
|
||||
<p><textarea name="prj:edit:status_desc" cols="70" rows="10"></textarea></p>
|
||||
<p>Issues:</p>
|
||||
<p><textarea name="prj:edit:issues" cols="70" rows="10"></textarea></p>
|
||||
<p>Mailing lists:
|
||||
<input type="text" name="prj:edit:maillists" /></p>
|
||||
<p>Project directory in <a href="/storage/">library</a>:
|
||||
<input type="text" name="prj:edit:storagedir" /></p>
|
||||
<p>Project directory in <a href="/svnroot/">SubVersion</a>:
|
||||
<input type="text" name="prj:edit:svndir" /></p>
|
||||
<p>Extra user rights (in LDAP filter syntax):
|
||||
<input type="text" name="prj:edit:extra_rights" /></p>
|
||||
<p>
|
||||
<a href="/cgi-bin/bugzilla/editproducts.cgi">Bugzilla product</a>:
|
||||
-- not implemented --</p>
|
||||
<!--
|
||||
<p>Add new CC address: <input type="text" name="prj:edit:ccadd" /></p>
|
||||
<select name="prj:edit:cclist" multiple="multiple">
|
||||
<option _id="cclist">user@domain.org</option>
|
||||
</select>
|
||||
<p>Remove selected CC <input type="checkbox" name="prj:edit:ccdel" /></p>
|
||||
-->
|
||||
<p><input type="submit" value="Add new project" /></p>
|
||||
</form>
|
||||
</div>
|
||||
<span _id="footer"></span>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,456 @@
|
|||
# SPDX-License-Identifier: Apache-2.0
|
||||
# Copyright (C) 2021 OKTET Labs Ltd. All rights reserved.
|
||||
|
||||
#
|
||||
# Class Project for Diary Management Application.
|
||||
#
|
||||
|
||||
PROJECT_TEMPLATE = AmritaTemplate.new("project.html")
|
||||
|
||||
require 'dbi'
|
||||
require_relative 'diary_datamapper'
|
||||
require_relative 'diary_policy'
|
||||
|
||||
class Project
|
||||
STATUS = ["NEW", "ACTIVE", "TERMINATED"]
|
||||
STATUS_DEFAULT = "ACTIVE"
|
||||
@@mapper = nil
|
||||
|
||||
def initialize(id = nil, values = nil)
|
||||
@id = id
|
||||
@attributes = values ? values : Hash.new
|
||||
@cc = nil
|
||||
@approvals = nil
|
||||
end
|
||||
|
||||
def self.get(id)
|
||||
mapper.find(id)
|
||||
end
|
||||
|
||||
def self.where(clause, *args)
|
||||
mapper.where(clause, *args)
|
||||
end
|
||||
|
||||
def save_approvals
|
||||
self.class.mapper.save_approvals(@id, @approvals)
|
||||
end
|
||||
|
||||
def approvals
|
||||
return @approvals unless @approvals.nil?
|
||||
@approvals = self.class.mapper.find_approvals(@id)
|
||||
end
|
||||
|
||||
def id
|
||||
@id
|
||||
end
|
||||
|
||||
def [](key)
|
||||
@attributes[key]
|
||||
end
|
||||
|
||||
def <=>(rhs)
|
||||
to_s <=> rhs.to_s
|
||||
end
|
||||
|
||||
def to_s
|
||||
@attributes["customer"] + "/" + @attributes["tag"]
|
||||
end
|
||||
|
||||
def self.mapper
|
||||
return @@mapper unless @@mapper.nil?
|
||||
@@mapper = ProjectMapper.new
|
||||
end
|
||||
|
||||
def customer
|
||||
Organization.find_or_create(@attributes["customer"])
|
||||
end
|
||||
|
||||
# we should not hardcode things
|
||||
def transform_links(text)
|
||||
return text
|
||||
end
|
||||
|
||||
def self.all(args)
|
||||
policy = DiaryEnv.instance.policy
|
||||
res = mapper.all(args)
|
||||
res = res & policy.project_list if
|
||||
policy.restriction.include?(:project)
|
||||
return res
|
||||
end
|
||||
end
|
||||
|
||||
class ProjectMapper < DataMapper
|
||||
def initialize
|
||||
super("project")
|
||||
end
|
||||
|
||||
def instantiate(id, values)
|
||||
Project.new(id, values)
|
||||
end
|
||||
|
||||
def insert(prj)
|
||||
super(prj)
|
||||
if prj.cc and prj.cc.length > 0
|
||||
@@database.do("insert into cc_list (prj_id, person) values " +
|
||||
(["(#{prj.id}, ?)"] * prj.cc.length).join(", "),
|
||||
*prj.cc);
|
||||
end
|
||||
save_approvals(prj.id, prj.approvals)
|
||||
end
|
||||
|
||||
def find_approvals(id)
|
||||
app = Hash.new
|
||||
@@database.select_all("select * from approval where prj_id = ?",
|
||||
id) { |row| app[row["who"]] = row["approver"] }
|
||||
app
|
||||
end
|
||||
|
||||
def save_approvals(id, app)
|
||||
@@database.do("delete from approval where prj_id = ?", id)
|
||||
app.each do |who, approver|
|
||||
@@database.do("insert into approval (prj_id, who, approver) " +
|
||||
"values (?, ?, ?)", id, who, approver)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class ProjectUI < Hash
|
||||
|
||||
PERSONS = %w(manager leader cust_man cust_tech)
|
||||
MANDATORY = %w(tag name prj_status manager customer cust_man prj_desc hide_hrs)
|
||||
OPTIONAL = %w(status_desc issues leader cust_tech maillists svndir
|
||||
storagedir bugzilla_prod extra_rights)
|
||||
|
||||
def status_list(selected)
|
||||
MyCGI::list_options(
|
||||
Project::STATUS.collect {|status| [status.to_s, status.to_s]},
|
||||
selected)
|
||||
end
|
||||
|
||||
def update(id = @id)
|
||||
@id = id
|
||||
self.clear
|
||||
if @id
|
||||
DataMapper.database.execute("select * from project where id = #{@id}") do |q|
|
||||
### For some reason MySQL driver always returns 0 in q.rows
|
||||
# if q.rows != 1
|
||||
# raise "Project #{@id} has #{q.rows} instances"
|
||||
# end
|
||||
row = q.fetch_hash
|
||||
raise "Project #{@id} has no instances" if not row
|
||||
self.replace(row)
|
||||
self.delete("id")
|
||||
end
|
||||
self["cc"] = Array.new
|
||||
DataMapper.database.select_all("select person from cc_list where " +
|
||||
"prj_id = #{@id}") do |row|
|
||||
self["cc"].push(row["person"])
|
||||
end
|
||||
end
|
||||
true
|
||||
end
|
||||
|
||||
def initialize(cgi)
|
||||
@id = nil
|
||||
@cgi = cgi
|
||||
@user = DiaryEnv.instance.user
|
||||
@policy = DiaryEnv.instance.policy
|
||||
end
|
||||
|
||||
def create(data)
|
||||
return unless @policy.can_edit_project?
|
||||
#raise "Create project #{data["name"]}"
|
||||
inst = data.keys
|
||||
inst -= ["cc"]
|
||||
DataMapper.database.do("insert into project (" + inst.join(", ") +
|
||||
") values (" + (inst.collect do "?" end).join(", ") +
|
||||
")",
|
||||
*(inst.collect do |field| data[field] end))
|
||||
@id = DataMapper.database.select_one("select last_insert_id()")[0]
|
||||
if data["cc"].length > 0
|
||||
DataMapper.database.do("insert into cc_list (prj_id, person) values " +
|
||||
(["(#{@id}, ?)"] * data["cc"].length).join(", "),
|
||||
*data["cc"]);
|
||||
end
|
||||
# Send notifications XXX
|
||||
end
|
||||
|
||||
def modify(current, data)
|
||||
return if not @policy.can_edit_project?(@id)
|
||||
changes = Array.new
|
||||
data.each_key do |field|
|
||||
if data[field] != current[field]
|
||||
changes.push(field)
|
||||
end
|
||||
end
|
||||
if (changes - ["cc"]).length > 0
|
||||
DataMapper.database.do("update project set " +
|
||||
(changes.collect do
|
||||
|field| field + " = ?" if field != "cc"
|
||||
end).join(", ") +
|
||||
" where id = #{@id}",
|
||||
*(changes.collect do |field| data[field] end));
|
||||
end
|
||||
if (current["cc"] - data["cc"]).length > 0
|
||||
DataMapper.database.do("delete from cc_list where " +
|
||||
"prj_id = #{@id} and (" +
|
||||
(["person = ?"] * (current["cc"] - data["cc"]).length
|
||||
).join(" or ") + ")",
|
||||
*(current["cc"] - data["cc"]))
|
||||
end
|
||||
if (data["cc"] - current["cc"]).length > 0
|
||||
DataMapper.database.do("insert into cc_list (prj_id, person) " +
|
||||
"values " +
|
||||
(["(#{@id}, ?)"] * (data["cc"] - current["cc"]).length
|
||||
).join(", "),
|
||||
*(data["cc"] - current["cc"]))
|
||||
end
|
||||
# Send notifications XXX
|
||||
#@people.notify("mis", "Project '#{data["name"]}' is changed")
|
||||
end
|
||||
|
||||
# Set project data to SQL tables.
|
||||
def set(data)
|
||||
MANDATORY.each do |field|
|
||||
if not data[field] or data[field].length == 0
|
||||
raise "Mandatory field #{field} is not set"
|
||||
end
|
||||
end
|
||||
data.each_key do |field|
|
||||
if not MANDATORY.include?(field) and
|
||||
not OPTIONAL.include?(field) and field != "cc"
|
||||
raise "Field #{field} is unknown"
|
||||
end
|
||||
end
|
||||
data["svndir"].sub!(/^\/*/, '')
|
||||
data["storagedir"].sub!(/^\/*/, '')
|
||||
data = data.delete_if do |field, value|
|
||||
value.length == 0
|
||||
end
|
||||
data.each do |field, value|
|
||||
data[field] = value
|
||||
end
|
||||
PERSONS.each do |pers|
|
||||
if data[pers] and not Person.find(data[pers])
|
||||
raise "No such person #{pers}: #{data[pers]} "
|
||||
end
|
||||
end
|
||||
if not data["cc"]
|
||||
data["cc"] = Array.new
|
||||
end
|
||||
if @id
|
||||
modify(self, data)
|
||||
else
|
||||
create(data)
|
||||
end
|
||||
end
|
||||
|
||||
attr_reader :id
|
||||
|
||||
def list
|
||||
return [] if @cgi.tree_params["prj"]["list"]["prj_status"].empty?
|
||||
args = Hash.new
|
||||
customer = @cgi.tree_params["prj"]["list"]["customer"]
|
||||
args[:customer] = customer if
|
||||
customer.is_a?(Array) and not customer.include?("*")
|
||||
args[:prj_status] = @cgi.tree_params["prj"]["list"]["prj_status"] ||
|
||||
[Project::STATUS_DEFAULT]
|
||||
Project.all(args).collect do |prj|
|
||||
a(:__id__ => MyCGI::element_id(prj.id)){
|
||||
list_item(prj, @cgi.tree_params["prj"]["list"]["edit"].
|
||||
include?(prj.id.to_s))}
|
||||
end
|
||||
end
|
||||
|
||||
def list_item(values = nil, edit = false)
|
||||
if (values == nil)
|
||||
edit = add = true
|
||||
values = Hash.new
|
||||
else
|
||||
add = false
|
||||
end
|
||||
if not @policy.can_edit_project?(@id)
|
||||
edit = add = false
|
||||
end
|
||||
|
||||
if edit
|
||||
edit_data = {
|
||||
:hiddens => @cgi.hiddens(@cgi.tree_flatten(["prj", "list"]).
|
||||
delete_if do |x|
|
||||
x == ["prj:list:edit", values["id"].to_s]
|
||||
end),
|
||||
:name => a(:value => values["name"]),
|
||||
:tag => a(:value => values["tag"],
|
||||
:size => Person::MAX_LENGTH),
|
||||
:customer => MyCGI::list_options(Organization.all.collect { |org|
|
||||
[org.uid, org.o] },
|
||||
values["customer"]),
|
||||
:cust_man => a(:value => values["cust_man"],
|
||||
:size => Person::MAX_LENGTH),
|
||||
:cust_tech => a(:value => values["cust_tech"],
|
||||
:size => Person::MAX_LENGTH),
|
||||
:manager => a(:value => values["manager"] || DiaryEnv.director[0].uid,
|
||||
:size => Person::MAX_LENGTH),
|
||||
:leader => a(:value => values["leader"],
|
||||
:size => Person::MAX_LENGTH),
|
||||
:prj_desc => values["prj_desc"] || "",
|
||||
:prj_status => status_list(@cgi.tree_params["prj"]["list"]["prj_status"]),
|
||||
:status_desc => values["status_desc"] || "",
|
||||
:issues => values["issues"] || "",
|
||||
:maillists => a(:value => values["maillists"]),
|
||||
:storagedir => a(:value => values["storagedir"]),
|
||||
:svndir => a(:value => values["svndir"]),
|
||||
:extra_rights => a(:value => values["extra_rights"]),
|
||||
:bugzilla_prod => a(:value => values["bugzilla_prod"]),
|
||||
# :cclist => MyCGI::list_options(values["cc"] ? values["cc"] : [])
|
||||
:hide_hrs => a(:checked => values["hide_hrs"] ? "checked" : nil)
|
||||
}
|
||||
edit_data[:id] = a(:value => values["id"]) if values["id"]
|
||||
return { :edit => edit_data }
|
||||
else
|
||||
view = {
|
||||
:id => a(:name => values["id"]),
|
||||
:edit_id => a(:value => values["id"]),
|
||||
:hiddens => @cgi.hiddens(@cgi.tree_flatten(["prj","list"])),
|
||||
:name => values["name"],
|
||||
:tag => values["tag"],
|
||||
:customer => Organization.find_or_create(values["customer"]).to_html,
|
||||
:cust_man => Person.find_or_create(values["cust_man"]).to_html,
|
||||
:manager => Person.find_or_create(values["manager"]).to_html,
|
||||
:prj_desc => values["prj_desc"],
|
||||
:prj_status => values["prj_status"]
|
||||
}
|
||||
if @policy.can_edit_project?(@id)
|
||||
approval_items = MyCGI::list_options(
|
||||
values.approvals.collect do |who, approver|
|
||||
[ who, "#{Person.find_or_create(who).to_s} => " +
|
||||
"#{Person.find_or_create(approver).to_s}"]
|
||||
end)
|
||||
view[:approval] = {
|
||||
:person => MyCGI::list_options(
|
||||
Person.find_by_org(DiaryEnv::HOME_ORGANIZATION).sort.collect { |x| [x.uid, x.to_s] })
|
||||
}
|
||||
view[:approval][:approval_list] = {
|
||||
:approval_item => approval_items
|
||||
} if not approval_items.empty?
|
||||
end
|
||||
view[:edit] = {} if @policy.can_edit_project?(@id)
|
||||
view[:cust_tech] = MyCGI::optional(
|
||||
Person.find_or_create(values["cust_tech"]).to_html) if
|
||||
values["cust_tech"]
|
||||
view[:leader] = MyCGI::optional(Person.find_or_create(values["leader"]).to_html) if
|
||||
values["leader"]
|
||||
view[:status_desc] = MyCGI::optional(values["status_desc"]) if
|
||||
values["status_desc"]
|
||||
view[:issues] = MyCGI::optional(values["issues"]) if values["issues"]
|
||||
view[:maillists] = MyCGI::optional(values["maillists"]) if
|
||||
values["maillists"]
|
||||
view[:storagedir] = MyCGI::optional(a(:href => "/storage/" +
|
||||
values["storagedir"]){values["storagedir"]}) if
|
||||
values["storagedir"]
|
||||
view[:svndir] = MyCGI::optional(a(:href => "/svnroot/" +
|
||||
values["svndir"]){ values["svndir"] }) if
|
||||
values["svndir"]
|
||||
view[:extra_rights] = MyCGI::optional(values["extra_rights"]) if
|
||||
values["extra_rights"]
|
||||
return { :view => a(:action => "#" +
|
||||
MyCGI::element_id(values["id"])){ view }}
|
||||
end
|
||||
end
|
||||
|
||||
def menu
|
||||
return {
|
||||
:status => status_list(
|
||||
@cgi.tree_params["prj"]["list"]["prj_status"].empty? ?
|
||||
Project::STATUS_DEFAULT :
|
||||
@cgi.tree_params["prj"]["list"]["prj_status"]),
|
||||
:customer => MyCGI::list_options([["*", "- All -"]] +
|
||||
@policy.customer_list.collect { |org|
|
||||
[org.uid, org.to_s] },
|
||||
@cgi.tree_params["prj"]["list"]["customer"])
|
||||
}
|
||||
end
|
||||
|
||||
def add_approval(id, who, approver)
|
||||
# TODO: user rights
|
||||
prj = Project.get(id)
|
||||
raise "No project #{id}" unless prj
|
||||
raise "You have no rights to edit project" unless
|
||||
@policy.can_edit_approval?(prj)
|
||||
prj.approvals[who] = approver
|
||||
prj.save_approvals
|
||||
end
|
||||
|
||||
def delete_approval(id, who)
|
||||
who = [who] unless who.is_a? Array
|
||||
# TODO: user rights
|
||||
prj = Project.get(id)
|
||||
raise "No project #{id}" unless prj
|
||||
raise "You have no rights to edit project" unless
|
||||
@policy.can_edit_approval?(prj)
|
||||
raise NeedConfirm.new("cancel_approval"),
|
||||
"Cancelling the approval mode will approve " +
|
||||
"ALL diary entries of " +
|
||||
"#{who.collect{ |x| Person.find(x).to_s }.join(", ")} " +
|
||||
"in project \"#{prj["name"]} (#{prj.to_s})\"!" if
|
||||
not DiaryEnv.confirmed?("cancel_approval")
|
||||
|
||||
who.each do |x|
|
||||
prj.approvals.delete(x)
|
||||
Diary.approve_all(prj, x)
|
||||
end
|
||||
prj.save_approvals
|
||||
end
|
||||
|
||||
def action
|
||||
if @cgi.tree_params["prj"].include?("approve")
|
||||
id = @cgi.tree_params["prj"]["action"]["id"]
|
||||
case @cgi.tree_params["prj"]["action"]["verb"]
|
||||
when "Add"
|
||||
add_approval(id,
|
||||
@cgi.tree_params["prj"]["approve"]["who"],
|
||||
@cgi.tree_params["prj"]["approve"]["approver"])
|
||||
@cgi.tree_params["prj"]["list"]["edit"].delete(id.to_s)
|
||||
return
|
||||
when "Delete"
|
||||
delete_approval(id,
|
||||
@cgi.tree_params["prj"]["approve"]["list"])
|
||||
@cgi.tree_params["prj"]["list"]["edit"].delete(id.to_s)
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
if @cgi.tree_params["prj"].include?("edit")
|
||||
attrs = @cgi.tree_params["prj"]["edit"]
|
||||
attrs["hide_hrs"] = [attrs.include?("hide_hrs") ? 1 : 0]
|
||||
if attrs.include?("id")
|
||||
update(attrs["id"])
|
||||
attrs.delete("id")
|
||||
set(attrs)
|
||||
else
|
||||
attrs["prj_status"] =
|
||||
attrs.include?("make_active") ? "ACTIVE" : "NEW"
|
||||
attrs.delete("make_active")
|
||||
set(attrs)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def show
|
||||
project_data = Hash.new
|
||||
MyCGI::add_commons(project_data)
|
||||
project_data[:menu] = menu
|
||||
project_data[:new] = list_item() if @policy.can_edit_project?
|
||||
|
||||
prj_list = list
|
||||
if prj_list.length == 0
|
||||
project_data[:nodata] = {}
|
||||
else
|
||||
project_data[:list] = list
|
||||
end
|
||||
|
||||
s = String.new
|
||||
PROJECT_TEMPLATE.expand(s, project_data)
|
||||
s
|
||||
end
|
||||
end
|
|
@ -0,0 +1,86 @@
|
|||
# SPDX-License-Identifier: Apache-2.0
|
||||
# Copyright (C) 2021 OKTET Labs Ltd. All rights reserved.
|
||||
|
||||
#
|
||||
# SQL class for Diary Management Application
|
||||
#
|
||||
|
||||
class SQL_Cache < Hash
|
||||
def initialize(sql, table, string_expr)
|
||||
@sql = sql
|
||||
@name = table
|
||||
@string_expr = string_expr
|
||||
@lists = Hash.new
|
||||
@fields = @sql.columns(@name).collect { |col| col["name"] }
|
||||
super(0)
|
||||
end
|
||||
|
||||
attr_reader :sql, :name
|
||||
attr_accessor :string_expr
|
||||
|
||||
def search(where)
|
||||
list = Array.new
|
||||
@sql.select_all("select * from #{@name} where #{where} order by id") do |row|
|
||||
self[row["id"]] = row.to_h
|
||||
list.push(self[row["id"]])
|
||||
end
|
||||
list
|
||||
end
|
||||
private :search
|
||||
|
||||
def list(where)
|
||||
if not @lists[where]
|
||||
@lists[where] = search(where).collect do |entry|
|
||||
string = @string_expr.dup
|
||||
entry.each do |field, value|
|
||||
string.gsub!(/#\{#{field}\}/, value.to_s) if value
|
||||
end
|
||||
[entry["id"].to_s, string]
|
||||
end
|
||||
end
|
||||
@lists[where]
|
||||
end
|
||||
|
||||
def [](id)
|
||||
id = id.to_i
|
||||
if super(id) == 0
|
||||
if search("id = #{id}").length != 1
|
||||
self[id] = nil
|
||||
end
|
||||
end
|
||||
super(id)
|
||||
end
|
||||
|
||||
def create(data)
|
||||
fields = data.keys & @fields
|
||||
@sql.do("insert into #{@name} (" +
|
||||
fields.join(", ") +
|
||||
") values (" + fields.collect do "?" end.join(", ") +
|
||||
")",
|
||||
*(fields.collect do |field| data[field] end))
|
||||
@sql.select_one("select last_insert_id()")[0]
|
||||
end
|
||||
|
||||
def modify(id, data)
|
||||
fields = (@fields & self[id].keys) - ["id"]
|
||||
changes = fields.inject([]) do |ch, field|
|
||||
if data.has_key?(field) and data[field] != self[id][field]
|
||||
ch.push(field)
|
||||
else
|
||||
ch
|
||||
end
|
||||
end
|
||||
@sql.do("update #{@name} set " +
|
||||
changes.collect do |field|
|
||||
field + " = ?"
|
||||
end.join(", ") + " where id = #{id}",
|
||||
*(changes.collect do |field| data[field] end)
|
||||
) if changes.length > 0
|
||||
end
|
||||
|
||||
def delete(id)
|
||||
@sql.do("delete from #{@name} where id = #{id}")
|
||||
super(id)
|
||||
end
|
||||
end
|
||||
|
Loading…
Reference in New Issue