[#123] Added latest edavis10:acts_as_journalized
This commit is contained in:
parent
62c9fd2185
commit
b1845fcfeb
|
@ -0,0 +1,22 @@
|
|||
## MAC OS
|
||||
.DS_Store
|
||||
|
||||
## TEXTMATE
|
||||
*.tmproj
|
||||
tmtags
|
||||
|
||||
## EMACS
|
||||
*~
|
||||
\#*
|
||||
.\#*
|
||||
|
||||
## VIM
|
||||
*.swp
|
||||
|
||||
## PROJECT::GENERAL
|
||||
coverage
|
||||
rdoc
|
||||
pkg
|
||||
|
||||
## PROJECT::SPECIFIC
|
||||
*.db
|
|
@ -0,0 +1,339 @@
|
|||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 2, June 1991
|
||||
|
||||
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The licenses for most software are designed to take away your
|
||||
freedom to share and change it. By contrast, the GNU General Public
|
||||
License is intended to guarantee your freedom to share and change free
|
||||
software--to make sure the software is free for all its users. This
|
||||
General Public License applies to most of the Free Software
|
||||
Foundation's software and to any other program whose authors commit to
|
||||
using it. (Some other Free Software Foundation software is covered by
|
||||
the GNU Lesser General Public License instead.) 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
|
||||
this service 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 make restrictions that forbid
|
||||
anyone to deny you these rights or to ask you to surrender the rights.
|
||||
These restrictions translate to certain responsibilities for you if you
|
||||
distribute copies of the software, or if you modify it.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must give the recipients all the rights that
|
||||
you have. 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.
|
||||
|
||||
We protect your rights with two steps: (1) copyright the software, and
|
||||
(2) offer you this license which gives you legal permission to copy,
|
||||
distribute and/or modify the software.
|
||||
|
||||
Also, for each author's protection and ours, we want to make certain
|
||||
that everyone understands that there is no warranty for this free
|
||||
software. If the software is modified by someone else and passed on, we
|
||||
want its recipients to know that what they have is not the original, so
|
||||
that any problems introduced by others will not reflect on the original
|
||||
authors' reputations.
|
||||
|
||||
Finally, any free program is threatened constantly by software
|
||||
patents. We wish to avoid the danger that redistributors of a free
|
||||
program will individually obtain patent licenses, in effect making the
|
||||
program proprietary. To prevent this, we have made it clear that any
|
||||
patent must be licensed for everyone's free use or not licensed at all.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. This License applies to any program or other work which contains
|
||||
a notice placed by the copyright holder saying it may be distributed
|
||||
under the terms of this General Public License. The "Program", below,
|
||||
refers to any such program or work, and a "work based on the Program"
|
||||
means either the Program or any derivative work under copyright law:
|
||||
that is to say, a work containing the Program or a portion of it,
|
||||
either verbatim or with modifications and/or translated into another
|
||||
language. (Hereinafter, translation is included without limitation in
|
||||
the term "modification".) Each licensee is addressed as "you".
|
||||
|
||||
Activities other than copying, distribution and modification are not
|
||||
covered by this License; they are outside its scope. The act of
|
||||
running the Program is not restricted, and the output from the Program
|
||||
is covered only if its contents constitute a work based on the
|
||||
Program (independent of having been made by running the Program).
|
||||
Whether that is true depends on what the Program does.
|
||||
|
||||
1. You may copy and distribute 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 and disclaimer of warranty; keep intact all the
|
||||
notices that refer to this License and to the absence of any warranty;
|
||||
and give any other recipients of the Program a copy of this License
|
||||
along with the Program.
|
||||
|
||||
You may charge a fee for the physical act of transferring a copy, and
|
||||
you may at your option offer warranty protection in exchange for a fee.
|
||||
|
||||
2. You may modify your copy or copies of the Program or any portion
|
||||
of it, thus forming a work based on the Program, and copy and
|
||||
distribute such modifications or work under the terms of Section 1
|
||||
above, provided that you also meet all of these conditions:
|
||||
|
||||
a) You must cause the modified files to carry prominent notices
|
||||
stating that you changed the files and the date of any change.
|
||||
|
||||
b) You must cause any work that you distribute or publish, that in
|
||||
whole or in part contains or is derived from the Program or any
|
||||
part thereof, to be licensed as a whole at no charge to all third
|
||||
parties under the terms of this License.
|
||||
|
||||
c) If the modified program normally reads commands interactively
|
||||
when run, you must cause it, when started running for such
|
||||
interactive use in the most ordinary way, to print or display an
|
||||
announcement including an appropriate copyright notice and a
|
||||
notice that there is no warranty (or else, saying that you provide
|
||||
a warranty) and that users may redistribute the program under
|
||||
these conditions, and telling the user how to view a copy of this
|
||||
License. (Exception: if the Program itself is interactive but
|
||||
does not normally print such an announcement, your work based on
|
||||
the Program is not required to print an announcement.)
|
||||
|
||||
These requirements apply to the modified work as a whole. If
|
||||
identifiable sections of that work are not derived from the Program,
|
||||
and can be reasonably considered independent and separate works in
|
||||
themselves, then this License, and its terms, do not apply to those
|
||||
sections when you distribute them as separate works. But when you
|
||||
distribute the same sections as part of a whole which is a work based
|
||||
on the Program, the distribution of the whole must be on the terms of
|
||||
this License, whose permissions for other licensees extend to the
|
||||
entire whole, and thus to each and every part regardless of who wrote it.
|
||||
|
||||
Thus, it is not the intent of this section to claim rights or contest
|
||||
your rights to work written entirely by you; rather, the intent is to
|
||||
exercise the right to control the distribution of derivative or
|
||||
collective works based on the Program.
|
||||
|
||||
In addition, mere aggregation of another work not based on the Program
|
||||
with the Program (or with a work based on the Program) on a volume of
|
||||
a storage or distribution medium does not bring the other work under
|
||||
the scope of this License.
|
||||
|
||||
3. You may copy and distribute the Program (or a work based on it,
|
||||
under Section 2) in object code or executable form under the terms of
|
||||
Sections 1 and 2 above provided that you also do one of the following:
|
||||
|
||||
a) Accompany it with the complete corresponding machine-readable
|
||||
source code, which must be distributed under the terms of Sections
|
||||
1 and 2 above on a medium customarily used for software interchange; or,
|
||||
|
||||
b) Accompany it with a written offer, valid for at least three
|
||||
years, to give any third party, for a charge no more than your
|
||||
cost of physically performing source distribution, a complete
|
||||
machine-readable copy of the corresponding source code, to be
|
||||
distributed under the terms of Sections 1 and 2 above on a medium
|
||||
customarily used for software interchange; or,
|
||||
|
||||
c) Accompany it with the information you received as to the offer
|
||||
to distribute corresponding source code. (This alternative is
|
||||
allowed only for noncommercial distribution and only if you
|
||||
received the program in object code or executable form with such
|
||||
an offer, in accord with Subsection b above.)
|
||||
|
||||
The source code for a work means the preferred form of the work for
|
||||
making modifications to it. For an executable work, complete source
|
||||
code means all the source code for all modules it contains, plus any
|
||||
associated interface definition files, plus the scripts used to
|
||||
control compilation and installation of the executable. However, as a
|
||||
special exception, the source code distributed need not include
|
||||
anything that is normally distributed (in either source or binary
|
||||
form) with the major components (compiler, kernel, and so on) of the
|
||||
operating system on which the executable runs, unless that component
|
||||
itself accompanies the executable.
|
||||
|
||||
If distribution of executable or object code is made by offering
|
||||
access to copy from a designated place, then offering equivalent
|
||||
access to copy the source code from the same place counts as
|
||||
distribution of the source code, even though third parties are not
|
||||
compelled to copy the source along with the object code.
|
||||
|
||||
4. You may not copy, modify, sublicense, or distribute the Program
|
||||
except as expressly provided under this License. Any attempt
|
||||
otherwise to copy, modify, sublicense or distribute the Program is
|
||||
void, and will automatically terminate your rights under this License.
|
||||
However, parties who have received copies, or rights, from you under
|
||||
this License will not have their licenses terminated so long as such
|
||||
parties remain in full compliance.
|
||||
|
||||
5. You are not required to accept this License, since you have not
|
||||
signed it. However, nothing else grants you permission to modify or
|
||||
distribute the Program or its derivative works. These actions are
|
||||
prohibited by law if you do not accept this License. Therefore, by
|
||||
modifying or distributing the Program (or any work based on the
|
||||
Program), you indicate your acceptance of this License to do so, and
|
||||
all its terms and conditions for copying, distributing or modifying
|
||||
the Program or works based on it.
|
||||
|
||||
6. Each time you redistribute the Program (or any work based on the
|
||||
Program), the recipient automatically receives a license from the
|
||||
original licensor to copy, distribute or modify the Program subject to
|
||||
these terms and conditions. You may not impose any further
|
||||
restrictions on the recipients' exercise of the rights granted herein.
|
||||
You are not responsible for enforcing compliance by third parties to
|
||||
this License.
|
||||
|
||||
7. If, as a consequence of a court judgment or allegation of patent
|
||||
infringement or for any other reason (not limited to patent issues),
|
||||
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
|
||||
distribute so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you
|
||||
may not distribute the Program at all. For example, if a patent
|
||||
license would not permit royalty-free redistribution of the Program by
|
||||
all those who receive copies directly or indirectly through you, then
|
||||
the only way you could satisfy both it and this License would be to
|
||||
refrain entirely from distribution of the Program.
|
||||
|
||||
If any portion of this section is held invalid or unenforceable under
|
||||
any particular circumstance, the balance of the section is intended to
|
||||
apply and the section as a whole is intended to apply in other
|
||||
circumstances.
|
||||
|
||||
It is not the purpose of this section to induce you to infringe any
|
||||
patents or other property right claims or to contest validity of any
|
||||
such claims; this section has the sole purpose of protecting the
|
||||
integrity of the free software distribution system, which is
|
||||
implemented by public license practices. Many people have made
|
||||
generous contributions to the wide range of software distributed
|
||||
through that system in reliance on consistent application of that
|
||||
system; it is up to the author/donor to decide if he or she is willing
|
||||
to distribute software through any other system and a licensee cannot
|
||||
impose that choice.
|
||||
|
||||
This section is intended to make thoroughly clear what is believed to
|
||||
be a consequence of the rest of this License.
|
||||
|
||||
8. If the distribution and/or use of the Program is restricted in
|
||||
certain countries either by patents or by copyrighted interfaces, the
|
||||
original copyright holder who places the Program under this License
|
||||
may add an explicit geographical distribution limitation excluding
|
||||
those countries, so that distribution is permitted only in or among
|
||||
countries not thus excluded. In such case, this License incorporates
|
||||
the limitation as if written in the body of this License.
|
||||
|
||||
9. The Free Software Foundation may publish revised and/or new versions
|
||||
of the 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 a version number of this License which applies to it and "any
|
||||
later version", you have the option of following the terms and conditions
|
||||
either of that version or of any later version published by the Free
|
||||
Software Foundation. If the Program does not specify a version number of
|
||||
this License, you may choose any version ever published by the Free Software
|
||||
Foundation.
|
||||
|
||||
10. If you wish to incorporate parts of the Program into other free
|
||||
programs whose distribution conditions are different, write to the author
|
||||
to ask for permission. For software which is copyrighted by the Free
|
||||
Software Foundation, write to the Free Software Foundation; we sometimes
|
||||
make exceptions for this. Our decision will be guided by the two goals
|
||||
of preserving the free status of all derivatives of our free software and
|
||||
of promoting the sharing and reuse of software generally.
|
||||
|
||||
NO WARRANTY
|
||||
|
||||
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, 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.
|
||||
|
||||
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
|
||||
REDISTRIBUTE 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.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
convey the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program 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 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program 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 this program; if not, write to the Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program is interactive, make it output a short notice like this
|
||||
when it starts in an interactive mode:
|
||||
|
||||
Gnomovision version 69, Copyright (C) year name of author
|
||||
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, the commands you use may
|
||||
be called something other than `show w' and `show c'; they could even be
|
||||
mouse-clicks or menu items--whatever suits your program.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or your
|
||||
school, if any, to sign a "copyright disclaimer" for the program, if
|
||||
necessary. Here is a sample; alter the names:
|
||||
|
||||
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
|
||||
`Gnomovision' (which makes passes at compilers) written by James Hacker.
|
||||
|
||||
<signature of Ty Coon>, 1 April 1989
|
||||
Ty Coon, President of Vice
|
||||
|
||||
This General Public License does not permit incorporating your program into
|
||||
proprietary programs. If your program is a subroutine library, you may
|
||||
consider it more useful to permit linking proprietary applications with the
|
||||
library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License.
|
|
@ -0,0 +1,87 @@
|
|||
"Acts_as_journalized" is a Redmine core plugin derived from the vestal_versions
|
||||
Ruby on Rails plugin. The parts are under different copyright and license conditions
|
||||
noted below.
|
||||
|
||||
The overall license terms applying to "Acts_as_journalized" as in
|
||||
this distribution are the terms of the GNU General Public License
|
||||
as published by the Free Software Foundation; either version 2 of
|
||||
the License, or (at your option) any later version.
|
||||
|
||||
This program 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 this program; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
|
||||
|
||||
For the individual files, the following copyrights and licenses apply:
|
||||
|
||||
app/controllers/**
|
||||
app/views/**
|
||||
app/helpers/**
|
||||
app/models/journal_observer.rb
|
||||
Copyright (C) 2006-2008 Jean-Philippe Lang
|
||||
|
||||
This program 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 2
|
||||
of the License, or (at your option) any later version.
|
||||
|
||||
This program 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 this program; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
|
||||
lib/acts_as_journalized.rb
|
||||
lib/journal_formatter.rb
|
||||
lib/redmine/acts/journalized/permissions.rb
|
||||
lib/redmine/acts/journalized/save_hooks.rb
|
||||
lib/redmine/acts/journalized/format_hooks.rb
|
||||
lib/redmine/acts/journalized/deprecated.rb
|
||||
Copyright (c) 2010 Finn GmbH
|
||||
|
||||
This program 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 2
|
||||
of the License, or (at your option) any later version.
|
||||
|
||||
This program 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 this program; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
|
||||
All remaining files are:
|
||||
Copyright (c) 2009 Steve Richert
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@ -0,0 +1,3 @@
|
|||
acts as journalized
|
||||
|
||||
A redmine core plugin for unification of journals, events and activities in redmine
|
|
@ -0,0 +1 @@
|
|||
67a8c4bee0a06420f1ba64eb9906a15d63bf5ac5 https://github.com/edavis10/acts_as_journalized
|
|
@ -0,0 +1,45 @@
|
|||
# This file is part of the acts_as_journalized plugin for the redMine
|
||||
# project management software
|
||||
#
|
||||
# Copyright (C) 2006-2008 Jean-Philippe Lang
|
||||
#
|
||||
# This program 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 journal 2
|
||||
# of the License, or (at your option) any later journal.
|
||||
#
|
||||
# This program 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 this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
class JournalsController < ApplicationController
|
||||
unloadable
|
||||
before_filter :find_journal
|
||||
|
||||
def edit
|
||||
if request.post?
|
||||
@journal.update_attribute(:notes, params[:notes]) if params[:notes]
|
||||
@journal.destroy if @journal.details.empty? && @journal.notes.blank?
|
||||
call_hook(:controller_journals_edit_post, { :journal => @journal, :params => params})
|
||||
respond_to do |format|
|
||||
format.html { redirect_to :controller => @journal.journaled.class.name.pluralize.downcase,
|
||||
:action => 'show', :id => @journal.journaled_id }
|
||||
format.js { render :action => 'update' }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def find_journal
|
||||
@journal = Journal.find(params[:id])
|
||||
(render_403; return false) unless @journal.editable_by?(User.current)
|
||||
@project = @journal.project
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
render_404
|
||||
end
|
||||
end
|
|
@ -0,0 +1,128 @@
|
|||
# This file is part of the acts_as_journalized plugin for the redMine
|
||||
# project management software
|
||||
#
|
||||
# Copyright (C) 2006-2008 Jean-Philippe Lang
|
||||
# Copyright (C) 2010 Finn GmbH, http://finn.de
|
||||
#
|
||||
# This program 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 journal 2
|
||||
# of the License, or (at your option) any later journal.
|
||||
#
|
||||
# This program 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 this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
module JournalsHelper
|
||||
unloadable
|
||||
include ApplicationHelper
|
||||
include ActionView::Helpers::TagHelper
|
||||
|
||||
def self.included(base)
|
||||
base.class_eval do
|
||||
if respond_to? :before_filter
|
||||
before_filter :find_optional_journal, :only => [:edit]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def render_journal(model, journal, options = {})
|
||||
return "" if journal.initial?
|
||||
journal_content = render_journal_details(journal, :label_updated_time_by)
|
||||
journal_content += render_notes(model, journal, options) unless journal.notes.blank?
|
||||
content_tag "div", journal_content, { :id => "change-#{journal.id}", :class => journal.css_classes }
|
||||
end
|
||||
|
||||
# This renders a journal entry wiht a header and details
|
||||
def render_journal_details(journal, header_label = :label_updated_time_by)
|
||||
header = <<-HTML
|
||||
<h4>
|
||||
<div style="float:right;">#{link_to "##{journal.anchor}", :anchor => "note-#{journal.anchor}"}</div>
|
||||
#{avatar(journal.user, :size => "24")}
|
||||
#{content_tag('a', '', :name => "note-#{journal.anchor}")}
|
||||
#{authoring journal.created_at, journal.user, :label => header_label}
|
||||
</h4>
|
||||
HTML
|
||||
|
||||
if journal.details.any?
|
||||
details = content_tag "ul", :class => "details" do
|
||||
journal.details.collect do |detail|
|
||||
if d = journal.render_detail(detail)
|
||||
content_tag("li", d)
|
||||
end
|
||||
end.compact
|
||||
end
|
||||
end
|
||||
|
||||
content_tag("div", "#{header}#{details}", :id => "change-#{journal.id}", :class => "journal")
|
||||
end
|
||||
|
||||
def render_notes(model, journal, options={})
|
||||
controller = model.class.name.downcase.pluralize
|
||||
action = 'edit'
|
||||
reply_links = authorize_for(controller, action)
|
||||
|
||||
if User.current.logged?
|
||||
editable = User.current.allowed_to?(options[:edit_permission], journal.project) if options[:edit_permission]
|
||||
if journal.user == User.current && options[:edit_own_permission]
|
||||
editable ||= User.current.allowed_to?(options[:edit_own_permission], journal.project)
|
||||
end
|
||||
end
|
||||
|
||||
unless journal.notes.blank?
|
||||
links = returning [] do |l|
|
||||
if reply_links
|
||||
l << link_to_remote(image_tag('comment.png'), :title => l(:button_quote),
|
||||
:url => {:controller => controller, :action => action, :id => model, :journal_id => journal})
|
||||
end
|
||||
if editable
|
||||
l << link_to_in_place_notes_editor(image_tag('edit.png'), "journal-#{journal.id}-notes",
|
||||
{ :controller => 'journals', :action => 'edit', :id => journal },
|
||||
:title => l(:button_edit))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
content = ''
|
||||
content << content_tag('div', links.join(' '), :class => 'contextual') unless links.empty?
|
||||
content << textilizable(journal, :notes)
|
||||
|
||||
css_classes = "wiki"
|
||||
css_classes << " editable" if editable
|
||||
|
||||
content_tag('div', content, :id => "journal-#{journal.id}-notes", :class => css_classes)
|
||||
end
|
||||
|
||||
def link_to_in_place_notes_editor(text, field_id, url, options={})
|
||||
onclick = "new Ajax.Request('#{url_for(url)}', {asynchronous:true, evalScripts:true, method:'get'}); return false;"
|
||||
link_to text, '#', options.merge(:onclick => onclick)
|
||||
end
|
||||
|
||||
# This may conveniently be used by controllers to find journals referred to in the current request
|
||||
def find_optional_journal
|
||||
@journal = Journal.find_by_id(params[:journal_id])
|
||||
end
|
||||
|
||||
def render_reply(journal)
|
||||
user = journal.user
|
||||
text = journal.notes
|
||||
|
||||
# Replaces pre blocks with [...]
|
||||
text = text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]')
|
||||
content = "#{ll(Setting.default_language, :text_user_wrote, user)}\n> "
|
||||
content << text.gsub(/(\r?\n|\r\n?)/, "\n> ") + "\n\n"
|
||||
|
||||
render(:update) do |page|
|
||||
page << "$('notes').value = \"#{escape_javascript content}\";"
|
||||
page.show 'update'
|
||||
page << "Form.Element.focus('notes');"
|
||||
page << "Element.scrollTo('update');"
|
||||
page << "$('notes').scrollTop = $('notes').scrollHeight - $('notes').clientHeight;"
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,111 @@
|
|||
# This file is part of the acts_as_journalized plugin for the redMine
|
||||
# project management software
|
||||
#
|
||||
# Copyright (c) 2009 Steve Richert
|
||||
# Copyright (c) 2010 Finn GmbH, http://finn.de
|
||||
#
|
||||
# This program 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 journal 2
|
||||
# of the License, or (at your option) any later journal.
|
||||
#
|
||||
# This program 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 this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
require_dependency 'journal_formatter'
|
||||
|
||||
# The ActiveRecord model representing journals.
|
||||
class Journal < ActiveRecord::Base
|
||||
unloadable
|
||||
|
||||
include Comparable
|
||||
include JournalFormatter
|
||||
include JournalDeprecated
|
||||
|
||||
# Make sure each journaled model instance only has unique version ids
|
||||
validates_uniqueness_of :version, :scope => [:journaled_id, :type]
|
||||
belongs_to :journaled
|
||||
belongs_to :user
|
||||
|
||||
# ActiveRecord::Base#changes is an existing method, so before serializing the +changes+ column,
|
||||
# the existing +changes+ method is undefined. The overridden +changes+ method pertained to
|
||||
# dirty attributes, but will not affect the partial updates functionality as that's based on
|
||||
# an underlying +changed_attributes+ method, not +changes+ itself.
|
||||
# undef_method :changes
|
||||
serialize :changes, Hash
|
||||
|
||||
# In conjunction with the included Comparable module, allows comparison of journal records
|
||||
# based on their corresponding version numbers, creation timestamps and IDs.
|
||||
def <=>(other)
|
||||
[version, created_at, id].map(&:to_i) <=> [other.version, other.created_at, other.id].map(&:to_i)
|
||||
end
|
||||
|
||||
# Returns whether the version has a version number of 1. Useful when deciding whether to ignore
|
||||
# the version during reversion, as initial versions have no serialized changes attached. Helps
|
||||
# maintain backwards compatibility.
|
||||
def initial?
|
||||
version < 2
|
||||
end
|
||||
|
||||
# The anchor number for html output
|
||||
def anchor
|
||||
version - 1
|
||||
end
|
||||
|
||||
# Possible shortcut to the associated project
|
||||
def project
|
||||
if journaled.respond_to?(:project)
|
||||
journaled.project
|
||||
elsif journaled.is_a? Project
|
||||
journaled
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def editable_by?(user)
|
||||
journaled.journal_editable_by?(user)
|
||||
end
|
||||
|
||||
def details
|
||||
attributes["changes"] || {}
|
||||
end
|
||||
|
||||
alias_method :changes, :details
|
||||
|
||||
def new_value_for(prop)
|
||||
details[prop.to_s].last if details.keys.include? prop.to_s
|
||||
end
|
||||
|
||||
def old_value_for(prop)
|
||||
details[prop.to_s].first if details.keys.include? prop.to_s
|
||||
end
|
||||
|
||||
# Returns a string of css classes
|
||||
def css_classes
|
||||
s = 'journal'
|
||||
s << ' has-notes' unless notes.blank?
|
||||
s << ' has-details' unless details.empty?
|
||||
s
|
||||
end
|
||||
|
||||
# This is here to allow people to disregard the difference between working with a
|
||||
# Journal and the object it is attached to.
|
||||
# The lookup is as follows:
|
||||
## => Call super if the method corresponds to one of our attributes (will end up in AR::Base)
|
||||
## => Try the journaled object with the same method and arguments
|
||||
## => On error, call super
|
||||
def method_missing(method, *args, &block)
|
||||
return super if attributes[method.to_s]
|
||||
journaled.send(method, *args, &block)
|
||||
rescue NoMethodError => e
|
||||
e.name == method ? super : raise(e)
|
||||
end
|
||||
|
||||
end
|
|
@ -0,0 +1,33 @@
|
|||
# redMine - project management software
|
||||
# Copyright (C) 2006-2007 Jean-Philippe Lang
|
||||
#
|
||||
# This program 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 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program 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 this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
class JournalObserver < ActiveRecord::Observer
|
||||
def after_create(journal)
|
||||
if journal.type == "IssueJournal" and journal.version > 1
|
||||
after_create_issue_journal(journal)
|
||||
end
|
||||
end
|
||||
|
||||
def after_create_issue_journal(journal)
|
||||
if Setting.notified_events.include?('issue_updated') ||
|
||||
(Setting.notified_events.include?('issue_note_added') && journal.notes.present?) ||
|
||||
(Setting.notified_events.include?('issue_status_updated') && journal.new_status.present?) ||
|
||||
(Setting.notified_events.include?('issue_priority_updated') && journal.new_value_for('priority_id').present?)
|
||||
Mailer.deliver_issue_edit(journal)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,8 @@
|
|||
<% form_remote_tag(:url => {}, :html => { :id => "journal-#{@journal.id}-form" }) do %>
|
||||
<%= text_area_tag :notes, @journal.notes, :class => 'wiki-edit',
|
||||
:rows => (@journal.notes.blank? ? 10 : [[10, @journal.notes.length / 50].max, 100].min) %>
|
||||
<%= call_hook(:view_journals_notes_form_after_notes, { :journal => @journal}) %>
|
||||
<p><%= submit_tag l(:button_save) %>
|
||||
<%= link_to l(:button_cancel), '#', :onclick => "Element.remove('journal-#{@journal.id}-form'); " +
|
||||
"Element.show('journal-#{@journal.id}-notes'); return false;" %></p>
|
||||
<% end %>
|
|
@ -0,0 +1,3 @@
|
|||
page.hide "journal-#{@journal.id}-notes"
|
||||
page.insert_html :after, "journal-#{@journal.id}-notes",
|
||||
:partial => 'notes_form'
|
|
@ -0,0 +1,11 @@
|
|||
if @journal.frozen?
|
||||
# journal was destroyed
|
||||
page.remove "change-#{@journal.id}"
|
||||
else
|
||||
page.replace "journal-#{@journal.id}-notes", render_notes(@journal.journaled, @journal,
|
||||
:edit_permission => :edit_issue_notes, :edit_own_permission => :edit_own_issue_notes)
|
||||
page.show "journal-#{@journal.id}-notes"
|
||||
page.remove "journal-#{@journal.id}-form"
|
||||
end
|
||||
|
||||
call_hook(:view_journals_update_rjs_bottom, { :page => page, :journal => @journal })
|
119
vendor/plugins/acts_as_journalized/db/migrate/20100714111651_generalize_journals.rb
vendored
Normal file
119
vendor/plugins/acts_as_journalized/db/migrate/20100714111651_generalize_journals.rb
vendored
Normal file
|
@ -0,0 +1,119 @@
|
|||
class GeneralizeJournals < ActiveRecord::Migration
|
||||
def self.up
|
||||
# This is provided here for migrating up after the JournalDetails has been removed
|
||||
unless Object.const_defined?("JournalDetails")
|
||||
Object.const_set("JournalDetails", Class.new(ActiveRecord::Base))
|
||||
end
|
||||
|
||||
change_table :journals do |t|
|
||||
t.rename :journalized_id, :journaled_id
|
||||
t.rename :created_on, :created_at
|
||||
|
||||
t.integer :version, :default => 0, :null => false
|
||||
t.string :activity_type
|
||||
t.text :changes
|
||||
t.string :type
|
||||
|
||||
t.index :journaled_id
|
||||
t.index :activity_type
|
||||
t.index :created_at
|
||||
t.index :type
|
||||
end
|
||||
|
||||
Journal.all.group_by(&:journaled_id).each_pair do |id, journals|
|
||||
journals.sort_by(&:created_at).each_with_index do |j, idx|
|
||||
j.update_attribute(:type, "#{j.journalized_type}Journal")
|
||||
j.update_attribute(:version, idx + 1)
|
||||
# FIXME: Find some way to choose the right activity here
|
||||
j.update_attribute(:activity_type, j.journalized_type.constantize.activity_provider_options.keys.first)
|
||||
end
|
||||
end
|
||||
|
||||
change_table :journals do |t|
|
||||
t.remove :journalized_type
|
||||
end
|
||||
|
||||
JournalDetails.all.each do |detail|
|
||||
journal = Journal.find(detail.journal_id)
|
||||
changes = journal.changes || {}
|
||||
if detail.property == 'attr' # Standard attributes
|
||||
changes[detail.prop_key.to_s] = [detail.old_value, detail.value]
|
||||
elsif detail.property == 'cf' # Custom fields
|
||||
changes["custom_values_" + detail.prop_key.to_s] = [detail.old_value, detail.value]
|
||||
elsif detail.property == 'attachment' # Attachment
|
||||
changes["attachments_" + detail.prop_key.to_s] = [detail.old_value, detail.value]
|
||||
end
|
||||
journal.update_attribute(:changes, changes.to_yaml)
|
||||
end
|
||||
|
||||
# Create creation journals for all activity providers
|
||||
providers = Redmine::Activity.providers.collect {|k, v| v.collect(&:constantize) }.flatten.compact.uniq
|
||||
providers.each do |p|
|
||||
next unless p.table_exists? # Objects not in the DB yet need creation journal entries
|
||||
p.find(:all).each do |o|
|
||||
unless o.last_journal
|
||||
o.send(:update_journal)
|
||||
created_at = nil
|
||||
[:created_at, :created_on, :updated_at, :updated_on].each do |m|
|
||||
if o.respond_to? m
|
||||
created_at = o.send(m)
|
||||
break
|
||||
end
|
||||
end
|
||||
p "Updating #{o}"
|
||||
o.last_journal.update_attribute(:created_at, created_at) if created_at and o.last_journal
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# drop_table :journal_details
|
||||
end
|
||||
|
||||
def self.down
|
||||
# create_table "journal_details", :force => true do |t|
|
||||
# t.integer "journal_id", :default => 0, :null => false
|
||||
# t.string "property", :limit => 30, :default => "", :null => false
|
||||
# t.string "prop_key", :limit => 30, :default => "", :null => false
|
||||
# t.string "old_value"
|
||||
# t.string "value"
|
||||
# end
|
||||
|
||||
change_table "journals" do |t|
|
||||
t.rename :journaled_id, :journalized_id
|
||||
t.rename :created_at, :created_on
|
||||
|
||||
t.string :journalized_type, :limit => 30, :default => "", :null => false
|
||||
end
|
||||
|
||||
custom_field_names = CustomField.all.group_by(&:type)[IssueCustomField].collect(&:name)
|
||||
Journal.all.each do |j|
|
||||
j.update_attribute(:journalized_type, j.journalized.class.name)
|
||||
# j.changes.each_pair do |prop_key, values|
|
||||
# if Issue.columns.collect(&:name).include? prop_key.to_s
|
||||
# property = :attr
|
||||
# elsif CustomField.find_by_id(prop_key.to_s)
|
||||
# property = :cf
|
||||
# else
|
||||
# property = :attachment
|
||||
# end
|
||||
# JournalDetail.create(:journal_id => j.id, :property => property,
|
||||
# :prop_key => prop_key, :old_value => values.first, :value => values.last)
|
||||
# end
|
||||
end
|
||||
|
||||
change_table "journals" do |t|
|
||||
t.remove_index :journaled_id
|
||||
t.remove_index :activity_type
|
||||
t.remove_index :created_at
|
||||
t.remove_index :type
|
||||
|
||||
t.remove :type
|
||||
t.remove :version
|
||||
t.remove :activity_type
|
||||
t.remove :changes
|
||||
end
|
||||
|
||||
# add_index "journal_details", ["journal_id"], :name => "journal_details_journal_id"
|
||||
# add_index "journals", ["journalized_id", "journalized_type"], :name => "journals_journalized_id"
|
||||
end
|
||||
end
|
|
@ -0,0 +1,49 @@
|
|||
class MergeWikiVersionsWithJournals < ActiveRecord::Migration
|
||||
def self.up
|
||||
# This is provided here for migrating up after the WikiContent::Version class has been removed
|
||||
unless WikiContent.const_defined?("Version")
|
||||
WikiContent.const_set("Version", Class.new(ActiveRecord::Base))
|
||||
end
|
||||
|
||||
WikiContent::Version.find_by_sql("SELECT * FROM wiki_content_versions").each do |wv|
|
||||
journal = WikiContentJournal.create!(:journaled_id => wv.wiki_content_id, :user_id => wv.author_id,
|
||||
:notes => wv.comments, :activity_type => "wiki_edits")
|
||||
changes = {}
|
||||
changes["compression"] = wv.compression
|
||||
changes["data"] = wv.data
|
||||
journal.update_attribute(:changes, changes.to_yaml)
|
||||
journal.update_attribute(:version, wv.version)
|
||||
end
|
||||
# drop_table :wiki_content_versions
|
||||
|
||||
change_table :wiki_contents do |t|
|
||||
t.rename :version, :lock_version
|
||||
end
|
||||
end
|
||||
|
||||
def self.down
|
||||
change_table :wiki_contents do |t|
|
||||
t.rename :lock_version, :version
|
||||
end
|
||||
|
||||
# create_table :wiki_content_versions do |t|
|
||||
# t.column :wiki_content_id, :integer, :null => false
|
||||
# t.column :page_id, :integer, :null => false
|
||||
# t.column :author_id, :integer
|
||||
# t.column :data, :binary
|
||||
# t.column :compression, :string, :limit => 6, :default => ""
|
||||
# t.column :comments, :string, :limit => 255, :default => ""
|
||||
# t.column :updated_on, :datetime, :null => false
|
||||
# t.column :version, :integer, :null => false
|
||||
# end
|
||||
# add_index :wiki_content_versions, :wiki_content_id, :name => :wiki_content_versions_wcid
|
||||
#
|
||||
# WikiContentJournal.all.each do |j|
|
||||
# WikiContent::Version.create(:wiki_content_id => j.journaled_id, :page_id => j.journaled.page_id,
|
||||
# :author_id => j.user_id, :data => j.changes["data"], :compression => j.changes["compression"],
|
||||
# :comments => j.notes, :updated_on => j.created_at, :version => j.version)
|
||||
# end
|
||||
|
||||
WikiContentJournal.destroy_all
|
||||
end
|
||||
end
|
|
@ -0,0 +1,17 @@
|
|||
$LOAD_PATH.unshift File.expand_path("../lib/", __FILE__)
|
||||
|
||||
require "acts_as_journalized"
|
||||
ActiveRecord::Base.send(:include, Redmine::Acts::Journalized)
|
||||
|
||||
require 'dispatcher'
|
||||
Dispatcher.to_prepare do
|
||||
# Model
|
||||
require_dependency "journal"
|
||||
|
||||
# this is for compatibility with current trunk
|
||||
# once the plugin is part of the core, this will not be needed
|
||||
# patches should then be ported onto the core
|
||||
# require_dependency File.dirname(__FILE__) + '/lib/acts_as_journalized/journal_patch'
|
||||
# require_dependency File.dirname(__FILE__) + '/lib/acts_as_journalized/journal_observer_patch'
|
||||
# require_dependency File.dirname(__FILE__) + '/lib/acts_as_journalized/activity_fetcher_patch'
|
||||
end
|
|
@ -0,0 +1,181 @@
|
|||
# This file is part of the acts_as_journalized plugin for the redMine
|
||||
# project management software
|
||||
#
|
||||
# Copyright (C) 2010 Finn GmbH, http://finn.de
|
||||
#
|
||||
# This program 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 journal 2
|
||||
# of the License, or (at your option) any later journal.
|
||||
#
|
||||
# This program 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 this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
|
||||
Dir[File.expand_path("../redmine/acts/journalized/*.rb", __FILE__)].each{|f| require f }
|
||||
require_dependency 'lib/ar_condition'
|
||||
|
||||
module Redmine
|
||||
module Acts
|
||||
module Journalized
|
||||
|
||||
def self.included(base)
|
||||
base.extend ClassMethods
|
||||
base.extend Versioned
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
|
||||
def plural_name
|
||||
self.name.underscore.pluralize
|
||||
end
|
||||
|
||||
# A model might provide as many activity_types as it wishes.
|
||||
# Activities are just different search options for the event a model provides
|
||||
def acts_as_activity(options = {})
|
||||
activity_hash = journalized_activity_hash(options)
|
||||
type = activity_hash[:type]
|
||||
acts_as_activity_provider activity_hash
|
||||
unless Redmine::Activity.providers[type].include? self.name
|
||||
Redmine::Activity.register type.to_sym, :class_name => self.name
|
||||
end
|
||||
end
|
||||
|
||||
# This call will add an activity and, if neccessary, start the journaling and
|
||||
# add an event callback on the model.
|
||||
# Versioning and acting as an Event may only be applied once.
|
||||
# To apply more than on activity, use acts_as_activity
|
||||
def acts_as_journalized(options = {}, &block)
|
||||
activity_hash, event_hash, journal_hash = split_option_hashes(options)
|
||||
|
||||
acts_as_activity(activity_hash)
|
||||
|
||||
return if journaled?
|
||||
|
||||
include Options
|
||||
include Changes
|
||||
include Creation
|
||||
include Users
|
||||
include Reversion
|
||||
include Reset
|
||||
include Reload
|
||||
include Permissions
|
||||
include SaveHooks
|
||||
include FormatHooks
|
||||
|
||||
# FIXME: When the transition to the new API is complete, remove me
|
||||
include Deprecated
|
||||
|
||||
journal_class.acts_as_event journalized_event_hash(event_hash)
|
||||
|
||||
(journal_hash[:except] ||= []) << self.primary_key << inheritance_column <<
|
||||
:updated_on << :updated_at << :lock_version << :lft << :rgt
|
||||
prepare_journaled_options(journal_hash)
|
||||
has_many :journals, journal_hash.merge({:class_name => journal_class.name,
|
||||
:foreign_key => "journaled_id"}), &block
|
||||
end
|
||||
|
||||
def journal_class
|
||||
journal_class_name = "#{name.gsub("::", "_")}Journal"
|
||||
if Object.const_defined?(journal_class_name)
|
||||
Object.const_get(journal_class_name)
|
||||
else
|
||||
Object.const_set(journal_class_name, Class.new(Journal)).tap do |c|
|
||||
# Run after the inherited hook to associate with the parent record.
|
||||
# This eager loads the associated project (for permissions) if possible
|
||||
if project_assoc = reflect_on_association(:project).try(:name)
|
||||
include_option = ", :include => :#{project_assoc.to_s}"
|
||||
end
|
||||
c.class_eval("belongs_to :journaled, :class_name => '#{name}' #{include_option}")
|
||||
c.class_eval("belongs_to :#{name.gsub("::", "_").underscore},
|
||||
:foreign_key => 'journaled_id' #{include_option}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
# Splits an option has into three hashes:
|
||||
## => [{ options prefixed with "activity_" }, { options prefixed with "event_" }, { other options }]
|
||||
def split_option_hashes(options)
|
||||
activity_hash = {}
|
||||
event_hash = {}
|
||||
journal_hash = {}
|
||||
|
||||
options.each_pair do |k, v|
|
||||
case
|
||||
when k.to_s =~ /^activity_(.+)$/
|
||||
activity_hash[$1.to_sym] = v
|
||||
when k.to_s =~ /^event_(.+)$/
|
||||
event_hash[$1.to_sym] = v
|
||||
else
|
||||
journal_hash[k.to_sym] = v
|
||||
end
|
||||
end
|
||||
[activity_hash, event_hash, journal_hash]
|
||||
end
|
||||
|
||||
# Merges the passed activity_hash with the options we require for
|
||||
# acts_as_journalized to work, as follows:
|
||||
# # type is the supplied or the pluralized class name
|
||||
# # timestamp is supplied or the journal's created_at
|
||||
# # author_key will always be the journal's author
|
||||
# #
|
||||
# # find_options are merged as follows:
|
||||
# # # select statement is enriched with the journal fields
|
||||
# # # journal association is added to the includes
|
||||
# # # if a project is associated with the model, this is added to the includes
|
||||
# # # the find conditions are extended to only choose journals which have the proper activity_type
|
||||
# => a valid activity hash
|
||||
def journalized_activity_hash(options)
|
||||
options.tap do |h|
|
||||
h[:type] ||= plural_name
|
||||
h[:timestamp] = "#{journal_class.table_name}.created_at"
|
||||
h[:author_key] = "#{journal_class.table_name}.user_id"
|
||||
|
||||
h[:find_options] ||= {} # in case it is nil
|
||||
h[:find_options] = {}.tap do |opts|
|
||||
cond = ARCondition.new
|
||||
cond.add(["#{journal_class.table_name}.activity_type = ?", h[:type]])
|
||||
cond.add(h[:find_options][:conditions]) if h[:find_options][:conditions]
|
||||
opts[:conditions] = cond.conditions
|
||||
|
||||
include_opts = []
|
||||
include_opts << :project if reflect_on_association(:project)
|
||||
if h[:find_options][:include]
|
||||
include_opts += case h[:find_options][:include]
|
||||
when Array then h[:find_options][:include]
|
||||
else [h[:find_options][:include]]
|
||||
end
|
||||
end
|
||||
include_opts.uniq!
|
||||
opts[:include] = [:journaled => include_opts]
|
||||
|
||||
#opts[:joins] = h[:find_options][:joins] if h[:find_options][:joins]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Merges the event hashes defaults with the options provided by the user
|
||||
# The defaults take their details from the journal
|
||||
def journalized_event_hash(options)
|
||||
unless options.has_key? :url
|
||||
options[:url] = Proc.new do |journal|
|
||||
{ :controller => plural_name,
|
||||
:action => 'show',
|
||||
:id => journal.journaled_id,
|
||||
:anchor => ("note-#{journal.anchor}" unless journal.initial?) }
|
||||
end
|
||||
end
|
||||
{ :description => :notes, :author => :user }.reverse_merge options
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,49 @@
|
|||
# This file is part of the acts_as_journalized plugin for the redMine
|
||||
# project management software
|
||||
#
|
||||
# Copyright (C) 2010 Finn GmbH, http://finn.de
|
||||
#
|
||||
# This program 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 journal 2
|
||||
# of the License, or (at your option) any later journal.
|
||||
#
|
||||
# This program 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 this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
# This module holds the formatting methods that each journal has.
|
||||
# It provides the hooks to apply different formatting to the details
|
||||
# of a specific journal.
|
||||
module JournalDeprecated
|
||||
unloadable
|
||||
# Old timestamps. created_at is what t.timestamps creates in recent Rails journals
|
||||
def created_on
|
||||
created_at
|
||||
end
|
||||
|
||||
# Old naming
|
||||
def journalized
|
||||
journaled
|
||||
end
|
||||
|
||||
# Old naming
|
||||
def journalized= obj
|
||||
journaled = obj
|
||||
end
|
||||
|
||||
|
||||
# Shortcut from more issue-specific journals
|
||||
def attachments
|
||||
journalized.respond_to?(:attachments) ? journalized.attachments : nil
|
||||
end
|
||||
|
||||
# deprecate :created_on => "use #created_at"
|
||||
# deprecate :journalized => "use journaled"
|
||||
# deprecate :attachments => "implement it yourself"
|
||||
end
|
|
@ -0,0 +1,190 @@
|
|||
# This file is part of the acts_as_journalized plugin for the redMine
|
||||
# project management software
|
||||
#
|
||||
# Copyright (C) 2010 Finn GmbH, http://finn.de
|
||||
#
|
||||
# This program 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 journal 2
|
||||
# of the License, or (at your option) any later journal.
|
||||
#
|
||||
# This program 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 this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
# This module holds the formatting methods that each journal has.
|
||||
# It provides the hooks to apply different formatting to the details
|
||||
# of a specific journal.
|
||||
module JournalFormatter
|
||||
unloadable
|
||||
mattr_accessor :formatters, :registered_fields
|
||||
include ApplicationHelper
|
||||
include CustomFieldsHelper
|
||||
include ActionView::Helpers::TagHelper
|
||||
include ActionView::Helpers::UrlHelper
|
||||
extend Redmine::I18n
|
||||
|
||||
def self.register(hash)
|
||||
if hash[:class]
|
||||
klazz = hash.delete(:class)
|
||||
registered_fields[klazz] ||= {}
|
||||
registered_fields[klazz].merge!(hash)
|
||||
else
|
||||
formatters.merge(hash)
|
||||
end
|
||||
end
|
||||
|
||||
# TODO: Document Formatters (can take up to three params, value, journaled, field ...)
|
||||
def self.default_formatters
|
||||
{ :plaintext => (Proc.new {|v,*| v.try(:to_s) }),
|
||||
:datetime => (Proc.new {|v,*| format_date(v.to_date) }),
|
||||
:named_association => (Proc.new do |value, journaled, field|
|
||||
association = journaled.class.reflect_on_association(field.to_sym)
|
||||
if association
|
||||
record = association.class_name.constantize.find_by_id(value.to_i)
|
||||
record.name if record
|
||||
end
|
||||
end),
|
||||
:fraction => (Proc.new {|v,*| "%0.02f" % v.to_f }),
|
||||
:decimal => (Proc.new {|v,*| v.to_i.to_s }),
|
||||
:id => (Proc.new {|v,*| "##{v}" }) }
|
||||
end
|
||||
|
||||
self.formatters = default_formatters
|
||||
self.registered_fields = {}
|
||||
|
||||
def format_attribute_detail(key, values, no_html=false)
|
||||
field = key.to_s.gsub(/\_id$/, "")
|
||||
label = l(("field_" + field).to_sym)
|
||||
|
||||
if format = JournalFormatter.registered_fields[self.class.name.to_sym][key]
|
||||
formatter = JournalFormatter.formatters[format]
|
||||
old_value = formatter.call(values.first, journaled, field) if values.first
|
||||
value = formatter.call(values.last, journaled, field) if values.last
|
||||
[label, old_value, value]
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
def format_custom_value_detail(custom_field, values, no_html)
|
||||
label = custom_field.name
|
||||
old_value = format_value(values.first, custom_field.field_format) if values.first
|
||||
value = format_value(values.last, custom_field.field_format) if values.last
|
||||
|
||||
[label, old_value, value]
|
||||
end
|
||||
|
||||
def format_attachment_detail(key, values, no_html)
|
||||
label = l(:label_attachment)
|
||||
old_value = values.first
|
||||
value = values.last
|
||||
|
||||
[label, old_value, value]
|
||||
end
|
||||
|
||||
def format_html_attachment_detail(key, value)
|
||||
if !value.blank? && a = Attachment.find_by_id(key.to_i)
|
||||
# Link to the attachment if it has not been removed
|
||||
# FIXME: this is broken => link_to_attachment(a)
|
||||
a.filename
|
||||
else
|
||||
content_tag("i", h(value)) if value.present?
|
||||
end
|
||||
end
|
||||
|
||||
def format_html_detail(label, old_value, value)
|
||||
label = content_tag('strong', label)
|
||||
old_value = content_tag("i", h(old_value)) if old_value && !old_value.blank?
|
||||
old_value = content_tag("strike", old_value) if old_value and value.blank?
|
||||
value = content_tag("i", h(value)) if value.present?
|
||||
value ||= ""
|
||||
[label, old_value, value]
|
||||
end
|
||||
|
||||
def property(detail)
|
||||
key = prop_key(detail)
|
||||
if key.start_with? "custom_values"
|
||||
:custom_field
|
||||
elsif key.start_with? "attachments"
|
||||
:attachment
|
||||
elsif journaled.class.columns.collect(&:name).include? key
|
||||
:attribute
|
||||
end
|
||||
end
|
||||
|
||||
def prop_key(detail)
|
||||
if detail.respond_to? :to_ary
|
||||
detail.first
|
||||
else
|
||||
detail
|
||||
end
|
||||
end
|
||||
|
||||
def values(detail)
|
||||
key = prop_key(detail)
|
||||
if detail != key
|
||||
detail.last
|
||||
else
|
||||
details[key.to_s]
|
||||
end
|
||||
end
|
||||
|
||||
def old_value(detail)
|
||||
values(detail).first
|
||||
end
|
||||
|
||||
def value(detail)
|
||||
values(detail).last
|
||||
end
|
||||
|
||||
def render_detail(detail, no_html=false)
|
||||
if detail.respond_to? :to_ary
|
||||
key = detail.first
|
||||
values = detail.last
|
||||
else
|
||||
key = detail
|
||||
values = details[key.to_s]
|
||||
end
|
||||
|
||||
case property(detail)
|
||||
when :attribute
|
||||
attr_detail = format_attribute_detail(key, values, no_html)
|
||||
when :custom_field
|
||||
custom_field = CustomField.find_by_id(key.sub("custom_values", "").to_i)
|
||||
cv_detail = format_custom_value_detail(custom_field, values, no_html)
|
||||
when :attachment
|
||||
attachment_detail = format_attachment_detail(key.sub("attachments", ""), values, no_html)
|
||||
end
|
||||
|
||||
label, old_value, value = attr_detail || cv_detail || attachment_detail
|
||||
Redmine::Hook.call_hook :helper_issues_show_detail_after_setting, {:detail => detail,
|
||||
:label => label, :value => value, :old_value => old_value }
|
||||
return nil unless label || old_value || value # print nothing if there are no values
|
||||
label, old_value, value = [label, old_value, value].collect(&:to_s)
|
||||
|
||||
unless no_html
|
||||
label, old_value, value = *format_html_detail(label, old_value, value)
|
||||
value = format_html_attachment_detail(key.sub("attachments", ""), value) if attachment_detail
|
||||
end
|
||||
|
||||
unless value.blank?
|
||||
if attr_detail || cv_detail
|
||||
unless old_value.blank?
|
||||
l(:text_journal_changed, :label => label, :old => old_value, :new => value)
|
||||
else
|
||||
l(:text_journal_set_to, :label => label, :value => value)
|
||||
end
|
||||
elsif attachment_detail
|
||||
l(:text_journal_added, :label => label, :value => value)
|
||||
end
|
||||
else
|
||||
l(:text_journal_deleted, :label => label, :old => old_value)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,162 @@
|
|||
# This file included as part of the acts_as_journalized plugin for
|
||||
# the redMine project management 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 journal 2
|
||||
# of the License, or (at your option) any later journal.
|
||||
#
|
||||
# This program 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 this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# The original copyright and license conditions are:
|
||||
# Copyright (c) 2009 Steve Richert
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining
|
||||
# a copy of this software and associated documentation files (the
|
||||
# "Software"), to deal in the Software without restriction, including
|
||||
# without limitation the rights to use, copy, modify, merge, publish,
|
||||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
module Redmine::Acts::Journalized
|
||||
# Provides the ability to manipulate hashes in the specific format that ActiveRecord gives to
|
||||
# dirty attribute changes: string keys and unique, two-element array values.
|
||||
module Changes
|
||||
def self.included(base) # :nodoc:
|
||||
Hash.send(:include, HashMethods)
|
||||
|
||||
base.class_eval do
|
||||
include InstanceMethods
|
||||
|
||||
after_update :merge_journal_changes
|
||||
end
|
||||
end
|
||||
|
||||
# Methods available to journaled ActiveRecord::Base instances in order to manage changes used
|
||||
# for journal creation.
|
||||
module InstanceMethods
|
||||
# Collects an array of changes from a record's journals between the given range and compiles
|
||||
# them into one summary hash of changes. The +from+ and +to+ arguments can each be either a
|
||||
# version number, a symbol representing an association proxy method, a string representing a
|
||||
# journal tag or a journal object itself.
|
||||
def changes_between(from, to)
|
||||
from_number, to_number = journals.journal_at(from), journals.journal_at(to)
|
||||
return {} if from_number == to_number
|
||||
chain = journals.between(from_number, to_number).reject(&:initial?)
|
||||
return {} if chain.empty?
|
||||
|
||||
backward = from_number > to_number
|
||||
backward ? chain.pop : chain.shift unless from_number == 1 || to_number == 1
|
||||
|
||||
chain.inject({}) do |changes, journal|
|
||||
changes.append_changes!(backward ? journal.changes.reverse_changes : journal.changes)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
# Before a new journal is created, the newly-changed attributes are appended onto a hash
|
||||
# of previously-changed attributes. Typically the previous changes will be empty, except in
|
||||
# the case that a control block is used where journals are to be merged. See
|
||||
# VestalVersions::Control for more information.
|
||||
def merge_journal_changes
|
||||
journal_changes.append_changes!(incremental_journal_changes)
|
||||
end
|
||||
|
||||
# Stores the cumulative changes that are eventually used for journal creation.
|
||||
def journal_changes
|
||||
@journal_changes ||= {}
|
||||
end
|
||||
|
||||
# Stores the incremental changes that are appended to the cumulative changes before journal
|
||||
# creation. Incremental changes are reset when the record is saved because they represent
|
||||
# a subset of the dirty attribute changes, which are reset upon save.
|
||||
def incremental_journal_changes
|
||||
changes.slice(*journaled_columns)
|
||||
end
|
||||
|
||||
# Simply resets the cumulative changes after journal creation.
|
||||
def reset_journal_changes
|
||||
@journal_changes = nil
|
||||
end
|
||||
end
|
||||
|
||||
# Instance methods included into Hash for dealing with manipulation of hashes in the specific
|
||||
# format of ActiveRecord::Base#changes.
|
||||
module HashMethods
|
||||
# When called on a hash of changes and given a second hash of changes as an argument,
|
||||
# +append_changes+ will run the second hash on top of the first, updating the last element
|
||||
# of each array value with its own, or creating its own key/value pair for missing keys.
|
||||
# Resulting non-unique array values are removed.
|
||||
#
|
||||
# == Example
|
||||
#
|
||||
# first = {
|
||||
# "first_name" => ["Steve", "Stephen"],
|
||||
# "age" => [25, 26]
|
||||
# }
|
||||
# second = {
|
||||
# "first_name" => ["Stephen", "Steve"],
|
||||
# "last_name" => ["Richert", "Jobs"],
|
||||
# "age" => [26, 54]
|
||||
# }
|
||||
# first.append_changes(second)
|
||||
# # => {
|
||||
# "last_name" => ["Richert", "Jobs"],
|
||||
# "age" => [25, 54]
|
||||
# }
|
||||
def append_changes(changes)
|
||||
changes.inject(self) do |new_changes, (attribute, change)|
|
||||
new_change = [new_changes.fetch(attribute, change).first, change.last]
|
||||
new_changes.merge(attribute => new_change)
|
||||
end.reject do |attribute, change|
|
||||
change.first == change.last
|
||||
end
|
||||
end
|
||||
|
||||
# Destructively appends a given hash of changes onto an existing hash of changes.
|
||||
def append_changes!(changes)
|
||||
replace(append_changes(changes))
|
||||
end
|
||||
|
||||
# Appends the existing hash of changes onto a given hash of changes. Relates to the
|
||||
# +append_changes+ method in the same way that Hash#reverse_merge relates to
|
||||
# Hash#merge.
|
||||
def prepend_changes(changes)
|
||||
changes.append_changes(self)
|
||||
end
|
||||
|
||||
# Destructively prepends a given hash of changes onto an existing hash of changes.
|
||||
def prepend_changes!(changes)
|
||||
replace(prepend_changes(changes))
|
||||
end
|
||||
|
||||
# Reverses the array values of a hash of changes. Useful for rejournal both backward and
|
||||
# forward through a record's history of changes.
|
||||
def reverse_changes
|
||||
inject({}){|nc,(a,c)| nc.merge!(a => c.reverse) }
|
||||
end
|
||||
|
||||
# Destructively reverses the array values of a hash of changes.
|
||||
def reverse_changes!
|
||||
replace(reverse_changes)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
77
vendor/plugins/acts_as_journalized/lib/redmine/acts/journalized/configuration.rb
vendored
Normal file
77
vendor/plugins/acts_as_journalized/lib/redmine/acts/journalized/configuration.rb
vendored
Normal file
|
@ -0,0 +1,77 @@
|
|||
# This file included as part of the acts_as_journalized plugin for
|
||||
# the redMine project management 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 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program 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 this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# The original copyright and license conditions are:
|
||||
# Copyright (c) 2009 Steve Richert
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining
|
||||
# a copy of this software and associated documentation files (the
|
||||
# "Software"), to deal in the Software without restriction, including
|
||||
# without limitation the rights to use, copy, modify, merge, publish,
|
||||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
module Redmine::Acts::Journalized
|
||||
# Allows for easy application-wide configuration of options passed into the +journaled+ method.
|
||||
module Configuration
|
||||
# The VestalVersions module is extended by VestalVersions::Configuration, allowing the
|
||||
# +configure method+ to be used as follows in a Rails initializer:
|
||||
#
|
||||
# VestalVersions.configure do |config|
|
||||
# config.class_name = "MyCustomVersion"
|
||||
# config.dependent = :destroy
|
||||
# end
|
||||
#
|
||||
# Each variable assignment in the +configure+ block corresponds directly with the options
|
||||
# available to the +journaled+ method. Assigning common options in an initializer can keep your
|
||||
# models tidy.
|
||||
#
|
||||
# If an option is given in both an initializer and in the options passed to +journaled+, the
|
||||
# value given in the model itself will take precedence.
|
||||
def configure
|
||||
yield Configuration
|
||||
end
|
||||
|
||||
class << self
|
||||
# Simply stores a hash of options given to the +configure+ block.
|
||||
def options
|
||||
@options ||= {}
|
||||
end
|
||||
|
||||
# If given a setter method name, will assign the first argument to the +options+ hash with
|
||||
# the method name (sans "=") as the key. If given a getter method name, will attempt to
|
||||
# a value from the +options+ hash for that key. If the key doesn't exist, defers to +super+.
|
||||
def method_missing(symbol, *args)
|
||||
if (method = symbol.to_s).sub!(/\=$/, '')
|
||||
options[method.to_sym] = args.first
|
||||
else
|
||||
options.fetch(method.to_sym, super)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
127
vendor/plugins/acts_as_journalized/lib/redmine/acts/journalized/creation.rb
vendored
Normal file
127
vendor/plugins/acts_as_journalized/lib/redmine/acts/journalized/creation.rb
vendored
Normal file
|
@ -0,0 +1,127 @@
|
|||
# This file included as part of the acts_as_journalized plugin for
|
||||
# the redMine project management 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 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program 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 this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# The original copyright and license conditions are:
|
||||
# Copyright (c) 2009 Steve Richert
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining
|
||||
# a copy of this software and associated documentation files (the
|
||||
# "Software"), to deal in the Software without restriction, including
|
||||
# without limitation the rights to use, copy, modify, merge, publish,
|
||||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
module Redmine::Acts::Journalized
|
||||
# Adds the functionality necessary to control journal creation on a journaled instance of
|
||||
# ActiveRecord::Base.
|
||||
module Creation
|
||||
def self.included(base) # :nodoc:
|
||||
base.class_eval do
|
||||
extend ClassMethods
|
||||
include InstanceMethods
|
||||
|
||||
after_save :create_journal, :if => :create_journal?
|
||||
|
||||
class << self
|
||||
alias_method_chain :prepare_journaled_options, :creation
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Class methods added to ActiveRecord::Base to facilitate the creation of new journals.
|
||||
module ClassMethods
|
||||
# Overrides the basal +prepare_journaled_options+ method defined in VestalVersions::Options
|
||||
# to extract the <tt>:only</tt> and <tt>:except</tt> options into +vestal_journals_options+.
|
||||
def prepare_journaled_options_with_creation(options)
|
||||
result = prepare_journaled_options_without_creation(options)
|
||||
|
||||
self.vestal_journals_options[:only] = Array(options.delete(:only)).map(&:to_s).uniq if options[:only]
|
||||
self.vestal_journals_options[:except] = Array(options.delete(:except)).map(&:to_s).uniq if options[:except]
|
||||
|
||||
result
|
||||
end
|
||||
end
|
||||
|
||||
# Instance methods that determine whether to save a journal and actually perform the save.
|
||||
module InstanceMethods
|
||||
private
|
||||
# Returns whether a new journal should be created upon updating the parent record.
|
||||
# A new journal will be created if
|
||||
# a) attributes have changed
|
||||
# b) no previous journal exists
|
||||
# c) journal notes were added
|
||||
# d) the parent record is already saved
|
||||
def create_journal?
|
||||
update_journal
|
||||
(journal_changes.present? or journal_notes.present? or journals.empty?) and !new_record?
|
||||
end
|
||||
|
||||
# Creates a new journal upon updating the parent record.
|
||||
# "update_journal" has been called in "update_journal?" at this point (to get a hold on association changes)
|
||||
# It must not be called again here.
|
||||
def create_journal
|
||||
journals << self.class.journal_class.create(journal_attributes)
|
||||
reset_journal_changes
|
||||
reset_journal
|
||||
true
|
||||
rescue Exception => e # FIXME: What to do? This likely means that the parent record is invalid!
|
||||
p e
|
||||
p e.message
|
||||
p e.backtrace
|
||||
false
|
||||
end
|
||||
|
||||
# Returns an array of column names that should be included in the changes of created
|
||||
# journals. If <tt>vestal_journals_options[:only]</tt> is specified, only those columns
|
||||
# will be journaled. Otherwise, if <tt>vestal_journals_options[:except]</tt> is specified,
|
||||
# all columns will be journaled other than those specified. Without either option, the
|
||||
# default is to journal all columns. At any rate, the four "automagic" timestamp columns
|
||||
# maintained by Rails are never journaled.
|
||||
def journaled_columns
|
||||
case
|
||||
when vestal_journals_options[:only] then self.class.column_names & vestal_journals_options[:only]
|
||||
when vestal_journals_options[:except] then self.class.column_names - vestal_journals_options[:except]
|
||||
else self.class.column_names
|
||||
end - %w(created_at updated_at)
|
||||
end
|
||||
|
||||
# Returns the activity type. Should be overridden in the journalized class to offer
|
||||
# multiple types
|
||||
def activity_type
|
||||
self.class.name.underscore.pluralize
|
||||
end
|
||||
|
||||
# Specifies the attributes used during journal creation. This is separated into its own
|
||||
# method so that it can be overridden by the VestalVersions::Users feature.
|
||||
def journal_attributes
|
||||
attributes = { :journaled_id => self.id, :activity_type => activity_type,
|
||||
:changes => journal_changes, :version => last_version + 1,
|
||||
:notes => journal_notes, :user_id => (journal_user.try(:id) || User.current) }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
68
vendor/plugins/acts_as_journalized/lib/redmine/acts/journalized/deprecated.rb
vendored
Normal file
68
vendor/plugins/acts_as_journalized/lib/redmine/acts/journalized/deprecated.rb
vendored
Normal file
|
@ -0,0 +1,68 @@
|
|||
# This file is part of the acts_as_journalized plugin for the redMine
|
||||
# project management software
|
||||
#
|
||||
# Copyright (C) 2010 Finn GmbH, http://finn.de
|
||||
#
|
||||
# This program 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 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program 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 this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
# These hooks make sure journals are properly created and updated with Redmine user detail,
|
||||
# notes and associated custom fields
|
||||
|
||||
module Redmine::Acts::Journalized
|
||||
module Deprecated
|
||||
# Old mailer API
|
||||
def recipients
|
||||
notified = project.notified_users
|
||||
notified.reject! {|user| !visible?(user)}
|
||||
notified.collect(&:mail)
|
||||
end
|
||||
|
||||
def current_journal
|
||||
last_journal
|
||||
end
|
||||
|
||||
# FIXME: When the new API is settled, remove me
|
||||
Redmine::Acts::Event::InstanceMethods.instance_methods(false).each do |m|
|
||||
if m.start_with? "event_"
|
||||
class_eval(<<-RUBY, __FILE__, __LINE__)
|
||||
def #{m}
|
||||
if last_journal.nil?
|
||||
begin
|
||||
journals << self.class.journal_class.create(journal_attributes)
|
||||
reset_journal_changes
|
||||
reset_journal
|
||||
true
|
||||
rescue Exception => e # FIXME: What to do? This likely means that the parent record is invalid!
|
||||
p e
|
||||
p e.message
|
||||
p e.backtrace
|
||||
false
|
||||
end
|
||||
journals.reload
|
||||
end
|
||||
return last_journal.#{m}
|
||||
end
|
||||
RUBY
|
||||
end
|
||||
end
|
||||
|
||||
def event_url(options = {})
|
||||
last_journal.event_url(options)
|
||||
end
|
||||
|
||||
# deprecate :recipients => "use #last_journal.recipients"
|
||||
# deprecate :current_journal => "use #last_journal"
|
||||
end
|
||||
end
|
42
vendor/plugins/acts_as_journalized/lib/redmine/acts/journalized/format_hooks.rb
vendored
Normal file
42
vendor/plugins/acts_as_journalized/lib/redmine/acts/journalized/format_hooks.rb
vendored
Normal file
|
@ -0,0 +1,42 @@
|
|||
# This file is part of the acts_as_journalized plugin for the redMine
|
||||
# project management software
|
||||
#
|
||||
# Copyright (C) 2010 Finn GmbH, http://finn.de
|
||||
#
|
||||
# This program 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 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program 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 this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
module Redmine::Acts::Journalized
|
||||
module FormatHooks
|
||||
def self.included(base)
|
||||
base.extend ClassMethods
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
# Shortcut to register a formatter for a number of fields
|
||||
def register_on_journal_formatter(formatter, *field_names)
|
||||
formatter = formatter.to_sym
|
||||
field_names.collect(&:to_s).each do |field|
|
||||
JournalFormatter.register :class => self.journal_class.name.to_sym, field => formatter
|
||||
end
|
||||
end
|
||||
|
||||
# Shortcut to register a new proc as a named formatter. Overwrites
|
||||
# existing formatters with the same name
|
||||
def register_journal_formatter(formatter)
|
||||
JournalFormatter.register formatter.to_sym => Proc.new
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,81 @@
|
|||
# This file included as part of the acts_as_journalized plugin for
|
||||
# the redMine project management 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 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program 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 this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# The original copyright and license conditions are:
|
||||
# Copyright (c) 2009 Steve Richert
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining
|
||||
# a copy of this software and associated documentation files (the
|
||||
# "Software"), to deal in the Software without restriction, including
|
||||
# without limitation the rights to use, copy, modify, merge, publish,
|
||||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
module Redmine::Acts::Journalized
|
||||
# Provides +journaled+ options conjournal and cleanup.
|
||||
module Options
|
||||
def self.included(base) # :nodoc:
|
||||
base.class_eval do
|
||||
extend ClassMethods
|
||||
end
|
||||
end
|
||||
|
||||
# Class methods that provide preparation of options passed to the +journaled+ method.
|
||||
module ClassMethods
|
||||
# The +prepare_journaled_options+ method has three purposes:
|
||||
# 1. Populate the provided options with default values where needed
|
||||
# 2. Prepare options for use with the +has_many+ association
|
||||
# 3. Save user-configurable options in a class-level variable
|
||||
#
|
||||
# Options are given priority in the following order:
|
||||
# 1. Those passed directly to the +journaled+ method
|
||||
# 2. Those specified in an initializer +configure+ block
|
||||
# 3. Default values specified in +prepare_journaled_options+
|
||||
#
|
||||
# The method is overridden in feature modules that require specific options outside the
|
||||
# standard +has_many+ associations.
|
||||
def prepare_journaled_options(options)
|
||||
options.symbolize_keys!
|
||||
options.reverse_merge!(Configuration.options)
|
||||
options.reverse_merge!(
|
||||
:class_name => 'Journal',
|
||||
:dependent => :delete_all
|
||||
)
|
||||
options.reverse_merge!(
|
||||
:order => "#{options[:class_name].constantize.table_name}.version ASC"
|
||||
)
|
||||
|
||||
class_inheritable_accessor :vestal_journals_options
|
||||
self.vestal_journals_options = options.dup
|
||||
|
||||
options.merge!(
|
||||
:extend => Array(options[:extend]).unshift(Versions)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
36
vendor/plugins/acts_as_journalized/lib/redmine/acts/journalized/permissions.rb
vendored
Normal file
36
vendor/plugins/acts_as_journalized/lib/redmine/acts/journalized/permissions.rb
vendored
Normal file
|
@ -0,0 +1,36 @@
|
|||
# This file is part of the acts_as_journalized plugin for the redMine
|
||||
# project management software
|
||||
#
|
||||
# Copyright (C) 2010 Finn GmbH, http://finn.de
|
||||
#
|
||||
# This program 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 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program 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 this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
module Redmine::Acts::Journalized
|
||||
module Permissions
|
||||
# Default implementation of journal editing permission
|
||||
# Is overridden if defined in the journalized model directly
|
||||
def journal_editable_by?(user)
|
||||
return true if user.admin?
|
||||
if respond_to? :editable_by?
|
||||
editable_by? user
|
||||
else
|
||||
permission = :"edit_#{self.class.to_s.pluralize.downcase}"
|
||||
p = @project || (project if respond_to? :project)
|
||||
options = { :global => p.present? }
|
||||
user.allowed_to? permission, p, options
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,60 @@
|
|||
# This file included as part of the acts_as_journalized plugin for
|
||||
# the redMine project management 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 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program 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 this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# The original copyright and license conditions are:
|
||||
# Copyright (c) 2009 Steve Richert
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining
|
||||
# a copy of this software and associated documentation files (the
|
||||
# "Software"), to deal in the Software without restriction, including
|
||||
# without limitation the rights to use, copy, modify, merge, publish,
|
||||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
module Redmine::Acts::Journalized
|
||||
# Ties into the existing ActiveRecord::Base#reload method to ensure that journal information
|
||||
# is properly reset.
|
||||
module Reload
|
||||
def self.included(base) # :nodoc:
|
||||
base.class_eval do
|
||||
include InstanceMethods
|
||||
|
||||
alias_method_chain :reload, :journals
|
||||
end
|
||||
end
|
||||
|
||||
# Adds instance methods into ActiveRecord::Base to tap into the +reload+ method.
|
||||
module InstanceMethods
|
||||
# Overrides ActiveRecord::Base#reload, resetting the instance-variable-cached journal number
|
||||
# before performing the original +reload+ method.
|
||||
def reload_with_journals(*args)
|
||||
reset_journal
|
||||
reload_without_journals(*args)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,65 @@
|
|||
# This file included as part of the acts_as_journalized plugin for
|
||||
# the redMine project management 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 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program 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 this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# The original copyright and license conditions are:
|
||||
# Copyright (c) 2009 Steve Richert
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining
|
||||
# a copy of this software and associated documentation files (the
|
||||
# "Software"), to deal in the Software without restriction, including
|
||||
# without limitation the rights to use, copy, modify, merge, publish,
|
||||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
module Redmine::Acts::Journalized
|
||||
# Adds the ability to "reset" (or hard revert) a journaled ActiveRecord::Base instance.
|
||||
module Reset
|
||||
def self.included(base) # :nodoc:
|
||||
base.class_eval do
|
||||
include InstanceMethods
|
||||
end
|
||||
end
|
||||
|
||||
# Adds the instance methods required to reset an object to a previous journal.
|
||||
module InstanceMethods
|
||||
# Similar to +revert_to!+, the +reset_to!+ method reverts an object to a previous journal,
|
||||
# only instead of creating a new record in the journal history, +reset_to!+ deletes all of
|
||||
# the journal history that occurs after the journal reverted to.
|
||||
#
|
||||
# The action taken on each journal record after the point of rejournal is determined by the
|
||||
# <tt>:dependent</tt> option given to the +journaled+ method. See the +journaled+ method
|
||||
# documentation for more details.
|
||||
def reset_to!(value)
|
||||
if saved = skip_journal{ revert_to!(value) }
|
||||
journals.send(:delete_records, journals.after(value))
|
||||
reset_journal
|
||||
end
|
||||
saved
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
110
vendor/plugins/acts_as_journalized/lib/redmine/acts/journalized/reversion.rb
vendored
Normal file
110
vendor/plugins/acts_as_journalized/lib/redmine/acts/journalized/reversion.rb
vendored
Normal file
|
@ -0,0 +1,110 @@
|
|||
# This file included as part of the acts_as_journalized plugin for
|
||||
# the redMine project management 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 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program 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 this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# The original copyright and license conditions are:
|
||||
# Copyright (c) 2009 Steve Richert
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining
|
||||
# a copy of this software and associated documentation files (the
|
||||
# "Software"), to deal in the Software without restriction, including
|
||||
# without limitation the rights to use, copy, modify, merge, publish,
|
||||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
module Redmine::Acts::Journalized
|
||||
# Enables versioned ActiveRecord::Base instances to revert to a previously saved version.
|
||||
module Reversion
|
||||
def self.included(base) # :nodoc:
|
||||
base.class_eval do
|
||||
include InstanceMethods
|
||||
end
|
||||
end
|
||||
|
||||
# Provides the base instance methods required to revert a journaled instance.
|
||||
module InstanceMethods
|
||||
# Returns the current version number for the versioned object.
|
||||
def version
|
||||
@version ||= last_version
|
||||
end
|
||||
|
||||
def last_journal
|
||||
journals.last
|
||||
end
|
||||
|
||||
# Accepts a value corresponding to a specific journal record, builds a history of changes
|
||||
# between that journal and the current journal, and then iterates over that history updating
|
||||
# the object's attributes until the it's reverted to its prior state.
|
||||
#
|
||||
# The single argument should adhere to one of the formats as documented in the +at+ method of
|
||||
# VestalVersions::Versions.
|
||||
#
|
||||
# After the object is reverted to the target journal, it is not saved. In order to save the
|
||||
# object after the rejournal, use the +revert_to!+ method.
|
||||
#
|
||||
# The journal number of the object will reflect whatever journal has been reverted to, and
|
||||
# the return value of the +revert_to+ method is also the target journal number.
|
||||
def revert_to(value)
|
||||
to_number = journals.journal_at(value)
|
||||
|
||||
changes_between(journal, to_number).each do |attribute, change|
|
||||
write_attribute(attribute, change.last)
|
||||
end
|
||||
|
||||
reset_journal(to_number)
|
||||
end
|
||||
|
||||
# Behaves similarly to the +revert_to+ method except that it automatically saves the record
|
||||
# after the rejournal. The return value is the success of the save.
|
||||
def revert_to!(value)
|
||||
revert_to(value)
|
||||
reset_journal if saved = save
|
||||
saved
|
||||
end
|
||||
|
||||
# Returns a boolean specifying whether the object has been reverted to a previous journal or
|
||||
# if the object represents the latest journal in the journal history.
|
||||
def reverted?
|
||||
version != last_version
|
||||
end
|
||||
|
||||
private
|
||||
# Returns the number of the last created journal in the object's journal history.
|
||||
#
|
||||
# If no associated journals exist, the object is considered at version 0.
|
||||
def last_version
|
||||
@last_version ||= journals.maximum(:version) || 0
|
||||
end
|
||||
|
||||
# Clears the cached version number instance variables so that they can be recalculated.
|
||||
# Useful after a new version is created.
|
||||
def reset_journal(version = nil)
|
||||
@last_version = nil if version.nil?
|
||||
@version = version
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
115
vendor/plugins/acts_as_journalized/lib/redmine/acts/journalized/save_hooks.rb
vendored
Normal file
115
vendor/plugins/acts_as_journalized/lib/redmine/acts/journalized/save_hooks.rb
vendored
Normal file
|
@ -0,0 +1,115 @@
|
|||
# This file is part of the acts_as_journalized plugin for the redMine
|
||||
# project management software
|
||||
#
|
||||
# Copyright (C) 2010 Finn GmbH, http://finn.de
|
||||
#
|
||||
# This program 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 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program 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 this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
# These hooks make sure journals are properly created and updated with Redmine user detail,
|
||||
# notes and associated custom fields
|
||||
module Redmine::Acts::Journalized
|
||||
module SaveHooks
|
||||
def self.included(base)
|
||||
base.extend ClassMethods
|
||||
|
||||
base.class_eval do
|
||||
before_save :init_journal
|
||||
after_save :reset_instance_variables
|
||||
|
||||
attr_reader :journal_notes, :journal_user
|
||||
end
|
||||
end
|
||||
|
||||
# Saves the current custom values, notes and journal to include them in the next journal
|
||||
# Called before save
|
||||
def init_journal(user = User.current, notes = "")
|
||||
@journal_notes ||= notes
|
||||
@journal_user ||= user
|
||||
@associations_before_save ||= {}
|
||||
|
||||
@associations = {}
|
||||
save_possible_association :custom_values, :key => :custom_field_id, :value => :value
|
||||
save_possible_association :attachments, :key => :id, :value => :filename
|
||||
|
||||
@current_journal ||= last_journal
|
||||
end
|
||||
|
||||
# Saves the notes and custom value changes in the last Journal
|
||||
# Called before create_journal
|
||||
def update_journal
|
||||
unless (@associations || {}).empty?
|
||||
changed_associations = {}
|
||||
changed_associations.merge!(possibly_updated_association :custom_values)
|
||||
changed_associations.merge!(possibly_updated_association :attachments)
|
||||
end
|
||||
|
||||
unless changed_associations.blank?
|
||||
update_extended_journal_contents(changed_associations)
|
||||
end
|
||||
end
|
||||
|
||||
def reset_instance_variables
|
||||
if last_journal != @current_journal
|
||||
if last_journal.user != @journal_user
|
||||
last_journal.update_attribute(:user_id, @journal_user.id)
|
||||
end
|
||||
end
|
||||
@associations_before_save = @current_journal = @journal_notes = @journal_user = nil
|
||||
end
|
||||
|
||||
def save_possible_association(method, options)
|
||||
@associations[method] = options
|
||||
if self.respond_to? method
|
||||
@associations_before_save[method] ||= send(method).inject({}) do |hash, cv|
|
||||
hash[cv.send(options[:key])] = cv.send(options[:value])
|
||||
hash
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def possibly_updated_association(method)
|
||||
if @associations_before_save[method]
|
||||
# Has custom values from init_journal_notes
|
||||
return changed_associations(method, @associations_before_save[method])
|
||||
end
|
||||
{}
|
||||
end
|
||||
|
||||
# Saves the notes and changed custom values to the journal
|
||||
# Creates a new journal, if no immediate attributes were changed
|
||||
def update_extended_journal_contents(changed_associations)
|
||||
journal_changes.merge!(changed_associations)
|
||||
end
|
||||
|
||||
def changed_associations(method, previous)
|
||||
send(method).reload # Make sure the associations are reloaded
|
||||
send(method).inject({}) do |hash, c|
|
||||
key = c.send(@associations[method][:key])
|
||||
new_value = c.send(@associations[method][:value])
|
||||
|
||||
if previous[key].blank? && new_value.blank?
|
||||
# The key was empty before, don't add a blank value
|
||||
elsif previous[key] != new_value
|
||||
# The key's value changed
|
||||
hash["#{method}#{key}"] = [previous[key], new_value]
|
||||
end
|
||||
hash
|
||||
end
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,86 @@
|
|||
# This file included as part of the acts_as_journalized plugin for
|
||||
# the redMine project management 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 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program 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 this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# The original copyright and license conditions are:
|
||||
# Copyright (c) 2009 Steve Richert
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining
|
||||
# a copy of this software and associated documentation files (the
|
||||
# "Software"), to deal in the Software without restriction, including
|
||||
# without limitation the rights to use, copy, modify, merge, publish,
|
||||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
module Redmine::Acts::Journalized
|
||||
# Provides a way for information to be associated with specific journals as to who was
|
||||
# responsible for the associated update to the parent.
|
||||
module Users
|
||||
def self.included(base) # :nodoc:
|
||||
Journal.send(:include, JournalMethods)
|
||||
|
||||
base.class_eval do
|
||||
include InstanceMethods
|
||||
|
||||
attr_accessor :updated_by
|
||||
alias_method_chain :journal_attributes, :user
|
||||
end
|
||||
end
|
||||
|
||||
# Methods added to journaled ActiveRecord::Base instances to enable journaling with additional
|
||||
# user information.
|
||||
module InstanceMethods
|
||||
private
|
||||
# Overrides the +journal_attributes+ method to include user information passed into the
|
||||
# parent object, by way of a +updated_by+ attr_accessor.
|
||||
def journal_attributes_with_user
|
||||
journal_attributes_without_user.merge(:user => updated_by || User.current)
|
||||
end
|
||||
end
|
||||
|
||||
# Instance methods added to Redmine::Acts::Journalized::Journal to accomodate incoming
|
||||
# user information.
|
||||
module JournalMethods
|
||||
def self.included(base) # :nodoc:
|
||||
base.class_eval do
|
||||
belongs_to :user
|
||||
|
||||
alias_method_chain :user=, :name
|
||||
end
|
||||
end
|
||||
|
||||
# Overrides the +user=+ method created by the polymorphic +belongs_to+ user association.
|
||||
# Based on the class of the object given, either the +user+ association columns or the
|
||||
# +user_name+ string column is populated.
|
||||
def user_with_name=(value)
|
||||
case value
|
||||
when ActiveRecord::Base then self.user_without_name = value
|
||||
else self.user = User.find_by_login(value)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
67
vendor/plugins/acts_as_journalized/lib/redmine/acts/journalized/versioned.rb
vendored
Normal file
67
vendor/plugins/acts_as_journalized/lib/redmine/acts/journalized/versioned.rb
vendored
Normal file
|
@ -0,0 +1,67 @@
|
|||
# This file included as part of the acts_as_journalized plugin for
|
||||
# the redMine project management 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 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program 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 this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# The original copyright and license conditions are:
|
||||
# Copyright (c) 2009 Steve Richert
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining
|
||||
# a copy of this software and associated documentation files (the
|
||||
# "Software"), to deal in the Software without restriction, including
|
||||
# without limitation the rights to use, copy, modify, merge, publish,
|
||||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
module Redmine::Acts::Journalized
|
||||
# Simply adds a flag to determine whether a model class if journaled.
|
||||
module Versioned
|
||||
def self.extended(base) # :nodoc:
|
||||
base.class_eval do
|
||||
class << self
|
||||
alias_method_chain :acts_as_journalized, :flag
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Overrides the +journaled+ method to first define the +journaled?+ class method before
|
||||
# deferring to the original +journaled+.
|
||||
def acts_as_journalized_with_flag(*args)
|
||||
acts_as_journalized_without_flag(*args)
|
||||
|
||||
class << self
|
||||
def journaled?
|
||||
true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# For all ActiveRecord::Base models that do not call the +journaled+ method, the +journaled?+
|
||||
# method will return false.
|
||||
def journaled?
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
111
vendor/plugins/acts_as_journalized/lib/redmine/acts/journalized/versions.rb
vendored
Normal file
111
vendor/plugins/acts_as_journalized/lib/redmine/acts/journalized/versions.rb
vendored
Normal file
|
@ -0,0 +1,111 @@
|
|||
# This file included as part of the acts_as_journalized plugin for
|
||||
# the redMine project management 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 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program 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 this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# The original copyright and license conditions are:
|
||||
# Copyright (c) 2009 Steve Richert
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining
|
||||
# a copy of this software and associated documentation files (the
|
||||
# "Software"), to deal in the Software without restriction, including
|
||||
# without limitation the rights to use, copy, modify, merge, publish,
|
||||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
module Redmine::Acts::Journalized
|
||||
# An extension module for the +has_many+ association with journals.
|
||||
module Versions
|
||||
# Returns all journals between (and including) the two given arguments. See documentation for
|
||||
# the +at+ extension method for what arguments are valid. If either of the given arguments is
|
||||
# invalid, an empty array is returned.
|
||||
#
|
||||
# The +between+ method preserves returns an array of journal records, preserving the order
|
||||
# given by the arguments. If the +from+ value represents a journal before that of the +to+
|
||||
# value, the array will be ordered from earliest to latest. The reverse is also true.
|
||||
def between(from, to)
|
||||
from_number, to_number = journal_at(from), journal_at(to)
|
||||
return [] if from_number.nil? || to_number.nil?
|
||||
|
||||
condition = (from_number == to_number) ? to_number : Range.new(*[from_number, to_number].sort)
|
||||
all(
|
||||
:conditions => {:version => condition},
|
||||
:order => "#{aliased_table_name}.version #{(from_number > to_number) ? 'DESC' : 'ASC'}"
|
||||
)
|
||||
end
|
||||
|
||||
# Returns all journal records created before the journal associated with the given value.
|
||||
def before(value)
|
||||
return [] if (version = journal_at(value)).nil?
|
||||
all(:conditions => "#{aliased_table_name}.version < #{version}")
|
||||
end
|
||||
|
||||
# Returns all journal records created after the journal associated with the given value.
|
||||
#
|
||||
# This is useful for dissociating records during use of the +reset_to!+ method.
|
||||
def after(value)
|
||||
return [] if (version = journal_at(value)).nil?
|
||||
all(:conditions => "#{aliased_table_name}.version > #{version}")
|
||||
end
|
||||
|
||||
# Returns a single journal associated with the given value. The following formats are valid:
|
||||
# * A Date or Time object: When given, +to_time+ is called on the value and the last journal
|
||||
# record in the history created before (or at) that time is returned.
|
||||
# * A Numeric object: Typically a positive integer, these values correspond to journal numbers
|
||||
# and the associated journal record is found by a journal number equal to the given value
|
||||
# rounded down to the nearest integer.
|
||||
# * A String: A string value represents a journal tag and the associated journal is searched
|
||||
# for by a matching tag value. *Note:* Be careful with string representations of numbers.
|
||||
# * A Symbol: Symbols represent association class methods on the +has_many+ journals
|
||||
# association. While all of the built-in association methods require arguments, additional
|
||||
# extension modules can be defined using the <tt>:extend</tt> option on the +journaled+
|
||||
# method. See the +journaled+ documentation for more information.
|
||||
# * A Version object: If a journal object is passed to the +at+ method, it is simply returned
|
||||
# untouched.
|
||||
def at(value)
|
||||
case value
|
||||
when Date, Time then last(:conditions => ["#{aliased_table_name}.created_at <= ?", value.to_time])
|
||||
when Numeric then find_by_version(value.floor)
|
||||
when Symbol then respond_to?(value) ? send(value) : nil
|
||||
when Journal then value
|
||||
end
|
||||
end
|
||||
|
||||
# Returns the journal number associated with the given value. In many cases, this involves
|
||||
# simply passing the value to the +at+ method and then returning the subsequent journal number.
|
||||
# Hoever, for Numeric values, the journal number can be returned directly and for Date/Time
|
||||
# values, a default value of 1 is given to ensure that times prior to the first journal
|
||||
# still return a valid journal number (useful for rejournal).
|
||||
def journal_at(value)
|
||||
case value
|
||||
when Date, Time then (v = at(value)) ? v.version : 1
|
||||
when Numeric then value.floor
|
||||
when Symbol then (v = at(value)) ? v.version : nil
|
||||
when String then nil
|
||||
when Journal then value.version
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,169 @@
|
|||
require File.join(File.dirname(__FILE__), 'test_helper')
|
||||
|
||||
class ChangesTest < Test::Unit::TestCase
|
||||
context "A journal's changes" do
|
||||
setup do
|
||||
@user = User.create(:name => 'Steve Richert')
|
||||
@user.update_attribute(:last_name, 'Jobs')
|
||||
@changes = @user.journals.last.changes
|
||||
end
|
||||
|
||||
should 'be a hash' do
|
||||
assert_kind_of Hash, @changes
|
||||
end
|
||||
|
||||
should 'not be empty' do
|
||||
assert !@changes.empty?
|
||||
end
|
||||
|
||||
should 'have string keys' do
|
||||
@changes.keys.each do |key|
|
||||
assert_kind_of String, key
|
||||
end
|
||||
end
|
||||
|
||||
should 'have array values' do
|
||||
@changes.values.each do |value|
|
||||
assert_kind_of Array, value
|
||||
end
|
||||
end
|
||||
|
||||
should 'have two-element values' do
|
||||
@changes.values.each do |value|
|
||||
assert_equal 2, value.size
|
||||
end
|
||||
end
|
||||
|
||||
should 'have unique-element values' do
|
||||
@changes.values.each do |value|
|
||||
assert_equal value.uniq, value
|
||||
end
|
||||
end
|
||||
|
||||
should "equal the model's changes" do
|
||||
@user.first_name = 'Stephen'
|
||||
model_changes = @user.changes
|
||||
@user.save
|
||||
changes = @user.journals.last.changes
|
||||
assert_equal model_changes, changes
|
||||
end
|
||||
end
|
||||
|
||||
context 'A hash of changes' do
|
||||
setup do
|
||||
@changes = {'first_name' => ['Steve', 'Stephen']}
|
||||
@other = {'first_name' => ['Catie', 'Catherine']}
|
||||
end
|
||||
|
||||
should 'properly append other changes' do
|
||||
expected = {'first_name' => ['Steve', 'Catherine']}
|
||||
changes = @changes.append_changes(@other)
|
||||
assert_equal expected, changes
|
||||
@changes.append_changes!(@other)
|
||||
assert_equal expected, @changes
|
||||
end
|
||||
|
||||
should 'properly prepend other changes' do
|
||||
expected = {'first_name' => ['Catie', 'Stephen']}
|
||||
changes = @changes.prepend_changes(@other)
|
||||
assert_equal expected, changes
|
||||
@changes.prepend_changes!(@other)
|
||||
assert_equal expected, @changes
|
||||
end
|
||||
|
||||
should 'be reversible' do
|
||||
expected = {'first_name' => ['Stephen', 'Steve']}
|
||||
changes = @changes.reverse_changes
|
||||
assert_equal expected, changes
|
||||
@changes.reverse_changes!
|
||||
assert_equal expected, @changes
|
||||
end
|
||||
end
|
||||
|
||||
context 'The changes between two journals' do
|
||||
setup do
|
||||
name = 'Steve Richert'
|
||||
@user = User.create(:name => name) # 1
|
||||
@user.update_attribute(:last_name, 'Jobs') # 2
|
||||
@user.update_attribute(:first_name, 'Stephen') # 3
|
||||
@user.update_attribute(:last_name, 'Richert') # 4
|
||||
@user.update_attribute(:name, name) # 5
|
||||
@version = @user.version
|
||||
end
|
||||
|
||||
should 'be a hash' do
|
||||
1.upto(@version) do |i|
|
||||
1.upto(@version) do |j|
|
||||
changes = @user.changes_between(i, j)
|
||||
assert_kind_of Hash, changes
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
should 'have string keys' do
|
||||
1.upto(@version) do |i|
|
||||
1.upto(@version) do |j|
|
||||
changes = @user.changes_between(i, j)
|
||||
changes.keys.each do |key|
|
||||
assert_kind_of String, key
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
should 'have array values' do
|
||||
1.upto(@version) do |i|
|
||||
1.upto(@version) do |j|
|
||||
changes = @user.changes_between(i, j)
|
||||
changes.values.each do |value|
|
||||
assert_kind_of Array, value
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
should 'have two-element values' do
|
||||
1.upto(@version) do |i|
|
||||
1.upto(@version) do |j|
|
||||
changes = @user.changes_between(i, j)
|
||||
changes.values.each do |value|
|
||||
assert_equal 2, value.size
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
should 'have unique-element values' do
|
||||
1.upto(@version) do |i|
|
||||
1.upto(@version) do |j|
|
||||
changes = @user.changes_between(i, j)
|
||||
changes.values.each do |value|
|
||||
assert_equal value.uniq, value
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
should 'be empty between identical versions' do
|
||||
assert @user.changes_between(1, @version).empty?
|
||||
assert @user.changes_between(@version, 1).empty?
|
||||
end
|
||||
|
||||
should 'be should reverse with direction' do
|
||||
1.upto(@version) do |i|
|
||||
i.upto(@version) do |j|
|
||||
up = @user.changes_between(i, j)
|
||||
down = @user.changes_between(j, i)
|
||||
assert_equal up, down.reverse_changes
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
should 'be empty with invalid arguments' do
|
||||
1.upto(@version) do |i|
|
||||
assert @user.changes_between(i, nil)
|
||||
assert @user.changes_between(nil, i)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,137 @@
|
|||
require File.join(File.dirname(__FILE__), 'test_helper')
|
||||
|
||||
class ConditionsTest < Test::Unit::TestCase
|
||||
context 'Converted :if conditions' do
|
||||
setup do
|
||||
User.class_eval do
|
||||
def true; true; end
|
||||
end
|
||||
end
|
||||
|
||||
should 'be an array' do
|
||||
assert_kind_of Array, User.vestal_journals_options[:if]
|
||||
User.prepare_journaled_options(:if => :true)
|
||||
assert_kind_of Array, User.vestal_journals_options[:if]
|
||||
end
|
||||
|
||||
should 'have proc values' do
|
||||
User.prepare_journaled_options(:if => :true)
|
||||
assert User.vestal_journals_options[:if].all?{|i| i.is_a?(Proc) }
|
||||
end
|
||||
|
||||
teardown do
|
||||
User.prepare_journaled_options(:if => [])
|
||||
end
|
||||
end
|
||||
|
||||
context 'Converted :unless conditions' do
|
||||
setup do
|
||||
User.class_eval do
|
||||
def true; true; end
|
||||
end
|
||||
end
|
||||
|
||||
should 'be an array' do
|
||||
assert_kind_of Array, User.vestal_journals_options[:unless]
|
||||
User.prepare_journaled_options(:unless => :true)
|
||||
assert_kind_of Array, User.vestal_journals_options[:unless]
|
||||
end
|
||||
|
||||
should 'have proc values' do
|
||||
User.prepare_journaled_options(:unless => :true)
|
||||
assert User.vestal_journals_options[:unless].all?{|i| i.is_a?(Proc) }
|
||||
end
|
||||
|
||||
teardown do
|
||||
User.prepare_journaled_options(:unless => [])
|
||||
end
|
||||
end
|
||||
|
||||
context 'A new journal' do
|
||||
setup do
|
||||
User.class_eval do
|
||||
def true; true; end
|
||||
def false; false; end
|
||||
end
|
||||
|
||||
@user = User.create(:name => 'Steve Richert')
|
||||
@count = @user.journals.count
|
||||
end
|
||||
|
||||
context 'with :if conditions' do
|
||||
context 'that pass' do
|
||||
setup do
|
||||
User.prepare_journaled_options(:if => [:true])
|
||||
@user.update_attribute(:last_name, 'Jobs')
|
||||
end
|
||||
|
||||
should 'be created' do
|
||||
assert_equal @count + 1, @user.journals.count
|
||||
end
|
||||
end
|
||||
|
||||
context 'that fail' do
|
||||
setup do
|
||||
User.prepare_journaled_options(:if => [:false])
|
||||
@user.update_attribute(:last_name, 'Jobs')
|
||||
end
|
||||
|
||||
should 'not be created' do
|
||||
assert_equal @count, @user.journals.count
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with :unless conditions' do
|
||||
context 'that pass' do
|
||||
setup do
|
||||
User.prepare_journaled_options(:unless => [:true])
|
||||
@user.update_attribute(:last_name, 'Jobs')
|
||||
end
|
||||
|
||||
should 'not be created' do
|
||||
assert_equal @count, @user.journals.count
|
||||
end
|
||||
end
|
||||
|
||||
context 'that fail' do
|
||||
setup do
|
||||
User.prepare_journaled_options(:unless => [:false])
|
||||
@user.update_attribute(:last_name, 'Jobs')
|
||||
end
|
||||
|
||||
should 'not be created' do
|
||||
assert_equal @count + 1, @user.journals.count
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with :if and :unless conditions' do
|
||||
context 'that pass' do
|
||||
setup do
|
||||
User.prepare_journaled_options(:if => [:true], :unless => [:true])
|
||||
@user.update_attribute(:last_name, 'Jobs')
|
||||
end
|
||||
|
||||
should 'not be created' do
|
||||
assert_equal @count, @user.journals.count
|
||||
end
|
||||
end
|
||||
|
||||
context 'that fail' do
|
||||
setup do
|
||||
User.prepare_journaled_options(:if => [:false], :unless => [:false])
|
||||
@user.update_attribute(:last_name, 'Jobs')
|
||||
end
|
||||
|
||||
should 'not be created' do
|
||||
assert_equal @count, @user.journals.count
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
teardown do
|
||||
User.prepare_journaled_options(:if => [], :unless => [])
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,39 @@
|
|||
require File.join(File.dirname(__FILE__), 'test_helper')
|
||||
|
||||
class ConfigurationTest < Test::Unit::TestCase
|
||||
context 'Global configuration options' do
|
||||
setup do
|
||||
module Extension; end
|
||||
|
||||
@options = {
|
||||
'class_name' => 'CustomVersion',
|
||||
:extend => Extension,
|
||||
:as => :parent
|
||||
}
|
||||
|
||||
VestalVersions.configure do |config|
|
||||
@options.each do |key, value|
|
||||
config.send("#{key}=", value)
|
||||
end
|
||||
end
|
||||
|
||||
@configuration = VestalVersions::Configuration.options
|
||||
end
|
||||
|
||||
should 'should be a hash' do
|
||||
assert_kind_of Hash, @configuration
|
||||
end
|
||||
|
||||
should 'have symbol keys' do
|
||||
assert @configuration.keys.all?{|k| k.is_a?(Symbol) }
|
||||
end
|
||||
|
||||
should 'store values identical to those given' do
|
||||
assert_equal @options.symbolize_keys, @configuration
|
||||
end
|
||||
|
||||
teardown do
|
||||
VestalVersions::Configuration.options.clear
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,152 @@
|
|||
require File.join(File.dirname(__FILE__), 'test_helper')
|
||||
|
||||
class ControlTest < Test::Unit::TestCase
|
||||
context 'Within a skip_journal block,' do
|
||||
setup do
|
||||
@user = User.create(:name => 'Steve Richert')
|
||||
@count = @user.journals.count
|
||||
end
|
||||
|
||||
context 'a model update' do
|
||||
setup do
|
||||
@user.skip_journal do
|
||||
@user.update_attribute(:last_name, 'Jobs')
|
||||
end
|
||||
end
|
||||
|
||||
should 'not create a journal' do
|
||||
assert_equal @count, @user.journals.count
|
||||
end
|
||||
end
|
||||
|
||||
context 'multiple model updates' do
|
||||
setup do
|
||||
@user.skip_journal do
|
||||
@user.update_attribute(:first_name, 'Stephen')
|
||||
@user.update_attribute(:last_name, 'Jobs')
|
||||
@user.update_attribute(:first_name, 'Steve')
|
||||
end
|
||||
end
|
||||
|
||||
should 'not create a journal' do
|
||||
assert_equal @count, @user.journals.count
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'Within a merge_journal block,' do
|
||||
setup do
|
||||
@user = User.create(:name => 'Steve Richert')
|
||||
@count = @user.journals.count
|
||||
end
|
||||
|
||||
context 'a model update' do
|
||||
setup do
|
||||
@user.merge_journal do
|
||||
@user.update_attribute(:last_name, 'Jobs')
|
||||
end
|
||||
end
|
||||
|
||||
should 'create a journal' do
|
||||
assert_equal @count + 1, @user.journals.count
|
||||
end
|
||||
end
|
||||
|
||||
context 'multiple model updates' do
|
||||
setup do
|
||||
@user.merge_journal do
|
||||
@user.update_attribute(:first_name, 'Stephen')
|
||||
@user.update_attribute(:last_name, 'Jobs')
|
||||
@user.update_attribute(:first_name, 'Steve')
|
||||
end
|
||||
end
|
||||
|
||||
should 'create a journal' do
|
||||
assert_equal @count + 1, @user.journals.count
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'Within a append_journal block' do
|
||||
context '(when no journals exist),' do
|
||||
setup do
|
||||
@user = User.create(:name => 'Steve Richert')
|
||||
@count = @user.journals.count
|
||||
end
|
||||
|
||||
context 'a model update' do
|
||||
setup do
|
||||
@user.append_journal do
|
||||
@user.update_attribute(:last_name, 'Jobs')
|
||||
end
|
||||
end
|
||||
|
||||
should 'create a journal' do
|
||||
assert_equal @count + 1, @user.journals.count
|
||||
end
|
||||
end
|
||||
|
||||
context 'multiple model updates' do
|
||||
setup do
|
||||
@user.append_journal do
|
||||
@user.update_attribute(:first_name, 'Stephen')
|
||||
@user.update_attribute(:last_name, 'Jobs')
|
||||
@user.update_attribute(:first_name, 'Steve')
|
||||
end
|
||||
end
|
||||
|
||||
should 'create a journal' do
|
||||
assert_equal @count + 1, @user.journals.count
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context '(when journals exist),' do
|
||||
setup do
|
||||
@user = User.create(:name => 'Steve Richert')
|
||||
@user.update_attribute(:last_name, 'Jobs')
|
||||
@user.update_attribute(:last_name, 'Richert')
|
||||
@last_journal = @user.journals.last
|
||||
@count = @user.journals.count
|
||||
end
|
||||
|
||||
context 'a model update' do
|
||||
setup do
|
||||
@user.append_journal do
|
||||
@user.update_attribute(:last_name, 'Jobs')
|
||||
end
|
||||
end
|
||||
|
||||
should 'not create a journal' do
|
||||
assert_equal @count, @user.journals.count
|
||||
end
|
||||
|
||||
should 'update the last journal' do
|
||||
last_journal = @user.journals(true).last
|
||||
assert_equal @last_journal.id, last_journal.id
|
||||
assert_not_equal @last_journal.attributes, last_journal.attributes
|
||||
end
|
||||
end
|
||||
|
||||
context 'multiple model updates' do
|
||||
setup do
|
||||
@user.append_journal do
|
||||
@user.update_attribute(:first_name, 'Stephen')
|
||||
@user.update_attribute(:last_name, 'Jobs')
|
||||
@user.update_attribute(:first_name, 'Steve')
|
||||
end
|
||||
end
|
||||
|
||||
should 'not create a journal' do
|
||||
assert_equal @count, @user.journals.count
|
||||
end
|
||||
|
||||
should 'update the last journal' do
|
||||
last_journal = @user.journals(true).last
|
||||
assert_equal @last_journal.id, last_journal.id
|
||||
assert_not_equal @last_journal.attributes, last_journal.attributes
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,110 @@
|
|||
require File.join(File.dirname(__FILE__), 'test_helper')
|
||||
|
||||
class CreationTest < Test::Unit::TestCase
|
||||
context 'The number of journals' do
|
||||
setup do
|
||||
@name = 'Steve Richert'
|
||||
@user = User.create(:name => @name)
|
||||
@count = @user.journals.count
|
||||
end
|
||||
|
||||
should 'initially equal zero' do
|
||||
assert_equal 0, @count
|
||||
end
|
||||
|
||||
should 'not increase when no changes are made in an update' do
|
||||
@user.update_attribute(:name, @name)
|
||||
assert_equal @count, @user.journals.count
|
||||
end
|
||||
|
||||
should 'not increase when no changes are made before a save' do
|
||||
@user.save
|
||||
assert_equal @count, @user.journals.count
|
||||
end
|
||||
|
||||
context 'after an update' do
|
||||
setup do
|
||||
@user.update_attribute(:last_name, 'Jobs')
|
||||
end
|
||||
|
||||
should 'increase by one' do
|
||||
assert_equal @count + 1, @user.journals.count
|
||||
end
|
||||
end
|
||||
|
||||
context 'after multiple updates' do
|
||||
setup do
|
||||
@user.update_attribute(:last_name, 'Jobs')
|
||||
@user.update_attribute(:last_name, 'Richert')
|
||||
end
|
||||
|
||||
should 'increase multiple times' do
|
||||
assert_operator @count + 1, :<, @user.journals.count
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "A created journal's changes" do
|
||||
setup do
|
||||
@user = User.create(:name => 'Steve Richert')
|
||||
@user.update_attribute(:last_name, 'Jobs')
|
||||
end
|
||||
|
||||
should 'not contain Rails timestamps' do
|
||||
%w(created_at created_on updated_at updated_on).each do |timestamp|
|
||||
assert_does_not_contain @user.journals.last.changes.keys, timestamp
|
||||
end
|
||||
end
|
||||
|
||||
context '(with :only options)' do
|
||||
setup do
|
||||
@only = %w(first_name)
|
||||
User.prepare_journaled_options(:only => @only)
|
||||
@user.update_attribute(:name, 'Steven Tyler')
|
||||
end
|
||||
|
||||
should 'only contain the specified columns' do
|
||||
assert_equal @only, @user.journals.last.changes.keys
|
||||
end
|
||||
|
||||
teardown do
|
||||
User.prepare_journaled_options(:only => nil)
|
||||
end
|
||||
end
|
||||
|
||||
context '(with :except options)' do
|
||||
setup do
|
||||
@except = %w(first_name)
|
||||
User.prepare_journaled_options(:except => @except)
|
||||
@user.update_attribute(:name, 'Steven Tyler')
|
||||
end
|
||||
|
||||
should 'not contain the specified columns' do
|
||||
@except.each do |column|
|
||||
assert_does_not_contain @user.journals.last.changes.keys, column
|
||||
end
|
||||
end
|
||||
|
||||
teardown do
|
||||
User.prepare_journaled_options(:except => nil)
|
||||
end
|
||||
end
|
||||
|
||||
context '(with both :only and :except options)' do
|
||||
setup do
|
||||
@only = %w(first_name)
|
||||
@except = @only
|
||||
User.prepare_journaled_options(:only => @only, :except => @except)
|
||||
@user.update_attribute(:name, 'Steven Tyler')
|
||||
end
|
||||
|
||||
should 'respect only the :only options' do
|
||||
assert_equal @only, @user.journals.last.changes.keys
|
||||
end
|
||||
|
||||
teardown do
|
||||
User.prepare_journaled_options(:only => nil, :except => nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,52 @@
|
|||
require File.join(File.dirname(__FILE__), 'test_helper')
|
||||
|
||||
class OptionsTest < Test::Unit::TestCase
|
||||
context 'Configuration options' do
|
||||
setup do
|
||||
@options = {:dependent => :destroy}
|
||||
@configuration = {:class_name => 'MyCustomVersion'}
|
||||
|
||||
VestalVersions::Configuration.options.clear
|
||||
@configuration.each{|k,v| VestalVersions::Configuration.send("#{k}=", v) }
|
||||
|
||||
@prepared_options = User.prepare_journaled_options(@options.dup)
|
||||
end
|
||||
|
||||
should 'have symbolized keys' do
|
||||
assert User.vestal_journals_options.keys.all?{|k| k.is_a?(Symbol) }
|
||||
end
|
||||
|
||||
should 'combine class-level and global configuration options' do
|
||||
combined_keys = (@options.keys + @configuration.keys).map(&:to_sym).uniq
|
||||
combined_options = @configuration.symbolize_keys.merge(@options.symbolize_keys)
|
||||
assert_equal @prepared_options.slice(*combined_keys), combined_options
|
||||
end
|
||||
|
||||
teardown do
|
||||
VestalVersions::Configuration.options.clear
|
||||
User.prepare_journaled_options({})
|
||||
end
|
||||
end
|
||||
|
||||
context 'Given no options, configuration options' do
|
||||
setup do
|
||||
@prepared_options = User.prepare_journaled_options({})
|
||||
end
|
||||
|
||||
should 'default to "VestalVersions::Version" for :class_name' do
|
||||
assert_equal 'VestalVersions::Version', @prepared_options[:class_name]
|
||||
end
|
||||
|
||||
should 'default to :delete_all for :dependent' do
|
||||
assert_equal :delete_all, @prepared_options[:dependent]
|
||||
end
|
||||
|
||||
should 'force the :as option value to :journaled' do
|
||||
assert_equal :journaled, @prepared_options[:as]
|
||||
end
|
||||
|
||||
should 'default to [VestalVersions::Versions] for :extend' do
|
||||
assert_equal [VestalVersions::Versions], @prepared_options[:extend]
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,19 @@
|
|||
require File.join(File.dirname(__FILE__), 'test_helper')
|
||||
|
||||
class ReloadTest < Test::Unit::TestCase
|
||||
context 'Reloading a reverted model' do
|
||||
setup do
|
||||
@user = User.create(:name => 'Steve Richert')
|
||||
first_version = @user.version
|
||||
@user.update_attribute(:last_name, 'Jobs')
|
||||
@last_version = @user.version
|
||||
@user.revert_to(first_version)
|
||||
end
|
||||
|
||||
should 'reset the journal number to the most recent journal' do
|
||||
assert_not_equal @last_journal, @user.journal
|
||||
@user.reload
|
||||
assert_equal @last_journal, @user.journal
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,112 @@
|
|||
require File.join(File.dirname(__FILE__), 'test_helper')
|
||||
|
||||
class ResetTest < Test::Unit::TestCase
|
||||
context 'Resetting a model' do
|
||||
setup do
|
||||
@original_dependent = User.reflect_on_association(:journals).options[:dependent]
|
||||
@user, @journals = User.new, []
|
||||
@names = ['Steve Richert', 'Stephen Richert', 'Stephen Jobs', 'Steve Jobs']
|
||||
@names.each do |name|
|
||||
@user.update_attribute(:name, name)
|
||||
@journals << @user.journal
|
||||
end
|
||||
end
|
||||
|
||||
should "properly revert the model's attributes" do
|
||||
@journals.reverse.each_with_index do |journal, i|
|
||||
@user.reset_to!(journal)
|
||||
assert_equal @names.reverse[i], @user.name
|
||||
end
|
||||
end
|
||||
|
||||
should 'dissociate all journals after the target' do
|
||||
@journals.reverse.each do |journal|
|
||||
@user.reset_to!(journal)
|
||||
assert_equal 0, @user.journals(true).after(journal).count
|
||||
end
|
||||
end
|
||||
|
||||
context 'with the :dependent option as :delete_all' do
|
||||
setup do
|
||||
User.reflect_on_association(:journals).options[:dependent] = :delete_all
|
||||
end
|
||||
|
||||
should 'delete all journals after the target journal' do
|
||||
@journals.reverse.each do |journal|
|
||||
later_journals = @user.journals.after(journal)
|
||||
@user.reset_to!(journal)
|
||||
later_journals.each do |later_journal|
|
||||
assert_raise ActiveRecord::RecordNotFound do
|
||||
later_journal.reload
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
should 'not destroy all journals after the target journal' do
|
||||
VestalVersions::Version.any_instance.stubs(:destroy).raises(RuntimeError)
|
||||
@journals.reverse.each do |journal|
|
||||
assert_nothing_raised do
|
||||
@user.reset_to!(journal)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with the :dependent option as :destroy' do
|
||||
setup do
|
||||
User.reflect_on_association(:journals).options[:dependent] = :destroy
|
||||
end
|
||||
|
||||
should 'delete all journals after the target journal' do
|
||||
@journals.reverse.each do |journal|
|
||||
later_journals = @user.journals.after(journal)
|
||||
@user.reset_to!(journal)
|
||||
later_journals.each do |later_journal|
|
||||
assert_raise ActiveRecord::RecordNotFound do
|
||||
later_journal.reload
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
should 'destroy all journals after the target journal' do
|
||||
VestalVersions::Version.any_instance.stubs(:destroy).raises(RuntimeError)
|
||||
@journals.reverse.each do |journal|
|
||||
later_journals = @user.journals.after(journal)
|
||||
if later_journals.empty?
|
||||
assert_nothing_raised do
|
||||
@user.reset_to!(journal)
|
||||
end
|
||||
else
|
||||
assert_raise RuntimeError do
|
||||
@user.reset_to!(journal)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with the :dependent option as :nullify' do
|
||||
setup do
|
||||
User.reflect_on_association(:journals).options[:dependent] = :nullify
|
||||
end
|
||||
|
||||
should 'leave all journals after the target journal' do
|
||||
@journals.reverse.each do |journal|
|
||||
later_journals = @user.journals.after(journal)
|
||||
@user.reset_to!(journal)
|
||||
later_journals.each do |later_journal|
|
||||
assert_nothing_raised do
|
||||
later_journal.reload
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
teardown do
|
||||
User.reflect_on_association(:journals).options[:dependent] = @original_dependent
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,68 @@
|
|||
require File.join(File.dirname(__FILE__), 'test_helper')
|
||||
|
||||
class RejournalTest < Test::Unit::TestCase
|
||||
context 'A model rejournal' do
|
||||
setup do
|
||||
@user, @attributes, @times = User.new, {}, {}
|
||||
names = ['Steve Richert', 'Stephen Richert', 'Stephen Jobs', 'Steve Jobs']
|
||||
time = names.size.hours.ago
|
||||
names.each do |name|
|
||||
@user.update_attribute(:name, name)
|
||||
@attributes[@user.journal] = @user.attributes
|
||||
time += 1.hour
|
||||
if last_journal = @user.journals.last
|
||||
last_journal.update_attribute(:created_at, time)
|
||||
end
|
||||
@times[@user.journal] = time
|
||||
end
|
||||
@user.reload.journals.reload
|
||||
@first_journal, @last_journal = @attributes.keys.min, @attributes.keys.max
|
||||
end
|
||||
|
||||
should 'return the new journal number' do
|
||||
new_journal = @user.revert_to(@first_journal)
|
||||
assert_equal @first_journal, new_journal
|
||||
end
|
||||
|
||||
should 'change the journal number when saved' do
|
||||
current_journal = @user.journal
|
||||
@user.revert_to!(@first_journal)
|
||||
assert_not_equal current_journal, @user.journal
|
||||
end
|
||||
|
||||
should 'do nothing for a invalid argument' do
|
||||
current_journal = @user.journal
|
||||
[nil, :bogus, 'bogus', (1..2)].each do |invalid|
|
||||
@user.revert_to(invalid)
|
||||
assert_equal current_journal, @user.journal
|
||||
end
|
||||
end
|
||||
|
||||
should 'be able to target a journal number' do
|
||||
@user.revert_to(1)
|
||||
assert 1, @user.journal
|
||||
end
|
||||
|
||||
should 'be able to target a date and time' do
|
||||
@times.each do |journal, time|
|
||||
@user.revert_to(time + 1.second)
|
||||
assert_equal journal, @user.journal
|
||||
end
|
||||
end
|
||||
|
||||
should 'be able to target a journal object' do
|
||||
@user.journals.each do |journal|
|
||||
@user.revert_to(journal)
|
||||
assert_equal journal.number, @user.journal
|
||||
end
|
||||
end
|
||||
|
||||
should "correctly roll back the model's attributes" do
|
||||
timestamps = %w(created_at created_on updated_at updated_on)
|
||||
@attributes.each do |journal, attributes|
|
||||
@user.revert_to!(journal)
|
||||
assert_equal attributes.except(*timestamps), @user.attributes.except(*timestamps)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,43 @@
|
|||
ActiveRecord::Base.establish_connection(
|
||||
:adapter => defined?(RUBY_ENGINE) && RUBY_ENGINE == 'jruby' ? 'jdbcsqlite3' : 'sqlite3',
|
||||
:database => File.join(File.dirname(__FILE__), 'test.db')
|
||||
)
|
||||
|
||||
class CreateSchema < ActiveRecord::Migration
|
||||
def self.up
|
||||
create_table :users, :force => true do |t|
|
||||
t.string :first_name
|
||||
t.string :last_name
|
||||
t.timestamps
|
||||
end
|
||||
|
||||
create_table :journals, :force => true do |t|
|
||||
t.belongs_to :journaled, :polymorphic => true
|
||||
t.belongs_to :user, :polymorphic => true
|
||||
t.string :user_name
|
||||
t.text :changes
|
||||
t.integer :number
|
||||
t.string :tag
|
||||
t.timestamps
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
CreateSchema.suppress_messages do
|
||||
CreateSchema.migrate(:up)
|
||||
end
|
||||
|
||||
class User < ActiveRecord::Base
|
||||
journaled
|
||||
|
||||
def name
|
||||
[first_name, last_name].compact.join(' ')
|
||||
end
|
||||
|
||||
def name=(names)
|
||||
self[:first_name], self[:last_name] = names.split(' ', 2)
|
||||
end
|
||||
end
|
||||
|
||||
class MyCustomVersion < VestalVersions::Version
|
||||
end
|
|
@ -0,0 +1,39 @@
|
|||
require File.join(File.dirname(__FILE__), 'test_helper')
|
||||
|
||||
class TaggingTest < Test::Unit::TestCase
|
||||
context 'Tagging a journal' do
|
||||
setup do
|
||||
@user = User.create(:name => 'Steve Richert')
|
||||
@user.update_attribute(:last_name, 'Jobs')
|
||||
end
|
||||
|
||||
should "update the journal record's tag column" do
|
||||
tag_name = 'TAG'
|
||||
last_journal = @user.journals.last
|
||||
assert_not_equal tag_name, last_journal.tag
|
||||
@user.tag_journal(tag_name)
|
||||
assert_equal tag_name, last_journal.reload.tag
|
||||
end
|
||||
|
||||
should 'create a journal record for an initial journal' do
|
||||
@user.revert_to(1)
|
||||
assert_nil @user.journals.at(1)
|
||||
@user.tag_journal('TAG')
|
||||
assert_not_nil @user.journals.at(1)
|
||||
end
|
||||
end
|
||||
|
||||
context 'A tagged journal' do
|
||||
setup do
|
||||
user = User.create(:name => 'Steve Richert')
|
||||
user.update_attribute(:last_name, 'Jobs')
|
||||
user.tag_journal('TAG')
|
||||
@journal = user.journals.last
|
||||
end
|
||||
|
||||
should 'return true for the "tagged?" method' do
|
||||
assert @journal.respond_to?(:tagged?)
|
||||
assert_equal true, @journal.tagged?
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,11 @@
|
|||
$: << File.join(File.dirname(__FILE__), '..', 'lib')
|
||||
$: << File.dirname(__FILE__)
|
||||
|
||||
require 'rubygems'
|
||||
require 'test/unit'
|
||||
require 'active_record'
|
||||
require 'shoulda'
|
||||
require 'mocha'
|
||||
require 'vestal_versions'
|
||||
require 'schema'
|
||||
begin; require 'redgreen'; rescue LoadError; end
|
|
@ -0,0 +1,25 @@
|
|||
require 'test_helper'
|
||||
|
||||
class UsersTest < Test::Unit::TestCase
|
||||
context 'The user responsible for an update' do
|
||||
setup do
|
||||
@updated_by = User.create(:name => 'Steve Jobs')
|
||||
@user = User.create(:name => 'Steve Richert')
|
||||
end
|
||||
|
||||
should 'default to nil' do
|
||||
@user.update_attributes(:first_name => 'Stephen')
|
||||
assert_nil @user.journals.last.user
|
||||
end
|
||||
|
||||
should 'accept and return an ActiveRecord user' do
|
||||
@user.update_attributes(:first_name => 'Stephen', :updated_by => @updated_by)
|
||||
assert_equal @updated_by, @user.journals.last.user
|
||||
end
|
||||
|
||||
should 'accept and return a string user name' do
|
||||
@user.update_attributes(:first_name => 'Stephen', :updated_by => @updated_by.name)
|
||||
assert_equal @updated_by.name, @user.journals.last.user
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,43 @@
|
|||
require File.join(File.dirname(__FILE__), 'test_helper')
|
||||
|
||||
class VersionTest < Test::Unit::TestCase
|
||||
context 'Versions' do
|
||||
setup do
|
||||
@user = User.create(:name => 'Stephen Richert')
|
||||
@user.update_attribute(:name, 'Steve Jobs')
|
||||
@user.update_attribute(:last_name, 'Richert')
|
||||
@first_journal, @last_journal = @user.journals.first, @user.journals.last
|
||||
end
|
||||
|
||||
should 'be comparable to another journal based on journal number' do
|
||||
assert @first_journal == @first_journal
|
||||
assert @last_journal == @last_journal
|
||||
assert @first_journal != @last_journal
|
||||
assert @last_journal != @first_journal
|
||||
assert @first_journal < @last_journal
|
||||
assert @last_journal > @first_journal
|
||||
assert @first_journal <= @last_journal
|
||||
assert @last_journal >= @first_journal
|
||||
end
|
||||
|
||||
should "not equal a separate model's journal with the same number" do
|
||||
user = User.create(:name => 'Stephen Richert')
|
||||
user.update_attribute(:name, 'Steve Jobs')
|
||||
user.update_attribute(:last_name, 'Richert')
|
||||
first_journal, last_journal = user.journals.first, user.journals.last
|
||||
assert_not_equal @first_journal, first_journal
|
||||
assert_not_equal @last_journal, last_journal
|
||||
end
|
||||
|
||||
should 'default to ordering by number when finding through association' do
|
||||
order = @user.journals.send(:scope, :find)[:order]
|
||||
assert_equal 'journals.number ASC', order
|
||||
end
|
||||
|
||||
should 'return true for the "initial?" method when the journal number is 1' do
|
||||
journal = @user.journals.build(:number => 1)
|
||||
assert_equal 1, journal.number
|
||||
assert_equal true, journal.initial?
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,18 @@
|
|||
require File.join(File.dirname(__FILE__), 'test_helper')
|
||||
|
||||
class VersionedTest < Test::Unit::TestCase
|
||||
context 'ActiveRecord models' do
|
||||
should 'respond to the "journaled?" method' do
|
||||
assert ActiveRecord::Base.respond_to?(:journaled?)
|
||||
assert User.respond_to?(:journaled?)
|
||||
end
|
||||
|
||||
should 'return true for the "journaled?" method if the model is journaled' do
|
||||
assert_equal true, User.journaled?
|
||||
end
|
||||
|
||||
should 'return false for the "journaled?" method if the model is not journaled' do
|
||||
assert_equal false, ActiveRecord::Base.journaled?
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,172 @@
|
|||
require File.join(File.dirname(__FILE__), 'test_helper')
|
||||
|
||||
class VersionsTest < Test::Unit::TestCase
|
||||
context 'A collection of associated journals' do
|
||||
setup do
|
||||
@user, @times = User.new, {}
|
||||
names = ['Steve Richert', 'Stephen Richert', 'Stephen Jobs', 'Steve Jobs']
|
||||
time = names.size.hours.ago
|
||||
names.each do |name|
|
||||
@user.update_attribute(:name, name)
|
||||
@user.tag_journal(@user.journal.to_s)
|
||||
time += 1.hour
|
||||
@user.journals.last.update_attribute(:created_at, time)
|
||||
@times[@user.journal] = time
|
||||
end
|
||||
end
|
||||
|
||||
should 'be searchable between two valid journal values' do
|
||||
@times.keys.each do |number|
|
||||
@times.values.each do |time|
|
||||
assert_kind_of Array, @user.journals.between(number, number)
|
||||
assert_kind_of Array, @user.journals.between(number, time)
|
||||
assert_kind_of Array, @user.journals.between(time, number)
|
||||
assert_kind_of Array, @user.journals.between(time, time)
|
||||
assert !@user.journals.between(number, number).empty?
|
||||
assert !@user.journals.between(number, time).empty?
|
||||
assert !@user.journals.between(time, number).empty?
|
||||
assert !@user.journals.between(time, time).empty?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
should 'return an empty array when searching between a valid and an invalid journal value' do
|
||||
@times.each do |number, time|
|
||||
assert_equal [], @user.journals.between(number, nil)
|
||||
assert_equal [], @user.journals.between(time, nil)
|
||||
assert_equal [], @user.journals.between(nil, number)
|
||||
assert_equal [], @user.journals.between(nil, time)
|
||||
end
|
||||
end
|
||||
|
||||
should 'return an empty array when searching between two invalid journal values' do
|
||||
assert_equal [], @user.journals.between(nil, nil)
|
||||
end
|
||||
|
||||
should 'be searchable before a valid journal value' do
|
||||
@times.sort.each_with_index do |(number, time), i|
|
||||
assert_equal i, @user.journals.before(number).size
|
||||
assert_equal i, @user.journals.before(time).size
|
||||
end
|
||||
end
|
||||
|
||||
should 'return an empty array when searching before an invalid journal value' do
|
||||
assert_equal [], @user.journals.before(nil)
|
||||
end
|
||||
|
||||
should 'be searchable after a valid journal value' do
|
||||
@times.sort.reverse.each_with_index do |(number, time), i|
|
||||
assert_equal i, @user.journals.after(number).size
|
||||
assert_equal i, @user.journals.after(time).size
|
||||
end
|
||||
end
|
||||
|
||||
should 'return an empty array when searching after an invalid journal value' do
|
||||
assert_equal [], @user.journals.after(nil)
|
||||
end
|
||||
|
||||
should 'be fetchable by journal number' do
|
||||
@times.keys.each do |number|
|
||||
assert_kind_of VestalVersions::Version, @user.journals.at(number)
|
||||
assert_equal number, @user.journals.at(number).number
|
||||
end
|
||||
end
|
||||
|
||||
should 'be fetchable by tag' do
|
||||
@times.keys.map{|n| [n, n.to_s] }.each do |number, tag|
|
||||
assert_kind_of VestalVersions::Version, @user.journals.at(tag)
|
||||
assert_equal number, @user.journals.at(tag).number
|
||||
end
|
||||
end
|
||||
|
||||
should "be fetchable by the exact time of a journal's creation" do
|
||||
@times.each do |number, time|
|
||||
assert_kind_of VestalVersions::Version, @user.journals.at(time)
|
||||
assert_equal number, @user.journals.at(time).number
|
||||
end
|
||||
end
|
||||
|
||||
should "be fetchable by any time after the model's creation" do
|
||||
@times.each do |number, time|
|
||||
assert_kind_of VestalVersions::Version, @user.journals.at(time + 30.minutes)
|
||||
assert_equal number, @user.journals.at(time + 30.minutes).number
|
||||
end
|
||||
end
|
||||
|
||||
should "return nil when fetching a time before the model's creation" do
|
||||
creation = @times.values.min
|
||||
assert_nil @user.journals.at(creation - 1.second)
|
||||
end
|
||||
|
||||
should 'be fetchable by an association extension method' do
|
||||
assert_kind_of VestalVersions::Version, @user.journals.at(:first)
|
||||
assert_kind_of VestalVersions::Version, @user.journals.at(:last)
|
||||
assert_equal @times.keys.min, @user.journals.at(:first).number
|
||||
assert_equal @times.keys.max, @user.journals.at(:last).number
|
||||
end
|
||||
|
||||
should 'be fetchable by a journal object' do
|
||||
@times.keys.each do |number|
|
||||
journal = @user.journals.at(number)
|
||||
assert_kind_of VestalVersions::Version, journal
|
||||
assert_kind_of VestalVersions::Version, @user.journals.at(journal)
|
||||
assert_equal number, @user.journals.at(journal).number
|
||||
end
|
||||
end
|
||||
|
||||
should 'return nil when fetching an invalid journal value' do
|
||||
assert_nil @user.journals.at(nil)
|
||||
end
|
||||
|
||||
should 'provide a journal number for any given numeric journal value' do
|
||||
@times.keys.each do |number|
|
||||
assert_kind_of Fixnum, @user.journals.number_at(number)
|
||||
assert_kind_of Fixnum, @user.journals.number_at(number + 0.5)
|
||||
assert_equal @user.journals.number_at(number), @user.journals.number_at(number + 0.5)
|
||||
end
|
||||
end
|
||||
|
||||
should 'provide a journal number for a valid tag' do
|
||||
@times.keys.map{|n| [n, n.to_s] }.each do |number, tag|
|
||||
assert_kind_of Fixnum, @user.journals.number_at(tag)
|
||||
assert_equal number, @user.journals.number_at(tag)
|
||||
end
|
||||
end
|
||||
|
||||
should 'return nil when providing a journal number for an invalid tag' do
|
||||
assert_nil @user.journals.number_at('INVALID')
|
||||
end
|
||||
|
||||
should 'provide a journal number of a journal corresponding to an association extension method' do
|
||||
assert_kind_of VestalVersions::Version, @user.journals.at(:first)
|
||||
assert_kind_of VestalVersions::Version, @user.journals.at(:last)
|
||||
assert_equal @times.keys.min, @user.journals.number_at(:first)
|
||||
assert_equal @times.keys.max, @user.journals.number_at(:last)
|
||||
end
|
||||
|
||||
should 'return nil when providing a journal number for an invalid association extension method' do
|
||||
assert_nil @user.journals.number_at(:INVALID)
|
||||
end
|
||||
|
||||
should "provide a journal number for any time after the model's creation" do
|
||||
@times.each do |number, time|
|
||||
assert_kind_of Fixnum, @user.journals.number_at(time + 30.minutes)
|
||||
assert_equal number, @user.journals.number_at(time + 30.minutes)
|
||||
end
|
||||
end
|
||||
|
||||
should "provide a journal number of 1 for a time before the model's creation" do
|
||||
creation = @times.values.min
|
||||
assert_equal 1, @user.journals.number_at(creation - 1.second)
|
||||
end
|
||||
|
||||
should 'provide a journal number for a given journal object' do
|
||||
@times.keys.each do |number|
|
||||
journal = @user.journals.at(number)
|
||||
assert_kind_of VestalVersions::Version, journal
|
||||
assert_kind_of Fixnum, @user.journals.number_at(journal)
|
||||
assert_equal number, @user.journals.number_at(journal)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue