{-
Copyright (C) 2008 Joachim Breitner
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2, or (at your option)
any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; see the file COPYING. If not, write to
the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
Boston, MA 02110-1301, USA.
-}
module HTML
( BundleInfo(..)
, mainPage
, userPage
, repoPage
, cgiMessagePage
, userFile
, repoFile
, patchDiffFile
, normalizeAuthor
, bundleInfoFilter
, maxState
, bundleURL
, stripIgnorethis
) where
import Text.XHtml hiding ((!))
import qualified Text.XHtml ((!))
import Text.Printf
import qualified Data.Map as M
import qualified Data.Set as S
import Data.Map ((!))
import qualified MultiMap as MM
import MultiMap ((!!!!))
import Data.List
import Data.Ord
import Data.Char
import Data.Maybe
import Data.Time
import System.Time
import System.Locale
import qualified Data.ByteString.Char8 as B
import Data.ByteString.Char8 (ByteString)
import Darcs
import Darcs.Watch.Data
-- not in ghc6.6
infixl 0 `on`
on :: (b -> b -> c) -> (a -> b) -> a -> a -> c
(*) `on` f = \x y -> f x * f y
data BundleInfo = BundleInfo
{ biBundleHash :: BundleHash
, biBundle :: PatchBundle
, biFileName :: FilePath
, biHistory :: [BundleHistory]
}
instance Eq BundleInfo where
(==) = (==) `on` biBundleHash
instance Ord BundleInfo where
compare = compare `on` (map (piDate.fst) . fst . biBundle)
mainPage date
nameMapping
patchCount
bundleCount
repoData
authorData
= showHtml $
header << (
thetitle << "DarcsWatch overview" +++
myHeader
) +++
body << (
h1 << "DarcsWatch overview" +++
p << ( "Welcome to DarcsWatch. If you want to know more, please read the " +++
hotlink "http://darcs.nomeata.de/darcswatch/documentation.html" << "Documentation" +++
"."
) +++
h2 << "Statistics" +++
p << stringToHtml (
printf "Tracking %d repositories and %d patches in %d bundles submitted by %d users."
(length repoData) (patchCount :: Int) (bundleCount :: Int) (length authorData)
) +++
h2 << "Listing by user" +++
unordList (map userLine authorData) +++
h2 << "Listing by repository" +++
unordList (map repoLine repoData) +++
footer (Just date)
)
where userLine (u, tracked, unapplied, obsolete, rejected) =
hotlink (userFile u) << M.findWithDefault u u nameMapping +++
" " ++
show tracked +++
" tracked patches, "+++
count Applicable "unapplied" unapplied +++
count Obsoleted "obsolete" obsolete +++
count Rejected "rejected" rejected
repoLine (r, inventory, tracked, applicable, obsolete, rejected) =
hotlink (repoFile r) << r +++
" " ++
show tracked +++
" patches in inventory, "+++
count Applied "tracked" tracked +++
count Applicable "applicable" applicable +++
count Obsoleted "obsolete" obsolete +++
count Rejected "rejected" rejected +++ " " +++
hotlink ("http://handra.rampa.sk/dawb/changes?repoPage=0&repoURL=" ++ r ++ "&repoSummary=on&repoLimit=50&repoElem=&repoRegex=") << "view repo"
count s t 0 = noHtml
count s t n = inColor (stateColor s) << (show n +++ " " +++ t) +++ " "
footer mDate =
p !!! [thestyle "font-size:80%"] << (
"darcswatch © Joachim Breitner <" +++
hotlink "mailto:mail@joachim-breitner.de" << "mail@joachim-breitner.de" +++
">. Source code at " +++
hotlink "http://darcs.nomeata.de/darcswatch/" << "http://darcs.nomeata.de/darcswatch/"+++
"." +++ maybe noHtml (\d -> " Last update " +++ calendarTimeToString d +++ ".") mDate
)
myHeader = script !!! [thetype "text/javascript", src "/javascript/jquery/jquery.js"] << noHtml
+++
script !!! [thetype "text/javascript"] << "\
\$(document).ready(function () { \
\ $('.diffshower').click(function () { \
\ var diffid = this.id.replace(/^diffshower_/,''); \
\ $('#diff_' + diffid).toggle(); \
\ $('#diff_' + diffid + ' pre:visible').each(function () {\
\ diffelem = this;\
\ if (!($(diffelem).text())) {\
\ $(diffelem).text('Diff is being loaded...');\
\ jQuery.get('diff_' + diffid + '.txt',\
\ callback=function(text) {\
\ $(diffelem).text(text);\
\ }\
\ ,type='text');\
\ }});\
\ })})\
\"
+++
style !!! [thetype "text/css" ] << "\
\span[title] { border-bottom: dotted 1px black; } \
\"
userPage date nameMapping u bundleInfos = showHtml $
header << (
thetitle << ("DarcsWatch overview for " +++ u) +++
myHeader
) +++
body << form !!! [ method "GET", action "cgi"] << (
h1 << ("DarcsWatch overview for " +++ M.findWithDefault u u nameMapping) +++
p << hotlink "." << "Return to main page" +++
bundleList nameMapping "Unapplied patch bundles" (bundleInfoFilter Applicable bundleInfos) +++
bundleList nameMapping "Unmatched patch bundles" (bundleInfoFilter New bundleInfos) +++
bundleList nameMapping "Applied patch bundles" (bundleInfoFilter Applied bundleInfos) +++
bundleList nameMapping "Obsoleted patch bundles" (bundleInfoFilter Obsoleted bundleInfos) +++
bundleList nameMapping "Rejected patch bundles" (bundleInfoFilter Rejected bundleInfos) +++
footer (Just date)
)
repoPage date nameMapping r repoInfo bundleInfos = showHtml $
header << (
thetitle << ("DarcsWatch overview for " +++ r) +++
myHeader
) +++
body << form !!! [ method "GET", action "cgi"] << (
h1 << ("DarcsWatch overview for " +++ r) +++
p << (hotlink "." << "Return to main page" +++ " " +++
hotlink ("http://handra.rampa.sk/dawb/changes?repoPage=0&repoURL=" ++ r ++ "&repoSummary=on&repoLimit=50&repoElem=&repoRegex=") << "View repository") +++
bundleList nameMapping "Unapplied patch bundles" (bundleInfoFilter Applicable bundleInfos) +++
bundleList nameMapping "Applied patch bundles" (bundleInfoFilter Applied bundleInfos) +++
bundleList nameMapping "Obsoleted patch bundles" (bundleInfoFilter Obsoleted bundleInfos) +++
bundleList nameMapping "Rejected patch bundles" (bundleInfoFilter Rejected bundleInfos) +++
thediv << (
"Last change in repository at " +++
maybe (toHtml "No idea when") toHtml (lastUpdate repoInfo) +++
", last check of repository at " +++
maybe (toHtml "No idea when") toHtml (lastCheck repoInfo) +++ "."
) +++
footer (Just date)
)
bundleList nameMapping title [] = noHtml
bundleList nameMapping title list =
h2 << title +++
unordList (map (bundleView nameMapping) list)
bundleInfoFilter state = sort . filter (\bi -> state == maxState (biHistory bi))
{-
unmatchedPage d = showHtml $
header << (
thetitle << ("DarcsWatch overview, unmatched patches") +++
myHeader
) +++
body << form !!! [ method "GET", action "cgi"] << (
h1 << ("DarcsWatch overview, unmatched Patches") +++
p << hotlink "." << "Return to main page" +++
patchList d (S.toList (unmatched d)) "Unmatched patches" False +++
footer (Just (date d))
)
-}
cgiMessagePage isError msg = showHtml $
header << (
thetitle << "DarcsWatch" +++
myHeader
) +++
body << (
h1 << "DarcsWatch" +++
p << (
(if isError then inColor "red" << "Error: " else noHtml)
+++
msg
) +++
footer Nothing
)
patchHistoryView history = unordList $
map (\(date,source,state) ->
date +++ ": " +++ showState state +++ " " +++ showSource source)
(reverse history)
showSource (ViaRepository repo) = " in repo " +++ hotlink (repoFile repo) << repo
showSource (ViaBugtracker url) = " in bug tracker ticket " +++ hotlink url << url
showSource (ViaWeb openid) = " via web interface by user " +++ hotlink openid << openid
showSource (ViaEMail from to subject mmid) = " via " +++
thespan !!! [ title $
"From: " ++ from ++ "\n" ++
"Subject: " ++ subject
] << "e-mail" +++ " to " +++
hotlink ("mailto:" ++ to) << to +++
maybe noHtml (\mid ->
" " +++ hotlink ("http://mid.gmane.org/" ++ mid)
(image !!!
[ src "http://gmane.org/favicon.ico"
, title "Search this mail on gmane.org"
, border 0
])
) mmid
showSource ManualImport = toHtml "via a manual import"
bundleView nameMapping (BundleInfo bundleHash (ps,ctx) bundleFileName history) =
p << (anchor !!! [name ("bundle-" ++ bundleHash)] << noHtml +++
strong << "Contents:" +++
" (" +++
(if length ps == 1
then "1 patch"
else show (length ps) ++ " patches" ) +++
")"
) +++
unordList (map (patchView nameMapping) (map fst ps)) +++
p << (strong << "History:") +++
patchHistoryView history +++
actions
where
actions = paragraph << (
strong << "Actions: " +++
hotlink bundleFileName << "Download .dpatch" +++
" "+++
if maxState history <= Applicable
then " "+++
select !!! [ name ("state-" ++ bundleHash), size "1"] << (
option !!! [ value "UNCHANGED" ] << "Mark patch as..." +++
option !!! [ value "OBSOLETE" ] << "Obsolete" +++
option !!! [ value "REJECTED" ] << "Rejected"
) +++
submit "submit" "Submit"
else noHtml
)
patchView nameMapping p =
piDate p +++ ": " +++ strong << (
(if piInverted p then stringToHtml "UNDO: " else noHtml) +++
piName p
) +++
" " +++
small << (
" by " +++
let u = B.unpack (piAuthor p) in
hotlink (userFile u) <<
M.findWithDefault u u nameMapping +++
" " +++
diffShower
) +++
(if null log then noHtml else pre << B.unlines log) +++
thediv !!! [identifier diffId, thestyle "display:none"] << pre noHtml
where pid = patchBasename p
diffShowerId = "diffshower_"++pid
diffId = "diff_"++pid
diffShower = anchor
!!! [identifier diffShowerId, theclass "diffshower", href "javascript:"]
<< "Show/Hide diff"
log = stripIgnorethis (piLog p)
maxState history = maximum $ New : map (\(_,_,s) -> s) history
{-
state d p r | p `S.member` ps = Applied
| ip `S.member` ps = maxState history
| amend_obsoleted = Obsoleted
| ip `S.member` subs && not (piInverted p) = Obsoleted
| otherwise = maxState history
where context = peContext (p2pe d ! p)
history = peStateHistory (p2pe d ! p)
ps = r2p d ! r
ip = inversePatch p
subs = M.keysSet (p2pe d)
amend_obsoleted = any (`laterThan` p) $ S.toList (u2p d ! (normalizeAuthor (piAuthor p)))
p1 `laterThan` p2 = piAuthor p1 == piAuthor p2
&& piName p1 == piName p2
&& piLog p1 == piLog p2
&& piInverted p1 == piInverted p2
&& piDate p1 > piDate p2
-}
showState New = "Seen bundle"
showState Applicable = "Not yet fully applied bundle"
showState Rejected = "Bundle marked rejected"
showState Obsoleted = "Bundle marked obsolete"
showState Applied = "Bundle fully applied"
stateColor New = "black"
stateColor Applicable = "red"
stateColor Obsoleted = "gray"
stateColor Rejected = "brown"
stateColor Applied = "green"
inColor c = thespan !!! [thestyle ("color:"++c)]
(!!!) :: ADDATTRS a => a -> [HtmlAttr] -> a
(!!!) = (Text.XHtml.!)
{-
userStats u d = " " +++
show (length ps) +++
" tracked patches, "+++
count Applicable "unapplied" +++
count Obsoleted "obsolete" +++
count Rejected "rejected"
where (ps, sps) = userData u d
count s t = case sps !!!! s of
[] -> noHtml
_ -> inColor (stateColor s) << (show (length ps) +++ " " +++ t) +++ " "
where ps = (sps !!!! s)
repoStats r d = " " +++
show (S.size (r2p d !!!! r)) +++
" patches in inventory "+++
count Applied "tracked" +++
count Applicable "applicable" +++
count Obsoleted "obsolete" +++
count Rejected "rejected"
where (ps, sps) = repoData r d
count s t = case sps !!!! s of
[] -> noHtml
_ -> inColor (stateColor s) << (show (length ps) +++ " " +++ t) +++ " "
where ps = (sps !!!! s)
repoData r d = (ps, sorted)
where ps = s2List $ r2mp d !!!! r
sorted = MM.fromList $ map (\p -> (state d p r, p)) ps
userData u d = (ps, sorted)
where ps = patchSort $ S.toList $ u2p d !!!! u
sorted = MM.fromList $ map (\p -> (state' p, p)) ps
state' p | S.null repos = New
| otherwise = S.findMax (S.map (state d p) repos)
where repos = p2pr d !!!! p
-}
userFile u = "user_" ++ normalizeAuthor u ++ ".html"
repoFile r = "repo_" ++ safeName r ++ ".html"
patchDiffFile p = "diff_" ++ patchBasename p ++ ".txt"
normalizeAuthor name | not (null r') && valid = email
| otherwise = safeName name
where r' = dropWhile (/='<') name
(email,r'') = span (/='>') (tail r')
valid = not (null email) && not (null r'') && all (isSafeFileChar) email
safeName n = map s n
where s c = if isSafeFileChar c then c else '_'
isSafeFileChar c = isAlpha c || isDigit c || c `elem` "-_.@:"
s2List = patchSort . S.toList
patchSort = sortBy (flip (compare `on` piDate))
bundleURL :: DarcsWatchConfig -> RepositoryURL -> BundleHash -> String
bundleURL config repo hash = cDarcsWatchURL config ++ repoFile repo ++ "#bundle-" ++ hash
stripIgnorethis [] = []
stripIgnorethis (x:xs) | B.pack "Ignore-this:" `B.isPrefixOf` x = stripIgnorethis xs
| B.null x = stripIgnorethis xs
| otherwise = x:xs
instance HTML ByteString where
toHtml = toHtml . B.unpack
instance HTML UTCTime where
toHtml = toHtml . formatTime defaultTimeLocale "%c"
instance HTML ZonedTime where
toHtml = toHtml . formatTime defaultTimeLocale "%c"