clean install

This commit is contained in:
2024-12-10 15:08:16 +01:00
commit e14eb2d8fd
31193 changed files with 3555714 additions and 0 deletions

View File

@@ -0,0 +1,40 @@
################
# GitLabCI template for Drupal projects.
#
# This template is designed to give any Contrib maintainer everything they need to test, without requiring modification.
# It is also designed to keep up to date with Core Development automatically through the use of include files that can be centrally maintained.
# As long as you include the project, ref and three files below, any future updates added by the Drupal Association will be used in your
# pipelines automatically. However, you can modify this template if you have additional needs for your project.
# The full documentation is on https://project.pages.drupalcode.org/gitlab_templates/
################
# For information on alternative values for 'ref' see https://project.pages.drupalcode.org/gitlab_templates/info/templates-version/
# To test a Drupal 7 project, change the first include filename from .main.yml to .main-d7.yml
include:
- project: $_GITLAB_TEMPLATES_REPO
ref: $_GITLAB_TEMPLATES_REF
file:
- "/includes/include.drupalci.main.yml"
- "/includes/include.drupalci.variables.yml"
- "/includes/include.drupalci.workflows.yml"
################
# Pipeline configuration variables are defined with default values and descriptions in the file
# https://git.drupalcode.org/project/gitlab_templates/-/blob/main/includes/include.drupalci.variables.yml
# Uncomment the lines below if you want to override any of the variables. The following is just an example.
################
variables:
_PHPUNIT_CONCURRENT: '1'
phpunit:
parallel:
matrix:
- _TARGET_DB_TYPE: 'mysql'
_TARGET_DB_VERSION: '8'
- _TARGET_DB_TYPE: 'pgsql'
_TARGET_DB_VERSION: '13.5'
- _TARGET_DB_TYPE: 'mariadb'
_TARGET_DB_VERSION: '10.6'
- _TARGET_DB_TYPE: 'sqlite'
_TARGET_DB_VERSION: '3'

339
web/modules/log/LICENSE.txt Normal file
View File

@@ -0,0 +1,339 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.

View File

@@ -0,0 +1,17 @@
{
"name": "drupal/log",
"type": "drupal-module",
"description": "Provides a log entity type for real world record keeping.",
"keywords": ["Drupal"],
"license": "GPL-2.0+",
"homepage": "https://www.drupal.org/project/log",
"support": {
"issues": "http://drupal.org/project/issues/log",
"source": "https://git.drupalcode.org/project/log"
},
"require": {
"drupal/entity": "^1.0",
"drupal/state_machine": "^1.0",
"drupal/token": "^1.0"
}
}

View File

@@ -0,0 +1,10 @@
langcode: en
status: true
dependencies:
module:
- log
id: log_clone_action
label: 'Clone'
type: log
plugin: 'log_clone_action'
configuration: { }

View File

@@ -0,0 +1,10 @@
langcode: en
status: true
dependencies:
module:
- log
id: log_delete_action
label: 'Delete log'
type: log
plugin: 'entity:delete_action:log'
configuration: { }

View File

@@ -0,0 +1,10 @@
langcode: en
status: true
dependencies:
module:
- log
id: log_mark_as_done_action
label: 'Mark as done'
type: log
plugin: log_mark_as_done_action
configuration: { }

View File

@@ -0,0 +1,10 @@
langcode: en
status: true
dependencies:
module:
- log
id: log_mark_as_pending_action
label: 'Mark as pending'
type: log
plugin: log_mark_as_pending_action
configuration: { }

View File

@@ -0,0 +1,10 @@
langcode: en
status: true
dependencies:
module:
- log
id: log_reschedule_action
label: 'Reschedule'
type: log
plugin: log_reschedule_action
configuration: { }

View File

@@ -0,0 +1,808 @@
langcode: en
status: true
dependencies:
module:
- log
- options
- user
id: log_admin
label: 'Log admin'
module: views
description: 'Find and manage log entities.'
tag: default
base_table: log_field_data
base_field: id
display:
default:
id: default
display_title: Master
display_plugin: default
position: 0
display_options:
title: Logs
fields:
log_bulk_form:
id: log_bulk_form
table: log
field: log_bulk_form
relationship: none
group_type: group
admin_label: ''
entity_type: log
plugin_id: bulk_form
label: 'Bulk update'
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
action_title: 'With selection'
include_exclude: exclude
selected_actions: { }
status:
id: status
table: log_field_data
field: status
relationship: none
group_type: group
admin_label: ''
entity_type: log
entity_field: status
plugin_id: field
label: Status
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
click_sort_column: value
type: list_default
settings: { }
group_column: value
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
timestamp:
id: timestamp
table: log_field_data
field: timestamp
relationship: none
group_type: group
admin_label: ''
entity_type: log
entity_field: timestamp
plugin_id: log_field
label: Timestamp
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
click_sort_column: value
type: timestamp
settings:
date_format: medium
custom_date_format: ''
timezone: ''
group_column: value
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
id:
id: id
table: log_field_data
field: id
relationship: none
group_type: group
admin_label: ''
entity_type: log
entity_field: id
plugin_id: field
label: ID
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
click_sort_column: value
type: number_integer
settings:
thousand_separator: ''
prefix_suffix: true
group_column: value
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
name:
id: name
table: log_field_data
field: name
relationship: none
group_type: group
admin_label: ''
entity_type: log
entity_field: name
plugin_id: field
label: Name
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
click_sort_column: value
type: string
settings:
link_to_entity: true
group_column: value
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
type:
id: type
table: log_field_data
field: type
relationship: none
group_type: group
admin_label: ''
entity_type: log
entity_field: type
plugin_id: field
label: 'Log type'
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
click_sort_column: target_id
type: entity_reference_label
settings:
link: false
group_column: target_id
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
uid:
id: uid
table: log_field_data
field: uid
relationship: none
group_type: group
admin_label: ''
entity_type: log
entity_field: uid
plugin_id: field
label: 'Authored by'
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
click_sort_column: target_id
type: entity_reference_label
settings:
link: true
group_column: target_id
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
operations:
id: operations
table: log
field: operations
relationship: none
group_type: group
admin_label: ''
entity_type: null
entity_field: null
plugin_id: entity_operations
label: 'Operations links'
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
destination: true
pager:
type: full
options:
offset: 0
items_per_page: 25
total_pages: null
id: 0
tags:
next: 'Next '
previous: ' Previous'
first: '« First'
last: 'Last »'
expose:
items_per_page: false
items_per_page_label: 'Items per page'
items_per_page_options: '5, 10, 25, 50'
items_per_page_options_all: false
items_per_page_options_all_label: '- All -'
offset: false
offset_label: Offset
quantity: 9
exposed_form:
type: basic
options:
submit_button: Filter
reset_button: false
reset_button_label: Reset
exposed_sorts_label: 'Sort by'
expose_sort_order: true
sort_asc_label: Asc
sort_desc_label: Desc
access:
type: perm
options:
perm: 'administer log'
cache:
type: tag
options: { }
empty:
area_text_custom:
id: area_text_custom
table: views
field: area_text_custom
relationship: none
group_type: group
admin_label: ''
plugin_id: text_custom
empty: true
content: 'No log entities available.'
tokenize: false
sorts:
timestamp:
id: timestamp
table: log_field_data
field: timestamp
relationship: none
group_type: group
admin_label: ''
entity_type: log
entity_field: timestamp
plugin_id: log_standard
order: DESC
expose:
label: ''
field_identifier: timestamp
exposed: false
granularity: second
arguments: { }
filters:
name:
id: name
table: log_field_data
field: name
relationship: none
group_type: group
admin_label: ''
entity_type: log
entity_field: name
plugin_id: string
operator: contains
value: ''
group: 1
exposed: true
expose:
operator_id: name_op
label: Name
description: ''
use_operator: false
operator: name_op
operator_limit_selection: false
operator_list: { }
identifier: name
required: false
remember: false
multiple: false
remember_roles:
authenticated: authenticated
anonymous: '0'
administrator: '0'
placeholder: ''
is_grouped: false
group_info:
label: ''
description: ''
identifier: ''
optional: true
widget: select
multiple: false
remember: false
default_group: All
default_group_multiple: { }
group_items: { }
type:
id: type
table: log_field_data
field: type
relationship: none
group_type: group
admin_label: ''
entity_type: log
entity_field: type
plugin_id: bundle
operator: in
value: { }
group: 1
exposed: true
expose:
operator_id: type_op
label: 'Log type'
description: ''
use_operator: false
operator: type_op
operator_limit_selection: false
operator_list: { }
identifier: type
required: false
remember: false
multiple: false
remember_roles:
authenticated: authenticated
anonymous: '0'
administrator: '0'
reduce: false
is_grouped: false
group_info:
label: ''
description: ''
identifier: ''
optional: true
widget: select
multiple: false
remember: false
default_group: All
default_group_multiple: { }
group_items: { }
filter_groups:
operator: AND
groups:
1: AND
style:
type: table
options:
grouping: { }
row_class: ''
default_row_class: true
columns:
log_bulk_form: log_bulk_form
status: status
timestamp: timestamp
id: id
name: name
type: type
uid: uid
operations: operations
default: timestamp
info:
log_bulk_form:
align: ''
separator: ''
empty_column: false
responsive: ''
status:
sortable: true
default_sort_order: asc
align: ''
separator: ''
empty_column: false
responsive: ''
timestamp:
sortable: true
default_sort_order: desc
align: ''
separator: ''
empty_column: false
responsive: ''
id:
sortable: true
default_sort_order: desc
align: ''
separator: ''
empty_column: false
responsive: ''
name:
sortable: true
default_sort_order: asc
align: ''
separator: ''
empty_column: false
responsive: ''
type:
sortable: true
default_sort_order: asc
align: ''
separator: ''
empty_column: false
responsive: ''
uid:
sortable: true
default_sort_order: asc
align: ''
separator: ''
empty_column: false
responsive: ''
operations:
align: ''
separator: ''
empty_column: false
responsive: ''
override: true
sticky: false
summary: ''
empty_table: true
caption: ''
description: ''
row:
type: fields
query:
type: views_query
options:
query_comment: ''
disable_sql_rewrite: false
distinct: false
replica: false
query_tags: { }
relationships: { }
header: { }
footer: { }
display_extenders: { }
cache_metadata:
max-age: 0
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- url
- url.query_args
- user.permissions
tags: { }
collection:
id: collection
display_title: Page
display_plugin: page
position: 1
display_options:
display_extenders: { }
path: admin/content/log
menu:
type: tab
title: Logs
description: ''
weight: 0
expanded: false
menu_name: admin
parent: ''
context: '0'
tab_options:
type: normal
title: Log
description: ''
weight: 0
cache_metadata:
max-age: 0
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- url
- url.query_args
- user.permissions
tags: { }

View File

@@ -0,0 +1,59 @@
# Schema for the configuration files of the log module.
log.type.*:
type: config_entity
label: 'Log type'
mapping:
id:
type: string
label: 'Machine-readable name'
label:
type: label
label: 'Type'
description:
type: text
label: 'Description'
name_pattern:
type: string
label: 'Pattern for auto-generating the log name, using tokens.'
workflow:
type: string
label: 'Workflow'
new_revision:
type: boolean
label: 'Create new revision'
condition.plugin.log_type:
type: condition.plugin
mapping:
bundles:
type: sequence
sequence:
type: string
views.sort.log_standard:
type: views_sort
label: 'Timestamp sort'
mapping:
granularity:
type: string
label: 'Granularity'
views.field.log_field:
type: views.field.field
label: 'Timestamp sort'
action.configuration.log_clone_action:
type: action_configuration_default
label: 'Configuration for the log clone action'
action.configuration.log_mark_as_done_action:
type: action_configuration_default
label: 'Configuration for the log mark as done action'
action.configuration.log_mark_as_pending_action:
type: action_configuration_default
label: 'Configuration for the log mark as pending action'
action.configuration.log_reschedule_action:
type: action_configuration_default
label: 'Configuration for the log reschedule action'

View File

@@ -0,0 +1,17 @@
name: Log
type: module
description: Provides a log entity type for real world record keeping.
core_version_requirement: ^9 || ^10 || ^11
package: Log
dependencies:
- drupal:system
- drupal:user
- drupal:views
- entity:entity
- state_machine:state_machine
- token:token
# Information added by Drupal.org packaging script on 2024-07-18
version: '2.3.0'
project: 'log'
datestamp: 1721317388

View File

@@ -0,0 +1,11 @@
entity.log.add_page:
route_name: 'entity.log.add_page'
title: 'Add Log'
appears_on:
- entity.log.collection
entity.log_type.add_form:
route_name: 'entity.log_type.add_form'
title: 'Add Log type'
appears_on:
- entity.log_type.collection

View File

@@ -0,0 +1,5 @@
entity.log_type.collection:
title: 'Log types'
route_name: entity.log_type.collection
description: 'List of log types'
parent: system.admin_structure

View File

@@ -0,0 +1,4 @@
entity.log.collection:
route_name: entity.log.collection
title: 'Logs'
base_route: system.admin_content

123
web/modules/log/log.module Normal file
View File

