From f453497f371ef98ace39e7489573965a64684c3d Mon Sep 17 00:00:00 2001 From: panlatent Date: Fri, 14 Jun 2024 09:28:37 +0800 Subject: [PATCH] Add build configuration to .readthedocs.yml --- .gitattributes | 10 + .readthedocs.yml | 5 + UPGRADE-1.x.md | 2 +- abstract/Action.php | 2 +- abstract/ConditionAction.php | 34 +++ abstract/Context.php | 4 +- abstract/ContextInterface.php | 3 + abstract/RunnerInterface.php | 8 + abstract/TriggerInterface.php | 1 - composer.json | 8 +- config/schedule.php | 10 + docs/assets/logo.png | Bin 0 -> 3899 bytes docs/installation-setup.md | 2 + docs/requirements.txt | 3 +- mkdocs.yml | 19 ++ src/Plugin.php | 18 +- src/Scheduler.php | 6 +- src/actions/Closure.php | 17 ++ src/actions/Console.php | 70 +++++-- src/actions/ElementAction.php | 61 ++++++ src/actions/HttpRequest.php | 64 +++++- src/actions/SendEmail.php | 60 ++++++ src/base/Timer.php | 51 +---- src/base/TimerInterface.php | 11 +- src/base/TimerTrait.php | 43 ---- src/builder/Interval.php | 20 ++ src/builder/Schedule.php | 24 ++- src/console/SchedulesController.php | 2 + src/controllers/ActionsController.php | 83 ++++++++ src/controllers/SchedulesController.php | 74 ++----- src/controllers/TimersController.php | 19 ++ src/controllers/WebCronController.php | 34 +++ src/di/ContainerAdapter.php | 24 +++ src/events/ActionEvent.php | 11 + src/events/ScheduleBuildEvent.php | 1 + src/events/ScheduleEvent.php | 16 +- src/events/ScheduleGroupEvent.php | 9 +- src/events/TimerEvent.php | 9 +- src/fields/Action.php | 10 + src/fields/Schedule.php | 10 + src/helpers/CronHelper.php | 6 + src/icon-mask.svg | 19 +- src/icon.svg | 18 +- src/migrations/Install.php | 105 ++++++---- src/models/Context.php | 17 +- src/models/Schedule.php | 56 ++--- src/models/ScheduleInfo.php | 11 + src/models/Settings.php | 33 +++ src/records/Action.php | 10 + src/records/Schedule.php | 10 +- src/services/Actions.php | 194 +++++++++++++++++ src/services/ScheduleGroups.php | 8 - src/services/Schedules.php | 40 ++-- src/services/Timers.php | 8 +- .../_components/actions/Console/settings.twig | 30 +++ .../actions/ElementAction/settings.twig | 63 ++++++ .../actions/HttpRequest/settings.twig | 146 +++++++++++++ .../actions/SendEmail/settings.twig | 48 +++++ src/templates/_components/timers/Cron.twig | 102 +++++++++ src/templates/_edit.twig | 195 ++++++++++++++---- src/templates/_includes/forms.twig | 6 + src/templates/_includes/forms/actionType.twig | 22 ++ src/templates/_layouts/settings.twig | 3 + src/templates/actions/_edit.twig | 53 +++++ src/templates/settings/general/index.twig | 1 - src/templates/settings/webcron/index.twig | 58 ++++++ src/timers/Cron.php | 179 ++++++++++++++++ src/timers/Custom.php | 1 + src/timers/DateTime.php | 1 + src/timers/Every.php | 1 + src/timers/Relay.php | 41 ++-- 71 files changed, 1908 insertions(+), 435 deletions(-) create mode 100644 .gitattributes create mode 100644 abstract/ConditionAction.php create mode 100644 abstract/RunnerInterface.php create mode 100644 docs/assets/logo.png create mode 100644 src/actions/Closure.php create mode 100644 src/actions/ElementAction.php create mode 100644 src/actions/SendEmail.php create mode 100644 src/controllers/ActionsController.php create mode 100644 src/controllers/WebCronController.php create mode 100644 src/di/ContainerAdapter.php create mode 100644 src/events/ActionEvent.php create mode 100644 src/fields/Action.php create mode 100644 src/fields/Schedule.php create mode 100644 src/models/ScheduleInfo.php create mode 100644 src/records/Action.php create mode 100644 src/services/Actions.php delete mode 100644 src/services/ScheduleGroups.php create mode 100644 src/templates/_components/actions/Console/settings.twig create mode 100644 src/templates/_components/actions/ElementAction/settings.twig create mode 100644 src/templates/_components/actions/HttpRequest/settings.twig create mode 100644 src/templates/_components/actions/SendEmail/settings.twig create mode 100644 src/templates/_components/timers/Cron.twig create mode 100644 src/templates/_includes/forms.twig create mode 100644 src/templates/_includes/forms/actionType.twig create mode 100644 src/templates/actions/_edit.twig create mode 100644 src/templates/settings/webcron/index.twig create mode 100644 src/timers/Cron.php diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..539986a --- /dev/null +++ b/.gitattributes @@ -0,0 +1,10 @@ +# Do not export those files in the Composer archive +/.github/ export-ignore +/docs/ export-ignore +/tests/ export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore +/.readthedocs.yml export-ignore +/CHANGELOG.md export-ignore +/codeception.yml export-ignore +/mkdocs.yml export-ignore diff --git a/.readthedocs.yml b/.readthedocs.yml index e2d68fd..a57dbfa 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -4,6 +4,11 @@ # Required version: 2 +build: + os: ubuntu-22.04 + tools: + python: "3.12" + # Build documentation in the docs/ directory with Sphinx # sphinx: # configuration: docs/conf.py diff --git a/UPGRADE-1.x.md b/UPGRADE-1.x.md index 1d237b1..c74b2e6 100644 --- a/UPGRADE-1.x.md +++ b/UPGRADE-1.x.md @@ -6,7 +6,7 @@ This is a 0.x - 1.0 update note. This article assumes that you have already used ## Documentation Promise -The new version will promise a relatively complete documentation.py +The new version will promise a relatively complete documentation ## Schedule diff --git a/abstract/Action.php b/abstract/Action.php index f11f489..82eab20 100644 --- a/abstract/Action.php +++ b/abstract/Action.php @@ -6,5 +6,5 @@ abstract class Action extends SavableComponent implements ActionInterface { - + public ?string $uid = null; } \ No newline at end of file diff --git a/abstract/ConditionAction.php b/abstract/ConditionAction.php new file mode 100644 index 0000000..d36390b --- /dev/null +++ b/abstract/ConditionAction.php @@ -0,0 +1,34 @@ +action; + } + + public function validateConditions(): bool + { + return true; + } + + public function execute(ContextInterface $context): bool + { + + + + return $this->action->execute($context); + } +} \ No newline at end of file diff --git a/abstract/Context.php b/abstract/Context.php index ca3269f..e8483b8 100644 --- a/abstract/Context.php +++ b/abstract/Context.php @@ -2,9 +2,7 @@ namespace panlatent\craft\actions\abstract; -use yii\base\Component; - -abstract class Context extends Component implements ContextInterface +abstract class Context implements ContextInterface { } \ No newline at end of file diff --git a/abstract/ContextInterface.php b/abstract/ContextInterface.php index 232ff68..1439d91 100644 --- a/abstract/ContextInterface.php +++ b/abstract/ContextInterface.php @@ -2,10 +2,13 @@ namespace panlatent\craft\actions\abstract; +use Psr\Container\ContainerInterface; use Psr\Log\LoggerInterface; interface ContextInterface { + public function getContainer(): ContainerInterface; + public function getErrors(): array; public function getLogger(): LoggerInterface; diff --git a/abstract/RunnerInterface.php b/abstract/RunnerInterface.php new file mode 100644 index 0000000..49de6cc --- /dev/null +++ b/abstract/RunnerInterface.php @@ -0,0 +1,8 @@ + [ + Schedule::closure(static fn() => true)->minute(), + Schedule::request('')->hourly()->onSuccess(function() { }), + Schedule::exec('ls')->hourly()->onSuccess(function() { + + }), + + Schedule::console('db/backup')->hourly()->onSuccess(function() { + + }), ], + ]; \ No newline at end of file diff --git a/docs/assets/logo.png b/docs/assets/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..b9cffa0e17bcc5c59e5d87be3351f1ca846a35c0 GIT binary patch literal 3899 zcmZ`+c|4Tc8-K?zF*9Y!PCsL^FEhg+rXhPHWXUqh(#XijIy2T1V_#duAVP~swuopE zBPvQnTuX>ZmndC42z$kYf+yJmM6=&*3{-VXzh=K&CKD~Zt*rqSw$2McKtceR17SY^ND|=rrUQTz zNa}Ch9i;RF!^OsE0PGbIVNdTQ3c&pXe~c|TgTS6_Pv502C7kT%Pof7>5w_Go1Qw-( z!m@4PV$jc+#aw^;U>1Xa>TDd)mDrWY7JNYj7a9P-wsIy2I9njj_7OmFAkvA}R=7if z{wR`HpeGp>?jOWq0r+qnTlFW?Nr-TNzW^F8Tp#%jfn)2OZ8Q?`4MIPnk0e^#B1{6Q zWP}b1gTf#Upa=v4PxbP~*_)dGptIlfkv?>K5DtwF3kySqVNrq9!)OgXJv}r=6RoMK z&PJ%yA_C~7aP36K6A^_v50_JwEZ-kN!XN@6?}6#{df5K=TLXx9#6qEBwI?^n3e%+hzB^0hE0#e|<{^&@7qbvj70SZeeQV5Dt3d24h~-g%`XVIa>e8 zD|oN>N3T02@OP)UW97UKNv-QkdU`cEsCd1?-pbiE<}SXrzRovmHI+fa+mtT8=6c1b zLg#dSwi+D!q8Sv7be9Mh8o1g@eS!(KVM%qX>52`+60%tPm*|T(dp(lX#yrlwJPA7@ z+uOm4EPNqsOe5)+ZmY(^ju=cSo9ghd^$*IaNtYa$PkDE;FqVR6&Xv84r>!7qq-Uia z0O8Dovm&{$s|KXUg4(JWu>pL>d-olRY{kmzkCC3s`o(D{BoAP@cDCQ&e33{{0^L`1 zOINoN+guo^9X&f-Qe!Ubwo`zA_o5@T0!DhwM3Dc{!aS>m|bXMb<26>W_V6Y3RE!t7JO@PLc!KIgjVOFc`>Ao>ziPa-y&Oo}$jx##BC7EHpW9Rh{183zlxmedm|iIJ<58@*nQtwdA&1 zd0kjjE@GT{P-=hLDj}}}u=1zeoHS{XnO@nYP92Hg<6giQW zBoFbL7kn1ng2n2*txd|`PZ!nP_x{f@v-h13?Vs!|JRwRP65;8!9~x|E%!(QNWaxBN zq{6k?(B8hhT>8PD?BTK&<4|0fz!@3W)!8=+f3#kFn>VJdwwT!%m+q63c^v;{f|j!< zE6zk`COgJc$MBpkq}t-4|3gwlxhS&7&%n*WYFTw%U0LmHmrhY!YkzHC=HWfz z*StHcxk3tSDUy!ox?AeK$+njW`f>pp_dkY!d2UYoy!i6@#w_gfSbZzdjLpDkth@Dh zI1G;z4KwaeAygi3I=?)~@F}{;#~F2m8Qas&mURKvgl}h0|9R@)n0H3akOll`Qo=Ubt6o( z*wyF~C2KgTq{c~Al;@h(gtbS8O}|v*o9D*UsfPLviLOrd@wFu}cSH^X5xuu9#V+sj zDrB4a*Ov1sXl`2#zl8RgP-%V&{Sh`IXv&rE!& z#n|>0>fm>;AuA5YRSr%iQo9~4N?~nBb_*yGUic@oA|=LgEBQkbhRvCn!%dH5m~Ba^ z2@+<;%f8ueI|}%G_X|ZY8}P#J6x|JLK$pXPq(VJJr!t8qw)~4*iV{S)c{edC^JmD# zV@U-}qlc6EIf@@pWW&r`=?!;3RlG`V&nO*JjF&Qs4mIJCP2g*DC1!qh+-zi}v9^u| zJL2cd5*ZVZ5<(V>yDOb~6N6!YQWj3?N%-=uq14LMpn&7D7w^(diVK9xdPg;pT>4>% zim`1oi5|DNX1)FFctT5T)um5#+W1=Ehz)8J5G~VIA9kx2j>zW2Cf3#&bel`&sSHe= zd+*%#QT^^dK5!B`Ng!;kco&y(GfzW6q$i}zGhyrfakp7wLLxL4+AQpJDgiFAMJ&gG zD4`pavrTi!g5??AZ)=hF2E#uiyRBJlHI-brFN^>u#B1x4A7g^TtK?X|iH@5h@nE@% zAUu3`j}+MbT4a~cw&i%guXAI#$(@SO$PXt19h!4{JxF4?4evsFTdswWf{DI8iJy9V z*0S9XCKA5ZcQ6iFTFQ2vntJ5_(K|*}#k2JTG3p3o>$Xo|>WR9C=Ew@n=C^g&n)Vn| z;(_Lkl~ga?g9GIDy4O4f7kv zKf@7=yIixL-nPs!79vf=2H#rI#PH226eBp!>>;*@3&2@ZnXnsYYPe}jUzZc^T^R}O z1S)Tg4k+`KLSapNM~}6k${s4~oR&P#f8EOvp|T2zK4DWQ{B&(@G{xgaJ76lplidDE z^{u`b^PZ6aK3zh*G$CiyH!ab zp*`A7+yd46-}{(kn5wAMI@@$?Q?k<=qJF;VrU$vC7a#TL#0dpKF4&OKI=3H0cwN<5 zI-Zp}U-lfc6U-Vt9nrMW>%|o#yYqknC=Em!Q))jw{m0W}Rkbd?=Q)L-gtAU`#!5}D z;ncBo51BTADj&7OCScZ&+xwr_*+Dz(e3zO!t6?V|8A4XMy4&Qfq6Gqb7TC@_>pX>{^d?D<9Pge^N-(5wDaLj5$&8b(@O(Gq8xJ05a>OqK&<%xRlxT(dIIx(@*UDZ*S4+7{P%j(EdSd3!$zb zMtioIijSE!QL9f*WUOdW^PF_2=pWkV2oS~D!}-Sv7y8sDH04<{rhaq`OrtB^Ige z>js6768Tbn7BaW@Ul}xSwZE6B-rW7voqw%HSh#nD2R&(pTr%yIYEVcq-5B7Pw3EEY z_nKm;V6SZFn3gVda{G%1z1`~NuR)+Xy+Yz1B67SBuU)`UEG_;R^{Uj`-J2CBnls&~ zVP5d)K$erA>D~L28 zgumkXbYjsLo?IvFoRt~3t{h>hejZEMX#LVdlq1?4I+B&8B%7{L>Jvd;i+kv8S;L#D zJIZVY2+vpg>$Vy{Y-^>Q27LDy}&c}cHVPy**Y_CDJaq{dQhooL{KC<*E^xi9Z|nu@01)v{~9aB6~N?V7T{ z#|rJsgr}#BWEbyvc)v19wEkKvjBox!&&TE942_seiqe4x4$10`N{Yu^C_8&;M<-N! sf)3Bn1W4_9Ed-y*D94NE^V`12upHXdy+NY#oc}`$v%RKO#s_2n3*jhC=l}o! literal 0 HcmV?d00001 diff --git a/docs/installation-setup.md b/docs/installation-setup.md index 4f02066..d78b543 100644 --- a/docs/installation-setup.md +++ b/docs/installation-setup.md @@ -1,5 +1,7 @@ # Installation and Setup + + You can install Schedule via the plugin store, or through Composer. ## Craft Plugin Store diff --git a/docs/requirements.txt b/docs/requirements.txt index 898468c..f522c59 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1 +1,2 @@ -mkdocs-material \ No newline at end of file +mkdocs-material +mkdocs-git-revision-date-localized-plugin \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index d5a07fd..838dda0 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -27,10 +27,23 @@ theme: - content.code.copy - navigation.instant - navigation.instant.progress + - navigation.footer + - navigation.path + - navigation.top + - toc.integrate + - search.suggest + - search.highlight + logo: assets/logo.png icon: repo: fontawesome/brands/github +copyright: Copyright © 2024 Panlatent + extra: + generator: false + social: + - icon: fontawesome/brands/github + link: https://github.com/panlatent version: provider: mike # alternate: @@ -90,6 +103,11 @@ markdown_extensions: custom_checkbox: true - pymdownx.tilde +plugins: + - search + - git-revision-date-localized: + type: timeago + nav: - 'introduction.md' - 'Getting Started': @@ -103,4 +121,5 @@ nav: - About: about.md repo_url: https://github.com/panlatent/schedule +repo_name: panlatent/schedule edit_uri: edit/main/docs \ No newline at end of file diff --git a/src/Plugin.php b/src/Plugin.php index 5ee3d23..a635a2f 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -20,6 +20,7 @@ use craft\web\UrlManager; use panlatent\schedule\console\SchedulesController; use panlatent\schedule\models\Settings; +use panlatent\schedule\services\Actions; use panlatent\schedule\services\Logs; use panlatent\schedule\services\Schedules; use panlatent\schedule\services\Timers; @@ -33,6 +34,7 @@ * * @package panlatent\schedule * @method Settings getSettings() + * @property-read Actions $actions * @property-read Schedules $schedules * @property-read Timers $timers * @property-read Settings $settings @@ -40,7 +42,7 @@ */ class Plugin extends \craft\base\Plugin { - public const EDITION_STANDARD = 'standard'; + public const EDITION_LITE = 'lite'; public const EDITION_PRO = 'pro'; // Properties @@ -63,6 +65,7 @@ public static function config(): array { return [ 'components' => [ + 'actions' => Actions::class, 'schedules' => Schedules::class, 'timers' => Timers::class, 'logs' => Logs::class, @@ -73,7 +76,7 @@ public static function config(): array public static function editions(): array { return [ - self::EDITION_STANDARD, + self::EDITION_LITE, self::EDITION_PRO, ]; } @@ -94,6 +97,10 @@ public function init(): void $this->_registerProjectConfigEvents(); $this->_registerUserPermissions(); $this->_registerVariables(); + + if ($this->settings->enabledWebCron) { + $this->_registerWebCron(); + } }); } @@ -229,4 +236,11 @@ private function _registerVariables(): void $variable->attachBehavior('schedule', CraftVariableBehavior::class); }); } + + private function _registerWebCron() + { + Event::on(UrlManager::class, UrlManager::EVENT_REGISTER_SITE_URL_RULES, function (RegisterUrlRulesEvent $event) { + $event->rules[$this->settings->endpoint] = 'schedule/web-cron/trigger'; + }); + } } \ No newline at end of file diff --git a/src/Scheduler.php b/src/Scheduler.php index 045d0c6..ceaf152 100644 --- a/src/Scheduler.php +++ b/src/Scheduler.php @@ -87,10 +87,8 @@ public function runSchedule(Schedule $schedule): bool */ public function getTriggerTimers(): array { - $timers = Plugin::getInstance()->timers->getActiveTimers(); - $now = new \DateTime('now', $this->timezone); - return array_filter($timers, static function (TimerInterface $timer) use($now) { - return (new CronExpression($timer->getCronExpression()))->isDue($now); + return array_filter(Plugin::getInstance()->timers->getActiveTimers(), static function (TimerInterface $timer) { + return $timer->isDue(); }); } diff --git a/src/actions/Closure.php b/src/actions/Closure.php new file mode 100644 index 0000000..cabe21a --- /dev/null +++ b/src/actions/Closure.php @@ -0,0 +1,17 @@ +buildCommand(), dirname(Craft::$app->request->getScriptFile()), null, null, $this->timeout ?: null); - - $process->run(function ($type, $buffer) use ($logId) { - $output = $buffer . "\n"; - Craft::$app->getDb()->createCommand() - ->update(Table::SCHEDULELOGS, [ - 'status' => self::STATUS_PROCESSING, - 'output' => new Expression("CONCAT([[output]],:output)", ['output' => $output]), - ], [ - 'id' => $logId, - ]) - ->execute(); - }); - - return $process->isSuccessful(); +// $process = new Process($this->buildCommand(), dirname(Craft::$app->request->getScriptFile()), null, null, $this->timeout ?: null); +// +// $process->run(function ($type, $buffer) use ($logId) { +// $output = $buffer . "\n"; +// Craft::$app->getDb()->createCommand() +// ->update(Table::SCHEDULELOGS, [ +// 'status' => self::STATUS_PROCESSING, +// 'output' => new Expression("CONCAT([[output]],:output)", ['output' => $output]), +// ], [ +// 'id' => $logId, +// ]) +// ->execute(); +// }); +// +// return $process->isSuccessful(); + } + + public function getSettingsHtml(): ?string + { + $suggestions = []; + + $process = new Process([Plugin::getInstance()->getSettings()->getCliPath(), 'craft', 'help/list'], Craft::getAlias('@root')); + $process->run(); + + if ($process->isSuccessful()) { + $lines = explode("\n", mb_convert_encoding($process->getOutput(), mb_internal_encoding())); + + $data = []; + foreach ($lines as $line) { + if (($pos = strpos($line, '/')) === false) { + $data[$line] = []; + continue; + } + + $data[substr($line, 0, $pos)][] = [ + 'name' => $line, + 'hint' => $line, + ]; + } + + foreach ($data as $label => $commandSuggestions) { + $suggestions[] = [ + 'label' => $label, + 'data' => $commandSuggestions, + ]; + } + } + + return Craft::$app->getView()->renderTemplate('schedule/_components/schedules/Console/settings', [ + 'schedule' => $this, + 'suggestions' => $suggestions, + ]); } } \ No newline at end of file diff --git a/src/actions/ElementAction.php b/src/actions/ElementAction.php new file mode 100644 index 0000000..51581d0 --- /dev/null +++ b/src/actions/ElementAction.php @@ -0,0 +1,61 @@ +getElements()->getAllElementTypes(); + + $elementTypeOptions = []; + $allElementActionOptions = []; + $allElementSourceOptions = []; + foreach ($elementTypes as $elementType) { + /** @var ElementInterface|string $elementType */ + $elementTypeOptions[] = ['label' => $elementType::displayName(), 'value' => $elementType]; + foreach ($elementType::actions('*') as $action) { + $allElementActionOptions[$elementType][] = [ + 'label' => $action['label'] ?? $action::displayName(), + 'value' => $action['type'] ?? $action, + ]; + } + + $allElementSourceOptions[$elementType] = []; + foreach($elementType::sources('index') as $source) { + if (isset($source['heading'])) { + continue; + } + $allElementSourceOptions[$elementType][] = [ + 'label' => $source['label'], + 'value' => $source['key'], + 'enabled' => false, + ]; + } + } + + return Craft::$app->getView()->renderTemplate('schedule/_components/actions/ElementAction/settings', [ + 'action' => $this, + 'elementTypeOptions' => $elementTypeOptions, + 'allElementActionOptions' => $allElementActionOptions, + 'allElementSourceOptions' => $allElementSourceOptions, + ]); + } +} \ No newline at end of file diff --git a/src/actions/HttpRequest.php b/src/actions/HttpRequest.php index 9c8d9cc..65d3f2c 100644 --- a/src/actions/HttpRequest.php +++ b/src/actions/HttpRequest.php @@ -2,19 +2,21 @@ namespace panlatent\schedule\actions; +use Alexanderpas\Common\HTTP\Method; +use Craft; use craft\helpers\Json; use GuzzleHttp\Client; +use GuzzleHttp\ClientInterface; use panlatent\craft\actions\abstract\Action; +use panlatent\craft\actions\abstract\ActionInterface; use panlatent\craft\actions\abstract\ContextInterface; use panlatent\craft\actions\abstract\OutputInterface; +use Psr\Container\NotFoundExceptionInterface; use Psr\Http\Message\ResponseInterface; class HttpRequest extends Action { - /** - * @var string - */ - public string $method = 'get'; + public string $method = Method::GET->value; /** * @var string Request URL default is echo api. @@ -43,23 +45,65 @@ class HttpRequest extends Action public function execute(ContextInterface $context): bool { - $client = new Client(); + try { + $client = $context->getContainer()->get(ClientInterface::class); + } catch (NotFoundExceptionInterface) { + $client = new Client(); + } + return $this->executeWithClient($context, $client); + } + public function executeWithClient(ContextInterface $context, ClientInterface $client): bool + { $response = $client->request($this->method, $this->url, $this->getOptions()); $statusCode = $response->getStatusCode(); - $context->setOutput(new class($response) implements OutputInterface { - public function __construct(public ResponseInterface $response) {} - }); +// $context->setOutput(new class($response) implements OutputInterface { +// public function __construct(public ResponseInterface $response) {} +// }); return $statusCode >= 200 && $statusCode < 400; } - protected function getClient(): Client + public function getMethods(): array { - return new Client(); + return [ + Method::GET->value, + Method::HEAD->value, + Method::POST->value, + Method::PUT->value, + Method::PATCH->value, + Method::OPTIONS->value, + ]; } + public function getContentTypes(): array + { + return [ + 'multipart/form-data', + 'application/x-www-form-urlencoded', + 'application/json', + 'application/xml', + 'text/html', + 'text/xml', + ]; + } + + public function getSettingsHtml(): ?string + { + return Craft::$app->getView()->renderTemplate('schedule/_components/actions/HttpRequest/settings', [ + 'action' => $this, + // Craft editableTable not support custom suggestions + // 'httpHeaderSuggestions' => [ + // [ + // 'label' => '', + // 'data' => $this->_getHttpHeaderSuggestions(), + // ] + // ], + ]); + } + + protected function getOptions(): array { $options = []; diff --git a/src/actions/SendEmail.php b/src/actions/SendEmail.php new file mode 100644 index 0000000..67f0b80 --- /dev/null +++ b/src/actions/SendEmail.php @@ -0,0 +1,60 @@ +emails as $email) { + if ($email['name']) { + $emails[App::parseEnv($email['email'])] = $email['name']; + } else { + $emails[] = App::parseEnv($email['email']); + } + } + + $subject = Craft::$app->view->renderString($this->subject, [ + 'action' => $this, + ]); + + $message = Craft::$app->view->renderString($this->message, [ + 'action' => $this, + ]); + + return Craft::$app->mailer + ->compose() + ->setTo($emails) + ->setHtmlBody($message) + ->setSubject($subject) + ->send(); + } + + public function getSettingsHtml(): ?string + { + return Craft::$app->getView()->renderTemplate('schedule/_components/actions/SendEmail/settings', [ + 'action' => $this, + ]); + } +} \ No newline at end of file diff --git a/src/base/Timer.php b/src/base/Timer.php index abc7f18..2f35826 100644 --- a/src/base/Timer.php +++ b/src/base/Timer.php @@ -9,7 +9,6 @@ use Craft; use craft\base\SavableComponent; -use panlatent\schedule\helpers\CronHelper; use panlatent\schedule\models\Schedule; use panlatent\schedule\Plugin; use yii\base\InvalidConfigException; @@ -39,16 +38,6 @@ abstract class Timer extends SavableComponent implements TimerInterface // Public Methods // ========================================================================= - /** - * @return string - */ - public function __toString() - { - return Craft::t('schedule', '# {order}' , [ - 'order' => (int)$this->sortOrder - ]); - } - /** * @return array */ @@ -57,36 +46,16 @@ public function rules(): array $rules = parent::rules(); $rules[] = [['scheduleId', 'enabled'], 'required']; $rules[] = [['scheduleId', 'sortOrder'], 'integer']; - $rules[] = [['minute', 'hour', 'day', 'month', 'week'], 'string']; $rules[] = [['enabled'], 'boolean']; - $rules[] = [['minute', 'hour', 'day', 'month', 'week'], function($property) { - if ($this->$property === null || $this->$property === '') { - $this->$property = '*'; - } - }]; - return $rules; } - public function trigger(): bool - { - return $this->getSchedule()->run(); - } - /** - * @inheritdoc + * @deprecated */ public function isValid(): bool { - return $this->getSchedule()->isValid() && $this->enabled; - } - - /** - * @inheritdoc - */ - public function getCronExpression(): string - { - return sprintf('%s %s %s %s %s', $this->minute, $this->hour, $this->day, $this->month, $this->week); + return true; } /** @@ -113,20 +82,4 @@ public function setSchedule(Schedule $schedule): void { $this->_schedule = $schedule; } - - /** - * @return string - */ - public function getCronDescription(): string - { - return CronHelper::toDescription($this->getCronExpression()); - } - - /** - * @param string $cron - */ - public function setCronExpression(string $cron): void - { - [$this->minute, $this->hour, $this->day, $this->month, $this->week, ] = explode(' ', $cron); - } } \ No newline at end of file diff --git a/src/base/TimerInterface.php b/src/base/TimerInterface.php index d9c2bd2..ff14f46 100644 --- a/src/base/TimerInterface.php +++ b/src/base/TimerInterface.php @@ -22,18 +22,11 @@ interface TimerInterface extends TriggerInterface * @see \panlatent\schedule\services\Timers::getAllTimers() * * @return bool whether to run the timer. + * @deprecated since 1.0.0 */ public function isValid(): bool; - /** - * Returns cron expression. - * - * @return string - */ - public function getCronExpression(): string; + public function isDue(): bool; - /** - * @return Schedule - */ public function getSchedule(): Schedule; } \ No newline at end of file diff --git a/src/base/TimerTrait.php b/src/base/TimerTrait.php index b27052a..faa1747 100644 --- a/src/base/TimerTrait.php +++ b/src/base/TimerTrait.php @@ -15,51 +15,8 @@ */ trait TimerTrait { - // Properties - // ========================================================================= - - /** - * @var int|null - */ public ?int $scheduleId = null; - - /** - * @var string|null - */ - public ?string $minute = null; - - /** - * @var string|null - */ - public ?string $hour = null; - - /** - * @var string|null - */ - public ?string $day = null; - - /** - * @var string|null - */ - public ?string $month = null; - - /** - * @var string|null - */ - public ?string $week = null; - - /** - * @var bool|null - */ public ?bool $enabled = true; - - /** - * @var int|null - */ public ?int $sortOrder = null; - - /** - * @var string|null - */ public ?string $uid = null; } \ No newline at end of file diff --git a/src/builder/Interval.php b/src/builder/Interval.php index 7126be5..e18a4b4 100644 --- a/src/builder/Interval.php +++ b/src/builder/Interval.php @@ -6,8 +6,28 @@ trait Interval { + public function minute(): static + { + return $this; + } + + public function daily(): static + { + return $this; + } + public function hourly(): static { return $this; } + + public function monthly(): static + { + return $this; + } + + public function yearly(): static + { + return $this; + } } \ No newline at end of file diff --git a/src/builder/Schedule.php b/src/builder/Schedule.php index 176756c..8e650f1 100644 --- a/src/builder/Schedule.php +++ b/src/builder/Schedule.php @@ -3,9 +3,11 @@ namespace panlatent\schedule\builder; use panlatent\craft\actions\abstract\ActionInterface; +use panlatent\schedule\actions\Closure; use panlatent\schedule\actions\Command; use panlatent\schedule\actions\Console; use panlatent\schedule\actions\HttpRequest; +use panlatent\schedule\models\Schedule as ScheduleModel; final class Schedule { @@ -16,16 +18,36 @@ public function __construct(protected ActionInterface $action) } - public static function command(string $command, array $arguments = []) + public static function closure(\Closure $closure) + { + $action = new Closure(); + $action->closure = $closure; + return new Schedule($action); + } + + public static function exec(string $command, array $arguments = []) { $action = new Command(); return new Schedule($action); } + public static function console(string $command, array $arguments = []) + { + $action = new Console(); + return new Schedule($action); + } + public static function request(string $url) { $action = new HttpRequest(); $action->url = $url; return new Schedule($action); } + + public function create(): ScheduleModel + { + $schedule = new ScheduleModel(); + $schedule->action = $this->action; + return $schedule; + } } \ No newline at end of file diff --git a/src/console/SchedulesController.php b/src/console/SchedulesController.php index efb2bdb..47f9b1e 100644 --- a/src/console/SchedulesController.php +++ b/src/console/SchedulesController.php @@ -114,6 +114,8 @@ public function actionList(): void $i = 1; $rows = []; + + $ungroupedSchedules = $schedules->getSchedulesByGroupId(); foreach ($ungroupedSchedules as $schedule) { if ($schedule->static) { diff --git a/src/controllers/ActionsController.php b/src/controllers/ActionsController.php new file mode 100644 index 0000000..a5cb6af --- /dev/null +++ b/src/controllers/ActionsController.php @@ -0,0 +1,83 @@ +actions->getActionById($actionId); + + if (!$action) { + throw new NotFoundHttpException('action not found'); + } + } + + $title = trim($action->name) ?: Craft::t('schedule', 'Edit Action'); + } else { + if ($action === null) { + $action = new HttpRequest(); + } + + $title = Craft::t('schedule', 'Create a new action'); + } + + $allActionTypes = Plugin::getInstance()->actions->getAllActionTypes(); + $actionTypeOptions = []; + foreach ($allActionTypes as $class) { + $actionTypeOptions[] = [ + 'label' => $class::displayName(), + 'value' => $class, + ]; + } + + return $this->asCpScreen() + ->title($title) + ->addCrumb(Craft::t('app', 'Settings'), 'settings') + ->addCrumb(Craft::t('app', 'Entry Types'), 'settings/entry-types') + ->action('schedule/actions/save') + ->redirectUrl('schedule/actions') + ->addAltAction(Craft::t('app', 'Save and continue editing'), [ + 'redirect' => 'settings/entry-types/{id}', + 'shortcut' => true, + 'retainScroll' => true, + ]) + ->contentTemplate('schedule/actions/_edit.twig', [ + 'actionId' => $actionId, + 'action' => $action, + 'typeName' => $action::displayName(), + 'actionTypes' => $allActionTypes, + 'actionTypeOptions' => $actionTypeOptions, + ]); + } + + + public function actionRenderSettings(): Response + { + $this->requirePostRequest(); + $this->requireAcceptsJson(); + + $type = $this->request->getRequiredBodyParam('type'); + $action = Plugin::getInstance()->actions->createAction($type); + + $view = Craft::$app->getView(); + $html = $action->getSettingsHtml(); + + return $this->asJson([ + 'settingsHtml' => $html, + 'headHtml' => $view->getHeadHtml(), + 'bodyHtml' => $view->getBodyHtml(), + ]); + } +} \ No newline at end of file diff --git a/src/controllers/SchedulesController.php b/src/controllers/SchedulesController.php index d2568b9..41341e7 100644 --- a/src/controllers/SchedulesController.php +++ b/src/controllers/SchedulesController.php @@ -10,11 +10,11 @@ use Craft; use craft\helpers\Json; use craft\web\Controller; -use panlatent\schedule\base\Schedule; -use panlatent\schedule\base\ScheduleInterface; +use panlatent\schedule\actions\HttpRequest; +use panlatent\schedule\models\Schedule; use panlatent\schedule\models\ScheduleGroup; use panlatent\schedule\Plugin; -use panlatent\schedule\schedules\HttpRequest; +use panlatent\schedule\timers\Cron; use yii\web\NotFoundHttpException; use yii\web\Response; @@ -39,7 +39,7 @@ public function actionSaveGroup(): Response $this->requirePostRequest(); $this->requireAcceptsJson(); - $schedules = Plugin::$plugin->getSchedules(); + $schedules = Plugin::getInstance()->schedules; $groupId = Craft::$app->getRequest()->getBodyParam('id'); $groupName = Craft::$app->getRequest()->getBodyParam('name'); @@ -74,7 +74,7 @@ public function actionDeleteGroup(): Response $this->requirePostRequest(); $this->requireAcceptsJson(); - $schedules = Plugin::$plugin->getSchedules(); + $schedules = Plugin::getInstance()->schedules; $groupId = Craft::$app->getRequest()->getBodyParam('id'); $group = $schedules->getGroupById($groupId); @@ -95,14 +95,13 @@ public function actionDeleteGroup(): Response * Edit a schedule. * * @param int|null $scheduleId - * @param ScheduleInterface|null $schedule + * @param Schedule|null $schedule * @return Response */ - public function actionEditSchedule(int $scheduleId = null, ScheduleInterface $schedule = null): Response + public function actionEditSchedule(int $scheduleId = null, Schedule $schedule = null): Response { - $schedules = Plugin::$plugin->getSchedules(); + $schedules = Plugin::getInstance()->schedules; - /** @var Schedule $schedule */ if ($schedule === null) { if ($scheduleId !== null) { $schedule = $schedules->getScheduleById($scheduleId); @@ -110,14 +109,17 @@ public function actionEditSchedule(int $scheduleId = null, ScheduleInterface $sc throw new NotFoundHttpException(); } } else { - $schedule = $schedules->createSchedule(HttpRequest::class); + $schedule = new Schedule(); + $schedule->timer = new Cron(); + $schedule->action = new HttpRequest(); } } - $isNewSchedule = $schedule->getIsNew(); + $isNewSchedule = !$schedule; $allGroups = $schedules->getAllGroups(); - $allScheduleTypes = $schedules->getAllScheduleTypes(); + $allActionTypes = Plugin::getInstance()->actions->getAllActionTypes(); + $allTimerTypes = Plugin::getInstance()->timers->getAllTimerTypes(); $groupOptions = [ [ @@ -133,51 +135,14 @@ public function actionEditSchedule(int $scheduleId = null, ScheduleInterface $sc ]; } - $scheduleInstances = []; - $scheduleTypeOptions = []; - foreach ($allScheduleTypes as $class) { - /** @var ScheduleInterface|string $class */ - $scheduleInstances[$class] = new $class(); - $scheduleTypeOptions[] = [ - 'label' => $class::displayName(), - 'value' => $class, - ]; - } - return $this->renderTemplate('schedule/_edit', [ 'isNewSchedule' => $isNewSchedule, 'groupOptions' => $groupOptions, 'schedule' => $schedule, - 'scheduleInstances' => $scheduleInstances, - 'scheduleTypes' => $allScheduleTypes, - 'scheduleTypeOptions' => $scheduleTypeOptions, - ]); - } - - /** - * @param string $scheduleType - * @return Response - */ - public function actionGetScheduleSettingsHtml(string $scheduleType): Response - { - if (!class_exists($scheduleType)) { - return $this->asFailure("schedule class $scheduleType not found"); - } - - /** @var Schedule $schedule */ - $schedule = new $scheduleType; - - $view = Craft::$app->getView(); - $view->startJsBuffer(); - $view->startCssBuffer(); - $html =$schedule->getSettingsHtml(); - $js = $view->clearJsBuffer(); - $css = $view->clearCssBuffer(); - - return $this->asJson([ - 'html' => $html, - 'js' => $js, - 'css' => $css, + 'actionTypes' => $allActionTypes, + 'actionTypeOptions' => array_map(static fn($class) => ['label' => $class::displayName(), 'value' => $class], $allActionTypes), + 'timerTypes' => $allTimerTypes, + 'timerTypeOptions' => array_map(static fn($class) => ['label' => $class::displayName(), 'value' => $class], $allTimerTypes), ]); } @@ -190,9 +155,8 @@ public function actionSaveSchedule(): ?Response { $this->requirePostRequest(); - $schedules = Plugin::$plugin->getSchedules(); + $schedules = Plugin::getInstance()->schedules; - /** @var Schedule $schedule */ $schedule = $schedules->createScheduleFromRequest(); if (!$schedules->saveSchedule($schedule)) { diff --git a/src/controllers/TimersController.php b/src/controllers/TimersController.php index 859ca4f..0797210 100644 --- a/src/controllers/TimersController.php +++ b/src/controllers/TimersController.php @@ -24,6 +24,7 @@ * * @package panlatent\schedule\controllers * @author Panlatent + * @deprecated since 1.0 */ class TimersController extends Controller { @@ -185,4 +186,22 @@ public function actionReorderTimers(): Response 'success' => Plugin::$plugin->getTimers()->reorderTimers($ids) ]); } + + public function actionRenderSettings(): Response + { + $this->requirePostRequest(); + $this->requireAcceptsJson(); + + $type = $this->request->getRequiredBodyParam('type'); + $action = Plugin::getInstance()->timers->createTimer($type); + + $view = Craft::$app->getView(); + $html = $action->getSettingsHtml(); + + return $this->asJson([ + 'settingsHtml' => $html, + 'headHtml' => $view->getHeadHtml(), + 'bodyHtml' => $view->getBodyHtml(), + ]); + } } \ No newline at end of file diff --git a/src/controllers/WebCronController.php b/src/controllers/WebCronController.php new file mode 100644 index 0000000..4af9eb6 --- /dev/null +++ b/src/controllers/WebCronController.php @@ -0,0 +1,34 @@ +requireSiteRequest(); + + $token = ''; + if ($this->request->isGet) { + $token = $this->request->getQueryParam('token'); + } else { + $token = $this->request->getBodyParam('token'); + } + + if ($token !== Plugin::getInstance()->settings->token) { + throw new ForbiddenHttpException(); + } + + $scheduler = new Scheduler(); + $scheduler->maxConcurrent = 0; + $scheduler->dispatch(); + + return null; + } +} \ No newline at end of file diff --git a/src/di/ContainerAdapter.php b/src/di/ContainerAdapter.php new file mode 100644 index 0000000..bbba111 --- /dev/null +++ b/src/di/ContainerAdapter.php @@ -0,0 +1,24 @@ +container->get($id); + } + + public function has(string $id): bool + { + return $this->container->has($id); + } +} \ No newline at end of file diff --git a/src/events/ActionEvent.php b/src/events/ActionEvent.php new file mode 100644 index 0000000..045a28e --- /dev/null +++ b/src/events/ActionEvent.php @@ -0,0 +1,11 @@ + + * @deprecated since 1.0 */ class ScheduleBuildEvent extends Event { diff --git a/src/events/ScheduleEvent.php b/src/events/ScheduleEvent.php index bb1c5f5..9775e2c 100644 --- a/src/events/ScheduleEvent.php +++ b/src/events/ScheduleEvent.php @@ -7,8 +7,8 @@ namespace panlatent\schedule\events; -use panlatent\schedule\base\ScheduleInterface; -use yii\base\Event; +use craft\events\ModelEvent; +use panlatent\schedule\models\Schedule; /** * Class ScheduleEvent @@ -16,15 +16,7 @@ * @package panlatent\schedule\events * @author Panlatent */ -class ScheduleEvent extends Event +class ScheduleEvent extends ModelEvent { - /** - * @var ScheduleInterface|null - */ - public ?ScheduleInterface $schedule = null; - - /** - * @var bool - */ - public bool $isNew = false; + public ?Schedule $schedule = null; } \ No newline at end of file diff --git a/src/events/ScheduleGroupEvent.php b/src/events/ScheduleGroupEvent.php index a66feed..fabe052 100644 --- a/src/events/ScheduleGroupEvent.php +++ b/src/events/ScheduleGroupEvent.php @@ -7,8 +7,8 @@ namespace panlatent\schedule\events; +use craft\events\ModelEvent; use panlatent\schedule\models\ScheduleGroup; -use yii\base\Event; /** * Class ScheduleGroupEvent @@ -16,15 +16,10 @@ * @package panlatent\schedule\events * @author Panlatent */ -class ScheduleGroupEvent extends Event +class ScheduleGroupEvent extends ModelEvent { /** * @var ScheduleGroup|null */ public ?ScheduleGroup $group = null; - - /** - * @var bool - */ - public bool $isNew = false; } \ No newline at end of file diff --git a/src/events/TimerEvent.php b/src/events/TimerEvent.php index 0e64248..c71dbc6 100644 --- a/src/events/TimerEvent.php +++ b/src/events/TimerEvent.php @@ -9,8 +9,8 @@ namespace panlatent\schedule\events; +use craft\events\ModelEvent; use panlatent\schedule\base\TimerInterface; -use yii\base\Event; /** * Class TimerEvent @@ -18,15 +18,10 @@ * @package panlatent\schedule\events * @author Panlatent */ -class TimerEvent extends Event +class TimerEvent extends ModelEvent { /** * @var TimerInterface|null */ public ?TimerInterface $timer = null; - - /** - * @var bool - */ - public bool $isNew = false; } \ No newline at end of file diff --git a/src/fields/Action.php b/src/fields/Action.php new file mode 100644 index 0000000..1354079 --- /dev/null +++ b/src/fields/Action.php @@ -0,0 +1,10 @@ +getDescription(); } + + public function nextTime(string $expression, int $total = 5): int + { + $cron = new \Cron\CronExpression($expression); + return $cron->getNextRunDate($time)->getTimeStamp(); + } } \ No newline at end of file diff --git a/src/icon-mask.svg b/src/icon-mask.svg index 67431e9..5a49a65 100644 --- a/src/icon-mask.svg +++ b/src/icon-mask.svg @@ -1,13 +1,6 @@ - - - - - \ No newline at end of file + + + + + + diff --git a/src/icon.svg b/src/icon.svg index 67431e9..b77820f 100644 --- a/src/icon.svg +++ b/src/icon.svg @@ -1,13 +1,5 @@ - - - - - \ No newline at end of file + + + + + diff --git a/src/migrations/Install.php b/src/migrations/Install.php index d1cac1d..91b18d9 100644 --- a/src/migrations/Install.php +++ b/src/migrations/Install.php @@ -20,58 +20,70 @@ class Install extends Migration /** * @inheritdoc */ - public function safeUp(): void + public function safeUp(): bool { - // Schedule Groups - $this->createTable('{{%schedulegroups}}', [ + $this->createTable('{{%schedule_actions}}', [ 'id' => $this->primaryKey(), - 'name' => $this->string()->notNull(), + 'type' => $this->string()->notNull(), + 'settings' => $this->text(), 'dateCreated' => $this->dateTime()->notNull(), 'dateUpdated' => $this->dateTime()->notNull(), 'uid' => $this->uid(), ]); - $this->createIndex(null, '{{%schedulegroups}}', ['name'], true); + $this->createTable('{{%schedule_schedulegroups}}', [ + 'id' => $this->primaryKey(), + 'name' => $this->string()->notNull(), + 'static' => $this->boolean()->defaultValue(false), + 'dateCreated' => $this->dateTime()->notNull(), + 'dateUpdated' => $this->dateTime()->notNull(), + 'uid' => $this->uid(), + ]); + $this->createIndex(null, '{{%schedule_schedulegroups}}', ['name'], true); - // Schedules - $this->createTable('{{%schedules}}', [ + $this->createTable('{{%schedule_schedules}}', [ 'id' => $this->primaryKey(), 'groupId' => $this->integer(), 'name' => $this->string()->notNull(), 'handle' => $this->string()->notNull(), 'description' => $this->string(), - 'type' => $this->string()->notNull(), - 'user' => $this->string(), - 'settings' => $this->text(), - 'static' => $this->boolean()->defaultValue(false), + // 'static' => $this->boolean()->defaultValue(false), + 'actionId' => $this->integer()->notNull(), + 'onSuccess' => $this->integer(), + 'onFailed' => $this->integer(), + //'enabledLog' => $this->boolean()->defaultValue(false), 'enabled' => $this->boolean()->notNull()->defaultValue(true), - 'enabledLog' => $this->boolean()->defaultValue(false), - 'lastStartedTime' => $this->bigInteger(), - 'lastFinishedTime' => $this->bigInteger(), - 'lastStatus' => $this->boolean(), 'sortOrder' => $this->smallInteger()->unsigned(), 'dateCreated' => $this->dateTime()->notNull(), 'dateUpdated' => $this->dateTime()->notNull(), 'uid' => $this->uid(), ]); + $this->createIndex(null, '{{%schedule_schedules}}', 'groupId'); + $this->createIndex(null, '{{%schedule_schedules}}', 'handle', true); + $this->createIndex(null, '{{%schedule_schedules}}', 'dateCreated'); + $this->createIndex(null, '{{%schedule_schedules}}', ['sortOrder', 'dateCreated']); + $this->addForeignKey(null, '{{%schedule_schedules}}', 'actionId', '{{%schedule_actions}}', 'id'); + $this->addForeignKey(null, '{{%schedule_schedules}}', 'onSuccess', '{{%schedule_actions}}', 'id', 'SET NULL'); + $this->addForeignKey(null, '{{%schedule_schedules}}', 'onFailed', '{{%schedule_actions}}', 'id', 'SET NULL'); - $this->createIndex(null, '{{%schedules}}', 'groupId'); - $this->createIndex(null, '{{%schedules}}', 'handle', true); - $this->createIndex(null, '{{%schedules}}', 'type'); - $this->createIndex(null, '{{%schedules}}', 'dateCreated'); - $this->createIndex(null, '{{%schedules}}', ['sortOrder', 'dateCreated']); - $this->addForeignKey(null, '{{%schedules}}', 'groupId', '{{%schedulegroups}}', 'id', 'SET NULL'); + $this->createTable('{{%schedule_scheduletraces}}', [ + 'id' => $this->primaryKey(), + 'scheduleId' => $this->integer()->notNull(), + 'traces' => $this->text(), +// 'firstStartedTime' => $this->bigInteger(), +// 'lastStartedTime' => $this->bigInteger(), +// 'lastFinishedTime' => $this->bigInteger(), +// 'lastStatus' => $this->boolean(), + 'dateCreated' => $this->dateTime()->notNull(), + 'dateUpdated' => $this->dateTime()->notNull(), + 'uid' => $this->uid(), + ]); + $this->addForeignKey(null, '{{%schedule_scheduletraces}}', 'scheduleId', '{{%schedule_schedules}}', 'id', 'CASCADE'); - // Schedule Timers - $this->createTable('{{%scheduletimers}}', [ + $this->createTable('{{%schedule_timers}}', [ 'id' => $this->primaryKey(), 'scheduleId' => $this->integer()->notNull(), 'type' => $this->string()->notNull(), - 'minute' => $this->string()->notNull()->defaultValue('*'), - 'hour' => $this->string()->notNull()->defaultValue('*'), - 'day' => $this->string()->notNull()->defaultValue('*'), - 'month' => $this->string()->notNull()->defaultValue('*'), - 'week' => $this->string()->notNull()->defaultValue('*'), 'settings' => $this->text(), 'enabled' => $this->boolean()->notNull()->defaultValue(true), 'sortOrder' => $this->smallInteger()->defaultValue(0), @@ -80,14 +92,7 @@ public function safeUp(): void 'uid' => $this->uid(), ]); - $this->createIndex(null, '{{%scheduletimers}}', 'scheduleId'); - $this->createIndex(null, '{{%scheduletimers}}', 'type'); - $this->createIndex(null, '{{%scheduletimers}}', ['enabled', 'dateCreated']); - $this->createIndex(null, '{{%scheduletimers}}', ['scheduleId', 'sortOrder']); - $this->addForeignKey(null, '{{%scheduletimers}}', 'scheduleId', '{{%schedules}}', 'id', 'CASCADE'); - - // Schedule Logs - $this->createTable('{{%schedulelogs}}', [ + $this->createTable('{{%schedule_tasks}}', [ 'id' => $this->primaryKey(), 'scheduleId' => $this->integer()->notNull(), 'status' => $this->string()->notNull(), @@ -102,10 +107,21 @@ public function safeUp(): void 'dateUpdated' => $this->dateTime()->notNull(), 'uid' => $this->uid(), ]); + $this->createIndex(null, '{{%schedule_tasks}}', 'scheduleId'); + $this->createIndex(null, '{{%schedule_tasks}}', ['scheduleId', 'sortOrder']); + $this->addForeignKey(null, '{{%schedule_tasks}}', 'scheduleId', '{{%schedule_schedules}}', 'id', 'CASCADE'); + + $this->createTable('{{%schedule_scheduler}}', [ + 'id' => $this->primaryKey(), + 'instanceId' => $this->string()->notNull(), + 'key' => $this->string()->notNull(), + 'value' => $this->text(), + 'dateCreated' => $this->dateTime()->notNull(), + 'dateUpdated' => $this->dateTime()->notNull(), + 'uid' => $this->uid(), + ]); - $this->createIndex(null, '{{%schedulelogs}}', 'scheduleId'); - $this->createIndex(null, '{{%schedulelogs}}', ['scheduleId', 'sortOrder']); - $this->addForeignKey(null, '{{%schedulelogs}}', 'scheduleId', '{{%schedules}}', 'id', 'CASCADE'); + return true; } /** @@ -113,9 +129,12 @@ public function safeUp(): void */ public function safeDown(): void { - $this->dropTableIfExists('{{%schedulelogs}}'); - $this->dropTableIfExists('{{%scheduletimers}}'); - $this->dropTableIfExists('{{%schedules}}'); - $this->dropTableIfExists('{{%schedulegroups}}'); + $this->dropTableIfExists('{{%schedule_scheduler}}'); + $this->dropTableIfExists('{{%schedule_tasks}}'); + $this->dropTableIfExists('{{%schedule_timers}}'); + $this->dropTableIfExists('{{%schedule_scheduletraces}}'); + $this->dropTableIfExists('{{%schedule_schedules}}'); + $this->dropTableIfExists('{{%schedule_schedulegroups}}'); + $this->dropTableIfExists('{{%schedule_actions}}'); } } \ No newline at end of file diff --git a/src/models/Context.php b/src/models/Context.php index 6300e6f..b8f826a 100644 --- a/src/models/Context.php +++ b/src/models/Context.php @@ -5,19 +5,26 @@ use panlatent\craft\actions\abstract\ContextInterface; use panlatent\craft\actions\abstract\InputInterface; use panlatent\craft\actions\abstract\OutputInterface; +use panlatent\schedule\di\ContainerAdapter; +use Psr\Container\ContainerInterface; use Psr\Log\LoggerInterface; -use yii\base\Component; +use yii\di\Container; -class Context extends Component implements ContextInterface +class Context implements ContextInterface { - public function __construct(protected LoggerInterface $logger, $config = []) + public function __construct(protected readonly LoggerInterface $logger, protected readonly ?ContainerInterface $container = null) { - parent::__construct($config); + } - public function getErrors(): array + public function getContainer(): ContainerInterface { + return $this->container ?? new ContainerAdapter(new Container()); + } + public function getErrors(): array + { + return []; } public function getLogger(): LoggerInterface diff --git a/src/models/Schedule.php b/src/models/Schedule.php index 38a8ac1..a41675f 100644 --- a/src/models/Schedule.php +++ b/src/models/Schedule.php @@ -4,6 +4,7 @@ use Craft; use craft\base\Model; +use DateTime; use panlatent\craft\actions\abstract\ActionInterface; use panlatent\schedule\base\TimerInterface; use panlatent\schedule\log\LogAdapter; @@ -11,31 +12,15 @@ /** * @property-read ScheduleGroup $group - * @property-read TimerInterface[] $timers - * @property-read ActionInterface[] $actions + * @property-read ScheduleInfo $info * @since 1.0.0 */ class Schedule extends Model { public ?int $id = null; - /** - * @var int|null - */ public ?int $groupId = null; - - /** - * @var string|null - */ public ?string $name = null; - - /** - * @var string|null - */ public ?string $handle = null; - - /** - * @var string|null - */ public ?string $description = null; /** @@ -46,6 +31,8 @@ class Schedule extends Model public ?ActionInterface $action = null; + public ?TimerInterface $timer = null; + /** * @var bool */ @@ -56,20 +43,7 @@ class Schedule extends Model */ public ?bool $enabledLog = null; - /** - * @var int|null - */ - public ?int $lastStartedTime = null; - - /** - * @var int|null - */ - public ?int $lastFinishedTime = null; - - /** - * @var string|null - */ - public ?string $lastStatus = null; +// public $timeout; /** * @var int|null @@ -78,13 +52,11 @@ class Schedule extends Model public ?string $uid = null; - /** - * @return TimerInterface[] - */ - public function getTimers(): array - { - return []; - } + public ?DateTime $dateCreated = null; + + public ?DateTime $dateUpdated = null; + + private ?ScheduleInfo $_info = null; /** * @return array @@ -95,6 +67,14 @@ public function getConditions(): array return []; } + public function getInfo(): ScheduleInfo + { + if ($this->_info === null) { + $this->_info = new ScheduleInfo(); + } + return $this->_info; + } + public function canRun(): bool { return true; diff --git a/src/models/ScheduleInfo.php b/src/models/ScheduleInfo.php new file mode 100644 index 0000000..de3ba89 --- /dev/null +++ b/src/models/ScheduleInfo.php @@ -0,0 +1,11 @@ +customCpNavName); } + + public function getSchedules(): array + { + return array_map(function($item) { + if ($item instanceof Schedule) { + return $item; + } + + if ($item instanceof ScheduleBuilder) { + return $item->create(); + } + + throw new InvalidConfigException(); + }, $this->schedules); + } } diff --git a/src/records/Action.php b/src/records/Action.php new file mode 100644 index 0000000..5cb02b6 --- /dev/null +++ b/src/records/Action.php @@ -0,0 +1,10 @@ + */ diff --git a/src/services/Actions.php b/src/services/Actions.php new file mode 100644 index 0000000..ebcc55e --- /dev/null +++ b/src/services/Actions.php @@ -0,0 +1,194 @@ + [ + Command::class, + Console::class, + ElementAction::class, + HttpRequest::class, + SendEmail::class, + ] + ]); + + $this->trigger(self::EVENT_REGISTER_ACTION_TYPES, $event); + + return $event->types; + } + + /** + * @return ActionInterface[] + */ + public function getAllActions(): array + { + if ($this->actions === null) { + $this->actions = []; + $rows = $this->createQuery()->all(); + foreach ($rows as $row) { + $action = $this->createAction($row); + $this->actions[] = $action; + } + } + + return $this->actions; + } + + public function getActionById(int $actionId) + { + return ArrayHelper::firstWhere($this->getAllActions(), 'id', $actionId); + } + + public function runAction(ActionInterface $action): bool + { + $context = new Context(new LogAdapter(Craft::$app->getLog()->getLogger(), 'action')); + + + return $action->execute($context); + } + + public function createAction(mixed $config): ActionInterface + { + try { + return ComponentHelper::createComponent($config, ActionInterface::class); + } catch (MissingComponentException $exception) { + + } + } + + public function saveAction(ActionInterface $action, bool $runValidation = true): bool + { + $isNew = $action->getIsNew(); + + if ($this->hasEventHandlers(self::EVENT_BEFORE_SAVE_ACTION)) { + $this->trigger(self::EVENT_BEFORE_SAVE_ACTION, new ActionEvent([ + 'action' => $action, + 'isNew' => $isNew, + ])); + } + + if (!$action->beforeSave($isNew)) { + return false; + } + + if ($isNew) { + $action->uid = StringHelper::UUID(); + } elseif ($action->uid === null) { + $action->uid = Db::uidById(Table::ACTIONS, $action->id); + } + + if ($runValidation && !$action->validate()) { + Craft::info('Action not saved due to validation error.', __METHOD__); + return false; + } + + $transaction = Craft::$app->getDb()->beginTransaction(); + try { + Craft::$app->getDb() + ->createCommand() + ->upsert(Table::ACTIONS, [ + 'type' => get_class($action), + 'settings' => $action->getSettings(), + 'dateUpdated' => $action->dateUpdated, + 'dateCreated' => $action->dateCreated, + 'uid' => $action->uid, + ], [ + 'type' => get_class($action), + 'settings' => $action->getSettings(), + 'dateUpdated' => $action->dateUpdated, + ]) + ->execute(); + + if ($isNew) { + $action->id = $record->id; + } + + $transaction->commit(); + } catch (\Throwable $exception) { + $transaction->rollBack(); + + throw $exception; + } + + // If has some respositories ... + // $this->__METHOD__[$action->id] = $action; + // $this->__METHOD__[$action->handle] = $action; + + if ($this->hasEventHandlers(self::EVENT_AFTER_SAVE_ACTION)) { + $this->trigger(self::EVENT_AFTER_SAVE_ACTION, new ActionEvent([ + 'action' => $action, + 'isNew' => $isNew, + ])); + } + + $action->afterSave($isNew); + + return true; + } + + private function createQuery(): Query + { + return (new Query()) + ->select([ + 'actions.id', + 'actions.type', + 'actions.settings', + 'actions.dateCreated', + 'actions.dateUpdated', + 'actions.uid', + ]) + ->from(['actions' => Table::ACTIONS]); + } +} \ No newline at end of file diff --git a/src/services/ScheduleGroups.php b/src/services/ScheduleGroups.php deleted file mode 100644 index 6d4ddca..0000000 --- a/src/services/ScheduleGroups.php +++ /dev/null @@ -1,8 +0,0 @@ -getRequest(); } - $type = $request->getBodyParam('type'); + $actionType = $request->getRequiredBodyParam('actionType'); + $actionConfig = $request->getBodyParam('actionTypes.' . $actionType) ?? []; + $action = Plugin::getInstance()->actions->createAction(['type' => $actionType] + $actionConfig); + + $timerType = $request->getBodyParam('timerType'); + $timerConfig = $request->getBodyParam('timeTypes.' . $timerType) ?? []; + $timer = Plugin::getInstance()->timers->createTimer(['type' => $timerType] + $timerConfig); return $this->createSchedule([ 'id' => $request->getBodyParam('scheduleId'), @@ -427,9 +433,9 @@ public function createScheduleFromRequest(Request $request = null): Schedule 'name' => $request->getBodyParam('name'), 'handle' => $request->getBodyParam('handle'), 'description' => $request->getBodyParam('description'), - 'type' => $type, - 'settings' => $request->getBodyParam('types.' . $type, []), - 'static' => (bool)$request->getBodyParam('static'), + //'static' => (bool)$request->getBodyParam('static'), + 'timer' => $timer, + 'action' => $action, 'enabled' => (bool)$request->getBodyParam('enabled'), 'enabledLog' => $request->getBodyParam('enabledLog'), ]); @@ -438,6 +444,7 @@ public function createScheduleFromRequest(Request $request = null): Schedule /** * @param mixed $config * @return Schedule + * @internal */ public function createSchedule(mixed $config): Schedule { @@ -451,8 +458,7 @@ public function createSchedule(mixed $config): Schedule */ public function saveSchedule(Schedule $schedule, bool $runValidation = true): bool { - /** @var Schedule $schedule */ - $isNewSchedule = $schedule->getIsNew(); + $isNewSchedule = !$schedule->id; if ($this->hasEventHandlers(self::EVENT_BEFORE_SAVE_SCHEDULE)) { $this->trigger(self::EVENT_BEFORE_SAVE_SCHEDULE, new ScheduleEvent([ @@ -461,10 +467,6 @@ public function saveSchedule(Schedule $schedule, bool $runValidation = true): bo ])); } - if (!$schedule->beforeSave($isNewSchedule)) { - return false; - } - if ($isNewSchedule) { $schedule->uid = StringHelper::UUID(); } elseif ($schedule->uid === null) { @@ -506,14 +508,15 @@ public function saveSchedule(Schedule $schedule, bool $runValidation = true): bo $record = new ScheduleRecord(); } + Plugin::getInstance()->actions->saveAction($schedule->action); + $record->groupId = $schedule->groupId; $record->name = $schedule->name; $record->handle = $schedule->handle; $record->description = $schedule->description; - $record->type = get_class($schedule); - $record->user = $schedule->user; - $record->settings = Json::encode($schedule->getSettings()); - $record->static = false; + $record->actionId = $schedule->action->id; + $record->onSuccess = null; + $record->onFailed = null; $record->enabled = (bool)$schedule->enabled; $record->enabledLog = (bool)$schedule->enabledLog; $record->save(false); @@ -540,8 +543,6 @@ public function saveSchedule(Schedule $schedule, bool $runValidation = true): bo $this->_schedules[] = $schedule; } - $schedule->afterSave($isNewSchedule); - if ($this->hasEventHandlers(self::EVENT_AFTER_SAVE_SCHEDULE)) { $this->trigger(self::EVENT_AFTER_SAVE_SCHEDULE, new ScheduleEvent([ 'schedule' => $schedule, @@ -729,9 +730,10 @@ private function _createScheduleQuery(): Query 'schedules.name', 'schedules.handle', 'schedules.description', - //'schedules.type', //'schedules.user', - //'schedules.settings', + 'schedules.actionId', + 'schedules.onSuccess', + 'schedules.onFailed', //'schedules.static', 'schedules.enabled', //'schedules.enabledLog', @@ -743,7 +745,7 @@ private function _createScheduleQuery(): Query 'schedules.uid', ]) ->from(['schedules' => Table::SCHEDULES]) - ->innerJoin(['actions' => Table::ACTIONS], '[[schedules.actionId]] = [[actions.id]]') + //->innerJoin(['actions' => Table::ACTIONS], '[[schedules.actionId]] = [[actions.id]]') ->orderBy('schedules.sortOrder'); } diff --git a/src/services/Timers.php b/src/services/Timers.php index 1a93470..10540c5 100644 --- a/src/services/Timers.php +++ b/src/services/Timers.php @@ -23,9 +23,7 @@ use panlatent\schedule\events\ScheduleEvent; use panlatent\schedule\events\TimerEvent; use panlatent\schedule\records\Timer as TimerRecord; -use panlatent\schedule\timers\Custom; -use panlatent\schedule\timers\DateTime; -use panlatent\schedule\timers\Every; +use panlatent\schedule\timers\Cron; use panlatent\schedule\timers\MissingTimer; use panlatent\schedule\timers\Relay; use Throwable; @@ -90,9 +88,7 @@ class Timers extends Component public function getAllTimerTypes(): array { $types = [ - Custom::class, - DateTime::class, - Every::class, + Cron::class, Relay::class, ]; diff --git a/src/templates/_components/actions/Console/settings.twig b/src/templates/_components/actions/Console/settings.twig new file mode 100644 index 0000000..8f06f2d --- /dev/null +++ b/src/templates/_components/actions/Console/settings.twig @@ -0,0 +1,30 @@ +{% import "_includes/forms" as forms %} + +{% set scheduleType = className(schedule) %} + +{{ forms.autosuggestField({ + label: 'Command'|t('schedule'), + required: true, + id: 'command', + name: 'command', + value: schedule.command, + errors: schedule.getErrors('command'), + suggestions: suggestions, +}) }} + +{{ forms.textField({ + label: 'Arguments'|t('schedule'), + id: 'arguments', + name: 'arguments', + value: schedule.arguments, + errors: schedule.getErrors('arguments'), +}) }} + +{{ forms.textField({ + label: 'Timeout'|t('schedule'), + id: 'timeout', + name: 'timeout', + value: schedule.timeout ?: '', + size: 5, + errors: schedule.getErrors('timeout'), +}) }} diff --git a/src/templates/_components/actions/ElementAction/settings.twig b/src/templates/_components/actions/ElementAction/settings.twig new file mode 100644 index 0000000..fff8769 --- /dev/null +++ b/src/templates/_components/actions/ElementAction/settings.twig @@ -0,0 +1,63 @@ +{% import "_includes/forms" as forms %} + +{{ forms.selectField({ + first: true, + label: "Element Type"|t("schedule"), + instructions: "", + id: "elementType", + name: "elementType", + value: action.elementType, + options: elementTypeOptions, + required: true, + errors: action.getErrors("elementType"), + toggle: true, +}) }} + + {% for elementType, elementActionOptions in allElementActionOptions %} + {% set isCurrent = (elementType == action.elementType) %} + + {% endfor %} + + diff --git a/src/templates/_components/actions/HttpRequest/settings.twig b/src/templates/_components/actions/HttpRequest/settings.twig new file mode 100644 index 0000000..c5286b9 --- /dev/null +++ b/src/templates/_components/actions/HttpRequest/settings.twig @@ -0,0 +1,146 @@ +{% import "_includes/forms" as forms %} + +{% set actionType = className(action) %} + +{% set methodOptions = [] %} +{% for method in action.methods %} + {% set methodOptions = methodOptions|merge([{ + label: method, + value: method, + }]) %} +{% endfor %} + +{{ forms.selectField({ + label: "HTTP Method"|t('action'), + required: true, + id: 'method', + name: 'method', + value: action.method, + options: methodOptions, + errors: action.getErrors('method'), +}) }} + +{{ forms.autosuggestField({ + label: 'Request URL'|t('schedule'), + requied: true, + id: 'url', + name: 'url', + value: action.url, + errors: action.getErrors('url'), + suggestEnvVars: true, +}) }} + +{{ forms.editableTableField({ + label: "Headers"|t("schedule"), + id: "headers", + name: "headers", + addRowLabel: "Add a header"|t("schedule"), + cols: { + enabled: { + type: 'checkbox', + thin: true, + checked: true, + }, + name: { + type: 'singleline', + heading: "Header Name"|t("schedule"), + placeholder: "Add Header Name"|t("schedule"), + code: true, + }, + value: { + type: 'singleline', + heading: "Header Value"|t("schedule"), + placeholder: "Add Header Value"|t("schedule"), + code: true, + } + }, + rows: action.headers, + defaultValues: { + enabled: true, + }, + allowAdd: true, + allowReorder: true, + allowDelete: true, + errors: action.getErrors("headers"), +}) }} + +{{ forms.editableTableField({ + label: "Query Params"|t("schedule"), + id: "queryParams", + name: "queryParams", + addRowLabel: "Add a parameter"|t("schedule"), + cols: { + enabled: { + type: 'checkbox', + thin: true, + }, + name: { + type: 'singleline', + heading: "Parameter"|t("schedule"), + placeholder: "Add URL Parameter"|t("schedule"), + code: true, + }, + value: { + type: 'singleline', + heading: "Value"|t("schedule"), + placeholder: "Add Value"|t("schedule"), + code: true, + }, + }, + rows: action.queryParams, + defaultValues: { + enabled: true, + }, + allowAdd: true, + allowReorder: true, + allowDelete: true, + errors: action.getErrors("queryParams"), +}) }} + +{% set contentTypeSuggestions = [] %} +{% for contentType in action.contentTypes %} + {% set contentTypeSuggestions = contentTypeSuggestions|merge([ + { + name: contentType, + hint: contentType, + } + ]) %} +{% endfor %} + + + +{% js %} + $('#{{ 'method'|namespaceInputId }}').change(function() { + var method = $(this).val(); + if (method === 'GET' || method === 'HEAD') { + $('#{{ 'requestContent'|namespaceInputId }}').addClass('hidden'); + } else { + $('#{{ 'requestContent'|namespaceInputId }}').removeClass('hidden'); + } + }); +{% endjs %} \ No newline at end of file diff --git a/src/templates/_components/actions/SendEmail/settings.twig b/src/templates/_components/actions/SendEmail/settings.twig new file mode 100644 index 0000000..c93c279 --- /dev/null +++ b/src/templates/_components/actions/SendEmail/settings.twig @@ -0,0 +1,48 @@ +{% import "_includes/forms" as forms %} + +{{ forms.editableTableField({ + allowAdd: true, + allowReorder: true, + allowDelete: true, + rows: settings.emails ?? [], + label: 'Addresses'|t('schedule'), + instructions: 'The email column supports environment variables'|t('schedule'), + cols: { + email: { + type: 'autosuggest', + suggestEnvVars: true, + heading: 'Email Address'|t('schedule') + }, + name: { + type: 'singleline', + heading: 'Name'|t('schedule') + } + }, + id: 'adresses', + name: 'adresses', + addRowLabel: "Add a address"|t("schedule"), + errors: action.getErrors('adresses') ?? [] +}) }} + +{{ forms.autoSuggestField({ + first: true, + label: "Subject"|t, + instructions: "", + id: "subject", + name: "subject", + value: action.subject, + required: true, + errors: action.getErrors("subject"), +}) }} + +{{ forms.textareaField({ + first: true, + label: "Message"|t('schedule'), + instructions: "", + id: "message", + name: "message", + value: action.message|nl2br, + rows: 10, + required: true, + errors: action.getErrors("message"), +}) }} diff --git a/src/templates/_components/timers/Cron.twig b/src/templates/_components/timers/Cron.twig new file mode 100644 index 0000000..12cc48d --- /dev/null +++ b/src/templates/_components/timers/Cron.twig @@ -0,0 +1,102 @@ +{% import "_includes/forms.twig" as forms %} + +
+ {{ forms.radio({ + label: 'Every'|t('schedule'), + name: 'mode', + value: 'every' + }) }} + + {% set options = [] %} + {% for label, option in timer.everyOptions %} + {% set options = options|merge([ + { + label: label|t('schedule'), + value: option + } + ]) %} + {% endfor %} + + {{ forms.select({ + required: true, + id: 'every', + name: 'every', + value: timer.every, + options: options, + errors: timer.getErrors('every'), + }) }} +
+ +
+ {{ forms.radio({ + label: 'Datetime'|t('schedule'), + name: 'mode', + value: 'datetime' + }) }} + + {% include "_includes/forms/datetime.twig" with { + name: 'datetime', + value: timer.datetime, + errors: timer.getErrors('datetime'), + } %} +
+ +
+ {{ forms.radio({ + label: 'Cron Expression'|t('schedule'), + name: 'mode', + value: 'expression' + }) }} + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+ +
+ +
+ +{{ forms.timezoneField({ + label: 'Timezone'|t('schedule'), + name: 'timezone', + value: timer.timezone ?: craft.app.timezone +}) }} + +{% css %} +#datetime fieldset { + display: inline-block; +} +.every-field { + margin: 12px 0; +} +.expression { + display: inline-block; + width: 100%; +} +.expression-item { + display: inline-block; + width: 18%; +} +.expression-item input { + width: 100%; +} +.every-field .datetimewrapper,.every-field .clear-btn { + display: inline-block; +} +{% endcss %} + diff --git a/src/templates/_edit.twig b/src/templates/_edit.twig index 5c79b0d..4d9a885 100644 --- a/src/templates/_edit.twig +++ b/src/templates/_edit.twig @@ -14,11 +14,11 @@ url: url('schedule') }] %} -{% set tabs = [ - {label: "Settings"|t('app'), url: '#settings'}, -] %} +{% set tabs = { + settings: { label: "Settings"|t('schedule'), url: '#settings' }, +} %} -{% import "_includes/forms" as forms %} +{% import "schedule/_includes/forms.twig" as forms %} {% block content %}
@@ -55,23 +55,49 @@ errors: schedule.getErrors('handle'), }) }} + {{ forms.selectField({ + label: 'When to execute'|t('schedule'), + disabled: not allowChange, + name: 'timerType', + value: className(schedule.timer), + options: timerTypeOptions, + toggle: true, + }) }} + + {% for timerType in timerTypes %} + {% set isCurrent = (timerType == className(schedule.timer)) %} + + {% endfor %} +
{{ forms.selectField({ - label: 'Schedule Type'|t('schedule'), + label: 'What to do'|t('schedule'), disabled: not allowChange, - name: 'type', - value: className(schedule), - options: scheduleTypeOptions, + name: 'actionType', + value: className(schedule.action), + options: actionTypeOptions, toggle: true, }) }} - {% for scheduleType in scheduleTypes %} - {% set isCurrent = (scheduleType == className(schedule)) %} - {% set scheduleTypeIds = scheduleTypeIds|merge([scheduleType|id]) %} - {% endblock %} -{% set scheduleTypeIds = [] %} -{% for scheduleType in scheduleTypes %} - {% if scheduleType != className(schedule) %} - {% set scheduleTypeIds = scheduleTypeIds|merge({(scheduleType): scheduleType|id}) %} +{% set actionTypeIds = [] %} +{% for actionType in actionTypes %} + {% if actionType != className(schedule.action) %} + {% set actionTypeIds = actionTypeIds|merge({(actionType): actionType|id}) %} + {% endif %} +{% endfor %} + +{% set timerTypeIds = [] %} +{% for timerType in timerTypes %} + {% if timerType != className(schedule.timer) %} + {% set timerTypeIds = timerTypeIds|merge({(timerType): timerType|id}) %} {% endif %} {% endfor %} {% js %} - var scheduleTypes = {{ scheduleTypeIds|json_encode|raw }}; - $('select[name=type]').on('change', function(value) { - var selectedType = $(this).val(); - if (selectedType in scheduleTypes) { - var id = '#' + scheduleTypes[selectedType]; - $.get("{{ actionUrl("schedule/schedules/get-schedule-settings-html") }}", { - scheduleType: selectedType, - }, function(ret) { - $(id).html(ret.html + ret.js); - }); - delete scheduleTypes[selectedType] + var actionTypes = {{ actionTypeIds|json_encode|raw }}; + $('select[name=actionType]').on('change', function(value) { + var actionType = $(this).val(); + if (actionType in actionTypes) { + var id = '#' + actionTypes[actionType]; + let _cancelToken = axios.CancelToken.source(); + Craft.sendActionRequest("POST", "schedule/actions/render-settings", { + cancelToken: _cancelToken.token, + data: { + type: actionType, + }, + }).then(function(response) { + let $settings = $(response.data.settingsHtml || ''); + $(id).html(response.data.settingsHtml); + Craft.appendHeadHtml(response.data.headHtml); + Craft.appendBodyHtml(response.data.bodyHtml); + }) + delete actionTypes[actionType] + } + }); + var timerTypes = {{ timerTypeIds|json_encode|raw }}; + $('select[name=timerType]').on('change', function(value) { + var timerType = $(this).val(); + if (timerType in timerTypes) { + var id = '#' + timerTypes[timerType]; + let _cancelToken = axios.CancelToken.source(); + Craft.sendActionRequest("POST", "schedule/timers/render-settings", { + cancelToken: _cancelToken.token, + data: { + type: timerType, + }, + }).then(function(response) { + let $settings = $(response.data.settingsHtml || ''); + $(id).html(response.data.settingsHtml); + Craft.appendHeadHtml(response.data.headHtml); + Craft.appendBodyHtml(response.data.bodyHtml); + }) + delete timerTypes[timerType] } }); {% endjs %} @@ -193,4 +301,5 @@ text-align: center; font-size: 10px; } -{% endcss %} \ No newline at end of file +{% endcss %} + diff --git a/src/templates/_includes/forms.twig b/src/templates/_includes/forms.twig new file mode 100644 index 0000000..49339c6 --- /dev/null +++ b/src/templates/_includes/forms.twig @@ -0,0 +1,6 @@ +{% extends "_includes/forms.twig" %} + +{% macro actionTypeField(config) %} + {% set config = config|merge({id: config.id ?? "entrytype#{random()}"}) %} + {{ _self.field(config, 'template:schedule/_includes/forms/actionType') }} +{% endmacro %} \ No newline at end of file diff --git a/src/templates/_includes/forms/actionType.twig b/src/templates/_includes/forms/actionType.twig new file mode 100644 index 0000000..ef7a75e --- /dev/null +++ b/src/templates/_includes/forms/actionType.twig @@ -0,0 +1,22 @@ +{%- set id = id ?? "actionType#{random()}" %} +{%- set containerId = "#{id}-container" %} + +
+ +
+ +{% js %} +$('#{{ id }}').click(function() { + const slideout = new Craft.CpScreenSlideout('schedule/actions/edit'); + slideout.on('submit', ev => { + createOption({ + text: ev.data.name, + value: ev.data.id, + }); + }); + slideout.on('close', () => { + selectize.focus(); + }); + return false; +}); +{% endjs %} diff --git a/src/templates/_layouts/settings.twig b/src/templates/_layouts/settings.twig index 7dd0321..f18ab59 100644 --- a/src/templates/_layouts/settings.twig +++ b/src/templates/_layouts/settings.twig @@ -8,7 +8,10 @@ {% if currentUser.admin %} {% set navItems = { 'general': { title: "General"|t('schedule') }, + 'webcron': { title: "Web Cron"|t('schedule') }, 'logs': { title: "Logs"|t('schedule') }, + 'actions': { heading: "Actions"|t('schedule') }, + 'httprequest': { title: "HTTP Request"|t('schedule') }, } %} {% endif %} diff --git a/src/templates/actions/_edit.twig b/src/templates/actions/_edit.twig new file mode 100644 index 0000000..eeee129 --- /dev/null +++ b/src/templates/actions/_edit.twig @@ -0,0 +1,53 @@ +{% import "_includes/forms.twig" as forms %} + +{{ forms.selectField({ + label: 'Type'|t('schedule'), + id: 'actionType', + name: 'actionType', + value: className(action), + options: actionTypeOptions, + toggle: true, +}) }} + +{% for actionType in actionTypes %} + {% set isCurrent = (actionType == className(action)) %} + +{% endfor %} + +{% set actionTypeIds = [] %} +{% for actionType in actionTypes %} + {% if actionType != className(action) %} + {% set actionTypeIds = actionTypeIds|merge({(actionType): actionType|id|namespaceInputId}) %} + {% endif %} +{% endfor %} + +{% js %} +var actionTypes = {{ actionTypeIds|json_encode|raw }}; +$('#{{ 'actionType'|namespaceInputId }}').on('change', function(value) { + var actionType = $(this).val(); + if (actionType in actionTypes) { + var id = '#' + actionTypes[actionType]; + let _cancelToken = axios.CancelToken.source(); + Craft.sendActionRequest("POST", "schedule/actions/render-settings", { + cancelToken: _cancelToken.token, + data: { + type: actionType, + }, + }).then(function(response) { + let $settings = $(response.data.settingsHtml || ''); + $(id).html(response.data.settingsHtml); + Craft.appendHeadHtml(response.data.headHtml); + Craft.appendBodyHtml(response.data.bodyHtml); + }) + delete actionTypes[actionType] + } +}); +{% endjs %} diff --git a/src/templates/settings/general/index.twig b/src/templates/settings/general/index.twig index bf14e95..e57d8e9 100644 --- a/src/templates/settings/general/index.twig +++ b/src/templates/settings/general/index.twig @@ -1,6 +1,5 @@ {% extends "schedule/_layouts/settings" %} -{% set fullPageFomr = true %} {% set settings = craft.schedule.settings %} {% import "_includes/forms" as forms %} diff --git a/src/templates/settings/webcron/index.twig b/src/templates/settings/webcron/index.twig new file mode 100644 index 0000000..34edc26 --- /dev/null +++ b/src/templates/settings/webcron/index.twig @@ -0,0 +1,58 @@ +{% extends "schedule/_layouts/settings" %} + +{% set settings = craft.schedule.settings %} + +{% import "_includes/forms" as forms %} + +{% block content %} +
+ + {{ csrfInput() }} + + {{ forms.lightSwitchField({ + first: true, + label: "EnabledWebCron"|t, + instructions: "", + id: "enabled-web-cron", + name: "enabledWebCron", + value: settings.enabledWebCron, + required: true, + errors: settings.getErrors("enabledWebCron"), + }) }} + + {{ forms.autoSuggestField({ + first: true, + label: "Endpoint"|t('schedule'), + instructions: "", + id: "endpoint", + name: "endpoint", + value: settings.endpoint, + required: true, + errors: settings.getErrors("endpoint"), + }) }} + + {{ forms.textField({ + first: true, + label: "Allow Methods"|t('schedule'), + instructions: "", + id: "allowMethods", + name: "allowMethods", + value: settings.allowMethods, + required: true, + errors: settings.getErrors("allowMethods"), + }) }} + + {{ forms.autoSuggestField({ + first: true, + label: "Token"|t, + instructions: "", + id: "token", + name: "token", + value: settings.token, + required: true, + errors: settings.getErrors("token"), + }) }} + + +
+{% endblock %} \ No newline at end of file diff --git a/src/timers/Cron.php b/src/timers/Cron.php new file mode 100644 index 0000000..50541b3 --- /dev/null +++ b/src/timers/Cron.php @@ -0,0 +1,179 @@ +$property === '') { + $this->$property = '*'; + } + }]; + + return $rules; + } + + public function getSettingsHtml(): ?string + { + return Craft::$app->getView()->renderTemplate('schedule/_components/timers/Cron', [ + 'timer' => $this, + 'modeOptions' => [ + ['label' => Craft::t('schedule', 'Every'), 'value' => self::MODE_EVERY], + ['label' => Craft::t('schedule', 'DateTime'), 'value' => self::MODE_DATETIME], + ['label' => Craft::t('schedule', 'Expression'), 'value' => self::MODE_EXPRESSION], + ] + ]); + } + + public function isDue(): bool + { + $now = new \DateTime('now', $this->timezone); + if (($this->mode === self::MODE_DATETIME) && $this->getDatetime()?->format('Y') !== $now->format('Y')) { + return false; + } + + return (new CronExpression($this->getCronExpression()))->isDue($now); + } + + public function getCronExpression(): string + { + return sprintf('%s %s %s %s %s', $this->minute, $this->hour, $this->day, $this->month, $this->week); + } + + public function setCronExpression(string $cron): void + { + [$this->minute, $this->hour, $this->day, $this->month, $this->week, ] = explode(' ', $cron); + } + + public function getCronDescription(): string + { + return CronHelper::toDescription($this->getCronExpression()); + } + + public function getDatetime(): ?\DateTime + { + if ($this->year === '*') { + return null; + } + $timezone = new DateTimeZone($this->timezone ?: Craft::$app->getTimeZone()); + $datetime = sprintf("%d-%d-%d %d:%d", $this->year, $this->month, $this->day, $this->hour, $this->minute); + return new \DateTime($datetime, $timezone); + } + + public function setDateTime(mixed $datetime): void + { + $datetime = DateTimeHelper::toDateTime($datetime); + $this->timezone = $datetime->getTimezone()->getName(); + $this->year = $datetime->format('Y'); + $this->month = $datetime->format('m'); + $this->day = $datetime->format('d'); + $this->hour = $datetime->format('H'); + $this->minute = $datetime->format('m'); + } + + public function getEvery(): string + { + if ($this->mode !== self::MODE_EVERY){ + return self::EVERY_MINUTE; + } + return match ($this->getCronExpression()) { + '* * * * *' => self::EVERY_MINUTE, + '0 * * * *' => self::EVERY_HOURLY, + '0 0 * * *' => self::EVERY_DAILY, + '0 0 1 * *' => self::EVERY_MONTHLY, + '0 0 * * 0' => self::EVERY_WEEKLY, + '0 0 1 1 *' => self::EVERY_YEARLY, + }; + } + + public function setEvery(string $unit): void + { + $this->setCronExpression(match ($unit) { + self::EVERY_MINUTE => '* * * * *', + self::EVERY_HOURLY => '0 * * * *', + self::EVERY_DAILY => '0 0 * * *', + self::EVERY_MONTHLY => '0 0 1 * *', + self::EVERY_WEEKLY => '0 0 * * 0', + self::EVERY_YEARLY => '0 0 1 1 *', + }); + } + + public function getEveryOptions(): array + { + return [ + Craft::t('schedule', 'Minute') => self::EVERY_MINUTE, + Craft::t('schedule', 'Hourly') => self::EVERY_HOURLY, + Craft::t('schedule', 'Daily') => self::EVERY_DAILY, + Craft::t('schedule', 'Monthly') => self::EVERY_MONTHLY, + Craft::t('schedule', 'Yearly') => self::EVERY_YEARLY, + Craft::t('schedule', 'Weekly') => self::EVERY_WEEKLY, + ]; + } +} \ No newline at end of file diff --git a/src/timers/Custom.php b/src/timers/Custom.php index 3fdf602..52ba36a 100644 --- a/src/timers/Custom.php +++ b/src/timers/Custom.php @@ -15,6 +15,7 @@ * * @package panlatent\schedule\timers * @author Panlatent + * @deprecated since 1.0 */ class Custom extends Timer { diff --git a/src/timers/DateTime.php b/src/timers/DateTime.php index e596631..2894190 100644 --- a/src/timers/DateTime.php +++ b/src/timers/DateTime.php @@ -17,6 +17,7 @@ * * @package panlatent\schedule\timers * @author Panlatent + * @deprecated since 1.0 */ class DateTime extends Timer { diff --git a/src/timers/Every.php b/src/timers/Every.php index cea415f..a057e8d 100644 --- a/src/timers/Every.php +++ b/src/timers/Every.php @@ -15,6 +15,7 @@ * * @package panlatent\schedule\timers * @author Panlatent + * @deprecated since 1.0 */ class Every extends Timer { diff --git a/src/timers/Relay.php b/src/timers/Relay.php index d6ced88..53a1b1f 100644 --- a/src/timers/Relay.php +++ b/src/timers/Relay.php @@ -8,9 +8,9 @@ namespace panlatent\schedule\timers; use Craft; +use Cron\CronExpression; use DateInterval; use DateTime; -use panlatent\schedule\base\Schedule; use panlatent\schedule\base\Timer; /** @@ -65,26 +65,37 @@ public function attributeLabels(): array return $attributeLabels; } - /** - * @inheritdoc - */ - public function getCronExpression(): string + public function isDue(): bool { - /** @var Schedule $schedule */ $schedule = $this->getSchedule(); - - if (!$schedule->getLastFinishedDate()) { - return '* * * * *'; + $lastFinishedTime = $schedule->getInfo()->getLastFinishedTime(); + if (!$lastFinishedTime) { + return true; } + $date = $lastFinishedTime->add(new DateInterval("PT{$this->wait}M")); + return $date->format('YmdHi') <= date('YmdHi'); - $date = $schedule->getLastFinishedDate()->add(new DateInterval("PT{$this->wait}M")); - if ($date->format('YmdHi') < date('YmdHi')) { - $date = new DateTime('now'); - } - - return $date->format('i H d m *'); +// $now = new \DateTime('now'); +// return (new CronExpression($this->getCronExpression()))->isDue($now); } +// public function getCronExpression(): string +// { +// $schedule = $this->getSchedule(); +// +// $lastFinishedTime = $schedule->getInfo()->getLastFinishedTime(); +// if (!$lastFinishedTime) { +// return '* * * * *'; +// } +// +// $date = $lastFinishedTime->add(new DateInterval("PT{$this->wait}M")); +// if ($date->format('YmdHi') <= date('YmdHi')) { +// $date = new DateTime('now'); +// } +// +// return $date->format('i H d m *'); +// } + /** * @inheritdoc */