diff of cc81f1e668c372c866660c886ad0e8a861d466f5

cc81f1e668c372c866660c886ad0e8a861d466f5
diff --git a/elm-frontti/src/Ajax_cmds.elm b/elm-frontti/src/Ajax_cmds.elm
index a697a05..e260121 100644
--- a/elm-frontti/src/Ajax_cmds.elm
+++ b/elm-frontti/src/Ajax_cmds.elm
@@ -127,3 +127,9 @@ saveSettings settings =
         , expect = Http.expectWhatever SettingsSaved
         , timeout = Nothing
         , tracker = Nothing}
+
+searchPreviouslyPosts search_term =
+    Http.post
+        { url = "/api/posts/search-previously"
+        , body = Http.stringBody "application/json" search_term
+        , expect = Http.expectJson PreviouslySearchResult (Json.list Article.previouslyDocDecoder)}
diff --git a/elm-frontti/src/Article.elm b/elm-frontti/src/Article.elm
index 533a048..31c6f4c 100644
--- a/elm-frontti/src/Article.elm
+++ b/elm-frontti/src/Article.elm
@@ -24,7 +24,9 @@ import Time
 --   "id": 1,
 --   "versions": [],
 --   "version": null,
---   "created_at": "2020-10-16T07:52:59Z"
+--   "created_at": "2020-10-16T07:52:59Z",
+--   "previosly": [{"id": 666,
+--                  "title": "Titteli"}]
 -- }
 
 import Creator exposing (Creator, creatorDecoder)
@@ -33,6 +35,9 @@ decodeApply : Decode.Decoder a -> Decode.Decoder (a -> b) -> Decode.Decoder b
 decodeApply value partial =
     Decode.andThen (\p -> Decode.map p value) partial
 
+type alias PreviousArticle =
+    { id: Int
+    , title: String}
 
 type alias Article =
     { creator : Creator
@@ -48,9 +53,16 @@ type alias Article =
     , created_at: Maybe Time.Posix
     , hidden : Bool
     , unlisted : Bool
+    , previously: List PreviousArticle
     }
 
 -- encoder
+
+encodePreviously prev =
+    object
+      [ ( "id", int prev.id)
+      , ( "title", string prev.title)]
+
 encode : Article -> Json.Value
 encode article =
     object
@@ -66,6 +78,7 @@ encode article =
         , ( "created_at", (maybe iso8601) article.created_at)
         , ( "hidden", bool article.hidden)
         , ( "unlisted", bool article.unlisted)
+        , ( "previously", list encodePreviously article.previously)
         ]
 
 
@@ -85,6 +98,13 @@ creator_Decoder = Decode.field "creator" creatorDecoder
 hiddenDecoder = Decode.field "hidden" Decode.bool
 unlistedDecoder = Decode.field "unlisted" Decode.bool
 
+previouslyDocDecoder =
+    Decode.succeed PreviousArticle
+        |> decodeApply idDecoder
+        |> decodeApply titleDecoder
+
+previouslyDecoder = Decode.field "previously" (Decode.list previouslyDocDecoder)
+
 -- |> == clojure's ->>
 articleDecoder : Decoder Article                    
 articleDecoder =
@@ -100,6 +120,7 @@ articleDecoder =
         |> decodeApply created_atDecoder
         |> decodeApply hiddenDecoder
         |> decodeApply unlistedDecoder
+        |> decodeApply previouslyDecoder
 
 type alias Title =
     { title : String
diff --git a/elm-frontti/src/Article_view.elm b/elm-frontti/src/Article_view.elm
index 0a61041..09c9a6e 100644
--- a/elm-frontti/src/Article_view.elm
+++ b/elm-frontti/src/Article_view.elm
@@ -42,4 +42,9 @@ articleView settings loginstate zone the_actual_post =
                                     |> List.filter ((/=) "")
                                     |> List.map ( \tag -> span [] [ a [ href ("/blog/tags/" ++ tag)
                                                                       , class "tag" ] [text tag]
-                                                                  , text ", "]))]
+                                                                  , text ", "]))
+                           , div [ class "previously" ]
+                               (  the_actual_post.previously
+                               |> List.map (\prev -> span [] [ a [ href ("/blog/post/" ++ (String.fromInt prev.id))]
+                                                                   [ text settings.previously_label ]
+                                                             , text ", "]))]
diff --git a/elm-frontti/src/Main.elm b/elm-frontti/src/Main.elm
index d6ce68f..7aeb21a 100644
--- a/elm-frontti/src/Main.elm
+++ b/elm-frontti/src/Main.elm
@@ -65,7 +65,7 @@ subscriptions _ = Sub.batch
                   [ tags ReceivedTag
                   , aceStateUpdate AceStateUpdate]
 
