<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[13268] sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events: Translate: Translation events sync from GitHub</title>
</head>
<body>
<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; }
#msg dl a { font-weight: bold}
#msg dl a:link { color:#fc3; }
#msg dl a:active { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { white-space: pre-line; overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta" style="font-size: 105%">
<dt style="float: left; width: 6em; font-weight: bold">Revision</dt> <dd><a style="font-weight: bold" href="http://meta.trac.wordpress.org/changeset/13268">13268</a><script type="application/ld+json">{"@context":"http://schema.org","@type":"EmailMessage","description":"Review this Commit","action":{"@type":"ViewAction","url":"http://meta.trac.wordpress.org/changeset/13268","name":"Review Commit"}}</script></dd>
<dt style="float: left; width: 6em; font-weight: bold">Author</dt> <dd>amieiro</dd>
<dt style="float: left; width: 6em; font-weight: bold">Date</dt> <dd>2024-03-04 11:41:16 +0000 (Mon, 04 Mar 2024)</dd>
</dl>
<pre style='padding-left: 1em; margin: 2em 0; border-left: 2px solid #ccc; line-height: 1.25; font-size: 105%; font-family: sans-serif'>Translate: Translation events sync from GitHub</pre>
<h3>Added Paths</h3>
<ul>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginswporggptranslationeventsLICENSE">sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/LICENSE</a></li>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginswporggptranslationeventsREADMEmd">sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/README.md</a></li>
<li>sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/assets/</li>
<li>sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/assets/css/</li>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginswporggptranslationeventsassetscsstranslationeventscss">sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/assets/css/translation-events.css</a></li>
<li>sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/assets/js/</li>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginswporggptranslationeventsassetsjstranslationeventsjs">sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/assets/js/translation-events.js</a></li>
<li>sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/includes/</li>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginswporggptranslationeventsincludesactiveeventscachephp">sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/includes/active-events-cache.php</a></li>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginswporggptranslationeventsincludeseventphp">sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/includes/event.php</a></li>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginswporggptranslationeventsincludesroutephp">sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/includes/route.php</a></li>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginswporggptranslationeventsincludesstatscalculatorphp">sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/includes/stats-calculator.php</a></li>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginswporggptranslationeventsincludesstatslistenerphp">sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/includes/stats-listener.php</a></li>
<li>sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/templates/</li>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginswporggptranslationeventstemplateseventphp">sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/templates/event.php</a></li>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginswporggptranslationeventstemplateseventsformphp">sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/templates/events-form.php</a></li>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginswporggptranslationeventstemplateseventsheaderphp">sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/templates/events-header.php</a></li>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginswporggptranslationeventstemplateseventslistphp">sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/templates/events-list.php</a></li>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginswporggptranslationeventstemplateseventsmyeventsphp">sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/templates/events-my-events.php</a></li>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginswporggptranslationeventstemplateshelperfunctionsphp">sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/templates/helper-functions.php</a></li>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginswporggptranslationeventswporggptranslationeventsphp">sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/wporg-gp-translation-events.php</a></li>
</ul>
<h3>Property Changed</h3>
<ul>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginswporggptranslationevents">sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/</a></li>
</ul>
</div>
<div id="patch">
<h3>Diff</h3>
<span class="cx" style="display: block; padding: 0 10px">Index: sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events
</span><span class="cx" style="display: block; padding: 0 10px">===================================================================
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">--- sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events 2024-03-04 04:54:42 UTC (rev 13267)
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events 2024-03-04 11:41:16 UTC (rev 13268)
</ins><a id="sitestrunkwordpressorgpublic_htmlwpcontentpluginswporggptranslationevents"></a>
<div class="propset"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Property changes: sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events</h4>
<pre class="diff"><span>
</span></pre></div>
<a id="svnignore"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:ignore</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+.github
+.wp-env.json
+bin
+composer.json
+composer.lock
+phpcs.xml
+phpunit.xml.dist
+tests
</ins><a id="sitestrunkwordpressorgpublic_htmlwpcontentpluginswporggptranslationeventsLICENSE"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/LICENSE</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/LICENSE (rev 0)
+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/LICENSE 2024-03-04 11:41:16 UTC (rev 13268)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,339 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ 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.
</ins></span></pre></div>
<a id="sitestrunkwordpressorgpublic_htmlwpcontentpluginswporggptranslationeventsREADMEmd"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/README.md</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/README.md (rev 0)
+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/README.md 2024-03-04 11:41:16 UTC (rev 13268)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,42 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+# wporg-gp-translation-events
+
+## Development environment
+First follow [instructions to install `wp-env`](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-env/#prerequisites).
+
+Then install dependencies:
+
+```shell
+composer install
+```
+
+Then you can run a local WordPress instance with the plugin installed:
+
+```shell
+composer dev:start
+```
+
+Once the environment is running, you must create the database tables needed by this plugin:
+
+```shell
+composer dev:db:schema
+```
+
+WordPress is now running at http://localhost:8888, user: `admin`, password: `password`.
+
+### Local environment
+
+If you are not using `wp-env`, you need to add the tables to the database of your local environment. To do this, you can run this command from the plugin folder:
+
+```shell
+wp db query < schema.sql
+```
+
+### Tests
+
+You can run tests in `wp-env` with the following command:
+
+> Note that `wp-env` must be running.
+
+```shell
+composer dev:test
+```
</ins></span></pre></div>
<a id="sitestrunkwordpressorgpublic_htmlwpcontentpluginswporggptranslationeventsassetscsstranslationeventscss"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/assets/css/translation-events.css</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/assets/css/translation-events.css (rev 0)
+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/assets/css/translation-events.css 2024-03-04 11:41:16 UTC (rev 13268)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,235 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+.translation-event-form label {
+ display: inline-block;
+ width: 140px;
+ vertical-align: top;
+}
+.translation-event-form #event-title,
+.translation-event-form #event-description {
+ width: 30%;
+}
+.translation-event-form input[type="text"],
+.translation-event-form input[type="date"] {
+ width: 13%;
+}
+.translation-event-form div {
+ margin-top: 1em;
+}
+.translation-event-form #submit-event {
+ margin-left: 10%;
+ width: 30%;
+ margin-top: 1em;
+ display: block;
+}
+.event-page-title {
+ border-bottom: #d9d8d8 thin solid;
+ padding-bottom: .5em;
+}
+.event-details-page {
+ border-bottom: #cfd4d4 thin solid;
+ width: 60%;
+ margin: 0 auto;
+}
+.event-details-left {
+ width: 77%;
+ float: left;
+ border-right: #dadfdf thin solid;
+ margin-right: 1%;
+ padding-right: 1em;
+}
+.event-details-right {
+ width: 22%;
+ float: right;
+ padding-top: 1em;
+}
+.event-details-stats {
+ clear: both;
+ margin: 1em;
+}
+.event-details-stats table {
+ width: 100%;
+ table-layout: fixed;
+}
+.event-details-stats table th, .event-details-stats table td{
+ padding: 1em;
+ text-align: center;
+}
+.event-details-stats table tr {
+ border-bottom: thin solid #f0f0f0;
+}
+.event-details-stats table th {
+ background-color: #e9e9e9;
+}
+.event-details-stats-totals {
+ font-weight: bold;
+}
+.hide-event-url {
+ display: none;
+}
+.translation-event-form button.save-draft {
+ background: #fff;
+ color: var(--gp-color-btn-primary-bg);
+}
+#event-url span {
+ width: 10%;
+ display: inline-block;
+}
+span.event-list-date {
+ display: block;
+ color: #939393;
+ font-size: .8em;
+ font-weight: normal;
+}
+span.event-list-date.events-i-am-attending {
+ font-size: .8em;
+ font-weight: normal;
+}
+.event-list-item a{
+ font-weight: bold;
+ font-size: 1.2em;
+}
+li.event-list-item {
+ list-style-type: none;
+}
+li.event-list-item p {
+ margin-top: 0;
+ color: #5a5a5a;
+ font-size: .95em;
+}
+h2.event_page_title {
+ border-bottom: #e0e0e0 thin solid;
+ padding-bottom: 0.5em;
+ font-weight: bold;
+}
+.event-list-top-bar {
+ display: block;
+ width: 80%;
+ margin: 0 auto;
+ text-align: right;
+}
+.event-list-nav li {
+ display: inline;
+ margin: 0 0.1rem;
+}
+.event-list-nav li a {
+ text-decoration: none;
+ font-weight: bold;
+}
+.event-list-nav li:not(:first-child):not(:last-child):before {
+ content: " | ";
+}
+.event-left-col {
+ width: 75%;
+ float: left;
+ border-right: var(--gp-color-secondary-400) thin solid;
+}
+.event-right-col {
+ width: 25%;
+ float: left;
+ padding-left: 1em;
+}
+.event-attending-list {
+ list-style-type: none;
+ padding: 0;
+}
+.event-attending-list li {
+ margin-bottom: 1em;
+}
+.event-list-nav li a.button {
+ background-color: var(--gp-color-btn-primary-bg) !important;
+ color: #fff !important;
+}
+a.event-link-draft {
+ color:#80807f;
+}
+span.event-label-draft, span.event-details-join-expired, span.active-events-before-translation-table {
+ font-weight: 500;
+ color: var( --gp-color-bubble-inactive-project-text );
+ border: 1px solid var( --gp-color-bubble-inactive-project-text );
+ font-size: .7em;
+ margin-right: 0.3em;
+ width: 6em;
+ text-align: center;
+ padding: 0.2em 0.5em;
+ border-radius: 1em;
+ text-transform: capitalize;
+}
+span.active-events-before-translation-table a {
+ color: var( --gp-color-bubble-inactive-project-text );
+ text-decoration: none;
+}
+.active-events-before-translation-table {
+ width: 100%;
+ border: 1px solid var( --gp-color-border-default );
+ background: var( --gp-color-status-waiting-subtle );
+ margin: 1rem 0;
+ padding: 0.3rem 0.8rem;
+}
+.event-page-wrapper {
+ margin: 0 auto;
+ width: 80%;
+}
+.event-list {
+ margin: 0;
+ padding: 0;
+}
+.event-details-head {
+ border-bottom: var(--gp-color-secondary-400) thin solid;
+}
+input[type="submit"].attend-btn {
+ width: 100%;
+ text-align: center;
+ display: inline;
+ margin-top: 1em;
+ font-weight: bold;
+}
+input[type="submit"].attending-btn, a.button.is-primary.attend-btn {
+ width: 100%;
+ text-align: center;
+ display: block;
+ margin-top: 1em;
+}
+.event-details-date {
+ background: #f4f4f4;
+ color: #606161;
+ padding: .5em;
+ font-size: .9em;
+ line-height: 1.8em;
+ font-weight: 500;
+}
+.event-page-edit-link {
+ float: right;
+ text-decoration: none;
+}
+.event-stats-summary {
+ margin-top: 1em;
+}
+.event-stats-summary summary {
+ background: #f8f8f8;
+ padding: 0.4em;
+ cursor: pointer;
+ color: var(--gp-color-primary-400);
+ font-weight: 500;
+ font-size: .9em;
+}
+.event-stats-summary p.event-stats-text {
+ margin: 0;
+ background: #f8f8f8;
+ padding: 0.8em;
+ font-size: .9em;
+ border-top: thin solid #e0e0e0;
+ font-family: monospace;
+}
+time.event-utc-time {
+ display: block;
+ font-size: .95em;
+}
+.event-utc-time:first-of-type{
+ border-bottom: #cdcdcd thin solid;
+ padding-bottom: 0.5em;
+ margin-bottom: 0.5em;
+}
+span.event-details-date-label {
+ font-weight: bold;
+ color: #5a5a5a;
+ display: block;
+}
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/assets/css/translation-events.css
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="sitestrunkwordpressorgpublic_htmlwpcontentpluginswporggptranslationeventsassetsjstranslationeventsjs"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/assets/js/translation-events.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/assets/js/translation-events.js (rev 0)
+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/assets/js/translation-events.js 2024-03-04 11:41:16 UTC (rev 13268)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,170 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+(
+ function ( $, $gp ) {
+ jQuery( document ).ready(
+ function ( $ ) {
+ $gp.notices.init();
+ const timezoneElement = $( '#event-timezone' );
+ if ( timezoneElement.length && ! timezoneElement.val() ) {
+ selectUserTimezone();
+ }
+ validateEventDates();
+ convertToUserLocalTime();
+
+ $( '.submit-event' ).on(
+ 'click',
+ function ( e ) {
+ e.preventDefault();
+ let eventStatus = $( this ).data( 'event-status' );
+ let isDraft = $( 'button.save-draft[data-event-status="draft"]:visible' ).length > 0;
+ handleSubmit( eventStatus, isDraft );
+ }
+ );
+
+ $( '.delete-event' ).on(
+ 'click',
+ function ( e ) {
+ e.preventDefault();
+ handleDelete()
+ }
+ );
+ }
+ );
+
+ /**
+ * Handles the form submission
+ *
+ * @param eventStatus The new status of the event
+ * @param isDraft Whether the current event status is a draft or not
+ */
+ function handleSubmit( eventStatus, isDraft ) {
+ if ( '' === $( '#event-start' ).val() ) {
+ $gp.notices.error( 'Event start date and time must be set.' );
+ return;
+ }
+ if ( '' === $( '#event-end' ).val() ) {
+ $gp.notices.error( 'Event end date and time must be set.' );
+ return;
+ }
+ if ( $( '#event-end' ).val() <= $( '#event-start' ).val() ) {
+ $gp.notices.error( 'Event end date and time must be later than event start date and time.' );
+ return;
+ }
+ if ( eventStatus === 'publish' && isDraft ) {
+ const submitPrompt = 'Are you sure you want to publish this event?';
+ if ( ! confirm( submitPrompt ) ) {
+ return;
+ }
+ }
+ $( '#event-form-action' ).val( eventStatus );
+ const $form = $( '.translation-event-form' );
+ const $is_creation = $( '#form-name' ).val() === 'create_event';
+
+ $.ajax(
+ {
+ type: 'POST',
+ url: $translation_event.url,
+ data:$form.serialize(),
+ success: function ( response ) {
+ if ( response.data.eventId ) {
+ history.replaceState( '', '', response.data.eventEditUrl );
+ $( '#form-name' ).val( 'edit_event' );
+ $( '.event-page-title' ).text( 'Edit Event' );
+ $( '#event-id' ).val( response.data.eventId );
+ if ( eventStatus === 'publish' ) {
+ $( 'button[data-event-status="draft"]' ).hide();
+ $( '#published-update-text' ).show();
+ $( 'button[data-event-status="publish"]' ).text( 'Update Event' );
+ }
+ if ( eventStatus === 'draft' ) {
+ $( 'button[data-event-status="draft"]' ).text( 'Update Draft' );
+ }
+ $( '#event-url' ).removeClass( 'hide-event-url' ).find( 'a' ).attr( 'href', response.data.eventUrl ).text( response.data.eventUrl );
+ if ( $is_creation ) {
+ $( '#delete-button' ).toggle();
+ }
+ $gp.notices.success( response.data.message );
+ }
+ },
+ error: function ( xhr, msg ) {
+ /* translators: %s: Error message. */
+ msg = xhr.responseJSON.data ? wp.i18n.sprintf( wp.i18n.__( 'Error: %s', 'gp-translation-events' ), xhr.responseJSON.data ) : wp.i18n.__( 'Error saving the event!', 'gp-translation-events' );
+ $gp.notices.error( msg );
+ },
+ }
+ );
+ }
+
+ function handleDelete() {
+ if ( ! confirm( 'Are you sure you want to delete this event?' ) ) {
+ return;
+ }
+ const $form = $( '.translation-event-form' );
+ $( '#form-name' ).val( 'delete_event' );
+ $( '#event-form-action' ).val( 'delete' );
+ $.ajax(
+ {
+ type: 'POST',
+ url: $translation_event.url,
+ data:$form.serialize(),
+ success: function ( response ) {
+ window.location = response.data.eventDeleteUrl;
+ },
+ error: function ( error ) {
+ $gp.notices.error( response.data.message );
+ },
+ }
+ );
+ }
+
+ function validateEventDates() {
+ const startDateTimeInput = $( '#event-start' );
+ const endDateTimeInput = $( '#event-end' );
+ if ( ! startDateTimeInput.length || ! endDateTimeInput.length ) {
+ return;
+ }
+
+ startDateTimeInput.add( endDateTimeInput ).on(
+ 'input',
+ function () {
+ endDateTimeInput.prop( 'min', startDateTimeInput.val() );
+ if (endDateTimeInput.val() < startDateTimeInput.val()) {
+ endDateTimeInput.val( startDateTimeInput.val() );
+ }
+ }
+ );
+ }
+ function selectUserTimezone() {
+ const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
+ // phpcs:disable WordPress.WhiteSpace.OperatorSpacing.NoSpaceBefore
+ // phpcs:disable WordPress.WhiteSpace.OperatorSpacing.NoSpaceAfter
+ document.querySelector( `#event-timezone option[value="${timezone}"]` ).selected = true
+ // phpcs:enable
+ }
+
+ function convertToUserLocalTime() {
+ const timeElements = document.querySelectorAll( 'time.event-utc-time' );
+ if ( timeElements.length === 0 ) {
+ return;
+ }
+ timeElements.forEach(
+ function ( timeEl ) {
+ const eventDateObj = new Date( timeEl.getAttribute( 'datetime' ) );
+ const userTimezoneOffset = new Date().getTimezoneOffset();
+ const userTimezoneOffsetMs = userTimezoneOffset * 60 * 1000;
+ const userLocalDateTime = new Date( eventDateObj.getTime() - userTimezoneOffsetMs );
+
+ const options = {
+ weekday: 'short',
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric',
+ hour: 'numeric',
+ minute: 'numeric',
+ timeZoneName: 'short'
+ };
+ timeEl.textContent = userLocalDateTime.toLocaleTimeString( navigator.language, options );
+ }
+ );
+ }
+ }( jQuery, $gp )
+);
</ins></span></pre></div>
<a id="sitestrunkwordpressorgpublic_htmlwpcontentpluginswporggptranslationeventsincludesactiveeventscachephp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/includes/active-events-cache.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/includes/active-events-cache.php (rev 0)
+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/includes/active-events-cache.php 2024-03-04 11:41:16 UTC (rev 13268)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,53 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+
+namespace Wporg\TranslationEvents;
+
+use Exception;
+
+class Active_Events_Cache {
+ public const CACHE_DURATION = 60 * 60 * 24; // 24 hours.
+ private const KEY = 'translation-events-active-events';
+
+ /**
+ * Cache active events.
+ *
+ * @param Event[] $events Events to cache.
+ *
+ * @throws Exception When it fails to cache events.
+ */
+ public function cache( array $events ): void {
+ if ( ! wp_cache_set( self::KEY, $events, '', self::CACHE_DURATION ) ) {
+ throw new Exception( 'Failed to cache active events' );
+ }
+ }
+
+ /**
+ * Returns the cached events, or null if nothing is cached.
+ *
+ * @return Event[]|null
+ * @throws Exception When it fails to retrieve cached events.
+ */
+ public function get(): ?array {
+ $result = wp_cache_get( self::KEY, '', false, $found );
+ if ( ! $found ) {
+ return null;
+ }
+
+ if ( ! is_array( $result ) ) {
+ throw new Exception( 'Cached events is not an array, something is wrong' );
+ }
+
+ return $result;
+ }
+
+ /**
+ * Invalidates the active events cache.
+ *
+ * @throws Exception When it fails to invalidate the cache.
+ */
+ public static function invalidate(): void {
+ if ( ! wp_cache_delete( self::KEY ) ) {
+ throw new Exception( 'Failed to invalidate cached events' );
+ }
+ }
+}
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/includes/active-events-cache.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="sitestrunkwordpressorgpublic_htmlwpcontentpluginswporggptranslationeventsincludeseventphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/includes/event.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/includes/event.php (rev 0)
+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/includes/event.php 2024-03-04 11:41:16 UTC (rev 13268)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,80 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+
+namespace Wporg\TranslationEvents;
+
+use DateTimeImmutable;
+use DateTimeZone;
+use Exception;
+
+class Event {
+ private int $id;
+ private DateTimeImmutable $start;
+ private DateTimeImmutable $end;
+ private DateTimeZone $timezone;
+
+ /**
+ * Make an Event from post meta.
+ *
+ * @throws Exception When dates are invalid.
+ */
+ public static function from_post_meta( int $id, array $meta ): Event {
+ if ( ! isset( $meta['_event_start'][0] ) || ! isset( $meta['_event_end'][0] ) || ! isset( $meta['_event_timezone'][0] ) ) {
+ throw new Exception( 'Invalid event meta' );
+ }
+
+ return new Event(
+ $id,
+ DateTimeImmutable::createFromFormat( 'Y-m-d H:i:s', $meta['_event_start'][0], new DateTimeZone( 'UTC' ) ),
+ DateTimeImmutable::createFromFormat( 'Y-m-d H:i:s', $meta['_event_end'][0], new DateTimeZone( 'UTC' ) ),
+ new DateTimeZone( $meta['_event_timezone'][0] ),
+ );
+ }
+
+ private function __construct( int $id, DateTimeImmutable $start, DateTimeImmutable $end, DateTimeZone $timezone ) {
+ $this->id = $id;
+ $this->start = $start;
+ $this->end = $end;
+ $this->timezone = $timezone;
+ }
+
+ public function id(): int {
+ return $this->id;
+ }
+
+ public function start(): DateTimeImmutable {
+ return $this->start;
+ }
+
+ public function end(): DateTimeImmutable {
+ return $this->end;
+ }
+
+ public function timezone(): DateTimeZone {
+ return $this->timezone;
+ }
+
+ /**
+ * Generate text for the end date.
+ *
+ * @param string $event_end The end date.
+ *
+ * @return string The end date text.
+ */
+ public static function get_end_date_text( string $event_end ): string {
+ $end_date_time = new DateTimeImmutable( $event_end );
+ $current_date_time = new DateTimeImmutable( 'now', new DateTimeZone( 'UTC' ) );
+
+ $interval = $end_date_time->diff( $current_date_time );
+ $hours_left = ( $interval->d * 24 ) + $interval->h;
+ $hours_in_a_day = 24;
+
+ if ( 0 === $hours_left ) {
+ /* translators: %s: Number of minutes left. */
+ return sprintf( _n( 'ends in %s minute', 'ends in %s minutes', $interval->i ), $interval->i );
+ } elseif ( $hours_left <= $hours_in_a_day ) {
+ /* translators: %s: Number of hours left. */
+ return sprintf( _n( 'ends in %s hour', 'ends in %s hours', $hours_left ), $hours_left );
+ }
+ return sprintf( 'until %s', $end_date_time->format( 'M j, Y' ) );
+ }
+}
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/includes/event.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="sitestrunkwordpressorgpublic_htmlwpcontentpluginswporggptranslationeventsincludesroutephp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/includes/route.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/includes/route.php (rev 0)
+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/includes/route.php 2024-03-04 11:41:16 UTC (rev 13268)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,404 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+
+namespace Wporg\TranslationEvents;
+
+use DateTime;
+use DateTimeZone;
+use Exception;
+use GP_Route;
+use WP_Query;
+use GP;
+
+class Route extends GP_Route {
+ public const USER_META_KEY_ATTENDING = 'translation-events-attending';
+
+ public function __construct() {
+ parent::__construct();
+ $this->template_path = __DIR__ . '/../templates/';
+ }
+
+ /**
+ * Loads the 'events_list' template.
+ *
+ * @return void
+ */
+ public function events_list() {
+ $current_datetime_utc = null;
+ try {
+ $current_datetime_utc = ( new DateTime( 'now', new DateTimeZone( 'UTC' ) ) )->format( 'Y-m-d H:i:s' );
+ } catch ( Exception $e ) {
+ // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
+ error_log( $e );
+ $this->die_with_error( esc_html__( 'Something is wrong.', 'gp-translation-events' ) );
+ }
+
+ $_current_events_paged = 1;
+ $_upcoming_events_paged = 1;
+ $_past_events_paged = 1;
+ $_user_attending_events_paged = 1;
+
+ // phpcs:disable WordPress.Security.NonceVerification.Recommended
+ if ( isset( $_GET['current_events_paged'] ) ) {
+ $value = sanitize_text_field( wp_unslash( $_GET['current_events_paged'] ) );
+ if ( is_numeric( $value ) ) {
+ $_current_events_paged = (int) $value;
+ }
+ }
+ if ( isset( $_GET['upcoming_events_paged'] ) ) {
+ $value = sanitize_text_field( wp_unslash( $_GET['upcoming_events_paged'] ) );
+ if ( is_numeric( $value ) ) {
+ $_upcoming_events_paged = (int) $value;
+ }
+ }
+ if ( isset( $_GET['past_events_paged'] ) ) {
+ $value = sanitize_text_field( wp_unslash( $_GET['past_events_paged'] ) );
+ if ( is_numeric( $value ) ) {
+ $_past_events_paged = (int) $value;
+ }
+ }
+ if ( isset( $_GET['user_attending_events_paged'] ) ) {
+ $value = sanitize_text_field( wp_unslash( $_GET['user_attending_events_paged'] ) );
+ if ( is_numeric( $value ) ) {
+ $_user_attending_events_paged = (int) $value;
+ }
+ }
+ // phpcs:enable
+
+ $current_events_args = array(
+ 'post_type' => Translation_Events::CPT,
+ 'posts_per_page' => 10,
+ 'paged' => $_current_events_paged,
+ 'post_status' => 'publish',
+ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
+ 'meta_query' => array(
+ array(
+ 'key' => '_event_start',
+ 'value' => $current_datetime_utc,
+ 'compare' => '<=',
+ 'type' => 'DATETIME',
+ ),
+ array(
+ 'key' => '_event_end',
+ 'value' => $current_datetime_utc,
+ 'compare' => '>=',
+ 'type' => 'DATETIME',
+ ),
+ ),
+ 'orderby' => 'meta_value',
+ 'order' => 'ASC',
+ );
+ $current_events_query = new WP_Query( $current_events_args );
+
+ $upcoming_events_args = array(
+ 'post_type' => Translation_Events::CPT,
+ 'posts_per_page' => 10,
+ 'paged' => $_upcoming_events_paged,
+ 'post_status' => 'publish',
+ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
+ 'meta_query' => array(
+ array(
+ 'key' => '_event_start',
+ 'value' => $current_datetime_utc,
+ 'compare' => '>=',
+ 'type' => 'DATETIME',
+ ),
+ ),
+ 'orderby' => 'meta_value',
+ 'order' => 'ASC',
+ );
+ $upcoming_events_query = new WP_Query( $upcoming_events_args );
+
+ $past_events_args = array(
+ 'post_type' => Translation_Events::CPT,
+ 'posts_per_page' => 10,
+ 'paged' => $_past_events_paged,
+ 'post_status' => 'publish',
+ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
+ 'meta_query' => array(
+ array(
+ 'key' => '_event_end',
+ 'value' => $current_datetime_utc,
+ 'compare' => '<',
+ 'type' => 'DATETIME',
+ ),
+ ),
+ 'orderby' => 'meta_value',
+ 'order' => 'ASC',
+ );
+ $past_events_query = new WP_Query( $past_events_args );
+
+ $user_attending_events = get_user_meta( get_current_user_id(), self::USER_META_KEY_ATTENDING, true ) ?: array( 0 );
+ $user_attending_events_args = array(
+ 'post_type' => Translation_Events::CPT,
+ 'post__in' => array_keys( $user_attending_events ),
+ 'posts_per_page' => 10,
+ 'paged' => $_user_attending_events_paged,
+ 'post_status' => 'publish',
+ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
+ 'meta_query' => array(
+ array(
+ 'key' => '_event_end',
+ 'value' => $current_datetime_utc,
+ 'compare' => '>',
+ 'type' => 'DATETIME',
+ ),
+ ),
+ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
+ 'meta_key' => '_event_start',
+ 'orderby' => 'meta_value',
+ 'order' => 'ASC',
+ );
+ $user_attending_events_query = new WP_Query( $user_attending_events_args );
+
+ $this->tmpl( 'events-list', get_defined_vars() );
+ }
+
+ /**
+ * Loads the 'events_create' template.
+ *
+ * @return void
+ */
+ public function events_create() {
+ global $wp;
+ if ( ! is_user_logged_in() ) {
+ wp_safe_redirect( wp_login_url( home_url( $wp->request ) ) );
+ exit;
+ }
+ $event_form_title = 'Create Event';
+ $event_form_name = 'create_event';
+ $css_show_url = 'hide-event-url';
+ $event_id = null;
+ $event_title = '';
+ $event_description = '';
+ $event_timezone = '';
+ $event_start = '';
+ $event_end = '';
+ $event_url = '';
+ $create_delete_button = true;
+ $visibility_delete_button = 'none';
+
+ $this->tmpl( 'events-form', get_defined_vars() );
+ }
+
+ /**
+ * Loads the 'events_edit' template.
+ *
+ * @param int $event_id The event ID.
+ *
+ * @return void
+ */
+ public function events_edit( int $event_id ) {
+ global $wp;
+ if ( ! is_user_logged_in() ) {
+ wp_safe_redirect( wp_login_url( home_url( $wp->request ) ) );
+ exit;
+ }
+ $event = get_post( $event_id );
+ if ( ! $event || Translation_Events::CPT !== $event->post_type || ! ( current_user_can( 'edit_post', $event->ID ) || intval( $event->post_author ) === get_current_user_id() ) ) {
+ $this->die_with_error( esc_html__( 'Event does not exist, or you do not have permission to edit it.', 'gp-translation-events' ), 403 );
+ }
+ if ( 'trash' === $event->post_status ) {
+ $this->die_with_error( esc_html__( 'You cannot edit a trashed event', 'gp-translation-events' ), 403 );
+ }
+
+ include ABSPATH . 'wp-admin/includes/post.php';
+ $event_form_title = 'Edit Event';
+ $event_form_name = 'edit_event';
+ $css_show_url = '';
+ $event_title = $event->post_title;
+ $event_description = $event->post_content;
+ $event_status = $event->post_status;
+ list( $permalink, $post_name ) = get_sample_permalink( $event_id );
+ $permalink = str_replace( '%pagename%', $post_name, $permalink );
+ $event_url = get_site_url() . gp_url( wp_make_link_relative( $permalink ) );
+ $event_timezone = get_post_meta( $event_id, '_event_timezone', true ) ?: '';
+ $create_delete_button = false;
+ $visibility_delete_button = 'inline-flex';
+
+ $stats_calculator = new Stats_Calculator();
+ if ( ! $stats_calculator->event_has_stats( $event ) ) {
+ $current_user = wp_get_current_user();
+ if ( $current_user->ID === $event->post_author || current_user_can( 'manage_options' ) ) {
+ $create_delete_button = true;
+ }
+ }
+
+ try {
+ $event_start = self::convertToTimezone( get_post_meta( $event_id, '_event_start', true ), $event_timezone );
+ $event_end = self::convertToTimezone( get_post_meta( $event_id, '_event_end', true ), $event_timezone );
+ } catch ( Exception $e ) {
+ // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
+ error_log( $e );
+ $this->die_with_error( esc_html__( 'Something is wrong.', 'gp-translation-events' ) );
+ }
+
+ $this->tmpl( 'events-form', get_defined_vars() );
+ }
+
+ /**
+ * Loads the 'event' template.
+ *
+ * @param string $event_slug The event slug.
+ *
+ * @return void
+ */
+ public function events_details( string $event_slug ) {
+ $user = wp_get_current_user();
+ $event = get_page_by_path( $event_slug, OBJECT, Translation_Events::CPT );
+ if ( ! $event ) {
+ $this->die_with_404();
+ }
+ /**
+ * Filter the ability to create, edit, or delete an event.
+ *
+ * @param bool $can_crud_event Whether the user can create, edit, or delete an event.
+ */
+ $can_crud_event = apply_filters( 'gp_translation_events_can_crud_event', GP::$permission->current_user_can( 'admin' ) );
+ if ( 'publish' !== $event->post_status && ! $can_crud_event ) {
+ $this->die_with_error( esc_html__( 'You are not authorized to view this page.', 'gp-translation-events' ), 403 );
+ }
+
+ $event_id = $event->ID;
+ $event_title = $event->post_title;
+ $event_description = $event->post_content;
+ $event_start = get_post_meta( $event->ID, '_event_start', true ) ?: '';
+ $event_end = get_post_meta( $event->ID, '_event_end', true ) ?: '';
+ $attending_event_ids = get_user_meta( $user->ID, self::USER_META_KEY_ATTENDING, true ) ?: array();
+ $user_is_attending = isset( $attending_event_ids[ $event_id ] );
+
+ $stats_calculator = new Stats_Calculator();
+ try {
+ $event_stats = $stats_calculator->for_event( $event );
+ } catch ( Exception $e ) {
+ // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
+ error_log( $e );
+ $this->die_with_error( esc_html__( 'Failed to calculate event stats', 'gp-translation-events' ) );
+ }
+
+ $this->tmpl( 'event', get_defined_vars() );
+ }
+
+ /**
+ * Toggle whether the current user is attending an event.
+ * If the user is not currently marked as attending, they will be marked as attending.
+ * If the user is currently marked as attending, they will be marked as not attending.
+ */
+ public function events_attend( int $event_id ) {
+ $user = wp_get_current_user();
+ if ( ! $user ) {
+ $this->die_with_error( esc_html__( 'Only logged-in users can attend events', 'gp-translation-events' ), 403 );
+ }
+
+ $event = get_post( $event_id );
+
+ if ( ! $event ) {
+ $this->die_with_404();
+ }
+
+ $event_ids = get_user_meta( $user->ID, self::USER_META_KEY_ATTENDING, true ) ?? array();
+ if ( ! $event_ids ) {
+ $event_ids = array();
+ }
+
+ if ( ! isset( $event_ids[ $event_id ] ) ) {
+ // Not yet attending, mark as attending.
+ $event_ids[ $event_id ] = true;
+ } else {
+ // Currently attending, mark as not attending.
+ unset( $event_ids[ $event_id ] );
+ }
+
+ update_user_meta( $user->ID, self::USER_META_KEY_ATTENDING, $event_ids );
+
+ wp_safe_redirect( gp_url( "/events/$event->post_name" ) );
+ exit;
+ }
+
+ /**
+ * Loads the 'events_user_created' template.
+ *
+ * @return void
+ */
+ public function my_events() {
+ global $wp;
+ if ( ! is_user_logged_in() ) {
+ wp_safe_redirect( wp_login_url( home_url( $wp->request ) ) );
+ exit;
+ }
+ include ABSPATH . 'wp-admin/includes/post.php';
+
+ $_events_i_created_paged = 1;
+ $_events_i_attended_paged = 1;
+
+ // phpcs:disable WordPress.Security.NonceVerification.Recommended
+ if ( isset( $_GET['events_i_created_paged'] ) ) {
+ $value = sanitize_text_field( wp_unslash( $_GET['events_i_created_paged'] ) );
+ if ( is_numeric( $value ) ) {
+ $_events_i_created_paged = (int) $value;
+ }
+ }
+ if ( isset( $_GET['events_i_attended_paged'] ) ) {
+ $value = sanitize_text_field( wp_unslash( $_GET['events_i_attended_paged'] ) );
+ if ( is_numeric( $value ) ) {
+ $_events_i_attended_paged = (int) $value;
+ }
+ }
+ // phpcs:enable
+
+ $user_id = get_current_user_id();
+ $events = get_user_meta( $user_id, self::USER_META_KEY_ATTENDING, true ) ?: array();
+ $events = array_keys( $events );
+ $current_datetime_utc = ( new DateTime( 'now', new DateTimeZone( 'UTC' ) ) )->format( 'Y-m-d H:i:s' );
+ $args = array(
+ 'post_type' => Translation_Events::CPT,
+ 'posts_per_page' => 10,
+ 'events_i_created_paged' => $_events_i_created_paged,
+ 'paged' => $_events_i_created_paged,
+ 'post_status' => array( 'publish', 'draft' ),
+ 'author' => $user_id,
+ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
+ 'meta_key' => '_event_start',
+ 'orderby' => 'meta_value',
+ 'order' => 'DESC',
+ );
+ $events_i_created_query = new WP_Query( $args );
+
+ $args = array(
+ 'post_type' => Translation_Events::CPT,
+ 'posts_per_page' => 10,
+ 'events_i_attended_paged' => $_events_i_attended_paged,
+ 'paged' => $_events_i_attended_paged,
+ 'post_status' => 'publish',
+ 'post__in' => $events,
+ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
+ 'meta_query' => array(
+ array(
+ 'key' => '_event_end',
+ 'value' => $current_datetime_utc,
+ 'compare' => '<',
+ 'type' => 'DATETIME',
+ ),
+ ),
+ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
+ 'meta_key' => '_event_end',
+ 'orderby' => 'meta_value',
+ 'order' => 'DESC',
+ );
+ $events_i_attended_query = new WP_Query( $args );
+
+ $this->tmpl( 'events-my-events', get_defined_vars() );
+ }
+
+ /**
+ * Convert date time stored in UTC to a date time in a time zone.
+ *
+ * @param string $date_time The date time in UTC.
+ * @param string $time_zone The time zone.
+ *
+ * @return string The date time in the time zone.
+ * @throws Exception When date is invalid.
+ */
+ public static function convertToTimezone( string $date_time, string $time_zone ): string {
+ return ( new DateTime( $date_time, new DateTimeZone( 'UTC' ) ) )->setTimezone( new DateTimeZone( $time_zone ) )->format( 'Y-m-d H:i:s' );
+ }
+}
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/includes/route.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="sitestrunkwordpressorgpublic_htmlwpcontentpluginswporggptranslationeventsincludesstatscalculatorphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/includes/stats-calculator.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/includes/stats-calculator.php (rev 0)
+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/includes/stats-calculator.php 2024-03-04 11:41:16 UTC (rev 13268)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,134 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+
+namespace Wporg\TranslationEvents;
+
+use Exception;
+use WP_Post;
+
+class Stats_Row {
+ public int $created;
+ public int $reviewed;
+ public int $users;
+
+ public function __construct( $created, $reviewed, $users ) {
+ $this->created = $created;
+ $this->reviewed = $reviewed;
+ $this->users = $users;
+ }
+}
+
+class Event_Stats {
+ /**
+ * Associative array of rows, with the locale as key.
+ *
+ * @var Stats_Row[]
+ */
+ private array $rows = array();
+
+ private Stats_Row $totals;
+
+ /**
+ * Add a stats row.
+ *
+ * @throws Exception When incorrect locale is passed.
+ */
+ public function add_row( string $locale, Stats_Row $row ) {
+ if ( ! $locale ) {
+ throw new Exception( 'locale must not be empty' );
+ }
+ $this->rows[ $locale ] = $row;
+ }
+
+ public function set_totals( Stats_Row $totals ) {
+ $this->totals = $totals;
+ }
+
+ /**
+ * Get an associative array of rows, with the locale as key.
+ *
+ * @return Stats_Row[]
+ */
+ public function rows(): array {
+ return $this->rows;
+ }
+
+ public function totals(): Stats_Row {
+ return $this->totals;
+ }
+}
+
+class Stats_Calculator {
+ /**
+ * Get stats for an event.
+ *
+ * @throws Exception When stats calculation failed.
+ */
+ public function for_event( WP_Post $event ): Event_Stats {
+ $stats = new Event_Stats();
+ global $wpdb;
+
+ // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery
+ // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching
+ // phpcs thinks we're doing a schema change but we aren't.
+ // phpcs:disable WordPress.DB.DirectDatabaseQuery.SchemaChange
+ $rows = $wpdb->get_results(
+ $wpdb->prepare(
+ "
+ select locale,
+ sum(action = 'create') as created,
+ count(*) as total,
+ count(distinct user_id) as users
+ from {$wpdb->base_prefix}event_actions
+ where event_id = %d
+ group by locale with rollup
+ ",
+ array(
+ $event->ID,
+ )
+ )
+ );
+ // phpcs:enable
+
+ foreach ( $rows as $index => $row ) {
+ $is_totals = null === $row->locale;
+ if ( $is_totals && array_key_last( $rows ) !== $index ) {
+ // If this is not the last row, something is wrong in the data in the database table
+ // or there's a bug in the query above.
+ throw new Exception(
+ 'Only the last row should have no locale but we found a non-last row with no locale.'
+ );
+ }
+
+ $stats_row = new Stats_Row(
+ $row->created,
+ $row->total - $row->created,
+ $row->users,
+ );
+
+ if ( ! $is_totals ) {
+ $stats->add_row( $row->locale, $stats_row );
+ } else {
+ $stats->set_totals( $stats_row );
+ }
+ }
+
+ return $stats;
+ }
+
+ /**
+ * Check if an event has stats.
+ *
+ * @param WP_Post $event The event to check.
+ *
+ * @return bool True if the event has stats, false otherwise.
+ */
+ public function event_has_stats( WP_Post $event ): bool {
+ try {
+ $stats = $this->for_event( $event );
+ } catch ( Exception $e ) {
+ return false;
+ }
+
+ return ! empty( $stats->rows() );
+ }
+}
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/includes/stats-calculator.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="sitestrunkwordpressorgpublic_htmlwpcontentpluginswporggptranslationeventsincludesstatslistenerphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/includes/stats-listener.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/includes/stats-listener.php (rev 0)
+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/includes/stats-listener.php 2024-03-04 11:41:16 UTC (rev 13268)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,179 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+
+namespace Wporg\TranslationEvents;
+
+use DateTimeImmutable;
+use DateTimeZone;
+use Exception;
+use GP_Translation;
+use GP_Translation_Set;
+
+class Stats_Listener {
+ const ACTION_CREATE = 'create';
+ const ACTION_APPROVE = 'approve';
+ const ACTION_REJECT = 'reject';
+ const ACTION_REQUEST_CHANGES = 'request_changes';
+
+ private Active_Events_Cache $active_events_cache;
+
+ public function __construct( Active_Events_Cache $active_events_cache ) {
+ $this->active_events_cache = $active_events_cache;
+ }
+
+ public function start(): void {
+ add_action(
+ 'gp_translation_created',
+ function ( $translation ) {
+ $happened_at = DateTimeImmutable::createFromFormat( 'Y-m-d H:i:s', $translation->date_added, new DateTimeZone( 'UTC' ) );
+ $this->handle_action( $translation, $translation->user_id, self::ACTION_CREATE, $happened_at );
+ },
+ );
+
+ add_action(
+ 'gp_translation_saved',
+ function ( $translation, $translation_before ) {
+ $user_id = $translation->user_id_last_modified;
+ $status = $translation->status;
+ $happened_at = DateTimeImmutable::createFromFormat( 'Y-m-d H:i:s', $translation->date_modified, new DateTimeZone( 'UTC' ) );
+
+ if ( $translation_before->status === $status ) {
+ // Translation hasn't changed status, so there's nothing for us to track.
+ return;
+ }
+
+ $action = null;
+ switch ( $status ) {
+ case 'current':
+ $action = self::ACTION_APPROVE;
+ break;
+ case 'rejected':
+ $action = self::ACTION_REJECT;
+ break;
+ case 'changesrequested':
+ $action = self::ACTION_REQUEST_CHANGES;
+ break;
+ }
+
+ if ( $action ) {
+ $this->handle_action( $translation, $user_id, $action, $happened_at );
+ }
+ },
+ 10,
+ 2,
+ );
+ }
+
+ private function handle_action( GP_Translation $translation, int $user_id, string $action, DateTimeImmutable $happened_at ): void {
+ try {
+ // Get events that are active when the action happened, for which the user is registered for.
+ $active_events = $this->get_active_events( $happened_at );
+ $events = $this->select_events_user_is_registered_for( $active_events, $user_id );
+
+ // phpcs:ignore Generic.Commenting.DocComment.MissingShort
+ /** @var GP_Translation_Set $translation_set Translation set */
+ $translation_set = ( new GP_Translation_Set() )->find_one( array( 'id' => $translation->translation_set_id ) );
+ global $wpdb;
+
+ foreach ( $events as $event ) {
+ // A given user can only do one action on a specific translation.
+ // So we insert ignore, which will keep only the first action.
+ // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery
+ // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching
+ $wpdb->query(
+ $wpdb->prepare(
+ "insert ignore into {$wpdb->base_prefix}event_actions (event_id, locale, user_id, original_id, action, happened_at) values (%d, %s, %d, %d, %s, %s)",
+ array(
+ // Start unique key.
+ 'event_id' => $event->id(),
+ 'locale' => $translation_set->locale,
+ 'user_id' => $user_id,
+ 'original_id' => $translation->original_id,
+ // End unique key.
+ 'action' => $action,
+ 'happened_at' => $happened_at->format( 'Y-m-d H:i:s' ),
+ ),
+ ),
+ );
+ // phpcs:enable
+ }
+ } catch ( Exception $exception ) {
+ // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
+ error_log( $exception );
+ }
+ }
+
+ /**
+ * Get active events at a given time.
+ *
+ * @return Event[]
+ * @throws Exception When it fails to get active events.
+ */
+ private function get_active_events( DateTimeImmutable $at ): array {
+ $events = $this->active_events_cache->get();
+ if ( null === $events ) {
+ $cache_duration = Active_Events_Cache::CACHE_DURATION;
+ $boundary_start = $at;
+ $boundary_end = $at->modify( "+$cache_duration seconds" );
+
+ // Get events for which start is before $boundary_end AND end is after $boundary_start.
+ $event_ids = get_posts(
+ array(
+ 'post_type' => Translation_Events::CPT,
+ 'post_status' => 'publish',
+ 'posts_per_page' => - 1,
+ 'fields' => 'ids',
+ 'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
+ array(
+ 'key' => '_event_start',
+ 'value' => $boundary_end->format( 'Y-m-d H:i:s' ),
+ 'compare' => '<',
+ 'type' => 'DATETIME',
+ ),
+ array(
+ 'key' => '_event_end',
+ 'value' => $boundary_start->format( 'Y-m-d H:i:s' ),
+ 'compare' => '>',
+ 'type' => 'DATETIME',
+ ),
+ ),
+ ),
+ );
+
+ $events = array();
+ foreach ( $event_ids as $event_id ) {
+ $meta = get_post_meta( $event_id );
+ $events[] = Event::from_post_meta( $event_id, $meta );
+ }
+
+ $this->active_events_cache->cache( $events );
+ }
+
+ // Filter out events that aren't actually active at $at.
+ return array_filter(
+ $events,
+ function ( $event ) use ( $at ) {
+ return $event->start() <= $at && $at <= $event->end();
+ }
+ );
+ }
+
+ /**
+ * Filter an array of events so that it only includes events the given user is attending.
+ *
+ * @param Event[] $events Events.
+ *
+ * @return Event[]
+ */
+ // phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
+ // phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.Found
+ private function select_events_user_is_registered_for( array $events, int $user_id ): array {
+ $attending_event_ids = get_user_meta( $user_id, Route::USER_META_KEY_ATTENDING, true );
+ return array_filter(
+ $events,
+ function ( Event $event ) use ( $attending_event_ids ) {
+ return isset( $attending_event_ids[ $event->id() ] );
+ }
+ );
+ }
+ // phpcs:enable
+}
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/includes/stats-listener.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="sitestrunkwordpressorgpublic_htmlwpcontentpluginswporggptranslationeventstemplateseventphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/templates/event.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/templates/event.php (rev 0)
+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/templates/event.php 2024-03-04 11:41:16 UTC (rev 13268)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,121 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Template for event page.
+ */
+
+namespace Wporg\TranslationEvents;
+
+use WP_Post;
+
+/** @var WP_Post $event */
+/** @var int $event_id */
+/** @var string $event_title */
+/** @var string $event_description */
+/** @var string $event_start */
+/** @var string $event_end */
+/** @var bool $user_is_attending */
+/** @var Event_Stats $event_stats */
+
+/* translators: %s: Event title. */
+gp_title( sprintf( __( 'Translation Events - %s' ), esc_html( $event_title ) ) );
+gp_breadcrumb_translation_events( array( esc_html( $event_title ) ) );
+gp_tmpl_header();
+gp_tmpl_load( 'events-header', get_defined_vars(), __DIR__ );
+?>
+
+<div class="event-page-wrapper">
+ <div class="event-details-head">
+ <h1>
+ <?php echo esc_html( $event_title ); ?>
+ <?php if ( 'draft' === $event->post_status ) : ?>
+ <span class="event-label-draft"><?php echo esc_html( $event->post_status ); ?></span>
+ <?php endif; ?>
+ </h1>
+ <p>
+ Host: <a href="<?php echo esc_attr( get_author_posts_url( $event->post_author ) ); ?>"><?php echo esc_html( get_the_author_meta( 'display_name', $event->post_author ) ); ?></a>
+ <?php if ( current_user_can( 'edit_post', $event_id ) ) : ?>
+ <a class="event-page-edit-link button" href="<?php echo esc_url( gp_url( 'events/edit/' . $event_id ) ); ?>"><span class="dashicons dashicons-edit"></span>Edit event</a>
+ <?php endif ?>
+ </p>
+ </div>
+ <div class="event-details-left">
+ <div class="event-page-content">
+ <?php
+ echo wp_kses_post( wpautop( make_clickable( $event_description ) ) );
+ ?>
+ </div>
+ <?php if ( ! empty( $event_stats->rows() ) ) : ?>
+ <div class="event-details-stats">
+ <h2>Stats</h2>
+ <table>
+ <thead>
+ <tr>
+ <th scope="col">Locale</th>
+ <th scope="col">Translations created</th>
+ <th scope="col">Translations reviewed</th>
+ <th scope="col">Contributors</th>
+ </tr>
+ </thead>
+ <tbody>
+ <?php /** @var $row Stats_Row */ ?>
+ <?php foreach ( $event_stats->rows() as $locale_ => $row ) : ?>
+ <tr>
+ <td><?php echo esc_html( $locale_ ); ?></td>
+ <td><?php echo esc_html( $row->created ); ?></td>
+ <td><?php echo esc_html( $row->reviewed ); ?></td>
+ <td><?php echo esc_html( $row->users ); ?></td>
+ </tr>
+ <?php endforeach ?>
+ <tr class="event-details-stats-totals">
+ <td>Total</td>
+ <td><?php echo esc_html( $event_stats->totals()->created ); ?></td>
+ <td><?php echo esc_html( $event_stats->totals()->reviewed ); ?></td>
+ <td><?php echo esc_html( $event_stats->totals()->users ); ?></td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <details class="event-stats-summary">
+ <summary>View stats summary in text </summary>
+ <p class="event-stats-text"><?php echo esc_html( sprintf( 'At the %s event, %d people contributed in %d languages (%s), translated %d strings and reviewed %d strings.', esc_html( $event_title ), esc_html( $event_stats->totals()->users ), count( $event_stats->rows() ), esc_html( implode( ',', array_keys( $event_stats->rows() ) ) ), esc_html( $event_stats->totals()->created ), esc_html( $event_stats->totals()->reviewed ) ) ); ?></p>
+ </details>
+ <?php endif ?>
+ </div>
+ <div class="event-details-right">
+ <div class="event-details-date">
+ <p>
+ <span class="event-details-date-label">Starts:</span> <time class="event-utc-time" datetime="<?php echo esc_attr( $event_start ); ?>"></time>
+ <span class="event-details-date-label">Ends:</span><time class="event-utc-time" datetime="<?php echo esc_attr( $event_end ); ?>"></time>
+ </p>
+ </div>
+ <?php if ( is_user_logged_in() ) : ?>
+ <div class="event-details-join">
+ <?php
+ $current_time = gmdate( 'Y-m-d H:i:s' );
+ if ( strtotime( $current_time ) > strtotime( $event_end ) ) :
+ ?>
+ <?php if ( $user_is_attending ) : ?>
+ <span class="event-details-join-expired"><?php esc_html_e( 'You attended', 'gp-translation-events' ); ?></span>
+ <?php endif ?>
+ <?php else : ?>
+ <form class="event-details-attend" method="post" action="<?php echo esc_url( gp_url( "/events/attend/$event_id" ) ); ?>">
+ <?php if ( ! $user_is_attending ) : ?>
+ <input type="submit" class="button is-primary attend-btn" value="Attend Event"/>
+ <?php else : ?>
+ <input type="submit" class="button is-secondary attending-btn" value="You're attending"/>
+ <?php endif ?>
+ </form>
+ <?php endif ?>
+ </div>
+ <?php else : ?>
+ <div class="event-details-join">
+ <p>
+ <?php global $wp; ?>
+ <a href="<?php echo esc_url( wp_login_url( home_url( $wp->request ) ) ); ?>" class="button is-primary attend-btn"><?php esc_html_e( 'Login to attend', 'gp-translation-events' ); ?></a>
+ </p>
+ </div>
+ <?php endif; ?>
+ </div>
+</div>
+<div class="clear"></div>
+<?php gp_tmpl_footer(); ?>
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/templates/event.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="sitestrunkwordpressorgpublic_htmlwpcontentpluginswporggptranslationeventstemplateseventsformphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/templates/events-form.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/templates/events-form.php (rev 0)
+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/templates/events-form.php 2024-03-04 11:41:16 UTC (rev 13268)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,114 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Template for event form.
+ */
+
+namespace Wporg\TranslationEvents;
+
+/** @var string $event_form_title */
+/** @var string $event_form_name */
+/** @var int $event_id */
+/** @var string $event_title */
+/** @var string $event_description */
+/** @var string $event_start */
+/** @var string $event_end */
+/** @var string $event_timezone */
+/** @var string $event_url */
+/** @var string $css_show_url */
+
+gp_title( __( 'Translation Events' ) . ' - ' . esc_html( $event_form_title . ' - ' . $event_title ) );
+gp_breadcrumb_translation_events( array( esc_html( $event_form_title ) ) );
+gp_tmpl_header();
+gp_tmpl_load( 'events-header', get_defined_vars(), __DIR__ );
+?>
+<div class="event-page-wrapper">
+<h2 class="event-page-title"><?php echo esc_html( $event_form_title ); ?></h2>
+<form class="translation-event-form" action="" method="post">
+ <?php wp_nonce_field( '_event_nonce', '_event_nonce' ); ?>
+ <input type="hidden" name="action" value="submit_event_ajax">
+ <input type="hidden" id="form-name" name="form_name" value="<?php echo esc_attr( $event_form_name ); ?>">
+ <input type="hidden" id="event-id" name="event_id" value="<?php echo esc_attr( $event_id ); ?>">
+ <input type="hidden" id="event-form-action" name="event_form_action">
+ <div>
+ <label for="event-title">Event Title</label>
+ <input type="text" id="event-title" name="event_title" value="<?php echo esc_html( $event_title ); ?>" required>
+ </div>
+ <div id="event-url" class="<?php echo esc_attr( $css_show_url ); ?>">
+ <label for="event-permalink">Event URL</label>
+ <a id="event-permalink" class="event-permalink" href="<?php echo esc_url( $event_url ); ?>" target="_blank"><?php echo esc_url( $event_url ); ?></a>
+ </div>
+ <div>
+ <label for="event-description">Event Description</label>
+ <textarea id="event-description" name="event_description" rows="4" required><?php echo esc_html( $event_description ); ?></textarea>
+ </div>
+ <div>
+ <label for="event-start">Start Date</label>
+ <input type="datetime-local" id="event-start" name="event_start" value="<?php echo esc_attr( $event_start ); ?>" required>
+ </div>
+ <div>
+ <label for="event-end">End Date</label>
+ <input type="datetime-local" id="event-end" name="event_end" value="<?php echo esc_attr( $event_end ); ?>" required>
+ </div>
+ <div>
+ <label for="event-timezone">Event Timezone</label>
+ <select id="event-timezone" name="event_timezone" required>
+ <?php
+ echo wp_kses(
+ wp_timezone_choice( $event_timezone, get_user_locale() ),
+ array(
+ 'optgroup' => array( 'label' => array() ),
+ 'option' => array(
+ 'value' => array(),
+ 'selected' => array(),
+ ),
+ )
+ );
+ ?>
+ </select>
+ </div>
+ <div class="submit-btn-group">
+ <label for="event-status"></label>
+ <?php if ( $event_id ) : ?>
+ <?php if ( isset( $event_status ) && 'draft' === $event_status ) : ?>
+ <button class="button is-primary save-draft submit-event" type="submit" data-event-status="draft">Update Draft</button>
+ <?php endif; ?>
+ <button class="button is-primary submit-event" type="submit" data-event-status="publish">
+ <?php echo ( isset( $event_status ) && 'publish' === $event_status ) ? esc_html( 'Update Event' ) : esc_html( 'Publish Event' ); ?>
+ </button>
+ <?php else : ?>
+ <button class="button is-primary save-draft submit-event" type="submit" data-event-status="draft">Save Draft</button>
+ <button class="button is-primary submit-event" type="submit" data-event-status="publish">Publish Event</button>
+ <?php endif; ?>
+ <?php if ( isset( $create_delete_button ) && $create_delete_button ) : ?>
+ <button id="delete-button" class="button is-destructive delete-event" type="submit" name="submit" value="Delete" style="display: <?php echo esc_attr( $visibility_delete_button ); ?>">Delete Event</button>
+ <?php endif; ?>
+ </div>
+ <div class="clear"></div>
+ <div class="published-update-text">
+ <?php
+ $visibility_published_button = 'none';
+ if ( isset( $event_status ) && 'publish' === $event_status ) {
+ $visibility_published_button = 'block';
+ }
+ ?>
+ <span id="published-update-text" style="display: <?php echo esc_attr( $visibility_published_button ); ?>">
+ <?php
+ $polyglots_slack_channel = 'https://wordpress.slack.com/archives/C02RP50LK';
+ echo wp_kses(
+ // translators: %s: Polyglots Slack channel URL.
+ sprintf( __( 'If you need to update the event slug, please, contact with an admin in the <a href="%s" target="_blank">Polyglots</a> channel in Slack.', 'gp-translation-events' ), $polyglots_slack_channel ),
+ array(
+ 'a' => array(
+ 'href' => array(),
+ 'target' => array(),
+ ),
+
+ )
+ );
+ ?>
+ </span>
+ </div>
+</form>
+</div>
+<div class="clear"></div>
+<?php gp_tmpl_footer(); ?>
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/templates/events-form.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="sitestrunkwordpressorgpublic_htmlwpcontentpluginswporggptranslationeventstemplateseventsheaderphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/templates/events-header.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/templates/events-header.php (rev 0)
+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/templates/events-header.php 2024-03-04 11:41:16 UTC (rev 13268)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,24 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+namespace Wporg\TranslationEvents;
+
+use GP;
+?>
+
+<div class="event-list-top-bar">
+ <ul class="event-list-nav">
+ <?php if ( is_user_logged_in() ) : ?>
+ <li><a href="<?php echo esc_url( gp_url( '/events/my-events/' ) ); ?>">My Events</a></li>
+ <?php
+ /**
+ * Filter the ability to create, edit, or delete an event.
+ *
+ * @param bool $can_crud_event Whether the user can create, edit, or delete an event.
+ */
+ $can_crud_event = apply_filters( 'gp_translation_events_can_crud_event', GP::$permission->current_user_can( 'admin' ) );
+ if ( $can_crud_event ) :
+ ?>
+ <li><a class="button is-primary" href="<?php echo esc_url( gp_url( '/events/new/' ) ); ?>">Create Event</a></li>
+ <?php endif; ?>
+ <?php endif; ?>
+ </ul>
+</div>
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/templates/events-header.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="sitestrunkwordpressorgpublic_htmlwpcontentpluginswporggptranslationeventstemplateseventslistphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/templates/events-list.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/templates/events-list.php (rev 0)
+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/templates/events-list.php 2024-03-04 11:41:16 UTC (rev 13268)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,184 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Events list page.
+ */
+
+namespace Wporg\TranslationEvents;
+
+use DateTime;
+use WP_Query;
+
+/** @var WP_Query $current_events_query */
+/** @var WP_Query $upcoming_events_query */
+/** @var WP_Query $past_events_query */
+
+gp_title( __( 'Translation Events', 'gp-translation-events' ) );
+gp_breadcrumb_translation_events();
+gp_tmpl_header();
+gp_tmpl_load( 'events-header', get_defined_vars(), __DIR__ );
+?>
+
+<div class="event-page-wrapper">
+ <h1 class="event_page_title"><?php esc_html_e( 'Translation Events', 'gp-translation-events' ); ?></h1>
+<div class="event-left-col">
+<?php
+if ( $current_events_query->have_posts() ) :
+ ?>
+ <h2><?php esc_html_e( 'Current events', 'gp-translation-events' ); ?></h2>
+ <ul class="event-list">
+ <?php
+ while ( $current_events_query->have_posts() ) :
+ $current_events_query->the_post();
+ $event_end = Event::get_end_date_text( get_post_meta( get_the_ID(), '_event_end', true ) );
+ $event_url = gp_url( wp_make_link_relative( get_the_permalink() ) );
+ ?>
+ <li class="event-list-item">
+ <a href="<?php echo esc_url( $event_url ); ?>"><?php the_title(); ?></a>
+ <span class="event-list-date"><?php echo esc_html( $event_end ); ?></span>
+ <?php the_excerpt(); ?>
+ </li>
+ <?php
+ endwhile;
+ ?>
+ </ul>
+
+ <?php
+ echo wp_kses_post(
+ paginate_links(
+ array(
+ 'total' => $current_events_query->max_num_pages,
+ 'current' => max( 1, $current_events_query->query_vars['paged'] ),
+ 'format' => '?current_events_paged=%#%',
+ 'prev_text' => '« Previous',
+ 'next_text' => 'Next »',
+ )
+ ) ?? ''
+ );
+
+ wp_reset_postdata();
+endif;
+if ( $upcoming_events_query->have_posts() ) :
+ ?>
+ <h2><?php esc_html_e( 'Upcoming events', 'gp-translation-events' ); ?></h2>
+ <ul class="event-list">
+ <?php
+ while ( $upcoming_events_query->have_posts() ) :
+ $upcoming_events_query->the_post();
+ $event_start = ( new DateTime( get_post_meta( get_the_ID(), '_event_start', true ) ) )->format( 'l, F j, Y' );
+ ?>
+ <li class="event-list-item">
+ <a href="<?php echo esc_url( gp_url( wp_make_link_relative( get_the_permalink() ) ) ); ?>"><?php the_title(); ?></a>
+ <span class="event-list-date"><?php echo esc_html( $event_start ); ?></span>
+ <?php the_excerpt(); ?>
+ </li>
+ <?php
+ endwhile;
+ ?>
+ </ul>
+
+ <?php
+ echo wp_kses_post(
+ paginate_links(
+ array(
+ 'total' => $upcoming_events_query->max_num_pages,
+ 'current' => max( 1, $upcoming_events_query->query_vars['paged'] ),
+ 'format' => '?upcoming_events_paged=%#%',
+ 'prev_text' => '« Previous',
+ 'next_text' => 'Next »',
+ )
+ ) ?? ''
+ );
+
+ wp_reset_postdata();
+endif;
+if ( $past_events_query->have_posts() ) :
+ ?>
+ <h2><?php esc_html_e( 'Past events', 'gp-translation-events' ); ?></h2>
+ <ul class="event-list">
+ <?php
+ while ( $past_events_query->have_posts() ) :
+ $past_events_query->the_post();
+ $event_start = ( new DateTime( get_post_meta( get_the_ID(), '_event_start', true ) ) )->format( 'M j, Y' );
+ $event_end = ( new DateTime( get_post_meta( get_the_ID(), '_event_end', true ) ) )->format( 'M j, Y' );
+ ?>
+ <li class="event-list-item">
+ <a href="<?php echo esc_url( gp_url( wp_make_link_relative( get_the_permalink() ) ) ); ?>"><?php the_title(); ?></a>
+ <?php if ( $event_start === $event_end ) : ?>
+ <span class="event-list-date"><?php echo esc_html( $event_start ); ?></span>
+ <?php else : ?>
+ <span class="event-list-date"><?php echo esc_html( $event_start ); ?> - <?php echo esc_html( $event_end ); ?></span>
+ <?php endif; ?>
+ <?php the_excerpt(); ?>
+ </li>
+ <?php
+ endwhile;
+ ?>
+ </ul>
+
+ <?php
+ echo wp_kses_post(
+ paginate_links(
+ array(
+ 'total' => $past_events_query->max_num_pages,
+ 'current' => max( 1, $past_events_query->query_vars['paged'] ),
+ 'format' => '?past_events_paged=%#%',
+ 'prev_text' => '« Previous',
+ 'next_text' => 'Next »',
+ )
+ ) ?? ''
+ );
+
+ wp_reset_postdata();
+endif;
+
+if ( 0 === $current_events_query->post_count && 0 === $upcoming_events_query->post_count && 0 === $past_events_query->post_count ) :
+ esc_html_e( 'No events found.', 'gp-translation-events' );
+endif;
+?>
+</div>
+<?php if ( is_user_logged_in() ) : ?>
+ <div class="event-right-col">
+ <h3 class="">Events I'm Attending</h3>
+ <?php if ( ! $user_attending_events_query->have_posts() ) : ?>
+ <p>You don't have any events to attend.</p>
+ <?php else : ?>
+ <ul class="event-attending-list">
+ <?php
+ while ( $user_attending_events_query->have_posts() ) :
+ $user_attending_events_query->the_post();
+ $event_start = ( new DateTime( get_post_meta( get_the_ID(), '_event_start', true ) ) )->format( 'M j, Y' );
+ $event_end = ( new DateTime( get_post_meta( get_the_ID(), '_event_end', true ) ) )->format( 'M j, Y' );
+ ?>
+ <li class="event-list-item">
+ <a href="<?php echo esc_url( gp_url( wp_make_link_relative( get_the_permalink() ) ) ); ?>"><?php the_title(); ?></a>
+ <?php if ( $event_start === $event_end ) : ?>
+ <span class="event-list-date events-i-am-attending"><?php echo esc_html( $event_start ); ?></span>
+ <?php else : ?>
+ <span class="event-list-date events-i-am-attending"><?php echo esc_html( $event_start ); ?> - <?php echo esc_html( $event_end ); ?></span>
+ <?php endif; ?>
+ </li>
+ <?php
+ endwhile;
+ ?>
+ </ul>
+ <?php
+ echo wp_kses_post(
+ paginate_links(
+ array(
+ 'total' => $user_attending_events_query->max_num_pages,
+ 'current' => max( 1, $user_attending_events_query->query_vars['paged'] ),
+ 'format' => '?user_attending_events_paged=%#%',
+ 'prev_text' => '« Previous',
+ 'next_text' => 'Next »',
+ )
+ ) ?? ''
+ );
+
+ wp_reset_postdata();
+ endif;
+ ?>
+ </div>
+<?php endif; ?>
+</div>
+<div class="clear"></div>
+<?php gp_tmpl_footer(); ?>
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/templates/events-list.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="sitestrunkwordpressorgpublic_htmlwpcontentpluginswporggptranslationeventstemplateseventsmyeventsphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/templates/events-my-events.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/templates/events-my-events.php (rev 0)
+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/templates/events-my-events.php 2024-03-04 11:41:16 UTC (rev 13268)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,122 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Template for My Events.
+ */
+
+namespace Wporg\TranslationEvents;
+
+use DateTime;
+use WP_Query;
+
+/** @var WP_Query $events_i_created_query */
+/** @var WP_Query $events_i_attended_query */
+
+gp_title( esc_html__( 'Translation Events', 'gp-translation-events' ) . ' - ' . esc_html__( 'My Events', 'gp-translation-events' ) );
+gp_breadcrumb_translation_events( array( esc_html__( 'My Events', 'gp-translation-events' ) ) );
+gp_tmpl_header();
+gp_tmpl_load( 'events-header', get_defined_vars(), __DIR__ );
+?>
+
+<div class="event-page-wrapper">
+ <h1 class="event_page_title"><?php esc_html_e( 'My Events', 'gp-translation-events' ); ?> </h1>
+ <h2 class="event_page_title"><?php esc_html_e( 'Events I have created', 'gp-translation-events' ); ?> </h2>
+ <?php if ( $events_i_created_query->have_posts() ) : ?>
+ <ul>
+ <?php
+ while ( $events_i_created_query->have_posts() ) :
+ $events_i_created_query->the_post();
+ $event_id = get_the_ID();
+ $event_start = get_post_meta( $event_id, '_event_start', true );
+ list( $permalink, $post_name ) = get_sample_permalink( $event_id );
+ $permalink = str_replace( '%pagename%', $post_name, $permalink );
+ $event_url = gp_url( wp_make_link_relative( $permalink ) );
+ $event_edit_url = gp_url( 'events/edit/' . $event_id );
+ $event_status = get_post_status( $event_id );
+ $event_start = ( new DateTime( get_post_meta( get_the_ID(), '_event_start', true ) ) )->format( 'M j, Y' );
+ $event_end = ( new DateTime( get_post_meta( get_the_ID(), '_event_end', true ) ) )->format( 'M j, Y' );
+ ?>
+ <li class="event-list-item">
+ <a class="event-link-<?php echo esc_attr( $event_status ); ?>" href="<?php echo esc_url( $event_url ); ?>"><?php the_title(); ?></a>
+ <a href="<?php echo esc_url( $event_edit_url ); ?>" class="button is-small action edit">Edit</a>
+ <?php if ( 'draft' === $event_status ) : ?>
+ <span class="event-label-<?php echo esc_attr( $event_status ); ?>"><?php echo esc_html( $event_status ); ?></span>
+ <?php endif; ?>
+ <?php if ( $event_start === $event_end ) : ?>
+ <span class="event-list-date events-i-am-attending"><?php echo esc_html( $event_start ); ?></span>
+ <?php else : ?>
+ <span class="event-list-date events-i-am-attending"><?php echo esc_html( $event_start ); ?> - <?php echo esc_html( $event_end ); ?></span>
+ <?php endif; ?>
+ <p><?php the_excerpt(); ?></p>
+ </li>
+ <?php endwhile; ?>
+ </ul>
+
+ <?php
+ echo wp_kses_post(
+ paginate_links(
+ array(
+ 'total' => $events_i_created_query->max_num_pages,
+ 'current' => max( 1, $events_i_created_query->query_vars['events_i_created_paged'] ),
+ 'format' => '?events_i_created_paged=%#%',
+ 'prev_text' => '« Previous',
+ 'next_text' => 'Next »',
+ )
+ ) ?? ''
+ );
+
+ wp_reset_postdata();
+ else :
+ echo 'No events found.';
+ endif;
+ ?>
+
+ <h2 class="event_page_title"><?php esc_html_e( 'Events I attended', 'gp-translation-events' ); ?> </h2>
+ <?php if ( $events_i_attended_query->have_posts() ) : ?>
+ <ul>
+ <?php
+ while ( $events_i_attended_query->have_posts() ) :
+ $events_i_attended_query->the_post();
+ $event_id = get_the_ID();
+ $event_start = get_post_meta( $event_id, '_event_start', true );
+ list( $permalink, $post_name ) = get_sample_permalink( $event_id );
+ $permalink = str_replace( '%pagename%', $post_name, $permalink );
+ $event_url = gp_url( wp_make_link_relative( $permalink ) );
+ $event_edit_url = gp_url( 'events/edit/' . $event_id );
+ $event_status = get_post_status( $event_id );
+ $event_start = ( new DateTime( get_post_meta( get_the_ID(), '_event_start', true ) ) )->format( 'M j, Y' );
+ $event_end = ( new DateTime( get_post_meta( get_the_ID(), '_event_end', true ) ) )->format( 'M j, Y' );
+ ?>
+ <li class="event-list-item">
+ <a class="event-link-<?php echo esc_attr( $event_status ); ?>" href="<?php echo esc_url( $event_url ); ?>"><?php the_title(); ?></a>
+ <?php if ( $event_start === $event_end ) : ?>
+ <span class="event-list-date events-i-am-attending"><?php echo esc_html( $event_start ); ?></span>
+ <?php else : ?>
+ <span class="event-list-date events-i-am-attending"><?php echo esc_html( $event_start ); ?> - <?php echo esc_html( $event_end ); ?></span>
+ <?php endif; ?>
+ <p><?php the_excerpt(); ?></p>
+ </li>
+ <?php endwhile; ?>
+ </ul>
+
+ <?php
+ echo wp_kses_post(
+ paginate_links(
+ array(
+ 'total' => $events_i_attended_query->max_num_pages,
+ 'current' => max( 1, $events_i_attended_query->query_vars['events_i_attended_paged'] ),
+ 'format' => '?events_i_attended_paged=%#%',
+ 'prev_text' => '« Previous',
+ 'next_text' => 'Next »',
+ )
+ ) ?? ''
+ );
+
+ wp_reset_postdata();
+ else :
+ echo 'No events found.';
+ endif;
+ ?>
+</div>
+<?php
+ gp_tmpl_footer();
+?>
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/templates/events-my-events.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="sitestrunkwordpressorgpublic_htmlwpcontentpluginswporggptranslationeventstemplateshelperfunctionsphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/templates/helper-functions.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/templates/helper-functions.php (rev 0)
+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/templates/helper-functions.php 2024-03-04 11:41:16 UTC (rev 13268)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,17 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Get event breadcrumb.
+ *
+ * @param array $extra_items Array of additional items to add to the breadcrumb.
+ *
+ * @return string HTML of the breadcrumb.
+ */
+function gp_breadcrumb_translation_events( $extra_items = array() ) {
+ $breadcrumb = array(
+ empty( $extra_items ) ? __( 'Events', 'gp-translation-events' ) : gp_link_get( gp_url( '/events' ), __( 'Events', 'gp-translation-events' ) ),
+ );
+ if ( ! empty( $extra_items ) ) {
+ $breadcrumb = array_merge( $breadcrumb, $extra_items );
+ }
+ return gp_breadcrumb( $breadcrumb );
+}
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/templates/helper-functions.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="sitestrunkwordpressorgpublic_htmlwpcontentpluginswporggptranslationeventswporggptranslationeventsphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/wporg-gp-translation-events.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/wporg-gp-translation-events.php (rev 0)
+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/wporg-gp-translation-events.php 2024-03-04 11:41:16 UTC (rev 13268)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,541 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Plugin Name: Translation Events
+ * Plugin URI: https://github.com/WordPress/wporg-gp-translation-events/
+ * Description: A WordPress plugin for creating translation events.
+ * Version: 1.0.0
+ * Requires at least: 6.4
+ * Tested up to: 6.4
+ * Requires PHP: 7.4
+ * Author: WordPress Contributors
+ * Author URI: https://github.com/WordPress/wporg-gp-translation-events/
+ * License: GPLv2 or later
+ * License URI: http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
+ * Text Domain: gp-translation-events
+ *
+ * @package Translation Events
+ */
+
+namespace Wporg\TranslationEvents;
+
+use DateTime;
+use DateTimeZone;
+use Exception;
+use GP;
+use WP_Post;
+use WP_Query;
+
+class Translation_Events {
+ const CPT = 'translation_event';
+
+ public static function get_instance() {
+ static $instance = null;
+ if ( null === $instance ) {
+ $instance = new self();
+ }
+ return $instance;
+ }
+
+ public function __construct() {
+ \add_action( 'wp_ajax_submit_event_ajax', array( $this, 'submit_event_ajax' ) );
+ \add_action( 'wp_ajax_nopriv_submit_event_ajax', array( $this, 'submit_event_ajax' ) );
+ \add_action( 'wp_enqueue_scripts', array( $this, 'register_translation_event_js' ) );
+ \add_action( 'init', array( $this, 'register_event_post_type' ) );
+ \add_action( 'add_meta_boxes', array( $this, 'event_meta_boxes' ) );
+ \add_action( 'save_post', array( $this, 'save_event_meta_boxes' ) );
+ \add_action( 'transition_post_status', array( $this, 'event_status_transition' ), 10, 3 );
+ \add_filter( 'gp_nav_menu_items', array( $this, 'gp_event_nav_menu_items' ), 10, 2 );
+ \add_filter( 'wp_insert_post_data', array( $this, 'generate_event_slug' ), 10, 2 );
+ \add_action( 'gp_init', array( $this, 'gp_init' ) );
+ \add_action( 'gp_before_translation_table', array( $this, 'add_active_events_current_user' ) );
+ \register_activation_hook( __FILE__, array( $this, 'activate' ) );
+ }
+
+ public function gp_init() {
+ require_once __DIR__ . '/templates/helper-functions.php';
+ require_once __DIR__ . '/includes/active-events-cache.php';
+ require_once __DIR__ . '/includes/event.php';
+ require_once __DIR__ . '/includes/route.php';
+ require_once __DIR__ . '/includes/stats-calculator.php';
+ require_once __DIR__ . '/includes/stats-listener.php';
+
+ GP::$router->add( '/events?', array( 'Wporg\TranslationEvents\Route', 'events_list' ) );
+ GP::$router->add( '/events/new', array( 'Wporg\TranslationEvents\Route', 'events_create' ) );
+ GP::$router->add( '/events/edit/(\d+)', array( 'Wporg\TranslationEvents\Route', 'events_edit' ) );
+ GP::$router->add( '/events/attend/(\d+)', array( 'Wporg\TranslationEvents\Route', 'events_attend' ), 'post' );
+ GP::$router->add( '/events/my-events', array( 'Wporg\TranslationEvents\Route', 'my_events' ) );
+ GP::$router->add( '/events/([a-z0-9_-]+)', array( 'Wporg\TranslationEvents\Route', 'events_details' ) );
+
+ $active_events_cache = new Active_Events_Cache();
+ $stats_listener = new Stats_Listener( $active_events_cache );
+ $stats_listener->start();
+ }
+
+ public function activate() {
+ global $wpdb;
+ $create_table = "
+ CREATE TABLE `{$wpdb->base_prefix}event_actions` (
+ `translate_event_actions_id` int(11) unsigned NOT NULL AUTO_INCREMENT,
+ `event_id` int(10) NOT NULL COMMENT 'Post_ID of the translation_event post in the wp_posts table',
+ `original_id` int(10) NOT NULL COMMENT 'ID of the translation',
+ `user_id` int(10) NOT NULL COMMENT 'ID of the user who made the action',
+ `action` enum('approve','create','reject','request_changes') NOT NULL COMMENT 'The action that the user made (create, reject, etc)',
+ `locale` varchar(10) CHARACTER SET latin1 COLLATE latin1_swedish_ci NOT NULL COMMENT 'Locale of the translation',
+ `happened_at` datetime NOT NULL COMMENT 'When the action happened, in UTC',
+ PRIMARY KEY (`translate_event_actions_id`),
+ UNIQUE KEY `event_per_translated_original_per_user` (`event_id`,`locale`,`original_id`,`user_id`)
+ ) COMMENT='Tracks translation actions that happened during a translation event'";
+ require_once ABSPATH . 'wp-admin/includes/upgrade.php';
+ dbDelta( $create_table );
+ }
+
+ /**
+ * Register the event post type.
+ */
+ public function register_event_post_type() {
+ $labels = array(
+ 'name' => 'Translation Events',
+ 'singular_name' => 'Translation Event',
+ 'menu_name' => 'Translation Events',
+ 'add_new' => 'Add New',
+ 'add_new_item' => 'Add New Translation Event',
+ 'edit_item' => 'Edit Translation Event',
+ 'new_item' => 'New Translation Event',
+ 'view_item' => 'View Translation Event',
+ 'search_items' => 'Search Translation Events',
+ 'not_found' => 'No translation events found',
+ 'not_found_in_trash' => 'No translation events found in trash',
+ );
+
+ $args = array(
+ 'labels' => $labels,
+ 'public' => true,
+ 'has_archive' => true,
+ 'menu_icon' => 'dashicons-calendar',
+ 'supports' => array( 'title', 'editor', 'thumbnail', 'revisions' ),
+ 'rewrite' => array( 'slug' => 'events' ),
+ 'show_ui' => false,
+ );
+
+ register_post_type( self::CPT, $args );
+ }
+ /**
+ * Add meta boxes for the event post type.
+ */
+ public function event_meta_boxes() {
+ \add_meta_box( 'event_dates', 'Event Dates', array( $this, 'event_dates_meta_box' ), self::CPT, 'normal', 'high' );
+ }
+
+ /**
+ * Output the event dates meta box.
+ *
+ * @param WP_Post $post The current post object.
+ */
+ public function event_dates_meta_box( WP_Post $post ) {
+ wp_nonce_field( 'event_dates_nonce', 'event_dates_nonce' );
+ $event_start = get_post_meta( $post->ID, '_event_start', true );
+ $event_end = get_post_meta( $post->ID, '_event_end', true );
+ echo '<label for="event_start">Start Date: </label>';
+ echo '<input type="date" id="event_start" name="event_start" value="' . esc_attr( $event_start ) . '" required>';
+ echo '<label for="event_end">End Date: </label>';
+ echo '<input type="date" id="event_end" name="event_end" value="' . esc_attr( $event_end ) . '" required>';
+ }
+
+ /**
+ * Save the event meta boxes.
+ *
+ * @param int $post_id The current post ID.
+ */
+ public function save_event_meta_boxes( int $post_id ) {
+ $nonces = array( 'event_dates' );
+ foreach ( $nonces as $nonce ) {
+ $nonce_name = $nonce . '_nonce';
+ if ( ! isset( $_POST[ $nonce_name ] ) ) {
+ return;
+ }
+ $nonce_value = sanitize_text_field( wp_unslash( $_POST[ $nonce_name ] ) );
+ if ( ! wp_verify_nonce( $nonce_value, $nonce_name ) ) {
+ return;
+ }
+ }
+
+ $fields = array( 'event_start', 'event_end' );
+ foreach ( $fields as $field ) {
+ if ( isset( $_POST[ $field ] ) ) {
+ update_post_meta( $post_id, '_' . $field, sanitize_text_field( wp_unslash( $_POST[ $field ] ) ) );
+ }
+ }
+ }
+
+ /**
+ * Validate the event dates.
+ *
+ * @param string $event_start The event start date.
+ * @param string $event_end The event end date.
+ * @return bool Whether the event dates are valid.
+ * @throws Exception When dates are invalid.
+ */
+ public function validate_event_dates( string $event_start, string $event_end ): bool {
+ if ( ! $event_start || ! $event_end ) {
+ return false;
+ }
+ $event_start = new DateTime( $event_start );
+ $event_end = new DateTime( $event_end );
+ if ( $event_start < $event_end ) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Handle the event form submission for the creation, editing, and deletion of events. This function is called via AJAX.
+ */
+ public function submit_event_ajax() {
+ if ( ! is_user_logged_in() ) {
+ wp_send_json_error( esc_html__( 'The user must be logged in.', 'gp-translation-events' ), 403 );
+ }
+ $action = isset( $_POST['form_name'] ) ? sanitize_text_field( wp_unslash( $_POST['form_name'] ) ) : '';
+ $event_id = null;
+ $event = null;
+ $response_message = '';
+ $form_actions = array( 'draft', 'publish', 'delete' );
+ $is_nonce_valid = false;
+ $nonce_name = '_event_nonce';
+ if ( ! in_array( $action, array( 'create_event', 'edit_event', 'delete_event' ), true ) ) {
+ wp_send_json_error( esc_html__( 'Invalid form name.', 'gp-translation-events' ), 403 );
+ }
+ /**
+ * Filter the ability to create, edit, or delete an event.
+ *
+ * @param bool $can_crud_event Whether the user can create, edit, or delete an event.
+ */
+ $can_crud_event = apply_filters( 'gp_translation_events_can_crud_event', GP::$permission->current_user_can( 'admin' ) );
+ if ( 'create_event' === $action && ( ! $can_crud_event ) ) {
+ wp_send_json_error( esc_html__( 'The user does not have permission to create an event.', 'gp-translation-events' ), 403 );
+ }
+ if ( 'edit_event' === $action ) {
+ $event_id = isset( $_POST['event_id'] ) ? sanitize_text_field( wp_unslash( $_POST['event_id'] ) ) : '';
+ $event = get_post( $event_id );
+ if ( ! ( $can_crud_event || current_user_can( 'edit_post', $event_id ) || intval( $event->post_author ) === get_current_user_id() ) ) {
+ wp_send_json_error( esc_html__( 'The user does not have permission to edit or delete the event.', 'gp-translation-events' ), 403 );
+ }
+ }
+ if ( 'delete_event' === $action ) {
+ $event_id = isset( $_POST['event_id'] ) ? sanitize_text_field( wp_unslash( $_POST['event_id'] ) ) : '';
+ $event = get_post( $event_id );
+ if ( ! ( $can_crud_event || current_user_can( 'delete_post', $event->ID ) || get_current_user_id() === $event->post_author ) ) {
+ wp_send_json_error( esc_html__( 'You do not have permission to delete this event.', 'gp-translation-events' ), 403 );
+ }
+ }
+ if ( isset( $_POST[ $nonce_name ] ) ) {
+ $nonce_value = sanitize_text_field( wp_unslash( $_POST[ $nonce_name ] ) );
+ if ( wp_verify_nonce( $nonce_value, $nonce_name ) ) {
+ $is_nonce_valid = true;
+ }
+ }
+ if ( ! $is_nonce_valid ) {
+ wp_send_json_error( esc_html__( 'Nonce verification failed.', 'gp-translation-events' ), 403 );
+ }
+ // This is a list of slugs that are not allowed, as they conflict with the event URLs.
+ $invalid_slugs = array( 'new', 'edit', 'attend', 'my-events' );
+ $title = isset( $_POST['event_title'] ) ? sanitize_text_field( wp_unslash( $_POST['event_title'] ) ) : '';
+ // This will be sanitized by santitize_post which is called in wp_insert_post.
+ $description = isset( $_POST['event_description'] ) ? force_balance_tags( wp_unslash( $_POST['event_description'] ) ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
+ $event_start = isset( $_POST['event_start'] ) ? sanitize_text_field( wp_unslash( $_POST['event_start'] ) ) : '';
+ $event_end = isset( $_POST['event_end'] ) ? sanitize_text_field( wp_unslash( $_POST['event_end'] ) ) : '';
+ $event_timezone = isset( $_POST['event_timezone'] ) ? sanitize_text_field( wp_unslash( $_POST['event_timezone'] ) ) : '';
+ if ( isset( $title ) && in_array( sanitize_title( $title ), $invalid_slugs, true ) ) {
+ wp_send_json_error( esc_html__( 'Invalid slug.', 'gp-translation-events' ), 422 );
+ }
+
+ $is_valid_event_date = false;
+ try {
+ $is_valid_event_date = $this->validate_event_dates( $event_start, $event_end );
+ } catch ( Exception $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
+ // Deliberately ignored, handled below.
+ }
+ if ( ! $is_valid_event_date ) {
+ wp_send_json_error( esc_html__( 'Invalid event dates.', 'gp-translation-events' ), 422 );
+ }
+
+ $event_status = '';
+ if ( isset( $_POST['event_form_action'] ) && in_array( $_POST['event_form_action'], $form_actions, true ) ) {
+ $event_status = sanitize_text_field( wp_unslash( $_POST['event_form_action'] ) );
+ }
+
+ if ( ! isset( $_POST['form_name'] ) ) {
+ wp_send_json_error( esc_html__( 'Form name must be set.', 'gp-translation-events' ), 422 );
+ }
+
+ if ( 'create_event' === $action ) {
+ $event_id = wp_insert_post(
+ array(
+ 'post_type' => self::CPT,
+ 'post_title' => $title,
+ 'post_content' => $description,
+ 'post_status' => $event_status,
+ )
+ );
+ $response_message = esc_html__( 'Event created successfully!', 'gp-translation-events' );
+ }
+ if ( 'edit_event' === $action ) {
+ if ( ! isset( $_POST['event_id'] ) ) {
+ wp_send_json_error( esc_html__( 'Event id is required.', 'gp-translation-events' ), 422 );
+ }
+ $event_id = sanitize_text_field( wp_unslash( $_POST['event_id'] ) );
+ $event = get_post( $event_id );
+ if ( ! $event || self::CPT !== $event->post_type || ! ( current_user_can( 'edit_post', $event->ID ) || intval( $event->post_author ) === get_current_user_id() ) ) {
+ wp_send_json_error( esc_html__( 'Event does not exist.', 'gp-translation-events' ), 404 );
+ }
+ wp_update_post(
+ array(
+ 'ID' => $event_id,
+ 'post_title' => $title,
+ 'post_content' => $description,
+ 'post_status' => $event_status,
+ )
+ );
+ $response_message = esc_html__( 'Event updated successfully!', 'gp-translation-events' );
+ }
+ if ( 'delete_event' === $action ) {
+ $event_id = sanitize_text_field( wp_unslash( $_POST['event_id'] ) );
+ $event = get_post( $event_id );
+ if ( ! $event || self::CPT !== $event->post_type ) {
+ wp_send_json_error( esc_html__( 'Event does not exist.', 'gp-translation-events' ), 404 );
+ }
+ if ( ! ( current_user_can( 'delete_post', $event->ID ) || get_current_user_id() === $event->post_author ) ) {
+ wp_send_json_error( 'You do not have permission to delete this event' );
+ }
+ $stats_calculator = new Stats_Calculator();
+ try {
+ $event_stats = $stats_calculator->for_event( $event );
+ } catch ( Exception $e ) {
+ wp_send_json_error( esc_html__( 'Failed to calculate event stats.', 'gp-translation-events' ), 500 );
+ }
+ if ( ! empty( $event_stats->rows() ) ) {
+ wp_send_json_error( esc_html__( 'Event has translations and cannot be deleted.', 'gp-translation-events' ), 422 );
+ }
+ wp_trash_post( $event_id );
+ $response_message = esc_html__( 'Event deleted successfully!', 'gp-translation-events' );
+ }
+ if ( ! $event_id ) {
+ wp_send_json_error( esc_html__( 'Event could not be created or updated.', 'gp-translation-events' ), 422 );
+ }
+ if ( 'delete_event' !== $_POST['form_name'] ) {
+ try {
+ update_post_meta( $event_id, '_event_start', $this->convert_to_utc( $event_start, $event_timezone ) );
+ update_post_meta( $event_id, '_event_end', $this->convert_to_utc( $event_end, $event_timezone ) );
+ } catch ( Exception $e ) {
+ wp_send_json_error( esc_html__( 'Invalid start or end', 'gp-translation-events' ), 422 );
+ }
+
+ update_post_meta( $event_id, '_event_timezone', $event_timezone );
+ }
+ try {
+ Active_Events_Cache::invalidate();
+ } catch ( Exception $e ) {
+ // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
+ error_log( $e );
+ }
+
+ list( $permalink, $post_name ) = get_sample_permalink( $event_id );
+ $permalink = str_replace( '%pagename%', $post_name, $permalink );
+ wp_send_json_success(
+ array(
+ 'message' => $response_message,
+ 'eventId' => $event_id,
+ 'eventUrl' => str_replace( '%pagename%', $post_name, $permalink ),
+ 'eventStatus' => $event_status,
+ 'eventEditUrl' => esc_url( gp_url( '/events/edit/' . $event_id ) ),
+ 'eventDeleteUrl' => esc_url( gp_url( '/events/my-events/' ) ),
+ )
+ );
+ }
+
+
+
+
+ /**
+ * Convert a date time in a time zone to UTC.
+ *
+ * @param string $date_time The date time in the time zone.
+ * @param string $time_zone The time zone.
+ * @return string The date time in UTC.
+ * @throws Exception When dates are invalid.
+ */
+ public function convert_to_utc( string $date_time, string $time_zone ): string {
+ $date_time = new DateTime( $date_time, new DateTimeZone( $time_zone ) );
+ $date_time->setTimezone( new DateTimeZone( 'UTC' ) );
+ return $date_time->format( 'Y-m-d H:i:s' );
+ }
+
+ public function register_translation_event_js() {
+ wp_register_style( 'translation-events-css', plugins_url( 'assets/css/translation-events.css', __FILE__ ), array(), filemtime( __DIR__ . '/assets/css/translation-events.css' ) );
+ gp_enqueue_style( 'translation-events-css' );
+ wp_register_script( 'translation-events-js', plugins_url( 'assets/js/translation-events.js', __FILE__ ), array( 'jquery', 'gp-common' ), filemtime( __DIR__ . '/assets/js/translation-events.js' ), false );
+ gp_enqueue_script( 'translation-events-js' );
+ wp_localize_script(
+ 'translation-events-js',
+ '$translation_event',
+ array(
+ 'url' => admin_url( 'admin-ajax.php' ),
+ '_event_nonce' => wp_create_nonce( self::CPT ),
+ )
+ );
+ }
+
+ /**
+ * Handle the event status transition.
+ *
+ * The user who creates the event will assist to it when it's published.
+ *
+ * @param string $new_status The new post status.
+ * @param string $old_status The old post status.
+ * @param WP_Post $post The post object.
+ */
+ public function event_status_transition( string $new_status, string $old_status, WP_Post $post ): void {
+ if ( self::CPT !== $post->post_type ) {
+ return;
+ }
+ if ( 'publish' === $new_status && ( 'new' === $old_status || 'draft' === $old_status ) ) {
+ $current_user_id = get_current_user_id();
+ $user_attending_events = get_user_meta( $current_user_id, Route::USER_META_KEY_ATTENDING, true ) ?: array();
+ $is_user_attending_event = in_array( $post->ID, $user_attending_events, true );
+ if ( ! $is_user_attending_event ) {
+ $new_user_attending_events = $user_attending_events;
+ $new_user_attending_events[ $post->ID ] = true;
+ update_user_meta( $current_user_id, Route::USER_META_KEY_ATTENDING, $new_user_attending_events, $user_attending_events );
+ }
+ }
+ }
+
+ /**
+ * Add the events link to the GlotPress main menu.
+ *
+ * @param array $items The menu items.
+ * @param string $location The menu location.
+ * @return array The modified menu items.
+ */
+ public function gp_event_nav_menu_items( array $items, string $location ): array {
+ if ( 'main' !== $location ) {
+ return $items;
+ }
+ $new[ esc_url( gp_url( '/events/' ) ) ] = esc_html__( 'Events', 'gp-translation-events' );
+ return array_merge( $items, $new );
+ }
+
+ /**
+ * Generate a slug for the event post type when we save a draft event or when we publish an event.
+ *
+ * Generate a slug based on the event title if:
+ * - The event is a draft.
+ * - The event is published and it was a draft just before.
+ *
+ * @param array $data An array of slashed post data.
+ * @param array $postarr An array of sanitized, but otherwise unmodified post data.
+ * @return array The modified post data.
+ */
+ public function generate_event_slug( array $data, array $postarr ): array {
+ if ( self::CPT === $data['post_type'] ) {
+ if ( 'draft' === $data['post_status'] ) {
+ $data['post_name'] = sanitize_title( $data['post_title'] );
+ }
+ if ( 'publish' === $data['post_status'] ) {
+ if ( is_numeric( $postarr['ID'] ) && 0 !== $postarr['ID'] ) {
+ $post = get_post( $postarr['ID'] );
+ if ( $post instanceof WP_Post ) {
+ if ( 'draft' === $post->post_status ) {
+ $data['post_name'] = sanitize_title( $data['post_title'] );
+ }
+ }
+ }
+ }
+ }
+
+ return $data;
+ }
+
+ /**
+ * Add the active events for the current user before the translation table.
+ *
+ * @return void
+ */
+ public function add_active_events_current_user(): void {
+ $user_attending_events = get_user_meta( get_current_user_id(), Route::USER_META_KEY_ATTENDING, true ) ?: array();
+ $current_datetime_utc = ( new DateTime( 'now', new DateTimeZone( 'UTC' ) ) )->format( 'Y-m-d H:i:s' );
+ $user_attending_events_args = array(
+ 'post_type' => self::CPT,
+ 'post__in' => array_keys( $user_attending_events ),
+ 'post_status' => 'publish',
+ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
+ 'meta_query' => array(
+ array(
+ 'key' => '_event_start',
+ 'value' => $current_datetime_utc,
+ 'compare' => '<=',
+ 'type' => 'DATETIME',
+ ),
+ array(
+ 'key' => '_event_end',
+ 'value' => $current_datetime_utc,
+ 'compare' => '>=',
+ 'type' => 'DATETIME',
+ ),
+ ),
+ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
+ 'meta_key' => '_event_start',
+ 'orderby' => 'meta_value',
+ 'order' => 'ASC',
+ );
+ $user_attending_events_query = new WP_Query( $user_attending_events_args );
+ $number_of_events = $user_attending_events_query->post_count;
+ if ( 0 === $number_of_events ) {
+ return;
+ }
+
+ $content = '<div id="active-events-before-translation-table" class="active-events-before-translation-table">';
+ /* translators: %d: Number of events */
+ $content .= sprintf( _n( 'Contributing to %d event:', 'Contributing to %d events:', $number_of_events, 'gp-translation-events' ), $number_of_events );
+ $content .= ' ';
+ if ( $number_of_events > 3 ) {
+ $counter = 0;
+ while ( $user_attending_events_query->have_posts() && $counter < 2 ) {
+ $user_attending_events_query->the_post();
+ $url = esc_url( gp_url( '/events/' . get_post_field( 'post_name', get_post() ) ) );
+ $content .= '<span class="active-events-before-translation-table"><a href="' . $url . '" target="_blank">' . get_the_title() . '</a></span>';
+ ++$counter;
+ }
+
+ $remaining_events = $number_of_events - 2;
+ $url = esc_url( gp_url( '/events/' ) );
+ /* translators: %d: Number of remaining events */
+ $content .= '<span class="remaining-events"><a href="' . $url . '" target="_blank">' . sprintf( esc_html__( ' and %d more events.', 'gp-translation-events' ), $remaining_events ) . '</a></span>';
+
+ } else {
+ while ( $user_attending_events_query->have_posts() ) {
+ $user_attending_events_query->the_post();
+ $url = esc_url( gp_url( '/events/' . get_post_field( 'post_name', get_post() ) ) );
+ $content .= '<span class="active-events-before-translation-table"><a href="' . $url . '" target="_blank">' . get_the_title() . '</a></span>';
+ }
+ }
+ $content .= '</div>';
+
+ echo wp_kses(
+ $content,
+ array(
+ 'div' => array(
+ 'id' => array(),
+ 'class' => array(),
+ ),
+ 'span' => array(
+ 'class' => array(),
+ ),
+ 'a' => array(
+ 'href' => array(),
+ 'target' => array(),
+ ),
+ )
+ );
+ }
+}
+Translation_Events::get_instance();
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/wporg-gp-translation-events.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span></div>
</body>
</html>