/
Polink.hs
2996 lines (2669 loc) · 109 KB
/
Polink.hs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
-- Copyright 2013 Metamocracy LLC
-- Written by Jim Snow (jsnow@metamocracy.com)
--
-- This file includes all the Yesod handlers and html generation.
-- needed for Yesod
{-# LANGUAGE TypeFamilies, QuasiQuotes, MultiParamTypeClasses,
TemplateHaskell, OverloadedStrings, TypeSynonymInstances,
FlexibleInstances, BangPatterns #-}
-- makes life easier for local functions
{-# LANGUAGE ScopedTypeVariables #-}
module Main where
import qualified Data.Text as T
import qualified Data.Text.Lazy as LT
import Yesod hiding (update, Entity)
import Yesod.Form.Jquery
import Yesod.Auth
import Yesod.Auth.BrowserId
import Web.ClientSession(getDefaultKey)
import Network.HTTP.Conduit (Manager, newManager, def)
import Control.Applicative
import Control.Monad (liftM)
import Data.Acid
import Data.Acid.Local
import Data.Acid.Advanced (query', update')
import Control.Exception (bracket)
import Control.Lens -- hiding (left, right)
import qualified Data.Map as M
import qualified Data.Set as S
import qualified Data.List as L
import qualified Data.Sequence as SEQ
import Data.List (sortBy)
import Data.Function (on)
import Text.Blaze
import Web.Cookie
import qualified Data.ByteString as B
import qualified Data.ByteString.Char8 as B8
import qualified Data.ByteString.Lazy.Char8 as LB8
import Text.Julius
import Text.Markdown (markdown, def)
import Data.Time.Calendar (Day, toGregorian, fromGregorian, addDays, Day(..))
import qualified Data.Char as C (toLower)
import System.Random (getStdRandom, randomR)
import System.Process (runCommand, system)
import Data.Time.Clock (getCurrentTime, utctDay, secondsToDiffTime)
import Text.Blaze.Internal (text)
import Data.Foldable
import Control.Concurrent
import PolinkState
import PolinkAPI
-- YESOD-RELATED TYPES AND INSTANCES
type Txt = T.Text
data InfluenceGraph = InfluenceGraph {httpMgr :: Manager, acid :: AcidState GraphState, fslock :: MVar ()}
--type GW = GWidget InfluenceGraph InfluenceGraph ()
type GW = WidgetT InfluenceGraph IO ()
-- Things that a given handler can be expected to have access to. See getConext.
data Ctx = Ctx {
cgs :: GraphState, -- application state
cmuser :: Maybe User, -- current logged in user
cmcb :: Maybe Entity, -- clipboard
cgw :: GW, -- widget that displays the above
cfslock :: MVar () -- filesystem mutex for svg-rendering path
}
-- Necessary instances if we want to use these types in URLs and forms.
instance PathPiece Lid where
fromPathPiece pp = fmap Lid (fromPathPiece pp)
toPathPiece (Lid id) = toPathPiece id
instance PathPiece Cid where
fromPathPiece pp = fmap Cid (fromPathPiece pp)
toPathPiece (Cid id) = toPathPiece id
instance PathPiece Eid where
fromPathPiece pp = fmap Eid (fromPathPiece pp)
toPathPiece (Eid id) = toPathPiece id
instance PathPiece Uid where
fromPathPiece pp = fmap Uid (fromPathPiece pp)
toPathPiece (Uid id) = toPathPiece id
instance PathPiece PTid where
fromPathPiece pp = fmap PTid (fromPathPiece pp)
toPathPiece (PTid id) = toPathPiece id
instance PathPiece OTid where
fromPathPiece pp = fmap OTid (fromPathPiece pp)
toPathPiece (OTid id) = toPathPiece id
instance PathPiece UTid where
fromPathPiece pp = fmap UTid (fromPathPiece pp)
toPathPiece (UTid id) = toPathPiece id
instance PathPiece Iid where
fromPathPiece pp = fmap Iid (fromPathPiece pp)
toPathPiece (Iid id) = toPathPiece id
instance PathPiece Id where
fromPathPiece pp =
case reads (T.unpack pp) of
(id, ""):_ -> Just id
[] -> Nothing
_ -> error "could not parse Id"
toPathPiece id = T.pack $ show id
instance PathPiece (Maybe Int) where
fromPathPiece "nothing" = Nothing
fromPathPiece pp = Just $ fromPathPiece pp
toPathPiece Nothing = "nothing"
toPathPiece (Just i) = T.pack $ show i
-- We want our own type here, because the default Day instance
-- isn't user friendly and has a limited range.
newtype PathDay = PathDay {unPathDay :: Day}
deriving (Eq, Ord, Show, Read)
mread s =
case reads s of
[(a, "")] -> Just a
_ -> Nothing
split :: Eq a => a -> [a] -> [[a]]
split sep xs =
go xs [] []
where
--go :: [a] -> [a] -> [[a]] -> [[a]]
go [] cur prev = reverse ((reverse cur):prev)
go (x:xs) cur prev
| x == sep = go xs [] ((reverse cur):prev)
| otherwise = go xs (x:cur) prev
instance PathPiece PathDay where
fromPathPiece daystring =
case split '-' (T.unpack daystring) of
(m:d:y:[]) -> do month <- mread m
day <- mread d
year <- mread y
return $ PathDay $ fromGregorian year month day
_ -> Nothing
toPathPiece day =
let (y,m,d) = toGregorian (unPathDay day)
in T.pack $ (show d) ++ "-" ++ (show m) ++ "-" ++ (show y)
instance ToMarkup Url where
toMarkup (Url t) = toMarkup t
-- We support "DELETE" even though there isn't an easy way to invoke that
-- method from a plain html form. (Browsers only seem to allow GET and POST.)
-- As a workaround, we have an alternate "POST" that just runs the delete handler.
mkYesod "InfluenceGraph" [parseRoutes|
/ HomeR GET
/explore ExploreR GET
/auth AuthR Auth getAuth
/newu NewUserR GET POST
/about AboutR GET
/rshelp RSHelpR GET
/recent RecentR GET
/u UsersR GET
/u/#Txt UserR GET
/uid/#Uid UserIDR GET POST
/e EntitiesR GET POST
/newperson NewPersonR GET POST
/neworg NewOrgR GET POST
/editperson/#Eid EditPersonR GET POST
/editorg/#Eid EditOrgR GET POST
/newlink/#Eid/#Eid NewLinkR GET POST
/newlink2/#Eid/#Eid NewLink2R POST
/editlink/#Lid EditLinkR GET POST
/newtag/#Eid NewTagR POST
/newcomment NewCommentR POST
/ename/#Txt EntityByNameR GET
/e/#Eid EntityR GET DELETE
/e/#Eid/n/#Txt EntityCanonR GET DELETE
/e/#Eid/orgchart/dot OrgChartDotR GET
/e/#Eid/orgchart/svg OrgChartSvgR GET
/e/#Eid/orgchart OrgChartR GET
-- /e/#Eid/orgchart/dot/#PathDay OrgChartDotDateR GET
-- /e/#Eid/orgchart/svg/#PathDay OrgChartSvgDateR GET
-- /e/#Eid/orgchart/date/#PathDay OrgChartDateR GET
/random RandomR GET
/dele/#Eid DelEntityR POST
/l/#Lid LinkR GET DELETE
/dell/#Lid DelLinkR POST
/c/#Cid CommentR GET DELETE
/delc/#Cid DelCommentR POST
/remc/#Cid RemCommentR POST
/c-context/#Cid CommentCtxR GET
/pt/#PTid PTagR GET DELETE
/delpt/#PTid DelPTagR POST
/ot/#OTid OTagR GET DELETE
/delot/#OTid DelOTagR POST
/ut/#UTid UTagR GET
/el/#Eid/#Eid LinksBetweenR GET
/i/#Iid IssueR GET
-- /i/#Iid/date/#PathDay IssueDateR GET
/i/#Iid/n/#Txt IssueCanonR GET
/i/#Iid/dot IssueDotR GET
/i/#Iid/svg IssueSvgR GET
-- /i/#Iid/dot/#PathDay IssueDotDateR GET
-- /i/#Iid/svg/#PathDay IssueSvgDateR GET
/i IssuesR GET
/newissue NewIssueR GET POST
/editissue/#Iid EditIssueR GET POST
/l/#Lid/newissuetag NewIssueTagR POST
/deli/#Iid DelIssueR POST
/delit/#Lid/#Iid DelIssueTagR POST
/wiki/#Txt WikiR GET
/rsstate RSStateR GET
|]
instance Yesod InfluenceGraph
where
maximumContentLength _ _ = Just (128 * 1024) -- No need for lengthy request bodies.
approot = if production
then ApprootStatic "http://polink.org"
else ApprootStatic "http://localhost:3001"
defaultLayout = layout
-- makeSessionBackend _ =
-- do key <- getDefaultKey
-- return $ Just $ clientSessionBackend key (60 * 24 * 14) -- 2 week session timeout.
-- More recent versions of Yesod do this differently...
makeSessionBackend _ =
do backend <- defaultClientSessionBackend (24*60*7) "mykey.aes"
return $ Just backend
-- let setTimeout cookie = cookie { setCookieMaxAge = Just week }
-- in fmap (customizeSessionCookies setTimeout)
instance YesodAuth InfluenceGraph where
type AuthId InfluenceGraph = T.Text
getAuthId = return . Just . credsIdent
loginDest _ = HomeR
logoutDest _ = HomeR
authPlugins _ = [authBrowserId def] -- Persona/browserid support.
authHttpManager = httpMgr
maybeAuthId = do
ms <- lookupSession "_ID"
case ms of
Nothing -> return Nothing
Just s -> return $ fromPathPiece s
instance RenderMessage InfluenceGraph FormMessage where
renderMessage _ _ = defaultFormMessage
instance YesodJquery InfluenceGraph
-- MISC UTILITY FUNCTIONS
fromEither (Left e) = error e
fromEither (Right a) = a
mTtoMInt :: Maybe T.Text -> Maybe Int
mTtoMInt Nothing = Nothing
mTtoMInt (Just s) =
case reads (T.unpack s) of
[] -> Nothing
((i,[]):[]) -> Just i
_ -> Nothing
tToInt s =
case mTtoMInt s of
Nothing -> 1
Just a -> if a > 1 then a else 1
caseJust :: Maybe a -> b -> (a -> b) -> b
caseJust m n j =
case m of
Nothing -> n
Just a -> j a
showDate :: Day -> String
showDate day = (showMonth m) ++ " " ++ (show d) ++ " " ++ (show y)
where
(y,m,d) = toGregorian day
showMonth month =
case month of
1 -> "January"
2 -> "February"
3 -> "March"
4 -> "April"
5 -> "May"
6 -> "June"
7 -> "July"
8 -> "August"
9 -> "September"
10 -> "October"
11 -> "November"
12 -> "December"
_ -> error "invalid month"
showDateRange' :: Bool -> Bool -> Maybe (Day, Bool) -> Maybe (Day, Bool) -> String
showDateRange' _ _ Nothing Nothing = ""
showDateRange' brief forceRange start end =
let mShowDate Nothing = ""
mShowDate (Just (d,approx)) =
if brief || approx
then let (y,_,_) = toGregorian d in show y
else showDate d
in
case end of
Nothing -> "(" ++ (mShowDate start) ++ (if forceRange then "-" else "") ++ ")"
Just _ -> "(" ++ (mShowDate start) ++ "-" ++ (mShowDate end) ++ ")"
showDateRange brief = showDateRange' brief False
showDateForceRange brief = showDateRange' brief True
reload =
do cr <- curRoute
redirect cr
redirectId id =
case id of
E eid -> redirect $ EntityR eid
L lid -> redirect $ LinkR lid
C cid -> redirect $ CommentR cid
U uid -> redirect $ UserIDR uid
PT ptid -> redirect $ PTagR ptid
OT otid -> redirect $ OTagR otid
UT utid -> redirect $ UTagR utid
clearAndReload cookie =
do deleteCookie cookie "/"
reload
setAndReload cookie val =
do setCookie (def{setCookieName = cookie,
setCookiePath = Just "/",
setCookieValue = (B8.pack val)})
reload
curRoute =
do mr <- getCurrentRoute
case mr of
Nothing -> return HomeR
Just r -> return r
ifSet param action1 action2 =
do marg <- lookupGetParam param
case marg of
Just arg -> action1 arg
_ -> action2
-- Check args to see if anything's set that we should act on.
-- This probably isn't the best way to register likes/agrees/etc...
handleArgs ig uid mid action =
do case mid of
Just id ->
ifSet
"like"
(\_ -> liftIO (update (acid ig) (AddLikeU uid id)) >> reload)
(ifSet
"dislike"
(\_ -> liftIO (update (acid ig) (AddDislikeU uid id)) >> reload)
(ifSet
"agree"
(\_ -> liftIO (update (acid ig) (AddAgreeU uid id)) >> reload)
(ifSet
"disagree"
(\_ -> liftIO (update (acid ig) (AddDisagreeU uid id)) >> reload)
action')))
Nothing -> action'
where
action' =
ifSet
"setcb"
(\setcb -> setAndReload "clipboard" (T.unpack setcb))
(ifSet
"clearcb"
(\_ -> clearAndReload "clipboard")
action)
-- Clipboard management.
-- (The clipboard allows us to create links.)
-- This could probably be handled entirely in javascript.
getSetEClipboardR :: Eid -> Handler TypedContent
getSetEClipboardR eid =
do setCookie (def{setCookieName = "clipboard",
setCookiePath = Just "/",
setCookieValue = (B8.pack $ show eid)})
getEntityR eid
getClearEClipboardR :: Handler Html
getClearEClipboardR =
do deleteCookie "clipboard" "/"
layout [whamlet|<p>clipboard cleared|]
-- If the user is authenticated via persona but hasn't created an account yet,
-- we redirect them to the account creation page.
-- newAccountRedirect :: Handler ()
newAccountRedirect =
do cr <- getCurrentRoute
--rtom <- getRouteToMaster
--if ((fmap rtom cr) == (Just NewUserR)) -- break inevitable redirect loop
if cr == Just (NewUserR)
then return ()
else
do maid <- maybeAuthId
case maid of
Nothing -> return ()
Just email ->
do ig <- getYesod
gs' <- liftIO $ query (acid ig) GetStateQ
let gs = fromEither gs'
case gs ^. usersByEmail . at email of
Nothing -> redirect NewUserR
Just _ -> return ()
-- Figure out who we're logged in as and what the current graph state is,
-- return the graphstate, user, a widget with the appropriate interface
-- to display who the user is, and lock (used for filesystem access).
getContext :: Maybe Id -> Handler Ctx
getContext mid =
do ig <- getYesod
let lock = fslock ig
gs' <- liftIO $ query (acid ig) GetStateQ
let gs = fromEither gs'
maid <- maybeAuthId
let ment = case mid of
(Just (E eid)) -> gs ^. entities ^. at eid
_ -> Nothing
case maid of
Nothing -> return $ Ctx gs Nothing Nothing [whamlet|
<div class="header">
$maybe ent <- ment
<span id="thisentity" eid=#{show $ idToInt $ E $ _eid ent} ename=#{_ecname ent}>
<center>
<span class="home">
<a href="@{HomeR}">Polink</a></center>
<center><a href=@{AuthR LoginR}>login or register</a>
<hr>
|] lock
Just email ->
case gs ^. (usersByEmail . at email) of
Nothing -> return $ Ctx gs Nothing Nothing [whamlet|<center><a href=@{NewUserR}>create user account</a>|] lock
Just uid ->
case gs ^. (users . at uid) of
Nothing -> return $ Ctx gs Nothing Nothing [whamlet|<center>error retrieving user info|] lock
Just user ->
handleArgs
ig uid mid
(do meids <- lookupCookie "clipboard"
(cbw,meid) <-
case meids of
Nothing -> return ([whamlet|empty|], Nothing)
Just eids ->
case reads (T.unpack eids) of
[] ->
return ([whamlet|<p>clipboard failed to parse: #{eids}|], Nothing)
((eid,_):_) ->
do cr <- curRoute
let w = [whamlet|
^{renderId gs (E eid)}
\ <a href="@{cr}?clearcb=1">clear</a>
|]
return (w, gs ^. entities ^. at eid)
let (Uid ident) = _uid user
return (Ctx
gs
(Just user)
meid
[whamlet|
<div class="header">
$maybe ent <- ment
<span id="thisentity" eid=#{show $ idToInt $ E $ _eid ent} ename=#{_ecname ent}>
<center>
<span class="home">
<a href="@{HomeR}">Polink</a></center>
<center>
logged in as
\ <a href=@{UserR (_name user)}>
<span id="username" uid=#{show ident} uname=#{_name user}>
#{_name user}
\ <a href=@{AuthR LogoutR}>logout</a>
<center><a href=@{NewPersonR}>add person</a> <a href=@{NewOrgR}>add organization</a>
<center>clipboard: ^{cbw}
<hr>
|] lock ))
-- Reject access if the current user doesn't have the given permission.
-- Otherwise, perform the action and return a widget showing the action results.
-- (However, we pretty much always do a redirect.)
reqAuth :: T.Text -> Perm -> (Ctx -> User -> Handler GW) -> Handler GW
reqAuth s p action =
do ctx <- getContext Nothing
case cmuser ctx of
Nothing -> return $ errW $ T.append "you must be logged in in order to " s
Just u ->
do reqPerm u p
action ctx u
-- Check if a user's account has a particular permission, bail out if it doesn't.
reqPerm :: User -> Perm -> Handler ()
reqPerm u p =
if permTest (u ^. authority) p
then return ()
else permissionDenied "you don't have the proper permissions to do that"
-- Check permission, return bool.
hasPerm :: Ctx -> Perm -> Bool
hasPerm ctx p =
case cmuser ctx of
Nothing -> False
Just u ->
if permTest (u ^. authority) p
then True
else False
-- Layout instance we use for defaultLayout.
-- We call this directly, though, for the frivolous reason that it's less typing.
--layout :: GWidget s InfluenceGraph () -> GHandler s InfluenceGraph RepHtml
layout :: GW -> Handler Html
layout w =
do newAccountRedirect
content <- widgetToPageContent
(do w
toWidget [lucius| |]
toWidget [julius|
function showhideid(id1, id2) {
showid(id1);
hideid(id2);
}
function showid(id) {
document.getElementById(id).style.display = 'block';
}
function hideid(id) {
document.getElementById(id).style.display = 'none';
}
|])
giveUrlRenderer
[hamlet|
$doctype 5
<html>
<head>
<link rel="stylesheet" type="text/css" href="http://polink.org/static/polink.css"/>
<title>#{pageTitle content}
<meta charset=utf-8>
<meta name="google-site-verification" content="5zrJy6KmCEdVaA9ypBMN3GotYm1XBzvkDuLDbpo7S5Q" />
^{pageHead content}
<body>
^{pageBody content}
<hr>
<div class="footer">
<center>
<a href="@{HomeR}">home</a>
<a href="http://www.google.com/cse/publicurl?cx=005382893767711408570:jzzodnuihb4">search</a>
<script src="http://code.jquery.com/jquery-1.10.1.js"></script>
<script src="http://polink.org/static/jquery.cookie.js"></script>
<script src="http://polink.org/static/polink.js"></script>
|]
-- Wrapper function for json generation, so we set the header appropriately.
jsonify j =
do addHeader "Access-Control-Allow-Origin" "*"
return $ toJSON j
-- Bail out with a given text string.
err :: T.Text -> Handler Html
err s = layout [whamlet|<p>#{s}|]
-- Same, but as a widget.
errW :: T.Text -> GW
errW s = [whamlet|<p>#{s}|]
-- Pagination.
-- Max number of entities we show at once.
perpage = 100
-- Given a Sequence of things, look at the get parameters to see which ones
-- we need to actually show, return those along with a navigation widget.
paginate :: SEQ.Seq a -> Handler (GW, SEQ.Seq a)
paginate xs =
do pagenumt <- lookupGetParam "page"
mall <- lookupGetParam "all"
let all = case mall of
Just _ -> True
_ -> False
cr <- curRoute
let pagenum = tToInt pagenumt
let len = SEQ.length xs
let doPrev = pagenum > 1
let doNext = len > pagenum * perpage
let doAll = len > perpage
let prevpage = pagenum - 1
let nextpage = pagenum + 1
let first = if all then 1 else ((pagenum-1) * perpage) + 1
let last = if all then len else min (len) ((first + perpage)-1)
return
([whamlet|
<center>
<p>
showing #{first}-#{last} of #{len}
$if all
$else
$if doPrev
\ <a href="@{cr}?page=#{prevpage}">prev</a>
$if doNext
\ <a href="@{cr}?page=#{nextpage}">next</a>
$if doAll
\ <a href="@{cr}?all=1">all</a>
|], if all
then xs
else SEQ.take perpage $ SEQ.drop ((pagenum-1)*perpage) xs)
-- Relatively lazy way to check length. This avoids scanning the whole list,
-- which could be huge.
atLeastLength :: [a] -> Int -> Int
atLeastLength xs max = go xs max 0
where
go _ 0 acc = acc
go [] max acc = acc
go (x:xs) max acc = go xs (max-1) (acc+1)
-- Same as paginate, but for lists.
paginateL :: [a] -> Handler (GW, [a])
paginateL xs =
do pagenumt <- lookupGetParam "page"
mall <- lookupGetParam "all"
let all = case mall of
Just _ -> True
_ -> False
cr <- curRoute
let pagenum = tToInt pagenumt
let xs' = drop ((pagenum-1)*perpage) xs
let xs'' = take perpage xs'
let doPrev = pagenum > 1
let pseudolen = atLeastLength xs' (perpage+1)
let doNext = pseudolen > perpage
let doAll = doNext || doPrev
let prevpage = pagenum - 1
let nextpage = pagenum + 1
let first = if all then 1 else ((pagenum-1) * perpage) + 1
let last = if all then length xs else min ((first + pseudolen)-1) ((first + perpage)-1)
return
([whamlet|
<center>
<p>
$if pseudolen > 0
showing #{first}-#{last}
$if all
\ of #{last}
$else
$if doPrev
\ <a href="@{cr}?page=#{prevpage}">prev</a>
$if doNext
\ <a href="@{cr}?page=#{nextpage}">next</a>
$if doAll
\ <a href="@{cr}?all=1">all</a>
$else
\ nothing here
|], if all then xs else xs'')
-- Helper functions to render objects identified by various kinds of ID.
-- Return as a widget.
renderEid gs eid@(Eid id) =
case (gs ^. entities ^. at eid) of
Nothing -> [whamlet|deleted entity <a href="@{EntityR eid}">#{show eid}</a>|]
Just e -> let en = e ^. ecname
in [whamlet|
<span class="entity" id=#{show id}>
$case (_etype e)
$of Person p
<span class="person">
person
$of Organization o
<span class="org">
organization
\ <a href="@{EntityR eid}">#{en}</a>|]
renderLid gs lid =
let (lw,ml) = renderLink gs lid
in [whamlet|
<a href=@{LinkR lid}>link</a>
\ ^{lw}
|]
getParent gs cid =
case gs ^. comments . at cid of
Nothing -> Nothing
Just comment ->
case comment ^. parent of
C cid' -> getParent gs cid'
root -> Just root
renderCid gs cid@(Cid id) =
let mcomment = gs ^. comments . at cid
in
[whamlet|
<span class="comment" id=#{show id}>
<a href=@{CommentR cid}>comment</a>
\ by
$maybe c <- mcomment
\ ^{renderUid gs (_author c)}
$nothing
\ deleted user
\ on
$maybe p <- getParent gs cid
\ ^{renderId gs p}
$nothing
\ deleted context
|]
renderUid gs uid@(Uid id) =
let mu = gs ^. users ^. at uid
in
[whamlet|
$maybe u <- fmap _name mu
<span class="user" id=#{show u}>
<a href="@{UserR u}">#{u}</a>
$nothing
deleted user #{show uid}
|]
renderPTid :: GraphState -> PTid -> GW
renderPTid gs ptid =
let mtag = gs ^. ptags ^. at ptid
in
[whamlet|
$maybe (ptag, eid) <- mtag
\ ^{renderId gs (E eid)} tagged #{ptToText ptag}
$nothing
\ deleted person tag
|]
renderOTid :: GraphState -> OTid -> GW
renderOTid gs otid =
let mtag = gs ^. otags ^. at otid
in
[whamlet|
$maybe (otag, eid) <- mtag
\ ^{renderId gs (E eid)} tagged #{otToText otag}
$nothing
\ deleted organization tag
|]
renderIid' :: Bool -> GraphState -> Iid -> GW
renderIid' terse gs iid =
let missue = gs ^. issues ^. at iid
in
[whamlet|
$maybe issue <- missue
$if terse
$else
\ issue
\ <a href="@{IssueR iid}">#{_iname issue}</a>
$nothing
\ deleted issue
|]
renderIid = renderIid' False
renderIidTerse = renderIid' True
renderId :: GraphState -> Id -> GW
renderId gs id =
case id of
E eid -> renderEid gs eid
L lid -> renderLid gs lid
C cid -> renderCid gs cid
U uid -> renderUid gs uid
PT ptid -> renderPTid gs ptid
OT otid -> renderOTid gs otid
I iid -> renderIid gs iid
-- For a given Id, return the users that agree/disagree/like/dislike.
votesById :: GraphState -> Id -> Maybe (S.Set Uid, S.Set Uid, S.Set Uid, S.Set Uid)
votesById gs id =
do a <- gs ^. agree ^. at id
da <- gs ^. disagree ^. at id
l <- gs ^. like ^. at id
dl <- gs ^. dislike ^. at id
return (a, da, l, dl)
-- Show the agree/disagree/like/dislike votes, along with vote buttons, for a given Id.
renderVotes :: GraphState -> Maybe User -> Id -> Route InfluenceGraph -> GW
renderVotes gs muser id cr =
case votesById gs id of
Nothing -> errW $ "id lookup failed"
Just (agrees, disagrees, likes, dislikes) ->
let w (heading::T.Text) set (action::T.Text) (tag::T.Text) =
[whamlet|
<div class="votelists">
<b>
#{heading}:
$if S.null set
\ no one
$else
$if S.size set == 1
\ user
$else
\ users
$forall u <- set
\ ^{renderId gs (U u)}
$maybe u <- muser
$if S.member (_uid u) set
$else
<form method="get">
<input type="hidden" name=#{action} value=1 />
<input type="submit" value=#{tag}>
|]
in
[whamlet|
^{w "this page verified by" agrees "agree" "verify"}
^{w "this page disputed by" disagrees "disagree" "dispute"}
$maybe u <- muser
$if S.size agrees == 1
$if S.member (_uid u) agrees
(you are the only verifier; disputing will delete this object)
^{w "liked by" likes "like" "like"}
^{w "disliked by" dislikes "dislike" "dislike"}
|]
-- Render friend/foe lists for a given user.
renderVotesUser :: GraphState -> Maybe User -> Id -> Route InfluenceGraph -> GW
renderVotesUser gs muser id cr =
case votesById gs id of
Nothing -> errW $ "id lookup failed"
Just (_, _, likes, dislikes) ->
let w (heading::T.Text) set (action::T.Text) (tag::T.Text) =
[whamlet|
<p>
<b>#{heading}:</b>
$if S.null set
no one
$else
$forall u <- set
\ ^{renderId gs (U u)}
$maybe u <- muser
$if S.member (_uid u) set
$else
$if id == (U $ _uid u)
$else
\ <form method="get">
<input type="hidden" name=#{action} value=1 />
<input type="submit" value=#{tag} />
|]
in
[whamlet|
^{w "friends" likes "like" "add as friend"}
^{w "disliked by" dislikes "dislike" "add as foe"}
|]
-- FRONT / INFORMATIONAL HANDLERS
-- Recent changes was getting cluttered by issue tags, so we use this to group
-- clusters of changes that are all about one thing.
mergedups :: Eq a => [a] -> [(a, Int)]
mergedups [] = []
mergedups (x:xs) = go (xs) x 1
where
go [] last count = [(last,count)]
go (x:xs) last count =
if x == last
then go xs last (count+1)
else (last, count) : go xs x 1
-- Show what's happened recently.
recentChanges :: GraphState -> SEQ.Seq Id -> Handler GW
recentChanges gs allids =
do (pw,ids) <- paginate allids
let idws = map (\(id, count) -> (renderId gs id, count)) (mergedups $ toList ids)
return $
[whamlet|
^{pw}
<ul>
$forall (idw, count) <- idws
<li> ^{idw}
$if count > 1
\ (#{show count} changes)
^{pw}
|]
getRecentR :: Handler Html
getRecentR = getHomeR
-- Front page.
getHomeR :: Handler Html
getHomeR =
do ctx <- getContext Nothing
let gs = cgs ctx
changes <- recentChanges gs (_recent gs)
layout $
do setTitle "polink"
[whamlet|
^{cgw ctx}
<h1><center>Welcome to polink.org, the social network anyone can edit!</center>
<p><center><a href="@{AboutR}">What is this?</a></center>
<p><center><a href="@{RSHelpR}">What are those funny colored rectangle things?</a></center>
<p>
<b>Points of Interest:</b>
<p>
<center>
<a href="@{EntityR (Eid 2)}">US Government</a> (<a href="@{OrgChartR (Eid 2)}">org chart</a>) <br>
<a href="@{EntityR (Eid 11)}">US Presidents</a>
<a href="@{EntityR (Eid 48)}">US Supreme Court</a> <br>
<a href="@{EntityR (Eid 21)}">US Senate</a>
<a href="@{EntityR (Eid 22)}">US House</a> <br>
<a href="@{EntitiesR}">all entities</a> <a href="@{IssuesR}">all issues</a> <a href="@{UsersR}">all users</a>
<br>
$maybe u <- cmuser ctx
<p><b>Get Involved:</b>
<p>If you're looking for someone or something that isn't here, please help us out by adding them.
<p>If you find inaccurate or duplicate information, please leave a comment and click the "dispute" button on that entry. You can also help us out by visiting a <a href="@{RandomR}">random</a> entity and verifying that the data is correct. (Use the "verify"/"dispute" buttons.)
<p>You may also establish a link between two entities by using the clipboard. For instance, to mark someone as a member of the House, click "copy to clipboard" on that person's page, then navigate to the <a href=@{EntityR (Eid 22)}>US House</a> and create the link with "member of" as the link type.
$nothing
<p>
<center>
<b>This site depends on data contributed by volunteers. Create an account to get involved!
<p><b>Recent Changes:</b>
^{changes}
|]
getExploreR :: Handler Html
getExploreR =
do ctx <- getContext Nothing
let gs = cgs ctx
req <- getRequest
layout
[whamlet|
^{cgw ctx}
<h1><center>If you are reading this, the javascript UI (via explore.js) probably isn't working. Sorry.</center>
<center>
$maybe rt <- reqToken req
<p>request token is #{rt}
$nothing
<p>request token unavailable
<script src="http://polink.org/static/explore.js"></script>
|]
-- USER MANAGEMENT
minLen :: T.Text -> Int -> T.Text -> Either T.Text T.Text
minLen e min t
| T.length t >= min = Right t
| otherwise = Left e
maxLen :: T.Text -> Int -> T.Text -> Either T.Text T.Text
maxLen e max t
| T.length t <= max = Right t
| otherwise = Left e
bracketLen :: T.Text -> Int -> Int -> T.Text -> Either T.Text T.Text
bracketLen e min max t
| T.length t < min || T.length t > max = Left e
| otherwise = Right t
--newUserForm :: Html -> MForm InfluenceGraph InfluenceGraph (FormResult (T.Text, Bool), Widget)
newUserForm =
renderDivs $
(,)
<$> areq userField "username" Nothing
<*> areq trueField "I agree to the terms below." Nothing
where
userField = check (bracketLen "username must be between 4 and 64 characters" 4 64) textField
trueField = checkBool (id) ("you must agree to the site policy"::T.Text) checkBoxField
getNewUserR :: Handler Html
getNewUserR =
do maid <- maybeAuthId
(widget, enctype) <- generateFormPost newUserForm
layout
[whamlet|
$maybe id <- maid
<p>
Successfully authenticated via browserId. You may now create a
user acount.
<p>Please enter a username. This is the name that other users will see.
<form method=post action=@{NewUserR} enctype=#{enctype}>
^{widget}
<input type=submit value="create user"><p>
<p><a href=@{AuthR LogoutR}>cancel</a>
<h1>Acceptable Use
<p>