packages feed

happs-tutorial-0.4: src/AppStateSetBased.hs

{-# LANGUAGE TemplateHaskell, FlexibleInstances, FlexibleContexts, 
             MultiParamTypeClasses, DeriveDataTypeable, TypeFamilies,
             TypeSynonymInstances, PatternSignatures #-}

module AppStateSetBased where  

import qualified Data.Map as M
import qualified Data.Set as S
import Data.List

import Control.Monad.Reader
import Control.Monad.State (modify,put,get,gets)
import Data.Generics
import HAppS.State
import SerializeableSessions
import SerializeableUsers
import Misc
--import Data.Graph.Inductive

-- Think of appdatastore as the database in a traditional web app.
-- Data there gets stored permanently
-- Data in appsessions is stored permanently too, but we don't care as much about its persistence,
-- it's just to keep track of who is logged in at a point in time.
-- appsessions field could be less complicated, just have M.Map Int SessionData
-- don't really see the advantage of declaring a wrapper over map.
data AppState = AppState {
  appsessions :: Sessions SessionData,  
  appdatastore :: S.Set User
} deriving (Show,Read,Typeable,Data)                                                        

instance Version AppState

$(deriveSerialize ''AppState) 

instance Component AppState where 
  type Dependencies AppState = End 
  initialValue = AppState { appsessions = (Sessions M.empty),
                         appdatastore = S.empty }


askDatastore :: Query AppState (S.Set User)                                    
askDatastore = do
  (s :: AppState ) <- ask
  return . appdatastore $ s


askSessions :: Query AppState (Sessions SessionData)
askSessions = return . appsessions =<< ask



modUsers :: ( S.Set User -> S.Set User ) -> Update AppState ()
modUsers f = modify (\s -> (AppState (appsessions s) (f $ appdatastore s)))                              

modSessions :: (Sessions SessionData -> Sessions SessionData) -> Update AppState ()
modSessions f = modify (\s -> (AppState (f $ appsessions s) (appdatastore s)))                           

isUser :: String -> Query AppState Bool
isUser name = do
  return . (S.member name) . (setmap username) =<< askDatastore

-- I tried to declare a functor instance for Set a but got blocked.
setmap f = S.fromList . map f . S.toList

updateUser olduser newuser = modUsers $ updateUserPure olduser newuser
updateUserPure o n users | username o == username n = S.insert n . S.delete o $ users
                         | otherwise = error "updateUser, username is not allowed to change"
{-
changeConsultantProfile olduser newcp = modUsers $ changeConsultantProfilePure olduser newcp
changeConsultantProfilePure olduser@(User u p mbCP mbJobs ) newcp users =
  S.insert (User u p newcp mbJobs ) . S.delete olduser $ users
-}

addUser :: String -> String -> Update AppState ()
addUser name pass = modUsers $ S.insert (User name (scramblepass pass) cp []) 
  where cp = ConsultantProfile "" "" False
changePassword :: User -> String -> String -> Update AppState ()
changePassword u oldpass newpass = modUsers $ changepasswordPure u oldpass newpass



-- modify is simply delete plus insert 
changepasswordPure :: User -> String -> String -> S.Set User -> S.Set User
changepasswordPure olduser@(User u realoldpass mbCP mbJobs ) inputtedOldpass inputtedNewpass users = 
   let hashedoldpass = scramblepass inputtedOldpass
       hashednewpass = scramblepass inputtedNewpass
       
   in if (realoldpass /= hashedoldpass)
     then users
     else do S.insert (User u hashednewpass mbCP mbJobs ) . S.delete olduser $ users

getUser :: String -> Query AppState (Maybe User)
getUser u = return . lookupUserByName u =<< askDatastore

lookupUserByName :: String -> S.Set User -> Maybe User
lookupUserByName u users = lookupUser ((==u) . username) users

-- list the job along with the username who posted the job
listAllJobs :: Query AppState [(Job,String)]
listAllJobs = liftM alljobs askDatastore
  where alljobs users = concatMap userJobs . S.toList $ users
        userJobs (User n _ _ js) = map (\j -> (j,n)) js 
  

lookupUser f users = find f . S.toList $ users
listUsers :: Query AppState [String]
listUsers = liftM (map username . S.toList) askDatastore

--listUsersWantingDevelopers = liftM (map username . filter wantingDeveloper . S.toList) askDatastore
--  where wantingDeveloper u = not . null . jobs $ u

newSession :: SessionData -> Update AppState SessionKey
newSession u = do
  key <- getRandom
  modSessions $ Sessions . (M.insert key u) . unsession
  return key

delSession :: SessionKey -> Update AppState ()
delSession sk = modSessions $ Sessions . (M.delete sk) . unsession


getSession::SessionKey -> Query AppState (Maybe SessionData)
getSession key = liftM (M.lookup key . unsession) askSessions

numSessions :: Query AppState Int
numSessions  =  liftM (M.size . unsession) askSessions


                    


--------------dummy data

datastoreDummyData :: S.Set User
datastoreDummyData = S.fromList [
  User "tphyahoo" (scramblepass "password") tphyahooProfile tphyahooJobs
  , User "zzz" (scramblepass "password") (ConsultantProfile "" "" False) serpinskiJobs
  ] 

tphyahooProfile = ConsultantProfile {
  --billing_rate = "it depends on the project"
  contact = "thomashartman1 at gmail, +48 51 365 3957"
  -- tell something about yourself. Edited via a text area. should replace newlines with <br> when displayed.
  , blurb = "I'm currently living in poland, doing a software sabbatical where I'm \
             \learning new things and writing and releasing open source software, including this tutorial."
  , consultant = True 
}

tphyahooJobs = map (\(j,b)-> Job { jobname = j, jobbudget = b, jobblurb ="make " ++ j ++ " using HAppS "} ) $
             [ ("darcshub", "$5000")
              , ("community wizard", "$500,000")
              , ("hpaste in happs", "karma points?")
              , ("facebook clone", "$10,000")
              , ("rentacoder clone", "12,000 Eu")
              , ("ebay clone", "")
              , ("reddit clone", "")
              , ("ripplepay clone", "best offer")
              , ("oscommerce clone", "$1500")
              , ("phpbb clone", "")
              , ("sql-ledger clone", "")]
serpinskiJobs =  map (\num -> ( Job ("job" ++ (show num)) "$0" "") ) [10..203]

-- create dummy data

initializeDummyData = modUsers g
  where g users = if ( S.null users)
                    then datastoreDummyData
                    else error failmsg
        failmsg = "initializeDummyData, for safety, only works if there is currently no data in app\
                   \Maybe you shouldd first do mv _local _local.bak to get any existing data out of the way."


-- define types which are upper case of methods below, eg AddUser, AuthUser...
-- these types work with HApppS query/update machinery
-- in ghci, try :i AddUser
$(mkMethods ''AppState
    ['askDatastore
     , 'getUser        
     , 'addUser
     , 'changePassword     
     , 'updateUser
     , 'isUser
     , 'listUsers     
     , 'listAllJobs
     , 'getSession
     , 'newSession
     , 'delSession
     , 'numSessions
     , 'initializeDummyData]
 )