Skip to content
GitLab
Menu
Projects
Groups
Snippets
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in
Toggle navigation
Menu
Open sidebar
minerva
core
Commits
279ee560
Commit
279ee560
authored
Sep 24, 2020
by
Piotr Gawron
Browse files
publication API does not use old model cache
parent
f999be16
Changes
9
Hide whitespace changes
Inline
Side-by-side
pom.xml
View file @
279ee560
...
...
@@ -41,6 +41,7 @@
<spring-security-test.version>
5.0.8.RELEASE
</spring-security-test.version>
<springframework.webflow.version>
2.5.1.RELEASE
</springframework.webflow.version>
<springframework.boot.version>
2.0.5.RELEASE
</springframework.boot.version>
<spring.data.version>
2.0.10.RELEASE
</spring.data.version>
<spring.restdocs.version>
2.0.3.RELEASE
</spring.restdocs.version>
...
...
rest-api/src/main/java/lcsb/mapviewer/api/projects/models/publications/PublicationsController.java
View file @
279ee560
...
...
@@ -26,7 +26,7 @@ public class PublicationsController extends BaseController {
public
Map
<
String
,
Object
>
getPublications
(
@PathVariable
(
value
=
"projectId"
)
String
projectId
,
@PathVariable
(
value
=
"modelId"
)
String
modelId
,
@RequestParam
(
value
=
"
start
"
,
defaultValue
=
"0"
)
String
start
,
@RequestParam
(
value
=
"
page
"
,
defaultValue
=
"0"
)
String
start
,
@RequestParam
(
value
=
"length"
,
defaultValue
=
"10"
)
Integer
length
,
@RequestParam
(
value
=
"sortColumn"
,
defaultValue
=
"pubmedId"
)
String
sortColumn
,
@RequestParam
(
value
=
"sortOrder"
,
defaultValue
=
"asc"
)
String
sortOrder
,
...
...
rest-api/src/main/java/lcsb/mapviewer/api/projects/models/publications/PublicationsRestImpl.java
View file @
279ee560
...
...
@@ -7,6 +7,8 @@ import org.apache.commons.lang3.math.NumberUtils;
import
org.apache.logging.log4j.LogManager
;
import
org.apache.logging.log4j.Logger
;
import
org.springframework.beans.factory.annotation.Autowired
;
import
org.springframework.data.domain.*
;
import
org.springframework.data.domain.Sort.Direction
;
import
org.springframework.stereotype.Service
;
import
org.springframework.transaction.annotation.Transactional
;
...
...
@@ -18,7 +20,12 @@ import lcsb.mapviewer.common.exception.InvalidArgumentException;
import
lcsb.mapviewer.model.Article
;
import
lcsb.mapviewer.model.map.*
;
import
lcsb.mapviewer.model.map.model.Model
;
import
lcsb.mapviewer.model.map.model.ModelData
;
import
lcsb.mapviewer.persist.dao.ArticleProperty
;
import
lcsb.mapviewer.persist.dao.map.ReactionProperty
;
import
lcsb.mapviewer.persist.dao.map.species.ElementProperty
;
import
lcsb.mapviewer.services.QueryException
;
import
lcsb.mapviewer.services.interfaces.*
;
@Transactional
@Service
...
...
@@ -31,9 +38,19 @@ public class PublicationsRestImpl extends BaseRestImpl {
private
PubmedParser
pubmedParser
;
private
IPublicationService
publicationService
;
private
IElementService
elementService
;;
private
IReactionService
reactionService
;
@Autowired
public
PublicationsRestImpl
(
PubmedParser
pubmedParser
)
{
public
PublicationsRestImpl
(
PubmedParser
pubmedParser
,
IPublicationService
publicationService
,
IElementService
elementService
,
IReactionService
reactionService
)
{
this
.
pubmedParser
=
pubmedParser
;
this
.
publicationService
=
publicationService
;
this
.
elementService
=
elementService
;
this
.
reactionService
=
reactionService
;
}
public
SortedMap
<
MiriamData
,
List
<
BioEntity
>>
getPublications
(
Collection
<
Model
>
models
)
{
...
...
@@ -57,176 +74,103 @@ public class PublicationsRestImpl extends BaseRestImpl {
}
public
Map
<
String
,
Object
>
getPublications
(
String
projectId
,
String
modelId
,
String
start
String
,
Integer
length
,
String
projectId
,
String
modelId
,
String
page
String
,
Integer
length
,
String
sortColumn
,
String
sortOrder
,
String
search
)
throws
QueryException
{
List
<
Model
>
models
=
getModels
(
projectId
,
modelId
);
SortColumn
sortColumnEnum
=
getSortOrderColumn
(
sortColumn
);
Comparator
<
Map
.
Entry
<
MiriamData
,
List
<
BioEntity
>>>
comparator
=
getComparatorForColumn
(
sortColumnEnum
,
sortOrder
);
int
page
=
parseInteger
(
pageString
,
"page"
);
ArticleProperty
sortProperty
=
extractSortColumn
(
sortColumn
);
Pageable
pageable
;
if
(
sortProperty
!=
null
)
{
pageable
=
PageRequest
.
of
(
page
,
length
,
Sort
.
by
(
Direction
.
fromString
(
sortOrder
),
sortProperty
.
name
()));
}
else
{
pageable
=
PageRequest
.
of
(
page
,
length
);
Integer
start
=
Math
.
max
(
0
,
Integer
.
valueOf
(
startString
));
List
<
Map
<
String
,
Object
>>
resultList
=
new
ArrayList
<>();
}
Map
<
ArticleProperty
,
Object
>
filterOptions
=
new
HashMap
<>();
SortedMap
<
MiriamData
,
List
<
BioEntity
>>
publications
=
getPublications
(
models
);
List
<
Map
.
Entry
<
MiriamData
,
List
<
BioEntity
>>>
filteredList
=
new
ArrayList
<>();
List
<
ModelData
>
models
=
getModelService
().
getModelsByMapId
(
projectId
,
modelId
);
filterOptions
.
put
(
ArticleProperty
.
MODEL
,
models
);
long
count
=
publicationService
.
getCountByFilter
(
pageable
,
filterOptions
);
for
(
Map
.
Entry
<
MiriamData
,
List
<
BioEntity
>>
entry
:
publications
.
entrySet
())
{
Set
<
Model
>
publicationModels
=
new
LinkedHashSet
<>();
for
(
BioEntity
bioEntity
:
entry
.
getValue
())
{
publicationModels
.
add
(
bioEntity
.
getModel
());
}
if
(
isSearchResult
(
entry
.
getKey
(),
search
,
publicationModels
))
{
filteredList
.
add
(
entry
);
}
if
(
search
!=
null
&&
!
search
.
trim
().
equals
(
""
))
{
filterOptions
.
put
(
ArticleProperty
.
TEXT
,
search
);
}
if
(
comparator
!=
null
)
{
filteredList
.
sort
(
comparator
);
Page
<
Article
>
articles
=
publicationService
.
getByFilter
(
pageable
,
filterOptions
);
List
<
MiriamData
>
mds
=
new
ArrayList
<>();
for
(
Article
article
:
articles
)
{
mds
.
add
(
new
MiriamData
(
MiriamType
.
PUBMED
,
article
.
getPubmedId
()));
}
int
index
=
0
;
for
(
Map
.
Entry
<
MiriamData
,
List
<
BioEntity
>>
entry
:
filteredList
)
{
if
(
index
>=
start
&&
index
<
start
+
length
)
{
List
<
Object
>
elements
=
new
ArrayList
<>();
for
(
BioEntity
object
:
entry
.
getValue
())
{
elements
.
add
(
createMinifiedSearchResult
(
object
));
}
Map
<
ElementProperty
,
List
<?
extends
Object
>>
elementSearch
=
new
HashMap
<>();
elementSearch
.
put
(
ElementProperty
.
ANNOTATION
,
mds
);
elementSearch
.
put
(
ElementProperty
.
MAP
,
models
);
Map
<
String
,
Object
>
row
=
new
TreeMap
<>();
row
.
put
(
"elements"
,
elements
);
row
.
put
(
"publication"
,
createAnnotation
(
entry
.
getKey
()));
resultList
.
add
(
row
);
}
index
++;
}
List
<
BioEntity
>
bioEntities
=
new
ArrayList
<>();
bioEntities
.
addAll
(
elementService
.
getElementsByFilter
(
elementSearch
));
Map
<
String
,
Object
>
result
=
new
TreeMap
<>();
result
.
put
(
"data"
,
resultList
);
result
.
put
(
"totalSize"
,
publications
.
size
());
result
.
put
(
"filteredSize"
,
filteredList
.
size
());
result
.
put
(
"start"
,
start
);
result
.
put
(
"length"
,
resultList
.
size
());
return
result
;
}
Map
<
ReactionProperty
,
List
<?
extends
Object
>>
reactionSearch
=
new
HashMap
<>();
elementSearch
.
put
(
ElementProperty
.
ANNOTATION
,
mds
);
elementSearch
.
put
(
ElementProperty
.
MAP
,
models
);
Comparator
<
Entry
<
MiriamData
,
List
<
BioEntity
>>>
getComparatorForColumn
(
SortColumn
sortColumnEnum
,
String
sortOrder
)
{
final
int
orderFactor
;
if
(
sortOrder
.
toLowerCase
().
equals
(
"desc"
))
{
orderFactor
=
-
1
;
}
else
{
orderFactor
=
1
;
}
if
(
sortColumnEnum
==
null
)
{
return
null
;
}
else
if
(
sortColumnEnum
.
equals
(
SortColumn
.
PUBMED_ID
))
{
return
new
Comparator
<
Map
.
Entry
<
MiriamData
,
List
<
BioEntity
>>>()
{
IntegerComparator
integerComparator
=
new
IntegerComparator
();
bioEntities
.
addAll
(
reactionService
.
getReactionsByFilter
(
reactionSearch
));
@Override
public
int
compare
(
Entry
<
MiriamData
,
List
<
BioEntity
>>
o1
,
Entry
<
MiriamData
,
List
<
BioEntity
>>
o2
)
{
Integer
id1
=
extractPubmedId
(
o1
.
getKey
());
Integer
id2
=
extractPubmedId
(
o2
.
getKey
());
return
integerComparator
.
compare
(
id1
,
id2
)
*
orderFactor
;
List
<
Map
<
String
,
Object
>>
resultList
=
new
ArrayList
<>();
}
Map
<
MiriamData
,
List
<
BioEntity
>>
publications
=
new
HashMap
<>();
};
}
else
if
(
sortColumnEnum
.
equals
(
SortColumn
.
YEAR
))
{
return
(
o1
,
o2
)
->
{
try
{
Article
article1
=
getArticle
(
o1
.
getKey
().
getResource
());
Article
article2
=
getArticle
(
o2
.
getKey
().
getResource
());
return
article1
.
getYear
().
compareTo
(
article2
.
getYear
())
*
orderFactor
;
}
catch
(
Exception
e
)
{
logger
.
error
(
"Problem with accessing article data "
,
e
);
return
0
;
}
};
}
else
if
(
sortColumnEnum
.
equals
(
SortColumn
.
JOURNAL
))
{
return
(
o1
,
o2
)
->
{
try
{
Article
article1
=
getArticle
(
o1
.
getKey
().
getResource
());
Article
article2
=
getArticle
(
o2
.
getKey
().
getResource
());
return
article1
.
getJournal
().
compareTo
(
article2
.
getJournal
())
*
orderFactor
;
}
catch
(
Exception
e
)
{
logger
.
error
(
"Problem with accessing article data "
,
e
);
return
0
;
}
};
}
else
if
(
sortColumnEnum
.
equals
(
SortColumn
.
TITLE
))
{
return
(
o1
,
o2
)
->
{
try
{
Article
article1
=
getArticle
(
o1
.
getKey
().
getResource
());
Article
article2
=
getArticle
(
o2
.
getKey
().
getResource
());
return
article1
.
getTitle
().
compareTo
(
article2
.
getTitle
())
*
orderFactor
;
}
catch
(
Exception
e
)
{
logger
.
error
(
"Problem with accessing article data "
,
e
);
return
0
;
for
(
BioEntity
bioEntity
:
bioEntities
)
{
for
(
MiriamData
md
:
bioEntity
.
getMiriamData
())
{
if
(
md
.
getDataType
().
equals
(
MiriamType
.
PUBMED
))
{
List
<
BioEntity
>
list
=
publications
.
computeIfAbsent
(
md
,
k
->
new
ArrayList
<>());
list
.
add
(
bioEntity
);
}
}
};
}
else
if
(
sortColumnEnum
.
equals
(
SortColumn
.
AUTHORS
))
{
return
(
o1
,
o2
)
->
{
try
{
Article
article1
=
getArticle
(
o1
.
getKey
().
getResource
());
Article
article2
=
getArticle
(
o2
.
getKey
().
getResource
());
return
article1
.
getStringAuthors
().
compareTo
(
article2
.
getStringAuthors
())
*
orderFactor
;
}
catch
(
Exception
e
)
{
logger
.
error
(
"Problem with accessing article data "
,
e
);
return
0
;
}
};
}
else
{
throw
new
InvalidArgumentException
(
"Unknown column type: "
+
sortColumnEnum
);
}
}
private
SortColumn
getSortOrderColumn
(
String
sortColumn
)
throws
QueryException
{
if
(!
sortColumn
.
isEmpty
())
{
for
(
SortColumn
type
:
SortColumn
.
values
())
{
if
(
type
.
commonName
.
toLowerCase
().
equals
(
sortColumn
.
toLowerCase
()))
{
return
type
;
}
for
(
Article
article
:
articles
)
{
List
<
Object
>
elements
=
new
ArrayList
<>();
for
(
BioEntity
bioEntity
:
publications
.
computeIfAbsent
(
new
MiriamData
(
MiriamType
.
PUBCHEM
,
article
.
getPubmedId
()),
k
->
new
ArrayList
<>()))
{
elements
.
add
(
createMinifiedSearchResult
(
bioEntity
));
}
throw
new
QueryException
(
"Unknown sortColumn: "
+
sortColumn
);
Map
<
String
,
Object
>
row
=
new
TreeMap
<>();
row
.
put
(
"elements"
,
elements
);
Map
<
String
,
Object
>
articleEntry
=
new
HashMap
<>();
articleEntry
.
put
(
"article"
,
article
);
row
.
put
(
"publication"
,
articleEntry
);
resultList
.
add
(
row
);
}
return
null
;
Map
<
String
,
Object
>
result
=
new
TreeMap
<>();
result
.
put
(
"data"
,
resultList
);
result
.
put
(
"totalSize"
,
count
);
result
.
put
(
"filteredSize"
,
articles
.
getTotalElements
());
result
.
put
(
"page"
,
page
);
result
.
put
(
"length"
,
resultList
.
size
());
return
result
;
}
private
boolean
isSearchResult
(
MiriamData
key
,
String
search
,
Set
<
Model
>
models
)
{
String
lowerCaseSearch
=
search
.
toLowerCase
();
if
(
search
.
isEmpty
())
{
return
true
;
}
Article
article
=
null
;
if
(
MiriamType
.
PUBMED
.
equals
(
key
.
getDataType
()))
{
try
{
article
=
pubmedParser
.
getPubmedArticleById
(
Integer
.
valueOf
(
key
.
getResource
()));
}
catch
(
PubmedSearchException
e
)
{
logger
.
error
(
"Problem with accessing info about pubmed"
,
e
);
}
private
ArticleProperty
extractSortColumn
(
String
sortColumn
)
throws
QueryException
{
if
(
sortColumn
==
null
)
{
return
null
;
}
if
(
article
!=
null
)
{
if
(
article
.
getPubmedId
().
toLowerCase
().
contains
(
lowerCaseSearch
))
{
return
true
;
}
else
if
(
article
.
getJournal
().
toLowerCase
().
contains
(
lowerCaseSearch
))
{
return
true
;
}
else
if
(
article
.
getStringAuthors
().
toLowerCase
().
contains
(
lowerCaseSearch
))
{
return
true
;
}
else
if
(
article
.
getTitle
().
toLowerCase
().
contains
(
lowerCaseSearch
))
{
return
true
;
}
else
if
(
article
.
getYear
().
toString
().
toLowerCase
().
contains
(
lowerCaseSearch
))
{
return
true
;
}
else
{
for
(
Model
model
:
models
)
{
if
(
model
.
getName
().
toLowerCase
().
contains
(
lowerCaseSearch
))
{
return
true
;
}
}
}
switch
(
sortColumn
.
toLowerCase
().
trim
())
{
case
""
:
return
null
;
case
"pubmedid"
:
return
ArticleProperty
.
PUBMED_ID
;
case
"year"
:
return
ArticleProperty
.
YEAR
;
case
"journal"
:
return
ArticleProperty
.
JOURNAL
;
case
"title"
:
return
ArticleProperty
.
TITLE
;
default
:
throw
new
QueryException
(
"Unknown sort order: "
+
sortColumn
);
}
return
false
;
}
/**
...
...
@@ -259,18 +203,4 @@ public class PublicationsRestImpl extends BaseRestImpl {
}
enum
SortColumn
{
PUBMED_ID
(
"pubmedId"
),
YEAR
(
"year"
),
JOURNAL
(
"journal"
),
TITLE
(
"title"
),
AUTHORS
(
"authors"
);
private
String
commonName
;
SortColumn
(
String
commonName
)
{
this
.
commonName
=
commonName
;
}
}
}
service/src/main/java/lcsb/mapviewer/services/impl/ModelService.java
View file @
279ee560
...
...
@@ -90,8 +90,8 @@ public class ModelService implements IModelService {
this
.
projectDao
=
projectDao
;
this
.
backend
=
backend
;
this
.
miriamConnector
=
miriamConnector
;
this
.
sbmlParameterDao
=
sbmlParameterDao
;
this
.
sbmlUnitDao
=
sbmlUnitDao
;
this
.
sbmlParameterDao
=
sbmlParameterDao
;
this
.
sbmlUnitDao
=
sbmlUnitDao
;
}
@Override
...
...
service/src/main/java/lcsb/mapviewer/services/impl/PublicationService.java
0 → 100644
View file @
279ee560
package
lcsb.mapviewer.services.impl
;
import
java.util.List
;
import
java.util.Map
;
import
org.apache.logging.log4j.LogManager
;
import
org.apache.logging.log4j.Logger
;
import
org.springframework.beans.factory.annotation.Autowired
;
import
org.springframework.data.domain.Page
;
import
org.springframework.data.domain.Pageable
;
import
org.springframework.stereotype.Service
;
import
org.springframework.transaction.annotation.Transactional
;
import
lcsb.mapviewer.annotation.services.PubmedParser
;
import
lcsb.mapviewer.annotation.services.PubmedSearchException
;
import
lcsb.mapviewer.common.exception.NotImplementedException
;
import
lcsb.mapviewer.model.Article
;
import
lcsb.mapviewer.persist.dao.ArticleDao
;
import
lcsb.mapviewer.persist.dao.ArticleProperty
;
import
lcsb.mapviewer.services.interfaces.IPublicationService
;
@Transactional
@Service
public
class
PublicationService
implements
IPublicationService
{
Logger
logger
=
LogManager
.
getLogger
();
@Autowired
private
ArticleDao
articleDao
;
@Autowired
private
PubmedParser
pubmedParser
;
@Override
public
Page
<
Article
>
getByFilter
(
Pageable
pageable
,
Map
<
ArticleProperty
,
Object
>
filterOptions
)
{
List
<
String
>
missingIds
=
articleDao
.
getMissingByFilter
(
filterOptions
);
for
(
String
id
:
missingIds
)
{
try
{
Article
article
=
pubmedParser
.
getPubmedArticleById
(
id
);
articleDao
.
add
(
article
);
}
catch
(
PubmedSearchException
e
)
{
logger
.
error
(
"Problem with accessing article"
,
e
);
}
}
return
articleDao
.
getByFilter
(
pageable
,
filterOptions
);
}
@Override
public
long
getCountByFilter
(
Pageable
pageable
,
Map
<
ArticleProperty
,
Object
>
filterOptions
)
{
return
articleDao
.
getCountByFilter
(
filterOptions
);
}
}
service/src/main/java/lcsb/mapviewer/services/impl/ReactionService.java
View file @
279ee560
...
...
@@ -9,6 +9,7 @@ import org.springframework.transaction.annotation.Transactional;
import
com.google.gson.internal.LinkedTreeMap
;
import
lcsb.mapviewer.common.exception.InvalidArgumentException
;
import
lcsb.mapviewer.common.exception.NotImplementedException
;
import
lcsb.mapviewer.model.Project
;
import
lcsb.mapviewer.model.map.MiriamType
;
import
lcsb.mapviewer.model.map.kinetics.SbmlParameter
;
...
...
@@ -16,6 +17,7 @@ import lcsb.mapviewer.model.map.model.ModelData;
import
lcsb.mapviewer.model.map.reaction.Reaction
;
import
lcsb.mapviewer.persist.dao.ProjectDao
;
import
lcsb.mapviewer.persist.dao.map.ReactionDao
;
import
lcsb.mapviewer.persist.dao.map.ReactionProperty
;
import
lcsb.mapviewer.services.ObjectNotFoundException
;
import
lcsb.mapviewer.services.QueryException
;
import
lcsb.mapviewer.services.interfaces.IModelService
;
...
...
@@ -84,7 +86,8 @@ public class ReactionService implements IReactionService {
@Override
public
Map
<
MiriamType
,
Integer
>
getAnnotationStatistics
(
String
projectId
,
String
mapId
)
throws
QueryException
{
Map
<
MiriamType
,
Integer
>
elementAnnotations
=
new
LinkedTreeMap
<>();
Map
<
MiriamType
,
Integer
>
data
=
reactionDao
.
getAnnotationStatistics
(
modelService
.
getModelsByMapId
(
projectId
,
mapId
));
Map
<
MiriamType
,
Integer
>
data
=
reactionDao
.
getAnnotationStatistics
(
modelService
.
getModelsByMapId
(
projectId
,
mapId
));
for
(
MiriamType
mt
:
MiriamType
.
values
())
{
if
(
data
.
get
(
mt
)
!=
null
)
{
elementAnnotations
.
put
(
mt
,
data
.
get
(
mt
));
...
...
@@ -95,4 +98,9 @@ public class ReactionService implements IReactionService {
return
elementAnnotations
;
}
@Override
public
List
<
Reaction
>
getReactionsByFilter
(
Map
<
ReactionProperty
,
List
<?
extends
Object
>>
reactionSearch
)
{
return
reactionDao
.
getByFilter
(
reactionSearch
);
}
}
service/src/main/java/lcsb/mapviewer/services/interfaces/IPublicationService.java
0 → 100644
View file @
279ee560
package
lcsb.mapviewer.services.interfaces
;
import
java.util.Map
;
import
org.springframework.data.domain.Page
;
import
org.springframework.data.domain.Pageable
;
import
lcsb.mapviewer.model.Article
;
import
lcsb.mapviewer.persist.dao.ArticleProperty
;
public
interface
IPublicationService
{
Page
<
Article
>
getByFilter
(
Pageable
pageable
,
Map
<
ArticleProperty
,
Object
>
filterOptions
);
long
getCountByFilter
(
Pageable
pageable
,
Map
<
ArticleProperty
,
Object
>
filterOptions
);
}
service/src/main/java/lcsb/mapviewer/services/interfaces/IReactionService.java
View file @
279ee560
...
...
@@ -5,6 +5,7 @@ import java.util.*;
import
lcsb.mapviewer.model.map.MiriamType
;
import
lcsb.mapviewer.model.map.kinetics.SbmlParameter
;
import
lcsb.mapviewer.model.map.reaction.Reaction
;
import
lcsb.mapviewer.persist.dao.map.ReactionProperty
;
import
lcsb.mapviewer.services.QueryException
;
public
interface
IReactionService
{
...
...
@@ -22,4 +23,6 @@ public interface IReactionService {
Map
<
MiriamType
,
Integer
>
getAnnotationStatistics
(
String
projectId
,
String
mapId
)
throws
QueryException
;
List
<
Reaction
>
getReactionsByFilter
(
Map
<
ReactionProperty
,
List
<?
extends
Object
>>
reactionSearch
);
}
web/src/test/java/lcsb/mapviewer/web/PublicationsControllerIntegrationTest.java
View file @
279ee560
...
...
@@ -79,8 +79,8 @@ public class PublicationsControllerIntegrationTest extends ControllerIntegration
fieldWithPath
(
"length"
)
.
description
(
"number of entries in this response"
)
.
type
(
"number"
),
fieldWithPath
(
"
start
"
)
.
description
(
"number of
first entry in this response
"
)
fieldWithPath
(
"
page
"
)
.
description
(
"
page
number of
result list
"
)
.
type
(
"number"
),
fieldWithPath
(
"totalSize"
)
.
description
(
"number of all log entries"
)
...
...
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