-initialModel url key viewstate = Model viewstate Nothing False False [] Nothing LoggedOut key url Nothing Time.utc []
+initialModel url key viewstate = Model viewstate Nothing False False [] Nothing LoggedOut key url Nothing Time.utc [] []
     
 viewStatePerUrl : Url.Url -> (ViewState, List (Cmd Msg))
 viewStatePerUrl url =
@@ -125,6 +125,8 @@ init _ url key =
 -- PORTS --
 port prompt : String -> Cmd msg
 port alert : String -> Cmd msg
+port showPreviousPostsModal: (() -> Cmd msg)
+port closePreviousPostsModal: (() -> Cmd msg)
 port tags : (String -> msg) -> Sub msg
 port aceStateUpdate : (String -> msg) -> Sub msg
 
@@ -481,7 +483,66 @@ update msg model =
                         
                 Err http_error ->
                     ( model
-                    , alert ("Error saving settings " ++ Debug.toString http_error))    
+                    , alert ("Error saving settings " ++ Debug.toString http_error))
+        ShowPreviousPostsModal ->
+            ( model
+            , showPreviousPostsModal ())
+        ClosePreviousPostsModel ->
+            ( model
+            , closePreviousPostsModal ())
+        PreviouslySearchInput search_term ->
+            ( model
+            , searchPreviouslyPosts search_term)
+        PreviouslySearchResult result ->
+            case result of
+                Ok previously_posts ->
+                    let article_previouslies = case model.postEditorSettings of
+                                                    Just settings -> settings.article.previously
+                                                    Nothing -> []
+                    in
+                    ({ model |
+                           searchedPosts = List.filter (\p -> not (List.member p article_previouslies)) previously_posts}
+                    , Cmd.none)
+                Err error ->
+                    ( model
+                    , alert (errToString error))
+        SelectPreviouslyPost selectedPost  ->
+            case model.postEditorSettings of
+                Just editorSettings -> 
+                    let new_posts = List.filter ((/=) selectedPost) model.searchedPosts
+                        article = editorSettings.article
+                        previously = article.previously
+                    in
+                        ({ model
+                             | searchedPosts = new_posts
+                             , postEditorSettings = Just { editorSettings
+                                                             | article = { article
+                                                                             | previously = selectedPost :: previously}}}
+                        , Cmd.none)
+                Nothing ->
+                    ( model
+                    , Cmd.none)
+        DropPreviously previous_post ->
+            case model.postEditorSettings of
+                Just editorSettings -> 
+                    let article = editorSettings.article
+                        previously = article.previously
+                    in
+                        ({ model
+                             | postEditorSettings = Just
+                               { editorSettings
+                                     | article =
+                                     { article
+                                           | previously = List.filter ((/=) previous_post) previously}}}
+                        , Cmd.none)
+                Nothing ->
+                    ( model
+                    , Cmd.none)
+        SetPreviouslyLabel label ->
+            ({ model | settings = Maybe.map (\settings ->
+                                                 { settings | previously_label = label})
+                   model.settings}
+            , Cmd.none)                       
             
 doGoHome_ model other_cmds =
     (model, Cmd.batch (List.append [ getSettings
@@ -565,7 +626,7 @@ view model =
                                                    Just editorSettings ->
                                                        let post = editorSettings.article
                                                            tag_index = editorSettings.selected_tag in
-                                                       PostEditor.postEditor post tag_index model.showImageModal model.loadedImages model.draggingImages editorSettings settings model.zone model.loginState
+                                                       PostEditor.postEditor post tag_index model.showImageModal model.loadedImages model.draggingImages editorSettings settings model.zone model.loginState model.searchedPosts
                                                    Nothing -> [ div [] [ text "No post loaded" ]]
                                            MediaList -> [ medialist model.loadedImages model.medialist_state ]
                                            SettingsEditor -> [ SettingsEditor.editor settings])
diff --git a/elm-frontti/src/Message.elm b/elm-frontti/src/Message.elm
index 8eb44b0..9c39456 100644
--- a/elm-frontti/src/Message.elm
+++ b/elm-frontti/src/Message.elm
@@ -72,7 +72,8 @@ type alias Model =
     , url : Url.Url
     , postEditorSettings: Maybe PostEditorSettings
     , zone : Time.Zone
-    , titles : List Article.Title }
+    , titles : List Article.Title
+    , searchedPosts : List Article.PreviousArticle}
     
 type Msg
   = PageReceived (Result Http.Error P.Page)