@@ -0,0 +1,123 @@
<?php
/**
* @file
* Hooks and customizations for the log module.
*/
use Drupal\Core\Render\Element;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\log\Entity\LogInterface;
use Drupal\log\Event\LogEvent;
/**
* Implements hook_help().
*/
function log_help($route_name, RouteMatchInterface $route_match) {
$output = '';
// Main module help for the log module.
if ($route_name == 'help.page.log') {
$output = '';
$output .= '<h3>' . t('About') . '</h3>';
$output .= '<p>' . t('Provides Log entity') . '</p>';
}
return $output;
}
/**
* Implements hook_ENTITY_TYPE_presave().
*/
function log_log_presave(LogInterface $log) {
// Dispatch an event on log presave.
// @todo Replace this with core event via https://www.drupal.org/node/2551893.
$event = new LogEvent($log);
$event_dispatcher = \Drupal::service('event_dispatcher');
$event_dispatcher->dispatch($event, LogEvent::PRESAVE);
}
/**
* Implements hook_ENTITY_TYPE_insert().
*/
function log_log_insert(LogInterface $log) {
// Dispatch an event on log insert.
// @todo Replace this with core event via https://www.drupal.org/node/2551893.
$event = new LogEvent($log);
$event_dispatcher = \Drupal::service('event_dispatcher');
$event_dispatcher->dispatch($event, LogEvent::INSERT);
}
/**
* Implements hook_ENTITY_TYPE_update().
*/
function log_log_update(LogInterface $log) {
// Dispatch an event on log update.
// @todo Replace this with core event via https://www.drupal.org/node/2551893.
$event = new LogEvent($log);
$event_dispatcher = \Drupal::service('event_dispatcher');
$event_dispatcher->dispatch($event, LogEvent::UPDATE);
}
/**
* Implements hook_ENTITY_TYPE_delete().
*/
function log_log_delete(LogInterface $log) {
// Dispatch an event on log delete.
// @todo Replace this with core event via https://www.drupal.org/node/2551893.
$event = new LogEvent($log);
$event_dispatcher = \Drupal::service('event_dispatcher');
$event_dispatcher->dispatch($event, LogEvent::DELETE);
}
/**
* Implements hook_theme().
*/
function log_theme() {
return [
'log' => [
'render element' => 'elements',
],
];
}
/**
* Implements hook_theme_suggestions_HOOK().
*/
function log_theme_suggestions_log(array $variables) {
$suggestions = [];
$log = $variables['elements']['#log'];
$sanitized_view_mode = strtr($variables['elements']['#view_mode'], '.', '_');
$suggestions[] = 'log__' . $sanitized_view_mode;
$suggestions[] = 'log__' . $log->bundle();
$suggestions[] = 'log__' . $log->bundle() . '__' . $sanitized_view_mode;
$suggestions[] = 'log__' . $log->id();
$suggestions[] = 'log__' . $log->id() . '__' . $sanitized_view_mode;
return $suggestions;
}
/**
* Prepares variables for log templates.
*
* Default template: log.html.twig.
*
* @param array $variables
* An associative array containing:
* - elements: An associative array containing the log information and any
* fields attached to the log. Properties used:
* - #log: A \Drupal\log\Entity\Log object. The log entity.
* - attributes: HTML attributes for the containing element.
*/
function template_preprocess_log(&$variables) {
$variables['log'] = $variables['elements']['#log'];
// Helpful $content variable for templates.
foreach (Element::children($variables['elements']) as $key) {
$variables['content'][$key] = $variables['elements'][$key];
}
}

View File

@@ -0,0 +1,12 @@
administer log types:
title: 'Administer log types'
description: 'Maintain the types of content available and the fields that are associated with those types.'
restrict access: true
view all log revisions:
title: 'View all log revisions'
description: 'Allow viewing log entity revisions.'
restrict access: true
revert all log revisions:
title: 'Revert all log revisions'
description: 'Allow reverting to a previous log entity revision.'
restrict access: true

View File

@@ -0,0 +1,20 @@
log.autocomplete.name:
path: '/log/{log_bundle}/autocomplete'
defaults:
_controller: '\Drupal\log\Controller\LogAutocompleteController::autocomplete'
requirements:
_entity_create_access: 'log:{log_bundle}'
log.log_clone_action_form:
path: '/admin/content/log/clone'
defaults:
_form: 'Drupal\log\Form\LogCloneActionForm'
requirements:
_entity_create_any_access: 'log'
log.log_schedule_action_form:
path: '/admin/content/log/reschedule'
defaults:
_form: 'Drupal\log\Form\LogRescheduleActionForm'
requirements:
_user_is_logged_in: 'TRUE'

View File

@@ -0,0 +1,6 @@
services:
log.log_route_context:
class: Drupal\log\ContextProvider\LogRouteContext
arguments: ['@current_route_match']
tags:
- { name: 'context_provider' }

View File

@@ -0,0 +1,3 @@
log:
label: Log
entity_type: log

View File

@@ -0,0 +1,18 @@
log_default:
id: log_default
group: log
label: 'Default'
states:
pending:
label: Pending
done:
label: Done
transitions:
done:
label: 'Done'
from: [pending]
to: done
to_pending:
label: 'Move to Pending'
from: [done]
to: pending

View File

@@ -0,0 +1,78 @@
<?php
namespace Drupal\log\ContextProvider;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Plugin\Context\Context;
use Drupal\Core\Plugin\Context\ContextProviderInterface;
use Drupal\Core\Plugin\Context\EntityContext;
use Drupal\Core\Plugin\Context\EntityContextDefinition;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\log\Entity\Log;
/**
* Sets the current log as a context on log routes.
*/
class LogRouteContext implements ContextProviderInterface {
use StringTranslationTrait;
/**
* The route match object.
*
* @var \Drupal\Core\Routing\RouteMatchInterface
*/
protected $routeMatch;
/**
* Constructs a new LogRouteContext.
*
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The route match object.
*/
public function __construct(RouteMatchInterface $route_match) {
$this->routeMatch = $route_match;
}
/**
* {@inheritdoc}
*/
public function getRuntimeContexts(array $unqualified_context_ids) {
$result = [];
$context_definition = EntityContextDefinition::create('log')->setRequired(FALSE);
$value = NULL;
if (($route_object = $this->routeMatch->getRouteObject())) {
$route_contexts = $route_object->getOption('parameters');
// Check for a log revision parameter first.
if (isset($route_contexts['log_revision']) && $revision = $this->routeMatch->getParameter('log_revision')) {
$value = $revision;
}
elseif (isset($route_contexts['log']) && $log = $this->routeMatch->getParameter('log')) {
$value = $log;
}
elseif ($this->routeMatch->getRouteName() == 'log.add') {
$log_type = $this->routeMatch->getParameter('log_type');
$value = Log::create(['type' => $log_type->id()]);
}
}
$cacheability = new CacheableMetadata();
$cacheability->setCacheContexts(['route']);
$context = new Context($context_definition, $value);
$context->addCacheableDependency($cacheability);
$result['log'] = $context;
return $result;
}
/**
* {@inheritdoc}
*/
public function getAvailableContexts() {
$context = EntityContext::fromEntityTypeId('log', $this->t('Log from URL'));
return ['log' => $context];
}
}

View File

@@ -0,0 +1,122 @@
<?php
namespace Drupal\log\Controller;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Database\Connection;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
/**
* Returns autocomplete responses for log names.
*/
class LogAutocompleteController extends ControllerBase {
/**
* The database service.
*
* @var \Drupal\Core\Database\Connection
*/
protected $database;
/**
* Constructs a LogAutocompleteController object.
*
* @param \Drupal\Core\Database\Connection $database
* A database connection.
*/
public function __construct(Connection $database) {
$this->database = $database;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('database')
);
}
/**
* Retrieves suggestions for log name autocompletion.
*
* @param string $log_bundle
* The log bundle name.
* @param \Symfony\Component\HttpFoundation\Request $request
* The current request.
*
* @return \Symfony\Component\HttpFoundation\JsonResponse
* A JSON response containing autocomplete suggestions.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
*/
public function autocomplete(string $log_bundle, Request $request) {
$matches = [];
if ($input = $request->query->get('q')) {
// A regular database query is used so the results returned can be sorted
// by usage.
$table_mapping = $this->entityTypeManager()->getStorage('log')->getTableMapping();
$query = $this->database->select($table_mapping->getDataTable(), 'log_field_data');
$query->fields('log_field_data', ['name']);
$query->addExpression('COUNT(name)', 'count');
$query->condition('type', $log_bundle);
$query->condition('name', '%' . $this->database->escapeLike($input) . '%', 'LIKE');
// Because a regular database query is used to sort by the usage of the
// log names, a minimal access control is done here.
// If the user has administer log or can view any log entity from any
// bundle, no further condition is added, if the user can see their own
// entities, the query is restricted by user, otherwise an empty set is
// returned.
switch ($this->typeOfAccess($log_bundle)) {
case 'none':
return new JsonResponse([]);
case 'own':
$query->condition('uid', $this->currentUser()->id());
break;
case 'any':
default:
// Nothing to do, full access.
}
$query->groupBy('name');
$query->orderBy('count', 'DESC');
$query->orderBy('name', 'ASC');
$matches = $query->execute()->fetchCol();
}
return new JsonResponse($matches);
}
/**
* Helper function that returns what filter must be applied to the user query.
*
* @param string $log_bundle
* The log bundle.
*
* @return string
* 'any' => Full access.
* 'own' => Access to own logs.
* 'none' => No access to logs.
*/
protected function typeOfAccess(string $log_bundle) {
$account = $this->currentUser();
if ($account->hasPermission('administer log') || $account->hasPermission('view any ' . $log_bundle . ' log')) {
return 'any';
}
if ($account->hasPermission('view own ' . $log_bundle . ' log')) {
return 'own';
}
return 'none';
}
}

View File

