mirror of
https://github.com/inverse-inc/sogo.git
synced 2026-05-29 23:25:27 +00:00
Monotone-Parent: d0eac9f630a7bec45962ad611bb2025a0a264075
Monotone-Revision: d4cbb070b0884fc3766c385032a8d95bc3f432a4 Monotone-Author: wsourdeau@inverse.ca Monotone-Date: 2009-10-08T13:04:00 Monotone-Branch: ca.inverse.sogo
This commit is contained in:
@@ -1,15 +1,37 @@
|
||||
2009-10-08 Wolfgang Sourdeau <wsourdeau@inverse.ca>
|
||||
|
||||
* Tests/webdavlib.py (MailDAVMailQuery._initSort): now handles the
|
||||
"order" attribute from the "ascending" parameter.
|
||||
|
||||
* Tests/test-maildav.py (DAVMailCollectionTest._testSort): added a
|
||||
boolean "ascending" parameter that defaults to True.
|
||||
(DAVMailCollectionTest.testREPORTMailQueryFilters): method
|
||||
replacing the filter part of testREPORTMailQuery.
|
||||
(DAVMailCollectionTest.testREPORTMailQuerySort): method
|
||||
replacing the sort part of testREPORTMailQuery.
|
||||
|
||||
* SoObjects/Mailer/SOGoMailFolder.m (_sortElementIsAscending:):
|
||||
new method that determines whether the sort ordering is ascending
|
||||
or not.
|
||||
(_sortOrderingsFromSortElement:): new method relacing
|
||||
"_mailSortingFromSortElement:", that returns an array of
|
||||
EOSortOrdering instances based on the "sort" element passed as
|
||||
parameter.
|
||||
|
||||
2009-10-07 Cyril Robert <crobert@inverse.ca>
|
||||
|
||||
* SoObjects/Mailer/EOQualifier+MailDAV.m (buildQualifierFromFilters:): Now
|
||||
returns an EOQualifier, fixed qualifier formats and constructor, refactored
|
||||
to regroup code.
|
||||
* Tests/test-maildav.py (testREPORTMailQuery): Disabled a few tests that
|
||||
don't pass for now.
|
||||
* SoObjects/Mailer/EOQualifier+MailDAV.m
|
||||
(buildQualifierFromFilters:): Now returns an EOQualifier, fixed
|
||||
qualifier formats and constructor, refactored to regroup code.
|
||||
* Tests/test-maildav.py (testREPORTMailQuery): Disabled a few
|
||||
tests that don't pass for now.
|
||||
* SoObjects/Mailer/EOQualifier+MailDAV.m (buildQualifierFromFilters:):
|
||||
Removed, everything is now done in (qualifierFromMailDAVMailFilters:).
|
||||
(qualifierFromMailDAVMailFilters:): Removed all parentheses in qualifier.
|
||||
* SoObjects/Mailer/EOQualifier+MailDAV.m (qualifierFromMailDAVMailFilters:):
|
||||
Fixed segfault when there are no filters.
|
||||
(qualifierFromMailDAVMailFilters:): Removed all parentheses in
|
||||
qualifier.
|
||||
* SoObjects/Mailer/EOQualifier+MailDAV.m
|
||||
(qualifierFromMailDAVMailFilters:): Fixed segfault when there are
|
||||
no filters.
|
||||
|
||||
2009-10-07 Francis Lachapelle <flachapelle@inverse.ca>
|
||||
|
||||
|
||||
@@ -38,6 +38,8 @@
|
||||
#import <DOM/DOMProtocols.h>
|
||||
#import <SaxObjC/XMLNamespaces.h>
|
||||
|
||||
#import <EOControl/EOSortOrdering.h>
|
||||
|
||||
#import <NGImap4/NGImap4Connection.h>
|
||||
#import <NGImap4/NGImap4Client.h>
|
||||
|
||||
@@ -1151,84 +1153,108 @@ static NSString *spoolFolder = nil;
|
||||
return davIMAPFieldsTable;
|
||||
}
|
||||
|
||||
- (NSDictionary *) _davIMAPFieldsForProperties: (NSArray *) properties
|
||||
- (BOOL) _sortElementIsAscending: (DOMElement *) sortElement
|
||||
{
|
||||
NSMutableDictionary *davIMAPFields;
|
||||
NSDictionary *davIMAPFieldsTable;
|
||||
NSString *imapField, *property;
|
||||
unsigned int count, max;
|
||||
|
||||
davIMAPFieldsTable = [self davIMAPFieldsTable];
|
||||
|
||||
max = [properties count];
|
||||
davIMAPFields = [NSMutableDictionary dictionaryWithCapacity: max];
|
||||
for (count = 0; count < max; count++)
|
||||
{
|
||||
property = [properties objectAtIndex: count];
|
||||
imapField = [davIMAPFieldsTable objectForKey: property];
|
||||
if (imapField)
|
||||
[davIMAPFields setObject: imapField forKey: property];
|
||||
else
|
||||
[self errorWithFormat: @"DAV property '%@' has no matching IMAP field,"
|
||||
@" response could be incomplete", property];
|
||||
}
|
||||
|
||||
return davIMAPFields;
|
||||
}
|
||||
|
||||
- (NSDictionary *) parseDAVRequestedProperties: (DOMElement *) propElement
|
||||
{
|
||||
NSArray *properties;
|
||||
NSDictionary *imapFieldsTable;
|
||||
|
||||
properties = [propElement flatPropertyNameOfSubElements];
|
||||
imapFieldsTable = [self _davIMAPFieldsForProperties: properties];
|
||||
|
||||
return imapFieldsTable;
|
||||
}
|
||||
|
||||
- (NSString *) _mailSortingFromSortElement: (DOMElement *) sortElement
|
||||
{
|
||||
NSArray *imapFields;
|
||||
NSString *davReverseAttr;
|
||||
NSMutableString *imapSortCriteria;
|
||||
BOOL orderIsAscending;
|
||||
|
||||
imapSortCriteria = [NSMutableString string];
|
||||
orderIsAscending = YES;
|
||||
|
||||
imapFields = [[self parseDAVRequestedProperties: sortElement] allValues];
|
||||
davReverseAttr = [[sortElement attribute: @"order"] uppercaseString];
|
||||
davReverseAttr = [sortElement attribute: @"order"];
|
||||
if ([davReverseAttr isEqualToString: @"descending"])
|
||||
[imapSortCriteria appendString: @"REVERSE "];
|
||||
orderIsAscending = NO;
|
||||
else if ([davReverseAttr length]
|
||||
&& ![davReverseAttr isEqualToString: @"ascending"])
|
||||
[self errorWithFormat: @"unrecognized sort order: '%@'",
|
||||
davReverseAttr];
|
||||
[imapSortCriteria
|
||||
appendString: [imapFields componentsJoinedByString: @" "]];
|
||||
|
||||
return imapSortCriteria;
|
||||
return orderIsAscending;
|
||||
}
|
||||
|
||||
- (NSArray *) _sortOrderingsFromSortElement: (DOMElement *) sortElement
|
||||
{
|
||||
NSArray *davSortCriterias;
|
||||
NSMutableArray *sortOrderings;
|
||||
SEL sortOrderingOrder;
|
||||
NSString *davSortVerb, *imapSortVerb;
|
||||
EOSortOrdering *currentOrdering;
|
||||
static NSMutableDictionary *criteriasMap = nil;
|
||||
int count, max;
|
||||
|
||||
if (!criteriasMap)
|
||||
{
|
||||
criteriasMap = [NSMutableDictionary new];
|
||||
[criteriasMap setObject: @"ARRIVAL"
|
||||
forKey: @"{urn:schemas:mailheader:}received"];
|
||||
[criteriasMap setObject: @"DATE"
|
||||
forKey: @"{urn:schemas:mailheader:}date"];
|
||||
[criteriasMap setObject: @"FROM"
|
||||
forKey: @"{urn:schemas:mailheader:}from"];
|
||||
[criteriasMap setObject: @"TO"
|
||||
forKey: @"{urn:schemas:mailheader:}to"];
|
||||
[criteriasMap setObject: @"CC"
|
||||
forKey: @"{urn:schemas:mailheader:}cc"];
|
||||
[criteriasMap setObject: @"SUBJECT"
|
||||
forKey: @"{DAV:}displayname"];
|
||||
[criteriasMap setObject: @"SUBJECT"
|
||||
forKey: @"{urn:schemas:mailheader:}subject"];
|
||||
[criteriasMap setObject: @"SIZE"
|
||||
forKey: @"{DAV:}getcontentlength"];
|
||||
}
|
||||
|
||||
sortOrderings = [NSMutableArray array];
|
||||
|
||||
if ([self _sortElementIsAscending: sortElement])
|
||||
sortOrderingOrder = EOCompareAscending;
|
||||
else
|
||||
sortOrderingOrder = EOCompareDescending;
|
||||
|
||||
davSortCriterias = [sortElement flatPropertyNameOfSubElements];
|
||||
max = [davSortCriterias count];
|
||||
for (count = 0; count < max; count++)
|
||||
{
|
||||
davSortVerb = [davSortCriterias objectAtIndex : count];
|
||||
imapSortVerb = [criteriasMap objectForKey: davSortVerb];
|
||||
if (imapSortVerb)
|
||||
{
|
||||
currentOrdering
|
||||
= [EOSortOrdering sortOrderingWithKey: imapSortVerb
|
||||
selector: sortOrderingOrder];
|
||||
[sortOrderings addObject: currentOrdering];
|
||||
}
|
||||
else
|
||||
[self errorWithFormat: @"unrecognized sort key: '%@'", davSortVerb];
|
||||
}
|
||||
|
||||
return sortOrderings;
|
||||
}
|
||||
|
||||
- (NSArray *) _fetchMessageProperties: (NSDictionary *) properties
|
||||
matchingQualifier: (EOQualifier *) searchQualifier
|
||||
andSorting: (NSString *) sorting
|
||||
andSortOrderings: (NSArray *) sortOrderings
|
||||
{
|
||||
NGImap4Client *client;
|
||||
NSDictionary *response;
|
||||
NSArray *messages;
|
||||
NSString *resultKey;
|
||||
|
||||
client = [[self imap4Connection] client];
|
||||
[imap4 selectFolder: [self imap4URL]];
|
||||
|
||||
if ([sorting length])
|
||||
response = [client sort: sorting
|
||||
qualifier: searchQualifier
|
||||
encoding: @"UTF-8"];
|
||||
if ([sortOrderings count])
|
||||
{
|
||||
response = [client sort: sortOrderings qualifier: searchQualifier
|
||||
encoding: @"UTF-8"];
|
||||
resultKey = @"sort";
|
||||
}
|
||||
else
|
||||
response = [client searchWithQualifier: searchQualifier];
|
||||
{
|
||||
response = [client searchWithQualifier: searchQualifier];
|
||||
resultKey = @"search";
|
||||
}
|
||||
|
||||
if ([[response objectForKey: @"result"] boolValue])
|
||||
messages = [response objectForKey: @"search"];
|
||||
messages = [response objectForKey: resultKey];
|
||||
else
|
||||
messages = nil;
|
||||
|
||||
@@ -1278,7 +1304,7 @@ static NSString *spoolFolder = nil;
|
||||
}
|
||||
|
||||
messageUrl = [NSString stringWithFormat: @"%@%@.eml",
|
||||
[self davURL], messageId];
|
||||
[self davURL], messageId];
|
||||
[propstats addObject: davElementWithContent (@"href", XMLNS_WEBDAV,
|
||||
messageUrl)];
|
||||
|
||||
@@ -1325,20 +1351,60 @@ static NSString *spoolFolder = nil;
|
||||
}
|
||||
|
||||
davString = [davElementWithContent (@"multistatus", XMLNS_WEBDAV, all)
|
||||
asWebDavStringWithNamespaces: nil];
|
||||
asWebDavStringWithNamespaces: nil];
|
||||
[response appendContentString: davString];
|
||||
NSZoneFree (NULL, selectors);
|
||||
}
|
||||
|
||||
- (NSDictionary *) _davIMAPFieldsForProperties: (NSArray *) properties
|
||||
{
|
||||
NSMutableDictionary *davIMAPFields;
|
||||
NSDictionary *davIMAPFieldsTable;
|
||||
NSString *imapField, *property;
|
||||
unsigned int count, max;
|
||||
|
||||
davIMAPFieldsTable = [self davIMAPFieldsTable];
|
||||
|
||||
max = [properties count];
|
||||
davIMAPFields = [NSMutableDictionary dictionaryWithCapacity: max];
|
||||
for (count = 0; count < max; count++)
|
||||
{
|
||||
property = [properties objectAtIndex: count];
|
||||
imapField = [davIMAPFieldsTable objectForKey: property];
|
||||
if (imapField)
|
||||
[davIMAPFields setObject: imapField forKey: property];
|
||||
else
|
||||
[self errorWithFormat: @"DAV property '%@' has no matching IMAP field,"
|
||||
@" response could be incomplete", property];
|
||||
}
|
||||
|
||||
return davIMAPFields;
|
||||
}
|
||||
|
||||
- (NSDictionary *) parseDAVRequestedProperties: (DOMElement *) propElement
|
||||
{
|
||||
NSArray *properties;
|
||||
NSDictionary *imapFieldsTable;
|
||||
|
||||
properties = [propElement flatPropertyNameOfSubElements];
|
||||
imapFieldsTable = [self _davIMAPFieldsForProperties: properties];
|
||||
|
||||
return imapFieldsTable;
|
||||
}
|
||||
|
||||
/* TODO:
|
||||
- populate only required keys in returned SOGoMailObject rather that
|
||||
fetching the whole envelope and stuff
|
||||
- use EOSortOrdering rather than an NSString
|
||||
*/
|
||||
- (id) davMailQuery: (id) queryContext
|
||||
{
|
||||
WOResponse *r;
|
||||
id <DOMDocument> document;
|
||||
DOMElement *documentElement, *propElement, *filterElement, *sortElement;
|
||||
NSDictionary *properties;
|
||||
NSArray *messages;
|
||||
NSArray *messages, *sortOrderings;
|
||||
EOQualifier *searchQualifier;
|
||||
NSString *sorting;
|
||||
|
||||
r = [context response];
|
||||
[r setContentEncoding: NSUTF8StringEncoding];
|
||||
@@ -1358,11 +1424,11 @@ static NSString *spoolFolder = nil;
|
||||
qualifierFromMailDAVMailFilters: filterElement];
|
||||
sortElement = [documentElement firstElementWithTag: @"sort"
|
||||
inNamespace: XMLNS_INVERSEDAV];
|
||||
sorting = [self _mailSortingFromSortElement: sortElement];
|
||||
sortOrderings = [self _sortOrderingsFromSortElement: sortElement];
|
||||
|
||||
messages = [self _fetchMessageProperties: properties
|
||||
matchingQualifier: searchQualifier
|
||||
andSorting: sorting];
|
||||
andSortOrderings: sortOrderings];
|
||||
[r setStatus: 207];
|
||||
[r appendContentString: @"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"];
|
||||
[self _appendProperties: [properties allKeys]
|
||||
|
||||
+52
-20
@@ -7,6 +7,9 @@ import unittest
|
||||
import webdavlib
|
||||
import time
|
||||
|
||||
# TODO
|
||||
# add test with multiple sort criterias
|
||||
|
||||
def fetchUserEmail(login):
|
||||
client = webdavlib.WebDAVClient(hostname, port,
|
||||
username, password)
|
||||
@@ -330,14 +333,14 @@ class DAVMailCollectionTest(unittest.TestCase):
|
||||
"filter %s:\n\tunexpected amount of refs: %d"
|
||||
% (filter[0], received_count))
|
||||
|
||||
def _testSort(self, sortOrder):
|
||||
def _testSort(self, sortOrder, ascending = True):
|
||||
expected_hrefs = sortOrder[1]
|
||||
expected_count = len(expected_hrefs)
|
||||
|
||||
received_count = 0
|
||||
url = "%sfolder%s" % (self.resource, "test-dav-mail")
|
||||
query = webdavlib.MailDAVMailQuery(url, ["displayname"],
|
||||
None, sortOrder[0])
|
||||
None, sortOrder[0], ascending)
|
||||
self.client.execute(query)
|
||||
self.assertEquals(query.response["status"], 207,
|
||||
"sortOrder %s:\n\tunexpected status: %d"
|
||||
@@ -348,18 +351,18 @@ class DAVMailCollectionTest(unittest.TestCase):
|
||||
for response_node in response_nodes:
|
||||
href_node = query.xpath_evaluate("D:href", response_node)[0]
|
||||
href = href_node.childNodes[0].nodeValue
|
||||
received_count = received_count + 1
|
||||
self.assertEquals(expected_hrefs[received_count], href,
|
||||
"sortOrder %s:\n\tunexpected href: %s (expecting: %s)"
|
||||
% (sortOrder[0], href,
|
||||
expected_hrefs[received_count]))
|
||||
received_count = received_count + 1
|
||||
|
||||
self.assertEquals(expected_count, received_count,
|
||||
"sortOrder %s:\n\tunexpected amount of refs: %d"
|
||||
% (sortOrder[0], received_count))
|
||||
|
||||
def testREPORTMailQuery(self):
|
||||
"""mail-query"""
|
||||
def testREPORTMailQueryFilters(self):
|
||||
"""mail-query filters"""
|
||||
self._deleteCollection("test-dav-mail")
|
||||
self._makeCollection("test-dav-mail")
|
||||
|
||||
@@ -549,6 +552,7 @@ class DAVMailCollectionTest(unittest.TestCase):
|
||||
"not": "true" }},
|
||||
[ msg2Loc, msg3Loc ]))
|
||||
|
||||
print "message flags are not handled yet"
|
||||
## 1. test filter: answered
|
||||
# ANSWERED, UNANSWERED
|
||||
## 1. test filter: draft
|
||||
@@ -579,33 +583,57 @@ class DAVMailCollectionTest(unittest.TestCase):
|
||||
[]))
|
||||
self._testFilters(filters)
|
||||
|
||||
self._deleteCollection("test-dav-mail")
|
||||
|
||||
def testREPORTMailQuerySort(self):
|
||||
"""mail-query sort"""
|
||||
self._deleteCollection("test-dav-mail")
|
||||
self._makeCollection("test-dav-mail")
|
||||
|
||||
msg1Loc = self._putMessage(self.client, "test-dav-mail", message1)
|
||||
parsed = webdavlib.HTTPUnparsedURL(msg1Loc)
|
||||
msg1Path = parsed.path
|
||||
msg2Loc = self._putMessage(self.client, "test-dav-mail", message2)
|
||||
parsed = webdavlib.HTTPUnparsedURL(msg2Loc)
|
||||
msg2Path = parsed.path
|
||||
msg3Loc = self._putMessage(self.client, "test-dav-mail", message3)
|
||||
parsed = webdavlib.HTTPUnparsedURL(msg3Loc)
|
||||
msg3Path = parsed.path
|
||||
|
||||
# 1. test sort: (receive-date) ARRIVAL
|
||||
self._testSort(([ "{urn:schemas:mailheader:}received" ],
|
||||
[ msg2Loc, msg3Loc, msg1Loc ]))
|
||||
[ msg1Loc, msg2Loc, msg3Loc ]))
|
||||
|
||||
# 1. test sort: (date) DATE
|
||||
self._testSort(([ "{urn:schemas:mailheader:}date" ],
|
||||
[ msg2Loc, msg1Loc, msg3Loc ]))
|
||||
|
||||
# 1. test sort: FROM
|
||||
self._testSort(([ "{urn:schemas:mailheader:}from" ],
|
||||
[ msg1Loc, msg2Loc, msg3Loc ]))
|
||||
|
||||
# 1. test sort: TO
|
||||
self._testSort(([ "{urn:schemas:mailheader:}to" ],
|
||||
[ msg1Loc, msg2Loc, msg3Loc ]))
|
||||
|
||||
# 1. test sort: CC
|
||||
self._testSort(([ "{urn:schemas:mailheader:}cc" ],
|
||||
[ msg3Loc, msg1Loc, msg2Loc ]))
|
||||
|
||||
# 1. test sort: FROM
|
||||
self._testSort(([ "{urn:schemas:mailheader:}from" ],
|
||||
[ msg1Loc, msg2Loc, msg3Loc ]))
|
||||
# 1. test sort: SUBJECT
|
||||
self._testSort(([ "{DAV:}displayname" ],
|
||||
[ msg3Loc, msg1Loc, msg2Loc ]))
|
||||
self._testSort(([ "{urn:schemas:mailheader:}subject" ],
|
||||
[ msg3Loc, msg1Loc, msg2Loc ]))
|
||||
|
||||
# 1. test sort: SIZE
|
||||
self._testSort(([ "{DAV:}getcontentlength" ],
|
||||
[ msg3Loc, msg1Loc, msg2Loc ]))
|
||||
|
||||
# 1. test sort: SUBJECT
|
||||
self._testSort(([ "{urn:schemas:mailheader:}displayName" ],
|
||||
[ msg3Loc, msg1Loc, msg2Loc ]))
|
||||
|
||||
# 1. test sort: TO
|
||||
self._testSort(([ "{urn:schemas:mailheader:}to" ],
|
||||
[ msg1Loc, msg2Loc, msg3Loc ]))
|
||||
# 1. test sort: REVERSE CC
|
||||
self._testSort(([ "{urn:schemas:mailheader:}cc" ],
|
||||
[ msg2Loc, msg1Loc, msg3Loc ]),
|
||||
False)
|
||||
|
||||
self._deleteCollection("test-dav-mail")
|
||||
|
||||
@@ -629,11 +657,15 @@ class DAVMailCollectionTest(unittest.TestCase):
|
||||
("{urn:schemas:httpmail:}textdescription",
|
||||
"<![CDATA[%s]]>" % message1, 0),
|
||||
("{urn:schemas:httpmail:}unreadcount", None, 0),
|
||||
("{urn:schemas:mailheader:}cc","2message1cc@cyril.dev, user10@cyril.dev", 0),
|
||||
("{urn:schemas:mailheader:}date", "Mon, 28 Sep 2009 11:42:14 GMT", 0),
|
||||
("{urn:schemas:mailheader:}from", "Cyril <message1from@cyril.dev>", 0),
|
||||
("{urn:schemas:mailheader:}cc",
|
||||
"2message1cc@cyril.dev, user10@cyril.dev", 0),
|
||||
("{urn:schemas:mailheader:}date",
|
||||
"Mon, 28 Sep 2009 11:42:14 GMT", 0),
|
||||
("{urn:schemas:mailheader:}from",
|
||||
"Cyril <message1from@cyril.dev>", 0),
|
||||
("{urn:schemas:mailheader:}in-reply-to", None, 0),
|
||||
("{urn:schemas:mailheader:}message-id","<4AC1F29sept6.5060801@cyril.dev>", 0),
|
||||
("{urn:schemas:mailheader:}message-id",
|
||||
"<4AC1F29sept6.5060801@cyril.dev>", 0),
|
||||
("{urn:schemas:mailheader:}received", message1_received, 0),
|
||||
("{urn:schemas:mailheader:}references",
|
||||
"<4AC3BF1B.3010806@inverse.ca>", 0),
|
||||
|
||||
+9
-4
@@ -315,7 +315,8 @@ class WebDAVSyncQuery(WebDAVREPORT):
|
||||
self._initProperties(properties)
|
||||
|
||||
class MailDAVMailQuery(WebDAVREPORT):
|
||||
def __init__(self, url, properties, filters = None, sort = None):
|
||||
def __init__(self, url, properties, filters = None,
|
||||
sort = None, ascending = True):
|
||||
WebDAVQuery.__init__(self, url)
|
||||
mailquery_tag = self.ns_mgr.register("mail-query",
|
||||
xmlns_inversedav)
|
||||
@@ -327,7 +328,7 @@ class MailDAVMailQuery(WebDAVREPORT):
|
||||
self._initFilters(filters)
|
||||
|
||||
if sort is not None and len(sort) > 0:
|
||||
self._initSort(sort)
|
||||
self._initSort(sort, ascending)
|
||||
|
||||
def _initFilters(self, filters):
|
||||
mailfilter_tag = self.ns_mgr.register("mail-filters",
|
||||
@@ -341,9 +342,13 @@ class MailDAVMailQuery(WebDAVREPORT):
|
||||
filters[filterk])
|
||||
mailfilter_node.append(filter_node)
|
||||
|
||||
def _initSort(self, sort):
|
||||
def _initSort(self, sort, ascending):
|
||||
sort_tag = self.ns_mgr.register("sort", xmlns_inversedav)
|
||||
sort_node = _WD_XMLTreeElement(sort_tag)
|
||||
if ascending:
|
||||
sort_attrs = { "order": "ascending" }
|
||||
else:
|
||||
sort_attrs = { "order": "descending" }
|
||||
sort_node = _WD_XMLTreeElement(sort_tag, sort_attrs)
|
||||
self.top_node.append(sort_node)
|
||||
|
||||
for item in sort:
|
||||
|
||||
Reference in New Issue
Block a user