@@ -130,6 +131,13 @@ type Msg
   | SetPageSize String
   | SaveSettings
   | SettingsSaved (Result Http.Error ())
+  | ShowPreviousPostsModal
+  | ClosePreviousPostsModel
+  | PreviouslySearchInput String
+  | PreviouslySearchResult (Result Http.Error (List Article.PreviousArticle))
+  | SelectPreviouslyPost Article.PreviousArticle
+  | DropPreviously Article.PreviousArticle
+  | SetPreviouslyLabel String
 
 -- ports
 port reallySetupAce : String -> Cmd msg
diff --git a/elm-frontti/src/PostEditor.elm b/elm-frontti/src/PostEditor.elm
index af3f1d3..98c99c7 100644
--- a/elm-frontti/src/PostEditor.elm
+++ b/elm-frontti/src/PostEditor.elm
@@ -58,8 +58,25 @@ editor params =
 filesDecoder : D.Decoder (List File)
 filesDecoder =
   D.at ["target","files"] (D.list File.decoder)
+
+previouslyButtons post loadedPosts =
+    div [ class "previously-buttons" ]
+        [ murja_button [ onClick ShowPreviousPostsModal ] [ text "Link previous posts"]
+        , ul []
+            ( post.previously
+            |> List.map (\p -> li [] [ text p.title, button [ onClick (DropPreviously p)] [ text "X"]]))
+        , node "dialog" [ id "previouslyModal" ]
+            [ div [ class "dialog" ]
+                  [ header [ class "previouslyHeader" ] [ button [ onClick ClosePreviousPostsModel ] [ text "X"]]
+                  , select [ multiple True
+                           , class "previouslyPostResult"]
+                      (List.map (\prev_post ->
+                                     option [ onClick (SelectPreviouslyPost prev_post) ] [ text prev_post.title ]) loadedPosts)
+                  , input [ type_ "text"
+                          , placeholder "Search for posts"
+                          , onInput PreviouslySearchInput] []]]]
       
-postEditor post tag showImageModal loadedImages draggingImages editorSettings app_settings tz loginState
+postEditor post tag showImageModal loadedImages draggingImages editorSettings app_settings tz loginState searchedPosts
     = [ div [ class "editor-top" ]
             [ div [ id "editor-buttons"
                   , class "editor-grouper"]
@@ -80,6 +97,7 @@ postEditor post tag showImageModal loadedImages draggingImages editorSettings ap
                                  , onClick GetListOfImages]
                         [text "Insert image"]]
             , tagView post tag
+            , previouslyButtons post searchedPosts
             , div [ class "editor-grouper" ]
                 [ label [ for "hidden"]
                       [ text "Hidden article"]
diff --git a/elm-frontti/src/Settings.elm b/elm-frontti/src/Settings.elm
index 900aa19..d5daad3 100644
--- a/elm-frontti/src/Settings.elm
+++ b/elm-frontti/src/Settings.elm
@@ -8,15 +8,19 @@ import Json.Encode as Json exposing (..)
 type alias Settings =
     { time_format : String
     , blog_title : String
-    , recent_post_count : Int}
+    , recent_post_count : Int
+    , previously_label: String
+    }
 
-settingsDecoder = Decode.map3 Settings
+settingsDecoder = Decode.map4 Settings
                   (Decode.field "time-format" Decode.string)
                   (Decode.field "blog-title" Decode.string)
                   (Decode.field "recent-post-count" Decode.int)
+                  (Decode.field "previously_label" Decode.string)
                      
 encodeSettings settings =
     object
         [ ( "time-format", string settings.time_format )
         , ( "blog-title", string settings.blog_title)
-        , ( "recent-post-count", int settings.recent_post_count)]
+        , ( "recent-post-count", int settings.recent_post_count)
+        , ( "previously_label", string settings.previously_label) ]
diff --git a/elm-frontti/src/SettingsEditor.elm b/elm-frontti/src/SettingsEditor.elm
index 226aa6c..4e8d11a 100644
--- a/elm-frontti/src/SettingsEditor.elm
+++ b/elm-frontti/src/SettingsEditor.elm
@@ -28,5 +28,11 @@ editor settings =
                 , value (String.fromInt settings.recent_post_count)
                 , type_ "number"] []
 
+        , label [ for "previously_label" ]
+            [ text "Previously link label" ]
+        , input [ id "previously"
+                , onInput SetPreviouslyLabel
+                , value settings.previously_label] []            
+
         , button [ onClick SaveSettings ]
             [ text "Save settings"]]
diff --git a/resources/css/murja.css b/resources/css/murja.css
index 2e5eb73..094259f 100644
--- a/resources/css/murja.css
+++ b/resources/css/murja.css
@@ -265,6 +265,14 @@ header {
     grid-column: 2;
 }
 
+.previouslyHeader {
+    display: flex;
+    justify-content: flex-end;
+}
+
+.previouslyPostResult {
+    display: block;
+}
 
 @media only screen and (max-device-width:480px)
 {
diff --git a/resources/js/murja-helper.js b/resources/js/murja-helper.js
index 902cc54..0006998 100644
--- a/resources/js/murja-helper.js
+++ b/resources/js/murja-helper.js
@@ -43,3 +43,12 @@ Object.defineProperty(HTMLElement.prototype, "dangerouslySetInnerHTML", {
         this.innerHTML = value
     }
 });
+
+app.ports.showPreviousPostsModal.subscribe(_ => {
+    document.getElementById('previouslyModal').showModal();
+});
+
+
+app.ports.closePreviousPostsModal.subscribe(_ => {
+    document.getElementById('previouslyModal').close();
+});
diff --git a/resources/sql/018-previously.sql b/resources/sql/018-previously.sql
new file mode 100644
index 0000000..b7fc8a5
--- /dev/null
+++ b/resources/sql/018-previously.sql
@@ -0,0 +1,17 @@
+CREATE TABLE IF NOT EXISTS blog.Previously_Link
+(
+	referencee_id INT NOT NULL,
+	referenced_id INT NOT NULL,
+	PRIMARY KEY (referencee_id, referenced_id),
+	FOREIGN KEY (referencee_id) REFERENCES blog.Post(id) ON UPDATE CASCADE ON DELETE CASCADE,
+	FOREIGN KEY (referenced_id) REFERENCES blog.Post(id) ON UPDATE CASCADE ON DELETE CASCADE
+);
+
+CREATE OR REPLACE VIEW blog.Previously_Link_Titles AS
+        SELECT pl.referencee_id,
+               pl.referenced_id AS id,
+	       p.Title AS title
+	FROM blog.Previously_Link pl
+	JOIN blog.Post p on pl.referenced_id = p.id;
+
+INSERT INTO blog.Settings VALUES ('previously_label', '"Previously"') ON CONFLICT DO NOTHING;
diff --git a/resources/sql/post-fns.sql b/resources/sql/post-fns.sql
index 9d29e8c..3eec83b 100644
--- a/resources/sql/post-fns.sql
+++ b/resources/sql/post-fns.sql
@@ -35,9 +35,11 @@ SELECT p.ID,
 			 'img_location',
 			 u.Img_location) as "creator",
        (SELECT MAX(version) + 1 FROM blog.Post_History phh WHERE (phh.id = p.id AND ((not phh.unlisted) OR $2) AND ((not phh.hidden) OR $2))) AS version,
-       json_agg(DISTINCT version) as "versions", p.hidden, p.unlisted
+       json_agg(DISTINCT version) as "versions", p.hidden, p.unlisted,
+       json_agg(to_jsonb (pl.*) - 'referencee_id') as previously
 FROM blog.Post p
 JOIN blog.Users u ON u.ID = p.creator_id
+LEFT JOIN blog.Previously_Link_Titles pl ON p.id = pl.referencee_id
 LEFT JOIN blog.Post_History ph ON (ph.id = p.id AND ((not ph.unlisted) OR $2) AND ((not ph.hidden) OR $2))
 WHERE p.ID = $1 AND (NOT p.hidden OR (p.hidden AND $2))
 GROUP BY p.ID, u.ID;
@@ -52,19 +54,23 @@ SELECT p.ID, p.Title, p.created_at, p.Content, p.tags, p.version, 0 AS "amount-o
 			 u.Nickname,
 			 'img_location',
 			 u.Img_location) as "creator",
-     json_agg(DISTINCT ph.version) as "versions", p.hidden, p.unlisted
+     json_agg(DISTINCT ph.version) as "versions", p.hidden, p.unlisted,
+     json_agg(to_jsonb (pl.*) - 'referencee_id') as previously
 FROM blog.Post_History p
 JOIN blog.Users u ON u.ID = p.creator_id
 LEFT JOIN blog.Post_History ph ON (ph.id = p.id AND (not ph.unlisted) AND (not ph.hidden))
+LEFT JOIN blog.Previously_Link_Titles pl ON p.id = pl.referencee_id
 WHERE p.ID = $1 AND p.version = $2 AND not p.hidden
 GROUP BY p.ID, p.Title, p.created_at, p.Content, p.tags, p.version, "amount-of-comments", u.username, u.nickname, u.img_location;
 
 
 -- name: get-all*
-SELECT p.id, p.Title, p.Content, p.created_at, p.tags, u.Username, u.Nickname, u.Img_location, COUNT(c.ID) AS "amount-of-comments", p.hidden, p.unlisted
+SELECT p.id, p.Title, p.Content, p.created_at, p.tags, u.Username, u.Nickname, u.Img_location, COUNT(c.ID) AS "amount-of-comments", p.hidden, p.unlisted,
+       json_agg(to_jsonb (pl.*) - 'referencee_id') as previously
 FROM blog.Post p
 JOIN blog.Users u ON u.ID = p.creator_id
 LEFT JOIN blog.Comment c ON c.parent_post_id = p.ID
+LEFT JOIN blog.Previously_Link_Titles pl ON p.id = pl.referencee_id
 WHERE NOT p.hidden
 GROUP BY p.ID, u.ID
 ORDER BY p.created_at DESC
@@ -78,11 +84,13 @@ ORDER BY p.created_at DESC
 -- $3 == show-hidden?
 -- name: get-page*
 -- returns: :array-hash
-SELECT p.ID, p.Title, p.Content, p.created_at, p.tags, COUNT(c.ID) AS "amount-of-comments", json_build_object('username', u.Username, 'nickname', u.Nickname, 'img_location', u.Img_location) as "creator", json_agg(DISTINCT version) as "versions", p.hidden, p.unlisted
+SELECT p.ID, p.Title, p.Content, p.created_at, p.tags, COUNT(c.ID) AS "amount-of-comments", json_build_object('username', u.Username, 'nickname', u.Nickname, 'img_location', u.Img_location) as "creator", json_agg(DISTINCT version) as "versions", p.hidden, p.unlisted,
+       json_agg(to_jsonb (pl.*) - 'referencee_id') as previously
 FROM blog.Post p
 JOIN blog.Users u ON u.ID = p.creator_id
 LEFT JOIN blog.Comment c ON c.parent_post_id = p.ID
 LEFT JOIN blog.Post_History ph ON (ph.id = p.id AND ((not ph.unlisted) OR $3) AND ((not ph.hidden) OR $3))
+LEFT JOIN blog.Previously_Link_Titles pl ON p.id = pl.referencee_id
 WHERE ((NOT p.unlisted) OR $3)
   AND ((NOT p.hidden) OR $3)
 GROUP BY p.ID, u.ID