@@ -0,0 +1,289 @@
<?php
namespace Drupal\log\Entity;
use Drupal\Core\Entity\EntityChangedTrait;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\RevisionLogEntityTrait;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\entity\Revision\RevisionableContentEntityBase;
use Drupal\user\EntityOwnerTrait;
/**
* Defines the Log entity.
*
* @ingroup log
*
* @ContentEntityType(
* id = "log",
* label = @Translation("Log"),
* bundle_label = @Translation("Log type"),
* label_collection = @Translation("Logs"),
* label_singular = @Translation("log"),
* label_plural = @Translation("logs"),
* label_count = @PluralTranslation(
* singular = "@count log",
* plural = "@count logs",
* ),
* handlers = {
* "storage" = "Drupal\log\LogStorage",
* "access" = "\Drupal\entity\UncacheableEntityAccessControlHandler",
* "list_builder" = "\Drupal\log\LogListBuilder",
* "permission_provider" = "\Drupal\entity\UncacheableEntityPermissionProvider",
* "view_builder" = "Drupal\Core\Entity\EntityViewBuilder",
* "views_data" = "Drupal\log\LogViewsData",
* "form" = {
* "add" = "Drupal\log\Form\LogForm",
* "edit" = "Drupal\log\Form\LogForm",
* "delete" = "Drupal\Core\Entity\ContentEntityDeleteForm",
* },
* "route_provider" = {
* "default" = "Drupal\entity\Routing\AdminHtmlRouteProvider",
* "revision" = "\Drupal\entity\Routing\RevisionRouteProvider",
* "delete-multiple" = "\Drupal\entity\Routing\DeleteMultipleRouteProvider",
* },
* "local_task_provider" = {
* "default" = "\Drupal\entity\Menu\DefaultEntityLocalTaskProvider",
* },
* },
* base_table = "log",
* data_table = "log_field_data",
* revision_table = "log_revision",
* translatable = TRUE,
* revisionable = TRUE,
* show_revision_ui = TRUE,
* admin_permission = "administer log",
* entity_keys = {
* "id" = "id",
* "revision" = "revision_id",
* "bundle" = "type",
* "label" = "name",
* "owner" = "uid",
* "uuid" = "uuid",
* "langcode" = "langcode",
* },
* bundle_entity_type = "log_type",
* field_ui_base_route = "entity.log_type.edit_form",
* common_reference_target = TRUE,
* permission_granularity = "bundle",
* links = {
* "canonical" = "/log/{log}",
* "add-page" = "/log/add",
* "add-form" = "/log/add/{log_type}",
* "collection" = "/admin/content/log",
* "delete-form" = "/log/{log}/delete",
* "delete-multiple-form" = "/log/delete",
* "edit-form" = "/log/{log}/edit",
* "revision" = "/log/{log}/revisions/{log_revision}/view",
* "revision-revert-form" = "/log/{log}/revisions/{log_revision}/revert",
* "version-history" = "/log/{log}/revisions",
* },
* revision_metadata_keys = {
* "revision_user" = "revision_user",
* "revision_created" = "revision_created",
* "revision_log_message" = "revision_log_message"
* },
* )
*/
class Log extends RevisionableContentEntityBase implements LogInterface {
use EntityChangedTrait;
use EntityOwnerTrait;
use RevisionLogEntityTrait;
/**
* {@inheritdoc}
*/
public function label() {
return $this->getName();
}
/**
* {@inheritdoc}
*/
public function getName() {
return $this->get('name')->value;
}
/**
* {@inheritdoc}
*/
public function setName($name) {
$this->set('name', $name);
return $this;
}
/**
* {@inheritdoc}
*/
public function getCreatedTime() {
return $this->get('created')->value;
}
/**
* {@inheritdoc}
*/
public function setCreatedTime($timestamp) {
$this->set('created', $timestamp);
return $this;
}
/**
* {@inheritdoc}
*/
public function getTypeNamePattern() {
/** @var \Drupal\log\Entity\LogTypeInterface $type */
$type = \Drupal::entityTypeManager()
->getStorage('log_type')
->load($this->bundle());
return $type->getNamePattern();
}
/**
* {@inheritdoc}
*/
public function getBundleLabel() {
/** @var \Drupal\log\Entity\LogTypeInterface $type */
$type = \Drupal::entityTypeManager()
->getStorage('log_type')
->load($this->bundle());
return $type->label();
}
/**
* {@inheritdoc}
*/
public static function getCurrentUserId() {
return [\Drupal::currentUser()->id()];
}
/**
* {@inheritdoc}
*/
public static function getRequestTime() {
return \Drupal::time()->getRequestTime();
}
/**
* {@inheritdoc}
*/
public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
$fields = parent::baseFieldDefinitions($entity_type);
$fields += static::ownerBaseFieldDefinitions($entity_type);
$fields += static::revisionLogBaseFieldDefinitions($entity_type);
$fields['name'] = BaseFieldDefinition::create('string')
->setLabel(t('Name'))
->setDescription(t('The name of the log. Leave this blank to automatically generate a name.'))
->setRevisionable(TRUE)
->setTranslatable(TRUE)
->setDefaultValue('')
->setSetting('max_length', 255)
->setSetting('text_processing', 0)
->setDisplayOptions('view', [
'label' => 'hidden',
'type' => 'string',
'weight' => -5,
])
->setDisplayOptions('form', [
'type' => 'string_textfield',
'weight' => -5,
])
->setDisplayConfigurable('form', TRUE);
$fields['timestamp'] = BaseFieldDefinition::create('timestamp')
->setLabel(t('Timestamp'))
->setDescription(t('Timestamp of the event being logged.'))
->setDefaultValueCallback(static::class . '::getRequestTime')
->setRevisionable(TRUE)
->setRequired(TRUE)
->setDisplayOptions('view', [
'label' => 'above',
'type' => 'timestamp',
'weight' => 10,
])
->setDisplayOptions('form', [
'type' => 'datetime_timestamp',
'weight' => 10,
])
->setDisplayConfigurable('view', TRUE)
->setDisplayConfigurable('form', TRUE);
$fields['status'] = BaseFieldDefinition::create('state')
->setLabel(t('Status'))
->setDescription(t('Indicates the status of the log.'))
->setRevisionable(TRUE)
->setRequired(TRUE)
->setSetting('max_length', 255)
->setDisplayOptions('view', [
'label' => 'hidden',
'type' => 'state_transition_form',
'weight' => 10,
])
->setDisplayOptions('form', [
'type' => 'options_select',
'weight' => 11,
])
->setDisplayConfigurable('form', TRUE)
->setDisplayConfigurable('view', TRUE)
->setSetting('workflow_callback', ['\Drupal\log\Entity\Log', 'getWorkflowId']);
$fields['uid'] = BaseFieldDefinition::create('entity_reference')
->setLabel(t('Authored by'))
->setDescription(t('The user ID of author of the log.'))
->setRevisionable(TRUE)
->setSetting('target_type', 'user')
->setSetting('handler', 'default')
->setDefaultValueCallback('Drupal\log\Entity\Log::getCurrentUserId')
->setDisplayOptions('view', [
'label' => 'hidden',
'type' => 'author',
'weight' => 0,
])
->setDisplayOptions('form', [
'type' => 'entity_reference_autocomplete',
'weight' => 12,
'settings' => [
'match_operator' => 'CONTAINS',
'size' => '60',
'autocomplete_type' => 'tags',
'placeholder' => '',
],
])
->setDisplayConfigurable('view', TRUE);
$fields['created'] = BaseFieldDefinition::create('created')
->setLabel(t('Authored on'))
->setDescription(t('The time that the log was created.'))
->setRevisionable(TRUE)
->setDefaultValueCallback(static::class . '::getRequestTime')
->setDisplayOptions('form', [
'type' => 'datetime_timestamp',
'weight' => 13,
])
->setDisplayConfigurable('view', TRUE)
->setDisplayConfigurable('form', TRUE);
$fields['changed'] = BaseFieldDefinition::create('changed')
->setLabel(t('Changed'))
->setDescription(t('The time the log was last edited.'))
->setRevisionable(TRUE);
return $fields;
}
/**
* Gets the workflow ID for the state field.
*
* @param \Drupal\log\Entity\LogInterface $log
* The log entity.
*
* @return string
* The workflow ID.
*/
public static function getWorkflowId(LogInterface $log) {
$workflow = LogType::load($log->bundle())->getWorkflowId();
return $workflow;
}
}

View File

@@ -0,0 +1,71 @@
<?php
namespace Drupal\log\Entity;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityChangedInterface;
use Drupal\Core\Entity\RevisionLogInterface;
use Drupal\user\EntityOwnerInterface;
/**
* Provides an interface for defining Log entities.
*
* @ingroup log
*/
interface LogInterface extends ContentEntityInterface, EntityChangedInterface, RevisionLogInterface, EntityOwnerInterface {
/**
* Gets the log name.
*
* @return string
* The log name.
*/
public function getName();
/**
* Sets the log name.
*
* @param string $name
* The log name.
*
* @return \Drupal\log\Entity\LogInterface
* The log entity.
*/
public function setName($name);
/**
* Gets the log creation timestamp.
*
* @return int
* Creation timestamp of the log.
*/
public function getCreatedTime();
/**
* Sets the log creation timestamp.
*
* @param int $timestamp
* Creation timestamp of the log.
*
* @return \Drupal\log\Entity\LogInterface
* The log entity.
*/
public function setCreatedTime($timestamp);
/**
* Gets the name pattern from the log type.
*
* @return string
* The name pattern.
*/
public function getTypeNamePattern();
/**
* Gets the label of the the log type.
*
* @return string
* The label of the log type.
*/
public function getBundleLabel();
}

View File

