module CronPixie exposing (..) import Html exposing (Html, div, text, h3, ul, li, span) import Html.Attributes exposing (class, title) import Html.Events exposing (..) import Date import Date.Format import Time exposing (Time, second) import String import List exposing (head, tail, reverse) import Maybe exposing (withDefault) import Task import Http exposing (stringPart, multipartBody, encodeUri) import Json.Decode exposing (..) import Json.Encode as Json import Result.Extra exposing (unpack) -- MODEL type alias Model = { strings : Strings , nonce : String , timer_period : Float , schedules : List Schedule } type alias Strings = { no_events : String , due : String , now : String , passed : String , weeks_abrv : String , days_abrv : String , hours_abrv : String , minutes_abrv : String , seconds_abrv : String , run_now : String } type alias Schedule = { name : String , display : String , interval : Maybe Int , events : Maybe (List Event) } type alias Event = { schedule : String , interval : Maybe Int , hook : String , args : List ( String, String ) , timestamp : Int , seconds_due : Int } type alias Divider = { name : String , val : Int } type alias Flags = { strings : Strings , nonce : String , timer_period : String , schedules : Value } init : Flags -> ( Model, Cmd Msg ) init flags = ( Model flags.strings flags.nonce (decodeTimerPeriod flags.timer_period) (decodeSchedules flags.schedules), Cmd.none ) -- MESSAGES type Msg = Tick Time | Fetch (Result Http.Error (List Schedule)) | RunNow Event | UpdateEvent (Result Http.Error String) -- VIEW view : Model -> Html Msg view model = div [] [ h3 [] [ text "Schedules" ] , ul [ class "cron-pixie-schedules" ] (List.map (scheduleView model) model.schedules) ] scheduleView : Model -> Schedule -> Html Msg scheduleView model schedule = li [] [ span [ class "cron-pixie-schedule-display", title schedule.name ] [ text schedule.display ] , eventsView model schedule.events ] eventsView : Model -> Maybe (List Event) -> Html Msg eventsView model events = case events of Just events_ -> ul [ class "cron-pixie-events" ] (List.map (eventView model) events_) Nothing -> text "" eventView : Model -> Event -> Html Msg eventView model event = li [] [ span [ class "cron-pixie-event-run dashicons dashicons-controls-forward", title model.strings.run_now, onClick (RunNow event) ] [] , span [ class "cron-pixie-event-hook" ] [ text event.hook ] , div [ class "cron-pixie-event-timestamp dashicons-before dashicons-clock" ] [ text " " , span [ class "cron-pixie-event-due" ] [ text (model.strings.due ++ ": " ++ (due event.timestamp)) ] , text " " , span [ class "cron-pixie-event-seconds-due" ] [ text ("(" ++ (displayInterval model event.seconds_due) ++ ")") ] ] ] due : Int -> String due timestamp = timestamp * 1000 |> toFloat |> Date.fromTime |> Date.Format.format "%Y-%m-%d %H:%M:%S" intervals : Model -> List Divider intervals model = [ { name = model.strings.weeks_abrv, val = 604800000 } , { name = model.strings.days_abrv, val = 86400000 } , { name = model.strings.hours_abrv, val = 3600000 } , { name = model.strings.minutes_abrv, val = 60000 } , { name = model.strings.seconds_abrv, val = 1000 } ] displayInterval : Model -> Int -> String displayInterval model seconds = let -- Convert everything to milliseconds so we can handle seconds in map. milliseconds = seconds * 1000 in if 0 > (seconds + 60) then -- Cron runs max every 60 seconds. model.strings.passed else if 0 > (toFloat seconds - model.timer_period) then -- If due now or in next refresh period, show "now". model.strings.now else divideInterval [] milliseconds (intervals model) |> reverse |> String.join " " divideInterval : List String -> Int -> List Divider -> List String divideInterval parts milliseconds dividers = case dividers of e1 :: rest -> divideInterval_ parts milliseconds (head dividers) (withDefault [] (tail dividers)) _ -> parts divideInterval_ : List String -> Int -> Maybe Divider -> List Divider -> List String divideInterval_ parts milliseconds divider dividers = case divider of Just divider_ -> let count = milliseconds // divider_.val in if 0 < count then divideInterval ((toString count ++ divider_.name) :: parts) (milliseconds % divider_.val) dividers else divideInterval parts milliseconds dividers Nothing -> parts -- UPDATE update : Msg -> Model -> ( Model, Cmd Msg ) update msg model = case msg of Tick newTime -> ( model, getSchedules model.nonce ) Fetch (Ok schedules) -> ( { model | schedules = schedules }, Cmd.none ) Fetch (Err _) -> ( model, Cmd.none ) RunNow event -> let dueEvent = { event | timestamp = (event.timestamp - event.seconds_due), seconds_due = 0 } in ( { model | schedules = List.map (updateScheduledEvent event dueEvent) model.schedules }, postEvent model.nonce dueEvent ) UpdateEvent (Ok schedules) -> ( model, Cmd.none ) UpdateEvent (Err _) -> ( model, Cmd.none ) getSchedules : String -> Cmd Msg getSchedules nonce = let encodedUrl = url "/wp-admin/admin-ajax.php" [ ( "action", "cron_pixie_schedules" ), ( "nonce", nonce ) ] in Http.send Fetch (Http.get encodedUrl schedulesDecoder) schedulesDecoder : Decoder (List Schedule) schedulesDecoder = list scheduleDecoder scheduleDecoder : Decoder Schedule scheduleDecoder = map4 Schedule (field "name" string) (field "display" string) (maybe (field "interval" int)) (maybe (field "events" (list eventDecoder))) eventDecoder : Decoder Event eventDecoder = map6 Event (oneOf [ field "schedule" string, succeed "false" ]) (maybe (field "interval" int)) (field "hook" string) (field "args" eventArgsDecoder) (field "timestamp" int) (field "seconds_due" int) eventArgsDecoder : Decoder (List ( String, String )) eventArgsDecoder = oneOf [ keyValuePairs string , succeed [] ] decodeSchedules : Value -> List Schedule decodeSchedules json = let result = decodeValue schedulesDecoder json in case result of Ok schedules -> schedules Err error -> [] decodeTimerPeriod : String -> Float decodeTimerPeriod string = let result = String.toFloat string in case result of Ok float -> float Err error -> 5.0 updateScheduledEvent : Event -> Event -> Schedule -> Schedule updateScheduledEvent oldEvent newEvent schedule = case schedule.events of Just events -> { schedule | events = Just <| List.map (updateMatchedEvent oldEvent newEvent) events } Nothing -> schedule updateMatchedEvent : Event -> Event -> Event -> Event updateMatchedEvent match newEvent event = if match == event then newEvent else event postEvent : String -> Event -> Cmd Msg postEvent nonce event = let url = "/wp-admin/admin-ajax.php" eventValue = Json.object [ ( "hook", Json.string event.hook ) , ( "args", Json.object (List.map (\( key, val ) -> ( key, Json.string val )) event.args) ) , ( "schedule", Json.string event.schedule ) , ( "timestamp", Json.int event.timestamp ) ] body = multipartBody [ stringPart "action" "cron_pixie_events" , stringPart "nonce" nonce , stringPart "model" (Json.encode 0 eventValue) -- , stringData "model" (Json.encode 0 eventValue) ] in Http.send UpdateEvent (Http.post url body string) url : String -> List ( String, String ) -> String url baseUrl args = case args of [] -> baseUrl _ -> baseUrl ++ "?" ++ String.join "&" (List.map queryPair args) queryPair : ( String, String ) -> String queryPair ( key, value ) = queryEscape key ++ "=" ++ queryEscape value queryEscape : String -> String queryEscape string = String.join "+" (String.split "%20" (encodeUri string)) -- SUBSCRIPTIONS subscriptions : Model -> Sub Msg subscriptions model = Time.every (model.timer_period * second) Tick -- MAIN main : Program Flags Model Msg main = Html.programWithFlags { init = init , view = view , update = update , subscriptions = subscriptions }