@@ -163,3 +171,10 @@ where id = $6;
 update blog.post
 set hidden = $2 
 where id = $1;
+
+-- name: link-previously
+INSERT INTO blog.Previously_Link VALUES ($1, $2);
+
+-- name: search-posts
+-- returns: :array-hash
+select post.id, post.title from blog.Post where (title ilike '%'||$1||'%' or content ilike '%'||$1||'%') and not unlisted and not hidden;
diff --git a/src/migration-list.lisp b/src/migration-list.lisp
index 740fbf7..896af6e 100644
--- a/src/migration-list.lisp
+++ b/src/migration-list.lisp
@@ -22,6 +22,7 @@
 (defmigration "015-image-post-pairing-view.up")
 (defmigration "016-hardcoded-hidden-unlisted")
 (defmigration "017-settings-in-db")
+(defmigration "018-previously")
 
 (defun prepare-e2e-migration ()
   (postmodern:execute "DELETE FROM blog.Users")
diff --git a/src/posts/post-db.lisp b/src/posts/post-db.lisp
index 42d8cf7..dacda5d 100644
--- a/src/posts/post-db.lisp
+++ b/src/posts/post-db.lisp
@@ -3,7 +3,7 @@
   (:import-from :com.inuoe.jzon :parse)
   (:import-from :halisql :defqueries)
   (:import-from :lisp-fixup :fix-timestamp)
-  (:export :get-tagged :get-post-version :get-page :get-titles-by-year :insert-post :update-post :get-post))
+  (:export :search-posts :link-previously :get-tagged :get-post-version :get-page :get-titles-by-year :insert-post :update-post :get-post))
 
 (in-package :murja.posts.post-db)
 
@@ -20,11 +20,16 @@
 	   (get-titles-by-year* allow-hidden?) 'list)))
 
 (defun fix-post (post)
-  (dolist (key (list "creator" "tags" "versions"))
+  (dolist (key (list "creator" "tags" "versions" "previously"))
     (when (gethash key post)
       (setf (gethash key post)
 	    (parse (gethash key post)))))
 
+  (setf (gethash "previously" post)
+	(or 
+	 (remove-if-not #'hash-table-p (coerce (gethash "previously" post) 'list))
+	 #()))
+
   (setf (gethash "created_at" post)
 	(fix-timestamp (gethash "created_at" post)))
   post)
@@ -51,4 +56,4 @@
     (log:info "Tag ~a returns posts ~a~%" tag (mapcar #'alexandria:hash-table-alist posts))
     (when posts
       (mapcar #'fix-post
-	      posts)))) 
+	      posts))))
diff --git a/src/routes/post-routes.lisp b/src/routes/post-routes.lisp
index 739e35d..ec72136 100644
--- a/src/routes/post-routes.lisp
+++ b/src/routes/post-routes.lisp
@@ -85,6 +85,14 @@
   (let ((creator-id (gethash "id" *user*)))
     (prin1-to-string (caar (murja.posts.post-db:insert-post "New title" "New post" creator-id "[]" t nil)))))
 
+(defroute search-prev ("/api/posts/search-previously" :method :post
+						      :decorators (@json
+								   @transaction
+								   @authenticated
+								   (@can? "create-post"))) ()
+  (let* ((search-body (hunchentoot:raw-post-data :force-text t)))
+    (stringify (murja.posts.post-db:search-posts search-body))))
+
 (defroute post-update-route ("/api/posts/post" :method :put 
 					       :decorators (@json
 							    @transaction
@@ -99,9 +107,13 @@
 				(gethash "tags" request-body) 'list))
 		    #())))
 	 (post-id (gethash "id" request-body))
+	 (previously-links (coerce (gethash "previously" request-body) 'list))
 	 (hidden (gethash "hidden" request-body))
 	 (unlisted (gethash "unlisted" request-body)))
     (log:info "updating post ~d" post-id)
 
     (murja.posts.post-db:update-post title content tags hidden unlisted post-id)
+    (dolist (link previously-links)
+      (let ((id (gethash "id" link)))
+	(murja.posts.post-db:link-previously post-id id)))
     ""))