@@ -0,0 +1,193 @@
<?php
namespace Drupal\log\Entity;
use Drupal\Core\Config\Entity\ConfigEntityBundleBase;
use Drupal\Core\Entity\EntityStorageInterface;
/**
* Defines the Log type entity.
*
* @ConfigEntityType(
* id = "log_type",
* label = @Translation("Log type"),
* label_collection = @Translation("Log types"),
* label_singular = @Translation("log type"),
* label_plural = @Translation("log types"),
* label_count = @PluralTranslation(
* singular = "@count log type",
* plural = "@count log types",
* ),
* handlers = {
* "list_builder" = "Drupal\log\LogTypeListBuilder",
* "view_builder" = "Drupal\Core\Entity\EntityViewBuilder",
* "form" = {
* "add" = "Drupal\log\Form\LogTypeForm",
* "edit" = "Drupal\log\Form\LogTypeForm",
* "delete" = "\Drupal\Core\Entity\EntityDeleteForm",
* },
* "route_provider" = {
* "default" = "Drupal\entity\Routing\DefaultHtmlRouteProvider",
* },
* },
* admin_permission = "administer log types",
* config_prefix = "type",
* bundle_of = "log",
* entity_keys = {
* "id" = "id",
* "label" = "label",
* "uuid" = "uuid"
* },
* links = {
* "canonical" = "/admin/structure/log-type/{log_type}",
* "add-form" = "/admin/structure/log-type/add",
* "edit-form" = "/admin/structure/log-type/{log_type}/edit",
* "delete-form" = "/admin/structure/log-type/{log_type}/delete",
* "collection" = "/admin/structure/log-type"
* },
* config_export = {
* "id",
* "label",
* "description",
* "name_pattern",
* "workflow",
* "new_revision",
* }
* )
*/
class LogType extends ConfigEntityBundleBase implements LogTypeInterface {
/**
* The Log type ID.
*
* @var string
*/
protected $id;
/**
* The Log type label.
*
* @var string
*/
protected $label;
/**
* A brief description of this log type.
*
* @var string
*/
protected $description;
/**
* Pattern for auto-generating the log name, using tokens.
*
* @var string
*/
protected $name_pattern;
/**
* The log type workflow ID.
*
* @var string
*/
protected $workflow;
/**
* Default value of the 'Create new revision' checkbox of this log type.
*
* @var bool
*/
protected $new_revision = TRUE;
/**
* {@inheritdoc}
*/
public function getDescription() {
return $this->description;
}
/**
* {@inheritdoc}
*/
public function setDescription($description) {
return $this->set('description', $description);
}
/**
* {@inheritdoc}
*/
public function getNamePattern() {
return $this->name_pattern;
}
/**
* {@inheritdoc}
*/
public function postSave(EntityStorageInterface $storage, $update = TRUE) {
parent::postSave($storage, $update);
// If the log type id changed, update all existing logs of that type.
if ($update && $this->getOriginalId() != $this->id()) {
$update_count = \Drupal::entityTypeManager()->getStorage('log')->updateType($this->getOriginalId(), $this->id());
if ($update_count) {
\Drupal::messenger()->addMessage(\Drupal::translation()->formatPlural($update_count,
'Changed the log type of 1 post from %old-type to %type.',
'Changed the log type of @count posts from %old-type to %type.',
[
'%old-type' => $this->getOriginalId(),
'%type' => $this->id(),
]));
}
}
if ($update) {
// Clear the cached field definitions as some settings affect the field
// definitions.
\Drupal::entityTypeManager()->clearCachedDefinitions();
\Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
}
}
/**
* {@inheritdoc}
*/
public function getWorkflowId() {
return $this->workflow;
}
/**
* {@inheritdoc}
*/
public function setWorkflowId($workflow_id) {
$this->workflow = $workflow_id;
return $this;
}
/**
* {@inheritdoc}
*/
public function calculateDependencies() {
parent::calculateDependencies();
// The log type must depend on the module that provides the workflow.
$workflow_manager = \Drupal::service('plugin.manager.workflow');
$workflow = $workflow_manager->createInstance($this->getWorkflowId());
$this->calculatePluginDependencies($workflow);
return $this;
}
/**
* {@inheritdoc}
*/
public function shouldCreateNewRevision() {
return $this->new_revision;
}
/**
* {@inheritdoc}
*/
public function setNewRevision($new_revision) {
return $this->set('new_revision', $new_revision);
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace Drupal\log\Entity;
use Drupal\Core\Config\Entity\ConfigEntityInterface;
use Drupal\Core\Entity\EntityDescriptionInterface;
use Drupal\Core\Entity\RevisionableEntityBundleInterface;
/**
* Provides an interface for defining Log type entities.
*/
interface LogTypeInterface extends ConfigEntityInterface, EntityDescriptionInterface, RevisionableEntityBundleInterface {
/**
* Returns the name pattern for a log type.
*
* @return string
* The log type name pattern.
*/
public function getNamePattern();
}

View File

@@ -0,0 +1,36 @@
<?php
namespace Drupal\log\Event;
use Drupal\Component\EventDispatcher\Event;
use Drupal\log\Entity\LogInterface;
/**
* Event that is fired by log save, delete and clone operations.
*/
class LogEvent extends Event {
const PRESAVE = 'log_presave';
const INSERT = 'log_insert';
const UPDATE = 'log_update';
const DELETE = 'log_delete';
const CLONE = 'log_clone';
/**
* The Log entity.
*
* @var \Drupal\log\Entity\LogInterface
*/
public LogInterface $log;
/**
* Constructs the object.
*
* @param \Drupal\log\Entity\LogInterface $log
* The Log entity.
*/
public function __construct(LogInterface $log) {
$this->log = $log;
}
}

View File

@@ -0,0 +1,125 @@
<?php
namespace Drupal\log\Form;
use Drupal\Core\Datetime\DrupalDateTime;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\ConfirmFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\TempStore\PrivateTempStoreFactory;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Base form class for configurable actions.
*/
abstract class LogActionFormBase extends ConfirmFormBase {
/**
* The tempstore factory.
*
* @var \Drupal\Core\TempStore\PrivateTempStoreFactory
*/
protected $tempStoreFactory;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $user;
/**
* The logs to clone.
*
* @var \Drupal\log\Entity\LogInterface[]
*/
protected $logs;
/**
* The action id.
*
* @var string
*/
protected $actionId;
/**
* Constructs a LogActionFormBase form object.
*
* @param \Drupal\Core\TempStore\PrivateTempStoreFactory $temp_store_factory
* The tempstore factory.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\Session\AccountInterface $user
* The current user.
*/
public function __construct(PrivateTempStoreFactory $temp_store_factory, EntityTypeManagerInterface $entity_type_manager, AccountInterface $user) {
$this->tempStoreFactory = $temp_store_factory;
$this->entityTypeManager = $entity_type_manager;
$this->user = $user;
$this->logs = $this->tempStoreFactory->get($this->actionId)->get($this->user->id());
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('tempstore.private'),
$container->get('entity_type.manager'),
$container->get('current_user')
);
}
/**
* {@inheritdoc}
*/
public function getCancelUrl() {
return new Url('entity.log.collection');
}
/**
* {@inheritdoc}
*/
public function getDescription() {
return '';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$form['date'] = [
'#type' => 'datetime',
'#title' => $this->t('New date'),
'#default_value' => new DrupalDateTime('midnight'),
'#required' => TRUE,
];
$form['revision_message'] = [
'#type' => 'textarea',
'#title' => $this->t('Revision message'),
'#description' => $this->t("Optionally add a message to describe this change. This will appear in the log's revisions."),
'#weight' => 10,
];
return parent::buildForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$this->tempStoreFactory->get($this->actionId)->delete($this->user->id());
$form_state->setRedirectUrl($this->getCancelUrl());
}
}

View File

@@ -0,0 +1,126 @@
<?php
namespace Drupal\log\Form;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\TempStore\PrivateTempStoreFactory;
use Drupal\log\Event\LogEvent;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
/**
* Provides a log clone confirmation form.
*/
class LogCloneActionForm extends LogActionFormBase {
/**
* The action id.
*
* @var string
*/
protected $actionId = 'log_clone_action';
/**
* The event dispatcher service.
*
* @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
*/
protected $eventDispatcher;
/**
* Constructs a LogCloneActionForm form object.
*
* @param \Drupal\Core\TempStore\PrivateTempStoreFactory $temp_store_factory
* The tempstore factory.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\Session\AccountInterface $user
* The current user.
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
* The event dispatcher service.
*/
public function __construct(PrivateTempStoreFactory $temp_store_factory, EntityTypeManagerInterface $entity_type_manager, AccountInterface $user, EventDispatcherInterface $event_dispatcher) {
parent::__construct($temp_store_factory, $entity_type_manager, $user);
$this->eventDispatcher = $event_dispatcher;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('tempstore.private'),
$container->get('entity_type.manager'),
$container->get('current_user'),
$container->get('event_dispatcher'),
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'log_clone_action_form';
}
/**
* {@inheritdoc}
*/
public function getQuestion() {
return $this->formatPlural(count($this->logs), 'Are you sure you want to clone this log?', 'Are you sure you want to clone these logs?');
}
/**
* {@inheritdoc}
*/
public function getConfirmText() {
return $this->t('Clone');
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
// Filter out logs the user doesn't have access to.
$inaccessible_logs = [];
$accessible_logs = [];
$current_user = $this->currentUser();
foreach ($this->logs as $log) {
if (!$log->access('view', $current_user) || !$log->access('create', $current_user)) {
$inaccessible_logs[] = $log;
continue;
}
$accessible_logs[] = $log;
}
/** @var \Drupal\Core\Datetime\DrupalDateTime $new_date */
if ($form_state->getValue('confirm') && !empty($accessible_logs)) {
$new_date = $form_state->getValue('date');
$count = count($this->logs);
foreach ($accessible_logs as $log) {
$cloned_log = $log->createDuplicate();
$cloned_log->set('timestamp', $new_date->getTimestamp());
$cloned_log->setOwnerId($current_user->id());
$cloned_log->setRevisionLogMessage($form_state->getValue('revision_message'));
// Dispatch the log_clone event.
$event = new LogEvent($cloned_log);
$this->eventDispatcher->dispatch($event, LogEvent::CLONE);
$event->log->save();
}
$this->messenger()->addMessage($this->formatPlural($count, 'Cloned 1 log.', 'Cloned @count logs.'));
}
// Add warning message if there were inaccessible logs.
if (!empty($inaccessible_logs)) {
$inaccessible_count = count($inaccessible_logs);
$this->messenger()->addWarning($this->formatPlural($inaccessible_count, 'Could not clone @count log because you do not have the necessary permissions.', 'Could not clone @count logs because you do not have the necessary permissions.'));
}
parent::submitForm($form, $form_state);
}
}

View File

@@ -0,0 +1,167 @@
<?php
namespace Drupal\log\Form;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\Core\Datetime\DateFormatterInterface;
use Drupal\Core\Entity\ContentEntityForm;
use Drupal\Core\Entity\EntityRepositoryInterface;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Field\FieldFilteredMarkup;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Session\AccountInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Form controller for Log entities.
*
* @ingroup log
*/
class LogForm extends ContentEntityForm {
/**
* The Current User object.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
/**
* The date formatter service.
*
* @var \Drupal\Core\Datetime\DateFormatterInterface
*/
protected $dateFormatter;
/**
* Constructs a LogForm object.
*
* @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository
* The entity repository.
* @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info
* The entity type bundle service.
* @param \Drupal\Component\Datetime\TimeInterface $time
* The time service.
* @param \Drupal\Core\Session\AccountInterface $current_user
* The current user.
* @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter
* The date formatter service.
*/
public function __construct(EntityRepositoryInterface $entity_repository, EntityTypeBundleInfoInterface $entity_type_bundle_info = NULL, TimeInterface $time = NULL, AccountInterface $current_user, DateFormatterInterface $date_formatter) {
parent::__construct($entity_repository, $entity_type_bundle_info, $time);
$this->currentUser = $current_user;
$this->dateFormatter = $date_formatter;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity.repository'),
$container->get('entity_type.bundle.info'),
$container->get('datetime.time'),
$container->get('current_user'),
$container->get('date.formatter')
);
}
/**
* {@inheritdoc}
*/
public function form(array $form, FormStateInterface $form_state) {
/** @var \Drupal\log\Entity\LogInterface $log */
$log = $this->entity;
// Changed must be sent to the client, for later overwrite error checking.
$form['changed'] = [
'#type' => 'hidden',
'#default_value' => $log->getChangedTime(),
];
$form += parent::form($form, $form_state);
// Set autocomplete for log names.
if (isset($form['name']) && $this->moduleHandler->moduleExists('views')) {
$num_of_logs = $this->entityTypeManager->getStorage('log')
->getQuery()
->condition('type', $log->bundle())
->count()
->accessCheck(TRUE)
->execute();
if ($num_of_logs > 0) {
$form['name']['widget'][0]['value']['#description'] = FieldFilteredMarkup::create($form['name']['widget'][0]['value']['#description'] . ' ' . $this->t('As you type, frequently used log names will be suggested.'));
$form['name']['widget'][0]['value']['#autocomplete_route_name'] = 'log.autocomplete.name';
$form['name']['widget'][0]['value']['#autocomplete_route_parameters'] = ['log_bundle' => $log->bundle()];
}
}
$form['advanced']['#attributes']['class'][] = 'entity-meta';
$form['meta'] = [
'#type' => 'details',
'#group' => 'advanced',
'#weight' => -10,
'#title' => $this->t('Status'),
'#attributes' => ['class' => ['entity-meta__header']],
'#tree' => TRUE,
'#access' => $this->currentUser->hasPermission('administer log'),
];
$form['meta']['status'] = [
'#type' => 'item',
'#markup' => $log->get('status')->first()->getLabel(),
'#access' => !$log->isNew(),
'#$log' => ['class' => ['entity-meta__title']],
];
$form['meta']['changed'] = [
'#type' => 'item',
'#title' => $this->t('Last saved'),
'#markup' => !$log->isNew() ? $this->dateFormatter->format($log->getChangedTime(), 'short') : $this->t('Not saved yet'),
'#wrapper_attributes' => ['class' => ['entity-meta__last-saved']],
];
$form['meta']['author'] = [
'#type' => 'item',
'#title' => $this->t('Author'),
'#markup' => $log->getOwner()->getAccountName(),
'#wrapper_attributes' => ['class' => ['entity-meta__author']],
];
// Author information for administrators.
$form['author'] = [
'#type' => 'details',
'#title' => $this->t('Authoring information'),
'#group' => 'advanced',
'#weight' => 90,
'#optional' => TRUE,
];
if (isset($form['uid'])) {
$form['uid']['#group'] = 'author';
}
if (isset($form['created'])) {
$form['created']['#group'] = 'author';
}
$form['#attached']['library'][] = 'core/drupal.form';
return $form;
}
/**
* {@inheritdoc}
*/
public function save(array $form, FormStateInterface $form_state) {
$status = parent::save($form, $form_state);
$entity_url = $this->entity->toUrl()->setAbsolute()->toString();
$this->messenger()->addMessage($this->t(
'Saved log: <a href=":url">%label</a>',
[
':url' => $entity_url,
'%label' => $this->entity->label(),
],
));
$form_state->setRedirectUrl($this->entity->toUrl());
return $status;
}
}

View File

@@ -0,0 +1,182 @@
<?php
namespace Drupal\log\Form;
use Drupal\Core\Datetime\DrupalDateTime;
use Drupal\Core\Form\FormStateInterface;
/**
* Provides a log reschedule confirmation form.
*/
class LogRescheduleActionForm extends LogActionFormBase {
/**
* The action id.
*
* @var string
*/
protected $actionId = 'log_reschedule_action';
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'log_reschedule_action_form';
}
/**
* {@inheritdoc}
*/
public function getQuestion() {
return $this->formatPlural(count($this->logs), 'Are you sure you want to reschedule this log?', 'Are you sure you want to reschedule these logs?');
}
/**
* {@inheritdoc}
*/
public function getConfirmText() {
return $this->t('Reschedule');
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$form = parent::buildForm($form, $form_state);
$form['type_of_date'] = [
'#type' => 'checkbox',
'#title' => $this->t('Reschedule by a relative date'),
'#weight' => -10,
];
$form['title'] = [
'#type' => 'html_tag',
'#tag' => 'h4',
'#value' => $form['date']['#title'],
'#weight' => -9,
];
// Datetime fields need to be wrapped for #states to work.
// @see https://www.drupal.org/project/drupal/issues/2419131
$form['absolute'] = [
'#type' => 'container',
'#states' => [
'visible' => [
':input[name="type_of_date"]' => ['checked' => FALSE],
],
],
];
$form['absolute']['date'] = $form['date'];
unset($form['absolute']['date']['#title']);
$form['absolute']['date']['#required'] = FALSE;
unset($form['date']);
$form['relative'] = [
'#type' => 'container',
'#attributes' => [
'class' => ['container-inline'],
],
'#states' => [
'visible' => [
':input[name="type_of_date"]' => ['checked' => TRUE],
],
],
];
$form['relative']['amount'] = [
'#type' => 'number',
'#size' => 4,
];
$form['relative']['time'] = [
'#type' => 'select',
'#options' => [
'hour' => $this->t('Hours'),
'day' => $this->t('Days'),
'week' => $this->t('Weeks'),
'month' => $this->t('Months'),
'year' => $this->t('Years'),
],
];
return $form;
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
$type_of_date = $form_state->getValue('type_of_date');
if ($type_of_date) {
$amount = $form_state->getValue('amount');
$time = $form_state->getValue('time');
if (empty($amount)) {
$form_state->setError($form['relative']['amount'], 'Please enter the amount of time for rescheduling.');
}
if (empty($time)) {
$form_state->setError($form['relative']['amount'], 'Please enter the time units for rescheduling.');
}
}
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
// Filter out logs the user doesn't have access to.
$inaccessible_logs = [];
$accessible_logs = [];
$current_user = $this->currentUser();
foreach ($this->logs as $log) {
if (!$log->get('timestamp')->access('edit', $current_user) || !$log->get('status')->access('edit', $current_user) || !$log->access('update', $current_user)) {
$inaccessible_logs[] = $log;
continue;
}
$accessible_logs[] = $log;
}
if ($form_state->getValue('confirm') && !empty($accessible_logs)) {
$count = count($accessible_logs);
$type_of_date = $form_state->getValue('type_of_date');
if ($type_of_date) {
$amount = $form_state->getValue('amount');
$time = $form_state->getValue('time');
$sign = ($amount >= 0) ? '+' : '';
foreach ($accessible_logs as $log) {
$new_date = new DrupalDateTime();
$new_date->setTimestamp($log->get('timestamp')->value);
$new_date->modify("$sign$amount $time");
if ($log->get('status')->first()->isTransitionAllowed('to_pending')) {
$log->get('status')->first()->applyTransitionById('to_pending');
}
$log->set('timestamp', $new_date->getTimestamp());
$log->setRevisionLogMessage($form_state->getValue('revision_message'));
$log->setNewRevision(TRUE);
$log->save();
}
}
else {
/** @var \Drupal\Core\Datetime\DrupalDateTime $new_date */
$new_date = $form_state->getValue('date');
foreach ($accessible_logs as $log) {
if ($log->get('status')->first()->isTransitionAllowed('to_pending')) {
$log->get('status')->first()->applyTransitionById('to_pending');
}
$log->set('timestamp', $new_date->getTimestamp());
$log->setRevisionLogMessage($form_state->getValue('revision_message'));
$log->setNewRevision(TRUE);
$log->save();
}
}
$this->messenger()->addMessage($this->formatPlural($count, 'Rescheduled 1 log.', 'Rescheduled @count logs.'));
}
// Add warning message if there were inaccessible logs.
if (!empty($inaccessible_logs)) {
$inaccessible_count = count($inaccessible_logs);
$this->messenger()->addWarning($this->formatPlural($inaccessible_count, 'Could not reschedule @count log because you do not have the necessary permissions.', 'Could not reschedule @count logs because you do not have the necessary permissions.'));
}
parent::submitForm($form, $form_state);
}
}

View File

