1
0
Fork 0
mirror of https://github.com/YunoHost-Apps/kanboard_ynh.git synced 2024-09-03 19:36:17 +02:00

Init files + sources

Kanboard 1.0.6
This commit is contained in:
mbugeia 2014-07-20 12:26:15 +02:00
parent 53c3f7a5b9
commit 5e51bcd6e9
315 changed files with 34626 additions and 0 deletions

35
manifest.json Normal file
View file

@ -0,0 +1,35 @@
{
name Kanboard,
id kanboard,
description {
en Kanboard is a simple visual task board web application,
fr Kanboard est une application web de management de tâches simples
},
developer {
name mbugeia,
email ,
url
},
multi_instance true,
arguments {
install [
{
name domain,
ask {
en Choose a domain for Kanboard,
fr Choisissez un domaine pour Kanboard
},
example domain.org
},
{
name path,
ask {
en Choose a path for Kanboard,
fr Choisissez un chemin pour Kanboard
},
example kanboard,
default kanboard
}
]
}
}

0
scripts/install Normal file
View file

0
scripts/remove Normal file
View file

0
scripts/upgrade Normal file
View file

55
sources/.gitignore vendored Normal file
View file

@ -0,0 +1,55 @@
# Compiled source #
###################
*.com
*.class
*.dll
*.exe
*.o
*.so
*.pyc
# Packages #
############
# it's better to unpack these files and commit the raw source
# git has its own built in compression methods
*.7z
*.dmg
*.gz
*.iso
*.jar
*.rar
*.tar
*.zip
# Logs and databases #
######################
*.log
*.sql
*.sqlite
*.sqlite-journal
# IDE generated files #
######################
.buildpath
.project
# OS generated files #
######################
.DS_Store
ehthumbs.db
Icon?
Thumbs.db
*.swp
.*.swp
*~
*.lock
*.out
# Vagrant #
###########
.vagrant
# App specific #
################
config.php
data/files

53
sources/.scrutinizer.yml Normal file
View file

@ -0,0 +1,53 @@
filter:
excluded_paths:
- 'vendor/*'
- 'tests/*'
- 'app/Templates/*'
paths: { }
tools:
php_sim:
enabled: true
min_mass: 16
filter:
excluded_paths:
- 'vendor/*'
- 'tests/*'
- 'app/Templates/*'
paths: { }
php_pdepend:
enabled: true
configuration_file: null
suffixes:
- php
excluded_dirs: { }
filter:
excluded_paths:
- 'vendor/*'
- 'tests/*'
- 'app/Templates/*'
paths: { }
php_analyzer:
enabled: true
extensions:
- php
dependency_paths: { }
filter:
excluded_paths:
- 'vendor/*'
- 'tests/*'
- 'app/Templates/*'
paths: { }
path_configs: { }
php_changetracking:
enabled: true
bug_patterns:
- '\bfix(?:es|ed)?\b'
feature_patterns:
- '\badd(?:s|ed)?\b'
- '\bimplement(?:s|ed)?\b'
filter:
excluded_paths:
- 'vendor/*'
- 'tests/*'
- 'app/Templates/*'
paths: { }

10
sources/.travis.yml Normal file
View file

@ -0,0 +1,10 @@
language: php
php:
- "5.6"
- "5.5"
- "5.4"
- "5.3"
before_script: wget https://phar.phpunit.de/phpunit.phar
script: php phpunit.phar

661
sources/LICENSE Normal file
View file

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

124
sources/README.markdown Normal file
View file

@ -0,0 +1,124 @@
Kanboard
========
Kanboard is a simple visual task board web application.
Official website: <http://kanboard.net>
- Inspired by the [Kanban methodology](http://en.wikipedia.org/wiki/Kanban)
- Get a visual and clear overview of your project
- Multiple boards with the ability to drag and drop tasks
- Minimalist software, focus only on essential features (Less is more)
- Open source and self-hosted
- Super simple installation
[![Build Status](https://travis-ci.org/fguillot/kanboard.svg)](https://travis-ci.org/fguillot/kanboard)
[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/fguillot/kanboard/badges/quality-score.png?s=2b6490781608657cc8c43d02285bfafb4f489528)](https://scrutinizer-ci.com/g/fguillot/kanboard/)
Features
--------
- Multiple boards/projects
- Boards customization, rename or add columns
- Tasks with different colors, categories, sub-tasks, attachments, Markdown support for the description
- Automatic actions
- Users management with a basic privileges separation (administrator or regular user)
- External authentication: Google and GitHub accounts as well as LDAP/ActiveDirectory
- Webhooks to create tasks from an external software
- Host anywhere (shared hosting, VPS, Raspberry Pi or localhost)
- No external dependencies
- **Super easy setup**, copy and paste files and you are done!
- Translations in English, French, Brazilian Portuguese, Spanish, German, Polish, Swedish and Chinese
Roadmap
-------
Kanboard is under active development, have a look to the roadmap: <http://kanboard.net/#roadmap>
Known bugs
----------
See Issues: <https://github.com/fguillot/kanboard/issues>
License
-------
GNU Affero General Public License version 3: <http://www.gnu.org/licenses/agpl-3.0.txt>
Authors
-------
Original author: [Frédéric Guillot](http://fredericguillot.com/)
Contributors:
- Alex Butum: https://github.com/dZkF9RWJT6wN8ux
- Claudio Lobo
- Gavlepeter: https://github.com/gavlepeter
- Jesusaplsoft: https://github.com/jesusaplsoft
- Kiswa: https://github.com/kiswa
- Levlaz: https://github.com/levlaz
- Mathgl67: https://github.com/mathgl67
- Matthieu Keller: https://github.com/maggick
- Maxime: https://github.com/EpocDotFr
- Moraxy: https://github.com/moraxy
- Nala Ginrut: https://github.com/NalaGinrut
- Nekohayo: https://github.com/nekohayo
- Olivier Maridat: https://github.com/oliviermaridat
- Poikilotherm: https://github.com/poikilotherm
- Raphaël Doursenaud: https://github.com/rdoursenaud
- Rzeka: https://github.com/rzeka
- Sebastien pacilly: https://github.com/spacilly
- Toomyem: https://github.com/Toomyem
- Troloo: https://github.com/troloo
- Typz: https://github.com/Typz
There is also many people who have reported bugs or proposed awesome ideas.
Documentation
-------------
### Using Kanboard
- [Usage examples](docs/usage-examples.markdown)
- [Manage users](docs/manage-users.markdown)
- [Syntax guide](docs/syntax-guide.markdown)
- [Automatic actions](docs/automatic-actions.markdown)
### Technical details
#### Installation
- [Installation instructions](docs/installation.markdown)
- [Installation on Ubuntu](docs/ubuntu-installation.markdown)
- [Installation on Debian](docs/debian-installation.markdown)
- [Installation on Centos](docs/centos-installation.markdown)
- [Upgrade Kanboard to a new version](docs/update.markdown)
- [Secure connections (HTTPS)](docs/secure-connections.markdown)
#### Database
- [Sqlite database management](docs/sqlite-database.markdown)
- [How to use Mysql](docs/mysql-configuration.markdown)
- [How to use Postgresql](docs/postgresql-configuration.markdown)
#### Authentication
- [LDAP authentication](docs/ldap-authentication.markdown)
- [Google authentication](docs/google-authentication.markdown)
- [GitHub authentication](docs/github-authentication.markdown)
#### Developers
- [Json-RPC API](docs/api-json-rpc.markdown)
- [How to use Kanboard with Vagrant](docs/vagrant.markdown)
- [Webhooks](docs/webhooks.markdown)
The documentation is written in [Markdown](http://en.wikipedia.org/wiki/Markdown).
If you want to improve the documentation, just send a pull-request.
FAQ
---
Go to the official website: <http://kanboard.net/faq>

29
sources/Vagrantfile vendored Normal file
View file

@ -0,0 +1,29 @@
# -*- mode: ruby -*-
# vi: set ft=ruby :
VAGRANTFILE_API_VERSION = "2"
$script = <<SCRIPT
# install packages
apt-get update
apt-get install -y apache2 php5 php5-sqlite php5-ldap php5-xdebug
service apache2 restart
rm -f /var/www/html/index.html
date > /etc/vagrant_provisioned_at
echo "Go to http://localhost:8080/ (admin/admin) !"
SCRIPT
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
# Image
config.vm.box = "ubuntu/trusty64"
config.vm.box_url = "http://cloud-images.ubuntu.com/vagrant/trusty/current/trusty-server-cloudimg-amd64-vagrant-disk1.box"
# Network
config.vm.network :forwarded_port, guest: 80, host: 8080
#config.vm.network "public_network", :bridge => "en0: Wi-Fi (AirPort)"
# Setup
config.vm.provision "shell", inline: $script
config.vm.synced_folder ".", "/var/www/html", owner: "www-data", group: "www-data"
end

1
sources/app/.htaccess Normal file
View file

@ -0,0 +1 @@
Deny from all

142
sources/app/Action/Base.php Normal file
View file

@ -0,0 +1,142 @@
<?php
namespace Action;
use Core\Listener;
/**
* Base class for automatic actions
*
* @package action
* @author Frederic Guillot
*/
abstract class Base implements Listener
{
/**
* Project id
*
* @access private
* @var integer
*/
private $project_id = 0;
/**
* User parameters
*
* @access private
* @var array
*/
private $params = array();
/**
* Execute the action
*
* @abstract
* @access public
* @param array $data Event data dictionary
* @return bool True if the action was executed or false when not executed
*/
abstract public function doAction(array $data);
/**
* Get the required parameter for the action (defined by the user)
*
* @abstract
* @access public
* @return array
*/
abstract public function getActionRequiredParameters();
/**
* Get the required parameter for the event (check if for the event data)
*
* @abstract
* @access public
* @return array
*/
abstract public function getEventRequiredParameters();
/**
* Constructor
*
* @access public
* @param integer $project_id Project id
*/
public function __construct($project_id)
{
$this->project_id = $project_id;
}
/**
* Set an user defined parameter
*
* @access public
* @param string $name Parameter name
* @param mixed $value Value
*/
public function setParam($name, $value)
{
$this->params[$name] = $value;
}
/**
* Get an user defined parameter
*
* @access public
* @param string $name Parameter name
* @param mixed $default_value Default value
* @return mixed
*/
public function getParam($name, $default_value = null)
{
return isset($this->params[$name]) ? $this->params[$name] : $default_value;
}
/**
* Check if an action is executable (right project and required parameters)
*
* @access public
* @param array $data Event data dictionary
* @return bool True if the action is executable
*/
public function isExecutable(array $data)
{
if (isset($data['project_id']) && $data['project_id'] == $this->project_id && $this->hasRequiredParameters($data)) {
return true;
}
return false;
}
/**
* Check if the event data has required parameters to execute the action
*
* @access public
* @param array $data Event data dictionary
* @return bool True if all keys are there
*/
public function hasRequiredParameters(array $data)
{
foreach ($this->getEventRequiredParameters() as $parameter) {
if (! isset($data[$parameter])) return false;
}
return true;
}
/**
* Execute the action
*
* @access public
* @param array $data Event data dictionary
* @return bool True if the action was executed or false when not executed
*/
public function execute(array $data)
{
if ($this->isExecutable($data)) {
return $this->doAction($data);
}
return false;
}
}

View file

@ -0,0 +1,85 @@
<?php
namespace Action;
use Model\Task;
/**
* Set a category automatically according to the color
*
* @package action
* @author Frederic Guillot
*/
class TaskAssignCategoryColor extends Base
{
/**
* Task model
*
* @accesss private
* @var \Model\Task
*/
private $task;
/**
* Constructor
*
* @access public
* @param integer $project_id Project id
* @param \Model\Task $task Task model instance
*/
public function __construct($project_id, Task $task)
{
parent::__construct($project_id);
$this->task = $task;
}
/**
* Get the required parameter for the action (defined by the user)
*
* @access public
* @return array
*/
public function getActionRequiredParameters()
{
return array(
'color_id' => t('Color'),
'category_id' => t('Category'),
);
}
/**
* Get the required parameter for the event
*
* @access public
* @return string[]
*/
public function getEventRequiredParameters()
{
return array(
'task_id',
'color_id',
);
}
/**
* Execute the action
*
* @access public
* @param array $data Event data dictionary
* @return bool True if the action was executed or false when not executed
*/
public function doAction(array $data)
{
if ($data['color_id'] == $this->getParam('color_id')) {
$this->task->update(array(
'id' => $data['task_id'],
'category_id' => $this->getParam('category_id'),
));
return true;
}
return false;
}
}

View file

@ -0,0 +1,85 @@
<?php
namespace Action;
use Model\Task;
/**
* Assign a color to a specific category
*
* @package action
* @author Frederic Guillot
*/
class TaskAssignColorCategory extends Base
{
/**
* Task model
*
* @accesss private
* @var \Model\Task
*/
private $task;
/**
* Constructor
*
* @access public
* @param integer $project_id Project id
* @param \Model\Task $task Task model instance
*/
public function __construct($project_id, Task $task)
{
parent::__construct($project_id);
$this->task = $task;
}
/**
* Get the required parameter for the action (defined by the user)
*
* @access public
* @return array
*/
public function getActionRequiredParameters()
{
return array(
'color_id' => t('Color'),
'category_id' => t('Category'),
);
}
/**
* Get the required parameter for the event
*
* @access public
* @return string[]
*/
public function getEventRequiredParameters()
{
return array(
'task_id',
'category_id',
);
}
/**
* Execute the action
*
* @access public
* @param array $data Event data dictionary
* @return bool True if the action was executed or false when not executed
*/
public function doAction(array $data)
{
if ($data['category_id'] == $this->getParam('category_id')) {
$this->task->update(array(
'id' => $data['task_id'],
'color_id' => $this->getParam('color_id'),
));
return true;
}
return false;
}
}

View file

@ -0,0 +1,85 @@
<?php
namespace Action;
use Model\Task;
/**
* Assign a color to a specific user
*
* @package action
* @author Frederic Guillot
*/
class TaskAssignColorUser extends Base
{
/**
* Task model
*
* @accesss private
* @var \Model\Task
*/
private $task;
/**
* Constructor
*
* @access public
* @param integer $project_id Project id
* @param \Model\Task $task Task model instance
*/
public function __construct($project_id, Task $task)
{
parent::__construct($project_id);
$this->task = $task;
}
/**
* Get the required parameter for the action (defined by the user)
*
* @access public
* @return array
*/
public function getActionRequiredParameters()
{
return array(
'color_id' => t('Color'),
'user_id' => t('Assignee'),
);
}
/**
* Get the required parameter for the event
*
* @access public
* @return string[]
*/
public function getEventRequiredParameters()
{
return array(
'task_id',
'owner_id',
);
}
/**
* Execute the action
*
* @access public
* @param array $data Event data dictionary
* @return bool True if the action was executed or false when not executed
*/
public function doAction(array $data)
{
if ($data['owner_id'] == $this->getParam('user_id')) {
$this->task->update(array(
'id' => $data['task_id'],
'color_id' => $this->getParam('color_id'),
));
return true;
}
return false;
}
}

View file

@ -0,0 +1,95 @@
<?php
namespace Action;
use Model\Task;
use Model\Acl;
/**
* Assign a task to the logged user
*
* @package action
* @author Frederic Guillot
*/
class TaskAssignCurrentUser extends Base
{
/**
* Task model
*
* @accesss private
* @var \Model\Task
*/
private $task;
/**
* Acl model
*
* @accesss private
* @var \Model\Acl
*/
private $acl;
/**
* Constructor
*
* @access public
* @param integer $project_id Project id
* @param \Model\Task $task Task model instance
* @param \Model\Acl $acl Acl model instance
*/
public function __construct($project_id, Task $task, Acl $acl)
{
parent::__construct($project_id);
$this->task = $task;
$this->acl = $acl;
}
/**
* Get the required parameter for the action (defined by the user)
*
* @access public
* @return array
*/
public function getActionRequiredParameters()
{
return array(
'column_id' => t('Column'),
);
}
/**
* Get the required parameter for the event
*
* @access public
* @return string[]
*/
public function getEventRequiredParameters()
{
return array(
'task_id',
'column_id',
);
}
/**
* Execute the action
*
* @access public
* @param array $data Event data dictionary
* @return bool True if the action was executed or false when not executed
*/
public function doAction(array $data)
{
if ($data['column_id'] == $this->getParam('column_id')) {
$this->task->update(array(
'id' => $data['task_id'],
'owner_id' => $this->acl->getUserId(),
));
return true;
}
return false;
}
}

View file

@ -0,0 +1,85 @@
<?php
namespace Action;
use Model\Task;
/**
* Assign a task to a specific user
*
* @package action
* @author Frederic Guillot
*/
class TaskAssignSpecificUser extends Base
{
/**
* Task model
*
* @accesss private
* @var \Model\Task
*/
private $task;
/**
* Constructor
*
* @access public
* @param integer $project_id Project id
* @param \Model\Task $task Task model instance
*/
public function __construct($project_id, Task $task)
{
parent::__construct($project_id);
$this->task = $task;
}
/**
* Get the required parameter for the action (defined by the user)
*
* @access public
* @return array
*/
public function getActionRequiredParameters()
{
return array(
'column_id' => t('Column'),
'user_id' => t('Assignee'),
);
}
/**
* Get the required parameter for the event
*
* @access public
* @return string[]
*/
public function getEventRequiredParameters()
{
return array(
'task_id',
'column_id',
);
}
/**
* Execute the action
*
* @access public
* @param array $data Event data dictionary
* @return bool True if the action was executed or false when not executed
*/
public function doAction(array $data)
{
if ($data['column_id'] == $this->getParam('column_id')) {
$this->task->update(array(
'id' => $data['task_id'],
'owner_id' => $this->getParam('user_id'),
));
return true;
}
return false;
}
}

View file

@ -0,0 +1,79 @@
<?php
namespace Action;
use Model\Task;
/**
* Close automatically a task
*
* @package action
* @author Frederic Guillot
*/
class TaskClose extends Base
{
/**
* Task model
*
* @accesss private
* @var \Model\Task
*/
private $task;
/**
* Constructor
*
* @access public
* @param integer $project_id Project id
* @param \Model\Task $task Task model instance
*/
public function __construct($project_id, Task $task)
{
parent::__construct($project_id);
$this->task = $task;
}
/**
* Get the required parameter for the action (defined by the user)
*
* @access public
* @return array
*/
public function getActionRequiredParameters()
{
return array(
'column_id' => t('Column'),
);
}
/**
* Get the required parameter for the event
*
* @access public
* @return string[]
*/
public function getEventRequiredParameters()
{
return array(
'task_id',
'column_id',
);
}
/**
* Execute the action
*
* @access public
* @param array $data Event data dictionary
* @return bool True if the action was executed or false when not executed
*/
public function doAction(array $data)
{
if ($data['column_id'] == $this->getParam('column_id')) {
$this->task->close($data['task_id']);
return true;
}
return false;
}
}

View file

@ -0,0 +1,83 @@
<?php
namespace Action;
use Model\Task;
/**
* Duplicate a task to another project
*
* @package action
* @author Frederic Guillot
*/
class TaskDuplicateAnotherProject extends Base
{
/**
* Task model
*
* @accesss private
* @var \Model\Task
*/
private $task;
/**
* Constructor
*
* @access public
* @param integer $project_id Project id
* @param \Model\Task $task Task model instance
*/
public function __construct($project_id, Task $task)
{
parent::__construct($project_id);
$this->task = $task;
}
/**
* Get the required parameter for the action (defined by the user)
*
* @access public
* @return array
*/
public function getActionRequiredParameters()
{
return array(
'column_id' => t('Column'),
'project_id' => t('Project'),
);
}
/**
* Get the required parameter for the event
*
* @access public
* @return string[]
*/
public function getEventRequiredParameters()
{
return array(
'task_id',
'column_id',
'project_id',
);
}
/**
* Execute the action
*
* @access public
* @param array $data Event data dictionary
* @return bool True if the action was executed or false when not executed
*/
public function doAction(array $data)
{
if ($data['column_id'] == $this->getParam('column_id') && $data['project_id'] != $this->getParam('project_id')) {
$this->task->duplicateToAnotherProject($data['task_id'], $this->getParam('project_id'));
return true;
}
return false;
}
}

View file

@ -0,0 +1,143 @@
<?php
namespace Controller;
/**
* Automatic actions management
*
* @package controller
* @author Frederic Guillot
*/
class Action extends Base
{
/**
* List of automatic actions for a given project
*
* @access public
*/
public function index()
{
$project_id = $this->request->getIntegerParam('project_id');
$project = $this->project->getById($project_id);
if (! $project) {
$this->session->flashError(t('Project not found.'));
$this->response->redirect('?controller=project');
}
$this->response->html($this->template->layout('action_index', array(
'values' => array('project_id' => $project['id']),
'project' => $project,
'actions' => $this->action->getAllByProject($project['id']),
'available_actions' => $this->action->getAvailableActions(),
'available_events' => $this->action->getAvailableEvents(),
'available_params' => $this->action->getAllActionParameters(),
'columns_list' => $this->board->getColumnsList($project['id']),
'users_list' => $this->project->getUsersList($project['id']),
'projects_list' => $this->project->getList(false),
'colors_list' => $this->task->getColors(),
'categories_list' => $this->category->getList($project['id']),
'menu' => 'projects',
'title' => t('Automatic actions')
)));
}
/**
* Define action parameters (step 2)
*
* @access public
*/
public function params()
{
$project_id = $this->request->getIntegerParam('project_id');
$project = $this->project->getById($project_id);
if (! $project) {
$this->session->flashError(t('Project not found.'));
$this->response->redirect('?controller=project');
}
$values = $this->request->getValues();
$action = $this->action->load($values['action_name'], $values['project_id']);
$this->response->html($this->template->layout('action_params', array(
'values' => $values,
'action_params' => $action->getActionRequiredParameters(),
'columns_list' => $this->board->getColumnsList($project['id']),
'users_list' => $this->project->getUsersList($project['id']),
'projects_list' => $this->project->getList(false),
'colors_list' => $this->task->getColors(),
'categories_list' => $this->category->getList($project['id']),
'project' => $project,
'menu' => 'projects',
'title' => t('Automatic actions')
)));
}
/**
* Create a new action (last step)
*
* @access public
*/
public function create()
{
$project_id = $this->request->getIntegerParam('project_id');
$project = $this->project->getById($project_id);
if (! $project) {
$this->session->flashError(t('Project not found.'));
$this->response->redirect('?controller=project');
}
$values = $this->request->getValues();
list($valid,) = $this->action->validateCreation($values);
if ($valid) {
if ($this->action->create($values)) {
$this->session->flash(t('Your automatic action have been created successfully.'));
}
else {
$this->session->flashError(t('Unable to create your automatic action.'));
}
}
$this->response->redirect('?controller=action&action=index&project_id='.$project['id']);
}
/**
* Confirmation dialog before removing an action
*
* @access public
*/
public function confirm()
{
$this->response->html($this->template->layout('action_remove', array(
'action' => $this->action->getById($this->request->getIntegerParam('action_id')),
'available_events' => $this->action->getAvailableEvents(),
'available_actions' => $this->action->getAvailableActions(),
'menu' => 'projects',
'title' => t('Remove an action')
)));
}
/**
* Remove an action
*
* @access public
*/
public function remove()
{
$this->checkCSRFParam();
$action = $this->action->getById($this->request->getIntegerParam('action_id'));
if ($action && $this->action->remove($action['id'])) {
$this->session->flash(t('Action removed successfully.'));
} else {
$this->session->flashError(t('Unable to remove this action.'));
}
$this->response->redirect('?controller=action&action=index&project_id='.$action['project_id']);
}
}

View file

@ -0,0 +1,29 @@
<?php
namespace Controller;
use Model\Project as ProjectModel;
/**
* Application controller
*
* @package controller
* @author Frederic Guillot
*/
class App extends Base
{
/**
* Redirect to the project creation page or the board controller
*
* @access public
*/
public function index()
{
if ($this->project->countByStatus(ProjectModel::ACTIVE)) {
$this->response->redirect('?controller=board');
}
else {
$this->redirectNoProject();
}
}
}

View file

@ -0,0 +1,249 @@
<?php
namespace Controller;
use Core\Registry;
use Core\Security;
use Core\Translator;
use Model\LastLogin;
/**
* Base controller
*
* @package controller
* @author Frederic Guillot
* @property \Model\Acl $acl
* @property \Model\Action $action
* @property \Model\Board $board
* @property \Model\Category $category
* @property \Model\Comment $comment
* @property \Model\Config $config
* @property \Model\File $file
* @property \Model\Google $google
* @property \Model\GitHub $gitHub
* @property \Model\LastLogin $lastLogin
* @property \Model\Ldap $ldap
* @property \Model\Project $project
* @property \Model\RememberMe $rememberMe
* @property \Model\SubTask $subTask
* @property \Model\Task $task
* @property \Model\User $user
*/
abstract class Base
{
/**
* Request instance
*
* @accesss public
* @var \Core\Request
*/
public $request;
/**
* Response instance
*
* @accesss public
* @var \Core\Response
*/
public $response;
/**
* Template instance
*
* @accesss public
* @var \Core\Template
*/
public $template;
/**
* Session instance
*
* @accesss public
* @var \Core\Session
*/
public $session;
/**
* Registry instance
*
* @access private
* @var \Core\Registry
*/
private $registry;
/**
* Constructor
*
* @access public
* @param \Core\Registry $registry Registry instance
*/
public function __construct(Registry $registry)
{
$this->registry = $registry;
}
/**
* Load automatically models
*
* @access public
* @param string $name Model name
* @return mixed
*/
public function __get($name)
{
$class = '\Model\\'.ucfirst($name);
$this->registry->$name = new $class($this->registry->shared('db'), $this->registry->shared('event'));
return $this->registry->shared($name);
}
/**
* Method executed before each action
*
* @access public
*/
public function beforeAction($controller, $action)
{
// Start the session
$this->session->open(BASE_URL_DIRECTORY, SESSION_SAVE_PATH);
// HTTP secure headers
$this->response->csp(array('style-src' => "'self' 'unsafe-inline'"));
$this->response->nosniff();
$this->response->xss();
$this->response->hsts();
$this->response->xframe();
// Load translations
$language = $this->config->get('language', 'en_US');
if ($language !== 'en_US') Translator::load($language);
// Set timezone
date_default_timezone_set($this->config->get('timezone', 'UTC'));
// Authentication
if (! $this->acl->isLogged() && ! $this->acl->isPublicAction($controller, $action)) {
// Try the remember me authentication first
if (! $this->rememberMe->authenticate()) {
// Redirect to the login form if not authenticated
$this->response->redirect('?controller=user&action=login');
}
else {
$this->lastLogin->create(
LastLogin::AUTH_REMEMBER_ME,
$this->acl->getUserId(),
$this->user->getIpAddress(),
$this->user->getUserAgent()
);
}
}
else if ($this->rememberMe->hasCookie()) {
$this->rememberMe->refresh();
}
// Check if the user is allowed to see this page
if (! $this->acl->isPageAccessAllowed($controller, $action)) {
$this->response->redirect('?controller=user&action=forbidden');
}
// Attach events
$this->action->attachEvents();
$this->project->attachEvents();
}
/**
* Application not found page (404 error)
*
* @access public
*/
public function notfound()
{
$this->response->html($this->template->layout('app_notfound', array('title' => t('Page not found'))));
}
/**
* Application forbidden page
*
* @access public
*/
public function forbidden()
{
$this->response->html($this->template->layout('app_forbidden', array('title' => t('Access Forbidden'))));
}
/**
* Check if the CSRF token from the URL is correct
*
* @access protected
*/
protected function checkCSRFParam()
{
if (! Security::validateCSRFToken($this->request->getStringParam('csrf_token'))) {
$this->forbidden();
}
}
/**
* Check if the current user have access to the given project
*
* @access protected
* @param integer $project_id Project id
*/
protected function checkProjectPermissions($project_id)
{
if ($this->acl->isRegularUser()) {
if ($project_id > 0 && ! $this->project->isUserAllowed($project_id, $this->acl->getUserId())) {
$this->forbidden();
}
}
}
/**
* Redirection when there is no project in the database
*
* @access protected
*/
protected function redirectNoProject()
{
$this->session->flash(t('There is no active project, the first step is to create a new project.'));
$this->response->redirect('?controller=project&action=create');
}
/**
* Common layout for task views
*
* @access protected
* @param string $template Template name
* @param array $params Template parameters
* @return string
*/
protected function taskLayout($template, array $params)
{
$content = $this->template->load($template, $params);
$params['task_content_for_layout'] = $content;
return $this->template->layout('task_layout', $params);
}
/**
* Common method to get a task for task views
*
* @access protected
* @return array
*/
protected function getTask()
{
$task = $this->task->getById($this->request->getIntegerParam('task_id'), true);
if (! $task) {
$this->notfound();
}
$this->checkProjectPermissions($task['project_id']);
return $task;
}
}

View file

@ -0,0 +1,426 @@
<?php
namespace Controller;
use Model\Project as ProjectModel;
use Model\User as UserModel;
use Core\Security;
/**
* Board controller
*
* @package controller
* @author Frederic Guillot
*/
class Board extends Base
{
/**
* Move a column up
*
* @access public
*/
public function moveUp()
{
$this->checkCSRFParam();
$project_id = $this->request->getIntegerParam('project_id');
$column_id = $this->request->getIntegerParam('column_id');
$this->board->moveUp($project_id, $column_id);
$this->response->redirect('?controller=board&action=edit&project_id='.$project_id);
}
/**
* Move a column down
*
* @access public
*/
public function moveDown()
{
$this->checkCSRFParam();
$project_id = $this->request->getIntegerParam('project_id');
$column_id = $this->request->getIntegerParam('column_id');
$this->board->moveDown($project_id, $column_id);
$this->response->redirect('?controller=board&action=edit&project_id='.$project_id);
}
/**
* Change a task assignee directly from the board
*
* @access public
*/
public function assign()
{
$task = $this->task->getById($this->request->getIntegerParam('task_id'));
$project = $this->project->getById($task['project_id']);
$projects = $this->project->getListByStatus(ProjectModel::ACTIVE);
if ($this->acl->isRegularUser()) {
$projects = $this->project->filterListByAccess($projects, $this->acl->getUserId());
}
if (! $project) $this->notfound();
$this->checkProjectPermissions($project['id']);
if ($this->request->isAjax()) {
$this->response->html($this->template->load('board_assign', array(
'errors' => array(),
'values' => $task,
'users_list' => $this->project->getUsersList($project['id']),
'projects' => $projects,
'current_project_id' => $project['id'],
'current_project_name' => $project['name'],
)));
}
else {
$this->response->html($this->template->layout('board_assign', array(
'errors' => array(),
'values' => $task,
'users_list' => $this->project->getUsersList($project['id']),
'projects' => $projects,
'current_project_id' => $project['id'],
'current_project_name' => $project['name'],
'menu' => 'boards',
'title' => t('Change assignee').' - '.$task['title'],
)));
}
}
/**
* Validate an assignee modification
*
* @access public
*/
public function assignTask()
{
$values = $this->request->getValues();
$this->checkProjectPermissions($values['project_id']);
list($valid,) = $this->task->validateAssigneeModification($values);
if ($valid && $this->task->update($values)) {
$this->session->flash(t('Task updated successfully.'));
}
else {
$this->session->flashError(t('Unable to update your task.'));
}
$this->response->redirect('?controller=board&action=show&project_id='.$values['project_id']);
}
/**
* Display the public version of a board
* Access checked by a simple token, no user login, read only, auto-refresh
*
* @access public
*/
public function readonly()
{
$token = $this->request->getStringParam('token');
$project = $this->project->getByToken($token);
// Token verification
if (! $project) {
$this->response->text('Not Authorized', 401);
}
// Display the board with a specific layout
$this->response->html($this->template->layout('board_public', array(
'project' => $project,
'columns' => $this->board->get($project['id']),
'categories' => $this->category->getList($project['id'], false),
'title' => $project['name'],
'no_layout' => true,
'auto_refresh' => true,
)));
}
/**
* Redirect the user to the default project
*
* @access public
*/
public function index()
{
$projects = $this->project->getListByStatus(ProjectModel::ACTIVE);
$project_id = 0;
$project_name = '';
if ($this->acl->isRegularUser()) {
$projects = $this->project->filterListByAccess($projects, $this->acl->getUserId());
}
if (empty($projects)) {
if ($this->acl->isAdminUser()) {
$this->redirectNoProject();
}
else {
$this->response->redirect('?controller=project&action=forbidden');
}
}
else if (! empty($_SESSION['user']['default_project_id']) && isset($projects[$_SESSION['user']['default_project_id']])) {
$project_id = $_SESSION['user']['default_project_id'];
$project_name = $projects[$_SESSION['user']['default_project_id']];
}
else {
list($project_id, $project_name) = each($projects);
}
$this->response->redirect('?controller=board&action=show&project_id='.$project_id);
}
/**
* Show a board for a given project
*
* @access public
*/
public function show()
{
$project_id = $this->request->getIntegerParam('project_id');
$user_id = $this->request->getIntegerParam('user_id', UserModel::EVERYBODY_ID);
$this->checkProjectPermissions($project_id);
$projects = $this->project->getAvailableList($this->acl->getUserId());
if (! isset($projects[$project_id])) {
$this->notfound();
}
$this->response->html($this->template->layout('board_index', array(
'users' => $this->project->getUsersList($project_id, true, true),
'filters' => array('user_id' => $user_id),
'projects' => $projects,
'current_project_id' => $project_id,
'current_project_name' => $projects[$project_id],
'board' => $this->board->get($project_id),
'categories' => $this->category->getList($project_id, true, true),
'menu' => 'boards',
'title' => $projects[$project_id],
'board_selector' => $projects,
)));
}
/**
* Display a form to edit a board
*
* @access public
*/
public function edit()
{
$project_id = $this->request->getIntegerParam('project_id');
$project = $this->project->getById($project_id);
if (! $project) $this->notfound();
$columns = $this->board->getColumns($project_id);
$values = array();
foreach ($columns as $column) {
$values['title['.$column['id'].']'] = $column['title'];
$values['task_limit['.$column['id'].']'] = $column['task_limit'] ?: null;
}
$this->response->html($this->template->layout('board_edit', array(
'errors' => array(),
'values' => $values + array('project_id' => $project_id),
'columns' => $columns,
'project' => $project,
'menu' => 'projects',
'title' => t('Edit board')
)));
}
/**
* Validate and update a board
*
* @access public
*/
public function update()
{
$project_id = $this->request->getIntegerParam('project_id');
$project = $this->project->getById($project_id);
if (! $project) $this->notfound();
$columns = $this->board->getColumns($project_id);
$data = $this->request->getValues();
$values = $columns_list = array();
foreach ($columns as $column) {
$columns_list[$column['id']] = $column['title'];
$values['title['.$column['id'].']'] = isset($data['title'][$column['id']]) ? $data['title'][$column['id']] : '';
$values['task_limit['.$column['id'].']'] = isset($data['task_limit'][$column['id']]) ? $data['task_limit'][$column['id']] : 0;
}
list($valid, $errors) = $this->board->validateModification($columns_list, $values);
if ($valid) {
if ($this->board->update($data)) {
$this->session->flash(t('Board updated successfully.'));
$this->response->redirect('?controller=board&action=edit&project_id='.$project['id']);
}
else {
$this->session->flashError(t('Unable to update this board.'));
}
}
$this->response->html($this->template->layout('board_edit', array(
'errors' => $errors,
'values' => $values + array('project_id' => $project_id),
'columns' => $columns,
'project' => $project,
'menu' => 'projects',
'title' => t('Edit board')
)));
}
/**
* Validate and add a new column
*
* @access public
*/
public function add()
{
$project_id = $this->request->getIntegerParam('project_id');
$project = $this->project->getById($project_id);
if (! $project) $this->notfound();
$columns = $this->board->getColumnsList($project_id);
$data = $this->request->getValues();
$values = array();
foreach ($columns as $column_id => $column_title) {
$values['title['.$column_id.']'] = $column_title;
}
list($valid, $errors) = $this->board->validateCreation($data);
if ($valid) {
if ($this->board->add($data)) {
$this->session->flash(t('Board updated successfully.'));
$this->response->redirect('?controller=board&action=edit&project_id='.$project['id']);
}
else {
$this->session->flashError(t('Unable to update this board.'));
}
}
$this->response->html($this->template->layout('board_edit', array(
'errors' => $errors,
'values' => $values + $data,
'columns' => $columns,
'project' => $project,
'menu' => 'projects',
'title' => t('Edit board')
)));
}
/**
* Confirmation dialog before removing a column
*
* @access public
*/
public function confirm()
{
$this->response->html($this->template->layout('board_remove', array(
'column' => $this->board->getColumn($this->request->getIntegerParam('column_id')),
'menu' => 'projects',
'title' => t('Remove a column from a board')
)));
}
/**
* Remove a column
*
* @access public
*/
public function remove()
{
$this->checkCSRFParam();
$column = $this->board->getColumn($this->request->getIntegerParam('column_id'));
if ($column && $this->board->removeColumn($column['id'])) {
$this->session->flash(t('Column removed successfully.'));
} else {
$this->session->flashError(t('Unable to remove this column.'));
}
$this->response->redirect('?controller=board&action=edit&project_id='.$column['project_id']);
}
/**
* Save the board (Ajax request made by the drag and drop)
*
* @access public
*/
public function save()
{
if ($this->request->isAjax()) {
$project_id = $this->request->getIntegerParam('project_id');
$values = $this->request->getValues();
if ($project_id > 0 && ! $this->project->isUserAllowed($project_id, $this->acl->getUserId())) {
$this->response->text('Not Authorized', 401);
}
if (isset($values['positions'])) {
$this->board->saveTasksPosition($values['positions']);
}
$this->response->html(
$this->template->load('board_show', array(
'current_project_id' => $project_id,
'board' => $this->board->get($project_id),
'categories' => $this->category->getList($project_id, false),
)),
201
);
}
else {
$this->response->status(401);
}
}
/**
* Check if the board have been changed
*
* @access public
*/
public function check()
{
if ($this->request->isAjax()) {
$project_id = $this->request->getIntegerParam('project_id');
$timestamp = $this->request->getIntegerParam('timestamp');
if ($project_id > 0 && ! $this->project->isUserAllowed($project_id, $this->acl->getUserId())) {
$this->response->text('Not Authorized', 401);
}
if ($this->project->isModifiedSince($project_id, $timestamp)) {
$this->response->html(
$this->template->load('board_show', array(
'current_project_id' => $project_id,
'board' => $this->board->get($project_id),
'categories' => $this->category->getList($project_id, false),
))
);
}
else {
$this->response->status(304);
}
}
else {
$this->response->status(401);
}
}
}

View file

@ -0,0 +1,191 @@
<?php
namespace Controller;
/**
* Categories management
*
* @package controller
* @author Frederic Guillot
*/
class Category extends Base
{
/**
* Get the current project (common method between actions)
*
* @access private
* @return array
*/
private function getProject()
{
$project_id = $this->request->getIntegerParam('project_id');
$project = $this->project->getById($project_id);
if (! $project) {
$this->session->flashError(t('Project not found.'));
$this->response->redirect('?controller=project');
}
return $project;
}
/**
* Get the category (common method between actions)
*
* @access private
* @param $project_id
* @return array
*/
private function getCategory($project_id)
{
$category = $this->category->getById($this->request->getIntegerParam('category_id'));
if (! $category) {
$this->session->flashError(t('Category not found.'));
$this->response->redirect('?controller=category&action=index&project_id='.$project_id);
}
return $category;
}
/**
* List of categories for a given project
*
* @access public
*/
public function index()
{
$project = $this->getProject();
$this->response->html($this->template->layout('category_index', array(
'categories' => $this->category->getList($project['id'], false),
'values' => array('project_id' => $project['id']),
'errors' => array(),
'project' => $project,
'menu' => 'projects',
'title' => t('Categories')
)));
}
/**
* Validate and save a new project
*
* @access public
*/
public function save()
{
$project = $this->getProject();
$values = $this->request->getValues();
list($valid, $errors) = $this->category->validateCreation($values);
if ($valid) {
if ($this->category->create($values)) {
$this->session->flash(t('Your category have been created successfully.'));
$this->response->redirect('?controller=category&action=index&project_id='.$project['id']);
}
else {
$this->session->flashError(t('Unable to create your category.'));
}
}
$this->response->html($this->template->layout('category_index', array(
'categories' => $this->category->getList($project['id'], false),
'values' => $values,
'errors' => $errors,
'project' => $project,
'menu' => 'projects',
'title' => t('Categories')
)));
}
/**
* Edit a category (display the form)
*
* @access public
*/
public function edit()
{
$project = $this->getProject();
$category = $this->getCategory($project['id']);
$this->response->html($this->template->layout('category_edit', array(
'values' => $category,
'errors' => array(),
'project' => $project,
'menu' => 'projects',
'title' => t('Categories')
)));
}
/**
* Edit a category (validate the form and update the database)
*
* @access public
*/
public function update()
{
$project = $this->getProject();
$values = $this->request->getValues();
list($valid, $errors) = $this->category->validateModification($values);
if ($valid) {
if ($this->category->update($values)) {
$this->session->flash(t('Your category have been updated successfully.'));
$this->response->redirect('?controller=category&action=index&project_id='.$project['id']);
}
else {
$this->session->flashError(t('Unable to update your category.'));
}
}
$this->response->html($this->template->layout('category_edit', array(
'values' => $values,
'errors' => $errors,
'project' => $project,
'menu' => 'projects',
'title' => t('Categories')
)));
}
/**
* Confirmation dialog before removing a category
*
* @access public
*/
public function confirm()
{
$project = $this->getProject();
$category = $this->getCategory($project['id']);
$this->response->html($this->template->layout('category_remove', array(
'project' => $project,
'category' => $category,
'menu' => 'projects',
'title' => t('Remove a category')
)));
}
/**
* Remove a category
*
* @access public
*/
public function remove()
{
$this->checkCSRFParam();
$project = $this->getProject();
$category = $this->getCategory($project['id']);
if ($this->category->remove($category['id'])) {
$this->session->flash(t('Category removed successfully.'));
} else {
$this->session->flashError(t('Unable to remove this category.'));
}
$this->response->redirect('?controller=category&action=index&project_id='.$project['id']);
}
}

View file

@ -0,0 +1,194 @@
<?php
namespace Controller;
/**
* Comment controller
*
* @package controller
* @author Frederic Guillot
*/
class Comment extends Base
{
/**
* Get the current comment
*
* @access private
* @return array
*/
private function getComment()
{
$comment = $this->comment->getById($this->request->getIntegerParam('comment_id'));
if (! $comment) {
$this->notfound();
}
if (! $this->acl->isAdminUser() && $comment['user_id'] != $this->acl->getUserId()) {
$this->forbidden();
}
return $comment;
}
/**
* Forbidden page for comments
*
* @access public
*/
public function forbidden()
{
$this->response->html($this->template->layout('comment_forbidden', array(
'menu' => 'tasks',
'title' => t('Access Forbidden')
)));
}
/**
* Add comment form
*
* @access public
*/
public function create()
{
$task = $this->getTask();
$this->response->html($this->taskLayout('comment_create', array(
'values' => array(
'user_id' => $this->acl->getUserId(),
'task_id' => $task['id'],
),
'errors' => array(),
'task' => $task,
'menu' => 'tasks',
'title' => t('Add a comment')
)));
}
/**
* Add a comment
*
* @access public
*/
public function save()
{
$task = $this->getTask();
$values = $this->request->getValues();
list($valid, $errors) = $this->comment->validateCreation($values);
if ($valid) {
if ($this->comment->create($values)) {
$this->session->flash(t('Comment added successfully.'));
}
else {
$this->session->flashError(t('Unable to create your comment.'));
}
$this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'#comments');
}
$this->response->html($this->taskLayout('comment_create', array(
'values' => $values,
'errors' => $errors,
'task' => $task,
'menu' => 'tasks',
'title' => t('Add a comment')
)));
}
/**
* Edit a comment
*
* @access public
*/
public function edit()
{
$task = $this->getTask();
$comment = $this->getComment();
$this->response->html($this->taskLayout('comment_edit', array(
'values' => $comment,
'errors' => array(),
'comment' => $comment,
'task' => $task,
'menu' => 'tasks',
'title' => t('Edit a comment')
)));
}
/**
* Update and validate a comment
*
* @access public
*/
public function update()
{
$task = $this->getTask();
$comment = $this->getComment();
$values = $this->request->getValues();
list($valid, $errors) = $this->comment->validateModification($values);
if ($valid) {
if ($this->comment->update($values)) {
$this->session->flash(t('Comment updated successfully.'));
}
else {
$this->session->flashError(t('Unable to update your comment.'));
}
$this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'#comment-'.$comment['id']);
}
$this->response->html($this->taskLayout('comment_edit', array(
'values' => $values,
'errors' => $errors,
'comment' => $comment,
'task' => $task,
'menu' => 'tasks',
'title' => t('Edit a comment')
)));
}
/**
* Confirmation dialog before removing a comment
*
* @access public
*/
public function confirm()
{
$task = $this->getTask();
$comment = $this->getComment();
$this->response->html($this->taskLayout('comment_remove', array(
'comment' => $comment,
'task' => $task,
'menu' => 'tasks',
'title' => t('Remove a comment')
)));
}
/**
* Remove a comment
*
* @access public
*/
public function remove()
{
$this->checkCSRFParam();
$task = $this->getTask();
$comment = $this->getComment();
if ($this->comment->remove($comment['id'])) {
$this->session->flash(t('Comment removed successfully.'));
}
else {
$this->session->flashError(t('Unable to remove this comment.'));
}
$this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'#comments');
}
}

View file

@ -0,0 +1,144 @@
<?php
namespace Controller;
use Model\File as FileModel;
/**
* File controller
*
* @package controller
* @author Frederic Guillot
*/
class File extends Base
{
/**
* File upload form
*
* @access public
*/
public function create()
{
$task = $this->getTask();
$this->response->html($this->taskLayout('file_new', array(
'task' => $task,
'menu' => 'tasks',
'max_size' => ini_get('upload_max_filesize'),
'title' => t('Attach a document')
)));
}
/**
* File upload (save files)
*
* @access public
*/
public function save()
{
$task = $this->getTask();
if ($this->file->upload($task['project_id'], $task['id'], 'files') === true) {
$this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'#attachments');
}
else {
$this->session->flashError(t('Unable to upload the file.'));
$this->response->redirect('?controller=file&action=create&task_id='.$task['id']);
}
}
/**
* File download
*
* @access public
*/
public function download()
{
$task = $this->getTask();
$file = $this->file->getById($this->request->getIntegerParam('file_id'));
$filename = FileModel::BASE_PATH.$file['path'];
if ($file['task_id'] == $task['id'] && file_exists($filename)) {
$this->response->forceDownload($file['name']);
$this->response->binary(file_get_contents($filename));
}
$this->response->redirect('?controller=task&action=show&task_id='.$task['id']);
}
/**
* Open a file (show the content in a popover)
*
* @access public
*/
public function open()
{
$task = $this->getTask();
$file = $this->file->getById($this->request->getIntegerParam('file_id'));
if ($file['task_id'] == $task['id']) {
$this->response->html($this->template->load('file_open', array(
'file' => $file
)));
}
}
/**
* Return the file content (work only for images)
*
* @access public
*/
public function image()
{
$task = $this->getTask();
$file = $this->file->getById($this->request->getIntegerParam('file_id'));
$filename = FileModel::BASE_PATH.$file['path'];
if ($file['task_id'] == $task['id'] && file_exists($filename)) {
$metadata = getimagesize($filename);
if (isset($metadata['mime'])) {
$this->response->contentType($metadata['mime']);
readfile($filename);
}
}
}
/**
* Remove a file
*
* @access public
*/
public function remove()
{
$this->checkCSRFParam();
$task = $this->getTask();
$file = $this->file->getById($this->request->getIntegerParam('file_id'));
if ($file['task_id'] == $task['id'] && $this->file->remove($file['id'])) {
$this->session->flash(t('File removed successfully.'));
} else {
$this->session->flashError(t('Unable to remove this file.'));
}
$this->response->redirect('?controller=task&action=show&task_id='.$task['id']);
}
/**
* Confirmation dialog before removing a file
*
* @access public
*/
public function confirm()
{
$task = $this->getTask();
$file = $this->file->getById($this->request->getIntegerParam('file_id'));
$this->response->html($this->taskLayout('file_remove', array(
'task' => $task,
'file' => $file,
'menu' => 'tasks',
'title' => t('Remove a file')
)));
}
}

View file

@ -0,0 +1,367 @@
<?php
namespace Controller;
use Model\Task as TaskModel;
/**
* Project controller
*
* @package controller
* @author Frederic Guillot
*/
class Project extends Base
{
/**
* Task search for a given project
*
* @access public
*/
public function search()
{
$project_id = $this->request->getIntegerParam('project_id');
$search = $this->request->getStringParam('search');
$project = $this->project->getById($project_id);
$tasks = array();
$nb_tasks = 0;
if (! $project) {
$this->session->flashError(t('Project not found.'));
$this->response->redirect('?controller=project');
}
$this->checkProjectPermissions($project['id']);
if ($search !== '') {
$filters = array(
array('column' => 'project_id', 'operator' => 'eq', 'value' => $project_id),
'or' => array(
array('column' => 'title', 'operator' => 'like', 'value' => '%'.$search.'%'),
//array('column' => 'description', 'operator' => 'like', 'value' => '%'.$search.'%'),
)
);
$tasks = $this->task->find($filters);
$nb_tasks = count($tasks);
}
$this->response->html($this->template->layout('project_search', array(
'tasks' => $tasks,
'nb_tasks' => $nb_tasks,
'values' => array(
'search' => $search,
'controller' => 'project',
'action' => 'search',
'project_id' => $project['id'],
),
'menu' => 'projects',
'project' => $project,
'columns' => $this->board->getColumnsList($project_id),
'categories' => $this->category->getList($project['id'], false),
'title' => $project['name'].($nb_tasks > 0 ? ' ('.$nb_tasks.')' : '')
)));
}
/**
* List of completed tasks for a given project
*
* @access public
*/
public function tasks()
{
$project_id = $this->request->getIntegerParam('project_id');
$project = $this->project->getById($project_id);
if (! $project) {
$this->session->flashError(t('Project not found.'));
$this->response->redirect('?controller=project');
}
$this->checkProjectPermissions($project['id']);
$filters = array(
array('column' => 'project_id', 'operator' => 'eq', 'value' => $project_id),
array('column' => 'is_active', 'operator' => 'eq', 'value' => TaskModel::STATUS_CLOSED),
);
$tasks = $this->task->find($filters);
$nb_tasks = count($tasks);
$this->response->html($this->template->layout('project_tasks', array(
'menu' => 'projects',
'project' => $project,
'columns' => $this->board->getColumnsList($project_id),
'categories' => $this->category->getList($project['id'], false),
'tasks' => $tasks,
'nb_tasks' => $nb_tasks,
'title' => $project['name'].' ('.$nb_tasks.')'
)));
}
/**
* List of projects
*
* @access public
*/
public function index()
{
$projects = $this->project->getAll(true, $this->acl->isRegularUser());
$nb_projects = count($projects);
$this->response->html($this->template->layout('project_index', array(
'projects' => $projects,
'nb_projects' => $nb_projects,
'menu' => 'projects',
'title' => t('Projects').' ('.$nb_projects.')'
)));
}
/**
* Display a form to create a new project
*
* @access public
*/
public function create()
{
$this->response->html($this->template->layout('project_new', array(
'errors' => array(),
'values' => array(),
'menu' => 'projects',
'title' => t('New project')
)));
}
/**
* Validate and save a new project
*
* @access public
*/
public function save()
{
$values = $this->request->getValues();
list($valid, $errors) = $this->project->validateCreation($values);
if ($valid) {
if ($this->project->create($values)) {
$this->session->flash(t('Your project have been created successfully.'));
$this->response->redirect('?controller=project');
}
else {
$this->session->flashError(t('Unable to create your project.'));
}
}
$this->response->html($this->template->layout('project_new', array(
'errors' => $errors,
'values' => $values,
'menu' => 'projects',
'title' => t('New Project')
)));
}
/**
* Display a form to edit a project
*
* @access public
*/
public function edit()
{
$project = $this->project->getById($this->request->getIntegerParam('project_id'));
if (! $project) {
$this->session->flashError(t('Project not found.'));
$this->response->redirect('?controller=project');
}
$this->response->html($this->template->layout('project_edit', array(
'errors' => array(),
'values' => $project,
'menu' => 'projects',
'title' => t('Edit project')
)));
}
/**
* Validate and update a project
*
* @access public
*/
public function update()
{
$values = $this->request->getValues() + array('is_active' => 0);
list($valid, $errors) = $this->project->validateModification($values);
if ($valid) {
if ($this->project->update($values)) {
$this->session->flash(t('Project updated successfully.'));
$this->response->redirect('?controller=project');
}
else {
$this->session->flashError(t('Unable to update this project.'));
}
}
$this->response->html($this->template->layout('project_edit', array(
'errors' => $errors,
'values' => $values,
'menu' => 'projects',
'title' => t('Edit Project')
)));
}
/**
* Confirmation dialog before to remove a project
*
* @access public
*/
public function confirm()
{
$project = $this->project->getById($this->request->getIntegerParam('project_id'));
if (! $project) {
$this->session->flashError(t('Project not found.'));
$this->response->redirect('?controller=project');
}
$this->response->html($this->template->layout('project_remove', array(
'project' => $project,
'menu' => 'projects',
'title' => t('Remove project')
)));
}
/**
* Remove a project
*
* @access public
*/
public function remove()
{
$this->checkCSRFParam();
$project_id = $this->request->getIntegerParam('project_id');
if ($project_id && $this->project->remove($project_id)) {
$this->session->flash(t('Project removed successfully.'));
} else {
$this->session->flashError(t('Unable to remove this project.'));
}
$this->response->redirect('?controller=project');
}
/**
* Enable a project
*
* @access public
*/
public function enable()
{
$this->checkCSRFParam();
$project_id = $this->request->getIntegerParam('project_id');
if ($project_id && $this->project->enable($project_id)) {
$this->session->flash(t('Project activated successfully.'));
} else {
$this->session->flashError(t('Unable to activate this project.'));
}
$this->response->redirect('?controller=project');
}
/**
* Disable a project
*
* @access public
*/
public function disable()
{
$this->checkCSRFParam();
$project_id = $this->request->getIntegerParam('project_id');
if ($project_id && $this->project->disable($project_id)) {
$this->session->flash(t('Project disabled successfully.'));
} else {
$this->session->flashError(t('Unable to disable this project.'));
}
$this->response->redirect('?controller=project');
}
/**
* Users list for the selected project
*
* @access public
*/
public function users()
{
$project = $this->project->getById($this->request->getIntegerParam('project_id'));
if (! $project) {
$this->session->flashError(t('Project not found.'));
$this->response->redirect('?controller=project');
}
$this->response->html($this->template->layout('project_users', array(
'project' => $project,
'users' => $this->project->getAllUsers($project['id']),
'menu' => 'projects',
'title' => t('Edit project access list')
)));
}
/**
* Allow a specific user for the selected project
*
* @access public
*/
public function allow()
{
$values = $this->request->getValues();
list($valid,) = $this->project->validateUserAccess($values);
if ($valid) {
if ($this->project->allowUser($values['project_id'], $values['user_id'])) {
$this->session->flash(t('Project updated successfully.'));
}
else {
$this->session->flashError(t('Unable to update this project.'));
}
}
$this->response->redirect('?controller=project&action=users&project_id='.$values['project_id']);
}
/**
* Revoke user access
*
* @access public
*/
public function revoke()
{
$this->checkCSRFParam();
$values = array(
'project_id' => $this->request->getIntegerParam('project_id'),
'user_id' => $this->request->getIntegerParam('user_id'),
);
list($valid,) = $this->project->validateUserAccess($values);
if ($valid) {
if ($this->project->revokeUser($values['project_id'], $values['user_id'])) {
$this->session->flash(t('Project updated successfully.'));
}
else {
$this->session->flashError(t('Unable to update this project.'));
}
}
$this->response->redirect('?controller=project&action=users&project_id='.$values['project_id']);
}
}

View file

@ -0,0 +1,186 @@
<?php
namespace Controller;
/**
* SubTask controller
*
* @package controller
* @author Frederic Guillot
*/
class Subtask extends Base
{
/**
* Get the current subtask
*
* @access private
* @return array
*/
private function getSubtask()
{
$subtask = $this->subTask->getById($this->request->getIntegerParam('subtask_id'));
if (! $subtask) {
$this->notfound();
}
return $subtask;
}
/**
* Creation form
*
* @access public
*/
public function create()
{
$task = $this->getTask();
$this->response->html($this->taskLayout('subtask_create', array(
'values' => array(
'task_id' => $task['id'],
),
'errors' => array(),
'users_list' => $this->project->getUsersList($task['project_id']),
'task' => $task,
'menu' => 'tasks',
'title' => t('Add a sub-task')
)));
}
/**
* Validation and creation
*
* @access public
*/
public function save()
{
$task = $this->getTask();
$values = $this->request->getValues();
list($valid, $errors) = $this->subTask->validate($values);
if ($valid) {
if ($this->subTask->create($values)) {
$this->session->flash(t('Sub-task added successfully.'));
}
else {
$this->session->flashError(t('Unable to create your sub-task.'));
}
if (isset($values['another_subtask']) && $values['another_subtask'] == 1) {
$this->response->redirect('?controller=subtask&action=create&task_id='.$task['id']);
}
$this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'#subtasks');
}
$this->response->html($this->taskLayout('subtask_create', array(
'values' => $values,
'errors' => $errors,
'users_list' => $this->project->getUsersList($task['project_id']),
'task' => $task,
'menu' => 'tasks',
'title' => t('Add a sub-task')
)));
}
/**
* Edit form
*
* @access public
*/
public function edit()
{
$task = $this->getTask();
$subtask = $this->getSubTask();
$this->response->html($this->taskLayout('subtask_edit', array(
'values' => $subtask,
'errors' => array(),
'users_list' => $this->project->getUsersList($task['project_id']),
'status_list' => $this->subTask->getStatusList(),
'subtask' => $subtask,
'task' => $task,
'menu' => 'tasks',
'title' => t('Edit a sub-task')
)));
}
/**
* Update and validate a subtask
*
* @access public
*/
public function update()
{
$task = $this->getTask();
$subtask = $this->getSubtask();
$values = $this->request->getValues();
list($valid, $errors) = $this->subTask->validate($values);
if ($valid) {
if ($this->subTask->update($values)) {
$this->session->flash(t('Sub-task updated successfully.'));
}
else {
$this->session->flashError(t('Unable to update your sub-task.'));
}
$this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'#subtasks');
}
$this->response->html($this->taskLayout('subtask_edit', array(
'values' => $values,
'errors' => $errors,
'users_list' => $this->project->getUsersList($task['project_id']),
'status_list' => $this->subTask->getStatusList(),
'subtask' => $subtask,
'task' => $task,
'menu' => 'tasks',
'title' => t('Edit a sub-task')
)));
}
/**
* Confirmation dialog before removing a subtask
*
* @access public
*/
public function confirm()
{
$task = $this->getTask();
$subtask = $this->getSubtask();
$this->response->html($this->taskLayout('subtask_remove', array(
'subtask' => $subtask,
'task' => $task,
'menu' => 'tasks',
'title' => t('Remove a sub-task')
)));
}
/**
* Remove a subtask
*
* @access public
*/
public function remove()
{
$this->checkCSRFParam();
$task = $this->getTask();
$subtask = $this->getSubtask();
if ($this->subTask->remove($subtask['id'])) {
$this->session->flash(t('Sub-task removed successfully.'));
}
else {
$this->session->flashError(t('Unable to remove this sub-task.'));
}
$this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'#subtasks');
}
}

View file

@ -0,0 +1,428 @@
<?php
namespace Controller;
use Model\Project as ProjectModel;
/**
* Task controller
*
* @package controller
* @author Frederic Guillot
*/
class Task extends Base
{
/**
* Webhook to create a task (useful for external software)
*
* @access public
*/
public function add()
{
$token = $this->request->getStringParam('token');
if ($this->config->get('webhooks_token') !== $token) {
$this->response->text('Not Authorized', 401);
}
$defaultProject = $this->project->getFirst();
$values = array(
'title' => $this->request->getStringParam('title'),
'description' => $this->request->getStringParam('description'),
'color_id' => $this->request->getStringParam('color_id', 'blue'),
'project_id' => $this->request->getIntegerParam('project_id', $defaultProject['id']),
'owner_id' => $this->request->getIntegerParam('owner_id'),
'column_id' => $this->request->getIntegerParam('column_id'),
'category_id' => $this->request->getIntegerParam('category_id'),
);
if ($values['column_id'] == 0) {
$values['column_id'] = $this->board->getFirstColumn($values['project_id']);
}
list($valid,) = $this->task->validateCreation($values);
if ($valid && $this->task->create($values)) {
$this->response->text('OK');
}
$this->response->text('FAILED');
}
/**
* Show a task
*
* @access public
*/
public function show()
{
$task = $this->getTask();
$this->response->html($this->taskLayout('task_show', array(
'files' => $this->file->getAll($task['id']),
'comments' => $this->comment->getAll($task['id']),
'subtasks' => $this->subTask->getAll($task['id']),
'task' => $task,
'columns_list' => $this->board->getColumnsList($task['project_id']),
'colors_list' => $this->task->getColors(),
'menu' => 'tasks',
'title' => $task['title'],
)));
}
/**
* Display a form to create a new task
*
* @access public
*/
public function create()
{
$project_id = $this->request->getIntegerParam('project_id');
$this->checkProjectPermissions($project_id);
$this->response->html($this->template->layout('task_new', array(
'errors' => array(),
'values' => array(
'project_id' => $project_id,
'column_id' => $this->request->getIntegerParam('column_id'),
'color_id' => $this->request->getStringParam('color_id'),
'owner_id' => $this->request->getIntegerParam('owner_id'),
'another_task' => $this->request->getIntegerParam('another_task'),
),
'projects_list' => $this->project->getListByStatus(ProjectModel::ACTIVE),
'columns_list' => $this->board->getColumnsList($project_id),
'users_list' => $this->project->getUsersList($project_id),
'colors_list' => $this->task->getColors(),
'categories_list' => $this->category->getList($project_id),
'menu' => 'tasks',
'title' => t('New task')
)));
}
/**
* Validate and save a new task
*
* @access public
*/
public function save()
{
$values = $this->request->getValues();
$values['creator_id'] = $this->acl->getUserId();
$this->checkProjectPermissions($values['project_id']);
list($valid, $errors) = $this->task->validateCreation($values);
if ($valid) {
if ($this->task->create($values)) {
$this->session->flash(t('Task created successfully.'));
if (isset($values['another_task']) && $values['another_task'] == 1) {
unset($values['title']);
unset($values['description']);
$this->response->redirect('?controller=task&action=create&'.http_build_query($values));
}
else {
$this->response->redirect('?controller=board&action=show&project_id='.$values['project_id']);
}
}
else {
$this->session->flashError(t('Unable to create your task.'));
}
}
$this->response->html($this->template->layout('task_new', array(
'errors' => $errors,
'values' => $values,
'projects_list' => $this->project->getListByStatus(ProjectModel::ACTIVE),
'columns_list' => $this->board->getColumnsList($values['project_id']),
'users_list' => $this->project->getUsersList($values['project_id']),
'colors_list' => $this->task->getColors(),
'categories_list' => $this->category->getList($values['project_id']),
'menu' => 'tasks',
'title' => t('New task')
)));
}
/**
* Display a form to edit a task
*
* @access public
*/
public function edit()
{
$task = $this->getTask();
if (! empty($task['date_due'])) {
$task['date_due'] = date(t('m/d/Y'), $task['date_due']);
}
else {
$task['date_due'] = '';
}
$task['score'] = $task['score'] ?: '';
$params = array(
'values' => $task,
'errors' => array(),
'task' => $task,
'columns_list' => $this->board->getColumnsList($task['project_id']),
'users_list' => $this->project->getUsersList($task['project_id']),
'colors_list' => $this->task->getColors(),
'categories_list' => $this->category->getList($task['project_id']),
'ajax' => $this->request->isAjax(),
'menu' => 'tasks',
'title' => t('Edit a task')
);
if ($this->request->isAjax()) {
$this->response->html($this->template->load('task_edit', $params));
}
else {
$this->response->html($this->template->layout('task_edit', $params));
}
}
/**
* Validate and update a task
*
* @access public
*/
public function update()
{
$task = $this->getTask();
$values = $this->request->getValues();
list($valid, $errors) = $this->task->validateModification($values);
if ($valid) {
if ($this->task->update($values)) {
$this->session->flash(t('Task updated successfully.'));
if ($this->request->getIntegerParam('ajax')) {
$this->response->redirect('?controller=board&action=show&project_id='.$task['project_id']);
}
else {
$this->response->redirect('?controller=task&action=show&task_id='.$values['id']);
}
}
else {
$this->session->flashError(t('Unable to update your task.'));
}
}
$this->response->html($this->template->layout('task_edit', array(
'values' => $values,
'errors' => $errors,
'task' => $task,
'columns_list' => $this->board->getColumnsList($values['project_id']),
'users_list' => $this->project->getUsersList($values['project_id']),
'colors_list' => $this->task->getColors(),
'categories_list' => $this->category->getList($values['project_id']),
'menu' => 'tasks',
'title' => t('Edit a task')
)));
}
/**
* Hide a task
*
* @access public
*/
public function close()
{
$this->checkCSRFParam();
$task = $this->getTask();
if ($this->task->close($task['id'])) {
$this->session->flash(t('Task closed successfully.'));
} else {
$this->session->flashError(t('Unable to close this task.'));
}
$this->response->redirect('?controller=task&action=show&task_id='.$task['id']);
}
/**
* Confirmation dialog before to close a task
*
* @access public
*/
public function confirmClose()
{
$task = $this->getTask();
$this->response->html($this->taskLayout('task_close', array(
'task' => $task,
'menu' => 'tasks',
'title' => t('Close a task')
)));
}
/**
* Open a task
*
* @access public
*/
public function open()
{
$this->checkCSRFParam();
$task = $this->getTask();
if ($this->task->open($task['id'])) {
$this->session->flash(t('Task opened successfully.'));
} else {
$this->session->flashError(t('Unable to open this task.'));
}
$this->response->redirect('?controller=task&action=show&task_id='.$task['id']);
}
/**
* Confirmation dialog before to open a task
*
* @access public
*/
public function confirmOpen()
{
$task = $this->getTask();
$this->response->html($this->taskLayout('task_open', array(
'task' => $task,
'menu' => 'tasks',
'title' => t('Open a task')
)));
}
/**
* Remove a task
*
* @access public
*/
public function remove()
{
$this->checkCSRFParam();
$task = $this->getTask();
if ($this->task->remove($task['id'])) {
$this->session->flash(t('Task removed successfully.'));
} else {
$this->session->flashError(t('Unable to remove this task.'));
}
$this->response->redirect('?controller=board&action=show&project_id='.$task['project_id']);
}
/**
* Confirmation dialog before removing a task
*
* @access public
*/
public function confirmRemove()
{
$task = $this->getTask();
$this->response->html($this->taskLayout('task_remove', array(
'task' => $task,
'menu' => 'tasks',
'title' => t('Remove a task')
)));
}
/**
* Duplicate a task (fill the form for a new task)
*
* @access public
*/
public function duplicate()
{
$task = $this->getTask();
if (! empty($task['date_due'])) {
$task['date_due'] = date(t('m/d/Y'), $task['date_due']);
}
else {
$task['date_due'] = '';
}
$task['score'] = $task['score'] ?: '';
$this->response->html($this->template->layout('task_new', array(
'errors' => array(),
'values' => $task,
'projects_list' => $this->project->getListByStatus(ProjectModel::ACTIVE),
'columns_list' => $this->board->getColumnsList($task['project_id']),
'users_list' => $this->project->getUsersList($task['project_id']),
'colors_list' => $this->task->getColors(),
'categories_list' => $this->category->getList($task['project_id']),
'duplicate' => true,
'menu' => 'tasks',
'title' => t('New task')
)));
}
/**
* Edit description form
*
* @access public
*/
public function editDescription()
{
$task = $this->getTask();
$params = array(
'values' => $task,
'errors' => array(),
'task' => $task,
'ajax' => $this->request->isAjax(),
'menu' => 'tasks',
'title' => t('Edit the description')
);
if ($this->request->isAjax()) {
$this->response->html($this->template->load('task_edit_description', $params));
}
else {
$this->response->html($this->taskLayout('task_edit_description', $params));
}
}
/**
* Save and validation the description
*
* @access public
*/
public function saveDescription()
{
$task = $this->getTask();
$values = $this->request->getValues();
list($valid, $errors) = $this->task->validateDescriptionCreation($values);
if ($valid) {
if ($this->task->update($values)) {
$this->session->flash(t('Task updated successfully.'));
}
else {
$this->session->flashError(t('Unable to update your task.'));
}
if ($this->request->getIntegerParam('ajax')) {
$this->response->redirect('?controller=board&action=show&project_id='.$task['project_id']);
}
else {
$this->response->redirect('?controller=task&action=show&task_id='.$task['id']);
}
}
$this->response->html($this->taskLayout('task_edit_description', array(
'values' => $values,
'errors' => $errors,
'task' => $task,
'menu' => 'tasks',
'title' => t('Edit the description')
)));
}
}

View file

@ -0,0 +1,366 @@
<?php
namespace Controller;
/**
* User controller
*
* @package controller
* @author Frederic Guillot
*/
class User extends Base
{
/**
* Logout and destroy session
*
* @access public
*/
public function logout()
{
$this->checkCSRFParam();
$this->rememberMe->destroy($this->acl->getUserId());
$this->session->close();
$this->response->redirect('?controller=user&action=login');
}
/**
* Display the form login
*
* @access public
*/
public function login()
{
if (isset($_SESSION['user'])) {
$this->response->redirect('?controller=app');
}
$this->response->html($this->template->layout('user_login', array(
'errors' => array(),
'values' => array(),
'no_layout' => true,
'title' => t('Login')
)));
}
/**
* Check credentials
*
* @access public
*/
public function check()
{
$values = $this->request->getValues();
list($valid, $errors) = $this->user->validateLogin($values);
if ($valid) {
$this->response->redirect('?controller=app');
}
$this->response->html($this->template->layout('user_login', array(
'errors' => $errors,
'values' => $values,
'no_layout' => true,
'title' => t('Login')
)));
}
/**
* List all users
*
* @access public
*/
public function index()
{
$users = $this->user->getAll();
$nb_users = count($users);
$this->response->html(
$this->template->layout('user_index', array(
'projects' => $this->project->getList(),
'users' => $users,
'nb_users' => $nb_users,
'menu' => 'users',
'title' => t('Users').' ('.$nb_users.')'
)));
}
/**
* Display a form to create a new user
*
* @access public
*/
public function create()
{
$this->response->html($this->template->layout('user_new', array(
'projects' => $this->project->getList(),
'errors' => array(),
'values' => array(),
'menu' => 'users',
'title' => t('New user')
)));
}
/**
* Validate and save a new user
*
* @access public
*/
public function save()
{
$values = $this->request->getValues();
list($valid, $errors) = $this->user->validateCreation($values);
if ($valid) {
if ($this->user->create($values)) {
$this->session->flash(t('User created successfully.'));
$this->response->redirect('?controller=user');
}
else {
$this->session->flashError(t('Unable to create your user.'));
}
}
$this->response->html($this->template->layout('user_new', array(
'projects' => $this->project->getList(),
'errors' => $errors,
'values' => $values,
'menu' => 'users',
'title' => t('New user')
)));
}
/**
* Display a form to edit a user
*
* @access public
*/
public function edit()
{
$user = $this->user->getById($this->request->getIntegerParam('user_id'));
if (! $user) $this->notfound();
if ($this->acl->isRegularUser() && $this->acl->getUserId() != $user['id']) {
$this->forbidden();
}
unset($user['password']);
$this->response->html($this->template->layout('user_edit', array(
'projects' => $this->project->filterListByAccess($this->project->getList(), $user['id']),
'errors' => array(),
'values' => $user,
'menu' => 'users',
'title' => t('Edit user')
)));
}
/**
* Validate and update a user
*
* @access public
*/
public function update()
{
$values = $this->request->getValues();
if ($this->acl->isAdminUser()) {
$values += array('is_admin' => 0);
}
else {
if ($this->acl->getUserId() != $values['id']) {
$this->forbidden();
}
if (isset($values['is_admin'])) {
unset($values['is_admin']); // Regular users can't be admin
}
}
list($valid, $errors) = $this->user->validateModification($values);
if ($valid) {
if ($this->user->update($values)) {
$this->session->flash(t('User updated successfully.'));
$this->response->redirect('?controller=user');
}
else {
$this->session->flashError(t('Unable to update your user.'));
}
}
$this->response->html($this->template->layout('user_edit', array(
'projects' => $this->project->filterListByAccess($this->project->getList(), $values['id']),
'errors' => $errors,
'values' => $values,
'menu' => 'users',
'title' => t('Edit user')
)));
}
/**
* Confirmation dialog before to remove a user
*
* @access public
*/
public function confirm()
{
$user = $this->user->getById($this->request->getIntegerParam('user_id'));
if (! $user) $this->notfound();
$this->response->html($this->template->layout('user_remove', array(
'user' => $user,
'menu' => 'users',
'title' => t('Remove user')
)));
}
/**
* Remove a user
*
* @access public
*/
public function remove()
{
$this->checkCSRFParam();
$user_id = $this->request->getIntegerParam('user_id');
if ($user_id && $this->user->remove($user_id)) {
$this->session->flash(t('User removed successfully.'));
} else {
$this->session->flashError(t('Unable to remove this user.'));
}
$this->response->redirect('?controller=user');
}
/**
* Google authentication
*
* @access public
*/
public function google()
{
$code = $this->request->getStringParam('code');
if ($code) {
$profile = $this->google->getGoogleProfile($code);
if (is_array($profile)) {
// If the user is already logged, link the account otherwise authenticate
if ($this->acl->isLogged()) {
if ($this->google->updateUser($this->acl->getUserId(), $profile)) {
$this->session->flash(t('Your Google Account is linked to your profile successfully.'));
}
else {
$this->session->flashError(t('Unable to link your Google Account.'));
}
$this->response->redirect('?controller=user');
}
else if ($this->google->authenticate($profile['id'])) {
$this->response->redirect('?controller=app');
}
else {
$this->response->html($this->template->layout('user_login', array(
'errors' => array('login' => t('Google authentication failed')),
'values' => array(),
'no_layout' => true,
'title' => t('Login')
)));
}
}
}
$this->response->redirect($this->google->getAuthorizationUrl());
}
/**
* Unlink a Google account
*
* @access public
*/
public function unlinkGoogle()
{
$this->checkCSRFParam();
if ($this->google->unlink($this->acl->getUserId())) {
$this->session->flash(t('Your Google Account is not linked anymore to your profile.'));
}
else {
$this->session->flashError(t('Unable to unlink your Google Account.'));
}
$this->response->redirect('?controller=user');
}
/**
* GitHub authentication
*
* @access public
*/
public function gitHub()
{
$code = $this->request->getStringParam('code');
if ($code) {
$profile = $this->gitHub->getGitHubProfile($code);
if (is_array($profile)) {
// If the user is already logged, link the account otherwise authenticate
if ($this->acl->isLogged()) {
if ($this->gitHub->updateUser($this->acl->getUserId(), $profile)) {
$this->session->flash(t('Your GitHub account was successfully linked to your profile.'));
}
else {
$this->session->flashError(t('Unable to link your GitHub Account.'));
}
$this->response->redirect('?controller=user');
}
else if ($this->gitHub->authenticate($profile['id'])) {
$this->response->redirect('?controller=app');
}
else {
$this->response->html($this->template->layout('user_login', array(
'errors' => array('login' => t('GitHub authentication failed')),
'values' => array(),
'no_layout' => true,
'title' => t('Login')
)));
}
}
}
$this->response->redirect($this->gitHub->getAuthorizationUrl());
}
/**
* Unlink a GitHub account
*
* @access public
*/
public function unlinkGitHub()
{
$this->checkCSRFParam();
$this->gitHub->revokeGitHubAccess();
if ($this->gitHub->unlink($this->acl->getUserId())) {
$this->session->flash(t('Your GitHub account is no longer linked to your profile.'));
}
else {
$this->session->flashError(t('Unable to unlink your GitHub Account.'));
}
$this->response->redirect('?controller=user');
}
}

161
sources/app/Core/Event.php Normal file
View file

@ -0,0 +1,161 @@
<?php
namespace Core;
/**
* Event dispatcher class
*
* @package core
* @author Frederic Guillot
*/
class Event
{
/**
* Contains all listeners
*
* @access private
* @var array
*/
private $listeners = array();
/**
* The last listener executed
*
* @access private
* @var string
*/
private $lastListener = '';
/**
* The last triggered event
*
* @access private
* @var string
*/
private $lastEvent = '';
/**
* Triggered events list
*
* @access private
* @var array
*/
private $events = array();
/**
* Attach a listener object to an event
*
* @access public
* @param string $eventName Event name
* @param Listener $listener Object that implements the Listener interface
*/
public function attach($eventName, Listener $listener)
{
if (! isset($this->listeners[$eventName])) {
$this->listeners[$eventName] = array();
}
$this->listeners[$eventName][] = $listener;
}
/**
* Trigger an event
*
* @access public
* @param string $eventName Event name
* @param array $data Event data
*/
public function trigger($eventName, array $data)
{
if (! $this->isEventTriggered($eventName)) {
$this->lastEvent = $eventName;
$this->events[] = $eventName;
if (isset($this->listeners[$eventName])) {
foreach ($this->listeners[$eventName] as $listener) {
if ($listener->execute($data)) {
$this->lastListener = get_class($listener);
}
}
}
}
}
/**
* Get the last listener executed
*
* @access public
* @return string Event name
*/
public function getLastListenerExecuted()
{
return $this->lastListener;
}
/**
* Get the last fired event
*
* @access public
* @return string Event name
*/
public function getLastTriggeredEvent()
{
return $this->lastEvent;
}
/**
* Get a list of triggered events
*
* @access public
* @return array
*/
public function getTriggeredEvents()
{
return $this->events;
}
/**
* Check if an event have been triggered
*
* @access public
* @param string $eventName Event name
* @return bool
*/
public function isEventTriggered($eventName)
{
return in_array($eventName, $this->events);
}
/**
* Flush the list of triggered events
*
* @access public
*/
public function clearTriggeredEvents()
{
$this->events = array();
$this->lastEvent = '';
}
/**
* Check if a listener bind to an event
*
* @access public
* @param string $eventName Event name
* @param mixed $instance Instance name or object itself
* @return bool Yes or no
*/
public function hasListener($eventName, $instance)
{
if (isset($this->listeners[$eventName])) {
foreach ($this->listeners[$eventName] as $listener) {
if ($listener instanceof $instance) {
return true;
}
}
}
return false;
}
}

View file

@ -0,0 +1,21 @@
<?php
namespace Core;
/**
* Event listener interface
*
* @package core
* @author Frederic Guillot
*/
interface Listener {
/**
* Execute the listener
*
* @access public
* @param array $data Event data
* @return boolean
*/
public function execute(array $data);
}

View file

@ -0,0 +1,37 @@
<?php
namespace Core;
/**
* Loader class
*
* @package core
* @author Frederic Guillot
*/
class Loader
{
/**
* Load the missing class
*
* @access public
* @param string $class Class name
*/
public function load($class)
{
$filename = __DIR__.DIRECTORY_SEPARATOR.'..'.DIRECTORY_SEPARATOR.str_replace('\\', DIRECTORY_SEPARATOR, $class).'.php';
if (file_exists($filename)) {
require $filename;
}
}
/**
* Register the autoloader
*
* @access public
*/
public function execute()
{
spl_autoload_register(array($this, 'load'));
}
}

View file

@ -0,0 +1,82 @@
<?php
namespace Core;
use RuntimeException;
/**
* The registry class is a dependency injection container
*
* @property mixed db
* @property mixed event
* @package core
* @author Frederic Guillot
*/
class Registry
{
/**
* Contains all dependencies
*
* @access private
* @var array
*/
private $container = array();
/**
* Contains all instances
*
* @access private
* @var array
*/
private $instances = array();
/**
* Set a dependency
*
* @access public
* @param string $name Unique identifier for the service/parameter
* @param mixed $value The value of the parameter or a closure to define an object
*/
public function __set($name, $value)
{
$this->container[$name] = $value;
}
/**
* Get a dependency
*
* @access public
* @param string $name Unique identifier for the service/parameter
* @return mixed The value of the parameter or an object
* @throws RuntimeException If the identifier is not found
*/
public function __get($name)
{
if (isset($this->container[$name])) {
if (is_callable($this->container[$name])) {
return $this->container[$name]();
}
else {
return $this->container[$name];
}
}
throw new \RuntimeException('Identifier not found in the registry: '.$name);
}
/**
* Return a shared instance of a dependency
*
* @access public
* @param string $name Unique identifier for the service/parameter
* @return mixed Same object instance of the dependency
*/
public function shared($name)
{
if (! isset($this->instances[$name])) {
$this->instances[$name] = $this->$name;
}
return $this->instances[$name];
}
}

View file

@ -0,0 +1,139 @@
<?php
namespace Core;
/**
* Request class
*
* @package core
* @author Frederic Guillot
*/
class Request
{
/**
* Get URL string parameter
*
* @access public
* @param string $name Parameter name
* @param string $default_value Default value
* @return string
*/
public function getStringParam($name, $default_value = '')
{
return isset($_GET[$name]) ? $_GET[$name] : $default_value;
}
/**
* Get URL integer parameter
*
* @access public
* @param string $name Parameter name
* @param integer $default_value Default value
* @return integer
*/
public function getIntegerParam($name, $default_value = 0)
{
return isset($_GET[$name]) && ctype_digit($_GET[$name]) ? (int) $_GET[$name] : $default_value;
}
/**
* Get a form value
*
* @access public
* @param string $name Form field name
* @return string|null
*/
public function getValue($name)
{
$values = $this->getValues();
return isset($values[$name]) ? $values[$name] : null;
}
/**
* Get form values or unserialized json request
*
* @access public
* @return array
*/
public function getValues()
{
if (! empty($_POST)) {
if (Security::validateCSRFFormToken($_POST)) {
return $_POST;
}
return array();
}
$result = json_decode($this->getBody(), true);
if ($result) {
return $result;
}
return array();
}
/**
* Get the raw body of the HTTP request
*
* @access public
* @return string
*/
public function getBody()
{
return file_get_contents('php://input');
}
/**
* Get the content of an uploaded file
*
* @access public
* @param string $name Form file name
* @return string
*/
public function getFileContent($name)
{
if (isset($_FILES[$name])) {
return file_get_contents($_FILES[$name]['tmp_name']);
}
return '';
}
/**
* Return true if the HTTP request is sent with the POST method
*
* @access public
* @return bool
*/
public function isPost()
{
return isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'POST';
}
/**
* Return true if the HTTP request is an Ajax request
*
* @access public
* @return bool
*/
public function isAjax()
{
return $this->getHeader('X-Requested-With') === 'XMLHttpRequest';
}
/**
* Return a HTTP header value
*
* @access public
* @param string $name Header name
* @return string
*/
public function getHeader($name)
{
$name = 'HTTP_'.str_replace('-', '_', strtoupper($name));
return isset($_SERVER[$name]) ? $_SERVER[$name] : '';
}
}

View file

@ -0,0 +1,254 @@
<?php
namespace Core;
/**
* Response class
*
* @package core
* @author Frederic Guillot
*/
class Response
{
/**
* Send no cache headers
*
* @access public
*/
public function nocache()
{
header('Pragma: no-cache');
header('Expires: Sat, 26 Jul 1997 05:00:00 GMT');
// Use no-store due to a Chrome bug: https://code.google.com/p/chromium/issues/detail?id=28035
header('Cache-Control: no-store, must-revalidate');
}
/**
* Send a custom Content-Type header
*
* @access public
* @param string $mimetype Mime-type
*/
public function contentType($mimetype)
{
header('Content-Type: '.$mimetype);
}
/**
* Force the browser to download an attachment
*
* @access public
* @param string $filename File name
*/
public function forceDownload($filename)
{
header('Content-Disposition: attachment; filename="'.$filename.'"');
}
/**
* Send a custom HTTP status code
*
* @access public
* @param integer $status_code HTTP status code
*/
public function status($status_code)
{
header('Status: '.$status_code);
header($_SERVER['SERVER_PROTOCOL'].' '.$status_code);
}
/**
* Redirect to another URL
*
* @access public
* @param string $url Redirection URL
*/
public function redirect($url)
{
header('Location: '.$url);
exit;
}
/**
* Send a Json response
*
* @access public
* @param array $data Data to serialize in json
* @param integer $status_code HTTP status code
*/
public function json(array $data, $status_code = 200)
{
$this->status($status_code);
$this->nocache();
header('Content-Type: application/json');
echo json_encode($data);
exit;
}
/**
* Send a text response
*
* @access public
* @param string $data Raw data
* @param integer $status_code HTTP status code
*/
public function text($data, $status_code = 200)
{
$this->status($status_code);
$this->nocache();
header('Content-Type: text/plain; charset=utf-8');
echo $data;
exit;
}
/**
* Send a HTML response
*
* @access public
* @param string $data Raw data
* @param integer $status_code HTTP status code
*/
public function html($data, $status_code = 200)
{
$this->status($status_code);
$this->nocache();
header('Content-Type: text/html; charset=utf-8');
echo $data;
exit;
}
/**
* Send a XML response
*
* @access public
* @param string $data Raw data
* @param integer $status_code HTTP status code
*/
public function xml($data, $status_code = 200)
{
$this->status($status_code);
$this->nocache();
header('Content-Type: text/xml; charset=utf-8');
echo $data;
exit;
}
/**
* Send a javascript response
*
* @access public
* @param string $data Raw data
* @param integer $status_code HTTP status code
*/
public function js($data, $status_code = 200)
{
$this->status($status_code);
header('Content-Type: text/javascript; charset=utf-8');
echo $data;
exit;
}
/**
* Send a binary response
*
* @access public
* @param string $data Raw data
* @param integer $status_code HTTP status code
*/
public function binary($data, $status_code = 200)
{
$this->status($status_code);
$this->nocache();
header('Content-Transfer-Encoding: binary');
header('Content-Type: application/octet-stream');
echo $data;
exit;
}
/**
* Send the security header: Content-Security-Policy
*
* @access public
* @param array $policies CSP rules
*/
public function csp(array $policies = array())
{
$policies['default-src'] = "'self'";
$values = '';
foreach ($policies as $policy => $hosts) {
if (is_array($hosts)) {
$acl = '';
foreach ($hosts as &$host) {
if ($host === '*' || $host === 'self' || strpos($host, 'http') === 0) {
$acl .= $host.' ';
}
}
}
else {
$acl = $hosts;
}
$values .= $policy.' '.trim($acl).'; ';
}
header('Content-Security-Policy: '.$values);
}
/**
* Send the security header: X-Content-Type-Options
*
* @access public
*/
public function nosniff()
{
header('X-Content-Type-Options: nosniff');
}
/**
* Send the security header: X-XSS-Protection
*
* @access public
*/
public function xss()
{
header('X-XSS-Protection: 1; mode=block');
}
/**
* Send the security header: Strict-Transport-Security (only if we use HTTPS)
*
* @access public
*/
public function hsts()
{
if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off') {
header('Strict-Transport-Security: max-age=31536000');
}
}
/**
* Send the security header: X-Frame-Options (deny by default)
*
* @access public
* @param string $mode Frame option mode
* @param array $urls Allowed urls for the given mode
*/
public function xframe($mode = 'DENY', array $urls = array())
{
header('X-Frame-Options: '.$mode.' '.implode(' ', $urls));
}
}

113
sources/app/Core/Router.php Normal file
View file

@ -0,0 +1,113 @@
<?php
namespace Core;
/**
* Router class
*
* @package core
* @author Frederic Guillot
*/
class Router
{
/**
* Controller name
*
* @access private
* @var string
*/
private $controller = '';
/**
* Action name
*
* @access private
* @var string
*/
private $action = '';
/**
* Registry instance
*
* @access private
* @var \Core\Registry
*/
private $registry;
/**
* Constructor
*
* @access public
* @param Registry $registry Registry instance
* @param string $controller Controller name
* @param string $action Action name
*/
public function __construct(Registry $registry, $controller = '', $action = '')
{
$this->registry = $registry;
$this->controller = empty($_GET['controller']) ? $controller : $_GET['controller'];
$this->action = empty($_GET['action']) ? $action : $_GET['action'];
}
/**
* Check controller and action parameter
*
* @access public
* @param string $value Controller or action name
* @param string $default_value Default value if validation fail
* @return string
*/
public function sanitize($value, $default_value)
{
return ! ctype_alpha($value) || empty($value) ? $default_value : strtolower($value);
}
/**
* Load a controller and execute the action
*
* @access public
* @param string $filename Controller filename
* @param string $class Class name
* @param string $method Method name
* @return bool
*/
public function load($filename, $class, $method)
{
if (file_exists($filename)) {
require $filename;
if (! method_exists($class, $method)) {
return false;
}
$instance = new $class($this->registry);
$instance->request = new Request;
$instance->response = new Response;
$instance->session = new Session;
$instance->template = new Template;
$instance->beforeAction($this->controller, $this->action);
$instance->$method();
return true;
}
return false;
}
/**
* Find a route
*
* @access public
*/
public function execute()
{
$this->controller = $this->sanitize($this->controller, 'app');
$this->action = $this->sanitize($this->action, 'index');
$filename = __DIR__.'/../Controller/'.ucfirst($this->controller).'.php';
if (! $this->load($filename, '\Controller\\'.$this->controller, $this->action)) {
die('Page not found!');
}
}
}

View file

@ -0,0 +1,87 @@
<?php
namespace Core;
/**
* Security class
*
* @package core
* @author Frederic Guillot
*/
class Security
{
/**
* Generate a random token with different methods: openssl or /dev/urandom or fallback to uniqid()
*
* @static
* @access public
* @return string Random token
*/
public static function generateToken()
{
if (function_exists('openssl_random_pseudo_bytes')) {
return bin2hex(\openssl_random_pseudo_bytes(30));
}
else if (ini_get('open_basedir') === '' && strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') {
return hash('sha256', file_get_contents('/dev/urandom', false, null, 0, 30));
}
return hash('sha256', uniqid(mt_rand(), true));
}
/**
* Generate and store a CSRF token in the current session
*
* @static
* @access public
* @return string Random token
*/
public static function getCSRFToken()
{
$nonce = self::generateToken();
if (empty($_SESSION['csrf_tokens'])) {
$_SESSION['csrf_tokens'] = array();
}
$_SESSION['csrf_tokens'][$nonce] = true;
return $nonce;
}
/**
* Check if the token exists for the current session (a token can be used only one time)
*
* @static
* @access public
* @param string $token CSRF token
* @return bool
*/
public static function validateCSRFToken($token)
{
if (isset($_SESSION['csrf_tokens'][$token])) {
unset($_SESSION['csrf_tokens'][$token]);
return true;
}
return false;
}
/**
* Check if the token used in a form is correct and then remove the value
*
* @static
* @access public
* @param array $values Form values
* @return bool
*/
public static function validateCSRFFormToken(array &$values)
{
if (! empty($values['csrf_token']) && self::validateCSRFToken($values['csrf_token'])) {
unset($values['csrf_token']);
return true;
}
return false;
}
}

View file

@ -0,0 +1,118 @@
<?php
namespace Core;
/**
* Session class
*
* @package core
* @author Frederic Guillot
*/
class Session
{
/**
* Sesion lifetime
*
* @var integer
*/
const SESSION_LIFETIME = 7200; // 2 hours
/**
* Open a session
*
* @access public
* @param string $base_path Cookie path
* @param string $save_path Custom session save path
*/
public function open($base_path = '/', $save_path = '')
{
if ($save_path !== '') {
session_save_path($save_path);
}
// HttpOnly and secure flags for session cookie
session_set_cookie_params(
self::SESSION_LIFETIME,
$base_path ?: '/',
null,
! empty($_SERVER['HTTPS']),
true
);
// Avoid session id in the URL
ini_set('session.use_only_cookies', '1');
// Ensure session ID integrity
ini_set('session.entropy_file', '/dev/urandom');
ini_set('session.entropy_length', '32');
ini_set('session.hash_bits_per_character', 6);
// If session was autostarted with session.auto_start = 1 in php.ini destroy it, otherwise we cannot login
if (isset($_SESSION))
{
session_destroy();
}
// Custom session name
session_name('__S');
session_start();
// Regenerate the session id to avoid session fixation issue
if (empty($_SESSION['__validated'])) {
session_regenerate_id(true);
$_SESSION['__validated'] = 1;
}
}
/**
* Destroy the session
*
* @access public
*/
public function close()
{
// Flush all sessions variables
$_SESSION = array();
// Destroy the session cookie
if (ini_get('session.use_cookies')) {
$params = session_get_cookie_params();
setcookie(
session_name(),
'',
time() - 42000,
$params['path'],
$params['domain'],
$params['secure'],
$params['httponly']
);
}
// Destroy session data
session_destroy();
}
/**
* Register a flash message (success notification)
*
* @access public
* @param string $message Message
*/
public function flash($message)
{
$_SESSION['flash_message'] = $message;
}
/**
* Register a flash error message (error notification)
*
* @access public
* @param string $message Message
*/
public function flashError($message)
{
$_SESSION['flash_error_message'] = $message;
}
}

View file

@ -0,0 +1,72 @@
<?php
namespace Core;
/**
* Template class
*
* @package core
* @author Frederic Guillot
*/
class Template
{
/**
* Template path
*
* @var string
*/
const PATH = 'app/Templates/';
/**
* Load a template
*
* Example:
*
* $template->load('template_name', ['bla' => 'value']);
*
* @access public
* @return string
*/
public function load()
{
if (func_num_args() < 1 || func_num_args() > 2) {
die('Invalid template arguments');
}
if (! file_exists(self::PATH.func_get_arg(0).'.php')) {
die('Unable to load the template: "'.func_get_arg(0).'"');
}
if (func_num_args() === 2) {
if (! is_array(func_get_arg(1))) {
die('Template variables must be an array');
}
extract(func_get_arg(1));
}
ob_start();
include self::PATH.func_get_arg(0).'.php';
return ob_get_clean();
}
/**
* Render a page layout
*
* @access public
* @param string $template_name Template name
* @param array $template_args Key/value map
* @param string $layout_name Layout name
* @return string
*/
public function layout($template_name, array $template_args = array(), $layout_name = 'layout')
{
return $this->load(
$layout_name,
$template_args + array('content_for_layout' => $this->load($template_name, $template_args))
);
}
}

View file

@ -0,0 +1,164 @@
<?php
namespace Core;
/**
* Translator class
*
* @package core
* @author Frederic Guillot
*/
class Translator
{
/**
* Locales path
*
* @var string
*/
const PATH = 'app/Locales/';
/**
* Locales
*
* @static
* @access private
* @var array
*/
private static $locales = array();
/**
* Get a translation
*
* $translator->translate('I have %d kids', 5);
*
* @access public
* @param $identifier
* @return string
*/
public function translate($identifier)
{
$args = func_get_args();
array_shift($args);
array_unshift($args, $this->get($identifier, $identifier));
foreach ($args as &$arg) {
$arg = htmlspecialchars($arg, ENT_QUOTES, 'UTF-8', false);
}
return call_user_func_array(
'sprintf',
$args
);
}
/**
* Get a formatted number
*
* $translator->number(1234.56);
*
* @access public
* @param float $number Number to format
* @return string
*/
public function number($number)
{
return number_format(
$number,
$this->get('number.decimals', 2),
$this->get('number.decimals_separator', '.'),
$this->get('number.thousands_separator', ',')
);
}
/**
* Get a formatted currency number
*
* $translator->currency(1234.56);
*
* @access public
* @param float $amount Number to format
* @return string
*/
public function currency($amount)
{
$position = $this->get('currency.position', 'before');
$symbol = $this->get('currency.symbol', '$');
$str = '';
if ($position === 'before') {
$str .= $symbol;
}
$str .= $this->number($amount);
if ($position === 'after') {
$str .= ' '.$symbol;
}
return $str;
}
/**
* Get a formatted datetime
*
* $translator->datetime('%Y-%m-%d', time());
*
* @access public
* @param string $format Format defined by the strftime function
* @param integer $timestamp Unix timestamp
* @return string
*/
public function datetime($format, $timestamp)
{
if (! $timestamp) {
return '';
}
$format = $this->get($format, $format);
if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
$format = str_replace('%e', '%d', $format);
$format = str_replace('%G', '%Y', $format);
$format = str_replace('%k', '%H', $format);
}
return strftime($format, (int) $timestamp);
}
/**
* Get an identifier from the translations or return the default
*
* @access public
* @param string $identifier Locale identifier
* @param string $default Default value
* @return string
*/
public function get($identifier, $default = '')
{
if (isset(self::$locales[$identifier])) {
return self::$locales[$identifier];
}
else {
return $default;
}
}
/**
* Load translations
*
* @static
* @access public
* @param string $language Locale code: fr_FR
*/
public static function load($language)
{
setlocale(LC_TIME, $language.'.UTF-8', $language);
$filename = self::PATH.$language.DIRECTORY_SEPARATOR.'translations.php';
if (file_exists($filename)) {
self::$locales = require $filename;
}
}
}

View file

@ -0,0 +1,51 @@
<?php
namespace Event;
use Core\Listener;
use Model\Project;
/**
* Task modification listener
*
* @package events
* @author Frederic Guillot
*/
class TaskModification implements Listener
{
/**
* Project model
*
* @accesss private
* @var \Model\Project
*/
private $project;
/**
* Constructor
*
* @access public
* @param \Model\Project $project Project model instance
*/
public function __construct(Project $project)
{
$this->project = $project;
}
/**
* Execute the action
*
* @access public
* @param array $data Event data dictionary
* @return bool True if the action was executed or false when not executed
*/
public function execute(array $data)
{
if (isset($data['project_id'])) {
$this->project->updateModificationDate($data['project_id']);
return true;
}
return false;
}
}

View file

@ -0,0 +1,390 @@
<?php
return array(
'English' => 'Englisch',
'German' => 'Deutsch',
'French' => 'Französisch',
'Polish' => 'Polnisch',
'Portuguese (Brazilian)' => 'Portugiesisch (Brasilien)',
'Spanish' => 'Spanisch',
'Chinese (Simplified)' => 'Chinesisch (vereinfacht)',
'None' => 'Kein',
'edit' => 'Bearbeiten',
'Edit' => 'Bearbeiten',
'remove' => 'Entfernen',
'Remove' => 'Entfernen',
'Update' => 'Aktualisieren',
'Yes' => 'Ja',
'No' => 'Nein',
'cancel' => 'Abbrechen',
'or' => 'oder',
'Yellow' => 'Gelb',
'Blue' => 'Blau',
'Green' => 'Grün',
'Purple' => 'Violett',
'Red' => 'Rot',
'Orange' => 'Orange',
'Grey' => 'Grau',
'Save' => 'Speichern',
'Login' => 'Anmelden',
'Official website:' => 'Offizielle Webseite:',
'Unassigned' => 'Nicht zugeordnet',
'View this task' => 'Aufgabe ansehen',
'Remove user' => 'Benutzer löschen',
'Do you really want to remove this user: "%s"?' => 'Soll dieser Benutzer wirklich gelöscht werden: «%s»?',
'New user' => 'Neuer Benutzer',
'All users' => 'Alle Benutzer',
'Username' => 'Benutzername',
'Password' => 'Passwort',
'Default Project' => 'Standardprojekt',
'Administrator' => 'Administrator',
'Sign in' => 'Anmelden',
'Users' => 'Benutzer',
'No user' => 'Kein Benutzer',
'Forbidden' => 'Verboten',
'Access Forbidden' => 'Zugang verboten',
'Only administrators can access to this page.' => 'Nur Administratoren haben Zugang zu dieser Seite.',
'Edit user' => 'Benutzer bearbeiten',
'Logout' => 'Abmelden',
'Bad username or password' => 'Falscher Benutzername oder Passwort',
'users' => 'Benutzer',
'projects' => 'Projekte',
'Edit project' => 'Projekt bearbeiten',
'Name' => 'Name',
'Activated' => 'Aktiviert',
'Projects' => 'Projekte',
'No project' => 'Keine Projekte',
'Project' => 'Projekt',
'Status' => 'Status',
'Tasks' => 'Aufgabe',
'Board' => 'Pinwand',
'Actions' => 'Aktionen',
'Inactive' => 'Inaktiv',
'Active' => 'Aktiv',
'Column %d' => 'Spalte %d',
'Add this column' => 'Diese Spalte hinzufügen',
'%d tasks on the board' => '%d Aufgaben auf dieser Pinwand',
'%d tasks in total' => '%d Aufgaben gesamt',
'Unable to update this board.' => 'Ändern dieser Pinwand nicht möglich.',
'Edit board' => 'Pinwand bearbeiten',
'Disable' => 'Deaktivieren',
'Enable' => 'Aktivieren',
'New project' => 'Neues Projekt',
'Do you really want to remove this project: "%s"?' => 'Soll dieses Projekt wirklich gelöscht werden: «%s»?',
'Remove project' => 'Projekt löschen',
'Boards' => 'Pinwände',
'Edit the board for "%s"' => 'Pinwand für «%s» bearbeiten',
'All projects' => 'Alle Projekte',
'Change columns' => 'Spalten ändern',
'Add a new column' => 'Neue Spalte hinzufügen',
'Title' => 'Titel',
'Add Column' => 'Neue Spalte',
'Project "%s"' => 'Projekt «%s»',
'Nobody assigned' => 'Nicht zugeordnet',
'Assigned to %s' => 'Zuständiger: %s',
'Remove a column' => 'Spalte löschen',
'Remove a column from a board' => 'Spalte einer Pinwand löschen',
'Unable to remove this column.' => 'Löschen dieser Spalte nicht möglich.',
'Do you really want to remove this column: "%s"?' => 'Soll diese Spalte wirklich gelöscht werden: «%s»?',
'This action will REMOVE ALL TASKS associated to this column!' => 'ALLE AUFGABEN dieser Spalte werden GELÖSCHT!',
'Settings' => 'Einstellungen',
'Application settings' => 'Anwendungskonfiguration',
'Language' => 'Sprache',
'Webhooks token:' => 'Webhooks Token:',
'API token:' => 'API Token:',
'More information' => 'Mehr Informationen',
'Database size:' => 'Datenbankgröße:',
'Download the database' => 'Download der Datenbank',
'Optimize the database' => 'Optimieren der Datenbank',
'(VACUUM command)' => '(VACUUM Kommando)',
'(Gzip compressed Sqlite file)' => '(Gzip komprimierte Sqlite Datei)',
'User settings' => 'Benutzereinstellungen',
'My default project:' => 'Standardprojekt:',
'Close a task' => 'Aufgabe abschließen',
'Do you really want to close this task: "%s"?' => 'Soll diese Aufgabe wirklich abgeschlossen werden: «%s»?',
'Edit a task' => 'Aufgabe bearbeiten',
'Column' => 'Spalte',
'Color' => 'Farbe',
'Assignee' => 'Zuständiger',
'Create another task' => 'Weitere Aufgabe erstellen',
'New task' => 'Neue Aufgabe',
'Open a task' => 'Öffne eine Aufgabe',
'Do you really want to open this task: "%s"?' => 'Soll diese Aufgabe wirklich wieder geöffnet werden: «%s»?',
'Back to the board' => 'Zurück zur Pinwand',
'Created on %B %e, %G at %k:%M %p' => 'Erstellt am %d.%m.%Y um %H:%M',
'There is nobody assigned' => 'Die Aufgabe wurde niemand zugewiesen',
'Column on the board:' => 'Spalte:',
'Status is open' => 'Status ist geöffnet',
'Status is closed' => 'Status ist abgeschlossen',
'Close this task' => 'Aufgabe abschließen',
'Open this task' => 'Aufgabe wieder öffnen',
'There is no description.' => 'Keine Beschreibung vorhanden.',
'Add a new task' => 'Neue Aufgabe hinzufügen',
'The username is required' => 'Der Benutzername ist obligatorisch',
'The maximum length is %d characters' => 'Die maximale Länge sind %d Zeichen',
'The minimum length is %d characters' => 'Die minimale Länge sind %d Zeichen',
'The password is required' => 'Das Passwort ist obligatorisch',
'This value must be an integer' => 'Dieser Wert muss eine Ganzzahl sein',
'The username must be unique' => 'Der Benutzername muss eindeutig sein',
'The username must be alphanumeric' => 'Der Benutzername muss alphanumerisch sein',
'The user id is required' => 'Die Benutzer ID ist obligatorisch',
'Passwords don\'t match' => 'Passwörter nicht gleich',
'The confirmation is required' => 'Die Bestätigung ist erforderlich',
'The column is required' => 'Die Spalte ist anzugeben',
'The project is required' => 'Das Projekt ist anzugeben',
'The color is required' => 'Die Farbe ist anzugeben',
'The id is required' => 'Die ID ist anzugeben',
'The project id is required' => 'Die Projekt ID ist anzugeben',
'The project name is required' => 'Der Projektname ist anzugeben',
'This project must be unique' => 'Der Projektname muss eindeutig sein',
'The title is required' => 'Der Titel ist anzugeben',
'The language is required' => 'Die Sprache ist erforderlich',
'There is no active project, the first step is to create a new project.' => 'Es gibt kein aktives Projekt. Der erste Schritt ist ein Projekt zu erstellen.',
'Settings saved successfully.' => 'Einstellungen erfolgreich gespeichert.',
'Unable to save your settings.' => 'Speichern der Einstellungen nicht möglich.',
'Database optimization done.' => 'Optimieren der Datenbank abgeschlossen.',
'Your project have been created successfully.' => 'Das Projekt wurde erfolgreich erstellt.',
'Unable to create your project.' => 'Erstellen des Projekts nicht möglich.',
'Project updated successfully.' => 'Projekt erfolgreich geändert.',
'Unable to update this project.' => 'Änderung des Projekts nicht möglich.',
'Unable to remove this project.' => 'Löschen des Projekts nicht möglich.',
'Project removed successfully.' => 'Projekt erfolgreich gelöscht.',
'Project activated successfully.' => 'Projekt erfolgreich aktiviert.',
'Unable to activate this project.' => 'Aktivieren des Projekts nicht möglich.',
'Project disabled successfully.' => 'Projekt erfolgreich deaktiviert.',
'Unable to disable this project.' => 'Deaktivieren des Projekts nicht möglich.',
'Unable to open this task.' => 'Wieder eröffnen der Aufgabe nicht möglich.',
'Task opened successfully.' => 'Aufgabe erfolgreich wieder eröffnet.',
'Unable to close this task.' => 'Abschließen der Aufgabe nicht möglich.',
'Task closed successfully.' => 'Aufgabe erfolgreich geschlossen.',
'Unable to update your task.' => 'Aktualisieren der Aufgabe nicht möglich.',
'Task updated successfully.' => 'Aufgabe erfolgreich aktualisiert.',
'Unable to create your task.' => 'Erstellen der Aufgabe nicht möglich.',
'Task created successfully.' => 'Aufgabe erfolgreich erstellt.',
'User created successfully.' => 'Benutzer erfolgreich erstellt.',
'Unable to create your user.' => 'Erstellen des Benutzers nicht möglich.',
'User updated successfully.' => 'Benutzer erfolgreich geändert.',
'Unable to update your user.' => 'Änderung des Benutzers nicht möglich.',
'User removed successfully.' => 'Benutzer erfolgreich gelöscht.',
'Unable to remove this user.' => 'Löschen des Benutzers nicht möglich.',
'Board updated successfully.' => 'Pinwand erfolgreich geändert.',
'Ready' => 'Bereit',
'Backlog' => 'Ideen',
'Work in progress' => 'In Arbeit',
'Done' => 'Erledigt',
'Application version:' => 'Version:',
'Completed on %B %e, %G at %k:%M %p' => 'Abgeschlossen am %d.%m.%Y um %H:%M',
'%B %e, %G at %k:%M %p' => '%d.%m.%Y um %H:%M',
'Date created' => 'Erstellt am',
'Date completed' => 'Abgeschlossen am',
'Id' => 'ID',
'No task' => 'Keine Aufgabe',
'Completed tasks' => 'Abgeschlossene Aufgaben',
'List of projects' => 'Liste der Projekte',
'Completed tasks for "%s"' => 'Abgeschlossene Aufgaben für «%s»',
'%d closed tasks' => '%d abgeschlossene Aufgaben',
'no task for this project' => 'Keine Aufgaben in diesem Projekt',
'Public link' => 'Öffentlicher Link',
'There is no column in your project!' => 'Es gibt keine Spalte in deinem Projekt!',
'Change assignee' => 'Zuständigkeit ändern',
'Change assignee for the task "%s"' => 'Zuständigkeit für diese Aufgabe ändern: «%s»',
'Timezone' => 'Zeitzone',
'Sorry, I didn\'t found this information in my database!' => 'Diese Information wurde in der Datenbank nicht gefunden!',
'Page not found' => 'Seite nicht gefunden',
'Story Points' => 'Aufwand (Story Points)',
'limit' => 'Limit',
'Task limit' => 'Maximale Anzahl von Aufgaben',
'This value must be greater than %d' => 'Dieser Wert muss größer sein als %d',
'Edit project access list' => 'Zugriffsberechtigungen des Projektes bearbeiten',
'Edit users access' => 'Benutzerzugriff',
'Allow this user' => 'Diesen Benutzer authorisieren',
'Project access list for "%s"' => 'Zugriffsliste für Projekt «%s»',
'Only those users have access to this project:' => 'Nur diese Benutzer haben Zugang zum Projekt:',
'Don\'t forget that administrators have access to everything.' => 'Nicht vergessen: Administratoren haben überall Zugang.',
'revoke' => 'entfernen',
'List of authorized users' => 'Liste der authorisierten Benutzer',
'User' => 'Benutzer',
'Everybody have access to this project.' => 'Jeder hat Zugang zu diesem Projekt.',
'You are not allowed to access to this project.' => 'Unzureichende Zugriffsrechte zu diesem Projekt.',
'Comments' => 'Kommentare',
'Post comment' => 'Kommentieren',
'Write your text in Markdown' => 'Schreibe deinen Text in Markdown-Syntax',
'Leave a comment' => 'Kommentar eingeben...',
'Comment is required' => 'Ein Kommentar wird benötigt',
'Leave a description' => 'Beschreibung eingeben...',
'Comment added successfully.' => 'Kommentar erfolgreich hinzugefügt.',
'Unable to create your comment.' => 'Hinzufügen eines Kommentars nicht möglich.',
'The description is required' => 'Eine Beschreibung wird benötigt',
'Edit this task' => 'Aufgabe bearbeiten',
'Due Date' => 'Fällig am',
'm/d/Y' => 'd.m.Y', // Date format parsed with php
'month/day/year' => 'TT.MM.JJJJ', // Help shown to the user
'Invalid date' => 'Ungültiges Datum',
'Must be done before %B %e, %G' => 'Muss vor dem %d.%m.%Y erledigt werden',
'%B %e, %G' => '%d.%m.%Y',
'Automatic actions' => 'Automatische Aktionen',
'Your automatic action have been created successfully.' => 'Die Automatische Aktion wurde erfolgreich erstellt.',
'Unable to create your automatic action.' => 'Automatische Aktion konnte nicht erstellt werden.',
'Remove an action' => 'Aktion löschen',
'Unable to remove this action.' => 'Aktion konnte nicht gelöscht werden',
'Action removed successfully.' => 'Aktion erfolgreich gelöscht.',
'Automatic actions for the project "%s"' => 'Automatische Aktionen für das Projekt «%s»',
'Defined actions' => 'Definierte Aktionen',
'Add an action' => 'Aktion hinzufügen',
'Event name' => 'Ereignis',
'Action name' => 'Aktion',
'Action parameters' => 'Aktionsparameter',
'Action' => 'Aktion',
'Event' => 'Ereignis',
'When the selected event occurs execute the corresponding action.' => 'Wenn das gewählte Ereignis eintritt, führe die zugehörige Aktion aus.',
'Next step' => 'Weiter',
'Define action parameters' => 'Aktionsparameter definieren',
'Save this action' => 'Aktion speichern',
'Do you really want to remove this action: "%s"?' => 'Soll diese Aktion wirklich gelöscht werden: «%s»?',
'Remove an automatic action' => 'Löschen einer automatischen Aktion',
'Close the task' => 'Aufgabe abschließen',
'Assign the task to a specific user' => 'Aufgabe einem Benutzer zuordnen',
'Assign the task to the person who does the action' => 'Aufgabe dem Benutzer zuordnen, der die Aktion ausgeführt hat',
'Duplicate the task to another project' => 'Aufgabe in ein anderes Projekt kopieren',
'Move a task to another column' => 'Aufgabe in andere Spalte verschoben',
'Move a task to another position in the same column' => 'Aufgabe an andere Position in der gleichen Spalte verschoben',
'Task modification' => 'Änderung einer Aufgabe',
'Task creation' => 'Erstellung einer Aufgabe',
'Open a closed task' => 'Abgeschlossenen Aufgabe wieder eröffnen',
'Closing a task' => 'Aufgabe abschließen',
'Assign a color to a specific user' => 'Einem Benutzer eine Farbe zuordnen',
'Column title' => 'Spaltentitel',
'Position' => 'Position',
'Move Up' => 'nach oben',
'Move Down' => 'nach unten',
'Duplicate to another project' => 'In ein anderes Projekt duplizieren',
'Duplicate' => 'Duplizieren',
'link' => 'Link',
'Update this comment' => 'Kommentar aktualisieren',
'Comment updated successfully.' => 'Kommentar erfolgreich aktualisiert.',
'Unable to update your comment.' => 'Kommentar konnte nicht aktualisiert werden.',
'Remove a comment' => 'Kommentar löschen',
'Comment removed successfully.' => 'Kommentar erfolgreich gelöscht.',
'Unable to remove this comment.' => 'Kommentar konnte nicht gelöscht werden.',
'Do you really want to remove this comment?' => 'Soll dieser Kommentar wirklich gelöscht werden?',
'Only administrators or the creator of the comment can access to this page.' => 'Nur Administratoren und der Ersteller des Kommentars könne diese Seite verwenden.',
'Details' => 'Details',
'Current password for the user "%s"' => 'Aktuelles Passwort für den Benutzer «%s»',
'The current password is required' => 'Das aktuelle Passwort wird benötigt',
'Wrong password' => 'Falsches Passwort',
'Reset all tokens' => 'Alle Tokens zurücksetzten',
'All tokens have been regenerated.' => 'Alle Tokens wurden zurückgesetzt.',
'Unknown' => 'Unbekannt',
'Last logins' => 'Letzte Anmeldungen',
'Login date' => 'Anmeldedatum',
'Authentication method' => 'Anmeldemethode',
'IP address' => 'IP Adresse',
'User agent' => 'User Agent',
'Persistent connections' => 'Bestehende Verbindungen',
'No session' => 'Keine Session',
'Expiration date' => 'Ablaufdatum',
'Remember Me' => 'Angemeldet bleiben',
'Creation date' => 'Erstellungsdatum',
'Filter by user' => 'Benutzer filtern',
'Filter by due date' => 'Fälligkeit filtern',
'Everybody' => 'Alle',
'Open' => 'Offen',
'Closed' => 'Abgeschlossen',
'Search' => 'Suchen',
'Nothing found.' => 'Nichts gefunden.',
'Search in the project "%s"' => 'Suche in Projekt «%s»',
'Due date' => 'Fälligkeitsdatum',
'Others formats accepted: %s and %s' => 'Andere akzeptierte Formate: %s und %s',
'Description' => 'Beschreibung',
'%d comments' => '%d Kommentare',
'%d comment' => '%d Kommentar',
'Email address invalid' => 'Ungültige Email-Adresse',
'Your Google Account is not linked anymore to your profile.' => 'Google Account nicht mehr mit dem Profil verbunden.',
'Unable to unlink your Google Account.' => 'Trennung der Verbindung zum Google Account nicht möglich.',
'Google authentication failed' => 'Zugang mit Google fehl geschlagen',
'Unable to link your Google Account.' => 'Verbindung mit diesem Google Account nicht möglich.',
'Your Google Account is linked to your profile successfully.' => 'Der Google Account wurde erfolgreich verbunden.',
'Email' => 'Email',
'Link my Google Account' => 'Verbinde meinen Google Account',
'Unlink my Google Account' => 'Verbindung mit meinem Google Account trennen',
'Login with my Google Account' => 'Anmelden mit meinem Google Account',
'Project not found.' => 'Das Projekt wurde nicht gefunden.',
'Task #%d' => 'Aufgabe #%d',
'Task removed successfully.' => 'Aufgabe erfolgreich gelöscht.',
'Unable to remove this task.' => 'Löschen der Aufgabe nicht möglich.',
'Remove a task' => 'Aufgabe löschen',
'Do you really want to remove this task: "%s"?' => 'Soll diese Aufgabe wirklich gelöscht werden: «%s»?',
'Assign automatically a color based on a category' => 'Automatisch eine Farbe anhand der Kategorie vergeben',
'Assign automatically a category based on a color' => 'Automatisch eine Kategorie anhand der Farbe vergeben',
'Task creation or modification' => 'Erstellung oder Änderung einer Aufgabe',
'Category' => 'Kategorie',
'Category:' => 'Kategorie:',
'Categories' => 'Kategorien',
'Category not found.' => 'Kategorie nicht gefunden.',
'Your category have been created successfully.' => 'Kategorie erfolgreich erstellt.',
'Unable to create your category.' => 'Erstellung der Kategorie nicht möglich.',
'Your category have been updated successfully.' => 'Kategorie erfolgreich aktualisiert.',
'Unable to update your category.' => 'Änderung der Kategorie nicht möglich.',
'Remove a category' => 'Kategorie löschen',
'Category removed successfully.' => 'Kategorie erfolgreich gelöscht.',
'Unable to remove this category.' => 'Löschen der Kategorie nicht möglich.',
'Category modification for the project "%s"' => 'Kategorie für das Projekt «%s» bearbeiten',
'Category Name' => 'Kategoriename',
'Categories for the project "%s"' => 'Kategorien des Projektes «%s»',
'Add a new category' => 'Neue Kategorie',
'Do you really want to remove this category: "%s"?' => 'Soll diese Kategorie wirklich gelöscht werden: «%s»?',
'Filter by category' => 'Kategorie filtern',
'All categories' => 'Alle Kategorien',
'No category' => 'keine Kategorie',
'The name is required' => 'Der Name ist erforderlich',
'Remove a file' => 'Datei löschen',
'Unable to remove this file.' => 'Löschen der Datei nicht möglich.',
'File removed successfully.' => 'Datei erfolgreich gelöscht.',
'Attach a document' => 'Datei anhängen',
'Do you really want to remove this file: "%s"?' => 'Soll diese Datei wirklich gelöscht werden: «%s»?',
'open' => 'öffnen',
'Attachments' => 'Anhänge',
'Edit the task' => 'Aufgabe bearbeiten',
'Edit the description' => 'Beschreibung bearbeiten',
'Add a comment' => 'Kommentar hinzufügen',
'Edit a comment' => 'Kommentar bearbeiten',
'Summary' => 'Zusammenfassung',
'Time tracking' => 'Zeiterfassung',
'Estimate:' => 'Geschätzt:',
'Spent:' => 'Aufgewendet:',
'Do you really want to remove this sub-task?' => 'Soll diese Unteraufgabe wirklich gelöscht werden: «%s»?',
'Remaining:' => 'Verbleibend:',
'hours' => 'Stunden',
'spent' => 'aufgewendet',
'estimated' => 'geschätzt',
'Sub-Tasks' => 'Unteraufgaben',
'Add a sub-task' => 'Unteraufgabe anlegen',
'Original Estimate' => 'Geschätzter Aufwand',
'Create another sub-task' => 'Weitere Unteraufgabe anlegen',
'Time Spent' => 'Aufgewendete Zeit',
'Edit a sub-task' => 'Unteraufgabe bearbeiten',
'Remove a sub-task' => 'Unteraufgabe löschen',
'The time must be a numeric value' => 'Zeit nur als nummerische Angabe',
'Todo' => 'Nicht gestartet',
'In progress' => 'In Bearbeitung',
'Done' => 'Erledigt',
'Sub-task removed successfully.' => 'Unteraufgabe erfolgreich gelöscht.',
'Unable to remove this sub-task.' => 'Löschen der Unteraufgabe nicht möglich.',
'Sub-task updated successfully.' => 'Unteraufgabe erfolgreich aktualisiert.',
'Unable to update your sub-task.' => 'Aktualisieren der Unteraufgabe nicht möglich.',
'Unable to create your sub-task.' => 'Erstellen der Unteraufgabe nicht möglich.',
'Sub-task added successfully.' => 'Unteraufgabe erfolgreich angelegt.',
'Maximum size: ' => 'Maximalgröße: ',
'Unable to upload the file.' => 'Hochladen der Datei nicht möglich.',
'Display another project' => 'Zu Projekt wechseln...',
'Your GitHub account was successfully linked to your profile.' => 'GitHub Account erfolgreich mit dem Profil verbunden.',
'Unable to link your GitHub Account.' => 'Verbindung mit diesem GitHub Account nicht möglich.',
'GitHub authentication failed' => 'Zugang mit GitHub fehl geschlagen',
'Your GitHub account is no longer linked to your profile.' => 'GitHub Account nicht mehr mit dem Profil verbunden.',
'Unable to unlink your GitHub Account.' => 'Trennung der Verbindung zum GitHub Account nicht möglich.',
'Login with my GitHub Account' => 'Anmelden mit meinem GitHub Account',
'Link my GitHub Account' => 'Mit meinem GitHub Account verbinden',
'Unlink my GitHub Account' => 'Verbindung mit meinem GitHub Account trennen',
'Created by %s' => 'Erstellt durch %s',
'Last modified on %B %e, %G at %k:%M %p' => 'Letzte Änderung am %d.%m.%Y um %H:%M',
);

View file

@ -0,0 +1,389 @@
<?php
return array(
'English' => 'Inglés',
'French' => 'Francés',
'Polish' => 'Polaco',
'Portuguese (Brazilian)' => 'Portugués (Brasil)',
'Spanish' => 'Español',
// 'German' => '',
// 'Chinese (Simplified)' => '',
// 'Swedish' => 'Suèdois',
'None' => 'Ninguno',
'edit' => 'modificar',
'Edit' => 'Modificar',
'remove' => 'suprimir',
'Remove' => 'Suprimir',
'Update' => 'Actualizar',
'Yes' => 'Sí',
'No' => 'No',
'cancel' => 'cancelar',
'or' => 'o',
'Yellow' => 'Amarillo',
'Blue' => 'Azul',
'Green' => 'Verde',
'Purple' => 'Púrpura',
'Red' => 'Rojo',
'Orange' => 'Naranja',
'Grey' => 'Gris',
'Save' => 'Guardar',
'Login' => 'Iniciar sesión (Ingresar)',
'Official website:' => 'Página web oficial :',
'Unassigned' => 'No asignado',
'View this task' => 'Ver esta tarea',
'Remove user' => 'Eliminar un usuario',
'Do you really want to remove this user: "%s"?' => '¿De verdad que deseas suprimir a este usuario: « %s » ?',
'New user' => 'Añadir un usuario',
'All users' => 'Todos los usuarios',
'Username' => 'Nombre de usuario',
'Password' => 'Contraseña',
'Default Project' => 'Proyecto por defecto',
'Administrator' => 'Administrador',
'Sign in' => 'Iniciar sesión',
'Users' => 'Usuarios',
'No user' => 'Ningún usuario',
'Forbidden' => 'Acceso denegado',
'Access Forbidden' => 'Acceso denegado',
'Only administrators can access to this page.' => 'Solo los administradores pueden acceder a esta página.',
'Edit user' => 'Editar un usuario',
'Logout' => 'Salir',
'Bad username or password' => 'Usuario o contraseña incorecto',
'users' => 'usuarios',
'projects' => 'proyectos',
'Edit project' => 'Editar el proyecto',
'Name' => 'Nombre',
'Activated' => 'Activado',
'Projects' => 'Proyectos',
'No project' => 'Ningún proyecto',
'Project' => 'Proyecto',
'Status' => 'Estado',
'Tasks' => 'Tareas',
'Board' => 'Tablero',
'Inactive' => 'Inactivo',
'Active' => 'Activo',
'Column %d' => 'Columna %d',
'Add this column' => 'Añadir esta columna',
'%d tasks on the board' => '%d tareas en el tablero',
'%d tasks in total' => '%d tareas en total',
'Unable to update this board.' => 'No se puede actualizar este tablero.',
'Edit board' => 'Editar este tablero',
'Disable' => 'Desactivar',
'Enable' => 'Activar',
'New project' => 'Nuevo proyecto',
'Do you really want to remove this project: "%s"?' => '¿De verdad que deseas eliminar este proyecto: « %s » ?',
'Remove project' => 'Suprimir el proyecto',
'Boards' => 'Tableros',
'Edit the board for "%s"' => 'Modificar el tablero por « %s »',
'All projects' => 'Todos los proyectos',
'Change columns' => 'Cambiar las columnas',
'Add a new column' => 'Añadir una nueva columna',
'Title' => 'Titulo',
'Add Column' => 'Nueva columna',
'Project "%s"' => 'Proyecto « %s »',
'Nobody assigned' => 'Nadie asignado',
'Assigned to %s' => 'Asignada a %s',
'Remove a column' => 'Suprimir esta columna',
'Remove a column from a board' => 'Suprimir una columna de un tablero',
'Unable to remove this column.' => 'No se puede suprimir esta columna.',
'Do you really want to remove this column: "%s"?' => '¿De vedad que deseas eliminar esta columna : « %s » ?',
'This action will REMOVE ALL TASKS associated to this column!' => '¡Esta acción SUPRIMIRÁ TODAS LAS TAREAS asociadas a esta columna!',
'Settings' => 'Preferencias',
'Application settings' => 'Parámetros de la aplicación',
'Language' => 'Idioma',
'Webhooks token:' => 'Identificador (token) para los webhooks :',
'More information' => 'Más informaciones',
'Database size:' => 'Tamaño de la base de datos:',
'Download the database' => 'Descargar la base de datos',
'Optimize the database' => 'Optimizar la base de datos',
'(VACUUM command)' => '(Comando VACUUM)',
'(Gzip compressed Sqlite file)' => '(Archivo Sqlite comprimido en Gzip)',
'User settings' => 'Parámetros de usuario',
'My default project:' => 'Mi proyecto por defecto: ',
'Close a task' => 'Cerrar una tarea',
'Do you really want to close this task: "%s"?' => '¿Realmente desea cerrar esta tarea: « %s » ?',
'Edit a task' => 'Editar una tarea',
'Column' => 'Columna',
'Color' => 'Color',
'Assignee' => 'Persona asignada',
'Create another task' => 'Crear una nueva tarea',
'New task' => 'Nueva tarea',
'Open a task' => 'Abrir una tarea',
'Do you really want to open this task: "%s"?' => '¿Realmente desea abrir esta tarea: « %s » ?',
'Back to the board' => 'Volver al tablero',
'Created on %B %e, %G at %k:%M %p' => 'Creado el %d/%m/%Y a las %H:%M',
'There is nobody assigned' => 'No hay nadie asignado a esta tarea',
'Column on the board:' => 'Columna en el tablero: ',
'Status is open' => 'Estado abierto',
'Status is closed' => 'Estado cerrado',
'Close this task' => 'Cerrar esta tarea',
'Open this task' => 'Abrir esta tarea',
'There is no description.' => 'No hay descripción.',
'Add a new task' => 'Añadir una nueva tarea',
'The username is required' => 'El nombre de usuario es obligatorio',
'The maximum length is %d characters' => 'La longitud máxima es de %d caracteres',
'The minimum length is %d characters' => 'La longitud mínima es de %d caracteres',
'The password is required' => 'La contraseña es obligatoria',
'This value must be an integer' => 'Este valor debe ser un entero',
'The username must be unique' => 'El nombre de usuario debe ser único',
'The username must be alphanumeric' => 'El nombre de usuario debe ser alfanumérico',
'The user id is required' => 'El identificador del usuario es obligatorio',
'Passwords doesn\'t matches' => 'Las contraseñas no corresponden',
'The confirmation is required' => 'La confirmación es obligatoria',
'The column is required' => 'La columna es obligatoria',
'The project is required' => 'El proyecto es obligatorio',
'The color is required' => 'El color es obligatorio',
'The id is required' => 'El identificador es obligatorio',
'The project id is required' => 'El identificador del proyecto es obligatorio',
'The project name is required' => 'El nombre del proyecto es obligatorio',
'This project must be unique' => 'El nombre del proyecto debe ser único',
'The title is required' => 'El titulo es obligatorio',
'The language is required' => 'El idioma es obligatorio',
'There is no active project, the first step is to create a new project.' => 'No hay proyectos activados, la primera etapa consiste en crear un nuevo proyecto.',
'Settings saved successfully.' => 'Parámetros guardados correctamente.',
'Unable to save your settings.' => 'No se pueden guardar sus parámetros.',
'Database optimization done.' => 'Optimización de la base de datos terminada.',
'Your project have been created successfully.' => 'El proyecto ha sido creado correctamente.',
'Unable to create your project.' => 'No se puede crear el proyecto.',
'Project updated successfully.' => 'El proyecto ha sido actualizado correctamente.',
'Unable to update this project.' => 'No se puede actualizar el proyecto.',
'Unable to remove this project.' => 'No se puede suprimir este proyecto.',
'Project removed successfully.' => 'El proyecto ha sido borrado correctamente.',
'Project activated successfully.' => 'El proyecto ha sido activado correctamente.',
'Unable to activate this project.' => 'No se puede activar el proyecto.',
'Project disabled successfully.' => 'El proyecto ha sido desactivado correctamente.',
'Unable to disable this project.' => 'No se puede desactivar el proyecto.',
'Unable to open this task.' => 'No se puede abrir esta tarea.',
'Task opened successfully.' => 'La tarea ha sido abierta correctamente.',
'Unable to close this task.' => 'No se puede cerrar esta tarea.',
'Task closed successfully.' => 'La tarea ha sido cerrada correctamente.',
'Unable to update your task.' => 'No se puede modificar esta tarea.',
'Task updated successfully.' => 'La tarea ha sido actualizada correctamente.',
'Unable to create your task.' => 'No se puede crear esta tarea.',
'Task created successfully.' => 'La tarea ha sido creada correctamente.',
'User created successfully.' => 'El usuario ha sido creado correctamente.',
'Unable to create your user.' => 'No se puede crear este usuario.',
'User updated successfully.' => 'El usuario ha sido actualizado correctamente.',
'Unable to update your user.' => 'No se puede actualizar este usuario.',
'User removed successfully.' => 'El usuario ha sido creado correctamente.',
'Unable to remove this user.' => 'No se puede crear este usuario.',
'Board updated successfully.' => 'El tablero ha sido actualizado correctamente.',
'Ready' => 'Listo',
'Backlog' => 'En espera',
'Work in progress' => 'En curso',
'Done' => 'Terminado',
'Application version:' => 'Versión de la aplicación:',
'Completed on %B %e, %G at %k:%M %p' => 'Completado el %d/%m/%Y a las %H:%M',
'%B %e, %G at %k:%M %p' => '%d/%m/%Y a las %H:%M',
'Date created' => 'Fecha de creación',
'Date completed' => 'Fecha de terminación',
'Id' => 'Identificador',
'No task' => 'Ninguna tarea',
'Completed tasks' => 'Tareas completadas',
'List of projects' => 'Lista de los proyectos',
'Completed tasks for "%s"' => 'Tarea completada por « %s »',
'%d closed tasks' => '%d tareas completadas',
'no task for this project' => 'ninguna tarea para este proyecto',
'Public link' => 'Enlace público',
'There is no column in your project!' => '¡No hay ninguna columna para este proyecto!',
'Change assignee' => 'Cambiar la persona asignada',
'Change assignee for the task "%s"' => 'Cambiar la persona asignada por la tarea « %s »',
'Timezone' => 'Zona horaria',
'Sorry, I didn\'t found this information in my database!' => 'Lo siento no he encontrado información en la base de datos!',
'Page not found' => 'Página no encontrada',
'Story Points' => 'Complejidad',
'limit' => 'límite',
'Task limit' => 'Número máximo de tareas',
'This value must be greater than %d' => 'Este valor no debe de ser más grande que %d',
'Edit project access list' => 'Editar los permisos del proyecto',
'Edit users access' => 'Editar los permisos de usuario',
'Allow this user' => 'Autorizar este usuario',
'Project access list for "%s"' => 'Permisos del proyecto « %s »',
'Only those users have access to this project:' => 'Solo estos usuarios tienen acceso a este proyecto:',
'Don\'t forget that administrators have access to everything.' => 'No olvide que los administradores tienen acceso a todo.',
'revoke' => 'revocar',
'List of authorized users' => 'Lista de los usuarios autorizados',
'User' => 'Usuario',
'Everybody have access to this project.' => 'Todo el mundo tiene acceso al proyecto.',
'You are not allowed to access to this project.' => 'No está autorizado a acceder a este proyecto.',
'Comments' => 'Comentarios',
'Post comment' => 'Commentar',
'Write your text in Markdown' => 'Redacta el texto en Markdown',
'Leave a comment' => 'Dejar un comentario',
'Comment is required' => 'El comentario es obligatorio',
'Leave a description' => 'Dejar una descripción',
'Comment added successfully.' => 'El comentario ha sido añadido correctamente.',
'Unable to create your comment.' => 'No se puede crear este comentario.',
'The description is required' => 'La descripción es obligatoria',
'Edit this task' => 'Editar esta tarea',
'Due Date' => 'Fecha límite',
'm/d/Y' => 'd/m/Y', // Date format parsed with php
'month/day/year' => 'día/mes/año', // Help shown to the user
'Invalid date' => 'Fecha no válida',
'Must be done before %B %e, %G' => 'Debe de estar hecho antes del %d/%m/%Y',
'%B %e, %G' => '%d/%m/%Y',
'Automatic actions' => 'Acciones automatizadas',
'Your automatic action have been created successfully.' => 'La acción automatizada ha sido creada correctamente.',
'Unable to create your automatic action.' => 'No se puede crear esta acción automatizada.',
'Remove an action' => 'Suprimir una acción',
'Unable to remove this action.' => 'No se puede suprimir esta accción.',
'Action removed successfully.' => 'La acción ha sido borrada correctamente.',
'Automatic actions for the project "%s"' => 'Acciones automatizadas para este proyecto « %s »',
'Defined actions' => 'Acciones definidas',
'Event name' => 'Nombre del evento',
'Action name' => 'Nombre de la acción',
'Action parameters' => 'Parámetros de la acción',
'Action' => 'Acción',
'Event' => 'Evento',
'When the selected event occurs execute the corresponding action.' => 'Cuando tiene lugar el evento seleccionado, ejecutar la acción correspondiente.',
'Next step' => 'Etapa siguiente',
'Define action parameters' => 'Definición de los parametros de la acción',
'Save this action' => 'Guardar esta acción',
'Do you really want to remove this action: "%s"?' => '¿Realmente desea suprimir esta acción « %s » ?',
'Remove an automatic action' => 'Suprimir una acción automatizada',
'Close the task' => 'Cerrar esta tarea',
'Assign the task to a specific user' => 'Asignar una tarea a un usuario especifico',
'Assign the task to the person who does the action' => 'Asignar la tarea al usuario que hace la acción',
'Duplicate the task to another project' => 'Duplicar la tarea a otro proyecto',
'Move a task to another column' => 'Mover una tarea a otra columna',
'Move a task to another position in the same column' => 'Mover una tarea a otra posición en la misma columna',
'Task modification' => 'Modificación de una tarea',
'Task creation' => 'Creación de una tarea',
'Open a closed task' => 'Abrir una tarea cerrada',
'Closing a task' => 'Cerrar una tarea',
'Assign a color to a specific user' => 'Asignar un color a un usuario específico',
'Column title' => 'Título de la columna',
'Position' => 'Posición',
'Move Up' => 'Mover hacia arriba',
'Move Down' => 'Mover hacia abajo',
'Duplicate to another project' => 'Duplicar a otro proyecto',
'Duplicate' => 'Duplicar',
'link' => 'enlace',
'Update this comment' => 'Actualizar este comentario',
'Comment updated successfully.' => 'El comentario ha sido actualizado correctamente.',
'Unable to update your comment.' => 'No se puede actualizar este comentario.',
'Remove a comment' => 'Suprimir un comentario',
'Comment removed successfully.' => 'El comentario ha sido suprimido correctamente.',
'Unable to remove this comment.' => 'No se puede suprimir este comentario.',
'Do you really want to remove this comment?' => '¿Desea suprimir este comentario?',
'Only administrators or the creator of the comment can access to this page.' => 'Sólo los administradores o el autor del comentario tienen acceso a esta página.',
'Details' => 'Detalles',
'Current password for the user "%s"' => 'Contraseña actual para el usuario: « %s »',
'The current password is required' => 'La contraseña es obligatoria',
'Wrong password' => 'contraseña incorrecta',
'Reset all tokens' => 'Reiniciar las fichas (tokens) de seguridad ',
'All tokens have been regenerated.' => 'Todas las fichas (tokens) han sido regeneradas.',
'Unknown' => 'Desconocido',
'Last logins' => 'Últimos ingresos',
'Login date' => 'Fecha de ingreso',
'Authentication method' => 'Método de autenticación',
'IP address' => 'Dirección IP',
'User agent' => 'Agente de usuario',
'Persistent connections' => 'Conexión persistente',
'No session' => 'No existe sesión',
'Expiration date' => 'Fecha de expiración',
'Remember Me' => 'Recuérdame',
'Creation date' => 'Fecha de creación',
'Filter by user' => 'Filtrado mediante usuario',
'Filter by due date' => 'Filtrado mediante fecha límite',
'Everybody' => 'Todo el mundo',
'Open' => 'Abierto',
'Closed' => 'Cerrado',
'Search' => 'Buscar',
'Nothing found.' => 'Nada hallado.',
'Search in the project "%s"' => 'Buscar en el proyecto "%s"',
'Due date' => 'Fecha límite',
'Others formats accepted: %s and %s' => 'Otros formatos aceptados: %s y %s',
'Description' => 'Descripción',
'%d comments' => '%d comentarios',
'%d comment' => '%d comentario',
'Email address invalid' => 'Dirección de correo inválida',
'Your Google Account is not linked anymore to your profile.' => 'Tu Cuenta en Google ya no se encuentra enlazada con tu perfil',
'Unable to unlink your Google Account.' => 'No puedo desenlazar tu Cuenta en Google.',
'Google authentication failed' => 'Ha fallado tu autenticación en Google',
'Unable to link your Google Account.' => 'No puedo enlazar con tu Cuenta en Google.',
'Your Google Account is linked to your profile successfully.' => 'Se ha enlazado correctamente tu Cuenta en Google con tu perfil.',
'Email' => 'Correo',
'Link my Google Account' => 'Enlaza con mi Cuenta en Google',
'Unlink my Google Account' => 'Desenlaza con mi Cuenta en Google',
'Login with my Google Account' => 'Ingresa con mi Cuenta en Google',
'Project not found.' => 'Proyecto no hallado.',
'Task #%d' => 'Tarea número %d',
'Task removed successfully.' => 'Tarea suprimida correctamente.',
'Unable to remove this task.' => 'No pude suprimir esta tarea.',
'Remove a task' => 'Borrar una tarea',
'Do you really want to remove this task: "%s"?' => '¿De verdad que quieres suprimir esta tarea: "%s"?',
'Assign automatically a color based on a category' => 'Asignar un color de forma automática basándose en la categoría',
'Assign automatically a category based on a color' => 'Asignar una categoría de forma automática basándose en el color',
'Task creation or modification' => 'Creación o Edición de Tarea',
'Category' => 'Categoría',
'Category:' => 'Categoría:',
'Categories' => 'Categorías',
'Category not found.' => 'Categoría no hallada.',
'Your category have been created successfully.' => 'Se ha creado tu categoría correctamente.',
'Unable to create your category.' => 'No pude crear tu categoría.',
'Your category have been updated successfully.' => 'Se ha actualizado tu categoría correctamente.',
'Unable to update your category.' => 'No pude actualizar tu categoría.',
'Remove a category' => 'Suprimir una categoría',
'Category removed successfully.' => 'Categoría suprimida correctamente.',
'Unable to remove this category.' => 'No pude suprimir esta categoría.',
'Category modification for the project "%s"' => 'Modificación de categoría pra el proyecto "%s"',
'Category Name' => 'Nombre de Categoría',
'Categories for the project "%s"' => 'Categorías para el proyecto',
'Add a new category' => 'Añadir una nueva categoría',
'Do you really want to remove this category: "%s"?' => '¿De verdad que quieres suprimir esta categoría: "%s"?',
'Filter by category' => 'Filtrar mendiante categoría',
'All categories' => 'Todas las categorías',
'No category' => 'Sin categoría',
'The name is required' => 'El nombre es obligatorio',
'Remove a file' => 'Borrar un fichero',
'Unable to remove this file.' => 'No pude borrar este fichero.',
'File removed successfully.' => 'Fichero borrado correctamente.',
'Attach a document' => 'Adjuntar un documento',
'Do you really want to remove this file: "%s"?' => '¿De verdad que quieres borrar este fichero: "%s"?',
'open' => 'abrir',
'Attachments' => 'Adjuntos',
'Edit the task' => 'Editar la tarea',
'Edit the description' => 'Editar la descripción',
'Add a comment' => 'Añadir un comentario',
'Edit a comment' => 'Editar un comentario',
'Summary' => 'Resumen',
'Time tracking' => 'Seguimiento temporal',
'Estimate:' => 'Estimado:',
'Spent:' => 'Transcurrido:',
'Do you really want to remove this sub-task?' => '¿De verdad que quieres suprimir esta sub-tarea?',
'Remaining:' => 'Quedando',
'hours' => 'horas',
'spent' => 'transcurrido',
'estimated' => 'estimado',
'Sub-Tasks' => 'Sub-Tareas',
'Add a sub-task' => 'Añadir una sub-tarea',
'Original Estimate' => 'Estimado Original',
'Create another sub-task' => 'Crear otra sub-tarea',
'Time Spent' => 'Tiempo Transcurrido',
'Edit a sub-task' => 'Editar una sub-tarea',
'Remove a sub-task' => 'Suprimir una sub-tarea',
'The time must be a numeric value' => 'El tiempo debe de ser un valor numérico',
'Todo' => 'Por hacer',
'In progress' => 'En progreso',
'Done' => 'Hecho',
'Sub-task removed successfully.' => 'Sub-tarea suprimida correctamente.',
'Unable to remove this sub-task.' => 'No pude suprimir esta sub-tarea.',
'Sub-task updated successfully.' => 'Sub-tarea actualizada correctamente.',
'Unable to update your sub-task.' => 'No pude actualizar tu sub-tarea.',
'Unable to create your sub-task.' => 'No pude crear tu sub-tarea.',
'Sub-task added successfully.' => 'Sub-tarea añadida correctamente.',
'Maximum size: ' => 'Tamaño máximo',
'Unable to upload the file.' => 'No pude cargar el fichero.',
'Actions' => 'Acciones',
// 'Display another project' => '',
// 'Your GitHub account was successfully linked to your profile.' => '',
// 'Unable to link your GitHub Account.' => '',
// 'GitHub authentication failed' => '',
// 'Your GitHub account is no longer linked to your profile.' => '',
// 'Unable to unlink your GitHub Account.' => '',
// 'Login with my GitHub Account' => '',
// 'Link my GitHub Account' => '',
// 'Unlink my GitHub Account' => '',
// 'Created by %s' => 'Créé par %s',
// 'Last modified on %B %e, %G at %k:%M %p' => '',
);

View file

@ -0,0 +1,387 @@
<?php
return array(
'English' => 'Anglais',
'French' => 'Français',
'Polish' => 'Polonais',
'Portuguese (Brazilian)' => 'Portugais (Brésil)',
'Spanish' => 'Espagnol',
'German' => 'Allemand',
'Chinese (Simplified)' => 'Chinois simplifié',
'Swedish' => 'Suèdois',
'None' => 'Aucun',
'edit' => 'modifier',
'Edit' => 'Modifier',
'remove' => 'supprimer',
'Remove' => 'Supprimer',
'Update' => 'Mettre à jour',
'Yes' => 'Oui',
'No' => 'Non',
'cancel' => 'annuler',
'or' => 'ou',
'Yellow' => 'Jaune',
'Blue' => 'Bleu',
'Green' => 'Vert',
'Purple' => 'Violet',
'Red' => 'Rouge',
'Orange' => 'Orange',
'Grey' => 'Gris',
'Save' => 'Enregistrer',
'Login' => 'Connexion',
'Official website:' => 'Site web officiel :',
'Unassigned' => 'Non assigné',
'View this task' => 'Voir cette tâche',
'Remove user' => 'Supprimer un utilisateur',
'Do you really want to remove this user: "%s"?' => 'Voulez-vous vraiment supprimer cet utilisateur : « %s » ?',
'New user' => 'Ajouter un utilisateur',
'All users' => 'Tous les utilisateurs',
'Username' => 'Identifiant',
'Password' => 'Mot de passe',
'Default Project' => 'Projet par défaut',
'Administrator' => 'Administrateur',
'Sign in' => 'Connexion',
'Users' => 'Utilisateurs',
'No user' => 'Aucun utilisateur',
'Forbidden' => 'Accès interdit',
'Access Forbidden' => 'Accès interdit',
'Only administrators can access to this page.' => 'Uniquement les administrateurs peuvent accéder à cette page.',
'Edit user' => 'Modifier un utilisateur',
'Logout' => 'Déconnexion',
'Bad username or password' => 'Identifiant ou mot de passe incorrect',
'users' => 'utilisateurs',
'projects' => 'projets',
'Edit project' => 'Modifier le projet',
'Name' => 'Nom',
'Activated' => 'Actif',
'Projects' => 'Projets',
'No project' => 'Aucun projet',
'Project' => 'Projet',
'Status' => 'État',
'Tasks' => 'Tâches',
'Board' => 'Tableau',
'Inactive' => 'Inactif',
'Active' => 'Actif',
'Column %d' => 'Colonne %d',
'Add this column' => 'Ajouter cette colonne',
'%d tasks on the board' => '%d tâches sur le tableau',
'%d tasks in total' => '%d tâches au total',
'Unable to update this board.' => 'Impossible de mettre à jour ce tableau.',
'Edit board' => 'Modifier le tableau',
'Disable' => 'Désactiver',
'Enable' => 'Activer',
'New project' => 'Nouveau projet',
'Do you really want to remove this project: "%s"?' => 'Voulez-vous vraiment supprimer ce projet : « %s » ?',
'Remove project' => 'Supprimer le projet',
'Boards' => 'Tableaux',
'Edit the board for "%s"' => 'Modifier le tableau pour « %s »',
'All projects' => 'Tous les projets',
'Change columns' => 'Changer les colonnes',
'Add a new column' => 'Ajouter une nouvelle colonne',
'Title' => 'Titre',
'Add Column' => 'Nouvelle colonne',
'Project "%s"' => 'Projet « %s »',
'Nobody assigned' => 'Personne assigné',
'Assigned to %s' => 'Assigné à %s',
'Remove a column' => 'Supprimer une colonne',
'Remove a column from a board' => 'Supprimer une colonne d\'un tableau',
'Unable to remove this column.' => 'Impossible de supprimer cette colonne.',
'Do you really want to remove this column: "%s"?' => 'Voulez vraiment supprimer cette colonne : « %s » ?',
'This action will REMOVE ALL TASKS associated to this column!' => 'Cette action va supprimer toutes les tâches associées à cette colonne !',
'Settings' => 'Préférences',
'Application settings' => 'Paramètres de l\'application',
'Language' => 'Langue',
'Webhooks token:' => 'Jeton de securité pour les webhooks :',
'More information' => 'Plus d\'informations',
'Database size:' => 'Taille de la base de données :',
'Download the database' => 'Télécharger la base de données',
'Optimize the database' => 'Optimiser la base de données',
'(VACUUM command)' => '(Commande VACUUM)',
'(Gzip compressed Sqlite file)' => '(Fichier Sqlite compressé en Gzip)',
'User settings' => 'Paramètres utilisateur',
'My default project:' => 'Mon projet par défaut : ',
'Close a task' => 'Fermer une tâche',
'Do you really want to close this task: "%s"?' => 'Voulez-vous vraiment fermer cettre tâche : « %s » ?',
'Edit a task' => 'Modifier une tâche',
'Column' => 'Colonne',
'Color' => 'Couleur',
'Assignee' => 'Personne assignée',
'Create another task' => 'Créer une autre tâche',
'New task' => 'Nouvelle tâche',
'Open a task' => 'Ouvrir une tâche',
'Do you really want to open this task: "%s"?' => 'Voulez-vous vraiment ouvrir cette tâche : « %s » ?',
'Back to the board' => 'Retour au tableau',
'Created on %B %e, %G at %k:%M %p' => 'Créé le %d/%m/%Y à %H:%M',
'There is nobody assigned' => 'Il n\'y a personne d\'assigné à cette tâche',
'Column on the board:' => 'Colonne sur le tableau : ',
'Status is open' => 'État ouvert',
'Status is closed' => 'État fermé',
'Close this task' => 'Fermer cette tâche',
'Open this task' => 'Ouvrir cette tâche',
'There is no description.' => 'Il n\'y a pas de description.',
'Add a new task' => 'Ajouter une nouvelle tâche',
'The username is required' => 'Le nom d\'utilisateur est obligatoire',
'The maximum length is %d characters' => 'La longueur maximale est de %d caractères',
'The minimum length is %d characters' => 'La longueur minimale est de %d caractères',
'The password is required' => 'Le mot de passe est obligatoire',
'This value must be an integer' => 'Cette valeur doit être un entier',
'The username must be unique' => 'Le nom d\'utilisateur doit être unique',
'The username must be alphanumeric' => 'Le nom d\'utilisateur doit être alpha-numérique',
'The user id is required' => 'L\'id de l\'utilisateur est obligatoire',
'Passwords don\'t match' => 'Les mots de passe ne correspondent pas',
'The confirmation is required' => 'Le confirmation est requise',
'The column is required' => 'La colonne est obligatoire',
'The project is required' => 'Le projet est obligatoire',
'The color is required' => 'La couleur est obligatoire',
'The id is required' => 'L\'identifiant est obligatoire',
'The project id is required' => 'L\'identifiant du projet est obligatoire',
'The project name is required' => 'Le nom du projet est obligatoire',
'This project must be unique' => 'Le nom du projet doit être unique',
'The title is required' => 'Le titre est obligatoire',
'The language is required' => 'La langue est obligatoire',
'There is no active project, the first step is to create a new project.' => 'Il n\'y a aucun projet actif, la première étape est de créer un nouveau projet.',
'Settings saved successfully.' => 'Paramètres sauvegardés avec succès.',
'Unable to save your settings.' => 'Impossible de sauvegarder vos réglages.',
'Database optimization done.' => 'Optmisation de la base de données terminée.',
'Your project have been created successfully.' => 'Votre projet a été créé avec succès.',
'Unable to create your project.' => 'Impossible de créer un projet.',
'Project updated successfully.' => 'Votre projet a été mis à jour avec succès.',
'Unable to update this project.' => 'Impossible de mettre à jour ce projet.',
'Unable to remove this project.' => 'Impossible de supprimer ce projet.',
'Project removed successfully.' => 'Votre projet a été supprimé avec succès.',
'Project activated successfully.' => 'Votre projet a été activé avec succès.',
'Unable to activate this project.' => 'Impossible d\'activer ce projet.',
'Project disabled successfully.' => 'Votre projet a été désactivé avec succès.',
'Unable to disable this project.' => 'Impossible de désactiver ce projet.',
'Unable to open this task.' => 'Impossible d\'ouvrir cette tâche.',
'Task opened successfully.' => 'Tâche ouverte avec succès.',
'Unable to close this task.' => 'Impossible de fermer cette tâche.',
'Task closed successfully.' => 'Tâche fermé avec succès.',
'Unable to update your task.' => 'Impossible de modifier cette tâche.',
'Task updated successfully.' => 'Tâche mise à jour avec succès.',
'Unable to create your task.' => 'Impossible de créer cette tâche.',
'Task created successfully.' => 'Tâche créée avec succès.',
'User created successfully.' => 'Utilisateur créé avec succès.',
'Unable to create your user.' => 'Impossible de créer cet utilisateur.',
'User updated successfully.' => 'Utilisateur mis à jour avec succès.',
'Unable to update your user.' => 'Impossible de mettre à jour cet utilisateur.',
'User removed successfully.' => 'Utilisateur supprimé avec succès.',
'Unable to remove this user.' => 'Impossible de supprimer cet utilisateur.',
'Board updated successfully.' => 'Tableau mis à jour avec succès.',
'Ready' => 'Prêt',
'Backlog' => 'En attente',
'Work in progress' => 'En cours',
'Done' => 'Terminé',
'Application version:' => 'Version de l\'application :',
'Completed on %B %e, %G at %k:%M %p' => 'Terminé le %d/%m/%Y à %H:%M',
'%B %e, %G at %k:%M %p' => '%d/%m/%Y à %H:%M',
'Date created' => 'Date de création',
'Date completed' => 'Date de clôture',
'Id' => 'Identifiant',
'No task' => 'Aucune tâche',
'Completed tasks' => 'Tâches terminées',
'List of projects' => 'Liste des projets',
'Completed tasks for "%s"' => 'Tâches terminées pour « %s »',
'%d closed tasks' => '%d tâches terminées',
'no task for this project' => 'aucune tâche pour ce projet',
'Public link' => 'Accès public',
'There is no column in your project!' => 'Il n\'y a aucune colonne dans votre projet !',
'Change assignee' => 'Changer la personne assignée',
'Change assignee for the task "%s"' => 'Changer la personne assignée pour la tâche « %s »',
'Timezone' => 'Fuseau horaire',
'Sorry, I didn\'t found this information in my database!' => 'Désolé, je n\'ai pas trouvé cette information dans ma base de données !',
'Page not found' => 'Page introuvable',
'Story Points' => 'Complexité',
'limit' => 'limite',
'Task limit' => 'Nombre maximum de tâches',
'This value must be greater than %d' => 'Cette valeur doit être plus grande que %d',
'Edit project access list' => 'Modifier l\'accès au projet',
'Edit users access' => 'Modifier les utilisateurs autorisés',
'Allow this user' => 'Autoriser cet utilisateur',
'Project access list for "%s"' => 'Liste des accès au projet « %s »',
'Only those users have access to this project:' => 'Seulement ces utilisateurs ont accès à ce projet :',
'Don\'t forget that administrators have access to everything.' => 'N\'oubliez pas que les administrateurs ont accès à tout.',
'revoke' => 'révoquer',
'List of authorized users' => 'Liste des utilisateurs autorisés',
'User' => 'Utilisateur',
'Everybody have access to this project.' => 'Tout le monde a accès au projet.',
'You are not allowed to access to this project.' => 'Vous n\'êtes pas autorisé à accéder à ce projet.',
'Comments' => 'Commentaires',
'Post comment' => 'Commenter',
'Write your text in Markdown' => 'Écrivez votre texte en Markdown',
'Leave a comment' => 'Laissez un commentaire',
'Comment is required' => 'Le commentaire est obligatoire',
'Leave a description' => 'Laissez une description',
'Comment added successfully.' => 'Commentaire ajouté avec succès.',
'Unable to create your comment.' => 'Impossible de sauvegarder votre commentaire.',
'The description is required' => 'La description est obligatoire',
'Edit this task' => 'Modifier cette tâche',
'Due Date' => 'Date d\'échéance',
'm/d/Y' => 'd/m/Y', // Date format parsed with php
'month/day/year' => 'jour/mois/année', // Help shown to the user
'Invalid date' => 'Date invalide',
'Must be done before %B %e, %G' => 'Doit être fait avant le %d/%m/%Y',
'%B %e, %G' => '%d/%m/%Y',
'Automatic actions' => 'Actions automatisées',
'Your automatic action have been created successfully.' => 'Votre action automatisée a été ajouté avec succès.',
'Unable to create your automatic action.' => 'Impossible de créer votre action automatisée.',
'Remove an action' => 'Supprimer une action',
'Unable to remove this action.' => 'Impossible de supprimer cette action',
'Action removed successfully.' => 'Action supprimée avec succès.',
'Automatic actions for the project "%s"' => 'Actions automatisées pour le projet « %s »',
'Defined actions' => 'Actions définies',
'Event name' => 'Nom de l\'événement',
'Action name' => 'Nom de l\'action',
'Action parameters' => 'Paramètres de l\'action',
'Action' => 'Action',
'Event' => 'Événement',
'When the selected event occurs execute the corresponding action.' => 'Lorsque l\'événement sélectionné se déclenche, executer l\'action correspondante.',
'Next step' => 'Étape suivante',
'Define action parameters' => 'Définition des paramètres de l\'action',
'Save this action' => 'Sauvegarder cette action',
'Do you really want to remove this action: "%s"?' => 'Voulez-vous vraiment supprimer cette action « %s » ?',
'Remove an automatic action' => 'Supprimer une action automatisée',
'Close the task' => 'Fermer cette tâche',
'Assign the task to a specific user' => 'Assigner la tâche à un utilisateur spécifique',
'Assign the task to the person who does the action' => 'Assigner la tâche à la personne qui fait l\'action',
'Duplicate the task to another project' => 'Dupliquer la tâche vers un autre projet',
'Move a task to another column' => 'Déplacement d\'une tâche vers un autre colonne',
'Move a task to another position in the same column' => 'Déplacement d\'une tâche à une autre position mais dans la même colonne',
'Task modification' => 'Modification d\'une tâche',
'Task creation' => 'Création d\'une tâche',
'Open a closed task' => 'Ouverture d\'une tâche fermée',
'Closing a task' => 'Fermeture d\'une tâche',
'Assign a color to a specific user' => 'Assigner une couleur à un utilisateur',
'Column title' => 'Titre de la colonne',
'Position' => 'Position',
'Move Up' => 'Déplacer vers le haut',
'Move Down' => 'Déplacer vers le bas',
'Duplicate to another project' => 'Dupliquer dans un autre projet',
'Duplicate' => 'Dupliquer',
'link' => 'lien',
'Update this comment' => 'Mettre à jour ce commentaire',
'Comment updated successfully.' => 'Commentaire mis à jour avec succès.',
'Unable to update your comment.' => 'Impossible de supprimer votre commentaire.',
'Remove a comment' => 'Supprimer un commentaire',
'Comment removed successfully.' => 'Commentaire supprimé avec succès.',
'Unable to remove this comment.' => 'Impossible de supprimer ce commentaire.',
'Do you really want to remove this comment?' => 'Voulez-vous vraiment supprimer ce commentaire ?',
'Only administrators or the creator of the comment can access to this page.' => 'Uniquement les administrateurs ou le créateur du commentaire peuvent accéder à cette page.',
'Details' => 'Détails',
'Current password for the user "%s"' => 'Mot de passe actuel pour l\'utilisateur « %s »',
'The current password is required' => 'Le mot de passe actuel est obligatoire',
'Wrong password' => 'Mauvais mot de passe',
'Reset all tokens' => 'Réinitialiser tous les jetons de sécurité',
'All tokens have been regenerated.' => 'Tous les jetons de sécurité ont été réinitialisés.',
'Unknown' => 'Inconnu',
'Last logins' => 'Dernières connexions',
'Login date' => 'Date de connexion',
'Authentication method' => 'Méthode d\'authentification',
'IP address' => 'Adresse IP',
'User agent' => 'Agent utilisateur',
'Persistent connections' => 'Connexions persistantes',
'No session' => 'Aucune session',
'Expiration date' => 'Date d\'expiration',
'Remember Me' => 'Connexion automatique',
'Creation date' => 'Date de création',
'Filter by user' => 'Filtrer par utilisateur',
'Filter by due date' => 'Filtrer par date d\'échéance',
'Everybody' => 'Tout le monde',
'Open' => 'Ouvert',
'Closed' => 'Fermé',
'Search' => 'Rechercher',
'Nothing found.' => 'Rien trouvé.',
'Search in the project "%s"' => 'Rechercher dans le projet « %s »',
'Due date' => 'Date d\'échéance',
'Others formats accepted: %s and %s' => 'Autres formats acceptés : %s et %s',
'Description' => 'Description',
'%d comments' => '%d commentaires',
'%d comment' => '%d commentaire',
'Email address invalid' => 'Adresse email invalide',
'Your Google Account is not linked anymore to your profile.' => 'Votre compte Google n\'est plus relié à votre profile.',
'Unable to unlink your Google Account.' => 'Impossible de supprimer votre compte Google.',
'Google authentication failed' => 'Authentification Google échouée',
'Unable to link your Google Account.' => 'Impossible de lier votre compte Google.',
'Your Google Account is linked to your profile successfully.' => 'Votre compte Google est désormais lié à votre profile.',
'Email' => 'Email',
'Link my Google Account' => 'Lier mon compte Google',
'Unlink my Google Account' => 'Ne plus utiliser mon compte Google',
'Login with my Google Account' => 'Se connecter avec mon compte Google',
'Project not found.' => 'Projet introuvable.',
'Task #%d' => 'Tâche n°%d',
'Task removed successfully.' => 'Tâche supprimée avec succès.',
'Unable to remove this task.' => 'Impossible de supprimer cette tâche.',
'Remove a task' => 'Supprimer une tâche',
'Do you really want to remove this task: "%s"?' => 'Voulez-vous vraiment supprimer cette tâche « %s » ?',
'Assign automatically a color based on a category' => 'Assigner automatiquement une couleur par rapport à une catégorie définie',
'Assign automatically a category based on a color' => 'Assigner automatiquement une catégorie par rapport à une couleur définie',
'Task creation or modification' => 'Création ou modification d\'une tâche',
'Category' => 'Catégorie',
'Category:' => 'Catégorie :',
'Categories' => 'Catégories',
'Category not found.' => 'Catégorie introuvable',
'Your category have been created successfully.' => 'Votre catégorie a été créé avec succès.',
'Unable to create your category.' => 'Impossible de créer votre catégorie.',
'Your category have been updated successfully.' => 'Votre catégorie a été mise à jour avec succès.',
'Unable to update your category.' => 'Impossible de mettre à jour votre catégorie.',
'Remove a category' => 'Supprimer une catégorie',
'Category removed successfully.' => 'Catégorie supprimée avec succès.',
'Unable to remove this category.' => 'Impossible de supprimer cette catégorie.',
'Category modification for the project "%s"' => 'Modification d\'une catégorie pour le projet « %s »',
'Category Name' => 'Nom de la catégorie',
'Categories for the project "%s"' => 'Catégories du projet « %s »',
'Add a new category' => 'Ajouter une nouvelle catégorie',
'Do you really want to remove this category: "%s"?' => 'Voulez-vous vraiment supprimer cette catégorie « %s » ?',
'Filter by category' => 'Filtrer par catégorie',
'All categories' => 'Toutes les catégories',
'No category' => 'Aucune catégorie',
'The name is required' => 'Le nom est requis',
'Remove a file' => 'Supprimer un fichier',
'Unable to remove this file.' => 'Impossible de supprimer ce fichier.',
'File removed successfully.' => 'Fichier supprimé avec succès.',
'Attach a document' => 'Joindre un document',
'Do you really want to remove this file: "%s"?' => 'Voulez-vous vraiment supprimer ce fichier « %s » ?',
'open' => 'ouvrir',
'Attachments' => 'Pièces-jointes',
'Edit the task' => 'Modifier la tâche',
'Edit the description' => 'Modifier la description',
'Add a comment' => 'Ajouter un commentaire',
'Edit a comment' => 'Modifier un commentaire',
'Summary' => 'Résumé',
'Time tracking' => 'Gestion du temps',
'Estimate:' => 'Estimation :',
'Spent:' => 'Passé :',
'Do you really want to remove this sub-task?' => 'Voulez-vous vraiment supprimer cette sous-tâche ?',
'Remaining:' => 'Restant :',
'hours' => 'heures',
'spent' => 'passé',
'estimated' => 'estimé',
'Sub-Tasks' => 'Sous-Tâches',
'Add a sub-task' => 'Ajouter une sous-tâche',
'Original Estimate' => 'Estimation originale',
'Create another sub-task' => 'Créer une autre sous-tâche',
'Time Spent' => 'Temps passé',
'Edit a sub-task' => 'Modifier une sous-tâche',
'Remove a sub-task' => 'Supprimer une sous-tâche',
'The time must be a numeric value' => 'Le temps doit-être une valeur numérique',
'Todo' => 'À faire',
'In progress' => 'En cours',
'Sub-task removed successfully.' => 'Sous-tâche supprimée avec succès.',
'Unable to remove this sub-task.' => 'Impossible de supprimer cette sous-tâche.',
'Sub-task updated successfully.' => 'Sous-tâche mise à jour avec succès.',
'Unable to update your sub-task.' => 'Impossible de mettre à jour votre sous-tâche.',
'Unable to create your sub-task.' => 'Impossible de créer votre sous-tâche.',
'Sub-task added successfully.' => 'Sous-tâche ajouté avec succès.',
'Maximum size: ' => 'Taille maximum : ',
'Unable to upload the file.' => 'Impossible de transférer le fichier.',
'Display another project' => 'Afficher un autre projet',
'Your GitHub account was successfully linked to your profile.' => 'Votre compte Github est désormais lié avec votre profile.',
'Unable to link your GitHub Account.' => 'Impossible de lier votre compte Github.',
'GitHub authentication failed' => 'L\'authentification avec Github à échouée',
'Your GitHub account is no longer linked to your profile.' => 'Votre compte Github n\'est plus relié avec votre profile.',
'Unable to unlink your GitHub Account.' => 'Impossible de déconnecter votre compte Github.',
'Login with my GitHub Account' => 'Se connecter avec mon compte Github',
'Link my GitHub Account' => 'Lier mon compte Github',
'Unlink my GitHub Account' => 'Ne plus utiliser mon compte Github',
'Created by %s' => 'Créé par %s',
'Last modified on %B %e, %G at %k:%M %p' => 'Modifié le %d/%m/%Y à %H:%M',
);

View file

@ -0,0 +1,390 @@
<?php
return array(
'English' => 'angielski',
'French' => 'francuski',
'Polish' => 'polski',
'Portuguese (Brazilian)' => 'Portugalski (brazylijski)',
'Spanish' => 'Hiszpański',
// 'German' => '',
// 'Chinese (Simplified)' => '',
'None' => 'Brak',
'edit' => 'edytuj',
'Edit' => 'Edytuj',
'remove' => 'usuń',
'Remove' => 'Usuń',
'Update' => 'Aktualizuj',
'Yes' => 'Tak',
'No' => 'Nie',
'cancel' => 'anuluj',
'or' => 'lub',
'Yellow' => 'Żółty',
'Blue' => 'Niebieski',
'Green' => 'Zielony',
'Purple' => 'Fioletowy',
'Red' => 'Czerwony',
'Orange' => 'Pomarańczowy',
'Grey' => 'Szary',
'Save' => 'Zapisz',
'Login' => 'Login',
'Official website:' => 'Oficjalna strona:',
'Unassigned' => 'Nieprzypisany',
'View this task' => 'Zobacz zadanie',
'Remove user' => 'Usuń użytkownika',
'Do you really want to remove this user: "%s"?' => 'Na pewno chcesz usunąć użytkownika: "%s"?',
'New user' => 'Nowy użytkownik',
'All users' => 'Wszyscy użytkownicy',
'Username' => 'Nazwa użytkownika',
'Password' => 'Hasło',
'Default Project' => 'Domyślny projekt',
'Administrator' => 'Administrator',
'Sign in' => 'Zaloguj',
'Users' => 'Użytkownicy',
'No user' => 'Brak użytkowników',
'Forbidden' => 'Zabroniony',
'Access Forbidden' => 'Dostęp zabroniony',
'Only administrators can access to this page.' => 'Tylko administrator może wejść na tą stronę.',
'Edit user' => 'Edytuj użytkownika',
'Logout' => 'Wyloguj',
'Bad username or password' => 'Zła nazwa uyżytkownika lub hasło',
'users' => 'użytkownicy',
'projects' => 'projekty',
'Edit project' => 'Edytuj projekt',
'Name' => 'Nazwa',
'Activated' => 'Aktywny',
'Projects' => 'Projekty',
'No project' => 'Brak projektów',
'Project' => 'Projekt',
'Status' => 'Status',
'Tasks' => 'Zadania',
'Board' => 'Tablica',
'Inactive' => 'Nieaktywny',
'Active' => 'Aktywny',
'Column %d' => 'Kolumna %d',
'Add this column' => 'Dodaj kolumnę',
'%d tasks on the board' => '%d zadań na tablicy',
'%d tasks in total' => '%d wszystkich zadań',
'Unable to update this board.' => 'Nie można zaktualizować tablicy.',
'Edit board' => 'Edytuj tablicę',
'Disable' => 'Wyłącz',
'Enable' => 'Włącz',
'New project' => 'Nowy projekt',
'Do you really want to remove this project: "%s"?' => 'Na pewno chcesz usunąć projekt: "%s"?',
'Remove project' => 'Usuń projekt',
'Boards' => 'Tablice',
'Edit the board for "%s"' => 'Edytuj tablię dla "%s"',
'All projects' => 'Wszystkie projekty',
'Change columns' => 'Zmień kolumny',
'Add a new column' => 'Dodaj nową kolumnę',
'Title' => 'Tytuł',
'Add Column' => 'Dodaj kolumnę',
'Project "%s"' => 'Projekt "%s"',
'Nobody assigned' => 'Nikt nie przypisany',
'Assigned to %s' => 'Przypisane do %s',
'Remove a column' => 'Usuń kolumnę',
'Remove a column from a board' => 'Usuń kolumnę z tablicy',
'Unable to remove this column.' => 'Nie udało się usunąć kolumny.',
'Do you really want to remove this column: "%s"?' => 'Na pewno chcesz usunąć kolumnę: "%s"?',
'This action will REMOVE ALL TASKS associated to this column!' => 'Wszystkie zadania w kolumnie zostaną usunięte!',
'Settings' => 'Ustawienia',
'Application settings' => 'Ustawienia aplikacji',
'Language' => 'Język',
'Webhooks token:' => 'Token :',
'More information' => 'Więcej informacji',
'Database size:' => 'Rozmiar bazy danych :',
'Download the database' => 'Pobierz bazę danych',
'Optimize the database' => 'Optymalizuj bazę danych',
'(VACUUM command)' => '(komenda VACUUM)',
'(Gzip compressed Sqlite file)' => '(baza danych spakowana Gzip)',
'User settings' => 'Ustawienia użytkownika',
'My default project:' => 'Mój domyślny projekt:',
'Close a task' => 'Zakończ zadanie',
'Do you really want to close this task: "%s"?' => 'Na pewno chcesz zakończyć to zadanie: "%s"?',
'Edit a task' => 'Edytuj zadanie',
'Column' => 'Kolumna',
'Color' => 'Kolor',
'Assignee' => 'Odpowiedzialny',
'Create another task' => 'Dodaj kolejne zadanie',
'New task' => 'Nowe zadanie',
'Open a task' => 'Otwórz zadanie',
'Do you really want to open this task: "%s"?' => 'Na pewno chcesz otworzyć zadanie: "%s"?',
'Back to the board' => 'Powrót do tablicy',
'Created on %B %e, %G at %k:%M %p' => 'Utworzono dnia %e %B %G o %k:%M',
'There is nobody assigned' => 'Nikt nie jest przypisany',
'Column on the board:' => 'Kolumna na tablicy:',
'Status is open' => 'Status otwarty',
'Status is closed' => 'Status zamknięty',
'Close this task' => 'Zamknij zadanie',
'Open this task' => 'Otwórz zadanie',
'There is no description.' => 'Brak opisu.',
'Add a new task' => 'Dodaj zadanie',
'The username is required' => 'Nazwa użytkownika jest wymagana',
'The maximum length is %d characters' => 'Maksymalna długość wynosi %d znaków',
'The minimum length is %d characters' => 'Minimalna długość wynosi %d znaków',
'The password is required' => 'Hasło jest wymagane',
'This value must be an integer' => 'Wartość musi być liczbą całkowitą',
'The username must be unique' => 'Nazwa użytkownika musi być unikalna',
'The username must be alphanumeric' => 'Nazwa użytkownika musi być alfanumeryczna',
'The user id is required' => 'ID użytkownika jest wymagane',
'Passwords don\'t match' => 'Hasła nie pasują do siebie',
'The confirmation is required' => 'Wymagane jest potwierdzenie',
'The column is required' => 'Kolumna jest wymagana',
'The project is required' => 'Projekt jest wymagany',
'The color is required' => 'Kolor jest wymagany',
'The id is required' => 'ID jest wymagane',
'The project id is required' => 'ID projektu jest wymagane',
'The project name is required' => 'Nazwa projektu jest wymagana',
'This project must be unique' => 'Projekt musi być unikalny',
'The title is required' => 'Tutył jest wymagany',
'The language is required' => 'Język jest wymagany',
'There is no active project, the first step is to create a new project.' => 'Brak aktywnych projektów. Pierwszym krokiem jest utworzenie nowego projektu.',
'Settings saved successfully.' => 'Ustawienia zapisane.',
'Unable to save your settings.' => 'Nie udało się zapisać ustawień.',
'Database optimization done.' => 'Optymalizacja bazy danych zakończona.',
'Your project have been created successfully.' => 'Projekt został pomyślnie utworzony.',
'Unable to create your project.' => 'Nie udało się stworzyć projektu.',
'Project updated successfully.' => 'Projekt zaktualizowany.',
'Unable to update this project.' => 'Nie można zaktualizować projektu.',
'Unable to remove this project.' => 'Nie można usunąć projektu.',
'Project removed successfully.' => 'Projekt usunięty.',
'Project activated successfully.' => 'Projekt aktywowany.',
'Unable to activate this project.' => 'Nie można aktywować projektu.',
'Project disabled successfully.' => 'Projekt wyłączony.',
'Unable to disable this project.' => 'Nie można wyłączyć projektu.',
'Unable to open this task.' => 'Nie można otworzyć tego zadania.',
'Task opened successfully.' => 'Zadanie otwarte.',
'Unable to close this task.' => 'Nie można zamknąć tego zadania.',
'Task closed successfully.' => 'Zadanie zamknięte.',
'Unable to update your task.' => 'Nie można zaktualizować tego zadania.',
'Task updated successfully.' => 'Zadanie zaktualizowane.',
'Unable to create your task.' => 'Nie można dodać zadania.',
'Task created successfully.' => 'Zadanie zostało utworzone.',
'User created successfully.' => 'Użytkownik dodany',
'Unable to create your user.' => 'Nie udało się dodać użytkownika.',
'User updated successfully.' => 'Użytkownik zaktualizowany.',
'Unable to update your user.' => 'Nie udało się zaktualizować użytkownika.',
'User removed successfully.' => 'Użytkownik usunięty.',
'Unable to remove this user.' => 'Nie udało się usunąć użytkownika.',
'Board updated successfully.' => 'Tablica została zaktualizowana.',
'Ready' => 'Gotowe',
'Backlog' => 'Log',
'Work in progress' => 'W trakcie',
'Done' => 'Zakończone',
'Application version:' => 'Wersja aplikacji:',
'Completed on %B %e, %G at %k:%M %p' => 'Zakończono dnia %e %B %G o %k:%M',
'%B %e, %G at %k:%M %p' => '%e %B %G o %k:%M',
'Date created' => 'Data utworzenia',
'Date completed' => 'Data zakończenia',
'Id' => 'Ident',
'No task' => 'Brak zadań',
'Completed tasks' => 'Ukończone zadania',
'List of projects' => 'Lista projektów',
'Completed tasks for "%s"' => 'Zadania zakończone dla "%s"',
'%d closed tasks' => '%d zamkniętych zadań',
'no task for this project' => 'brak zadań dla tego projektu',
'Public link' => 'Link publiczny',
'There is no column in your project!' => 'Brak kolumny w Twoim projekcie',
'Change assignee' => 'Zmień odpowiedzialną osobę',
'Change assignee for the task "%s"' => 'Zmień odpowiedzialną osobę dla zadania "%s"',
'Timezone' => 'Strefa czasowa',
'Actions' => 'Akcje',
'Confirmation' => 'Powtórzenie hasła',
'Description' => 'Opis',
'Sorry, I didn\'t found this information in my database!' => 'Niestety nie znaleziono tej informacji w bazie danych',
'Page not found' => 'Strona nie istnieje',
'Story Points' => 'Poziom trudności',
'limit' => 'limit',
'Task limit' => 'Limit zadań',
'This value must be greater than %d' => 'Wartość musi być większa niż %d',
'Edit project access list' => 'Edycja list dostępu dla projektu',
'Edit users access' => 'Edytuj dostęp',
'Allow this user' => 'Dodaj użytkownika',
'Project access list for "%s"' => 'Lista uprawnionych dla projektu "%s"',
'Only those users have access to this project:' => 'Użytkownicy mający dostęp:',
'Don\'t forget that administrators have access to everything.' => 'Pamiętaj: Administratorzy mają zawsze dostęp do wszystkiego!',
'revoke' => 'odbierz dostęp',
'List of authorized users' => 'Lista użytkowników mających dostęp',
'User' => 'Użytkownik',
'Everybody have access to this project.' => 'Każdy ma dostęp do tego projektu.',
'You are not allowed to access to this project.' => 'Nie masz dostępu do tego projektu.',
'Comments' => 'Komentarze',
'Post comment' => 'Dodaj komentarz',
'Write your text in Markdown' => 'Możesz użyć Markdown',
'Leave a comment' => 'Zostaw komentarz',
'Comment is required' => 'Komentarz jest wymagany',
'Comment added successfully.' => 'Komentarz dodany',
'Unable to create your comment.' => 'Nie udało się dodać komentarza',
'The description is required' => 'Opis jest wymagany',
'Edit this task' => 'Edytuj zadanie',
'Due Date' => 'Termin',
'm/d/Y' => 'd/m/Y', // Date format parsed with php
'month/day/year' => 'dzień/miesiąc/rok', // Help shown to the user
'Invalid date' => 'Błędna data',
'Must be done before %B %e, %G' => 'Termin do %e %B %G',
'%B %e, %G' => '%e %B %G',
'Automatic actions' => 'Akcje automatyczne',
'Your automatic action have been created successfully.' => 'Twoja akcja została dodana',
'Unable to create your automatic action.' => 'Nie udało się utworzyć akcji',
'Remove an action' => 'Usuń akcję',
'Unable to remove this action.' => 'Nie można usunąć akcji',
'Action removed successfully.' => 'Akcja usunięta',
'Automatic actions for the project "%s"' => 'Akcje automatyczne dla projektu "%s"',
'Defined actions' => 'Zdefiniowane akcje',
'Event name' => 'Nazwa zdarzenia',
'Action name' => 'Nazwa akcji',
'Action parameters' => 'Parametry akcji',
'Action' => 'Akcja',
'Event' => 'Zdarzenie',
'When the selected event occurs execute the corresponding action.' => 'Gdy następuje wybrane zdarzenie, uruchom odpowiednią akcję',
'Next step' => 'Następny krok',
'Define action parameters' => 'Zdefiniuj parametry akcji',
'Save this action' => 'Zapisz akcję',
'Do you really want to remove this action: "%s"?' => 'Na pewno chcesz usunąć akcję "%s"?',
'Remove an automatic action' => 'Usuń akcję automatyczną',
'Close the task' => 'Zamknij zadanie',
'Assign the task to a specific user' => 'Przypisz zadanie do wybranego użytkownika',
'Assign the task to the person who does the action' => 'Przypisz zadanie to osoby wykonującej akcję',
'Duplicate the task to another project' => 'Kopiuj zadanie do innego projektu',
'Move a task to another column' => 'Przeniesienie zadania do innej kolumny',
'Move a task to another position in the same column' => 'Zmiania pozycji zadania w kolumnie',
'Task modification' => 'Modyfikacja zadania',
'Task creation' => 'Tworzenie zadania',
'Open a closed task' => 'Otwarcie zamkniętego zadania',
'Closing a task' => 'Zamknięcie zadania',
'Assign a color to a specific user' => 'Przypisz kolor do wybranego użytkownika',
'Add an action' => 'Nowa akcja',
'Column title' => 'Tytuł kolumny',
'Position' => 'Pozycja',
'Move Up' => 'Przenieś wyżej',
'Move Down' => 'Przenieś niżej',
'Duplicate to another project' => 'Skopiuj do innego projektu',
'Duplicate' => 'Utwórz kopię',
'link' => 'link',
'Update this comment' => 'Zapisz komentarz',
'Comment updated successfully.' => 'Komentarz został zapisany.',
'Unable to update your comment.' => 'Nie udało się zapisanie komentarza.',
'Remove a comment' => 'Usuń komentarz',
'Comment removed successfully.' => 'Komentarz został usunięty.',
'Unable to remove this comment.' => 'Nie udało się usunąć komentarza.',
'Do you really want to remove this comment?' => 'Czy na pewno usunąć ten komentarz?',
'Only administrators or the creator of the comment can access to this page.' => 'Tylko administratorzy oraz autor komentarza ma dostęp do tej strony.',
'Details' => 'Szczegóły',
'Current password for the user "%s"' => 'Aktualne hasło dla użytkownika "%s"',
'The current password is required' => 'Wymanage jest aktualne hasło',
'Wrong password' => 'Błędne hasło',
'Reset all tokens' => 'Zresetuj wszystkie tokeny',
'All tokens have been regenerated.' => 'Wszystkie tokeny zostały zresetowane.',
'Unknown' => 'Nieznany',
'Last logins' => 'Ostatnie logowania',
'Login date' => 'Data logowania',
'Authentication method' => 'Sposób autentykacji',
'IP address' => 'Adres IP',
'User agent' => 'Przeglądarka',
'Persistent connections' => 'Stałe połączenia',
'No session' => 'Brak sesji',
'Expiration date' => 'Data zakończenia',
'Remember Me' => 'Pamiętaj mnie',
'Creation date' => 'Data utworzenia',
// 'Filter by user' => '',
// 'Filter by due date' => ',
// 'Everybody' => '',
// 'Open' => '',
// 'Closed' => '',
// 'Search' => '',
// 'Nothing found.' => '',
// 'Search in the project "%s"' => '',
// 'Due date' => '',
// 'Others formats accepted: %s and %s' => '',
// 'Description' => '',
// '%d comments' => '',
// '%d comment' => '',
// 'Email address invalid' => '',
// 'Your Google Account is not linked anymore to your profile.' => '',
// 'Unable to unlink your Google Account.' => '',
// 'Google authentication failed' => '',
// 'Unable to link your Google Account.' => '',
// 'Your Google Account is linked to your profile successfully.' => '',
// 'Email' => '',
// 'Link my Google Account' => '',
// 'Unlink my Google Account' => '',
// 'Login with my Google Account' => '',
// 'Project not found.' => '',
// 'Task #%d' => '',
// 'Task removed successfully.' => '',
// 'Unable to remove this task.' => '',
// 'Remove a task' => '',
// 'Do you really want to remove this task: "%s"?' => '',
// 'Assign automatically a color based on a category' => '',
// 'Assign automatically a category based on a color' => '',
// 'Task creation or modification' => '',
// 'Category' => '',
// 'Category:' => '',
// 'Categories' => '',
// 'Category not found.' => '',
// 'Your category have been created successfully.' => '',
// 'Unable to create your category.' => '',
// 'Your category have been updated successfully.' => '',
// 'Unable to update your category.' => '',
// 'Remove a category' => '',
// 'Category removed successfully.' => '',
// 'Unable to remove this category.' => '',
// 'Category modification for the project "%s"' => '',
// 'Category Name' => '',
// 'Categories for the project "%s"' => '',
// 'Add a new category' => '',
// 'Do you really want to remove this category: "%s"?' => '',
// 'Filter by category' => '',
// 'All categories' => '',
// 'No category' => '',
// 'The name is required' => '',
// 'Remove a file' => '',
// 'Unable to remove this file.' => '',
// 'File removed successfully.' => '',
// 'Attach a document' => '',
// 'Do you really want to remove this file: "%s"?' => '',
// 'open' => '',
// 'Attachments' => '',
// 'Edit the task' => '',
// 'Edit the description' => '',
// 'Add a comment' => '',
// 'Edit a comment' => '',
// 'Summary' => '',
// 'Time tracking' => '',
// 'Estimate:' => '',
// 'Spent:' => '',
// 'Do you really want to remove this sub-task?' => '',
// 'Remaining:' => '',
// 'hours' => '',
// 'spent' => '',
// 'estimated' => '',
// 'Sub-Tasks' => '',
// 'Add a sub-task' => '',
// 'Original Estimate' => '',
// 'Create another sub-task' => '',
// 'Time Spent' => '',
// 'Edit a sub-task' => '',
// 'Remove a sub-task' => '',
// 'The time must be a numeric value' => '',
// 'Todo' => '',
// 'In progress' => '',
// 'Done' => '',
// 'Sub-task removed successfully.' => '',
// 'Unable to remove this sub-task.' => '',
// 'Sub-task updated successfully.' => '',
// 'Unable to update your sub-task.' => '',
// 'Unable to create your sub-task.' => '',
// 'Sub-task added successfully.' => '',
// 'Maximum size: ' => '',
// 'Unable to upload the file.' => '',
// 'Display another project' => '',
// 'Your GitHub account was successfully linked to your profile.' => '',
// 'Unable to link your GitHub Account.' => '',
// 'GitHub authentication failed' => '',
// 'Your GitHub account is no longer linked to your profile.' => '',
// 'Unable to unlink your GitHub Account.' => '',
// 'Login with my GitHub Account' => '',
// 'Link my GitHub Account' => '',
// 'Unlink my GitHub Account' => '',
// 'Created by %s' => 'Créé par %s',
// 'Last modified on %B %e, %G at %k:%M %p' => '',
);

View file

@ -0,0 +1,387 @@
<?php
return array(
'English' => 'Inglês',
'French' => 'Francês',
'Polish' => 'Polonês',
'Portuguese (Brazilian)' => 'Português (Brasil)',
'Spanish' => 'Espanhol',
// 'German' => '',
// 'Chinese (Simplified)' => '',
'None' => 'Nenhum',
'edit' => 'editar',
'Edit' => 'Editar',
'remove' => 'apagar',
'Remove' => 'Apagar',
'Update' => 'Atualizar',
'Yes' => 'Sim',
'No' => 'Não',
'cancel' => 'cancelar',
'or' => 'ou',
'Yellow' => 'Amarelo',
'Blue' => 'Azul',
'Green' => 'Verde',
'Purple' => 'Violeta',
'Red' => 'Vermelho',
'Orange' => 'Laranja',
'Grey' => 'Cinza',
'Save' => 'Salvar',
'Login' => 'Login',
'Official website:' => 'Site web oficial :',
'Unassigned' => 'Não Atribuída',
'View this task' => 'Ver esta tarefa',
'Remove user' => 'Remover usuário',
'Do you really want to remove this user: "%s"?' => 'Quer realmente remover este usuário: "%s"?',
'New user' => 'Novo usuário',
'All users' => 'Todos os usuários',
'Username' => 'Nome do usuário',
'Password' => 'Senha',
'Default Project' => 'Projeto default',
'Administrator' => 'Administrador',
'Sign in' => 'Logar',
'Users' => 'Usuários',
'No user' => 'Sem usuário',
'Forbidden' => 'Proibido',
'Access Forbidden' => 'Acesso negado',
'Only administrators can access to this page.' => 'Somente administradores têm acesso a esta página.',
'Edit user' => 'Editar usuário',
'Logout' => 'Logout',
'Bad username or password' => 'Usuário ou senha inválidos',
'users' => 'usuários',
'projects' => 'projetos',
'Edit project' => 'Editar projeto',
'Name' => 'Nome',
'Activated' => 'Ativo',
'Projects' => 'Projetos',
'No project' => 'Nenhum projeto',
'Project' => 'Projeto',
'Status' => 'Status',
'Tasks' => 'Tarefas',
'Board' => 'Quadro',
'Inactive' => 'Inativo',
'Active' => 'Ativo',
'Column %d' => 'Coluna %d',
'Add this column' => 'Adicionar esta coluna',
'%d tasks on the board' => '%d tarefas no quadro',
'%d tasks in total' => '%d tarefas no total',
'Unable to update this board.' => 'Impossível atualizar este quadro.',
'Edit board' => 'Modificar quadro',
'Disable' => 'Desativar',
'Enable' => 'Ativar',
'New project' => 'Novo projeto',
'Do you really want to remove this project: "%s"?' => 'Quer realmente remover este projeto: "%s" ?',
'Remove project' => 'Remover projeto',
'Boards' => 'Quadros',
'Edit the board for "%s"' => 'Editar o quadro para "%s"',
'All projects' => 'Todos os projetos',
'Change columns' => 'Modificar colunas',
'Add a new column' => 'Adicionar uma nova coluna',
'Title' => 'Título',
'Add Column' => 'Adicionar coluna',
'Project "%s"' => 'Projeto "%s"',
'Nobody assigned' => 'Ninguém designado',
'Assigned to %s' => 'Designado para %s',
'Remove a column' => 'Remover uma coluna',
'Remove a column from a board' => 'Remover uma coluna do quadro',
'Unable to remove this column.' => 'Impossível remover esta coluna.',
'Do you really want to remove this column: "%s"?' => 'Quer realmente remover esta coluna: "%s"?',
'This action will REMOVE ALL TASKS associated to this column!' => 'Esta ação vai REMOVER TODAS AS TAREFAS associadas a esta coluna!',
'Settings' => 'Preferências',
'Application settings' => 'Preferências da aplicação',
'Language' => 'Idioma',
'Webhooks token:' => 'Token de webhooks:',
'More information' => 'Mais informação',
'Database size:' => 'Tamanho do banco de dados:',
'Download the database' => 'Download do banco de dados',
'Optimize the database' => 'Otimizar o banco de dados',
'(VACUUM command)' => '(Comando VACUUM)',
'(Gzip compressed Sqlite file)' => '(Arquivo Sqlite comprimido com Gzip)',
'User settings' => 'Configurações do usuário',
'My default project:' => 'Meu projeto default:',
'Close a task' => 'Encerrar uma tarefa',
'Do you really want to close this task: "%s"?' => 'Quer realmente encerrar esta tarefa: "%s"?',
'Edit a task' => 'Editar uma tarefa',
'Column' => 'Coluna',
'Color' => 'Cor',
'Assignee' => 'Designação',
'Create another task' => 'Criar uma outra tarefa (aproveitando os dados preenchidos)',
'New task' => 'Nova tarefa',
'Open a task' => 'Abrir uma tarefa',
'Do you really want to open this task: "%s"?' => 'Quer realmente abrir esta tarefa: "%s"?',
'Back to the board' => 'Voltar ao quadro',
'Created on %B %e, %G at %k:%M %p' => 'Criado em %d %B %G às %H:%M',
'There is nobody assigned' => 'Não há ninguém designado',
'Column on the board:' => 'Coluna no quadro:',
'Status is open' => 'Status está aberto',
'Status is closed' => 'Status está fechado',
'Close this task' => 'Fechar esta tarefa',
'Open this task' => 'Abrir esta tarefa',
'There is no description.' => 'Não há descrição.',
'Add a new task' => 'Adicionar uma nova tarefa',
'The username is required' => 'O nome de usuário é obrigatório',
'The maximum length is %d characters' => 'O tamanho máximo são %d caracteres',
'The minimum length is %d characters' => 'O tamanho mínimo são %d caracteres',
'The password is required' => 'A senha é obrigatória',
'This value must be an integer' => 'O valor deve ser um inteiro',
'The username must be unique' => 'O nome de usuário deve ser único',
'The username must be alphanumeric' => 'O nome de usuário deve ser alfanumérico, sem espaços ou _',
'The user id is required' => 'O id de usuário é obrigatório',
'Passwords don\'t match' => 'As senhas não conferem',
'The confirmation is required' => 'A confirmação é obrigatória',
'The column is required' => 'A coluna é obrigatória',
'The project is required' => 'O projeto é obrigatório',
'The color is required' => 'A cor é obrigatória',
'The id is required' => 'O id é obrigatório',
'The project id is required' => 'O id do projeto é obrigatório',
'The project name is required' => 'O nome do projeto é obrigatório',
'This project must be unique' => 'Este projeto deve ser único',
'The title is required' => 'O título é obrigatório',
'The language is required' => 'O idioma é obrigatório',
'There is no active project, the first step is to create a new project.' => 'Não há projeto ativo. O primeiro passo é criar um novo projeto.',
'Settings saved successfully.' => 'Configurações salvas com sucesso.',
'Unable to save your settings.' => 'Impossível salvar suas configurações.',
'Database optimization done.' => 'Otimização do banco de dados terminada.',
'Your project have been created successfully.' => 'Seu projeto foi criado com sucesso.',
'Unable to create your project.' => 'Impossível criar seu projeto.',
'Project updated successfully.' => 'Projeto atualizado com sucesso.',
'Unable to update this project.' => 'Impossível atualizar este projeto.',
'Unable to remove this project.' => 'Impossível remover este projeto.',
'Project removed successfully.' => 'Projeto removido com sucesso.',
'Project activated successfully.' => 'Projeto ativado com sucesso.',
'Unable to activate this project.' => 'Impossível ativar este projeto.',
'Project disabled successfully.' => 'Projeto desabilitado com sucesso.',
'Unable to disable this project.' => 'Impossível desabilitar este projeto.',
'Unable to open this task.' => 'Impossível abrir esta tarefa.',
'Task opened successfully.' => 'Tarefa aberta com sucesso.',
'Unable to close this task.' => 'Impossível encerrar esta tarefa.',
'Task closed successfully.' => 'Tarefa encerrada com sucesso.',
'Unable to update your task.' => 'Impossível atualizar sua tarefa.',
'Task updated successfully.' => 'Tarefa atualizada com sucesso.',
'Unable to create your task.' => 'Impossível criar sua tarefa.',
'Task created successfully.' => 'Tarefa criada com sucesso.',
'User created successfully.' => 'Usuário criado com sucesso.',
'Unable to create your user.' => 'Impossível criar seu usuário.',
'User updated successfully.' => 'Usuário atualizado com sucesso.',
'Unable to update your user.' => 'Impossível atualizar seu usuário.',
'User removed successfully.' => 'Usuário removido com sucesso.',
'Unable to remove this user.' => 'Impossível remover este usuário.',
'Board updated successfully.' => 'Quadro atualizado com sucesso.',
'Ready' => 'Pronto',
'Backlog' => 'Backlog',
'Work in progress' => 'Em andamento',
'Done' => 'Encerrado',
'Application version:' => 'Versão da aplicação:',
'Completed on %B %e, %G at %k:%M %p' => 'Encerrado em %d %B %G às %H:%M',
'%B %e, %G at %k:%M %p' => '%d %B %G às %H:%M',
'Date created' => 'Data de criação',
'Date completed' => 'Data de encerramento',
'Id' => 'Id',
'No task' => 'Nenhuma tarefa',
'Completed tasks' => 'tarefas completadas',
'List of projects' => 'Lista de projetos',
'Completed tasks for "%s"' => 'Tarefas completadas por "%s"',
'%d closed tasks' => '%d tarefas encerradas',
'no task for this project' => 'nenhuma tarefa para este projeto',
'Public link' => 'Link público',
'There is no column in your project!' => 'Não há colunas no seu projeto!',
'Change assignee' => 'Mudar a designação',
'Change assignee for the task "%s"' => 'Modificar designação para a tarefa "%s"',
'Timezone' => 'Fuso horário',
'Sorry, I didn\'t found this information in my database!' => 'Desculpe, não encontrei esta informação no meu banco de dados!',
'Page not found' => 'Página não encontrada',
'Story Points' => 'Complexidade',
'limit' => 'limite',
'Task limit' => 'Limite da tarefa',
'This value must be greater than %d' => 'Este valor deve ser maior que %d',
'Edit project access list' => 'Editar lista de acesso ao projeto', // new translations to brazilian portuguese starts here
'Edit users access' => 'Editar acesso de usuários',
'Allow this user' => 'Permitir esse usuário',
'Project access list for "%s"' => 'Lista de acesso ao projeto para "%s"',
'Only those users have access to this project:' => 'Somente estes usuários têm acesso a este projeto:',
'Don\'t forget that administrators have access to everything.' => 'Não esqueça que administradores têm acesso a tudo.',
'revoke' => 'revogar',
'List of authorized users' => 'Lista de usuários autorizados',
'User' => 'Usuário',
'Everybody have access to this project.' => 'Todos têm acesso a este projeto.',
'You are not allowed to access to this project.' => 'Você não está autorizado a acessar este projeto.',
'Comments' => 'Comentários',
'Post comment' => 'Postar comentário',
'Write your text in Markdown' => 'Escreva seu texto em Markdown',
'Leave a comment' => 'Deixe um comentário',
'Comment is required' => 'Comentário é obrigatório',
'Leave a description' => 'Deixe uma descrição',
'Comment added successfully.' => 'Cpmentário adicionado com sucesso.',
'Unable to create your comment.' => 'Impossível criar seu comentário.',
'The description is required' => 'A descrição é obrigatória',
'Edit this task' => 'Editar esta tarefa',
'Due Date' => 'Data de vencimento',
'm/d/Y' => 'd/m/Y', // Date format parsed with php
'month/day/year' => 'dia/mês/ano', // Help shown to the user
'Invalid date' => 'Data inválida',
'Must be done before %B %e, %G' => 'Deve ser feito antes de %d %B %G',
'%B %e, %G' => '%d %B %G',
'Automatic actions' => 'Ações automáticas',
'Your automatic action have been created successfully.' => 'Sua ação automética foi criada com sucesso.',
'Unable to create your automatic action.' => 'Impossível criar sua ação automática.',
'Remove an action' => 'Remover uma ação',
'Unable to remove this action.' => 'Impossível remover esta ação',
'Action removed successfully.' => 'Ação removida com sucesso.',
'Automatic actions for the project "%s"' => 'Ações automáticas para o projeto "%s"',
'Defined actions' => 'Ações definidas',
'Event name' => 'Nome do evento',
'Action name' => 'Nome da ação',
'Action parameters' => 'Parâmetros da ação',
'Action' => 'Ação',
'Event' => 'Evento',
'When the selected event occurs execute the corresponding action.' => 'Quando o evento selecionado ocorrer, execute a ação correspondente',
'Next step' => 'Próximo passo',
'Define action parameters' => 'Definir parêmetros da ação',
'Save this action' => 'Salvar esta ação',
'Do you really want to remove this action: "%s"?' => 'Você quer realmente remover esta ação: "%s"?',
'Remove an automatic action' => 'Remove uma ação automática',
'Close the task' => 'Fechar tarefa',
'Assign the task to a specific user' => 'Designar a tarefa para um usuário específico',
'Assign the task to the person who does the action' => 'Designar a tarefa para a pessoa que executa a ação',
'Duplicate the task to another project' => 'Duplicar a tarefa para um outro projeto',
'Move a task to another column' => 'Mover a tarefa para outra coluna',
'Move a task to another position in the same column' => 'Mover a tarefa para outra posição, na mesma coluna',
'Task modification' => 'Modificação de tarefa',
'Task creation' => 'Criação de tarefa',
'Open a closed task' => 'Reabrir uma tarefa fechada',
'Closing a task' => 'Fechando uma tarefa',
'Assign a color to a specific user' => 'Designar uma cor para um usuário específico',
// 'Column title' => '',
// 'Position' => '',
// 'Move Up' => '',
// 'Move Down' => '',
// 'Duplicate to another project' => '',
// 'Duplicate' => '',
// 'link' => '',
// 'Update this comment' => '',
// 'Comment updated successfully.' => '',
// 'Unable to update your comment.' => '',
// 'Remove a comment' => '',
// 'Comment removed successfully.' => '',
// 'Unable to remove this comment.' => '',
// 'Do you really want to remove this comment?' => '',
// 'Only administrators or the creator of the comment can access to this page.' => '',
// 'Details' => '',
// 'Current password for the user "%s"' => '',
// 'The current password is required' => '',
// 'Wrong password' => '',
// 'Reset all tokens' => '',
// 'All tokens have been regenerated.' => '',
// 'Unknown' => '',
// 'Last logins' => '',
// 'Login date' => '',
// 'Authentication method' => '',
// 'IP address' => '',
// 'User agent' => '',
// 'Persistent connections' => '',
// 'No session' => '',
// 'Expiration date' => '',
// 'Remember Me' => '',
// 'Creation date' => '',
// 'Filter by user' => '',
// 'Filter by due date' => ',
// 'Everybody' => '',
// 'Open' => '',
// 'Closed' => '',
// 'Search' => '',
// 'Nothing found.' => '',
// 'Search in the project "%s"' => '',
// 'Due date' => '',
// 'Others formats accepted: %s and %s' => '',
// 'Description' => '',
// '%d comments' => '',
// '%d comment' => '',
// 'Email address invalid' => '',
// 'Your Google Account is not linked anymore to your profile.' => '',
// 'Unable to unlink your Google Account.' => '',
// 'Google authentication failed' => '',
// 'Unable to link your Google Account.' => '',
// 'Your Google Account is linked to your profile successfully.' => '',
// 'Email' => '',
// 'Link my Google Account' => '',
// 'Unlink my Google Account' => '',
// 'Login with my Google Account' => '',
// 'Project not found.' => '',
// 'Task #%d' => '',
// 'Task removed successfully.' => '',
// 'Unable to remove this task.' => '',
// 'Remove a task' => '',
// 'Do you really want to remove this task: "%s"?' => '',
// 'Assign automatically a color based on a category' => '',
// 'Assign automatically a category based on a color' => '',
// 'Task creation or modification' => '',
// 'Category' => '',
// 'Category:' => '',
// 'Categories' => '',
// 'Category not found.' => '',
// 'Your category have been created successfully.' => '',
// 'Unable to create your category.' => '',
// 'Your category have been updated successfully.' => '',
// 'Unable to update your category.' => '',
// 'Remove a category' => '',
// 'Category removed successfully.' => '',
// 'Unable to remove this category.' => '',
// 'Category modification for the project "%s"' => '',
// 'Category Name' => '',
// 'Categories for the project "%s"' => '',
// 'Add a new category' => '',
// 'Do you really want to remove this category: "%s"?' => '',
// 'Filter by category' => '',
// 'All categories' => '',
// 'No category' => '',
// 'The name is required' => '',
// 'Remove a file' => '',
// 'Unable to remove this file.' => '',
// 'File removed successfully.' => '',
// 'Attach a document' => '',
// 'Do you really want to remove this file: "%s"?' => '',
// 'open' => '',
// 'Attachments' => '',
// 'Edit the task' => '',
// 'Edit the description' => '',
// 'Add a comment' => '',
// 'Edit a comment' => '',
// 'Summary' => '',
// 'Time tracking' => '',
// 'Estimate:' => '',
// 'Spent:' => '',
// 'Do you really want to remove this sub-task?' => '',
// 'Remaining:' => '',
// 'hours' => '',
// 'spent' => '',
// 'estimated' => '',
// 'Sub-Tasks' => '',
// 'Add a sub-task' => '',
// 'Original Estimate' => '',
// 'Create another sub-task' => '',
// 'Time Spent' => '',
// 'Edit a sub-task' => '',
// 'Remove a sub-task' => '',
// 'The time must be a numeric value' => '',
// 'Todo' => '',
// 'In progress' => '',
// 'Done' => '',
// 'Sub-task removed successfully.' => '',
// 'Unable to remove this sub-task.' => '',
// 'Sub-task updated successfully.' => '',
// 'Unable to update your sub-task.' => '',
// 'Unable to create your sub-task.' => '',
// 'Sub-task added successfully.' => '',
// 'Maximum size: ' => '',
// 'Unable to upload the file.' => '',
// 'Display another project' => '',
// 'Your GitHub account was successfully linked to your profile.' => '',
// 'Unable to link your GitHub Account.' => '',
// 'GitHub authentication failed' => '',
// 'Your GitHub account is no longer linked to your profile.' => '',
// 'Unable to unlink your GitHub Account.' => '',
// 'Login with my GitHub Account' => '',
// 'Link my GitHub Account' => '',
// 'Unlink my GitHub Account' => '',
// 'Created by %s' => 'Créé par %s',
// 'Last modified on %B %e, %G at %k:%M %p' => '',
);

View file

@ -0,0 +1,389 @@
<?php
return array(
'English' => 'Engelska',
'French' => 'Franska',
'Polish' => 'Polska',
'Portuguese (Brazilian)' => 'Portugisiska (Brasilien)',
'Spanish' => 'Spanska',
'German' => 'Tyska',
'Swedish' => 'Svenska',
'None' => 'Ingen',
'edit' => 'redigera',
'Edit' => 'Redigera',
'remove' => 'ta bort',
'Remove' => 'Ta bort',
'Update' => 'Uppdatera',
'Yes' => 'Ja',
'No' => 'Nej',
'cancel' => 'avbryt',
'or' => 'eller',
'Yellow' => 'Gul',
'Blue' => 'Blå',
'Green' => 'Grön',
'Purple' => 'Lila',
'Red' => 'Röd',
'Orange' => 'Orange',
'Grey' => 'Grå',
'Save' => 'Spara',
'Login' => 'Login',
'Official website:' => 'Officiell webbsida:',
'Unassigned' => 'Ej tilldelad',
'View this task' => 'Se denna uppgift',
'Remove user' => 'Ta bort användare',
'Do you really want to remove this user: "%s"?' => 'Vill du verkligen ta bort användaren: "%s"?',
'New user' => 'Ny användare',
'All users' => 'Alla användare',
'Username' => 'Användarnamn',
'Password' => 'Lösenord',
'Default Project' => 'Standardprojekt',
'Administrator' => 'Administratör',
'Sign in' => 'Logga in',
'Users' => 'Användare',
'No user' => 'Ingen användare',
'Forbidden' => 'Ej tillåten',
'Access Forbidden' => 'Ej tillåten',
'Only administrators can access to this page.' => 'Bara adminstratörer har tillgång till denna sida.',
'Edit user' => 'Ändra användare',
'Logout' => 'Logga ut',
'Bad username or password' => 'Fel användarnamn eller lösenord',
'users' => 'Användare',
'projects' => 'projekt',
'Edit project' => 'Ändra projekt',
'Name' => 'Namn',
'Activated' => 'Aktiverad',
'Projects' => 'Projekt',
'No project' => 'Inget projekt',
'Project' => 'Tavlor för projekt och planering',
'Status' => 'Status',
'Tasks' => 'Uppgifter',
'Board' => 'Tavla',
'Actions' => 'Åtgärder',
'Inactive' => 'Inaktiv',
'Active' => 'Aktiv',
'Column %d' => 'Kolumn %d',
'Add this column' => 'Lägg till kolumnen',
'%d tasks on the board' => '%d uppgifter på tavlan',
'%d tasks in total' => '%d uppgifter totalt',
'Unable to update this board.' => 'Kunde inte uppdatera tavlan',
'Edit board' => 'Ändra tavlan',
'Disable' => 'Inaktivera',
'Enable' => 'Aktivera',
'New project' => 'Nytt projekt',
'Do you really want to remove this project: "%s"?' => 'Vill du verkligen ta bort projektet: "%s" ?',
'Remove project' => 'Ta bort projekt',
'Boards' => 'Tavlor',
'Edit the board for "%s"' => 'Ändra tavlan för "%s"',
'All projects' => 'Alla projekt',
'Change columns' => 'Ändra kolumner',
'Add a new column' => 'Lägg till ny kolumn',
'Title' => 'Titel',
'Add Column' => 'Lägg till kolumn',
'Project "%s"' => 'Tavlan "%s" är aktiv',
'Nobody assigned' => 'Ingen tilldelad',
'Assigned to %s' => 'Tilldelad %s',
'Remove a column' => 'Ta bort en kolumn',
'Remove a column from a board' => 'Ta bort en kolumn från tavlan',
'Unable to remove this column.' => 'Kunde inte ta bort kolumnen.',
'Do you really want to remove this column: "%s"?' => 'Vill du verkligen ta bort kolumnen: "%s"?',
'This action will REMOVE ALL TASKS associated to this column!' => 'Denna åtgärd kommer att TA BORT ALLA uppgifter kopplade till kolumnen!',
'Settings' => 'Inställningar',
'Application settings' => 'Applikationsinställningar',
'Language' => 'Språk',
'Webhooks token:' => 'Token för webhooks:',
'More information' => 'Mer information',
'Database size:' => 'Databasstorlek:',
'Download the database' => 'Ladda ner databasen',
'Optimize the database' => 'Optimera databasen',
'(VACUUM command)' => '(Vacuum kommando)',
'(Gzip compressed Sqlite file)' => '(Gzip komprimera Sqlite filen)',
'User settings' => 'Användarinställningar',
'My default project:' => 'Mitt standardprojekt:',
'Close a task' => 'Stäng en uppgift',
'Do you really want to close this task: "%s"?' => 'Vill du verkligen stänga uppgiften: "%s"?',
'Edit a task' => 'Ändra en uppgift',
'Column' => 'Kolumn',
'Color' => 'Färg',
'Assignee' => 'Uppdragsinnehavare',
'Create another task' => 'Skapa ännu en uppgift',
'New task' => 'Ny uppgift',
'Open a task' => 'Öppna en uppgift',
'Do you really want to open this task: "%s"?' => 'Vill du verkligen öppna denna uppgift: "%s"?',
'Back to the board' => 'Tillbaka till tavlan',
'Created on %B %e, %G at %k:%M %p' => 'Skapad %d %B %G kl %H:%M',
'There is nobody assigned' => 'Det finns ingen tilldelad',
'Column on the board:' => 'Kolumn på tavlan:',
'Status is open' => 'Statusen är öppen',
'Status is closed' => 'Statusen är stängd',
'Close this task' => 'Stäng uppgiften',
'Open this task' => 'Öppna uppgiften',
'There is no description.' => 'Det finns ingen beskrivning.',
'Add a new task' => 'Lägg till en ny uppgift',
'The username is required' => 'Användarnamnet måste anges',
'The maximum length is %d characters' => 'Max antal bokstäver %d',
'The minimum length is %d characters' => 'Minst antal bokstäver %d',
'The password is required' => 'Lösenordet måste anges.',
'This value must be an integer' => 'Detta värde måste vara ett heltal.',
'The username must be unique' => 'Användarnamnet måste vara unikt',
'The username must be alphanumeric' => 'Användarnamnet måste vara alfanumeriskt',
'The user id is required' => 'Användar-ID måste anges',
'Passwords doesn\'t matches' => 'Fel lösenord',
'The confirmation is required' => 'Bekräftelse behövs.',
'The column is required' => 'Kolumnen måste anges',
'The project is required' => 'Projektet måste anges',
'The color is required' => 'Färgen måste anges',
'The id is required' => 'Aktuellt ID måste anges',
'The project id is required' => 'Projekt-ID måste anges',
'The project name is required' => 'Ett projektnamn måste anges',
'This project must be unique' => 'Detta projekt måste vara unikt',
'The title is required' => 'En titel måste anges.',
'The language is required' => 'Språket måste anges',
'There is no active project, the first step is to create a new project.' => 'Inget projekt är aktiverat, första steget är att skapa ett nytt projekt',
'Settings saved successfully.' => 'Inställningarna har sparats.',
'Unable to save your settings.' => 'Kunde inte spara dina ändringar',
'Database optimization done.' => 'Databasen har optimerats.',
'Your project have been created successfully.' => 'Ditt projekt har skapats.',
'Unable to create your project.' => 'Kunde inte skapa ditt projekt.',
'Project updated successfully.' => 'Projektet har uppdaterats.',
'Unable to update this project.' => 'Kunde inte uppdatera detta projekt.',
'Unable to remove this project.' => 'Kunde inte ta bort detta projekt.',
'Project removed successfully.' => 'Projektet har tagits bort.',
'Project activated successfully.' => 'Projektet har aktiverats.',
'Unable to activate this project.' => 'Kunde inte aktivera detta projekt.',
'Project disabled successfully.' => 'Projektet har stängts.',
'Unable to disable this project.' => 'Kunde inte stänga detta projekt.',
'Unable to open this task.' => 'Kunde inte öppna denna uppgift.',
'Task opened successfully.' => 'Uppgiften har öppnats.',
'Unable to close this task.' => 'Kunde inte stänga denna uppgift.',
'Task closed successfully.' => 'Uppgiften har stängts.',
'Unable to update your task.' => 'Kunde inte uppdatera din uppgift.',
'Task updated successfully.' => 'Uppgiften har uppdaterats.',
'Unable to create your task.' => 'Kunde inte skapa din uppgift.',
'Task created successfully.' => 'Uppgiften har skapats.',
'User created successfully.' => 'Användaren har skapats.',
'Unable to create your user.' => 'Kunde inte skapa din användare.',
'User updated successfully.' => 'Användaren har updaterats.',
'Unable to update your user.' => 'Kunde inte uppdatera din användare.',
'User removed successfully.' => 'Användaren har tagits bort.',
'Unable to remove this user.' => 'Kunde inte ta bort denna användare.',
'Board updated successfully.' => 'Tavlan uppdaterad.',
'Ready' => 'Denna månad',
'Backlog' => 'Att göra',
'Work in progress' => 'Pågående',
'Done' => 'Klart',
'Application version:' => 'Version:',
'Completed on %B %e, %G at %k:%M %p' => 'Slutfört %d %B %G kl %H:%M',
'%B %e, %G at %k:%M %p' => '%d %B %G kl %H:%M',
'Date created' => 'Skapat datum',
'Date completed' => 'Slutfört datum',
'Id' => 'ID',
'No task' => 'Ingen uppgift',
'Completed tasks' => 'Slutförda uppgifter',
'List of projects' => 'Lista med projekt',
'Completed tasks for "%s"' => 'Slutföra uppgifter för "%s"',
'%d closed tasks' => '%d stängda uppgifter',
'no task for this project' => 'inga uppgifter i detta projekt',
'Public link' => 'Publik länk',
'There is no column in your project!' => 'Det saknas kolumner i ditt projekt!',
'Change assignee' => 'Ändra uppdragsinnehavare',
'Change assignee for the task "%s"' => 'Ändra uppdragsinnehavare för uppgiften "%s"',
'Timezone' => 'Tidszon',
'Sorry, I didn\'t found this information in my database!' => 'Informationen kunde inte hittas i databasen.',
'Page not found' => 'Sidan hittas inte',
'Story Points' => 'Ungefärligt antal timmar',
'limit' => 'max',
'Task limit' => 'Uppgiftsbegränsning',
'This value must be greater than %d' => 'Värdet måste vara större än %d',
'Edit project access list' => 'Ändra projektåtkomst lista',
'Edit users access' => 'Användaråtkomst',
'Allow this user' => 'Tillåt användare',
'Project access list for "%s"' => 'Behörighetslista för "%s"',
'Only those users have access to this project:' => 'Bara de användarna har tillgång till detta projekt.',
'Don\'t forget that administrators have access to everything.' => 'Glöm inte att administratörerna har rätt att göra allt.',
'revoke' => 'Dra tillbaka behörighet',
'List of authorized users' => 'Lista med behöriga användare',
'User' => 'Användare',
'Everybody have access to this project.' => 'Alla har tillgång till detta projekt.',
'You are not allowed to access to this project.' => 'Du har inte tillgång till detta projekt.',
'%B %e, %G at %k:%M %p' => '%d %B %G kl %H:%M',
'Comments' => 'Kommentarer',
'Post comment' => 'Ladda upp kommentar',
'Write your text in Markdown' => 'Exempelsyntax för text',
'Leave a comment' => 'Lämna en kommentar',
'Comment is required' => 'En kommentar måste lämnas',
'Leave a description' => 'Lämna en beskrivning',
'Comment added successfully.' => 'Kommentaren har lagts till.',
'Unable to create your comment.' => 'Kommentaren kunde inte laddas upp.',
'The description is required' => 'En beskrivning måste lämnas',
'Edit this task' => 'Ändra denna uppgift',
'Due Date' => 'Måldatum',
'm/d/Y' => 'd/m/Y', // Date format parsed with php
'month/day/year' => 'dag/månad/år', // Help shown to the user
'Invalid date' => 'Ej tillåtet datum',
'Must be done before %B %e, %G' => 'Måste vara klart innan %B %e, %G',
'%B %e, %G' => '%d %B %G',
'Automatic actions' => 'Automatiska åtgärder',
'Your automatic action have been created successfully.' => 'Din automatiska åtgärd har skapats.',
'Unable to create your automatic action.' => 'Kunde inte skapa din automatiska åtgärd.',
'Remove an action' => 'Ta bort en åtgärd',
'Unable to remove this action.' => 'Kunde inte ta bort denna åtgärd.',
'Action removed successfully.' => 'Åtgärden har tagits bort.',
'Automatic actions for the project "%s"' => 'Automatiska åtgärder för projektet "%s"',
'Defined actions' => 'Definierade åtgärder',
'Event name' => 'Händelsenamn',
'Action name' => 'Åtgärdsnamn',
'Action parameters' => 'Åtgärdsparametrar',
'Action' => 'Åtgärd',
'Event' => 'Händelse',
'When the selected event occurs execute the corresponding action.' => 'När händelsen inträffar, kör inställd åtgärd.',
'Next step' => 'Nästa steg',
'Define action parameters' => 'Definiera upp händelseparametrar',
'Save this action' => 'Spara denna åtgärd',
'Do you really want to remove this action: "%s"?' => 'Vill du verkligen ta bort denna åtgärd: "%s"?',
'Remove an automatic action' => 'Ta bort en automatiskt åtgärd',
'Close the task' => 'Stäng uppgiften',
'Assign the task to a specific user' => 'Tilldela uppgiften till en specifik användare',
'Assign the task to the person who does the action' => 'Tilldela uppgiften till personen som skapar den',
'Duplicate the task to another project' => 'Kopiera uppgiften till ett annat projekt',
'Move a task to another column' => 'Flytta en uppgift till en annan kolumn',
'Move a task to another position in the same column' => 'Flytta en uppgift till ett nytt läge i samma kolumn',
'Task modification' => 'Ändra uppgift',
'Task creation' => 'Skapa uppgift',
'Open a closed task' => 'Öppna en stängd uppgift',
'Closing a task' => 'Stänger en uppgift',
'Assign a color to a specific user' => 'Tilldela en färg till en specifik användare',
'Column title' => 'Kolumnens titel',
'Position' => 'Position',
'Move Up' => 'Flytta upp',
'Move Down' => 'Flytta ned',
'Kopiera till ett annat projekt' => '',
'Duplicate' => 'Kopiera uppgiften',
'link' => 'länk',
'Update this comment' => 'Uppdatera kommentaren',
'Comment updated successfully.' => 'Kommentaren har uppdaterats.',
'Unable to update your comment.' => 'Kunde inte uppdatera din kommentar.',
'Remove a comment' => 'Ta bort en kommentar',
'Comment removed successfully.' => 'Kommentaren har tagits bort.',
'Unable to remove this comment.' => 'Kunde inte ta bort denna kommentar.',
'Do you really want to remove this comment?' => 'Är du säker på att du vill ta bort denna kommentar?',
'Only administrators or the creator of the comment can access to this page.' => 'Bara administratörer eller skaparen av kommentaren har tillgång till denna sida.',
'Details' => 'Detaljer',
'Current password for the user "%s"' => 'Nuvarande lösenord för användaren %s"',
'The current password is required' => 'Det nuvarande lösenordet måste anges',
'Wrong password' => 'Fel lösenord',
'Reset all tokens' => 'Återställ alla tokens',
'All tokens have been regenerated.' => 'Alla tokens har degenererats.',
'Unknown' => 'Okänd',
'Last logins' => 'Senaste inloggningarna',
'Login date' => 'Inloggningsdatum',
'Authentication method' => 'Autentiseringsmetoder',
'IP address' => 'IP-adress',
'User agent' => 'Användaragent/webbläsare',
'Persistent connections' => 'Beständiga anslutningar',
'No session' => 'Ingen session',
'Expiration date' => 'Förfallodatum',
'Remember Me' => 'Kom ihåg mig',
'Creation date' => 'Skapatdatum',
'Filter by user' => 'Filtrera på användare',
'Filter by due date' => 'Filtrera på slutdatum',
'Everybody' => 'Alla',
'Open' => 'Öppen',
'Closed' => 'Stängd',
'Search' => 'Sök',
'Nothing found.' => 'Inget kunde hittas.',
'Search in the project "%s"' => 'Sök i projektet "%s"',
'Due date' => 'Måldatum',
'Others formats accepted: %s and %s' => 'Andra format som accepteras: %s and %s',
'Description' => 'Beskrivning',
'%d comments' => '%d kommentarer',
'%d comment' => '%d kommentar',
'Email address invalid' => 'Epost-adressen ogiltig',
'Your Google Account is not linked anymore to your profile.' => 'Ditt Google-konto är inte längre länkat till din profil',
'Unable to unlink your Google Account.' => 'Kunde inte ta bort länken till ditt Google-konto.',
'Google authentication failed' => 'Autentiseringen för Google misslyckades',
'Unable to link your Google Account.' => 'Kunde inte länka till ditt Google-konto',
'Your Google Account is linked to your profile successfully.' => 'Ditt Google-konto har kopplats till din profil.',
'Email' => 'Epost',
'Link my Google Account' => 'Länka till mitt Google-konto',
'Unlink my Google Account' => 'Ta bort länken till mitt Google-konto',
'Login with my Google Account' => 'Logga in med mitt Google-konto',
'Project not found.' => 'Projektet kunde inte hittas',
'Task #%d' => 'Uppgift #%d',
'Task removed successfully.' => 'Uppgiften har tagits bort',
'Unable to remove this task.' => 'Kunde inte ta bort denna uppgift',
'Remove a task' => 'Ta bort en uppgift',
'Do you really want to remove this task: "%s"?' => 'Vill du verkligen ta bort denna uppgift: "%s"?',
'Assign automatically a color based on a category' => 'Tilldela automatiskt en färg baserad på en kategori',
'Assign automatically a category based on a color' => 'Tilldela automatiskt en kategori baserad på en färg',
'Task creation or modification' => 'Skapa eller ändra uppgift',
'Category' => 'Kategori',
'Category:' => 'Kategori:',
'Categories' => 'Ange kategorier',
'Category not found.' => 'Kategorin hittades inte',
'Your category have been created successfully.' => 'Din kategori har skapats',
'Unable to create your category.' => 'Kunde inte skapa din kategori',
'Your category have been updated successfully.' => 'Din kategori har uppdaterats',
'Unable to update your category.' => 'Kunde inte uppdatera din kategori',
'Remove a category' => 'Ta bort en kategori',
'Category removed successfully.' => 'Kategorin har tagits bort',
'Unable to remove this category.' => 'Kunde inte ta bort denna kategori',
'Category modification for the project "%s"' => 'Ändring av kategori för projektet "%s"',
'Category Name' => 'Kategorinamn',
'Categories for the project "%s"' => 'Kategorier för projektet "%s"',
'Add a new category' => 'Lägg till en kategori',
'Do you really want to remove this category: "%s"?' => 'Vill du verkligen ta bort denna kategori: "%s"?',
'Filter by category' => 'Filtrera på kategori',
'All categories' => 'Alla kategorier',
'No category' => 'Ingen kategori',
'The name is required' => 'Namnet måste anges',
'Remove a file' => 'Ta bort en fil',
'Unable to remove this file.' => 'Kunde inte ta bort denna fil',
'File removed successfully.' => 'Filen har tagits bort',
'Attach a document' => 'Bifoga ett dokument',
'Do you really want to remove this file: "%s"?' => 'Vill du verkligen ta bort denna fil:"%s"?',
'open' => 'öppen',
'Attachments' => 'Bifogade filer',
'Edit the task' => 'Ändra uppgiften',
'Edit the description' => 'Ändra beskrivningen',
'Add a comment' => 'Lägg till kommentar',
'Edit a comment' => 'Ändra en kommentar',
'Summary' => 'Sammanfattning',
'Time tracking' => 'Tidsåtgång',
'Estimate:' => 'Uppskattning',
'Spent:' => 'Nedlagd tid',
'Do you really want to remove this sub-task?' => 'Vill du verkligen ta bort deluppgiften?',
'Remaining:' => 'Återstående:',
'hours' => 'timmar',
'spent' => 'nedlagt',
'estimated' => 'uppskattat',
'Sub-Tasks' => 'Deluppgifter',
'Add a sub-task' => 'Lägg till deluppgift',
'Original Estimate' => 'Ursprunglig uppskattning',
'Create another sub-task' => 'Skapa en till deluppgift',
'Time Spent' => 'Nedlagd tid',
'Edit a sub-task' => 'Ändra en deluppgift',
'Remove a sub-task' => 'Ta bort en deluppgift',
'The time must be a numeric value' => 'Tiden måste ha ett numeriskt värde',
'Todo' => 'Att göra',
'In progress' => 'Pågående',
'Done' => 'Slutfört',
'Sub-task removed successfully.' => 'Deluppgiften har tagits bort.',
'Unable to remove this sub-task.' => 'Kunde inte ta bort denna deluppgift.',
'Sub-task updated successfully.' => 'Deluppgiften har uppdaterats.',
'Unable to update your sub-task.' => 'Kunde inte uppdatera din deluppgift.',
'Unable to create your sub-task.' => 'Kunde inte skapa din deluppgift.',
'Sub-task added successfully.' => 'Deluppgiften har lagts till.',
'Maximum size: ' => 'Maxstorlek: ',
'Unable to upload the file.' => 'Kunde inte ladda upp filen.',
'Display another project' => 'Visa ett annat projekt',
'Your GitHub account was successfully linked to your profile.' => 'Ditt GitHub-konto har anslutits till din profil.',
'Unable to link your GitHub Account.' => 'Kunde inte ansluta ditt GitHub-konto.',
'GitHub authentication failed' => 'GitHub-verifiering misslyckades',
'Your GitHub account is no longer linked to your profile.' => 'Ditt GitHub-konto är inte längre anslutet till din profil.',
'Unable to unlink your GitHub Account.' => 'Kunde inte koppla ifrån ditt GitHub-konto.',
'Login with my GitHub Account' => 'Logga in med mitt GitHub-konto',
'Link my GitHub Account' => 'Anslut mitt GitHub-konto',
'Unlink my GitHub Account' => 'Koppla ifrån mitt GitHub-konto',
'Created by %s' => 'Skapad av %s',
'Last modified on %B %e, %G at %k:%M %p' => 'Senaste ändring %B %e, %G kl %k:%M %p'',
);

View file

@ -0,0 +1,395 @@
<?php
return array(
'English' => '英语',
'French' => '法语',
'Polish' => '波兰语',
'Portuguese (Brazilian)' => '葡萄牙语 (巴西)',
'Spanish' => '西班牙语',
'German' => '德语',
'Chinese (Simplified)' => '中文(简体)',
'None' => '未知',
'edit' => '修改',
'Edit' => '修改',
'remove' => '移除',
'Remove' => '移除',
'Update' => '更新',
'Yes' => '是',
'No' => '否',
'cancel' => '取消',
'or' => '或者',
'Yellow' => '黄色',
'Blue' => '蓝色',
'Green' => '绿色',
'Purple' => '紫色',
'Red' => '红色',
'Orange' => '橘色',
'Grey' => '灰色',
'Save' => '保存',
'Login' => '登陆',
'Official website:' => '官方网站:',
'Unassigned' => '未指定',
'View this task' => '查看该任务',
'Add a sub-task' => '添加一个子任务',
'Original Estimate' => '初步预计耗时',
'hours' => '小时',
'Create another sub-task' => '创建另一个子任务',
'Remove user' => '移除用户',
'Do you really want to remove this user: "%s"?' => '你确定要移除这个用户吗:"%s"',
'New user' => '新用户',
'All users' => '所有用户',
'Username' => '用户名',
'Password' => '密码',
'Default Project' => '默认项目',
'Administrator' => '管理员',
'Sign in' => '注册',
'Users' => '用户组',
'No user' => '没有用户',
'Forbidden' => '禁止',
'Access Forbidden' => '禁止进入',
'Only administrators can access to this page.' => '只有管理员可以查看该页面。',
'Edit user' => '修改用户',
'Logout' => '登出',
'Bad username or password' => '用户名或密码错误',
'users' => '用户组',
'projects' => '项目群',
'Edit project' => '修改项目',
'Name' => '名称',
'Activated' => '已激活',
'Projects' => '项目群',
'No project' => '无项目',
'Project' => '项目',
'Status' => '状态',
'Tasks' => '任务群',
'Board' => '看板',
'Inactive' => '未激活',
'Active' => '激活',
'Column %d' => '第%d栏目',
'Add this column' => '加入该栏目',
'%d tasks on the board' => '看板目前有%d个任务',
'%d tasks in total' => '总共有%d个任务',
'Unable to update this board.' => '无法更新该看板。',
'Edit board' => '修改看板',
'Disable' => '禁用',
'Enable' => '启用',
'New project' => '新项目',
'Do you really want to remove this project: "%s"?' => '你确定要移除该项目吗:"%s"',
'Remove project' => '移除项目',
'Boards' => '看板群',
'Edit the board for "%s"' => '为"%s"修改看板',
'All projects' => '所有项目',
'Change columns' => '更改栏目',
'Add a new column' => '添加新栏目',
'Title' => '标题',
'Add Column' => '添加栏目',
'Project "%s"' => '项目 "%s"',
'Nobody assigned' => '无人被指派',
'Assigned to %s' => '指派给 %s',
'Remove a column' => '移除一个栏目',
'Remove a column from a board' => '从看板移除一个栏目',
'Unable to remove this column.' => '无法移除该栏目。',
'Do you really want to remove this column: "%s"?' => '你确定要移除该栏目:"%s"吗?',
'This action will REMOVE ALL TASKS associated to this column!' => '该行为将移除与该栏目相关的所有项目!',
'Settings' => '设置',
'Application settings' => '应用设置',
'Language' => '语言',
'Webhooks token:' => '页面钩子令牌:',
'More information' => '更多信息',
'Database size:' => '数据库大小:',
'Download the database' => '下载数据库',
'Optimize the database' => '优化数据库',
'(VACUUM command)' => '(数据库归整指令)',
'(Gzip compressed Sqlite file)' => '(用Gzip压缩Sqlite文件)',
'User settings' => '用户设置',
'My default project:' => '我的默认项目:',
'Close a task' => '关闭一个项目',
'Do you really want to close this task: "%s"?' => '你确定要关闭该项目?"%s"',
'Edit a task' => '修改一个项目',
'Column' => '栏目',
'Color' => '颜色',
'Assignee' => '负责人',
'Create another task' => '创建另一个项目',
'New task' => '新项目',
'Open a task' => '开一个项目',
'Do you really want to open this task: "%s"?' => '你确定要开这个项目吗?"%s"',
'Back to the board' => '回到看板',
'Created on %B %e, %G at %k:%M %p' => '在%d/%m/%Y %H:%M创建',
'There is nobody assigned' => '无人负责',
'Column on the board:' => '看板上的栏目:',
'Status is open' => '开放状态',
'Status is closed' => '关闭状态',
'Close this task' => '关闭该项目',
'Open this task' => '开放该项目',
'There is no description.' => '无描述。',
'Add a new task' => '添加新任务',
'The username is required' => '需要用户名',
'The maximum length is %d characters' => '最长%d个英文字符',
'The minimum length is %d characters' => '最短%d个英文自负',
'The password is required' => '需要密码',
'This value must be an integer' => '该值必须为整数',
'The username must be unique' => '用户名必须唯一',
'The username must be alphanumeric' => '用户名必须是英文字符或数字组成',
'The user id is required' => '用户id是必须的',
'Passwords doesn\'t matches' => '密码不匹配',
'The confirmation is required' => '需要确认',
'The column is required' => '需要指定栏目',
'The project is required' => '需要指定项目',
'The color is required' => '需要指定颜色',
'The id is required' => '需要指定id',
'The project id is required' => '需要指定项目id',
'The project name is required' => '需要指定项目名称',
'This project must be unique' => '项目名称必须唯一',
'The title is required' => '需要指定标题',
'The language is required' => '需要指定语言',
'There is no active project, the first step is to create a new project.' => '尚无活跃项目,请首先创建一个新项目。',
'Settings saved successfully.' => '设置成功保存。',
'Unable to save your settings.' => '无法保存你的设置。',
'Database optimization done.' => '数据库优化完成。',
'Your project have been created successfully.' => '您的项目已经成功创建。',
'Unable to create your project.' => '无法为您创建项目。',
'Project updated successfully.' => '项目成功更新。',
'Unable to update this project.' => '无法更新该项目。',
'Unable to remove this project.' => '无法移除该项目。',
'Project removed successfully.' => '项目成功移除。',
'Project activated successfully.' => '项目成功激活。',
'Unable to activate this project.' => '无法激活该项目。',
'Project disabled successfully.' => '项目成功禁用。',
'Unable to disable this project.' => '无法禁用该项目。',
'Unable to open this task.' => '无法开启该任务。',
'Task opened successfully.' => '任务开启成功。',
'Unable to close this task.' => '无法关闭该任务。',
'Task closed successfully.' => '任务成功关闭。',
'Unable to update your task.' => '无法更新您的任务。',
'Task updated successfully.' => '任务成功更新。',
'Unable to create your task.' => '无法为您创建项目。',
'Task created successfully.' => '任务成功创建。',
'User created successfully.' => '用户成功创建。',
'Unable to create your user.' => '无法创建用户。',
'User updated successfully.' => '用户成功更新。',
'Unable to update your user.' => '无法为您更新用户。',
'User removed successfully.' => '用户成功移除。',
'Unable to remove this user.' => '无法移除该用户。',
'Board updated successfully.' => '看板成功更新。',
'Ready' => '预备',
'Backlog' => '积压',
'Work in progress' => '工作进行中',
'Done' => '完成',
'Application version:' => '应用程序版本',
'Completed on %B %e, %G at %k:%M %p' => '于%Y年%m月%d日%H时%M分完成',
'%B %e, %G at %k:%M %p' => '%Y年%m月%d日%H时%M分',
'Date created' => '创建时间',
'Date completed' => '完成时间',
'Id' => '昵称',
'No task' => '无任务',
'Completed tasks' => '已完成任务',
'List of projects' => '项目列表',
'Completed tasks for "%s"' => '任务因"%s"原因完成',
'%d closed tasks' => '%d个已关闭任务',
'no task for this project' => '该项目尚无任务',
'Public link' => '公开链接',
'There is no column in your project!' => '该项目尚无栏目项!',
'Change assignee' => '被指派人变更',
'Change assignee for the task "%s"' => '为任务"%s"更改被指派人',
'Timezone' => '时区',
'Sorry, I didn\'t found this information in my database!' => '抱歉,无法在数据库中找到该信息!',
'Page not found' => '页面未找到',
'Story Points' => '评估分值',
'limit' => '限制',
'Task limit' => '任务限制',
'This value must be greater than %d' => '该数值必须大于%d',
'Edit project access list' => '编辑项目存取列表',
'Edit users access' => '编辑用户存取权限',
'Allow this user' => '允许该用户',
'Project access list for "%s"' => '"%s"的项目存取列表',
'Only those users have access to this project:' => '只有这些用户有该项目的存取权限:',
'Don\'t forget that administrators have access to everything.' => '别忘了管理员有一切的权限。',
'revoke' => '撤销',
'List of authorized users' => '已授权的用户列表',
'User' => '用户',
'Everybody have access to this project.' => '任何人都有该项目权限。',
'You are not allowed to access to this project.' => '您对该项目没有权限。',
'Comments' => '评论',
'Post comment' => '发表评论',
'Write your text in Markdown' => '用Markdown格式编写',
'Leave a comment' => '留言',
'Comment is required' => '必须得有评论',
'Leave a description' => '给一个描述',
'Comment added successfully.' => '评论成功添加。',
'Unable to create your comment.' => '无法创建评论。',
'The description is required' => '必须得有描述',
'Edit this task' => '编辑该任务',
'Due Date' => '到期',
'm/d/Y' => 'Y/m/d', // Date format parsed with php
'month/day/year' => '年/月/日', // Help shown to the user
'Invalid date' => '无效日期',
'Must be done before %B %e, %G' => '必须在%Y年%m月%d日前完成',
'%B %e, %G' => '%Y/%m/%d',
'Automatic actions' => '自动行为',
'Add an action' => '添加一个行为',
'Assign automatically a color based on a category' => '基于一个分类自动指派颜色',
'Assign automatically a category based on a color' => '基于一种颜色自动指派分类',
'Your automatic action have been created successfully.' => '您的自动行为已成功创建',
'Unable to create your automatic action.' => '无法为您创建自动行为。',
'Remove an action' => '移除一个行为。',
'Unable to remove this action.' => '无法移除该行为',
'Action removed successfully.' => '成功移除行为。',
'Automatic actions for the project "%s"' => '项目"%s"的自动行为',
'Defined actions' => '已定义的行为',
'Event name' => '事件名称',
'Action name' => '行为名称',
'Action parameters' => '行为参数',
'Action' => '行为',
'Actions' => '行为',
'Event' => '事件',
'When the selected event occurs execute the corresponding action.' => '当所选事件发生时执行相应行为。',
'Next step' => '下一步',
'Define action parameters' => '定义行为参数',
'Save this action' => '保存该行为',
'Do you really want to remove this action: "%s"?' => '确定要益处该行为"%s"吗?',
'Remove an automatic action' => '移除一个自动行为',
'Close the task' => '关闭任务',
'Assign the task to a specific user' => '将该任务指派给一个用户',
'Assign the task to the person who does the action' => '将任务指派给产生该行为的用户',
'Duplicate the task to another project' => '复制该任务到另一项目',
'Move a task to another column' => '移动任务到另一栏目',
'Move a task to another position in the same column' => '将任务移到该栏目另一位置',
'Task modification' => '任务修改',
'Task creation' => '任务创建',
'Open a closed task' => '开启已关闭任务',
'Closing a task' => '正在关闭任务',
'Assign a color to a specific user' => '为特定用户指派颜色',
'Column title' => '栏目名称',
'Position' => '位置',
'Move Up' => '往上移',
'Move Down' => '往下移',
'Duplicate to another project' => '复制到另一项目',
'Duplicate' => '复制',
'link' => '连接',
'Update this comment' => '更新该评论',
'Comment updated successfully.' => '评论成功更新。',
'Unable to update your comment.' => '无法更新您的评论。',
'Remove a comment' => '移除评论',
'Comment removed successfully.' => '评论成功移除。',
'Unable to remove this comment.' => '无法移除该评论。',
'Do you really want to remove this comment?' => '确定要移除评论吗?',
'Only administrators or the creator of the comment can access to this page.' => '只有管理员或评论创建者可以进入该页面。',
'Details' => '细节',
'Current password for the user "%s"' => '用户"%s"的当前密码',
'The current password is required' => '需要输入当前密码',
'Wrong password' => '密码错误',
'Confirmation' => '再输一次新密码',
'Reset all tokens' => '重置所有令牌',
'All tokens have been regenerated.' => '所有令牌都重新生成了。',
'Unknown' => '未知',
'Last logins' => '上次登录',
'Login date' => '登录日期',
'Authentication method' => '认证方式',
'IP address' => 'IP地址',
'User agent' => '用户代理',
'Persistent connections' => '持续连接',
'No session' => '无会话',
'Expiration date' => '过期',
'Remember Me' => '记住我',
'Creation date' => '创建日期',
'Filter by user' => '按用户过滤',
'Filter by due date' => '按到期时间过滤',
'Everybody' => '所有人',
'Open' => '打开',
'Closed' => '关闭',
'Search' => '查找',
'Nothing found.' => '没找到。',
'Search in the project "%s"' => '在项目"%s"中查找',
'Due date' => '到期',
'Others formats accepted: %s and %s' => '允许其他格式:%s和%s',
'Description' => '描述',
'%d comments' => '%d个评论',
'%d comment' => '%d个评论',
'Email address invalid' => 'Email地址无效',
'Your Google Account is not linked anymore to your profile.' => '您的google帐号不再与您的账户配置关联。',
'Unable to unlink your Google Account.' => '无法去除您google帐号的关联',
'Google authentication failed' => 'google验证失败',
'Unable to link your Google Account.' => '无法关联您的google帐号。',
'Your Google Account is linked to your profile successfully.' => '您的google帐号已成功与账户配置关联。',
'Email' => 'Email',
'Link my Google Account' => '关联我的google帐号',
'Unlink my Google Account' => '去除我的google帐号关联',
'Login with my Google Account' => '用我的google帐号登录',
'Project not found.' => '未发现项目',
'Task #%d' => '任务 #%d',
'Task removed successfully.' => '任务成功去除',
'Unable to remove this task.' => '无法移除该任务。',
'Remove a task' => '移除一个任务',
'Do you really want to remove this task: "%s"?' => '确定要溢出该任务"%s"吗?',
'Assign a color to a specific category' => '指派颜色给一个特定分类',
'Task creation or modification' => '任务创建或修改',
'Category' => '分类',
'Category:' => '分类:',
'Categories' => '分类',
'Category not found.' => '未找到分类。',
'Your category have been created successfully.' => '成功为您创建分类。',
'Unable to create your category.' => '无法为您创建分类。',
'Your category have been updated successfully.' => '成功为您更新分类。',
'Unable to update your category.' => '无法为您更新分类。',
'Remove a category' => '移除一个分类',
'Category removed successfully.' => '分类成功移除。',
'Unable to remove this category.' => '无法移除该分类。',
'Category modification for the project "%s"' => '为项目"%s"修改分类',
'Category Name' => '分类名称',
'Categories for the project "%s"' => '项目"%s"的分类',
'Add a new category' => '加入一个新分类',
'Do you really want to remove this category: "%s"?' => '您确定要移除分类"%s"吗?',
'Filter by category' => '按分类过滤',
'All categories' => '所有分类',
'No category' => '无分类',
'The name is required' => '必须要有名字',
'Remove a file' => '移除一个文件',
'Unable to remove this file.' => '无法移除该文件。',
'File removed successfully.' => '文件成功移除。',
'Attach a document' => '附上一个文档',
'Do you really want to remove this file: "%s"?' => '您确定要移除文件"%s"吗?',
'open' => '打开',
'Attachments' => '附件',
'Edit the task' => '修改任务',
'Edit the description' => '修改描述',
'Add a comment' => '添加一个注释',
'Edit a comment' => '修改一个注释',
'Summary' => '概要',
// 'Time tracking' => '',
// 'Estimate:' => '',
// 'Spent:' => '',
// 'Do you really want to remove this sub-task?' => '',
// 'Remaining:' => '',
// 'hours' => '',
// 'spent' => '',
// 'estimated' => '',
// 'Sub-Tasks' => '',
// 'Add a sub-task' => '',
// 'Original Estimate' => '',
// 'Create another sub-task' => '',
// 'Time Spent' => '',
// 'Edit a sub-task' => '',
// 'Remove a sub-task' => '',
// 'The time must be a numeric value' => '',
// 'Todo' => '',
// 'In progress' => '',
// 'Done' => '',
// 'Sub-task removed successfully.' => '',
// 'Unable to remove this sub-task.' => '',
// 'Sub-task updated successfully.' => '',
// 'Unable to update your sub-task.' => '',
// 'Unable to create your sub-task.' => '',
// 'Sub-task added successfully.' => '',
// 'Maximum size: ' => '',
// 'Unable to upload the file.' => '',
// 'Display another project' => '',
// 'Your GitHub account was successfully linked to your profile.' => '',
// 'Unable to link your GitHub Account.' => '',
// 'GitHub authentication failed' => '',
// 'Your GitHub account is no longer linked to your profile.' => '',
// 'Unable to unlink your GitHub Account.' => '',
// 'Login with my GitHub Account' => '',
// 'Link my GitHub Account' => '',
// 'Unlink my GitHub Account' => '',
// 'Created by %s' => 'Créé par %s',
// 'Last modified on %B %e, %G at %k:%M %p' => '',
);

176
sources/app/Model/Acl.php Normal file
View file

@ -0,0 +1,176 @@
<?php
namespace Model;
/**
* Acl model
*
* @package model
* @author Frederic Guillot
*/
class Acl extends Base
{
/**
* Controllers and actions allowed from outside
*
* @access private
* @var array
*/
private $public_actions = array(
'user' => array('login', 'check', 'google', 'github'),
'task' => array('add'),
'board' => array('readonly'),
);
/**
* Controllers and actions allowed for regular users
*
* @access private
* @var array
*/
private $user_actions = array(
'app' => array('index'),
'board' => array('index', 'show', 'assign', 'assigntask', 'save', 'check'),
'project' => array('tasks', 'index', 'forbidden', 'search'),
'user' => array('index', 'edit', 'update', 'forbidden', 'logout', 'index', 'unlinkgoogle', 'unlinkgithub'),
'config' => array('index', 'removeremembermetoken'),
'comment' => array('create', 'save', 'confirm', 'remove', 'update', 'edit', 'forbidden'),
'file' => array('create', 'save', 'download', 'confirm', 'remove', 'open', 'image'),
'subtask' => array('create', 'save', 'edit', 'update', 'confirm', 'remove'),
'task' => array(
'show',
'create',
'save',
'edit',
'update',
'close',
'confirmclose',
'open',
'confirmopen',
'duplicate',
'remove',
'confirmremove',
'editdescription',
'savedescription',
),
);
/**
* Return true if the specified controller/action is allowed according to the given acl
*
* @access public
* @param array $acl Acl list
* @param string $controller Controller name
* @param string $action Action name
* @return bool
*/
public function isAllowedAction(array $acl, $controller, $action)
{
if (isset($acl[$controller])) {
return in_array($action, $acl[$controller]);
}
return false;
}
/**
* Return true if the given action is public
*
* @access public
* @param string $controller Controller name
* @param string $action Action name
* @return bool
*/
public function isPublicAction($controller, $action)
{
return $this->isAllowedAction($this->public_actions, $controller, $action);
}
/**
* Return true if the given action is allowed for a regular user
*
* @access public
* @param string $controller Controller name
* @param string $action Action name
* @return bool
*/
public function isUserAction($controller, $action)
{
return $this->isAllowedAction($this->user_actions, $controller, $action);
}
/**
* Return true if the logged user is admin
*
* @access public
* @return bool
*/
public function isAdminUser()
{
return isset($_SESSION['user']['is_admin']) && $_SESSION['user']['is_admin'] === true;
}
/**
* Return true if the logged user is not admin
*
* @access public
* @return bool
*/
public function isRegularUser()
{
return isset($_SESSION['user']['is_admin']) && $_SESSION['user']['is_admin'] === false;
}
/**
* Get the connected user id
*
* @access public
* @return integer
*/
public function getUserId()
{
return isset($_SESSION['user']['id']) ? (int) $_SESSION['user']['id'] : 0;
}
/**
* Check is the user is connected
*
* @access public
* @return bool
*/
public function isLogged()
{
return ! empty($_SESSION['user']);
}
/**
* Check is the user was authenticated with the RememberMe or set the value
*
* @access public
* @param bool $value Set true if the user use the RememberMe
* @return bool
*/
public function isRememberMe($value = null)
{
if ($value !== null) {
$_SESSION['is_remember_me'] = $value;
}
return empty($_SESSION['is_remember_me']) ? false : $_SESSION['is_remember_me'];
}
/**
* Check if an action is allowed for the logged user
*
* @access public
* @param string $controller Controller name
* @param string $action Action name
* @return bool
*/
public function isPageAccessAllowed($controller, $action)
{
return $this->isPublicAction($controller, $action) ||
$this->isAdminUser() ||
($this->isRegularUser() && $this->isUserAction($controller, $action));
}
}

View file

@ -0,0 +1,273 @@
<?php
namespace Model;
use LogicException;
use SimpleValidator\Validator;
use SimpleValidator\Validators;
/**
* Action model
*
* @package model
* @author Frederic Guillot
*/
class Action extends Base
{
/**
* SQL table name for actions
*
* @var string
*/
const TABLE = 'actions';
/**
* SQL table name for action parameters
*
* @var string
*/
const TABLE_PARAMS = 'action_has_params';
/**
* Return the name and description of available actions
*
* @access public
* @return array
*/
public function getAvailableActions()
{
return array(
'TaskClose' => t('Close the task'),
'TaskAssignSpecificUser' => t('Assign the task to a specific user'),
'TaskAssignCurrentUser' => t('Assign the task to the person who does the action'),
'TaskDuplicateAnotherProject' => t('Duplicate the task to another project'),
'TaskAssignColorUser' => t('Assign a color to a specific user'),
'TaskAssignColorCategory' => t('Assign automatically a color based on a category'),
'TaskAssignCategoryColor' => t('Assign automatically a category based on a color'),
);
}
/**
* Return the name and description of available actions
*
* @access public
* @return array
*/
public function getAvailableEvents()
{
return array(
Task::EVENT_MOVE_COLUMN => t('Move a task to another column'),
Task::EVENT_MOVE_POSITION => t('Move a task to another position in the same column'),
Task::EVENT_UPDATE => t('Task modification'),
Task::EVENT_CREATE => t('Task creation'),
Task::EVENT_OPEN => t('Open a closed task'),
Task::EVENT_CLOSE => t('Closing a task'),
Task::EVENT_CREATE_UPDATE => t('Task creation or modification'),
);
}
/**
* Return actions and parameters for a given project
*
* @access public
* @param $project_id
* @return array
*/
public function getAllByProject($project_id)
{
$actions = $this->db->table(self::TABLE)->eq('project_id', $project_id)->findAll();
foreach ($actions as &$action) {
$action['params'] = $this->db->table(self::TABLE_PARAMS)->eq('action_id', $action['id'])->findAll();
}
return $actions;
}
/**
* Return all actions and parameters
*
* @access public
* @return array
*/
public function getAll()
{
$actions = $this->db->table(self::TABLE)->findAll();
foreach ($actions as &$action) {
$action['params'] = $this->db->table(self::TABLE_PARAMS)->eq('action_id', $action['id'])->findAll();
}
return $actions;
}
/**
* Get all required action parameters for all registered actions
*
* @access public
* @return array All required parameters for all actions
*/
public function getAllActionParameters()
{
$params = array();
foreach ($this->getAll() as $action) {
$action = $this->load($action['action_name'], $action['project_id']);
$params += $action->getActionRequiredParameters();
}
return $params;
}
/**
* Fetch an action
*
* @access public
* @param integer $action_id Action id
* @return array Action data
*/
public function getById($action_id)
{
$action = $this->db->table(self::TABLE)->eq('id', $action_id)->findOne();
$action['params'] = $this->db->table(self::TABLE_PARAMS)->eq('action_id', $action_id)->findAll();
return $action;
}
/**
* Remove an action
*
* @access public
* @param integer $action_id Action id
* @return bool Success or not
*/
public function remove($action_id)
{
return $this->db->table(self::TABLE)->eq('id', $action_id)->remove();
}
/**
* Create an action
*
* @access public
* @param array $values Required parameters to save an action
* @return bool Success or not
*/
public function create(array $values)
{
$this->db->startTransaction();
$action = array(
'project_id' => $values['project_id'],
'event_name' => $values['event_name'],
'action_name' => $values['action_name'],
);
if (! $this->db->table(self::TABLE)->save($action)) {
$this->db->cancelTransaction();
return false;
}
$action_id = $this->db->getConnection()->getLastId();
foreach ($values['params'] as $param_name => $param_value) {
$action_param = array(
'action_id' => $action_id,
'name' => $param_name,
'value' => $param_value,
);
if (! $this->db->table(self::TABLE_PARAMS)->save($action_param)) {
$this->db->cancelTransaction();
return false;
}
}
$this->db->closeTransaction();
return true;
}
/**
* Load all actions and attach events
*
* @access public
*/
public function attachEvents()
{
foreach ($this->getAll() as $action) {
$listener = $this->load($action['action_name'], $action['project_id']);
foreach ($action['params'] as $param) {
$listener->setParam($param['name'], $param['value']);
}
$this->event->attach($action['event_name'], $listener);
}
}
/**
* Load an action
*
* @access public
* @param string $name Action class name
* @param integer $project_id Project id
* @throws \LogicException
* @return \Core\Listener Action Instance
* @throw LogicException
*/
public function load($name, $project_id)
{
switch ($name) {
case 'TaskClose':
$className = '\Action\TaskClose';
return new $className($project_id, new Task($this->db, $this->event));
case 'TaskAssignCurrentUser':
$className = '\Action\TaskAssignCurrentUser';
return new $className($project_id, new Task($this->db, $this->event), new Acl($this->db, $this->event));
case 'TaskAssignSpecificUser':
$className = '\Action\TaskAssignSpecificUser';
return new $className($project_id, new Task($this->db, $this->event));
case 'TaskDuplicateAnotherProject':
$className = '\Action\TaskDuplicateAnotherProject';
return new $className($project_id, new Task($this->db, $this->event));
case 'TaskAssignColorUser':
$className = '\Action\TaskAssignColorUser';
return new $className($project_id, new Task($this->db, $this->event));
case 'TaskAssignColorCategory':
$className = '\Action\TaskAssignColorCategory';
return new $className($project_id, new Task($this->db, $this->event));
case 'TaskAssignCategoryColor':
$className = '\Action\TaskAssignCategoryColor';
return new $className($project_id, new Task($this->db, $this->event));
default:
throw new LogicException('Action not found: '.$name);
}
}
/**
* Validate action creation
*
* @access public
* @param array $values Required parameters to save an action
* @return array $valid, $errors [0] = Success or not, [1] = List of errors
*/
public function validateCreation(array $values)
{
$v = new Validator($values, array(
new Validators\Required('project_id', t('The project id is required')),
new Validators\Integer('project_id', t('This value must be an integer')),
new Validators\Required('event_name', t('This value is required')),
new Validators\Required('action_name', t('This value is required')),
new Validators\Required('params', t('This value is required')),
));
return array(
$v->execute(),
$v->getErrors()
);
}
}

View file

@ -0,0 +1,58 @@
<?php
namespace Model;
require __DIR__.'/../../vendor/SimpleValidator/Validator.php';
require __DIR__.'/../../vendor/SimpleValidator/Base.php';
require __DIR__.'/../../vendor/SimpleValidator/Validators/Required.php';
require __DIR__.'/../../vendor/SimpleValidator/Validators/Unique.php';
require __DIR__.'/../../vendor/SimpleValidator/Validators/MaxLength.php';
require __DIR__.'/../../vendor/SimpleValidator/Validators/MinLength.php';
require __DIR__.'/../../vendor/SimpleValidator/Validators/Integer.php';
require __DIR__.'/../../vendor/SimpleValidator/Validators/Equals.php';
require __DIR__.'/../../vendor/SimpleValidator/Validators/AlphaNumeric.php';
require __DIR__.'/../../vendor/SimpleValidator/Validators/GreaterThan.php';
require __DIR__.'/../../vendor/SimpleValidator/Validators/Date.php';
require __DIR__.'/../../vendor/SimpleValidator/Validators/Email.php';
require __DIR__.'/../../vendor/SimpleValidator/Validators/Numeric.php';
use Core\Event;
use PicoDb\Database;
/**
* Base model class
*
* @package model
* @author Frederic Guillot
*/
abstract class Base
{
/**
* Database instance
*
* @access protected
* @var \PicoDb\Database
*/
protected $db;
/**
* Event dispatcher instance
*
* @access protected
* @var \Core\Event
*/
protected $event;
/**
* Constructor
*
* @access public
* @param \PicoDb\Database $db Database instance
* @param \Core\Event $event Event dispatcher instance
*/
public function __construct(Database $db, Event $event)
{
$this->db = $db;
$this->event = $event;
}
}

359
sources/app/Model/Board.php Normal file
View file

@ -0,0 +1,359 @@
<?php
namespace Model;
use SimpleValidator\Validator;
use SimpleValidator\Validators;
/**
* Board model
*
* @package model
* @author Frederic Guillot
*/
class Board extends Base
{
/**
* SQL table name
*
* @var string
*/
const TABLE = 'columns';
/**
* Save task positions for each column
*
* @access public
* @param array $values [['task_id' => X, 'column_id' => X, 'position' => X], ...]
* @return boolean
*/
public function saveTasksPosition(array $values)
{
$taskModel = new Task($this->db, $this->event);
$this->db->startTransaction();
foreach ($values as $value) {
if (! $taskModel->move($value['task_id'], $value['column_id'], $value['position'])) {
$this->db->cancelTransaction();
return false;
}
}
$this->db->closeTransaction();
return true;
}
/**
* Create a board with default columns, must be executed inside a transaction
*
* @access public
* @param integer $project_id Project id
* @param array $columns List of columns title ['column1', 'column2', ...]
* @return boolean
*/
public function create($project_id, array $columns)
{
$position = 0;
foreach ($columns as $title) {
$values = array(
'title' => $title,
'position' => ++$position,
'project_id' => $project_id,
);
if (! $this->db->table(self::TABLE)->save($values)) {
return false;
}
}
return true;
}
/**
* Add a new column to the board
*
* @access public
* @param array $values ['title' => X, 'project_id' => X]
* @return boolean
*/
public function add(array $values)
{
$values['position'] = $this->getLastColumnPosition($values['project_id']) + 1;
return $this->db->table(self::TABLE)->save($values);
}
/**
* Update columns
*
* @access public
* @param array $values Form values
* @return boolean
*/
public function update(array $values)
{
$this->db->startTransaction();
foreach (array('title', 'task_limit') as $field) {
foreach ($values[$field] as $column_id => $field_value) {
if ($field === 'task_limit' && empty($field_value)) {
$field_value = 0;
}
$this->updateColumn($column_id, array($field => $field_value));
}
}
$this->db->closeTransaction();
return true;
}
/**
* Update a column
*
* @access public
* @param integer $column_id Column id
* @param array $values Form values
* @return boolean
*/
public function updateColumn($column_id, array $values)
{
return $this->db->table(self::TABLE)->eq('id', $column_id)->update($values);
}
/**
* Move a column down, increment the column position value
*
* @access public
* @param integer $project_id Project id
* @param integer $column_id Column id
* @return boolean
*/
public function moveDown($project_id, $column_id)
{
$columns = $this->db->table(self::TABLE)->eq('project_id', $project_id)->asc('position')->listing('id', 'position');
$positions = array_flip($columns);
if (isset($columns[$column_id]) && $columns[$column_id] < count($columns)) {
$position = ++$columns[$column_id];
$columns[$positions[$position]]--;
$this->db->startTransaction();
$this->db->table(self::TABLE)->eq('id', $column_id)->update(array('position' => $position));
$this->db->table(self::TABLE)->eq('id', $positions[$position])->update(array('position' => $columns[$positions[$position]]));
$this->db->closeTransaction();
return true;
}
return false;
}
/**
* Move a column up, decrement the column position value
*
* @access public
* @param integer $project_id Project id
* @param integer $column_id Column id
* @return boolean
*/
public function moveUp($project_id, $column_id)
{
$columns = $this->db->table(self::TABLE)->eq('project_id', $project_id)->asc('position')->listing('id', 'position');
$positions = array_flip($columns);
if (isset($columns[$column_id]) && $columns[$column_id] > 1) {
$position = --$columns[$column_id];
$columns[$positions[$position]]++;
$this->db->startTransaction();
$this->db->table(self::TABLE)->eq('id', $column_id)->update(array('position' => $position));
$this->db->table(self::TABLE)->eq('id', $positions[$position])->update(array('position' => $columns[$positions[$position]]));
$this->db->closeTransaction();
return true;
}
return false;
}
/**
* Get all columns and tasks for a given project
*
* @access public
* @param integer $project_id Project id
* @param array $filters
* @return array
*/
public function get($project_id, array $filters = array())
{
$this->db->startTransaction();
$columns = $this->getColumns($project_id);
$filters[] = array('column' => 'project_id', 'operator' => 'eq', 'value' => $project_id);
$filters[] = array('column' => 'is_active', 'operator' => 'eq', 'value' => Task::STATUS_OPEN);
$taskModel = new Task($this->db, $this->event);
$tasks = $taskModel->find($filters);
foreach ($columns as &$column) {
$column['tasks'] = array();
foreach ($tasks as &$task) {
if ($task['column_id'] == $column['id']) {
$column['tasks'][] = $task;
}
}
}
$this->db->closeTransaction();
return $columns;
}
/**
* Get the first column id for a given project
*
* @access public
* @param integer $project_id Project id
* @return integer
*/
public function getFirstColumn($project_id)
{
return $this->db->table(self::TABLE)->eq('project_id', $project_id)->asc('position')->findOneColumn('id');
}
/**
* Get the list of columns sorted by position [ column_id => title ]
*
* @access public
* @param integer $project_id Project id
* @return array
*/
public function getColumnsList($project_id)
{
return $this->db->table(self::TABLE)->eq('project_id', $project_id)->asc('position')->listing('id', 'title');
}
/**
* Get all columns sorted by position for a given project
*
* @access public
* @param integer $project_id Project id
* @return array
*/
public function getColumns($project_id)
{
return $this->db->table(self::TABLE)->eq('project_id', $project_id)->asc('position')->findAll();
}
/**
* Get the number of columns for a given project
*
* @access public
* @param integer $project_id Project id
* @return integer
*/
public function countColumns($project_id)
{
return $this->db->table(self::TABLE)->eq('project_id', $project_id)->count();
}
/**
* Get a column by the id
*
* @access public
* @param integer $column_id Column id
* @return array
*/
public function getColumn($column_id)
{
return $this->db->table(self::TABLE)->eq('id', $column_id)->findOne();
}
/**
* Get the position of the last column for a given project
*
* @access public
* @param integer $project_id Project id
* @return integer
*/
public function getLastColumnPosition($project_id)
{
return (int) $this->db
->table(self::TABLE)
->eq('project_id', $project_id)
->desc('position')
->findOneColumn('position');
}
/**
* Remove a column and all tasks associated to this column
*
* @access public
* @param integer $column_id Column id
* @return boolean
*/
public function removeColumn($column_id)
{
return $this->db->table(self::TABLE)->eq('id', $column_id)->remove();
}
/**
* Validate column modification
*
* @access public
* @param array $columns Original columns List
* @param array $values Required parameters to update a column
* @return array $valid, $errors [0] = Success or not, [1] = List of errors
*/
public function validateModification(array $columns, array $values)
{
$rules = array();
foreach ($columns as $column_id => $column_title) {
$rules[] = new Validators\Integer('task_limit['.$column_id.']', t('This value must be an integer'));
$rules[] = new Validators\GreaterThan('task_limit['.$column_id.']', t('This value must be greater than %d', 0), 0);
$rules[] = new Validators\Required('title['.$column_id.']', t('The title is required'));
$rules[] = new Validators\MaxLength('title['.$column_id.']', t('The maximum length is %d characters', 50), 50);
}
$v = new Validator($values, $rules);
return array(
$v->execute(),
$v->getErrors()
);
}
/**
* Validate column creation
*
* @access public
* @param array $values Required parameters to save an action
* @return array $valid, $errors [0] = Success or not, [1] = List of errors
*/
public function validateCreation(array $values)
{
$v = new Validator($values, array(
new Validators\Required('project_id', t('The project id is required')),
new Validators\Integer('project_id', t('This value must be an integer')),
new Validators\Required('title', t('The title is required')),
new Validators\MaxLength('title', t('The maximum length is %d characters', 50), 50),
));
return array(
$v->execute(),
$v->getErrors()
);
}
}

View file

@ -0,0 +1,165 @@
<?php
namespace Model;
use SimpleValidator\Validator;
use SimpleValidator\Validators;
/**
* Category model
*
* @package model
* @author Frederic Guillot
*/
class Category extends Base
{
/**
* SQL table name
*
* @var string
*/
const TABLE = 'project_has_categories';
/**
* Get a category by the id
*
* @access public
* @param integer $category_id Category id
* @return array
*/
public function getById($category_id)
{
return $this->db->table(self::TABLE)->eq('id', $category_id)->findOne();
}
/**
* Return the list of all categories
*
* @access public
* @param integer $project_id Project id
* @param bool $prepend_none If true, prepend to the list the value 'None'
* @param bool $prepend_all If true, prepend to the list the value 'All'
* @return array
*/
public function getList($project_id, $prepend_none = true, $prepend_all = false)
{
$listing = $this->db->table(self::TABLE)
->eq('project_id', $project_id)
->asc('name')
->listing('id', 'name');
$prepend = array();
if ($prepend_all) {
$prepend[-1] = t('All categories');
}
if ($prepend_none) {
$prepend[0] = t('No category');
}
return $prepend + $listing;
}
/**
* Return all categories for a given project
*
* @access public
* @param integer $project_id Project id
* @return array
*/
public function getAll($project_id)
{
return $this->db->table(self::TABLE)
->eq('project_id', $project_id)
->asc('name')
->findAll();
}
/**
* Create a category
*
* @access public
* @param array $values Form values
* @return bool
*/
public function create(array $values)
{
return $this->db->table(self::TABLE)->save($values);
}
/**
* Update a category
*
* @access public
* @param array $values Form values
* @return bool
*/
public function update(array $values)
{
return $this->db->table(self::TABLE)->eq('id', $values['id'])->save($values);
}
/**
* Remove a category
*
* @access public
* @param integer $category_id Category id
* @return bool
*/
public function remove($category_id)
{
$this->db->startTransaction();
$r1 = $this->db->table(Task::TABLE)->eq('category_id', $category_id)->update(array('category_id' => 0));
$r2 = $this->db->table(self::TABLE)->eq('id', $category_id)->remove();
$this->db->closeTransaction();
return $r1 && $r2;
}
/**
* Validate category creation
*
* @access public
* @param array $values Form values
* @return array $valid, $errors [0] = Success or not, [1] = List of errors
*/
public function validateCreation(array $values)
{
$v = new Validator($values, array(
new Validators\Required('project_id', t('The project id is required')),
new Validators\Integer('project_id', t('The project id must be an integer')),
new Validators\Required('name', t('The name is required')),
new Validators\MaxLength('name', t('The maximum length is %d characters', 50), 50)
));
return array(
$v->execute(),
$v->getErrors()
);
}
/**
* Validate category modification
*
* @access public
* @param array $values Form values
* @return array $valid, $errors [0] = Success or not, [1] = List of errors
*/
public function validateModification(array $values)
{
$v = new Validator($values, array(
new Validators\Required('id', t('The id is required')),
new Validators\Integer('id', t('The id must be an integer')),
new Validators\Required('project_id', t('The project id is required')),
new Validators\Integer('project_id', t('The project id must be an integer')),
new Validators\Required('name', t('The name is required')),
new Validators\MaxLength('name', t('The maximum length is %d characters', 50), 50)
));
return array(
$v->execute(),
$v->getErrors()
);
}
}

View file

@ -0,0 +1,171 @@
<?php
namespace Model;
use SimpleValidator\Validator;
use SimpleValidator\Validators;
/**
* Comment model
*
* @package model
* @author Frederic Guillot
*/
class Comment extends Base
{
/**
* SQL table name
*
* @var string
*/
const TABLE = 'comments';
/**
* Get all comments for a given task
*
* @access public
* @param integer $task_id Task id
* @return array
*/
public function getAll($task_id)
{
return $this->db
->table(self::TABLE)
->columns(
self::TABLE.'.id',
self::TABLE.'.date',
self::TABLE.'.task_id',
self::TABLE.'.user_id',
self::TABLE.'.comment',
User::TABLE.'.username'
)
->join(User::TABLE, 'id', 'user_id')
->orderBy(self::TABLE.'.date', 'ASC')
->eq(self::TABLE.'.task_id', $task_id)
->findAll();
}
/**
* Get a comment
*
* @access public
* @param integer $comment_id Comment id
* @return array
*/
public function getById($comment_id)
{
return $this->db
->table(self::TABLE)
->columns(
self::TABLE.'.id',
self::TABLE.'.task_id',
self::TABLE.'.user_id',
self::TABLE.'.date',
self::TABLE.'.comment',
User::TABLE.'.username'
)
->join(User::TABLE, 'id', 'user_id')
->eq(self::TABLE.'.id', $comment_id)
->findOne();
}
/**
* Get the number of comments for a given task
*
* @access public
* @param integer $task_id Task id
* @return integer
*/
public function count($task_id)
{
return $this->db
->table(self::TABLE)
->eq(self::TABLE.'.task_id', $task_id)
->count();
}
/**
* Save a comment in the database
*
* @access public
* @param array $values Form values
* @return boolean
*/
public function create(array $values)
{
$values['date'] = time();
return $this->db->table(self::TABLE)->save($values);
}
/**
* Update a comment in the database
*
* @access public
* @param array $values Form values
* @return boolean
*/
public function update(array $values)
{
return $this->db
->table(self::TABLE)
->eq('id', $values['id'])
->update(array('comment' => $values['comment']));
}
/**
* Remove a comment
*
* @access public
* @param integer $comment_id Comment id
* @return boolean
*/
public function remove($comment_id)
{
return $this->db->table(self::TABLE)->eq('id', $comment_id)->remove();
}
/**
* Validate comment creation
*
* @access public
* @param array $values Required parameters to save an action
* @return array $valid, $errors [0] = Success or not, [1] = List of errors
*/
public function validateCreation(array $values)
{
$v = new Validator($values, array(
new Validators\Required('task_id', t('This value is required')),
new Validators\Integer('task_id', t('This value must be an integer')),
new Validators\Required('user_id', t('This value is required')),
new Validators\Integer('user_id', t('This value must be an integer')),
new Validators\Required('comment', t('Comment is required'))
));
return array(
$v->execute(),
$v->getErrors()
);
}
/**
* Validate comment modification
*
* @access public
* @param array $values Required parameters to save an action
* @return array $valid, $errors [0] = Success or not, [1] = List of errors
*/
public function validateModification(array $values)
{
$v = new Validator($values, array(
new Validators\Required('id', t('This value is required')),
new Validators\Integer('id', t('This value must be an integer')),
new Validators\Required('comment', t('Comment is required'))
));
return array(
$v->execute(),
$v->getErrors()
);
}
}

193
sources/app/Model/File.php Normal file
View file

@ -0,0 +1,193 @@
<?php
namespace Model;
/**
* File model
*
* @package model
* @author Frederic Guillot
*/
class File extends Base
{
/**
* SQL table name
*
* @var string
*/
const TABLE = 'task_has_files';
/**
* Directory where are stored files
*
* @var string
*/
const BASE_PATH = 'data/files/';
/**
* Get a file by the id
*
* @access public
* @param integer $file_id File id
* @return array
*/
public function getById($file_id)
{
return $this->db->table(self::TABLE)->eq('id', $file_id)->findOne();
}
/**
* Remove a file
*
* @access public
* @param integer $file_id File id
* @return bool
*/
public function remove($file_id)
{
$file = $this->getbyId($file_id);
if (! empty($file) && @unlink(self::BASE_PATH.$file['path'])) {
return $this->db->table(self::TABLE)->eq('id', $file_id)->remove();
}
return false;
}
/**
* Remove all files for a given task
*
* @access public
* @param integer $task_id Task id
* @return bool
*/
public function removeAll($task_id)
{
$files = $this->getAll($task_id);
foreach ($files as $file) {
$this->remove($file['id']);
}
}
/**
* Create a file entry in the database
*
* @access public
* @param integer $task_id Task id
* @param string $name Filename
* @param string $path Path on the disk
* @param bool $is_image Image or not
* @return bool
*/
public function create($task_id, $name, $path, $is_image)
{
return $this->db->table(self::TABLE)->save(array(
'task_id' => $task_id,
'name' => $name,
'path' => $path,
'is_image' => $is_image ? '1' : '0',
));
}
/**
* Get all files for a given task
*
* @access public
* @param integer $task_id Task id
* @return array
*/
public function getAll($task_id)
{
return $this->db->table(self::TABLE)
->eq('task_id', $task_id)
->asc('name')
->findAll();
}
/**
* Check if a filename is an image
*
* @access public
* @param string $filename Filename
* @return bool
*/
public function isImage($filename)
{
return getimagesize($filename) !== false;
}
/**
* Generate the path for a new filename
*
* @access public
* @param integer $project_id Project id
* @param integer $task_id Task id
* @param string $filename Filename
* @return string
*/
public function generatePath($project_id, $task_id, $filename)
{
return $project_id.DIRECTORY_SEPARATOR.$task_id.DIRECTORY_SEPARATOR.hash('sha1', $filename.time());
}
/**
* Check if the base directory is created correctly
*
* @access public
*/
public function setup()
{
if (! is_dir(self::BASE_PATH)) {
if (! mkdir(self::BASE_PATH, 0755, true)) {
die('Unable to create the upload directory: "'.self::BASE_PATH.'"');
}
}
if (! is_writable(self::BASE_PATH)) {
die('The directory "'.self::BASE_PATH.'" must be writeable by your webserver user');
}
}
/**
* Handle file upload
*
* @access public
* @param integer $project_id Project id
* @param integer $task_id Task id
* @param string $form_name File form name
* @return bool
*/
public function upload($project_id, $task_id, $form_name)
{
$this->setup();
$result = array();
if (! empty($_FILES[$form_name])) {
foreach ($_FILES[$form_name]['error'] as $key => $error) {
if ($error == UPLOAD_ERR_OK && $_FILES[$form_name]['size'][$key] > 0) {
$original_filename = basename($_FILES[$form_name]['name'][$key]);
$uploaded_filename = $_FILES[$form_name]['tmp_name'][$key];
$destination_filename = $this->generatePath($project_id, $task_id, $original_filename);
@mkdir(self::BASE_PATH.dirname($destination_filename), 0755, true);
if (@move_uploaded_file($uploaded_filename, self::BASE_PATH.$destination_filename)) {
$result[] = $this->create(
$task_id,
$original_filename,
$destination_filename,
$this->isImage(self::BASE_PATH.$destination_filename)
);
}
}
}
}
return count(array_unique($result)) === 1;
}
}

View file

@ -0,0 +1,178 @@
<?php
namespace Model;
require __DIR__.'/../../vendor/OAuth/bootstrap.php';
use OAuth\Common\Storage\Session;
use OAuth\Common\Consumer\Credentials;
use OAuth\Common\Http\Uri\UriFactory;
use OAuth\ServiceFactory;
use OAuth\Common\Http\Exception\TokenResponseException;
/**
* GitHub model
*
* @package model
*/
class GitHub extends Base
{
/**
* Authenticate a GitHub user
*
* @access public
* @param string $github_id GitHub user id
* @return boolean
*/
public function authenticate($github_id)
{
$userModel = new User($this->db, $this->event);
$user = $userModel->getByGitHubId($github_id);
if ($user) {
// Create the user session
$userModel->updateSession($user);
// Update login history
$lastLogin = new LastLogin($this->db, $this->event);
$lastLogin->create(
LastLogin::AUTH_GITHUB,
$user['id'],
$userModel->getIpAddress(),
$userModel->getUserAgent()
);
return true;
}
return false;
}
/**
* Unlink a GitHub account for a given user
*
* @access public
* @param integer $user_id User id
* @return boolean
*/
public function unlink($user_id)
{
$userModel = new User($this->db, $this->event);
return $userModel->update(array(
'id' => $user_id,
'github_id' => '',
));
}
/**
* Update the user table based on the GitHub profile information
*
* @access public
* @param integer $user_id User id
* @param array $profile GitHub profile
* @return boolean
* @todo Don't overwrite existing email/name with empty GitHub data
*/
public function updateUser($user_id, array $profile)
{
$userModel = new User($this->db, $this->event);
return $userModel->update(array(
'id' => $user_id,
'github_id' => $profile['id'],
'email' => $profile['email'],
'name' => $profile['name'],
));
}
/**
* Get the GitHub service instance
*
* @access public
* @return \OAuth\OAuth2\Service\GitHub
*/
public function getService()
{
$uriFactory = new UriFactory();
$currentUri = $uriFactory->createFromSuperGlobalArray($_SERVER);
$currentUri->setQuery('controller=user&action=gitHub');
$storage = new Session(false);
$credentials = new Credentials(
GITHUB_CLIENT_ID,
GITHUB_CLIENT_SECRET,
$currentUri->getAbsoluteUri()
);
$serviceFactory = new ServiceFactory();
return $serviceFactory->createService(
'gitHub',
$credentials,
$storage,
array('')
);
}
/**
* Get the authorization URL
*
* @access public
* @return \OAuth\Common\Http\Uri\Uri
*/
public function getAuthorizationUrl()
{
return $this->getService()->getAuthorizationUri();
}
/**
* Get GitHub profile information from the API
*
* @access public
* @param string $code GitHub authorization code
* @return bool|array
*/
public function getGitHubProfile($code)
{
try {
$gitHubService = $this->getService();
$gitHubService->requestAccessToken($code);
return json_decode($gitHubService->request('user'), true);
}
catch (TokenResponseException $e) {
return false;
}
return false;
}
/**
* Revokes this user's GitHub tokens for Kanboard
*
* @access public
* @return bool|array
* @todo Currently this simply removes all our tokens for this user, ideally it should
* restrict itself to the one in question
*/
public function revokeGitHubAccess()
{
try {
$gitHubService = $this->getService();
$basicAuthHeader = array('Authorization' => 'Basic ' .
base64_encode(GITHUB_CLIENT_ID.':'.GITHUB_CLIENT_SECRET));
return json_decode($gitHubService->request('/applications/'.GITHUB_CLIENT_ID.'/tokens', 'DELETE', null, $basicAuthHeader), true);
}
catch (TokenResponseException $e) {
return false;
}
return false;
}
}

View file

@ -0,0 +1,152 @@
<?php
namespace Model;
require __DIR__.'/../../vendor/OAuth/bootstrap.php';
use OAuth\Common\Storage\Session;
use OAuth\Common\Consumer\Credentials;
use OAuth\Common\Http\Uri\UriFactory;
use OAuth\ServiceFactory;
use OAuth\Common\Http\Exception\TokenResponseException;
/**
* Google model
*
* @package model
* @author Frederic Guillot
*/
class Google extends Base
{
/**
* Authenticate a Google user
*
* @access public
* @param string $google_id Google unique id
* @return boolean
*/
public function authenticate($google_id)
{
$userModel = new User($this->db, $this->event);
$user = $userModel->getByGoogleId($google_id);
if ($user) {
// Create the user session
$userModel->updateSession($user);
// Update login history
$lastLogin = new LastLogin($this->db, $this->event);
$lastLogin->create(
LastLogin::AUTH_GOOGLE,
$user['id'],
$userModel->getIpAddress(),
$userModel->getUserAgent()
);
return true;
}
return false;
}
/**
* Unlink a Google account for a given user
*
* @access public
* @param integer $user_id User id
* @return boolean
*/
public function unlink($user_id)
{
$userModel = new User($this->db, $this->event);
return $userModel->update(array(
'id' => $user_id,
'google_id' => '',
));
}
/**
* Update the user table based on the Google profile information
*
* @access public
* @param integer $user_id User id
* @param array $profile Google profile
* @return boolean
*/
public function updateUser($user_id, array $profile)
{
$userModel = new User($this->db, $this->event);
return $userModel->update(array(
'id' => $user_id,
'google_id' => $profile['id'],
'email' => $profile['email'],
'name' => $profile['name'],
));
}
/**
* Get the Google service instance
*
* @access public
* @return \OAuth\OAuth2\Service\Google
*/
public function getService()
{
$uriFactory = new UriFactory();
$currentUri = $uriFactory->createFromSuperGlobalArray($_SERVER);
$currentUri->setQuery('controller=user&action=google');
$storage = new Session(false);
$credentials = new Credentials(
GOOGLE_CLIENT_ID,
GOOGLE_CLIENT_SECRET,
$currentUri->getAbsoluteUri()
);
$serviceFactory = new ServiceFactory();
return $serviceFactory->createService(
'google',
$credentials,
$storage,
array('userinfo_email', 'userinfo_profile')
);
}
/**
* Get the authorization URL
*
* @access public
* @return \OAuth\Common\Http\Uri\Uri
*/
public function getAuthorizationUrl()
{
return $this->getService()->getAuthorizationUri();
}
/**
* Get Google profile information from the API
*
* @access public
* @param string $code Google authorization code
* @return bool|array
*/
public function getGoogleProfile($code)
{
try {
$googleService = $this->getService();
$googleService->requestAccessToken($code);
return json_decode($googleService->request('https://www.googleapis.com/oauth2/v1/userinfo'), true);
}
catch (TokenResponseException $e) {
return false;
}
return false;
}
}

View file

@ -0,0 +1,92 @@
<?php
namespace Model;
/**
* LastLogin model
*
* @package model
* @author Frederic Guillot
*/
class LastLogin extends Base
{
/**
* SQL table name
*
* @var string
*/
const TABLE = 'last_logins';
/**
* Number of connections to keep for history
*
* @var integer
*/
const NB_LOGINS = 10;
/**
* Authentication methods
*
* @var string
*/
const AUTH_DATABASE = 'database';
const AUTH_REMEMBER_ME = 'remember_me';
const AUTH_LDAP = 'ldap';
const AUTH_GOOGLE = 'google';
const AUTH_GITHUB = 'github';
/**
* Create a new record
*
* @access public
* @param string $auth_type Authentication method
* @param integer $user_id User id
* @param string $ip IP Address
* @param string $user_agent User Agent
* @return array
*/
public function create($auth_type, $user_id, $ip, $user_agent)
{
// Cleanup old sessions if necessary
$connections = $this->db
->table(self::TABLE)
->eq('user_id', $user_id)
->desc('date_creation')
->findAllByColumn('id');
if (count($connections) >= self::NB_LOGINS) {
$this->db->table(self::TABLE)
->eq('user_id', $user_id)
->notin('id', array_slice($connections, 0, self::NB_LOGINS - 1))
->remove();
}
return $this->db
->table(self::TABLE)
->insert(array(
'auth_type' => $auth_type,
'user_id' => $user_id,
'ip' => $ip,
'user_agent' => $user_agent,
'date_creation' => time(),
));
}
/**
* Get the last connections for a given user
*
* @access public
* @param integer $user_id User id
* @return array
*/
public function getAll($user_id)
{
return $this->db
->table(self::TABLE)
->eq('user_id', $user_id)
->desc('date_creation')
->columns('id', 'auth_type', 'ip', 'user_agent', 'date_creation')
->findAll();
}
}

105
sources/app/Model/Ldap.php Normal file
View file

@ -0,0 +1,105 @@
<?php
namespace Model;
/**
* LDAP model
*
* @package model
* @author Frederic Guillot
*/
class Ldap extends Base
{
/**
* Authenticate a user
*
* @access public
* @param string $username Username
* @param string $password Password
* @return null|boolean
*/
public function authenticate($username, $password)
{
if (! function_exists('ldap_connect')) {
die('The PHP LDAP extension is required');
}
// Skip SSL certificate verification
if (! LDAP_SSL_VERIFY) {
putenv('LDAPTLS_REQCERT=never');
}
$ldap = ldap_connect(LDAP_SERVER, LDAP_PORT);
if (! is_resource($ldap)) {
die('Unable to connect to the LDAP server: "'.LDAP_SERVER.'"');
}
ldap_set_option($ldap, LDAP_OPT_PROTOCOL_VERSION, 3);
ldap_set_option($ldap, LDAP_OPT_REFERRALS, 0);
if (! @ldap_bind($ldap, LDAP_USERNAME, LDAP_PASSWORD)) {
die('Unable to bind to the LDAP server: "'.LDAP_SERVER.'"');
}
$sr = @ldap_search($ldap, LDAP_ACCOUNT_BASE, sprintf(LDAP_USER_PATTERN, $username), array(LDAP_ACCOUNT_FULLNAME, LDAP_ACCOUNT_EMAIL));
if ($sr === false) {
return false;
}
$info = ldap_get_entries($ldap, $sr);
// User not found
if (count($info) == 0 || $info['count'] == 0) {
return false;
}
if (@ldap_bind($ldap, $info[0]['dn'], $password)) {
return $this->create($username, $info[0][LDAP_ACCOUNT_FULLNAME][0], $info[0][LDAP_ACCOUNT_EMAIL][0]);
}
return false;
}
/**
* Create automatically a new local user after the LDAP authentication
*
* @access public
* @param string $username Username
* @param string $name Name of the user
* @param string $email Email address
* @return bool
*/
public function create($username, $name, $email)
{
$userModel = new User($this->db, $this->event);
$user = $userModel->getByUsername($username);
// There is an existing user account
if ($user) {
if ($user['is_ldap_user'] == 1) {
// LDAP user already created
return true;
}
else {
// There is already a local user with that username
return false;
}
}
// Create a LDAP user
$values = array(
'username' => $username,
'name' => $name,
'email' => $email,
'is_admin' => 0,
'is_ldap_user' => 1,
);
return $userModel->create($values);
}
}

View file

@ -0,0 +1,584 @@
<?php
namespace Model;
use SimpleValidator\Validator;
use SimpleValidator\Validators;
use Event\TaskModification;
use Core\Security;
/**
* Project model
*
* @package model
* @author Frederic Guillot
*/
class Project extends Base
{
/**
* SQL table name for projects
*
* @var string
*/
const TABLE = 'projects';
/**
* SQL table name for users
*
* @var string
*/
const TABLE_USERS = 'project_has_users';
/**
* Value for active project
*
* @var integer
*/
const ACTIVE = 1;
/**
* Value for inactive project
*
* @var integer
*/
const INACTIVE = 0;
/**
* Get a list of people that can be assigned for tasks
*
* @access public
* @param integer $project_id Project id
* @param bool $prepend_unassigned Prepend the 'Unassigned' value
* @param bool $prepend_everybody Prepend the 'Everbody' value
* @return array
*/
public function getUsersList($project_id, $prepend_unassigned = true, $prepend_everybody = false)
{
$allowed_users = $this->getAllowedUsers($project_id);
$userModel = new User($this->db, $this->event);
if (empty($allowed_users)) {
$allowed_users = $userModel->getList();
}
if ($prepend_unassigned) {
$allowed_users = array(t('Unassigned')) + $allowed_users;
}
if ($prepend_everybody) {
$allowed_users = array(User::EVERYBODY_ID => t('Everybody')) + $allowed_users;
}
return $allowed_users;
}
/**
* Get a list of allowed people for a project
*
* @access public
* @param integer $project_id Project id
* @return array
*/
public function getAllowedUsers($project_id)
{
return $this->db
->table(self::TABLE_USERS)
->join(User::TABLE, 'id', 'user_id')
->eq('project_id', $project_id)
->asc('username')
->listing('user_id', 'username');
}
/**
* Get allowed and not allowed users for a project
*
* @access public
* @param integer $project_id Project id
* @return array
*/
public function getAllUsers($project_id)
{
$users = array(
'allowed' => array(),
'not_allowed' => array(),
);
$userModel = new User($this->db, $this->event);
$all_users = $userModel->getList();
$users['allowed'] = $this->getAllowedUsers($project_id);
foreach ($all_users as $user_id => $username) {
if (! isset($users['allowed'][$user_id])) {
$users['not_allowed'][$user_id] = $username;
}
}
return $users;
}
/**
* Allow a specific user for a given project
*
* @access public
* @param integer $project_id Project id
* @param integer $user_id User id
* @return bool
*/
public function allowUser($project_id, $user_id)
{
return $this->db
->table(self::TABLE_USERS)
->save(array('project_id' => $project_id, 'user_id' => $user_id));
}
/**
* Revoke a specific user for a given project
*
* @access public
* @param integer $project_id Project id
* @param integer $user_id User id
* @return bool
*/
public function revokeUser($project_id, $user_id)
{
return $this->db
->table(self::TABLE_USERS)
->eq('project_id', $project_id)
->eq('user_id', $user_id)
->remove();
}
/**
* Check if a specific user is allowed to access to a given project
*
* @access public
* @param integer $project_id Project id
* @param integer $user_id User id
* @return bool
*/
public function isUserAllowed($project_id, $user_id)
{
// If there is nobody specified, everybody have access to the project
$nb_users = $this->db
->table(self::TABLE_USERS)
->eq('project_id', $project_id)
->count();
if ($nb_users < 1) return true;
// Check if user has admin rights
$nb_users = $this->db
->table(User::TABLE)
->eq('id', $user_id)
->eq('is_admin', 1)
->count();
if ($nb_users > 0) return true;
// Otherwise, allow only specific users
return (bool) $this->db
->table(self::TABLE_USERS)
->eq('project_id', $project_id)
->eq('user_id', $user_id)
->count();
}
/**
* Get a project by the id
*
* @access public
* @param integer $project_id Project id
* @return array
*/
public function getById($project_id)
{
return $this->db->table(self::TABLE)->eq('id', $project_id)->findOne();
}
/**
* Get a project by the name
*
* @access public
* @param string $project_name Project name
* @return array
*/
public function getByName($project_name)
{
return $this->db->table(self::TABLE)->eq('name', $project_name)->findOne();
}
/**
* Fetch project data by using the token
*
* @access public
* @param string $token Token
* @return array
*/
public function getByToken($token)
{
return $this->db->table(self::TABLE)->eq('token', $token)->findOne();
}
/**
* Return the first project from the database (no sorting)
*
* @access public
* @return array
*/
public function getFirst()
{
return $this->db->table(self::TABLE)->findOne();
}
/**
* Get all projects, optionaly fetch stats for each project and can check users permissions
*
* @access public
* @param bool $fetch_stats If true, return metrics about each projects
* @param bool $check_permissions If true, remove projects not allowed for the current user
* @return array
*/
public function getAll($fetch_stats = false, $check_permissions = false)
{
if (! $fetch_stats) {
return $this->db->table(self::TABLE)->asc('name')->findAll();
}
$this->db->startTransaction();
$projects = $this->db
->table(self::TABLE)
->asc('name')
->findAll();
$boardModel = new Board($this->db, $this->event);
$taskModel = new Task($this->db, $this->event);
$aclModel = new Acl($this->db, $this->event);
foreach ($projects as $pkey => &$project) {
if ($check_permissions && ! $this->isUserAllowed($project['id'], $aclModel->getUserId())) {
unset($projects[$pkey]);
}
else {
$columns = $boardModel->getcolumns($project['id']);
$project['nb_active_tasks'] = 0;
foreach ($columns as &$column) {
$column['nb_active_tasks'] = $taskModel->countByColumnId($project['id'], $column['id']);
$project['nb_active_tasks'] += $column['nb_active_tasks'];
}
$project['columns'] = $columns;
$project['nb_tasks'] = $taskModel->countByProjectId($project['id']);
$project['nb_inactive_tasks'] = $project['nb_tasks'] - $project['nb_active_tasks'];
}
}
$this->db->closeTransaction();
return $projects;
}
/**
* Return the list of all projects
*
* @access public
* @param bool $prepend If true, prepend to the list the value 'None'
* @return array
*/
public function getList($prepend = true)
{
if ($prepend) {
return array(t('None')) + $this->db->table(self::TABLE)->asc('name')->listing('id', 'name');
}
return $this->db->table(self::TABLE)->asc('name')->listing('id', 'name');
}
/**
* Get all projects with all its data for a given status
*
* @access public
* @param integer $status Proejct status: self::ACTIVE or self:INACTIVE
* @return array
*/
public function getAllByStatus($status)
{
return $this->db
->table(self::TABLE)
->asc('name')
->eq('is_active', $status)
->findAll();
}
/**
* Get a list of project by status
*
* @access public
* @param integer $status Project status: self::ACTIVE or self:INACTIVE
* @return array
*/
public function getListByStatus($status)
{
return $this->db
->table(self::TABLE)
->asc('name')
->eq('is_active', $status)
->listing('id', 'name');
}
/**
* Return the number of projects by status
*
* @access public
* @param integer $status Status: self::ACTIVE or self:INACTIVE
* @return integer
*/
public function countByStatus($status)
{
return $this->db
->table(self::TABLE)
->eq('is_active', $status)
->count();
}
/**
* Filter a list of projects for a given user
*
* @access public
* @param array $projects Project list: ['project_id' => 'project_name']
* @param integer $user_id User id
* @return array
*/
public function filterListByAccess(array $projects, $user_id)
{
foreach ($projects as $project_id => $project_name) {
if (! $this->isUserAllowed($project_id, $user_id)) {
unset($projects[$project_id]);
}
}
return $projects;
}
/**
* Return a list of projects for a given user
*
* @access public
* @param integer $user_id User id
* @return array
*/
public function getAvailableList($user_id)
{
return $this->filterListByAccess($this->getListByStatus(self::ACTIVE), $user_id);
}
/**
* Create a project
*
* @access public
* @param array $values Form values
* @return integer Project id
*/
public function create(array $values)
{
$this->db->startTransaction();
$values['token'] = Security::generateToken();
if (! $this->db->table(self::TABLE)->save($values)) {
$this->db->cancelTransaction();
return false;
}
$project_id = $this->db->getConnection()->getLastId();
$boardModel = new Board($this->db, $this->event);
$boardModel->create($project_id, array(
t('Backlog'),
t('Ready'),
t('Work in progress'),
t('Done'),
));
$this->db->closeTransaction();
return (int) $project_id;
}
/**
* Check if the project have been modified
*
* @access public
* @param integer $project_id Project id
* @param integer $timestamp Timestamp
* @return bool
*/
public function isModifiedSince($project_id, $timestamp)
{
return (bool) $this->db->table(self::TABLE)
->eq('id', $project_id)
->gt('last_modified', $timestamp)
->count();
}
/**
* Update modification date
*
* @access public
* @param integer $project_id Project id
* @return bool
*/
public function updateModificationDate($project_id)
{
return $this->db->table(self::TABLE)->eq('id', $project_id)->save(array(
'last_modified' => time()
));
}
/**
* Update a project
*
* @access public
* @param array $values Form values
* @return bool
*/
public function update(array $values)
{
return $this->db->table(self::TABLE)->eq('id', $values['id'])->save($values);
}
/**
* Remove a project
*
* @access public
* @param integer $project_id Project id
* @return bool
*/
public function remove($project_id)
{
return $this->db->table(self::TABLE)->eq('id', $project_id)->remove();
}
/**
* Enable a project
*
* @access public
* @param integer $project_id Project id
* @return bool
*/
public function enable($project_id)
{
return $this->db
->table(self::TABLE)
->eq('id', $project_id)
->save(array('is_active' => 1));
}
/**
* Disable a project
*
* @access public
* @param integer $project_id Project id
* @return bool
*/
public function disable($project_id)
{
return $this->db
->table(self::TABLE)
->eq('id', $project_id)
->save(array('is_active' => 0));
}
/**
* Validate project creation
*
* @access public
* @param array $values Form values
* @return array $valid, $errors [0] = Success or not, [1] = List of errors
*/
public function validateCreation(array $values)
{
$v = new Validator($values, array(
new Validators\Required('name', t('The project name is required')),
new Validators\MaxLength('name', t('The maximum length is %d characters', 50), 50),
new Validators\Unique('name', t('This project must be unique'), $this->db->getConnection(), self::TABLE)
));
return array(
$v->execute(),
$v->getErrors()
);
}
/**
* Validate project modification
*
* @access public
* @param array $values Form values
* @return array $valid, $errors [0] = Success or not, [1] = List of errors
*/
public function validateModification(array $values)
{
$v = new Validator($values, array(
new Validators\Required('id', t('The project id is required')),
new Validators\Integer('id', t('This value must be an integer')),
new Validators\Required('name', t('The project name is required')),
new Validators\MaxLength('name', t('The maximum length is %d characters', 50), 50),
new Validators\Unique('name', t('This project must be unique'), $this->db->getConnection(), self::TABLE),
new Validators\Integer('is_active', t('This value must be an integer'))
));
return array(
$v->execute(),
$v->getErrors()
);
}
/**
* Validate allowed users
*
* @access public
* @param array $values Form values
* @return array $valid, $errors [0] = Success or not, [1] = List of errors
*/
public function validateUserAccess(array $values)
{
$v = new Validator($values, array(
new Validators\Required('project_id', t('The project id is required')),
new Validators\Integer('project_id', t('This value must be an integer')),
new Validators\Required('user_id', t('The user id is required')),
new Validators\Integer('user_id', t('This value must be an integer')),
));
return array(
$v->execute(),
$v->getErrors()
);
}
/**
* Attach events
*
* @access public
*/
public function attachEvents()
{
$events = array(
Task::EVENT_UPDATE,
Task::EVENT_CREATE,
Task::EVENT_CLOSE,
Task::EVENT_OPEN,
);
$listener = new TaskModification($this);
foreach ($events as $event_name) {
$this->event->attach($event_name, $listener);
}
}
}

View file

@ -0,0 +1,337 @@
<?php
namespace Model;
use Core\Security;
/**
* RememberMe model
*
* @package model
* @author Frederic Guillot
*/
class RememberMe extends Base
{
/**
* SQL table name
*
* @var string
*/
const TABLE = 'remember_me';
/**
* Cookie name
*
* @var string
*/
const COOKIE_NAME = '__R';
/**
* Expiration (60 days)
*
* @var integer
*/
const EXPIRATION = 5184000;
/**
* Get a remember me record
*
* @access public
* @param $token
* @param $sequence
* @return mixed
*/
public function find($token, $sequence)
{
return $this->db
->table(self::TABLE)
->eq('token', $token)
->eq('sequence', $sequence)
->gt('expiration', time())
->findOne();
}
/**
* Get all sessions for a given user
*
* @access public
* @param integer $user_id User id
* @return array
*/
public function getAll($user_id)
{
return $this->db
->table(self::TABLE)
->eq('user_id', $user_id)
->desc('date_creation')
->columns('id', 'ip', 'user_agent', 'date_creation', 'expiration')
->findAll();
}
/**
* Authenticate the user with the cookie
*
* @access public
* @return bool
*/
public function authenticate()
{
$credentials = $this->readCookie();
if ($credentials !== false) {
$record = $this->find($credentials['token'], $credentials['sequence']);
if ($record) {
// Update the sequence
$this->writeCookie(
$record['token'],
$this->update($record['token'], $record['sequence']),
$record['expiration']
);
// Create the session
$user = new User($this->db, $this->event);
$acl = new Acl($this->db, $this->event);
$user->updateSession($user->getById($record['user_id']));
$acl->isRememberMe(true);
return true;
}
}
return false;
}
/**
* Update the database and the cookie with a new sequence
*
* @access public
*/
public function refresh()
{
$credentials = $this->readCookie();
if ($credentials !== false) {
$record = $this->find($credentials['token'], $credentials['sequence']);
if ($record) {
// Update the sequence
$this->writeCookie(
$record['token'],
$this->update($record['token'], $record['sequence']),
$record['expiration']
);
}
}
}
/**
* Remove a session record
*
* @access public
* @param integer $session_id Session id
* @return mixed
*/
public function remove($session_id)
{
return $this->db
->table(self::TABLE)
->eq('id', $session_id)
->remove();
}
/**
* Remove the current RememberMe session and the cookie
*
* @access public
* @param integer $user_id User id
*/
public function destroy($user_id)
{
$credentials = $this->readCookie();
if ($credentials !== false) {
$this->deleteCookie();
$this->db
->table(self::TABLE)
->eq('user_id', $user_id)
->eq('token', $credentials['token'])
->remove();
}
}
/**
* Create a new RememberMe session
*
* @access public
* @param integer $user_id User id
* @param string $ip IP Address
* @param string $user_agent User Agent
* @return array
*/
public function create($user_id, $ip, $user_agent)
{
$token = hash('sha256', $user_id.$user_agent.$ip.Security::generateToken());
$sequence = Security::generateToken();
$expiration = time() + self::EXPIRATION;
$this->cleanup($user_id);
$this->db
->table(self::TABLE)
->insert(array(
'user_id' => $user_id,
'ip' => $ip,
'user_agent' => $user_agent,
'token' => $token,
'sequence' => $sequence,
'expiration' => $expiration,
'date_creation' => time(),
));
return array(
'token' => $token,
'sequence' => $sequence,
'expiration' => $expiration,
);
}
/**
* Remove old sessions for a given user
*
* @access public
* @param integer $user_id User id
* @return bool
*/
public function cleanup($user_id)
{
return $this->db
->table(self::TABLE)
->eq('user_id', $user_id)
->lt('expiration', time())
->remove();
}
/**
* Return a new sequence token and update the database
*
* @access public
* @param string $token Session token
* @param string $sequence Sequence token
* @return string
*/
public function update($token, $sequence)
{
$new_sequence = Security::generateToken();
$this->db
->table(self::TABLE)
->eq('token', $token)
->eq('sequence', $sequence)
->update(array('sequence' => $new_sequence));
return $new_sequence;
}
/**
* Encode the cookie
*
* @access public
* @param string $token Session token
* @param string $sequence Sequence token
* @return string
*/
public function encodeCookie($token, $sequence)
{
return implode('|', array($token, $sequence));
}
/**
* Decode the value of a cookie
*
* @access public
* @param string $value Raw cookie data
* @return array
*/
public function decodeCookie($value)
{
list($token, $sequence) = explode('|', $value);
return array(
'token' => $token,
'sequence' => $sequence,
);
}
/**
* Return true if the current user has a RememberMe cookie
*
* @access public
* @return bool
*/
public function hasCookie()
{
return ! empty($_COOKIE[self::COOKIE_NAME]);
}
/**
* Write and encode the cookie
*
* @access public
* @param string $token Session token
* @param string $sequence Sequence token
* @param string $expiration Cookie expiration
*/
public function writeCookie($token, $sequence, $expiration)
{
setcookie(
self::COOKIE_NAME,
$this->encodeCookie($token, $sequence),
$expiration,
BASE_URL_DIRECTORY,
null,
! empty($_SERVER['HTTPS']),
true
);
}
/**
* Read and decode the cookie
*
* @access public
* @return mixed
*/
public function readCookie()
{
if (empty($_COOKIE[self::COOKIE_NAME])) {
return false;
}
return $this->decodeCookie($_COOKIE[self::COOKIE_NAME]);
}
/**
* Remove the cookie
*
* @access public
*/
public function deleteCookie()
{
setcookie(
self::COOKIE_NAME,
'',
time() - 3600,
BASE_URL_DIRECTORY,
null,
! empty($_SERVER['HTTPS']),
true
);
}
}

View file

@ -0,0 +1,179 @@
<?php
namespace Model;
use SimpleValidator\Validator;
use SimpleValidator\Validators;
/**
* Subtask model
*
* @package model
* @author Frederic Guillot
*/
class SubTask extends Base
{
/**
* SQL table name
*
* @var string
*/
const TABLE = 'task_has_subtasks';
/**
* Task "done" status
*
* @var integer
*/
const STATUS_DONE = 2;
/**
* Task "in progress" status
*
* @var integer
*/
const STATUS_INPROGRESS = 1;
/**
* Task "todo" status
*
* @var integer
*/
const STATUS_TODO = 0;
/**
* Get available status
*
* @access public
* @return array
*/
public function getStatusList()
{
$status = array(
self::STATUS_TODO => t('Todo'),
self::STATUS_INPROGRESS => t('In progress'),
self::STATUS_DONE => t('Done'),
);
asort($status);
return $status;
}
/**
* Get all subtasks for a given task
*
* @access public
* @param integer $task_id Task id
* @return array
*/
public function getAll($task_id)
{
$status = $this->getStatusList();
$subtasks = $this->db->table(self::TABLE)
->eq('task_id', $task_id)
->columns(self::TABLE.'.*', User::TABLE.'.username')
->join(User::TABLE, 'id', 'user_id')
->findAll();
foreach ($subtasks as &$subtask) {
$subtask['status_name'] = $status[$subtask['status']];
}
return $subtasks;
}
/**
* Get a subtask by the id
*
* @access public
* @param integer $subtask_id Subtask id
* @return array
*/
public function getById($subtask_id)
{
return $this->db->table(self::TABLE)->eq('id', $subtask_id)->findOne();
}
/**
* Create
*
* @access public
* @param array $values Form values
* @return bool
*/
public function create(array $values)
{
if (isset($values['another_subtask'])) {
unset($values['another_subtask']);
}
if (isset($values['time_estimated']) && empty($values['time_estimated'])) {
$values['time_estimated'] = 0;
}
if (isset($values['time_spent']) && empty($values['time_spent'])) {
$values['time_spent'] = 0;
}
return $this->db->table(self::TABLE)->save($values);
}
/**
* Update
*
* @access public
* @param array $values Form values
* @return bool
*/
public function update(array $values)
{
if (isset($values['time_estimated']) && empty($values['time_estimated'])) {
$values['time_estimated'] = 0;
}
if (isset($values['time_spent']) && empty($values['time_spent'])) {
$values['time_spent'] = 0;
}
return $this->db->table(self::TABLE)->eq('id', $values['id'])->save($values);
}
/**
* Remove
*
* @access public
* @param integer $subtask_id Subtask id
* @return bool
*/
public function remove($subtask_id)
{
return $this->db->table(self::TABLE)->eq('id', $subtask_id)->remove();
}
/**
* Validate creation/modification
*
* @access public
* @param array $values Form values
* @return array $valid, $errors [0] = Success or not, [1] = List of errors
*/
public function validate(array $values)
{
$v = new Validator($values, array(
new Validators\Required('task_id', t('The task id is required')),
new Validators\Integer('task_id', t('The task id must be an integer')),
new Validators\Required('title', t('The title is required')),
new Validators\MaxLength('title', t('The maximum length is %d characters', 100), 100),
new Validators\Integer('user_id', t('The user id must be an integer')),
new Validators\Integer('status', t('The status must be an integer')),
new Validators\Numeric('time_estimated', t('The time must be a numeric value')),
new Validators\Numeric('time_spent', t('The time must be a numeric value')),
));
return array(
$v->execute(),
$v->getErrors()
);
}
}

670
sources/app/Model/Task.php Normal file
View file

@ -0,0 +1,670 @@
<?php
namespace Model;
use SimpleValidator\Validator;
use SimpleValidator\Validators;
use DateTime;
/**
* Task model
*
* @package model
* @author Frederic Guillot
*/
class Task extends Base
{
/**
* SQL table name
*
* @var string
*/
const TABLE = 'tasks';
/**
* Task status
*
* @var integer
*/
const STATUS_OPEN = 1;
const STATUS_CLOSED = 0;
/**
* Events
*
* @var string
*/
const EVENT_MOVE_COLUMN = 'task.move.column';
const EVENT_MOVE_POSITION = 'task.move.position';
const EVENT_UPDATE = 'task.update';
const EVENT_CREATE = 'task.create';
const EVENT_CLOSE = 'task.close';
const EVENT_OPEN = 'task.open';
const EVENT_CREATE_UPDATE = 'task.create_update';
/**
* Get available colors
*
* @access public
* @return array
*/
public function getColors()
{
return array(
'yellow' => t('Yellow'),
'blue' => t('Blue'),
'green' => t('Green'),
'purple' => t('Purple'),
'red' => t('Red'),
'orange' => t('Orange'),
'grey' => t('Grey'),
);
}
/**
* Fetch one task
*
* @access public
* @param integer $task_id Task id
* @param boolean $more If true, fetch all related information
* @return array
*/
public function getById($task_id, $more = false)
{
if ($more) {
$sql = '
SELECT
tasks.id,
tasks.title,
tasks.description,
tasks.date_creation,
tasks.date_completed,
tasks.date_modification,
tasks.date_due,
tasks.color_id,
tasks.project_id,
tasks.column_id,
tasks.owner_id,
tasks.creator_id,
tasks.position,
tasks.is_active,
tasks.score,
tasks.category_id,
project_has_categories.name AS category_name,
projects.name AS project_name,
columns.title AS column_title,
users.username AS assignee_username,
creators.username AS creator_username
FROM tasks
LEFT JOIN users ON users.id = tasks.owner_id
LEFT JOIN users AS creators ON creators.id = tasks.creator_id
LEFT JOIN project_has_categories ON project_has_categories.id = tasks.category_id
LEFT JOIN projects ON projects.id = tasks.project_id
LEFT JOIN columns ON columns.id = tasks.column_id
WHERE tasks.id = ?
';
$rq = $this->db->execute($sql, array($task_id));
return $rq->fetch(\PDO::FETCH_ASSOC);
}
else {
return $this->db->table(self::TABLE)->eq('id', $task_id)->findOne();
}
}
/**
* Count all tasks for a given project and status
*
* @access public
* @param integer $project_id Project id
* @param array $status List of status id
* @return array
*/
public function getAll($project_id, array $status = array(self::STATUS_OPEN, self::STATUS_CLOSED))
{
return $this->db
->table(self::TABLE)
->eq('project_id', $project_id)
->in('is_active', $status)
->findAll();
}
/**
* Count all tasks for a given project and status
*
* @access public
* @param integer $project_id Project id
* @param array $status List of status id
* @return integer
*/
public function countByProjectId($project_id, array $status = array(self::STATUS_OPEN, self::STATUS_CLOSED))
{
return $this->db
->table(self::TABLE)
->eq('project_id', $project_id)
->in('is_active', $status)
->count();
}
/**
* Get tasks that match defined filters
*
* @access public
* @param array $filters Filters: [ ['column' => '...', 'operator' => '...', 'value' => '...'], ... ]
* @param array $sorting Sorting: [ 'column' => 'date_creation', 'direction' => 'asc']
* @return array
*/
public function find(array $filters, array $sorting = array())
{
$table = $this->db
->table(self::TABLE)
->columns(
'(SELECT count(*) FROM comments WHERE task_id=tasks.id) AS nb_comments',
'(SELECT count(*) FROM task_has_files WHERE task_id=tasks.id) AS nb_files',
'tasks.id',
'tasks.title',
'tasks.description',
'tasks.date_creation',
'tasks.date_completed',
'tasks.date_due',
'tasks.color_id',
'tasks.project_id',
'tasks.column_id',
'tasks.owner_id',
'tasks.position',
'tasks.is_active',
'tasks.score',
'tasks.category_id',
'users.username'
)
->join('users', 'id', 'owner_id');
foreach ($filters as $key => $filter) {
if ($key === 'or') {
$table->beginOr();
foreach ($filter as $subfilter) {
$table->$subfilter['operator']($subfilter['column'], $subfilter['value']);
}
$table->closeOr();
}
else if (isset($filter['operator']) && isset($filter['column']) && isset($filter['value'])) {
$table->$filter['operator']($filter['column'], $filter['value']);
}
}
if (empty($sorting)) {
$table->orderBy('tasks.position', 'ASC');
}
else {
$table->orderBy($sorting['column'], $sorting['direction']);
}
return $table->findAll();
}
/**
* Count the number of tasks for a given column and status
*
* @access public
* @param integer $project_id Project id
* @param integer $column_id Column id
* @param array $status List of status id
* @return integer
*/
public function countByColumnId($project_id, $column_id, array $status = array(self::STATUS_OPEN))
{
return $this->db
->table(self::TABLE)
->eq('project_id', $project_id)
->eq('column_id', $column_id)
->in('is_active', $status)
->count();
}
/**
* Duplicate a task
*
* @access public
* @param integer $task_id Task id
* @return boolean
*/
public function duplicate($task_id)
{
$this->db->startTransaction();
// Get the original task
$task = $this->getById($task_id);
// Cleanup data
unset($task['id']);
unset($task['date_completed']);
// Assign new values
$task['date_creation'] = time();
$task['is_active'] = 1;
$task['position'] = $this->countByColumnId($task['project_id'], $task['column_id']);
// Save task
if (! $this->db->table(self::TABLE)->save($task)) {
$this->db->cancelTransaction();
return false;
}
$task_id = $this->db->getConnection()->getLastId();
$this->db->closeTransaction();
// Trigger events
$this->event->trigger(self::EVENT_CREATE_UPDATE, array('task_id' => $task_id) + $task);
$this->event->trigger(self::EVENT_CREATE, array('task_id' => $task_id) + $task);
return $task_id;
}
/**
* Duplicate a task to another project (always copy to the first column)
*
* @access public
* @param integer $task_id Task id
* @param integer $project_id Destination project id
* @return boolean
*/
public function duplicateToAnotherProject($task_id, $project_id)
{
$this->db->startTransaction();
$boardModel = new Board($this->db, $this->event);
// Get the original task
$task = $this->getById($task_id);
// Cleanup data
unset($task['id']);
unset($task['date_completed']);
// Assign new values
$task['date_creation'] = time();
$task['owner_id'] = 0;
$task['category_id'] = 0;
$task['is_active'] = 1;
$task['column_id'] = $boardModel->getFirstColumn($project_id);
$task['project_id'] = $project_id;
$task['position'] = $this->countByColumnId($task['project_id'], $task['column_id']);
// Save task
if (! $this->db->table(self::TABLE)->save($task)) {
$this->db->cancelTransaction();
return false;
}
$task_id = $this->db->getConnection()->getLastId();
$this->db->closeTransaction();
// Trigger events
$this->event->trigger(self::EVENT_CREATE_UPDATE, array('task_id' => $task_id) + $task);
$this->event->trigger(self::EVENT_CREATE, array('task_id' => $task_id) + $task);
return $task_id;
}
/**
* Create a task
*
* @access public
* @param array $values Form values
* @return boolean
*/
public function create(array $values)
{
$this->db->startTransaction();
// Prepare data
if (isset($values['another_task'])) {
unset($values['another_task']);
}
if (! empty($values['date_due']) && ! is_numeric($values['date_due'])) {
$values['date_due'] = $this->parseDate($values['date_due']);
}
else {
$values['date_due'] = 0;
}
if (empty($values['score'])) {
$values['score'] = 0;
}
$values['date_creation'] = time();
$values['position'] = $this->countByColumnId($values['project_id'], $values['column_id']);
// Save task
if (! $this->db->table(self::TABLE)->save($values)) {
$this->db->cancelTransaction();
return false;
}
$task_id = $this->db->getConnection()->getLastId();
$this->db->closeTransaction();
// Trigger events
$this->event->trigger(self::EVENT_CREATE_UPDATE, array('task_id' => $task_id) + $values);
$this->event->trigger(self::EVENT_CREATE, array('task_id' => $task_id) + $values);
return $task_id;
}
/**
* Update a task
*
* @access public
* @param array $values Form values
* @return boolean
*/
public function update(array $values)
{
// Prepare data
if (! empty($values['date_due']) && ! is_numeric($values['date_due'])) {
$values['date_due'] = $this->parseDate($values['date_due']);
}
// Force integer fields at 0 (for Postgresql)
if (isset($values['date_due']) && empty($values['date_due'])) {
$values['date_due'] = 0;
}
if (isset($values['score']) && empty($values['score'])) {
$values['score'] = 0;
}
$original_task = $this->getById($values['id']);
if ($original_task === false) {
return false;
}
$updated_task = $values;
$updated_task['date_modification'] = time();
unset($updated_task['id']);
$result = $this->db->table(self::TABLE)->eq('id', $values['id'])->update($updated_task);
// Trigger events
if ($result) {
$events = array(
self::EVENT_CREATE_UPDATE,
self::EVENT_UPDATE,
);
if (isset($values['column_id']) && $original_task['column_id'] != $values['column_id']) {
$events[] = self::EVENT_MOVE_COLUMN;
}
else if (isset($values['position']) && $original_task['position'] != $values['position']) {
$events[] = self::EVENT_MOVE_POSITION;
}
$event_data = array_merge($original_task, $values);
$event_data['task_id'] = $original_task['id'];
foreach ($events as $event) {
$this->event->trigger($event, $event_data);
}
}
return $result;
}
/**
* Mark a task closed
*
* @access public
* @param integer $task_id Task id
* @return boolean
*/
public function close($task_id)
{
$result = $this->db
->table(self::TABLE)
->eq('id', $task_id)
->update(array(
'is_active' => 0,
'date_completed' => time()
));
if ($result) {
$this->event->trigger(self::EVENT_CLOSE, array('task_id' => $task_id) + $this->getById($task_id));
}
return $result;
}
/**
* Mark a task open
*
* @access public
* @param integer $task_id Task id
* @return boolean
*/
public function open($task_id)
{
$result = $this->db
->table(self::TABLE)
->eq('id', $task_id)
->update(array(
'is_active' => 1,
'date_completed' => ''
));
if ($result) {
$this->event->trigger(self::EVENT_OPEN, array('task_id' => $task_id) + $this->getById($task_id));
}
return $result;
}
/**
* Remove a task
*
* @access public
* @param integer $task_id Task id
* @return boolean
*/
public function remove($task_id)
{
$file = new File($this->db, $this->event);
$file->removeAll($task_id);
return $this->db->table(self::TABLE)->eq('id', $task_id)->remove();
}
/**
* Move a task to another column or to another position
*
* @access public
* @param integer $task_id Task id
* @param integer $column_id Column id
* @param integer $position Position (must be greater than 1)
* @return boolean
*/
public function move($task_id, $column_id, $position)
{
$this->event->clearTriggeredEvents();
return $this->update(array(
'id' => $task_id,
'column_id' => $column_id,
'position' => $position,
));
}
/**
* Validate task creation
*
* @access public
* @param array $values Form values
* @return array $valid, $errors [0] = Success or not, [1] = List of errors
*/
public function validateCreation(array $values)
{
$v = new Validator($values, array(
new Validators\Required('color_id', t('The color is required')),
new Validators\Required('project_id', t('The project is required')),
new Validators\Integer('project_id', t('This value must be an integer')),
new Validators\Required('column_id', t('The column is required')),
new Validators\Integer('column_id', t('This value must be an integer')),
new Validators\Integer('owner_id', t('This value must be an integer')),
new Validators\Integer('creator_id', t('This value must be an integer')),
new Validators\Integer('score', t('This value must be an integer')),
new Validators\Required('title', t('The title is required')),
new Validators\MaxLength('title', t('The maximum length is %d characters', 200), 200),
new Validators\Date('date_due', t('Invalid date'), $this->getDateFormats()),
));
return array(
$v->execute(),
$v->getErrors()
);
}
/**
* Validate description creation
*
* @access public
* @param array $values Form values
* @return array $valid, $errors [0] = Success or not, [1] = List of errors
*/
public function validateDescriptionCreation(array $values)
{
$v = new Validator($values, array(
new Validators\Required('id', t('The id is required')),
new Validators\Integer('id', t('This value must be an integer')),
new Validators\Required('description', t('The description is required')),
));
return array(
$v->execute(),
$v->getErrors()
);
}
/**
* Validate task modification
*
* @access public
* @param array $values Form values
* @return array $valid, $errors [0] = Success or not, [1] = List of errors
*/
public function validateModification(array $values)
{
$v = new Validator($values, array(
new Validators\Required('id', t('The id is required')),
new Validators\Integer('id', t('This value must be an integer')),
new Validators\Required('color_id', t('The color is required')),
new Validators\Required('project_id', t('The project is required')),
new Validators\Integer('project_id', t('This value must be an integer')),
new Validators\Required('column_id', t('The column is required')),
new Validators\Integer('column_id', t('This value must be an integer')),
new Validators\Integer('owner_id', t('This value must be an integer')),
new Validators\Integer('score', t('This value must be an integer')),
new Validators\Required('title', t('The title is required')),
new Validators\MaxLength('title', t('The maximum length is %d characters', 200), 200),
new Validators\Date('date_due', t('Invalid date'), $this->getDateFormats()),
));
return array(
$v->execute(),
$v->getErrors()
);
}
/**
* Validate assignee change
*
* @access public
* @param array $values Form values
* @return array $valid, $errors [0] = Success or not, [1] = List of errors
*/
public function validateAssigneeModification(array $values)
{
$v = new Validator($values, array(
new Validators\Required('id', t('The id is required')),
new Validators\Integer('id', t('This value must be an integer')),
new Validators\Required('project_id', t('The project is required')),
new Validators\Integer('project_id', t('This value must be an integer')),
new Validators\Required('owner_id', t('This value is required')),
new Validators\Integer('owner_id', t('This value must be an integer')),
));
return array(
$v->execute(),
$v->getErrors()
);
}
/**
* Return a timestamp if the given date format is correct otherwise return 0
*
* @access public
* @param string $value Date to parse
* @param string $format Date format
* @return integer
*/
public function getValidDate($value, $format)
{
$date = DateTime::createFromFormat($format, $value);
if ($date !== false) {
$errors = DateTime::getLastErrors();
if ($errors['error_count'] === 0 && $errors['warning_count'] === 0) {
$timestamp = $date->getTimestamp();
return $timestamp > 0 ? $timestamp : 0;
}
}
return 0;
}
/**
* Parse a date ad return a unix timestamp, try different date formats
*
* @access public
* @param string $value Date to parse
* @return integer
*/
public function parseDate($value)
{
foreach ($this->getDateFormats() as $format) {
$timestamp = $this->getValidDate($value, $format);
if ($timestamp !== 0) {
return $timestamp;
}
}
return null;
}
/**
* Return the list of supported date formats
*
* @access public
* @return array
*/
public function getDateFormats()
{
return array(
t('m/d/Y'),
'Y-m-d',
'Y_m_d',
);
}
}

450
sources/app/Model/User.php Normal file
View file

@ -0,0 +1,450 @@
<?php
namespace Model;
use SimpleValidator\Validator;
use SimpleValidator\Validators;
/**
* User model
*
* @package model
* @author Frederic Guillot
*/
class User extends Base
{
/**
* SQL table name
*
* @var string
*/
const TABLE = 'users';
/**
* Id used for everbody (filtering)
*
* @var integer
*/
const EVERYBODY_ID = -1;
/**
* Get a specific user by id
*
* @access public
* @param integer $user_id User id
* @return array
*/
public function getById($user_id)
{
return $this->db->table(self::TABLE)->eq('id', $user_id)->findOne();
}
/**
* Get a specific user by the Google id
*
* @access public
* @param string $google_id Google unique id
* @return array
*/
public function getByGoogleId($google_id)
{
return $this->db->table(self::TABLE)->eq('google_id', $google_id)->findOne();
}
/**
* Get a specific user by the GitHub id
*
* @access public
* @param string $github_id GitHub user id
* @return array
*/
public function getByGitHubId($github_id)
{
return $this->db->table(self::TABLE)->eq('github_id', $github_id)->findOne();
}
/**
* Get a specific user by the username
*
* @access public
* @param string $username Username
* @return array
*/
public function getByUsername($username)
{
return $this->db->table(self::TABLE)->eq('username', $username)->findOne();
}
/**
* Get all users
*
* @access public
* @return array
*/
public function getAll()
{
return $this->db
->table(self::TABLE)
->asc('username')
->columns('id', 'username', 'name', 'email', 'is_admin', 'default_project_id', 'is_ldap_user')
->findAll();
}
/**
* List all users (key-value pairs with id/username)
*
* @access public
* @return array
*/
public function getList()
{
return $this->db->table(self::TABLE)->asc('username')->listing('id', 'username');
}
/**
* Add a new user in the database
*
* @access public
* @param array $values Form values
* @return boolean
*/
public function create(array $values)
{
if (isset($values['confirmation'])) {
unset($values['confirmation']);
}
if (isset($values['password'])) {
$values['password'] = \password_hash($values['password'], PASSWORD_BCRYPT);
}
if (empty($values['is_admin'])) {
$values['is_admin'] = 0;
}
if (empty($values['is_ldap_user'])) {
$values['is_ldap_user'] = 0;
}
return $this->db->table(self::TABLE)->save($values);
}
/**
* Modify a new user
*
* @access public
* @param array $values Form values
* @return array
*/
public function update(array $values)
{
if (! empty($values['password'])) {
$values['password'] = \password_hash($values['password'], PASSWORD_BCRYPT);
}
else {
unset($values['password']);
}
if (isset($values['confirmation'])) {
unset($values['confirmation']);
}
if (isset($values['current_password'])) {
unset($values['current_password']);
}
if (empty($values['is_admin'])) {
$values['is_admin'] = 0;
}
if (empty($values['is_ldap_user'])) {
$values['is_ldap_user'] = 0;
}
$result = $this->db->table(self::TABLE)->eq('id', $values['id'])->update($values);
if (session_id() !== '' && $_SESSION['user']['id'] == $values['id']) {
$this->updateSession();
}
return $result;
}
/**
* Remove a specific user
*
* @access public
* @param integer $user_id User id
* @return boolean
*/
public function remove($user_id)
{
$this->db->startTransaction();
// All tasks assigned to this user will be unassigned
$this->db->table(Task::TABLE)->eq('owner_id', $user_id)->update(array('owner_id' => ''));
$this->db->table(self::TABLE)->eq('id', $user_id)->remove();
$this->db->closeTransaction();
return true;
}
/**
* Update user session information
*
* @access public
* @param array $user User data
*/
public function updateSession(array $user = array())
{
if (empty($user)) {
$user = $this->getById($_SESSION['user']['id']);
}
if (isset($user['password'])) {
unset($user['password']);
}
$user['id'] = (int) $user['id'];
$user['default_project_id'] = (int) $user['default_project_id'];
$user['is_admin'] = (bool) $user['is_admin'];
$user['is_ldap_user'] = (bool) $user['is_ldap_user'];
$_SESSION['user'] = $user;
}
/**
* Validate user creation
*
* @access public
* @param array $values Form values
* @return array $valid, $errors [0] = Success or not, [1] = List of errors
*/
public function validateCreation(array $values)
{
$v = new Validator($values, array(
new Validators\Required('username', t('The username is required')),
new Validators\MaxLength('username', t('The maximum length is %d characters', 50), 50),
new Validators\Unique('username', t('The username must be unique'), $this->db->getConnection(), self::TABLE, 'id'),
new Validators\Required('password', t('The password is required')),
new Validators\MinLength('password', t('The minimum length is %d characters', 6), 6),
new Validators\Required('confirmation', t('The confirmation is required')),
new Validators\Equals('password', 'confirmation', t('Passwords don\'t match')),
new Validators\Integer('default_project_id', t('This value must be an integer')),
new Validators\Integer('is_admin', t('This value must be an integer')),
new Validators\Email('email', t('Email address invalid')),
));
return array(
$v->execute(),
$v->getErrors()
);
}
/**
* Validate user modification
*
* @access public
* @param array $values Form values
* @return array $valid, $errors [0] = Success or not, [1] = List of errors
*/
public function validateModification(array $values)
{
if (! empty($values['password'])) {
return $this->validatePasswordModification($values);
}
$v = new Validator($values, array(
new Validators\Required('id', t('The user id is required')),
new Validators\Required('username', t('The username is required')),
new Validators\MaxLength('username', t('The maximum length is %d characters', 50), 50),
new Validators\Unique('username', t('The username must be unique'), $this->db->getConnection(), self::TABLE, 'id'),
new Validators\Integer('default_project_id', t('This value must be an integer')),
new Validators\Integer('is_admin', t('This value must be an integer')),
new Validators\Email('email', t('Email address invalid')),
));
return array(
$v->execute(),
$v->getErrors()
);
}
/**
* Validate password modification
*
* @access public
* @param array $values Form values
* @return array $valid, $errors [0] = Success or not, [1] = List of errors
*/
public function validatePasswordModification(array $values)
{
$v = new Validator($values, array(
new Validators\Required('id', t('The user id is required')),
new Validators\Required('username', t('The username is required')),
new Validators\MaxLength('username', t('The maximum length is %d characters', 50), 50),
new Validators\Unique('username', t('The username must be unique'), $this->db->getConnection(), self::TABLE, 'id'),
new Validators\Required('current_password', t('The current password is required')),
new Validators\Required('password', t('The password is required')),
new Validators\MinLength('password', t('The minimum length is %d characters', 6), 6),
new Validators\Required('confirmation', t('The confirmation is required')),
new Validators\Equals('password', 'confirmation', t('Passwords don\'t match')),
new Validators\Integer('default_project_id', t('This value must be an integer')),
new Validators\Integer('is_admin', t('This value must be an integer')),
new Validators\Email('email', t('Email address invalid')),
));
if ($v->execute()) {
// Check password
list($authenticated,) = $this->authenticate($_SESSION['user']['username'], $values['current_password']);
if ($authenticated) {
return array(true, array());
}
else {
return array(false, array('current_password' => array(t('Wrong password'))));
}
}
return array(false, $v->getErrors());
}
/**
* Validate user login
*
* @access public
* @param array $values Form values
* @return array $valid, $errors [0] = Success or not, [1] = List of errors
*/
public function validateLogin(array $values)
{
$v = new Validator($values, array(
new Validators\Required('username', t('The username is required')),
new Validators\MaxLength('username', t('The maximum length is %d characters', 50), 50),
new Validators\Required('password', t('The password is required')),
));
$result = $v->execute();
$errors = $v->getErrors();
if ($result) {
list($authenticated, $method) = $this->authenticate($values['username'], $values['password']);
if ($authenticated === true) {
// Create the user session
$user = $this->getByUsername($values['username']);
$this->updateSession($user);
// Update login history
$lastLogin = new LastLogin($this->db, $this->event);
$lastLogin->create(
$method,
$user['id'],
$this->getIpAddress(),
$this->getUserAgent()
);
// Setup the remember me feature
if (! empty($values['remember_me'])) {
$rememberMe = new RememberMe($this->db, $this->event);
$credentials = $rememberMe->create($user['id'], $this->getIpAddress(), $this->getUserAgent());
$rememberMe->writeCookie($credentials['token'], $credentials['sequence'], $credentials['expiration']);
}
}
else {
$result = false;
$errors['login'] = t('Bad username or password');
}
}
return array(
$result,
$errors
);
}
/**
* Authenticate a user
*
* @access public
* @param string $username Username
* @param string $password Password
* @return array
*/
public function authenticate($username, $password)
{
// Database authentication
$user = $this->db->table(self::TABLE)->eq('username', $username)->eq('is_ldap_user', 0)->findOne();
$authenticated = $user && \password_verify($password, $user['password']);
$method = LastLogin::AUTH_DATABASE;
// LDAP authentication
if (! $authenticated && LDAP_AUTH) {
$ldap = new Ldap($this->db, $this->event);
$authenticated = $ldap->authenticate($username, $password);
$method = LastLogin::AUTH_LDAP;
}
return array($authenticated, $method);
}
/**
* Get the user agent of the connected user
*
* @access public
* @return string
*/
public function getUserAgent()
{
return empty($_SERVER['HTTP_USER_AGENT']) ? t('Unknown') : $_SERVER['HTTP_USER_AGENT'];
}
/**
* Get the real IP address of the connected user
*
* @access public
* @param bool $only_public Return only public IP address
* @return string
*/
public function getIpAddress($only_public = false)
{
$keys = array(
'HTTP_CLIENT_IP',
'HTTP_X_FORWARDED_FOR',
'HTTP_X_FORWARDED',
'HTTP_X_CLUSTER_CLIENT_IP',
'HTTP_FORWARDED_FOR',
'HTTP_FORWARDED',
'REMOTE_ADDR'
);
foreach ($keys as $key) {
if (isset($_SERVER[$key])) {
foreach (explode(',', $_SERVER[$key]) as $ip_address) {
$ip_address = trim($ip_address);
if ($only_public) {
// Return only public IP address
if (filter_var($ip_address, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== false) {
return $ip_address;
}
}
else {
return $ip_address;
}
}
}
}
return t('Unknown');
}
}

View file

@ -0,0 +1,286 @@
<?php
namespace Schema;
use Core\Security;
const VERSION = 21;
function version_21($pdo)
{
$pdo->exec("ALTER TABLE tasks ADD COLUMN creator_id INTEGER DEFAULT '0'");
$pdo->exec("ALTER TABLE tasks ADD COLUMN date_modification INTEGER DEFAULT '0'");
}
function version_20($pdo)
{
$pdo->exec("ALTER TABLE users ADD COLUMN github_id VARCHAR(30)");
}
function version_19($pdo)
{
$pdo->exec("ALTER TABLE config ADD COLUMN api_token VARCHAR(255) DEFAULT '".Security::generateToken()."'");
}
function version_18($pdo)
{
$pdo->exec("
CREATE TABLE task_has_subtasks (
id INT NOT NULL AUTO_INCREMENT,
title VARCHAR(255),
status INT DEFAULT 0,
time_estimated INT DEFAULT 0,
time_spent INT DEFAULT 0,
task_id INT,
user_id INT,
PRIMARY KEY (id),
FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE
) ENGINE=InnoDB CHARSET=utf8"
);
}
function version_17($pdo)
{
$pdo->exec("
CREATE TABLE task_has_files (
id INT NOT NULL AUTO_INCREMENT,
name VARCHAR(50),
path VARCHAR(255),
is_image TINYINT(1) DEFAULT 0,
task_id INT,
PRIMARY KEY (id),
FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE
) ENGINE=InnoDB CHARSET=utf8"
);
}
function version_16($pdo)
{
$pdo->exec("
CREATE TABLE project_has_categories (
id INT NOT NULL AUTO_INCREMENT,
name VARCHAR(255),
project_id INT,
PRIMARY KEY (id),
UNIQUE KEY `idx_project_category` (project_id, name),
FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE
) ENGINE=InnoDB CHARSET=utf8"
);
$pdo->exec("ALTER TABLE tasks ADD COLUMN category_id INT DEFAULT 0");
}
function version_15($pdo)
{
$pdo->exec("ALTER TABLE projects ADD COLUMN last_modified INT DEFAULT 0");
}
function version_14($pdo)
{
$pdo->exec("ALTER TABLE users ADD COLUMN name VARCHAR(255)");
$pdo->exec("ALTER TABLE users ADD COLUMN email VARCHAR(255)");
$pdo->exec("ALTER TABLE users ADD COLUMN google_id VARCHAR(30)");
}
function version_13($pdo)
{
$pdo->exec("ALTER TABLE users ADD COLUMN is_ldap_user TINYINT(1) DEFAULT 0");
}
function version_12($pdo)
{
$pdo->exec("
CREATE TABLE remember_me (
id INT NOT NULL AUTO_INCREMENT,
user_id INT,
ip VARCHAR(40),
user_agent VARCHAR(255),
token VARCHAR(255),
sequence VARCHAR(255),
expiration INT,
date_creation INT,
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE,
PRIMARY KEY (id)
) ENGINE=InnoDB CHARSET=utf8"
);
$pdo->exec("
CREATE TABLE last_logins (
id INT NOT NULL AUTO_INCREMENT,
auth_type VARCHAR(25),
user_id INT,
ip VARCHAR(40),
user_agent VARCHAR(255),
date_creation INT,
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE,
PRIMARY KEY (id),
INDEX (user_id)
) ENGINE=InnoDB CHARSET=utf8"
);
}
function version_11($pdo)
{
}
function version_10($pdo)
{
}
function version_9($pdo)
{
}
function version_8($pdo)
{
}
function version_7($pdo)
{
}
function version_6($pdo)
{
}
function version_5($pdo)
{
}
function version_4($pdo)
{
}
function version_3($pdo)
{
}
function version_2($pdo)
{
}
function version_1($pdo)
{
$pdo->exec("
CREATE TABLE config (
language CHAR(5) DEFAULT 'en_US',
webhooks_token VARCHAR(255),
timezone VARCHAR(50) DEFAULT 'UTC'
) ENGINE=InnoDB CHARSET=utf8
");
$pdo->exec("
CREATE TABLE users (
id INT NOT NULL AUTO_INCREMENT,
username VARCHAR(50),
password VARCHAR(255),
is_admin TINYINT DEFAULT 0,
default_project_id INT DEFAULT 0,
PRIMARY KEY (id)
) ENGINE=InnoDB CHARSET=utf8
");
$pdo->exec("
CREATE TABLE projects (
id INT NOT NULL AUTO_INCREMENT,
name VARCHAR(50) UNIQUE,
is_active TINYINT DEFAULT 1,
token VARCHAR(255),
PRIMARY KEY (id)
) ENGINE=InnoDB CHARSET=utf8
");
$pdo->exec("
CREATE TABLE project_has_users (
id INT NOT NULL AUTO_INCREMENT,
project_id INT,
user_id INT,
PRIMARY KEY (id),
UNIQUE KEY `idx_project_user` (project_id, user_id),
FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE,
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB CHARSET=utf8
");
$pdo->exec("
CREATE TABLE columns (
id INT NOT NULL AUTO_INCREMENT,
title VARCHAR(255),
position INT NOT NULL,
project_id INT NOT NULL,
task_limit INT DEFAULT '0',
UNIQUE KEY `idx_title_project` (title, project_id),
PRIMARY KEY (id),
FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE
) ENGINE=InnoDB CHARSET=utf8
");
$pdo->exec("
CREATE TABLE tasks (
id INT NOT NULL AUTO_INCREMENT,
title VARCHAR(255),
description TEXT,
date_creation INT,
date_completed INT,
date_due INT,
color_id VARCHAR(50),
project_id INT,
column_id INT,
owner_id INT DEFAULT '0',
position INT,
score INT,
is_active TINYINT DEFAULT 1,
PRIMARY KEY (id),
INDEX `idx_task_active` (is_active),
FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE,
FOREIGN KEY(column_id) REFERENCES columns(id) ON DELETE CASCADE
) ENGINE=InnoDB CHARSET=utf8
");
$pdo->exec("
CREATE TABLE comments (
id INT NOT NULL AUTO_INCREMENT,
task_id INT,
user_id INT,
date INT,
comment TEXT,
PRIMARY KEY (id),
FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE,
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB CHARSET=utf8
");
$pdo->exec("
CREATE TABLE actions (
id INT NOT NULL AUTO_INCREMENT,
project_id INT,
event_name VARCHAR(50),
action_name VARCHAR(50),
PRIMARY KEY (id),
FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE
) ENGINE=InnoDB CHARSET=utf8
");
$pdo->exec("
CREATE TABLE action_has_params (
id INT NOT NULL AUTO_INCREMENT,
action_id INT,
name VARCHAR(50),
value VARCHAR(50),
PRIMARY KEY (id),
FOREIGN KEY(action_id) REFERENCES actions(id) ON DELETE CASCADE
) ENGINE=InnoDB CHARSET=utf8
");
$pdo->exec("
INSERT INTO users
(username, password, is_admin)
VALUES ('admin', '".\password_hash('admin', PASSWORD_BCRYPT)."', '1')
");
$pdo->exec("
INSERT INTO config
(webhooks_token)
VALUES ('".Security::generateToken()."')
");
}

View file

@ -0,0 +1,172 @@
<?php
namespace Schema;
use Core\Security;
const VERSION = 2;
function version_2($pdo)
{
$pdo->exec("ALTER TABLE tasks ADD COLUMN creator_id INTEGER DEFAULT 0");
$pdo->exec("ALTER TABLE tasks ADD COLUMN date_modification INTEGER DEFAULT 0");
}
function version_1($pdo)
{
$pdo->exec("
CREATE TABLE config (
language CHAR(5) DEFAULT 'en_US',
webhooks_token VARCHAR(255),
timezone VARCHAR(50) DEFAULT 'UTC',
api_token VARCHAR(255)
);
CREATE TABLE users (
id SERIAL PRIMARY KEY,
username VARCHAR(50),
password VARCHAR(255),
is_admin BOOLEAN DEFAULT '0',
default_project_id INTEGER DEFAULT 0,
is_ldap_user BOOLEAN DEFAULT '0',
name VARCHAR(255),
email VARCHAR(255),
google_id VARCHAR(255),
github_id VARCHAR(30)
);
CREATE TABLE remember_me (
id SERIAL PRIMARY KEY,
user_id INTEGER,
ip VARCHAR(40),
user_agent VARCHAR(255),
token VARCHAR(255),
sequence VARCHAR(255),
expiration INTEGER,
date_creation INTEGER,
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
);
CREATE TABLE last_logins (
id SERIAL PRIMARY KEY,
auth_type VARCHAR(25),
user_id INTEGER,
ip VARCHAR(40),
user_agent VARCHAR(255),
date_creation INTEGER,
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
);
CREATE TABLE projects (
id SERIAL PRIMARY KEY,
name VARCHAR(255) UNIQUE,
is_active BOOLEAN DEFAULT '1',
token VARCHAR(255),
last_modified INTEGER DEFAULT 0
);
CREATE TABLE project_has_users (
id SERIAL PRIMARY KEY,
project_id INTEGER,
user_id INTEGER,
FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE,
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE,
UNIQUE(project_id, user_id)
);
CREATE TABLE project_has_categories (
id SERIAL PRIMARY KEY,
name VARCHAR(255),
project_id INTEGER,
UNIQUE (project_id, name),
FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE
);
CREATE TABLE columns (
id SERIAL PRIMARY KEY,
title VARCHAR(255),
position INTEGER,
project_id INTEGER,
task_limit INTEGER DEFAULT 0,
FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE,
UNIQUE (title, project_id)
);
CREATE TABLE tasks (
id SERIAL PRIMARY KEY,
title VARCHAR(255),
description TEXT,
date_creation INTEGER,
color_id VARCHAR(255),
project_id INTEGER,
column_id INTEGER,
owner_id INTEGER DEFAULT 0,
position INTEGER,
is_active BOOLEAN DEFAULT '1',
date_completed INTEGER,
score INTEGER,
date_due INTEGER,
category_id INTEGER DEFAULT 0,
FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE,
FOREIGN KEY(column_id) REFERENCES columns(id) ON DELETE CASCADE
);
CREATE TABLE task_has_subtasks (
id SERIAL PRIMARY KEY,
title VARCHAR(255),
status SMALLINT DEFAULT 0,
time_estimated INTEGER DEFAULT 0,
time_spent INTEGER DEFAULT 0,
task_id INTEGER,
user_id INTEGER,
FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE
);
CREATE TABLE task_has_files (
id SERIAL PRIMARY KEY,
name VARCHAR(255),
path VARCHAR(255),
is_image BOOLEAN DEFAULT '0',
task_id INTEGER,
FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE
);
CREATE TABLE comments (
id SERIAL PRIMARY KEY,
task_id INTEGER,
user_id INTEGER,
date INTEGER,
comment TEXT,
FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE,
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
);
CREATE TABLE actions (
id SERIAL PRIMARY KEY,
project_id INTEGER,
event_name VARCHAR(50),
action_name VARCHAR(50),
FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE
);
CREATE TABLE action_has_params (
id SERIAL PRIMARY KEY,
action_id INTEGER,
name VARCHAR(50),
value VARCHAR(50),
FOREIGN KEY(action_id) REFERENCES actions(id) ON DELETE CASCADE
);
");
$pdo->exec("
INSERT INTO users
(username, password, is_admin)
VALUES ('admin', '".\password_hash('admin', PASSWORD_BCRYPT)."', '1')
");
$pdo->exec("
INSERT INTO config
(webhooks_token, api_token)
VALUES ('".Security::generateToken()."', '".Security::generateToken()."')
");
}

View file

@ -0,0 +1,307 @@
<?php
namespace Schema;
use Core\Security;
const VERSION = 21;
function version_21($pdo)
{
$pdo->exec("ALTER TABLE tasks ADD COLUMN creator_id INTEGER DEFAULT '0'");
$pdo->exec("ALTER TABLE tasks ADD COLUMN date_modification INTEGER DEFAULT '0'");
}
function version_20($pdo)
{
$pdo->exec("ALTER TABLE users ADD COLUMN github_id TEXT");
}
function version_19($pdo)
{
$pdo->exec("ALTER TABLE config ADD COLUMN api_token TEXT DEFAULT '".Security::generateToken()."'");
}
function version_18($pdo)
{
$pdo->exec("
CREATE TABLE task_has_subtasks (
id INTEGER PRIMARY KEY,
title TEXT COLLATE NOCASE,
status INTEGER DEFAULT 0,
time_estimated INTEGER DEFAULT 0,
time_spent INTEGER DEFAULT 0,
task_id INTEGER,
user_id INTEGER,
FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE
)"
);
}
function version_17($pdo)
{
$pdo->exec("
CREATE TABLE task_has_files (
id INTEGER PRIMARY KEY,
name TEXT COLLATE NOCASE,
path TEXT,
is_image INTEGER DEFAULT 0,
task_id INTEGER,
FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE
)"
);
}
function version_16($pdo)
{
$pdo->exec("
CREATE TABLE project_has_categories (
id INTEGER PRIMARY KEY,
name TEXT COLLATE NOCASE,
project_id INT,
UNIQUE (project_id, name),
FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE
)"
);
$pdo->exec("ALTER TABLE tasks ADD COLUMN category_id INTEGER DEFAULT 0");
}
function version_15($pdo)
{
$pdo->exec("ALTER TABLE projects ADD COLUMN last_modified INTEGER DEFAULT 0");
}
function version_14($pdo)
{
$pdo->exec("ALTER TABLE users ADD COLUMN name TEXT");
$pdo->exec("ALTER TABLE users ADD COLUMN email TEXT");
$pdo->exec("ALTER TABLE users ADD COLUMN google_id TEXT");
}
function version_13($pdo)
{
$pdo->exec("ALTER TABLE users ADD COLUMN is_ldap_user INTEGER DEFAULT 0");
}
function version_12($pdo)
{
$pdo->exec(
'CREATE TABLE remember_me (
id INTEGER PRIMARY KEY,
user_id INTEGER,
ip TEXT,
user_agent TEXT,
token TEXT,
sequence TEXT,
expiration INTEGER,
date_creation INTEGER,
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
)'
);
$pdo->exec(
'CREATE TABLE last_logins (
id INTEGER PRIMARY KEY,
auth_type TEXT,
user_id INTEGER,
ip TEXT,
user_agent TEXT,
date_creation INTEGER,
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
)'
);
$pdo->exec('CREATE INDEX last_logins_user_idx ON last_logins(user_id)');
}
function version_11($pdo)
{
$pdo->exec(
'ALTER TABLE comments RENAME TO comments_bak'
);
$pdo->exec(
'CREATE TABLE comments (
id INTEGER PRIMARY KEY,
task_id INTEGER,
user_id INTEGER,
date INTEGER,
comment TEXT,
FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE,
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
)'
);
$pdo->exec(
'INSERT INTO comments SELECT * FROM comments_bak'
);
$pdo->exec(
'DROP TABLE comments_bak'
);
}
function version_10($pdo)
{
$pdo->exec(
'CREATE TABLE actions (
id INTEGER PRIMARY KEY,
project_id INTEGER,
event_name TEXT,
action_name TEXT,
FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE
)'
);
$pdo->exec(
'CREATE TABLE action_has_params (
id INTEGER PRIMARY KEY,
action_id INTEGER,
name TEXT,
value TEXT,
FOREIGN KEY(action_id) REFERENCES actions(id) ON DELETE CASCADE
)'
);
}
function version_9($pdo)
{
$pdo->exec("ALTER TABLE tasks ADD COLUMN date_due INTEGER");
}
function version_8($pdo)
{
$pdo->exec(
'CREATE TABLE comments (
id INTEGER PRIMARY KEY,
task_id INTEGER,
user_id INTEGER,
date INTEGER,
comment TEXT,
FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE,
FOREIGN KEY(user_id) REFERENCES tasks(id) ON DELETE CASCADE
)'
);
}
function version_7($pdo)
{
$pdo->exec("
CREATE TABLE project_has_users (
id INTEGER PRIMARY KEY,
project_id INTEGER,
user_id INTEGER,
FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE,
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE,
UNIQUE(project_id, user_id)
)
");
}
function version_6($pdo)
{
$pdo->exec("ALTER TABLE columns ADD COLUMN task_limit INTEGER DEFAULT '0'");
}
function version_5($pdo)
{
$pdo->exec("ALTER TABLE tasks ADD COLUMN score INTEGER");
}
function version_4($pdo)
{
$pdo->exec("ALTER TABLE config ADD COLUMN timezone TEXT DEFAULT 'UTC'");
}
function version_3($pdo)
{
$pdo->exec('ALTER TABLE projects ADD COLUMN token TEXT');
// For each existing project, assign a different token
$rq = $pdo->prepare("SELECT id FROM projects WHERE token IS NULL");
$rq->execute();
$results = $rq->fetchAll(\PDO::FETCH_ASSOC);
if ($results !== false) {
foreach ($results as &$result) {
$rq = $pdo->prepare('UPDATE projects SET token=? WHERE id=?');
$rq->execute(array(Security::generateToken(), $result['id']));
}
}
}
function version_2($pdo)
{
$pdo->exec('ALTER TABLE tasks ADD COLUMN date_completed INTEGER');
$pdo->exec('UPDATE tasks SET date_completed=date_creation WHERE is_active=0');
}
function version_1($pdo)
{
$pdo->exec("
CREATE TABLE config (
language TEXT,
webhooks_token TEXT
)
");
$pdo->exec("
CREATE TABLE users (
id INTEGER PRIMARY KEY,
username TEXT,
password TEXT,
is_admin INTEGER DEFAULT 0,
default_project_id INTEGER DEFAULT 0
)
");
$pdo->exec("
CREATE TABLE projects (
id INTEGER PRIMARY KEY,
name TEXT NOCASE UNIQUE,
is_active INTEGER DEFAULT 1
)
");
$pdo->exec("
CREATE TABLE columns (
id INTEGER PRIMARY KEY,
title TEXT,
position INTEGER,
project_id INTEGER,
FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE,
UNIQUE (title, project_id)
)
");
$pdo->exec("
CREATE TABLE tasks (
id INTEGER PRIMARY KEY,
title TEXT,
description TEXT,
date_creation INTEGER,
color_id TEXT,
project_id INTEGER,
column_id INTEGER,
owner_id INTEGER DEFAULT '0',
position INTEGER,
is_active INTEGER DEFAULT 1,
FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE,
FOREIGN KEY(column_id) REFERENCES columns(id) ON DELETE CASCADE
)
");
$pdo->exec("
INSERT INTO users
(username, password, is_admin)
VALUES ('admin', '".\password_hash('admin', PASSWORD_BCRYPT)."', '1')
");
$pdo->exec("
INSERT INTO config
(language, webhooks_token)
VALUES ('en_US', '".Security::generateToken()."')
");
}

View file

@ -0,0 +1,77 @@
<section id="main">
<div class="page-header">
<h2><?= t('Automatic actions for the project "%s"', $project['name']) ?></h2>
<ul>
<li><a href="?controller=project"><?= t('All projects') ?></a></li>
</ul>
</div>
<section>
<?php if (! empty($actions)): ?>
<h3><?= t('Defined actions') ?></h3>
<table>
<tr>
<th><?= t('Event name') ?></th>
<th><?= t('Action name') ?></th>
<th><?= t('Action parameters') ?></th>
<th><?= t('Action') ?></th>
</tr>
<?php foreach ($actions as $action): ?>
<tr>
<td><?= Helper\in_list($action['event_name'], $available_events) ?></td>
<td><?= Helper\in_list($action['action_name'], $available_actions) ?></td>
<td>
<ul>
<?php foreach ($action['params'] as $param): ?>
<li>
<?= Helper\in_list($param['name'], $available_params) ?> =
<strong>
<?php if (Helper\contains($param['name'], 'column_id')): ?>
<?= Helper\in_list($param['value'], $columns_list) ?>
<?php elseif (Helper\contains($param['name'], 'user_id')): ?>
<?= Helper\in_list($param['value'], $users_list) ?>
<?php elseif (Helper\contains($param['name'], 'project_id')): ?>
<?= Helper\in_list($param['value'], $projects_list) ?>
<?php elseif (Helper\contains($param['name'], 'color_id')): ?>
<?= Helper\in_list($param['value'], $colors_list) ?>
<?php elseif (Helper\contains($param['name'], 'category_id')): ?>
<?= Helper\in_list($param['value'], $categories_list) ?>
<?php endif ?>
</strong>
</li>
<?php endforeach ?>
</ul>
</td>
<td>
<a href="?controller=action&amp;action=confirm&amp;action_id=<?= $action['id'] ?>"><?= t('Remove') ?></a>
</td>
</tr>
<?php endforeach ?>
</table>
<?php endif ?>
<h3><?= t('Add an action') ?></h3>
<form method="post" action="?controller=action&amp;action=params&amp;project_id=<?= $project['id'] ?>" autocomplete="off">
<?= Helper\form_csrf() ?>
<?= Helper\form_hidden('project_id', $values) ?>
<?= Helper\form_label(t('Event'), 'event_name') ?>
<?= Helper\form_select('event_name', $available_events, $values) ?><br/>
<?= Helper\form_label(t('Action'), 'action_name') ?>
<?= Helper\form_select('action_name', $available_actions, $values) ?><br/>
<div class="form-help">
<?= t('When the selected event occurs execute the corresponding action.') ?>
</div>
<div class="form-actions">
<input type="submit" value="<?= t('Next step') ?>" class="btn btn-blue"/>
</div>
</form>
</section>
</section>

View file

@ -0,0 +1,43 @@
<section id="main">
<div class="page-header">
<h2><?= t('Automatic actions for the project "%s"', $project['name']) ?></h2>
<ul>
<li><a href="?controller=project"><?= t('All projects') ?></a></li>
</ul>
</div>
<section>
<h3><?= t('Define action parameters') ?></h3>
<form method="post" action="?controller=action&amp;action=create&amp;project_id=<?= $project['id'] ?>" autocomplete="off">
<?= Helper\form_csrf() ?>
<?= Helper\form_hidden('project_id', $values) ?>
<?= Helper\form_hidden('event_name', $values) ?>
<?= Helper\form_hidden('action_name', $values) ?>
<?php foreach ($action_params as $param_name => $param_desc): ?>
<?php if (Helper\contains($param_name, 'column_id')): ?>
<?= Helper\form_label($param_desc, $param_name) ?>
<?= Helper\form_select('params['.$param_name.']', $columns_list, $values) ?><br/>
<?php elseif (Helper\contains($param_name, 'user_id')): ?>
<?= Helper\form_label($param_desc, $param_name) ?>
<?= Helper\form_select('params['.$param_name.']', $users_list, $values) ?><br/>
<?php elseif (Helper\contains($param_name, 'project_id')): ?>
<?= Helper\form_label($param_desc, $param_name) ?>
<?= Helper\form_select('params['.$param_name.']', $projects_list, $values) ?><br/>
<?php elseif (Helper\contains($param_name, 'color_id')): ?>
<?= Helper\form_label($param_desc, $param_name) ?>
<?= Helper\form_select('params['.$param_name.']', $colors_list, $values) ?><br/>
<?php elseif (Helper\contains($param_name, 'category_id')): ?>
<?= Helper\form_label($param_desc, $param_name) ?>
<?= Helper\form_select('params['.$param_name.']', $categories_list, $values) ?><br/>
<?php endif ?>
<?php endforeach ?>
<div class="form-actions">
<input type="submit" value="<?= t('Save this action') ?>" class="btn btn-blue"/>
<?= t('or') ?> <a href="?controller=action&amp;action=index&amp;project_id=<?= $project['id'] ?>"><?= t('cancel') ?></a>
</div>
</form>
</section>
</section>

View file

@ -0,0 +1,16 @@
<section id="main">
<div class="page-header">
<h2><?= t('Remove an automatic action') ?></h2>
</div>
<div class="confirm">
<p class="alert alert-info">
<?= t('Do you really want to remove this action: "%s"?', Helper\in_list($action['event_name'], $available_events).'/'.Helper\in_list($action['action_name'], $available_actions)) ?>
</p>
<div class="form-actions">
<a href="?controller=action&amp;action=remove&amp;action_id=<?= $action['id'].Helper\param_csrf() ?>" class="btn btn-red"><?= t('Yes') ?></a>
<?= t('or') ?> <a href="?controller=action&amp;action=index&amp;project_id=<?= $action['project_id'] ?>"><?= t('cancel') ?></a>
</div>
</div>
</section>

View file

@ -0,0 +1,9 @@
<section id="main">
<div class="page-header">
<h2><?= t('Forbidden') ?></h2>
</div>
<p class="alert alert-error">
<?= t('Access Forbidden') ?>
</p>
</section>

View file

@ -0,0 +1,9 @@
<section id="main">
<div class="page-header">
<h2><?= t('Page not found') ?></h2>
</div>
<p class="alert alert-error">
<?= t('Sorry, I didn\'t found this information in my database!') ?>
</p>
</section>

View file

@ -0,0 +1,26 @@
<section id="main">
<div class="page-header board">
<h2>
<?= t('Project "%s"', $current_project_name) ?>
</h2>
</div>
<section>
<h3><?= t('Change assignee for the task "%s"', $values['title']) ?></h3>
<form method="post" action="?controller=board&amp;action=assignTask" autocomplete="off">
<?= Helper\form_csrf() ?>
<?= Helper\form_hidden('id', $values) ?>
<?= Helper\form_hidden('project_id', $values) ?>
<?= Helper\form_label(t('Assignee'), 'owner_id') ?>
<?= Helper\form_select('owner_id', $users_list, $values, $errors) ?><br/>
<div class="form-actions">
<input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
<?= t('or') ?> <a href="?controller=board&amp;action=show&amp;project_id=<?= $values['project_id'] ?>"><?= t('cancel') ?></a>
</div>
</form>
</section>
</section>

View file

@ -0,0 +1,66 @@
<section id="main">
<div class="page-header">
<h2><?= t('Edit the board for "%s"', $project['name']) ?></h2>
<ul>
<li><a href="?controller=project"><?= t('All projects') ?></a></li>
</ul>
</div>
<section>
<h3><?= t('Change columns') ?></h3>
<form method="post" action="?controller=board&amp;action=update&amp;project_id=<?= $project['id'] ?>" autocomplete="off">
<?= Helper\form_csrf() ?>
<?php $i = 0; ?>
<table>
<tr>
<th><?= t('Position') ?></th>
<th><?= t('Column title') ?></th>
<th><?= t('Task limit') ?></th>
<th><?= t('Actions') ?></th>
</tr>
<?php foreach ($columns as $column): ?>
<tr>
<td><?= Helper\form_label(t('Column %d', ++$i), 'title['.$column['id'].']', array('title="column_id='.$column['id'].'"')) ?></td>
<td><?= Helper\form_text('title['.$column['id'].']', $values, $errors, array('required')) ?></td>
<td><?= Helper\form_number('task_limit['.$column['id'].']', $values, $errors, array('placeholder="'.t('limit').'"')) ?></td>
<td>
<ul>
<?php if ($column['position'] != 1): ?>
<li>
<a href="?controller=board&amp;action=moveUp&amp;project_id=<?= $project['id'] ?>&amp;column_id=<?= $column['id'].Helper\param_csrf() ?>"><?= t('Move Up') ?></a>
</li>
<?php endif ?>
<?php if ($column['position'] != count($columns)): ?>
<li>
<a href="?controller=board&amp;action=moveDown&amp;project_id=<?= $project['id'] ?>&amp;column_id=<?= $column['id'].Helper\param_csrf() ?>"><?= t('Move Down') ?></a>
</li>
<?php endif ?>
<li>
<a href="?controller=board&amp;action=confirm&amp;project_id=<?= $project['id'] ?>&amp;column_id=<?= $column['id'] ?>"><?= t('Remove') ?></a>
</li>
</ul>
</td>
</tr>
<?php endforeach ?>
</table>
<div class="form-actions">
<input type="submit" value="<?= t('Update') ?>" class="btn btn-blue"/>
<?= t('or') ?> <a href="?controller=project"><?= t('cancel') ?></a>
</div>
</form>
<h3><?= t('Add a new column') ?></h3>
<form method="post" action="?controller=board&amp;action=add&amp;project_id=<?= $project['id'] ?>" autocomplete="off">
<?= Helper\form_csrf() ?>
<?= Helper\form_hidden('project_id', $values) ?>
<?= Helper\form_label(t('Title'), 'title') ?>
<?= Helper\form_text('title', $values, $errors, array('required')) ?>
<div class="form-actions">
<input type="submit" value="<?= t('Add this column') ?>" class="btn btn-blue"/>
<?= t('or') ?> <a href="?controller=project"><?= t('cancel') ?></a>
</div>
</form>
</section>
</section>

View file

@ -0,0 +1,31 @@
<section id="main">
<div class="page-header board">
<h2>
<?= t('Project "%s"', $current_project_name) ?>
</h2>
</div>
<div class="project-menu">
<ul>
<li>
<span class="hide-tablet"><?= t('Filter by user') ?></span>
<?= Helper\form_select('user_id', $users, $filters) ?>
</li>
<li>
<span class="hide-tablet"><?= t('Filter by category') ?></span>
<?= Helper\form_select('category_id', $categories, $filters) ?>
</li>
<li><a href="#" id="filter-due-date"><?= t('Filter by due date') ?></a></li>
<li><a href="?controller=project&amp;action=search&amp;project_id=<?= $current_project_id ?>"><?= t('Search') ?></a></li>
<li><a href="?controller=project&amp;action=tasks&amp;project_id=<?= $current_project_id ?>"><?= t('Completed tasks') ?></a></li>
</ul>
</div>
<?php if (empty($board)): ?>
<p class="alert alert-error"><?= t('There is no column in your project!') ?></p>
<?php else: ?>
<?= Helper\template('board_show', array('current_project_id' => $current_project_id, 'board' => $board, 'categories' => $categories)) ?>
<?php endif ?>
</section>

View file

@ -0,0 +1,34 @@
<section id="main" class="public-board">
<?php if (empty($columns)): ?>
<p class="alert alert-error"><?= t('There is no column in your project!') ?></p>
<?php else: ?>
<table id="board">
<tr>
<?php $column_with = round(100 / count($columns), 2); ?>
<?php foreach ($columns as $column): ?>
<th width="<?= $column_with ?>%">
<?= Helper\escape($column['title']) ?>
<?php if ($column['task_limit']): ?>
<span title="<?= t('Task limit') ?>" class="task-limit">(<?= Helper\escape(count($column['tasks']).'/'.$column['task_limit']) ?>)</span>
<?php endif ?>
</th>
<?php endforeach ?>
</tr>
<tr>
<?php foreach ($columns as $column): ?>
<td class="column <?= $column['task_limit'] && count($column['tasks']) > $column['task_limit'] ? 'task-limit-warning' : '' ?>">
<?php foreach ($column['tasks'] as $task): ?>
<div class="task-board task-<?= $task['color_id'] ?>">
<?= Helper\template('board_task', array('task' => $task, 'categories' => $categories, 'not_editable' => true)) ?>
</div>
<?php endforeach ?>
</td>
<?php endforeach ?>
</tr>
</table>
<?php endif ?>
</section>

View file

@ -0,0 +1,17 @@
<section id="main">
<div class="page-header">
<h2><?= t('Remove a column') ?></h2>
</div>
<div class="confirm">
<p class="alert alert-info">
<?= t('Do you really want to remove this column: "%s"?', $column['title']) ?>
<?= t('This action will REMOVE ALL TASKS associated to this column!') ?>
</p>
<div class="form-actions">
<a href="?controller=board&amp;action=remove&amp;column_id=<?= $column['id'].Helper\param_csrf() ?>" class="btn btn-red"><?= t('Yes') ?></a>
<?= t('or') ?> <a href="?controller=board&amp;action=edit&amp;project_id=<?= $column['project_id'] ?>"><?= t('cancel') ?></a>
</div>
</div>
</section>

View file

@ -0,0 +1,49 @@
<table id="board" data-project-id="<?= $current_project_id ?>" data-time="<?= time() ?>" data-check-interval="<?= BOARD_CHECK_INTERVAL ?>" data-csrf-token=<?= \Core\Security::getCSRFToken() ?>>
<tr>
<?php $column_with = round(100 / count($board), 2); ?>
<?php foreach ($board as $column): ?>
<th width="<?= $column_with ?>%">
<div class="board-add-icon">
<a href="?controller=task&amp;action=create&amp;project_id=<?= $column['project_id'] ?>&amp;column_id=<?= $column['id'] ?>" title="<?= t('Add a new task') ?>">+</a>
</div>
<?= Helper\escape($column['title']) ?>
<?php if ($column['task_limit']): ?>
<span title="<?= t('Task limit') ?>" class="task-limit">
(
<span id="task-number-column-<?= $column['id'] ?>"><?= count($column['tasks']) ?></span>
/
<?= Helper\escape($column['task_limit']) ?>
)
</span>
<?php else: ?>
<span title="<?= t('Task count') ?>" class="task-count">
(<span id="task-number-column-<?= $column['id'] ?>"><?= count($column['tasks']) ?></span>)
</span>
<?php endif ?>
</th>
<?php endforeach ?>
</tr>
<tr>
<?php foreach ($board as $column): ?>
<td
id="column-<?= $column['id'] ?>"
class="column <?= $column['task_limit'] && count($column['tasks']) > $column['task_limit'] ? 'task-limit-warning' : '' ?>"
data-column-id="<?= $column['id'] ?>"
data-task-limit="<?= $column['task_limit'] ?>"
>
<?php foreach ($column['tasks'] as $task): ?>
<div class="task-board draggable-item task-<?= $task['color_id'] ?>"
data-task-id="<?= $task['id'] ?>"
data-owner-id="<?= $task['owner_id'] ?>"
data-category-id="<?= $task['category_id'] ?>"
data-due-date="<?= $task['date_due'] ?>"
title="<?= t('View this task') ?>">
<?= Helper\template('board_task', array('task' => $task, 'categories' => $categories)) ?>
</div>
<?php endforeach ?>
</td>
<?php endforeach ?>
</tr>
</table>

View file

@ -0,0 +1,76 @@
<?php if (isset($not_editable)): ?>
#<?= $task['id'] ?> -
<span class="task-board-user">
<?php if (! empty($task['owner_id'])): ?>
<?= t('Assigned to %s', $task['username']) ?>
<?php else: ?>
<span class="task-board-nobody"><?= t('Nobody assigned') ?></span>
<?php endif ?>
</span>
<?php if ($task['score']): ?>
<span class="task-score"><?= Helper\escape($task['score']) ?></span>
<?php endif ?>
<div class="task-board-title">
<?= Helper\escape($task['title']) ?>
</div>
<?php else: ?>
<a class="task-edit-popover" href="?controller=task&amp;action=edit&amp;task_id=<?= $task['id'] ?>" title="<?= t('Edit this task') ?>">#<?= $task['id'] ?></a> -
<span class="task-board-user">
<?php if (! empty($task['owner_id'])): ?>
<a class="assignee-popover" href="?controller=board&amp;action=assign&amp;task_id=<?= $task['id'] ?>" title="<?= t('Change assignee') ?>"><?= t('Assigned to %s', $task['username']) ?></a>
<?php else: ?>
<a class="assignee-popover" href="?controller=board&amp;action=assign&amp;task_id=<?= $task['id'] ?>" title="<?= t('Change assignee') ?>" class="task-board-nobody"><?= t('Nobody assigned') ?></a>
<?php endif ?>
</span>
<?php if ($task['score']): ?>
<span class="task-score"><?= Helper\escape($task['score']) ?></span>
<?php endif ?>
<div class="task-board-title">
<a href="?controller=task&amp;action=show&amp;task_id=<?= $task['id'] ?>" title="<?= t('View this task') ?>"><?= Helper\escape($task['title']) ?></a>
</div>
<?php endif ?>
<?php if ($task['category_id']): ?>
<div class="task-board-category-container">
<span class="task-board-category">
<?= Helper\in_list($task['category_id'], $categories) ?>
</span>
</div>
<?php endif ?>
<?php if (! empty($task['date_due']) || ! empty($task['nb_files']) || ! empty($task['nb_comments']) || ! empty($task['description'])): ?>
<div class="task-board-footer">
<?php if (! empty($task['date_due'])): ?>
<div class="task-board-date">
<?= dt('%B %e, %G', $task['date_due']) ?>
</div>
<?php endif ?>
<div class="task-board-icons">
<?php if (! empty($task['nb_files'])): ?>
<?= $task['nb_files'] ?> <i class="fa fa-paperclip" title="<?= t('Attachments') ?>"></i>
<?php endif ?>
<?php if (! empty($task['nb_comments'])): ?>
<?= $task['nb_comments'] ?> <i class="fa fa-comment-o" title="<?= p($task['nb_comments'], t('%d comment', $task['nb_comments']), t('%d comments', $task['nb_comments'])) ?>"></i>
<?php endif ?>
<?php if (! empty($task['description'])): ?>
<a class="task-board-popover" href='?controller=task&amp;action=editDescription&amp;task_id=<?= $task['id'] ?>'><i class="fa fa-file-text-o" title="<?= t('Description') ?>"></i></a>
<?php endif ?>
</div>
</div>
<?php endif ?>

View file

@ -0,0 +1,24 @@
<section id="main">
<div class="page-header">
<h2><?= t('Category modification for the project "%s"', $project['name']) ?></h2>
<ul>
<li><a href="?controller=project"><?= t('All projects') ?></a></li>
</ul>
</div>
<section>
<form method="post" action="?controller=category&amp;action=update&amp;project_id=<?= $project['id'] ?>" autocomplete="off">
<?= Helper\form_csrf() ?>
<?= Helper\form_hidden('id', $values) ?>
<?= Helper\form_hidden('project_id', $values) ?>
<?= Helper\form_label(t('Category Name'), 'name') ?>
<?= Helper\form_text('name', $values, $errors, array('required')) ?>
<div class="form-actions">
<input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
</div>
</form>
</section>
</section>

View file

@ -0,0 +1,49 @@
<section id="main">
<div class="page-header">
<h2><?= t('Categories for the project "%s"', $project['name']) ?></h2>
<ul>
<li><a href="?controller=project"><?= t('All projects') ?></a></li>
</ul>
</div>
<section>
<?php if (! empty($categories)): ?>
<table>
<tr>
<th><?= t('Category Name') ?></th>
<th><?= t('Actions') ?></th>
</tr>
<?php foreach ($categories as $category_id => $category_name): ?>
<tr>
<td><?= Helper\escape($category_name) ?></td>
<td>
<ul>
<li>
<a href="?controller=category&amp;action=edit&amp;project_id=<?= $project['id'] ?>&amp;category_id=<?= $category_id ?>"><?= t('Edit') ?></a>
</li>
<li>
<a href="?controller=category&amp;action=confirm&amp;project_id=<?= $project['id'] ?>&amp;category_id=<?= $category_id ?>"><?= t('Remove') ?></a>
</li>
</ul>
</td>
</tr>
<?php endforeach ?>
</table>
<?php endif ?>
<h3><?= t('Add a new category') ?></h3>
<form method="post" action="?controller=category&amp;action=save&amp;project_id=<?= $project['id'] ?>" autocomplete="off">
<?= Helper\form_csrf() ?>
<?= Helper\form_hidden('project_id', $values) ?>
<?= Helper\form_label(t('Category Name'), 'name') ?>
<?= Helper\form_text('name', $values, $errors, array('required')) ?>
<div class="form-actions">
<input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
</div>
</form>
</section>
</section>

View file

@ -0,0 +1,16 @@
<section id="main">
<div class="page-header">
<h2><?= t('Remove a category') ?></h2>
</div>
<div class="confirm">
<p class="alert alert-info">
<?= t('Do you really want to remove this category: "%s"?', $category['name']) ?>
</p>
<div class="form-actions">
<a href="?controller=category&amp;action=remove&amp;project_id=<?= $project['id'] ?>&amp;category_id=<?= $category['id'].Helper\param_csrf() ?>" class="btn btn-red"><?= t('Yes') ?></a>
<?= t('or') ?> <a href="?controller=category&amp;project_id=<?= $project['id'] ?>"><?= t('cancel') ?></a>
</div>
</div>
</section>

View file

@ -0,0 +1,17 @@
<div class="page-header">
<h2><?= t('Add a comment') ?></h2>
</div>
<form method="post" action="?controller=comment&amp;action=save&amp;task_id=<?= $task['id'] ?>" autocomplete="off">
<?= Helper\form_csrf() ?>
<?= Helper\form_hidden('task_id', $values) ?>
<?= Helper\form_hidden('user_id', $values) ?>
<?= Helper\form_textarea('comment', $values, $errors, array('autofocus', 'required', 'placeholder="'.t('Leave a comment').'"'), 'comment-textarea') ?><br/>
<div class="form-help"><a href="http://kanboard.net/documentation/syntax-guide" target="_blank" rel="noreferrer"><?= t('Write your text in Markdown') ?></a></div>
<div class="form-actions">
<input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
<?= t('or') ?>
<a href="?controller=task&amp;action=show&amp;task_id=<?= $task['id'] ?>"><?= t('cancel') ?></a>
</div>
</form>

View file

@ -0,0 +1,16 @@
<div class="page-header">
<h2><?= t('Edit a comment') ?></h2>
</div>
<form method="post" action="?controller=comment&amp;action=update&amp;task_id=<?= $task['id'] ?>&amp;comment_id=<?= $comment['id'] ?>" autocomplete="off">
<?= Helper\form_csrf() ?>
<?= Helper\form_hidden('id', $values) ?>
<?= Helper\form_textarea('comment', $values, $errors, array('autofocus', 'required', 'placeholder="'.t('Leave a comment').'"'), 'comment-textarea') ?><br/>
<div class="form-actions">
<input type="submit" value="<?= t('Update') ?>" class="btn btn-blue"/>
<?= t('or') ?>
<a href="?controller=task&amp;action=show&amp;task_id=<?= $task['id'] ?>"><?= t('cancel') ?></a>
</div>
</form>

View file

@ -0,0 +1,9 @@
<section id="main">
<div class="page-header">
<h2><?= t('Forbidden') ?></h2>
</div>
<p class="alert alert-error">
<?= t('Only administrators or the creator of the comment can access to this page.') ?>
</p>
</section>

View file

@ -0,0 +1,16 @@
<div class="page-header">
<h2><?= t('Remove a comment') ?></h2>
</div>
<div class="confirm">
<p class="alert alert-info">
<?= t('Do you really want to remove this comment?') ?>
</p>
<?= Helper\template('comment_show', array('comment' => $comment, 'task' => $task, 'preview' => true)) ?>
<div class="form-actions">
<a href="?controller=comment&amp;action=remove&amp;task_id=<?= $task['id'] ?>&amp;comment_id=<?= $comment['id'].Helper\param_csrf() ?>" class="btn btn-red"><?= t('Yes') ?></a>
<?= t('or') ?> <a href="?controller=task&amp;action=show&amp;task_id=<?= $task['id'] ?>#comment-<?= $comment['id'] ?>"><?= t('cancel') ?></a>
</div>
</div>

View file

@ -0,0 +1,28 @@
<div class="comment <?= isset($preview) ? 'comment-preview' : '' ?>" id="comment-<?= $comment['id'] ?>">
<p class="comment-title">
<span class="comment-username"><?= Helper\escape($comment['username']) ?></span> @ <span class="comment-date"><?= dt('%B %e, %G at %k:%M %p', $comment['date']) ?></span>
</p>
<div class="comment-inner">
<?php if (! isset($preview)): ?>
<ul class="comment-actions">
<li><a href="#comment-<?= $comment['id'] ?>"><?= t('link') ?></a></li>
<?php if (Helper\is_admin() || Helper\is_current_user($comment['user_id'])): ?>
<li>
<a href="?controller=comment&amp;action=confirm&amp;task_id=<?= $task['id'] ?>&amp;comment_id=<?= $comment['id'] ?>"><?= t('remove') ?></a>
</li>
<li>
<a href="?controller=comment&amp;action=edit&amp;task_id=<?= $task['id'] ?>&amp;comment_id=<?= $comment['id'] ?>"><?= t('edit') ?></a>
</li>
<?php endif ?>
</ul>
<?php endif ?>
<div class="markdown">
<?= Helper\parse($comment['comment']) ?>
</div>
</div>
</div>

View file

@ -0,0 +1,126 @@
<section id="main">
<?php if ($user['is_admin']): ?>
<div class="page-header">
<h2><?= t('Application settings') ?></h2>
</div>
<section>
<form method="post" action="?controller=config&amp;action=save" autocomplete="off">
<?= Helper\form_csrf() ?>
<?= Helper\form_label(t('Language'), 'language') ?>
<?= Helper\form_select('language', $languages, $values, $errors) ?><br/>
<?= Helper\form_label(t('Timezone'), 'timezone') ?>
<?= Helper\form_select('timezone', $timezones, $values, $errors) ?><br/>
<div class="form-actions">
<input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
</div>
</form>
</section>
<?php endif ?>
<div class="page-header">
<h2><?= t('User settings') ?></h2>
</div>
<section class="settings">
<ul>
<li>
<strong><?= t('My default project:') ?> </strong>
<?= (isset($user['default_project_id']) && isset($projects[$user['default_project_id']])) ? Helper\escape($projects[$user['default_project_id']]) : t('None') ?>,
<a href="?controller=user&amp;action=edit&amp;user_id=<?= $user['id'] ?>"><?= t('edit') ?></a>
</li>
</ul>
</section>
<?php if ($user['is_admin']): ?>
<div class="page-header">
<h2><?= t('More information') ?></h2>
</div>
<section class="settings">
<ul>
<li><a href="?controller=config&amp;action=tokens<?= Helper\param_csrf() ?>"><?= t('Reset all tokens') ?></a></li>
<li>
<?= t('Webhooks token:') ?>
<strong><?= Helper\escape($values['webhooks_token']) ?></strong>
</li>
<li>
<?= t('API token:') ?>
<strong><?= Helper\escape($values['api_token']) ?></strong>
</li>
<?php if (DB_DRIVER === 'sqlite'): ?>
<li>
<?= t('Database size:') ?>
<strong><?= Helper\format_bytes($db_size) ?></strong>
</li>
<li>
<a href="?controller=config&amp;action=downloadDb<?= Helper\param_csrf() ?>"><?= t('Download the database') ?></a>
<?= t('(Gzip compressed Sqlite file)') ?>
</li>
<li>
<a href="?controller=config&amp;action=optimizeDb <?= Helper\param_csrf() ?>"><?= t('Optimize the database') ?></a>
<?= t('(VACUUM command)') ?>
</li>
<?php endif ?>
<li>
<?= t('Official website:') ?>
<a href="http://kanboard.net/" target="_blank" rel="noreferer">http://kanboard.net/</a>
</li>
<li>
<?= t('Application version:') ?>
<?= APP_VERSION ?>
</li>
</ul>
</section>
<?php endif ?>
<div class="page-header" id="last-logins">
<h2><?= t('Last logins') ?></h2>
</div>
<?php if (! empty($last_logins)): ?>
<table class="table-small table-hover">
<tr>
<th><?= t('Login date') ?></th>
<th><?= t('Authentication method') ?></th>
<th><?= t('IP address') ?></th>
<th><?= t('User agent') ?></th>
</tr>
<?php foreach($last_logins as $login): ?>
<tr>
<td><?= dt('%B %e, %G at %k:%M %p', $login['date_creation']) ?></td>
<td><?= Helper\escape($login['auth_type']) ?></td>
<td><?= Helper\escape($login['ip']) ?></td>
<td><?= Helper\escape($login['user_agent']) ?></td>
</tr>
<?php endforeach ?>
</table>
<?php endif ?>
<div class="page-header" id="remember-me">
<h2><?= t('Persistent connections') ?></h2>
</div>
<?php if (empty($remember_me_sessions)): ?>
<p class="alert alert-info"><?= t('No session') ?></p>
<?php else: ?>
<table class="table-small table-hover">
<tr>
<th><?= t('Creation date') ?></th>
<th><?= t('Expiration date') ?></th>
<th><?= t('IP address') ?></th>
<th><?= t('User agent') ?></th>
<th><?= t('Action') ?></th>
</tr>
<?php foreach($remember_me_sessions as $session): ?>
<tr>
<td><?= dt('%B %e, %G at %k:%M %p', $session['date_creation']) ?></td>
<td><?= dt('%B %e, %G at %k:%M %p', $session['expiration']) ?></td>
<td><?= Helper\escape($session['ip']) ?></td>
<td><?= Helper\escape($session['user_agent']) ?></td>
<td><a href="?controller=config&amp;action=removeRememberMeToken&amp;id=<?= $session['id'].Helper\param_csrf() ?>"><?= t('Remove') ?></a></td>
</tr>
<?php endforeach ?>
</table>
<?php endif ?>
</section>

View file

@ -0,0 +1,14 @@
<div class="page-header">
<h2><?= t('Attach a document') ?></h2>
</div>
<form action="?controller=file&amp;action=save&amp;task_id=<?= $task['id'] ?>" method="post" enctype="multipart/form-data">
<?= Helper\form_csrf() ?>
<input type="file" name="files[]" multiple />
<div class="form-help"><?= t('Maximum size: ') ?><?= is_integer($max_size) ? Helper\format_bytes($max_size) : $max_size ?></div>
<div class="form-actions">
<input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
<?= t('or') ?>
<a href="?controller=task&amp;action=show&amp;task_id=<?= $task['id'] ?>"><?= t('cancel') ?></a>
</div>
</form>

View file

@ -0,0 +1,6 @@
<div class="page-header">
<h2><?= Helper\escape($file['name']) ?></h2>
<div class="task-file-viewer">
<img src="?controller=file&amp;action=image&amp;file_id=<?= $file['id'] ?>&amp;task_id=<?= $file['task_id'] ?>" alt="<?= Helper\escape($file['name']) ?>"/>
</div>
</div>

View file

@ -0,0 +1,14 @@
<div class="page-header">
<h2><?= t('Remove a file') ?></h2>
</div>
<div class="confirm">
<p class="alert alert-info">
<?= t('Do you really want to remove this file: "%s"?', Helper\escape($file['name'])) ?>
</p>
<div class="form-actions">
<a href="?controller=file&amp;action=remove&amp;task_id=<?= $task['id'] ?>&amp;file_id=<?= $file['id'].Helper\param_csrf() ?>" class="btn btn-red"><?= t('Yes') ?></a>
<?= t('or') ?> <a href="?controller=task&amp;action=show&amp;task_id=<?= $task['id'] ?>"><?= t('cancel') ?></a>
</div>
</div>

View file

@ -0,0 +1,17 @@
<div class="page-header">
<h2><?= t('Attachments') ?></h2>
</div>
<ul class="task-show-files">
<?php foreach ($files as $file): ?>
<li>
<a href="?controller=file&amp;action=download&amp;file_id=<?= $file['id'] ?>&amp;task_id=<?= $task['id'] ?>"><?= Helper\escape($file['name']) ?></a>
<span class="task-show-file-actions">
<?php if ($file['is_image']): ?>
<a href="?controller=file&amp;action=open&amp;file_id=<?= $file['id'] ?>&amp;task_id=<?= $task['id'] ?>" class="file-popover"><?= t('open') ?></a>,
<?php endif ?>
<a href="?controller=file&amp;action=confirm&amp;file_id=<?= $file['id'] ?>&amp;task_id=<?= $task['id'] ?>"><?= t('remove') ?></a>
</span>
</li>
<?php endforeach ?>
</ul>

View file

@ -0,0 +1,76 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<meta name="mobile-web-app-capable" content="yes">
<?= Helper\js('assets/js/jquery-1.11.1.min.js') ?>
<?= Helper\js('assets/js/jquery-ui-1.10.4.custom.min.js') ?>
<?= Helper\js('assets/js/jquery.ui.touch-punch.min.js') ?>
<?= Helper\js('assets/js/chosen.jquery.min.js') ?>
<?= Helper\js('assets/js/app.js') ?>
<?= Helper\css('assets/css/app.css') ?>
<?= Helper\css('assets/css/font-awesome.min.css') ?>
<?= Helper\css('assets/css/jquery-ui-1.10.4.custom.css'); ?>
<?= Helper\css('assets/css/chosen.min.css'); ?>
<link rel="icon" type="image/png" href="assets/img/favicon.png">
<link rel="apple-touch-icon" href="assets/img/touch-icon-iphone.png">
<link rel="apple-touch-icon" sizes="72x72" href="assets/img/touch-icon-ipad.png">
<link rel="apple-touch-icon" sizes="114x114" href="assets/img/touch-icon-iphone-retina.png">
<link rel="apple-touch-icon" sizes="144x144" href="assets/img/touch-icon-ipad-retina.png">
<title><?= isset($title) ? Helper\escape($title).' - Kanboard' : 'Kanboard' ?></title>
<?php if (isset($auto_refresh)): ?>
<meta http-equiv="refresh" content="<?= BOARD_PUBLIC_CHECK_INTERVAL ?>" >
<?php endif ?>
</head>
<body>
<?php if (isset($no_layout)): ?>
<?= $content_for_layout ?>
<?php else: ?>
<header>
<nav>
<a class="logo" href="?">kanboard</a>
<ul>
<?php if (isset($board_selector)): ?>
<li>
<select id="board-selector" data-placeholder="<?= t('Display another project') ?>">
<option value=""></option>
<?php foreach($board_selector as $board_id => $board_name): ?>
<option value="<?= $board_id ?>"><?= Helper\escape($board_name) ?></option>
<?php endforeach ?>
</select>
</li>
<?php endif ?>
<li <?= isset($menu) && $menu === 'boards' ? 'class="active"' : '' ?>>
<a href="?controller=board"><?= t('Boards') ?></a>
</li>
<li <?= isset($menu) && $menu === 'projects' ? 'class="active"' : '' ?>>
<a href="?controller=project"><?= t('Projects') ?></a>
</li>
<li <?= isset($menu) && $menu === 'users' ? 'class="active"' : '' ?>>
<a href="?controller=user"><?= t('Users') ?></a>
</li>
<li <?= isset($menu) && $menu === 'config' ? 'class="active"' : '' ?>>
<a href="?controller=config"><?= t('Settings') ?></a>
</li>
<li>
<a href="?controller=user&amp;action=logout<?= Helper\param_csrf() ?>"><?= t('Logout') ?></a>
(<?= Helper\escape(Helper\get_username()) ?>)
</li>
</ul>
</nav>
</header>
<section class="page">
<?= Helper\flash('<div class="alert alert-success alert-fade-out">%s</div>') ?>
<?= Helper\flash_error('<div class="alert alert-error">%s</div>') ?>
<?= $content_for_layout ?>
</section>
<?php endif ?>
</body>
</html>

View file

@ -0,0 +1,25 @@
<section id="main">
<div class="page-header">
<h2><?= t('Edit project') ?></h2>
<ul>
<li><a href="?controller=project"><?= t('All projects') ?></a></li>
</ul>
</div>
<section>
<form method="post" action="?controller=project&amp;action=update&amp;project_id=<?= $values['id'] ?>" autocomplete="off">
<?= Helper\form_csrf() ?>
<?= Helper\form_hidden('id', $values) ?>
<?= Helper\form_label(t('Name'), 'name') ?>
<?= Helper\form_text('name', $values, $errors, array('required')) ?>
<?= Helper\form_checkbox('is_active', t('Activated'), 1, isset($values['is_active']) && $values['is_active'] == 1 ? true : false) ?><br/>
<div class="form-actions">
<input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
<?= t('or') ?> <a href="?controller=project"><?= t('cancel') ?></a>
</div>
</form>
</section>
</section>

View file

@ -0,0 +1,100 @@
<section id="main">
<div class="page-header">
<h2><?= t('Projects') ?><span id="page-counter"> (<?= $nb_projects ?>)</span></h2>
<?php if (Helper\is_admin()): ?>
<ul>
<li><a href="?controller=project&amp;action=create"><?= t('New project') ?></a></li>
</ul>
<?php endif ?>
</div>
<section>
<?php if (empty($projects)): ?>
<p class="alert"><?= t('No project') ?></p>
<?php else: ?>
<table>
<tr>
<th><?= t('Project') ?></th>
<th><?= t('Status') ?></th>
<th><?= t('Tasks') ?></th>
<th><?= t('Board') ?></th>
<?php if (Helper\is_admin()): ?>
<th><?= t('Actions') ?></th>
<?php endif ?>
</tr>
<?php foreach ($projects as $project): ?>
<tr>
<td>
<a href="?controller=board&amp;action=show&amp;project_id=<?= $project['id'] ?>" title="project_id=<?= $project['id'] ?>"><?= Helper\escape($project['name']) ?></a>
</td>
<td>
<?= $project['is_active'] ? t('Active') : t('Inactive') ?>
</td>
<td>
<ul>
<?php if ($project['nb_tasks'] > 0): ?>
<?php if ($project['nb_active_tasks'] > 0): ?>
<li><a href="?controller=board&amp;action=show&amp;project_id=<?= $project['id'] ?>"><?= t('%d tasks on the board', $project['nb_active_tasks']) ?></a></li>
<?php endif ?>
<?php if ($project['nb_inactive_tasks'] > 0): ?>
<li><a href="?controller=project&amp;action=tasks&amp;project_id=<?= $project['id'] ?>"><?= t('%d closed tasks', $project['nb_inactive_tasks']) ?></a></li>
<?php endif ?>
<li><?= t('%d tasks in total', $project['nb_tasks']) ?></li>
<?php else: ?>
<li><?= t('no task for this project') ?></li>
<?php endif ?>
</ul>
</td>
<td>
<ul>
<?php foreach ($project['columns'] as $column): ?>
<li>
<span title="column_id=<?= $column['id'] ?>"><?= Helper\escape($column['title']) ?></span> (<?= $column['nb_active_tasks'] ?>)
</li>
<?php endforeach ?>
</ul>
</td>
<?php if (Helper\is_admin()): ?>
<td>
<ul>
<li>
<a href="?controller=category&amp;action=index&amp;project_id=<?= $project['id'] ?>"><?= t('Categories') ?></a>
</li>
<li>
<a href="?controller=project&amp;action=edit&amp;project_id=<?= $project['id'] ?>"><?= t('Edit project') ?></a>
</li>
<li>
<a href="?controller=project&amp;action=users&amp;project_id=<?= $project['id'] ?>"><?= t('Edit users access') ?></a>
</li>
<li>
<a href="?controller=board&amp;action=edit&amp;project_id=<?= $project['id'] ?>"><?= t('Edit board') ?></a>
</li>
<li>
<a href="?controller=action&amp;action=index&amp;project_id=<?= $project['id'] ?>"><?= t('Automatic actions') ?></a>
</li>
<li>
<?php if ($project['is_active']): ?>
<a href="?controller=project&amp;action=disable&amp;project_id=<?= $project['id'].Helper\param_csrf() ?>"><?= t('Disable') ?></a>
<?php else: ?>
<a href="?controller=project&amp;action=enable&amp;project_id=<?= $project['id'].Helper\param_csrf() ?>"><?= t('Enable') ?></a>
<?php endif ?>
</li>
<li>
<a href="?controller=project&amp;action=confirm&amp;project_id=<?= $project['id'] ?>"><?= t('Remove') ?></a>
</li>
<li>
<a href="?controller=board&amp;action=readonly&amp;token=<?= $project['token'] ?>" target="_blank"><?= t('Public link') ?></a>
</li>
</ul>
</td>
<?php endif ?>
</tr>
<?php endforeach ?>
</table>
<?php endif ?>
</section>
</section>

View file

@ -0,0 +1,21 @@
<section id="main">
<div class="page-header">
<h2><?= t('New project') ?></h2>
<ul>
<li><a href="?controller=project"><?= t('All projects') ?></a></li>
</ul>
</div>
<section>
<form method="post" action="?controller=project&amp;action=save" autocomplete="off">
<?= Helper\form_csrf() ?>
<?= Helper\form_label(t('Name'), 'name') ?>
<?= Helper\form_text('name', $values, $errors, array('autofocus', 'required')) ?>
<div class="form-actions">
<input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
<?= t('or') ?> <a href="?controller=project"><?= t('cancel') ?></a>
</div>
</form>
</section>
</section>

View file

@ -0,0 +1,16 @@
<section id="main">
<div class="page-header">
<h2><?= t('Remove project') ?></h2>
</div>
<div class="confirm">
<p class="alert alert-info">
<?= t('Do you really want to remove this project: "%s"?', $project['name']) ?>
</p>
<div class="form-actions">
<a href="?controller=project&amp;action=remove&amp;project_id=<?= $project['id'].Helper\param_csrf() ?>" class="btn btn-red"><?= t('Yes') ?></a>
<?= t('or') ?> <a href="?controller=project"><?= t('cancel') ?></a>
</div>
</div>
</section>

View file

@ -0,0 +1,31 @@
<section id="main">
<div class="page-header">
<h2>
<?= t('Search in the project "%s"', $project['name']) ?>
<?php if (! empty($nb_tasks)): ?>
<span id="page-counter"> (<?= $nb_tasks ?>)</span>
<?php endif ?>
</h2>
<ul>
<li><a href="?controller=board&amp;action=show&amp;project_id=<?= $project['id'] ?>"><?= t('Back to the board') ?></a></li>
<li><a href="?controller=project&amp;action=tasks&amp;project_id=<?= $project['id'] ?>"><?= t('Completed tasks') ?></a></li>
<li><a href="?controller=project&amp;action=index"><?= t('List of projects') ?></a></li>
</ul>
</div>
<section>
<form method="get" action="?" autocomplete="off">
<?= Helper\form_hidden('controller', $values) ?>
<?= Helper\form_hidden('action', $values) ?>
<?= Helper\form_hidden('project_id', $values) ?>
<?= Helper\form_text('search', $values, array(), array('autofocus', 'required', 'placeholder="'.t('Search').'"')) ?>
<input type="submit" value="<?= t('Search') ?>" class="btn btn-blue"/>
</form>
<?php if (empty($tasks) && ! empty($values['search'])): ?>
<p class="alert"><?= t('Nothing found.') ?></p>
<?php elseif (! empty($tasks)): ?>
<?= Helper\template('task_table', array('tasks' => $tasks, 'categories' => $categories, 'columns' => $columns)) ?>
<?php endif ?>
</section>
</section>

View file

@ -0,0 +1,17 @@
<section id="main">
<div class="page-header">
<h2><?= t('Completed tasks for "%s"', $project['name']) ?><span id="page-counter"> (<?= $nb_tasks ?>)</span></h2>
<ul>
<li><a href="?controller=board&amp;action=show&amp;project_id=<?= $project['id'] ?>"><?= t('Back to the board') ?></a></li>
<li><a href="?controller=project&amp;action=search&amp;project_id=<?= $project['id'] ?>"><?= t('Search') ?></a></li>
<li><a href="?controller=project&amp;action=index"><?= t('List of projects') ?></a></li>
</ul>
</div>
<section>
<?php if (empty($tasks)): ?>
<p class="alert"><?= t('No task') ?></p>
<?php else: ?>
<?= Helper\template('task_table', array('tasks' => $tasks, 'categories' => $categories, 'columns' => $columns)) ?>
<?php endif ?>
</section>
</section>

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