Initial clean commit (reset history)

This commit is contained in:
choibk 2026-01-05 15:54:29 +09:00
commit 2795213dda
427 changed files with 46708 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
/redmine_dmsf.iml
Gemfile.lock
.history
tmp/

147
.rubocop.yml Normal file
View File

@ -0,0 +1,147 @@
# Redmine plugin for Document Management System "Features"
#
# Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
plugins:
- rubocop-rails
- rubocop-performance
AllCops:
TargetRubyVersion: 3.2
TargetRailsVersion: 7.1
SuggestExtensions: false
NewCops: enable
Exclude:
- '**/vendor/**/*'
# Rules for DMSF
Layout/LineLength:
Exclude:
- app/models/dmsf_query.rb
Lint/SymbolConversion:
Exclude:
- test/unit/dmsf_file_revision_test.rb
Lint/ScriptPermission:
Exclude:
- extra/xapian_indexer.rb
Lint/UselessAssignment:
Exclude:
- lib/redmine_dmsf/lockable.rb
Naming/BlockForwarding:
EnforcedStyle: explicit
Metrics/AbcSize:
Enabled: false
Metrics/BlockLength:
Enabled: false
Metrics/BlockNesting:
Enabled: false
Metrics/ClassLength:
Enabled: false
Metrics/CyclomaticComplexity:
Enabled: false
Metrics/MethodLength:
Enabled: false
Metrics/ModuleLength:
Enabled: false
Metrics/ParameterLists:
Enabled: false
Metrics/PerceivedComplexity:
Enabled: false
Naming/AccessorMethodName:
Exclude:
- lib/dav4rack/resource.rb
Naming/PredicatePrefix:
Exclude:
- patches/attachable_patch.rb
Style/HashSyntax:
EnforcedShorthandSyntax: either
Style/ZeroLengthPredicate:
Exclude:
- lib/redmine_dmsf/webdav/dmsf_resource.rb
Rails/BulkChangeTable:
Exclude:
- db/migrate/20170217141601_add_dmsf_not_inheritable_to_custom_fields.rb
Rails/DangerousColumnNames:
Exclude:
- db/migrate/20170330131901_create_dmsf_folder_permissions.rb
Rails/DynamicFindBy:
AllowedMethods:
- find_by_token
- find_by_param
Rails/SkipsModelValidations:
Exclude:
- app/helpers/dmsf_upload_helper.rb # touch is Okay
- app/models/dmsf_workflow.rb # update doesn't work here
- lib/redmine_dmsf/patches/user_patch.rb
- lib/redmine_dmsf/patches/role_patch.rb
- db/migrate/20170526144701_dmsf_attachable.rb
- db/migrate/20170421101901_dmsf_file_container_rollback.rb
- db/migrate/20170118142001_dmsf_file_container.rb
- db/migrate/20160222140401_approval_workflow_std_fields.rb
- db/migrate/20160215125801_approval_workflow_status.rb
- db/migrate/20140519133201_trash_bin.rb
- db/migrate/07_dmsf_1_4_4.rb
- db/migrate/20240829093801_rename_dmsf_digest_token.rb
Rails/ThreeStateBooleanColumn:
Exclude:
- db/migrate/04_dmsf_0_9_0.rb
- db/migrate/20170217141601_add_dmsf_not_inheritable_to_custom_fields.rb
Rails/UniqueValidationWithoutIndex:
Exclude:
- app/models/dmsf_folder.rb # TODO: Create a case insensitive index in DB migrations
- app/models/dmsf_file.rb
- app/models/dmsf_workflow_step.rb # Impossible due to steps sorting
Style/ExpandPathArguments:
Enabled: false
Style/FrozenStringLiteralComment:
Exclude:
- init.rb
Style/OpenStructUse:
Exclude:
- lib/dav4rack/utils.rb
Style/OptionalBooleanParameter:
Exclude:
- lib/redmine_dmsf/field_formats/dmsf_file_format.rb
- lib/redmine_dmsf/field_formats/dmsf_file_revision_format.rb

1256
CHANGELOG.md Normal file

File diff suppressed because it is too large Load Diff

46
CODE_OF_CONDUCT.md Normal file
View File

@ -0,0 +1,46 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at karel.picman@kontron.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/

14
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,14 @@
## How to contribute to Redmine DMSF plugin
#### **Did you find a bug?**
* **Ensure the bug was not already reported** by searching on GitHub under [Issues](https://github.com/danmunn/redmine_dmsf/issues).
* If you're unable to find an open issue addressing the problem, [open a new one](https://github.com/danmunn/redmine_dmsf/issues/new). Be sure to include a **title and clear description**, as much relevant information as possible, **Redmine and the plugin version**, a part of the **production.log** related to the issue and **screen-shots**.
#### **A wrong or missing translation?**
* Feel free to update language files (_config/locales/*.yml_) in your mother language or languages you are familiar with and send them back to us via a **pull request**.
#### **Do you want to patch the existing code?**
* Make a new fork of the current development branch (_devel_). Make your changes and [create a pull request](https://github.com/danmunn/redmine_dmsf/compare). Only pull requests into the **development branch** except those fixing serious errors will be accepted.

38
Gemfile Normal file
View File

@ -0,0 +1,38 @@
# frozen_string_literal: true
# Redmine plugin for Document Management System "Features"
#
# Vít Jonáš <vit.jonas@gmail.com>, Daniel Munn <dan.munn@munnster.co.uk>, Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
source 'https://rubygems.org' do
gem 'ox' # Dav4Rack
gem 'rake' unless Dir.exist?(File.expand_path('../../redmine_dashboard', __FILE__))
gem 'uuidtools'
gem 'zip-zip' unless Dir.exist?(File.expand_path('../../vault', __FILE__))
# Redmine extensions
gem 'active_record_union'
gem 'simple_enum'
group :xapian do
gem 'xapian-ruby'
end
unless %w[easy_gantt custom_tables]
.any? { |plugin| Dir.exist?(File.expand_path("../../#{plugin}", __FILE__)) }
group :test do
gem 'rails-controller-testing'
end
end
end

617
LICENSE.md Normal file
View File

@ -0,0 +1,617 @@
# GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc.
<https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies of this
license document, but changing it is not allowed.
## Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom
to share and change all versions of a program--to make sure it remains
free software for all its users. We, the Free Software Foundation, use
the GNU General Public License for most of our software; it applies
also to any other work released this way by its authors. You can apply
it to your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you
have certain responsibilities if you distribute copies of the
software, or if you modify it: responsibilities to respect the freedom
of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the
manufacturer can do so. This is fundamentally incompatible with the
aim of protecting users' freedom to change the software. The
systematic pattern of such abuse occurs in the area of products for
individuals to use, which is precisely where it is most unacceptable.
Therefore, we have designed this version of the GPL to prohibit the
practice for those products. If such problems arise substantially in
other domains, we stand ready to extend this provision to those
domains in future versions of the GPL, as needed to protect the
freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish
to avoid the special danger that patents applied to a free program
could make it effectively proprietary. To prevent this, the GPL
assures that patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
## TERMS AND CONDITIONS
### 0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds
of works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of
an exact copy. The resulting work is called a "modified version" of
the earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user
through a computer network, with no transfer of a copy, is not
conveying.
An interactive user interface displays "Appropriate Legal Notices" to
the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
### 1. Source Code.
The "source code" for a work means the preferred form of the work for
making modifications to it. "Object code" means any non-source form of
a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users can
regenerate automatically from other parts of the Corresponding Source.
The Corresponding Source for a work in source code form is that same
work.
### 2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not convey,
without conditions so long as your license otherwise remains in force.
You may convey covered works to others for the sole purpose of having
them make modifications exclusively for you, or provide you with
facilities for running those works, provided that you comply with the
terms of this License in conveying all material for which you do not
control copyright. Those thus making or running the covered works for
you must do so exclusively on your behalf, under your direction and
control, on terms that prohibit them from making any copies of your
copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under the
conditions stated below. Sublicensing is not allowed; section 10 makes
it unnecessary.
### 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such
circumvention is effected by exercising rights under this License with
respect to the covered work, and you disclaim any intention to limit
operation or modification of the work as a means of enforcing, against
the work's users, your or third parties' legal rights to forbid
circumvention of technological measures.
### 4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
### 5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these
conditions:
- a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
- b) The work must carry prominent notices stating that it is
released under this License and any conditions added under
section 7. This requirement modifies the requirement in section 4
to "keep intact all notices".
- c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
- d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
### 6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms of
sections 4 and 5, provided that you also convey the machine-readable
Corresponding Source under the terms of this License, in one of these
ways:
- a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
- b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the Corresponding
Source from a network server at no charge.
- c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
- d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
- e) Convey the object code using peer-to-peer transmission,
provided you inform other peers where the object code and
Corresponding Source of the work are being offered to the general
public at no charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal,
family, or household purposes, or (2) anything designed or sold for
incorporation into a dwelling. In determining whether a product is a
consumer product, doubtful cases shall be resolved in favor of
coverage. For a particular product received by a particular user,
"normally used" refers to a typical or common use of that class of
product, regardless of the status of the particular user or of the way
in which the particular user actually uses, or expects or is expected
to use, the product. A product is a consumer product regardless of
whether the product has substantial commercial, industrial or
non-consumer uses, unless such uses represent the only significant
mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to
install and execute modified versions of a covered work in that User
Product from a modified version of its Corresponding Source. The
information must suffice to ensure that the continued functioning of
the modified object code is in no case prevented or interfered with
solely because modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or
updates for a work that has been modified or installed by the
recipient, or for the User Product in which it has been modified or
installed. Access to a network may be denied when the modification
itself materially and adversely affects the operation of the network
or violates the rules and protocols for communication across the
network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
### 7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders
of that material) supplement the terms of this License with terms:
- a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
- b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
- c) Prohibiting misrepresentation of the origin of that material,
or requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
- d) Limiting the use for publicity purposes of names of licensors
or authors of the material; or
- e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
- f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions
of it) with contractual assumptions of liability to the recipient,
for any liability that these contractual assumptions directly
impose on those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions; the
above requirements apply either way.
### 8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your license
from a particular copyright holder is reinstated (a) provisionally,
unless and until the copyright holder explicitly and finally
terminates your license, and (b) permanently, if the copyright holder
fails to notify you of the violation by some reasonable means prior to
60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
### 9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or run
a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
### 10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
### 11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims owned
or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within the
scope of its coverage, prohibits the exercise of, or is conditioned on
the non-exercise of one or more of the rights that are specifically
granted under this License. You may not convey a covered work if you
are a party to an arrangement with a third party that is in the
business of distributing software, under which you make payment to the
third party based on the extent of your activity of conveying the
work, and under which the third party grants, to any of the parties
who would receive the covered work from you, a discriminatory patent
license (a) in connection with copies of the covered work conveyed by
you (or copies made from those copies), or (b) primarily for and in
connection with specific products or compilations that contain the
covered work, unless you entered into that arrangement, or that patent
license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
### 12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under
this License and any other pertinent obligations, then as a
consequence you may not convey it at all. For example, if you agree to
terms that obligate you to collect a royalty for further conveying
from those to whom you convey the Program, the only way you could
satisfy both those terms and this License would be to refrain entirely
from conveying the Program.
### 13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
### 14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions
of the GNU General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in
detail to address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies that a certain numbered version of the GNU General Public
License "or any later version" applies to it, you have the option of
following the terms and conditions either of that numbered version or
of any later version published by the Free Software Foundation. If the
Program does not specify a version number of the GNU General Public
License, you may choose any version ever published by the Free
Software Foundation.
If the Program specifies that a proxy can decide which future versions
of the GNU General Public License can be used, that proxy's public
statement of acceptance of a version permanently authorizes you to
choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
### 15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT
WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND
PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE
DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR
CORRECTION.
### 16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR
CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES
ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT
NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR
LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM
TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER
PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
### 17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.

339
README.md Normal file
View File

@ -0,0 +1,339 @@
# Redmine DMSF Plugin 4.2.3
[![GitHub CI](https://github.com/picman/redmine_dmsf/actions/workflows/rubyonrails.yml/badge.svg?branch=master)](https://github.com/picman/redmine_dmsf/actions/workflows/rubyonrails.yml)
[![Support Ukraine Badge](https://bit.ly/support-ukraine-now)](https://github.com/support-ukraine/support-ukraine)
Redmine DMSF is Document Management System Features plugin for Redmine issue tracking system; It is aimed to replace current Redmine's Documents module.
Redmine DMSF now comes bundled with WebDAV functionality: if switched on within plugin settings this will be accessible from _/dmsf/webdav_.
WebDAV functionality is provided through Dav4Rack library.
The development has been supported by [Kontron](https://www.kontron.com) and has been released as open source thanks to their generosity.
Project home: <https://github.com/picman/redmine_dmsf>
Redmine Document Management System "Features" plugin is distributed under GNU General Public License v3 (GPL).
33Redmine is a flexible project management web application, released under the terms of the GNU General Public License v2 (GPL) at <https://www.redmine.org/>
Further information about the GPL license can be found at
<https://www.gnu.org/licenses/gpl-3.0.html>
## Features
* Directory structure
* Document versioning / revision history
* Document locking
* Multi (drag/drop depending on browser) upload/download
* Direct document or document link sending via email
* Configurable document approval workflow
* Document access auditing
* Integration with Redmine's activity feed
* Wiki macros for a quick content linking
* Full read/write WebDAV functionality
* Optional document content full-text search
* Documents and folders' symbolic links
* Trash bin
* Documents attachable to issues
* Office documents are displayed inline
* Editing of office documents
* REST API
* DMS Document revision as a custom field type
* Compatible with Redmine 6
## Dependencies
* Redmine 6.1 or higher
### Full-text search (optional)
#### Indexing
If you want to use full-text search features, you must setup file content indexing.
It is necessary to index DMSF files with omindex before searching attempts to receive some output:
1. Change the configuration part of redmine_dmsf/extra/xapian_indexer.rb file according to your environment.
(The path to the index database set in xapian_indexer.rb must corresponds to the path set in the plugin's settings.)
2. Run `ruby redmine_dmsf/extra/xapian_indexer.rb -v`
This command should be run on regular basis (e.g. from cron)
Example of cron job (once per hour at 8th minute):
8 * * * * root /usr/bin/ruby redmine_dmsf/extra/xapian_indexer.rb
See redmine_dmsf/extra/xapian_indexer.rb for help.
#### Searching
If you want to use fulltext search abilities, install xapian packages. In case of using of Bitnami
stack or Ruby installed via RVM it might be necessary to install Xapian bindings from sources. See https://xapian.org
for details.
To index some files with omega you may have to install some other packages like
xpdf, antiword, ...
From Omega documentation:
* HTML (.html, .htm, .shtml, .shtm, .xhtml, .xhtm)
* PHP (.php) - our HTML parser knows to ignore PHP code
* text files (.txt, .text)
* SVG (.svg)
* CSV (Comma-Separated Values) files (.csv)
* PDF (.pdf) if pdftotext is available (comes with poppler or xpdf)
* PostScript (.ps, .eps, .ai) if ps2pdf (from ghostscript) and pdftotext (comes with poppler or xpdf) are available
* OpenOffice/StarOffice documents (.sxc, .stc, .sxd, .std, .sxi, .sti, .sxm, .sxw, .sxg, .stw) if unzip is available
* OpenDocument format documents (.odt, .ods, .odp, .odg, .odc, .odf, .odb, .odi, .odm, .ott, .ots, .otp, .otg, .otc, .otf, .oti, .oth) if unzip is available
* MS Word documents (.dot) if antiword is available (.doc files are left to libmagic, as they may actually be RTF (AbiWord saves RTF when asked to save as .doc, and Microsoft Word quietly loads RTF files with a .doc extension), or plain-text).
* MS Excel documents (.xls, .xlb, .xlt, .xlr, .xla) if xls2csv is available (comes with catdoc)
* MS Powerpoint documents (.ppt, .pps) if catppt is available (comes with catdoc)
* MS Office 2007 documents (.docx, .docm, .dotx, .dotm, .xlsx, .xlsm, .xltx, .xltm, .pptx, .pptm, .potx, .potm, .ppsx, .ppsm) if unzip is available
* Wordperfect documents (.wpd) if wpd2text is available (comes with libwpd)
* MS Works documents (.wps, .wpt) if wps2text is available (comes with libwps)
* MS Outlook message (.msg) if perl with Email::Outlook::Message and HTML::Parser modules is available
* MS Publisher documents (.pub) if pub2xhtml is available (comes with libmspub)
* AbiWord documents (.abw)
* Compressed AbiWord documents (.zabw)
* Rich Text Format documents (.rtf) if unrtf is available
* Perl POD documentation (.pl, .pm, .pod) if pod2text is available
* reStructured text (.rst, .rest) if rst2html is available (comes with docutils)
* Markdown (.md, .markdown) if markdown is available
* TeX DVI files (.dvi) if catdvi is available
* DjVu files (.djv, .djvu) if djvutxt is available
* XPS files (.xps) if unzip is available
* Debian packages (.deb, .udeb) if dpkg-deb is available
* RPM packages (.rpm) if rpm is available
* Atom feeds (.atom)
* MAFF (.maff) if unzip is available
* MHTML (.mhtml, .mht) if perl with MIME::Tools is available
* MIME email messages (.eml) and USENET articles if perl with MIME::Tools and HTML::Parser is available
* vCard files (.vcf, .vcard) if perl with Text::vCard is available
You can use following commands to install some of the required indexing tools:
On Debian use:
```
sudo apt-get install xapian-omega ruby-xapian libxapian-dev poppler-utils antiword unzip catdoc libwpd-tools \
libwps-tools gzip unrtf catdvi djview djview3 uuid uuid-dev xz-utils libemail-outlook-message-perl
```
On Ubuntu use:
```
sudo apt-get install xapian-omega ruby-xapian libxapian-dev poppler-utils antiword unzip catdoc libwpd-tools \
libwps-tools gzip unrtf catdvi djview djview3 uuid uuid-dev xz-utils libemail-outlook-message-perl
```
On CentOS use:
```
sudo yum install xapian-core xapian-bindings-ruby libxapian-dev poppler-utils antiword unzip catdoc libwpd-tools \
libwps-tools gzip unrtf catdvi djview djview3 uuid uuid-dev xz libemail-outlook-message-perl
```
## Inline displaying of office documents (optional)
If LibreOffice binary `libreoffice` is present in the server, office documents (.odt, .ods,...) are displayed inline.
The command must be runable by the web app's user. Test it in advance, e.g:
`sudo -u www-data libreoffice --convert-to pdf my_document.odt`
`libreoffice` package is available in the most of Linux distributions, e.g. on Debain based systems:
```
sudo apt install libreoffice liblibreoffice-java
```
## Usage
DMSF is designed to act as project module, so it must be checked as an enabled module within the project settings.
Search will now automatically search DMSF content when a Redmine search is performed, additionally a "Documents" and "Folders" check box will be visible, allowing you to search DMSF content exclusively.
## Linking DMSF object from Wiki entries (macros)
You can link DMSF object from Wikis using a macro tag `{{ }}`. List of available macros with their description is
available from the wiki's toolbar.
## Hooks
You can implement these hooks in your plugin and extend DMSF functionality in certain events.
E.g.
class DmsfUploadControllerHooks < Redmine::Hook::Listener
def dmsf_upload_controller_after_commit(context={})
context[:controller].flash[:info] = 'Okay'
end
end
**dmsf_upload_controller_after_commit**
Called after all uploaded files are committed.
parameters: *files*
**dmsf_helper_upload_after_commit**
Called after an individual file is committed. The controller is not available.
Parameters: *file*
**dmsf_workflow_controller_before_approval**
Called before an approval. If the hook returns false, the approval is not recorded.
parameters: *revision*, *step_action*
**dmsf_files_controller_before_view**
Allows a preview of the file by an external plugin. If the hook returns true, the file is not sent by DMSF. It is
expected that the file is sent by the hook.
parameters: *file*
## Setup / Upgrade
You can either clone the master branch or download the latest zipped version. Before installing ensure that the Redmine
instance is stopped.
git clone git@github.com:picman/redmine_dmsf.git
wget https://github.com/picman/redmine_dmsf/archive/master.zip
1. In case of upgrade **BACKUP YOUR DATABASE, ORIGINAL PLUGIN AND THE FOLDER WITH DOCUMENTS** first!!!
2. Put redmine_dmsf plugin directory into plugins. The plugins sub-directory must be named just **redmine_dmsf**. In case
of need rename _redmine_dmsf-x.y.z_ to *redmine_dmsf*.
3. **Go to the redmine directory**
`cd redmine`
4. Install dependencies:
`bundle install`
4.1 In production environment
bundle config set --local without 'development test'
bundle install
4.2 Without Xapian fulltext search (on Windows)
bundle config set --local without 'xapian'
bundle install
5. Initialize/Update database:
`RAILS_ENV=production bundle exec rake redmine:plugins:migrate NAME=redmine_dmsf`
6. Install assets
`RAILS_ENV="production" bundle exec rake assets:precompile`
7. The access rights must be set for web server, e.g.:
`chown -R www-data:www-data plugins/redmine_dmsf`
8. Restart the web server, e.g.:
`systemctl restart apache2`
9. You should configure the plugin via Redmine interface: Administration -> Plugins -> DMSF -> Configure. (You should check and then save the plugin's configuration after each upgrade.)
10. Don't forget to grant permissions for DMSF in Administration -> Roles and permissions
11. Assign DMSF permissions to appropriate roles.
12. There are a few rake tasks:
I) To convert documents from the standard Redmine document module
Available options:
* project - id or identifier of a project (default to all projects)
* dry_run - perform just a check without any conversion
* issues - Convert also files attached to issues
Example:
rake redmine:dmsf_convert_documents project=test RAILS_ENV="production"
(If you don't run the rake task as the web server user, don't forget to change the ownership of the imported files, e.g.
chown -R www-data:www-data /redmine/files/dmsf
afterwards)
II) To alert all users who are expected to do an approval in the current approval steps
Example:
rake redmine:dmsf_alert_approvals RAILS_ENV="production"
III) To create missing checksums for all document revisions
Available options:
* dry_run - test, no changes to the database
* forceSHA256 - replace old MD5 with SHA256
Example:
bundle exec rake redmine:dmsf_create_digests RAILS_ENV="production"
bundle exec rake redmine:dmsf_create_digests forceSHA256=1 RAILS_ENV="production"
bundle exec rake redmine:dmsf_create_digests dry_run=1 RAILS_ENV="production"
IV) To maintain DMSF
* Remove all files with no database record from the document directory
* Remove all links project_id = -1 (added links to an issue which hasn't been created)
Available options:
* dry_run - No physical deletion but to list of all unused files only
Example:
rake redmine:dmsf_maintenance RAILS_ENV="production"
rake redmine:dmsf_maintenance dry_run=1 RAILS_ENV="production"
### WebDAV
In order to enable WebDAV module, it is necessary to put the following code into yor `config/additional_environment.rb`
```ruby
# Redmine DMSF's WebDAV
require Rails.root.join('plugins', 'redmine_dmsf', 'lib', 'redmine_dmsf', 'webdav', 'custom_middleware').to_s
config.middleware.insert_before ActionDispatch::Cookies, RedmineDmsf::Webdav::CustomMiddleware
```
### Installation in a sub-uri
In order to documents and folders are available via WebDAV in case that the Redmine is configured to be run in a sub-uri
it's necessary to add the following configuration option into your `config/additional_environment.rb`:
```ruby
config.relative_url_root = '/redmine'
```
## Uninstalling DMSF
Before uninstalling the DMSF plugin, please ensure that the Redmine instance is stopped.
1. `cd [redmine-install-dir]`
2. `rake redmine:plugins:migrate NAME=redmine_dmsf VERSION=0 RAILS_ENV=production`
3. `rm plugins/redmine_dmsf -Rf`
After these steps re-start your instance of Redmine.
## Contributing
If you've added something, why not share it. Fork the repository (github.com/picman/redmine_dmsf),
make the changes and send a pull request to the maintainers.
Changes with tests, and full documentation are preferred.
## Additional Documentation
[CHANGELOG.md](CHANGELOG.md) - Project changelog
---
Special thanks to <a href="https://jetbrains.com"><img src="jetbrains-variant-3.svg" alt="JetBrains logo" width="59" height="68"></a> for providing an excellent IDE.

View File

@ -0,0 +1,158 @@
# frozen_string_literal: true
# Redmine plugin for Document Management System "Features"
#
# Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
require "#{File.dirname(__FILE__)}/../../lib/redmine_dmsf/preview"
# Context menu controller
class DmsfContextMenusController < ApplicationController
helper :context_menus
helper :watchers
helper :dmsf
before_action :find_folder
before_action :find_dmsf_file
before_action :find_dmsf_folder
before_action :find_dmsf_project
def dmsf
if @dmsf_file
@locked = @dmsf_file.locked?
@project = @dmsf_file.project
@allowed = User.current.allowed_to? :file_manipulation, @project
@unlockable = @allowed && @dmsf_file.unlockable? && (!@dmsf_file.locked_for_user? ||
User.current.allowed_to?(:force_file_unlock, @project))
@email_allowed = User.current.allowed_to?(:email_documents, @project)
@preview = RedmineDmsf.office_bin.present? && RedmineDmsf::Preview.office_available?
elsif @dmsf_folder
@locked = @dmsf_folder.locked?
@project = @dmsf_folder.project
@allowed = User.current.allowed_to?(:folder_manipulation, @project)
@unlockable = @allowed && @dmsf_folder.unlockable? && (!@dmsf_folder.locked_for_user? ||
User.current.allowed_to?(:force_file_unlock, @project))
@email_allowed = User.current.allowed_to?(:email_documents, @project)
elsif @dmsf_link # url link
@locked = false
@unlockable = false
@project = @dmsf_link.project
@allowed = User.current.allowed_to? :file_manipulation, @project
@email_allowed = false
elsif @dmsf_project # project
@locked = false
@unlockable = false
@project = @dmsf_project
@allowed = User.current.allowed_to? :view_project_watchers, @project
@email_allowed = false
else # multiple selection
@project = project
@locked = false
@unlockable = false
@allowed = User.current.allowed_to?(:file_manipulation, @project) &&
User.current.allowed_to?(:folder_manipulation, @project)
@email_allowed = User.current.allowed_to?(:email_documents, @project)
end
@back_url = params['back_url'].presence || dmsf_folder_path(id: @project, folder_id: @folder)
@notifications = Setting.notified_events.include?('dmsf_legacy_notifications')
render layout: false
rescue ActiveRecord::RecordNotFound
render_404
end
def trash
if @dmsf_file
@project = @dmsf_file.project
@allowed_restore = User.current.allowed_to? :file_manipulation, @project
@allowed_delete = User.current.allowed_to? :file_delete, @project
elsif @dmsf_folder
@project = @dmsf_folder.project
@allowed_restore = User.current.allowed_to? :folder_manipulation, @project
@allowed_delete = @allowed_restore
elsif @dmsf_link # url link
@project = @dmsf_link.project
@allowed_restore = User.current.allowed_to? :file_manipulation, @project
@allowed_delete = User.current.allowed_to? :file_delete, @project
else # multiple selection
@project = project
@allowed_restore = User.current.allowed_to?(:file_manipulation, @project) &&
User.current.allowed_to?(:folder_manipulation, @project)
@allowed_delete = User.current.allowed_to?(:file_delete, @project) &&
User.current.allowed_to?(:folder_manipulation, @project)
end
render layout: false
rescue ActiveRecord::RecordNotFound
render_404
end
private
def project
prj = nil
params[:ids].each do |id|
item = case id
when /file-(\d+)/
DmsfFile.find_by(id: Regexp.last_match(1))
when /(file|url)-link-(\d+)/
DmsfLink.find_by(id: Regexp.last_match(2))
when /folder-(\d+)/
DmsfFolder.find_by(id: Regexp.last_match(1))
when /folder-link-(\d+)/
DmsfLink.find_by(id: Regexp.last_match(1))
end
prj ||= item&.project
return nil if prj != item&.project
end
prj
end
def find_folder
@folder = DmsfFolder.find params[:folder_id] if params[:folder_id].present?
rescue ActiveRecord::RecordNotFound
render_404
end
def find_dmsf_file
return unless params[:ids].present? && params[:ids].size == 1 && !@dmsf_folder
case params[:ids][0]
when /file-(\d+)/
@dmsf_file = DmsfFile.find_by(id: Regexp.last_match(1))
when /(file|url)-link-(\d+)/
@dmsf_link = DmsfLink.find_by(id: Regexp.last_match(2))
@dmsf_file = DmsfFile.find_by(id: @dmsf_link.target_id) if @dmsf_link && @dmsf_link.target_type != 'DmsfUrl'
end
end
def find_dmsf_folder
return unless params[:ids].present? && params[:ids].size == 1 && !@dmsf_file
case params[:ids][0]
when /folder-(\d+)/
@dmsf_folder = DmsfFolder.find_by(id: Regexp.last_match(1))
when /folder-link-(\d+)/
@dmsf_link = DmsfLink.find_by(id: Regexp.last_match(1))
@dmsf_folder = DmsfFolder.find_by(id: @dmsf_link.target_id) if @dmsf_link
end
end
def find_dmsf_project
return unless params[:ids].present? && params[:ids].size == 1 && !@dmsf_project &&
params[:ids][0] =~ /project-(\d+)/
@dmsf_project = Project.find_by(id: Regexp.last_match(1))
end
end

View File

@ -0,0 +1,855 @@
# frozen_string_literal: true
# Redmine plugin for Document Management System "Features"
#
# Vít Jonáš <vit.jonas@gmail.com>, Daniel Munn <dan.munn@munnster.co.uk>, Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
require "#{File.dirname(__FILE__)}/../../lib/redmine_dmsf/dmsf_zip"
# DMSF controller
class DmsfController < ApplicationController
include RedmineDmsf::DmsfZip
before_action :find_project, except: %i[expand_folder index digest reset_digest]
before_action :authorize, except: %i[expand_folder index digest reset_digest]
before_action :authorize_global, only: [:index]
before_action :find_folder,
except: %i[new create edit_root save_root add_email append_email autocomplete_for_user digest
reset_digest]
before_action :find_parent, only: %i[new create delete]
before_action :permissions?
# Also try to lookup folder by title if this is an API call
before_action :find_folder_by_title, only: [:show]
before_action :query, only: %i[expand_folder show trash empty_trash index]
before_action :project_roles, only: %i[new edit create save]
before_action :find_target_folder, only: %i[copymove entries_operation]
before_action :check_target_folder, only: [:entries_operation]
before_action :require_login, only: %i[digest reset_digest]
accept_api_auth :show, :create, :save, :delete, :entries_operation
helper :custom_fields
helper :dmsf_folder_permissions
helper :queries
include QueriesHelper
helper :dmsf_queries
include DmsfQueriesHelper
helper :context_menus
helper :watchers
def permissions?
if !DmsfFolder.permissions?(@folder, allow_system: false)
render_403
elsif @folder && @project && (@folder.project != @project)
render_404
end
true
end
def expand_folder
@idnt = params[:idnt].present? ? params[:idnt].to_i + 1 : 0
@query.project = Project.find_by(id: params[:project_id]) if params[:project_id].present?
@query.dmsf_folder_id = @folder&.id
@query.deleted = false
@query.sub_projects = true
respond_to do |format|
format.js { render action: 'query_rows' }
end
end
def index
@query.sub_projects = true
show
end
def show
@system_folder = @folder&.system
@locked = @folder&.locked?
@folder_manipulation_allowed = User.current.allowed_to?(:folder_manipulation, @project)
@file_manipulation_allowed = User.current.allowed_to?(:file_manipulation, @project)
@trash_enabled = @folder_manipulation_allowed && @file_manipulation_allowed
@notifications = Setting.notified_events.include?('dmsf_legacy_notifications')
@query.dmsf_folder_id = @folder&.id
@query.deleted = false
@query.sub_projects |= RedmineDmsf.dmsf_projects_as_subfolders?
if @folder&.deleted? || (params[:folder_title].present? && !@folder)
render_404
return
end
if @query.valid?
respond_to do |format|
format.html do
# Warn about searching in sub-folders
if @folder && params['set_filter'].present? && params['f'].present?
flash.now[:warning] = l(:notice_search_in_subfolders)
end
@dmsf_count = @query.dmsf_count
@dmsf_pages = Paginator.new @dmsf_count, per_page_option, params['page']
render layout: !request.xhr?
end
format.api { @offset, @limit = api_offset_and_limit }
format.csv do
send_data query_to_csv(@query.dmsf_nodes, @query), type: 'text/csv; header=present', filename: 'dmsf.csv'
end
end
else
respond_to do |format|
format.html do
@dmsf_count = 0
@dmsf_pages = Paginator.new @dmsf_count, per_page_option, params['page']
render layout: !request.xhr?
end
format.any(:atom, :csv, :pdf) { head :unprocessable_entity }
format.api { render_validation_errors(@query) }
end
end
end
def trash
@folder_manipulation_allowed = User.current.allowed_to? :folder_manipulation, @project
@file_manipulation_allowed = User.current.allowed_to? :file_manipulation, @project
@file_delete_allowed = User.current.allowed_to? :file_delete, @project
@query.deleted = true
respond_to do |format|
format.html do
@dmsf_count = @query.dmsf_count
@dmsf_pages = Paginator.new @dmsf_count, per_page_option, params['page']
render layout: !request.xhr?
end
end
end
def download_email_entries
file_path = helpers.email_entry_tmp_file_path(params[:entry])
if File.exist?(file_path)
# IE has got a tendency to cache files
expires_in 0.years, 'must-revalidate' => true
send_file(
file_path,
filename: 'Documents.zip',
type: 'application/zip',
disposition: 'attachment'
)
else
render_404
end
rescue StandardError => e
flash[:error] = e.message
end
def copymove
@ids = params[:ids].uniq
member = Member.find_by(project_id: @project.id, user_id: User.current.id)
@fast_links = member&.dmsf_fast_links
unless @fast_links
@projects = DmsfFolder.allowed_target_projects_on_copy
@folders = DmsfFolder.directory_tree(@target_project)
@target_folder = DmsfFolder.visible.find(params[:target_folder_id]) if params[:target_folder_id].present?
end
@back_url = params[:back_url]
render layout: !request.xhr?
end
def entries_operation
# Download/Email
if @selected_folders.blank? && @selected_files.blank? && @selected_links.blank?
flash[:warning] = l(:warning_no_entries_selected)
redirect_back_or_default dmsf_folder_path(id: @project, folder_id: @folder)
return
end
if @selected_dir_links.present? && (params[:email_entries].present? || params[:download_entries].present?)
@selected_folders = DmsfLink.where(id: @selected_dir_links).pluck(:target_id) | @selected_folders
end
if @selected_file_links.present? && (params[:email_entries].present? || params[:download_entries].present?)
@selected_files = DmsfLink.where(id: @selected_file_links).pluck(:target_id) | @selected_files
end
begin
if params[:email_entries].present?
email_entries @selected_folders, @selected_files
elsif params[:restore_entries].present?
restore_entries @selected_folders, @selected_files, @selected_links
redirect_back_or_default dmsf_folder_path(id: @project, folder_id: @folder)
elsif params[:delete_entries].present?
delete_entries @selected_folders, @selected_files, @selected_links, false
redirect_back_or_default dmsf_folder_path id: @project, folder_id: @folder
elsif params[:destroy_entries].present?
delete_entries @selected_folders, @selected_files, @selected_links, true
redirect_back_or_default dmsf_folder_path(id: @project, folder_id: @folder)
elsif params[:copy_entries].present?
copy_entries @selected_folders, @selected_files, @selected_links
redirect_back_or_default dmsf_folder_path(id: @project, folder_id: @folder)
elsif params[:move_entries].present?
move_entries @selected_folders, @selected_files, @selected_links
redirect_back_or_default dmsf_folder_path(id: @project, folder_id: @folder)
else
download_entries @selected_folders, @selected_files
end
rescue DmsfFileNotFoundError
render_404
rescue DmsfAccessError
render_403
rescue StandardError => e
flash[:error] = e.message
Rails.logger.error e.message
redirect_back_or_default dmsf_folder_path(id: @project, folder_id: @folder)
end
end
def entries_email
file_path = helpers.email_entry_tmp_file_path(params[:email][:zipped_content])
params[:email][:zipped_content] = file_path
if params[:email][:to].strip.blank?
flash[:error] = l(:error_email_to_must_be_entered)
else
DmsfMailer.deliver_send_documents @project, params[:email].permit!, User.current
if File.exist?(file_path)
File.delete(file_path)
else
flash[:error] = l(:header_minimum_filesize)
end
flash[:notice] = l(:notice_email_sent, params[:email][:to])
end
redirect_back_or_default dmsf_folder_path(id: @project, folder_id: @folder)
end
def new
@folder = DmsfFolder.new
@pathfolder = @parent
render action: 'edit'
end
def edit
@parent = @folder.dmsf_folder
@pathfolder = copy_folder(@folder)
@force_file_unlock_allowed = User.current.allowed_to?(:force_file_unlock, @project)
@redirect_to_folder_id = params[:redirect_to_folder_id]
@notifications = Setting.notified_events.include?('dmsf_legacy_notifications')
end
def edit_root
@notifications = Setting.notified_events.include?('dmsf_legacy_notifications')
end
def create
@folder = DmsfFolder.new
@folder.project = @project
@folder.user = User.current
saved = @folder.update_from_params(params)
respond_to do |format|
format.js
format.api { render_validation_errors(@folder) unless saved }
format.html do
if saved
flash[:notice] = l(:notice_folder_created)
redirect_to dmsf_folder_path(id: @project, folder_id: @folder.dmsf_folder)
else
@pathfolder = @parent
render action: 'edit'
end
end
end
end
def save
unless params[:dmsf_folder]
redirect_to dmsf_folder_path(id: @project, folder_id: @folder)
return
end
@pathfolder = copy_folder(@folder)
saved = @folder.update_from_params(params)
respond_to do |format|
format.api { render_validation_errors(@folder) unless saved }
format.html do
if saved
flash[:notice] = l(:notice_folder_details_were_saved)
if @folder.project == @project
redirect_to_folder_id = params[:dmsf_folder][:redirect_to_folder_id]
redirect_to_folder_id = @folder.dmsf_folder.id if @folder.dmsf_folder && redirect_to_folder_id.blank?
end
redirect_to dmsf_folder_path(id: @project, folder_id: redirect_to_folder_id)
else
render action: 'edit'
end
end
end
end
def delete
commit = params[:commit] == 'yes'
result = @folder.delete(commit: commit)
if result
flash[:notice] = l(:notice_folder_deleted)
else
flash[:error] = @folder.errors.full_messages.to_sentence
end
respond_to do |format|
format.html do
if commit
redirect_to trash_dmsf_path(@project)
else
redirect_to dmsf_folder_path(id: @project, folder_id: @parent)
end
end
format.api { result ? render_api_ok : render_validation_errors(@folder) }
end
end
def restore
if @folder.restore
flash[:notice] = l(:notice_dmsf_folder_restored)
else
flash[:error] = @folder.errors.full_messages.to_sentence
end
redirect_to trash_dmsf_path(@project)
end
def save_root
if params[:project]
@project.dmsf_description = params[:project][:dmsf_description]
if @project.save
flash[:notice] = l(:notice_folder_details_were_saved)
else
flash[:error] = @project.errors.full_messages.to_sentence
end
end
redirect_to dmsf_folder_path(id: @project)
end
def notify_activate
if @folder&.notification || (@folder.nil? && @project.dmsf_notification)
flash[:warning] = l(:warning_folder_notifications_already_activated)
else
if @folder
@folder.notify_activate
else
@project.dmsf_notification = true
@project.save!
end
flash[:notice] = l(:notice_folder_notifications_activated)
end
redirect_back_or_default dmsf_folder_path(id: @project, folder_id: @folder)
end
def notify_deactivate
if (@folder && !@folder.notification) || (@folder.nil? && !@project.dmsf_notification)
flash[:warning] = l(:warning_folder_notifications_already_deactivated)
else
if @folder
@folder.notify_deactivate
else
@project.dmsf_notification = nil
@project.save!
end
flash[:notice] = l(:notice_folder_notifications_deactivated)
end
redirect_back_or_default dmsf_folder_path(id: @project, folder_id: @folder)
end
def lock
if @folder.nil?
flash[:warning] = l(:warning_folder_unlockable)
elsif @folder.locked?
flash[:warning] = l(:warning_folder_already_locked)
else
@folder.lock!
flash[:notice] = l(:notice_folder_locked)
end
redirect_back_or_default dmsf_folder_path(id: @project, folder_id: @folder)
end
def unlock
if @folder.nil?
flash[:warning] = l(:warning_folder_unlockable)
elsif !@folder.locked?
flash[:warning] = l(:warning_folder_not_locked)
elsif @folder.locks[0].user == User.current || User.current.allowed_to?(:force_file_unlock, @project)
@folder.unlock!
flash[:notice] = l(:notice_folder_unlocked)
else
flash[:error] = l(:error_only_user_that_locked_folder_can_unlock_it)
end
redirect_back_or_default dmsf_folder_path(id: @project, folder_id: @folder)
end
def add_email
@principals = users_for_new_users
end
def append_email
@principals = Principal.where(id: params[:user_ids]).to_a
head :success if @principals.blank?
end
def autocomplete_for_user
@principals = users_for_new_users
respond_to do |format|
format.js
end
end
# Move the dragged object to the given destination
def drop
result = false
object = nil
if params[:dmsf_folder].present? &&
params[:dmsf_folder][:drag_id].present? &&
params[:dmsf_folder][:drop_id].present? &&
params[:dmsf_folder][:drag_id] =~ /(.+)-(\d+)/
type = Regexp.last_match(1)
id = Regexp.last_match(2)
if params[:dmsf_folder][:drop_id] =~ /^(\d+)(p|f)span$/
case type
when 'file'
object = DmsfFile.find_by(id: id)
when 'folder'
object = DmsfFolder.find_by(id: id)
when 'file-link', 'folder-link', 'url-link'
object = DmsfLink.find_by(id: id)
end
if object
case Regexp.last_match(2)
when 'p'
project = Project.find_by(id: Regexp.last_match(1))
if project &&
User.current.allowed_to?(:file_manipulation, project) &&
User.current.allowed_to?(:folder_manipulation, project)
result = object.move_to(project, nil)
end
when 'f'
dmsf_folder = DmsfFolder.find_by(id: Regexp.last_match(1))
if dmsf_folder
if dmsf_folder == object.dmsf_folder
object.errors.add(:base, l(:error_target_folder_same))
elsif object.dmsf_folder&.locked_for_user?
object.errors.add(:base, l(:error_folder_is_locked))
else
result = object.move_to(dmsf_folder.project, dmsf_folder)
end
end
end
end
end
end
respond_to do |format|
if result
format.js { head :ok }
else
format.js do
if object
flash.now[:error] = object.errors.full_messages.to_sentence
else
head :unprocessable_entity
end
end
end
end
end
def empty_trash
@query.deleted = true
@query.dmsf_nodes.each do |node|
case node.type
when 'folder'
folder = DmsfFolder.find_by(id: node.id)
folder&.delete commit: true
when 'file'
file = DmsfFile.find_by(id: node.id)
file&.delete commit: true
when /link$/
link = DmsfLink.find_by(id: node.id)
link&.delete commit: true
end
end
redirect_back_or_default trash_dmsf_path id: @project
end
# Reset WebDAV digest
def digest; end
def reset_digest
if request.post?
raise StandardError, l(:notice_account_wrong_password) unless User.current.check_password?(params[:password])
# We have to create a token first to prevent an autogenerated token's value
token = Token.create!(user_id: User.current.id, action: 'dmsf_webdav_digest')
token.value = ActiveSupport::Digest.hexdigest(
"#{User.current.login}:#{RedmineDmsf::Webdav::AUTHENTICATION_REALM}:#{params[:password]}"
)
token.save
flash[:notice] = l(:notice_webdav_digest_reset)
end
rescue StandardError => e
flash[:error] = e.message
ensure
redirect_to my_account_path
end
private
def users_for_new_users
User.active.visible.member_of(@project).like(params[:q]).order(:type, :lastname).to_a
end
def email_entries(selected_folders, selected_files)
raise DmsfAccessError unless User.current.allowed_to?(:email_documents, @project)
zip = Zip.new
zip_entries(zip, selected_folders, selected_files)
zipped_content = zip.finish
max_filesize = RedmineDmsf.dmsf_max_email_filesize
raise DmsfEmailMaxFileSizeError if max_filesize.positive? && File.size(zipped_content) > max_filesize * 1_048_576
zip.dmsf_files.each do |f|
# Action
audit = DmsfFileRevisionAccess.new
audit.user = User.current
audit.dmsf_file_revision = f.last_revision
audit.action = DmsfFileRevisionAccess::EMAIL_ACTION
audit.save!
end
# Notification
begin
DmsfMailer.deliver_files_downloaded @project, zip.dmsf_files, request.remote_ip
rescue StandardError => e
Rails.logger.error "Could not send email notifications: #{e.message}"
end
@email_params = {
zipped_content: helpers.tmp_entry_identifier(zipped_content),
folders: selected_folders,
files: selected_files,
subject: "#{@project.name} #{l(:label_dmsf_file_plural).downcase}",
from: RedmineDmsf.dmsf_documents_email_from,
reply_to: RedmineDmsf.dmsf_documents_email_reply_to
}
@back_url = params[:back_url]
render action: 'email_entries'
ensure
zip&.close
end
def download_entries(selected_folders, selected_files)
zip = Zip.new
zip_entries(zip, selected_folders, selected_files)
zip.dmsf_files.each do |f|
# Action
audit = DmsfFileRevisionAccess.new
audit.user = User.current
audit.dmsf_file_revision = f.last_revision
audit.action = DmsfFileRevisionAccess::DOWNLOAD_ACTION
audit.save!
end
# Notifications
begin
DmsfMailer.deliver_files_downloaded @project, zip.dmsf_files, request.remote_ip
rescue StandardError => e
Rails.logger.error "Could not send email notifications: #{e.message}"
end
send_file(
zip.finish,
filename: filename_for_content_disposition("#{@project.name}-#{DateTime.current.strftime('%y%m%d%H%M%S')}.zip"),
type: 'application/zip',
disposition: 'attachment'
)
ensure
zip&.close
end
def zip_entries(zip, selected_folders, selected_files)
member = Member.find_by(user_id: User.current.id, project_id: @project.id)
selected_folders.each do |selected_folder_id|
folder = DmsfFolder.visible.find_by(id: selected_folder_id)
raise DmsfFileNotFoundError unless folder
zip.add_dmsf_folder folder, member, folder&.dmsf_folder&.dmsf_path_str
end
selected_files.each do |selected_file_id|
file = DmsfFile.visible.find_by(id: selected_file_id)
raise DmsfFileNotFoundError unless file&.last_revision && File.exist?(file.last_revision&.disk_file)
unless (file.project == @project) || User.current.allowed_to?(:view_dmsf_files, file.project)
raise DmsfAccessError
end
zip.add_dmsf_file file, member, file.dmsf_folder&.dmsf_path_str
end
max_files = RedmineDmsf.dmsf_max_file_download
raise DmsfZipMaxFilesError if max_files.positive? && zip.dmsf_files.length > max_files
zip
end
def restore_entries(selected_folders, selected_files, selected_links)
# Folders
selected_folders.each do |id|
folder = DmsfFolder.find_by(id: id)
raise DmsfFileNotFoundError unless folder
end
# Files
selected_files.each do |id|
file = DmsfFile.find_by(id: id)
raise DmsfFileNotFoundError unless file
flash[:error] = file.errors.full_messages.to_sentence unless file.restore
end
# Links
selected_links.each do |id|
link = DmsfLink.find_by(id: id)
raise DmsfFileNotFoundError unless link
flash[:error] = link.errors.full_messages.to_sentence unless link.restore
end
end
def delete_entries(selected_folders, selected_files, selected_links, commit)
# Folders
selected_folders.each do |id|
raise DmsfAccessError unless User.current.allowed_to?(:folder_manipulation, @project)
folder = DmsfFolder.find_by(id: id)
if folder
raise StandardError, folder.errors.full_messages.to_sentence unless folder.delete(commit: commit)
elsif !commit
raise DmsfFileNotFoundError
end
end
# Files
deleted_files = []
not_deleted_files = []
if selected_files.any?
raise DmsfAccessError unless User.current.allowed_to?(:file_delete, @project)
selected_files.each do |id|
file = DmsfFile.find_by(id: id)
if file
if file.delete(commit: commit)
deleted_files << file unless commit
else
not_deleted_files << file
end
elsif !commit
raise DmsfFileNotFoundError
end
end
end
# Activities
unless deleted_files.empty?
begin
recipients = DmsfMailer.deliver_files_deleted(@project, deleted_files)
if RedmineDmsf.dmsf_display_notified_recipients? && recipients.any?
max_receivers = RedmineDmsf.dmsf_max_notification_receivers_info
to = recipients.collect { |user, _| user.name }.first(max_receivers).join(', ')
if to.present?
to << (recipients.count > max_receivers ? ',...' : '.')
flash[:warning] = l(:warning_email_notifications, to: to)
end
end
rescue StandardError => e
Rails.logger.error "Could not send email notifications: #{e.message}"
end
end
unless not_deleted_files.empty?
flash[:warning] = l(:warning_some_entries_were_not_deleted, entries: not_deleted_files.map(&:title).join(', '))
end
# Links
if selected_links.any?
raise DmsfAccessError unless User.current.allowed_to?(:folder_manipulation, @project)
selected_links.each do |id|
link = DmsfLink.find_by(id: id)
link&.delete commit: commit
end
end
flash[:notice] = l(:notice_entries_deleted) if flash[:error].blank? && flash[:warning].blank?
end
def copy_entries(selected_folders, selected_files, selected_links)
# Folders
selected_folders.each do |id|
folder = DmsfFolder.find_by(id: id)
new_folder = folder.copy_to(@target_project, @target_folder)
raise(StandardError, new_folder.errors.full_messages.to_sentence) unless new_folder.errors.empty?
end
# Files
selected_files.each do |id|
file = DmsfFile.find_by(id: id)
new_file = file.copy_to(@target_project, @target_folder)
raise(StandardError, new_file.errors.full_messages.to_sentence) unless new_file.errors.empty?
end
# Links
selected_links.each do |id|
link = DmsfLink.find_by(id: id)
new_link = link.copy_to(@target_project, @target_folder)
raise(StandardError, new_link.errors.full_messages.to_sentence) unless new_link.errors.empty?
end
flash[:notice] = l(:notice_entries_copied) if flash[:error].blank? && flash[:warning].blank?
end
def move_entries(selected_folders, selected_files, selected_links)
# Permissions
raise DmsfAccessError if selected_folders.any? && !User.current.allowed_to?(:folder_manipulation, @project)
if (selected_folders.any? || selected_links.any?) && !User.current.allowed_to?(:file_manipulation, @project)
raise DmsfAccessError
end
# Folders
selected_folders.each do |id|
folder = DmsfFolder.find_by(id: id)
unless folder.move_to(@target_project, @target_folder)
raise(StandardError, folder.errors.full_messages.to_sentence)
end
end
# Files
selected_files.each do |id|
file = DmsfFile.find_by(id: id)
raise(StandardError, file.errors.full_messages.to_sentence) unless file.move_to(@target_project, @target_folder)
end
# Links
selected_links.each do |id|
link = DmsfLink.find_by(id: id)
raise(StandardError, link.errors.full_messages.to_sentence) unless link.move_to(@target_project, @target_folder)
end
flash[:notice] = l(:notice_entries_moved) if flash[:error].blank? && flash[:warning].blank?
end
def find_folder
@folder = DmsfFolder.find(params[:folder_id]) if params[:folder_id].present?
rescue ActiveRecord::RecordNotFound
render_404
end
def find_folder_by_title
return unless api_request? && !@folder && params[:folder_title].present?
@folder = DmsfFolder.find_by(title: params[:folder_title], project_id: @project.id)
render_404 unless @folder
end
def find_parent
@parent = DmsfFolder.visible.find params[:parent_id] if params[:parent_id].present?
rescue ActiveRecord::RecordNotFound
render_404
end
def copy_folder(folder)
copy = folder.clone
copy.id = folder.id
copy
end
def query
retrieve_default_query true
@query = retrieve_query DmsfQuery, true
end
def retrieve_default_query(use_session)
return if params[:query_id].present?
return if api_request?
return if params[:set_filter]
if params[:without_default].present?
params[:set_filter] = 1
return
end
if !params[:set_filter] && use_session && session[:issue_query]
query_id, project_id = session[:issue_query].values_at(:id, :project_id)
return if DmsfQuery.exists?(id: query_id) && project_id == @project&.id
end
default_query = DmsfQuery.default(project: @project)
return unless default_query
params[:query_id] = default_query.id
end
def project_roles
@project_roles = Role.givable.joins(:member_roles).joins(:members).where(members: { project_id: @project.id })
.distinct
end
def find_target_folder
@target_project = if params[:dmsf_entries] && params[:dmsf_entries][:target_project_id].present?
Project.find params[:dmsf_entries][:target_project_id]
else
@project
end
if params[:dmsf_entries] && params[:dmsf_entries][:target_folder_id].present?
target_folder_id = params[:dmsf_entries][:target_folder_id]
@target_folder = DmsfFolder.find(target_folder_id)
raise ActiveRecord::RecordNotFound unless DmsfFolder.visible.exists?(id: target_folder_id)
@target_project = @target_folder&.project
end
rescue ActiveRecord::RecordNotFound
render_404
end
def check_target_folder
if params[:ids].present?
@selected_folders = params[:ids].grep(/folder-\d+/).map { |x| Regexp.last_match(1).to_i if x =~ /folder-(\d+)/ }
@selected_files = params[:ids].grep(/file-\d+/).map { |x| Regexp.last_match(1).to_i if x =~ /file-(\d+)/ }
@selected_dir_links = params[:ids].grep(/folder-link-\d+/)
.map { |x| Regexp.last_match(1).to_i if x =~ /folder-link-(\d+)/ }
@selected_file_links = params[:ids].grep(/file-link-\d+/)
.map { |x| Regexp.last_match(1).to_i if x =~ /file-link-(\d+)/ }
@selected_url_links = params[:ids].grep(/url-link-\d+/)
.map { |x| Regexp.last_match(1).to_i if x =~ /url-link-(\d+)/ }
@selected_links = @selected_dir_links + @selected_file_links + @selected_url_links
else
@selected_folders = []
@selected_files = []
@selected_links = []
end
return unless params[:copy_entries].present? || params[:move_entries].present?
begin
# Prevent copying/moving to the same destination
folders = DmsfFolder.where(id: @selected_folders).to_a
files = DmsfFile.where(id: @selected_files).to_a
links = DmsfLink.where(id: @selected_links).to_a
(folders + files + links).each do |entry|
if entry.dmsf_folder
raise DmsfParentError if entry.dmsf_folder == @target_folder || entry == @target_folder
elsif @target_folder.nil?
raise DmsfParentError if entry.project == @target_project
end
end
# Prevent recursion
if params[:move_entries].present?
folders.each do |entry|
raise DmsfParentError if entry.any_child?(@target_folder)
end
end
# Check permissions
if (@target_folder && (@target_folder.locked_for_user? ||
!DmsfFolder.permissions?(@target_folder, allow_system: false))) ||
!@target_project.allows_to?(:folder_manipulation)
raise DmsfAccessError
end
rescue DmsfParentError
flash[:error] = l(:error_target_folder_same)
redirect_back_or_default dmsf_folder_path(id: @project, folder_id: @folder)
rescue DmsfAccessError
render_403
end
end
end

View File

@ -0,0 +1,378 @@
# frozen_string_literal: true
# Redmine plugin for Document Management System "Features"
#
# Vít Jonáš <vit.jonas@gmail.com>, Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
# Files controller
class DmsfFilesController < ApplicationController
menu_item :dmsf
before_action :find_file, except: %i[delete_revision obsolete_revision]
before_action :find_revision, only: %i[delete_revision obsolete_revision]
before_action :find_folder, only: %i[delete create_revision]
before_action :authorize
before_action :permissions?
accept_api_auth :show, :view, :delete, :create_revision
helper :custom_fields
helper :dmsf_workflows
helper :dmsf
helper :queries
helper :watchers
helper :context_menus
include QueriesHelper
def permissions?
render_403 if @file && !DmsfFolder.permissions?(@file.dmsf_folder, allow_system: true, file: true)
true
end
def view
if params[:download].blank?
@revision = @file.last_revision
else
@revision = DmsfFileRevision.find(params[:download].to_i)
raise DmsfAccessError if @revision.dmsf_file != @file
end
check_project @revision.dmsf_file
raise ActionController::MissingFile if @file.deleted?
# Action
access = DmsfFileRevisionAccess.new
access.user = User.current
access.dmsf_file_revision = @revision
access.action = DmsfFileRevisionAccess::DOWNLOAD_ACTION
access.save!
# Notifications
begin
DmsfMailer.deliver_files_downloaded @project, [@file], request.remote_ip
rescue StandardError => e
Rails.logger.error "Could not send email notifications: #{e.message}"
end
# Allow a preview of the file by an external plugin
results = call_hook(:dmsf_files_controller_before_view, { file: @revision.disk_file })
return if results.first == true
member = Member.find_by(user_id: User.current.id, project_id: @file.project.id)
# IE has got a tendency to cache files
expires_in 0.years, 'must-revalidate' => true
# PDF preview
pdf_preview = (params[:disposition] != 'attachment') && params[:filename].blank? && @file.pdf_preview
filename = filename_for_content_disposition(@revision.formatted_name(member))
if !api_request? && pdf_preview.present? && (RedmineDmsf.office_bin.present? || params[:preview].present?)
basename = File.basename(filename, '.*')
send_file pdf_preview, filename: "#{basename}.pdf", type: 'application/pdf', disposition: 'inline'
# Text preview
elsif !api_request? && params[:download].blank? && (@file.size <= Setting.file_max_size_displayed.to_i.kilobyte) &&
(@file.text? || @file.markdown? || @file.textile?) && !@file.html? && formats.include?(:html)
@content = File.read(@revision.disk_file, mode: 'rb')
render action: 'document'
# Offer the file for download
else
params[:disposition] = 'attachment' if params[:filename].present?
send_file @revision.disk_file,
filename: filename,
type: @revision.detect_content_type,
disposition: params[:disposition].presence || @revision.dmsf_file.disposition
end
rescue DmsfAccessError => e
Rails.logger.error e.message
render_403
rescue StandardError => e
Rails.logger.error e.message
render_404
end
def show
@revision = @file.last_revision
@file_delete_allowed = User.current.allowed_to?(:file_delete, @project)
@file_manipulation_allowed = User.current.allowed_to?(:file_manipulation, @project)
@revision_count = @file.dmsf_file_revisions.visible.all.size
@revision_pages = Paginator.new @revision_count, params['per_page'] ? params['per_page'].to_i : 25, params['page']
@notifications = Setting.notified_events.include?('dmsf_legacy_notifications')
respond_to do |format|
format.html { render layout: !request.xhr? }
format.api
end
end
def create_revision
if params[:dmsf_file_revision] && !@file.locked_for_user?
revision = DmsfFileRevision.new
revision.title = params[:dmsf_file_revision][:title].scrub.strip
revision.name = params[:dmsf_file_revision][:name].scrub.strip
revision.description = params[:dmsf_file_revision][:description]&.scrub&.strip
revision.comment = params[:dmsf_file_revision][:comment]&.scrub&.strip
revision.dmsf_file = @file
last_revision = @file.last_revision
revision.source_revision = last_revision
revision.user = User.current
# Version
revision.major_version = DmsfUploadHelper.db_version(params[:version_major])
revision.minor_version = DmsfUploadHelper.db_version(params[:version_minor])
revision.patch_version = DmsfUploadHelper.db_version(params[:version_patch])
if params[:dmsf_attachments].present?
keys = params[:dmsf_attachments].keys
file_upload = params[:dmsf_attachments][keys.first] if keys&.first
end
if file_upload
upload = DmsfUpload.create_from_uploaded_attachment(@project, @folder, file_upload)
if upload
revision.size = upload.size
revision.disk_filename = revision.new_storage_filename
revision.mime_type = upload.mime_type
revision.digest = upload.digest
end
else
revision.size = last_revision.size
revision.disk_filename = last_revision.disk_filename
revision.mime_type = last_revision.mime_type
revision.digest = last_revision.digest
end
# Custom fields
revision.copy_custom_field_values(params[:dmsf_file_revision][:custom_field_values], last_revision)
@file.name = revision.name
ok = true
if revision.save
revision.assign_workflow params[:dmsf_workflow_id]
if upload
begin
FileUtils.mv upload.tempfile_path, revision.disk_file(search_if_not_exists: false)
rescue StandardError => e
Rails.logger.error e.message
flash[:error] = e.message
revision.destroy
ok = false
end
end
if ok && @file.locked? && !@file.locks.empty?
begin
@file.unlock!
flash[:notice] = "#{l(:notice_file_unlocked)}, "
rescue StandardError => e
Rails.logger.error "Cannot unlock the file: #{e.message}"
ok = false
end
end
if ok && @file.save
@file.last_revision = revision
call_hook :dmsf_helper_upload_after_commit, { file: @file }
begin
recipients = DmsfMailer.deliver_files_updated(@project, [@file])
if RedmineDmsf.dmsf_display_notified_recipients? && recipients.any?
max_notifications = RedmineDmsf.dmsf_max_notification_receivers_info
to = recipients.collect { |user, _| user.name }.first(max_notifications).join(', ')
if to.present?
to << (recipients.count > max_notifications ? ',...' : '.')
end
end
rescue StandardError => e
Rails.logger.error "Could not send email notifications: #{e.message}"
end
else
ok = false
end
else
ok = false
end
end
respond_to do |format|
format.html do
flash[:error] = l(:error_file_is_locked) if @file.locked_for_user?
flash[:warning] = l(:warning_email_notifications, to: to) if to.present?
flash[:error] = @file.errors.full_messages.to_sentence if @file.errors.any?
flash[:error] = revision.errors.full_messages.to_sentence if revision.errors.any?
flash[:notice] = (flash[:notice].nil? ? '' : flash[:notice]) + l(:notice_file_revision_created) if ok
redirect_back_or_default dmsf_folder_path(id: @project, folder_id: @folder)
end
format.api
end
end
def delete
if @file
commit = params[:commit] == 'yes'
result = @file.delete(commit: commit)
if result
flash[:notice] = l(:notice_file_deleted)
if commit
container = @file.container
container&.dmsf_file_removed @file
else
begin
recipients = DmsfMailer.deliver_files_deleted(@project, [@file])
if RedmineDmsf.dmsf_display_notified_recipients? && recipients.any?
max_notification = RedmineDmsf.dmsf_max_notification_receivers_info
to = recipients.collect { |user, _| user.name }.first(max_notification).join(', ')
if to.present?
to << (recipients.count > max_notification ? ',...' : '.')
flash[:warning] = l(:warning_email_notifications, to: to)
end
end
rescue StandardError => e
Rails.logger.error "Could not send email notifications: #{e.message}"
end
end
else
msg = @file.errors.full_messages.to_sentence
flash[:error] = msg
Rails.logger.error msg
end
end
respond_to do |format|
format.html { redirect_back_or_default dmsf_folder_path(id: @project, folder_id: @folder) }
format.api { result ? render_api_ok : render_validation_errors(@file) }
end
end
def delete_revision
if @revision
if @revision.delete commit: true
if @file.name != @file.last_revision.name
@file.name = @file.last_revision.name
@file.save!
end
flash[:notice] = l(:notice_revision_deleted)
else
flash[:error] = @revision.errors.full_messages.to_sentence
end
end
redirect_to action: 'show', id: @file
end
def obsolete_revision
if @revision
if @revision.obsolete
flash[:notice] = l(:notice_revision_obsoleted)
else
flash[:error] = @revision.errors.full_messages.to_sentence
end
end
redirect_to action: 'show', id: @file
end
def lock
if @file.locked?
flash[:warning] = l(:warning_file_already_locked)
else
begin
@file.lock!
flash[:notice] = l(:notice_file_locked)
rescue StandardError => e
flash[:error] = e.message
end
end
redirect_back_or_default dmsf_folder_path(id: @project, folder_id: @folder)
end
def unlock
if @file.locked?
if @file.locks[0].user == User.current || User.current.allowed_to?(:force_file_unlock, @file.project)
begin
@file.unlock!
flash[:notice] = l(:notice_file_unlocked)
rescue StandardError => e
flash[:error] = e.message
end
else
flash[:error] = l(:error_only_user_that_locked_file_can_unlock_it)
end
else
flash[:warning] = l(:warning_file_not_locked)
end
redirect_back_or_default dmsf_folder_path(id: @project, folder_id: @folder)
end
def notify_activate
if @file.notification
flash[:warning] = l(:warning_file_notifications_already_activated)
else
@file.notify_activate
flash[:notice] = l(:notice_file_notifications_activated)
end
redirect_back_or_default dmsf_folder_path(id: @project, folder_id: @folder)
end
def notify_deactivate
if @file.notification
@file.notify_deactivate
flash[:notice] = l(:notice_file_notifications_deactivated)
else
flash[:warning] = l(:warning_file_notifications_already_deactivated)
end
redirect_back_or_default dmsf_folder_path(id: @project, folder_id: @folder)
end
def restore
if @file.restore
flash[:notice] = l(:notice_dmsf_file_restored)
else
flash[:error] = @file.errors.full_messages.to_sentence
end
redirect_to trash_dmsf_path(@project)
end
def thumbnail
tbnail = @file.thumbnail(size: params[:size])
if tbnail
if stale?(etag: tbnail)
send_file tbnail,
filename: filename_for_content_disposition(@file.last_revision.disk_file),
type: @file.last_revision.detect_content_type,
disposition: 'inline'
end
else
head :not_found
end
end
private
def find_file
@file = DmsfFile.find params[:id]
@project = @file.project
rescue ActiveRecord::RecordNotFound
render_404
end
def find_revision
@revision = DmsfFileRevision.visible.find params[:id]
@file = @revision.dmsf_file
@project = @file.project
rescue ActiveRecord::RecordNotFound
render_404
end
def find_folder
@folder = DmsfFolder.find params[:folder_id] if params[:folder_id].present?
rescue ActiveRecord::RecordNotFound
render_404
end
def check_project(entry)
return unless entry && entry.project != @project
raise DmsfAccessError, l(:error_entry_project_does_not_match_current_project)
end
end

View File

@ -0,0 +1,81 @@
# frozen_string_literal: true
# Redmine plugin for Document Management System "Features"
#
# Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
# Folder permissions controller
class DmsfFolderPermissionsController < ApplicationController
before_action :find_folder,
only: %i[destroy new autocomplete_for_user],
if: -> { params[:dmsf_folder_id].present? }
before_action :find_project
before_action :authorize
before_action :permissions?
helper :dmsf
def permissions?
render_403 unless DmsfFolder.permissions?(@dmsf_folder)
true
end
def new
@principals = users_for_new_users
end
def append
@principals = Principal.where(id: params[:user_ids]).to_a
head :ok if @principals.blank?
end
def autocomplete_for_user
@principals = users_for_new_users
respond_to do |format|
format.js
end
end
private
def users_for_new_users
scope = Principal.active.visible.member_of(@project).like(params[:q]).order(:type, :lastname)
if @dmsf_folder
users = @dmsf_folder.permissions_users
if users.any?
ids = users.collect(&:id)
scope = scope.where.not(id: ids)
end
end
scope.to_a
end
def find_project
@project = Project.visible.find_by_param(params[:project_id])
rescue DmsfAccessError
render_403
rescue ActiveRecord::RecordNotFound
render_404
end
def find_folder
@dmsf_folder = DmsfFolder.visible.find(params[:dmsf_folder_id])
rescue DmsfAccessError
render_403
rescue ActiveRecord::RecordNotFound
render_404
end
end

View File

@ -0,0 +1,35 @@
# frozen_string_literal: true
# Redmine plugin for Document Management System "Features"
#
# Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
# Help controller
class DmsfHelpController < ApplicationController
def show_wiki_syntax
lang = current_language.to_s
template = "dmsf_help/#{lang}/wiki_syntax"
lang = 'en' unless lookup_context.exists?(template)
render template: "dmsf_help/#{lang}/wiki_syntax", layout: nil
end
def show_dmsf_help
lang = current_language.to_s
template = "dmsf_help/#{lang}/dmsf_help"
lang = 'en' unless lookup_context.exists?(template)
render template: "dmsf_help/#{lang}/dmsf_help", layout: nil
end
end

View File

@ -0,0 +1,222 @@
# frozen_string_literal: true
# Redmine plugin for Document Management System "Features"
#
# Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
# Links controller
class DmsfLinksController < ApplicationController
model_object DmsfLink
before_action :find_model_object, only: %i[destroy restore]
before_action :find_link_project
before_action :find_folder, only: [:destroy]
before_action :authorize
before_action :permissions?
protect_from_forgery except: :new
accept_api_auth :create
helper :dmsf
def permissions?
render_403 if @dmsf_link && !DmsfFolder.permissions?(@dmsf_link.dmsf_folder)
true
end
def initialize
@dmsf_link = nil
super
end
def new
@dmsf_link = DmsfLink.new
member = Member.find_by(project_id: params[:project_id], user_id: User.current.id)
@fast_links = member&.dmsf_fast_links
@dmsf_link.project_id = params[:project_id]
@dmsf_link.dmsf_folder_id = params[:dmsf_folder_id]
@dmsf_file_id = params[:dmsf_file_id]
@type = params[:type]
@dmsf_link.target_project_id = params[:project_id]
@back_url = params[:back_url]
if @type == 'link_to'
if @dmsf_file_id
f = DmsfFile.find_by(id: @dmsf_file_id)
@dmsf_link.name = f&.last_revision&.title
else
titles = DmsfFolder.where(id: @dmsf_link.dmsf_folder_id).pluck(:title)
@dmsf_link.name = titles.first if titles.any?
end
end
@container = params[:container]
respond_to do |format|
format.html
format.js
end
end
def autocomplete_for_project
respond_to do |format|
format.js
end
end
def autocomplete_for_folder
respond_to do |format|
format.js
end
end
def create
@dmsf_link = DmsfLink.new
@dmsf_link.user = User.current
if params[:dmsf_link][:type] == 'link_from'
# Link from
if params[:dmsf_link][:container].blank?
@dmsf_link.project_id = params[:dmsf_link][:project_id]
@dmsf_link.dmsf_folder_id = params[:dmsf_link][:dmsf_folder_id]
else
# A container link
@dmsf_link.project_id = -1
@dmsf_link.dmsf_folder_id = nil
end
@dmsf_link.target_project_id = params[:dmsf_link][:target_project_id]
if params[:external_link] == 'true'
@dmsf_link.external_url = params[:dmsf_link][:external_url]
@dmsf_link.target_type = 'DmsfUrl'
elsif params[:dmsf_link][:target_file_id].present?
@dmsf_link.target_id = params[:dmsf_link][:target_file_id]
@dmsf_link.target_type = DmsfFile.model_name.to_s
else
@dmsf_link.target_id = if DmsfLinksHelper.number?(params[:dmsf_link][:target_folder_id])
params[:dmsf_link][:target_folder_id].to_i
end
@dmsf_link.target_type = DmsfFolder.model_name.to_s
end
@dmsf_link.name = params[:dmsf_link][:name].scrub.strip
result = @dmsf_link.save
if result
flash[:notice] = l(:notice_successful_create)
else
msg = @dmsf_link.errors.full_messages.to_sentence
flash[:error] = msg
Rails.logger.error msg
end
else
# Link to
if DmsfLinksHelper.number?(params[:dmsf_link][:target_folder_id])
@dmsf_link.dmsf_folder_id = params[:dmsf_link][:target_folder_id].to_i
end
if params[:dmsf_link][:target_project_id].present?
@dmsf_link.project_id = params[:dmsf_link][:target_project_id]
else
project_ids = DmsfFolder.where(id: params[:dmsf_link][:target_folder_id]).pluck(:project_id)
unless project_ids.any?
render_404
return
end
@dmsf_link.project_id = project_ids.first
end
@dmsf_link.target_project_id = params[:dmsf_link][:project_id]
if params[:dmsf_link][:dmsf_file_id].present?
@dmsf_link.target_id = params[:dmsf_link][:dmsf_file_id]
@dmsf_link.target_type = DmsfFile.model_name.to_s
else
@dmsf_link.target_id = params[:dmsf_link][:dmsf_folder_id]
@dmsf_link.target_type = DmsfFolder.model_name.to_s
end
@dmsf_link.name = params[:dmsf_link][:name]
result = @dmsf_link.save
if result
flash[:notice] = l(:notice_successful_create)
else
flash[:error] = @dmsf_link.errors.full_messages.to_sentence
end
end
respond_to do |format|
format.html do
if params[:dmsf_link][:type] == 'link_from'
redirect_back_or_default dmsf_folder_path(id: @project, folder_id: @dmsf_link.dmsf_folder_id)
elsif params[:dmsf_link][:dmsf_file_id].present?
redirect_back_or_default dmsf_file_path(@dmsf_link.target_file)
else
folder = @dmsf_link.target_folder.dmsf_folder if @dmsf_link.target_folder
redirect_back_or_default dmsf_folder_path(id: @project, folder_id: folder)
end
end
format.api { render_validation_errors(@dmsf_link) unless result }
format.js
end
end
def destroy
begin
if @dmsf_link
commit = params[:commit] == 'yes'
if @dmsf_link.delete(commit: commit)
flash[:notice] = l(:notice_successful_delete)
else
flash[:error] = @dmsf_link.errors.full_messages.to_sentence
end
end
rescue StandardError => e
errors.add :base, e.message
return false
end
redirect_back_or_default dmsf_folder_path(id: @project, folder_id: @folder)
end
def restore
if @dmsf_link.restore
flash[:notice] = l(:notice_dmsf_link_restored)
else
flash[:error] = @dmsf_link.errors.full_messages.to_sentence
end
redirect_to trash_dmsf_path(@project)
end
private
def l_params
params.fetch(:dmsf_link, {}).permit(:project_id)
end
def find_link_project
if @dmsf_link
@project = @dmsf_link.project
else
pid = if params[:dmsf_link].present?
if params[:dmsf_link].fetch(:project_id, nil).blank?
params[:dmsf_link].fetch(:target_project_id, nil)
else
params[:dmsf_link][:project_id]
end
else
params[:project_id]
end
@project = Project.find(pid)
end
rescue ActiveRecord::RecordNotFound
render_404
end
def find_folder
@folder = DmsfFolder.find params[:folder_id] if params[:folder_id].present?
rescue ActiveRecord::RecordNotFound
render_404
end
end

View File

@ -0,0 +1,47 @@
# frozen_string_literal: true
# Redmine plugin for Document Management System "Features"
#
# Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
# Public URL controller
class DmsfPublicUrlsController < ApplicationController
model_object DmsfPublicUrl
before_action :authorize, only: [:create]
skip_before_action :check_if_login_required, only: [:show]
def show
dmsf_public_url = DmsfPublicUrl.where('token = ? AND expire_at >= ?', params[:token], DateTime.current).first
if dmsf_public_url
revision = dmsf_public_url.dmsf_file.last_revision
begin
# IE has got a tendency to cache files
expires_in(0.years, 'must-revalidate' => true)
send_file(revision.disk_file,
filename: filename_for_content_disposition(revision.name),
type: revision.detect_content_type,
disposition: dmsf_public_url.dmsf_file.disposition)
rescue StandardError => e
Rails.logger.error e.message
render_404
end
else
render_404
end
end
def create; end
end

View File

@ -0,0 +1,53 @@
# frozen_string_literal: true
# Redmine plugin for Document Management System "Features"
#
# Vít Jonáš <vit.jonas@gmail.com>, Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
# State controller
class DmsfStateController < ApplicationController
menu_item :dmsf
before_action :find_project
before_action :authorize
def user_pref_save
member = @project.members.find_by(user_id: User.current.id)
if member
if Setting.notified_events.include?('dmsf_legacy_notifications')
member.dmsf_mail_notification = params[:email_notify]
end
member.dmsf_title_format = params[:title_format]
member.dmsf_fast_links = params[:fast_links].present?
if format_valid?(member.dmsf_title_format) && member.save
flash[:notice] = l(:notice_your_preferences_were_saved)
else
flash[:error] = l(:notice_your_preferences_were_not_saved)
end
else
flash[:warning] = l(:user_is_not_project_member)
end
@project.update(dmsf_act_as_attachable: params[:act_as_attachable]) if RedmineDmsf.dmsf_act_as_attachable?
@project.update default_dmsf_query_id: params[:default_dmsf_query]
redirect_to settings_project_path(@project, tab: 'dmsf')
end
private
def format_valid?(format)
format.blank? || (/%(t|d|v|i|r)/.match?(format) && format.length < 256)
end
end

View File

@ -0,0 +1,154 @@
# frozen_string_literal: true
# Redmine plugin for Document Management System "Features"
#
# Vít Jonáš <vit.jonas@gmail.com>, Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
# Upload controller
class DmsfUploadController < ApplicationController
menu_item :dmsf
before_action :find_project, except: %i[upload delete_dmsf_attachment delete_dmsf_link_attachment]
before_action :authorize, except: %i[upload delete_dmsf_attachment delete_dmsf_link_attachment]
before_action :authorize_global, only: %i[upload delete_dmsf_attachment delete_dmsf_link_attachment]
before_action :find_folder, except: %i[upload commit delete_dmsf_attachment delete_dmsf_link_attachment]
before_action :permissions?, except: %i[upload commit delete_dmsf_attachment delete_dmsf_link_attachment]
helper :custom_fields
helper :dmsf_workflows
helper :dmsf
accept_api_auth :upload, :commit
def permissions?
render_403 unless DmsfFolder.permissions?(@folder)
true
end
def upload_files
uploaded_files = params[:dmsf_attachments]
@uploads = []
# Commit
if params[:commit] == l(:label_dmsf_upload_commit)
uploaded_files&.each do |key, uploaded_file|
upload = DmsfUpload.create_from_uploaded_attachment(@project, @folder, uploaded_file)
next unless upload
@uploads.push upload
params[:committed_files][key][:disk_filename] = upload.disk_filename
params[:committed_files][key][:digest] = upload.digest
params[:committed_files][key][:tempfile_path] = upload.tempfile_path
end
commit_files if params[:committed_files].present?
# Upload
else
# standard file input uploads
uploaded_files&.each_value do |uploaded_file|
upload = DmsfUpload.create_from_uploaded_attachment(@project, @folder, uploaded_file)
@uploads.push(upload) if upload
end
end
flash.now[:error] = "#{l(:label_attachment)} #{l('activerecord.errors.messages.invalid')}" if @uploads.empty?
end
# REST API and Redmine attachment form
def upload
unless request.media_type == 'application/octet-stream'
head :not_acceptable
return
end
@attachment = Attachment.new(file: request.body)
@attachment.author = User.current
@attachment.filename = params[:filename].presence || Redmine::Utils.random_hex(16)
@attachment.content_type = params[:content_type].presence
begin
Attachment.skip_callback(:commit, :after, :reuse_existing_file_if_possible, raise: false)
saved = @attachment.save
ensure
Attachment.set_callback(:commit, :after, :reuse_existing_file_if_possible)
end
respond_to do |format|
format.js
format.api do
if saved
render action: 'upload', status: :created
else
render_validation_errors @attachment
end
end
end
end
def commit_files
commit_files_internal params[:committed_files]
end
# REST API file commit
def commit
@files = []
attachments = params[:attachments]
return unless attachments
@folder = DmsfFolder.visible.find_by(id: attachments[:folder_id]) if attachments[:folder_id].present?
# standard file input uploads
uploaded_files = attachments.slice('uploaded_file')
uploaded_files.each_value do |uploaded_file|
upload = DmsfUpload.create_from_uploaded_attachment(@project, @folder, uploaded_file)
next unless upload
uploaded_file[:disk_filename] = upload.disk_filename
uploaded_file[:tempfile_path] = upload.tempfile_path
uploaded_file[:size] = upload.size
uploaded_file[:digest] = upload.digest
uploaded_file[:mime_type] = upload.mime_type
end
commit_files_internal uploaded_files
end
def delete_dmsf_attachment
attachment = Attachment.find(params[:id])
attachment.destroy
rescue ActiveRecord::RecordNotFound
render_404
end
def delete_dmsf_link_attachment
link = DmsfLink.find(params[:id])
link.destroy
rescue ActiveRecord::RecordNotFound
render_404
end
private
def commit_files_internal(committed_files)
@files, failed_uploads = DmsfUploadHelper.commit_files_internal(committed_files, @project, @folder, self)
call_hook :dmsf_upload_controller_after_commit, { files: @files }
respond_to do |format|
format.js
format.api { render_validation_errors(failed_uploads) unless failed_uploads.empty? }
format.html { redirect_to dmsf_folder_path(id: @project, folder_id: @folder) }
end
end
def find_folder
@folder = DmsfFolder.visible.find(params[:folder_id]) if params.key?('folder_id')
rescue DmsfAccessError
render_403
end
end

View File

@ -0,0 +1,544 @@
# frozen_string_literal: true
# Redmine plugin for Document Management System "Features"
#
# Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
# WorkflowsController
class DmsfWorkflowsController < ApplicationController
model_object DmsfWorkflow
menu_item :dmsf_approvalworkflows
self.main_menu = false
before_action :find_model_object, except: %i[create new index assign assignment]
before_action :find_project
before_action :authorize_custom
before_action :permissions?, only: %i[new_action assignment start]
before_action :approver_candidates, only: %i[remove_step show reorder_steps add_step]
before_action :prevent_from_editing, only: %i[destroy remove_step update add_step update_step reorder_steps]
layout :workflows_layout
helper :dmsf
def permissions?
revision = DmsfFileRevision.find_by(id: params[:dmsf_file_revision_id]) if params[:dmsf_file_revision_id].present?
render_403 unless revision&.dmsf_file || DmsfFolder.permissions?(revision&.dmsf_file&.dmsf_folder)
true
end
def initialize
@dmsf_workflow = nil
@project = nil
super
end
def index
@status = params[:status] || 1
@workflow_pages, @workflows = paginate(DmsfWorkflow.status(@status).global.sorted, per_page: 25)
@path = dmsf_workflows_path
end
def action
# Noting to do
end
def show
# Noting to do
end
def new_action
unless params[:commit] == l(:button_submit) && request.post?
redirect_back_or_default dmsf_folder_path(id: @project, folder_id: @folder)
return
end
action = DmsfWorkflowStepAction.new(
dmsf_workflow_step_assignment_id: params[:dmsf_workflow_step_assignment_id],
action: params[:step_action].to_i >= 10 ? DmsfWorkflowStepAction::ACTION_DELEGATE : params[:step_action],
note: params[:note]
)
revision = DmsfFileRevision.find_by(id: params[:dmsf_file_revision_id])
result = call_hook(:dmsf_workflow_controller_before_approval,
{ dmsf_file_revision: revision, step_action: params[:step_action] })
if (result.blank? || result.first) && action.save
if revision
if @dmsf_workflow.try_finish? revision, action, (params[:step_action].to_i / 10)
if revision.dmsf_file
begin
revision.dmsf_file.unlock!(force_file_unlock_allowed: true) unless RedmineDmsf.dmsf_keep_documents_locked?
rescue DmsfLockError => e
flash[:info] = e.message
end
end
if revision.workflow == DmsfWorkflow::STATE_APPROVED
# Just approved
if Setting.notified_events.include?('dmsf_workflow_plural')
recipients = DmsfMailer.get_notify_users(@project, revision.dmsf_file, force_notification: true)
DmsfMailer.deliver_workflow_notification(
recipients,
@dmsf_workflow,
revision,
:text_email_subject_approved,
:text_email_finished_approved,
:text_email_to_see_history
)
if RedmineDmsf.dmsf_display_notified_recipients? && recipients.present?
max_notifications = RedmineDmsf.dmsf_max_notification_receivers_info
to = recipients.collect(&:name).first(max_notifications).join(', ')
if to.present?
to << (recipients.count > max_notifications ? ',...' : '.')
flash[:warning] = l(:warning_email_notifications, to: to)
end
end
end
elsif Setting.notified_events.include?('dmsf_workflow_plural') # Just rejected
recipients = @dmsf_workflow.participiants
recipients.push revision.dmsf_workflow_assigned_by_user
recipients.uniq!
recipients &= DmsfMailer.get_notify_users(@project, revision.dmsf_file, force_notification: true)
DmsfMailer.deliver_workflow_notification(
recipients,
@dmsf_workflow,
revision,
:text_email_subject_rejected,
:text_email_finished_rejected,
:text_email_to_see_history,
action.note
)
if RedmineDmsf.dmsf_display_notified_recipients? && recipients.present?
max_notifications = RedmineDmsf.dmsf_max_notification_receivers_info
to = recipients.collect(&:name).first(max_notifications).join(', ')
if to.present?
to << (recipients.count > max_notifications ? ',...' : '.')
flash[:warning] = l(:warning_email_notifications, to: to)
end
end
end
elsif action.action == DmsfWorkflowStepAction::ACTION_DELEGATE
# Delegation
if Setting.notified_events.include?('dmsf_workflow_plural')
delegate = User.active.find_by(id: params[:step_action].to_i / 10)
if DmsfMailer.get_notify_users(@project, revision.dmsf_file, force_notification: true).include?(delegate)
DmsfMailer.deliver_workflow_notification(
[delegate],
@dmsf_workflow,
revision,
:text_email_subject_delegated,
:text_email_finished_delegated,
:text_email_to_proceed,
action.note,
action.dmsf_workflow_step_assignment.dmsf_workflow_step
)
if RedmineDmsf.dmsf_display_notified_recipients?
flash[:warning] = l(:warning_email_notifications, to: delegate.name)
end
end
end
else
# Next step
assignments = @dmsf_workflow.next_assignments revision.id
if assignments.any? &&
Setting.notified_events.include?('dmsf_workflow_plural') &&
assignments.first.dmsf_workflow_step.step != action.dmsf_workflow_step_assignment.dmsf_workflow_step.step
# Next step
assignments.each do |assignment|
next if assignment.user.nil? ||
DmsfMailer.get_notify_users(@project, revision.dmsf_file, force_notification: true)
.exclude?(assignment.user)
DmsfMailer.deliver_workflow_notification(
[assignment.user],
@dmsf_workflow,
revision,
:text_email_subject_requires_approval,
:text_email_finished_step,
:text_email_to_proceed,
nil,
assignment.dmsf_workflow_step
)
end
to = revision.dmsf_workflow_assigned_by_user
if to && DmsfMailer.get_notify_users(@project, revision.dmsf_file, force_notification: true)
.include?(to)
DmsfMailer.deliver_workflow_notification(
[to],
@dmsf_workflow,
revision,
:text_email_subject_updated,
:text_email_finished_step_short,
:text_email_to_see_status
)
end
if RedmineDmsf.dmsf_display_notified_recipients?
recipients = assignments.collect(&:user)
recipients << to if to
recipients.uniq!
recipients &= DmsfMailer.get_notify_users(@project, revision.dmsf_file, force_notification: true)
unless recipients.empty?
max_notifications = RedmineDmsf.dmsf_max_notification_receivers_info
to = recipients.collect(&:name).first(max_notifications).join(', ')
if to.present?
to << (recipients.count > max_notifications ? ',...' : '.')
flash[:warning] = l(:warning_email_notifications, to: to)
end
end
end
end
end
end
flash[:notice] = l(:notice_successful_update)
elsif action.action != DmsfWorkflowStepAction::ACTION_APPROVE && action.note.blank?
flash[:error] = l(:error_empty_note)
end
redirect_back_or_default dmsf_folder_path(id: @project, folder_id: @folder)
end
def assign; end
def assignment
if params[:commit] == l(:button_submit) && params[:dmsf_workflow_id].present? && params[:dmsf_workflow_id] != '-1'
# DMS file
if params[:dmsf_file_revision_id].present? && params[:dmsf_link_id].blank? && params[:attachment_id].blank?
revision = DmsfFileRevision.find_by(id: params[:dmsf_file_revision_id])
begin
if revision
@project ||= revision.dmsf_file.project
revision.set_workflow(params[:dmsf_workflow_id], params[:action])
revision.assign_workflow(params[:dmsf_workflow_id])
if request.post?
if revision.save
file = DmsfFile.find_by(id: revision.dmsf_file_id)
if file
begin
file.lock!
rescue DmsfLockError => e
Rails.logger.warn e.message
end
flash[:notice] = l(:notice_successful_update)
end
else
flash[:error] = l(:error_workflow_assign)
end
end
end
rescue StandardError => e
flash[:error] = e.message
end
# DMS link (attached)
elsif params[:dmsf_link_id].present?
@dmsf_link_id = params[:dmsf_link_id]
@dmsf_workflow_id = params[:dmsf_workflow_id]
# Attachment (attached)
elsif params[:attachment_id].present?
@attachment_id = params[:attachment_id]
@dmsf_workflow_id = params[:dmsf_workflow_id]
end
end
respond_to do |format|
format.html { redirect_back_or_default dmsf_folder_path(id: @project, folder_id: @folder) }
format.js
end
end
def log
if params[:dmsf_file_revision_id].present?
@revision = DmsfFileRevision.find_by(id: params[:dmsf_file_revision_id])
elsif params[:dmsf_file_id].present?
dmsf_file = DmsfFile.find_by(id: params[:dmsf_file_id])
@revision = dmsf_file.last_revision if dmsf_file
elsif params[:dmsf_link_id].present?
dmsf_link = DmsfLink.find_by(id: params[:dmsf_link_id])
if dmsf_link
dmsf_file = dmsf_link.target_file
@revision = dmsf_file.last_revision
end
end
respond_to do |format|
format.html
format.js
end
end
def new
@dmsf_workflow = DmsfWorkflow.new
# Reload
if params[:dmsf_workflow] && params[:dmsf_workflow][:name].present?
@dmsf_workflow.name = params[:dmsf_workflow][:name]
elsif params[:dmsf_workflow] && params[:dmsf_workflow][:id].present?
names = DmsfWorkflow.where(id: params[:dmsf_workflow][:id]).pluck(:name)
@dmsf_workflow.name = names.first
end
render layout: !request.xhr?
end
def edit
redirect_to dmsf_workflow_path(@dmsf_workflow)
end
def create
if params[:dmsf_workflow]
if params[:dmsf_workflow][:id].to_i.positive?
wf = DmsfWorkflow.find_by(id: params[:dmsf_workflow][:id])
@dmsf_workflow = wf.copy_to(@project, params[:dmsf_workflow][:name]) if wf
else
@dmsf_workflow = DmsfWorkflow.new
@dmsf_workflow.name = params[:dmsf_workflow][:name]
@dmsf_workflow.project_id = @project.id if @project
@dmsf_workflow.author = User.current
@dmsf_workflow.save
end
end
if request.post? && @dmsf_workflow&.valid?
flash[:notice] = l(:notice_successful_create)
if @project
redirect_to settings_project_path(@project, tab: 'dmsf_workflow')
else
redirect_to dmsf_workflows_path
end
else
render action: 'new'
end
end
def update
if params[:dmsf_workflow]
res = @dmsf_workflow.update(name: params[:dmsf_workflow][:name]) if params[:dmsf_workflow][:name].present?
res = @dmsf_workflow.update(status: params[:dmsf_workflow][:status]) if params[:dmsf_workflow][:status].present?
if res
flash[:notice] = l(:notice_successful_update)
if @project
redirect_to settings_project_path(@project, tab: 'dmsf_workflow')
else
redirect_to dmsf_workflows_path
end
else
flash[:error] = @dmsf_workflow.errors.full_messages.to_sentence
redirect_to dmsf_workflow_path(@dmsf_workflow)
end
else
redirect_to dmsf_workflow_path(@dmsf_workflow)
end
end
def destroy
begin
@dmsf_workflow.destroy
flash[:notice] = l(:notice_successful_delete)
rescue StandardError
flash[:error] = l(:error_unable_delete_dmsf_workflow)
end
if @project
redirect_to settings_project_path(@project, tab: 'dmsf_workflow')
else
redirect_to dmsf_workflows_path
end
end
def autocomplete_for_user
respond_to do |format|
format.js
end
end
def new_step
@steps = @dmsf_workflow.dmsf_workflow_steps.select('step, MAX(name) AS name').group(:step)
respond_to do |format|
format.html
format.js
end
end
def add_step
if request.post?
step = if params[:step] == '0'
@dmsf_workflow.dmsf_workflow_steps.collect(&:step).uniq.count + 1
else
params[:step].to_i
end
operator = params[:commit] == l(:dmsf_and) ? DmsfWorkflowStep::OPERATOR_AND : DmsfWorkflowStep::OPERATOR_OR
user_ids = User.where(id: params[:user_ids]).ids
if user_ids.any?
user_ids.each do |user_id|
ws = DmsfWorkflowStep.new
ws.dmsf_workflow_id = @dmsf_workflow.id
ws.step = step
ws.user_id = user_id
ws.operator = operator
ws.name = params[:name]
if ws.save
@dmsf_workflow.dmsf_workflow_steps << ws
else
flash[:error] = ws.errors.full_messages.to_sentence
end
end
else
flash[:error] = l(:error_workflow_assign)
end
end
respond_to do |format|
format.html
end
end
def remove_step
if request.delete?
DmsfWorkflowStep.where(dmsf_workflow_id: @dmsf_workflow.id, step: params[:step]).find_each do |ws|
@dmsf_workflow.dmsf_workflow_steps.delete ws
end
@dmsf_workflow.dmsf_workflow_steps.each do |ws|
n = ws.step.to_i
next unless n > params[:step].to_i
ws.step = n - 1
flash[:error] = l(:notice_cannot_renumber_steps) unless ws.save
end
end
redirect_back_or_default dmsf_workflow_path(@dmsf_workflow)
end
def reorder_steps
if request.put?
if @assigned
flash[:error] = l(:error_dmsf_workflow_assigned)
else
@dmsf_workflow.reorder_steps params[:step].to_i, params[:dmsf_workflow][:position].to_i
end
end
respond_to do |format|
format.html
format.js
end
end
def start
revision = DmsfFileRevision.find_by(id: params[:dmsf_file_revision_id])
if revision
revision.set_workflow(@dmsf_workflow.id, params[:action])
if revision.save
if Setting.notified_events.include?('dmsf_workflow_plural')
@dmsf_workflow.notify_users(@project, revision, self)
end
flash[:notice] = l(:notice_workflow_started)
else
flash[:error] = l(:notice_cannot_start_workflow)
end
end
redirect_back_or_default dmsf_folder_path(id: @project, folder_id: @folder)
end
def update_step
# Name
if params[:dmsf_workflow].present?
index = params[:step].to_i
step = @dmsf_workflow.dmsf_workflow_steps[index]
step.name = params[:dmsf_workflow][:step_name]
if step.save
@dmsf_workflow.dmsf_workflow_steps.where(step: step.step).find_each do |s|
s.name = step.name
flash[:error] = s.errors.full_messages.to_sentence unless s.save
end
else
flash[:error] = step.errors.full_messages.to_sentence
end
end
# Operators/Assignees
if params[:operator_step].present?
params[:operator_step].each do |id, operator|
step = DmsfWorkflowStep.find_by(id: id)
next unless step
step.operator = operator.to_i
step.user_id = params[:assignee][id]
next if step.save
flash[:error] = step.errors.full_messages.to_sentence
Rails.logger.error step.errors.full_messages.to_sentence
end
end
redirect_to dmsf_workflow_path(@dmsf_workflow)
end
def delete_step
step = DmsfWorkflowStep.find_by(id: params[:step])
if step
# Safe the name
if step.name.present?
@dmsf_workflow.dmsf_workflow_steps.each do |s|
if s.step == step.step
s.name = step.name
s.save!
end
end
end
# Destroy
step.destroy
end
redirect_to dmsf_workflow_path(@dmsf_workflow)
end
private
def find_project
if @dmsf_workflow
if @dmsf_workflow.project # Project workflow
@project = @dmsf_workflow.project
elsif params[:dmsf_file_revision_id].present? # Global workflow
revision = DmsfFileRevision.find_by(id: params[:dmsf_file_revision_id])
@project = revision.dmsf_file.project if revision&.dmsf_file
elsif params[:project_id].present?
@project = Project.find params[:project_id]
end
elsif params[:dmsf_workflow].present?
@project = Project.find params[:dmsf_workflow][:project_id]
elsif params[:project_id].present?
@project = Project.find params[:project_id]
elsif params[:id].present?
@project = Project.find(params[:id])
end
rescue ActiveRecord::RecordNotFound
@project = nil
end
def workflows_layout
@project ? 'base' : 'admin'
end
def authorize_custom
if @project
authorize
else
require_admin
end
end
def approver_candidates
@approving_candidates = @project ? @project.users.to_a : User.active.to_a
end
def prevent_from_editing
# A workflow in use can be neither edited nor deleted
@assigned = DmsfFileRevision.exists?(dmsf_workflow_id: @dmsf_workflow.id)
return unless @assigned && !request.put?
flash[:error] = l(:error_dmsf_workflow_assigned)
if @project
redirect_back_or_default settings_project_path(@project, tab: 'dmsf_workflow')
else
redirect_back_or_default dmsf_workflows_path
end
end
end

View File

@ -0,0 +1,23 @@
# frozen_string_literal: true
# Redmine plugin for Document Management System "Features"
#
# Vít Jonáš <vit.jonas@gmail.com>, Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
# Access error
class DmsfAccessError < StandardError
# Nothing to do
end

View File

@ -0,0 +1,31 @@
# frozen_string_literal: true
# Redmine plugin for Document Management System "Features"
#
# Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
# Max file size error
class DmsfEmailMaxFileSizeError < StandardError
include Redmine::I18n
def initialize(message = nil)
if message.present?
super
else
super(l(:error_max_email_filesize_exceeded, number: RedmineDmsf.dmsf_max_email_filesize))
end
end
end

View File

@ -0,0 +1,23 @@
# frozen_string_literal: true
# Redmine plugin for Document Management System "Features"
#
# Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
# Not found error
class DmsfFileNotFoundError < StandardError
# nothing to do
end

View File

@ -0,0 +1,23 @@
# frozen_string_literal: true
# Redmine plugin for Document Management System "Features"
#
# Daniel Munn <dan.munn@munnster.co.uk>, Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
# Lock error
class DmsfLockError < StandardError
# Nothing to do
end

View File

@ -0,0 +1,23 @@
# frozen_string_literal: true
# Redmine plugin for Document Management System "Features"
#
# Vít Jonáš <vit.jonas@gmail.com>, Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
# Parent error
class DmsfParentError < StandardError
# Nothing to do
end

View File

@ -0,0 +1,31 @@
# frozen_string_literal: true
# Redmine plugin for Document Management System "Features"
#
# Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
# File count exceeded
class DmsfZipMaxFilesError < StandardError
include Redmine::I18n
def initialize(message = nil)
if message.present?
super
else
super(l(:error_max_files_exceeded, number: RedmineDmsf.dmsf_max_file_download))
end
end
end

View File

@ -0,0 +1,39 @@
# frozen_string_literal: true
# Redmine plugin for Document Management System "Features"
#
# Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
# Files helper
module DmsfFilesHelper
def clean_wiki_text(text)
# If there is <p> tag, the text is moved one column to the right by Redmin's CSS. A new line causes double new line.
text.gsub('<p>', '')
.gsub('</p>', '')
.gsub("\n\n", '<br>')
.gsub("\n\t", '<br>')
end
def render_document_content(dmsf_file, content)
if dmsf_file.markdown?
render partial: 'common/markup', locals: { markup_text_formatting: markdown_formatter, markup_text: content }
elsif dmsf_file.textile?
render partial: 'common/markup', locals: { markup_text_formatting: 'textile', markup_text: content }
else
render partial: 'common/file', locals: { content: content, filename: dmsf_file.name }
end
end
end

View File

@ -0,0 +1,35 @@
# frozen_string_literal: true
# Redmine plugin for Document Management System "Features"
#
# Vít Jonáš <vit.jonas@gmail.com>, Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
# Folders permissions helper
module DmsfFolderPermissionsHelper
def users_checkboxes(users, inherited: false)
s = []
id = inherited ? 'inherited_permissions[user_ids][]' : 'permissions[user_ids][]'
users.each do |user|
content = check_box_tag(id, user.id, true, disabled: inherited, id: nil) + user.name
s << content_tag(:label, content, id: "user_permission_ids_#{user.id}", class: 'inline')
end
safe_join s
end
def render_principals_for_new_folder_permissions(users)
principals_check_box_tags 'user_ids[]', users
end
end

View File

@ -0,0 +1,91 @@
# frozen_string_literal: true
# Redmine plugin for Document Management System "Features"
#
# Vít Jonáš <vit.jonas@gmail.com>, Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
require 'tmpdir'
require 'csv'
# DMSF helper
module DmsfHelper
include Redmine::I18n
def self.temp_filename(filename)
filename = sanitize_filename(filename)
timestamp = DateTime.current.strftime('%y%m%d%H%M%S')
timestamp.succ! while Rails.root.join("tmp/#{timestamp}_#{filename}").exist?
"#{timestamp}_#{filename}"
end
def self.sanitize_filename(filename)
# Get only the filename, not the whole path
just_filename = File.basename(filename.gsub('\\\\', '/'))
# Replace all non alphanumeric, hyphens or periods with underscore
just_filename.gsub!(/[^\w.\-]/, '_')
# Keep the extension if any
if !/^[a-zA-Z0-9_.\-]*$/.match?(just_filename) && just_filename =~ /(.[a-zA-Z0-9]+)$/
extension = Regexp.last_match(1)
just_filename = Digest::SHA256.hexdigest(just_filename) << extension
end
just_filename
end
def plugin_asset_path(plugin, asset_type, source)
File.join('/plugin_assets', plugin.to_s, asset_type, source)
end
def render_principals_for_new_email(users)
principals_check_box_tags 'user_ids[]', users
end
def webdav_url(project, folder)
url = ["#{Setting.protocol}:/", Setting.host_name, 'dmsf', 'webdav']
if project
url << ERB::Util.url_encode(RedmineDmsf::Webdav::ProjectResource.create_project_name(project))
if folder
folders = []
while folder
folders << folder
folder = folder.dmsf_folder
end
folders.reverse_each do |f|
url << f.title
end
end
end
url << ''
url.join '/'
end
# Downloads zipped files securely by sanitizing the params[:entry]. Characters considered unsafe
# are replaced with a underscore. The file_path is joined with File.join instead of Rails.root.join to eliminate
# the risk of overriding the absolute path (Rails.root/tmp) with file_name when given as absoulte path too. This
# makes the path double secure.
def email_entry_tmp_file_path(entry)
sanitized_entry = DmsfHelper.sanitize_filename(entry)
file_name = "#{RedmineDmsf::DmsfZip::FILE_PREFIX}#{sanitized_entry}.zip"
Rails.root.join 'tmp', file_name
end
# Extracts the variable part of the temp file name to be used as identifier in the
# download email entries route.
def tmp_entry_identifier(zipped_content)
path = Pathname.new(zipped_content)
zipped_file = path.basename(path.extname).to_s
zipped_file.delete_prefix RedmineDmsf::DmsfZip::FILE_PREFIX
end
end

View File

@ -0,0 +1,48 @@
# frozen_string_literal: true
# Redmine plugin for Document Management System "Features"
#
# Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
# Links helper
module DmsfLinksHelper
def folder_tree_options_for_select(folder_tree, options = {})
s = []
folder_tree.each do |name, id|
tag_options = { value: id }
tag_options[:selected] = 'selected' if id == options[:selected]
s << content_tag('option', name, tag_options)
end
safe_join s
end
# An integer test
def self.number?(str)
str&.match?(/\A\d+\Z/)
end
def files_for_select(project_id, folder_id = nil)
files = []
if DmsfLinksHelper.number?(folder_id)
folder = DmsfFolder.find_by(id: folder_id)
files = folder.dmsf_files.visible.to_a if folder
elsif project_id
project = Project.find_by(id: project_id)
files = project.dmsf_files.visible.to_a if project
end
files
end
end

View File

@ -0,0 +1,326 @@
# frozen_string_literal: true
# Redmine plugin for Document Management System "Features"
#
# Vít Jonáš <vit.jonas@gmail.com>, Daniel Munn <dan.munn@munnster.co.uk>, Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
# Queries helper
module DmsfQueriesHelper
include ApplicationHelper
def column_value(column, item, value)
return super unless item.is_a?(DmsfFolder)
case column.name
when :modified
val = super
case item.type
when 'file'
file = DmsfFile.find_by(id: item.id)
if !item.deleted? && file&.locked?
return content_tag(:span, val) +
link_to(sprite_icon('unlock', nil, icon_only: true, size: '12'),
unlock_dmsf_files_path(id: file,
back_url: dmsf_folder_path(id: file.project,
folder_id: file.dmsf_folder)),
title: l(:title_locked_by_user, user: file.locked_by), class: 'icon icon-unlock')
end
when 'folder'
folder = DmsfFolder.find_by(id: item.id)
if !item.deleted? && folder&.locked?
return content_tag(:span, val) +
link_to(sprite_icon('unlock', nil, icon_only: true, size: '12'),
unlock_dmsf_path(id: folder.project,
folder_id: folder.id,
back_url: dmsf_folder_path(id: folder.project,
folder_id: folder.dmsf_folder)),
title: l(:title_locked_by_user, user: folder.locked_by), class: 'icon icon-unlock')
end
end
content_tag(:span, val) + content_tag(:span, '', class: 'icon icon-none')
when :id
case item.type
when 'file'
if item.deleted?
h(value)
else
link_to h(value), dmsf_file_path(id: item.id)
end
when 'file-link'
if item.deleted?
h(item.revision_id)
else
link_to h(item.revision_id), dmsf_file_path(id: item.revision_id)
end
when 'folder'
if item.id
if item.deleted?
h(value)
else
link_to h(value), edit_dmsf_path(id: item.project_id, folder_id: item.id)
end
elsif item.deleted?
h(item.project_id)
else
link_to h(item.project_id), edit_root_dmsf_path(id: item.project_id)
end
when 'folder-link'
if item.id
if item.deleted?
h(item.revision_id)
else
link_to h(item.revision_id), edit_dmsf_path(id: item.project_id, folder_id: item.revision_id)
end
elsif item.deleted?
h(item.project_id)
else
link_to h(item.project_id), edit_root_dmsf_path(id: item.project_id)
end
when 'project'
link_to h(item.project_id), edit_root_dmsf_path(id: item.project_id)
else
h(value)
end
when :author
if value
user = User.find_by(id: value)
if user
link_to user.name, user_path(id: value)
else
super
end
else
super
end
when :title
case item.type
when 'project'
tag = h("[#{value}]")
tag = if item.project.module_enabled?(:dmsf)
link_to(sprite_icon('folder', nil, icon_only: true), dmsf_folder_path(id: item.project),
class: 'icon icon-folder') +
link_to(tag, dmsf_folder_path(id: item.project), class: 'dmsf-label')
else
sprite_icon 'folder', tag
end
unless filter_any?
path = expand_folder_dmsf_path
columns = params['c']
if columns.present?
path << '?'
path << columns.map { |col| "c[]=#{col}" }.join('&')
end
tag = content_tag(
'span',
'',
class: 'dmsf-expander',
onclick: "dmsfToggle(this, '#{item.id}', null,'#{escape_javascript(path)}')"
) + tag
tag = content_tag('div', tag, class: 'row-control dmsf-row-control')
end
tag += content_tag('div', item.filename, class: 'dmsf-filename')
if item.project.watched_by?(User.current)
tag += link_to(sprite_icon('unwatch', nil, icon_only: true, size: '12'),
watch_path(object_type: 'project', object_id: item.project.id),
title: l(:button_unwatch),
method: 'delete',
class: 'icon icon-fav')
end
tag
when 'folder'
if item&.deleted?
tag = content_tag(:span, sprite_icon('folder', h(value)), class: 'icon icon-folder')
else
tag = link_to(sprite_icon('folder', nil,
icon_only: true,
css_class: item.system ? 'dmsf-system' : ''),
dmsf_folder_path(id: item.project, folder_id: item.id), class: 'icon icon-folder')
tag += link_to(h(value), dmsf_folder_path(id: item.project, folder_id: item.id), class: 'dmsf-label')
unless filter_any?
path = expand_folder_dmsf_path
columns = params['c']
if columns.present?
path << '?'
path << columns.map { |col| "c[]=#{col}" }.join('&')
end
tag = content_tag(
'span',
'',
class: 'dmsf-expander',
onclick: "dmsfToggle(this, '#{item.project.id}', '#{item.id}','#{escape_javascript(path)}')"
) + tag
tag = content_tag('div', tag, class: 'row-control dmsf-row-control')
end
end
tag += content_tag('div', item.filename, class: 'dmsf-filename', title: l(:title_filename_for_download))
if !item&.deleted? && item.watched_by?(User.current)
tag += link_to(sprite_icon('unwatch', nil, icon_only: true, size: '12'),
watch_path(object_type: 'dmsf_folder', object_id: item.id),
title: l(:button_unwatch),
method: 'delete',
class: 'icon icon-fav')
end
tag
when 'folder-link'
if item&.deleted?
tag = content_tag(:span, sprite_icon('folder', h(value)), class: 'icon icon-folder')
else
# For links, we use revision_id containing dmsf_folder.id in fact
tag = link_to(sprite_icon('folder', nil, icon_only: true, css_class: 'dmsf-gray'),
dmsf_folder_path(id: item.project, folder_id: item.revision_id), class: 'icon icon-folder')
tag += link_to(h(value), dmsf_folder_path(id: item.project, folder_id: item.revision_id), class: 'dmsf-label')
tag = content_tag('span', '', class: 'dmsf-expander') + tag unless filter_any?
end
tag + content_tag('div', item.filename, class: 'dmsf-filename', title: l(:label_target_folder))
when 'file', 'file-link'
icon_name = icon_for_mime_type(Redmine::MimeType.css_class_of(item.filename))
icon_class = icon_class_for_mime_type(item.filename)
if item&.deleted?
tag = content_tag(:span, sprite_icon(icon_name, h(value)), class: "icon #{icon_class}")
else
# For links, we use revision_id containing dmsf_file.id in fact
file_view_url = url_for(
{ controller: :dmsf_files, action: 'view', id: item.type == 'file' ? item.id : item.revision_id }
)
content_type = Redmine::MimeType.of(item.filename)
content_type = 'application/octet-stream' if content_type.blank?
options = { class: "dmsf-label icon #{icon_class}",
'data-downloadurl': "#{content_type}:#{h(value)}:#{file_view_url}" }
unless previewable?(item.filename, content_type)
options[:target] = '_blank'
options[:rel] = 'noopener'
end
tag = link_to(sprite_icon(icon_name,
nil,
icon_only: true,
css_class: item.type == 'file-link' ? 'dmsf-gray' : ''),
file_view_url,
options)
options[:class] = 'dmsf-label'
tag += link_to(h(value), file_view_url, options)
tag = content_tag('span', '', class: 'dmsf-expander') + tag unless filter_any?
end
member = Member.find_by(user_id: User.current.id, project_id: item.project_id)
revision = DmsfFileRevision.find_by(id: item.customized_id)
filename = revision ? revision.formatted_name(member) : item.filename
tag += content_tag('div', filename, class: 'dmsf-filename', title: l(:title_filename_for_download))
if (item.type == 'file') && !item&.deleted? && revision.dmsf_file&.watched_by?(User.current)
tag += link_to(sprite_icon('unwatch', nil, icon_only: true, size: '12'),
watch_path(object_type: 'dmsf_file', object_id: item.id),
title: l(:button_unwatch),
method: 'delete',
class: 'icon icon-fav')
end
tag
when 'url-link'
if item&.deleted?
tag = content_tag(:span, sprite_icon('link', h(value)), class: 'icon icon-link')
else
tag = link_to(sprite_icon('link', nil, icon_only: true, css_class: 'dmsf-gray'),
item.filename,
target: '_blank',
rel: 'noopener',
class: 'icon icon-link')
tag += link_to(h(value), item.filename, target: '_blank', rel: 'noopener')
tag = content_tag('span', '', class: 'dmsf-expander') + tag unless filter_any?
end
tag + content_tag('div', item.filename, class: 'dmsf-filename', title: l(:field_url))
else
h(value)
end
when :size
number_to_human_size value
when :workflow
if value
if item.workflow_id && !item&.deleted?
url = if item.type == 'file'
log_dmsf_workflow_path project_id: item.project_id, id: item.workflow_id, dmsf_file_id: item.id
else
log_dmsf_workflow_path project_id: item.project_id, id: item.workflow_id, dmsf_link_id: item.id
end
text, names = DmsfWorkflow.workflow_info(item.workflow, item.workflow_id, item.customized_id)
link_to h(text), url, remote: true, title: names
else
h(DmsfWorkflow.workflow_str(value.to_i))
end
else
super
end
when :comment
value.present? ? content_tag('div', textilizable(value), class: 'wiki') : ''
else
super
end
end
def csv_value(column, object, value)
case column.name
when :size
ActiveSupport::NumberHelper.number_to_human_size value
when :workflow
if value
text, _names = DmsfWorkflow.workflow_info(object.workflow, object.workflow_id, object.revision_id)
text
else
super
end
when :author
if value
user = User.find_by(id: value)
if user
user.name
else
super
end
end
else
super
end
end
def filter_any?
# :v - value, :op - operator
size = params[:op]&.keys&.size
if size
if (size == 1) && params[:op].key?('title') && params[:op]['title'] == '~' && params[:v]['title'].join.empty?
return false
end
return true
end
false
end
def previewable?(filename, content_type)
case content_type
when 'text/markdown', 'text/x-textile'
true
else
Redmine::MimeType.is_type?('text', filename) || Redmine::SyntaxHighlighting.filename_supported?(filename)
end
end
def icon_class_for_mime_type(mime)
case Redmine::MimeType.of(mime)
when 'application/pdf'
'icon-pdf'
when 'text/plain'
'icon-txt'
else
'icon-file'
end
end
end

View File

@ -0,0 +1,214 @@
# frozen_string_literal: true
# Redmine plugin for Document Management System "Features"
#
# Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
# Upload helper
module DmsfUploadHelper
include Redmine::I18n
def self.commit_files_internal(committed_files, project, folder, controller = nil, container = nil, new_object: false)
failed_uploads = []
files = []
if committed_files
failed_uploads = []
committed_files.each_value do |committed_file|
name = committed_file[:name]
new_revision = DmsfFileRevision.new
file = DmsfFile.visible.find_file_by_name(project, folder, name)
unless file
link = DmsfLink.find_link_by_file_name(project, folder, name)
file = link.target_file if link
end
if file
if file.last_revision
last_revision = file.last_revision
new_revision.source_revision = last_revision
end
else
file = DmsfFile.new
file.project_id = project.id
file.name = name
file.dmsf_folder = folder
file.notification = RedmineDmsf.dmsf_default_notifications?
end
if file.locked_for_user?
failed_uploads.push file
next
end
new_revision.dmsf_file = file
new_revision.user = User.current
new_revision.name = name
new_revision.title = committed_file[:title]
new_revision.description = committed_file[:description]
new_revision.comment = committed_file[:comment]
new_revision.major_version = if committed_file[:version_major].present?
DmsfUploadHelper.db_version committed_file[:version_major]
else
1
end
new_revision.minor_version = if committed_file[:version_minor].present?
DmsfUploadHelper.db_version committed_file[:version_minor]
end
new_revision.patch_version = if committed_file[:version_patch].present?
DmsfUploadHelper.db_version committed_file[:version_patch]
end
new_revision.mime_type = committed_file[:mime_type]
new_revision.size = committed_file[:size]
new_revision.digest = committed_file[:digest]
# Custom fields
new_revision.copy_custom_field_values(committed_file[:custom_field_values])
# Need to save file first to generate id for it in case of creation.
# File id is needed to properly generate revision disk filename
unless new_revision.valid?
Rails.logger.error new_revision.errors.full_messages.to_sentence
failed_uploads.push new_revision
next
end
unless file.save
Rails.logger.error file.errors.full_messages.to_sentence
failed_uploads.push file
next
end
new_revision.disk_filename = new_revision.new_storage_filename
if new_revision.save
new_revision.assign_workflow committed_file[:dmsf_workflow_id]
begin
# If the path is not present we get it from the token
if committed_file[:token].present?
a = Attachment.find_by_token(committed_file[:token])
committed_file[:tempfile_path] = a.diskfile if a
end
FileUtils.mv committed_file[:tempfile_path], new_revision.disk_file(search_if_not_exists: false)
FileUtils.chmod 'u=wr,g=r', new_revision.disk_file(search_if_not_exists: false)
file.last_revision = new_revision
files.push file
container.dmsf_file_added file if container && !new_object
Redmine::Hook.call_hook :dmsf_helper_upload_after_commit, { file: file }
rescue StandardError => e
Rails.logger.error e.message
controller.flash[:error] = e.message if controller
failed_uploads.push file
end
else
failed_uploads.push new_revision
end
# Approval workflow
next if committed_file[:workflow_id].blank?
wf = DmsfWorkflow.find_by(id: committed_file[:workflow_id])
next unless wf
# Assign the workflow
new_revision.set_workflow wf.id, 'assign'
new_revision.assign_workflow wf.id
# Start the workflow
new_revision.set_workflow wf.id, 'start'
if new_revision.save
wf.notify_users project, new_revision, controller
begin
file.lock!
rescue DmsfLockError => e
Rails.logger.warn e.message
end
else
Rails.logger.error l(:error_workflow_assign)
end
end
# Notifications
begin
recipients = DmsfMailer.deliver_files_updated(project, files)
if RedmineDmsf.dmsf_display_notified_recipients? && recipients.any?
max_recipients = RedmineDmsf.dmsf_max_notification_receivers_info
to = recipients.collect { |user, _| user.name }.first(max_recipients).join(', ')
if to.present?
to << (recipients.count > max_recipients ? ',...' : '.')
controller.flash[:warning] = l(:warning_email_notifications, to: to) if controller
end
end
rescue StandardError => e
Rails.logger.error "Could not send email notifications: #{e.message}"
end
end
if failed_uploads.present? && controller
controller.flash[:warning] = l(:warning_some_files_were_not_committed,
files: failed_uploads.pluck(:name).join(', '))
end
[files, failed_uploads]
end
# 0..99, A..Z except O
def self.major_version_select_options
(0..99).to_a + ('A'..'Y').to_a - ['O']
end
# 0..999, ' '
def self.minor_version_select_options
(0..999).to_a + [' ']
end
class << self
alias patch_version_select_options minor_version_select_options
end
# 1 -> 2, -1 -> -2, A -> B
def self.increase_version(version)
return nil unless version
return 1 if (version == ' ') || ((-version) == ' '.ord)
if Integer(version)
if version >= 0
if (version + 1) < 1000
version + 1
else
version
end
elsif -(version - 1) < 'Z'.ord
version - 1
else
version
end
end
rescue StandardError
if (version.ord + 1) < 'Z'.ord
(version.ord + 1).chr
else
version
end
end
# 1 -> 1, -65 -> A
def self.gui_version(version)
return ' ' unless version
return version if version >= 0
(-version).chr
end
# 1 -> 1, A -> -65
def self.db_version(version)
return nil if version.nil? || version == '&nbsp;'
version.to_i if Integer(version)
rescue StandardError
version = ' ' if version.blank?
-version.ord
end
end

View File

@ -0,0 +1,106 @@
# frozen_string_literal: true
# Redmine plugin for Document Management System "Features"
#
# Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
# Workflows helper
module DmsfWorkflowsHelper
def render_principals_for_new_dmsf_workflow_users(workflow, dmsf_workflow_step_assignment_id = nil,
dmsf_file_revision_id = nil)
scope = workflow.delegates(params[:q], dmsf_workflow_step_assignment_id, dmsf_file_revision_id)
principal_count = scope.count
principal_pages = Redmine::Pagination::Paginator.new principal_count, 100, params['page']
principals = scope.offset(principal_pages.offset).limit(principal_pages.per_page).to_a
# Delegation
s = []
s << if dmsf_workflow_step_assignment_id
content_tag(
'div',
content_tag('div', principals_radio_button_tags('step_action', principals),
id: 'dmsf_users_for_delegate'),
class: 'objects-selection'
)
# New step
else
content_tag(
'div',
content_tag('div', principals_check_box_tags('user_ids[]', principals), id: 'users'),
class: 'objects-selection'
)
end
links = pagination_links_full(principal_pages, principal_count, per_page_links: false) do |text, parameters, _|
link_to text,
autocomplete_for_user_dmsf_workflow_path(workflow, parameters.merge(q: params[:q], format: 'js')),
remote: true
end
s << content_tag('span', links, class: 'pagination')
safe_join s
end
def dmsf_workflow_steps_options_for_select(steps)
options = [[l(:dmsf_new_step), 0]]
steps.each do |step|
options << [step.name.presence || step.step.to_s, step.step]
end
options_for_select options, 0
end
def dmsf_workflows_for_select(project, dmsf_workflow_id)
options = [['', -1]]
DmsfWorkflow.active.sorted.where(['project_id = ? OR project_id IS NULL', project&.id]).find_each do |wf|
options << if wf.project_id
[wf.name, wf.id]
else
["#{wf.name} #{l(:note_global)}", wf.id]
end
end
options_for_select options, selected: dmsf_workflow_id
end
def dmsf_all_workflows_for_select(dmsf_workflow_id)
options = [['', 0]]
DmsfWorkflow.active.sorted.find_each do |wf|
if wf.project_id
prj = Project.find_by(id: wf.project_id)
if User.current.allowed_to?(:manage_workflows, prj)
# Local approval workflows
options << if prj
["#{wf.name} (#{prj.name})", wf.id]
else
[wf.name, wf.id]
end
end
else
# Global approval workflows
options << ["#{wf.name} #{l(:note_global)}", wf.id]
end
end
options_for_select options, selected: dmsf_workflow_id
end
def principals_radio_button_tags(name, principals)
s = []
principals.each do |principal|
n = principal.id * 10
id = "principal_#{n}"
s << radio_button_tag(name, n, false, onclick: 'noteMandatory(true);', id: id)
s << content_tag(:label, h(principal), for: id)
s << tag.br
end
safe_join s
end
end

684
app/models/dmsf_file.rb Normal file
View File

@ -0,0 +1,684 @@
# frozen_string_literal: true
# Redmine plugin for Document Management System "Features"
#
# Vít Jonáš <vit.jonas@gmail.com>, Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
require "#{File.dirname(__FILE__)}/../../lib/redmine_dmsf/lockable"
require "#{File.dirname(__FILE__)}/../../lib/redmine_dmsf/plugin"
require 'English'
# File
class DmsfFile < ApplicationRecord
include RedmineDmsf::Lockable
belongs_to :project
belongs_to :dmsf_folder
belongs_to :deleted_by_user, class_name: 'User'
has_many :dmsf_file_revisions, -> { order(created_at: :desc, id: :desc) }, dependent: :destroy, inverse_of: :dmsf_file
has_many :locks, -> { where(entity_type: 0).order(updated_at: :desc) },
class_name: 'DmsfLock', foreign_key: 'entity_id', dependent: :destroy, inverse_of: :dmsf_file
has_many :referenced_links, -> { where target_type: DmsfFile.model_name.to_s },
class_name: 'DmsfLink', foreign_key: 'target_id', dependent: :destroy, inverse_of: false
has_many :dmsf_public_urls, dependent: :destroy
STATUS_DELETED = 1
STATUS_ACTIVE = 0
scope :visible, -> { where(deleted: STATUS_ACTIVE) }
scope :deleted, -> { where(deleted: STATUS_DELETED) }
validates :name, dmsf_file_name: true
validates :name, length: { maximum: 255 }
validates :name,
uniqueness: {
scope: %i[dmsf_folder_id project_id deleted],
conditions: -> { where(deleted: STATUS_ACTIVE) },
case_sensitive: true
}
acts_as_event(
title: proc { |o|
@searched_revision = nil
o.dmsf_file_revisions.visible.each do |r|
key = "DmsfFile-#{o.id}-#{r.id}"
@desc = Redmine::Search.cache_store.fetch(key)
next unless @desc
Redmine::Search.cache_store.delete key
@searched_revision = r
break
end
if @searched_revision && (@searched_revision != o.last_revision)
"#{o.name} (r#{@searched_revision.id})"
else
o.name
end
},
description: proc { |o|
unless @desc
# Set desc to an empty string if o.description is nil
@desc = o.description.nil? ? +'' : +o.description
@desc += ' / ' if o.description.present? && o.last_revision.comment.present?
@desc += o.last_revision.comment if o.last_revision.comment.present?
end
@desc
},
url: proc { |o|
if @searched_revision
{ controller: 'dmsf_files', action: 'view', id: o.id, download: @searched_revision.id,
filename: o.name }
else
{ controller: 'dmsf_files', action: 'view', id: o.id, filename: o.name }
end
},
datetime: proc { |o|
if @searched_revision
@searched_revision.updated_at
else
o.updated_at
end
},
author: proc { |o|
if @searched_revision
@searched_revision.user
else
o.last_revision.user
end
}
)
acts_as_watchable
acts_as_searchable(
columns: [
"#{table_name}.name",
"#{DmsfFileRevision.table_name}.title",
"#{DmsfFileRevision.table_name}.description",
"#{DmsfFileRevision.table_name}.comment"
],
project_key: 'project_id',
date_column: "#{table_name}.updated_at"
)
before_create :default_values
before_destroy :delete_system_folder_before
after_destroy :delete_system_folder_after
attr_writer :last_revision
def self.previews_storage_path
Rails.root.join 'tmp/dmsf_previews'
end
def default_values
return unless RedmineDmsf.dmsf_default_notifications? && (!dmsf_folder || !dmsf_folder.system)
self.notification = true
end
def initialize(*args)
super
self.watcher_user_ids = [] if new_record?
end
def self.storage_path
path = RedmineDmsf.dmsf_storage_directory
pn = Pathname.new(path)
return pn if pn.absolute?
Rails.root.join path
end
def self.find_file_by_name(project, folder, name)
findn_file_by_name project&.id, folder, name
end
def self.findn_file_by_name(project_id, folder, name)
visible.find_by project_id: project_id, dmsf_folder_id: folder&.id, name: name
end
def approval_allowed_zero_minor
RedmineDmsf.only_approval_zero_minor_version? ? last_revision.minor_version&.zero? : true
end
def last_revision
unless defined?(@last_revision)
@last_revision = deleted? ? dmsf_file_revisions.first : dmsf_file_revisions.visible.first
end
@last_revision
end
def deleted?
deleted == STATUS_DELETED
end
def locked_by
if lock && lock.reverse[0]
user = lock.reverse[0].user
return user == User.current ? l(:label_me) : user.name if user
end
''
end
def delete(commit: false)
if locked_for_user? && !User.current.allowed_to?(:force_file_unlock, project)
Rails.logger.info l(:error_file_is_locked)
if lock.reverse[0].user
errors.add(:base, l(:title_locked_by_user, user: lock.reverse[0].user))
else
errors.add(:base, l(:error_file_is_locked))
end
return false
end
begin
# Revisions and links of a deleted file SHOULD be deleted too
dmsf_file_revisions.each { |r| r.delete(commit: commit, force: true) }
if commit
destroy
else
self.deleted = STATUS_DELETED
self.deleted_by_user = User.current
save
end
rescue StandardError => e
Rails.logger.error e.message
errors.add :base, e.message
false
end
end
def restore
if dmsf_folder_id && (dmsf_folder.nil? || dmsf_folder.deleted?)
errors.add(:base, l(:error_parent_folder))
return false
end
dmsf_file_revisions.each(&:restore)
self.deleted = STATUS_ACTIVE
self.deleted_by_user = nil
save
end
def title
last_revision ? last_revision.title : name
end
def description
last_revision ? last_revision.description : ''
end
def version
last_revision ? last_revision.version : '0'
end
def workflow
last_revision&.workflow
end
def size
last_revision ? last_revision.size : 0
end
def dmsf_path
path = dmsf_folder ? dmsf_folder.dmsf_path : []
path.push self
path
end
def dmsf_path_str
dmsf_path.map(&:title).join('/')
end
def notify?
notification || dmsf_folder&.notify? || (!dmsf_folder && project.dmsf_notification)
end
def get_all_watchers(watchers)
watchers.concat notified_watchers
if dmsf_folder
watchers.concat dmsf_folder.notified_watchers
else
watchers.concat project.notified_watchers
end
end
def notify_deactivate
self.notification = nil
save!
end
def notify_activate
self.notification = true
save!
end
# Returns an array of projects that current user can copy file to
def self.allowed_target_projects_on_copy
projects = []
if User.current.admin?
projects = Project.visible.has_module('dmsf').all
elsif User.current.logged?
User.current.memberships.each do |m|
projects << m.project if m.project.module_enabled?('dmsf') &&
m.roles.detect { |r| r.allowed_to?(:file_manipulation) }
end
end
projects
end
def move_to(project, folder)
unless last_revision
errors.add :base, l(:error_at_least_one_revision_must_be_present)
Rails.logger.error l(:error_at_least_one_revision_must_be_present)
return false
end
source = "#{self.project.identifier}:#{dmsf_path_str}"
self.project = project
self.dmsf_folder = folder
new_revision = last_revision.clone
new_revision.user_id = last_revision.user_id
new_revision.workflow = nil
new_revision.dmsf_workflow_id = nil
new_revision.dmsf_workflow_assigned_by_user_id = nil
new_revision.dmsf_workflow_assigned_at = nil
new_revision.dmsf_workflow_started_by_user_id = nil
new_revision.dmsf_workflow_started_at = nil
new_revision.dmsf_file = self
new_revision.comment = l(:comment_moved_from, source: source)
new_revision.custom_values = []
last_revision.custom_values.each do |cv|
new_revision.custom_values << CustomValue.new({ custom_field: cv.custom_field, value: cv.value })
end
self.last_revision = new_revision
save && new_revision.save
end
def copy_to(project, folder = nil)
copy_to_filename project, folder, name
end
def copy_to_filename(project, folder, filename)
file = DmsfFile.new
file.dmsf_folder_id = folder.id if folder
file.project_id = project.id
if DmsfFile.exists?(project_id: file.project_id, dmsf_folder_id: file.dmsf_folder_id, name: filename)
1.step do |i|
gen_filename = " #{filename} #{l(:dmsf_copy, n: i)}"
unless DmsfFile.exists?(project_id: file.project_id, dmsf_folder_id: file.dmsf_folder_id, name: gen_filename)
filename = gen_filename
break
end
end
end
file.name = filename
file.notification = RedmineDmsf.dmsf_default_notifications?
if file.save && last_revision
new_revision = last_revision.clone
new_revision.dmsf_file = file
new_revision.disk_filename = new_revision.new_storage_filename
# Assign the same workflow if it's a global one or we are in the same project
new_revision.workflow = nil
new_revision.dmsf_workflow_id = nil
new_revision.dmsf_workflow_assigned_by_user_id = nil
new_revision.dmsf_workflow_assigned_at = nil
new_revision.dmsf_workflow_started_by_user_id = nil
new_revision.dmsf_workflow_started_at = nil
wf = last_revision.dmsf_workflow
if wf && (wf.project.nil? || (wf.project.id == project.id))
new_revision.set_workflow wf.id, nil
new_revision.assign_workflow wf.id
end
if File.exist? last_revision.disk_file
FileUtils.cp last_revision.disk_file, new_revision.disk_file(search_if_not_exists: false)
end
new_revision.comment = l(:comment_copied_from, source: "#{self.project.identifier}:#{dmsf_path_str}")
new_revision.custom_values = []
last_revision.custom_values.each do |cv|
v = CustomValue.new
v.custom_field = cv.custom_field
v.value = cv.value
new_revision.custom_values << v
end
if new_revision.save
file.last_revision = new_revision
else
errors.add :base, new_revision.errors.full_messages.to_sentence
Rails.logger.error new_revision.errors.full_messages.to_sentence
file.delete commit: true
file = nil
end
else
Rails.logger.error file.errors.full_messages.to_sentence
file.delete commit: true
file = nil
end
file
end
# To fulfill searchable module expectations
def self.search(tokens, projects = nil, options = {}, user = User.current)
tokens = [] << tokens unless tokens.is_a?(Array)
projects = [] << projects if projects.is_a?(Project)
project_ids = projects.collect(&:id) if projects
limit_options = ["dmsf_files.updated_at #{options[:before] ? '<' : '>'} ?", options[:offset]] if options[:offset]
columns = if options[:titles_only]
[searchable_options[:columns][1]]
else
searchable_options[:columns]
end
token_clauses = columns.collect { |column| "(LOWER(#{column}) LIKE ?)" }
sql = (["(#{token_clauses.join(' OR ')})"] * tokens.size).join(options[:all_words] ? ' AND ' : ' OR ')
find_options = [sql, * (tokens.collect { |w| "%#{w.downcase}%" } * token_clauses.size).sort]
project_conditions = []
project_conditions << Project.allowed_to_condition(user, :view_dmsf_files)
project_conditions << "#{Project.table_name}.id IN (#{project_ids.join(',')})" if project_ids.present?
scope = visible
.joins('JOIN dmsf_file_revisions ON dmsf_file_revisions.dmsf_file_id = dmsf_files.id')
.joins(:project)
scope = scope.limit(options[:limit]) if options[:limit].present?
scope = scope.where(limit_options) if limit_options.present?
scope = scope.where(project_conditions.join(' AND '))
results = scope.where(find_options).uniq.to_a
results.delete_if { |x| !DmsfFolder.permissions?(x.dmsf_folder) }
if !options[:titles_only] && RedmineDmsf::Plugin.lib_available?('xapian')
database = nil
begin
lang = RedmineDmsf.dmsf_stemming_lang
databasepath = File.join(RedmineDmsf.dmsf_index_database, lang)
database = Xapian::Database.new(databasepath)
rescue StandardError => e
Rails.logger.error "REDMINE_XAPIAN ERROR: Xapian database is not properly set, initiated or it's corrupted."
Rails.logger.error e.message
end
if database
enquire = Xapian::Enquire.new(database)
# Combine the rest of the command line arguments with spaces between
# them, so that simple queries don't have to be quoted at the shell
# level.
query_string = tokens.map { |x| x[-1, 1].eql?('*') ? x : "#{x}*" }.join(' ')
qp = Xapian::QueryParser.new
stemmer = Xapian::Stem.new(lang)
qp.stemmer = stemmer
qp.database = database
case RedmineDmsf.dmsf_stemming_strategy
when 'STEM_NONE'
qp.stemming_strategy = Xapian::QueryParser::STEM_NONE
when 'STEM_SOME'
qp.stemming_strategy = Xapian::QueryParser::STEM_SOME
when 'STEM_ALL'
qp.stemming_strategy = Xapian::QueryParser::STEM_ALL
end
qp.default_op = if options[:all_words]
Xapian::Query::OP_AND
else
Xapian::Query::OP_OR
end
flags = Xapian::QueryParser::FLAG_WILDCARD
flags |= Xapian::QueryParser::FLAG_CJK_NGRAM if RedmineDmsf.dmsf_enable_cjk_ngrams?
query = qp.parse_query(query_string, flags)
enquire.query = query
matchset = enquire.mset(0, 1000)
matchset&.matches&.each do |m|
docdata = m.document.data { url }
dochash = Hash[*docdata.scan(%r{(url|sample|modtime|author|type|size)=/?([^\n\]]+)}).flatten]
filename = dochash['url']
next unless filename
dmsf_attrs = filename.scan(%r{^\d{4}/\d{2}/(\d{12}_(\d+)_.*)$})
id_attribute = 0
id_attribute = dmsf_attrs[0][1] if dmsf_attrs.length.positive?
next if dmsf_attrs.empty? || id_attribute.to_i.zero?
dmsf_file = DmsfFile.visible.where(limit_options).find_by(id: id_attribute)
next unless dmsf_file && DmsfFolder.permissions?(dmsf_file.dmsf_folder) &&
user.allowed_to?(:view_dmsf_files, dmsf_file.project) &&
(project_ids.blank? || project_ids.include?(dmsf_file.project_id))
rev_id = DmsfFileRevision.where(dmsf_file_id: dmsf_file.id, disk_filename: dmsf_attrs[0][0])
.pick(:id)
if dochash['sample']
Redmine::Search.cache_store.write("DmsfFile-#{dmsf_file.id}-#{rev_id}",
dochash['sample'].force_encoding('UTF-8'))
end
break if options[:limit].present? && results.count >= options[:limit]
results << dmsf_file
end
end
end
[results, results.count]
end
def self.search_result_ranks_and_ids(tokens, user = User.current, projects = nil, options = {})
r = search(tokens, projects, options, user)[0]
r.map { |f| [f.updated_at.to_i, f.id] }
end
def display_name
member = Member.find_by(user_id: User.current.id, project_id: project_id)
fname = formatted_name(member)
return "#{fname[0, 25]}...#{fname[-25, 25]}" if fname.length > 50
fname
end
def text?
filename = last_revision&.disk_filename
Redmine::MimeType.is_type?('text', filename) ||
Redmine::SyntaxHighlighting.filename_supported?(filename)
end
def image?
Redmine::MimeType.is_type?('image', last_revision&.disk_filename)
end
def pdf?
Redmine::MimeType.of(last_revision&.disk_filename) == 'application/pdf'
end
def video?
Redmine::MimeType.is_type?('video', last_revision&.disk_filename)
end
def html?
Redmine::MimeType.of(last_revision&.disk_filename) == 'text/html'
end
def office_doc?
case File.extname(last_revision&.disk_filename)
when '.odt', '.ods', '.odp', '.odg', # LibreOffice
'.doc', '.docx', '.docm', '.xls', '.xlsx', '.xlsm', '.ppt', '.pptx', '.pptm', # MS Office
'.rtf' # Universal
true
else
false
end
end
def markdown?
Redmine::MimeType.of(last_revision&.disk_filename) == 'text/markdown'
end
def textile?
Redmine::MimeType.of(last_revision&.disk_filename) == 'text/x-textile'
end
def disposition
image? || pdf? || video? || html? ? 'inline' : 'attachment'
end
def thumbnailable?
Redmine::Thumbnail.convert_available? && (image? || (pdf? && Redmine::Thumbnail.gs_available?))
end
def previewable?
office_doc? && RedmineDmsf::Preview.office_available? && size <= Setting.file_max_size_displayed.to_i.kilobyte
end
# Deletes all previews
def self.clear_previews
Dir.glob(File.join(DmsfFile.previews_storage_path, '*.pdf')).each do |file|
File.delete file
end
end
def pdf_preview
return '' unless previewable?
target = File.join(DmsfFile.previews_storage_path, "#{File.basename(last_revision&.disk_file.to_s, '.*')}.pdf")
begin
RedmineDmsf::Preview.generate last_revision&.disk_file.to_s, target
rescue StandardError => e
Rails.logger.error do
%(An error occurred while generating preview for #{last_revision&.disk_file} to #{target}\n
Exception was: #{e.message})
end
''
end
end
def text_preview(limit)
result = +'No preview available'
if text?
begin
f = File.new(last_revision.disk_file)
f.each_line do |line|
case f.lineno
when 1
result = line
when limit.to_i + 1
break
else
result << line
end
end
rescue StandardError => e
result = e.message
end
end
result
end
def formatted_name(member)
if last_revision
last_revision.formatted_name(member)
else
name
end
end
def owner?(user)
last_revision&.user == user
end
def involved?(user)
dmsf_file_revisions.each do |file_revision|
return true if file_revision.user == user
end
false
end
def assigned?(user)
return false unless last_revision&.dmsf_workflow
last_revision.dmsf_workflow.next_assignments(last_revision.id).each do |assignment|
return true if assignment.user == user
end
false
end
def custom_value(custom_field)
return nill unless last_revision
last_revision.custom_field_values.each do |cv|
return cv if cv.custom_field == custom_field
end
nil
end
def extension
File.extname(last_revision.disk_filename).strip.downcase[1..] if last_revision
end
def thumbnail(options = {})
size = options[:size].to_i
if size.positive?
# Limit the number of thumbnails per image
size = (size / 50) * 50
# Maximum thumbnail size
size = 800 if size > 800
else
size = Setting.thumbnails_size.to_i
end
size = 100 unless size.positive?
target = File.join(Attachment.thumbnails_storage_path, "#{id}_#{last_revision.digest}_#{size}.thumb")
begin
Redmine::Thumbnail.generate last_revision.disk_file.to_s, target, size, pdf?
rescue StandardError => e
Rails.logger.error do
%(An error occured while generating thumbnail for #{last_revision.disk_file} to #{target}\n
Exception was: #{e.message})
end
nil
end
end
def locked_title
if locked_for_user?
return l(:title_locked_by_user, user: lock.reverse[0].user) if lock.reverse[0].user
return "#{l(:label_user)} #{l('activerecord.errors.messages.invalid')}"
end
l(:title_unlock_file)
end
def container
return unless dmsf_folder&.system && dmsf_folder.title&.match(/(^\d+)/)
id = Regexp.last_match(1).to_i
case dmsf_folder.dmsf_folder&.title
when '.Issues'
Issue.visible.find_by id: id
end
end
def to_s
name
end
def delete_system_folder_before
@parent_folder = dmsf_folder
end
def delete_system_folder_after
return unless @parent_folder&.system && @parent_folder.dmsf_files.empty? && @parent_folder.dmsf_links.empty?
@parent_folder&.destroy
end
end

View File

@ -0,0 +1,466 @@
# frozen_string_literal: true
# Redmine plugin for Document Management System "Features"
#
# Vít Jonáš <vit.jonas@gmail.com>, Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
# File revision
class DmsfFileRevision < ApplicationRecord
belongs_to :dmsf_file, inverse_of: :dmsf_file_revisions
belongs_to :source_revision,
class_name: 'DmsfFileRevision',
foreign_key: 'source_dmsf_file_revision_id',
inverse_of: false
belongs_to :user
belongs_to :deleted_by_user, class_name: 'User'
belongs_to :dmsf_workflow_started_by_user, class_name: 'User'
belongs_to :dmsf_workflow_assigned_by_user, class_name: 'User'
belongs_to :dmsf_workflow
has_many :dmsf_file_revision_access, dependent: :destroy
has_many :dmsf_workflow_step_assignment, dependent: :destroy
before_destroy :delete_source_revision
STATUS_DELETED = 1
STATUS_ACTIVE = 0
PATCH_VERSION = 1
MINOR_VERSION = 2
MAJOR_VERSION = 3
PROTOCOLS = {
'application/msword' => 'ms-word',
'application/excel' => 'ms-excel',
'application/vnd.ms-excel' => 'ms-excel',
'application/vnd.ms-powerpoint' => 'ms-powerpoint',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => 'ms-word',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => 'ms-excel',
'application/vnd.openxmlformats-officedocument.presentationml.presentation' => 'ms-powerpoint',
'application/vnd.openxmlformats-officedocument.presentationml.slideshow' => 'ms-powerpoint',
'application/vnd.oasis.opendocument.spreadsheet' => 'ms-excel',
'application/vnd.oasis.opendocument.text' => 'ms-word',
'application/vnd.oasis.opendocument.presentation' => 'ms-powerpoint',
'application/vnd.ms-excel.sheet.macroenabled.12' => 'ms-excel'
}.freeze
scope :visible, -> { where(deleted: STATUS_ACTIVE) }
scope :deleted, -> { where(deleted: STATUS_DELETED) }
acts_as_customizable
acts_as_event(
title: proc { |o|
if o.source_dmsf_file_revision_id.present?
"#{l(:label_dmsf_updated)}: #{o.dmsf_file.dmsf_path_str}"
else
"#{l(:label_created)}: #{o.dmsf_file.dmsf_path_str}"
end
},
url: proc { |o| { controller: 'dmsf_files', action: 'show', id: o.dmsf_file } },
datetime: proc { |o| o.updated_at },
description: proc { |o| "#{o.description}\n#{o.comment}" },
author: proc { |o| o.user }
)
acts_as_activity_provider(
type: 'dmsf_file_revisions',
timestamp: "#{DmsfFileRevision.table_name}.updated_at",
author_key: "#{DmsfFileRevision.table_name}.user_id",
permission: :view_dmsf_file_revisions,
scope: proc {
DmsfFileRevision
.joins(:dmsf_file)
.joins("JOIN #{Project.table_name} ON #{Project.table_name}.id = #{DmsfFile.table_name}.project_id")
.visible
}
)
validates :title, presence: true
validates :title, length: { maximum: 255 }
validates :major_version, presence: true
validates :name, dmsf_file_name: true
validates :name, length: { maximum: 255 }
validates :disk_filename, length: { maximum: 255 }
validates :name, dmsf_file_extension: true
validates :description, length: { maximum: 1.kilobyte }
validates :size, dmsf_max_file_size: true
def visible?(_user = nil)
deleted == STATUS_ACTIVE
end
def project
dmsf_file&.project
end
def folder
dmsf_file&.dmsf_folder
end
def self.remove_extension(filename)
filename[0, (filename.length - File.extname(filename).length)]
end
def self.filename_to_title(filename)
remove_extension(filename).gsub(/_+/, ' ')
end
def delete(commit: false, force: true)
if dmsf_file.locked_for_user?
errors.add :base, l(:error_file_is_locked)
return false
end
if !commit && !force && (dmsf_file.dmsf_file_revisions.length <= 1)
errors.add :base, l(:error_at_least_one_revision_must_be_present)
return false
end
if commit
destroy
else
self.deleted = DmsfFile::STATUS_DELETED
self.deleted_by_user = User.current
save
end
end
def obsolete
if dmsf_file.locked_for_user?
errors.add :base, l(:error_file_is_locked)
return false
end
self.workflow = DmsfWorkflow::STATE_OBSOLETE
save
end
def restore
self.deleted = DmsfFile::STATUS_ACTIVE
self.deleted_by_user = nil
save
end
def version
DmsfFileRevision.version major_version, minor_version, patch_version
end
def self.version(major_version, minor_version, patch_version)
return unless major_version
ver = DmsfUploadHelper.gui_version(major_version).to_s
if minor_version
ver << ".#{DmsfUploadHelper.gui_version(minor_version)}" if -minor_version != ' '.ord
ver << ".#{DmsfUploadHelper.gui_version(patch_version)}" if patch_version.present? && (-patch_version != ' '.ord)
end
ver
end
def storage_base_path
time = created_at || DateTime.current
DmsfFile.storage_path.join(time.strftime('%Y')).join time.strftime('%m')
end
def disk_file(search_if_not_exists: true)
path = storage_base_path
begin
FileUtils.mkdir_p(path)
rescue StandardError => e
Rails.logger.error e.message
end
filename = path.join(disk_filename)
if search_if_not_exists && !File.exist?(filename)
# Let's search for the physical file in source revisions
dmsf_file.dmsf_file_revisions.where(created_at: ...created_at).order(created_at: :desc).each do |rev|
filename = rev.disk_file
break if File.exist?(filename)
end
end
filename.to_s
end
def new_storage_filename
raise DmsfAccessError, 'File id is not set' unless dmsf_file&.id
filename = DmsfHelper.sanitize_filename(name)
timestamp = DateTime.current.strftime('%y%m%d%H%M%S')
timestamp.succ! while File.exist? storage_base_path.join("#{timestamp}_#{dmsf_file.id}_#{filename}")
"#{timestamp}_#{dmsf_file.id}_#{filename}"
end
def detect_content_type
content_type = mime_type
content_type = Redmine::MimeType.of(disk_filename) if content_type.blank?
content_type = 'application/octet-stream' if content_type.blank?
content_type
end
def clone
new_revision = DmsfFileRevision.new
new_revision.dmsf_file = dmsf_file
new_revision.disk_filename = disk_filename
new_revision.size = size
new_revision.mime_type = mime_type
new_revision.title = title
new_revision.description = description
new_revision.workflow = workflow
new_revision.major_version = major_version
new_revision.minor_version = minor_version
new_revision.patch_version = patch_version
new_revision.source_revision = self
new_revision.user = User.current
new_revision.name = name
new_revision.digest = digest
new_revision
end
def workflow_str(name)
str = ''
if name && dmsf_workflow_id
names = DmsfWorkflow.where(id: dmsf_workflow_id).pluck(:name)
str = "#{names.first} - " if names.any?
end
case workflow
when DmsfWorkflow::STATE_WAITING_FOR_APPROVAL
str + l(:title_waiting_for_approval)
when DmsfWorkflow::STATE_APPROVED
str + l(:title_approved)
when DmsfWorkflow::STATE_ASSIGNED
str + l(:title_assigned)
when DmsfWorkflow::STATE_REJECTED
str + l(:title_rejected)
when DmsfWorkflow::STATE_OBSOLETE
str + l(:title_obsolete)
else
str + l(:title_none)
end
end
def set_workflow(dmsf_workflow_id, commit)
self.dmsf_workflow_id = dmsf_workflow_id
if commit == 'start'
self.workflow = DmsfWorkflow::STATE_WAITING_FOR_APPROVAL
self.dmsf_workflow_started_by_user = User.current
self.dmsf_workflow_started_at = DateTime.current
else
self.workflow = DmsfWorkflow::STATE_ASSIGNED
self.dmsf_workflow_assigned_by_user = User.current
self.dmsf_workflow_assigned_at = DateTime.current
end
end
def assign_workflow(dmsf_workflow_id)
wf = DmsfWorkflow.find_by(id: dmsf_workflow_id)
wf.assign(id) if wf && id
end
def reset_workflow
self.workflow = nil
self.dmsf_workflow_id = nil
self.dmsf_workflow_assigned_by_user_id = nil
self.dmsf_workflow_assigned_at = nil
self.dmsf_workflow_started_by_user_id = nil
self.dmsf_workflow_started_at = nil
end
def increase_version(version_to_increase)
# Patch version
self.patch_version = case version_to_increase
when PATCH_VERSION
patch_version ||= 0
DmsfUploadHelper.increase_version patch_version
end
# Minor version
self.minor_version = case version_to_increase
when MINOR_VERSION
DmsfUploadHelper.increase_version minor_version
when MAJOR_VERSION
major_version.negative? ? -' '.ord : 0
else
minor_version
end
# Major version
self.major_version = case version_to_increase
when MAJOR_VERSION
DmsfUploadHelper.increase_version major_version
else
major_version
end
end
def copy_file_content(open_file)
sha = Digest::SHA256.new
File.open(disk_file(search_if_not_exists: false), 'wb') do |f|
if open_file.respond_to?(:read)
while (buffer = open_file.read(8192))
f.write buffer
sha.update buffer
end
else
f.write open_file
sha.update open_file
end
end
self.digest = sha.hexdigest
end
# Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
def available_custom_fields
DmsfFileRevisionCustomField.all
end
def iversion
parts = version.split '.'
parts.size == 2 ? ((parts[0].to_i * 1_000) + parts[1].to_i) : 0
end
def formatted_name(member)
format = if member&.dmsf_title_format.present?
member.dmsf_title_format
else
RedmineDmsf.dmsf_global_title_format
end
return name if format.blank?
if name =~ /(.*)(\..*)$/
filename = Regexp.last_match(1)
ext = Regexp.last_match(2)
else
filename = name
end
format2 = format
format2 = format2.sub('%t', title)
format2 = format2.sub('%f', filename)
format2 = format2.sub('%d', updated_at.strftime('%Y%m%d%H%M%S'))
format2 = format2.sub('%v', version)
format2 = format2.sub('%i', dmsf_file.id.to_s)
format2 = format2.sub('%r', id.to_s)
format2 += ext if ext
format2
end
def create_digest
self.digest = Digest::SHA256.file(path).hexdigest
rescue StandardError => e
Rails.logger.error e.message
self.digest = 0
end
# Returns either MD5 or SHA256 depending on the way self.digest was computed
def digest_type
return nil if digest.blank?
digest.size < 64 ? 'MD5' : 'SHA256'
end
def tooltip
text = description.presence || +''
if comment.present?
text += ' / ' if text.present?
text += comment
end
text
end
def workflow_tooltip
tooltip = +''
if dmsf_workflow
case workflow
when DmsfWorkflow::STATE_WAITING_FOR_APPROVAL, DmsfWorkflow::STATE_ASSIGNED
assignments = dmsf_workflow.next_assignments(id)
assignments&.each_with_index do |assignment, index|
tooltip << ', ' if index.positive?
tooltip << assignment.user.name
end
when DmsfWorkflow::STATE_APPROVED, DmsfWorkflow::STATE_REJECTED
action = DmsfWorkflowStepAction.joins(:dmsf_workflow_step_assignment)
.where(dmsf_workflow_step_assignments: { dmsf_file_revision_id: id })
.order('dmsf_workflow_step_actions.created_at')
.last
tooltip << action.author.name if action
end
end
tooltip
end
def protocol
@protocol ||= PROTOCOLS[mime_type.downcase] if mime_type
@protocol
end
def delete_source_revision
DmsfFileRevision.where(source_dmsf_file_revision_id: id).find_each do |d|
d.source_revision = source_revision
d.save!
end
return unless RedmineDmsf.physical_file_delete?
dependencies = DmsfFileRevision.where(disk_filename: disk_filename).all.size
FileUtils.rm_f(disk_file) if dependencies <= 1
end
def copy_custom_field_values(values, source_revision = nil)
# For a new revision we need to remember attachments' ids
if source_revision
ids = []
source_revision.custom_field_values.each do |cv|
ids << cv.value if cv.custom_field.format.is_a?(Redmine::FieldFormat::AttachmentFormat)
end
end
# ActionParameters => Hash
h = DmsfFileRevision.params_to_hash(values)
# From a REST API call we don't get "20" => "Project" but "CustomFieldValue20" => "Project"
h.transform_keys! { |key| key.to_i.zero? && key.to_s.match(/(\d+)/) ? :Regexp.last_match(0) : key }
# Super
self.custom_field_values = h
# For a new revision we need to duplicate attachments
return unless source_revision
i = 0
custom_field_values.each do |cv|
next unless cv.custom_field.format.is_a?(Redmine::FieldFormat::AttachmentFormat)
if cv.value.blank? && h[cv.custom_field.id.to_s].present?
a = Attachment.find_by(id: ids[i])
copy = a.copy
copy.save
cv.value = copy.id
end
i += 1
end
end
# Converts ActionParameters to an ordinary Hash
def self.params_to_hash(params)
result = {}
return result if params.blank?
h = params.permit!.to_hash
h.each do |key, value|
if value.is_a?(Hash)
value = value.except('blank')
_, v = value.first
# We need a symbols here
result[key] = if v&.key?('file') && v['file'].blank?
nil
else
v&.symbolize_keys
end
else
result[key] = value
end
end
result
end
end

View File

@ -0,0 +1,57 @@
# frozen_string_literal: true
# Redmine plugin for Document Management System "Features"
#
# Vít Jonáš <vit.jonas@gmail.com>, Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
# File revision access
class DmsfFileRevisionAccess < ApplicationRecord
belongs_to :dmsf_file_revision
belongs_to :user
delegate :dmsf_file, to: :dmsf_file_revision, allow_nil: false
delegate :project, to: :dmsf_file, allow_nil: false
scope :access_grouped,
lambda {
select('user_id, COUNT(*) AS count, MIN(created_at) AS first_at, MAX(created_at) AS last_at').group('user_id')
}
DOWNLOAD_ACTION = 0
EMAIL_ACTION = 1
acts_as_event(
title: proc { |ra| "#{l(:label_dmsf_downloaded)}: #{ra.dmsf_file.dmsf_path_str}" },
url: proc { |ra| { controller: 'dmsf_files', action: 'show', id: ra.dmsf_file } },
datetime: proc { |ra| ra.updated_at },
description: proc { |ra| ra.dmsf_file_revision.comment },
author: proc { |ra| ra.user }
)
acts_as_activity_provider(
type: 'dmsf_file_revision_accesses',
timestamp: "#{DmsfFileRevisionAccess.table_name}.updated_at",
author_key: "#{DmsfFileRevisionAccess.table_name}.user_id",
permission: :view_dmsf_file_revision_accesses,
scope: proc {
DmsfFileRevisionAccess
.joins(:dmsf_file_revision)
.joins("JOIN #{DmsfFile.table_name} ON dmsf_files.id = dmsf_file_revisions.dmsf_file_id")
.joins("JOIN #{Project.table_name} on dmsf_files.project_id = projects.id")
.where(dmsf_files: { deleted: DmsfFile::STATUS_ACTIVE })
}
)
end

View File

@ -0,0 +1,80 @@
# encode: utf-8
# frozen_string_literal: true
# Redmine plugin for Document Management System "Features"
#
# Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
# File revision access query
class DmsfFileRevisionAccessQuery < Query
attr_accessor :revision_id
self.queried_class = DmsfFileRevisionAccess
self.view_permission = :view_dmsf_files
# Standard columns
self.available_columns = [
QueryColumn.new(:user, frozen: true),
QueryColumn.new(:count, frozen: true),
QueryColumn.new(:first_at, frozen: true),
QueryColumn.new(:last_at, frozen: true)
]
def initialize(attributes = nil, *_args)
super(attributes)
self.sort_criteria = []
self.filters = {}
end
######################################################################################################################
# Inherited
#
def base_scope
@scope ||= DmsfFileRevisionAccess.where(dmsf_file_revision_id: revision_id)
@scope
end
# Returns the issue count
def access_count
base_scope
.group(:user_id)
.count.size
rescue ::ActiveRecord::StatementInvalid => e
raise StatementInvalid, e.message
end
def type
'DmsfFileRevisionAccessQuery'
end
def available_columns
@available_columns ||= self.class.available_columns.dup
@available_columns
end
######################################################################################################################
# New
def accesses(options = {})
base_scope
.access_grouped
.joins(:user)
.order(Arel.sql('COUNT(*) DESC'))
.limit(options[:limit])
.offset(options[:offset])
end
end

View File

@ -0,0 +1,40 @@
# frozen_string_literal: true
# Redmine plugin for Document Management System "Features"
#
# Vít Jonáš <vit.jonas@gmail.com>, Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
# Custom field
class DmsfFileRevisionCustomField < CustomField
def type_name
:menu_dmsf
end
def compare_values?(x_value, y_value)
if x_value.is_a?(Array) && y_value.is_a?(Array)
y_value.reject!(&:empty)
return true if y_value.empty?
x_value.reject!(&:empty?)
y_value.each do |b|
return true if x_value.include?(b)
end
false
else
x_value == y_value
end
end
end

616
app/models/dmsf_folder.rb Normal file
View File

@ -0,0 +1,616 @@
# frozen_string_literal: true
# Redmine plugin for Document Management System "Features"
#
# Vít Jonáš <vit.jonas@gmail.com>, Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
require "#{File.dirname(__FILE__)}/../../lib/redmine_dmsf/lockable"
# Folder
class DmsfFolder < ApplicationRecord
include RedmineDmsf::Lockable
belongs_to :project
belongs_to :dmsf_folder
belongs_to :deleted_by_user, class_name: 'User'
belongs_to :user
has_many :dmsf_folders, -> { order :title }, dependent: :destroy, inverse_of: :dmsf_folder
has_many :dmsf_files, dependent: :destroy
has_many :folder_links, -> { where(target_type: 'DmsfFolder').order(:name) },
class_name: 'DmsfLink', foreign_key: 'dmsf_folder_id', dependent: :destroy, inverse_of: :dmsf_folder
has_many :file_links, -> { where(target_type: 'DmsfFile') },
class_name: 'DmsfLink', foreign_key: 'dmsf_folder_id', dependent: :destroy, inverse_of: :dmsf_folder
has_many :url_links, -> { where(target_type: 'DmsfUrl') },
class_name: 'DmsfLink', foreign_key: 'dmsf_folder_id', dependent: :destroy, inverse_of: :dmsf_folder
has_many :dmsf_links, dependent: :destroy
has_many :referenced_links, -> { where(target_type: 'DmsfFolder') },
class_name: 'DmsfLink', foreign_key: 'target_id', dependent: :destroy, inverse_of: :dmsf_folder
has_many :locks, -> { where(entity_type: 1).order(updated_at: :desc) },
class_name: 'DmsfLock', foreign_key: 'entity_id', dependent: :destroy, inverse_of: :dmsf_folder
has_many :dmsf_folder_permissions, dependent: :destroy
INVALID_CHARACTERS = '\[\]\/\\\?":<>#%\*'
STATUS_DELETED = 1
STATUS_ACTIVE = 0
AVAILABLE_COLUMNS = %w[id title size modified version workflow author description comment].freeze
DEFAULT_COLUMNS = %w[title size modified version workflow author].freeze
def self.visible_condition(system: true)
Project.allowed_to_condition(User.current, :view_dmsf_folders) do |role, user|
if role.member?
group_ids = user.group_ids.join(',')
group_ids = -1 if group_ids.blank?
allowed = system && role.allowed_to?(:display_system_folders) ? 1 : 0
%{
((dfp.object_id IS NULL) OR
(dfp.object_id = #{role.id} AND dfp.object_type = 'Role') OR
((dfp.object_id = #{user.id} OR dfp.object_id IN (#{group_ids})) AND dfp.object_type = 'User')) AND
(#{DmsfFolder.table_name}.system = #{DmsfFolder.connection.quoted_false} OR 1 = #{allowed})
}
end
end
end
scope :visible, (lambda do |*args|
system = args.shift || true
joins(:project)
.joins("LEFT JOIN #{DmsfFolderPermission.table_name} dfp ON #{DmsfFolder.table_name}.id = dfp.dmsf_folder_id")
.where(deleted: STATUS_ACTIVE).where(DmsfFolder.visible_condition(system: system)).distinct
end)
scope :deleted, lambda {
joins(:project)
.joins("LEFT JOIN #{DmsfFolderPermission.table_name} dfp ON #{DmsfFolder.table_name}.id = dfp.dmsf_folder_id")
.where(deleted: STATUS_DELETED).where(DmsfFolder.visible_condition).distinct
}
scope :issystem, -> { where(system: true, deleted: STATUS_ACTIVE) }
acts_as_customizable
acts_as_searchable columns: ["#{table_name}.title", "#{table_name}.description"],
project_key: 'project_id',
date_column: 'updated_at',
permission: :view_dmsf_files,
scope: proc { DmsfFolder.visible }
acts_as_watchable
acts_as_event title: proc { |o| o.title },
description: proc { |o| o.description },
url: proc { |o| { controller: 'dmsf', action: 'show', id: o.project, folder_id: o } },
datetime: proc { |o| o.updated_at },
author: proc { |o| o.user }
validates :title, presence: true, dmsf_file_name: true
validates :title, uniqueness: { scope: %i[dmsf_folder_id project_id deleted],
conditions: -> { where(deleted: STATUS_ACTIVE) }, case_sensitive: true }
validates :description, length: { maximum: 65_535 }
validates :dmsf_folder, dmsf_folder_parent: true, if: proc { |folder| !folder.new_record? }
before_create :default_values
def visible?(_user = User.current)
return DmsfFolder.visible.exists?(id: id) if respond_to?(:type) && /^folder/.match?(type)
true
end
def self.permissions?(folder, allow_system: true, file: false)
# Administrator?
return true if User.current&.admin? || folder.nil?
# Permissions to the project?
# If file is true we work just with the file and not viewing the folder
return false unless file || User.current&.allowed_to?(:view_dmsf_folders, folder.project)
# System folder?
if folder&.system
return false unless allow_system || User.current.allowed_to?(:display_system_folders, folder.project)
return false if (folder.title != '.Issues') && !folder.issue&.visible?(User.current)
end
# Permissions to the folder?
if folder.dmsf_folder_permissions.any?
role_ids = User.current.roles_for_project(folder.project).map(&:id)
role_permission_ids = folder.dmsf_folder_permissions.roles.map(&:object_id)
return true if role_ids.intersect?(role_permission_ids)
principal_ids = folder.dmsf_folder_permissions.users.map(&:object_id)
return true if principal_ids.include?(User.current.id)
user_group_ids = User.current.groups.map(&:id)
principal_ids.intersect? user_group_ids
else
DmsfFolder.permissions? folder.dmsf_folder, allow_system: allow_system, file: file
end
end
def initialize(*args)
super
self.watcher_user_ids = [] if new_record?
end
def default_values
self.notification = true if RedmineDmsf.dmsf_default_notifications? && !system
end
def locked_by
if lock && lock.reverse[0]
user = lock.reverse[0].user
return user == User.current ? l(:label_me) : user.name if user
end
''
end
def delete(commit: false)
if locked?
errors.add :base, l(:error_folder_is_locked)
return false
end
if commit
destroy
else
delete_recursively
end
end
def delete_recursively
self.deleted = STATUS_DELETED
self.deleted_by_user = User.current
save!
dmsf_files.each(&:delete)
dmsf_links.each(&:delete)
dmsf_folders.each(&:delete_recursively)
end
def deleted?
deleted == STATUS_DELETED
end
def restore
if dmsf_folder&.deleted?
errors.add :base, l(:error_parent_folder)
return false
end
restore_recursively
end
def restore_recursively
self.deleted = STATUS_ACTIVE
self.deleted_by_user = nil
save!
dmsf_files.each(&:restore)
dmsf_links.each(&:restore)
dmsf_folders.each(&:restore_recursively)
end
def dmsf_path
folder = self
path = []
while folder
path.unshift folder
folder = folder.dmsf_folder
end
path
end
def dmsf_path_str
path = dmsf_path
string_path = path.map(&:title)
string_path.join '/'
end
def notify?
notification || dmsf_folder&.notify? || (!dmsf_folder && project.dmsf_notification)
end
def get_all_watchers(watchers)
watchers << notified_watchers
watchers << if dmsf_folder
dmsf_folder.notified_watchers
else
project.notified_watchers
end
end
def notify_deactivate
self.notification = nil
save!
end
def notify_activate
self.notification = true
save!
end
def self.directory_tree(project)
tree = [[l(:link_documents), nil]]
project = Project.find(project) unless project.is_a?(Project)
folders = project.dmsf_folders.visible.to_a
folders.delete_if(&:locked_for_user?)
folders.each do |folder|
tree.push ["...#{folder.title}", folder.id]
DmsfFolder.directory_subtree tree, folder, 2
end
tree
end
def folder_tree
tree = [[title, id]]
DmsfFolder.directory_subtree tree, self, 1
tree
end
def self.file_list(files)
options = []
options.push ['', nil, { label: 'none' }]
files.each do |f|
options.push [f.title, f.id]
end
options
end
# Returns an array of projects that current user can copy folder to
def self.allowed_target_projects_on_copy
projects = []
if User.current.admin?
projects = Project.visible.has_module('dmsf').all
elsif User.current.logged?
User.current.memberships.each do |m|
if m.project.module_enabled?('dmsf') &&
m.roles.detect { |r| r.allowed_to?(:folder_manipulation) && r.allowed_to?(:file_manipulation) }
projects << m.project
end
end
end
projects
end
def move_to(target_project, target_folder)
if project != target_project
dmsf_files.visible.find_each do |f|
f.move_to target_project, f.dmsf_folder
end
dmsf_folders.visible.find_each do |s|
s.move_to target_project, s.dmsf_folder
end
dmsf_links.visible.find_each do |l|
l.move_to target_project, l.dmsf_folder
end
self.project = target_project
end
self.dmsf_folder = target_folder
save
end
def copy_to(project, folder, copy_files: true)
new_folder = DmsfFolder.new
new_folder.dmsf_folder = folder
new_folder.project = folder ? folder.project : project
new_folder.title = title
new_folder.description = description
new_folder.user = User.current
new_folder.custom_values = []
new_folder.custom_field_values =
custom_field_values.each_with_object({}) do |v, h|
h[v.custom_field_id] = v.value
end
unless new_folder.save
Rails.logger.error new_folder.errors.full_messages.to_sentence
return new_folder
end
if copy_files
dmsf_files.visible.find_each do |f|
f.copy_to project, new_folder
end
dmsf_links.visible.find_each do |l|
l.copy_to project, new_folder
end
end
dmsf_folders.visible.find_each do |s|
s.copy_to project, new_folder, copy_files: copy_files
end
dmsf_folder_permissions.find_each do |p|
p.copy_to new_folder
end
new_folder
end
# Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
def available_custom_fields
DmsfFileRevisionCustomField.all
end
def custom_values
# We need to response with DmsfFileRevision custom values if DmsfFolder just covers files in the union of the main
# view
if respond_to?(:type) && /^file/.match?(type)
return CustomValue.where(customized_type: 'DmsfFileRevision', customized_id: revision_id)
end
super
end
def modified
last_update = updated_at
time = DmsfFolder.where(['project_id = ? AND dmsf_folder_id = ? AND updated_at > ?', project_id, id, last_update])
.maximum(:updated_at)
last_update = time if time
time = DmsfFile.where(['project_id = ? AND dmsf_folder_id = ? AND updated_at > ?', project_id, id, last_update])
.maximum(:updated_at)
last_update = time if time
time = DmsfLink.where(['project_id = ? AND dmsf_folder_id = ? AND updated_at > ?', project_id, id, last_update])
.maximum(:updated_at)
last_update = time if time
last_update
end
# Number of items in the folder
def items
dmsf_folders.visible.where(project_id: project_id).all.size +
dmsf_files.visible.where(project_id: project_id).all.size +
dmsf_links.visible.where(project_id: project_id).all.size
end
def self.column_on?(column)
RedmineDmsf.dmsf_columns.include? column
end
def custom_value(custom_field)
custom_field_values.each do |cv|
return cv if cv.custom_field == custom_field
end
nil
end
def self.get_column_position(column)
dmsf_columns = RedmineDmsf.dmsf_columns
pos = 0
# 0 - checkbox
# 1 - id
if dmsf_columns.include?('id')
pos += 1
return pos if column == 'id'
elsif column == 'id'
return nil
end
# 2 - title
if dmsf_columns.include?('title')
pos += 1
return pos if column == 'title'
elsif column == 'title'
return nil
end
# 3 - size
if dmsf_columns.include?('size')
pos += 1
return pos if column == 'size'
elsif column == 'size'
return nil
end
# 4 - modified
if dmsf_columns.include?('modified')
pos += 1
return pos if column == 'modified'
elsif column == 'modified'
return nil
end
# 5 - version
if dmsf_columns.include?('version')
pos += 1
return pos if column == 'version'
elsif column == 'version'
return nil
end
# 6 - workflow
if dmsf_columns.include?('workflow')
pos += 1
return pos if column == 'workflow'
elsif column == 'workflow'
return nil
end
# 7 - author
if dmsf_columns.include?('author')
pos += 1
return pos if column == 'author'
elsif column == 'author'
return nil
end
# 9 - custom fields
DmsfFileRevisionCustomField.visible.each do |c|
pos += 1 if DmsfFolder.column_on?(c.name)
end
# 8 - commands
pos += 1
return pos if column == 'commands'
# 9 - (position)
pos += 1
return pos if column == 'position'
# 10 - (size calculated)
pos += 1
return pos if column == 'size_calculated'
# 11 - (modified calculated)
pos += 1
return pos if column == 'modified_calculated'
# 12 - (version calculated)
pos += 1
return pos if column == 'version_calculated'
# 13 - (clear title)
pos += 1
return pos if column == 'clear_title'
nil
end
def issue
if @issue.nil? && system
issue_id = title.to_i
@issue = Issue.find_by(id: issue_id) if issue_id.positive?
end
@issue
end
def update_from_params(params)
# Attributes
self.title = params[:dmsf_folder][:title].scrub.strip
self.description = params[:dmsf_folder][:description].scrub.strip
self.dmsf_folder_id = params[:parent_id].presence || params[:dmsf_folder][:dmsf_folder_id]
self.system = params[:dmsf_folder][:system].present?
# Custom fields
if params[:dmsf_folder][:custom_field_values].present?
i = 0
params[:dmsf_folder][:custom_field_values].each do |param|
custom_field_values[i].value = param[1]
i += 1
end
end
# Permissions
dmsf_folder_permissions.delete_all
if params[:permissions]
params[:permissions][:role_ids]&.each do |role_id|
permission = DmsfFolderPermission.new
permission.object_id = role_id
permission.object_type = Role.model_name.to_s
dmsf_folder_permissions << permission
end
params[:permissions][:user_ids]&.each do |user_id|
permission = DmsfFolderPermission.new
permission.object_id = user_id
permission.object_type = User.model_name.to_s
dmsf_folder_permissions << permission
end
end
# Save
save
end
ALL_INVALID_CHARACTERS = /[#{INVALID_CHARACTERS}]/
def self.get_valid_title(title)
# 1. Invalid characters are replaced with dots.
# 2. Two or more dots in a row are replaced with a single dot.
# 3. Windows' WebClient does not like a dot at the end.
title.scrub.gsub(ALL_INVALID_CHARACTERS, '.').gsub(/\.{2,}/, '.').chomp('.')
end
def permission_for_role(role)
dmsf_folder_permissions.roles.exists? object_id: role.id
end
def permissions_users
Principal.active.where id: dmsf_folder_permissions.users.map(&:object_id)
end
def inherited_permissions_from
return nil unless dmsf_folder
if dmsf_folder.dmsf_folder_permissions.any?
dmsf_folder
else
dmsf_folder.inherited_permissions_from
end
end
def self.visible_folders(folders, project)
allowed = RedmineDmsf.dmsf_act_as_attachable? &&
(project.dmsf_act_as_attachable == Project::ATTACHABLE_DMS_AND_ATTACHMENTS) &&
User.current.allowed_to?(:display_system_folders, project)
folders.reject do |folder|
if folder.system
if allowed
issue_id = folder.title.to_i
if issue_id.positive?
issue = Issue.find_by(id: issue_id)
!issue&.visible? User.current
else
false
end
else
true
end
else
false
end
end
end
def css_classes(trash)
classes = []
if trash
if system
classes << 'dmsf-system'
else
classes << 'hascontextmenu'
classes << 'dmsf-gray' if type.match?(/link$/)
end
else
classes << 'dmsf-tree'
if %w[folder project].include?(type)
classes << 'dmsf-collapsed'
classes << 'dmsf-not-loaded'
else
classes << 'dmsf-child'
end
if system
classes << 'dmsf-system'
else
classes << 'hascontextmenu'
classes << 'dmsf-draggable' if type != 'project'
classes << 'dmsf-droppable' if %w[project folder].include? type
classes << 'dmsf-gray' if type.match?(/link$/)
end
end
classes.join ' '
end
def empty?
!(dmsf_folders.visible.exists? || dmsf_files.visible.exists? || dmsf_links.visible.exists?)
end
def to_s
title
end
# Check whether any child folder is equal to the folder
def any_child?(folder)
dmsf_folders.each do |child|
return true if child == folder
child.any_child? folder
end
false
end
class << self
def directory_subtree(tree, folder, level)
folders = folder.dmsf_folders.visible.to_a
folders.delete_if(&:locked_for_user?)
folders.each do |subfolder|
tree.push ["#{'...' * level}#{subfolder.title}", subfolder.id]
DmsfFolder.directory_subtree tree, subfolder, level + 1
end
end
end
end

View File

@ -0,0 +1,35 @@
# frozen_string_literal: true
# Redmine plugin for Document Management System "Features"
#
# Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
# Folder permission
class DmsfFolderPermission < ApplicationRecord
belongs_to :dmsf_folder
scope :users, -> { where(object_type: User.model_name.to_s) }
scope :roles, -> { where(object_type: Role.model_name.to_s) }
def copy_to(folder)
permission = DmsfFolderPermission.new
permission.dmsf_folder_id = folder.id
permission.object_id = object_id
permission.object_type = object_type
permission.save!
permission
end
end

149
app/models/dmsf_link.rb Normal file
View File

@ -0,0 +1,149 @@
# frozen_string_literal: true
# Redmine plugin for Document Management System "Features"
#
# Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
# Link class
class DmsfLink < ApplicationRecord
include ActiveModel::Validations
belongs_to :project
belongs_to :dmsf_folder
belongs_to :deleted_by_user, class_name: 'User'
belongs_to :user
validates :name, presence: true, length: { maximum: 255 }
# There can be project_id = -1 when attaching links to an issue. The project_id is assigned later when saving the
# issue.
validates :external_url, length: { maximum: 255 }
validates :external_url, dmsf_url: true
STATUS_DELETED = 1
STATUS_ACTIVE = 0
scope :visible, -> { where(deleted: STATUS_ACTIVE) }
scope :deleted, -> { where(deleted: STATUS_DELETED) }
before_destroy :delete_system_folder_before
after_destroy :delete_system_folder_after
def target_folder_id
if target_type == DmsfFolder.model_name.to_s
target_id
else
dmsf_folder_ids = DmsfFile.where(id: target_id).pluck(:dmsf_folder_id)
dmsf_folder_ids.first
end
end
def target_folder
@target_folder = DmsfFolder.find_by(id: target_folder_id) if @target_folder.nil? && target_folder_id
@target_folder
end
def target_file_id
target_id if target_type == DmsfFile.model_name.to_s
end
def target_file
@target_file = DmsfFile.find_by(id: target_file_id) if @target_file.nil? && target_file_id
@target_file
end
def target_project
@target_project ||= Project.find_by(id: target_project_id)
@target_project
end
def title
name
end
def self.find_link_by_file_name(project, folder, filename)
links = DmsfLink.where(project_id: project.id,
dmsf_folder_id: folder&.id,
target_type: DmsfFile.model_name.to_s).visible.all
links.each do |link|
return link if link&.target_file&.name == filename
end
nil
end
def path
path = if target_type == DmsfFile.model_name.to_s && target_file
target_file.dmsf_path.map { |element| element.is_a?(DmsfFile) ? element.display_name : element.title }
.join('/')
else
target_folder ? target_folder.dmsf_path_str : +''
end
path.insert(0, "#{target_project.name}:") if project_id != target_project_id
return "#{path[0, 25]}...#{path[-25, 25]}" if path && path.length > 50
path
end
def move_to(target_project, target_folder)
self.project = target_project
self.dmsf_folder = target_folder
save
end
def copy_to(project, folder)
link = DmsfLink.new
link.target_project_id = target_project_id
link.target_id = target_id
link.target_type = target_type
link.name = name
link.external_url = external_url
link.project_id = project.id
link.dmsf_folder_id = folder&.id
link.user = User.current
link.save!
link
end
def delete(commit: false)
if commit
destroy
else
self.deleted = STATUS_DELETED
self.deleted_by_user = User.current
save validate: false
end
end
def restore
if dmsf_folder_id && (dmsf_folder.nil? || dmsf_folder.deleted?)
errors.add :base, l(:error_parent_folder)
return false
end
self.deleted = STATUS_ACTIVE
self.deleted_by_user = nil
save validate: false
end
def delete_system_folder_before
@parent_folder = dmsf_folder
end
def delete_system_folder_after
return unless @parent_folder&.system && @parent_folder.dmsf_files.empty? && @parent_folder.dmsf_links.empty?
@parent_folder&.destroy
end
end

69
app/models/dmsf_lock.rb Normal file
View File

@ -0,0 +1,69 @@
# frozen_string_literal: true
# Redmine plugin for Document Management System "Features"
#
# Vít Jonáš <vit.jonas@gmail.com>, Daniel Munn <dan.munn@munnster.co.uk>, Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
require 'simple_enum'
# Lock
class DmsfLock < ApplicationRecord
before_create :generate_uuid
belongs_to :dmsf_file, foreign_key: 'entity_id', inverse_of: :locks
belongs_to :dmsf_folder, foreign_key: 'entity_id', inverse_of: :locks
belongs_to :user
# At the moment apparently we're only supporting a write lock?
as_enum :lock_type, %i[type_write type_other]
as_enum :lock_scope, %i[scope_exclusive scope_shared]
# We really loosely bind the value in the belongs_to above
# here we just ensure the data internal to the model is correct
# to ensure everything lists fine - it's the same as a join
# just without running the join in the first place
def dmsf_file
entity_type.zero? ? super : nil
end
# See the file, exact same scenario
def dmsf_folder
entity_type == 1 ? super : nil
end
def expired?
expires_at && (expires_at <= Time.current)
end
def generate_uuid
self.uuid = UUIDTools::UUID.random_create.to_s
end
# Let's allow our UUID to be searchable
def self.find(*args)
if args&.first.is_a?(String) && !args.first.match(/^\d*$/)
lock = find_by_uuid(*args)
raise ActiveRecord::RecordNotFound, "Couldn't find lock with uuid = #{args.first}" if lock.nil?
lock
else
super
end
end
def self.find_by_param(...)
find(...)
end
end

216
app/models/dmsf_mailer.rb Normal file
View File

@ -0,0 +1,216 @@
# frozen_string_literal: true
# Redmine plugin for Document Management System "Features"
#
# Vít Jonáš <vit.jonas@gmail.com>, Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
require 'mailer'
# Mailer
class DmsfMailer < Mailer
layout 'mailer'
def self.deliver_files_updated(project, files)
hash = {}
files.each do |file|
users = get_notify_users(project, file)
users.each do |user|
(hash[user] ||= []) << file
end
end
hash.each do |user, docs|
files_updated(user, project, docs).deliver_later
end
end
def files_updated(user, project, files)
redmine_headers 'Project' => project.identifier if project
@files = files
@project = project
@author = files.first.last_revision.user if files.first.last_revision
@author ||= User.anonymous
message_id project
set_language_if_valid user.language
mail to: user, subject: "[#{@project.name} - #{l(:menu_dmsf)}] #{l(:text_email_doc_updated_subject)}"
end
def self.deliver_files_deleted(project, files)
hash = {}
files.each do |file|
users = get_notify_users(project, file)
users.each do |user|
(hash[user] ||= []) << file
end
end
hash.each do |user, docs|
files_deleted(user, project, docs).deliver_later
end
end
def files_deleted(user, project, files)
redmine_headers 'Project' => project.identifier if project
@files = files
@project = project
@author = files.first.deleted_by_user
@author ||= User.anonymous
message_id project
set_language_if_valid user.language
mail to: user, subject: "[#{@project.name} - #{l(:menu_dmsf)}] #{l(:text_email_doc_deleted_subject)}"
end
def self.deliver_files_downloaded(project, files, remote_ip)
hash = {}
files.each do |file|
users = get_notify_users(project, file)
users.each do |user|
(hash[user] ||= []) << file if user.pref.receive_download_notification == '1'
end
end
hash.each do |user, docs|
files_downloaded(user, project, docs, remote_ip).deliver_later
end
end
def files_downloaded(user, project, files, remote_ip)
redmine_headers 'Project' => project.identifier if project
@files = files
@project = project
@author = User.current
@remote_ip = remote_ip
message_id project
set_language_if_valid user.language
mail to: user, subject: "[#{@project.name} - #{l(:menu_dmsf)}] #{l(:text_email_doc_downloaded_subject)}"
end
def self.deliver_send_documents(project, email_params, author)
send_documents(User.current, project, email_params, author).deliver_now
end
def send_documents(_, project, email_params, author)
redmine_headers 'Project' => project.identifier if project
@body = email_params[:body]
@links_only = email_params[:links_only] == '1'
@public_urls = email_params[:public_urls] == '1'
@expired_at = email_params[:expired_at]
@folders = email_params[:folders]
@files = email_params[:files]
@author = author
unless @links_only
if File.exist?(email_params[:zipped_content])
zipped_content_data = File.binread(email_params[:zipped_content])
attachments['Documents.zip'] = { content_type: 'application/zip', content: zipped_content_data }
else
Rails.logger.error "Cannot attach #{email_params[:zipped_content]}, it doesn't exist."
end
end
skip_no_self_notified = false
begin
# We need to switch off no_self_notified temporarily otherwise the email won't be sent
if (author == User.current) && author.pref.no_self_notified
author.pref.no_self_notified = false
skip_no_self_notified = true
end
res = mail(to: email_params[:to], cc: email_params[:cc], subject: email_params[:subject],
'From' => email_params[:from], 'Reply-To' => email_params[:reply_to])
ensure
author.pref.no_self_notified = true if skip_no_self_notified
end
res
end
def self.deliver_workflow_notification(users, workflow, revision, subject_id, text1_id, text2_id, notice = nil,
step = nil)
step_name = step&.name.present? ? step.name : step&.step
users.each do |user|
workflow_notification(user, workflow, revision, subject_id.to_s, text1_id.to_s, text2_id.to_s, notice,
step_name).deliver_now
end
end
def workflow_notification(user, workflow, revision, subject_id, text1_id, text2_id, notice, step_name)
return unless user && workflow && revision
if revision.dmsf_file&.project
@project = revision.dmsf_file.project
redmine_headers 'Project' => @project.identifier
end
set_language_if_valid user.language
@user = user
message_id workflow
@workflow = workflow
@revision = revision
@text1 = l(text1_id, name: workflow.name, filename: revision.dmsf_file.name, notice: notice, stepname: step_name)
@text2 = l(text2_id)
@notice = notice
@author = revision.dmsf_workflow_assigned_by_user
@author ||= User.anonymous
skip_no_self_notified = false
begin
# We need to switch off no_self_notified temporarily otherwise the email won't be sent
if (@author == user) && @author.pref.no_self_notified
@author.pref.no_self_notified = false
skip_no_self_notified = true
end
mail to: user,
subject:
"[#{@project.name} - #{l(:field_label_dmsf_workflow)}] #{@workflow.name} #{l(subject_id)} #{step_name}"
ensure
@author.pref.no_self_notified = true if skip_no_self_notified
end
end
# force_notification = true => approval workflow's notifications
def self.get_notify_users(project, file, force_notification: false)
return [] unless project.active?
# Notifications
if (force_notification && Setting.notified_events.include?('dmsf_workflow_plural')) ||
(Setting.notified_events.include?('dmsf_legacy_notifications') && file&.notify?)
notify_members = project.members.active.select do |notify_member|
notify_user = notify_member.user
if notify_user == User.current && notify_user.pref.no_self_notified
false
elsif notify_member.dmsf_mail_notification.nil?
case notify_user.mail_notification
when 'all'
true
when 'selected'
notify_member.mail_notification?
when 'only_my_events'
file.involved?(notify_user) || file.assigned?(notify_user)
when 'only_owner'
file.owner? notify_user
when 'only_assigned'
file.assigned? notify_user
else
false
end
else
notify_member.dmsf_mail_notification
end
end
users = notify_members.collect(&:user)
else
users = []
end
# Watchers
watchers = []
file&.get_all_watchers(watchers)
users.concat watchers
users.delete(User.current) if User.current&.pref&.no_self_notified
users.uniq
end
end

View File

@ -0,0 +1,35 @@
# encode: utf-8
# frozen_string_literal: true
# Redmine plugin for Document Management System "Features"
#
# Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
# Public URL
class DmsfPublicUrl < ApplicationRecord
include ActiveModel::Validations
belongs_to :dmsf_file
belongs_to :user
before_save :generate_unique_token
private
def generate_unique_token
self.token ||= SecureRandom.hex(16)
end
end

586
app/models/dmsf_query.rb Normal file
View File

@ -0,0 +1,586 @@
# frozen_string_literal: true
# Redmine plugin for Document Management System "Features"
#
# Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
# Query
class DmsfQuery < Query
attr_accessor :dmsf_folder_id, :deleted, :sub_projects
self.queried_class = DmsfFolder
self.view_permission = :view_dmsf_files
# Standard columns
self.available_columns = [
QueryColumn.new(:id, sortable: 'id', caption: :label_column_id),
DmsfQueryTitleColumn.new(:title, sortable: 'title', frozen: true, caption: :label_column_title),
QueryColumn.new(:size, sortable: 'size', caption: :label_column_size),
DmsfQueryModifiedColumn.new(:modified, sortable: 'updated', caption: :label_column_modified),
DmsfQueryVersionColumn.new(:version,
sortable: %(major_version minor_version patch_version),
caption: :label_column_version),
QueryColumn.new(:workflow, sortable: 'workflow', caption: :label_column_workflow),
QueryColumn.new(:author, sortable: %(firstname lastname), caption: :label_column_author),
QueryColumn.new(:description, sortable: 'description', caption: :label_column_description),
QueryColumn.new(:comment, sortable: 'comment', caption: :label_column_comment)
]
def initialize(attributes = nil, *_args)
super(attributes)
self.sort_criteria = []
self.filters ||= { 'title' => { operator: '~', values: [''] } }
self.dmsf_folder_id = nil
self.deleted = false
self.sub_projects = false
end
######################################################################################################################
# Inherited
#
def available_columns
unless @available_columns
@available_columns = self.class.available_columns.dup
@available_columns += DmsfFileRevisionCustomField.visible.collect do |cf|
QueryCustomFieldColumn.new(cf)
end
end
@available_columns
end
def groupable_columns
# TODO: Implement grouping, then remove this method.
[]
end
def default_columns_names
unless @default_column_names
@default_column_names = []
columns = available_columns
columns&.each do |column|
name = if column.is_a?(QueryCustomFieldColumn)
column.custom_field.attributes['name']
else
column.name.to_s
end
@default_column_names << column.name if DmsfFolder.column_on?(name)
end
end
@default_column_names
end
def default_sort_criteria
[%w[title ASC]]
end
def base_scope
@scope ||= [dmsf_folders_scope, dmsf_folder_links_scope, dmsf_projects_scope, dmsf_files_scope,
dmsf_file_links_scope, dmsf_url_links_scope].compact.inject(:union_all)
@scope
end
# Returns the count of all items
def dmsf_count
# We cannot use this due to the permissions
# base_scope.where(statement).count
dmsf_nodes.size
rescue ::ActiveRecord::StatementInvalid => e
raise StatementInvalid, e.message
end
def initialize_available_filters
add_available_filter 'author', type: :list, values: -> { author_values }
add_available_filter 'title', type: :text
add_available_filter 'updated', type: :date_past
add_available_filter 'locked', type: :list, values: [[l(:general_text_yes), '1'], [l(:general_text_no), '0']]
add_available_filter 'workflow', type: :list, values: [
[l(:title_waiting_for_approval), '1'],
[l(:title_approved), '2'],
[l(:title_assigned), '3'],
[l(:title_rejected), '4'],
[l(:title_obsolete), '5']
]
add_custom_fields_filters DmsfFileRevisionCustomField.visible
end
def statement
unless @statement
filters_clauses = []
filters.each_key do |field|
v = values_for(field).clone
next if v.blank?
operator = operator_for(field)
case field
when 'author'
v.push(User.current.id.to_s) if v.delete('me')
when 'title'
next if (operator == '~') && v.join.empty?
end
if field =~ /cf_(\d+)$/
# custom field
available_filters # Initialize available filters #1380
sql_cf = +sql_for_custom_field(field, operator, v, Regexp.last_match(1))
# This is what we get
# SELECT ct.id FROM dmsf_folders ct LEFT OUTER JOIN custom_values ON custom_values.customized_type='DmsfFolder' AND custom_values.customized_id=ct.id AND custom_values.custom_field_id=78 WHERE dmsf_folders.id = ct.id AND (custom_values.value IN ('A')) AND (1=1))
# This is what we need
# SELECT customized_id FROM custom_values WHERE customized_type=dmsf_folder.customized_type AND custom_values.customized_id=dmsf_folders.customized_id AND custom_field_id=78 AND custom_values.value IN ('A')))
sql_cf.gsub! ' AND (1=1)', ''
sql_cf.gsub!(
"SELECT ct.id FROM dmsf_folders ct LEFT OUTER JOIN custom_values ON custom_values.customized_type='DmsfFolder' AND custom_values.customized_id=ct.id AND custom_values.custom_field_id=",
'SELECT custom_values.customized_id FROM custom_values WHERE custom_values.customized_type=dmsf_folders.customized_type AND custom_values.customized_id=dmsf_folders.customized_id AND custom_values.custom_field_id='
)
sql_cf.gsub! 'WHERE dmsf_folders.id = ct.id AND (', 'AND '
sql_cf.gsub!(/\)$/, '')
filters_clauses << sql_cf
else
filters_clauses << "(#{sql_for_field(field, operator, v, queried_table_name, field)})"
end
end
filters_clauses.compact_blank!
@statement = filters_clauses.any? ? filters_clauses.join(' AND ') : nil
end
@statement
end
def validate_query_filters
# Skip validation for empty title (default filter)
filter = filters.delete('title')
super
# Add it back
filters['title'] = filter if filter
end
def columns
cols = super
# Just move the optional column Id to the beginning as it isn't frozen
id_index = cols.index { |col| col.name == :id }
if id_index == 1
id_col = cols.delete_at(id_index)
cols.insert 0, id_col
end
cols
end
######################################################################################################################
# New
def dmsf_nodes(options = {})
order_option = ['sort', group_by_sort_order, options[:order] || sort_clause&.first].flatten.compact_blank
if order_option.size > 1
DmsfFileRevisionCustomField.visible.pluck(:id).each do |id|
order_option[1].gsub! "cf_#{id}.value", "cf_#{id}"
end
if order_option[1] =~ /^(firstname|major_version),? (lastname|minor_version)( patch_version)? (DESC|ASC)$/
order_option[1] = if Regexp.last_match(3).present?
"#{Regexp.last_match(1)} #{Regexp.last_match(4)}, #{Regexp.last_match(2)}
#{Regexp.last_match(4)}, #{Regexp.last_match(3)} #{Regexp.last_match(4)}"
else
"#{Regexp.last_match(1)} #{Regexp.last_match(4)}, #{Regexp.last_match(2)}
#{Regexp.last_match(4)}"
end
end
end
items = case ActiveRecord::Base.connection.adapter_name.downcase
when /sqlserver/i
# This is just a workaround for #1352.
# limit and offset cause an error in case of MS SQL
base_scope.where(statement).order order_option
else
base_scope.where(statement).order(order_option).limit(options[:limit]).offset options[:offset]
end.to_a
fo = filters_on?
items.delete_if do |item|
case item.type
when 'project'
prj = Project.find_by(id: item.id)
!prj&.dmsf_available?
when 'folder'
dmsf_folder = DmsfFolder.find_by(id: item.id)
!DmsfFolder.permissions?(dmsf_folder, allow_system: false)
when 'file'
if fo
dmsf_file = DmsfFile.find_by(id: item.id)
if dmsf_file.dmsf_folder
!DmsfFolder.permissions?(dmsf_file.dmsf_folder, allow_system: false)
else
!dmsf_file.project.dmsf_available?
end
else
false
end
when /link$/
if fo
dmsf_link = DmsfLink.find_by(id: item.id)
if dmsf_link.dmsf_folder
!dmsf_link.dmsf_folder.visible? || !DmsfFolder.permissions?(dmsf_link.dmsf_folder, allow_system: false)
else
!dmsf_link.project&.dmsf_available?
end
else
false
end
end
end
items
end
def extra_columns
[]
end
def self.default(project = nil, user = User.current)
# User's default
if user&.logged? && (query_id = user.pref.default_dmsf_query).present?
query = find_by(id: query_id)
return query if query&.visible?
end
# Project's default
project = project[:project] if project.is_a?(Hash)
query = project&.default_dmsf_query
return query if query&.visibility == VISIBILITY_PUBLIC
# Global default
query = find_by(id: RedmineDmsf.dmsf_default_query)
return query if query&.visibility == VISIBILITY_PUBLIC
nil
end
private
def filters_on?
filters.each_key do |field|
return true if values_for(field).any?(&:present?)
end
false
end
def sub_query
case ActiveRecord::Base.connection.adapter_name.downcase
when /sqlserver/i
'dmsf_file_revisions.id = (SELECT TOP 1 r.id FROM dmsf_file_revisions r
WHERE r.created_at = (SELECT MAX(created_at) FROM dmsf_file_revisions rr WHERE rr.dmsf_file_id = dmsf_files.id)
AND r.dmsf_file_id = dmsf_files.id ORDER BY id DESC)'
else
'dmsf_file_revisions.id = (SELECT r.id FROM dmsf_file_revisions r WHERE r.created_at = (SELECT MAX(created_at)
FROM dmsf_file_revisions rr WHERE rr.dmsf_file_id = dmsf_files.id) AND r.dmsf_file_id = dmsf_files.id ORDER BY id
DESC LIMIT 1)'
end
end
def integer_type
if Redmine::Database.mysql?
ActiveRecord::Base.connection.type_to_sql(:signed)
else
ActiveRecord::Base.connection.type_to_sql(:integer)
end
end
def now
case ActiveRecord::Base.connection.adapter_name.downcase
when /sqlserver/i
'GETDATE()'
when /sqlite/i
"datetime('now')"
else
'NOW()'
end
end
def get_cf_query(id, type, table)
aggr_func = Redmine::Database.mysql? || Redmine::Database.sqlite? ? 'GROUP_CONCAT(value)' : "STRING_AGG(value, ',')"
",(SELECT #{aggr_func} FROM custom_values WHERE custom_field_id = #{id} AND customized_type = '#{type}' AND
customized_id = #{table}.id GROUP BY custom_field_id) AS cf_#{id}"
end
def dmsf_projects_scope
return nil unless sub_projects
cf_columns = +''
DmsfFileRevisionCustomField.visible.order(:position).pluck(:id).each do |id|
cf_columns << ",NULL AS cf_#{id}"
end
scope = Project.select(%{
projects.id AS id,
projects.id AS project_id,
CAST(NULL AS #{integer_type}) AS revision_id,
projects.name AS title,
projects.identifier AS filename,
CAST(NULL AS #{integer_type}) AS size,
projects.updated_on AS updated,
CAST(NULL AS #{integer_type}) AS major_version,
CAST(NULL AS #{integer_type}) AS minor_version,
CAST(NULL AS #{integer_type}) AS patch_version,
CAST(NULL AS #{integer_type}) AS workflow,
CAST(NULL AS #{integer_type}) AS workflow_id,
'' AS firstname,
'' AS lastname,
CAST(NULL AS #{integer_type}) AS author,
'project' AS type,
CAST(0 AS #{integer_type}) AS deleted,
'' AS customized_type,
0 AS customized_id,
projects.description AS description,
'' AS comment,
0 AS locked,
0 AS "system",
0 AS sort#{cf_columns}}).visible
if dmsf_folder_id || deleted
scope.none
else
scope = scope.non_templates if scope.respond_to?(:non_templates)
if project.nil? && filters_on?
scope
else
scope.where projects: { parent_id: project&.id }
end
end
end
def dmsf_folders_scope
cf_columns = +''
DmsfFileRevisionCustomField.visible.order(:position).pluck(:id).each do |id|
cf_columns << get_cf_query(id, 'DmsfFolder', 'dmsf_folders')
end
scope = DmsfFolder.select(%{
dmsf_folders.id AS id,
dmsf_folders.project_id AS project_id,
CAST(NULL AS #{integer_type}) AS revision_id,
dmsf_folders.title AS title,
NULL AS filename,
CAST(NULL AS #{integer_type}) AS size,
dmsf_folders.updated_at AS updated,
CAST(NULL AS #{integer_type}) AS major_version,
CAST(NULL AS #{integer_type}) AS minor_version,
CAST(NULL AS #{integer_type}) AS patch_version,
CAST(NULL AS #{integer_type}) AS workflow,
CAST(NULL AS #{integer_type}) AS workflow_id,
users.firstname AS firstname,
users.lastname AS lastname,
users.id AS author,
'folder' AS type,
dmsf_folders.deleted AS deleted,
'DmsfFolder' AS customized_type,
dmsf_folders.id AS customized_id,
dmsf_folders.description AS description,
'' AS comment,
(CASE WHEN dmsf_locks.id IS NULL THEN 0 ELSE 1 END) AS locked,
(CASE WHEN (dmsf_folders.system = #{ActiveRecord::Base.connection.quoted_true}) THEN 1 ELSE 0 END) AS "system",
1 AS sort#{cf_columns}})
.joins('LEFT JOIN users ON dmsf_folders.user_id = users.id')
.joins("LEFT JOIN dmsf_locks ON dmsf_folders.id = dmsf_locks.entity_id AND
dmsf_locks.entity_type = 1 AND (dmsf_locks.expires_at IS NULL
OR dmsf_locks.expires_at > #{now})")
scope = deleted ? scope.deleted : scope.visible
if dmsf_folder_id
scope.where dmsf_folders: { dmsf_folder_id: dmsf_folder_id }
elsif project.nil? && filters_on?
scope
elsif statement.present? || deleted
scope.where dmsf_folders: { project_id: project&.id }
else
scope.where dmsf_folders: { project_id: project&.id, dmsf_folder_id: nil }
end
end
def dmsf_folder_links_scope
cf_columns = +''
DmsfFileRevisionCustomField.visible.order(:position).pluck(:id).each do |id|
cf_columns << get_cf_query(id, 'DmsfFolder', 'dmsf_folders')
end
scope = DmsfLink.select(%{
dmsf_links.id AS id,
dmsf_links.target_project_id AS project_id,
dmsf_links.target_id AS revision_id,
dmsf_links.name AS title,
dmsf_folders.title AS filename,
CAST(NULL AS #{integer_type}) AS size,
COALESCE(dmsf_folders.updated_at, dmsf_links.updated_at) AS updated,
CAST(NULL AS #{integer_type}) AS major_version,
CAST(NULL AS #{integer_type}) AS minor_version,
CAST(NULL AS #{integer_type}) AS patch_version,
CAST(NULL AS #{integer_type}) AS workflow,
CAST(NULL AS #{integer_type}) AS workflow_id,
users.firstname AS firstname,
users.lastname AS lastname,
users.id AS author,
'folder-link' AS type,
dmsf_links.deleted AS deleted,
'DmsfFolder' AS customized_type,
dmsf_folders.id AS customized_id,
dmsf_folders.description AS description,
'' AS comment,
(CASE WHEN dmsf_locks.id IS NULL THEN 0 ELSE 1 END) AS locked,
0 AS "system",
1 AS sort#{cf_columns}})
.joins('LEFT JOIN dmsf_folders ON dmsf_links.target_id = dmsf_folders.id')
.joins('LEFT JOIN users ON users.id = COALESCE(dmsf_folders.user_id, dmsf_links.user_id)')
.joins("LEFT JOIN dmsf_locks ON dmsf_folders.id = dmsf_locks.entity_id AND
dmsf_locks.entity_type = 1 AND (dmsf_locks.expires_at IS NULL OR
dmsf_locks.expires_at > #{now})")
scope = deleted ? scope.deleted : scope.visible
if dmsf_folder_id
scope.where dmsf_links: { target_type: 'DmsfFolder', dmsf_folder_id: dmsf_folder_id }
elsif project.nil? && filters_on?
scope
elsif statement.present? || deleted
scope.where dmsf_links: { target_type: 'DmsfFolder', project_id: project&.id }
else
scope.where dmsf_links: { target_type: 'DmsfFolder', project_id: project&.id, dmsf_folder_id: nil }
end
end
def dmsf_files_scope
cf_columns = +''
DmsfFileRevisionCustomField.visible.order(:position).pluck(:id).each do |id|
cf_columns << get_cf_query(id, 'DmsfFileRevision', 'dmsf_file_revisions')
end
scope = DmsfFile.select(%{
dmsf_files.id AS id,
dmsf_files.project_id AS project_id,
dmsf_file_revisions.id AS revision_id,
dmsf_file_revisions.title AS title,
dmsf_file_revisions.name AS filename,
dmsf_file_revisions.size AS size,
dmsf_file_revisions.updated_at AS updated,
dmsf_file_revisions.major_version AS major_version,
dmsf_file_revisions.minor_version AS minor_version,
dmsf_file_revisions.patch_version AS patch_version,
dmsf_file_revisions.workflow AS workflow,
dmsf_file_revisions.dmsf_workflow_id AS workflow_id,
users.firstname AS firstname,
users.lastname AS lastname,
users.id AS author,
'file' AS type,
dmsf_files.deleted AS deleted,
'DmsfFileRevision' AS customized_type,
dmsf_file_revisions.id AS customized_id,
dmsf_file_revisions.description AS description,
dmsf_file_revisions.comment AS comment,
(CASE WHEN dmsf_locks.id IS NULL THEN 0 ELSE 1 END) AS locked,
0 AS "system",
2 AS sort#{cf_columns}})
.joins(:dmsf_file_revisions)
.joins('LEFT JOIN users ON dmsf_file_revisions.user_id = users.id ')
.joins("LEFT JOIN dmsf_locks ON dmsf_files.id = dmsf_locks.entity_id AND dmsf_locks.entity_type = 0
AND (dmsf_locks.expires_at IS NULL OR dmsf_locks.expires_at > #{now})")
.where(sub_query)
scope = deleted ? scope.deleted : scope.visible
if dmsf_folder_id
scope.where dmsf_files: { dmsf_folder_id: dmsf_folder_id }
elsif project.nil? && filters_on?
scope
elsif statement.present? || deleted
scope.where dmsf_files: { project_id: project&.id }
else
scope.where(dmsf_files: { project_id: project&.id, dmsf_folder_id: nil })
end
end
def dmsf_file_links_scope
cf_columns = +''
DmsfFileRevisionCustomField.visible.order(:position).pluck(:id).each do |id|
cf_columns << get_cf_query(id, 'DmsfFileRevision', 'dmsf_file_revisions')
end
scope = DmsfLink.select(%{
dmsf_links.id AS id,
dmsf_files.project_id AS project_id,
dmsf_files.id AS revision_id,
dmsf_links.name AS title,
dmsf_file_revisions.name AS filename,
dmsf_file_revisions.size AS size,
dmsf_file_revisions.updated_at AS updated,
dmsf_file_revisions.major_version AS major_version,
dmsf_file_revisions.minor_version AS minor_version,
dmsf_file_revisions.patch_version AS patch_version,
dmsf_file_revisions.workflow AS workflow,
dmsf_file_revisions.dmsf_workflow_id AS workflow_id,
users.firstname AS firstname,
users.lastname AS lastname,
users.id AS author,
'file-link' AS type,
dmsf_links.deleted AS deleted,
'DmsfFileRevision' AS customized_type,
dmsf_file_revisions.id AS customized_id,
dmsf_file_revisions.description AS description,
dmsf_file_revisions.comment AS comment,
(CASE WHEN dmsf_locks.id IS NULL THEN 0 ELSE 1 END) AS locked,
0 AS "system",
2 AS sort#{cf_columns}})
.joins('JOIN dmsf_files ON dmsf_files.id = dmsf_links.target_id')
.joins('JOIN dmsf_file_revisions ON dmsf_file_revisions.dmsf_file_id = dmsf_files.id')
.joins('LEFT JOIN users ON dmsf_file_revisions.user_id = users.id ')
.joins("LEFT JOIN dmsf_locks ON dmsf_files.id = dmsf_locks.entity_id AND dmsf_locks.entity_type = 0
AND (dmsf_locks.expires_at IS NULL OR dmsf_locks.expires_at > #{now})")
.where(sub_query)
scope = deleted ? scope.deleted : scope.visible
if dmsf_folder_id
scope.where dmsf_links: { target_type: 'DmsfFile', dmsf_folder_id: dmsf_folder_id }
elsif project.nil? && filters_on?
scope
elsif statement.present? || deleted
scope.where dmsf_links: { target_type: 'DmsfFile', project_id: project&.id }
else
scope.where dmsf_links: { target_type: 'DmsfFile', project_id: project&.id, dmsf_folder_id: nil }
end
end
def dmsf_url_links_scope
cf_columns = +''
DmsfFileRevisionCustomField.visible.order(:position).pluck(:id).each do |id|
cf_columns << ",NULL AS cf_#{id}"
end
scope = DmsfLink.select(%{
dmsf_links.id AS id,
dmsf_links.project_id AS project_id,
CAST(NULL AS #{integer_type}) AS revision_id,
dmsf_links.name AS title,
dmsf_links.external_url AS filename,
CAST(NULL AS #{integer_type}) AS size,
dmsf_links.updated_at AS updated,
CAST(NULL AS #{integer_type}) AS major_version,
CAST(NULL AS #{integer_type}) AS minor_version,
CAST(NULL AS #{integer_type}) AS patch_version,
CAST(NULL AS #{integer_type}) AS workflow,
CAST(NULL AS #{integer_type}) AS workflow_id,
users.firstname AS firstname,
users.lastname AS lastname,
users.id AS author,
'url-link' AS type,
dmsf_links.deleted AS deleted,
'' AS customized_type,
0 AS customized_id,
'' AS description,
'' AS comment,
0 AS locked,
0 AS "system",
2 AS sort#{cf_columns}})
.joins('LEFT JOIN users ON dmsf_links.user_id = users.id ')
scope = deleted ? scope.deleted : scope.visible
if dmsf_folder_id
scope.where dmsf_links: { target_type: 'DmsfUrl', dmsf_folder_id: dmsf_folder_id }
elsif project.nil? && filters_on?
scope
elsif statement.present? || deleted
scope.where dmsf_links: { target_type: 'DmsfUrl', project_id: project&.id }
else
scope.where dmsf_links: { target_type: 'DmsfUrl', project_id: project&.id, dmsf_folder_id: nil }
end
end
end

View File

@ -0,0 +1,27 @@
# frozen_string_literal: true
# Redmine plugin for Document Management System "Features"
#
# Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
require 'query'
# Modify column
class DmsfQueryModifiedColumn < QueryColumn
def value_object(object)
object.updated
end
end

View File

@ -0,0 +1,27 @@
# frozen_string_literal: true
# Redmine plugin for Document Management System "Features"
#
# Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
require 'query'
# Title column
class DmsfQueryTitleColumn < QueryColumn
def css_classes
'dmsf-title'
end
end

View File

@ -0,0 +1,27 @@
# frozen_string_literal: true
# Redmine plugin for Document Management System "Features"
#
# Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
require 'query'
# Version column
class DmsfQueryVersionColumn < QueryColumn
def value_object(object)
DmsfFileRevision.version object.major_version, object.minor_version, object.patch_version
end
end

133
app/models/dmsf_upload.rb Normal file
View File

@ -0,0 +1,133 @@
# frozen_string_literal: true
# Redmine plugin for Document Management System "Features"
#
# Vít Jonáš <vit.jonas@gmail.com>, Daniel Munn <dan.munn@munnster.co.uk>, Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
# Upload
class DmsfUpload
attr_accessor :name, :disk_filename, :mime_type, :title, :description, :comment, :major_version, :minor_version,
:patch_version, :locked, :workflow, :custom_values, :tempfile_path, :digest, :token
attr_reader :size
def disk_file
Rails.root.join 'tmp', disk_filename
end
def self.create_from_uploaded_attachment(project, folder, uploaded_file)
a = Attachment.find_by_token(uploaded_file[:token]) if uploaded_file[:token].present?
if a
uploaded = {
disk_filename: DmsfHelper.temp_filename(a.filename),
content_type: a.content_type,
original_filename: a.filename,
comment: uploaded_file[:description],
tempfile_path: a.diskfile,
token: uploaded_file[:token],
digest: a.digest
}
DmsfUpload.new project, folder, uploaded
else
Rails.logger.error "An attachment not found by its token: #{uploaded_file[:token]}"
nil
end
end
def initialize(project, folder = nil, uploaded = nil)
unless uploaded
@name = ''
@disk_filename = ''
@mime_type = ''
@size = 0
@tempfile_path = ''
@token = ''
@digest = ''
if RedmineDmsf.empty_minor_version_by_default?
@major_version = 1
@minor_version = nil
else
@major_version = 0
@minor_version = 0
end
@patch_version = nil
@workflow = nil
revision = DmsfFileRevision.new
@custom_values = revision.custom_field_values
return
end
@name = uploaded[:original_filename]
file = DmsfFile.find_file_by_name(project, folder, @name)
unless file
link = DmsfLink.find_link_by_file_name(project, folder, @name)
file = link.target_file if link
end
@disk_filename = uploaded[:disk_filename]
@mime_type = uploaded[:content_type]
@size = File.size?(uploaded[:tempfile_path])
unless @size
@size = 0
Rails.logger.error "Cannot find #{uploaded[:tempfile_path]}"
end
@tempfile_path = uploaded[:tempfile_path]
@token = uploaded[:token]
@digest = uploaded[:digest]
if file.nil? || file.last_revision.nil?
@title = DmsfFileRevision.filename_to_title(@name)
@description = uploaded[:comment]
if RedmineDmsf.empty_minor_version_by_default?
@major_version = 1
@minor_version = nil
else
@major_version = 0
@minor_version = 0
end
@patch_version = nil
@workflow = nil
file = DmsfFile.new
file.project_id = project.id
revision = DmsfFileRevision.new
revision.dmsf_file = file
@custom_values = revision.custom_field_values
else
last_revision = file.last_revision
@title = last_revision.title
if last_revision.description.present?
@description = last_revision.description
@comment = uploaded[:comment] if uploaded[:comment].present?
elsif uploaded[:comment].present?
@comment = uploaded[:comment]
end
@major_version = last_revision.major_version
@minor_version = last_revision.minor_version
@patch_version = last_revision.patch_version
@workflow = last_revision.workflow
@custom_values = Array.new(file.last_revision.custom_values)
# Add default value for CFs not existing
present_custom_fields = file.last_revision.custom_values.collect(&:custom_field).uniq
file.last_revision.available_custom_fields.each do |cf|
if cf.default_value && present_custom_fields.exclude?(cf)
@custom_values << CustomValue.new({ custom_field: cf, value: cf.default_value })
end
end
end
@locked = file&.locked_for_user?
end
end

259
app/models/dmsf_workflow.rb Normal file
View File

@ -0,0 +1,259 @@
# frozen_string_literal: true
# Redmine plugin for Document Management System "Features"
#
# Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
# Workflow
class DmsfWorkflow < ApplicationRecord
has_many :dmsf_workflow_steps, -> { order(step: :asc) }, dependent: :destroy, inverse_of: :dmsf_workflow
belongs_to :author, class_name: 'User'
scope :sorted, -> { order name: :asc }
scope :global, -> { where project_id: nil }
scope :active, -> { where status: STATUS_ACTIVE }
scope :status, ->(arg) { where arg.blank? ? nil : { status: arg.to_i } }
validates :name, presence: true, length: { maximum: 255 }, dmsf_workflow_name: true
STATE_ASSIGNED = 3
STATE_WAITING_FOR_APPROVAL = 1
STATE_APPROVED = 2
STATE_REJECTED = 4
STATE_OBSOLETE = 5
STATUS_LOCKED = 0
STATUS_ACTIVE = 1
def self.workflow_str(workflow)
case workflow
when STATE_WAITING_FOR_APPROVAL
l(:title_waiting_for_approval)
when STATE_APPROVED
l(:title_approved)
when STATE_ASSIGNED
l(:title_assigned)
when STATE_REJECTED
l(:title_rejected)
when DmsfWorkflow::STATE_OBSOLETE
l(:title_obsolete)
end
end
def self.workflow_info(workflow, workflow_id, revision_id)
text = ''
names = ''
if workflow.to_i == STATE_WAITING_FOR_APPROVAL
dmsf_workflow = DmsfWorkflow.find_by(id: workflow_id)
if dmsf_workflow
assignments = dmsf_workflow.next_assignments(revision_id)
if assignments.any?
user_ids = assignments.map(&:user_id)
names = User.where(id: user_ids).all.map(&:name).join(',')
workflow_step_id = assignments.first[:dmsf_workflow_step_id]
if workflow_step_id
step = DmsfWorkflowStep.find_by(id: workflow_step_id)
text = step.name if step&.name.present?
end
end
end
end
text = DmsfWorkflow.workflow_str(workflow.to_i) if text.blank?
[text, names]
end
def participiants
users = []
dmsf_workflow_steps.each do |step|
users << step.user unless users.include? step.user
end
users
end
def self.workflows(project)
where(project_id: project)
end
def project
Project.find_by(id: project_id) if project_id
end
def to_s
name
end
def reorder_steps(step, move_to)
DmsfWorkflow.transaction do
dmsf_workflow_steps.each do |ws|
if ws.step == step
ws.update_attribute :step, move_to
elsif ws.step >= move_to && ws.step < step
# Move up
ws.update_attribute :step, ws.step + 1
elsif ws.step <= move_to && ws.step > step
# Move down
ws.update_attribute :step, ws.step - 1
end
end
end
end
def delegates(query, dmsf_workflow_step_assignment_id, dmsf_file_revision_id)
sql = if dmsf_workflow_step_assignment_id && dmsf_file_revision_id
[
%(id NOT IN (SELECT a.user_id FROM dmsf_workflow_step_assignments a WHERE id = ?)
AND id IN (SELECT m.user_id FROM members m JOIN dmsf_files f ON f.project_id = m.project_id
JOIN dmsf_file_revisions r ON r.dmsf_file_id = f.id WHERE r.id = ?)),
dmsf_workflow_step_assignment_id, dmsf_file_revision_id
]
elsif project
['id IN (SELECT user_id FROM members WHERE project_id = ?)', project.id]
else
'1=1'
end
if query.present?
User.active.sorted.where(sql).like query
else
User.active.sorted.where sql
end
end
def next_assignments(dmsf_file_revision_id)
results = []
nsteps = dmsf_workflow_steps.collect(&:step).uniq
nsteps.each do |i|
step_is_finished = false
steps = dmsf_workflow_steps.filter_map { |s| s.step == i ? s : nil }
steps.each do |step|
step.dmsf_workflow_step_assignments.where(dmsf_file_revision_id: dmsf_file_revision_id)
.find_each do |assignment|
assignment.dmsf_workflow_step_actions.find_each do |action|
case action.action
when DmsfWorkflowStepAction::ACTION_APPROVE
step_is_finished = true
# Try to find another unfinished AND step
exists = false
stps = dmsf_workflow_steps
.filter_map { |s| s.step == i && s.operator == DmsfWorkflowStep::OPERATOR_AND ? s : nil }
stps.each do |s|
s.dmsf_workflow_step_assignments.where(dmsf_file_revision_id: dmsf_file_revision_id).find_each do |a|
exists = a.add?(dmsf_file_revision_id)
break if exists
end
break if exists
end
step_is_finished = false if exists
break
when DmsfWorkflowStepAction::ACTION_REJECT
return []
end
end
break if step_is_finished
end
break if step_is_finished
end
next if step_is_finished
steps.each do |step|
step.dmsf_workflow_step_assignments.where(dmsf_file_revision_id: dmsf_file_revision_id)
.find_each do |assignment|
results << assignment if assignment.add?(dmsf_file_revision_id)
end
end
return results
end
results
end
def assign(dmsf_file_revision_id)
dmsf_workflow_steps.each do |ws|
ws.assign(dmsf_file_revision_id)
end
end
def try_finish?(revision, action, user_id)
case action.action
when DmsfWorkflowStepAction::ACTION_APPROVE
assignments = next_assignments(revision.id)
return false unless assignments.empty?
revision.workflow = DmsfWorkflow::STATE_APPROVED
revision.save!
return true
when DmsfWorkflowStepAction::ACTION_REJECT
revision.workflow = DmsfWorkflow::STATE_REJECTED
revision.save!
return true
when DmsfWorkflowStepAction::ACTION_DELEGATE
dmsf_workflow_steps.each do |step|
step.dmsf_workflow_step_assignments.each do |assignment|
next unless assignment.id == action.dmsf_workflow_step_assignment_id
assignment.user_id = user_id
assignment.save!
return false
end
end
end
false
end
def copy_to(project, name = nil)
new_wf = dup
new_wf.name = name if name
new_wf.project_id = project&.id
new_wf.author = User.current
if new_wf.save
dmsf_workflow_steps.each do |step|
step.copy_to(new_wf)
end
end
new_wf
end
def locked?
status == STATUS_LOCKED
end
def active?
status == STATUS_ACTIVE
end
def notify_users(project, revision, controller)
assignments = next_assignments(revision.id)
recipients = assignments.collect(&:user).uniq
recipients &= DmsfMailer.get_notify_users(project, revision.dmsf_file, force_notification: true)
DmsfMailer.deliver_workflow_notification(
recipients,
self,
revision,
:text_email_subject_started,
:text_email_started,
:text_email_to_proceed,
nil,
assignments.first&.dmsf_workflow_step
)
return unless RedmineDmsf.dmsf_display_notified_recipients? && controller && recipients.present?
max_recipients = RedmineDmsf.dmsf_max_notification_receivers_info
to = recipients.collect(&:name).first(max_recipients).join(', ')
return if to.blank?
to << (recipients.count > max_recipients.to_i ? ',...' : '.')
controller.flash[:warning] = l(:warning_email_notifications, to: to) if controller
end
end

View File

@ -0,0 +1,69 @@
# frozen_string_literal: true
# Redmine plugin for Document Management System "Features"
#
# Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
# Workflow step
class DmsfWorkflowStep < ApplicationRecord
belongs_to :dmsf_workflow
belongs_to :user
has_many :dmsf_workflow_step_assignments, dependent: :destroy
validates :step, presence: true
validates :operator, presence: true
validates :user_id, uniqueness: { scope: %i[dmsf_workflow_id step], case_sensitive: true }
validates :name, length: { maximum: 30 }
OPERATOR_OR = 0
OPERATOR_AND = 1
def soperator
DmsfWorkflowStep.soperator(operator)
end
def self.soperator(operator)
operator == 1 ? l(:dmsf_and) : l(:dmsf_or)
end
def assign(dmsf_file_revision_id)
step_assignment = DmsfWorkflowStepAssignment.new
step_assignment.dmsf_workflow_step_id = id
step_assignment.user_id = user_id
step_assignment.dmsf_file_revision_id = dmsf_file_revision_id
step_assignment.save!
end
def finished?(dmsf_file_revision_id)
dmsf_workflow_step_assignments.each do |assignment|
next if assignment.dmsf_file_revision_id == dmsf_file_revision_id
return false if assignment.dmsf_workflow_step_actions.empty?
assignment.dmsf_workflow_step_actions.each do |act|
return false unless act.finished?
end
end
end
def copy_to(workflow)
new_step = dup
new_step.dmsf_workflow_id = workflow.id
new_step.save!
new_step
end
end

View File

@ -0,0 +1,98 @@
# frozen_string_literal: true
# Redmine plugin for Document Management System "Features"
#
# Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
# Workflow step action
class DmsfWorkflowStepAction < ApplicationRecord
belongs_to :dmsf_workflow_step_assignment
belongs_to :author, class_name: 'User'
validates :action, presence: true
validates :note, presence: true, unless: -> { action == DmsfWorkflowStepAction::ACTION_APPROVE }
validates :dmsf_workflow_step_assignment_id,
unless: -> { action == DmsfWorkflowStepAction::ACTION_DELEGATE },
uniqueness: { scope: [:action], case_sensitive: true }
ACTION_APPROVE = 1
ACTION_REJECT = 2
ACTION_DELEGATE = 3
ACTION_ASSIGN = 4
ACTION_START = 5
def initialize(*args)
super
self.author_id = User.current.id if User.current
end
def self.finished?(action)
[DmsfWorkflowStepAction::ACTION_APPROVE, DmsfWorkflowStepAction::ACTION_REJECT].include? action
end
def finished?
DmsfWorkflowStepAction.finished? action
end
def self.action_str(action)
return unless action
case action.to_i
when ACTION_APPROVE
l(:title_approval)
when ACTION_REJECT
l(:title_rejection)
when ACTION_DELEGATE
l(:title_delegation)
when ACTION_ASSIGN
l(:title_assignment)
when ACTION_START
l(:title_start)
end
end
def self.action_type_str(action)
return unless action
case action.to_i
when ACTION_APPROVE
'approval'
when ACTION_REJECT
'rejection'
when ACTION_DELEGATE
'delegation'
when ACTION_ASSIGN
'assignment'
when ACTION_START
'start'
end
end
def self.workflow_str(action)
return unless action
case action.to_i
when ACTION_REJECT
l(:title_rejected)
when ACTION_ASSIGN
l(:title_assigned)
when ACTION_START, ACTION_DELEGATE, ACTION_APPROVE
l(:title_waiting_for_approval)
else
l(:title_none)
end
end
end

View File

@ -0,0 +1,43 @@
# frozen_string_literal: true
# Redmine plugin for Document Management System "Features"
#
# Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
# Workflow step assignment
class DmsfWorkflowStepAssignment < ApplicationRecord
belongs_to :dmsf_workflow_step
belongs_to :user
belongs_to :dmsf_file_revision
has_many :dmsf_workflow_step_actions, dependent: :destroy
validates :dmsf_workflow_step_id, uniqueness: { scope: [:dmsf_file_revision_id], case_sensitive: true }
def add?(dmsf_file_revision_id)
if dmsf_file_revision_id == self.dmsf_file_revision_id
add = true
dmsf_workflow_step_actions.each do |action|
if action.finished?
add = false
break
end
end
return add
end
false
end
end

View File

@ -0,0 +1,32 @@
# frozen_string_literal: true
# Redmine plugin for Document Management System "Features"
#
# Vít Jonáš <vit.jonas@gmail.com>, Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
# File extension validator according to the Redmine whitelist and blacklist for file upload.
class DmsfFileExtensionValidator < ActiveModel::EachValidator
include Redmine::I18n
def validate_each(record, attribute, value)
return unless attribute.to_s == 'name'
extension = File.extname(value)
return if Attachment.valid_extension?(extension)
record.errors.add(:base, l(:error_attachment_extension_not_allowed, extension: extension))
end
end

View File

@ -0,0 +1,27 @@
# frozen_string_literal: true
# Redmine plugin for Document Management System "Features"
#
# Vít Jonáš <vit.jonas@gmail.com>, Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
# File name validator
class DmsfFileNameValidator < ActiveModel::EachValidator
ALL_INVALID_CHARACTERS = /\A[^#{DmsfFolder::INVALID_CHARACTERS}]*\z/
def validate_each(record, attribute, value)
record.errors.add attribute, :error_contains_invalid_character unless ALL_INVALID_CHARACTERS.match?(value)
end
end

View File

@ -0,0 +1,33 @@
# frozen_string_literal: true
# Redmine plugin for Document Management System "Features"
#
# Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
# Folder parent validator
class DmsfFolderParentValidator < ActiveModel::EachValidator
# Don't allow save folders with a parent pointing to the same folder
def validate_each(record, attribute, value)
folder = value
while folder
if folder == record
record.errors.add attribute, :invalid
return
end
folder = folder.dmsf_folder
end
end
end

View File

@ -0,0 +1,32 @@
# frozen_string_literal: true
# Redmine plugin for Document Management System "Features"
#
# Vít Jonáš <vit.jonas@gmail.com>, Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
# Max file size validator
class DmsfMaxFileSizeValidator < ActiveModel::EachValidator
include Redmine::I18n
def validate_each(record, attribute, value)
return unless value && (value > Setting.attachment_max_size.to_i.kilobytes)
record.errors.add attribute,
l(:error_attachment_too_big, max_size: ActiveSupport::NumberHelper.number_to_human_size(
Setting.attachment_max_size.to_i.kilobytes
))
end
end

View File

@ -0,0 +1,37 @@
# frozen_string_literal: true
# Redmine plugin for Document Management System "Features"
#
# Vít Jonáš <vit.jonas@gmail.com>, Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
# URL validator
class DmsfUrlValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
return unless record.target_type == 'DmsfUrl'
begin
if value.present?
# https://www.google.com/search?q=寿司
URI.parse value.split('?').first
else
record.errors.add attribute, :invalid
end
rescue URI::InvalidURIError => e
record.errors.add attribute, :invalid
Rails.logger.error e.message
end
end
end

View File

@ -0,0 +1,39 @@
# frozen_string_literal: true
# Redmine plugin for Document Management System "Features"
#
# Vít Jonáš <vit.jonas@gmail.com>, Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
# Workflow name validator
class DmsfWorkflowNameValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
if record.project_id
if record.id
if DmsfWorkflow.exists?(['(project_id IS NULL OR (project_id = ? AND id != ?)) AND name = ?',
record.project_id,
record.id, value])
record.errors.add attribute, :taken
end
elsif DmsfWorkflow.exists?(['(project_id IS NULL OR project_id = ?) AND name = ?', record.project_id, value])
record.errors.add attribute, :taken
end
elsif record.id
record.errors.add attribute, :taken if DmsfWorkflow.exists?(['name = ? AND id != ?', value, record.id])
elsif DmsfWorkflow.exists?(name: value)
record.errors.add attribute, :taken
end
end
end

View File

@ -0,0 +1,34 @@
<%
# Redmine plugin for Document Management System "Features"
#
# Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
%>
<h3 class="title"><%= l(:label_email_address_add) %></h3>
<%= form_tag(append_email_dmsf_path(id: @project), remote: true, method: :post, id: 'new-user-form') do %>
<%= hidden_field_tag :project_id, @project.id %>
<p><%= label_tag 'user_search', l(:label_user_search) %><%= text_field_tag 'user_search', nil %></p>
<%= javascript_tag "observeSearchfield('user_search', 'users_for_users', '#{ escape_javascript url_for(
controller: 'dmsf', action: 'autocomplete_for_user') }')" %>
<div id="users_for_watcher">
<%= render_principals_for_new_email @principals %>
</div>
<p class="buttons">
<%= submit_tag l(:button_add), name: nil, onclick: 'hideModal(this);' %>
<%= submit_tag l(:button_cancel), name: nil, onclick: 'hideModal(this);', type: 'button' %>
</p>
<% end %>

View File

@ -0,0 +1,33 @@
<%
# Redmine plugin for Document Management System "Features"
#
# Vít Jonáš <vit.jonas@gmail.com>, Daniel Munn <dan.munn@munnster.co.uk>, Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
%>
<% if object %>
<div>
<% object.custom_field_values.each do |custom_value| %>
<% unless custom_value.value.blank? %>
<div class="status attribute">
<%= content_tag :div, h(custom_value.custom_field.name), class: 'label' %>
<div class="value">
<%= show_value custom_value %>
</div>
</div>
<% end %>
<% end %>
</div>
<% end %>

View File

@ -0,0 +1,35 @@
<%
# Redmine plugin for Document Management System "Features"
#
# Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
%>
<% if @project %>
<div class="dmsf-header">
<div class="wiki dmsf-description">
<%= textilizable @folder ? @folder.description : @project.dmsf_description %>
</div>
<% if @folder && @folder.custom_field_values.any? { |o| o.value.present? } %>
<ul>
<% render_custom_field_values(@folder) do |custom_field, formatted| %>
<li class="<%= custom_field.css_classes %>">
<span class="label"><%= custom_field.name %>:</span> <%= formatted %>
</li>
<% end %>
</ul>
<% end %>
</div>
<% end %>

View File

@ -0,0 +1,29 @@
<%
# Redmine plugin for Document Management System "Features"
#
# Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
%>
<p>
<%= l(:text_dmsf_webdav_digest_reset) %>
</p>
<%= form_tag(dmsf_reset_digest_path) do %>
<label for="password">
<%= l(:field_password) %>
</label>
<%= password_field_tag 'password', nil, autofocus: true %>
<input type="submit" name="reset" value="<%= l(:button_reset) %>" onclick="hideModal(this);">
<% end %>

View File

@ -0,0 +1,96 @@
<%
# Redmine plugin for Document Management System "Features"
#
# Vít Jonáš <vit.jonas@gmail.com>, Daniel Munn <dan.munn@munnster.co.uk>, Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
%>
<% html_title l(:dmsf) %>
<% if @project %>
<div class="contextual">
<% unless @locked || @system_folder %>
<% if @file_manipulation_allowed %>
<%= link_to sprite_icon('add', l(:label_document_new)),
multi_dmsf_upload_path(id: @project, folder_id: @folder), class: 'icon icon-add',
data: { cy: 'button__new-file--dmsf' } %>
<% end %>
<% if @folder_manipulation_allowed %>
<%= link_to sprite_icon('add', l(:link_create_folder)), new_dmsf_path(id: @project, parent_id: @folder),
class: 'icon icon-add', data: { cy: 'button__create-folder--dmsf' } %>
<% end %>
<% end %>
<%= actions_dropdown do %>
<%= render partial: 'dmsf_context_menus/main', locals: {
folder_manipulation_allowed: @folder_manipulation_allowed,
system_folder: @system_folder,
folder: @folder,
project: @project,
locked: @locked,
file_manipulation_allowed: @file_manipulation_allowed,
trash_enabled: @trash_enabled,
notifications: @notifications} %>
<% end %>
</div>
<% end %>
<%= render partial: 'path', locals: { folder: @folder, filename: nil, title: nil } %>
<%= render partial: 'description' %>
<%= form_tag(@project ? dmsf_folder_path(id: @project, folder_id: @folder) : dmsf_index_path,
method: :get, id: 'query_form', class: 'dmsf-query-form') do %>
<%= hidden_field_tag('folder_id', @folder.id) if @folder %>
<div id="dmsf-query-form">
<%= render partial: 'queries/query_form' %>
</div>
<% end %>
<% if @query.valid? %>
<% if @dmsf_count == 0 %>
<p class="nodata"><%= l(:label_no_data) %></p>
<% else %>
<%= render partial: 'query_list', locals: { query: @query, dmsf_pages: @dmsf_pages, folder: @folder } %>
<span class="pagination"><%= pagination_links_full @dmsf_pages, @dmsf_count %></span>
<% end %>
<% end %>
<%= context_menu %>
<% if !@folder&.system && (@project || RedmineDmsf.dmsf_webdav?) %>
<% other_formats_links do |f| %>
<% if @project %>
<%= f.link_to 'CSV', url: { action: :show, id: @project, folder_id: @folder, encoding: Encoding::UTF_8 } %>
<% end %>
<% if RedmineDmsf.dmsf_webdav? %>
<span>
<%= link_to 'WebDAV', webdav_url(@project, @folder), class: 'webdav' %>
</span>
<% end %>
<% end %>
<% end %>
<% content_for :sidebar do %>
<%= render partial: 'dmsf/sidebar' %>
<% project_or_folder = @folder? @folder : @project %>
<% if project_or_folder&.watchers.present? && User.current.allowed_to?(:view_dmsf_folder_watchers, @project) %>
<div id="watchers">
<%= render partial: 'watchers/watchers', locals: { watched: @folder ? @folder : @project } %>
</div>
<% end %>
<% end %>
<% javascript_tag do %>
"$('#ajax-indicator').hide();"
<% end %>

View File

@ -0,0 +1,49 @@
<%
# Redmine plugin for Document Management System "Features"
#
# Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
%>
<h2 class="dmsf-header">
<% if folder %>
<%= link_to l(:link_documents), dmsf_folder_path(id: @project) %>
<% folder.dmsf_path.each do |path_element| %>
/
<% if filename.blank? && (path_element == folder.dmsf_path.last) %>
<%= h(path_element.title) %>
<% else %>
<%= link_to h(path_element.title), dmsf_folder_path(id: @project, folder_id: path_element) %>
<% end %>
<% end %>
<% if folder.locked? && !(filename || title) %>
<%= content_tag('span', sprite_icon('unlock', nil, icon_only: true, size: '12'),
title: l(:title_locked_by_user, user: folder.locked_by)) %>
<% end %>
<% else %>
<% if @project %>
<%= link_to l(:link_documents), dmsf_folder_path(id: @project) %>
<% else %>
<%= l(:link_documents) %>
<% end %>
<% end %>
<% if filename %>
/
<%= h(filename) %>
<% end %>
<% if title %>
&#187; <%= title %>
<% end %>
</h2>

View File

@ -0,0 +1,44 @@
<%
# Redmine plugin for Document Management System "Features"
#
# Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
%>
<% query_options = nil unless defined?(query_options) %>
<% query_options ||= {} %>
<%= form_tag({}, data: { cm_url: query.deleted ? dmsf_trash_context_menu_path : dmsf_context_menu_path(folder_id: folder) }) do %>
<%= hidden_field_tag 'back_url', url_for(params: request.query_parameters), id: nil %>
<%= query_columns_hidden_tags(query) %>
<div class="autoscroll">
<table class="list dmsf odd-even <%= query.css_classes %> context-menu-container" data-hascontextmenu="true">
<thead>
<tr>
<th class="checkbox hide-when-print">
<%= check_box_tag 'check_all', '', false, class: 'toggle-selection', title: "#{l(:button_check_all)}/#{l(:button_uncheck_all)}" %>
</th>
<% query.inline_columns.each do |column| %>
<%= column_header(query, column, query_options) %>
<% end %>
<th class="buttons"></th>
</tr>
</thead>
<tbody>
<%= render partial: 'query_rows', locals: { query: query, dmsf_pages: dmsf_pages } %>
</tbody>
</table>
</div>
<% end %>

View File

@ -0,0 +1,110 @@
<%
# Redmine plugin for Document Management System "Features"
#
# Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
%>
<% options = {} %>
<% if dmsf_pages %>
<% options[:offset] = dmsf_pages.offset %>
<% options[:limit] = dmsf_pages.per_page %>
<% end %>
<% query.dmsf_nodes(options).each do |node| %>
<% case node.type %>
<% when 'project'%>
<% id = "#{node.id}pspan" %>
<% when 'folder' %>
<% id = "#{node.id}fspan" %>
<% else %>
<% id = "#{node.id}item" %>
<% end %>
<% @idnt ||= 0 %>
<tr id="<%= id %>" data-cy="row__<%= node.title %>--<%= node.type %>_<%= node.id %>"
class="<%= cycle('odd', 'even') %>
<%= node.css_classes(query.deleted) %> <%= params[:classes] %> <%= @idnt > 0 ? "idnt idnt-#{@idnt}" : nil %>">
<td class="checkbox hide-when-print">
<%= check_box_tag('ids[]', "#{node.type}-#{node.id}", false, id: nil) unless node.system %>
</td>
<% query.inline_columns.each do |column| %>
<% classes = column.css_classes.to_s.dup %>
<% classes << ' dmsf-gray' if node.type.match?(/link$/) %>
<% classes << ' dmsf-system' if node.system %>
<%= content_tag 'td', column_content(column, node), class: classes %>
<% end %>
<td class="buttons">
<% unless node.system %>
<%= link_to_context_menu %>
<% end %>
</td>
</tr>
<% end %>
<% unless query.deleted || query&.project.nil? %>
<%= javascript_tag do %>
$(function() {
$("table.dmsf tr").removeClass("ui-draggable-dragging ui-droppable-active ui-droppable-hover ui-draggable-handle");
$(".dmsf-draggable").draggable({
helper: function(event, ui) {
var ret = $(this).clone();
var width = $(this)[0].offsetWidth;
var myHelper = [];
myHelper.push('<table style="width: ' + width + 'px; background-color: #ffffdd;">');
myHelper.push(ret.html());
myHelper.push('</table>');
helper = myHelper.join('');
return helper;
},
axis: "y",
revert: "invalid"
});
$(".dmsf-droppable" ).droppable({
drop: function(event, ui) {
let handle = $(this);
let dragObjectId = ui.draggable.find("td").find("input").val()
let dropObjectId = handle.attr('id');
let data = {};
handle.addClass("ui-state-highlight ajax-loading")
data['dmsf_folder'] = { drag_id: dragObjectId, drop_id: dropObjectId};
$.ajax({
url: '<%= dmsf_folder_url(query.project) %>',
type: 'put',
dataType: 'script',
data: data,
error: function(jqXHR, textStatus, errorThrown){
alert(errorThrown);
ui.draggable.animate(ui.draggable.data("ui-draggable").originalPosition, "slow");
},
complete: function(jqXHR, textStatus, errorThrown){
handle.removeClass("ui-state-highlight ajax-loading")
if(textStatus == 'success'){
if(!handle.hasClass('dmsf-not-loaded')){
var m = handle.attr("id").match(/^(\d+)span$/);
if(m){
$('.' + m[1]).remove();
handle.removeClass("dmsf-expanded");
handle.addClass("dmsf-collapsed dmsf-not-loaded");
}
ui.draggable.remove();
}
window.location.reload();
}
}
});
}
});
});
<% end %>
<% end %>

View File

@ -0,0 +1,20 @@
<%
# Redmine plugin for Document Management System "Features"
#
# Vít Jonáš <vit.jonas@gmail.com>, Daniel Munn <dan.munn@munnster.co.uk>, Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
%>
<%= render_sidebar_queries DmsfQuery, @project %>

View File

@ -0,0 +1,24 @@
<%
# Redmine plugin for Document Management System "Features"
#
# Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
%>
var modal = $('#ajax-modal');
modal.html("<%= escape_javascript(render partial: 'dmsf/add_email') %>");
showModal('ajax-modal', '400px');
modal.addClass('new-user');

View File

@ -0,0 +1,30 @@
<%
# Redmine plugin for Document Management System "Features"
#
# Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
%>
let to = $('#email_to');
let text = to.val();
<% @principals.each do |p| %>
if(text.indexOf("<%= p.mail %>") < 0){
if(text.length > 0){
text += ', '
}
text += "<%= p.mail %>"
}
<% end %>
to.val(text);

View File

@ -0,0 +1,20 @@
<%
# Redmine plugin for Document Management System "Features"
#
# Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
%>
$('#users_for_watcher').html('<%= escape_javascript(render_principals_for_new_email(@principals)) %>');

View File

@ -0,0 +1,72 @@
<%
# Redmine plugin for Document Management System "Features"
#
# Vít Jonáš <vit.jonas@gmail.com>, Daniel Munn <dan.munn@munnster.co.uk>, Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
%>
<%= render partial: '/dmsf/path', locals: { folder: @folder, filename: nil, title: nil } %>
<% if @projects.present? || @fast_links %>
<%= form_tag(entries_operations_dmsf_path, id: 'copyForm') do %>
<% @ids.each do |id| %>
<%= hidden_field_tag 'ids[]', id %>
<% end %>
<%= hidden_field_tag 'back_url', @back_url %>
<%= hidden_field_tag 'id', @project %>
<%= hidden_field_tag 'copy_entries', true %>
<%= hidden_field_tag 'move_entries', true %>
<div class="box tabular">
<% unless @fast_links %>
<p>
<%= label_tag 'dmsf_entries[target_project_id]', l(:field_target_project) %>
<%= select_tag 'dmsf_entries[target_project_id]',
project_tree_options_for_select(@projects, selected: @target_project) %>
</p>
<% end %>
<p>
<%= label_tag 'dmsf_entries[target_folder_id]', l(:field_target_folder) %><%= ' #' if @fast_links %>
<% if @fast_links %>
<%= text_field_tag 'dmsf_entries[target_folder_id]', '', required: true, max_length: 255 %>
<% else %>
<%= select_tag 'dmsf_entries[target_folder_id]', options_for_select(@folders, selected: @target_folder&.id) %>
<% end %>
</p>
</div>
<p>
<%= submit_tag l(:button_copy), id: 'copy_button', data: { cy: "button__copy--dmsf" } %>
<%# TODO: Lock and proper permissions %>
<% if User.current.allowed_to?(:folder_manipulation, @project) &&
User.current.allowed_to?(:file_manipulation, @project) %>
<%= submit_tag l(:button_move), id: 'move_button', data: { cy: "button__move--dmsf" } %>
<% end %>
</p>
<% end %>
<% end %>
<%= javascript_tag do %>
$('#move_button').click(function(event) {
$('input#copy_entries').remove()
});
<% unless @fast_links %>
$('#dmsf_entries_target_project_id').change(function () {
$('#content').load("<%= copymove_entries_path(id: @project, folder_id: @folder, ids: @ids) %>",
$('#copyForm').serialize());
});
$('#dmsf_entries_target_project_id').select2();
$('#dmsf_entries_target_folder_id').select2();
<% end %>
<% end %>

View File

@ -0,0 +1,4 @@
api.dmsf_folder do
api.id @folder.id
api.title @folder.title
end

View File

@ -0,0 +1,21 @@
<%
# Redmine plugin for Document Management System "Features"
#
# Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
%>
$('#ajax-modal').html('<%= escape_javascript render partial: 'digest' %>');
showModal('ajax-modal', '30%', '<%= l(:label_dmsf_webdav_digest) %>');

View File

@ -0,0 +1,20 @@
<%
# Redmine plugin for Document Management System "Features"
#
# Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
%>
throw new Error("<%= j flash[:error] %>");

View File

@ -0,0 +1,128 @@
<%
# Redmine plugin for Document Management System "Features"
#
# Vít Jonáš <vit.jonas@gmail.com>, Daniel Munn <dan.munn@munnster.co.uk>, Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
%>
<% html_title l(:dmsf) %>
<div class="contextual">
<% if !@folder.new_record? && User.current.allowed_to?(:folder_manipulation, @project) && !@folder.system %>
<% if @folder.locked? %>
<%= link_to_if @folder.unlockable? && (!@folder.locked_for_user? || @force_file_unlock_allowed),
sprite_icon('unlock', l(:button_unlock)),
unlock_dmsf_path(id: @project, folder_id: @folder,
back_url: edit_dmsf_url(id: @project, folder_id: @folder)),
title: l(:title_unlock_file), class: 'icon icon-unlock' %>
<% else %>
<%= link_to sprite_icon('lock', l(:button_lock)),
lock_dmsf_path(id: @project, folder_id: @folder,
back_url: edit_dmsf_url(id: @project, folder_id: @folder)),
title: l(:title_lock_file), class: 'icon icon-lock' %>
<% end %>
<%= actions_dropdown do %>
<%= render partial: 'dmsf_context_menus/folder',
locals: { dmsf_folder: @folder,
locked: @folder.locked?,
allowed: true,
dmsf_link: nil,
project: @project,
folder: @folder.dmsf_folder,
edit: true,
unlockable: @folder.unlockable? && (!@folder.locked_for_user? || User.current.allowed_to?(:force_file_unlock, @project)),
email_allowed: User.current.allowed_to?(:email_documents, @project),
notifications: @notifications,
back_url: edit_dmsf_url(id: @project, folder_id: @folder)
} %>
<% end %>
<% end %>
</div>
<% create = @pathfolder == @parent %>
<%= render partial: 'path',
locals: { folder: @pathfolder, filename: create ? l(:heading_new_folder) : nil, title: nil } %>
<%= labelled_form_for(@folder,
url: { action: create ? 'create' : 'save', id: @project, folder_id: @folder, parent_id: @parent },
html: { method: :post }) do |f| %>
<%= error_messages_for @folder %>
<%= f.hidden_field :redirect_to_folder_id, value: @redirect_to_folder_id %>
<div class="box tabular">
<p>
<%= f.text_field :title, required: true %>
</p>
<p>
<%= f.text_area :description, rows: 8, class: 'wiki-edit dmsf-description' %>
</p>
<p>
<% dir = @folder.inherited_permissions_from %>
<% if dir %>
<%= label_tag '', l(:label_inherited_permissions) %>
<% @project_roles.each do |role| %>
<% checked = dir.permission_for_role(role) %>
<% if checked %>
<label class="inline">
<%= check_box_tag 'inherited_permissions[role_ids][]', role.id, checked, disabled: 'disabled', id: nil %>
<%= role %>
</label>
<% end %>
<% end %>
<span id="inherited_user_permissions">
<br>
<% users = dir.permissions_users %>
<% checkboxes = users_checkboxes(users, inherited: true) %>
<%= checkboxes %>
</span>
<% if checkboxes.present? %>
<br>
<% end %>
<% end %>
<%= label_tag '', l(:label_dmsf_permissions) %>
<% @project_roles.each do |role| %>
<% checked = @folder.permission_for_role(role) %>
<label class="inline">
<%= check_box_tag 'permissions[role_ids][]', role.id, checked, id: nil %>
<%= role %>
</label>
<% end %>
<span id="user_permissions">
<br>
<% users = @folder.permissions_users %>
<% checkboxes = users_checkboxes(users) %>
<%= checkboxes %>
</span>
<% if checkboxes.present? %>
<br>
<% end %>
<span class="search_for_watchers">
<%= link_to sprite_icon('add', l(:label_search_for_watchers), size: 12),
new_dmsf_folder_permissions_path(project_id: @project, dmsf_folder_id: @folder),
remote: true,
method: :get %>
</span>
</p>
<% values = @folder ? @folder.custom_field_values : (@parent ? @parent.custom_field_values : DmsfFolder.new.custom_field_values) %>
<% values.each do |value| %>
<p><%= custom_field_tag_with_label :dmsf_folder, value %></p>
<% end %>
</div>
<div class="form-actions">
<%= submit_tag create ? l(:button_create) : l(:submit_save), class: 'button-positive',
data: { cy: "button__submit--dmsf_folder" } %>
</div>
<% end %>
<%= wikitoolbar_for 'dmsf_folder_description' %>

View File

@ -0,0 +1,51 @@
<%
# Redmine plugin for Document Management System "Features"
#
# Vít Jonáš <vit.jonas@gmail.com>, Daniel Munn <dan.munn@munnster.co.uk>, Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
%>
<% html_title(l(:dmsf)) %>
<div class="contextual">
<% if @notifications && User.current.allowed_to?(:folder_manipulation, @project) %>
<% if @project.dmsf_notification %>
<%= link_to sprite_icon('email', l(:label_notifications_off)),
notify_deactivate_dmsf_path(id: @project, back_url: edit_root_dmsf_path(id: @project)),
title: l(:title_notifications_active_deactivate),
class: 'icon icon-email' %>
<% else %>
<%= link_to sprite_icon('email-disabled', l(:label_notifications_on)),
notify_activate_dmsf_path(id: @project, back_url: edit_root_dmsf_path(id: @project)),
title: l(:title_notifications_active_deactivate),
class: 'icon icon-email-add' %>
<% end %>
<% end %>
</div>
<%= render partial: 'path', locals: { folder: nil, filename: nil, title: nil } %>
<%= labelled_form_for(@project, url: { action: 'save_root', id: @project }, html: { method: :post }) do |f| %>
<div class="box tabular">
<p>
<%= f.text_area :dmsf_description, rows: 8, class: 'wiki-edit dmsf-description', label: l(:field_description) %>
</p>
<div class="form-actions">
<%= f.submit l(:submit_save), class: 'button-positive' %>
</div>
</div>
<% end %>
<%= wikitoolbar_for 'project_dmsf_description' %>

View File

@ -0,0 +1,74 @@
<%
# Redmine plugin for Document Management System "Features"
#
# Vít Jonáš <vit.jonas@gmail.com>, Daniel Munn <dan.munn@munnster.co.uk>, Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
%>
<% html_title l(:dmsf) %>
<%= render partial: 'path', locals: { folder: @folder, filename: nil, title: l(:heading_send_documents_by_email) } %>
<%= form_tag(email_entries_path(id: @project, folder_id: @folder), { method: :post }) do %>
<%= hidden_field_tag 'email[zipped_content]', @email_params[:zipped_content] %>
<%= hidden_field_tag 'email[folders]', @email_params[:folders].to_json %>
<%= hidden_field_tag 'email[files]', @email_params[:files].to_json %>
<%= hidden_field_tag 'email[from]', @email_params[:from] %>
<%= hidden_field_tag 'email[reply_to]', @email_params[:reply_to] %>
<%= hidden_field_tag 'back_url', @back_url %>
<div class="box tabular">
<p>
<%= label_tag '', l(:label_email_from) %>
<%= text_field_tag 'email[from_disabled]', @email_params[:from], class: 'dmsf-full-width', disabled: true %>
</p>
<p>
<%= label_tag 'email[to]', l(:label_email_to) %>
<span>
<%= text_field_tag 'email[to]', @email_params[:to], class: 'dmsf-full-width', required: true %>
<%= link_to sprite_icon('add', l(:button_add)), add_email_dmsf_path(id: @project),
title: l(:label_email_address_add), class: 'icon icon-add', remote: true %>
</span>
</p>
<p>
<%= label_tag 'email[cc]', l(:label_email_cc) %>
<%= text_field_tag 'email[cc]', @email_params[:cc], class: 'dmsf-full-width' %>
</p>
<p>
<%= label_tag 'email[subject]', l(:label_email_subject) %>
<%= text_field_tag 'email[subject]', @email_params[:subject], class: 'dmsf-full-width' %>
</p>
<p>
<%= label_tag '', l(:label_email_documents) %>
<span>
<%= link_to 'Documents.zip', download_email_entries_path(id: @project, folder_id: @folder, entry: @email_params[:zipped_content]) %>
<%= l(:label_or) %>
<%= check_box_tag('email[links_only]', 1, RedmineDmsf.dmsf_documents_email_links_only?,
onchange: "$('#public_url').toggle($('#email_links_only').prop('checked'))")
%>
<%= l(:label_links_only) %>
<%= render partial: 'dmsf_public_urls/new' %>
</span>
</p>
<p>
<%= label_tag 'email[body]', l(:label_email_body) %>
<%= text_area_tag 'email[body]', @email_params['body'], rows: '20', class: 'dmsf-full-width wiki-edit' %>
</p>
<div class="form-actions">
<%= submit_tag l(:label_email_send), class: 'button-positive' %>
</div>
</div>
<% end %>
<%= wikitoolbar_for 'email_body' %>

View File

@ -0,0 +1,20 @@
<%
# Redmine plugin for Document Management System "Features"
#
# Vít Jonáš <vit.jonas@gmail.com>, Daniel Munn <dan.munn@munnster.co.uk>, Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
%>
<%= render partial: 'main', locals: {} %>

View File

@ -0,0 +1,23 @@
<%
# Redmine plugin for Document Management System "Features"
#
# Vít Jonáš <vit.jonas@gmail.com>, Daniel Munn <dan.munn@munnster.co.uk>, Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
%>
// Add rows
$('#<%= params[:row_id] %>').after(
'<%= escape_javascript(render(partial: 'dmsf/query_rows', locals: { query: @query, dmsf_pages: nil })) %>'
);

View File

@ -0,0 +1,5 @@
api.dmsf_folder do
api.id @folder.id
api.title @folder.title
api.description @folder.description
end

View File

@ -0,0 +1,28 @@
api.dmsf do
api.array :dmsf_nodes, api_meta(total_count: @query.dmsf_nodes().count, limit: @limit, offset: @offset) do
@query.dmsf_nodes(offset: @offset, limit: @limit).each do |node|
api.node do
api.id node.id
api.title node.title
api.type node.type
case node.type
when 'file', 'url-link'
api.filename node.filename
when 'file-link', 'folder-link'
api.target_id node.revision_id.to_i
api.target_project_id node.project_id
end
end
end
end
if @folder
api.found_folder do
api.id @folder.id
api.title @folder.title
render_api_custom_values @folder.visible_custom_field_values, api
end
end
end

View File

@ -0,0 +1,20 @@
<%
# Redmine plugin for Document Management System "Features"
#
# Vít Jonáš <vit.jonas@gmail.com>, Daniel Munn <dan.munn@munnster.co.uk>, Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
%>
<%= render partial: 'main', locals: {} %>

View File

@ -0,0 +1,42 @@
<%
# Redmine plugin for Document Management System "Features"
#
# Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
%>
<% html_title l(:dmsf) %>
<div class="contextual">
<% if @file_delete_allowed %>
<%= link_to sprite_icon('del', l(:label_empty_trash_bin)), empty_trash_path(id: @project), method: :delete %>
<% end %>
</div>
<h2><%= l(:link_trash_bin) %></h2>
<%= form_tag(trash_dmsf_path(id: @project), method: :get, id: 'query_form', class: 'dmsf-query-form') do %>
<div id="dmsf-query-form">
<%= render partial: 'queries/query_form' %>
</div>
<% end %>
<%= render partial: 'query_list', locals: { query: @query, dmsf_pages: @dmsf_pages } %>
<span class="pagination"><%= pagination_links_full @dmsf_pages, @dmsf_count %></span>
<%= context_menu %>
<% content_for :sidebar do %>
<%= render partial: 'dmsf/sidebar' %>
<% end %>

View File

@ -0,0 +1,82 @@
<%
# Redmine plugin for Document Management System "Features"
#
# Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
%>
<% workflows_available = DmsfWorkflow.where(['project_id = ? OR project_id IS NULL', project&.id]).exists? %>
<% wf = DmsfWorkflow.find_by(id: dmsf_file.last_revision.dmsf_workflow_id) if dmsf_file.last_revision.dmsf_workflow_id %>
<% file_approval_allowed = User.current.allowed_to?(:file_approval, project) %>
<% allowed = User.current && (dmsf_file.last_revision.dmsf_workflow_assigned_by_user == User.current) && wf %>
<% allowed_minor = dmsf_file.approval_allowed_zero_minor %>
<% if file_approval_allowed %>
<% case dmsf_file.last_revision.workflow %>
<% when DmsfWorkflow::STATE_WAITING_FOR_APPROVAL %>
<% if wf %>
<% assignments = wf.next_assignments(dmsf_file.last_revision.id) %>
<% index = assignments.find_index{ |assignment| assignment.user_id == User.current.id } if assignments %>
<% assignment_id = (index && assignments && assignments[index]) ? assignments[index].id : nil %>
<% if allowed_minor %>
<%= context_menu_link sprite_icon('checked', l(:title_approval)),
action_dmsf_workflow_path(project_id: project.id, id: wf.id,
dmsf_workflow_step_assignment_id: assignment_id,
dmsf_file_revision_id: dmsf_file.last_revision.id,
back_url: back_url),
remote: true, class: 'icon icon-ok', id: 'dmsf-cm-workflow',
disabled: !assignments || !index %>
<% else %>
<%= context_menu_link sprite_icon('checked', l(:title_approval_minor)),
action_dmsf_workflow_path(project_id: project.id, id: wf.id,
dmsf_workflow_step_assignment_id: assignment_id,
dmsf_file_revision_id: dmsf_file.last_revision.id,
back_url: back_url),
remote: true, class: 'icon icon-ok', id: 'dmsf-cm-workflow', disabled: true %>
<% end %>
<% end %>
<% when DmsfWorkflow::STATE_ASSIGNED %>
<% if allowed_minor %>
<%= context_menu_link sprite_icon('checked', l(:title_start)),
start_dmsf_workflow_path(id: dmsf_file.last_revision.dmsf_workflow_id,
dmsf_file_revision_id: dmsf_file.last_revision.id,
back_url: back_url),
class: 'icon icon-ok', disabled: !allowed %>
<% else %>
<%= context_menu_link sprite_icon('checked', l(:title_start_minor)),
start_dmsf_workflow_path(id: dmsf_file.last_revision.dmsf_workflow_id,
dmsf_file_revision_id: dmsf_file.last_revision.id,
back_url: back_url),
class: 'icon icon-ok', disabled: true %>
<% end %>
<% when DmsfWorkflow::STATE_APPROVED, DmsfWorkflow::STATE_REJECTED, DmsfWorkflow::STATE_OBSOLETE %>
<%# %>
<% else %>
<% if allowed_minor %>
<%= context_menu_link sprite_icon('checked', l(:title_assignment)),
assign_dmsf_workflow_path(id: project.id, project_id: project.id,
dmsf_file_revision_id: dmsf_file.last_revision.id,
back_url: back_url),
remote: true, class: 'icon icon-ok', id: 'dmsf-cm-workflow',
disabled: locked || !workflows_available %>
<% else %>
<%= context_menu_link sprite_icon('checked', l(:title_assignment_minor)),
assign_dmsf_workflow_path(id: project.id, project_id: project.id,
dmsf_file_revision_id: dmsf_file.last_revision.id,
back_url: back_url),
remote: true, class: 'icon icon-ok', id: 'dmsf-cm-workflow', disabled: true %>
<% end %>
<% end %>
<% end %>

View File

@ -0,0 +1,116 @@
<%
# Redmine plugin for Document Management System "Features"
#
# Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
%>
<li>
<%= context_menu_link sprite_icon('edit', l(:button_edit)), dmsf_file_path(id: dmsf_file, back_url: back_url),
class: 'icon icon-edit', data: { cy: "icon__edit--dmsf_file_#{dmsf_file.id}" },
disabled: !allowed || (locked && !unlockable) %>
</li>
<% unless dmsf_link %>
<li>
<%= context_menu_link sprite_icon('copy', "#{l(:button_copy)}/#{l(:button_move)}"),
copymove_entries_path(id: project, folder_id: folder, ids: ["file-#{dmsf_file.id}"],
back_url: back_url), title: l(:title_copy), class: 'icon icon-copy',
data: { cy: "icon__copy--dmsf_file_#{dmsf_file.id}" } %>
</li>
<li>
<%= link_to sprite_icon('link', l(:label_link_to)),
new_dmsf_link_path(project_id: dmsf_file.project.id, dmsf_folder_id: dmsf_file.dmsf_folder,
dmsf_file_id: dmsf_file.id, type: 'link_to', back_url: back_url),
title: l(:title_create_link), class: 'icon dmsf-icon-link',
data: { cy: "icon__link_to--dmsf_file_#{dmsf_file.id}" } %>
</li>
<% end %>
<li>
<% if locked %>
<%= context_menu_link sprite_icon('unlock', l(:button_unlock)), unlock_dmsf_files_path(id: dmsf_file, back_url: back_url),
class: 'icon icon-unlock', data: { cy: "icon__unlock--dmsf_file_#{dmsf_file.id}" },
title: l(:title_locked_by_user, user: dmsf_file.locked_by), disabled: !unlockable %>
<% else %>
<%= context_menu_link sprite_icon('lock', l(:button_lock)), lock_dmsf_files_path(id: dmsf_file, back_url: back_url),
class: 'icon icon-lock', data: { cy: "icon__lock--dmsf_file_#{dmsf_file.id}" }, disabled: !allowed %>
<% end %>
</li>
<% if notifications %>
<li>
<% if dmsf_file.notification %>
<%= context_menu_link sprite_icon('email', l(:label_notifications_off)),
notify_deactivate_dmsf_files_path(id: dmsf_file, back_url: back_url),
class: 'icon icon-email', data: { cy: "icon__email--dmsf_file_#{dmsf_file.id}" },
disabled: !allowed || locked %>
<% else %>
<%= context_menu_link sprite_icon('email-disabled', l(:label_notifications_on)),
notify_activate_dmsf_files_path(id: dmsf_file, back_url: back_url),
class: 'icon icon-email-add',
data: { cy: "icon__email_add--dmsf_file_#{dmsf_file.id}" },
disabled: !allowed || locked %>
<% end %>
</li>
<% end %>
<li>
<%= render partial: 'approval_workflow', locals: { dmsf_file: dmsf_file, project: project, locked: locked,
back_url: back_url } %>
</li>
<li>
<% member = Member.find_by(user_id: User.current.id, project_id: dmsf_file.project.id) %>
<% filename = dmsf_file.last_revision&.formatted_name(member) %>
<%= context_menu_link sprite_icon('download', l(:button_download)),
static_dmsf_file_path(dmsf_file, filename: filename),
class: 'icon icon-download', data: { cy: "icon__download--dmsf_file_#{dmsf_file.id}" },
disabled: false %>
</li>
<li>
<%= context_menu_link sprite_icon('email', l(:field_mail)),
entries_operations_dmsf_path(id: project, folder_id: folder, ids: params[:ids],
email_entries: true, back_url: back_url),
method: :post, class: 'icon icon-email',
data: { cy: "icon__email--dmsf_file_#{dmsf_file.id}" }, disabled: !email_allowed %>
</li>
<% if RedmineDmsf.dmsf_webdav? %>
<li>
<% if dmsf_file.last_revision && dmsf_file.last_revision.protocol %>
<% url = "#{dmsf_file.last_revision.protocol}:ofe|u|#{Setting.protocol.strip}://#{Setting.host_name.strip}/dmsf/webdav/#{Addressable::URI.escape(RedmineDmsf::Webdav::ProjectResource.create_project_name(dmsf_file.project))}/" %>
<% if dmsf_file.dmsf_folder %>
<% url << "#{dmsf_file.dmsf_folder.dmsf_path_str}/" %>
<% end %>
<% url << dmsf_file.name %>
<% end %>
<% icon_name = icon_for_mime_type(Redmine::MimeType.css_class_of(dmsf_file.name)) %>
<%= context_menu_link sprite_icon(icon_name, l(:button_edit_content)), url, class: 'icon icon-file',
disabled: url.blank? || (locked && !unlockable) ||
(Setting.plugin_redmine_dmsf['dmsf_webdav_strategy'] != 'WEBDAV_READ_WRITE') %>
</li>
<% end %>
<li>
<%= render partial: 'dmsf_context_menus/watch', locals: { object: dmsf_file } %>
</li>
<% if @preview %>
<li>
<%= context_menu_link sprite_icon('zoom-in', l(:label_preview)), view_dmsf_file_path(dmsf_file, preview: true),
class: 'icon icon-zoom-in', disabled: false %>
</li>
<% end %>
<li>
<%= context_menu_link sprite_icon('del', l(:button_delete)),
dmsf_link ? dmsf_link_path(id: dmsf_link, folder_id: folder, back_url: back_url) : dmsf_file_path(id: dmsf_file,
folder_id: folder, back_url: back_url),
method: :delete, class: 'icon icon-del', data: { confirm: l(:text_are_you_sure),
cy: "icon__delete--dmsf_file_#{dmsf_file.id}" }, id: 'dmsf-cm-delete',
disabled: !allowed || (locked && !dmsf_link) %>
</li>

View File

@ -0,0 +1,30 @@
<%
# Redmine plugin for Document Management System "Features"
#
# Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
%>
<li>
<%= context_menu_link sprite_icon('reload', l(:title_restore)),
dmsf_link ? restore_dmsf_link_path(id: dmsf_link) : restore_dmsf_file_path(id: dmsf_file),
class: 'icon icon-cancel', disabled: !allowed_restore %>
</li>
<li>
<%= context_menu_link sprite_icon('del', l(:button_delete)),
dmsf_link ? dmsf_link_path(id: dmsf_link, commit: 'yes') : dmsf_file_path(id: dmsf_file, commit: 'yes'),
data: { confirm: l(:text_are_you_sure) }, method: :delete, class: 'icon icon-del',
id: 'dmsf-cm-delete', disabled: !allowed_delete %>
</li>

View File

@ -0,0 +1,104 @@
<%
# Redmine plugin for Document Management System "Features"
#
# Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
%>
<% unless edit %>
<li>
<%= context_menu_link sprite_icon('edit', l(:button_edit)),
edit_dmsf_path(id: dmsf_folder.project, folder_id: dmsf_folder, back_url: back_url),
class: 'icon icon-edit', data: { cy: "icon__edit--dmsf_folder_#{dmsf_folder.id}" },
disabled: !allowed || locked %>
</li>
<% end %>
<% unless dmsf_link %>
<li>
<%= context_menu_link sprite_icon('copy', "#{l(:button_copy)}/#{l(:button_move)}"),
copymove_entries_path(id: project, folder_id: folder, ids: ["folder-#{dmsf_folder.id}"],
back_url: back_url), class: 'icon icon-copy',
data: { cy: "icon__copy--dmsf_folder_#{dmsf_folder.id}" },
disabled: !allowed || locked %>
</li>
<li>
<%= context_menu_link sprite_icon('link', l(:label_link_to)),
new_dmsf_link_path(project_id: project.id, dmsf_folder_id: dmsf_folder.id, type: 'link_to',
back_url: back_url), class: 'icon dmsf-icon-link',
data: { cy: "icon__link_to--dmsf_folder_#{dmsf_folder.id}" } %>
</li>
<% end %>
<% unless edit %>
<li>
<% if locked %>
<%= context_menu_link sprite_icon('unlock', l(:button_unlock)),
unlock_dmsf_path(id: dmsf_folder.project, folder_id: dmsf_folder, back_url: back_url),
title: l(:title_locked_by_user, user: dmsf_folder.locked_by), class: 'icon icon-unlock',
data: { cy: "icon__unlock--dmsf_folder_#{dmsf_folder.id}" },
disabled: !allowed || !unlockable %>
<% else %>
<%= context_menu_link sprite_icon('lock', l(:button_lock)),
lock_dmsf_path(id: dmsf_folder.project, folder_id: dmsf_folder, back_url: back_url),
class: 'icon icon-lock', data: { cy: "icon__lock--dmsf_folder_#{dmsf_folder.id}" },
disabled: !allowed %>
<% end %>
</li>
<% end %>
<% if notifications %>
<li>
<% if dmsf_folder.notification %>
<%= context_menu_link sprite_icon('email', l(:label_notifications_off)),
notify_deactivate_dmsf_path(id: dmsf_folder.project, folder_id: dmsf_folder,
back_url: back_url), class: 'icon icon-email',
data: { cy: "icon__email--dmsf_folder_#{dmsf_folder.id}" },
disabled: !allowed || locked || !dmsf_folder.notification? %>
<% else %>
<%= context_menu_link sprite_icon('email-disabled', l(:label_notifications_on)),
notify_activate_dmsf_path(id: dmsf_folder.project, folder_id: dmsf_folder,
back_url: back_url), class: 'icon icon-email-add',
data: { cy: "icon__email_add--dmsf_folder_#{dmsf_folder.id}" },
disabled: !allowed || locked || dmsf_folder.notification? %>
<% end %>
</li>
<% end %>
<% unless edit %>
<li>
<%= context_menu_link sprite_icon('download', l(:button_download)),
entries_operations_dmsf_path(id: project, folder_id: folder, ids: params[:ids],
download_entries: true, back_url: back_url),
method: :post, class: 'icon icon-download',
data: { cy: "icon__download--dmsf_folder_#{dmsf_folder.id}" },
id: 'dmsf-cm-download', disabled: false %>
</li>
<li>
<%= context_menu_link sprite_icon('email', l(:field_mail)),
entries_operations_dmsf_path(id: dmsf_folder.project, folder_id: folder, ids: params[:ids],
email_entries: true, back_url: back_url),
method: :post, class: 'icon icon-email',
data: { cy: "icon__email--dmsf_folder_#{dmsf_folder.id}" },
disabled: !email_allowed %>
</li>
<% end %>
<li>
<%= render partial: 'dmsf_context_menus/watch', locals: { object: dmsf_folder } %>
</li>
<li>
<%= context_menu_link sprite_icon('del', l(:button_delete)),
dmsf_link ? dmsf_link_path(id: dmsf_link, folder_id: folder, back_url: back_url) :
delete_dmsf_path(id: dmsf_folder.project, folder_id: dmsf_folder, parent_id: folder, back_url: back_url),
data: { confirm: "#{l(:text_are_you_sure)}\n#{l(:text_not_empty) unless dmsf_folder.empty?}",
cy: "icon__delete--dmsf_folder_#{dmsf_folder.id}" }, method: :delete,
class: 'icon icon-del', id: 'dmsf-cm-delete', disabled: !allowed || (locked && !dmsf_link) %>
</li>

View File

@ -0,0 +1,30 @@
<%
# Redmine plugin for Document Management System "Features"
#
# Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
%>
<li>
<%= context_menu_link sprite_icon('reload', l(:title_restore)),
dmsf_link ? restore_dmsf_link_path(id: dmsf_link) : restore_dmsf_path(id: project, folder_id: dmsf_folder),
class: 'icon icon-cancel', disabled: !allowed_restore %>
</li>
<li>
<%= context_menu_link sprite_icon('del', l(:button_delete)),
dmsf_link ? dmsf_link_path(id: dmsf_link, commit: 'yes') : delete_dmsf_path(id: project, folder_id: dmsf_folder, commit: 'yes'),
data: { confirm: l(:text_are_you_sure) }, method: :delete, class: 'icon icon-del',
id: 'dmsf-cm-delete', disabled: !allowed_delete %>
</li>

View File

@ -0,0 +1,86 @@
<%
# Redmine plugin for Document Management System "Features"
#
# Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
%>
<% if folder_manipulation_allowed && !system_folder %>
<% if folder.nil? %>
<%= link_to sprite_icon('edit', l(:button_edit)),
edit_root_dmsf_path(id: project),
title: l(:link_edit, title: l(:link_documents)),
class: 'icon icon-edit',
data: { cy: 'button__edit--dmsf' } %>
<% elsif !locked %>
<%= link_to sprite_icon('edit', l(:button_edit)),
edit_dmsf_path(id: project, folder_id: folder, redirect_to_folder_id: folder.id),
title: l(:link_edit, title: h(folder.title)),
class: 'icon icon-edit',
data: { cy: 'button__edit--dmsf' } %>
<% end %>
<% if folder && (!locked || User.current.allowed_to?(:force_file_unlock, project)) %>
<% if folder.locked? %>
<%= link_to_if folder.unlockable?,
sprite_icon('unlock', l(:button_unlock)),
unlock_dmsf_path(id: project, folder_id: folder, current: request.url),
title: l(:title_unlock_folder),
class: 'icon icon-unlock',
data: { cy: 'button__unlock--dmsf' } %>
<% else %>
<%= link_to sprite_icon('lock', l(:button_lock)),
lock_dmsf_path(id: project, folder_id: folder, current: request.url),
title: l(:title_lock_folder), class: 'icon icon-lock',
data: { cy: 'button__lock--dmsf' } %>
<% end %>
<% end %>
<% if notifications && !locked %>
<% if ((folder && folder.notification) || (!folder && project.dmsf_notification)) %>
<%= link_to sprite_icon('email', l(:label_notifications_off)),
notify_deactivate_dmsf_path(id: project, folder_id: folder),
title: l(:title_notifications_active_deactivate),
class: 'icon icon-email',
data: { cy: 'button__notifications-off--dmsf' } %>
<% else %>
<%= link_to sprite_icon('email-disabled', l(:label_notifications_on)),
notify_activate_dmsf_path(id: project, folder_id: folder),
title: l(:title_notifications_not_active_activate),
class: 'icon icon-email-add',
data: { cy: 'button__notifications-on--dmsf' } %>
<% end %>
<% end %>
<% if file_manipulation_allowed && !locked %>
<%= link_to sprite_icon('link', l(:label_link_from)),
new_dmsf_link_path(project_id: project.id, dmsf_folder_id: folder ? folder.id : folder,
type: 'link_from'),
title: l(:title_create_link),
class: 'icon dmsf-icon-link',
data: { cy: 'button__create-link--dmsf' } %>
<% end %>
<% end %>
<%= render partial: 'dmsf_context_menus/watch', locals: { object: folder ? folder : project } %>
<% if trash_enabled %>
<%= link_to sprite_icon('del', l(:link_trash_bin)),
trash_dmsf_path(project),
title: l(:link_trash_bin),
class: 'icon icon-del',
data: { cy: 'button__trash--dmsf' } %>
<% else %>
<span class="icon icon-del">
<%= sprite_icon('del', l(:link_trash_bin)) %>
</span>
<% end %>
<%= link_to sprite_icon('help', l(:label_help)), dmsf_help_path, class: 'icon icon-help',
onclick: "window.open('#{dmsf_help_url}','_blank', 'width=640,height=960'); return false;" %>

View File

@ -0,0 +1,43 @@
<%
# Redmine plugin for Document Management System "Features"
#
# Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
%>
<li>
<%= context_menu_link sprite_icon('download', l(:button_download)),
entries_operations_dmsf_path(id: project, folder_id: folder, ids: params[:ids],
download_entries: true),
method: :post, class: 'icon icon-download', disabled: project.nil? %>
</li>
<li>
<%= context_menu_link sprite_icon('email', l(:field_mail)),
entries_operations_dmsf_path(id: project, folder_id: folder, ids: params[:ids],
email_entries: true),
method: :post, class: 'icon icon-email', disabled: !email_allowed %>
</li>
<li>
<%= context_menu_link sprite_icon('copy', "#{l(:button_copy)}/#{l(:button_move)}"),
copymove_entries_path(id: project, folder_id: folder, ids: params[:ids], back_url: back_url),
class: 'icon icon-copy', disabled: project.nil? %>
</li>
<li>
<%= context_menu_link sprite_icon('del', l(:button_delete)),
entries_operations_dmsf_path(id: project, folder_id: folder, ids: params[:ids],
delete_entries: true),
method: :post, class: 'icon icon-del', data: { confirm: l(:text_are_you_sure) },
id: 'dmsf-cm-delete', disabled: !allowed %>
</li>

View File

@ -0,0 +1,32 @@
<%
# Redmine plugin for Document Management System "Features"
#
# Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
%>
<li>
<%= context_menu_link sprite_icon('reload', l(:title_restore)),
entries_operations_dmsf_path(id: project, folder_id: folder, ids: params[:ids],
restore_entries: true),
method: :post, class: 'icon icon-cancel', disabled: !allowed_restore %>
</li>
<li>
<%= context_menu_link sprite_icon('del', l(:button_delete)),
entries_operations_dmsf_path(id: project, folder_id: folder, ids: params[:ids],
destroy_entries: true),
method: :post, class: 'icon icon-del', data: { confirm: l(:text_are_you_sure) },
id: 'dmsf-cm-delete', disabled: !allowed_delete %>
</li>

View File

@ -0,0 +1,22 @@
<%
# Redmine plugin for Document Management System "Features"
#
# Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
%>
<li>
<%= render partial: 'dmsf_context_menus/watch', locals: { object: dmsf_project } %>
</li>

View File

@ -0,0 +1,37 @@
<%
# Redmine plugin for Document Management System "Features"
#
# Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
%>
<%= link_to_function sprite_icon('group', l(:title_download_entries)),
"$('#revision_access_#{revision.id}').toggle(); $('.drdn.expanded').removeClass('expanded');",
class: 'icon icon-group dmsf-revision-action-button' %>
<% member = Member.find_by(user_id: User.current.id, project_id: revision.dmsf_file.project.id) %>
<% filename = revision.formatted_name(member) %>
<%= link_to sprite_icon('download', l(:button_download)),
static_dmsf_file_path(file, download: revision, filename: filename),
class: 'icon icon-download', disabled: false %>
<%= link_to sprite_icon('close', l(:title_obsolete_revision)),
obsolete_revision_path(revision),
data: { confirm: l(:text_are_you_sure) },
class: 'icon icon-close dmsf-revision-action-button' if file_manipulation_allowed && (revision.workflow == DmsfWorkflow::STATE_APPROVED) %>
<% if file_delete_allowed && (file.dmsf_file_revisions.visible.count > 1) %>
<%= delete_link delete_revision_path(revision),
{ data: { confirm: l(:text_are_you_sure) },
title: l(:title_delete_revision),
class: 'icon icon-del dmsf-revision-action-button' } %>
<% end %>

View File

@ -0,0 +1,46 @@
<%
# Redmine plugin for Document Management System "Features"
#
# Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
%>
<% if notifications %>
<% if file.notification %>
<%= link_to sprite_icon('email', l(:label_notifications_off)),
notify_deactivate_dmsf_files_path(id: file, back_url: back_url),
title: l(:title_notifications_active_deactivate),
class: 'icon icon-email' %>
<% else %>
<%= link_to sprite_icon('email-disabled', l(:label_notifications_on)),
notify_activate_dmsf_files_path(id: file, back_url: back_url),
title: l(:title_notifications_not_active_activate),
class: 'icon icon-email-add' %>
<% end %>
<% end %>
<%= link_to sprite_icon('link', l(:label_link_to)),
new_dmsf_link_path(project_id: project.id, dmsf_folder_id: file.dmsf_folder ? file.dmsf_folder.id : nil,
dmsf_file_id: file.id, type: 'link_to', back_url: back_url),
title: l(:title_create_link), class: 'icon dmsf-icon-link' %>
<%= context_menu_link sprite_icon('copy', "#{l(:button_copy)}/#{l(:button_move)}"),
copymove_entries_path(id: project, folder_id: file.dmsf_folder, ids: ["file-#{file.id}"],
back_url: back_url), class: 'icon icon-copy' %>
<% member = Member.find_by(user_id: User.current.id, project_id: file.project.id) %>
<% filename = file.last_revision&.formatted_name(member) %>
<%= link_to sprite_icon('download', l(:button_download)),
static_dmsf_file_path(file, filename: filename), class: 'icon icon-download', disabled: false %>
<%= render partial: 'dmsf_context_menus/watch', locals: { object: file } %>
<%= delete_link(dmsf_file_path(id: file, details: true),
back_url: dmsf_folder_path(id: file.project, folder_id: file.dmsf_folder)) if file_delete_allowed %>

View File

@ -0,0 +1,24 @@
<%
# Redmine plugin for Document Management System "Features"
#
# Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
%>
<li>
<%= context_menu_link sprite_icon('del', l(:button_delete)), dmsf_link_path(dmsf_link), method: :delete,
class: 'icon icon-del', data: { confirm: l(:text_are_you_sure) }, id: 'dmsf-cm-delete',
disabled: !allowed %>
</li>

View File

@ -0,0 +1,26 @@
<%
# Redmine plugin for Document Management System "Features"
#
# Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
%>
<% watched = object.watched_by?(User.current) %>
<% css = [watcher_css([object]), watched ? 'icon icon-fav' : 'icon icon-fav-off'].join(' ') %>
<% icon = watched ? 'unwatch' : 'watch' %>
<% text = watched ? l(:button_unwatch) : l(:button_watch) %>
<% url = watch_path(object_type: object.class.to_s.underscore, object_id: object.id) %>
<% method = watched ? 'delete' : 'post' %>
<%= context_menu_link sprite_icon(icon, text), url, method: method, class: css, disabled: !User.current.logged? %>

View File

@ -0,0 +1,48 @@
<%
# Redmine plugin for Document Management System "Features"
#
# Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
%>
<ul>
<% if @dmsf_file %>
<%= render partial: 'file', locals: { project: @project, folder: @folder, dmsf_file: @dmsf_file,
dmsf_link: @dmsf_link, locked: @locked, unlockable: @unlockable,
allowed: @allowed, email_allowed: @email_allowed,
notifications: @notifications, preview: @preview, back_url: @back_url } %>
<% elsif @dmsf_folder %>
<%= render partial: 'folder', locals: { project: @project, folder: @folder, dmsf_folder: @dmsf_folder,
dmsf_link: @dmsf_link, locked: @locked, unlockable: @unlockable,
allowed: @allowed, email_allowed: @email_allowed, edit: false,
notifications: @notifications, back_url: @back_url } %>
<% elsif @dmsf_link %>
<%= render partial: 'url', locals: { dmsf_link: @dmsf_link, allowed: @allowed, back_url: @back_url } %>
<% elsif @dmsf_project %>
<%= render partial: 'project', locals: { dmsf_project: @dmsf_project, allowed: @allowed, back_url: @back_url } %>
<% else %>
<%= render partial: 'multiple', locals: { project: @project, folder: @folder, allowed: @allowed,
email_allowed: @email_allowed, back_url: @back_url } %>
<% end %>
</ul>
<%= javascript_tag do %>
$('#dmsf-cm-delete').click(function (event) {
$('#context-menu').hide();
});
$('#dmsf-cm-workflow').click(function (event) {
$('#context-menu').hide();
});
<% end %>

View File

@ -0,0 +1,38 @@
<%
# Redmine plugin for Document Management System "Features"
#
# Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
%>
<ul>
<% if @dmsf_file || @dmsf_link %>
<%= render(partial: 'file_trash', locals: { project: @project, folder: @folder, dmsf_file: @dmsf_file,
dmsf_link: @dmsf_link, allowed_delete: @allowed_delete,
allowed_restore: @allowed_restore }) %>
<% elsif @dmsf_folder %>
<%= render(partial: 'folder_trash', locals: { project: @project, folder: @folder, dmsf_folder: @dmsf_folder,
dmsf_link: @dmsf_link, allowed_delete: @allowed_delete,
allowed_restore: @allowed_restore }) %>
<% else %>
<%= render(partial: 'multiple_trash', locals: { project: @project, folder: @folder, allowed_delete: @allowed_delete,
allowed_restore: @allowed_restore }) %>
<% end %>
</ul>
<%= javascript_tag do %>
$('#dmsf-cm-delete').click(function (event) {
$('#context-menu').hide();
});
<% end %>

View File

@ -0,0 +1,85 @@
<%
# Redmine plugin for Document Management System "Features"
#
# Vít Jonáš <vit.jonas@gmail.com>, Daniel Munn <dan.munn@munnster.co.uk>, Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
%>
<div id="dmsf_new_revision" class="box tabular">
<strong><%= l(:heading_new_revision) %>
<a href="#" id="new_revision_form_content_toggle" data-cy="toggle__new_revision_from_content--dmsf">[+]</a>
</strong>
<div id="new_revision_form_content">
<% if @file.locked_for_user? %>
<p class="warning"><%= l(:info_file_locked) %></p>
<% else %>
<%= labelled_form_for(@revision, url: { action: 'create_revision', id: @file },
html: { method: :post, multipart: true, id: 'new_revision_form' }) do |f| %>
<%= hidden_field_tag 'back_url', params[:back_url] %>
<div class="splitcontent">
<div class="splitcontentleft">
<p>
<%= f.text_field(:title) %>
</p>
</div>
<div class="splitcontentright">
<p>
<%= f.text_field :name, label: l(:label_file) %>
</p>
</div>
</div>
<p>
<%= f.text_area :description, rows: 6, class: 'wiki-edit dmsf-description' %>
</p>
<div class="splitcontent">
<div class="splitcontentleft">
<p>
<%= render partial: 'dmsf_files/version_selector', locals: {
label_tag_name: 'version',
select_tag_name_patch: 'version_patch',
select_tag_name_minor: 'version_minor',
select_tag_name_major: 'version_major',
revision_or_upload: @file.last_revision } %>
</p>
</div>
<div class="splitcontentright">
<div class="custom_fields">
<% @revision.custom_field_values.each do |value| %>
<% value.value = nil if value.custom_field.dmsf_not_inheritable %>
<p><%= custom_field_tag_with_label(:dmsf_file_revision, value) %></p>
<% end %>
</div>
</div>
</div>
<p>
<%= label_tag 'file_upload', l(:label_new_content) %>
<span class="dmsf-uploader">
<%= render partial: 'dmsf_upload/form',
locals: { multiple: false, container: nil, awf: false } %>
</span>
</p>
<p>
<%= f.text_area :comment, rows: 2, label: l(:label_comment), class: 'wiki-edit dmsf-description' %>
</p>
<div class="form-actions">
<%= f.submit l(:button_create), class: 'button-positive', data: { cy: "button__submit--file_dmsf"} %>
</div>
<% end %>
<% end %>
</div>
</div>
<%= wikitoolbar_for 'dmsf_file_revision_description' %>
<%= wikitoolbar_for 'dmsf_file_revision_comment' %>

View File

@ -0,0 +1,104 @@
<%
# Redmine plugin for Document Management System "Features"
#
# Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
%>
<% cls = link ? 'dmsf-gray' : '' %>
<td class="<%= cls %>">
<% file_view_url = url_for({ controller: :dmsf_files, action: 'view', id: dmsf_file }) %>
<% icon_name = icon_for_mime_type(Redmine::MimeType.css_class_of(dmsf_file.name)) %>
<%= link_to sprite_icon(icon_name, h(link ? link.name : dmsf_file.title)),
file_view_url,
target: '_blank',
rel: 'noopener',
title: h(dmsf_file.last_revision.try(:tooltip)),
'data-downloadurl' => "#{dmsf_file.last_revision.detect_content_type}:#{h(dmsf_file.name)}:#{file_view_url}" %>
</td>
<td class="<%= cls %>">
<span class="size">(<%= number_to_human_size dmsf_file.last_revision.size %>)</span>
<%= link_to '', view_dmsf_file_url(dmsf_file, download: dmsf_file.last_revision, disposition: 'attachment'),
title: l(:title_title_version_version_download, title: h(dmsf_file.last_revision.title),
version: dmsf_file.last_revision.version), class: 'icon icon-download dmsf-revision-action-button' %>
</td>
<td class="<%= cls %>">
<%= dmsf_file.description unless dmsf_file.description.blank? %>
</td>
<td class="<%= cls %>">
<span class="author"><%= dmsf_file.last_revision.user %>, <%= format_time(dmsf_file.last_revision.updated_at) %></span>
</td>
<% # Command icons %>
<td class="<%= cls %>">
<span class="dmsf_upload_select">
<% # Details %>
<% if User.current.allowed_to? :file_manipulation, dmsf_file.project %>
<%= link_to sprite_icon('edit', ''), dmsf_file_path(id: dmsf_file, back_url: issue_path(@issue)),
title: l(:link_details, title: h(dmsf_file.last_revision.title)),
class: 'icon-only icon-edit' %>
<% else %>
<span class="icon-only"></span>
<% end %>
<% # Email %>
<%= link_to sprite_icon('email', ''), entries_operations_dmsf_path(id: dmsf_file.project_id, email_entries: 'email',
ids: ["file-#{dmsf_file.id}"],
back_url: issue_path(@issue)),
method: :post, title: l(:heading_send_documents_by_email), class: 'icon-only icon-email-disabled' %>
<% # Lock %>
<% if !dmsf_file.locked? %>
<%= link_to sprite_icon('lock', ''), lock_dmsf_files_path(id: dmsf_file, back_url: issue_path(@issue)),
title: l(:title_lock_file), class: 'icon-only icon-lock' %>
<% elsif dmsf_file.unlockable? && (!dmsf_file.locked_for_user? || User.current.allowed_to?(:force_file_unlock, dmsf_file.project)) %>
<%= link_to sprite_icon('unlock', ''), unlock_dmsf_files_path(id: dmsf_file, back_url: issue_path(@issue)),
title: dmsf_file.locked_title, class: 'icon-only icon-unlock' %>
<% else %>
<%= content_tag('span', sprite_icon('unlock', ''), title: dmsf_file.locked_title) %>
<% end %>
<% if !dmsf_file.locked? %>
<% # Notifications %>
<% if dmsf_file.notification %>
<%= link_to sprite_icon('email', ''), notify_deactivate_dmsf_files_path(id: dmsf_file,
back_url: issue_path(@issue)),
title: l(:title_notifications_active_deactivate), class: 'icon-only icon-email' %>
<% else %>
<%= link_to sprite_icon('email-disabled', ''), notify_activate_dmsf_files_path(id: dmsf_file,
back_url: issue_path(@issue)),
title: l(:title_notifications_not_active_activate), class: 'icon-only icon-email-add' %>
<% end %>
<% else %>
<span class="icon-only"></span>
<span class="icon-only"></span>
<% end %>
<% # Delete %>
<% if @issue.attributes_editable? &&
((link && User.current.allowed_to?(:file_manipulation, dmsf_file.project)) ||
(!link && User.current.allowed_to?(:file_delete, dmsf_file.project))) %>
<% url = if link
dmsf_link_path(link, commit: 'yes', back_url: issue_path(@issue))
else
dmsf_file_path(id: dmsf_file, commit: 'yes', back_url: issue_path(@issue))
end %>
<%= delete_link url, {}, '' %>
<% end %>
<% # Approval workflow %>
<% wf = DmsfWorkflow.find_by(id: dmsf_file.last_revision.dmsf_workflow_id) if dmsf_file.last_revision.dmsf_workflow_id %>
<%= render partial: 'dmsf_workflows/approval_workflow_button',
locals: { file: dmsf_file,
file_approval_allowed: User.current.allowed_to?(:file_approval, dmsf_file.project),
workflows_available: DmsfWorkflow.where(['project_id = ? OR project_id IS NULL', dmsf_file.project_id]).exists?,
project: dmsf_file.project, wf: wf, dmsf_link_id: nil, back_url: issue_path(@issue) } %>
</span>
</td>

View File

@ -0,0 +1,36 @@
<%
# Redmine plugin for Document Management System "Features"
#
# Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
%>
<% if links.present? %>
<hr>
<p>
<strong><%= l(:menu_dmsf) %></strong>
</p>
<div class="attachments dmsf-parent-container">
<% # DMS documents & links %>
<table>
<% links.each do |dmsf_file, link, _| %>
<tr>
<%= render partial: 'dmsf_files/link', locals: { dmsf_file: dmsf_file, link: link } %>
</tr>
<% end %>
</table>
<%= render partial: 'dmsf_files/thumbnails', locals: { links: links, thumbnails: thumbnails, link_to: true } %>
</div>
<% end %>

Some files were not shown because too many files have changed in this diff Show More