@@ -0,0 +1,140 @@
<?php
namespace Drupal\log\Form;
use Drupal\Core\Entity\EntityForm;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\state_machine\WorkflowManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Form controller for Log type entities.
*
* @package Drupal\log\Form
*/
class LogTypeForm extends EntityForm {
/**
* The workflow manager.
*
* @var \Drupal\state_machine\WorkflowManagerInterface
*/
protected $workflowManager;
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* Constructs a new LogTypeForm object.
*
* @param \Drupal\state_machine\WorkflowManagerInterface $workflow_manager
* The workflow manager.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
*/
public function __construct(WorkflowManagerInterface $workflow_manager, ModuleHandlerInterface $module_handler) {
$this->workflowManager = $workflow_manager;
$this->moduleHandler = $module_handler;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('plugin.manager.workflow'),
$container->get('module_handler')
);
}
/**
* {@inheritdoc}
*/
public function form(array $form, FormStateInterface $form_state) {
$form = parent::form($form, $form_state);
$log_type = $this->entity;
$form['label'] = [
'#type' => 'textfield',
'#title' => $this->t('Label'),
'#maxlength' => 255,
'#default_value' => $log_type->label(),
'#description' => $this->t('Label for the Log type.'),
'#required' => TRUE,
];
$form['id'] = [
'#type' => 'machine_name',
'#default_value' => $log_type->id(),
'#machine_name' => [
'exists' => '\Drupal\log\Entity\LogType::load',
],
'#disabled' => !$log_type->isNew(),
];
$form['description'] = [
'#type' => 'textarea',
'#title' => $this->t('Description'),
'#default_value' => $log_type->getDescription(),
];
$form['name_pattern'] = [
'#type' => 'textfield',
'#title' => $this->t('Name pattern'),
'#maxlength' => 255,
'#default_value' => $log_type->getNamePattern() ?: 'Log [log:id]',
'#description' => $this->t('When filled in, log names of this type will be auto-generated using this naming pattern. Leave empty for not auto generating log names.'),
'#required' => TRUE,
];
$form['token_help'] = [
'#theme' => 'token_tree_link',
'#token_types' => ['log'],
];
$form['workflow'] = [
'#type' => 'select',
'#title' => $this->t('Workflow'),
'#options' => $this->workflowManager->getGroupedLabels('log'),
'#default_value' => $log_type->getWorkflowId(),
'#description' => $this->t('Used by all logs of this type.'),
];
$form['new_revision'] = [
'#type' => 'checkbox',
'#title' => $this->t('Create new revision'),
'#default_value' => $log_type->shouldCreateNewRevision(),
];
return $form;
}
/**
* {@inheritdoc}
*/
public function save(array $form, FormStateInterface $form_state) {
$log_type = $this->entity;
$status = $log_type->save();
switch ($status) {
case SAVED_NEW:
$this->messenger()->addMessage($this->t('Created the %label Log type.', [
'%label' => $log_type->label(),
]));
break;
default:
$this->messenger()->addMessage($this->t('Saved the %label Log type.', [
'%label' => $log_type->label(),
]));
}
$form_state->setRedirectUrl($log_type->toUrl('collection'));
return $status;
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace Drupal\log;
use Drupal\Core\Entity\EntityInterface;
use Drupal\entity\BulkFormEntityListBuilder;
/**
* Defines a class to build a listing of Log entities.
*
* @ingroup log
*/
class LogListBuilder extends BulkFormEntityListBuilder {
/**
* {@inheritdoc}
*/
public function buildHeader() {
$header['id'] = $this->t('Log ID');
$header['label'] = $this->t('Label');
$header['type'] = $this->t('Type');
return $header + parent::buildHeader();
}
/**
* {@inheritdoc}
*/
public function buildRow(EntityInterface $entity) {
/** @var \Drupal\log\Entity\LogInterface $entity */
$row['id'] = ['#markup' => $entity->id()];
$row['name'] = $entity->toLink($entity->label(), 'canonical')->toRenderable();
$row['type'] = ['#markup' => $entity->getBundleLabel()];
return $row + parent::buildRow($entity);
}
}

View File

@@ -0,0 +1,156 @@
<?php
namespace Drupal\log;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Cache\MemoryCache\MemoryCacheInterface;
use Drupal\Core\Database\Connection;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\Sql\SqlContentEntityStorage;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Render\BubbleableMetadata;
use Drupal\Core\Utility\Token;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Defines the controller class for logs.
*
* This extends the base storage class, adding required special handling for
* log entities.
*/
class LogStorage extends SqlContentEntityStorage {
/**
* The token service.
*
* @var \Drupal\Core\Utility\Token
*/
protected $token;
/**
* Constructs a SqlContentEntityStorage object.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type definition.
* @param \Drupal\Core\Database\Connection $database
* The database connection to be used.
* @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
* The entity field manager.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache
* The cache backend to be used.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
* @param \Drupal\Core\Cache\MemoryCache\MemoryCacheInterface $memory_cache
* The memory cache backend to be used.
* @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info
* The entity type bundle info.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\Utility\Token $token
* The token service.
*/
public function __construct(EntityTypeInterface $entity_type, Connection $database, EntityFieldManagerInterface $entity_field_manager, CacheBackendInterface $cache, LanguageManagerInterface $language_manager, MemoryCacheInterface $memory_cache, EntityTypeBundleInfoInterface $entity_type_bundle_info, EntityTypeManagerInterface $entity_type_manager, Token $token) {
parent::__construct($entity_type, $database, $entity_field_manager, $cache, $language_manager, $memory_cache, $entity_type_bundle_info, $entity_type_manager);
$this->token = $token;
}
/**
* {@inheritdoc}
*/
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
return new static(
$entity_type,
$container->get('database'),
$container->get('entity_field.manager'),
$container->get('cache.entity'),
$container->get('language_manager'),
$container->get('entity.memory_cache'),
$container->get('entity_type.bundle.info'),
$container->get('entity_type.manager'),
$container->get('token'),
);
}
/**
* {@inheritdoc}
*/
protected function doPostSave(EntityInterface $entity, $update) {
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
if ($update && $this->entityType->isTranslatable()) {
$this->invokeTranslationHooks($entity);
}
// Get the log's current name.
$current_name = $entity->get('name')->value;
// We will automatically set the log name under two conditions:
// 1. Saving new/existing logs without a name.
// 2. Updating existing logs that were saved using the naming pattern.
$set_name = FALSE;
if (empty($current_name)) {
$set_name = TRUE;
}
elseif ($update && !empty($entity->original)) {
// Generate a log name using the original entity.
$original_generated_name = $this->generateLogName($entity->original);
// Compare the current log name to what would have been the original
// auto-generated name, to determine if the name was auto-generated
// previously. If it was, we will regenerate it.
if ($current_name == $original_generated_name) {
$set_name = TRUE;
}
}
// We must run the parent method before we set the name, so that new logs
// have an ID that can be used in token replacements.
// Also, we must run the parent method after the logic above, because the
// parent method unsets $entity->original.
parent::doPostSave($entity, $update);
// Set the log name, if necessary.
if ($set_name) {
// Generate a new name.
$new_name = $this->generateLogName($entity);
// If the name has been changed, update the entity.
if ($current_name != $new_name) {
$entity->set('name', $new_name);
$entity->save();
}
}
}
/**
* Helper method for generating a log name.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The log entity.
*
* @return string
* Returns the generated log name.
*/
protected function generateLogName(EntityInterface $entity) {
// Get the log type's naming pattern.
$name_pattern = $entity->getTypeNamePattern();
// Pass in an empty bubbleable metadata object, so we can avoid starting a
// renderer, for example if this happens in a REST resource creating
// context.
return $this->token->replace(
$name_pattern,
['log' => $entity],
[],
new BubbleableMetadata()
);
}
}

View File

