Commit 77faa9f3 authored by Romain Courteaud's avatar Romain Courteaud

Bug fix: make the ProxyField compatible with the RelationField.

Remove useless ProxyField properties.
Modify code indentation.


git-svn-id: https://svn.erp5.org/repos/public/erp5/trunk@5896 20353a03-c40f-0410-a6d1-a30d3c3de9de
parent 127cbb83
......@@ -36,6 +36,7 @@ from Products.ERP5Form import RelationField
from Products.ERP5Form.RelationField import MAX_SELECT, new_content_prefix
from Globals import get_request
from Products.PythonScripts.Utility import allow_class
from AccessControl import ClassSecurityInfo
import string
from zLOG import LOG
......@@ -505,17 +506,25 @@ class MultiRelationStringFieldValidator(Validator.LinesValidator, RelationField
# Can return editor
return MultiRelationEditor(field.id, base_category, portal_type, portal_type_item, catalog_index, relation_setter_id, relation_editor_list)
MultiRelationStringFieldWidgetInstance = MultiRelationStringFieldWidget()
MultiRelationStringFieldValidatorInstance = MultiRelationStringFieldValidator()
class MultiRelationStringField(ZMIField):
meta_type = "MultiRelationStringField"
is_relation_field = 1
security = ClassSecurityInfo()
widget = MultiRelationStringFieldWidgetInstance
validator = MultiRelationStringFieldValidatorInstance
security.declareProtected('Access contents information', 'get_value')
def get_value(self, id, **kw):
"""Get value for id.
Optionally pass keyword arguments that get passed to TALES
expression.
"""
if id == 'is_relation_field':
result = 1
else:
result = ZMIField.get_value(self, id, **kw)
return result
##############################################################################
#
# Copyright (c) 2002 Nexedi SARL and Contributors. All Rights Reserved.
# Copyright (c) 2006 Nexedi SARL and Contributors. All Rights Reserved.
# Jean-Paul Smets <jp@nexedi.com>
#
# WARNING: This program as such is intended to be used by professional
......@@ -40,9 +40,11 @@ from Products.PythonScripts.Utility import allow_class
from Products.PythonScripts.standard import url_quote_plus
from AccessControl import ClassSecurityInfo
import string
from zLOG import LOG, WARNING
from Acquisition import aq_base, aq_inner, aq_acquire, aq_chain
class ProxyWidget(Widget.Widget):
"""
......@@ -52,7 +54,7 @@ class ProxyWidget(Widget.Widget):
are defined in order to minimize code duplication.
"""
property_names = Widget.Widget.property_names + [
property_names = [
'form_id',
'field_id',
'extra_context',
......@@ -61,27 +63,21 @@ class ProxyWidget(Widget.Widget):
form_id = fields.StringField(
'form_id',
title='Form ID',
description=(
"ID of the master form."),
description= \
"ID of the master form.",
default="",
required=1)
field_id = fields.StringField(
'field_id',
title='Field ID',
description=(
"ID of the field in the master form."),
description= \
"ID of the field in the master form.",
default="",
required=1)
default = fields.StringField(
'default',
title='Default',
description=(
"Default value."),
default="",
required=0)
# XXX FIXME This seems against the definition of proxy field...
# Remove it as soon as possible
extra_context = fields.ListTextAreaField(
'extra_context',
title='Extra Context',
......@@ -93,58 +89,32 @@ class ProxyWidget(Widget.Widget):
"""
Render proxy field
"""
form = field.aq_parent
try:
proxy_form = getattr(form, field.get_value('form_id'))
proxy_field = getattr(proxy_form, field.get_value('field_id'))
except AttributeError:
LOG('ProxyField', WARNING,
'could not get a field from a proxy field %s in %s' % \
(field.id, form.id))
return ''
extra_context = REQUEST.other.get('erp5_extra_context', {})
for k, v in field.get_value('extra_context'):
extra_context[k] = v
REQUEST.other['erp5_extra_context'] = extra_context
return proxy_field.widget.render(proxy_field, key, value, REQUEST)
result = ''
proxy_field = field.getTemplateField()
if proxy_field is not None:
REQUEST = field.updateContext(REQUEST)
result = proxy_field.widget.render(proxy_field, key, value, REQUEST)
return result
def render_view(self, field, value):
"""
Display proxy field
"""
if type(value) == type('') and value == '':
return ''
form = field.aq_parent
try:
proxy_form = getattr(form, field.get_value('form_id'))
proxy_field = getattr(proxy_form, field.get_value('field_id'))
except AttributeError:
LOG('ProxyField', WARNING,
'could not get a field from a proxy field %s in %s' % \
(field.id, form.id))
return ''
REQUEST = get_request()
extra_context = REQUEST.other.get('erp5_extra_context', {})
for k, v in field.get_value('extra_context'):
extra_context[k] = v
REQUEST.other['erp5_extra_context'] = extra_context
return proxy_field.widget.render_view(proxy_field, value)
result = ''
proxy_field = field.getTemplateField()
if proxy_field is not None:
result = proxy_field.widget.render_view(proxy_field, value)
return result
class ProxyValidator(Validator.Validator):
"""
Validation of entered value through proxy field
"""
property_names = Validator.Validator.property_names
property_names = []
def validate(self, field, key, REQUEST):
form = field.aq_parent
proxy_form = getattr(form, field.get_value('form_id'))
proxy_field = getattr(proxy_form, field.get_value('field_id'))
extra_context = REQUEST.other.get('erp5_extra_context', {})
for k, v in field.get_value('extra_context'):
extra_context[k] = v
REQUEST.other['erp5_extra_context'] = extra_context
proxy_field = field.getTemplateField()
REQUEST = field.updateContext(REQUEST)
try:
result = proxy_field.validator.validate(proxy_field, key, REQUEST)
except ValidationError, error:
......@@ -157,22 +127,49 @@ ProxyValidatorInstance = ProxyValidator()
class ProxyField(ZMIField):
meta_type = "ProxyField"
security = ClassSecurityInfo()
widget = ProxyWidgetInstance
validator = ProxyValidatorInstance
def _get_default(self, key, value, REQUEST):
def getTemplateField(self):
"""
Return default value of the field.
Return template field of the proxy field.
"""
value = ZMIField._get_default(self, key, value, REQUEST)
if value in (None, ''):
form = self.aq_parent
object = form.aq_parent
try:
proxy_form = getattr(form, self.get_value('form_id'))
proxy_self = getattr(proxy_form, self.get_value('field_id'))
proxy_form = getattr(object, self.get_value('form_id'))
proxy_field = aq_base(getattr(proxy_form, self.get_value('field_id')))
proxy_field = proxy_field.__of__(form)
except AttributeError:
pass
LOG('ProxyField', WARNING,
'Could not get a field from a proxy field %s in %s' % \
(self.id, object.id))
proxy_field = None
return proxy_field
def updateContext(self, REQUEST):
"""
Update the REQUEST
"""
extra_context = REQUEST.other.get('erp5_extra_context', {})
for k, v in self.get_value('extra_context'):
extra_context[k] = v
REQUEST.other['erp5_extra_context'] = extra_context
return REQUEST
security.declareProtected('Access contents information', 'get_value')
def get_value(self, id, **kw):
"""Get value for id.
Optionally pass keyword arguments that get passed to TALES
expression.
"""
if id in self.widget.property_names:
result = ZMIField.get_value(self, id, **kw)
else:
value = proxy_self.get_value('default', REQUEST=REQUEST)
return value
proxy_field = self.getTemplateField()
if proxy_field is not None:
result = proxy_field.get_value(id, **kw)
return result
......@@ -36,6 +36,7 @@ from Products.PythonScripts.Utility import allow_class
import string
from AccessControl import ClassSecurityInfo
from zLOG import LOG
MAX_SELECT = 30 # Max. number of catalog result
new_content_prefix = '_newContent_'
......@@ -44,16 +45,13 @@ new_content_prefix = '_newContent_'
class RelationStringFieldWidget(Widget.TextWidget, Widget.ListWidget):
"""
RelationStringField widget
Works like a string field but includes one buttons
- one search button which updates the field and sets a relation
- creates object if not there
"""
property_names = Widget.TextWidget.property_names + \
['update_method', 'jump_method', 'allow_jump', 'base_category', 'portal_type', 'allow_creation', 'container_getter_id', 'catalog_index',
['update_method', 'jump_method', 'allow_jump', 'base_category',
'portal_type', 'allow_creation', 'container_getter_id', 'catalog_index',
'relation_setter_id', 'columns','sort','parameter_list','list_method',
'first_item', 'items', 'size', 'extra_item']
......@@ -159,7 +157,6 @@ class RelationStringFieldWidget(Widget.TextWidget, Widget.ListWidget):
default='',
required=0)
def render(self, field, key, value, REQUEST):
"""Render text input field.
"""
......@@ -169,7 +166,8 @@ class RelationStringFieldWidget(Widget.TextWidget, Widget.ListWidget):
portal_url = getToolByName(here, 'portal_url')
portal_url_string = portal_url()
portal_object = portal_url.getPortalObject()
html_string = Widget.TextWidget.render(self, field, key, value, REQUEST)
html_string = Widget.TextWidget.render(self, field, key, value,
REQUEST)
if REQUEST.has_key(relation_item_id):
# Define default tales on the fly
......@@ -189,20 +187,39 @@ class RelationStringFieldWidget(Widget.TextWidget, Widget.ListWidget):
#elif value != field.get_value('default'):
else:
html_string += '&nbsp;<input type="image" src="%s/images/exec16.png" value="update..." name="%s/portal_selections/viewSearchRelatedDocumentDialog%s:method"/>' \
% (portal_url_string, portal_object.getPath(),
html_string += \
'&nbsp;<input type="image" ' \
'src="%s/images/exec16.png" value="update..." ' \
'name="%s/portal_selections/viewSearchRelatedDocumentDialog%s' \
':method"/>' % \
(portal_url_string, portal_object.getPath(),
getattr(field.aq_parent, '_v_relation_field_index', 0))
relation_field_index = getattr(field.aq_parent, '_v_relation_field_index', 0)
field.aq_parent._v_relation_field_index = relation_field_index + 1 # Increase index
if value not in ( None, '' ) and not REQUEST.has_key(relation_item_id) and value == field.get_value('default') and field.get_value('allow_jump') == 1 :
relation_field_index = getattr(field.aq_parent,
'_v_relation_field_index', 0)
# Increase index
field.aq_parent._v_relation_field_index = relation_field_index + 1
# import pdb; pdb.set_trace()
# field.get_value('default')
if (value not in ( None, '' )) and \
(not REQUEST.has_key(relation_item_id)) and \
(value == field.get_value('default')) and \
(field.get_value('allow_jump') == 1):
if REQUEST.get('selection_name') is not None:
html_string += '&nbsp;&nbsp;<a href="%s/%s?field_id=%s&form_id=%s&selection_name=%s&selection_index=%s"><img src="%s/images/jump.png"/></a>' \
% (here.absolute_url(), field.get_value('jump_method'), field.id, field.aq_parent.id, REQUEST.get('selection_name'), REQUEST.get('selection_index'),portal_url_string)
html_string += '&nbsp;&nbsp;<a href="%s/%s?field_id=%s&' \
'form_id=%s&selection_name=%s&selection_index=%s' \
'"><img src="%s/images/jump.png"/></a>' % \
(here.absolute_url(), field.get_value('jump_method'),
field.id, field.aq_parent.id,
REQUEST.get('selection_name'),
REQUEST.get('selection_index'),
portal_url_string)
else:
html_string += '&nbsp;&nbsp;<a href="%s/%s?field_id=%s&form_id=%s"><img src="%s/images/jump.png"/></a>' \
% (here.absolute_url(), field.get_value('jump_method'), field.id, field.aq_parent.id,portal_url_string)
html_string += '&nbsp;&nbsp;<a href="%s/%s?field_id=%s&' \
'form_id=%s"><img src="%s/images/jump.png"/></a>' \
% (here.absolute_url(), field.get_value('jump_method'),
field.id, field.aq_parent.id,portal_url_string)
return html_string
def render_view(self, field, value):
......@@ -216,9 +233,12 @@ class RelationStringFieldWidget(Widget.TextWidget, Widget.ListWidget):
portal_url_string = getToolByName(here, 'portal_url')()
if value not in ('', None):
html_string = '<a href="%s/%s?field_id=%s&form_id=%s">%s</a>' \
% (here.absolute_url(), field.get_value('jump_method'), field.id, field.aq_parent.id, html_string)
html_string += '&nbsp;&nbsp;<a href="%s/%s?field_id=%s&form_id=%s"><img src="%s/images/jump.png"/></a>' \
% (here.absolute_url(), field.get_value('jump_method'), field.id, field.aq_parent.id, portal_url_string)
% (here.absolute_url(), field.get_value('jump_method'),
field.id, field.aq_parent.id, html_string)
html_string += '&nbsp;&nbsp;<a href="%s/%s?field_id=%s&form_id=%s">' \
'<img src="%s/images/jump.png"/></a>' % \
(here.absolute_url(), field.get_value('jump_method'),
field.id, field.aq_parent.id, portal_url_string)
return html_string
class RelationEditor:
......@@ -226,9 +246,9 @@ class RelationEditor:
A class holding all values required to update a relation
"""
def __init__(self, field_id, base_category, portal_type, uid, portal_type_item,
key, value, relation_setter_id, container_getter_id,
display_text):
def __init__(self, field_id, base_category, portal_type, uid,
portal_type_item, key, value, relation_setter_id,
container_getter_id, display_text):
self.field_id = field_id
self.uid = uid
self.base_category = base_category
......@@ -248,7 +268,8 @@ class RelationEditor:
relation_item_id = 'item_%s' % self.field_id
REQUEST.set(relation_item_id, ((self.display_text, self.uid),))
REQUEST.set(relation_field_id, self.uid)
REQUEST.set(self.field_id[len('field_'):], self.value) # XXX Dirty
# XXX Dirty
REQUEST.set(self.field_id[len('field_'):], self.value)
else:
# Make sure no default value appears
REQUEST.set(self.field_id[len('field_'):], None)
......@@ -258,14 +279,16 @@ class RelationEditor:
def edit(self, o):
if self.uid is not None:
if type(self.uid) is type('a') and self.uid.startswith(new_content_prefix):
if type(self.uid) is type('a') and \
self.uid.startswith(new_content_prefix):
# Create a new content
portal_type = self.uid[len(new_content_prefix):]
container = None
for p_item in self.portal_type_item:
if p_item[0] == portal_type:
if self.container_getter_id:
container = getattr(o, self.container_getter_id)(portal_type = portal_type)
container = getattr(o, self.container_getter_id)(
portal_type=portal_type)
else:
portal_module = o.getPortalObject().getDefaultModuleId( p_item[0] )
container = getattr(o.getPortalObject(), portal_module)
......@@ -310,7 +333,8 @@ class RelationStringFieldValidator(Validator.StringValidator):
"""
message_names = Validator.StringValidator.message_names +\
['relation_result_too_long', 'relation_result_ambiguous', 'relation_result_empty',]
['relation_result_too_long', 'relation_result_ambiguous',
'relation_result_empty',]
relation_result_too_long = "Too many documents were found."
relation_result_ambiguous = "Select appropriate document in the list."
......@@ -369,16 +393,17 @@ class RelationStringFieldValidator(Validator.StringValidator):
display_text = 'Object has been deleted'
return RelationEditor(key, base_category, portal_type, relation_uid,
portal_type_item, catalog_index, value,
relation_setter_id, container_getter_id, display_text)
relation_setter_id, container_getter_id,
display_text)
# We must be able to erase the relation
if value == '':
display_text = 'Delete the relation'
# Will be interpreted by Base_edit as "delete relation"
# (with no uid and value = '')
return RelationEditor(key, base_category, portal_type, None,
portal_type_item, catalog_index, value,
relation_setter_id, container_getter_id, display_text)
# Will be interpreted by Base_edit as "delete relation" (with no uid and value = '')
kw ={}
kw[catalog_index] = value
......@@ -407,7 +432,8 @@ class RelationStringFieldValidator(Validator.StringValidator):
else:
display_text = 'Object has been deleted'
return RelationEditor(key, base_category, portal_type, relation_uid,
return RelationEditor(
key, base_category, portal_type, relation_uid,
portal_type_item, catalog_index, value,
relation_setter_id, container_getter_id, display_text)
# If the length is 0, raise an error
......@@ -419,8 +445,8 @@ class RelationStringFieldValidator(Validator.StringValidator):
# If the length is short, raise an error
elif len(relation_list) < MAX_SELECT:
#menu_item_list += [('-', '')]
menu_item_list += map(lambda x: (x.getObject().getProperty(catalog_index), x.uid),
relation_list)
menu_item_list.extend([(x.getObject().getProperty(catalog_index),
x.uid) for x in relation_list])
REQUEST.set(relation_item_id, menu_item_list)
self.raise_error('relation_result_ambiguous', field)
else:
......@@ -435,10 +461,20 @@ RelationStringFieldValidatorInstance = RelationStringFieldValidator()
class RelationStringField(ZMIField):
meta_type = "RelationStringField"
is_relation_field = 1
security = ClassSecurityInfo()
widget = RelationStringFieldWidgetInstance
validator = RelationStringFieldValidatorInstance
security.declareProtected('Access contents information', 'get_value')
def get_value(self, id, **kw):
"""Get value for id.
Optionally pass keyword arguments that get passed to TALES
expression.
"""
if id == 'is_relation_field':
result = 1
else:
result = ZMIField.get_value(self, id, **kw)
return result
......@@ -920,10 +920,10 @@ class SelectionTool( UniqueObject, SimpleItem ):
return object
# Related document searching
def viewSearchRelatedDocumentDialog(self, index, form_id, REQUEST=None, sub_index=None, **kw):
def viewSearchRelatedDocumentDialog(self, index, form_id, REQUEST=None,
sub_index=None, **kw):
"""
Returns a search related document dialog
A set of forwarders us defined to circumvent limitations of HTML
"""
if sub_index != None:
......@@ -945,10 +945,12 @@ class SelectionTool( UniqueObject, SimpleItem ):
o.immediateReindexObject()
object_uid = o.getUid()
else:
return "Sorrry, Error, the calling object was not catalogued. Do not know how to do ?"
return "Sorrry, Error, the calling object was not catalogued. " \
"Do not know how to do ?"
# Find the field which was clicked on
form = getattr(o, form_id) # Important to get from the object instead of self
# Important to get from the object instead of self
form = getattr(o, form_id)
field = None
relation_index = 0
......@@ -959,13 +961,39 @@ class SelectionTool( UniqueObject, SimpleItem ):
for field in form.get_fields(include_disabled=0):
if field.get_value('editable',REQUEST=REQUEST):
field_list.append(field)
relation_field_found = 0
for field in field_list:
if getattr(field, 'is_relation_field', None):
try:
dumb = field.get_value('is_relation_field')
# XXX FIXME Exception name is not in locals.
# This can be related to a bad python file import
# I already had this kind of error with another python software,
# and the only solution I found was to use ihooks to
# import python files.
# I have to check this.
# except KeyError:
except:
pass
# relation_index += 1
else:
if index == relation_index:
relation_field_found = 1
break
else:
relation_index += 1
# if getattr(field, 'is_relation_field', None):
# if index == relation_index:
# relation_field_found = 1
# break
# else:
# relation_index += 1
if not relation_field_found:
raise SelectionError, "SelectionTool: can not find the relation" \
" field %s" % index
field_value = REQUEST.form['field_%s' % field.id]
selection_name = 'Base_viewRelatedObjectList'
......@@ -973,7 +1001,9 @@ class SelectionTool( UniqueObject, SimpleItem ):
# reselt current selection
self.portal_selections.setSelectionFor( selection_name, None)
# XXX portal_status_message = "Please select one object to precise the value: '%s' in the field: '%s'" % ( field_value, field.get_orig_value('title') )
# XXX portal_status_message =
# "Please select one object to precise the value:
# '%s' in the field: '%s'" % (field_value, field.get_orig_value('title'))
portal_status_message = "Please select one object."
if field.meta_type == "MultiRelationStringField":
......@@ -982,21 +1012,29 @@ class SelectionTool( UniqueObject, SimpleItem ):
# we need to facilitate user search
# first: store current field value in the selection
base_category = field.get_value( 'base_category')
property_get_related_uid_method_name = "get"+ string.join( map( lambda x: string.upper(x[0]) + x[1:] ,string.split(base_category,'_') ) , '' ) + "UidList"
base_category = field.get_value('base_category')
current_uid_list = getattr( o, property_get_related_uid_method_name )( portal_type=map(lambda x:x[0],field.get_value('portal_type')))
property_get_related_uid_method_name = \
"get%sUidList" % ''.join(['%s%s' % (x[0].upper(), x[1:]) \
for x in base_category.split('_')])
current_uid_list = getattr(o, property_get_related_uid_method_name)\
(portal_type=[x[0] for x in \
field.get_value('portal_type')])
# Checked current uid
kw ={}
kw[field.get_value('catalog_index')] = field_value
self.portal_selections.setSelectionParamsFor(selection_name, kw.copy())
self.portal_selections.setSelectionCheckedUidsFor(selection_name , current_uid_list )
self.portal_selections.setSelectionParamsFor(selection_name,
kw.copy())
self.portal_selections.setSelectionCheckedUidsFor(
selection_name,
current_uid_list)
field_value = ''
REQUEST.form['field_%s' % field.id] = field_value
# XXX portal_status_message = "Please select one or more object to define the field: '%s'" % field.get_orig_value('title')
# XXX portal_status_message =
# "Please select one or more object to define the field:
# '%s'" % field.get_orig_value('title')
portal_status_message = "Please select one (or more) object."
......@@ -1011,8 +1049,6 @@ class SelectionTool( UniqueObject, SimpleItem ):
REQUEST.form_pickle = form_pickle
REQUEST.form_signature = form_signature
base_category = None
kw = {}
......@@ -1101,29 +1137,26 @@ class SelectionTool( UniqueObject, SimpleItem ):
def _aq_dynamic(self, name):
"""
Generate viewSearchRelatedDocumentDialog0, viewSearchRelatedDocumentDialog1,... if necessary
Generate viewSearchRelatedDocumentDialog0,
viewSearchRelatedDocumentDialog1,... if necessary
"""
aq_base_name = getattr(aq_base(self), name, None)
if aq_base_name == None:
dynamic_method_name = 'viewSearchRelatedDocumentDialog'
zope_security = '__roles__'
if (name[:len(dynamic_method_name)] == dynamic_method_name) and (name[-len(zope_security):] != zope_security) :
#method_count_string = name[len(dynamic_method_name):]
method_count_string_list = string.split( name[len(dynamic_method_name):] , '_' )
if (name[:len(dynamic_method_name)] == dynamic_method_name) and \
(name[-len(zope_security):] != zope_security):
method_count_string_list = string.split(
name[len(dynamic_method_name):],
'_')
method_count_string = method_count_string_list[0]
# be sure that method name is correct
try:
method_count = string.atoi(method_count_string)
except TypeError:
return aq_base_name
else:
if len(method_count_string_list) > 1:
# be sure that method name is correct
try:
......@@ -1133,20 +1166,25 @@ class SelectionTool( UniqueObject, SimpleItem ):
else:
sub_index = None
# generate dynamicaly needed forwarder methods
def viewSearchRelatedDocumentDialogWrapper(self, form_id, REQUEST=None, **kw):
def viewSearchRelatedDocumentDialogWrapper(self, form_id,
REQUEST=None, **kw):
"""
viewSearchRelatedDocumentDialog Wrapper
"""
LOG('SelectionTool.viewSearchRelatedDocumentDialogWrapper, kw',0,kw)
LOG('SelectionTool.viewSearchRelatedDocumentDialogWrapper, kw',
0, kw)
if sub_index == None:
return self.viewSearchRelatedDocumentDialog(method_count, form_id, REQUEST=REQUEST, **kw)
return self.viewSearchRelatedDocumentDialog(
method_count, form_id,
REQUEST=REQUEST, **kw)
else:
return self.viewSearchRelatedDocumentDialog(method_count, form_id, REQUEST=REQUEST, sub_index=sub_index, **kw)
return self.viewSearchRelatedDocumentDialog(
method_count, form_id,
REQUEST=REQUEST, sub_index=sub_index, **kw)
setattr(self.__class__, name, viewSearchRelatedDocumentDialogWrapper)
setattr(self.__class__, name,
viewSearchRelatedDocumentDialogWrapper)
klass = aq_base(self).__class__
if hasattr(klass, 'security'):
......@@ -1154,8 +1192,8 @@ class SelectionTool( UniqueObject, SimpleItem ):
klass.security.declareProtected(ERP5Permissions.View, name)
else:
# XXX security declaration always failed....
LOG('WARNING ERP5Form SelectionTool, security not defined on',0,klass.__name__)
LOG('WARNING ERP5Form SelectionTool, security not defined on',
0, klass.__name__)
return getattr(self, name)
else:
return aq_base_name
......
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