mirror of
https://github.com/commercialhaskell/stackage.git
synced 2026-01-12 07:18:31 +01:00
13158 lines
473 KiB
Diff
13158 lines
473 KiB
Diff
diff -ru orig/MFlow.cabal new/MFlow.cabal
|
|
--- orig/MFlow.cabal 2014-06-10 05:51:27.009015855 +0300
|
|
+++ new/MFlow.cabal 2014-06-10 05:51:25.000000000 +0300
|
|
@@ -105,10 +105,10 @@
|
|
extensible-exceptions , base >4.0 && <5,
|
|
bytestring , containers , RefSerialize , TCache ,
|
|
stm >2, time, old-time , vector , directory ,
|
|
- utf8-string , wai , case-insensitive ,
|
|
+ utf8-string , wai , wai-extra, resourcet, case-insensitive ,
|
|
http-types , conduit ,conduit-extra, text , parsec , warp ,
|
|
warp-tls , random , blaze-html , blaze-markup ,
|
|
- monadloc, clientsession
|
|
+ monadloc, clientsession, pwstore-fast
|
|
exposed-modules: MFlow MFlow.Wai.Blaze.Html.All MFlow.Forms
|
|
MFlow.Forms.Admin MFlow.Cookies MFlow.Wai
|
|
MFlow.Forms.Blaze.Html MFlow.Forms.Test
|
|
@@ -128,7 +128,7 @@
|
|
-- hamlet , shakespeare, monadloc , aws , network , hscolour ,
|
|
-- persistent-template , persistent-sqlite , persistent ,
|
|
-- conduit , http-conduit , monad-logger , safecopy ,
|
|
--- time
|
|
+-- time, acid-state
|
|
-- main-is: demos-blaze.hs
|
|
-- buildable: True
|
|
-- hs-source-dirs: Demos
|
|
@@ -142,4 +142,4 @@
|
|
-- InitialConfig GenerateForm GenerateFormUndo GenerateFormUndoMsg WebService
|
|
-- LazyLoad
|
|
-- ghc-options: -iDemos -threaded -rtsopts
|
|
---
|
|
+
|
|
diff -ru orig/Setup.lhs new/Setup.lhs
|
|
--- orig/Setup.lhs 2014-06-10 05:51:26.953015857 +0300
|
|
+++ new/Setup.lhs 2014-06-10 05:51:25.000000000 +0300
|
|
@@ -1,5 +1,5 @@
|
|
-#! /usr/bin/runghc
|
|
-
|
|
-> import Distribution.Simple
|
|
->
|
|
-> main = defaultMain
|
|
+#! /usr/bin/runghc
|
|
+
|
|
+> import Distribution.Simple
|
|
+>
|
|
+> main = defaultMain
|
|
diff -ru orig/src/MFlow/Cookies.hs new/src/MFlow/Cookies.hs
|
|
--- orig/src/MFlow/Cookies.hs 2014-06-10 05:51:26.961015857 +0300
|
|
+++ new/src/MFlow/Cookies.hs 2014-06-10 05:51:25.000000000 +0300
|
|
@@ -1,188 +1,188 @@
|
|
-{-# OPTIONS -XScopedTypeVariables -XOverloadedStrings #-}
|
|
-
|
|
-module MFlow.Cookies (
|
|
- CookieT,
|
|
- Cookie(..),
|
|
- contentHtml,
|
|
- cookieuser,
|
|
- cookieHeaders,
|
|
- getCookies,
|
|
- paranoidEncryptCookie,
|
|
- paranoidDecryptCookie,
|
|
- encryptCookie,
|
|
- decryptCookie
|
|
- )
|
|
-where
|
|
-import Control.Monad(MonadPlus(..), guard, replicateM_, when)
|
|
-import Data.Char
|
|
-import Data.Maybe(fromMaybe, fromJust)
|
|
-import System.IO.Unsafe
|
|
-import Control.Exception(handle)
|
|
-import Data.Typeable
|
|
-import Unsafe.Coerce
|
|
-import Data.Monoid
|
|
-import Text.Parsec
|
|
-import Control.Monad.Identity
|
|
-import Data.ByteString.Char8 as B
|
|
-import Web.ClientSession
|
|
-import System.Environment
|
|
-
|
|
---import Debug.Trace
|
|
---(!>)= flip trace
|
|
-
|
|
-contentHtml :: (ByteString, ByteString)
|
|
-contentHtml= ("Content-Type", "text/html; charset=UTF-8")
|
|
-
|
|
-type CookieT = (B.ByteString,B.ByteString,B.ByteString,Maybe B.ByteString)
|
|
-
|
|
-data Cookie
|
|
- = UnEncryptedCookie CookieT
|
|
- | EncryptedCookie CookieT
|
|
- | ParanoidCookie CookieT
|
|
- deriving (Eq, Read, Show)
|
|
-
|
|
-cookieuser :: String
|
|
-cookieuser= "cookieuser"
|
|
-
|
|
-getCookies httpreq=
|
|
- case lookup "Cookie" $ httpreq of
|
|
- Just str -> splitCookies str :: [(B.ByteString, B.ByteString)]
|
|
- Nothing -> []
|
|
-
|
|
-cookieHeaders cs = Prelude.map (\c-> ( "Set-Cookie", showCookie c)) cs
|
|
-
|
|
-showCookie :: Cookie -> B.ByteString
|
|
-showCookie c@(EncryptedCookie _) = showCookie' $ decryptAndToTuple c
|
|
-showCookie c@(ParanoidCookie _) = showCookie' $ decryptAndToTuple c
|
|
-showCookie (UnEncryptedCookie c) = showCookie' c
|
|
-
|
|
-showCookie' (n,v,p,me) = n <> "=" <> v <>
|
|
- ";path=" <> p <>
|
|
- showMaxAge me
|
|
-
|
|
-showMaxAge Nothing = ""
|
|
-showMaxAge (Just e) = ";Max-age=" <> e
|
|
-
|
|
-splitCookies cookies = f cookies []
|
|
- where
|
|
- f s r | B.null s = r
|
|
- f xs0 r =
|
|
- let
|
|
- xs = B.dropWhile (==' ') xs0
|
|
- name = B.takeWhile (/='=') xs
|
|
- xs1 = B.dropWhile (/='=') xs
|
|
- xs2 = B.dropWhile (=='=') xs1
|
|
- val = B.takeWhile (/=';') xs2
|
|
- xs3 = B.dropWhile (/=';') xs2
|
|
- xs4 = B.dropWhile (==';') xs3
|
|
- xs5 = B.dropWhile (==' ') xs4
|
|
- in f xs5 ((name,val):r)
|
|
-
|
|
+{-# OPTIONS -XScopedTypeVariables -XOverloadedStrings #-}
|
|
+
|
|
+module MFlow.Cookies (
|
|
+ CookieT,
|
|
+ Cookie(..),
|
|
+ contentHtml,
|
|
+ cookieuser,
|
|
+ cookieHeaders,
|
|
+ getCookies,
|
|
+ paranoidEncryptCookie,
|
|
+ paranoidDecryptCookie,
|
|
+ encryptCookie,
|
|
+ decryptCookie
|
|
+ )
|
|
+where
|
|
+import Control.Monad(MonadPlus(..), guard, replicateM_, when)
|
|
+import Data.Char
|
|
+import Data.Maybe(fromMaybe, fromJust)
|
|
+import System.IO.Unsafe
|
|
+import Control.Exception(handle)
|
|
+import Data.Typeable
|
|
+import Unsafe.Coerce
|
|
+import Data.Monoid
|
|
+import Text.Parsec
|
|
+import Control.Monad.Identity
|
|
+import Data.ByteString.Char8 as B
|
|
+import Web.ClientSession
|
|
+import System.Environment
|
|
+
|
|
+--import Debug.Trace
|
|
+--(!>)= flip trace
|
|
+
|
|
+contentHtml :: (ByteString, ByteString)
|
|
+contentHtml= ("Content-Type", "text/html; charset=UTF-8")
|
|
+
|
|
+type CookieT = (B.ByteString,B.ByteString,B.ByteString,Maybe B.ByteString)
|
|
+
|
|
+data Cookie
|
|
+ = UnEncryptedCookie CookieT
|
|
+ | EncryptedCookie CookieT
|
|
+ | ParanoidCookie CookieT
|
|
+ deriving (Eq, Read, Show)
|
|
+
|
|
+cookieuser :: String
|
|
+cookieuser= "cookieuser"
|
|
+
|
|
+getCookies httpreq=
|
|
+ case lookup "Cookie" $ httpreq of
|
|
+ Just str -> splitCookies str :: [(B.ByteString, B.ByteString)]
|
|
+ Nothing -> []
|
|
+
|
|
+cookieHeaders cs = Prelude.map (\c-> ( "Set-Cookie", showCookie c)) cs
|
|
+
|
|
+showCookie :: Cookie -> B.ByteString
|
|
+showCookie c@(EncryptedCookie _) = showCookie' $ decryptAndToTuple c
|
|
+showCookie c@(ParanoidCookie _) = showCookie' $ decryptAndToTuple c
|
|
+showCookie (UnEncryptedCookie c) = showCookie' c
|
|
+
|
|
+showCookie' (n,v,p,me) = n <> "=" <> v <>
|
|
+ ";path=" <> p <>
|
|
+ showMaxAge me
|
|
+
|
|
+showMaxAge Nothing = ""
|
|
+showMaxAge (Just e) = ";Max-age=" <> e
|
|
+
|
|
+splitCookies cookies = f cookies []
|
|
+ where
|
|
+ f s r | B.null s = r
|
|
+ f xs0 r =
|
|
+ let
|
|
+ xs = B.dropWhile (==' ') xs0
|
|
+ name = B.takeWhile (/='=') xs
|
|
+ xs1 = B.dropWhile (/='=') xs
|
|
+ xs2 = B.dropWhile (=='=') xs1
|
|
+ val = B.takeWhile (/=';') xs2
|
|
+ xs3 = B.dropWhile (/=';') xs2
|
|
+ xs4 = B.dropWhile (==';') xs3
|
|
+ xs5 = B.dropWhile (==' ') xs4
|
|
+ in f xs5 ((name,val):r)
|
|
+
|
|
----------------------------
|
|
-
|
|
---readEnv :: Parser [(String,String)]
|
|
-readEnv = (do
|
|
- n <- urlEncoded
|
|
- string "="
|
|
- v <- urlEncoded
|
|
- return (n,v)) `sepBy` (string "&")
|
|
-
|
|
-urlEncoded :: Parsec String () String
|
|
-urlEncoded
|
|
- = many ( alphaNum `mplus` extra `mplus` safe
|
|
- `mplus` do{ char '+' ; return ' '}
|
|
- `mplus` do{ char '%' ; hexadecimal }
|
|
- )
|
|
-
|
|
-
|
|
---extra :: Parser Char
|
|
-extra = satisfy (`Prelude.elem` "!*'(),/\"")
|
|
---
|
|
---safe :: Parser Char
|
|
-safe = satisfy (`Prelude.elem` "$-_.")
|
|
-----
|
|
---hexadecimal :: Parser HexString
|
|
-hexadecimal = do d1 <- hexDigit
|
|
- d2 <- hexDigit
|
|
- return .chr $ toInt d1* 16 + toInt d2
|
|
- where toInt d | isDigit d = ord d - ord '0'
|
|
- toInt d | isHexDigit d = (ord d - ord 'A') + 10
|
|
- toInt d = error ("hex2int: illegal hex digit " ++ [d])
|
|
-
|
|
-
|
|
-
|
|
-decryptCookie :: Cookie -> IO Cookie
|
|
-decryptCookie c@(UnEncryptedCookie _) = return c
|
|
-decryptCookie (EncryptedCookie c) = decryptCookie' c
|
|
-decryptCookie (ParanoidCookie c) = paranoidDecryptCookie c
|
|
-
|
|
--- Uses 4 seperate keys, corresponding to the 4 seperate fields in the Cookie.
|
|
-paranoidEncryptCookie :: CookieT -> IO Cookie
|
|
-paranoidEncryptCookie (a,b,c,d) = do
|
|
- key1 <- getKey "CookieKey1.key"
|
|
- key2 <- getKey "CookieKey2.key"
|
|
- key3 <- getKey "CookieKey3.key"
|
|
- key4 <- getKey "CookieKey4.key"
|
|
- iv1 <- randomIV
|
|
- iv2 <- randomIV
|
|
- iv3 <- randomIV
|
|
- iv4 <- randomIV
|
|
- return $ ParanoidCookie
|
|
- ( encrypt key1 iv1 a,
|
|
- encrypt key2 iv2 b,
|
|
- encrypt key3 iv3 c,
|
|
- encryptMaybe key4 iv4 d)
|
|
-
|
|
-paranoidDecryptCookie :: CookieT -> IO Cookie
|
|
-paranoidDecryptCookie (a,b,c,d) = do
|
|
- key1 <- getKey "CookieKey1.key"
|
|
- key2 <- getKey "CookieKey2.key"
|
|
- key3 <- getKey "CookieKey3.key"
|
|
- key4 <- getKey "CookieKey4.key"
|
|
- return $ UnEncryptedCookie
|
|
- ( decryptFM key1 a,
|
|
- decryptFM key2 b,
|
|
- decryptFM key3 c,
|
|
- decryptMaybe key4 d)
|
|
-
|
|
--- Uses a single key to encrypt all 4 fields.
|
|
-encryptCookie :: CookieT -> IO Cookie
|
|
-encryptCookie (a,b,c,d) = do
|
|
- key <- getKey "CookieKey.key"
|
|
- iv1 <- randomIV
|
|
- iv2 <- randomIV
|
|
- iv3 <- randomIV
|
|
- iv4 <- randomIV
|
|
- return $ EncryptedCookie
|
|
- ( encrypt key iv1 a,
|
|
- encrypt key iv2 b,
|
|
- encrypt key iv3 c,
|
|
- encryptMaybe key iv4 d)
|
|
-
|
|
-decryptCookie' :: CookieT -> IO Cookie
|
|
-decryptCookie' (a,b,c,d) = do
|
|
- key <- getKey "CookieKey.key"
|
|
- return $ UnEncryptedCookie
|
|
- ( decryptFM key a,
|
|
- decryptFM key b,
|
|
- decryptFM key c,
|
|
- decryptMaybe key d)
|
|
-
|
|
-encryptMaybe :: Key -> IV -> Maybe ByteString -> Maybe ByteString
|
|
-encryptMaybe k i (Just s) = Just $ encrypt k i s
|
|
-encryptMaybe _ _ Nothing = Nothing
|
|
-
|
|
-decryptMaybe :: Key -> Maybe ByteString -> Maybe ByteString
|
|
-decryptMaybe k (Just s) = Just $ fromMaybe "" $ decrypt k s
|
|
-decryptMaybe _ Nothing = Nothing
|
|
-
|
|
-decryptFM :: Key -> ByteString -> ByteString
|
|
-decryptFM k b = fromMaybe "" $ decrypt k b
|
|
-
|
|
-cookieToTuple :: Cookie -> CookieT
|
|
-cookieToTuple (UnEncryptedCookie c) = c
|
|
-cookieToTuple (EncryptedCookie c) = c
|
|
-cookieToTuple (ParanoidCookie c) = c
|
|
-
|
|
-decryptAndToTuple :: Cookie -> CookieT
|
|
-decryptAndToTuple = cookieToTuple . unsafePerformIO . decryptCookie
|
|
+
|
|
+--readEnv :: Parser [(String,String)]
|
|
+readEnv = (do
|
|
+ n <- urlEncoded
|
|
+ string "="
|
|
+ v <- urlEncoded
|
|
+ return (n,v)) `sepBy` (string "&")
|
|
+
|
|
+urlEncoded :: Parsec String () String
|
|
+urlEncoded
|
|
+ = many ( alphaNum `mplus` extra `mplus` safe
|
|
+ `mplus` do{ char '+' ; return ' '}
|
|
+ `mplus` do{ char '%' ; hexadecimal }
|
|
+ )
|
|
+
|
|
+
|
|
+--extra :: Parser Char
|
|
+extra = satisfy (`Prelude.elem` "!*'(),/\"")
|
|
+--
|
|
+--safe :: Parser Char
|
|
+safe = satisfy (`Prelude.elem` "$-_.")
|
|
+----
|
|
+--hexadecimal :: Parser HexString
|
|
+hexadecimal = do d1 <- hexDigit
|
|
+ d2 <- hexDigit
|
|
+ return .chr $ toInt d1* 16 + toInt d2
|
|
+ where toInt d | isDigit d = ord d - ord '0'
|
|
+ toInt d | isHexDigit d = (ord d - ord 'A') + 10
|
|
+ toInt d = error ("hex2int: illegal hex digit " ++ [d])
|
|
+
|
|
+
|
|
+
|
|
+decryptCookie :: Cookie -> IO Cookie
|
|
+decryptCookie c@(UnEncryptedCookie _) = return c
|
|
+decryptCookie (EncryptedCookie c) = decryptCookie' c
|
|
+decryptCookie (ParanoidCookie c) = paranoidDecryptCookie c
|
|
+
|
|
+-- Uses 4 seperate keys, corresponding to the 4 seperate fields in the Cookie.
|
|
+paranoidEncryptCookie :: CookieT -> IO Cookie
|
|
+paranoidEncryptCookie (a,b,c,d) = do
|
|
+ key1 <- getKey "CookieKey1.key"
|
|
+ key2 <- getKey "CookieKey2.key"
|
|
+ key3 <- getKey "CookieKey3.key"
|
|
+ key4 <- getKey "CookieKey4.key"
|
|
+ iv1 <- randomIV
|
|
+ iv2 <- randomIV
|
|
+ iv3 <- randomIV
|
|
+ iv4 <- randomIV
|
|
+ return $ ParanoidCookie
|
|
+ ( encrypt key1 iv1 a,
|
|
+ encrypt key2 iv2 b,
|
|
+ encrypt key3 iv3 c,
|
|
+ encryptMaybe key4 iv4 d)
|
|
+
|
|
+paranoidDecryptCookie :: CookieT -> IO Cookie
|
|
+paranoidDecryptCookie (a,b,c,d) = do
|
|
+ key1 <- getKey "CookieKey1.key"
|
|
+ key2 <- getKey "CookieKey2.key"
|
|
+ key3 <- getKey "CookieKey3.key"
|
|
+ key4 <- getKey "CookieKey4.key"
|
|
+ return $ UnEncryptedCookie
|
|
+ ( decryptFM key1 a,
|
|
+ decryptFM key2 b,
|
|
+ decryptFM key3 c,
|
|
+ decryptMaybe key4 d)
|
|
+
|
|
+-- Uses a single key to encrypt all 4 fields.
|
|
+encryptCookie :: CookieT -> IO Cookie
|
|
+encryptCookie (a,b,c,d) = do
|
|
+ key <- getKey "CookieKey.key"
|
|
+ iv1 <- randomIV
|
|
+ iv2 <- randomIV
|
|
+ iv3 <- randomIV
|
|
+ iv4 <- randomIV
|
|
+ return $ EncryptedCookie
|
|
+ ( encrypt key iv1 a,
|
|
+ encrypt key iv2 b,
|
|
+ encrypt key iv3 c,
|
|
+ encryptMaybe key iv4 d)
|
|
+
|
|
+decryptCookie' :: CookieT -> IO Cookie
|
|
+decryptCookie' (a,b,c,d) = do
|
|
+ key <- getKey "CookieKey.key"
|
|
+ return $ UnEncryptedCookie
|
|
+ ( decryptFM key a,
|
|
+ decryptFM key b,
|
|
+ decryptFM key c,
|
|
+ decryptMaybe key d)
|
|
+
|
|
+encryptMaybe :: Key -> IV -> Maybe ByteString -> Maybe ByteString
|
|
+encryptMaybe k i (Just s) = Just $ encrypt k i s
|
|
+encryptMaybe _ _ Nothing = Nothing
|
|
+
|
|
+decryptMaybe :: Key -> Maybe ByteString -> Maybe ByteString
|
|
+decryptMaybe k (Just s) = Just $ fromMaybe "" $ decrypt k s
|
|
+decryptMaybe _ Nothing = Nothing
|
|
+
|
|
+decryptFM :: Key -> ByteString -> ByteString
|
|
+decryptFM k b = fromMaybe "" $ decrypt k b
|
|
+
|
|
+cookieToTuple :: Cookie -> CookieT
|
|
+cookieToTuple (UnEncryptedCookie c) = c
|
|
+cookieToTuple (EncryptedCookie c) = c
|
|
+cookieToTuple (ParanoidCookie c) = c
|
|
+
|
|
+decryptAndToTuple :: Cookie -> CookieT
|
|
+decryptAndToTuple = cookieToTuple . unsafePerformIO . decryptCookie
|
|
diff -ru orig/src/MFlow/Forms/Admin.hs new/src/MFlow/Forms/Admin.hs
|
|
--- orig/src/MFlow/Forms/Admin.hs 2014-06-10 05:51:26.985015856 +0300
|
|
+++ new/src/MFlow/Forms/Admin.hs 2014-06-10 05:51:25.000000000 +0300
|
|
@@ -1,175 +1,175 @@
|
|
-{-# OPTIONS
|
|
- -XScopedTypeVariables
|
|
-
|
|
- #-}
|
|
-module MFlow.Forms.Admin(adminLoop, wait, addAdminWF) where
|
|
+{-# OPTIONS
|
|
+ -XScopedTypeVariables
|
|
+
|
|
+ #-}
|
|
+module MFlow.Forms.Admin(adminLoop, wait, addAdminWF) where
|
|
import MFlow.Forms
|
|
import MFlow
|
|
-import MFlow.Forms.Blaze.Html
|
|
-import Text.Blaze.Html5 hiding (map)
|
|
-import Control.Applicative
|
|
-import Control.Workflow
|
|
-import Control.Monad.Trans
|
|
-import Data.TCache
|
|
-import Data.TCache.IndexQuery
|
|
-import System.Exit
|
|
-import System.IO
|
|
-import System.IO.Unsafe
|
|
-import Data.ByteString.Lazy.Char8 as B (unpack,tail,hGetNonBlocking,append, pack)
|
|
-import System.IO
|
|
-import Data.RefSerialize hiding ((<|>))
|
|
-import Data.Typeable
|
|
-import Data.Monoid
|
|
-import Data.Maybe
|
|
-import Data.Map as M (keys, toList)
|
|
-import System.Exit
|
|
-import Control.Exception as E
|
|
-import Control.Concurrent
|
|
-import Control.Concurrent.MVar
|
|
-import GHC.Conc
|
|
-
|
|
-
|
|
-ssyncCache= putStr "sync..." >> syncCache >> putStrLn "done"
|
|
-
|
|
--- | A small console interpreter with some commands:
|
|
---
|
|
--- [@sync@] Synchronize the cache with persistent storage (see `syncCache`)
|
|
---
|
|
--- [@flush@] Flush the cache
|
|
---
|
|
--- [@end@] Synchronize and exit
|
|
---
|
|
--- [@abort@] Exit. Do not synchronize
|
|
---
|
|
--- on exception, for example Control-c, it sync and exits.
|
|
--- It must be used as the last statement of the main procedure.
|
|
-adminLoop :: IO ()
|
|
-adminLoop= do
|
|
- msgs <- getMessageFlows
|
|
- putStrLn ""
|
|
- putStrLn $ "Served:"
|
|
- mapM putStrLn [ " http://server:port/"++ i | i <- M.keys msgs]
|
|
- putStrLn ""
|
|
- putStrLn "Commands: sync, flush, end, abort"
|
|
- adminLoop1
|
|
- `E.catch` (\(e:: E.SomeException) ->do
|
|
- ssyncCache
|
|
- error $ "\nException: "++ show e)
|
|
-
|
|
-adminLoop1= do
|
|
- putStr ">"; hFlush stdout
|
|
- op <- getLine
|
|
- case op of
|
|
- "sync" -> ssyncCache
|
|
- "flush" -> atomically flushAll >> putStrLn "flushed cache"
|
|
- "end" -> ssyncCache >> putStrLn "bye" >> exitWith ExitSuccess
|
|
- "abort" -> exitWith ExitSuccess
|
|
- _ -> return()
|
|
- adminLoop1
|
|
-
|
|
--- | execute the process and wait for its finalization.
|
|
--- then it synchronizes the cache
|
|
+import MFlow.Forms.Blaze.Html
|
|
+import Text.Blaze.Html5 hiding (map)
|
|
+import Control.Applicative
|
|
+import Control.Workflow
|
|
+import Control.Monad.Trans
|
|
+import Data.TCache
|
|
+import Data.TCache.IndexQuery
|
|
+import System.Exit
|
|
+import System.IO
|
|
+import System.IO.Unsafe
|
|
+import Data.ByteString.Lazy.Char8 as B (unpack,tail,hGetNonBlocking,append, pack)
|
|
+import System.IO
|
|
+import Data.RefSerialize hiding ((<|>))
|
|
+import Data.Typeable
|
|
+import Data.Monoid
|
|
+import Data.Maybe
|
|
+import Data.Map as M (keys, toList)
|
|
+import System.Exit
|
|
+import Control.Exception as E
|
|
+import Control.Concurrent
|
|
+import Control.Concurrent.MVar
|
|
+import GHC.Conc
|
|
+
|
|
+
|
|
+ssyncCache= putStr "sync..." >> syncCache >> putStrLn "done"
|
|
+
|
|
+-- | A small console interpreter with some commands:
|
|
+--
|
|
+-- [@sync@] Synchronize the cache with persistent storage (see `syncCache`)
|
|
+--
|
|
+-- [@flush@] Flush the cache
|
|
+--
|
|
+-- [@end@] Synchronize and exit
|
|
+--
|
|
+-- [@abort@] Exit. Do not synchronize
|
|
+--
|
|
+-- on exception, for example Control-c, it sync and exits.
|
|
+-- It must be used as the last statement of the main procedure.
|
|
+adminLoop :: IO ()
|
|
+adminLoop= do
|
|
+ msgs <- getMessageFlows
|
|
+ putStrLn ""
|
|
+ putStrLn $ "Served:"
|
|
+ mapM putStrLn [ " http://server:port/"++ i | i <- M.keys msgs]
|
|
+ putStrLn ""
|
|
+ putStrLn "Commands: sync, flush, end, abort"
|
|
+ adminLoop1
|
|
+ `E.catch` (\(e:: E.SomeException) ->do
|
|
+ ssyncCache
|
|
+ error $ "\nException: "++ show e)
|
|
+
|
|
+adminLoop1= do
|
|
+ putStr ">"; hFlush stdout
|
|
+ op <- getLine
|
|
+ case op of
|
|
+ "sync" -> ssyncCache
|
|
+ "flush" -> atomically flushAll >> putStrLn "flushed cache"
|
|
+ "end" -> ssyncCache >> putStrLn "bye" >> exitWith ExitSuccess
|
|
+ "abort" -> exitWith ExitSuccess
|
|
+ _ -> return()
|
|
+ adminLoop1
|
|
+
|
|
+-- | execute the process and wait for its finalization.
|
|
+-- then it synchronizes the cache
|
|
wait f= do
|
|
putChar '\n'
|
|
putStrLn "Using configuration: "
|
|
mapM_ putStrLn [k ++"= "++ show v | (k,v) <- M.toList config]
|
|
- putChar '\n'
|
|
- mv <- newEmptyMVar
|
|
- forkIO (f1 >> putMVar mv True)
|
|
- putStrLn "wait: ready"
|
|
+ putChar '\n'
|
|
+ mv <- newEmptyMVar
|
|
+ forkIO (f1 >> putMVar mv True)
|
|
+ putStrLn "wait: ready"
|
|
takeMVar mv
|
|
- return ()
|
|
- `E.catch` (\(e:: E.SomeException) ->do
|
|
- ssyncCache
|
|
- error $ "Signal: "++ show e)
|
|
-
|
|
- where
|
|
- f1= do
|
|
- mv <- newEmptyMVar
|
|
- n <- getNumProcessors
|
|
- putStr "Running in "
|
|
- putStr $ show n
|
|
- putStrLn " core(s)"
|
|
- hFlush stdout
|
|
- f
|
|
-
|
|
--- | Install the admin flow in the list of flows handled by `HackMessageFlow`
|
|
--- this gives access to an administrator page. It is necessary to
|
|
--- create an admin user with `setAdminUser`.
|
|
---
|
|
--- The administration page is reached with the path \"adminserv\"
|
|
-addAdminWF= addMessageFlows[("adminserv", runFlow $ transientNav adminMFlow)]
|
|
-
|
|
-
|
|
-adminMFlow :: FlowM Html IO ()
|
|
-adminMFlow= do
|
|
- let admin = getAdminName
|
|
- u <- getUser (Just admin) $ p << b << "Please login as Administrator" ++> userLogin
|
|
- op <- ask $ p <<< wlink "sync" (b << "sync")
|
|
- <|> p <<< wlink "flush" (b << "flush")
|
|
- <|> p <<< wlink "errors"(b << "errors")
|
|
- <|> p <<< wlink "users" (b << "users")
|
|
- <|> p <<< wlink "end" (b << "end")
|
|
- <|> wlink "abort" (b << "abort")
|
|
-
|
|
- case op of
|
|
- "users" -> users
|
|
- "sync" -> liftIO $ syncCache >> print "syncronized cache"
|
|
- "flush" -> liftIO $ atomically flushAll >> print "flushed cache"
|
|
-
|
|
- "errors" -> errors
|
|
- "end" -> liftIO $ syncCache >> print "bye" >> exitWith(ExitSuccess)
|
|
- "abort" -> liftIO $ exitWith(ExitSuccess)
|
|
- _ -> return()
|
|
- adminMFlow
|
|
-
|
|
-
|
|
-errors= do
|
|
- size <- liftIO $ hFileSize hlog
|
|
- if size == 0
|
|
- then ask $ wlink () (b << "no error log")
|
|
- else do
|
|
- liftIO $ hSeek hlog AbsoluteSeek 0
|
|
- log <- liftIO $ hGetNonBlocking hlog (fromIntegral size)
|
|
-
|
|
- let ls :: [[String ]]= runR readp $ pack "[" `append` (B.tail log) `append` pack "]"
|
|
- let rows= [wlink (Prelude.head e) (b << Prelude.head e) `waction` optionsUser : map (\x ->noWidget <++ fromStr x) (Prelude.tail e) | e <- ls]
|
|
- showFormList rows 0 10
|
|
- breturn()
|
|
-
|
|
-
|
|
-
|
|
-
|
|
-
|
|
-
|
|
-
|
|
-users= do
|
|
- users <- liftIO $ atomically $ return . map fst =<< indexOf userName
|
|
-
|
|
- showFormList [[wlink u (b << u) `waction` optionsUser ] | u<- users] 0 10
|
|
-
|
|
-showFormList
|
|
- :: [[View Html IO ()]]
|
|
- -> Int -> Int -> FlowM Html IO b
|
|
-showFormList ls n l= do
|
|
- nav <- ask $ updown n l <|> (list **> updown n l)
|
|
- showFormList ls nav l
|
|
-
|
|
- where
|
|
- list= table <<< firstOf (span1 n l [tr <<< cols e | e <- ls ])
|
|
-
|
|
- cols e= firstOf[td <<< c | c <- e]
|
|
- span1 n l = take l . drop n
|
|
- updown n l= wlink ( n +l) (b << "up ") <|> wlink ( n -l) (b << "down ") <++ br
|
|
-
|
|
-optionsUser us = do
|
|
- wfs <- liftIO $ return . M.keys =<< getMessageFlows
|
|
- stats <- let u= undefined
|
|
- in liftIO $ mapM (\wf -> getWFHistory wf (Token wf us u u u u u u)) wfs
|
|
- let wfss= filter (isJust . snd) $ zip wfs stats
|
|
- if null wfss
|
|
- then ask $ b << " not logs for this user" ++> wlink () (b << "Press here")
|
|
- else do
|
|
- wf <- ask $ firstOf [ wlink wf (p << wf) | (wf,_) <- wfss]
|
|
- ask $ p << unpack (showHistory . fromJust . fromJust $ lookup wf wfss) ++> wlink () (p << "press to menu")
|
|
-
|
|
+ return ()
|
|
+ `E.catch` (\(e:: E.SomeException) ->do
|
|
+ ssyncCache
|
|
+ error $ "Signal: "++ show e)
|
|
+
|
|
+ where
|
|
+ f1= do
|
|
+ mv <- newEmptyMVar
|
|
+ n <- getNumProcessors
|
|
+ putStr "Running in "
|
|
+ putStr $ show n
|
|
+ putStrLn " core(s)"
|
|
+ hFlush stdout
|
|
+ f
|
|
+
|
|
+-- | Install the admin flow in the list of flows handled by `HackMessageFlow`
|
|
+-- this gives access to an administrator page. It is necessary to
|
|
+-- create an admin user with `setAdminUser`.
|
|
+--
|
|
+-- The administration page is reached with the path \"adminserv\"
|
|
+addAdminWF= addMessageFlows[("adminserv", runFlow $ transientNav adminMFlow)]
|
|
+
|
|
+
|
|
+adminMFlow :: FlowM Html IO ()
|
|
+adminMFlow= do
|
|
+ let admin = getAdminName
|
|
+ u <- getUser (Just admin) $ p << b << "Please login as Administrator" ++> userLogin
|
|
+ op <- ask $ p <<< wlink "sync" (b << "sync")
|
|
+ <|> p <<< wlink "flush" (b << "flush")
|
|
+ <|> p <<< wlink "errors"(b << "errors")
|
|
+ <|> p <<< wlink "users" (b << "users")
|
|
+ <|> p <<< wlink "end" (b << "end")
|
|
+ <|> wlink "abort" (b << "abort")
|
|
+
|
|
+ case op of
|
|
+ "users" -> users
|
|
+ "sync" -> liftIO $ syncCache >> print "syncronized cache"
|
|
+ "flush" -> liftIO $ atomically flushAll >> print "flushed cache"
|
|
+
|
|
+ "errors" -> errors
|
|
+ "end" -> liftIO $ syncCache >> print "bye" >> exitWith(ExitSuccess)
|
|
+ "abort" -> liftIO $ exitWith(ExitSuccess)
|
|
+ _ -> return()
|
|
+ adminMFlow
|
|
+
|
|
+
|
|
+errors= do
|
|
+ size <- liftIO $ hFileSize hlog
|
|
+ if size == 0
|
|
+ then ask $ wlink () (b << "no error log")
|
|
+ else do
|
|
+ liftIO $ hSeek hlog AbsoluteSeek 0
|
|
+ log <- liftIO $ hGetNonBlocking hlog (fromIntegral size)
|
|
+
|
|
+ let ls :: [[String ]]= runR readp $ pack "[" `append` (B.tail log) `append` pack "]"
|
|
+ let rows= [wlink (Prelude.head e) (b << Prelude.head e) `waction` optionsUser : map (\x ->noWidget <++ fromStr x) (Prelude.tail e) | e <- ls]
|
|
+ showFormList rows 0 10
|
|
+ breturn()
|
|
+
|
|
+
|
|
+
|
|
+
|
|
+
|
|
+
|
|
+
|
|
+users= do
|
|
+ users <- liftIO $ atomically $ return . map fst =<< indexOf userName
|
|
+
|
|
+ showFormList [[wlink u (b << u) `waction` optionsUser ] | u<- users] 0 10
|
|
+
|
|
+showFormList
|
|
+ :: [[View Html IO ()]]
|
|
+ -> Int -> Int -> FlowM Html IO b
|
|
+showFormList ls n l= do
|
|
+ nav <- ask $ updown n l <|> (list **> updown n l)
|
|
+ showFormList ls nav l
|
|
+
|
|
+ where
|
|
+ list= table <<< firstOf (span1 n l [tr <<< cols e | e <- ls ])
|
|
+
|
|
+ cols e= firstOf[td <<< c | c <- e]
|
|
+ span1 n l = take l . drop n
|
|
+ updown n l= wlink ( n +l) (b << "up ") <|> wlink ( n -l) (b << "down ") <++ br
|
|
+
|
|
+optionsUser us = do
|
|
+ wfs <- liftIO $ return . M.keys =<< getMessageFlows
|
|
+ stats <- let u= undefined
|
|
+ in liftIO $ mapM (\wf -> getWFHistory wf (Token wf us u u u u u u)) wfs
|
|
+ let wfss= filter (isJust . snd) $ zip wfs stats
|
|
+ if null wfss
|
|
+ then ask $ b << " not logs for this user" ++> wlink () (b << "Press here")
|
|
+ else do
|
|
+ wf <- ask $ firstOf [ wlink wf (p << wf) | (wf,_) <- wfss]
|
|
+ ask $ p << unpack (showHistory . fromJust . fromJust $ lookup wf wfss) ++> wlink () (p << "press to menu")
|
|
+
|
|
diff -ru orig/src/MFlow/Forms/Blaze/Html.hs new/src/MFlow/Forms/Blaze/Html.hs
|
|
--- orig/src/MFlow/Forms/Blaze/Html.hs 2014-06-10 05:51:26.989015856 +0300
|
|
+++ new/src/MFlow/Forms/Blaze/Html.hs 2014-06-10 05:51:25.000000000 +0300
|
|
@@ -58,7 +58,7 @@
|
|
in if msel then tag ! selected (toValue ("" ::String)) else tag
|
|
|
|
|
|
- formAction action form = St.form ! acceptCharset "UTF-8" ! At.action (toValue action) ! method (toValue ("post" :: String)) $ form
|
|
+ formAction action method1 form = St.form ! acceptCharset "UTF-8" ! At.action (toValue action) ! method (toValue method1) $ form
|
|
|
|
fromStr= toMarkup
|
|
fromStrNoEncode = preEscapedToMarkup
|
|
diff -ru orig/src/MFlow/Forms/Internals.hs new/src/MFlow/Forms/Internals.hs
|
|
--- orig/src/MFlow/Forms/Internals.hs 2014-06-10 05:51:26.981015856 +0300
|
|
+++ new/src/MFlow/Forms/Internals.hs 2014-06-10 05:51:25.000000000 +0300
|
|
@@ -1,313 +1,345 @@
|
|
------------------------------------------------------------------------------
|
|
---
|
|
--- Module : MFlow.Forms.Internals
|
|
--- Copyright :
|
|
--- License : BSD3
|
|
---
|
|
--- Maintainer : agocorona@gmail.com
|
|
--- Stability : experimental
|
|
--- Portability :
|
|
---
|
|
--- |
|
|
---
|
|
------------------------------------------------------------------------------
|
|
-{-# OPTIONS -XDeriveDataTypeable
|
|
- -XExistentialQuantification
|
|
- -XScopedTypeVariables
|
|
- -XFlexibleInstances
|
|
- -XUndecidableInstances
|
|
- -XMultiParamTypeClasses
|
|
- -XGeneralizedNewtypeDeriving
|
|
- -XFlexibleContexts
|
|
- -XOverlappingInstances
|
|
- -XRecordWildCards
|
|
-#-}
|
|
-
|
|
-module MFlow.Forms.Internals where
|
|
-import MFlow
|
|
-import MFlow.Cookies
|
|
-import Control.Applicative
|
|
-import Data.Monoid
|
|
-import Control.Monad.Trans
|
|
-import Control.Monad.State
|
|
-import Data.ByteString.Lazy.UTF8 as B hiding (length, foldr, take)
|
|
-import qualified Data.ByteString.UTF8 as SB
|
|
-import Data.Typeable
|
|
-import Data.RefSerialize hiding((<|>))
|
|
-import Data.TCache
|
|
-import Data.TCache.Memoization
|
|
-import Data.TCache.DefaultPersistence
|
|
-import Data.TCache.Memoization
|
|
-import Data.Dynamic
|
|
-import qualified Data.Map as M
|
|
-import Unsafe.Coerce
|
|
-import Control.Workflow as WF
|
|
-import Control.Monad.Identity
|
|
-import Data.List
|
|
-import System.IO.Unsafe
|
|
-import Control.Concurrent.MVar
|
|
+-----------------------------------------------------------------------------
|
|
+--
|
|
+-- Module : MFlow.Forms.Internals
|
|
+-- Copyright :
|
|
+-- License : BSD3
|
|
+--
|
|
+-- Maintainer : agocorona@gmail.com
|
|
+-- Stability : experimental
|
|
+-- Portability :
|
|
+--
|
|
+-- |
|
|
+--
|
|
+-----------------------------------------------------------------------------
|
|
+{-# OPTIONS -XDeriveDataTypeable
|
|
+ -XExistentialQuantification
|
|
+ -XScopedTypeVariables
|
|
+ -XFlexibleInstances
|
|
+ -XUndecidableInstances
|
|
+ -XMultiParamTypeClasses
|
|
+ -XGeneralizedNewtypeDeriving
|
|
+ -XFlexibleContexts
|
|
+ -XOverlappingInstances
|
|
+ -XRecordWildCards
|
|
+#-}
|
|
+
|
|
+module MFlow.Forms.Internals where
|
|
+import MFlow
|
|
+import MFlow.Cookies
|
|
+import Control.Applicative
|
|
+import Data.Monoid
|
|
+import Control.Monad.Trans
|
|
+import Control.Monad.State
|
|
+import Data.ByteString.Lazy.UTF8 as B hiding (length, foldr, take)
|
|
+import qualified Data.ByteString.UTF8 as SB
|
|
+import Data.Typeable
|
|
+import Data.RefSerialize hiding((<|>))
|
|
+import Data.TCache
|
|
+import Data.TCache.Memoization
|
|
+import Data.TCache.DefaultPersistence
|
|
+import Data.TCache.Memoization
|
|
+import Data.Dynamic
|
|
+import qualified Data.Map as M
|
|
+import Unsafe.Coerce
|
|
+import Control.Workflow as WF
|
|
+import Control.Monad.Identity
|
|
+import Data.List
|
|
+import System.IO.Unsafe
|
|
+import Control.Concurrent.MVar
|
|
import qualified Data.Text as T
|
|
import Data.Char
|
|
import Data.List(stripPrefix)
|
|
import Data.Maybe(isJust)
|
|
import Control.Concurrent.STM
|
|
import Data.TCache.Memoization
|
|
-
|
|
---
|
|
----- for traces
|
|
---
|
|
-
|
|
-import Control.Exception as CE
|
|
-import Control.Concurrent
|
|
-import Control.Monad.Loc
|
|
-
|
|
--- debug
|
|
---import Debug.Trace
|
|
---(!>) = flip trace
|
|
-
|
|
-
|
|
-data FailBack a = BackPoint a | NoBack a | GoBack deriving (Show,Typeable)
|
|
-
|
|
-
|
|
-instance (Serialize a) => Serialize (FailBack a ) where
|
|
- showp (BackPoint x)= insertString (fromString iCanFailBack) >> showp x
|
|
- showp (NoBack x) = insertString (fromString noFailBack) >> showp x
|
|
- showp GoBack = insertString (fromString repeatPlease)
|
|
-
|
|
- readp = choice [icanFailBackp,repeatPleasep,noFailBackp]
|
|
- where
|
|
- noFailBackp = symbol noFailBack >> readp >>= return . NoBack
|
|
- icanFailBackp = symbol iCanFailBack >> readp >>= return . BackPoint
|
|
- repeatPleasep = symbol repeatPlease >> return GoBack
|
|
-
|
|
-iCanFailBack= "B"
|
|
-repeatPlease= "G"
|
|
-noFailBack= "N"
|
|
-
|
|
-newtype Sup m a = Sup { runSup :: m (FailBack a ) }
|
|
-
|
|
-class MonadState s m => Supervise s m where
|
|
- supBack :: s -> m () -- called before backtracing. state passed is the previous
|
|
- supBack = const $ return () -- By default the state passed is the last one
|
|
-
|
|
- supervise :: m (FailBack a) -> m (FailBack a)
|
|
- supervise= id
|
|
-
|
|
-
|
|
-
|
|
-instance (Supervise s m)=> Monad (Sup m) where
|
|
- fail _ = Sup . return $ GoBack
|
|
- return x = Sup . return $ NoBack x
|
|
- x >>= f = Sup $ loop
|
|
- where
|
|
- loop = do
|
|
- s <- get
|
|
- v <- supervise $ runSup x -- !> "loop"
|
|
- case v of
|
|
- NoBack y -> supervise $ runSup (f y) -- !> "runback"
|
|
- BackPoint y -> do
|
|
- z <- supervise $ runSup (f y) -- !> "BACK"
|
|
- case z of
|
|
- GoBack -> supBack s >> loop -- !> "BACKTRACKING"
|
|
- other -> return other
|
|
- GoBack -> return $ GoBack
|
|
-
|
|
-
|
|
-fromFailBack (NoBack x) = x
|
|
-fromFailBack (BackPoint x)= x
|
|
-toFailBack x= NoBack x
|
|
-
|
|
-
|
|
--- | the FlowM monad executes the page navigation. It perform Backtracking when necessary to syncronize
|
|
--- when the user press the back button or when the user enter an arbitrary URL. The instruction pointer
|
|
--- is moved to the right position within the procedure to handle the request.
|
|
---
|
|
--- However this is transparent to the programmer, who codify in the style of a console application.
|
|
-newtype FlowM v m a= FlowM {runFlowM :: FlowMM v m a} deriving (Monad,MonadIO,Functor,MonadState(MFlowState v))
|
|
-flowM= FlowM
|
|
---runFlowM= runView
|
|
-
|
|
-{-# NOINLINE breturn #-}
|
|
-
|
|
--- | Use this instead of return to return from a computation with ask statements
|
|
---
|
|
--- This way when the user press the back button, the computation will execute back, to
|
|
--- the returned code, according with the user navigation.
|
|
-breturn :: (Monad m) => a -> FlowM v m a
|
|
-breturn = flowM . Sup . return . BackPoint -- !> "breturn"
|
|
-
|
|
-
|
|
-instance (Supervise s m,MonadIO m) => MonadIO (Sup m) where
|
|
- liftIO f= Sup $ liftIO f >>= \ x -> return $ NoBack x
|
|
-
|
|
-instance (Monad m,Functor m) => Functor (Sup m) where
|
|
- fmap f g= Sup $ do
|
|
- mr <- runSup g
|
|
- case mr of
|
|
- BackPoint x -> return . BackPoint $ f x
|
|
- NoBack x -> return . NoBack $ f x
|
|
- GoBack -> return $ GoBack
|
|
-
|
|
-
|
|
-liftSup f = Sup $ f >>= \x -> return $ NoBack x
|
|
-instance MonadTrans Sup where
|
|
- lift f = Sup $ f >>= \x -> return $ NoBack x
|
|
-
|
|
-
|
|
-instance (Supervise s m,MonadState s m) => MonadState s (Sup m) where
|
|
- get= lift get -- !> "get"
|
|
- put= lift . put
|
|
-
|
|
-type WState view m = StateT (MFlowState view) m
|
|
-type FlowMM view m= Sup (WState view m)
|
|
-
|
|
-data FormElm view a = FormElm view (Maybe a) deriving Typeable
|
|
-
|
|
-instance (Monoid view,Serialize a) => Serialize (FormElm view a) where
|
|
- showp (FormElm _ x)= showp x
|
|
- readp= readp >>= \x -> return $ FormElm mempty x
|
|
-
|
|
--- | @View v m a@ is a widget (formlet) with formatting `v` running the monad `m` (usually `IO`) and which return a value of type `a`
|
|
---
|
|
--- It has 'Applicative', 'Alternative' and 'Monad' instances.
|
|
---
|
|
--- Things to know about these instances:
|
|
---
|
|
--- If the View expression does not validate, ask will present the page again.
|
|
---
|
|
--- /Alternative instance/: Both alternatives are executed. The rest is as usual
|
|
---
|
|
--- /Monad Instance/:
|
|
---
|
|
--- The rendering of each statement is added to the previous. If you want to avoid this, use 'wcallback'
|
|
---
|
|
--- The execution is stopped when the statement has a formlet-widget that does not validate and
|
|
--- return an invalid response (So it will present the page again if no other widget in the expression validates).
|
|
---
|
|
--- The monadic code is executed from the beginning each time the page is presented or refreshed
|
|
---
|
|
--- use 'pageFlow' if your page has more than one monadic computation with dynamic behaviour
|
|
---
|
|
--- use 'pageFlow' to identify each subflow branch of a conditional
|
|
---
|
|
--- For example:
|
|
---
|
|
--- > pageFlow "myid" $ do
|
|
--- > r <- formlet1
|
|
--- > liftIO $ ioaction1 r
|
|
--- > s <- formlet2
|
|
--- > liftIO $ ioaction2 s
|
|
--- > case s of
|
|
--- > True -> pageFlow "idtrue" $ do ....
|
|
--- > False -> paeFlow "idfalse" $ do ...
|
|
--- > ...
|
|
---
|
|
--- Here if @formlet2@ do not validate, @ioaction2@ is not executed. But if @formLet1@ validates and the
|
|
--- page is refreshed two times (because @formlet2@ has failed, see above),then @ioaction1@ is executed two times.
|
|
--- use 'cachedByKey' if you want to avoid repeated IO executions.
|
|
-newtype View v m a = View { runView :: WState v m (FormElm v a)}
|
|
-
|
|
-
|
|
-instance Monad m => Supervise (MFlowState v) (WState v m) where
|
|
- supBack st= do -- the previous state is recovered, with the exception of these fields:
|
|
- MFlowState{..} <- get
|
|
- put st{ mfEnv= mfEnv,mfToken=mfToken
|
|
- , mfPath=mfPath
|
|
- , mfData=mfData
|
|
- , mfTrace= mfTrace
|
|
+
|
|
+--
|
|
+---- for traces
|
|
+--
|
|
+
|
|
+import Control.Exception as CE
|
|
+import Control.Concurrent
|
|
+import Control.Monad.Loc
|
|
+
|
|
+-- debug
|
|
+import Debug.Trace
|
|
+(!>) = flip trace
|
|
+
|
|
+
|
|
+data FailBack a = BackPoint a | NoBack a | GoBack deriving (Show,Typeable)
|
|
+
|
|
+instance Functor FailBack where
|
|
+ fmap f GoBack= GoBack
|
|
+ fmap f (BackPoint x)= BackPoint $ f x
|
|
+ fmap f (NoBack x)= NoBack $ f x
|
|
+
|
|
+instance Applicative FailBack where
|
|
+ pure x = NoBack x
|
|
+ _ <*> GoBack = GoBack
|
|
+ GoBack <*> _ = GoBack
|
|
+ k <*> x = NoBack $ (fromFailBack k) (fromFailBack x)
|
|
+
|
|
+instance Alternative FailBack where
|
|
+ empty= GoBack
|
|
+ GoBack <|> f = f
|
|
+ f <|> _ = f
|
|
+
|
|
+instance (Serialize a) => Serialize (FailBack a ) where
|
|
+ showp (BackPoint x)= insertString (fromString iCanFailBack) >> showp x
|
|
+ showp (NoBack x) = insertString (fromString noFailBack) >> showp x
|
|
+ showp GoBack = insertString (fromString repeatPlease)
|
|
+
|
|
+ readp = choice [icanFailBackp,repeatPleasep,noFailBackp]
|
|
+ where
|
|
+ noFailBackp = symbol noFailBack >> readp >>= return . NoBack
|
|
+ icanFailBackp = symbol iCanFailBack >> readp >>= return . BackPoint
|
|
+ repeatPleasep = symbol repeatPlease >> return GoBack
|
|
+
|
|
+iCanFailBack= "B"
|
|
+repeatPlease= "G"
|
|
+noFailBack= "N"
|
|
+
|
|
+newtype Sup m a = Sup { runSup :: m (FailBack a ) }
|
|
+
|
|
+class MonadState s m => Supervise s m where
|
|
+ supBack :: s -> m () -- called before backtracing. state passed is the previous
|
|
+ supBack = const $ return () -- By default the state passed is the last one
|
|
+
|
|
+ supervise :: m (FailBack a) -> m (FailBack a)
|
|
+ supervise= id
|
|
+
|
|
+
|
|
+
|
|
+instance (Supervise s m)=> Monad (Sup m) where
|
|
+ fail _ = Sup . return $ GoBack
|
|
+ return x = Sup . return $ NoBack x
|
|
+ x >>= f = Sup $ loop
|
|
+ where
|
|
+ loop = do
|
|
+ s <- get
|
|
+ v <- supervise $ runSup x -- !> "loop"
|
|
+ case v of
|
|
+ NoBack y -> supervise $ runSup (f y) -- !> "runback"
|
|
+ BackPoint y -> do
|
|
+ z <- supervise $ runSup (f y) -- !> "BACK"
|
|
+ case z of
|
|
+ GoBack -> supBack s >> loop -- !> "BACKTRACKING"
|
|
+ other -> return other
|
|
+ GoBack -> return $ GoBack
|
|
+
|
|
+
|
|
+fromFailBack (NoBack x) = x
|
|
+fromFailBack (BackPoint x)= x
|
|
+toFailBack x= NoBack x
|
|
+
|
|
+instance (Monad m,Applicative m) => Applicative (Sup m) where
|
|
+ pure x = Sup . return $ NoBack x
|
|
+ f <*> g= Sup $ do
|
|
+ k <- runSup f
|
|
+ x <- runSup g
|
|
+ return $ k <*> x
|
|
+
|
|
+instance(Monad m, Applicative m) => Alternative (Sup m) where
|
|
+ empty = Sup . return $ GoBack
|
|
+ f <|> g= Sup $ do
|
|
+ x <- runSup f
|
|
+ case x of
|
|
+ GoBack -> runSup g !> "GOBACK"
|
|
+ _ -> return x
|
|
+
|
|
+-- | the FlowM monad executes the page navigation. It perform Backtracking when necessary to syncronize
|
|
+-- when the user press the back button or when the user enter an arbitrary URL. The instruction pointer
|
|
+-- is moved to the right position within the procedure to handle the request.
|
|
+--
|
|
+-- However this is transparent to the programmer, who codify in the style of a console application.
|
|
+newtype FlowM v m a= FlowM {runFlowM :: FlowMM v m a}
|
|
+ deriving (Applicative,Alternative,Monad,MonadIO,Functor
|
|
+ ,MonadState(MFlowState v))
|
|
+
|
|
+--runFlowM= runView
|
|
+
|
|
+{-# NOINLINE breturn #-}
|
|
+
|
|
+-- | Use this instead of return to return from a computation with ask statements
|
|
+--
|
|
+-- This way when the user press the back button, the computation will execute back, to
|
|
+-- the returned code, according with the user navigation.
|
|
+breturn :: (Monad m) => a -> FlowM v m a
|
|
+breturn = FlowM . Sup . return . BackPoint -- !> "breturn"
|
|
+
|
|
+
|
|
+instance (Supervise s m,MonadIO m) => MonadIO (Sup m) where
|
|
+ liftIO f= Sup $ liftIO f >>= \ x -> return $ NoBack x
|
|
+
|
|
+instance (Monad m,Functor m) => Functor (Sup m) where
|
|
+ fmap f g= Sup $ do
|
|
+ mr <- runSup g
|
|
+ case mr of
|
|
+ BackPoint x -> return . BackPoint $ f x
|
|
+ NoBack x -> return . NoBack $ f x
|
|
+ GoBack -> return $ GoBack
|
|
+
|
|
+
|
|
+liftSup f = Sup $ f >>= \x -> return $ NoBack x
|
|
+instance MonadTrans Sup where
|
|
+ lift f = Sup $ f >>= \x -> return $ NoBack x
|
|
+
|
|
+
|
|
+instance (Supervise s m,MonadState s m) => MonadState s (Sup m) where
|
|
+ get= lift get -- !> "get"
|
|
+ put= lift . put
|
|
+
|
|
+type WState view m = StateT (MFlowState view) m
|
|
+type FlowMM view m= Sup (WState view m)
|
|
+
|
|
+data FormElm view a = FormElm view (Maybe a) deriving Typeable
|
|
+
|
|
+instance (Monoid view,Serialize a) => Serialize (FormElm view a) where
|
|
+ showp (FormElm _ x)= showp x
|
|
+ readp= readp >>= \x -> return $ FormElm mempty x
|
|
+
|
|
+
|
|
+-- | @View v m a@ is a widget (formlet) with formatting `v` running the monad `m` (usually `IO`) and which return a value of type `a`
|
|
+--
|
|
+-- It has 'Applicative', 'Alternative' and 'Monad' instances.
|
|
+--
|
|
+-- Things to know about these instances:
|
|
+--
|
|
+-- If the View expression does not validate, ask will present the page again.
|
|
+--
|
|
+-- /Alternative instance/: Both alternatives are executed. The rest is as usual
|
|
+--
|
|
+-- /Monad Instance/:
|
|
+--
|
|
+-- The rendering of each statement is added to the previous. If you want to avoid this, use 'wcallback'
|
|
+--
|
|
+-- The execution is stopped when the statement has a formlet-widget that does not validate and
|
|
+-- return an invalid response (So it will present the page again if no other widget in the expression validates).
|
|
+--
|
|
+-- The monadic code is executed from the beginning each time the page is presented or refreshed
|
|
+--
|
|
+-- use 'pageFlow' if your page has more than one monadic computation with dynamic behaviour
|
|
+--
|
|
+-- use 'pageFlow' to identify each subflow branch of a conditional
|
|
+--
|
|
+-- For example:
|
|
+--
|
|
+-- > pageFlow "myid" $ do
|
|
+-- > r <- formlet1
|
|
+-- > liftIO $ ioaction1 r
|
|
+-- > s <- formlet2
|
|
+-- > liftIO $ ioaction2 s
|
|
+-- > case s of
|
|
+-- > True -> pageFlow "idtrue" $ do ....
|
|
+-- > False -> paeFlow "idfalse" $ do ...
|
|
+-- > ...
|
|
+--
|
|
+-- Here if @formlet2@ do not validate, @ioaction2@ is not executed. But if @formLet1@ validates and the
|
|
+-- page is refreshed two times (because @formlet2@ has failed, see above),then @ioaction1@ is executed two times.
|
|
+-- use 'cachedByKey' if you want to avoid repeated IO executions.
|
|
+newtype View v m a = View { runView :: WState v m (FormElm v a)}
|
|
+
|
|
+
|
|
+instance Monad m => Supervise (MFlowState v) (WState v m) where
|
|
+ supBack st= do -- the previous state is recovered, with the exception of these fields:
|
|
+ MFlowState{..} <- get
|
|
+ put st{ mfEnv= mfEnv,mfToken=mfToken
|
|
+ , mfPath=mfPath
|
|
+ , mfData=mfData
|
|
+ , mfTrace= mfTrace
|
|
, inSync=False
|
|
- , newAsk=False}
|
|
-
|
|
-
|
|
-
|
|
-
|
|
-instance MonadLoc (FlowM v IO) where
|
|
- withLoc loc f = FlowM . Sup $ do
|
|
- withLoc loc $ do
|
|
- s <- get
|
|
- (r,s') <- lift $ do
|
|
- rs@(r,s') <- runStateT (runSup (runFlowM f) ) s
|
|
- `CE.catch` (handler1 loc s)
|
|
- case mfTrace s' of
|
|
- [] -> return rs
|
|
- trace -> return(r, s'{mfTrace= loc:trace})
|
|
- put s'
|
|
- return r
|
|
-
|
|
- where
|
|
- handler1 loc s (e :: SomeException)= do
|
|
- case CE.fromException e :: Maybe WFErrors of
|
|
- Just e -> CE.throw e -- !> ("TROWNF=" ++ show e)
|
|
- Nothing ->
|
|
- case CE.fromException e :: Maybe AsyncException of
|
|
- Just e -> CE.throw e -- !> ("TROWN ASYNCF=" ++ show e)
|
|
- Nothing ->
|
|
- return (GoBack, s{mfTrace= [show e]})
|
|
-
|
|
-
|
|
---instance (Serialize a,Typeable a, FormInput v) => MonadLoc (FlowM v (Workflow IO)) a where
|
|
--- withLoc loc f = FlowM . Sup $
|
|
--- withLoc loc $ do
|
|
--- s <- get
|
|
--- (r,s') <- lift . WF.step $ exec1d "jkkjk" ( runStateT (runSup $ runFlowM f) s) `CMT.catch` (handler1 loc s)
|
|
--- put s'
|
|
--- return r
|
|
---
|
|
--- where
|
|
--- handler1 loc s (e :: SomeException)=
|
|
--- return (GoBack, s{mfTrace= Just ["exception: " ++show e]})
|
|
-
|
|
-instance FormInput v => MonadLoc (View v IO) where
|
|
- withLoc loc f = View $ do
|
|
- withLoc loc $ do
|
|
- s <- get
|
|
- (r,s') <- lift $ do
|
|
- rs@(r,s') <- runStateT (runView f) s
|
|
- `CE.catch` (handler1 loc s)
|
|
- case mfTrace s' of
|
|
- [] -> return rs
|
|
- trace -> return(r, s'{mfTrace= loc:trace})
|
|
- put s'
|
|
- return r
|
|
-
|
|
- where
|
|
- handler1 loc s (e :: SomeException)= do
|
|
- case CE.fromException e :: Maybe WFErrors of
|
|
- Just e -> CE.throw e -- !> ("TROWN=" ++ show e)
|
|
- Nothing ->
|
|
- case CE.fromException e :: Maybe AsyncException of
|
|
- Just e -> CE.throw e -- !> ("TROWN ASYNC=" ++ show e)
|
|
- Nothing ->
|
|
- return (FormElm mempty Nothing, s{mfTrace= [show e]}) -- !> loc
|
|
-
|
|
-
|
|
-
|
|
-
|
|
-
|
|
-
|
|
-
|
|
-instance Functor (FormElm view ) where
|
|
- fmap f (FormElm form x)= FormElm form (fmap f x)
|
|
-
|
|
-instance (Monad m,Functor m) => Functor (View view m) where
|
|
- fmap f x= View $ fmap (fmap f) $ runView x
|
|
-
|
|
-
|
|
-instance (Monoid view,Functor m, Monad m) => Applicative (View view m) where
|
|
- pure a = View . return . FormElm mempty $ Just a
|
|
- View f <*> View g= View $
|
|
- f >>= \(FormElm form1 k) ->
|
|
- g >>= \(FormElm form2 x) ->
|
|
- return $ FormElm (form1 `mappend` form2) (k <*> x)
|
|
-
|
|
-instance (FormInput view,Functor m, Monad m) => Alternative (View view m) where
|
|
- empty= View $ return $ FormElm mempty Nothing
|
|
+ , newAsk=False}
|
|
+
|
|
+
|
|
+
|
|
+
|
|
+instance MonadLoc (FlowM v IO) where
|
|
+ withLoc loc f = FlowM . Sup $ do
|
|
+ withLoc loc $ do
|
|
+ s <- get
|
|
+ (r,s') <- lift $ do
|
|
+ rs@(r,s') <- runStateT (runSup (runFlowM f) ) s
|
|
+ `CE.catch` (handler1 loc s)
|
|
+ case mfTrace s' of
|
|
+ [] -> return rs
|
|
+ trace -> return(r, s'{mfTrace= loc:trace})
|
|
+ put s'
|
|
+ return r
|
|
+
|
|
+ where
|
|
+ handler1 loc s (e :: SomeException)= do
|
|
+ case CE.fromException e :: Maybe WFErrors of
|
|
+ Just e -> CE.throw e -- !> ("TROWNF=" ++ show e)
|
|
+ Nothing ->
|
|
+ case CE.fromException e :: Maybe AsyncException of
|
|
+ Just e -> CE.throw e -- !> ("TROWN ASYNCF=" ++ show e)
|
|
+ Nothing ->
|
|
+ return (GoBack, s{mfTrace= [show e]})
|
|
+
|
|
+
|
|
+--instance (Serialize a,Typeable a, FormInput v) => MonadLoc (FlowM v (Workflow IO)) a where
|
|
+-- withLoc loc f = FlowM . Sup $
|
|
+-- withLoc loc $ do
|
|
+-- s <- get
|
|
+-- (r,s') <- lift . WF.step $ exec1d "jkkjk" ( runStateT (runSup $ runFlowM f) s) `CMT.catch` (handler1 loc s)
|
|
+-- put s'
|
|
+-- return r
|
|
+--
|
|
+-- where
|
|
+-- handler1 loc s (e :: SomeException)=
|
|
+-- return (GoBack, s{mfTrace= Just ["exception: " ++show e]})
|
|
+
|
|
+instance FormInput v => MonadLoc (View v IO) where
|
|
+ withLoc loc f = View $ do
|
|
+ withLoc loc $ do
|
|
+ s <- get
|
|
+ (r,s') <- lift $ do
|
|
+ rs@(r,s') <- runStateT (runView f) s
|
|
+ `CE.catch` (handler1 loc s)
|
|
+ case mfTrace s' of
|
|
+ [] -> return rs
|
|
+ trace -> return(r, s'{mfTrace= loc:trace})
|
|
+ put s'
|
|
+ return r
|
|
+
|
|
+ where
|
|
+ handler1 loc s (e :: SomeException)= do
|
|
+ case CE.fromException e :: Maybe WFErrors of
|
|
+ Just e -> CE.throw e -- !> ("TROWN=" ++ show e)
|
|
+ Nothing ->
|
|
+ case CE.fromException e :: Maybe AsyncException of
|
|
+ Just e -> CE.throw e -- !> ("TROWN ASYNC=" ++ show e)
|
|
+ Nothing ->
|
|
+ return (FormElm mempty Nothing, s{mfTrace= [show e]}) -- !> loc
|
|
+
|
|
+
|
|
+
|
|
+
|
|
+
|
|
+
|
|
+
|
|
+instance Functor (FormElm view ) where
|
|
+ fmap f (FormElm form x)= FormElm form (fmap f x)
|
|
+
|
|
+instance (Monad m,Functor m) => Functor (View view m) where
|
|
+ fmap f x= View $ fmap (fmap f) $ runView x
|
|
+
|
|
+
|
|
+instance (Monoid view,Functor m, Monad m) => Applicative (View view m) where
|
|
+ pure a = View . return . FormElm mempty $ Just a
|
|
+ View f <*> View g= View $
|
|
+ f >>= \(FormElm form1 k) ->
|
|
+ g >>= \(FormElm form2 x) ->
|
|
+ return $ FormElm (form1 `mappend` form2) (k <*> x)
|
|
+
|
|
+instance (FormInput view,Functor m, Monad m) => Alternative (View view m) where
|
|
+ empty= View $ return $ FormElm mempty Nothing
|
|
View f <|> View g= View $ do
|
|
- path <- gets mfPagePath
|
|
+ path <- gets mfPagePath
|
|
FormElm form1 k <- f
|
|
s1 <- get
|
|
let path1 = mfPagePath s1
|
|
- put s1{mfPagePath=path}
|
|
+ put s1{mfPagePath=path}
|
|
FormElm form2 x <- g
|
|
s2 <- get
|
|
(mix,hasform) <- controlForms s1 s2 form1 form2
|
|
@@ -317,259 +349,259 @@
|
|
(_,Just _) -> path2
|
|
_ -> path
|
|
if hasform then put s2{needForm= HasForm,mfPagePath= path3}
|
|
- else put s2{mfPagePath=path3}
|
|
- return $ FormElm mix (k <|> x)
|
|
-
|
|
-
|
|
-instance (FormInput view, Monad m) => Monad (View view m) where
|
|
- View x >>= f = View $ do
|
|
- FormElm form1 mk <- x
|
|
- case mk of
|
|
- Just k -> do
|
|
+ else put s2{mfPagePath=path3}
|
|
+ return $ FormElm mix (k <|> x)
|
|
+
|
|
+
|
|
+instance (FormInput view, Monad m) => Monad (View view m) where
|
|
+ View x >>= f = View $ do
|
|
+ FormElm form1 mk <- x
|
|
+ case mk of
|
|
+ Just k -> do
|
|
st'' <- get
|
|
let st = st''{ linkMatched = False }
|
|
- put st
|
|
+ put st
|
|
FormElm form2 mk <- runView $ f k
|
|
st' <- get
|
|
(mix, hasform) <- controlForms st st' form1 form2
|
|
when hasform $ put st'{needForm= HasForm}
|
|
-
|
|
- return $ FormElm mix mk
|
|
- Nothing ->
|
|
- return $ FormElm form1 Nothing
|
|
-
|
|
-
|
|
-
|
|
- return = View . return . FormElm mempty . Just
|
|
--- fail msg= View . return $ FormElm [inRed msg] Nothing
|
|
-
|
|
-
|
|
-
|
|
-
|
|
-instance (FormInput v,Monad m, Functor m, Monoid a) => Monoid (View v m a) where
|
|
- mappend x y = mappend <$> x <*> y -- beware that both operands must validate to generate a sum
|
|
- mempty= return mempty
|
|
-
|
|
-
|
|
--- | It is a callback in the view monad. The callback rendering substitutes the widget rendering
|
|
--- when the latter is validated, without afecting the rendering of other widgets. This allow
|
|
--- the simultaneous execution of different behaviours in different widgets in the
|
|
--- same page. The inspiration is the callback primitive in the Seaside Web Framework
|
|
--- that allows similar functionality (See <http://www.seaside.st>)
|
|
---
|
|
--- This is the visible difference with 'waction' callbacks, which execute a
|
|
--- a flow in the FlowM monad that takes complete control of the navigation, while wactions are
|
|
--- executed whithin the same page.
|
|
-wcallback
|
|
- :: Monad m =>
|
|
- View view m a -> (a -> View view m b) -> View view m b
|
|
-wcallback (View x) f = View $ do
|
|
- FormElm form1 mk <- x
|
|
- case mk of
|
|
- Just k -> do
|
|
- modify $ \st -> st{linkMatched= False, needForm=NoElems}
|
|
+
|
|
+ return $ FormElm mix mk
|
|
+ Nothing ->
|
|
+ return $ FormElm form1 Nothing
|
|
+
|
|
+
|
|
+
|
|
+ return = View . return . FormElm mempty . Just
|
|
+-- fail msg= View . return $ FormElm [inRed msg] Nothing
|
|
+
|
|
+
|
|
+
|
|
+
|
|
+instance (FormInput v,Monad m, Functor m, Monoid a) => Monoid (View v m a) where
|
|
+ mappend x y = mappend <$> x <*> y -- beware that both operands must validate to generate a sum
|
|
+ mempty= return mempty
|
|
+
|
|
+
|
|
+-- | It is a callback in the view monad. The callback rendering substitutes the widget rendering
|
|
+-- when the latter is validated, without afecting the rendering of other widgets. This allow
|
|
+-- the simultaneous execution of different behaviours in different widgets in the
|
|
+-- same page. The inspiration is the callback primitive in the Seaside Web Framework
|
|
+-- that allows similar functionality (See <http://www.seaside.st>)
|
|
+--
|
|
+-- This is the visible difference with 'waction' callbacks, which execute a
|
|
+-- a flow in the FlowM monad that takes complete control of the navigation, while wactions are
|
|
+-- executed whithin the same page.
|
|
+wcallback
|
|
+ :: Monad m =>
|
|
+ View view m a -> (a -> View view m b) -> View view m b
|
|
+wcallback (View x) f = View $ do
|
|
+ FormElm form1 mk <- x
|
|
+ case mk of
|
|
+ Just k -> do
|
|
+ modify $ \st -> st{linkMatched= False, needForm=NoElems}
|
|
runView (f k)
|
|
-
|
|
- Nothing -> return $ FormElm form1 Nothing
|
|
-
|
|
-
|
|
-
|
|
-
|
|
-
|
|
-
|
|
-instance Monoid view => MonadTrans (View view) where
|
|
- lift f = View $ (lift f) >>= \x -> return $ FormElm mempty $ Just x
|
|
-
|
|
-instance MonadTrans (FlowM view) where
|
|
- lift f = FlowM $ lift (lift f) -- >>= \x -> return x
|
|
-
|
|
-instance (FormInput view, Monad m)=> MonadState (MFlowState view) (View view m) where
|
|
- get = View $ get >>= \x -> return $ FormElm mempty $ Just x
|
|
- put st = View $ put st >>= \x -> return $ FormElm mempty $ Just x
|
|
-
|
|
---instance (Monad m)=> MonadState (MFlowState view) (FlowM view m) where
|
|
--- get = FlowM $ get >>= \x -> return $ FormElm [] $ Just x
|
|
--- put st = FlowM $ put st >>= \x -> return $ FormElm [] $ Just x
|
|
-
|
|
-
|
|
-instance (FormInput view,MonadIO m) => MonadIO (View view m) where
|
|
- liftIO io= let x= liftIO io in x `seq` lift x -- to force liftIO==unsafePerformIO on the Identity monad
|
|
-
|
|
--- | Execute the widget in a monad and return the result in another.
|
|
-changeMonad :: (Monad m, Executable m1)
|
|
- => View v m1 a -> View v m a
|
|
-changeMonad w= View . StateT $ \s ->
|
|
- let (r,s')= execute $ runStateT ( runView w) s
|
|
- in mfSequence s' `seq` return (r,s')
|
|
+
|
|
+ Nothing -> return $ FormElm form1 Nothing
|
|
+
|
|
+
|
|
+
|
|
+
|
|
+
|
|
+
|
|
+instance Monoid view => MonadTrans (View view) where
|
|
+ lift f = View $ (lift f) >>= \x -> return $ FormElm mempty $ Just x
|
|
+
|
|
+instance MonadTrans (FlowM view) where
|
|
+ lift f = FlowM $ lift (lift f) -- >>= \x -> return x
|
|
+
|
|
+instance (FormInput view, Monad m)=> MonadState (MFlowState view) (View view m) where
|
|
+ get = View $ get >>= \x -> return $ FormElm mempty $ Just x
|
|
+ put st = View $ put st >>= \x -> return $ FormElm mempty $ Just x
|
|
+
|
|
+--instance (Monad m)=> MonadState (MFlowState view) (FlowM view m) where
|
|
+-- get = FlowM $ get >>= \x -> return $ FormElm [] $ Just x
|
|
+-- put st = FlowM $ put st >>= \x -> return $ FormElm [] $ Just x
|
|
+
|
|
+
|
|
+instance (FormInput view,MonadIO m) => MonadIO (View view m) where
|
|
+ liftIO io= let x= liftIO io in x `seq` lift x -- to force liftIO==unsafePerformIO on the Identity monad
|
|
+
|
|
+-- | Execute the widget in a monad and return the result in another.
|
|
+changeMonad :: (Monad m, Executable m1)
|
|
+ => View v m1 a -> View v m a
|
|
+changeMonad w= View . StateT $ \s ->
|
|
+ let (r,s')= execute $ runStateT ( runView w) s
|
|
+ in mfSequence s' `seq` return (r,s')
|
|
|
|
|
|
|
|
----- some combinators ----
|
|
-
|
|
--- | Join two widgets in the same page
|
|
--- the resulting widget, when `ask`ed with it, return a 2 tuple of their validation results
|
|
--- if both return Noting, the widget return @Nothing@ (invalid).
|
|
---
|
|
--- it has a low infix priority: @infixr 2@
|
|
---
|
|
--- > r <- ask widget1 <+> widget2
|
|
--- > case r of (Just x, Nothing) -> ..
|
|
-(<+>) , mix :: (Monad m, FormInput view)
|
|
- => View view m a
|
|
- -> View view m b
|
|
- -> View view m (Maybe a, Maybe b)
|
|
-mix digest1 digest2= View $ do
|
|
+
|
|
+-- | Join two widgets in the same page
|
|
+-- the resulting widget, when `ask`ed with it, return a 2 tuple of their validation results
|
|
+-- if both return Noting, the widget return @Nothing@ (invalid).
|
|
+--
|
|
+-- it has a low infix priority: @infixr 2@
|
|
+--
|
|
+-- > r <- ask widget1 <+> widget2
|
|
+-- > case r of (Just x, Nothing) -> ..
|
|
+(<+>) , mix :: (Monad m, FormInput view)
|
|
+ => View view m a
|
|
+ -> View view m b
|
|
+ -> View view m (Maybe a, Maybe b)
|
|
+mix digest1 digest2= View $ do
|
|
FormElm f1 mx' <- runView digest1
|
|
- s1 <- get
|
|
+ s1 <- get
|
|
FormElm f2 my' <- runView digest2
|
|
s2 <- get
|
|
(mix, hasform) <- controlForms s1 s2 f1 f2
|
|
- when hasform $ put s2{needForm= HasForm}
|
|
- return $ FormElm mix
|
|
- $ case (mx',my') of
|
|
- (Nothing, Nothing) -> Nothing
|
|
- other -> Just other
|
|
-
|
|
-infixr 2 <+>
|
|
-
|
|
-(<+>) = mix
|
|
-
|
|
-
|
|
-
|
|
--- | The first elem result (even if it is not validated) is discarded, and the secod is returned
|
|
--- . This contrast with the applicative operator '*>' which fails the whole validation if
|
|
--- the validation of the first elem fails.
|
|
---
|
|
--- The first element is displayed however, as happens in the case of '*>' .
|
|
---
|
|
--- Here @w\'s@ are widgets and @r\'s@ are returned values
|
|
---
|
|
--- @(w1 <* w2)@ will return @Just r1@ only if w1 and w2 are validated
|
|
---
|
|
--- @(w1 <** w2)@ will return @Just r1@ even if w2 is not validated
|
|
---
|
|
--- it has a low infix priority: @infixr 1@
|
|
-
|
|
-(**>) :: (Functor m, Monad m, FormInput view)
|
|
- => View view m a -> View view m b -> View view m b
|
|
+ when hasform $ put s2{needForm= HasForm}
|
|
+ return $ FormElm mix
|
|
+ $ case (mx',my') of
|
|
+ (Nothing, Nothing) -> Nothing
|
|
+ other -> Just other
|
|
+
|
|
+infixr 2 <+>
|
|
+
|
|
+(<+>) = mix
|
|
+
|
|
+
|
|
+
|
|
+-- | The first elem result (even if it is not validated) is discarded, and the secod is returned
|
|
+-- . This contrast with the applicative operator '*>' which fails the whole validation if
|
|
+-- the validation of the first elem fails.
|
|
+--
|
|
+-- The first element is displayed however, as happens in the case of '*>' .
|
|
+--
|
|
+-- Here @w\'s@ are widgets and @r\'s@ are returned values
|
|
+--
|
|
+-- @(w1 <* w2)@ will return @Just r1@ only if w1 and w2 are validated
|
|
+--
|
|
+-- @(w1 <** w2)@ will return @Just r1@ even if w2 is not validated
|
|
+--
|
|
+-- it has a low infix priority: @infixr 1@
|
|
+
|
|
+(**>) :: (Functor m, Monad m, FormInput view)
|
|
+ => View view m a -> View view m b -> View view m b
|
|
--(**>) form1 form2 = valid form1 *> form2
|
|
(**>) f g = View $ do
|
|
FormElm form1 k <- runView $ valid f
|
|
- s1 <- get
|
|
+ s1 <- get
|
|
FormElm form2 x <- runView g
|
|
s2 <- get
|
|
(mix,hasform) <- controlForms s1 s2 form1 form2
|
|
- when hasform $ put s2{needForm= HasForm}
|
|
- return $ FormElm mix (k *> x)
|
|
-
|
|
-
|
|
-
|
|
-valid form= View $ do
|
|
- FormElm form mx <- runView form
|
|
- return $ FormElm form $ Just undefined
|
|
-
|
|
-infixr 1 **> , <**
|
|
-
|
|
--- | The second elem result (even if it is not validated) is discarded, and the first is returned
|
|
--- . This contrast with the applicative operator '*>' which fails the whole validation if
|
|
--- the validation of the second elem fails.
|
|
--- The second element is displayed however, as in the case of '<*'.
|
|
--- see the `<**` examples
|
|
---
|
|
--- it has a low infix priority: @infixr 1@
|
|
-(<**) :: (Functor m, Monad m, FormInput view) =>
|
|
- View view m a -> View view m b -> View view m a
|
|
--- (<**) form1 form2 = form1 <* valid form2
|
|
+ when hasform $ put s2{needForm= HasForm}
|
|
+ return $ FormElm mix (k *> x)
|
|
+
|
|
+
|
|
+
|
|
+valid form= View $ do
|
|
+ FormElm form mx <- runView form
|
|
+ return $ FormElm form $ Just undefined
|
|
+
|
|
+infixr 1 **> , <**
|
|
+
|
|
+-- | The second elem result (even if it is not validated) is discarded, and the first is returned
|
|
+-- . This contrast with the applicative operator '*>' which fails the whole validation if
|
|
+-- the validation of the second elem fails.
|
|
+-- The second element is displayed however, as in the case of '<*'.
|
|
+-- see the `<**` examples
|
|
+--
|
|
+-- it has a low infix priority: @infixr 1@
|
|
+(<**) :: (Functor m, Monad m, FormInput view) =>
|
|
+ View view m a -> View view m b -> View view m a
|
|
+-- (<**) form1 form2 = form1 <* valid form2
|
|
(<**) f g = View $ do
|
|
FormElm form1 k <- runView f
|
|
- s1 <- get
|
|
+ s1 <- get
|
|
FormElm form2 x <- runView $ valid g
|
|
s2 <- get
|
|
(mix,hasform) <- controlForms s1 s2 form1 form2
|
|
- when hasform $ put s2{needForm= HasForm}
|
|
- return $ FormElm mix (k <* x)
|
|
-
|
|
+ when hasform $ put s2{needForm= HasForm}
|
|
+ return $ FormElm mix (k <* x)
|
|
+
|
|
|
|
|
|
-------- Flow control
|
|
-
|
|
--- | True if the flow is going back (as a result of the back button pressed in the web browser).
|
|
--- Usually this check is nos necessary unless conditional code make it necessary
|
|
---
|
|
--- @menu= do
|
|
--- mop <- getGoStraighTo
|
|
--- case mop of
|
|
--- Just goop -> goop
|
|
--- Nothing -> do
|
|
--- r \<- `ask` option1 \<|> option2
|
|
--- case r of
|
|
--- op1 -> setGoStraighTo (Just goop1) >> goop1
|
|
--- op2 -> setGoStraighTo (Just goop2) >> goop2@
|
|
---
|
|
--- This pseudocode below would execute the ask of the menu once. But the user will never have
|
|
--- the possibility to see the menu again. To let him choose other option, the code
|
|
--- has to be change to
|
|
---
|
|
--- @menu= do
|
|
--- mop <- getGoStraighTo
|
|
--- back <- `goingBack`
|
|
--- case (mop,back) of
|
|
--- (Just goop,False) -> goop
|
|
--- _ -> do
|
|
--- r \<- `ask` option1 \<|> option2
|
|
--- case r of
|
|
--- op1 -> setGoStraighTo (Just goop1) >> goop1
|
|
--- op2 -> setGoStraighTo (Just goop2) >> goop2@
|
|
---
|
|
--- However this is very specialized. Normally the back button detection is not necessary.
|
|
--- In a persistent flow (with step) even this default entry option would be completely automatic,
|
|
--- since the process would restart at the last page visited.
|
|
-goingBack :: MonadState (MFlowState view) m => m Bool
|
|
-goingBack = do
|
|
- st <- get
|
|
- return $ not (inSync st) && not (newAsk st)
|
|
-
|
|
--- | Will prevent the Suprack beyond the point where 'preventGoingBack' is located.
|
|
--- If the user press the back button beyond that point, the flow parameter is executed, usually
|
|
--- it is an ask statement with a message. If the flow is not going back, it does nothing. It is a cut in Supracking
|
|
---
|
|
--- It is useful when an undoable transaction has been commited. For example, after a payment.
|
|
---
|
|
--- This example show a message when the user go back and press again to pay
|
|
---
|
|
--- > ask $ wlink () << b << "press here to pay 100000 $ "
|
|
--- > payIt
|
|
--- > preventGoingBack . ask $ b << "You paid 10000 $ one time"
|
|
--- > ++> wlink () << b << " Please press here to complete the proccess"
|
|
--- > ask $ wlink () << b << "OK, press here to go to the menu or press the back button to verify that you can not pay again"
|
|
--- > where
|
|
--- > payIt= liftIO $ print "paying"
|
|
-
|
|
-preventGoingBack
|
|
- :: ( Functor m, MonadIO m, FormInput v) => FlowM v m () -> FlowM v m ()
|
|
-preventGoingBack msg= do
|
|
- back <- goingBack
|
|
- if not back then breturn() else do
|
|
- breturn() -- will not go back beyond this
|
|
- clearEnv
|
|
- modify $ \s -> s{newAsk= True}
|
|
- msg
|
|
-
|
|
-
|
|
--- | executes the first computation when going forward and the second computation when backtracking.
|
|
--- Depending on how the second computation finishes, the flow will resume forward or backward.
|
|
-onBacktrack :: Monad m => m a -> FlowM v m a -> FlowM v m a
|
|
-onBacktrack doit onback= do
|
|
- back <- goingBack
|
|
- case back of
|
|
- False -> (lift doit) >>= breturn
|
|
- True -> onback
|
|
-
|
|
--- | less powerflul version of `onBacktrack`: The second computation simply undo the effect of
|
|
--- the first one, and the flow continues backward ever. It can be used as a rollback mechanism in
|
|
--- the context of long running transactions.
|
|
-compensate :: Monad m => m a -> m a -> FlowM v m a
|
|
-compensate doit undoit= doit `onBacktrack` ( (lift undoit) >> fail "")
|
|
+
|
|
+-- | True if the flow is going back (as a result of the back button pressed in the web browser).
|
|
+-- Usually this check is nos necessary unless conditional code make it necessary
|
|
+--
|
|
+-- @menu= do
|
|
+-- mop <- getGoStraighTo
|
|
+-- case mop of
|
|
+-- Just goop -> goop
|
|
+-- Nothing -> do
|
|
+-- r \<- `ask` option1 \<|> option2
|
|
+-- case r of
|
|
+-- op1 -> setGoStraighTo (Just goop1) >> goop1
|
|
+-- op2 -> setGoStraighTo (Just goop2) >> goop2@
|
|
+--
|
|
+-- This pseudocode below would execute the ask of the menu once. But the user will never have
|
|
+-- the possibility to see the menu again. To let him choose other option, the code
|
|
+-- has to be change to
|
|
+--
|
|
+-- @menu= do
|
|
+-- mop <- getGoStraighTo
|
|
+-- back <- `goingBack`
|
|
+-- case (mop,back) of
|
|
+-- (Just goop,False) -> goop
|
|
+-- _ -> do
|
|
+-- r \<- `ask` option1 \<|> option2
|
|
+-- case r of
|
|
+-- op1 -> setGoStraighTo (Just goop1) >> goop1
|
|
+-- op2 -> setGoStraighTo (Just goop2) >> goop2@
|
|
+--
|
|
+-- However this is very specialized. Normally the back button detection is not necessary.
|
|
+-- In a persistent flow (with step) even this default entry option would be completely automatic,
|
|
+-- since the process would restart at the last page visited.
|
|
+goingBack :: MonadState (MFlowState view) m => m Bool
|
|
+goingBack = do
|
|
+ st <- get
|
|
+ return $ not (inSync st) && not (newAsk st)
|
|
+
|
|
+-- | Will prevent the Suprack beyond the point where 'preventGoingBack' is located.
|
|
+-- If the user press the back button beyond that point, the flow parameter is executed, usually
|
|
+-- it is an ask statement with a message. If the flow is not going back, it does nothing. It is a cut in Supracking
|
|
+--
|
|
+-- It is useful when an undoable transaction has been commited. For example, after a payment.
|
|
+--
|
|
+-- This example show a message when the user go back and press again to pay
|
|
+--
|
|
+-- > ask $ wlink () << b << "press here to pay 100000 $ "
|
|
+-- > payIt
|
|
+-- > preventGoingBack . ask $ b << "You paid 10000 $ one time"
|
|
+-- > ++> wlink () << b << " Please press here to complete the proccess"
|
|
+-- > ask $ wlink () << b << "OK, press here to go to the menu or press the back button to verify that you can not pay again"
|
|
+-- > where
|
|
+-- > payIt= liftIO $ print "paying"
|
|
+
|
|
+preventGoingBack
|
|
+ :: ( Functor m, MonadIO m, FormInput v) => FlowM v m () -> FlowM v m ()
|
|
+preventGoingBack msg= do
|
|
+ back <- goingBack
|
|
+ if not back then breturn() else do
|
|
+ breturn() -- will not go back beyond this
|
|
+ clearEnv
|
|
+ modify $ \s -> s{newAsk= True}
|
|
+ msg
|
|
+
|
|
+
|
|
+-- | executes the first computation when going forward and the second computation when backtracking.
|
|
+-- Depending on how the second computation finishes, the flow will resume forward or backward.
|
|
+onBacktrack :: Monad m => m a -> FlowM v m a -> FlowM v m a
|
|
+onBacktrack doit onback= do
|
|
+ back <- goingBack
|
|
+ case back of
|
|
+ False -> (lift doit) >>= breturn
|
|
+ True -> onback
|
|
+
|
|
+-- | less powerflul version of `onBacktrack`: The second computation simply undo the effect of
|
|
+-- the first one, and the flow continues backward ever. It can be used as a rollback mechanism in
|
|
+-- the context of long running transactions.
|
|
+compensate :: Monad m => m a -> m a -> FlowM v m a
|
|
+compensate doit undoit= doit `onBacktrack` ( (lift undoit) >> fail "")
|
|
|
|
|
|
--orElse :: FormInput v => FlowM v IO a -> FlowM v IO a -> FlowM v IO a
|
|
@@ -595,91 +627,96 @@
|
|
-- case mr of
|
|
-- Nothing -> retry
|
|
-- Just v -> return v
|
|
-
|
|
+
|
|
type Lang= String
|
|
|
|
-needForm1 st= case needForm st of
|
|
- HasForm -> False
|
|
- HasElems -> True
|
|
- NoElems -> False
|
|
-
|
|
-data NeedForm= HasForm | HasElems | NoElems deriving Show
|
|
-
|
|
-data MFlowState view= MFlowState{
|
|
- mfSequence :: Int,
|
|
- mfCached :: Bool,
|
|
- newAsk :: Bool,
|
|
- inSync :: Bool,
|
|
- mfLang :: Lang,
|
|
- mfEnv :: Params,
|
|
- needForm :: NeedForm,
|
|
- mfToken :: Token,
|
|
- mfkillTime :: Int,
|
|
- mfSessionTime :: Integer,
|
|
- mfCookies :: [Cookie],
|
|
- mfHttpHeaders :: [(SB.ByteString,SB.ByteString)],
|
|
- mfHeader :: view -> view,
|
|
- mfDebug :: Bool,
|
|
- mfRequirements :: [Requirement],
|
|
- mfData :: M.Map TypeRep Void,
|
|
- mfAjax :: Maybe (M.Map String Void),
|
|
- mfSeqCache :: Int,
|
|
- notSyncInAction :: Bool,
|
|
-
|
|
- -- Link management
|
|
+--needForm1 st= case needForm st of
|
|
+-- HasForm -> False
|
|
+-- HasElems _ -> True
|
|
+-- NoElems -> False
|
|
+
|
|
+
|
|
+
|
|
+data NeedForm= HasForm | HasElems | NoElems deriving Show
|
|
+
|
|
+data MFlowState view= MFlowState{
|
|
+ mfSequence :: Int,
|
|
+ mfCached :: Bool,
|
|
+ newAsk :: Bool,
|
|
+ inSync :: Bool,
|
|
+ mfLang :: Lang,
|
|
+ mfEnv :: Params,
|
|
+ needForm :: NeedForm,
|
|
+ mfFileUpload :: Bool,
|
|
+ mfToken :: Token,
|
|
+ mfkillTime :: Int,
|
|
+ mfSessionTime :: Integer,
|
|
+ mfCookies :: [Cookie],
|
|
+ mfHttpHeaders :: [(SB.ByteString,SB.ByteString)],
|
|
+ mfHeader :: view -> view,
|
|
+ mfDebug :: Bool,
|
|
+ mfRequirements :: [Requirement],
|
|
+ mfInstalledScripts :: [WebRequirement],
|
|
+ mfData :: M.Map TypeRep Void,
|
|
+ mfAjax :: Maybe (M.Map String Void),
|
|
+ mfSeqCache :: Int,
|
|
+ notSyncInAction :: Bool,
|
|
+
|
|
+ -- Link management
|
|
mfPath :: [String],
|
|
- mfPagePath :: [String],
|
|
- mfPrefix :: String,
|
|
--- mfPIndex :: Int,
|
|
- mfPageFlow :: Bool,
|
|
+ mfPagePath :: [String],
|
|
+ mfPrefix :: String,
|
|
+-- mfPIndex :: Int,
|
|
+ mfPageFlow :: Bool,
|
|
linkMatched :: Bool,
|
|
--- mfPendingPath :: [String],
|
|
-
|
|
-
|
|
- mfAutorefresh :: Bool,
|
|
- mfTrace :: [String],
|
|
- mfClear :: Bool
|
|
- }
|
|
- deriving Typeable
|
|
-
|
|
-type Void = Char
|
|
-
|
|
-mFlowState0 :: (FormInput view) => MFlowState view
|
|
-mFlowState0 = MFlowState 0 False True True "en"
|
|
- [] NoElems (error "token of mFlowState0 used")
|
|
- 0 0 [] [] stdHeader False [] M.empty Nothing 0 False [] [] "" False False False [] False
|
|
-
|
|
-
|
|
--- | Set user-defined data in the context of the session.
|
|
---
|
|
--- The data is indexed by type in a map. So the user can insert-retrieve different kinds of data
|
|
--- in the session context.
|
|
---
|
|
--- This example define @addHistory@ and @getHistory@ to maintain a Html log in the session of a Flow:
|
|
---
|
|
--- > newtype History = History ( Html) deriving Typeable
|
|
--- > setHistory html= setSessionData $ History html
|
|
--- > getHistory= getSessionData `onNothing` return (History mempty) >>= \(History h) -> return h
|
|
--- > addHistory html= do
|
|
--- > html' <- getHistory
|
|
--- > setHistory $ html' `mappend` html
|
|
-
|
|
-setSessionData :: (Typeable a,MonadState (MFlowState view) m) => a -> m ()
|
|
-setSessionData x=
|
|
- modify $ \st -> st{mfData= M.insert (typeOf x ) (unsafeCoerce x) (mfData st)}
|
|
-
|
|
-delSessionData x=
|
|
- modify $ \st -> st{mfData= M.delete (typeOf x ) (mfData st)}
|
|
-
|
|
--- | Get the session data of the desired type if there is any.
|
|
-getSessionData :: (Typeable a, MonadState (MFlowState view) m) => m (Maybe a)
|
|
-getSessionData = resp where
|
|
- resp= gets mfData >>= \list ->
|
|
- case M.lookup ( typeOf $ typeResp resp ) list of
|
|
- Just x -> return . Just $ unsafeCoerce x
|
|
- Nothing -> return $ Nothing
|
|
- typeResp :: m (Maybe x) -> x
|
|
- typeResp= undefined
|
|
+-- mfPendingPath :: [String],
|
|
+
|
|
+
|
|
+ mfAutorefresh :: Bool,
|
|
+ mfTrace :: [String],
|
|
+ mfClear :: Bool
|
|
+ }
|
|
+ deriving Typeable
|
|
+
|
|
+type Void = Char
|
|
+
|
|
+mFlowState0 :: (FormInput view) => MFlowState view
|
|
+mFlowState0 = MFlowState 0 False True True "en"
|
|
+ [] NoElems False (error "token of mFlowState0 used")
|
|
+ 0 0 [] [] stdHeader False [] [] M.empty Nothing 0 False
|
|
+ [] [] "" False False False [] False
|
|
+
|
|
+
|
|
+-- | Set user-defined data in the context of the session.
|
|
+--
|
|
+-- The data is indexed by type in a map. So the user can insert-retrieve different kinds of data
|
|
+-- in the session context.
|
|
+--
|
|
+-- This example define @addHistory@ and @getHistory@ to maintain a Html log in the session of a Flow:
|
|
+--
|
|
+-- > newtype History = History ( Html) deriving Typeable
|
|
+-- > setHistory html= setSessionData $ History html
|
|
+-- > getHistory= getSessionData `onNothing` return (History mempty) >>= \(History h) -> return h
|
|
+-- > addHistory html= do
|
|
+-- > html' <- getHistory
|
|
+-- > setHistory $ html' `mappend` html
|
|
+
|
|
+setSessionData :: (Typeable a,MonadState (MFlowState view) m) => a -> m ()
|
|
+setSessionData x=
|
|
+ modify $ \st -> st{mfData= M.insert (typeOf x ) (unsafeCoerce x) (mfData st)}
|
|
+
|
|
+delSessionData x=
|
|
+ modify $ \st -> st{mfData= M.delete (typeOf x ) (mfData st)}
|
|
+
|
|
+-- | Get the session data of the desired type if there is any.
|
|
+getSessionData :: (Typeable a, MonadState (MFlowState view) m) => m (Maybe a)
|
|
+getSessionData = resp where
|
|
+ resp= gets mfData >>= \list ->
|
|
+ case M.lookup ( typeOf $ typeResp resp ) list of
|
|
+ Just x -> return . Just $ unsafeCoerce x
|
|
+ Nothing -> return $ Nothing
|
|
+ typeResp :: m (Maybe x) -> x
|
|
+ typeResp= undefined
|
|
|
|
-- | getSessionData specialized for the View monad. if Nothing, the monadic computation
|
|
-- does not continue.
|
|
@@ -691,554 +728,555 @@
|
|
-- | Return the session identifier
|
|
getSessionId :: MonadState (MFlowState v) m => m String
|
|
getSessionId= gets mfToken >>= return . key
|
|
-
|
|
--- | Return the user language. Now it is fixed to "en"
|
|
-getLang :: MonadState (MFlowState view) m => m String
|
|
-getLang= gets mfLang
|
|
-
|
|
-getToken :: MonadState (MFlowState view) m => m Token
|
|
-getToken= gets mfToken
|
|
-
|
|
-
|
|
--- get a parameter form the las received response
|
|
-getEnv :: MonadState (MFlowState view) m => m Params
|
|
-getEnv = gets mfEnv
|
|
-
|
|
-stdHeader v = v
|
|
-
|
|
-
|
|
--- | Set the header-footer that will enclose the widgets. It must be provided in the
|
|
--- same formatting than them, altrough with normalization to byteStrings any formatting can be used
|
|
---
|
|
--- This header uses XML trough Haskell Server Pages (<http://hackage.haskell.org/package/hsp>)
|
|
---
|
|
--- @
|
|
--- setHeader $ \c ->
|
|
--- \<html\>
|
|
--- \<head\>
|
|
--- \<title\> my title \</title\>
|
|
--- \<meta name= \"Keywords\" content= \"sci-fi\" /\>)
|
|
--- \</head\>
|
|
--- \<body style= \"margin-left:5%;margin-right:5%\"\>
|
|
--- \<% c %\>
|
|
--- \</body\>
|
|
--- \</html\>
|
|
--- @
|
|
---
|
|
--- This header uses "Text.XHtml"
|
|
---
|
|
--- @
|
|
--- setHeader $ \c ->
|
|
--- `thehtml`
|
|
--- << (`header`
|
|
--- << (`thetitle` << title +++
|
|
--- `meta` ! [`name` \"Keywords\",content \"sci-fi\"])) +++
|
|
--- `body` ! [`style` \"margin-left:5%;margin-right:5%\"] c
|
|
--- @
|
|
---
|
|
--- This header uses both. It uses byteString tags
|
|
---
|
|
--- @
|
|
--- setHeader $ \c ->
|
|
--- `bhtml` [] $
|
|
--- `btag` "head" [] $
|
|
--- (`toByteString` (thetitle << title) `append`
|
|
--- `toByteString` <meta name= \"Keywords\" content= \"sci-fi\" />) `append`
|
|
--- `bbody` [(\"style\", \"margin-left:5%;margin-right:5%\")] c
|
|
--- @
|
|
---
|
|
-setHeader :: MonadState (MFlowState view) m => (view -> view) -> m ()
|
|
-setHeader header= do
|
|
- fs <- get
|
|
- put fs{mfHeader= header}
|
|
-
|
|
-
|
|
-
|
|
--- | Return the current header
|
|
-getHeader :: ( Monad m) => FlowM view m (view -> view)
|
|
-getHeader= gets mfHeader
|
|
-
|
|
--- | Add another header embedded in the previous one
|
|
-addHeader new= do
|
|
- fhtml <- getHeader
|
|
- setHeader $ fhtml . new
|
|
-
|
|
--- | Set an HTTP cookie
|
|
-setCookie :: MonadState (MFlowState view) m
|
|
- => String -- ^ name
|
|
- -> String -- ^ value
|
|
- -> String -- ^ path
|
|
- -> Maybe Integer -- ^ Max-Age in seconds. Nothing for a session cookie
|
|
- -> m ()
|
|
-setCookie n v p me=
|
|
- modify $ \st -> st{mfCookies= (UnEncryptedCookie
|
|
- ( SB.fromString n,
|
|
- SB.fromString v,
|
|
- SB.fromString p,
|
|
- fmap (SB.fromString . show) me)):mfCookies st }
|
|
-
|
|
-setParanoidCookie :: MonadState (MFlowState view) m
|
|
- => String -- ^ name
|
|
- -> String -- ^ value
|
|
- -> String -- ^ path
|
|
+
|
|
+-- | Return the user language. Now it is fixed to "en"
|
|
+getLang :: MonadState (MFlowState view) m => m String
|
|
+getLang= gets mfLang
|
|
+
|
|
+getToken :: MonadState (MFlowState view) m => m Token
|
|
+getToken= gets mfToken
|
|
+
|
|
+
|
|
+-- get a parameter form the las received response
|
|
+getEnv :: MonadState (MFlowState view) m => m Params
|
|
+getEnv = gets mfEnv
|
|
+
|
|
+stdHeader v = v
|
|
+
|
|
+
|
|
+-- | Set the header-footer that will enclose the widgets. It must be provided in the
|
|
+-- same formatting than them, altrough with normalization to byteStrings any formatting can be used
|
|
+--
|
|
+-- This header uses XML trough Haskell Server Pages (<http://hackage.haskell.org/package/hsp>)
|
|
+--
|
|
+-- @
|
|
+-- setHeader $ \c ->
|
|
+-- \<html\>
|
|
+-- \<head\>
|
|
+-- \<title\> my title \</title\>
|
|
+-- \<meta name= \"Keywords\" content= \"sci-fi\" /\>)
|
|
+-- \</head\>
|
|
+-- \<body style= \"margin-left:5%;margin-right:5%\"\>
|
|
+-- \<% c %\>
|
|
+-- \</body\>
|
|
+-- \</html\>
|
|
+-- @
|
|
+--
|
|
+-- This header uses "Text.XHtml"
|
|
+--
|
|
+-- @
|
|
+-- setHeader $ \c ->
|
|
+-- `thehtml`
|
|
+-- << (`header`
|
|
+-- << (`thetitle` << title +++
|
|
+-- `meta` ! [`name` \"Keywords\",content \"sci-fi\"])) +++
|
|
+-- `body` ! [`style` \"margin-left:5%;margin-right:5%\"] c
|
|
+-- @
|
|
+--
|
|
+-- This header uses both. It uses byteString tags
|
|
+--
|
|
+-- @
|
|
+-- setHeader $ \c ->
|
|
+-- `bhtml` [] $
|
|
+-- `btag` "head" [] $
|
|
+-- (`toByteString` (thetitle << title) `append`
|
|
+-- `toByteString` <meta name= \"Keywords\" content= \"sci-fi\" />) `append`
|
|
+-- `bbody` [(\"style\", \"margin-left:5%;margin-right:5%\")] c
|
|
+-- @
|
|
+--
|
|
+setHeader :: MonadState (MFlowState view) m => (view -> view) -> m ()
|
|
+setHeader header= do
|
|
+ fs <- get
|
|
+ put fs{mfHeader= header}
|
|
+
|
|
+
|
|
+
|
|
+-- | Return the current header
|
|
+getHeader :: ( Monad m) => FlowM view m (view -> view)
|
|
+getHeader= gets mfHeader
|
|
+
|
|
+-- | Add another header embedded in the previous one
|
|
+addHeader new= do
|
|
+ fhtml <- getHeader
|
|
+ setHeader $ fhtml . new
|
|
+
|
|
+-- | Set an HTTP cookie
|
|
+setCookie :: MonadState (MFlowState view) m
|
|
+ => String -- ^ name
|
|
+ -> String -- ^ value
|
|
+ -> String -- ^ path
|
|
+ -> Maybe Integer -- ^ Max-Age in seconds. Nothing for a session cookie
|
|
+ -> m ()
|
|
+setCookie n v p me=
|
|
+ modify $ \st -> st{mfCookies= (UnEncryptedCookie
|
|
+ ( SB.fromString n,
|
|
+ SB.fromString v,
|
|
+ SB.fromString p,
|
|
+ fmap (SB.fromString . show) me)):mfCookies st }
|
|
+
|
|
+setParanoidCookie :: MonadState (MFlowState view) m
|
|
+ => String -- ^ name
|
|
+ -> String -- ^ value
|
|
+ -> String -- ^ path
|
|
-> Maybe Integer -- ^ Max-Age in seconds. Nothing for a session cookie
|
|
- -> m ()
|
|
-setParanoidCookie n v p me = setEncryptedCookie' n v p me paranoidEncryptCookie
|
|
+ -> m ()
|
|
+setParanoidCookie n v p me = setEncryptedCookie' n v p me paranoidEncryptCookie
|
|
|
|
-setEncryptedCookie :: MonadState (MFlowState view) m
|
|
- => String -- ^ name
|
|
- -> String -- ^ value
|
|
- -> String -- ^ path
|
|
+setEncryptedCookie :: MonadState (MFlowState view) m
|
|
+ => String -- ^ name
|
|
+ -> String -- ^ value
|
|
+ -> String -- ^ path
|
|
-> Maybe Integer -- ^ Max-Age in seconds. Nothing for a session cookie
|
|
- -> m ()
|
|
-setEncryptedCookie n v p me = setEncryptedCookie' n v p me encryptCookie
|
|
-
|
|
-setEncryptedCookie' n v p me encFunc=
|
|
- modify $ \st -> st{mfCookies =
|
|
- (unsafePerformIO $ encFunc
|
|
- ( SB.fromString n,
|
|
- SB.fromString v,
|
|
- SB.fromString p,
|
|
- fmap (SB.fromString . show) me)):mfCookies st }
|
|
-
|
|
--- | Set an HTTP Response header
|
|
-setHttpHeader :: MonadState (MFlowState view) m
|
|
- => SB.ByteString -- ^ name
|
|
- -> SB.ByteString -- ^ value
|
|
- -> m ()
|
|
-setHttpHeader n v =
|
|
- modify $ \st -> st{mfHttpHeaders = nubBy (\ x y -> fst x == fst y) $ (n,v):mfHttpHeaders st}
|
|
-
|
|
-
|
|
--- | Set
|
|
--- 1) the timeout of the flow execution since the last user interaction.
|
|
--- Once passed, the flow executes from the begining.
|
|
---
|
|
--- 2) In persistent flows
|
|
--- it set the session state timeout for the flow, that is persistent. If the
|
|
--- flow is not persistent, it has no effect.
|
|
---
|
|
--- As the other state primitives, it can be run in the Flow and in the View monad
|
|
---
|
|
--- `transient` flows restart anew.
|
|
--- persistent flows (that use `step`) restart at the las saved execution point, unless
|
|
--- the session time has expired for the user.
|
|
-setTimeouts :: ( MonadState (MFlowState v) m) => Int -> Integer -> m ()
|
|
-setTimeouts kt st= do
|
|
- fs <- get
|
|
- put fs{ mfkillTime= kt, mfSessionTime= st}
|
|
-
|
|
-
|
|
-getWFName :: MonadState (MFlowState view) m => m String
|
|
-getWFName = do
|
|
- fs <- get
|
|
- return . twfname $ mfToken fs
|
|
-
|
|
-getCurrentUser :: MonadState (MFlowState view) m => m String
|
|
-getCurrentUser = do
|
|
- st<- gets mfToken
|
|
- return $ tuser st
|
|
-
|
|
-type Name= String
|
|
-type Type= String
|
|
-type Value= String
|
|
-type Checked= Bool
|
|
-type OnClick= Maybe String
|
|
-
|
|
-normalize :: (Monad m, FormInput v) => View v m a -> View B.ByteString m a
|
|
-normalize f= View . StateT $ \s ->do
|
|
- (FormElm fs mx, s') <- runStateT ( runView f) $ unsafeCoerce s
|
|
- return (FormElm (toByteString fs ) mx,unsafeCoerce s')
|
|
-
|
|
-
|
|
-
|
|
--- | Minimal interface for defining the basic form and link elements. The core of MFlow is agnostic
|
|
--- about the rendering package used. Every formatting (either HTML or not) used with MFlow must have an
|
|
--- instance of this class.
|
|
--- See "MFlow.Forms.Blaze.Html for the instance for blaze-html" "MFlow.Forms.XHtml" for the instance
|
|
--- for @Text.XHtml@ and MFlow.Forms.HSP for the instance for Haskell Server Pages.
|
|
-class (Monoid view,Typeable view) => FormInput view where
|
|
- toByteString :: view -> B.ByteString
|
|
- toHttpData :: view -> HttpData
|
|
- fromStr :: String -> view
|
|
- fromStrNoEncode :: String -> view
|
|
- ftag :: String -> view -> view
|
|
- inred :: view -> view
|
|
- flink :: String -> view -> view
|
|
- flink1:: String -> view
|
|
- flink1 verb = flink verb (fromStr verb)
|
|
- finput :: Name -> Type -> Value -> Checked -> OnClick -> view
|
|
- ftextarea :: String -> T.Text -> view
|
|
- fselect :: String -> view -> view
|
|
- foption :: String -> view -> Bool -> view
|
|
- foption1 :: String -> Bool -> view
|
|
- foption1 val msel= foption val (fromStr val) msel
|
|
- formAction :: String -> view -> view
|
|
- attrs :: view -> Attribs -> view
|
|
-
|
|
-
|
|
-
|
|
---instance (MonadIO m) => MonadIO (FlowM view m) where
|
|
--- liftIO io= let x= liftIO io in x `seq` lift x -- to force liftIO==unsafePerformIO onf the Identity monad
|
|
-
|
|
---instance Executable (View v m) where
|
|
--- execute f = execute $ evalStateT f mFlowState0
|
|
-
|
|
-
|
|
---instance (Monad m, Executable m, Monoid view, FormInput view)
|
|
--- => Executable (StateT (MFlowState view) m) where
|
|
--- execute f= execute $ evalStateT f mFlowState0
|
|
-
|
|
--- | Cached widgets operate with widgets in the Identity monad, but they may perform IO using the execute instance
|
|
--- of the monad m, which is usually the IO monad. execute basically \"sanctifies\" the use of unsafePerformIO for a transient purpose
|
|
--- such is caching. This is defined in "Data.TCache.Memoization". The programmer can create his
|
|
--- own instance for his monad.
|
|
---
|
|
--- With `cachedWidget` it is possible to cache the rendering of a widget as a ByteString (maintaining type safety)
|
|
---, permanently or for a certain time. this is very useful for complex widgets that present information. Specially it they must access to databases.
|
|
---
|
|
--- @
|
|
--- import MFlow.Wai.Blaze.Html.All
|
|
--- import Some.Time.Library
|
|
--- addMessageFlows [(noscript, time)]
|
|
--- main= run 80 waiMessageFlow
|
|
--- time=do ask $ cachedWidget \"time\" 5
|
|
--- $ wlink () b << \"the time is \" ++ show (execute giveTheTime) ++ \" click here\"
|
|
--- time
|
|
--- @
|
|
---
|
|
--- this pseudocode would update the time every 5 seconds. The execution of the IO computation
|
|
--- giveTheTime must be executed inside the cached widget to avoid unnecesary IO executions.
|
|
---
|
|
--- NOTE: the rendering of cached widgets are shared by all users
|
|
-cachedWidget :: (MonadIO m,Typeable view
|
|
- , FormInput view, Typeable a, Executable m )
|
|
- => String -- ^ The key of the cached object for the retrieval
|
|
- -> Int -- ^ Timeout of the caching. Zero means the whole server run
|
|
- -> View view Identity a -- ^ The cached widget, in the Identity monad
|
|
- -> View view m a -- ^ The cached result
|
|
-cachedWidget key t mf = View . StateT $ \s -> do
|
|
- let((FormElm form _), sec)= execute $! cachedByKey key t $ proc mf s{mfCached=True}
|
|
- let((FormElm _ mx2), s2) = execute $ runStateT ( runView mf) s{mfSeqCache= sec,mfCached=True}
|
|
- let s''= s{inSync = inSync s2
|
|
- ,mfRequirements=mfRequirements s2
|
|
+ -> m ()
|
|
+setEncryptedCookie n v p me = setEncryptedCookie' n v p me encryptCookie
|
|
+
|
|
+setEncryptedCookie' n v p me encFunc=
|
|
+ modify $ \st -> st{mfCookies =
|
|
+ (unsafePerformIO $ encFunc
|
|
+ ( SB.fromString n,
|
|
+ SB.fromString v,
|
|
+ SB.fromString p,
|
|
+ fmap (SB.fromString . show) me)):mfCookies st }
|
|
+
|
|
+-- | Set an HTTP Response header
|
|
+setHttpHeader :: MonadState (MFlowState view) m
|
|
+ => SB.ByteString -- ^ name
|
|
+ -> SB.ByteString -- ^ value
|
|
+ -> m ()
|
|
+setHttpHeader n v =
|
|
+ modify $ \st -> st{mfHttpHeaders = nubBy (\ x y -> fst x == fst y) $ (n,v):mfHttpHeaders st}
|
|
+
|
|
+
|
|
+-- | Set
|
|
+-- 1) the timeout of the flow execution since the last user interaction.
|
|
+-- Once passed, the flow executes from the begining.
|
|
+--
|
|
+-- 2) In persistent flows
|
|
+-- it set the session state timeout for the flow, that is persistent. If the
|
|
+-- flow is not persistent, it has no effect.
|
|
+--
|
|
+-- As the other state primitives, it can be run in the Flow and in the View monad
|
|
+--
|
|
+-- `transient` flows restart anew.
|
|
+-- persistent flows (that use `step`) restart at the las saved execution point, unless
|
|
+-- the session time has expired for the user.
|
|
+setTimeouts :: ( MonadState (MFlowState v) m) => Int -> Integer -> m ()
|
|
+setTimeouts kt st= do
|
|
+ fs <- get
|
|
+ put fs{ mfkillTime= kt, mfSessionTime= st}
|
|
+
|
|
+
|
|
+getWFName :: MonadState (MFlowState view) m => m String
|
|
+getWFName = do
|
|
+ fs <- get
|
|
+ return . twfname $ mfToken fs
|
|
+
|
|
+getCurrentUser :: MonadState (MFlowState view) m => m String
|
|
+getCurrentUser = do
|
|
+ st<- gets mfToken
|
|
+ return $ tuser st
|
|
+
|
|
+type Name= String
|
|
+type Type= String
|
|
+type Value= String
|
|
+type Checked= Bool
|
|
+type OnClick= Maybe String
|
|
+
|
|
+normalize :: (Monad m, FormInput v) => View v m a -> View B.ByteString m a
|
|
+normalize f= View . StateT $ \s ->do
|
|
+ (FormElm fs mx, s') <- runStateT ( runView f) $ unsafeCoerce s
|
|
+ return (FormElm (toByteString fs ) mx,unsafeCoerce s')
|
|
+
|
|
+
|
|
+
|
|
+-- | Minimal interface for defining the basic form and link elements. The core of MFlow is agnostic
|
|
+-- about the rendering package used. Every formatting (either HTML or not) used with MFlow must have an
|
|
+-- instance of this class.
|
|
+-- See "MFlow.Forms.Blaze.Html for the instance for blaze-html" "MFlow.Forms.XHtml" for the instance
|
|
+-- for @Text.XHtml@ and MFlow.Forms.HSP for the instance for Haskell Server Pages.
|
|
+class (Monoid view,Typeable view) => FormInput view where
|
|
+ toByteString :: view -> B.ByteString
|
|
+ toHttpData :: view -> HttpData
|
|
+ fromStr :: String -> view
|
|
+ fromStrNoEncode :: String -> view
|
|
+ ftag :: String -> view -> view
|
|
+ inred :: view -> view
|
|
+ flink :: String -> view -> view
|
|
+ flink1:: String -> view
|
|
+ flink1 verb = flink verb (fromStr verb)
|
|
+ finput :: Name -> Type -> Value -> Checked -> OnClick -> view
|
|
+ ftextarea :: String -> T.Text -> view
|
|
+ fselect :: String -> view -> view
|
|
+ foption :: String -> view -> Bool -> view
|
|
+ foption1 :: String -> Bool -> view
|
|
+ foption1 val msel= foption val (fromStr val) msel
|
|
+ formAction :: String -> String -> view -> view
|
|
+ attrs :: view -> Attribs -> view
|
|
+
|
|
+
|
|
+
|
|
+--instance (MonadIO m) => MonadIO (FlowM view m) where
|
|
+-- liftIO io= let x= liftIO io in x `seq` lift x -- to force liftIO==unsafePerformIO onf the Identity monad
|
|
+
|
|
+--instance Executable (View v m) where
|
|
+-- execute f = execute $ evalStateT f mFlowState0
|
|
+
|
|
+
|
|
+--instance (Monad m, Executable m, Monoid view, FormInput view)
|
|
+-- => Executable (StateT (MFlowState view) m) where
|
|
+-- execute f= execute $ evalStateT f mFlowState0
|
|
+
|
|
+-- | Cached widgets operate with widgets in the Identity monad, but they may perform IO using the execute instance
|
|
+-- of the monad m, which is usually the IO monad. execute basically \"sanctifies\" the use of unsafePerformIO for a transient purpose
|
|
+-- such is caching. This is defined in "Data.TCache.Memoization". The programmer can create his
|
|
+-- own instance for his monad.
|
|
+--
|
|
+-- With `cachedWidget` it is possible to cache the rendering of a widget as a ByteString (maintaining type safety)
|
|
+--, permanently or for a certain time. this is very useful for complex widgets that present information. Specially it they must access to databases.
|
|
+--
|
|
+-- @
|
|
+-- import MFlow.Wai.Blaze.Html.All
|
|
+-- import Some.Time.Library
|
|
+-- addMessageFlows [(noscript, time)]
|
|
+-- main= run 80 waiMessageFlow
|
|
+-- time=do ask $ cachedWidget \"time\" 5
|
|
+-- $ wlink () b << \"the time is \" ++ show (execute giveTheTime) ++ \" click here\"
|
|
+-- time
|
|
+-- @
|
|
+--
|
|
+-- this pseudocode would update the time every 5 seconds. The execution of the IO computation
|
|
+-- giveTheTime must be executed inside the cached widget to avoid unnecesary IO executions.
|
|
+--
|
|
+-- NOTE: the rendering of cached widgets are shared by all users
|
|
+cachedWidget :: (MonadIO m,Typeable view
|
|
+ , FormInput view, Typeable a, Executable m )
|
|
+ => String -- ^ The key of the cached object for the retrieval
|
|
+ -> Int -- ^ Timeout of the caching. Zero means the whole server run
|
|
+ -> View view Identity a -- ^ The cached widget, in the Identity monad
|
|
+ -> View view m a -- ^ The cached result
|
|
+cachedWidget key t mf = View . StateT $ \s -> do
|
|
+ let((FormElm form _), sec)= execute $! cachedByKey key t $ proc mf s{mfCached=True}
|
|
+ let((FormElm _ mx2), s2) = execute $ runStateT ( runView mf) s{mfSeqCache= sec,mfCached=True}
|
|
+ let s''= s{inSync = inSync s2
|
|
+ ,mfRequirements=mfRequirements s2
|
|
,mfPath= mfPath s2
|
|
- ,mfPagePath= mfPagePath s2
|
|
- ,needForm= needForm s2
|
|
- ,mfPageFlow= mfPageFlow s2
|
|
+ ,mfPagePath= mfPagePath s2
|
|
+ ,needForm= needForm s2
|
|
+ ,mfPageFlow= mfPageFlow s2
|
|
,mfSeqCache= mfSeqCache s + mfSeqCache s2 - sec}
|
|
return $ (mfSeqCache s'') `seq` form `seq` ((FormElm form mx2), s'')
|
|
-
|
|
- -- !> ("enter: "++show (mfSeqCache s) ++" exit: "++ show ( mfSeqCache s2))
|
|
- where
|
|
- proc mf s= runStateT (runView mf) s >>= \(r,_) -> mfSeqCache s `seq` return (r,mfSeqCache s )
|
|
-
|
|
--- | A shorter name for `cachedWidget`
|
|
-wcached :: (MonadIO m,Typeable view
|
|
- , FormInput view, Typeable a, Executable m )
|
|
- => String -- ^ The key of the cached object for the retrieval
|
|
- -> Int -- ^ Timeout of the caching. Zero means sessionwide
|
|
- -> View view Identity a -- ^ The cached widget, in the Identity monad
|
|
- -> View view m a -- ^ The cached result
|
|
-wcached= cachedWidget
|
|
-
|
|
--- | Unlike `cachedWidget`, which cache the rendering but not the user response, @wfreeze@
|
|
--- cache also the user response. This is useful for pseudo-widgets which just show information
|
|
--- while the controls are in other non freezed widgets. A freezed widget ever return the first user response
|
|
--- It is faster than `cachedWidget`.
|
|
--- It is not restricted to the Identity monad.
|
|
---
|
|
--- NOTE: the content of freezed widgets are shared by all users
|
|
-wfreeze :: (MonadIO m,Typeable view
|
|
- , FormInput view, Typeable a, Executable m )
|
|
- => String -- ^ The key of the cached object for the retrieval
|
|
- -> Int -- ^ Timeout of the caching. Zero means sessionwide
|
|
- -> View view m a -- ^ The cached widget
|
|
- -> View view m a -- ^ The cached result
|
|
-wfreeze key t mf = View . StateT $ \s -> do
|
|
- (FormElm f mx, req,seq,ajax) <- cachedByKey key t $ proc mf s{mfCached=True}
|
|
- return ((FormElm f mx), s{mfRequirements=req ,mfSeqCache= seq,mfAjax=ajax})
|
|
- where
|
|
- proc mf s= do
|
|
- (r,s) <- runStateT (runView mf) s
|
|
- return (r,mfRequirements s, mfSeqCache s, mfAjax s)
|
|
-
|
|
-
|
|
-
|
|
-{- | Execute the Flow, in the @FlowM view m@ monad. It is used as parameter of `hackMessageFlow`
|
|
-`waiMessageFlow` or `addMessageFlows`
|
|
-
|
|
-The flow is executed in a loop. When the flow is finished, it is started again
|
|
-
|
|
-@main= do
|
|
- addMessageFlows [(\"noscript\",transient $ runFlow mainf)]
|
|
- forkIO . run 80 $ waiMessageFlow
|
|
- adminLoop
|
|
-@
|
|
--}
|
|
-runFlow :: (FormInput view, MonadIO m)
|
|
- => FlowM view (Workflow m) () -> Token -> Workflow m ()
|
|
-runFlow f t=
|
|
- loop (startState t) f t
|
|
- where
|
|
- loop s f t = do
|
|
- (mt,s) <- runFlowOnce2 s f
|
|
- let t'= fromFailBack mt
|
|
- let t''= t'{tpath=[twfname t']}
|
|
- liftIO $ do
|
|
- flushRec t''
|
|
+
|
|
+ -- !> ("enter: "++show (mfSeqCache s) ++" exit: "++ show ( mfSeqCache s2))
|
|
+ where
|
|
+ proc mf s= runStateT (runView mf) s >>= \(r,_) -> mfSeqCache s `seq` return (r,mfSeqCache s )
|
|
+
|
|
+-- | A shorter name for `cachedWidget`
|
|
+wcached :: (MonadIO m,Typeable view
|
|
+ , FormInput view, Typeable a, Executable m )
|
|
+ => String -- ^ The key of the cached object for the retrieval
|
|
+ -> Int -- ^ Timeout of the caching. Zero means sessionwide
|
|
+ -> View view Identity a -- ^ The cached widget, in the Identity monad
|
|
+ -> View view m a -- ^ The cached result
|
|
+wcached= cachedWidget
|
|
+
|
|
+-- | Unlike `cachedWidget`, which cache the rendering but not the user response, @wfreeze@
|
|
+-- cache also the user response. This is useful for pseudo-widgets which just show information
|
|
+-- while the controls are in other non freezed widgets. A freezed widget ever return the first user response
|
|
+-- It is faster than `cachedWidget`.
|
|
+-- It is not restricted to the Identity monad.
|
|
+--
|
|
+-- NOTE: the content of freezed widgets are shared by all users
|
|
+wfreeze :: (MonadIO m,Typeable view
|
|
+ , FormInput view, Typeable a, Executable m )
|
|
+ => String -- ^ The key of the cached object for the retrieval
|
|
+ -> Int -- ^ Timeout of the caching. Zero means sessionwide
|
|
+ -> View view m a -- ^ The cached widget
|
|
+ -> View view m a -- ^ The cached result
|
|
+wfreeze key t mf = View . StateT $ \s -> do
|
|
+ (FormElm f mx, req,seq,ajax) <- cachedByKey key t $ proc mf s{mfCached=True}
|
|
+ return ((FormElm f mx), s{mfRequirements=req ,mfSeqCache= seq,mfAjax=ajax})
|
|
+ where
|
|
+ proc mf s= do
|
|
+ (r,s) <- runStateT (runView mf) s
|
|
+ return (r,mfRequirements s, mfSeqCache s, mfAjax s)
|
|
+
|
|
+
|
|
+
|
|
+{- | Execute the Flow, in the @FlowM view m@ monad. It is used as parameter of `hackMessageFlow`
|
|
+`waiMessageFlow` or `addMessageFlows`
|
|
+
|
|
+The flow is executed in a loop. When the flow is finished, it is started again
|
|
+
|
|
+@main= do
|
|
+ addMessageFlows [(\"noscript\",transient $ runFlow mainf)]
|
|
+ forkIO . run 80 $ waiMessageFlow
|
|
+ adminLoop
|
|
+@
|
|
+-}
|
|
+runFlow :: (FormInput view, MonadIO m)
|
|
+ => FlowM view (Workflow m) () -> Token -> Workflow m ()
|
|
+runFlow f t=
|
|
+ loop (startState t) f t
|
|
+ where
|
|
+ loop s f t = do
|
|
+ (mt,s) <- runFlowOnce2 s f
|
|
+ let t'= fromFailBack mt
|
|
+ let t''= t'{tpath=[twfname t']}
|
|
+ liftIO $ do
|
|
+ flushRec t''
|
|
sendToMF t'' t''
|
|
let s'= case mfSequence s of
|
|
-1 -> s -- !> "end of recovery loop"
|
|
- _ -> s{mfPath=[twfname t],mfPagePath=[],mfEnv=[]}
|
|
- loop s' f t''{tpath=[]} -- !> "LOOPAGAIN"
|
|
-
|
|
-inRecovery= -1
|
|
-
|
|
-runFlowOnce :: (FormInput view, MonadIO m)
|
|
- => FlowM view (Workflow m) () -> Token -> Workflow m ()
|
|
-runFlowOnce f t= runFlowOnce1 f t >> return ()
|
|
-
|
|
-runFlowOnce1 f t = runFlowOnce2 (startState t) f
|
|
-
|
|
+ _ -> s{mfPath=[twfname t],mfPagePath=[],mfEnv=[]}
|
|
+ loop s' f t''{tpath=[]} -- !> "LOOPAGAIN"
|
|
+
|
|
+inRecovery= -1
|
|
+
|
|
+runFlowOnce :: (FormInput view, MonadIO m)
|
|
+ => FlowM view (Workflow m) () -> Token -> Workflow m ()
|
|
+runFlowOnce f t= runFlowOnce1 f t >> return ()
|
|
+
|
|
+runFlowOnce1 f t = runFlowOnce2 (startState t) f
|
|
+
|
|
startState t= mFlowState0{mfToken=t
|
|
- ,mfSequence= inRecovery
|
|
- ,mfPath= tpath t
|
|
+ ,mfSequence= inRecovery
|
|
+ ,mfPath= tpath t
|
|
,mfEnv= tenv t
|
|
- ,mfPagePath=[]}
|
|
+ ,mfPagePath=[]}
|
|
|
|
-runFlowOnce2 s f =
|
|
- runStateT (runSup . runFlowM $ do
|
|
- backInit
|
|
- f
|
|
- getToken) s
|
|
-
|
|
-
|
|
- where
|
|
- backInit= do
|
|
- s <- get -- !> "BackInit"
|
|
- case mfTrace s of
|
|
+runFlowOnce2 s f =
|
|
+ runStateT (runSup . runFlowM $ do
|
|
+ backInit
|
|
+ f
|
|
+ getToken) s
|
|
+
|
|
+
|
|
+ where
|
|
+ backInit= do
|
|
+ s <- get -- !> "BackInit"
|
|
+ case mfTrace s of
|
|
[] -> do
|
|
let t = mfToken s
|
|
back <- goingBack
|
|
recover <- lift $ isInRecover
|
|
- when (back && not recover) . modify $ \s -> s{ newAsk= True,mfPagePath=[twfname t]}
|
|
+ when (back && not recover) . modify $ \s -> s{ newAsk= True,mfPagePath=[twfname t]}
|
|
breturn ()
|
|
-
|
|
- tr -> error $ disp tr
|
|
- where
|
|
- disp tr= "TRACE (error in the last line):\n\n" ++(concat $ intersperse "\n" tr)
|
|
- -- to restart the flow in case of going back before the first page of the flow
|
|
+
|
|
+ tr -> error $ disp tr
|
|
+ where
|
|
+ disp tr= "TRACE (error in the last line):\n\n" ++(concat $ intersperse "\n" tr)
|
|
+ -- to restart the flow in case of going back before the first page of the flow
|
|
|
|
runFlowOnceReturn
|
|
:: FormInput v => MFlowState v -> FlowM v m a -> Token -> m (FailBack a, MFlowState v)
|
|
-runFlowOnceReturn s f t =
|
|
- runStateT (runSup $ runFlowM f) (startState t)
|
|
-
|
|
-
|
|
-
|
|
--- | Run a persistent flow inside the current flow. It is identified by the procedure and
|
|
--- the string identifier.
|
|
--- unlike the normal flows, that run within infinite loops, runFlowIn executes once.
|
|
--- In subsequent executions, the flow will get the intermediate responses from te log
|
|
+runFlowOnceReturn s f t =
|
|
+ runStateT (runSup $ runFlowM f) (startState t)
|
|
+
|
|
+
|
|
+
|
|
+-- | Run a persistent flow inside the current flow. It is identified by the procedure and
|
|
+-- the string identifier.
|
|
+-- unlike the normal flows, that run within infinite loops, runFlowIn executes once.
|
|
+-- In subsequent executions, the flow will get the intermediate responses from te log
|
|
-- and will return the result without asking again.
|
|
-- This is useful for asking once, storing in the log and subsequently retrieving user
|
|
--- defined configurations by means of persistent flows with web formularies.
|
|
-runFlowIn
|
|
- :: (MonadIO m,
|
|
- FormInput view)
|
|
- => String
|
|
- -> FlowM view (Workflow IO) b
|
|
- -> FlowM view m b
|
|
-runFlowIn wf f= FlowM . Sup $ do
|
|
- st <- get
|
|
- let t = mfToken st
|
|
- (r,st') <- liftIO $ exec1nc wf $ runFlow1 st f t
|
|
- put st{mfPath= mfPath st'}
|
|
- case r of
|
|
- GoBack -> delWF wf ()
|
|
- return r
|
|
-
|
|
- where
|
|
- runFlow1 st f t= runStateT (runSup . runFlowM $ f) st
|
|
-
|
|
-
|
|
--- | to unlift a FlowM computation. useful for executing the configuration generated by runFLowIn
|
|
--- outside of the web flow (FlowM) monad
|
|
-runFlowConf :: (FormInput view, MonadIO m) => FlowM view m a -> m a
|
|
-runFlowConf f = do
|
|
- q <- liftIO newEmptyMVar -- `debug` (i++w++u)
|
|
+-- defined configurations by means of persistent flows with web formularies.
|
|
+runFlowIn
|
|
+ :: (MonadIO m,
|
|
+ FormInput view)
|
|
+ => String
|
|
+ -> FlowM view (Workflow IO) b
|
|
+ -> FlowM view m b
|
|
+runFlowIn wf f= FlowM . Sup $ do
|
|
+ st <- get
|
|
+ let t = mfToken st
|
|
+ (r,st') <- liftIO $ exec1nc wf $ runFlow1 st f t
|
|
+ put st{mfPath= mfPath st'}
|
|
+ case r of
|
|
+ GoBack -> delWF wf ()
|
|
+ return r
|
|
+
|
|
+ where
|
|
+ runFlow1 st f t= runStateT (runSup . runFlowM $ f) st
|
|
+
|
|
+
|
|
+-- | to unlift a FlowM computation. useful for executing the configuration generated by runFLowIn
|
|
+-- outside of the web flow (FlowM) monad
|
|
+runFlowConf :: (FormInput view, MonadIO m) => FlowM view m a -> m a
|
|
+runFlowConf f = do
|
|
+ q <- liftIO newEmptyMVar -- `debug` (i++w++u)
|
|
qr <- liftIO newEmptyMVar
|
|
- block <- liftIO $ newMVar True
|
|
- let t= Token "" "" "" [] [] block q qr
|
|
- evalStateT (runSup . runFlowM $ f ) mFlowState0{mfToken=t} >>= return . fromFailBack -- >> return ()
|
|
-
|
|
+ block <- liftIO $ newMVar True
|
|
+ let t= Token "" "" "" [] [] block q qr
|
|
+ evalStateT (runSup . runFlowM $ f ) mFlowState0{mfToken=t} >>= return . fromFailBack -- >> return ()
|
|
+
|
|
|
|
-- | run a transient Flow from the IO monad.
|
|
--runNav :: String -> FlowM Html IO () -> IO ()
|
|
--runNav ident f= exec1 ident $ runFlowOnce (transientNav f) undefined
|
|
-
|
|
-
|
|
--- | Clears the environment
|
|
-clearEnv :: MonadState (MFlowState view) m => m ()
|
|
-clearEnv= do
|
|
- st <- get
|
|
- put st{ mfEnv= []}
|
|
-
|
|
-
|
|
-
|
|
-instance (FormInput v,Serialize a)
|
|
- => Serialize (a,MFlowState v) where
|
|
- showp (x,s)= case mfDebug s of
|
|
- False -> showp x
|
|
- True -> showp(x, mfEnv s)
|
|
- readp= choice[nodebug, debug]
|
|
- where
|
|
- nodebug= readp >>= \x -> return (x, mFlowState0{mfSequence= inRecovery})
|
|
- debug= do
|
|
- (x,env) <- readp
|
|
- return (x,mFlowState0{mfEnv= env,mfSequence= inRecovery})
|
|
-
|
|
-
|
|
-
|
|
--- | stores the result of the flow in a persistent log. When restarted, it get the result
|
|
--- from the log and it does not execute it again. When no results are in the log, the computation
|
|
--- is executed. It is equivalent to 'Control.Workflow.step' but in the FlowM monad.
|
|
-step
|
|
- :: (Serialize a,
|
|
- Typeable view,
|
|
- FormInput view,
|
|
- MonadIO m,
|
|
- Typeable a) =>
|
|
- FlowM view m a
|
|
- -> FlowM view (Workflow m) a
|
|
-step f= do
|
|
- s <- get
|
|
- flowM $ Sup $ do
|
|
- (r,s') <- lift . WF.step $ runStateT (runSup $ runFlowM f) s
|
|
-
|
|
- -- when recovery of a workflow, the MFlow state is not considered
|
|
- when( mfSequence s' /= inRecovery) $ put s' -- !> (show $ mfSequence s') -- else put s{newAsk=True}
|
|
- return r
|
|
-
|
|
--- | to execute transient flows as if they were persistent
|
|
+
|
|
+
|
|
+-- | Clears the environment
|
|
+clearEnv :: MonadState (MFlowState view) m => m ()
|
|
+clearEnv= do
|
|
+ st <- get
|
|
+ put st{ mfEnv= []}
|
|
+
|
|
+
|
|
+
|
|
+instance (FormInput v,Serialize a)
|
|
+ => Serialize (a,MFlowState v) where
|
|
+ showp (x,s)= case mfDebug s of
|
|
+ False -> showp x
|
|
+ True -> showp(x, mfEnv s)
|
|
+ readp= choice[nodebug, debug]
|
|
+ where
|
|
+ nodebug= readp >>= \x -> return (x, mFlowState0{mfSequence= inRecovery})
|
|
+ debug= do
|
|
+ (x,env) <- readp
|
|
+ return (x,mFlowState0{mfEnv= env,mfSequence= inRecovery})
|
|
+
|
|
+
|
|
+
|
|
+-- | stores the result of the flow in a persistent log. When restarted, it get the result
|
|
+-- from the log and it does not execute it again. When no results are in the log, the computation
|
|
+-- is executed. It is equivalent to 'Control.Workflow.step' but in the FlowM monad.
|
|
+step
|
|
+ :: (Serialize a,
|
|
+ Typeable view,
|
|
+ FormInput view,
|
|
+ MonadIO m,
|
|
+ Typeable a) =>
|
|
+ FlowM view m a
|
|
+ -> FlowM view (Workflow m) a
|
|
+step f= do
|
|
+ s <- get
|
|
+ FlowM $ Sup $ do
|
|
+ (r,s') <- lift . WF.step $ runStateT (runSup $ runFlowM f) s
|
|
+
|
|
+ -- when recovery of a workflow, the MFlow state is not considered
|
|
+ when( mfSequence s' /= inRecovery) $ put s' -- !> (show $ mfSequence s') -- else put s{newAsk=True}
|
|
+ return r
|
|
+
|
|
+-- | to execute transient flows as if they were persistent
|
|
-- it can be used instead of step, but it does log nothing.
|
|
-- Thus, it is faster and convenient when no session state must be stored beyond the lifespan of
|
|
--- the server process.
|
|
---
|
|
--- > transient $ runFlow f === runFlow $ transientNav f
|
|
-transientNav
|
|
- :: (Serialize a,
|
|
- Typeable view,
|
|
- FormInput view,
|
|
- Typeable a) =>
|
|
- FlowM view IO a
|
|
- -> FlowM view (Workflow IO) a
|
|
-transientNav f= do
|
|
- s <- get
|
|
- flowM $ Sup $ do
|
|
- (r,s') <- lift . unsafeIOtoWF $ runStateT (runSup $ runFlowM f) s
|
|
- put s'
|
|
- return r
|
|
-
|
|
---stepWFRef
|
|
--- :: (Serialize a,
|
|
--- Typeable view,
|
|
--- FormInput view,
|
|
--- MonadIO m,
|
|
--- Typeable a) =>
|
|
--- FlowM view m a
|
|
--- -> FlowM view (Workflow m) (WFRef (FailBack a),a)
|
|
---stepWFRef f= do
|
|
--- s <- get
|
|
--- flowM $ Sup $ do
|
|
--- (r,s') <- lift . WF.stepWFRef $ runStateT (runSup $ runFlowM f) s
|
|
--- -- when recovery of a workflow, the MFlow state is not considered
|
|
--- when( mfSequence s' >0) $ put s'
|
|
--- return r
|
|
-
|
|
---step f= do
|
|
--- s <- get
|
|
--- flowM $ Sup $ do
|
|
--- (r,s') <- do
|
|
--- (br,s') <- runStateT (runSup $ runFlowM f) s
|
|
--- case br of
|
|
--- NoBack r -> WF.step $ return r
|
|
--- BackPoint r -> WF.step $ return r
|
|
--- GoBack -> undoStep
|
|
--- -- when recovery of a workflow, the MFlow state is not considered
|
|
--- when( mfSequence s' >0) $ put s'
|
|
--- return r
|
|
-
|
|
-
|
|
-
|
|
---stepDebug
|
|
--- :: (Serialize a,
|
|
--- Typeable view,
|
|
--- FormInput view,
|
|
--- Monoid view,
|
|
--- MonadIO m,
|
|
--- Typeable a) =>
|
|
--- FlowM view m a
|
|
--- -> FlowM view (Workflow m) a
|
|
---stepDebug f= Sup $ do
|
|
--- s <- get
|
|
--- (r, s') <- lift $ do
|
|
--- (r',stat)<- do
|
|
--- rec <- isInRecover
|
|
--- case rec of
|
|
--- True ->do (r', s'') <- getStep 0
|
|
--- return (r',s{mfEnv= mfEnv (s'' `asTypeOf`s)})
|
|
--- False -> return (undefined,s)
|
|
--- (r'', s''') <- WF.stepDebug $ runStateT (runSup f) stat >>= \(r,s)-> return (r, s)
|
|
--- return $ (r'' `asTypeOf` r', s''' )
|
|
--- put s'
|
|
--- return r
|
|
-
|
|
-
|
|
-
|
|
-data ParamResult v a= NoParam | NotValidated String v | Validated a deriving (Read, Show)
|
|
-
|
|
-valToMaybe (Validated x)= Just x
|
|
-valToMaybe _= Nothing
|
|
-
|
|
-isValidated (Validated x)= True
|
|
-isValidated _= False
|
|
-
|
|
-fromValidated (Validated x)= x
|
|
-fromValidated NoParam= error $ "fromValidated : NoParam"
|
|
-fromValidated (NotValidated s err)= error $ "fromValidated: NotValidated "++ s
|
|
-
|
|
-
|
|
-
|
|
-getParam1 :: (Monad m, MonadState (MFlowState v) m, Typeable a, Read a, FormInput v)
|
|
- => String -> Params -> m (ParamResult v a)
|
|
-getParam1 par req = case lookup par req of
|
|
- Just x -> readParam x
|
|
- Nothing -> return NoParam
|
|
+-- the server process.
|
|
+--
|
|
+-- > transient $ runFlow f === runFlow $ transientNav f
|
|
+transientNav
|
|
+ :: (Serialize a,
|
|
+ Typeable view,
|
|
+ FormInput view,
|
|
+ Typeable a) =>
|
|
+ FlowM view IO a
|
|
+ -> FlowM view (Workflow IO) a
|
|
+transientNav f= do
|
|
+ s <- get
|
|
+ FlowM $ Sup $ do
|
|
+ (r,s') <- lift . unsafeIOtoWF $ runStateT (runSup $ runFlowM f) s
|
|
+ put s'
|
|
+ return r
|
|
+
|
|
+--stepWFRef
|
|
+-- :: (Serialize a,
|
|
+-- Typeable view,
|
|
+-- FormInput view,
|
|
+-- MonadIO m,
|
|
+-- Typeable a) =>
|
|
+-- FlowM view m a
|
|
+-- -> FlowM view (Workflow m) (WFRef (FailBack a),a)
|
|
+--stepWFRef f= do
|
|
+-- s <- get
|
|
+-- flowM $ Sup $ do
|
|
+-- (r,s') <- lift . WF.stepWFRef $ runStateT (runSup $ runFlowM f) s
|
|
+-- -- when recovery of a workflow, the MFlow state is not considered
|
|
+-- when( mfSequence s' >0) $ put s'
|
|
+-- return r
|
|
+
|
|
+--step f= do
|
|
+-- s <- get
|
|
+-- flowM $ Sup $ do
|
|
+-- (r,s') <- do
|
|
+-- (br,s') <- runStateT (runSup $ runFlowM f) s
|
|
+-- case br of
|
|
+-- NoBack r -> WF.step $ return r
|
|
+-- BackPoint r -> WF.step $ return r
|
|
+-- GoBack -> undoStep
|
|
+-- -- when recovery of a workflow, the MFlow state is not considered
|
|
+-- when( mfSequence s' >0) $ put s'
|
|
+-- return r
|
|
+
|
|
+
|
|
+
|
|
+--stepDebug
|
|
+-- :: (Serialize a,
|
|
+-- Typeable view,
|
|
+-- FormInput view,
|
|
+-- Monoid view,
|
|
+-- MonadIO m,
|
|
+-- Typeable a) =>
|
|
+-- FlowM view m a
|
|
+-- -> FlowM view (Workflow m) a
|
|
+--stepDebug f= Sup $ do
|
|
+-- s <- get
|
|
+-- (r, s') <- lift $ do
|
|
+-- (r',stat)<- do
|
|
+-- rec <- isInRecover
|
|
+-- case rec of
|
|
+-- True ->do (r', s'') <- getStep 0
|
|
+-- return (r',s{mfEnv= mfEnv (s'' `asTypeOf`s)})
|
|
+-- False -> return (undefined,s)
|
|
+-- (r'', s''') <- WF.stepDebug $ runStateT (runSup f) stat >>= \(r,s)-> return (r, s)
|
|
+-- return $ (r'' `asTypeOf` r', s''' )
|
|
+-- put s'
|
|
+-- return r
|
|
+
|
|
+
|
|
+
|
|
+data ParamResult v a= NoParam | NotValidated String v | Validated a deriving (Read, Show)
|
|
+
|
|
+valToMaybe (Validated x)= Just x
|
|
+valToMaybe _= Nothing
|
|
+
|
|
+isValidated (Validated x)= True
|
|
+isValidated _= False
|
|
+
|
|
+fromValidated (Validated x)= x
|
|
+fromValidated NoParam= error $ "fromValidated : NoParam"
|
|
+fromValidated (NotValidated s err)= error $ "fromValidated: NotValidated "++ s
|
|
+
|
|
+
|
|
+
|
|
+getParam1 :: (Monad m, MonadState (MFlowState v) m, Typeable a, Read a, FormInput v)
|
|
+ => String -> Params -> m (ParamResult v a)
|
|
+getParam1 par req = case lookup par req of
|
|
+ Just x -> readParam x
|
|
+ Nothing -> return NoParam
|
|
|
|
-- Read a segment in the REST path. if it does not match with the type requested
|
|
--- or if there is no remaining segment, it returns Nothing
|
|
+-- or if there is no remaining segment, it returns Nothing
|
|
getRestParam :: (Read a, Typeable a, Monad m, Functor m, MonadState (MFlowState v) m, FormInput v)
|
|
- => m (Maybe a)
|
|
-getRestParam= do
|
|
- st <- get
|
|
- let lpath = mfPath st
|
|
+ => m (Maybe a)
|
|
+getRestParam= do
|
|
+ st <- get
|
|
+ let lpath = mfPath st
|
|
if linkMatched st
|
|
- then return Nothing
|
|
+ then return Nothing
|
|
else case stripPrefix (mfPagePath st) lpath of
|
|
Nothing -> return Nothing
|
|
- Just [] -> return Nothing
|
|
- Just xs ->
|
|
- case stripPrefix (mfPrefix st) (head xs) of
|
|
- Nothing -> return Nothing
|
|
- Just name -> do
|
|
- r <- fmap valToMaybe $ readParam name
|
|
- when (isJust r) $ modify $ \s -> s{inSync= True
|
|
- ,linkMatched= True
|
|
- ,mfPagePath= mfPagePath s++[name]}
|
|
- return r
|
|
-
|
|
+ Just [] -> return Nothing
|
|
+ Just xs -> do
|
|
+-- case stripPrefix (mfPrefix st) (head xs) of
|
|
+-- Nothing -> return Nothing
|
|
+-- Just name ->
|
|
+ let name= head xs
|
|
+ r <- fmap valToMaybe $ readParam name
|
|
+ when (isJust r) $ modify $ \s -> s{inSync= True
|
|
+ ,linkMatched= True
|
|
+ ,mfPagePath= mfPagePath s++[name]}
|
|
+ return r
|
|
+
|
|
|
|
|
|
-- | return the value of a post or get param in the form ?param=value¶m2=value2...
|
|
@@ -1246,31 +1284,31 @@
|
|
st <- get
|
|
r <- getParam1 par $ mfEnv st
|
|
return $ valToMaybe r
|
|
-
|
|
-readParam :: (Monad m, MonadState (MFlowState v) m, Typeable a, Read a, FormInput v)
|
|
- => String -> m (ParamResult v a)
|
|
-readParam x1 = r
|
|
- where
|
|
- r= do
|
|
- modify $ \s -> s{inSync= True}
|
|
- maybeRead x1
|
|
-
|
|
- getType :: m (ParamResult v a) -> a
|
|
- getType= undefined
|
|
- x= getType r
|
|
- maybeRead str= do
|
|
- let typeofx = typeOf x
|
|
+
|
|
+readParam :: (Monad m, MonadState (MFlowState v) m, Typeable a, Read a, FormInput v)
|
|
+ => String -> m (ParamResult v a)
|
|
+readParam x1 = r
|
|
+ where
|
|
+ r= do
|
|
+ modify $ \s -> s{inSync= True}
|
|
+ maybeRead x1
|
|
+
|
|
+ getType :: m (ParamResult v a) -> a
|
|
+ getType= undefined
|
|
+ x= getType r
|
|
+ maybeRead str= do
|
|
+ let typeofx = typeOf x
|
|
if typeofx == typeOf ( undefined :: String) then
|
|
return . Validated $ unsafeCoerce str
|
|
else if typeofx == typeOf (undefined :: T.Text) then
|
|
- return . Validated . unsafeCoerce $ T.pack str
|
|
- else case readsPrec 0 $ str of
|
|
- [(x,"")] -> return $ Validated x
|
|
- _ -> do
|
|
- let err= inred . fromStr $ "can't read \"" ++ str ++ "\" as type " ++ show (typeOf x)
|
|
- return $ NotValidated str err
|
|
-
|
|
-
|
|
+ return . Validated . unsafeCoerce $ T.pack str
|
|
+ else case readsPrec 0 $ str of
|
|
+ [(x,"")] -> return $ Validated x
|
|
+ _ -> do
|
|
+ let err= inred . fromStr $ "can't read \"" ++ str ++ "\" as type " ++ show (typeOf x)
|
|
+ return $ NotValidated str err
|
|
+
|
|
+
|
|
---- Requirements
|
|
|
|
|
|
@@ -1278,39 +1316,41 @@
|
|
-- Web page or in the server when a widget specifies this. @requires@ is the
|
|
-- procedure to be called with the list of requirements.
|
|
-- Various widgets in the page can require the same element, MFlow will install it once.
|
|
+
|
|
+
|
|
requires rs =do
|
|
st <- get
|
|
let l = mfRequirements st
|
|
--- let rs'= map Requirement rs \\ l
|
|
put st {mfRequirements= l ++ map Requirement rs}
|
|
|
|
-
|
|
+unfold (JScriptFile f ss)= JScript loadScript:map (\s-> JScriptFile f [s]) ss
|
|
+unfold x= [x]
|
|
|
|
data Requirement= forall a.(Show a,Typeable a,Requirements a) => Requirement a deriving Typeable
|
|
|
|
class Requirements a where
|
|
- installRequirements :: (Monad m,FormInput view) => Bool -> [a] -> m view
|
|
+ installRequirements :: (MonadState (MFlowState view) m,MonadIO m,FormInput view) => [a] -> m view
|
|
|
|
instance Show Requirement where
|
|
show (Requirement a)= show a ++ "\n"
|
|
|
|
-installAllRequirements :: ( Monad m, FormInput view) => WState view m view
|
|
+installAllRequirements :: ( MonadIO m, FormInput view) => WState view m view
|
|
installAllRequirements= do
|
|
st <- get
|
|
let rs = mfRequirements st
|
|
- auto = mfAutorefresh st
|
|
- installAllRequirements1 auto mempty rs
|
|
+ installAllRequirements1 mempty rs
|
|
+
|
|
where
|
|
|
|
- installAllRequirements1 _ v []= return v
|
|
- installAllRequirements1 auto v rs= do
|
|
+ installAllRequirements1 v []= return v
|
|
+ installAllRequirements1 v rs= do
|
|
let typehead= case head rs of {Requirement r -> typeOf r}
|
|
(rs',rs'')= partition1 typehead rs
|
|
v' <- installRequirements2 rs'
|
|
- installAllRequirements1 auto (v `mappend` v') rs''
|
|
+ installAllRequirements1 (v `mappend` v') rs''
|
|
where
|
|
installRequirements2 []= return $ fromStrNoEncode ""
|
|
- installRequirements2 (Requirement r:rs)= installRequirements auto $ r:unmap rs
|
|
+ installRequirements2 (Requirement r:rs)= installRequirements $ r:unmap rs
|
|
unmap []=[]
|
|
unmap (Requirement r:rs)= unsafeCoerce r:unmap rs
|
|
partition1 typehead xs = foldr select ([],[]) xs
|
|
@@ -1320,155 +1360,194 @@
|
|
in if typer== typehead then ( x:ts,fs)
|
|
else (ts, x:fs)
|
|
|
|
--- Web requirements ---
|
|
-loadjsfile filename lcallbacks=
|
|
- let name= addrStr filename in
|
|
- "var fileref = document.getElementById('"++name++"');\
|
|
- \if (fileref === null){\
|
|
+-- Web requirements ---
|
|
+loadjsfile filename=
|
|
+ let name= addrStr filename
|
|
+ in "\n"++name++"=loadScript('"++name++"','"++filename++"');\n"
|
|
+
|
|
+loadScript ="function loadScript(name, filename){\
|
|
+ \var fileref = document.getElementById(name);\
|
|
+ \if (fileref === null){\
|
|
\fileref=document.createElement('script');\
|
|
- \fileref.setAttribute('id','"++name++"');\
|
|
- \fileref.setAttribute('type','text/javascript');\
|
|
- \fileref.setAttribute('src',\'" ++ filename ++ "\');\
|
|
- \document.getElementsByTagName('head')[0].appendChild(fileref);};"
|
|
- ++ onload
|
|
- where
|
|
- onload= case lcallbacks of
|
|
- [] -> ""
|
|
- cs -> "fileref.onload = function() {"++ (concat $ nub cs)++"};"
|
|
-
|
|
-
|
|
-loadjs content= content
|
|
-
|
|
-
|
|
-loadcssfile filename=
|
|
- "var fileref=document.createElement('link');\
|
|
- \fileref.setAttribute('rel', 'stylesheet');\
|
|
- \fileref.setAttribute('type', 'text/css');\
|
|
- \fileref.setAttribute('href', \'"++filename++"\');\
|
|
- \document.getElementsByTagName('head')[0].appendChild(fileref);"
|
|
-
|
|
-
|
|
-loadcss content=
|
|
- "var fileref=document.createElement('link');\
|
|
- \fileref.setAttribute('rel', 'stylesheet');\
|
|
- \fileref.setAttribute('type', 'text/css');\
|
|
- \fileref.innerText=\""++content++"\";\
|
|
- \document.getElementsByTagName('head')[0].appendChild(fileref);"
|
|
-
|
|
-
|
|
-data WebRequirement= JScriptFile
|
|
- String
|
|
- [String] -- ^ Script URL and the list of scripts to be executed when loaded
|
|
- | CSSFile String -- ^ a CSS file URL
|
|
- | CSS String -- ^ a String with a CSS description
|
|
- | JScript String -- ^ a string with a valid JavaScript
|
|
- | ServerProc (String, Flow) -- ^ a server procedure
|
|
- deriving(Typeable,Eq,Ord,Show)
|
|
-
|
|
-instance Eq (String, Flow) where
|
|
+ \fileref.setAttribute('id',name);\
|
|
+ \fileref.setAttribute('type','text/javascript');\
|
|
+ \fileref.setAttribute('src',filename);\
|
|
+ \document.getElementsByTagName('head')[0].appendChild(fileref);}\
|
|
+ \return fileref};\n\
|
|
+ \function addLoadEvent(elem,func) {\
|
|
+ \var oldonload = elem.onload;\
|
|
+ \if (typeof elem.onload != 'function') {\
|
|
+ \elem.onload = func;\
|
|
+ \} else {\
|
|
+ \elem.onload = function() {\
|
|
+ \if (oldonload) {\
|
|
+ \oldonload();\
|
|
+ \}\
|
|
+ \func();\
|
|
+ \}\
|
|
+ \}\
|
|
+ \}"
|
|
+
|
|
+loadCallback depend script=
|
|
+ let varname= addrStr depend in
|
|
+ "\naddLoadEvent("++varname++",function(){"++ script++"});"
|
|
+
|
|
+
|
|
+
|
|
+
|
|
+loadcssfile filename=
|
|
+ "var fileref=document.createElement('link');\
|
|
+ \fileref.setAttribute('rel', 'stylesheet');\
|
|
+ \fileref.setAttribute('type', 'text/css');\
|
|
+ \fileref.setAttribute('href', \'"++filename++"\');\
|
|
+ \document.getElementsByTagName('head')[0].appendChild(fileref);"
|
|
+
|
|
+
|
|
+loadcss content=
|
|
+ "var fileref=document.createElement('link');\
|
|
+ \fileref.setAttribute('rel', 'stylesheet');\
|
|
+ \fileref.setAttribute('type', 'text/css');\
|
|
+ \fileref.innerText=\""++content++"\";\
|
|
+ \document.getElementsByTagName('head')[0].appendChild(fileref);"
|
|
+
|
|
+
|
|
+data WebRequirement= JScriptFile
|
|
+ String
|
|
+ [String] -- ^ Script URL and the list of scripts to be executed when loaded
|
|
+ | CSSFile String -- ^ a CSS file URL
|
|
+ | CSS String -- ^ a String with a CSS description
|
|
+ | JScript String -- ^ a string with a valid JavaScript
|
|
+ | ServerProc (String, Flow) -- ^ a server procedure
|
|
+ deriving(Typeable,Eq,Ord,Show)
|
|
+
|
|
+instance Eq (String, Flow) where
|
|
(x,_) == (y,_)= x == y
|
|
-
|
|
-instance Ord (String, Flow) where
|
|
- compare(x,_) (y,_)= compare x y
|
|
-instance Show (String, Flow) where
|
|
- show (x,_)= show x
|
|
-
|
|
-instance Requirements WebRequirement where
|
|
- installRequirements= installWebRequirements
|
|
-
|
|
-
|
|
-
|
|
-installWebRequirements :: (Monad m,FormInput view) => Bool -> [WebRequirement] -> m view
|
|
-installWebRequirements auto rs= do
|
|
- let s = jsRequirements auto $ sort rs
|
|
-
|
|
- return $ ftag "script" (fromStrNoEncode s)
|
|
-
|
|
-
|
|
-jsRequirements _ []= ""
|
|
-
|
|
-jsRequirements False (r@(JScriptFile f c) : r'@(JScriptFile f' c'):rs)
|
|
- | f==f' = jsRequirements False $ JScriptFile f (nub $ c++c'):rs
|
|
- | otherwise= strRequirement r ++ jsRequirements False (r':rs)
|
|
-
|
|
-jsRequirements True (r@(JScriptFile f c) : r'@(JScriptFile f' c'):rs)
|
|
- | f==f' = concatMap strRequirement(map JScript $ nub (c' ++ c)) ++ jsRequirements True rs
|
|
- | otherwise= strRequirement r ++ jsRequirements True (r':rs)
|
|
-
|
|
-
|
|
-
|
|
-
|
|
-jsRequirements auto (r:r':rs)
|
|
- | r== r' = jsRequirements auto $ r:rs
|
|
- | otherwise= strRequirement r ++ jsRequirements auto (r':rs)
|
|
-
|
|
-jsRequirements auto (r:rs)= strRequirement r++jsRequirements auto rs
|
|
-
|
|
-strRequirement (CSSFile s') = loadcssfile s'
|
|
-strRequirement (CSS s') = loadcss s'
|
|
-strRequirement (JScriptFile s' call) = loadjsfile s' call
|
|
-strRequirement (JScript s') = loadjs s'
|
|
-strRequirement (ServerProc f)= (unsafePerformIO $! addMessageFlows [f]) `seq` ""
|
|
-
|
|
-
|
|
-
|
|
-
|
|
-
|
|
---- AJAX ----
|
|
-ajaxScript=
|
|
- "function loadXMLObj()" ++
|
|
- "{" ++
|
|
- "var xmlhttp;" ++
|
|
- "if (window.XMLHttpRequest)" ++
|
|
- "{"++
|
|
- " xmlhttp=new XMLHttpRequest();" ++
|
|
- " }" ++
|
|
- "else" ++
|
|
- "{"++
|
|
- " xmlhttp=new ActiveXObject('Microsoft.XMLHTTP');" ++
|
|
- " }" ++
|
|
- "return xmlhttp" ++
|
|
- "};" ++
|
|
-
|
|
- " xmlhttp= loadXMLObj();" ++
|
|
- " noparam= '';"++
|
|
- ""++
|
|
- "function doServer (servproc,param,param2){" ++
|
|
- " xmlhttp.open('GET',servproc+'?ajax='+param+'&val='+param2,true);" ++
|
|
- " xmlhttp.send();};" ++
|
|
- ""++
|
|
- "xmlhttp.onreadystatechange=function()" ++
|
|
- " {" ++
|
|
- " if (xmlhttp.readyState== 4 && xmlhttp.status==200)" ++
|
|
- " {" ++
|
|
- " eval(xmlhttp.responseText);" ++
|
|
- " }" ++
|
|
- " };" ++
|
|
- ""
|
|
-
|
|
-formPrefix st form anchored= do
|
|
+
|
|
+instance Ord (String, Flow) where
|
|
+ compare(x,_) (y,_)= compare x y
|
|
+instance Show (String, Flow) where
|
|
+ show (x,_)= show x
|
|
+
|
|
+instance Requirements WebRequirement where
|
|
+ installRequirements= installWebRequirements
|
|
+
|
|
+
|
|
+
|
|
+installWebRequirements
|
|
+ :: (MonadState(MFlowState view) m,MonadIO m,FormInput view) => [WebRequirement] -> m view
|
|
+installWebRequirements rs= do
|
|
+ installed <- gets mfInstalledScripts
|
|
+ let rs'= (nub rs) \\ installed
|
|
+
|
|
+ strs <- mapM strRequirement rs' -- !>( "OLD="++show installed) !> ("new="++show rs')
|
|
+ case null strs of
|
|
+ True -> return mempty
|
|
+ False -> return . ftag "script" . fromStrNoEncode $ concat strs
|
|
+
|
|
+
|
|
+strRequirement r=do
|
|
+ r1 <- strRequirement' r
|
|
+ modify $ \st -> st{mfInstalledScripts= mfInstalledScripts st ++ [r]}
|
|
+ return r1
|
|
+
|
|
+strRequirement' (CSSFile scr) = return $ loadcssfile scr
|
|
+strRequirement' (CSS scr) = return $ loadcss scr
|
|
+strRequirement' (JScriptFile file scripts) = do
|
|
+ installed <- gets mfInstalledScripts
|
|
+ let hasLoadScript (JScriptFile _ _)= True
|
|
+ hasLoadScript _= False
|
|
+ inst2= dropWhile (not . hasLoadScript) installed
|
|
+ hasSameFile file (JScriptFile fil _)= if file== fil then True else False
|
|
+ hasSameFile _ _= False
|
|
+ case (inst2,find (hasSameFile file) inst2) of
|
|
+ ([],_) ->
|
|
+ -- no script file has been loaded previously
|
|
+ return $ loadScript <> loadjsfile file <> concatMap(loadCallback file) scripts
|
|
+ (_,Just _) -> do
|
|
+ -- This script file has been already loaded or demanded for load
|
|
+ autorefresh <- gets mfAutorefresh
|
|
+ case autorefresh of
|
|
+ -- demanded for load, not loaded
|
|
+ False -> return $ concatMap(loadCallback file) scripts
|
|
+ -- already loaded
|
|
+ True -> return $ concat scripts
|
|
+ -- other script file has been loaded or demanded load, so loadScript is already installed
|
|
+ _ -> return $ loadjsfile file <> concatMap(loadCallback file) scripts
|
|
+
|
|
+
|
|
+strRequirement' (JScript scr) = return scr
|
|
+strRequirement' (ServerProc f)= do
|
|
+ liftIO $ addMessageFlows [f]
|
|
+ return ""
|
|
+
|
|
+
|
|
+
|
|
+
|
|
+
|
|
+--- AJAX ----
|
|
+ajaxScript=
|
|
+ "function loadXMLObj()" ++
|
|
+ "{" ++
|
|
+ "var xmlhttp;" ++
|
|
+ "if (window.XMLHttpRequest)" ++
|
|
+ "{"++
|
|
+ " xmlhttp=new XMLHttpRequest();" ++
|
|
+ " }" ++
|
|
+ "else" ++
|
|
+ "{"++
|
|
+ " xmlhttp=new ActiveXObject('Microsoft.XMLHTTP');" ++
|
|
+ " }" ++
|
|
+ "return xmlhttp" ++
|
|
+ "};" ++
|
|
+
|
|
+ " xmlhttp= loadXMLObj();" ++
|
|
+ " noparam= '';"++
|
|
+ ""++
|
|
+ "function doServer (servproc,param,param2){" ++
|
|
+ " xmlhttp.open('GET',servproc+'?ajax='+param+'&val='+param2,true);" ++
|
|
+ " xmlhttp.send();};" ++
|
|
+ ""++
|
|
+ "xmlhttp.onreadystatechange=function()" ++
|
|
+ " {" ++
|
|
+ " if (xmlhttp.readyState== 4 && xmlhttp.status==200)" ++
|
|
+ " {" ++
|
|
+ " eval(xmlhttp.responseText);" ++
|
|
+ " }" ++
|
|
+ " };" ++
|
|
+ ""
|
|
+
|
|
+formPrefix st form anchored= do
|
|
let verb = twfname $ mfToken st
|
|
- path = currentPath st
|
|
- (anchor,anchorf)
|
|
- <- case anchored of
|
|
- True -> do
|
|
- anchor <- genNewId
|
|
- return ('#':anchor, (ftag "a") mempty `attrs` [("name",anchor)])
|
|
- False -> return (mempty,mempty)
|
|
- return $ formAction (path ++ anchor ) $ anchorf <> form -- !> anchor
|
|
-
|
|
--- | insert a form tag if the widget has form input fields. If not, it does nothing
|
|
-insertForm w=View $ do
|
|
- FormElm forms mx <- runView w
|
|
- st <- get
|
|
- cont <- case needForm1 st of
|
|
- True -> do
|
|
- frm <- formPrefix st forms False
|
|
- put st{needForm= HasForm}
|
|
- return frm
|
|
- _ -> return forms
|
|
-
|
|
- return $ FormElm cont mx
|
|
+ path = currentPath st
|
|
+ hasfile= mfFileUpload st
|
|
+ attr= case hasfile of
|
|
+ True -> [("enctype","multipart/form-data")]
|
|
+ False -> []
|
|
+ (anchor,anchorf)
|
|
+ <- case anchored of
|
|
+ True -> do
|
|
+ anchor <- genNewId
|
|
+ return ('#':anchor, (ftag "a") mempty `attrs` [("name",anchor)])
|
|
+ False -> return (mempty,mempty)
|
|
+ return $ formAction (path ++ anchor ) "POST" ( anchorf <> form ) `attrs` attr
|
|
+
|
|
+
|
|
+
|
|
+
|
|
+
|
|
+
|
|
+-- | insert a form tag if the widget has form input fields. If not, it does nothing
|
|
+insertForm w=View $ do
|
|
+ FormElm forms mx <- runView w
|
|
+ st <- get
|
|
+ cont <- case needForm st of
|
|
+ HasElems -> do
|
|
+ frm <- formPrefix st forms False
|
|
+ put st{needForm= HasForm}
|
|
+ return frm
|
|
+ _ -> return forms
|
|
+
|
|
+ return $ FormElm cont mx
|
|
|
|
-- isert a form tag if necessary when two pieces of HTML have to mix as a result of >>= >> <|> or <+> operators
|
|
controlForms :: (FormInput v, MonadState (MFlowState v) m)
|
|
@@ -1481,40 +1560,40 @@
|
|
v1' <- formPrefix s1 v1 True
|
|
return (v1' <> v2 , True)
|
|
|
|
- _ -> return (v1 <> v2, False)
|
|
-
|
|
-currentPath st= concat ['/':v| v <- mfPagePath st ]
|
|
-
|
|
--- | Generate a new string. Useful for creating tag identifiers and other attributes.
|
|
---
|
|
--- if the page is refreshed, the identifiers generated are the same.
|
|
-genNewId :: MonadState (MFlowState view) m => m String
|
|
-genNewId= do
|
|
- st <- get
|
|
- case mfCached st of
|
|
- False -> do
|
|
- let n= mfSequence st
|
|
- prefseq= mfPrefix st
|
|
- put $ st{mfSequence= n+1}
|
|
-
|
|
- return $ 'p':show n++prefseq
|
|
- True -> do
|
|
- let n = mfSeqCache st
|
|
- put $ st{mfSeqCache=n+1}
|
|
- return $ 'c' : (show n)
|
|
-
|
|
--- | get the next ideitifier that will be created by genNewId
|
|
-getNextId :: MonadState (MFlowState view) m => m String
|
|
-getNextId= do
|
|
- st <- get
|
|
- case mfCached st of
|
|
- False -> do
|
|
- let n= mfSequence st
|
|
- prefseq= mfPrefix st
|
|
- return $ 'p':show n++prefseq
|
|
- True -> do
|
|
- let n = mfSeqCache st
|
|
- return $ 'c' : (show n)
|
|
+ _ -> return (v1 <> v2, False)
|
|
+
|
|
+currentPath st= concat ['/':v| v <- mfPagePath st ]
|
|
+
|
|
+-- | Generate a new string. Useful for creating tag identifiers and other attributes.
|
|
+--
|
|
+-- if the page is refreshed, the identifiers generated are the same.
|
|
+genNewId :: MonadState (MFlowState view) m => m String
|
|
+genNewId= do
|
|
+ st <- get
|
|
+ case mfCached st of
|
|
+ False -> do
|
|
+ let n= mfSequence st
|
|
+ prefseq= mfPrefix st
|
|
+ put $ st{mfSequence= n+1}
|
|
+
|
|
+ return $ 'p':show n++prefseq
|
|
+ True -> do
|
|
+ let n = mfSeqCache st
|
|
+ put $ st{mfSeqCache=n+1}
|
|
+ return $ 'c' : (show n)
|
|
+
|
|
+-- | get the next ideitifier that will be created by genNewId
|
|
+getNextId :: MonadState (MFlowState view) m => m String
|
|
+getNextId= do
|
|
+ st <- get
|
|
+ case mfCached st of
|
|
+ False -> do
|
|
+ let n= mfSequence st
|
|
+ prefseq= mfPrefix st
|
|
+ return $ 'p':show n++prefseq
|
|
+ True -> do
|
|
+ let n = mfSeqCache st
|
|
+ return $ 'c' : (show n)
|
|
+
|
|
|
|
|
|
-
|
|
diff -ru orig/src/MFlow/Forms/Test.hs new/src/MFlow/Forms/Test.hs
|
|
--- orig/src/MFlow/Forms/Test.hs 2014-06-10 05:51:26.977015856 +0300
|
|
+++ new/src/MFlow/Forms/Test.hs 2014-06-10 05:51:25.000000000 +0300
|
|
@@ -1,404 +1,404 @@
|
|
------------------------------------------------------------------------------
|
|
---
|
|
--- Module : MFlow.Forms.Test
|
|
--- Copyright :
|
|
--- License : BSD3
|
|
---
|
|
--- Maintainer : agocorona@gmail.com
|
|
--- Stability : experimental
|
|
--- Portability :
|
|
---
|
|
--- |
|
|
---
|
|
------------------------------------------------------------------------------
|
|
-{-# OPTIONS
|
|
- -XOverlappingInstances
|
|
- -XFlexibleInstances
|
|
- -XUndecidableInstances
|
|
- -XPatternGuards
|
|
- -XRecordWildCards
|
|
- #-}
|
|
-
|
|
-module MFlow.Forms.Test (Generate(..),runTest,runTest1,inject, ask, askt, userWidget, getUser, getUserSimple, verify) where
|
|
-import MFlow.Forms hiding(ask,askt,getUser,userWidget,getUserSimple)
|
|
-import qualified MFlow.Forms (ask)
|
|
-import MFlow.Forms.Internals
|
|
-import MFlow.Forms(FormInput(..))
|
|
-import MFlow.Forms.Admin
|
|
-import Control.Workflow as WF
|
|
-import Control.Concurrent
|
|
-import Control.Monad
|
|
-import MFlow
|
|
-import qualified Data.Map as M
|
|
-import Control.Monad.Trans
|
|
-import System.IO.Unsafe
|
|
-import System.Random
|
|
-import Data.Char(chr, ord)
|
|
-import Data.List
|
|
-import Data.Typeable
|
|
-import qualified Data.ByteString.Lazy.Char8 as B
|
|
-import Control.Concurrent.MVar
|
|
-import Data.TCache.Memoization
|
|
-import Control.Monad.State
|
|
-import Data.Monoid
|
|
-import Data.Maybe
|
|
-import Data.IORef
|
|
-import MFlow.Cookies(cookieuser)
|
|
-
|
|
-import Data.Dynamic
|
|
-import Data.TCache.Memoization
|
|
-
|
|
-
|
|
-
|
|
-class Generate a where
|
|
- generate :: IO a
|
|
-
|
|
-instance Generate a => Generate (Maybe a) where
|
|
- generate= do
|
|
- b <- randomRIO(0,1 :: Int)
|
|
- case b of 0 -> generate >>= return . Just ; _ -> return Nothing
|
|
-
|
|
-instance Generate String where
|
|
- generate= replicateM 5 $ randomRIO ('a','z')
|
|
-
|
|
-instance Generate Int where
|
|
- generate= randomRIO(1,1000)
|
|
-
|
|
-instance Generate Integer where
|
|
- generate= randomRIO(1,1000)
|
|
-
|
|
-
|
|
-instance (Generate a, Generate b) => Generate (a,b) where
|
|
- generate= fmap (,) generate `ap` generate
|
|
-
|
|
-
|
|
-instance (Generate a, Generate b) => Generate (Maybe a,Maybe b) where
|
|
- generate= do
|
|
- r <- generate
|
|
- case r of
|
|
- (Nothing,Nothing) -> generate
|
|
- other -> return other
|
|
-
|
|
-
|
|
-instance (Bounded a, Enum a) => Generate a where
|
|
- generate= mx
|
|
- where
|
|
- mx= do
|
|
- let x= typeOfIO mx
|
|
- n <- randomRIO ( fromEnum $ minBound `asTypeOf` x
|
|
- , fromEnum $ maxBound `asTypeOf` x)
|
|
- return $ toEnum n
|
|
- where
|
|
- typeOfIO :: IO a -> a
|
|
- typeOfIO = undefined
|
|
-
|
|
--- | run a list of flows with a number of simultaneous threads
|
|
-runTest :: [(Int, Flow)] -> IO ()
|
|
-runTest ps= do
|
|
- mapM_ (forkIO . run1) ps
|
|
- putStrLn $ "started " ++ (show . sum . fst $ unzip ps) ++ " threads"
|
|
-
|
|
- where
|
|
- run1 (nusers, proc) = replicateM_ nusers $ runTest1 proc
|
|
-
|
|
-runTest1 f = do
|
|
- atomicModifyIORef testNumber (\n -> (n+1,n+1))
|
|
- name <- generate
|
|
- x <- generate
|
|
- y <- generate
|
|
+-----------------------------------------------------------------------------
|
|
+--
|
|
+-- Module : MFlow.Forms.Test
|
|
+-- Copyright :
|
|
+-- License : BSD3
|
|
+--
|
|
+-- Maintainer : agocorona@gmail.com
|
|
+-- Stability : experimental
|
|
+-- Portability :
|
|
+--
|
|
+-- |
|
|
+--
|
|
+-----------------------------------------------------------------------------
|
|
+{-# OPTIONS
|
|
+ -XOverlappingInstances
|
|
+ -XFlexibleInstances
|
|
+ -XUndecidableInstances
|
|
+ -XPatternGuards
|
|
+ -XRecordWildCards
|
|
+ #-}
|
|
+
|
|
+module MFlow.Forms.Test (Generate(..),runTest,runTest1,inject, ask, askt, userWidget, getUser, getUserSimple, verify) where
|
|
+import MFlow.Forms hiding(ask,askt,getUser,userWidget,getUserSimple)
|
|
+import qualified MFlow.Forms (ask)
|
|
+import MFlow.Forms.Internals
|
|
+import MFlow.Forms(FormInput(..))
|
|
+import MFlow.Forms.Admin
|
|
+import Control.Workflow as WF
|
|
+import Control.Concurrent
|
|
+import Control.Monad
|
|
+import MFlow
|
|
+import qualified Data.Map as M
|
|
+import Control.Monad.Trans
|
|
+import System.IO.Unsafe
|
|
+import System.Random
|
|
+import Data.Char(chr, ord)
|
|
+import Data.List
|
|
+import Data.Typeable
|
|
+import qualified Data.ByteString.Lazy.Char8 as B
|
|
+import Control.Concurrent.MVar
|
|
+import Data.TCache.Memoization
|
|
+import Control.Monad.State
|
|
+import Data.Monoid
|
|
+import Data.Maybe
|
|
+import Data.IORef
|
|
+import MFlow.Cookies(cookieuser)
|
|
+
|
|
+import Data.Dynamic
|
|
+import Data.TCache.Memoization
|
|
+
|
|
+
|
|
+
|
|
+class Generate a where
|
|
+ generate :: IO a
|
|
+
|
|
+instance Generate a => Generate (Maybe a) where
|
|
+ generate= do
|
|
+ b <- randomRIO(0,1 :: Int)
|
|
+ case b of 0 -> generate >>= return . Just ; _ -> return Nothing
|
|
+
|
|
+instance Generate String where
|
|
+ generate= replicateM 5 $ randomRIO ('a','z')
|
|
+
|
|
+instance Generate Int where
|
|
+ generate= randomRIO(1,1000)
|
|
+
|
|
+instance Generate Integer where
|
|
+ generate= randomRIO(1,1000)
|
|
+
|
|
+
|
|
+instance (Generate a, Generate b) => Generate (a,b) where
|
|
+ generate= fmap (,) generate `ap` generate
|
|
+
|
|
+
|
|
+instance (Generate a, Generate b) => Generate (Maybe a,Maybe b) where
|
|
+ generate= do
|
|
+ r <- generate
|
|
+ case r of
|
|
+ (Nothing,Nothing) -> generate
|
|
+ other -> return other
|
|
+
|
|
+
|
|
+instance (Bounded a, Enum a) => Generate a where
|
|
+ generate= mx
|
|
+ where
|
|
+ mx= do
|
|
+ let x= typeOfIO mx
|
|
+ n <- randomRIO ( fromEnum $ minBound `asTypeOf` x
|
|
+ , fromEnum $ maxBound `asTypeOf` x)
|
|
+ return $ toEnum n
|
|
+ where
|
|
+ typeOfIO :: IO a -> a
|
|
+ typeOfIO = undefined
|
|
+
|
|
+-- | run a list of flows with a number of simultaneous threads
|
|
+runTest :: [(Int, Flow)] -> IO ()
|
|
+runTest ps= do
|
|
+ mapM_ (forkIO . run1) ps
|
|
+ putStrLn $ "started " ++ (show . sum . fst $ unzip ps) ++ " threads"
|
|
+
|
|
+ where
|
|
+ run1 (nusers, proc) = replicateM_ nusers $ runTest1 proc
|
|
+
|
|
+runTest1 f = do
|
|
+ atomicModifyIORef testNumber (\n -> (n+1,n+1))
|
|
+ name <- generate
|
|
+ x <- generate
|
|
+ y <- generate
|
|
z <- generate
|
|
-
|
|
- let t = Token x y z [] [] undefined undefined undefined
|
|
- WF.start name f t
|
|
-
|
|
-testNumber= unsafePerformIO $ newIORef 0
|
|
-
|
|
-getTestNumber :: MonadIO m => m Int
|
|
-getTestNumber= liftIO $ readIORef testNumber
|
|
-
|
|
--- | inject substitutes an expression by other. It may be used to override
|
|
--- ask interaction with the user. It should bee used infix for greater readability:
|
|
---
|
|
--- > ask something `inject` const someother
|
|
---
|
|
--- The parameter passed is the test number
|
|
--- if the flow has not been executed by runTest, inject return the original
|
|
-inject :: MonadIO m => m b -> (Int -> b) -> m b
|
|
-inject exp v= do
|
|
- n <- getTestNumber
|
|
- if n== 0 then exp else exp `seq` return $ v n
|
|
-
|
|
--- | a simulated ask that generate simulated user input of the type expected.
|
|
---
|
|
--- It forces the web page rendering, since it is monadic and can contain
|
|
--- side effects and load effects to be tested.
|
|
---
|
|
--- it is a substitute of 'ask' from "MFlow.Forms" for testing purposes.
|
|
-
|
|
--- execute 'runText' to initiate threads under different load conditions.
|
|
-ask :: (Generate a, MonadIO m, Functor m, FormInput v,Typeable v) => View v m a -> FlowM v m a
|
|
-ask w = do
|
|
- FormElm forms mx <- FlowM . lift $ runView w
|
|
- r <- liftIO generate
|
|
- let n= B.length $ toByteString forms
|
|
- breturn $ n `seq` mx `seq` r
|
|
--- let u= undefined
|
|
--- liftIO $ runStateT (runView mf) s
|
|
--- bool <- liftIO generate
|
|
--- case bool of
|
|
--- False -> fail ""
|
|
--- True -> do
|
|
--- b <- liftIO generate
|
|
--- r <- liftIO generate
|
|
--- case (b,r) of
|
|
--- (True,x) -> breturn x
|
|
--- _ -> ask w
|
|
-
|
|
-
|
|
--- | instead of generating a result like `ask`, the result is given as the first parameter
|
|
--- so it does not need a Generate instance.
|
|
---
|
|
--- It forces the web page rendering, since it is monadic so it can contain
|
|
--- side effects and load effects to be tested.
|
|
-askt :: (MonadIO m, FormInput v) => (Int -> a) -> View v m a -> FlowM v m a
|
|
-askt v w = do
|
|
- FormElm forms mx <- FlowM . lift $ runView w
|
|
- n <- getTestNumber
|
|
- let l= B.length $ toByteString forms
|
|
- breturn $ l `seq` mx `seq` v n
|
|
-
|
|
---mvtestopts :: MVar (M.Map String (Int,Dynamic))
|
|
---mvtestopts = unsafePerformIO $ newMVar M.empty
|
|
-
|
|
---asktn :: (Typeable a,MonadIO m) => [a] -> View v m a -> FlowM v m a
|
|
---asktn xs w= do
|
|
--- v <- liftIO $ do
|
|
--- let k = addrStr xs
|
|
--- opts <- takeMVar mvtestopts
|
|
--- let r = M.lookup k opts
|
|
--- case r of
|
|
--- Nothing -> do
|
|
--- putMVar mvtestopts $ M.singleton k (0,toDyn xs)
|
|
--- return $ head xs
|
|
--- Just (i,d) -> do
|
|
--- putMVar mvtestopts $ M.insert k (i+1,d) opts
|
|
--- return $ (fromMaybe (error err1) $ fromDynamic d) !! i
|
|
---
|
|
--- askt v w
|
|
---
|
|
--- where
|
|
--- err1= "MFlow.Forms.Test: asktn: fromDynamic error"
|
|
-
|
|
-
|
|
--- | verify a property. if not true, throw the error message.
|
|
---
|
|
--- It is intended to be used in a infix notation, on the right of the code,
|
|
--- in order to separate the code assertions from the application code and make clearly
|
|
--- visible them as a form of documentation.
|
|
--- separated from it:
|
|
---
|
|
--- > liftIO $ print (x :: Int) `verify` (return $ x > 10, "x < = 10")
|
|
---
|
|
--- the expression is monadic to allow for complex verifications that may involve IO actions
|
|
-verifyM :: Monad m => m b -> (m Bool, String) -> m b
|
|
-verifyM f (mprop, msg)= do
|
|
- prop <- mprop
|
|
- case prop of
|
|
- True -> f
|
|
- False -> error msg
|
|
-
|
|
--- | a pure version of verifyM
|
|
-verify :: a -> (Bool, String) -> a
|
|
-verify f (prop, msg)= do
|
|
- case prop of
|
|
- True -> f
|
|
- False -> error msg
|
|
-
|
|
-
|
|
---
|
|
---match form=do
|
|
--- marches <- readIORef matches
|
|
--- return $ head map (m s) matches
|
|
--- where
|
|
--- m s (ms,ps) = case and $ map (flip isInfixOf $ s) ms of
|
|
--- True -> Just ps
|
|
--- False -> Nothing
|
|
---
|
|
---composeParams (Gen ps) form= zip (getParams form) ps
|
|
--- where
|
|
--- getParams form=
|
|
--- let search name form
|
|
--- | null form = mempty
|
|
--- | isPrefix name form = drop (length name) form
|
|
--- | otherwise= search name $ tail form
|
|
---
|
|
--- par s= takeWhile(/='\"') . dropWhile (/='\"') . tail . dropWhile (/='=') $ s
|
|
--- getPar= par $ search "name"
|
|
--- in getPar form
|
|
---
|
|
-
|
|
-waction :: (Functor m, MonadIO m,Generate a, FormInput view)
|
|
- => View view m a
|
|
- -> (a -> FlowM view m b)
|
|
- -> View view m b
|
|
-waction w f= do
|
|
- x <- liftIO generate
|
|
- MFlow.Forms.waction (return x) f
|
|
-
|
|
-userWidget :: ( MonadIO m, Functor m
|
|
- , FormInput view)
|
|
- => Maybe String
|
|
- -> View view m (Maybe (String,String), Maybe String)
|
|
- -> View view m String
|
|
-userWidget muser formuser= do
|
|
- user <- getCurrentUser
|
|
- if muser== Just user then return user
|
|
- else if isJust muser then do
|
|
- let user= fromJust muser
|
|
- login user >> return user
|
|
- else liftIO generate >>= \u -> login u >> return u
|
|
-
|
|
- where
|
|
- login uname= do
|
|
- st <- get
|
|
- let t = mfToken st
|
|
- t'= t{tuser= uname}
|
|
- put st{mfToken= t'}
|
|
- return ()
|
|
-
|
|
-getUserSimple :: ( MonadIO m, FormInput view, Typeable view
|
|
- , Functor m)
|
|
- => FlowM view m String
|
|
-getUserSimple= getUser Nothing userFormLine
|
|
-
|
|
-
|
|
-getUser :: ( FormInput view, Typeable view
|
|
- , Functor m,MonadIO m)
|
|
- => Maybe String
|
|
- -> View view m (Maybe (String,String), Maybe String)
|
|
- -> FlowM view m String
|
|
-getUser mu form= ask $ userWidget mu form
|
|
-
|
|
---wmodify
|
|
--- :: (Functor m, MonadIO m, FormInput v, Generate (Maybe a)) =>
|
|
--- View v m a1
|
|
--- -> ([v] -> Maybe a -> WState v m ([v], Maybe b))
|
|
--- -> View v m b
|
|
---wmodify formt act = do
|
|
--- x <- liftIO generate
|
|
--- formt `MFlow.Forms.wmodify` (\ f _-> return (f,x)) `MFlow.Forms.wmodify` act
|
|
-
|
|
-{-
|
|
-type Var= String
|
|
-data Test= Test{tflink:: [(Var,String)]
|
|
- ,selectOptions :: [(Var,[String])]
|
|
- ,tfinput :: [(Var, String)]
|
|
- ,tftextarea :: [(Var, String)]
|
|
- }
|
|
- deriving(Read,Show)
|
|
-
|
|
-type TestM = Test -> Test
|
|
-
|
|
-instance Monoid TestM where
|
|
- mempty= id
|
|
- mappend= (.)
|
|
-
|
|
-instance FormInput TestM where
|
|
- ftag = const id
|
|
- inred = const id
|
|
- fromStr = const id
|
|
- flink var _= let(n,v)=break (=='=') var in \t ->t{tflink= (n,tail v):tflink t}
|
|
- finput n _ v _ _ = \t -> t{tfinput = (n,v):tfinput t}
|
|
- ftextarea n v= \t -> t{tftextarea = (n,v):tftextarea t}
|
|
- fselect n _= \t -> t{selectOptions=(n,[]):selectOptions t}
|
|
- foption o _ _= \t ->
|
|
- let (n,opts)= head $ selectOptions t
|
|
- in t{selectOptions=(n,o:opts):tail (selectOptions t)}
|
|
- formAction _ _= id
|
|
- addAttributes _ _= id
|
|
-
|
|
-generateGenerate Test{..}= do
|
|
- b <- generate
|
|
- case b of
|
|
- True -> genLink
|
|
- False -> genForm
|
|
-
|
|
- where
|
|
- genForm= do
|
|
- -- one on every generate is incomplete
|
|
- n <- randomRIO(0,10) :: IO Int
|
|
- case n of
|
|
- 0 -> do
|
|
- genInput
|
|
-
|
|
- _ -> do
|
|
- r1 <- genInput
|
|
- r2 <- genSelect
|
|
- r3 <- genTextArea
|
|
- return $ r1++r2++r3
|
|
- genLink= do
|
|
- let n = length tflink
|
|
- if n == 0 then genForm
|
|
- else do
|
|
- r <- randomRIO(0,n )
|
|
- return [tflink !! r]
|
|
-
|
|
- genSelect=do
|
|
- let n = length selectOptions
|
|
- if n== 0
|
|
- then return []
|
|
- else mapM gen selectOptions
|
|
- where
|
|
- gen(s,os)= do
|
|
- let m = length os
|
|
- j <- randomRIO(0,m)
|
|
- return (s, os !! j)
|
|
-
|
|
- genInput= do
|
|
- let n = length tftextarea
|
|
- if n==0
|
|
- then return []
|
|
- else mapM gen tfinput
|
|
- where gen(n,_)= do
|
|
- str <- generate
|
|
- return $ (n,str)
|
|
-
|
|
- genTextArea= do
|
|
- let n = length tfinput
|
|
- if n==0
|
|
- then return []
|
|
- else mapM gen tftextarea
|
|
- where
|
|
- gen(n,_)= do
|
|
- str <- generate
|
|
- return $ (n,str)
|
|
-
|
|
-pwf= "pwf"
|
|
-ind= "ind"
|
|
-instance Processable Params where
|
|
- pwfname = fromMaybe noScript . lookup pwf
|
|
- puser= fromMaybe anonymous . lookup cookieuser
|
|
- pind = fromMaybe "0" . lookup ind
|
|
- getParams = id
|
|
-
|
|
-
|
|
-
|
|
-runTest nusers = do
|
|
- wfs <- getMessageFlows
|
|
- replicateM nusers $ gen wfs
|
|
- where
|
|
- gen wfs = do
|
|
- u <- generate
|
|
- mapM (genTraffic u) $ M.toList wfs
|
|
-
|
|
- genTraffic u (n,_)= forkIO $ iterategenerates [(pwf,n),(cookieuser,u)] []
|
|
-
|
|
- iterategenerates ident msg= iterate [] msg
|
|
- where
|
|
- iterate cs msg= do
|
|
- (HttpData ps cooks test,_) <- msgScheduler $ ident ++ cs++ msg
|
|
- let cs'= cs++ map (\(a,b,c,d)-> (a,b)) cooks
|
|
- resp <- generateGenerate . read $ B.unpack test
|
|
- iterate cs' resp
|
|
-
|
|
- -}
|
|
+
|
|
+ let t = Token x y z [] [] undefined undefined undefined
|
|
+ WF.start name f t
|
|
+
|
|
+testNumber= unsafePerformIO $ newIORef 0
|
|
+
|
|
+getTestNumber :: MonadIO m => m Int
|
|
+getTestNumber= liftIO $ readIORef testNumber
|
|
+
|
|
+-- | inject substitutes an expression by other. It may be used to override
|
|
+-- ask interaction with the user. It should bee used infix for greater readability:
|
|
+--
|
|
+-- > ask something `inject` const someother
|
|
+--
|
|
+-- The parameter passed is the test number
|
|
+-- if the flow has not been executed by runTest, inject return the original
|
|
+inject :: MonadIO m => m b -> (Int -> b) -> m b
|
|
+inject exp v= do
|
|
+ n <- getTestNumber
|
|
+ if n== 0 then exp else exp `seq` return $ v n
|
|
+
|
|
+-- | a simulated ask that generate simulated user input of the type expected.
|
|
+--
|
|
+-- It forces the web page rendering, since it is monadic and can contain
|
|
+-- side effects and load effects to be tested.
|
|
+--
|
|
+-- it is a substitute of 'ask' from "MFlow.Forms" for testing purposes.
|
|
+
|
|
+-- execute 'runText' to initiate threads under different load conditions.
|
|
+ask :: (Generate a, MonadIO m, Functor m, FormInput v,Typeable v) => View v m a -> FlowM v m a
|
|
+ask w = do
|
|
+ FormElm forms mx <- FlowM . lift $ runView w
|
|
+ r <- liftIO generate
|
|
+ let n= B.length $ toByteString forms
|
|
+ breturn $ n `seq` mx `seq` r
|
|
+-- let u= undefined
|
|
+-- liftIO $ runStateT (runView mf) s
|
|
+-- bool <- liftIO generate
|
|
+-- case bool of
|
|
+-- False -> fail ""
|
|
+-- True -> do
|
|
+-- b <- liftIO generate
|
|
+-- r <- liftIO generate
|
|
+-- case (b,r) of
|
|
+-- (True,x) -> breturn x
|
|
+-- _ -> ask w
|
|
+
|
|
+
|
|
+-- | instead of generating a result like `ask`, the result is given as the first parameter
|
|
+-- so it does not need a Generate instance.
|
|
+--
|
|
+-- It forces the web page rendering, since it is monadic so it can contain
|
|
+-- side effects and load effects to be tested.
|
|
+askt :: (MonadIO m, FormInput v) => (Int -> a) -> View v m a -> FlowM v m a
|
|
+askt v w = do
|
|
+ FormElm forms mx <- FlowM . lift $ runView w
|
|
+ n <- getTestNumber
|
|
+ let l= B.length $ toByteString forms
|
|
+ breturn $ l `seq` mx `seq` v n
|
|
+
|
|
+--mvtestopts :: MVar (M.Map String (Int,Dynamic))
|
|
+--mvtestopts = unsafePerformIO $ newMVar M.empty
|
|
+
|
|
+--asktn :: (Typeable a,MonadIO m) => [a] -> View v m a -> FlowM v m a
|
|
+--asktn xs w= do
|
|
+-- v <- liftIO $ do
|
|
+-- let k = addrStr xs
|
|
+-- opts <- takeMVar mvtestopts
|
|
+-- let r = M.lookup k opts
|
|
+-- case r of
|
|
+-- Nothing -> do
|
|
+-- putMVar mvtestopts $ M.singleton k (0,toDyn xs)
|
|
+-- return $ head xs
|
|
+-- Just (i,d) -> do
|
|
+-- putMVar mvtestopts $ M.insert k (i+1,d) opts
|
|
+-- return $ (fromMaybe (error err1) $ fromDynamic d) !! i
|
|
+--
|
|
+-- askt v w
|
|
+--
|
|
+-- where
|
|
+-- err1= "MFlow.Forms.Test: asktn: fromDynamic error"
|
|
+
|
|
+
|
|
+-- | verify a property. if not true, throw the error message.
|
|
+--
|
|
+-- It is intended to be used in a infix notation, on the right of the code,
|
|
+-- in order to separate the code assertions from the application code and make clearly
|
|
+-- visible them as a form of documentation.
|
|
+-- separated from it:
|
|
+--
|
|
+-- > liftIO $ print (x :: Int) `verify` (return $ x > 10, "x < = 10")
|
|
+--
|
|
+-- the expression is monadic to allow for complex verifications that may involve IO actions
|
|
+verifyM :: Monad m => m b -> (m Bool, String) -> m b
|
|
+verifyM f (mprop, msg)= do
|
|
+ prop <- mprop
|
|
+ case prop of
|
|
+ True -> f
|
|
+ False -> error msg
|
|
+
|
|
+-- | a pure version of verifyM
|
|
+verify :: a -> (Bool, String) -> a
|
|
+verify f (prop, msg)= do
|
|
+ case prop of
|
|
+ True -> f
|
|
+ False -> error msg
|
|
+
|
|
+
|
|
+--
|
|
+--match form=do
|
|
+-- marches <- readIORef matches
|
|
+-- return $ head map (m s) matches
|
|
+-- where
|
|
+-- m s (ms,ps) = case and $ map (flip isInfixOf $ s) ms of
|
|
+-- True -> Just ps
|
|
+-- False -> Nothing
|
|
+--
|
|
+--composeParams (Gen ps) form= zip (getParams form) ps
|
|
+-- where
|
|
+-- getParams form=
|
|
+-- let search name form
|
|
+-- | null form = mempty
|
|
+-- | isPrefix name form = drop (length name) form
|
|
+-- | otherwise= search name $ tail form
|
|
+--
|
|
+-- par s= takeWhile(/='\"') . dropWhile (/='\"') . tail . dropWhile (/='=') $ s
|
|
+-- getPar= par $ search "name"
|
|
+-- in getPar form
|
|
+--
|
|
+
|
|
+waction :: (Functor m, MonadIO m,Generate a, FormInput view)
|
|
+ => View view m a
|
|
+ -> (a -> FlowM view m b)
|
|
+ -> View view m b
|
|
+waction w f= do
|
|
+ x <- liftIO generate
|
|
+ MFlow.Forms.waction (return x) f
|
|
+
|
|
+userWidget :: ( MonadIO m, Functor m
|
|
+ , FormInput view)
|
|
+ => Maybe String
|
|
+ -> View view m (Maybe (String,String), Maybe String)
|
|
+ -> View view m String
|
|
+userWidget muser formuser= do
|
|
+ user <- getCurrentUser
|
|
+ if muser== Just user then return user
|
|
+ else if isJust muser then do
|
|
+ let user= fromJust muser
|
|
+ login user >> return user
|
|
+ else liftIO generate >>= \u -> login u >> return u
|
|
+
|
|
+ where
|
|
+ login uname= do
|
|
+ st <- get
|
|
+ let t = mfToken st
|
|
+ t'= t{tuser= uname}
|
|
+ put st{mfToken= t'}
|
|
+ return ()
|
|
+
|
|
+getUserSimple :: ( MonadIO m, FormInput view, Typeable view
|
|
+ , Functor m)
|
|
+ => FlowM view m String
|
|
+getUserSimple= getUser Nothing userFormLine
|
|
+
|
|
+
|
|
+getUser :: ( FormInput view, Typeable view
|
|
+ , Functor m,MonadIO m)
|
|
+ => Maybe String
|
|
+ -> View view m (Maybe (String,String), Maybe String)
|
|
+ -> FlowM view m String
|
|
+getUser mu form= ask $ userWidget mu form
|
|
+
|
|
+--wmodify
|
|
+-- :: (Functor m, MonadIO m, FormInput v, Generate (Maybe a)) =>
|
|
+-- View v m a1
|
|
+-- -> ([v] -> Maybe a -> WState v m ([v], Maybe b))
|
|
+-- -> View v m b
|
|
+--wmodify formt act = do
|
|
+-- x <- liftIO generate
|
|
+-- formt `MFlow.Forms.wmodify` (\ f _-> return (f,x)) `MFlow.Forms.wmodify` act
|
|
+
|
|
+{-
|
|
+type Var= String
|
|
+data Test= Test{tflink:: [(Var,String)]
|
|
+ ,selectOptions :: [(Var,[String])]
|
|
+ ,tfinput :: [(Var, String)]
|
|
+ ,tftextarea :: [(Var, String)]
|
|
+ }
|
|
+ deriving(Read,Show)
|
|
+
|
|
+type TestM = Test -> Test
|
|
+
|
|
+instance Monoid TestM where
|
|
+ mempty= id
|
|
+ mappend= (.)
|
|
+
|
|
+instance FormInput TestM where
|
|
+ ftag = const id
|
|
+ inred = const id
|
|
+ fromStr = const id
|
|
+ flink var _= let(n,v)=break (=='=') var in \t ->t{tflink= (n,tail v):tflink t}
|
|
+ finput n _ v _ _ = \t -> t{tfinput = (n,v):tfinput t}
|
|
+ ftextarea n v= \t -> t{tftextarea = (n,v):tftextarea t}
|
|
+ fselect n _= \t -> t{selectOptions=(n,[]):selectOptions t}
|
|
+ foption o _ _= \t ->
|
|
+ let (n,opts)= head $ selectOptions t
|
|
+ in t{selectOptions=(n,o:opts):tail (selectOptions t)}
|
|
+ formAction _ _= id
|
|
+ addAttributes _ _= id
|
|
+
|
|
+generateGenerate Test{..}= do
|
|
+ b <- generate
|
|
+ case b of
|
|
+ True -> genLink
|
|
+ False -> genForm
|
|
+
|
|
+ where
|
|
+ genForm= do
|
|
+ -- one on every generate is incomplete
|
|
+ n <- randomRIO(0,10) :: IO Int
|
|
+ case n of
|
|
+ 0 -> do
|
|
+ genInput
|
|
+
|
|
+ _ -> do
|
|
+ r1 <- genInput
|
|
+ r2 <- genSelect
|
|
+ r3 <- genTextArea
|
|
+ return $ r1++r2++r3
|
|
+ genLink= do
|
|
+ let n = length tflink
|
|
+ if n == 0 then genForm
|
|
+ else do
|
|
+ r <- randomRIO(0,n )
|
|
+ return [tflink !! r]
|
|
+
|
|
+ genSelect=do
|
|
+ let n = length selectOptions
|
|
+ if n== 0
|
|
+ then return []
|
|
+ else mapM gen selectOptions
|
|
+ where
|
|
+ gen(s,os)= do
|
|
+ let m = length os
|
|
+ j <- randomRIO(0,m)
|
|
+ return (s, os !! j)
|
|
+
|
|
+ genInput= do
|
|
+ let n = length tftextarea
|
|
+ if n==0
|
|
+ then return []
|
|
+ else mapM gen tfinput
|
|
+ where gen(n,_)= do
|
|
+ str <- generate
|
|
+ return $ (n,str)
|
|
+
|
|
+ genTextArea= do
|
|
+ let n = length tfinput
|
|
+ if n==0
|
|
+ then return []
|
|
+ else mapM gen tftextarea
|
|
+ where
|
|
+ gen(n,_)= do
|
|
+ str <- generate
|
|
+ return $ (n,str)
|
|
+
|
|
+pwf= "pwf"
|
|
+ind= "ind"
|
|
+instance Processable Params where
|
|
+ pwfname = fromMaybe noScript . lookup pwf
|
|
+ puser= fromMaybe anonymous . lookup cookieuser
|
|
+ pind = fromMaybe "0" . lookup ind
|
|
+ getParams = id
|
|
+
|
|
+
|
|
+
|
|
+runTest nusers = do
|
|
+ wfs <- getMessageFlows
|
|
+ replicateM nusers $ gen wfs
|
|
+ where
|
|
+ gen wfs = do
|
|
+ u <- generate
|
|
+ mapM (genTraffic u) $ M.toList wfs
|
|
+
|
|
+ genTraffic u (n,_)= forkIO $ iterategenerates [(pwf,n),(cookieuser,u)] []
|
|
+
|
|
+ iterategenerates ident msg= iterate [] msg
|
|
+ where
|
|
+ iterate cs msg= do
|
|
+ (HttpData ps cooks test,_) <- msgScheduler $ ident ++ cs++ msg
|
|
+ let cs'= cs++ map (\(a,b,c,d)-> (a,b)) cooks
|
|
+ resp <- generateGenerate . read $ B.unpack test
|
|
+ iterate cs' resp
|
|
+
|
|
+ -}
|
|
diff -ru orig/src/MFlow/Forms/Widgets.hs new/src/MFlow/Forms/Widgets.hs
|
|
--- orig/src/MFlow/Forms/Widgets.hs 2014-06-10 05:51:26.977015856 +0300
|
|
+++ new/src/MFlow/Forms/Widgets.hs 2014-06-10 05:51:25.000000000 +0300
|
|
@@ -1,114 +1,114 @@
|
|
-
|
|
-{- |
|
|
-Some dynamic widgets, widgets that dynamically edit content in other widgets,
|
|
-widgets for templating, content management and multilanguage. And some primitives
|
|
-to create other active widgets.
|
|
--}
|
|
+
|
|
+{- |
|
|
+Some dynamic widgets, widgets that dynamically edit content in other widgets,
|
|
+widgets for templating, content management and multilanguage. And some primitives
|
|
+to create other active widgets.
|
|
+-}
|
|
-- {-# OPTIONS -F -pgmF cpphs #-}
|
|
-{-# OPTIONS -cpp -pgmPcpphs -optP--cpp #-}
|
|
-{-# LANGUAGE UndecidableInstances,ExistentialQuantification
|
|
- , FlexibleInstances, OverlappingInstances, FlexibleContexts
|
|
+{-# OPTIONS -cpp -pgmPcpphs -optP--cpp #-}
|
|
+{-# LANGUAGE UndecidableInstances,ExistentialQuantification
|
|
+ , FlexibleInstances, OverlappingInstances, FlexibleContexts
|
|
, OverloadedStrings, DeriveDataTypeable , ScopedTypeVariables
|
|
- , StandaloneDeriving #-}
|
|
-
|
|
-
|
|
-
|
|
-
|
|
-module MFlow.Forms.Widgets (
|
|
--- * Ajax refreshing of widgets
|
|
-autoRefresh, noAutoRefresh, appendUpdate, prependUpdate, push, UpdateMethod(..), lazy
|
|
-
|
|
--- * JQueryUi widgets
|
|
-,datePicker, getSpinner, wautocomplete, wdialog,
|
|
-
|
|
--- * User Management
|
|
-userFormOrName,maybeLogout, wlogin,
|
|
-
|
|
--- * Active widgets
|
|
-wEditList,wautocompleteList
|
|
-, wautocompleteEdit,
|
|
-
|
|
--- * Editing widgets
|
|
-delEdited, getEdited, setEdited, prependWidget,appendWidget,setWidget
|
|
-
|
|
--- * Content Management
|
|
-,tField, tFieldEd, htmlEdit, edTemplate, dField, template, witerate,tfieldKey
|
|
-
|
|
--- * Multilanguage
|
|
-,mFieldEd, mField
|
|
-
|
|
--- * utility
|
|
-,insertForm, readtField, writetField
|
|
-
|
|
-
|
|
-) where
|
|
-import MFlow
|
|
-import MFlow.Forms
|
|
-import MFlow.Forms.Internals
|
|
-import Data.Monoid
|
|
+ , StandaloneDeriving #-}
|
|
+
|
|
+
|
|
+
|
|
+
|
|
+module MFlow.Forms.Widgets (
|
|
+-- * Ajax refreshing of widgets
|
|
+autoRefresh, noAutoRefresh, appendUpdate, prependUpdate, push, UpdateMethod(..), lazy
|
|
+
|
|
+-- * JQueryUi widgets
|
|
+,datePicker, getSpinner, wautocomplete, wdialog,
|
|
+
|
|
+-- * User Management
|
|
+userFormOrName,maybeLogout, wlogin,
|
|
+
|
|
+-- * Active widgets
|
|
+wEditList,wautocompleteList
|
|
+, wautocompleteEdit,
|
|
+
|
|
+-- * Editing widgets
|
|
+delEdited, getEdited, setEdited, prependWidget,appendWidget,setWidget
|
|
+
|
|
+-- * Content Management
|
|
+,tField, tFieldEd, htmlEdit, edTemplate, dField, template, witerate,tfieldKey
|
|
+
|
|
+-- * Multilanguage
|
|
+,mFieldEd, mField
|
|
+
|
|
+-- * utility
|
|
+,insertForm, readtField, writetField
|
|
+
|
|
+
|
|
+) where
|
|
+import MFlow
|
|
+import MFlow.Forms
|
|
+import MFlow.Forms.Internals
|
|
+import Data.Monoid
|
|
import Data.ByteString.Lazy.UTF8 as B hiding (length,span)
|
|
-import Data.ByteString.Lazy.Char8 (unpack)
|
|
-import Control.Monad.Trans
|
|
-import Data.Typeable
|
|
-import Data.List
|
|
-import System.IO.Unsafe
|
|
-
|
|
-import Control.Monad.State
|
|
-import Data.TCache
|
|
-import Data.TCache.Defs
|
|
-import Data.TCache.Memoization
|
|
-import Data.RefSerialize hiding ((<|>))
|
|
-import qualified Data.Map as M
|
|
-import Data.IORef
|
|
-import MFlow.Cookies
|
|
-import Data.Maybe
|
|
-import Data.Char
|
|
-import Control.Monad.Identity
|
|
-import Control.Workflow(killWF)
|
|
-import Unsafe.Coerce
|
|
-import Control.Exception
|
|
+import Data.ByteString.Lazy.Char8 (unpack)
|
|
+import Control.Monad.Trans
|
|
+import Data.Typeable
|
|
+import Data.List
|
|
+import System.IO.Unsafe
|
|
+
|
|
+import Control.Monad.State
|
|
+import Data.TCache
|
|
+import Data.TCache.Defs
|
|
+import Data.TCache.Memoization
|
|
+import Data.RefSerialize hiding ((<|>))
|
|
+import qualified Data.Map as M
|
|
+import Data.IORef
|
|
+import MFlow.Cookies
|
|
+import Data.Maybe
|
|
+import Data.Char
|
|
+import Control.Monad.Identity
|
|
+import Control.Workflow(killWF)
|
|
+import Unsafe.Coerce
|
|
+import Control.Exception
|
|
import MFlow.Forms.Cache
|
|
-
|
|
-
|
|
-
|
|
---jqueryScript= "//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"
|
|
---jqueryScript1="//code.jquery.com/jquery-1.9.1.js"
|
|
---
|
|
---jqueryCSS1= "//code.jquery.com/ui/1.9.1/themes/base/jquery-ui.css"
|
|
---jqueryCSS= "//code.jquery.com/ui/1.10.3/themes/smoothness/jquery-ui.css"
|
|
---
|
|
---jqueryUI1= "//code.jquery.com/ui/1.9.1/jquery-ui.js"
|
|
---jqueryUI= "//code.jquery.com/ui/1.10.3/jquery-ui.js"
|
|
-
|
|
-jqueryScript= getConfig "cjqueryScript" "//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"
|
|
-jqueryCSS= getConfig "cjqueryCSS" "//code.jquery.com/ui/1.10.3/themes/smoothness/jquery-ui.css"
|
|
-jqueryUI= getConfig "cjqueryUI" "//code.jquery.com/ui/1.10.3/jquery-ui.js"
|
|
-nicEditUrl= getConfig "cnicEditUrl" "//js.nicedit.com/nicEdit-latest.js"
|
|
-------- User Management ------
|
|
-
|
|
--- | Present a user form if not logged in. Otherwise, the user name and a logout link is presented.
|
|
--- The paremeters and the behaviour are the same as 'userWidget'.
|
|
--- Only the display is different
|
|
-userFormOrName mode wid= userWidget mode wid `wmodify` f <** maybeLogout
|
|
- where
|
|
- f _ justu@(Just u) = return ([fromStr u], justu) -- !> "input"
|
|
- f felem Nothing = do
|
|
- us <- getCurrentUser -- getEnv cookieuser
|
|
- if us == anonymous
|
|
- then return (felem, Nothing)
|
|
- else return([fromStr us], Just us)
|
|
-
|
|
--- | Display a logout link if the user is logged. Nothing otherwise
|
|
-maybeLogout :: (MonadIO m,Functor m,FormInput v) => View v m ()
|
|
-maybeLogout= do
|
|
- us <- getCurrentUser
|
|
- if us/= anonymous
|
|
- then do
|
|
- cmd <- ajax $ const $ return "window.location=='/'" --refresh
|
|
- fromStr " " ++> ((wlink () (fromStr "logout")) <![("onclick",cmd "''")]) `waction` const logout
|
|
+
|
|
+
|
|
+
|
|
+--jqueryScript= "//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"
|
|
+--jqueryScript1="//code.jquery.com/jquery-1.9.1.js"
|
|
+--
|
|
+--jqueryCSS1= "//code.jquery.com/ui/1.9.1/themes/base/jquery-ui.css"
|
|
+--jqueryCSS= "//code.jquery.com/ui/1.10.3/themes/smoothness/jquery-ui.css"
|
|
+--
|
|
+--jqueryUI1= "//code.jquery.com/ui/1.9.1/jquery-ui.js"
|
|
+--jqueryUI= "//code.jquery.com/ui/1.10.3/jquery-ui.js"
|
|
+
|
|
+jqueryScript= getConfig "cjqueryScript" "//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"
|
|
+jqueryCSS= getConfig "cjqueryCSS" "//code.jquery.com/ui/1.10.3/themes/smoothness/jquery-ui.css"
|
|
+jqueryUI= getConfig "cjqueryUI" "//code.jquery.com/ui/1.10.3/jquery-ui.js"
|
|
+nicEditUrl= getConfig "cnicEditUrl" "//js.nicedit.com/nicEdit-latest.js"
|
|
+------- User Management ------
|
|
+
|
|
+-- | Present a user form if not logged in. Otherwise, the user name and a logout link is presented.
|
|
+-- The paremeters and the behaviour are the same as 'userWidget'.
|
|
+-- Only the display is different
|
|
+userFormOrName mode wid= userWidget mode wid `wmodify` f <** maybeLogout
|
|
+ where
|
|
+ f _ justu@(Just u) = return ([fromStr u], justu) -- !> "input"
|
|
+ f felem Nothing = do
|
|
+ us <- getCurrentUser -- getEnv cookieuser
|
|
+ if us == anonymous
|
|
+ then return (felem, Nothing)
|
|
+ else return([fromStr us], Just us)
|
|
+
|
|
+-- | Display a logout link if the user is logged. Nothing otherwise
|
|
+maybeLogout :: (MonadIO m,Functor m,FormInput v) => View v m ()
|
|
+maybeLogout= do
|
|
+ us <- getCurrentUser
|
|
+ if us/= anonymous
|
|
+ then do
|
|
+ cmd <- ajax $ const $ return "window.location=='/'" --refresh
|
|
+ fromStr " " ++> ((wlink () (fromStr "logout")) <![("onclick",cmd "''")]) `waction` const logout
|
|
else noWidget
|
|
|
|
-
|
|
+
|
|
data Medit view m a = Medit (M.Map B.ByteString [(String,View view m a)])
|
|
|
|
#if defined(__GLASGOW_HASKELL__) && (__GLASGOW_HASKELL__ >= 707)
|
|
@@ -129,46 +129,46 @@
|
|
ta :: Medit v m a -> a
|
|
ta= undefined
|
|
|
|
-#endif
|
|
-
|
|
--- | If not logged, it present a page flow which askm for the user name, then the password if not logged
|
|
---
|
|
--- If logged, it present the user name and a link to logout
|
|
---
|
|
--- normally to be used with autoRefresh and pageFlow when used with other widgets.
|
|
-wlogin :: (MonadIO m,Functor m,FormInput v) => View v m ()
|
|
-wlogin= do
|
|
- username <- getCurrentUser
|
|
- if username /= anonymous
|
|
+#endif
|
|
+
|
|
+-- | If not logged, it present a page flow which askm for the user name, then the password if not logged
|
|
+--
|
|
+-- If logged, it present the user name and a link to logout
|
|
+--
|
|
+-- normally to be used with autoRefresh and pageFlow when used with other widgets.
|
|
+wlogin :: (MonadIO m,Functor m,FormInput v) => View v m ()
|
|
+wlogin= wform $ do
|
|
+ username <- getCurrentUser
|
|
+ if username /= anonymous
|
|
then do
|
|
private; noCache;noStore
|
|
- return username
|
|
- else do
|
|
+ return username
|
|
+ else do
|
|
name <- getString Nothing <! hint "login name"
|
|
<! size (9 :: Int)
|
|
- <++ ftag "br" mempty
|
|
+ <++ ftag "br" mempty
|
|
pass <- getPassword <! hint "password"
|
|
- <! size 9
|
|
- <++ ftag "br" mempty
|
|
- <** submitButton "login"
|
|
- val <- userValidate (name,pass)
|
|
- case val of
|
|
- Just msg -> notValid msg
|
|
- Nothing -> login name >> (return name)
|
|
-
|
|
- `wcallback` (\name -> ftag "b" (fromStr $ "logged as " ++ name++ " ")
|
|
- ++> pageFlow "logout" (submitButton "logout")) -- wlink ("logout" :: String) (ftag "b" $ fromStr " logout"))
|
|
- `wcallback` const (logout >> wlogin)
|
|
-
|
|
-focus = [("onload","this.focus()")]
|
|
-hint s= [("placeholder",s)]
|
|
-size n= [("size",show n)]
|
|
-
|
|
-getEdited1 id= do
|
|
- Medit stored <- getSessionData `onNothing` return (Medit M.empty)
|
|
- return $ fromMaybe [] $ M.lookup id stored
|
|
-
|
|
--- | Return the list of edited widgets (added by the active widgets) for a given identifier
|
|
+ <! size 9
|
|
+ <++ ftag "br" mempty
|
|
+ <** submitButton "login"
|
|
+ val <- userValidate (name,pass)
|
|
+ case val of
|
|
+ Just msg -> notValid msg
|
|
+ Nothing -> login name >> (return name)
|
|
+
|
|
+ `wcallback` (\name -> ftag "b" (fromStr $ "logged as " ++ name++ " ")
|
|
+ ++> pageFlow "logout" (submitButton "logout")) -- wlink ("logout" :: String) (ftag "b" $ fromStr " logout"))
|
|
+ `wcallback` const (logout >> wlogin)
|
|
+
|
|
+focus = [("onload","this.focus()")]
|
|
+hint s= [("placeholder",s)]
|
|
+size n= [("size",show n)]
|
|
+
|
|
+getEdited1 id= do
|
|
+ Medit stored <- getSessionData `onNothing` return (Medit M.empty)
|
|
+ return $ fromMaybe [] $ M.lookup id stored
|
|
+
|
|
+-- | Return the list of edited widgets (added by the active widgets) for a given identifier
|
|
getEdited
|
|
|
|
#if defined(__GLASGOW_HASKELL__) && (__GLASGOW_HASKELL__ >= 707)
|
|
@@ -181,15 +181,15 @@
|
|
|
|
#endif
|
|
|
|
- B.ByteString -> m [View v m1 a]
|
|
+ B.ByteString -> m [View v m1 a]
|
|
+
|
|
+getEdited id= do
|
|
+ r <- getEdited1 id
|
|
+ let (_,ws)= unzip r
|
|
+ return ws
|
|
|
|
-getEdited id= do
|
|
- r <- getEdited1 id
|
|
- let (_,ws)= unzip r
|
|
- return ws
|
|
-
|
|
--- | Deletes the list of edited widgets for a certain identifier and with the type of the witness widget parameter
|
|
-delEdited
|
|
+-- | Deletes the list of edited widgets for a certain identifier and with the type of the witness widget parameter
|
|
+delEdited
|
|
#if defined(__GLASGOW_HASKELL__) && (__GLASGOW_HASKELL__ >= 707)
|
|
:: (Typeable v, Typeable a, MonadIO m, Typeable m1,
|
|
#else
|
|
@@ -197,312 +197,312 @@
|
|
#endif
|
|
MonadState (MFlowState view) m)
|
|
=> B.ByteString -- ^ identifier
|
|
- -> [View v m1 a] -> m () -- ^ withess
|
|
-delEdited id witness=do
|
|
- Medit stored <- getSessionData `onNothing` return (Medit (M.empty))
|
|
- let (ks, ws)= unzip $ fromMaybe [] $ M.lookup id stored
|
|
-
|
|
- return $ ws `asTypeOf` witness
|
|
- liftIO $ mapM flushCached ks
|
|
- let stored'= M.delete id stored
|
|
- setSessionData . Medit $ stored'
|
|
-
|
|
-
|
|
-
|
|
--- setEdited id ([] `asTypeOf` (zip (repeat "") witness))
|
|
-
|
|
-setEdited id ws= do
|
|
- Medit stored <- getSessionData `onNothing` return (Medit (M.empty))
|
|
- let stored'= M.insert id ws stored
|
|
- setSessionData . Medit $ stored'
|
|
-
|
|
-
|
|
-addEdited id w= do
|
|
- ws <- getEdited1 id
|
|
- setEdited id (w:ws)
|
|
-
|
|
+ -> [View v m1 a] -> m () -- ^ withess
|
|
+delEdited id witness=do
|
|
+ Medit stored <- getSessionData `onNothing` return (Medit (M.empty))
|
|
+ let (ks, ws)= unzip $ fromMaybe [] $ M.lookup id stored
|
|
+
|
|
+ return $ ws `asTypeOf` witness
|
|
+ liftIO $ mapM flushCached ks
|
|
+ let stored'= M.delete id stored
|
|
+ setSessionData . Medit $ stored'
|
|
+
|
|
+
|
|
+
|
|
+-- setEdited id ([] `asTypeOf` (zip (repeat "") witness))
|
|
+
|
|
+setEdited id ws= do
|
|
+ Medit stored <- getSessionData `onNothing` return (Medit (M.empty))
|
|
+ let stored'= M.insert id ws stored
|
|
+ setSessionData . Medit $ stored'
|
|
+
|
|
+
|
|
+addEdited id w= do
|
|
+ ws <- getEdited1 id
|
|
+ setEdited id (w:ws)
|
|
+
|
|
#if defined(__GLASGOW_HASKELL__) && (__GLASGOW_HASKELL__ >= 707)
|
|
modifyWidget :: (MonadIO m,Executable m,Typeable a,FormInput v, Typeable Identity, Typeable m)
|
|
-#else
|
|
-modifyWidget :: (MonadIO m,Executable m,Typeable a,FormInput v)
|
|
+#else
|
|
+modifyWidget :: (MonadIO m,Executable m,Typeable a,FormInput v)
|
|
#endif
|
|
- => B.ByteString -> B.ByteString -> View v Identity a -> View v m B.ByteString
|
|
-modifyWidget selector modifier w = View $ do
|
|
- ws <- getEdited selector
|
|
- let n = length (ws `asTypeOf` [w])
|
|
- let key= "widget"++ show selector ++ show n ++ show (typeOf $ typ w)
|
|
- let cw = wcached key 0 w
|
|
- addEdited selector (key,cw)
|
|
- FormElm form _ <- runView cw
|
|
- let elem= toByteString form
|
|
- return . FormElm mempty . Just $ selector <> "." <> modifier <>"('" <> elem <> "');"
|
|
- where
|
|
- typ :: View v Identity a -> a
|
|
- typ = undefined
|
|
-
|
|
--- | Return the javascript to be executed on the browser to prepend a widget to the location
|
|
--- identified by the selector (the bytestring parameter), The selector must have the form of a jquery expression
|
|
--- . It stores the added widgets in the edited list, that is accessed with 'getEdited'
|
|
---
|
|
--- The resulting string can be executed in the browser. 'ajax' will return the code to
|
|
--- execute the complete ajax roundtrip. This code returned by ajax must be in an eventhabdler.
|
|
---
|
|
--- This example will insert a widget in the div when the element with identifier
|
|
--- /clickelem/ is clicked. when the form is sbmitted, the widget values are returned
|
|
--- and the list of edited widgets are deleted.
|
|
---
|
|
--- > id1<- genNewId
|
|
--- > let sel= "$('#" <> fromString id1 <> "')"
|
|
--- > callAjax <- ajax . const $ prependWidget sel wn
|
|
--- > let installevents= "$(document).ready(function(){\
|
|
--- > \$('#clickelem').click(function(){"++callAjax "''"++"});})"
|
|
--- >
|
|
--- > requires [JScriptFile jqueryScript [installevents] ]
|
|
--- > ws <- getEdited sel
|
|
--- > r <- (div <<< manyOf ws) <! [("id",id1)]
|
|
--- > delEdited sel ws'
|
|
--- > return r
|
|
-
|
|
+ => B.ByteString -> B.ByteString -> View v Identity a -> View v m B.ByteString
|
|
+modifyWidget selector modifier w = View $ do
|
|
+ ws <- getEdited selector
|
|
+ let n = length (ws `asTypeOf` [w])
|
|
+ let key= "widget"++ show selector ++ show n ++ show (typeOf $ typ w)
|
|
+ let cw = wcached key 0 w
|
|
+ addEdited selector (key,cw)
|
|
+ FormElm form _ <- runView cw
|
|
+ let elem= toByteString form
|
|
+ return . FormElm mempty . Just $ selector <> "." <> modifier <>"('" <> elem <> "');"
|
|
+ where
|
|
+ typ :: View v Identity a -> a
|
|
+ typ = undefined
|
|
+
|
|
+-- | Return the javascript to be executed on the browser to prepend a widget to the location
|
|
+-- identified by the selector (the bytestring parameter), The selector must have the form of a jquery expression
|
|
+-- . It stores the added widgets in the edited list, that is accessed with 'getEdited'
|
|
+--
|
|
+-- The resulting string can be executed in the browser. 'ajax' will return the code to
|
|
+-- execute the complete ajax roundtrip. This code returned by ajax must be in an eventhabdler.
|
|
+--
|
|
+-- This example will insert a widget in the div when the element with identifier
|
|
+-- /clickelem/ is clicked. when the form is sbmitted, the widget values are returned
|
|
+-- and the list of edited widgets are deleted.
|
|
+--
|
|
+-- > id1<- genNewId
|
|
+-- > let sel= "$('#" <> fromString id1 <> "')"
|
|
+-- > callAjax <- ajax . const $ prependWidget sel wn
|
|
+-- > let installevents= "$(document).ready(function(){\
|
|
+-- > \$('#clickelem').click(function(){"++callAjax "''"++"});})"
|
|
+-- >
|
|
+-- > requires [JScriptFile jqueryScript [installevents] ]
|
|
+-- > ws <- getEdited sel
|
|
+-- > r <- (div <<< manyOf ws) <! [("id",id1)]
|
|
+-- > delEdited sel ws'
|
|
+-- > return r
|
|
+
|
|
prependWidget
|
|
#if defined(__GLASGOW_HASKELL__) && (__GLASGOW_HASKELL__ >= 707)
|
|
:: (Typeable a, MonadIO m, Executable m, FormInput v, Typeable Identity, Typeable m)
|
|
-#else
|
|
+#else
|
|
:: (Typeable a, MonadIO m, Executable m, FormInput v)
|
|
-#endif
|
|
- => B.ByteString -- ^ jquery selector
|
|
- -> View v Identity a -- ^ widget to prepend
|
|
- -> View v m B.ByteString -- ^ string returned with the jquery string to be executed in the browser
|
|
-prependWidget sel w= modifyWidget sel "prepend" w
|
|
-
|
|
--- | Like 'prependWidget' but append the widget instead of prepend.
|
|
-appendWidget
|
|
+#endif
|
|
+ => B.ByteString -- ^ jquery selector
|
|
+ -> View v Identity a -- ^ widget to prepend
|
|
+ -> View v m B.ByteString -- ^ string returned with the jquery string to be executed in the browser
|
|
+prependWidget sel w= modifyWidget sel "prepend" w
|
|
+
|
|
+-- | Like 'prependWidget' but append the widget instead of prepend.
|
|
+appendWidget
|
|
#if defined(__GLASGOW_HASKELL__) && (__GLASGOW_HASKELL__ >= 707)
|
|
:: (Typeable a, MonadIO m, Executable m, FormInput v, Typeable Identity, Typeable m) =>
|
|
#else
|
|
:: (Typeable a, MonadIO m, Executable m, FormInput v) =>
|
|
#endif
|
|
- B.ByteString -> View v Identity a -> View v m B.ByteString
|
|
-appendWidget sel w= modifyWidget sel "append" w
|
|
-
|
|
--- | L ike 'prependWidget' but set the entire content of the selector instead of prepending an element
|
|
-setWidget
|
|
+ B.ByteString -> View v Identity a -> View v m B.ByteString
|
|
+appendWidget sel w= modifyWidget sel "append" w
|
|
+
|
|
+-- | L ike 'prependWidget' but set the entire content of the selector instead of prepending an element
|
|
+setWidget
|
|
#if defined(__GLASGOW_HASKELL__) && (__GLASGOW_HASKELL__ >= 707)
|
|
:: (Typeable a, MonadIO m, Executable m, FormInput v, Typeable m, Typeable Identity) =>
|
|
#else
|
|
:: (Typeable a, MonadIO m, Executable m, FormInput v) =>
|
|
-#endif
|
|
- B.ByteString -> View v Identity a -> View v m B.ByteString
|
|
-setWidget sel w= modifyWidget sel "html" w
|
|
-
|
|
-
|
|
--- | Inside a tag, it add and delete widgets of the same type. When the form is submitted
|
|
--- or a wlink is pressed, this widget return the list of validated widgets.
|
|
--- the event for adding a new widget is attached , as a click event to the element of the page with the identifier /wEditListAdd/
|
|
--- that the user will choose.
|
|
---
|
|
--- This example add or delete editable text boxes, with two initial boxes with
|
|
--- /hi/, /how are you/ as values. Tt uses blaze-html:
|
|
---
|
|
--- > r <- ask $ addLink
|
|
--- > ++> br
|
|
--- > ++> (El.div `wEditList` getString1 $ ["hi", "how are you"]) "addid"
|
|
--- > <++ br
|
|
--- > <** submitButton "send"
|
|
--- >
|
|
--- > ask $ p << (show r ++ " returned")
|
|
--- > ++> wlink () (p << text " back to menu")
|
|
--- > mainmenu
|
|
--- > where
|
|
--- > addLink = a ! At.id "addid"
|
|
--- > ! href "#"
|
|
--- > $ text "add"
|
|
--- > delBox = input ! type_ "checkbox"
|
|
--- > ! checked ""
|
|
--- > ! onclick "this.parentNode.parentNode.removeChild(this.parentNode)"
|
|
--- > getString1 mx= El.div <<< delBox ++> getString mx <++ br
|
|
-
|
|
-wEditList :: (Typeable a,Read a
|
|
+#endif
|
|
+ B.ByteString -> View v Identity a -> View v m B.ByteString
|
|
+setWidget sel w= modifyWidget sel "html" w
|
|
+
|
|
+
|
|
+-- | Inside a tag, it add and delete widgets of the same type. When the form is submitted
|
|
+-- or a wlink is pressed, this widget return the list of validated widgets.
|
|
+-- the event for adding a new widget is attached , as a click event to the element of the page with the identifier /wEditListAdd/
|
|
+-- that the user will choose.
|
|
+--
|
|
+-- This example add or delete editable text boxes, with two initial boxes with
|
|
+-- /hi/, /how are you/ as values. Tt uses blaze-html:
|
|
+--
|
|
+-- > r <- ask $ addLink
|
|
+-- > ++> br
|
|
+-- > ++> (El.div `wEditList` getString1 $ ["hi", "how are you"]) "addid"
|
|
+-- > <++ br
|
|
+-- > <** submitButton "send"
|
|
+-- >
|
|
+-- > ask $ p << (show r ++ " returned")
|
|
+-- > ++> wlink () (p << text " back to menu")
|
|
+-- > mainmenu
|
|
+-- > where
|
|
+-- > addLink = a ! At.id "addid"
|
|
+-- > ! href "#"
|
|
+-- > $ text "add"
|
|
+-- > delBox = input ! type_ "checkbox"
|
|
+-- > ! checked ""
|
|
+-- > ! onclick "this.parentNode.parentNode.removeChild(this.parentNode)"
|
|
+-- > getString1 mx= El.div <<< delBox ++> getString mx <++ br
|
|
+
|
|
+wEditList :: (Typeable a,Read a
|
|
,FormInput view
|
|
#if defined(__GLASGOW_HASKELL__) && (__GLASGOW_HASKELL__ >= 707)
|
|
,Functor m,MonadIO m, Executable m, Typeable m, Typeable Identity)
|
|
-#else
|
|
+#else
|
|
,Functor m,MonadIO m, Executable m)
|
|
-#endif
|
|
- => (view ->view) -- ^ The holder tag
|
|
- -> (Maybe String -> View view Identity a) -- ^ the contained widget, initialized by a string
|
|
- -> [String] -- ^ The initial list of values.
|
|
- -> String -- ^ The id of the button or link that will create a new list element when clicked
|
|
- -> View view m [a]
|
|
-wEditList holderview w xs addId = do
|
|
- let ws= map (w . Just) xs
|
|
- wn= w Nothing
|
|
- id1<- genNewId
|
|
- let sel= "$('#" <> fromString id1 <> "')"
|
|
- callAjax <- ajax . const $ prependWidget sel wn
|
|
- let installevents= "$(document).ready(function(){$('#"++addId++"').click(function(){"++callAjax "''"++"});})"
|
|
-
|
|
- requires [JScriptFile jqueryScript [installevents] ]
|
|
-
|
|
- ws' <- getEdited sel
|
|
-
|
|
- r <- (holderview <<< (allOf $ ws' ++ map changeMonad ws)) <! [("id",id1)]
|
|
- delEdited sel ws'
|
|
- return r
|
|
-
|
|
-
|
|
-
|
|
-
|
|
--- | Present the JQuery autocompletion list, from a procedure defined by the programmer, to a text box.
|
|
-wautocomplete
|
|
- :: (Show a, MonadIO m, FormInput v)
|
|
- => Maybe String -- ^ Initial value
|
|
- -> (String -> IO a) -- ^ Autocompletion procedure: will receive a prefix and return a list of strings
|
|
- -> View v m String
|
|
-wautocomplete mv autocomplete = do
|
|
- text1 <- genNewId
|
|
- ajaxc <- ajax $ \u -> do
|
|
- r <- liftIO $ autocomplete u
|
|
- return $ jaddtoautocomp text1 r
|
|
-
|
|
-
|
|
- requires [JScriptFile jqueryScript [] -- [events]
|
|
- ,CSSFile jqueryCSS
|
|
- ,JScriptFile jqueryUI []]
|
|
-
|
|
-
|
|
- getString mv <! [("type", "text")
|
|
- ,("id", text1)
|
|
- ,("oninput", ajaxc $ "$('#"++text1++"').attr('value')" )
|
|
- ,("autocomplete", "off")]
|
|
-
|
|
-
|
|
- where
|
|
- jaddtoautocomp text1 us= "$('#"<>fromString text1<>"').autocomplete({ source: " <> fromString( show us) <> " });"
|
|
-
|
|
-
|
|
--- | Produces a text box. It gives a autocompletion list to the textbox. When return
|
|
--- is pressed in the textbox, the box content is used to create a widget of a kind defined
|
|
--- by the user, which will be situated above of the textbox. When submitted, the result is the content
|
|
--- of the created widgets (the validated ones).
|
|
---
|
|
--- 'wautocompleteList' is an specialization of this widget, where
|
|
--- the widget parameter is fixed, with a checkbox that delete the eleement when unselected
|
|
--- . This fixed widget is as such (using generic 'FormElem' class tags):
|
|
---
|
|
--- > ftag "div" <<< ftag "input" mempty
|
|
--- > `attrs` [("type","checkbox")
|
|
--- > ,("checked","")
|
|
--- > ,("onclick","this.parentNode.parentNode.removeChild(this.parentNode)")]
|
|
--- > ++> ftag "span" (fromStr $ fromJust x )
|
|
--- > ++> whidden( fromJust x)
|
|
-wautocompleteEdit
|
|
+#endif
|
|
+ => (view ->view) -- ^ The holder tag
|
|
+ -> (Maybe String -> View view Identity a) -- ^ the contained widget, initialized by a string
|
|
+ -> [String] -- ^ The initial list of values.
|
|
+ -> String -- ^ The id of the button or link that will create a new list element when clicked
|
|
+ -> View view m [a]
|
|
+wEditList holderview w xs addId = do
|
|
+ let ws= map (w . Just) xs
|
|
+ wn= w Nothing
|
|
+ id1<- genNewId
|
|
+ let sel= "$('#" <> fromString id1 <> "')"
|
|
+ callAjax <- ajax . const $ prependWidget sel wn
|
|
+ let installevents= "$(document).ready(function(){$('#"++addId++"').click(function(){"++callAjax "''"++"});})"
|
|
+
|
|
+ requires [JScriptFile jqueryScript [installevents] ]
|
|
+
|
|
+ ws' <- getEdited sel
|
|
+
|
|
+ r <- (holderview <<< (allOf $ ws' ++ map changeMonad ws)) <! [("id",id1)]
|
|
+ delEdited sel ws'
|
|
+ return r
|
|
+
|
|
+
|
|
+
|
|
+
|
|
+-- | Present the JQuery autocompletion list, from a procedure defined by the programmer, to a text box.
|
|
+wautocomplete
|
|
+ :: (Show a, MonadIO m, FormInput v)
|
|
+ => Maybe String -- ^ Initial value
|
|
+ -> (String -> IO a) -- ^ Autocompletion procedure: will receive a prefix and return a list of strings
|
|
+ -> View v m String
|
|
+wautocomplete mv autocomplete = do
|
|
+ text1 <- genNewId
|
|
+ ajaxc <- ajax $ \u -> do
|
|
+ r <- liftIO $ autocomplete u
|
|
+ return $ jaddtoautocomp text1 r
|
|
+
|
|
+
|
|
+ requires [JScriptFile jqueryScript [] -- [events]
|
|
+ ,CSSFile jqueryCSS
|
|
+ ,JScriptFile jqueryUI []]
|
|
+
|
|
+
|
|
+ getString mv <! [("type", "text")
|
|
+ ,("id", text1)
|
|
+ ,("oninput", ajaxc $ "$('#"++text1++"').attr('value')" )
|
|
+ ,("autocomplete", "off")]
|
|
+
|
|
+
|
|
+ where
|
|
+ jaddtoautocomp text1 us= "$('#"<>fromString text1<>"').autocomplete({ source: " <> fromString( show us) <> " });"
|
|
+
|
|
+
|
|
+-- | Produces a text box. It gives a autocompletion list to the textbox. When return
|
|
+-- is pressed in the textbox, the box content is used to create a widget of a kind defined
|
|
+-- by the user, which will be situated above of the textbox. When submitted, the result is the content
|
|
+-- of the created widgets (the validated ones).
|
|
+--
|
|
+-- 'wautocompleteList' is an specialization of this widget, where
|
|
+-- the widget parameter is fixed, with a checkbox that delete the eleement when unselected
|
|
+-- . This fixed widget is as such (using generic 'FormElem' class tags):
|
|
+--
|
|
+-- > ftag "div" <<< ftag "input" mempty
|
|
+-- > `attrs` [("type","checkbox")
|
|
+-- > ,("checked","")
|
|
+-- > ,("onclick","this.parentNode.parentNode.removeChild(this.parentNode)")]
|
|
+-- > ++> ftag "span" (fromStr $ fromJust x )
|
|
+-- > ++> whidden( fromJust x)
|
|
+wautocompleteEdit
|
|
:: (Typeable a, MonadIO m,Functor m, Executable m
|
|
#if defined(__GLASGOW_HASKELL__) && (__GLASGOW_HASKELL__ >= 707)
|
|
, FormInput v, Typeable m, Typeable Identity)
|
|
-#else
|
|
+#else
|
|
, FormInput v)
|
|
-#endif
|
|
- => String -- ^ the initial text of the box
|
|
- -> (String -> IO [String]) -- ^ the autocompletion procedure: receives a prefix, return a list of options.
|
|
- -> (Maybe String -> View v Identity a) -- ^ the widget to add, initialized with the string entered in the box
|
|
- -> [String] -- ^ initial set of values
|
|
- -> View v m [a] -- ^ resulting widget
|
|
-wautocompleteEdit phold autocomplete elem values= do
|
|
- id1 <- genNewId
|
|
- let textx= id1++"text"
|
|
- let sel= "$('#" <> fromString id1 <> "')"
|
|
- ajaxc <- ajax $ \(c:u) ->
|
|
- case c of
|
|
- 'f' -> prependWidget sel (elem $ Just u)
|
|
- _ -> do
|
|
- r <- liftIO $ autocomplete u
|
|
- return $ jaddtoautocomp textx r
|
|
-
|
|
-
|
|
- requires [JScriptFile jqueryScript [events textx ajaxc]
|
|
- ,CSSFile jqueryCSS
|
|
- ,JScriptFile jqueryUI []]
|
|
-
|
|
- ws' <- getEdited sel
|
|
-
|
|
- r<- (ftag "div" mempty `attrs` [("id", id1)]
|
|
- ++> allOf (ws' ++ (map (changeMonad . elem . Just) values)))
|
|
- <++ ftag "input" mempty
|
|
- `attrs` [("type", "text")
|
|
- ,("id", textx)
|
|
- ,("placeholder", phold)
|
|
- ,("oninput", ajaxc $ "'n'+$('#"++textx++"').val()" )
|
|
- ,("autocomplete", "off")]
|
|
- delEdited sel ws'
|
|
- return r
|
|
- where
|
|
- events textx ajaxc=
|
|
- "$(document).ready(function(){ \
|
|
- \$('#"++textx++"').keydown(function(){ \
|
|
- \if(event.keyCode == 13){ \
|
|
- \var v= $('#"++textx++"').val(); \
|
|
- \if(event.preventDefault) event.preventDefault();\
|
|
- \else if(event.returnValue) event.returnValue = false;" ++
|
|
- ajaxc "'f'+v"++";"++
|
|
- " $('#"++textx++"').val('');\
|
|
- \}\
|
|
- \});\
|
|
- \});"
|
|
-
|
|
- jaddtoautocomp textx us= "$('#"<>fromString textx<>"').autocomplete({ source: " <> fromString( show us) <> " });"
|
|
+#endif
|
|
+ => String -- ^ the initial text of the box
|
|
+ -> (String -> IO [String]) -- ^ the autocompletion procedure: receives a prefix, return a list of options.
|
|
+ -> (Maybe String -> View v Identity a) -- ^ the widget to add, initialized with the string entered in the box
|
|
+ -> [String] -- ^ initial set of values
|
|
+ -> View v m [a] -- ^ resulting widget
|
|
+wautocompleteEdit phold autocomplete elem values= do
|
|
+ id1 <- genNewId
|
|
+ let textx= id1++"text"
|
|
+ let sel= "$('#" <> fromString id1 <> "')"
|
|
+ ajaxc <- ajax $ \(c:u) ->
|
|
+ case c of
|
|
+ 'f' -> prependWidget sel (elem $ Just u)
|
|
+ _ -> do
|
|
+ r <- liftIO $ autocomplete u
|
|
+ return $ jaddtoautocomp textx r
|
|
+
|
|
+
|
|
+ requires [JScriptFile jqueryScript [events textx ajaxc]
|
|
+ ,CSSFile jqueryCSS
|
|
+ ,JScriptFile jqueryUI []]
|
|
+
|
|
+ ws' <- getEdited sel
|
|
+
|
|
+ r<- (ftag "div" mempty `attrs` [("id", id1)]
|
|
+ ++> allOf (ws' ++ (map (changeMonad . elem . Just) values)))
|
|
+ <++ ftag "input" mempty
|
|
+ `attrs` [("type", "text")
|
|
+ ,("id", textx)
|
|
+ ,("placeholder", phold)
|
|
+ ,("oninput", ajaxc $ "'n'+$('#"++textx++"').val()" )
|
|
+ ,("autocomplete", "off")]
|
|
+ delEdited sel ws'
|
|
+ return r
|
|
+ where
|
|
+ events textx ajaxc=
|
|
+ "$(document).ready(function(){ \
|
|
+ \$('#"++textx++"').keydown(function(){ \
|
|
+ \if(event.keyCode == 13){ \
|
|
+ \var v= $('#"++textx++"').val(); \
|
|
+ \if(event.preventDefault) event.preventDefault();\
|
|
+ \else if(event.returnValue) event.returnValue = false;" ++
|
|
+ ajaxc "'f'+v"++";"++
|
|
+ " $('#"++textx++"').val('');\
|
|
+ \}\
|
|
+ \});\
|
|
+ \});"
|
|
+
|
|
+ jaddtoautocomp textx us= "$('#"<>fromString textx<>"').autocomplete({ source: " <> fromString( show us) <> " });"
|
|
|
|
|
|
#if defined(__GLASGOW_HASKELL__) && (__GLASGOW_HASKELL__ >= 707)
|
|
deriving instance Typeable Identity
|
|
#endif
|
|
-
|
|
--- | A specialization of 'wutocompleteEdit' which make appear each chosen option with
|
|
--- a checkbox that deletes the element when uncheched. The result, when submitted, is the list of selected elements.
|
|
+
|
|
+-- | A specialization of 'wutocompleteEdit' which make appear each chosen option with
|
|
+-- a checkbox that deletes the element when uncheched. The result, when submitted, is the list of selected elements.
|
|
wautocompleteList
|
|
#if defined(__GLASGOW_HASKELL__) && (__GLASGOW_HASKELL__ >= 707)
|
|
:: (Functor m, MonadIO m, Executable m, FormInput v, Typeable m, Typeable Identity) =>
|
|
-#else
|
|
+#else
|
|
:: (Functor m, MonadIO m, Executable m, FormInput v) =>
|
|
-#endif
|
|
- String -> (String -> IO [String]) -> [String] -> View v m [String]
|
|
-wautocompleteList phold serverproc values=
|
|
- wautocompleteEdit phold serverproc wrender1 values
|
|
- where
|
|
- wrender1 x= ftag "div" <<< ftag "input" mempty
|
|
- `attrs` [("type","checkbox")
|
|
- ,("checked","")
|
|
- ,("onclick","this.parentNode.parentNode.removeChild(this.parentNode)")]
|
|
- ++> ftag "span" (fromStr $ fromJust x )
|
|
- ++> whidden( fromJust x)
|
|
-
|
|
-------- Templating and localization ---------
|
|
-
|
|
-data TField = TField {tfieldKey :: Key, tfieldContent :: B.ByteString} deriving (Read, Show,Typeable)
|
|
-
|
|
-instance Indexable TField where
|
|
- key (TField k _)= k
|
|
- defPath _= "texts/"
|
|
-
|
|
-
|
|
-instance Serializable TField where
|
|
- serialize (TField k content) = content
|
|
- deserialKey k content= TField k content -- applyDeserializers [des1,des2] k bs
|
|
-
|
|
-
|
|
- setPersist = \_ -> Just filePersist
|
|
-
|
|
-
|
|
-
|
|
-writetField k s= atomically $ writeDBRef (getDBRef k) $ TField k $ toByteString s
|
|
-
|
|
-
|
|
-readtField text k= atomically $ do
|
|
- let ref = getDBRef k
|
|
- mr <- readDBRef ref
|
|
- case mr of
|
|
- Just (TField k v) -> if v /= mempty then return $ fromStrNoEncode $ toString v else return text
|
|
- Nothing -> return text
|
|
-
|
|
+#endif
|
|
+ String -> (String -> IO [String]) -> [String] -> View v m [String]
|
|
+wautocompleteList phold serverproc values=
|
|
+ wautocompleteEdit phold serverproc wrender1 values
|
|
+ where
|
|
+ wrender1 x= ftag "div" <<< ftag "input" mempty
|
|
+ `attrs` [("type","checkbox")
|
|
+ ,("checked","")
|
|
+ ,("onclick","this.parentNode.parentNode.removeChild(this.parentNode)")]
|
|
+ ++> ftag "span" (fromStr $ fromJust x )
|
|
+ ++> whidden( fromJust x)
|
|
+
|
|
+------- Templating and localization ---------
|
|
+
|
|
+data TField = TField {tfieldKey :: Key, tfieldContent :: B.ByteString} deriving (Read, Show,Typeable)
|
|
+
|
|
+instance Indexable TField where
|
|
+ key (TField k _)= k
|
|
+ defPath _= "texts/"
|
|
+
|
|
+
|
|
+instance Serializable TField where
|
|
+ serialize (TField k content) = content
|
|
+ deserialKey k content= TField k content -- applyDeserializers [des1,des2] k bs
|
|
+
|
|
+
|
|
+ setPersist = \_ -> Just filePersist
|
|
+
|
|
+
|
|
+
|
|
+writetField k s= atomically $ writeDBRef (getDBRef k) $ TField k $ toByteString s
|
|
+
|
|
+
|
|
+readtField text k= atomically $ do
|
|
+ let ref = getDBRef k
|
|
+ mr <- readDBRef ref
|
|
+ case mr of
|
|
+ Just (TField k v) -> if v /= mempty then return $ fromStrNoEncode $ toString v else return text
|
|
+ Nothing -> return text
|
|
+
|
|
-- | Creates a rich text editor aroun a text field or a text area widget.
|
|
-- This code:
|
|
--
|
|
@@ -512,221 +512,224 @@
|
|
-- > <** submitButton "enter"
|
|
--
|
|
-- Creates a rich text area with bold and italic buttons. The buttons are the ones alled
|
|
--- in the nicEdit editor.
|
|
-htmlEdit :: (Monad m, FormInput v) => [String] -> UserStr -> View v m a -> View v m a
|
|
-htmlEdit buttons jsuser w = do
|
|
- id <- genNewId
|
|
-
|
|
- let installHtmlField=
|
|
- "\nfunction installHtmlField(muser,cookieuser,name,buttons){\
|
|
- \if(muser== '' || document.cookie.search(cookieuser+'='+muser) != -1)\
|
|
- \bkLib.onDomLoaded(function() {\
|
|
- \var myNicEditor = new nicEditor({buttonList : buttons});\
|
|
- \myNicEditor.panelInstance(name);\
|
|
- \})};\n"
|
|
- install= "installHtmlField('"++jsuser++"','"++cookieuser++"','"++id++"',"++show buttons++");\n"
|
|
-
|
|
- requires [JScriptFile nicEditUrl [installHtmlField,install]]
|
|
- w <! [("id",id)]
|
|
-
|
|
-
|
|
-
|
|
-
|
|
--- | A widget that display the content of an html, But if the user has edition privileges,
|
|
--- it permits to edit it in place. So the editor could see the final appearance
|
|
--- of what he writes.
|
|
---
|
|
--- When the user click the save, the content is saved and
|
|
--- identified by the key. Then, from now on, all the users will see the saved
|
|
--- content instead of the code content.
|
|
---
|
|
--- The content is saved in a file by default (/texts/ in this versions), but there is
|
|
--- a configurable version (`tFieldGen`). The content of the element and the formatting
|
|
--- is cached in memory, so the display is, theoretically, very fast.
|
|
---
|
|
-
|
|
-tFieldEd
|
|
- :: (Functor m, MonadIO m, Executable m,
|
|
- FormInput v) =>
|
|
- UserStr -> Key -> v -> View v m ()
|
|
-tFieldEd muser k text= wfreeze k 0 $ do
|
|
- content <- liftIO $ readtField text k
|
|
- nam <- genNewId
|
|
- let ipanel= nam++"panel"
|
|
- name= nam++"-"++k
|
|
- install= "\ninstallEditField('"++muser++"','"++cookieuser++"','"++name++"','"++ipanel++"');\n"
|
|
- getTexts :: (Token -> IO ())
|
|
- getTexts token = do
|
|
- let (k,s):_ = tenv token
|
|
- liftIO $ do
|
|
- writetField k $ (fromStrNoEncode s `asTypeOf` text)
|
|
- flushCached k
|
|
- sendFlush token $ HttpData [] [] ""
|
|
- return()
|
|
-
|
|
- requires [JScriptFile nicEditUrl [install]
|
|
- ,JScript ajaxSendText
|
|
- ,JScript installEditField
|
|
--- ,JScriptFile jqueryScript []
|
|
- ,ServerProc ("_texts", transient getTexts)]
|
|
+-- in the nicEdit editor.
|
|
+htmlEdit :: (Monad m, FormInput v) => [String] -> UserStr -> View v m a -> View v m a
|
|
+htmlEdit buttons jsuser w = do
|
|
+ id <- genNewId
|
|
+
|
|
+ let installHtmlField=
|
|
+ "\nfunction installHtmlField(muser,cookieuser,name,buttons){\
|
|
+ \if(muser== '' || document.cookie.search(cookieuser+'='+muser) != -1)\
|
|
+ \bkLib.onDomLoaded(function() {\
|
|
+ \var myNicEditor = new nicEditor({buttonList : buttons});\
|
|
+ \myNicEditor.panelInstance(name);\
|
|
+ \})};\n"
|
|
+ install= "installHtmlField('"++jsuser++"','"++cookieuser++"','"++id++"',"++show buttons++");\n"
|
|
+
|
|
+ requires [JScript installHtmlField ,JScriptFile nicEditUrl [install]]
|
|
+ w <! [("id",id)]
|
|
+
|
|
+
|
|
+
|
|
+
|
|
+-- | A widget that display the content of an html, But if the user has edition privileges,
|
|
+-- it permits to edit it in place. So the editor could see the final appearance
|
|
+-- of what he writes.
|
|
+--
|
|
+-- When the user click the save, the content is saved and
|
|
+-- identified by the key. Then, from now on, all the users will see the saved
|
|
+-- content instead of the code content.
|
|
+--
|
|
+-- The content is saved in a file by default (/texts/ in this versions), but there is
|
|
+-- a configurable version (`tFieldGen`). The content of the element and the formatting
|
|
+-- is cached in memory, so the display is, theoretically, very fast.
|
|
+--
|
|
+
|
|
+tFieldEd
|
|
+ :: (Functor m, MonadIO m, Executable m,
|
|
+ FormInput v) =>
|
|
+ UserStr -> Key -> v -> View v m ()
|
|
+tFieldEd muser k text= wfreeze k 0 $ do
|
|
+ content <- liftIO $ readtField text k
|
|
+ nam <- genNewId
|
|
+ let ipanel= nam++"panel"
|
|
+ name= nam++"-"++k
|
|
+ install= "installEditField('"++muser++"','"++cookieuser++"','"++name++"','"++ipanel++"');\n"
|
|
+ getTexts :: (Token -> IO ())
|
|
+ getTexts token = do
|
|
+ let (k,s):_ = tenv token
|
|
+ liftIO $ do
|
|
+ writetField k $ (fromStrNoEncode s `asTypeOf` text)
|
|
+ flushCached k
|
|
+ sendFlush token $ HttpData [] [] ""
|
|
+ return()
|
|
+
|
|
+ requires [JScriptFile nicEditUrl [install]
|
|
+ ,JScript ajaxSendText
|
|
+ ,JScript installEditField
|
|
+ ,ServerProc ("_texts", transient getTexts)]
|
|
|
|
us <- getCurrentUser
|
|
when(us== muser) noCache
|
|
-
|
|
- (ftag "div" mempty `attrs` [("id",ipanel)]) ++>
|
|
- notValid (ftag "span" content `attrs` [("id", name)])
|
|
-
|
|
-
|
|
-
|
|
-installEditField=
|
|
- "\nfunction installEditField(muser,cookieuser,name,ipanel){\
|
|
- \if(muser== '' || document.cookie.search(cookieuser+'='+muser) != -1)\
|
|
- \bkLib.onDomLoaded(function() {\
|
|
- \var myNicEditor = new nicEditor({fullPanel : true, onSave : function(content, id, instance) {\
|
|
- \ajaxSendText(id,content);\
|
|
- \myNicEditor.removeInstance(name);\
|
|
- \myNicEditor.removePanel(ipanel);\
|
|
- \}});\
|
|
- \myNicEditor.addInstance(name);\
|
|
- \myNicEditor.setPanel(ipanel);\
|
|
- \})};\n"
|
|
-
|
|
-ajaxSendText = "\nfunction ajaxSendText(id,content){\
|
|
- \var arr= id.split('-');\
|
|
- \var k= arr[1];\
|
|
- \$.ajax({\
|
|
- \type: 'POST',\
|
|
- \url: '/_texts',\
|
|
- \data: k + '='+ encodeURIComponent(content),\
|
|
- \success: function (resp) {},\
|
|
- \error: function (xhr, status, error) {\
|
|
- \var msg = $('<div>' + xhr + '</div>');\
|
|
- \id1.html(msg);\
|
|
- \}\
|
|
- \});\
|
|
- \return false;\
|
|
- \};\n"
|
|
-
|
|
--- | a text field. Read the cached field value and present it without edition.
|
|
-tField :: (MonadIO m,Functor m, Executable m, FormInput v)
|
|
- => Key
|
|
- -> View v m ()
|
|
-tField k = wfreeze k 0 $ do
|
|
- content <- liftIO $ readtField (fromStrNoEncode "not found") k
|
|
- notValid content
|
|
-
|
|
--- | A multilanguage version of tFieldEd. For a field with @key@ it add a suffix with the
|
|
--- two characters of the language used.
|
|
-mFieldEd muser k content= do
|
|
- lang <- getLang
|
|
- tFieldEd muser (k ++ ('-':lang)) content
|
|
-
|
|
-
|
|
-
|
|
--- | A multilanguage version of tField
|
|
-mField k= do
|
|
- lang <- getLang
|
|
- tField $ k ++ ('-':lang)
|
|
-
|
|
-newtype IteratedId= IteratedId String deriving Typeable
|
|
-
|
|
--- | Permits to iterate the presentation of data and//or input fields and widgets within
|
|
--- a web page that does not change. The placeholders are created with dField. Both are widget
|
|
--- modifiers: The latter gets a widget and create a placeholder in the page that is updated
|
|
--- via ajax. The content of the update is the rendering of the widget at each iteration.
|
|
--- The former gets a wider widget which contains dField elements and permit the iteration.
|
|
--- Whenever a link or a form within the witerate widget is activated, the result is the
|
|
--- placeholders filled with the new html content. This content can be data, a input field,
|
|
--- a link or a widget. No navigation happens.
|
|
---
|
|
--- This permits even faster updates than autoRefresh. since the latter refresh the whole
|
|
--- widget and it does not permits modifications of the layout at runtime.
|
|
---
|
|
--- When edTemplate or template is used on top of witerate, the result is editable at runtime,
|
|
--- and the span placeholders generated, that are updated via ajax can be relocated within
|
|
--- the layout of the template.
|
|
---
|
|
--- Additionally, contrary to some javascript frameworks, the pages generated with this
|
|
--- mechanism are searchable by web crawlers.
|
|
-
|
|
-witerate
|
|
- :: (MonadIO m, Functor m, FormInput v) =>
|
|
- View v m a -> View v m a
|
|
-witerate w= do
|
|
- name <- genNewId
|
|
- setSessionData $ IteratedId name
|
|
- st <- get
|
|
- let t= mfkillTime st
|
|
- let installAutoEval=
|
|
- "$(document).ready(function(){\
|
|
- \autoEvalLink('"++name++"',0);\
|
|
- \autoEvalForm('"++name++"');\
|
|
- \})\n"
|
|
+
|
|
+ (ftag "div" mempty `attrs` [("id",ipanel)]) ++>
|
|
+ notValid (ftag "span" content `attrs` [("id", name)])
|
|
+
|
|
+
|
|
+
|
|
+installEditField=
|
|
+ "\nfunction installEditField(muser,cookieuser,name,ipanel){\
|
|
+ \if(muser== '' || document.cookie.search(cookieuser+'='+muser) != -1){\
|
|
+ \var myNicEditor = new nicEditor({fullPanel : true, onSave : function(content, id, instance) {\
|
|
+ \ajaxSendText(id,content);\
|
|
+ \myNicEditor.removeInstance(name);\
|
|
+ \myNicEditor.removePanel(ipanel);\
|
|
+ \}});\
|
|
+ \myNicEditor.addInstance(name);\
|
|
+ \myNicEditor.setPanel(ipanel);\
|
|
+ \}};\n"
|
|
+
|
|
+ajaxSendText = "\nfunction ajaxSendText(id,content){\
|
|
+ \var arr= id.split('-');\
|
|
+ \var k= arr[1];\
|
|
+ \$.ajax({\
|
|
+ \type: 'POST',\
|
|
+ \url: '/_texts',\
|
|
+ \data: k + '='+ encodeURIComponent(content),\
|
|
+ \success: function (resp) {},\
|
|
+ \error: function (xhr, status, error) {\
|
|
+ \var msg = $('<div>' + xhr + '</div>');\
|
|
+ \id1.html(msg);\
|
|
+ \}\
|
|
+ \});\
|
|
+ \return false;\
|
|
+ \};\n"
|
|
+
|
|
+-- | a text field. Read the cached field value and present it without edition.
|
|
+tField :: (MonadIO m,Functor m, Executable m, FormInput v)
|
|
+ => Key
|
|
+ -> View v m ()
|
|
+tField k = wfreeze k 0 $ do
|
|
+ content <- liftIO $ readtField (fromStrNoEncode "not found") k
|
|
+ notValid content
|
|
+
|
|
+-- | A multilanguage version of tFieldEd. For a field with @key@ it add a suffix with the
|
|
+-- two characters of the language used.
|
|
+mFieldEd muser k content= do
|
|
+ lang <- getLang
|
|
+ tFieldEd muser (k ++ ('-':lang)) content
|
|
+
|
|
+
|
|
+
|
|
+-- | A multilanguage version of tField
|
|
+mField k= do
|
|
+ lang <- getLang
|
|
+ tField $ k ++ ('-':lang)
|
|
+
|
|
+data IteratedId = IteratedId String String deriving (Typeable, Show)
|
|
+
|
|
+-- | Permits to iterate the presentation of data and//or input fields and widgets within
|
|
+-- a web page that does not change. The placeholders are created with dField. Both are widget
|
|
+-- modifiers: The latter gets a widget and create a placeholder in the page that is updated
|
|
+-- via ajax. The content of the update is the rendering of the widget at each iteration.
|
|
+-- The former gets a wider widget which contains dField elements and permit the iteration.
|
|
+-- Whenever a link or a form within the witerate widget is activated, the result is the
|
|
+-- placeholders filled with the new html content. This content can be data, a input field,
|
|
+-- a link or a widget. No navigation happens.
|
|
+--
|
|
+-- This permits even faster updates than autoRefresh. since the latter refresh the whole
|
|
+-- widget and it does not permits modifications of the layout at runtime.
|
|
+--
|
|
+-- When edTemplate or template is used on top of witerate, the result is editable at runtime,
|
|
+-- and the span placeholders generated, that are updated via ajax can be relocated within
|
|
+-- the layout of the template.
|
|
+--
|
|
+-- Additionally, contrary to some javascript frameworks, the pages generated with this
|
|
+-- mechanism are searchable by web crawlers.
|
|
+
|
|
+witerate
|
|
+ :: (MonadIO m, Functor m, FormInput v) =>
|
|
+ View v m a -> View v m a
|
|
+witerate w= do
|
|
+ name <- genNewId
|
|
+ setSessionData $ IteratedId name mempty
|
|
+ st <- get
|
|
+ let t= mfkillTime st
|
|
+ let installAutoEval=
|
|
+ "$(document).ready(function(){\
|
|
+ \autoEvalLink('"++name++"',0);\
|
|
+ \autoEvalForm('"++name++"');\
|
|
+ \})\n"
|
|
let r = lookup ("auto"++name) $ mfEnv st
|
|
w'= w `wcallback` (const $ do
|
|
+ setSessionData $ IteratedId name mempty
|
|
modify $ \s -> s{mfPagePath=mfPagePath st
|
|
,mfSequence= mfSequence st
|
|
- ,mfRequirements= if r== Nothing then mfRequirements s else []
|
|
,mfHttpHeaders=[]}
|
|
- w)
|
|
-
|
|
- ret <- case r of
|
|
- Nothing -> do
|
|
- requires [JScript autoEvalLink
|
|
- ,JScript autoEvalForm
|
|
- ,JScript $ timeoutscript t
|
|
+ w)
|
|
+
|
|
+ ret <- case r of
|
|
+ Nothing -> do
|
|
+ requires [JScript autoEvalLink
|
|
+ ,JScript autoEvalForm
|
|
+ ,JScript $ timeoutscript t
|
|
,JScriptFile jqueryScript [installAutoEval]
|
|
- ,JScript setId]
|
|
-
|
|
- (ftag "div" <<< w') <! [("id",name)]
|
|
-
|
|
- Just sind -> View $ do
|
|
- let t= mfToken st
|
|
- modify $ \s -> s{mfRequirements=[],mfHttpHeaders=[]} -- !> "just"
|
|
- resetCachePolicy
|
|
- FormElm _ mr <- runView w'
|
|
- setCachePolicy
|
|
- reqs <- return . map ( \(Requirement r) -> unsafeCoerce r) =<< gets mfRequirements
|
|
- let js = jsRequirements True reqs
|
|
-
|
|
- st' <- get
|
|
- liftIO . sendFlush t $ HttpData
|
|
- (mfHttpHeaders st')
|
|
- (mfCookies st') (fromString js)
|
|
- put st'{mfAutorefresh=True, inSync=True}
|
|
- return $ FormElm mempty Nothing
|
|
-
|
|
- delSessionData $ IteratedId name
|
|
+ ,JScript setId]
|
|
+
|
|
+ (ftag "div" <<< w') <! [("id",name)]
|
|
+
|
|
+ Just sind -> refresh $ View $ do
|
|
+ FormElm _ mr <- runView w'
|
|
+ IteratedId _ render <- getSessionData `onNothing` return (IteratedId name mempty)
|
|
+ return $ FormElm (fromStrNoEncode render) mr
|
|
+
|
|
+-- View $ do
|
|
+-- let t= mfToken st
|
|
+-- modify $ \s -> s{mfRequirements=[],mfHttpHeaders=[]} -- !> "just"
|
|
+-- resetCachePolicy
|
|
+-- FormElm _ mr <- runView w'
|
|
+-- setCachePolicy
|
|
+--
|
|
+-- reqs <- installAllRequirements
|
|
+--
|
|
+-- st' <- get
|
|
+-- liftIO . sendFlush t $ HttpData
|
|
+-- (mfHttpHeaders st')
|
|
+-- (mfCookies st') (toByteString reqs)
|
|
+-- put st'{mfAutorefresh=True, inSync=True}
|
|
+-- return $ FormElm mempty Nothing
|
|
+
|
|
+ delSessionData $ IteratedId name mempty
|
|
return ret
|
|
|
|
|
|
-
|
|
-autoEvalLink = "\nfunction autoEvalLink(id,ind){\
|
|
- \var id1= $('#'+id);\
|
|
+
|
|
+autoEvalLink = "\nfunction autoEvalLink(id,ind){\
|
|
+ \var id1= $('#'+id);\
|
|
\var ida= $('#'+id+' a[class!=\"_noAutoRefresh\"]');\
|
|
- \ida.off('click');\
|
|
- \ida.click(function () {\
|
|
- \if (hadtimeout == true) return true;\
|
|
- \var pdata = $(this).attr('data-value');\
|
|
- \var actionurl = $(this).attr('href');\
|
|
- \var dialogOpts = {\
|
|
+ \ida.off('click');\
|
|
+ \ida.click(function () {\
|
|
+ \if (hadtimeout == true) return true;\
|
|
+ \var pdata = $(this).attr('data-value');\
|
|
+ \var actionurl = $(this).attr('href');\
|
|
+ \var dialogOpts = {\
|
|
\type: 'GET',\
|
|
- \url: actionurl+'?auto'+id+'='+ind,\
|
|
- \data: pdata,\
|
|
- \success: function (resp) {\
|
|
+ \url: actionurl+'?auto'+id+'='+ind,\
|
|
+ \data: pdata,\
|
|
+ \success: function (resp) {\
|
|
\eval(resp);\
|
|
\autoEvalLink(id,ind);\
|
|
- \autoEvalForm(id);\
|
|
- \},\
|
|
- \error: function (xhr, status, error) {\
|
|
- \var msg = $('<div>' + xhr + '</div>');\
|
|
- \id1.html(msg);\
|
|
- \}\
|
|
- \};\
|
|
- \$.ajax(dialogOpts);\
|
|
- \return false;\
|
|
- \});\
|
|
- \}\n"
|
|
+ \autoEvalForm(id);\
|
|
+ \},\
|
|
+ \error: function (xhr, status, error) {\
|
|
+ \var msg = $('<div>' + xhr + '</div>');\
|
|
+ \id1.html(msg);\
|
|
+ \}\
|
|
+ \};\
|
|
+ \$.ajax(dialogOpts);\
|
|
+ \return false;\
|
|
+ \});\
|
|
+ \}\n"
|
|
|
|
autoEvalForm = "\nfunction autoEvalForm(id) {\
|
|
\var buttons= $('#'+id+' input[type=\"submit\"]');\
|
|
@@ -736,12 +739,12 @@
|
|
\if ($(this).attr('class') != '_noAutoRefresh'){\
|
|
\event.preventDefault();\
|
|
\if (hadtimeout == true) return true;\
|
|
- \var $form = $(this).closest('form');\
|
|
+ \var $form = $(this).closest('form');\
|
|
\var url = $form.attr('action');\
|
|
\pdata = 'auto'+id+'=true&'+this.name+'='+this.value+'&'+$form.serialize();\
|
|
\postForm(id,url,pdata);\
|
|
\return false;\
|
|
- \}else {\
|
|
+ \}else {\
|
|
\noajax= true;\
|
|
\return true;\
|
|
\}\
|
|
@@ -751,7 +754,7 @@
|
|
\idform.submit(function(event) {\
|
|
\if(noajax) {noajax=false; return true;}\
|
|
\event.preventDefault();\
|
|
- \var $form = $(this);\
|
|
+ \var $form = $(this);\
|
|
\var url = $form.attr('action');\
|
|
\var pdata = 'auto'+id+'=true&' + $form.serialize();\
|
|
\postForm(id,url,pdata);\
|
|
@@ -759,290 +762,291 @@
|
|
\}\
|
|
\function postForm(id,url,pdata){\
|
|
\var id1= $('#'+id);\
|
|
- \$.ajax({\
|
|
- \type: 'POST',\
|
|
- \url: url,\
|
|
- \data: 'auto'+id+'=true&'+this.name+'='+this.value+'&'+pdata,\
|
|
- \success: function (resp) {\
|
|
+ \$.ajax({\
|
|
+ \type: 'POST',\
|
|
+ \url: url,\
|
|
+ \data: 'auto'+id+'=true&'+this.name+'='+this.value+'&'+pdata,\
|
|
+ \success: function (resp) {\
|
|
\eval(resp);\
|
|
\autoEvalLink(id,0);\
|
|
- \autoEvalForm(id);\
|
|
+ \autoEvalForm(id);\
|
|
\},\
|
|
- \error: function (xhr, status, error) {\
|
|
- \var msg = $('<div>' + xhr + '</div>');\
|
|
- \id1.html(msg);\
|
|
+ \error: function (xhr, status, error) {\
|
|
+ \var msg = $('<div>' + xhr + '</div>');\
|
|
+ \id1.html(msg);\
|
|
\}\
|
|
- \});\
|
|
+ \});\
|
|
\}"
|
|
|
|
-
|
|
-
|
|
-setId= "function setId(id,v){document.getElementById(id).innerHTML= v;};\n"
|
|
-
|
|
--- | Present a widget via AJAX if it is within a 'witerate' context. In the first iteration it present the
|
|
--- widget surrounded by a placeholder. subsequent iterations will send just the javascript code
|
|
--- necessary for the refreshing of the placeholder.
|
|
-dField
|
|
- :: (Monad m, FormInput view) =>
|
|
- View view m b -> View view m b
|
|
-dField w= View $ do
|
|
- id <- genNewId
|
|
- FormElm render mx <- runView w
|
|
- st <- get
|
|
- let env = mfEnv st
|
|
-
|
|
- IteratedId name <- getSessionData `onNothing` return (IteratedId noid)
|
|
- let r = lookup ("auto"++name) env
|
|
+
|
|
+
|
|
+setId= "function setId(id,v){document.getElementById(id).innerHTML= v;};\n"
|
|
+
|
|
+-- | Present a widget via AJAX if it is within a 'witerate' context. In the first iteration it present the
|
|
+-- widget surrounded by a placeholder. subsequent iterations will send just the javascript code
|
|
+-- necessary for the refreshing of the placeholder.
|
|
+dField
|
|
+ :: (Monad m, FormInput view) =>
|
|
+ View view m b -> View view m b
|
|
+dField w= View $ do
|
|
+ id <- genNewId
|
|
+ FormElm render mx <- runView w
|
|
+ st <- get
|
|
+ let env = mfEnv st
|
|
+
|
|
+ IteratedId name scripts <- getSessionData `onNothing` return (IteratedId noid mempty)
|
|
+ let r = lookup ("auto"++name) env
|
|
if r == Nothing || (name == noid && newAsk st== True)
|
|
- then do
|
|
--- requires [JScriptFile jqueryScript ["$(document).ready(function() {setId('"++id++"','" ++ toString (toByteString render)++"')});\n"]]
|
|
- return $ FormElm((ftag "span" render) `attrs` [("id",id)]) mx
|
|
- else do
|
|
- requires [JScript $ "setId('"++id++"','" ++ toString (toByteString $ render)++"');\n"]
|
|
- return $ FormElm mempty mx
|
|
-
|
|
-noid= "noid"
|
|
-
|
|
+ then return $ FormElm((ftag "span" render) `attrs` [("id",id)]) mx
|
|
+ else do
|
|
+ setSessionData $ IteratedId name $ scripts <> "setId('"++id++"','" ++ toString (toByteString $ render)++"');"
|
|
+ return $ FormElm mempty mx
|
|
+
|
|
+noid= "noid"
|
|
+
|
|
|
|
-- | permits the edition of the rendering of a widget at run time. Once saved, the new rendering
|
|
-- becomes the new rendering of the widget for all the users. You must keep the active elements of the
|
|
-- template
|
|
--
|
|
-- the first parameter is the user that has permissions for edition. the second is a key that
|
|
--- identifies the template.
|
|
-edTemplate
|
|
- :: (MonadIO m, FormInput v, Typeable a) =>
|
|
- UserStr -> Key -> View v m a -> View v m a
|
|
-edTemplate muser k w= View $ do
|
|
- nam <- genNewId
|
|
-
|
|
- let ipanel= nam++"panel"
|
|
- name= nam++"-"++k
|
|
- install= "installEditField('"++muser++"','"++cookieuser++"','"++name++"','"++ipanel++"');\n"
|
|
-
|
|
-
|
|
- requires [JScriptFile nicEditUrl [install]
|
|
- ,JScript ajaxSendText
|
|
- ,JScript installEditField
|
|
- ,JScriptFile jqueryScript []
|
|
- ,ServerProc ("_texts", transient getTexts)]
|
|
+-- identifies the template.
|
|
+edTemplate
|
|
+ :: (MonadIO m, FormInput v, Typeable a) =>
|
|
+ UserStr -> Key -> View v m a -> View v m a
|
|
+edTemplate muser k w= View $ do
|
|
+ nam <- genNewId
|
|
+
|
|
+ let ipanel= nam++"panel"
|
|
+ name= nam++"-"++k
|
|
+ install= "installEditField('"++muser++"','"++cookieuser++"','"++name++"','"++ipanel++"');\n"
|
|
+
|
|
+
|
|
+ requires [JScript installEditField
|
|
+ ,JScriptFile nicEditUrl [install]
|
|
+ ,JScript ajaxSendText
|
|
+ ,JScriptFile jqueryScript []
|
|
+ ,ServerProc ("_texts", transient getTexts)]
|
|
us <- getCurrentUser
|
|
- when(us== muser) noCache
|
|
- FormElm text mx <- runView w
|
|
- content <- liftIO $ readtField text k
|
|
-
|
|
- return $ FormElm (ftag "div" mempty `attrs` [("id",ipanel)] <>
|
|
- ftag "span" content `attrs` [("id", name)])
|
|
- mx
|
|
- where
|
|
- getTexts :: (Token -> IO ())
|
|
- getTexts token= do
|
|
- let (k,s):_ = tenv token
|
|
- liftIO $ do
|
|
- writetField k $ (fromStrNoEncode s `asTypeOf` viewFormat w)
|
|
- flushCached k
|
|
- sendFlush token $ HttpData [] [] ""
|
|
- return()
|
|
-
|
|
- viewFormat :: View v m a -> v
|
|
- viewFormat= undefined -- is a type function
|
|
-
|
|
+ when(us== muser) noCache
|
|
+ FormElm text mx <- runView w
|
|
+ content <- liftIO $ readtField text k
|
|
+
|
|
+ return $ FormElm (ftag "div" mempty `attrs` [("id",ipanel)] <>
|
|
+ ftag "span" content `attrs` [("id", name)])
|
|
+ mx
|
|
+ where
|
|
+ getTexts :: Token -> IO () -- low level server process
|
|
+ getTexts token= do
|
|
+ let (k,s):_ = tenv token
|
|
+ liftIO $ do
|
|
+ writetField k $ (fromStrNoEncode s `asTypeOf` viewFormat w)
|
|
+ flushCached k
|
|
+ sendFlush token $ HttpData [] [] "" --empty response
|
|
+
|
|
+ return()
|
|
+
|
|
+
|
|
+ viewFormat :: View v m a -> v
|
|
+ viewFormat= undefined -- is a type function
|
|
+
|
|
-- | Does the same than template but without the edition facility
|
|
-template
|
|
- :: (MonadIO m, FormInput v, Typeable a) =>
|
|
- Key -> View v m a -> View v m a
|
|
-template k w= View $ do
|
|
- FormElm text mx <- runView w
|
|
- let content= unsafePerformIO $ readtField text k
|
|
- return $ FormElm content mx
|
|
-
|
|
-
|
|
-
|
|
-------------------- JQuery widgets -------------------
|
|
--- | present the JQuery datepicker calendar to choose a date.
|
|
--- The second parameter is the configuration. Use \"()\" by default.
|
|
--- See http://jqueryui.com/datepicker/
|
|
-datePicker :: (Monad m, FormInput v) => String -> Maybe String -> View v m (Int,Int,Int)
|
|
-datePicker conf jd= do
|
|
- id <- genNewId
|
|
- let setit= "$(document).ready(function() {\
|
|
- \$( '#"++id++"' ).datepicker "++ conf ++";\
|
|
- \});"
|
|
-
|
|
- requires
|
|
- [CSSFile jqueryCSS
|
|
- ,JScriptFile jqueryScript []
|
|
- ,JScriptFile jqueryUI [setit]]
|
|
-
|
|
- s <- getString jd <! [("id",id)]
|
|
- let (month,r) = span (/='/') s
|
|
- let (day,r2)= span(/='/') $ tail r
|
|
- return (read day,read month, read $ tail r2)
|
|
-
|
|
--- | present a jQuery dialog with a widget. When a button is pressed it return the result.
|
|
--- The first parameter is the configuration. To make it modal, use \"({modal: true})\" see <http://jqueryui.com/dialog/> for
|
|
--- the available configurations.
|
|
---
|
|
--- The enclosed widget will be wrapped within a form tag if the user do not encloses it using wform.f
|
|
-wdialog :: (Monad m, FormInput v) => String -> String -> View v m a -> View v m a
|
|
-wdialog conf title w= do
|
|
- id <- genNewId
|
|
- let setit= "$(document).ready(function() {\
|
|
- \$('#"++id++"').dialog "++ conf ++";\
|
|
- \var idform= $('#"++id++" form');\
|
|
- \idform.submit(function(){$(this).dialog(\"close\")})\
|
|
- \});"
|
|
-
|
|
- modify $ \st -> st{needForm= HasForm}
|
|
- requires
|
|
- [CSSFile jqueryCSS
|
|
- ,JScriptFile jqueryScript []
|
|
- ,JScriptFile jqueryUI [setit]]
|
|
-
|
|
- (ftag "div" <<< insertForm w) <! [("id",id),("title", title)]
|
|
-
|
|
-
|
|
-
|
|
-
|
|
-
|
|
--- | Capture the form or link submissions and send them via jQuery AJAX.
|
|
--- The response is the new presentation of the widget, that is updated. No new page is generated
|
|
--- but the functionality is equivalent. Only the activated widget rendering is updated
|
|
--- in the client, so a widget with autoRefresh can be used in heavyweight pages.
|
|
--- If AJAX/JavaScript are not available, the widget is refreshed normally, via a new page.
|
|
---
|
|
--- autoRefresh encloses the widget in a form tag if it includes form elements.
|
|
---
|
|
--- If there are more than one autoRefresh, they must be enclosed within 'pageFlow' elements
|
|
-autoRefresh
|
|
- :: (MonadIO m,
|
|
- FormInput v)
|
|
- => View v m a
|
|
- -> View v m a
|
|
-autoRefresh = update "html"
|
|
-
|
|
--- | In some cases, it is neccessary that a link or form inside a 'autoRefresh' or 'update' block
|
|
--- should not be autorefreshed, since it produces side effects in the rest of the page that
|
|
--- affect to the rendering of the whole. If you like to refresh the whole page, simply add
|
|
--- noAutoRefresh attribute to the widget to force the refresh of the whole page when it is activated.
|
|
---
|
|
--- That behaviour is common at the last sentence of the 'autoRefresh' block.
|
|
---
|
|
--- This is a cascade menu example.
|
|
---
|
|
--- > r <- page $ autoRefresh $ ul <<< do
|
|
--- > li <<< wlink OptionA << "option A"
|
|
--- > ul <<< li <<< (wlink OptionA1 << "Option A1" <! noAutoRefresh)
|
|
--- > <|> li <<< (wlink OptionA2 << "Option A2" <! noAutoRefresh)
|
|
--- > <|>...
|
|
--- > maybe other content
|
|
--- >
|
|
--- > case r of
|
|
--- > OptionA1 -> pageA1
|
|
--- > OptionA2 -> pageA2
|
|
---
|
|
--- when @option A@ is clicked, the two sub-options appear with autorefresh. Only the two
|
|
--- lines are returned by the server using AJAX. but when Option A1-2 is pressed we want to
|
|
--- present other pages, so we add the noAutorefresh attribute.
|
|
---
|
|
--- NOTE: the noAutoRefresh attribute should be added to the <a/> or <form/> tags.
|
|
-noAutoRefresh= [("class","_noAutoRefresh")]
|
|
-
|
|
--- | does the same than `autoRefresh` but append the result of each request to the bottom of the widget
|
|
---
|
|
--- all the comments and remarks of `autoRefresh` apply here
|
|
-appendUpdate :: (MonadIO m,
|
|
- FormInput v)
|
|
- => View v m a
|
|
- -> View v m a
|
|
-appendUpdate= update "append"
|
|
-
|
|
--- | does the same than `autoRefresh` but prepend the result of each request before the current widget content
|
|
---
|
|
--- all the comments and remarks of `autoRefresh` apply here
|
|
-prependUpdate :: (MonadIO m,
|
|
- FormInput v)
|
|
- => View v m a
|
|
- -> View v m a
|
|
-prependUpdate= update "prepend"
|
|
-
|
|
-update :: (MonadIO m, FormInput v)
|
|
- => B.ByteString
|
|
- -> View v m a
|
|
- -> View v m a
|
|
-update method w= do
|
|
- id <- genNewId
|
|
+template
|
|
+ :: (MonadIO m, FormInput v, Typeable a) =>
|
|
+ Key -> View v m a -> View v m a
|
|
+template k w= View $ do
|
|
+ FormElm text mx <- runView w
|
|
+ let content= unsafePerformIO $ readtField text k
|
|
+ return $ FormElm content mx
|
|
+
|
|
+
|
|
+
|
|
+------------------- JQuery widgets -------------------
|
|
+-- | present the JQuery datepicker calendar to choose a date.
|
|
+-- The second parameter is the configuration. Use \"()\" by default.
|
|
+-- See http://jqueryui.com/datepicker/
|
|
+datePicker :: (Monad m, FormInput v) => String -> Maybe String -> View v m (Int,Int,Int)
|
|
+datePicker conf jd= do
|
|
+ id <- genNewId
|
|
+ let setit= "$(document).ready(function() {\
|
|
+ \$( '#"++id++"' ).datepicker "++ conf ++";\
|
|
+ \});"
|
|
+
|
|
+ requires
|
|
+ [CSSFile jqueryCSS
|
|
+ ,JScriptFile jqueryScript []
|
|
+ ,JScriptFile jqueryUI [setit]]
|
|
+
|
|
+ s <- getString jd <! [("id",id)]
|
|
+ let (month,r) = span (/='/') s
|
|
+ let (day,r2)= span(/='/') $ tail r
|
|
+ return (read day,read month, read $ tail r2)
|
|
+
|
|
+-- | present a jQuery dialog with a widget. When a button is pressed it return the result.
|
|
+-- The first parameter is the configuration. To make it modal, use \"({modal: true})\" see <http://jqueryui.com/dialog/> for
|
|
+-- the available configurations.
|
|
+--
|
|
+-- The enclosed widget will be wrapped within a form tag if the user do not encloses it using wform.f
|
|
+wdialog :: (Monad m, FormInput v) => String -> String -> View v m a -> View v m a
|
|
+wdialog conf title w= do
|
|
+ id <- genNewId
|
|
+ let setit= "$(document).ready(function() {\
|
|
+ \$('#"++id++"').dialog "++ conf ++";\
|
|
+ \var idform= $('#"++id++" form');\
|
|
+ \idform.submit(function(){$(this).dialog(\"close\")})\
|
|
+ \});"
|
|
+
|
|
+ modify $ \st -> st{needForm= HasForm}
|
|
+ requires
|
|
+ [CSSFile jqueryCSS
|
|
+ ,JScriptFile jqueryScript []
|
|
+ ,JScriptFile jqueryUI [setit]]
|
|
+
|
|
+ (ftag "div" <<< insertForm w) <! [("id",id),("title", title)]
|
|
+
|
|
+
|
|
+
|
|
+
|
|
+
|
|
+-- | Capture the form or link submissions and send them via jQuery AJAX.
|
|
+-- The response is the new presentation of the widget, that is updated. No new page is generated
|
|
+-- but the functionality is equivalent. Only the activated widget rendering is updated
|
|
+-- in the client, so a widget with autoRefresh can be used in heavyweight pages.
|
|
+-- If AJAX/JavaScript are not available, the widget is refreshed normally, via a new page.
|
|
+--
|
|
+-- autoRefresh encloses the widget in a form tag if it includes form elements.
|
|
+--
|
|
+-- If there are more than one autoRefresh, they must be enclosed within 'pageFlow' elements
|
|
+autoRefresh
|
|
+ :: (MonadIO m,
|
|
+ FormInput v)
|
|
+ => View v m a
|
|
+ -> View v m a
|
|
+autoRefresh = update "html"
|
|
+
|
|
+-- | In some cases, it is neccessary that a link or form inside a 'autoRefresh' or 'update' block
|
|
+-- should not be autorefreshed, since it produces side effects in the rest of the page that
|
|
+-- affect to the rendering of the whole. If you like to refresh the whole page, simply add
|
|
+-- noAutoRefresh attribute to the widget to force the refresh of the whole page when it is activated.
|
|
+--
|
|
+-- That behaviour is common at the last sentence of the 'autoRefresh' block.
|
|
+--
|
|
+-- This is a cascade menu example.
|
|
+--
|
|
+-- > r <- page $ autoRefresh $ ul <<< do
|
|
+-- > li <<< wlink OptionA << "option A"
|
|
+-- > ul <<< li <<< (wlink OptionA1 << "Option A1" <! noAutoRefresh)
|
|
+-- > <|> li <<< (wlink OptionA2 << "Option A2" <! noAutoRefresh)
|
|
+-- > <|>...
|
|
+-- > maybe other content
|
|
+-- >
|
|
+-- > case r of
|
|
+-- > OptionA1 -> pageA1
|
|
+-- > OptionA2 -> pageA2
|
|
+--
|
|
+-- when @option A@ is clicked, the two sub-options appear with autorefresh. Only the two
|
|
+-- lines are returned by the server using AJAX. but when Option A1-2 is pressed we want to
|
|
+-- present other pages, so we add the noAutorefresh attribute.
|
|
+--
|
|
+-- NOTE: the noAutoRefresh attribute should be added to the <a/> or <form/> tags.
|
|
+noAutoRefresh= [("class","_noAutoRefresh")]
|
|
+
|
|
+-- | does the same than `autoRefresh` but append the result of each request to the bottom of the widget
|
|
+--
|
|
+-- all the comments and remarks of `autoRefresh` apply here
|
|
+appendUpdate :: (MonadIO m,
|
|
+ FormInput v)
|
|
+ => View v m a
|
|
+ -> View v m a
|
|
+appendUpdate= update "append"
|
|
+
|
|
+-- | does the same than `autoRefresh` but prepend the result of each request before the current widget content
|
|
+--
|
|
+-- all the comments and remarks of `autoRefresh` apply here
|
|
+prependUpdate :: (MonadIO m,
|
|
+ FormInput v)
|
|
+ => View v m a
|
|
+ -> View v m a
|
|
+prependUpdate= update "prepend"
|
|
+
|
|
+update :: (MonadIO m, FormInput v)
|
|
+ => String
|
|
+ -> View v m a
|
|
+ -> View v m a
|
|
+update method w= do
|
|
+ id <- genNewId
|
|
st <- get
|
|
-
|
|
- let t = mfkillTime st -1
|
|
-
|
|
- installscript=
|
|
- "$(document).ready(function(){\
|
|
- \ajaxGetLink('"++id++"');\
|
|
- \ajaxPostForm('"++id++"');\
|
|
+
|
|
+ let t = mfkillTime st -1
|
|
+
|
|
+ installscript=
|
|
+ "$(document).ready(function(){\
|
|
+ \ajaxGetLink('"++id++"');\
|
|
+ \ajaxPostForm('"++id++"');\
|
|
\});"
|
|
st <- get
|
|
let insync = inSync st
|
|
- let env= mfEnv st
|
|
- let r= lookup ("auto"++id) env
|
|
+ let env= mfEnv st
|
|
+ let r= lookup ("auto"++id) env
|
|
if r == Nothing
|
|
- then do
|
|
- requires [JScript $ timeoutscript t
|
|
- ,JScript ajaxGetLink
|
|
- ,JScript ajaxPostForm
|
|
- ,JScriptFile jqueryScript [installscript]]
|
|
- (ftag "div" <<< insertForm w) <! [("id",id)]
|
|
-
|
|
- else View $ do
|
|
- let t= mfToken st -- !> "JUST"
|
|
- modify $ \s -> s{mfHttpHeaders=[]} -- !> "just"
|
|
- resetCachePolicy
|
|
- FormElm form mr <- runView $ insertForm w
|
|
- setCachePolicy
|
|
- st' <- get
|
|
- let HttpData ctype c s= toHttpData $ method <> " " <> toByteString form
|
|
-
|
|
- (liftIO . sendFlush t $ HttpData (ctype ++
|
|
- mfHttpHeaders st') (mfCookies st' ++ c) s)
|
|
- put st'{mfAutorefresh=True,newAsk=True}
|
|
-
|
|
- return $ FormElm mempty Nothing
|
|
-
|
|
- where
|
|
- -- | adapted from http://www.codeproject.com/Articles/341151/Simple-AJAX-POST-Form-and-AJAX-Fetch-Link-to-Modal
|
|
+ then do
|
|
+ requires [JScript $ timeoutscript t
|
|
+ ,JScript ajaxGetLink
|
|
+ ,JScript ajaxPostForm
|
|
+ ,JScriptFile jqueryScript [installscript]]
|
|
+ (ftag "div" <<< insertForm w) <! [("id",id)]
|
|
+
|
|
+ else refresh $ fromStr (method <> " ") ++> insertForm w
|
|
+-- View $ do
|
|
+-- let t= mfToken st -- !> "JUST"
|
|
+-- modify $ \s -> s{mfHttpHeaders=[]} -- !> "just"
|
|
+-- resetCachePolicy
|
|
+-- FormElm form mr <- runView $ insertForm w
|
|
+-- setCachePolicy
|
|
+-- st' <- get
|
|
+-- let HttpData ctype c s= toHttpData $ method <> " " <> toByteString form
|
|
+--
|
|
+-- (liftIO . sendFlush t $ HttpData (ctype ++
|
|
+-- mfHttpHeaders st') (mfCookies st' ++ c) s)
|
|
+-- put st'{mfAutorefresh=True,newAsk=True}
|
|
+--
|
|
+-- return $ FormElm mempty Nothing
|
|
+
|
|
+ where
|
|
+ -- | adapted from http://www.codeproject.com/Articles/341151/Simple-AJAX-POST-Form-and-AJAX-Fetch-Link-to-Modal
|
|
-- \url: actionurl+'?bustcache='+ new Date().getTime()+'&auto'+id+'=true',\n\
|
|
ajaxGetLink = "\nfunction ajaxGetLink(id){\
|
|
- \var id1= $('#'+id);\
|
|
+ \var id1= $('#'+id);\
|
|
\var ida= $('#'+id+' a[class!=\"_noAutoRefresh\"]');\
|
|
- \ida.off('click');\
|
|
- \ida.click(function () {\
|
|
- \if (hadtimeout == true) return true;\
|
|
- \var pdata = $(this).attr('data-value');\
|
|
- \var actionurl = $(this).attr('href');\
|
|
- \var dialogOpts = {\
|
|
+ \ida.off('click');\
|
|
+ \ida.click(function () {\
|
|
+ \if (hadtimeout == true) return true;\
|
|
+ \var pdata = $(this).attr('data-value');\
|
|
+ \var actionurl = $(this).attr('href');\
|
|
+ \var dialogOpts = {\
|
|
\type: 'GET',\
|
|
- \url: actionurl+'?auto'+id+'=true',\
|
|
- \data: pdata,\
|
|
- \success: function (resp) {\
|
|
- \var ind= resp.indexOf(' ');\
|
|
- \var dat= resp.substr(ind);\
|
|
- \var method= resp.substr(0,ind);\
|
|
- \if(method== 'html')id1.html(dat);\
|
|
- \else if (method == 'append') id1.append(dat);\
|
|
+ \url: actionurl+'?auto'+id+'=true',\
|
|
+ \data: pdata,\
|
|
+ \success: function (resp) {\
|
|
+ \var ind= resp.indexOf(' ');\
|
|
+ \var dat= resp.substr(ind);\
|
|
+ \var method= resp.substr(0,ind);\
|
|
+ \if(method== 'html')id1.html(dat);\
|
|
+ \else if (method == 'append') id1.append(dat);\
|
|
\else if (method == 'prepend') id1.prepend(dat);\
|
|
- \else $(':root').html(resp);\
|
|
+ \else $(':root').html(resp);\
|
|
\ajaxGetLink(id);\
|
|
- \ajaxPostForm(id);\
|
|
- \},\
|
|
- \error: function (xhr, status, error) {\
|
|
- \var msg = $('<div>' + xhr + '</div>');\
|
|
- \id1.html(msg);\
|
|
- \}\
|
|
- \};\
|
|
- \$.ajax(dialogOpts);\
|
|
- \return false;\
|
|
- \});\
|
|
- \}\n"
|
|
+ \ajaxPostForm(id);\
|
|
+ \},\
|
|
+ \error: function (xhr, status, error) {\
|
|
+ \var msg = $('<div>' + xhr + '</div>');\
|
|
+ \id1.html(msg);\
|
|
+ \}\
|
|
+ \};\
|
|
+ \$.ajax(dialogOpts);\
|
|
+ \return false;\
|
|
+ \});\
|
|
+ \}\n"
|
|
|
|
ajaxPostForm = "\nfunction ajaxPostForm(id) {\
|
|
\var buttons= $('#'+id+' input[type=\"submit\"]');\
|
|
@@ -1051,13 +1055,13 @@
|
|
\buttons.click(function(event) {\
|
|
\if ($(this).attr('class') != '_noAutoRefresh'){\
|
|
\event.preventDefault();\
|
|
- \if (hadtimeout == true) return true;\
|
|
- \var $form = $(this).closest('form');\
|
|
+ \if (hadtimeout == true) return true;\
|
|
+ \var $form = $(this).closest('form');\
|
|
\var url = $form.attr('action');\
|
|
\pdata = 'auto'+id+'=true&'+this.name+'='+this.value+'&'+$form.serialize();\
|
|
\postForm(id,url,pdata);\
|
|
\return false;\
|
|
- \}else {\
|
|
+ \}else {\
|
|
\noajax= true;\
|
|
\return true;\
|
|
\}\
|
|
@@ -1067,197 +1071,197 @@
|
|
\idform.submit(function(event) {\
|
|
\if(noajax) {noajax=false; return true;}\
|
|
\event.preventDefault();\
|
|
- \var $form = $(this);\
|
|
+ \var $form = $(this);\
|
|
\var url = $form.attr('action');\
|
|
\var pdata = 'auto'+id+'=true&' + $form.serialize();\
|
|
\postForm(id,url,pdata);\
|
|
\return false;})\
|
|
\}\
|
|
\function postForm(id,url,pdata){\
|
|
- \var id1= $('#'+id);\
|
|
- \$.ajax({\
|
|
- \type: 'POST',\
|
|
- \url: url,\
|
|
- \data: pdata,\
|
|
- \success: function (resp) {\
|
|
- \var ind= resp.indexOf(' ');\
|
|
- \var dat = resp.substr(ind);\
|
|
- \var method= resp.substr(0,ind);\
|
|
- \if(method== 'html')id1.html(dat);\
|
|
- \else if (method == 'append') id1.append(dat);\
|
|
+ \var id1= $('#'+id);\
|
|
+ \$.ajax({\
|
|
+ \type: 'POST',\
|
|
+ \url: url,\
|
|
+ \data: pdata,\
|
|
+ \success: function (resp) {\
|
|
+ \var ind= resp.indexOf(' ');\
|
|
+ \var dat = resp.substr(ind);\
|
|
+ \var method= resp.substr(0,ind);\
|
|
+ \if(method== 'html')id1.html(dat);\
|
|
+ \else if (method == 'append') id1.append(dat);\
|
|
\else if (method == 'prepend') id1.prepend(dat);\
|
|
\else $(':root').html(resp);\
|
|
- \ajaxGetLink(id);\
|
|
- \ajaxPostForm(id);\
|
|
- \},\
|
|
- \error: function (xhr, status, error) {\
|
|
- \var msg = $('<div>' + xhr + '</div>');\
|
|
- \id1.html(msg);\
|
|
- \}\
|
|
- \});\
|
|
- \};"
|
|
-
|
|
-
|
|
-
|
|
-
|
|
-timeoutscript t=
|
|
- "\nvar hadtimeout=false;\
|
|
- \if("++show t++" > 0)setTimeout(function() {hadtimeout=true; }, "++show (t*1000)++");\n"
|
|
-
|
|
-
|
|
-data UpdateMethod= Append | Prepend | Html deriving Show
|
|
-
|
|
--- | continously execute a widget and update the content.
|
|
--- The update method specify how the update is done. 'Html' means a substitution of content.
|
|
--- The second parameter is the delay for the next retry in case of disconnection, in milliseconds.
|
|
---
|
|
--- It can be used to show data updates in the server. The widget is executed in a different process than
|
|
--- the one of the rest of the page.
|
|
--- Updates in the session context are not seen by the push widget. It has his own context.
|
|
--- To communicate with te widget, use DBRef's or TVar and the
|
|
--- STM semantics for waiting updates using 'retry'.
|
|
---
|
|
--- Widgets in a push can have links and forms, but since they are asunchonous, they can not
|
|
--- return inputs. but they can modify the server state.
|
|
--- push ever return an invalid response to the calling widget, so it never
|
|
--- triggers the advance of the navigation.
|
|
---
|
|
---
|
|
--- This example is a counter increased each second:
|
|
---
|
|
--- > pushIncrease= do
|
|
--- > tv <- liftIO $ newTVarIO 0
|
|
--- > page $ push 0 Html $ do
|
|
--- > n <- atomic $ readTVar tv
|
|
--- > atomic $ writeTVar tv $ n + 1
|
|
--- > liftIO $ threadDelay 1000000
|
|
--- > b << (show n) ++> noWidget
|
|
---
|
|
---
|
|
--- This other simulates a console output that echoes what is entered in a text box
|
|
--- below. It has two widgets: a push output in append mode and a text box input.
|
|
--- The communication uses a TVar. The push widget wait for updates in the TVar.
|
|
--- because the second widget uses autoRefresh, all happens in the same page.
|
|
---
|
|
--- It is recommended to add a timeout to the push widget, like in the example:
|
|
---
|
|
--- > pushSample= do
|
|
--- > tv <- liftIO $ newTVarIO $ Just "init"
|
|
--- > page $ push Append 1000 (disp tv) <** input tv
|
|
--- >
|
|
--- > where
|
|
--- > disp tv= do
|
|
--- > setTimeouts 100 0
|
|
--- > line <- tget tv
|
|
--- > p << line ++> noWidget
|
|
--- >
|
|
--- > input tv= autoRefresh $ do
|
|
--- > line <- getString Nothing <** submitButton "Enter"
|
|
--- > tput tv line
|
|
--- >
|
|
--- > tput tv x = atomic $ writeTVar tv ( Just x) !> "WRITE"
|
|
--- >
|
|
--- > tget tv= atomic $ do
|
|
--- > mr <- readTVar tv
|
|
--- > case mr of
|
|
--- > Nothing -> retry
|
|
--- > Just r -> do
|
|
--- > writeTVar tv Nothing
|
|
--- > return r
|
|
-
|
|
-push :: FormInput v
|
|
- => UpdateMethod
|
|
- -> Int
|
|
- -> View v IO ()
|
|
- -> View v IO ()
|
|
-push method' wait w= push' . map toLower $ show method'
|
|
- where
|
|
- push' method= do
|
|
- id <- genNewId
|
|
- st <- get
|
|
- let token= mfToken st
|
|
-
|
|
- procname= "_push" ++ tind token ++ id
|
|
- installscript=
|
|
- "$(document).ready(function(){\n"
|
|
- ++ "ajaxPush('"++id++"',"++show wait++");"
|
|
- ++ "})\n"
|
|
-
|
|
- new <- gets newAsk
|
|
-
|
|
- when new $ do
|
|
- killWF procname token{twfname= procname}
|
|
- let proc=runFlow . transientNav . ask $ w'
|
|
- requires [ServerProc (procname, proc),
|
|
- JScript $ ajaxPush procname,
|
|
- JScriptFile jqueryScript [installscript]]
|
|
-
|
|
- (ftag "div" <<< noWidget) <! [("id",id)]
|
|
- <++ ftag "div" mempty `attrs` [("id",id++"status")]
|
|
-
|
|
- where
|
|
- w' = do
|
|
- modify $ \s -> s{inSync= True,newAsk=True}
|
|
- w
|
|
-
|
|
-
|
|
-
|
|
- ajaxPush procname=" function ajaxPush(id,waititime){\
|
|
- \var cnt=0; \
|
|
- \var id1= $('#'+id);\
|
|
- \var idstatus= $('#'+id+'status');\
|
|
- \var ida= $('#'+id+' a');\
|
|
- \var actionurl='/"++procname++"';\
|
|
- \var dialogOpts = {\
|
|
- \cache: false,\
|
|
- \type: 'GET',\
|
|
- \url: actionurl,\
|
|
- \data: '',\
|
|
- \success: function (resp) {\
|
|
- \idstatus.html('')\
|
|
- \cnt=0;\
|
|
- \id1."++method++"(resp);\
|
|
- \ajaxPush1();\
|
|
- \},\
|
|
- \error: function (xhr, status, error) {\
|
|
- \cnt= cnt + 1;\
|
|
- \if (false) \
|
|
- \idstatus.html('no more retries');\
|
|
- \else {\
|
|
- \idstatus.html('waiting');\
|
|
- \setTimeout(function() { idstatus.html('retrying');ajaxPush1(); }, waititime);\
|
|
- \}\
|
|
- \}\
|
|
- \};\
|
|
- \function ajaxPush1(){\
|
|
- \$.ajax(dialogOpts);\
|
|
- \return false;\
|
|
- \}\
|
|
- \ajaxPush1();\
|
|
- \}"
|
|
-
|
|
-
|
|
-
|
|
-
|
|
--- | show the jQuery spinner widget. the first parameter is the configuration . Use \"()\" by default.
|
|
--- See http://jqueryui.com/spinner
|
|
-getSpinner
|
|
- :: (MonadIO m, Read a,Show a, Typeable a, FormInput view) =>
|
|
- String -> Maybe a -> View view m a
|
|
-getSpinner conf mv= do
|
|
- id <- genNewId
|
|
- let setit= "$(document).ready(function() {\
|
|
- \var spinner = $( '#"++id++"' ).spinner "++conf++";\
|
|
- \spinner.spinner( \"enable\" );\
|
|
- \});"
|
|
- requires
|
|
- [CSSFile jqueryCSS
|
|
- ,JScriptFile jqueryScript []
|
|
- ,JScriptFile jqueryUI [setit]]
|
|
-
|
|
- getTextBox mv <! [("id",id)]
|
|
+ \ajaxGetLink(id);\
|
|
+ \ajaxPostForm(id);\
|
|
+ \},\
|
|
+ \error: function (xhr, status, error) {\
|
|
+ \var msg = $('<div>' + xhr + '</div>');\
|
|
+ \id1.html(msg);\
|
|
+ \}\
|
|
+ \});\
|
|
+ \};"
|
|
+
|
|
+
|
|
+
|
|
+
|
|
+timeoutscript t=
|
|
+ "\nvar hadtimeout=false;\
|
|
+ \if("++show t++" > 0)setTimeout(function() {hadtimeout=true; }, "++show (t*1000)++");\n"
|
|
+
|
|
+
|
|
+data UpdateMethod= Append | Prepend | Html deriving Show
|
|
+
|
|
+-- | continously execute a widget and update the content.
|
|
+-- The update method specify how the update is done. 'Html' means a substitution of content.
|
|
+-- The second parameter is the delay for the next retry in case of disconnection, in milliseconds.
|
|
+--
|
|
+-- It can be used to show data updates in the server. The widget is executed in a different process than
|
|
+-- the one of the rest of the page.
|
|
+-- Updates in the session context are not seen by the push widget. It has his own context.
|
|
+-- To communicate with te widget, use DBRef's or TVar and the
|
|
+-- STM semantics for waiting updates using 'retry'.
|
|
+--
|
|
+-- Widgets in a push can have links and forms, but since they are asunchonous, they can not
|
|
+-- return inputs. but they can modify the server state.
|
|
+-- push ever return an invalid response to the calling widget, so it never
|
|
+-- triggers the advance of the navigation.
|
|
+--
|
|
+--
|
|
+-- This example is a counter increased each second:
|
|
+--
|
|
+-- > pushIncrease= do
|
|
+-- > tv <- liftIO $ newTVarIO 0
|
|
+-- > page $ push 0 Html $ do
|
|
+-- > n <- atomic $ readTVar tv
|
|
+-- > atomic $ writeTVar tv $ n + 1
|
|
+-- > liftIO $ threadDelay 1000000
|
|
+-- > b << (show n) ++> noWidget
|
|
+--
|
|
+--
|
|
+-- This other simulates a console output that echoes what is entered in a text box
|
|
+-- below. It has two widgets: a push output in append mode and a text box input.
|
|
+-- The communication uses a TVar. The push widget wait for updates in the TVar.
|
|
+-- because the second widget uses autoRefresh, all happens in the same page.
|
|
+--
|
|
+-- It is recommended to add a timeout to the push widget, like in the example:
|
|
+--
|
|
+-- > pushSample= do
|
|
+-- > tv <- liftIO $ newTVarIO $ Just "init"
|
|
+-- > page $ push Append 1000 (disp tv) <** input tv
|
|
+-- >
|
|
+-- > where
|
|
+-- > disp tv= do
|
|
+-- > setTimeouts 100 0
|
|
+-- > line <- tget tv
|
|
+-- > p << line ++> noWidget
|
|
+-- >
|
|
+-- > input tv= autoRefresh $ do
|
|
+-- > line <- getString Nothing <** submitButton "Enter"
|
|
+-- > tput tv line
|
|
+-- >
|
|
+-- > tput tv x = atomic $ writeTVar tv ( Just x) !> "WRITE"
|
|
+-- >
|
|
+-- > tget tv= atomic $ do
|
|
+-- > mr <- readTVar tv
|
|
+-- > case mr of
|
|
+-- > Nothing -> retry
|
|
+-- > Just r -> do
|
|
+-- > writeTVar tv Nothing
|
|
+-- > return r
|
|
+
|
|
+push :: FormInput v
|
|
+ => UpdateMethod
|
|
+ -> Int
|
|
+ -> View v IO ()
|
|
+ -> View v IO ()
|
|
+push method' wait w= push' . map toLower $ show method'
|
|
+ where
|
|
+ push' method= do
|
|
+ id <- genNewId
|
|
+ st <- get
|
|
+ let token= mfToken st
|
|
+
|
|
+ procname= "_push" ++ tind token ++ id
|
|
+ installscript=
|
|
+ "$(document).ready(function(){\n"
|
|
+ ++ "ajaxPush('"++id++"',"++show wait++");"
|
|
+ ++ "})\n"
|
|
+
|
|
+ new <- gets newAsk
|
|
+
|
|
+ when new $ do
|
|
+ killWF procname token{twfname= procname}
|
|
+ let proc=runFlow . transientNav . ask $ w'
|
|
+ requires [ServerProc (procname, proc),
|
|
+ JScript $ ajaxPush procname,
|
|
+ JScriptFile jqueryScript [installscript]]
|
|
+
|
|
+ (ftag "div" <<< noWidget) <! [("id",id)]
|
|
+ <++ ftag "div" mempty `attrs` [("id",id++"status")]
|
|
+
|
|
+ where
|
|
+ w' = do
|
|
+ modify $ \s -> s{inSync= True,newAsk=True}
|
|
+ w
|
|
+
|
|
+
|
|
+
|
|
+ ajaxPush procname=" function ajaxPush(id,waititime){\
|
|
+ \var cnt=0; \
|
|
+ \var id1= $('#'+id);\
|
|
+ \var idstatus= $('#'+id+'status');\
|
|
+ \var ida= $('#'+id+' a');\
|
|
+ \var actionurl='/"++procname++"';\
|
|
+ \var dialogOpts = {\
|
|
+ \cache: false,\
|
|
+ \type: 'GET',\
|
|
+ \url: actionurl,\
|
|
+ \data: '',\
|
|
+ \success: function (resp) {\
|
|
+ \idstatus.html('');\
|
|
+ \cnt=0;\
|
|
+ \id1."++method++"(resp);\
|
|
+ \ajaxPush1();\
|
|
+ \},\
|
|
+ \error: function (xhr, status, error) {\
|
|
+ \cnt= cnt + 1;\
|
|
+ \if (false) \
|
|
+ \idstatus.html('no more retries');\
|
|
+ \else {\
|
|
+ \idstatus.html('waiting');\
|
|
+ \setTimeout(function() { idstatus.html('retrying');ajaxPush1(); }, waititime);\
|
|
+ \}\
|
|
+ \}\
|
|
+ \};\
|
|
+ \function ajaxPush1(){\
|
|
+ \$.ajax(dialogOpts);\
|
|
+ \return false;\
|
|
+ \}\
|
|
+ \ajaxPush1();\
|
|
+ \}"
|
|
+
|
|
+
|
|
+
|
|
+
|
|
+-- | show the jQuery spinner widget. the first parameter is the configuration . Use \"()\" by default.
|
|
+-- See http://jqueryui.com/spinner
|
|
+getSpinner
|
|
+ :: (MonadIO m, Read a,Show a, Typeable a, FormInput view) =>
|
|
+ String -> Maybe a -> View view m a
|
|
+getSpinner conf mv= do
|
|
+ id <- genNewId
|
|
+ let setit= "$(document).ready(function() {\
|
|
+ \var spinner = $( '#"++id++"' ).spinner "++conf++";\
|
|
+ \spinner.spinner( \"enable\" );\
|
|
+ \});"
|
|
+ requires
|
|
+ [CSSFile jqueryCSS
|
|
+ ,JScriptFile jqueryScript []
|
|
+ ,JScriptFile jqueryUI [setit]]
|
|
+
|
|
+ getTextBox mv <! [("id",id)]
|
|
+
|
|
|
|
-
|
|
|
|
-- | takes as argument a widget and delay the load until it is visible. The renderring to
|
|
-- be shown during the load is the specified in the first parameter. The resulting lazy
|
|
@@ -1277,50 +1281,31 @@
|
|
--
|
|
-- lazy temprendering $ img ! href imageurl ++> noWidget
|
|
lazy :: (FormInput v,Functor m,MonadIO m) => v -> View v m a -> View v m a
|
|
-lazy v w= do
|
|
- id <- genNewId
|
|
+lazy v w= do
|
|
+ id <- genNewId
|
|
st <- get
|
|
let path = currentPath st
|
|
- env = mfEnv st
|
|
- r= lookup ("auto"++id) env
|
|
- t = mfkillTime st -1
|
|
+ env = mfEnv st
|
|
+ r= lookup ("auto"++id) env
|
|
+ t = mfkillTime st -1
|
|
installscript = "$(document).ready(function(){\
|
|
\function lazyexec(){lazy('"++id++"','"++ path ++"',lazyexec)};\
|
|
\$(window).one('scroll',lazyexec);\
|
|
\$(window).trigger('scroll');\
|
|
\});"
|
|
|
|
--- installscript2= "$(window).one('scroll',function(){\
|
|
--- \function lazyexec(){lazy('"++id++"','"++ path ++"',lazyexec)};\
|
|
--- \lazyexec()});"
|
|
-
|
|
-
|
|
- if r == Nothing then View $ do
|
|
- requires [JScript lazyScript
|
|
+ if r == Nothing then View $ do
|
|
+ requires [JScript lazyScript
|
|
,JScriptFile jqueryScript [installscript,scrollposition]]
|
|
- FormElm rendering mx <- runView w
|
|
+ reqs <- gets mfRequirements
|
|
+ FormElm _ mx <- runView w
|
|
+ modify $ \st-> st{mfRequirements= reqs} --ignore requirements
|
|
return $ FormElm (ftag "div" v `attrs` [("id",id)]) mx
|
|
-
|
|
- else View $ do
|
|
- resetCachePolicy
|
|
- st' <- get
|
|
- FormElm form mx <- runView w
|
|
- setCachePolicy
|
|
- let t= mfToken st'
|
|
- reqs <- installAllRequirements
|
|
- let HttpData ctype c s= toHttpData $ toByteString form
|
|
- liftIO . sendFlush t $ HttpData (ctype ++
|
|
- mfHttpHeaders st') (mfCookies st' ++ c)
|
|
- $ toByteString reqs <> s -- !> (unpack $ toByteString reqs)
|
|
- put st'{mfAutorefresh=True,inSync= True}
|
|
-
|
|
- return $ FormElm mempty mx
|
|
-
|
|
+
|
|
+ else refresh w
|
|
|
|
where
|
|
|
|
-
|
|
-
|
|
scrollposition= "$.fn.scrollposition= function(){\
|
|
\var pos= $(this).position();\
|
|
\if (typeof(pos)==='undefined') {return 1;}\
|
|
@@ -1339,24 +1324,37 @@
|
|
\lastCall = now;\
|
|
\if(id1.scrollposition() > 0){\
|
|
\$(window).one('scroll',f);}\
|
|
- \else{\
|
|
- \var dialogOpts = {\
|
|
+ \else{\
|
|
+ \var dialogOpts = {\
|
|
\type: 'GET',\
|
|
- \url: actionurl+'?auto'+id+'=true',\
|
|
- \success: function (resp) {\
|
|
+ \url: actionurl+'?auto'+id+'=true',\
|
|
+ \success: function (resp) {\
|
|
\id1.html(resp);\
|
|
- \$(window).trigger('scroll');\
|
|
+ \$(window).trigger('scroll');\
|
|
\},\
|
|
- \error: function (xhr, status, error) {\
|
|
- \var msg = $('<div>' + xhr + '</div>');\
|
|
+ \error: function (xhr, status, error) {\
|
|
+ \var msg = $('<div>' + xhr + '</div>');\
|
|
\id1.html(msg);\
|
|
- \}\
|
|
- \};\
|
|
- \$.ajax(dialogOpts);\
|
|
+ \}\
|
|
+ \};\
|
|
+ \$.ajax(dialogOpts);\
|
|
\}}};"
|
|
|
|
+refresh w= View $ do
|
|
+ resetCachePolicy
|
|
+ modify $ \st -> st{mfAutorefresh=True,inSync= True}
|
|
+ FormElm form mx <- runView w -- !> show (mfInstalledScripts st')
|
|
+ setCachePolicy
|
|
+ st' <- get
|
|
+ let t= mfToken st'
|
|
+ reqs <- installAllRequirements
|
|
+ let HttpData ctype c s= toHttpData $ toByteString form
|
|
+ liftIO . sendFlush t $ HttpData (ctype ++
|
|
+ mfHttpHeaders st') (mfCookies st' ++ c)
|
|
+ $ s <> toByteString reqs
|
|
+ return $ FormElm mempty mx
|
|
|
|
-waitAndExecute= "function waitAndExecute(sym,f) {\
|
|
- \if (eval(sym)) {f();}\
|
|
- \else {setTimeout(function() {waitAndExecute(sym,f)}, 50);}\
|
|
- \}\n"
|
|
+--waitAndExecute= "function waitAndExecute(sym,f) {\
|
|
+-- \if (eval(sym)) {f();}\
|
|
+-- \else {setTimeout(function() {waitAndExecute(sym,f)}, 50);}\
|
|
+-- \}\n"
|
|
diff -ru orig/src/MFlow/Forms/XHtml.hs new/src/MFlow/Forms/XHtml.hs
|
|
--- orig/src/MFlow/Forms/XHtml.hs 2014-06-10 05:51:26.973015856 +0300
|
|
+++ new/src/MFlow/Forms/XHtml.hs 2014-06-10 05:51:25.000000000 +0300
|
|
@@ -1,66 +1,66 @@
|
|
------------------------------------------------------------------------------
|
|
---
|
|
--- Module : Control.MessageFlow.Forms.XHtml
|
|
--- Copyright : Alberto Gónez Corona
|
|
--- License : BSD3
|
|
---
|
|
--- Maintainer : agocorona@gmail.com
|
|
--- Stability : experimental
|
|
---
|
|
------------------------------------------------------------------------------
|
|
-{- | Instances of `FormInput` for the 'Text.XHtml' module of the xhtml package
|
|
--}
|
|
-
|
|
-{-# OPTIONS -XMultiParamTypeClasses
|
|
- -XFlexibleInstances
|
|
- -XUndecidableInstances
|
|
- -XTypeSynonymInstances
|
|
- -XFlexibleContexts
|
|
- -XTypeOperators
|
|
- #-}
|
|
-
|
|
-
|
|
-module MFlow.Forms.XHtml where
|
|
-
|
|
-import MFlow (HttpData(..))
|
|
-import MFlow.Forms
|
|
-import MFlow.Cookies(contentHtml)
|
|
-import Data.ByteString.Lazy.Char8(pack,unpack)
|
|
-import qualified Data.Text as T
|
|
-import Text.XHtml.Strict as X
|
|
-import Control.Monad.Trans
|
|
-import Data.Typeable
|
|
-
|
|
-instance Monad m => ADDATTRS (View Html m a) where
|
|
- widget ! atrs= widget `wmodify` \fs mx -> return ((head fs ! atrs:tail fs), mx)
|
|
-
|
|
-
|
|
-
|
|
-instance FormInput Html where
|
|
- toByteString = pack. showHtmlFragment
|
|
- toHttpData = HttpData [contentHtml] [] . toByteString
|
|
- ftag t= tag t
|
|
- inred = X.bold ![X.thestyle "color:red"]
|
|
- finput n t v f c= X.input ! ([thetype t ,name n, value v] ++ if f then [checked] else []
|
|
- ++ case c of Just s ->[strAttr "onclick" s]; _ -> [] )
|
|
- ftextarea name text= X.textarea ! [X.name name] << T.unpack text
|
|
-
|
|
- fselect name list = select ![ X.name name] << list
|
|
- foption name v msel= X.option ! ([value name] ++ selected msel) << v
|
|
- where
|
|
- selected msel = if msel then [X.selected] else []
|
|
-
|
|
- attrs tag attrs = tag ! (map (\(n,v) -> strAttr n v) attrs)
|
|
-
|
|
-
|
|
-
|
|
- formAction action form = X.form ! [X.action action, method "post"] << form
|
|
- fromStr = stringToHtml
|
|
- fromStrNoEncode= primHtml
|
|
-
|
|
- flink v str = toHtml $ hotlink ( v) << str
|
|
-
|
|
-instance Typeable Html where
|
|
- typeOf = \_ -> mkTyConApp (mkTyCon3 "xhtml" "Text.XHtml.Strict" "Html") []
|
|
-
|
|
-
|
|
+-----------------------------------------------------------------------------
|
|
+--
|
|
+-- Module : Control.MessageFlow.Forms.XHtml
|
|
+-- Copyright : Alberto Gónez Corona
|
|
+-- License : BSD3
|
|
+--
|
|
+-- Maintainer : agocorona@gmail.com
|
|
+-- Stability : experimental
|
|
+--
|
|
+-----------------------------------------------------------------------------
|
|
+{- | Instances of `FormInput` for the 'Text.XHtml' module of the xhtml package
|
|
+-}
|
|
+
|
|
+{-# OPTIONS -XMultiParamTypeClasses
|
|
+ -XFlexibleInstances
|
|
+ -XUndecidableInstances
|
|
+ -XTypeSynonymInstances
|
|
+ -XFlexibleContexts
|
|
+ -XTypeOperators
|
|
+ #-}
|
|
+
|
|
+
|
|
+module MFlow.Forms.XHtml where
|
|
+
|
|
+import MFlow (HttpData(..))
|
|
+import MFlow.Forms
|
|
+import MFlow.Cookies(contentHtml)
|
|
+import Data.ByteString.Lazy.Char8(pack,unpack)
|
|
+import qualified Data.Text as T
|
|
+import Text.XHtml.Strict as X
|
|
+import Control.Monad.Trans
|
|
+import Data.Typeable
|
|
+
|
|
+instance Monad m => ADDATTRS (View Html m a) where
|
|
+ widget ! atrs= widget `wmodify` \fs mx -> return ((head fs ! atrs:tail fs), mx)
|
|
+
|
|
+
|
|
+
|
|
+instance FormInput Html where
|
|
+ toByteString = pack. showHtmlFragment
|
|
+ toHttpData = HttpData [contentHtml] [] . toByteString
|
|
+ ftag t= tag t
|
|
+ inred = X.bold ![X.thestyle "color:red"]
|
|
+ finput n t v f c= X.input ! ([thetype t ,name n, value v] ++ if f then [checked] else []
|
|
+ ++ case c of Just s ->[strAttr "onclick" s]; _ -> [] )
|
|
+ ftextarea name text= X.textarea ! [X.name name] << T.unpack text
|
|
+
|
|
+ fselect name list = select ![ X.name name] << list
|
|
+ foption name v msel= X.option ! ([value name] ++ selected msel) << v
|
|
+ where
|
|
+ selected msel = if msel then [X.selected] else []
|
|
+
|
|
+ attrs tag attrs = tag ! (map (\(n,v) -> strAttr n v) attrs)
|
|
+
|
|
+
|
|
+
|
|
+ formAction action form = X.form ! [X.action action, method "post"] << form
|
|
+ fromStr = stringToHtml
|
|
+ fromStrNoEncode= primHtml
|
|
+
|
|
+ flink v str = toHtml $ hotlink ( v) << str
|
|
+
|
|
+instance Typeable Html where
|
|
+ typeOf = \_ -> mkTyConApp (mkTyCon3 "xhtml" "Text.XHtml.Strict" "Html") []
|
|
+
|
|
+
|
|
diff -ru orig/src/MFlow/Forms.hs new/src/MFlow/Forms.hs
|
|
--- orig/src/MFlow/Forms.hs 2014-06-10 05:51:26.961015857 +0300
|
|
+++ new/src/MFlow/Forms.hs 2014-06-10 05:51:25.000000000 +0300
|
|
@@ -1,1039 +1,1047 @@
|
|
-{-# OPTIONS -XDeriveDataTypeable
|
|
- -XUndecidableInstances
|
|
- -XExistentialQuantification
|
|
- -XMultiParamTypeClasses
|
|
- -XTypeSynonymInstances
|
|
- -XFlexibleInstances
|
|
- -XScopedTypeVariables
|
|
- -XFunctionalDependencies
|
|
- -XFlexibleContexts
|
|
- -XRecordWildCards
|
|
- -XIncoherentInstances
|
|
- -XTypeFamilies
|
|
- -XTypeOperators
|
|
- -XOverloadedStrings
|
|
- -XTemplateHaskell
|
|
- -XNoMonomorphismRestriction
|
|
-
|
|
-#-}
|
|
-
|
|
-{- |
|
|
-MFlow run stateful server processes. This version is the first stateful web framework
|
|
-that is as RESTful as a web framework can be.
|
|
-
|
|
-The routes are expressed as normal, monadic haskell code in the FlowM monad. Local links
|
|
-point to alternative routes within this monadic computation just like a textual menu
|
|
-in a console application. Any GET page is directly reachable by means of a RESTful URL.
|
|
-
|
|
-At any moment the flow can respond to the back button or to any RESTful path that the user may paste in the navigation bar.
|
|
-If the procedure is waiting for another different page, the FlowM monad backtrack until the path partially match
|
|
-. From this position the execution goes forward until the rest of the path match. This way the
|
|
-statelessness is optional. However, it is possible to store a session state, which may backtrack or
|
|
-not when the navigation goes back and forth. It is upto the programmer.
|
|
-
|
|
-
|
|
-All the flow of requests and responses are coded by the programmer in a single procedure.
|
|
-Allthoug single request-response flows are possible. Therefore, the code is
|
|
-more understandable. It is not continuation based. It uses a log for thread state persistence and backtracking for
|
|
-handling the back button. Back button state syncronization is supported out-of-the-box
|
|
-
|
|
-The MFlow architecture is scalable, since the state is serializable and small
|
|
-
|
|
-The processes are stopped and restarted by the
|
|
-application server on demand, including the execution state (if the Wokflow monad is used).
|
|
-Therefore session management is automatic. State consistence and transactions are given by the TCache package.
|
|
-
|
|
-The processes interact trough widgets, that are an extension of formlets with
|
|
-additional applicative combinators, formatting, link management, callbacks, modifiers, caching,
|
|
-byteString conversion and AJAX. All is coded in pure haskell.
|
|
-
|
|
-The interfaces and communications are abstract, but there are bindings for blaze-html, HSP, Text.XHtml and byteString
|
|
-, Hack and WAI but it can be extended to non Web based architectures.
|
|
-
|
|
-Bindings for hack, and hsp >= 0.8, are not compiled by Hackage, and do not appear, but are included in the package files.
|
|
-To use them, add then to the exported modules and execute cabal install
|
|
-
|
|
-It is designed for applications that can be run with no deployment with runghc in order
|
|
-to speed up the development process. see <http://haskell-web.blogspot.com.es/2013/05/a-web-application-in-tweet.html>
|
|
-
|
|
-This module implement stateful processes (flows) that are optionally persistent.
|
|
-This means that they automatically store and recover his execution state. They are executed by the MFlow app server.
|
|
-defined in the "MFlow" module.
|
|
-
|
|
-These processses interact with the user trough user interfaces made of widgets (see below) that return back statically typed responses to
|
|
-the calling process. Because flows are stateful, not request-response, the code is more understandable, because
|
|
-all the flow of request and responses is coded by the programmer in a single procedure in the FlowM monad. Allthoug
|
|
-single request-response flows and callbacks are possible.
|
|
-
|
|
-This module is abstract with respect to the formatting (here referred with the type variable @view@) . For an
|
|
-instantiation for "Text.XHtml" import "MFlow.Forms.XHtml", "MFlow.Hack.XHtml.All" or "MFlow.Wai.XHtml.All" .
|
|
-To use Haskell Server Pages import "MFlow.Forms.HSP". However the functionalities are documented here.
|
|
-
|
|
-`ask` is the only method for user interaction. It run in the @MFlow view m@ monad, with @m@ the monad chosen by the user, usually IO.
|
|
-It send user interfaces (in the @View view m@ monad) and return statically
|
|
-typed responses. The user interface definitions are based on a extension of
|
|
-formLets (<http://www.haskell.org/haskellwiki/Formlets>) with the addition of caching, links, formatting, attributes,
|
|
- extra combinators, callbaks and modifiers.
|
|
-The interaction with the user is stateful. In the same computation there may be many
|
|
-request-response interactions, in the same way than in the case of a console applications.
|
|
-
|
|
-* APPLICATION SERVER
|
|
-
|
|
-Therefore, session and state management is simple and transparent: it is in the haskell
|
|
-structures in the scope of the computation. `transient` (normal) procedures have no persistent session state
|
|
-and `stateless` procedures accept a single request and return a single response.
|
|
-
|
|
-`MFlow.Forms.step` is a lifting monad transformer that permit persistent server procedures that
|
|
-remember the execution state even after system shutdowns by using the package workflow (<http://hackage.haskell.org/package/Workflow>) internally.
|
|
-This state management is transparent. There is no programer interface for session management.
|
|
-
|
|
-The programmer set the process timeout and the session timeout with `setTimeouts`.
|
|
-If the procedure has been stopped due to the process timeout or due to a system shutdowm,
|
|
-the procedure restart in the last state when a request for this procedure arrives
|
|
-(if the procedure uses the `step` monad transformer)
|
|
-
|
|
-* WIDGETS
|
|
-
|
|
-The correctness of the web responses is assured by the use of formLets.
|
|
-But unlike formLets in its current form, it permits the definition of widgets.
|
|
-/A widget is a combination of formLets and links within its own formatting template/, all in
|
|
-the same definition in the same source file, in plain declarative Haskell style.
|
|
-
|
|
-The formatting is abstract. It has to implement the 'FormInput' class.
|
|
-There are instances for Text.XHtml ("MFlow.Forms.XHtml"), Haskell Server Pages ("MFlow.Forms.HSP")
|
|
-and ByteString. So widgets
|
|
-can use any formatting that is instance of `FormInput`.
|
|
-It is possible to use more than one format in the same widget.
|
|
-
|
|
-Links defined with `wlink` are treated the same way than forms. They are type safe and return values
|
|
- to the same flow of execution.
|
|
-It is posssible to combine links and forms in the same widget by using applicative combinators but also
|
|
-additional applicative combinators like \<+> !*> , |*|. Widgets are also monoids, so they can
|
|
-be combined as such.
|
|
-
|
|
-* NEW IN THIS RELEASE
|
|
-
|
|
-[@Runtime templates@] 'template', 'edTemplate', 'witerate' and 'dField' permit the edition of
|
|
-the widget content at runtime, and the management of placeholders with input fields and data fields
|
|
-within the template with no navigation in the client, little bandwidth usage and little server load. Enven less
|
|
-than using 'autoRefresh'.
|
|
-
|
|
-* IN PREVIOUS RELEASES
|
|
-
|
|
-{@AutoRefresh@] Using `autoRefresh`, Dynamic widgets can refresh themselves with new information without forcing a refresh of the whole page
|
|
-
|
|
-[@Push@] With `push` a widget can push new content to the browser when something in the server happens
|
|
-
|
|
-[@Error traces@] using the monadloc package, now each runtime error (in a monadic statement) has a complete execution trace.
|
|
-
|
|
-
|
|
-[@RESTful URLs@] Now each page is directly reachable by means of a intuitive, RESTful url, whose path is composed by the sucession
|
|
-of links clicked to reach such page and such point in the procedure. Just what you would expect.
|
|
-
|
|
-[@Page flows@] each widget-formlet can have its own independent behaviour within the page. They can
|
|
-refresh independently trough AJAX by means of 'autoRefresh'. Additionally, 'pageFlow' initiates the page flow mode or a
|
|
-subpage flow by adding a well know indetifier prefix for links and form parameters.
|
|
-
|
|
-[@Modal Dialogs@] 'wdialog' present a widget within a modal or non modal jQuery dialog. while a monadic
|
|
-widget-formlet can add different form elements depending on the user responses, 'wcallback' can
|
|
-substitute the widget by other. (See 'Demos/demos.blaze.hs' for some examples)
|
|
-
|
|
-[@JQuery widgets@] with MFlow interface: 'getSpinner', 'datePicker', 'wdialog'
|
|
-
|
|
-[@WAI interface@] Now MFlow works with Snap and other WAI developments. Include "MFlow.Wai" or "MFlow.Wai.Blaze.Html.All" to use it.
|
|
-
|
|
-[@blaze-html support@] see <http://hackage.haskell.org/package/blaze-html> import "MFlow.Forms.Blaze.Html" or "MFlow.Wai.Blaze.Html.All" to use Blaze-Html
|
|
-
|
|
-[@AJAX@] Now an ajax procedures (defined with 'ajax' can perform many interactions with the browser widgets, instead
|
|
-of a single request-response (see 'ajaxSend').
|
|
-
|
|
-[@Active widgets@] "MFlow.Forms.Widgets" contains active widgets that interact with the
|
|
-server via Ajax and dynamically control other widgets: 'wEditList', 'autocomplete' 'autocompleteEdit' and others.
|
|
-
|
|
-[@Requirements@] a widget can specify javaScript files, JavasScript online scipts, CSS files, online CSS and server processes
|
|
- and any other instance of the 'Requrement' class. See 'requires' and 'WebRequirements'
|
|
-
|
|
-[@content-management@] for templating and online edition of the content template. See 'tFieldEd' 'tFieldGen' and 'tField'
|
|
-
|
|
-[@multilanguage@] see 'mField' and 'mFieldEd'
|
|
-
|
|
-[@URLs to internal states@] if the web navigation is trough GET forms or links,
|
|
- an URL can express a direct path to the n-th step of a flow, So this URL can be shared with other users.
|
|
-Just like in the case of an ordinary stateless application.
|
|
-
|
|
-
|
|
-[@Back Button@] This is probably the first implementation in any language where the navigation
|
|
-can be expressed procedurally and still it works well with the back button, thanks
|
|
-to monad magic. (See <http://haskell-web.blogspot.com.es/2012/03//failback-monad.html>)
|
|
-
|
|
-
|
|
-[@Cached widgets@] with `cachedWidget` it is possible to cache the rendering of a widget as a ByteString (maintaining type safety)
|
|
-, the caching can be permanent or for a certain time. this is very useful for complex widgets that present information. Specially if
|
|
-the widget content comes from a database and it is shared by all users.
|
|
-
|
|
-
|
|
-[@Callbacks@] `waction` add a callback to a widget. It is executed when its input is validated.
|
|
-The callback may initate a flow of interactions with the user or simply executes an internal computation.
|
|
-Callbacks are necessary for the creation of abstract container
|
|
-widgets that may not know the behaviour of its content. with callbacks, the widget manages its content as black boxes.
|
|
-
|
|
-
|
|
-[@Modifiers@] `wmodify` change the visualization and result returned by the widget. For example it may hide a
|
|
-login form and substitute it by the username if already logged.
|
|
-
|
|
-Example:
|
|
-
|
|
-@ ask $ wform userloginform \``validate`\` valdateProc \``waction`\` loginProc \``wmodify`\` hideIfLogged@
|
|
-
|
|
-
|
|
-[@attributes for formLet elements@] to add atributes to widgets. See the '<!' opèrator
|
|
-
|
|
-
|
|
-[@ByteString normalization and hetereogeneous formatting@] For caching the rendering of widgets at the
|
|
- ByteString level, and to permit many formatring styles
|
|
-in the same page, there are operators that combine different formats which are converted to ByteStrings.
|
|
-For example the header and footer may be coded in XML, while the formlets may be formatted using Text.XHtml.
|
|
-
|
|
-[@File Server@] With file caching. See "MFlow.FileServer"
|
|
-
|
|
-
|
|
--}
|
|
-
|
|
-module MFlow.Forms(
|
|
-
|
|
--- * Basic definitions
|
|
--- FormLet(..),
|
|
-FlowM, View(..), FormElm(..), FormInput(..)
|
|
-
|
|
--- * Users
|
|
-, Auth(..), userRegister, setAuthMethod, userValidate, isLogged, setAdminUser, getAdminName
|
|
-,getCurrentUser,getUserSimple, getUser, userFormLine, userLogin,logout, paranoidLogout
|
|
-,encryptedLogout, userWidget, paranoidUserWidget, encryptedUserWidget, login, paranoidLogin, encryptedLogin,
|
|
-userName,
|
|
--- * User interaction
|
|
-ask, page, askt, clearEnv, wstateless, pageFlow,
|
|
--- * formLets
|
|
--- | They usually produce the HTML form elements (depending on the FormInput instance used)
|
|
--- It is possible to modify their attributes with the `<!` operator.
|
|
--- They are combined with applicative ombinators and some additional ones
|
|
--- formatting can be added with the formatting combinators.
|
|
--- modifiers change their presentation and behaviour
|
|
-getString,getInt,getInteger, getTextBox
|
|
-,getMultilineText,getBool,getSelect, setOption,setSelectedOption, getPassword,
|
|
-getRadio, setRadio, setRadioActive, wlabel, getCheckBoxes, genCheckBoxes, setCheckBox,
|
|
-submitButton,resetButton, whidden, wlink, absLink, getKeyValueParam,
|
|
+{-# OPTIONS -XDeriveDataTypeable
|
|
+ -XUndecidableInstances
|
|
+ -XExistentialQuantification
|
|
+ -XMultiParamTypeClasses
|
|
+ -XTypeSynonymInstances
|
|
+ -XFlexibleInstances
|
|
+ -XScopedTypeVariables
|
|
+ -XFunctionalDependencies
|
|
+ -XFlexibleContexts
|
|
+ -XRecordWildCards
|
|
+ -XIncoherentInstances
|
|
+ -XTypeFamilies
|
|
+ -XTypeOperators
|
|
+ -XOverloadedStrings
|
|
+ -XTemplateHaskell
|
|
+ -XNoMonomorphismRestriction
|
|
+
|
|
+#-}
|
|
+
|
|
+{- |
|
|
+MFlow run stateful server processes. This version is the first stateful web framework
|
|
+that is as RESTful as a web framework can be.
|
|
+
|
|
+The routes are expressed as normal, monadic haskell code in the FlowM monad. Local links
|
|
+point to alternative routes within this monadic computation just like a textual menu
|
|
+in a console application. Any GET page is directly reachable by means of a RESTful URL.
|
|
+
|
|
+At any moment the flow can respond to the back button or to any RESTful path that the user may paste in the navigation bar.
|
|
+If the procedure is waiting for another different page, the FlowM monad backtrack until the path partially match
|
|
+. From this position the execution goes forward until the rest of the path match. This way the
|
|
+statelessness is optional. However, it is possible to store a session state, which may backtrack or
|
|
+not when the navigation goes back and forth. It is upto the programmer.
|
|
+
|
|
+
|
|
+All the flow of requests and responses are coded by the programmer in a single procedure.
|
|
+Allthoug single request-response flows are possible. Therefore, the code is
|
|
+more understandable. It is not continuation based. It uses a log for thread state persistence and backtracking for
|
|
+handling the back button. Back button state syncronization is supported out-of-the-box
|
|
+
|
|
+The MFlow architecture is scalable, since the state is serializable and small
|
|
+
|
|
+The processes are stopped and restarted by the
|
|
+application server on demand, including the execution state (if the Wokflow monad is used).
|
|
+Therefore session management is automatic. State consistence and transactions are given by the TCache package.
|
|
+
|
|
+The processes interact trough widgets, that are an extension of formlets with
|
|
+additional applicative combinators, formatting, link management, callbacks, modifiers, caching,
|
|
+byteString conversion and AJAX. All is coded in pure haskell.
|
|
+
|
|
+The interfaces and communications are abstract, but there are bindings for blaze-html, HSP, Text.XHtml and byteString
|
|
+, Hack and WAI but it can be extended to non Web based architectures.
|
|
+
|
|
+Bindings for hack, and hsp >= 0.8, are not compiled by Hackage, and do not appear, but are included in the package files.
|
|
+To use them, add then to the exported modules and execute cabal install
|
|
+
|
|
+It is designed for applications that can be run with no deployment with runghc in order
|
|
+to speed up the development process. see <http://haskell-web.blogspot.com.es/2013/05/a-web-application-in-tweet.html>
|
|
+
|
|
+This module implement stateful processes (flows) that are optionally persistent.
|
|
+This means that they automatically store and recover his execution state. They are executed by the MFlow app server.
|
|
+defined in the "MFlow" module.
|
|
+
|
|
+These processses interact with the user trough user interfaces made of widgets (see below) that return back statically typed responses to
|
|
+the calling process. Because flows are stateful, not request-response, the code is more understandable, because
|
|
+all the flow of request and responses is coded by the programmer in a single procedure in the FlowM monad. Allthoug
|
|
+single request-response flows and callbacks are possible.
|
|
+
|
|
+This module is abstract with respect to the formatting (here referred with the type variable @view@) . For an
|
|
+instantiation for "Text.XHtml" import "MFlow.Forms.XHtml", "MFlow.Hack.XHtml.All" or "MFlow.Wai.XHtml.All" .
|
|
+To use Haskell Server Pages import "MFlow.Forms.HSP". However the functionalities are documented here.
|
|
+
|
|
+`ask` is the only method for user interaction. It run in the @MFlow view m@ monad, with @m@ the monad chosen by the user, usually IO.
|
|
+It send user interfaces (in the @View view m@ monad) and return statically
|
|
+typed responses. The user interface definitions are based on a extension of
|
|
+formLets (<http://www.haskell.org/haskellwiki/Formlets>) with the addition of caching, links, formatting, attributes,
|
|
+ extra combinators, callbaks and modifiers.
|
|
+The interaction with the user is stateful. In the same computation there may be many
|
|
+request-response interactions, in the same way than in the case of a console applications.
|
|
+
|
|
+* APPLICATION SERVER
|
|
+
|
|
+Therefore, session and state management is simple and transparent: it is in the haskell
|
|
+structures in the scope of the computation. `transient` (normal) procedures have no persistent session state
|
|
+and `stateless` procedures accept a single request and return a single response.
|
|
+
|
|
+`MFlow.Forms.step` is a lifting monad transformer that permit persistent server procedures that
|
|
+remember the execution state even after system shutdowns by using the package workflow (<http://hackage.haskell.org/package/Workflow>) internally.
|
|
+This state management is transparent. There is no programer interface for session management.
|
|
+
|
|
+The programmer set the process timeout and the session timeout with `setTimeouts`.
|
|
+If the procedure has been stopped due to the process timeout or due to a system shutdowm,
|
|
+the procedure restart in the last state when a request for this procedure arrives
|
|
+(if the procedure uses the `step` monad transformer)
|
|
+
|
|
+* WIDGETS
|
|
+
|
|
+The correctness of the web responses is assured by the use of formLets.
|
|
+But unlike formLets in its current form, it permits the definition of widgets.
|
|
+/A widget is a combination of formLets and links within its own formatting template/, all in
|
|
+the same definition in the same source file, in plain declarative Haskell style.
|
|
+
|
|
+The formatting is abstract. It has to implement the 'FormInput' class.
|
|
+There are instances for Text.XHtml ("MFlow.Forms.XHtml"), Haskell Server Pages ("MFlow.Forms.HSP")
|
|
+and ByteString. So widgets
|
|
+can use any formatting that is instance of `FormInput`.
|
|
+It is possible to use more than one format in the same widget.
|
|
+
|
|
+Links defined with `wlink` are treated the same way than forms. They are type safe and return values
|
|
+ to the same flow of execution.
|
|
+It is posssible to combine links and forms in the same widget by using applicative combinators but also
|
|
+additional applicative combinators like \<+> !*> , |*|. Widgets are also monoids, so they can
|
|
+be combined as such.
|
|
+
|
|
+* NEW IN THIS RELEASE
|
|
+
|
|
+[@Runtime templates@] 'template', 'edTemplate', 'witerate' and 'dField' permit the edition of
|
|
+the widget content at runtime, and the management of placeholders with input fields and data fields
|
|
+within the template with no navigation in the client, little bandwidth usage and little server load. Enven less
|
|
+than using 'autoRefresh'.
|
|
+
|
|
+* IN PREVIOUS RELEASES
|
|
+
|
|
+{@AutoRefresh@] Using `autoRefresh`, Dynamic widgets can refresh themselves with new information without forcing a refresh of the whole page
|
|
+
|
|
+[@Push@] With `push` a widget can push new content to the browser when something in the server happens
|
|
+
|
|
+[@Error traces@] using the monadloc package, now each runtime error (in a monadic statement) has a complete execution trace.
|
|
+
|
|
+
|
|
+[@RESTful URLs@] Now each page is directly reachable by means of a intuitive, RESTful url, whose path is composed by the sucession
|
|
+of links clicked to reach such page and such point in the procedure. Just what you would expect.
|
|
+
|
|
+[@Page flows@] each widget-formlet can have its own independent behaviour within the page. They can
|
|
+refresh independently trough AJAX by means of 'autoRefresh'. Additionally, 'pageFlow' initiates the page flow mode or a
|
|
+subpage flow by adding a well know indetifier prefix for links and form parameters.
|
|
+
|
|
+[@Modal Dialogs@] 'wdialog' present a widget within a modal or non modal jQuery dialog. while a monadic
|
|
+widget-formlet can add different form elements depending on the user responses, 'wcallback' can
|
|
+substitute the widget by other. (See 'Demos/demos.blaze.hs' for some examples)
|
|
+
|
|
+[@JQuery widgets@] with MFlow interface: 'getSpinner', 'datePicker', 'wdialog'
|
|
+
|
|
+[@WAI interface@] Now MFlow works with Snap and other WAI developments. Include "MFlow.Wai" or "MFlow.Wai.Blaze.Html.All" to use it.
|
|
+
|
|
+[@blaze-html support@] see <http://hackage.haskell.org/package/blaze-html> import "MFlow.Forms.Blaze.Html" or "MFlow.Wai.Blaze.Html.All" to use Blaze-Html
|
|
+
|
|
+[@AJAX@] Now an ajax procedures (defined with 'ajax' can perform many interactions with the browser widgets, instead
|
|
+of a single request-response (see 'ajaxSend').
|
|
+
|
|
+[@Active widgets@] "MFlow.Forms.Widgets" contains active widgets that interact with the
|
|
+server via Ajax and dynamically control other widgets: 'wEditList', 'autocomplete' 'autocompleteEdit' and others.
|
|
+
|
|
+[@Requirements@] a widget can specify javaScript files, JavasScript online scipts, CSS files, online CSS and server processes
|
|
+ and any other instance of the 'Requrement' class. See 'requires' and 'WebRequirements'
|
|
+
|
|
+[@content-management@] for templating and online edition of the content template. See 'tFieldEd' 'tFieldGen' and 'tField'
|
|
+
|
|
+[@multilanguage@] see 'mField' and 'mFieldEd'
|
|
+
|
|
+[@URLs to internal states@] if the web navigation is trough GET forms or links,
|
|
+ an URL can express a direct path to the n-th step of a flow, So this URL can be shared with other users.
|
|
+Just like in the case of an ordinary stateless application.
|
|
+
|
|
+
|
|
+[@Back Button@] This is probably the first implementation in any language where the navigation
|
|
+can be expressed procedurally and still it works well with the back button, thanks
|
|
+to monad magic. (See <http://haskell-web.blogspot.com.es/2012/03//failback-monad.html>)
|
|
+
|
|
+
|
|
+[@Cached widgets@] with `cachedWidget` it is possible to cache the rendering of a widget as a ByteString (maintaining type safety)
|
|
+, the caching can be permanent or for a certain time. this is very useful for complex widgets that present information. Specially if
|
|
+the widget content comes from a database and it is shared by all users.
|
|
+
|
|
+
|
|
+[@Callbacks@] `waction` add a callback to a widget. It is executed when its input is validated.
|
|
+The callback may initate a flow of interactions with the user or simply executes an internal computation.
|
|
+Callbacks are necessary for the creation of abstract container
|
|
+widgets that may not know the behaviour of its content. with callbacks, the widget manages its content as black boxes.
|
|
+
|
|
+
|
|
+[@Modifiers@] `wmodify` change the visualization and result returned by the widget. For example it may hide a
|
|
+login form and substitute it by the username if already logged.
|
|
+
|
|
+Example:
|
|
+
|
|
+@ ask $ wform userloginform \``validate`\` valdateProc \``waction`\` loginProc \``wmodify`\` hideIfLogged@
|
|
+
|
|
+
|
|
+[@attributes for formLet elements@] to add atributes to widgets. See the '<!' opèrator
|
|
+
|
|
+
|
|
+[@ByteString normalization and hetereogeneous formatting@] For caching the rendering of widgets at the
|
|
+ ByteString level, and to permit many formatring styles
|
|
+in the same page, there are operators that combine different formats which are converted to ByteStrings.
|
|
+For example the header and footer may be coded in XML, while the formlets may be formatted using Text.XHtml.
|
|
+
|
|
+[@File Server@] With file caching. See "MFlow.FileServer"
|
|
+
|
|
+
|
|
+-}
|
|
+
|
|
+module MFlow.Forms(
|
|
+
|
|
+-- * Basic definitions
|
|
+-- FormLet(..),
|
|
+FlowM, View(..), FormElm(..), FormInput(..)
|
|
+
|
|
+-- * Users
|
|
+, Auth(..), userRegister, setAuthMethod, userValidate, isLogged, setAdminUser, getAdminName
|
|
+,getCurrentUser,getUserSimple, getUser, userFormLine, userLogin,logout, paranoidLogout
|
|
+,encryptedLogout, userWidget, paranoidUserWidget, encryptedUserWidget, login, paranoidLogin, encryptedLogin,
|
|
+userName,
|
|
+-- * User interaction
|
|
+ask, page, askt, clearEnv, wstateless, pageFlow,
|
|
+-- * formLets
|
|
+-- | They usually produce the HTML form elements (depending on the FormInput instance used)
|
|
+-- It is possible to modify their attributes with the `<!` operator.
|
|
+-- They are combined with applicative ombinators and some additional ones
|
|
+-- formatting can be added with the formatting combinators.
|
|
+-- modifiers change their presentation and behaviour
|
|
+getString,getInt,getInteger, getTextBox
|
|
+,getMultilineText,getBool,getSelect, setOption,setSelectedOption, getPassword,
|
|
+getRadio, setRadio, setRadioActive, wlabel, getCheckBoxes, genCheckBoxes, setCheckBox,
|
|
+submitButton,resetButton, whidden, wlink, absLink, getKeyValueParam, fileUpload,
|
|
getRestParam, returning, wform, firstOf, manyOf, allOf, wraw, wrender, notValid
|
|
--- * FormLet modifiers
|
|
-,validate, noWidget, stop, waction, wcallback, wmodify,
|
|
-
|
|
--- * Caching widgets
|
|
-cachedWidget, wcached, wfreeze,
|
|
-
|
|
--- * Widget combinators
|
|
-(<+>),(|*>),(|+|), (**>),(<**),(<|>),(<*),(<$>),(<*>),(>:>)
|
|
-
|
|
----- * Normalized (convert to ByteString) widget combinators
|
|
----- | These dot operators are indentical to the non dot operators, with the addition of the conversion of the arguments to lazy byteStrings
|
|
-----
|
|
----- The purpose is to combine heterogeneous formats into byteString-formatted widgets that
|
|
----- can be cached with `cachedWidget`
|
|
---,(.<+>.), (.|*>.), (.|+|.), (.**>.),(.<**.), (.<|>.),
|
|
-
|
|
--- * Formatting combinators
|
|
-,(<<<),(++>),(<++),(<!)
|
|
-
|
|
----- * Normalized (convert to ByteString) formatting combinators
|
|
----- | Some combinators that convert the formatting of their arguments to lazy byteString
|
|
-----(.<<.),(.<++.),(.++>.)
|
|
-
|
|
--- * ByteString tags
|
|
-,btag,bhtml,bbody
|
|
-
|
|
--- * Normalization
|
|
-,flatten, normalize
|
|
-
|
|
--- * Running the flow monad
|
|
-,runFlow, transientNav, runFlowOnce, runFlowIn
|
|
-,runFlowConf,MFlow.Forms.Internals.step
|
|
--- * controlling backtracking
|
|
-,goingBack,returnIfForward, breturn, preventGoingBack, compensate, onBacktrack, retry
|
|
-
|
|
+-- * FormLet modifiers
|
|
+,validate, noWidget, stop, waction, wcallback, wmodify,
|
|
+
|
|
+-- * Caching widgets
|
|
+cachedWidget, wcached, wfreeze,
|
|
+
|
|
+-- * Widget combinators
|
|
+(<+>),(|*>),(|+|), (**>),(<**),(<|>),(<*),(<$>),(<*>),(>:>)
|
|
+
|
|
+---- * Normalized (convert to ByteString) widget combinators
|
|
+---- | These dot operators are indentical to the non dot operators, with the addition of the conversion of the arguments to lazy byteStrings
|
|
+----
|
|
+---- The purpose is to combine heterogeneous formats into byteString-formatted widgets that
|
|
+---- can be cached with `cachedWidget`
|
|
+--,(.<+>.), (.|*>.), (.|+|.), (.**>.),(.<**.), (.<|>.),
|
|
+
|
|
+-- * Formatting combinators
|
|
+,(<<<),(++>),(<++),(<!)
|
|
+
|
|
+---- * Normalized (convert to ByteString) formatting combinators
|
|
+---- | Some combinators that convert the formatting of their arguments to lazy byteString
|
|
+----(.<<.),(.<++.),(.++>.)
|
|
+
|
|
+-- * ByteString tags
|
|
+,btag,bhtml,bbody
|
|
+
|
|
+-- * Normalization
|
|
+,flatten, normalize
|
|
+
|
|
+-- * Running the flow monad
|
|
+,runFlow, transientNav, runFlowOnce, runFlowIn
|
|
+,runFlowConf,MFlow.Forms.Internals.step
|
|
+-- * controlling backtracking
|
|
+,goingBack,returnIfForward, breturn, preventGoingBack, compensate, onBacktrack, retry
|
|
+
|
|
-- * Setting parameters
|
|
-,setHttpHeader
|
|
-,setHeader
|
|
-,addHeader
|
|
-,getHeader
|
|
-,setSessionData
|
|
+,setHttpHeader
|
|
+,setHeader
|
|
+,addHeader
|
|
+,getHeader
|
|
+,setSessionData
|
|
,getSessionData
|
|
-,getSData
|
|
-,delSessionData
|
|
-,setTimeouts
|
|
-
|
|
--- * Cookies
|
|
-,setCookie
|
|
-,setParanoidCookie
|
|
-,setEncryptedCookie
|
|
--- * Ajax
|
|
-,ajax
|
|
-,ajaxSend
|
|
-,ajaxSend_
|
|
--- * Requirements
|
|
-,Requirements(..)
|
|
-,WebRequirement(..)
|
|
-,requires
|
|
+,getSData
|
|
+,delSessionData
|
|
+,setTimeouts
|
|
+
|
|
+-- * Cookies
|
|
+,setCookie
|
|
+,setParanoidCookie
|
|
+,setEncryptedCookie
|
|
+-- * Ajax
|
|
+,ajax
|
|
+,ajaxSend
|
|
+,ajaxSend_
|
|
+-- * Requirements
|
|
+,Requirements(..)
|
|
+,WebRequirement(..)
|
|
+,requires
|
|
-- * Utility
|
|
-,getSessionId
|
|
-,getLang
|
|
-,genNewId
|
|
-,getNextId
|
|
-,changeMonad
|
|
-,FailBack
|
|
-,fromFailBack
|
|
-,toFailBack
|
|
-
|
|
-)
|
|
-where
|
|
-
|
|
-import Data.RefSerialize hiding ((<|>),empty)
|
|
-import Data.TCache
|
|
-import Data.TCache.Memoization
|
|
-import MFlow
|
|
-import MFlow.Forms.Internals
|
|
-import MFlow.Cookies
|
|
+,getSessionId
|
|
+,getLang
|
|
+,genNewId
|
|
+,getNextId
|
|
+,changeMonad
|
|
+,FailBack
|
|
+,fromFailBack
|
|
+,toFailBack
|
|
+
|
|
+)
|
|
+where
|
|
+
|
|
+import Data.RefSerialize hiding ((<|>),empty)
|
|
+import Data.TCache
|
|
+import Data.TCache.Memoization
|
|
+import MFlow
|
|
+import MFlow.Forms.Internals
|
|
+import MFlow.Cookies
|
|
import Data.ByteString.Lazy.Char8 as B(ByteString,cons,append,empty,fromChunks,unpack)
|
|
import Data.ByteString.Lazy.UTF8 hiding (length, take)
|
|
-import qualified Data.String as S
|
|
-import qualified Data.Text as T
|
|
-import Data.Text.Encoding
|
|
-import Data.List
|
|
---import qualified Data.CaseInsensitive as CI
|
|
-import Data.Typeable
|
|
-import Data.Monoid
|
|
-import Control.Monad.State.Strict
|
|
-import Data.Maybe
|
|
-import Control.Applicative
|
|
-import Control.Exception
|
|
-import Control.Concurrent
|
|
-import Control.Workflow as WF
|
|
-import Control.Monad.Identity
|
|
-import Unsafe.Coerce
|
|
-import Data.List(intersperse)
|
|
-import Data.IORef
|
|
-import qualified Data.Map as M
|
|
-import System.IO.Unsafe
|
|
-import Data.Char(isNumber,toLower)
|
|
-import Network.HTTP.Types.Header
|
|
-import MFlow.Forms.Cache
|
|
-
|
|
--- | Validates a form or widget result against a validating procedure
|
|
---
|
|
--- @getOdd= getInt Nothing `validate` (\x -> return $ if mod x 2==0 then Nothing else Just "only odd numbers, please")@
|
|
-validate
|
|
- :: (FormInput view, Monad m) =>
|
|
- View view m a
|
|
- -> (a -> WState view m (Maybe view))
|
|
- -> View view m a
|
|
-validate formt val= View $ do
|
|
- FormElm form mx <- runView formt
|
|
- case mx of
|
|
- Just x -> do
|
|
- me <- val x
|
|
- modify (\s -> s{inSync= True})
|
|
- case me of
|
|
- Just str ->
|
|
- return $ FormElm ( form <> inred str) Nothing
|
|
- Nothing -> return $ FormElm form mx
|
|
- _ -> return $ FormElm form mx
|
|
-
|
|
--- | Actions are callbacks that are executed when a widget is validated.
|
|
--- A action may be a complete flow in the flowM monad. It takes complete control of the navigation
|
|
--- while it is executed. At the end it return the result to the caller and display the original
|
|
--- calling page.
|
|
--- It is useful when the widget is inside widget containers that may treat it as a black box.
|
|
---
|
|
--- It returns a result that can be significative or, else, be ignored with '<**' and '**>'.
|
|
--- An action may or may not initiate his own dialog with the user via `ask`
|
|
-waction
|
|
- :: (FormInput view, Monad m)
|
|
- => View view m a
|
|
- -> (a -> FlowM view m b)
|
|
- -> View view m b
|
|
-waction f ac = do
|
|
- x <- f
|
|
- s <- get
|
|
- let env = mfEnv s
|
|
- let seq = mfSequence s
|
|
- put s{mfSequence=mfSequence s+ 100,mfEnv=[],newAsk=True}
|
|
- r <- flowToView $ ac x
|
|
- modify $ \s-> s{mfSequence= seq, mfEnv= env}
|
|
- return r
|
|
- where
|
|
- flowToView x=
|
|
- View $ do
|
|
- r <- runSup $ runFlowM x
|
|
- case r of
|
|
- NoBack x ->
|
|
- return (FormElm mempty $ Just x)
|
|
- BackPoint x->
|
|
- return (FormElm mempty $ Just x)
|
|
- GoBack-> do
|
|
- modify $ \s ->s{notSyncInAction= True}
|
|
- return (FormElm mempty Nothing)
|
|
-
|
|
--- | change the rendering and the return value of a page. This is superseeded by page flows.
|
|
-wmodify :: (Monad m, FormInput v)
|
|
- => View v m a
|
|
- -> (v -> Maybe a -> WState v m (v, Maybe b))
|
|
- -> View v m b
|
|
-wmodify formt act = View $ do
|
|
- FormElm f mx <- runView formt
|
|
- (f',mx') <- act f mx
|
|
- return $ FormElm f' mx'
|
|
-
|
|
--- | Display a text box and return a non empty String
|
|
-getString :: (FormInput view,Monad m) =>
|
|
- Maybe String -> View view m String
|
|
-getString ms = getTextBox ms
|
|
- `validate`
|
|
- \s -> if null s then return (Just $ fromStr "")
|
|
- else return Nothing
|
|
-
|
|
--- | Display a text box and return an Integer (if the value entered is not an Integer, fails the validation)
|
|
-getInteger :: (FormInput view, MonadIO m) =>
|
|
- Maybe Integer -> View view m Integer
|
|
-getInteger = getTextBox
|
|
-
|
|
--- | Display a text box and return a Int (if the value entered is not an Int, fails the validation)
|
|
-getInt :: (FormInput view, MonadIO m) =>
|
|
- Maybe Int -> View view m Int
|
|
-getInt = getTextBox
|
|
-
|
|
--- | Display a password box
|
|
-getPassword :: (FormInput view,
|
|
- Monad m) =>
|
|
- View view m String
|
|
-getPassword = getParam Nothing "password" Nothing
|
|
-
|
|
-newtype Radio a= Radio a
|
|
-
|
|
--- | Implement a radio button that perform a submit when pressed.
|
|
--- the parameter is the name of the radio group
|
|
-setRadioActive :: (FormInput view, MonadIO m,
|
|
- Read a, Typeable a, Eq a, Show a) =>
|
|
- a -> String -> View view m (Radio a)
|
|
-setRadioActive v n = View $ do
|
|
- st <- get
|
|
- put st{needForm= HasElems}
|
|
- let env = mfEnv st
|
|
- mn <- getParam1 n env
|
|
- let str = if typeOf v == typeOf(undefined :: String)
|
|
- then unsafeCoerce v else show v
|
|
- return $ FormElm (finput n "radio" str
|
|
- ( isValidated mn && v== fromValidated mn) (Just "this.form.submit()"))
|
|
- (fmap Radio $ valToMaybe mn)
|
|
-
|
|
-
|
|
--- | Implement a radio button
|
|
--- the parameter is the name of the radio group
|
|
-setRadio :: (FormInput view, MonadIO m,
|
|
- Read a, Typeable a, Eq a, Show a) =>
|
|
- a -> String -> View view m (Radio a)
|
|
-setRadio v n= View $ do
|
|
- st <- get
|
|
- put st{needForm= HasElems}
|
|
- let env = mfEnv st
|
|
- mn <- getParam1 n env
|
|
- let str = if typeOf v == typeOf(undefined :: String)
|
|
- then unsafeCoerce v else show v
|
|
- return $ FormElm (finput n "radio" str
|
|
- ( isValidated mn && v== fromValidated mn) Nothing)
|
|
- (fmap Radio $ valToMaybe mn)
|
|
-
|
|
--- | encloses a set of Radio boxes. Return the option selected
|
|
-getRadio
|
|
- :: (Monad m, Functor m, FormInput view) =>
|
|
- [String -> View view m (Radio a)] -> View view m a
|
|
-getRadio rs= do
|
|
- id <- genNewId
|
|
- Radio r <- firstOf $ map (\r -> r id) rs
|
|
- return r
|
|
-
|
|
-data CheckBoxes = CheckBoxes [String]
|
|
-
|
|
-instance Monoid CheckBoxes where
|
|
- mappend (CheckBoxes xs) (CheckBoxes ys)= CheckBoxes $ xs ++ ys
|
|
- mempty= CheckBoxes []
|
|
-
|
|
---instance (Monad m, Functor m) => Monoid (View v m CheckBoxes) where
|
|
--- mappend x y= mappend <$> x <*> y
|
|
--- mempty= return (CheckBoxes [])
|
|
-
|
|
-
|
|
--- | Display a text box and return the value entered if it is readable( Otherwise, fail the validation)
|
|
-setCheckBox :: (FormInput view, MonadIO m) =>
|
|
- Bool -> String -> View view m CheckBoxes
|
|
-setCheckBox checked v= View $ do
|
|
- n <- genNewId
|
|
- st <- get
|
|
- put st{needForm= HasElems}
|
|
- let env = mfEnv st
|
|
- strs= map snd $ filter ((==) n . fst) env
|
|
- mn= if null strs then Nothing else Just $ head strs
|
|
- val = inSync st
|
|
- let ret= case val of -- !> show val of
|
|
- True -> Just $ CheckBoxes strs -- !> show strs
|
|
- False -> Nothing
|
|
- return $ FormElm
|
|
- ( finput n "checkbox" v
|
|
- ( checked || (isJust mn && v== fromJust mn)) Nothing)
|
|
- ret
|
|
-
|
|
--- | Read the checkboxes dinamically created by JavaScript within the view parameter
|
|
--- see for example `selectAutocomplete` in "MFlow.Forms.Widgets"
|
|
-genCheckBoxes :: (Monad m, FormInput view) => view -> View view m CheckBoxes
|
|
-genCheckBoxes v= View $ do
|
|
- n <- genNewId
|
|
- st <- get
|
|
- put st{needForm= HasElems}
|
|
- let env = mfEnv st
|
|
- strs= map snd $ filter ((==) n . fst) env
|
|
- mn= if null strs then Nothing else Just $ head strs
|
|
-
|
|
- val <- gets inSync
|
|
- let ret= case val of
|
|
- True -> Just $ CheckBoxes strs
|
|
- False -> Nothing
|
|
- return $ FormElm (ftag "span" v `attrs`[("id",n)]) ret
|
|
-
|
|
-whidden :: (Monad m, FormInput v,Read a, Show a, Typeable a) => a -> View v m a
|
|
-whidden x= View $ do
|
|
- n <- genNewId
|
|
- env <- gets mfEnv
|
|
- let showx= case cast x of
|
|
- Just x' -> x'
|
|
- Nothing -> show x
|
|
- r <- getParam1 n env
|
|
- return . FormElm (finput n "hidden" showx False Nothing) $ valToMaybe r
|
|
-
|
|
-getCheckBoxes :: (FormInput view, Monad m)=> View view m CheckBoxes -> View view m [String]
|
|
-getCheckBoxes boxes = View $ do
|
|
- n <- genNewId
|
|
- st <- get
|
|
- let env = mfEnv st
|
|
- let form= finput n "hidden" "" False Nothing
|
|
- mr <- getParam1 n env
|
|
-
|
|
- let env = mfEnv st
|
|
- modify $ \st -> st{needForm= HasElems}
|
|
- FormElm form2 mr2 <- runView boxes
|
|
- return $ FormElm (form <> form2) $
|
|
- case (mr `asTypeOf` Validated ("" :: String),mr2) of
|
|
- (NoParam,_) -> Nothing
|
|
- (Validated _,Nothing) -> Just []
|
|
- (Validated _, Just (CheckBoxes rs)) -> Just rs
|
|
-
|
|
-
|
|
-
|
|
-
|
|
-
|
|
-getTextBox
|
|
- :: (FormInput view,
|
|
- Monad m,
|
|
- Typeable a,
|
|
- Show a,
|
|
- Read a) =>
|
|
- Maybe a -> View view m a
|
|
-getTextBox ms = getParam Nothing "text" ms
|
|
-
|
|
-
|
|
-getParam
|
|
- :: (FormInput view,
|
|
- Monad m,
|
|
- Typeable a,
|
|
- Show a,
|
|
- Read a) =>
|
|
- Maybe String -> String -> Maybe a -> View view m a
|
|
-getParam look type1 mvalue = View $ do
|
|
- tolook <- case look of
|
|
- Nothing -> genNewId
|
|
- Just n -> return n
|
|
- let nvalue x = case x of
|
|
- Nothing -> ""
|
|
- Just v ->
|
|
- case cast v of
|
|
- Just v' -> v'
|
|
- Nothing -> show v
|
|
- st <- get
|
|
- let env = mfEnv st
|
|
- put st{needForm= HasElems}
|
|
- r <- getParam1 tolook env
|
|
- case r of
|
|
- Validated x -> return $ FormElm (finput tolook type1 (nvalue $ Just x) False Nothing) $ Just x
|
|
- NotValidated s err -> return $ FormElm (finput tolook type1 s False Nothing <> err) $ Nothing
|
|
- NoParam -> return $ FormElm (finput tolook type1 (nvalue mvalue) False Nothing) $ Nothing
|
|
-
|
|
-
|
|
-
|
|
---getCurrentName :: MonadState (MFlowState view) m => m String
|
|
---getCurrentName= do
|
|
--- st <- get
|
|
--- let parm = mfSequence st
|
|
--- return $ "p"++show parm
|
|
-
|
|
-
|
|
--- | Display a multiline text box and return its content
|
|
-getMultilineText :: (FormInput view
|
|
- , Monad m)
|
|
- => T.Text
|
|
- -> View view m T.Text
|
|
-getMultilineText nvalue = View $ do
|
|
- tolook <- genNewId
|
|
- env <- gets mfEnv
|
|
- r <- getParam1 tolook env
|
|
- case r of
|
|
- Validated x -> return $ FormElm (ftextarea tolook x) $ Just x
|
|
- NotValidated s err -> return $ FormElm (ftextarea tolook (T.pack s)) Nothing
|
|
- NoParam -> return $ FormElm (ftextarea tolook nvalue) Nothing
|
|
-
|
|
-
|
|
---instance (MonadIO m, Functor m, FormInput view) => FormLet Bool m view where
|
|
--- digest mv = getBool b "True" "False"
|
|
--- where
|
|
--- b= case mv of
|
|
--- Nothing -> Nothing
|
|
--- Just bool -> Just $ case bool of
|
|
--- True -> "True"
|
|
--- False -> "False"
|
|
-
|
|
--- | Display a dropdown box with the two values (second (true) and third parameter(false))
|
|
--- . With the value of the first parameter selected.
|
|
-getBool :: (FormInput view,
|
|
- Monad m, Functor m) =>
|
|
- Bool -> String -> String -> View view m Bool
|
|
-getBool mv truestr falsestr= do
|
|
- r <- getSelect $ setOption truestr (fromStr truestr) <! (if mv then [("selected","true")] else [])
|
|
- <|> setOption falsestr(fromStr falsestr) <! if not mv then [("selected","true")] else []
|
|
- if r == truestr then return True else return False
|
|
-
|
|
-
|
|
-
|
|
--- | Display a dropdown box with the options in the first parameter is optionally selected
|
|
--- . It returns the selected option.
|
|
-getSelect :: (FormInput view,
|
|
- Monad m,Typeable a, Read a) =>
|
|
- View view m (MFOption a) -> View view m a
|
|
-getSelect opts = View $ do
|
|
- tolook <- genNewId
|
|
- st <- get
|
|
- let env = mfEnv st
|
|
- put st{needForm= HasElems}
|
|
- r <- getParam1 tolook env
|
|
- setSessionData $ fmap MFOption $ valToMaybe r
|
|
- FormElm form mr <- (runView opts)
|
|
-
|
|
- return $ FormElm (fselect tolook form) $ valToMaybe r
|
|
-
|
|
-
|
|
-newtype MFOption a= MFOption a deriving Typeable
|
|
-
|
|
-instance (FormInput view,Monad m, Functor m) => Monoid (View view m (MFOption a)) where
|
|
- mappend = (<|>)
|
|
- mempty = Control.Applicative.empty
|
|
-
|
|
--- | Set the option for getSelect. Options are concatenated with `<|>`
|
|
-setOption
|
|
- :: (Monad m, Show a, Eq a, Typeable a, FormInput view) =>
|
|
- a -> view -> View view m (MFOption a)
|
|
-setOption n v = do
|
|
- mo <- getSessionData
|
|
- case mo of
|
|
- Nothing -> setOption1 n v False
|
|
- Just Nothing -> setOption1 n v False
|
|
- Just (Just (MFOption o)) -> setOption1 n v $ n == o
|
|
-
|
|
--- | Set the selected option for getSelect. Options are concatenated with `<|>`
|
|
-setSelectedOption
|
|
- :: (Monad m, Show a, Eq a, Typeable a, FormInput view) =>
|
|
- a -> view -> View view m (MFOption a)
|
|
-setSelectedOption n v= do
|
|
- mo <- getSessionData
|
|
- case mo of
|
|
- Nothing -> setOption1 n v True
|
|
- Just Nothing -> setOption1 n v True
|
|
- Just (Just o) -> setOption1 n v $ n == o
|
|
-
|
|
-
|
|
-setOption1 :: (FormInput view,
|
|
- Monad m, Typeable a, Eq a, Show a) =>
|
|
- a -> view -> Bool -> View view m (MFOption a)
|
|
-setOption1 nam val check= View $ do
|
|
- st <- get
|
|
- let env = mfEnv st
|
|
- put st{needForm= HasElems}
|
|
- let n = if typeOf nam == typeOf(undefined :: String)
|
|
- then unsafeCoerce nam
|
|
- else show nam
|
|
-
|
|
- return . FormElm (foption n val check) . Just $ MFOption nam
|
|
-
|
|
---fileUpload :: (FormInput view,
|
|
--- Monad m) =>
|
|
--- View view m T.Text
|
|
---fileUpload= getParam Nothing "file" Nothing
|
|
-
|
|
-
|
|
--- | Enclose Widgets within some formating.
|
|
--- @view@ is intended to be instantiated to a particular format
|
|
---
|
|
--- NOTE: It has a infix priority : @infixr 5@ less than the one of @++>@ and @<++@ of the operators, so use parentheses when appropriate,
|
|
--- unless the we want to enclose all the widgets in the right side.
|
|
--- Most of the type errors in the DSL are due to the low priority of this operator.
|
|
---
|
|
--- This is a widget, which is a table with some links. it returns an Int
|
|
---
|
|
--- > import MFlow.Forms.Blaze.Html
|
|
--- >
|
|
--- > tableLinks :: View Html Int
|
|
--- > table ! At.style "border:1;width:20%;margin-left:auto;margin-right:auto"
|
|
--- > <<< caption << text "choose an item"
|
|
--- > ++> thead << tr << ( th << b << text "item" <> th << b << text "times chosen")
|
|
--- > ++> (tbody
|
|
--- > <<< tr ! rowspan "2" << td << linkHome
|
|
--- > ++> (tr <<< td <<< wlink IPhone (b << text "iphone") <++ td << ( b << text (fromString $ show ( cart V.! 0)))
|
|
--- > <|> tr <<< td <<< wlink IPod (b << text "ipad") <++ td << ( b << text (fromString $ show ( cart V.! 1)))
|
|
--- > <|> tr <<< td <<< wlink IPad (b << text "ipod") <++ td << ( b << text (fromString $ show ( cart V.! 2))))
|
|
--- > )
|
|
-(<<<) :: (Monad m, Monoid view)
|
|
- => (view ->view)
|
|
- -> View view m a
|
|
- -> View view m a
|
|
-(<<<) v form= View $ do
|
|
- FormElm f mx <- runView form
|
|
- return $ FormElm (v f) mx
|
|
-
|
|
-
|
|
-infixr 5 <<<
|
|
-
|
|
-
|
|
-
|
|
-
|
|
-
|
|
-
|
|
--- | Append formatting code to a widget
|
|
---
|
|
--- @ getString "hi" <++ H1 << "hi there"@
|
|
---
|
|
--- It has a infix prority: @infixr 6@ higuer that '<<<' and most other operators
|
|
-(<++) :: (Monad m, Monoid v)
|
|
- => View v m a
|
|
- -> v
|
|
- -> View v m a
|
|
-(<++) form v= View $ do
|
|
- FormElm f mx <- runView form
|
|
- return $ FormElm ( f <> v) mx
|
|
-
|
|
-infixr 6 ++>
|
|
-infixr 6 <++
|
|
--- | Prepend formatting code to a widget
|
|
---
|
|
--- @bold << "enter name" ++> getString Nothing @
|
|
---
|
|
--- It has a infix prority: @infixr 6@ higuer that '<<<' and most other operators
|
|
-(++>) :: (Monad m, Monoid view)
|
|
- => view -> View view m a -> View view m a
|
|
-html ++> w = -- (html <>) <<< digest
|
|
- View $ do
|
|
- FormElm f mx <- runView w
|
|
- return $ FormElm (html <> f) mx
|
|
-
|
|
-
|
|
-
|
|
--- | Add attributes to the topmost tag of a widget
|
|
---
|
|
--- it has a fixity @infix 8@
|
|
-infixl 8 <!
|
|
-widget <! attribs= View $ do
|
|
- FormElm fs mx <- runView widget
|
|
- return $ FormElm (fs `attrs` attribs) mx -- (head fs `attrs` attribs:tail fs) mx
|
|
--- case fs of
|
|
--- [hfs] -> return $ FormElm [hfs `attrs` attribs] mx
|
|
--- _ -> error $ "operator <! : malformed widget: "++ concatMap (unpack. toByteString) fs
|
|
-
|
|
-
|
|
--- | Is an example of login\/register validation form needed by 'userWidget'. In this case
|
|
--- the form field appears in a single line. it shows, in sequence, entries for the username,
|
|
--- password, a button for loging, a entry to repeat password necesary for registering
|
|
--- and a button for registering.
|
|
--- The user can build its own user login\/validation forms by modifying this example
|
|
---
|
|
--- @ userFormLine=
|
|
--- (User \<\$\> getString (Just \"enter user\") \<\*\> getPassword \<\+\> submitButton \"login\")
|
|
--- \<\+\> fromStr \" password again\" \+\> getPassword \<\* submitButton \"register\"
|
|
--- @
|
|
-userFormLine :: (FormInput view, Functor m, Monad m)
|
|
- => View view m (Maybe (UserStr,PasswdStr), Maybe PasswdStr)
|
|
-userFormLine=
|
|
- ((,) <$> getString (Just "enter user") <! [("size","5")]
|
|
- <*> getPassword <! [("size","5")]
|
|
- <** submitButton "login")
|
|
- <+> (fromStr " password again" ++> getPassword <! [("size","5")]
|
|
- <** submitButton "register")
|
|
-
|
|
--- | Example of user\/password form (no validation) to be used with 'userWidget'
|
|
-userLogin :: (FormInput view, Functor m, Monad m)
|
|
- => View view m (Maybe (UserStr,PasswdStr), Maybe String)
|
|
-userLogin=
|
|
- ((,) <$> fromStr "Enter User: " ++> getString Nothing <! [("size","4")]
|
|
- <*> fromStr " Enter Pass: " ++> getPassword <! [("size","4")]
|
|
- <** submitButton "login")
|
|
- <+> (noWidget
|
|
- <* noWidget)
|
|
-
|
|
-
|
|
-
|
|
--- | Empty widget that does not validate. May be used as \"empty boxes\" inside larger widgets.
|
|
---
|
|
--- It returns a non valid value.
|
|
-noWidget :: (FormInput view,
|
|
- Monad m, Functor m) =>
|
|
- View view m a
|
|
-noWidget= Control.Applicative.empty
|
|
+import qualified Data.String as S
|
|
+import qualified Data.Text as T
|
|
+import Data.Text.Encoding
|
|
+import Data.List
|
|
+--import qualified Data.CaseInsensitive as CI
|
|
+import Data.Typeable
|
|
+import Data.Monoid
|
|
+import Control.Monad.State.Strict
|
|
+import Data.Maybe
|
|
+import Control.Applicative
|
|
+import Control.Exception
|
|
+import Control.Concurrent
|
|
+import Control.Workflow as WF
|
|
+import Control.Monad.Identity
|
|
+import Unsafe.Coerce
|
|
+import Data.List(intersperse)
|
|
+import Data.IORef
|
|
+import qualified Data.Map as M
|
|
+import System.IO.Unsafe
|
|
+import Data.Char(isNumber,toLower)
|
|
+import Network.HTTP.Types.Header
|
|
+import MFlow.Forms.Cache
|
|
+
|
|
+-- | Validates a form or widget result against a validating procedure
|
|
+--
|
|
+-- @getOdd= getInt Nothing `validate` (\x -> return $ if mod x 2==0 then Nothing else Just "only odd numbers, please")@
|
|
+validate
|
|
+ :: (FormInput view, Monad m) =>
|
|
+ View view m a
|
|
+ -> (a -> WState view m (Maybe view))
|
|
+ -> View view m a
|
|
+validate formt val= View $ do
|
|
+ FormElm form mx <- runView formt
|
|
+ case mx of
|
|
+ Just x -> do
|
|
+ me <- val x
|
|
+ modify (\s -> s{inSync= True})
|
|
+ case me of
|
|
+ Just str ->
|
|
+ return $ FormElm ( form <> inred str) Nothing
|
|
+ Nothing -> return $ FormElm form mx
|
|
+ _ -> return $ FormElm form mx
|
|
+
|
|
+-- | Actions are callbacks that are executed when a widget is validated.
|
|
+-- A action may be a complete flow in the flowM monad. It takes complete control of the navigation
|
|
+-- while it is executed. At the end it return the result to the caller and display the original
|
|
+-- calling page.
|
|
+-- It is useful when the widget is inside widget containers that may treat it as a black box.
|
|
+--
|
|
+-- It returns a result that can be significative or, else, be ignored with '<**' and '**>'.
|
|
+-- An action may or may not initiate his own dialog with the user via `ask`
|
|
+waction
|
|
+ :: (FormInput view, Monad m)
|
|
+ => View view m a
|
|
+ -> (a -> FlowM view m b)
|
|
+ -> View view m b
|
|
+waction f ac = do
|
|
+ x <- f
|
|
+ s <- get
|
|
+ let env = mfEnv s
|
|
+ let seq = mfSequence s
|
|
+ put s{mfSequence=mfSequence s+ 100,mfEnv=[],newAsk=True}
|
|
+ r <- flowToView $ ac x
|
|
+ modify $ \s-> s{mfSequence= seq, mfEnv= env}
|
|
+ return r
|
|
+ where
|
|
+ flowToView x=
|
|
+ View $ do
|
|
+ r <- runSup $ runFlowM x
|
|
+ case r of
|
|
+ NoBack x ->
|
|
+ return (FormElm mempty $ Just x)
|
|
+ BackPoint x->
|
|
+ return (FormElm mempty $ Just x)
|
|
+ GoBack-> do
|
|
+ modify $ \s ->s{notSyncInAction= True}
|
|
+ return (FormElm mempty Nothing)
|
|
+
|
|
+-- | change the rendering and the return value of a page. This is superseeded by page flows.
|
|
+wmodify :: (Monad m, FormInput v)
|
|
+ => View v m a
|
|
+ -> (v -> Maybe a -> WState v m (v, Maybe b))
|
|
+ -> View v m b
|
|
+wmodify formt act = View $ do
|
|
+ FormElm f mx <- runView formt
|
|
+ (f',mx') <- act f mx
|
|
+ return $ FormElm f' mx'
|
|
+
|
|
+-- | Display a text box and return a non empty String
|
|
+getString :: (FormInput view,Monad m) =>
|
|
+ Maybe String -> View view m String
|
|
+getString ms = getTextBox ms
|
|
+ `validate`
|
|
+ \s -> if null s then return (Just $ fromStr "")
|
|
+ else return Nothing
|
|
+
|
|
+-- | Display a text box and return an Integer (if the value entered is not an Integer, fails the validation)
|
|
+getInteger :: (FormInput view, MonadIO m) =>
|
|
+ Maybe Integer -> View view m Integer
|
|
+getInteger = getTextBox
|
|
+
|
|
+-- | Display a text box and return a Int (if the value entered is not an Int, fails the validation)
|
|
+getInt :: (FormInput view, MonadIO m) =>
|
|
+ Maybe Int -> View view m Int
|
|
+getInt = getTextBox
|
|
+
|
|
+-- | Display a password box
|
|
+getPassword :: (FormInput view,
|
|
+ Monad m) =>
|
|
+ View view m String
|
|
+getPassword = getParam Nothing "password" Nothing
|
|
+
|
|
+newtype Radio a= Radio a
|
|
+
|
|
+-- | Implement a radio button that perform a submit when pressed.
|
|
+-- the parameter is the name of the radio group
|
|
+setRadioActive :: (FormInput view, MonadIO m,
|
|
+ Read a, Typeable a, Eq a, Show a) =>
|
|
+ a -> String -> View view m (Radio a)
|
|
+setRadioActive v n = View $ do
|
|
+ st <- get
|
|
+ put st{needForm= HasElems }
|
|
+ let env = mfEnv st
|
|
+ mn <- getParam1 n env
|
|
+ let str = if typeOf v == typeOf(undefined :: String)
|
|
+ then unsafeCoerce v else show v
|
|
+ return $ FormElm (finput n "radio" str
|
|
+ ( isValidated mn && v== fromValidated mn) (Just "this.form.submit()"))
|
|
+ (fmap Radio $ valToMaybe mn)
|
|
+
|
|
+
|
|
+-- | Implement a radio button
|
|
+-- the parameter is the name of the radio group
|
|
+setRadio :: (FormInput view, MonadIO m,
|
|
+ Read a, Typeable a, Eq a, Show a) =>
|
|
+ a -> String -> View view m (Radio a)
|
|
+setRadio v n= View $ do
|
|
+ st <- get
|
|
+ put st{needForm= HasElems}
|
|
+ let env = mfEnv st
|
|
+ mn <- getParam1 n env
|
|
+ let str = if typeOf v == typeOf(undefined :: String)
|
|
+ then unsafeCoerce v else show v
|
|
+ return $ FormElm (finput n "radio" str
|
|
+ ( isValidated mn && v== fromValidated mn) Nothing)
|
|
+ (fmap Radio $ valToMaybe mn)
|
|
+
|
|
+-- | encloses a set of Radio boxes. Return the option selected
|
|
+getRadio
|
|
+ :: (Monad m, Functor m, FormInput view) =>
|
|
+ [String -> View view m (Radio a)] -> View view m a
|
|
+getRadio rs= do
|
|
+ id <- genNewId
|
|
+ Radio r <- firstOf $ map (\r -> r id) rs
|
|
+ return r
|
|
+
|
|
+data CheckBoxes = CheckBoxes [String]
|
|
+
|
|
+instance Monoid CheckBoxes where
|
|
+ mappend (CheckBoxes xs) (CheckBoxes ys)= CheckBoxes $ xs ++ ys
|
|
+ mempty= CheckBoxes []
|
|
+
|
|
+--instance (Monad m, Functor m) => Monoid (View v m CheckBoxes) where
|
|
+-- mappend x y= mappend <$> x <*> y
|
|
+-- mempty= return (CheckBoxes [])
|
|
+
|
|
+
|
|
+-- | Display a text box and return the value entered if it is readable( Otherwise, fail the validation)
|
|
+setCheckBox :: (FormInput view, MonadIO m) =>
|
|
+ Bool -> String -> View view m CheckBoxes
|
|
+setCheckBox checked v= View $ do
|
|
+ n <- genNewId
|
|
+ st <- get
|
|
+ put st{needForm= HasElems}
|
|
+ let env = mfEnv st
|
|
+ strs= map snd $ filter ((==) n . fst) env
|
|
+ mn= if null strs then Nothing else Just $ head strs
|
|
+ val = inSync st
|
|
+ let ret= case val of -- !> show val of
|
|
+ True -> Just $ CheckBoxes strs -- !> show strs
|
|
+ False -> Nothing
|
|
+ return $ FormElm
|
|
+ ( finput n "checkbox" v
|
|
+ ( checked || (isJust mn && v== fromJust mn)) Nothing)
|
|
+ ret
|
|
+
|
|
+-- | Read the checkboxes dinamically created by JavaScript within the view parameter
|
|
+-- see for example `selectAutocomplete` in "MFlow.Forms.Widgets"
|
|
+genCheckBoxes :: (Monad m, FormInput view) => view -> View view m CheckBoxes
|
|
+genCheckBoxes v= View $ do
|
|
+ n <- genNewId
|
|
+ st <- get
|
|
+ put st{needForm= HasElems}
|
|
+ let env = mfEnv st
|
|
+ strs= map snd $ filter ((==) n . fst) env
|
|
+ mn= if null strs then Nothing else Just $ head strs
|
|
+
|
|
+ val <- gets inSync
|
|
+ let ret= case val of
|
|
+ True -> Just $ CheckBoxes strs
|
|
+ False -> Nothing
|
|
+ return $ FormElm (ftag "span" v `attrs`[("id",n)]) ret
|
|
+
|
|
+whidden :: (Monad m, FormInput v,Read a, Show a, Typeable a) => a -> View v m a
|
|
+whidden x= View $ do
|
|
+ n <- genNewId
|
|
+ env <- gets mfEnv
|
|
+ let showx= case cast x of
|
|
+ Just x' -> x'
|
|
+ Nothing -> show x
|
|
+ r <- getParam1 n env
|
|
+ return . FormElm (finput n "hidden" showx False Nothing) $ valToMaybe r
|
|
+
|
|
+getCheckBoxes :: (FormInput view, Monad m)=> View view m CheckBoxes -> View view m [String]
|
|
+getCheckBoxes boxes = View $ do
|
|
+ n <- genNewId
|
|
+ st <- get
|
|
+ let env = mfEnv st
|
|
+ let form= finput n "hidden" "" False Nothing
|
|
+ mr <- getParam1 n env
|
|
+
|
|
+ let env = mfEnv st
|
|
+ modify $ \st -> st{needForm= HasElems}
|
|
+ FormElm form2 mr2 <- runView boxes
|
|
+ return $ FormElm (form <> form2) $
|
|
+ case (mr `asTypeOf` Validated ("" :: String),mr2) of
|
|
+ (NoParam,_) -> Nothing
|
|
+ (Validated _,Nothing) -> Just []
|
|
+ (Validated _, Just (CheckBoxes rs)) -> Just rs
|
|
+
|
|
+
|
|
+
|
|
+
|
|
+
|
|
+getTextBox
|
|
+ :: (FormInput view,
|
|
+ Monad m,
|
|
+ Typeable a,
|
|
+ Show a,
|
|
+ Read a) =>
|
|
+ Maybe a -> View view m a
|
|
+getTextBox ms = getParam Nothing "text" ms
|
|
+
|
|
+
|
|
+getParam
|
|
+ :: (FormInput view,
|
|
+ Monad m,
|
|
+ Typeable a,
|
|
+ Show a,
|
|
+ Read a) =>
|
|
+ Maybe String -> String -> Maybe a -> View view m a
|
|
+getParam look type1 mvalue = View $ do
|
|
+ tolook <- case look of
|
|
+ Nothing -> genNewId
|
|
+ Just n -> return n
|
|
+ let nvalue x = case x of
|
|
+ Nothing -> ""
|
|
+ Just v ->
|
|
+ case cast v of
|
|
+ Just v' -> v'
|
|
+ Nothing -> show v
|
|
+ st <- get
|
|
+ let env = mfEnv st
|
|
+ put st{needForm= HasElems}
|
|
+ r <- getParam1 tolook env
|
|
+ case r of
|
|
+ Validated x -> return $ FormElm (finput tolook type1 (nvalue $ Just x) False Nothing) $ Just x
|
|
+ NotValidated s err -> return $ FormElm (finput tolook type1 s False Nothing <> err) $ Nothing
|
|
+ NoParam -> return $ FormElm (finput tolook type1 (nvalue mvalue) False Nothing) $ Nothing
|
|
+
|
|
+
|
|
+
|
|
+--getCurrentName :: MonadState (MFlowState view) m => m String
|
|
+--getCurrentName= do
|
|
+-- st <- get
|
|
+-- let parm = mfSequence st
|
|
+-- return $ "p"++show parm
|
|
+
|
|
+
|
|
+-- | Display a multiline text box and return its content
|
|
+getMultilineText :: (FormInput view
|
|
+ , Monad m)
|
|
+ => T.Text
|
|
+ -> View view m T.Text
|
|
+getMultilineText nvalue = View $ do
|
|
+ tolook <- genNewId
|
|
+ env <- gets mfEnv
|
|
+ r <- getParam1 tolook env
|
|
+ case r of
|
|
+ Validated x -> return $ FormElm (ftextarea tolook x) $ Just x
|
|
+ NotValidated s err -> return $ FormElm (ftextarea tolook (T.pack s)) Nothing
|
|
+ NoParam -> return $ FormElm (ftextarea tolook nvalue) Nothing
|
|
+
|
|
+
|
|
+--instance (MonadIO m, Functor m, FormInput view) => FormLet Bool m view where
|
|
+-- digest mv = getBool b "True" "False"
|
|
+-- where
|
|
+-- b= case mv of
|
|
+-- Nothing -> Nothing
|
|
+-- Just bool -> Just $ case bool of
|
|
+-- True -> "True"
|
|
+-- False -> "False"
|
|
+
|
|
+-- | Display a dropdown box with the two values (second (true) and third parameter(false))
|
|
+-- . With the value of the first parameter selected.
|
|
+getBool :: (FormInput view,
|
|
+ Monad m, Functor m) =>
|
|
+ Bool -> String -> String -> View view m Bool
|
|
+getBool mv truestr falsestr= do
|
|
+ r <- getSelect $ setOption truestr (fromStr truestr) <! (if mv then [("selected","true")] else [])
|
|
+ <|> setOption falsestr(fromStr falsestr) <! if not mv then [("selected","true")] else []
|
|
+ if r == truestr then return True else return False
|
|
+
|
|
+
|
|
+
|
|
+-- | Display a dropdown box with the options in the first parameter is optionally selected
|
|
+-- . It returns the selected option.
|
|
+getSelect :: (FormInput view,
|
|
+ Monad m,Typeable a, Read a) =>
|
|
+ View view m (MFOption a) -> View view m a
|
|
+getSelect opts = View $ do
|
|
+ tolook <- genNewId
|
|
+ st <- get
|
|
+ let env = mfEnv st
|
|
+ put st{needForm= HasElems}
|
|
+ r <- getParam1 tolook env
|
|
+ setSessionData $ fmap MFOption $ valToMaybe r
|
|
+ FormElm form mr <- (runView opts)
|
|
+
|
|
+ return $ FormElm (fselect tolook form) $ valToMaybe r
|
|
+
|
|
+
|
|
+newtype MFOption a= MFOption a deriving Typeable
|
|
+
|
|
+instance (FormInput view,Monad m, Functor m) => Monoid (View view m (MFOption a)) where
|
|
+ mappend = (<|>)
|
|
+ mempty = Control.Applicative.empty
|
|
+
|
|
+-- | Set the option for getSelect. Options are concatenated with `<|>`
|
|
+setOption
|
|
+ :: (Monad m, Show a, Eq a, Typeable a, FormInput view) =>
|
|
+ a -> view -> View view m (MFOption a)
|
|
+setOption n v = do
|
|
+ mo <- getSessionData
|
|
+ case mo of
|
|
+ Nothing -> setOption1 n v False
|
|
+ Just Nothing -> setOption1 n v False
|
|
+ Just (Just (MFOption o)) -> setOption1 n v $ n == o
|
|
+
|
|
+-- | Set the selected option for getSelect. Options are concatenated with `<|>`
|
|
+setSelectedOption
|
|
+ :: (Monad m, Show a, Eq a, Typeable a, FormInput view) =>
|
|
+ a -> view -> View view m (MFOption a)
|
|
+setSelectedOption n v= do
|
|
+ mo <- getSessionData
|
|
+ case mo of
|
|
+ Nothing -> setOption1 n v True
|
|
+ Just Nothing -> setOption1 n v True
|
|
+ Just (Just o) -> setOption1 n v $ n == o
|
|
+
|
|
+
|
|
+setOption1 :: (FormInput view,
|
|
+ Monad m, Typeable a, Eq a, Show a) =>
|
|
+ a -> view -> Bool -> View view m (MFOption a)
|
|
+setOption1 nam val check= View $ do
|
|
+ st <- get
|
|
+ let env = mfEnv st
|
|
+ put st{needForm= HasElems}
|
|
+ let n = if typeOf nam == typeOf(undefined :: String)
|
|
+ then unsafeCoerce nam
|
|
+ else show nam
|
|
+
|
|
+ return . FormElm (foption n val check) . Just $ MFOption nam
|
|
+
|
|
+-- | upload a file to a temporary file in the server
|
|
+--
|
|
+-- The user can move, rename it etc.
|
|
+fileUpload :: (FormInput view,
|
|
+ Monad m,Functor m) =>
|
|
+ View view m (String
|
|
+ ,String
|
|
+ ,String
|
|
+ ) -- ^ ( original file, file type, temporal uploaded)
|
|
+fileUpload=
|
|
+ getParam Nothing "file" Nothing <** modify ( \st -> st{mfFileUpload = True})
|
|
+
|
|
+
|
|
+
|
|
+-- | Enclose Widgets within some formating.
|
|
+-- @view@ is intended to be instantiated to a particular format
|
|
+--
|
|
+-- NOTE: It has a infix priority : @infixr 5@ less than the one of @++>@ and @<++@ of the operators, so use parentheses when appropriate,
|
|
+-- unless the we want to enclose all the widgets in the right side.
|
|
+-- Most of the type errors in the DSL are due to the low priority of this operator.
|
|
+--
|
|
+-- This is a widget, which is a table with some links. it returns an Int
|
|
+--
|
|
+-- > import MFlow.Forms.Blaze.Html
|
|
+-- >
|
|
+-- > tableLinks :: View Html Int
|
|
+-- > table ! At.style "border:1;width:20%;margin-left:auto;margin-right:auto"
|
|
+-- > <<< caption << text "choose an item"
|
|
+-- > ++> thead << tr << ( th << b << text "item" <> th << b << text "times chosen")
|
|
+-- > ++> (tbody
|
|
+-- > <<< tr ! rowspan "2" << td << linkHome
|
|
+-- > ++> (tr <<< td <<< wlink IPhone (b << text "iphone") <++ td << ( b << text (fromString $ show ( cart V.! 0)))
|
|
+-- > <|> tr <<< td <<< wlink IPod (b << text "ipad") <++ td << ( b << text (fromString $ show ( cart V.! 1)))
|
|
+-- > <|> tr <<< td <<< wlink IPad (b << text "ipod") <++ td << ( b << text (fromString $ show ( cart V.! 2))))
|
|
+-- > )
|
|
+(<<<) :: (Monad m, Monoid view)
|
|
+ => (view ->view)
|
|
+ -> View view m a
|
|
+ -> View view m a
|
|
+(<<<) v form= View $ do
|
|
+ FormElm f mx <- runView form
|
|
+ return $ FormElm (v f) mx
|
|
+
|
|
+
|
|
+infixr 5 <<<
|
|
+
|
|
+
|
|
+
|
|
+
|
|
+
|
|
+
|
|
+-- | Append formatting code to a widget
|
|
+--
|
|
+-- @ getString "hi" '<++' H1 '<<' "hi there"@
|
|
+--
|
|
+-- It has a infix prority: @infixr 6@ higher than '<<<' and most other operators.
|
|
+(<++) :: (Monad m, Monoid v)
|
|
+ => View v m a
|
|
+ -> v
|
|
+ -> View v m a
|
|
+(<++) form v= View $ do
|
|
+ FormElm f mx <- runView form
|
|
+ return $ FormElm ( f <> v) mx
|
|
+
|
|
+infixr 6 ++>
|
|
+infixr 6 <++
|
|
+-- | Prepend formatting code to a widget
|
|
+--
|
|
+-- @bold '<<' "enter name" '++>' 'getString' 'Nothing' @
|
|
+--
|
|
+-- It has a infix prority: @infixr 6@ higher than '<<<' and most other operators
|
|
+(++>) :: (Monad m, Monoid view)
|
|
+ => view -> View view m a -> View view m a
|
|
+html ++> w = -- (html <>) <<< digest
|
|
+ View $ do
|
|
+ FormElm f mx <- runView w
|
|
+ return $ FormElm (html <> f) mx
|
|
+
|
|
+
|
|
+
|
|
+-- | Add attributes to the topmost tag of a widget
|
|
+--
|
|
+-- It has a fixity @infix 8@
|
|
+infixl 8 <!
|
|
+widget <! attribs= View $ do
|
|
+ FormElm fs mx <- runView widget
|
|
+ return $ FormElm (fs `attrs` attribs) mx -- (head fs `attrs` attribs:tail fs) mx
|
|
+-- case fs of
|
|
+-- [hfs] -> return $ FormElm [hfs `attrs` attribs] mx
|
|
+-- _ -> error $ "operator <! : malformed widget: "++ concatMap (unpack. toByteString) fs
|
|
+
|
|
+
|
|
+-- | Is an example of login\/register validation form needed by 'userWidget'. In this case
|
|
+-- the form field appears in a single line. it shows, in sequence, entries for the username,
|
|
+-- password, a button for loging, a entry to repeat password necesary for registering
|
|
+-- and a button for registering.
|
|
+-- The user can build its own user login\/validation forms by modifying this example
|
|
+--
|
|
+-- @ userFormLine=
|
|
+-- (User \<\$\> getString (Just \"enter user\") \<\*\> getPassword \<\+\> submitButton \"login\")
|
|
+-- \<\+\> fromStr \" password again\" \+\> getPassword \<\* submitButton \"register\"
|
|
+-- @
|
|
+userFormLine :: (FormInput view, Functor m, Monad m)
|
|
+ => View view m (Maybe (UserStr,PasswdStr), Maybe PasswdStr)
|
|
+userFormLine=
|
|
+ ((,) <$> getString (Just "enter user") <! [("size","5")]
|
|
+ <*> getPassword <! [("size","5")]
|
|
+ <** submitButton "login")
|
|
+ <+> (fromStr " password again" ++> getPassword <! [("size","5")]
|
|
+ <** submitButton "register")
|
|
+
|
|
+-- | Example of user\/password form (no validation) to be used with 'userWidget'
|
|
+userLogin :: (FormInput view, Functor m, Monad m)
|
|
+ => View view m (Maybe (UserStr,PasswdStr), Maybe String)
|
|
+userLogin=
|
|
+ ((,) <$> fromStr "Enter User: " ++> getString Nothing <! [("size","4")]
|
|
+ <*> fromStr " Enter Pass: " ++> getPassword <! [("size","4")]
|
|
+ <** submitButton "login")
|
|
+ <+> (noWidget
|
|
+ <* noWidget)
|
|
+
|
|
+
|
|
+
|
|
+-- | Empty widget that does not validate. May be used as \"empty boxes\" inside larger widgets.
|
|
+--
|
|
+-- It returns a non valid value.
|
|
+noWidget :: (FormInput view,
|
|
+ Monad m, Functor m) =>
|
|
+ View view m a
|
|
+noWidget= Control.Applicative.empty
|
|
|
|
-- | a sinonym of noWidget that can be used in a monadic expression in the View monad does not continue
|
|
-stop :: (FormInput view,
|
|
- Monad m, Functor m) =>
|
|
+stop :: (FormInput view,
|
|
+ Monad m, Functor m) =>
|
|
View view m a
|
|
stop= Control.Applicative.empty
|
|
-
|
|
--- | Render a Show-able value and return it
|
|
-wrender
|
|
- :: (Monad m, Functor m, Show a, FormInput view) =>
|
|
- a -> View view m a
|
|
-wrender x = (fromStr $ show x) ++> return x
|
|
-
|
|
--- | Render raw view formatting. It is useful for displaying information.
|
|
-wraw :: Monad m => view -> View view m ()
|
|
-wraw x= View . return . FormElm x $ Just ()
|
|
-
|
|
--- To display some rendering and return a no valid value
|
|
-notValid :: Monad m => view -> View view m a
|
|
-notValid x= View . return $ FormElm x Nothing
|
|
-
|
|
--- | Wether the user is logged or is anonymous
|
|
-isLogged :: MonadState (MFlowState v) m => m Bool
|
|
-isLogged= do
|
|
- rus <- return . tuser =<< gets mfToken
|
|
- return . not $ rus == anonymous
|
|
-
|
|
--- | return the result if going forward
|
|
---
|
|
--- If the process is backtraking, it does not validate,
|
|
--- in order to continue the backtracking
|
|
-returnIfForward :: (Monad m, FormInput view,Functor m) => b -> View view m b
|
|
-returnIfForward x = do
|
|
- back <- goingBack
|
|
- if back then noWidget else return x
|
|
-
|
|
--- | forces backtracking if the widget validates, because a previous page handle this widget response
|
|
--- . This is useful for recurrent cached widgets or `absLink`s that are present in multiple pages. For example
|
|
--- in the case of menus or common options. The active elements of this widget must be cached with no timeout.
|
|
-retry :: Monad m => View v m a -> View v m ()
|
|
-retry w = View $ do
|
|
- FormElm v mx <- runView w
|
|
- when (isJust mx) $ modify $ \st -> st{inSync = False}
|
|
- return $ FormElm v Nothing
|
|
-
|
|
--- | It creates a widget for user login\/registering. If a user name is specified
|
|
--- in the first parameter, it is forced to login\/password as this specific user.
|
|
--- If this user was already logged, the widget return the user without asking.
|
|
--- If the user press the register button, the new user-password is registered and the
|
|
--- user logged.
|
|
-
|
|
-userWidget :: ( MonadIO m, Functor m
|
|
- , FormInput view)
|
|
- => Maybe String
|
|
- -> View view m (Maybe (UserStr,PasswdStr), Maybe String)
|
|
- -> View view m String
|
|
-userWidget muser formuser = userWidget' muser formuser login1
|
|
-
|
|
--- | Uses 4 different keys to encrypt the 4 parts of a MFlow cookie.
|
|
-
|
|
-paranoidUserWidget muser formuser = userWidget' muser formuser paranoidLogin1
|
|
-
|
|
--- | Uses a single key to encrypt the MFlow cookie.
|
|
-
|
|
-encryptedUserWidget muser formuser = userWidget' muser formuser encryptedLogin1
|
|
-
|
|
-userWidget' muser formuser login1Func = do
|
|
- user <- getCurrentUser
|
|
- if muser== Just user || isNothing muser && user/= anonymous
|
|
- then returnIfForward user
|
|
- else formuser `validate` val muser `wcallback` login1Func
|
|
- where
|
|
- val _ (Nothing,_) = return . Just $ fromStr "Plese fill in the user/passwd to login, or user/passwd/passwd to register"
|
|
-
|
|
- val mu (Just us, Nothing)=
|
|
- if isNothing mu || isJust mu && fromJust mu == fst us
|
|
- then userValidate us
|
|
- else return . Just $ fromStr "This user has no permissions for this task"
|
|
-
|
|
- val mu (Just us, Just p)=
|
|
- if isNothing mu || isJust mu && fromJust mu == fst us
|
|
- then if Data.List.length p > 0 && snd us== p
|
|
- then return Nothing
|
|
- else return . Just $ fromStr "The passwords do not match"
|
|
- else return . Just $ fromStr "wrong user for the operation"
|
|
-
|
|
--- val _ _ = return . Just $ fromStr "Please fill in the fields for login or register"
|
|
-
|
|
-login1
|
|
- :: (MonadIO m, MonadState (MFlowState view) m) =>
|
|
- (Maybe (UserStr, PasswdStr), Maybe t) -> m UserStr
|
|
-login1 uname = login1' uname login
|
|
-
|
|
-paranoidLogin1 uname = login1' uname paranoidLogin
|
|
-
|
|
-encryptedLogin1 uname = login1' uname encryptedLogin
|
|
-
|
|
-login1' (Just (uname,_), Nothing) loginFunc= loginFunc uname >> return uname
|
|
-login1' (Just us@(u,p), Just _) loginFunc= do -- register button pressed
|
|
- userRegister u p
|
|
- loginFunc u
|
|
- return u
|
|
-
|
|
--- | change the user
|
|
---
|
|
--- It is supposed that the user has been validated
|
|
-
|
|
-login uname = login' uname setCookie
|
|
-
|
|
-paranoidLogin uname = login' uname setParanoidCookie
|
|
-
|
|
-encryptedLogin uname = login' uname setEncryptedCookie
|
|
-
|
|
-login'
|
|
- :: (Num a1, S.IsString a, MonadIO m,
|
|
- MonadState (MFlowState view) m) =>
|
|
- String -> (String -> String -> a -> Maybe a1 -> m ()) -> m ()
|
|
-login' uname setCookieFunc = do
|
|
- back <- goingBack
|
|
- if back then return () else do
|
|
- st <- get
|
|
- let t = mfToken st
|
|
- u = tuser t
|
|
- when (u /= uname) $ do
|
|
- let t'= t{tuser= uname}
|
|
- -- moveState (twfname t) t t'
|
|
- put st{mfToken= t'}
|
|
- liftIO $ deleteTokenInList t
|
|
- liftIO $ addTokenToList t'
|
|
- setCookieFunc cookieuser uname "/" (Just $ 365*24*60*60)
|
|
-
|
|
-logout = logout' setCookie
|
|
-
|
|
-paranoidLogout = logout' setParanoidCookie
|
|
-
|
|
-encryptedLogout = logout' setEncryptedCookie
|
|
-
|
|
-
|
|
--- | logout. The user is reset to the `anonymous` user
|
|
-logout'
|
|
- :: (Num a1,S.IsString a, MonadIO m,
|
|
- MonadState (MFlowState view) m) =>
|
|
- (String -> [Char] -> a -> Maybe a1 -> m ()) -> m ()
|
|
+
|
|
+-- | Render a Show-able value and return it
|
|
+wrender
|
|
+ :: (Monad m, Functor m, Show a, FormInput view) =>
|
|
+ a -> View view m a
|
|
+wrender x = (fromStr $ show x) ++> return x
|
|
+
|
|
+-- | Render raw view formatting. It is useful for displaying information.
|
|
+wraw :: Monad m => view -> View view m ()
|
|
+wraw x= View . return . FormElm x $ Just ()
|
|
+
|
|
+-- To display some rendering and return a no valid value
|
|
+notValid :: Monad m => view -> View view m a
|
|
+notValid x= View . return $ FormElm x Nothing
|
|
+
|
|
+-- | Wether the user is logged or is anonymous
|
|
+isLogged :: MonadState (MFlowState v) m => m Bool
|
|
+isLogged= do
|
|
+ rus <- return . tuser =<< gets mfToken
|
|
+ return . not $ rus == anonymous
|
|
+
|
|
+-- | return the result if going forward
|
|
+--
|
|
+-- If the process is backtraking, it does not validate,
|
|
+-- in order to continue the backtracking
|
|
+returnIfForward :: (Monad m, FormInput view,Functor m) => b -> View view m b
|
|
+returnIfForward x = do
|
|
+ back <- goingBack
|
|
+ if back then noWidget else return x
|
|
+
|
|
+-- | forces backtracking if the widget validates, because a previous page handle this widget response
|
|
+-- . This is useful for recurrent cached widgets or `absLink`s that are present in multiple pages. For example
|
|
+-- in the case of menus or common options. The active elements of this widget must be cached with no timeout.
|
|
+retry :: Monad m => View v m a -> View v m ()
|
|
+retry w = View $ do
|
|
+ FormElm v mx <- runView w
|
|
+ when (isJust mx) $ modify $ \st -> st{inSync = False}
|
|
+ return $ FormElm v Nothing
|
|
+
|
|
+-- | It creates a widget for user login\/registering. If a user name is specified
|
|
+-- in the first parameter, it is forced to login\/password as this specific user.
|
|
+-- If this user was already logged, the widget return the user without asking.
|
|
+-- If the user press the register button, the new user-password is registered and the
|
|
+-- user logged.
|
|
+
|
|
+userWidget :: ( MonadIO m, Functor m
|
|
+ , FormInput view)
|
|
+ => Maybe String
|
|
+ -> View view m (Maybe (UserStr,PasswdStr), Maybe String)
|
|
+ -> View view m String
|
|
+userWidget muser formuser = userWidget' muser formuser login1
|
|
+
|
|
+-- | Uses 4 different keys to encrypt the 4 parts of a MFlow cookie.
|
|
+
|
|
+paranoidUserWidget muser formuser = userWidget' muser formuser paranoidLogin1
|
|
+
|
|
+-- | Uses a single key to encrypt the MFlow cookie.
|
|
+
|
|
+encryptedUserWidget muser formuser = userWidget' muser formuser encryptedLogin1
|
|
+
|
|
+userWidget' muser formuser login1Func = do
|
|
+ user <- getCurrentUser
|
|
+ if muser== Just user || isNothing muser && user/= anonymous
|
|
+ then returnIfForward user
|
|
+ else formuser `validate` val muser `wcallback` login1Func
|
|
+ where
|
|
+ val _ (Nothing,_) = return . Just $ fromStr "Plese fill in the user/passwd to login, or user/passwd/passwd to register"
|
|
+
|
|
+ val mu (Just us, Nothing)=
|
|
+ if isNothing mu || isJust mu && fromJust mu == fst us
|
|
+ then userValidate us
|
|
+ else return . Just $ fromStr "This user has no permissions for this task"
|
|
+
|
|
+ val mu (Just us, Just p)=
|
|
+ if isNothing mu || isJust mu && fromJust mu == fst us
|
|
+ then if Data.List.length p > 0 && snd us== p
|
|
+ then return Nothing
|
|
+ else return . Just $ fromStr "The passwords do not match"
|
|
+ else return . Just $ fromStr "wrong user for the operation"
|
|
+
|
|
+-- val _ _ = return . Just $ fromStr "Please fill in the fields for login or register"
|
|
+
|
|
+login1
|
|
+ :: (MonadIO m, MonadState (MFlowState view) m) =>
|
|
+ (Maybe (UserStr, PasswdStr), Maybe t) -> m UserStr
|
|
+login1 uname = login1' uname login
|
|
+
|
|
+paranoidLogin1 uname = login1' uname paranoidLogin
|
|
+
|
|
+encryptedLogin1 uname = login1' uname encryptedLogin
|
|
+
|
|
+login1' (Just (uname,_), Nothing) loginFunc= loginFunc uname >> return uname
|
|
+login1' (Just us@(u,p), Just _) loginFunc= do -- register button pressed
|
|
+ userRegister u p
|
|
+ loginFunc u
|
|
+ return u
|
|
+
|
|
+-- | change the user
|
|
+--
|
|
+-- It is supposed that the user has been validated
|
|
+
|
|
+login uname = login' uname setCookie
|
|
+
|
|
+paranoidLogin uname = login' uname setParanoidCookie
|
|
+
|
|
+encryptedLogin uname = login' uname setEncryptedCookie
|
|
+
|
|
+login'
|
|
+ :: (Num a1, S.IsString a, MonadIO m,
|
|
+ MonadState (MFlowState view) m) =>
|
|
+ String -> (String -> String -> a -> Maybe a1 -> m ()) -> m ()
|
|
+login' uname setCookieFunc = do
|
|
+ back <- goingBack
|
|
+ if back then return () else do
|
|
+ st <- get
|
|
+ let t = mfToken st
|
|
+ u = tuser t
|
|
+ when (u /= uname) $ do
|
|
+ let t'= t{tuser= uname}
|
|
+ -- moveState (twfname t) t t'
|
|
+ put st{mfToken= t'}
|
|
+ liftIO $ deleteTokenInList t
|
|
+ liftIO $ addTokenToList t'
|
|
+ setCookieFunc cookieuser uname "/" (Just $ 365*24*60*60)
|
|
+
|
|
+logout = logout' setCookie
|
|
+
|
|
+paranoidLogout = logout' setParanoidCookie
|
|
+
|
|
+encryptedLogout = logout' setEncryptedCookie
|
|
+
|
|
+
|
|
+-- | logout. The user is reset to the `anonymous` user
|
|
+logout'
|
|
+ :: (Num a1,S.IsString a, MonadIO m,
|
|
+ MonadState (MFlowState view) m) =>
|
|
+ (String -> [Char] -> a -> Maybe a1 -> m ()) -> m ()
|
|
logout' setCookieFunc = do
|
|
- public
|
|
- back <- goingBack
|
|
- if back then return () else do
|
|
- st <- get
|
|
- let t = mfToken st
|
|
- t'= t{tuser= anonymous}
|
|
- when (tuser t /= anonymous) $ do
|
|
--- moveState (twfname t) t t'
|
|
- put st{mfToken= t'}
|
|
--- liftIO $ deleteTokenInList t
|
|
- liftIO $ addTokenToList t'
|
|
- setCookieFunc cookieuser anonymous "/" (Just $ -1000)
|
|
-
|
|
--- | If not logged, perform login. otherwise return the user
|
|
---
|
|
--- @getUserSimple= getUser Nothing userFormLine@
|
|
-getUserSimple :: ( FormInput view, Typeable view)
|
|
- => FlowM view IO String
|
|
-getUserSimple= getUser Nothing userFormLine
|
|
-
|
|
--- | Very basic user authentication. The user is stored in a cookie.
|
|
--- it looks for the cookie. If no cookie, it ask to the user for a `userRegister`ed
|
|
--- user-password combination.
|
|
--- The user-password combination is only asked if the user has not logged already
|
|
--- otherwise, the stored username is returned.
|
|
---
|
|
--- @getUser mu form= ask $ userWidget mu form@
|
|
-getUser :: ( FormInput view, Typeable view)
|
|
- => Maybe String
|
|
- -> View view IO (Maybe (UserStr,PasswdStr), Maybe String)
|
|
- -> FlowM view IO String
|
|
-getUser mu form= ask $ userWidget mu form
|
|
-
|
|
--- | Authentication against `userRegister`ed users.
|
|
--- to be used with `validate`
|
|
-userValidate :: (FormInput view,MonadIO m) => (UserStr,PasswdStr) -> m (Maybe view)
|
|
-userValidate (u,p) = liftIO $ do
|
|
- Auth _ val <- getAuthMethod
|
|
- val u p >>= return . fmap fromStr
|
|
-
|
|
-
|
|
-
|
|
--- | for compatibility with the same procedure in 'MFLow.Forms.Test.askt'.
|
|
--- This is the non testing version
|
|
---
|
|
--- > askt v w= ask w
|
|
---
|
|
--- hide one or the other
|
|
-askt :: FormInput v => (Int -> a) -> View v IO a -> FlowM v IO a
|
|
-askt v w = ask w
|
|
-
|
|
-
|
|
--- | It is the way to interact with the user.
|
|
--- It takes a widget and return the input result. If the widget is not validated (return @Nothing@)
|
|
--- , the page is presented again
|
|
---
|
|
--- If the environment or the URL has the parameters being looked at, maybe as a result of a previous interaction,
|
|
--- it will not ask to the user and return the result.
|
|
--- To force asking in any case, add an `clearEnv` statement before.
|
|
--- It also handles ajax requests
|
|
---
|
|
--- 'ask' also synchronizes the execution of the flow with the user page navigation by
|
|
-
|
|
--- * Backtracking (invoking previous 'ask' staement in the flow) when detecting mismatches between
|
|
--- get and post parameters and what is expected by the widgets
|
|
--- until a total or partial match is found.
|
|
---
|
|
--- * Advancing in the flow by matching a single requests with one or more sucessive ask statements
|
|
---
|
|
--- Backtracking and advancing can occur in a single request, so the flow in any state can reach any
|
|
--- other state in the flow if the request has the required parameters.
|
|
-ask :: (FormInput view) =>
|
|
- View view IO a -> FlowM view IO a
|
|
+ public
|
|
+ back <- goingBack
|
|
+ if back then return () else do
|
|
+ st <- get
|
|
+ let t = mfToken st
|
|
+ t'= t{tuser= anonymous}
|
|
+ when (tuser t /= anonymous) $ do
|
|
+-- moveState (twfname t) t t'
|
|
+ put st{mfToken= t'}
|
|
+-- liftIO $ deleteTokenInList t
|
|
+ liftIO $ addTokenToList t'
|
|
+ setCookieFunc cookieuser anonymous "/" (Just $ -1000)
|
|
+
|
|
+-- | If not logged, perform login. otherwise return the user
|
|
+--
|
|
+-- @getUserSimple= getUser Nothing userFormLine@
|
|
+getUserSimple :: ( FormInput view, Typeable view)
|
|
+ => FlowM view IO String
|
|
+getUserSimple= getUser Nothing userFormLine
|
|
+
|
|
+-- | Very basic user authentication. The user is stored in a cookie.
|
|
+-- it looks for the cookie. If no cookie, it ask to the user for a `userRegister`ed
|
|
+-- user-password combination.
|
|
+-- The user-password combination is only asked if the user has not logged already
|
|
+-- otherwise, the stored username is returned.
|
|
+--
|
|
+-- @getUser mu form= ask $ userWidget mu form@
|
|
+getUser :: ( FormInput view, Typeable view)
|
|
+ => Maybe String
|
|
+ -> View view IO (Maybe (UserStr,PasswdStr), Maybe String)
|
|
+ -> FlowM view IO String
|
|
+getUser mu form= ask $ userWidget mu form
|
|
+
|
|
+-- | Authentication against `userRegister`ed users.
|
|
+-- to be used with `validate`
|
|
+userValidate :: (FormInput view,MonadIO m) => (UserStr,PasswdStr) -> m (Maybe view)
|
|
+userValidate (u,p) = liftIO $ do
|
|
+ Auth _ val <- getAuthMethod
|
|
+ val u p >>= return . fmap fromStr
|
|
+
|
|
+
|
|
+
|
|
+-- | for compatibility with the same procedure in 'MFLow.Forms.Test.askt'.
|
|
+-- This is the non testing version
|
|
+--
|
|
+-- > askt v w= ask w
|
|
+--
|
|
+-- hide one or the other
|
|
+askt :: FormInput v => (Int -> a) -> View v IO a -> FlowM v IO a
|
|
+askt v w = ask w
|
|
+
|
|
+
|
|
+-- | It is the way to interact with the user.
|
|
+-- It takes a widget and return the input result. If the widget is not validated (return @Nothing@)
|
|
+-- , the page is presented again
|
|
+--
|
|
+-- If the environment or the URL has the parameters being looked at, maybe as a result of a previous interaction,
|
|
+-- it will not ask to the user and return the result.
|
|
+-- To force asking in any case, add an `clearEnv` statement before.
|
|
+-- It also handles ajax requests
|
|
+--
|
|
+-- 'ask' also synchronizes the execution of the flow with the user page navigation by
|
|
+
|
|
+-- * Backtracking (invoking previous 'ask' staement in the flow) when detecting mismatches between
|
|
+-- get and post parameters and what is expected by the widgets
|
|
+-- until a total or partial match is found.
|
|
+--
|
|
+-- * Advancing in the flow by matching a single requests with one or more sucessive ask statements
|
|
+--
|
|
+-- Backtracking and advancing can occur in a single request, so the flow in any state can reach any
|
|
+-- other state in the flow if the request has the required parameters.
|
|
+ask :: (FormInput view) =>
|
|
+ View view IO a -> FlowM view IO a
|
|
ask w = do
|
|
- resetCachePolicy
|
|
+ resetCachePolicy
|
|
st1 <- get >>= \s -> return s{mfSequence=
|
|
let seq= mfSequence s in
|
|
if seq ==inRecovery then 0 else seq
|
|
- ,mfHttpHeaders =[],mfAutorefresh= False }
|
|
- if not . null $ mfTrace st1 then fail "" else do
|
|
- -- AJAX
|
|
- let env= mfEnv st1
|
|
- mv1= lookup "ajax" env
|
|
- majax1= mfAjax st1
|
|
-
|
|
- case (majax1,mv1,M.lookup (fromJust mv1)(fromJust majax1), lookup "val" env) of
|
|
- (Just ajaxl,Just v1,Just f, Just v2) -> do
|
|
- FlowM . lift $ (unsafeCoerce f) v2
|
|
- FlowM $ lift nextMessage
|
|
- ask w
|
|
- -- END AJAX
|
|
-
|
|
+ ,mfHttpHeaders =[],mfAutorefresh= False }
|
|
+ if not . null $ mfTrace st1 then fail "" else do
|
|
+ -- AJAX
|
|
+ let env= mfEnv st1
|
|
+ mv1= lookup "ajax" env
|
|
+ majax1= mfAjax st1
|
|
+
|
|
+ case (majax1,mv1,M.lookup (fromJust mv1)(fromJust majax1), lookup "val" env) of
|
|
+ (Just ajaxl,Just v1,Just f, Just v2) -> do
|
|
+ FlowM . lift $ (unsafeCoerce f) v2
|
|
+ FlowM $ lift nextMessage
|
|
+ ask w
|
|
+ -- END AJAX
|
|
+
|
|
_ -> do
|
|
|
|
-- mfPagePath : contains the REST path of the page.
|
|
@@ -1044,252 +1052,250 @@
|
|
-- if exist and it is not prefix of the current path being navigated to, backtrack
|
|
else if not $ pagepath `isPrefixOf` mfPath st1 then fail "" -- !> ("pagepath fail with "++ show (mfPath st1))
|
|
else do
|
|
-
|
|
- let st= st1{needForm= NoElems, inSync= False, mfRequirements= [], linkMatched= False}
|
|
- put st
|
|
- FormElm forms mx <- FlowM . lift $ runView w
|
|
- setCachePolicy
|
|
- st' <- get
|
|
- if notSyncInAction st' then put st'{notSyncInAction=False}>> ask w
|
|
-
|
|
- else
|
|
- case mx of
|
|
- Just x -> do
|
|
- put st'{newAsk= True, mfEnv=[]}
|
|
- breturn x -- !> ("BRETURN "++ show (mfPagePath st') )
|
|
-
|
|
- Nothing ->
|
|
- if not (inSync st') && not (newAsk st')
|
|
- -- !> ("insync="++show (inSync st'))
|
|
- -- !> ("newask="++show (newAsk st'))
|
|
- then fail "" -- !> "FAIL sync"
|
|
- else if mfAutorefresh st' then do
|
|
- resetState st st' -- !> ("EN AUTOREFRESH" ++ show [ mfPagePath st,mfPath st,mfPagePath st'])
|
|
+
|
|
+ let st= st1{needForm= NoElems, inSync= False, linkMatched= False
|
|
+ ,mfRequirements= []
|
|
+ ,mfInstalledScripts= if newAsk st1 then [] else mfInstalledScripts st1}
|
|
+ put st
|
|
+ FormElm forms mx <- FlowM . lift $ runView w
|
|
+ setCachePolicy
|
|
+ st' <- get
|
|
+ if notSyncInAction st' then put st'{notSyncInAction=False}>> ask w
|
|
+
|
|
+ else
|
|
+ case mx of
|
|
+ Just x -> do
|
|
+ put st'{newAsk= True, mfEnv=[]}
|
|
+ breturn x -- !> ("BRETURN "++ show (mfPagePath st') )
|
|
+
|
|
+ Nothing ->
|
|
+ if not (inSync st') && not (newAsk st')
|
|
+ -- !> ("insync="++show (inSync st'))
|
|
+ -- !> ("newask="++show (newAsk st'))
|
|
+ then fail "" -- !> "FAIL sync"
|
|
+ else if mfAutorefresh st' then do
|
|
+ resetState st st' -- !> ("EN AUTOREFRESH" ++ show [ mfPagePath st,mfPath st,mfPagePath st'])
|
|
-- modify $ \st -> st{mfPagePath=mfPagePath st'} !> "REPEAT"
|
|
- FlowM $ lift nextMessage
|
|
- ask w
|
|
- else do
|
|
+ FlowM $ lift nextMessage
|
|
+ ask w
|
|
+ else do
|
|
reqs <- FlowM $ lift installAllRequirements -- !> "REPEAT"
|
|
-
|
|
- let header= mfHeader st'
|
|
- t= mfToken st'
|
|
- cont <- case (needForm1 st') of
|
|
- True -> do
|
|
- frm <- formPrefix st' forms False -- !> ("formPrefix="++ show(mfPagePath st'))
|
|
- return . header $ reqs <> frm
|
|
- _ -> return . header $ reqs <> forms
|
|
-
|
|
- let HttpData ctype c s= toHttpData cont
|
|
- liftIO . sendFlush t $ HttpData (ctype ++ mfHttpHeaders st') (mfCookies st' ++ c) s
|
|
-
|
|
-
|
|
- resetState st st'
|
|
+ st' <- get
|
|
+ let header= mfHeader st'
|
|
+ t= mfToken st'
|
|
+ cont <- case (needForm st') of
|
|
+ HasElems -> do
|
|
+ frm <- formPrefix st' forms False -- !> ("formPrefix="++ show(mfPagePath st'))
|
|
+ return . header $ reqs <> frm
|
|
+ _ -> return . header $ reqs <> forms
|
|
+
|
|
+ let HttpData ctype c s= toHttpData cont
|
|
+ liftIO . sendFlush t $ HttpData (ctype ++ mfHttpHeaders st') (mfCookies st' ++ c) s
|
|
+
|
|
+ resetState st st'
|
|
FlowM $ lift nextMessage -- !> "NEXTMESSAGE"
|
|
- ask w
|
|
- where
|
|
- resetState st st'=
|
|
- put st{mfCookies=[]
|
|
- -- ,mfHttpHeaders=[]
|
|
- ,newAsk= False
|
|
- ,mfToken= mfToken st'
|
|
- ,mfPageFlow= mfPageFlow st'
|
|
- ,mfAjax= mfAjax st'
|
|
--- ,mfSeqCache= mfSeqCache st'
|
|
- ,mfData= mfData st' }
|
|
-
|
|
-
|
|
--- | A synonym of ask.
|
|
---
|
|
--- Maybe more appropiate for pages with long interactions with the user
|
|
--- while the result has little importance.
|
|
-page
|
|
- :: (FormInput view) =>
|
|
- View view IO a -> FlowM view IO a
|
|
-page= ask
|
|
-
|
|
-nextMessage :: MonadIO m => WState view m ()
|
|
-nextMessage = do
|
|
- st <- get
|
|
- let t= mfToken st
|
|
- t1= mfkillTime st
|
|
- t2= mfSessionTime st
|
|
- msg <- liftIO ( receiveReqTimeout t1 t2 t)
|
|
- let req= getParams msg
|
|
- env= updateParams inPageFlow (mfEnv st) req -- !> ("PAGEFLOW="++ show inPageFlow)
|
|
- npath= pwfPath msg
|
|
- path= mfPath st
|
|
- inPageFlow= mfPagePath st `isPrefixOf` npath
|
|
-
|
|
- put st{ mfPath= npath
|
|
-
|
|
-
|
|
- , mfPageFlow= inPageFlow
|
|
-
|
|
- , mfEnv= env }
|
|
-
|
|
-
|
|
- where
|
|
-
|
|
- comparePaths _ n [] xs= n
|
|
- comparePaths o n _ [] = o
|
|
- comparePaths o n (v:path) (v': npath) | v== v' = comparePaths o (n+1)path npath
|
|
- | otherwise= n
|
|
-
|
|
- updateParams :: Bool -> Params -> Params -> Params
|
|
- updateParams False _ req= req
|
|
- updateParams True env req=
|
|
- let params= takeWhile isparam env
|
|
- fs= fst $ head req
|
|
- parms= (case findIndex (\p -> fst p == fs) params of
|
|
- Nothing -> params
|
|
- Just i -> Data.List.take i params)
|
|
- ++ req
|
|
- in parms
|
|
--- !> "IN PAGE FLOW" !> ("parms=" ++ show parms )
|
|
--- !> ("env=" ++ show env)
|
|
--- !> ("req=" ++ show req)
|
|
-
|
|
-
|
|
-
|
|
-isparam ('p': r:_,_)= isNumber r
|
|
-isparam ('c': r:_,_)= isNumber r
|
|
-isparam _= False
|
|
-
|
|
--- | Creates a stateless flow (see `stateless`) whose behaviour is defined as a widget. It is a
|
|
--- higuer level form of the latter
|
|
-wstateless
|
|
- :: (Typeable view, FormInput view) =>
|
|
- View view IO () -> Flow
|
|
-wstateless w = runFlow . transientNav . ask $ w **> (stop `asTypeOf` w)
|
|
-
|
|
-
|
|
-
|
|
-
|
|
-
|
|
-
|
|
--- | Wrap a widget with form element within a form-action element.
|
|
--- Usually this is not necessary since this wrapping is done automatically by the @Wiew@ monad, unless
|
|
--- there are more than one form in the page.
|
|
-wform :: (Monad m, FormInput view)
|
|
- => View view m b -> View view m b
|
|
-wform x = View $ do
|
|
- FormElm form mr <- (runView $ x )
|
|
- st <- get
|
|
- form1 <- formPrefix st form True
|
|
- put st{needForm=HasForm}
|
|
- return $ FormElm form1 mr
|
|
-
|
|
-
|
|
-
|
|
-
|
|
-resetButton :: (FormInput view, Monad m) => String -> View view m ()
|
|
+ ask w
|
|
+ where
|
|
+ resetState st st'=
|
|
+ put st{mfCookies=[]
|
|
+ ,mfInstalledScripts= mfInstalledScripts st'
|
|
+ ,newAsk= False
|
|
+ ,mfToken= mfToken st'
|
|
+ ,mfPageFlow= mfPageFlow st'
|
|
+ ,mfAjax= mfAjax st'
|
|
+ ,mfData= mfData st' }
|
|
+
|
|
+
|
|
+-- | A synonym of ask.
|
|
+--
|
|
+-- Maybe more appropiate for pages with long interactions with the user
|
|
+-- while the result has little importance.
|
|
+page
|
|
+ :: (FormInput view) =>
|
|
+ View view IO a -> FlowM view IO a
|
|
+page= ask
|
|
+
|
|
+nextMessage :: MonadIO m => WState view m ()
|
|
+nextMessage = do
|
|
+ st <- get
|
|
+ let t= mfToken st
|
|
+ t1= mfkillTime st
|
|
+ t2= mfSessionTime st
|
|
+ msg <- liftIO ( receiveReqTimeout t1 t2 t)
|
|
+ let req= getParams msg
|
|
+ env= updateParams inPageFlow (mfEnv st) req -- !> ("PAGEFLOW="++ show inPageFlow)
|
|
+ npath= pwfPath msg
|
|
+ path= mfPath st
|
|
+ inPageFlow= mfPagePath st `isPrefixOf` npath
|
|
+
|
|
+ put st{ mfPath= npath
|
|
+ , mfPageFlow= inPageFlow
|
|
+ , mfEnv= env }
|
|
+
|
|
+ where
|
|
+
|
|
+-- comparePaths _ n [] xs= n
|
|
+-- comparePaths o n _ [] = o
|
|
+-- comparePaths o n (v:path) (v': npath) | v== v' = comparePaths o (n+1)path npath
|
|
+-- | otherwise= n
|
|
+
|
|
+ updateParams :: Bool -> Params -> Params -> Params
|
|
+ updateParams False _ req= req
|
|
+ updateParams True env req=
|
|
+ let params= takeWhile isparam req -- env
|
|
+ fs= fst $ head req
|
|
+ parms= (case findIndex (\p -> fst p == fs) params of
|
|
+ Nothing -> params
|
|
+ Just i -> Data.List.take i params)
|
|
+ ++ req
|
|
+ in parms
|
|
+-- !> "IN PAGE FLOW" !> ("parms=" ++ show parms )
|
|
+-- !> ("env=" ++ show env)
|
|
+-- !> ("req=" ++ show req)
|
|
+
|
|
+
|
|
+
|
|
+isparam ('p': r:_,_)= isNumber r
|
|
+isparam ('c': r:_,_)= isNumber r
|
|
+isparam _= False
|
|
+
|
|
+-- | Creates a stateless flow (see `stateless`) whose behaviour is defined as a widget. It is a
|
|
+-- higuer level form of the latter
|
|
+wstateless
|
|
+ :: (Typeable view, FormInput view) =>
|
|
+ View view IO () -> Flow
|
|
+wstateless w = runFlow . transientNav . ask $ w **> (stop `asTypeOf` w)
|
|
+
|
|
+
|
|
+
|
|
+
|
|
+
|
|
+
|
|
+-- | Wrap a widget with form element within a form-action element.
|
|
+-- Usually this is not necessary since this wrapping is done automatically by the @Wiew@ monad, unless
|
|
+-- there are more than one form in the page.
|
|
+wform :: (Monad m, FormInput view)
|
|
+ => View view m b -> View view m b
|
|
+wform= insertForm
|
|
+--wform x = View $ do
|
|
+-- FormElm form mr <- (runView $ x )
|
|
+-- st <- get
|
|
+-- form1 <- formPrefix st form True
|
|
+-- put st{needForm=HasForm}
|
|
+-- return $ FormElm form1 mr
|
|
+
|
|
+
|
|
+
|
|
+
|
|
+resetButton :: (FormInput view, Monad m) => String -> View view m ()
|
|
resetButton label= View $ return $ FormElm (finput "reset" "reset" label False Nothing)
|
|
- $ Just ()
|
|
-
|
|
-submitButton :: (FormInput view, Monad m) => String -> View view m String
|
|
-submitButton label= getParam Nothing "submit" $ Just label
|
|
-
|
|
-newtype AjaxSessionId= AjaxSessionId String deriving Typeable
|
|
-
|
|
--- | Install the server code and return the client code for an AJAX interaction.
|
|
--- It is very lightweight, It does no t need jQuery.
|
|
---
|
|
--- This example increases the value of a text box each time the box is clicked
|
|
---
|
|
--- > ask $ do
|
|
--- > let elemval= "document.getElementById('text1').value"
|
|
--- > ajaxc <- ajax $ \n -> return $ elemval <> "='" <> B.pack(show(read n +1)) <> "'"
|
|
--- > b << text "click the box"
|
|
--- > ++> getInt (Just 0) <! [("id","text1"),("onclick", ajaxc elemval)]
|
|
-ajax :: (MonadIO m, FormInput v)
|
|
- => (String -> View v m ByteString) -- ^ user defined procedure, executed in the server.Receives the value of the javascript expression and must return another javascript expression that will be executed in the web browser
|
|
- -> View v m (String -> String) -- ^ returns a function that accept a javascript expression and return a javascript event handler expression that invokes the ajax server procedure
|
|
-ajax f = do
|
|
- requires[JScript ajaxScript]
|
|
- t <- gets mfToken
|
|
- id <- genNewId
|
|
- installServerControl id $ \x-> do
|
|
- setSessionData $ AjaxSessionId id
|
|
- r <- f x
|
|
- liftIO $ sendFlush t (HttpData [("Content-Type", "text/plain")][] r )
|
|
- return ()
|
|
-
|
|
-installServerControl :: (FormInput v,MonadIO m) => String -> (String -> View v m ()) -> View v m (String -> String)
|
|
-installServerControl id f= do
|
|
- t <- gets mfToken
|
|
- st <- get
|
|
- let ajxl = fromMaybe M.empty $ mfAjax st
|
|
- let ajxl'= M.insert id (unsafeCoerce f ) ajxl
|
|
- put st{mfAjax=Just ajxl'}
|
|
- return $ \param -> "doServer("++"'" ++ twfname t ++"','"++id++"',"++ param++")"
|
|
-
|
|
--- | Send the javascript expression, generated by the procedure parameter as a ByteString, execute it in the browser and the result is returned back
|
|
---
|
|
--- The @ajaxSend@ invocation must be inside a ajax procedure or else a /No ajax session set/ error will be produced
|
|
-ajaxSend
|
|
- :: (Read a,Monoid v, MonadIO m) => View v m ByteString -> View v m a
|
|
-ajaxSend cmd= View $ do
|
|
- AjaxSessionId id <- getSessionData `onNothing` error "no AjaxSessionId set"
|
|
- env <- getEnv
|
|
- t <- getToken
|
|
- case (lookup "ajax" $ env, lookup "val" env) of
|
|
- (Nothing,_) -> return $ FormElm mempty Nothing
|
|
- (Just id, Just _) -> do
|
|
- FormElm __ (Just str) <- runView cmd
|
|
- liftIO $ sendFlush t $ HttpData [("Content-Type", "text/plain")][] $ str <> readEvalLoop t id "''"
|
|
- nextMessage
|
|
- env <- getEnv
|
|
- case (lookup "ajax" $ env,lookup "val" env) of
|
|
- (Nothing,_) -> return $ FormElm mempty Nothing
|
|
- (Just id, Just v2) -> do
|
|
- return $ FormElm mempty . Just $ read v2
|
|
- where
|
|
- readEvalLoop t id v = "doServer('"<> fromString (twfname t)<>"','"<> fromString id<>"',"<>v<>");" :: ByteString
|
|
-
|
|
--- | Like @ajaxSend@ but the result is ignored
|
|
-ajaxSend_
|
|
- :: (MonadIO m, Monoid v) => View v m ByteString -> View v m ()
|
|
-ajaxSend_ = ajaxSend
|
|
-
|
|
-wlabel
|
|
- :: (Monad m, FormInput view) => view -> View view m a -> View view m a
|
|
-wlabel str w = do
|
|
- id <- genNewId
|
|
- ftag "label" str `attrs` [("for",id)] ++> w <! [("id",id)]
|
|
-
|
|
-
|
|
+ $ Just ()
|
|
+
|
|
+submitButton :: (FormInput view, Monad m) => String -> View view m String
|
|
+submitButton label= getParam Nothing "submit" $ Just label
|
|
+
|
|
+newtype AjaxSessionId= AjaxSessionId String deriving Typeable
|
|
+
|
|
+-- | Install the server code and return the client code for an AJAX interaction.
|
|
+-- It is very lightweight, It does no t need jQuery.
|
|
+--
|
|
+-- This example increases the value of a text box each time the box is clicked
|
|
+--
|
|
+-- > ask $ do
|
|
+-- > let elemval= "document.getElementById('text1').value"
|
|
+-- > ajaxc <- ajax $ \n -> return $ elemval <> "='" <> B.pack(show(read n +1)) <> "'"
|
|
+-- > b << text "click the box"
|
|
+-- > ++> getInt (Just 0) <! [("id","text1"),("onclick", ajaxc elemval)]
|
|
+ajax :: (MonadIO m, FormInput v)
|
|
+ => (String -> View v m ByteString) -- ^ user defined procedure, executed in the server.Receives the value of the javascript expression and must return another javascript expression that will be executed in the web browser
|
|
+ -> View v m (String -> String) -- ^ returns a function that accept a javascript expression and return a javascript event handler expression that invokes the ajax server procedure
|
|
+ajax f = do
|
|
+ requires[JScript ajaxScript]
|
|
+ t <- gets mfToken
|
|
+ id <- genNewId
|
|
+ installServerControl id $ \x-> do
|
|
+ setSessionData $ AjaxSessionId id
|
|
+ r <- f x
|
|
+ liftIO $ sendFlush t (HttpData [("Content-Type", "text/plain")][] r )
|
|
+ return ()
|
|
+
|
|
+installServerControl :: (FormInput v,MonadIO m) => String -> (String -> View v m ()) -> View v m (String -> String)
|
|
+installServerControl id f= do
|
|
+ t <- gets mfToken
|
|
+ st <- get
|
|
+ let ajxl = fromMaybe M.empty $ mfAjax st
|
|
+ let ajxl'= M.insert id (unsafeCoerce f ) ajxl
|
|
+ put st{mfAjax=Just ajxl'}
|
|
+ return $ \param -> "doServer("++"'" ++ twfname t ++"','"++id++"',"++ param++")"
|
|
+
|
|
+-- | Send the javascript expression, generated by the procedure parameter as a ByteString, execute it in the browser and the result is returned back
|
|
+--
|
|
+-- The @ajaxSend@ invocation must be inside a ajax procedure or else a /No ajax session set/ error will be produced
|
|
+ajaxSend
|
|
+ :: (Read a,Monoid v, MonadIO m) => View v m ByteString -> View v m a
|
|
+ajaxSend cmd= View $ do
|
|
+ AjaxSessionId id <- getSessionData `onNothing` error "no AjaxSessionId set"
|
|
+ env <- getEnv
|
|
+ t <- getToken
|
|
+ case (lookup "ajax" $ env, lookup "val" env) of
|
|
+ (Nothing,_) -> return $ FormElm mempty Nothing
|
|
+ (Just id, Just _) -> do
|
|
+ FormElm __ (Just str) <- runView cmd
|
|
+ liftIO $ sendFlush t $ HttpData [("Content-Type", "text/plain")][] $ str <> readEvalLoop t id "''"
|
|
+ nextMessage
|
|
+ env <- getEnv
|
|
+ case (lookup "ajax" $ env,lookup "val" env) of
|
|
+ (Nothing,_) -> return $ FormElm mempty Nothing
|
|
+ (Just id, Just v2) -> do
|
|
+ return $ FormElm mempty . Just $ read v2
|
|
+ where
|
|
+ readEvalLoop t id v = "doServer('"<> fromString (twfname t)<>"','"<> fromString id<>"',"<>v<>");" :: ByteString
|
|
+
|
|
+-- | Like @ajaxSend@ but the result is ignored
|
|
+ajaxSend_
|
|
+ :: (MonadIO m, Monoid v) => View v m ByteString -> View v m ()
|
|
+ajaxSend_ = ajaxSend
|
|
+
|
|
+wlabel
|
|
+ :: (Monad m, FormInput view) => view -> View view m a -> View view m a
|
|
+wlabel str w = do
|
|
+ id <- genNewId
|
|
+ ftag "label" str `attrs` [("for",id)] ++> w <! [("id",id)]
|
|
+
|
|
+
|
|
-- | Creates a link to a the next step within the flow.
|
|
-- A link can be composed with other widget elements.
|
|
-- It can not be broken by its own definition.
|
|
--- It points to the page that created it.
|
|
-wlink :: (Typeable a, Show a, MonadIO m, FormInput view)
|
|
- => a -> view -> View view m a
|
|
-wlink x v= View $ do
|
|
- verb <- getWFName
|
|
+-- It points to the page that created it.
|
|
+wlink :: (Typeable a, Show a, MonadIO m, FormInput view)
|
|
+ => a -> view -> View view m a
|
|
+wlink x v= View $ do
|
|
+ verb <- getWFName
|
|
st <- get
|
|
-
|
|
- let name = mfPrefix st ++ (map toLower $ if typeOf x== typeOf(undefined :: String)
|
|
- then unsafeCoerce x
|
|
- else show x)
|
|
+
|
|
+ let name = --mfPrefix st ++
|
|
+ (map toLower $ if typeOf x== typeOf(undefined :: String)
|
|
+ then unsafeCoerce x
|
|
+ else show x)
|
|
lpath = mfPath st
|
|
- newPath= mfPagePath st ++ [name]
|
|
-
|
|
- r <- if linkMatched st then return Nothing -- only a link match per page or monadic sentence in page
|
|
- else
|
|
- case newPath `isPrefixOf` lpath of
|
|
- True -> do
|
|
- modify $ \s -> s{inSync= True
|
|
+ newPath= mfPagePath st ++ [name]
|
|
+
|
|
+ r <- if linkMatched st then return Nothing -- only a link match per page or monadic sentence in page
|
|
+ else
|
|
+ case newPath `isPrefixOf` lpath of
|
|
+ True -> do
|
|
+ modify $ \s -> s{inSync= True
|
|
,linkMatched= True
|
|
- ,mfPagePath= newPath }
|
|
-
|
|
+ ,mfPagePath= newPath }
|
|
+
|
|
return $ Just x
|
|
--- !> (name ++ "<-" ++ "link path=" ++show newPath)
|
|
+-- !> (name ++ "<-" ++ "link path=" ++show newPath)
|
|
False -> return Nothing
|
|
-- !> ( "NOT MATCHED "++name++" link path= "++show newPath
|
|
--- ++ "path="++ show lpath)
|
|
+-- ++ "path="++ show lpath)
|
|
|
|
- let path= concat ['/':v| v <- newPath ]
|
|
- return $ FormElm (flink path v) r
|
|
+ let path= concat ['/':v| v <- newPath ]
|
|
+ return $ FormElm (flink path v) r
|
|
|
|
-- Creates an absolute link. While a `wlink` path depend on the page where it is located and
|
|
-- ever points to the code of the page that had it inserted, an absLink point to the first page
|
|
@@ -1313,299 +1319,300 @@
|
|
-- > p << "third statement" ++> (absLink "here" << p << "will present the first statement alone")
|
|
-- > p << "fourth statement" ++> wlink () << p << "will not reach here"
|
|
--absLink x = wcached (show x) 0 . wlink x
|
|
-absLink x v= View $ do
|
|
- verb <- getWFName
|
|
+absLink x v= View $ do
|
|
+ verb <- getWFName
|
|
st <- get
|
|
-
|
|
- let name = mfPrefix st ++ (map toLower $ if typeOf x== typeOf(undefined :: String)
|
|
- then unsafeCoerce x
|
|
- else show x)
|
|
+
|
|
+ let name = -- mfPrefix st
|
|
+ (map toLower $ if typeOf x== typeOf(undefined :: String)
|
|
+ then unsafeCoerce x
|
|
+ else show x)
|
|
|
|
lpath = mfPath st
|
|
- newPath= mfPagePath st ++ [name]
|
|
- r <- if linkMatched st then return Nothing -- only a link match per page or monadic sentence in page
|
|
- else
|
|
- case newPath `isPrefixOf` lpath of
|
|
- True -> do
|
|
- modify $ \s -> s{inSync= True
|
|
+ newPath= mfPagePath st ++ [name]
|
|
+ r <- if linkMatched st then return Nothing -- only a link match per page or monadic sentence in page
|
|
+ else
|
|
+ case newPath `isPrefixOf` lpath of
|
|
+ True -> do
|
|
+ modify $ \s -> s{inSync= True
|
|
,linkMatched= True
|
|
- ,mfPagePath= newPath }
|
|
-
|
|
- return $ Just x -- !> (name ++ "<- abs" ++ "lpath=" ++show lpath)
|
|
- False -> return Nothing -- !> ( "NOT MATCHED "++name++" LP= "++show lpath)
|
|
-
|
|
- path <- liftIO $ cachedByKey (show x) 0 . return $ currentPath st ++ ('/':name)
|
|
-
|
|
- return $ FormElm (flink path v) r -- !> name
|
|
-
|
|
-
|
|
-
|
|
--- | When some user interface return some response to the server, but it is not produced by
|
|
--- a form or a link, but for example by an script, @returning@ convert this code into a
|
|
--- widget.
|
|
---
|
|
--- At runtime the parameter is read from the environment and validated.
|
|
---
|
|
--- . The parameter is the visualization code, that accept a serialization function that generate
|
|
--- the server invocation string, used by the visualization to return the value by means
|
|
--- of an script, usually.
|
|
-returning :: (Typeable a, Read a, Show a,Monad m, FormInput view)
|
|
- => ((a->String) ->view) -> View view m a
|
|
-returning expr=View $ do
|
|
- verb <- getWFName
|
|
- name <- genNewId
|
|
- env <- gets mfEnv
|
|
- let string x=
|
|
- let showx= case cast x of
|
|
- Just x' -> x'
|
|
- _ -> show x
|
|
- in (verb ++ "?" ++ name ++ "=" ++ showx)
|
|
- toSend= expr string
|
|
- r <- getParam1 name env
|
|
- return $ FormElm toSend $ valToMaybe r
|
|
-
|
|
-
|
|
-
|
|
-
|
|
-
|
|
---instance (Widget a b m view, Monoid view) => Widget [a] b m view where
|
|
--- widget xs = View $ do
|
|
--- forms <- mapM(\x -> (runView $ widget x )) xs
|
|
--- let vs = concatMap (\(FormElm v _) -> v) forms
|
|
--- res = filter isJust $ map (\(FormElm _ r) -> r) forms
|
|
--- res1= if null res then Nothing else head res
|
|
--- return $ FormElm [mconcat vs] res1
|
|
-
|
|
--- | Concat a list of widgets of the same type, return a the first validated result
|
|
-firstOf :: (FormInput view, Monad m, Functor m)=> [View view m a] -> View view m a
|
|
+ ,mfPagePath= newPath }
|
|
+
|
|
+ return $ Just x -- !> (name ++ "<- abs" ++ "lpath=" ++show lpath)
|
|
+ False -> return Nothing -- !> ( "NOT MATCHED "++name++" LP= "++show lpath)
|
|
+
|
|
+ path <- liftIO $ cachedByKey (show x) 0 . return $ currentPath st ++ ('/':name)
|
|
+
|
|
+ return $ FormElm (flink path v) r -- !> name
|
|
+
|
|
+
|
|
+
|
|
+-- | When some user interface return some response to the server, but it is not produced by
|
|
+-- a form or a link, but for example by an script, @returning@ convert this code into a
|
|
+-- widget.
|
|
+--
|
|
+-- At runtime the parameter is read from the environment and validated.
|
|
+--
|
|
+-- . The parameter is the visualization code, that accept a serialization function that generate
|
|
+-- the server invocation string, used by the visualization to return the value by means
|
|
+-- of an script, usually.
|
|
+returning :: (Typeable a, Read a, Show a,Monad m, FormInput view)
|
|
+ => ((a->String) ->view) -> View view m a
|
|
+returning expr=View $ do
|
|
+ verb <- getWFName
|
|
+ name <- genNewId
|
|
+ env <- gets mfEnv
|
|
+ let string x=
|
|
+ let showx= case cast x of
|
|
+ Just x' -> x'
|
|
+ _ -> show x
|
|
+ in (verb ++ "?" ++ name ++ "=" ++ showx)
|
|
+ toSend= expr string
|
|
+ r <- getParam1 name env
|
|
+ return $ FormElm toSend $ valToMaybe r
|
|
+
|
|
+
|
|
+
|
|
+
|
|
+
|
|
+--instance (Widget a b m view, Monoid view) => Widget [a] b m view where
|
|
+-- widget xs = View $ do
|
|
+-- forms <- mapM(\x -> (runView $ widget x )) xs
|
|
+-- let vs = concatMap (\(FormElm v _) -> v) forms
|
|
+-- res = filter isJust $ map (\(FormElm _ r) -> r) forms
|
|
+-- res1= if null res then Nothing else head res
|
|
+-- return $ FormElm [mconcat vs] res1
|
|
+
|
|
+-- | Concat a list of widgets of the same type, return a the first validated result
|
|
+firstOf :: (FormInput view, Monad m, Functor m)=> [View view m a] -> View view m a
|
|
firstOf xs= foldl' (<|>) noWidget xs
|
|
--- View $ do
|
|
--- forms <- mapM runView xs
|
|
--- let vs = concatMap (\(FormElm v _) -> [mconcat v]) forms
|
|
--- res = filter isJust $ map (\(FormElm _ r) -> r) forms
|
|
--- res1= if null res then Nothing else head res
|
|
--- return $ FormElm vs res1
|
|
-
|
|
--- | from a list of widgets, it return the validated ones.
|
|
-manyOf :: (FormInput view, MonadIO m, Functor m)=> [View view m a] -> View view m [a]
|
|
-manyOf xs= whidden () *> (View $ do
|
|
- forms <- mapM runView xs
|
|
- let vs = mconcat $ map (\(FormElm v _) -> v) forms
|
|
- res1= catMaybes $ map (\(FormElm _ r) -> r) forms
|
|
- return . FormElm vs $ Just res1)
|
|
+-- View $ do
|
|
+-- forms <- mapM runView xs
|
|
+-- let vs = concatMap (\(FormElm v _) -> [mconcat v]) forms
|
|
+-- res = filter isJust $ map (\(FormElm _ r) -> r) forms
|
|
+-- res1= if null res then Nothing else head res
|
|
+-- return $ FormElm vs res1
|
|
+
|
|
+-- | from a list of widgets, it return the validated ones.
|
|
+manyOf :: (FormInput view, MonadIO m, Functor m)=> [View view m a] -> View view m [a]
|
|
+manyOf xs= whidden () *> (View $ do
|
|
+ forms <- mapM runView xs
|
|
+ let vs = mconcat $ map (\(FormElm v _) -> v) forms
|
|
+ res1= catMaybes $ map (\(FormElm _ r) -> r) forms
|
|
+ return . FormElm vs $ Just res1)
|
|
|
|
-- | like manyOf, but does not validate if one or more of the widgets does not validate
|
|
allOf xs= manyOf xs `validate` \rs ->
|
|
if length rs== length xs
|
|
then return Nothing
|
|
else return $ Just mempty
|
|
-
|
|
-(>:>) :: (Monad m, Monoid v) => View v m a -> View v m [a] -> View v m [a]
|
|
-(>:>) w ws = View $ do
|
|
- FormElm fs mxs <- runView $ ws
|
|
- FormElm f1 mx <- runView w
|
|
- return $ FormElm (f1 <> fs)
|
|
- $ case( mx,mxs) of
|
|
- (Just x, Just xs) -> Just $ x:xs
|
|
- (Nothing, mxs) -> mxs
|
|
- (Just x, _) -> Just [x]
|
|
-
|
|
--- | Intersperse a widget in a list of widgets. the results is a 2-tuple of both types.
|
|
---
|
|
--- it has a infix priority @infixr 5@
|
|
-(|*>) :: (MonadIO m, Functor m, FormInput view)
|
|
- => View view m r
|
|
- -> [View view m r']
|
|
- -> View view m (Maybe r,Maybe r')
|
|
-(|*>) x xs= View $ do
|
|
- fs <- mapM runView xs
|
|
- FormElm fx rx <- runView x
|
|
+
|
|
+(>:>) :: (Monad m, Monoid v) => View v m a -> View v m [a] -> View v m [a]
|
|
+(>:>) w ws = View $ do
|
|
+ FormElm fs mxs <- runView $ ws
|
|
+ FormElm f1 mx <- runView w
|
|
+ return $ FormElm (f1 <> fs)
|
|
+ $ case( mx,mxs) of
|
|
+ (Just x, Just xs) -> Just $ x:xs
|
|
+ (Nothing, mxs) -> mxs
|
|
+ (Just x, _) -> Just [x]
|
|
+
|
|
+-- | Intersperse a widget in a list of widgets. the results is a 2-tuple of both types.
|
|
+--
|
|
+-- it has a infix priority @infixr 5@
|
|
+(|*>) :: (MonadIO m, Functor m, FormInput view)
|
|
+ => View view m r
|
|
+ -> [View view m r']
|
|
+ -> View view m (Maybe r,Maybe r')
|
|
+(|*>) x xs= View $ do
|
|
+ fs <- mapM runView xs
|
|
+ FormElm fx rx <- runView x
|
|
let (fxs, rxss) = unzip $ map (\(FormElm v r) -> (v,r)) fs
|
|
rs= filter isJust rxss
|
|
- rxs= if null rs then Nothing else head rs
|
|
- return $ FormElm (fx <> mconcat (intersperse fx fxs) <> fx)
|
|
- $ case (rx,rxs) of
|
|
- (Nothing, Nothing) -> Nothing
|
|
- other -> Just other
|
|
-
|
|
-
|
|
-
|
|
-infixr 5 |*>
|
|
-
|
|
--- | Put a widget before and after other. Useful for navigation links in a page that appears at toAdd
|
|
--- and at the bottom of a page.
|
|
-
|
|
--- It has a low infix priority: @infixr 1@
|
|
-(|+|) :: (Functor m, FormInput view, MonadIO m)
|
|
- => View view m r
|
|
- -> View view m r'
|
|
- -> View view m (Maybe r, Maybe r')
|
|
-(|+|) w w'= w |*> [w']
|
|
-
|
|
-infixr 1 |+|
|
|
-
|
|
-
|
|
--- | Flatten a binary tree of tuples of Maybe results produced by the \<+> operator
|
|
--- into a single tuple with the same elements in the same order.
|
|
--- This is useful for easing matching. For example:
|
|
---
|
|
--- @ res \<- ask $ wlink1 \<+> wlink2 wform \<+> wlink3 \<+> wlink4@
|
|
---
|
|
--- @res@ has type:
|
|
---
|
|
--- @Maybe (Maybe (Maybe (Maybe (Maybe a,Maybe b),Maybe c),Maybe d),Maybe e)@
|
|
---
|
|
--- but @flatten res@ has type:
|
|
---
|
|
--- @ (Maybe a, Maybe b, Maybe c, Maybe d, Maybe e)@
|
|
-
|
|
-flatten :: Flatten (Maybe tree) list => tree -> list
|
|
-flatten res= doflat $ Just res
|
|
-
|
|
-class Flatten tree list where
|
|
- doflat :: tree -> list
|
|
-
|
|
-
|
|
-type Tuple2 a b= Maybe (Maybe a, Maybe b)
|
|
-type Tuple3 a b c= Maybe ( (Tuple2 a b), Maybe c)
|
|
-type Tuple4 a b c d= Maybe ( (Tuple3 a b c), Maybe d)
|
|
-type Tuple5 a b c d e= Maybe ( (Tuple4 a b c d), Maybe e)
|
|
-type Tuple6 a b c d e f= Maybe ( (Tuple5 a b c d e), Maybe f)
|
|
-
|
|
-instance Flatten (Tuple2 a b) (Maybe a, Maybe b) where
|
|
- doflat (Just(ma,mb))= (ma,mb)
|
|
- doflat Nothing= (Nothing,Nothing)
|
|
-
|
|
-instance Flatten (Tuple3 a b c) (Maybe a, Maybe b,Maybe c) where
|
|
- doflat (Just(mx,mc))= let(ma,mb)= doflat mx in (ma,mb,mc)
|
|
- doflat Nothing= (Nothing,Nothing,Nothing)
|
|
-
|
|
-instance Flatten (Tuple4 a b c d) (Maybe a, Maybe b,Maybe c,Maybe d) where
|
|
- doflat (Just(mx,mc))= let(ma,mb,md)= doflat mx in (ma,mb,md,mc)
|
|
- doflat Nothing= (Nothing,Nothing,Nothing,Nothing)
|
|
-
|
|
-instance Flatten (Tuple5 a b c d e) (Maybe a, Maybe b,Maybe c,Maybe d,Maybe e) where
|
|
- doflat (Just(mx,mc))= let(ma,mb,md,me)= doflat mx in (ma,mb,md,me,mc)
|
|
- doflat Nothing= (Nothing,Nothing,Nothing,Nothing,Nothing)
|
|
-
|
|
-instance Flatten (Tuple6 a b c d e f) (Maybe a, Maybe b,Maybe c,Maybe d,Maybe e,Maybe f) where
|
|
- doflat (Just(mx,mc))= let(ma,mb,md,me,mf)= doflat mx in (ma,mb,md,me,mf,mc)
|
|
- doflat Nothing= (Nothing,Nothing,Nothing,Nothing,Nothing,Nothing)
|
|
-
|
|
---infixr 7 .<<.
|
|
----- | > (.<<.) w x = w $ toByteString x
|
|
---(.<<.) :: (FormInput view) => (ByteString -> ByteString) -> view -> ByteString
|
|
---(.<<.) w x = w ( toByteString x)
|
|
---
|
|
----- | > (.<+>.) x y = normalize x <+> normalize y
|
|
---(.<+>.)
|
|
--- :: (Monad m, FormInput v, FormInput v1) =>
|
|
--- View v m a -> View v1 m b -> View ByteString m (Maybe a, Maybe b)
|
|
---(.<+>.) x y = normalize x <+> normalize y
|
|
---
|
|
----- | > (.|*>.) x y = normalize x |*> map normalize y
|
|
---(.|*>.)
|
|
--- :: (Functor m, MonadIO m, FormInput v, FormInput v1) =>
|
|
--- View v m r
|
|
--- -> [View v1 m r'] -> View ByteString m (Maybe r, Maybe r')
|
|
---(.|*>.) x y = normalize x |*> map normalize y
|
|
---
|
|
----- | > (.|+|.) x y = normalize x |+| normalize y
|
|
---(.|+|.)
|
|
--- :: (Functor m, MonadIO m, FormInput v, FormInput v1) =>
|
|
--- View v m r -> View v1 m r' -> View ByteString m (Maybe r, Maybe r')
|
|
---(.|+|.) x y = normalize x |+| normalize y
|
|
---
|
|
----- | > (.**>.) x y = normalize x **> normalize y
|
|
---(.**>.)
|
|
--- :: (Monad m, Functor m, FormInput v, FormInput v1) =>
|
|
--- View v m a -> View v1 m b -> View ByteString m b
|
|
---(.**>.) x y = normalize x **> normalize y
|
|
---
|
|
----- | > (.<**.) x y = normalize x <** normalize y
|
|
---(.<**.)
|
|
--- :: (Monad m, Functor m, FormInput v, FormInput v1) =>
|
|
--- View v m a -> View v1 m b -> View ByteString m a
|
|
---(.<**.) x y = normalize x <** normalize y
|
|
---
|
|
----- | > (.<|>.) x y= normalize x <|> normalize y
|
|
---(.<|>.)
|
|
--- :: (Monad m, Functor m, FormInput v, FormInput v1) =>
|
|
--- View v m a -> View v1 m a -> View ByteString m a
|
|
---(.<|>.) x y= normalize x <|> normalize y
|
|
---
|
|
----- | > (.<++.) x v= normalize x <++ toByteString v
|
|
---(.<++.) :: (Monad m, FormInput v, FormInput v') => View v m a -> v' -> View ByteString m a
|
|
---(.<++.) x v= normalize x <++ toByteString v
|
|
---
|
|
----- | > (.++>.) v x= toByteString v ++> normalize x
|
|
---(.++>.) :: (Monad m, FormInput v, FormInput v') => v -> View v' m a -> View ByteString m a
|
|
---(.++>.) v x= toByteString v ++> normalize x
|
|
-
|
|
-
|
|
-instance FormInput ByteString where
|
|
- toByteString= id
|
|
- toHttpData = HttpData [contentHtml ] []
|
|
- ftag x= btag x []
|
|
- inred = btag "b" [("style", "color:red")]
|
|
- finput n t v f c= btag "input" ([("type", t) ,("name", n),("value", v)] ++ if f then [("checked","true")] else []
|
|
- ++ case c of Just s ->[( "onclick", s)]; _ -> [] ) ""
|
|
- ftextarea name text= btag "textarea" [("name", name)] $ fromChunks [encodeUtf8 text]
|
|
-
|
|
- fselect name options= btag "select" [("name", name)] options
|
|
-
|
|
- foption value content msel= btag "option" ([("value", value)] ++ selected msel) content
|
|
- where
|
|
- selected msel = if msel then [("selected","true")] else []
|
|
-
|
|
- attrs = addAttrs
|
|
-
|
|
-
|
|
- formAction action form = btag "form" [("action", action),("method", "post")] form
|
|
- fromStr = fromString
|
|
- fromStrNoEncode= fromString
|
|
-
|
|
- flink v str = btag "a" [("href", v)] str
|
|
-
|
|
------- page Flows ----
|
|
-
|
|
--- | Prepares the state for a page flow. It add a prefix to every form element or link identifier for the formlets and also
|
|
--- keep the state of the links clicked and form imput entered within the widget.
|
|
--- If the computation within the widget has branches @if@ @case@ etc, each branch must have its pageFlow with a distinct identifier.
|
|
--- See <http://haskell-web.blogspot.com.es/2013/06/the-promising-land-of-monadic-formlets.html>
|
|
-pageFlow
|
|
- :: (Monad m, Functor m, FormInput view) =>
|
|
- String -> View view m a -> View view m a
|
|
-pageFlow str widget=do
|
|
- s <- get
|
|
-
|
|
- if mfPageFlow s == False
|
|
- then do
|
|
- put s{mfPrefix= str ++ mfPrefix s
|
|
- ,mfSequence=0
|
|
+ rxs= if null rs then Nothing else head rs
|
|
+ return $ FormElm (fx <> mconcat (intersperse fx fxs) <> fx)
|
|
+ $ case (rx,rxs) of
|
|
+ (Nothing, Nothing) -> Nothing
|
|
+ other -> Just other
|
|
+
|
|
+
|
|
+
|
|
+infixr 5 |*>
|
|
+
|
|
+-- | Put a widget before and after other. Useful for navigation links in a page that appears at toAdd
|
|
+-- and at the bottom of a page.
|
|
+
|
|
+-- It has a low infix priority: @infixr 1@
|
|
+(|+|) :: (Functor m, FormInput view, MonadIO m)
|
|
+ => View view m r
|
|
+ -> View view m r'
|
|
+ -> View view m (Maybe r, Maybe r')
|
|
+(|+|) w w'= w |*> [w']
|
|
+
|
|
+infixr 1 |+|
|
|
+
|
|
+
|
|
+-- | Flatten a binary tree of tuples of Maybe results produced by the \<+> operator
|
|
+-- into a single tuple with the same elements in the same order.
|
|
+-- This is useful for easing matching. For example:
|
|
+--
|
|
+-- @ res \<- ask $ wlink1 \<+> wlink2 wform \<+> wlink3 \<+> wlink4@
|
|
+--
|
|
+-- @res@ has type:
|
|
+--
|
|
+-- @Maybe (Maybe (Maybe (Maybe (Maybe a,Maybe b),Maybe c),Maybe d),Maybe e)@
|
|
+--
|
|
+-- but @flatten res@ has type:
|
|
+--
|
|
+-- @ (Maybe a, Maybe b, Maybe c, Maybe d, Maybe e)@
|
|
+
|
|
+flatten :: Flatten (Maybe tree) list => tree -> list
|
|
+flatten res= doflat $ Just res
|
|
+
|
|
+class Flatten tree list where
|
|
+ doflat :: tree -> list
|
|
+
|
|
+
|
|
+type Tuple2 a b= Maybe (Maybe a, Maybe b)
|
|
+type Tuple3 a b c= Maybe ( (Tuple2 a b), Maybe c)
|
|
+type Tuple4 a b c d= Maybe ( (Tuple3 a b c), Maybe d)
|
|
+type Tuple5 a b c d e= Maybe ( (Tuple4 a b c d), Maybe e)
|
|
+type Tuple6 a b c d e f= Maybe ( (Tuple5 a b c d e), Maybe f)
|
|
+
|
|
+instance Flatten (Tuple2 a b) (Maybe a, Maybe b) where
|
|
+ doflat (Just(ma,mb))= (ma,mb)
|
|
+ doflat Nothing= (Nothing,Nothing)
|
|
+
|
|
+instance Flatten (Tuple3 a b c) (Maybe a, Maybe b,Maybe c) where
|
|
+ doflat (Just(mx,mc))= let(ma,mb)= doflat mx in (ma,mb,mc)
|
|
+ doflat Nothing= (Nothing,Nothing,Nothing)
|
|
+
|
|
+instance Flatten (Tuple4 a b c d) (Maybe a, Maybe b,Maybe c,Maybe d) where
|
|
+ doflat (Just(mx,mc))= let(ma,mb,md)= doflat mx in (ma,mb,md,mc)
|
|
+ doflat Nothing= (Nothing,Nothing,Nothing,Nothing)
|
|
+
|
|
+instance Flatten (Tuple5 a b c d e) (Maybe a, Maybe b,Maybe c,Maybe d,Maybe e) where
|
|
+ doflat (Just(mx,mc))= let(ma,mb,md,me)= doflat mx in (ma,mb,md,me,mc)
|
|
+ doflat Nothing= (Nothing,Nothing,Nothing,Nothing,Nothing)
|
|
+
|
|
+instance Flatten (Tuple6 a b c d e f) (Maybe a, Maybe b,Maybe c,Maybe d,Maybe e,Maybe f) where
|
|
+ doflat (Just(mx,mc))= let(ma,mb,md,me,mf)= doflat mx in (ma,mb,md,me,mf,mc)
|
|
+ doflat Nothing= (Nothing,Nothing,Nothing,Nothing,Nothing,Nothing)
|
|
+
|
|
+--infixr 7 .<<.
|
|
+---- | > (.<<.) w x = w $ toByteString x
|
|
+--(.<<.) :: (FormInput view) => (ByteString -> ByteString) -> view -> ByteString
|
|
+--(.<<.) w x = w ( toByteString x)
|
|
+--
|
|
+---- | > (.<+>.) x y = normalize x <+> normalize y
|
|
+--(.<+>.)
|
|
+-- :: (Monad m, FormInput v, FormInput v1) =>
|
|
+-- View v m a -> View v1 m b -> View ByteString m (Maybe a, Maybe b)
|
|
+--(.<+>.) x y = normalize x <+> normalize y
|
|
+--
|
|
+---- | > (.|*>.) x y = normalize x |*> map normalize y
|
|
+--(.|*>.)
|
|
+-- :: (Functor m, MonadIO m, FormInput v, FormInput v1) =>
|
|
+-- View v m r
|
|
+-- -> [View v1 m r'] -> View ByteString m (Maybe r, Maybe r')
|
|
+--(.|*>.) x y = normalize x |*> map normalize y
|
|
+--
|
|
+---- | > (.|+|.) x y = normalize x |+| normalize y
|
|
+--(.|+|.)
|
|
+-- :: (Functor m, MonadIO m, FormInput v, FormInput v1) =>
|
|
+-- View v m r -> View v1 m r' -> View ByteString m (Maybe r, Maybe r')
|
|
+--(.|+|.) x y = normalize x |+| normalize y
|
|
+--
|
|
+---- | > (.**>.) x y = normalize x **> normalize y
|
|
+--(.**>.)
|
|
+-- :: (Monad m, Functor m, FormInput v, FormInput v1) =>
|
|
+-- View v m a -> View v1 m b -> View ByteString m b
|
|
+--(.**>.) x y = normalize x **> normalize y
|
|
+--
|
|
+---- | > (.<**.) x y = normalize x <** normalize y
|
|
+--(.<**.)
|
|
+-- :: (Monad m, Functor m, FormInput v, FormInput v1) =>
|
|
+-- View v m a -> View v1 m b -> View ByteString m a
|
|
+--(.<**.) x y = normalize x <** normalize y
|
|
+--
|
|
+---- | > (.<|>.) x y= normalize x <|> normalize y
|
|
+--(.<|>.)
|
|
+-- :: (Monad m, Functor m, FormInput v, FormInput v1) =>
|
|
+-- View v m a -> View v1 m a -> View ByteString m a
|
|
+--(.<|>.) x y= normalize x <|> normalize y
|
|
+--
|
|
+---- | > (.<++.) x v= normalize x <++ toByteString v
|
|
+--(.<++.) :: (Monad m, FormInput v, FormInput v') => View v m a -> v' -> View ByteString m a
|
|
+--(.<++.) x v= normalize x <++ toByteString v
|
|
+--
|
|
+---- | > (.++>.) v x= toByteString v ++> normalize x
|
|
+--(.++>.) :: (Monad m, FormInput v, FormInput v') => v -> View v' m a -> View ByteString m a
|
|
+--(.++>.) v x= toByteString v ++> normalize x
|
|
+
|
|
+
|
|
+instance FormInput ByteString where
|
|
+ toByteString= id
|
|
+ toHttpData = HttpData [contentHtml ] []
|
|
+ ftag x= btag x []
|
|
+ inred = btag "b" [("style", "color:red")]
|
|
+ finput n t v f c= btag "input" ([("type", t) ,("name", n),("value", v)] ++ if f then [("checked","true")] else []
|
|
+ ++ case c of Just s ->[( "onclick", s)]; _ -> [] ) ""
|
|
+ ftextarea name text= btag "textarea" [("name", name)] $ fromChunks [encodeUtf8 text]
|
|
+
|
|
+ fselect name options= btag "select" [("name", name)] options
|
|
+
|
|
+ foption value content msel= btag "option" ([("value", value)] ++ selected msel) content
|
|
+ where
|
|
+ selected msel = if msel then [("selected","true")] else []
|
|
+
|
|
+ attrs = addAttrs
|
|
+
|
|
+
|
|
+ formAction action method form = btag "form" [("action", action),("method", method)] form
|
|
+ fromStr = fromString
|
|
+ fromStrNoEncode= fromString
|
|
+
|
|
+ flink v str = btag "a" [("href", v)] str
|
|
+
|
|
+------ page Flows ----
|
|
+
|
|
+-- | Prepares the state for a page flow. It add a prefix to every form element or link identifier for the formlets and also
|
|
+-- keep the state of the links clicked and form imput entered within the widget.
|
|
+-- If the computation within the widget has branches @if@ @case@ etc, each branch must have its pageFlow with a distinct identifier.
|
|
+-- See <http://haskell-web.blogspot.com.es/2013/06/the-promising-land-of-monadic-formlets.html>
|
|
+pageFlow
|
|
+ :: (Monad m, Functor m, FormInput view) =>
|
|
+ String -> View view m a -> View view m a
|
|
+pageFlow str widget=do
|
|
+ s <- get
|
|
+
|
|
+ if mfPageFlow s == False
|
|
+ then do
|
|
+ put s{mfPrefix= str ++ mfPrefix s
|
|
+ ,mfSequence=0
|
|
,mfPageFlow= True
|
|
- } -- !> ("PARENT pageflow. prefix="++ str)
|
|
-
|
|
- r<- widget <** (modify (\s' -> s'{mfSequence= mfSequence s
|
|
+ } -- !> ("PARENT pageflow. prefix="++ str)
|
|
+
|
|
+ r<- widget <** (modify (\s' -> s'{mfSequence= mfSequence s
|
|
,mfPrefix= mfPrefix s
|
|
- }))
|
|
+ }))
|
|
modify (\s -> s{mfPageFlow=False} )
|
|
- return r -- !> ("END PARENT pageflow. prefix="++ str))
|
|
-
|
|
-
|
|
- else do
|
|
- put s{mfPrefix= str++ mfPrefix s,mfSequence=0} -- !> ("PARENT pageflow. prefix="++ str) -- !> ("CHILD pageflow. prefix="++ str)
|
|
-
|
|
- widget <** (modify (\s' -> s'{mfSequence= mfSequence s
|
|
- ,mfPrefix= mfPrefix s}))
|
|
- -- !> ("END CHILD pageflow. prefix="++ str))
|
|
-
|
|
-
|
|
-
|
|
---acum map []= map
|
|
---acum map (x:xs) =
|
|
--- let map' = case M.lookup x map of
|
|
--- Nothing -> M.insert x 1 map
|
|
--- Just n -> M.insert x (n+1) map
|
|
--- in acum map' xs
|
|
-
|
|
+ return r -- !> ("END PARENT pageflow. prefix="++ str))
|
|
+
|
|
+
|
|
+ else do
|
|
+ put s{mfPrefix= str++ mfPrefix s,mfSequence=0} -- !> ("PARENT pageflow. prefix="++ str) -- !> ("CHILD pageflow. prefix="++ str)
|
|
+
|
|
+ widget <** (modify (\s' -> s'{mfSequence= mfSequence s
|
|
+ ,mfPrefix= mfPrefix s}))
|
|
+ -- !> ("END CHILD pageflow. prefix="++ str))
|
|
+
|
|
+
|
|
+
|
|
+--acum map []= map
|
|
+--acum map (x:xs) =
|
|
+-- let map' = case M.lookup x map of
|
|
+-- Nothing -> M.insert x 1 map
|
|
+-- Just n -> M.insert x (n+1) map
|
|
+-- in acum map' xs
|
|
+
|
|
diff -ru orig/src/MFlow/Hack/Response.hs new/src/MFlow/Hack/Response.hs
|
|
--- orig/src/MFlow/Hack/Response.hs 2014-06-10 05:51:26.969015857 +0300
|
|
+++ new/src/MFlow/Hack/Response.hs 2014-06-10 05:51:25.000000000 +0300
|
|
@@ -1,13 +1,13 @@
|
|
{-# OPTIONS -XExistentialQuantification -XTypeSynonymInstances
|
|
- -XFlexibleInstances -XDeriveDataTypeable -XOverloadedStrings #-}
|
|
-module MFlow.Hack.Response where
|
|
-
|
|
+ -XFlexibleInstances -XDeriveDataTypeable -XOverloadedStrings #-}
|
|
+module MFlow.Hack.Response where
|
|
+
|
|
import Hack
|
|
-import MFlow.Cookies
|
|
+import MFlow.Cookies
|
|
import Data.ByteString.Lazy.Char8 as B
|
|
|
|
-import MFlow
|
|
-import Data.Typeable
|
|
+import MFlow
|
|
+import Data.Typeable
|
|
import Data.Monoid
|
|
import System.IO.Unsafe
|
|
import Data.Map as M
|
|
@@ -18,9 +18,9 @@
|
|
--(!>)= flip trace
|
|
|
|
|
|
-
|
|
-class ToResponse a where
|
|
- toResponse :: a -> Response
|
|
+
|
|
+class ToResponse a where
|
|
+ toResponse :: a -> Response
|
|
|
|
|
|
|
|
@@ -31,28 +31,28 @@
|
|
mappend (TResp x) (TResp y)=
|
|
case cast y of
|
|
Just y' -> TResp $ mappend x y'
|
|
- Nothing -> error $ "fragment of type " ++ show ( typeOf y) ++ " after fragment of type " ++ show ( typeOf x)
|
|
-
|
|
+ Nothing -> error $ "fragment of type " ++ show ( typeOf y) ++ " after fragment of type " ++ show ( typeOf x)
|
|
+
|
|
|
|
instance ToResponse TResp where
|
|
- toResponse (TResp x)= toResponse x
|
|
+ toResponse (TResp x)= toResponse x
|
|
toResponse (TRespR r)= toResponse r
|
|
-
|
|
-instance ToResponse Response where
|
|
- toResponse = id
|
|
-
|
|
-instance ToResponse ByteString where
|
|
- toResponse x= Response{status=200, headers=[contentHtml {-,("Content-Length",show $ B.length x) -}], body= x}
|
|
-
|
|
-instance ToResponse String where
|
|
+
|
|
+instance ToResponse Response where
|
|
+ toResponse = id
|
|
+
|
|
+instance ToResponse ByteString where
|
|
+ toResponse x= Response{status=200, headers=[contentHtml {-,("Content-Length",show $ B.length x) -}], body= x}
|
|
+
|
|
+instance ToResponse String where
|
|
toResponse x= Response{status=200, headers=[contentHtml{-,("Content-Length",show $ B.length x) -}], body= B.pack x}
|
|
|
|
instance ToResponse HttpData where
|
|
toResponse (HttpData hs cookies x)= (toResponse x) {headers= hs++ cookieHeaders cookies}
|
|
- toResponse (Error NotFound str)= Response{status=404, headers=[], body= getNotFoundResponse str}
|
|
-
|
|
-instance Typeable Env where
|
|
- typeOf = \_-> mkTyConApp (mkTyCon3 "hack-handler-simpleserver" "Hack" "Env") []
|
|
-
|
|
---instance Typeable Response where
|
|
--- typeOf = \_-> mkTyConApp (mkTyCon "Hack.Response")[]
|
|
+ toResponse (Error NotFound str)= Response{status=404, headers=[], body= getNotFoundResponse str}
|
|
+
|
|
+instance Typeable Env where
|
|
+ typeOf = \_-> mkTyConApp (mkTyCon3 "hack-handler-simpleserver" "Hack" "Env") []
|
|
+
|
|
+--instance Typeable Response where
|
|
+-- typeOf = \_-> mkTyConApp (mkTyCon "Hack.Response")[]
|
|
diff -ru orig/src/MFlow/Hack/XHtml.hs new/src/MFlow/Hack/XHtml.hs
|
|
--- orig/src/MFlow/Hack/XHtml.hs 2014-06-10 05:51:26.969015857 +0300
|
|
+++ new/src/MFlow/Hack/XHtml.hs 2014-06-10 05:51:25.000000000 +0300
|
|
@@ -27,14 +27,14 @@
|
|
import Text.XHtml
|
|
import Data.Typeable
|
|
import Data.ByteString.Lazy.Char8 as B(pack,unpack, length, ByteString)
|
|
-
|
|
-instance ToResponse Html where
|
|
+
|
|
+instance ToResponse Html where
|
|
toResponse x= Response{ status=200, headers=[]
|
|
- , Hack.body= pack $ showHtml x}
|
|
+ , Hack.body= pack $ showHtml x}
|
|
--
|
|
---instance Typeable Html where
|
|
--- typeOf = \_ -> mkTyConApp (mkTyCon "Text.XHtml.Strict.Html") []
|
|
+--instance Typeable Html where
|
|
+-- typeOf = \_ -> mkTyConApp (mkTyCon "Text.XHtml.Strict.Html") []
|
|
--
|
|
---instance ConvertTo Html TResp where
|
|
+--instance ConvertTo Html TResp where
|
|
-- convert = TResp
|
|
|
|
diff -ru orig/src/MFlow/Hack.hs new/src/MFlow/Hack.hs
|
|
--- orig/src/MFlow/Hack.hs 2014-06-10 05:51:26.957015857 +0300
|
|
+++ new/src/MFlow/Hack.hs 2014-06-10 05:51:25.000000000 +0300
|
|
@@ -8,33 +8,33 @@
|
|
|
|
see <http://hackage.haskell.org/package/hack>
|
|
-}
|
|
-
|
|
+
|
|
module MFlow.Hack(
|
|
module MFlow.Cookies
|
|
,module MFlow
|
|
,hackMessageFlow)
|
|
where
|
|
-
|
|
-import Data.Typeable
|
|
-import Hack
|
|
-
|
|
-import Control.Concurrent.MVar(modifyMVar_, readMVar)
|
|
-import Control.Monad(when)
|
|
-
|
|
-
|
|
-import Data.ByteString.Lazy.Char8 as B(pack, unpack, length, ByteString)
|
|
-import Control.Concurrent(ThreadId(..))
|
|
-import System.IO.Unsafe
|
|
+
|
|
+import Data.Typeable
|
|
+import Hack
|
|
+
|
|
+import Control.Concurrent.MVar(modifyMVar_, readMVar)
|
|
+import Control.Monad(when)
|
|
+
|
|
+
|
|
+import Data.ByteString.Lazy.Char8 as B(pack, unpack, length, ByteString)
|
|
+import Control.Concurrent(ThreadId(..))
|
|
+import System.IO.Unsafe
|
|
import Control.Concurrent.MVar
|
|
-import Control.Concurrent
|
|
-import Control.Exception
|
|
+import Control.Concurrent
|
|
+import Control.Exception
|
|
import qualified Data.Map as M
|
|
-import Data.Maybe
|
|
+import Data.Maybe
|
|
import Data.TCache
|
|
-import Data.TCache.DefaultPersistence
|
|
-import Control.Workflow hiding (Indexable(..))
|
|
-
|
|
-import MFlow
|
|
+import Data.TCache.DefaultPersistence
|
|
+import Control.Workflow hiding (Indexable(..))
|
|
+
|
|
+import MFlow
|
|
import MFlow.Cookies
|
|
|
|
import MFlow.Hack.Response
|
|
@@ -46,13 +46,13 @@
|
|
|
|
|
|
flow= "flow"
|
|
-
|
|
-instance Processable Env where
|
|
- pwfname env= if null sc then noScript else sc
|
|
- where
|
|
+
|
|
+instance Processable Env where
|
|
+ pwfname env= if null sc then noScript else sc
|
|
+ where
|
|
sc= tail $ pathInfo env
|
|
- puser env = fromMaybe anonymous $ lookup cookieuser $ http env
|
|
-
|
|
+ puser env = fromMaybe anonymous $ lookup cookieuser $ http env
|
|
+
|
|
pind env= fromMaybe (error ": No FlowID") $ lookup flow $ http env
|
|
getParams= http
|
|
-- getServer env= serverName env
|
|
@@ -60,38 +60,38 @@
|
|
-- getPort env= serverPort env
|
|
|
|
|
|
-
|
|
----------------------------------------------
|
|
-
|
|
-
|
|
-
|
|
---
|
|
---instance ConvertTo String TResp where
|
|
--- convert = TResp . pack
|
|
+
|
|
+---------------------------------------------
|
|
+
|
|
+
|
|
+
|
|
--
|
|
---instance ConvertTo ByteString TResp where
|
|
+--instance ConvertTo String TResp where
|
|
+-- convert = TResp . pack
|
|
+--
|
|
+--instance ConvertTo ByteString TResp where
|
|
-- convert = TResp
|
|
--
|
|
---
|
|
---instance ConvertTo Error TResp where
|
|
+--
|
|
+--instance ConvertTo Error TResp where
|
|
-- convert (Error e)= TResp . pack $ errorResponse e
|
|
--
|
|
--instance ToResponse v =>ConvertTo (HttpData v) TResp where
|
|
--- convert= TRespR
|
|
+-- convert= TRespR
|
|
+
|
|
+
|
|
+--webScheduler :: Env
|
|
+-- -> ProcList
|
|
+-- -> IO (TResp, ThreadId)
|
|
+--webScheduler = msgScheduler
|
|
+
|
|
+--theDir= unsafePerformIO getCurrentDirectory
|
|
+
|
|
+wFMiddleware :: (Env -> Bool) -> (Env-> IO Response) -> (Env -> IO Response)
|
|
+wFMiddleware filter f = \ env -> if filter env then hackMessageFlow env else f env -- !> "new message"
|
|
|
|
-
|
|
---webScheduler :: Env
|
|
--- -> ProcList
|
|
--- -> IO (TResp, ThreadId)
|
|
---webScheduler = msgScheduler
|
|
-
|
|
---theDir= unsafePerformIO getCurrentDirectory
|
|
-
|
|
-wFMiddleware :: (Env -> Bool) -> (Env-> IO Response) -> (Env -> IO Response)
|
|
-wFMiddleware filter f = \ env -> if filter env then hackMessageFlow env else f env -- !> "new message"
|
|
-
|
|
-- | An instance of the abstract "MFlow" scheduler to the Hack interface
|
|
--- it accept the list of processes being scheduled and return a hack handler
|
|
+-- it accept the list of processes being scheduled and return a hack handler
|
|
--
|
|
-- Example:
|
|
--
|
|
@@ -107,112 +107,112 @@
|
|
-- concat [ "http:\/\/server\/"++ i ++ "\n" | (i,_) \<- msgs]
|
|
-- @
|
|
--hackMessageFlow :: [(String, (Token -> Workflow IO ()))]
|
|
--- -> (Env -> IO Response)
|
|
---hackMessageFlow messageFlows =
|
|
--- unsafePerformIO (addMessageFlows messageFlows) `seq`
|
|
--- hackWorkflow -- wFMiddleware f other
|
|
+-- -> (Env -> IO Response)
|
|
+--hackMessageFlow messageFlows =
|
|
+-- unsafePerformIO (addMessageFlows messageFlows) `seq`
|
|
+-- hackWorkflow -- wFMiddleware f other
|
|
-- where
|
|
-- f env = unsafePerformIO $ do
|
|
-- paths <- getMessageFlows >>=
|
|
-- return (pwfname env `elem` paths)
|
|
|
|
--- other= (\env -> defaultResponse $ "options: " ++ opts)
|
|
+-- other= (\env -> defaultResponse $ "options: " ++ opts)
|
|
-- (paths,_)= unzip messageFlows
|
|
-- opts= concatMap (\s -> "<a href=\""++ s ++"\">"++s ++"</a>, ") paths
|
|
-
|
|
-
|
|
-splitPath ""= ("","","")
|
|
-splitPath str=
|
|
- let
|
|
- strr= reverse str
|
|
- (ext, rest)= span (/= '.') strr
|
|
- (mod, path)= span(/='/') $ tail rest
|
|
- in (tail $ reverse path, reverse mod, reverse ext)
|
|
-
|
|
-
|
|
-
|
|
-hackMessageFlow :: Env -> IO Response
|
|
-hackMessageFlow req1= do
|
|
- let httpreq1= http req1
|
|
- let cookies= {-# SCC "getCookies" #-} getCookies httpreq1
|
|
-
|
|
- (flowval , retcookies) <- case lookup ( flow) cookies of
|
|
- Just fl -> return (fl, [])
|
|
- Nothing -> do
|
|
- fl <- newFlow
|
|
+
|
|
+
|
|
+splitPath ""= ("","","")
|
|
+splitPath str=
|
|
+ let
|
|
+ strr= reverse str
|
|
+ (ext, rest)= span (/= '.') strr
|
|
+ (mod, path)= span(/='/') $ tail rest
|
|
+ in (tail $ reverse path, reverse mod, reverse ext)
|
|
+
|
|
+
|
|
+
|
|
+hackMessageFlow :: Env -> IO Response
|
|
+hackMessageFlow req1= do
|
|
+ let httpreq1= http req1
|
|
+ let cookies= {-# SCC "getCookies" #-} getCookies httpreq1
|
|
+
|
|
+ (flowval , retcookies) <- case lookup ( flow) cookies of
|
|
+ Just fl -> return (fl, [])
|
|
+ Nothing -> do
|
|
+ fl <- newFlow
|
|
return ( fl, [( flow, fl, "/",(Just $ show $ 365*24*60*60))])
|
|
-
|
|
-{- for state persistence in cookies
|
|
- putStateCookie req1 cookies
|
|
- let retcookies= case getStateCookie req1 of
|
|
- Nothing -> retcookies1
|
|
- Just ck -> ck:retcookies1
|
|
--}
|
|
-
|
|
- let input=
|
|
- case ( requestMethod req1, lookup "Content-Type" httpreq1 ) of
|
|
- (POST,Just "application/x-www-form-urlencoded") -> urlDecode . unpack $ hackInput req1
|
|
- (GET, _) -> urlDecode . queryString $ req1
|
|
- _ -> []
|
|
-
|
|
- let req = case retcookies of
|
|
- [] -> req1{http= (input ++ cookies) ++ http req1} -- !> "REQ"
|
|
- _ -> req1{http=(flow, flowval): ( input ++ cookies ) ++ http req1} -- !> "REQ"
|
|
-
|
|
-
|
|
+
|
|
+{- for state persistence in cookies
|
|
+ putStateCookie req1 cookies
|
|
+ let retcookies= case getStateCookie req1 of
|
|
+ Nothing -> retcookies1
|
|
+ Just ck -> ck:retcookies1
|
|
+-}
|
|
+
|
|
+ let input=
|
|
+ case ( requestMethod req1, lookup "Content-Type" httpreq1 ) of
|
|
+ (POST,Just "application/x-www-form-urlencoded") -> urlDecode . unpack $ hackInput req1
|
|
+ (GET, _) -> urlDecode . queryString $ req1
|
|
+ _ -> []
|
|
+
|
|
+ let req = case retcookies of
|
|
+ [] -> req1{http= (input ++ cookies) ++ http req1} -- !> "REQ"
|
|
+ _ -> req1{http=(flow, flowval): ( input ++ cookies ) ++ http req1} -- !> "REQ"
|
|
+
|
|
+
|
|
(resp',th) <- msgScheduler req
|
|
-
|
|
|
|
- let resp''= toResponse resp'
|
|
+
|
|
+ let resp''= toResponse resp'
|
|
let headers1= case retcookies of [] -> headers resp''; _ -> (cookieHeaders retcookies)
|
|
- let resp = resp''{status=200, headers= headers1 {-,("Content-Length",show $ B.length x) -}}
|
|
-
|
|
+ let resp = resp''{status=200, headers= headers1 {-,("Content-Length",show $ B.length x) -}}
|
|
+
|
|
return resp
|
|
|
|
-
|
|
-------persistent state in cookies (not tested)
|
|
-
|
|
-tvresources :: MVar (Maybe ( M.Map string string))
|
|
-tvresources= unsafePerformIO $ newMVar Nothing
|
|
-statCookieName= "stat"
|
|
-
|
|
-putStateCookie req cookies=
|
|
- case lookup statCookieName cookies of
|
|
- Nothing -> return ()
|
|
- Just (statCookieName, str , "/", _) -> modifyMVar_ tvresources $
|
|
- \mmap -> case mmap of
|
|
- Just map -> return $ Just $ M.insert (keyResource req) str map
|
|
- Nothing -> return $ Just $ M.fromList [((keyResource req), str) ]
|
|
-
|
|
-getStateCookie req= do
|
|
- mr<- readMVar tvresources
|
|
- case mr of
|
|
- Nothing -> return Nothing
|
|
- Just map -> case M.lookup (keyResource req) map of
|
|
- Nothing -> return Nothing
|
|
- Just str -> do
|
|
- swapMVar tvresources Nothing
|
|
- return $ Just (statCookieName, str , "/")
|
|
-
|
|
-{-
|
|
-persistInCookies= setPersist PersistStat{readStat=readResource, writeStat=writeResource, deleteStat=deleteResource}
|
|
- where
|
|
- writeResource stat= modifyMVar_ tvresources $ \mmap ->
|
|
- case mmap of
|
|
- Just map-> return $ Just $ M.insert (keyResource stat) (serialize stat) map
|
|
- Nothing -> return $ Just $ M.fromList [((keyResource stat), (serialize stat)) ]
|
|
- readResource stat= do
|
|
- mstr <- withMVar tvresources $ \mmap ->
|
|
- case mmap of
|
|
- Just map -> return $ M.lookup (keyResource stat) map
|
|
- Nothing -> return Nothing
|
|
- case mstr of
|
|
- Nothing -> return Nothing
|
|
- Just str -> return $ deserialize str
|
|
-
|
|
- deleteResource stat= modifyMVar_ tvresources $ \mmap->
|
|
- case mmap of
|
|
- Just map -> return $ Just $ M.delete (keyResource stat) map
|
|
- Nothing -> return $ Nothing
|
|
+
|
|
+------persistent state in cookies (not tested)
|
|
+
|
|
+tvresources :: MVar (Maybe ( M.Map string string))
|
|
+tvresources= unsafePerformIO $ newMVar Nothing
|
|
+statCookieName= "stat"
|
|
+
|
|
+putStateCookie req cookies=
|
|
+ case lookup statCookieName cookies of
|
|
+ Nothing -> return ()
|
|
+ Just (statCookieName, str , "/", _) -> modifyMVar_ tvresources $
|
|
+ \mmap -> case mmap of
|
|
+ Just map -> return $ Just $ M.insert (keyResource req) str map
|
|
+ Nothing -> return $ Just $ M.fromList [((keyResource req), str) ]
|
|
+
|
|
+getStateCookie req= do
|
|
+ mr<- readMVar tvresources
|
|
+ case mr of
|
|
+ Nothing -> return Nothing
|
|
+ Just map -> case M.lookup (keyResource req) map of
|
|
+ Nothing -> return Nothing
|
|
+ Just str -> do
|
|
+ swapMVar tvresources Nothing
|
|
+ return $ Just (statCookieName, str , "/")
|
|
+
|
|
+{-
|
|
+persistInCookies= setPersist PersistStat{readStat=readResource, writeStat=writeResource, deleteStat=deleteResource}
|
|
+ where
|
|
+ writeResource stat= modifyMVar_ tvresources $ \mmap ->
|
|
+ case mmap of
|
|
+ Just map-> return $ Just $ M.insert (keyResource stat) (serialize stat) map
|
|
+ Nothing -> return $ Just $ M.fromList [((keyResource stat), (serialize stat)) ]
|
|
+ readResource stat= do
|
|
+ mstr <- withMVar tvresources $ \mmap ->
|
|
+ case mmap of
|
|
+ Just map -> return $ M.lookup (keyResource stat) map
|
|
+ Nothing -> return Nothing
|
|
+ case mstr of
|
|
+ Nothing -> return Nothing
|
|
+ Just str -> return $ deserialize str
|
|
+
|
|
+ deleteResource stat= modifyMVar_ tvresources $ \mmap->
|
|
+ case mmap of
|
|
+ Just map -> return $ Just $ M.delete (keyResource stat) map
|
|
+ Nothing -> return $ Nothing
|
|
|
|
-}
|
|
diff -ru orig/src/MFlow/Wai/Blaze/Html/All.hs new/src/MFlow/Wai/Blaze/Html/All.hs
|
|
--- orig/src/MFlow/Wai/Blaze/Html/All.hs 2014-06-10 05:51:26.969015857 +0300
|
|
+++ new/src/MFlow/Wai/Blaze/Html/All.hs 2014-06-10 05:51:25.000000000 +0300
|
|
@@ -1,97 +1,99 @@
|
|
-----------------------------------------------------------------------------
|
|
---
|
|
--- Module : MFlow.Wai.Blaze.Html.All
|
|
--- Copyright :
|
|
--- License : BSD3
|
|
---
|
|
--- Maintainer : agocorona@gmail.com
|
|
--- Stability : experimental
|
|
--- Portability :
|
|
---
|
|
--- |
|
|
---
|
|
------------------------------------------------------------------------------
|
|
-
|
|
-module MFlow.Wai.Blaze.Html.All (
|
|
- module Data.TCache
|
|
-,module MFlow
|
|
-,module MFlow.Forms
|
|
-,module MFlow.Forms.Widgets
|
|
-,module MFlow.Forms.Blaze.Html
|
|
-,module MFlow.Forms.Admin
|
|
-,module Control.Applicative
|
|
-,module Text.Blaze.Html5
|
|
-,module Text.Blaze.Html5.Attributes
|
|
-,module Control.Monad.IO.Class
|
|
-,module MFlow.Forms.WebApi
|
|
-,module MFlow.Forms.Cache
|
|
-,runNavigation
|
|
-,runSecureNavigation
|
|
-) where
|
|
-
|
|
-import MFlow
|
|
-import MFlow.Wai
|
|
-import MFlow.Forms
|
|
-import MFlow.Forms.Widgets
|
|
-import MFlow.Forms.Admin
|
|
-import MFlow.Forms.Blaze.Html
|
|
-import MFlow.Forms.WebApi
|
|
-import MFlow.Forms.Cache
|
|
-import Text.Blaze.Html5 hiding (map)
|
|
-import Text.Blaze.Html5.Attributes hiding (label,span,style,cite,title,summary,step,form)
|
|
-import Network.Wai
|
|
-import Network.Wai.Handler.Warp hiding (getPort) --(run,defaultSettings,Settings ,setPort)
|
|
-import Data.TCache
|
|
-import Text.Blaze.Internal(text)
|
|
-
|
|
-import Control.Workflow (Workflow, unsafeIOtoWF)
|
|
-
|
|
-
|
|
-import Control.Applicative
|
|
-import Control.Monad(when, unless)
|
|
-import Control.Monad.IO.Class
|
|
-import System.Environment
|
|
-import Data.Maybe(fromMaybe)
|
|
-import Data.Char(isNumber)
|
|
-import Network.Wai.Handler.WarpTLS as TLS
|
|
-
|
|
--- | The port is read from the first exectution parameter.
|
|
--- If no parameter, it is read from the PORT environment variable.
|
|
--- if this does not exist, the port 80 is used.
|
|
-getPort= do
|
|
- args <- getArgs
|
|
- port <- case args of
|
|
- port:xs -> return port
|
|
- _ -> do
|
|
- env <- getEnvironment
|
|
- return $ fromMaybe "80" $ lookup "PORT" env
|
|
- let porti= if and $ map isNumber port then fromIntegral $ read port
|
|
- else 80
|
|
- putStr "using port "
|
|
- print porti
|
|
- return porti
|
|
-
|
|
--- | run a persistent flow. It uses `getPort` to get the port
|
|
--- The first parameter is the first element in the URL path.
|
|
--- It also set the home page
|
|
-runNavigation :: String -> FlowM Html (Workflow IO) () -> IO ()
|
|
-runNavigation n f= do
|
|
- unless (null n) $ setNoScript n
|
|
- addMessageFlows[(n, runFlow f)]
|
|
- porti <- getPort
|
|
- wait $ run porti waiMessageFlow
|
|
- --runSettings defaultSettings{settingsTimeout = 20, settingsPort= porti} waiMessageFlow
|
|
-
|
|
--- | Exactly the same as runNavigation, but with TLS added.
|
|
--- Expects certificate.pem and key.pem in project directory.
|
|
-
|
|
-runSecureNavigation = runSecureNavigation' TLS.defaultTlsSettings defaultSettings
|
|
-
|
|
-runSecureNavigation' :: TLSSettings -> Settings -> String -> FlowM Html (Workflow IO) () -> IO ()
|
|
-runSecureNavigation' t s n f = do
|
|
- unless (null n) $ setNoScript n
|
|
- addMessageFlows[(n, runFlow f)]
|
|
- porti <- getPort
|
|
--- let s' = setPort porti s
|
|
+----------------------------------------------------------------------------
|
|
+--
|
|
+-- Module : MFlow.Wai.Blaze.Html.All
|
|
+-- Copyright :
|
|
+-- License : BSD3
|
|
+--
|
|
+-- Maintainer : agocorona@gmail.com
|
|
+-- Stability : experimental
|
|
+-- Portability :
|
|
+--
|
|
+-- |
|
|
+--
|
|
+-----------------------------------------------------------------------------
|
|
+
|
|
+module MFlow.Wai.Blaze.Html.All (
|
|
+ module Data.TCache
|
|
+,module MFlow
|
|
+,module MFlow.Forms
|
|
+,module MFlow.Forms.Widgets
|
|
+,module MFlow.Forms.Blaze.Html
|
|
+,module MFlow.Forms.Admin
|
|
+,module Control.Applicative
|
|
+,module Text.Blaze.Html5
|
|
+,module Text.Blaze.Html5.Attributes
|
|
+,module Control.Monad.IO.Class
|
|
+,module MFlow.Forms.WebApi
|
|
+,module MFlow.Forms.Cache
|
|
+,runNavigation
|
|
+,runSecureNavigation
|
|
+,runSecureNavigation'
|
|
+) where
|
|
+
|
|
+import MFlow
|
|
+import MFlow.Wai
|
|
+import MFlow.Forms
|
|
+import MFlow.Forms.Widgets
|
|
+import MFlow.Forms.Admin
|
|
+import MFlow.Forms.Blaze.Html
|
|
+import MFlow.Forms.WebApi
|
|
+import MFlow.Forms.Cache
|
|
+import Text.Blaze.Html5 hiding (map)
|
|
+import Text.Blaze.Html5.Attributes hiding (label,span,style,cite,title,summary,step,form)
|
|
+import Network.Wai
|
|
+import Network.Wai.Handler.Warp --(run,defaultSettings,Settings ,setPort)
|
|
+import Data.TCache
|
|
+import Text.Blaze.Internal(text)
|
|
+
|
|
+import Control.Workflow (Workflow, unsafeIOtoWF)
|
|
+
|
|
+
|
|
+import Control.Applicative
|
|
+import Control.Monad(when, unless)
|
|
+import Control.Monad.IO.Class
|
|
+import System.Environment
|
|
+import Data.Maybe(fromMaybe)
|
|
+import Data.Char(isNumber)
|
|
+import Network.Wai.Handler.WarpTLS as TLS
|
|
+
|
|
+
|
|
+getPortW= do
|
|
+ args <- getArgs
|
|
+ port <- case args of
|
|
+ port:xs -> return port
|
|
+ _ -> do
|
|
+ env <- getEnvironment
|
|
+ return $ fromMaybe "80" $ lookup "PORT" env
|
|
+ let porti= if and $ map isNumber port then fromIntegral $ read port
|
|
+ else 80
|
|
+ putStr "using port "
|
|
+ print porti
|
|
+ return porti
|
|
+
|
|
+-- | run a persistent flow. It uses `getPortW` to get the port
|
|
+-- The first parameter is the first element in the URL path.
|
|
+-- It also set the home page
|
|
+-- The port is read from the first parameter passed to the executable.
|
|
+-- If no parameter, it is read from the PORT environment variable.
|
|
+-- if this does not exist, the port 80 is used.
|
|
+runNavigation :: String -> FlowM Html (Workflow IO) () -> IO ()
|
|
+runNavigation n f= do
|
|
+ unless (null n) $ setNoScript n
|
|
+ addMessageFlows[(n, runFlow f)]
|
|
+ porti <- getPortW
|
|
+ wait $ run porti waiMessageFlow
|
|
+ --runSettings defaultSettings{settingsTimeout = 20, settingsPort= porti} waiMessageFlow
|
|
+
|
|
+-- | Exactly the same as runNavigation, but with TLS added.
|
|
+-- Expects certificate.pem and key.pem in project directory.
|
|
+
|
|
+runSecureNavigation = runSecureNavigation' TLS.defaultTlsSettings defaultSettings
|
|
+
|
|
+runSecureNavigation' :: TLSSettings -> Settings -> String -> FlowM Html (Workflow IO) () -> IO ()
|
|
+runSecureNavigation' t s n f = do
|
|
+ unless (null n) $ setNoScript n
|
|
+ addMessageFlows[(n, runFlow f)]
|
|
+ porti <- getPortW
|
|
+-- let s' = setPort porti s
|
|
-- wait $ TLS.runTLS t s' waiMessageFlow
|
|
- wait $ TLS.runTLS t s{settingsPort = porti} waiMessageFlow
|
|
+ wait $ TLS.runTLS t s{settingsPort = porti} waiMessageFlow
|
|
diff -ru orig/src/MFlow/Wai/Response.hs new/src/MFlow/Wai/Response.hs
|
|
--- orig/src/MFlow/Wai/Response.hs 2014-06-10 05:51:26.965015857 +0300
|
|
+++ new/src/MFlow/Wai/Response.hs 2014-06-10 05:51:25.000000000 +0300
|
|
@@ -1,62 +1,62 @@
|
|
-{-# OPTIONS -XExistentialQuantification -XTypeSynonymInstances
|
|
- -XFlexibleInstances -XDeriveDataTypeable -XOverloadedStrings #-}
|
|
-module MFlow.Wai.Response where
|
|
-
|
|
-import Network.Wai
|
|
-import MFlow.Cookies
|
|
-
|
|
-import Data.ByteString.Lazy.UTF8
|
|
-import MFlow
|
|
-import Data.Typeable
|
|
-import Data.Monoid
|
|
-import System.IO.Unsafe
|
|
-import Data.Map as M
|
|
-import Data.CaseInsensitive
|
|
-import Network.HTTP.Types
|
|
-import Control.Workflow(WFErrors(..))
|
|
---import Data.String
|
|
---import Debug.Trace
|
|
---
|
|
---(!>)= flip trace
|
|
-
|
|
-
|
|
-
|
|
-class ToResponse a where
|
|
- toResponse :: a -> Response
|
|
-
|
|
-
|
|
-
|
|
-data TResp = TRempty | forall a.ToResponse a=>TRespR a | forall a.(Typeable a, ToResponse a, Monoid a) => TResp a deriving Typeable
|
|
-
|
|
-instance Monoid TResp where
|
|
- mempty = TRempty
|
|
- mappend (TResp x) (TResp y)=
|
|
- case cast y of
|
|
- Just y' -> TResp $ mappend x y'
|
|
- Nothing -> error $ "fragment of type " ++ show ( typeOf y) ++ " after fragment of type " ++ show ( typeOf x)
|
|
-
|
|
-
|
|
-mkParams = Prelude.map mkparam
|
|
-mkparam (x,y)= (mk x, y)
|
|
-
|
|
-instance ToResponse TResp where
|
|
- toResponse (TResp x)= toResponse x
|
|
- toResponse (TRespR r)= toResponse r
|
|
-
|
|
-instance ToResponse Response where
|
|
- toResponse = id
|
|
-
|
|
-instance ToResponse ByteString where
|
|
- toResponse x= responseLBS status200 [mkparam contentHtml] x
|
|
-
|
|
-instance ToResponse String where
|
|
- toResponse x= responseLBS status200 [mkparam contentHtml] $ fromString x
|
|
-
|
|
-instance ToResponse HttpData where
|
|
- toResponse (HttpData hs cookies x)= responseLBS status200 (mkParams ( hs <> cookieHeaders cookies)) x
|
|
- toResponse (Error str)= responseLBS status404 [("Content-Type", "text/html")] str
|
|
-
|
|
--- toResponse $ error "FATAL ERROR: HttpData errors should not reach here: MFlow.Forms.Response.hs "
|
|
-
|
|
-
|
|
-
|
|
+{-# OPTIONS -XExistentialQuantification -XTypeSynonymInstances
|
|
+ -XFlexibleInstances -XDeriveDataTypeable -XOverloadedStrings #-}
|
|
+module MFlow.Wai.Response where
|
|
+
|
|
+import Network.Wai
|
|
+import MFlow.Cookies
|
|
+
|
|
+import Data.ByteString.Lazy.UTF8
|
|
+import MFlow
|
|
+import Data.Typeable
|
|
+import Data.Monoid
|
|
+import System.IO.Unsafe
|
|
+import Data.Map as M
|
|
+import Data.CaseInsensitive
|
|
+import Network.HTTP.Types
|
|
+import Control.Workflow(WFErrors(..))
|
|
+--import Data.String
|
|
+--import Debug.Trace
|
|
+--
|
|
+--(!>)= flip trace
|
|
+
|
|
+
|
|
+
|
|
+class ToResponse a where
|
|
+ toResponse :: a -> Response
|
|
+
|
|
+
|
|
+
|
|
+data TResp = TRempty | forall a.ToResponse a=>TRespR a | forall a.(Typeable a, ToResponse a, Monoid a) => TResp a deriving Typeable
|
|
+
|
|
+instance Monoid TResp where
|
|
+ mempty = TRempty
|
|
+ mappend (TResp x) (TResp y)=
|
|
+ case cast y of
|
|
+ Just y' -> TResp $ mappend x y'
|
|
+ Nothing -> error $ "fragment of type " ++ show ( typeOf y) ++ " after fragment of type " ++ show ( typeOf x)
|
|
+
|
|
+
|
|
+mkParams = Prelude.map mkparam
|
|
+mkparam (x,y)= (mk x, y)
|
|
+
|
|
+instance ToResponse TResp where
|
|
+ toResponse (TResp x)= toResponse x
|
|
+ toResponse (TRespR r)= toResponse r
|
|
+
|
|
+instance ToResponse Response where
|
|
+ toResponse = id
|
|
+
|
|
+instance ToResponse ByteString where
|
|
+ toResponse x= responseLBS status200 [mkparam contentHtml] x
|
|
+
|
|
+instance ToResponse String where
|
|
+ toResponse x= responseLBS status200 [mkparam contentHtml] $ fromString x
|
|
+
|
|
+instance ToResponse HttpData where
|
|
+ toResponse (HttpData hs cookies x)= responseLBS status200 (mkParams ( hs <> cookieHeaders cookies)) x
|
|
+ toResponse (Error str)= responseLBS status404 [("Content-Type", "text/html")] str
|
|
+
|
|
+-- toResponse $ error "FATAL ERROR: HttpData errors should not reach here: MFlow.Forms.Response.hs "
|
|
+
|
|
+
|
|
+
|
|
diff -ru orig/src/MFlow/Wai.hs new/src/MFlow/Wai.hs
|
|
--- orig/src/MFlow/Wai.hs 2014-06-10 05:51:26.957015857 +0300
|
|
+++ new/src/MFlow/Wai.hs 2014-06-10 05:51:25.000000000 +0300
|
|
@@ -1,184 +1,209 @@
|
|
-{-# LANGUAGE UndecidableInstances
|
|
- , CPP
|
|
- , TypeSynonymInstances
|
|
- , MultiParamTypeClasses
|
|
- , DeriveDataTypeable
|
|
- , FlexibleInstances
|
|
- , OverloadedStrings #-}
|
|
-
|
|
-module MFlow.Wai(
|
|
- module MFlow.Cookies
|
|
- ,module MFlow
|
|
- ,waiMessageFlow)
|
|
-where
|
|
-
|
|
-import Data.Typeable
|
|
-import Network.Wai
|
|
-
|
|
-import Control.Concurrent.MVar(modifyMVar_, readMVar)
|
|
-import Control.Monad(when)
|
|
-
|
|
-
|
|
-import qualified Data.ByteString.Lazy.Char8 as B(empty,pack, unpack, length, ByteString,tail)
|
|
-import Data.ByteString.Lazy(fromChunks)
|
|
+{-# LANGUAGE UndecidableInstances
|
|
+ , CPP
|
|
+ , TypeSynonymInstances
|
|
+ , MultiParamTypeClasses
|
|
+ , DeriveDataTypeable
|
|
+ , FlexibleInstances
|
|
+ , OverloadedStrings #-}
|
|
+
|
|
+module MFlow.Wai(
|
|
+ module MFlow.Cookies
|
|
+ ,module MFlow
|
|
+ ,waiMessageFlow)
|
|
+where
|
|
+
|
|
+import Data.Typeable
|
|
+import Network.Wai
|
|
+
|
|
+import Control.Concurrent.MVar(modifyMVar_, readMVar)
|
|
+import Control.Monad(when)
|
|
+
|
|
+
|
|
+import qualified Data.ByteString.Lazy.Char8 as B(empty,pack, unpack, length, ByteString,tail)
|
|
+import Data.ByteString.Lazy(fromChunks)
|
|
import Data.ByteString.UTF8 hiding (span)
|
|
-import qualified Data.ByteString as SB hiding (pack, unpack)
|
|
-import Control.Concurrent(ThreadId(..))
|
|
-import System.IO.Unsafe
|
|
-import Control.Concurrent.MVar
|
|
-import Control.Concurrent
|
|
-import Control.Monad.Trans
|
|
-import Control.Exception
|
|
-import qualified Data.Map as M
|
|
-import Data.Maybe
|
|
-import Data.TCache
|
|
-import Data.TCache.DefaultPersistence
|
|
-import Control.Workflow hiding (Indexable(..))
|
|
-
|
|
-import MFlow
|
|
-import MFlow.Cookies
|
|
-import Data.Monoid
|
|
-import MFlow.Wai.Response
|
|
-import Network.Wai
|
|
-import Network.HTTP.Types -- hiding (urlDecode)
|
|
-import Data.Conduit
|
|
-import Data.Conduit.Lazy
|
|
-import qualified Data.Conduit.List as CList
|
|
-import Data.CaseInsensitive
|
|
-import System.Time
|
|
-import qualified Data.Text as T
|
|
-
|
|
-
|
|
---import Debug.Trace
|
|
---(!>) = flip trace
|
|
-
|
|
-flow= "flow"
|
|
-
|
|
-instance Processable Request where
|
|
- pwfPath env= if Prelude.null sc then [noScript] else Prelude.map T.unpack sc
|
|
- where
|
|
- sc= let p= pathInfo env
|
|
- p'= reverse p
|
|
- in case p' of
|
|
- [] -> []
|
|
- p' -> if T.null $ head p' then reverse(tail p') else p
|
|
-
|
|
-
|
|
- puser env = fromMaybe anonymous $ fmap toString $ lookup ( mk $ fromString cookieuser) $ requestHeaders env
|
|
-
|
|
- pind env= fromMaybe (error ": No FlowID") $ fmap toString $ lookup (mk flow) $ requestHeaders env
|
|
- getParams= mkParams1 . requestHeaders
|
|
- where
|
|
- mkParams1 = Prelude.map mkParam1
|
|
- mkParam1 ( x,y)= (toString $ original x, toString y)
|
|
-
|
|
--- getServer env= serverName env
|
|
--- getPath env= pathInfo env
|
|
--- getPort env= serverPort env
|
|
-
|
|
-
|
|
-splitPath ""= ("","","")
|
|
-splitPath str=
|
|
- let
|
|
- strr= reverse str
|
|
- (ext, rest)= span (/= '.') strr
|
|
- (mod, path)= span(/='/') $ tail rest
|
|
- in (tail $ reverse path, reverse mod, reverse ext)
|
|
-
|
|
-
|
|
-waiMessageFlow :: Application
|
|
-waiMessageFlow req1= do
|
|
- let httpreq1= requestHeaders req1
|
|
-
|
|
- let cookies = getCookies httpreq1
|
|
-
|
|
- (flowval , retcookies) <- case lookup flow cookies of
|
|
- Just fl -> return (fl, [])
|
|
- Nothing -> do
|
|
- fl <- liftIO $ newFlow
|
|
- return (fl, [UnEncryptedCookie (flow, fl, "/",Nothing):: Cookie])
|
|
-
|
|
-{- for state persistence in cookies
|
|
- putStateCookie req1 cookies
|
|
- let retcookies= case getStateCookie req1 of
|
|
- Nothing -> retcookies1
|
|
- Just ck -> ck:retcookies1
|
|
--}
|
|
-
|
|
- input <- case parseMethod $ requestMethod req1 of
|
|
- Right POST -> do
|
|
-
|
|
- inp <- liftIO $ requestBody req1 $$ CList.consume
|
|
-
|
|
- return . parseSimpleQuery $ SB.concat inp
|
|
-
|
|
-
|
|
-
|
|
- Right GET ->
|
|
- return . Prelude.map (\(x,y) -> (x,fromMaybe "" y)) $ queryString req1
|
|
-
|
|
-
|
|
- let req = case retcookies of
|
|
- [] -> req1{requestHeaders= mkParams (input ++ cookies) ++ requestHeaders req1} -- !> "REQ"
|
|
- _ -> req1{requestHeaders= mkParams ((flow, flowval): input ++ cookies) ++ requestHeaders req1} -- !> "REQ"
|
|
-
|
|
-
|
|
- (resp',th) <- liftIO $ msgScheduler req -- !> (show $ requestHeaders req)
|
|
-
|
|
- let resp= case (resp',retcookies) of
|
|
- (_,[]) -> resp'
|
|
- (error@(Error _),_) -> error
|
|
- (HttpData hs co str,_) -> HttpData hs (co++ retcookies) str
|
|
-
|
|
- return $ toResponse resp
|
|
-
|
|
-
|
|
-------persistent state in cookies (not tested)
|
|
-
|
|
-tvresources :: MVar (Maybe ( M.Map string string))
|
|
-tvresources= unsafePerformIO $ newMVar Nothing
|
|
-statCookieName= "stat"
|
|
-
|
|
-putStateCookie req cookies=
|
|
- case lookup statCookieName cookies of
|
|
- Nothing -> return ()
|
|
- Just (statCookieName, str , "/", _) -> modifyMVar_ tvresources $
|
|
- \mmap -> case mmap of
|
|
- Just map -> return $ Just $ M.insert (keyResource req) str map
|
|
- Nothing -> return $ Just $ M.fromList [((keyResource req), str) ]
|
|
-
|
|
-getStateCookie req= do
|
|
- mr<- readMVar tvresources
|
|
- case mr of
|
|
- Nothing -> return Nothing
|
|
- Just map -> case M.lookup (keyResource req) map of
|
|
- Nothing -> return Nothing
|
|
- Just str -> do
|
|
- swapMVar tvresources Nothing
|
|
- return $ Just (statCookieName, str , "/")
|
|
-
|
|
-
|
|
-
|
|
-
|
|
-{-
|
|
-persistInCookies= setPersist PersistStat{readStat=readResource, writeStat=writeResource, deleteStat=deleteResource}
|
|
- where
|
|
- writeResource stat= modifyMVar_ tvresources $ \mmap ->
|
|
- case mmap of
|
|
- Just map-> return $ Just $ M.insert (keyResource stat) (serialize stat) map
|
|
- Nothing -> return $ Just $ M.fromList [((keyResource stat), (serialize stat)) ]
|
|
- readResource stat= do
|
|
- mstr <- withMVar tvresources $ \mmap ->
|
|
- case mmap of
|
|
- Just map -> return $ M.lookup (keyResource stat) map
|
|
- Nothing -> return Nothing
|
|
- case mstr of
|
|
- Nothing -> return Nothing
|
|
- Just str -> return $ deserialize str
|
|
-
|
|
- deleteResource stat= modifyMVar_ tvresources $ \mmap->
|
|
- case mmap of
|
|
- Just map -> return $ Just $ M.delete (keyResource stat) map
|
|
- Nothing -> return $ Nothing
|
|
-
|
|
--}
|
|
+import qualified Data.ByteString.Char8 as SB -- hiding (pack, unpack)
|
|
+import Control.Concurrent(ThreadId(..))
|
|
+import System.IO.Unsafe
|
|
+import Control.Concurrent.MVar
|
|
+import Control.Concurrent
|
|
+import Control.Monad.Trans
|
|
+import Control.Exception
|
|
+import qualified Data.Map as M
|
|
+import Data.Maybe
|
|
+import Data.TCache
|
|
+import Data.TCache.DefaultPersistence
|
|
+import Control.Workflow hiding (Indexable(..))
|
|
+
|
|
+import MFlow
|
|
+import MFlow.Cookies
|
|
+import Data.Monoid
|
|
+import MFlow.Wai.Response
|
|
+import Network.Wai
|
|
+import Network.Wai.Parse
|
|
+import qualified Data.Conduit.Binary as CB
|
|
+import Control.Monad.Trans.Resource
|
|
+import Network.HTTP.Types
|
|
+import Data.Conduit
|
|
+import Data.Conduit.Lazy
|
|
+import qualified Data.Conduit.List as CList
|
|
+import Data.CaseInsensitive
|
|
+import System.Time
|
|
+import System.Directory
|
|
+import System.IO
|
|
+import qualified Data.Text as T
|
|
+
|
|
+
|
|
+import Debug.Trace
|
|
+(!>) = flip trace
|
|
+
|
|
+flow= "flow"
|
|
+
|
|
+instance Processable Request where
|
|
+ pwfPath env= if Prelude.null sc then [noScript] else Prelude.map T.unpack sc
|
|
+ where
|
|
+ sc= let p= pathInfo env
|
|
+ p'= reverse p
|
|
+ in case p' of
|
|
+ [] -> []
|
|
+ p' -> if T.null $ head p' then reverse(tail p') else p
|
|
+
|
|
+
|
|
+ puser env = fromMaybe anonymous $ fmap toString $ lookup ( mk $ fromString cookieuser) $ requestHeaders env
|
|
+
|
|
+ pind env= fromMaybe (error ": No FlowID") $ fmap toString $ lookup (mk flow) $ requestHeaders env
|
|
+ getParams= mkParams1 . requestHeaders
|
|
+ where
|
|
+ mkParams1 = Prelude.map mkParam1
|
|
+ mkParam1 ( x,y)= (toString $ original x, toString y)
|
|
+
|
|
+toApp :: (Request -> IO Response) -> Application
|
|
+#if MIN_VERSION_wai(3, 0, 0)
|
|
+toApp f req sendResponse = f req >>= sendResponse
|
|
+#else
|
|
+toApp = id
|
|
+#endif
|
|
+
|
|
+waiMessageFlow :: Application
|
|
+waiMessageFlow = toApp $ \req1 -> do
|
|
+ let httpreq1= requestHeaders req1
|
|
+
|
|
+ let cookies = getCookies httpreq1
|
|
+
|
|
+ (flowval , retcookies) <- case lookup flow cookies of
|
|
+ Just fl -> return (fl, [])
|
|
+ Nothing -> do
|
|
+ fl <- liftIO $ newFlow
|
|
+ return (fl, [UnEncryptedCookie (flow, fl, "/",Nothing):: Cookie])
|
|
+
|
|
+{- for state persistence in cookies
|
|
+ putStateCookie req1 cookies
|
|
+ let retcookies= case getStateCookie req1 of
|
|
+ Nothing -> retcookies1
|
|
+ Just ck -> ck:retcookies1
|
|
+-}
|
|
+
|
|
+ (params,files) <- case parseMethod $ requestMethod req1 of
|
|
+ Right GET -> do
|
|
+ return (Prelude.map (\(x,y) -> (x,fromMaybe "" y)) $ queryString req1,[])
|
|
+
|
|
+ Right POST -> do
|
|
+
|
|
+ case getRequestBodyType req1 of
|
|
+ Nothing -> error $ "getRequestBodyType: "
|
|
+ Just rbt ->
|
|
+ runResourceT $ withInternalState $ \state -> liftIO $ do
|
|
+ let backend file info= do
|
|
+ (key, (fp, h)) <- flip runInternalState state $ allocate (do
|
|
+ tempDir <- getTemporaryDirectory
|
|
+ openBinaryTempFile tempDir "upload.tmp") (\(_, h) -> hClose h)
|
|
+ CB.sinkHandle h
|
|
+ lift $ release key
|
|
+ return fp
|
|
+#if MIN_VERSION_wai(3, 0, 0)
|
|
+ let backend' file info getBS = do
|
|
+ let src = do
|
|
+ bs <- liftIO getBS
|
|
+ when (not $ SB.null bs) $ do
|
|
+ Data.Conduit.yield bs
|
|
+ src
|
|
+ src $$ backend file info
|
|
+ sinkRequestBody backend' rbt (requestBody req1)
|
|
+#else
|
|
+ requestBody req1 $$ sinkRequestBody backend rbt
|
|
+#endif
|
|
+
|
|
+-- let fileparams= Prelude.map (\(param,FileInfo filename contentype content)
|
|
+-- -> (param, SB.pack content )) files
|
|
+-- let fileparams= Prelude.map (\(param,fileinfo)
|
|
+-- -> (param, fileinfo )) files
|
|
+-- return $ fileparams++ params
|
|
+ let filesp= Prelude.map (\(param,FileInfo filename contentype tempfile)
|
|
+ -> (mk param, fromString $ show(filename,contentype,tempfile) )) files
|
|
+-- let filesp= Prelude.map (\(a,b) -> ( mk a, fromString $ show b)) files
|
|
+
|
|
+
|
|
+ let req = case retcookies of
|
|
+ [] -> req1{requestHeaders= filesp ++ mkParams (params ++ cookies) ++ requestHeaders req1}
|
|
+ _ -> req1{requestHeaders= filesp ++ mkParams ((flow, flowval): params ++ cookies) ++ requestHeaders req1}
|
|
+
|
|
+
|
|
+ (resp',th) <- liftIO $ msgScheduler req -- !> (show $ requestHeaders req)
|
|
+
|
|
+ let resp= case (resp',retcookies) of
|
|
+ (_,[]) -> resp'
|
|
+ (error@(Error _),_) -> error
|
|
+ (HttpData hs co str,_) -> HttpData hs (co++ retcookies) str
|
|
+
|
|
+ return $ toResponse resp
|
|
+
|
|
+
|
|
+------persistent state in cookies (not tested)
|
|
+
|
|
+tvresources :: MVar (Maybe ( M.Map string string))
|
|
+tvresources= unsafePerformIO $ newMVar Nothing
|
|
+statCookieName= "stat"
|
|
+
|
|
+putStateCookie req cookies=
|
|
+ case lookup statCookieName cookies of
|
|
+ Nothing -> return ()
|
|
+ Just (statCookieName, str , "/", _) -> modifyMVar_ tvresources $
|
|
+ \mmap -> case mmap of
|
|
+ Just map -> return $ Just $ M.insert (keyResource req) str map
|
|
+ Nothing -> return $ Just $ M.fromList [((keyResource req), str) ]
|
|
+
|
|
+getStateCookie req= do
|
|
+ mr<- readMVar tvresources
|
|
+ case mr of
|
|
+ Nothing -> return Nothing
|
|
+ Just map -> case M.lookup (keyResource req) map of
|
|
+ Nothing -> return Nothing
|
|
+ Just str -> do
|
|
+ swapMVar tvresources Nothing
|
|
+ return $ Just (statCookieName, str , "/")
|
|
+
|
|
+
|
|
+
|
|
+
|
|
+{-
|
|
+persistInCookies= setPersist PersistStat{readStat=readResource, writeStat=writeResource, deleteStat=deleteResource}
|
|
+ where
|
|
+ writeResource stat= modifyMVar_ tvresources $ \mmap ->
|
|
+ case mmap of
|
|
+ Just map-> return $ Just $ M.insert (keyResource stat) (serialize stat) map
|
|
+ Nothing -> return $ Just $ M.fromList [((keyResource stat), (serialize stat)) ]
|
|
+ readResource stat= do
|
|
+ mstr <- withMVar tvresources $ \mmap ->
|
|
+ case mmap of
|
|
+ Just map -> return $ M.lookup (keyResource stat) map
|
|
+ Nothing -> return Nothing
|
|
+ case mstr of
|
|
+ Nothing -> return Nothing
|
|
+ Just str -> return $ deserialize str
|
|
+
|
|
+ deleteResource stat= modifyMVar_ tvresources $ \mmap->
|
|
+ case mmap of
|
|
+ Just map -> return $ Just $ M.delete (keyResource stat) map
|
|
+ Nothing -> return $ Nothing
|
|
+
|
|
+-}
|
|
diff -ru orig/src/MFlow.hs new/src/MFlow.hs
|
|
--- orig/src/MFlow.hs 2014-06-10 05:51:26.957015857 +0300
|
|
+++ new/src/MFlow.hs 2014-06-10 05:51:25.000000000 +0300
|
|
@@ -1,545 +1,550 @@
|
|
-{- | Non monadic low level primitives that implement the MFlow application server.
|
|
-See "MFlow.Form" for the higher level interface that you may use.
|
|
-
|
|
-it implements an scheduler of 'Processable' messages that are served according with
|
|
-the source identification and the verb invoked.
|
|
-The scheduler executed the appropriate workflow (using the workflow package).
|
|
-The workflow will send additional messages to the source and wait for the responses.
|
|
-The diaglog is identified by a 'Token', which is associated to the flow.
|
|
-. The computation state is optionally logged. On timeout, the process is killed. When invoked again,
|
|
-the execution state is recovered as if no interruption took place.
|
|
-
|
|
-There is no asumption about message codification, so instantiations
|
|
-of this scheduler for different infrastructures is possible,
|
|
-including non-Web based ones as long as they support or emulate cookies.
|
|
-
|
|
-"MFlow.Hack" is an instantiation for the Hack interface in a Web context.
|
|
-
|
|
-"MFlow.Wai" is a instantiation for the WAI interface.
|
|
-
|
|
-"MFlow.Forms" implements a monadic type safe interface with composabe widgets and and applicative
|
|
-combinator as well as an higher comunication interface.
|
|
-
|
|
-"MFlow.Forms.XHtml" is an instantiation for the Text.XHtml format
|
|
-
|
|
-"MFlow.Forms.Blaze.Html" is an instantaiation for blaze-html
|
|
-
|
|
-"MFlow.Forms.HSP" is an instantiation for the Haskell Server Pages format
|
|
-
|
|
-There are some @*.All@ packages that contain a mix of these instantiations.
|
|
-For exmaple, "MFlow.Wai.Blaze.Html.All" includes most of all necessary for using MFlow with
|
|
-Wai <http://hackage.haskell.org/package/wai> and
|
|
-Blaze-html <http://hackage.haskell.org/package/blaze-html>
|
|
-
|
|
-
|
|
-In order to manage resources, there are primitives that kill the process and its state after a timeout.
|
|
-
|
|
-All these details are hidden in the monad of "MFlow.Forms" that provides an higher level interface.
|
|
-
|
|
-Fragment based streaming: 'sendFragment' are provided only at this level.
|
|
-
|
|
-'stateless' and 'transient' server processeses are also possible. the first are request-response
|
|
- . `transient` processes do not persist after timeout, so they restart anew after a timeout or a crash.
|
|
-
|
|
--}
|
|
-
|
|
-
|
|
-{-# LANGUAGE DeriveDataTypeable, UndecidableInstances
|
|
- ,ExistentialQuantification
|
|
- ,MultiParamTypeClasses
|
|
- ,FunctionalDependencies
|
|
- ,TypeSynonymInstances
|
|
- ,FlexibleInstances
|
|
- ,FlexibleContexts
|
|
- ,RecordWildCards
|
|
- ,OverloadedStrings
|
|
- ,ScopedTypeVariables
|
|
-
|
|
- #-}
|
|
-module MFlow (
|
|
-Flow, Params, HttpData(..),Processable(..)
|
|
-, Token(..), ProcList
|
|
--- * low level comunication primitives. Use `ask` instead
|
|
-,flushRec, flushResponse, receive, receiveReq, receiveReqTimeout, send, sendFlush, sendFragment
|
|
-, sendEndFragment, sendToMF
|
|
--- * Flow configuration
|
|
-,setNoScript,addMessageFlows,getMessageFlows,delMessageFlow, transient, stateless,anonymous
|
|
-,noScript,hlog, setNotFoundResponse,getNotFoundResponse,
|
|
--- * ByteString tags
|
|
--- | very basic but efficient bytestring tag formatting
|
|
-btag, bhtml, bbody,Attribs, addAttrs
|
|
--- * user
|
|
-, userRegister, setAdminUser, getAdminName, Auth(..),getAuthMethod, setAuthMethod
|
|
+{- | Non monadic low level primitives that implement the MFlow application server.
|
|
+See "MFlow.Form" for the higher level interface that you may use.
|
|
+
|
|
+it implements an scheduler of 'Processable' messages that are served according with
|
|
+the source identification and the verb invoked.
|
|
+The scheduler executed the appropriate workflow (using the workflow package).
|
|
+The workflow will send additional messages to the source and wait for the responses.
|
|
+The diaglog is identified by a 'Token', which is associated to the flow.
|
|
+. The computation state is optionally logged. On timeout, the process is killed. When invoked again,
|
|
+the execution state is recovered as if no interruption took place.
|
|
+
|
|
+There is no asumption about message codification, so instantiations
|
|
+of this scheduler for different infrastructures is possible,
|
|
+including non-Web based ones as long as they support or emulate cookies.
|
|
+
|
|
+"MFlow.Hack" is an instantiation for the Hack interface in a Web context.
|
|
+
|
|
+"MFlow.Wai" is a instantiation for the WAI interface.
|
|
+
|
|
+"MFlow.Forms" implements a monadic type safe interface with composabe widgets and and applicative
|
|
+combinator as well as an higher comunication interface.
|
|
+
|
|
+"MFlow.Forms.XHtml" is an instantiation for the Text.XHtml format
|
|
+
|
|
+"MFlow.Forms.Blaze.Html" is an instantaiation for blaze-html
|
|
+
|
|
+"MFlow.Forms.HSP" is an instantiation for the Haskell Server Pages format
|
|
+
|
|
+There are some @*.All@ packages that contain a mix of these instantiations.
|
|
+For exmaple, "MFlow.Wai.Blaze.Html.All" includes most of all necessary for using MFlow with
|
|
+Wai <http://hackage.haskell.org/package/wai> and
|
|
+Blaze-html <http://hackage.haskell.org/package/blaze-html>
|
|
+
|
|
+
|
|
+In order to manage resources, there are primitives that kill the process and its state after a timeout.
|
|
+
|
|
+All these details are hidden in the monad of "MFlow.Forms" that provides an higher level interface.
|
|
+
|
|
+Fragment based streaming: 'sendFragment' are provided only at this level.
|
|
+
|
|
+'stateless' and 'transient' server processeses are also possible. the first are request-response
|
|
+ . `transient` processes do not persist after timeout, so they restart anew after a timeout or a crash.
|
|
+
|
|
+-}
|
|
+
|
|
+
|
|
+{-# LANGUAGE DeriveDataTypeable, UndecidableInstances
|
|
+ ,ExistentialQuantification
|
|
+ ,MultiParamTypeClasses
|
|
+ ,FunctionalDependencies
|
|
+ ,TypeSynonymInstances
|
|
+ ,FlexibleInstances
|
|
+ ,FlexibleContexts
|
|
+ ,RecordWildCards
|
|
+ ,OverloadedStrings
|
|
+ ,ScopedTypeVariables
|
|
+ ,BangPatterns
|
|
+ #-}
|
|
+module MFlow (
|
|
+Flow, Params, HttpData(..),Processable(..)
|
|
+, Token(..), ProcList
|
|
+-- * low level comunication primitives. Use `ask` instead
|
|
+,flushRec, flushResponse, receive, receiveReq, receiveReqTimeout, send, sendFlush, sendFragment
|
|
+, sendEndFragment, sendToMF
|
|
+-- * Flow configuration
|
|
+,setNoScript,addMessageFlows,getMessageFlows,delMessageFlow, transient, stateless,anonymous
|
|
+,noScript,hlog, setNotFoundResponse,getNotFoundResponse,
|
|
+-- * ByteString tags
|
|
+-- | very basic but efficient bytestring tag formatting
|
|
+btag, bhtml, bbody,Attribs, addAttrs
|
|
+-- * user
|
|
+, userRegister, setAdminUser, getAdminName, Auth(..),getAuthMethod, setAuthMethod
|
|
-- * static files
|
|
-- * config
|
|
-,config, getConfig
|
|
-,setFilesPath
|
|
--- * internal use
|
|
-,addTokenToList,deleteTokenInList, msgScheduler,serveFile,newFlow
|
|
-,UserStr,PasswdStr, User(..),eUser
|
|
-
|
|
-)
|
|
-where
|
|
-import Control.Concurrent.MVar
|
|
-import Data.IORef
|
|
-import GHC.Conc(unsafeIOToSTM)
|
|
-import Data.Typeable
|
|
-import Data.Maybe(isJust, isNothing, fromMaybe, fromJust)
|
|
-import Data.Char(isSeparator)
|
|
-import Data.List(isPrefixOf,isSuffixOf,isInfixOf, elem , span, (\\),intersperse)
|
|
-import Control.Monad(when)
|
|
-
|
|
-import Data.Monoid
|
|
-import Control.Concurrent(forkIO,threadDelay,killThread, myThreadId, ThreadId)
|
|
-import Data.Char(toLower)
|
|
-
|
|
-import Unsafe.Coerce
|
|
-import System.IO.Unsafe
|
|
-import Data.TCache
|
|
-import Data.TCache.DefaultPersistence hiding(Indexable(..))
|
|
-import Data.TCache.Memoization
|
|
-import qualified Data.ByteString.Lazy.Char8 as B (head, readFile,ByteString, concat,pack, unpack,empty,append,cons,fromChunks)
|
|
-import Data.ByteString.Lazy.Internal (ByteString(Chunk))
|
|
-import qualified Data.ByteString.Char8 as SB
|
|
-import qualified Data.Map as M
|
|
-import System.IO
|
|
-import System.Time
|
|
-import Control.Workflow
|
|
-import MFlow.Cookies
|
|
-import Control.Monad.Trans
|
|
-import qualified Control.Exception as CE
|
|
-import Data.RefSerialize hiding (empty)
|
|
-import qualified Data.Text as T
|
|
-import System.Posix.Internals
|
|
-import Control.Exception
|
|
---import Debug.Trace
|
|
---(!>) = flip trace
|
|
-
|
|
-
|
|
--- | a Token identifies a flow that handle messages. The scheduler compose a Token with every `Processable`
|
|
--- message that arrives and send the mesage to the appropriate flow.
|
|
-data Token = Token{twfname,tuser, tind :: String , tpath :: [String], tenv:: Params, tblock:: MVar Bool, tsendq :: MVar Req, trecq :: MVar Resp} deriving Typeable
|
|
-
|
|
-instance Indexable Token where
|
|
- key (Token w u i _ _ _ _ _ )= i
|
|
--- if u== anonymous then u ++ i -- ++ "@" ++ w
|
|
--- else u -- ++ "@" ++ w
|
|
-
|
|
-instance Show Token where
|
|
- show t = "Token " ++ key t
|
|
-
|
|
-instance Read Token where
|
|
- readsPrec _ ('T':'o':'k':'e': 'n':' ':str1)
|
|
- | anonymous `isPrefixOf` str1= [(Token w anonymous i [] [] (newVar True) (newVar 0) (newVar 0), tail str2)]
|
|
- | otherwise = [(Token w ui "0" [] [] (newVar True) (newVar 0) (newVar 0), tail str2)]
|
|
-
|
|
- where
|
|
-
|
|
- (ui,str')= span(/='@') str1
|
|
- i = drop (length anonymous) ui
|
|
- (w,str2) = span (not . isSeparator) $ tail str'
|
|
- newVar _= unsafePerformIO $ newEmptyMVar
|
|
-
|
|
-
|
|
- readsPrec _ str= error $ "parse error in Token read from: "++ str
|
|
-
|
|
-instance Serializable Token where
|
|
- serialize = B.pack . show
|
|
- deserialize= read . B.unpack
|
|
- setPersist = \_ -> Just filePersist
|
|
-
|
|
-iorefqmap= unsafePerformIO . newMVar $ M.empty
|
|
-
|
|
-addTokenToList t@Token{..} =
|
|
- modifyMVar_ iorefqmap $ \ map ->
|
|
- return $ M.insert ( tind ++ twfname ++ tuser ) t map
|
|
-
|
|
-deleteTokenInList t@Token{..} =
|
|
- modifyMVar_ iorefqmap $ \ map ->
|
|
- return $ M.delete (tind ++ twfname ++ tuser) map
|
|
-
|
|
-getToken msg= do
|
|
- qmap <- readMVar iorefqmap
|
|
- let u= puser msg ; w= pwfname msg ; i=pind msg; ppath=pwfPath msg;penv= getParams msg
|
|
- let mqs = M.lookup ( i ++ w ++ u) qmap
|
|
- case mqs of
|
|
- Nothing -> do
|
|
- q <- newEmptyMVar -- `debug` (i++w++u)
|
|
+,config,getConfig
|
|
+,setFilesPath
|
|
+-- * internal use
|
|
+,addTokenToList,deleteTokenInList, msgScheduler,serveFile,newFlow
|
|
+,UserStr,PasswdStr, User(..),eUser
|
|
+
|
|
+)
|
|
+where
|
|
+import Control.Concurrent.MVar
|
|
+import Data.IORef
|
|
+import GHC.Conc(unsafeIOToSTM)
|
|
+import Data.Typeable
|
|
+import Data.Maybe(isJust, isNothing, fromMaybe, fromJust)
|
|
+import Data.Char(isSeparator)
|
|
+import Data.List(isPrefixOf,isSuffixOf,isInfixOf, elem , span, (\\),intersperse)
|
|
+import Control.Monad(when)
|
|
+
|
|
+import Data.Monoid
|
|
+import Control.Concurrent(forkIO,threadDelay,killThread, myThreadId, ThreadId)
|
|
+import Data.Char(toLower)
|
|
+
|
|
+import Unsafe.Coerce
|
|
+import System.IO.Unsafe
|
|
+import Data.TCache
|
|
+import Data.TCache.DefaultPersistence hiding(Indexable(..))
|
|
+import Data.TCache.Memoization
|
|
+import qualified Data.ByteString.Lazy.Char8 as B (head, readFile,ByteString, concat,pack, unpack,empty,append,cons,fromChunks)
|
|
+import Data.ByteString.Lazy.Internal (ByteString(Chunk))
|
|
+import qualified Data.ByteString.Char8 as SB
|
|
+import qualified Data.Map as M
|
|
+import System.IO
|
|
+import System.Time
|
|
+import Control.Workflow
|
|
+import MFlow.Cookies
|
|
+import Control.Monad.Trans
|
|
+import qualified Control.Exception as CE
|
|
+import Data.RefSerialize hiding (empty)
|
|
+import qualified Data.Text as T
|
|
+import System.Posix.Internals
|
|
+import Control.Exception
|
|
+import Crypto.PasswordStore
|
|
+
|
|
+
|
|
+--import Debug.Trace
|
|
+--(!>) = flip trace
|
|
+
|
|
+
|
|
+-- | a Token identifies a flow that handle messages. The scheduler compose a Token with every `Processable`
|
|
+-- message that arrives and send the mesage to the appropriate flow.
|
|
+data Token = Token{twfname,tuser, tind :: String , tpath :: [String], tenv:: Params, tblock:: MVar Bool, tsendq :: MVar Req, trecq :: MVar Resp} deriving Typeable
|
|
+
|
|
+instance Indexable Token where
|
|
+ key (Token w u i _ _ _ _ _ )= i
|
|
+-- if u== anonymous then u ++ i -- ++ "@" ++ w
|
|
+-- else u -- ++ "@" ++ w
|
|
+
|
|
+instance Show Token where
|
|
+ show t = "Token " ++ key t
|
|
+
|
|
+instance Read Token where
|
|
+ readsPrec _ ('T':'o':'k':'e': 'n':' ':str1)
|
|
+ | anonymous `isPrefixOf` str1= [(Token w anonymous i [] [] (newVar True) (newVar 0) (newVar 0), tail str2)]
|
|
+ | otherwise = [(Token w ui "0" [] [] (newVar True) (newVar 0) (newVar 0), tail str2)]
|
|
+
|
|
+ where
|
|
+
|
|
+ (ui,str')= span(/='@') str1
|
|
+ i = drop (length anonymous) ui
|
|
+ (w,str2) = span (not . isSeparator) $ tail str'
|
|
+ newVar _= unsafePerformIO $ newEmptyMVar
|
|
+
|
|
+
|
|
+ readsPrec _ str= error $ "parse error in Token read from: "++ str
|
|
+
|
|
+instance Serializable Token where
|
|
+ serialize = B.pack . show
|
|
+ deserialize= read . B.unpack
|
|
+ setPersist = \_ -> Just filePersist
|
|
+
|
|
+iorefqmap= unsafePerformIO . newMVar $ M.empty
|
|
+
|
|
+addTokenToList t@Token{..} =
|
|
+ modifyMVar_ iorefqmap $ \ map ->
|
|
+ return $ M.insert ( tind ++ twfname ++ tuser ) t map
|
|
+
|
|
+deleteTokenInList t@Token{..} =
|
|
+ modifyMVar_ iorefqmap $ \ map ->
|
|
+ return $ M.delete (tind ++ twfname ++ tuser) map
|
|
+
|
|
+getToken msg= do
|
|
+ qmap <- readMVar iorefqmap
|
|
+ let u= puser msg ; w= pwfname msg ; i=pind msg; ppath=pwfPath msg;penv= getParams msg
|
|
+ let mqs = M.lookup ( i ++ w ++ u) qmap
|
|
+ case mqs of
|
|
+ Nothing -> do
|
|
+ q <- newEmptyMVar -- `debug` (i++w++u)
|
|
qr <- newEmptyMVar
|
|
- pblock <- newMVar True
|
|
- let token= Token w u i ppath penv pblock q qr
|
|
- addTokenToList token
|
|
- return token
|
|
-
|
|
- Just token -> return token{tpath= ppath, tenv= penv}
|
|
-
|
|
-
|
|
-type Flow= (Token -> Workflow IO ())
|
|
-
|
|
-data HttpData = HttpData [(SB.ByteString,SB.ByteString)] [Cookie] ByteString | Error ByteString deriving (Typeable, Show)
|
|
-
|
|
-
|
|
-instance Monoid HttpData where
|
|
- mempty= HttpData [] [] B.empty
|
|
- mappend (HttpData h c s) (HttpData h' c' s')= HttpData (h++h') (c++ c') $ mappend s s'
|
|
-
|
|
--- | List of (wfname, workflow) pairs, to be scheduled depending on the message's pwfname
|
|
-type ProcList = WorkflowList IO Token ()
|
|
-
|
|
-
|
|
-data Req = forall a.( Processable a, Typeable a)=> Req a deriving Typeable
|
|
-
|
|
-type Params = [(String,String)]
|
|
-
|
|
-class Processable a where
|
|
- pwfname :: a -> String
|
|
- pwfname s= Prelude.head $ pwfPath s
|
|
- pwfPath :: a -> [String]
|
|
- puser :: a -> String
|
|
- pind :: a -> String
|
|
- getParams :: a -> Params
|
|
-
|
|
-instance Processable Token where
|
|
- pwfname = twfname
|
|
- pwfPath = tpath
|
|
- puser = tuser
|
|
- pind = tind
|
|
- getParams = tenv
|
|
-
|
|
-instance Processable Req where
|
|
- pwfname (Req x)= pwfname x
|
|
- pwfPath (Req x)= pwfPath x
|
|
- puser (Req x)= puser x
|
|
- pind (Req x)= pind x
|
|
- getParams (Req x)= getParams x
|
|
--- getServer (Req x)= getServer x
|
|
--- getPort (Req x)= getPort x
|
|
-
|
|
-data Resp = Fragm HttpData
|
|
- | EndFragm HttpData
|
|
- | Resp HttpData
|
|
-
|
|
-
|
|
-
|
|
-
|
|
--- | The anonymous user
|
|
-anonymous= "anon#"
|
|
-
|
|
--- | It is the path of the root flow
|
|
+ pblock <- newMVar True
|
|
+ let token= Token w u i ppath penv pblock q qr
|
|
+ addTokenToList token
|
|
+ return token
|
|
+
|
|
+ Just token -> return token{tpath= ppath, tenv= penv}
|
|
+
|
|
+
|
|
+type Flow= (Token -> Workflow IO ())
|
|
+
|
|
+data HttpData = HttpData [(SB.ByteString,SB.ByteString)] [Cookie] ByteString | Error ByteString deriving (Typeable, Show)
|
|
+
|
|
+
|
|
+instance Monoid HttpData where
|
|
+ mempty= HttpData [] [] B.empty
|
|
+ mappend (HttpData h c s) (HttpData h' c' s')= HttpData (h++h') (c++ c') $ mappend s s'
|
|
+
|
|
+-- | List of (wfname, workflow) pairs, to be scheduled depending on the message's pwfname
|
|
+type ProcList = WorkflowList IO Token ()
|
|
+
|
|
+
|
|
+data Req = forall a.( Processable a, Typeable a)=> Req a deriving Typeable
|
|
+
|
|
+type Params = [(String,String)]
|
|
+
|
|
+class Processable a where
|
|
+ pwfname :: a -> String
|
|
+ pwfname s= Prelude.head $ pwfPath s
|
|
+ pwfPath :: a -> [String]
|
|
+ puser :: a -> String
|
|
+ pind :: a -> String
|
|
+ getParams :: a -> Params
|
|
+
|
|
+instance Processable Token where
|
|
+ pwfname = twfname
|
|
+ pwfPath = tpath
|
|
+ puser = tuser
|
|
+ pind = tind
|
|
+ getParams = tenv
|
|
+
|
|
+instance Processable Req where
|
|
+ pwfname (Req x)= pwfname x
|
|
+ pwfPath (Req x)= pwfPath x
|
|
+ puser (Req x)= puser x
|
|
+ pind (Req x)= pind x
|
|
+ getParams (Req x)= getParams x
|
|
+-- getServer (Req x)= getServer x
|
|
+-- getPort (Req x)= getPort x
|
|
+
|
|
+data Resp = Fragm HttpData
|
|
+ | EndFragm HttpData
|
|
+ | Resp HttpData
|
|
+
|
|
+
|
|
+
|
|
+
|
|
+-- | The anonymous user
|
|
+anonymous= "anon#"
|
|
+
|
|
+-- | It is the path of the root flow
|
|
noScriptRef= unsafePerformIO $ newIORef "noscript"
|
|
-
|
|
-noScript= unsafePerformIO $ readIORef noScriptRef
|
|
+
|
|
+noScript= unsafePerformIO $ readIORef noScriptRef
|
|
|
|
-- | set the flow to be executed when the URL has no path. The home page.
|
|
--
|
|
-- By default it is "noscript".
|
|
-- Although it is changed by `runNavigation` to his own flow name.
|
|
-setNoScript scr= writeIORef noScriptRef scr
|
|
-
|
|
-{-
|
|
-instance (Monad m, Show a) => Traceable (Workflow m a) where
|
|
- debugf iox str = do
|
|
- x <- iox
|
|
- return $ debug x (str++" => Workflow "++ show x)
|
|
--}
|
|
--- | send a complete response
|
|
---send :: Token -> HttpData -> IO()
|
|
-send t@(Token _ _ _ _ _ _ _ qresp) msg= do
|
|
- ( putMVar qresp . Resp $ msg ) -- !> ("<<<<< send "++ show t)
|
|
-
|
|
-sendFlush t msg= flushRec t >> send t msg -- !> "sendFlush "
|
|
-
|
|
--- | send a response fragment. Useful for streaming. the last packet must be sent trough 'send'
|
|
-sendFragment :: Token -> HttpData -> IO()
|
|
-sendFragment (Token _ _ _ _ _ _ _ qresp) msg= putMVar qresp . Fragm $ msg
|
|
-
|
|
-{-# DEPRECATED sendEndFragment "use \"send\" to end a fragmented response instead" #-}
|
|
-sendEndFragment :: Token -> HttpData -> IO()
|
|
-sendEndFragment (Token _ _ _ _ _ _ _ qresp) msg= putMVar qresp $ EndFragm msg
|
|
-
|
|
---emptyReceive (Token queue _ _)= emptyQueue queue
|
|
-receive :: Typeable a => Token -> IO a
|
|
-receive t= receiveReq t >>= return . fromReq
|
|
-
|
|
-flushResponse t@(Token _ _ _ _ _ _ _ qresp)= tryTakeMVar qresp
|
|
-
|
|
-
|
|
-flushRec t@(Token _ _ _ _ _ _ queue _)= tryTakeMVar queue -- !> "flushRec"
|
|
-
|
|
-receiveReq :: Token -> IO Req
|
|
+setNoScript scr= writeIORef noScriptRef scr
|
|
+
|
|
+{-
|
|
+instance (Monad m, Show a) => Traceable (Workflow m a) where
|
|
+ debugf iox str = do
|
|
+ x <- iox
|
|
+ return $ debug x (str++" => Workflow "++ show x)
|
|
+-}
|
|
+-- | send a complete response
|
|
+--send :: Token -> HttpData -> IO()
|
|
+send t@(Token _ _ _ _ _ _ _ qresp) msg= do
|
|
+ ( putMVar qresp . Resp $ msg ) -- !> ("<<<<< send "++ show t)
|
|
+
|
|
+sendFlush t msg= flushRec t >> send t msg -- !> "sendFlush "
|
|
+
|
|
+-- | send a response fragment. Useful for streaming. the last packet must be sent trough 'send'
|
|
+sendFragment :: Token -> HttpData -> IO()
|
|
+sendFragment (Token _ _ _ _ _ _ _ qresp) msg= putMVar qresp . Fragm $ msg
|
|
+
|
|
+{-# DEPRECATED sendEndFragment "use \"send\" to end a fragmented response instead" #-}
|
|
+sendEndFragment :: Token -> HttpData -> IO()
|
|
+sendEndFragment (Token _ _ _ _ _ _ _ qresp) msg= putMVar qresp $ EndFragm msg
|
|
+
|
|
+--emptyReceive (Token queue _ _)= emptyQueue queue
|
|
+receive :: Typeable a => Token -> IO a
|
|
+receive t= receiveReq t >>= return . fromReq
|
|
+
|
|
+flushResponse t@(Token _ _ _ _ _ _ _ qresp)= tryTakeMVar qresp
|
|
+
|
|
+
|
|
+flushRec t@(Token _ _ _ _ _ _ queue _)= tryTakeMVar queue -- !> "flushRec"
|
|
+
|
|
+receiveReq :: Token -> IO Req
|
|
receiveReq t@(Token _ _ _ _ _ _ queue _)= do
|
|
r <- readMVar queue -- !> (">>>>>> receiveReq ")
|
|
- return r -- !> "receiveReq >>>>"
|
|
-
|
|
-fromReq :: Typeable a => Req -> a
|
|
-fromReq (Req x) = x' where
|
|
- x'= case cast x of
|
|
- Nothing -> error $ "receive: received type: "++ show (typeOf x) ++ " does not match the desired type:" ++ show (typeOf x')
|
|
- Just y -> y
|
|
-
|
|
-
|
|
-receiveReqTimeout :: Int
|
|
- -> Integer
|
|
- -> Token
|
|
- -> IO Req
|
|
-receiveReqTimeout 0 0 t= receiveReq t
|
|
-receiveReqTimeout time time2 t=
|
|
- let id= keyWF (twfname t) t in withKillTimeout id time time2 (receiveReq t)
|
|
-
|
|
-
|
|
-delMsgHistory t = do
|
|
- let statKey= keyWF (twfname t) t -- !> "wf" --let qnme= keyWF wfname t
|
|
- delWFHistory1 statKey -- `debug` "delWFHistory"
|
|
-
|
|
-
|
|
-
|
|
--- | executes a simple request-response computation that receive the params and return a response
|
|
---
|
|
--- It is used with `addMessageFlows`
|
|
---
|
|
--- There is a higuer level version @wstateless@ in "MFLow.Forms"
|
|
-stateless :: (Params -> IO HttpData) -> Flow
|
|
-stateless f = transient proc
|
|
- where
|
|
- proc t@(Token _ _ _ _ _ _ queue qresp) = loop t queue qresp
|
|
- loop t queue qresp=do
|
|
- req <- takeMVar queue -- !> (">>>>>> stateless " ++ thread t)
|
|
- resp <- f (getParams req)
|
|
- (putMVar qresp $ Resp resp ) -- !> ("<<<<<< stateless " ++thread t)
|
|
- loop t queue qresp -- !> ("enviado stateless " ++ thread t)
|
|
-
|
|
-
|
|
-
|
|
--- | Executes a monadic computation that send and receive messages, but does
|
|
--- not store its state in permanent storage. The process once stopped, will restart anew
|
|
---
|
|
----- It is used with `addMessageFlows` `hackMessageFlow` or `waiMessageFlow`
|
|
-transient :: (Token -> IO ()) -> Flow
|
|
-transient f= unsafeIOtoWF . f -- WF(\s -> f t>>= \x-> return (s, x) )
|
|
-
|
|
-
|
|
-_messageFlows :: MVar (WorkflowList IO Token ()) -- MVar (M.Map String (Token -> Workflow IO ()))
|
|
-_messageFlows= unsafePerformIO $ newMVar emptyFList
|
|
- where
|
|
- emptyFList= M.empty :: WorkflowList IO Token ()
|
|
-
|
|
--- | add a list of flows to be scheduled. Each entry in the list is a pair @(path, flow)@
|
|
-addMessageFlows wfs= modifyMVar_ _messageFlows(\ms -> return $ M.union (M.fromList $ map flt wfs)ms)
|
|
- where flt ("",f)= (noScript,f)
|
|
- flt e= e
|
|
-
|
|
--- | return the list of the scheduler
|
|
-getMessageFlows = readMVar _messageFlows
|
|
-
|
|
-delMessageFlow wfname= modifyMVar_ _messageFlows (\ms -> return $ M.delete wfname ms)
|
|
-
|
|
-
|
|
-sendToMF Token{..} msg= putMVar tsendq (Req msg) -- !> "sendToMF"
|
|
-
|
|
---recFromMF :: (Typeable a, Typeable c, Processable a) => Token -> a -> IO c
|
|
-recFromMF t@Token{..} = do
|
|
- m <- takeMVar trecq -- !> "recFromMF <<<<<< "
|
|
- case m of
|
|
- Resp r -> return r -- !> "<<<<<< recFromMF"
|
|
- Fragm r -> do
|
|
- result <- getStream r
|
|
- return result
|
|
-
|
|
- where
|
|
- getStream r = do
|
|
- mr <- takeMVar trecq
|
|
- case mr of
|
|
- Fragm h -> do
|
|
- rest <- unsafeInterleaveIO $ getStream h
|
|
- let result= mappend r rest
|
|
- return result
|
|
- EndFragm h -> do
|
|
- let result= mappend r h
|
|
- return result
|
|
-
|
|
- Resp h -> do
|
|
- let result= mappend r h
|
|
- return result
|
|
-
|
|
-
|
|
-
|
|
-
|
|
--- | The scheduler creates a Token with every `Processable`
|
|
--- message that arrives and send the mesage to the appropriate flow, then wait for the response
|
|
--- and return it.
|
|
---
|
|
--- It is the core of the application server. "MFLow.Wai" and "MFlow.Hack" use it
|
|
-msgScheduler
|
|
- :: (Typeable a,Processable a)
|
|
- => a -> IO (HttpData, ThreadId)
|
|
-msgScheduler x = do
|
|
+ return r -- !> "receiveReq >>>>"
|
|
+
|
|
+fromReq :: Typeable a => Req -> a
|
|
+fromReq (Req x) = x' where
|
|
+ x'= case cast x of
|
|
+ Nothing -> error $ "receive: received type: "++ show (typeOf x) ++ " does not match the desired type:" ++ show (typeOf x')
|
|
+ Just y -> y
|
|
+
|
|
+
|
|
+receiveReqTimeout :: Int
|
|
+ -> Integer
|
|
+ -> Token
|
|
+ -> IO Req
|
|
+receiveReqTimeout 0 0 t= receiveReq t
|
|
+receiveReqTimeout time time2 t=
|
|
+ let id= keyWF (twfname t) t in withKillTimeout id time time2 (receiveReq t)
|
|
+
|
|
+
|
|
+delMsgHistory t = do
|
|
+ let statKey= keyWF (twfname t) t -- !> "wf" --let qnme= keyWF wfname t
|
|
+ delWFHistory1 statKey -- `debug` "delWFHistory"
|
|
+
|
|
+
|
|
+
|
|
+-- | executes a simple request-response computation that receive the params and return a response
|
|
+--
|
|
+-- It is used with `addMessageFlows`
|
|
+--
|
|
+-- There is a higuer level version @wstateless@ in "MFLow.Forms"
|
|
+stateless :: (Params -> IO HttpData) -> Flow
|
|
+stateless f = transient proc
|
|
+ where
|
|
+ proc t@(Token _ _ _ _ _ _ queue qresp) = loop t queue qresp
|
|
+ loop t queue qresp=do
|
|
+ req <- takeMVar queue -- !> (">>>>>> stateless " ++ thread t)
|
|
+ resp <- f (getParams req)
|
|
+ (putMVar qresp $ Resp resp ) -- !> ("<<<<<< stateless " ++thread t)
|
|
+ loop t queue qresp -- !> ("enviado stateless " ++ thread t)
|
|
+
|
|
+
|
|
+
|
|
+-- | Executes a monadic computation that send and receive messages, but does
|
|
+-- not store its state in permanent storage. The process once stopped, will restart anew
|
|
+--
|
|
+---- It is used with `addMessageFlows` `hackMessageFlow` or `waiMessageFlow`
|
|
+transient :: (Token -> IO ()) -> Flow
|
|
+transient f= unsafeIOtoWF . f -- WF(\s -> f t>>= \x-> return (s, x) )
|
|
+
|
|
+
|
|
+_messageFlows :: MVar (WorkflowList IO Token ()) -- MVar (M.Map String (Token -> Workflow IO ()))
|
|
+_messageFlows= unsafePerformIO $ newMVar emptyFList
|
|
+ where
|
|
+ emptyFList= M.empty :: WorkflowList IO Token ()
|
|
+
|
|
+-- | add a list of flows to be scheduled. Each entry in the list is a pair @(path, flow)@
|
|
+addMessageFlows wfs= modifyMVar_ _messageFlows(\ms -> return $ M.union (M.fromList $ map flt wfs)ms)
|
|
+ where flt ("",f)= (noScript,f)
|
|
+ flt e= e
|
|
+
|
|
+-- | return the list of the scheduler
|
|
+getMessageFlows = readMVar _messageFlows
|
|
+
|
|
+delMessageFlow wfname= modifyMVar_ _messageFlows (\ms -> return $ M.delete wfname ms)
|
|
+
|
|
+
|
|
+sendToMF Token{..} msg= putMVar tsendq (Req msg) -- !> "sendToMF"
|
|
+
|
|
+--recFromMF :: (Typeable a, Typeable c, Processable a) => Token -> a -> IO c
|
|
+recFromMF t@Token{..} = do
|
|
+ m <- takeMVar trecq -- !> "recFromMF <<<<<< "
|
|
+ case m of
|
|
+ Resp r -> return r -- !> "<<<<<< recFromMF"
|
|
+ Fragm r -> do
|
|
+ result <- getStream r
|
|
+ return result
|
|
+
|
|
+ where
|
|
+ getStream r = do
|
|
+ mr <- takeMVar trecq
|
|
+ case mr of
|
|
+ Fragm h -> do
|
|
+ rest <- unsafeInterleaveIO $ getStream h
|
|
+ let result= mappend r rest
|
|
+ return result
|
|
+ EndFragm h -> do
|
|
+ let result= mappend r h
|
|
+ return result
|
|
+
|
|
+ Resp h -> do
|
|
+ let result= mappend r h
|
|
+ return result
|
|
+
|
|
+
|
|
+
|
|
+
|
|
+-- | The scheduler creates a Token with every `Processable`
|
|
+-- message that arrives and send the mesage to the appropriate flow, then wait for the response
|
|
+-- and return it.
|
|
+--
|
|
+-- It is the core of the application server. "MFLow.Wai" and "MFlow.Hack" use it
|
|
+msgScheduler
|
|
+ :: (Typeable a,Processable a)
|
|
+ => a -> IO (HttpData, ThreadId)
|
|
+msgScheduler x = do
|
|
token <- getToken x
|
|
- th <- myThreadId
|
|
+ th <- myThreadId
|
|
let wfname = takeWhile (/='/') $ pwfname x
|
|
- criticalSection (tblock token) $ do
|
|
- sendToMF token x -- !> show th
|
|
- th <- startMessageFlow wfname token
|
|
- r <- recFromMF token
|
|
- return (r,th) -- !> let HttpData _ _ r1=r in B.unpack r1
|
|
+ criticalSection (tblock token) $ do
|
|
+ sendToMF token x -- !> show th
|
|
+ th <- startMessageFlow wfname token
|
|
+ r <- recFromMF token
|
|
+ return (r,th) -- !> let HttpData _ _ r1=r in B.unpack r1
|
|
where
|
|
- criticalSection mv f= bracket
|
|
- (takeMVar mv)
|
|
- (putMVar mv)
|
|
+ criticalSection mv f= bracket
|
|
+ (takeMVar mv)
|
|
+ (putMVar mv)
|
|
$ const $ f
|
|
-
|
|
- --start the flow if not started yet
|
|
- startMessageFlow wfname token =
|
|
- forkIO $ do
|
|
- wfs <- getMessageFlows
|
|
- r <- startWF wfname token wfs -- !>( "init wf " ++ wfname)
|
|
- case r of
|
|
- Left NotFound -> do
|
|
- (sendFlush token =<< serveFile (pwfname x))
|
|
+
|
|
+ --start the flow if not started yet
|
|
+ startMessageFlow wfname token =
|
|
+ forkIO $ do
|
|
+ wfs <- getMessageFlows
|
|
+ r <- startWF wfname token wfs -- !>( "init wf " ++ wfname)
|
|
+ case r of
|
|
+ Left NotFound -> do
|
|
+ (sendFlush token =<< serveFile (pwfname x))
|
|
`CE.catch` \(e:: CE.SomeException) -> do
|
|
- showError wfname token (show e)
|
|
--- sendFlush token (Error NotFound $ "Not found: " <> pack wfname)
|
|
- deleteTokenInList token
|
|
-
|
|
- Left AlreadyRunning -> return () -- !> ("already Running " ++ wfname)
|
|
-
|
|
- Left Timeout -> do
|
|
- hFlush stdout -- !> ("TIMEOUT in msgScheduler" ++ (show $ unsafePerformIO myThreadId))
|
|
- deleteTokenInList token
|
|
-
|
|
- Left (WFException e)-> do
|
|
- showError wfname token e
|
|
- moveState wfname token token{tind= "error/"++tuser token}
|
|
- deleteTokenInList token -- !> "DELETETOKEN"
|
|
-
|
|
-
|
|
- Right _ -> delMsgHistory token >> return () -- !> ("finished " ++ wfname)
|
|
-
|
|
-
|
|
-
|
|
-showError wfname token@Token{..} e= do
|
|
- t <- return . calendarTimeToString =<< toCalendarTime =<< getClockTime
|
|
- let msg= errorMessage t e tuser (Prelude.concat $ intersperse "/" tpath) tenv
|
|
- logError msg
|
|
- fresp <- getNotFoundResponse
|
|
- let admin= getAdminName
|
|
- sendFlush token . Error $ fresp (tuser== admin) $ Prelude.concat[ "<br/>"++ s | s <- lines msg]
|
|
-
|
|
-
|
|
-errorMessage t e u path env=
|
|
- "\n---------------------ERROR-------------------------\
|
|
- \\nTIME=" ++ t ++"\n\n" ++
|
|
- e++
|
|
- "\n\nUSER= " ++ u ++
|
|
- "\n\nPATH= " ++ path ++
|
|
- "\n\nREQUEST:\n\n" ++
|
|
- show env
|
|
-
|
|
-line= unsafePerformIO $ newMVar ()
|
|
-
|
|
-logError err= do
|
|
- takeMVar line
|
|
- putStrLn err
|
|
- hSeek hlog SeekFromEnd 0
|
|
- hPutStrLn hlog err
|
|
- hFlush hlog
|
|
- putMVar line ()
|
|
-
|
|
-logFileName= "errlog"
|
|
-
|
|
-
|
|
-
|
|
--- | The handler of the error log
|
|
-hlog= unsafePerformIO $ openFile logFileName ReadWriteMode
|
|
-
|
|
------- USER MANAGEMENT -------
|
|
-
|
|
-data Auth = Auth{
|
|
- uregister :: UserStr -> PasswdStr -> (IO (Maybe String)),
|
|
- uvalidate :: UserStr -> PasswdStr -> (IO (Maybe String))}
|
|
-
|
|
-_authMethod= unsafePerformIO $ newIORef $ Auth tCacheRegister tCacheValidate
|
|
-
|
|
--- | set an authentication method
|
|
-setAuthMethod auth= writeIORef _authMethod auth
|
|
-
|
|
-getAuthMethod = readIORef _authMethod
|
|
-
|
|
-
|
|
-data User= User
|
|
- { userName :: String
|
|
- , upassword :: String
|
|
- } deriving (Read, Show, Typeable)
|
|
-
|
|
-
|
|
-eUser= User (error1 "username") (error1 "password")
|
|
-
|
|
-error1 s= error $ s ++ " undefined"
|
|
-
|
|
-userPrefix= "user/"
|
|
-instance Indexable User where
|
|
- key User{userName= user}= keyUserName user
|
|
-
|
|
--- | Return the key name of an user
|
|
-keyUserName n= userPrefix++n
|
|
-
|
|
-instance Serializable User where
|
|
- serialize= B.pack . show
|
|
- deserialize= read . B.unpack
|
|
- setPersist = \_ -> Just filePersist
|
|
-
|
|
--- | Register an user/password
|
|
-tCacheRegister :: String -> String -> IO (Maybe String)
|
|
-tCacheRegister user password = atomically $ do
|
|
- withSTMResources [newuser] doit
|
|
- where
|
|
- newuser= User user password
|
|
- doit [Just (User _ _)] = resources{toReturn= Just "user already exist"}
|
|
- doit [Nothing] = resources{toAdd= [newuser],toReturn= Nothing}
|
|
-
|
|
-tCacheValidate :: UserStr -> PasswdStr -> IO (Maybe String)
|
|
-tCacheValidate u p =
|
|
- let user= eUser{userName=u}
|
|
- in atomically
|
|
- $ withSTMResources [user]
|
|
- $ \ mu -> case mu of
|
|
- [Nothing] -> resources{toReturn= err }
|
|
- [Just (User _ pass )] -> resources{toReturn=
|
|
- case pass==p of
|
|
- True -> Nothing
|
|
- False -> err
|
|
- }
|
|
-
|
|
- where
|
|
- err= Just "Username or password invalid"
|
|
-
|
|
-userRegister u p= liftIO $ do
|
|
- Auth reg _ <- getAuthMethod :: IO Auth
|
|
- reg u p
|
|
-
|
|
-
|
|
-newtype Config= Config1 (M.Map String String) deriving (Read,Show,Typeable)
|
|
-
|
|
---defConfig= Config1 $ M.fromList
|
|
--- [("cadmin","admin")
|
|
--- ,("cjqueryScript","//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js")
|
|
--- ,("cjqueryCSS","//code.jquery.com/ui/1.10.3/themes/smoothness/jquery-ui.css")
|
|
--- ,("cjqueryUI","//code.jquery.com/ui/1.10.3/jquery-ui.js")
|
|
--- ,("cnicEditUrl","//js.nicedit.com/nicEdit-latest.js")]
|
|
-
|
|
+ showError wfname token (show e)
|
|
+-- sendFlush token (Error NotFound $ "Not found: " <> pack wfname)
|
|
+ deleteTokenInList token
|
|
+
|
|
+ Left AlreadyRunning -> return () -- !> ("already Running " ++ wfname)
|
|
+
|
|
+ Left Timeout -> do
|
|
+ hFlush stdout -- !> ("TIMEOUT in msgScheduler" ++ (show $ unsafePerformIO myThreadId))
|
|
+ deleteTokenInList token
|
|
+
|
|
+ Left (WFException e)-> do
|
|
+ showError wfname token e
|
|
+ moveState wfname token token{tind= "error/"++tuser token}
|
|
+ deleteTokenInList token -- !> "DELETETOKEN"
|
|
+
|
|
+
|
|
+ Right _ -> delMsgHistory token >> return () -- !> ("finished " ++ wfname)
|
|
+
|
|
+
|
|
+
|
|
+showError wfname token@Token{..} e= do
|
|
+ t <- return . calendarTimeToString =<< toCalendarTime =<< getClockTime
|
|
+ let msg= errorMessage t e tuser (Prelude.concat $ intersperse "/" tpath) tenv
|
|
+ logError msg
|
|
+ fresp <- getNotFoundResponse
|
|
+ let admin= getAdminName
|
|
+ sendFlush token . Error $ fresp (tuser== admin) $ Prelude.concat[ "<br/>"++ s | s <- lines msg]
|
|
+
|
|
+
|
|
+errorMessage t e u path env=
|
|
+ "\n---------------------ERROR-------------------------\
|
|
+ \\nTIME=" ++ t ++"\n\n" ++
|
|
+ e++
|
|
+ "\n\nUSER= " ++ u ++
|
|
+ "\n\nPATH= " ++ path ++
|
|
+ "\n\nREQUEST:\n\n" ++
|
|
+ show env
|
|
+
|
|
+line= unsafePerformIO $ newMVar ()
|
|
+
|
|
+logError err= do
|
|
+ takeMVar line
|
|
+ putStrLn err
|
|
+ hSeek hlog SeekFromEnd 0
|
|
+ hPutStrLn hlog err
|
|
+ hFlush hlog
|
|
+ putMVar line ()
|
|
+
|
|
+logFileName= "errlog"
|
|
+
|
|
+
|
|
+
|
|
+-- | The handler of the error log
|
|
+hlog= unsafePerformIO $ openFile logFileName ReadWriteMode
|
|
+
|
|
+------ USER MANAGEMENT -------
|
|
+
|
|
+data Auth = Auth{
|
|
+ uregister :: UserStr -> PasswdStr -> (IO (Maybe String)),
|
|
+ uvalidate :: UserStr -> PasswdStr -> (IO (Maybe String))}
|
|
+
|
|
+_authMethod= unsafePerformIO $ newIORef $ Auth tCacheRegister tCacheValidate
|
|
+
|
|
+-- | set an authentication method. That includes the registration and validation calls.
|
|
+-- both return Nothing if sucessful. Otherwise they return a text mesage explaining the failure
|
|
+setAuthMethod auth= writeIORef _authMethod auth
|
|
+
|
|
+getAuthMethod = readIORef _authMethod
|
|
+
|
|
+
|
|
+data User= User
|
|
+ { userName :: String
|
|
+ , upassword :: String
|
|
+ } deriving (Read, Show, Typeable)
|
|
+
|
|
+
|
|
+eUser= User (error1 "username") (error1 "password")
|
|
+
|
|
+error1 s= error $ s ++ " undefined"
|
|
+
|
|
+userPrefix= "user/"
|
|
+instance Indexable User where
|
|
+ key User{userName= user}= keyUserName user
|
|
+
|
|
+-- | Return the key name of an user
|
|
+keyUserName n= userPrefix++n
|
|
+
|
|
+instance Serializable User where
|
|
+ serialize= B.pack . show
|
|
+ deserialize= read . B.unpack
|
|
+ setPersist = \_ -> Just filePersist
|
|
+
|
|
+-- | Register an user/password
|
|
+tCacheRegister :: String -> String -> IO (Maybe String)
|
|
+tCacheRegister user password= tCacheRegister' 14 user password
|
|
+
|
|
+tCacheRegister' strength user password= do
|
|
+ salted_password <- makePassword (SB.pack password) strength
|
|
+ atomically $ do
|
|
+ let newuser = User user (SB.unpack salted_password)
|
|
+ withSTMResources [newuser] $ doit newuser
|
|
+ where
|
|
+ doit newuser [Just (User _ _)] = resources{toReturn= Just "user already exist"}
|
|
+ doit newuser [Nothing] = resources{toAdd= [newuser],toReturn= Nothing}
|
|
+
|
|
+
|
|
+-- withSTMResources [newuser] doit
|
|
+-- where
|
|
+-- newuser= User user password
|
|
+-- doit [Just (User _ _)] = resources{toReturn= Just "user already exist"}
|
|
+-- doit [Nothing] = resources{toAdd= [newuser],toReturn= Nothing}
|
|
+
|
|
+tCacheValidate :: UserStr -> PasswdStr -> IO (Maybe String)
|
|
+tCacheValidate u p =
|
|
+ let user= eUser{userName=u}
|
|
+ in atomically
|
|
+ $ withSTMResources [user]
|
|
+ $ \ mu -> case mu of
|
|
+ [Nothing] -> resources{toReturn= err }
|
|
+ [Just u@(User _ pass )] -> resources{toReturn =
|
|
+ case verifyPassword (SB.pack p) (SB.pack pass)
|
|
+ || pass== p of -- for backward compatibility for unhashed passwords
|
|
+ True -> Nothing
|
|
+ False -> err
|
|
+ }
|
|
+ where
|
|
+ err= Just "Username or password invalid"
|
|
+
|
|
+-- | register an user with the auth Method
|
|
+userRegister :: MonadIO m => UserStr -> PasswdStr -> m (Maybe String)
|
|
+userRegister !u !p= liftIO $ do
|
|
+ Auth reg _ <- getAuthMethod :: IO Auth
|
|
+ reg u p
|
|
+
|
|
+
|
|
+newtype Config= Config1 (M.Map String String) deriving (Read,Show,Typeable)
|
|
+
|
|
+
|
|
data Config0 = Config{cadmin :: UserStr -- ^ Administrator name
|
|
,cjqueryScript :: String -- ^ URL of jquery
|
|
,cjqueryCSS :: String -- ^ URL of jqueryCSS
|
|
,cjqueryUI :: String -- ^ URL of jqueryUI
|
|
,cnicEditUrl :: String -- ^ URL of the nicEdit editor
|
|
}
|
|
- deriving (Read, Show, Typeable)
|
|
+ deriving (Read, Show, Typeable)
|
|
|
|
---defConfig0= Config "admin" "//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"
|
|
--- "//code.jquery.com/ui/1.10.3/themes/smoothness/jquery-ui.css"
|
|
--- "//code.jquery.com/ui/1.10.3/jquery-ui.js"
|
|
--- "//js.nicedit.com/nicEdit-latest.js"
|
|
---
|
|
---writeDefConfig0= writeFile "sal" $ show defConfig0
|
|
|
|
change :: Config0 -> Config
|
|
change Config{..} = Config1 $ M.fromList
|
|
@@ -558,370 +563,366 @@
|
|
|
|
readOld :: ByteString -> Config
|
|
readOld s= (change . read . B.unpack $ s)
|
|
-
|
|
-keyConfig= "mflow.config"
|
|
+
|
|
+keyConfig= "mflow.config"
|
|
instance Indexable Config where key _= keyConfig
|
|
|
|
-rconf :: DBRef Config
|
|
-rconf= getDBRef keyConfig
|
|
-
|
|
-instance Serializable Config where
|
|
- serialize = B.pack . show
|
|
+rconf :: DBRef Config
|
|
+rconf= getDBRef keyConfig
|
|
+
|
|
+instance Serializable Config where
|
|
+ serialize (Config1 c)= B.pack $ "Config1 (fromList[\n" <> (concat . intersperse ",\n" $ map show (M.toList c)) <> "])"
|
|
deserialize s = unsafePerformIO $ (return $! read $! B.unpack s)
|
|
- `CE.catch` \(e :: SomeException) -> return (readOld s)
|
|
- setPersist = \_ -> Just filePersist
|
|
+ `CE.catch` \(e :: SomeException) -> return (readOld s)
|
|
+ setPersist = \_ -> Just filePersist
|
|
|
|
-- | read a config variable from the config file \"mflow.config\". if it is not set, uses the second parameter and
|
|
-- add it to the configuration list, so next time the administrator can change it in the configuration file
|
|
-getConfig k v= case M.lookup k config of
|
|
+getConfig k v= case M.lookup k config of
|
|
Nothing -> unsafePerformIO $ setConfig k v >> return v
|
|
Just s -> s
|
|
|
|
-- | set an user-defined config variable
|
|
-setConfig k v= atomically $ do
|
|
- Config1 conf <- readConfig
|
|
- writeDBRef rconf $ Config1 $ M.insert k v conf
|
|
+setConfig k v= atomically $ do
|
|
+ Config1 conf <- readConfig
|
|
+ writeDBRef rconf $ Config1 $ M.insert k v conf
|
|
|
|
|
|
-- user ---
|
|
-
|
|
-type UserStr= String
|
|
-type PasswdStr= String
|
|
+
|
|
+type UserStr= String
|
|
+type PasswdStr= String
|
|
|
|
|
|
-- | set the Administrator user and password.
|
|
-- It must be defined in Main , before any configuration parameter is read, before the execution
|
|
--- of any flow
|
|
-setAdminUser :: MonadIO m => UserStr -> PasswdStr -> m ()
|
|
-setAdminUser user password= liftIO $ do
|
|
+-- of any flow
|
|
+setAdminUser :: MonadIO m => UserStr -> PasswdStr -> m ()
|
|
+setAdminUser user password= liftIO $ do
|
|
userRegister user password
|
|
setConfig "cadmin" user
|
|
--- atomically $ do
|
|
--- Config1 conf <- readConfig
|
|
--- writeDBRef rconf $ Config1 $ M.insert "cadmin" user conf
|
|
-
|
|
-
|
|
-
|
|
-getAdminName= getConfig "cadmin" "admin"
|
|
-
|
|
-
|
|
---------------- ERROR RESPONSES --------
|
|
-
|
|
-defNotFoundResponse isAdmin msg= fresp $
|
|
- case isAdmin of
|
|
- True -> B.pack msg
|
|
- _ -> "The administrator has been notified"
|
|
- where
|
|
- fresp msg=
|
|
- "<html><h4>Error 404: Page not found or error ocurred</h4> <p style=\"font-family:courier\">" <> msg <>"</p>" <>
|
|
- "<br/>" <> opts <> "<br/><a href=\"/\" >press here to go home</a></html>"
|
|
-
|
|
-
|
|
- paths= Prelude.map B.pack . M.keys $ unsafePerformIO getMessageFlows
|
|
- opts= "options: " <> B.concat (Prelude.map (\s ->
|
|
- "<a href=\"/"<> s <>"\">"<> s <>"</a>, ") $ filter (\s -> B.head s /= '_') paths)
|
|
-
|
|
-notFoundResponse= unsafePerformIO $ newIORef defNotFoundResponse
|
|
-
|
|
--- | set the 404 "not found" response.
|
|
---
|
|
--- The parameter is as follows:
|
|
--- (Bool Either if the user is Administrator or not
|
|
--- -> String The error string
|
|
--- -> HttpData) The response. See `defNotFoundResponse` code for an example
|
|
-
|
|
-setNotFoundResponse ::
|
|
- (Bool
|
|
- -> String
|
|
- -> ByteString)
|
|
- -> IO ()
|
|
-
|
|
-setNotFoundResponse f= liftIO $ writeIORef notFoundResponse f
|
|
-getNotFoundResponse= liftIO $ readIORef notFoundResponse
|
|
-
|
|
---------------- BASIC BYTESTRING TAGS -------------------
|
|
-
|
|
-
|
|
-type Attribs= [(String,String)]
|
|
--- | Writes a XML tag in a ByteString. It is the most basic form of formatting. For
|
|
--- more sophisticated formatting , use "MFlow.Forms.XHtml" or "MFlow.Forms.HSP".
|
|
-btag :: String -> Attribs -> ByteString -> ByteString
|
|
-btag t rs v= "<" <> pt <> attrs rs <> ">" <> v <> "</" <> pt <> ">"
|
|
- where
|
|
- pt= B.pack t
|
|
- attrs []= B.empty
|
|
- attrs rs= B.pack $ concatMap(\(n,v) -> (' ' : n) ++ "=\"" ++ v++ "\"" ) rs
|
|
-
|
|
--- |
|
|
--- > bhtml ats v= btag "html" ats v
|
|
-bhtml :: Attribs -> ByteString -> ByteString
|
|
-bhtml ats v= btag "html" ats v
|
|
-
|
|
-
|
|
--- |
|
|
--- > bbody ats v= btag "body" ats v
|
|
-bbody :: Attribs -> ByteString -> ByteString
|
|
-bbody ats v= btag "body" ats v
|
|
-
|
|
-addAttrs :: ByteString -> Attribs -> ByteString
|
|
-addAttrs (Chunk "<" (Chunk tag rest)) rs=
|
|
- Chunk "<"(Chunk tag (B.pack $ concatMap(\(n,v) -> (' ' : n) ++ "=" ++ v ) rs)) <> rest
|
|
-
|
|
-addAttrs other _ = error $ "addAttrs: byteString is not a tag: " ++ show other
|
|
-
|
|
-
|
|
-------------------- FILE SERVER -----------
|
|
-
|
|
--- | Set the path of the files in the web server. The links to the files are relative to it.
|
|
--- The files are cached (memoized) according with the "Data.TCache" policies in the program space. This avoid the blocking of
|
|
--- the efficient GHC threads by frequent IO calls.So it enhances the performance
|
|
--- in the context of heavy concurrence.
|
|
--- It uses 'Data.TCache.Memoization'.
|
|
--- The caching-uncaching follows the `setPersist` criteria
|
|
-setFilesPath :: MonadIO m => String -> m ()
|
|
-setFilesPath path= liftIO $ writeIORef rfilesPath path
|
|
-
|
|
-rfilesPath= unsafePerformIO $ newIORef "files/"
|
|
-
|
|
-serveFile path'= do
|
|
- when(let hpath= Prelude.head path' in hpath == '/' || hpath =='\\') $ error noperm
|
|
- when(not(".." `isSuffixOf` path') && ".." `isInfixOf` path') $ error noperm
|
|
- filesPath <- readIORef rfilesPath
|
|
- let path= filesPath ++ path'
|
|
- mr <- cachedByKey path 0 $ (B.readFile path >>= return . Just) `CE.catch` ioerr (return Nothing)
|
|
- case mr of
|
|
- Nothing -> error "not found" -- return $ HttpData [setMime "text/plain"] [] $ pack $ "not accessible"
|
|
- Just r ->
|
|
- let ext = reverse . takeWhile (/='.') $ reverse path
|
|
- mmime= lookup (map toLower ext) mimeTable
|
|
- mime = case mmime of Just m -> m ;Nothing -> "application/octet-stream"
|
|
-
|
|
- in return $ HttpData [setMime mime, ("Cache-Control", "max-age=360000")] [] r
|
|
- where
|
|
- noperm= "no permissions"
|
|
- ioerr x= \(e :: CE.IOException) -> x
|
|
- setMime x= ("Content-Type",x)
|
|
-
|
|
---------------------- FLOW ID GENERATOR ------------
|
|
-
|
|
-data NFlow= NFlow !Integer deriving (Read, Show, Typeable)
|
|
-
|
|
-
|
|
-
|
|
-instance Indexable NFlow where
|
|
- key _= "Flow"
|
|
-
|
|
-instance Serializable NFlow where
|
|
- serialize= B.pack . show
|
|
- deserialize= read . B.unpack
|
|
- setPersist = \_ -> Just filePersist
|
|
-
|
|
-rflow= getDBRef . key $ NFlow undefined
|
|
-
|
|
-newFlow= do
|
|
- TOD t _ <- getClockTime
|
|
- atomically $ do
|
|
- NFlow n <- readDBRef rflow `onNothing` return (NFlow 0)
|
|
- writeDBRef rflow . NFlow $ n+1
|
|
- return . SB.pack . show $ t + n
|
|
-
|
|
-
|
|
-mimeTable=[
|
|
- ("html", "text/html"),
|
|
- ("htm", "text/html"),
|
|
- ("txt", "text/plain"),
|
|
- ("hs", "text/plain"),
|
|
- ("lhs", "text/plain"),
|
|
- ("jpeg", "image/jpeg"),
|
|
- ("pdf", "application/pdf"),
|
|
- ("js", "application/x-javascript"),
|
|
- ("gif", "image/gif"),
|
|
- ("bmp", "image/bmp"),
|
|
- ("ico", "image/x-icon"),
|
|
- ("doc", "application/msword"),
|
|
- ("jpg", "image/jpeg"),
|
|
- ("eps", "application/postscript"),
|
|
- ("zip", "application/zip"),
|
|
- ("exe", "application/octet-stream"),
|
|
- ("tif", "image/tiff"),
|
|
- ("tiff", "image/tiff"),
|
|
- ("mov", "video/quicktime"),
|
|
- ("movie", "video/x-sgi-movie"),
|
|
- ("mp2", "video/mpeg"),
|
|
- ("mp3", "audio/mpeg"),
|
|
- ("mpa", "video/mpeg"),
|
|
- ("mpe", "video/mpeg"),
|
|
- ("mpeg", "video/mpeg"),
|
|
- ("mpg", "video/mpeg"),
|
|
- ("mpp", "application/vnd.ms-project"),
|
|
- ("323", "text/h323"),
|
|
- ("*", "application/octet-stream"),
|
|
- ("acx", "application/internet-property-stream"),
|
|
- ("ai", "application/postscript"),
|
|
- ("aif", "audio/x-aiff"),
|
|
- ("aifc", "audio/x-aiff"),
|
|
- ("aiff", "audio/x-aiff"),
|
|
- ("asf", "video/x-ms-asf"),
|
|
- ("asr", "video/x-ms-asf"),
|
|
- ("asx", "video/x-ms-asf"),
|
|
- ("au", "audio/basic"),
|
|
- ("avi", "video/x-msvideo"),
|
|
- ("axs", "application/olescript"),
|
|
- ("bas", "text/plain"),
|
|
- ("bcpio", "application/x-bcpio"),
|
|
- ("bin", "application/octet-stream"),
|
|
- ("c", "text/plain"),
|
|
- ("cat", "application/vnd.ms-pkiseccat"),
|
|
- ("cdf", "application/x-cdf"),
|
|
- ("cdf", "application/x-netcdf"),
|
|
- ("cer", "application/x-x509-ca-cert"),
|
|
- ("class", "application/octet-stream"),
|
|
- ("clp", "application/x-msclip"),
|
|
- ("cmx", "image/x-cmx"),
|
|
- ("cod", "image/cis-cod"),
|
|
- ("cpio", "application/x-cpio"),
|
|
- ("crd", "application/x-mscardfile"),
|
|
- ("crl", "application/pkix-crl"),
|
|
- ("crt", "application/x-x509-ca-cert"),
|
|
- ("csh", "application/x-csh"),
|
|
- ("css", "text/css"),
|
|
- ("dcr", "application/x-director"),
|
|
- ("der", "application/x-x509-ca-cert"),
|
|
- ("dir", "application/x-director"),
|
|
- ("dll", "application/x-msdownload"),
|
|
- ("dms", "application/octet-stream"),
|
|
- ("dot", "application/msword"),
|
|
- ("dvi", "application/x-dvi"),
|
|
- ("dxr", "application/x-director"),
|
|
- ("eps", "application/postscript"),
|
|
- ("etx", "text/x-setext"),
|
|
- ("evy", "application/envoy"),
|
|
- ("fif", "application/fractals"),
|
|
- ("flr", "x-world/x-vrml"),
|
|
- ("gtar", "application/x-gtar"),
|
|
- ("gz", "application/x-gzip"),
|
|
- ("h", "text/plain"),
|
|
- ("hdf", "application/x-hdf"),
|
|
- ("hlp", "application/winhlp"),
|
|
- ("hqx", "application/mac-binhex40"),
|
|
- ("hta", "application/hta"),
|
|
- ("htc", "text/x-component"),
|
|
- ("htt", "text/webviewhtml"),
|
|
- ("ief", "image/ief"),
|
|
- ("iii", "application/x-iphone"),
|
|
- ("ins", "application/x-internet-signup"),
|
|
- ("isp", "application/x-internet-signup"),
|
|
- ("jfif", "image/pipeg"),
|
|
- ("jpe", "image/jpeg"),
|
|
- ("latex", "application/x-latex"),
|
|
- ("lha", "application/octet-stream"),
|
|
- ("lsf", "video/x-la-asf"),
|
|
- ("lsx", "video/x-la-asf"),
|
|
- ("lzh", "application/octet-stream"),
|
|
- ("m13", "application/x-msmediaview"),
|
|
- ("m14", "application/x-msmediaview"),
|
|
- ("m3u", "audio/x-mpegurl"),
|
|
- ("man", "application/x-troff-man"),
|
|
- ("mdb", "application/x-msaccess"),
|
|
- ("me", "application/x-troff-me"),
|
|
- ("mht", "message/rfc822"),
|
|
- ("mhtml", "message/rfc822"),
|
|
- ("mid", "audio/mid"),
|
|
- ("mny", "application/x-msmoney"),
|
|
- ("mpv2", "video/mpeg"),
|
|
- ("ms", "application/x-troff-ms"),
|
|
- ("msg", "application/vnd.ms-outlook"),
|
|
- ("mvb", "application/x-msmediaview"),
|
|
- ("nc", "application/x-netcdf"),
|
|
- ("nws", "message/rfc822"),
|
|
- ("oda", "application/oda"),
|
|
- ("p10", "application/pkcs10"),
|
|
- ("p12", "application/x-pkcs12"),
|
|
- ("p7b", "application/x-pkcs7-certificates"),
|
|
- ("p7c", "application/x-pkcs7-mime"),
|
|
- ("p7m", "application/x-pkcs7-mime"),
|
|
- ("p7r", "application/x-pkcs7-certreqresp"),
|
|
- ("p7s", "application/x-pkcs7-signature"),
|
|
- ("png", "image/png"),
|
|
- ("pbm", "image/x-portable-bitmap"),
|
|
- ("pfx", "application/x-pkcs12"),
|
|
- ("pgm", "image/x-portable-graymap"),
|
|
- ("pko", "application/ynd.ms-pkipko"),
|
|
- ("pma", "application/x-perfmon"),
|
|
- ("pmc", "application/x-perfmon"),
|
|
- ("pml", "application/x-perfmon"),
|
|
- ("pmr", "application/x-perfmon"),
|
|
- ("pmw", "application/x-perfmon"),
|
|
- ("pnm", "image/x-portable-anymap"),
|
|
- ("pot", "application/vnd.ms-powerpoint"),
|
|
- ("ppm", "image/x-portable-pixmap"),
|
|
- ("pps", "application/vnd.ms-powerpoint"),
|
|
- ("ppt", "application/vnd.ms-powerpoint"),
|
|
- ("prf", "application/pics-rules"),
|
|
- ("ps", "application/postscript"),
|
|
- ("pub", "application/x-mspublisher"),
|
|
- ("qt", "video/quicktime"),
|
|
- ("ra", "audio/x-pn-realaudio"),
|
|
- ("ram", "audio/x-pn-realaudio"),
|
|
- ("ras", "image/x-cmu-raster"),
|
|
- ("rgb", "image/x-rgb"),
|
|
- ("rmi", "audio/mid"),
|
|
- ("roff", "application/x-troff"),
|
|
- ("rtf", "application/rtf"),
|
|
- ("rtx", "text/richtext"),
|
|
- ("scd", "application/x-msschedule"),
|
|
- ("sct", "text/scriptlet"),
|
|
- ("setpay", "application/set-payment-initiation"),
|
|
- ("setreg", "application/set-registration-initiation"),
|
|
- ("sh", "application/x-sh"),
|
|
- ("shar", "application/x-shar"),
|
|
- ("sit", "application/x-stuffit"),
|
|
- ("snd", "audio/basic"),
|
|
- ("spc", "application/x-pkcs7-certificates"),
|
|
- ("spl", "application/futuresplash"),
|
|
- ("src", "application/x-wais-source"),
|
|
- ("sst", "application/vnd.ms-pkicertstore"),
|
|
- ("stl", "application/vnd.ms-pkistl"),
|
|
- ("stm", "text/html"),
|
|
- ("sv4cpio", "application/x-sv4cpio"),
|
|
- ("sv4crc", "application/x-sv4crc"),
|
|
- ("svg", "image/svg+xml"),
|
|
- ("swf", "application/x-shockwave-flash"),
|
|
- ("t", "application/x-troff"),
|
|
- ("tar", "application/x-tar"),
|
|
- ("tcl", "application/x-tcl"),
|
|
- ("tex", "application/x-tex"),
|
|
- ("texi", "application/x-texinfo"),
|
|
- ("texinfo", "application/x-texinfo"),
|
|
- ("tgz", "application/x-compressed"),
|
|
- ("tr", "application/x-troff"),
|
|
- ("trm", "application/x-msterminal"),
|
|
- ("tsv", "text/tab-separated-values"),
|
|
- ("uls", "text/iuls"),
|
|
- ("ustar", "application/x-ustar"),
|
|
- ("vcf", "text/x-vcard"),
|
|
- ("vrml", "x-world/x-vrml"),
|
|
- ("wav", "audio/x-wav"),
|
|
- ("wcm", "application/vnd.ms-works"),
|
|
- ("wdb", "application/vnd.ms-works"),
|
|
- ("wks", "application/vnd.ms-works"),
|
|
- ("wmf", "application/x-msmetafile"),
|
|
- ("wps", "application/vnd.ms-works"),
|
|
- ("wri", "application/x-mswrite"),
|
|
- ("wrl", "x-world/x-vrml"),
|
|
- ("wrz", "x-world/x-vrml"),
|
|
- ("xaf", "x-world/x-vrml"),
|
|
- ("xbm", "image/x-xbitmap"),
|
|
- ("xla", "application/vnd.ms-excel"),
|
|
- ("xlc", "application/vnd.ms-excel"),
|
|
- ("xlm", "application/vnd.ms-excel"),
|
|
- ("xls", "application/vnd.ms-excel"),
|
|
- ("xlt", "application/vnd.ms-excel"),
|
|
- ("xlw", "application/vnd.ms-excel"),
|
|
- ("xof", "x-world/x-vrml"),
|
|
- ("xpm", "image/x-xpixmap"),
|
|
- ("xwd", "image/x-xwindowdump"),
|
|
- ("z", "application/x-compress")
|
|
-
|
|
- ]
|
|
-
|
|
+
|
|
+
|
|
+getAdminName= getConfig "cadmin" "admin"
|
|
+
|
|
+
|
|
+--------------- ERROR RESPONSES --------
|
|
+
|
|
+defNotFoundResponse isAdmin msg= fresp $
|
|
+ case isAdmin of
|
|
+ True -> B.pack msg
|
|
+ _ -> "The administrator has been notified"
|
|
+ where
|
|
+ fresp msg=
|
|
+ "<html><h4>Error 404: Page not found or error ocurred</h4> <p style=\"font-family:courier\">" <> msg <>"</p>" <>
|
|
+ "<br/>" <> opts <> "<br/><a href=\"/\" >press here to go home</a></html>"
|
|
+
|
|
+
|
|
+ paths= Prelude.map B.pack . M.keys $ unsafePerformIO getMessageFlows
|
|
+ opts= "options: " <> B.concat (Prelude.map (\s ->
|
|
+ "<a href=\"/"<> s <>"\">"<> s <>"</a>, ") $ filter (\s -> B.head s /= '_') paths)
|
|
+
|
|
+notFoundResponse= unsafePerformIO $ newIORef defNotFoundResponse
|
|
+
|
|
+-- | set the 404 "not found" response.
|
|
+--
|
|
+-- The parameter is as follows:
|
|
+-- (Bool Either if the user is Administrator or not
|
|
+-- -> String The error string
|
|
+-- -> HttpData) The response. See `defNotFoundResponse` code for an example
|
|
+
|
|
+setNotFoundResponse ::
|
|
+ (Bool
|
|
+ -> String
|
|
+ -> ByteString)
|
|
+ -> IO ()
|
|
+
|
|
+setNotFoundResponse f= liftIO $ writeIORef notFoundResponse f
|
|
+getNotFoundResponse= liftIO $ readIORef notFoundResponse
|
|
+
|
|
+--------------- BASIC BYTESTRING TAGS -------------------
|
|
+
|
|
+
|
|
+type Attribs= [(String,String)]
|
|
+-- | Writes a XML tag in a ByteString. It is the most basic form of formatting. For
|
|
+-- more sophisticated formatting , use "MFlow.Forms.XHtml" or "MFlow.Forms.HSP".
|
|
+btag :: String -> Attribs -> ByteString -> ByteString
|
|
+btag t rs v= "<" <> pt <> attrs rs <> ">" <> v <> "</" <> pt <> ">"
|
|
+ where
|
|
+ pt= B.pack t
|
|
+ attrs []= B.empty
|
|
+ attrs rs= B.pack $ concatMap(\(n,v) -> (' ' : n) ++ "=\"" ++ v++ "\"" ) rs
|
|
+
|
|
+-- |
|
|
+-- > bhtml ats v= btag "html" ats v
|
|
+bhtml :: Attribs -> ByteString -> ByteString
|
|
+bhtml ats v= btag "html" ats v
|
|
+
|
|
+
|
|
+-- |
|
|
+-- > bbody ats v= btag "body" ats v
|
|
+bbody :: Attribs -> ByteString -> ByteString
|
|
+bbody ats v= btag "body" ats v
|
|
+
|
|
+addAttrs :: ByteString -> Attribs -> ByteString
|
|
+addAttrs (Chunk "<" (Chunk tag rest)) rs=
|
|
+ Chunk "<"(Chunk tag (B.pack $ concatMap(\(n,v) -> (' ' : n) ++ "=" ++ v ) rs)) <> rest
|
|
+
|
|
+addAttrs other _ = error $ "addAttrs: byteString is not a tag: " ++ show other
|
|
+
|
|
+
|
|
+------------------- FILE SERVER -----------
|
|
+
|
|
+-- | Set the path of the files in the web server. The links to the files are relative to it.
|
|
+-- The files are cached (memoized) according with the "Data.TCache" policies in the program space. This avoid the blocking of
|
|
+-- the efficient GHC threads by frequent IO calls.So it enhances the performance
|
|
+-- in the context of heavy concurrence.
|
|
+-- It uses 'Data.TCache.Memoization'.
|
|
+-- The caching-uncaching follows the `setPersist` criteria
|
|
+setFilesPath :: MonadIO m => String -> m ()
|
|
+setFilesPath !path= liftIO $ writeIORef rfilesPath path
|
|
+
|
|
+rfilesPath= unsafePerformIO $ newIORef "files/"
|
|
+
|
|
+serveFile path'= do
|
|
+ when(let hpath= Prelude.head path' in hpath == '/' || hpath =='\\') $ error noperm
|
|
+ when(not(".." `isSuffixOf` path') && ".." `isInfixOf` path') $ error noperm
|
|
+ filesPath <- readIORef rfilesPath
|
|
+ let path= filesPath ++ path'
|
|
+ mr <- cachedByKey path 0 $ (B.readFile path >>= return . Just) `CE.catch` ioerr (return Nothing)
|
|
+ case mr of
|
|
+ Nothing -> error "not found" -- return $ HttpData [setMime "text/plain"] [] $ pack $ "not accessible"
|
|
+ Just r ->
|
|
+ let ext = reverse . takeWhile (/='.') $ reverse path
|
|
+ mmime= lookup (map toLower ext) mimeTable
|
|
+ mime = case mmime of Just m -> m ;Nothing -> "application/octet-stream"
|
|
+
|
|
+ in return $ HttpData [setMime mime, ("Cache-Control", "max-age=360000")] [] r
|
|
+ where
|
|
+ noperm= "no permissions"
|
|
+ ioerr x= \(e :: CE.IOException) -> x
|
|
+ setMime x= ("Content-Type",x)
|
|
+
|
|
+--------------------- FLOW ID GENERATOR ------------
|
|
+
|
|
+data NFlow= NFlow !Integer deriving (Read, Show, Typeable)
|
|
+
|
|
+
|
|
+
|
|
+instance Indexable NFlow where
|
|
+ key _= "Flow"
|
|
+
|
|
+instance Serializable NFlow where
|
|
+ serialize= B.pack . show
|
|
+ deserialize= read . B.unpack
|
|
+ setPersist = \_ -> Just filePersist
|
|
+
|
|
+rflow= getDBRef . key $ NFlow undefined
|
|
+
|
|
+newFlow= do
|
|
+ TOD t _ <- getClockTime
|
|
+ atomically $ do
|
|
+ NFlow n <- readDBRef rflow `onNothing` return (NFlow 0)
|
|
+ writeDBRef rflow . NFlow $ n+1
|
|
+ return . SB.pack . show $ t + n
|
|
+
|
|
+
|
|
+mimeTable=[
|
|
+ ("html", "text/html"),
|
|
+ ("htm", "text/html"),
|
|
+ ("txt", "text/plain"),
|
|
+ ("hs", "text/plain"),
|
|
+ ("lhs", "text/plain"),
|
|
+ ("jpeg", "image/jpeg"),
|
|
+ ("pdf", "application/pdf"),
|
|
+ ("js", "application/x-javascript"),
|
|
+ ("gif", "image/gif"),
|
|
+ ("bmp", "image/bmp"),
|
|
+ ("ico", "image/x-icon"),
|
|
+ ("doc", "application/msword"),
|
|
+ ("jpg", "image/jpeg"),
|
|
+ ("eps", "application/postscript"),
|
|
+ ("zip", "application/zip"),
|
|
+ ("exe", "application/octet-stream"),
|
|
+ ("tif", "image/tiff"),
|
|
+ ("tiff", "image/tiff"),
|
|
+ ("mov", "video/quicktime"),
|
|
+ ("movie", "video/x-sgi-movie"),
|
|
+ ("mp2", "video/mpeg"),
|
|
+ ("mp3", "audio/mpeg"),
|
|
+ ("mpa", "video/mpeg"),
|
|
+ ("mpe", "video/mpeg"),
|
|
+ ("mpeg", "video/mpeg"),
|
|
+ ("mpg", "video/mpeg"),
|
|
+ ("mpp", "application/vnd.ms-project"),
|
|
+ ("323", "text/h323"),
|
|
+ ("*", "application/octet-stream"),
|
|
+ ("acx", "application/internet-property-stream"),
|
|
+ ("ai", "application/postscript"),
|
|
+ ("aif", "audio/x-aiff"),
|
|
+ ("aifc", "audio/x-aiff"),
|
|
+ ("aiff", "audio/x-aiff"),
|
|
+ ("asf", "video/x-ms-asf"),
|
|
+ ("asr", "video/x-ms-asf"),
|
|
+ ("asx", "video/x-ms-asf"),
|
|
+ ("au", "audio/basic"),
|
|
+ ("avi", "video/x-msvideo"),
|
|
+ ("axs", "application/olescript"),
|
|
+ ("bas", "text/plain"),
|
|
+ ("bcpio", "application/x-bcpio"),
|
|
+ ("bin", "application/octet-stream"),
|
|
+ ("c", "text/plain"),
|
|
+ ("cat", "application/vnd.ms-pkiseccat"),
|
|
+ ("cdf", "application/x-cdf"),
|
|
+ ("cdf", "application/x-netcdf"),
|
|
+ ("cer", "application/x-x509-ca-cert"),
|
|
+ ("class", "application/octet-stream"),
|
|
+ ("clp", "application/x-msclip"),
|
|
+ ("cmx", "image/x-cmx"),
|
|
+ ("cod", "image/cis-cod"),
|
|
+ ("cpio", "application/x-cpio"),
|
|
+ ("crd", "application/x-mscardfile"),
|
|
+ ("crl", "application/pkix-crl"),
|
|
+ ("crt", "application/x-x509-ca-cert"),
|
|
+ ("csh", "application/x-csh"),
|
|
+ ("css", "text/css"),
|
|
+ ("dcr", "application/x-director"),
|
|
+ ("der", "application/x-x509-ca-cert"),
|
|
+ ("dir", "application/x-director"),
|
|
+ ("dll", "application/x-msdownload"),
|
|
+ ("dms", "application/octet-stream"),
|
|
+ ("dot", "application/msword"),
|
|
+ ("dvi", "application/x-dvi"),
|
|
+ ("dxr", "application/x-director"),
|
|
+ ("eps", "application/postscript"),
|
|
+ ("etx", "text/x-setext"),
|
|
+ ("evy", "application/envoy"),
|
|
+ ("fif", "application/fractals"),
|
|
+ ("flr", "x-world/x-vrml"),
|
|
+ ("gtar", "application/x-gtar"),
|
|
+ ("gz", "application/x-gzip"),
|
|
+ ("h", "text/plain"),
|
|
+ ("hdf", "application/x-hdf"),
|
|
+ ("hlp", "application/winhlp"),
|
|
+ ("hqx", "application/mac-binhex40"),
|
|
+ ("hta", "application/hta"),
|
|
+ ("htc", "text/x-component"),
|
|
+ ("htt", "text/webviewhtml"),
|
|
+ ("ief", "image/ief"),
|
|
+ ("iii", "application/x-iphone"),
|
|
+ ("ins", "application/x-internet-signup"),
|
|
+ ("isp", "application/x-internet-signup"),
|
|
+ ("jfif", "image/pipeg"),
|
|
+ ("jpe", "image/jpeg"),
|
|
+ ("latex", "application/x-latex"),
|
|
+ ("lha", "application/octet-stream"),
|
|
+ ("lsf", "video/x-la-asf"),
|
|
+ ("lsx", "video/x-la-asf"),
|
|
+ ("lzh", "application/octet-stream"),
|
|
+ ("m13", "application/x-msmediaview"),
|
|
+ ("m14", "application/x-msmediaview"),
|
|
+ ("m3u", "audio/x-mpegurl"),
|
|
+ ("man", "application/x-troff-man"),
|
|
+ ("mdb", "application/x-msaccess"),
|
|
+ ("me", "application/x-troff-me"),
|
|
+ ("mht", "message/rfc822"),
|
|
+ ("mhtml", "message/rfc822"),
|
|
+ ("mid", "audio/mid"),
|
|
+ ("mny", "application/x-msmoney"),
|
|
+ ("mpv2", "video/mpeg"),
|
|
+ ("ms", "application/x-troff-ms"),
|
|
+ ("msg", "application/vnd.ms-outlook"),
|
|
+ ("mvb", "application/x-msmediaview"),
|
|
+ ("nc", "application/x-netcdf"),
|
|
+ ("nws", "message/rfc822"),
|
|
+ ("oda", "application/oda"),
|
|
+ ("p10", "application/pkcs10"),
|
|
+ ("p12", "application/x-pkcs12"),
|
|
+ ("p7b", "application/x-pkcs7-certificates"),
|
|
+ ("p7c", "application/x-pkcs7-mime"),
|
|
+ ("p7m", "application/x-pkcs7-mime"),
|
|
+ ("p7r", "application/x-pkcs7-certreqresp"),
|
|
+ ("p7s", "application/x-pkcs7-signature"),
|
|
+ ("png", "image/png"),
|
|
+ ("pbm", "image/x-portable-bitmap"),
|
|
+ ("pfx", "application/x-pkcs12"),
|
|
+ ("pgm", "image/x-portable-graymap"),
|
|
+ ("pko", "application/ynd.ms-pkipko"),
|
|
+ ("pma", "application/x-perfmon"),
|
|
+ ("pmc", "application/x-perfmon"),
|
|
+ ("pml", "application/x-perfmon"),
|
|
+ ("pmr", "application/x-perfmon"),
|
|
+ ("pmw", "application/x-perfmon"),
|
|
+ ("pnm", "image/x-portable-anymap"),
|
|
+ ("pot", "application/vnd.ms-powerpoint"),
|
|
+ ("ppm", "image/x-portable-pixmap"),
|
|
+ ("pps", "application/vnd.ms-powerpoint"),
|
|
+ ("ppt", "application/vnd.ms-powerpoint"),
|
|
+ ("prf", "application/pics-rules"),
|
|
+ ("ps", "application/postscript"),
|
|
+ ("pub", "application/x-mspublisher"),
|
|
+ ("qt", "video/quicktime"),
|
|
+ ("ra", "audio/x-pn-realaudio"),
|
|
+ ("ram", "audio/x-pn-realaudio"),
|
|
+ ("ras", "image/x-cmu-raster"),
|
|
+ ("rgb", "image/x-rgb"),
|
|
+ ("rmi", "audio/mid"),
|
|
+ ("roff", "application/x-troff"),
|
|
+ ("rtf", "application/rtf"),
|
|
+ ("rtx", "text/richtext"),
|
|
+ ("scd", "application/x-msschedule"),
|
|
+ ("sct", "text/scriptlet"),
|
|
+ ("setpay", "application/set-payment-initiation"),
|
|
+ ("setreg", "application/set-registration-initiation"),
|
|
+ ("sh", "application/x-sh"),
|
|
+ ("shar", "application/x-shar"),
|
|
+ ("sit", "application/x-stuffit"),
|
|
+ ("snd", "audio/basic"),
|
|
+ ("spc", "application/x-pkcs7-certificates"),
|
|
+ ("spl", "application/futuresplash"),
|
|
+ ("src", "application/x-wais-source"),
|
|
+ ("sst", "application/vnd.ms-pkicertstore"),
|
|
+ ("stl", "application/vnd.ms-pkistl"),
|
|
+ ("stm", "text/html"),
|
|
+ ("sv4cpio", "application/x-sv4cpio"),
|
|
+ ("sv4crc", "application/x-sv4crc"),
|
|
+ ("svg", "image/svg+xml"),
|
|
+ ("swf", "application/x-shockwave-flash"),
|
|
+ ("t", "application/x-troff"),
|
|
+ ("tar", "application/x-tar"),
|
|
+ ("tcl", "application/x-tcl"),
|
|
+ ("tex", "application/x-tex"),
|
|
+ ("texi", "application/x-texinfo"),
|
|
+ ("texinfo", "application/x-texinfo"),
|
|
+ ("tgz", "application/x-compressed"),
|
|
+ ("tr", "application/x-troff"),
|
|
+ ("trm", "application/x-msterminal"),
|
|
+ ("tsv", "text/tab-separated-values"),
|
|
+ ("uls", "text/iuls"),
|
|
+ ("ustar", "application/x-ustar"),
|
|
+ ("vcf", "text/x-vcard"),
|
|
+ ("vrml", "x-world/x-vrml"),
|
|
+ ("wav", "audio/x-wav"),
|
|
+ ("wcm", "application/vnd.ms-works"),
|
|
+ ("wdb", "application/vnd.ms-works"),
|
|
+ ("wks", "application/vnd.ms-works"),
|
|
+ ("wmf", "application/x-msmetafile"),
|
|
+ ("wps", "application/vnd.ms-works"),
|
|
+ ("wri", "application/x-mswrite"),
|
|
+ ("wrl", "x-world/x-vrml"),
|
|
+ ("wrz", "x-world/x-vrml"),
|
|
+ ("xaf", "x-world/x-vrml"),
|
|
+ ("xbm", "image/x-xbitmap"),
|
|
+ ("xla", "application/vnd.ms-excel"),
|
|
+ ("xlc", "application/vnd.ms-excel"),
|
|
+ ("xlm", "application/vnd.ms-excel"),
|
|
+ ("xls", "application/vnd.ms-excel"),
|
|
+ ("xlt", "application/vnd.ms-excel"),
|
|
+ ("xlw", "application/vnd.ms-excel"),
|
|
+ ("xof", "x-world/x-vrml"),
|
|
+ ("xpm", "image/x-xpixmap"),
|
|
+ ("xwd", "image/x-xwindowdump"),
|
|
+ ("z", "application/x-compress")
|
|
+
|
|
+ ]
|
|
+
|