diff --git a/WordPress/Classes/Networking/Remote Objects/RemoteTaxonomyPaging.h b/WordPress/Classes/Networking/Remote Objects/RemoteTaxonomyPaging.h new file mode 100644 index 000000000000..8b216f81ce50 --- /dev/null +++ b/WordPress/Classes/Networking/Remote Objects/RemoteTaxonomyPaging.h @@ -0,0 +1,53 @@ +#import + +typedef NS_ENUM(NSUInteger, RemoteTaxonomyPagingResultsOrder) { + RemoteTaxonomyPagingOrderAscending = 0, + RemoteTaxonomyPagingOrderDescending +}; + +typedef NS_ENUM(NSUInteger, RemoteTaxonomyPagingResultsOrdering) { + /* Order the results by the name of the taxonomy. + */ + RemoteTaxonomyPagingResultsOrderingByName = 0, + /* Order the results by the number of posts associated with the taxonomy. + */ + RemoteTaxonomyPagingResultsOrderingByCount +}; + + +/** + @class RemoteTaxonomyPaging + @brief A paging object for passing parameters to the API when requesting paged lists of taxonomies. + See each remote API for specifics regarding default values and limits. + WP.com/REST Jetpack: https://developer.wordpress.com/docs/api/1.1/get/sites/%24site/categories/ + XML-RPC: https://codex.wordpress.org/XML-RPC_WordPress_API/Taxonomies + */ +@interface RemoteTaxonomyPaging : NSObject + +/** + @brief The max number of taxonomies to return. + */ +@property (nonatomic, strong) NSNumber *number; + +/** + @brief 0-indexed offset for paging. + */ +@property (nonatomic, strong) NSNumber *offset; + +/** + @brief Return the Nth 1-indexed page of tags. Takes precedence over the offset parameter. + @attention Not supported in XML-RPC. + */ +@property (nonatomic, strong) NSNumber *page; + +/** + @brief Return the taxonomies in ascending or descending order. Defaults YES via the API. + */ +@property (nonatomic, assign) RemoteTaxonomyPagingResultsOrder order; + +/** + @brief Return the taxonomies ordering by name or associated count. + */ +@property (nonatomic, assign) RemoteTaxonomyPagingResultsOrdering orderBy; + +@end diff --git a/WordPress/Classes/Networking/Remote Objects/RemoteTaxonomyPaging.m b/WordPress/Classes/Networking/Remote Objects/RemoteTaxonomyPaging.m new file mode 100644 index 000000000000..b795c5f7319f --- /dev/null +++ b/WordPress/Classes/Networking/Remote Objects/RemoteTaxonomyPaging.m @@ -0,0 +1,5 @@ +#import "RemoteTaxonomyPaging.h" + +@implementation RemoteTaxonomyPaging + +@end diff --git a/WordPress/Classes/Networking/TaxonomyServiceRemote.h b/WordPress/Classes/Networking/TaxonomyServiceRemote.h index 5bd5c782ff19..7d57eb3d0476 100644 --- a/WordPress/Classes/Networking/TaxonomyServiceRemote.h +++ b/WordPress/Classes/Networking/TaxonomyServiceRemote.h @@ -2,26 +2,52 @@ @class RemotePostCategory; @class RemotePostTag; +@class RemoteTaxonomyPaging; -/* - Interface for requesting taxonomy such as tags and categories on a site. +/* Interface for requesting taxonomy such as tags and categories on a site. */ @protocol TaxonomyServiceRemote +/* Create a new category with the site. + */ +- (void)createCategory:(RemotePostCategory *)category + success:(void (^)(RemotePostCategory *category))success + failure:(void (^)(NSError *error))failure; + /* Fetch a list of categories associated with the site. + * Note: Requests no paging parameters via the API defaulting the response. */ - (void)getCategoriesWithSuccess:(void (^)(NSArray *categories))success failure:(void (^)(NSError *error))failure; -/* Create a new category with the site. +/* Fetch a list of categories associated with the site with paging. */ -- (void)createCategory:(RemotePostCategory *)category - success:(void (^)(RemotePostCategory *category))success - failure:(void (^)(NSError *error))failure; +- (void)getCategoriesWithPaging:(RemoteTaxonomyPaging *)paging + success:(void (^)(NSArray *categories))success + failure:(void (^)(NSError *error))failure; + +/* Fetch a list of categories whose names or slugs match the provided search query. Case-insensitive. + */ +- (void)searchCategoriesWithName:(NSString *)nameQuery + success:(void (^)(NSArray *categories))success + failure:(void (^)(NSError *error))failure; /* Fetch a list of tags associated with the site. + * Note: Requests no paging parameters via the API defaulting the response. */ - (void)getTagsWithSuccess:(void (^)(NSArray *tags))success failure:(void (^)(NSError *error))failure; +/* Fetch a list of tags associated with the site with paging. + */ +- (void)getTagsWithPaging:(RemoteTaxonomyPaging *)paging + success:(void (^)(NSArray *tags))success + failure:(void (^)(NSError *error))failure; + +/* Fetch a list of tags whose names or slugs match the provided search query. Case-insensitive. + */ +- (void)searchTagsWithName:(NSString *)nameQuery + success:(void (^)(NSArray *tags))success + failure:(void (^)(NSError *error))failure; + @end \ No newline at end of file diff --git a/WordPress/Classes/Networking/TaxonomyServiceRemoteREST.m b/WordPress/Classes/Networking/TaxonomyServiceRemoteREST.m index f080dd515a6b..198df48f3137 100644 --- a/WordPress/Classes/Networking/TaxonomyServiceRemoteREST.m +++ b/WordPress/Classes/Networking/TaxonomyServiceRemoteREST.m @@ -2,76 +2,167 @@ #import "WordPressComApi.h" #import "RemotePostCategory.h" #import "RemotePostTag.h" +#import "RemoteTaxonomyPaging.h" + +static NSString * const TaxonomyRESTCategoryIdentifier = @"categories"; +static NSString * const TaxonomyRESTTagIdentifier = @"tags"; + +static NSString * const TaxonomyRESTIDParameter = @"ID"; +static NSString * const TaxonomyRESTNameParameter = @"name"; +static NSString * const TaxonomyRESTSlugParameter = @"slug"; +static NSString * const TaxonomyRESTParentParameter = @"parent"; +static NSString * const TaxonomyRESTSearchParameter = @"search"; +static NSString * const TaxonomyRESTOrderParameter = @"order"; +static NSString * const TaxonomyRESTOrderByParameter = @"order_by"; +static NSString * const TaxonomyRESTNumberParameter = @"number"; +static NSString * const TaxonomyRESTOffsetParameter = @"offset"; +static NSString * const TaxonomyRESTPageParameter = @"page"; @implementation TaxonomyServiceRemoteREST #pragma mark - categories -- (void)getCategoriesWithSuccess:(void (^)(NSArray *))success - failure:(void (^)(NSError *))failure -{ - NSString *path = [NSString stringWithFormat:@"sites/%@/categories?context=edit", self.siteID]; - NSString *requestUrl = [self pathForEndpoint:path - withVersion:ServiceRemoteRESTApiVersion_1_1]; - - [self.api GET:requestUrl - parameters:nil - success:^(AFHTTPRequestOperation *operation, id responseObject) { - if (success) { - success([self remoteCategoriesWithJSONArray:[responseObject arrayForKey:@"categories"]]); - } - } failure:^(AFHTTPRequestOperation *operation, NSError *error) { - if (failure) { - failure(error); - } - }]; -} - - (void)createCategory:(RemotePostCategory *)category success:(void (^)(RemotePostCategory *))success failure:(void (^)(NSError *))failure { NSParameterAssert(category.name.length > 0); - NSString *path = [NSString stringWithFormat:@"sites/%@/categories/new?context=edit", self.siteID]; - NSString *requestUrl = [self pathForEndpoint:path - withVersion:ServiceRemoteRESTApiVersion_1_1]; NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; - parameters[@"name"] = category.name; + parameters[TaxonomyRESTNameParameter] = category.name; if (category.parentID) { - parameters[@"parent"] = category.parentID; + parameters[TaxonomyRESTParentParameter] = category.parentID; } + + [self createTaxonomyWithType:TaxonomyRESTCategoryIdentifier + parameters:parameters + success:^(NSDictionary *taxonomyDictionary) { + RemotePostCategory *receivedCategory = [self remoteCategoryWithJSONDictionary:taxonomyDictionary]; + if (success) { + success(receivedCategory); + } + } failure:failure]; +} + +- (void)getCategoriesWithSuccess:(void (^)(NSArray *))success + failure:(void (^)(NSError *))failure +{ + [self getCategoriesWithPaging:nil + success:success + failure:failure]; +} + +- (void)getCategoriesWithPaging:(RemoteTaxonomyPaging *)paging + success:(void (^)(NSArray *categories))success + failure:(void (^)(NSError *error))failure +{ + [self getTaxonomyWithType:TaxonomyRESTCategoryIdentifier + parameters:[self parametersForPaging:paging] + success:^(NSDictionary *responseObject) { + if (success) { + success([self remoteCategoriesWithJSONArray:[responseObject arrayForKey:TaxonomyRESTCategoryIdentifier]]); + } + } failure:failure]; +} + +- (void)searchCategoriesWithName:(NSString *)nameQuery + success:(void (^)(NSArray *tags))success + failure:(void (^)(NSError *error))failure +{ + NSParameterAssert(nameQuery.length > 0); + [self getTaxonomyWithType:TaxonomyRESTCategoryIdentifier + parameters:@{TaxonomyRESTSearchParameter: nameQuery} + success:^(NSDictionary *responseObject) { + if (success) { + success([self remoteCategoriesWithJSONArray:[responseObject arrayForKey:TaxonomyRESTCategoryIdentifier]]); + } + } failure:failure]; +} + +#pragma mark - tags + +- (void)getTagsWithSuccess:(void (^)(NSArray *tags))success + failure:(void (^)(NSError *error))failure +{ + [self getTagsWithPaging:nil + success:success + failure:failure]; +} + +- (void)getTagsWithPaging:(RemoteTaxonomyPaging *)paging + success:(void (^)(NSArray *tags))success + failure:(void (^)(NSError *error))failure +{ + [self getTaxonomyWithType:TaxonomyRESTTagIdentifier + parameters:[self parametersForPaging:paging] + success:^(NSDictionary *responseObject) { + if (success) { + success([self remoteTagsWithJSONArray:[responseObject arrayForKey:TaxonomyRESTTagIdentifier]]); + } + } failure:failure]; +} + +- (void)searchTagsWithName:(NSString *)nameQuery + success:(void (^)(NSArray *tags))success + failure:(void (^)(NSError *error))failure +{ + NSParameterAssert(nameQuery.length > 0); + [self getTaxonomyWithType:TaxonomyRESTTagIdentifier + parameters:@{TaxonomyRESTSearchParameter: nameQuery} + success:^(NSDictionary *responseObject) { + if (success) { + success([self remoteTagsWithJSONArray:[responseObject arrayForKey:TaxonomyRESTTagIdentifier]]); + } + } failure:failure]; +} + +#pragma mark - default methods +- (void)createTaxonomyWithType:(NSString *)typeIdentifier + parameters:(NSDictionary *)parameters + success:(void (^)(NSDictionary *taxonomyDictionary))success + failure:(void (^)(NSError *error))failure +{ + NSString *path = [NSString stringWithFormat:@"sites/%@/%@/new?context=edit", self.siteID, typeIdentifier]; + NSString *requestUrl = [self pathForEndpoint:path withVersion:ServiceRemoteRESTApiVersion_1_1]; + [self.api POST:requestUrl parameters:parameters - success:^(AFHTTPRequestOperation *operation, id responseObject) { - RemotePostCategory *receivedCategory = [self remoteCategoryWithJSONDictionary:responseObject]; + success:^(AFHTTPRequestOperation * _Nonnull operation, id _Nonnull responseObject) { + NSAssert([responseObject isKindOfClass:[NSDictionary class]], @"responseObject should be a dictionary"); + if (![responseObject isKindOfClass:[NSDictionary class]]) { + responseObject = nil; + } if (success) { - success(receivedCategory); + success(responseObject); } - } failure:^(AFHTTPRequestOperation *operation, NSError *error) { + } failure:^(AFHTTPRequestOperation * _Nullable operation, NSError * _Nonnull error) { if (failure) { failure(error); } }]; } -#pragma mark - tags - -- (void)getTagsWithSuccess:(void (^)(NSArray *tags))success - failure:(void (^)(NSError *error))failure +- (void)getTaxonomyWithType:(NSString *)typeIdentifier + parameters:(id)parameters + success:(void (^)(NSDictionary *responseObject))success + failure:(void (^)(NSError *error))failure { - NSString *path = [NSString stringWithFormat:@"sites/%@/tags?context=edit", self.siteID]; + NSString *path = [NSString stringWithFormat:@"sites/%@/%@?context=edit", self.siteID, typeIdentifier]; NSString *requestUrl = [self pathForEndpoint:path withVersion:ServiceRemoteRESTApiVersion_1_1]; [self.api GET:requestUrl - parameters:nil - success:^(AFHTTPRequestOperation *operation, id responseObject) { + parameters:parameters + success:^(AFHTTPRequestOperation * _Nonnull operation, id _Nonnull responseObject) { + NSAssert([responseObject isKindOfClass:[NSDictionary class]], @"responseObject should be a dictionary"); + if (![responseObject isKindOfClass:[NSDictionary class]]) { + responseObject = nil; + } if (success) { - success([self remoteTagsWithJSONArray:[responseObject arrayForKey:@"tags"]]); + success(responseObject); } - } failure:^(AFHTTPRequestOperation *operation, NSError *error) { + } failure:^(AFHTTPRequestOperation * _Nullable operation, NSError * _Nonnull error) { if (failure) { failure(error); } @@ -89,10 +180,14 @@ - (void)getTagsWithSuccess:(void (^)(NSArray *tags))success - (RemotePostCategory *)remoteCategoryWithJSONDictionary:(NSDictionary *)jsonCategory { + if (!jsonCategory) { + return nil; + } + RemotePostCategory *category = [RemotePostCategory new]; - category.categoryID = [jsonCategory numberForKey:@"ID"]; - category.name = [jsonCategory stringForKey:@"name"]; - category.parentID = [jsonCategory numberForKey:@"parent"]; + category.categoryID = [jsonCategory numberForKey:TaxonomyRESTIDParameter]; + category.name = [jsonCategory stringForKey:TaxonomyRESTNameParameter]; + category.parentID = [jsonCategory numberForKey:TaxonomyRESTParentParameter]; return category; } @@ -105,11 +200,50 @@ - (RemotePostCategory *)remoteCategoryWithJSONDictionary:(NSDictionary *)jsonCat - (RemotePostTag *)remoteTagWithJSONDictionary:(NSDictionary *)jsonTag { + if (!jsonTag) { + return nil; + } + RemotePostTag *tag = [RemotePostTag new]; - tag.tagID = [jsonTag numberForKey:@"ID"]; - tag.name = [jsonTag stringForKey:@"name"]; - tag.slug = [jsonTag stringForKey:@"slug"]; + tag.tagID = [jsonTag numberForKey:TaxonomyRESTIDParameter]; + tag.name = [jsonTag stringForKey:TaxonomyRESTNameParameter]; + tag.slug = [jsonTag stringForKey:TaxonomyRESTSlugParameter]; return tag; } +- (NSDictionary *)parametersForPaging:(RemoteTaxonomyPaging *)paging +{ + if (!paging) { + return nil; + } + + NSMutableDictionary *dictionary = [NSMutableDictionary dictionary]; + + if (paging.number) { + [dictionary setObject:paging.number forKey:TaxonomyRESTNumberParameter]; + } + + if (paging.offset) { + [dictionary setObject:paging.offset forKey:TaxonomyRESTOffsetParameter]; + } + + if (paging.page) { + [dictionary setObject:paging.page forKey:TaxonomyRESTPageParameter]; + } + + if (paging.order == RemoteTaxonomyPagingOrderAscending) { + [dictionary setObject:@"ASC" forKey:TaxonomyRESTOrderParameter]; + } else if (paging.order == RemoteTaxonomyPagingOrderDescending) { + [dictionary setObject:@"DESC" forKey:TaxonomyRESTOrderParameter]; + } + + if (paging.orderBy == RemoteTaxonomyPagingResultsOrderingByName) { + [dictionary setObject:@"name" forKey:TaxonomyRESTOrderByParameter]; + } else if (paging.orderBy == RemoteTaxonomyPagingResultsOrderingByCount) { + [dictionary setObject:@"count" forKey:TaxonomyRESTOrderByParameter]; + } + + return dictionary.count ? dictionary : nil; +} + @end diff --git a/WordPress/Classes/Networking/TaxonomyServiceRemoteXMLRPC.m b/WordPress/Classes/Networking/TaxonomyServiceRemoteXMLRPC.m index 5ef6209f8460..811b2276775d 100644 --- a/WordPress/Classes/Networking/TaxonomyServiceRemoteXMLRPC.m +++ b/WordPress/Classes/Networking/TaxonomyServiceRemoteXMLRPC.m @@ -1,46 +1,143 @@ #import "TaxonomyServiceRemoteXMLRPC.h" #import "RemotePostCategory.h" #import "RemotePostTag.h" +#import "RemoteTaxonomyPaging.h" #import #import +static NSString * const TaxonomyXMLRPCCategoryIdentifier = @"category"; +static NSString * const TaxonomyXMLRPCTagIdentifier = @"post_tag"; + +static NSString * const TaxonomyXMLRPCIDParameter = @"term_id"; +static NSString * const TaxonomyXMLRPCSlugParameter = @"slug"; +static NSString * const TaxonomyXMLRPCNameParameter = @"name"; +static NSString * const TaxonomyXMLRPCParentParameter = @"parent"; +static NSString * const TaxonomyXMLRPCSearchParameter = @"search"; +static NSString * const TaxonomyXMLRPCOrderParameter = @"order"; +static NSString * const TaxonomyXMLRPCOrderByParameter = @"order_by"; +static NSString * const TaxonomyXMLRPCNumberParameter = @"number"; +static NSString * const TaxonomyXMLRPCOffsetParameter = @"offset"; + + @implementation TaxonomyServiceRemoteXMLRPC #pragma mark - categories +- (void)createCategory:(RemotePostCategory *)category + success:(void (^)(RemotePostCategory *))success + failure:(void (^)(NSError *))failure +{ + NSMutableDictionary *extraParameters = [NSMutableDictionary dictionary]; + [extraParameters setObject:category.name ?: [NSNull null] forKey:TaxonomyXMLRPCNameParameter]; + if ([category.parentID integerValue] > 0) { + [extraParameters setObject:category.parentID forKey:TaxonomyXMLRPCParentParameter]; + } + + [self createTaxonomyWithType:TaxonomyXMLRPCCategoryIdentifier + parameters:extraParameters + success:^(NSString *responseString) { + + RemotePostCategory *newCategory = [RemotePostCategory new]; + NSString *categoryID = responseString; + newCategory.categoryID = [categoryID numericValue]; + if (success) { + success(newCategory); + } + + } failure:failure]; +} + - (void)getCategoriesWithSuccess:(void (^)(NSArray *))success failure:(void (^)(NSError *))failure { - NSArray *parameters = [self XMLRPCArgumentsWithExtra:@"category"]; - [self.api callMethod:@"wp.getTerms" - parameters:parameters - success:^(AFHTTPRequestOperation *operation, id responseObject) { - NSAssert([responseObject isKindOfClass:[NSArray class]], @"Response should be an array."); - if (success) { - success([self remoteCategoriesFromXMLRPCArray:responseObject]); - } - } failure:^(AFHTTPRequestOperation *operation, NSError *error) { - if (failure) { - failure(error); - } - }]; + [self getCategoriesWithPaging:nil + success:success + failure:failure]; } -- (void)createCategory:(RemotePostCategory *)category - success:(void (^)(RemotePostCategory *))success - failure:(void (^)(NSError *))failure +- (void)getCategoriesWithPaging:(RemoteTaxonomyPaging *)paging + success:(void (^)(NSArray *categories))success + failure:(void (^)(NSError *error))failure +{ + [self getTaxonomiesWithType:TaxonomyXMLRPCCategoryIdentifier + parameters:[self parametersForPaging:paging] + success:^(NSArray *responseArray) { + if (success) { + success([self remoteCategoriesFromXMLRPCArray:responseArray]); + } + } failure:failure]; +} + +- (void)searchCategoriesWithName:(NSString *)nameQuery + success:(void (^)(NSArray *))success + failure:(void (^)(NSError *))failure +{ + NSDictionary *searchParameters = @{TaxonomyXMLRPCSearchParameter: nameQuery}; + [self getTaxonomiesWithType:TaxonomyXMLRPCCategoryIdentifier + parameters:searchParameters + success:^(NSArray *responseArray) { + if (success) { + success([self remoteCategoriesFromXMLRPCArray:responseArray]); + } + } failure:failure]; +} + +#pragma mark - tags + +- (void)getTagsWithSuccess:(void (^)(NSArray *))success + failure:(void (^)(NSError *))failure { - NSDictionary *extraParameters = @{ - @"name" : category.name ?: [NSNull null], - @"parent_id" : category.parentID ?: @0, - @"taxonomy" : @"category", - }; - NSArray *parameters = [self XMLRPCArgumentsWithExtra:extraParameters]; + [self getTagsWithPaging:nil + success:success + failure:failure]; +} +- (void)getTagsWithPaging:(RemoteTaxonomyPaging *)paging + success:(void (^)(NSArray *tags))success + failure:(void (^)(NSError *error))failure +{ + [self getTaxonomiesWithType:TaxonomyXMLRPCTagIdentifier + parameters:[self parametersForPaging:paging] + success:^(NSArray *responseArray) { + if (success) { + success([self remoteTagsFromXMLRPCArray:responseArray]); + } + } failure:failure]; +} +- (void)searchTagsWithName:(NSString *)nameQuery + success:(void (^)(NSArray *))success + failure:(void (^)(NSError *))failure +{ + NSDictionary *searchParameters = @{TaxonomyXMLRPCSearchParameter: nameQuery}; + [self getTaxonomiesWithType:TaxonomyXMLRPCTagIdentifier + parameters:searchParameters + success:^(NSArray *responseArray) { + if (success) { + success([self remoteTagsFromXMLRPCArray:responseArray]); + } + } failure:failure]; +} + +#pragma mark - default methods + +- (void)createTaxonomyWithType:(NSString *)typeIdentifier + parameters:(NSDictionary *)parameters + success:(void (^)(NSString *responseString))success + failure:(void (^)(NSError *error))failure +{ + NSMutableDictionary *mutableParametersDict = [NSMutableDictionary dictionaryWithDictionary:@{@"taxonomy": typeIdentifier}]; + NSArray *xmlrpcParameters = nil; + if (parameters.count) { + [mutableParametersDict addEntriesFromDictionary:parameters]; + } + + xmlrpcParameters = [self XMLRPCArgumentsWithExtra:mutableParametersDict]; + [self.api callMethod:@"wp.newTerm" - parameters:parameters + parameters:xmlrpcParameters success:^(AFHTTPRequestOperation *operation, id responseObject) { + NSAssert([responseObject isKindOfClass:[NSString class]], @"wp.newTerm response should be a string"); if (![responseObject respondsToSelector:@selector(numericValue)]) { NSString *errorMessage = @"Invalid response to wp.newTerm"; @@ -52,12 +149,10 @@ - (void)createCategory:(RemotePostCategory *)category } return; } - RemotePostCategory *newCategory = [RemotePostCategory new]; - NSString *categoryID = (NSString *)responseObject; - newCategory.categoryID = [categoryID numericValue]; if (success) { - success(newCategory); + success(responseObject); } + } failure:^(AFHTTPRequestOperation *operation, NSError *error) { if (failure) { failure(error); @@ -65,17 +160,26 @@ - (void)createCategory:(RemotePostCategory *)category }]; } -#pragma mark - tags - -- (void)getTagsWithSuccess:(void (^)(NSArray *))success failure:(void (^)(NSError *))failure +- (void)getTaxonomiesWithType:(NSString *)typeIdentifier + parameters:(NSDictionary *)parameters + success:(void (^)(NSArray *responseArray))success + failure:(void (^)(NSError *error))failure { - NSArray *parameters = [self XMLRPCArgumentsWithExtra:@"post_tag"]; + NSArray *xmlrpcParameters = nil; + if (parameters.count) { + xmlrpcParameters = [self XMLRPCArgumentsWithExtra:@[typeIdentifier, parameters]]; + }else { + xmlrpcParameters = [self XMLRPCArgumentsWithExtra:typeIdentifier]; + } [self.api callMethod:@"wp.getTerms" - parameters:parameters + parameters:xmlrpcParameters success:^(AFHTTPRequestOperation *operation, id responseObject) { NSAssert([responseObject isKindOfClass:[NSArray class]], @"Response should be an array."); + if (![responseObject isKindOfClass:[NSArray class]]) { + responseObject = nil; + } if (success) { - success([self remoteTagsFromXMLRPCArray:responseObject]); + success(responseObject); } } failure:^(AFHTTPRequestOperation *operation, NSError *error) { if (failure) { @@ -95,10 +199,14 @@ - (void)getTagsWithSuccess:(void (^)(NSArray *))success failure - (RemotePostCategory *)remoteCategoryFromXMLRPCDictionary:(NSDictionary *)xmlrpcDictionary { + if (!xmlrpcDictionary) { + return nil; + } + RemotePostCategory *category = [RemotePostCategory new]; - category.categoryID = [xmlrpcDictionary numberForKey:@"term_id"]; - category.name = [xmlrpcDictionary stringForKey:@"name"]; - category.parentID = [xmlrpcDictionary numberForKey:@"parent"]; + category.categoryID = [xmlrpcDictionary numberForKey:TaxonomyXMLRPCIDParameter]; + category.name = [xmlrpcDictionary stringForKey:TaxonomyXMLRPCNameParameter]; + category.parentID = [xmlrpcDictionary numberForKey:TaxonomyXMLRPCParentParameter]; return category; } @@ -111,11 +219,46 @@ - (RemotePostCategory *)remoteCategoryFromXMLRPCDictionary:(NSDictionary *)xmlrp - (RemotePostTag *)remoteTagFromXMLRPCDictionary:(NSDictionary *)xmlrpcDictionary { + if (!xmlrpcDictionary) { + return nil; + } + RemotePostTag *tag = [RemotePostTag new]; - tag.tagID = [xmlrpcDictionary numberForKey:@"term_id"]; - tag.name = [xmlrpcDictionary stringForKey:@"name"]; - tag.slug = [xmlrpcDictionary stringForKey:@"slug"]; + tag.tagID = [xmlrpcDictionary numberForKey:TaxonomyXMLRPCIDParameter]; + tag.name = [xmlrpcDictionary stringForKey:TaxonomyXMLRPCNumberParameter]; + tag.slug = [xmlrpcDictionary stringForKey:TaxonomyXMLRPCSlugParameter]; return tag; } +- (NSDictionary *)parametersForPaging:(RemoteTaxonomyPaging *)paging +{ + if (!paging) { + return nil; + } + + NSMutableDictionary *dictionary = [NSMutableDictionary dictionary]; + + if (paging.number) { + [dictionary setObject:paging.number forKey:TaxonomyXMLRPCNumberParameter]; + } + + if (paging.offset) { + [dictionary setObject:paging.offset forKey:TaxonomyXMLRPCOffsetParameter]; + } + + if (paging.order == RemoteTaxonomyPagingOrderAscending) { + [dictionary setObject:@"ASC" forKey:TaxonomyXMLRPCOrderParameter]; + } else if (paging.order == RemoteTaxonomyPagingOrderDescending) { + [dictionary setObject:@"DESC" forKey:TaxonomyXMLRPCOrderParameter]; + } + + if (paging.orderBy == RemoteTaxonomyPagingResultsOrderingByName) { + [dictionary setObject:@"name" forKey:TaxonomyXMLRPCOrderByParameter]; + } else if (paging.orderBy == RemoteTaxonomyPagingResultsOrderingByCount) { + [dictionary setObject:@"count" forKey:TaxonomyXMLRPCOrderByParameter]; + } + + return dictionary.count ? dictionary : nil; +} + @end diff --git a/WordPress/Classes/Services/PostCategoryService.h b/WordPress/Classes/Services/PostCategoryService.h index 555d681950c2..781d5b1a5635 100644 --- a/WordPress/Classes/Services/PostCategoryService.h +++ b/WordPress/Classes/Services/PostCategoryService.h @@ -19,6 +19,4 @@ forBlogObjectID:(NSManagedObjectID *)blogObjectID success:(void (^)(PostCategory *category))success failure:(void (^)(NSError *error))failure; - - @end diff --git a/WordPress/Classes/Services/PostCategoryService.m b/WordPress/Classes/Services/PostCategoryService.m index 74c15c0c6570..c247bdacc972 100644 --- a/WordPress/Classes/Services/PostCategoryService.m +++ b/WordPress/Classes/Services/PostCategoryService.m @@ -98,7 +98,13 @@ - (void)syncCategoriesForBlog:(Blog *)blog if (!blog) { return; } - [self mergeCategories:categories forBlog:blog completionHandler:success]; + [self mergeCategories:categories + forBlog:blog + completionHandler:^(NSArray *postCategories) { + if (success) { + success(); + } + }]; }]; } failure:failure]; } @@ -149,9 +155,9 @@ - (void)createCategoryWithName:(NSString *)name } failure:failure]; } -- (void)mergeCategories:(NSArray *)categories forBlog:(Blog *)blog completionHandler:(void (^)(void))completion +- (void)mergeCategories:(NSArray *)remoteCategories forBlog:(Blog *)blog completionHandler:(void (^)(NSArray *categories))completion { - NSSet *remoteSet = [NSSet setWithArray:[categories valueForKey:@"categoryID"]]; + NSSet *remoteSet = [NSSet setWithArray:[remoteCategories valueForKey:@"categoryID"]]; NSSet *localSet = [blog.categories valueForKey:@"categoryID"]; NSMutableSet *toDelete = [localSet mutableCopy]; [toDelete minusSet:remoteSet]; @@ -163,8 +169,10 @@ - (void)mergeCategories:(NSArray *)categories forBlog:(Blog *)blog completionHan } } } - - for (RemotePostCategory *remoteCategory in categories) { + + NSMutableArray *categories = [NSMutableArray arrayWithCapacity:remoteCategories.count]; + + for (RemotePostCategory *remoteCategory in remoteCategories) { PostCategory *category = [self findWithBlogObjectID:blog.objectID andCategoryID:remoteCategory.categoryID]; if (!category) { category = [self newCategoryForBlog:blog]; @@ -172,12 +180,14 @@ - (void)mergeCategories:(NSArray *)categories forBlog:(Blog *)blog completionHan } category.categoryName = remoteCategory.name; category.parentID = remoteCategory.parentID; + + [categories addObject:category]; } [[ContextManager sharedInstance] saveContext:self.managedObjectContext]; if (completion) { - completion(); + completion(categories); } } diff --git a/WordPress/Classes/Services/PostTagService.h b/WordPress/Classes/Services/PostTagService.h index 89206f90d325..0927a6b54645 100644 --- a/WordPress/Classes/Services/PostTagService.h +++ b/WordPress/Classes/Services/PostTagService.h @@ -1,13 +1,27 @@ #import "LocalCoreDataService.h" @class Blog; +@class PostTag; @interface PostTagService : LocalCoreDataService -/* Fetches the associated tags for blog and replaces all currently persisted PostTag entities for blog.tags +/* Sync an initial batch of tags for blog via default remote parameters and responses. */ - (void)syncTagsForBlog:(Blog *)blog - success:(void (^)())success - failure:(void (^)(NSError *error))failure; + success:(void (^)())success + failure:(void (^)(NSError *error))failure; + +/* Sync additional tags for blog via paging maintained within an instance of PostTagService. + */ +- (void)loadMoreTagsForBlog:(Blog *)blog + success:(void (^)(NSArray *tags))success + failure:(void (^)(NSError *error))failure; + +/* Search tags for blog matching a name or slug of the query. Case-insensitive search. + */ +- (void)searchTagsWithName:(NSString *)nameQuery + blog:(Blog *)blog + success:(void (^)(NSArray *tags))success + failure:(void (^)(NSError *error))failure; @end diff --git a/WordPress/Classes/Services/PostTagService.m b/WordPress/Classes/Services/PostTagService.m index fc4f639b2b21..d3985c75e80b 100644 --- a/WordPress/Classes/Services/PostTagService.m +++ b/WordPress/Classes/Services/PostTagService.m @@ -6,6 +6,22 @@ #import "TaxonomyServiceRemote.h" #import "TaxonomyServiceRemoteREST.h" #import "TaxonomyServiceRemoteXMLRPC.h" +#import "RemoteTaxonomyPaging.h" + +@interface PostTagService () + +@property (nonatomic, strong) RemoteTaxonomyPaging *remotePaging; + +@end + +static void logErrorForRetrievingBlog(Blog *blog, NSError *error) +{ + NSString *message = @"Could not retrieve blog from context"; + if (error) { + message = [NSString stringWithFormat:@"%@ with error: %@", message, error]; + } + DDLogError(message); +}; @implementation PostTagService @@ -14,18 +30,17 @@ - (void)syncTagsForBlog:(Blog *)blog failure:(void (^)(NSError *error))failure { id remote = [self remoteForBlog:blog]; - NSManagedObjectID *blogID = blog.objectID; + NSManagedObjectID *blogObjectID = blog.objectID; [remote getTagsWithSuccess:^(NSArray *remoteTags) { [self.managedObjectContext performBlock:^{ - - Blog *blog = (Blog *)[self.managedObjectContext existingObjectWithID:blogID error:nil]; - if (!blog) { + NSError *error; + Blog *blog = (Blog *)[self.managedObjectContext existingObjectWithID:blogObjectID error:&error]; + if (!blog || error) { + logErrorForRetrievingBlog(blog, error); return; } - NSArray *tags = [self tagsFromRemoteTags:remoteTags]; - blog.tags = [NSSet setWithArray:tags]; - + [self mergeTagsWithRemoteTags:remoteTags blog:blog]; [[ContextManager sharedInstance] saveContext:self.managedObjectContext]; if (success) { @@ -35,6 +50,70 @@ - (void)syncTagsForBlog:(Blog *)blog } failure:failure]; } +- (void)loadMoreTagsForBlog:(Blog *)blog + success:(void (^)(NSArray *tags))success + failure:(void (^)(NSError *error))failure +{ + RemoteTaxonomyPaging *paging = self.remotePaging; + if (!paging) { + paging = [[RemoteTaxonomyPaging alloc] init]; + paging.number = @(100); + // start the offset at 0 + paging.offset = @(0); + self.remotePaging = paging; + } + + id remote = [self remoteForBlog:blog]; + NSManagedObjectID *blogObjectID = blog.objectID; + [remote getTagsWithPaging:paging + success:^(NSArray *remoteTags) { + NSError *error; + Blog *blog = (Blog *)[self.managedObjectContext existingObjectWithID:blogObjectID error:&error]; + if (!blog || error) { + logErrorForRetrievingBlog(blog, error); + return; + } + + // increment the offset by the number of tags being requested for the next paging request + self.remotePaging.offset = @(self.remotePaging.offset.integerValue + self.remotePaging.number.integerValue); + + NSArray *tags = [self mergeTagsWithRemoteTags:remoteTags blog:blog]; + [[ContextManager sharedInstance] saveContext:self.managedObjectContext]; + + if (success) { + success(tags); + } + } failure:failure]; +} + +- (void)searchTagsWithName:(NSString *)nameQuery + blog:(Blog *)blog + success:(void (^)(NSArray *tags))success + failure:(void (^)(NSError *error))failure +{ + NSParameterAssert(nameQuery.length > 0); + id remote = [self remoteForBlog:blog]; + NSManagedObjectID *blogObjectID = blog.objectID; + [remote searchTagsWithName:nameQuery + success:^(NSArray *remoteTags) { + + NSError *error; + Blog *blog = (Blog *)[self.managedObjectContext existingObjectWithID:blogObjectID error:&error]; + if (!blog || error) { + logErrorForRetrievingBlog(blog, error); + return; + } + + NSArray *tags = [self mergeTagsWithRemoteTags:remoteTags blog:blog]; + [[ContextManager sharedInstance] saveContext:self.managedObjectContext]; + + if (success) { + success(tags); + } + + } failure:failure]; +} + #pragma mark - helpers - (id)remoteForBlog:(Blog *)blog { @@ -45,27 +124,49 @@ - (void)syncTagsForBlog:(Blog *)blog } } -- (NSArray *)tagsFromRemoteTags:(NSArray *)remoteTags +- (NSArray *)mergeTagsWithRemoteTags:(NSArray *)remoteTags blog:(Blog *)blog { + if (!remoteTags.count) { + return nil; + } + NSMutableArray *tags = [NSMutableArray arrayWithCapacity:remoteTags.count]; for (RemotePostTag *remoteTag in remoteTags) { - [tags addObject:[self tagFromRemoteTag:remoteTag]]; + [tags addObject:[self tagFromRemoteTag:remoteTag blog:blog]]; } return [NSArray arrayWithArray:tags]; } -- (PostTag *)tagFromRemoteTag:(RemotePostTag *)remoteTag +- (PostTag *)tagFromRemoteTag:(RemotePostTag *)remoteTag blog:(Blog *)blog { - NSEntityDescription *entityDescription = [NSEntityDescription entityForName:[PostTag entityName] - inManagedObjectContext:self.managedObjectContext]; + PostTag *tag = [self existingTagForRemoteTag:remoteTag blog:blog]; + if (!tag) { + NSEntityDescription *entityDescription = [NSEntityDescription entityForName:[PostTag entityName] + inManagedObjectContext:self.managedObjectContext]; + tag = [[PostTag alloc] initWithEntity:entityDescription insertIntoManagedObjectContext:self.managedObjectContext]; + tag.tagID = remoteTag.tagID; + tag.blog = blog; + } - PostTag *tag = [[PostTag alloc] initWithEntity:entityDescription insertIntoManagedObjectContext:self.managedObjectContext]; - tag.tagID = remoteTag.tagID; tag.name = remoteTag.name; tag.slug = remoteTag.slug; return tag; } +- (PostTag *)existingTagForRemoteTag:(RemotePostTag *)remoteTag blog:(Blog *)blog +{ + NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:[PostTag entityName]]; + request.predicate = [NSPredicate predicateWithFormat:@"blog = %@ AND tagID = %@", blog, remoteTag.tagID]; + NSError *error; + NSArray *tags = [self.managedObjectContext executeFetchRequest:request error:&error]; + if (error) { + DDLogError(@"Error when retrieving PostTag by tagID: %@", error); + return nil; + } + + return [tags firstObject]; +} + @end diff --git a/WordPress/WordPress.xcodeproj/project.pbxproj b/WordPress/WordPress.xcodeproj/project.pbxproj index b1af42509a2d..089a544f97a4 100644 --- a/WordPress/WordPress.xcodeproj/project.pbxproj +++ b/WordPress/WordPress.xcodeproj/project.pbxproj @@ -39,6 +39,7 @@ 082AB9D61C4EEA72000CA523 /* RemotePostTag.m in Sources */ = {isa = PBXBuildFile; fileRef = 082AB9D51C4EEA72000CA523 /* RemotePostTag.m */; }; 082AB9D91C4EEEF4000CA523 /* PostTagService.m in Sources */ = {isa = PBXBuildFile; fileRef = 082AB9D81C4EEEF4000CA523 /* PostTagService.m */; }; 082AB9DD1C4F035E000CA523 /* PostTag.m in Sources */ = {isa = PBXBuildFile; fileRef = 082AB9DC1C4F035E000CA523 /* PostTag.m */; }; + 08A6FD9C1C5960AB00AC33E4 /* RemoteTaxonomyPaging.m in Sources */ = {isa = PBXBuildFile; fileRef = 08A6FD9B1C5960AB00AC33E4 /* RemoteTaxonomyPaging.m */; }; 08CC677E1C49B65A00153AD7 /* MenuItem.m in Sources */ = {isa = PBXBuildFile; fileRef = 08CC67791C49B65A00153AD7 /* MenuItem.m */; }; 08CC677F1C49B65A00153AD7 /* Menu.m in Sources */ = {isa = PBXBuildFile; fileRef = 08CC677A1C49B65A00153AD7 /* Menu.m */; }; 08CC67801C49B65A00153AD7 /* MenuLocation.m in Sources */ = {isa = PBXBuildFile; fileRef = 08CC677D1C49B65A00153AD7 /* MenuLocation.m */; }; @@ -745,6 +746,8 @@ 082AB9D81C4EEEF4000CA523 /* PostTagService.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PostTagService.m; sourceTree = ""; }; 082AB9DB1C4F035E000CA523 /* PostTag.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PostTag.h; sourceTree = ""; }; 082AB9DC1C4F035E000CA523 /* PostTag.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PostTag.m; sourceTree = ""; }; + 08A6FD9A1C5960AB00AC33E4 /* RemoteTaxonomyPaging.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RemoteTaxonomyPaging.h; path = "Remote Objects/RemoteTaxonomyPaging.h"; sourceTree = ""; }; + 08A6FD9B1C5960AB00AC33E4 /* RemoteTaxonomyPaging.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RemoteTaxonomyPaging.m; path = "Remote Objects/RemoteTaxonomyPaging.m"; sourceTree = ""; }; 08CC67771C49B52E00153AD7 /* WordPress 45.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "WordPress 45.xcdatamodel"; sourceTree = ""; }; 08CC67781C49B65A00153AD7 /* MenuItem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MenuItem.h; sourceTree = ""; }; 08CC67791C49B65A00153AD7 /* MenuItem.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MenuItem.m; sourceTree = ""; }; @@ -3547,6 +3550,8 @@ E1A6DBD719DC7D080071AC1E /* RemotePostCategory.m */, 082AB9D41C4EEA72000CA523 /* RemotePostTag.h */, 082AB9D51C4EEA72000CA523 /* RemotePostTag.m */, + 08A6FD9A1C5960AB00AC33E4 /* RemoteTaxonomyPaging.h */, + 08A6FD9B1C5960AB00AC33E4 /* RemoteTaxonomyPaging.m */, E1A6DBD819DC7D080071AC1E /* RemotePost.h */, E1A6DBD919DC7D080071AC1E /* RemotePost.m */, E6374DC41C44550700F24720 /* RemotePublicizeConnection.swift */, @@ -4532,6 +4537,7 @@ B57273601B66CCEF000D1C4F /* AlertInternalView.swift in Sources */, 7059CD210F332B6500A0660B /* WPCategoryTree.m in Sources */, B5E167F419C08D18009535AA /* NSCalendar+Helpers.swift in Sources */, + 08A6FD9C1C5960AB00AC33E4 /* RemoteTaxonomyPaging.m in Sources */, BE1071FC1BC75E7400906AFF /* WPStyleGuide+Blog.swift in Sources */, E149D64E19349E69006A843D /* AccountServiceRemoteREST.m in Sources */, 5D9282F91B54697D00066CED /* RemoteReaderSiteInfo.m in Sources */,