Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
erp5-Boxiang
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
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
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Hamza
erp5-Boxiang
Commits
9606e08e
Commit
9606e08e
authored
Jan 21, 2019
by
Romain Courteaud
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
[erp5_core/erp5_web_renderjs_ui] Update jIO 3.36.0
parent
7c7d1aa1
Changes
3
Hide whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
624 additions
and
4 deletions
+624
-4
bt5/erp5_web_renderjs_ui/PathTemplateItem/web_page_module/rjs_jio_js.js
...enderjs_ui/PathTemplateItem/web_page_module/rjs_jio_js.js
+311
-1
bt5/erp5_web_renderjs_ui/PathTemplateItem/web_page_module/rjs_jio_js.xml
...nderjs_ui/PathTemplateItem/web_page_module/rjs_jio_js.xml
+2
-2
product/ERP5/bootstrap/erp5_core/SkinTemplateItem/portal_skins/erp5_core/jio.js.js
...p5_core/SkinTemplateItem/portal_skins/erp5_core/jio.js.js
+311
-1
No files found.
bt5/erp5_web_renderjs_ui/PathTemplateItem/web_page_module/rjs_jio_js.js
View file @
9606e08e
...
...
@@ -8849,7 +8849,7 @@ return new Parser;
var
ceilHeapSize
=
function
(
v
)
{
// The asm.js spec says:
// The heap object's byteLength must be either
// 2^n for n in [12, 24) or 2^24 * n for n
≥
1.
// 2^n for n in [12, 24) or 2^24 * n for n
≥
1.
// Also, byteLengths smaller than 2^16 are deprecated.
var
p
;
// If v is smaller than 2^16, the smallest possible solution
...
...
@@ -12959,6 +12959,316 @@ return new Parser;
jIO
.
addStorage
(
'
union
'
,
UnionStorage
);
}(
jIO
,
RSVP
));
/*
* Copyright 2019, Nexedi SA
*
* This program is free software: you can Use, Study, Modify and Redistribute
* it under the terms of the GNU General Public License version 3, or (at your
* option) any later version, as published by the Free Software Foundation.
*
* You can also Link and Combine this program with other software covered by
* the terms of any of the Free Software licenses or any of the Open Source
* Initiative approved licenses and Convey the resulting work. Corresponding
* source of such a combination shall include the source code for all other
* software used.
*
* This program is distributed WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
*
* See COPYING file for full licensing terms.
* See https://www.nexedi.com/licensing for rationale and options.
*/
/**
* JIO Linshare Storage. Type = "linshare".
* Linshare "database" storage.
* http://download.linshare.org/components/linshare-core/2.2.2/
* Can't set up id, implied can't put new document
*/
/*global jIO, RSVP, UriTemplate, FormData, Blob*/
/*jslint nomen: true*/
(
function
(
jIO
,
RSVP
,
UriTemplate
,
FormData
,
Blob
)
{
"
use strict
"
;
function
makeRequest
(
storage
,
uuid
,
options
,
download
)
{
if
(
options
===
undefined
)
{
options
=
{};
}
if
(
options
.
xhrFields
===
undefined
)
{
options
.
xhrFields
=
{};
}
if
(
options
.
headers
===
undefined
)
{
options
.
headers
=
{};
}
// Prefer JSON by default
if
(
download
===
true
)
{
options
.
url
=
storage
.
_blob_template
.
expand
({
uuid
:
uuid
});
options
.
dataType
=
'
blob
'
;
}
else
{
options
.
url
=
storage
.
_url_template
.
expand
({
uuid
:
uuid
||
""
});
if
(
!
options
.
headers
.
hasOwnProperty
(
'
Accept
'
))
{
options
.
headers
.
Accept
=
'
application/json
'
;
options
.
dataType
=
'
json
'
;
}
}
// Use cookie based auth
if
(
storage
.
hasOwnProperty
(
'
_access_token
'
))
{
options
.
headers
.
Authorization
=
"
Basic
"
+
storage
.
_access_token
;
}
else
{
options
.
xhrFields
.
withCredentials
=
true
;
}
return
new
RSVP
.
Queue
()
.
push
(
function
()
{
return
jIO
.
util
.
ajax
(
options
);
})
.
push
(
function
(
event
)
{
if
(
download
===
true
)
{
return
(
event
.
target
.
response
||
// sinon does not fill the response attribute
new
Blob
([
event
.
target
.
responseText
],
{
type
:
'
text/plain
'
})
);
}
return
(
event
.
target
.
response
||
// sinon does not fill the response attribute
JSON
.
parse
(
event
.
target
.
responseText
)
);
});
}
/**
* The JIO Linshare Storage extension
*
* @class LinshareStorage
* @constructor
*/
function
LinshareStorage
(
spec
)
{
if
(
typeof
spec
.
url
!==
"
string
"
||
!
spec
.
url
)
{
throw
new
TypeError
(
"
Linshare 'url' must be a string
"
+
"
which contains more than one character.
"
);
}
this
.
_url_template
=
UriTemplate
.
parse
(
spec
.
url
+
'
/linshare/webservice/rest/user/v2/documents/{uuid}
'
);
this
.
_blob_template
=
UriTemplate
.
parse
(
spec
.
url
+
'
/linshare/webservice/rest/user/v2/documents/{uuid}/download
'
);
if
(
spec
.
hasOwnProperty
(
'
access_token
'
))
{
this
.
_access_token
=
spec
.
access_token
;
}
}
var
capacity_list
=
[
'
list
'
,
'
include
'
];
LinshareStorage
.
prototype
.
hasCapacity
=
function
(
name
)
{
return
(
capacity_list
.
indexOf
(
name
)
!==
-
1
);
};
function
sortByModificationDate
(
entry1
,
entry2
)
{
var
date1
=
entry1
.
modificationDate
,
date2
=
entry2
.
modificationDate
;
return
(
date1
===
date2
)
?
0
:
((
date1
<
date2
)
?
1
:
-
1
);
}
function
getDocumentList
(
storage
,
options
)
{
return
makeRequest
(
storage
,
""
,
{
type
:
"
GET
"
})
.
push
(
function
(
entry_list
)
{
// Linshare only allow to get the full list of documents
// First, sort the entries by modificationDate in order to
// drop the 'old' entries with the same 'name'
// (as linshare does not to update an existing doc)
entry_list
.
sort
(
sortByModificationDate
);
// Only return one document per name
// Keep the newer document
var
entry_dict
=
{},
i
,
len
=
entry_list
.
length
,
entry_name
,
entry
,
result_list
=
[];
for
(
i
=
0
;
i
<
len
;
i
+=
1
)
{
entry_name
=
entry_list
[
i
].
name
;
// If we only need one precise name, no need to check the others
if
(
!
options
.
hasOwnProperty
(
'
only_id
'
)
||
(
options
.
only_id
===
entry_name
))
{
if
(
!
entry_dict
.
hasOwnProperty
(
entry_name
))
{
entry
=
{
id
:
entry_name
,
value
:
{},
_linshare_uuid
:
entry_list
[
i
].
uuid
};
if
(
options
.
include_docs
===
true
)
{
entry
.
doc
=
JSON
.
parse
(
entry_list
[
i
].
metaData
)
||
{};
}
result_list
.
push
(
entry
);
if
(
options
.
all_revision
!==
true
)
{
// If we only want to fetch 'one revision',
// ie, the latest document matching this id
entry_dict
[
entry_name
]
=
null
;
if
(
options
.
only_id
===
entry_name
)
{
// Document has been found, no need to check all the others
break
;
}
}
}
}
}
return
result_list
;
});
}
LinshareStorage
.
prototype
.
buildQuery
=
function
(
options
)
{
return
getDocumentList
(
this
,
{
include_docs
:
options
.
include_docs
});
};
LinshareStorage
.
prototype
.
get
=
function
(
id
)
{
// It is not possible to get a document by its name
// The only way is to list all of them, and find it manually
return
getDocumentList
(
this
,
{
include_docs
:
true
,
only_id
:
id
})
.
push
(
function
(
result_list
)
{
if
(
result_list
.
length
===
1
)
{
return
result_list
[
0
].
doc
;
}
throw
new
jIO
.
util
.
jIOError
(
"
Can't find document with id :
"
+
id
,
404
);
});
};
function
createLinshareDocument
(
storage
,
id
,
doc
,
blob
)
{
var
data
=
new
FormData
();
data
.
append
(
'
file
'
,
blob
,
id
);
data
.
append
(
'
filesize
'
,
blob
.
size
);
data
.
append
(
'
filename
'
,
id
);
data
.
append
(
'
description
'
,
doc
.
title
||
doc
.
description
||
''
);
data
.
append
(
'
metadata
'
,
jIO
.
util
.
stringify
(
doc
));
return
makeRequest
(
storage
,
''
,
{
type
:
'
POST
'
,
data
:
data
});
}
LinshareStorage
.
prototype
.
put
=
function
(
id
,
doc
)
{
var
storage
=
this
;
return
getDocumentList
(
storage
,
{
include_docs
:
true
,
only_id
:
id
})
.
push
(
function
(
result_list
)
{
if
(
result_list
.
length
===
1
)
{
// Update existing document metadata
var
data
=
{
uuid
:
result_list
[
0
].
_linshare_uuid
,
metaData
:
jIO
.
util
.
stringify
(
doc
),
name
:
id
,
description
:
doc
.
title
||
doc
.
description
||
''
};
return
makeRequest
(
storage
,
result_list
[
0
].
_linshare_uuid
,
{
type
:
'
PUT
'
,
headers
:
{
'
Content-Type
'
:
'
application/json
'
},
data
:
jIO
.
util
.
stringify
(
data
)
});
}
// Create a new one
return
createLinshareDocument
(
storage
,
id
,
doc
,
new
Blob
());
});
};
LinshareStorage
.
prototype
.
remove
=
function
(
id
)
{
var
storage
=
this
;
// Delete all entries matching the id
return
getDocumentList
(
storage
,
{
only_id
:
id
,
all_revision
:
true
})
.
push
(
function
(
result_list
)
{
var
promise_list
=
[],
i
,
len
=
result_list
.
length
;
for
(
i
=
0
;
i
<
len
;
i
+=
1
)
{
promise_list
.
push
(
makeRequest
(
storage
,
result_list
[
i
].
_linshare_uuid
,
{
type
:
"
DELETE
"
})
);
}
return
RSVP
.
all
(
promise_list
);
});
};
LinshareStorage
.
prototype
.
allAttachments
=
function
(
id
)
{
return
this
.
get
(
id
)
.
push
(
function
()
{
return
{
enclosure
:
{}};
});
};
function
restrictAttachmentId
(
name
)
{
if
(
name
!==
"
enclosure
"
)
{
throw
new
jIO
.
util
.
jIOError
(
"
attachment name
"
+
name
+
"
is forbidden in linshare
"
,
400
);
}
}
LinshareStorage
.
prototype
.
putAttachment
=
function
(
id
,
name
,
blob
)
{
restrictAttachmentId
(
name
);
var
storage
=
this
;
return
storage
.
get
(
id
)
.
push
(
function
(
doc
)
{
// Create a new document with the same id but a different blob content
return
createLinshareDocument
(
storage
,
id
,
doc
,
blob
);
});
};
LinshareStorage
.
prototype
.
getAttachment
=
function
(
id
,
name
)
{
restrictAttachmentId
(
name
);
var
storage
=
this
;
// It is not possible to get a document by its name
// The only way is to list all of them, and find it manually
return
getDocumentList
(
storage
,
{
only_id
:
id
})
.
push
(
function
(
result_list
)
{
if
(
result_list
.
length
===
1
)
{
return
makeRequest
(
storage
,
result_list
[
0
].
_linshare_uuid
,
{
},
true
);
}
throw
new
jIO
.
util
.
jIOError
(
"
Can't find document with id :
"
+
id
,
404
);
});
};
jIO
.
addStorage
(
'
linshare
'
,
LinshareStorage
);
}(
jIO
,
RSVP
,
UriTemplate
,
FormData
,
Blob
));
/*
* Copyright 2013, Nexedi SA
*
...
...
bt5/erp5_web_renderjs_ui/PathTemplateItem/web_page_module/rjs_jio_js.xml
View file @
9606e08e
...
...
@@ -236,7 +236,7 @@
</item>
<item>
<key>
<string>
serial
</string>
</key>
<value>
<string>
97
0.58387.45482.44544
</string>
</value>
<value>
<string>
97
3.4826.12688.30276
</string>
</value>
</item>
<item>
<key>
<string>
state
</string>
</key>
...
...
@@ -254,7 +254,7 @@
</tuple>
<state>
<tuple>
<float>
15
39695044.71
</float>
<float>
15
48090618.17
</float>
<string>
UTC
</string>
</tuple>
</state>
...
...
product/ERP5/bootstrap/erp5_core/SkinTemplateItem/portal_skins/erp5_core/jio.js.js
View file @
9606e08e
...
...
@@ -8849,7 +8849,7 @@ return new Parser;
var
ceilHeapSize
=
function
(
v
)
{
// The asm.js spec says:
// The heap object's byteLength must be either
// 2^n for n in [12, 24) or 2^24 * n for n
≥
1.
// 2^n for n in [12, 24) or 2^24 * n for n
≥
1.
// Also, byteLengths smaller than 2^16 are deprecated.
var
p
;
// If v is smaller than 2^16, the smallest possible solution
...
...
@@ -12959,6 +12959,316 @@ return new Parser;
jIO
.
addStorage
(
'
union
'
,
UnionStorage
);
}(
jIO
,
RSVP
));
/*
* Copyright 2019, Nexedi SA
*
* This program is free software: you can Use, Study, Modify and Redistribute
* it under the terms of the GNU General Public License version 3, or (at your
* option) any later version, as published by the Free Software Foundation.
*
* You can also Link and Combine this program with other software covered by
* the terms of any of the Free Software licenses or any of the Open Source
* Initiative approved licenses and Convey the resulting work. Corresponding
* source of such a combination shall include the source code for all other
* software used.
*
* This program is distributed WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
*
* See COPYING file for full licensing terms.
* See https://www.nexedi.com/licensing for rationale and options.
*/
/**
* JIO Linshare Storage. Type = "linshare".
* Linshare "database" storage.
* http://download.linshare.org/components/linshare-core/2.2.2/
* Can't set up id, implied can't put new document
*/
/*global jIO, RSVP, UriTemplate, FormData, Blob*/
/*jslint nomen: true*/
(
function
(
jIO
,
RSVP
,
UriTemplate
,
FormData
,
Blob
)
{
"
use strict
"
;
function
makeRequest
(
storage
,
uuid
,
options
,
download
)
{
if
(
options
===
undefined
)
{
options
=
{};
}
if
(
options
.
xhrFields
===
undefined
)
{
options
.
xhrFields
=
{};
}
if
(
options
.
headers
===
undefined
)
{
options
.
headers
=
{};
}
// Prefer JSON by default
if
(
download
===
true
)
{
options
.
url
=
storage
.
_blob_template
.
expand
({
uuid
:
uuid
});
options
.
dataType
=
'
blob
'
;
}
else
{
options
.
url
=
storage
.
_url_template
.
expand
({
uuid
:
uuid
||
""
});
if
(
!
options
.
headers
.
hasOwnProperty
(
'
Accept
'
))
{
options
.
headers
.
Accept
=
'
application/json
'
;
options
.
dataType
=
'
json
'
;
}
}
// Use cookie based auth
if
(
storage
.
hasOwnProperty
(
'
_access_token
'
))
{
options
.
headers
.
Authorization
=
"
Basic
"
+
storage
.
_access_token
;
}
else
{
options
.
xhrFields
.
withCredentials
=
true
;
}
return
new
RSVP
.
Queue
()
.
push
(
function
()
{
return
jIO
.
util
.
ajax
(
options
);
})
.
push
(
function
(
event
)
{
if
(
download
===
true
)
{
return
(
event
.
target
.
response
||
// sinon does not fill the response attribute
new
Blob
([
event
.
target
.
responseText
],
{
type
:
'
text/plain
'
})
);
}
return
(
event
.
target
.
response
||
// sinon does not fill the response attribute
JSON
.
parse
(
event
.
target
.
responseText
)
);
});
}
/**
* The JIO Linshare Storage extension
*
* @class LinshareStorage
* @constructor
*/
function
LinshareStorage
(
spec
)
{
if
(
typeof
spec
.
url
!==
"
string
"
||
!
spec
.
url
)
{
throw
new
TypeError
(
"
Linshare 'url' must be a string
"
+
"
which contains more than one character.
"
);
}
this
.
_url_template
=
UriTemplate
.
parse
(
spec
.
url
+
'
/linshare/webservice/rest/user/v2/documents/{uuid}
'
);
this
.
_blob_template
=
UriTemplate
.
parse
(
spec
.
url
+
'
/linshare/webservice/rest/user/v2/documents/{uuid}/download
'
);
if
(
spec
.
hasOwnProperty
(
'
access_token
'
))
{
this
.
_access_token
=
spec
.
access_token
;
}
}
var
capacity_list
=
[
'
list
'
,
'
include
'
];
LinshareStorage
.
prototype
.
hasCapacity
=
function
(
name
)
{
return
(
capacity_list
.
indexOf
(
name
)
!==
-
1
);
};
function
sortByModificationDate
(
entry1
,
entry2
)
{
var
date1
=
entry1
.
modificationDate
,
date2
=
entry2
.
modificationDate
;
return
(
date1
===
date2
)
?
0
:
((
date1
<
date2
)
?
1
:
-
1
);
}
function
getDocumentList
(
storage
,
options
)
{
return
makeRequest
(
storage
,
""
,
{
type
:
"
GET
"
})
.
push
(
function
(
entry_list
)
{
// Linshare only allow to get the full list of documents
// First, sort the entries by modificationDate in order to
// drop the 'old' entries with the same 'name'
// (as linshare does not to update an existing doc)
entry_list
.
sort
(
sortByModificationDate
);
// Only return one document per name
// Keep the newer document
var
entry_dict
=
{},
i
,
len
=
entry_list
.
length
,
entry_name
,
entry
,
result_list
=
[];
for
(
i
=
0
;
i
<
len
;
i
+=
1
)
{
entry_name
=
entry_list
[
i
].
name
;
// If we only need one precise name, no need to check the others
if
(
!
options
.
hasOwnProperty
(
'
only_id
'
)
||
(
options
.
only_id
===
entry_name
))
{
if
(
!
entry_dict
.
hasOwnProperty
(
entry_name
))
{
entry
=
{
id
:
entry_name
,
value
:
{},
_linshare_uuid
:
entry_list
[
i
].
uuid
};
if
(
options
.
include_docs
===
true
)
{
entry
.
doc
=
JSON
.
parse
(
entry_list
[
i
].
metaData
)
||
{};
}
result_list
.
push
(
entry
);
if
(
options
.
all_revision
!==
true
)
{
// If we only want to fetch 'one revision',
// ie, the latest document matching this id
entry_dict
[
entry_name
]
=
null
;
if
(
options
.
only_id
===
entry_name
)
{
// Document has been found, no need to check all the others
break
;
}
}
}
}
}
return
result_list
;
});
}
LinshareStorage
.
prototype
.
buildQuery
=
function
(
options
)
{
return
getDocumentList
(
this
,
{
include_docs
:
options
.
include_docs
});
};
LinshareStorage
.
prototype
.
get
=
function
(
id
)
{
// It is not possible to get a document by its name
// The only way is to list all of them, and find it manually
return
getDocumentList
(
this
,
{
include_docs
:
true
,
only_id
:
id
})
.
push
(
function
(
result_list
)
{
if
(
result_list
.
length
===
1
)
{
return
result_list
[
0
].
doc
;
}
throw
new
jIO
.
util
.
jIOError
(
"
Can't find document with id :
"
+
id
,
404
);
});
};
function
createLinshareDocument
(
storage
,
id
,
doc
,
blob
)
{
var
data
=
new
FormData
();
data
.
append
(
'
file
'
,
blob
,
id
);
data
.
append
(
'
filesize
'
,
blob
.
size
);
data
.
append
(
'
filename
'
,
id
);
data
.
append
(
'
description
'
,
doc
.
title
||
doc
.
description
||
''
);
data
.
append
(
'
metadata
'
,
jIO
.
util
.
stringify
(
doc
));
return
makeRequest
(
storage
,
''
,
{
type
:
'
POST
'
,
data
:
data
});
}
LinshareStorage
.
prototype
.
put
=
function
(
id
,
doc
)
{
var
storage
=
this
;
return
getDocumentList
(
storage
,
{
include_docs
:
true
,
only_id
:
id
})
.
push
(
function
(
result_list
)
{
if
(
result_list
.
length
===
1
)
{
// Update existing document metadata
var
data
=
{
uuid
:
result_list
[
0
].
_linshare_uuid
,
metaData
:
jIO
.
util
.
stringify
(
doc
),
name
:
id
,
description
:
doc
.
title
||
doc
.
description
||
''
};
return
makeRequest
(
storage
,
result_list
[
0
].
_linshare_uuid
,
{
type
:
'
PUT
'
,
headers
:
{
'
Content-Type
'
:
'
application/json
'
},
data
:
jIO
.
util
.
stringify
(
data
)
});
}
// Create a new one
return
createLinshareDocument
(
storage
,
id
,
doc
,
new
Blob
());
});
};
LinshareStorage
.
prototype
.
remove
=
function
(
id
)
{
var
storage
=
this
;
// Delete all entries matching the id
return
getDocumentList
(
storage
,
{
only_id
:
id
,
all_revision
:
true
})
.
push
(
function
(
result_list
)
{
var
promise_list
=
[],
i
,
len
=
result_list
.
length
;
for
(
i
=
0
;
i
<
len
;
i
+=
1
)
{
promise_list
.
push
(
makeRequest
(
storage
,
result_list
[
i
].
_linshare_uuid
,
{
type
:
"
DELETE
"
})
);
}
return
RSVP
.
all
(
promise_list
);
});
};
LinshareStorage
.
prototype
.
allAttachments
=
function
(
id
)
{
return
this
.
get
(
id
)
.
push
(
function
()
{
return
{
enclosure
:
{}};
});
};
function
restrictAttachmentId
(
name
)
{
if
(
name
!==
"
enclosure
"
)
{
throw
new
jIO
.
util
.
jIOError
(
"
attachment name
"
+
name
+
"
is forbidden in linshare
"
,
400
);
}
}
LinshareStorage
.
prototype
.
putAttachment
=
function
(
id
,
name
,
blob
)
{
restrictAttachmentId
(
name
);
var
storage
=
this
;
return
storage
.
get
(
id
)
.
push
(
function
(
doc
)
{
// Create a new document with the same id but a different blob content
return
createLinshareDocument
(
storage
,
id
,
doc
,
blob
);
});
};
LinshareStorage
.
prototype
.
getAttachment
=
function
(
id
,
name
)
{
restrictAttachmentId
(
name
);
var
storage
=
this
;
// It is not possible to get a document by its name
// The only way is to list all of them, and find it manually
return
getDocumentList
(
storage
,
{
only_id
:
id
})
.
push
(
function
(
result_list
)
{
if
(
result_list
.
length
===
1
)
{
return
makeRequest
(
storage
,
result_list
[
0
].
_linshare_uuid
,
{
},
true
);
}
throw
new
jIO
.
util
.
jIOError
(
"
Can't find document with id :
"
+
id
,
404
);
});
};
jIO
.
addStorage
(
'
linshare
'
,
LinshareStorage
);
}(
jIO
,
RSVP
,
UriTemplate
,
FormData
,
Blob
));
/*
* Copyright 2013, Nexedi SA
*
...
...
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