@@ -0,0 +1,57 @@
<?php
namespace Drupal\log;
use Drupal\Core\Config\Entity\ConfigEntityListBuilder;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Url;
/**
* Provides a listing of Log type entities.
*/
class LogTypeListBuilder extends ConfigEntityListBuilder {
/**
* {@inheritdoc}
*/
public function buildHeader() {
$header['label'] = $this->t('Log type');
$header['id'] = $this->t('Machine name');
return $header + parent::buildHeader();
}
/**
* {@inheritdoc}
*/
public function buildRow(EntityInterface $entity) {
$row['label'] = $entity->label();
$row['id'] = $entity->id();
// You probably want a few more properties here...
return $row + parent::buildRow($entity);
}
/**
* {@inheritdoc}
*/
public function getDefaultOperations(EntityInterface $entity) {
$operations = parent::getDefaultOperations($entity);
// Place the edit operation after the operations added by field_ui.module
// which have the weights 15, 20, 25.
if (isset($operations['edit'])) {
$operations['edit']['weight'] = 30;
}
return $operations;
}
/**
* {@inheritdoc}
*/
public function render() {
$build = parent::render();
$build['table']['#empty'] = $this->t('No log types available. <a href=":link">Add log type</a>.', [
':link' => Url::fromRoute('entity.log_type.add_form')->toString(),
]);
return $build;
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Drupal\log;
use Drupal\entity\EntityViewsData;
/**
* Provides views data for the file entity type.
*/
class LogViewsData extends EntityViewsData {
/**
* {@inheritdoc}
*/
public function getViewsData() {
$data = parent::getViewsData();
$data['log_field_data']['timestamp']['sort']['id'] = 'log_standard';
$data['log_field_data']['timestamp']['field']['id'] = 'log_field';
return $data;
}
}

View File

@@ -0,0 +1,95 @@
<?php
namespace Drupal\log\Plugin\Action;
use Drupal\Component\Plugin\DependentPluginInterface;
use Drupal\Core\Action\ActionBase;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\TempStore\PrivateTempStore;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Base class for the configurable actions for logs.
*/
abstract class LogActionBase extends ActionBase implements DependentPluginInterface, ContainerFactoryPluginInterface {
/**
* The tempstore object.
*
* @var \Drupal\Core\TempStore\PrivateTempStore
*/
protected $tempStore;
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $user;
/**
* Constructs a LogActionBase object.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin ID for the plugin instance.
* @param array $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\TempStore\PrivateTempStore $temp_store
* The tempstore factory.
* @param \Drupal\Core\Session\AccountInterface $user
* The current user.
*/
public function __construct(array $configuration, $plugin_id, array $plugin_definition, PrivateTempStore $temp_store, AccountInterface $user) {
$this->tempStore = $temp_store;
$this->user = $user;
parent::__construct($configuration, $plugin_id, $plugin_definition);
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
$temp_store = $container->get('tempstore.private')->get($plugin_id);
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$temp_store,
$container->get('current_user')
);
}
/**
* {@inheritdoc}
*/
public function executeMultiple(array $entities) {
$this->tempStore->set($this->user->id(), $entities);
}
/**
* {@inheritdoc}
*/
public function execute($object = NULL) {
$this->executeMultiple([$object]);
}
/**
* {@inheritdoc}
*/
public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) {
/** @var \Drupal\log\Entity\LogInterface $object */
return $object->access('update', $account, $return_as_object);
}
/**
* {@inheritdoc}
*/
public function calculateDependencies() {
return [];
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace Drupal\log\Plugin\Action;
use Drupal\Core\Session\AccountInterface;
/**
* Action that clones a log entity.
*
* @Action(
* id = "log_clone_action",
* label = @Translation("Clones a log"),
* type = "log",
* confirm_form_route_name = "log.log_clone_action_form"
* )
*/
class LogClone extends LogActionBase {
/**
* {@inheritdoc}
*/
public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) {
/** @var \Drupal\log\Entity\LogInterface $object */
$result = $object->access('view', $account, TRUE)
->andIf($object->access('create', $account, TRUE));
return $return_as_object ? $result : $result->isAllowed();
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace Drupal\log\Plugin\Action;
/**
* Action that marks a log as done.
*
* @Action(
* id = "log_mark_as_done_action",
* label = @Translation("Sets a Log as done"),
* type = "log"
* )
*/
class LogMarkAsDone extends LogStateChangeBase {
/**
* {@inheritdoc}
*/
protected $targetState = 'done';
}

View File

@@ -0,0 +1,21 @@
<?php
namespace Drupal\log\Plugin\Action;
/**
* Action that marks a log as pending.
*
* @Action(
* id = "log_mark_as_pending_action",
* label = @Translation("Sets a Log as pending"),
* type = "log"
* )
*/
class LogMarkAsPending extends LogStateChangeBase {
/**
* {@inheritdoc}
*/
protected $targetState = 'pending';
}

View File

@@ -0,0 +1,30 @@
<?php
namespace Drupal\log\Plugin\Action;
use Drupal\Core\Session\AccountInterface;
/**
* Action that reschedules a log entity.
*
* @Action(
* id = "log_reschedule_action",
* label = @Translation("Reschedules a log"),
* type = "log",
* confirm_form_route_name = "log.log_schedule_action_form"
* )
*/
class LogReschedule extends LogActionBase {
/**
* {@inheritdoc}
*/
public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) {
/** @var \Drupal\log\Entity\LogInterface $object */
$result = $object->get('timestamp')->access('edit', $account, TRUE)
->andIf($object->access('update', $account, TRUE));
return $return_as_object ? $result : $result->isAllowed();
}
}

View File

@@ -0,0 +1,111 @@
<?php
namespace Drupal\log\Plugin\Action;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Action\Plugin\Action\EntityActionBase;
use Drupal\Core\Session\AccountInterface;
use Drupal\log\Entity\LogInterface;
/**
* Base class for actions that change the log status state.
*/
abstract class LogStateChangeBase extends EntityActionBase {
/**
* The target state to transition to.
*
* @var string
*/
protected $targetState;
/**
* {@inheritdoc}
*/
public function execute(LogInterface $log = NULL) {
// Bail if there is no log.
if (empty($log)) {
return;
}
// Apply the transition to target state if not already the current state.
/** @var \Drupal\state_machine\Plugin\Field\FieldType\StateItemInterface $state_item */
$state_item = $log->get('status')->first();
if ($state_item->getOriginalId() !== $this->targetState && $transition = $state_item->getWorkflow()->findTransition($state_item->getOriginalId(), $this->targetState)) {
$state_item->applyTransition($transition);
$log->setNewRevision(TRUE);
// Validate the entity before saving.
$violations = $log->validate();
if ($violations->count() > 0) {
$this->messenger()->addWarning(
$this->t('Could not change the status of <a href=":entity_link">%entity_label</a>: validation failed.',
[
':entity_link' => $log->toUrl()->setAbsolute()->toString(),
'%entity_label' => $log->label(),
],
),
);
return;
}
$log->save();
}
}
/**
* {@inheritdoc}
*/
public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) {
/** @var \Drupal\log\Entity\LogInterface $object */
// First check entity and state field access.
$result = $object->get('status')->access('edit', $account, TRUE)
->andIf($object->access('update', $account, TRUE));
// Save the state field.
/** @var \Drupal\state_machine\Plugin\Field\FieldType\StateItemInterface $state_item */
$state_item = $object->get('status')->first();
// If the state field is already in the target state, return early.
// The workflow will not allow a transition to the same state but the
// action itself does not need to fail.
if ($state_item->getOriginalId() === $this->targetState) {
return $return_as_object ? $result : $result->isAllowed();
}
// Check that the target state exists for the workflow.
$workflow = $state_item->getWorkflow();
$target_state = $workflow->getState($this->targetState);
// Deny access if the workflow does not support the target state.
if (empty($target_state)) {
$result = $result->orIf(AccessResult::forbidden(
$this->t(
'The %workflow workflow does not support the %target_state state.',
[
'%workflow' => $workflow->getLabel(),
'%target_state' => $this->targetState,
],
),
));
}
// Else check that a transition exists to the desired target state.
else {
$transition = $workflow->findTransition($state_item->getOriginalId(), $this->targetState);
$result = $result->orIf(AccessResult::forbiddenIf(
empty($transition) || !$state_item->isTransitionAllowed($transition->getId()),
$this->t(
'The state transition from %original_state to %target_state is not allowed.',
[
'%original' => $state_item->getOriginalLabel(),
'%target_state' => $target_state->getLabel(),
],
),
));
}
return $return_as_object ? $result : $result->isAllowed();
}
}

View File

@@ -0,0 +1,74 @@
<?php
namespace Drupal\log\Plugin\migrate\source\d7;
use Drupal\migrate\Row;
use Drupal\migrate_drupal\Plugin\migrate\source\d7\FieldableEntity;
/**
* Log source from database.
*
* @MigrateSource(
* id = "d7_log",
* source_module = "log"
* )
*/
class Log extends FieldableEntity {
/**
* {@inheritdoc}
*/
public function query() {
$query = $this->select('log', 'l')
->fields('l')
->distinct()
->orderBy('id');
if (isset($this->configuration['bundle'])) {
$query->condition('l.type', (array) $this->configuration['bundle'], 'IN');
}
return $query;
}
/**
* {@inheritdoc}
*/
public function fields() {
$fields = [
'id' => $this->t('The log ID'),
'name' => $this->t('The log name'),
'type' => $this->t('The log type'),
'uid' => $this->t('The log author ID'),
'timestamp' => $this->t('Timestamp of the event being logged'),
'created' => $this->t('Timestamp when the log was created'),
'changed' => $this->t('Timestamp when the log was last modified'),
'done' => $this->t('Boolean indicating whether the log is done (the event happened)'),
];
return $fields;
}
/**
* {@inheritdoc}
*/
public function prepareRow(Row $row) {
$id = $row->getSourceProperty('id');
$type = $row->getSourceProperty('type');
// Get Field API field values.
foreach ($this->getFields('log', $type) as $field_name => $field) {
$row->setSourceProperty($field_name, $this->getFieldValues('log', $field_name, $id));
}
return parent::prepareRow($row);
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['id']['type'] = 'integer';
return $ids;
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace Drupal\log\Plugin\views\field;
use Drupal\views\Plugin\views\field\EntityField;
/**
* Field handler to enable custom click-sort behavior for timestamp and id.
*
* @ViewsField("log_field")
*/
class LogField extends EntityField {
/**
* {@inheritdoc}
*/
public function clickSort($order) {
// No column selected, can't continue.
if (empty($this->options['click_sort_column'])) {
return;
}
$this->ensureMyTable();
$field_storage_definition = $this->getFieldStorageDefinition();
$column = $this->getTableMapping()->getFieldColumnName($field_storage_definition, $this->options['click_sort_column']);
if (!isset($this->aliases[$column])) {
// Column is not in query; add a sort on it (without adding the column).
$this->aliases[$column] = $this->tableAlias . '.' . $column;
}
$this->query->addOrderBy(NULL, NULL, $order, $this->aliases[$column]);
$this->query->addOrderBy($this->tableAlias, 'id', $order);
}
}

View File

@@ -0,0 +1,59 @@
<?php
namespace Drupal\log\Plugin\views\sort;
use Drupal\views\Plugin\views\sort\Date;
/**
* Sort handler for logs based on timestamp and id.
*
* @ViewsSort("log_standard")
*/
class LogStandardSort extends Date {
/**
* {@inheritdoc}
*/
public function query() {
$this->ensureMyTable();
switch ($this->options['granularity']) {
case 'second':
default:
$this->query->addOrderBy($this->tableAlias, $this->realField, $this->options['order']);
$this->query->addOrderBy($this->tableAlias, 'id', $this->options['order']);
return;
case 'minute':
$formula = $this->getDateFormat('YmdHi');
break;
case 'hour':
$formula = $this->getDateFormat('YmdH');
break;
case 'day':
$formula = $this->getDateFormat('Ymd');
break;
case 'month':
$formula = $this->getDateFormat('Ym');
break;
case 'year':
$formula = $this->getDateFormat('Y');
break;
}
$this->query->addOrderBy(NULL, $formula, $this->options['order'], $this->tableAlias . '_' . $this->field . '_' . $this->options['granularity']);
$this->query->addOrderBy($this->tableAlias, 'id', $this->options['order']);
}
/**
* {@inheritdoc}
*/
public function getDateField() {
return $this->query->getDateField("$this->tableAlias.timestamp");
}
}

View File

@@ -0,0 +1,22 @@
{#
/**
* @file log.html.twig
* Default theme implementation to present Log data.
*
* This template is used when viewing Log pages.
*
*
* Available variables:
* - content: A list of content items. Use 'content' to print all content, or
* - attributes: HTML attributes for the container element.
*
* @see template_preprocess_log()
*
* @ingroup themeable
*/
#}
<div{{ attributes.addClass('log') }}>
{% if content %}
{{- content -}}
{% endif %}
</div>

View File

@@ -0,0 +1,7 @@
id: default
label: default
description: 'Test log type'
name_pattern: '[log:id:value]'
langcode: en
workflow: log_default
new_revision: TRUE

View File

@@ -0,0 +1,7 @@
id: name_pattern
label: Name Pattern
description: 'Test log type (with name pattern)'
name_pattern: '[log:id:value] [log:status:value]'
langcode: en
workflow: log_default
new_revision: TRUE

View File

@@ -0,0 +1,14 @@
name: 'Log module tests'
type: module
description: 'Support module for log testing.'
package: Testing
core_version_requirement: ^9 || ^10 || ^11
dependencies:
- log:log
- token:token
# Information added by Drupal.org packaging script on 2024-07-18
version: '2.3.0'
project: 'log'
datestamp: 1721317388

View File

@@ -0,0 +1,61 @@
langcode: en
status: true
dependencies:
module:
- datetime
- log
- options
- user
id: log_test_view
label: 'Log test view'
module: views
description: ''
tag: ''
base_table: log_field_data
base_field: id
display:
default:
display_options:
defaults:
fields: false
pager: false
sorts: false
row:
type: fields
fields:
id:
id: id
table: log_field_data
field: id
relationship: none
entity_type: log
entity_field: id
plugin_id: field
name:
id: name
table: log_field_data
field: name
relationship: none
entity_type: log
entity_field: name
plugin_id: field
sorts:
timestamp:
id: timestamp
table: log_field_data
field: timestamp
relationship: none
group_type: group
admin_label: ''
order: DESC
exposed: false
expose:
label: ''
granularity: second
entity_type: log
entity_field: timestamp
plugin_id: log_standard
display_plugin: default
display_title: Master
id: default
position: 0

View File

@@ -0,0 +1,306 @@
<?php
namespace Drupal\Tests\log\Functional;
use Drupal\log\Entity\LogInterface;
/**
* Tests the Log form actions.
*
* @group Log
*/
class LogActionsTest extends LogTestBase {
/**
* Tests cloning a single log.
*/
public function testCloneSingleLog() {
// Create new user.
$original_user = $this->createUser([], 'Original user');
$this->assertNotEquals($this->loggedInUser->id(), $original_user->id());
// Create log.
$timestamp = \Drupal::time()->getRequestTime();
$log = $this->createLogEntity([
'uid' => $original_user->id(),
'name' => $this->randomMachineName(),
'created' => \Drupal::time()->getRequestTime(),
'done' => TRUE,
'timestamp' => $timestamp,
]);
$log->save();
$num_of_logs = $this->storage->getQuery()->count()->accessCheck(TRUE)->execute();
$this->assertEquals(1, $num_of_logs, 'There is one log in the system.');
$edit = [];
$edit['action'] = 'log_clone_action';
$edit['log_bulk_form[0]'] = TRUE;
$this->drupalGet('admin/content/log');
$this->submitForm($edit, $this->t('Apply to selected items'));
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->pageTextContains($this->t('Are you sure you want to clone this log?'));
$this->assertSession()->pageTextContains($this->t('New date'));
$new_timestamp = strtotime(date('Y-n-j', strtotime('+1 day', $timestamp)));
$edit_clone = [];
$edit_clone['date[date]'] = date('Y-m-d', $new_timestamp);
$this->submitForm($edit_clone, $this->t('Clone'));
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->addressEquals('admin/content/log');
$this->assertSession()->pageTextContains($this->t('Cloned 1 log'));
/** @var \Drupal\log\Entity\LogInterface[] $logs */
$logs = $this->storage->loadMultiple();
$this->assertEquals(2, count($logs), 'There are two logs in the system.');
$this->assertEquals($this->loggedInUser->id(), $logs[2]->getOwnerId(), 'Owner on the new log has been updated.');
$this->assertEquals($new_timestamp, $logs[2]->get('timestamp')->value, 'Timestamp on the new log has been updated.');
}
/**
* Tests cloning multiple logs.
*/
public function testCloneMultipleLogs() {
// Create new user.
$original_user = $this->createUser([], 'Original user');
$this->assertNotEquals($this->loggedInUser->id(), $original_user->id());
// Create logs.
$timestamp = \Drupal::time()->getRequestTime();
for ($i = 0; $i < 3; $i++) {
$log = $this->createLogEntity([
'uid' => $original_user->id(),
'name' => $this->randomMachineName(),
'created' => \Drupal::time()->getRequestTime(),
'done' => TRUE,
'timestamp' => $timestamp,
]);
$log->save();
}
$num_of_logs = $this->storage->getQuery()->count()->accessCheck(TRUE)->execute();
$this->assertEquals(3, $num_of_logs, 'There are three logs in the system.');
$edit = [];
$edit['action'] = 'log_clone_action';
for ($i = 0; $i < 3; $i++) {
$edit['log_bulk_form[' . $i . ']'] = TRUE;
}
$this->drupalGet('admin/content/log');
$this->submitForm($edit, $this->t('Apply to selected items'));
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->pageTextContains($this->t('Are you sure you want to clone these logs?'));
$this->assertSession()->pageTextContains($this->t('New date'));
$new_timestamp = strtotime(date('Y-n-j', strtotime('+1 day', $timestamp)));
$edit_clone = [];
$edit_clone['date[date]'] = date('Y-m-d', $new_timestamp);
$this->submitForm($edit_clone, $this->t('Clone'));
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->addressEquals('admin/content/log');
$this->assertSession()->pageTextContains($this->t('Cloned 3 logs'));
$logs = $this->storage->loadMultiple();
$this->assertEquals(6, count($logs), 'There are six logs in the system.');
for ($i = 1; $i <= 3; $i++) {
$this->assertEquals($this->loggedInUser->id(), $logs[3 + $i]->getOwnerId(), 'Owner on the new log has been updated');
$this->assertEquals($new_timestamp, $logs[3 + $i]->get('timestamp')->value, 'Timestamp on the new log has been updated.');
}
}
/**
* Tests rescheduling a single log to an absolute date.
*/
public function testRescheduleSingleLogAbsolute() {
$timestamp = \Drupal::time()->getRequestTime();
$log = $this->createLogEntity([
'name' => $this->randomMachineName(),
'created' => \Drupal::time()->getRequestTime(),
'done' => TRUE,
'timestamp' => $timestamp,
]);
$log->save();
$num_of_logs = $this->storage->getQuery()->count()->accessCheck(TRUE)->execute();
$this->assertEquals(1, $num_of_logs, 'There is one log in the system.');
$edit = [];
$edit['action'] = 'log_reschedule_action';
$edit['log_bulk_form[0]'] = TRUE;
$this->drupalGet('admin/content/log');
$this->submitForm($edit, $this->t('Apply to selected items'));
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->pageTextContains($this->t('Are you sure you want to reschedule this log?'));
$this->assertSession()->pageTextContains($this->t('New date'));
$new_timestamp = strtotime(date('Y-n-j', strtotime('+1 day', $timestamp)));
$edit_reschedule = [];
$edit_reschedule['date[date]'] = date('Y-m-d', $new_timestamp);
$this->submitForm($edit_reschedule, $this->t('Reschedule'));
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->addressEquals('admin/content/log');
$this->assertSession()->pageTextContains($this->t('Rescheduled 1 log'));
$logs = $this->storage->loadMultiple();
$this->assertEquals(1, $num_of_logs, 'There is one log in the system.');
$log = reset($logs);
$this->assertEquals($new_timestamp, $log->get('timestamp')->value, 'Timestamp on the log has changed.');
$this->assertEquals('pending', $log->get('status')->value, 'Log has been set to pending.');
}
/**
* Tests rescheduling multiple logs to an absolute date.
*/
public function testRescheduleMultipleLogsAbsolute() {
$timestamp = \Drupal::time()->getRequestTime();
for ($i = 0; $i < 3; $i++) {
$timestamp = strtotime(date('Y-n-j', strtotime('+1 day', $timestamp)));
$log = $this->createLogEntity([
'name' => $this->randomMachineName(),
'created' => \Drupal::time()->getRequestTime(),
'done' => TRUE,
'timestamp' => $timestamp,
]);
$log->save();
}
$num_of_logs = $this->storage->getQuery()->count()->accessCheck(TRUE)->execute();
$this->assertEquals(3, $num_of_logs, 'There are three logs in the system.');
$edit = [];
$edit['action'] = 'log_reschedule_action';
for ($i = 0; $i < 3; $i++) {
$edit['log_bulk_form[' . $i . ']'] = TRUE;
}
$this->drupalGet('admin/content/log');
$this->submitForm($edit, $this->t('Apply to selected items'));
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->pageTextContains($this->t('Are you sure you want to reschedule these logs?'));
$this->assertSession()->pageTextContains($this->t('New date'));
$new_timestamp = strtotime('+1 day', $timestamp);
$edit_reschedule = [];
$edit_reschedule['date[date]'] = date('Y-m-d', $new_timestamp);
$this->submitForm($edit_reschedule, $this->t('Reschedule'));
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->addressEquals('admin/content/log');
$this->assertSession()->pageTextContains($this->t('Rescheduled 3 logs'));
$logs = $this->storage->loadMultiple();
$this->assertEquals(3, count($logs), 'There are three logs in the system.');
foreach ($logs as $log) {
$this->assertEquals($new_timestamp, $log->get('timestamp')->value, 'Timestamp on the log has changed.');
$this->assertEquals('pending', $log->get('status')->value, 'Log has been set to pending.');
}
}
/**
* Tests rescheduling a single log to an relative date.
*/
public function testRescheduleSingleLogRelative() {
$timestamp = \Drupal::time()->getRequestTime();
$log = $this->createLogEntity([
'name' => $this->randomMachineName(),
'created' => \Drupal::time()->getRequestTime(),
'done' => TRUE,
'timestamp' => $timestamp,
]);
$log->save();
$num_of_logs = $this->storage->getQuery()->count()->accessCheck(TRUE)->execute();
$this->assertEquals(1, $num_of_logs, 'There is one log in the system.');
$edit = [];
$edit['action'] = 'log_reschedule_action';
$edit['log_bulk_form[0]'] = TRUE;
$this->drupalGet('admin/content/log');
$this->submitForm($edit, $this->t('Apply to selected items'));
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->pageTextContains($this->t('Are you sure you want to reschedule this log?'));
$this->assertSession()->pageTextContains($this->t('New date'));
$edit_reschedule = [];
$edit_reschedule['type_of_date'] = 1;
$this->submitForm($edit_reschedule, $this->t('Reschedule'));
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->addressEquals('admin/content/log/reschedule');
$this->assertSession()->pageTextContains($this->t('Please enter the amount of time for rescheduling.'));
$new_timestamp = strtotime('+1 day', $timestamp);
$edit_reschedule = [];
$edit_reschedule['type_of_date'] = 1;
$edit_reschedule['amount'] = 1;
$edit_reschedule['time'] = 'day';
$this->submitForm($edit_reschedule, $this->t('Reschedule'));
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->addressEquals('admin/content/log');
$this->assertSession()->pageTextContains($this->t('Rescheduled 1 log'));
$logs = $this->storage->loadMultiple();
$this->assertEquals(1, $num_of_logs, 'There is one log in the system.');
$log = reset($logs);
$this->assertEquals($new_timestamp, $log->get('timestamp')->value, 'Timestamp on the log has changed.');
$this->assertEquals('pending', $log->get('status')->value, 'Log has been set to pending.');
}
/**
* Tests rescheduling multiple logs to an relative date.
*/
public function testRescheduleMultipleLogsRelative() {
$timestamp = \Drupal::time()->getRequestTime();
$expected_timestamps = [];
for ($i = 0; $i < 3; $i++) {
$timestamp = strtotime('+1 day', $timestamp);
$log = $this->createLogEntity([
'name' => $this->randomMachineName(),
'created' => \Drupal::time()->getRequestTime(),
'done' => TRUE,
'timestamp' => $timestamp,
]);
$log->save();
// Save the expected timestamp for the log.
$new_timestamp = strtotime('-1 month', $timestamp);
$expected_timestamps[$log->id()] = $new_timestamp;
}
$num_of_logs = $this->storage->getQuery()->count()->accessCheck(TRUE)->execute();
$this->assertEquals(3, $num_of_logs, 'There are three logs in the system.');
$edit = [];
$edit['action'] = 'log_reschedule_action';
for ($i = 0; $i < 3; $i++) {
$edit['log_bulk_form[' . $i . ']'] = TRUE;
}
$this->drupalGet('admin/content/log');
$this->submitForm($edit, $this->t('Apply to selected items'));
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->pageTextContains($this->t('Are you sure you want to reschedule these logs?'));
$this->assertSession()->pageTextContains($this->t('New date'));
$edit_reschedule = [];
$edit_reschedule['type_of_date'] = 1;
$edit_reschedule['amount'] = -1;
$edit_reschedule['time'] = 'month';
$this->submitForm($edit_reschedule, $this->t('Reschedule'));
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->addressEquals('admin/content/log');
$this->assertSession()->pageTextContains($this->t('Rescheduled 3 logs'));
$logs = $this->storage->loadMultiple();
$this->assertEquals(3, count($logs), 'There are three logs in the system.');
$log_timestamps = array_map(function (LogInterface $log) {
return $log->get('timestamp')->value;
}, $logs);
$this->assertEquals($expected_timestamps, $log_timestamps, 'Logs have been rescheduled');
}
}

View File

@@ -0,0 +1,113 @@
<?php
namespace Drupal\Tests\log\Functional;
use Drupal\Core\StringTranslation\StringTranslationTrait;
/**
* Tests the Log CRUD.
*
* @group Log
*/
class LogCRUDTest extends LogTestBase {
use StringTranslationTrait;
/**
* Fields are displayed correctly.
*/
public function testFieldsVisibility() {
$this->drupalGet('log/add/default');
$this->assertSession()->statusCodeEquals('200');
$assert_session = $this->assertSession();
$assert_session->fieldExists('name[0][value]');
$assert_session->fieldExists('timestamp[0][value][date]');
$assert_session->fieldExists('timestamp[0][value][time]');
$assert_session->fieldExists('status');
$assert_session->fieldExists('revision_log_message[0][value]');
$assert_session->fieldExists('uid[0][target_id]');
$assert_session->fieldExists('created[0][value][date]');
$assert_session->fieldExists('created[0][value][time]');
}
/**
* Create Log entity.
*/
public function testCreateLog() {
$assert_session = $this->assertSession();
$name = $this->randomMachineName();
$edit = [
'name[0][value]' => $name,
];
$this->drupalGet('log/add/default');
$this->submitForm($edit, $this->t('Save'));
$result = $this->storage
->getQuery()
->range(0, 1)
->accessCheck(TRUE)
->execute();
$log_id = reset($result);
$log = $this->storage->load($log_id);
$this->assertEquals($log->get('name')->value, $name, 'Log has been saved.');
$assert_session->pageTextContains("Saved log: $name");
$assert_session->pageTextContains($name);
}
/**
* Display log entity.
*/
public function testViewLog() {
$edit = [
'name' => $this->randomMachineName(),
'created' => \Drupal::time()->getRequestTime(),
'done' => TRUE,
];
$log = $this->createLogEntity($edit);
$log->save();
$this->drupalGet($log->toUrl('canonical'));
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->pageTextContains($edit['name']);
$this->assertSession()->responseContains(\Drupal::service('date.formatter')->format(\Drupal::time()->getRequestTime()));
}
/**
* Edit log entity.
*/
public function testEditLog() {
$log = $this->createLogEntity();
$log->save();
$edit = [
'name[0][value]' => $this->randomMachineName(),
];
$this->drupalGet($log->toUrl('edit-form'));
$this->submitForm($edit, $this->t('Save'));
$this->assertSession()->pageTextContains($edit['name[0][value]']);
}
/**
* Delete log entity.
*/
public function testDeleteLog() {
$log = $this->createLogEntity();
$log->save();
$label = $log->getName();
$log_id = $log->id();
$this->drupalGet($log->toUrl('delete-form'));
$this->submitForm([], $this->t('Delete'));
$this->assertSession()->responseContains($this->t('The @entity-type %label has been deleted.', [
'@entity-type' => $log->getEntityType()->getSingularLabel(),
'%label' => $label,
]));
$this->assertNull($this->storage->load($log_id));
}
}

View File

@@ -0,0 +1,100 @@
<?php
namespace Drupal\Tests\log\Functional;
use Drupal\Core\StringTranslation\StringTranslationTrait;
/**
* Tests the Log name pattern.
*
* @group Log
*/
class LogNamePatternTest extends LogTestBase {
use StringTranslationTrait;
/**
* Tests creating a log entity without name.
*/
public function testCreateLogWithoutName() {
$edit = [
'status' => 'done',
];
$this->drupalGet('log/add/name_pattern');
$this->submitForm($edit, $this->t('Save'));
$result = $this->storage
->getQuery()
->range(0, 1)
->accessCheck(TRUE)
->execute();
$log_id = reset($result);
$log = $this->storage->load($log_id);
$this->assertEquals($log->label(), $log_id . ' done', 'Log name is the pattern and not the name.');
$this->drupalGet($log->toUrl('canonical'));
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->pageTextContains($log_id);
}
/**
* Tests creating a log entity with name.
*/
public function testCreateLogWithName() {
$name = $this->randomMachineName();
$edit = [
'name[0][value]' => $name,
];
$this->drupalGet('log/add/name_pattern');
$this->submitForm($edit, $this->t('Save'));
$result = $this->storage
->getQuery()
->range(0, 1)
->accessCheck(TRUE)
->execute();
$log_id = reset($result);
$log = $this->storage->load($log_id);
$this->assertEquals($log->get('name')->value, $name, 'Log name is the pattern and not the name.');
$this->drupalGet($log->toUrl('canonical'));
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->pageTextContains($name);
}
/**
* Edit log entity.
*/
public function testEditLog() {
$log = $this->createLogEntity(['type' => 'name_pattern']);
$log->save();
// Test that a manually set name does not get overwritten.
$edit = [
'name[0][value]' => $this->randomMachineName(),
];
$this->drupalGet($log->toUrl('edit-form'));
$this->submitForm($edit, $this->t('Save'));
$this->assertSession()->pageTextContains($edit['name[0][value]']);
// Test that clearing the name forces it to be auto-generated.
$edit = [
'name[0][value]' => '',
'status' => 'pending',
];
$this->drupalGet($log->toUrl('edit-form'));
$this->submitForm($edit, $this->t('Save'));
$this->assertSession()->pageTextContains($log->id() . ' pending');
// Test that updating a log with an auto-generated name automatically
// updates the name.
$edit = [
'status' => 'done',
];
$this->drupalGet($log->toUrl('edit-form'));
$this->submitForm($edit, $this->t('Save'));
$this->assertSession()->pageTextContains($log->id() . ' done');
}
}

View File

@@ -0,0 +1,99 @@
<?php
namespace Drupal\Tests\log\Functional;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Tests\BrowserTestBase;
/**
* Tests the Log CRUD.
*/
abstract class LogTestBase extends BrowserTestBase {
use StringTranslationTrait;
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* The log storage handler.
*
* @var \Drupal\group\Entity\Storage\GroupRoleStorageInterface
*/
protected $storage;
/**
* Modules to install.
*
* @var array
*/
protected static $modules = [
'entity',
'user',
'log',
'log_test',
'field',
'text',
];
/**
* A test user with administrative privileges.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
/** @var \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager */
$entity_type_manager = $this->container->get('entity_type.manager');
$this->storage = $entity_type_manager->getStorage('log');
$this->adminUser = $this->drupalCreateUser($this->getAdministratorPermissions());
$this->drupalLogin($this->adminUser);
drupal_flush_all_caches();
}
/**
* Gets the permissions for the admin user.
*
* @return string[]
* The permissions.
*/
protected function getAdministratorPermissions() {
return [
'access administration pages',
'administer log',
'view any log',
'create default log',
'view any default log',
'update own default log',
'update any default log',
'delete own default log',
'delete any default log',
];
}
/**
* Creates a log entity.
*
* @param array $values
* Array of values to feed the entity.
*
* @return \Drupal\log\Entity\LogInterface
* The log entity.
*/
protected function createLogEntity(array $values = []) {
$entity = $this->storage->create($values + [
'name' => $this->randomMachineName(),
'created' => \Drupal::time()->getRequestTime(),
'type' => 'default',
]);
return $entity;
}
}

View File

@@ -0,0 +1,100 @@
<?php
namespace Drupal\Tests\log\Kernel;
use Drupal\Core\Action\ActionInterface;
use Drupal\KernelTests\KernelTestBase;
use Drupal\Tests\log\Traits\LogCreationTrait;
/**
* Tests for log actions.
*
* @group Log
*/
class LogActionsTest extends KernelTestBase {
use LogCreationTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'system',
'user',
'log',
'log_test',
'datetime',
'state_machine',
];
/**
* The action manager.
*
* @var \Drupal\Core\Action\ActionManager
*/
protected $actionManager;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->actionManager = $this->container->get('plugin.manager.action');
$this->installEntitySchema('user');
$this->installEntitySchema('log');
$this->installSchema('system', ['sequences']);
$this->installConfig(['log', 'log_test']);
}
/**
* Tests that all the custom actions are available for the log entity type.
*/
public function testAvailableActions() {
$definitions = $this->actionManager->getDefinitionsByType('log');
$expected_actions = [
'log_mark_as_done_action',
'log_mark_as_pending_action',
'log_clone_action',
'log_reschedule_action',
];
foreach ($expected_actions as $expected_action) {
$this->assertTrue(in_array($expected_action, array_keys($definitions)));
}
}
/**
* Tests that the mark as done action sets the right state.
*/
public function testMarkAsDoneAction() {
$action = $this->actionManager->createInstance('log_mark_as_done_action');
$this->assertTrue($action instanceof ActionInterface, 'The action implements the correct interface.');
$new_log = $this->createLogEntity([
'name' => $this->randomMachineName(),
'status' => 'pending',
]);
$new_log->save();
$action->execute($new_log);
$storage = $this->container->get('entity_type.manager')->getStorage('log');
$log = $storage->load($new_log->id());
$this->assertEquals('done', $log->get('status')->value);
}
/**
* Tests that the mark as pending action sets the right state.
*/
public function testMarkAsPendingAction() {
$action = $this->actionManager->createInstance('log_mark_as_pending_action');
$this->assertTrue($action instanceof ActionInterface, 'The action implements the correct interface.');
$new_log = $this->createLogEntity([
'name' => $this->randomMachineName(),
'status' => 'done',
]);
$new_log->save();
$action->execute($new_log);
$storage = $this->container->get('entity_type.manager')->getStorage('log');
$log = $storage->load($new_log->id());
$this->assertEquals('pending', $log->get('status')->value);
}
}

View File

@@ -0,0 +1,203 @@
<?php
namespace Drupal\Tests\log\Kernel;
use Drupal\Component\Serialization\Json;
use Drupal\KernelTests\Core\Entity\EntityKernelTestBase;
use Drupal\Tests\log\Traits\LogCreationTrait;
use Symfony\Component\HttpFoundation\Request;
/**
* Tests name autocomplete for logs.
*
* @group Log
*/
class NameAutocompleteTest extends EntityKernelTestBase {
use LogCreationTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'log',
'log_test',
'datetime',
'entity',
'state_machine',
];
/**
* An admin account.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $adminAccount;
/**
* An account with 'view any default log' permission.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $anyAccount;
/**
* An account with 'view own default log' permission.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $ownAccount;
/**
* An account with no view permissions.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $noneAccount;
/**
* A collection of logs.
*
* @var \Drupal\log\Entity\LogInterface[]
*/
protected $logs = [];
/**
* The request stack used for testing.
*
* @var \Symfony\Component\HttpFoundation\RequestStack
*/
protected $requestStack;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installEntitySchema('log');
$this->installConfig(['log', 'log_test']);
// Create the test user accounts.
$this->adminAccount = $this->createUser([], ['administer log']);
$this->anyAccount = $this->createUser([], [
'view any default log',
'create default log',
]);
$this->ownAccount = $this->createUser([], [
'view own default log',
'create default log',
]);
$this->noneAccount = $this->createUser([], ['create default log']);
// Create the different log entries.
$this->logs[] = $this->createLogEntity([
'name' => 'First log',
'uid' => $this->adminAccount->id(),
]);
$this->logs[] = $this->createLogEntity([
'name' => 'Second log',
'uid' => $this->adminAccount->id(),
]);
$this->logs[] = $this->createLogEntity([
'name' => 'Third log',
'uid' => $this->ownAccount->id(),
]);
}
/**
* Returns the result of an autocomplete request.
*
* @param string $input
* The label of the entity to query by.
*
* @return mixed
* The JSON value encoded in its appropriate PHP type.
*
* @throws \Exception
*/
protected function getAutocompleteResult($input) {
// Rebuild the route cache on each request to avoid parameter bag cache
// leaks.
$this->container->get('router.builder')->rebuild();
// Build the autocomplete request, 'q' is the right parameter to mock this.
$request = Request::create('/log/default/autocomplete');
$request->query->set('q', $input);
/** @var \Symfony\Component\HttpKernel\HttpKernelInterface $http_kernel */
$http_kernel = $this->container->get('http_kernel');
$response = $http_kernel->handle($request);
// The response is a JsonResponse and the content is a string that needs to
// be decoded to array.
$result = $response->getContent();
return Json::decode($result);
}
/**
* Tests name autocomplete route.
*/
public function testLogNameAutocomplete() {
// Tests admin account with an autocomplete query that shouldn't return any
// logs.
$this->container->get('current_user')->setAccount($this->adminAccount);
$result = $this->getAutocompleteResult('nonsense');
$this->assertEmpty($result, 'No results for non matching search query.');
// Tests admin account so it returns the complete set of logs.
$result = $this->getAutocompleteResult('log');
$this->assertEquals(count($this->logs), count($result), 'Number of results for matching query and admin user is as expected.');
// With an account that has 'view any default log' permission, the result
// should be the complete set of logs.
$this->container->get('current_user')->setAccount($this->anyAccount);
$result = $this->getAutocompleteResult('log');
$this->assertEquals(3, count($result), 'Number of results for matching query and user with view any permission is as expected.');
// With an account that has 'view own default log' permission, the result
// should be the logs belonging to that user.
$this->container->get('current_user')->setAccount($this->ownAccount);
$result = $this->getAutocompleteResult('log');
$this->assertEquals(1, count($result), 'Number of results for matching query and user with view own permission is as expected.');
$own_log = array_filter($this->logs, function ($log) {
/** @var \Drupal\log\Entity\LogInterface $log */
return $log->id() == $this->ownAccount->id();
});
$own_log = reset($own_log);
$this->assertEquals($result[0], $own_log->label(), 'The right log for the user is returned.');
// With an account with no permissions and the right query, there should be
// no results anyway.
$this->container->get('current_user')->setAccount($this->noneAccount);
$result = $this->getAutocompleteResult('log');
$this->assertEmpty($result, 'No results for user without permissions.');
}
/**
* Tests the order of logs returned.
*/
public function testLogNameAutocompleteMultipleLogs() {
// Add a duplicate log that should be on top of the results.
$this->logs[] = $this->createLogEntity([
'name' => 'Z log',
'uid' => $this->adminAccount->id(),
]);
$this->logs[] = $this->createLogEntity([
'name' => 'Z log',
'uid' => $this->adminAccount->id(),
]);
$this->container->get('current_user')->setAccount($this->adminAccount);
$result = $this->getAutocompleteResult('log');
$this->assertEquals(count($this->logs) - 1, count($result), 'Duplicated log is not duplicated in the autocomplete results.');
$expected_order = [
'Z log',
'First log',
'Second log',
'Third log',
];
$this->assertEquals($expected_order, $result, 'Order of results is as expected.');
}
}

