Commit 648c2d42 authored by Kevin Deldycke's avatar Kevin Deldycke

First rework of Web API scripts.

git-svn-id: https://svn.erp5.org/repos/public/erp5/trunk@9360 20353a03-c40f-0410-a6d1-a30d3c3de9de
parent 193acde9
......@@ -36,7 +36,7 @@
</item>
<item>
<key> <string>title</string> </key>
<value> <string>Web Core Logic and Utils scripts</string> </value>
<value> <string>web core logic and utils scripts</string> </value>
</item>
</dictionary>
</pickle>
......
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<tuple>
<tuple>
<string>Products.PythonScripts.PythonScript</string>
<string>PythonScript</string>
</tuple>
<none/>
</tuple>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>Python_magic</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>Script_magic</string> </key>
<value> <int>3</int> </value>
</item>
<item>
<key> <string>__ac_local_roles__</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>_bind_names</string> </key>
<value>
<object>
<klass>
<global name="NameAssignments" module="Shared.DC.Scripts.Bindings"/>
</klass>
<tuple/>
<state>
<dictionary>
<item>
<key> <string>_asgns</string> </key>
<value>
<dictionary>
<item>
<key> <string>name_container</string> </key>
<value> <string>container</string> </value>
</item>
<item>
<key> <string>name_context</string> </key>
<value> <string>context</string> </value>
</item>
<item>
<key> <string>name_m_self</string> </key>
<value> <string>script</string> </value>
</item>
<item>
<key> <string>name_subpath</string> </key>
<value> <string>traverse_subpath</string> </value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</state>
</object>
</value>
</item>
<item>
<key> <string>_body</string> </key>
<value> <string># This script is part of ERP5 Web\n
#\n
# ERP5 Web is an extension to ERP5 which provides a way\n
# to create web sites which can display selected\n
# ERP5 contents through multiple custom web layouts.\n
#\n
# This script returns a list of document values (ie. objects or brains)\n
# which are considered as part of this section. It can be\n
# a list of web pages (usual case), a list of products\n
# (online catalog), a list of tenders (e-government), etc.\n
# \n
# This script shows an implementation which consists in\n
# listing documents which are either associated to this section\n
# through \'aggregate\' or which are meet the predicate define\n
# by the section (ex. which are part of a given publication_section)\n
# and which are in "published" state and of a "Web Page" portal_type\n
#\n
# This script can be changed to meet other requirements. For example\n
# one may want to display a list of products in a section. In this case,\n
# this script must return a list of documents of type "Product"\n
# with a "validated" state and in the appropriate product family.\n
#\n
# This script is intended to be overriden by creating an instance \n
# in each section if necessary\n
\n
portal_catalog = container.portal_catalog\n
# New implementation.\n
\n
# First find the web section we are in\n
current_node = context\n
while not current_node.getPortalType() in (\'Web Section\', \'Web Site\', \'Domain\'):\n
current_node = current_node.aq_parent\n
\n
# Then find the publication categories referenced by the web section\n
section_categories = current_node.getMembershipCriterionCategoryList()\n
\n
\n
def membercheck(x):\n
the_categories = x.getCategoriesList()\n
if the_categories:\n
for the_category in the_categories:\n
if the_category in section_categories:\n
if x.getValidationState()==\'published\':\n
return x\n
\n
# this is just for debugging purposes, not used at the moment\n
def displaytitle(x):\n
return x.title\n
\n
# get all web pages and check if their publication category is in the category list of the web section\n
#reference_list = map(lambda x:x.getReference(), context.getSourceValueList(portal_type="Web Page"))\n
reference_list = map(lambda x:x.getReference(), portal_catalog(portal_type=(\'Web Page\',)))\n
if reference_list:\n
li = filter(membercheck, list(context.portal_catalog(portal_type="Web Page")) )\n
#li = map (displaytitle,li)\n
return li\n
return "[] Empty reference_list"\n
\n
\n
## FYI - old implementation - broken\n
#from Products.ERP5Form.Selection import DomainSelection\n
#\n
#if not context.getMembershipCriterionCategoryList() and not context.getSourceList():\n
# return []\n
#\n
#def content_cmp(c1, c2):\n
# c1_value = c1.getObject() \n
# c2_value = c2.getObject() \n
# return cmp(c1_value.getIntIndex(), c2_value.getIntIndex()) \n
#\n
#if context.getMembershipCriterionCategoryList():\n
# domain = DomainSelection(domain_dict = {\'web_site\': context})\n
# kw[\'selection_domain\'] = domain\n
# kw[\'portal_type\'] = \'Web Page\'\n
# kw[\'validation_state\'] = \'published\'\n
# if not kw.has_key(\'sort_on\'): kw[\'sort_on\'] = \'int_index\'\n
# brain_list = list(context.portal_catalog(**kw))\n
#else:\n
# brain_list = []\n
#\n
## Make a new search with local documents\n
#reference_list = map(lambda x:x.getReference(), context.getSourceValueList(portal_type="Web Page"))\n
#if reference_list:\n
# brain_list.extend(list(context.portal_catalog(reference= reference_list, portal_type="Web Page")))\n
#\n
## Make sure a single reference is counted once only\n
#brain_dict = {}\n
#none_list = []\n
#for b in brain_list:\n
# o = b.getObject()\n
# if o is not None:\n
# reference = o.getReference()\n
# if reference is None:\n
# none_list.append(b)\n
# else:\n
# brain_dict[reference] = b\n
#\n
#brain_list = brain_dict.values() + none_list\n
#for bee in brain_list:\n
# bee = bee.title\n
#\n
## Sort documents\n
#brain_list.sort(content_cmp)\n
#\n
#return brain_list\n
</string> </value>
</item>
<item>
<key> <string>_code</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>_filepath</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>_owner</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>_params</string> </key>
<value> <string>**kw</string> </value>
</item>
<item>
<key> <string>errors</string> </key>
<value>
<tuple/>
</value>
</item>
<item>
<key> <string>func_code</string> </key>
<value>
<object>
<klass>
<global name="FuncCode" module="Shared.DC.Scripts.Signature"/>
</klass>
<tuple/>
<state>
<dictionary>
<item>
<key> <string>co_argcount</string> </key>
<value> <int>0</int> </value>
</item>
<item>
<key> <string>co_varnames</string> </key>
<value>
<tuple>
<string>kw</string>
<string>_getattr_</string>
<string>container</string>
<string>portal_catalog</string>
<string>context</string>
<string>current_node</string>
<string>section_categories</string>
<string>membercheck</string>
<string>displaytitle</string>
<string>map</string>
<string>reference_list</string>
<string>filter</string>
<string>list</string>
<string>li</string>
</tuple>
</value>
</item>
</dictionary>
</state>
</object>
</value>
</item>
<item>
<key> <string>func_defaults</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>WebSection_getDocumentValueList</string> </value>
</item>
<item>
<key> <string>warnings</string> </key>
<value>
<tuple/>
</value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<tuple>
<tuple>
<string>Products.PythonScripts.PythonScript</string>
<string>PythonScript</string>
</tuple>
<none/>
</tuple>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>Python_magic</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>Script_magic</string> </key>
<value> <int>3</int> </value>
</item>
<item>
<key> <string>__ac_local_roles__</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>_bind_names</string> </key>
<value>
<object>
<klass>
<global name="NameAssignments" module="Shared.DC.Scripts.Bindings"/>
</klass>
<tuple/>
<state>
<dictionary>
<item>
<key> <string>_asgns</string> </key>
<value>
<dictionary>
<item>
<key> <string>name_container</string> </key>
<value> <string>container</string> </value>
</item>
<item>
<key> <string>name_context</string> </key>
<value> <string>context</string> </value>
</item>
<item>
<key> <string>name_m_self</string> </key>
<value> <string>script</string> </value>
</item>
<item>
<key> <string>name_subpath</string> </key>
<value> <string>traverse_subpath</string> </value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</state>
</object>
</value>
</item>
<item>
<key> <string>_body</string> </key>
<value> <string encoding="cdata"><![CDATA[
# This script is part of ERP5 Web\n
#\n
# ERP5 Web is an extension to ERP5 which provides a way\n
# to create web sites which can display selected\n
# ERP5 contents through multiple custom web layouts.\n
#\n
# This script returns a list of tuples in the form\n
# (title, value) where title is the title of the \n
# document provided as value. The document value\n
# must be wrapped in the acquisition context \n
# (ex. Web Site / Section / Module / Document).\n
#\n
# Scripts of page templates which use this script\n
# and need to generate the URLs of document values\n
# should must use the WebSite_getDocumentUrl API \n
# or eventually the absolute_url API in order\n
# to benefit from virtual hosting features. \n
# (ie. do not use getUrl)\n
# NOTE: Clarification needed when to use which ?\n
#\n
# This script can be used to produce breadcrumbs\n
# in page templates or in widgets. It can be customised\n
# to implement different behaviours such as\n
#\n
# - CMS like breadcrumb (hierarchy of contents)\n
# - Wiki like breadcrumb (history of navigation)\n
# - or any other behaviour\n
#\n
# The default implementation is hierarchical\n
#\n
# WebSite_getBreadcrumbValue=[<WebSite at /erp5/web_site_module/1>, <Domain at /erp5/web_site_module/1/1>, <Document at /erp5/web_page_module/1 used for /erp5/web_site_module/1/1>]\n
\n
crumb = context\n
crumb_list = []\n
\n
while hasattr(crumb,\'getParent\'): #\'getPortalType\'):\n
crumb_list.append((crumb.title,crumb ))\n
if crumb.getPortalType() == \'Web Site\':\n
crumb = None # Don\'t go higher than the first Web Site found\n
else:\n
###context.log(\'KevLog 1 >>>>>\', repr(crumb))\n
####context.log(\'KevLog 2 >>>>>\', repr(crumb.getParent()))\n
###context.log(\'KevLog 3 >>>>>\', repr(crumb.getParentValue()))\n
\n
crumb = crumb.aq_parent#getParentValue()\n
\n
crumb_list.reverse()\n
\n
return crumb_list\n
]]></string> </value>
</item>
<item>
<key> <string>_code</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>_filepath</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>_owner</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>_params</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>errors</string> </key>
<value>
<tuple/>
</value>
</item>
<item>
<key> <string>func_code</string> </key>
<value>
<object>
<klass>
<global name="FuncCode" module="Shared.DC.Scripts.Signature"/>
</klass>
<tuple/>
<state>
<dictionary>
<item>
<key> <string>co_argcount</string> </key>
<value> <int>0</int> </value>
</item>
<item>
<key> <string>co_varnames</string> </key>
<value>
<tuple>
<string>context</string>
<string>crumb</string>
<string>crumb_list</string>
<string>hasattr</string>
<string>_getattr_</string>
<string>None</string>
</tuple>
</value>
</item>
</dictionary>
</state>
</object>
</value>
</item>
<item>
<key> <string>func_defaults</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>WebSite_getBreadcrumbItemList</string> </value>
</item>
<item>
<key> <string>warnings</string> </key>
<value>
<tuple/>
</value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<tuple>
<tuple>
<string>Products.PythonScripts.PythonScript</string>
<string>PythonScript</string>
</tuple>
<none/>
</tuple>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>Python_magic</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>Script_magic</string> </key>
<value> <int>3</int> </value>
</item>
<item>
<key> <string>__ac_local_roles__</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>_bind_names</string> </key>
<value>
<object>
<klass>
<global name="NameAssignments" module="Shared.DC.Scripts.Bindings"/>
</klass>
<tuple/>
<state>
<dictionary>
<item>
<key> <string>_asgns</string> </key>
<value>
<dictionary>
<item>
<key> <string>name_container</string> </key>
<value> <string>container</string> </value>
</item>
<item>
<key> <string>name_context</string> </key>
<value> <string>context</string> </value>
</item>
<item>
<key> <string>name_m_self</string> </key>
<value> <string>script</string> </value>
</item>
<item>
<key> <string>name_subpath</string> </key>
<value> <string>traverse_subpath</string> </value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</state>
</object>
</value>
</item>
<item>
<key> <string>_body</string> </key>
<value> <string encoding="cdata"><![CDATA[
# This script is part of ERP5 Web\n
#\n
# ERP5 Web is an extension to ERP5 which provides a way\n
# to create web sites which can display selected\n
# ERP5 contents through multiple custom web layouts.\n
#\n
# This script returns the default document to display\n
# as the front page of a given section.\n
#\n
# The default implementation should look at published\n
# documents which are associated to the section\n
# through aggregate relation and try to display those\n
# which are in the closest possible language.\n
#\n
# This behaviour can be overriden to implemet more complex\n
# policies (ex. display the last version in the appropriate\n
# language rather than the latest version in a different\n
# language)\n
\n
nextsection=context\n
while hasattr(nextsection,\'getParent\') and hasattr(nextsection,\'getPortalType\'):\n
if nextsection.getPortalType() == \'Web Section\':\n
break\n
else:\n
nextsection = nextsection.aq_parent\n
context.log("krumb",nextsection)\n
\n
\n
if not hasattr(nextsection,\'getCategories\'):\n
return None # Context is not a Section. Changed later\n
\n
context.log("Kontexttitle",context.title)\n
myaggregate = context.getAggregateValue()\n
context.log("My Aggegate => ",myaggregate)\n
if myaggregate==None:\n
context.log("No aggregate found","!!!")\n
return None\n
return myaggregate\n
]]></string> </value>
</item>
<item>
<key> <string>_code</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>_filepath</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>_owner</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>_params</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>errors</string> </key>
<value>
<tuple/>
</value>
</item>
<item>
<key> <string>func_code</string> </key>
<value>
<object>
<klass>
<global name="FuncCode" module="Shared.DC.Scripts.Signature"/>
</klass>
<tuple/>
<state>
<dictionary>
<item>
<key> <string>co_argcount</string> </key>
<value> <int>0</int> </value>
</item>
<item>
<key> <string>co_varnames</string> </key>
<value>
<tuple>
<string>context</string>
<string>nextsection</string>
<string>hasattr</string>
<string>_getattr_</string>
<string>None</string>
<string>myaggregate</string>
</tuple>
</value>
</item>
</dictionary>
</state>
</object>
</value>
</item>
<item>
<key> <string>func_defaults</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>WebSite_getDefaultWebPageValue</string> </value>
</item>
<item>
<key> <string>warnings</string> </key>
<value>
<tuple/>
</value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
......@@ -68,19 +68,83 @@
</item>
<item>
<key> <string>_body</string> </key>
<value> <string>#XXX This script should be named WebSite_getDocument\n
document = context.WebSite_getDocumentValue()\n
if document is not None:\n
section_url = context.WebSite_getSection()\n
if section_url is not None:\n
doc_ref = document.getReference()\n
if doc_ref is not None:\n
return \'%s/%s\' % (section_url, doc_ref)\n
return section_url\n
else:\n
return document.absolute_url()\n
return context.WebSite_getUrl()\n
</string> </value>
<value> <string encoding="cdata"><![CDATA[
# This script is part of ERP5 Web\n
#\n
# ERP5 Web is an extension to ERP5 which provides a way\n
# to create web sites which can display selected\n
# ERP5 contents through multiple custom web layouts.\n
#\n
# This script returns the absolute URL of the current \n
# document in a pretty way and taking into account\n
# virtual hosting. The purpose of this script is to be\n
# able to access documents through a URL such as:\n
#\n
# www.mysite.com/mysection/a-document-reference\n
#\n
# even if the physical path of the document is\n
#\n
# /erp5/web_page_module/33\n
#\n
# The default script looks in the acquisition context\n
# for the first relevant section and build a URL\n
# based on the section absolute_url and on the \n
# document reference. \n
#\n
# More sophisticated behaviours are possible.\n
#\n
# It may be useful to change the behaviour\n
# of WebSite_getDocumentUrl for non anonymous\n
# users or for documents which are not published.\n
#\n
# NOTE: it is still unknown whether this script\n
# needs to be integrated or not to the absolute_url API\n
\n
# FYI old implementation\n
\n
# First build the main section URL\n
main_section = context\n
main_section_path = None\n
#while main_section_path is None and main_section is not None:\n
# if hasattr(main_section, \'getPortalType\'):\n
# if main_section.getPortalType() == \'Web Section\':\n
# main_section_path = str(main_section.getObject().absolute_url())\n
# elif main_section.getPortalType() == \'Web Site\':\n
# main_section_path = str(main_section.getObject().absolute_url())\n
# main_section = main_section.aq_parent\n
while hasattr(main_section,\'aq_parent\'):\n
main_section_path = str(main_section.absolute_url())\n
main_section = main_section.aq_parent\n
\n
\n
\n
# Then get the object\n
brain=None\n
if brain is None: brain=context\n
object = brain.getObject()\n
#if object is not None:\n
# if object.hasReference():\n
# reference = object.getReference()\n
# else:\n
# reference = None\n
#else:\n
# return ""\n
\n
#if reference:\n
# return "%s/%s" % (main_section_path, reference)\n
\n
if context.getPortalType()=="Web Page":\n
billl=context.WebSite_getBreadcrumbItemList()\n
billl=billl[1][1].WebSite_getDocumentUrl() #get the URL of the section\n
context.log("bc",billl)\n
return billl+"/"+context.getReference()\n
#context.log("hello",context.title+"==>"+context.aq_parent.WebSite_getDocumentUrl()+"--"+context.aq_inner.getUrl()+"--"+context.aq_inner.absolute_url()+"--"+context.aq_parent.absolute_url())\n
\n
return "%s/%s" % (main_section_path, object.getRelativeUrl())\n
]]></string> </value>
</item>
<item>
<key> <string>_code</string> </key>
......@@ -94,6 +158,12 @@ return context.WebSite_getUrl()\n
<none/>
</value>
</item>
<item>
<key> <string>_owner</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>_params</string> </key>
<value> <string></string> </value>
......@@ -122,12 +192,17 @@ return context.WebSite_getUrl()\n
<key> <string>co_varnames</string> </key>
<value>
<tuple>
<string>_getattr_</string>
<string>context</string>
<string>document</string>
<string>main_section</string>
<string>None</string>
<string>section_url</string>
<string>doc_ref</string>
<string>main_section_path</string>
<string>hasattr</string>
<string>str</string>
<string>_getattr_</string>
<string>brain</string>
<string>object</string>
<string>billl</string>
<string>_getitem_</string>
</tuple>
</value>
</item>
......
......@@ -68,16 +68,52 @@
</item>
<item>
<key> <string>_body</string> </key>
<value> <string>portal_type = context.getPortalType()\n
if portal_type == \'Web Site\':\n
return None\n
if portal_type == \'Web Section\' and not getattr(context.REQUEST, \'editable_mode\', None):\n
document_value = context.WebSite_getDefaultDocumentValue()\n
if document_value == None:\n
# No default document is defined, return context as document value\n
<value> <string># This script is part of ERP5 Web\n
#\n
# ERP5 Web is an extension to ERP5 which provides a way\n
# to create web sites which can display selected\n
# ERP5 contents through multiple custom web layouts.\n
#\n
# This script returns the document which is being displayed\n
# in the current in the form of a value (object).\n
#\n
# If a name parameter is provided, it will lookup\n
# the catalog to find a document. If no name parameter\n
# is provided, it will analyse the current context\n
# (ex. Section, Web Page, etc.) and try to find out\n
# which document is being displayed. For example,\n
# sections which define a default document may \n
# actually display that document rather than \n
# themselves when asked to render.\n
#\n
# NOTE: portal param probably useless\n
\n
\n
portal_catalog = container.portal_catalog\n
\n
if name is None:\n
if context.getPortalType() == \'Web Page\':\n
return context\n
return document_value\n
return context\n
elif context.getPortalType() in (\'Web Site\', \'Web Section\'):\n
return context.WebSite_getDefaultWebPageValue()\n
else:\n
return None\n
\n
#"else:" (mostly code from the old implementation)\n
\n
#from Products.ERP5Type.Cache import CachingMethod\n
def getDocument(name):\n
# First try to get a published document\n
matching_document_list = portal_catalog(reference = \'%s\' % name, portal_type=(\'Web Page\',), validation_state=\'published\')\n
if len(matching_document_list):\n
return matching_document_list[0].getObject()\n
# Then try to get any document\n
matching_document_list = portal_catalog(reference = \'%s\' % name, portal_type=(\'Web Page\',))\n
if len(matching_document_list):\n
return matching_document_list[0].getObject()\n
return None \n
\n
return getDocument(name)\n
</string> </value>
</item>
<item>
......@@ -92,9 +128,15 @@ return context\n
<none/>
</value>
</item>
<item>
<key> <string>_owner</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>_params</string> </key>
<value> <string></string> </value>
<value> <string>portal=None, name=None</string> </value>
</item>
<item>
<key> <string>errors</string> </key>
......@@ -114,18 +156,20 @@ return context\n
<dictionary>
<item>
<key> <string>co_argcount</string> </key>
<value> <int>0</int> </value>
<value> <int>2</int> </value>
</item>
<item>
<key> <string>co_varnames</string> </key>
<value>
<tuple>
<string>portal</string>
<string>name</string>
<string>_getattr_</string>
<string>context</string>
<string>portal_type</string>
<string>container</string>
<string>portal_catalog</string>
<string>None</string>
<string>getattr</string>
<string>document_value</string>
<string>context</string>
<string>getDocument</string>
</tuple>
</value>
</item>
......@@ -137,7 +181,10 @@ return context\n
<item>
<key> <string>func_defaults</string> </key>
<value>
<tuple>
<none/>
<none/>
</tuple>
</value>
</item>
<item>
......
......@@ -30,7 +30,7 @@
</item>
<item>
<key> <string>title</string> </key>
<value> <string>Default Web Theme</string> </value>
<value> <string>default web theme</string> </value>
</item>
</dictionary>
</pickle>
......
......@@ -30,7 +30,7 @@
</item>
<item>
<key> <string>title</string> </key>
<value> <string>Experiments for Future Generic Concepts</string> </value>
<value> <string>experiments for future generic concepts</string> </value>
</item>
</dictionary>
</pickle>
......
2006-08-23 Stefan
* First rework of Web API scripts.
2006-08-23 Kevin
* Multiple Publication Sections can be set on Web Pages.
* One default page can be set on Web Section through agregate relation.
......
183
\ No newline at end of file
187
\ No newline at end of file
0.2.10
\ No newline at end of file
0.3
\ No newline at end of file
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