Skip to content
Toggle navigation
Projects
Groups
Snippets
Help
Antti Väyrynen
/
Moya
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Wiki
Settings
Activity
Graph
Charts
Create a new issue
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Commit 12e50381
authored
Feb 04, 2017
by
Tuomas Riihimäki
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add timelapse possibility to legacy placemap image generator
1 parent
3747c7f2
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
315 additions
and
300 deletions
code/moya-web/pom.xml
code/moya-web/src/main/java/fi/codecrew/moya/servlet/PlaceMapServlet.java
code/moya-web/pom.xml
View file @
12e5038
...
@@ -35,8 +35,11 @@
...
@@ -35,8 +35,11 @@
<artifactId>
primefaces-extensions
</artifactId>
<artifactId>
primefaces-extensions
</artifactId>
<version>
${primefaces.extensions}
</version>
<version>
${primefaces.extensions}
</version>
</dependency>
</dependency>
<dependency>
<groupId>
joda-time
</groupId>
<artifactId>
joda-time
</artifactId>
<version>
2.3
</version>
</dependency>
<dependency>
<dependency>
<groupId>
org.primefaces.themes
</groupId>
<groupId>
org.primefaces.themes
</groupId>
<artifactId>
all-themes
</artifactId>
<artifactId>
all-themes
</artifactId>
...
...
code/moya-web/src/main/java/fi/codecrew/moya/servlet/PlaceMapServlet.java
View file @
12e5038
...
@@ -43,6 +43,8 @@ import javax.servlet.http.HttpServlet;
...
@@ -43,6 +43,8 @@ import javax.servlet.http.HttpServlet;
import
javax.servlet.http.HttpServletRequest
;
import
javax.servlet.http.HttpServletRequest
;
import
javax.servlet.http.HttpServletResponse
;
import
javax.servlet.http.HttpServletResponse
;
import
fi.codecrew.moya.enums.apps.UserPermission
;
import
org.joda.time.format.ISODateTimeFormat
;
import
org.slf4j.Logger
;
import
org.slf4j.Logger
;
import
org.slf4j.LoggerFactory
;
import
org.slf4j.LoggerFactory
;
...
@@ -58,307 +60,317 @@ import fi.codecrew.moya.model.Place;
...
@@ -58,307 +60,317 @@ import fi.codecrew.moya.model.Place;
import
fi.codecrew.moya.utilities.moyamessage.MoyaEventType
;
import
fi.codecrew.moya.utilities.moyamessage.MoyaEventType
;
/**
/**
*
* @author tuukka
* @author tuukka
*/
*/
@WebServlet
(
"/PlaceMap"
)
@WebServlet
(
"/PlaceMap"
)
public
class
PlaceMapServlet
extends
HttpServlet
{
public
class
PlaceMapServlet
extends
HttpServlet
{
private
static
final
Logger
logger
=
LoggerFactory
.
getLogger
(
PlaceMapServlet
.
class
);
private
static
final
Logger
logger
=
LoggerFactory
.
getLogger
(
PlaceMapServlet
.
class
);
private
static
final
long
serialVersionUID
=
8769688627918936258L
;
private
static
final
long
serialVersionUID
=
8769688627918936258L
;
@EJB
@EJB
private
transient
PlaceBeanLocal
placeBean
;
private
transient
PlaceBeanLocal
placeBean
;
@EJB
@EJB
private
transient
PermissionBeanLocal
permbean
;
private
transient
PermissionBeanLocal
permbean
;
@EJB
@EJB
private
transient
LoggingBeanLocal
loggerbean
;
private
transient
LoggingBeanLocal
loggerbean
;
@EJB
@EJB
private
UserBeanLocal
userBean
;
private
UserBeanLocal
userBean
;
@EJB
@EJB
private
BarcodeBeanLocal
barcodeBean
;
private
BarcodeBeanLocal
barcodeBean
;
private
static
final
String
PARAMETER_EVENT_MAP_ID
=
"mapid"
;
private
static
final
String
PARAMETER_EVENT_MAP_ID
=
"mapid"
;
private
static
final
String
PARAMETER_USER_ID
=
"userid"
;
private
static
final
String
PARAMETER_USER_ID
=
"userid"
;
private
static
final
String
PARAMETER_TOKEN
=
"token"
;
private
static
final
String
PARAMETER_TOKEN
=
"token"
;
private
static
final
String
PARAMETER_DATE
=
"date"
;
/**
/**
* Processes requests for both HTTP <code>GET</code> and <code>POST</code>
* Processes requests for both HTTP <code>GET</code> and <code>POST</code>
* methods.
* methods.
*
*
* @param request
* @param request servlet request
* servlet request
* @param response servlet response
* @param response
* @throws ServletException if a servlet-specific error occurs
* servlet response
* @throws IOException if an I/O error occurs
* @throws ServletException
*/
* if a servlet-specific error occurs
protected
void
processRequest
(
HttpServletRequest
request
,
HttpServletResponse
response
)
* @throws IOException
throws
ServletException
,
IOException
{
* if an I/O error occurs
logger
.
debug
(
"Begin processing request"
);
*/
// response.setContentType("text/html;charset=UTF-8");
protected
void
processRequest
(
HttpServletRequest
request
,
HttpServletResponse
response
)
throws
ServletException
,
IOException
{
// PrintWriter out = response.getWriter();
logger
.
debug
(
"Begin processing request"
);
ServletOutputStream
ostream
=
null
;
// response.setContentType("text/html;charset=UTF-8");
try
{
// Integer placeId = getIntegerParameter(request,
// PrintWriter out = response.getWriter();
// PARAMETER_SELECTED_PLACE_ID);
ServletOutputStream
ostream
=
null
;
try
{
Integer
mapId
=
getIntegerParameter
(
request
,
PARAMETER_EVENT_MAP_ID
);
// Integer placeId = getIntegerParameter(request,
String
timelapseDate
=
request
.
getParameter
(
PARAMETER_DATE
);
// PARAMETER_SELECTED_PLACE_ID);
EventMap
map
=
placeBean
.
findMap
(
mapId
);
Integer
mapId
=
getIntegerParameter
(
request
,
PARAMETER_EVENT_MAP_ID
);
logger
.
debug
(
"Mapid: {}"
,
mapId
);
ostream
=
response
.
getOutputStream
();
Integer
userId
=
getIntegerParameter
(
request
,
PARAMETER_USER_ID
);
if
(
map
==
null
||
map
.
getMapData
()
==
null
)
{
String
userToken
=
request
.
getParameter
(
PARAMETER_TOKEN
);
logger
.
warn
(
"Error handling map {}!"
,
map
);
response
.
setStatus
(
HttpServletResponse
.
SC_NOT_FOUND
);
ostream
.
print
(
"Map error!"
);
return
;
}
EventMap
map
=
placeBean
.
findMap
(
mapId
);
String
[]
splittype
=
map
.
getMimeType
().
split
(
"/"
);
logger
.
debug
(
"Mapid: {}"
,
mapId
);
String
type
=
"png"
;
ostream
=
response
.
getOutputStream
();
if
(
splittype
.
length
>
0
)
{
if
(
map
==
null
||
map
.
getMapData
()
==
null
)
{
type
=
splittype
[
splittype
.
length
-
1
];
logger
.
warn
(
"Error handling map {}!"
,
map
);
}
response
.
setStatus
(
HttpServletResponse
.
SC_NOT_FOUND
);
ostream
.
print
(
"Map error!"
);
response
.
setContentType
(
map
.
getMimeType
());
}
else
{
if
(
timelapseDate
==
null
||
timelapseDate
.
trim
().
isEmpty
())
{
Integer
userId
=
getIntegerParameter
(
request
,
PARAMETER_USER_ID
);
String
[]
splittype
=
map
.
getMimeType
().
split
(
"/"
);
String
userToken
=
request
.
getParameter
(
PARAMETER_TOKEN
);
String
type
=
"png"
;
EventUser
user
=
null
;
if
(
splittype
.
length
>
0
)
if
(
userToken
!=
null
)
{
type
=
splittype
[
splittype
.
length
-
1
];
user
=
barcodeBean
.
getUserFromLongTextCode
(
userToken
);
}
response
.
setContentType
(
map
.
getMimeType
());
printPlaceMapToStream
(
ostream
,
type
,
map
,
userId
,
userToken
);
if
(!
permbean
.
hasPermission
(
MapPermission
.
VIEW
)
&&
user
==
null
)
{
logger
.
debug
(
"Flushing ostream"
);
loggerbean
.
sendMessage
(
MoyaEventType
.
USER_INSUFFICIENT_PERMISSIONS
,
permbean
.
getCurrentUser
(),
ostream
.
flush
();
"User tried to print the placemap to stream without sufficient permissions"
);
}
throw
new
EJBAccessException
(
"Not enough permissions to print placemap"
);
}
}
catch
(
EJBException
e
)
{
// Find for user only with sufficient permissions
if
(
user
!=
null
&&
userId
!=
0
&&
permbean
.
hasPermission
(
UserPermission
.
VIEW_ALL
))
{
user
=
userBean
.
findByUserId
(
userId
,
false
);
logger
.
debug
(
"Permission denied. Returning SC_FORBIDDEN!"
);
}
response
.
setContentType
(
"text/html;charset=UTF-8"
);
response
.
setStatus
(
HttpServletResponse
.
SC_FORBIDDEN
);
printPlaceMapToStream
(
ostream
,
type
,
map
,
user
);
ostream
=
response
.
getOutputStream
();
ostream
.
print
(
"Error 403 \nPermission denied! Please login before accessing resource"
);
}
else
{
// e.printStackTrace();
Date
date
=
ISODateTimeFormat
.
dateTime
().
parseDateTime
(
timelapseDate
).
toDate
();
printTimelapseToStream
(
ostream
,
type
,
map
,
date
);
}
finally
{
}
if
(
ostream
!=
null
)
{
ostream
.
close
();
ostream
.
flush
();
}
}
}
catch
(
EJBException
e
)
{
}
logger
.
debug
(
"Permission denied. Returning SC_FORBIDDEN!"
);
private
void
printPlaceMapToStream
(
OutputStream
outputStream
,
String
filetype
,
EventMap
map
,
Integer
userid
,
String
userToken
)
throws
IOException
response
.
setContentType
(
"text/html;charset=UTF-8"
);
{
response
.
setStatus
(
HttpServletResponse
.
SC_FORBIDDEN
);
ostream
=
response
.
getOutputStream
();
ostream
.
print
(
"Error 403 \nPermission denied! Please login before accessing resource"
);
EventUser
user
=
null
;
// e.printStackTrace();
if
(
userToken
!=
null
)
user
=
barcodeBean
.
getUserFromLongTextCode
(
userToken
);
}
finally
{
if
(
ostream
!=
null
)
{
ostream
.
close
();
if
(!
permbean
.
hasPermission
(
MapPermission
.
VIEW
)
&&
user
==
null
)
}
{
}
loggerbean
.
sendMessage
(
MoyaEventType
.
USER_INSUFFICIENT_PERMISSIONS
,
permbean
.
getCurrentUser
(),
"User tried to print the placemap to stream without sufficient permissions"
);
}
throw
new
EJBAccessException
(
"Not enough permissions to print placemap"
);
}
private
void
printTimelapseToStream
(
OutputStream
ostr
,
String
filetype
,
EventMap
map
,
Date
date
)
throws
IOException
{
long
begin
=
new
Date
().
getTime
();
BufferedImage
image
=
drawMap
(
map
,
new
TimelapsePicker
(
date
));
ImageIO
.
write
(
image
,
filetype
,
ostr
);
// List<Place> selectedPlaceList = placeBean.findPlaces(placeIds);
}
// logger.debug("SelectedPlaceList: size {}, {} ",
private
void
printPlaceMapToStream
(
OutputStream
outputStream
,
String
filetype
,
EventMap
map
,
EventUser
user
)
throws
IOException
{
// selectedPlaceList.size(), selectedPlaceList);
List
<
Place
>
userplaces
=
null
;
// map = eventMapFacade.find(eventId, mapId);
if
(
user
!=
null
)
{
userplaces
=
placeBean
.
findPlacePrintlistForUser
(
user
);
// if (map == null) {
}
// throw new PermissionDeniedException(secubean, user,
// Yes this is is in correct order.
// "Map not found with id: " + mapId + " and event id: " +
if
(
user
==
null
)
{
// eventbean.getCurrentEvent());
user
=
permbean
.
getCurrentUser
();
// }
}
// logger.debug("Got map object {}", map);
BufferedImage
image
=
drawMap
(
map
,
new
LegacyColorPicker
(
user
,
userplaces
));
List
<
Place
>
places
=
map
.
getPlaces
();
ImageIO
.
write
(
image
,
filetype
,
outputStream
);
List
<
Place
>
userplaces
=
null
;
}
private
BufferedImage
drawMap
(
EventMap
map
,
PlaceColorPicker
colorPicker
)
throws
IOException
{
if
(
userid
!=
null
&&
userid
!=
0
)
{
BufferedImage
image
=
ImageIO
.
read
(
new
ByteArrayInputStream
(
map
.
getMapData
()));
EventUser
placesForUser
=
userBean
.
findByUserId
(
userid
,
false
);
Graphics2D
g2d
=
image
.
createGraphics
();
if
(
user
!=
null
)
{
for
(
Place
place
:
map
.
getPlaces
())
{
userplaces
=
placeBean
.
findPlacePrintlistForUser
(
placesForUser
);
drawPlace
(
place
,
g2d
,
colorPicker
);
}
}
}
return
image
;
}
if
(
user
!=
null
)
{
userplaces
=
placeBean
.
findPlacePrintlistForUser
(
user
);
}
/***
* Convert request parameter into integer
*
if
(
user
==
null
)
* @param request
user
=
permbean
.
getCurrentUser
();
* @param parameter
// List<Place> selectedPlaces = placemapBean.findSelectedPlaces(map);
* @return
*/
BufferedImage
image
=
ImageIO
.
read
(
new
ByteArrayInputStream
(
map
.
getMapData
()));
private
static
Integer
getIntegerParameter
(
HttpServletRequest
request
,
String
parameter
)
{
try
{
Graphics2D
g2d
=
image
.
createGraphics
();
String
valueString
=
request
.
getParameter
(
parameter
);
for
(
Place
place
:
places
)
{
Integer
value
=
Integer
.
parseInt
(
valueString
);
if
(
userplaces
!=
null
)
{
return
value
;
if
(
userplaces
.
contains
(
place
))
{
}
catch
(
NumberFormatException
nfe
)
{
drawPlace
(
place
,
g2d
,
user
,
true
,
true
);
}
}
else
{
return
null
;
drawPlace
(
place
,
g2d
,
user
,
true
,
false
);
}
}
// <editor-fold defaultstate="collapsed"
}
else
{
// desc="HttpServlet methods. Click on the + sign on the left to edit the
drawPlace
(
place
,
g2d
,
user
,
false
,
false
);
// code.">
}
}
/**
* Handles the HTTP <code>GET</code> method.
// BufferedImage image = map.getMapWithPlaces(, selectedPlaces);
*
* @param request servlet request
logger
.
debug
(
"Prewrite {}"
,
new
Date
().
getTime
()
-
begin
);
* @param response servlet response
ImageIO
.
write
(
image
,
filetype
,
outputStream
);
* @throws ServletException if a servlet-specific error occurs
logger
.
debug
(
"postwrite {}"
,
new
Date
().
getTime
()
-
begin
);
* @throws IOException if an I/O error occurs
*/
}
@Override
protected
void
doGet
(
HttpServletRequest
request
,
HttpServletResponse
response
)
/***
throws
ServletException
,
IOException
{
* Convert request parameter into integer
processRequest
(
request
,
response
);
*
}
* @param request
* @param parameter
/**
* @return
* Handles the HTTP <code>POST</code> method.
*/
*
private
static
Integer
getIntegerParameter
(
HttpServletRequest
request
,
* @param request servlet request
String
parameter
)
{
* @param response servlet response
try
{
* @throws ServletException if a servlet-specific error occurs
String
valueString
=
request
.
getParameter
(
parameter
);
* @throws IOException if an I/O error occurs
Integer
value
=
Integer
.
parseInt
(
valueString
);
*/
return
value
;
@Override
}
catch
(
NumberFormatException
nfe
)
{
protected
void
doPost
(
HttpServletRequest
request
,
HttpServletResponse
response
)
}
throws
ServletException
,
IOException
{
return
null
;
processRequest
(
request
,
response
);
}
}
/**
// <editor-fold defaultstate="collapsed"
* Returns a short description of the servlet.
// desc="HttpServlet methods. Click on the + sign on the left to edit the code.">
*
/**
* @return a String containing servlet description
* Handles the HTTP <code>GET</code> method.
*/
*
@Override
* @param request
public
String
getServletInfo
()
{
* servlet request
return
"Short description"
;
* @param response
}
// </editor-fold>
* servlet response
* @throws ServletException
private
static
final
Color
RESERVED_COLOR
=
Color
.
RED
;
* if a servlet-specific error occurs
private
static
final
Color
SELECTED_COLOR
=
Color
.
BLUE
;
* @throws IOException
private
static
final
Color
OWNED_COLOR
=
Color
.
GREEN
;
* if an I/O error occurs
private
static
final
Color
BORDER_COLOR
=
Color
.
BLACK
;
*/
private
static
final
Color
LOCKED_COLOR
=
Color
.
DARK_GRAY
;
@Override
protected
void
doGet
(
HttpServletRequest
request
,
private
static
interface
PlaceColorPicker
{
HttpServletResponse
response
)
throws
ServletException
,
IOException
{
Color
getColor
(
Place
p
);
processRequest
(
request
,
response
);
}
}
public
static
class
TimelapsePicker
implements
PlaceColorPicker
{
/**
* Handles the HTTP <code>POST</code> method.
private
final
Date
date
;
*
* @param request
public
TimelapsePicker
(
Date
date
)
{
* servlet request
this
.
date
=
date
;
* @param response
}
* servlet response
* @throws ServletException
@Override
* if a servlet-specific error occurs
public
Color
getColor
(
Place
p
)
{
* @throws IOException
* if an I/O error occurs
if
(
p
.
getReserverSlot
()
!=
null
&&
date
.
after
(
p
.
getReserverSlot
().
getUsed
()))
{
*/
return
RESERVED_COLOR
;
@Override
}
protected
void
doPost
(
HttpServletRequest
request
,
return
null
;
HttpServletResponse
response
)
throws
ServletException
,
IOException
{
}
processRequest
(
request
,
response
);
}
}
public
static
class
LegacyColorPicker
implements
PlaceColorPicker
{
/**
private
final
EventUser
user
;
* Returns a short description of the servlet.
// private final boolean onlyFrame;
*
// private final boolean hilight;
* @return a String containing servlet description
private
final
List
<
Place
>
userplaces
;
*/
@Override
// public LegacyColorPicker(EventUser u, boolean onlyFrame, boolean
public
String
getServletInfo
()
{
// hilight) {
return
"Short description"
;
// this.user = u;
}
// </editor-fold>
// this.onlyFrame = onlyFrame;
// this.hilight = hilight;
private
static
final
Color
RESERVED_COLOR
=
Color
.
RED
;
// }
private
static
final
Color
SELECTED_COLOR
=
Color
.
BLUE
;
private
static
final
Color
OWNED_COLOR
=
Color
.
GREEN
;
public
LegacyColorPicker
(
EventUser
user2
,
List
<
Place
>
userplaces
)
{
private
static
final
Color
BORDER_COLOR
=
Color
.
BLACK
;
this
.
user
=
user2
;
private
static
final
Color
LOCKED_COLOR
=
Color
.
DARK_GRAY
;
this
.
userplaces
=
userplaces
;
private
static
void
drawPlace
(
Place
p
,
Graphics2D
g
,
EventUser
user
,
boolean
onlyFrame
,
boolean
hilight
)
{
}
if
(
p
.
isDisabled
())
{
return
;
public
Color
getColor
(
Place
p
)
{
}
Color
color
=
null
;
Color
color
=
null
;
// Check if we wan
if
(!
onlyFrame
)
{
if
(
userplaces
==
null
)
{
if
(!
p
.
isBuyable
())
{
if
(!
p
.
isBuyable
())
{
logger
.
debug
(
"Setting color as locked place."
);
logger
.
debug
(
"Setting color as locked place."
);
color
=
LOCKED_COLOR
;
color
=
LOCKED_COLOR
;
}
}
if
(
p
.
isReservedFor
(
user
))
{
if
(
p
.
isReservedFor
(
user
))
{
// logger.debug("Setting place selected {}", p);
// logger.debug("Setting place selected {}", p);
color
=
SELECTED_COLOR
;
color
=
SELECTED_COLOR
;
}
else
if
(
user
.
equals
(
p
.
getCurrentUser
())
}
else
if
(
user
.
equals
(
p
.
getCurrentUser
())
||
(
p
.
getGroup
()
!=
null
&&
user
.
equals
(
p
.
getGroup
().
getCreator
()))
||
(
p
.
getGroup
()
!=
null
&&
user
.
equals
(
p
.
getGroup
().
getCreator
()))
||
(
p
.
getPlaceReserver
()
!=
null
&&
user
.
equals
(
p
||
(
p
.
getPlaceReserver
()
!=
null
&&
user
.
equals
(
p
.
getPlaceReserver
().
getUser
())))
{
.
getPlaceReserver
().
getUser
())))
{
color
=
OWNED_COLOR
;
color
=
OWNED_COLOR
;
// logger.debug("Setting place owned {}", p);
// logger.debug("Setting place owned {}", p);
}
else
if
(
p
.
isTaken
())
{
}
else
if
(
p
.
isTaken
())
{
color
=
RESERVED_COLOR
;
color
=
RESERVED_COLOR
;
// logger.debug("Setting place Reserved {}", p);
// logger.debug("Setting place Reserved {}", p);
}
else
if
(
p
.
getProduct
()
!=
null
&&
p
.
getProduct
()
!=
null
&&
p
.
getProduct
().
getColor
()
!=
null
}
else
if
(
p
.
getProduct
()
!=
null
&&
p
.
getProduct
()
!=
null
&&
p
.
getProduct
().
getColor
()
!=
null
&&
!
p
.
getProduct
().
getColor
().
isEmpty
())
{
&&
!
p
.
getProduct
().
getColor
().
isEmpty
())
{
try
{
try
{
color
=
Color
.
decode
(
p
.
getProduct
().
getColor
());
color
=
Color
.
decode
(
p
.
getProduct
().
getColor
());
}
catch
(
NumberFormatException
x
)
{
}
catch
(
NumberFormatException
x
)
{
logger
.
error
(
"Cannot convert string {} to color."
,
p
.
getProduct
().
getColor
());
logger
.
error
(
"Cannot convert string {} to color."
,
p
.
getProduct
().
getColor
());
}
}
}
else
{
}
else
{
// too much debugging -TKjne
// too much debugging -TKjne
// logger.debug("Nothing special for this place. Color should be default.");
// logger.debug("Nothing special for this place. Color
// should be default.");
}
}
}
}
else
if
(
userplaces
.
contains
(
p
))
{
if
(
hilight
)
{
color
=
OWNED_COLOR
;
color
=
OWNED_COLOR
;
}
}
return
color
;
}
if
(
color
!=
null
)
{
}
//logger.debug("Setting fill color: {} for p", color, p);
g
.
setColor
(
color
);
;
g
.
fill
(
new
Rectangle
(
p
.
getMapX
()+
1
,
p
.
getMapY
()+
1
,
p
.
getWidth
()-
1
,
p
.
getHeight
()-
1
));
}
private
static
void
drawPlace
(
Place
p
,
Graphics2D
g
,
PlaceColorPicker
colorPicker
)
{
if
(
p
.
isDisabled
())
{
g
.
setColor
(
BORDER_COLOR
);
return
;
g
.
draw
(
new
Rectangle
(
p
.
getMapX
(),
p
.
getMapY
(),
p
.
getWidth
(),
p
.
getHeight
()));
}
}
Color
color
=
colorPicker
.
getColor
(
p
);
if
(
color
!=
null
)
{
// logger.debug("Setting fill color: {} for p", color, p);
g
.
setColor
(
color
);
g
.
fill
(
new
Rectangle
(
p
.
getMapX
()
+
1
,
p
.
getMapY
()
+
1
,
p
.
getWidth
()
-
1
,
p
.
getHeight
()
-
1
));
}
g
.
setColor
(
BORDER_COLOR
);
g
.
draw
(
new
Rectangle
(
p
.
getMapX
(),
p
.
getMapY
(),
p
.
getWidth
(),
p
.
getHeight
()));
}
}
}
Write
Preview
Markdown
is supported
Attach a file
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 post a comment