Commit 1aa7eefa authored by Przemyslaw Kaminski's avatar Przemyslaw Kaminski

Merge branch 'dev' into dev-doc-annotation-issue

parents 44a2d2ad b3ad95a1
......@@ -92,10 +92,11 @@ CREATE TABLE public.nodes_nodes (
node2_id INTEGER NOT NULL REFERENCES public.nodes(id) ON DELETE CASCADE,
score REAL,
category INTEGER,
PRIMARY KEY (node1_id,node2_id)
PRIMARY KEY (node1_id, node2_id)
);
ALTER TABLE public.nodes_nodes OWNER TO gargantua;
---------------------------------------------------------------
CREATE TABLE public.node_node_ngrams (
node1_id INTEGER NOT NULL REFERENCES public.nodes (id) ON DELETE CASCADE,
......@@ -107,7 +108,6 @@ PRIMARY KEY (node1_id, node2_id, ngrams_id, ngrams_type)
);
ALTER TABLE public.node_node_ngrams OWNER TO gargantua;
CREATE TABLE public.node_node_ngrams2 (
node_id INTEGER NOT NULL REFERENCES public.nodes (id) ON DELETE CASCADE,
nodengrams_id INTEGER NOT NULL REFERENCES public.node_ngrams (id) ON DELETE CASCADE,
......
......@@ -4,9 +4,18 @@ MASTER_USER = gargantua
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = PASSWORD_TO_CHANGE
# Frames
FRAME_WRITE_URL = URL_TO_CHANGE
FRAME_CALC_URL = URL_TO_CHANGE
[network]
# Emails From address (sent by smtp)
MAIL = username@gargantext.org
HOST = localhost
# if remote smtp host
# HOST_USER = user
# HOST_password = password
[database]
# PostgreSQL access
DB_HOST = 127.0.0.1
......@@ -14,7 +23,8 @@ DB_PORT = 5432
DB_NAME = gargandbV5
DB_USER = gargantua
DB_PASS = PASSWORD_TO_CHANGE
# Logs
[logs]
LOG_FILE = /var/log/gargantext/backend.log
LOG_LEVEL = DEBUG
LOG_FORMATTER = verbose
......@@ -208,6 +208,16 @@ library:
- servant-xml
- simple-reflect
- singletons # (IGraph)
# for mail
- smtp-mail
- mime-mail
# for password generation
- cprng-aes
- binary
- crypto-random
- split
- stemmer
- string-conversions
......
......@@ -5,7 +5,7 @@ import Data.Swagger
import Data.Text (Text)
import Gargantext.Prelude
import qualified Gargantext.Prelude.Utils as Crypto (hash)
import qualified Gargantext.Core.Crypto.Hash as Crypto (hash)
import GHC.Generics (Generic)
data HashedResponse a = HashedResponse { hash :: Text, value :: a }
......
......@@ -55,7 +55,6 @@ instance ToJSON a => MimeRender HTML a where
mimeRender _ = encode
------------------------------------------------------------------------
get :: RepoCmdM env err m =>
ListId -> m (Headers '[Header "Content-Disposition" Text] NgramsList)
get lId = do
......@@ -74,7 +73,6 @@ get' lId = fromList
<$> mapM (getNgramsTableMap lId) ngramsTypes
------------------------------------------------------------------------
-- TODO : purge list
post :: FlowCmdM env err m
=> ListId
......@@ -88,7 +86,6 @@ post l m = do
------------------------------------------------------------------------
------------------------------------------------------------------------
type PostAPI = Summary "Update List"
:> "add"
:> "form"
......
......@@ -67,15 +67,18 @@ getTermsWith f ls ngt lt = Map.fromListWith (<>)
Nothing -> (f'' t, [])
Just r -> (f'' r, map f'' [t])
mapTermListRoot :: [ListId] -> NgramsType
-> NgramsRepo -> Map Text (ListType, (Maybe Text))
mapTermListRoot :: [ListId]
-> NgramsType
-> NgramsRepo
-> Map Text (ListType, (Maybe Text))
mapTermListRoot nodeIds ngramsType repo =
Map.fromList [ (t, (_nre_list nre, _nre_root nre))
| (t, nre) <- Map.toList ngrams
]
where ngrams = listNgramsFromRepo nodeIds ngramsType repo
filterListWithRoot :: ListType -> Map Text (ListType, Maybe Text)
filterListWithRoot :: ListType
-> Map Text (ListType, Maybe Text)
-> Map Text (Maybe RootTerm)
filterListWithRoot lt m = Map.fromList
$ map (\(t,(_,r)) -> (t,r))
......
{-|
Module : Gargantext.API.Node.Contact
Description :
Copyright : (c) CNRS, 2017-Present
License : AGPL + CECILL v3
Maintainer : team@gargantext.org
Stability : experimental
Portability : POSIX
-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}
module Gargantext.API.Node.Contact
where
import Data.Aeson
import Data.Either (Either(Right))
import Data.Maybe (Maybe(..))
import Data.Swagger
import Data.Text (Text)
import GHC.Generics (Generic)
import Gargantext.API.Admin.Orchestrator.Types (JobLog(..))
import Gargantext.API.Admin.Settings (HasSettings)
import Gargantext.API.Node
import Gargantext.API.Node.Corpus.New (AsyncJobs)
import Gargantext.API.Prelude (GargServer, simuLogs)
import Gargantext.Core (Lang(..))
import Gargantext.Core.Types.Individu (User(..))
import Gargantext.Database.Action.Flow (flow)
import Gargantext.Database.Action.Flow.Types (FlowCmdM)
import Gargantext.Database.Admin.Types.Hyperdata (HyperdataAnnuaire(..), HyperdataContact)
import Gargantext.Database.Admin.Types.Hyperdata.Contact (hyperdataContact)
import Gargantext.Database.Admin.Types.Node
import Gargantext.Prelude (($), liftBase, (.), printDebug, pure)
import Gargantext.Text.Terms (TermType(..))
import Servant
import Servant.Job.Async (JobFunction(..), serveJobsAPI)
import Test.QuickCheck (elements)
import Test.QuickCheck.Arbitrary
------------------------------------------------------------------------
type API = "contact" :> Summary "Contact endpoint"
:> API_Async
:<|> Capture "contact_id" NodeId
:> NodeNodeAPI HyperdataContact
api :: UserId -> CorpusId -> GargServer API
api uid cid = (api_async (RootId (NodeId uid)) cid)
:<|> (nodeNodeAPI (Proxy :: Proxy HyperdataContact) uid cid)
type API_Async = AsyncJobs JobLog '[JSON] AddContactParams JobLog
------------------------------------------------------------------------
data AddContactParams = AddContactParams { firstname :: !Text, lastname :: !Text }
| AddContactParamsAdvanced { firstname :: !Text
, lastname :: !Text
-- TODO add others fields
}
deriving (Generic)
----------------------------------------------------------------------
api_async :: User -> NodeId -> GargServer API_Async
api_async u nId =
serveJobsAPI $
JobFunction (\p log ->
let
log' x = do
printDebug "addContact" x
liftBase $ log x
in addContact u nId p (liftBase . log')
)
addContact :: (HasSettings env, FlowCmdM env err m)
=> User
-> NodeId
-> AddContactParams
-> (JobLog -> m ())
-> m JobLog
addContact u nId (AddContactParams fn ln) logStatus = do
logStatus JobLog { _scst_succeeded = Just 1
, _scst_failed = Just 0
, _scst_remaining = Just 1
, _scst_events = Just []
}
_ <- flow (Nothing :: Maybe HyperdataAnnuaire) u (Right [nId]) (Multi EN) [[hyperdataContact fn ln]]
pure JobLog { _scst_succeeded = Just 2
, _scst_failed = Just 0
, _scst_remaining = Just 0
, _scst_events = Just []
}
addContact _uId _nId _p logStatus = do
simuLogs logStatus 10
------------------------------------------------------------------------
-- TODO unPrefix "pn_" FromJSON, ToJSON, ToSchema, adapt frontend.
instance FromJSON AddContactParams where
parseJSON = genericParseJSON (defaultOptions { sumEncoding = ObjectWithSingleField })
instance ToJSON AddContactParams where
toJSON = genericToJSON (defaultOptions { sumEncoding = ObjectWithSingleField })
instance ToSchema AddContactParams
instance Arbitrary AddContactParams where
arbitrary = elements [AddContactParams "Pierre" "Dupont"]
------------------------------------------------------------------------
......@@ -74,30 +74,11 @@ addToAnnuaireWithForm _cid (AnnuaireWithForm ft _d _l) logStatus = do
printDebug "ft" ft
-- let
-- parse = case ft of
-- CSV_HAL -> Parser.parseFormat Parser.CsvHal
-- CSV -> Parser.parseFormat Parser.CsvGargV3
-- WOS -> Parser.parseFormat Parser.WOS
-- PresseRIS -> Parser.parseFormat Parser.RisPresse
-- docs <- liftBase
-- $ splitEvery 500
-- <$> take 1000000
-- <$> parse (cs d)
logStatus JobLog { _scst_succeeded = Just 1
, _scst_failed = Just 0
, _scst_remaining = Just 1
, _scst_events = Just []
}
-- cid' <- flowCorpus "user1"
-- (Right [cid])
-- (Multi $ fromMaybe EN l)
-- (map (map toHyperdataDocument) docs)
-- printDebug "cid'" cid'
pure JobLog { _scst_succeeded = Just 2
, _scst_failed = Just 0
, _scst_remaining = Just 0
......
......@@ -19,22 +19,19 @@ Main exports of Gargantext:
module Gargantext.API.Node.Corpus.Export
where
import Data.Aeson.TH (deriveJSON)
import qualified Data.List as List
import qualified Data.Map as Map
import Data.Map (Map)
import Data.Maybe (fromMaybe)
import Data.Set (Set)
import qualified Data.Set as Set
import Data.Swagger
import Data.Text (Text)
import GHC.Generics (Generic)
import Servant
import Gargantext.API.Ngrams
import Gargantext.API.Ngrams.Tools (filterListWithRoot, mapTermListRoot, getRepo)
import Gargantext.API.Prelude (GargNoServer)
import Gargantext.Core.Types --
import Gargantext.Core.Crypto.Hash (hash)
import Gargantext.Core.Types
import Gargantext.Core.Utils.Prefix (unPrefix, unPrefixSwagger)
import Gargantext.Database.Action.Metrics.NgramsByNode (getNgramsByNodeOnlyUser)
import Gargantext.Database.Admin.Config (userMaster)
......@@ -42,13 +39,16 @@ import Gargantext.Database.Admin.Types.Hyperdata (HyperdataDocument(..))
import Gargantext.Database.Admin.Types.Node (Node, NodeId, ListId, CorpusId)
import Gargantext.Database.Prelude (Cmd)
import Gargantext.Database.Query.Table.Node
import Gargantext.Database.Query.Table.Node.Select (selectNodesWithUsername)
import Gargantext.Database.Query.Table.Node.Error (HasNodeError)
import Gargantext.Database.Query.Table.Node.Select (selectNodesWithUsername)
import Gargantext.Database.Query.Table.NodeNode (selectDocNodes)
import Gargantext.Database.Schema.Node (_node_id, _node_hyperdata)
import Gargantext.Database.Schema.Ngrams (NgramsType(..))
import Gargantext.Database.Schema.Node (_node_id, _node_hyperdata)
import Gargantext.Prelude
import Gargantext.Prelude.Utils (hash)
import Servant
import qualified Data.List as List
import qualified Data.Map as Map
import qualified Data.Set as Set
-- Corpus Export
......
......@@ -213,12 +213,12 @@ addToCorpusWithQuery u cid (WithQuery q dbs l _nid) logStatus = do
, _scst_remaining = Just 5
, _scst_events = Just []
}
printDebug "addToCorpusWithQuery" cid
printDebug "addToCorpusWithQuery" (cid, dbs)
-- TODO add cid
-- TODO if cid is folder -> create Corpus
-- if cid is corpus -> add to corpus
-- if cid is root -> create corpus in Private
txts <- mapM (\db -> getDataText db (Multi l) q (Just 10000)) [database2origin dbs]
txts <- mapM (\db -> getDataText db (Multi l) q Nothing) [database2origin dbs]
logStatus JobLog { _scst_succeeded = Just 2
, _scst_failed = Just 0
......
......@@ -30,7 +30,7 @@ import Gargantext.API.Ngrams (TODO)
import Gargantext.Database.Admin.Types.Node
import Gargantext.Database.Prelude -- (Cmd, CmdM)
import Gargantext.Prelude
import Gargantext.Prelude.Utils (hash)
import Gargantext.Core.Crypto.Hash (hash)
import Servant
import Servant.Multipart
import Servant.Swagger (HasSwagger(toSwagger))
......
......@@ -41,10 +41,10 @@ type API = Summary " Update node according to NodeType params"
:> AsyncJobs JobLog '[JSON] UpdateNodeParams JobLog
------------------------------------------------------------------------
data UpdateNodeParams = UpdateNodeParamsList { methodList :: Method }
| UpdateNodeParamsGraph { methodGraph :: GraphMetric }
| UpdateNodeParamsTexts { methodTexts :: Granularity }
| UpdateNodeParamsBoard { methodBoard :: Charts }
data UpdateNodeParams = UpdateNodeParamsList { methodList :: !Method }
| UpdateNodeParamsGraph { methodGraph :: !GraphMetric }
| UpdateNodeParamsTexts { methodTexts :: !Granularity }
| UpdateNodeParamsBoard { methodBoard :: !Charts }
deriving (Generic)
----------------------------------------------------------------------
......
......@@ -7,7 +7,6 @@ Maintainer : team@gargantext.org
Stability : experimental
Portability : POSIX
-}
{-# OPTIONS_GHC -fno-warn-name-shadowing #-}
......@@ -51,7 +50,7 @@ import qualified Gargantext.API.Node.Corpus.Annuaire as Annuaire
import qualified Gargantext.API.Node.Corpus.Export as Export
import qualified Gargantext.API.Node.Corpus.New as New
import qualified Gargantext.API.Public as Public
import qualified Gargantext.API.Node.Contact as Contact
type GargAPI = "api" :> Summary "API " :> GargAPIVersion
-- | TODO :<|> Summary "Latest API" :> GargAPI'
......@@ -116,9 +115,7 @@ type GargPrivateAPI' =
:<|> "annuaire" :> Summary "Contact endpoint"
:> Capture "annuaire_id" NodeId
:> "contact"
:> Capture "contact_id" NodeId
:> NodeNodeAPI HyperdataContact
:> Contact.API
-- Document endpoint
:<|> "document" :> Summary "Document endpoint"
......@@ -208,7 +205,7 @@ serverPrivateGargAPI' (AuthenticatedUser (NodeId uid))
:<|> nodeNodeAPI (Proxy :: Proxy HyperdataAny) uid
:<|> Export.getCorpus -- uid
:<|> nodeAPI (Proxy :: Proxy HyperdataAnnuaire) uid
:<|> nodeNodeAPI (Proxy :: Proxy HyperdataContact) uid
:<|> Contact.api uid
:<|> withAccess (Proxy :: Proxy TableNgramsApi) Proxy uid
<$> PathNode <*> apiNgramsTableDoc
......@@ -246,7 +243,6 @@ waitAPI n = do
pure $ "Waited: " <> (cs $ show n)
----------------------------------------
addCorpusWithQuery :: User -> GargServer New.AddWithQuery
addCorpusWithQuery user cid =
serveJobsAPI $
......
......@@ -28,7 +28,7 @@ import Gargantext.API.Prelude (GargServer)
import Gargantext.Core.Utils.Prefix (unPrefix, unPrefixSwagger)
import Gargantext.Database.Query.Facet
import Gargantext.Database.Action.Search
import Gargantext.Database.Admin.Types.Hyperdata (HyperdataDocument)
import Gargantext.Database.Admin.Types.Hyperdata (HyperdataContact)
import Gargantext.Database.Admin.Types.Node
import Gargantext.Prelude
import Servant
......@@ -60,7 +60,7 @@ instance ToSchema SearchDocResults where
declareNamedSchema = genericDeclareNamedSchema (unPrefixSwagger "sdr_")
data SearchPairedResults =
SearchPairedResults { spr_results :: [FacetPaired Int UTCTime HyperdataDocument Int [Pair Int Text]] }
SearchPairedResults { spr_results :: [FacetPaired Int UTCTime HyperdataContact Int] }
deriving (Generic)
$(deriveJSON (unPrefix "spr_") ''SearchPairedResults)
......@@ -89,12 +89,12 @@ searchDocs nId (SearchQuery q) o l order =
-----------------------------------------------------------------------
type SearchPairsAPI = Summary ""
:> "list"
:> Capture "list" ListId
:> Capture "annuaire" AnnuaireId
:> SearchAPI SearchPairedResults
searchPairs :: NodeId -> GargServer SearchPairsAPI
searchPairs pId lId (SearchQuery q) o l order =
SearchPairedResults <$> searchInCorpusWithContacts pId lId q o l order
searchPairs pId aId (SearchQuery q) o l order =
SearchPairedResults <$> searchInCorpusWithContacts pId aId q o l order
-----------------------------------------------------------------------
{-|
Module : Gargantext.Core.Crypto.Hash
Description : Useful Tools near Prelude of the project
Copyright : (c) CNRS, 2017-Present
License : AGPL + CECILL v3
Maintainer : team@gargantext.org
Stability : experimental
Portability : POSIX
-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Gargantext.Core.Crypto.Hash
where
import Prelude (String)
import Data.Set (Set)
import Data.List (foldl)
import Data.Text (Text)
import Gargantext.Prelude
import qualified Data.ByteString.Lazy.Char8 as Char
import qualified Data.Digest.Pure.SHA as SHA (sha256, showDigest)
import qualified Data.Set as Set
import qualified Data.Text as Text
--------------------------------------------------------------------------
-- | Use this datatype to keep traceability of hashes
-- TODO use newtype
type Hash = Text
-- | Class to make hashes
class IsHashable a where
hash :: a -> Hash
-- | Main API to hash text
-- using sha256 for now
instance IsHashable Char.ByteString where
hash = Text.pack
. SHA.showDigest
. SHA.sha256
instance {-# OVERLAPPING #-} IsHashable String where
hash = hash . Char.pack
instance IsHashable Text where
hash = hash . Text.unpack
instance IsHashable (Set Hash) where
hash = hash . foldl (<>) "" . Set.toList
instance {-# OVERLAPPABLE #-} IsHashable a => IsHashable [a] where
hash = hash . Set.fromList . map hash
{-|
Module : Gargantext.Core.Crypto.Pass
Description :
Copyright : (c) CNRS, 2017-Present
License : Public Domain
Maintainer : team@gargantext.org
Stability : experimental
Portability : POSIX
To avoid weak password, just offer an easy way to make "good" one and
let user add his own entropy.
Thanks to
https://zuttobenkyou.wordpress.com/2011/12/23/simple-password-generation-with-haskell/
-}
module Gargantext.Core.Crypto.Pass
where
-- import Data.List (nub)
-- import System.Environment (getArgs)
-- import System.IO (hSetEcho)
import Control.Monad.State
import Crypto.Random (cprgGenerate)
import Crypto.Random.AESCtr
import Data.Binary (decode)
import Prelude
import qualified Data.ByteString.Lazy as B
keysChar, keysNum, keysPunc, keysCharNum, keysAll, keysHex :: String
keysChar = ['a'..'z'] ++ ['A'..'Z']
keysHex = ['a'..'f']
keysNum = ['0'..'9']
keysPunc = "`~!@#$%^&*()-_=+[{]}\\|;:'\",<.>/? "
keysCharNum = keysChar ++ keysNum
keysAll = keysChar ++ keysNum ++ keysPunc
giveKey :: String -> Char -> Int -> Char
giveKey keysCustom c n = extractChar $ case c of
'i' -> (keysNum ++ keysHex)
'j' -> keysNum
'k' -> keysChar
'l' -> keysCharNum
';' -> keysPunc
'h' -> (keysCharNum ++ keysCustom)
'\n' -> ['\n']
_ -> keysAll
where
extractChar xs = xs!!mod n (length xs)
showRandomKey :: Int -> String -> StateT AESRNG IO ()
showRandomKey len keysCustom = handleKey =<< liftIO getChar
where
handleKey key = case key of
'\n' -> liftIO (putChar '\n') >> showRandomKey len keysCustom
'q' -> (liftIO $ putStrLn "\nBye!") >> return ()
_ -> mapM_ f [0..len] >> (liftIO $ putStrLn []) >> showRandomKey len keysCustom
where
f _ = liftIO
. putChar
. giveKey keysCustom key
. (\n -> mod n (length (keysAll ++ keysCustom) - 1))
=<< aesRandomInt
aesRandomInt :: StateT AESRNG IO Int
aesRandomInt = do
aesState <- get
-- aesState <- liftIO makeSystem
-- let aesState = 128
let (bs, aesState') = cprgGenerate 64 aesState
put aesState'
return (decode $ B.fromChunks [bs])
gargPass :: IO (Int, AESRNG)
gargPass = do
-- let as = ["alphanumeric","punctuation"]
-- let as' = filter (\c -> elem c keysAll) . nub $ unwords as
aesState <- makeSystem -- gather entropy from the system to use as the initial seed
--_ <- runStateT (showRandomKey len as') aesState -- enter loop
-- return ()
pass <- runStateT aesRandomInt aesState -- enter loop
pure pass
{-
main :: IO ()
main = do
hSetBuffering stdin NoBuffering -- disable buffering from STDIN
hSetBuffering stdout NoBuffering -- disable buffering from STDOUT
hSetEcho stdin False -- disable terminal echo
as <- getArgs
let as' = filter (\c -> elem c keysAll) . nub $ unwords as
mapM_ putStrLn
[ []
, "poke: 'q' quit"
, " 'j' number"
, " 'k' letter"
, " 'l' alphanumeric"
, " ';' punctuation"
, " 'h' alphanumeric" ++ (if null as' then [] else " + " ++ as')
, " 'i' hexadecimal"
, " 'ENTER' newline"
, " else any"
, []
]
aesState <- makeSystem -- gather entropy from the system to use as the initial seed
_ <- runStateT (showRandomKey as') aesState -- enter loop
return ()
-}
{-|
Module : Gargantext.Core.Mail
Description :
Copyright : (c) CNRS, 2017-Present
License : AGPL + CECILL v3
Maintainer : team@gargantext.org
Stability : experimental
Portability : POSIX
-}
{-# LANGUAGE OverloadedStrings #-}
module Gargantext.Core.Mail
(gargMail)
where
import Data.Maybe
import Network.Mail.SMTP hiding (htmlPart)
import Gargantext.Prelude
import Network.Mail.Mime (plainPart)
-- | TODO add parameters
gargMail :: IO ()
gargMail = sendMail "localhost" mail
where
mail = simpleMail from to cc bcc subject [body]
from = Address (Just "François Rabelais") "francois.rabelais@gargantext.org"
to = [Address (Just "Anoe") "alexandre@localhost"]
cc = []
bcc = []
subject = "email subject"
body = plainPart "email body"
......@@ -16,12 +16,40 @@ Gargantext's database.
module Gargantext.Database ( module Gargantext.Database.Prelude
, module Gargantext.Database.Schema.NodeNode
, insertDB
-- , module Gargantext.Database.Bashql
)
where
import Gargantext.Database.Prelude (connectGargandb)
-- import Gargantext.Database.Bashql
import Gargantext.Prelude
import Gargantext.Database.Prelude -- (connectGargandb)
-- import Gargantext.Database.Schema.Node
-- import Gargantext.Database.Query.Table.Node
import Gargantext.Database.Schema.NodeNode -- (NodeNode(..))
import Gargantext.Database.Query.Table.NodeNode
class InsertDB a where
insertDB :: a -> Cmd err Int
{-
class DeleteDB a where
deleteDB :: a -> Cmd err Int
-}
instance InsertDB [NodeNode] where
insertDB = insertNodeNode
{-
instance InsertDB [Node a] where
insertDB = insertNodes'
instance InsertDB [NodeNodeNgram] where
insertDB = ...
-}
......@@ -27,6 +27,7 @@ module Gargantext.Database.Action.Flow -- (flowDatabase, ngrams2list)
( FlowCmdM
, getDataText
, flowDataText
, flow
, flowCorpusFile
, flowCorpus
......@@ -68,7 +69,7 @@ import Gargantext.Database.Action.Flow.Utils (insertDocNgrams)
import Gargantext.Database.Query.Table.Node
import Gargantext.Database.Query.Table.Node.Document.Insert -- (insertDocuments, ReturnId(..), addUniqIdsDoc, addUniqIdsContact, ToDbData(..))
import Gargantext.Database.Query.Tree.Root (getOrMkRoot, getOrMk_RootWithCorpus)
import Gargantext.Database.Action.Search (searchInDatabase)
import Gargantext.Database.Action.Search (searchDocInDatabase)
import Gargantext.Database.Admin.Config (userMaster, corpusMasterName)
import Gargantext.Database.Query.Table.Node.Error (HasNodeError(..))
import Gargantext.Database.Admin.Types.Hyperdata
......@@ -106,11 +107,9 @@ allDataOrigins = map InternalOrigin API.externalAPIs
<> map ExternalOrigin API.externalAPIs
---------------
data DataText = DataOld ![NodeId]
| DataNew ![[HyperdataDocument]]
-- TODO use the split parameter in config file
getDataText :: FlowCmdM env err m
=> DataOrigin
......@@ -126,7 +125,7 @@ getDataText (InternalOrigin _) _la q _li = do
(UserName userMaster)
(Left "")
(Nothing :: Maybe HyperdataCorpus)
ids <- map fst <$> searchInDatabase cId (stemIt q)
ids <- map fst <$> searchDocInDatabase cId (stemIt q)
pure $ DataOld ids
-------------------------------------------------------------------------------
......
......@@ -34,9 +34,9 @@ data FavOrTrash = IsFav | IsTrash
moreLike :: CorpusId -> Maybe Offset -> Maybe Limit -> Maybe OrderBy
-> FavOrTrash -> Cmd err [FacetDoc]
moreLike cId o l order ft = do
moreLike cId o _l order ft = do
priors <- getPriors ft cId
moreLikeWith cId o l order ft priors
moreLikeWith cId o (Just 3) order ft priors
---------------------------------------------------------------------------
getPriors :: FavOrTrash -> CorpusId -> Cmd err (Events Bool)
......
......@@ -28,7 +28,7 @@ import Gargantext.Database.Query.Table.Node
import Gargantext.Database.Query.Table.Node.Error
import Gargantext.Database.Query.Table.Node.UpdateOpaleye (updateHyperdata)
import Gargantext.Prelude
import Gargantext.Prelude.Utils (hash)
import Gargantext.Core.Crypto.Hash (hash)
import Gargantext.Database.Prelude
import Control.Lens (view)
import Gargantext.Config (GargConfig(..))
......
This diff is collapsed.
......@@ -14,6 +14,7 @@ module Gargantext.Database.Action.Share
where
import Control.Lens (view)
import Gargantext.Database
import Gargantext.Core.Types.Individu (User(..))
import Gargantext.Database.Action.Flow.Utils (getUserId)
import Gargantext.Database.Admin.Config (hasNodeType, isInNodeTypes)
......@@ -23,7 +24,7 @@ import Gargantext.Database.Admin.Types.Node -- (NodeType(..))
import Gargantext.Database.Prelude (Cmd)
import Gargantext.Database.Query.Table.Node (getNode, getNodesWith)
import Gargantext.Database.Query.Table.Node.Error (HasNodeError, errorWith)
import Gargantext.Database.Query.Table.NodeNode (insertNodeNode, deleteNodeNode)
import Gargantext.Database.Query.Table.NodeNode (deleteNodeNode)
import Gargantext.Database.Query.Tree.Root (getRootId)
import Gargantext.Database.Schema.Node
import Gargantext.Database.Schema.NodeNode (NodeNodePoly(..))
......@@ -45,7 +46,7 @@ data ShareNodeWith = ShareNodeWith_User { snwu_nodetype :: NodeType
shareNodeWith :: HasNodeError err
=> ShareNodeWith
-> NodeId
-> Cmd err Int64
-> Cmd err Int
shareNodeWith (ShareNodeWith_User NodeFolderShared u) n = do
nodeToCheck <- getNode n
userIdCheck <- getUserId u
......@@ -56,7 +57,7 @@ shareNodeWith (ShareNodeWith_User NodeFolderShared u) n = do
then errorWith "[G.D.A.S.shareNodeWith] Can share to others only"
else do
folderSharedId <- getFolderId u NodeFolderShared
insertNodeNode [NodeNode folderSharedId n Nothing Nothing]
insertDB ([NodeNode folderSharedId n Nothing Nothing]:: [NodeNode])
shareNodeWith (ShareNodeWith_Node NodeFolderPublic nId) n = do
nodeToCheck <- getNode n
......@@ -66,7 +67,7 @@ shareNodeWith (ShareNodeWith_Node NodeFolderPublic nId) n = do
else do
folderToCheck <- getNode nId
if hasNodeType folderToCheck NodeFolderPublic
then insertNodeNode [NodeNode nId n Nothing Nothing]
then insertDB ([NodeNode nId n Nothing Nothing] :: [NodeNode])
else errorWith "[G.D.A.S.shareNodeWith] Can share NodeWith NodeFolderPublic only"
shareNodeWith _ _ = errorWith "[G.D.A.S.shareNodeWith] Not implemented for this NodeType"
......
......@@ -54,6 +54,15 @@ defaultHyperdataContact = HyperdataContact (Just "bdd")
(Just "DO NOT expose this")
(Just "DO NOT expose this")
hyperdataContact :: FirstName -> LastName -> HyperdataContact
hyperdataContact fn ln = HyperdataContact Nothing
(Just (contactWho fn ln))
[]
Nothing
Nothing
Nothing
Nothing
Nothing
-- TOD0 contact metadata (Type is too flat)
data ContactMetaData =
......@@ -78,12 +87,20 @@ data ContactWho =
, _cw_freetags :: [Text]
} deriving (Eq, Show, Generic)
type FirstName = Text
type LastName = Text
defaultContactWho :: ContactWho
defaultContactWho = ContactWho (Just "123123")
(Just "First Name")
(Just "Last Name")
["keyword A"]
["freetag A"]
defaultContactWho = contactWho "Pierre" "Dupont"
contactWho :: FirstName -> LastName -> ContactWho
contactWho fn ln = ContactWho Nothing
(Just fn)
(Just ln)
[]
[]
data ContactWhere =
ContactWhere { _cw_organization :: [Text]
......@@ -150,6 +167,12 @@ instance FromField HyperdataContact where
instance QueryRunnerColumnDefault PGJsonb HyperdataContact where
queryRunnerColumnDefault = fieldQueryRunnerColumn
instance QueryRunnerColumnDefault (Nullable PGJsonb) HyperdataContact where
queryRunnerColumnDefault = fieldQueryRunnerColumn
-- | All lenses
makeLenses ''ContactWho
makeLenses ''ContactWhere
......
......@@ -48,7 +48,7 @@ import GHC.Generics (Generic)
import Gargantext.Core.Utils.Prefix (unPrefix, unPrefixSwagger, wellNamedSchema)
import Gargantext.Database.Prelude (fromField')
import Gargantext.Prelude
import Opaleye (QueryRunnerColumnDefault, queryRunnerColumnDefault, PGJsonb, fieldQueryRunnerColumn)
import Opaleye (QueryRunnerColumnDefault, queryRunnerColumnDefault, PGJsonb, fieldQueryRunnerColumn, Nullable)
import Test.QuickCheck (elements)
import Test.QuickCheck.Arbitrary
......
......@@ -157,6 +157,7 @@ instance Arbitrary NodeId where
type ParentId = NodeId
type CorpusId = NodeId
type CommunityId = NodeId
type ListId = NodeId
type DocumentId = NodeId
type DocId = NodeId
......@@ -241,6 +242,8 @@ data NodeType = NodeUser
| NodeFolderPublic
| NodeFolder
-- | NodeAnalysis | NodeCommunity
| NodeCorpus | NodeCorpusV3 | NodeTexts | NodeDocument
| NodeAnnuaire | NodeContact
| NodeGraph | NodePhylo
......@@ -336,4 +339,7 @@ instance QueryRunnerColumnDefault (Nullable PGInt4) NodeId
where
queryRunnerColumnDefault = fieldQueryRunnerColumn
instance (QueryRunnerColumnDefault (Nullable O.PGTimestamptz) UTCTime)
where
queryRunnerColumnDefault = fieldQueryRunnerColumn
......@@ -45,7 +45,6 @@ import qualified Data.List as DL
import qualified Database.PostgreSQL.Simple as PGS
-------------------------------------------------------
class HasConnectionPool env where
connPool :: Getter env (Pool Connection)
......
......@@ -29,6 +29,8 @@ module Gargantext.Database.Query.Facet
, FacetDocRead
, FacetPaired(..)
, FacetPairedRead
, FacetPairedReadNull
, FacetPairedReadNullAgg
, OrderBy(..)
)
where
......@@ -111,44 +113,61 @@ instance (Typeable i, Typeable l, ToSchema i, ToSchema l) => ToSchema (Pair i l)
instance (Arbitrary i, Arbitrary l) => Arbitrary (Pair i l) where
arbitrary = Pair <$> arbitrary <*> arbitrary
data FacetPaired id date hyperdata score pair =
data FacetPaired id date hyperdata score =
FacetPaired {_fp_id :: id
,_fp_date :: date
,_fp_hyperdata :: hyperdata
,_fp_score :: score
,_fp_pair :: pair
} deriving (Show, Generic)
$(deriveJSON (unPrefix "_fp_") ''FacetPaired)
$(makeAdaptorAndInstance "pFacetPaired" ''FacetPaired)
instance ( ToSchema id
, ToSchema date
, ToSchema hyperdata
, ToSchema score
, ToSchema pair
, Typeable id
, Typeable date
, Typeable hyperdata
, Typeable score
, Typeable pair
) => ToSchema (FacetPaired id date hyperdata score pair) where
) => ToSchema (FacetPaired id date hyperdata score) where
declareNamedSchema = wellNamedSchema "_fp_"
instance ( Arbitrary id
, Arbitrary date
, Arbitrary hyperdata
, Arbitrary score
, Arbitrary pair
) => Arbitrary (FacetPaired id date hyperdata score pair) where
arbitrary = FacetPaired <$> arbitrary <*> arbitrary <*> arbitrary <*> arbitrary <*> arbitrary
) => Arbitrary (FacetPaired id date hyperdata score) where
arbitrary = FacetPaired <$> arbitrary <*> arbitrary <*> arbitrary <*> arbitrary
type FacetPairedRead = FacetPaired (Column PGInt4 )
(Column PGTimestamptz)
(Column PGJsonb )
(Column PGInt4 )
( Column (Nullable PGInt4)
, Column (Nullable PGText)
type FacetPairedReadNull = FacetPaired (Column (Nullable PGInt4) )
(Column (Nullable PGTimestamptz))
(Column (Nullable PGJsonb) )
(Column (Nullable PGInt4) )
type FacetPairedReadNullAgg = FacetPaired (Aggregator (Column (Nullable PGInt4) )
(Column (Nullable PGInt4) )
)
(Aggregator (Column (Nullable PGTimestamptz))
(Column (Nullable PGTimestamptz))
)
(Aggregator (Column (Nullable PGJsonb) )
(Column (Nullable PGJsonb) )
)
(Aggregator (Column (Nullable PGInt4) )
(Column (Nullable PGInt4) )
)
-- | JSON instance
$(deriveJSON (unPrefix "facetDoc_") ''Facet)
......
......@@ -22,7 +22,15 @@ Multiple Join functions with Opaleye.
------------------------------------------------------------------------
module Gargantext.Database.Query.Join
module Gargantext.Database.Query.Join ( leftJoin2
, leftJoin3
, leftJoin4
, leftJoin5
, leftJoin6
, leftJoin7
, leftJoin8
, leftJoin9
)
where
import Control.Applicative ((<*>))
......@@ -33,17 +41,24 @@ import Opaleye
import Opaleye.Internal.Join (NullMaker(..))
import qualified Opaleye.Internal.Unpackspec()
--leftJoin3 :: Query columnsL1 -> Query columnsR -> Query columnsL
-- -> ((columnsL1, columnsR) -> Column PGBool)
-- -> ((columnsL, (columnsL1, nullableColumnsR1)) -> Column PGBool)
-- -> Query (columnsL, nullableColumnsR)
--leftJoin3 q1 q2 q3 cond12 cond23 = leftJoin q3 (leftJoin q1 q2 cond12) cond23
join3 :: Query columnsA -> Query columnsB -> Query columnsC
------------------------------------------------------------------------
leftJoin2 :: (Default Unpackspec fieldsL fieldsL,
Default Unpackspec fieldsR fieldsR,
Default NullMaker fieldsR nullableFieldsR) =>
Select fieldsL
-> Select fieldsR
-> ((fieldsL, fieldsR) -> Column PGBool)
-> Select (fieldsL, nullableFieldsR)
leftJoin2 = leftJoin
------------------------------------------------------------------------
-- | LeftJoin3 in two ways to write it
_leftJoin3 :: Query columnsA -> Query columnsB -> Query columnsC
-> ((columnsA, columnsB, columnsC) -> Column PGBool)
-> Query (columnsA, columnsB, columnsC)
join3 q1 q2 q3 cond = ((,,) <$> q1 <*> q2 <*> q3) >>> keepWhen cond
_leftJoin3 q1 q2 q3 cond = ((,,) <$> q1 <*> q2 <*> q3) >>> keepWhen cond
------------------------------------------------------------------------
leftJoin3
:: (Default Unpackspec fieldsL1 fieldsL1,
Default Unpackspec fieldsL2 fieldsL2,
......
{-|
Module : Gargantext.Database.Query.Prelude
Description :
Copyright : (c) CNRS, 2017-Present
License : AGPL + CECILL v3
Maintainer : team@gargantext.org
Stability : experimental
Portability : POSIX
-}
------------------------------------------------------------------------
module Gargantext.Database.Query.Prelude
( module Gargantext.Database.Query.Join
, module Gargantext.Database.Query.Table.Node
, module Gargantext.Database.Query.Table.NodeNode
, module Control.Arrow
)
where
import Control.Arrow (returnA)
import Gargantext.Database.Query.Join
import Gargantext.Database.Query.Table.Node
import Gargantext.Database.Query.Table.NodeNode
......@@ -59,7 +59,7 @@ selectNgramsByDoc lIds dId nt = runOpaQuery (query lIds dId nt)
_postNgrams :: CorpusId -> DocId -> [Text] -> Cmd err Int
_postNgrams = undefined
_dbGetNgramsDb :: Cmd err [NgramsDb]
_dbGetNgramsDb :: Cmd err [NgramsDB]
_dbGetNgramsDb = runOpaQuery queryNgramsTable
......
......@@ -190,6 +190,23 @@ node nodeType name hyperData parentId userId =
insertNodes :: [NodeWrite] -> Cmd err Int64
insertNodes ns = mkCmd $ \conn -> runInsert_ conn $ Insert nodeTable ns rCount Nothing
{-
insertNodes' :: [Node a] -> Cmd err Int64
insertNodes' ns = mkCmd $ \conn -> runInsert_ conn
$ Insert nodeTable ns' rCount Nothing
where
ns' :: [NodeWrite]
ns' = map (\(Node i t u p n d h)
-> Node (pgNodeId <$> i)
(pgInt4 $ nodeTypeId t)
(pgInt4 u)
(pgNodeId <$> p)
(pgStrictText n)
(pgUTCTime <$> d)
(pgJSONB $ cs $ encode h)
) ns
-}
insertNodesR :: [NodeWrite] -> Cmd err [NodeId]
insertNodesR ns = mkCmd $ \conn ->
runInsert_ conn (Insert nodeTable ns (rReturning (\(Node i _ _ _ _ _ _) -> i)) Nothing)
......
......@@ -30,10 +30,13 @@ import Gargantext.Database.Schema.Node
import Opaleye
import Protolude
-- TODO getAllTableDocuments
getAllDocuments :: ParentId -> Cmd err (TableResult (Node HyperdataDocument))
getAllDocuments pId = getAllChildren pId (Proxy :: Proxy HyperdataDocument)
(Just NodeDocument)
-- TODO getAllTableContacts
getAllContacts :: ParentId -> Cmd err (TableResult (Node HyperdataContact))
getAllContacts pId = getAllChildren pId (Proxy :: Proxy HyperdataContact)
(Just NodeContact)
......
......@@ -72,7 +72,7 @@ import Gargantext.Database.Admin.Types.Hyperdata
import Gargantext.Database.Admin.Types.Node
import Gargantext.Database.Prelude (Cmd, runPGSQuery)
import Gargantext.Prelude
import Gargantext.Prelude.Utils (hash)
import Gargantext.Core.Crypto.Hash (hash)
import qualified Data.Text as DT (pack, concat, take)
-- TODO : the import of Document constructor below does not work
......
......@@ -71,9 +71,33 @@ getNodeNode n = runOpaQuery (selectNodeNode $ pgNodeId n)
returnA -< ns
------------------------------------------------------------------------
insertNodeNode :: [NodeNode] -> Cmd err Int64
insertNodeNode ns = mkCmd $ \conn -> runInsert_ conn
$ Insert nodeNodeTable ns' rCount Nothing
-- TODO (refactor with Children)
{-
getNodeNodeWith :: NodeId -> proxy a -> Maybe NodeType -> Cmd err [a]
getNodeNodeWith pId _ maybeNodeType = runOpaQuery query
where
query = selectChildren pId maybeNodeType
selectChildren :: ParentId
-> Maybe NodeType
-> Query NodeRead
selectChildren parentId maybeNodeType = proc () -> do
row@(Node nId typeName _ parent_id _ _ _) <- queryNodeTable -< ()
(NodeNode _ n1id n2id _ _) <- queryNodeNodeTable -< ()
let nodeType = maybe 0 nodeTypeId maybeNodeType
restrict -< typeName .== pgInt4 nodeType
restrict -< (.||) (parent_id .== (pgNodeId parentId))
( (.&&) (n1id .== pgNodeId parentId)
(n2id .== nId))
returnA -< row
-}
------------------------------------------------------------------------
insertNodeNode :: [NodeNode] -> Cmd err Int
insertNodeNode ns = mkCmd $ \conn -> fromIntegral <$> (runInsert_ conn
$ Insert nodeNodeTable ns' rCount (Just DoNothing))
where
ns' :: [NodeNodeWrite]
ns' = map (\(NodeNode n1 n2 x y)
......@@ -83,6 +107,8 @@ insertNodeNode ns = mkCmd $ \conn -> runInsert_ conn
(pgInt4 <$> y)
) ns
------------------------------------------------------------------------
type Node1_Id = NodeId
type Node2_Id = NodeId
......
......@@ -42,7 +42,7 @@ type NgramsId = Int
type NgramsTerms = Text
type Size = Int
data NgramsPoly id terms n = NgramsDb { _ngrams_id :: !id
data NgramsPoly id terms n = NgramsDB { _ngrams_id :: !id
, _ngrams_terms :: !terms
, _ngrams_n :: !n
} deriving (Show)
......@@ -59,14 +59,14 @@ type NgramsReadNull = NgramsPoly (Column (Nullable PGInt4))
(Column (Nullable PGText))
(Column (Nullable PGInt4))
type NgramsDb = NgramsPoly Int Text Int
type NgramsDB = NgramsPoly Int Text Int
$(makeAdaptorAndInstance "pNgramsDb" ''NgramsPoly)
makeLenses ''NgramsPoly
ngramsTable :: Table NgramsWrite NgramsRead
ngramsTable = Table "ngrams" (pNgramsDb NgramsDb { _ngrams_id = optional "id"
ngramsTable = Table "ngrams" (pNgramsDb NgramsDB { _ngrams_id = optional "id"
, _ngrams_terms = required "terms"
, _ngrams_n = required "n"
}
......
......@@ -25,7 +25,6 @@ import Prelude hiding (null, id, map, sum)
------------------------------------------------------------------------
-- Main polymorphic Node definition
data NodePoly id
typename
userId
......@@ -53,7 +52,6 @@ $(makeLenses ''NodePoly)
$(makeAdaptorAndInstance "pNode" ''NodePoly)
$(makeLensesWith abbreviatedFields ''NodePoly)
------------------------------------------------------------------------
nodeTable :: Table NodeWrite NodeRead
nodeTable = Table "nodes" (pNode Node { _node_id = optional "id"
, _node_typename = required "typename"
......
......@@ -54,7 +54,9 @@ $(makeAdaptorAndInstance "pNodeNode" ''NodeNodePoly)
makeLenses ''NodeNodePoly
nodeNodeTable :: Table NodeNodeWrite NodeNodeRead
nodeNodeTable = Table "nodes_nodes" (pNodeNode
nodeNodeTable =
Table "nodes_nodes"
( pNodeNode
NodeNode { _nn_node1_id = required "node1_id"
, _nn_node2_id = required "node2_id"
, _nn_score = optional "score"
......@@ -62,8 +64,6 @@ nodeNodeTable = Table "nodes_nodes" (pNodeNode
}
)
instance QueryRunnerColumnDefault (Nullable PGInt4) Int where
queryRunnerColumnDefault = fieldQueryRunnerColumn
......
......@@ -37,7 +37,7 @@ import Data.Profunctor.Product.TH (makeAdaptorAndInstance)
import Data.Swagger hiding (required, in_)
import GHC.Generics (Generic)
import Gargantext.Core.Utils.Prefix (unPrefix, unPrefixSwagger)
import Opaleye hiding (FromField)
import Opaleye hiding (FromField, readOnly)
import Opaleye.Internal.QueryArr (Query)
import Test.QuickCheck.Arbitrary
......
......@@ -292,9 +292,6 @@ deviation = sqrt . variance
movingAverage :: (Eq b, Fractional b) => Int -> [b] -> [b]
movingAverage steps xs = map mean $ chunkAlong steps 1 xs
ma :: [Double] -> [Double]
ma = movingAverage 3
-----------------------------------------------------------------------
-----------------------------------------------------------------------
--- Map in Map = Map2
......
......@@ -14,9 +14,6 @@ Portability : POSIX
module Gargantext.Prelude.Utils
where
import Prelude (String)
import Data.Set (Set)
import Data.List (foldl)
import Control.Lens (view)
import Control.Monad.Random.Class (MonadRandom)
import Control.Monad.Reader (MonadReader)
......@@ -26,11 +23,9 @@ import GHC.IO (FilePath)
import Gargantext.API.Admin.Settings
import Gargantext.Database.Admin.Types.Node (NodeId, NodeType)
import Gargantext.Prelude
import Gargantext.Core.Crypto.Hash
import System.Directory (createDirectoryIfMissing)
import System.Random (newStdGen)
import qualified Data.ByteString.Lazy.Char8 as Char
import qualified Data.Digest.Pure.SHA as SHA (sha256, showDigest)
import qualified Data.Set as Set
import qualified Data.Text as Text
import qualified System.Random.Shuffle as SRS
......@@ -38,34 +33,6 @@ import qualified System.Random.Shuffle as SRS
shuffle :: MonadRandom m => [a] -> m [a]
shuffle ns = SRS.shuffleM ns
--------------------------------------------------------------------------
-- | Use this datatype to keep traceability of hashes
-- TODO use newtype
type Hash = Text
-- | Class to make hashes
class IsHashable a where
hash :: a -> Hash
-- | Main API to hash text
-- using sha256 for now
instance IsHashable Char.ByteString where
hash = Text.pack
. SHA.showDigest
. SHA.sha256
instance {-# OVERLAPPING #-} IsHashable String where
hash = hash . Char.pack
instance IsHashable Text where
hash = hash . Text.unpack
instance IsHashable (Set Hash) where
hash = hash . foldl (<>) "" . Set.toList
instance {-# OVERLAPPABLE #-} IsHashable a => IsHashable [a] where
hash = hash . Set.fromList . map hash
--------------------------------------------------------------------------
data NodeToHash = NodeToHash { nodeType :: NodeType
, nodeId :: NodeId
......
......@@ -53,7 +53,7 @@ extra-deps:
- git: https://github.com/np/patches-map
commit: 8c6f38c4844ead53e664cf9c82ba461715dbe445
- git: https://github.com/delanoe/haskell-opaleye.git #- opaleye-0.6.7002.0
commit: 53385de076be09f728a1b58c035a18e9ff9bcfd6
commit: 63ee65d974e9d20eaaf17a2e83652175988cbb79
- git: https://github.com/delanoe/hsparql.git
commit: 308c74b71a1abb0a91546fa57d353131248e3a7f
- Unique-0.4.7.6@sha256:a1ff411f4d68c756e01e8d532fbe8e57f1ac77f2cc0ee8a999770be2bca185c5,2723
......@@ -85,5 +85,5 @@ extra-deps:
- ghc-byteorder-4.11.0.0.10@sha256:5ee4a907279bfec27b0f9de7b8fba4cecfd34395a0235a7784494de70ad4e98f,1535
# Matrix Computation
- accelerate-1.2.0.1
- smtp-mail-0.2.0.0@sha256:b91c81f6dbb41a9ceee8c443385118684ecec55006b77f7d3c0e49cffd2468cf,1211
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment