Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
haskell-gargantext
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
160
Issues
160
List
Board
Labels
Milestones
Merge Requests
14
Merge Requests
14
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
gargantext
haskell-gargantext
Commits
3c47008a
Verified
Commit
3c47008a
authored
Nov 09, 2023
by
Przemyslaw Kaminski
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
[EPO] patents works now
parent
1f30a608
Pipeline
#5347
failed with stages
in 34 seconds
Changes
9
Pipelines
1
Hide whitespace changes
Inline
Side-by-side
Showing
9 changed files
with
205 additions
and
12 deletions
+205
-12
gargantext.cabal
gargantext.cabal
+2
-0
GraphQL.hs
src/Gargantext/API/GraphQL.hs
+4
-0
User.hs
src/Gargantext/API/GraphQL/User.hs
+26
-0
New.hs
src/Gargantext/API/Node/Corpus/New.hs
+11
-2
API.hs
src/Gargantext/Core/Text/Corpus/API.hs
+3
-3
EPO.hs
src/Gargantext/Core/Text/Corpus/API/EPO.hs
+124
-0
Flow.hs
src/Gargantext/Database/Action/Flow.hs
+6
-4
User.hs
src/Gargantext/Database/Admin/Types/Hyperdata/User.hs
+11
-2
User.hs
src/Gargantext/Database/Query/Table/User.hs
+18
-1
No files found.
gargantext.cabal
View file @
3c47008a
...
...
@@ -79,6 +79,7 @@ library
Gargantext.Core.Text.Context
Gargantext.Core.Text.Corpus.API
Gargantext.Core.Text.Corpus.API.Arxiv
Gargantext.Core.Text.Corpus.API.EPO
Gargantext.Core.Text.Corpus.API.Pubmed
Gargantext.Core.Text.Corpus.API.OpenAlex
Gargantext.Core.Text.Corpus.Query
...
...
@@ -523,6 +524,7 @@ library
, servant-blaze ^>= 0.9.1
, servant-cassava ^>= 0.10.1
, servant-client ^>= 0.18.3
, servant-client-core ^>= 0.18.3
, servant-ekg ^>= 0.3.1
, servant-flatten ^>= 0.2
, servant-job >= 0.2.0.0
...
...
src/Gargantext/API/GraphQL.hs
View file @
3c47008a
...
...
@@ -82,6 +82,8 @@ data Mutation m
=
Mutation
{
update_user_info
::
GQLUserInfo
.
UserInfoMArgs
->
m
Int
,
update_user_pubmed_api_key
::
GQLUser
.
UserPubmedAPIKeyMArgs
->
m
Int
,
update_user_epo_api_user
::
GQLUser
.
UserEPOAPIUserMArgs
->
m
Int
,
update_user_epo_api_token
::
GQLUser
.
UserEPOAPITokenMArgs
->
m
Int
,
delete_team_membership
::
GQLTeam
.
TeamDeleteMArgs
->
m
[
Int
]
,
update_node_context_category
::
GQLCTX
.
NodeContextCategoryMArgs
->
m
[
Int
]
}
deriving
(
Generic
,
GQLType
)
...
...
@@ -127,6 +129,8 @@ rootResolver authenticatedUser policyManager =
,
tree_branch
=
GQLTree
.
resolveBreadcrumb
}
,
mutationResolver
=
Mutation
{
update_user_info
=
GQLUserInfo
.
updateUserInfo
,
update_user_pubmed_api_key
=
GQLUser
.
updateUserPubmedAPIKey
,
update_user_epo_api_user
=
GQLUser
.
updateUserEPOAPIUser
,
update_user_epo_api_token
=
GQLUser
.
updateUserEPOAPIToken
,
delete_team_membership
=
GQLTeam
.
deleteTeamMembership
,
update_node_context_category
=
GQLCTX
.
updateNodeContextCategory
}
,
subscriptionResolver
=
Undefined
}
...
...
src/Gargantext/API/GraphQL/User.hs
View file @
3c47008a
...
...
@@ -47,6 +47,18 @@ data UserPubmedAPIKeyMArgs
,
api_key
::
Text
}
deriving
(
Generic
,
GQLType
)
data
UserEPOAPIUserMArgs
=
UserEPOAPIUserMArgs
{
user_id
::
Int
,
api_user
::
Text
}
deriving
(
Generic
,
GQLType
)
data
UserEPOAPITokenMArgs
=
UserEPOAPITokenMArgs
{
user_id
::
Int
,
api_token
::
Text
}
deriving
(
Generic
,
GQLType
)
-- | Function to resolve user from a query.
resolveUsers
::
(
CmdCommon
env
)
...
...
@@ -82,3 +94,17 @@ updateUserPubmedAPIKey UserPubmedAPIKeyMArgs { user_id, api_key } = do
_
<-
lift
$
DBUser
.
updateUserPubmedAPIKey
(
Individu
.
RootId
$
UnsafeMkNodeId
user_id
)
api_key
pure
1
updateUserEPOAPIUser
::
(
CmdCommon
env
,
HasSettings
env
)
=>
UserEPOAPIUserMArgs
->
GqlM'
e
env
Int
updateUserEPOAPIUser
UserEPOAPIUserMArgs
{
user_id
,
api_user
}
=
do
_
<-
lift
$
DBUser
.
updateUserEPOAPIUser
(
Individu
.
RootId
$
UnsafeMkNodeId
user_id
)
api_user
pure
1
updateUserEPOAPIToken
::
(
CmdCommon
env
,
HasSettings
env
)
=>
UserEPOAPITokenMArgs
->
GqlM'
e
env
Int
updateUserEPOAPIToken
UserEPOAPITokenMArgs
{
user_id
,
api_token
}
=
do
_
<-
lift
$
DBUser
.
updateUserEPOAPIToken
(
Individu
.
RootId
$
UnsafeMkNodeId
user_id
)
api_token
pure
1
src/Gargantext/API/Node/Corpus/New.hs
View file @
3c47008a
...
...
@@ -31,6 +31,7 @@ import Data.Conduit.Internal (zipSources)
import
Data.Swagger
import
Data.Text
qualified
as
T
import
Data.Text.Encoding
qualified
as
TE
import
EPO.API.Client.Types
qualified
as
EPO
import
Gargantext.API.Admin.Orchestrator.Types
(
JobLog
(
..
),
AsyncJobs
)
import
Gargantext.API.Admin.Types
(
HasSettings
)
import
Gargantext.API.Ngrams
(
commitStatePatch
,
Versioned
(
..
))
...
...
@@ -144,6 +145,8 @@ data WithQuery = WithQuery
,
_wq_node_id
::
!
Int
,
_wq_flowListWith
::
!
FlowSocialListWith
,
_wq_pubmedAPIKey
::
!
(
Maybe
Text
)
,
_wq_epoAPIUser
::
!
(
Maybe
Text
)
,
_wq_epoAPIToken
::
!
(
Maybe
Text
)
}
deriving
(
Show
,
Eq
,
Generic
)
...
...
@@ -163,6 +166,8 @@ instance Arbitrary WithQuery where
<*>
arbitrary
<*>
arbitrary
<*>
arbitrary
<*>
arbitrary
<*>
arbitrary
------------------------------------------------------------------------
...
...
@@ -204,12 +209,16 @@ addToCorpusWithQuery user cid (WithQuery { _wq_query = q
,
_wq_datafield
=
datafield
,
_wq_lang
=
l
,
_wq_flowListWith
=
flw
,
_wq_pubmedAPIKey
=
mPubmedAPIKey
})
maybeLimit
jobHandle
=
do
,
_wq_pubmedAPIKey
=
mPubmedAPIKey
,
..
})
maybeLimit
jobHandle
=
do
-- TODO ...
$
(
logLocM
)
DEBUG
$
T
.
pack
$
"(cid, dbs) "
<>
show
(
cid
,
dbs
)
$
(
logLocM
)
DEBUG
$
T
.
pack
$
"datafield "
<>
show
datafield
$
(
logLocM
)
DEBUG
$
T
.
pack
$
"flowListWith "
<>
show
flw
let
mEPOAuthKey
=
EPO
.
AuthKey
<$>
(
EPO
.
User
<$>
_wq_epoAPIUser
)
<*>
(
EPO
.
Token
<$>
_wq_epoAPIToken
)
addLanguageToCorpus
cid
l
case
datafield
of
...
...
@@ -233,7 +242,7 @@ addToCorpusWithQuery user cid (WithQuery { _wq_query = q
let
db
=
database2origin
dbs
-- mPubmedAPIKey <- getUserPubmedAPIKey user
-- printDebug "[addToCorpusWithQuery] mPubmedAPIKey" mPubmedAPIKey
eTxt
<-
getDataText
db
(
Multi
l
)
q
mPubmedAPIKey
maybeLimit
eTxt
<-
getDataText
db
(
Multi
l
)
q
mPubmedAPIKey
m
EPOAuthKey
m
aybeLimit
-- printDebug "[G.A.N.C.New] lTxts" lTxts
case
eTxt
of
...
...
src/Gargantext/Core/Text/Corpus/API.hs
View file @
3c47008a
...
...
@@ -23,7 +23,7 @@ import Control.Monad.Except
import
Data.Text
qualified
as
T
import
EPO.API.Client.Types
qualified
as
EPO
import
Gargantext.API.Admin.Orchestrator.Types
(
ExternalAPIs
(
..
),
externalAPIs
)
import
Gargantext.Core
(
Lang
(
..
),
toISO639
)
import
Gargantext.Core
(
Lang
(
..
),
toISO639
,
toISO639EN
)
import
Gargantext.Core.Text.Corpus.API.Arxiv
qualified
as
Arxiv
import
Gargantext.Core.Text.Corpus.API.EPO
qualified
as
EPO
import
Gargantext.Core.Text.Corpus.API.Hal
qualified
as
HAL
...
...
@@ -53,7 +53,7 @@ get :: ExternalAPIs
->
Maybe
Corpus
.
Limit
-- -> IO [HyperdataDocument]
->
IO
(
Either
GetCorpusError
(
Maybe
Integer
,
ConduitT
()
HyperdataDocument
IO
()
))
get
externalAPI
la
q
mPubmedAPIKey
limit
=
do
get
externalAPI
la
q
mPubmedAPIKey
epoAuthKey
limit
=
do
-- For PUBMED, HAL, IsTex, Isidore and OpenAlex, we want to send the query as-it.
-- For Arxiv we parse the query into a structured boolean query we submit over.
case
externalAPI
of
...
...
@@ -73,6 +73,6 @@ get externalAPI la q mPubmedAPIKey limit = do
docs
<-
ISIDORE
.
get
la
(
Corpus
.
getLimit
<$>
limit
)
(
Just
$
Corpus
.
getRawQuery
q
)
Nothing
pure
$
Right
(
Just
$
fromIntegral
$
length
docs
,
yieldMany
docs
)
EPO
->
do
first
ExternalAPIError
<$>
EPO
.
get
(
fromMaybe
""
Nothing
{- email -}
)
q
(
toISO639
la
)
limit
first
ExternalAPIError
<$>
EPO
.
get
epoAuthKey
q
(
toISO639EN
la
)
limit
where
parse_query
=
first
(
InvalidInputQuery
q
.
T
.
pack
)
$
Corpus
.
parseQuery
q
src/Gargantext/Core/Text/Corpus/API/EPO.hs
0 → 100644
View file @
3c47008a
{-|
Module : Gargantext.Core.Text.Corpus.API.EPO
Description : EPO (patents) API interface
Copyright : (c) CNRS, 2023
License : AGPL + CECILL v3
Maintainer : team@gargantext.org
Stability : experimental
Portability : POSIX
-}
module
Gargantext.Core.Text.Corpus.API.EPO
where
import
Conduit
import
Data.LanguageCodes
(
ISO639_1
)
import
Data.Map.Strict
qualified
as
Map
import
Data.Text
qualified
as
T
import
EPO.API.Client.Types
qualified
as
EPO
import
EPO.API.Client.Implementation
qualified
as
EPO
import
Gargantext.Core
(
iso639ToText
)
import
Gargantext.Core.Text.Corpus.Query
qualified
as
Corpus
import
Gargantext.Database.Admin.Types.Hyperdata
(
HyperdataDocument
(
..
))
import
Network.URI
(
parseURI
)
import
Protolude
import
Servant.Client.Core
(
ClientError
(
ConnectionError
))
get
::
Maybe
EPO
.
AuthKey
->
Corpus
.
RawQuery
->
ISO639_1
->
Maybe
Corpus
.
Limit
->
IO
(
Either
ClientError
(
Maybe
Integer
,
ConduitT
()
HyperdataDocument
IO
()
))
get
Nothing
_
_
_
=
do
-- throwIO $ EPO.OtherError "AuthKey is required"
pure
$
Left
$
ConnectionError
$
toException
$
ErrorCall
"AuthKey is required"
get
(
Just
authKey
)
q
lang
mLimit
=
do
let
_limit
=
Corpus
.
getLimit
$
fromMaybe
10000
mLimit
case
parseURI
"http://localhost:3000"
of
Nothing
->
pure
$
Left
$
ConnectionError
$
toException
$
ErrorCall
"Cannot parse API URL"
Just
apiUrl
->
do
EPO
.
Paginated
{
..
}
<-
EPO
.
searchEPOAPI
apiUrl
authKey
1
20
(
Corpus
.
getRawQuery
q
)
pure
$
Right
(
Just
$
fromIntegral
total
,
yieldMany
items
.|
mapC
(
toDoc
lang
)
)
toDoc
::
ISO639_1
->
EPO
.
HyperdataDocument
->
HyperdataDocument
toDoc
lang
(
EPO
.
HyperdataDocument
{
..
})
=
HyperdataDocument
{
_hd_bdd
=
Just
"EPO"
,
_hd_doi
=
Nothing
,
_hd_url
=
Nothing
,
_hd_uniqId
=
id
,
_hd_uniqIdBdd
=
id
,
_hd_page
=
Nothing
,
_hd_title
=
Map
.
lookup
lang
titles
,
_hd_authors
=
authors_
,
_hd_institutes
=
Nothing
,
_hd_source
=
Nothing
,
_hd_abstract
=
Map
.
lookup
lang
abstracts
,
_hd_publication_date
=
publication_date
,
_hd_publication_year
=
publication_year
,
_hd_publication_month
=
publication_month
,
_hd_publication_day
=
publication_day
,
_hd_publication_hour
=
Nothing
,
_hd_publication_minute
=
Nothing
,
_hd_publication_second
=
Nothing
,
_hd_language_iso2
=
Just
$
iso639ToText
lang
}
where
authors_
=
if
authors
==
[]
then
Nothing
else
Just
(
T
.
intercalate
", "
authors
)
-- EPO.withAuthKey authKey $ \token -> do
-- let range = EPO.Range { rBegin = 1, rEnd = limit }
-- (len, docsC) <- EPO.searchPublishedDataWithFetchC token (Just $ Corpus.getRawQuery q) (Just range)
-- pure (len, docsC .|
-- takeC limit .|
-- mapC (toDoc lang))
-- toDoc :: ISO639_1 -> EPO.ExchangeDocument -> HyperdataDocument
-- toDoc lang (EPO.ExchangeDocument { bibliographicData = EPO.BibliographicData { .. }
-- , abstracts } ) =
-- HyperdataDocument { _hd_bdd = Just "EPO"
-- , _hd_doi = Nothing
-- , _hd_url = Nothing
-- , _hd_uniqId = EPO.documentIdToText <$> (head documentIds)
-- , _hd_uniqIdBdd = EPO.documentIdToText <$> (head documentIds)
-- , _hd_page = Nothing
-- , _hd_title = Map.lookup lang inventionTitlesMap
-- , _hd_authors = authors parties
-- , _hd_institutes = Nothing
-- , _hd_source = Nothing
-- , _hd_abstract = Map.lookup lang abstractMap
-- , _hd_publication_date = T.pack <$> showGregorian <$> publicationDate
-- , _hd_publication_year = year
-- , _hd_publication_month = month
-- , _hd_publication_day = day
-- , _hd_publication_hour = Nothing
-- , _hd_publication_minute = Nothing
-- , _hd_publication_second = Nothing
-- , _hd_language_iso2 = Just $ iso639ToText lang }
-- where
-- authors :: EPO.Parties -> Maybe Text
-- authors (EPO.Parties { inventors = Nothing }) = Nothing
-- authors (EPO.Parties { inventors = Just EPO.Inventors { inventors } }) =
-- Just $ T.intercalate ", " (getInventorName <$> inventors)
-- getInventorName :: EPO.Inventor -> Text
-- getInventorName (EPO.Inventor { inventorName = EPO.InventorName { name } }) = name
-- abstractMap :: Map.Map ISO639_1 Text
-- abstractMap = Map.fromList [(l, text) | (EPO.Abstract { lang = l, text }) <- abstracts]
-- EPO.PublicationReferenceDet { documentIds } = publicationReference
-- dates :: [Day]
-- dates = catMaybes (EPO.date <$> documentIds)
-- publicationDate :: Maybe Day
-- publicationDate = head dates
-- (year, month, day) = case publicationDate of
-- Nothing -> (Nothing, Nothing, Nothing)
-- Just pd -> let (y, m, d) = toGregorian pd in
-- (Just $ fromIntegral y, Just m, Just d)
-- inventionTitlesMap :: Map ISO639_1 Text
-- inventionTitlesMap = Map.fromList [(l, text) | EPO.InventionTitle { lang = l, text } <- inventionTitles]
src/Gargantext/Database/Action/Flow.hs
View file @
3c47008a
...
...
@@ -69,6 +69,7 @@ import Data.Proxy
import
Data.Set
qualified
as
Set
import
Data.Swagger
import
Data.Text
qualified
as
T
import
EPO.API.Client.Types
qualified
as
EPO
import
Gargantext.API.Ngrams.Tools
(
getTermsWith
)
import
Gargantext.API.Ngrams.Types
qualified
as
NT
import
Gargantext.Core
(
Lang
(
..
),
PosTagAlgo
(
..
),
NLPServerConfig
)
...
...
@@ -160,12 +161,13 @@ getDataText :: (HasNodeError err)
->
TermType
Lang
->
API
.
RawQuery
->
Maybe
PUBMED
.
APIKey
->
Maybe
EPO
.
AuthKey
->
Maybe
API
.
Limit
->
DBCmd
err
(
Either
API
.
GetCorpusError
DataText
)
getDataText
(
ExternalOrigin
api
)
la
q
mPubmedAPIKey
li
=
do
eRes
<-
liftBase
$
API
.
get
api
(
_tt_lang
la
)
q
mPubmedAPIKey
li
getDataText
(
ExternalOrigin
api
)
la
q
mPubmedAPIKey
mAuthKey
li
=
do
eRes
<-
liftBase
$
API
.
get
api
(
_tt_lang
la
)
q
mPubmedAPIKey
mAuthKey
li
pure
$
DataNew
<$>
eRes
getDataText
(
InternalOrigin
_
)
_la
q
_
_li
=
do
getDataText
(
InternalOrigin
_
)
_la
q
_
_
_
li
=
do
(
_masterUserId
,
_masterRootId
,
cId
)
<-
getOrMk_RootWithCorpus
(
UserName
userMaster
)
(
Left
""
)
...
...
@@ -180,7 +182,7 @@ getDataText_Debug :: (HasNodeError err)
->
Maybe
API
.
Limit
->
DBCmd
err
()
getDataText_Debug
a
l
q
li
=
do
result
<-
getDataText
a
l
q
Nothing
li
result
<-
getDataText
a
l
q
Nothing
Nothing
li
case
result
of
Left
err
->
liftBase
$
putText
$
show
err
Right
res
->
liftBase
$
printDataText
res
...
...
src/Gargantext/Database/Admin/Types/Hyperdata/User.hs
View file @
3c47008a
...
...
@@ -40,6 +40,8 @@ data HyperdataUser =
,
_hu_shared
::
!
(
Maybe
HyperdataContact
)
,
_hu_public
::
!
(
Maybe
HyperdataPublic
)
,
_hu_pubmed_api_key
::
!
(
Maybe
PUBMED
.
APIKey
)
,
_hu_epo_api_user
::
!
(
Maybe
Text
)
,
_hu_epo_api_token
::
!
(
Maybe
Text
)
}
deriving
(
Eq
,
Show
,
Generic
)
instance
GQLType
HyperdataUser
where
...
...
@@ -71,7 +73,9 @@ defaultHyperdataUser =
{
_hu_private
=
Just
defaultHyperdataPrivate
,
_hu_shared
=
Just
defaultHyperdataContact
,
_hu_public
=
Just
defaultHyperdataPublic
,
_hu_pubmed_api_key
=
Nothing
}
,
_hu_pubmed_api_key
=
Nothing
,
_hu_epo_api_user
=
Nothing
,
_hu_epo_api_token
=
Nothing
}
defaultHyperdataPublic
::
HyperdataPublic
defaultHyperdataPublic
=
HyperdataPublic
"pseudo"
[
1
..
10
]
...
...
@@ -100,7 +104,12 @@ $(deriveJSON (unPrefix "_hpu_") ''HyperdataPublic)
-- | Arbitrary instances
instance
Arbitrary
HyperdataUser
where
arbitrary
=
HyperdataUser
<$>
arbitrary
<*>
arbitrary
<*>
arbitrary
<*>
arbitrary
arbitrary
=
HyperdataUser
<$>
arbitrary
<*>
arbitrary
<*>
arbitrary
<*>
arbitrary
<*>
arbitrary
<*>
arbitrary
instance
Arbitrary
HyperdataPrivate
where
arbitrary
=
pure
defaultHyperdataPrivate
...
...
src/Gargantext/Database/Query/Table/User.hs
View file @
3c47008a
...
...
@@ -34,6 +34,8 @@ module Gargantext.Database.Query.Table.User
,
updateUserForgotPasswordUUID
,
getUserPubmedAPIKey
,
updateUserPubmedAPIKey
,
updateUserEPOAPIUser
,
updateUserEPOAPIToken
,
getUser
,
insertNewUsers
,
selectUsersLightWith
...
...
@@ -56,7 +58,7 @@ import Data.UUID qualified as UUID
import
Gargantext.Core
(
HasDBid
)
import
Gargantext.Core.Types.Individu
import
Gargantext.Database.Admin.Config
(
nodeTypeId
)
import
Gargantext.Database.Admin.Types.Hyperdata
(
HyperdataUser
(
..
),
hu_pubmed_api_key
)
import
Gargantext.Database.Admin.Types.Hyperdata
(
HyperdataUser
(
..
),
hu_pubmed_api_key
,
hu_epo_api_user
,
hu_epo_api_token
)
import
Gargantext.Database.Admin.Types.Node
(
NodeType
(
NodeUser
),
Node
,
NodeId
(
..
),
pgNodeId
)
import
Gargantext.Database.Admin.Types.Node
(
UserId
(
..
))
import
Gargantext.Database.Prelude
...
...
@@ -275,6 +277,21 @@ updateUserPubmedAPIKey (RootId uId) apiKey = do
_
<-
updateNodeWithType
uId
NodeUser
(
Proxy
::
Proxy
HyperdataUser
)
(
\
h
->
h
&
hu_pubmed_api_key
?~
apiKey
)
pure
1
updateUserPubmedAPIKey
_
_
=
undefined
updateUserEPOAPIUser
::
(
HasDBid
NodeType
,
HasNodeError
err
)
=>
User
->
Text
->
DBCmd
err
Int64
updateUserEPOAPIUser
(
RootId
uId
)
apiUser
=
do
_
<-
updateNodeWithType
uId
NodeUser
(
Proxy
::
Proxy
HyperdataUser
)
(
\
h
->
h
&
hu_epo_api_user
?~
apiUser
)
pure
1
updateUserEPOAPIUser
_
_
=
undefined
updateUserEPOAPIToken
::
(
HasDBid
NodeType
,
HasNodeError
err
)
=>
User
->
Text
->
DBCmd
err
Int64
updateUserEPOAPIToken
(
RootId
uId
)
apiToken
=
do
_
<-
updateNodeWithType
uId
NodeUser
(
Proxy
::
Proxy
HyperdataUser
)
(
\
h
->
h
&
hu_epo_api_token
?~
apiToken
)
pure
1
updateUserEPOAPIToken
_
_
=
undefined
------------------------------------------------------------------
-- | Select User with some parameters
-- Not optimized version
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment