Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
erp5
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Labels
Merge Requests
141
Merge Requests
141
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Analytics
Analytics
CI / CD
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Jobs
Commits
Open sidebar
nexedi
erp5
Commits
807dc150
Commit
807dc150
authored
Apr 27, 2022
by
Arnaud Fontaine
Browse files
Options
Browse Files
Download
Plain Diff
Merge remote-tracking branch 'origin/master' into zope4py2
parents
f7d47011
593b6cf6
Pipeline
#21141
failed with stage
Changes
10
Pipelines
2
Hide whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
85 additions
and
199 deletions
+85
-199
bt5/erp5_km/SkinTemplateItem/portal_skins/erp5_km_theme/km_css/km.css.zpt
...TemplateItem/portal_skins/erp5_km_theme/km_css/km.css.zpt
+0
-28
bt5/erp5_km/SkinTemplateItem/portal_skins/erp5_km_theme/km_js/erp5_km.js.js
...mplateItem/portal_skins/erp5_km_theme/km_js/erp5_km.js.js
+0
-60
bt5/erp5_web_multiflex5_theme/SkinTemplateItem/portal_skins/erp5_web_multiflex5_theme/mf54_content.css.zpt
...rtal_skins/erp5_web_multiflex5_theme/mf54_content.css.zpt
+0
-41
product/CMFActivity/ActivityTool.py
product/CMFActivity/ActivityTool.py
+47
-25
product/CMFActivity/tests/testCMFActivity.py
product/CMFActivity/tests/testCMFActivity.py
+36
-0
product/ERP5/bootstrap/erp5_xhtml_style/SkinTemplateItem/portal_skins/erp5_xhtml_style/erp5.css.css
...inTemplateItem/portal_skins/erp5_xhtml_style/erp5.css.css
+0
-30
product/ERP5Type/XMLExportImport/OFSXMLExportImport.py
product/ERP5Type/XMLExportImport/OFSXMLExportImport.py
+0
-13
product/ERP5Type/XMLExportImport/ppml.py
product/ERP5Type/XMLExportImport/ppml.py
+1
-1
product/ERP5Type/XMLExportImport/xyap.py
product/ERP5Type/XMLExportImport/xyap.py
+1
-0
product/ERP5Type/ZopePatch.py
product/ERP5Type/ZopePatch.py
+0
-1
No files found.
bt5/erp5_km/SkinTemplateItem/portal_skins/erp5_km_theme/km_css/km.css.zpt
View file @
807dc150
...
@@ -643,34 +643,6 @@ div.download-document-format-list-menu ul li.toggle-hidden-format-dialog-selecti
...
@@ -643,34 +643,6 @@ div.download-document-format-list-menu ul li.toggle-hidden-format-dialog-selecti
height:20px;
height:20px;
}
}
/* DMS PDF navigation */
div.pdf-preview-navigation img{
width: 4px;
height: 8px;
margin-right: 4px;
background-repeat: no-repeat;
vertical-align: middle;
}
div.pdf-preview-navigation img.shaded{
opacity: 0.2;
}
div.pdf-preview-navigation img.first{
background-image:url("km_img/2leftarrowv.png");
}
div.pdf-preview-navigation img.previous{
background-image:url("km_img/1leftarrowv.png");
}
div.pdf-preview-navigation img.next{
background-image:url("km_img/1rightarrowv.png");
}
div.pdf-preview-navigation img.last{
background-image:url("km_img/2rightarrowv.png");
width: 5px;
}
/* Editable mode actions */
/* Editable mode actions */
.actions{
.actions{
...
...
bt5/erp5_km/SkinTemplateItem/portal_skins/erp5_km_theme/km_js/erp5_km.js.js
View file @
807dc150
...
@@ -146,66 +146,6 @@ function initialize_form(){
...
@@ -146,66 +146,6 @@ function initialize_form(){
}
}
}
}
// function make_pdf_navigation_asynchronous_form(){
// /*
// Make default PDF navigation in preview in asynchronous way.
// */
//
// function splitUrl(url){
// jQuery.url.setUrl(image_src)
// base_url = jQuery.url.attr("base") + jQuery.url.attr("path");
// query_dict = {"display": jQuery.url.param("display"),
// "format": jQuery.url.param("format"),
// "resolution:int": jQuery.url.param("resolution:int"),
// "frame": parseInt(jQuery.url.param("frame"))};
// query_string = $.param(query_dict);
// console.log(base_url);
// console.log(query_string);
// }
//
// function loadThumbnail(event){
// event.preventDefault();
// navigation_url = this.href;
//
// image = $("div.pdf-preview-content img");
// image_url = image.attr("src");
//
// // read current image URL
// jQuery.url.setUrl(image_url);
// base_url = jQuery.url.attr("base") + jQuery.url.attr("path");
// query_dict = {"display": jQuery.url.param("display"),
// "format": jQuery.url.param("format"),
// "resolution:int": jQuery.url.param("resolution:int")};
//
//
// // get frame index from navigation link
// jQuery.url.setUrl(navigation_url);
// frame = parseInt(jQuery.url.param("selection_index"))
// query_dict["frame"]=frame;
//
// // construct new thumbnail URL
// query_string = $.param(query_dict);
// new_image_url = base_url + "?" +query_string;
// console.log(new_image_url);
//
// image.attr("src", new_image_url);
//
// }
//
// pdf_preview_navigation = $('div.pdf-preview-navigation');
// if(pdf_preview_navigation.length){
//
// // XXX: set hooks
// // $("div.pdf-preview-navigation a img.first").parent("a").click(loadThumbnail);
// // $("div.pdf-preview-navigation a img.previous").parent("a").click(loadThumbnail);
// // $("div.pdf-preview-navigation a img.next").parent("a").click(loadThumbnail);
// // $("div.pdf-preview-navigation a img.last").parent("a").click(loadThumbnail);
//
// }
// }
// $(document).ready(make_pdf_navigation_asynchronous_form);
jQuery
.
fn
.
highlight
=
function
(
str
,
className
)
{
jQuery
.
fn
.
highlight
=
function
(
str
,
className
)
{
/*
/*
Highlight search word in HTML content.
Highlight search word in HTML content.
...
...
bt5/erp5_web_multiflex5_theme/SkinTemplateItem/portal_skins/erp5_web_multiflex5_theme/mf54_content.css.zpt
View file @
807dc150
...
@@ -469,16 +469,6 @@ div.bottom_actions button#input-save-view span.image {
...
@@ -469,16 +469,6 @@ div.bottom_actions button#input-save-view span.image {
background-image: url('<tal:block tal:replace="site_url"/>/images/save-preview.png');
background-image: url('<tal:block tal:replace="site_url"/>/images/save-preview.png');
}
}
div.pdf-preview-navigation {
font-size:120%;
text-align:center;
}
div.pdf-preview-navigation img {
float:none;
margin:0;
}
div.error > div > input,
div.error > div > input,
div.error > div > textarea {
div.error > div > textarea {
border:1px solid red;
border:1px solid red;
...
@@ -687,35 +677,4 @@ div#main_content table.listbox tfoot .pageNavigation button:hover {
...
@@ -687,35 +677,4 @@ div#main_content table.listbox tfoot .pageNavigation button:hover {
background: #eee;
background: #eee;
}
}
/* DMS PDF navigation
XXX: probably part of erp5_dms.bt5 */
div.pdf-preview-navigation img{
width: 22px;
height: 22px;
margin-right: 1px;
background-repeat: no-repeat;
vertical-align: middle;
}
div.pdf-preview-navigation img.shaded{
opacity: 0.2;
}
div.pdf-preview-navigation img.first{
background-image:url("images/2leftarrowb.png");
}
div.pdf-preview-navigation img.previous{
background-image:url("images/1leftarrowb.png");
}
div.pdf-preview-navigation img.next{
background-image:url("images/1rightarrowb.png");
}
div.pdf-preview-navigation img.last{
background-image:url("images/2rightarrowb.png");
}
</tal:block>
</tal:block>
\ No newline at end of file
product/CMFActivity/ActivityTool.py
View file @
807dc150
...
@@ -27,6 +27,7 @@ from __future__ import absolute_import
...
@@ -27,6 +27,7 @@ from __future__ import absolute_import
#
#
##############################################################################
##############################################################################
import
copy
import
socket
import
socket
import
urllib
import
urllib
import
threading
import
threading
...
@@ -195,6 +196,9 @@ class Message(BaseMessage):
...
@@ -195,6 +196,9 @@ class Message(BaseMessage):
exc_type
=
None
exc_type
=
None
is_executed
=
MESSAGE_NOT_EXECUTED
is_executed
=
MESSAGE_NOT_EXECUTED
traceback
=
None
traceback
=
None
user_name
=
None
user_object
=
None
user_folder_path
=
None
document_uid
=
None
document_uid
=
None
is_registered
=
False
is_registered
=
False
line
=
None
line
=
None
...
@@ -224,7 +228,14 @@ class Message(BaseMessage):
...
@@ -224,7 +228,14 @@ class Message(BaseMessage):
# was generated.
# was generated.
# Strip last stack entry, since it will always be the same.
# Strip last stack entry, since it will always be the same.
self
.
call_traceback
=
''
.
join
(
format_list
(
extract_stack
()[:
-
1
]))
self
.
call_traceback
=
''
.
join
(
format_list
(
extract_stack
()[:
-
1
]))
self
.
user_name
=
getSecurityManager
().
getUser
().
getIdOrUserName
()
user
=
getSecurityManager
().
getUser
()
self
.
user_object
=
copy
.
deepcopy
(
aq_base
(
user
))
# Note: userfolders are not ERP5 objects, so use OFS API.
self
.
user_folder_path
=
getattr
(
aq_parent
(
user
),
'getPhysicalPath'
,
lambda
:
None
,
)()
# Store REQUEST Info
# Store REQUEST Info
self
.
request_info
=
{}
self
.
request_info
=
{}
if
request
is
not
None
:
if
request
is
not
None
:
...
@@ -298,31 +309,42 @@ class Message(BaseMessage):
...
@@ -298,31 +309,42 @@ class Message(BaseMessage):
pass
pass
return
1
return
1
def
changeUser
(
self
,
user_name
,
activity_tool
):
def
changeUser
(
self
,
activity_tool
):
"""restore the security context for the calling user."""
"""restore the security context for the calling user."""
portal
=
activity_tool
.
getPortalObject
()
portal
=
activity_tool
.
getPortalObject
()
portal_uf
=
portal
.
acl_users
user
=
self
.
user_object
uf
=
portal_uf
if
user
is
None
and
self
.
user_name
is
not
None
:
# BBB
user
=
uf
.
getUserById
(
user_name
)
user_name
=
self
.
user_name
# if the user is not found, try to get it from a parent acl_users
user_folder
=
portal_user_folder
=
portal
.
acl_users
# XXX this is still far from perfect, because we need to store all
user
=
user_folder
.
getUserById
(
user_name
)
# information about the user (like original user folder, roles) to
# if the user is not found, try to get it from a parent acl_users
# replay the activity with exactly the same security context as if
# XXX this is still far from perfect, because we need to store all
# it had been executed without activity.
# information about the user (like original user folder, roles) to
if
user
is
None
:
# replay the activity with exactly the same security context as if
uf
=
portal
.
aq_parent
.
acl_users
# it had been executed without activity.
user
=
uf
.
getUserById
(
user_name
)
if
user
is
None
:
if
user
is
None
and
user_name
==
system_user
.
getUserName
():
user_folder
=
portal
.
aq_parent
.
acl_users
# The following logic partly comes from unrestricted_apply()
user
=
user_folder
.
getUserById
(
user_name
)
# implementation in ERP5Type.UnrestrictedMethod but we get roles
if
user
is
None
and
user_name
==
system_user
.
getUserName
():
# from the portal to have more roles.
# The following logic partly comes from unrestricted_apply()
uf
=
portal_uf
# implementation in ERP5Type.UnrestrictedMethod but we get roles
role_list
=
uf
.
valid_roles
()
# from the portal to have more roles.
user
=
PrivilegedUser
(
user_name
,
None
,
role_list
,
()).
__of__
(
uf
)
user_folder
=
portal_user_folder
user
=
PrivilegedUser
(
user_name
,
None
,
user_folder
.
valid_roles
(),
(),
)
else
:
user_folder
=
portal
.
getPhysicalRoot
().
unrestrictedTraverse
(
self
.
user_folder_path
,
)
user_name
=
user
.
getIdOrUserName
()
if
user
is
not
None
:
if
user
is
not
None
:
user
=
user
.
__of__
(
u
f
)
user
=
user
.
__of__
(
u
ser_folder
)
newSecurityManager
(
None
,
user
)
newSecurityManager
(
None
,
user
)
transaction
.
get
().
setUser
(
user_name
,
'/'
.
join
(
u
f
.
getPhysicalPath
()))
transaction
.
get
().
setUser
(
user_name
,
'/'
.
join
(
u
ser_folder
.
getPhysicalPath
()))
else
:
else
:
LOG
(
"CMFActivity"
,
WARNING
,
LOG
(
"CMFActivity"
,
WARNING
,
"Unable to find user %r in the portal"
%
user_name
)
"Unable to find user %r in the portal"
%
user_name
)
...
@@ -347,7 +369,7 @@ class Message(BaseMessage):
...
@@ -347,7 +369,7 @@ class Message(BaseMessage):
try
:
try
:
# Change user if required (TO BE DONE)
# Change user if required (TO BE DONE)
# We will change the user only in order to execute this method
# We will change the user only in order to execute this method
self
.
changeUser
(
self
.
user_name
,
activity_tool
)
self
.
changeUser
(
activity_tool
)
# XXX: There is no check to see if user is allowed to access
# XXX: There is no check to see if user is allowed to access
# that method !
# that method !
method
=
getattr
(
obj
,
self
.
method_id
)
method
=
getattr
(
obj
,
self
.
method_id
)
...
@@ -420,7 +442,7 @@ Named Parameters: %r
...
@@ -420,7 +442,7 @@ Named Parameters: %r
try
:
try
:
# Change user if required (TO BE DONE)
# Change user if required (TO BE DONE)
# We will change the user only in order to execute this method
# We will change the user only in order to execute this method
user
=
self
.
changeUser
(
self
.
user_name
,
activity_tool
)
user
=
self
.
changeUser
(
activity_tool
)
active_obj
=
obj
.
activate
(
activity
=
activity
,
**
self
.
activity_kw
)
active_obj
=
obj
.
activate
(
activity
=
activity
,
**
self
.
activity_kw
)
getattr
(
active_obj
,
self
.
method_id
)(
*
self
.
args
,
**
self
.
kw
)
getattr
(
active_obj
,
self
.
method_id
)(
*
self
.
args
,
**
self
.
kw
)
finally
:
finally
:
...
@@ -1664,7 +1686,7 @@ class ActivityTool (BaseTool):
...
@@ -1664,7 +1686,7 @@ class ActivityTool (BaseTool):
message
=
m
.
_message
message
=
m
.
_message
if
user_name
!=
message
.
user_name
:
if
user_name
!=
message
.
user_name
:
user_name
=
message
.
user_name
user_name
=
message
.
user_name
message
.
changeUser
(
user_name
,
m
.
object
)
message
.
changeUser
(
m
.
object
)
m
.
result
=
getattr
(
m
.
object
,
method_id
)(
*
m
.
args
,
**
m
.
kw
)
m
.
result
=
getattr
(
m
.
object
,
method_id
)(
*
m
.
args
,
**
m
.
kw
)
except
Exception
:
except
Exception
:
m
.
raised
()
m
.
raised
()
...
...
product/CMFActivity/tests/testCMFActivity.py
View file @
807dc150
...
@@ -30,6 +30,10 @@ import inspect
...
@@ -30,6 +30,10 @@ import inspect
import
warnings
import
warnings
from
functools
import
wraps
from
functools
import
wraps
from
itertools
import
product
from
itertools
import
product
from
AccessControl.SecurityManagement
import
getSecurityManager
from
AccessControl.SecurityManagement
import
setSecurityManager
from
AccessControl.SecurityManagement
import
newSecurityManager
from
Acquisition
import
aq_base
,
aq_parent
from
Products.ERP5Type.tests.utils
import
LogInterceptor
from
Products.ERP5Type.tests.utils
import
LogInterceptor
from
Testing
import
ZopeTestCase
from
Testing
import
ZopeTestCase
from
Products.ERP5Type.tests.ERP5TypeTestCase
import
ERP5TypeTestCase
from
Products.ERP5Type.tests.ERP5TypeTestCase
import
ERP5TypeTestCase
...
@@ -40,6 +44,7 @@ from Products.CMFActivity.Activity.SQLBase import INVOKE_ERROR_STATE
...
@@ -40,6 +44,7 @@ from Products.CMFActivity.Activity.SQLBase import INVOKE_ERROR_STATE
from
Products.CMFActivity.Activity.Queue
import
VALIDATION_ERROR_DELAY
from
Products.CMFActivity.Activity.Queue
import
VALIDATION_ERROR_DELAY
from
Products.CMFActivity.Activity.SQLDict
import
SQLDict
from
Products.CMFActivity.Activity.SQLDict
import
SQLDict
from
Products.CMFActivity.Errors
import
ActivityPendingError
,
ActivityFlushError
from
Products.CMFActivity.Errors
import
ActivityPendingError
,
ActivityFlushError
from
Products.PluggableAuthService.PropertiedUser
import
PropertiedUser
from
erp5.portal_type
import
Organisation
from
erp5.portal_type
import
Organisation
from
AccessControl.SecurityManagement
import
newSecurityManager
from
AccessControl.SecurityManagement
import
newSecurityManager
from
zLOG
import
LOG
from
zLOG
import
LOG
...
@@ -2798,3 +2803,34 @@ return [x.getObject() for x in context.portal_catalog(limit=100)]
...
@@ -2798,3 +2803,34 @@ return [x.getObject() for x in context.portal_catalog(limit=100)]
self
.
portal
.
portal_activities
.
manageActivitiesAdvanced
()
self
.
portal
.
portal_activities
.
manageActivitiesAdvanced
()
self
.
portal
.
portal_activities
.
manageLoadBalancing
()
self
.
portal
.
portal_activities
.
manageLoadBalancing
()
self
.
assertEqual
(
catched_warnings
,
[])
self
.
assertEqual
(
catched_warnings
,
[])
@
for_each_activity
def
testSpawnTimeUserGroupAndRoleUsedDuringExecution
(
self
,
activity
):
obj
=
self
.
portal
.
organisation_module
.
newContent
(
portal_type
=
'Organisation'
)
self
.
tic
()
# This user cannot be created by userfolder API, validating that activity
# execution does not use it.
# Using a PropertiedUser because it is the lowest-level class which has a
# groups notion.
artificial_user
=
PropertiedUser
(
id
=
'this user does not exist'
,
login
=
'does not matter'
,
).
__of__
(
self
.
portal
.
acl_users
)
artificial_user
.
_addGroups
(
groups
=
(
'group 1'
,
'group 2'
))
artificial_user
.
_addRoles
(
roles
=
(
'role 1'
,
'role 2'
))
initial_security_manager
=
getSecurityManager
()
def
checkUserGroupAndRole
(
organisation_self
):
user
=
getSecurityManager
().
getUser
()
self
.
assertIs
(
type
(
aq_base
(
user
)),
PropertiedUser
)
self
.
assertEqual
(
aq_parent
(
user
),
aq_parent
(
artificial_user
))
self
.
assertEqual
(
user
.
getId
(),
artificial_user
.
getId
())
self
.
assertItemsEqual
(
user
.
getGroups
(),
artificial_user
.
getGroups
())
self
.
assertItemsEqual
(
user
.
getRoles
(),
artificial_user
.
getRoles
())
Organisation
.
checkUserGroupAndRole
=
checkUserGroupAndRole
try
:
newSecurityManager
(
None
,
artificial_user
)
obj
.
activate
(
activity
=
activity
).
checkUserGroupAndRole
()
self
.
tic
()
finally
:
setSecurityManager
(
initial_security_manager
)
del
Organisation
.
checkUserGroupAndRole
product/ERP5/bootstrap/erp5_xhtml_style/SkinTemplateItem/portal_skins/erp5_xhtml_style/erp5.css.css
View file @
807dc150
...
@@ -1162,36 +1162,6 @@ div.search .searchPages .selected{
...
@@ -1162,36 +1162,6 @@ div.search .searchPages .selected{
display
:
none
;
display
:
none
;
}
}
/* DMS PDF navigation
XXX: probably part of erp5_dms.bt5 */
div
.pdf-preview-navigation
img
{
width
:
22px
;
height
:
22px
;
margin-right
:
1px
;
background-repeat
:
no-repeat
;
vertical-align
:
middle
;
}
div
.pdf-preview-navigation
img
.shaded
{
opacity
:
0.2
;
}
div
.pdf-preview-navigation
img
.first
{
background-image
:
url("images/2leftarrowb.png")
;
}
div
.pdf-preview-navigation
img
.previous
{
background-image
:
url("images/1leftarrowb.png")
;
}
div
.pdf-preview-navigation
img
.next
{
background-image
:
url("images/1rightarrowb.png")
;
}
div
.pdf-preview-navigation
img
.last
{
background-image
:
url("images/2rightarrowb.png")
;
}
.horizontal_align_form_box
>
div
.input
{
.horizontal_align_form_box
>
div
.input
{
float
:
right
;
float
:
right
;
width
:
70%
;
/* because label width is 30%*/
width
:
70%
;
/* because label width is 30%*/
...
...
product/ERP5Type/XMLExportImport/OFSXMLExportImport.py
deleted
100644 → 0
View file @
f7d47011
##############################################################################
#
# Copyright (c) 2001,2002 Zope Foundation and Contributors.
# Copyright (c) 2002,2005 Nexedi SARL and Contributors. All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE
#
##############################################################################
product/ERP5Type/XMLExportImport/ppml.py
View file @
807dc150
# -*- coding: utf-8 -*-
##############################################################################
##############################################################################
#
#
# Copyright (c) 2001,2002 Zope Corporation and Contributors. All Rights Reserved.
# Copyright (c) 2001,2002 Zope Corporation and Contributors. All Rights Reserved.
...
@@ -22,7 +23,6 @@ from marshal import loads as mloads
...
@@ -22,7 +23,6 @@ from marshal import loads as mloads
from
.xyap
import
NoBlanks
from
.xyap
import
NoBlanks
from
.xyap
import
xyap
from
.xyap
import
xyap
import
re
from
marshal
import
dumps
as
mdumps
from
marshal
import
dumps
as
mdumps
#from zLOG import LOG
#from zLOG import LOG
...
...
product/ERP5Type/XMLExportImport/xyap.py
View file @
807dc150
# -*- coding: utf-8 -*-
"""Yet another XML parser
"""Yet another XML parser
This is meant to be very simple:
This is meant to be very simple:
...
...
product/ERP5Type/ZopePatch.py
View file @
807dc150
...
@@ -38,7 +38,6 @@ if WITH_LEGACY_WORKFLOW:
...
@@ -38,7 +38,6 @@ if WITH_LEGACY_WORKFLOW:
from
Products.ERP5Type.patches
import
BTreeFolder2
from
Products.ERP5Type.patches
import
BTreeFolder2
if
WITH_LEGACY_WORKFLOW
:
if
WITH_LEGACY_WORKFLOW
:
from
Products.ERP5Type.patches
import
WorkflowTool
from
Products.ERP5Type.patches
import
WorkflowTool
from
Products.ERP5Type.patches
import
WorkflowTool
from
Products.ERP5Type.patches
import
DynamicType
from
Products.ERP5Type.patches
import
DynamicType
from
Products.ERP5Type.patches
import
Expression
from
Products.ERP5Type.patches
import
Expression
from
Products.ERP5Type.patches
import
sqltest
from
Products.ERP5Type.patches
import
sqltest
...
...
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