View File

@@ -0,0 +1,146 @@
<?php
namespace Drupal\Tests\log\Kernel;
use Drupal\Tests\log\Traits\LogCreationTrait;
use Drupal\Tests\views\Kernel\ViewsKernelTestBase;
use Drupal\views\Tests\ViewTestData;
use Drupal\views\Views;
/**
* Tests for Drupal\log\Plugin\views\sort\LogTimestampIdSort handler.
*
* @group Log
*/
class SortTimestampIdTest extends ViewsKernelTestBase {
use LogCreationTrait;
/**
* {@inheritdoc}
*/
protected static $modules = ['log', 'log_test', 'datetime', 'state_machine'];
/**
* Views used by this test.
*
* @var array
*/
public static $testViews = ['log_test_view'];
/**
* ASC expected result.
*
* @var array
*/
protected $expectedResultASC = [];
/**
* DESC expected result.
*
* @var array
*/
protected $expectedResultDESC = [];
/**
* {@inheritdoc}
*/
protected function setUp($import_test_views = TRUE): void {
parent::setUp();
$this->installEntitySchema('log');
$this->installConfig(['log', 'log_test']);
ViewTestData::createTestViews(get_class($this), ['log_test']);
// Establish two different timestamps so the sort is meaningful.
$first_timestamp = 376185600;
$second_timestamp = 386121600;
// Three entities is the minimum amount to test two with the same timestamp
// and different ID and one with unique timestamp.
$first_entity = $this->createLogEntity([
'name' => 'First',
'timestamp' => $first_timestamp,
]);
$second_entity = $this->createLogEntity([
'name' => 'Second',
'timestamp' => $first_timestamp,
]);
$third_entity = $this->createLogEntity([
'name' => 'Third',
'timestamp' => $second_timestamp,
]);
// Fill the expected results for the combinations.
$this->expectedResultASC = [
['name' => $first_entity->get('name')->value, 'id' => $first_entity->id()],
['name' => $second_entity->get('name')->value, 'id' => $second_entity->id()],
['name' => $third_entity->get('name')->value, 'id' => $third_entity->id()],
];
$this->expectedResultDESC = [
['name' => $third_entity->get('name')->value, 'id' => $third_entity->id()],
['name' => $second_entity->get('name')->value, 'id' => $second_entity->id()],
['name' => $first_entity->get('name')->value, 'id' => $first_entity->id()],
];
}
/**
* Tests the sorting: Timestamp /ID ASC.
*/
public function testLogTimestampIdAscSort() {
$view = Views::getView('log_test_view');
$view->setDisplay();
$view->displayHandlers->get('default')->overrideOption('sorts', [
'timestamp' => [
'id' => 'timestamp',
'table' => 'log_field_data',
'field' => 'timestamp',
'relationship' => 'none',
'order' => 'ASC',
'plugin_id' => 'log_standard',
],
]);
$this->executeView($view);
$this->assertEquals(3, count($view->result), 'The number of returned rows match.');
$this->assertIdenticalResultset($view, $this->expectedResultASC, [
'name' => 'name',
'id' => 'id',
], 'ASC sort displays as expected');
$view->destroy();
unset($view);
}
/**
* Tests the sorting: Timestamp/ID DESC.
*/
public function testLogTimestampIdDescSort() {
$view = Views::getView('log_test_view');
$view->setDisplay();
$view->displayHandlers->get('default')->overrideOption('sorts', [
'timestamp' => [
'id' => 'timestamp',
'table' => 'log_field_data',
'field' => 'timestamp',
'relationship' => 'none',
'order' => 'DESC',
'plugin_id' => 'log_standard',
],
]);
$this->executeView($view);
$this->assertEquals(3, count($view->result), 'The number of returned rows match.');
$this->assertIdenticalResultset($view, $this->expectedResultDESC, [
'name' => 'name',
'id' => 'id',
], 'DESC sort displays as expected');
$view->destroy();
unset($view);
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace Drupal\Tests\log\Traits;
use Drupal\log\Entity\Log;
/**
* Provides methods to create log entities.
*
* This trait is meant to be used only by test classes.
*/
trait LogCreationTrait {
/**
* Creates a log entity.
*
* @param array $values
* Array of values to feed the entity.
*
* @return \Drupal\log\Entity\LogInterface
* The log entity.
*
* @throws \Drupal\Core\Entity\EntityStorageException
*/
protected function createLogEntity(array $values = []) {
/** @var \Drupal\log\Entity\LogInterface $entity */
$entity = Log::create($values + [
'name' => $this->randomMachineName(),
'type' => 'default',
]);
$entity->save();
return $entity;
}
}